mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
chore(hyde): harden Elmhurst automation for per-cert builds (window cleanup + reset)
Support for the per-cert Elmhurst worksheet loop: - goto()/save_close(): fall back networkidle -> domcontentloaded on timeout so an incomplete-validation state (half-set RR, 0-area window) no longer hangs navigation. - add_combined_window(): select glazing first (AutoPostBack), then JS input+change+blur the width/height with settle waits so the Area cell computes before Add (a plain .fill() raced the postback -> 0.00 row). - set_single_window(): leave the grid holding exactly one row, deleting rows carried over from a prior build of the SHARED assessment (the window grid is the only accumulating page). - reset_transient_state(): return the shared assessment to neutral after a run (clear RR age band, secondary = No, meter = Single). KNOWN RESIDUAL: the window grid still intermittently persists a 0.00 area through Save & Close (Elmhurst's "each Add wipes the previous row" + last-row-undeletable behaviour), and the secondary-heating dialog's modal background can intercept the cascade click. Reliable autonomous grinding of per-cert builds needs these two hardened further; the loop currently completes clean single-main gas certs (see the 47084930 flat-roof slice) but stalls on secondary-heating + stale-window combinations. build_ 100021924710.py is the WIP template for that harder shape. No engine change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
8e33a8de53
commit
7987c94876
2 changed files with 313 additions and 10 deletions
231
scripts/hyde/build_100021924710.py
Normal file
231
scripts/hyde/build_100021924710.py
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
"""Elmhurst build for UPRN 100021924710 (RdSAP-21.0.1, MID-TERRACE HOUSE, band B,
|
||||
SOLID BRICK 250 mm uninsulated, pitched insulated (assumed) loft, floor to
|
||||
UNHEATED space, mains-gas COMBI (PCDB 19008) + gas room-heater SECONDARY, DUAL
|
||||
meter, 11 DG windows ~18.32 m², TFA 116. Lodged SAP 63; engine 65.0 (Δ +2.0).
|
||||
|
||||
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_100021924710.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_100021924710")
|
||||
|
||||
|
||||
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", "mid-terrace") or \
|
||||
_pick(page, "DropDownListPropertyType2", "mid terrace")
|
||||
E.set_text(page, "TextBoxStoreys", "2")
|
||||
E.set_text(page, "TextBoxHabitableRooms", "5")
|
||||
E.set_text(page, "TextBoxHeatedHabitableRooms", "5")
|
||||
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", "59.20")
|
||||
E.set_text(page, f"{DIM}TextBoxRoomHeightLowestFloor", "2.74")
|
||||
E.set_text(page, f"{DIM}TextBoxWallPerimeterLowestFloor", "16.0")
|
||||
E.set_text(page, f"{DIM}TextBoxPartyWallLengthLowestFloor", "21.1")
|
||||
E.set_text(page, f"{DIM}TextBoxFloorArea1stFloor", "57.10")
|
||||
E.set_text(page, f"{DIM}TextBoxRoomHeight1stFloor", "2.74")
|
||||
E.set_text(page, f"{DIM}TextBoxWallPerimeter1stFloor", "15.7")
|
||||
E.set_text(page, f"{DIM}TextBoxPartyWallLength1stFloor", "21.7")
|
||||
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"))
|
||||
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", "unknown") or \
|
||||
_pick(page, f"{ROOF}DropDownListThickness", "assumed")
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def floors(page):
|
||||
E.goto(page, "Floors", "WebFormFloors.aspx")
|
||||
_pick(page, f"{FLOOR}DropDownListLocation", "unheated")
|
||||
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)
|
||||
E.set_single_window(page, 18.32, "North",
|
||||
_glazing(page) if False else "Double post or during 2022")
|
||||
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):
|
||||
return "Double with unknown install date"
|
||||
|
||||
|
||||
def ventilation(page):
|
||||
E.goto(page, "VentilationAndCooling", "WebFormVentilationAndCooling.aspx")
|
||||
E.click_tab(page, "TabContainer_TabPanelVentilationPanel")
|
||||
E.set_text(page, f"{VP}TextBoxIntermittentFans", "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", "8")
|
||||
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
|
||||
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())
|
||||
# dual meter (cert meter_type 1)
|
||||
E.click_tab(page, "TabContainer_TabPanelMeters")
|
||||
try:
|
||||
E.set_select(page, "TabContainer_TabPanelMeters_RadioButtonListElectricityType", "Dual")
|
||||
except Exception:
|
||||
pass
|
||||
E.save_close(page)
|
||||
|
||||
|
||||
def 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", "secondary",
|
||||
"water_heating", "download", "cleanup"]
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2 or sys.argv[1] not in _ORDER:
|
||||
print("usage: build_100021924710.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())
|
||||
|
|
@ -235,24 +235,96 @@ def add_combined_window(
|
|||
) -> None:
|
||||
"""Add ONE window row of `total_area_m2` (width=area, height=1.0). Sets the
|
||||
required per-row fields (orientation/location/building-part) so the
|
||||
Recommendations validation passes. Caller then deletes the prior rows."""
|
||||
Recommendations validation passes. Caller then deletes the prior rows.
|
||||
|
||||
Glazing MUST be selected first (it AutoPostBacks and re-renders the form);
|
||||
width/height are set via JS input+change+blur events with settle waits so
|
||||
the Area cell (18.32 = w×h) computes before Add — a plain Playwright
|
||||
`.fill()` raced the postback and Elmhurst added a 0.00-area row."""
|
||||
set_select(page, f"{_WP}DropDownListExtGlazing", glazing)
|
||||
page.locator(f"#{FP}{_WP}TextBoxExtWidth").fill(str(total_area_m2))
|
||||
h = page.locator(f"#{FP}{_WP}TextBoxExtHeight")
|
||||
h.fill("1.00")
|
||||
commit(page, lambda: h.press("Tab"))
|
||||
page.locator(f"#{FP}{_WP}DropDownListExtOrientation").select_option(orientation)
|
||||
page.locator(f"#{FP}{_WP}DropDownListExtBuildingPartId").select_option("Main")
|
||||
page.locator(f"#{FP}{_WP}DropDownListExtLocation").select_option("External wall")
|
||||
page.wait_for_timeout(200)
|
||||
page.wait_for_timeout(SETTLE_MS)
|
||||
|
||||
def _jset(suffix: str, value: str) -> None:
|
||||
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}));}}""",
|
||||
[f"{FP}{suffix}", value],
|
||||
)
|
||||
page.wait_for_timeout(300)
|
||||
|
||||
_jset(f"{_WP}TextBoxExtWidth", str(total_area_m2))
|
||||
_jset(f"{_WP}TextBoxExtHeight", "1.00")
|
||||
for suffix, opt in (
|
||||
(f"{_WP}DropDownListExtOrientation", orientation),
|
||||
(f"{_WP}DropDownListExtBuildingPartId", "Main"),
|
||||
(f"{_WP}DropDownListExtLocation", "External wall"),
|
||||
):
|
||||
loc = page.locator(f"#{FP}{suffix}")
|
||||
if loc.count():
|
||||
loc.select_option(opt)
|
||||
page.wait_for_timeout(300)
|
||||
before = window_row_count(page)
|
||||
_jsclick(page, f"{FP}{_WP}ButtonAddWindow")
|
||||
for _ in range(20):
|
||||
page.wait_for_timeout(200)
|
||||
page.wait_for_timeout(250)
|
||||
if window_row_count(page) > before:
|
||||
break
|
||||
|
||||
|
||||
def set_single_window(
|
||||
page: Page, total_area_m2: float, orientation: str = "North",
|
||||
glazing: str = "Double post or during 2022",
|
||||
) -> None:
|
||||
"""Leave the window grid holding EXACTLY one row of `total_area_m2`,
|
||||
deleting any rows carried over from a prior build of the shared
|
||||
assessment. Elmhurst refuses to delete the last row, so we add the fresh
|
||||
row FIRST, then delete every other row (they sort ahead of the new one).
|
||||
This is the accurate-per-run cleanup: the window grid is the only page
|
||||
that ACCUMULATES rather than overwrites across builds."""
|
||||
add_combined_window(page, total_area_m2, orientation, glazing)
|
||||
guard = 0
|
||||
while window_row_count(page) > 1 and guard < 25:
|
||||
guard += 1
|
||||
before = window_row_count(page)
|
||||
delete_first_window(page)
|
||||
if window_row_count(page) >= before:
|
||||
break # couldn't delete (last-row guard / stuck) — stop
|
||||
|
||||
|
||||
def reset_transient_state(page: Page) -> None:
|
||||
"""Return the shared assessment to a NEUTRAL state after a run so the next
|
||||
build (or Khalim's) is not polluted by this cert's carried-over settings.
|
||||
Clears the room-in-roof age band, forces secondary-heating present = No,
|
||||
and the electricity meter back to Single. The window grid is left to the
|
||||
next build's `set_single_window` (Elmhurst can't empty it to zero rows).
|
||||
Call once at the end of a build session."""
|
||||
goto(page, "PropertyDescription", "WebFormPropertyDescription.aspx")
|
||||
rr = page.locator("#ContentBody_ContentPlaceHolder1_DropDownListRoomInRoofMain")
|
||||
if rr.count() and rr.input_value():
|
||||
rr.select_option("")
|
||||
page.wait_for_timeout(SETTLE_MS)
|
||||
save_close(page)
|
||||
goto(page, "SpaceHeating", "WebFormSpaceHeating.aspx")
|
||||
page.wait_for_timeout(SETTLE_MS)
|
||||
sec = page.locator(f"#{FP}DropDownListSecondaryHeatingPresent")
|
||||
if sec.count():
|
||||
try:
|
||||
set_select(page, "DropDownListSecondaryHeatingPresent", "No")
|
||||
except Exception:
|
||||
pass
|
||||
meters = "TabContainer_TabPanelMeters"
|
||||
click_tab(page, meters)
|
||||
et = page.locator(f"#{FP}{meters}_RadioButtonListElectricityType")
|
||||
if et.count():
|
||||
try:
|
||||
set_select(page, f"{meters}_RadioButtonListElectricityType", "Single")
|
||||
except Exception:
|
||||
pass
|
||||
save_close(page)
|
||||
|
||||
|
||||
def delete_first_window(page: Page) -> None:
|
||||
"""Delete the first window row via the Yes/No modal. Will refuse to drop the
|
||||
LAST row (Elmhurst blocks it) — so add the replacement BEFORE deleting."""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue