Handle wall thickness "Unmeasurable" 🟩

This commit is contained in:
Daniel Roth 2026-04-30 16:41:16 +00:00
parent 6c70c5a535
commit 78da2f88b6
6 changed files with 236 additions and 6 deletions

View file

@ -66,9 +66,11 @@ class PasHubRdSapSiteNotesExtractor:
val = self._get_in(list_to_process, key) val = self._get_in(list_to_process, key)
return val is not None and val.lower() != "not known" return val is not None and val.lower() != "not known"
def _wall_thickness_in(self, list_to_process: List[str]) -> int: def _wall_thickness_in(self, list_to_process: List[str]) -> Optional[int]:
val = self._get_in(list_to_process, "Wall thickness:") val = self._get_in(list_to_process, "Wall thickness:")
return int(val.split()[0]) if val else 0 if not val or val.split()[0].lower() == "unmeasurable":
return None
return int(val.split()[0])
def _section(self, start: str, end: str) -> List[str]: def _section(self, start: str, end: str) -> List[str]:
try: try:

View file

@ -802,6 +802,26 @@ class TestExtractNoPropertyPhoto:
assert result.general.number_of_extensions == 2 assert result.general.number_of_extensions == 2
class TestWallThicknessExtraction:
def _extractor(self) -> PasHubRdSapSiteNotesExtractor:
return PasHubRdSapSiteNotesExtractor([])
def test_numeric_value_returns_int(self) -> None:
assert self._extractor()._wall_thickness_in(["Wall thickness:", "310 mm"]) == 310
def test_unmeasurable_returns_none(self) -> None:
assert self._extractor()._wall_thickness_in(["Wall thickness:", "Unmeasurable"]) is None
def test_unmeasurable_lowercase_returns_none(self) -> None:
assert self._extractor()._wall_thickness_in(["Wall thickness:", "unmeasurable"]) is None
def test_unmeasurable_uppercase_returns_none(self) -> None:
assert self._extractor()._wall_thickness_in(["Wall thickness:", "UNMEASURABLE"]) is None
def test_missing_field_returns_none(self) -> None:
assert self._extractor()._wall_thickness_in([]) is None
class TestSolidMasonryPartyWall: class TestSolidMasonryPartyWall:
@pytest.fixture @pytest.fixture
def bc(self) -> BuildingConstruction: def bc(self) -> BuildingConstruction:

View file

