mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-27 23:35:01 +00:00
Merge pull request #1582 from Hestia-Homes/feat/pashub-water-heating-1565
PAS Hub mapper: water-heating code/fuel for cylinder dwellings (#1565)
This commit is contained in:
commit
84f432a44c
2 changed files with 120 additions and 2 deletions
|
|
@ -6066,6 +6066,20 @@ def _map_sap_heating(
|
|||
)
|
||||
)
|
||||
|
||||
# Water heating is only typed for cylinder dwellings; a combi leaves the WHC
|
||||
# None so HW inherits the main system (issue #1565).
|
||||
cylinder_present = heating.water_heating.cylinder_size != "No Cylinder"
|
||||
water_heating_code = (
|
||||
_pashub_water_heating_code(heating.water_heating.system)
|
||||
if cylinder_present
|
||||
else None
|
||||
)
|
||||
water_heating_fuel = (
|
||||
_PASHUB_STANDARD_ELECTRICITY_FUEL
|
||||
if water_heating_code == _PASHUB_WHC_ELECTRIC_IMMERSION
|
||||
else None
|
||||
)
|
||||
|
||||
return SapHeating(
|
||||
instantaneous_wwhrs=InstantaneousWwhrs(),
|
||||
main_heating_details=[
|
||||
|
|
@ -6087,12 +6101,16 @@ def _map_sap_heating(
|
|||
shower_outlets=shower_outlets,
|
||||
cylinder_size=(
|
||||
heating.water_heating.cylinder_size
|
||||
if heating.water_heating.cylinder_size != "No Cylinder"
|
||||
if cylinder_present
|
||||
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,
|
||||
water_heating_code=water_heating_code,
|
||||
water_heating_fuel=water_heating_fuel,
|
||||
immersion_heating_type=_pashub_immersion_type_code(
|
||||
heating.water_heating.immersion_type, cylinder_present
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -7276,6 +7294,48 @@ def _pashub_glazing_type_int(label: Optional[str]) -> Union[int, str]:
|
|||
return _PASHUB_GLAZING_TYPE_TO_SAP10[key]
|
||||
|
||||
|
||||
# PasHub water-heating `system` label → SAP10 water-heating code (WHC) that the
|
||||
# calculator's water cascade keys off (cert_to_inputs: `_WHC_FROM_MAIN_HEATING`
|
||||
# 901 inherits the main fuel; `_WHC_ELECTRIC_IMMERSION` 903; heat-network water
|
||||
# 950). Only lodged for cylinder dwellings — combi dwellings leave it None and HW
|
||||
# inherits from the main system (issue #1565).
|
||||
_PASHUB_WATER_HEATING_SYSTEM_TO_SAP10: Dict[str, int] = {
|
||||
"From main heating 1": 901,
|
||||
"Electric immersion": 903,
|
||||
"Hot water only community scheme - boilers": 950,
|
||||
}
|
||||
_PASHUB_WHC_ELECTRIC_IMMERSION: Final[int] = 903
|
||||
_PASHUB_STANDARD_ELECTRICITY_FUEL: Final[int] = 30 # Table 32 standard electricity
|
||||
|
||||
|
||||
def _pashub_water_heating_code(system_label: Optional[str]) -> Optional[int]:
|
||||
"""Resolve a PasHub water-heating `system` label to its SAP10 WHC code. Blank
|
||||
is "no lodging" (None); an unmapped label strict-raises `UnmappedPasHubLabel`.
|
||||
Call only for cylinder dwellings (#1565)."""
|
||||
if system_label is None or not system_label.strip():
|
||||
return None
|
||||
key = system_label.strip()
|
||||
if key not in _PASHUB_WATER_HEATING_SYSTEM_TO_SAP10:
|
||||
raise UnmappedPasHubLabel("water heating system", system_label)
|
||||
return _PASHUB_WATER_HEATING_SYSTEM_TO_SAP10[key]
|
||||
|
||||
|
||||
def _pashub_immersion_type_code(
|
||||
immersion_label: Optional[str], cylinder_present: bool
|
||||
) -> Optional[int]:
|
||||
"""Resolve a PasHub `immersion_type` label to the SAP10 `immersion_heating_type`
|
||||
code, reusing the Elmhurst map (1 = dual, 2 = single, RdSAP 10 §10.5). None
|
||||
when no cylinder or no immersion lodged; unmapped label strict-raises."""
|
||||
if not cylinder_present or immersion_label is None:
|
||||
return None
|
||||
key = immersion_label.strip()
|
||||
if key in ("", "None"):
|
||||
return None
|
||||
if key not in _ELMHURST_IMMERSION_TYPE_LABEL_TO_SAP10:
|
||||
raise UnmappedPasHubLabel("immersion type", immersion_label)
|
||||
return _ELMHURST_IMMERSION_TYPE_LABEL_TO_SAP10[key]
|
||||
|
||||
|
||||
def _resolve_elmhurst_underfloor_subtype(
|
||||
main_floor: ElmhurstFloorDetails,
|
||||
main_age_band: str,
|
||||
|
|
|
|||
|
|
@ -223,6 +223,63 @@ class TestGlazingTypeCoding:
|
|||
EpcPropertyDataMapper.from_site_notes(survey)
|
||||
|
||||
|
||||
class TestWaterHeatingCoding:
|
||||
"""`from_site_notes` must int-code the PAS Hub water-heating fields for
|
||||
**cylinder** dwellings — `water_heating_code` (WHC), `water_heating_fuel`,
|
||||
`immersion_heating_type` — not leave them None/raw. Combi dwellings (no
|
||||
cylinder) keep the None default (HW inherits from the main system), so they
|
||||
are untouched (issue #1565). example1 is a cylinder dwelling.
|
||||
"""
|
||||
|
||||
def _survey(self, **water: object) -> PasHubRdSapSiteNotes:
|
||||
data = load("pashub_rdsap_site_notes_example1.json")
|
||||
data["heating_and_hot_water"]["water_heating"].update(water)
|
||||
return from_dict(PasHubRdSapSiteNotes, data)
|
||||
|
||||
def test_cylinder_from_main_maps_whc_901(self) -> None:
|
||||
result = EpcPropertyDataMapper.from_site_notes(
|
||||
self._survey(system="From main heating 1", cylinder_size="Normal (90-130 litres)")
|
||||
)
|
||||
assert result.sap_heating.water_heating_code == 901
|
||||
assert result.sap_heating.water_heating_fuel is None
|
||||
|
||||
def test_electric_immersion_maps_903_electric_fuel_and_single_immersion(self) -> None:
|
||||
result = EpcPropertyDataMapper.from_site_notes(
|
||||
self._survey(
|
||||
system="Electric immersion",
|
||||
cylinder_size="Normal (90-130 litres)",
|
||||
immersion_type="Single",
|
||||
)
|
||||
)
|
||||
assert result.sap_heating.water_heating_code == 903
|
||||
assert result.sap_heating.water_heating_fuel == 30 # standard electricity
|
||||
assert result.sap_heating.immersion_heating_type == 2 # single
|
||||
|
||||
def test_community_scheme_maps_950(self) -> None:
|
||||
result = EpcPropertyDataMapper.from_site_notes(
|
||||
self._survey(
|
||||
system="Hot water only community scheme - boilers",
|
||||
cylinder_size="Normal (90-130 litres)",
|
||||
)
|
||||
)
|
||||
assert result.sap_heating.water_heating_code == 950
|
||||
|
||||
def test_combi_leaves_whc_none(self) -> None:
|
||||
result = EpcPropertyDataMapper.from_site_notes(
|
||||
self._survey(system="From main heating 1", cylinder_size="No Cylinder")
|
||||
)
|
||||
assert result.sap_heating.water_heating_code is None
|
||||
|
||||
def test_unknown_cylinder_system_strict_raises(self) -> None:
|
||||
with pytest.raises(UnmappedPasHubLabel):
|
||||
EpcPropertyDataMapper.from_site_notes(
|
||||
self._survey(
|
||||
system="Perpetual motion boiler",
|
||||
cylinder_size="Normal (90-130 litres)",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class TestFromSiteNotesExample1:
|
||||
"""
|
||||
Fixture: pashub_rdsap_site_notes_example1.json
|
||||
|
|
@ -581,6 +638,7 @@ class TestFromSiteNotesExample1:
|
|||
cylinder_size="Normal (90-130 litres)",
|
||||
cylinder_insulation_type="Factory fitted",
|
||||
cylinder_insulation_thickness_mm=12,
|
||||
water_heating_code=901, # cylinder + "From main heating 1" → WHC 901
|
||||
shower_outlets=ShowerOutlets(
|
||||
shower_outlet=ShowerOutlet(
|
||||
shower_outlet_type="Non-Electric Shower"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue