mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
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>
297 lines
13 KiB
Python
297 lines
13 KiB
Python
"""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 m², 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())
|