Merge pull request #1577 from Hestia-Homes/fix/sap15-energyelement-party-wall-description

Default description on bare N/A-rated wall elements in SAP-15.0 certs
This commit is contained in:
Jun-te Kim 2026-07-14 17:36:46 +01:00 committed by GitHub
commit af83606183
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 273 additions and 0 deletions

View file

@ -3913,6 +3913,32 @@ def _normalize_sap_schema_16_x(data: Dict[str, Any]) -> Dict[str, Any]:
items: List[Any] = cast(List[Any], value)
return [cast(Dict[str, Any], x) for x in items if isinstance(x, dict)]
# Every "Top-floor flat" SAP-Schema-15.0 cert we've seen (confirmed on 21
# real certs from task 9d271e98-96e6-4be6-bb50-e9be6f954003, portfolio
# 796 — e.g. cert 8204-4998-7729-4026-5693, UPRN 100061740136) lodges a
# second `walls[]` entry with both ratings 0 and no `description` key at
# all — `{"energy_efficiency_rating": 0,
# "environmental_efficiency_rating": 0}`. Per the gov EPC code table
# (epc_codes.csv: energy_efficiency_rating 0 == "N/A"), rating 0 just
# means "not applicable" generically — the payload carries no other
# field (no wall type/construction code), so we cannot tell from the
# data alone *why* it's N/A (a party wall to a neighbour is a plausible
# cause on a top-floor flat, but unconfirmed). Whatever the cause,
# RdSapSchema17_1 requires `description`, so default it to "" rather
# than fabricate construction text: `_joined_descriptions`
# (heat_transmission.py) already filters out falsy descriptions, so an
# N/A-rated element with no description contributes nothing to the wall
# U-value derivation either way — this default is calc-neutral, purely
# unblocking the parse.
for elements_key in ("roofs", "walls", "floors", "windows"):
for element in _dicts(d.get(elements_key)):
if (
"description" not in element
and element.get("energy_efficiency_rating") == 0
and element.get("environmental_efficiency_rating") == 0
):
element["description"] = ""
windows: Any = d.get("windows")
if isinstance(windows, list) and windows:
window_list: List[Any] = cast(List[Any], windows)

View file

@ -140,3 +140,25 @@ class TestFromSapSchema15_0:
assert epc.has_hot_water_cylinder is False
assert epc.sap_heating.cylinder_size == 1
def test_defaults_description_on_a_bare_n_a_rated_wall_element(self) -> None:
# Task 9d271e98-96e6-4be6-bb50-e9be6f954003 (portfolio 796 / scenario
# 1268) failed 21 "Top-floor flat" properties with "EnergyElement:
# missing required field 'description'": every one lodges a second
# `walls[]` entry with both ratings 0 and no description key at all
# (`{"energy_efficiency_rating": 0, "environmental_efficiency_rating":
# 0}`) — property_id 735232 / UPRN 100061740136 / cert
# 8204-4998-7729-4026-5693 here. Rating 0 == "N/A" per the gov EPC
# code table (epc_codes.csv); the payload carries no other field, so
# the specific cause of the N/A (party wall? something else?) isn't
# determinable from the data. Must default to "" rather than fail
# loud regardless of cause (calc-neutral — `_joined_descriptions`
# already drops falsy descriptions).
epc = EpcPropertyDataMapper.from_api_response(
load("sap_15_0_uprn_100061740136.json")
)
assert len(epc.walls) == 2
assert epc.walls[0].description == "Cavity wall, as built, no insulation (assumed)"
assert epc.walls[1].description == ""
assert epc.walls[1].energy_efficiency_rating == 0

View file

