diff --git a/domain/sap10_calculator/rdsap/cert_to_inputs.py b/domain/sap10_calculator/rdsap/cert_to_inputs.py index a2b988c9..6a4bfbba 100644 --- a/domain/sap10_calculator/rdsap/cert_to_inputs.py +++ b/domain/sap10_calculator/rdsap/cert_to_inputs.py @@ -2082,23 +2082,6 @@ def _off_peak_low_rate_gbp_per_kwh(tariff: Tariff) -> float: return low * _PENCE_TO_GBP -def _off_peak_low_rate_gbp_per_kwh_via_meter_heuristic(meter_type: object) -> float: - """Off-peak low-rate £/kWh for callsites that detect off-peak via the - `_is_off_peak_meter` heuristic (RdSAP meter code 3 = Unknown is - treated as off-peak for electric end-uses; see _is_off_peak_meter - docstring). When the meter resolves to a known off-peak tariff - (codes 1/4/5), bills at that tariff's Table 32 low rate; when the - meter resolves to STANDARD (codes 2 = Single, 3 = Unknown), falls - back to the SEVEN_HOUR rate (5.50, Table 32 code 31). Codifies the - heuristic that pre-S0380.138 was baked into the literal - `prices.e7_low_rate_p_per_kwh` constant.""" - tariff = tariff_from_meter_type(meter_type) - if tariff is Tariff.STANDARD: - _high, low = _tariff_high_low_rates_p_per_kwh(Tariff.SEVEN_HOUR) - return low * _PENCE_TO_GBP - return _off_peak_low_rate_gbp_per_kwh(tariff) - - # Tariff → (high_rate_fuel_code, low_rate_fuel_code) for the SAP 10.2 # Table 12d (CO2) / Table 12e (PE) monthly factors. Mirror of the # Table 32 cost-rates dict above: 7-hour and 10-hour tariffs split into @@ -2660,30 +2643,25 @@ def _pv_export_credit_gbp_per_kwh() -> float: def _pv_dwelling_import_price_gbp_per_kwh( - meter_type: object, prices: PriceTable + tariff: Tariff, prices: PriceTable ) -> float: """PV dwelling-consumption price per kWh per SAP 10.2 Appendix M1 §6 - (p.94): "apply the normal import electricity price to PV energy used - within the dwelling". Onsite-consumed PV displaces grid IMPORTS, so - it bills at the standard electricity import tariff (Table 32 code 30 - under the RdSAP10 amendment per ADR-0010 §10 = 13.19 p/kWh — the - same rate `_fuel_cost`'s `other_uses_p_per_kwh` already pays for - lighting/pumps/fans, and crucially the same rate Table 32 code 60 - pays for the EXPORT credit. In Table 32 these collapse to a single - 13.19 p value, so the IMPORT/EXPORT split is mathematically - equivalent to the legacy single-rate-EXPORT credit — but the - distinction matters when an off-peak tariff lands: §6 then directs - a weighted Table 12a high/low rate, deferred until the first off- - peak cost cert ships.""" - if _is_off_peak_meter(meter_type, fuel_is_electric=True): - # Off-peak weighted Table 12a rate (deferred — `_fuel_cost` - # short-circuits Tariff != STANDARD before reaching this path). - # Routes through the meter-heuristic helper so an Unknown-meter - # cert (code 3 = "treat as off-peak for electric end-uses" per - # _is_off_peak_meter) falls back to the SEVEN_HOUR low rate - # rather than raising on STANDARD. - return _off_peak_low_rate_gbp_per_kwh_via_meter_heuristic(meter_type) - return table_32_unit_price_p_per_kwh(30) * _PENCE_TO_GBP + (PDF p.94, lines 5510-5513): "apply the normal import electricity + price to PV energy used within the dwelling … In the case of the + former, use a weighted average of high and low rates (Table 12a)." + + Onsite-consumed PV displaces the dwelling's "all other uses" + electricity (lighting / appliances / pumps), so it bills at the same + Table 12a Grid 2 ALL_OTHER_USES rate `_other_fuel_cost_gbp_per_kwh` + derives — a STANDARD-tariff dwelling pays the flat Table 32 code 30 + 13.19 p/kWh (unchanged from the legacy single-rate path), while an + off-peak dwelling pays the weighted high/low blend (7-hour: + 0.90 × 15.29 + 0.10 × 5.50 = 14.311 p/kWh, matching worksheet + (252)/(269) "PV used in dwelling" on case 19). + + Pre-S0380.233 the off-peak branch returned the bare low rate + (5.50 p/kWh), under-crediting onsite PV on every off-peak cert.""" + return _other_fuel_cost_gbp_per_kwh(tariff, prices) def _other_fuel_cost_gbp_per_kwh( @@ -6137,9 +6115,7 @@ def _fuel_cost( pv_dwelling_kwh_per_yr=pv_dwelling_kwh_per_yr, pv_exported_kwh_per_yr=pv_exported_kwh_per_yr, pv_dwelling_import_price_gbp_per_kwh=( - _pv_dwelling_import_price_gbp_per_kwh( - epc.sap_energy_source.meter_type, prices - ) + _pv_dwelling_import_price_gbp_per_kwh(_rdsap_tariff(epc), prices) ), ) @@ -6976,7 +6952,7 @@ def cert_to_inputs( pv_generation_kwh_per_yr=_pv_generation_kwh_per_yr(epc, climate), pv_export_credit_gbp_per_kwh=_pv_export_credit_gbp_per_kwh(), pv_dwelling_import_price_gbp_per_kwh=_pv_dwelling_import_price_gbp_per_kwh( - epc.sap_energy_source.meter_type, prices + _rdsap_tariff(epc), prices ), # SAP 10.2 Appendix M1 §3-4 PV split — the cascade applies # IMPORT PEF (Table 12) to the onsite portion and EXPORT PEF 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 bde4bb44..ad5b3553 100644 --- a/tests/domain/sap10_calculator/rdsap/test_cert_to_inputs.py +++ b/tests/domain/sap10_calculator/rdsap/test_cert_to_inputs.py @@ -61,6 +61,7 @@ from domain.sap10_calculator.rdsap.cert_to_inputs import ( _main_floor_u_value, # pyright: ignore[reportPrivateUsage] _main_space_heating_high_rate_fraction, # pyright: ignore[reportPrivateUsage] _other_fuel_cost_gbp_per_kwh, # pyright: ignore[reportPrivateUsage] + _pv_dwelling_import_price_gbp_per_kwh, # pyright: ignore[reportPrivateUsage] _pv_eligible_demand_monthly_kwh, # pyright: ignore[reportPrivateUsage] _primary_loss_applies, # pyright: ignore[reportPrivateUsage] _rdsap_extract_fans_default, # pyright: ignore[reportPrivateUsage] @@ -1852,6 +1853,28 @@ def test_pv_eligible_demand_excludes_low_rate_main_space_heating() -> None: assert abs(included[m] - (base[m] + secondary[m] + main_1[m])) <= 1e-9 +def test_pv_dwelling_import_price_blends_high_low_on_off_peak() -> None: + # Arrange — SAP 10.2 Appendix M1 §6 (PDF p.94, lines 5510-5513): PV + # used in the dwelling is credited at "a weighted average of high and + # low rates (Table 12a)". On a 7-hour tariff the ALL_OTHER_USES blend + # is 0.90 × 15.29 + 0.10 × 5.50 = 14.311 p/kWh (worksheet case 19 + # (252) "PV used in dwelling" = 14.3110). STANDARD tariff has no + # split → flat Table 32 code 30 = 13.19 p/kWh (unchanged). + from domain.sap10_calculator.tables.table_12a import Tariff + + # Act + off_peak = _pv_dwelling_import_price_gbp_per_kwh( + Tariff.SEVEN_HOUR, SAP_10_2_SPEC_PRICES + ) + standard = _pv_dwelling_import_price_gbp_per_kwh( + Tariff.STANDARD, SAP_10_2_SPEC_PRICES + ) + + # Assert + assert abs(off_peak - 0.14311) <= 1e-6 + assert abs(standard - 0.1319) <= 1e-6 + + def test_is_off_peak_meter_recognises_bare_18_hour_lodging() -> None: # Arrange — RdSAP 10 §17 page 85 row 10-2 lodges 18-hour meter # as the bare "18-hour" or "18 Hour" form (Elmhurst Summary §14.2