diff --git a/domain/epc/property_overlays/main_heating_system_overlay.py b/domain/epc/property_overlays/main_heating_system_overlay.py index f02839105..0ca83c206 100644 --- a/domain/epc/property_overlays/main_heating_system_overlay.py +++ b/domain/epc/property_overlays/main_heating_system_overlay.py @@ -92,6 +92,12 @@ _ELECTRICITY_FUEL = 29 _HOUSE_COAL_FUEL = 33 _ELECTRIC_ROOM_HEATER_CODES = frozenset(range(691, 702)) +# Heat pumps (SAP Table 4a 211-224 wet, 521-527 warm-air) are category 4 and +# unambiguously electric (natural fuel 29). Modellable on the default code's SPF +# without a PCDB index (ADR-0041). +_HEAT_PUMP_CATEGORY = 4 +_HEAT_PUMP_CODES = frozenset(range(211, 225)) | frozenset(range(521, 528)) + # SAP Table 4c full boiler-control code: programmer + room thermostat + TRVs. The # landlord names the boiler, not its controls — but a gas boiler installed under # modern Building Regs must carry compliant controls, and this overlay already @@ -169,6 +175,8 @@ def _category_for(code: int) -> Optional[int]: as a room-heater system, not whatever category the replaced system carried.""" if code in _SOLID_FUEL_ROOM_HEATER_CODES: return _ROOM_HEATER_CATEGORY + if code in _HEAT_PUMP_CODES: + return _HEAT_PUMP_CATEGORY return None @@ -176,7 +184,7 @@ def _natural_fuel_for(code: int) -> Optional[int]: """The fuel an archetype unambiguously implies (a coherent default), or None where the fuel is ambiguous and must come from the `main_fuel` override. A present `main_fuel` override wins by last-wins composition.""" - if code in _ELECTRIC_ROOM_HEATER_CODES: + if code in _ELECTRIC_ROOM_HEATER_CODES or code in _HEAT_PUMP_CODES: return _ELECTRICITY_FUEL if code in _SOLID_FUEL_ROOM_HEATER_CODES: return _HOUSE_COAL_FUEL diff --git a/tests/domain/epc/test_main_heating_system_overlay.py b/tests/domain/epc/test_main_heating_system_overlay.py index 0658bbf65..458311723 100644 --- a/tests/domain/epc/test_main_heating_system_overlay.py +++ b/tests/domain/epc/test_main_heating_system_overlay.py @@ -472,10 +472,11 @@ def test_air_source_heat_pump_decodes_to_the_default_ashp_code() -> None: def test_air_source_heat_pump_drags_its_coherent_companions() -> None: # A heat pump is unambiguously electric (natural fuel 29) and SAP Table 4a - # category 4, billed on an off-peak (Dual) meter (§12 Rule 3 lists heat pumps - # 211-224). The overlay drags these code-derived companions so a system-only - # override is self-coherent on a cert that lodged a different system - # (ADR-0035 / ADR-0041). + # category 4. Unlike storage/CPSU it does NOT imply an off-peak tariff (SAP + # §12 Rule 3 is conditional, not forcing), so the coherent default meter is + # single-rate — forcing Dual would assume an off-peak split it may not have + # and mis-bill it. The overlay drags these code-derived companions so a + # system-only override is self-coherent (ADR-0035 / ADR-0041). # Act simulation = main_heating_overlay_for("Air source heat pump", 0) @@ -486,4 +487,4 @@ def test_air_source_heat_pump_drags_its_coherent_companions() -> None: heating = simulation.heating assert heating.main_heating_category == 4 assert heating.main_fuel_type == 29 - assert heating.meter_type == "Dual" + assert heating.meter_type == "Single"