diff --git a/backend/documents_parser/elmhurst_extractor.py b/backend/documents_parser/elmhurst_extractor.py index 11e94fba..a8c9e596 100644 --- a/backend/documents_parser/elmhurst_extractor.py +++ b/backend/documents_parser/elmhurst_extractor.py @@ -290,6 +290,10 @@ class ElmhurstSiteNotesExtractor: party_wall_type=self._local_str(lines, "Party Wall Type"), thickness_mm=thickness_mm, insulation_thickness_mm=insulation_thickness_mm, + # Summary §7 "Dry-lining: Yes/No" on the main/extension wall. + # RdSAP 10 §5.8 + Table 14 dry-lining R=0.17 adjustment. The + # alt-wall path reads its own "Alternative Wall N Dry-lining". + dry_lined=self._local_bool(lines, "Dry-lining"), alternative_walls=self._alternative_walls_from_lines(lines), # Summary §7 lodges the per-BP "Curtain Wall Age" line only # when `Type: CW Curtain Wall`. Per RdSAP 10 §5.18 (PDF @@ -548,6 +552,20 @@ class ElmhurstSiteNotesExtractor: if self._is_next_rir_row(lines[j]): break tokens.append(lines[j]) + # Every RIR row ends with [default_u, "Yes"/"No", u_value]; the + # "Yes"/"No" is the unique u_value_known marker (gable types and + # insulation cells never take that value). Stop once we've + # appended that flag plus the trailing u_value numeric so the + # LAST surface row (no next-row name to bound it) does not + # over-read into the following section and shift the trailing + # token slotting — which silently zeroed Common Wall 2's + # default_u (case 43: 1.90 -> 0.00). + if ( + len(tokens) >= 2 + and tokens[-2] in ("Yes", "No") + and self._RIR_NUMERIC_RE.match(tokens[-1]) + ): + break # First two numerics = length, height length = float(tokens[0]) if tokens and self._RIR_NUMERIC_RE.match(tokens[0]) else 0.0 height = float(tokens[1]) if len(tokens) > 1 and self._RIR_NUMERIC_RE.match(tokens[1]) else 0.0 @@ -698,6 +716,7 @@ class ElmhurstSiteNotesExtractor: party_wall_type=ext_party_wall_type, thickness_mm=main_walls.thickness_mm, insulation_thickness_mm=main_walls.insulation_thickness_mm, + dry_lined=main_walls.dry_lined, alternative_walls=self._alternative_walls_from_lines(wall_lines), ) else: diff --git a/datatypes/epc/domain/epc_property_data.py b/datatypes/epc/domain/epc_property_data.py index 1472963d..baf1db00 100644 --- a/datatypes/epc/domain/epc_property_data.py +++ b/datatypes/epc/domain/epc_property_data.py @@ -505,7 +505,7 @@ class SapBuildingPart: building_part_number: Optional[int] = ( None # Not sure how we get this from site notes ) - wall_dry_lined: Optional[bool] = None # Don't think we have this in site notes + wall_dry_lined: Optional[bool] = None # Summary §7 "Dry-lining: Yes/No" wall_thickness_mm: Optional[int] = None # Union[str, int]: a numeric mm value when the API lodges # `wall_insulation_thickness == "measured"` (resolved from the diff --git a/datatypes/epc/domain/mapper.py b/datatypes/epc/domain/mapper.py index d09a46e6..b3fc944f 100644 --- a/datatypes/epc/domain/mapper.py +++ b/datatypes/epc/domain/mapper.py @@ -4436,6 +4436,12 @@ def _map_elmhurst_building_part( wall_is_basement=_elmhurst_wall_is_basement(walls.wall_type), wall_insulation_type=_elmhurst_wall_insulation_int(walls.insulation), wall_thickness_measured=not walls.thickness_unknown, + # Summary §7 "Dry-lining: Yes" → RdSAP 10 §5.8 Table 14 R=0.17 + # adjustment in the cascade (`dry_lined=bool(part.wall_dry_lined)`). + # Emit None (not False) when undried so the field stays absent for + # the non-dry-lined majority (cascade-equivalent: bool(None) == False); + # only a lodged "Yes" populates it. + wall_dry_lined=walls.dry_lined or None, party_wall_construction=_elmhurst_party_wall_construction_int( walls.party_wall_type ), diff --git a/datatypes/epc/surveys/elmhurst_site_notes.py b/datatypes/epc/surveys/elmhurst_site_notes.py index eded346f..4614d33c 100644 --- a/datatypes/epc/surveys/elmhurst_site_notes.py +++ b/datatypes/epc/surveys/elmhurst_site_notes.py @@ -94,6 +94,12 @@ class WallDetails: # "Insulation Thickness" / "100 mm" line pair when a composite or # retrofit insulation is recorded. None when the PDF omits the line. insulation_thickness_mm: Optional[int] = None + # Summary §7 "Dry-lining: Yes/No" on the main/extension wall (distinct + # from the per-alt-wall `AlternativeWall.dry_lined`). Per RdSAP 10 + # §5.8 + Table 14 a dry-lined uninsulated wall adds R=0.17 m²K/W → + # U = 1/(1/U_base + 0.17). Previously unread, so dry-lined solid/ + # cavity walls were billed at the un-adjusted (higher) base U. + dry_lined: bool = False # Per-BP curtain-wall installation age, lodged in Summary §7 as # "Curtain Wall Age" when `wall_type` is "CW Curtain Wall". Per # RdSAP 10 §5.18 (PDF p.48) the curtain-wall U-value keys on this