diff --git a/domain/sap10_calculator/rdsap/cert_to_inputs.py b/domain/sap10_calculator/rdsap/cert_to_inputs.py index 5298b0c5..29730560 100644 --- a/domain/sap10_calculator/rdsap/cert_to_inputs.py +++ b/domain/sap10_calculator/rdsap/cert_to_inputs.py @@ -385,7 +385,9 @@ def _table_4f_circulation_pump_kwh(main: Optional[MainHeatingDetail]) -> float: 2 → 41 kWh (2013 or later) Table 4f footnote a) then multiplies the row by 1.3 when the room - thermostat is absent (control code 2101 / 2102). + thermostat is absent — the same "no room thermostat" criterion as the + interlock rule, i.e. `_BOILER_NO_ROOM_THERMOSTAT_CONTROL_CODES` + (2101 / 2102 / 2107 / 2111; bypass and TRVs are not a room thermostat). """ if not _is_wet_boiler_main(main): return 0.0 @@ -1267,22 +1269,32 @@ _CONTROL_TYPE_BY_CODE: Final[dict[int, int]] = { } -# SAP 10.2 Table 4e Group 1 (PDF p.171) — boiler control codes providing -# NO thermostatic control of room temperature, i.e. no room thermostat -# ("No time or thermostatic control of room temperature" 2101 / -# "Programmer, no room thermostat" 2102 — the two Group-1 rows carrying -# the "+0.6 °C / Table 4c(2)" annotation). Per RdSAP 10 §3 (PDF p.57) -# boiler interlock is "assumed present if there is a room thermostat and -# (for stored hot water systems heated by the boiler) a cylinder -# thermostat. Otherwise not interlocked." A gas/liquid-fuel boiler under -# one of these controls therefore has NO boiler interlock regardless of -# the cylinder thermostat, triggering the Table 4c(2) (PDF p.169) "No -# thermostatic control of room temperature – regular boiler" -5pp Space -# + DHW seasonal-efficiency adjustment. The combi rows of Table 4c(2) -# take Space -5 / DHW 0; the DHW leg is gated separately on a cylinder -# being present (regular boiler) at the call site. +# SAP 10.2 Table 4e Group 1 (PDF p.171) — boiler control codes with NO +# boiler interlock because they lack a room thermostat (or an equivalent +# device). SAP 10.2 §9.4.11 (PDF p.66) is explicit: "A boiler system with +# no room thermostat (or a device equivalent in this context, such as a +# flow switch or boiler energy manager), even if there is a cylinder +# thermostat, must be considered as having no interlock", and "TRVs alone +# (other than some communicating TRVs) do not perform the boiler interlock +# function". A *fixed bypass* likewise provides no interlock — it exists to +# keep water circulating when the TRVs close. The Group-1 rows without a +# room thermostat / flow switch / boiler energy manager are therefore: +# 2101 "No time or thermostatic control of room temperature" +# 2102 "Programmer, no room thermostat" +# 2107 "Programmer, TRVs and bypass" ← bypass ≠ interlock +# 2111 "TRVs and bypass" ← bypass ≠ interlock +# (2108 "Programmer, TRVs and flow switch" and 2109 "… boiler energy +# manager" carry an interlock-equivalent device, so they are INTERLOCKED +# and excluded; 2103-2106/2113 all carry a room thermostat.) Each of these +# triggers the Table 4c(2) (PDF p.169) "No thermostatic control of room +# temperature – regular boiler" -5pp Space + DHW seasonal-efficiency +# adjustment. The combi rows of Table 4c(2) take Space -5 / DHW 0; the DHW +# leg is gated separately on a cylinder being present at the call site. +# NB this is the interlock criterion only — the separate "+0.6 °C" Table 4e +# temperature adjustment applies to 2101/2102 alone (it lives in +# `_CONTROL_TEMPERATURE_ADJUSTMENT_BY_CODE`, where 2107/2111 stay at 0.0). _BOILER_NO_ROOM_THERMOSTAT_CONTROL_CODES: Final[frozenset[int]] = frozenset( - {2101, 2102} + {2101, 2102, 2107, 2111} ) 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 08a25d75..a00fca28 100644 --- a/tests/domain/sap10_calculator/rdsap/test_cert_to_inputs.py +++ b/tests/domain/sap10_calculator/rdsap/test_cert_to_inputs.py @@ -4720,6 +4720,44 @@ def test_table_4c_no_boiler_interlock_applies_minus_5_dhw_adjustment_when_cylind ) +def test_controls_2107_2111_count_as_no_room_thermostat_per_sap_9_4_11() -> None: + # Arrange — SAP 10.2 §9.4.11 (PDF p.66): a boiler with no room + # thermostat (or an equivalent device — flow switch / boiler energy + # manager) has no interlock; "TRVs alone ... do not perform the boiler + # interlock function" and a fixed bypass exists precisely to keep water + # circulating when the TRVs close. Control 2107 ("Programmer, TRVs and + # bypass") and 2111 ("TRVs and bypass") therefore carry the same + # no-room-thermostat treatment as 2101/2102 — including the Table 4f + # footnote a) ×1.3 circulation-pump uplift — which they previously + # missed (the set held only 2101/2102). Control 2106 ("Programmer, room + # thermostat and TRVs") HAS a room thermostat → interlock → no uplift. + # A 2013+ wet gas boiler pumps 41 kWh (Table 4f); ×1.3 = 53.3. + import dataclasses + + from domain.sap10_calculator.rdsap.cert_to_inputs import _table_4f_circulation_pump_kwh # pyright: ignore[reportPrivateUsage] + + base = _gas_boiler_detail() # cat-2 wet boiler + no_stat_2107 = dataclasses.replace( + base, main_heating_control=2107, central_heating_pump_age=2, + ) + no_stat_2111 = dataclasses.replace( + base, main_heating_control=2111, central_heating_pump_age=2, + ) + with_stat_2106 = dataclasses.replace( + base, main_heating_control=2106, central_heating_pump_age=2, + ) + + # Act + pump_2107 = _table_4f_circulation_pump_kwh(no_stat_2107) # pyright: ignore[reportPrivateUsage] + pump_2111 = _table_4f_circulation_pump_kwh(no_stat_2111) # pyright: ignore[reportPrivateUsage] + pump_2106 = _table_4f_circulation_pump_kwh(with_stat_2106) # pyright: ignore[reportPrivateUsage] + + # Assert — bypass/TRVs-only controls get the ×1.3 uplift; 2106 does not. + assert abs(pump_2107 - 41.0 * 1.3) <= 1e-9 + assert abs(pump_2111 - 41.0 * 1.3) <= 1e-9 + assert abs(pump_2106 - 41.0) <= 1e-9 + + def test_sap_9_4_11_no_boiler_interlock_applies_minus_5_pcdb_space_heating_when_main_is_gas_oil_boiler_with_cylinder_no_thermostat() -> None: """SAP 10.2 §9.4.11 (PDF p.30) "Boiler interlock":