diff --git a/domain/epc/property_overlays/main_heating_system_overlay.py b/domain/epc/property_overlays/main_heating_system_overlay.py index c1c9f2cc..ded78999 100644 --- a/domain/epc/property_overlays/main_heating_system_overlay.py +++ b/domain/epc/property_overlays/main_heating_system_overlay.py @@ -12,10 +12,11 @@ field-wise with the main_fuel / water_heating overlays. electricity tariff (meter) and, for storage heaters, its charge control. Rather than hand-attach those per archetype (easy to forget when a new system is added), they are **derived from the SAP code**: the off-peak meter from the -calculator's single off-peak classification (`OFF_PEAK_IMPLYING_HEATING_CODES`, -SAP §12), and the conservative manual charge control for storage heaters. So -adding a heating archetype is just adding its code — coherent companions fall -out. Synthesis owns coherence; the calculator never normalises a lodged cert. +overlay's assumed-Dual classification (`_ASSUMED_DUAL_METER_CODES` — the §12 +off-peak systems plus all-electric room-heater dwellings), and the conservative +manual charge control for storage heaters. So adding a heating archetype is just +adding its code — coherent companions fall out. Synthesis owns coherence; the +calculator never normalises a lodged cert. The SEDBUK A-G efficiency band the Hyde "Heating" column carries is NOT honoured yet (no efficiency slot on the overlay/MainHeatingDetail) -- archetypes map to @@ -45,6 +46,19 @@ _OFF_PEAK_METER = "Dual" # split (the mirror of the storage→Dual drag, ADR-0035). _SINGLE_RATE_METER = "Single" +# Electric room heaters (SAP Table 4a 691). They don't *require* off-peak the way +# storage/CPSU do, so they're absent from the calculator's §12 +# `OFF_PEAK_IMPLYING_HEATING_CODES` (Rules 1-2). But a dwelling heated by them is +# all-electric and realistically billed on Economy 7 — its immersion hot water +# charges overnight and §12 Rule 3 gives the room heaters a 10-hour off-peak +# window. So when the landlord names only the system, the coherent meter to +# assume is Dual; the §12 dispatch then applies the realistic high/low split +# (not a single-rate over-penalty, nor an all-low over-credit). +_ROOM_HEATER_CODES = frozenset({691}) +# Codes for which the overlay assumes a Dual (off-peak) meter: the §12-mandated +# off-peak systems plus the all-electric room-heater dwellings above. +_ASSUMED_DUAL_METER_CODES = OFF_PEAK_IMPLYING_HEATING_CODES | _ROOM_HEATER_CODES + # SAP Table 4e Group 4 storage charge-control code. Manual charge control is the # *conservative* assumption when the landlord didn't tell us the control: its # +0.7 C mean-internal-temperature adjustment is the largest of the storage @@ -100,9 +114,10 @@ _MAIN_HEATING_CODES: dict[str, int] = { def _meter_for(code: int) -> str: """The coherent meter a heating code implies: an off-peak ("Dual") meter for - the calculator's §12 off-peak systems, an explicit single-rate ("Single") - meter for every other system. Always set — never left to bleed.""" - return _OFF_PEAK_METER if code in OFF_PEAK_IMPLYING_HEATING_CODES else _SINGLE_RATE_METER + the §12 off-peak systems and all-electric room-heater dwellings + (`_ASSUMED_DUAL_METER_CODES`), an explicit single-rate ("Single") meter for + every other system. Always set — never left to bleed.""" + return _OFF_PEAK_METER if code in _ASSUMED_DUAL_METER_CODES else _SINGLE_RATE_METER def _control_for(code: int) -> Optional[int]: diff --git a/tests/domain/epc/test_main_heating_system_overlay.py b/tests/domain/epc/test_main_heating_system_overlay.py index c85f1f38..7abccefd 100644 --- a/tests/domain/epc/test_main_heating_system_overlay.py +++ b/tests/domain/epc/test_main_heating_system_overlay.py @@ -11,12 +11,10 @@ import pytest from domain.epc.property_overrides.main_heating_system_type import MainHeatingSystemType from domain.epc.property_overlays.main_fuel_overlay import fuel_overlay_for from domain.epc.property_overlays.main_heating_system_overlay import ( + _ASSUMED_DUAL_METER_CODES, _MAIN_HEATING_CODES, main_heating_overlay_for, ) -from domain.sap10_calculator.tables.table_12a import ( - OFF_PEAK_IMPLYING_HEATING_CODES, -) from domain.epc.property_overlays.water_heating_overlay import ( water_heating_overlay_for, ) @@ -241,7 +239,7 @@ def test_off_peak_archetypes_drag_dual_others_drag_single() -> None: for value, code in _MAIN_HEATING_CODES.items(): simulation = main_heating_overlay_for(value, 0) assert simulation is not None and simulation.heating is not None - expected = "Dual" if code in OFF_PEAK_IMPLYING_HEATING_CODES else "Single" + expected = "Dual" if code in _ASSUMED_DUAL_METER_CODES else "Single" assert simulation.heating.meter_type == expected, value