mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
Map RdSAP-Schema-17.0 certs to EpcPropertyData 🟩
Dispatch RdSAP-Schema-17.0 through from_api_response, parse-fix the schema (data-driven required->optional, validated against the 1000-cert 17.0 corpus per ADR-0028 — incl. SapHeating.cylinder_insulation_type and the has_hot_water_cylinder / has_fixed_air_conditioning / has_heated_separate_ conservatory flags), and port the defensive mapper reads (dwelling_type str/dict/number, photovoltaic_supply guard, sap_floor_dimensions guard). All 1000 corpus certs now parse and map. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3995433816
commit
26651ca71c
2 changed files with 117 additions and 53 deletions
|
|
@ -458,7 +458,15 @@ class EpcPropertyDataMapper:
|
|||
uprn=schema.uprn,
|
||||
assessment_type=schema.assessment_type,
|
||||
sap_version=schema.sap_version,
|
||||
dwelling_type=schema.dwelling_type.value,
|
||||
# ADR-0028: 17.0 lodges dwelling_type as str (182/1000), localised
|
||||
# dict, or a plain number — coerce all three to str.
|
||||
dwelling_type=(
|
||||
schema.dwelling_type
|
||||
if isinstance(schema.dwelling_type, str)
|
||||
else schema.dwelling_type.value
|
||||
if hasattr(schema.dwelling_type, "value")
|
||||
else str(schema.dwelling_type)
|
||||
),
|
||||
property_type=str(schema.property_type),
|
||||
built_form=str(schema.built_form),
|
||||
address_line_1=schema.address_line_1,
|
||||
|
|
@ -549,7 +557,9 @@ class EpcPropertyDataMapper:
|
|||
percent_roof_area=es.photovoltaic_supply.none_or_no_details.percent_roof_area,
|
||||
)
|
||||
)
|
||||
if es.photovoltaic_supply
|
||||
# ADR-0028: photovoltaic_supply can be absent, None, or a
|
||||
# sparse shape without none_or_no_details — guard the read.
|
||||
if getattr(es.photovoltaic_supply, "none_or_no_details", None)
|
||||
else None
|
||||
),
|
||||
),
|
||||
|
|
@ -563,22 +573,10 @@ class EpcPropertyDataMapper:
|
|||
party_wall_construction=_api_party_wall_construction_int(
|
||||
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
|
||||
],
|
||||
sap_floor_dimensions=_api_build_sap_floor_dimensions(
|
||||
bp.sap_floor_dimensions or [],
|
||||
bp.floor_heat_loss,
|
||||
),
|
||||
building_part_number=bp.building_part_number,
|
||||
wall_dry_lined=bp.wall_dry_lined == "Y",
|
||||
wall_thickness_mm=bp.wall_thickness,
|
||||
|
|
@ -2230,6 +2228,14 @@ class EpcPropertyDataMapper:
|
|||
from_dict(RdSapSchema17_1, data)
|
||||
)
|
||||
)
|
||||
if schema == "RdSAP-Schema-17.0":
|
||||
from datatypes.epc.schema.rdsap_schema_17_0 import RdSapSchema17_0
|
||||
|
||||
return _clear_basement_flag_when_system_built(
|
||||
EpcPropertyDataMapper.from_rdsap_schema_17_0(
|
||||
from_dict(RdSapSchema17_0, data)
|
||||
)
|
||||
)
|
||||
|
||||
raise ValueError(f"Unsupported EPC schema: {schema!r}")
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -29,6 +29,10 @@ class MainHeatingDetail:
|
|||
main_heating_category: int
|
||||
main_heating_fraction: int
|
||||
main_heating_data_source: int
|
||||
boiler_flue_type: Optional[int] = None
|
||||
fan_flue_present: Optional[str] = None
|
||||
central_heating_pump_age: Optional[int] = None
|
||||
main_heating_index_number: Optional[int] = None
|
||||
sap_main_heating_code: Optional[int] = None
|
||||
|
||||
|
||||
|
|
@ -40,8 +44,13 @@ 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: cylinder_insulation_type is absent in 308/1000 17.0 certs.
|
||||
cylinder_insulation_type: Optional[int] = None
|
||||
cylinder_thermostat: Optional[str] = None
|
||||
secondary_fuel_type: Optional[int] = None
|
||||
secondary_heating_type: Optional[int] = None
|
||||
cylinder_insulation_thickness: Optional[int] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -52,7 +61,8 @@ class PhotovoltaicSupplyNoneOrNoDetails:
|
|||
|
||||
@dataclass
|
||||
class PhotovoltaicSupply:
|
||||
none_or_no_details: PhotovoltaicSupplyNoneOrNoDetails
|
||||
# ADR-0028 data-driven required→optional: 3/1000 omit none_or_no_details.
|
||||
none_or_no_details: Optional[PhotovoltaicSupplyNoneOrNoDetails] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -69,27 +79,47 @@ class SapFloorDimension:
|
|||
floor: int
|
||||
room_height: Measurement
|
||||
total_floor_area: Measurement
|
||||
party_wall_length: Measurement
|
||||
party_wall_length: Union[Measurement, int]
|
||||
heat_loss_perimeter: Measurement
|
||||
floor_insulation: Optional[int] = None
|
||||
floor_construction: Optional[int] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
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
|
||||
class SapRoomInRoof:
|
||||
"""Room-in-roof details. floor_area is usually a Measurement but some certs
|
||||
lodge a plain number (ADR-0028) — read via `_measurement_value`."""
|
||||
|
||||
floor_area: Union[Measurement, int, float]
|
||||
insulation: str
|
||||
roof_room_connected: str
|
||||
construction_age_band: str
|
||||
party_wall_construction: Union[int, str]
|
||||
wall_thickness_measured: str
|
||||
roof_insulation_location: Union[int, str]
|
||||
roof_insulation_thickness: str
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class SapBuildingPart:
|
||||
# Data-driven required→optional (ADR-0028): a conservatory-shaped part can
|
||||
# carry only a subset of fields. Every field is Optional (the
|
||||
# 21.0.1/20.0.0/18.0 precedent). 17.0 corpus: 2/1000 omit identifier,
|
||||
# wall_thickness, or roof_insulation_thickness.
|
||||
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
|
||||
roof_insulation_thickness: Optional[Union[str, int]] = None
|
||||
sap_room_in_roof: Optional[SapRoomInRoof] = None
|
||||
wall_insulation_thickness: Optional[str] = None
|
||||
floor_insulation_thickness: Optional[str] = None
|
||||
flat_roof_insulation_thickness: Optional[Union[str, int]] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -117,15 +147,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: some certs lodge a reduced alternative-improvement shape (only
|
||||
# improvement_details/-type). Parse-only — 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
|
||||
|
|
@ -137,6 +169,20 @@ class RenewableHeatIncentive:
|
|||
|
||||
|
||||
@dataclass
|
||||
class SapWindow:
|
||||
"""Per-window geometry. ADR-0028: only 10/1000 17.0 certs lodge this array;
|
||||
window_area arrives as a Measurement and is read via `_measurement_value`.
|
||||
Mirrors the 20.0.0/18.0 SapWindow shape. This is the per-spec Validation
|
||||
Cohort — its lodged geometry is used directly, never synthesised over."""
|
||||
|
||||
orientation: int
|
||||
window_area: float
|
||||
window_type: int
|
||||
glazing_type: int
|
||||
window_location: int
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class RdSapSchema17_0:
|
||||
uprn: int
|
||||
roofs: List[EnergyElement]
|
||||
|
|
@ -152,7 +198,8 @@ class RdSapSchema17_0:
|
|||
built_form: int
|
||||
door_count: int
|
||||
glazed_area: int
|
||||
glazing_gap: str
|
||||
# ADR-0028: glazing_gap lodged as int, str, or omitted (482/1000) — widen.
|
||||
glazing_gap: Optional[Union[int, str]] = None
|
||||
region_code: int
|
||||
report_type: int
|
||||
sap_heating: SapHeating
|
||||
|
|
@ -161,7 +208,9 @@ class RdSapSchema17_0:
|
|||
uprn_source: str
|
||||
country_code: str
|
||||
main_heating: List[EnergyElement]
|
||||
dwelling_type: DescriptionV1
|
||||
# ADR-0028: 182/1000 lodge dwelling_type as a plain str, not a localised
|
||||
# DescriptionV1 object. Widen so both shapes parse.
|
||||
dwelling_type: Union[str, DescriptionV1]
|
||||
language_code: int
|
||||
property_type: int
|
||||
address_line_1: str
|
||||
|
|
@ -174,11 +223,13 @@ class RdSapSchema17_0:
|
|||
transaction_type: int
|
||||
conservatory_type: int
|
||||
heated_room_count: int
|
||||
pvc_window_frames: str
|
||||
# ADR-0028: missing in 343/1000 — widen + default.
|
||||
pvc_window_frames: Optional[str] = None
|
||||
registration_date: str
|
||||
sap_energy_source: SapEnergySource
|
||||
secondary_heating: EnergyElement
|
||||
lzc_energy_sources: List[int]
|
||||
# ADR-0028: present in only 95/1000 — default to empty.
|
||||
lzc_energy_sources: List[int] = field(default_factory=list)
|
||||
sap_building_parts: List[SapBuildingPart]
|
||||
low_energy_lighting: int
|
||||
solar_water_heating: str
|
||||
|
|
@ -190,14 +241,17 @@ class RdSapSchema17_0:
|
|||
energy_rating_current: int
|
||||
lighting_cost_current: CostAmount
|
||||
main_heating_controls: List[EnergyElement]
|
||||
multiple_glazing_type: int
|
||||
# ADR-0028: int code (1-7) or the string "ND" (Not Defined, 54/1000) — widen
|
||||
# so both parse; the synthesis maps "ND" to a default.
|
||||
multiple_glazing_type: Union[int, str]
|
||||
open_fireplaces_count: int
|
||||
has_hot_water_cylinder: str
|
||||
# ADR-0028: a handful of 17.0 certs omit these boolean flags — default them.
|
||||
has_hot_water_cylinder: Optional[str] = None
|
||||
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
|
||||
|
|
@ -205,7 +259,7 @@ class RdSapSchema17_0:
|
|||
hot_water_cost_potential: CostAmount
|
||||
renewable_heat_incentive: RenewableHeatIncentive
|
||||
energy_consumption_current: int
|
||||
has_fixed_air_conditioning: str
|
||||
has_fixed_air_conditioning: Optional[str] = None
|
||||
multiple_glazed_proportion: int
|
||||
calculation_software_version: str
|
||||
energy_consumption_potential: int
|
||||
|
|
@ -213,10 +267,14 @@ class RdSapSchema17_0:
|
|||
fixed_lighting_outlets_count: int
|
||||
current_energy_efficiency_band: str
|
||||
environmental_impact_potential: int
|
||||
has_heated_separate_conservatory: str
|
||||
has_heated_separate_conservatory: Optional[str] = None
|
||||
potential_energy_efficiency_band: str
|
||||
co2_emissions_current_per_floor_area: int
|
||||
low_energy_fixed_lighting_outlets_count: int
|
||||
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 entirely.
|
||||
# The 10 rich certs use lodged window_area directly; the windowless majority
|
||||
# synthesise from the glazed_area band.
|
||||
sap_windows: List[SapWindow] = field(default_factory=list)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue