docs(elmhurst): validate slice-5 wall fix against accredited Elmhurst worksheet (uprn 200004296092)

Built the cert in accredited Elmhurst RdSAP10-Online on the lodged register
inputs; downloaded Input Summary + SAP Worksheets (committed alongside).

Confirmed:
- Slice 5 DIRECTION validated: the externally-insulated solid-brick wall is
  billed insulated by both tools (Elmhurst U 0.48, engine U 0.29), far below
  the uninsulated 1.40 the pre-fix §5.8 gate produced.
- ROOF matches EXACTLY (U 0.30, 9.35 W/K).
- Engine 71.74 = lodged 71; Elmhurst current SAP 66 (its "unknown external
  insulation" default is more conservative + this build used a generic combi
  and no keyed wall thickness, so 66 is not a clean engine-comparison).

Follow-up lead (documented in ELMHURST_FINDINGS.md): the engine feeds the
UNMEASURED lodged wall_thickness (360 mm, measured=N) into the §5.6 formula,
whereas RdSAP §5.3 uses measured thickness / Table-3 default otherwise. The
party-wall "difference" is NOT a bug (engine 0.25 is spec-correct, Table 15
row 4). Also adds the build script scripts/hyde/build_200004296092.py.

No engine change in this commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jun-te Kim 2026-07-02 20:49:58 +00:00
parent 0e784cbb1c
commit 8e487a9bae
6 changed files with 1932 additions and 0 deletions

View file

@ -0,0 +1,70 @@
# Elmhurst validation — UPRN 200004296092 (corpus, slice-5 wall fix)
Built in accredited Elmhurst RdSAP10-Online on the lodged register inputs
(end-terrace house, solid brick + **external insulation, thickness "NI"**,
pitched 150 mm loft, solid ground floor, mains-gas combi, TFA 62).
Artefacts: `elmhurst_summary.pdf` (Input Summary), `elmhurst_worksheet.pdf`
(SAP Worksheets).
## Headline
- **Lodged SAP 71**; **engine 71.74** (post slice-5 §5.4 100 mm fix, was 64.35);
**accredited Elmhurst current SAP 66** (band D), potential 75.
- Slice 5 is **directionally validated**: the externally-insulated wall must be
billed insulated, not at the uninsulated 1.40 the §5.8 gate produced. Both
Elmhurst (0.48) and the engine (0.29) are far below 1.40.
## Fabric line-by-line (engine dive vs Elmhurst worksheet)
| Element | Engine | Elmhurst | Note |
|---|---|---|---|
| External wall | 21.1 W/K (U 0.29) | 30.7 W/K (U **0.48**, "SolidWallDensePlaster**200**") | see WALL below |
| Roof | 9.35 W/K (U 0.30) | 9.35 W/K (U 0.30) | **exact match** |
| Ground floor | 21.2 W/K | 22.7 W/K (U 0.73) | close |
| Party wall | **8.57 W/K** | **0.00 W/K** | see PARTY below |
| Windows | 7.96 W/K | ~same area (4.31 + 4×2.89 split) | ~ |
The wall (engine 9.6) and party wall (engine +8.6) differences nearly cancel,
so the two HTCs are close; the residual SAP gap (71.7 vs 66) is dominated by the
heating system — this build used a GENERIC Post-98 condensing combi, whereas the
engine uses the lodged PCDB 16137 (Ideal Logic combi 30). Not a clean 1e-4
comparison for that reason.
## Leads (both partly confounded by an imperfect build — see caveats)
1. **WALL — base thickness on an unmeasured wall.** The cert lodges
`wall_thickness 360` but `wall_thickness_measured = N`. Elmhurst modelled a
**~200 mm** base wall ("SolidWallDensePlaster200") — but note my build did
NOT enter a wall thickness, so Elmhurst used its own default; had I keyed
360 mm it might match the engine. The open question is genuine though:
RdSAP §5.3 uses the documentary/measured thickness, and when NOT measured
the engine perhaps should fall to the Table-3 default rather than trust the
unmeasured lodged 360 mm. WORTH A DEDICATED CHECK (build with the thickness
keyed both ways).
2. **PARTY WALL — NOT an engine gap.** Elmhurst rendered the party wall as
"Solid, U 0.00", but the engine's **0.25** is SPEC-CORRECT: RdSAP §5.10
Table 15 row 4 ("unable to determine, house") = 0.25. The cert lodges
`party_wall_construction = 0` (unable to determine), so 0.25 is right; the
Elmhurst 0.00 reflects how my build's party-wall dropdown resolved (a
"Solid" masonry party wall, Table 15 row 1 = 0), not the cert's "unknown".
Build artifact, not a bug.
## Build caveats (why the 71.7-vs-66 residual isn't cleanly attributable)
- Heating: built a GENERIC Post-98 condensing combi; the engine uses the lodged
PCDB 16137 (Ideal Logic combi 30) — different efficiency.
- Wall thickness: not keyed into Elmhurst (defaulted 200 mm) vs engine's 360 mm.
- Party wall: entered as a resolved type (0.00) vs the cert's "unable to
determine" (engine 0.25).
So this build's clean, confirmed results are: (a) the ROOF matches exactly
(U 0.30, 9.35 W/K), (b) slice 5's DIRECTION is validated (insulated wall, not
the uninsulated 1.40). The wall-thickness question (lead 1) is the one genuine
follow-up.
## Slice-5 calibration note (banked)
The accredited "External insulation, **Unknown** thickness" selection in Elmhurst
produced U 0.48 (≈ a 50 mm equivalent on its 200 mm base), NOT the 0.29 the
§5.4-literal "assume 100 mm" gives on the engine's 360 mm base. The 100 mm fix
nevertheless matches the LODGED corpus better (MAE 0.658 → 0.647, within held) —
lodged assessors typically knew the real (~100 mm) thickness the register then
flattens to "NI". Per the campaign rule (engine = lodged is the goal; Elmhurst
arbitrates only when lodged can't be explained from register data), the 100 mm
fix STANDS; the lodged-vs-Elmhurst gap here is explained by the register's "NI"
discarding the assessor's thickness knowledge. But the base-thickness and
party-wall leads above are genuine and independent of the insulation default.

View file

@ -0,0 +1,254 @@
Summary Information
Surveyor: P960-0001
Name: Richard Matthew Ratcliff Title: Mr. Tel Number: 07760 443 469
Survey Reference: 001431 My Reference: Khalim-test
Current SAP rating: D 66 Potential SAP rating: C 75 Emissions (t/year): 1.927 tonnes
Current EI rating: C 72 Potential EI rating: C 76 Fuel Bill: £1026
Property Details:
RdSAP version: RdSAP10
Reference Number: P960-0001-001431
My Reference: Khalim-test
Lodgement Required: No
Regs Region: England
EPC Language: English
UPRN:
Postcode: W6 9BF
Region: Thames Valley
House Name: 28 Distillery Wharf
House No: 28
Street: Distillery Wharf
Locality:
Town: London
County:
Property Tenure: Owner-occupied
Transaction Type: Marketed sale
Inspection Date: 25/06/2025
Process date: 02/07/2026
Check for the existence of No
an EPC:
Does an EPC exist at the No
point of carrying out this
energy assessment:
Reason why another energy
assessment needs to be
undertaken:
RdSAP Inputs
Property Description:
1.0 Property type: H House
E End-Terrace
2.0 Number of Storeys: 2
Habitable Rooms: 3
Heated Habitable Rooms: 3
3.0 Date Built: Main Property B 1900-1929
4.0 Dimensions:
Dimension type: Internal
Main Property
Floor Room Heat Loss Party Wall
Area Height Wall Perimeter Length
[m2] [m] [m] [m]
1st Floor: 31.16 2.40 15.97 6.79
Lowest Floor: 31.16 2.40 15.97 6.79 No
5.0 Conservatory:
Is there a conservatory? No
7.0 Walls:
© Elmhurst Energy Systems Limited Registered Office Unit 16, St Johns Business Park, Lutterworth, Leicestershire LE17 4HB
Summary Information
Main Property
Type SO Solid Brick
Insulation E External
Insulation Thickness Unknown
Conductivity Known No
Thermal Conductivity Lambda = 0.04 W/m*K
Wall Thickness Unknown No
Wall Thickness 260 mm
U-value Known No
Party Wall Type S Solid masonry / timber / system build
8.0 Roofs:
Main Property
Type PA Pitched (slates/tiles), access to loft
Insulation J Joists
Insulation Thickness 150 mm
U-value Known No
8.1 Rooms in Roof:
9.0 Floors:
Main Property
Location G Ground floor
Type S Solid
Insulation A As built
Default U-value 0.73
U-value Known No
10.0 Doors:
Total Number of Doors 2
Number of Insulated Doors 0
11.0 Windows:
Frame Frame Glazing Building U g Draught Permanent
W H Area Glazing Type Location Orient. Data-Source
Type Factor Gap Part value value Proofed Shutters
Double with unknown
4.31 1.00 4.31 PVC 0.70 12 mm Main External wall North Manufacturer 2.80 0.76 Yes None
install date
Double with unknown
2.89 1.00 2.89 PVC 0.70 12 mm Main External wall West Manufacturer 2.80 0.76 Yes None
install date
Double with unknown
2.89 1.00 2.89 PVC 0.70 12 mm Main External wall North Manufacturer 2.80 0.76 Yes None
install date
Double with unknown
2.89 1.00 2.89 PVC 0.70 12 mm Main External wall East Manufacturer 2.80 0.76 Yes None
install date
Draught Proofing 67 %
12.0 Ventilation & Cooling
No. of open chimneys 0
No. of open flues 0
No. of open chimneys/open flues attached to closed fire 0
No. of flues attached to solid fuel boiler 0
No. of open flues attached to other heater 0
No. of blocked chimneys 0
No. of intermittent extract fans 0
No. of passive vents 0
No. of flueless gas fires 0
Fixed Space Cooling No
Draught Lobby Unable to determine
© Elmhurst Energy Systems Limited Registered Office Unit 16, St Johns Business Park, Lutterworth, Leicestershire LE17 4HB
Summary Information
12.1 Mechanical Ventilation
Mechanical Ventilation No
12.2 Air Pressure Test
Test Method Not available
13.0 Lighting
Total number of bulbs 6
Number of LED and CFL Known Yes
Number of LED lights 0
Number of CFL lights 0
Total number of Low Energy 0
Total number of incandescents 6
14.0 Main Heating1
PCDF boiler Reference 0
Main Heating EES Code BGW
Main Heating SAP Code 104
Heat Emitter Radiators
Heat pump age 2013 or later
Flue Type Balanced
Fan Assisted Flue Yes
Design flow temperature <= 35°C
PCDF Heating Controls 0
Main Heating Controls EES CBE
Main Heating Controls Sap SAP code 2106, Programmer, room thermostat and TRVs
PCDF Compensator 0
Percentage of Heat 100 %
14.1 Main Heating2
PCDF boiler Reference 0
Main Heating EES Code
Main Heating SAP Code 0
Percentage of Heat 0%
14.1 Community Heating/Heat Network
Heating Type None
14.2 Meters
Electricity meter type Single
Main gas Yes
Electricity Smart Meter Present No
Gas Smart Meter Present No
15.0 Water Heating
Water Heating Code HWP
Water Heating SapCode 901
Water Heating Fuel Type Mains gas
15.1 Hot Water Cylinder
Hot Water Cylinder Present No
15.2 Community Hot Water
PCDF boiler Reference 0
16.0 Solar water heating
Solar Water Heating No
17.0 Waste Water Heat Recovery System
Is WWHRS present in the property? No / Unknown
1x.0 Baths and Showers
Total Number of Baths 2
Number of Baths Connected 0
Description Type Connected
1 Non-electric shower None
© Elmhurst Energy Systems Limited Registered Office Unit 16, St Johns Business Park, Lutterworth, Leicestershire LE17 4HB
Summary Information
18.0 Flue Gas Heat Recovery System
Present No
19.0 Photovoltaic Panel
Photovoltaic Panel None
Export capable meter No
20.0 Wind Turbine
Terrain Type Urban
Wind turbine present? No
22.0 Special Features
21.0 Small-Scale Hydro
Electricity generated [kWh/year] 0.00
© Elmhurst Energy Systems Limited Registered Office Unit 16, St Johns Business Park, Lutterworth, Leicestershire LE17 4HB
Summary Information
Recommendations
Loft insulation (SAP increase too small)
Flat roof insulation (Not applicable)
Room-in-roof insulation (Not applicable)
Cavity wall insulation (Not applicable)
Solid wall insulation (SAP increase too small)
Floor insulation (solid floor) (Recommended)
Hot water cylinder insulation (Not applicable)
Draught proofing (SAP increase too small)
Low energy lighting (Recommended)
Cylinder thermostat (Not applicable)
Heating controls for wet central heating system (Already installed)
Upgrade boiler, same fuel (Already installed)
Change heating to condensing gas condensing boiler (fuel switch) (Not applicable)
Flue gas heat recovery in conjunction with new boiler (Not applicable)
Solar water heating (SAP increase too small)
Heat recovery system for mixer showers (SAP increase too small)
Double glazed windows (Already installed)
Insulated doors (SAP increase too small)
Solar photovoltaic panels (Recommended)
Wind turbine (Not applicable)
PV diverter (Not applicable)
PV battery (Not applicable)
Water heating controls (Not applicable)
Alternative Recommendations
External wall insulation with cavity insulation (Not applicable)
Biomass boiler (alternative) (Not applicable)
Micro CHP (alternative) (Not applicable)
Related Party Disclosure
No related party
Addenda
When considering the PV installation consider installing PV battery and a PV diverter for water heating
© Elmhurst Energy Systems Limited Registered Office Unit 16, St Johns Business Park, Lutterworth, Leicestershire LE17 4HB

View file

@ -0,0 +1,297 @@
"""Elmhurst build for UPRN 200004296092 (RdSAP-Schema-21.0.1, END-TERRACE HOUSE,
2-storey, band B, SOLID BRICK 360 mm WITH EXTERNAL INSULATION (thickness lodged
"NI" = unknown), pitched 150 mm loft, solid uninsulated ground floor, mains-gas
COMBI (PCDB 16137 Ideal Logic combi 30 ~ generic Post-98 condensing combi),
control 2106 (CBE), double glazed ~4.31 , 2 doors, TFA ~62, party wall 6.79 m.
Engine 71.74 = lodged 71 AFTER the §5.4 "assume 100 mm if unknown" wall fix
(slice 5); this build validates that against the accredited Elmhurst worksheet.
The KEY page is `walls`: it prints Elmhurst's insulation dropdown so we can see
how the accredited tool handles External insulation of unknown thickness. Run:
DISPLAY=:99 python scripts/hyde/build_200004296092.py <page>
"""
from __future__ import annotations
import sys
from pathlib import Path
import elmhurst_lib as E
DIM = "TabContainer_TabPanelMain_WebUserControlDimensionsMain_"
WALL = ("TabContainer_TabPanelMain_InnerTabContainerMain_"
"TabPanelExternalWallMain_WebUserControlWallMain_")
PWALL = ("TabContainer_TabPanelMain_InnerTabContainerMain_"
"TabPanelPartyWallMain_WebUserControlPartyWallMain_")
ROOF = "TabContainer_TabPanelMain_WebUserControlRoofMain_"
FLOOR = "TabContainer_TabPanelMain_WebUserControlFloorsMain_"
WP = "TabContainer_TabPanelWindowsPanel_"
DP = "TabContainer_TabPanelDoorsPanel_"
VP = "TabContainer_TabPanelVentilationPanel_"
APT = "TabContainer_TabPanelAirPressureTest_"
LP = "TabContainer_TabPanelLighting_"
MV = "TabContainer_TabPanelMechVent_"
WH = "TabContainer_TabPanelWaterHeating_"
MH1B = "TabContainer_TabPanelMainHeating1_WebUserControlMainHeating1_"
_SAMPLE_DIR = (Path(__file__).parent.parent.parent
/ "backend/epc_api/json_samples/real_life_examples"
/ "RdSAP-Schema-21.0.1/uprn_200004296092")
def _pick(page, suffix, contains):
val = page.evaluate(
"""(a)=>{const s=document.getElementById(a[0]);if(!s)return null;
for(const o of s.options){if(o.text.toLowerCase().includes(a[1].toLowerCase()))return o.value;}return null;}""",
[f"{E.FP}{suffix}", contains])
if val is not None:
E.set_select(page, suffix, val)
return val
def _options(page, suffix):
return page.evaluate(
"""(id)=>{const s=document.getElementById(id);if(!s)return [];
return Array.from(s.options).map(o=>o.text);}""", f"{E.FP}{suffix}")
def property_description(page):
E.goto(page, "PropertyDescription", "WebFormPropertyDescription.aspx")
E.set_select(page, "DropDownListPropertyType1", "H House")
print("built_form ->", _pick(page, "DropDownListPropertyType2", "end-terrace")
or _pick(page, "DropDownListPropertyType2", "end terrace"))
E.set_text(page, "TextBoxStoreys", "2")
E.set_text(page, "TextBoxHabitableRooms", "3")
E.set_text(page, "TextBoxHeatedHabitableRooms", "3")
print("date ->", _pick(page, "DropDownListDateBuiltMain", "1900-1929")
or _pick(page, "DropDownListDateBuiltMain", "1900"))
E.set_select(page, "DropDownListDateBuiltFirst", "")
E.set_select(page, "DropDownListRoomInRoofMain", "")
E.save_close(page)
def dimensions(page):
E.goto(page, "Dimensions", "WebFormDimensions.aspx")
E.set_text(page, f"{DIM}TextBoxFloorAreaLowestFloor", "31.16")
E.set_text(page, f"{DIM}TextBoxRoomHeightLowestFloor", "2.40")
E.set_text(page, f"{DIM}TextBoxWallPerimeterLowestFloor", "15.97")
E.set_text(page, f"{DIM}TextBoxPartyWallLengthLowestFloor", "6.79")
E.set_text(page, f"{DIM}TextBoxFloorArea1stFloor", "31.16")
E.set_text(page, f"{DIM}TextBoxRoomHeight1stFloor", "2.40")
E.set_text(page, f"{DIM}TextBoxWallPerimeter1stFloor", "15.97")
E.set_text(page, f"{DIM}TextBoxPartyWallLength1stFloor", "6.79")
E.save_close(page)
def walls(page):
E.goto(page, "Walls", "WebFormWalls.aspx")
print("WALL TYPE options:", _options(page, f"{WALL}DropDownListType"))
_pick(page, f"{WALL}DropDownListType", "solid brick") \
or _pick(page, f"{WALL}DropDownListType", "brick")
page.wait_for_timeout(600)
print("INSULATION options:", _options(page, f"{WALL}DropDownListInsulation"))
ins = (_pick(page, f"{WALL}DropDownListInsulation", "external")
or _pick(page, f"{WALL}DropDownListInsulation", "ext"))
print("insulation ->", ins)
page.wait_for_timeout(600)
# If a thickness dropdown appears for external insulation, show it (this is
# the slice-5 question: what does Elmhurst do with UNKNOWN thickness?).
for suf in (f"{WALL}DropDownListInsulationThickness",
f"{WALL}DropDownListExternalInsulationThickness"):
if page.locator(f"#{E.FP}{suf}").count():
print("INSULATION THICKNESS options:", _options(page, suf),
"current:", page.locator(f"#{E.FP}{suf}").input_value())
print("party wall ->",
_pick(page, f"{PWALL}DropDownListPartyWallType", "determine")
or _pick(page, f"{PWALL}DropDownListPartyWallType", "masonry"))
E.save_close(page)
def roofs(page):
E.goto(page, "Roofs", "WebFormRoofs.aspx")
_pick(page, f"{ROOF}DropDownListType", "access to loft") \
or _pick(page, f"{ROOF}DropDownListType", "loft")
_pick(page, f"{ROOF}DropDownListInsulationAt", "joists")
E.set_select(page, f"{ROOF}DropDownListThickness", "150 mm")
E.save_close(page)
def floors(page):
E.goto(page, "Floors", "WebFormFloors.aspx")
E.set_select(page, f"{FLOOR}DropDownListLocation", "G Ground floor")
_pick(page, f"{FLOOR}DropDownListType", "solid")
E.set_select(page, f"{FLOOR}DropDownListInsulation", "A As built")
E.save_close(page)
def openings(page):
E.goto(page, "Openings", "WebFormOpenings.aspx")
E.click_tab(page, "TabContainer_TabPanelWindowsPanel")
_add_window(page, 4.31, "North", _glazing(page))
_delete_zero_rows(page)
E.click_tab(page, "TabContainer_TabPanelDoorsPanel")
E.set_text(page, f"{DP}TextBoxDoors", "2")
E.set_text(page, f"{DP}TextBoxDoorsInsulated", "0")
E.set_text(page, f"{DP}TextBoxDraughtProofedDoors", "0")
E.save_close(page)
def _glazing(page):
for needle in ("unknown install date", "before 2002", "pre 2002"):
for opt in _options(page, f"{WP}DropDownListExtGlazing"):
low = opt.lower()
if needle in low and "triple" not in low and "single" not in low and "known data" not in low:
return opt
return "Double post or during 2022"
def _add_window(page, area, orientation, glazing):
print("glazing ->", glazing)
E.set_select(page, f"{WP}DropDownListExtGlazing", glazing)
page.wait_for_timeout(400)
ft = page.locator(f"#{E.FP}{WP}DropDownListExtFrameType")
if ft.count():
ft.select_option("PVC")
gg = page.locator(f"#{E.FP}{WP}DropDownListExtGlazingGap")
if gg.count():
gg.select_option("12 mm")
wid = f"{E.FP}{WP}TextBoxExtWidth"
page.evaluate(
"""(a)=>{const e=document.getElementById(a[0]);if(e){e.value=a[1];
e.dispatchEvent(new Event('input',{bubbles:true}));
e.dispatchEvent(new Event('change',{bubbles:true}));
e.dispatchEvent(new Event('blur',{bubbles:true}));}}""", [wid, str(area)])
page.locator(f"#{E.FP}{WP}TextBoxExtHeight").fill("1.00")
page.locator(f"#{E.FP}{WP}DropDownListExtOrientation").select_option(orientation)
page.locator(f"#{E.FP}{WP}DropDownListExtBuildingPartId").select_option("Main")
page.locator(f"#{E.FP}{WP}DropDownListExtLocation").select_option("External wall")
page.wait_for_timeout(300)
before = E.window_row_count(page)
page.evaluate("(id)=>{const e=document.getElementById(id); if(e)e.click();}", f"{E.FP}{WP}ButtonAddWindow")
for _ in range(25):
page.wait_for_timeout(200)
if E.window_row_count(page) > before:
break
def _grid_rows(page):
return page.evaluate(
"""()=>{const t=document.querySelector("[id*=GridViewExtendedWidows]");
if(!t)return[];return Array.from(t.querySelectorAll('tr')).slice(1)
.map(r=>Array.from(r.querySelectorAll('td')).map(c=>c.innerText.trim()));}""")
def _delete_zero_rows(page):
g = 0
while g < 6 and E.window_row_count(page) > 1:
g += 1
rows = _grid_rows(page)
bad = next((i for i, c in enumerate(rows) if len(c) > 1 and c[1] in ("0.00", "0", "0.0")), None)
if bad is None:
break
_delete_row(page, bad)
page.wait_for_timeout(400)
def _delete_row(page, idx):
before = E.window_row_count(page)
btn = page.evaluate(
"""(i)=>{const b=document.querySelectorAll("[id*='GridViewExtendedWidows_DeleteButton_']");return b[i]?b[i].id:null;}""", idx)
if not btn:
return
page.evaluate("(id)=>{const e=document.getElementById(id); if(e)e.click();}", btn)
page.wait_for_selector(f"#{E.FP}DeleteWindowDialog_LinkButtonYes", state="visible", timeout=5000)
page.evaluate("(id)=>{const e=document.getElementById(id); if(e)e.click();}", f"{E.FP}DeleteWindowDialog_LinkButtonYes")
for _ in range(20):
page.wait_for_timeout(200)
if E.window_row_count(page) < before:
break
def ventilation(page):
E.goto(page, "VentilationAndCooling", "WebFormVentilationAndCooling.aspx")
E.click_tab(page, "TabContainer_TabPanelVentilationPanel")
E.set_text(page, f"{VP}TextBoxIntermittentFans", "0")
cool = page.locator(f"#{E.FP}{VP}CheckBoxFixedSpaceCooling")
if cool.count() and cool.is_checked():
E.commit(page, cool.uncheck)
E.click_tab(page, "TabContainer_TabPanelMechVent")
mv = page.locator(f"#{E.FP}{MV}CheckBoxMechanicalVentilation")
if mv.count() and mv.is_checked():
E.commit(page, mv.uncheck)
E.click_tab(page, "TabContainer_TabPanelAirPressureTest")
E.set_select(page, f"{APT}DropDownListTestMethod", "Not available")
E.click_tab(page, "TabContainer_TabPanelLighting")
E.set_text(page, f"{LP}TextBoxLightsTotal", "6")
E.set_text(page, f"{LP}TextBoxLedLightsTotal", "0")
E.set_text(page, f"{LP}TextBoxCflLightsTotal", "0")
E.save_close(page)
def space_heating(page):
E.goto(page, "SpaceHeating", "WebFormSpaceHeating.aspx")
page.wait_for_timeout(1000)
mhc = page.locator(f"#{E.MH1}TextBoxMainHeatingCode")
code = mhc.input_value() if mhc.count() else ""
if code and code not in ("0",):
print(f"clearing leftover MainHeatingCode {code}")
page.evaluate("""(id)=>{const e=document.getElementById(id);if(e){e.value='';
e.dispatchEvent(new Event('change',{bubbles:true}));}}""", f"{E.MH1}TextBoxMainHeatingCode")
page.wait_for_timeout(400)
E.save_close(page)
return
# Generic Post-1998 condensing combi (~89% SAP 4b, ≈ PCDB 16137 Ideal Logic
# combi 30). combi vs regular is set on the water page (from primary + no
# cylinder). Control 2106 = CBE (programmer, room thermostat and TRVs).
E.set_heating_dialog(page, f"{MH1B}ButtonMainHeatingCode",
"^Gas", "Mains gas", "Boilers", "Post 1998", "Condensing",
"Combi condens")
print("code:", page.locator(f"#{E.MH1}TextBoxMainHeatingCode").input_value())
E.set_heating_dialog(page, f"{MH1B}ButtonMainHeatingControls",
"^Boilers", "^Standard", "CBE Programmer, room thermostat and TRVs")
print("control:", page.locator(f"#{E.MH1}TextBoxMainHeatingControls").input_value())
E.set_select(page, "DropDownListSecondaryHeatingPresent", "No")
E.save_close(page)
def water_heating(page):
E.goto(page, "WaterHeating", "WebFormWaterHeating.aspx")
E.click_tab(page, "TabContainer_TabPanelWaterHeating")
page.wait_for_timeout(400)
E.clear_hot_water_cylinder(page)
E.set_heating_dialog(page, f"{WH}ButtonWaterHeatingCode",
"From Space Heating", "From the primary heating system")
print("water code:", page.locator(f"#{E.FP}{WH}TextBoxWaterHeatingCode").input_value())
E.save_close(page)
def download(page):
_SAMPLE_DIR.mkdir(parents=True, exist_ok=True)
print("navigating via Recommendations ...")
with page.expect_navigation(wait_until="networkidle", timeout=60_000):
page.click("#ContentBody_buttonActionRecommendations_Link", timeout=15_000)
page.wait_for_timeout(1500)
for btn, fname in (("ContentBody_ContentPlaceHolder1_LinkButtonSummary", "elmhurst_summary.pdf"),
("ContentBody_ContentPlaceHolder1_ButtonDebugInforPdf", "elmhurst_worksheet.pdf")):
try:
with page.expect_download(timeout=60_000) as dl:
page.click(f"#{btn}", timeout=15_000)
dl.value.save_as(str(_SAMPLE_DIR / fname))
print("saved", fname)
except Exception as e:
print("!! download failed", fname, type(e).__name__, str(e)[:120])
_ORDER = ["property_description", "dimensions", "walls", "roofs", "floors",
"openings", "ventilation", "space_heating", "water_heating", "download"]
def main():
if len(sys.argv) < 2 or sys.argv[1] not in _ORDER:
print("usage: build_200004296092.py <" + "|".join(_ORDER) + ">")
return 2
with E.session() as (ctx, page):
globals()[sys.argv[1]](page)
print("done:", sys.argv[1], "->", page.url)
return 0
if __name__ == "__main__":
raise SystemExit(main())