Dispatch and map RdSAP-Schema-17.1 certs end-to-end 🟩

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jun-te Kim 2026-06-11 12:21:17 +00:00
parent 4ea5367da6
commit 25e550ce3c
2 changed files with 73 additions and 35 deletions

View file

@ -607,7 +607,12 @@ class EpcPropertyDataMapper:
uprn=schema.uprn,
assessment_type=schema.assessment_type,
sap_version=schema.sap_version,
dwelling_type=schema.dwelling_type.value,
# ADR-0028: 17.1 lodges dwelling_type as str OR localised dict.
dwelling_type=(
schema.dwelling_type
if isinstance(schema.dwelling_type, str)
else schema.dwelling_type.value
),
property_type=str(schema.property_type),
built_form=str(schema.built_form),
address_line_1=schema.address_line_1,
@ -701,7 +706,7 @@ class EpcPropertyDataMapper:
percent_roof_area=es.photovoltaic_supply.none_or_no_details.percent_roof_area,
)
)
if es.photovoltaic_supply
if getattr(es.photovoltaic_supply, "none_or_no_details", None)
else None
),
),
@ -2130,6 +2135,14 @@ class EpcPropertyDataMapper:
from_dict(RdSapSchema18_0, data)
)
)
if schema == "RdSAP-Schema-17.1":
from datatypes.epc.schema.rdsap_schema_17_1 import RdSapSchema17_1
return _clear_basement_flag_when_system_built(
EpcPropertyDataMapper.from_rdsap_schema_17_1(
from_dict(RdSapSchema17_1, data)
)
)
raise ValueError(f"Unsupported EPC schema: {schema!r}")

View file

