diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index 26704922..51cffde3 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -72,10 +72,9 @@ _SAP_KNOWN_WALL_TYPES: Final[frozenset[int]] = frozenset({1, 2, 3, 4, 5}) # RdSAP age band M. Used only for that fallback + secondary age-band logic. _SAP_DEFAULT_AGE_BAND: Final[str] = "M" # full-SAP standard `ventilation_type` code → MechanicalVentilationKind name -# (None = natural). MVHR (7) is deferred to natural — like the RdSAP API path -# (_API_MECHANICAL_VENTILATION_TO_KIND code 4), its (24a) formula needs the -# PCDB heat-recovery efficiency, not yet plumbed; mapping to MVHR with a null -# efficiency would mis-model it as MV. None of the corpus lodges 7. +# (None = natural). MVHR (code 8) uses the same PCDB Table 4g fallback +# (66% × 0.70 in-use factor) as the RdSAP-21 API path when no PCDF index is +# lodged — the cascade handles a null index gracefully. _SAP_VENTILATION_TYPE_TO_MV_KIND: Final[Dict[int, Optional[str]]] = { 1: None, # natural (with intermittent extract fans) 2: None, # passive stack — treated as natural @@ -83,8 +82,10 @@ _SAP_VENTILATION_TYPE_TO_MV_KIND: Final[Dict[int, Optional[str]]] = { 4: "EXTRACT_OR_PIV_OUTSIDE", # positive input from outside 5: "EXTRACT_OR_PIV_OUTSIDE", # mechanical extract, centralised (MEV c) 6: "EXTRACT_OR_PIV_OUTSIDE", # mechanical extract, decentralised (MEV dc) - 7: None, # MVHR — deferred (efficiency not plumbed) - 8: "MV", # balanced mechanical, no heat recovery + 7: "MV", # balanced without heat recovery (MV) + 8: "MVHR", # balanced with heat recovery (MVHR) + 9: None, # natural with extract fans and/or passive vents (legacy) + 10: None, # natural with extract fans and passive vents } # rdsap_uvalues WALL_CAVITY = 4 (D7 fallback; U comes from the description). _SAP_DEFAULT_WALL_CONSTRUCTION: Final[int] = 4 diff --git a/datatypes/epc/domain/tests/test_from_sap_schema.py b/datatypes/epc/domain/tests/test_from_sap_schema.py index 28c86599..d4b3330b 100644 --- a/datatypes/epc/domain/tests/test_from_sap_schema.py +++ b/datatypes/epc/domain/tests/test_from_sap_schema.py @@ -410,6 +410,31 @@ class TestFromSapSchema17_1Ventilation: with pytest.raises(UnmappedApiCode): EpcPropertyDataMapper.from_sap_schema_17_1(schema) + @pytest.mark.parametrize("code", [9, 10]) + def test_natural_ventilation_codes_9_and_10_map_to_none(self, code: int) -> None: + data = load("sap_17_1.json") + data["sap_ventilation"]["ventilation_type"] = code + schema = from_dict(SapSchema17_1, data) + result = EpcPropertyDataMapper.from_sap_schema_17_1(schema) + assert result.sap_ventilation is not None + assert result.sap_ventilation.mechanical_ventilation_kind is None + + def test_ventilation_type_7_is_mv(self) -> None: + data = load("sap_17_1.json") + data["sap_ventilation"]["ventilation_type"] = 7 + schema = from_dict(SapSchema17_1, data) + result = EpcPropertyDataMapper.from_sap_schema_17_1(schema) + assert result.sap_ventilation is not None + assert result.sap_ventilation.mechanical_ventilation_kind == "MV" + + def test_ventilation_type_8_is_mvhr(self) -> None: + data = load("sap_17_1.json") + data["sap_ventilation"]["ventilation_type"] = 8 + schema = from_dict(SapSchema17_1, data) + result = EpcPropertyDataMapper.from_sap_schema_17_1(schema) + assert result.sap_ventilation is not None + assert result.sap_ventilation.mechanical_ventilation_kind == "MVHR" + class TestFromSapSchema17_1Heating: """Slice D6: full-SAP sap_heating (differing field names) maps onto the