"""Elmhurst build for UPRN 100031768368 (RdSAP-21.0.1, END-TERRACE HOUSE, band C, SOLID BRICK 280 mm uninsulated, pitched 100 mm loft insulation, SOLID ground floor uninsulated, mains-gas COMBI (PCDB 18250, Baxi 630 Combi 88.4%) + gas ROOM-HEATER SECONDARY, single electric shower with WWHRS (no mixer shower), extract fan lodged (1) + 1 other-heater flue, 10 DG windows across 2 orientations (~13.48 m2 total: East 5.52 m2, West 5.13+2.83 m2 across two glazing eras), TFA 76. Lodged SAP 61; engine 59.12 (Delta -1.88). Per-cert Elmhurst validation, continuing the 1-2 SAP-band reconciliation pass. Uses set_window_groups (3 orientation/glazing groups, not a single collapsed window — see the 100061275133 finding that a single-orientation simplification meaningfully understates solar gain) + reset_transient_state at the end so the shared assessment is left neutral. Run: DISPLAY=:99 python scripts/hyde/build_100031768368.py """ 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_" 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_100031768368") 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 property_description(page): E.goto(page, "PropertyDescription", "WebFormPropertyDescription.aspx") E.set_select(page, "DropDownListPropertyType1", "H House") _pick(page, "DropDownListPropertyType2", "end") E.set_text(page, "TextBoxStoreys", "2") E.set_text(page, "TextBoxHabitableRooms", "4") E.set_text(page, "TextBoxHeatedHabitableRooms", "4") print("date ->", _pick(page, "DropDownListDateBuiltMain", "1930-1949")) 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", "37.75") E.set_text(page, f"{DIM}TextBoxRoomHeightLowestFloor", "2.45") E.set_text(page, f"{DIM}TextBoxWallPerimeterLowestFloor", "20.1") E.set_text(page, f"{DIM}TextBoxPartyWallLengthLowestFloor", "5.0") E.set_text(page, f"{DIM}TextBoxFloorArea1stFloor", "37.75") E.set_text(page, f"{DIM}TextBoxRoomHeight1stFloor", "2.45") E.set_text(page, f"{DIM}TextBoxWallPerimeter1stFloor", "20.1") E.set_text(page, f"{DIM}TextBoxPartyWallLength1stFloor", "5.0") E.save_close(page) def walls(page): E.goto(page, "Walls", "WebFormWalls.aspx") _pick(page, f"{WALL}DropDownListType", "solid brick") page.wait_for_timeout(500) print("insulation ->", _pick(page, f"{WALL}DropDownListInsulation", "as built")) print("party ->", _pick(page, f"{PWALL}DropDownListPartyWallType", "determine") or _pick(page, f"{PWALL}DropDownListPartyWallType", "masonry")) for suf in (f"{WALL}TextBoxThickness", f"{WALL}TextBoxWallThickness"): if page.locator(f"#{E.FP}{suf}").count(): E.set_text(page, suf, "280"); print("wall thickness set 280 via", suf); break 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") _pick(page, f"{ROOF}DropDownListThickness", "100") E.save_close(page) def floors(page): E.goto(page, "Floors", "WebFormFloors.aspx") _pick(page, f"{FLOOR}DropDownListLocation", "ground floor") page.wait_for_timeout(400) _pick(page, f"{FLOOR}DropDownListType", "solid") ins = page.locator(f"#{E.FP}{FLOOR}DropDownListInsulation") if ins.count(): 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") page.wait_for_timeout(500) # 3 orientation/glazing-era groups (not a single collapsed window): # East 5.52 m2 (double pre-2002), West 5.13 m2 (double pre-2002), West # 2.83 m2 (double 2002-2022, the one larger newer window index 8). # The glazing dropdown retains its last-selected value across adds; if # the next row wants the SAME text, selecting it again fires no 'change' # event (identical value -> no transition) and the row commits with a # blank Glazing Type. Force a transition through a different option # first so every add gets a genuine change event. for area, orient, glazing in ( (5.5174, "East", "Double pre 2002"), (5.1346, "West", "Double pre 2002"), (2.8288, "West", "Double between 2002 and 2021"), ): E.set_select(page, f"{E._WP}DropDownListExtGlazing", "Secondary glazing") # noqa: SLF001 page.wait_for_timeout(300) E.add_combined_window(page, area, orient, glazing) page.wait_for_timeout(500) guard = 0 while E.window_row_count(page) > 3 and guard < 25: guard += 1 i = E.window_row_count(page) - 1 if not E._delete_window_at_index(page, i): # noqa: SLF001 break 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", "1") # cert: percent_draughtproofed=100 E.save_close(page) def ventilation(page): E.goto(page, "VentilationAndCooling", "WebFormVentilationAndCooling.aspx") E.click_tab(page, "TabContainer_TabPanelVentilationPanel") E.set_text(page, f"{VP}TextBoxIntermittentFans", "1") # cert: extract_fans_count=1 E.set_text(page, f"{VP}TextBoxFluesOtherHeater", "1") # cert: other_flues_count=1 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", "13") E.set_text(page, f"{LP}TextBoxLedLightsTotal", "10") E.set_text(page, f"{LP}TextBoxCflLightsTotal", "2") E.save_close(page) def space_heating(page): # PCDB exact-boiler entry: cert's main_heating_index_number 18250 # resolves to Baxi 630 Combi, 88.4% winter efficiency. E.goto(page, "SpaceHeating", "WebFormSpaceHeating.aspx") page.wait_for_timeout(1000) E.clear_main_heating_code(page) desc = E.set_pcdb_boiler(page, 18250) print("PCDB boiler ->", desc) 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()) # cert lodges secondary_heating_type=605 ("Room heaters, mains gas", # SAP10 Table 4a eff 0.40) — a genuine secondary system, unlike # 100061275133's "None". The dialog is a 4-level cascade (fuel -> # sub-fuel -> heater class -> specific product), not 3 like main # heating controls. Probed all 12 RGx options by resolved SAP code; # "RGH Flush fitting live effect gas fire, sealed to chimney" is the # exact match for code 605. E.set_select(page, "DropDownListSecondaryHeatingPresent", "Yes") page.wait_for_timeout(900) E.set_heating_dialog(page, "ButtonSecondaryHeatingCode", "^Gas", "Mains gas", "Room Heaters", "RGH") 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.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 baths_showers(page): """cert: number_baths=1, shower_outlets=[electric shower, WWHRS=1], mixer_shower_count=0 — an instantaneous ELECTRIC shower with WWHRS fitted, not a mixer shower off the main system.""" E.goto(page, "WaterHeating", "WebFormWaterHeating.aspx") E.click_tab(page, "TabContainer_TabPanelBathsShowers") page.wait_for_timeout(400) print("(manual check needed: set 1 bath, 1 electric shower with WWHRS)") def download(page): _SAMPLE_DIR.mkdir(parents=True, exist_ok=True) E.goto(page, "Recommendations", "WebFormRecommendations.aspx") page.wait_for_timeout(1500) with page.expect_navigation(wait_until="networkidle", timeout=40000): page.click("#ContentBody_buttonActionSummary_Link", timeout=15000) page.wait_for_timeout(2000) for btn, fname in (("ContentBody_ContentPlaceHolder1_LinkButtonSummary", "elmhurst_summary.pdf"), ("ContentBody_ContentPlaceHolder1_ButtonDebugInforPdf", "elmhurst_worksheet.pdf")): try: with page.expect_download(timeout=60000) as dl: page.click(f"#{btn}", timeout=15000) 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]) def cleanup(page): """Leave the shared assessment neutral for the next build.""" E.reset_transient_state(page) print("reset transient state (RR/secondary/meter)") _ORDER = ["property_description", "dimensions", "walls", "roofs", "floors", "openings", "ventilation", "space_heating", "baths_showers", "water_heating", "download", "cleanup"] def main(): if len(sys.argv) < 2 or sys.argv[1] not in _ORDER: print("usage: build_100031768368.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())