diff --git a/domain/sap10_calculator/rdsap/cert_to_inputs.py b/domain/sap10_calculator/rdsap/cert_to_inputs.py index d4185d59..7afbd5d4 100644 --- a/domain/sap10_calculator/rdsap/cert_to_inputs.py +++ b/domain/sap10_calculator/rdsap/cert_to_inputs.py @@ -103,6 +103,7 @@ from domain.sap10_calculator.tables.table_12a import ( rdsap_tariff_for_cert, space_heating_high_rate_fraction, tariff_from_meter_type, + water_heating_high_rate_fraction, ) from domain.sap10_calculator.tables.table_32 import ( additional_standing_charges_gbp, @@ -2231,6 +2232,7 @@ def _hot_water_fuel_cost_gbp_per_kwh( tariff: Tariff, prices: PriceTable, *, + water_heating_code: Optional[int] = None, inherit_main_for_community_heating: bool = False, ) -> float: """Hot water bills at the *water-heating* fuel's rate. When the @@ -2239,8 +2241,16 @@ def _hot_water_fuel_cost_gbp_per_kwh( water fuel is a non-electric fuel (gas / oil / LPG), tariff is not consulted — those fuels are single-rate per Table 32. For cert 000565 HW routes to gas combi via WHC 914 → tariff branch - not taken. TODO: Table 12a Grid 1 WH high-rate-fraction split for - electric WH on off-peak (currently uses 100% low rate). + not taken. + + HP-DHW exception: when DHW is heated by the main system (WHC + ∈ {901, 902, 914}) and that main is a PCDB Table 362 heat pump, the + HW bills per SAP 10.2 Table 12a Grid 1 WH column (PDF p.191) — the + ASHP/GSHP-from-database row carries a 0.70 high-rate fraction at + 7-hour and 10-hour, NOT 100% off-peak low rate. Electric IMMERSION + (WHC 903) is a different Table 12a row (off-peak immersion 0.17 / + Table 13) and stays on the 100%-low-rate fallback until that slice + lands. `inherit_main_for_community_heating`: per S0380.173, when WHC ∈ {901, 902, 914} AND main is a heat network, ignore the cert- @@ -2253,6 +2263,18 @@ def _hot_water_fuel_cost_gbp_per_kwh( return _fuel_cost_gbp_per_kwh(main, prices) water_electric = _is_electric_water(water_heating_fuel) if water_electric and tariff is not Tariff.STANDARD: + if ( + water_heating_code in _WATER_INHERIT_FROM_MAIN_CODES + and main is not None + and main.main_heating_index_number is not None + and heat_pump_record(main.main_heating_index_number) is not None + ): + high_rate, low_rate = _tariff_high_low_rates_p_per_kwh(tariff) + high_frac = water_heating_high_rate_fraction( + Table12aSystem.ASHP_APP_N, tariff + ) + blended = high_frac * high_rate + (1.0 - high_frac) * low_rate + return blended * _PENCE_TO_GBP return _off_peak_low_rate_gbp_per_kwh(tariff) if water_heating_fuel is not None: return prices.unit_price_p_per_kwh(water_heating_fuel) * _PENCE_TO_GBP @@ -6963,6 +6985,7 @@ def cert_to_inputs( _water_heating_main(epc), _rdsap_tariff(epc), prices, + water_heating_code=epc.sap_heating.water_heating_code, inherit_main_for_community_heating=_community_hw_inherit, ) hw_co2_factor = _hot_water_co2_factor_kg_per_kwh( diff --git a/tests/domain/sap10_calculator/rdsap/test_cert_to_inputs.py b/tests/domain/sap10_calculator/rdsap/test_cert_to_inputs.py index 627cc2d9..a21ee098 100644 --- a/tests/domain/sap10_calculator/rdsap/test_cert_to_inputs.py +++ b/tests/domain/sap10_calculator/rdsap/test_cert_to_inputs.py @@ -3212,6 +3212,48 @@ def test_space_heating_electric_room_heater_off_peak_bills_at_direct_acting_high assert abs(gas_rate - 0.0550) > 1e-6 +def test_hot_water_from_pcdb_heat_pump_bills_at_app_n_wh_high_rate() -> None: + # Arrange — when DHW is heated by the main heat pump (WHC 901/902/914 + # "from main system") and that main carries a PCDB Table 362 record, + # SAP 10.2 Table 12a Grid 1 WH column (PDF p.191) bills it on the + # ASHP/GSHP-from-database row: 0.70 high-rate fraction at 7-hour and + # 10-hour. `_hot_water_fuel_cost_gbp_per_kwh` previously billed any + # electric off-peak HW at 100% low rate (its TODO), over-crediting the + # HP-DHW cat-4 cluster. Electric IMMERSION (WHC 903) is a different + # Table 12a row (off-peak immersion 0.17 / Table 13) and must stay on + # the 100%-low-rate fallback here. + from domain.sap10_calculator.tables.table_12a import Tariff + from domain.sap10_calculator.rdsap.cert_to_inputs import ( + _hot_water_fuel_cost_gbp_per_kwh, # pyright: ignore[reportPrivateUsage] + ) + pcdb_heat_pump_main = MainHeatingDetail( + has_fghrs=False, + main_fuel_type=29, # electricity (heat pump), API enum + heat_emitter_type=1, + emitter_temperature=0, + main_heating_control=2210, + main_heating_category=4, + sap_main_heating_code=None, + main_heating_index_number=104351, # PCDB Table 362 heat pump + ) + + # Act — DHW from the main HP (WHC 901) vs a separate electric + # immersion (WHC 903), both on a 10-hour off-peak tariff. + rate_from_hp = _hot_water_fuel_cost_gbp_per_kwh( + 29, pcdb_heat_pump_main, Tariff.TEN_HOUR, SAP_10_2_SPEC_PRICES, + water_heating_code=901, + ) + rate_immersion = _hot_water_fuel_cost_gbp_per_kwh( + 29, pcdb_heat_pump_main, Tariff.TEN_HOUR, SAP_10_2_SPEC_PRICES, + water_heating_code=903, + ) + + # Assert — HP-DHW: 0.70 × 14.68 p + 0.30 × 7.50 p = 12.526 p; immersion + # stays at the 10-hour low rate 7.50 p (£0.0750). + assert abs(rate_from_hp - 0.12526) <= 1e-6 + assert abs(rate_immersion - 0.0750) <= 1e-6 + + def test_space_heating_pcdb_heat_pump_without_sap_code_bills_at_app_n_high_rate() -> None: # Arrange — an API-path heat pump resolves via its PCDB Table 362 # index alone (data_source=1, no Table-4a SAP code lodged), so