Map oil and solid-fuel boilers to their SAP codes and companion set 🟩

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Khalim Conn-Kowlessar 2026-07-24 09:15:53 +00:00
parent 04774a8888
commit 0dd164567c

View file

@ -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: