diff --git a/domain/epc/property_overlays/main_heating_system_overlay.py b/domain/epc/property_overlays/main_heating_system_overlay.py index 904a774bf..466466594 100644 --- a/domain/epc/property_overlays/main_heating_system_overlay.py +++ b/domain/epc/property_overlays/main_heating_system_overlay.py @@ -71,6 +71,13 @@ _ASSUMED_DUAL_METER_CODES = OFF_PEAK_IMPLYING_HEATING_CODES | _ROOM_HEATER_CODES # systems that take a charge control. _MANUAL_CHARGE_CONTROL = 2401 _STORAGE_HEATER_CODES = frozenset(range(401, 410)) +# SAP Table 4a "electric storage heaters" category. Set on a storage-system +# override so the overlaid cert is internally consistent (code 401-409 ↔ category +# 7) — mirroring the control/fuel/meter companions the overlay already drags. A +# storage code with a stale non-storage category is a latent trap: the SAP 10.2 +# Table 12a resolver classifies by category, so a leftover room-heater category +# 10 would price the off-peak storage heating at the peak rate. +_STORAGE_HEATER_CATEGORY = 7 # High heat retention storage heaters (SAP Table 4a 409) take a SINGLE intrinsic # charge control — Table 4e 2404 (HHR, 0 C adjustment). It is named by the # archetype, not unobserved, so 409 drags 2404 rather than the manual default @@ -266,10 +273,17 @@ def _control_for(code: int) -> Optional[int]: def _category_for(code: int) -> Optional[int]: """The SAP Table 4a heating category a code implies, where the archetype - fixes it. Room heaters are category 10 — set so a system-only override reads - as a room-heater system, not whatever category the replaced system carried.""" + fixes it. Set so a system-only override reads as the new system's category, + not whatever category the replaced system carried — room heaters are 10, + electric storage heaters (incl. HHR 409) are 7, heat pumps 4, heat networks + 6. Setting storage → 7 keeps the overlaid cert consistent (code 401-409 ↔ + category 7); without it a storage override left a stale category behind, + which the SAP 10.2 Table 12a resolver (keyed on category) could misread as a + non-storage system and price the off-peak storage heating at the peak rate.""" if code in _CATEGORY_10_ROOM_HEATER_CODES: return _ROOM_HEATER_CATEGORY + if code in _STORAGE_HEATER_CODES: + return _STORAGE_HEATER_CATEGORY if code in _HEAT_PUMP_CODES: return _HEAT_PUMP_CATEGORY if code in _HEAT_NETWORK_CODES: diff --git a/tests/domain/epc/test_main_heating_system_overlay.py b/tests/domain/epc/test_main_heating_system_overlay.py index 9f83c9b3f..dae992cba 100644 --- a/tests/domain/epc/test_main_heating_system_overlay.py +++ b/tests/domain/epc/test_main_heating_system_overlay.py @@ -229,6 +229,9 @@ def test_high_heat_retention_storage_drags_its_intrinsic_companions() -> None: assert simulation.heating.main_heating_control == 2404 assert simulation.heating.main_fuel_type == 29 assert simulation.heating.meter_type == "Dual" + # Storage category (7) — consistent with storage code 409, so the SAP 10.2 + # Table 12a resolver prices the off-peak storage heating off-peak, not peak. + assert simulation.heating.main_heating_category == 7 @pytest.mark.parametrize( @@ -288,6 +291,9 @@ def test_storage_heaters_drag_along_conservative_manual_charge_control( assert simulation is not None assert simulation.heating is not None assert simulation.heating.main_heating_control == 2401 + # ...and the storage category (7), consistent with the storage code, so the + # SAP 10.2 Table 12a resolver prices these off-peak, not at the peak rate. + assert simulation.heating.main_heating_category == 7 def test_direct_acting_electric_does_not_drag_along_a_control() -> None: