Merge pull request #1460 from Hestia-Homes/fix/table-12a-underfloor-rows

Electric underfloor codes resolve their Table 12a Grid 1 rows (ADR-0050 deferred follow-up)
This commit is contained in:
KhalimCK 2026-07-03 17:53:59 +01:00 committed by GitHub
commit 6dfaecc352
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 95 additions and 1 deletions

View file

@ -2603,6 +2603,23 @@ _ELECTRIC_STORAGE_BOILER_CODES: Final[frozenset[int]] = frozenset(
{193, 194, 195, 196}
)
_ELECTRIC_CPSU_CODES: Final[frozenset[int]] = frozenset({192})
# Electric underfloor (Table 4a 421-425) — SAP 10.2 Table 12a Grid 1 (PDF
# p.184) names the codes in its row labels:
# 422/423 "Integrated (storage+direct-acting) systems (applies to storage
# heaters 408 and underfloor heating 422 and 423)" → 7-hour 0.20
# 424/425 "Underfloor heating (in screed above insulation, in timber floor
# or immediately below floor covering)" → 7-hour 0.90 / 10-hour 0.50
# 421 "in concrete slab (off-peak only)" is named in NEITHER underfloor
# row — the slab is the storage medium, so it takes the "Other
# storage heaters" row (0.00 at 7-hour or 24-hour), consistent with
# §12 Rule 2 listing 421 among the off-peak-implying storage codes.
# Before these branches every underfloor code fell to the None all-low-rate
# fallback, over-crediting a 7-hour screed dwelling's space heat at 5.50p/kWh
# instead of the blended ~14.3p (ADR-0050 deferred follow-up; 22 live
# 7-hour-metered underfloor dwellings on portfolio 796).
_SLAB_UNDERFLOOR_CODES: Final[frozenset[int]] = frozenset({421})
_INTEGRATED_UNDERFLOOR_CODES: Final[frozenset[int]] = frozenset({422, 423})
_SCREED_OR_TIMBER_UNDERFLOOR_CODES: Final[frozenset[int]] = frozenset({424, 425})
def _table_12a_system_for_main(
@ -2622,7 +2639,9 @@ def _table_12a_system_for_main(
- ASHP / GSHP (codes 211-224, 521-524, PCDB index) wired
- Storage heaters (cat 7): 408 INTEGRATED_STORAGE_DIRECT (0.20),
all others OTHER_STORAGE_HEATERS (0.00) wired
- Underfloor heating (421-422) TODO
- Underfloor heating: 421 slab OTHER_STORAGE_HEATERS (0.00),
422/423 integrated INTEGRATED_STORAGE_DIRECT (0.20),
424/425 screed/timber UNDERFLOOR_HEATING (0.90/0.50) wired
- Direct-acting electric boiler (191) 0.90/0.50; electric storage
boilers (193/194/195/196) 0.00 wired
- Electric CPSU (192) Appendix F high-rate cascade still TODO
@ -2634,6 +2653,19 @@ 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 UNDERFLOOR heating — routed by the Table 4a code per the Grid 1
# row labels (see `_SLAB_UNDERFLOOR_CODES` et al. above), checked BEFORE
# the storage branch so a stale category 7 cannot drag a screed system
# (0.90 at 7-hour) down to the all-night storage row — the code is more
# authoritative than the coarser category, mirroring the storage-over-
# room-heater precedent below. Gated on `_is_electric_main` belt-and-braces.
if _is_electric_main(main) and code is not None:
if code in _SCREED_OR_TIMBER_UNDERFLOOR_CODES:
return Table12aSystem.UNDERFLOOR_HEATING
if code in _INTEGRATED_UNDERFLOOR_CODES:
return Table12aSystem.INTEGRATED_STORAGE_DIRECT
if code in _SLAB_UNDERFLOOR_CODES:
return Table12aSystem.OTHER_STORAGE_HEATERS
# 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

View file

@ -1079,6 +1079,68 @@ 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_electric_underfloor_codes_map_to_their_table_12a_grid1_rows() -> None:
# Arrange — SAP 10.2 Table 12a Grid 1 (PDF p.184/191) names the underfloor
# codes explicitly: "Integrated (storage+direct-acting) systems (applies to
# storage heaters 408 and underfloor heating 422 and 423)" → 7-hour 0.20;
# "Underfloor heating (in screed above insulation, in timber floor or
# immediately below floor covering)" (424/425) → 7-hour 0.90 / 10-hour
# 0.50. Code 421 ("in concrete slab, off-peak only") is named in NEITHER
# underfloor row — the slab is the storage medium, so it takes "Other
# storage heaters" (0.00 at 7-hour or 24-hour), consistent with §12 Rule 2
# listing 421 among the off-peak-implying storage codes. Before this
# resolver branch every underfloor code fell to the None fallback and
# billed space heat 100% at the night rate — on portfolio 796, 22 of the
# 90 underfloor-override dwellings sit on a 7-hour Dual meter and were
# over-credited (e.g. screed 424 at 5.50p/kWh instead of the blended
# ~14.3p).
from domain.sap10_calculator.tables.table_12a import (
Table12aSystem,
Tariff,
space_heating_high_rate_fraction,
)
def _underfloor_main(code: int) -> MainHeatingDetail:
return MainHeatingDetail(
has_fghrs=False,
main_fuel_type=29, # electricity
heat_emitter_type=0,
emitter_temperature=1,
main_heating_control=2701,
main_heating_category=8, # electric underfloor
sap_main_heating_code=code,
)
# Act
slab = _table_12a_system_for_main(_underfloor_main(421))
integrated = [_table_12a_system_for_main(_underfloor_main(c)) for c in (422, 423)]
screed = [_table_12a_system_for_main(_underfloor_main(c)) for c in (424, 425)]
# Assert — the three spec rows, with their published fractions.
assert slab is Table12aSystem.OTHER_STORAGE_HEATERS
assert all(s is Table12aSystem.INTEGRATED_STORAGE_DIRECT for s in integrated)
assert all(s is Table12aSystem.UNDERFLOOR_HEATING for s in screed)
assert space_heating_high_rate_fraction(slab, Tariff.SEVEN_HOUR) == 0.00
assert (
space_heating_high_rate_fraction(
Table12aSystem.INTEGRATED_STORAGE_DIRECT, Tariff.SEVEN_HOUR
)
== 0.20
)
assert (
space_heating_high_rate_fraction(
Table12aSystem.UNDERFLOOR_HEATING, Tariff.SEVEN_HOUR
)
== 0.90
)
assert (
space_heating_high_rate_fraction(
Table12aSystem.UNDERFLOOR_HEATING, Tariff.TEN_HOUR
)
== 0.50
)
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