From cf088c36fee16e5400801a531ba08014e2976e21 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 14 Apr 2026 13:20:09 +0000 Subject: [PATCH] =?UTF-8?q?Map=20to=20domain=20from=2021.0.1=20schema=20?= =?UTF-8?q?=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- datatypes/epc/domain/epc_property_data.py | 5 +- datatypes/epc/domain/mapper.py | 317 +++++++++++++++++- .../domain/tests/test_from_rdsap_schema.py | 42 +-- datatypes/epc/schema/rdsap_schema_21_0_1.py | 9 +- 4 files changed, 325 insertions(+), 48 deletions(-) diff --git a/datatypes/epc/domain/epc_property_data.py b/datatypes/epc/domain/epc_property_data.py index 1a1d8b91..37b460c4 100644 --- a/datatypes/epc/domain/epc_property_data.py +++ b/datatypes/epc/domain/epc_property_data.py @@ -1,13 +1,12 @@ from dataclasses import dataclass from datetime import date -from typing import Any, List, Optional, Union +from typing import List, Optional, Union from datatypes.epc.domain.epc import Epc @dataclass class EnergyElement: - # description is a plain string in schema 21.0.0 (no longer a localised object) description: str energy_efficiency_rating: int environmental_efficiency_rating: int @@ -54,7 +53,7 @@ class ShowerOutlets: class SapHeating: instantaneous_wwhrs: InstantaneousWwhrs main_heating_details: List[MainHeatingDetail] - has_fixed_air_conditioning: str + has_fixed_air_conditioning: bool cylinder_size: Optional[int] = ( None # int code from API; not directly available from site notes ) diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index 2171275a..e55ef114 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -1,23 +1,57 @@ from datetime import date from typing import List, Union +from regex import T + from datatypes.epc.domain.epc_property_data import ( + EnergyElement, EpcPropertyData, InstantaneousWwhrs, MainHeatingDetail, + PhotovoltaicSupply, + PhotovoltaicSupplyNoneOrNoDetails, + PvBatteries, + PvBattery, + SapAlternativeWall, SapBuildingPart, SapEnergySource, SapFloorDimension, SapHeating, + SapRoomInRoof, SapWindow, + ShowerOutlet, + ShowerOutlets, + WindTurbineDetails, + WindowTransmissionDetails, +) +from datatypes.epc.schema.rdsap_schema_17_0 import ( + RdSapSchema17_0, + EnergyElement as EnergyElement_17_0, +) +from datatypes.epc.schema.rdsap_schema_17_1 import ( + RdSapSchema17_1, + EnergyElement as EnergyElement_17_1, +) +from datatypes.epc.schema.rdsap_schema_18_0 import ( + RdSapSchema18_0, + EnergyElement as EnergyElement_18_0, +) +from datatypes.epc.schema.rdsap_schema_19_0 import ( + RdSapSchema19_0, + EnergyElement as EnergyElement_19_0, +) +from datatypes.epc.schema.rdsap_schema_20_0_0 import ( + RdSapSchema20_0_0, + EnergyElement as EnergyElement_20_0, +) +from datatypes.epc.schema.rdsap_schema_21_0_0 import ( + RdSapSchema21_0_0, + EnergyElement as EnergyElement_21_0, +) +from datatypes.epc.schema.rdsap_schema_21_0_1 import ( + RdSapSchema21_0_1, + EnergyElement as EnergyElement_21_0_1, ) -from datatypes.epc.schema.rdsap_schema_17_0 import RdSapSchema17_0 -from datatypes.epc.schema.rdsap_schema_17_1 import RdSapSchema17_1 -from datatypes.epc.schema.rdsap_schema_18_0 import RdSapSchema18_0 -from datatypes.epc.schema.rdsap_schema_19_0 import RdSapSchema19_0 -from datatypes.epc.schema.rdsap_schema_20_0_0 import RdSapSchema20_0_0 -from datatypes.epc.schema.rdsap_schema_21_0_0 import RdSapSchema21_0_0 -from datatypes.epc.schema.rdsap_schema_21_0_1 import RdSapSchema21_0_1 from datatypes.epc.surveys.pashub_rdsap_site_notes import ( BuildingConstruction, BuildingMeasurements, @@ -58,7 +92,9 @@ class EpcPropertyDataMapper: for ext_c in construction.extensions: matching = [m for m in measurements.extensions if m.id == ext_c.id] if matching: - sap_building_parts.append(_map_extension_building_part(ext_c, matching[0])) + sap_building_parts.append( + _map_extension_building_part(ext_c, matching[0]) + ) total_floor_area = round( sum( @@ -97,7 +133,8 @@ class EpcPropertyDataMapper: has_fixed_air_conditioning=ventilation.has_fixed_air_conditioning, wet_rooms_count=0, # no equivalent in site notes extensions_count=general.number_of_extensions, - heated_rooms_count=room_counts.number_of_heated_rooms or 0, # absent in site notes → 0 + heated_rooms_count=room_counts.number_of_heated_rooms + or 0, # absent in site notes → 0 open_chimneys_count=room_counts.number_of_open_chimneys, habitable_rooms_count=room_counts.number_of_habitable_rooms, insulated_door_count=room_counts.number_of_insulated_external_doors, @@ -137,8 +174,258 @@ class EpcPropertyDataMapper: raise NotImplementedError @staticmethod - def from_rdsap_schema_21_0_1(_schema: RdSapSchema21_0_1) -> EpcPropertyData: - raise NotImplementedError + def from_rdsap_schema_21_0_1(schema: RdSapSchema21_0_1) -> EpcPropertyData: + es = schema.sap_energy_source + return EpcPropertyData( + # General + uprn=schema.uprn, + assessment_type=schema.assessment_type, + sap_version=schema.sap_version, + dwelling_type=schema.dwelling_type, + property_type=str(schema.property_type), + built_form=str(schema.built_form), + address_line_1=schema.address_line_1, + address_line_2=schema.address_line_2, + postcode=schema.postcode, + post_town=schema.post_town, + status=schema.status, + tenure=str(schema.tenure), + transaction_type=str(schema.transaction_type), + inspection_date=date.fromisoformat(schema.inspection_date), + completion_date=date.fromisoformat(schema.completion_date), + registration_date=date.fromisoformat(schema.registration_date), + total_floor_area_m2=float(schema.total_floor_area), + # Property flags + solar_water_heating=schema.solar_water_heating == "Y", + has_hot_water_cylinder=schema.has_hot_water_cylinder == "true", + has_fixed_air_conditioning=schema.has_fixed_air_conditioning == "true", + conservatory_type=schema.conservatory_type, + has_conservatory=schema.conservatory_type != 1, + # Counts + door_count=schema.door_count, + habitable_rooms_count=schema.habitable_room_count, + heated_rooms_count=schema.heated_room_count, + wet_rooms_count=schema.wet_rooms_count, + extensions_count=schema.extensions_count, + open_chimneys_count=schema.open_chimneys_count, + insulated_door_count=schema.insulated_door_count, + draughtproofed_door_count=schema.draughtproofed_door_count, + # Lighting + led_fixed_lighting_bulbs_count=schema.led_fixed_lighting_bulbs_count, + cfl_fixed_lighting_bulbs_count=schema.cfl_fixed_lighting_bulbs_count, + incandescent_fixed_lighting_bulbs_count=schema.incandescent_fixed_lighting_bulbs_count, + # Energy elements + roofs=EpcPropertyDataMapper._map_21_0_01_energy_elements(schema.roofs), + walls=EpcPropertyDataMapper._map_21_0_01_energy_elements(schema.walls), + floors=EpcPropertyDataMapper._map_21_0_01_energy_elements(schema.floors), + main_heating=EpcPropertyDataMapper._map_21_0_01_energy_elements( + schema.main_heating + ), + window=EpcPropertyDataMapper._map_21_0_01_energy_element(schema.window), + lighting=EpcPropertyDataMapper._map_21_0_01_energy_element(schema.lighting), + hot_water=EpcPropertyDataMapper._map_21_0_01_energy_element( + schema.hot_water + ), + secondary_heating=EpcPropertyDataMapper._map_21_0_01_energy_element( + schema.secondary_heating + ), + # SAP heating + sap_heating=SapHeating( + instantaneous_wwhrs=InstantaneousWwhrs( + wwhrs_index_number1=schema.sap_heating.instantaneous_wwhrs.wwhrs_index_number1, + wwhrs_index_number2=schema.sap_heating.instantaneous_wwhrs.wwhrs_index_number2, + ), + main_heating_details=[ + MainHeatingDetail( + has_fghrs=d.has_fghrs == "Y", + main_fuel_type=d.main_fuel_type, + boiler_flue_type=d.boiler_flue_type, + fan_flue_present=d.fan_flue_present == "Y", + heat_emitter_type=d.heat_emitter_type, + emitter_temperature=d.emitter_temperature, + main_heating_number=d.main_heating_number, + boiler_ignition_type=d.boiler_ignition_type, + main_heating_control=d.main_heating_control, + main_heating_category=d.main_heating_category, + main_heating_fraction=d.main_heating_fraction, + sap_main_heating_code=d.sap_main_heating_code, + central_heating_pump_age=d.central_heating_pump_age, + main_heating_data_source=d.main_heating_data_source, + main_heating_index_number=d.main_heating_index_number, + ) + for d in schema.sap_heating.main_heating_details + ], + has_fixed_air_conditioning=schema.sap_heating.has_fixed_air_conditioning + == "true", + cylinder_size=schema.sap_heating.cylinder_size, + water_heating_code=schema.sap_heating.water_heating_code, + water_heating_fuel=schema.sap_heating.water_heating_fuel, + immersion_heating_type=schema.sap_heating.immersion_heating_type, + shower_outlets=( + ShowerOutlets( + ShowerOutlet( + shower_wwhrs=schema.sap_heating.shower_outlets.shower_outlet.shower_wwhrs, + shower_outlet_type=schema.sap_heating.shower_outlets.shower_outlet.shower_outlet_type, + ) + ) + if schema.sap_heating.shower_outlets + else None + ), + cylinder_insulation_type=schema.sap_heating.cylinder_insulation_type, + cylinder_thermostat=schema.sap_heating.cylinder_thermostat, + secondary_fuel_type=schema.sap_heating.secondary_fuel_type, + secondary_heating_type=schema.sap_heating.secondary_heating_type, + cylinder_insulation_thickness=schema.sap_heating.cylinder_insulation_thickness, + ), + # SAP windows + sap_windows=[ + SapWindow( + pvc_frame=w.pvc_frame, + glazing_gap=w.glazing_gap, + orientation=w.orientation, + window_type=w.window_type, + frame_factor=w.frame_factor, + glazing_type=w.glazing_type, + window_width=w.window_width, + window_height=w.window_height, + draught_proofed=w.draught_proofed == "true", + window_location=w.window_location, + window_wall_type=w.window_wall_type, + permanent_shutters_present=w.permanent_shutters_present == "Y", + window_transmission_details=WindowTransmissionDetails( + u_value=w.window_transmission_details.u_value, + data_source=w.window_transmission_details.data_source, + solar_transmittance=w.window_transmission_details.solar_transmittance, + ), + permanent_shutters_insulated=w.permanent_shutters_insulated, + ) + for w in schema.sap_windows + ], + # SAP energy source + sap_energy_source=SapEnergySource( + mains_gas=es.mains_gas == "Y", + meter_type=str(es.meter_type), + pv_battery_count=es.pv_battery_count, + wind_turbines_count=es.wind_turbines_count, + gas_smart_meter_present=es.gas_smart_meter_present == "true", + is_dwelling_export_capable=es.is_dwelling_export_capable == "true", + wind_turbines_terrain_type=str(es.wind_turbines_terrain_type), + electricity_smart_meter_present=es.electricity_smart_meter_present + == "true", + pv_connection=es.pv_connection, + photovoltaic_supply=( + PhotovoltaicSupply( + none_or_no_details=PhotovoltaicSupplyNoneOrNoDetails( + percent_roof_area=es.photovoltaic_supply.none_or_no_details.percent_roof_area, + ) + ) + if es.photovoltaic_supply + else None + ), + wind_turbine_details=( + WindTurbineDetails( + hub_height=es.wind_turbine_details.hub_height, + rotor_diameter=es.wind_turbine_details.rotor_diameter, + ) + if es.wind_turbine_details + else None + ), + pv_batteries=( + PvBatteries( + pv_battery=PvBattery( + battery_capacity=es.pv_batteries.pv_battery.battery_capacity + ) + ) + if es.pv_batteries + else None + ), + ), + # SAP building parts + sap_building_parts=[ + SapBuildingPart( + identifier=bp.identifier, + construction_age_band=bp.construction_age_band, + wall_construction=bp.wall_construction, + wall_insulation_type=bp.wall_insulation_type, + wall_thickness_measured=bp.wall_thickness_measured == "Y", + party_wall_construction=bp.party_wall_construction, + sap_floor_dimensions=[ + SapFloorDimension( + room_height_m=fd.room_height.value, + total_floor_area_m2=fd.total_floor_area.value, + party_wall_length_m=( + float(fd.party_wall_length) + if isinstance(fd.party_wall_length, int) + else fd.party_wall_length.value + ), + heat_loss_perimeter_m=fd.heat_loss_perimeter.value, + floor=fd.floor, + floor_insulation=fd.floor_insulation, + floor_construction=fd.floor_construction, + ) + for fd in bp.sap_floor_dimensions + ], + building_part_number=bp.building_part_number, + wall_dry_lined=bp.wall_dry_lined == "Y", + wall_thickness_mm=bp.wall_thickness, + wall_insulation_thickness=bp.wall_insulation_thickness, + floor_heat_loss=bp.floor_heat_loss, + floor_insulation_thickness=bp.floor_insulation_thickness, + roof_construction=bp.roof_construction, + roof_insulation_location=bp.roof_insulation_location, + roof_insulation_thickness=bp.roof_insulation_thickness, + sap_room_in_roof=( + SapRoomInRoof( + floor_area=bp.sap_room_in_roof.floor_area, + construction_age_band=bp.sap_room_in_roof.construction_age_band, + ) + if bp.sap_room_in_roof + else None + ), + sap_alternative_wall_1=( + SapAlternativeWall( + wall_area=bp.sap_alternative_wall_1.wall_area, + wall_dry_lined=bp.sap_alternative_wall_1.wall_dry_lined, + wall_construction=bp.sap_alternative_wall_1.wall_construction, + wall_insulation_type=bp.sap_alternative_wall_1.wall_insulation_type, + wall_thickness_measured=bp.sap_alternative_wall_1.wall_thickness_measured, + wall_insulation_thickness=bp.sap_alternative_wall_1.wall_insulation_thickness, + ) + if bp.sap_alternative_wall_1 + else None + ), + sap_alternative_wall_2=( + SapAlternativeWall( + wall_area=bp.sap_alternative_wall_2.wall_area, + wall_dry_lined=bp.sap_alternative_wall_2.wall_dry_lined, + wall_construction=bp.sap_alternative_wall_2.wall_construction, + wall_insulation_type=bp.sap_alternative_wall_2.wall_insulation_type, + wall_thickness_measured=bp.sap_alternative_wall_2.wall_thickness_measured, + wall_insulation_thickness=bp.sap_alternative_wall_2.wall_insulation_thickness, + ) + if bp.sap_alternative_wall_2 + else None + ), + ) + for bp in schema.sap_building_parts + ], + ) + + @staticmethod + def _map_21_0_01_energy_element( + element: EnergyElement_21_0_1, + ) -> EnergyElement: + return EnergyElement( + description=element.description.value, + energy_efficiency_rating=element.energy_efficiency_rating, + environmental_efficiency_rating=element.environmental_efficiency_rating, + ) + + @staticmethod + def _map_21_0_01_energy_elements( + elements: List[EnergyElement_21_0_1], + ) -> List[EnergyElement]: + return [EpcPropertyDataMapper._map_21_0_01_energy_element(e) for e in elements] # --------------------------------------------------------------------------- @@ -212,14 +499,18 @@ def _map_sap_window(window: Window) -> SapWindow: ) -def _map_sap_heating(heating: HeatingAndHotWater, ventilation: Ventilation) -> SapHeating: +def _map_sap_heating( + heating: HeatingAndHotWater, ventilation: Ventilation +) -> SapHeating: main = heating.main_heating secondary = heating.secondary_heating # secondary_fuel_type is an int code in the domain model; we can't map a # site-notes string directly, so leave it None unless there is secondary heating. # The string fuel type is preserved via sap_heating when needed. - secondary_fuel_type = None if secondary.secondary_fuel == "No Secondary Heating" else None + secondary_fuel_type = ( + None if secondary.secondary_fuel == "No Secondary Heating" else None + ) return SapHeating( instantaneous_wwhrs=InstantaneousWwhrs(), diff --git a/datatypes/epc/domain/tests/test_from_rdsap_schema.py b/datatypes/epc/domain/tests/test_from_rdsap_schema.py index 5fb6a310..9e6fa0b9 100644 --- a/datatypes/epc/domain/tests/test_from_rdsap_schema.py +++ b/datatypes/epc/domain/tests/test_from_rdsap_schema.py @@ -57,12 +57,10 @@ class TestFromRdSapSchema17_0: assert result.door_count == 2 def test_built_form(self, result: EpcPropertyData) -> None: - # built_form: 2 → "Semi-detached" - assert result.built_form == "Semi-detached" + assert result.built_form == "2" def test_property_type(self, result: EpcPropertyData) -> None: - # property_type: 2 → "Flat" - assert result.property_type == "Flat" + assert result.property_type == "2" # --------------------------------------------------------------------------- @@ -98,12 +96,10 @@ class TestFromRdSapSchema17_1: assert result.door_count == 4 def test_built_form(self, result: EpcPropertyData) -> None: - # built_form: 1 → "Detached" - assert result.built_form == "Detached" + assert result.built_form == "1" def test_property_type(self, result: EpcPropertyData) -> None: - # property_type: 0 → "House" - assert result.property_type == "House" + assert result.property_type == "0" # --------------------------------------------------------------------------- @@ -138,12 +134,10 @@ class TestFromRdSapSchema18_0: assert result.door_count == 2 def test_built_form(self, result: EpcPropertyData) -> None: - # built_form: 4 → "Mid-terrace" - assert result.built_form == "Mid-terrace" + assert result.built_form == "4" def test_property_type(self, result: EpcPropertyData) -> None: - # property_type: 0 → "House" - assert result.property_type == "House" + assert result.property_type == "0" # --------------------------------------------------------------------------- @@ -179,12 +173,10 @@ class TestFromRdSapSchema19_0: assert result.door_count == 1 def test_built_form(self, result: EpcPropertyData) -> None: - # built_form: 2 → "Semi-detached" - assert result.built_form == "Semi-detached" + assert result.built_form == "2" def test_property_type(self, result: EpcPropertyData) -> None: - # property_type: 0 → "House" - assert result.property_type == "House" + assert result.property_type == "0" # --------------------------------------------------------------------------- @@ -219,12 +211,10 @@ class TestFromRdSapSchema20_0_0: assert result.door_count == 2 def test_built_form(self, result: EpcPropertyData) -> None: - # built_form: 2 → "Semi-detached" - assert result.built_form == "Semi-detached" + assert result.built_form == "2" def test_property_type(self, result: EpcPropertyData) -> None: - # property_type: 0 → "House" - assert result.property_type == "House" + assert result.property_type == "0" # --------------------------------------------------------------------------- @@ -258,12 +248,10 @@ class TestFromRdSapSchema21_0_0: assert result.door_count == 3 def test_built_form(self, result: EpcPropertyData) -> None: - # built_form: 2 → "Semi-detached" - assert result.built_form == "Semi-detached" + assert result.built_form == "2" def test_property_type(self, result: EpcPropertyData) -> None: - # property_type: 0 → "House" - assert result.property_type == "House" + assert result.property_type == "0" # --------------------------------------------------------------------------- @@ -296,12 +284,10 @@ class TestFromRdSapSchema21_0_1: assert result.dwelling_type == "Mid-terrace house" def test_property_type(self, result: EpcPropertyData) -> None: - # property_type: 0 → "House" - assert result.property_type == "House" + assert result.property_type == "0" def test_built_form(self, result: EpcPropertyData) -> None: - # built_form: 2 → "Semi-detached" - assert result.built_form == "Semi-detached" + assert result.built_form == "2" def test_address_line_1(self, result: EpcPropertyData) -> None: assert result.address_line_1 == "1 Some Street" diff --git a/datatypes/epc/schema/rdsap_schema_21_0_1.py b/datatypes/epc/schema/rdsap_schema_21_0_1.py index 046e4fec..9b3dbd1d 100644 --- a/datatypes/epc/schema/rdsap_schema_21_0_1.py +++ b/datatypes/epc/schema/rdsap_schema_21_0_1.py @@ -33,13 +33,14 @@ class ShowerOutlets: @dataclass class InstantaneousWwhrs: """References WWHRS product index numbers (introduced in 21.0.0).""" + wwhrs_index_number1: Optional[int] = None wwhrs_index_number2: Optional[int] = None @dataclass class MainHeatingDetail: - has_fghrs: str + has_fghrs: str # TODO: make bool main_fuel_type: int heat_emitter_type: int emitter_temperature: Union[int, str] @@ -49,7 +50,7 @@ class MainHeatingDetail: main_heating_fraction: int main_heating_data_source: int boiler_flue_type: Optional[int] = None - fan_flue_present: Optional[str] = None + fan_flue_present: Optional[str] = None # TODO: make bool boiler_ignition_type: Optional[int] = None central_heating_pump_age: Optional[int] = None main_heating_index_number: Optional[int] = None @@ -132,10 +133,10 @@ class SapWindow: glazing_type: int window_width: float window_height: float - draught_proofed: str + draught_proofed: str # TODO: make bool window_location: int window_wall_type: int - permanent_shutters_present: str + permanent_shutters_present: str # TODO: make bool window_transmission_details: WindowTransmissionDetails permanent_shutters_insulated: str