From e02e6a76e1a94f61a796bf18af64031b8270f1b6 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 29 Jun 2026 20:32:31 +0000 Subject: [PATCH] =?UTF-8?q?Preserve=20an=20existing=20more-off-peak=20cert?= =?UTF-8?q?=20meter=20under=20a=20heating=20overlay=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../main_heating_system_overlay.py | 5 +++ .../modelling/scoring/overlay_applicator.py | 37 ++++++++++++++++++- domain/modelling/simulation.py | 7 ++++ 3 files changed, 47 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 ded78999..e763fea7 100644 --- a/domain/epc/property_overlays/main_heating_system_overlay.py +++ b/domain/epc/property_overlays/main_heating_system_overlay.py @@ -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, ) ) diff --git a/domain/modelling/scoring/overlay_applicator.py b/domain/modelling/scoring/overlay_applicator.py index 93875bbf..36fedb83 100644 --- a/domain/modelling/scoring/overlay_applicator.py +++ b/domain/modelling/scoring/overlay_applicator.py @@ -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 diff --git a/domain/modelling/simulation.py b/domain/modelling/simulation.py index 457fa094..401aaf27 100644 --- a/domain/modelling/simulation.py +++ b/domain/modelling/simulation.py @@ -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)