Assume a dual Economy-7 meter for all-electric room-heater dwellings 🟩

Electric room heaters (691) get a Dual meter from the overlay (not single-rate):
an all-electric room-heater dwelling realistically bills on Economy 7, and the
§12 dispatch then applies a high/low split rather than a single-rate over-penalty.
Overlay owns its assumed-meter policy via _ASSUMED_DUAL_METER_CODES (the §12
off-peak systems + room heaters), keeping OFF_PEAK_IMPLYING_HEATING_CODES pure.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-29 16:18:13 +00:00
parent 13f1981776
commit 4e96ea91a5
2 changed files with 24 additions and 11 deletions

View file

@ -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]:

View file

@ -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