From 0dd164567ccd826bdd14ca99e1ab6b1e4a5d83e4 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Fri, 24 Jul 2026 09:15:53 +0000 Subject: [PATCH] =?UTF-8?q?Map=20oil=20and=20solid-fuel=20boilers=20to=20t?= =?UTF-8?q?heir=20SAP=20codes=20and=20companion=20set=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) --- .../main_heating_system_overlay.py | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/domain/epc/property_overlays/main_heating_system_overlay.py b/domain/epc/property_overlays/main_heating_system_overlay.py index 946c648ca..651177065 100644 --- a/domain/epc/property_overlays/main_heating_system_overlay.py +++ b/domain/epc/property_overlays/main_heating_system_overlay.py @@ -217,6 +217,22 @@ _GAS_BOILER_CODES = frozenset({102, 104, 120}) _COMBI_CODES = frozenset({104}) _GAS_BOILER_CATEGORY = 2 _MAINS_GAS_FUEL = 26 + +# Non-gas wet boilers modelled on the gas-boiler pattern (ADR-0067). Oil boilers +# take the modern/condensing SAP Table 4b codes (A-G deferred, like gas): 127 +# regular condensing (84%), 130 combi condensing (82%) — 130 is the combi (no +# cylinder). A solid-fuel boiler is a single conservative member — SAP Table 4a +# 151 (manual-feed independent boiler, 0.60), the lowest-efficiency independent +# solid boiler, so it never over-credits an unknown coal/wood/dual fuel; the +# property's `main_fuel` override drives carbon (it does not change the code). +_OIL_BOILER_CODES = frozenset({127, 130}) +_OIL_BOILER_COMBI_CODES = frozenset({130}) +_SOLID_FUEL_BOILER_CODES = frozenset({151}) +_FUEL_BOILER_CODES = _OIL_BOILER_CODES | _SOLID_FUEL_BOILER_CODES +_FUEL_BOILER_CATEGORY = 2 +# Fan flue: a modern condensing oil boiler is room-sealed / fan-assisted (like a +# gas boiler); a solid-fuel boiler vents through a conventional (non-fanned) flue. +_FAN_FLUE_BOILER_CODES = _OIL_BOILER_CODES # SAP Table 4a "from the main system" water-heating code — a gas boiler heats hot # water from itself, so the override routes water heating to the main system on # mains gas (clearing a storage dwelling's old electric-immersion arrangement). @@ -288,6 +304,13 @@ _MAIN_HEATING_CODES: dict[str, int] = { "Gas room heater, flush live-effect": 605, "Gas room heater, open flue 1980 or later": 603, "Gas room heater, open flue pre-1980": 601, + # Oil / solid-fuel wet boilers off the fuel-agnostic gas default (ADR-0067): + # oil regular 127 / oil combi 130 (Table 4b condensing), solid 151 (Table 4a + # manual-feed independent). Companions (fuel, category, control, cylinder, fan + # flue) drag from `_fuel_boiler_overlay`, modelled on the gas-boiler path. + "Oil boiler, regular": 127, + "Oil boiler, combi": 130, + "Solid fuel boiler": 151, } @@ -368,6 +391,12 @@ def _natural_fuel_for(code: int) -> Optional[int]: or code in _ELECTRIC_UNDERFLOOR_CODES ): return _ELECTRICITY_FUEL + if code in _OIL_BOILER_CODES: + return _OIL_FUEL + if code in _SOLID_FUEL_BOILER_CODES: + # Solid fuel is ambiguous (coal / wood / dual); house coal is the + # conservative default, overridden by the property's `main_fuel` (ADR-0067). + return _HOUSE_COAL_FUEL if code in _SOLID_FUEL_ROOM_HEATER_CODES: return _HOUSE_COAL_FUEL if code in _OIL_ROOM_HEATER_CODES: @@ -398,6 +427,32 @@ def _gas_boiler_overlay(code: int) -> HeatingOverlay: ) +def _fuel_boiler_overlay(code: int) -> HeatingOverlay: + """The coherent companion set for a non-gas wet boiler (oil / solid fuel), + modelled on the gas-boiler pattern (ADR-0067): SAP Table 4a category 2, full + modern controls (Table 4e 2106), a single-rate meter, hot water from the main + system, and a cylinder unless it is a combi. + + Differs from the gas boiler deliberately: the natural fuel is oil / house coal + (the property's `main_fuel` override wins by last-wins composition and drives + carbon); `gas_connection_available` is left UNSET (None) so it inherits the + lodged EPC — an oil/solid boiler needs no gas main, unlike a gas boiler which + forces it True; and a solid-fuel boiler vents through a conventional + (non-fanned) flue.""" + fuel = _natural_fuel_for(code) + return HeatingOverlay( + sap_main_heating_code=code, + main_heating_category=_FUEL_BOILER_CATEGORY, + main_fuel_type=fuel, + main_heating_control=_FULL_BOILER_CONTROL, + fan_flue_present=code in _FAN_FLUE_BOILER_CODES, + meter_type=_SINGLE_RATE_METER, + water_heating_code=_FROM_MAIN_WATER_HEATING_CODE, + water_heating_fuel=fuel, + has_hot_water_cylinder=code not in _OIL_BOILER_COMBI_CODES, + ) + + def natural_fuel_for(main_heating_value: str) -> Optional[int]: """The RdSAP `main_fuel` code a heating archetype unambiguously implies — its natural fuel (ADR-0041) — or None for an unmapped archetype. Exposed so a @@ -416,6 +471,8 @@ def main_heating_overlay_for( return None if code in _GAS_BOILER_CODES: return EpcSimulation(heating=_gas_boiler_overlay(code)) + if code in _FUEL_BOILER_CODES: + return EpcSimulation(heating=_fuel_boiler_overlay(code)) category = _category_for(code) control = _control_for(code) if category is None or control is None: