mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
fix(synthesis): Coherent Heating System — synthesisers own the whole boundary
A dwelling's heating is one conceptual system, but its fields are scattered across EpcPropertyData (a gov-API schema mirror): the cluster on sap_heating, the electricity tariff on sap_energy_source.meter_type, hot-water flags loose at top level. Three places synthesise a heating system — Measure Options, Landlord Overrides, EPC Prediction's donor — and each hand-copied a different ad-hoc subset. The override and donor both dropped meter_type, so an electric-storage system landed on the template's single-rate meter and billed overnight heat at the peak rate: property 713406 scored SAP 13 (G) vs ~50 (E), inflating the HHRSH measure to +45.8 and overshooting the plan to band A. Establish a single Coherent Heating System boundary (CONTEXT.md) that every synthesiser must cover, with a source-appropriate fill policy (ADR-0035): - Override overlay *completes* the partial system the landlord named. Companion fields are now DERIVED from the SAP code, not hand-attached per archetype: the off-peak meter from the calculator's single off-peak classification (new OFF_PEAK_IMPLYING_HEATING_CODES = SAP §12 Rules 1-2), and an unobserved storage charge control defaults to the conservative manual control (Table 4e 2401). So adding a heating archetype is just adding its code — companions can't be forgotten. A contract test guards it (every off-peak code drags a Dual meter). - Prediction's heating donor now *carries* the donor's meter_type alongside its sap_heating cluster — the donor is already coherent. Coherence is a synthesis-time obligation only; the calculator still scores a real lodged cert exactly as lodged. Verified on 713406: baseline 13 -> 47.8 (E), matching its recorded rating; the phantom HHRSH recommendation is gone and the plan no longer overshoots to A. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
80985865a2
commit
db3bf00602
8 changed files with 246 additions and 6 deletions
|
|
@ -292,6 +292,10 @@ _Avoid_: "windows" as a Measure (name **double glazing** / **secondary glazing**
|
|||
The rule fixing the single lighting Measure the **Lighting** Recommendation offers. We convert **all non-LED bulbs** (incandescent + CFL + low-energy-unknown) to **LED** — all the way to LED, not the legacy "fill to low energy", because SAP rates LED efficacy above CFL (ADR-0023). One Measure, no planning gate (lighting isn't planning-restricted). Offered only when the dwelling lodges at least one non-LED bulb; a dwelling already all-LED, or one that lodged **no** bulb counts (nothing to size against), gets no Recommendation. Unlike the fabric measures it is a **whole-dwelling** Measure — its **Simulation Overlay** writes the four top-level bulb counts directly (`led = total`, others 0), the first overlay surface that isn't a building part / window / system sub-object. Priced at a flat **average price per bulb** × the count of non-LED bulbs replaced. A free Optimiser candidate (it *improves* SAP), contrast the forced ventilation **Measure Dependency**.
|
||||
_Avoid_: "low energy lighting" as the upgrade target (we go to **LED**); treating it as a forced dependency (it is a free candidate); pricing by floor area (it's per-bulb count × average)
|
||||
|
||||
**Coherent Heating System**:
|
||||
The set of fields that together describe a dwelling's heating and must move **as a unit** whenever a heating system is *synthesised* — never a subset. It spans the main heating cluster (fuel, system, emitter, **controls**), the **hot water** it implies, the **off-peak meter** an electric-storage or CPSU system runs on, and the hot-water cylinder / solar flags. The fields are physically scattered across the `EpcPropertyData` (a gov-API schema mirror — the cluster, a loose meter on the energy source, loose top-level flags), but the *concept* is one system. Three places synthesise a heating system and each must cover the whole set with its own **fill policy**: a **Measure Option** sets a designed end-state (HHRSH / ASHP), a **Landlord Override** *completes* the partial system the landlord named with conservative assumptions for the fields they couldn't tell us (the off-peak meter is *derived* from the SAP code; an unobserved storage charge control defaults to the conservative manual one), and **EPC Prediction**'s heating donor *carries* a real neighbour's whole coherent set. Setting only a subset mis-scores: an electric-storage system left on a single-rate meter bills its overnight heat at the peak rate and collapses the SAP. **Coherence is owned by the synthesiser, never by the calculator** — a real lodged cert is scored exactly as lodged (ADR-0035).
|
||||
_Avoid_: heating overlay (that is one *mechanism*, not the field set); meter as a separate concern (it is part of the system); normalising a lodged EPC (coherence is a synthesis-time obligation only)
|
||||
|
||||
**Heating Eligibility**:
|
||||
The rule fixing which **Measure Options** the single **Heating & Hot Water** Recommendation offers (ADR-0024, expanded). The competing Options are **mutually-exclusive** (the Optimiser picks at most one) and fall in two families: **whole-system replacements** — `high_heat_retention_storage_heaters`, `air_source_heat_pump` — which change main heating + **controls + fuel + meter + the implied hot water** at once (never a separate HW measure; the legacy heating-vs-HW split double-counted); and, for a dwelling keeping a serviceable wet boiler, **partial upgrades** — `gas_boiler_upgrade` (a like-for-like condensing **gas** boiler: gas→gas, or non-gas→gas only where mains gas is present; combi or regular-plus-cylinder, shaped by the dwelling) and the **system tune-up** (keep the boiler; install better **controls** + fix the **cylinder**), the tune-up offered at two competing control levels: `system_tune_up` (standard, SAP code 2106) and `system_tune_up_zoned` (time-and-temperature zone control, 2110 — more SAP uplift, more cost). Each Option is a **fixed, real, contractor-installable end-state** (ASHP via a fixed PCDB heat-pump index; HHR storage via `sap_main_heating_code=409`; the gas boiler via Table 4b code 102/104; controls via 2106/2110), not a derived ideal; **Product** stays cost-only, but a partial/bundle cost is **composed per dwelling** from the components the overlay installs (ADR-0025/0027), not a flat scalar. Eligibility encodes **only physical/planning installability** — the **Optimiser owns the economics**, so it must not re-gate on cost proxies: **ASHP** → houses/bungalows that are not **listed**/**heritage** and not already a heat pump (flats excluded — individual siting needs a survey; a **conservation area** still gets the offer, unlike glazing); **HHR storage** → off-gas or currently-electric dwellings, not community-heated or already HHR; **boiler upgrade / tune-up** → an existing (non-electric) wet boiler, the gas end-state gated on a mains-gas connection, a partial control upgrade offered only when it genuinely improves the existing control (never a downgrade or no-op). Floor area, fabric, fuel, and built form are **not** gates (the legacy ASHP built-form / 120 m² rule is dropped — no authoritative basis). A free Optimiser candidate, not a forced **Measure Dependency**.
|
||||
_Avoid_: separate "heating" and "hot water" recommendations (HW folds into each Option); gating ASHP on floor area / built form / fabric (eligibility is physical/planning only — the Optimiser decides cost-effectiveness); treating the whole-system replacements and the partial boiler/tune-up upgrades as **separate** Recommendations (they are mutually-exclusive Options within the one heating rec — separate recs would let the Optimiser co-select and double-charge); a standalone hot-water-only or controls-only Recommendation (controls + cylinder fold into the boiler/tune-up Option)
|
||||
|
|
|
|||
64
docs/adr/0035-coherent-heating-system-synthesis.md
Normal file
64
docs/adr/0035-coherent-heating-system-synthesis.md
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
# Coherent Heating System is owned by the synthesiser, not the calculator
|
||||
|
||||
## Status
|
||||
|
||||
accepted
|
||||
|
||||
## Context
|
||||
|
||||
A dwelling's heating is one conceptual **system**, but its fields are physically
|
||||
scattered across `EpcPropertyData` because that type mirrors the gov-API / RdSAP
|
||||
schema: the main cluster lives on `sap_heating`, the electricity tariff on
|
||||
`sap_energy_source.meter_type`, and hot-water flags (`has_hot_water_cylinder`,
|
||||
`solar_water_heating`) on loose top-level fields.
|
||||
|
||||
Three places **synthesise** a heating system — a Measure Option (HHRSH/ASHP), a
|
||||
Landlord Override, and EPC Prediction's heating donor — and each was hand-copying
|
||||
a *different ad-hoc subset* of those fields. The override and the donor both
|
||||
omitted `meter_type`. Property 713406 (a predicted property whose landlord
|
||||
override set "Electric storage heaters") landed on the oil structural template's
|
||||
single-rate meter, so its overnight storage heat billed at the peak electricity
|
||||
rate: the calculator scored it **SAP 13 (G)** instead of **~50 (E)**, which then
|
||||
inflated the HHRSH measure to **+45.8 SAP** and pushed the plan to band A.
|
||||
|
||||
## Decision
|
||||
|
||||
Treat **"a heating system"** as one named boundary (a **Coherent Heating
|
||||
System** — see CONTEXT.md). Whoever *synthesises* a heating system owns covering
|
||||
the whole boundary, with a fill policy suited to its source:
|
||||
|
||||
- **Measure** → set a designed end-state (already did).
|
||||
- **Landlord Override** → *complete* the partial system the landlord named.
|
||||
Companion fields are **derived from the SAP code**, not hand-attached per
|
||||
archetype: the off-peak meter from the calculator's single off-peak
|
||||
classification (`OFF_PEAK_IMPLYING_HEATING_CODES`, SAP §12 Rules 1-2), and —
|
||||
where a field has no logical pairing — a **conservative** default (storage
|
||||
charge control → manual, SAP Table 4e 2401, the lowest-SAP storage control).
|
||||
So adding a heating archetype is just adding its code; coherent companions
|
||||
fall out and cannot be forgotten.
|
||||
- **EPC Prediction donor** → *carry* a real neighbour's whole coherent set
|
||||
(including its `meter_type`), since the donor is already internally coherent.
|
||||
|
||||
**Coherence is a synthesis-time obligation, never a calculator normalisation.**
|
||||
A real lodged cert is scored exactly as lodged — the calculator must not "fix" a
|
||||
genuinely single-rate storage dwelling (its existing Unknown-meter inference is
|
||||
a separate, spec-faithful net for certs that lodged *Unknown*, and does not fire
|
||||
on an explicit meter). A contract test guards the override path: every off-peak
|
||||
code the archetype map can emit must drag a `Dual` meter.
|
||||
|
||||
## Considered Options
|
||||
|
||||
- **Reify a `HeatingSystem` value object** all three produce/consume — rejected:
|
||||
overlaps `HeatingOverlay` and is a large, risky restructure for the value.
|
||||
- **A global coherence pass over the assembled effective EPC** — rejected: it
|
||||
would corrupt real lodged certs (force storage→dual on a genuine single-rate
|
||||
lodgement). Coherence must be scoped to synthesis points.
|
||||
- **Set `meter_type="Unknown"` and lean on the calculator's storage inference**
|
||||
— rejected as the primary mechanism: an explicit `Dual` is self-contained,
|
||||
matches the measure overlay, carries display value, and does not depend on a
|
||||
repo *extension* that could later be tightened toward spec.
|
||||
|
||||
This amends [ADR-0029](0029-epc-prediction-from-comparable-properties.md) (the
|
||||
heating donor now carries the meter) and extends
|
||||
[ADR-0032](0032-landlord-override-epc-overlay.md) (the override overlay derives
|
||||
coherent companions).
|
||||
|
|
@ -7,6 +7,16 @@ 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
|
||||
calculator's single off-peak classification (`OFF_PEAK_IMPLYING_HEATING_CODES`,
|
||||
SAP §12), 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
|
||||
|
|
@ -20,11 +30,30 @@ 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,
|
||||
)
|
||||
|
||||
# Canonical system archetype → representative `sap_main_heating_code` (SAP Table
|
||||
# 4b boiler rows / Table 4a). Codes map to the modern/condensing variant (A-G
|
||||
# efficiency deferred): 102 regular condensing, 104 condensing combi, 120 CPSU,
|
||||
# 404 fan storage heaters, 191 direct-acting electric boiler.
|
||||
# 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"
|
||||
|
||||
# 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))
|
||||
|
||||
# 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. Companion fields (meter / charge control) are NOT
|
||||
# listed here — they are derived from the code below, so a new archetype is just
|
||||
# a code.
|
||||
_MAIN_HEATING_CODES: dict[str, int] = {
|
||||
"Gas boiler, combi": 104,
|
||||
"Gas boiler, regular": 102,
|
||||
|
|
@ -37,10 +66,29 @@ _MAIN_HEATING_CODES: dict[str, int] = {
|
|||
}
|
||||
|
||||
|
||||
def _meter_for(code: int) -> Optional[str]:
|
||||
"""The coherent off-peak meter a heating code implies, or None when the
|
||||
system is single-rate. Keyed off the calculator's §12 off-peak set so the
|
||||
"which systems are off-peak" knowledge has one home."""
|
||||
return _OFF_PEAK_METER if code in OFF_PEAK_IMPLYING_HEATING_CODES else None
|
||||
|
||||
|
||||
def _charge_control_for(code: int) -> Optional[int]:
|
||||
"""The conservative storage charge control to assume when unobserved, or
|
||||
None for systems that don't take one."""
|
||||
return _MANUAL_CHARGE_CONTROL if code in _STORAGE_HEATER_CODES else None
|
||||
|
||||
|
||||
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
|
||||
return EpcSimulation(heating=HeatingOverlay(sap_main_heating_code=code))
|
||||
return EpcSimulation(
|
||||
heating=HeatingOverlay(
|
||||
sap_main_heating_code=code,
|
||||
meter_type=_meter_for(code),
|
||||
main_heating_control=_charge_control_for(code),
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -86,13 +86,20 @@ class EpcPrediction:
|
|||
(a recent cert reflects the current system). This makes the predicted
|
||||
heating both representative and internally coherent, rather than whatever
|
||||
the size-representative template happened to carry. No donor (no neighbour
|
||||
lodges a main heating system) leaves the template's heating in place."""
|
||||
lodges a main heating system) leaves the template's heating in place.
|
||||
|
||||
The coherent heating system spans more than `sap_heating` (ADR-0035): its
|
||||
electricity tariff (`sap_energy_source.meter_type`) and hot-water flags
|
||||
live on loose top-level fields. Carry the donor's whole set, not a subset
|
||||
— otherwise a donated storage system lands on the template's single-rate
|
||||
meter and the SAP score collapses (off-peak heat billed at the peak rate)."""
|
||||
donor = _heating_donor(comparables.members)
|
||||
if donor is None:
|
||||
return
|
||||
predicted.sap_heating = copy.deepcopy(donor.epc.sap_heating)
|
||||
predicted.has_hot_water_cylinder = donor.epc.has_hot_water_cylinder
|
||||
predicted.solar_water_heating = donor.epc.solar_water_heating
|
||||
predicted.sap_energy_source.meter_type = donor.epc.sap_energy_source.meter_type
|
||||
|
||||
@staticmethod
|
||||
def _apply_glazing_mode(
|
||||
|
|
|
|||
|
|
@ -262,6 +262,20 @@ _RULE_3_TEN_HOUR_CODES: Final[frozenset[int]] = frozenset(
|
|||
+ [691, 692, 693, 694, 699] # electric room heaters (Table 4a)
|
||||
)
|
||||
|
||||
# The heating codes whose *presence* implies an off-peak (dual) meter: electric
|
||||
# CPSU (Rule 1) and storage-based electric (Rule 2). These charge overnight and
|
||||
# cannot run economically on a single rate, so the §12 dispatch already infers
|
||||
# off-peak for them when the meter is Unknown (see `tariff_dispatch`). Exposed so
|
||||
# *synthesis* (Landlord-Override / EPC-Prediction) can pair a coherent off-peak
|
||||
# meter with such a system from the SAP code alone — the single source of "which
|
||||
# systems are off-peak". Rule 3 (direct-acting electric, heat pumps, room
|
||||
# heaters) is deliberately NOT here: those run on demand and live on single-rate
|
||||
# meters too. A "Dual" meter on any of these lets the §12 dispatch resolve the
|
||||
# specific tariff (CPSU → 10-hour, storage → 7-hour).
|
||||
OFF_PEAK_IMPLYING_HEATING_CODES: Final[frozenset[int]] = (
|
||||
_RULE_1_CPSU_CODES | _RULE_2_STORAGE_CODES
|
||||
)
|
||||
|
||||
|
||||
def _meter_is_unknown(meter_type: object) -> bool:
|
||||
"""True when the meter is the RdSAP "Unknown" sentinel (code 3 / the
|
||||
|
|
|
|||
|
|
@ -11,8 +11,12 @@ import pytest
|
|||
from domain.epc.property_overrides.main_heating_system_type import MainHeatingSystemType
|
||||
from domain.epc.property_overlays.main_fuel_overlay import fuel_overlay_for
|
||||
from domain.epc.property_overlays.main_heating_system_overlay import (
|
||||
_MAIN_HEATING_CODES,
|
||||
main_heating_overlay_for,
|
||||
)
|
||||
from domain.sap10_calculator.tables.table_12a import (
|
||||
OFF_PEAK_IMPLYING_HEATING_CODES,
|
||||
)
|
||||
from domain.epc.property_overlays.water_heating_overlay import (
|
||||
water_heating_overlay_for,
|
||||
)
|
||||
|
|
@ -73,6 +77,88 @@ def test_storage_heater_subtypes_decode_to_their_codes(
|
|||
assert simulation.heating.sap_main_heating_code == code
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"main_heating_value",
|
||||
[
|
||||
"Electric storage heaters, old",
|
||||
"Electric storage heaters, slimline",
|
||||
"Electric storage heaters, convector",
|
||||
"Electric storage heaters, fan",
|
||||
],
|
||||
)
|
||||
def test_storage_heaters_carry_an_off_peak_meter(main_heating_value: str) -> None:
|
||||
# Storage heaters run on an Economy 7 (off-peak) tariff by design; setting
|
||||
# only the heating code while leaving a single-rate meter bills every heating
|
||||
# kWh at the peak rate and collapses the score (an override-set storage
|
||||
# dwelling left on an oil donor's single meter scored SAP 13 vs ~50 on
|
||||
# Economy 7). The overlay carries the off-peak meter, like the HHRSH measure.
|
||||
simulation = main_heating_overlay_for(main_heating_value, 0)
|
||||
assert simulation is not None
|
||||
assert simulation.heating is not None
|
||||
assert simulation.heating.meter_type == "Dual"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"main_heating_value", ["Gas boiler, combi", "Direct-acting electric"]
|
||||
)
|
||||
def test_non_storage_heating_leaves_the_meter_untouched(
|
||||
main_heating_value: str,
|
||||
) -> None:
|
||||
# Only storage heaters imply an off-peak tariff; gas and direct-acting
|
||||
# electric (single-rate) keep whatever meter the dwelling already has.
|
||||
simulation = main_heating_overlay_for(main_heating_value, 0)
|
||||
assert simulation is not None
|
||||
assert simulation.heating is not None
|
||||
assert simulation.heating.meter_type is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"main_heating_value",
|
||||
[
|
||||
"Electric storage heaters, old",
|
||||
"Electric storage heaters, slimline",
|
||||
"Electric storage heaters, convector",
|
||||
"Electric storage heaters, fan",
|
||||
],
|
||||
)
|
||||
def test_storage_heaters_drag_along_conservative_manual_charge_control(
|
||||
main_heating_value: str,
|
||||
) -> None:
|
||||
# The landlord names the system, not its charge control. Manual charge
|
||||
# control (Table 4e code 2401, +0.7 C MIT adjustment) is the lowest-SAP
|
||||
# storage control, so it's the safe assumption that never over-credits.
|
||||
simulation = main_heating_overlay_for(main_heating_value, 0)
|
||||
assert simulation is not None
|
||||
assert simulation.heating is not None
|
||||
assert simulation.heating.main_heating_control == 2401
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"main_heating_value", ["Gas boiler, combi", "Direct-acting electric"]
|
||||
)
|
||||
def test_non_storage_heating_does_not_drag_along_a_charge_control(
|
||||
main_heating_value: str,
|
||||
) -> None:
|
||||
# Charge control is a storage-heater concept; other systems keep their own.
|
||||
simulation = main_heating_overlay_for(main_heating_value, 0)
|
||||
assert simulation is not None
|
||||
assert simulation.heating is not None
|
||||
assert simulation.heating.main_heating_control is None
|
||||
|
||||
|
||||
def test_off_peak_archetypes_drag_a_dual_meter_others_do_not() -> None:
|
||||
# Contract (the drag-along guard): the off-peak meter is derived from the SAP
|
||||
# code via the calculator's single off-peak classification, so any heating
|
||||
# archetype in the map whose code implies off-peak MUST synthesise a Dual
|
||||
# meter — adding an off-peak system can never silently leave it single-rate —
|
||||
# and a single-rate system must never gain one.
|
||||
for value, code in _MAIN_HEATING_CODES.items():
|
||||
simulation = main_heating_overlay_for(value, 0)
|
||||
assert simulation is not None and simulation.heating is not None
|
||||
expected = "Dual" if code in OFF_PEAK_IMPLYING_HEATING_CODES else None
|
||||
assert simulation.heating.meter_type == expected, value
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"main_heating_value",
|
||||
["Unknown", "", "Air source heat pump", "Community heating"],
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from datatypes.epc.domain.epc_property_data import (
|
|||
EpcPropertyData,
|
||||
MainHeatingDetail,
|
||||
SapBuildingPart,
|
||||
SapEnergySource,
|
||||
SapFloorDimension,
|
||||
SapHeating,
|
||||
SapWindow,
|
||||
|
|
@ -47,6 +48,7 @@ def _epc(
|
|||
water_heating_code: Optional[int] = 1,
|
||||
has_hot_water_cylinder: bool = True,
|
||||
solar_water_heating: bool = False,
|
||||
meter_type: str = "2",
|
||||
) -> EpcPropertyData:
|
||||
epc: EpcPropertyData = object.__new__(EpcPropertyData)
|
||||
epc.property_type = "2"
|
||||
|
|
@ -84,6 +86,9 @@ def _epc(
|
|||
epc.sap_heating = heating
|
||||
epc.has_hot_water_cylinder = has_hot_water_cylinder
|
||||
epc.solar_water_heating = solar_water_heating
|
||||
energy: SapEnergySource = object.__new__(SapEnergySource)
|
||||
energy.meter_type = meter_type
|
||||
epc.sap_energy_source = energy
|
||||
return epc
|
||||
|
||||
|
||||
|
|
@ -543,3 +548,14 @@ def test_applies_a_known_wall_override_over_the_mode() -> None:
|
|||
|
||||
# Assert — the known override overrides the cohort mode.
|
||||
assert predicted.sap_building_parts[0].wall_construction == 2
|
||||
|
||||
|
||||
def test_heating_donor_carries_the_donors_off_peak_meter() -> None:
|
||||
# The coherent heating system spans the meter (ADR-0035): the donor's
|
||||
# off-peak meter must travel with its heating cluster, replacing the
|
||||
# template's single-rate meter — otherwise a donated storage system bills at
|
||||
# the peak rate and the score collapses.
|
||||
predicted = _epc(meter_type="2") # the structural template's single meter
|
||||
donor = _epc(meter_type="Dual", main_fuel_type=29) # the cohort's heating
|
||||
EpcPrediction._apply_heating_donor(predicted, _cohort(donor))
|
||||
assert predicted.sap_energy_source.meter_type == "Dual"
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@ def _comparable(
|
|||
energy: SapEnergySource = object.__new__(SapEnergySource)
|
||||
energy.photovoltaic_supply = None
|
||||
energy.photovoltaic_arrays = None
|
||||
energy.meter_type = "2"
|
||||
epc.sap_energy_source = energy
|
||||
return ComparableProperty(
|
||||
epc=epc,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue