mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
Merge pull request #1426 from Hestia-Homes/fix/hhrsh-storage-offpeak-pricing
Fix HHRSH storage heaters priced at peak rate (engine 48 vs accredited Elmhurst 71)
This commit is contained in:
commit
4ca3491eb2
4 changed files with 97 additions and 13 deletions
|
|
@ -59,6 +59,14 @@ _HEAT_PUMP_CATEGORY = 4
|
|||
_HHR_STORAGE_OVERLAY = HeatingOverlay(
|
||||
main_fuel_type=_ELECTRICITY_FUEL,
|
||||
sap_main_heating_code=_HHR_STORAGE_SAP_CODE,
|
||||
# RdSAP electric-storage-heater category (7). Set so the overlaid EPC stays
|
||||
# internally consistent: without it the pre-existing system's category (e.g.
|
||||
# 10 = electric room heaters) survives alongside the new storage code 409,
|
||||
# and the SAP 10.2 Table 12a resolver — which classifies by category —
|
||||
# misreads the off-peak storage heating as direct-acting electric and prices
|
||||
# 100% at the peak rate (verified: UPRN 100020942571 HHRSH SAP 48 vs
|
||||
# accredited Elmhurst 71; category 7 -> 73). See ADR-0024.
|
||||
main_heating_category=7,
|
||||
main_heating_control=2404,
|
||||
water_heating_code=903,
|
||||
water_heating_fuel=_ELECTRICITY_FUEL,
|
||||
|
|
|
|||
|
|
@ -2574,6 +2574,17 @@ _ELECTRIC_ROOM_HEATER_SAP_CODES: Final[frozenset[int]] = frozenset(
|
|||
{691, 692, 693, 694, 699}
|
||||
)
|
||||
|
||||
# SAP 10.2 Table 4a electric STORAGE-heater codes (PDF p.169, codes 401-409:
|
||||
# old-large-volume / slimline / fan / integrated-storage-direct 408 / high-heat-
|
||||
# retention 409, etc.). Mirrors the storage subset of `table_12a._RULE_2_STORAGE_
|
||||
# CODES` (heaters only — excludes underfloor 421/422 and storage boilers 193/195,
|
||||
# which have their own Table 12a rows). Used to classify storage heaters by SAP
|
||||
# code as well as RdSAP `main_heating_category == 7`, so a storage-coded system
|
||||
# is priced off-peak even when its category is stale or inconsistent.
|
||||
_ELECTRIC_STORAGE_HEATER_SAP_CODES: Final[frozenset[int]] = frozenset(
|
||||
range(401, 410)
|
||||
)
|
||||
|
||||
# SAP 10.2 Table 4a electric boilers (PDF p.170, codes 191-196) → their
|
||||
# Table 12a Grid 1 SH rows (PDF p.191). NOTE the boiler families do NOT all
|
||||
# share a row — read the spec exactly:
|
||||
|
|
@ -2623,6 +2634,26 @@ def _table_12a_system_for_main(
|
|||
main.main_heating_index_number is not None
|
||||
and heat_pump_record(main.main_heating_index_number) is not None
|
||||
)
|
||||
# Electric STORAGE heaters — SAP 10.2 Table 12a Grid 1 (PDF p.191).
|
||||
# Detected by RdSAP main_heating_category 7 OR, authoritatively, by a Table
|
||||
# 4a electric-storage-heater SAP code (401-409). Keyed on the CODE as well as
|
||||
# the category, and checked BEFORE the room-heater branch below, so a storage
|
||||
# system whose category is stale or inconsistent — e.g. a recommendation
|
||||
# `HeatingOverlay` that sets `sap_main_heating_code=409` (HHR storage) but
|
||||
# leaves the pre-existing electric-room-heater `main_heating_category=10` — is
|
||||
# still priced off-peak here instead of being misclassified as direct-acting
|
||||
# (1.00 high-rate → 100% peak) by the category-10 gate. A specific Table 4a
|
||||
# code is more authoritative than the coarser category. Code 408 is an
|
||||
# "Integrated (storage + direct-acting) system" → 0.20 SH high-rate fraction
|
||||
# at 7-hour; every other storage code → "Other storage heaters" → 0.00
|
||||
# (charged wholly off-peak). Gated on `_is_electric_main` belt-and-braces.
|
||||
if _is_electric_main(main) and (
|
||||
main.main_heating_category == 7
|
||||
or (code is not None and code in _ELECTRIC_STORAGE_HEATER_SAP_CODES)
|
||||
):
|
||||
if code == 408:
|
||||
return Table12aSystem.INTEGRATED_STORAGE_DIRECT
|
||||
return Table12aSystem.OTHER_STORAGE_HEATERS
|
||||
# Electric room heaters are direct-acting electric → SAP 10.2 Table
|
||||
# 12a Grid 1 (PDF p.191) "Other systems including direct-acting
|
||||
# electric" row (7-hour high-rate fraction 1.00, 10-hour 0.50).
|
||||
|
|
@ -2634,9 +2665,9 @@ def _table_12a_system_for_main(
|
|||
# main_heating_category 1, NOT 10, so the category-only gate missed it
|
||||
# and it fell through to None → 100% off-peak LOW rate, billing
|
||||
# direct-acting heaters as if they charged overnight like storage
|
||||
# (cert 2958 +32.2 SAP, the worst over-rate in the sample). Distinct
|
||||
# from electric STORAGE heaters (category 7), which DO charge off-peak
|
||||
# and correctly fall through to None here (→ 100% low rate). Gated on
|
||||
# (cert 2958 +32.2 SAP, the worst over-rate in the sample). Electric
|
||||
# STORAGE heaters (category 7 / codes 401-409) are handled by the
|
||||
# storage-heater branch above, so they never reach here. Gated on
|
||||
# `_is_electric_main` so a non-electric room heater (gas / solid-fuel
|
||||
# cat 10) is excluded; all callers already pre-gate on electric.
|
||||
if _is_electric_main(main) and (
|
||||
|
|
@ -2665,16 +2696,6 @@ def _table_12a_system_for_main(
|
|||
211 <= code <= 217 or 221 <= code <= 227 or 521 <= code <= 524
|
||||
):
|
||||
return Table12aSystem.ASHP_OTHER
|
||||
# Electric STORAGE heaters (RdSAP main_heating_category 7) — SAP 10.2
|
||||
# Table 12a Grid 1 (PDF p.191). Code 408 is an "Integrated (storage +
|
||||
# direct-acting) system" → 0.20 SH high-rate fraction at 7-hour; every
|
||||
# other storage code is "Other storage heaters" → 0.00 (charged wholly
|
||||
# off-peak, the same 100%-low-rate the None fallback already gave).
|
||||
# Gated on `_is_electric_main` belt-and-braces (all callers pre-gate).
|
||||
if main.main_heating_category == 7 and _is_electric_main(main):
|
||||
if code == 408:
|
||||
return Table12aSystem.INTEGRATED_STORAGE_DIRECT
|
||||
return Table12aSystem.OTHER_STORAGE_HEATERS
|
||||
# Electric boilers (Table 4a codes 191-196) — resolve the misleading TODO
|
||||
# that lumped them as one "direct-acting" family. They split across THREE
|
||||
# distinct Table 12a Grid 1 rows (see `_DIRECT_ACTING_ELECTRIC_BOILER_CODES`
|
||||
|
|
|
|||
|
|
@ -76,6 +76,12 @@ def test_electric_storage_dwelling_yields_an_hhr_storage_bundle() -> None:
|
|||
].overlay.heating == HeatingOverlay(
|
||||
main_fuel_type=30,
|
||||
sap_main_heating_code=409,
|
||||
# Storage category (7) pins the fix for the off-peak-pricing bug: the
|
||||
# overlay must set it so the Table 12a resolver classifies the off-peak
|
||||
# storage heating as storage (0.00 high-rate) and not direct-acting
|
||||
# electric (1.00) via a stale category. See test_cert_to_inputs
|
||||
# `test_storage_heater_classified_by_sap_code_over_stale_category`.
|
||||
main_heating_category=7,
|
||||
main_heating_control=2404,
|
||||
water_heating_code=903,
|
||||
water_heating_fuel=30,
|
||||
|
|
|
|||
|
|
@ -1054,6 +1054,55 @@ def test_electric_boilers_191_195_map_to_distinct_table_12a_grid1_rows() -> None
|
|||
assert space_heating_high_rate_fraction(storage, Tariff.SEVEN_HOUR) == 0.00
|
||||
|
||||
|
||||
def test_storage_heater_classified_by_sap_code_over_stale_category() -> None:
|
||||
# Regression: electric STORAGE heaters must resolve to a storage Table 12a
|
||||
# Grid 1 row from their Table 4a SAP code (401-409) even when the RdSAP
|
||||
# `main_heating_category` is stale/inconsistent (== 10, electric room
|
||||
# heaters). This is exactly the state the HHRSH recommendation overlay
|
||||
# produces — it sets `sap_main_heating_code=409` on a dwelling that was
|
||||
# electric room heaters (category 10) — and the category-10 room-heater
|
||||
# branch used to win, mispricing the off-peak storage heating as
|
||||
# direct-acting electric (1.00 high-rate → 100% peak). Verified real-cert
|
||||
# impact: UPRN 100020942571 HHRSH SAP 48 (peak-priced) vs accredited
|
||||
# Elmhurst 71; code-authoritative classification restores it to 73.
|
||||
from domain.sap10_calculator.tables.table_12a import (
|
||||
Table12aSystem,
|
||||
Tariff,
|
||||
space_heating_high_rate_fraction,
|
||||
)
|
||||
|
||||
def _storage_main(code: int, category: int) -> MainHeatingDetail:
|
||||
return MainHeatingDetail(
|
||||
has_fghrs=False,
|
||||
main_fuel_type=29, # electricity
|
||||
heat_emitter_type=0,
|
||||
emitter_temperature=1,
|
||||
main_heating_control=2404, # HHRSH controls
|
||||
main_heating_category=category,
|
||||
sap_main_heating_code=code,
|
||||
)
|
||||
|
||||
# HHR storage (409) with a stale room-heater category (10) — the overlay case.
|
||||
hhr_stale = _table_12a_system_for_main(_storage_main(409, category=10))
|
||||
assert hhr_stale is Table12aSystem.OTHER_STORAGE_HEATERS
|
||||
assert space_heating_high_rate_fraction(hhr_stale, Tariff.SEVEN_HOUR) == 0.00
|
||||
|
||||
# Integrated storage/direct-acting (408) is still its own row, code-driven.
|
||||
integrated = _table_12a_system_for_main(_storage_main(408, category=10))
|
||||
assert integrated is Table12aSystem.INTEGRATED_STORAGE_DIRECT
|
||||
assert space_heating_high_rate_fraction(integrated, Tariff.SEVEN_HOUR) == 0.20
|
||||
|
||||
# The canonical consistent case (category 7) is unchanged.
|
||||
hhr_consistent = _table_12a_system_for_main(_storage_main(409, category=7))
|
||||
assert hhr_consistent is Table12aSystem.OTHER_STORAGE_HEATERS
|
||||
|
||||
# A genuine electric room heater (code 691, category 10) is NOT swept into
|
||||
# the storage branch — it stays direct-acting (1.00 high-rate at 7-hour).
|
||||
room_heater = _table_12a_system_for_main(_storage_main(691, category=10))
|
||||
assert room_heater is Table12aSystem.OTHER_DIRECT_ACTING_ELECTRIC
|
||||
assert space_heating_high_rate_fraction(room_heater, Tariff.SEVEN_HOUR) == 1.00
|
||||
|
||||
|
||||
def test_heat_network_distribution_electricity_per_sap_10_2_appendix_c_3_2() -> None:
|
||||
# Arrange — heat-network main (Table 4a code 301 = community heating,
|
||||
# category 6). SAP 10.2 Appendix C §C3.2 (PDF p.51): distribution
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue