mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-06-08 11:17:27 +00:00
fix(mapper): resolve gas-boiler main fuel from §14.2 mains-gas meter
A Summary §14.0 Table 4b gas boiler (SAP code 101-119) lodges no §14.0 "Fuel Type" string in the newer Elmhurst export. The carrier was resolved only from §15.0 "Water Heating Fuel Type" — fine when the same boiler heats the water, but a gas boiler paired with a SEPARATE electric immersion lodges §15.0 "Electricity", so `_elmhurst_gas_boiler_main_fuel` returned None and the cascade strict-raised MissingMainFuelType. Cert 001431 boiler-1/boiler-2 "before" variants are exactly this config: §14.0 SAP code 102/104 (mains-gas boiler), §15.0 electric immersion (code 909), §14.2 Meters "Main gas: Yes". The meter flag is the authoritative carrier signal — a 101-119 boiler on mains gas burns mains gas — so adopt it (SAP10 main_fuel 26 per _ELMHURST_MAIN_FUEL_TO_SAP10 "Mains gas") when §15.0 can't disambiguate. §15.0 gas/LPG still wins when present (keeps LPG-vs-mains-gas precision); no mains-gas meter + non-gas §15.0 still strict-raises rather than guessing. Spec: SAP 10.2 Table 4b "Seasonal efficiency for gas and liquid fuel boilers" (PDF p.168), rows 101-119. Both certs now resolve main_fuel=26 and compute (was: hard raise). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
bb8307413f
commit
6b04514645
2 changed files with 92 additions and 14 deletions
|
|
@ -4504,3 +4504,64 @@ def test_elmhurst_wall_is_basement_disambiguates_system_built_from_basement() ->
|
|||
# Other constructions defer to the API code-6 heuristic.
|
||||
assert _elmhurst_wall_is_basement("CA Cavity") is None
|
||||
assert _elmhurst_wall_is_basement("") is None
|
||||
|
||||
|
||||
def test_gas_boiler_main_fuel_inferred_from_mains_gas_meter_when_hw_is_electric() -> None:
|
||||
# Arrange — the boiler-2/before variant of cert 001431 lodges §14.0
|
||||
# "Main Heating SAP Code: 102" (a Table 4b gas-boiler row, 101-119)
|
||||
# with NO §14.0 "Fuel Type" string and a SEPARATE electric immersion
|
||||
# for hot water (§15.0 "Water Heating Fuel Type: Electricity",
|
||||
# SAP code 909). The §15.0-water-fuel disambiguation can't fire
|
||||
# (electricity is not a gas/LPG carrier), so the mapper used to leave
|
||||
# main_fuel_type empty and the cascade strict-raised MissingMainFuelType.
|
||||
# The §14.2 Meters "Main gas: Yes" lodgement is the authoritative
|
||||
# carrier signal: a 101-119 gas boiler on mains gas burns mains gas
|
||||
# (SAP10 main_fuel 26 per _ELMHURST_MAIN_FUEL_TO_SAP10 "Mains gas").
|
||||
from datatypes.epc.domain.mapper import (
|
||||
_elmhurst_gas_boiler_main_fuel, # pyright: ignore[reportPrivateUsage]
|
||||
)
|
||||
|
||||
gas_boiler_sap_code = 102
|
||||
electric_immersion_fuel = 30 # §15.0 "Electricity" → Table 32 code 30
|
||||
|
||||
# Act — electric HW, but the dwelling is on mains gas.
|
||||
resolved = _elmhurst_gas_boiler_main_fuel(
|
||||
gas_boiler_sap_code, electric_immersion_fuel, main_gas=True
|
||||
)
|
||||
|
||||
# Assert — mains gas (26), not a strict-raise.
|
||||
assert resolved == 26
|
||||
|
||||
|
||||
def test_gas_boiler_main_fuel_prefers_section_15_gas_carrier_over_meter() -> None:
|
||||
# Arrange — when §15.0 DOES resolve a gas/LPG carrier (combi heats
|
||||
# space + water from the one appliance) it stays authoritative, so a
|
||||
# bottled-LPG boiler (main_fuel 5) is not overwritten by the mains-gas
|
||||
# meter flag.
|
||||
from datatypes.epc.domain.mapper import (
|
||||
_elmhurst_gas_boiler_main_fuel, # pyright: ignore[reportPrivateUsage]
|
||||
)
|
||||
|
||||
lpg_water_fuel = 5 # bottled LPG
|
||||
|
||||
# Act
|
||||
resolved = _elmhurst_gas_boiler_main_fuel(104, lpg_water_fuel, main_gas=True)
|
||||
|
||||
# Assert — §15.0 gas/LPG carrier wins.
|
||||
assert resolved == 5
|
||||
|
||||
|
||||
def test_gas_boiler_main_fuel_without_mains_gas_meter_still_unresolved() -> None:
|
||||
# Arrange — no mains gas meter AND §15.0 is electric: the carrier
|
||||
# genuinely can't be determined (e.g. an LPG boiler whose §15.0 lodges
|
||||
# an electric immersion), so the helper returns None and the caller
|
||||
# strict-raises rather than guessing.
|
||||
from datatypes.epc.domain.mapper import (
|
||||
_elmhurst_gas_boiler_main_fuel, # pyright: ignore[reportPrivateUsage]
|
||||
)
|
||||
|
||||
# Act
|
||||
resolved = _elmhurst_gas_boiler_main_fuel(102, 30, main_gas=False)
|
||||
|
||||
# Assert
|
||||
assert resolved is None
|
||||
|
|
|
|||
|
|
@ -4662,29 +4662,44 @@ _GAS_BOILER_SAP_MAIN_HEATING_CODES: Final[frozenset[int]] = (
|
|||
# case still strict-raises `MissingMainFuelType` to force a mapper fix.
|
||||
_GAS_LPG_MAIN_FUEL_CODES: Final[frozenset[int]] = frozenset({1, 5, 6, 7, 26, 27})
|
||||
|
||||
# SAP10 main-fuel code for mains gas (`_ELMHURST_MAIN_FUEL_TO_SAP10`
|
||||
# "Mains gas"). Used when a Table 4b gas boiler's carrier can't be read
|
||||
# from §14.0 / §15.0 but the §14.2 Meters "Main gas: Yes" lodgement
|
||||
# confirms the dwelling is on mains gas.
|
||||
_MAINS_GAS_MAIN_FUEL_CODE: Final[int] = 26
|
||||
|
||||
|
||||
def _elmhurst_gas_boiler_main_fuel(
|
||||
sap_main_heating_code: Optional[int],
|
||||
water_heating_fuel_code: Optional[int],
|
||||
main_gas: bool = False,
|
||||
) -> Optional[int]:
|
||||
"""Derive a gas/LPG main-fuel code for a Table 4b gas boiler whose
|
||||
§14.0 "Fuel Type" string is absent (newer Elmhurst export form).
|
||||
|
||||
Returns the §15.0 water-heating fuel code when, and only when, the
|
||||
SAP main-heating code is a Table 4b gas-boiler row (101-119) AND the
|
||||
§15.0 fuel resolves to a gas/LPG carrier — the same combi/boiler
|
||||
heats space + water, so §15.0 names the boiler's carrier. Returns
|
||||
None otherwise (non-gas-boiler code, or §15.0 lodges a non-gas fuel
|
||||
such as an electric immersion), leaving the caller to strict-raise.
|
||||
For a Table 4b gas-boiler row (101-119) the carrier is resolved, in
|
||||
priority order:
|
||||
1. §15.0 "Water Heating Fuel Type" when it resolves to a gas/LPG
|
||||
carrier — the same combi/boiler heats space + water, so §15.0 names
|
||||
the boiler's carrier and disambiguates mains-gas-vs-LPG precisely.
|
||||
2. The §14.2 Meters "Main gas: Yes" flag → mains gas (code 26). This
|
||||
covers a gas boiler paired with a SEPARATE electric immersion (where
|
||||
§15.0 lodges "Electricity", not the boiler's fuel): the meter still
|
||||
proves the boiler burns mains gas.
|
||||
|
||||
Returns None otherwise (non-gas-boiler code, or a gas boiler with no
|
||||
mains-gas meter and a non-gas §15.0 — e.g. an LPG boiler whose carrier
|
||||
is genuinely undeterminable), leaving the caller to strict-raise.
|
||||
|
||||
Spec: SAP 10.2 Table 4b "Seasonal efficiency for gas and liquid fuel
|
||||
boilers" (PDF p.168) — rows 101-119 are gas-family boilers.
|
||||
"""
|
||||
if (
|
||||
sap_main_heating_code in _GAS_BOILER_SAP_MAIN_HEATING_CODES
|
||||
and water_heating_fuel_code in _GAS_LPG_MAIN_FUEL_CODES
|
||||
):
|
||||
if sap_main_heating_code not in _GAS_BOILER_SAP_MAIN_HEATING_CODES:
|
||||
return None
|
||||
if water_heating_fuel_code in _GAS_LPG_MAIN_FUEL_CODES:
|
||||
return water_heating_fuel_code
|
||||
if main_gas:
|
||||
return _MAINS_GAS_MAIN_FUEL_CODE
|
||||
return None
|
||||
|
||||
|
||||
|
|
@ -5403,13 +5418,15 @@ def _map_elmhurst_sap_heating(survey: ElmhurstSiteNotes) -> SapHeating:
|
|||
# gas vs LPG vs biogas). The newer Elmhurst export leaves §14.0
|
||||
# "Fuel Type" empty and lodges only the SAP code (e.g. 104 condensing
|
||||
# combi, EES "BGW"); the §15.0 "Water Heating Fuel Type" names the
|
||||
# carrier because the same combi/boiler heats space + water. Adopt it
|
||||
# only when it resolves to a gas/LPG fuel, so a regular boiler paired
|
||||
# with an electric immersion (where §15.0 lodges "Electricity") still
|
||||
# strict-raises rather than mis-billing the gas boiler as electric.
|
||||
# carrier because the same combi/boiler heats space + water. When the
|
||||
# boiler instead pairs with a SEPARATE electric immersion (§15.0
|
||||
# lodges "Electricity"), the §14.2 Meters "Main gas: Yes" flag is the
|
||||
# authoritative carrier signal → mains gas. Without either, the gas
|
||||
# boiler still strict-raises rather than being mis-billed.
|
||||
if main_fuel_int is None:
|
||||
main_fuel_int = _elmhurst_gas_boiler_main_fuel(
|
||||
mh.main_heating_sap_code, water_heating_fuel,
|
||||
main_gas=survey.meters.main_gas,
|
||||
)
|
||||
# Solid-fuel main heating: SAP code rows 150-160 (open / closed
|
||||
# room heaters with boiler) and 600-636 (independent solid-fuel
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue