diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index 85d2b022..98d3394d 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import List, Optional, Union from datatypes.epc.domain.dwelling import ( Dwelling, @@ -15,29 +15,46 @@ from datatypes.epc.domain.dwelling import ( WallDetails, WindowDetails, ) +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 ( PasHubRdSapSiteNotes, Window, ) +AnyRdSapSchema = Union[ + RdSapSchema17_0, + RdSapSchema17_1, + RdSapSchema18_0, + RdSapSchema19_0, + RdSapSchema20_0_0, + RdSapSchema21_0_0, + RdSapSchema21_0_1, +] + class DwellingMapper: @staticmethod def from_site_notes(survey: PasHubRdSapSiteNotes) -> Dwelling: return Dwelling( - property_details=_property_details(survey), - floor_dimensions=_floor_dimensions(survey), - walls=_walls(survey), - roof=_roof(survey), - floor=_floor(survey), - windows=[_window(w) for w in survey.windows], - main_heating=_main_heating(survey), - hot_water=_hot_water(survey), - ventilation=_ventilation(survey), - renewables=_renewables(survey), - lighting=_lighting(survey), - secondary_heating=_secondary_heating(survey), + property_details=_sn_property_details(survey), + floor_dimensions=_sn_floor_dimensions(survey), + walls=_sn_walls(survey), + roof=_sn_roof(survey), + floor=_sn_floor(survey), + windows=[_sn_window(w) for w in survey.windows], + main_heating=_sn_main_heating(survey), + hot_water=_sn_hot_water(survey), + ventilation=_sn_ventilation(survey), + renewables=_sn_renewables(survey), + lighting=_sn_lighting(survey), + secondary_heating=_sn_secondary_heating(survey), number_of_habitable_rooms=survey.room_count_elements.number_of_habitable_rooms, number_of_external_doors=survey.room_count_elements.number_of_external_doors, number_of_open_chimneys=survey.room_count_elements.number_of_open_chimneys, @@ -48,8 +65,16 @@ class DwellingMapper: waste_water_heat_recovery=survey.room_count_elements.waste_water_heat_recovery, ) + @staticmethod + def from_rdsap_schema(_schema: AnyRdSapSchema) -> Dwelling: + raise NotImplementedError -def _property_details(survey: PasHubRdSapSiteNotes) -> PropertyDetails: + +# --------------------------------------------------------------------------- +# Site notes helpers +# --------------------------------------------------------------------------- + +def _sn_property_details(survey: PasHubRdSapSiteNotes) -> PropertyDetails: return PropertyDetails( property_type=survey.general.property_type, built_form=survey.general.detachment_type, @@ -64,7 +89,7 @@ def _property_details(survey: PasHubRdSapSiteNotes) -> PropertyDetails: ) -def _floor_dimensions(survey: PasHubRdSapSiteNotes) -> List[FloorDimensions]: +def _sn_floor_dimensions(survey: PasHubRdSapSiteNotes) -> List[FloorDimensions]: dims = [ FloorDimensions( total_floor_area_m2=f.area_m2, @@ -87,7 +112,7 @@ def _floor_dimensions(survey: PasHubRdSapSiteNotes) -> List[FloorDimensions]: return dims -def _walls(survey: PasHubRdSapSiteNotes) -> WallDetails: +def _sn_walls(survey: PasHubRdSapSiteNotes) -> WallDetails: mb = survey.building_construction.main_building return WallDetails( construction_type=mb.walls_construction_type, @@ -97,7 +122,7 @@ def _walls(survey: PasHubRdSapSiteNotes) -> WallDetails: ) -def _roof(survey: PasHubRdSapSiteNotes) -> RoofDetails: +def _sn_roof(survey: PasHubRdSapSiteNotes) -> RoofDetails: mb = survey.roof_space.main_building return RoofDetails( construction_type=mb.construction_type, @@ -107,7 +132,7 @@ def _roof(survey: PasHubRdSapSiteNotes) -> RoofDetails: ) -def _floor(survey: PasHubRdSapSiteNotes) -> FloorDetails: +def _sn_floor(survey: PasHubRdSapSiteNotes) -> FloorDetails: f = survey.building_construction.floor return FloorDetails( construction_type=f.floor_construction, @@ -115,7 +140,7 @@ def _floor(survey: PasHubRdSapSiteNotes) -> FloorDetails: ) -def _window(w: Window) -> WindowDetails: +def _sn_window(w: Window) -> WindowDetails: return WindowDetails( glazing_type=w.glazing_type, orientation=w.orientation, @@ -127,7 +152,7 @@ def _window(w: Window) -> WindowDetails: ) -def _main_heating(survey: PasHubRdSapSiteNotes) -> MainHeatingSystem: +def _sn_main_heating(survey: PasHubRdSapSiteNotes) -> MainHeatingSystem: mh = survey.heating_and_hot_water.main_heating return MainHeatingSystem( fuel=mh.fuel, @@ -143,7 +168,7 @@ def _main_heating(survey: PasHubRdSapSiteNotes) -> MainHeatingSystem: ) -def _hot_water(survey: PasHubRdSapSiteNotes) -> HotWaterSystem: +def _sn_hot_water(survey: PasHubRdSapSiteNotes) -> HotWaterSystem: wh = survey.heating_and_hot_water.water_heating return HotWaterSystem( source=wh.system, @@ -154,14 +179,14 @@ def _hot_water(survey: PasHubRdSapSiteNotes) -> HotWaterSystem: ) -def _secondary_heating(survey: PasHubRdSapSiteNotes) -> Optional[SecondaryHeatingSystem]: +def _sn_secondary_heating(survey: PasHubRdSapSiteNotes) -> Optional[SecondaryHeatingSystem]: fuel = survey.heating_and_hot_water.secondary_heating.secondary_fuel if fuel == "No Secondary Heating": return None return SecondaryHeatingSystem(fuel=fuel) -def _ventilation(survey: PasHubRdSapSiteNotes) -> VentilationDetails: +def _sn_ventilation(survey: PasHubRdSapSiteNotes) -> VentilationDetails: v = survey.ventilation return VentilationDetails( ventilation_type=v.ventilation_type, @@ -178,7 +203,7 @@ def _ventilation(survey: PasHubRdSapSiteNotes) -> VentilationDetails: ) -def _renewables(survey: PasHubRdSapSiteNotes) -> RenewablesDetails: +def _sn_renewables(survey: PasHubRdSapSiteNotes) -> RenewablesDetails: r = survey.renewables return RenewablesDetails( has_photovoltaic=r.photovoltaic_array, @@ -189,7 +214,7 @@ def _renewables(survey: PasHubRdSapSiteNotes) -> RenewablesDetails: ) -def _lighting(survey: PasHubRdSapSiteNotes) -> LightingDetails: +def _sn_lighting(survey: PasHubRdSapSiteNotes) -> LightingDetails: rc = survey.room_count_elements return LightingDetails( number_of_led_bulbs=rc.number_of_fixed_led_bulbs, diff --git a/datatypes/epc/domain/tests/test_from_rdsap_schema.py b/datatypes/epc/domain/tests/test_from_rdsap_schema.py new file mode 100644 index 00000000..a285147e --- /dev/null +++ b/datatypes/epc/domain/tests/test_from_rdsap_schema.py @@ -0,0 +1,187 @@ +import json +import os +from typing import Any, Dict + +import pytest + +from datatypes.epc.domain import Dwelling +from datatypes.epc.domain.mapper import DwellingMapper +from datatypes.epc.schema.rdsap_schema_21_0_1 import RdSapSchema21_0_1 +from datatypes.epc.schema.tests.helpers import from_dict + +FIXTURES = os.path.join( + os.path.dirname(__file__), + "../../schema/tests/fixtures", +) + + +def load(filename: str) -> Dict[str, Any]: + with open(os.path.join(FIXTURES, filename)) as f: + return json.load(f) # type: ignore[no-any-return] + + +class TestFromRdSapSchema21_0_1: + + @pytest.fixture + def dwelling(self) -> Dwelling: + schema = from_dict(RdSapSchema21_0_1, load("21_0_1.json")) + return DwellingMapper.from_rdsap_schema(schema) + + # --- property_details --- + + def test_property_type(self, dwelling: Dwelling) -> None: + # property_type: 0 → House + assert dwelling.property_details.property_type == "House" + + def test_built_form(self, dwelling: Dwelling) -> None: + # built_form: 2 → Semi-detached + assert dwelling.property_details.built_form == "Semi-detached" + + def test_tenure(self, dwelling: Dwelling) -> None: + # tenure: 1 → Owner-occupied + assert dwelling.property_details.tenure == "Owner-occupied" + + def test_construction_age_band(self, dwelling: Dwelling) -> None: + # taken directly from sap_building_parts[0].construction_age_band + assert dwelling.property_details.construction_age_band == "M" + + def test_mains_gas_available(self, dwelling: Dwelling) -> None: + # sap_energy_source.mains_gas: "Y" + assert dwelling.property_details.mains_gas_available is True + + def test_electricity_smart_meter(self, dwelling: Dwelling) -> None: + # sap_energy_source.electricity_smart_meter_present: "true" + assert dwelling.property_details.electricity_smart_meter is True + + def test_gas_smart_meter(self, dwelling: Dwelling) -> None: + # sap_energy_source.gas_smart_meter_present: "false" + assert dwelling.property_details.gas_smart_meter is False + + # --- floor_dimensions --- + + def test_floor_count(self, dwelling: Dwelling) -> None: + # one SapFloorDimension in the fixture + assert len(dwelling.floor_dimensions) == 1 + + def test_floor_area(self, dwelling: Dwelling) -> None: + # total_floor_area.value: 45.82 + assert dwelling.floor_dimensions[0].total_floor_area_m2 == 45.82 + + def test_floor_height(self, dwelling: Dwelling) -> None: + # room_height.value: 2.45 + assert dwelling.floor_dimensions[0].height_m == 2.45 + + def test_heat_loss_perimeter(self, dwelling: Dwelling) -> None: + # heat_loss_perimeter.value: 19.5 + assert dwelling.floor_dimensions[0].heat_loss_perimeter_m == 19.5 + + def test_party_wall_length(self, dwelling: Dwelling) -> None: + # party_wall_length.value: 7.9 + assert dwelling.floor_dimensions[0].party_wall_length_m == 7.9 + + # --- walls --- + + def test_wall_construction_type(self, dwelling: Dwelling) -> None: + # wall_construction: 4 → Cavity wall + assert dwelling.walls.construction_type == "Cavity wall" + + def test_wall_insulation_type(self, dwelling: Dwelling) -> None: + # wall_insulation_type: 2 → As built, insulated (assumed) + assert dwelling.walls.insulation_type == "As built, insulated (assumed)" + + def test_wall_thickness(self, dwelling: Dwelling) -> None: + # wall_thickness_measured: "N" → thickness unknown + assert dwelling.walls.thickness_mm is None + + # --- roof --- + + def test_roof_insulation_thickness(self, dwelling: Dwelling) -> None: + # roof_insulation_thickness: "200mm" + assert dwelling.roof.insulation_thickness_mm == 200 + + def test_roof_has_rooms_in_roof(self, dwelling: Dwelling) -> None: + # sap_room_in_roof is present in the fixture + assert dwelling.roof.has_rooms_in_roof is True + + # --- windows --- + + def test_window_count(self, dwelling: Dwelling) -> None: + assert len(dwelling.windows) == 1 + + def test_window_height(self, dwelling: Dwelling) -> None: + assert dwelling.windows[0].height_m == 2.0 + + def test_window_width(self, dwelling: Dwelling) -> None: + assert dwelling.windows[0].width_m == 1.2 + + def test_window_draught_proofed(self, dwelling: Dwelling) -> None: + # draught_proofed: "true" + assert dwelling.windows[0].draught_proofed is True + + # --- main_heating --- + + def test_main_heating_fuel(self, dwelling: Dwelling) -> None: + # main_fuel_type: 26 → Mains gas + assert dwelling.main_heating.fuel == "Mains gas" + + def test_main_heating_flue_gas_heat_recovery(self, dwelling: Dwelling) -> None: + # has_fghrs: "N" + assert dwelling.main_heating.flue_gas_heat_recovery is False + + # --- hot_water --- + + def test_hot_water_cylinder_present(self, dwelling: Dwelling) -> None: + # has_hot_water_cylinder: "true" + assert dwelling.hot_water.cylinder_size is not None + + # --- secondary_heating --- + + def test_secondary_heating_present(self, dwelling: Dwelling) -> None: + # secondary_fuel_type: 25 → electricity + assert dwelling.secondary_heating is not None + + # --- ventilation --- + + def test_no_fixed_air_conditioning(self, dwelling: Dwelling) -> None: + # has_fixed_air_conditioning: "false" + assert dwelling.ventilation.has_fixed_air_conditioning is False + + # --- renewables --- + + def test_no_solar_hot_water(self, dwelling: Dwelling) -> None: + # solar_water_heating: "N" + assert dwelling.renewables.has_solar_hot_water is False + + def test_no_wind_turbines(self, dwelling: Dwelling) -> None: + # wind_turbines_count: 0 + assert dwelling.renewables.has_wind_turbines is False + + def test_pv_battery_count(self, dwelling: Dwelling) -> None: + # pv_battery_count: 1 + assert dwelling.renewables.number_of_pv_batteries == 1 + + # --- lighting --- + + def test_led_bulbs(self, dwelling: Dwelling) -> None: + assert dwelling.lighting.number_of_led_bulbs == 10 + + def test_cfl_bulbs(self, dwelling: Dwelling) -> None: + assert dwelling.lighting.number_of_cfl_bulbs == 5 + + def test_incandescent_bulbs(self, dwelling: Dwelling) -> None: + assert dwelling.lighting.number_of_incandescent_bulbs == 0 + + # --- dwelling-level counts --- + + def test_habitable_rooms(self, dwelling: Dwelling) -> None: + assert dwelling.number_of_habitable_rooms == 5 + + def test_external_doors(self, dwelling: Dwelling) -> None: + assert dwelling.number_of_external_doors == 3 + + def test_open_chimneys(self, dwelling: Dwelling) -> None: + assert dwelling.number_of_open_chimneys == 1 + + def test_no_conservatory(self, dwelling: Dwelling) -> None: + # conservatory_type: 1 → no conservatory + assert dwelling.has_conservatory is False