mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-19 17:03:02 +00:00
The heating Recommendation Generator (HHRSH first). Emits one "Heating & Hot Water" Recommendation whose competing whole-system bundles the Optimiser picks from; this slice builds the high-heat-retention storage Option. Its overlay is the absolute HHR end-state (Table 4a code 409 + control 2404 + dual off-peak meter + off-peak electric cylinder), pinned against the relodged after-cert in the next slice. Eligibility translates legacy is_high_heat_retention_valid to structured predicates (electric or off-gas main, not already HHR/heat-pump). mains_gas and the heat emitter are unchanged by the measure, so unset. ADR-0024. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
103 lines
4.3 KiB
Python
103 lines
4.3 KiB
Python
"""The heating Recommendation Generator.
|
|
|
|
Detects a dwelling whose heating system should be replaced and emits one
|
|
"Heating & Hot Water" Recommendation of competing whole-system bundles — the
|
|
Optimiser picks at most one (ADR-0024). Each bundle is a whole-system change:
|
|
main heating + controls + fuel + meter + the implied hot water, folded into one
|
|
Measure Option's `HeatingOverlay`. Hot water is never a separate competing
|
|
measure; the legacy heating-vs-HW split double-counted.
|
|
|
|
This slice covers the high-heat-retention storage (HHRSH) bundle; the ASHP and
|
|
boiler bundles land in later slices. Detection + pricing only — impact is
|
|
produced by scoring (ADR-0016).
|
|
"""
|
|
|
|
from typing import Optional
|
|
|
|
from datatypes.epc.domain.epc_property_data import EpcPropertyData, MainHeatingDetail
|
|
from domain.modelling.recommendation import Cost, MeasureOption, Recommendation
|
|
from domain.modelling.simulation import EpcSimulation, HeatingOverlay
|
|
from repositories.product.product_repository import ProductRepository
|
|
|
|
_HEATING_SURFACE = "Heating & Hot Water"
|
|
|
|
_HHR_STORAGE_MEASURE_TYPE = "high_heat_retention_storage_heaters"
|
|
|
|
# Electricity main-fuel code (Elmhurst → SAP10 Table 12).
|
|
_ELECTRICITY_FUEL = 30
|
|
# Table 4a SAP main-heating code for high-heat-retention storage heaters; an
|
|
# existing HHR system lodges this already, so it is not re-recommended.
|
|
_HHR_STORAGE_SAP_CODE = 409
|
|
# RdSAP main_heating_category for a heat pump (Table 4a) — an existing heat pump
|
|
# is never downgraded to storage heaters.
|
|
_HEAT_PUMP_CATEGORY = 4
|
|
|
|
# The HHRSH bundle's absolute end-state (ADR-0024): high-heat-retention storage
|
|
# heaters (Table 4a code 409) on a dual off-peak meter, with an off-peak
|
|
# electric immersion hot-water cylinder. Pinned against the relodged after-cert
|
|
# in the cascade tests; `mains_gas` and the heat emitter are unchanged by this
|
|
# measure, so they are not written.
|
|
_HHR_STORAGE_OVERLAY = HeatingOverlay(
|
|
main_fuel_type=_ELECTRICITY_FUEL,
|
|
sap_main_heating_code=_HHR_STORAGE_SAP_CODE,
|
|
main_heating_control=2404,
|
|
water_heating_code=903,
|
|
water_heating_fuel=_ELECTRICITY_FUEL,
|
|
cylinder_size=2,
|
|
cylinder_insulation_type=1,
|
|
cylinder_insulation_thickness_mm=120,
|
|
has_hot_water_cylinder=True,
|
|
meter_type="Dual",
|
|
)
|
|
|
|
|
|
def recommend_heating(
|
|
epc: EpcPropertyData, products: ProductRepository
|
|
) -> Optional[Recommendation]:
|
|
"""Return a "Heating & Hot Water" Recommendation of competing whole-system
|
|
bundles for the dwelling, else None when no bundle is eligible."""
|
|
options: list[MeasureOption] = []
|
|
|
|
hhr_option = _hhr_storage_option(epc, products)
|
|
if hhr_option is not None:
|
|
options.append(hhr_option)
|
|
|
|
if not options:
|
|
return None
|
|
return Recommendation(surface=_HEATING_SURFACE, options=tuple(options))
|
|
|
|
|
|
def _hhr_storage_option(
|
|
epc: EpcPropertyData, products: ProductRepository
|
|
) -> Optional[MeasureOption]:
|
|
"""The high-heat-retention storage bundle, offered for an electrically-heated
|
|
(or off-gas) dwelling that is not already HHR or a heat pump."""
|
|
if not _hhr_storage_eligible(epc):
|
|
return None
|
|
product = products.get(_HHR_STORAGE_MEASURE_TYPE)
|
|
return MeasureOption(
|
|
measure_type=_HHR_STORAGE_MEASURE_TYPE,
|
|
description=(
|
|
"Replace the heating with high heat retention storage heaters on an "
|
|
"off-peak tariff, with an off-peak electric hot-water cylinder"
|
|
),
|
|
overlay=EpcSimulation(heating=_HHR_STORAGE_OVERLAY),
|
|
cost=Cost(
|
|
total=product.unit_cost_per_m2, contingency_rate=product.contingency_rate
|
|
),
|
|
material_id=product.id,
|
|
)
|
|
|
|
|
|
def _hhr_storage_eligible(epc: EpcPropertyData) -> bool:
|
|
"""HHR storage suits an electrically-heated or off-gas dwelling, unless it is
|
|
already HHR or a heat pump (translated from legacy `HeatingRecommender.
|
|
is_high_heat_retention_valid`, which keyed on description strings)."""
|
|
main: MainHeatingDetail = epc.sap_heating.main_heating_details[0]
|
|
if main.sap_main_heating_code == _HHR_STORAGE_SAP_CODE:
|
|
return False
|
|
if main.main_heating_category == _HEAT_PUMP_CATEGORY:
|
|
return False
|
|
off_gas: bool = epc.sap_energy_source is None or not epc.sap_energy_source.mains_gas
|
|
electric_main: bool = main.main_fuel_type == _ELECTRICITY_FUEL
|
|
return electric_main or off_gas
|