diff --git a/domain/sap10_calculator/tables/pcdb/parser.py b/domain/sap10_calculator/tables/pcdb/parser.py index 06041443..8e017693 100644 --- a/domain/sap10_calculator/tables/pcdb/parser.py +++ b/domain/sap10_calculator/tables/pcdb/parser.py @@ -182,6 +182,13 @@ class HeatPumpRecord: `max_output_kw` (spec §A.23 field 30) is the PSR-denominator per PDF p.100 line 5946 ("maximum nominal output of the package"). + `heating_duration_code` (format-465 position 48) encodes the + package's daily heating duration per SAP 10.2 Appendix N3.5 (PDF + p.105 line 6099): "24", "16", "9", or "V" (Variable). Drives the + extended-heating-schedule day allocation via Table N4/N5. Per + footnote 48, modern records always lodge "V"; the fixed durations + are retained for legacy purposes. + `psr_groups` carries the PSR-dependent efficiency table (up to 14 rows) used by SAP 10.2 Appendix N3.6 (space heating) and N3.7(a) (water heating), interpolated at the dwelling's PSR per spec PDF @@ -199,6 +206,7 @@ class HeatPumpRecord: vessel_heat_loss_kwh_per_day: Optional[float] vessel_heat_exchanger_area_m2: Optional[float] max_output_kw: Optional[float] + heating_duration_code: Optional[str] psr_groups: tuple[PsrEfficiencyGroup, ...] raw: tuple[str, ...] @@ -216,6 +224,10 @@ _HP_IDX_VESSEL_VOLUME_L: Final[int] = 24 _HP_IDX_VESSEL_HEAT_LOSS_KWH_PER_DAY: Final[int] = 25 _HP_IDX_VESSEL_HEAT_EXCHANGER_AREA_M2: Final[int] = 26 _HP_IDX_MAX_OUTPUT_KW: Final[int] = 47 +# Format 465 position 48 — daily heating duration code per SAP 10.2 +# Appendix N3.5 (PDF p.105 line 6099). Cohort ground-truth: "V" lodged +# on Mitsubishi PUZ-WM50VHA (104568) and Daikin EDLQ05CAV3 (102421). +_HP_IDX_HEATING_DURATION_CODE: Final[int] = 48 # Format 465 PSR-group block: idx[58] is the group count; groups start # at idx[59], 9 fields wide, with PSR / η_space,1 / η_water,3 at the @@ -315,6 +327,7 @@ def parse_heat_pump_row_raw(raw: tuple[str, ...]) -> HeatPumpRecord: def at(idx: int) -> str: return raw[idx] if idx < len(raw) else "" + duration_raw = at(_HP_IDX_HEATING_DURATION_CODE).strip() return HeatPumpRecord( pcdb_id=int(raw[0]), brand_name=at(_HP_IDX_BRAND_NAME), @@ -331,6 +344,7 @@ def parse_heat_pump_row_raw(raw: tuple[str, ...]) -> HeatPumpRecord: at(_HP_IDX_VESSEL_HEAT_EXCHANGER_AREA_M2) ), max_output_kw=_parse_optional_float(at(_HP_IDX_MAX_OUTPUT_KW)), + heating_duration_code=duration_raw if duration_raw else None, psr_groups=_parse_psr_groups(raw), raw=raw, ) diff --git a/domain/sap10_calculator/tests/test_pcdb_table_362_lookup.py b/domain/sap10_calculator/tests/test_pcdb_table_362_lookup.py index c9bbfda0..f3f8ffeb 100644 --- a/domain/sap10_calculator/tests/test_pcdb_table_362_lookup.py +++ b/domain/sap10_calculator/tests/test_pcdb_table_362_lookup.py @@ -57,6 +57,24 @@ def test_heat_pump_record_returns_verified_mitsubishi_ecodan_104568_header() -> assert record.max_output_kw == 4.39 +def test_heat_pump_record_heating_duration_code_for_104568_is_variable() -> None: + """SAP 10.2 Appendix N3.5 (PDF p.106) — extended heating duration is + sourced from PCDB Table 362 field "Daily heating duration", encoded + as "24" / "16" / "9" / "V" (Variable). Per spec PDF p.105 line 6099 + + footnote 48, modern PCDB records always lodge "V"; the fixed + durations are retained for legacy purposes. + + Mitsubishi PUZ-WM50VHA (104568) lodges duration "V" at format-465 + position 48 — confirmed by inspecting the raw row in pcdb10.dat. + """ + # Arrange / Act + record = heat_pump_record(104568) + + # Assert + assert record is not None + assert record.heating_duration_code == "V" + + def test_heat_pump_record_returns_none_for_unknown_pcdb_id() -> None: """An index number not in Table 362 returns None so callers can fall back to a Table 4a heat-pump category default."""