"""Elmhurst build for UPRN 100010086084 (SAP-Schema-15.0, cert 8493-5079-7429-8996-5293, SEMI-DETACHED HOUSE, 9 Pear Tree Avenue, Crewe CW1 3SH, band C 1930-1949, SOLID BRICK uninsulated (thickness not lodged -- UNKNOWN, not a specific mm value), pitched 300mm loft insulation, SOLID ground floor uninsulated, mains-gas generic (no-PCDB) REGULAR boiler — SAP Table 4b code 115 (pre-1998, balanced/open flue, "BGL Pre 98 Wall mount BF/OF") + programmer/room-thermostat control (2104/CBC) + radiators + cylinder (size 2 = "Normal (Less than 130L)" on the live tool, foam, 25mm), gas room-heater SECONDARY (603, approximated as RGB "Modern fire with open flue post 1980" -- the cert gives no finer detail), TFA 68, single meter, 2-storey / 2 floor-dims (no extensions). Partial double glazing (multiple_glazing_type 1, multiple_glazed_proportion 50 -- NOT modelled as partial; this mapper's own `_api_cascade_glazing_type(1)` treats it as 100% double too, so the Elmhurst build matches the mapper's own simplification), glazed_area band 1 (Normal): 0.148*68=10.06 m^2 combined window. 2011 lodged 45; this RdSAP10 engine on gov-API inputs produces 53. Elmhurst's OWN worksheet, built here, gives 46 (window South-facing: 48 -- see below). NEITHER is the "right answer" to chase blindly -- see the investigation write-up in tests/domain/sap10_calculator/test_real_cert_sap_accuracy.py next to this cert's RealCertExpectation for the full comparison, what was ruled out, and the two real-but-unfixed bugs this build surfaced (wall- thickness-unknown U-value fallback mismatch; a window U-value divergence). GOTCHAS specific to this build (fold into the next SAP-Schema-15.0 cert): * The shared assessment WILL carry a stale PCDB boiler ref on Main Heating 1 from a prior build -- clear `TextBoxPCDFBoilerReference` to "0" (JS + save) BEFORE `ButtonMainHeatingCode` becomes clickable (mirrors the documented storage-heater "clear the PCDF boiler first" pattern). * Wall thickness: use `CheckBoxWallThicknessUnknown`, NOT a blank/skipped field -- an untouched thickness textbox silently keeps whatever a PRIOR cert set (bit this build once: inherited a stale 280mm, landing on the exact documented Table 13 200/280mm edge case). * `set_single_window`'s orientation choice is NOT neutral for a cert this reduced-field: North gave 46, South gave 48 (max solar gain) on this cert. South is the fairer bound (this mapper's own N/E/S/W synthesis spreads solar gain; a single North-facing pick is the worst case). * The Roof/Cylinder thickness dropdowns need an EXACT option-text match ("300 mm" not "300 mm or more"; "25 mm" not "25") -- `set_select` fails silently (no exception) on a non-matching value and leaves the field at its previous/inherited state. ALWAYS verify with `.input_value()` after setting, don't trust the call succeeded. * Address/region are carried over from whatever cert built the shared assessment last -- explicitly set them (see `address()` below) even though region is provably inert for the SAP rating itself (UK-average weather always, `_region_index` in cert_to_inputs.py) -- other summary fields (post town) still read from it. Run: DISPLAY=:99 python scripts/hyde/build_100010086084.py """ from __future__ import annotations import sys 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_" DP = "TabContainer_TabPanelDoorsPanel_" VP = "TabContainer_TabPanelVentilationPanel_" APT = "TabContainer_TabPanelAirPressureTest_" LP = "TabContainer_TabPanelLighting_" MV = "TabContainer_TabPanelMechVent_" WH = "TabContainer_TabPanelWaterHeating_" MH1B = "TabContainer_TabPanelMainHeating1_WebUserControlMainHeating1_" 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 address(page): page.goto(E.ENTRY_URL, wait_until="networkidle", timeout=60_000) E.set_text(page, "TextBoxHouseNo", "9") E.set_text(page, "TextBoxStreet", "Pear Tree Avenue") E.set_text(page, "TextBoxTown", "Crewe") E.set_text(page, "TextBoxPostCode", "CW1 3SH") page.wait_for_timeout(300) E.set_select(page, "DropDownListRegion", "West Pennines") page.click("#ContentBody_ContentPlaceHolderNavigations_ButtonSaveClose", timeout=15_000) page.wait_for_timeout(1000) def property_description(page): E.goto(page, "PropertyDescription", "WebFormPropertyDescription.aspx") E.set_select(page, "DropDownListPropertyType1", "H House") _pick(page, "DropDownListPropertyType2", "semi") # built_form 2 E.set_text(page, "TextBoxStoreys", "2") E.set_text(page, "TextBoxHabitableRooms", "3") E.set_text(page, "TextBoxHeatedHabitableRooms", "3") print("date ->", _pick(page, "DropDownListDateBuiltMain", "1930-1949")) # band C E.set_select(page, "DropDownListDateBuiltFirst", "") E.set_select(page, "DropDownListRoomInRoofMain", "") E.save_close(page) def dimensions(page): E.goto(page, "Dimensions", "WebFormDimensions.aspx") # Ground floor: area 37.74, heat_loss_perimeter 21.05 -> geometry-derived # party wall (the shared/depth side) ~4.58m. E.set_text(page, f"{DIM}TextBoxFloorAreaLowestFloor", "37.74") E.set_text(page, f"{DIM}TextBoxRoomHeightLowestFloor", "2.41") E.set_text(page, f"{DIM}TextBoxWallPerimeterLowestFloor", "21.05") E.set_text(page, f"{DIM}TextBoxPartyWallLengthLowestFloor", "4.58") # First floor: area 39.48, heat_loss_perimeter 18.45 -> party wall ~6.75m. E.set_text(page, f"{DIM}TextBoxFloorArea1stFloor", "39.48") E.set_text(page, f"{DIM}TextBoxRoomHeight1stFloor", "2.45") E.set_text(page, f"{DIM}TextBoxWallPerimeter1stFloor", "18.45") E.set_text(page, f"{DIM}TextBoxPartyWallLength1stFloor", "6.75") E.save_close(page) def walls(page): E.goto(page, "Walls", "WebFormWalls.aspx") E.set_select(page, f"{WALL}DropDownListType", "SO Solid brick") page.wait_for_timeout(400) print("insulation ->", _pick(page, f"{WALL}DropDownListInsulation", "as built")) # Thickness NOT lodged on this schema -- mark unknown explicitly, don't # leave the textbox untouched (it silently inherits a stale value). cb = page.locator(f"#{E.FP}{WALL}CheckBoxWallThicknessUnknown") if cb.count() and not cb.is_checked(): E.commit(page, cb.check) pw = _pick(page, f"{PWALL}DropDownListPartyWallType", "solid") \ or _pick(page, f"{PWALL}DropDownListPartyWallType", "determine") print("party wall ->", pw) E.save_close(page) def roofs(page): E.goto(page, "Roofs", "WebFormRoofs.aspx") _pick(page, f"{ROOF}DropDownListType", "access to loft") _pick(page, f"{ROOF}DropDownListInsulationAt", "joists") E.set_select(page, f"{ROOF}DropDownListThickness", "300 mm") # exact match print("roof thickness now:", page.locator(f"#{E.FP}{ROOF}DropDownListThickness").input_value()) 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") # glazed_area band 1 (Normal): total = 0.148*68*1.00 = 10.06 m^2. multiple_ # glazing_type=1 -> this mapper's own _api_cascade_glazing_type(1) treats # it as 100% double (ignores multiple_glazed_proportion=50 the same way), # so "Double, unknown install date" matches the mapper's own assumption. # South (not North) -- max solar gain, the fairer single-orientation # bound against this mapper's own N/E/S/W synthesis (see module docstring). E.set_single_window(page, 10.06, "South", "Double with unknown install date") E.click_tab(page, "TabContainer_TabPanelDoorsPanel") E.set_text(page, f"{DP}TextBoxDoors", "1") E.set_text(page, f"{DP}TextBoxDoorsInsulated", "0") E.set_text(page, f"{DP}TextBoxDraughtProofedDoors", "0") E.save_close(page) def ventilation(page): E.goto(page, "VentilationAndCooling", "WebFormVentilationAndCooling.aspx") E.click_tab(page, "TabContainer_TabPanelVentilationPanel") # This cert's register lodges open_fireplaces_count=0 (epc.json) -- the # shared assessment's ventilation tab silently inherits a stale # TextBoxOpenChimneys="2" from a PRIOR cert build (same untouched-field- # keeps-prior-value gotcha documented above for wall thickness) unless # explicitly zeroed here. E.set_text(page, f"{VP}TextBoxOpenChimneys", "0") E.set_text(page, f"{VP}TextBoxChimneysFluesClosedFire", "0") E.set_text(page, f"{VP}TextBoxBlockedChimneys", "0") 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") # low_energy_lighting 43% of 7 fixed outlets -> 3 low-energy. E.set_text(page, f"{LP}TextBoxLightsTotal", "7") E.set_text(page, f"{LP}TextBoxLedLightsTotal", "3") E.set_text(page, f"{LP}TextBoxCflLightsTotal", "0") E.save_close(page) def space_heating(page): # SAP Table 4b code 115: pre-1998 gas boiler, balanced/open flue, no PCDB # index (main_heating_data_source=2). Regular boiler (has_hot_water_ # cylinder=true, water_heating_code 901 "from primary") + radiators # (heat_emitter_type=1), fan_flue_present=N. E.goto(page, "SpaceHeating", "WebFormSpaceHeating.aspx") page.wait_for_timeout(1000) # The shared assessment's Main Heating 1 slot carries a stale PCDB boiler # ref from a prior build -- clear it first (JS + save) so the SAP-code # button appears; mirrors the documented storage-heater pattern. ref = page.locator(f"#{E.FP}{MH1B}TextBoxPCDFBoilerReference") if ref.count() and ref.input_value() not in ("", "0"): print(f"clearing leftover PCDF boiler ref {ref.input_value()}") page.evaluate("""(id)=>{const e=document.getElementById(id);if(e){e.value='0'; e.dispatchEvent(new Event('change',{bubbles:true}));}}""", f"{E.FP}{MH1B}TextBoxPCDFBoilerReference") page.wait_for_timeout(500) E.save_close(page) page.wait_for_load_state("networkidle", timeout=20_000) page.wait_for_selector( f"#{E.NAV}SpaceHeating_Link", state="visible", timeout=30_000 ) 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 (no-PCDB) mains-gas REGULAR boiler, pre-1998 balanced/open flue # (matches boiler_flue_type=1, fan_flue_present=N) -> Gas -> Mains gas -> # Boilers -> Pre 1998 - Balanced/Open Flue -> Wall mount (regular, not # combi/back-boiler -- matches has_hot_water_cylinder=true). E.set_heating_dialog(page, f"{MH1B}ButtonMainHeatingCode", "^Gas", "Mains gas", "Boilers", "Balanced.*Open", "Wall mount") print("code:", page.locator(f"#{E.MH1}TextBoxMainHeatingCode").input_value()) # Control 2104 = "Programmer and room thermostat" (matches the cert's own # main_heating_controls description) -- no TRVs. NB: CBC is "Programmer # and room thermostat"; CBB is "Room thermostat and TRVs" -- do not swap. E.set_heating_dialog(page, f"{MH1B}ButtonMainHeatingControls", "^Boilers", "^Standard", "CBC Programmer and room thermostat") print("control:", page.locator(f"#{E.MH1}TextBoxMainHeatingControls").input_value()) # Explicit flue type (BUG fixed): the "Balanced/Open Flue" boiler-code # CATEGORY picked above is a combined Table 4b group name (BF/OF) that # does NOT set the separate `RadioButtonListFlueType` select on this # page -- that field kept its inherited/default "Balanced" from a # prior cert (silent-stale-value class of bug, see docstring), which # mismatches the lodged gov-API `boiler_flue_type=1` ("Open"). Set it # explicitly to match. E.set_select(page, f"{MH1B}RadioButtonListFlueType", "Open") print("flue type:", page.locator(f"#{E.FP}{MH1B}RadioButtonListFlueType").input_value()) E.save_close(page) def secondary(page): # Lodged secondary: SAP 603 = room heaters, mains gas (no finer detail # lodged -- RGB "Modern fire with open flue post 1980" is the generic # approximation). E.goto(page, "SpaceHeating", "WebFormSpaceHeating.aspx") page.wait_for_timeout(600) E.set_select(page, "DropDownListSecondaryHeatingPresent", "Yes") page.wait_for_timeout(900) E.set_heating_dialog(page, "ButtonSecondaryHeatingCode", "^Gas", "Mains gas", "Room Heater", "RGB Modern fire with open flue") tb = page.locator(f"#{E.FP}TextBoxSecondaryHeatingCode") print("secondary code:", tb.input_value() if tb.count() else "?") 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.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()) # cylinder: lodged cylinder_size code 2 = SAP10 "Normal" (110 L; see # cert_to_inputs.py _CYLINDER_SIZE_CODE_TO_LITRES). BUG (fixed): this # dropdown's