from datetime import date from typing import List, Optional, Sequence, Union from domain.epc.epc_property_data import ( EnergyElement, EpcPropertyData, InstantaneousWwhrs, MainHeatingDetail, PhotovoltaicSupply, PhotovoltaicSupplyNoneOrNoDetails, PvBatteries, PvBattery, SapAlternativeWall, SapBuildingPart, SapEnergySource, SapFloorDimension, SapHeating, SapRoomInRoof, SapVentilation, 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.surveys.elmhurst_site_notes import ( ElmhurstSiteNotes, VentilationAndCooling as ElmhurstVentilation, Window as ElmhurstWindow, ) from datatypes.epc.surveys.pashub_rdsap_site_notes import ( BuildingConstruction, BuildingMeasurements, ExtensionConstruction, ExtensionMeasurements, ExtensionRoofSpace, FloorMeasurement, HeatingAndHotWater, PasHubRdSapSiteNotes, RoofSpaceDetail, Ventilation, WaterUse, Window, ) AnyRdSapSchema = Union[ RdSapSchema17_0, RdSapSchema17_1, RdSapSchema18_0, RdSapSchema19_0, RdSapSchema20_0_0, RdSapSchema21_0_0, RdSapSchema21_0_1, ] class EpcPropertyDataMapper: @staticmethod def from_site_notes(survey: PasHubRdSapSiteNotes) -> EpcPropertyData: general = survey.general metadata = survey.inspection_metadata address_parts = [p.strip() for p in metadata.property_address.split(", ")] postcode = address_parts[-1] if address_parts else "" post_town = ( address_parts[-3] if len(address_parts) >= 4 else (address_parts[-2] if len(address_parts) >= 3 else "") ) address_line_1 = ( ", ".join(address_parts[:-3]) if len(address_parts) >= 4 else ( ", ".join(address_parts[:-2]) if len(address_parts) >= 3 else address_parts[0] if address_parts else "" ) ) construction = survey.building_construction measurements = survey.building_measurements heating = survey.heating_and_hot_water ventilation = survey.ventilation renewables = survey.renewables room_counts = survey.room_count_elements roof_space = survey.roof_space sap_building_parts = [ _map_main_building_part( construction, measurements, roof_space.main_building ) ] if construction.extensions and measurements.extensions: for ext_c in construction.extensions: matching_m = [m for m in measurements.extensions if m.id == ext_c.id] matching_r = [ r for r in (roof_space.extensions or []) if r.id == ext_c.id ] if matching_m: sap_building_parts.append( _map_extension_building_part( ext_c, matching_m[0], matching_r[0] if matching_r else None ) ) total_floor_area = round( sum( floor.total_floor_area_m2 for part in sap_building_parts for floor in part.sap_floor_dimensions ), 2, ) # TODO: verify that is the correct approach return EpcPropertyData( dwelling_type=f"{general.detachment_type} {general.property_type.lower()}", inspection_date=general.inspection_date, tenure=general.tenure, transaction_type=general.transaction_type, roofs=[], walls=[], floors=[], main_heating=[], door_count=room_counts.number_of_external_doors, sap_heating=_map_sap_heating(heating, ventilation, survey.water_use), sap_windows=[_map_sap_window(w) for w in survey.windows], sap_energy_source=SapEnergySource( mains_gas=general.mains_gas_available, meter_type=general.electric_meter_type, pv_battery_count=renewables.number_of_pv_batteries, wind_turbines_count=0 if not renewables.wind_turbines else 1, gas_smart_meter_present=general.gas_smart_meter, is_dwelling_export_capable=general.dwelling_export_capable, wind_turbines_terrain_type=general.terrain_type, electricity_smart_meter_present=general.electricity_smart_meter, pv_connection=renewables.pv_connection, photovoltaic_supply=( PhotovoltaicSupply( none_or_no_details=PhotovoltaicSupplyNoneOrNoDetails( percent_roof_area=renewables.percent_roof_covered_pv, ) ) if renewables.percent_roof_covered_pv is not None else None ), ), sap_building_parts=sap_building_parts, solar_water_heating=renewables.solar_hot_water, has_hot_water_cylinder=heating.water_heating.cylinder_size != "No Cylinder", 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 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, cfl_fixed_lighting_bulbs_count=room_counts.number_of_fixed_cfl_bulbs, led_fixed_lighting_bulbs_count=room_counts.number_of_fixed_led_bulbs, incandescent_fixed_lighting_bulbs_count=room_counts.number_of_fixed_incandescent_bulbs, total_floor_area_m2=total_floor_area, built_form=general.detachment_type, property_type=general.property_type, has_conservatory=survey.conservatories.has_conservatory, blocked_chimneys_count=room_counts.number_of_blocked_chimneys, draughtproofed_door_count=room_counts.number_of_draughtproofed_external_doors, address_line_1=address_line_1, post_town=post_town, postcode=postcode, report_reference=metadata.report_reference, number_of_storeys=general.number_of_storeys, any_unheated_rooms=room_counts.any_unheated_rooms, waste_water_heat_recovery=room_counts.waste_water_heat_recovery, hydro=renewables.hydro, photovoltaic_array=renewables.photovoltaic_array, sap_ventilation=_map_sap_ventilation(ventilation), ) @staticmethod def from_elmhurst_site_notes(survey: ElmhurstSiteNotes) -> EpcPropertyData: pd = survey.property_details built_form = _strip_code(survey.attachment) property_type = _strip_code(survey.property_type) prefix = pd.house_number or pd.house_name or "" address_line_1 = f"{prefix}, {pd.street}" if prefix else pd.street return EpcPropertyData( dwelling_type=f"{built_form} {property_type.lower()}", inspection_date=pd.inspection_date, tenure=pd.tenure, transaction_type=pd.transaction_type, address_line_1=address_line_1, post_town=pd.town, postcode=pd.postcode, report_reference=pd.reference_number, roofs=[], walls=[], floors=[], main_heating=[], door_count=survey.door_count, sap_heating=_map_elmhurst_sap_heating(survey), sap_windows=[_map_elmhurst_window(w) for w in survey.windows], sap_energy_source=SapEnergySource( mains_gas=survey.meters.main_gas, meter_type=survey.meters.electricity_meter_type, pv_battery_count=0, wind_turbines_count=1 if survey.renewables.wind_turbine_present else 0, gas_smart_meter_present=survey.meters.gas_smart_meter, is_dwelling_export_capable=survey.renewables.export_capable_meter, wind_turbines_terrain_type=survey.renewables.wind_turbines_terrain_type, electricity_smart_meter_present=survey.meters.electricity_smart_meter, ), sap_building_parts=[_map_elmhurst_building_part(survey)], solar_water_heating=survey.renewables.solar_water_heating, has_hot_water_cylinder=survey.water_heating.hot_water_cylinder_present, has_fixed_air_conditioning=survey.ventilation.fixed_space_cooling, wet_rooms_count=0, extensions_count=0, heated_rooms_count=survey.heated_habitable_rooms, open_chimneys_count=survey.ventilation.open_chimneys_count, habitable_rooms_count=survey.habitable_rooms, insulated_door_count=survey.insulated_door_count, cfl_fixed_lighting_bulbs_count=survey.lighting.cfl_count, led_fixed_lighting_bulbs_count=survey.lighting.led_count, incandescent_fixed_lighting_bulbs_count=survey.lighting.incandescent_count, total_floor_area_m2=round( sum(f.area_m2 for f in survey.dimensions.floors), 2 ), built_form=built_form, property_type=property_type, has_conservatory=survey.has_conservatory, blocked_chimneys_count=survey.ventilation.blocked_chimneys_count, number_of_storeys=survey.number_of_storeys, hydro=survey.renewables.hydro_electricity_generated_kwh > 0, photovoltaic_array=survey.renewables.photovoltaic_panel != "None", sap_ventilation=_map_elmhurst_ventilation(survey.ventilation), percent_draughtproofed=survey.draught_proofing_percent, waste_water_heat_recovery=( "None" if not survey.renewables.wwhrs_present else "Present" ), any_unheated_rooms=survey.heated_habitable_rooms < survey.habitable_rooms, low_energy_fixed_lighting_bulbs_count=( survey.lighting.low_energy_count if not survey.lighting.led_cfl_count_known else None ), energy_rating_current=survey.current_sap_rating, energy_rating_potential=survey.potential_sap_rating, environmental_impact_current=survey.current_ei_rating, environmental_impact_potential=survey.potential_ei_rating, co2_emissions_current=survey.co2_emissions_current_t, ) @staticmethod def from_rdsap_schema_17_0(schema: RdSapSchema17_0) -> EpcPropertyData: es = schema.sap_energy_source return EpcPropertyData( uprn=schema.uprn, assessment_type=schema.assessment_type, sap_version=schema.sap_version, dwelling_type=schema.dwelling_type.value, 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), 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, door_count=schema.door_count, habitable_rooms_count=schema.habitable_room_count, heated_rooms_count=schema.heated_room_count, wet_rooms_count=0, extensions_count=schema.extensions_count, open_chimneys_count=0, insulated_door_count=schema.insulated_door_count, draughtproofed_door_count=None, led_fixed_lighting_bulbs_count=0, cfl_fixed_lighting_bulbs_count=0, incandescent_fixed_lighting_bulbs_count=0, roofs=EpcPropertyDataMapper._map_energy_elements(schema.roofs), walls=EpcPropertyDataMapper._map_energy_elements(schema.walls), floors=EpcPropertyDataMapper._map_energy_elements(schema.floors), main_heating=EpcPropertyDataMapper._map_energy_elements( schema.main_heating ), window=EpcPropertyDataMapper._map_energy_element(schema.window), lighting=EpcPropertyDataMapper._map_energy_element(schema.lighting), hot_water=EpcPropertyDataMapper._map_energy_element(schema.hot_water), secondary_heating=EpcPropertyDataMapper._map_energy_element( schema.secondary_heating ), sap_heating=SapHeating( instantaneous_wwhrs=InstantaneousWwhrs(), main_heating_details=[ MainHeatingDetail( has_fghrs=d.has_fghrs == "Y", main_fuel_type=d.main_fuel_type, boiler_flue_type=None, fan_flue_present=None, heat_emitter_type=d.heat_emitter_type, emitter_temperature=d.emitter_temperature, main_heating_number=d.main_heating_number, 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=None, main_heating_data_source=d.main_heating_data_source, main_heating_index_number=None, ) 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, cylinder_insulation_type=schema.sap_heating.cylinder_insulation_type, cylinder_thermostat=None, secondary_fuel_type=None, secondary_heating_type=None, cylinder_insulation_thickness_mm=None, ), sap_windows=[], sap_energy_source=SapEnergySource( mains_gas=es.mains_gas == "Y", meter_type=str(es.meter_type), pv_battery_count=0, wind_turbines_count=es.wind_turbines_count, gas_smart_meter_present=False, is_dwelling_export_capable=False, wind_turbines_terrain_type=str(es.wind_turbines_terrain_type), electricity_smart_meter_present=False, 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 ), ), 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=None, floor_construction=None, ) 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=None, roof_construction=bp.roof_construction, roof_insulation_location=bp.roof_insulation_location, roof_insulation_thickness=bp.roof_insulation_thickness, sap_room_in_roof=None, ) for bp in schema.sap_building_parts ], ) @staticmethod def from_rdsap_schema_17_1(schema: RdSapSchema17_1) -> EpcPropertyData: es = schema.sap_energy_source return EpcPropertyData( uprn=schema.uprn, assessment_type=schema.assessment_type, sap_version=schema.sap_version, dwelling_type=schema.dwelling_type.value, 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), 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, door_count=schema.door_count, habitable_rooms_count=schema.habitable_room_count, heated_rooms_count=schema.heated_room_count, wet_rooms_count=0, extensions_count=schema.extensions_count, open_chimneys_count=0, insulated_door_count=schema.insulated_door_count, draughtproofed_door_count=None, led_fixed_lighting_bulbs_count=0, cfl_fixed_lighting_bulbs_count=0, incandescent_fixed_lighting_bulbs_count=0, roofs=EpcPropertyDataMapper._map_energy_elements(schema.roofs), walls=EpcPropertyDataMapper._map_energy_elements(schema.walls), floors=EpcPropertyDataMapper._map_energy_elements(schema.floors), main_heating=EpcPropertyDataMapper._map_energy_elements( schema.main_heating ), window=EpcPropertyDataMapper._map_energy_element(schema.window), lighting=EpcPropertyDataMapper._map_energy_element(schema.lighting), hot_water=EpcPropertyDataMapper._map_energy_element(schema.hot_water), secondary_heating=EpcPropertyDataMapper._map_energy_element( schema.secondary_heating ), sap_heating=SapHeating( instantaneous_wwhrs=InstantaneousWwhrs(), 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, 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=None, 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, 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_mm=schema.sap_heating.cylinder_insulation_thickness, ), sap_windows=[], sap_energy_source=SapEnergySource( mains_gas=es.mains_gas == "Y", meter_type=str(es.meter_type), pv_battery_count=0, wind_turbines_count=es.wind_turbines_count, gas_smart_meter_present=False, is_dwelling_export_capable=False, wind_turbines_terrain_type=str(es.wind_turbines_terrain_type), electricity_smart_meter_present=False, 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 ), ), 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=None, roof_construction=bp.roof_construction, roof_insulation_location=bp.roof_insulation_location, roof_insulation_thickness=bp.roof_insulation_thickness, sap_room_in_roof=None, ) for bp in schema.sap_building_parts ], ) @staticmethod def from_rdsap_schema_18_0(schema: RdSapSchema18_0) -> EpcPropertyData: es = schema.sap_energy_source return EpcPropertyData( uprn=schema.uprn, assessment_type=schema.assessment_type, sap_version=schema.sap_version, dwelling_type=schema.dwelling_type.value, 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), 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, door_count=schema.door_count, habitable_rooms_count=schema.habitable_room_count, heated_rooms_count=schema.heated_room_count, wet_rooms_count=0, extensions_count=schema.extensions_count, open_chimneys_count=0, insulated_door_count=schema.insulated_door_count, draughtproofed_door_count=None, led_fixed_lighting_bulbs_count=0, cfl_fixed_lighting_bulbs_count=0, incandescent_fixed_lighting_bulbs_count=0, roofs=EpcPropertyDataMapper._map_energy_elements(schema.roofs), walls=EpcPropertyDataMapper._map_energy_elements(schema.walls), floors=EpcPropertyDataMapper._map_energy_elements(schema.floors), main_heating=EpcPropertyDataMapper._map_energy_elements( schema.main_heating ), window=EpcPropertyDataMapper._map_energy_element(schema.window), lighting=EpcPropertyDataMapper._map_energy_element(schema.lighting), hot_water=EpcPropertyDataMapper._map_energy_element(schema.hot_water), secondary_heating=EpcPropertyDataMapper._map_energy_element( schema.secondary_heating ), sap_heating=SapHeating( instantaneous_wwhrs=InstantaneousWwhrs(), 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, 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, 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_mm=schema.sap_heating.cylinder_insulation_thickness, ), sap_windows=[], sap_energy_source=SapEnergySource( mains_gas=es.mains_gas == "Y", meter_type=str(es.meter_type), pv_battery_count=0, wind_turbines_count=es.wind_turbines_count, gas_smart_meter_present=False, is_dwelling_export_capable=False, wind_turbines_terrain_type=str(es.wind_turbines_terrain_type), electricity_smart_meter_present=False, 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 ), ), 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.value, construction_age_band=bp.sap_room_in_roof.construction_age_band, ) if bp.sap_room_in_roof else None ), ) for bp in schema.sap_building_parts ], ) @staticmethod def from_rdsap_schema_19_0(schema: RdSapSchema19_0) -> EpcPropertyData: es = schema.sap_energy_source return EpcPropertyData( uprn=schema.uprn, assessment_type=schema.assessment_type, sap_version=schema.sap_version, dwelling_type=schema.dwelling_type.value, 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), 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, door_count=schema.door_count, habitable_rooms_count=schema.habitable_room_count, heated_rooms_count=schema.heated_room_count, wet_rooms_count=0, extensions_count=schema.extensions_count, open_chimneys_count=0, insulated_door_count=schema.insulated_door_count, draughtproofed_door_count=None, led_fixed_lighting_bulbs_count=0, cfl_fixed_lighting_bulbs_count=0, incandescent_fixed_lighting_bulbs_count=0, roofs=EpcPropertyDataMapper._map_energy_elements(schema.roofs), walls=EpcPropertyDataMapper._map_energy_elements(schema.walls), floors=EpcPropertyDataMapper._map_energy_elements(schema.floors), main_heating=EpcPropertyDataMapper._map_energy_elements( schema.main_heating ), window=EpcPropertyDataMapper._map_energy_element(schema.window), lighting=EpcPropertyDataMapper._map_energy_element(schema.lighting), hot_water=EpcPropertyDataMapper._map_energy_element(schema.hot_water), secondary_heating=EpcPropertyDataMapper._map_energy_element( schema.secondary_heating ), sap_heating=SapHeating( instantaneous_wwhrs=InstantaneousWwhrs(), 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, 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, 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_mm=schema.sap_heating.cylinder_insulation_thickness, ), # 19.0 has no per-window list; individual window fields are at schema root sap_windows=[], sap_energy_source=SapEnergySource( mains_gas=es.mains_gas == "Y", meter_type=str(es.meter_type), pv_battery_count=0, wind_turbines_count=es.wind_turbines_count, gas_smart_meter_present=False, is_dwelling_export_capable=False, wind_turbines_terrain_type=str(es.wind_turbines_terrain_type), electricity_smart_meter_present=False, 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 ), ), 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 is a Measurement in 19.0 floor_area=bp.sap_room_in_roof.floor_area.value, construction_age_band=bp.sap_room_in_roof.construction_age_band, ) if bp.sap_room_in_roof else None ), ) for bp in schema.sap_building_parts ], ) @staticmethod def from_rdsap_schema_20_0_0(schema: RdSapSchema20_0_0) -> EpcPropertyData: es = schema.sap_energy_source return EpcPropertyData( 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), 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, door_count=schema.door_count, habitable_rooms_count=schema.habitable_room_count, heated_rooms_count=schema.heated_room_count, wet_rooms_count=0, extensions_count=schema.extensions_count, open_chimneys_count=0, insulated_door_count=schema.insulated_door_count, draughtproofed_door_count=None, led_fixed_lighting_bulbs_count=0, cfl_fixed_lighting_bulbs_count=0, incandescent_fixed_lighting_bulbs_count=0, roofs=EpcPropertyDataMapper._map_energy_elements(schema.roofs), walls=EpcPropertyDataMapper._map_energy_elements(schema.walls), floors=EpcPropertyDataMapper._map_energy_elements(schema.floors), main_heating=EpcPropertyDataMapper._map_energy_elements( schema.main_heating ), window=EpcPropertyDataMapper._map_energy_element(schema.window), lighting=EpcPropertyDataMapper._map_energy_element(schema.lighting), hot_water=EpcPropertyDataMapper._map_energy_element(schema.hot_water), secondary_heating=EpcPropertyDataMapper._map_energy_element( schema.secondary_heating ), sap_heating=SapHeating( # 20.0.0 uses room counts not product index numbers; domain fields default to None instantaneous_wwhrs=InstantaneousWwhrs(), 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, 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, 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_mm=schema.sap_heating.cylinder_insulation_thickness, ), # 20.0.0 SapWindow lacks frame/gap/draught fields present in later schemas sap_windows=[ SapWindow( frame_material=None, glazing_gap=0, orientation=w.orientation, window_type=w.window_type, glazing_type=w.glazing_type, window_width=0.0, window_height=0.0, draught_proofed=False, window_location=w.window_location, window_wall_type=0, permanent_shutters_present=False, ) for w in schema.sap_windows ], sap_energy_source=SapEnergySource( mains_gas=es.mains_gas == "Y", meter_type=str(es.meter_type), pv_battery_count=0, wind_turbines_count=es.wind_turbines_count, gas_smart_meter_present=False, is_dwelling_export_capable=False, wind_turbines_terrain_type=str(es.wind_turbines_terrain_type), electricity_smart_meter_present=False, 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 ), ), 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 ), ) for bp in schema.sap_building_parts ], ) @staticmethod def from_rdsap_schema_21_0_0(schema: RdSapSchema21_0_0) -> EpcPropertyData: es = schema.sap_energy_source return EpcPropertyData( 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), 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, 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, 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, roofs=EpcPropertyDataMapper._map_energy_elements(schema.roofs), walls=EpcPropertyDataMapper._map_energy_elements(schema.walls), floors=EpcPropertyDataMapper._map_energy_elements(schema.floors), main_heating=EpcPropertyDataMapper._map_energy_elements( schema.main_heating ), window=EpcPropertyDataMapper._map_energy_element(schema.window), lighting=EpcPropertyDataMapper._map_energy_element(schema.lighting), hot_water=EpcPropertyDataMapper._map_energy_element(schema.hot_water), secondary_heating=EpcPropertyDataMapper._map_energy_element( schema.secondary_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_mm=schema.sap_heating.cylinder_insulation_thickness, ), sap_windows=[ SapWindow( frame_material="PVC" if w.pvc_frame == "true" else None, 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=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=[ 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 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_energy_elements(schema.roofs), walls=EpcPropertyDataMapper._map_energy_elements(schema.walls), floors=EpcPropertyDataMapper._map_energy_elements(schema.floors), main_heating=EpcPropertyDataMapper._map_energy_elements( schema.main_heating ), window=EpcPropertyDataMapper._map_energy_element(schema.window), lighting=EpcPropertyDataMapper._map_energy_element(schema.lighting), hot_water=EpcPropertyDataMapper._map_energy_element(schema.hot_water), secondary_heating=EpcPropertyDataMapper._map_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_mm=schema.sap_heating.cylinder_insulation_thickness, ), # SAP windows sap_windows=[ SapWindow( frame_material="PVC" if w.pvc_frame == "true" else None, 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_energy_element( element: Union[ EnergyElement_17_0, EnergyElement_17_1, EnergyElement_18_0, EnergyElement_19_0, EnergyElement_20_0, EnergyElement_21_0, EnergyElement_21_0_1, ], ) -> EnergyElement: description = ( element.description if isinstance(element.description, str) else element.description.value ) return EnergyElement( description=description, energy_efficiency_rating=element.energy_efficiency_rating, environmental_efficiency_rating=element.environmental_efficiency_rating, ) @staticmethod def _map_energy_elements( elements: Sequence[ Union[ EnergyElement_17_0, EnergyElement_17_1, EnergyElement_18_0, EnergyElement_19_0, EnergyElement_20_0, EnergyElement_21_0, EnergyElement_21_0_1, ] ], ) -> List[EnergyElement]: return [EpcPropertyDataMapper._map_energy_element(e) for e in elements] # --------------------------------------------------------------------------- # Private helpers # --------------------------------------------------------------------------- def _strip_code(value: str) -> str: """Strip leading uppercase code from Elmhurst coded strings, e.g. 'CA Cavity' → 'Cavity'.""" parts = value.split(" ", 1) return parts[1] if len(parts) > 1 else value def _extract_age_band(age_range: str) -> str: """Return the letter code from a site-notes age range, e.g. 'I: 1996 - 2002' → 'I'.""" return age_range.split(":")[0].strip() def _map_floor_dimensions(floors: List[FloorMeasurement]) -> List[SapFloorDimension]: return [ SapFloorDimension( room_height_m=floor.height_m, total_floor_area_m2=floor.area_m2, party_wall_length_m=floor.pwl_m, heat_loss_perimeter_m=floor.heat_loss_perimeter_m, floor=int(floor.name.split()[-1]), ) for floor in floors ] def _map_roof( roof: Optional[Union[RoofSpaceDetail, ExtensionRoofSpace]], ) -> tuple[Optional[str], Optional[Union[str, int]]]: if roof is None: return None, None thickness: Optional[Union[str, int]] = ( roof.insulation_thickness_mm if roof.insulation_thickness_mm is not None else roof.insulation_thickness ) return roof.insulation_at or None, thickness def _map_main_building_part( construction: BuildingConstruction, measurements: BuildingMeasurements, roof: RoofSpaceDetail, ) -> SapBuildingPart: main = construction.main_building floor = construction.floor roof_location, roof_thickness = _map_roof(roof) return SapBuildingPart( identifier="main", construction_age_band=_extract_age_band(main.age_range), wall_construction=main.walls_construction_type, wall_insulation_type=main.walls_insulation_type, wall_thickness_measured=main.wall_thickness_mm is not None, party_wall_construction=main.party_wall_construction_type, sap_floor_dimensions=_map_floor_dimensions(measurements.main_building.floors), wall_thickness_mm=main.wall_thickness_mm, roof_insulation_location=roof_location, roof_insulation_thickness=roof_thickness, floor_type=floor.floor_type, floor_construction_type=floor.floor_construction, floor_insulation_type_str=floor.floor_insulation_type, floor_u_value_known=floor.floor_u_value_known, ) def _map_extension_building_part( ext_c: ExtensionConstruction, ext_m: ExtensionMeasurements, roof: Optional[ExtensionRoofSpace], ) -> SapBuildingPart: roof_location, roof_thickness = _map_roof(roof) return SapBuildingPart( identifier=f"extension_{ext_c.id}", construction_age_band=_extract_age_band(ext_c.age_range), wall_construction=ext_c.walls_construction_type, wall_insulation_type=ext_c.walls_insulation_type, wall_thickness_measured=ext_c.wall_thickness_mm is not None, party_wall_construction=ext_c.party_wall_construction_type, sap_floor_dimensions=_map_floor_dimensions(ext_m.floors), wall_thickness_mm=ext_c.wall_thickness_mm, roof_insulation_location=roof_location, roof_insulation_thickness=roof_thickness, ) def _map_sap_window(window: Window) -> SapWindow: return SapWindow( frame_material=window.frame_type, glazing_gap=window.glazing_gap, orientation=window.orientation, window_type=window.window_type, glazing_type=window.glazing_type, window_width=window.width_m, window_height=window.height_m, draught_proofed=window.draught_proofed, window_location=window.location, window_wall_type=window.wall_type, permanent_shutters_present=window.permanent_shutters, ) def _map_sap_heating( heating: HeatingAndHotWater, ventilation: Ventilation, water_use: WaterUse ) -> 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 ) shower_outlets = ( ShowerOutlets( shower_outlet=ShowerOutlet( shower_outlet_type=water_use.showers[0].outlet_type, ) ) if water_use.showers else None ) _ELECTRIC_SYSTEM_TYPES = {"electric storage heaters", "electric underfloor heating"} _raw_fuel = main.fuel.split(", ")[0] if main.fuel else "" fuel_type = ( _raw_fuel if _raw_fuel else ( "Electricity" if main.system_type.lower() in _ELECTRIC_SYSTEM_TYPES else _raw_fuel ) ) return SapHeating( instantaneous_wwhrs=InstantaneousWwhrs(), main_heating_details=[ MainHeatingDetail( has_fghrs=main.flue_gas_heat_recovery_system, main_fuel_type=fuel_type, heat_emitter_type=main.emitter, emitter_temperature=main.emitter_temperature, fan_flue_present=main.fan_assist, main_heating_control=main.controls, condensing=main.condensing, weather_compensator=main.weather_compensator, central_heating_pump_age_str=main.central_heating_pump_age, ) ], has_fixed_air_conditioning=ventilation.has_fixed_air_conditioning, secondary_fuel_type=secondary_fuel_type, secondary_heating_type=heating.secondary_heating.secondary_system, shower_outlets=shower_outlets, cylinder_size=( heating.water_heating.cylinder_size if heating.water_heating.cylinder_size != "No Cylinder" else None ), cylinder_insulation_type=heating.water_heating.insulation_type, cylinder_insulation_thickness_mm=heating.water_heating.insulation_thickness_mm, immersion_heating_type=heating.water_heating.immersion_type, ) def _map_sap_ventilation(ventilation: Ventilation) -> SapVentilation: return SapVentilation( ventilation_type=ventilation.ventilation_type, draught_lobby=ventilation.draught_lobby, pressure_test=ventilation.pressure_test, open_flues_count=ventilation.number_of_open_flues, closed_flues_count=ventilation.number_of_closed_flues, boiler_flues_count=ventilation.number_of_boiler_flues, other_flues_count=ventilation.number_of_other_flues, extract_fans_count=ventilation.number_of_extract_fans, passive_vents_count=ventilation.number_of_passive_vents, flueless_gas_fires_count=ventilation.number_of_flueless_gas_fires, ventilation_in_pcdf_database=ventilation.ventilation_in_pcdf_database, ) def _map_elmhurst_building_part(survey: ElmhurstSiteNotes) -> SapBuildingPart: dims = survey.dimensions floor_dims = [ SapFloorDimension( room_height_m=f.room_height_m, total_floor_area_m2=f.area_m2, party_wall_length_m=f.party_wall_length_m, heat_loss_perimeter_m=f.heat_loss_perimeter_m, floor=i, ) for i, f in enumerate(dims.floors) ] return SapBuildingPart( identifier="main", construction_age_band=_strip_code(survey.construction_age_band), wall_construction=_strip_code(survey.walls.wall_type), wall_insulation_type=_strip_code(survey.walls.insulation), wall_thickness_measured=not survey.walls.thickness_unknown, party_wall_construction=_strip_code(survey.walls.party_wall_type), sap_floor_dimensions=floor_dims, wall_thickness_mm=survey.walls.thickness_mm, roof_insulation_location=_strip_code(survey.roof.insulation), roof_insulation_thickness=survey.roof.insulation_thickness_mm, floor_type=_strip_code(survey.floor.location), floor_construction_type=_strip_code(survey.floor.floor_type), floor_insulation_type_str=_strip_code(survey.floor.insulation), floor_u_value_known=survey.floor.u_value_known, ) def _map_elmhurst_window(w: ElmhurstWindow) -> SapWindow: return SapWindow( frame_material=w.frame_type or None, glazing_gap=w.glazing_gap or "", orientation=w.orientation, window_type="Window", glazing_type=w.glazing_type, window_width=w.width_m, window_height=w.height_m, draught_proofed=w.draught_proofed, window_location=w.building_part, window_wall_type=w.location, permanent_shutters_present=w.permanent_shutters, frame_factor=w.frame_factor, window_transmission_details=WindowTransmissionDetails( u_value=w.u_value, solar_transmittance=w.g_value, data_source=w.data_source, ), ) def _map_elmhurst_sap_heating(survey: ElmhurstSiteNotes) -> SapHeating: mh = survey.main_heating sap_control = mh.heating_controls_sap control = ( sap_control.split(", ", 1)[1] if sap_control.startswith("SAP code") and ", " in sap_control else sap_control ) shower_outlets = ( ShowerOutlets( shower_outlet=ShowerOutlet( shower_outlet_type=survey.baths_and_showers.showers[0].outlet_type ) ) if survey.baths_and_showers.showers else None ) return SapHeating( instantaneous_wwhrs=InstantaneousWwhrs(), main_heating_details=[ MainHeatingDetail( has_fghrs=survey.renewables.flue_gas_heat_recovery_present, main_fuel_type=mh.fuel_type, heat_emitter_type=mh.heat_emitter, emitter_temperature=mh.design_flow_temperature, fan_flue_present=mh.fan_assisted_flue, main_heating_control=control, central_heating_pump_age_str=mh.heat_pump_age, ) ], has_fixed_air_conditioning=survey.ventilation.fixed_space_cooling, shower_outlets=shower_outlets, cylinder_size=( None if not survey.water_heating.hot_water_cylinder_present else survey.water_heating.water_heating_code ), water_heating_code=survey.water_heating.water_heating_sap_code, ) def _map_elmhurst_ventilation(v: ElmhurstVentilation) -> SapVentilation: return SapVentilation( ventilation_type=None, draught_lobby=v.draught_lobby != "Not present", pressure_test=v.pressure_test_method, open_flues_count=v.open_flues_count, closed_flues_count=v.open_chimneys_closed_fire_count, boiler_flues_count=v.solid_fuel_boiler_flues_count, other_flues_count=v.other_heater_flues_count, extract_fans_count=v.extract_fans_count, passive_vents_count=v.passive_vents_count, flueless_gas_fires_count=v.flueless_gas_fires_count, ventilation_in_pcdf_database=None, )