From 789103af27ba959d105818cda00dc0e412246039 Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Wed, 24 Jun 2026 10:47:33 +0000 Subject: [PATCH] Map full-SAP opening-type 6 (rooflight) as a roof window 5 modelling_e2e cohort certs (full-SAP 19.1.0) were skipped with "unmapped API sap_opening_type code: 6". Code 6 is "Ext Rooflight"; SAP 10.2 treats roof windows and rooflights as the same inclined-glazing family. Fix: add 6 to the known opening-type taxonomy and route it onto the roof-window path (`sap_roof_windows`) alongside code 5, via a `_SAP_ROOF_WINDOW_TYPES` {5, 6} set. Genuinely-unknown codes (e.g. 99) still raise. Cert 9878-3908-6309-6714-8200 now maps + calculates (sap 81). Regression test: a type-6 opening maps onto sap_roof_windows. Co-Authored-By: Claude Opus 4.8 (1M context) --- datatypes/epc/domain/mapper.py | 8 ++++++-- .../epc/domain/tests/test_from_sap_schema.py | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index 1658383f..67a4472c 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -56,8 +56,12 @@ from datatypes.epc.schema.sap_schema_17_1 import ( # full-SAP opening-type codes: 1/2/3 = door, 4 = window, 5 = roof window. _SAP_OPENING_TYPE_WINDOW: Final[int] = 4 _SAP_OPENING_TYPE_ROOF_WINDOW: Final[int] = 5 +# 6 = "Ext Rooflight" (seen on full-SAP 19.1.0 certs) — SAP 10.2 treats roof +# windows and rooflights as the same inclined-glazing family, so model it on the +# roof-window path alongside code 5. +_SAP_ROOF_WINDOW_TYPES: Final[frozenset[int]] = frozenset({5, 6}) _SAP_OPENING_TYPE_DOORS: Final[frozenset[int]] = frozenset({1, 2, 3}) -_SAP_KNOWN_OPENING_TYPES: Final[frozenset[int]] = frozenset({1, 2, 3, 4, 5}) +_SAP_KNOWN_OPENING_TYPES: Final[frozenset[int]] = frozenset({1, 2, 3, 4, 5, 6}) # full-SAP wall_type codes: 1/2/3 = external (exposed), 4 = party, 5 = internal. _SAP_WALL_TYPES_EXPOSED: Final[frozenset[int]] = frozenset({1, 2, 3}) _SAP_WALL_TYPE_PARTY: Final[int] = 4 @@ -864,7 +868,7 @@ class EpcPropertyDataMapper: for bp in schema.sap_building_parts: for op in bp.sap_openings: ot = types.get(op.type) - if ot is None or ot.type != _SAP_OPENING_TYPE_ROOF_WINDOW: + if ot is None or ot.type not in _SAP_ROOF_WINDOW_TYPES: continue roof_windows.append( SapRoofWindow( diff --git a/datatypes/epc/domain/tests/test_from_sap_schema.py b/datatypes/epc/domain/tests/test_from_sap_schema.py index 4cc08213..c35b59db 100644 --- a/datatypes/epc/domain/tests/test_from_sap_schema.py +++ b/datatypes/epc/domain/tests/test_from_sap_schema.py @@ -245,6 +245,22 @@ class TestFromSapSchema17_1RoofWindows: result = self._map("sap_17_1.json") assert not result.sap_roof_windows + def test_rooflight_opening_type_6_maps_as_roof_window(self) -> None: + # SAP-19.1.0 certs lodge rooflights as opening-type 6 ("Ext Rooflight"). + # SAP 10.2 treats roof windows and rooflights as the same inclined-glazing + # family, so code 6 maps onto sap_roof_windows like code 5 — previously it + # raised UnmappedApiCode and skipped the whole cert. + data = load("sap_17_1_house.json") + for ot in data["sap_opening_types"]: + if ot["type"] == 5: + ot["type"] = 6 + schema = from_dict(SapSchema17_1, data) + + result = EpcPropertyDataMapper.from_sap_schema_17_1(schema) + + assert result.sap_roof_windows is not None + assert len(result.sap_roof_windows) == 2 + class TestFromSapSchema17_1UnknownOpeningType: """Slice 4d (D2): an opening-type code outside {1,2,3 door, 4 window,