diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index 0f0582d11..017f3ac19 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -3998,6 +3998,22 @@ def _normalize_sap_schema_16_x(data: Dict[str, Any]) -> Dict[str, Any]: mh.setdefault("main_heating_index_number", mh["boiler_index_number"]) mh.setdefault("emitter_temperature", None) + # Some 16.x/15.0 certs (e.g. cert 9568-3034-6211-6089-5960, UPRN + # 100010349400, SAP-Schema-15.0) lodge `sap_heating.cylinder_size` but + # omit the top-level `has_hot_water_cylinder` boolean entirely — + # RdSapSchema17_1 requires both as independent fields. Unlike + # `multiple_glazed_proportion` above, this is NOT a guess: RdSAP 10 + # §10.5's `cylinder_size` code 1 means "no cylinder" by definition + # (see `_CYLINDER_SIZE_CODE_TO_LITRES` / cert_to_inputs.py:5902), the + # exact same fact `has_hot_water_cylinder` encodes — confirmed 1:1 + # across every real 15.0/16.x fixture that lodges both fields + # (cylinder_size == 1 <-> has_hot_water_cylinder == false, every + # other code <-> true). Derive rather than leave the cert unmappable. + if "has_hot_water_cylinder" not in d: + cylinder_size = heating.get("cylinder_size") + if isinstance(cylinder_size, int): + d["has_hot_water_cylinder"] = cylinder_size != 1 + for bp in _dicts(d.get("sap_building_parts")): for fd in _dicts(bp.get("sap_floor_dimensions")): fd.setdefault("party_wall_length", 0) diff --git a/datatypes/epc/domain/tests/test_from_sap_schema_15_0.py b/datatypes/epc/domain/tests/test_from_sap_schema_15_0.py index eb4c8649d..a95345344 100644 --- a/datatypes/epc/domain/tests/test_from_sap_schema_15_0.py +++ b/datatypes/epc/domain/tests/test_from_sap_schema_15_0.py @@ -86,3 +86,24 @@ class TestFromSapSchema15_0: assert len(epc.sap_building_parts) == 1 assert epc.sap_building_parts[0].construction_age_band == "D" + + def test_derives_has_hot_water_cylinder_from_cylinder_size_when_absent( + self, + ) -> None: + # Task b9fcd354-2a33-4fc4-a64e-388c060a055c also failed property_id + # 749229 (UPRN 100010349400) with "RdSapSchema17_1: missing required + # field 'has_hot_water_cylinder'": cert 9568-3034-6211-6089-5960 lodges + # `sap_heating.cylinder_size` but omits the top-level + # `has_hot_water_cylinder` boolean entirely. Unlike + # `multiple_glazed_proportion` (deliberately NOT defaulted — see the NB + # above), `cylinder_size` code 1 means "no cylinder" by definition + # (RdSAP 10 §10.5 Table 28 / cert_to_inputs.py `_CYLINDER_SIZE_CODE_*`) + # — the exact same fact as `has_hot_water_cylinder`, confirmed 1:1 + # across every real 15.0/16.x fixture that lodges both fields. Must be + # derived rather than left to fail loud. + epc = EpcPropertyDataMapper.from_api_response( + load("sap_15_0_uprn_100010349400.json") + ) + + assert epc.has_hot_water_cylinder is False + assert epc.sap_heating.cylinder_size == 1 diff --git a/datatypes/epc/schema/tests/fixtures/sap_15_0_uprn_100010349400.json b/datatypes/epc/schema/tests/fixtures/sap_15_0_uprn_100010349400.json new file mode 100644 index 000000000..e2ef2f26a --- /dev/null +++ b/datatypes/epc/schema/tests/fixtures/sap_15_0_uprn_100010349400.json @@ -0,0 +1,244 @@ +{ + "uprn": 100010349400, + "roofs": [ + { + "description": "Pitched, 200 mm loft insulation", + "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": "Solid, limited insulation (assumed)", + "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 71% of fixed outlets", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + }, + "postcode": "BB10 2HD", + "hot_water": { + "description": "From main system", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + }, + "post_town": "BURNLEY", + "built_form": 2, + "created_at": "2011-09-16 17:02:16", + "glazed_area": 1, + "region_code": 19, + "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": "Y", + "heat_emitter_type": 1, + "boiler_index_number": 457, + "main_heating_number": 1, + "main_heating_control": 2104, + "main_heating_category": 2, + "main_heating_fraction": 1, + "main_heating_data_source": 1 + } + ], + "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": "Semi-detached house", + "language_code": 1, + "property_type": 0, + "address_line_1": "2, Moorview Close", + "schema_version": "LIG-15.0", + "assessment_type": "RdSAP", + "completion_date": "2011-09-16", + "inspection_date": "2011-09-16", + "extensions_count": 0, + "measurement_type": 1, + "total_floor_area": 52, + "transaction_type": 3, + "conservatory_type": 1, + "heated_room_count": 4, + "registration_date": "2011-09-16", + "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": 7, + "roof_construction": 5, + "wall_construction": 4, + "building_part_number": 1, + "sap_floor_dimensions": [ + { + "floor": 0, + "room_height": 2.3, + "floor_insulation": 1, + "total_floor_area": 26.1, + "floor_construction": 1, + "heat_loss_perimeter": 14.45 + }, + { + "floor": 1, + "room_height": 2.3, + "total_floor_area": 26.1, + "heat_loss_perimeter": 14.45 + } + ], + "wall_insulation_type": 4, + "construction_age_band": "I", + "roof_insulation_location": 2, + "roof_insulation_thickness": "200mm" + } + ], + "low_energy_lighting": 71, + "solar_water_heating": "N", + "bedf_revision_number": 313, + "habitable_room_count": 4, + "heating_cost_current": 373, + "co2_emissions_current": 2.2, + "energy_rating_current": 69, + "lighting_cost_current": 42, + "main_heating_controls": [ + { + "description": "Programmer and room thermostat", + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 3 + } + ], + "multiple_glazing_type": 1, + "open_fireplaces_count": 0, + "heating_cost_potential": 322, + "hot_water_cost_current": 86, + "mechanical_ventilation": 0, + "suggested_improvements": [ + { + "sequence": 1, + "typical_saving": 8, + "indicative_cost": "\u00a35", + "improvement_type": "E", + "improvement_details": { + "improvement_number": 35 + }, + "improvement_category": 1, + "energy_performance_rating": 69, + "environmental_impact_rating": 71 + }, + { + "sequence": 2, + "typical_saving": 68, + "indicative_cost": "\u00a31,500 - \u00a33,500", + "improvement_type": "I", + "improvement_details": { + "improvement_number": 20 + }, + "improvement_category": 2, + "energy_performance_rating": 74, + "environmental_impact_rating": 76 + }, + { + "sequence": 3, + "typical_saving": 22, + "indicative_cost": "\u00a34,000 - \u00a36,000", + "improvement_type": "N", + "improvement_details": { + "improvement_number": 19 + }, + "improvement_category": 3, + "energy_performance_rating": 75, + "environmental_impact_rating": 78 + }, + { + "sequence": 4, + "typical_saving": 214, + "indicative_cost": "\u00a311,000 - \u00a320,000", + "improvement_type": "U", + "improvement_details": { + "improvement_number": 34 + }, + "improvement_category": 3, + "energy_performance_rating": 88, + "environmental_impact_rating": 91 + }, + { + "sequence": 5, + "typical_saving": 18, + "indicative_cost": "\u00a31,500 - \u00a34,000", + "improvement_type": "V", + "improvement_details": { + "improvement_number": 44 + }, + "improvement_category": 3, + "energy_performance_rating": 89, + "environmental_impact_rating": 92 + } + ], + "co2_emissions_potential": 1.7, + "energy_rating_potential": 74, + "lighting_cost_potential": 32, + "hot_water_cost_potential": 70, + "renewable_heat_incentive": { + "water_heating": 1739, + "space_heating_existing_dwelling": 5036, + "space_heating_with_loft_and_cavity_insulation": 5036 + }, + "seller_commission_report": "Y", + "energy_consumption_current": 216, + "has_fixed_air_conditioning": "false", + "multiple_glazed_proportion": 100, + "calculation_software_version": 9.9, + "energy_consumption_potential": 173, + "environmental_impact_current": 70, + "fixed_lighting_outlets_count": 7, + "current_energy_efficiency_band": "C", + "environmental_impact_potential": 76, + "has_heated_separate_conservatory": "false", + "potential_energy_efficiency_band": "C", + "co2_emissions_current_per_floor_area": 41, + "low_energy_fixed_lighting_outlets_count": 5 +} \ No newline at end of file