mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-22 08:48:38 +00:00
The heat-network mirror of keep_existing_off_peak_meter: the override's 2313 default applies only when the replaced system's control is cross-family (stale); an observed community control survives. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
454 lines
25 KiB
Python
454 lines
25 KiB
Python
"""Map a Landlord-Override main-heating-system value to a heating Simulation Overlay.
|
|
|
|
A main-heating-system value is one canonical system archetype ("Gas boiler,
|
|
combi", "Electric storage heaters, fan"). The calculator reads the primary
|
|
system's `sap_main_heating_code` (SAP Table 4a/4b), so the overlay maps the
|
|
archetype to a representative code and emits a whole-dwelling `HeatingOverlay`
|
|
targeting `main_heating_details[0]` (`building_part` is ignored). It composes
|
|
field-wise with the main_fuel / water_heating overlays.
|
|
|
|
**Coherent Heating System / drag-along (ADR-0035):** a landlord tells us the
|
|
*system*, not the dependent fields a coherent heating system carries — its
|
|
electricity tariff (meter) and, for storage heaters, its charge control. Rather
|
|
than hand-attach those per archetype (easy to forget when a new system is
|
|
added), they are **derived from the SAP code**: the off-peak meter from the
|
|
overlay's assumed-Dual classification (`_ASSUMED_DUAL_METER_CODES` — the §12
|
|
off-peak systems plus all-electric room-heater dwellings), and the conservative
|
|
manual charge control for storage heaters. So adding a heating archetype is just
|
|
adding its code — coherent companions fall out. Synthesis owns coherence; the
|
|
calculator never normalises a lodged cert.
|
|
|
|
The SEDBUK A-G efficiency band the Hyde "Heating" column carries is NOT honoured
|
|
yet (no efficiency slot on the overlay/MainHeatingDetail) -- archetypes map to
|
|
their modern/condensing Table 4b code, so an old low-rated boiler is currently
|
|
modelled at the condensing efficiency. Heat pumps and community heating (which
|
|
resolve via main_heating_index_number / community codes, not a Table 4b code)
|
|
are left UNKNOWN until modelled. Unresolvable values produce no overlay.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Optional
|
|
|
|
from domain.modelling.simulation import EpcSimulation, HeatingOverlay
|
|
from domain.sap10_calculator.tables.table_12a import (
|
|
OFF_PEAK_IMPLYING_HEATING_CODES,
|
|
)
|
|
|
|
# Off-peak (Economy 7) meter. Electric storage / CPSU systems charge overnight at
|
|
# the low rate and cannot run economically on a single-rate meter; "Dual" lets
|
|
# the §12 dispatch resolve the specific tariff (storage 7-hour, CPSU 10-hour).
|
|
_OFF_PEAK_METER = "Dual"
|
|
# Single-rate meter (SAP 10.2 Table 12a code 2 → STANDARD tariff). Every non-off-
|
|
# peak archetype synthesises this *explicitly* rather than leaving the meter
|
|
# untouched: switching OFF storage heaters must not let the dwelling's old "Dual"
|
|
# meter bleed through and bill the new gas/direct-acting system on an Economy-7
|
|
# split (the mirror of the storage→Dual drag, ADR-0035).
|
|
_SINGLE_RATE_METER = "Single"
|
|
# Meter-agnostic codes: the archetype cannot determine the tariff, so the overlay
|
|
# defers `meter_type` to the cert (`_meter_for` → None). Screed underfloor (424) is
|
|
# "standard or off peak" — 81% of the corpus lodge off-peak (ADR-0046).
|
|
_METER_AGNOSTIC_CODES = frozenset({424})
|
|
|
|
# Electric room heaters (SAP Table 4a 691). They don't *require* off-peak the way
|
|
# storage/CPSU do, so they're absent from the calculator's §12
|
|
# `OFF_PEAK_IMPLYING_HEATING_CODES` (Rules 1-2). But a dwelling heated by them is
|
|
# all-electric and realistically billed on Economy 7 — its immersion hot water
|
|
# charges overnight and §12 Rule 3 gives the room heaters a 10-hour off-peak
|
|
# window. So when the landlord names only the system, the coherent meter to
|
|
# assume is Dual; the §12 dispatch then applies the realistic high/low split
|
|
# (not a single-rate over-penalty, nor an all-low over-credit).
|
|
_ROOM_HEATER_CODES = frozenset({691})
|
|
# Codes for which the overlay assumes a Dual (off-peak) meter: the §12-mandated
|
|
# off-peak systems plus the all-electric room-heater dwellings above.
|
|
_ASSUMED_DUAL_METER_CODES = OFF_PEAK_IMPLYING_HEATING_CODES | _ROOM_HEATER_CODES
|
|
|
|
# SAP Table 4e Group 4 storage charge-control code. Manual charge control is the
|
|
# *conservative* assumption when the landlord didn't tell us the control: its
|
|
# +0.7 C mean-internal-temperature adjustment is the largest of the storage
|
|
# controls (automatic / Celect +0.4, HHR 0), so it never over-credits an
|
|
# unobserved control. Scoped to storage *heaters* (Table 4a 401-409) — the only
|
|
# systems that take a charge control.
|
|
_MANUAL_CHARGE_CONTROL = 2401
|
|
_STORAGE_HEATER_CODES = frozenset(range(401, 410))
|
|
# SAP Table 4a "electric storage heaters" category. Set on a storage-system
|
|
# override so the overlaid cert is internally consistent (code 401-409 ↔ category
|
|
# 7) — mirroring the control/fuel/meter companions the overlay already drags. A
|
|
# storage code with a stale non-storage category is a latent trap: the SAP 10.2
|
|
# Table 12a resolver classifies by category, so a leftover room-heater category
|
|
# 10 would price the off-peak storage heating at the peak rate.
|
|
_STORAGE_HEATER_CATEGORY = 7
|
|
# High heat retention storage heaters (SAP Table 4a 409) take a SINGLE intrinsic
|
|
# charge control — Table 4e 2404 (HHR, 0 C adjustment). It is named by the
|
|
# archetype, not unobserved, so 409 drags 2404 rather than the manual default
|
|
# (ADR-0044) — the only storage code that overrides `_MANUAL_CHARGE_CONTROL`.
|
|
_HHR_STORAGE_CODE = 409
|
|
_HHR_CHARGE_CONTROL = 2404
|
|
|
|
# SAP Table 4a category 10 ("Room heaters") and its conservative Table 4e Group 6
|
|
# control. A landlord names a room heater, not its control; code 2601 ("no
|
|
# thermostatic control of room temperature") is the lowest-SAP room-heater
|
|
# control — the modal one real solid-fuel room-heater certs lodge — so it never
|
|
# over-credits an unobserved control (the room-heater mirror of the storage
|
|
# manual-charge default). Scoped per family as archetypes land; solid-fuel room
|
|
# heaters are Table 4a 631-636.
|
|
_ROOM_HEATER_CATEGORY = 10
|
|
_ROOM_HEATER_CONTROL = 2601
|
|
_SOLID_FUEL_ROOM_HEATER_CODES = frozenset(range(631, 637))
|
|
# Oil room heaters (SAP Table 4a 621-625) — category 10, conservative room-heater
|
|
# control, natural fuel heating oil (RdSAP main_fuel 28).
|
|
_OIL_ROOM_HEATER_CODES = frozenset(range(621, 626))
|
|
_OIL_FUEL = 28
|
|
# Gas (incl. LPG) room heaters (SAP Table 4a 601-613) — category 10, conservative
|
|
# control, natural fuel mains gas (26); an LPG dwelling is refined by a main_fuel
|
|
# override (the overlay can't see the mains connection).
|
|
_GAS_ROOM_HEATER_CODES = frozenset(range(601, 614))
|
|
# Fuel-burning room heaters (solid + oil + gas) share category 10 + the
|
|
# conservative room-heater control; only the natural fuel differs by family.
|
|
# (Distinct from `_ROOM_HEATER_CODES` above, the electric-room-heater dual-meter
|
|
# set.)
|
|
_CATEGORY_10_ROOM_HEATER_CODES = (
|
|
_SOLID_FUEL_ROOM_HEATER_CODES | _OIL_ROOM_HEATER_CODES | _GAS_ROOM_HEATER_CODES
|
|
)
|
|
|
|
# Natural-fuel coherence (ADR-0041): where an archetype unambiguously implies a
|
|
# fuel, the overlay drags it so a system-only override is self-coherent on a cert
|
|
# that lodged a different fuel. A later `main_fuel` override still wins (last-wins
|
|
# composition); a contradicting landlord fuel is logged, not silently overridden.
|
|
# Electric room heaters (Table 4a 691-701) are unambiguously electricity (RdSAP
|
|
# main_fuel code 29). Solid fuel is ambiguous (coal / anthracite / smokeless /
|
|
# dual fuel / wood logs / pellets), so it defaults to house coal (33) — the most
|
|
# common solid fuel — when the landlord gives no `main_fuel` override; a specific
|
|
# solid-fuel override still wins by last-wins composition.
|
|
_ELECTRICITY_FUEL = 29
|
|
_HOUSE_COAL_FUEL = 33
|
|
_ELECTRIC_ROOM_HEATER_CODES = frozenset(range(691, 702))
|
|
# Electric boilers — SAP Table 4a 191 (direct-acting) and 192 (electric CPSU) —
|
|
# unambiguously electric, so they drag electricity like the electric room heaters
|
|
# (ADR-0045, closing the prior no-fuel gap on 191/192).
|
|
_ELECTRIC_BOILER_CODES = frozenset({191, 192})
|
|
# SAP Table 4a "boiler system" category — codes 191/192 are wet electric
|
|
# boilers, same category the gas-boiler overlay stamps. Forced so an
|
|
# electric-boiler override never keeps the replaced system's category: on
|
|
# portfolio 796 an inherited 6 (community) or 4 (heat pump) suppressed the
|
|
# HHRSH pathway, and an inherited 7 (storage) made the Table 12a resolver bill
|
|
# direct-acting heat wholly at the off-peak low rate (ADR-0052, #1444).
|
|
_ELECTRIC_BOILER_CATEGORY = 2
|
|
# SAP Table 4e Group 1 boiler control: 2101 ("no time or thermostatic control
|
|
# of room temperature", +0.6 C) is the conservative default when the landlord
|
|
# named only the system — the largest Group 1 mean-internal-temperature
|
|
# adjustment, so an unobserved control is never over-credited (the boiler
|
|
# mirror of storage 2401 / room-heater 2601 / underfloor 2701). Deliberately
|
|
# NOT the gas boilers' modern 2106: the gas archetypes assume a whole modern
|
|
# condensing install; the electric-boiler archetypes describe an existing
|
|
# system of unknown vintage (ADR-0052).
|
|
_CONSERVATIVE_BOILER_CONTROL = 2101
|
|
# Electric underfloor — SAP Table 4a 421/422 (off-peak) and 424 (screed) — also
|
|
# unambiguously electric (ADR-0046).
|
|
_ELECTRIC_UNDERFLOOR_CODES = frozenset({421, 422, 424})
|
|
|
|
# SAP Table 4a "electric underfloor heating" category. Stamped so an underfloor
|
|
# override never leaves the replaced system's category behind — property 711795
|
|
# (PRD #1435 WS2) kept a replaced heat network's category 6, which the heating
|
|
# generator's `is_heat_network_main` gate read as community heating: no heating
|
|
# upgrade was offered at all and the baseline mis-rated 51 vs 67 (ADR-0050).
|
|
_UNDERFLOOR_CATEGORY = 8
|
|
# SAP Table 4e Group 7 underfloor control: 2701 ("no time or thermostatic
|
|
# control of room temperature", +0.3 C) is the conservative default when the
|
|
# landlord named only the system — the largest mean-internal-temperature
|
|
# adjustment in the group, so an unobserved control is never over-credited
|
|
# (the underfloor mirror of storage-manual 2401 and room-heater 2601, ADR-0050).
|
|
_UNDERFLOOR_CONTROL = 2701
|
|
|
|
# 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 4e Group 2 heat-pump control: 2201 ("no time or thermostatic
|
|
# control of room temperature", +0.3 C) is the conservative default when the
|
|
# landlord named only the system — the largest Group 2 adjustment, so an
|
|
# unobserved control is never over-credited. Deliberately NOT the ASHP
|
|
# Measure's 2210 zone control: a measure designs a modern end-state
|
|
# (ADR-0024); an override describes an existing pump (ADR-0053). Scoped to the
|
|
# WET heat pumps (211-224) — warm-air pumps (521-527) take Table 4e Group 5
|
|
# and stay unmapped (no archetype maps to them; the incomplete-companion raise
|
|
# guards that a future one cannot land without choosing its Group 5 control).
|
|
_WET_HEAT_PUMP_CODES = frozenset(range(211, 225))
|
|
_CONSERVATIVE_HEAT_PUMP_CONTROL = 2201
|
|
|
|
# Community / heat-network heating (SAP Table 4a 301-304) is category 6; the
|
|
# calculator's `_is_heat_network` keys off code OR category 6. The boiler-driven
|
|
# schemes (301/302/303) are dominantly mains gas (community) — RdSAP main_fuel 20
|
|
# — per real community certs; the heat-pump scheme (304) is electricity
|
|
# (community), added with its archetype (ADR-0041).
|
|
_HEAT_NETWORK_CATEGORY = 6
|
|
_HEAT_NETWORK_CODES = frozenset({301, 302, 303, 304})
|
|
_COMMUNITY_BOILER_CODES = frozenset({301, 302, 303})
|
|
# SAP Table 4e Group 3 heat-network control: 2313 ("flat rate charging, room
|
|
# thermostat and TRVs") is the default when the landlord named only the scheme
|
|
# — a per-axis choice (ADR-0053, decided with Khalim). KIT axis: community
|
|
# schemes skew modern and the portfolio's lodged community certs are
|
|
# overwhelmingly thermostatted (modal 2306; none lodge 2301), so a room
|
|
# thermostat + TRVs is the evidence-backed assumption (temperature adjustment
|
|
# 0.0). CHARGING axis: flat rate (Table 4c(3) space 1.05 / DHW 1.05) — the
|
|
# billing arrangement is scheme paperwork an override cannot see, so the
|
|
# usage-linked credit (1.0/1.0) is never assumed. An assessor-OBSERVED Group 3
|
|
# control on the cert is kept in preference to this default (the overlay sets
|
|
# `keep_existing_heat_network_control`; the applicator honours it).
|
|
_DEFAULT_HEAT_NETWORK_CONTROL = 2313
|
|
_COMMUNITY_GAS_FUEL = 20
|
|
|
|
# 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
|
|
# assumes the modern condensing efficiency for the boiler (A-G deferred), so
|
|
# assuming modern controls is the *coherent* default (decided with Khalim). It
|
|
# also overwrites any stale storage charge control (2401) the dwelling carried
|
|
# before the switch. (Conservative-vs-modern trade-off documented in ADR-0035.)
|
|
_FULL_BOILER_CONTROL = 2106
|
|
|
|
# Gas-boiler archetypes (Table 4b 102 regular / 104 combi / 120 CPSU) and the
|
|
# subset that heats hot water instantaneously (a combi has no cylinder; a regular
|
|
# boiler and a CPSU heat a cylinder). A gas boiler implies a mains-gas connection
|
|
# + a gas main fuel + a gas-boiler heating category (Table 4a cat 2) + a fanned
|
|
# room-sealed flue — all set so a heating-system-only override is self-coherent.
|
|
_GAS_BOILER_CODES = frozenset({102, 104, 120})
|
|
_COMBI_CODES = frozenset({104})
|
|
_GAS_BOILER_CATEGORY = 2
|
|
_MAINS_GAS_FUEL = 26
|
|
# 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).
|
|
_FROM_MAIN_WATER_HEATING_CODE = 901
|
|
|
|
# Canonical system archetype → representative SAP `sap_main_heating_code`. Codes
|
|
# map to the modern/condensing variant (A-G efficiency deferred): 102 regular
|
|
# condensing, 104 condensing combi, 120 CPSU, 401-404 storage heaters, 191
|
|
# direct-acting electric, 691 panel/convector/radiant electric room heaters
|
|
# (Table 4a — direct-acting, so a single-rate meter, NOT off-peak storage).
|
|
# Companion fields (meter / control / fuel / hot water) are NOT listed here —
|
|
# they are derived from the code below, so a new archetype is just a code
|
|
# (ADR-0035 drag-along).
|
|
_MAIN_HEATING_CODES: dict[str, int] = {
|
|
"Gas boiler, combi": 104,
|
|
"Gas boiler, regular": 102,
|
|
"Gas CPSU": 120,
|
|
"Electric storage heaters, old": 401,
|
|
"Electric storage heaters, slimline": 402,
|
|
"Electric storage heaters, convector": 403,
|
|
"Electric storage heaters, fan": 404,
|
|
# High heat retention storage heaters — SAP Table 4a 409. Its intrinsic HHR
|
|
# charge control (2404) and electricity fuel drag from the code below, not the
|
|
# generic manual-storage default (ADR-0044).
|
|
"Electric storage heaters, high heat retention": 409,
|
|
"Direct-acting electric": 191,
|
|
# A-rated electric boiler off the Gas CPSU dumping ground — SAP Table 4a 191
|
|
# (direct-acting electric boiler). Unambiguously electric, so it drags
|
|
# electricity below (ADR-0045).
|
|
"Electric boiler": 191,
|
|
# Electric CPSU — SAP Table 4a 192. Electric (drags 29), on an off-peak Dual
|
|
# meter (already in _ASSUMED_DUAL_METER_CODES; §12 Rule 1 10-hour) — ADR-0045.
|
|
"Electric CPSU": 192,
|
|
# Electric underfloor — dedicated SAP Table 4a codes, all electric (drag 29).
|
|
# 421 concrete slab / 422 integrated are off-peak (Dual, §12 Rule 2); 424
|
|
# screed is tariff-ambiguous ("standard or off peak") so it DEFERS the meter to
|
|
# the cert (ADR-0046).
|
|
"Electric underfloor, in concrete slab (off-peak)": 421,
|
|
"Electric underfloor, integrated storage and direct-acting": 422,
|
|
"Electric underfloor, in screed above insulation": 424,
|
|
"Electric room heaters": 691,
|
|
"Solid fuel room heater, closed": 633,
|
|
# Solid-fuel room heaters off the Gas CPSU dumping ground (ADR-0045). SAP
|
|
# Table 4a: 631 open fire, 632 open fire + back boiler, 634 closed + boiler.
|
|
# Companions (house-coal fuel 33, room-heater control 2601, category 10) drag
|
|
# from the code — 631/632/634 are already in the 631-636 solid-fuel set.
|
|
"Solid fuel room heater, open fire": 631,
|
|
"Solid fuel room heater, open fire with back boiler": 632,
|
|
"Solid fuel room heater, closed with boiler": 634,
|
|
# Default air-source heat pump — SAP Table 4a 211 ("flow temp in other
|
|
# cases", default SPF 2.30). Modellable without a PCDB index; an actual
|
|
# product index would refine it (ADR-0041).
|
|
"Air source heat pump": 211,
|
|
# Boiler-driven community / heat-network heating — SAP Table 4a 301. The
|
|
# calculator derives the heat-source efficiency (80%) + DLF (1.50 default)
|
|
# from the code (cert_to_inputs). A generic "Community heating" with no named
|
|
# source stays unmapped (-> None) — the source can't be assumed (ADR-0041).
|
|
"Community heating, boilers": 301,
|
|
# Community CHP + boilers — SAP Table 4a 302 (heat network, category 6).
|
|
"Community heating, CHP and boilers": 302,
|
|
# Modern standalone oil room heater — SAP Table 4a 623 ("Oil room heater,
|
|
# 2000 or later", no boiler). Natural fuel heating oil (ADR-0041).
|
|
"Oil room heater, 2000 or later": 623,
|
|
# Gas room heaters — each catalogue variant to its own SAP Table 4a code
|
|
# (wide efficiency spread, so no single representative). Natural fuel mains
|
|
# gas; an LPG dwelling is refined by a `main_fuel` override (ADR-0041).
|
|
"Gas room heater, condensing fire": 611,
|
|
"Gas room heater, decorative fuel-effect": 612,
|
|
"Gas room heater, flush live-effect": 605,
|
|
"Gas room heater, open flue 1980 or later": 603,
|
|
"Gas room heater, open flue pre-1980": 601,
|
|
}
|
|
|
|
|
|
def _meter_for(code: int) -> Optional[str]:
|
|
"""The coherent meter a heating code implies: an off-peak ("Dual") meter for
|
|
the §12 off-peak systems and all-electric room-heater dwellings
|
|
(`_ASSUMED_DUAL_METER_CODES`), an explicit single-rate ("Single") meter for
|
|
every other system — set explicitly so it never bleeds.
|
|
|
|
Exception: a **meter-agnostic** code (`_METER_AGNOSTIC_CODES` — screed
|
|
underfloor 424, "standard or off peak") returns ``None`` so the overlay leaves
|
|
`meter_type` unset and the cert's lodged tariff stands. The archetype genuinely
|
|
cannot know the tariff, and unlike a switched-off storage system it is not a
|
|
re-metering, so deferring is faithful, not a bleed (ADR-0046)."""
|
|
if code in _METER_AGNOSTIC_CODES:
|
|
return None
|
|
return _OFF_PEAK_METER if code in _ASSUMED_DUAL_METER_CODES else _SINGLE_RATE_METER
|
|
|
|
|
|
def _control_for(code: int) -> Optional[int]:
|
|
"""The control to assume when the landlord named only the system: a
|
|
conservative manual charge control for storage heaters, full modern controls
|
|
for a gas boiler, the conservative in-group default for the other mapped
|
|
families (room heaters, underfloor, electric boilers). Overwrites a stale
|
|
control inherited from the system being replaced."""
|
|
if code == _HHR_STORAGE_CODE:
|
|
return _HHR_CHARGE_CONTROL
|
|
if code in _STORAGE_HEATER_CODES:
|
|
return _MANUAL_CHARGE_CONTROL
|
|
if code in _GAS_BOILER_CODES:
|
|
return _FULL_BOILER_CONTROL
|
|
if code in _CATEGORY_10_ROOM_HEATER_CODES or code in _ELECTRIC_ROOM_HEATER_CODES:
|
|
return _ROOM_HEATER_CONTROL
|
|
if code in _ELECTRIC_UNDERFLOOR_CODES:
|
|
return _UNDERFLOOR_CONTROL
|
|
if code in _ELECTRIC_BOILER_CODES:
|
|
return _CONSERVATIVE_BOILER_CONTROL
|
|
if code in _WET_HEAT_PUMP_CODES:
|
|
return _CONSERVATIVE_HEAT_PUMP_CONTROL
|
|
if code in _HEAT_NETWORK_CODES:
|
|
return _DEFAULT_HEAT_NETWORK_CONTROL
|
|
return None
|
|
|
|
|
|
def _category_for(code: int) -> Optional[int]:
|
|
"""The SAP Table 4a heating category a code implies, where the archetype
|
|
fixes it. Set so a system-only override reads as the new system's category,
|
|
not whatever category the replaced system carried — room heaters are 10,
|
|
electric storage heaters (incl. HHR 409) are 7, heat pumps 4, heat networks
|
|
6. Setting storage → 7 keeps the overlaid cert consistent (code 401-409 ↔
|
|
category 7); without it a storage override left a stale category behind,
|
|
which the SAP 10.2 Table 12a resolver (keyed on category) could misread as a
|
|
non-storage system and price the off-peak storage heating at the peak rate."""
|
|
if code in _CATEGORY_10_ROOM_HEATER_CODES or code in _ELECTRIC_ROOM_HEATER_CODES:
|
|
return _ROOM_HEATER_CATEGORY
|
|
if code in _STORAGE_HEATER_CODES:
|
|
return _STORAGE_HEATER_CATEGORY
|
|
if code in _ELECTRIC_UNDERFLOOR_CODES:
|
|
return _UNDERFLOOR_CATEGORY
|
|
if code in _ELECTRIC_BOILER_CODES:
|
|
return _ELECTRIC_BOILER_CATEGORY
|
|
if code in _HEAT_PUMP_CODES:
|
|
return _HEAT_PUMP_CATEGORY
|
|
if code in _HEAT_NETWORK_CODES:
|
|
return _HEAT_NETWORK_CATEGORY
|
|
return None
|
|
|
|
|
|
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
|
|
or code in _HEAT_PUMP_CODES
|
|
or code == _HHR_STORAGE_CODE
|
|
or code in _ELECTRIC_BOILER_CODES
|
|
or code in _ELECTRIC_UNDERFLOOR_CODES
|
|
):
|
|
return _ELECTRICITY_FUEL
|
|
if code in _SOLID_FUEL_ROOM_HEATER_CODES:
|
|
return _HOUSE_COAL_FUEL
|
|
if code in _OIL_ROOM_HEATER_CODES:
|
|
return _OIL_FUEL
|
|
if code in _GAS_ROOM_HEATER_CODES:
|
|
return _MAINS_GAS_FUEL
|
|
if code in _COMMUNITY_BOILER_CODES:
|
|
return _COMMUNITY_GAS_FUEL
|
|
return None
|
|
|
|
|
|
def _gas_boiler_overlay(code: int) -> HeatingOverlay:
|
|
"""The coherent gas-boiler companion set: a mains-gas connection + gas main
|
|
fuel, the gas-boiler heating category, a fanned room-sealed flue, full modern
|
|
controls, a single-rate meter, and a hot-water arrangement drawn from the
|
|
main system (a combi has no cylinder; a regular boiler / CPSU keeps one)."""
|
|
return HeatingOverlay(
|
|
sap_main_heating_code=code,
|
|
main_heating_category=_GAS_BOILER_CATEGORY,
|
|
main_fuel_type=_MAINS_GAS_FUEL,
|
|
gas_connection_available=True,
|
|
main_heating_control=_FULL_BOILER_CONTROL,
|
|
fan_flue_present=True,
|
|
meter_type=_SINGLE_RATE_METER,
|
|
water_heating_code=_FROM_MAIN_WATER_HEATING_CODE,
|
|
water_heating_fuel=_MAINS_GAS_FUEL,
|
|
has_hot_water_cylinder=code not in _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
|
|
plausibility check can compare it against a landlord `main_fuel` override."""
|
|
code = _MAIN_HEATING_CODES.get(main_heating_value)
|
|
if code is None:
|
|
return None
|
|
return _natural_fuel_for(code)
|
|
|
|
|
|
def main_heating_overlay_for(
|
|
main_heating_value: str, building_part: int
|
|
) -> Optional[EpcSimulation]:
|
|
code = _MAIN_HEATING_CODES.get(main_heating_value)
|
|
if code is None:
|
|
return None
|
|
if code in _GAS_BOILER_CODES:
|
|
return EpcSimulation(heating=_gas_boiler_overlay(code))
|
|
category = _category_for(code)
|
|
control = _control_for(code)
|
|
if category is None or control is None:
|
|
# A system-replacing override must stamp a complete, coherent companion
|
|
# set; an unmapped category or control leaves the field unset, so the
|
|
# effective cert inherits the REPLACED system's value — the silent
|
|
# mis-rating class ADR-0048 exists to stop. Every mapped archetype now
|
|
# carries confident defaults (ADR-0050/0052/0053), so an incomplete set
|
|
# can only mean a new archetype landed without choosing its companions:
|
|
# fail loudly at overlay time rather than ship an incoherent cert
|
|
# (ADR-0048's intended end-state, #1444 Part C).
|
|
raise ValueError(
|
|
f"Landlord main_heating_system {main_heating_value!r} (SAP code "
|
|
f"{code}) resolves an incomplete companion set (category="
|
|
f"{category}, control={control}); add the archetype's SAP Table 4a "
|
|
f"category and conservative Table 4e control before mapping it"
|
|
)
|
|
return EpcSimulation(
|
|
heating=HeatingOverlay(
|
|
sap_main_heating_code=code,
|
|
main_heating_category=category,
|
|
main_fuel_type=_natural_fuel_for(code),
|
|
meter_type=_meter_for(code),
|
|
main_heating_control=control,
|
|
# A landlord override describes the existing dwelling, so its assumed
|
|
# off-peak meter must not downgrade a more-off-peak cert meter
|
|
# (e.g. a 24-hour all-low tariff). Measures, which re-meter for real,
|
|
# build their HeatingOverlay directly and leave this False.
|
|
keep_existing_off_peak_meter=True,
|
|
# Likewise a heat-network override's control default (2313) must
|
|
# not overwrite an assessor-OBSERVED Group 3 control on a community
|
|
# cert being confirmed — only a stale cross-family control is
|
|
# replaced (ADR-0053).
|
|
keep_existing_heat_network_control=code in _HEAT_NETWORK_CODES,
|
|
)
|
|
)
|