Merge remote-tracking branch 'origin/main' into fix/full-sap-export-capable-mapping

This commit is contained in:
Khalim Conn-Kowlessar 2026-07-03 12:11:25 +00:00
commit 325c0ea257
52 changed files with 12569 additions and 65 deletions

View file

@ -169,6 +169,34 @@ Pattern: `with E.session() as (ctx,page): E.goto(...); E.set_text/set_select(...
lines 17/18). This drove the first campaign mapper fix — see Banked findings.
## Banked findings (fold new ones in here as the corpus grows)
- **Register cost-triangulation (no Elmhurst needed):** the register lodges
`heating_cost_current` / `hot_water_cost_current` / `lighting_cost_current`
at RETAIL prices — absolute £ never match SAP Table-12 spec prices, but for
same-fuel end-uses the RATIOS localize a divergence without a worksheet.
Normalize by lighting: `(lodged_hw/lodged_light) / (our_hw/our_light)` ≈ 1
means our water cascade agrees with the lodging software; »1 flags the
divergent end-use. CAVEAT: the register's "hot water cost" INCLUDES the
SAP (64a) electric-shower electricity — add `instant_shower` cost to our
hw bucket before comparing on any electric-shower cert, else every
shower-outlet-type-2 cert false-positives at ratio ~2 (corpus-1000 band
triage, 36 certs, mean ratio 2.0 → 1.04 after adding (64a)).
- **Lodging-software conventions diverge from Elmhurst on two known axes**
(mark such corpus certs ⚠, don't tune): (a) out-of-range-PSR database
heat pumps — SAP 10.2 N2 footnotes 44/45 mandate the reciprocal extension
toward 100% at 2× the record's largest PSR (Elmhurst does this, golden
case 56); at least one software bills 100% direct electric + standard
schedule instead (corpus 4510053280, lodged 51 vs engine/Elmhurst-method
75). (b) a lodged 0 extract fans — Elmhurst renders 0 as "unknown" →
Table 5 age-band default (worksheet case 46); LIG-21.0 lodgements
(calculation_software_version 10.2.2.0) honour the 0 literally (corpus
quadruplet at Δ 2.56, fans account for 1.42).
- **"insulated (assumed)" roof ≠ observed retrofit:** RdSAP renders
"(assumed)" when insulation PRESENCE is an age-band assumption — Table 18
age default governs, for BOTH the "ND" and "NI" thickness sentinels
(which are lodging-software noise). The §5.11.4 50 mm row (U 0.68) is
only for the bare "insulated" description (observed retrofit, depth
unknown). Fixed in u_roof (corpus 74.2% → 75.5%); unit-pinned in
test_rdsap_uvalues.
- **MAPPER GAP — cylinder insulation thickness dropped (RdSAP-17.0+):** the mapper
carries `cylinder_size` + `cylinder_insulation_type` but NOT
`cylinder_insulation_thickness` → `EpcPropertyData.sap_heating.cylinder_

View file

@ -0,0 +1,510 @@
{
"uprn": 100040550095,
"roofs": [
{
"description": "Pitched, 175 mm loft insulation",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"walls": [
{
"description": "Granite or whin, as built, no insulation (assumed)",
"energy_efficiency_rating": 1,
"environmental_efficiency_rating": 1
},
{
"description": "Solid brick, as built, no insulation (assumed)",
"energy_efficiency_rating": 1,
"environmental_efficiency_rating": 1
}
],
"floors": [
{
"description": "Solid, no insulation (assumed)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"status": "entered",
"tenure": 3,
"window": {
"description": "Fully double glazed",
"energy_efficiency_rating": 2,
"environmental_efficiency_rating": 2
},
"addendum": {
"stone_walls": "true"
},
"lighting": {
"description": "Excellent lighting efficiency",
"energy_efficiency_rating": 5,
"environmental_efficiency_rating": 5
},
"postcode": "TQ1 2BW",
"hot_water": {
"description": "Electric immersion, standard tariff",
"energy_efficiency_rating": 1,
"environmental_efficiency_rating": 5
},
"post_town": "TORQUAY",
"built_form": 4,
"created_at": "2026-05-20 13:21:34",
"door_count": 2,
"region_code": 15,
"report_type": 2,
"sap_heating": {
"number_baths": 1,
"cylinder_size": 2,
"shower_outlets": [
{
"shower_wwhrs": 1,
"shower_outlet_type": 2
}
],
"number_baths_wwhrs": 0,
"water_heating_code": 903,
"water_heating_fuel": 29,
"main_heating_details": [
{
"has_fghrs": "N",
"main_fuel_type": 29,
"heat_emitter_type": 0,
"emitter_temperature": "NA",
"main_heating_number": 1,
"main_heating_control": 2603,
"main_heating_category": 10,
"main_heating_fraction": 1,
"sap_main_heating_code": 691,
"main_heating_data_source": 2
}
],
"immersion_heating_type": 2,
"cylinder_insulation_type": 2,
"has_fixed_air_conditioning": "false",
"cylinder_insulation_thickness": 38
},
"sap_version": 10.2,
"sap_windows": [
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 1,
"window_type": 1,
"glazing_type": 3,
"window_width": 0.91,
"window_height": 1.64,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 1,
"window_type": 1,
"glazing_type": 3,
"window_width": 0.91,
"window_height": 1.64,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 1,
"window_type": 1,
"glazing_type": 3,
"window_width": 0.62,
"window_height": 0.91,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 5,
"window_type": 1,
"glazing_type": 3,
"window_width": 1.2,
"window_height": 0.88,
"draught_proofed": "true",
"window_location": 1,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 5,
"window_type": 1,
"glazing_type": 3,
"window_width": 1.02,
"window_height": 0.82,
"draught_proofed": "true",
"window_location": 1,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 5,
"window_type": 1,
"glazing_type": 3,
"window_width": 0.6,
"window_height": 1.14,
"draught_proofed": "true",
"window_location": 1,
"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": "Room heaters, electric",
"energy_efficiency_rating": 1,
"environmental_efficiency_rating": 5
}
],
"air_tightness": {
"description": "(not tested)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"dwelling_type": "Mid-terrace house",
"language_code": 1,
"pressure_test": 4,
"property_type": 0,
"address_line_1": "11 Meadfoot Lane",
"assessment_type": "RdSAP",
"completion_date": "2026-05-20",
"inspection_date": "2026-05-20",
"extensions_count": 1,
"measurement_type": 1,
"total_floor_area": 59,
"transaction_type": 8,
"conservatory_type": 1,
"heated_room_count": 3,
"registration_date": "2026-05-20",
"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": 1,
"sap_building_parts": [
{
"identifier": "Main Dwelling",
"wall_dry_lined": "N",
"wall_thickness": 510,
"floor_heat_loss": 7,
"roof_construction": 4,
"wall_construction": 1,
"building_part_number": 1,
"sap_floor_dimensions": [
{
"floor": 0,
"room_height": {
"value": 2.31,
"quantity": "metres"
},
"floor_insulation": 1,
"total_floor_area": {
"value": 17.29,
"quantity": "square metres"
},
"party_wall_length": {
"value": 7.27,
"quantity": "metres"
},
"floor_construction": 1,
"heat_loss_perimeter": {
"value": 4.72,
"quantity": "metres"
}
},
{
"floor": 1,
"room_height": {
"value": 2.35,
"quantity": "metres"
},
"total_floor_area": {
"value": 17.29,
"quantity": "square metres"
},
"party_wall_length": {
"value": 7.27,
"quantity": "metres"
},
"heat_loss_perimeter": {
"value": 4.72,
"quantity": "metres"
}
}
],
"wall_insulation_type": 4,
"construction_age_band": "A",
"party_wall_construction": 1,
"wall_thickness_measured": "Y",
"roof_insulation_location": 2,
"roof_insulation_thickness": "175mm",
"wall_insulation_thickness": "NI",
"floor_insulation_thickness": "NI"
},
{
"identifier": "Extension 1",
"wall_dry_lined": "N",
"wall_thickness": 185,
"floor_heat_loss": 7,
"sap_room_in_roof": {
"floor_area": 12.24,
"room_in_roof_type_2": {
"gable_wall_type_1": 1,
"gable_wall_type_2": 3,
"gable_wall_height_1": 2.5,
"gable_wall_height_2": 2.5,
"gable_wall_length_1": 7.39,
"gable_wall_length_2": 7.39,
"common_wall_height_1": 1.5,
"common_wall_height_2": 1.5,
"common_wall_length_1": 1.4,
"common_wall_length_2": 1.4
},
"construction_age_band": "A"
},
"roof_construction": 5,
"wall_construction": 3,
"building_part_number": 2,
"sap_floor_dimensions": [
{
"floor": 0,
"room_height": {
"value": 2.27,
"quantity": "metres"
},
"floor_insulation": 1,
"total_floor_area": {
"value": 12.24,
"quantity": "square metres"
},
"party_wall_length": {
"value": 7.39,
"quantity": "metres"
},
"floor_construction": 1,
"heat_loss_perimeter": {
"value": 2.8,
"quantity": "metres"
}
}
],
"wall_insulation_type": 4,
"construction_age_band": "A",
"party_wall_construction": 0,
"wall_thickness_measured": "Y",
"roof_insulation_location": 4,
"roof_insulation_thickness": "ND",
"wall_insulation_thickness": "NI",
"floor_insulation_thickness": "NI"
}
],
"solar_water_heating": "N",
"habitable_room_count": 3,
"heating_cost_current": {
"value": 1266,
"currency": "GBP"
},
"insulated_door_count": 0,
"co2_emissions_current": 1.1,
"energy_rating_average": 60,
"energy_rating_current": 40,
"lighting_cost_current": {
"value": 43,
"currency": "GBP"
},
"main_heating_controls": [
{
"description": "Programmer and appliance thermostats",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"has_hot_water_cylinder": "true",
"heating_cost_potential": {
"value": 366,
"currency": "GBP"
},
"hot_water_cost_current": {
"value": 679,
"currency": "GBP"
},
"mechanical_ventilation": 0,
"percent_draughtproofed": 100,
"suggested_improvements": [
{
"sequence": 1,
"typical_saving": {
"value": 449,
"currency": "GBP"
},
"indicative_cost": "\u00a37,500 - \u00a311,000",
"improvement_type": "Q",
"improvement_details": {
"improvement_number": 7
},
"improvement_category": 5,
"energy_performance_rating": 55,
"environmental_impact_rating": 88
},
{
"sequence": 2,
"typical_saving": {
"value": 57,
"currency": "GBP"
},
"indicative_cost": "\u00a35,000 - \u00a310,000",
"improvement_type": "W2",
"improvement_details": {
"improvement_number": 58
},
"improvement_category": 5,
"energy_performance_rating": 56,
"environmental_impact_rating": 89
},
{
"sequence": 3,
"typical_saving": {
"value": 100,
"currency": "GBP"
},
"indicative_cost": "\u00a320 - \u00a340",
"improvement_type": "C",
"improvement_details": {
"improvement_number": 2
},
"improvement_category": 5,
"energy_performance_rating": 59,
"environmental_impact_rating": 89
},
{
"sequence": 4,
"typical_saving": {
"value": 671,
"currency": "GBP"
},
"indicative_cost": "\u00a33,500 - \u00a310,000",
"improvement_type": "T",
"improvement_details": {
"improvement_number": 29
},
"improvement_category": 5,
"energy_performance_rating": 76,
"environmental_impact_rating": 83
},
{
"sequence": 5,
"typical_saving": {
"value": 220,
"currency": "GBP"
},
"indicative_cost": "\u00a38,000 - \u00a310,000",
"improvement_type": "U",
"improvement_details": {
"improvement_number": 34
},
"improvement_category": 5,
"energy_performance_rating": 81,
"environmental_impact_rating": 84
}
],
"co2_emissions_potential": 1.1,
"energy_rating_potential": 81,
"lighting_cost_potential": {
"value": 43,
"currency": "GBP"
},
"schema_version_original": "21.0.1",
"alternative_improvements": [
{
"sequence": 1,
"typical_saving": {
"value": 822,
"currency": "GBP"
},
"improvement_type": "J2",
"improvement_details": {
"improvement_number": 54
},
"improvement_category": 6,
"energy_performance_rating": 79,
"environmental_impact_rating": 96
},
{
"sequence": 2,
"typical_saving": {
"value": 524,
"currency": "GBP"
},
"improvement_type": "Z3",
"improvement_details": {
"improvement_number": 53
},
"improvement_category": 6,
"energy_performance_rating": 71,
"environmental_impact_rating": 74
}
],
"hot_water_cost_potential": {
"value": 301,
"currency": "GBP"
},
"renewable_heat_incentive": {
"water_heating": 2452.35,
"space_heating_existing_dwelling": 4576.09
},
"draughtproofed_door_count": 2,
"energy_consumption_current": 189,
"has_fixed_air_conditioning": "false",
"multiple_glazed_proportion": 100,
"calculation_software_version": "5.02r0344",
"energy_consumption_potential": 102,
"environmental_impact_current": 84,
"current_energy_efficiency_band": "E",
"environmental_impact_potential": 84,
"led_fixed_lighting_bulbs_count": 7,
"has_heated_separate_conservatory": "false",
"potential_energy_efficiency_band": "B",
"co2_emissions_current_per_floor_area": 18,
"incandescent_fixed_lighting_bulbs_count": 0
}

View file

@ -0,0 +1,700 @@
{
"uprn": 100050355518,
"roofs": [
{
"description": "Pitched, 200 mm loft insulation",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
},
{
"description": "Pitched, limited insulation",
"energy_efficiency_rating": 1,
"environmental_efficiency_rating": 1
}
],
"walls": [
{
"description": "Cavity wall, as built, no insulation (assumed)",
"energy_efficiency_rating": 2,
"environmental_efficiency_rating": 2
}
],
"floors": [
{
"description": "Suspended, no insulation (assumed)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"status": "entered",
"tenure": 1,
"window": {
"description": "Partial double glazing",
"energy_efficiency_rating": 1,
"environmental_efficiency_rating": 1
},
"addendum": {
"addendum_numbers": [
15
],
"cavity_fill_recommended": "true"
},
"lighting": {
"description": "Below average lighting efficiency",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
},
"postcode": "BD23 1BG",
"hot_water": {
"description": "From main system, no cylinder thermostat",
"energy_efficiency_rating": 2,
"environmental_efficiency_rating": 2
},
"post_town": "SKIPTON",
"psv_count": 0,
"built_form": 2,
"created_at": "2026-05-18 08:00:25",
"door_count": 1,
"region_code": 7,
"report_type": 2,
"sap_heating": {
"number_baths": 1,
"cylinder_size": 2,
"shower_outlets": [
{
"shower_wwhrs": 1,
"shower_outlet_type": 2
},
{
"shower_wwhrs": 1,
"shower_outlet_type": 2
}
],
"water_heating_code": 901,
"water_heating_fuel": 26,
"cylinder_thermostat": "N",
"secondary_fuel_type": 5,
"main_heating_details": [
{
"has_fghrs": "N",
"main_fuel_type": 26,
"boiler_flue_type": 2,
"fan_flue_present": "N",
"heat_emitter_type": 1,
"main_heating_number": 1,
"main_heating_control": 2102,
"main_heating_category": 2,
"main_heating_fraction": 1,
"sap_main_heating_code": 117,
"central_heating_pump_age": 0,
"main_heating_data_source": 2
}
],
"immersion_heating_type": "NA",
"secondary_heating_type": 631,
"cylinder_insulation_type": 2,
"has_fixed_air_conditioning": "false",
"cylinder_insulation_thickness": 80
},
"sap_version": 10.2,
"sap_windows": [
{
"pvc_frame": "true",
"orientation": 1,
"window_type": 1,
"glazing_type": 5,
"window_width": 0.4,
"window_height": 1.18,
"draught_proofed": "false",
"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": 5,
"window_width": 5.03,
"window_height": 1.72,
"draught_proofed": "false",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 5,
"window_type": 1,
"glazing_type": 3,
"window_width": 1.19,
"window_height": 1,
"draught_proofed": "false",
"window_location": 2,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 5,
"window_type": 1,
"glazing_type": 3,
"window_width": 1.74,
"window_height": 2.05,
"draught_proofed": "false",
"window_location": 2,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 5,
"window_type": 2,
"glazing_type": 3,
"window_width": 0.6,
"window_height": 0.9,
"draught_proofed": "false",
"window_location": 2,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 5,
"window_type": 2,
"glazing_type": 3,
"window_width": 0.6,
"window_height": 0.9,
"draught_proofed": "false",
"window_location": 2,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 1,
"window_type": 1,
"glazing_type": 3,
"window_width": 2.34,
"window_height": 1.18,
"draught_proofed": "false",
"window_location": 1,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 5,
"window_type": 1,
"glazing_type": 3,
"window_width": 1.79,
"window_height": 2.06,
"draught_proofed": "false",
"window_location": 2,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 1,
"window_type": 1,
"glazing_type": 5,
"window_width": 5.03,
"window_height": 1.23,
"draught_proofed": "false",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 1,
"window_type": 1,
"glazing_type": 5,
"window_width": 1.11,
"window_height": 1.17,
"draught_proofed": "false",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 5,
"window_type": 1,
"glazing_type": 3,
"window_width": 1.19,
"window_height": 1,
"draught_proofed": "false",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 1,
"window_type": 1,
"glazing_type": 3,
"window_width": 1.18,
"window_height": 1.15,
"draught_proofed": "false",
"window_location": 1,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 5,
"window_type": 1,
"glazing_type": 3,
"window_width": 1.1,
"window_height": 1,
"draught_proofed": "false",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 5,
"window_type": 1,
"glazing_type": 3,
"window_width": 1.66,
"window_height": 1.17,
"draught_proofed": "false",
"window_location": 1,
"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": "26 Regent Crescent",
"assessment_type": "RdSAP",
"completion_date": "2026-05-18",
"inspection_date": "2026-05-15",
"extensions_count": 2,
"measurement_type": 1,
"open_flues_count": 0,
"total_floor_area": 122,
"transaction_type": 15,
"conservatory_type": 1,
"has_draught_lobby": "true",
"heated_room_count": 5,
"other_flues_count": 0,
"registration_date": "2026-05-18",
"sap_energy_source": {
"mains_gas": "Y",
"meter_type": 2,
"pv_diverter": "false",
"pv_connection": 0,
"pv_battery_count": 0,
"photovoltaic_supply": {
"none_or_no_details": {
"percent_roof_area": 0
}
},
"wind_turbines_count": 0,
"gas_smart_meter_present": "false",
"is_dwelling_export_capable": "false",
"wind_turbines_terrain_type": 2,
"electricity_smart_meter_present": "false",
"is_hydro_output_connected_to_dwelling_meter": "false"
},
"secondary_heating": {
"description": "Room heaters, anthracite",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"closed_flues_count": 0,
"extract_fans_count": 1,
"sap_building_parts": [
{
"identifier": "Main Dwelling",
"wall_dry_lined": "N",
"wall_thickness": 230,
"floor_heat_loss": 7,
"roof_construction": 4,
"wall_construction": 4,
"building_part_number": 1,
"sap_floor_dimensions": [
{
"floor": 0,
"room_height": {
"value": 2.53,
"quantity": "metres"
},
"floor_insulation": 1,
"total_floor_area": {
"value": 35.63,
"quantity": "square metres"
},
"party_wall_length": {
"value": 6.09,
"quantity": "metres"
},
"floor_construction": 2,
"heat_loss_perimeter": {
"value": 7.95,
"quantity": "metres"
}
},
{
"floor": 1,
"room_height": {
"value": 2.38,
"quantity": "metres"
},
"total_floor_area": {
"value": 35.63,
"quantity": "square metres"
},
"party_wall_length": {
"value": 6.09,
"quantity": "metres"
},
"heat_loss_perimeter": {
"value": 13.37,
"quantity": "metres"
}
}
],
"wall_insulation_type": 4,
"construction_age_band": "C",
"party_wall_construction": 0,
"wall_thickness_measured": "Y",
"roof_insulation_location": 2,
"roof_insulation_thickness": "200mm",
"wall_insulation_thickness": "NI"
},
{
"identifier": "Extension 1",
"wall_dry_lined": "N",
"wall_thickness": 320,
"floor_heat_loss": 7,
"roof_construction": 4,
"wall_construction": 4,
"building_part_number": 2,
"sap_floor_dimensions": [
{
"floor": 0,
"room_height": {
"value": 2.29,
"quantity": "metres"
},
"floor_insulation": 1,
"total_floor_area": {
"value": 15.62,
"quantity": "square metres"
},
"party_wall_length": 0,
"floor_construction": 2,
"heat_loss_perimeter": {
"value": 7.95,
"quantity": "metres"
}
},
{
"floor": 1,
"room_height": {
"value": 2.24,
"quantity": "metres"
},
"total_floor_area": {
"value": 15.62,
"quantity": "square metres"
},
"party_wall_length": 0,
"heat_loss_perimeter": {
"value": 11.19,
"quantity": "metres"
}
}
],
"wall_insulation_type": 4,
"construction_age_band": "C",
"party_wall_construction": "NA",
"wall_thickness_measured": "Y",
"roof_insulation_location": 2,
"roof_insulation_thickness": "200mm",
"wall_insulation_thickness": "NI"
},
{
"identifier": "Extension 2",
"wall_dry_lined": "N",
"wall_thickness": 320,
"floor_heat_loss": 7,
"roof_construction": 8,
"wall_construction": 4,
"building_part_number": 3,
"sap_floor_dimensions": [
{
"floor": 0,
"room_height": {
"value": 2.86,
"quantity": "metres"
},
"floor_insulation": 1,
"total_floor_area": {
"value": 19.28,
"quantity": "square metres"
},
"party_wall_length": 0,
"floor_construction": 2,
"heat_loss_perimeter": {
"value": 12.66,
"quantity": "metres"
}
}
],
"wall_insulation_type": 4,
"construction_age_band": "C",
"party_wall_construction": "NA",
"wall_thickness_measured": "Y",
"roof_insulation_location": 7,
"wall_insulation_thickness": "NI",
"sloping_ceiling_insulation_thickness": "AB"
}
],
"boilers_flues_count": 0,
"open_chimneys_count": 0,
"solar_water_heating": "N",
"habitable_room_count": 5,
"heating_cost_current": {
"value": 3523,
"currency": "GBP"
},
"insulated_door_count": 0,
"co2_emissions_current": 12,
"energy_rating_average": 60,
"energy_rating_current": 36,
"lighting_cost_current": {
"value": 84,
"currency": "GBP"
},
"main_heating_controls": [
{
"description": "Programmer, no room thermostat",
"energy_efficiency_rating": 1,
"environmental_efficiency_rating": 1
}
],
"blocked_chimneys_count": 1,
"has_hot_water_cylinder": "true",
"heating_cost_potential": {
"value": 1387,
"currency": "GBP"
},
"hot_water_cost_current": {
"value": 574,
"currency": "GBP"
},
"mechanical_ventilation": 0,
"percent_draughtproofed": 0,
"suggested_improvements": [
{
"sequence": 1,
"typical_saving": 260,
"indicative_cost": "\u00a3900 - \u00a31,200",
"improvement_type": "A2",
"improvement_details": {
"improvement_number": 45
},
"improvement_category": 5,
"energy_performance_rating": 39,
"environmental_impact_rating": 30
},
{
"sequence": 2,
"typical_saving": 606,
"indicative_cost": "\u00a3900 - \u00a31,500",
"improvement_type": "B",
"improvement_details": {
"improvement_number": 6
},
"improvement_category": 5,
"energy_performance_rating": 47,
"environmental_impact_rating": 38
},
{
"sequence": 3,
"typical_saving": 251,
"indicative_cost": "\u00a35,000 - \u00a310,000",
"improvement_type": "W1",
"improvement_details": {
"improvement_number": 57
},
"improvement_category": 5,
"energy_performance_rating": 51,
"environmental_impact_rating": 42
},
{
"sequence": 4,
"typical_saving": 134,
"indicative_cost": "\u00a3150 - \u00a3250",
"improvement_type": "D",
"improvement_details": {
"improvement_number": 10
},
"improvement_category": 5,
"energy_performance_rating": 53,
"environmental_impact_rating": 44
},
{
"sequence": 5,
"typical_saving": 276,
"indicative_cost": "\u00a3220 - \u00a3250",
"improvement_type": "G",
"improvement_details": {
"improvement_number": 12
},
"improvement_category": 5,
"energy_performance_rating": 57,
"environmental_impact_rating": 49
},
{
"sequence": 6,
"typical_saving": 629,
"indicative_cost": "\u00a32,200 - \u00a33,500",
"improvement_type": "I",
"improvement_details": {
"improvement_number": 20
},
"improvement_category": 5,
"energy_performance_rating": 67,
"environmental_impact_rating": 61
},
{
"sequence": 7,
"typical_saving": 197,
"indicative_cost": "\u00a34,500 - \u00a36,000",
"improvement_type": "O",
"improvement_details": {
"improvement_number": 8
},
"improvement_category": 5,
"energy_performance_rating": 70,
"environmental_impact_rating": 66
},
{
"sequence": 8,
"typical_saving": 233,
"indicative_cost": "\u00a38,000 - \u00a310,000",
"improvement_type": "U",
"improvement_details": {
"improvement_number": 34
},
"improvement_category": 5,
"energy_performance_rating": 74,
"environmental_impact_rating": 67
}
],
"co2_emissions_potential": 4.6,
"energy_rating_potential": 74,
"lighting_cost_potential": {
"value": 84,
"currency": "GBP"
},
"schema_version_original": "LIG-21.0.1",
"alternative_improvements": [
{
"sequence": 1,
"typical_saving": 172,
"improvement_type": "Q2",
"improvement_details": {
"improvement_number": 55
},
"improvement_category": 6,
"energy_performance_rating": 50,
"environmental_impact_rating": 40
},
{
"sequence": 2,
"typical_saving": 774,
"improvement_type": "J2",
"improvement_details": {
"improvement_number": 54
},
"improvement_category": 6,
"energy_performance_rating": 64,
"environmental_impact_rating": 82
},
{
"sequence": 3,
"typical_saving": 828,
"improvement_type": "Z3",
"improvement_details": {
"improvement_number": 53
},
"improvement_category": 6,
"energy_performance_rating": 69,
"environmental_impact_rating": 66
}
],
"flueless_gas_fires_count": 0,
"hot_water_cost_potential": {
"value": 358,
"currency": "GBP"
},
"renewable_heat_incentive": {
"water_heating": 3736.92,
"space_heating_existing_dwelling": 24523.93
},
"draughtproofed_door_count": 0,
"energy_consumption_current": 466,
"has_fixed_air_conditioning": "false",
"multiple_glazed_proportion": 52,
"calculation_software_version": "4.3.74",
"energy_consumption_potential": 170,
"environmental_impact_current": 27,
"current_energy_efficiency_band": "F",
"environmental_impact_potential": 67,
"has_heated_separate_conservatory": "false",
"potential_energy_efficiency_band": "C",
"co2_emissions_current_per_floor_area": 98,
"low_energy_fixed_lighting_bulbs_count": 17,
"incandescent_fixed_lighting_bulbs_count": 4
}

View file

@ -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
}

View file

@ -0,0 +1,351 @@
{
"uprn": 100051051866,
"roofs": [
{
"description": "Pitched, insulated (assumed)",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"walls": [
{
"description": "Cavity wall, as built, insulated (assumed)",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"floors": [
{
"description": "Suspended, limited insulation (assumed)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"status": "entered",
"tenure": 1,
"window": {
"description": "Fully double glazed",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
},
"addendum": {
"addendum_numbers": [
15
]
},
"lighting": {
"description": "Excellent lighting efficiency",
"energy_efficiency_rating": 5,
"environmental_efficiency_rating": 5
},
"postcode": "S20 7NQ",
"hot_water": {
"description": "From main system",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
},
"post_town": "SHEFFIELD",
"psv_count": 0,
"built_form": 1,
"created_at": "2026-05-18 17:37:45",
"door_count": 1,
"region_code": 11,
"report_type": 2,
"sap_heating": {
"number_baths": 1,
"cylinder_size": 2,
"shower_outlets": [
{
"shower_wwhrs": 1,
"shower_outlet_type": 4
}
],
"number_baths_wwhrs": 0,
"water_heating_code": 901,
"water_heating_fuel": 26,
"cylinder_thermostat": "Y",
"main_heating_details": [
{
"has_fghrs": "N",
"main_fuel_type": 26,
"boiler_flue_type": 2,
"fan_flue_present": "Y",
"heat_emitter_type": 1,
"emitter_temperature": 1,
"main_heating_number": 1,
"main_heating_control": 2104,
"main_heating_category": 2,
"main_heating_fraction": 1,
"sap_main_heating_code": 102,
"central_heating_pump_age": 0,
"main_heating_data_source": 2,
"main_heating_index_number": 18773
}
],
"immersion_heating_type": "NA",
"cylinder_insulation_type": 1,
"has_fixed_air_conditioning": "false",
"cylinder_insulation_thickness": 50
},
"sap_version": 10.2,
"sap_windows": [
{
"pvc_frame": "true",
"orientation": 7,
"window_type": 1,
"glazing_type": 2,
"window_width": {
"value": 1.6,
"quantity": "m"
},
"window_height": {
"value": 1.2,
"quantity": "m"
},
"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": {
"value": 1.2,
"quantity": "m"
},
"window_height": {
"value": 1.2,
"quantity": "m"
},
"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": {
"value": 1.6,
"quantity": "m"
},
"window_height": {
"value": 1.6,
"quantity": "m"
},
"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": {
"value": 1.6,
"quantity": "m"
},
"window_height": {
"value": 1.2,
"quantity": "m"
},
"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": {
"value": 0.5,
"quantity": "m"
},
"window_height": {
"value": 1.2,
"quantity": "m"
},
"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": "Detached bungalow",
"language_code": 1,
"pressure_test": 4,
"property_type": 1,
"address_line_1": "3 Pentland Gardens",
"address_line_2": "Waterthorpe",
"assessment_type": "RdSAP",
"completion_date": "2026-05-18",
"inspection_date": "2026-05-18",
"extensions_count": 0,
"measurement_type": 1,
"open_flues_count": 0,
"total_floor_area": 60,
"transaction_type": 1,
"conservatory_type": 2,
"heated_room_count": 4,
"other_flues_count": 0,
"registration_date": "2026-05-18",
"sap_energy_source": {
"mains_gas": "Y",
"meter_type": 3,
"pv_connection": 0,
"photovoltaic_supply": {
"none_or_no_details": {
"percent_roof_area": 0
}
},
"wind_turbines_count": 0,
"gas_smart_meter_present": "false",
"is_dwelling_export_capable": "false",
"wind_turbines_terrain_type": 2,
"electricity_smart_meter_present": "false"
},
"secondary_heating": {
"description": "None",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"closed_flues_count": 0,
"extract_fans_count": 0,
"sap_building_parts": [
{
"identifier": "Main Dwelling",
"wall_dry_lined": "N",
"wall_thickness": 300,
"floor_heat_loss": 7,
"roof_construction": 4,
"wall_construction": 4,
"building_part_number": 1,
"sap_floor_dimensions": [
{
"floor": 0,
"room_height": 2.4,
"floor_insulation": 1,
"total_floor_area": 60,
"party_wall_length": 0,
"floor_construction": 2,
"heat_loss_perimeter": 32
}
],
"wall_insulation_type": 4,
"construction_age_band": "I",
"party_wall_construction": "NA",
"wall_thickness_measured": "Y",
"roof_insulation_location": 4,
"roof_insulation_thickness": "NI"
}
],
"boilers_flues_count": 0,
"open_chimneys_count": 0,
"solar_water_heating": "N",
"habitable_room_count": 4,
"heating_cost_current": {
"value": 701,
"currency": "GBP"
},
"insulated_door_count": 0,
"co2_emissions_current": 2.3,
"energy_rating_average": 60,
"energy_rating_current": 70,
"lighting_cost_current": {
"value": 40,
"currency": "GBP"
},
"main_heating_controls": [
{
"description": "Programmer and room thermostat",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
}
],
"blocked_chimneys_count": 0,
"has_hot_water_cylinder": "true",
"heating_cost_potential": {
"value": 701,
"currency": "GBP"
},
"hot_water_cost_current": {
"value": 235,
"currency": "GBP"
},
"mechanical_ventilation": 0,
"percent_draughtproofed": 100,
"schema_version_current": "LIG-21.0",
"suggested_improvements": [
{
"sequence": 1,
"typical_saving": 238,
"indicative_cost": "\u00a38,000 - \u00a310,000",
"improvement_type": "U",
"improvement_details": {
"improvement_number": 34
},
"improvement_category": 5,
"energy_performance_rating": 82,
"environmental_impact_rating": 73
}
],
"co2_emissions_potential": 2.1,
"energy_rating_potential": 82,
"lighting_cost_potential": {
"value": 40,
"currency": "GBP"
},
"schema_version_original": "LIG-21.0",
"hot_water_cost_potential": {
"value": 235,
"currency": "GBP"
},
"renewable_heat_incentive": {
"water_heating": 2544.67,
"space_heating_existing_dwelling": 6292.1
},
"draughtproofed_door_count": 1,
"energy_consumption_current": 209,
"has_fixed_air_conditioning": "false",
"multiple_glazed_proportion": 100,
"calculation_software_version": "10.2.2.0",
"energy_consumption_potential": 185,
"environmental_impact_current": 70,
"cfl_fixed_lighting_bulbs_count": 0,
"current_energy_efficiency_band": "C",
"environmental_impact_potential": 73,
"led_fixed_lighting_bulbs_count": 8,
"has_heated_separate_conservatory": "false",
"potential_energy_efficiency_band": "B",
"co2_emissions_current_per_floor_area": 38,
"incandescent_fixed_lighting_bulbs_count": 0
}

View file

@ -0,0 +1,410 @@
{
"uprn": 100061275133,
"roofs": [
{
"description": "Pitched, 300 mm loft insulation",
"energy_efficiency_rating": 5,
"environmental_efficiency_rating": 5
}
],
"walls": [
{
"description": "Solid brick, with external insulation",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"floors": [
{
"description": "Suspended, no insulation (assumed)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"status": "entered",
"tenure": 2,
"window": {
"description": "High performance glazing",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
},
"lighting": {
"description": "Excellent lighting efficiency",
"energy_efficiency_rating": 5,
"environmental_efficiency_rating": 5
},
"postcode": "ME7 2ES",
"hot_water": {
"description": "From main system",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
},
"post_town": "GILLINGHAM",
"built_form": 2,
"created_at": "2026-05-20 08:39:48",
"door_count": 2,
"region_code": 14,
"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": 18908
}
],
"immersion_heating_type": "NA",
"has_fixed_air_conditioning": "false"
},
"sap_version": 10.2,
"sap_windows": [
{
"orientation": 8,
"window_type": 1,
"glazing_type": 13,
"window_width": 1.19,
"window_height": 1.13,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"orientation": 8,
"window_type": 1,
"glazing_type": 13,
"window_width": 0.57,
"window_height": 0.55,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"orientation": 8,
"window_type": 1,
"glazing_type": 13,
"window_width": 1.19,
"window_height": 1.11,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"orientation": 2,
"window_type": 1,
"glazing_type": 13,
"window_width": 1.05,
"window_height": 0.85,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"orientation": 2,
"window_type": 1,
"glazing_type": 13,
"window_width": 0.57,
"window_height": 0.56,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"orientation": 4,
"window_type": 1,
"glazing_type": 13,
"window_width": 2.11,
"window_height": 1.13,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"orientation": 4,
"window_type": 1,
"glazing_type": 13,
"window_width": 1.8,
"window_height": 0.97,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"orientation": 4,
"window_type": 1,
"glazing_type": 13,
"window_width": 1.8,
"window_height": 0.97,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"orientation": 4,
"window_type": 1,
"glazing_type": 13,
"window_width": 1.2,
"window_height": 0.99,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"orientation": 8,
"window_type": 1,
"glazing_type": 13,
"window_width": 1.8,
"window_height": 0.97,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"orientation": 8,
"window_type": 1,
"glazing_type": 13,
"window_width": 1.19,
"window_height": 0.97,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"orientation": 8,
"window_type": 1,
"glazing_type": 13,
"window_width": 1.18,
"window_height": 0.99,
"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": "75 Toronto Road",
"assessment_type": "RdSAP",
"completion_date": "2026-05-20",
"inspection_date": "2026-05-19",
"extensions_count": 0,
"measurement_type": 1,
"total_floor_area": 78,
"transaction_type": 8,
"conservatory_type": 1,
"heated_room_count": 3,
"registration_date": "2026-05-20",
"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
},
"sap_building_parts": [
{
"identifier": "Main Dwelling",
"wall_dry_lined": "N",
"wall_thickness": 400,
"floor_heat_loss": 7,
"roof_construction": 4,
"wall_construction": 3,
"building_part_number": 1,
"sap_floor_dimensions": [
{
"floor": 0,
"room_height": {
"value": 2.4,
"quantity": "metres"
},
"floor_insulation": 1,
"total_floor_area": {
"value": 38.99,
"quantity": "square metres"
},
"party_wall_length": {
"value": 5.09,
"quantity": "metres"
},
"floor_construction": 0,
"heat_loss_perimeter": {
"value": 17.92,
"quantity": "metres"
}
},
{
"floor": 1,
"room_height": {
"value": 2.4,
"quantity": "metres"
},
"total_floor_area": {
"value": 38.99,
"quantity": "square metres"
},
"party_wall_length": {
"value": 5.09,
"quantity": "metres"
},
"heat_loss_perimeter": {
"value": 17.92,
"quantity": "metres"
}
}
],
"wall_insulation_type": 1,
"construction_age_band": "B",
"party_wall_construction": 1,
"wall_thickness_measured": "Y",
"roof_insulation_location": 2,
"roof_insulation_thickness": "300mm",
"wall_insulation_thickness": "100mm",
"floor_insulation_thickness": "NI"
}
],
"solar_water_heating": "N",
"habitable_room_count": 3,
"heating_cost_current": {
"value": 608,
"currency": "GBP"
},
"insulated_door_count": 0,
"co2_emissions_current": 1.9,
"energy_rating_average": 60,
"energy_rating_current": 76,
"lighting_cost_current": {
"value": 58,
"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": 608,
"currency": "GBP"
},
"hot_water_cost_current": {
"value": 182,
"currency": "GBP"
},
"mechanical_ventilation": 0,
"percent_draughtproofed": 100,
"suggested_improvements": [
{
"sequence": 1,
"typical_saving": {
"value": 216,
"currency": "GBP"
},
"indicative_cost": "\u00a38,000 - \u00a310,000",
"improvement_type": "U",
"improvement_details": {
"improvement_number": 34
},
"improvement_category": 5,
"energy_performance_rating": 81,
"environmental_impact_rating": 79
}
],
"co2_emissions_potential": 1.8,
"energy_rating_potential": 81,
"lighting_cost_potential": {
"value": 58,
"currency": "GBP"
},
"schema_version_original": "21.0.1",
"hot_water_cost_potential": {
"value": 182,
"currency": "GBP"
},
"renewable_heat_incentive": {
"water_heating": 2203.16,
"space_heating_existing_dwelling": 5517.85
},
"draughtproofed_door_count": 2,
"energy_consumption_current": 133,
"has_fixed_air_conditioning": "false",
"multiple_glazed_proportion": 100,
"calculation_software_version": "5.02r0344",
"energy_consumption_potential": 118,
"environmental_impact_current": 78,
"current_energy_efficiency_band": "C",
"environmental_impact_potential": 79,
"led_fixed_lighting_bulbs_count": 5,
"has_heated_separate_conservatory": "false",
"potential_energy_efficiency_band": "B",
"co2_emissions_current_per_floor_area": 24,
"incandescent_fixed_lighting_bulbs_count": 0
}

View file

@ -0,0 +1,337 @@
{
"uprn": 10012334488,
"roofs": [
{
"description": "(another dwelling above)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"walls": [
{
"description": "System built, as built, insulated (assumed)",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"floors": [
{
"description": "(another dwelling below)",
"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": "Excellent lighting efficiency",
"energy_efficiency_rating": 5,
"environmental_efficiency_rating": 5
},
"postcode": "SL4 3BP",
"hot_water": {
"description": "From main system, no cylinder thermostat",
"energy_efficiency_rating": 1,
"environmental_efficiency_rating": 4
},
"post_town": "WINDSOR",
"built_form": "NR",
"created_at": "2026-05-14 18:33:57",
"door_count": 1,
"region_code": 17,
"report_type": 2,
"sap_heating": {
"number_baths": 1,
"cylinder_size": 0,
"shower_outlets": [
{
"shower_wwhrs": 1,
"shower_outlet_type": 1
}
],
"number_baths_wwhrs": 0,
"water_heating_code": 901,
"water_heating_fuel": 29,
"main_heating_details": [
{
"has_fghrs": "N",
"main_fuel_type": 29,
"heat_emitter_type": 2,
"emitter_temperature": "NA",
"main_heating_number": 1,
"main_heating_control": 2110,
"main_heating_category": 2,
"main_heating_fraction": 1,
"sap_main_heating_code": 195,
"central_heating_pump_age": 0,
"main_heating_data_source": 2
}
],
"immersion_heating_type": "NA",
"has_fixed_air_conditioning": "false"
},
"sap_version": 10.2,
"sap_windows": [
{
"pvc_frame": "true",
"orientation": 3,
"window_type": 2,
"glazing_type": 2,
"window_width": 0.85,
"window_height": 1.11,
"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": 2,
"glazing_type": 2,
"window_width": 0.85,
"window_height": 1.11,
"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": 2,
"glazing_type": 2,
"window_width": 0.85,
"window_height": 1.11,
"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": 2,
"glazing_type": 2,
"window_width": 0.85,
"window_height": 1.11,
"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": 2,
"glazing_type": 2,
"window_width": 0.85,
"window_height": 1.11,
"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": 2,
"glazing_type": 2,
"window_width": 0.85,
"window_height": 1.11,
"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 underfloor heating, electric",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 5
}
],
"air_tightness": {
"description": "(not tested)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"dwelling_type": "Mid-floor flat",
"language_code": 1,
"pressure_test": 4,
"property_type": 2,
"address_line_1": "Flat 12",
"address_line_2": "Alexandra Court 25a",
"address_line_3": "St. Leonards Road",
"assessment_type": "RdSAP",
"completion_date": "2026-05-14",
"inspection_date": "2026-05-14",
"extensions_count": 0,
"measurement_type": 1,
"sap_flat_details": {
"level": 2,
"top_storey": "N",
"storey_count": 4,
"flat_location": 3,
"heat_loss_corridor": 1
},
"total_floor_area": 62,
"transaction_type": 14,
"conservatory_type": 1,
"has_draught_lobby": "true",
"heated_room_count": 2,
"registration_date": "2026-05-14",
"sap_energy_source": {
"mains_gas": "N",
"meter_type": 1,
"pv_connection": 0,
"photovoltaic_supply": {
"none_or_no_details": {
"percent_roof_area": 0
}
},
"wind_turbines_count": 0,
"gas_smart_meter_present": "false",
"is_dwelling_export_capable": "false",
"wind_turbines_terrain_type": 1,
"electricity_smart_meter_present": "false"
},
"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",
"wall_thickness": 260,
"floor_heat_loss": 6,
"roof_construction": 3,
"wall_construction": 8,
"building_part_number": 1,
"sap_floor_dimensions": [
{
"floor": 0,
"room_height": {
"value": 2.48,
"quantity": "metres"
},
"total_floor_area": {
"value": 62,
"quantity": "square metres"
},
"party_wall_length": {
"value": 23.99,
"quantity": "metres"
},
"heat_loss_perimeter": {
"value": 11.89,
"quantity": "metres"
}
}
],
"wall_insulation_type": 4,
"construction_age_band": "L",
"party_wall_construction": 1,
"wall_thickness_measured": "Y",
"roof_insulation_location": "ND",
"roof_insulation_thickness": "ND",
"wall_insulation_thickness": "NI"
}
],
"solar_water_heating": "N",
"habitable_room_count": 2,
"heating_cost_current": {
"value": 136,
"currency": "GBP"
},
"insulated_door_count": 0,
"co2_emissions_current": 0.5,
"energy_rating_average": 60,
"energy_rating_current": 72,
"lighting_cost_current": {
"value": 51,
"currency": "GBP"
},
"main_heating_controls": [
{
"description": "Time and temperature zone control",
"energy_efficiency_rating": 5,
"environmental_efficiency_rating": 5
}
],
"has_hot_water_cylinder": "true",
"heating_cost_potential": {
"value": 136,
"currency": "GBP"
},
"hot_water_cost_current": {
"value": 893,
"currency": "GBP"
},
"mechanical_ventilation": 0,
"percent_draughtproofed": 100,
"suggested_improvements": [
{
"sequence": 1,
"typical_saving": {
"value": 91,
"currency": "GBP"
},
"indicative_cost": "\u00a3600 - \u00a31,500",
"improvement_type": "Y",
"improvement_details": {
"improvement_number": 49
},
"improvement_category": 5,
"energy_performance_rating": 74,
"environmental_impact_rating": 93
}
],
"co2_emissions_potential": 0.5,
"energy_rating_potential": 74,
"lighting_cost_potential": {
"value": 51,
"currency": "GBP"
},
"schema_version_original": "21.0.1",
"hot_water_cost_potential": {
"value": 802,
"currency": "GBP"
},
"renewable_heat_incentive": {
"water_heating": 2802.07,
"space_heating_existing_dwelling": 628.72
},
"draughtproofed_door_count": 1,
"energy_consumption_current": 92,
"has_fixed_air_conditioning": "false",
"multiple_glazed_proportion": 100,
"calculation_software_version": "5.02r0344",
"energy_consumption_potential": 85,
"environmental_impact_current": 93,
"current_energy_efficiency_band": "C",
"environmental_impact_potential": 93,
"led_fixed_lighting_bulbs_count": 23,
"has_heated_separate_conservatory": "false",
"potential_energy_efficiency_band": "C",
"co2_emissions_current_per_floor_area": 9,
"incandescent_fixed_lighting_bulbs_count": 0
}

View file

@ -0,0 +1,491 @@
{
"uprn": 10070056075,
"roofs": [
{
"description": "Pitched, insulated",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"walls": [
{
"description": "Cavity wall, as built, insulated (assumed)",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"floors": [
{
"description": "Suspended, insulated (assumed)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"status": "entered",
"tenure": 1,
"window": {
"description": "Fully double glazed",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
},
"addendum": {
"addendum_numbers": [
15
]
},
"lighting": {
"description": "Excellent lighting efficiency",
"energy_efficiency_rating": 5,
"environmental_efficiency_rating": 5
},
"postcode": "BD10 9UG",
"hot_water": {
"description": "From main system",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
},
"post_town": "BRADFORD",
"psv_count": 0,
"built_form": 1,
"created_at": "2026-05-15 10:22:34",
"door_count": 1,
"region_code": 11,
"report_type": 2,
"sap_heating": {
"number_baths": 1,
"cylinder_size": 4,
"shower_outlets": [
{
"shower_wwhrs": 1,
"shower_outlet_type": 4
}
],
"number_baths_wwhrs": 0,
"water_heating_code": 901,
"water_heating_fuel": 26,
"cylinder_thermostat": "Y",
"main_heating_details": [
{
"has_fghrs": "N",
"main_fuel_type": 26,
"boiler_flue_type": 2,
"fan_flue_present": "Y",
"heat_emitter_type": 1,
"emitter_temperature": 1,
"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": 19012
}
],
"immersion_heating_type": "NA",
"cylinder_insulation_type": 1,
"has_fixed_air_conditioning": "false",
"cylinder_insulation_thickness": 80
},
"sap_version": 10.2,
"sap_windows": [
{
"pvc_frame": "true",
"orientation": 5,
"window_type": 1,
"glazing_type": 2,
"window_width": {
"value": 1,
"quantity": "m"
},
"window_height": {
"value": 1.2,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 5,
"window_type": 1,
"glazing_type": 2,
"window_width": {
"value": 1,
"quantity": "m"
},
"window_height": {
"value": 1.2,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 5,
"window_type": 1,
"glazing_type": 2,
"window_width": {
"value": 1,
"quantity": "m"
},
"window_height": {
"value": 1.2,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 5,
"window_type": 1,
"glazing_type": 2,
"window_width": {
"value": 1,
"quantity": "m"
},
"window_height": {
"value": 1.2,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 5,
"window_type": 1,
"glazing_type": 2,
"window_width": {
"value": 1,
"quantity": "m"
},
"window_height": {
"value": 1.2,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 5,
"window_type": 1,
"glazing_type": 2,
"window_width": {
"value": 1,
"quantity": "m"
},
"window_height": {
"value": 1.2,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 5,
"window_type": 1,
"glazing_type": 2,
"window_width": {
"value": 1,
"quantity": "m"
},
"window_height": {
"value": 1.2,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 5,
"window_type": 1,
"glazing_type": 2,
"window_width": {
"value": 1,
"quantity": "m"
},
"window_height": {
"value": 1.2,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 5,
"window_type": 1,
"glazing_type": 2,
"window_width": {
"value": 1,
"quantity": "m"
},
"window_height": {
"value": 1.2,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 5,
"window_type": 1,
"glazing_type": 2,
"window_width": {
"value": 1,
"quantity": "m"
},
"window_height": {
"value": 1.2,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 5,
"window_type": 1,
"glazing_type": 2,
"window_width": {
"value": 1,
"quantity": "m"
},
"window_height": {
"value": 1.2,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 5,
"window_type": 1,
"glazing_type": 2,
"window_width": {
"value": 1,
"quantity": "m"
},
"window_height": {
"value": 1.2,
"quantity": "m"
},
"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": "Detached house",
"language_code": 1,
"pressure_test": 4,
"property_type": 0,
"address_line_1": "36 Longlands",
"address_line_2": "Idle",
"assessment_type": "RdSAP",
"completion_date": "2026-05-15",
"inspection_date": "2026-05-08",
"extensions_count": 0,
"measurement_type": 1,
"open_flues_count": 0,
"total_floor_area": 139,
"transaction_type": 5,
"conservatory_type": 1,
"heated_room_count": 8,
"other_flues_count": 0,
"registration_date": "2026-05-15",
"sap_energy_source": {
"mains_gas": "Y",
"meter_type": 1,
"pv_connection": 0,
"photovoltaic_supply": {
"none_or_no_details": {
"percent_roof_area": 0
}
},
"wind_turbines_count": 0,
"gas_smart_meter_present": "false",
"is_dwelling_export_capable": "false",
"wind_turbines_terrain_type": 2,
"electricity_smart_meter_present": "false"
},
"secondary_heating": {
"description": "None",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"closed_flues_count": 0,
"extract_fans_count": 0,
"sap_building_parts": [
{
"identifier": "Main Dwelling",
"wall_dry_lined": "N",
"wall_thickness": 400,
"floor_heat_loss": 7,
"roof_construction": 8,
"wall_construction": 4,
"building_part_number": 1,
"sap_floor_dimensions": [
{
"floor": 0,
"room_height": 2.3,
"floor_insulation": 1,
"total_floor_area": 69.6,
"party_wall_length": 0,
"floor_construction": 2,
"heat_loss_perimeter": 37.7
},
{
"floor": 1,
"room_height": 2.3,
"total_floor_area": 69.6,
"party_wall_length": 0,
"heat_loss_perimeter": 37.7
}
],
"wall_insulation_type": 4,
"construction_age_band": "K",
"party_wall_construction": "NA",
"wall_thickness_measured": "N",
"roof_insulation_location": 7,
"wall_insulation_thickness": "NI",
"sloping_ceiling_insulation_thickness": "AB"
}
],
"boilers_flues_count": 0,
"open_chimneys_count": 0,
"solar_water_heating": "N",
"habitable_room_count": 8,
"heating_cost_current": {
"value": 938,
"currency": "GBP"
},
"insulated_door_count": 0,
"co2_emissions_current": 3.0,
"energy_rating_average": 60,
"energy_rating_current": 80,
"lighting_cost_current": {
"value": 89,
"currency": "GBP"
},
"main_heating_controls": [
{
"description": "Programmer, room thermostat and TRVs",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"blocked_chimneys_count": 0,
"has_hot_water_cylinder": "true",
"heating_cost_potential": {
"value": 938,
"currency": "GBP"
},
"hot_water_cost_current": {
"value": 236,
"currency": "GBP"
},
"mechanical_ventilation": 0,
"percent_draughtproofed": 100,
"schema_version_current": "LIG-21.0",
"suggested_improvements": [
{
"sequence": 1,
"typical_saving": 308,
"indicative_cost": "\u00a38,000 - \u00a310,000",
"improvement_type": "U",
"improvement_details": {
"improvement_number": 34
},
"improvement_category": 5,
"energy_performance_rating": 87,
"environmental_impact_rating": 81
}
],
"co2_emissions_potential": 2.8,
"energy_rating_potential": 87,
"lighting_cost_potential": {
"value": 89,
"currency": "GBP"
},
"schema_version_original": "LIG-21.0",
"hot_water_cost_potential": {
"value": 236,
"currency": "GBP"
},
"renewable_heat_incentive": {
"water_heating": 2744.25,
"space_heating_existing_dwelling": 9473.16
},
"draughtproofed_door_count": 1,
"energy_consumption_current": 117,
"has_fixed_air_conditioning": "false",
"multiple_glazed_proportion": 100,
"calculation_software_version": "10.2.2.0",
"energy_consumption_potential": 106,
"environmental_impact_current": 79,
"cfl_fixed_lighting_bulbs_count": 0,
"current_energy_efficiency_band": "C",
"environmental_impact_potential": 81,
"led_fixed_lighting_bulbs_count": 18,
"has_heated_separate_conservatory": "false",
"potential_energy_efficiency_band": "B",
"co2_emissions_current_per_floor_area": 21,
"incandescent_fixed_lighting_bulbs_count": 0
}

View file

@ -0,0 +1,316 @@
{
"uprn": 10091578598,
"roofs": [
{
"description": "(another dwelling above)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"walls": [
{
"description": "Cavity wall, as built, insulated (assumed)",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"floors": [
{
"description": "(another dwelling below)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"status": "entered",
"tenure": 2,
"window": {
"description": "Fully double glazed",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
},
"lighting": {
"description": "Good lighting efficiency",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
},
"postcode": "RM1 3FB",
"hot_water": {
"description": "From main system",
"energy_efficiency_rating": 1,
"environmental_efficiency_rating": 5
},
"post_town": "ROMFORD",
"built_form": "NR",
"created_at": "2026-05-16 17:47:19",
"door_count": 1,
"region_code": 2,
"report_type": 2,
"sap_heating": {
"number_baths": 1,
"cylinder_size": 4,
"number_baths_wwhrs": 0,
"water_heating_code": 901,
"water_heating_fuel": 29,
"cylinder_thermostat": "Y",
"main_heating_details": [
{
"has_fghrs": "N",
"main_fuel_type": 29,
"heat_emitter_type": 1,
"emitter_temperature": "NA",
"main_heating_number": 1,
"main_heating_control": 2106,
"main_heating_category": 2,
"main_heating_fraction": 1,
"sap_main_heating_code": 195,
"central_heating_pump_age": 0,
"main_heating_data_source": 2
}
],
"immersion_heating_type": "NA",
"cylinder_insulation_type": 1,
"has_fixed_air_conditioning": "false",
"cylinder_insulation_thickness": 50
},
"sap_version": 10.2,
"sap_windows": [
{
"pvc_frame": "true",
"orientation": 2,
"window_type": 1,
"glazing_type": 2,
"window_width": 1.15,
"window_height": 1.65,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 2,
"window_type": 1,
"glazing_type": 2,
"window_width": 1.15,
"window_height": 1.65,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 2,
"window_type": 1,
"glazing_type": 2,
"window_width": 1.15,
"window_height": 1.65,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 2,
"window_type": 1,
"glazing_type": 2,
"window_width": 1.15,
"window_height": 1.65,
"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, electric",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 5
}
],
"air_tightness": {
"description": "(not tested)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"dwelling_type": "Mid-floor flat",
"language_code": 1,
"pressure_test": 4,
"property_type": 2,
"address_line_1": "41 Verve Apartments",
"address_line_2": "5 Mercury Gardens",
"assessment_type": "RdSAP",
"completion_date": "2026-05-16",
"inspection_date": "2026-05-16",
"extensions_count": 0,
"measurement_type": 1,
"sap_flat_details": {
"level": 2,
"top_storey": "N",
"storey_count": 6,
"flat_location": 3,
"heat_loss_corridor": 2,
"unheated_corridor_length": 6
},
"total_floor_area": 60,
"transaction_type": 8,
"conservatory_type": 1,
"heated_room_count": 2,
"registration_date": "2026-05-16",
"sap_energy_source": {
"mains_gas": "N",
"meter_type": 4,
"pv_connection": 0,
"photovoltaic_supply": {
"none_or_no_details": {
"percent_roof_area": 0
}
},
"wind_turbines_count": 0,
"gas_smart_meter_present": "false",
"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
},
"sap_building_parts": [
{
"identifier": "Main Dwelling",
"wall_dry_lined": "N",
"wall_thickness": 300,
"floor_heat_loss": 6,
"roof_construction": 3,
"wall_construction": 4,
"building_part_number": 1,
"sap_floor_dimensions": [
{
"floor": 0,
"room_height": {
"value": 2.5,
"quantity": "metres"
},
"total_floor_area": {
"value": 60,
"quantity": "square metres"
},
"party_wall_length": {
"value": 20,
"quantity": "metres"
},
"heat_loss_perimeter": {
"value": 12,
"quantity": "metres"
}
}
],
"wall_insulation_type": 4,
"construction_age_band": "I",
"sap_alternative_wall_1": {
"wall_area": 15,
"sheltered_wall": "Y",
"wall_dry_lined": "N",
"wall_thickness": 300,
"wall_construction": 4,
"wall_insulation_type": 4,
"wall_thickness_measured": "Y",
"wall_insulation_thickness": "NI"
},
"party_wall_construction": 0,
"wall_thickness_measured": "Y",
"roof_insulation_location": "ND",
"roof_insulation_thickness": "ND",
"wall_insulation_thickness": "NI"
}
],
"solar_water_heating": "N",
"habitable_room_count": 2,
"heating_cost_current": {
"value": 202,
"currency": "GBP"
},
"insulated_door_count": 0,
"co2_emissions_current": 0.6,
"energy_rating_average": 60,
"energy_rating_current": 71,
"lighting_cost_current": {
"value": 55,
"currency": "GBP"
},
"main_heating_controls": [
{
"description": "Programmer, room thermostat and TRVs",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"has_hot_water_cylinder": "true",
"heating_cost_potential": {
"value": 370,
"currency": "GBP"
},
"hot_water_cost_current": {
"value": 847,
"currency": "GBP"
},
"mechanical_ventilation": 0,
"percent_draughtproofed": 80,
"suggested_improvements": [
{
"sequence": 1,
"typical_saving": {
"value": 76,
"currency": "GBP"
},
"indicative_cost": "\u00a3150 - \u00a3250",
"improvement_type": "D",
"improvement_details": {
"improvement_number": 10
},
"improvement_category": 5,
"energy_performance_rating": 79,
"environmental_impact_rating": 92
}
],
"co2_emissions_potential": 0.6,
"energy_rating_potential": 79,
"lighting_cost_potential": {
"value": 48,
"currency": "GBP"
},
"schema_version_original": "21.0.1",
"hot_water_cost_potential": {
"value": 611,
"currency": "GBP"
},
"renewable_heat_incentive": {
"water_heating": 2659.46,
"space_heating_existing_dwelling": 1061.64
},
"draughtproofed_door_count": 0,
"energy_consumption_current": 102,
"has_fixed_air_conditioning": "false",
"multiple_glazed_proportion": 100,
"calculation_software_version": "5.02r0344",
"energy_consumption_potential": 102,
"environmental_impact_current": 92,
"current_energy_efficiency_band": "C",
"environmental_impact_potential": 92,
"has_heated_separate_conservatory": "false",
"potential_energy_efficiency_band": "C",
"co2_emissions_current_per_floor_area": 10,
"low_energy_fixed_lighting_bulbs_count": 20,
"incandescent_fixed_lighting_bulbs_count": 0
}

View file

@ -0,0 +1,366 @@
{
"uprn": 10094975827,
"roofs": [
{
"description": "Pitched, insulated (assumed)",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
}
],
"walls": [
{
"description": "Solid brick, as built, no insulation (assumed)",
"energy_efficiency_rating": 2,
"environmental_efficiency_rating": 2
}
],
"floors": [
{
"description": "(other premises below)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"status": "entered",
"tenure": 3,
"window": {
"description": "Fully double glazed",
"energy_efficiency_rating": 2,
"environmental_efficiency_rating": 2
},
"lighting": {
"description": "Excellent lighting efficiency",
"energy_efficiency_rating": 5,
"environmental_efficiency_rating": 5
},
"postcode": "B8 2NG",
"hot_water": {
"description": "From main system",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
},
"post_town": "BIRMINGHAM",
"psv_count": 0,
"built_form": 3,
"created_at": "2026-05-14 13:27:59",
"door_count": 1,
"region_code": 6,
"report_type": 2,
"sap_heating": {
"number_baths": 1,
"cylinder_size": 1,
"shower_outlets": [
{
"shower_wwhrs": 1,
"shower_outlet_type": 1
}
],
"number_baths_wwhrs": 0,
"water_heating_code": 901,
"water_heating_fuel": 26,
"cylinder_thermostat": "N",
"main_heating_details": [
{
"has_fghrs": "N",
"main_fuel_type": 26,
"boiler_flue_type": 2,
"fan_flue_present": "Y",
"heat_emitter_type": 1,
"emitter_temperature": 1,
"main_heating_number": 1,
"main_heating_control": 2107,
"main_heating_category": 2,
"main_heating_fraction": 1,
"central_heating_pump_age": 0,
"main_heating_data_source": 1,
"main_heating_index_number": 18559
}
],
"immersion_heating_type": "NA",
"has_fixed_air_conditioning": "false"
},
"sap_version": 10.2,
"sap_windows": [
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 1,
"window_type": 1,
"glazing_type": 3,
"window_width": {
"value": 1.01,
"quantity": "m"
},
"window_height": {
"value": 1.61,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 1,
"window_type": 1,
"glazing_type": 3,
"window_width": {
"value": 1.66,
"quantity": "m"
},
"window_height": {
"value": 1.46,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 3,
"window_type": 1,
"glazing_type": 3,
"window_width": {
"value": 1.09,
"quantity": "m"
},
"window_height": {
"value": 1.29,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 3,
"window_type": 1,
"glazing_type": 3,
"window_width": {
"value": 1.09,
"quantity": "m"
},
"window_height": {
"value": 1.55,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 3,
"window_type": 1,
"glazing_type": 3,
"window_width": {
"value": 1.79,
"quantity": "m"
},
"window_height": {
"value": 1.92,
"quantity": "m"
},
"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": "Top-floor flat",
"language_code": 1,
"pressure_test": 4,
"property_type": 2,
"address_line_1": "Flat 2",
"address_line_2": "874 Washwood Heath Road",
"assessment_type": "RdSAP",
"completion_date": "2026-05-14",
"inspection_date": "2026-05-13",
"extensions_count": 0,
"measurement_type": 1,
"open_flues_count": 0,
"sap_flat_details": {
"level": 3,
"top_storey": "N",
"storey_count": 2,
"flat_location": 1,
"heat_loss_corridor": 0
},
"total_floor_area": 78,
"transaction_type": 8,
"conservatory_type": 1,
"heated_room_count": 3,
"other_flues_count": 0,
"registration_date": "2026-05-14",
"sap_energy_source": {
"mains_gas": "Y",
"meter_type": 3,
"pv_connection": 0,
"photovoltaic_supply": {
"none_or_no_details": {
"percent_roof_area": 0
}
},
"wind_turbines_count": 0,
"gas_smart_meter_present": "false",
"is_dwelling_export_capable": "false",
"wind_turbines_terrain_type": 2,
"electricity_smart_meter_present": "false"
},
"secondary_heating": {
"description": "None",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"closed_flues_count": 0,
"extract_fans_count": 0,
"sap_building_parts": [
{
"identifier": "Main Dwelling",
"wall_dry_lined": "N",
"wall_thickness": 340,
"floor_heat_loss": 3,
"roof_construction": 4,
"wall_construction": 3,
"building_part_number": 1,
"sap_floor_dimensions": [
{
"floor": 0,
"room_height": 2.44,
"total_floor_area": 77.87,
"party_wall_length": 11.57,
"heat_loss_perimeter": 25.03
}
],
"wall_insulation_type": 4,
"construction_age_band": "B",
"party_wall_construction": 1,
"wall_thickness_measured": "Y",
"roof_insulation_location": 4,
"roof_insulation_thickness": "NI",
"wall_insulation_thickness": "NI"
}
],
"boilers_flues_count": 0,
"open_chimneys_count": 0,
"solar_water_heating": "N",
"habitable_room_count": 3,
"heating_cost_current": {
"value": 952,
"currency": "GBP"
},
"insulated_door_count": 0,
"co2_emissions_current": 3.2,
"energy_rating_average": 60,
"energy_rating_current": 66,
"lighting_cost_current": {
"value": 56,
"currency": "GBP"
},
"main_heating_controls": [
{
"description": "Programmer, TRVs and bypass",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
}
],
"blocked_chimneys_count": 0,
"has_hot_water_cylinder": "false",
"heating_cost_potential": {
"value": 735,
"currency": "GBP"
},
"hot_water_cost_current": {
"value": 197,
"currency": "GBP"
},
"mechanical_ventilation": 0,
"percent_draughtproofed": 100,
"schema_version_current": "LIG-21.0",
"suggested_improvements": [
{
"sequence": 1,
"typical_saving": 171,
"indicative_cost": "\u00a37,500 - \u00a311,000",
"improvement_type": "Q",
"improvement_details": {
"improvement_number": 7
},
"improvement_category": 5,
"energy_performance_rating": 70,
"environmental_impact_rating": 70
},
{
"sequence": 2,
"typical_saving": 45,
"indicative_cost": "\u00a3220 - \u00a3250",
"improvement_type": "G",
"improvement_details": {
"improvement_number": 14
},
"improvement_category": 5,
"energy_performance_rating": 72,
"environmental_impact_rating": 71
}
],
"co2_emissions_potential": 2.6,
"energy_rating_potential": 72,
"lighting_cost_potential": {
"value": 56,
"currency": "GBP"
},
"schema_version_original": "LIG-21.0",
"hot_water_cost_potential": {
"value": 198,
"currency": "GBP"
},
"renewable_heat_incentive": {
"water_heating": 2548.95,
"space_heating_existing_dwelling": 10093.12
},
"draughtproofed_door_count": 1,
"energy_consumption_current": 227,
"has_fixed_air_conditioning": "false",
"multiple_glazed_proportion": 100,
"calculation_software_version": "10.2.2.0",
"energy_consumption_potential": 181,
"environmental_impact_current": 64,
"cfl_fixed_lighting_bulbs_count": 0,
"current_energy_efficiency_band": "D",
"environmental_impact_potential": 71,
"led_fixed_lighting_bulbs_count": 8,
"has_heated_separate_conservatory": "false",
"potential_energy_efficiency_band": "C",
"co2_emissions_current_per_floor_area": 41,
"incandescent_fixed_lighting_bulbs_count": 0
}

View file

@ -0,0 +1,321 @@
{
"uprn": 103001004,
"roofs": [
{
"description": "Pitched, Unknown loft insulation",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
}
],
"walls": [
{
"description": "Cavity wall, filled cavity",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"floors": [
{
"description": "Solid, no insulation (assumed)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"status": "entered",
"tenure": 2,
"window": {
"description": "Fully double glazed",
"energy_efficiency_rating": 2,
"environmental_efficiency_rating": 2
},
"lighting": {
"description": "Good lighting efficiency",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
},
"postcode": "NE33 1TN",
"hot_water": {
"description": "Electric immersion, off-peak",
"energy_efficiency_rating": 2,
"environmental_efficiency_rating": 4
},
"post_town": "SOUTH SHIELDS",
"built_form": "NR",
"created_at": "2026-04-16 10:25:37",
"door_count": 0,
"region_code": 1,
"report_type": 2,
"sap_heating": {
"number_baths": 0,
"cylinder_size": 3,
"shower_outlets": [
{
"shower_outlet": {
"shower_wwhrs": 1,
"shower_outlet_type": 1
}
}
],
"cylinder_heat_loss": 1.8,
"number_baths_wwhrs": 0,
"water_heating_code": 903,
"water_heating_fuel": 29,
"main_heating_details": [
{
"has_fghrs": "N",
"main_fuel_type": 20,
"heat_emitter_type": 0,
"emitter_temperature": "NA",
"main_heating_number": 1,
"main_heating_control": 2307,
"main_heating_category": 6,
"main_heating_fraction": 1,
"sap_main_heating_code": 301,
"main_heating_data_source": 2,
"community_heat_distribution_type": 6
}
],
"immersion_heating_type": 2,
"has_fixed_air_conditioning": "false"
},
"sap_version": 10.2,
"sap_windows": [
{
"pvc_frame": "true",
"glazing_gap": 12,
"orientation": 7,
"window_type": 1,
"glazing_type": 3,
"window_width": 1.21,
"window_height": 1.44,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": 12,
"orientation": 7,
"window_type": 1,
"glazing_type": 3,
"window_width": 1.77,
"window_height": 1.14,
"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": "Community scheme",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
}
],
"air_tightness": {
"description": "(not tested)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"dwelling_type": "Ground-floor flat",
"language_code": 1,
"pressure_test": 4,
"property_type": 2,
"address_line_1": "3 Julius Court",
"address_line_2": "Mile End Road",
"assessment_type": "RdSAP",
"completion_date": "2026-04-16",
"inspection_date": "2026-04-16",
"extensions_count": 0,
"measurement_type": 1,
"sap_flat_details": {
"level": 1,
"top_storey": "N",
"storey_count": 1,
"flat_location": 0,
"heat_loss_corridor": 1
},
"total_floor_area": 31,
"transaction_type": 8,
"conservatory_type": 1,
"has_draught_lobby": "true",
"heated_room_count": 2,
"registration_date": "2026-04-16",
"sap_energy_source": {
"mains_gas": "Y",
"meter_type": 1,
"pv_connection": 0,
"photovoltaic_supply": {
"none_or_no_details": {
"percent_roof_area": 0
}
},
"wind_turbines_count": 0,
"gas_smart_meter_present": "false",
"is_dwelling_export_capable": "false",
"wind_turbines_terrain_type": 2,
"electricity_smart_meter_present": "false"
},
"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": 7,
"roof_construction": 4,
"wall_construction": 4,
"building_part_number": 1,
"sap_floor_dimensions": [
{
"floor": 0,
"room_height": {
"value": 2.37,
"quantity": "metres"
},
"floor_insulation": 1,
"total_floor_area": {
"value": 30.55,
"quantity": "square metres"
},
"party_wall_length": {
"value": 16.75,
"quantity": "metres"
},
"floor_construction": 1,
"heat_loss_perimeter": {
"value": 5.37,
"quantity": "metres"
}
}
],
"wall_insulation_type": 2,
"construction_age_band": "F",
"party_wall_construction": 0,
"wall_thickness_measured": "N",
"roof_insulation_location": 2,
"roof_insulation_thickness": "NI",
"wall_insulation_thickness": "NI",
"floor_insulation_thickness": "NI"
}
],
"solar_water_heating": "N",
"habitable_room_count": 2,
"heating_cost_current": {
"value": 251,
"currency": "GBP"
},
"insulated_door_count": 0,
"co2_emissions_current": 1.2,
"energy_rating_average": 60,
"energy_rating_current": 50,
"lighting_cost_current": {
"value": 29,
"currency": "GBP"
},
"main_heating_controls": [
{
"description": "Flat rate charging, TRVs",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
}
],
"has_hot_water_cylinder": "true",
"heating_cost_potential": {
"value": 313,
"currency": "GBP"
},
"hot_water_cost_current": {
"value": 1054,
"currency": "GBP"
},
"mechanical_ventilation": 0,
"percent_draughtproofed": 100,
"suggested_improvements": [
{
"sequence": 1,
"typical_saving": {
"value": 35,
"currency": "GBP"
},
"indicative_cost": "\u00a3900 - \u00a31,200",
"improvement_type": "A",
"improvement_details": {
"improvement_number": 5
},
"improvement_category": 5,
"energy_performance_rating": 52,
"environmental_impact_rating": 81
},
{
"sequence": 2,
"typical_saving": {
"value": 598,
"currency": "GBP"
},
"indicative_cost": "\u00a320 - \u00a340",
"improvement_type": "C",
"improvement_details": {
"improvement_number": 1
},
"improvement_category": 5,
"energy_performance_rating": 68,
"environmental_impact_rating": 83
},
{
"sequence": 3,
"typical_saving": {
"value": 51,
"currency": "GBP"
},
"indicative_cost": "\u00a3600 - \u00a31,500",
"improvement_type": "Y",
"improvement_details": {
"improvement_number": 49
},
"improvement_category": 5,
"energy_performance_rating": 70,
"environmental_impact_rating": 83
}
],
"co2_emissions_potential": 1.0,
"energy_rating_potential": 70,
"lighting_cost_potential": {
"value": 29,
"currency": "GBP"
},
"schema_version_original": "21.0.1",
"hot_water_cost_potential": {
"value": 308,
"currency": "GBP"
},
"renewable_heat_incentive": {
"water_heating": 5719.21,
"space_heating_existing_dwelling": 1270.63
},
"draughtproofed_door_count": 0,
"energy_consumption_current": 367,
"has_fixed_air_conditioning": "false",
"multiple_glazed_proportion": 100,
"calculation_software_version": "5.02r0342",
"energy_consumption_potential": 214,
"environmental_impact_current": 78,
"current_energy_efficiency_band": "E",
"environmental_impact_potential": 83,
"has_heated_separate_conservatory": "false",
"potential_energy_efficiency_band": "C",
"co2_emissions_current_per_floor_area": 41,
"low_energy_fixed_lighting_bulbs_count": 5,
"incandescent_fixed_lighting_bulbs_count": 0
}

View file

@ -0,0 +1,372 @@
{
"uprn": 200004017091,
"roofs": [
{
"description": "Pitched, 100 mm loft insulation",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
}
],
"walls": [
{
"description": "Timber frame, as built, insulated (assumed)",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"floors": [
{
"description": "Solid, no insulation (assumed)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"status": "entered",
"tenure": 1,
"window": {
"description": "Fully double glazed",
"energy_efficiency_rating": 2,
"environmental_efficiency_rating": 2
},
"addendum": {
"addendum_numbers": [
15
]
},
"lighting": {
"description": "Good lighting efficiency",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
},
"postcode": "BN14 9EL",
"hot_water": {
"description": "From main system, no cylinder thermostat",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
},
"post_town": "WORTHING",
"built_form": 3,
"created_at": "2026-04-21 13:18:06",
"door_count": 2,
"region_code": 14,
"report_type": 2,
"sap_heating": {
"number_baths": 1,
"cylinder_size": 0,
"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": 17803
}
],
"immersion_heating_type": "NA",
"has_fixed_air_conditioning": "false"
},
"sap_version": 10.2,
"sap_windows": [
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 1,
"window_type": 1,
"glazing_type": 3,
"window_width": 1.1,
"window_height": 1.2,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 1,
"window_type": 1,
"glazing_type": 3,
"window_width": 1.1,
"window_height": 1.2,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 5,
"window_type": 1,
"glazing_type": 3,
"window_width": 1.1,
"window_height": 1.2,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"glazing_gap": "16+",
"orientation": 5,
"window_type": 1,
"glazing_type": 3,
"window_width": 0.8,
"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",
"glazing_gap": "16+",
"orientation": 5,
"window_type": 1,
"glazing_type": 3,
"window_width": 1.5,
"window_height": 0.8,
"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": "End-terrace house",
"language_code": 1,
"pressure_test": 4,
"property_type": 0,
"address_line_1": "3 Radbone Close",
"assessment_type": "RdSAP",
"completion_date": "2026-04-21",
"inspection_date": "2026-04-20",
"extensions_count": 0,
"measurement_type": 1,
"total_floor_area": 65,
"transaction_type": 1,
"conservatory_type": 1,
"heated_room_count": 4,
"registration_date": "2026-04-21",
"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": "false",
"is_dwelling_export_capable": "false",
"wind_turbines_terrain_type": 2,
"electricity_smart_meter_present": "false"
},
"secondary_heating": {
"description": "None",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"extract_fans_count": 1,
"sap_building_parts": [
{
"identifier": "Main Dwelling",
"wall_dry_lined": "N",
"wall_thickness": 280,
"floor_heat_loss": 7,
"roof_construction": 4,
"wall_construction": 5,
"building_part_number": 1,
"sap_floor_dimensions": [
{
"floor": 0,
"room_height": {
"value": 2.37,
"quantity": "metres"
},
"floor_insulation": 1,
"total_floor_area": {
"value": 32.38,
"quantity": "square metres"
},
"party_wall_length": {
"value": 7.42,
"quantity": "metres"
},
"floor_construction": 1,
"heat_loss_perimeter": {
"value": 16.15,
"quantity": "metres"
}
},
{
"floor": 1,
"room_height": {
"value": 2.35,
"quantity": "metres"
},
"total_floor_area": {
"value": 32.37,
"quantity": "square metres"
},
"party_wall_length": {
"value": 7.42,
"quantity": "metres"
},
"heat_loss_perimeter": {
"value": 16.15,
"quantity": "metres"
}
}
],
"wall_insulation_type": 4,
"construction_age_band": "G",
"party_wall_construction": 0,
"wall_thickness_measured": "Y",
"roof_insulation_location": 2,
"roof_insulation_thickness": "100mm",
"wall_insulation_thickness": "NI",
"floor_insulation_thickness": "NI"
}
],
"solar_water_heating": "N",
"habitable_room_count": 4,
"heating_cost_current": {
"value": 556,
"currency": "GBP"
},
"insulated_door_count": 0,
"co2_emissions_current": 2.2,
"energy_rating_average": 60,
"energy_rating_current": 71,
"lighting_cost_current": {
"value": 51,
"currency": "GBP"
},
"main_heating_controls": [
{
"description": "Programmer, room thermostat and TRVs",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"has_hot_water_cylinder": "true",
"heating_cost_potential": {
"value": 516,
"currency": "GBP"
},
"hot_water_cost_current": {
"value": 333,
"currency": "GBP"
},
"mechanical_ventilation": 0,
"percent_draughtproofed": 71,
"suggested_improvements": [
{
"sequence": 1,
"typical_saving": {
"value": 44,
"currency": "GBP"
},
"indicative_cost": "\u00a35,000 - \u00a310,000",
"improvement_type": "W2",
"improvement_details": {
"improvement_number": 58
},
"improvement_category": 5,
"energy_performance_rating": 73,
"environmental_impact_rating": 74
},
{
"sequence": 2,
"typical_saving": {
"value": 143,
"currency": "GBP"
},
"indicative_cost": "\u00a34,000 - \u00a37,000",
"improvement_type": "N",
"improvement_details": {
"improvement_number": 19
},
"improvement_category": 5,
"energy_performance_rating": 76,
"environmental_impact_rating": 79
},
{
"sequence": 3,
"typical_saving": {
"value": 212,
"currency": "GBP"
},
"indicative_cost": "\u00a38,000 - \u00a310,000",
"improvement_type": "U",
"improvement_details": {
"improvement_number": 34
},
"improvement_category": 5,
"energy_performance_rating": 81,
"environmental_impact_rating": 80
}
],
"co2_emissions_potential": 1.5,
"energy_rating_potential": 81,
"lighting_cost_potential": {
"value": 51,
"currency": "GBP"
},
"schema_version_original": "21.0.1",
"hot_water_cost_potential": {
"value": 186,
"currency": "GBP"
},
"renewable_heat_incentive": {
"water_heating": 3562.39,
"space_heating_existing_dwelling": 4593.09
},
"draughtproofed_door_count": 0,
"energy_consumption_current": 184,
"has_fixed_air_conditioning": "false",
"multiple_glazed_proportion": 100,
"calculation_software_version": "5.02r0342",
"energy_consumption_potential": 118,
"environmental_impact_current": 72,
"current_energy_efficiency_band": "C",
"environmental_impact_potential": 80,
"has_heated_separate_conservatory": "false",
"potential_energy_efficiency_band": "B",
"co2_emissions_current_per_floor_area": 33,
"low_energy_fixed_lighting_bulbs_count": 12,
"incandescent_fixed_lighting_bulbs_count": 0
}

View file

@ -0,0 +1,70 @@
# Elmhurst validation — UPRN 200004296092 (corpus, slice-5 wall fix)
Built in accredited Elmhurst RdSAP10-Online on the lodged register inputs
(end-terrace house, solid brick + **external insulation, thickness "NI"**,
pitched 150 mm loft, solid ground floor, mains-gas combi, TFA 62).
Artefacts: `elmhurst_summary.pdf` (Input Summary), `elmhurst_worksheet.pdf`
(SAP Worksheets).
## Headline
- **Lodged SAP 71**; **engine 71.74** (post slice-5 §5.4 100 mm fix, was 64.35);
**accredited Elmhurst current SAP 66** (band D), potential 75.
- Slice 5 is **directionally validated**: the externally-insulated wall must be
billed insulated, not at the uninsulated 1.40 the §5.8 gate produced. Both
Elmhurst (0.48) and the engine (0.29) are far below 1.40.
## Fabric line-by-line (engine dive vs Elmhurst worksheet)
| Element | Engine | Elmhurst | Note |
|---|---|---|---|
| External wall | 21.1 W/K (U 0.29) | 30.7 W/K (U **0.48**, "SolidWallDensePlaster**200**") | see WALL below |
| Roof | 9.35 W/K (U 0.30) | 9.35 W/K (U 0.30) | **exact match** |
| Ground floor | 21.2 W/K | 22.7 W/K (U 0.73) | close |
| Party wall | **8.57 W/K** | **0.00 W/K** | see PARTY below |
| Windows | 7.96 W/K | ~same area (4.31 + 4×2.89 split) | ~ |
The wall (engine 9.6) and party wall (engine +8.6) differences nearly cancel,
so the two HTCs are close; the residual SAP gap (71.7 vs 66) is dominated by the
heating system — this build used a GENERIC Post-98 condensing combi, whereas the
engine uses the lodged PCDB 16137 (Ideal Logic combi 30). Not a clean 1e-4
comparison for that reason.
## Leads (both partly confounded by an imperfect build — see caveats)
1. **WALL — base thickness on an unmeasured wall.** The cert lodges
`wall_thickness 360` but `wall_thickness_measured = N`. Elmhurst modelled a
**~200 mm** base wall ("SolidWallDensePlaster200") — but note my build did
NOT enter a wall thickness, so Elmhurst used its own default; had I keyed
360 mm it might match the engine. The open question is genuine though:
RdSAP §5.3 uses the documentary/measured thickness, and when NOT measured
the engine perhaps should fall to the Table-3 default rather than trust the
unmeasured lodged 360 mm. WORTH A DEDICATED CHECK (build with the thickness
keyed both ways).
2. **PARTY WALL — NOT an engine gap.** Elmhurst rendered the party wall as
"Solid, U 0.00", but the engine's **0.25** is SPEC-CORRECT: RdSAP §5.10
Table 15 row 4 ("unable to determine, house") = 0.25. The cert lodges
`party_wall_construction = 0` (unable to determine), so 0.25 is right; the
Elmhurst 0.00 reflects how my build's party-wall dropdown resolved (a
"Solid" masonry party wall, Table 15 row 1 = 0), not the cert's "unknown".
Build artifact, not a bug.
## Build caveats (why the 71.7-vs-66 residual isn't cleanly attributable)
- Heating: built a GENERIC Post-98 condensing combi; the engine uses the lodged
PCDB 16137 (Ideal Logic combi 30) — different efficiency.
- Wall thickness: not keyed into Elmhurst (defaulted 200 mm) vs engine's 360 mm.
- Party wall: entered as a resolved type (0.00) vs the cert's "unable to
determine" (engine 0.25).
So this build's clean, confirmed results are: (a) the ROOF matches exactly
(U 0.30, 9.35 W/K), (b) slice 5's DIRECTION is validated (insulated wall, not
the uninsulated 1.40). The wall-thickness question (lead 1) is the one genuine
follow-up.
## Slice-5 calibration note (banked)
The accredited "External insulation, **Unknown** thickness" selection in Elmhurst
produced U 0.48 (≈ a 50 mm equivalent on its 200 mm base), NOT the 0.29 the
§5.4-literal "assume 100 mm" gives on the engine's 360 mm base. The 100 mm fix
nevertheless matches the LODGED corpus better (MAE 0.658 → 0.647, within held) —
lodged assessors typically knew the real (~100 mm) thickness the register then
flattens to "NI". Per the campaign rule (engine = lodged is the goal; Elmhurst
arbitrates only when lodged can't be explained from register data), the 100 mm
fix STANDS; the lodged-vs-Elmhurst gap here is explained by the register's "NI"
discarding the assessor's thickness knowledge. But the base-thickness and
party-wall leads above are genuine and independent of the insulation default.

View file

@ -0,0 +1,361 @@
{
"uprn": 200004296092,
"roofs": [
{
"description": "Pitched, 150 mm loft insulation",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"walls": [
{
"description": "Solid brick, with external insulation",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"floors": [
{
"description": "Solid, no insulation (assumed)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"status": "entered",
"tenure": 1,
"window": {
"description": "Fully double glazed",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
},
"lighting": {
"description": "Excellent lighting efficiency",
"energy_efficiency_rating": 5,
"environmental_efficiency_rating": 5
},
"postcode": "LL16 3RL",
"hot_water": {
"description": "From main system",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
},
"post_town": "DENBIGH",
"psv_count": 0,
"built_form": 3,
"created_at": "2026-05-14 14:33:05",
"door_count": 2,
"region_code": 7,
"report_type": 2,
"sap_heating": {
"number_baths": 0,
"cylinder_size": 1,
"shower_outlets": [
{
"shower_wwhrs": 1,
"shower_outlet_type": 2
}
],
"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": 1,
"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": 16137
}
],
"immersion_heating_type": "NA",
"has_fixed_air_conditioning": "false"
},
"sap_version": 10.2,
"sap_windows": [
{
"pvc_frame": "true",
"orientation": 5,
"window_type": 1,
"glazing_type": 2,
"window_width": {
"value": 1.07,
"quantity": "m"
},
"window_height": {
"value": 1.34,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 5,
"window_type": 1,
"glazing_type": 2,
"window_width": {
"value": 1.07,
"quantity": "m"
},
"window_height": {
"value": 1.34,
"quantity": "m"
},
"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": {
"value": 0.47,
"quantity": "m"
},
"window_height": {
"value": 0.85,
"quantity": "m"
},
"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": {
"value": 0.47,
"quantity": "m"
},
"window_height": {
"value": 0.85,
"quantity": "m"
},
"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": {
"value": 0.76,
"quantity": "m"
},
"window_height": {
"value": 0.84,
"quantity": "m"
},
"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": "WLS",
"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": "End-terrace house",
"language_code": 1,
"pressure_test": 4,
"property_type": 0,
"address_line_1": "54 Myddleton Avenue",
"assessment_type": "RdSAP",
"completion_date": "2026-05-14",
"inspection_date": "2026-05-14",
"extensions_count": 0,
"measurement_type": 1,
"open_flues_count": 0,
"total_floor_area": 62,
"transaction_type": 1,
"conservatory_type": 1,
"heated_room_count": 3,
"other_flues_count": 0,
"registration_date": "2026-05-14",
"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
},
"closed_flues_count": 0,
"extract_fans_count": 0,
"sap_building_parts": [
{
"identifier": "Main Dwelling",
"wall_dry_lined": "N",
"wall_thickness": 360,
"floor_heat_loss": 7,
"roof_construction": 4,
"wall_construction": 3,
"building_part_number": 1,
"sap_floor_dimensions": [
{
"floor": 0,
"room_height": 2.4,
"floor_insulation": 1,
"total_floor_area": 31.16,
"party_wall_length": 6.79,
"floor_construction": 1,
"heat_loss_perimeter": 15.97
},
{
"floor": 1,
"room_height": 2.4,
"total_floor_area": 31.16,
"party_wall_length": 6.79,
"heat_loss_perimeter": 15.97
}
],
"wall_insulation_type": 1,
"construction_age_band": "B",
"party_wall_construction": 0,
"wall_thickness_measured": "Y",
"roof_insulation_location": 2,
"roof_insulation_thickness": "150mm",
"wall_insulation_thickness": "NI"
}
],
"boilers_flues_count": 0,
"open_chimneys_count": 0,
"solar_water_heating": "N",
"habitable_room_count": 3,
"heating_cost_current": {
"value": 589,
"currency": "GBP"
},
"insulated_door_count": 0,
"co2_emissions_current": 1.8,
"energy_rating_average": 60,
"energy_rating_current": 71,
"lighting_cost_current": {
"value": 50,
"currency": "GBP"
},
"main_heating_controls": [
{
"description": "Programmer, room thermostat and TRVs",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"blocked_chimneys_count": 0,
"has_hot_water_cylinder": "false",
"heating_cost_potential": {
"value": 543,
"currency": "GBP"
},
"hot_water_cost_current": {
"value": 275,
"currency": "GBP"
},
"mechanical_ventilation": 0,
"percent_draughtproofed": 100,
"schema_version_current": "LIG-21.0",
"suggested_improvements": [
{
"sequence": 1,
"typical_saving": 47,
"indicative_cost": "\u00a35,000 - \u00a310,000",
"improvement_type": "W2",
"improvement_details": {
"improvement_number": 58
},
"improvement_category": 5,
"energy_performance_rating": 72,
"environmental_impact_rating": 79
},
{
"sequence": 2,
"typical_saving": 275,
"indicative_cost": "\u00a38,000 - \u00a310,000",
"improvement_type": "U",
"improvement_details": {
"improvement_number": 34
},
"improvement_category": 5,
"energy_performance_rating": 84,
"environmental_impact_rating": 81
}
],
"co2_emissions_potential": 1.4,
"energy_rating_potential": 84,
"lighting_cost_potential": {
"value": 50,
"currency": "GBP"
},
"schema_version_original": "LIG-21.0",
"hot_water_cost_potential": {
"value": 275,
"currency": "GBP"
},
"renewable_heat_incentive": {
"water_heating": 1768.31,
"space_heating_existing_dwelling": 5933.75
},
"draughtproofed_door_count": 2,
"energy_consumption_current": 169,
"has_fixed_air_conditioning": "false",
"multiple_glazed_proportion": 100,
"calculation_software_version": "10.2.2.0",
"energy_consumption_potential": 130,
"environmental_impact_current": 77,
"cfl_fixed_lighting_bulbs_count": 0,
"current_energy_efficiency_band": "C",
"environmental_impact_potential": 81,
"led_fixed_lighting_bulbs_count": 12,
"has_heated_separate_conservatory": "false",
"potential_energy_efficiency_band": "B",
"co2_emissions_current_per_floor_area": 29,
"incandescent_fixed_lighting_bulbs_count": 0
}

View file

@ -0,0 +1,254 @@
Summary Information
Surveyor: P960-0001
Name: Richard Matthew Ratcliff Title: Mr. Tel Number: 07760 443 469
Survey Reference: 001431 My Reference: Khalim-test
Current SAP rating: D 66 Potential SAP rating: C 75 Emissions (t/year): 1.927 tonnes
Current EI rating: C 72 Potential EI rating: C 76 Fuel Bill: £1026
Property Details:
RdSAP version: RdSAP10
Reference Number: P960-0001-001431
My Reference: Khalim-test
Lodgement Required: No
Regs Region: England
EPC Language: English
UPRN:
Postcode: W6 9BF
Region: Thames Valley
House Name: 28 Distillery Wharf
House No: 28
Street: Distillery Wharf
Locality:
Town: London
County:
Property Tenure: Owner-occupied
Transaction Type: Marketed sale
Inspection Date: 25/06/2025
Process date: 02/07/2026
Check for the existence of No
an EPC:
Does an EPC exist at the No
point of carrying out this
energy assessment:
Reason why another energy
assessment needs to be
undertaken:
RdSAP Inputs
Property Description:
1.0 Property type: H House
E End-Terrace
2.0 Number of Storeys: 2
Habitable Rooms: 3
Heated Habitable Rooms: 3
3.0 Date Built: Main Property B 1900-1929
4.0 Dimensions:
Dimension type: Internal
Main Property
Floor Room Heat Loss Party Wall
Area Height Wall Perimeter Length
[m2] [m] [m] [m]
1st Floor: 31.16 2.40 15.97 6.79
Lowest Floor: 31.16 2.40 15.97 6.79 No
5.0 Conservatory:
Is there a conservatory? No
7.0 Walls:
© Elmhurst Energy Systems Limited Registered Office Unit 16, St Johns Business Park, Lutterworth, Leicestershire LE17 4HB
Summary Information
Main Property
Type SO Solid Brick
Insulation E External
Insulation Thickness Unknown
Conductivity Known No
Thermal Conductivity Lambda = 0.04 W/m*K
Wall Thickness Unknown No
Wall Thickness 260 mm
U-value Known No
Party Wall Type S Solid masonry / timber / system build
8.0 Roofs:
Main Property
Type PA Pitched (slates/tiles), access to loft
Insulation J Joists
Insulation Thickness 150 mm
U-value Known No
8.1 Rooms in Roof:
9.0 Floors:
Main Property
Location G Ground floor
Type S Solid
Insulation A As built
Default U-value 0.73
U-value Known No
10.0 Doors:
Total Number of Doors 2
Number of Insulated Doors 0
11.0 Windows:
Frame Frame Glazing Building U g Draught Permanent
W H Area Glazing Type Location Orient. Data-Source
Type Factor Gap Part value value Proofed Shutters
Double with unknown
4.31 1.00 4.31 PVC 0.70 12 mm Main External wall North Manufacturer 2.80 0.76 Yes None
install date
Double with unknown
2.89 1.00 2.89 PVC 0.70 12 mm Main External wall West Manufacturer 2.80 0.76 Yes None
install date
Double with unknown
2.89 1.00 2.89 PVC 0.70 12 mm Main External wall North Manufacturer 2.80 0.76 Yes None
install date
Double with unknown
2.89 1.00 2.89 PVC 0.70 12 mm Main External wall East Manufacturer 2.80 0.76 Yes None
install date
Draught Proofing 67 %
12.0 Ventilation & Cooling
No. of open chimneys 0
No. of open flues 0
No. of open chimneys/open flues attached to closed fire 0
No. of flues attached to solid fuel boiler 0
No. of open flues attached to other heater 0
No. of blocked chimneys 0
No. of intermittent extract fans 0
No. of passive vents 0
No. of flueless gas fires 0
Fixed Space Cooling No
Draught Lobby Unable to determine
© Elmhurst Energy Systems Limited Registered Office Unit 16, St Johns Business Park, Lutterworth, Leicestershire LE17 4HB
Summary Information
12.1 Mechanical Ventilation
Mechanical Ventilation No
12.2 Air Pressure Test
Test Method Not available
13.0 Lighting
Total number of bulbs 6
Number of LED and CFL Known Yes
Number of LED lights 0
Number of CFL lights 0
Total number of Low Energy 0
Total number of incandescents 6
14.0 Main Heating1
PCDF boiler Reference 0
Main Heating EES Code BGW
Main Heating SAP Code 104
Heat Emitter Radiators
Heat pump age 2013 or later
Flue Type Balanced
Fan Assisted Flue Yes
Design flow temperature <= 35°C
PCDF Heating Controls 0
Main Heating Controls EES CBE
Main Heating Controls Sap SAP code 2106, Programmer, room thermostat and TRVs
PCDF Compensator 0
Percentage of Heat 100 %
14.1 Main Heating2
PCDF boiler Reference 0
Main Heating EES Code
Main Heating SAP Code 0
Percentage of Heat 0%
14.1 Community Heating/Heat Network
Heating Type None
14.2 Meters
Electricity meter type Single
Main gas Yes
Electricity Smart Meter Present No
Gas Smart Meter Present No
15.0 Water Heating
Water Heating Code HWP
Water Heating SapCode 901
Water Heating Fuel Type Mains gas
15.1 Hot Water Cylinder
Hot Water Cylinder Present No
15.2 Community Hot Water
PCDF boiler Reference 0
16.0 Solar water heating
Solar Water Heating No
17.0 Waste Water Heat Recovery System
Is WWHRS present in the property? No / Unknown
1x.0 Baths and Showers
Total Number of Baths 2
Number of Baths Connected 0
Description Type Connected
1 Non-electric shower None
© Elmhurst Energy Systems Limited Registered Office Unit 16, St Johns Business Park, Lutterworth, Leicestershire LE17 4HB
Summary Information
18.0 Flue Gas Heat Recovery System
Present No
19.0 Photovoltaic Panel
Photovoltaic Panel None
Export capable meter No
20.0 Wind Turbine
Terrain Type Urban
Wind turbine present? No
22.0 Special Features
21.0 Small-Scale Hydro
Electricity generated [kWh/year] 0.00
© Elmhurst Energy Systems Limited Registered Office Unit 16, St Johns Business Park, Lutterworth, Leicestershire LE17 4HB
Summary Information
Recommendations
Loft insulation (SAP increase too small)
Flat roof insulation (Not applicable)
Room-in-roof insulation (Not applicable)
Cavity wall insulation (Not applicable)
Solid wall insulation (SAP increase too small)
Floor insulation (solid floor) (Recommended)
Hot water cylinder insulation (Not applicable)
Draught proofing (SAP increase too small)
Low energy lighting (Recommended)
Cylinder thermostat (Not applicable)
Heating controls for wet central heating system (Already installed)
Upgrade boiler, same fuel (Already installed)
Change heating to condensing gas condensing boiler (fuel switch) (Not applicable)
Flue gas heat recovery in conjunction with new boiler (Not applicable)
Solar water heating (SAP increase too small)
Heat recovery system for mixer showers (SAP increase too small)
Double glazed windows (Already installed)
Insulated doors (SAP increase too small)
Solar photovoltaic panels (Recommended)
Wind turbine (Not applicable)
PV diverter (Not applicable)
PV battery (Not applicable)
Water heating controls (Not applicable)
Alternative Recommendations
External wall insulation with cavity insulation (Not applicable)
Biomass boiler (alternative) (Not applicable)
Micro CHP (alternative) (Not applicable)
Related Party Disclosure
No related party
Addenda
When considering the PV installation consider installing PV battery and a PV diverter for water heating
© Elmhurst Energy Systems Limited Registered Office Unit 16, St Johns Business Park, Lutterworth, Leicestershire LE17 4HB

View file

@ -0,0 +1,384 @@
{
"uprn": 217091901,
"roofs": [
{
"description": "Flat, no insulation",
"energy_efficiency_rating": 1,
"environmental_efficiency_rating": 1
}
],
"walls": [
{
"description": "Solid brick, as built, no insulation (assumed)",
"energy_efficiency_rating": 2,
"environmental_efficiency_rating": 2
}
],
"floors": [
{
"description": "(another dwelling below)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"status": "entered",
"tenure": 3,
"window": {
"description": "Single glazed",
"energy_efficiency_rating": 1,
"environmental_efficiency_rating": 1
},
"lighting": {
"description": "Good lighting efficiency",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
},
"postcode": "W11 1EP",
"hot_water": {
"description": "From main system",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
},
"post_town": "LONDON",
"built_form": "NR",
"created_at": "2026-05-16 15:45:17",
"door_count": 0,
"region_code": 17,
"report_type": 2,
"sap_heating": {
"number_baths": 1,
"cylinder_size": 1,
"shower_outlets": [
{
"shower_wwhrs": 1,
"shower_outlet_type": 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": 2107,
"main_heating_category": 2,
"main_heating_fraction": 1,
"central_heating_pump_age": 0,
"main_heating_data_source": 1,
"main_heating_index_number": 18658
}
],
"immersion_heating_type": "NA",
"has_fixed_air_conditioning": "false"
},
"sap_version": 10.2,
"sap_windows": [
{
"pvc_frame": "true",
"orientation": 5,
"window_type": 1,
"glazing_type": 5,
"window_width": 1.2,
"window_height": 1.8,
"draught_proofed": "false",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 5,
"window_type": 1,
"glazing_type": 5,
"window_width": 1.2,
"window_height": 1.8,
"draught_proofed": "false",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 9,
"window_type": 2,
"glazing_type": 5,
"window_width": 0.5,
"window_height": 0.5,
"draught_proofed": "false",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 9,
"window_type": 2,
"glazing_type": 5,
"window_width": 0.7,
"window_height": 0.5,
"draught_proofed": "false",
"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": 5,
"window_width": 1.2,
"window_height": 1.8,
"draught_proofed": "false",
"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": 5,
"window_width": 1.1,
"window_height": 1.4,
"draught_proofed": "false",
"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": "Top-floor flat",
"language_code": 1,
"pressure_test": 4,
"property_type": 2,
"address_line_1": "Second Floor Flat",
"address_line_2": "216 Westbourne Park Road",
"assessment_type": "RdSAP",
"completion_date": "2026-05-16",
"inspection_date": "2026-05-16",
"extensions_count": 0,
"measurement_type": 1,
"sap_flat_details": {
"level": 3,
"top_storey": "Y",
"storey_count": 3,
"flat_location": 2,
"heat_loss_corridor": 0
},
"total_floor_area": 63,
"transaction_type": 8,
"conservatory_type": 1,
"has_draught_lobby": "true",
"heated_room_count": 3,
"registration_date": "2026-05-16",
"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
},
"sap_building_parts": [
{
"identifier": "Main Dwelling",
"wall_dry_lined": "N",
"wall_thickness": 280,
"floor_heat_loss": 6,
"roof_construction": 1,
"wall_construction": 3,
"building_part_number": 1,
"sap_floor_dimensions": [
{
"floor": 0,
"room_height": {
"value": 2.85,
"quantity": "metres"
},
"total_floor_area": {
"value": 63.29,
"quantity": "square metres"
},
"party_wall_length": {
"value": 22.8,
"quantity": "metres"
},
"heat_loss_perimeter": {
"value": 14,
"quantity": "metres"
}
}
],
"wall_insulation_type": 4,
"construction_age_band": "A",
"party_wall_construction": 0,
"wall_thickness_measured": "Y",
"roof_insulation_location": 6,
"wall_insulation_thickness": "NI",
"flat_roof_insulation_thickness": "AB"
}
],
"solar_water_heating": "N",
"habitable_room_count": 3,
"heating_cost_current": {
"value": 967,
"currency": "GBP"
},
"insulated_door_count": 0,
"co2_emissions_current": 2.8,
"energy_rating_average": 60,
"energy_rating_current": 62,
"lighting_cost_current": {
"value": 43,
"currency": "GBP"
},
"main_heating_controls": [
{
"description": "Programmer, TRVs and bypass",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
}
],
"blocked_chimneys_count": 1,
"has_hot_water_cylinder": "false",
"heating_cost_potential": {
"value": 330,
"currency": "GBP"
},
"hot_water_cost_current": {
"value": 167,
"currency": "GBP"
},
"mechanical_ventilation": 0,
"percent_draughtproofed": 0,
"suggested_improvements": [
{
"sequence": 1,
"typical_saving": {
"value": 411,
"currency": "GBP"
},
"indicative_cost": "\u00a3900 - \u00a31,200",
"improvement_type": "A2",
"improvement_details": {
"improvement_number": 45
},
"improvement_category": 5,
"energy_performance_rating": 74,
"environmental_impact_rating": 76
},
{
"sequence": 2,
"typical_saving": {
"value": 129,
"currency": "GBP"
},
"indicative_cost": "\u00a37,500 - \u00a311,000",
"improvement_type": "Q",
"improvement_details": {
"improvement_number": 7
},
"improvement_category": 5,
"energy_performance_rating": 78,
"environmental_impact_rating": 81
},
{
"sequence": 3,
"typical_saving": {
"value": 18,
"currency": "GBP"
},
"indicative_cost": "\u00a3150 - \u00a3250",
"improvement_type": "D",
"improvement_details": {
"improvement_number": 10
},
"improvement_category": 5,
"energy_performance_rating": 79,
"environmental_impact_rating": 82
},
{
"sequence": 4,
"typical_saving": {
"value": 78,
"currency": "GBP"
},
"indicative_cost": "\u00a34,500 - \u00a36,000",
"improvement_type": "O",
"improvement_details": {
"improvement_number": 8
},
"improvement_category": 5,
"energy_performance_rating": 81,
"environmental_impact_rating": 85
}
],
"co2_emissions_potential": 1.0,
"energy_rating_potential": 81,
"lighting_cost_potential": {
"value": 45,
"currency": "GBP"
},
"schema_version_original": "21.0.1",
"hot_water_cost_potential": {
"value": 167,
"currency": "GBP"
},
"renewable_heat_incentive": {
"water_heating": 2022.69,
"space_heating_existing_dwelling": 9246.82
},
"draughtproofed_door_count": 0,
"energy_consumption_current": 245,
"has_fixed_air_conditioning": "false",
"multiple_glazed_proportion": 0,
"calculation_software_version": "5.02r0344",
"energy_consumption_potential": 90,
"environmental_impact_current": 59,
"cfl_fixed_lighting_bulbs_count": 2,
"current_energy_efficiency_band": "D",
"environmental_impact_potential": 85,
"led_fixed_lighting_bulbs_count": 9,
"has_heated_separate_conservatory": "false",
"potential_energy_efficiency_band": "B",
"co2_emissions_current_per_floor_area": 45,
"incandescent_fixed_lighting_bulbs_count": 0
}

View file

@ -0,0 +1,299 @@
{
"uprn": 4510053280,
"roofs": [
{
"description": "(another dwelling above)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"walls": [
{
"description": "Solid brick, as built, no insulation (assumed)",
"energy_efficiency_rating": 2,
"environmental_efficiency_rating": 2
},
{
"description": "Solid brick, as built, partial insulation (assumed)",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
}
],
"floors": [
{
"description": "Solid, no insulation (assumed)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"status": "entered",
"tenure": 2,
"window": {
"description": "Full secondary glazing",
"energy_efficiency_rating": 2,
"environmental_efficiency_rating": 2
},
"lighting": {
"description": "Good lighting efficiency",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
},
"postcode": "NE1 4DG",
"hot_water": {
"description": "From main system, no cylinder thermostat",
"energy_efficiency_rating": 1,
"environmental_efficiency_rating": 4
},
"post_town": "NEWCASTLE UPON TYNE",
"built_form": "NR",
"created_at": "2026-05-24 12:41:32",
"door_count": 0,
"region_code": 1,
"report_type": 2,
"sap_heating": {
"number_baths": 1,
"cylinder_size": 1,
"number_baths_wwhrs": 0,
"water_heating_code": 901,
"water_heating_fuel": 29,
"secondary_fuel_type": 29,
"main_heating_details": [
{
"has_fghrs": "N",
"main_fuel_type": 29,
"heat_emitter_type": 1,
"emitter_temperature": 0,
"main_heating_number": 1,
"main_heating_control": 2203,
"main_heating_category": 4,
"main_heating_fraction": 1,
"mcs_installed_heat_pump": "false",
"central_heating_pump_age": 0,
"main_heating_data_source": 1,
"main_heating_index_number": 100053
}
],
"immersion_heating_type": "NA",
"secondary_heating_type": 691,
"has_fixed_air_conditioning": "false"
},
"sap_version": 10.2,
"sap_windows": [
{
"orientation": 4,
"window_type": 1,
"glazing_type": 4,
"window_width": 1.3,
"window_height": 1.46,
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"orientation": 4,
"window_type": 1,
"glazing_type": 4,
"window_width": 1.3,
"window_height": 1.46,
"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": "Air source heat pump, radiators, electric",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 5
}
],
"air_tightness": {
"description": "(not tested)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"dwelling_type": "Ground-floor flat",
"language_code": 1,
"pressure_test": 4,
"property_type": 2,
"address_line_1": "64 Waterloo Street",
"assessment_type": "RdSAP",
"completion_date": "2026-05-24",
"inspection_date": "2026-05-22",
"extensions_count": 0,
"measurement_type": 1,
"sap_flat_details": {
"level": 1,
"top_storey": "N",
"storey_count": 3,
"flat_location": 0,
"heat_loss_corridor": 2,
"unheated_corridor_length": 3.5
},
"total_floor_area": 47,
"transaction_type": 8,
"conservatory_type": 1,
"has_draught_lobby": "true",
"heated_room_count": 2,
"registration_date": "2026-05-24",
"sap_energy_source": {
"mains_gas": "N",
"meter_type": 2,
"pv_connection": 0,
"photovoltaic_supply": {
"none_or_no_details": {
"percent_roof_area": 0
}
},
"wind_turbines_count": 0,
"gas_smart_meter_present": "false",
"is_dwelling_export_capable": "false",
"wind_turbines_terrain_type": 1,
"electricity_smart_meter_present": "false"
},
"secondary_heating": {
"description": "Room heaters, electric",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"lzc_energy_sources": [
9
],
"sap_building_parts": [
{
"identifier": "Main Dwelling",
"wall_dry_lined": "N",
"wall_thickness": 520,
"floor_heat_loss": 7,
"roof_construction": 3,
"wall_construction": 3,
"building_part_number": 1,
"sap_floor_dimensions": [
{
"floor": 0,
"room_height": {
"value": 2.32,
"quantity": "metres"
},
"floor_insulation": 1,
"total_floor_area": {
"value": 47,
"quantity": "square metres"
},
"party_wall_length": {
"value": 8,
"quantity": "metres"
},
"floor_construction": 1,
"heat_loss_perimeter": {
"value": 14.15,
"quantity": "metres"
}
}
],
"wall_insulation_type": 4,
"construction_age_band": "B",
"sap_alternative_wall_1": {
"wall_area": 8.12,
"sheltered_wall": "Y",
"wall_dry_lined": "N",
"wall_thickness": 520,
"wall_construction": 3,
"wall_insulation_type": 4,
"wall_thickness_measured": "Y",
"wall_insulation_thickness": "NI"
},
"party_wall_construction": 0,
"wall_thickness_measured": "Y",
"roof_insulation_location": "ND",
"roof_insulation_thickness": "ND",
"wall_insulation_thickness": "NI",
"floor_insulation_thickness": "NI"
}
],
"solar_water_heating": "N",
"habitable_room_count": 2,
"heating_cost_current": {
"value": 278,
"currency": "GBP"
},
"insulated_door_count": 0,
"co2_emissions_current": 0.8,
"energy_rating_average": 60,
"energy_rating_current": 51,
"lighting_cost_current": {
"value": 39,
"currency": "GBP"
},
"main_heating_controls": [
{
"description": "Room thermostat only",
"energy_efficiency_rating": 2,
"environmental_efficiency_rating": 2
}
],
"has_hot_water_cylinder": "false",
"heating_cost_potential": {
"value": 272,
"currency": "GBP"
},
"hot_water_cost_current": {
"value": 1295,
"currency": "GBP"
},
"mechanical_ventilation": 0,
"percent_draughtproofed": 100,
"suggested_improvements": [
{
"sequence": 1,
"typical_saving": {
"value": 68,
"currency": "GBP"
},
"indicative_cost": "\u00a3220 - \u00a3250",
"improvement_type": "G",
"improvement_details": {
"improvement_number": 16
},
"improvement_category": 5,
"energy_performance_rating": 53,
"environmental_impact_rating": 88
}
],
"co2_emissions_potential": 0.8,
"energy_rating_potential": 53,
"lighting_cost_potential": {
"value": 39,
"currency": "GBP"
},
"schema_version_original": "21.0.1",
"hot_water_cost_potential": {
"value": 1233,
"currency": "GBP"
},
"renewable_heat_incentive": {
"water_heating": 5072.68,
"space_heating_existing_dwelling": 2884.0
},
"draughtproofed_door_count": 0,
"energy_consumption_current": 190,
"has_fixed_air_conditioning": "false",
"multiple_glazed_proportion": 100,
"calculation_software_version": "5.02r0344",
"energy_consumption_potential": 182,
"environmental_impact_current": 88,
"current_energy_efficiency_band": "E",
"environmental_impact_potential": 88,
"has_heated_separate_conservatory": "false",
"potential_energy_efficiency_band": "E",
"co2_emissions_current_per_floor_area": 18,
"low_energy_fixed_lighting_bulbs_count": 6,
"incandescent_fixed_lighting_bulbs_count": 0
}

View file

@ -0,0 +1,324 @@
{
"uprn": 47084930,
"roofs": [
{
"description": "Flat, insulated",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
}
],
"walls": [
{
"description": "Cavity wall, with internal insulation",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"floors": [
{
"description": "(other premises below)",
"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": "Excellent lighting efficiency",
"energy_efficiency_rating": 5,
"environmental_efficiency_rating": 5
},
"postcode": "NE12 8AQ",
"hot_water": {
"description": "From main system",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
},
"post_town": "NEWCASTLE UPON TYNE",
"built_form": "NR",
"created_at": "2026-05-06 10:59:38",
"door_count": 0,
"region_code": 1,
"report_type": 2,
"sap_heating": {
"number_baths": 1,
"cylinder_size": 1,
"shower_outlets": [
{
"shower_wwhrs": 1,
"shower_outlet_type": 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": 17815
}
],
"immersion_heating_type": "NA",
"has_fixed_air_conditioning": "false"
},
"sap_version": 10.2,
"sap_windows": [
{
"pvc_frame": "true",
"orientation": 3,
"window_type": 1,
"glazing_type": 2,
"window_width": 1.23,
"window_height": 1.61,
"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.23,
"window_height": 1.61,
"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.23,
"window_height": 1.61,
"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.23,
"window_height": 1.61,
"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.23,
"window_height": 1.61,
"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": "Top-floor flat",
"language_code": 1,
"pressure_test": 4,
"property_type": 2,
"address_line_1": "83 Station Road",
"address_line_2": "Forest Hall",
"assessment_type": "RdSAP",
"completion_date": "2026-05-06",
"inspection_date": "2026-05-05",
"extensions_count": 0,
"measurement_type": 1,
"sap_flat_details": {
"level": 3,
"top_storey": "Y",
"storey_count": 2,
"flat_location": 1,
"heat_loss_corridor": 1
},
"total_floor_area": 64,
"transaction_type": 8,
"conservatory_type": 1,
"has_draught_lobby": "true",
"heated_room_count": 3,
"registration_date": "2026-05-06",
"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
},
"sap_building_parts": [
{
"identifier": "Main Dwelling",
"wall_dry_lined": "N",
"wall_thickness": 380,
"floor_heat_loss": 3,
"roof_construction": 1,
"wall_construction": 4,
"building_part_number": 1,
"sap_floor_dimensions": [
{
"floor": 0,
"room_height": {
"value": 2.32,
"quantity": "metres"
},
"total_floor_area": {
"value": 64.26,
"quantity": "square metres"
},
"party_wall_length": {
"value": 18.15,
"quantity": "metres"
},
"heat_loss_perimeter": {
"value": 18.15,
"quantity": "metres"
}
}
],
"wall_insulation_type": 3,
"construction_age_band": "E",
"party_wall_construction": 0,
"wall_thickness_measured": "Y",
"roof_insulation_location": 6,
"wall_insulation_thickness": "100mm",
"flat_roof_insulation_thickness": "75mm"
}
],
"solar_water_heating": "N",
"habitable_room_count": 3,
"heating_cost_current": {
"value": 623,
"currency": "GBP"
},
"insulated_door_count": 0,
"co2_emissions_current": 2.0,
"energy_rating_average": 60,
"energy_rating_current": 74,
"lighting_cost_current": {
"value": 42,
"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": 553,
"currency": "GBP"
},
"hot_water_cost_current": {
"value": 207,
"currency": "GBP"
},
"mechanical_ventilation": 0,
"percent_draughtproofed": 100,
"suggested_improvements": [
{
"sequence": 1,
"typical_saving": {
"value": 69,
"currency": "GBP"
},
"indicative_cost": "\u00a3900 - \u00a31,200",
"improvement_type": "A2",
"improvement_details": {
"improvement_number": 45
},
"improvement_category": 5,
"energy_performance_rating": 76,
"environmental_impact_rating": 78
}
],
"co2_emissions_potential": 1.8,
"energy_rating_potential": 76,
"lighting_cost_potential": {
"value": 42,
"currency": "GBP"
},
"schema_version_original": "21.0.1",
"hot_water_cost_potential": {
"value": 208,
"currency": "GBP"
},
"renewable_heat_incentive": {
"water_heating": 2384.75,
"space_heating_existing_dwelling": 5681.26
},
"draughtproofed_door_count": 0,
"energy_consumption_current": 170,
"has_fixed_air_conditioning": "false",
"multiple_glazed_proportion": 100,
"calculation_software_version": "5.02r0344",
"energy_consumption_potential": 154,
"environmental_impact_current": 76,
"current_energy_efficiency_band": "C",
"environmental_impact_potential": 78,
"led_fixed_lighting_bulbs_count": 9,
"has_heated_separate_conservatory": "false",
"potential_energy_efficiency_band": "C",
"co2_emissions_current_per_floor_area": 31,
"incandescent_fixed_lighting_bulbs_count": 0
}

View file

@ -0,0 +1,315 @@
{
"uprn": 74061136,
"roofs": [
{
"description": "(another dwelling above)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"walls": [
{
"description": "Solid brick, with internal insulation",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
},
{
"description": "Timber frame, with additional insulation",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"floors": [
{
"description": "(other premises below)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"status": "entered",
"tenure": 1,
"window": {
"description": "Fully double glazed",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
},
"lighting": {
"description": "Excellent lighting efficiency",
"energy_efficiency_rating": 5,
"environmental_efficiency_rating": 5
},
"postcode": "S41 7TP",
"hot_water": {
"description": "Electric instantaneous at point of use",
"energy_efficiency_rating": 1,
"environmental_efficiency_rating": 5
},
"post_town": "Chesterfield,",
"psv_count": 0,
"built_form": 4,
"created_at": "2026-05-20 13:45:13",
"door_count": 1,
"region_code": 6,
"report_type": 2,
"sap_heating": {
"number_baths": 1,
"cylinder_size": 1,
"shower_outlets": [
{
"shower_wwhrs": 1,
"shower_outlet_type": 1
}
],
"number_baths_wwhrs": 0,
"water_heating_code": 909,
"water_heating_fuel": 29,
"cylinder_thermostat": "N",
"main_heating_details": [
{
"has_fghrs": "N",
"main_fuel_type": 29,
"storage_heaters": [
{
"index_number": 230002,
"number_of_heaters": 1,
"high_heat_retention": "true"
}
],
"fan_flue_present": "Y",
"heat_emitter_type": 0,
"emitter_temperature": "NA",
"main_heating_number": 1,
"main_heating_control": 2404,
"main_heating_category": 7,
"main_heating_fraction": 50,
"sap_main_heating_code": 409,
"main_heating_data_source": 2
}
],
"immersion_heating_type": "NA",
"has_fixed_air_conditioning": "false"
},
"sap_version": 10.2,
"sap_windows": [
{
"pvc_frame": "true",
"orientation": 5,
"window_type": 1,
"glazing_type": 2,
"window_width": {
"value": 1,
"quantity": "m"
},
"window_height": {
"value": 1.6,
"quantity": "m"
},
"draught_proofed": "true",
"window_location": 0,
"window_wall_type": 1,
"permanent_shutters_present": "N",
"permanent_shutters_insulated": "N"
},
{
"pvc_frame": "true",
"orientation": 5,
"window_type": 1,
"glazing_type": 2,
"window_width": {
"value": 1,
"quantity": "m"
},
"window_height": {
"value": 1.6,
"quantity": "m"
},
"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": "Address Matched",
"country_code": "ENG",
"main_heating": [
{
"description": "Electric storage heaters",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 5
}
],
"air_tightness": {
"description": "(not tested)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"dwelling_type": "Mid-floor flat",
"language_code": 1,
"pressure_test": 4,
"property_type": 2,
"address_line_1": "Flat 3,",
"address_line_2": "14-20 Corporation Street,",
"assessment_type": "RdSAP",
"completion_date": "2026-05-20",
"inspection_date": "2026-05-20",
"extensions_count": 0,
"measurement_type": 1,
"open_flues_count": 0,
"sap_flat_details": {
"level": 2,
"top_storey": "N",
"storey_count": 3,
"flat_location": 1,
"heat_loss_corridor": 2,
"unheated_corridor_length": 6
},
"total_floor_area": 24,
"transaction_type": 1,
"conservatory_type": 2,
"heated_room_count": 1,
"other_flues_count": 0,
"registration_date": "2026-05-20",
"sap_energy_source": {
"mains_gas": "N",
"meter_type": 3,
"pv_connection": 0,
"photovoltaic_supply": {
"none_or_no_details": {
"percent_roof_area": 0
}
},
"wind_turbines_count": 0,
"gas_smart_meter_present": "false",
"is_dwelling_export_capable": "false",
"wind_turbines_terrain_type": 2,
"electricity_smart_meter_present": "false"
},
"secondary_heating": {
"description": "Portable electric heaters (assumed)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"closed_flues_count": 0,
"extract_fans_count": 0,
"sap_building_parts": [
{
"identifier": "Main Dwelling",
"wall_dry_lined": "N",
"wall_thickness": 300,
"floor_heat_loss": 3,
"roof_construction": 3,
"wall_construction": 3,
"building_part_number": 1,
"sap_floor_dimensions": [
{
"floor": 0,
"room_height": 2.4,
"total_floor_area": 24,
"party_wall_length": 8,
"heat_loss_perimeter": 12
}
],
"wall_insulation_type": 3,
"construction_age_band": "B",
"sap_alternative_wall_1": {
"wall_area": 14.4,
"sheltered_wall": "Y",
"wall_dry_lined": "N",
"wall_thickness": 150,
"wall_construction": 5,
"wall_insulation_type": 3,
"wall_thickness_measured": "Y",
"wall_insulation_thickness": "NI"
},
"party_wall_construction": 0,
"wall_thickness_measured": "Y",
"roof_insulation_location": "ND",
"roof_insulation_thickness": "ND",
"wall_insulation_thickness": "measured",
"wall_insulation_thickness_measured": 25,
"wall_insulation_thermal_conductivity": 3
}
],
"boilers_flues_count": 0,
"open_chimneys_count": 0,
"solar_water_heating": "N",
"habitable_room_count": 1,
"heating_cost_current": {
"value": 348,
"currency": "GBP"
},
"insulated_door_count": 0,
"co2_emissions_current": 0.4,
"energy_rating_average": 60,
"energy_rating_current": 73,
"lighting_cost_current": {
"value": 23,
"currency": "GBP"
},
"main_heating_controls": [
{
"description": "Controls for high heat retention storage heaters",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"blocked_chimneys_count": 0,
"has_hot_water_cylinder": "false",
"heating_cost_potential": {
"value": 348,
"currency": "GBP"
},
"hot_water_cost_current": {
"value": 344,
"currency": "GBP"
},
"mechanical_ventilation": 0,
"percent_draughtproofed": 100,
"schema_version_current": "LIG-21.0",
"suggested_improvements": [
{
"sequence": 1,
"typical_saving": 66,
"indicative_cost": "\u00a3600 - \u00a31,500",
"improvement_type": "Y",
"improvement_details": {
"improvement_number": 49
},
"improvement_category": 5,
"energy_performance_rating": 76,
"environmental_impact_rating": 92
}
],
"co2_emissions_potential": 0.4,
"energy_rating_potential": 76,
"lighting_cost_potential": {
"value": 23,
"currency": "GBP"
},
"schema_version_original": "LIG-21.0",
"hot_water_cost_potential": {
"value": 278,
"currency": "GBP"
},
"renewable_heat_incentive": {
"water_heating": 1079.39,
"space_heating_existing_dwelling": 2013.05
},
"draughtproofed_door_count": 1,
"energy_consumption_current": 200,
"has_fixed_air_conditioning": "false",
"multiple_glazed_proportion": 100,
"calculation_software_version": "10.2.2.0",
"energy_consumption_potential": 187,
"environmental_impact_current": 91,
"cfl_fixed_lighting_bulbs_count": 0,
"current_energy_efficiency_band": "C",
"environmental_impact_potential": 92,
"led_fixed_lighting_bulbs_count": 6,
"has_heated_separate_conservatory": "false",
"potential_energy_efficiency_band": "C",
"co2_emissions_current_per_floor_area": 19,
"incandescent_fixed_lighting_bulbs_count": 0
}

View file

@ -2888,6 +2888,15 @@ def _hot_water_fuel_cost_gbp_per_kwh(
)
blended = high_frac * high_rate + (1.0 - high_frac) * low_rate
return blended * _PENCE_TO_GBP
if water_heating_code in _INSTANTANEOUS_WATER_CODES:
# SAP 10.2 §12 (PDF line 2680): the Table-13 off-peak on-peak HW
# split is keyed on the CYLINDER size — it presumes stored water
# charged overnight. An instantaneous point-of-use heater (WHC
# 907/909, no cylinder) heats on demand and cannot shift to the
# off-peak window, so 100% bills at the HIGH rate (Table 12a WH
# high-rate fraction 1.0), not the off-peak low rate.
high_rate, _low = _tariff_high_low_rates_p_per_kwh(tariff)
return high_rate * _PENCE_TO_GBP
return _off_peak_low_rate_gbp_per_kwh(tariff)
if water_heating_fuel is not None:
return prices.unit_price_p_per_kwh(water_heating_fuel) * _PENCE_TO_GBP
@ -2942,6 +2951,11 @@ def _hot_water_high_rate_fraction(
single_immersion=effective_single,
tariff=tariff,
)
if water_heating_code in _INSTANTANEOUS_WATER_CODES:
# Instantaneous point-of-use electric DHW (WHC 907/909) has no
# cylinder to charge overnight — it heats on demand, wholly at the
# high/day rate (see `_hot_water_fuel_cost_gbp_per_kwh`).
return 1.0
return 0.0
@ -5307,6 +5321,35 @@ _AGE_BANDS_A_TO_E: Final[frozenset[str]] = frozenset({"A", "B", "C", "D", "E"})
_SUSPENDED_TIMBER_FLOOR_TYPE: Final[str] = "Suspended timber"
_GROUND_FLOOR_TYPE: Final[str] = "Ground floor"
_FLOOR_U_SEALED_THRESHOLD: Final[float] = 0.5
# Table 19 footnote (1): age bands whose default floor_construction is
# suspended timber when the construction is unknown. Mirrors
# `_SUSPENDED_TIMBER_DEFAULT_BANDS` in rdsap_uvalues.py — duplicated
# locally rather than cross-imported (private symbol) per this module's
# existing convention (see `_main_floor_u_value`).
_TABLE_19_FOOTNOTE_1_SUSPENDED_TIMBER_BANDS: Final[frozenset[str]] = frozenset({"A", "B"})
def _effective_floor_description(
epc: EpcPropertyData, main: SapBuildingPart,
) -> Optional[str]:
"""Mirror `heat_transmission_section_from_cert`'s
`effective_floor_description` rule: the per-bp `floor_construction_type`
lodgement ("Suspended timber" / "Solid") takes precedence over the
global `epc.floors[].description` since it's the explicit per-part
Elmhurst Summary §3/§9 lodgement. Falls back to the joined global
description when the per-bp field is unlodged (the gov-API mapper
often leaves it `None`). Inlined (vs importing from
heat_transmission) to keep cert_to_inputs free of cross-module
private symbol imports.
"""
if main.floor_construction_type:
return main.floor_construction_type
descs = [
d for d in
(getattr(f, "description", None) for f in (epc.floors or []))
if d
]
return " | ".join(descs) if descs else None
def _main_floor_u_value(epc: EpcPropertyData) -> Optional[float]:
@ -5343,21 +5386,6 @@ def _main_floor_u_value(epc: EpcPropertyData) -> Optional[float]:
int(raw_floor_ins) if isinstance(raw_floor_ins, (int, float))
else (0 if raw_floor_ins == "NI" else None)
)
# Mirror heat_transmission's `effective_floor_description`: the per-bp
# `floor_construction_type` takes precedence over a joined
# `epc.floors[].description` since the per-part lodgement is the
# explicit Elmhurst Summary §3/§9 surface. Inline the join (vs
# importing from heat_transmission) to keep cert_to_inputs free of
# cross-module private symbol imports.
if main.floor_construction_type:
effective_floor_description = main.floor_construction_type
else:
descs = [
d for d in
(getattr(f, "description", None) for f in (epc.floors or []))
if d
]
effective_floor_description = " | ".join(descs) if descs else None
return u_floor(
country=Country.from_code(epc.country_code) if epc.country_code else None,
age_band=main.construction_age_band,
@ -5366,10 +5394,49 @@ def _main_floor_u_value(epc: EpcPropertyData) -> Optional[float]:
area_m2=ground_fd.total_floor_area_m2,
perimeter_m=ground_fd.heat_loss_perimeter_m,
wall_thickness_mm=main.wall_thickness_mm,
description=effective_floor_description,
description=_effective_floor_description(epc, main),
)
def _floor_construction_is_suspended_timber(
epc: EpcPropertyData, main: SapBuildingPart,
) -> bool:
"""RdSAP 10 Table 19 footnote (1) construction resolution for the §5
(12) floor-infiltration rule. Tri-state, mirroring `u_floor`'s
`_floor_is_suspended_from_description` / `_SUSPENDED_TIMBER_DEFAULT_
BANDS` cascade (rdsap_uvalues.py) so the (12) infiltration rule and
the floor U-value calc agree on which floors are suspended timber:
1. An explicit construction lodgement (`_effective_floor_description`
per-bp `floor_construction_type` else the joined
`epc.floors[].description`) decides: "Suspended, ..." is timber
UNLESS it's "Suspended, not timber" (suspended concrete etc. has
no floorboard-gap infiltration); "Solid, ..." is not suspended.
2. No description signal AND no lodged `floor_construction` code at
all Table 19 footnote (1): unknown construction defaults to
suspended timber only for age bands A/B.
"""
desc = _effective_floor_description(epc, main)
if desc is not None:
d = desc.strip().lower()
if d.startswith("suspended"):
return "not timber" not in d
if d.startswith("solid"):
return False
ground_fd = next(
(fd for fd in main.sap_floor_dimensions if fd.floor == 0),
main.sap_floor_dimensions[0] if main.sap_floor_dimensions else None,
)
construction_known = (
ground_fd is not None
and _int_or_none(ground_fd.floor_construction) is not None
)
if construction_known:
return False
age = (main.construction_age_band or "").strip().upper()
return age in _TABLE_19_FOOTNOTE_1_SUSPENDED_TIMBER_BANDS
def _has_suspended_timber_floor_per_spec(
epc: EpcPropertyData,
) -> tuple[bool, bool]:
@ -5395,9 +5462,13 @@ def _has_suspended_timber_floor_per_spec(
infiltration 0.2.
The rule only applies when the Main bp's lowest floor is a
"Ground floor" with "Suspended timber" construction. All other
combinations fall through to `(False, False)` and the cascade
enters 0 for (12).
"Ground floor" with "Suspended timber" construction. Construction is
resolved the same tri-state way `u_floor` resolves it (RdSAP 10
Table 19 footnote 1): the explicit per-bp `floor_construction_type` /
joined `epc.floors[].description` lodgement, else when neither
lodges a construction signal the footnote-1 age-band default
(suspended timber for bands A/B only). All other combinations fall
through to `(False, False)` and the cascade enters 0 for (12).
"""
if not epc.sap_building_parts:
return False, False
@ -5407,7 +5478,7 @@ def _has_suspended_timber_floor_per_spec(
return True, False
if main.floor_type != _GROUND_FLOOR_TYPE:
return False, False
if main.floor_construction_type != _SUSPENDED_TIMBER_FLOOR_TYPE:
if not _floor_construction_is_suspended_timber(epc, main):
return False, False
age = (main.construction_age_band or "").strip().upper()
if age in _AGE_BANDS_F_TO_M:
@ -7090,6 +7161,18 @@ def _cylinder_storage_loss_override(
# 550 kWh/yr). The Table-2b temperature factor still applies (49)→(50).
declared_loss = _float_or_none(getattr(sh, "cylinder_heat_loss", None))
volume_l = _cylinder_volume_l_from_code(epc)
if volume_l is None and _int_or_none(sh.cylinder_size) == 0:
# gov-API cylinder_size=0 = "size not determined" for a present
# cylinder (has_hot_water_cylinder=true). RdSAP 10 §10.5 Table 28:
# "If the actual size is not determined, the size ... is taken as
# according to Table 28" → the "Normal" 110 L baseline, which STILL
# incurs a storage loss. Without this the code lookup returned None
# and the `if volume_l is None: return None` below dropped the loss,
# under-costing the DHW (7 corpus certs; e.g. 200004017091 +1.75 ->
# +0.39). Gated on the EXPLICIT 0 so a full-SAP cert whose RdSAP
# cylinder_size is simply unlodged (None, e.g. 10091568921) keeps its
# own cylinder handling rather than a forced 110 L RdSAP default.
volume_l = _CYLINDER_SIZE_NOT_DETERMINED_L
if declared_loss is not None:
storage_56m = cylinder_storage_loss_monthly_kwh(
volume_l=volume_l or 0.0,
@ -7517,8 +7600,17 @@ def _fuel_cost(
secondary_fuel = (
epc.sap_heating.secondary_fuel_type if epc.sap_heating else None
)
# Canonicalise the colliding gov-API secondary fuel enums (5 anthracite /
# 9 dual fuel / 33 coal — `_GOV_API_COLLISION_FUELS`) BEFORE the price
# lookup, exactly as the main-fuel boundary and the CO2/display
# `_secondary_fuel_cost_gbp_per_kwh` already do. Without this the raw
# same-value Table-32 lookup mis-priced e.g. anthracite (enum 5) at the
# bulk-LPG code-5 rate (12.19 vs 3.64 p/kWh) — a 3× over-cost that sank
# cert 100050355518 (lodged 36, engine 20.8). This inline cost path had
# diverged from the already-canonicalised display helper.
secondary_high_rate_gbp_per_kwh = (
table_32_unit_price_p_per_kwh(secondary_fuel) * _PENCE_TO_GBP
table_32_unit_price_p_per_kwh(canonical_fuel_code(secondary_fuel))
* _PENCE_TO_GBP
if secondary_fuel is not None
else other_uses_gbp_per_kwh
)

View file

@ -922,6 +922,22 @@ def heat_transmission_from_cert(
raw_rafter_thickness = getattr(part, "rafter_insulation_thickness", None)
if raw_rafter_thickness is not None:
raw_roof_thickness = raw_rafter_thickness
elif "flat" in (part.roof_construction_type or "").lower():
# RdSAP 10 §5.11 — a FLAT roof lodges its insulation thickness in
# the dedicated gov-API `flat_roof_insulation_thickness` field
# (e.g. "75mm"), NOT `roof_insulation_thickness` (which stays None
# for flat roofs). Prefer it so a measured flat-roof thickness
# bills at its Table-16 U instead of the uninsulated age-band flat
# default. Mirrors the rafter branch above. Cert 47084930: flat
# roof "75mm" -> U 0.5 (accredited Elmhurst worksheet, "insulated
# flat roof") vs the age-E default 1.5 (roof 96.4 -> 32.1 W/K). An
# "AB"/"NI" (as-built/unknown) value parses to None and keeps the
# age-band default, unchanged.
raw_flat_thickness = getattr(
part, "flat_roof_insulation_thickness", None
)
if raw_flat_thickness is not None:
raw_roof_thickness = raw_flat_thickness
roof_thickness = _parse_thickness_mm(raw_roof_thickness)
floor_ins_thickness = _parse_thickness_mm(getattr(part, "floor_insulation_thickness", None))

View file

@ -157,6 +157,11 @@ _WALL_INSULATION_INTERNAL: Final[int] = 3
WALL_INSULATION_CAVITY_PLUS_EXTERNAL: Final[int] = 6
WALL_INSULATION_CAVITY_PLUS_INTERNAL: Final[int] = 7
# RdSAP 10 §5.4 (PDF p.32): "assume 100 mm if thickness is unknown" — the
# default added-insulation thickness for an External / Internal wall
# insulation lodgement (type 1/3) with no measured thickness.
_WALL_ADDED_INSULATION_UNKNOWN_THICKNESS_MM: Final[int] = 100
# RdSAP 10 §4-6 (page 73): default thermal conductivity of insulation when
# no documentary evidence is available. Used to convert the lodged
# `wall_insulation_thickness` (mm) into thermal resistance (m²K/W) via
@ -348,7 +353,13 @@ def _insulation_bucket(thickness_mm: Optional[int], insulation_present: bool) ->
to the 50 mm bucket when `insulation_present=True`; when not
present, the as-built (bucket 0) row applies regardless.
"""
if insulation_present and (thickness_mm is None or thickness_mm == 0):
# Insulation known to be present must never fall to the uninsulated
# (bucket 0) row. RdSAP 10 Table 6 footnote routes present-but-unknown
# thickness to the 50 mm row; a KNOWN thin lodgement (5-24 mm, below the
# smallest tabulated 50 mm column) must not read as WORSE (higher U) than
# the unknown case — so it too rounds up to the 50 mm row rather than to
# bucket 0. Only genuinely absent insulation (present=False) maps to 0.
if insulation_present and (thickness_mm is None or thickness_mm < 25):
return 50
if thickness_mm is None:
return 0
@ -589,6 +600,28 @@ def u_wall(
ctry = country if country is not None else Country.ENG
age_idx = _age_index(age_band)
band = _AGE_BANDS[age_idx]
# RdSAP 10 §5.4 (PDF p.32): "the U-value with additional insulation is
# based on the thickness of insulation of mineral wool type (assume
# 100 mm if thickness is unknown)". A SOLID-BRICK or STONE wall lodging
# External or Internal insulation (wall_insulation_type 1/3) but no
# measured insulation thickness (gov-API "NI" → None) takes the 100 mm
# default so the §5.8 Table-14 R-value adjustment (brick/stone branches
# below) fires instead of billing the wall at its uninsulated
# §5.6/Table-6 U (over-counting fabric loss → under-rate; corpus cert
# 200004296092, solid brick 360 mm, walls 101.7 W/K → PE +59.6). Scoped
# to brick/stone because only those constructions have a §5.8 added-
# insulation R-value path here — cavity uses the separate
# `WALL_INSULATION_CAVITY_PLUS_*` composite, and timber/system-built
# carry no §5.8 branch, so a thickness default there would be inert or
# mis-routed.
if (
construction
in (WALL_STONE_GRANITE, WALL_STONE_SANDSTONE, WALL_SOLID_BRICK)
and wall_insulation_type
in (_WALL_INSULATION_EXTERNAL, _WALL_INSULATION_INTERNAL)
and (insulation_thickness_mm is None or insulation_thickness_mm == 0)
):
insulation_thickness_mm = _WALL_ADDED_INSULATION_UNKNOWN_THICKNESS_MM
# RdSAP 10 Tables 6-10 stone rows + footnote (a) (PDF p.33-39), §5.6
# formula (PDF p.40), §5.8 + Table 14 (PDF p.41-42). A documentary wall
# thickness (per §5.3) routes stone in age bands A-E off the §5.6 formula,
@ -998,13 +1031,30 @@ def u_roof(
# (band J = 0.16), matching the cohort's "ND" (thickness None)
# vaulted roofs which already reach col (1) by falling through.
return _ROOF_BY_AGE.get(age_band.upper(), 0.4)
if (
age_band is not None
and insulation_thickness_mm == 0
and _described_as_insulated(description)
and description is not None
and "(assumed)" in description.lower()
):
# RdSAP renders "insulated (assumed)" when insulation PRESENCE is
# itself assumed (age-band / as-built assumption), not an observed
# retrofit — so the §5.11.4 50 mm footnote (below) does not apply;
# the thickness "cannot be determined" and the Table 18 age-band
# default governs (§5.11.4 opening clause). Identical
# "Pitched, insulated (assumed)" corpus dwellings lodge thickness
# "ND" (None → Table 18 by fallthrough) or "NI" (0 → previously the
# 50 mm row) purely per lodging software; both take Table 18.
table_18 = _FLAT_ROOF_BY_AGE if is_flat_roof else roof_by_age
return table_18.get(age_band.upper(), 0.4)
if insulation_thickness_mm == 0 and _described_as_insulated(description):
# Spec §5.11.4 (page 44 footnote): "If retrofit insulation
# present of unknown thickness use 50 mm". The cert encodes
# "thickness unknown but retrofit insulation present" as the
# "NI" sentinel which `_parse_thickness_mm` parses to 0. Without
# this override the Table 16 row-0 lookup below returns the
# uninsulated 2.30 W/m²K.
# "thickness unknown but OBSERVED retrofit insulation" as the
# "NI" sentinel (parsed to 0) + an unqualified "insulated"
# description (no "(assumed)"). Without this override the Table
# 16 row-0 lookup below returns the uninsulated 2.30 W/m²K.
return 0.68 # Table 16 row 50, "Insulation at joists at ceiling level"
if insulation_thickness_mm is not None:
# nearest tabulated thickness <= supplied (Table 16 column (1)

View file

@ -31,6 +31,7 @@ from domain.sap10_ml.rdsap_uvalues import (
WALL_STONE_SANDSTONE,
WALL_SYSTEM_BUILT,
WALL_TIMBER_FRAME,
_insulation_bucket,
thermal_bridging_y,
u_door,
u_exposed_floor,
@ -114,6 +115,43 @@ def test_u_wall_solid_brick_with_ni_thickness_uses_50mm_row_per_table6_footnote(
assert result == pytest.approx(0.55, abs=0.001)
def test_u_wall_solid_brick_present_insulation_thin_known_thickness_not_uninsulated() -> None:
# Arrange — a solid-brick wall lodging internal insulation (type 3)
# with a KNOWN thin thickness (10 mm) but no documentary wall
# thickness (so the §5.8 formula branch, which needs wall_thickness,
# cannot fire) falls to the Table-6 bucket. A known 10 mm value sits
# below the smallest tabulated 50 mm column, but insulation IS present
# — it must NOT read as WORSE (higher U) than the present-but-unknown
# case, which the footnote routes to the 50 mm row. Pre-fix,
# `_insulation_bucket(10, present=True)` returned 0 (uninsulated,
# U=1.7), under-crediting the wall (corpus cert 5117280: Δ 4.44).
# Act
result = u_wall(
country=Country.ENG,
age_band="B",
construction=WALL_SOLID_BRICK,
insulation_thickness_mm=10,
insulation_present=True,
wall_insulation_type=3,
)
# Assert — present insulation → 50 mm row (U=0.55), never uninsulated 1.7.
assert result == pytest.approx(0.55, abs=0.001)
def test_insulation_bucket_present_thin_thickness_rounds_up_to_50_not_zero() -> None:
# Direct guard on the bucket: present insulation never maps to bucket 0.
assert _insulation_bucket(10, True) == 50
assert _insulation_bucket(24, True) == 50
assert _insulation_bucket(None, True) == 50
assert _insulation_bucket(50, True) == 50
assert _insulation_bucket(100, True) == 100
# Genuinely absent insulation still routes thin/zero to bucket 0.
assert _insulation_bucket(10, False) == 0
assert _insulation_bucket(None, False) == 0
def test_u_wall_cavity_as_built_insulated_assumed_routes_to_as_built_row() -> None:
# Arrange — a cavity lodged "Cavity wall, as built, insulated (assumed)"
# with wall_insulation_type=4 is in its AS-BUILT state, NOT a retrofit
@ -835,6 +873,34 @@ def test_u_wall_stone_granite_with_external_insulation_applies_5_8_table_14_r_va
assert abs(result - 0.57) <= 1e-4
def test_u_wall_solid_brick_external_insulation_unknown_thickness_assumes_100mm() -> None:
# Arrange — RdSAP 10 §5.4 (PDF p.32): "the U-value with additional
# insulation is based on the thickness of insulation of mineral wool
# type (assume 100 mm if thickness is unknown)". A solid brick / stone
# wall lodging External or Internal insulation (wall_insulation_type
# 1/3) but NO measured insulation thickness (gov-API "NI" → parsed to
# None) must take the 100 mm §5.4 default so the §5.8 Table-14 R-value
# fires — NOT fall through to the uninsulated §5.6 U. Corpus cert
# 200004296092 (age-B solid brick 360 mm, "with external insulation",
# thickness NI) was billed at the uninsulated 1.40 (walls 101.7 W/K,
# HLP 3.46) → PE +59.6, SAP 6.65. With 100 mm: U₀(§5.6, 360 mm brick)
# + R(100 mm, λ=0.04 → 2.5) → 0.29.
# Act
result = u_wall(
country=Country.ENG,
age_band="B",
construction=WALL_SOLID_BRICK,
insulation_thickness_mm=None, # gov-API "NI" — thickness unknown
insulation_present=True,
wall_insulation_type=1, # External
wall_thickness_mm=360,
)
# Assert — 100 mm default applied, not the uninsulated 1.40.
assert abs(result - 0.29) <= 1e-4
def test_u_wall_stone_granite_age_g_with_wall_thickness_ignores_5_6_formula_per_age_a_to_e_gate() -> None:
# Arrange — §5.6 (PDF p.40) heading explicitly scopes the formula
# to "age bands A to E". For age F onwards Table 6 gives literal
@ -1059,16 +1125,41 @@ def test_u_roof_description_with_measured_transmittance_returns_parsed_value() -
assert result == pytest.approx(0.11, abs=0.001)
def test_u_roof_ni_thickness_with_insulated_description_applies_50mm_per_section_5_11_4() -> None:
# Arrange — 346 corpus certs lodge roof_insulation_thickness="NI"
# (Not Indicated, parsed to 0 by _parse_thickness_mm). When the
# description also signals retrofit insulation ("Pitched, insulated
# (assumed)" / "Flat, insulated" / "Roof room(s), insulated
# (assumed)"), RdSAP 10 §5.11.4 (page 44) footnote applies:
def test_u_roof_ni_thickness_with_observed_insulated_description_applies_50mm_per_section_5_11_4() -> None:
# Arrange — a roof lodging thickness "NI" (parsed to 0) whose
# description signals OBSERVED insulation without the "(assumed)"
# qualifier ("Pitched, insulated" — RdSAP renders the bare form when
# the assessor saw the insulation but not its depth). RdSAP 10
# §5.11.4 (page 44) footnote applies:
# "If retrofit insulation present of unknown thickness use 50 mm".
# That maps to Table 16 row "50 mm at joists at ceiling level" = 0.68
# W/m²K — vs the current 2.30 we return when thickness=0 hits the
# Table 16 row-0 lookup.
# W/m²K — vs the 2.30 the Table 16 row-0 lookup would give.
# Act
result = u_roof(
country=Country.ENG,
age_band="C",
insulation_thickness_mm=0, # parsed from "NI"
description="Pitched, insulated",
)
# Assert
assert result == pytest.approx(0.68, abs=0.01)
def test_u_roof_ni_thickness_with_assumed_insulated_description_uses_table18_default() -> None:
# Arrange — RdSAP renders "insulated (assumed)" when insulation
# presence is ASSUMED (age-band / as-built assumption), not an
# observed retrofit — so the §5.11.4 "retrofit insulation present of
# unknown thickness use 50 mm" footnote does NOT apply; the thickness
# "cannot be determined" and Table 18's age-band default governs
# (§5.11.4 opening clause). The corpus lodges the SAME description
# with thickness "ND" (102 certs → already Table 18 via the
# thickness-None fallthrough) or "NI" (27 certs → previously took the
# 50 mm row 0.68), purely per lodging software; both must land on the
# same Table 18 value (band C pitched → 0.40). The loc-4 NI cohort
# was systematically under-rated by ~1 SAP before this (e.g. corpus
# cert 10094975827 band B: Δ 1.87).
# Act
result = u_roof(
@ -1078,8 +1169,8 @@ def test_u_roof_ni_thickness_with_insulated_description_applies_50mm_per_section
description="Pitched, insulated (assumed)",
)
# Assert
assert result == pytest.approx(0.68, abs=0.01)
# Assert — Table 18 column (1) band C.
assert result == pytest.approx(0.40, abs=0.01)
def test_u_roof_ni_thickness_with_no_insulation_description_stays_at_2_30() -> None:
@ -1324,18 +1415,20 @@ def test_u_roof_vaulted_ni_unknown_band_j_uses_col1_age_band_not_50mm() -> None:
assert abs(result - 0.16) <= 1e-4
def test_u_roof_normal_pitched_ni_insulated_still_returns_50mm_per_5_11_4() -> None:
def test_u_roof_normal_pitched_ni_observed_insulated_still_returns_50mm_per_5_11_4() -> None:
# Arrange — regression guard: the is_sloping_ceiling flag defaults
# False, so a NORMAL pitched roof (with loft) lodged NI + "insulated
# (assumed)" must STILL hit the §5.11.4 retrofit-50 mm row (U=0.68).
# Same inputs as the sloping test above minus is_sloping_ceiling.
# False, so a NORMAL pitched roof (with loft) lodged NI + an
# unqualified "insulated" description (observed retrofit, no
# "(assumed)") must STILL hit the §5.11.4 retrofit-50 mm row
# (U=0.68). The "(assumed)" form routes to Table 18 instead — see
# test_u_roof_ni_thickness_with_assumed_insulated_description_uses_table18_default.
# Act
result = u_roof(
country=Country.ENG,
age_band="J",
insulation_thickness_mm=0,
description="Pitched, insulated (assumed)",
description="Pitched, insulated",
)
# Assert

View file

@ -0,0 +1,243 @@
"""Build the RdSAP-21.0.1 corpus campaign ledger (scripts/corpus_1000/worklist.md).
Runs all 1000 corpus certs through the gauge's own path
(from_api_response -> Sap10Calculator.calculate, mirroring
tests/infrastructure/epc_client/test_sap_accuracy_corpus.py), ranks by
|engine - lodged| descending, attaches a short dwelling signature per cert,
and groups same-signature certs into named clusters.
USAGE
-----
PYTHONPATH=/workspaces/model python scripts/corpus_1000/build_worklist.py
Rewrites worklist.md in place, PRESERVING existing per-cert statuses
([x] / 🔧 / / and trailing `<-` notes) keyed by uprn, so it doubles as
the re-rank step after each fix lands.
"""
from __future__ import annotations
import json
import re
from collections import defaultdict
from pathlib import Path
from typing import Any, Optional
from datatypes.epc.domain.mapper import EpcPropertyDataMapper
from domain.sap10_calculator.calculator import Sap10Calculator
ROOT = Path(__file__).resolve().parents[2]
CORPUS = ROOT / "backend/epc_api/json_samples/RdSAP-Schema-21.0.1/corpus.jsonl"
WORKLIST = ROOT / "scripts/corpus_1000/worklist.md"
# Table 12 fuel names (abbreviated) for the signature column.
_FUEL = {
1: "gas", 2: "gas-bulk", 3: "biogas", 4: "biogas", 7: "bLPG", 9: "LPG",
17: "LPG-sc", 10: "oil", 15: "bioliquid", 16: "B30K", 18: "rapeseed",
58: "biodiesel", 51: "housecoal", 20: "coal-anth", 21: "smokeless",
22: "anthracite", 23: "manuf-smokeless", 12: "wood-log", 13: "wood-pellet",
75: "wood-pellet-bag", 71: "wood-chip", 48: "dual-mineral-wood",
26: "gas", 25: "elec", 29: "elec", 30: "elec7hr", 31: "elec10hr",
32: "elec18hr", 33: "elec24hr", 34: "elec-heatnet", 35: "elec",
36: "elec", 37: "elec", 47: "heatnet", 6: "heatnet",
}
def _fuel_name(code: Optional[int]) -> str:
if code is None:
return "?"
return _FUEL.get(code, f"fuel{code}")
def _lead(desc: Optional[str], n: int = 3) -> str:
"""First n comma/word tokens of a register description, lowercased."""
if not desc:
return "?"
return " ".join(desc.lower().replace(",", " ").split()[:n])
def signature(doc: dict[str, Any]) -> tuple[str, str]:
"""(cluster_key, human signature) for one cert."""
h = doc.get("sap_heating") or {}
mhd = (h.get("main_heating_details") or [{}])
m0 = mhd[0] if mhd else {}
code = m0.get("sap_main_heating_code")
idx = m0.get("main_heating_index_number")
fuel = _fuel_name(m0.get("main_fuel_type"))
heat = f"h{code or ('pcdb' + str(idx) if idx else '?')}/{fuel}"
if len(mhd) > 1:
heat += "+2mains"
walls = doc.get("walls") or []
wall = _lead(walls[0].get("description") if walls else None, 2)
roofs = doc.get("roofs") or []
roof = _lead(roofs[0].get("description") if roofs else None, 2)
es = doc.get("sap_energy_source") or {}
sec = (doc.get("secondary_heating") or {}).get("description") or "None"
feats: list[str] = []
pv = es.get("photovoltaic_supply") or {}
if es.get("pv_connection") not in (0, None) or not pv.get("none_or_no_details"):
feats.append("PV")
if (es.get("wind_turbines_count") or 0) > 0:
feats.append("wind")
if sec != "None":
feats.append("sec:" + _lead(sec, 2))
if (doc.get("extensions_count") or 0) > 0:
feats.append(f"ext{doc.get('extensions_count')}")
if (doc.get("conservatory_type") or 1) != 1:
feats.append(f"consv{doc.get('conservatory_type')}")
for bp in doc.get("sap_building_parts") or []:
if bp.get("sap_room_in_roof") is not None:
feats.append("RR")
break
mv = doc.get("mechanical_ventilation")
if mv not in (0, None):
feats.append(f"mv{mv}")
if (h.get("water_heating_code") or 901) != 901:
feats.append(f"whw{h.get('water_heating_code')}")
key = f"{heat} | {wall} | {roof}" + (" | " + ",".join(sorted(set(feats))) if feats else "")
sig = key
return key, sig
_LINE_RE = re.compile(
r"^- \[(?P<st>[ x])\] |^- (?P<emoji>🔧|⚠|⛔) "
)
def load_existing_statuses() -> dict[str, tuple[str, str]]:
"""uprn -> (status token, trailing note) from the current worklist."""
out: dict[str, tuple[str, str]] = {}
if not WORKLIST.exists():
return out
for line in WORKLIST.read_text().splitlines():
m = re.match(r"^- (?:\[(?P<st>[ x])\]|(?P<emoji>🔧|⚠|⛔)) `(?P<uprn>\w+)`", line)
if not m:
continue
status = f"[{m.group('st')}]" if m.group("st") is not None else m.group("emoji")
note = ""
if "<-" in line:
note = "<-" + line.split("<-", 1)[1]
out[m.group("uprn")] = (status, note)
return out
def main() -> None:
docs = [json.loads(l) for l in CORPUS.read_text().splitlines() if l.strip()]
calc = Sap10Calculator()
rows: list[dict[str, Any]] = []
skipped: list[tuple[str, str]] = []
sap_abs: list[float] = []
for idx, doc in enumerate(docs, 1):
# Some corpus docs lodge no top-level uprn; fall back to the corpus
# line number so ledger statuses don't collide on "None".
uprn = str(doc.get("uprn") or f"line{idx}")
lodged = doc.get("energy_rating_current")
if lodged is None:
skipped.append((uprn, "no lodged SAP"))
continue
try:
epc = EpcPropertyDataMapper.from_api_response(doc)
result = calc.calculate(epc)
except Exception as e:
skipped.append((uprn, f"{type(e).__name__}: {str(e)[:70]}"))
continue
eng = result.sap_score_continuous
delta = eng - lodged
sap_abs.append(abs(delta))
key, sig = signature(doc)
rows.append({
"uprn": uprn, "lodged": lodged, "eng": eng, "delta": delta,
"key": key, "sig": sig,
})
n = len(sap_abs)
within = sum(1 for e in sap_abs if e < 0.5) / n
mae = sum(sap_abs) / n
# Cluster: same signature key, only certs still outside 0.5.
clusters: dict[str, list[dict[str, Any]]] = defaultdict(list)
for r in rows:
if abs(r["delta"]) >= 0.5:
clusters[r["key"]].append(r)
ranked = sorted(
clusters.items(),
key=lambda kv: -sum(abs(r["delta"]) for r in kv[1]),
)
prev = load_existing_statuses()
lines: list[str] = []
lines.append("# RdSAP-21.0.1 corpus campaign ledger")
lines.append("")
lines.append(
f"**Gauge:** {n} computed / {len(skipped)} skipped · "
f"SAP within-0.5 = **{within:.1%}** · MAE = **{mae:.3f}** "
f"(floors in test_sap_accuracy_corpus.py)"
)
lines.append("")
lines.append(
"Statuses: `[ ]` todo · `[x]` resolved ≤0.5 · 🔧 fix landed · "
"⚠ xfail engine bug · ⛔ unbuildable. "
"Line: `uprn · lodged L / eng E / Δ · signature`. "
"Rebuild/re-rank: `PYTHONPATH=. python scripts/corpus_1000/build_worklist.py` "
"(preserves statuses & `<-` notes)."
)
lines.append("")
lines.append(f"## Clusters (certs with |Δ| ≥ 0.5, ranked by summed |Δ|) — {sum(len(v) for v in clusters.values())} certs in {len(ranked)} clusters")
lines.append("")
for i, (key, members) in enumerate(ranked, 1):
members.sort(key=lambda r: -abs(r["delta"]))
tot = sum(abs(r["delta"]) for r in members)
lines.append(
f"### C{i:03d} · Σ|Δ| {tot:.1f} · {len(members)} certs · `{key}`"
)
for r in members:
st, note = prev.get(r["uprn"], ("[ ]", ""))
frag = (
f"- {st} `{r['uprn']}` · L {r['lodged']} / E {r['eng']:.2f} / "
f"Δ {r['delta']:+.2f}"
)
if note:
frag += f" {note}"
lines.append(frag)
lines.append("")
if skipped:
lines.append(f"## Skipped ({len(skipped)})")
for uprn, why in skipped:
st, note = prev.get(uprn, ("", ""))
lines.append(f"- {st} `{uprn}` · {why} {note}".rstrip())
lines.append("")
resolved = [r for r in rows if abs(r["delta"]) < 0.5]
lines.append(f"## Resolved ≤0.5 ({len(resolved)} certs — no action)")
lines.append("")
lines.append("<details><summary>expand</summary>")
lines.append("")
for r in sorted(resolved, key=lambda r: -abs(r["delta"])):
st, note = prev.get(r["uprn"], ("[x]", ""))
if st == "[ ]":
st = "[x]"
lines.append(
f"- {st} `{r['uprn']}` · L {r['lodged']} / E {r['eng']:.2f} / "
f"Δ {r['delta']:+.2f} · `{r['key']}` {note}".rstrip()
)
lines.append("")
lines.append("</details>")
lines.append("")
WORKLIST.write_text("\n".join(lines))
print(f"wrote {WORKLIST}")
print(f"gauge: {n} computed / {len(skipped)} skipped, within-0.5 {within:.1%}, MAE {mae:.3f}")
print(f"clusters: {len(ranked)}, worst 5:")
for key, members in ranked[:5]:
tot = sum(abs(r["delta"]) for r in members)
print(f" Σ{tot:7.1f} n={len(members):3d} {key}")
if __name__ == "__main__":
main()

View file

@ -0,0 +1,58 @@
"""Demand-driven divergence analyzer. Flags corpus certs where the SAP miss is
driven by FABRIC/DEMAND (PE & CO2 move with SAP), not cost the fixable class.
Tags nbp (building parts) and room-in-roof so multi-part / RR bugs surface."""
from __future__ import annotations
import json
from pathlib import Path
from datatypes.epc.domain.mapper import EpcPropertyDataMapper
from domain.sap10_calculator.calculator import calculate_sap_from_inputs
from domain.sap10_calculator.rdsap.cert_to_inputs import (
SAP_10_2_SPEC_PRICES, cert_to_demand_inputs, cert_to_inputs)
CORPUS = Path("backend/epc_api/json_samples/RdSAP-Schema-21.0.1/corpus.jsonl")
def has_rr(doc):
for bp in doc.get("sap_building_parts") or []:
r = bp.get("room_in_roof") or bp.get("rooms_in_roof")
if r: return True
for r in doc.get("roofs") or []:
if "room in roof" in (r.get("description") or "").lower(): return True
return False
rows = []
for line in CORPUS.read_text().splitlines():
doc = json.loads(line)
L = doc.get("energy_rating_current"); peL = doc.get("energy_consumption_current")
co2L = doc.get("co2_emissions_current")
if L is None: continue
try:
epc = EpcPropertyDataMapper.from_api_response(doc)
r = calculate_sap_from_inputs(cert_to_inputs(epc, prices=SAP_10_2_SPEC_PRICES))
d = calculate_sap_from_inputs(cert_to_demand_inputs(epc, prices=SAP_10_2_SPEC_PRICES))
except Exception:
continue
dsap = r.sap_score_continuous - L
if abs(dsap) < 0.5: continue
peE = getattr(d, "primary_energy_kwh_per_m2", None)
dpe = (peE - peL) if (peE is not None and peL is not None) else None
co2E = d.co2_kg_per_yr/1000 if d.co2_kg_per_yr is not None else None
dco2 = (co2E - co2L) if (co2E is not None and co2L is not None) else None
nbp = len(doc.get("sap_building_parts") or [])
rr = has_rr(doc)
# demand-driven: PE moves same direction as SAP-miss inverse (PE up -> SAP down)
# i.e. dpe and dsap have OPPOSITE signs, and |dpe| is material
demand = (dpe is not None and abs(dpe) >= 4 and (dpe > 0) == (dsap < 0))
rows.append((doc.get("uprn"), L, round(r.sap_score_continuous,1), round(dsap,2),
round(dpe,1) if dpe is not None else None,
round(dco2,3) if dco2 is not None else None, nbp, rr, demand))
demand_rows = [r for r in rows if r[8]]
demand_rows.sort(key=lambda x:-abs(x[3]))
print(f"diverging certs: {len(rows)} | demand-driven: {len(demand_rows)}")
mp = [r for r in demand_rows if r[6] > 1]
rr = [r for r in demand_rows if r[7]]
print(f" demand+multipart(nbp>1): {len(mp)} | demand+RR: {len(rr)}")
print(f"\n=== top demand-driven divergences ===")
print(f"{'uprn':13s} {'L':>3} {'E':>5} {'dSAP':>6} {'dPE':>6} {'dCO2':>6} {'nbp':>3} {'RR':>3}")
for r in demand_rows[:32]:
print(f"{str(r[0]):13s} {r[1]:3d} {r[2]:5.1f} {r[3]:+6.2f} {str(r[4]):>6} {str(r[5]):>6} {r[6]:3d} {'Y' if r[7] else '':>3}")

View file

@ -0,0 +1,61 @@
"""Electric-main cohort analysis: for every corpus cert whose main heating is
electric, dump heating SAP code / tariff (meter_type) / lodged vs engine SAP /
cost-vs-demand signature. Used to steer a spec-grounded Table-12a tariff fix
without regressing in-band certs."""
from __future__ import annotations
import json
from pathlib import Path
from datatypes.epc.domain.mapper import EpcPropertyDataMapper
from domain.sap10_calculator.calculator import calculate_sap_from_inputs
from domain.sap10_calculator.rdsap.cert_to_inputs import (
SAP_10_2_SPEC_PRICES, cert_to_demand_inputs, cert_to_inputs, _is_electric_main,
_first_main_heating,
)
CORPUS = Path("backend/epc_api/json_samples/RdSAP-Schema-21.0.1/corpus.jsonl")
def hcode(doc):
sh = doc.get("sap_heating") or {}
ds = sh.get("main_heating_details") or [{}]
return ds[0].get("main_heating_category"), ds[0].get("main_fuel_type")
rows = []
for line in CORPUS.read_text().splitlines():
doc = json.loads(line)
try:
epc = EpcPropertyDataMapper.from_api_response(doc)
except Exception:
continue
main = _first_main_heating(epc)
if not _is_electric_main(main):
continue
try:
r = calculate_sap_from_inputs(cert_to_inputs(epc, prices=SAP_10_2_SPEC_PRICES))
d = calculate_sap_from_inputs(cert_to_demand_inputs(epc, prices=SAP_10_2_SPEC_PRICES))
except Exception as e:
continue
L = doc.get("energy_rating_current"); pe_l = doc.get("energy_consumption_current")
if L is None: continue
dsap = r.sap_score_continuous - L
dpe = (d.pe_per_m2 - pe_l) if (pe_l is not None and hasattr(d,'pe_per_m2')) else None
cat, fuel = hcode(doc)
mt = (doc.get("sap_energy_source") or {}).get("meter_type")
sh = doc.get("sap_heating") or {}
code = (sh.get("main_heating_details") or [{}])[0].get("main_heating_code") \
or (sh.get("main_heating_details") or [{}])[0].get("main_heating_index_number")
rows.append((doc.get("uprn"), code, cat, mt, sh.get("water_heating_code"),
L, round(r.sap_score_continuous,1), round(dsap,2)))
rows.sort(key=lambda x:-abs(x[7]))
print(f"electric-main certs: {len(rows)}")
print(f"{'uprn':14s} {'hcode':>6} {'cat':>4} {'mtr':>4} {'whc':>5} {'L':>3} {'E':>6} {'dSAP':>6}")
for r in rows:
print(f"{str(r[0]):14s} {str(r[1]):>6} {str(r[2]):>4} {str(r[3]):>4} {str(r[4]):>5} {r[5]:3d} {r[6]:6.1f} {r[7]:+6.2f}")
# summary by (hcode, meter)
from collections import defaultdict
agg = defaultdict(lambda:[0,0.0,0,0])
for r in rows:
k=(r[1], r[3]); a=agg[k]; a[0]+=1; a[1]+=r[7]; a[2]+=(abs(r[7])>0.5); a[3]+=(abs(r[7])<=0.5)
print("\n=== by (hcode, meter): n, meanΔ, #div>0.5, #inband ===")
for k,a in sorted(agg.items(), key=lambda x:-abs(x[1][1])):
print(f" code {str(k[0]):>6} meter {str(k[1]):>4}: n={a[0]:3d} meanΔ={a[1]/a[0]:+6.2f} div={a[2]:3d} inband={a[3]:3d}")

View file

@ -0,0 +1,46 @@
"""Field-level systematic-bias scan: mean Δ(SAP) grouped by each mapped
attribute. A subgroup with large |meanΔ| AND enough n is a systematic mapping
error for that attribute (would cancel in the overall ~0 bias)."""
from __future__ import annotations
import json, statistics
from collections import defaultdict
from datatypes.epc.domain.mapper import EpcPropertyDataMapper
from domain.sap10_calculator.calculator import calculate_sap_from_inputs
from domain.sap10_calculator.rdsap.cert_to_inputs import SAP_10_2_SPEC_PRICES, cert_to_inputs
CORPUS="backend/epc_api/json_samples/RdSAP-Schema-21.0.1/corpus.jsonl"
G=defaultdict(lambda: defaultdict(list))
def gv(x): return x.get('value') if isinstance(x,dict) else x
for line in open(CORPUS):
d=json.loads(line); L=d.get('energy_rating_current')
if L is None: continue
try:
epc=EpcPropertyDataMapper.from_api_response(d)
r=calculate_sap_from_inputs(cert_to_inputs(epc,prices=SAP_10_2_SPEC_PRICES))
except Exception: continue
dsap=r.sap_score_continuous-L
bp=(d.get('sap_building_parts') or [{}])[0]
sh=d.get('sap_heating') or {}; md=(sh.get('main_heating_details') or [{}])[0]
G['wall_con'][bp.get('wall_construction')].append(dsap)
G['wall_ins'][bp.get('wall_insulation_type')].append(dsap)
G['wall_con+ins'][(bp.get('wall_construction'),bp.get('wall_insulation_type'))].append(dsap)
G['roof_con'][bp.get('roof_construction')].append(dsap)
G['floor_hl'][bp.get('floor_heat_loss')].append(dsap)
G['age'][bp.get('construction_age_band')].append(dsap)
G['main_cat'][md.get('main_heating_category')].append(dsap)
G['whc'][sh.get('water_heating_code')].append(dsap)
G['meter'][(d.get('sap_energy_source') or {}).get('meter_type')].append(dsap)
G['country'][d.get('country_code')].append(dsap)
G['dwelling'][d.get('dwelling_type')].append(dsap)
G['nbp'][len(d.get('sap_building_parts') or [])].append(dsap)
G['glz'][tuple(sorted(set(w.get('glazing_type') for w in (d.get('sap_windows') or []))))[:1]].append(dsap)
print("=== subgroups with |meanΔ| >= 0.5 and n >= 8 (systematic-bias candidates) ===")
hits=[]
for field, groups in G.items():
for key, vals in groups.items():
if len(vals) >= 8:
m=statistics.mean(vals)
if abs(m) >= 0.5:
hits.append((abs(m), field, key, len(vals), m, statistics.median(vals)))
for am,field,key,n,m,med in sorted(hits, reverse=True):
print(f" {field:12s} = {str(key):20s} n={n:4d} meanΔ={m:+.2f} medianΔ={med:+.2f}")
if not hits: print(" (none — no systematic subgroup bias)")

View file

@ -0,0 +1,56 @@
"""Mapper faithfulness audit: for identity pass-through fields (dims, counts,
thicknesses) compare the RAW cert value to what EpcPropertyDataMapper produced.
Any mismatch is a mapper bug silently feeding the SAP engine wrong inputs."""
from __future__ import annotations
import json
from collections import defaultdict
from datatypes.epc.domain.mapper import EpcPropertyDataMapper
CORPUS="backend/epc_api/json_samples/RdSAP-Schema-21.0.1/corpus.jsonl"
def gv(x): return x.get('value') if isinstance(x,dict) else x
def num(x):
x=gv(x)
if x is None: return None
s=str(x).lower().replace('mm','').replace('m2','').strip()
try: return float(s)
except: return s # keep strings like 'measured'/'NI' for equality
mism=defaultdict(list)
n=0
for line in open(CORPUS):
d=json.loads(line)
try: epc=EpcPropertyDataMapper.from_api_response(d)
except Exception: continue
n+=1; u=d.get('uprn')
# top-level counts
checks=[('door_count', d.get('door_count'), epc.door_count),
('habitable', d.get('habitable_room_count'), getattr(epc,'habitable_room_count',None)),
('led', d.get('led_fixed_lighting_bulbs_count'), epc.led_fixed_lighting_bulbs_count),
('cfl', d.get('cfl_fixed_lighting_bulbs_count'), epc.cfl_fixed_lighting_bulbs_count),
('incand', d.get('incandescent_fixed_lighting_bulbs_count'), epc.incandescent_fixed_lighting_bulbs_count)]
for name,rv,ev in checks:
if rv is not None and ev is not None and num(rv)!=num(ev):
mism[name].append((u,rv,ev))
# per-building-part fabric
rbps=d.get('sap_building_parts') or []; ebps=epc.sap_building_parts or []
for i,rbp in enumerate(rbps):
if i>=len(ebps):
mism['DROPPED_building_part'].append((u,len(rbps),len(ebps))); break
ebp=ebps[i]
for name,rv,ev in [('wall_thick',rbp.get('wall_thickness'),getattr(ebp,'wall_thickness_mm',None)),
('wall_ins_thick',rbp.get('wall_insulation_thickness'),getattr(ebp,'wall_insulation_thickness',None)),
('roof_ins_thick',rbp.get('roof_insulation_thickness'),getattr(ebp,'roof_insulation_thickness',None)),
('wall_con',rbp.get('wall_construction'),getattr(ebp,'wall_construction',None)),
('wall_ins_type',rbp.get('wall_insulation_type'),getattr(ebp,'wall_insulation_type',None))]:
if rv is not None and ev is not None and num(rv)!=num(ev):
mism[name].append((u,rv,ev))
# windows: count + total area
rw=d.get('sap_windows') or []; ew=epc.sap_windows or []
if len(rw)!=len(ew): mism['window_count'].append((u,len(rw),len(ew)))
ra=sum((gv(w.get('window_width')) or 0)*(gv(w.get('window_height')) or 0) for w in rw)
ea=sum((float(w.window_width) if w.window_width else 0)*(float(w.window_height) if w.window_height else 0) for w in ew)
if abs(ra-ea)>0.02: mism['window_area'].append((u,round(ra,2),round(ea,2)))
print(f"audited {n} certs")
print("=== field mismatches (raw cert vs EpcPropertyData) ===")
for k,v in sorted(mism.items(), key=lambda x:-len(x[1])):
print(f"\n[{len(v):4d}] {k}")
for u,rv,ev in v[:5]: print(f" {u}: raw={rv} epc={ev}")
if not mism: print(" (no mismatches — mapper is identity-faithful on all checked fields)")

View file

@ -0,0 +1,76 @@
"""Categorize every corpus cert outside +-0.5 SAP by the REASON it diverges,
combining the computed cost-vs-demand signature, the electric heating code /
meter, and the ledger's RR/multipart tokens."""
from __future__ import annotations
import json, re
from pathlib import Path
from collections import defaultdict
from datatypes.epc.domain.mapper import EpcPropertyDataMapper
from domain.sap10_calculator.calculator import calculate_sap_from_inputs
from domain.sap10_calculator.rdsap.cert_to_inputs import (
SAP_10_2_SPEC_PRICES, cert_to_demand_inputs, cert_to_inputs, _is_electric_main,
_first_main_heating)
CORPUS = Path("backend/epc_api/json_samples/RdSAP-Schema-21.0.1/corpus.jsonl")
# ledger signatures (for RR / multipart tokens)
sig_of={}; cur=""
for l in Path("scripts/corpus_1000/worklist.md").read_text().splitlines():
if l.startswith('### C'):
m=re.search(r'`([^`]*)`',l); cur=m.group(1) if m else ""
m=re.match(r'^- .*?`([^`]+)`',l)
if m: sig_of[m.group(1)]=cur
def bucket(doc, dsap, dpe, dco2, code, mt, sig):
rr = 'RR' in (sig or '')
elec = code in range(191,600) if code else False
demand = dpe is not None and abs(dpe) >= 4 and (dpe>0)==(dsap<0)
cost = dpe is not None and abs(dpe) < 4
# 1. electric storage-boiler / CPSU on off-peak (spec §12 Rule 2/1)
if code in (192,193,194,195,196) and dsap>0:
return "elec storage/CPSU boiler → §12 off-peak (spec-faithful; lodged over-billed)"
# 2. electric direct + storage heaters, tariff/cost
if elec and cost:
return "electric tariff/cost (meter-column & high-rate fraction; spec-faithful vs lodged)"
if elec and demand:
return "electric + fabric/demand mismatch"
if elec:
return "electric other"
# 3. RR
if rr:
return "room-in-roof surface geometry (spec-mandated, worksheet-validated; high variance)"
# 4. non-elec demand/fabric
if demand:
return "non-electric fabric/demand (per-cert: glazing area, lodged geometry, mixed walls)"
if cost:
return "non-electric cost (secondary fuel / cylinder / tariff)"
return "unclassified"
cnt=defaultdict(list)
band=defaultdict(int)
for line in CORPUS.read_text().splitlines():
doc=json.loads(line); L=doc.get('energy_rating_current')
if L is None: continue
try:
epc=EpcPropertyDataMapper.from_api_response(doc)
r=calculate_sap_from_inputs(cert_to_inputs(epc, prices=SAP_10_2_SPEC_PRICES))
d=calculate_sap_from_inputs(cert_to_demand_inputs(epc, prices=SAP_10_2_SPEC_PRICES))
except Exception: continue
dsap=r.sap_score_continuous-L
if abs(dsap)<0.5: continue
peL=doc.get('energy_consumption_current'); co2L=doc.get('co2_emissions_current')
dpe=(d.primary_energy_kwh_per_m2-peL) if peL is not None else None
dco2=(d.co2_kg_per_yr/1000-co2L) if co2L is not None else None
sh=doc.get('sap_heating') or {}; det=(sh.get('main_heating_details') or [{}])[0]
code=det.get('sap_main_heating_code'); mt=(doc.get('sap_energy_source') or {}).get('meter_type')
b=bucket(doc,dsap,dpe,dco2,code,mt,sig_of.get(str(doc.get('uprn')),''))
cnt[b].append((doc.get('uprn'),L,round(r.sap_score_continuous,1),round(dsap,1)))
ad=abs(dsap); band['0.5-1' if ad<1 else '1-2' if ad<2 else '2-4' if ad<4 else '4+']+=1
total=sum(len(v) for v in cnt.values())
print(f"TOTAL diverging (|Δ|≥0.5): {total}\n")
print("by |Δ| band:", dict(band), "\n")
print("=== by REASON (count, mean|Δ|, examples) ===")
for b,rows in sorted(cnt.items(), key=lambda x:-len(x[1])):
mabs=sum(abs(x[3]) for x in rows)/len(rows)
ex=", ".join(f"{x[0]}({x[3]:+.1f})" for x in sorted(rows,key=lambda x:-abs(x[3]))[:3])
print(f"\n[{len(rows):3d}] mean|Δ|={mabs:.1f} {b}\n e.g. {ex}")

File diff suppressed because it is too large Load diff

View 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 , 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())

View file

@ -0,0 +1,223 @@
"""Elmhurst build for UPRN 100050881708 (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 , 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_100050881708.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_100050881708")
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", "31.62")
E.set_text(page, f"{DIM}TextBoxRoomHeightLowestFloor", "2.40")
E.set_text(page, f"{DIM}TextBoxWallPerimeterLowestFloor", "16.4")
E.set_text(page, f"{DIM}TextBoxPartyWallLengthLowestFloor", "6.2")
E.set_text(page, f"{DIM}TextBoxFloorArea1stFloor", "31.62")
E.set_text(page, f"{DIM}TextBoxRoomHeight1stFloor", "2.40")
E.set_text(page, f"{DIM}TextBoxWallPerimeter1stFloor", "16.4")
E.set_text(page, f"{DIM}TextBoxPartyWallLength1stFloor", "6.2")
E.save_close(page)
def walls(page):
E.goto(page, "Walls", "WebFormWalls.aspx")
_pick(page, f"{WALL}DropDownListType", "cavity")
page.wait_for_timeout(500)
print("insulation ->", _pick(page, f"{WALL}DropDownListInsulation", "filled"))
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", "225")
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, 9.56, "North",
_glazing(page) if False else "Double post or during 2022")
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):
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", "2")
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())
E.set_select(page, "DropDownListSecondaryHeatingPresent", "No")
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", "water_heating", "download", "cleanup"]
def main():
if len(sys.argv) < 2 or sys.argv[1] not in _ORDER:
print("usage: build_100050881708.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())

View file

@ -0,0 +1,230 @@
"""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 , 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())

View file

@ -0,0 +1,297 @@
"""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 , 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())

View file

@ -0,0 +1,287 @@
"""Elmhurst build for UPRN 217091901 (RdSAP-21.0.1, TOP-FLOOR FLAT, band A,
SOLID BRICK 280 mm UNINSULATED, FLAT ROOF no insulation (As Built), floor over
ANOTHER DWELLING below (zero loss), mains-gas COMBI (PCDB 18658), 6 DG windows
~8.62 , no doors, TFA 63. Lodged SAP 62; engine 60.82 (Δ 1.18, PE +8.7).
Per-cert worksheet validation. The roof dominates loss (145.6 W/K at flat-default
U 2.30); this build confirms the accredited uninsulated flat-roof + solid-brick
wall U against the engine. Run:
DISPLAY=:99 python scripts/hyde/build_217091901.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_")
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_217091901")
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")
_pick(page, "DropDownListPropertyType1", "flat") or \
E.set_select(page, "DropDownListPropertyType1", "F Flat")
E.set_text(page, "TextBoxStoreys", "1")
E.set_text(page, "TextBoxHabitableRooms", "3")
E.set_text(page, "TextBoxHeatedHabitableRooms", "3")
print("date ->", _pick(page, "DropDownListDateBuiltMain", "before 1900"))
E.set_select(page, "DropDownListDateBuiltFirst", "")
E.save_close(page)
def flats(page):
E.goto(page, "Flats", "WebFormFlats.aspx")
print("flat positions:", _options(page, "" if False else "DropDownListPositionOfFlat"))
_pick(page, "DropDownListPositionOfFlat", "top") or \
E.set_select(page, "DropDownListPositionOfFlat", "Top Floor")
E.set_text(page, "TextBoxFloor", "3")
E.set_select(page, "RadioButtonListFlatCoridor", "None")
E.save_close(page)
def dimensions(page):
E.goto(page, "Dimensions", "WebFormDimensions.aspx")
E.set_text(page, f"{DIM}TextBoxFloorAreaLowestFloor", "63.29")
E.set_text(page, f"{DIM}TextBoxRoomHeightLowestFloor", "2.85")
E.set_text(page, f"{DIM}TextBoxWallPerimeterLowestFloor", "14.0")
E.set_text(page, f"{DIM}TextBoxPartyWallLengthLowestFloor", "22.8")
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 opts:", _options(page, f"{WALL}DropDownListInsulation"))
_pick(page, f"{WALL}DropDownListInsulation", "as built") or \
_pick(page, f"{WALL}DropDownListInsulation", "none")
E.save_close(page)
def roofs(page):
E.goto(page, "Roofs", "WebFormRoofs.aspx")
print("ROOF TYPE opts:", _options(page, f"{ROOF}DropDownListType"))
_pick(page, f"{ROOF}DropDownListType", "flat")
page.wait_for_timeout(600)
_pick(page, f"{ROOF}DropDownListInsulationAt", "flat roof insulation")
page.wait_for_timeout(600)
# discover the thickness/additional field that appears for flat-roof insulation
for suf in (f"{ROOF}DropDownListThickness", f"{ROOF}DropDownListInsulation",
f"{ROOF}DropDownListAdditionalInsulation"):
if page.locator(f"#{E.FP}{suf}").count():
print(f" {suf.split('_')[-1]} opts:", _options(page, suf))
_pick(page, suf, "as built") or _pick(page, suf, "unknown") or \
_pick(page, suf, "none") or _pick(page, suf, "nil")
E.save_close(page)
def floors(page):
E.goto(page, "Floors", "WebFormFloors.aspx")
print("FLOOR LOCATION opts:", _options(page, f"{FLOOR}DropDownListLocation"))
# "(other premises below)" = above a partially-heated space
_pick(page, f"{FLOOR}DropDownListLocation", "another dwelling below")
E.save_close(page)
def openings(page):
E.goto(page, "Openings", "WebFormOpenings.aspx")
E.click_tab(page, "TabContainer_TabPanelWindowsPanel")
E.set_single_window(page, 8.62, "North", _glazing(page))
_delete_zero_rows(page)
E.click_tab(page, "TabContainer_TabPanelDoorsPanel")
E.set_text(page, f"{DP}TextBoxDoors", "0")
E.set_text(page, f"{DP}TextBoxDoorsInsulated", "0")
E.set_text(page, f"{DP}TextBoxDraughtProofedDoors", "0")
E.save_close(page)
def _glazing(page):
# Cert glazed_type=5 → SINGLE glazing (epc_codes.csv, RdSAP-Schema-21).
for opt in _options(page, f"{WP}DropDownListExtGlazing"):
if "single" in opt.lower():
return opt
return "Single glazed"
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
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)
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])
_ORDER = ["property_description", "flats", "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_217091901.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())

View file

@ -0,0 +1,299 @@
"""Elmhurst build for UPRN 47084930 (RdSAP-21.0.1, TOP-FLOOR FLAT, band E,
CAVITY wall with INTERNAL insulation (100 mm), FLAT ROOF insulated 75 mm, floor
over OTHER PREMISES below, mains-gas COMBI (PCDB 17815), 5 DG windows ~9.9 ,
no doors, TFA 64. Lodged SAP 74; engine 69.51 (Δ 4.49, PE +47 over-count).
Per-cert worksheet validation. Suspected engine gap: the flat-roof insulation
thickness is lodged in `flat_roof_insulation_thickness` (75 mm) but the U-value
calc reads `roof_insulation_thickness` (None) bills the roof at the age-E flat
default 1.5 (96 W/K) instead of the 75 mm insulated ~0.5 (~30 W/K). This build
gets the accredited flat-roof + floor U. Run:
DISPLAY=:99 python scripts/hyde/build_47084930.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_")
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_47084930")
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")
_pick(page, "DropDownListPropertyType1", "flat") or \
E.set_select(page, "DropDownListPropertyType1", "F Flat")
E.set_text(page, "TextBoxStoreys", "1")
E.set_text(page, "TextBoxHabitableRooms", "3")
E.set_text(page, "TextBoxHeatedHabitableRooms", "3")
print("date ->", _pick(page, "DropDownListDateBuiltMain", "1967-1975"))
E.set_select(page, "DropDownListDateBuiltFirst", "")
E.save_close(page)
def flats(page):
E.goto(page, "Flats", "WebFormFlats.aspx")
print("flat positions:", _options(page, "" if False else "DropDownListPositionOfFlat"))
_pick(page, "DropDownListPositionOfFlat", "top") or \
E.set_select(page, "DropDownListPositionOfFlat", "Top Floor")
E.set_text(page, "TextBoxFloor", "3")
E.set_select(page, "RadioButtonListFlatCoridor", "None")
E.save_close(page)
def dimensions(page):
E.goto(page, "Dimensions", "WebFormDimensions.aspx")
E.set_text(page, f"{DIM}TextBoxFloorAreaLowestFloor", "64.26")
E.set_text(page, f"{DIM}TextBoxRoomHeightLowestFloor", "2.32")
E.set_text(page, f"{DIM}TextBoxWallPerimeterLowestFloor", "18.15")
E.set_text(page, f"{DIM}TextBoxPartyWallLengthLowestFloor", "18.15")
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(500)
print("insulation opts:", _options(page, f"{WALL}DropDownListInsulation"))
_pick(page, f"{WALL}DropDownListInsulation", "internal")
page.wait_for_timeout(500)
for suf in (f"{WALL}DropDownListInsulationThickness",):
if page.locator(f"#{E.FP}{suf}").count():
print("wall ins thickness opts:", _options(page, suf))
_pick(page, suf, "100")
E.save_close(page)
def roofs(page):
E.goto(page, "Roofs", "WebFormRoofs.aspx")
print("ROOF TYPE opts:", _options(page, f"{ROOF}DropDownListType"))
_pick(page, f"{ROOF}DropDownListType", "flat")
page.wait_for_timeout(600)
# discover flat-roof insulation fields
for suf in (f"{ROOF}DropDownListInsulationAt", f"{ROOF}DropDownListThickness",
f"{ROOF}DropDownListInsulation"):
if page.locator(f"#{E.FP}{suf}").count():
print(f" {suf.split('_')[-1]} opts:", _options(page, suf))
# flat roof insulated 75 mm
if page.locator(f"#{E.FP}{ROOF}DropDownListThickness").count():
_pick(page, f"{ROOF}DropDownListThickness", "75")
E.save_close(page)
def floors(page):
E.goto(page, "Floors", "WebFormFloors.aspx")
print("FLOOR LOCATION opts:", _options(page, f"{FLOOR}DropDownListLocation"))
# "(other premises below)" = above a partially-heated space
(_pick(page, f"{FLOOR}DropDownListLocation", "partially heated")
or _pick(page, f"{FLOOR}DropDownListLocation", "other premises")
or _pick(page, f"{FLOOR}DropDownListLocation", "unheated"))
page.wait_for_timeout(400)
tsel = page.locator(f"#{E.FP}{FLOOR}DropDownListType")
if tsel.count():
_pick(page, f"{FLOOR}DropDownListType", "solid")
E.save_close(page)
def openings(page):
E.goto(page, "Openings", "WebFormOpenings.aspx")
E.click_tab(page, "TabContainer_TabPanelWindowsPanel")
_add_window(page, 9.9, "North", _glazing(page))
_delete_zero_rows(page)
E.click_tab(page, "TabContainer_TabPanelDoorsPanel")
E.set_text(page, f"{DP}TextBoxDoors", "0")
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
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)
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])
_ORDER = ["property_description", "flats", "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_47084930.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())

View file

@ -235,24 +235,129 @@ 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 then set via a Playwright `.fill()` + Tab so the ASP.NET
AutoPostBack fires and the server-side Area (w×h) commits a JS-set of the
value bypasses the postback and Elmhurst persists a 0.00-area row.
Frame Type + Glazing Gap are REQUIRED by the Recommendations validation
gate (else "Frame Type/Glazing Gap must be entered" blocks the worksheet)."""
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 _fill_tab(suffix: str, value: str) -> None:
loc = page.locator(f"#{FP}{suffix}")
loc.fill(value)
loc.press("Tab")
page.wait_for_timeout(1200) # AutoPostBack settle
_fill_tab(f"{_WP}TextBoxExtWidth", str(total_area_m2))
_fill_tab(f"{_WP}TextBoxExtHeight", "1.00")
# Frame Type (PVC) + Glazing Gap (12 mm) are validation-required per row.
for suffix, opt in (
(f"{_WP}DropDownListExtFrameType", "PVC"),
(f"{_WP}DropDownListExtGlazingGap", "12 mm"),
(f"{_WP}DropDownListExtOrientation", orientation),
(f"{_WP}DropDownListExtBuildingPartId", "Main"),
(f"{_WP}DropDownListExtLocation", "External wall"),
):
loc = page.locator(f"#{FP}{suffix}")
if loc.count():
try:
loc.select_option(opt)
except Exception:
pass
page.wait_for_timeout(250)
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 _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 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
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:
"""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."""
@ -362,6 +467,63 @@ def select_boiler(page: Page, query: str, ref: str) -> str:
return "ok"
def clear_main_heating_code(page: Page) -> None:
"""Empty Main Heating 1's SAP code via a real server postback (set the
cascade's top-level dropdown to '' + OK). REQUIRED before `set_pcdb_boiler`:
while any code is set, the PCDF reference textbox and the boiler-search
magnifying glass both render `aspNetDisabled`. They only enable when the
code is empty server-side (a JS-clear alone flips the class cosmetically but
leaves the postback unwired)."""
page.evaluate(
"(id)=>{const e=document.getElementById(id); if(e)e.click();}",
f"{MH1}ButtonMainHeatingCode",
)
page.wait_for_timeout(SETTLE_MS * 3)
page.evaluate(
"""(id)=>{const s=document.getElementById(id);if(s){s.value='';
s.dispatchEvent(new Event('change',{bubbles:true}));}}""",
f"{HEATING_DIALOG}DropDownList1",
)
page.wait_for_timeout(SETTLE_MS * 3)
dialog_commit(page, "OK")
page.wait_for_timeout(SETTLE_MS * 2)
def set_pcdb_boiler(page: Page, pcdb_index: "int | str") -> str:
"""Set Main Heating 1 to the EXACT PCDB boiler by its EPC
`main_heating_index_number` (e.g. 18908 = Worcester Greenstar 4000,
GR4700iW 25 C NG, 88.70%). Elmhurst resolves it to the product's PCDB
winter efficiency the same value the SAP engine uses so the worksheet
and our calculator compare like-for-like (not against the generic BGW
SAP-Table 84%). Returns the resolved boiler description.
Hard-won mechanism (supersedes the old "PCDB disabled" note):
1. The PCDF reference textbox is only writable while the main heating CODE
is empty call `clear_main_heating_code(page)` first.
2. The index must be entered via REAL keyboard + Tab. A JS-set `.value`
does NOT survive the field's AutoPostBack, so the boiler never
resolves (the original failure mode). Ctrl-A + type + Tab works.
3. The `TextBoxPCDFBoilerReference` direct-entry path is far simpler than
the magnifying-glass grid (whose modal has no layout box, so
mouse/Playwright clicks can't target rows). The grid search DOES work
(JS-focus the FullSearch box + `keyboard.type` + fire
HyperLinkActionBoilerSearch "Ref No" column == the PCDB index) if a
name lookup is ever needed, but direct entry is preferred."""
fld = f"{MH1}TextBoxPCDFBoilerReference"
page.evaluate(
"(id)=>{const e=document.getElementById(id);if(e){e.focus();e.select();}}", fld
)
page.wait_for_timeout(SETTLE_MS)
page.keyboard.press("Control+a")
page.keyboard.type(str(pcdb_index), delay=40)
page.keyboard.press("Tab")
page.wait_for_timeout(SETTLE_MS * 5) # AutoPostBack resolves the PCDB record
return page.evaluate(
"""()=>{const e=document.querySelector('[id*=LabelMHSSapCodeDescription]');
return e?e.innerText.trim():'';}"""
)
def read_default_uvalue(
page: Page, suffix: Optional[str] = None, label_regex: str = r"Default U-value"
) -> Optional[float]:

View file

@ -27,6 +27,7 @@ from datatypes.epc.surveys.elmhurst_site_notes import (
)
from datatypes.epc.domain.epc_property_data import (
EnergyElement,
EpcPropertyData,
MainHeatingDetail,
PhotovoltaicArray,
@ -738,6 +739,30 @@ def test_cylinder_present_but_size_not_determined_defaults_to_normal_110l() -> N
assert volume_l is not None and abs(volume_l - 110.0) <= 1e-9
def test_cylinder_size_not_determined_still_incurs_storage_loss() -> None:
# Arrange — the size-0 (not determined) cylinder's Table 28 default of
# 110 L must flow into the STORAGE-LOSS override too, not just the Table
# 13 high-rate fraction. `_cylinder_storage_loss_override` read the
# volume via `_cylinder_volume_l_from_code` (None for size 0) and bailed
# `if volume_l is None: return None`, dropping the loss entirely — while
# the rest of the DHW cascade used `_hot_water_cylinder_volume_l`
# (110 L). A present cylinder with no storage loss under-costs the DHW
# and over-rates the dwelling (7 corpus certs lodge cylinder_size=0 +
# has_cylinder=true). RdSAP 10 §10.5 Table 28: a present-but-unsized
# cylinder is a 110 L cylinder and incurs the Table 2 storage loss.
from domain.sap10_calculator.rdsap.cert_to_inputs import (
_cylinder_storage_loss_override, # pyright: ignore[reportPrivateUsage]
)
epc = _cylinder_epc(cylinder_size=0)
# Act
storage_56m = _cylinder_storage_loss_override(epc, None)
# Assert — a real 110 L cylinder loss series, not the dropped None.
assert storage_56m is not None
assert sum(storage_56m) > 0.0
def test_cylinder_size_inaccessible_code_5_solid_fuel_boiler_uses_160l() -> None:
# Arrange — RdSAP 10 §10.5 Table 28: an "Inaccessible" cylinder (code 5)
# heated "from a solid fuel boiler" uses 160 litres.
@ -1867,6 +1892,79 @@ def test_main_floor_u_value_routes_suspended_timber_via_floor_construction_type(
assert sealed is False
def test_has_suspended_timber_floor_per_spec_falls_back_to_floors_description() -> None:
"""RdSAP 10 §5 (12) reads the Main bp's `floor_construction_type`
lodgement, but the gov-API mapper often leaves that field `None`
while the global `epc.floors[].description` still carries the
assessor's "Suspended, ..." observation (cert 100061275133: Main bp
`floor_construction_type=None`, `epc.floors[0].description=
"Suspended, no insulation (assumed)"`). `_main_floor_u_value` already
falls back to this joined description (`effective_floor_description`);
the (12) infiltration rule must apply the same fallback so a
suspended-timber ground floor doesn't silently enter (12)=0."""
from dataclasses import replace
# Arrange
main = replace(
make_building_part(
construction_age_band="B",
floor_dimensions=[make_floor_dimension(floor=0)],
),
floor_type="Ground floor",
floor_construction_type=None,
)
epc = make_minimal_sap10_epc(sap_building_parts=[main])
epc.floors = [
EnergyElement(
description="Suspended, no insulation (assumed)",
energy_efficiency_rating=0,
environmental_efficiency_rating=0,
),
]
# Act
has_susp, sealed = _has_suspended_timber_floor_per_spec(epc)
# Assert — description signal routes to the suspended-timber branch;
# band B is A-E with no supplied U-value and no retrofit insulation
# signal, so rule (b) applies "unsealed" → (12) = 0.2.
assert has_susp is True
assert sealed is False
def test_has_suspended_timber_floor_per_spec_excludes_suspended_not_timber() -> None:
""""Suspended, not timber" (e.g. suspended concrete) starts with the
same "Suspended" prefix as timber floors but must NOT trigger the
§5 (12) rule the rule is specifically about air infiltration
through timber floorboard gaps (see the API mapper's
`_API_FLOOR_CONSTRUCTION_TO_STR` comment: "only 'Suspended timber'
triggers the §5 (12)... adjustment")."""
from dataclasses import replace
# Arrange
main = replace(
make_building_part(
construction_age_band="B",
floor_dimensions=[make_floor_dimension(floor=0)],
),
floor_type="Ground floor",
floor_construction_type=None,
)
epc = make_minimal_sap10_epc(sap_building_parts=[main])
epc.floors = [
EnergyElement(
description="Suspended, not timber",
energy_efficiency_rating=0,
environmental_efficiency_rating=0,
),
]
# Act
has_susp, sealed = _has_suspended_timber_floor_per_spec(epc)
# Assert
assert has_susp is False
assert sealed is False
def test_rdsap_extract_fans_default_per_table_5() -> None:
# Arrange — RdSAP 10 §4.1 Table 5 (PDF p.28) "Extract fans" default
# when the lodged number is unknown. The Summary §12.0 "No. of
@ -2686,6 +2784,46 @@ def test_dual_fuel_secondary_api_enum_9_prices_as_dual_fuel_not_lpg() -> None:
assert secondary_factor_code == 10
def test_fuel_cost_secondary_colliding_fuel_priced_at_canonical_rate() -> None:
# Arrange — the SAP-DRIVING `_fuel_cost` cascade prices the secondary
# via its own inline `secondary_high_rate_gbp_per_kwh`, distinct from
# the CO2/display `_secondary_fuel_cost_gbp_per_kwh`. It must apply the
# SAME `_GOV_API_COLLISION_FUELS` canonicalisation, else an ANTHRACITE
# secondary (gov-API `secondary_fuel_type=5`) resolves to the same-value
# Table-32 code 5 = bulk LPG (12.19 p/kWh) instead of anthracite
# (3.64 p/kWh) — a 3× over-cost that sank corpus cert 100050355518
# (lodged 36, engine 20.8). Anthracite room-heater secondary on a
# mains-gas boiler main.
gas_boiler_main = MainHeatingDetail(
has_fghrs=False, main_fuel_type=26, heat_emitter_type=1,
emitter_temperature=1, main_heating_control=2106,
main_heating_category=2, sap_main_heating_code=102,
)
anthracite_secondary_epc = make_minimal_sap10_epc(
total_floor_area_m2=_TYPICAL_TFA_M2,
habitable_rooms_count=4,
country_code="ENG",
sap_building_parts=[make_building_part(construction_age_band="D")],
sap_heating=make_sap_heating(
main_heating_details=[gas_boiler_main],
secondary_fuel_type=5, # gov-API enum: anthracite
secondary_heating_type=631, # room heater
),
)
# Act
inputs = cert_to_inputs(anthracite_secondary_epc)
result = Sap10Calculator().calculate(anthracite_secondary_epc)
fc = inputs.fuel_cost
secondary_kwh = result.secondary_heating_fuel_kwh_per_yr
# Assert — anthracite 3.64 p/kWh (0.0364), NOT the collision LPG
# 12.19 p/kWh the raw same-value Table-32 lookup returns.
assert secondary_kwh > 0.0
assert abs(fc.secondary_total_cost_gbp - secondary_kwh * 0.0364) <= 1e-6
assert abs(fc.secondary_total_cost_gbp - secondary_kwh * 0.1219) > 1.0
def test_is_electric_water_dual_fuel_table_32_code_10_is_not_electric() -> None:
# Arrange — same API/Table 32 collision as `_is_electric_main` per
# S0380.136 docstring.
@ -4891,6 +5029,42 @@ def test_hot_water_from_pcdb_heat_pump_bills_at_app_n_wh_high_rate() -> None:
assert abs(rate_immersion - 0.0750) <= 1e-6
def test_instantaneous_electric_dhw_off_peak_bills_at_high_rate() -> None:
# Arrange — SAP 10.2 §12 (PDF line 2680): the off-peak on-peak HW
# proportion (Table 13) is "dependent on the total floor area and the
# CYLINDER SIZE" — it presumes a stored-water cylinder that can be
# charged overnight. An INSTANTANEOUS point-of-use electric heater
# (WHC 907/909, no cylinder) heats water on demand and CANNOT shift to
# the off-peak window, so 100% of its consumption is at the high rate
# (Table 12a high-rate fraction 1.0). The generic off-peak else-branch
# billed it at 100% off-peak LOW rate (5.50 p/kWh at 7-hour), 3× under-
# costing the DHW and over-rating the dwelling — corpus cert 74061136
# (HHR-storage flat, WHC 909) lodged 73, engine 80.7.
from domain.sap10_calculator.tables.table_12a import Tariff
from domain.sap10_calculator.rdsap.cert_to_inputs import (
_hot_water_fuel_cost_gbp_per_kwh, # pyright: ignore[reportPrivateUsage]
_hot_water_high_rate_fraction, # pyright: ignore[reportPrivateUsage]
)
storage_main = MainHeatingDetail(
has_fghrs=False, main_fuel_type=29, heat_emitter_type=0,
emitter_temperature="NA", main_heating_control=2404,
main_heating_category=7, sap_main_heating_code=409,
)
# Act — instantaneous electric DHW (WHC 909) on a 7-hour off-peak tariff.
rate = _hot_water_fuel_cost_gbp_per_kwh(
29, storage_main, Tariff.SEVEN_HOUR, SAP_10_2_SPEC_PRICES,
water_heating_code=909,
)
fraction = _hot_water_high_rate_fraction(
29, storage_main, Tariff.SEVEN_HOUR, water_heating_code=909,
)
# Assert — 100% at the 7-hour HIGH rate (15.29 p/kWh), NOT the low 5.50.
assert abs(rate - 0.1529) <= 1e-6
assert fraction == 1.0
def test_heat_pump_water_efficiency_is_floored_at_100pct_per_app_n3_7() -> None:
# Arrange — SAP 10.2 Appendix N3.7 ("Thermal efficiency for water
# heating heat pumps", PDF p.109): "multiply the thermal efficiency

View file

@ -727,6 +727,313 @@ _EXPECTATIONS: Final[tuple[RealCertExpectation, ...]] = (
cert_num="0018-2049-6232-4431-6934",
sap_score=54,
),
# UPRN 4510053280 (corpus-1000 C001; the corpus doc lodges no cert number).
# RdSAP-Schema-21.0.1 native — GROUND-FLOOR FLAT 47 m², band B, solid brick
# 520 mm uninsulated (+ 8.12 m² sheltered alternative wall), roof "(another
# dwelling above)", ASHP radiators PCDB 100053 (Mitsubishi Ecodan 5 kW,
# 2010 record: PSR table 0.22.0, η_space 332.8% @ 2.0, η_water 187.5%,
# duration "24"), no cylinder, electric room-heater secondary, no PV.
# Lodged 51 / engine 74.94 (Δ +23.9, the corpus' worst cert). Dwelling PSR
# = 4.62 kW / (94.0 W/K × 24.2 K) = 2.031, just past the record's largest
# 2.0 → SAP 10.2 N2 (PDF p.101 footnotes 44/45) heat-pump extension:
# reciprocal-interpolate η_space toward 100% at PSR 4.0 → 321% × 0.95 =
# 305%. That is what the engine does, and what accredited Elmhurst did on
# the SAME record family / same PSR shape (golden case 56, record 100061,
# PSR 3.107 → 139.66%). The LODGED software instead treated the
# out-of-range record as invalid and billed direct electric: forcing
# η_space = η_water = 100% + the standard 9-h schedule reproduces the
# lodged CO2 to 1% (0.808 vs 0.800 t) and PE within 6% (178 vs 190),
# whereas the engine's spec path gives 0.444 t / 98.8. A lodged-software
# methodology gap (worklist ⚠), NOT an engine bug — do not tune. PINNED to
# the observed engine 75.
RealCertExpectation(
schema="RdSAP-Schema-21.0.1",
sample="uprn_4510053280",
cert_num="uprn-4510053280",
sap_score=75,
),
# UPRN 10094975827 (corpus; no cert number lodged). RdSAP-Schema-21.0.1
# native — TOP-FLOOR FLAT, band B, solid brick, pitched roof lodged
# thickness "NI" + description "Pitched, insulated (assumed)", electric
# room heaters. The "insulated (assumed)" Table-18 cert: presence-assumed
# insulation is NOT the §5.11.4 observed-retrofit 50 mm row (0.68) — it
# takes the Table 18 age-band default (band B pitched → 0.40), the same
# value the "ND" lodgement of this description reaches. Was engine 64.13
# (Δ 1.87 vs lodged 66); the fix lands 65.83 → rounds to 66 = lodged.
# PINNED to the observed engine 66 = lodged.
RealCertExpectation(
schema="RdSAP-Schema-21.0.1",
sample="uprn_10094975827",
cert_num="uprn-10094975827",
sap_score=66,
),
# UPRN 100050355518 (corpus C003; no cert number lodged). RdSAP-21.0.1
# native — SEMI-DETACHED HOUSE, 122 m², 2 extensions (3 building parts),
# mains-gas boiler main (SAP 117) + ANTHRACITE room-heater secondary
# (gov-API secondary_fuel_type=5, SAP 631). The secondary-fuel collision
# cert: enum 5 collides with Table-32 code 5 (bulk LPG 12.19 p/kWh); the
# SAP-driving _fuel_cost path priced the anthracite secondary at the LPG
# rate instead of anthracite 3.64 p/kWh — a 3× over-cost on ~1431 kWh
# (+£100/yr → ECF 6.24 vs 4.67). Engine was 20.8 (Δ 15.2 vs lodged 36);
# canonicalising the secondary fuel lands 35.5 = lodged 36. CO2/PE
# already matched lodged (0.055 t / 1.7 kWh) — a pure cost-side gap, the
# signature that localised it. PINNED to the observed engine 36 = lodged.
RealCertExpectation(
schema="RdSAP-Schema-21.0.1",
sample="uprn_100050355518",
cert_num="uprn-100050355518",
sap_score=36,
),
# UPRN 74061136 (corpus; no cert number lodged). RdSAP-21.0.1 native —
# MID-FLOOR FLAT 24 m², band B, HHR electric storage heaters (SAP 409) on
# an Unknown meter (type 3 → off-peak 7-hour per RdSAP 10 §12 Rule 2),
# INSTANTANEOUS electric point-of-use DHW (WHC 909, no cylinder). The
# instantaneous-DHW off-peak cert: its DHW was billed 100% at the 7-hour
# LOW rate (5.50 p) but SAP 10.2 §12 (Table-13 on-peak split is keyed on
# CYLINDER size) presumes stored water — an instantaneous heater has no
# cylinder, heats on demand, so 100% bills at the HIGH rate (15.29 p).
# Mid-floor flat → roof/floor both zero-loss, so PE matched lodged (+1.6)
# and the +7.72 over-rate isolated cleanly to the DHW tariff. Fix lands
# 71.75 (Δ 1.25 vs lodged 73); the tariff error is gone, the residual is
# a separate small fabric effect. PINNED to the observed engine 72.
RealCertExpectation(
schema="RdSAP-Schema-21.0.1",
sample="uprn_74061136",
cert_num="uprn-74061136",
sap_score=72,
),
# UPRN 200004296092 (corpus; no cert number lodged). RdSAP-21.0.1 native —
# END-TERRACE HOUSE, TFA 62, age B, mains-gas COMBI (PCDB 16137), SOLID
# BRICK 360 mm "with external insulation" (wall_insulation_type 1) but
# thickness lodged "NI" (unknown). The wall added-insulation unknown-
# thickness cert: the §5.8 R-value gate needs thickness > 0, so the
# externally-insulated wall fell through to its UNINSULATED §5.6 U (1.40,
# walls 101.7 W/K, HLP 3.46) → PE +59.6, engine 64.35 (Δ 6.65 vs lodged
# 71). RdSAP 10 §5.4 (PDF p.32) "assume 100 mm if thickness is unknown"
# applies the 100 mm §5.8 default → U ~0.29, engine 71.74 = lodged. PINNED
# to the observed engine 72.
RealCertExpectation(
schema="RdSAP-Schema-21.0.1",
sample="uprn_200004296092",
cert_num="uprn-200004296092",
sap_score=72,
),
# UPRN 200004017091 (corpus; no cert number lodged). RdSAP-21.0.1 native —
# END-TERRACE HOUSE, mains-gas boiler, hot-water cylinder lodged with
# `cylinder_size=0` (present but "size not determined") + has_cylinder=true.
# The unsized-cylinder storage-loss cert: `_cylinder_storage_loss_override`
# read the volume via `_cylinder_volume_l_from_code` → None for size 0 →
# bailed and DROPPED the cylinder's Table-2 storage loss, under-costing the
# DHW → engine 72.75 (Δ +1.75 vs lodged 71). RdSAP 10 §10.5 Table 28: an
# unsized present cylinder is the 110 L "Normal" default and still incurs
# the loss → engine 71.39 = lodged 71. PINNED to the observed engine 71.
RealCertExpectation(
schema="RdSAP-Schema-21.0.1",
sample="uprn_200004017091",
cert_num="uprn-200004017091",
sap_score=71,
),
# UPRN 47084930 (corpus; no cert number lodged). RdSAP-21.0.1 native —
# TOP-FLOOR FLAT, band E, cavity wall + 100 mm internal insulation, FLAT
# ROOF insulated 75 mm, floor over other premises, mains-gas combi. The
# flat-roof insulation-thickness cert: the 75 mm is lodged in
# `flat_roof_insulation_thickness`, but heat_transmission read
# `roof_insulation_thickness` (None) → billed the flat roof at the
# uninsulated age-E default 1.5 (roof 96.4 W/K) → PE +47, engine 69.51
# (Δ 4.49 vs lodged 74). Preferring the flat field → U 0.5 (roof 32.1
# W/K), engine 74.34 = lodged 74. VALIDATED against the accredited
# Elmhurst worksheet (built on the lodged inputs, evidence saved:
# elmhurst_summary.pdf / elmhurst_worksheet.pdf): "insulated flat roof"
# U 0.5, floor 0.70 and wall 0.25 both matching the engine. PINNED 74.
RealCertExpectation(
schema="RdSAP-Schema-21.0.1",
sample="uprn_47084930",
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,
),
# UPRN 217091901 (corpus; no cert number lodged). RdSAP-21.0.1 native —
# TOP-FLOOR FLAT, band A (before 1900), SOLID BRICK 280 mm uninsulated,
# FLAT ROOF no insulation (As Built), floor over ANOTHER DWELLING below
# (zero loss), mains-gas COMBI, SINGLE glazing (glazed_type 5), TFA 63.
# Lodged 62 / engine 61 (60.82). Built in accredited Elmhurst RdSAP10
# (evidence saved: elmhurst_summary.pdf / elmhurst_worksheet.pdf): EVERY
# heat-loss element matches the engine EXACTLY — walls (solid U 1.70,
# 53.18 W/K), flat roof (uninsulated U 2.30, 145.57 W/K), floor (0),
# party (0), and — with the faithful SINGLE glazing entered — WINDOWS
# (8.62 m² × effective U 4.0268 = 34.7114 W/K, identical to the engine).
# This confirms the mapper''s glazed_type 5 → single (raw U 4.8) is
# spec-correct (epc_codes.csv: glazed_type 5 = single glazing), NOT the
# double it superficially resembles. The 1.18 vs lodged is a cost-side
# methodology residual (PE +8.7 kWh/m², CO2/PE-driven), not a fabric bug.
# PINNED to the observed engine 61.
RealCertExpectation(
schema="RdSAP-Schema-21.0.1",
sample="uprn_217091901",
cert_num="uprn-217091901",
sap_score=61,
),
# UPRN 100061275133 (corpus; no cert number lodged). RdSAP-21.0.1 native —
# 2-storey SEMI-DETACHED HOUSE, band B, SOLID BRICK 400 mm with EXTERNAL
# insulation, pitched 300 mm loft, SUSPENDED uninsulated ground floor,
# mains-gas COMBI, double glazing (glazed_type 13, 2022+), TFA 78.
# Lodged 76 / engine 76 (76.33, was 77.24 before the §5 (12) fix below).
# Built in accredited Elmhurst RdSAP10 (evidence saved: elmhurst_summary.pdf
# / elmhurst_worksheet.pdf): the checkable FABRIC matches the engine
# closely — ROOF (300 mm loft, U 0.14) and WINDOWS (15.32 m² × eff U
# 1.3258 = 20.3106 W/K) match EXACTLY; the externally-insulated WALL is
# U 0.29 (engine) vs 0.30 (Elmhurst) — which VALIDATES the slice-5
# external-insulation default (100 mm added over solid brick,
# `_WALL_ADDED_INSULATION_UNKNOWN_THICKNESS_MM`); the suspended ground
# floor is U 0.68 (engine) vs 0.68 (Elmhurst) — EXACT after rebuilding
# with 0 extract fans + PCDB 18908 (was 0.70 pre-rebuild). Ventilation
# (21) shelter-adjusted ACH is EXACT too (0.6938 = 0.6938), and dwelling
# volume (5) 196.8995 m³ matches (confirming the mapper's +0.25 m
# upper-floor height convention mirrors Elmhurst's own internal one).
# §5 (12) fix: the Main bp lodges no `floor_construction_type` (gov-API
# leaves it None), but `epc.floors[0].description` = "Suspended, no
# insulation (assumed)" — `_has_suspended_timber_floor_per_spec` was
# only reading the per-bp field and silently entered (12)=0 for a
# genuinely suspended-timber floor. Now falls back to the joined
# description (mirroring `_main_floor_u_value`'s existing fallback),
# applying (12)=0.2 unsealed; engine converges EXACTLY on lodged (76).
# Elmhurst's OWN overall worksheet SAP (72) still differs from ours
# (76.33) even on these now-identical inputs — traced to SAP 10.2
# Table 11: Elmhurst's (201)/(202)/(215) show it assumes 10% of space
# heat comes from a portable-electric secondary (658 kWh/yr @ the
# electric rate) purely from the main system's category (gas boiler),
# with NO secondary lodged (`secondary_heating.description == "None"`).
# `_secondary_fraction` only applies Table 11 when a secondary is
# actually lodged or forced by §A.2.2 — deliberately, per the S-B30
# note a few lines below: applying Table 11 unconditionally regressed
# the full 1000-cert corpus MAE by +0.16 (lodged software doesn't
# apply Elmhurst's blanket convention). Verified: forcing Table 11 on
# for just this cert moves 76.33 -> 73.62, ~62% of the 76.33-vs-72 gap.
# The remaining ~1.6 pts is a second, separate cause: (6) Solar gains —
# the BUILD entered all 12 windows as one 15.32 m2 "North" group
# (E.set_single_window), but the cert's real sap_windows lodge 3
# orientations (NW 7.05 m2, NE 1.21 m2, SE 7.06 m2 incl. the largest
# single window) — collapsing to due-north understates solar gain
# (Elmhurst Jan (83)=56.9 W vs our real-orientation 123.4 W). Verified:
# forcing our engine to ALSO use a single synthetic North window (i.e.
# replicating the build's simplification, not a cert or engine fact)
# plus the forced Table 11 above reproduces 72.62 (precise Elmhurst
# value is 72.0681, not the rounded "72" -- true residual after Table
# 11 + windows was ~0.55, not ~1.6). Third cause found by walking the
# rest of the worksheet monthly: (43) Average daily hot water use is
# 117.4056 L/day (Elmhurst) vs our 110.7281 L/day -- SAME occupancy
# (42)=2.4234 both sides, so the gap is in shower/bath demand, not
# occupancy. Root cause: cert's `mixer_shower_count` is unlodged (raw
# `shower_outlets=None`), which `_mixer_shower_flow_rates_from_cert`
# maps to 0 showers (see its docstring: gov-API "no shower_outlets
# entry" convention, validated on cert 2225) -- but Elmhurst's build
# shows a "Non-electric shower" fixture (1 vented mixer @ Table J4),
# since its UI silently defaults one when the build script never
# explicitly configured "0 showers" to match the cert. Verified: a
# single 7 L/min vented mixer reproduces Elmhurst's (42a) monthly
# series EXACTLY (4 d.p., all 12 months) and closes hot water to
# 2616.04 vs Elmhurst's 2616.03 (Δ0.005). Corpus-wide check (170
# certs sharing "bath present, no shower data lodged") shows NO
# systematic bias (mean dSAP +0.12, median -0.04, 46.5% over-rated) --
# confirms the mapper's 0-showers convention is correct on average;
# this is a THIRD build-entry artifact, not a mapper bug. Combining
# all three (windows + Table 11 + 1 shower): SAP 72.4567 vs Elmhurst's
# precise 72.0681 -- residual 0.39, ~91% of the original 4.26-point
# gap (76.33 vs 72.07) now precisely explained. Conclusion: the
# calculator is faithful (matches lodged 76 exactly using the cert's
# REAL windows and REAL shower data); all three residuals are Elmhurst
# -reconciliation-build artifacts or deliberate spec choices, not
# mapping or calculator bugs. PINNED to the observed exact match vs
# lodged.
RealCertExpectation(
schema="RdSAP-Schema-21.0.1",
sample="uprn_100061275133",
cert_num="uprn-100061275133",
sap_score=76,
),
# UPRN 100051051866 (corpus; no cert number lodged). RdSAP-21.0.1 native —
# DETACHED BUNGALOW, band I, CAVITY wall as-built insulated, pitched
# insulated loft, SUSPENDED limited-insulation floor, mains-gas regular
# boiler + cylinder (size 2, insulation type 1, 50 mm — a properly
# insulated cylinder, so NOT the type-0/NULL zero-storage-loss bug), TFA 60.
# Lodged 70 / engine 71 (71.04). Reduced-field cavity fabric already
# Elmhurst-validated on sibling certs; the +1.04 is a cost-side residual
# (PE 13.6 kWh/m² under-count) with no fabric anomaly — walls 29.9, roof
# 15.6, floor 29.4, windows 15.6 W/K all register-explained. PINNED to the
# observed engine 71; the register data fully accounts for the engine output.
RealCertExpectation(
schema="RdSAP-Schema-21.0.1",
sample="uprn_100051051866",
cert_num="uprn-100051051866",
sap_score=71,
),
# UPRN 10070056075 (corpus; no cert number lodged). RdSAP-21.0.1 native —
# DETACHED HOUSE, band K, CAVITY wall as-built insulated, pitched insulated
# loft, SUSPENDED insulated floor, mains-gas regular boiler + cylinder
# (size 4, insulation type 1, 80 mm — properly insulated), dual meter,
# TFA 139. Lodged 80 / engine 79 (78.99). Cavity fabric already
# Elmhurst-validated on sibling certs; the 1.01 is a cost-side residual
# (PE +5.4 kWh/m² over-count) with no fabric anomaly — walls 50.0, roof
# 20.1, floor 16.7, windows 26.7 W/K all register-explained. PINNED to the
# observed engine 79; the register data fully accounts for the engine output.
RealCertExpectation(
schema="RdSAP-Schema-21.0.1",
sample="uprn_10070056075",
cert_num="uprn-10070056075",
sap_score=79,
),
# UPRN 10012334488 (corpus; no cert number lodged). RdSAP-21.0.1 native —
# mid-floor FLAT, mains-electric with SAP heating code 195 ("Electric water
# storage boiler", Table 4a PDF p.170) serving underfloor heating, on an
# off-peak (7-hour) meter, TFA 62. Lodged 72 / engine 85 (85.20). This is a
# PURE COST gap, NOT a fabric bug: PE (1.4 kWh/m²) and CO2 (+0.01 t) both
# match lodged, so demand is right. Code 195 is a STORAGE boiler → SAP 10.2
# §12 Rule 2 routes it to the 7-hour tariff and bills space+water heat mostly
# at the off-peak low rate (5.5 p/kWh) — the spec-faithful treatment. The
# lodged software billed it far higher (implying peak-rate billing of a
# storage system), the same lodged-outlier pattern as the §12/PSR xfail
# cohort. PINNED to the spec-faithful engine 85 — the engine follows SAP
# §12/Table-4a; matching lodged 72 would require billing a storage boiler at
# peak rate, against spec. (Not Elmhurst-verified: my RdSAP10 builds carry a
# combi/heating artifact that makes them unreliable on the E7 storage side.)
RealCertExpectation(
schema="RdSAP-Schema-21.0.1",
sample="uprn_10012334488",
cert_num="uprn-10012334488",
sap_score=85,
),
# UPRN 10091578598 (corpus; no cert number lodged). RdSAP-21.0.1 native —
# FLAT, mains-electric SAP code 195 ("Electric water storage boiler"),
# off-peak meter, TFA 60. Lodged 71 / engine 79 (78.81). Same signature as
# 10012334488: PURE COST gap (PE +1.6, CO2 0.04 both match lodged → demand
# right); code 195 is a §12-Rule-2 storage system billed at the 7-hour
# off-peak rate per spec. Lodged over-billed the storage boiler at a higher
# rate. PINNED to the spec-faithful engine 79 — matching lodged would tune
# against SAP §12/Table-4a.
RealCertExpectation(
schema="RdSAP-Schema-21.0.1",
sample="uprn_10091578598",
cert_num="uprn-10091578598",
sap_score=79,
),
)

View file

@ -245,15 +245,18 @@ def test_part_geometry_floorless_part_honours_full_key_contract() -> None:
assert early["cantilever_floor_area_m2"] == 0.0
def test_roof_insulated_assumed_with_ni_thickness_uses_50mm_per_section_5_11_4() -> None:
# Arrange — 346 corpus certs lodge roof_insulation_thickness="NI"
# with descriptions like "Pitched, insulated (assumed)". The
# retrofit-insulation signal in the description must flow from
# epc.roofs[0].description through heat_transmission_from_cert
# into u_roof so the 50 mm assumption fires per RdSAP 10 §5.11.4.
# Geometry: 100 m² ground floor → top floor area also 100 m² →
# roof_area = 100 m². roof_w_per_k expected = 0.68 × 100 = 68
# W/K (vs uninsulated 2.30 × 100 = 230 W/K).
def test_roof_insulated_assumed_with_ni_thickness_uses_table18_default() -> None:
# Arrange — corpus certs lodge roof_insulation_thickness="NI" with
# descriptions like "Pitched, insulated (assumed)". The description
# must flow from epc.roofs[0].description through
# heat_transmission_from_cert into u_roof, where "(assumed)"
# (presence assumed, not observed retrofit) routes to the Table 18
# age-band default per RdSAP 10 §5.11.4 — band C pitched → 0.40 —
# matching the "ND" lodgement of the same description. (The bare
# "insulated" form keeps the §5.11.4 50 mm row; unit-pinned in
# test_rdsap_uvalues.) Geometry: 100 m² ground floor → top floor
# area also 100 m² → roof_area = 100 m². roof_w_per_k expected =
# 0.40 × 100 = 40 W/K (vs uninsulated 2.30 × 100 = 230 W/K).
main = make_building_part(
identifier="Main Dwelling",
construction_age_band="C",
@ -287,7 +290,7 @@ def test_roof_insulated_assumed_with_ni_thickness_uses_50mm_per_section_5_11_4()
result = heat_transmission_from_cert(epc)
# Assert
assert result.roof_w_per_k == pytest.approx(68.0, abs=2.0)
assert result.roof_w_per_k == pytest.approx(40.0, abs=2.0)
def test_roof_insulation_location_rafters_drives_table16_column_2_api_int_path() -> None:
@ -367,6 +370,46 @@ def test_rafter_insulation_thickness_field_drives_table16_column_2() -> None:
assert abs(result.roof_w_per_k - 25.0) <= 1e-4
def test_flat_roof_insulation_thickness_field_drives_insulated_u() -> None:
# Arrange — a FLAT roof lodges its insulation thickness in the DEDICATED
# gov-EPC API `flat_roof_insulation_thickness` field (e.g. "75mm"),
# leaving `roof_insulation_thickness` None (that field is for pitched-loft
# joists). heat_transmission must prefer the flat field when the part is a
# flat roof so the measured thickness bills at its RdSAP 10 §5.11 Table-16
# U (75 mm → 0.5) instead of the uninsulated age-band flat default (age E
# → 1.5). Cert 47084930 (top-floor flat, flat roof "75mm"): roof 96.4 ->
# 32.1 W/K, engine 69.51 -> 74.34 = lodged 74, matching the accredited
# Elmhurst worksheet ("insulated flat roof", U 0.5). Mirrors the rafter
# field. Geometry: 100 m² plan → roof 100 m². 0.5 × 100 = 50 W/K.
main = make_building_part(
construction_age_band="E",
wall_construction=4,
wall_insulation_type=4,
party_wall_construction=1,
roof_construction=1, # flat
floor_dimensions=[
make_floor_dimension(
total_floor_area_m2=100.0, room_height_m=2.5,
party_wall_length_m=0.0, heat_loss_perimeter_m=40.0, floor=0,
),
],
)
main.roof_construction_type = "Flat"
main.roof_insulation_thickness = None # gov leaves this None for flat roofs
main.flat_roof_insulation_thickness = "75mm" # the thickness lives here
epc = make_minimal_sap10_epc(
total_floor_area_m2=100.0,
country_code="ENG",
sap_building_parts=[main],
)
# Act
result = heat_transmission_from_cert(epc)
# Assert — 75 mm insulated flat roof at U 0.5, not the age-E default 1.5.
assert abs(result.roof_w_per_k - 50.0) <= 1e-4
def test_lodged_roof_u_value_overrides_construction_default() -> None:
# Arrange — RdSAP 10 §5.1: where an element's U-value is known from the
# assessment (documentary evidence / the lodged RdSAP output) it is used

View file

@ -196,7 +196,50 @@ _CORPUS = Path(
# 0.74 -> 0.742 via the heat-pump water-heating 100% floor (App N3.7): cert
# 100110101713 moves inside +-0.5 (|err| 4.97 -> 0.49). See the _MAX_SAP_MAE
# log below for the paired space-heating PSR-extension + water-floor slices.
_MIN_WITHIN_HALF_SAP = 0.742
# 0.742 -> 0.755 (MAE 0.722 -> 0.708, CO2 0.09 -> 0.075, PE 3.5 -> 3.2) via
# the "insulated (assumed)" roof Table-18 fix (RdSAP 10 §5.11.4): a pitched
# roof lodging thickness "NI" + description "insulated (assumed)" was hitting
# the §5.11.4 retrofit-50 mm row (0.68) while the SAME description lodged
# with "ND" fell through to the Table 18 age-band default (e.g. 0.40 A-G) —
# the sentinel choice is lodging-software noise, and "(assumed)" means the
# insulation PRESENCE is an age-band assumption, not an observed retrofit,
# so Table 18's "thickness cannot be determined" clause governs both forms.
# The bare "insulated" description (observed retrofit) keeps the 50 mm row.
# The 27-cert loc4+NI cohort was systematically under-rated ~1 SAP (e.g.
# 10094975827 band B -1.87 -> inside). Unit-pinned in test_rdsap_uvalues.
# 0.755 -> 0.770 (MAE 0.708 -> 0.668) via the secondary-fuel collision fix
# on the SAP-DRIVING cost path (`_fuel_cost`): the gov-API secondary fuel
# enums 5 (anthracite) / 9 (dual fuel) / 33 (coal) collide with same-valued
# Table-32 codes for OTHER fuels, and `_fuel_cost`'s inline secondary rate
# passed the raw enum to the price lookup (unlike the already-canonicalised
# main-fuel boundary + CO2/display helper). Anthracite priced at the bulk-LPG
# code-5 rate (12.19 vs 3.64 p/kWh) 3x over-cost the secondary -> under-rate.
# Worst: cert 100050355518 (2 extensions, anthracite room-heater 2ndary)
# lodged 36 / engine 20.8 -> 35.9. 55 corpus certs carry a colliding
# secondary. Unit-pinned in test_cert_to_inputs (fuel_cost cost path, not
# just the display helper the prior enum-9 test guarded).
# 0.770 -> 0.773 via the unsized-cylinder storage-loss fix: a present
# cylinder lodging gov-API cylinder_size=0 ("size not determined") resolved
# to None volume in `_cylinder_storage_loss_override`, so `if volume_l is
# None: return None` DROPPED its storage loss (under-costing the DHW ->
# over-rate). RdSAP 10 §10.5 Table 28: an unsized present cylinder is the
# 110 L "Normal" default and still incurs the Table-2 loss (the value the
# Table-13 fraction path already used). Gated on the explicit 0 so a
# full-SAP unlodged size (None) keeps its own handling. 7 corpus certs;
# within 77.0% -> 77.3%, PE 3.2 -> 3.05, CO2 0.074 -> 0.073.
# 0.773 -> 0.777 via the flat-roof insulation-thickness fix: a FLAT roof
# lodges its thickness in the dedicated `flat_roof_insulation_thickness`
# field (e.g. "75mm"), but heat_transmission read `roof_insulation_thickness`
# (None for flat roofs) -> billed the roof at the uninsulated age-band flat
# default (age E 1.5) instead of the insulated Table-16 U. Prefer the flat
# field when the part is flat (mirrors the rafter-thickness branch). Cert
# 47084930 (top-floor flat, flat roof 75mm): roof 96.4 -> 32.1 W/K, engine
# 69.51 -> 74.34 = lodged 74, VALIDATED against the accredited Elmhurst
# worksheet ("insulated flat roof" U 0.5). within 77.3% -> 77.7%, PE 3.1 ->
# 2.97, CO2 0.074 -> 0.072.
# 77.7% -> 77.8% via the §5 (12) suspended-timber floor-infiltration
# fallback (see the `_MAX_SAP_MAE` note below for the mechanism).
_MIN_WITHIN_HALF_SAP = 0.778
# 0.793 -> 0.789 via the §12 Unknown-meter + dual-electric-immersion off-peak
# trigger (RdSAP 10 PDF p.62): Apartment 241 (main 691 + 903 dual immersion)
# -5.38 -> -1.05. Worksheet-validated on "simulated case 48" (Elmhurst SAP 57,
@ -266,9 +309,34 @@ _MIN_WITHIN_HALF_SAP = 0.742
# oversized-PSR water eff 0.60 x 128.55% = 77.13% is floored to 100% (=
# accredited Elmhurst (216)), taking the cert 68.03 -> 72.51 (|err| 4.97 ->
# 0.49, now inside +-0.5). In-range heat pumps keep their > 100% water COP.
_MAX_SAP_MAE = 0.722
_MAX_CO2_MAE_TONNES = 0.09 # t CO2 / yr vs co2_emissions_current
_MAX_PE_PER_M2_MAE = 3.5 # kWh / m2 / yr vs energy_consumption_current
# 0.669 -> 0.659 via the instantaneous-electric-DHW off-peak fix: WHC 907/909
# (point-of-use, no cylinder) on an off-peak tariff was billed 100% at the
# off-peak LOW rate, but SAP 10.2 §12 (Table 13 on-peak proportion is keyed on
# CYLINDER size) presumes stored water; an instantaneous heater has no cylinder
# and heats on demand -> 100% high rate. Cert 74061136 (HHR-storage flat,
# WHC 909) +7.72 -> -1.25. Both the cost rate and the ADR-0014/CO2-PE fraction
# fixed (fraction 0 -> 1.0). Unit-pinned in test_cert_to_inputs.
# 0.659 -> 0.648 via the wall added-insulation unknown-thickness fix: a
# SOLID-BRICK/STONE wall lodging External/Internal insulation (type 1/3) with
# thickness "NI" (unknown) was billed UNINSULATED (§5.8 R-value gate needs a
# thickness > 0), over-counting wall heat loss. RdSAP 10 §5.4 (PDF p.32):
# "assume 100 mm if thickness is unknown" -> apply the 100 mm §5.8 default.
# Cert 200004296092 (age-B solid brick 360 mm + external, NI): walls 101.7 W/K
# (U~1.4) -> ~0.29, PE +59.6 -> ~0, SAP 64.35 -> 71.74 (lodged 71). Scoped to
# brick/stone (the only §5.8 R-value path); within-0.5 held at 77.0%, MAE
# 0.658 -> 0.647, PE 3.2 -> 3.1. Unit-pinned in test_rdsap_uvalues.
# 77.7% -> 77.8% (MAE 0.637 -> 0.636) via the §5 (12) suspended-timber
# floor-infiltration fallback: `_has_suspended_timber_floor_per_spec`
# only read the Main bp's per-part `floor_construction_type` lodgement,
# which the gov-API mapper frequently leaves `None` even when the global
# `epc.floors[].description` carries an explicit "Suspended, ..."
# observation (`_main_floor_u_value` already fell back to this joined
# description for the U-value calc — the infiltration rule did not).
# Cert 100061275133 (Elmhurst-validated build): SAP 77.24 -> 76.33, now
# an EXACT match to lodged (76). Unit-pinned in test_cert_to_inputs.
_MAX_SAP_MAE = 0.637
_MAX_CO2_MAE_TONNES = 0.072 # t CO2 / yr vs co2_emissions_current
_MAX_PE_PER_M2_MAE = 3.0 # kWh / m2 / yr vs energy_consumption_current
def _load_corpus() -> list[dict[str, Any]]: