diff --git a/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-21.0.1/uprn_100050881708/elmhurst_summary.pdf b/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-21.0.1/uprn_100050881708/elmhurst_summary.pdf new file mode 100644 index 000000000..28a0ee26d Binary files /dev/null and b/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-21.0.1/uprn_100050881708/elmhurst_summary.pdf differ diff --git a/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-21.0.1/uprn_100050881708/elmhurst_worksheet.pdf b/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-21.0.1/uprn_100050881708/elmhurst_worksheet.pdf new file mode 100644 index 000000000..418feb6de Binary files /dev/null and b/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-21.0.1/uprn_100050881708/elmhurst_worksheet.pdf differ diff --git a/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-21.0.1/uprn_100050881708/epc.json b/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-21.0.1/uprn_100050881708/epc.json new file mode 100644 index 000000000..100e4eb06 --- /dev/null +++ b/backend/epc_api/json_samples/real_life_examples/RdSAP-Schema-21.0.1/uprn_100050881708/epc.json @@ -0,0 +1,426 @@ +{ + "uprn": 100050881708, + "roofs": [ + { + "description": "Pitched, 225 mm loft insulation", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "walls": [ + { + "description": "Cavity wall, filled cavity", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "floors": [ + { + "description": "To unheated space, no insulation (assumed)", + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + } + ], + "status": "entered", + "tenure": 3, + "window": { + "description": "Fully double glazed", + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 3 + }, + "lighting": { + "description": "Below average lighting efficiency", + "energy_efficiency_rating": 2, + "environmental_efficiency_rating": 2 + }, + "postcode": "S65 3RW", + "hot_water": { + "description": "From main system", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + }, + "post_town": "ROTHERHAM", + "built_form": 2, + "created_at": "2026-05-27 15:09:39", + "door_count": 2, + "region_code": 3, + "report_type": 2, + "sap_heating": { + "number_baths": 1, + "cylinder_size": 1, + "number_baths_wwhrs": 0, + "water_heating_code": 901, + "water_heating_fuel": 26, + "main_heating_details": [ + { + "has_fghrs": "N", + "main_fuel_type": 26, + "boiler_flue_type": 2, + "fan_flue_present": "Y", + "heat_emitter_type": 1, + "emitter_temperature": 0, + "main_heating_number": 1, + "main_heating_control": 2106, + "main_heating_category": 2, + "main_heating_fraction": 1, + "central_heating_pump_age": 0, + "main_heating_data_source": 1, + "main_heating_index_number": 17741 + } + ], + "immersion_heating_type": "NA", + "has_fixed_air_conditioning": "false" + }, + "sap_version": 10.2, + "sap_windows": [ + { + "pvc_frame": "true", + "orientation": 7, + "window_type": 1, + "glazing_type": 2, + "window_width": 1.7, + "window_height": 1.4, + "draught_proofed": "true", + "window_location": 0, + "window_wall_type": 1, + "permanent_shutters_present": "N", + "permanent_shutters_insulated": "N" + }, + { + "pvc_frame": "true", + "orientation": 7, + "window_type": 1, + "glazing_type": 2, + "window_width": 1.2, + "window_height": 1, + "draught_proofed": "true", + "window_location": 0, + "window_wall_type": 1, + "permanent_shutters_present": "N", + "permanent_shutters_insulated": "N" + }, + { + "pvc_frame": "true", + "orientation": 7, + "window_type": 1, + "glazing_type": 2, + "window_width": 0.6, + "window_height": 1, + "draught_proofed": "true", + "window_location": 0, + "window_wall_type": 1, + "permanent_shutters_present": "N", + "permanent_shutters_insulated": "N" + }, + { + "pvc_frame": "true", + "orientation": 3, + "window_type": 1, + "glazing_type": 2, + "window_width": 0.6, + "window_height": 0.7, + "draught_proofed": "true", + "window_location": 0, + "window_wall_type": 1, + "permanent_shutters_present": "N", + "permanent_shutters_insulated": "N" + }, + { + "pvc_frame": "true", + "orientation": 3, + "window_type": 1, + "glazing_type": 2, + "window_width": 0.6, + "window_height": 1, + "draught_proofed": "true", + "window_location": 0, + "window_wall_type": 1, + "permanent_shutters_present": "N", + "permanent_shutters_insulated": "N" + }, + { + "pvc_frame": "true", + "orientation": 3, + "window_type": 1, + "glazing_type": 2, + "window_width": 1.2, + "window_height": 1, + "draught_proofed": "true", + "window_location": 0, + "window_wall_type": 1, + "permanent_shutters_present": "N", + "permanent_shutters_insulated": "N" + }, + { + "pvc_frame": "true", + "orientation": 3, + "window_type": 1, + "glazing_type": 2, + "window_width": 1.2, + "window_height": 1, + "draught_proofed": "true", + "window_location": 0, + "window_wall_type": 1, + "permanent_shutters_present": "N", + "permanent_shutters_insulated": "N" + }, + { + "pvc_frame": "true", + "orientation": 3, + "window_type": 1, + "glazing_type": 2, + "window_width": 1.1, + "window_height": 0.8, + "draught_proofed": "true", + "window_location": 0, + "window_wall_type": 1, + "permanent_shutters_present": "N", + "permanent_shutters_insulated": "N" + }, + { + "pvc_frame": "true", + "orientation": 1, + "window_type": 1, + "glazing_type": 2, + "window_width": 0.6, + "window_height": 0.7, + "draught_proofed": "true", + "window_location": 0, + "window_wall_type": 1, + "permanent_shutters_present": "N", + "permanent_shutters_insulated": "N" + }, + { + "pvc_frame": "true", + "orientation": 1, + "window_type": 1, + "glazing_type": 2, + "window_width": 0.6, + "window_height": 1.1, + "draught_proofed": "true", + "window_location": 0, + "window_wall_type": 1, + "permanent_shutters_present": "N", + "permanent_shutters_insulated": "N" + } + ], + "schema_type": "RdSAP-Schema-21.0.1", + "uprn_source": "Energy Assessor", + "country_code": "ENG", + "main_heating": [ + { + "description": "Boiler and radiators, mains gas", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "air_tightness": { + "description": "(not tested)", + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + }, + "dwelling_type": "Semi-detached house", + "language_code": 1, + "pressure_test": 4, + "property_type": 0, + "address_line_1": "9 Woodgrove Road", + "assessment_type": "RdSAP", + "completion_date": "2026-05-27", + "inspection_date": "2026-05-27", + "extensions_count": 0, + "measurement_type": 1, + "total_floor_area": 63, + "transaction_type": 14, + "conservatory_type": 1, + "heated_room_count": 3, + "registration_date": "2026-05-27", + "sap_energy_source": { + "mains_gas": "Y", + "meter_type": 2, + "pv_connection": 0, + "photovoltaic_supply": { + "none_or_no_details": { + "percent_roof_area": 0 + } + }, + "wind_turbines_count": 0, + "gas_smart_meter_present": "true", + "is_dwelling_export_capable": "false", + "wind_turbines_terrain_type": 2, + "electricity_smart_meter_present": "true" + }, + "secondary_heating": { + "description": "None", + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + }, + "extract_fans_count": 2, + "sap_building_parts": [ + { + "identifier": "Main Dwelling", + "wall_dry_lined": "N", + "floor_heat_loss": 2, + "roof_construction": 4, + "wall_construction": 4, + "building_part_number": 1, + "sap_floor_dimensions": [ + { + "floor": 0, + "room_height": { + "value": 2.4, + "quantity": "metres" + }, + "floor_insulation": 1, + "total_floor_area": { + "value": 31.62, + "quantity": "square metres" + }, + "party_wall_length": { + "value": 6.2, + "quantity": "metres" + }, + "floor_construction": 2, + "heat_loss_perimeter": { + "value": 16.4, + "quantity": "metres" + } + }, + { + "floor": 1, + "room_height": { + "value": 2.41, + "quantity": "metres" + }, + "total_floor_area": { + "value": 31.62, + "quantity": "square metres" + }, + "party_wall_length": { + "value": 6.2, + "quantity": "metres" + }, + "heat_loss_perimeter": { + "value": 16.4, + "quantity": "metres" + } + } + ], + "wall_insulation_type": 2, + "construction_age_band": "B", + "party_wall_construction": 1, + "wall_thickness_measured": "N", + "roof_insulation_location": 2, + "roof_insulation_thickness": "225mm", + "wall_insulation_thickness": "NI", + "floor_insulation_thickness": "NI" + } + ], + "solar_water_heating": "N", + "habitable_room_count": 3, + "heating_cost_current": { + "value": 777, + "currency": "GBP" + }, + "insulated_door_count": 0, + "co2_emissions_current": 2.3, + "energy_rating_average": 60, + "energy_rating_current": 70, + "lighting_cost_current": { + "value": 79, + "currency": "GBP" + }, + "main_heating_controls": [ + { + "description": "Programmer, room thermostat and TRVs", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "has_hot_water_cylinder": "false", + "heating_cost_potential": { + "value": 671, + "currency": "GBP" + }, + "hot_water_cost_current": { + "value": 172, + "currency": "GBP" + }, + "mechanical_ventilation": 0, + "percent_draughtproofed": 100, + "suggested_improvements": [ + { + "sequence": 1, + "typical_saving": { + "value": 110, + "currency": "GBP" + }, + "indicative_cost": "\u00a35,000 - \u00a310,000", + "improvement_type": "W1", + "improvement_details": { + "improvement_number": 57 + }, + "improvement_category": 5, + "energy_performance_rating": 73, + "environmental_impact_rating": 75 + }, + { + "sequence": 2, + "typical_saving": { + "value": 30, + "currency": "GBP" + }, + "indicative_cost": "\u00a3120 - \u00a3140", + "improvement_type": "E", + "improvement_details": { + "improvement_number": 35 + }, + "improvement_category": 5, + "energy_performance_rating": 74, + "environmental_impact_rating": 75 + }, + { + "sequence": 3, + "typical_saving": { + "value": 184, + "currency": "GBP" + }, + "indicative_cost": "\u00a38,000 - \u00a310,000", + "improvement_type": "U", + "improvement_details": { + "improvement_number": 34 + }, + "improvement_category": 5, + "energy_performance_rating": 79, + "environmental_impact_rating": 77 + } + ], + "co2_emissions_potential": 1.9, + "energy_rating_potential": 79, + "lighting_cost_potential": { + "value": 45, + "currency": "GBP" + }, + "schema_version_original": "21.0.1", + "hot_water_cost_potential": { + "value": 172, + "currency": "GBP" + }, + "renewable_heat_incentive": { + "water_heating": 2070.91, + "space_heating_existing_dwelling": 7543.77 + }, + "draughtproofed_door_count": 2, + "energy_consumption_current": 205, + "has_fixed_air_conditioning": "false", + "multiple_glazed_proportion": 100, + "calculation_software_version": "5.02r0344", + "energy_consumption_potential": 161, + "environmental_impact_current": 71, + "current_energy_efficiency_band": "C", + "environmental_impact_potential": 77, + "has_heated_separate_conservatory": "false", + "potential_energy_efficiency_band": "C", + "co2_emissions_current_per_floor_area": 37, + "low_energy_fixed_lighting_bulbs_count": 4, + "incandescent_fixed_lighting_bulbs_count": 4 +} \ No newline at end of file diff --git a/scripts/hyde/elmhurst_lib.py b/scripts/hyde/elmhurst_lib.py index 25f9b0eb2..fb99966fb 100644 --- a/scripts/hyde/elmhurst_lib.py +++ b/scripts/hyde/elmhurst_lib.py @@ -278,24 +278,52 @@ def add_combined_window( break +def _delete_window_at_index(page: Page, i: int) -> bool: + """Delete the window grid row at DOM index `i` via its per-row Delete + image button + the Yes modal (JS-clicked to bypass the modal background + that intercepts real clicks). Returns True if a delete fired.""" + bid = ( + "ContentBody_ContentPlaceHolder1_TabContainer_TabPanelWindowsPanel_" + f"GridViewExtendedWidows_DeleteButton_{i}" + ) + if not page.evaluate("(id)=>!!document.getElementById(id)", bid): + return False + page.evaluate("(id)=>{const e=document.getElementById(id);if(e)e.click();}", bid) + page.wait_for_timeout(2000) + yid = "ContentBody_ContentPlaceHolder1_DeleteWindowDialog_LinkButtonYes" + if not page.evaluate( + "(id)=>{const e=document.getElementById(id);return e?e.offsetParent!==null:false;}", + yid, + ): + return False + page.evaluate("(id)=>{const e=document.getElementById(id);if(e)e.click();}", yid) + page.wait_for_timeout(2500) + return True + + 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.""" + """Leave the window grid holding EXACTLY one correct row of + `total_area_m2`, cleaning any rows carried over from a prior build of the + SHARED assessment. + + The grid can't be emptied (Elmhurst refuses to delete the last row) and it + has no inline edit, so per [[window-grid]] the reliable pattern is: ADD the + fresh window (it inserts at DOM index 0), then delete the STALE rows BELOW + it (indices high->low, never index 0) so the freshly-added row survives as + the single remaining window. Deleting index 0 (delete_first_window) would + drop the good row and strand a 0.00-area junk row that then blocks the + Recommendations gate.""" add_combined_window(page, total_area_m2, orientation, glazing) + page.wait_for_timeout(500) 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 + i = window_row_count(page) - 1 # bottom-most (oldest) stale row + if not _delete_window_at_index(page, i): + break def reset_transient_state(page: Page) -> None: diff --git a/tests/domain/sap10_calculator/test_real_cert_sap_accuracy.py b/tests/domain/sap10_calculator/test_real_cert_sap_accuracy.py index 9f3bce1b2..d7b72cb49 100644 --- a/tests/domain/sap10_calculator/test_real_cert_sap_accuracy.py +++ b/tests/domain/sap10_calculator/test_real_cert_sap_accuracy.py @@ -823,6 +823,25 @@ _EXPECTATIONS: Final[tuple[RealCertExpectation, ...]] = ( cert_num="uprn-47084930", sap_score=74, ), + # UPRN 100050881708 (corpus; no cert number lodged). RdSAP-21.0.1 native — + # SEMI-DETACHED HOUSE, band B, cavity FILLED wall, pitched 225 mm loft, + # SUSPENDED ground floor "to unheated space", mains-gas combi, no cylinder. + # Lodged 70 / engine 72 (71.55). Built in accredited Elmhurst RdSAP10 + # (evidence saved: elmhurst_summary.pdf / elmhurst_worksheet.pdf): the WALL + # (cavity, U 0.70, 69.56 m²) and ROOF (insulated slope 225 mm, U 0.19) + # match the engine EXACTLY (walls 48.8 W/K, roof 6.0 W/K). The engine is + # faithful on the checkable fabric; the +1.55 vs lodged is a small + # methodology gap. (Elmhurst''s worksheet SAP 61 is NOT comparable — the + # build entered the suspended ground floor as an upper "above unheated + # space" floor, which Elmhurst bills via Table-20 exposed 1.20; the cert''s + # §3.12 suspended-ground-floor U is 0.74, which the engine uses.) PINNED to + # the observed engine 72. + RealCertExpectation( + schema="RdSAP-Schema-21.0.1", + sample="uprn_100050881708", + cert_num="uprn-100050881708", + sap_score=72, + ), )