From 59215c822ecd7b8030d1672e736ceb6dae2a9133 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 14 Jul 2026 16:01:04 +0000 Subject: [PATCH] =?UTF-8?q?Resolve=20PasHub=20main-heating=20control=20lab?= =?UTF-8?q?el=20to=20its=20SAP=20Table=204e=20code=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- datatypes/epc/domain/mapper.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index b3bfdb370..7dda6b10e 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -6031,7 +6031,7 @@ def _map_sap_heating( heat_emitter_type=main.emitter, emitter_temperature=main.emitter_temperature, fan_flue_present=main.fan_assist, - main_heating_control=main.controls, + main_heating_control=_pashub_main_heating_control_code(main.controls), condensing=main.condensing, weather_compensator=main.weather_compensator, central_heating_pump_age_str=main.central_heating_pump_age, @@ -7081,6 +7081,33 @@ def _pashub_main_fuel_code(fuel_label: str) -> Union[int, str]: return code +# PasHub Site-Notes surveyed main-heating "Controls" labels mapped to their +# SAP 10.2 Table 4e (PDF p.171) code, which the calculator's `_control_type` +# keys off. Copying the raw label onto `main_heating_control` strict-raises +# `UnmappedSapCode` deep in the calculator; resolving it here at the boundary +# (ADR-0015) hands the cascade the int it expects. Seeded with the boiler +# (Group 1) control labels confirmed in the live cohort — all standard +# time-and-temperature single-zone controls that resolve to control type 2. +_PASHUB_MAIN_HEATING_CONTROL_TO_SAP10: Dict[str, int] = { + "Programmer, room thermostat and TRVs": 2106, +} + + +def _pashub_main_heating_control_code(control_label: str) -> Union[int, str]: + """Resolve a PasHub surveyed main-heating Controls label to its SAP 10.2 + Table 4e code at the mapper boundary (ADR-0015). A genuinely blank label is + the "no controls lodged" shape and passes through unchanged; a non-empty + label the lookup does not cover strict-raises `UnmappedPasHubLabel` so the + gap is fixed here, never strict-raised deep in the calculator as + `UnmappedSapCode`.""" + if not control_label: + return control_label + code = _PASHUB_MAIN_HEATING_CONTROL_TO_SAP10.get(control_label) + if code is None: + raise UnmappedPasHubLabel("main heating control", control_label) + return code + + def _resolve_elmhurst_underfloor_subtype( main_floor: ElmhurstFloorDetails, main_age_band: str,