From e27ecf4a2656683d9a68d6596c16d38e7993f3eb Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Thu, 2 Jul 2026 11:45:49 +0000 Subject: [PATCH] Set electric-storage category (7) on storage property overlays MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `_category_for` mapped room heaters / heat pumps / heat networks but returned None for electric-storage-heater codes (401-409), so a storage system override emitted `sap_main_heating_code=409` + `main_heating_category=None` while `_control_for` right beside it correctly set the storage charge control — an asymmetry. Map storage codes to category 7 so the overlaid cert is internally consistent (code 401-409 <-> category 7), matching the control/fuel/meter companions the overlay drags. Consistency follow-up to the modelling HHRSH off-peak-pricing fix (#1426): that PR's Table 12a resolver already prices any storage-coded system off-peak regardless of category, so this is NOT a live mispricing — it removes a latent code<->category inconsistency that other category-keyed logic (responsiveness, pumps/fans) could trip on. Tests: storage/HHRSH overlay tests now also assert `main_heating_category==7`. tests/domain/epc (347) + prediction/override/reclassify (118) green; source pyright-clean. Co-Authored-By: Claude Opus 4.8 --- .../main_heating_system_overlay.py | 18 ++++++++++++++++-- .../epc/test_main_heating_system_overlay.py | 6 ++++++ 2 files changed, 22 insertions(+), 2 deletions(-) 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: