diff --git a/backend/documents_parser/elmhurst_extractor.py b/backend/documents_parser/elmhurst_extractor.py index 1bb3b3c2..061d972f 100644 --- a/backend/documents_parser/elmhurst_extractor.py +++ b/backend/documents_parser/elmhurst_extractor.py @@ -1044,6 +1044,19 @@ class ElmhurstSiteNotesExtractor: lines = self._section_lines("14.0 Main Heating1", "14.1 Main Heating2") pct_raw = self._local_val(lines, "Percentage of Heat") pct = int(pct_raw.split()[0]) if pct_raw else 0 + # §14.0 "Main Heating SAP Code" identifies Main 1 by SAP 10.2 + # Table 4a code (e.g. 224 = "Air source heat pump, 2013 or + # later"). PCDB-boiler certs leave this empty / lodge "0" — the + # PCDB index in `PCDF boiler Reference` is the identifier in + # that case. Treat 0 (or absent) as None so the mapper can + # distinguish "no SAP code lodged" from a real Table 4a code. + sap_code_raw = self._local_val(lines, "Main Heating SAP Code") + main_heating_sap_code: Optional[int] = None + if sap_code_raw is not None: + head = sap_code_raw.split()[0] if sap_code_raw.split() else "" + if head.isdigit(): + v = int(head) + main_heating_sap_code = v if v > 0 else None # The "Secondary Heating SapCode" key is lodged inside §14.1 Main # Heating2 — Elmhurst uses the Main-2 block to also carry the # cert's secondary heating system (when one exists). Look for it @@ -1069,6 +1082,7 @@ class ElmhurstSiteNotesExtractor: percentage_of_heat=pct, pcdf_boiler_reference=self._local_val(lines, "PCDF boiler Reference"), heat_pump_age=self._local_val(lines, "Heat pump age"), + main_heating_sap_code=main_heating_sap_code, secondary_heating_sap_code=secondary_code, ) diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index 1e5cec98..963f5abc 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -3826,6 +3826,24 @@ def _map_elmhurst_sap_heating(survey: ElmhurstSiteNotes) -> SapHeating: heat_emitter_int = _elmhurst_heat_emitter_int(mh.heat_emitter) sap_control_int = _elmhurst_sap_control_code(sap_control) main_heating_category = _elmhurst_main_heating_category(mh, pcdb_index) + # Strict-raise mirror of [[unmapped-api-code]] — when Main 1 has + # neither a PCDB boiler reference nor a lodged Table 4a SAP code, + # the mapper has no identifier for the heat source and the cascade + # would silently fall back to the 0.80 gas-boiler default. First + # surfaced on cert 000565 where Main 1 is a heat pump lodging + # `PCDF boiler Reference = 0` + `Main Heating SAP Code = 224`; if + # the extractor (or a future variant cert) drops both, raise so the + # gap surfaces here instead of as a SAP-delta residual downstream. + if mh.main_heating_sap_code is None and pcdb_index is None: + raise UnmappedElmhurstLabel( + "main_heating", + ( + f"§14.0 Main Heating1 has neither PCDF boiler reference " + f"({mh.pcdf_boiler_reference!r}) nor SAP code " + f"({mh.main_heating_sap_code!r}); cannot identify the " + f"heat source" + ), + ) # Shower-outlet classification: SAP10.2 Appendix J routes electric # showers via §J line 64a (their own kWh stream) and treats mixer # showers as drawing from the HW system. The Summary PDF lodges @@ -3874,6 +3892,12 @@ def _map_elmhurst_sap_heating(survey: ElmhurstSiteNotes) -> SapHeating: # number drives PCDB lookup in the cascade. main_heating_index_number=pcdb_index, main_heating_data_source=1 if pcdb_index is not None else None, + # §14.0 "Main Heating SAP Code" — Table 4a integer + # identifying Main 1 when no PCDB boiler reference is + # lodged (e.g. heat pump SAP code 224 on cert 000565). + # The cascade's `seasonal_efficiency` reads this when + # there is no PCDB Table 105/362 record to override. + sap_main_heating_code=mh.main_heating_sap_code, ) ], has_fixed_air_conditioning=survey.ventilation.fixed_space_cooling, diff --git a/datatypes/epc/surveys/elmhurst_site_notes.py b/datatypes/epc/surveys/elmhurst_site_notes.py index fa87f167..7c5396a0 100644 --- a/datatypes/epc/surveys/elmhurst_site_notes.py +++ b/datatypes/epc/surveys/elmhurst_site_notes.py @@ -204,6 +204,14 @@ class MainHeating: None # e.g. "17742 Potterton, Promax 33 Combi ErP, 88.30%" ) heat_pump_age: Optional[str] = None + # Section 14.0 "Main Heating SAP Code" — the SAP 10.2 Table 4a code + # identifying Main 1 when no PCDB boiler reference is lodged (e.g. + # heat pump certs lodge `PCDF boiler Reference = 0` + SAP code = 224 + # for "Air source heat pump, 2013 or later"). None when the line is + # absent or lodged as 0 (= "no code lodged"; PCDB-listed boilers + # leave §14.0 SAP code empty and identify themselves via the PCDB + # index instead). + main_heating_sap_code: Optional[int] = None # Section 14.0 also lodges a secondary heating system (when one is # installed). The SAP code is the integer the cascade reads via # `SapHeating.secondary_heating_type` to apply the Table 11