mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-30 13:10:47 +00:00
fix(electric-heaters): code-699 "electric heaters assumed" bills Table 12a direct-acting split
A "No system present: electric heaters assumed" lodging carries SAP Table 4a code 699 (electric room heaters) but RdSAP main_heating_category 1, NOT 10. `_table_12a_system_for_main` keyed the direct-acting-electric routing on category==10 only, so the category-1 form fell through to None and `_space_heating_fuel_cost_gbp_per_kwh` billed space heating 100% at the off-peak LOW rate — as if direct-acting room heaters charged overnight like storage. Per RdSAP 10 §12 Rule 3 (PDF p.62) electric room heaters (691-694, 699) route to the 10-hour tariff, and SAP 10.2 Table 12a Grid 1 (PDF p.191) gives the "other direct-acting electric" row a 0.50 high-rate fraction at 10-hour (1.00 at 7-hour). Route those SAP codes — the same set §12 Rule 3 already uses — to OTHER_DIRECT_ACTING_ELECTRIC alongside the category-10 gate. Found via the PE/CO2-vs-cost split on the worst over-rater in the /tmp sample: cert 2958 PE +0% / CO2 -1% (energy correct) but SAP +32.2 — a pure cost-side bug. Space rate 7.50 -> 11.09 p/kWh; cert 2958 +32.2 -> +14.7. The committed corpus gauge is unchanged (its 3 non-category-10 code-699 certs are all on Single meters -> STANDARD tariff, so this split never applies to them); the win is on the unbiased /tmp population's single worst cert. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
9ee3821138
commit
e7177a8bd4
2 changed files with 70 additions and 9 deletions
|
|
@ -2393,6 +2393,15 @@ _TARIFF_HIGH_LOW_FUEL_CODES_TABLE_12: Final[dict[Tariff, tuple[int, int]]] = {
|
|||
}
|
||||
|
||||
|
||||
# SAP Table 4a electric room-heater codes (panel/convector/radiant 691,
|
||||
# fan 692, portable 693, water-/oil-filled 694, "electric heaters assumed"
|
||||
# 699) — the same set RdSAP 10 §12 Rule 3 (PDF p.62) routes to the 10-hour
|
||||
# tariff. They are direct-acting electric for the Table 12a Grid 1 SH split.
|
||||
_ELECTRIC_ROOM_HEATER_SAP_CODES: Final[frozenset[int]] = frozenset(
|
||||
{691, 692, 693, 694, 699}
|
||||
)
|
||||
|
||||
|
||||
def _table_12a_system_for_main(
|
||||
main: Optional[MainHeatingDetail],
|
||||
) -> Optional[Table12aSystem]:
|
||||
|
|
@ -2421,15 +2430,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 room heaters (RdSAP main_heating_category 10) 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). Distinct from electric STORAGE
|
||||
# heaters (category 7), which charge off-peak and correctly fall
|
||||
# through to None here (→ 100% low rate). 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, this is belt-and-braces.
|
||||
if main.main_heating_category == 10 and _is_electric_main(main):
|
||||
# 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).
|
||||
# Identified EITHER by RdSAP main_heating_category 10 OR by a Table 4a
|
||||
# electric room-heater SAP code (691-694 panel/fan/portable/water-
|
||||
# filled, 699 "electric heaters assumed" — the SAME set RdSAP 10 §12
|
||||
# Rule 3 (PDF p.62) routes to the 10-hour tariff). The "No system
|
||||
# present: electric heaters assumed" lodging (code 699) carries
|
||||
# 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
|
||||
# `_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 (
|
||||
main.main_heating_category == 10
|
||||
or (code is not None and code in _ELECTRIC_ROOM_HEATER_SAP_CODES)
|
||||
):
|
||||
return Table12aSystem.OTHER_DIRECT_ACTING_ELECTRIC
|
||||
# A PCDB Table 362 record IS a heat pump by definition (the Appendix-N
|
||||
# efficiency cascade keys off it), whether or not a Table-4a SAP code
|
||||
|
|
|
|||
|
|
@ -715,6 +715,47 @@ def test_non_integrated_storage_heater_bills_100_percent_low_rate() -> None:
|
|||
assert abs(inputs.space_heating_fuel_cost_gbp_per_kwh - 0.0550) <= 1e-9
|
||||
|
||||
|
||||
def test_no_system_electric_heaters_assumed_code_699_bills_direct_acting_split() -> None:
|
||||
# Arrange — "No system present: electric heaters assumed" lodges SAP
|
||||
# Table 4a code 699 (electric room heaters) but RdSAP main_heating_
|
||||
# category 1, NOT 10, on a Dual (Economy-7) meter. RdSAP 10 §12 Rule 3
|
||||
# (PDF p.62) routes electric room heaters (691-694, 699) to the 10-hour
|
||||
# tariff, and SAP 10.2 Table 12a Grid 1 (PDF p.191) gives the "other
|
||||
# direct-acting electric" row a 0.50 high-rate fraction at 10-hour.
|
||||
# The scalar SH rate is therefore the blend 0.50 × high (14.68) + 0.50
|
||||
# × low (7.50) = 11.09 p/kWh — NOT the 7.50 p/kWh of 100% off-peak that
|
||||
# the category-10-only gate produced when it missed this category-1
|
||||
# lodging.
|
||||
from dataclasses import replace
|
||||
|
||||
main = MainHeatingDetail(
|
||||
has_fghrs=False,
|
||||
main_fuel_type=29, # electricity
|
||||
heat_emitter_type=0,
|
||||
emitter_temperature=1,
|
||||
main_heating_control=2699,
|
||||
main_heating_category=1, # NOT 10 — the "electric heaters assumed" form
|
||||
sap_main_heating_code=699,
|
||||
)
|
||||
base = make_minimal_sap10_epc(
|
||||
total_floor_area_m2=_TYPICAL_TFA_M2,
|
||||
habitable_rooms_count=4,
|
||||
country_code="ENG",
|
||||
sap_building_parts=[make_building_part(construction_age_band="E")],
|
||||
sap_heating=make_sap_heating(main_heating_details=[main]),
|
||||
)
|
||||
epc = replace(
|
||||
base,
|
||||
sap_energy_source=replace(base.sap_energy_source, meter_type="1"),
|
||||
)
|
||||
|
||||
# Act
|
||||
inputs = cert_to_inputs(epc)
|
||||
|
||||
# Assert — blended 0.50×14.68 + 0.50×7.50 = 11.09 p/kWh.
|
||||
assert abs(inputs.space_heating_fuel_cost_gbp_per_kwh - 0.1109) <= 1e-9
|
||||
|
||||
|
||||
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