mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
Corpus validation of the modelling_e2e photovoltaic_supply-as-list fix. Cert 6102-6227-8000-0083-2292 (RdSAP-20.0.0 semi, gas combi + 2× 1.14 kW PV arrays) crashed from_rdsap_schema_20_0_0 on the measured-array list; the fix routes it through the dict-tolerant _map_schema_21_pv. PV correctly credited: engine 61 (no PV) → 66 (+5). Built in Elmhurst (evidence: epc.json + summary + worksheet, fabric+heating; the PV "New Technologies" Panel-details grid deferred): worksheet 55 = engine-on-Elmhurst-inputs 55 exactly → calculator faithful. The +6 engine-vs- Elmhurst base-dwelling residual is the documented RdSAP-default gap (band-C cavity- uninsulated suspended-floor semi). Pinned engine 66. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
307 lines
14 KiB
Python
307 lines
14 KiB
Python
"""Elmhurst build for UPRN 22086693 (RdSAP-Schema-20.0.0, SEMI-DETACHED HOUSE,
|
||
2-storey, band C 1930-1949, cavity UNINSULATED, mains-gas COMBI (no PCDB index →
|
||
generic SAP Table 4b combi), control 2106 (CBE), pitched 200 mm loft, suspended
|
||
uninsulated floor, party wall 6.8 m, double glazed, 2× PV ARRAYS @1.14 kW
|
||
(orientations SE/SW, pitch 30°, overshading modest), electric SECONDARY room
|
||
heater (SAP 691 REA), TFA ~74, window 11.84 m². Engine 66 / lodged 72.
|
||
|
||
P2 of the modelling_e2e corpus validation — the photovoltaic_supply-as-list fix
|
||
cert. The PV adds +5 (engine 61→66) so it IS credited; the −6 vs lodged is a
|
||
fabric/heating gap to localise. NEW build elements: generic (no-PCDB) combi +
|
||
the Renewables/PV page. Run:
|
||
DISPLAY=:99 python scripts/hyde/build_22086693.py <page>
|
||
"""
|
||
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_"
|
||
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_"
|
||
|
||
|
||
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")
|
||
_pick(page, "DropDownListPropertyType2", "semi") # built_form 2
|
||
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")) # 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")
|
||
E.set_text(page, f"{DIM}TextBoxFloorAreaLowestFloor", "36.86")
|
||
E.set_text(page, f"{DIM}TextBoxRoomHeightLowestFloor", "2.30")
|
||
E.set_text(page, f"{DIM}TextBoxWallPerimeterLowestFloor", "13.4")
|
||
E.set_text(page, f"{DIM}TextBoxPartyWallLengthLowestFloor", "6.8")
|
||
E.set_text(page, f"{DIM}TextBoxFloorArea1stFloor", "36.86")
|
||
E.set_text(page, f"{DIM}TextBoxRoomHeight1stFloor", "2.30")
|
||
E.set_text(page, f"{DIM}TextBoxWallPerimeter1stFloor", "17.4")
|
||
E.set_text(page, f"{DIM}TextBoxPartyWallLength1stFloor", "6.8")
|
||
E.save_close(page)
|
||
|
||
|
||
def walls(page):
|
||
E.goto(page, "Walls", "WebFormWalls.aspx")
|
||
E.set_select(page, f"{WALL}DropDownListType", "CA Cavity")
|
||
page.wait_for_timeout(400)
|
||
print("insulation ->", _pick(page, f"{WALL}DropDownListInsulation", "as built")) # uninsulated
|
||
# Semi party wall: cavity. Match "masonry filled" → CF (U≈0); avoid loose "filled".
|
||
pw = _pick(page, f"{PWALL}DropDownListPartyWallType", "masonry filled") \
|
||
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", "200 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", "suspended timber")
|
||
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, 11.84, "North", _glazing(page))
|
||
_delete_zero_rows(page)
|
||
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 _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 fix_window(page):
|
||
"""Edit-in-place: overwrite the shared assessment's leftover window width to ours."""
|
||
E.goto(page, "Openings", "WebFormOpenings.aspx")
|
||
E.click_tab(page, "TabContainer_TabPanelWindowsPanel")
|
||
page.wait_for_timeout(800)
|
||
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, "11.84"])
|
||
print("window width now:", page.locator(f"#{wid}").input_value())
|
||
E.save_close(page)
|
||
|
||
|
||
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", "10")
|
||
E.set_text(page, f"{LP}TextBoxLedLightsTotal", "10") # 100% LED
|
||
E.set_text(page, f"{LP}TextBoxCflLightsTotal", "0")
|
||
E.save_close(page)
|
||
|
||
|
||
def space_heating(page):
|
||
# Generic (no-PCDB) mains-gas combi. The shared assessment may carry a prior
|
||
# cert's storage SEB / PCDB boiler — clear it first if present, then select a
|
||
# generic gas combi via the boiler dialog. Two passes if a SAP code is bound.
|
||
E.goto(page, "SpaceHeating", "WebFormSpaceHeating.aspx")
|
||
page.wait_for_timeout(1000)
|
||
# Pass 1: clear a leftover SAP-table MainHeatingCode (e.g. SEB) so the boiler
|
||
# search/dialog is usable.
|
||
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 boiler via the cascade: Gas → Mains gas →
|
||
# Boilers → Post 1998 → Condensing (combi vs regular is set by the water-
|
||
# heating page: "from primary" + no cylinder = combi). ~89% SAP Table 4b.
|
||
E.set_heating_dialog(page, f"{MH1B}ButtonMainHeatingCode",
|
||
"^Gas", "Mains gas", "Boilers", "Post 1998", "Condensing",
|
||
"Combi condens") # L6 = BGW Post 98 Combi condens. (SAP 4b ~89%)
|
||
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.save_close(page)
|
||
|
||
|
||
def secondary(page):
|
||
# Lodged secondary: SAP 691 = REA electric panel/convector/radiant room heater.
|
||
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",
|
||
"Electric", "Electric", "Room Heater", "REA Panel")
|
||
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 renewables(page):
|
||
# 2× PV arrays @1.14 kW. Discover the Renewables/PV page structure.
|
||
for url in ("WebFormRenewables.aspx", "WebFormPhotovoltaics.aspx", "WebFormSolar.aspx"):
|
||
try:
|
||
E.goto(page, url.replace("WebForm", "").replace(".aspx", ""), url)
|
||
page.wait_for_timeout(800)
|
||
print("PV page:", url, "->", page.url)
|
||
inputs = page.evaluate(
|
||
"""()=>Array.from(document.querySelectorAll("[id*=ContentPlaceHolder1] input,[id*=ContentPlaceHolder1] select")).map(e=>e.id.replace('ContentBody_ContentPlaceHolder1_','')).filter(i=>/pv|photov|solar|peak|orient|pitch|oversh|panel/i.test(i)).slice(0,30)""")
|
||
print("PV-ish fields:", inputs)
|
||
return
|
||
except Exception as e:
|
||
print(" ", url, "->", type(e).__name__)
|
||
|
||
|
||
_ORDER = ["property_description", "dimensions", "walls", "roofs", "floors",
|
||
"openings", "fix_window", "ventilation", "space_heating", "secondary",
|
||
"water_heating", "renewables"]
|
||
|
||
|
||
def main():
|
||
if len(sys.argv) < 2 or sys.argv[1] not in _ORDER:
|
||
print("usage: build_22086693.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())
|