@ -1,4 +1,4 @@
from dataclasses import dataclass
from dataclasses import dataclass, field
from typing import List, Optional, Union
from .common import CostAmount, DescriptionV1, Measurement
@ -36,7 +36,7 @@ class MainHeatingDetail:
sap_main_heating_code: Optional[int] = None
@dataclass
@dataclass(kw_only=True)
class SapHeating:
cylinder_size: int
water_heating_code: int
@ -44,8 +44,9 @@ class SapHeating:
instantaneous_wwhrs: Optional[InstantaneousWwhrs]
main_heating_details: List[MainHeatingDetail]
immersion_heating_type: Union[int, str]
cylinder_insulation_type: int
has_fixed_air_conditioning: str
# ADR-0028: 325/1000 omit cylinder_insulation_type — default it.
cylinder_insulation_type: Optional[int] = None
cylinder_thermostat: Optional[str] = None
secondary_fuel_type: Optional[int] = None
secondary_heating_type: Optional[int] = None
@ -60,7 +61,7 @@ class PhotovoltaicSupplyNoneOrNoDetails:
@dataclass
class PhotovoltaicSupply:
none_or_no_details: PhotovoltaicSupplyNoneOrNoDetails
none_or_no_details: Optional[PhotovoltaicSupplyNoneOrNoDetails] = None
@dataclass
@ -84,23 +85,26 @@ class SapFloorDimension:
floor_construction: Optional[int] = None
@dataclass
@dataclass(kw_only=True)
class SapBuildingPart:
identifier: str
wall_dry_lined: str
wall_thickness: int
floor_heat_loss: int
roof_construction: int
wall_construction: int
building_part_number: int
sap_floor_dimensions: List[SapFloorDimension]
wall_insulation_type: int
construction_age_band: str
party_wall_construction: Union[int, str]
wall_thickness_measured: str
roof_insulation_location: Union[int, str]
# ADR-0028: 17/1000 lodge a conservatory-shaped part with none of the
# construction fields. Every field is Optional (the 18.0/20.0.0 precedent);
# the all-None part flows through harmlessly.
identifier: Optional[str] = None
wall_dry_lined: Optional[str] = None
wall_thickness: Optional[int] = None
floor_heat_loss: Optional[int] = None
roof_construction: Optional[int] = None
wall_construction: Optional[int] = None
building_part_number: Optional[int] = None
sap_floor_dimensions: Optional[List[SapFloorDimension]] = None
wall_insulation_type: Optional[int] = None
construction_age_band: Optional[str] = None
party_wall_construction: Optional[Union[int, str]] = None
wall_thickness_measured: Optional[str] = None
roof_insulation_location: Optional[Union[int, str]] = None
# Can be a thickness string (e.g. "100mm") or 0 for uninsulated flat roofs
roof_insulation_thickness: Union[str, int]
roof_insulation_thickness: Optional[Union[str, int]] = None
wall_insulation_thickness: Optional[str] = None
@ -129,15 +133,17 @@ class SuggestedImprovement:
environmental_impact_rating: int
@dataclass
@dataclass(kw_only=True)
class AlternativeImprovement:
sequence: int
typical_saving: CostAmount
improvement_type: str
improvement_details: ImprovementDetails
improvement_category: int
energy_performance_rating: int
environmental_impact_rating: int
# ADR-0028: parse-only (the mapper does not read alternative_improvements);
# a reduced shape lodges only some fields, so every field is Optional.
sequence: Optional[int] = None
typical_saving: Optional[CostAmount] = None
improvement_type: Optional[str] = None
improvement_details: Optional[ImprovementDetails] = None
improvement_category: Optional[int] = None
energy_performance_rating: Optional[int] = None
environmental_impact_rating: Optional[int] = None
@dataclass
@ -149,6 +155,19 @@ class RenewableHeatIncentive:
@dataclass
class SapWindow:
"""Per-window geometry. ADR-0028: only 14/1000 17.1 certs lodge this array
(all band-4); window_area arrives as a Measurement and is read via
`_measurement_value`. The extra pvc_frame/glazing_gap keys are ignored."""
orientation: int
window_area: float
window_type: int
glazing_type: int
window_location: int
@dataclass(kw_only=True)
class RdSapSchema17_1:
uprn: int
roofs: List[EnergyElement]
@ -164,7 +183,8 @@ class RdSapSchema17_1:
built_form: int
door_count: int
glazed_area: int
glazing_gap: str
# ADR-0028: lodged as int, str, or omitted (478/1000) — widen + default.
glazing_gap: Optional[Union[int, str]] = None
region_code: int
report_type: int
sap_heating: SapHeating
@ -173,7 +193,8 @@ class RdSapSchema17_1:
uprn_source: str
country_code: str
main_heating: List[EnergyElement]
dwelling_type: DescriptionV1
# ADR-0028: 259/1000 lodge dwelling_type as a plain str, not DescriptionV1.
dwelling_type: Union[str, DescriptionV1]
language_code: int
property_type: int
address_line_1: str
@ -186,11 +207,11 @@ class RdSapSchema17_1:
transaction_type: int
conservatory_type: int
heated_room_count: int
pvc_window_frames: str
pvc_window_frames: Optional[str] = None
registration_date: str
sap_energy_source: SapEnergySource
secondary_heating: EnergyElement
lzc_energy_sources: List[int]
lzc_energy_sources: List[int] = field(default_factory=list)
sap_building_parts: List[SapBuildingPart]
low_energy_lighting: int
solar_water_heating: str
@ -202,14 +223,15 @@ class RdSapSchema17_1:
energy_rating_current: int
lighting_cost_current: CostAmount
main_heating_controls: List[EnergyElement]
multiple_glazing_type: int
# ADR-0028: int code (1-7) or "ND" (Not Defined, 56/1000) — widen.
multiple_glazing_type: Union[int, str]
open_fireplaces_count: int
has_hot_water_cylinder: str
heating_cost_potential: CostAmount
hot_water_cost_current: CostAmount
mechanical_ventilation: int
percent_draughtproofed: int
suggested_improvements: List[SuggestedImprovement]
suggested_improvements: List[SuggestedImprovement] = field(default_factory=list)
co2_emissions_potential: float
energy_rating_potential: int
lighting_cost_potential: CostAmount
@ -232,3 +254,6 @@ class RdSapSchema17_1:
sap_flat_details: Optional[SapFlatDetails] = None
address_line_2: Optional[str] = None
alternative_improvements: Optional[List[AlternativeImprovement]] = None
# ADR-0028: additive — the placeholder schema omitted sap_windows, dropping
# the 14 rich certs' lodged geometry. Default [] = windowless (synthesised).
sap_windows: List[SapWindow] = field(default_factory=list)