diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index 31be5a966..9458e337e 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -6491,6 +6491,11 @@ def _map_sap_heating( has_fghrs=main.flue_gas_heat_recovery_system, main_fuel_type=main_fuel, sap_main_heating_code=sap_main_heating_code, + main_heating_category=( + _PASHUB_HEATING_CATEGORY_HEAT_PUMP + if main.system_type.lower() in _PASHUB_HEAT_PUMP_SYSTEM_TYPES + else None + ), heat_emitter_type=_pashub_heat_emitter_code(main.emitter), emitter_temperature=main.emitter_temperature, fan_flue_present=main.fan_assist, @@ -7941,6 +7946,29 @@ _PASHUB_HEAT_NETWORK_CONTROL_TO_SAP10: Dict[str, int] = { } +# PasHub `system_type` labels that are SAP 10.2 Table 4e GROUP 2: HEAT PUMPS +# WITH RADIATORS OR UNDERFLOOR HEATING (spec PDF p.172). Their control labels +# carry 22xx codes; the system's efficiency comes from the lodged PCDB product +# (`main_heating_index_number`), not a Table 4a rd row, so +# `_pashub_main_heating_system_code` correctly leaves the code None. +_PASHUB_HEAT_PUMP_SYSTEM_TYPES: frozenset[str] = frozenset({ + "heat pump with radiators or underfloor heating", +}) +# Table 4a main-heating category 4 (heat pumps) — the calculator's Appendix +# N3.6/N3.7 efficiency, extended-heating and §4f pumps+fans paths all gate on +# `main_heating_category == 4` (same category the Elmhurst mapper lodges via +# `_ELMHURST_HEATING_CATEGORY_HEAT_PUMP`). Without it a PCDB heat pump is +# billed as 100%-efficient direct electric. +_PASHUB_HEATING_CATEGORY_HEAT_PUMP: Final[int] = 4 + + +# PasHub surveyed heat-pump control labels → their SAP 10.2 Table 4e Group 2 +# code (PCDB `heat_pump_controls`, control type 2, temp adjustment 0.0). +_PASHUB_HEAT_PUMP_CONTROL_TO_SAP10: Dict[str, int] = { + "Programmer, room thermostat and TRVs": 2210, +} + + # PasHub `system_type` labels that are SAP 10.2 Table 4e GROUP 4: ELECTRIC # STORAGE SYSTEMS (spec PDF p.173). A distinct group from boiler/heat-network, # so its control labels carry their own 24xx codes (see the boiler comment on @@ -8013,9 +8041,10 @@ def _pashub_main_heating_control_code( The resolution is system-aware: the same control label recurs across Table 4e's system groups under different 2xxx codes, so the code is chosen by the system's group — Group 1 boiler (`_PASHUB_MAIN_HEATING_CONTROL_TO_SAP10`), - Group 3 heat network (`_PASHUB_HEAT_NETWORK_CONTROL_TO_SAP10`), Group 4 - electric storage (`_PASHUB_STORAGE_HEATER_CONTROL_TO_SAP10`) or Group 6 room - heater (`_PASHUB_ROOM_HEATER_CONTROL_TO_SAP10`). Any other system group, or a + Group 2 heat pump (`_PASHUB_HEAT_PUMP_CONTROL_TO_SAP10`), Group 3 heat + network (`_PASHUB_HEAT_NETWORK_CONTROL_TO_SAP10`), Group 4 electric storage + (`_PASHUB_STORAGE_HEATER_CONTROL_TO_SAP10`) or Group 6 room heater + (`_PASHUB_ROOM_HEATER_CONTROL_TO_SAP10`). Any other system group, or a label the group's lookup does not cover, strict-raises `UnmappedPasHubLabel` (naming the label and system) rather than silently taking a wrong-group code — the gap is fixed here, never mis-rated or strict-raised deep in the @@ -8025,6 +8054,8 @@ def _pashub_main_heating_control_code( system = system_type.lower() if system in _PASHUB_BOILER_SYSTEM_TYPES: group_map = _PASHUB_MAIN_HEATING_CONTROL_TO_SAP10 + elif system in _PASHUB_HEAT_PUMP_SYSTEM_TYPES: + group_map = _PASHUB_HEAT_PUMP_CONTROL_TO_SAP10 elif system in _PASHUB_HEAT_NETWORK_SYSTEM_TYPES: group_map = _PASHUB_HEAT_NETWORK_CONTROL_TO_SAP10 elif system in _PASHUB_STORAGE_HEATER_SYSTEM_TYPES: @@ -8035,8 +8066,9 @@ def _pashub_main_heating_control_code( raise UnmappedPasHubLabel( "main heating control", f"{control_label!r} on unsupported system {system_type!r} " - f"(only Table 4e Group 1 boiler, Group 3 heat-network, Group 4 " - f"electric-storage + Group 6 room-heater codes are mapped)", + f"(only Table 4e Group 1 boiler, Group 2 heat-pump, Group 3 " + f"heat-network, Group 4 electric-storage + Group 6 room-heater " + f"codes are mapped)", ) code = group_map.get(control_label) if code is None: