Merge pull request #1584 from Hestia-Homes/worktree-issue-1564-secondary-heating-fuel

Map PasHub secondary-heating fuel to a SAP10 fuel code
This commit is contained in:
KhalimCK 2026-07-14 18:38:45 +01:00 committed by GitHub
commit 7a556022c2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 58 additions and 6 deletions

View file

@ -6042,12 +6042,7 @@ def _map_sap_heating(
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
)
secondary_fuel_type = _pashub_secondary_fuel_code(secondary.secondary_fuel)
shower_outlets = (
ShowerOutlets(
@ -7115,6 +7110,28 @@ _PASHUB_MAIN_FUEL_TO_SAP10: Dict[str, int] = {
"Electricity": 30, # the standard-electricity fuel code
}
# PasHub surveyed secondary-heating `secondary_fuel` label → SAP 10.2 Table 32
# fuel code that `secondary_fuel_type` (an int in the domain model) consumes.
# "No Secondary Heating" is the no-secondary-heater shape and maps to None,
# not a fuel code.
_PASHUB_SECONDARY_FUEL_TO_SAP10: Dict[str, int] = {
"Electricity": 30, # standard electricity, Table 32 code 30
}
def _pashub_secondary_fuel_code(fuel_label: str) -> Optional[int]:
"""Resolve a PasHub surveyed secondary-heating Fuel label to a SAP10
Table 32 fuel code at the mapper boundary (ADR-0015). "No Secondary
Heating" is the no-secondary-heater shape and maps to None; a non-empty
label the lookup does not cover strict-raises `UnmappedPasHubLabel` so
the gap is fixed here, never silently mis-billed downstream."""
if not fuel_label or fuel_label == "No Secondary Heating":
return None
code = _PASHUB_SECONDARY_FUEL_TO_SAP10.get(fuel_label)
if code is None:
raise UnmappedPasHubLabel("secondary fuel", fuel_label)
return code
def _pashub_main_fuel_code(fuel_label: str) -> Union[int, str]:
"""Resolve a PasHub surveyed main-heating Fuel label to a SAP10 fuel code

View file

@ -113,6 +113,41 @@ class TestWallConstructionCoding:
EpcPropertyDataMapper.from_site_notes(survey)
class TestSecondaryFuelTypeCoding:
"""`from_site_notes` must int-code the PAS Hub secondary-heating
`secondary_fuel` label to the SAP 10.2 Table 32 `secondary_fuel_type`
code, not leave it hardcoded `None`. ~67/205 Guinness cohort properties
lodge a "Panel, convector or radiant heaters" secondary heater whose
fuel was silently dropped (issue #1564).
"""
@pytest.mark.parametrize(
"label, expected_code",
[
("Electricity", 30), # standard electricity, Table 32 code 30
("No Secondary Heating", None), # no secondary heater lodged
("", None), # no lodging
],
)
def test_label_maps_to_sap_code(
self, label: str, expected_code: Optional[int]
) -> None:
data = load("pashub_rdsap_site_notes_example1.json")
data["heating_and_hot_water"]["secondary_heating"]["secondary_fuel"] = label
survey = from_dict(PasHubRdSapSiteNotes, data)
result = EpcPropertyDataMapper.from_site_notes(survey)
assert result.sap_heating.secondary_fuel_type == expected_code
def test_unknown_label_strict_raises(self) -> None:
data = load("pashub_rdsap_site_notes_example1.json")
data["heating_and_hot_water"]["secondary_heating"][
"secondary_fuel"
] = "Bulk LPG"
survey = from_dict(PasHubRdSapSiteNotes, data)
with pytest.raises(UnmappedPasHubLabel):
EpcPropertyDataMapper.from_site_notes(survey)
class TestWallInsulationTypeCoding:
"""`from_site_notes` must int-code the PAS Hub `walls_insulation_type` label
to the SAP10 wall-insulation code `u_wall` consumes (matching the Elmhurst