Map to domain from 21.0.1 schema 🟩

This commit is contained in:
Daniel Roth 2026-04-14 13:20:09 +00:00
parent 1f6fabd171
commit cf088c36fe
4 changed files with 325 additions and 48 deletions

View file

@ -1,13 +1,12 @@
from dataclasses import dataclass from dataclasses import dataclass
from datetime import date from datetime import date
from typing import Any, List, Optional, Union from typing import List, Optional, Union
from datatypes.epc.domain.epc import Epc from datatypes.epc.domain.epc import Epc
@dataclass @dataclass
class EnergyElement: class EnergyElement:
# description is a plain string in schema 21.0.0 (no longer a localised object)
description: str description: str
energy_efficiency_rating: int energy_efficiency_rating: int
environmental_efficiency_rating: int environmental_efficiency_rating: int
@ -54,7 +53,7 @@ class ShowerOutlets:
class SapHeating: class SapHeating:
instantaneous_wwhrs: InstantaneousWwhrs instantaneous_wwhrs: InstantaneousWwhrs
main_heating_details: List[MainHeatingDetail] main_heating_details: List[MainHeatingDetail]
has_fixed_air_conditioning: str has_fixed_air_conditioning: bool
cylinder_size: Optional[int] = ( cylinder_size: Optional[int] = (
None # int code from API; not directly available from site notes None # int code from API; not directly available from site notes
) )

View file

@ -1,23 +1,57 @@
from datetime import date from datetime import date
from typing import List, Union from typing import List, Union
from regex import T
from datatypes.epc.domain.epc_property_data import ( from datatypes.epc.domain.epc_property_data import (
EnergyElement,
EpcPropertyData, EpcPropertyData,
InstantaneousWwhrs, InstantaneousWwhrs,
MainHeatingDetail, MainHeatingDetail,
PhotovoltaicSupply,
PhotovoltaicSupplyNoneOrNoDetails,
PvBatteries,
PvBattery,
SapAlternativeWall,
SapBuildingPart, SapBuildingPart,
SapEnergySource, SapEnergySource,
SapFloorDimension, SapFloorDimension,
SapHeating, SapHeating,
SapRoomInRoof,
SapWindow, 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 ( from datatypes.epc.surveys.pashub_rdsap_site_notes import (
BuildingConstruction, BuildingConstruction,
BuildingMeasurements, BuildingMeasurements,
@ -58,7 +92,9 @@ class EpcPropertyDataMapper:
for ext_c in construction.extensions: for ext_c in construction.extensions:
matching = [m for m in measurements.extensions if m.id == ext_c.id] matching = [m for m in measurements.extensions if m.id == ext_c.id]
if matching: 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( total_floor_area = round(
sum( sum(
@ -97,7 +133,8 @@ class EpcPropertyDataMapper:
has_fixed_air_conditioning=ventilation.has_fixed_air_conditioning, has_fixed_air_conditioning=ventilation.has_fixed_air_conditioning,
wet_rooms_count=0, # no equivalent in site notes wet_rooms_count=0, # no equivalent in site notes
extensions_count=general.number_of_extensions, 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, open_chimneys_count=room_counts.number_of_open_chimneys,
habitable_rooms_count=room_counts.number_of_habitable_rooms, habitable_rooms_count=room_counts.number_of_habitable_rooms,
insulated_door_count=room_counts.number_of_insulated_external_doors, insulated_door_count=room_counts.number_of_insulated_external_doors,
@ -137,8 +174,258 @@ class EpcPropertyDataMapper:
raise NotImplementedError raise NotImplementedError
@staticmethod @staticmethod
def from_rdsap_schema_21_0_1(_schema: RdSapSchema21_0_1) -> EpcPropertyData: def from_rdsap_schema_21_0_1(schema: RdSapSchema21_0_1) -> EpcPropertyData:
raise NotImplementedError 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 main = heating.main_heating
secondary = heating.secondary_heating secondary = heating.secondary_heating
# secondary_fuel_type is an int code in the domain model; we can't map a # 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. # site-notes string directly, so leave it None unless there is secondary heating.
# The string fuel type is preserved via sap_heating when needed. # 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( return SapHeating(
instantaneous_wwhrs=InstantaneousWwhrs(), instantaneous_wwhrs=InstantaneousWwhrs(),

View file

@ -57,12 +57,10 @@ class TestFromRdSapSchema17_0:
assert result.door_count == 2 assert result.door_count == 2
def test_built_form(self, result: EpcPropertyData) -> None: def test_built_form(self, result: EpcPropertyData) -> None:
# built_form: 2 → "Semi-detached" assert result.built_form == "2"
assert result.built_form == "Semi-detached"
def test_property_type(self, result: EpcPropertyData) -> None: def test_property_type(self, result: EpcPropertyData) -> None:
# property_type: 2 → "Flat" assert result.property_type == "2"
assert result.property_type == "Flat"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -98,12 +96,10 @@ class TestFromRdSapSchema17_1:
assert result.door_count == 4 assert result.door_count == 4
def test_built_form(self, result: EpcPropertyData) -> None: def test_built_form(self, result: EpcPropertyData) -> None:
# built_form: 1 → "Detached" assert result.built_form == "1"
assert result.built_form == "Detached"
def test_property_type(self, result: EpcPropertyData) -> None: def test_property_type(self, result: EpcPropertyData) -> None:
# property_type: 0 → "House" assert result.property_type == "0"
assert result.property_type == "House"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -138,12 +134,10 @@ class TestFromRdSapSchema18_0:
assert result.door_count == 2 assert result.door_count == 2
def test_built_form(self, result: EpcPropertyData) -> None: def test_built_form(self, result: EpcPropertyData) -> None:
# built_form: 4 → "Mid-terrace" assert result.built_form == "4"
assert result.built_form == "Mid-terrace"
def test_property_type(self, result: EpcPropertyData) -> None: def test_property_type(self, result: EpcPropertyData) -> None:
# property_type: 0 → "House" assert result.property_type == "0"
assert result.property_type == "House"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -179,12 +173,10 @@ class TestFromRdSapSchema19_0:
assert result.door_count == 1 assert result.door_count == 1
def test_built_form(self, result: EpcPropertyData) -> None: def test_built_form(self, result: EpcPropertyData) -> None:
# built_form: 2 → "Semi-detached" assert result.built_form == "2"
assert result.built_form == "Semi-detached"
def test_property_type(self, result: EpcPropertyData) -> None: def test_property_type(self, result: EpcPropertyData) -> None:
# property_type: 0 → "House" assert result.property_type == "0"
assert result.property_type == "House"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -219,12 +211,10 @@ class TestFromRdSapSchema20_0_0:
assert result.door_count == 2 assert result.door_count == 2
def test_built_form(self, result: EpcPropertyData) -> None: def test_built_form(self, result: EpcPropertyData) -> None:
# built_form: 2 → "Semi-detached" assert result.built_form == "2"
assert result.built_form == "Semi-detached"
def test_property_type(self, result: EpcPropertyData) -> None: def test_property_type(self, result: EpcPropertyData) -> None:
# property_type: 0 → "House" assert result.property_type == "0"
assert result.property_type == "House"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -258,12 +248,10 @@ class TestFromRdSapSchema21_0_0:
assert result.door_count == 3 assert result.door_count == 3
def test_built_form(self, result: EpcPropertyData) -> None: def test_built_form(self, result: EpcPropertyData) -> None:
# built_form: 2 → "Semi-detached" assert result.built_form == "2"
assert result.built_form == "Semi-detached"
def test_property_type(self, result: EpcPropertyData) -> None: def test_property_type(self, result: EpcPropertyData) -> None:
# property_type: 0 → "House" assert result.property_type == "0"
assert result.property_type == "House"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@ -296,12 +284,10 @@ class TestFromRdSapSchema21_0_1:
assert result.dwelling_type == "Mid-terrace house" assert result.dwelling_type == "Mid-terrace house"
def test_property_type(self, result: EpcPropertyData) -> None: def test_property_type(self, result: EpcPropertyData) -> None:
# property_type: 0 → "House" assert result.property_type == "0"
assert result.property_type == "House"
def test_built_form(self, result: EpcPropertyData) -> None: def test_built_form(self, result: EpcPropertyData) -> None:
# built_form: 2 → "Semi-detached" assert result.built_form == "2"
assert result.built_form == "Semi-detached"
def test_address_line_1(self, result: EpcPropertyData) -> None: def test_address_line_1(self, result: EpcPropertyData) -> None:
assert result.address_line_1 == "1 Some Street" assert result.address_line_1 == "1 Some Street"

View file

@ -33,13 +33,14 @@ class ShowerOutlets:
@dataclass @dataclass
class InstantaneousWwhrs: class InstantaneousWwhrs:
"""References WWHRS product index numbers (introduced in 21.0.0).""" """References WWHRS product index numbers (introduced in 21.0.0)."""
wwhrs_index_number1: Optional[int] = None wwhrs_index_number1: Optional[int] = None
wwhrs_index_number2: Optional[int] = None wwhrs_index_number2: Optional[int] = None
@dataclass @dataclass
class MainHeatingDetail: class MainHeatingDetail:
has_fghrs: str has_fghrs: str # TODO: make bool
main_fuel_type: int main_fuel_type: int
heat_emitter_type: int heat_emitter_type: int
emitter_temperature: Union[int, str] emitter_temperature: Union[int, str]
@ -49,7 +50,7 @@ class MainHeatingDetail:
main_heating_fraction: int main_heating_fraction: int
main_heating_data_source: int main_heating_data_source: int
boiler_flue_type: Optional[int] = None 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 boiler_ignition_type: Optional[int] = None
central_heating_pump_age: Optional[int] = None central_heating_pump_age: Optional[int] = None
main_heating_index_number: Optional[int] = None main_heating_index_number: Optional[int] = None
@ -132,10 +133,10 @@ class SapWindow:
glazing_type: int glazing_type: int
window_width: float window_width: float
window_height: float window_height: float
draught_proofed: str draught_proofed: str # TODO: make bool
window_location: int window_location: int
window_wall_type: int window_wall_type: int
permanent_shutters_present: str permanent_shutters_present: str # TODO: make bool
window_transmission_details: WindowTransmissionDetails window_transmission_details: WindowTransmissionDetails
permanent_shutters_insulated: str permanent_shutters_insulated: str