mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +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.
|
# Other constructions defer to the API code-6 heuristic.
|
||||||
assert _elmhurst_wall_is_basement("CA Cavity") is None
|
assert _elmhurst_wall_is_basement("CA Cavity") is None
|
||||||
assert _elmhurst_wall_is_basement("") 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.
|
# 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})
|
_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(
|
def _elmhurst_gas_boiler_main_fuel(
|
||||||
sap_main_heating_code: Optional[int],
|
sap_main_heating_code: Optional[int],
|
||||||
water_heating_fuel_code: Optional[int],
|
water_heating_fuel_code: Optional[int],
|
||||||
|
main_gas: bool = False,
|
||||||
) -> Optional[int]:
|
) -> Optional[int]:
|
||||||
"""Derive a gas/LPG main-fuel code for a Table 4b gas boiler whose
|
"""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).
|
§14.0 "Fuel Type" string is absent (newer Elmhurst export form).
|
||||||
|
|
||||||
Returns the §15.0 water-heating fuel code when, and only when, the
|
For a Table 4b gas-boiler row (101-119) the carrier is resolved, in
|
||||||
SAP main-heating code is a Table 4b gas-boiler row (101-119) AND the
|
priority order:
|
||||||
§15.0 fuel resolves to a gas/LPG carrier — the same combi/boiler
|
1. §15.0 "Water Heating Fuel Type" when it resolves to a gas/LPG
|
||||||
heats space + water, so §15.0 names the boiler's carrier. Returns
|
carrier — the same combi/boiler heats space + water, so §15.0 names
|
||||||
None otherwise (non-gas-boiler code, or §15.0 lodges a non-gas fuel
|
the boiler's carrier and disambiguates mains-gas-vs-LPG precisely.
|
||||||
such as an electric immersion), leaving the caller to strict-raise.
|
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
|
Spec: SAP 10.2 Table 4b "Seasonal efficiency for gas and liquid fuel
|
||||||
boilers" (PDF p.168) — rows 101-119 are gas-family boilers.
|
boilers" (PDF p.168) — rows 101-119 are gas-family boilers.
|
||||||
"""
|
"""
|
||||||
if (
|
if sap_main_heating_code not in _GAS_BOILER_SAP_MAIN_HEATING_CODES:
|
||||||
sap_main_heating_code in _GAS_BOILER_SAP_MAIN_HEATING_CODES
|
return None
|
||||||
and water_heating_fuel_code in _GAS_LPG_MAIN_FUEL_CODES
|
if water_heating_fuel_code in _GAS_LPG_MAIN_FUEL_CODES:
|
||||||
):
|
|
||||||
return water_heating_fuel_code
|
return water_heating_fuel_code
|
||||||
|
if main_gas:
|
||||||
|
return _MAINS_GAS_MAIN_FUEL_CODE
|
||||||
return None
|
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
|
# 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
|
# "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
|
# combi, EES "BGW"); the §15.0 "Water Heating Fuel Type" names the
|
||||||
# carrier because the same combi/boiler heats space + water. Adopt it
|
# carrier because the same combi/boiler heats space + water. When the
|
||||||
# only when it resolves to a gas/LPG fuel, so a regular boiler paired
|
# boiler instead pairs with a SEPARATE electric immersion (§15.0
|
||||||
# with an electric immersion (where §15.0 lodges "Electricity") still
|
# lodges "Electricity"), the §14.2 Meters "Main gas: Yes" flag is the
|
||||||
# strict-raises rather than mis-billing the gas boiler as electric.
|
# authoritative carrier signal → mains gas. Without either, the gas
|
||||||
|
# boiler still strict-raises rather than being mis-billed.
|
||||||
if main_fuel_int is None:
|
if main_fuel_int is None:
|
||||||
main_fuel_int = _elmhurst_gas_boiler_main_fuel(
|
main_fuel_int = _elmhurst_gas_boiler_main_fuel(
|
||||||
mh.main_heating_sap_code, water_heating_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
|
# Solid-fuel main heating: SAP code rows 150-160 (open / closed
|
||||||
# room heaters with boiler) and 600-636 (independent solid-fuel
|
# room heaters with boiler) and 600-636 (independent solid-fuel
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue