Preserve an existing more-off-peak cert meter under a heating overlay 🟩

A landlord heating override DESCRIBES the dwelling, so its assumed off-peak meter
(Dual/E7) is a coherent default — it must not downgrade a cert that already
lodges a more-off-peak meter (e.g. property 709874's 24-hour all-low tariff). The
overlay opts in via keep_existing_off_peak_meter; _fold_heating then keeps the
cert's meter when both it and the desired meter are off-peak. Heating MEASURES
build HeatingOverlay directly and leave the flag False, so they still re-meter
for real (Elmhurst re-lodges 18-hour -> Dual on a storage install) — the cascade
pins stay green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-06-29 20:32:31 +00:00
parent e3c023e0f6
commit e02e6a76e1
3 changed files with 47 additions and 2 deletions

View file

@ -164,5 +164,10 @@ def main_heating_overlay_for(
sap_main_heating_code=code,
meter_type=_meter_for(code),
main_heating_control=_control_for(code),
# A landlord override describes the existing dwelling, so its assumed
# off-peak meter must not downgrade a more-off-peak cert meter
# (e.g. a 24-hour all-low tariff). Measures, which re-meter for real,
# build their HeatingOverlay directly and leave this False.
keep_existing_off_peak_meter=True,
)
)

View file

@ -152,6 +152,22 @@ _SAP_HEATING_FIELDS: tuple[str, ...] = (
_ENERGY_SOURCE_FIELDS: tuple[str, ...] = ("meter_type", "mains_gas")
def _is_off_peak_meter(meter_type: object) -> bool:
"""True iff the meter resolves to an off-peak Table 12a tariff (not the
STANDARD single-rate column). Unparseable / absent meters count as not
off-peak so a coherent override meter still applies to them."""
from domain.sap10_calculator.exceptions import UnmappedSapCode
from domain.sap10_calculator.tables.table_12a import (
Tariff,
tariff_from_meter_type,
)
try:
return tariff_from_meter_type(meter_type) is not Tariff.STANDARD
except UnmappedSapCode:
return False
def _fold_heating(epc: EpcPropertyData, overlay: HeatingOverlay) -> None:
"""Write a `HeatingOverlay`'s non-``None`` fields onto the (copied) dwelling,
routing each to its home: the primary ``main_heating_details[0]``, the
@ -181,8 +197,25 @@ def _fold_heating(epc: EpcPropertyData, overlay: HeatingOverlay) -> None:
epc.has_hot_water_cylinder = overlay.has_hot_water_cylinder
for field_name in _ENERGY_SOURCE_FIELDS:
value = getattr(overlay, field_name)
if value is not None:
setattr(epc.sap_energy_source, field_name, value)
if value is None:
continue
# A landlord heating override's assumed meter (Dual for off-peak
# systems) is a coherent default, not a re-metering of the cert: when it
# opts in (`keep_existing_off_peak_meter`), don't downgrade a cert that
# already lodges a MORE off-peak meter (e.g. a 24-hour all-low tariff) to
# the overlay's 7-hour E7 — keep the cert's (more specific) one. Single/
# unknown existing meters still receive the off-peak meter, and a switch
# to single-rate still resets it (its desired value isn't off-peak).
# Heating MEASURES leave the flag False — they re-meter for real
# (Elmhurst re-lodges 18-hour → Dual on a storage install).
if (
field_name == "meter_type"
and overlay.keep_existing_off_peak_meter
and _is_off_peak_meter(value)
and _is_off_peak_meter(epc.sap_energy_source.meter_type)
):
continue
setattr(epc.sap_energy_source, field_name, value)
# `SolarOverlay` fields all live on `sap_energy_source` (the home of the SAP

View file

@ -193,6 +193,13 @@ class HeatingOverlay:
# sap_energy_source
meter_type: Optional[str] = None
mains_gas: Optional[bool] = None
# A landlord heating-system override DESCRIBES the existing dwelling, so its
# assumed off-peak (`meter_type`) is a coherent default, not a re-metering:
# it must not downgrade a cert that already lodges a MORE off-peak meter
# (e.g. a 24-hour all-low tariff → the overlay's 7-hour E7). A heating
# MEASURE re-meters for real (Elmhurst re-lodges 18-hour → Dual on a storage
# install), so it leaves this False. `_fold_heating` reads it.
keep_existing_off_peak_meter: bool = False
@dataclass(frozen=True)