From 3684a142ac71ba9cb5ecf0f8774ae876bda14cd5 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Thu, 4 Jun 2026 20:51:09 +0000 Subject: [PATCH] =?UTF-8?q?S0380.231:=20Dual-meter=20electric=20room=20hea?= =?UTF-8?q?ters=20resolve=20to=2010-hour=20tariff=20(RdSAP=2010=20=C2=A712?= =?UTF-8?q?=20Rule=203)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RdSAP 10 §12 (PDF p.62) Dual-meter dispatch: "the choice between 7-hour and 10-hour is made by the main heating type ... if the main system is a direct-acting electric boiler (191), or electric room heaters ... it is 10-hour tariff." The electric room-heater codes — Table 4a 691 (panel/ convector/radiant), 692 (fan), 693 (portable), 694 (water-/oil-filled), 699 (assumed) — were missing from `_RULE_3_TEN_HOUR_CODES` (the long- standing TODO there), so a Dual-meter room-heater cert fell through to Rule 4 (7-hour default). Compounded with S0380.230 (which routes room heaters to Table 12a OTHER_DIRECT_ACTING_ELECTRIC): at 7-hour the high-rate fraction is 1.00 (all at 15.29 p), but at the correct 10-hour it is 0.50 split over the 10-hour rates (14.68 / 7.50 p) → blended ~11 p. Without this fix .230 over-charged and flipped the cluster from over- to under-rating. 1,000-cert 2026 API sample: cat-10 mean |err| 7.11 → 5.26, signed mean +5.08 → -0.86 (now balanced, 22 over / 26 under — the systematic directional bias is gone). Overall mean |err| 2.16 → 2.04. Full §4 suite green (2406 passed). Co-Authored-By: Claude Opus 4.8 --- domain/sap10_calculator/tables/table_12a.py | 9 +++++--- .../domain/sap10_calculator/test_table_12a.py | 23 +++++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/domain/sap10_calculator/tables/table_12a.py b/domain/sap10_calculator/tables/table_12a.py index f98e1c27..2ee7c331 100644 --- a/domain/sap10_calculator/tables/table_12a.py +++ b/domain/sap10_calculator/tables/table_12a.py @@ -250,13 +250,16 @@ _RULE_2_STORAGE_CODES: Final[frozenset[int]] = frozenset( # Rule 3: direct-acting electric + heat pumps + electric room heaters # → 10-hour. §12 lists "heat pump (211 to 224, 521 to 524, or # database)" — the "database" branch fires when the cert lodges a -# PCDB Table 362 heat-pump index regardless of SAP code. +# PCDB Table 362 heat-pump index regardless of SAP code. §12 also +# names "electric room heaters" verbatim (RdSAP 10 PDF p.62) — Table 4a +# electric room-heater codes 691 (panel/convector/radiant), 692 (fan), +# 693 (portable), 694 (water-/oil-filled), 699 (assumed). Without these +# a Dual-meter room-heater cert fell through to Rule 4 (7-hour default). _RULE_3_TEN_HOUR_CODES: Final[frozenset[int]] = frozenset( [191] # direct-acting electric boiler + list(range(211, 225)) # heat pumps 211-224 + list(range(521, 525)) # warm-air heat pumps 521-524 - # TODO: electric room heater codes (SAP Table 4a row 6xx for - # electric panel / radiant heaters) when a fixture surfaces them. + + [691, 692, 693, 694, 699] # electric room heaters (Table 4a) ) diff --git a/tests/domain/sap10_calculator/test_table_12a.py b/tests/domain/sap10_calculator/test_table_12a.py index 2b294ca2..a15cb2ed 100644 --- a/tests/domain/sap10_calculator/test_table_12a.py +++ b/tests/domain/sap10_calculator/test_table_12a.py @@ -17,12 +17,35 @@ from domain.sap10_calculator.tables.table_12a import ( Table12aSystem, Tariff, other_use_high_rate_fraction, + rdsap_tariff_for_cert, space_heating_high_rate_fraction, tariff_from_meter_type, water_heating_high_rate_fraction, ) +def test_dual_meter_electric_room_heater_resolves_to_ten_hour_tariff() -> None: + # Arrange — RdSAP 10 §12 (PDF p.62) Dual-meter tariff dispatch: for a + # Dual meter the choice between 7-hour and 10-hour is made by the main + # heating type. Rule 3 verbatim: "if the main system ... is a direct- + # acting electric boiler (191), or electric room heaters ... it is + # 10-hour tariff." The electric room-heater codes are Table 4a 691 + # (panel/convector/radiant), 692 (fan), 693 (portable), 694 (water-/ + # oil-filled), 699 (assumed). Pre-slice these fell through to Rule 4 + # (7-hour default), so a Dual-meter room-heater cert was billed at the + # 7-hour rates with Table 12a high-rate fraction 1.00 instead of the + # 10-hour rates with fraction 0.50 — over-charging direct-acting heat + # once S0380.230 routed it to OTHER_DIRECT_ACTING_ELECTRIC. + + # Act / Assert — every electric room-heater code on a Dual meter → 10-hour. + for code in (691, 692, 693, 694, 699): + assert rdsap_tariff_for_cert(1, main_1_sap_code=code) is Tariff.TEN_HOUR + # Storage heaters (Rule 2) stay 7-hour; a gas room heater (non-Rule-3 + # code) keeps the Rule 4 Dual default of 7-hour. + assert rdsap_tariff_for_cert(1, main_1_sap_code=401) is Tariff.SEVEN_HOUR + assert rdsap_tariff_for_cert(1, main_1_sap_code=601) is Tariff.SEVEN_HOUR + + def test_tariff_enum_has_five_members() -> None: """Table 12a columns: standard (no off-peak split), 7-hour, 10-hour, 18-hour, 24-hour. Worksheet-shape fidelity: TEN_HOUR is included for