@ -0,0 +1,225 @@
{
"uprn": 100061740136,
"roofs": [
{
"description": "Pitched, no insulation(assumed)",
"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
},
{
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"floors": [
{
"description": "(other premises below)",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
}
],
"status": "entered",
"windows": [
{
"description": "Fully double glazed",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
}
],
"lighting": {
"description": "Low energy lighting in all fixed outlets",
"energy_efficiency_rating": 5,
"environmental_efficiency_rating": 5
},
"postcode": "PO19 3NG",
"hot_water": {
"description": "From main system",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
},
"post_town": "CHICHESTER",
"built_form": 3,
"created_at": "2011-06-25 16:07:44",
"glazed_area": 1,
"region_code": 14,
"report_type": 2,
"sap_heating": {
"cylinder_size": 1,
"water_heating_code": 901,
"water_heating_fuel": 26,
"main_heating_details": [
{
"main_fuel_type": 26,
"boiler_flue_type": 2,
"fan_flue_present": "N",
"heat_emitter_type": 1,
"main_heating_number": 1,
"main_heating_control": 2107,
"main_heating_category": 2,
"main_heating_fraction": 1,
"main_heating_data_source": 2
}
],
"has_fixed_air_conditioning": "false"
},
"sap_version": 9.9,
"schema_type": "SAP-Schema-15.0",
"uprn_source": "Energy Assessor",
"country_code": "EAW",
"main_heating": [
{
"description": "Boiler and radiators, mains gas",
"energy_efficiency_rating": 4,
"environmental_efficiency_rating": 4
}
],
"dwelling_type": "Top-floor flat",
"language_code": 1,
"property_type": 2,
"address_line_1": "22, Duncan Road",
"schema_version": "LIG-15.0",
"assessment_type": "RdSAP",
"completion_date": "2011-06-25",
"inspection_date": "2011-06-21",
"extensions_count": 0,
"measurement_type": 1,
"sap_flat_details": {
"level": 3,
"top_storey": "Y",
"flat_location": 0,
"heat_loss_corridor": 2,
"unheated_corridor_length": 8.49
},
"total_floor_area": 30,
"transaction_type": 3,
"conservatory_type": 1,
"heated_room_count": 1,
"registration_date": "2011-06-25",
"restricted_access": 0,
"sap_energy_source": {
"main_gas": "Y",
"meter_type": 2,
"photovoltaic_supply": {
"percent_roof_area": 0
},
"wind_turbines_count": 0,
"wind_turbines_terrain_type": 2
},
"secondary_heating": {
"description": "None",
"energy_efficiency_rating": 0,
"environmental_efficiency_rating": 0
},
"sap_building_parts": [
{
"identifier": "Main Dwelling",
"floor_heat_loss": 6,
"roof_construction": 5,
"wall_construction": 4,
"building_part_number": 1,
"sap_floor_dimensions": [
{
"floor": 0,
"room_height": 2.28,
"total_floor_area": 29.64,
"heat_loss_perimeter": 18.41
}
],
"wall_insulation_type": 4,
"construction_age_band": "D",
"roof_insulation_location": 4,
"roof_insulation_thickness": "NI"
}
],
"low_energy_lighting": 100,
"solar_water_heating": "N",
"bedf_revision_number": 310,
"habitable_room_count": 1,
"heating_cost_current": 457,
"co2_emissions_current": 2.5,
"energy_rating_current": 55,
"lighting_cost_current": 18,
"main_heating_controls": [
{
"description": "Programmer, TRVs and bypass",
"energy_efficiency_rating": 3,
"environmental_efficiency_rating": 3
}
],
"multiple_glazing_type": 1,
"open_fireplaces_count": 0,
"has_hot_water_cylinder": "false",
"heating_cost_potential": 361,
"hot_water_cost_current": 66,
"mechanical_ventilation": 0,
"suggested_improvements": [
{
"sequence": 1,
"typical_saving": 32,
"indicative_cost": "?100 - ?300",
"improvement_type": "B",
"improvement_details": {
"improvement_number": 6
},
"improvement_category": 1,
"energy_performance_rating": 58,
"environmental_impact_rating": 58
},
{
"sequence": 2,
"typical_saving": 24,
"indicative_cost": "?350 - ?450",
"improvement_type": "G",
"improvement_details": {
"improvement_number": 14
},
"improvement_category": 1,
"energy_performance_rating": 60,
"environmental_impact_rating": 61
},
{
"sequence": 3,
"typical_saving": 52,
"indicative_cost": "?1,500 - ?3,500",
"improvement_type": "I",
"improvement_details": {
"improvement_number": 20
},
"improvement_category": 2,
"energy_performance_rating": 64,
"environmental_impact_rating": 67
}
],
"co2_emissions_potential": 1.9,
"energy_rating_potential": 64,
"lighting_cost_potential": 18,
"hot_water_cost_potential": 55,
"renewable_heat_incentive": {
"water_heating": 1418,
"space_heating_existing_dwelling": 6225,
"space_heating_with_loft_insulation": 3572,
"space_heating_with_cavity_insulation": 5600,
"space_heating_with_loft_and_cavity_insulation": 2891
},
"seller_commission_report": "Y",
"energy_consumption_current": 438,
"has_fixed_air_conditioning": "false",
"multiple_glazed_proportion": 100,
"calculation_software_version": "1.3.1.0",
"energy_consumption_potential": 326,
"environmental_impact_current": 55,
"fixed_lighting_outlets_count": 4,
"current_energy_efficiency_band": "D",
"environmental_impact_potential": 67,
"has_heated_separate_conservatory": "false",
"potential_energy_efficiency_band": "D",
"co2_emissions_current_per_floor_area": 85,
"low_energy_fixed_lighting_outlets_count": 4
}