mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
ventilation(): the cert lodges no extract-fan count and RdSAP 10 Table 5's age-band B default is 0, but the build entered 1 — inflating Elmhurst's own (8)/(21) infiltration lines relative to both the cert and our engine. space_heating(): switched from the generic "Combi condens" SAP-Table dialog code (84% efficiency, no PCDB record) to elmhurst_lib.set_pcdb_boiler(18908) via clear_main_heating_code — resolves the exact lodged product (Worcester Greenstar 4000, 88.70%) and its correct keep-hot classification (None), matching the SAP engine's own PCDB lookup and removing the "Standard Combi" keep-hot artifact the reused generic code carried. Also corrected the file's stale docstring (described a different property entirely — mid-terrace/250mm/DUAL meter/11 windows/lodged 63 — left over from a copy-paste). Verified against the refreshed worksheet: (21) shelter-adjusted ACH 0.6938 now EXACT vs our engine's ventilation_from_cert; (61) combi loss monthly kWh matches ours to ~0.01 kWh (was carrying a ~360 kWh/yr keep-hot artifact). Both open items from the PR #1439 handoff are resolved. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
230 lines
10 KiB
Python
230 lines
10 KiB
Python
"""Elmhurst build for UPRN 100061275133 (RdSAP-21.0.1, SEMI-DETACHED HOUSE, band B,
|
|
SOLID BRICK 400 mm with EXTERNAL insulation, pitched 300 mm loft, ground floor
|
|
SUSPENDED, no insulation (assumed) — no per-bp floor_construction_type lodged,
|
|
mains-gas COMBI (PCDB 18908, Worcester Greenstar 4000 88.70%), no secondary
|
|
heating, single meter, 12 DG windows ~15.32 m², TFA 78. Lodged SAP 76; engine
|
|
76 (76.33, exact match after the §5 (12) floor-infiltration fix).
|
|
|
|
Per-cert Elmhurst validation. Uses set_single_window (deletes the shared
|
|
assessment's carried-over window rows) + reset_transient_state at the end so the
|
|
assessment is left neutral. Run:
|
|
DISPLAY=:99 python scripts/hyde/build_100061275133.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_"
|
|
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_100061275133")
|
|
|
|
|
|
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", "semi")
|
|
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"))
|
|
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", "38.99")
|
|
E.set_text(page, f"{DIM}TextBoxRoomHeightLowestFloor", "2.40")
|
|
E.set_text(page, f"{DIM}TextBoxWallPerimeterLowestFloor", "17.92")
|
|
E.set_text(page, f"{DIM}TextBoxPartyWallLengthLowestFloor", "5.09")
|
|
E.set_text(page, f"{DIM}TextBoxFloorArea1stFloor", "38.99")
|
|
E.set_text(page, f"{DIM}TextBoxRoomHeight1stFloor", "2.40")
|
|
E.set_text(page, f"{DIM}TextBoxWallPerimeter1stFloor", "17.92")
|
|
E.set_text(page, f"{DIM}TextBoxPartyWallLength1stFloor", "5.09")
|
|
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", "external"))
|
|
print("party ->", _pick(page, f"{PWALL}DropDownListPartyWallType", "determine")
|
|
or _pick(page, f"{PWALL}DropDownListPartyWallType", "masonry"))
|
|
# wall thickness (cert lodges 400 mm; Elmhurst else defaults 260)
|
|
for suf in (f"{WALL}TextBoxThickness", f"{WALL}TextBoxWallThickness"):
|
|
if page.locator(f"#{E.FP}{suf}").count():
|
|
E.set_text(page, suf, "400"); print("wall thickness set 400 via", suf); break
|
|
else:
|
|
print("thickness field not found; wall fields:", [i for i in page.evaluate("()=>Array.from(document.querySelectorAll('[id*=Wall]')).map(e=>e.id)")][:20])
|
|
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", "300")
|
|
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", "suspended")
|
|
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)
|
|
E.set_single_window(page, 15.32, "North", _glazing(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", "2") # cert: percent_draughtproofed=100
|
|
E.save_close(page)
|
|
|
|
|
|
def _glazing(page):
|
|
# glazed_type 13 -> double glazed, 2022 or later (U 1.4)
|
|
return "Double post or during 2022"
|
|
|
|
|
|
def ventilation(page):
|
|
E.goto(page, "VentilationAndCooling", "WebFormVentilationAndCooling.aspx")
|
|
E.click_tab(page, "TabContainer_TabPanelVentilationPanel")
|
|
# Cert lodges no extract-fan count; RdSAP 10 Table 5 age-band default for
|
|
# band B (A-E) is 0, and our engine's ventilation_from_cert computes 0 —
|
|
# was wrongly entered as 1 here, inflating Elmhurst's (8)/(21) ACH vs ours.
|
|
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", "5")
|
|
E.set_text(page, f"{LP}TextBoxLedLightsTotal", "5")
|
|
E.set_text(page, f"{LP}TextBoxCflLightsTotal", "0")
|
|
E.save_close(page)
|
|
|
|
|
|
def space_heating(page):
|
|
# PCDB exact-boiler entry (elmhurst_lib.set_pcdb_boiler): the cert's
|
|
# main_heating_index_number 18908 resolves to Worcester Greenstar 4000
|
|
# GR4700iW 25 C NG, 88.70% winter efficiency — matching the SAP engine's
|
|
# PCDB lookup exactly, vs the generic "Combi condens" SAP-Table code
|
|
# (84%) the old dialog-driven entry carried a keep-hot artifact from.
|
|
E.goto(page, "SpaceHeating", "WebFormSpaceHeating.aspx")
|
|
page.wait_for_timeout(1000)
|
|
E.clear_main_heating_code(page)
|
|
desc = E.set_pcdb_boiler(page, 18908)
|
|
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())
|
|
E.set_select(page, "DropDownListSecondaryHeatingPresent", "No")
|
|
E.save_close(page)
|
|
|
|
|
|
def _unused_secondary(page):
|
|
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")
|
|
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 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",
|
|
"water_heating", "download", "cleanup"]
|
|
|
|
|
|
def main():
|
|
if len(sys.argv) < 2 or sys.argv[1] not in _ORDER:
|
|
print("usage: build_100061275133.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())
|