@ -1581,7 +1581,7 @@ def _map_main_building_part(
construction_age_band=_extract_age_band(main.age_range), construction_age_band=_extract_age_band(main.age_range),
wall_construction=main.walls_construction_type, wall_construction=main.walls_construction_type,
wall_insulation_type=main.walls_insulation_type, wall_insulation_type=main.walls_insulation_type,
wall_thickness_measured=main.wall_thickness_mm > 0, wall_thickness_measured=main.wall_thickness_mm is not None,
party_wall_construction=main.party_wall_construction_type, party_wall_construction=main.party_wall_construction_type,
sap_floor_dimensions=_map_floor_dimensions(measurements.main_building.floors), sap_floor_dimensions=_map_floor_dimensions(measurements.main_building.floors),
wall_thickness_mm=main.wall_thickness_mm, wall_thickness_mm=main.wall_thickness_mm,
@ -1605,7 +1605,7 @@ def _map_extension_building_part(
construction_age_band=_extract_age_band(ext_c.age_range), construction_age_band=_extract_age_band(ext_c.age_range),
wall_construction=ext_c.walls_construction_type, wall_construction=ext_c.walls_construction_type,
wall_insulation_type=ext_c.walls_insulation_type, wall_insulation_type=ext_c.walls_insulation_type,
wall_thickness_measured=ext_c.wall_thickness_mm > 0, wall_thickness_measured=ext_c.wall_thickness_mm is not None,
party_wall_construction=ext_c.party_wall_construction_type, party_wall_construction=ext_c.party_wall_construction_type,
sap_floor_dimensions=_map_floor_dimensions(ext_m.floors), sap_floor_dimensions=_map_floor_dimensions(ext_m.floors),
wall_thickness_mm=ext_c.wall_thickness_mm, wall_thickness_mm=ext_c.wall_thickness_mm,

View file

@ -694,3 +694,21 @@ class TestFromSiteNotesMiscTopLevel:
def test_photovoltaic_array(self, result: EpcPropertyData) -> None: def test_photovoltaic_array(self, result: EpcPropertyData) -> None:
# renewables.photovoltaic_array: false # renewables.photovoltaic_array: false
assert result.photovoltaic_array is False assert result.photovoltaic_array is False
class TestUnmeasurableWallThickness:
"""wall_thickness_mm=None in site notes → wall_thickness_measured=False in domain."""
@pytest.fixture
def result(self) -> EpcPropertyData:
survey = from_dict(
PasHubRdSapSiteNotes,
load("pashub_rdsap_site_notes_example_unmeasurable_wall.json"),
)
return EpcPropertyDataMapper.from_site_notes(survey)
def test_wall_thickness_measured_is_false(self, result: EpcPropertyData) -> None:
assert result.sap_building_parts[0].wall_thickness_measured is False
def test_wall_thickness_mm_is_none(self, result: EpcPropertyData) -> None:
assert result.sap_building_parts[0].wall_thickness_mm is None

View file

@ -44,7 +44,7 @@ class MainBuildingConstruction:
walls_insulation_type: str walls_insulation_type: str
thermal_conductivity_of_wall_insulation: str thermal_conductivity_of_wall_insulation: str
wall_u_value_known: bool wall_u_value_known: bool
wall_thickness_mm: int wall_thickness_mm: Optional[int]
party_wall_construction_type: str party_wall_construction_type: str
filled_cavity_indicators: Optional[str] = None filled_cavity_indicators: Optional[str] = None
@ -59,7 +59,7 @@ class ExtensionConstruction:
walls_insulation_type: str walls_insulation_type: str
thermal_conductivity_of_wall_insulation: str thermal_conductivity_of_wall_insulation: str
wall_u_value_known: bool wall_u_value_known: bool
wall_thickness_mm: int wall_thickness_mm: Optional[int]
party_wall_construction_type: str party_wall_construction_type: str
filled_cavity_indicators: Optional[str] = None filled_cavity_indicators: Optional[str] = None

View file

@ -0,0 +1,190 @@
{
"inspection_metadata": {
"inspection_surveyor": "test",
"email_address": "test@test.com",
"report_reference": "49D422A9-0779-44DD-9665-464D35DFF1A8",
"created_on": "2026-03-31",
"date_of_inspection": "2026-03-31",
"property_address": "1, Test Street, Test Town, Test County, TE1 1ST"
},
"general": {
"epc_checked_before_assessment": true,
"epc_exists_at_point_of_assessment": false,
"inspection_date": "2026-03-31",
"transaction_type": "None of the Above",
"tenure": "Rented Social",
"property_type": "House",
"detachment_type": "Mid-terrace",
"number_of_storeys": 2,
"terrain_type": "Suburban",
"number_of_extensions": 0,
"electricity_smart_meter": true,
"electric_meter_type": "Single",
"dwelling_export_capable": true,
"mains_gas_available": true,
"gas_smart_meter": true,
"gas_meter_accessible": true,
"measurements_location": "Internal"
},
"building_construction": {
"main_building": {
"age_range": "I: 1996 - 2002",
"age_indicators": "local knowledge",
"walls_construction_type": "Cavity",
"cavity_construction_indicators": "stretcher bond",
"walls_insulation_type": "As built",
"thermal_conductivity_of_wall_insulation": "Unknown",
"wall_u_value_known": false,
"wall_thickness_mm": null,
"party_wall_construction_type": "Cavity Masonry, Unfilled"
},
"floor": {
"floor_type": "Ground Floor",
"floor_construction": "Suspended, not timber",
"floor_insulation_type": "As Built",
"floor_u_value_known": false
}
},
"building_measurements": {
"main_building": {
"floors": [
{
"name": "Floor 1",
"area_m2": 24.78,
"height_m": 2.37,
"heat_loss_perimeter_m": 14.21,
"pwl_m": 6.15
},
{
"name": "Floor 0",
"area_m2": 24.78,
"height_m": 2.35,
"heat_loss_perimeter_m": 14.21,
"pwl_m": 6.15
}
]
}
},
"roof_space": {
"main_building": {
"construction_type": "Pitched roof (Slates or tiles), Access to loft",
"insulation_at": "Joists",
"roof_u_value_known": false,
"insulation_thickness_mm": 100,
"cavity_wall_construction_indicators": "No indicator of construction visible",
"rooms_in_roof": false
}
},
"windows": [
{
"id": 1,
"location": "Main Building",
"wall_type": "External wall",
"glazing_type": "Double glazing, Unknown install date",
"window_type": "Window",
"frame_type": "Wooden or PVC",
"glazing_gap": "16 mm or more",
"draught_proofed": true,
"permanent_shutters": false,
"height_m": 1.36,
"width_m": 1.0,
"orientation": "South East"
}
],
"heating_and_hot_water": {
"main_heating": {
"selection_method": "PCDF Search",
"system_type": "Boiler with radiators or underfloor heating",
"product_id": 18400,
"manufacturer": "Vaillant",
"model": "ecoFIT sustain 415",
"orig_manufacturer": "Vaillant",
"fuel": "Mains gas",
"summer_efficiency": 0,
"type": "Regular",
"condensing": true,
"year": "2018 - current",
"mount": "Wall",
"open_flue": "Room-sealed",
"fan_assist": true,
"status": "Normal status for an actual product",
"central_heating_pump_age": "Unknown",
"controls": "Programmer, room thermostat and TRVs",
"flue_gas_heat_recovery_system": false,
"weather_compensator": false,
"emitter": "Radiators",
"emitter_temperature": "Unknown"
},
"secondary_heating": {
"secondary_fuel": "No Secondary Heating"
},
"water_heating": {
"type": "Regular",
"system": "From main heating 1",
"cylinder_size": "Normal (90-130 litres)",
"cylinder_measured_heat_loss": "Not known",
"insulation_type": "Factory fitted",
"insulation_thickness_mm": 12,
"has_thermostat": true
}
},
"ventilation": {
"ventilation_type": "Natural",
"has_fixed_air_conditioning": false,
"number_of_open_flues": 0,
"number_of_closed_flues": 0,
"number_of_boiler_flues": 0,
"number_of_other_flues": 0,
"number_of_extract_fans": 2,
"number_of_passive_vents": 0,
"number_of_flueless_gas_fires": 0,
"pressure_test": "No test",
"draught_lobby": false
},
"conservatories": {
"has_conservatory": false
},
"renewables": {
"wind_turbines": false,
"solar_hot_water": false,
"photovoltaic_array": false,
"number_of_pv_batteries": 0,
"hydro": false
},
"room_count_elements": {
"number_of_habitable_rooms": 2,
"any_unheated_rooms": true,
"number_of_heated_rooms": 0,
"number_of_external_doors": 2,
"number_of_insulated_external_doors": 0,
"number_of_draughtproofed_external_doors": 2,
"number_of_open_chimneys": 0,
"number_of_blocked_chimneys": 0,
"number_of_fixed_incandescent_bulbs": 0,
"exact_led_cfl_count_known": true,
"number_of_fixed_led_bulbs": 5,
"number_of_fixed_cfl_bulbs": 4,
"waste_water_heat_recovery": "None"
},
"water_use": {
"number_of_baths": 1,
"number_of_special_features": 0,
"showers": [
{
"id": 1,
"outlet_type": "Non-Electric Shower"
}
]
},
"customer_response": {
"customer_present": true,
"willing_to_answer_satisfaction_survey": false
},
"addendum": {
"addendum": "None",
"related_party_disclosure": "No related party",
"hard_to_treat_cavity_access_issues": false,
"hard_to_treat_cavity_high_exposure": false,
"hard_to_treat_cavity_narrow_cavities": false
}
}