mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-19 17:03:02 +00:00
A landlord heating override DESCRIBES the dwelling, so its assumed off-peak meter (Dual/E7) is a coherent default — it must not downgrade a cert that already lodges a more-off-peak meter (e.g. property 709874's 24-hour all-low tariff). The overlay opts in via keep_existing_off_peak_meter; _fold_heating then keeps the cert's meter when both it and the desired meter are off-peak. Heating MEASURES build HeatingOverlay directly and leave the flag False, so they still re-meter for real (Elmhurst re-lodges 18-hour -> Dual on a storage install) — the cascade pins stay green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
173 lines
8.8 KiB
Python
173 lines
8.8 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"
|
|
|
|
# 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 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,
|
|
"Direct-acting electric": 191,
|
|
"Electric room heaters": 691,
|
|
}
|
|
|
|
|
|
def _meter_for(code: int) -> 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. Always set — never left to bleed."""
|
|
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, None for systems that take neither (direct-acting electric).
|
|
Overwrites a stale control inherited from the system being replaced."""
|
|
if code in _STORAGE_HEATER_CODES:
|
|
return _MANUAL_CHARGE_CONTROL
|
|
if code in _GAS_BOILER_CODES:
|
|
return _FULL_BOILER_CONTROL
|
|
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,
|
|
mains_gas=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 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))
|
|
return EpcSimulation(
|
|
heating=HeatingOverlay(
|
|
sap_main_heating_code=code,
|
|
meter_type=_meter_for(code),
|
|
main_heating_control=_control_for(code),
|
|
# 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,
|
|
)
|
|
)
|