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)