diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index 78243a09..0ba34d7a 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -2593,9 +2593,18 @@ def _sap_17_1_heating(schema: SapSchema17_1) -> SapHeating: main_heating_details=[ MainHeatingDetail( has_fghrs=False, - main_fuel_type=d.main_fuel_type, - heat_emitter_type=d.heat_emitter_type, - emitter_temperature=d.emitter_temperature, + # D6b: code-based systems (category 6/10) lodge no explicit + # fuel/emitter — the calculator drives them off + # sap_main_heating_code instead. Default the absent fields so + # the cert still maps (0 = unspecified; emitter_temperature "NA"). + main_fuel_type=d.main_fuel_type if d.main_fuel_type is not None else 0, + heat_emitter_type=( + d.heat_emitter_type if d.heat_emitter_type is not None else 0 + ), + emitter_temperature=( + d.emitter_temperature if d.emitter_temperature is not None else "NA" + ), + sap_main_heating_code=d.main_heating_code, main_heating_control=d.main_heating_control, fan_flue_present=d.is_flue_fan_present == "true", boiler_flue_type=d.main_heating_flue_type, diff --git a/datatypes/epc/domain/tests/test_from_sap_schema.py b/datatypes/epc/domain/tests/test_from_sap_schema.py index e462991b..f9351340 100644 --- a/datatypes/epc/domain/tests/test_from_sap_schema.py +++ b/datatypes/epc/domain/tests/test_from_sap_schema.py @@ -254,6 +254,23 @@ class TestFromSapSchema17_1EnergySource: assert sample.sap_energy_source.wind_turbines_count == 0 +class TestFromSapSchema17_1CodeBasedHeating: + """Slice D6b: ~27% of full-SAP certs use a code-based heating encoding + (main_heating_category 6/10 — community heating, heat pumps) lodging + main_heating_code instead of explicit heat_emitter_type/main_fuel_type. + A known variant — map main_heating_code → sap_main_heating_code rather than + fail loud, so the cert maps for completeness (score is a later concern).""" + + def test_code_based_heating_cert_maps(self) -> None: + # Previously raised "missing required field 'heat_emitter_type'". + result = EpcPropertyDataMapper.from_api_response(load("sap_17_1_codeheat.json")) + assert isinstance(result, EpcPropertyData) + + def test_main_heating_code_carried(self) -> None: + result = EpcPropertyDataMapper.from_api_response(load("sap_17_1_codeheat.json")) + assert result.sap_heating.main_heating_details[0].sap_main_heating_code == 691 + + class TestFromSapSchema17_1Ventilation: """Slice D5-vent: full-SAP sap_ventilation → measured air permeability (AP4), ventilation_type → MechanicalVentilationKind, sheltered sides, wet rooms and diff --git a/datatypes/epc/schema/sap_schema_17_1.py b/datatypes/epc/schema/sap_schema_17_1.py index 2ec199b9..195e4f53 100644 --- a/datatypes/epc/schema/sap_schema_17_1.py +++ b/datatypes/epc/schema/sap_schema_17_1.py @@ -126,10 +126,13 @@ class SapMainHeatingDetail: `is_flue_fan_present` vs `fan_flue_present`, `main_heating_flue_type` vs `boiler_flue_type`); the mapper translates them.""" - main_fuel_type: int - heat_emitter_type: int - emitter_temperature: Union[int, str] main_heating_control: int + # code-based heating (category 6/10 — community heating, heat pumps) lodges + # main_heating_code instead of explicit fuel/emitter; both then absent. + main_fuel_type: Optional[int] = None + heat_emitter_type: Optional[int] = None + emitter_temperature: Optional[Union[int, str]] = None + main_heating_code: Optional[int] = None main_heating_number: Optional[int] = None main_heating_category: Optional[int] = None main_heating_fraction: Optional[Union[int, float]] = None diff --git a/datatypes/epc/schema/tests/fixtures/sap_17_1_codeheat.json b/datatypes/epc/schema/tests/fixtures/sap_17_1_codeheat.json new file mode 100644 index 00000000..63c717a3 --- /dev/null +++ b/datatypes/epc/schema/tests/fixtures/sap_17_1_codeheat.json @@ -0,0 +1,280 @@ +{ + "uprn": 100071290385, + "roofs": [ + { + "description": "Average thermal transmittance 0.16 W/m\u00b2K", + "energy_efficiency_rating": 4, + "environmental_efficiency_rating": 4 + } + ], + "walls": [ + { + "description": "Average thermal transmittance 0.63 W/m\u00b2K", + "energy_efficiency_rating": 3, + "environmental_efficiency_rating": 3 + } + ], + "floors": [ + { + "description": "(other premises below)", + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + } + ], + "status": "entered", + "tenure": 1, + "windows": { + "description": "High performance glazing", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + }, + "lighting": { + "description": "Low energy lighting in all fixed outlets", + "energy_efficiency_rating": 5, + "environmental_efficiency_rating": 5 + }, + "postcode": "B7 5SP", + "data_type": 4, + "hot_water": { + "description": "Electric immersion, standard tariff", + "energy_efficiency_rating": 1, + "environmental_efficiency_rating": 2 + }, + "post_town": "BIRMINGHAM", + "built_form": 2, + "created_at": "2019-05-28 09:11:44", + "living_area": 9.5, + "orientation": 4, + "region_code": 6, + "report_type": 3, + "sap_heating": { + "water_fuel_type": 39, + "water_heating_code": 903, + "hot_water_store_size": 90, + "main_heating_details": [ + { + "main_fuel_type": 39, + "main_heating_code": 691, + "emitter_temperature": "NA", + "main_heating_number": 1, + "main_heating_control": 2603, + "main_heating_category": 10, + "main_heating_fraction": 1, + "main_heating_data_source": 3 + } + ], + "has_hot_water_cylinder": "true", + "immersion_heating_type": 1, + "has_cylinder_thermostat": "true", + "hot_water_store_heat_loss": 1.37, + "has_fixed_air_conditioning": "false", + "secondary_heating_category": 1, + "is_cylinder_in_heated_space": "true", + "hot_water_store_heat_loss_source": 2 + }, + "sap_version": 9.92, + "schema_type": "SAP-Schema-17.1", + "uprn_source": "Energy Assessor", + "country_code": "ENG", + "main_heating": [ + { + "description": "Room heaters, electric", + "energy_efficiency_rating": 1, + "environmental_efficiency_rating": 2 + } + ], + "air_tightness": { + "description": "(not tested)", + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + }, + "dwelling_type": "Top-floor flat", + "language_code": 1, + "property_type": 2, + "address_line_1": "208, Nechells Park Road", + "assessment_date": "2019-05-24", + "assessment_type": "SAP", + "completion_date": "2019-05-28", + "inspection_date": "2019-05-28", + "sap_ventilation": { + "psv_count": 0, + "pressure_test": 3, + "air_permeability": 15, + "open_flues_count": 0, + "ventilation_type": 1, + "extract_fans_count": 1, + "open_fireplaces_count": 0, + "sheltered_sides_count": 2, + "flueless_gas_fires_count": 0 + }, + "design_water_use": 1, + "sap_data_version": 9.92, + "sap_flat_details": { + "level": 3 + }, + "total_floor_area": 13, + "transaction_type": 6, + "conservatory_type": 1, + "registration_date": "2019-05-28", + "sap_energy_source": { + "electricity_tariff": 1, + "wind_turbines_count": 0, + "wind_turbine_terrain_type": 2, + "fixed_lighting_outlets_count": 15, + "low_energy_fixed_lighting_outlets_count": 15, + "low_energy_fixed_lighting_outlets_percentage": 100 + }, + "sap_opening_types": [ + { + "name": "Glazing", + "type": 4, + "u_value": 1.4, + "data_source": 2, + "frame_factor": 0.7, + "glazing_type": 7, + "solar_transmittance": 0.63 + }, + { + "name": "Solid Door", + "type": 1, + "u_value": 1.8, + "data_source": 2, + "glazing_type": 1 + } + ], + "secondary_heating": { + "description": "None", + "energy_efficiency_rating": 0, + "environmental_efficiency_rating": 0 + }, + "sap_building_parts": [ + { + "sap_roofs": [ + { + "name": "Roof 1", + "u_value": 0.16, + "roof_type": 2, + "description": "Insulated Ceiling", + "total_roof_area": 12.62 + } + ], + "sap_walls": [ + { + "name": "External Wall 1", + "u_value": 0.34, + "wall_type": 2, + "description": "Upgraded Ext Walls", + "total_wall_area": 20.54, + "is_curtain_walling": "false" + }, + { + "name": "External Wall 2", + "u_value": 1, + "wall_type": 2, + "description": "Wall to Lobby", + "total_wall_area": 16.38, + "is_curtain_walling": "false" + }, + { + "name": "Party Wall 0", + "u_value": 0, + "wall_type": 4, + "total_wall_area": 9.88 + } + ], + "identifier": "Main Dwelling", + "overshading": 2, + "sap_openings": [ + { + "name": "W1", + "type": "Glazing", + "width": 1200, + "height": 2000, + "location": "External Wall 1", + "orientation": 8 + }, + { + "name": "D1", + "type": "Solid Door", + "width": 900, + "height": 2100, + "location": "External Wall 2", + "orientation": 0 + } + ], + "construction_year": 2019, + "sap_thermal_bridges": { + "thermal_bridge_code": 1 + }, + "building_part_number": 1, + "sap_floor_dimensions": [ + { + "storey": 1, + "u_value": 0, + "floor_type": 4, + "storey_height": 2.6, + "heat_loss_area": 0, + "total_floor_area": 12.62 + } + ], + "thermal_mass_parameter": 250 + } + ], + "heating_cost_current": { + "value": 278, + "currency": "GBP" + }, + "co2_emissions_current": 1.5, + "energy_rating_average": 60, + "energy_rating_current": 59, + "lighting_cost_current": { + "value": 16, + "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": 278, + "currency": "GBP" + }, + "hot_water_cost_current": { + "value": 206, + "currency": "GBP" + }, + "co2_emissions_potential": 1.5, + "energy_rating_potential": 59, + "lighting_cost_potential": { + "value": 16, + "currency": "GBP" + }, + "schema_version_original": "LIG-17.0", + "hot_water_cost_potential": { + "value": 206, + "currency": "GBP" + }, + "is_in_smoke_control_area": "unknown", + "renewable_heat_incentive": { + "rhi_new_dwelling": { + "space_heating": 1637, + "water_heating": 1212 + } + }, + "seller_commission_report": "Y", + "energy_consumption_current": 716, + "has_fixed_air_conditioning": "false", + "multiple_glazed_percentage": 100, + "calculation_software_version": "4.09r14", + "energy_consumption_potential": 716, + "environmental_impact_current": 63, + "current_energy_efficiency_band": "D", + "environmental_impact_potential": 63, + "has_heated_separate_conservatory": "false", + "potential_energy_efficiency_band": "D", + "co2_emissions_current_per_floor_area": 121 +} \ No newline at end of file