From 694bb0b569d806485d98531f97afd709369dadaf Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 20 Jan 2026 15:57:56 +0000 Subject: [PATCH 01/68] Define classes --- backend/condition/domain/asset_condition.py | 11 +++++++++++ backend/condition/domain/element.py | 4 ++++ backend/condition/domain/mapping/lbwf_mapper.py | 9 +++++++++ backend/condition/domain/mapping/mapper.py | 11 +++++++++++ .../parsing/records/lbwf/lbwf_asset_condition.py | 15 ++++++++------- .../condition/parsing/records/lbwf/lbwf_house.py | 4 ++-- 6 files changed, 45 insertions(+), 9 deletions(-) create mode 100644 backend/condition/domain/asset_condition.py create mode 100644 backend/condition/domain/element.py create mode 100644 backend/condition/domain/mapping/lbwf_mapper.py create mode 100644 backend/condition/domain/mapping/mapper.py diff --git a/backend/condition/domain/asset_condition.py b/backend/condition/domain/asset_condition.py new file mode 100644 index 00000000..df87fd9f --- /dev/null +++ b/backend/condition/domain/asset_condition.py @@ -0,0 +1,11 @@ +from dataclasses import dataclass +from typing import Optional + +from backend.condition.domain.element import Element + +@dataclass +class AssetCondition: + uprn: int + element: Element + condition_description: str + renewal_year: Optional[int] = None \ No newline at end of file diff --git a/backend/condition/domain/element.py b/backend/condition/domain/element.py new file mode 100644 index 00000000..89246f9c --- /dev/null +++ b/backend/condition/domain/element.py @@ -0,0 +1,4 @@ +from enum import Enum + +class Element(Enum): + pass \ No newline at end of file diff --git a/backend/condition/domain/mapping/lbwf_mapper.py b/backend/condition/domain/mapping/lbwf_mapper.py new file mode 100644 index 00000000..154f3db4 --- /dev/null +++ b/backend/condition/domain/mapping/lbwf_mapper.py @@ -0,0 +1,9 @@ +from typing import Any, List +from backend.condition.domain.asset_condition import AssetCondition +from backend.condition.domain.mapping.mapper import Mapper + + +class LbwfMapper(Mapper): + + def map_asset_conditions(self, client_data: List[Any]) -> List[AssetCondition]: + raise NotImplementedError \ No newline at end of file diff --git a/backend/condition/domain/mapping/mapper.py b/backend/condition/domain/mapping/mapper.py new file mode 100644 index 00000000..b314e01c --- /dev/null +++ b/backend/condition/domain/mapping/mapper.py @@ -0,0 +1,11 @@ +from abc import ABC, abstractmethod +from typing import Any, List + +from backend.condition.domain.asset_condition import AssetCondition + +class Mapper(ABC): + + @abstractmethod + def map_asset_conditions(self, client_data: List[Any]) -> List[AssetCondition]: + #TODO: client_data should be properly typed + pass \ No newline at end of file diff --git a/backend/condition/parsing/records/lbwf/lbwf_asset_condition.py b/backend/condition/parsing/records/lbwf/lbwf_asset_condition.py index dffd1e53..2b4c4992 100644 --- a/backend/condition/parsing/records/lbwf/lbwf_asset_condition.py +++ b/backend/condition/parsing/records/lbwf/lbwf_asset_condition.py @@ -1,5 +1,6 @@ from dataclasses import dataclass from datetime import date +from typing import Optional @dataclass @@ -16,11 +17,11 @@ class LbwfAssetCondition: element_code_description: str attribute_code: str attribute_code_description: str - element_date_value: str | None = None - element_numerical_value: int | None = None - element_text_value: str | None = None - quantity: int | None = None - install_date: date | None = None - remaining_life: int | None = None - element_comments: str | None = None + element_date_value: Optional[str] = None + element_numerical_value: Optional[int] = None + element_text_value: Optional[str] = None + quantity: Optional[int] = None + install_date: Optional[date] = None + remaining_life: Optional[int] = None + element_comments: Optional[str] = None diff --git a/backend/condition/parsing/records/lbwf/lbwf_house.py b/backend/condition/parsing/records/lbwf/lbwf_house.py index 6db16862..3b472fbe 100644 --- a/backend/condition/parsing/records/lbwf/lbwf_house.py +++ b/backend/condition/parsing/records/lbwf/lbwf_house.py @@ -8,8 +8,8 @@ class LbwfHouse: uprn: int reference: int address: str - epc: str # TODO: make enum - shdf: bool + epc: str # TODO: make enum? + shdf: str house: str fail_decency: int assets: List[LbwfAssetCondition] \ No newline at end of file From c8abc19e599440fbe1faed11b37014e3a6e8b187 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 20 Jan 2026 17:18:51 +0000 Subject: [PATCH 02/68] =?UTF-8?q?Map=20LbwfHouse=20to=20AssetCondition=20l?= =?UTF-8?q?ist=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/condition/domain/asset_condition.py | 9 +- backend/condition/domain/element.py | 125 ++++++++- .../condition/domain/mapping/lbwf_mapper.py | 8 +- .../tests/mapping/test_lbwf_mapper.py | 239 ++++++++++++++++++ .../tests/parsing/test_lbwf_parser.py | 2 +- 5 files changed, 375 insertions(+), 8 deletions(-) create mode 100644 backend/condition/tests/mapping/test_lbwf_mapper.py diff --git a/backend/condition/domain/asset_condition.py b/backend/condition/domain/asset_condition.py index df87fd9f..2b7946c2 100644 --- a/backend/condition/domain/asset_condition.py +++ b/backend/condition/domain/asset_condition.py @@ -6,6 +6,9 @@ from backend.condition.domain.element import Element @dataclass class AssetCondition: uprn: int - element: Element - condition_description: str - renewal_year: Optional[int] = None \ No newline at end of file + element: Element # TODO: should HHSRS elements be handled differently? + condition_description: str # TODO: this probably needs to be some sort of enum so it's searchable/filterable on the frontend + quantity: int + renewal_year: Optional[int] = None + source: Optional[str] = None + # TODO: add install_date diff --git a/backend/condition/domain/element.py b/backend/condition/domain/element.py index 89246f9c..021c8492 100644 --- a/backend/condition/domain/element.py +++ b/backend/condition/domain/element.py @@ -1,4 +1,123 @@ -from enum import Enum +from enum import StrEnum -class Element(Enum): - pass \ No newline at end of file + +class Element(StrEnum): + AHR_CAT = "Accessible Housing Register Category" + ASBESTOS = "Asbestos Present" + ASSETSAREA = "Assets Area for Decent Homes and Investment" + DECNTHMINC = "Include for Decent Homes Reporting - LBWF Stock" + EICINSFREQ = "EICR - Elec Install Conditions Report Inspection Frequency" + EXTBALCONY = "Private Balconies in External Area" + EXTBKSDDR1 = "Back and Side Doors 1 in External Area" + EXTBKSDDR2 = "Back and Side Doors 2 in External Area" + EXTBPOINTG = "Brickwork Pointing in External Area" + EXTCHIMNEY = "Chimneys in External Area" + EXTDWNPTYP = "Downpipes in External Area" + EXTDRPKERB = "Drop Kerb in External Area" + EXTEXTDECS = "External Decorations in External Area" + EXTFASOFBR = "Fascia / Soffit / Bargeboard in External Area" + EXTGARDOOR = "Garage Door in External Area" + EXTGARROOF = "Garage Roof in External Area" + EXTGARSTDR = "Garage and Store Doors in External Area" + EXTGARSTRF = "Garage and Store Roofs in External Area" + EXTGARSTWD = "Garage and Store Windows in External Area" + EXTGARWDWS = "Garage Windows in External Area" + EXTGUTRTYP = "Gutters in External Area" + EXTHARDSTD = "Hardstanding in External Area" + EXTINTDWNP = "Internal Downpipes in External Area" + EXTLINTELS = "Lintels in External Area" + EXTOUTBOH = "Overhaul of Outbuilding in External Area" + EXTPARKING = "Parking in External Area" + EXTPCHCNPY = "Porch and / or Canopy in External Area" + EXTPTFRDR1 = "Patio and French Doors 1 in External Area" + EXTROOF1 = "Roof Covering 1 in External Area" + EXTROOF2 = "Roof Covering 2 in External Area" + EXTROOF3 = "Roof Covering 3 in External Area" + EXTRFSTR1 = "Roof Structure 1 in External Area" + EXTRFSTR2 = "Roof Structure 2 in External Area" + EXTRFSTR3 = "Roof Structure 3 in External Area" + EXTSTOREY = "Number of Storeys within the Property or Block" + EXTSTRDOOR = "Store Door in External Area" + EXTSTRINSP = "Structural Defects in External Area" + EXTSTRROOF = "Store Roof in External Area" + EXTSTRWDWS = "Store Windows in External Area" + EXTWALLFN1 = "Wall Finish 1 in External Area" + EXTWALLFN2 = "Wall Finish 2 in External Area" + EXTWALLINS = "Wall Insulation Improvement in External Area" + EXTWALLSPL = "Wall Spalling in External Area" + EXTWALLSTR = "Wall Structure in External Area" + EXTWNDWS1 = "Windows 1 in External Area" + EXTWNDWS2 = "Windows 2 in External Area" + FFHHDAMP = "Fitness for Human Habitation - Serious problem with damp" + FFHHDRNWC = "Fitness for Human Habitation - Problems with the drainage or the lavatories" + FFHHHCWAT = "Fitness for Human Habitation - Problem with the supply of hot and cold water" + FFHHNEGLC = "Fitness for Human Habitation - Building neglected and is in a bad condition" + FFHHNONAT = "Fitness for Human Habitation - Not enough natural light" + FFHHNOVEN = "Fitness for Human Habitation - Not enough ventilation" + FFHHPRPCK = "Fitness for Human Habitation - Difficult to prepare and cook food or wash up" + FFHHUNLAY = "Fitness for Human Habitation - Unsafe layout" + FFHHUNSTA = "Fitness for Human Habitation - Building is unstable" + FRARISKRTG = "Fire Risk Assessment Rating" + FRAEVACSTR = "Fire Risk Assessment Evacuation Strategy" + FRATYPE = "Fire Risk Assessment Type" + FLVL = "Floor Level of Front Door" + HHSRSASB = "Asbestos (and MMF)" + HHSRSBIOC = "Biocides" + HHSRSCO = "Carbon monoxide" + HHSRSCOLD = "Excess cold" + HHSRSCLOW = "Collision hazards and low headroom" + HHSRSCROWD = "Crowding and space" + HHSRSDAMP = "Damp and mould growth" + HHSRSDOMES = "Domestic hygeine, Pests and Refuse" + HHSRSELEC = "Electrical hazards" + HHSRSENTRP = "Collision and entrapment" + HHSRSENTRY = "Entry by intruders" + HHSRSEXPLO = "Explosions" + HHSRSFBATH = "Falls associated with baths etc" + HHSRSFBETW = "Falling between levels" + HHSRSFIRE = "Fire" + HHSRSFLAME = "Flames, hot surfaces etc" + HHSRSFLEVE = "Falling on level surfaces etc" + HHSRSFOOD = "Food safety" + HHSRSFSTAI = "Falling on stairs etc" + HHSRSFUEL = "Uncombusted fuel gas" + HHSRSHEAT = "Excess heat" + HHSRSLEAD = "Lead" + HHSRSLIGHT = "Lighting" + HHSRSNO2 = "Nitrogen dioxide" + HHSRSNOISE = "Noise" + HHSRSORGAN = "Volatile organic compounds" + HHSRSPERS = "Personal hygeine, Sanitation and Drainage" + HHSRSPOSI = "Position and operability of amenities etc" + HHSRSRADIA = "Radiation" + HHSRSSO2 = "Sulphur dioxide and smoke" + HHSRSSTRUC = "Structural collapse and falling elements" + HHSRSWATER = "Water supply" + INTACCRAMP = "Access Ramp 1:12 Gradient to Property" + INTADDWCW = "Additional WCs and / or WHBs in Property" + INTBTHADEQ = "Adequacy of Bathroom Location in Property" + INTBTHREML = "Source of Bathroom Remaining Life in Property" + INTBTHRLOC = "Location of Bathroom in Property" + INTBOILERF = "Boiler Fuel in Property" + INTCHEXTNT = "Extent of Central Heating in Property" + INTCKRLOC = "Adequacy of Cooker Location in Property" + INTCOMHTG = "Community Heating in Property" + INTELECTRC = "Electrics Required in Property" + INTFLRLVL = "Floor Level Location for Property" + INTFRDOOR = "Type and Location of Front Door in Property" + INTFRDRFRR = "Front Door Fire Rating in Property" + INTGASAVAI = "Gas Available in Property" + INTHEATREC = "Heat Recovery Units in Property" + INTHTDISYS = "Heating Distribution System in Property" + INTHTIMP = "Heating Improvement Required in Property" + INTKITADEQ = "Adequacy of Kitchen and Type in Property" + INTKITREML = "Source of Kitchen Remaining Life in Property" + INTLOFTINS = "Size in mm of Loft Insulation Thickness in Property" + INTNSEINSL = "Adequacy of Noise Insulation in Property" + INTPROGHTG = "Programmable Heating in Property" + INTSMKDET = "Smoke Detectors in Property" + INTSTEPSFD = "Number of Steps to Front Door for Property" + INTTNTINST = "Tenant Installed Kitchen in Property" + INTWDWTYPE = "Windows in Property" + INTWTRHTNG = "Type of Water Heating in Property" + QUALITYSTD = "Quality standard" diff --git a/backend/condition/domain/mapping/lbwf_mapper.py b/backend/condition/domain/mapping/lbwf_mapper.py index 154f3db4..0bbdc0d6 100644 --- a/backend/condition/domain/mapping/lbwf_mapper.py +++ b/backend/condition/domain/mapping/lbwf_mapper.py @@ -1,9 +1,15 @@ -from typing import Any, List +from typing import Any, List, Optional from backend.condition.domain.asset_condition import AssetCondition +from backend.condition.domain.element import Element from backend.condition.domain.mapping.mapper import Mapper +from backend.condition.parsing.records.lbwf.lbwf_asset_condition import LbwfAssetCondition class LbwfMapper(Mapper): def map_asset_conditions(self, client_data: List[Any]) -> List[AssetCondition]: + raise NotImplementedError + + @staticmethod + def _map_element(lbwf_asset: LbwfAssetCondition) -> Optional[Element]: raise NotImplementedError \ No newline at end of file diff --git a/backend/condition/tests/mapping/test_lbwf_mapper.py b/backend/condition/tests/mapping/test_lbwf_mapper.py new file mode 100644 index 00000000..e26b9928 --- /dev/null +++ b/backend/condition/tests/mapping/test_lbwf_mapper.py @@ -0,0 +1,239 @@ +from typing import List +import pytest +from datetime import date + +from backend.condition.domain.mapping.lbwf_mapper import LbwfMapper +from backend.condition.parsing.records.lbwf.lbwf_house import LbwfHouse +from backend.condition.parsing.records.lbwf.lbwf_asset_condition import LbwfAssetCondition +from backend.condition.domain.element import Element +from backend.condition.domain.asset_condition import AssetCondition + +def test_lbwf_mapper_maps_house(): + # arrange + lbwf_house = LbwfHouse( + uprn=1, + reference=100, + address="123 Fake Street, London, A10 1AB", + epc="F", + shdf="NO", + house="HOUSE", + fail_decency=2025, + assets=[ + LbwfAssetCondition( + prop_ref=100, + domna=100, + address="123 Fake Street, London, A10 1AB", + ownership="LBWF_OWNED", + prop_status="OCCP", + prop_type="HOU", + prop_sub_type="TERRACED", + element_group="ASSETS", + element_code="AHR_CAT", + element_code_description="Accessible Housing Register Category", + attribute_code="F", + attribute_code_description="General Needs", + element_date_value=None, + element_numerical_value=None, + element_text_value=None, + quantity=1, + install_date=None, + remaining_life=None, + element_comments=None, + ), + LbwfAssetCondition( + prop_ref=100, + domna=100, + address="123 Fake Street, London, A10 1AB", + ownership="LBWF_OWNED", + prop_status="OCCP", + prop_type="HOU", + prop_sub_type="TERRACED", + element_group="ASSETS", + element_code="FLVL", + element_code_description="Floor Level of Front Door", + attribute_code="0G", + attribute_code_description="Ground Floor", + element_date_value=None, + element_numerical_value=None, + element_text_value=None, + quantity=1, + install_date=None, + remaining_life=None, + element_comments=None, + ), + LbwfAssetCondition( + prop_ref=100, + domna=100, + address="123 Fake Street, London, A10 1AB", + ownership="LBWF_OWNED", + prop_status="OCCP", + prop_type="HOU", + prop_sub_type="TERRACED", + element_group="ASSETS", + element_code="ASBESTOS", + element_code_description="Asbestos Present", + attribute_code="YES", + attribute_code_description="Yes", + element_date_value=None, + element_numerical_value=None, + element_text_value=None, + quantity=None, + install_date=None, + remaining_life=None, + element_comments="Source of Data = ACT", + ), + LbwfAssetCondition( + prop_ref=100, + domna=100, + address="123 Fake Street, London, A10 1AB", + ownership="LBWF_OWNED", + prop_status="OCCP", + prop_type="HOU", + prop_sub_type="TERRACED", + element_group="ASSETS", + element_code="INTBTHRLOC", + element_code_description="Location of Bathroom in Property", + attribute_code="ENTRANCE", + attribute_code_description="Bathroom on Entrance Level in Property", + element_date_value=None, + element_numerical_value=None, + element_text_value=None, + quantity=1, + install_date=None, + remaining_life=None, + element_comments="Source of Data = Codeman", + ), + LbwfAssetCondition( + prop_ref=100, + domna=100, + address="123 Fake Street, London, A10 1AB", + ownership="LBWF_OWNED", + prop_status="OCCP", + prop_type="HOU", + prop_sub_type="TERRACED", + element_group="ASSETS", + element_code="INTCHEXTNT", + element_code_description="Extent of Central Heating in Property", + attribute_code="NONE", + attribute_code_description="No Central Heating in Property", + element_date_value=None, + element_numerical_value=None, + element_text_value=None, + quantity=1, + install_date=None, + remaining_life=None, + element_comments="Source of Data = Codeman", + ), + LbwfAssetCondition( + prop_ref=100, + domna=100, + address="123 Fake Street, London, A10 1AB", + ownership="LBWF_OWNED", + prop_status="OCCP", + prop_type="HOU", + prop_sub_type="TERRACED", + element_group="ASSETS", + element_code="HHSRSFIRE", + element_code_description="Fire", + attribute_code="TYPRISK", + attribute_code_description="Category 4 - Typical Risk", + element_date_value=None, + element_numerical_value=None, + element_text_value=None, + quantity=1, + install_date=None, + remaining_life=None, + element_comments="Source of Data = Morgan Sindall", + ), + LbwfAssetCondition( + prop_ref=100, + domna=100, + address="123 Fake Street, London, A10 1AB", + ownership="LBWF_OWNED", + prop_status="OCCP", + prop_type="HOU", + prop_sub_type="TERRACED", + element_group="ASSETS", + element_code="EXTWALLFN1", + element_code_description="Wall Finish 1 in External Area", + attribute_code="RENDERPBBL", + attribute_code_description="Render or Pebbledash Wall Finish 1 in External Area", + element_date_value=None, + element_numerical_value=None, + element_text_value=None, + quantity=1, + install_date=date(2009,4,1), + remaining_life=26, + element_comments="Source of Data = Codeman", + ), + ] + ) + mapper = LbwfMapper() + + current_year = 2026 + + expected_assets: List[AssetCondition] = [ + AssetCondition( + uprn=1, + element=Element.AHR_CAT, + condition_description="General Needs", + quantity=1, + renewal_year=None, + source=None + ), + AssetCondition( + uprn=1, + element=Element.FLVL, + condition_description="Ground Floor", + quantity=1, + renewal_year=None, + source=None + ), + AssetCondition( + uprn=1, + element=Element.ASBESTOS, + condition_description="Yes", + quantity=None, + renewal_year=None, + source="Source of Data = ACT" + ), + AssetCondition( + uprn=1, + element=Element.INTBTHRLOC, + condition_description="Bathroom on Entrance Level in Property", + quantity=1, + renewal_year=None, + source="Source of Data = Codeman" + ), + AssetCondition( + uprn=1, + element=Element.INTCHEXTNT, + condition_description="No Central Heating in Property", + quantity=1, + renewal_year=None, + source="Source of Data = Codeman" + ), + AssetCondition( + uprn=1, + element=Element.HHSRSFIRE, + condition_description="Category 4 - Typical Risk", + quantity=1, + renewal_year=None, + source="Source of Data = Morgan Sindall" + ), + AssetCondition( + uprn=1, + element=Element.EXTWALLFN1, + condition_description="Render or Pebbledash Wall Finish 1 in External Area", + quantity=1, + renewal_year=2052, + source="Source of Data = Codeman" + ), + + ] + + # act + actual_assets: List[AssetCondition] = mapper.map_asset_conditions(lbwf_house) + + # assert + assert actual_assets == expected_assets \ No newline at end of file diff --git a/backend/condition/tests/parsing/test_lbwf_parser.py b/backend/condition/tests/parsing/test_lbwf_parser.py index 7556b845..beb81a03 100644 --- a/backend/condition/tests/parsing/test_lbwf_parser.py +++ b/backend/condition/tests/parsing/test_lbwf_parser.py @@ -112,7 +112,7 @@ def lbwf_homes_xlsx_bytes() -> BytesIO: return stream -def test_lbwf_parser_passes_houses(lbwf_homes_xlsx_bytes): +def test_lbwf_parser_parses_houses(lbwf_homes_xlsx_bytes): # arrange parser = LbwfParser() From fc08a7df4ff64dcc2c64c9ad1de692c588adf66d Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Wed, 21 Jan 2026 11:01:25 +0000 Subject: [PATCH 03/68] =?UTF-8?q?Map=20LbwfHouse=20to=20AssetCondition=20l?= =?UTF-8?q?ist=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../condition/domain/mapping/lbwf_mapper.py | 52 +++++++++++++++++-- backend/condition/domain/mapping/mapper.py | 2 +- .../tests/mapping/test_lbwf_mapper.py | 2 +- 3 files changed, 50 insertions(+), 6 deletions(-) diff --git a/backend/condition/domain/mapping/lbwf_mapper.py b/backend/condition/domain/mapping/lbwf_mapper.py index 0bbdc0d6..63434240 100644 --- a/backend/condition/domain/mapping/lbwf_mapper.py +++ b/backend/condition/domain/mapping/lbwf_mapper.py @@ -1,15 +1,59 @@ from typing import Any, List, Optional +from datetime import datetime, date + from backend.condition.domain.asset_condition import AssetCondition from backend.condition.domain.element import Element from backend.condition.domain.mapping.mapper import Mapper from backend.condition.parsing.records.lbwf.lbwf_asset_condition import LbwfAssetCondition +from backend.condition.parsing.records.lbwf.lbwf_house import LbwfHouse +from utils.logger import setup_logger +logger = setup_logger() class LbwfMapper(Mapper): - def map_asset_conditions(self, client_data: List[Any]) -> List[AssetCondition]: - raise NotImplementedError + def map_asset_conditions_for_property(self, client_data: Any) -> List[AssetCondition]: + assert isinstance(client_data, LbwfHouse) # TODO: think of a better way to do this + + mapped_assets: List[AssetCondition] = [] + + uprn: int = client_data.uprn + for raw_asset in client_data.assets: + try: + element: Element = LbwfMapper._map_element(raw_asset.element_code) + except: + logger.warning(f"Unrecognised LBWF Asset Element Code: {raw_asset.element_code}. Skipping record") + continue + + + mapped_assets.append( + AssetCondition( + uprn=uprn, + element=element, + condition_description=raw_asset.attribute_code_description, + quantity=raw_asset.quantity, + renewal_year=LbwfMapper._calculate_renewal_year(raw_asset), + source=raw_asset.element_comments, + ) + ) + + return mapped_assets + + @staticmethod - def _map_element(lbwf_asset: LbwfAssetCondition) -> Optional[Element]: - raise NotImplementedError \ No newline at end of file + def _map_element(lbwf_element_code: LbwfAssetCondition) -> Element: + return Element[lbwf_element_code] + + @staticmethod + def _calculate_renewal_year(lbwf_asset: LbwfAssetCondition) -> Optional[int]: + remaining_life_years: Optional[int] = lbwf_asset.remaining_life + if not remaining_life_years: + return None + + try: + survey_year: int = datetime.now().year # TODO: get survey year from filename or elsewhere + return survey_year + remaining_life_years + except: + logger.debug(f"Unable to map LBWF Asset remaining life {remaining_life_years} to renewal year, returning None") + return None \ No newline at end of file diff --git a/backend/condition/domain/mapping/mapper.py b/backend/condition/domain/mapping/mapper.py index b314e01c..f08fa4e1 100644 --- a/backend/condition/domain/mapping/mapper.py +++ b/backend/condition/domain/mapping/mapper.py @@ -6,6 +6,6 @@ from backend.condition.domain.asset_condition import AssetCondition class Mapper(ABC): @abstractmethod - def map_asset_conditions(self, client_data: List[Any]) -> List[AssetCondition]: + def map_asset_conditions_for_property(self, client_data: Any) -> List[AssetCondition]: #TODO: client_data should be properly typed pass \ No newline at end of file diff --git a/backend/condition/tests/mapping/test_lbwf_mapper.py b/backend/condition/tests/mapping/test_lbwf_mapper.py index e26b9928..3e066d27 100644 --- a/backend/condition/tests/mapping/test_lbwf_mapper.py +++ b/backend/condition/tests/mapping/test_lbwf_mapper.py @@ -233,7 +233,7 @@ def test_lbwf_mapper_maps_house(): ] # act - actual_assets: List[AssetCondition] = mapper.map_asset_conditions(lbwf_house) + actual_assets: List[AssetCondition] = mapper.map_asset_conditions_for_property(lbwf_house) # assert assert actual_assets == expected_assets \ No newline at end of file From 25923cbc9fb0f9c9f7f6bfa72ffb46480df2e8c0 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Wed, 21 Jan 2026 12:09:02 +0000 Subject: [PATCH 04/68] add mapping to processor --- backend/condition/parsing/factory.py | 8 ++++++++ backend/condition/processor.py | 13 ++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/backend/condition/parsing/factory.py b/backend/condition/parsing/factory.py index 01dce75d..ea54d3e0 100644 --- a/backend/condition/parsing/factory.py +++ b/backend/condition/parsing/factory.py @@ -1,3 +1,5 @@ +from backend.condition.domain.mapping.lbwf_mapper import LbwfMapper +from backend.condition.domain.mapping.mapper import Mapper from backend.condition.file_type import FileType from backend.condition.parsing.parser import Parser from backend.condition.parsing.lbwf_parser import LbwfParser @@ -7,3 +9,9 @@ def select_parser(file_type: FileType) -> Parser: return LbwfParser() raise ValueError("Unrecognised file type, unable to instantiate Parser") + +def select_mapper(file_type: FileType) -> Mapper: + if file_type is FileType.LBWF: + return LbwfMapper() + + raise ValueError("Unrecognised file type, unable to instantiate Mapper") diff --git a/backend/condition/processor.py b/backend/condition/processor.py index fb06c888..4f379b23 100644 --- a/backend/condition/processor.py +++ b/backend/condition/processor.py @@ -1,9 +1,11 @@ from typing import Any, BinaryIO, List +from backend.condition.domain.asset_condition import AssetCondition +from backend.condition.domain.mapping.mapper import Mapper from backend.condition.parsing.parser import Parser from utils.logger import setup_logger from backend.condition.file_type import FileType, detect_file_type -from backend.condition.parsing.factory import select_parser +from backend.condition.parsing.factory import select_parser, select_mapper def process_file(file_stream: BinaryIO, source_key: str) -> None: print(f"[processor] Received file: {source_key}") @@ -11,8 +13,13 @@ def process_file(file_stream: BinaryIO, source_key: str) -> None: # Instantiation file_type: FileType = detect_file_type(source_key) parser: Parser = select_parser(file_type) + mapper: Mapper = select_mapper(file_type) # Orchestration - records: List[Any] = parser.parse(file_stream) + raw_properties: List[Any] = parser.parse(file_stream) - print(records) # temp \ No newline at end of file + assets: List[AssetCondition] = [] + for p in raw_properties: + assets.extend(mapper.map_asset_conditions_for_property(p)) + + print(assets) # temp \ No newline at end of file From dfe04601551d94f3b831a405a3095a0d62f34cb0 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Wed, 21 Jan 2026 12:14:16 +0000 Subject: [PATCH 05/68] Pass survey year to mapper rather than using today's year --- backend/condition/domain/mapping/lbwf_mapper.py | 10 ++++++---- backend/condition/domain/mapping/mapper.py | 4 ++-- backend/condition/processor.py | 5 ++++- backend/condition/tests/mapping/test_lbwf_mapper.py | 4 ++-- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/backend/condition/domain/mapping/lbwf_mapper.py b/backend/condition/domain/mapping/lbwf_mapper.py index 63434240..bc44e4c3 100644 --- a/backend/condition/domain/mapping/lbwf_mapper.py +++ b/backend/condition/domain/mapping/lbwf_mapper.py @@ -12,7 +12,7 @@ logger = setup_logger() class LbwfMapper(Mapper): - def map_asset_conditions_for_property(self, client_data: Any) -> List[AssetCondition]: + def map_asset_conditions_for_property(self, client_data: Any, survey_year: Optional[int]) -> List[AssetCondition]: assert isinstance(client_data, LbwfHouse) # TODO: think of a better way to do this mapped_assets: List[AssetCondition] = [] @@ -32,7 +32,7 @@ class LbwfMapper(Mapper): element=element, condition_description=raw_asset.attribute_code_description, quantity=raw_asset.quantity, - renewal_year=LbwfMapper._calculate_renewal_year(raw_asset), + renewal_year=LbwfMapper._calculate_renewal_year(raw_asset, survey_year), source=raw_asset.element_comments, ) ) @@ -46,13 +46,15 @@ class LbwfMapper(Mapper): return Element[lbwf_element_code] @staticmethod - def _calculate_renewal_year(lbwf_asset: LbwfAssetCondition) -> Optional[int]: + def _calculate_renewal_year(lbwf_asset: LbwfAssetCondition, survey_year: Optional[int]) -> Optional[int]: remaining_life_years: Optional[int] = lbwf_asset.remaining_life if not remaining_life_years: return None + if not survey_year: + return None + try: - survey_year: int = datetime.now().year # TODO: get survey year from filename or elsewhere return survey_year + remaining_life_years except: logger.debug(f"Unable to map LBWF Asset remaining life {remaining_life_years} to renewal year, returning None") diff --git a/backend/condition/domain/mapping/mapper.py b/backend/condition/domain/mapping/mapper.py index f08fa4e1..4e51d46b 100644 --- a/backend/condition/domain/mapping/mapper.py +++ b/backend/condition/domain/mapping/mapper.py @@ -1,11 +1,11 @@ from abc import ABC, abstractmethod -from typing import Any, List +from typing import Any, List, Optional from backend.condition.domain.asset_condition import AssetCondition class Mapper(ABC): @abstractmethod - def map_asset_conditions_for_property(self, client_data: Any) -> List[AssetCondition]: + def map_asset_conditions_for_property(self, client_data: Any, survey_year: Optional[int]) -> List[AssetCondition]: #TODO: client_data should be properly typed pass \ No newline at end of file diff --git a/backend/condition/processor.py b/backend/condition/processor.py index 4f379b23..cc44e38a 100644 --- a/backend/condition/processor.py +++ b/backend/condition/processor.py @@ -1,4 +1,5 @@ from typing import Any, BinaryIO, List +from datetime import datetime from backend.condition.domain.asset_condition import AssetCondition from backend.condition.domain.mapping.mapper import Mapper @@ -18,8 +19,10 @@ def process_file(file_stream: BinaryIO, source_key: str) -> None: # Orchestration raw_properties: List[Any] = parser.parse(file_stream) + survey_year = datetime.now().year # TODO: get this from filepath or elsewhere + assets: List[AssetCondition] = [] for p in raw_properties: - assets.extend(mapper.map_asset_conditions_for_property(p)) + assets.extend(mapper.map_asset_conditions_for_property(p, survey_year)) print(assets) # temp \ No newline at end of file diff --git a/backend/condition/tests/mapping/test_lbwf_mapper.py b/backend/condition/tests/mapping/test_lbwf_mapper.py index 3e066d27..926e34c1 100644 --- a/backend/condition/tests/mapping/test_lbwf_mapper.py +++ b/backend/condition/tests/mapping/test_lbwf_mapper.py @@ -170,7 +170,7 @@ def test_lbwf_mapper_maps_house(): ) mapper = LbwfMapper() - current_year = 2026 + survey_year = 2026 expected_assets: List[AssetCondition] = [ AssetCondition( @@ -233,7 +233,7 @@ def test_lbwf_mapper_maps_house(): ] # act - actual_assets: List[AssetCondition] = mapper.map_asset_conditions_for_property(lbwf_house) + actual_assets: List[AssetCondition] = mapper.map_asset_conditions_for_property(lbwf_house, survey_year) # assert assert actual_assets == expected_assets \ No newline at end of file From 7bd70ae001c63acaf58e34bd069af0d31326c129 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Wed, 21 Jan 2026 12:36:35 +0000 Subject: [PATCH 06/68] =?UTF-8?q?include=20install=5Fdate=20on=20AssetCond?= =?UTF-8?q?ition=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/condition/domain/asset_condition.py | 5 +++-- .../tests/mapping/test_lbwf_mapper.py | 21 ++++++++++++------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/backend/condition/domain/asset_condition.py b/backend/condition/domain/asset_condition.py index 2b7946c2..dffbdf88 100644 --- a/backend/condition/domain/asset_condition.py +++ b/backend/condition/domain/asset_condition.py @@ -1,5 +1,6 @@ from dataclasses import dataclass from typing import Optional +from datetime import date from backend.condition.domain.element import Element @@ -7,8 +8,8 @@ from backend.condition.domain.element import Element class AssetCondition: uprn: int element: Element # TODO: should HHSRS elements be handled differently? - condition_description: str # TODO: this probably needs to be some sort of enum so it's searchable/filterable on the frontend + condition_description: str # TODO: this probably needs to be some sort of enum so it's searchable/filterable on the frontend. Could be hard to map from string though quantity: int renewal_year: Optional[int] = None source: Optional[str] = None - # TODO: add install_date + install_date: Optional[date] = None diff --git a/backend/condition/tests/mapping/test_lbwf_mapper.py b/backend/condition/tests/mapping/test_lbwf_mapper.py index 926e34c1..151e5d19 100644 --- a/backend/condition/tests/mapping/test_lbwf_mapper.py +++ b/backend/condition/tests/mapping/test_lbwf_mapper.py @@ -179,7 +179,8 @@ def test_lbwf_mapper_maps_house(): condition_description="General Needs", quantity=1, renewal_year=None, - source=None + source=None, + install_date=None, ), AssetCondition( uprn=1, @@ -187,7 +188,8 @@ def test_lbwf_mapper_maps_house(): condition_description="Ground Floor", quantity=1, renewal_year=None, - source=None + source=None, + install_date=None, ), AssetCondition( uprn=1, @@ -195,7 +197,8 @@ def test_lbwf_mapper_maps_house(): condition_description="Yes", quantity=None, renewal_year=None, - source="Source of Data = ACT" + source="Source of Data = ACT", + install_date=None, ), AssetCondition( uprn=1, @@ -203,7 +206,8 @@ def test_lbwf_mapper_maps_house(): condition_description="Bathroom on Entrance Level in Property", quantity=1, renewal_year=None, - source="Source of Data = Codeman" + source="Source of Data = Codeman", + install_date=None, ), AssetCondition( uprn=1, @@ -211,7 +215,8 @@ def test_lbwf_mapper_maps_house(): condition_description="No Central Heating in Property", quantity=1, renewal_year=None, - source="Source of Data = Codeman" + source="Source of Data = Codeman", + install_date=None, ), AssetCondition( uprn=1, @@ -219,7 +224,8 @@ def test_lbwf_mapper_maps_house(): condition_description="Category 4 - Typical Risk", quantity=1, renewal_year=None, - source="Source of Data = Morgan Sindall" + source="Source of Data = Morgan Sindall", + install_date=None, ), AssetCondition( uprn=1, @@ -227,7 +233,8 @@ def test_lbwf_mapper_maps_house(): condition_description="Render or Pebbledash Wall Finish 1 in External Area", quantity=1, renewal_year=2052, - source="Source of Data = Codeman" + source="Source of Data = Codeman", + install_date=date(2009,4,1), ), ] From a8ff74c2ea1c71e0a822152ff3bee6129806d435 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Wed, 21 Jan 2026 12:37:23 +0000 Subject: [PATCH 07/68] =?UTF-8?q?include=20install=5Fdate=20on=20AssetCond?= =?UTF-8?q?ition=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/condition/domain/mapping/lbwf_mapper.py | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/condition/domain/mapping/lbwf_mapper.py b/backend/condition/domain/mapping/lbwf_mapper.py index bc44e4c3..0af21b7a 100644 --- a/backend/condition/domain/mapping/lbwf_mapper.py +++ b/backend/condition/domain/mapping/lbwf_mapper.py @@ -34,6 +34,7 @@ class LbwfMapper(Mapper): quantity=raw_asset.quantity, renewal_year=LbwfMapper._calculate_renewal_year(raw_asset, survey_year), source=raw_asset.element_comments, + install_date=raw_asset.install_date, ) ) From d43d9d9069d2610943df7cb99a83603cb4699f64 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Wed, 21 Jan 2026 15:13:17 +0000 Subject: [PATCH 08/68] =?UTF-8?q?Parse=20Peabody=20condition=20data=20xlsx?= =?UTF-8?q?=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/condition/file_type.py | 4 + backend/condition/parsing/factory.py | 4 + backend/condition/parsing/peabody_parser.py | 7 + .../peabody/peabody_asset_condition.py | 22 ++++ .../records/peabody/peabody_property.py | 9 ++ .../tests/parsing/test_parsing_factory.py | 11 ++ .../tests/parsing/test_peabody_parser.py | 124 ++++++++++++++++++ 7 files changed, 181 insertions(+) create mode 100644 backend/condition/parsing/peabody_parser.py create mode 100644 backend/condition/parsing/records/peabody/peabody_asset_condition.py create mode 100644 backend/condition/parsing/records/peabody/peabody_property.py create mode 100644 backend/condition/tests/parsing/test_peabody_parser.py diff --git a/backend/condition/file_type.py b/backend/condition/file_type.py index b9a4357f..07a0669c 100644 --- a/backend/condition/file_type.py +++ b/backend/condition/file_type.py @@ -2,6 +2,7 @@ from enum import Enum class FileType(Enum): LBWF = "lbwf" + Peabody = "peabody" def detect_file_type(filepath: str) -> FileType: path = filepath.lower() @@ -9,4 +10,7 @@ def detect_file_type(filepath: str) -> FileType: if "lbwf" in path: return FileType.LBWF + if "peadbody" in path: + return FileType.Peabody + raise ValueError("Unrecognised file path") \ No newline at end of file diff --git a/backend/condition/parsing/factory.py b/backend/condition/parsing/factory.py index ea54d3e0..3a28df78 100644 --- a/backend/condition/parsing/factory.py +++ b/backend/condition/parsing/factory.py @@ -3,10 +3,14 @@ from backend.condition.domain.mapping.mapper import Mapper from backend.condition.file_type import FileType from backend.condition.parsing.parser import Parser from backend.condition.parsing.lbwf_parser import LbwfParser +from backend.condition.parsing.peabody_parser import PeabodyParser def select_parser(file_type: FileType) -> Parser: if file_type is FileType.LBWF: return LbwfParser() + + if file_type is FileType.Peabody: + return PeabodyParser() raise ValueError("Unrecognised file type, unable to instantiate Parser") diff --git a/backend/condition/parsing/peabody_parser.py b/backend/condition/parsing/peabody_parser.py new file mode 100644 index 00000000..e276e48e --- /dev/null +++ b/backend/condition/parsing/peabody_parser.py @@ -0,0 +1,7 @@ +from typing import Any, BinaryIO +from backend.condition.parsing.parser import Parser + + +class PeabodyParser(Parser): + def parse(self, file_stream: BinaryIO) -> Any: + raise NotImplementedError \ No newline at end of file diff --git a/backend/condition/parsing/records/peabody/peabody_asset_condition.py b/backend/condition/parsing/records/peabody/peabody_asset_condition.py new file mode 100644 index 00000000..5682d13a --- /dev/null +++ b/backend/condition/parsing/records/peabody/peabody_asset_condition.py @@ -0,0 +1,22 @@ +from dataclasses import dataclass +from datetime import datetime +from typing import Optional + +@dataclass +class PeabodyAssetCondition: + lo_reference: str + full_address: str + location_type_code: int + parent_lo_reference: str + element_code: int + element: int + sub_element_code: int + sub_element: str + material_code: int + material_or_answer: str + renewal_quantity: int + renewal: int + cloned: str + lo_type_code: int + renewal_cost: Optional[float] = None + condition_survey_date: Optional[datetime] = None \ No newline at end of file diff --git a/backend/condition/parsing/records/peabody/peabody_property.py b/backend/condition/parsing/records/peabody/peabody_property.py new file mode 100644 index 00000000..1bff1b55 --- /dev/null +++ b/backend/condition/parsing/records/peabody/peabody_property.py @@ -0,0 +1,9 @@ +from dataclasses import dataclass +from typing import List + +from backend.condition.parsing.records.peabody.peabody_asset_condition import PeabodyAssetCondition + +@dataclass +class PeabodyProperty: + uprn: int + assets: List[PeabodyAssetCondition] \ No newline at end of file diff --git a/backend/condition/tests/parsing/test_parsing_factory.py b/backend/condition/tests/parsing/test_parsing_factory.py index 481418d7..e2b478ff 100644 --- a/backend/condition/tests/parsing/test_parsing_factory.py +++ b/backend/condition/tests/parsing/test_parsing_factory.py @@ -11,5 +11,16 @@ def test_selects_lbwf_parser(): # act actual_class_name = select_parser(file_type).__class__.__name__ + # assert + assert expected_class_name == actual_class_name + +def test_selects_peabody_parser(): + # arrange + file_type = FileType.Peabody + expected_class_name = "PeabodyParser" + + # act + actual_class_name = select_parser(file_type).__class__.__name__ + # assert assert expected_class_name == actual_class_name \ No newline at end of file diff --git a/backend/condition/tests/parsing/test_peabody_parser.py b/backend/condition/tests/parsing/test_peabody_parser.py new file mode 100644 index 00000000..5196e65d --- /dev/null +++ b/backend/condition/tests/parsing/test_peabody_parser.py @@ -0,0 +1,124 @@ +from typing import Any +import pytest +from io import BytesIO +from openpyxl import Workbook +from datetime import datetime + +from backend.condition.parsing.peabody_parser import PeabodyParser +from backend.condition.parsing.records.peabody.peabody_asset_condition import PeabodyAssetCondition +from backend.condition.parsing.records.peabody.peabody_property import PeabodyProperty + +@pytest.fixture +def peabody_assets_xlsx_bytes() -> BytesIO: + wb = Workbook() + survey_records_d_and_lower = wb.active + survey_records_d_and_lower.title = "Survey Records - D & Lower" + survey_records_d_and_lower.append([ + "Lo_Reference", + "full_address", + "location_type_code", + "Parent_Lo_Reference", + "Element_Code", + "Element", + "Sub_Element_Code", + "Sub_Element", + "Material_Code", + "material_or_answer", + "Renewal_Quantity", + "Renewal_Year", + "Renewal_Cost", + "cloned", + "lo_type_code", + "condition_survey_date", + ]) + survey_records_d_and_lower.append([ + "B000RAND", + "1-11 RANDOM HOUSE LONDON", + 3, + "RAND2EST", + 110, + "ROOFS", + 1, + "Primary Roof", + 9, + "Other", + 3, + 2054, + 330, + "N", + 3, + datetime(2025,12,4,9,17,0) + ]) + survey_records_d_and_lower.append([ + "B000FAKE", + "3-10 FAKE CLOSE LONDON", + 3, + "FAKEEST", + 100, + "GENERAL", + 15, + "External Decoration", + 2, + "Normal", + 1, + 2035, + 1500.7, + "N", + 3, + datetime(2025,7,5,0,0,0) + ]) + survey_records_d_and_lower.append([ + "B000MIS", + "99 MISC ROAD LONDON", + 3, + "300828", + 54, + "HHSRS", + 29, + "HHSRS Structural Collapse & Falling Elements", + 4, + "HHSRS Moderate", + 2, + 2027, + None, + "N", + 3, + None + ]) + survey_records_d_and_lower.append([ + "B000MIS", + "99 MISC ROAD LONDON", + 3, + "300828", + 53, + "External", + 2, + "Chimney", + 2, + "Present", + 33, + 2053, + 3531, + "N", + 3, + None + ]) + + + stream = BytesIO() + wb.save(stream) + stream.seek(0) + + return stream + +def test_peabody_parser_parses_conditions(peabody_assets_xlsx_bytes): + # arrange + parser = PeabodyParser() + + # act + result: Any = parser.parse(peabody_assets_xlsx_bytes) + + # assert + assert len(result) == 3 + + assert all(isinstance(item, PeabodyProperty) for item in result) From 4e190328cc93481a025a35291c73ac3ea43259dd Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Wed, 21 Jan 2026 16:25:58 +0000 Subject: [PATCH 09/68] =?UTF-8?q?Parse=20Peabody=20condition=20data=20xlsx?= =?UTF-8?q?=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/condition/parsing/lbwf_parser.py | 8 +- backend/condition/parsing/peabody_parser.py | 142 +++++++++++++++++- .../peabody/peabody_asset_condition.py | 2 +- .../records/peabody/peabody_property.py | 2 + 4 files changed, 147 insertions(+), 7 deletions(-) diff --git a/backend/condition/parsing/lbwf_parser.py b/backend/condition/parsing/lbwf_parser.py index 8d52f6d5..63512c41 100644 --- a/backend/condition/parsing/lbwf_parser.py +++ b/backend/condition/parsing/lbwf_parser.py @@ -8,13 +8,13 @@ from backend.condition.parsing.records.lbwf.lbwf_house import LbwfHouse from backend.condition.utils.date_utils import normalise_date from utils.logger import setup_logger -logger = setup_logger +logger = setup_logger() class LbwfParser(Parser): def parse(self, file_stream: BinaryIO) -> Any: wb: Workbook = load_workbook(file_stream) - address_to_uprn_map: Dict[str, int] = self._generate_address_to_uprn_dict(wb) + address_to_uprn_map: Dict[str, int] = LbwfParser._generate_address_to_uprn_dict(wb) assets = self._parse_assets(wb) houses = self._parse_houses(wb, address_to_uprn_map) @@ -132,7 +132,7 @@ class LbwfParser(Parser): @staticmethod def _generate_address_to_uprn_dict(wb: Workbook) -> Dict[str, int | None]: - sheet: Workbook = wb["All Energy Breakdown "] + sheet = wb["All Energy Breakdown "] rows: Iterator[Tuple[object | None, ...]] = sheet.iter_rows(values_only=True) @@ -159,6 +159,7 @@ class LbwfParser(Parser): return mapping + @staticmethod def _get_column_indexes_by_name( headers: Tuple[object | None, ...] ) -> Dict[str, int]: @@ -170,6 +171,7 @@ class LbwfParser(Parser): return index + @staticmethod def _get_uprn_from_address(address: str, address_to_uprn_map: Dict[str, int]) -> int | None: pseudo_name = address.split(",")[0] diff --git a/backend/condition/parsing/peabody_parser.py b/backend/condition/parsing/peabody_parser.py index e276e48e..d2229e1c 100644 --- a/backend/condition/parsing/peabody_parser.py +++ b/backend/condition/parsing/peabody_parser.py @@ -1,7 +1,143 @@ -from typing import Any, BinaryIO -from backend.condition.parsing.parser import Parser +from typing import Any, BinaryIO, Dict, Iterator, List, Tuple, DefaultDict +from openpyxl import Workbook, load_workbook +from collections import defaultdict +from backend.condition.parsing.parser import Parser +from backend.condition.parsing.records.peabody.peabody_asset_condition import PeabodyAssetCondition +from backend.condition.parsing.records.peabody.peabody_property import PeabodyProperty +from utils.logger import setup_logger + +logger = setup_logger() class PeabodyParser(Parser): def parse(self, file_stream: BinaryIO) -> Any: - raise NotImplementedError \ No newline at end of file + wb: Workbook = load_workbook(file_stream) + address_to_uprn_map: Dict[str, int] = PeabodyParser._generate_address_to_uprn_dict(wb) + + assets = self._parse_assets(wb) + + return self._group_assets_into_properties( + assets=assets, + address_to_uprn_map=address_to_uprn_map, + ) + + + @staticmethod + def _parse_assets(wb: Workbook) -> List[PeabodyAssetCondition]: + assets_sheet = wb["Survey Records - D & Lower"] + asset_rows = assets_sheet.iter_rows(values_only=True) + + asset_headers = next(asset_rows) + asset_header_indexes = PeabodyParser._get_column_indexes_by_name(asset_headers) + + assets: List[PeabodyAssetCondition] = [] + for row in asset_rows: + try: + assets.append( + PeabodyParser._map_row_to_asset_record(row, asset_header_indexes) + ) + except Exception as e: + logger.error(f"Error mapping Peabody row to asset record: {e}") + continue + + return assets + + @staticmethod + def _group_assets_into_properties( + assets: List[PeabodyAssetCondition], + address_to_uprn_map: Dict[str, int], + ) -> List[PeabodyProperty]: + assets_by_address: DefaultDict[str, List[PeabodyAssetCondition]] = defaultdict(list) + + for asset in assets: + if asset.full_address is None: + continue + + address = asset.full_address + assets_by_address[address].append(asset) + + properties: List[PeabodyProperty] = [] + + for address, grouped_assets in assets_by_address.items(): + uprn = address_to_uprn_map.get(address) + + if uprn is None: + logger.warning(f"No UPRN found for address: {address}") + continue + + properties.append( + PeabodyProperty( + uprn=uprn, + assets=grouped_assets, + ) + ) + + return properties + + + @staticmethod + def _map_row_to_asset_record( + row: Any | Tuple[object | None, ...], + header_indexes: Dict[str, int], + ) -> PeabodyAssetCondition: + return PeabodyAssetCondition( + lo_reference=row[header_indexes["Lo_Reference"]], + full_address=row[header_indexes["full_address"]], + location_type_code=row[header_indexes["location_type_code"]], + parent_lo_reference=row[header_indexes["Parent_Lo_Reference"]], + element_code=row[header_indexes["Element_Code"]], + element=row[header_indexes["Element"]], + sub_element_code=row[header_indexes["Sub_Element_Code"]], + sub_element=row[header_indexes["Sub_Element"]], + material_code=row[header_indexes["Material_Code"]], + material_or_answer=row[header_indexes["material_or_answer"]], + renewal_quantity=row[header_indexes["Renewal_Quantity"]], + renewal_year=row[header_indexes["Renewal_Year"]], + renewal_cost=row[header_indexes["Renewal_Cost"]], + cloned=row[header_indexes["cloned"]], + lo_type_code=row[header_indexes["lo_type_code"]], + condition_survey_date=row[header_indexes["condition_survey_date"]], + ) + + @staticmethod + def _generate_address_to_uprn_dict(wb: Workbook) -> Dict[str, int | None]: + sheet = wb["Survey Records - D & Lower"] + rows: Iterator[Tuple[object | None, ...]] = sheet.iter_rows(values_only=True) + + headers = next(rows) + header_indexes: Dict[str, int] = PeabodyParser._get_column_indexes_by_name(headers) + + address_idx = header_indexes["full_address"] + + + address_to_uprn: Dict[str, int] = {} + # Generate random UPRNs for now + next_uprn = 1 # TODO: get real UPRNs + + for row in rows: + address = row[address_idx] + + if address is None: + continue + + # Optional normalization + address = address.strip() + + if address not in address_to_uprn: + address_to_uprn[address] = next_uprn + next_uprn += 1 + + return address_to_uprn + + + @staticmethod + def _get_column_indexes_by_name( + headers: Tuple[object | None, ...] + ) -> Dict[str, int]: + index: Dict[str, int] = {} + + for i, header in enumerate(headers): + if isinstance(header, str): + index[header] = i + + return index \ No newline at end of file diff --git a/backend/condition/parsing/records/peabody/peabody_asset_condition.py b/backend/condition/parsing/records/peabody/peabody_asset_condition.py index 5682d13a..a82e87f1 100644 --- a/backend/condition/parsing/records/peabody/peabody_asset_condition.py +++ b/backend/condition/parsing/records/peabody/peabody_asset_condition.py @@ -15,7 +15,7 @@ class PeabodyAssetCondition: material_code: int material_or_answer: str renewal_quantity: int - renewal: int + renewal_year: int cloned: str lo_type_code: int renewal_cost: Optional[float] = None diff --git a/backend/condition/parsing/records/peabody/peabody_property.py b/backend/condition/parsing/records/peabody/peabody_property.py index 1bff1b55..bfa6b65b 100644 --- a/backend/condition/parsing/records/peabody/peabody_property.py +++ b/backend/condition/parsing/records/peabody/peabody_property.py @@ -5,5 +5,7 @@ from backend.condition.parsing.records.peabody.peabody_asset_condition import Pe @dataclass class PeabodyProperty: + # This could just be a uprn:assets dict, but making it a dataclass for consistency with + # other client models, might change in future uprn: int assets: List[PeabodyAssetCondition] \ No newline at end of file From ad03d11bc9443b02adb9fc363e5302a257d282e3 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Wed, 21 Jan 2026 17:26:15 +0000 Subject: [PATCH 10/68] fix typo in file type detector --- backend/condition/file_type.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/condition/file_type.py b/backend/condition/file_type.py index 07a0669c..e0736814 100644 --- a/backend/condition/file_type.py +++ b/backend/condition/file_type.py @@ -10,7 +10,7 @@ def detect_file_type(filepath: str) -> FileType: if "lbwf" in path: return FileType.LBWF - if "peadbody" in path: + if "peabody" in path: return FileType.Peabody raise ValueError("Unrecognised file path") \ No newline at end of file From a07020d085d2517737624dd24d42bba37316e1f3 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Wed, 21 Jan 2026 17:33:14 +0000 Subject: [PATCH 11/68] =?UTF-8?q?Detect=20block-level=20asset=20conditions?= =?UTF-8?q?=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peabody/peabody_asset_condition.py | 6 +++- .../tests/parsing/test_peabody_parser.py | 31 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/backend/condition/parsing/records/peabody/peabody_asset_condition.py b/backend/condition/parsing/records/peabody/peabody_asset_condition.py index a82e87f1..5451d570 100644 --- a/backend/condition/parsing/records/peabody/peabody_asset_condition.py +++ b/backend/condition/parsing/records/peabody/peabody_asset_condition.py @@ -19,4 +19,8 @@ class PeabodyAssetCondition: cloned: str lo_type_code: int renewal_cost: Optional[float] = None - condition_survey_date: Optional[datetime] = None \ No newline at end of file + condition_survey_date: Optional[datetime] = None + + @property + def is_block_level(self) -> bool: + raise NotImplementedError \ No newline at end of file diff --git a/backend/condition/tests/parsing/test_peabody_parser.py b/backend/condition/tests/parsing/test_peabody_parser.py index 5196e65d..830e8f2c 100644 --- a/backend/condition/tests/parsing/test_peabody_parser.py +++ b/backend/condition/tests/parsing/test_peabody_parser.py @@ -122,3 +122,34 @@ def test_peabody_parser_parses_conditions(peabody_assets_xlsx_bytes): assert len(result) == 3 assert all(isinstance(item, PeabodyProperty) for item in result) + +def test_peabody_asset_is_block_level(): + # arrange + asset_condition = PeabodyAssetCondition( + lo_reference="", + full_address="1-80 PRINCESS ALICE HOUSE LONDON", + location_type_code=0, + parent_lo_reference="", + element_code=0, + element="", + sub_element_code=0, + sub_element="", + material_code=0, + material_or_answer="", + renewal_quantity=0, + renewal_year=2026, + cloned="", + lo_type_code=0, + renewal_cost=None, + condition_survey_date=None + ) + + expected_block_level = True + + # act + actual_block_level = asset_condition.is_block_level + + # assert + assert expected_block_level == actual_block_level + + \ No newline at end of file From 187d7fbadd2284f58c31e89f30e91f13352ceb7e Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 22 Jan 2026 09:06:05 +0000 Subject: [PATCH 12/68] =?UTF-8?q?Detect=20block-level=20asset=20conditions?= =?UTF-8?q?=20-=20additional=20test=20cases=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tests/parsing/test_peabody_parser.py | 69 +++++++++++-------- 1 file changed, 41 insertions(+), 28 deletions(-) diff --git a/backend/condition/tests/parsing/test_peabody_parser.py b/backend/condition/tests/parsing/test_peabody_parser.py index 830e8f2c..63ed0799 100644 --- a/backend/condition/tests/parsing/test_peabody_parser.py +++ b/backend/condition/tests/parsing/test_peabody_parser.py @@ -1,5 +1,5 @@ -from typing import Any import pytest +from typing import Any from io import BytesIO from openpyxl import Workbook from datetime import datetime @@ -123,33 +123,46 @@ def test_peabody_parser_parses_conditions(peabody_assets_xlsx_bytes): assert all(isinstance(item, PeabodyProperty) for item in result) -def test_peabody_asset_is_block_level(): - # arrange - asset_condition = PeabodyAssetCondition( - lo_reference="", - full_address="1-80 PRINCESS ALICE HOUSE LONDON", - location_type_code=0, - parent_lo_reference="", - element_code=0, - element="", - sub_element_code=0, - sub_element="", - material_code=0, - material_or_answer="", - renewal_quantity=0, - renewal_year=2026, - cloned="", - lo_type_code=0, - renewal_cost=None, - condition_survey_date=None - ) +@pytest.fixture +def asset_condition_factory(): + def _factory(full_address: str) -> PeabodyAssetCondition: + return PeabodyAssetCondition( + lo_reference="", + full_address=full_address, + location_type_code=0, + parent_lo_reference="", + element_code=0, + element="", + sub_element_code=0, + sub_element="", + material_code=0, + material_or_answer="", + renewal_quantity=0, + renewal_year=2026, + cloned="", + lo_type_code=0, + renewal_cost=None, + condition_survey_date=None, + ) - expected_block_level = True + return _factory - # act - actual_block_level = asset_condition.is_block_level +@pytest.mark.parametrize( + "full_address, expected_block_level", + [ + ("1-80 PRINCESS ALICE HOUSE LONDON", True), + ("FLATS A-D 7 ST CHARLES SQUARE LONDON", True), + ("9A-9H HEDGEGATE COURT LONDON", True), + ("BLOCK MILNE HOUSE LONDON", True), + ("25 HAVERSHAM COURT GREENFORD", False), + ("FLAT 10 SPARROW COURT SOUTHMERE DRIVE LONDON SE2 9ES", False) + ], +) +def test_peabody_asset_is_block_level( + asset_condition_factory, + full_address, + expected_block_level, +): + asset_condition = asset_condition_factory(full_address) - # assert - assert expected_block_level == actual_block_level - - \ No newline at end of file + assert asset_condition.is_block_level == expected_block_level \ No newline at end of file From 80f3325cf07c7a5820f3efaa1bc1e34f628cead2 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 22 Jan 2026 09:08:15 +0000 Subject: [PATCH 13/68] =?UTF-8?q?Detect=20block-level=20asset=20conditions?= =?UTF-8?q?=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../records/peabody/peabody_asset_condition.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/backend/condition/parsing/records/peabody/peabody_asset_condition.py b/backend/condition/parsing/records/peabody/peabody_asset_condition.py index 5451d570..71fa1e9d 100644 --- a/backend/condition/parsing/records/peabody/peabody_asset_condition.py +++ b/backend/condition/parsing/records/peabody/peabody_asset_condition.py @@ -1,3 +1,5 @@ +import re + from dataclasses import dataclass from datetime import datetime from typing import Optional @@ -23,4 +25,15 @@ class PeabodyAssetCondition: @property def is_block_level(self) -> bool: - raise NotImplementedError \ No newline at end of file + if not self.full_address: + return False + + address = self.full_address.upper() + + block_level_patterns = [ + r"\bBLOCK\b", # "BLOCK MILNE HOUSE" + r"\bFLATS\b", # "FLATS A-D ..." + r"\b\d+[A-Z]?-\d+[A-Z]?\b", # "1-80", "9A-9H" + ] + + return any(re.search(pattern, address) for pattern in block_level_patterns) \ No newline at end of file From f8db0cadbafa33fee75419c8fb38c55cadf04f75 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 22 Jan 2026 09:15:37 +0000 Subject: [PATCH 14/68] Ignore block level assets during parsing --- backend/condition/parsing/peabody_parser.py | 7 +++--- .../peabody/peabody_asset_condition.py | 2 +- .../tests/parsing/test_peabody_parser.py | 24 +++++++++++++++++-- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/backend/condition/parsing/peabody_parser.py b/backend/condition/parsing/peabody_parser.py index d2229e1c..b053f2ea 100644 --- a/backend/condition/parsing/peabody_parser.py +++ b/backend/condition/parsing/peabody_parser.py @@ -33,9 +33,10 @@ class PeabodyParser(Parser): assets: List[PeabodyAssetCondition] = [] for row in asset_rows: try: - assets.append( - PeabodyParser._map_row_to_asset_record(row, asset_header_indexes) - ) + asset = PeabodyParser._map_row_to_asset_record(row, asset_header_indexes) + if not asset.is_block_level: + assets.append(asset) + except Exception as e: logger.error(f"Error mapping Peabody row to asset record: {e}") continue diff --git a/backend/condition/parsing/records/peabody/peabody_asset_condition.py b/backend/condition/parsing/records/peabody/peabody_asset_condition.py index 71fa1e9d..b1624999 100644 --- a/backend/condition/parsing/records/peabody/peabody_asset_condition.py +++ b/backend/condition/parsing/records/peabody/peabody_asset_condition.py @@ -32,7 +32,7 @@ class PeabodyAssetCondition: block_level_patterns = [ r"\bBLOCK\b", # "BLOCK MILNE HOUSE" - r"\bFLATS\b", # "FLATS A-D ..." + r"\bFLATS\b", # "FLATS A-D ..." r"\b\d+[A-Z]?-\d+[A-Z]?\b", # "1-80", "9A-9H" ] diff --git a/backend/condition/tests/parsing/test_peabody_parser.py b/backend/condition/tests/parsing/test_peabody_parser.py index 63ed0799..fb0e9d51 100644 --- a/backend/condition/tests/parsing/test_peabody_parser.py +++ b/backend/condition/tests/parsing/test_peabody_parser.py @@ -33,7 +33,25 @@ def peabody_assets_xlsx_bytes() -> BytesIO: ]) survey_records_d_and_lower.append([ "B000RAND", - "1-11 RANDOM HOUSE LONDON", + "1 RANDOM HOUSE LONDON", + 3, + "RAND2EST", + 110, + "ROOFS", + 1, + "Primary Roof", + 9, + "Other", + 3, + 2054, + 330, + "N", + 3, + datetime(2025,12,4,9,17,0) + ]) + survey_records_d_and_lower.append([ + "B000BLOCK", + "1100 BLOCK", 3, "RAND2EST", 110, @@ -51,7 +69,7 @@ def peabody_assets_xlsx_bytes() -> BytesIO: ]) survey_records_d_and_lower.append([ "B000FAKE", - "3-10 FAKE CLOSE LONDON", + "3 FAKE CLOSE LONDON", 3, "FAKEEST", 100, @@ -163,6 +181,8 @@ def test_peabody_asset_is_block_level( full_address, expected_block_level, ): + # arrange asset_condition = asset_condition_factory(full_address) + # act + assert assert asset_condition.is_block_level == expected_block_level \ No newline at end of file From 3cdc871aaea0575be8d97f0ea5a391681b8e8962 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 22 Jan 2026 09:23:01 +0000 Subject: [PATCH 15/68] =?UTF-8?q?Detect=20block-level=20asset=20conditions?= =?UTF-8?q?=20-=20additional=20test=20cases=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/condition/tests/parsing/test_peabody_parser.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/condition/tests/parsing/test_peabody_parser.py b/backend/condition/tests/parsing/test_peabody_parser.py index fb0e9d51..32ff79d8 100644 --- a/backend/condition/tests/parsing/test_peabody_parser.py +++ b/backend/condition/tests/parsing/test_peabody_parser.py @@ -172,6 +172,8 @@ def asset_condition_factory(): ("FLATS A-D 7 ST CHARLES SQUARE LONDON", True), ("9A-9H HEDGEGATE COURT LONDON", True), ("BLOCK MILNE HOUSE LONDON", True), + ("81A-B GORE ROAD LONDON", True), + ("73 & 74 HARVEST COURT ST. ALBANS", True), ("25 HAVERSHAM COURT GREENFORD", False), ("FLAT 10 SPARROW COURT SOUTHMERE DRIVE LONDON SE2 9ES", False) ], From 9ccbbafb29817d64f0e4c68262b663f04bc80331 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 22 Jan 2026 09:24:23 +0000 Subject: [PATCH 16/68] =?UTF-8?q?Detect=20block-level=20asset=20conditions?= =?UTF-8?q?=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../parsing/records/peabody/peabody_asset_condition.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/backend/condition/parsing/records/peabody/peabody_asset_condition.py b/backend/condition/parsing/records/peabody/peabody_asset_condition.py index b1624999..01215a26 100644 --- a/backend/condition/parsing/records/peabody/peabody_asset_condition.py +++ b/backend/condition/parsing/records/peabody/peabody_asset_condition.py @@ -31,9 +31,11 @@ class PeabodyAssetCondition: address = self.full_address.upper() block_level_patterns = [ - r"\bBLOCK\b", # "BLOCK MILNE HOUSE" - r"\bFLATS\b", # "FLATS A-D ..." - r"\b\d+[A-Z]?-\d+[A-Z]?\b", # "1-80", "9A-9H" + r"\bBLOCK\b", # BLOCK MILNE HOUSE + r"\bFLATS\b", # FLATS A-D + r"\b\d+[A-Z]?-\d+[A-Z]?\b", # 1-80, 9A-9H + r"\b\d+[A-Z]-[A-Z]\b", # 81A-B + r"\b\d+\s*&\s*\d+\b", # 73 & 74 ] return any(re.search(pattern, address) for pattern in block_level_patterns) \ No newline at end of file From 1634e04dda32bc4159b56a98970d68cd1779b5f4 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 22 Jan 2026 09:30:50 +0000 Subject: [PATCH 17/68] consistent address trimming when assmebling property objects --- backend/condition/parsing/peabody_parser.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/backend/condition/parsing/peabody_parser.py b/backend/condition/parsing/peabody_parser.py index b053f2ea..17a955d7 100644 --- a/backend/condition/parsing/peabody_parser.py +++ b/backend/condition/parsing/peabody_parser.py @@ -54,7 +54,7 @@ class PeabodyParser(Parser): if asset.full_address is None: continue - address = asset.full_address + address = asset.full_address.strip() assets_by_address[address].append(asset) properties: List[PeabodyProperty] = [] @@ -121,7 +121,6 @@ class PeabodyParser(Parser): if address is None: continue - # Optional normalization address = address.strip() if address not in address_to_uprn: From a7201b0dc406a50f43b8307ed765b57458da0523 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 22 Jan 2026 12:20:17 +0000 Subject: [PATCH 18/68] Start writing peabody mapper. Now rethink model before continuing --- .../condition/domain/mapping/lbwf_mapper.py | 1 - .../domain/mapping/peabody_mapper.py | 14 +++++++ backend/condition/parsing/peabody_parser.py | 4 +- .../tests/mapping/test_peabody_mapper.py | 38 +++++++++++++++++++ 4 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 backend/condition/domain/mapping/peabody_mapper.py create mode 100644 backend/condition/tests/mapping/test_peabody_mapper.py diff --git a/backend/condition/domain/mapping/lbwf_mapper.py b/backend/condition/domain/mapping/lbwf_mapper.py index 0af21b7a..8b556284 100644 --- a/backend/condition/domain/mapping/lbwf_mapper.py +++ b/backend/condition/domain/mapping/lbwf_mapper.py @@ -1,5 +1,4 @@ from typing import Any, List, Optional -from datetime import datetime, date from backend.condition.domain.asset_condition import AssetCondition from backend.condition.domain.element import Element diff --git a/backend/condition/domain/mapping/peabody_mapper.py b/backend/condition/domain/mapping/peabody_mapper.py new file mode 100644 index 00000000..88d0a626 --- /dev/null +++ b/backend/condition/domain/mapping/peabody_mapper.py @@ -0,0 +1,14 @@ +from typing import Any, List, Optional + +from backend.condition.domain.asset_condition import AssetCondition +from backend.condition.domain.element import Element +from backend.condition.domain.mapping.mapper import Mapper +from backend.condition.parsing.records.peabody.peabody_asset_condition import PeabodyAssetCondition +from backend.condition.parsing.records.peabody.peabody_property import PeabodyProperty +from utils.logger import setup_logger + +logger = setup_logger() + +class PeabodyMapper(Mapper): + def map_asset_conditions_for_property(self, client_data: Any, survey_year: Optional[int]) -> List[AssetCondition]: + raise NotImplementedError \ No newline at end of file diff --git a/backend/condition/parsing/peabody_parser.py b/backend/condition/parsing/peabody_parser.py index 17a955d7..b8a548a7 100644 --- a/backend/condition/parsing/peabody_parser.py +++ b/backend/condition/parsing/peabody_parser.py @@ -35,7 +35,9 @@ class PeabodyParser(Parser): try: asset = PeabodyParser._map_row_to_asset_record(row, asset_header_indexes) if not asset.is_block_level: - assets.append(asset) + # Block-level condition surveys are out of scope for now + # until we have a wider think on how to handle block + assets.append(asset) # TODO: handle block-level assets except Exception as e: logger.error(f"Error mapping Peabody row to asset record: {e}") diff --git a/backend/condition/tests/mapping/test_peabody_mapper.py b/backend/condition/tests/mapping/test_peabody_mapper.py new file mode 100644 index 00000000..fc70b015 --- /dev/null +++ b/backend/condition/tests/mapping/test_peabody_mapper.py @@ -0,0 +1,38 @@ +from datetime import datetime + +from backend.condition.domain.mapping.peabody_mapper import PeabodyMapper +from backend.condition.domain.element import Element +from backend.condition.parsing.records.peabody.peabody_asset_condition import PeabodyAssetCondition +from backend.condition.parsing.records.peabody.peabody_property import PeabodyProperty +from backend.condition.domain.asset_condition import AssetCondition + +def test_peabody_mapper_maps_property(): + # arrange + peabody_property = PeabodyProperty( + uprn=1, + assets=[ + PeabodyAssetCondition( + lo_reference="1000RAND0000", + full_address="FLAT 1 RANDOM SQUARE FAKE STREET LONDON E1 1EE", + location_type_code=1, + parent_lo_reference="RAND1000", + element_code=50, + element="Internal", + sub_element_code=3, + sub_element="CCU", + material_code=2, + material_or_answer="RCD/MCB CCU", + renewal_quantity=1, + renewal_year=2038, + renewal_cost=500, + cloned="N", + lo_type_code=1, + condition_survey_date=datetime(2024,2,15,0,0,0), + ) + ] + ) + + # act + + # assert + assert False #temp \ No newline at end of file From 3289dc226dbbcbf17f5bce14339b329515c53664 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 22 Jan 2026 13:28:51 +0000 Subject: [PATCH 19/68] Rename Element to LbwfElement --- backend/condition/domain/asset_condition.py | 7 +-- .../domain/{element.py => lbwf_element.py} | 18 +++++-- .../condition/domain/mapping/lbwf_mapper.py | 50 ++++++++++++------- .../domain/mapping/peabody_mapper.py | 13 +++-- .../tests/mapping/test_lbwf_mapper.py | 34 +++++++------ .../tests/mapping/test_peabody_mapper.py | 13 +++-- 6 files changed, 84 insertions(+), 51 deletions(-) rename backend/condition/domain/{element.py => lbwf_element.py} (92%) diff --git a/backend/condition/domain/asset_condition.py b/backend/condition/domain/asset_condition.py index dffbdf88..a489090f 100644 --- a/backend/condition/domain/asset_condition.py +++ b/backend/condition/domain/asset_condition.py @@ -2,13 +2,14 @@ from dataclasses import dataclass from typing import Optional from datetime import date -from backend.condition.domain.element import Element +from backend.condition.domain.lbwf_element import LbwfElement + @dataclass class AssetCondition: uprn: int - element: Element # TODO: should HHSRS elements be handled differently? - condition_description: str # TODO: this probably needs to be some sort of enum so it's searchable/filterable on the frontend. Could be hard to map from string though + element: LbwfElement # TODO: should HHSRS elements be handled differently? + condition_description: str # TODO: this probably needs to be some sort of enum so it's searchable/filterable on the frontend. Could be hard to map from string though quantity: int renewal_year: Optional[int] = None source: Optional[str] = None diff --git a/backend/condition/domain/element.py b/backend/condition/domain/lbwf_element.py similarity index 92% rename from backend/condition/domain/element.py rename to backend/condition/domain/lbwf_element.py index 021c8492..52928c72 100644 --- a/backend/condition/domain/element.py +++ b/backend/condition/domain/lbwf_element.py @@ -1,7 +1,7 @@ from enum import StrEnum -class Element(StrEnum): +class LbwfElement(StrEnum): AHR_CAT = "Accessible Housing Register Category" ASBESTOS = "Asbestos Present" ASSETSAREA = "Assets Area for Decent Homes and Investment" @@ -49,12 +49,20 @@ class Element(StrEnum): EXTWNDWS1 = "Windows 1 in External Area" EXTWNDWS2 = "Windows 2 in External Area" FFHHDAMP = "Fitness for Human Habitation - Serious problem with damp" - FFHHDRNWC = "Fitness for Human Habitation - Problems with the drainage or the lavatories" - FFHHHCWAT = "Fitness for Human Habitation - Problem with the supply of hot and cold water" - FFHHNEGLC = "Fitness for Human Habitation - Building neglected and is in a bad condition" + FFHHDRNWC = ( + "Fitness for Human Habitation - Problems with the drainage or the lavatories" + ) + FFHHHCWAT = ( + "Fitness for Human Habitation - Problem with the supply of hot and cold water" + ) + FFHHNEGLC = ( + "Fitness for Human Habitation - Building neglected and is in a bad condition" + ) FFHHNONAT = "Fitness for Human Habitation - Not enough natural light" FFHHNOVEN = "Fitness for Human Habitation - Not enough ventilation" - FFHHPRPCK = "Fitness for Human Habitation - Difficult to prepare and cook food or wash up" + FFHHPRPCK = ( + "Fitness for Human Habitation - Difficult to prepare and cook food or wash up" + ) FFHHUNLAY = "Fitness for Human Habitation - Unsafe layout" FFHHUNSTA = "Fitness for Human Habitation - Building is unstable" FRARISKRTG = "Fire Risk Assessment Rating" diff --git a/backend/condition/domain/mapping/lbwf_mapper.py b/backend/condition/domain/mapping/lbwf_mapper.py index 8b556284..dcd1d748 100644 --- a/backend/condition/domain/mapping/lbwf_mapper.py +++ b/backend/condition/domain/mapping/lbwf_mapper.py @@ -1,29 +1,37 @@ from typing import Any, List, Optional from backend.condition.domain.asset_condition import AssetCondition -from backend.condition.domain.element import Element +from backend.condition.domain.lbwf_element import LbwfElement from backend.condition.domain.mapping.mapper import Mapper -from backend.condition.parsing.records.lbwf.lbwf_asset_condition import LbwfAssetCondition +from backend.condition.parsing.records.lbwf.lbwf_asset_condition import ( + LbwfAssetCondition, +) from backend.condition.parsing.records.lbwf.lbwf_house import LbwfHouse from utils.logger import setup_logger logger = setup_logger() + class LbwfMapper(Mapper): - - def map_asset_conditions_for_property(self, client_data: Any, survey_year: Optional[int]) -> List[AssetCondition]: - assert isinstance(client_data, LbwfHouse) # TODO: think of a better way to do this + + def map_asset_conditions_for_property( + self, client_data: Any, survey_year: Optional[int] + ) -> List[AssetCondition]: + assert isinstance( + client_data, LbwfHouse + ) # TODO: think of a better way to do this mapped_assets: List[AssetCondition] = [] uprn: int = client_data.uprn for raw_asset in client_data.assets: try: - element: Element = LbwfMapper._map_element(raw_asset.element_code) + element: LbwfElement = LbwfMapper._map_element(raw_asset.element_code) except: - logger.warning(f"Unrecognised LBWF Asset Element Code: {raw_asset.element_code}. Skipping record") + logger.warning( + f"Unrecognised LBWF Asset Element Code: {raw_asset.element_code}. Skipping record" + ) continue - mapped_assets.append( AssetCondition( @@ -31,7 +39,9 @@ class LbwfMapper(Mapper): element=element, condition_description=raw_asset.attribute_code_description, quantity=raw_asset.quantity, - renewal_year=LbwfMapper._calculate_renewal_year(raw_asset, survey_year), + renewal_year=LbwfMapper._calculate_renewal_year( + raw_asset, survey_year + ), source=raw_asset.element_comments, install_date=raw_asset.install_date, ) @@ -39,23 +49,25 @@ class LbwfMapper(Mapper): return mapped_assets + @staticmethod + def _map_element(lbwf_element_code: LbwfAssetCondition) -> LbwfElement: + return LbwfElement[lbwf_element_code] - @staticmethod - def _map_element(lbwf_element_code: LbwfAssetCondition) -> Element: - return Element[lbwf_element_code] - - @staticmethod - def _calculate_renewal_year(lbwf_asset: LbwfAssetCondition, survey_year: Optional[int]) -> Optional[int]: + def _calculate_renewal_year( + lbwf_asset: LbwfAssetCondition, survey_year: Optional[int] + ) -> Optional[int]: remaining_life_years: Optional[int] = lbwf_asset.remaining_life if not remaining_life_years: return None - + if not survey_year: return None - + try: return survey_year + remaining_life_years except: - logger.debug(f"Unable to map LBWF Asset remaining life {remaining_life_years} to renewal year, returning None") - return None \ No newline at end of file + logger.debug( + f"Unable to map LBWF Asset remaining life {remaining_life_years} to renewal year, returning None" + ) + return None diff --git a/backend/condition/domain/mapping/peabody_mapper.py b/backend/condition/domain/mapping/peabody_mapper.py index 88d0a626..4c647380 100644 --- a/backend/condition/domain/mapping/peabody_mapper.py +++ b/backend/condition/domain/mapping/peabody_mapper.py @@ -1,14 +1,19 @@ from typing import Any, List, Optional from backend.condition.domain.asset_condition import AssetCondition -from backend.condition.domain.element import Element +from backend.condition.domain.lbwf_element import LbwfElement from backend.condition.domain.mapping.mapper import Mapper -from backend.condition.parsing.records.peabody.peabody_asset_condition import PeabodyAssetCondition +from backend.condition.parsing.records.peabody.peabody_asset_condition import ( + PeabodyAssetCondition, +) from backend.condition.parsing.records.peabody.peabody_property import PeabodyProperty from utils.logger import setup_logger logger = setup_logger() + class PeabodyMapper(Mapper): - def map_asset_conditions_for_property(self, client_data: Any, survey_year: Optional[int]) -> List[AssetCondition]: - raise NotImplementedError \ No newline at end of file + def map_asset_conditions_for_property( + self, client_data: Any, survey_year: Optional[int] + ) -> List[AssetCondition]: + raise NotImplementedError diff --git a/backend/condition/tests/mapping/test_lbwf_mapper.py b/backend/condition/tests/mapping/test_lbwf_mapper.py index 151e5d19..c007b575 100644 --- a/backend/condition/tests/mapping/test_lbwf_mapper.py +++ b/backend/condition/tests/mapping/test_lbwf_mapper.py @@ -4,10 +4,13 @@ from datetime import date from backend.condition.domain.mapping.lbwf_mapper import LbwfMapper from backend.condition.parsing.records.lbwf.lbwf_house import LbwfHouse -from backend.condition.parsing.records.lbwf.lbwf_asset_condition import LbwfAssetCondition -from backend.condition.domain.element import Element +from backend.condition.parsing.records.lbwf.lbwf_asset_condition import ( + LbwfAssetCondition, +) +from backend.condition.domain.lbwf_element import LbwfElement from backend.condition.domain.asset_condition import AssetCondition + def test_lbwf_mapper_maps_house(): # arrange lbwf_house = LbwfHouse( @@ -162,11 +165,11 @@ def test_lbwf_mapper_maps_house(): element_numerical_value=None, element_text_value=None, quantity=1, - install_date=date(2009,4,1), + install_date=date(2009, 4, 1), remaining_life=26, element_comments="Source of Data = Codeman", ), - ] + ], ) mapper = LbwfMapper() @@ -175,7 +178,7 @@ def test_lbwf_mapper_maps_house(): expected_assets: List[AssetCondition] = [ AssetCondition( uprn=1, - element=Element.AHR_CAT, + element=LbwfElement.AHR_CAT, condition_description="General Needs", quantity=1, renewal_year=None, @@ -184,7 +187,7 @@ def test_lbwf_mapper_maps_house(): ), AssetCondition( uprn=1, - element=Element.FLVL, + element=LbwfElement.FLVL, condition_description="Ground Floor", quantity=1, renewal_year=None, @@ -193,7 +196,7 @@ def test_lbwf_mapper_maps_house(): ), AssetCondition( uprn=1, - element=Element.ASBESTOS, + element=LbwfElement.ASBESTOS, condition_description="Yes", quantity=None, renewal_year=None, @@ -202,7 +205,7 @@ def test_lbwf_mapper_maps_house(): ), AssetCondition( uprn=1, - element=Element.INTBTHRLOC, + element=LbwfElement.INTBTHRLOC, condition_description="Bathroom on Entrance Level in Property", quantity=1, renewal_year=None, @@ -211,7 +214,7 @@ def test_lbwf_mapper_maps_house(): ), AssetCondition( uprn=1, - element=Element.INTCHEXTNT, + element=LbwfElement.INTCHEXTNT, condition_description="No Central Heating in Property", quantity=1, renewal_year=None, @@ -220,7 +223,7 @@ def test_lbwf_mapper_maps_house(): ), AssetCondition( uprn=1, - element=Element.HHSRSFIRE, + element=LbwfElement.HHSRSFIRE, condition_description="Category 4 - Typical Risk", quantity=1, renewal_year=None, @@ -229,18 +232,19 @@ def test_lbwf_mapper_maps_house(): ), AssetCondition( uprn=1, - element=Element.EXTWALLFN1, + element=LbwfElement.EXTWALLFN1, condition_description="Render or Pebbledash Wall Finish 1 in External Area", quantity=1, renewal_year=2052, source="Source of Data = Codeman", - install_date=date(2009,4,1), + install_date=date(2009, 4, 1), ), - ] # act - actual_assets: List[AssetCondition] = mapper.map_asset_conditions_for_property(lbwf_house, survey_year) + actual_assets: List[AssetCondition] = mapper.map_asset_conditions_for_property( + lbwf_house, survey_year + ) # assert - assert actual_assets == expected_assets \ No newline at end of file + assert actual_assets == expected_assets diff --git a/backend/condition/tests/mapping/test_peabody_mapper.py b/backend/condition/tests/mapping/test_peabody_mapper.py index fc70b015..2d2446e5 100644 --- a/backend/condition/tests/mapping/test_peabody_mapper.py +++ b/backend/condition/tests/mapping/test_peabody_mapper.py @@ -1,11 +1,14 @@ from datetime import datetime from backend.condition.domain.mapping.peabody_mapper import PeabodyMapper -from backend.condition.domain.element import Element -from backend.condition.parsing.records.peabody.peabody_asset_condition import PeabodyAssetCondition +from backend.condition.domain.lbwf_element import LbwfElement +from backend.condition.parsing.records.peabody.peabody_asset_condition import ( + PeabodyAssetCondition, +) from backend.condition.parsing.records.peabody.peabody_property import PeabodyProperty from backend.condition.domain.asset_condition import AssetCondition + def test_peabody_mapper_maps_property(): # arrange peabody_property = PeabodyProperty( @@ -27,12 +30,12 @@ def test_peabody_mapper_maps_property(): renewal_cost=500, cloned="N", lo_type_code=1, - condition_survey_date=datetime(2024,2,15,0,0,0), + condition_survey_date=datetime(2024, 2, 15, 0, 0, 0), ) - ] + ], ) # act # assert - assert False #temp \ No newline at end of file + assert False # temp From 03fb727994672c9d389c7c44a129ab2679c4705d Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 22 Jan 2026 15:00:58 +0000 Subject: [PATCH 20/68] =?UTF-8?q?Remodel=20dataclasses=20map=20from=20LBWF?= =?UTF-8?q?=20objects=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/condition/domain/aspect_type.py | 20 +++ backend/condition/domain/asset_condition.py | 25 ++- backend/condition/domain/element.py | 152 ++++++++++++++++++ .../tests/mapping/test_lbwf_mapper.py | 108 ++++++++++--- 4 files changed, 274 insertions(+), 31 deletions(-) create mode 100644 backend/condition/domain/aspect_type.py create mode 100644 backend/condition/domain/element.py diff --git a/backend/condition/domain/aspect_type.py b/backend/condition/domain/aspect_type.py new file mode 100644 index 00000000..45d0f24b --- /dev/null +++ b/backend/condition/domain/aspect_type.py @@ -0,0 +1,20 @@ +from enum import Enum + + +class AspectType(str, Enum): + MATERIAL = "material" + CONDITION = "condition" + TYPE = "type" + CONFIGURATION = "configuration" + PRESENCE = "presence" + RISK = "risk" + SEVERITY = "severity" + LOCATION = "location" + FINISH = "finish" + INSULATION = "insulation" + POINTING = "pointing" + SPALLING = "spalling" + LINTELS = "lintels" + CLADDING = "cladding" + CATEGORY = "category" + QUANTITY = "quantity" diff --git a/backend/condition/domain/asset_condition.py b/backend/condition/domain/asset_condition.py index a489090f..cd57d9ff 100644 --- a/backend/condition/domain/asset_condition.py +++ b/backend/condition/domain/asset_condition.py @@ -1,16 +1,27 @@ from dataclasses import dataclass -from typing import Optional from datetime import date +from typing import Optional +from xml.dom.minidom import Element -from backend.condition.domain.lbwf_element import LbwfElement +from backend.condition.domain.aspect_type import AspectType +from backend.condition.domain.element import Element @dataclass class AssetCondition: uprn: int - element: LbwfElement # TODO: should HHSRS elements be handled differently? - condition_description: str # TODO: this probably needs to be some sort of enum so it's searchable/filterable on the frontend. Could be hard to map from string though - quantity: int - renewal_year: Optional[int] = None - source: Optional[str] = None + + element: Element + aspect_type: AspectType + + value: Optional[str] = None + + quantity: Optional[int] = None install_date: Optional[date] = None + renewal_year: Optional[int] = None + + element_instance: Optional[int] = None + element_location: Optional[str] = None + + source_system: Optional[str] = None + comments: Optional[str] = None diff --git a/backend/condition/domain/element.py b/backend/condition/domain/element.py new file mode 100644 index 00000000..d9698ddf --- /dev/null +++ b/backend/condition/domain/element.py @@ -0,0 +1,152 @@ +from enum import Enum + + +class Element(str, Enum): + + # ====================== + # PROPERTY / GENERAL + # ====================== + PROPERTY_TYPE = "property_type" + PROPERTY_CONSTRUCTION_TYPE = "property_construction_type" + PROPERTY_CLASSIFICATION = "property_classification" + PROPERTY_AGE_BAND = "property_age_band" + STOREY_COUNT = "storey_count" + FLOOR_LEVEL_FRONT_DOOR = "floor_level_front_door" + ACCESSIBLE_HOUSING_REGISTER = "accessible_housing_register" + ASBESTOS = "asbestos" + + # ====================== + # EXTERNAL – ROOF + # ====================== + ROOF_COVERING = "roof_covering" + ROOF_STRUCTURE = "roof_structure" + ROOF_CHIMNEY = "roof_chimney" + ROOF_FASCIA = "roof_fascia" + ROOF_SOFFIT = "roof_soffit" + RAINWATER_GOODS = "rainwater_goods" + ROOF_PORCH_CANOPY = "roof_porch_canopy" + LOFT_INSULATION = "loft_insulation" + + # ====================== + # EXTERNAL – WALLS + # ====================== + EXTERNAL_WALL = "external_wall" + + # ====================== + # EXTERNAL – WINDOWS + # ====================== + WINDOWS = "windows" + COMMUNAL_WINDOWS = "communal_windows" + SECONDARY_GLAZING = "secondary_glazing" + + # ====================== + # EXTERNAL – DOORS + # ====================== + FRONT_DOOR = "front_door" + REAR_DOOR = "rear_door" + STORE_DOOR = "store_door" + GARAGE_DOOR = "garage_door" + COMMUNAL_ENTRANCE_DOOR = "communal_entrance_door" + + # ====================== + # EXTERNAL – AREAS + # ====================== + PATHS_AND_HARDSTANDINGS = "paths_and_hardstandings" + PARKING_AREAS = "parking_areas" + BOUNDARY_WALLS = "boundary_walls" + FENCING = "fencing" + GATES = "gates" + RETAINING_WALLS = "retaining_walls" + PRIVATE_BALCONY = "private_balcony" + BALCONY_BALUSTRADE = "balcony_balustrade" + OUTBUILDINGS = "outbuildings" + GARAGE_STRUCTURE = "garage_structure" + + # ====================== + # INTERNAL – KITCHEN + # ====================== + KITCHEN = "kitchen" + KITCHEN_SPACE_LAYOUT = "kitchen_space_layout" + TENANT_INSTALLED_KITCHEN = "tenant_installed_kitchen" + + # ====================== + # INTERNAL – BATHROOM + # ====================== + BATHROOM = "bathroom" + + # ====================== + # INTERNAL – HEATING / WATER + # ====================== + HEATING_BOILER = "heating_boiler" + HEATING_DISTRIBUTION = "heating_distribution" + HEATING_EXTENT = "heating_extent" + SECONDARY_HEATING = "secondary_heating" + HOT_WATER_SYSTEM = "hot_water_system" + COLD_WATER_STORAGE = "cold_water_storage" + PROGRAMMABLE_HEATING = "programmable_heating" + + # ====================== + # INTERNAL – ELECTRICS / FIRE + # ====================== + ELECTRICAL_WIRING = "electrical_wiring" + CONSUMER_UNIT = "consumer_unit" + SMOKE_DETECTION = "smoke_detection" + HEAT_DETECTION = "heat_detection" + CARBON_MONOXIDE_DETECTION = "carbon_monoxide_detection" + FIRE_DOOR_RATING = "fire_door_rating" + + # ====================== + # COMMUNAL SYSTEMS + # ====================== + COMMUNAL_HEATING = "communal_heating" + COMMUNAL_BOILER = "communal_boiler" + COMMUNAL_ELECTRICS = "communal_electrics" + COMMUNAL_FIRE_ALARM = "communal_fire_alarm" + COMMUNAL_EMERGENCY_LIGHTING = "communal_emergency_lighting" + COMMUNAL_LIFT = "communal_lift" + COMMUNAL_DOOR_ENTRY = "communal_door_entry" + COMMUNAL_CCTV = "communal_cctv" + COMMUNAL_BIN_STORE = "communal_bin_store" + COMMUNAL_REFUSE_CHUTE = "communal_refuse_chute" + + # ========================================================== + # HHSRS – ALL 29 HAZARDS + # ========================================================== + + # --- Physiological requirements (4) + HHSRS_DAMP_AND_MOULD = "hhsrs_damp_and_mould" + HHSRS_EXCESS_COLD = "hhsrs_excess_cold" + HHSRS_EXCESS_HEAT = "hhsrs_excess_heat" + HHSRS_ASBESTOS_AND_MMF = "hhsrs_asbestos_and_mmf" + + # --- Psychological requirements (4) + HHSRS_CROWDING_AND_SPACE = "hhsrs_crowding_and_space" + HHSRS_ENTRY_BY_INTRUDERS = "hhsrs_entry_by_intruders" + HHSRS_LIGHTING = "hhsrs_lighting" + HHSRS_NOISE = "hhsrs_noise" + + # --- Protection against infection (6) + HHSRS_DOMESTIC_HYGIENE_PESTS_REFUSE = "hhsrs_domestic_hygiene_pests_refuse" + HHSRS_FOOD_SAFETY = "hhsrs_food_safety" + HHSRS_PERSONAL_HYGIENE_SANITATION = "hhsrs_personal_hygiene_sanitation" + HHSRS_WATER_SUPPLY = "hhsrs_water_supply" + HHSRS_FALLS_ASSOCIATED_WITH_BATHS = "hhsrs_falls_associated_with_baths" + HHSRS_SURFACES_MOULD = "hhsrs_surfaces_mould" + + # --- Protection against accidents (10) + HHSRS_FALLS_ON_LEVEL_SURFACES = "hhsrs_falls_on_level_surfaces" + HHSRS_FALLS_ON_STAIRS = "hhsrs_falls_on_stairs" + HHSRS_FALLS_BETWEEN_LEVELS = "hhsrs_falls_between_levels" + HHSRS_ELECTRICAL_HAZARDS = "hhsrs_electrical_hazards" + HHSRS_FIRE = "hhsrs_fire" + HHSRS_FLAMES_HOT_SURFACES = "hhsrs_flames_hot_surfaces" + HHSRS_COLLISION_AND_ENTRAPMENT = "hhsrs_collision_and_entrapment" + HHSRS_EXPLOSION = "hhsrs_explosion" + HHSRS_STRUCTURAL_COLLAPSE = "hhsrs_structural_collapse" + HHSRS_UNSAFE_GAS = "hhsrs_unsafe_gas" + + # --- Protection against pollution (4) + HHSRS_CARBON_MONOXIDE = "hhsrs_carbon_monoxide" + HHSRS_LEAD = "hhsrs_lead" + HHSRS_RADIATION = "hhsrs_radiation" + HHSRS_UNCOMBUSTED_FUEL_GAS = "hhsrs_uncombusted_fuel_gas" diff --git a/backend/condition/tests/mapping/test_lbwf_mapper.py b/backend/condition/tests/mapping/test_lbwf_mapper.py index c007b575..f930fdb4 100644 --- a/backend/condition/tests/mapping/test_lbwf_mapper.py +++ b/backend/condition/tests/mapping/test_lbwf_mapper.py @@ -1,7 +1,10 @@ from typing import List +from xml.dom.minidom import Element import pytest from datetime import date +from backend.condition.domain.aspect_type import AspectType +from backend.condition.domain.element import Element from backend.condition.domain.mapping.lbwf_mapper import LbwfMapper from backend.condition.parsing.records.lbwf.lbwf_house import LbwfHouse from backend.condition.parsing.records.lbwf.lbwf_asset_condition import ( @@ -85,6 +88,27 @@ def test_lbwf_mapper_maps_house(): remaining_life=None, element_comments="Source of Data = ACT", ), + LbwfAssetCondition( + prop_ref=100, + domna=100, + address="123 Fake Street, London, A10 1AB", + ownership="LBWF_OWNED", + prop_status="OCCP", + prop_type="HOU", + prop_sub_type="TERRACED", + element_group="ASSETS", + element_code="HHSRSASB", + element_code_description="Asbestos (and MMF)", + attribute_code="TYPRISK", + attribute_code_description="Category 4 - Typical Risk", + element_date_value=None, + element_numerical_value=None, + element_text_value=None, + quantity=None, + install_date=None, + remaining_life=None, + element_comments="Source of Data = ACT", + ), LbwfAssetCondition( prop_ref=100, domna=100, @@ -158,9 +182,9 @@ def test_lbwf_mapper_maps_house(): prop_sub_type="TERRACED", element_group="ASSETS", element_code="EXTWALLFN1", - element_code_description="Wall Finish 1 in External Area", - attribute_code="RENDERPBBL", - attribute_code_description="Render or Pebbledash Wall Finish 1 in External Area", + element_code_description="Wall Finish 2 in External Area", + attribute_code="SMTHRENDER", + attribute_code_description="Smooth Render Wall Finish 1 in External Area", element_date_value=None, element_numerical_value=None, element_text_value=None, @@ -178,66 +202,102 @@ def test_lbwf_mapper_maps_house(): expected_assets: List[AssetCondition] = [ AssetCondition( uprn=1, - element=LbwfElement.AHR_CAT, - condition_description="General Needs", + element=Element.ACCESSIBLE_HOUSING_REGISTER, + aspect_type=AspectType.CATEGORY, + element_instance=None, + value="General Needs", quantity=1, renewal_year=None, - source=None, install_date=None, + comments=None, ), AssetCondition( uprn=1, - element=LbwfElement.FLVL, - condition_description="Ground Floor", + element=Element.FLOOR_LEVEL_FRONT_DOOR, + aspect_type=AspectType.LOCATION, + element_instance=None, + value="Ground Floor", quantity=1, renewal_year=None, - source=None, install_date=None, + comments=None, ), AssetCondition( uprn=1, - element=LbwfElement.ASBESTOS, - condition_description="Yes", + element=Element.ASBESTOS, + aspect_type=AspectType.PRESENCE, + element_instance=None, + value="Yes", quantity=None, renewal_year=None, - source="Source of Data = ACT", install_date=None, + comments="Source of Data = ACT", ), AssetCondition( uprn=1, - element=LbwfElement.INTBTHRLOC, - condition_description="Bathroom on Entrance Level in Property", + element=Element.HHSRS_ASBESTOS_AND_MMF, + aspect_type=AspectType.RISK, + element_instance=None, + value="Category 4 - Typical Risk", + quantity=None, + renewal_year=None, + install_date=None, + comments="Source of Data = ACT", + ), + AssetCondition( + uprn=1, + element=Element.BATHROOM, + aspect_type=AspectType.LOCATION, + element_instance=None, + value="Bathroom on Entrance Level in Property", quantity=1, renewal_year=None, - source="Source of Data = Codeman", install_date=None, + comments="Source of Data = Codeman", ), AssetCondition( uprn=1, - element=LbwfElement.INTCHEXTNT, - condition_description="No Central Heating in Property", + element=Element.HEATING_EXTENT, + aspect_type=AspectType.CONFIGURATION, + element_instance=None, + value="No Central Heating in Property", quantity=1, renewal_year=None, - source="Source of Data = Codeman", install_date=None, + comments="Source of Data = Codeman", ), AssetCondition( uprn=1, - element=LbwfElement.HHSRSFIRE, - condition_description="Category 4 - Typical Risk", + element=Element.HHSRS_FIRE, + aspect_type=AspectType.RISK, + element_instance=None, + value="Category 4 - Typical Risk", quantity=1, renewal_year=None, - source="Source of Data = Morgan Sindall", install_date=None, + comments="Source of Data = Morgan Sindall", ), AssetCondition( uprn=1, - element=LbwfElement.EXTWALLFN1, - condition_description="Render or Pebbledash Wall Finish 1 in External Area", + element=Element.EXTERNAL_WALL, + aspect_type=AspectType.FINISH, + element_instance=1, + value="Render or Pebbledash", quantity=1, renewal_year=2052, - source="Source of Data = Codeman", install_date=date(2009, 4, 1), + comments="Source of Data = Codeman", + ), + AssetCondition( + uprn=1, + element=Element.EXTERNAL_WALL, + aspect_type=AspectType.FINISH, + element_instance=2, + value="Smooth Render Wall Finish 1 in External Area", + quantity=1, + renewal_year=2052, + install_date=date(2009, 4, 1), + comments="Source of Data = Codeman", ), ] From fa72d162395f3e70c34600757afda49c98cacdd1 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 22 Jan 2026 15:21:31 +0000 Subject: [PATCH 21/68] small tidies --- .../condition/domain/mapping/{ => lbwf}/lbwf_mapper.py | 0 .../domain/mapping/{ => peabody}/peabody_mapper.py | 1 - backend/condition/parsing/factory.py | 8 +++++--- backend/condition/processor.py | 5 +++-- backend/condition/tests/mapping/test_lbwf_mapper.py | 3 +-- backend/condition/tests/mapping/test_peabody_mapper.py | 3 +-- 6 files changed, 10 insertions(+), 10 deletions(-) rename backend/condition/domain/mapping/{ => lbwf}/lbwf_mapper.py (100%) rename backend/condition/domain/mapping/{ => peabody}/peabody_mapper.py (90%) diff --git a/backend/condition/domain/mapping/lbwf_mapper.py b/backend/condition/domain/mapping/lbwf/lbwf_mapper.py similarity index 100% rename from backend/condition/domain/mapping/lbwf_mapper.py rename to backend/condition/domain/mapping/lbwf/lbwf_mapper.py diff --git a/backend/condition/domain/mapping/peabody_mapper.py b/backend/condition/domain/mapping/peabody/peabody_mapper.py similarity index 90% rename from backend/condition/domain/mapping/peabody_mapper.py rename to backend/condition/domain/mapping/peabody/peabody_mapper.py index 4c647380..8413b888 100644 --- a/backend/condition/domain/mapping/peabody_mapper.py +++ b/backend/condition/domain/mapping/peabody/peabody_mapper.py @@ -1,7 +1,6 @@ from typing import Any, List, Optional from backend.condition.domain.asset_condition import AssetCondition -from backend.condition.domain.lbwf_element import LbwfElement from backend.condition.domain.mapping.mapper import Mapper from backend.condition.parsing.records.peabody.peabody_asset_condition import ( PeabodyAssetCondition, diff --git a/backend/condition/parsing/factory.py b/backend/condition/parsing/factory.py index 3a28df78..7233a1df 100644 --- a/backend/condition/parsing/factory.py +++ b/backend/condition/parsing/factory.py @@ -1,21 +1,23 @@ -from backend.condition.domain.mapping.lbwf_mapper import LbwfMapper +from backend.condition.domain.mapping.lbwf.lbwf_mapper import LbwfMapper from backend.condition.domain.mapping.mapper import Mapper from backend.condition.file_type import FileType from backend.condition.parsing.parser import Parser from backend.condition.parsing.lbwf_parser import LbwfParser from backend.condition.parsing.peabody_parser import PeabodyParser + def select_parser(file_type: FileType) -> Parser: if file_type is FileType.LBWF: return LbwfParser() - + if file_type is FileType.Peabody: return PeabodyParser() raise ValueError("Unrecognised file type, unable to instantiate Parser") + def select_mapper(file_type: FileType) -> Mapper: if file_type is FileType.LBWF: return LbwfMapper() - + raise ValueError("Unrecognised file type, unable to instantiate Mapper") diff --git a/backend/condition/processor.py b/backend/condition/processor.py index cc44e38a..a48e22f4 100644 --- a/backend/condition/processor.py +++ b/backend/condition/processor.py @@ -8,6 +8,7 @@ from utils.logger import setup_logger from backend.condition.file_type import FileType, detect_file_type from backend.condition.parsing.factory import select_parser, select_mapper + def process_file(file_stream: BinaryIO, source_key: str) -> None: print(f"[processor] Received file: {source_key}") @@ -19,10 +20,10 @@ def process_file(file_stream: BinaryIO, source_key: str) -> None: # Orchestration raw_properties: List[Any] = parser.parse(file_stream) - survey_year = datetime.now().year # TODO: get this from filepath or elsewhere + survey_year = datetime.now().year # TODO: get this from filepath or elsewhere assets: List[AssetCondition] = [] for p in raw_properties: assets.extend(mapper.map_asset_conditions_for_property(p, survey_year)) - print(assets) # temp \ No newline at end of file + print(assets) # temp diff --git a/backend/condition/tests/mapping/test_lbwf_mapper.py b/backend/condition/tests/mapping/test_lbwf_mapper.py index f930fdb4..f4266ac4 100644 --- a/backend/condition/tests/mapping/test_lbwf_mapper.py +++ b/backend/condition/tests/mapping/test_lbwf_mapper.py @@ -5,12 +5,11 @@ from datetime import date from backend.condition.domain.aspect_type import AspectType from backend.condition.domain.element import Element -from backend.condition.domain.mapping.lbwf_mapper import LbwfMapper +from backend.condition.domain.mapping.lbwf.lbwf_mapper import LbwfMapper from backend.condition.parsing.records.lbwf.lbwf_house import LbwfHouse from backend.condition.parsing.records.lbwf.lbwf_asset_condition import ( LbwfAssetCondition, ) -from backend.condition.domain.lbwf_element import LbwfElement from backend.condition.domain.asset_condition import AssetCondition diff --git a/backend/condition/tests/mapping/test_peabody_mapper.py b/backend/condition/tests/mapping/test_peabody_mapper.py index 2d2446e5..de027fe7 100644 --- a/backend/condition/tests/mapping/test_peabody_mapper.py +++ b/backend/condition/tests/mapping/test_peabody_mapper.py @@ -1,7 +1,6 @@ from datetime import datetime -from backend.condition.domain.mapping.peabody_mapper import PeabodyMapper -from backend.condition.domain.lbwf_element import LbwfElement +from backend.condition.domain.mapping.peabody.peabody_mapper import PeabodyMapper from backend.condition.parsing.records.peabody.peabody_asset_condition import ( PeabodyAssetCondition, ) From 212d62e8358eaee6f722b90d295660074d08537c Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 22 Jan 2026 16:28:10 +0000 Subject: [PATCH 22/68] =?UTF-8?q?Map=20to=20new=20dataclasses=20from=20LBW?= =?UTF-8?q?F=20objects=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/condition/domain/aspect_type.py | 9 + backend/condition/domain/asset_condition.py | 1 - backend/condition/domain/element.py | 18 +- .../domain/mapping/lbwf/lbwf_element_map.py | 351 ++++++++++++++++++ .../domain/mapping/lbwf/lbwf_mapper.py | 25 +- .../tests/mapping/test_lbwf_mapper.py | 36 +- 6 files changed, 417 insertions(+), 23 deletions(-) create mode 100644 backend/condition/domain/mapping/lbwf/lbwf_element_map.py diff --git a/backend/condition/domain/aspect_type.py b/backend/condition/domain/aspect_type.py index 45d0f24b..0f9a406a 100644 --- a/backend/condition/domain/aspect_type.py +++ b/backend/condition/domain/aspect_type.py @@ -5,6 +5,7 @@ class AspectType(str, Enum): MATERIAL = "material" CONDITION = "condition" TYPE = "type" + AREA = "area" CONFIGURATION = "configuration" PRESENCE = "presence" RISK = "risk" @@ -18,3 +19,11 @@ class AspectType(str, Enum): CLADDING = "cladding" CATEGORY = "category" QUANTITY = "quantity" + ADEQUACY = "adequacy" + RATING = "rating" + STRATEGY = "strategy" + EXTENT = "extent" + DISTRIBUTION = "distribution" + STRUCTURE = "structure" + COVERING = "covering" + FIRE_RATING = "fire_rating" diff --git a/backend/condition/domain/asset_condition.py b/backend/condition/domain/asset_condition.py index cd57d9ff..1b157a6b 100644 --- a/backend/condition/domain/asset_condition.py +++ b/backend/condition/domain/asset_condition.py @@ -21,7 +21,6 @@ class AssetCondition: renewal_year: Optional[int] = None element_instance: Optional[int] = None - element_location: Optional[str] = None source_system: Optional[str] = None comments: Optional[str] = None diff --git a/backend/condition/domain/element.py b/backend/condition/domain/element.py index d9698ddf..e082bd4f 100644 --- a/backend/condition/domain/element.py +++ b/backend/condition/domain/element.py @@ -6,7 +6,7 @@ class Element(str, Enum): # ====================== # PROPERTY / GENERAL # ====================== - PROPERTY_TYPE = "property_type" + PROPERTY = "property" PROPERTY_CONSTRUCTION_TYPE = "property_construction_type" PROPERTY_CLASSIFICATION = "property_classification" PROPERTY_AGE_BAND = "property_age_band" @@ -14,18 +14,15 @@ class Element(str, Enum): FLOOR_LEVEL_FRONT_DOOR = "floor_level_front_door" ACCESSIBLE_HOUSING_REGISTER = "accessible_housing_register" ASBESTOS = "asbestos" + QUALITY_STANDARD = "quality_standard" # ====================== # EXTERNAL – ROOF # ====================== - ROOF_COVERING = "roof_covering" - ROOF_STRUCTURE = "roof_structure" - ROOF_CHIMNEY = "roof_chimney" - ROOF_FASCIA = "roof_fascia" - ROOF_SOFFIT = "roof_soffit" + ROOF = "roof" RAINWATER_GOODS = "rainwater_goods" - ROOF_PORCH_CANOPY = "roof_porch_canopy" LOFT_INSULATION = "loft_insulation" + PORCH_CANOPY = "porch_canopy" # ====================== # EXTERNAL – WALLS @@ -35,13 +32,14 @@ class Element(str, Enum): # ====================== # EXTERNAL – WINDOWS # ====================== - WINDOWS = "windows" + EXTERNAL_WINDOWS = "external_windows" COMMUNAL_WINDOWS = "communal_windows" SECONDARY_GLAZING = "secondary_glazing" # ====================== # EXTERNAL – DOORS # ====================== + EXTERNAL_DOOR = "external_door" FRONT_DOOR = "front_door" REAR_DOOR = "rear_door" STORE_DOOR = "store_door" @@ -84,6 +82,9 @@ class Element(str, Enum): HOT_WATER_SYSTEM = "hot_water_system" COLD_WATER_STORAGE = "cold_water_storage" PROGRAMMABLE_HEATING = "programmable_heating" + HEATING_SYSTEM = "heating_system" + BOILER_FUEL = "boiler_fuel" + WATER_HEATING = "water_heating" # ====================== # INTERNAL – ELECTRICS / FIRE @@ -94,6 +95,7 @@ class Element(str, Enum): HEAT_DETECTION = "heat_detection" CARBON_MONOXIDE_DETECTION = "carbon_monoxide_detection" FIRE_DOOR_RATING = "fire_door_rating" + FIRE_RISK_ASSESSMENT = "fire" # ====================== # COMMUNAL SYSTEMS diff --git a/backend/condition/domain/mapping/lbwf/lbwf_element_map.py b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py new file mode 100644 index 00000000..6927e2fd --- /dev/null +++ b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py @@ -0,0 +1,351 @@ +from dataclasses import dataclass +from typing import Optional + +from backend.condition.domain.element import Element +from backend.condition.domain.aspect_type import AspectType + + +@dataclass(frozen=True) +class LbwfElementMapping: + element: Element + aspect_type: AspectType + element_instance: Optional[int] = None + + +LBWF_ELEMENT_MAP: dict[str, LbwfElementMapping] = { + # ========================================================== + # PROPERTY / GENERAL + # ========================================================== + "AHR_CAT": LbwfElementMapping( + element=Element.ACCESSIBLE_HOUSING_REGISTER, + aspect_type=AspectType.CATEGORY, + ), + "ASSETSAREA": LbwfElementMapping( + element=Element.PROPERTY, + aspect_type=AspectType.AREA, + ), + # "DECNTHMINC": LbwfElementMapping( + # element=Element.DECENT_HOMES, + # aspect_type=AspectType.INCLUSION, + # ), # Ignore this one + "QUALITYSTD": LbwfElementMapping( + element=Element.QUALITY_STANDARD, + aspect_type=AspectType.TYPE, + ), + "EXTSTOREY": LbwfElementMapping( + element=Element.PROPERTY, + aspect_type=AspectType.CONFIGURATION, + ), + "FLVL": LbwfElementMapping( + element=Element.FLOOR_LEVEL_FRONT_DOOR, + aspect_type=AspectType.LOCATION, + ), + # ========================================================== + # ASBESTOS (NON-HHSRS RECORD) + # ========================================================== + "ASBESTOS": LbwfElementMapping( + element=Element.ASBESTOS, + aspect_type=AspectType.PRESENCE, + ), + # ========================================================== + # INTERNAL – BATHROOMS & KITCHENS + # ========================================================== + "INTBTHRLOC": LbwfElementMapping( + element=Element.BATHROOM, + aspect_type=AspectType.LOCATION, + ), + "INTBTHADEQ": LbwfElementMapping( + element=Element.BATHROOM, + aspect_type=AspectType.ADEQUACY, + ), + "INTKITADEQ": LbwfElementMapping( + element=Element.KITCHEN, + aspect_type=AspectType.ADEQUACY, + ), + "INTCKRLOC": LbwfElementMapping( + element=Element.KITCHEN, + aspect_type=AspectType.LOCATION, + ), + # ========================================================== + # INTERNAL – HEATING + # ========================================================== + "INTCHEXTNT": LbwfElementMapping( + element=Element.HEATING_EXTENT, + aspect_type=AspectType.CONFIGURATION, + ), + "INTCHDIST": LbwfElementMapping( + element=Element.HEATING_DISTRIBUTION, + aspect_type=AspectType.TYPE, + ), + "INTCHBLR": LbwfElementMapping( + element=Element.HEATING_BOILER, + aspect_type=AspectType.TYPE, + ), + # ========================================================== + # INTERNAL – FIRE + # ========================================================== + "FRARISKRTG": LbwfElementMapping( + element=Element.FIRE_RISK_ASSESSMENT, + aspect_type=AspectType.RATING, + ), + "FRATYPE": LbwfElementMapping( + element=Element.FIRE_RISK_ASSESSMENT, + aspect_type=AspectType.TYPE, + ), + "FRAEVACSTR": LbwfElementMapping( + element=Element.FIRE_RISK_ASSESSMENT, + aspect_type=AspectType.STRATEGY, + ), + "INTSMKDET": LbwfElementMapping( + element=Element.SMOKE_DETECTION, + aspect_type=AspectType.PRESENCE, + ), + "INTCHEXTNT": LbwfElementMapping( + element=Element.HEATING_SYSTEM, + aspect_type=AspectType.EXTENT, + ), + # ========================================================== + # HEATING & SERVICES + # ========================================================== + "INTBOILERF": LbwfElementMapping( + element=Element.BOILER_FUEL, + aspect_type=AspectType.TYPE, + ), + "INTHTDISYS": LbwfElementMapping( + element=Element.HEATING_SYSTEM, + aspect_type=AspectType.DISTRIBUTION, + ), + "INTWTRHTNG": LbwfElementMapping( + element=Element.WATER_HEATING, + aspect_type=AspectType.TYPE, + ), + # ========================================================== + # EXTERNAL – WALLS (INSTANCED) + # ========================================================== + "EXTWALLSTR": LbwfElementMapping( + element=Element.EXTERNAL_WALL, + aspect_type=AspectType.STRUCTURE, + element_instance=1, + ), + "EXTWALLFN1": LbwfElementMapping( + element=Element.EXTERNAL_WALL, + aspect_type=AspectType.FINISH, + element_instance=1, + ), + "EXTWALLFN2": LbwfElementMapping( + element=Element.EXTERNAL_WALL, + aspect_type=AspectType.FINISH, + element_instance=2, + ), + "EXTWALLINS": LbwfElementMapping( + element=Element.EXTERNAL_WALL, + aspect_type=AspectType.INSULATION, + ), + "EXTWALLSPL": LbwfElementMapping( + element=Element.EXTERNAL_WALL, + aspect_type=AspectType.CONDITION, + ), + # ========================================================== + # EXTERNAL – ROOFS (INSTANCED) + # ========================================================== + "EXTRFSTR1": LbwfElementMapping( + element=Element.ROOF, + aspect_type=AspectType.STRUCTURE, + element_instance=1, + ), + "EXTRFSTR2": LbwfElementMapping( + element=Element.ROOF, + aspect_type=AspectType.STRUCTURE, + element_instance=2, + ), + "EXTRFSTR3": LbwfElementMapping( + element=Element.ROOF, + aspect_type=AspectType.STRUCTURE, + element_instance=3, + ), + "EXTROOF1": LbwfElementMapping( + element=Element.ROOF, + aspect_type=AspectType.COVERING, + element_instance=1, + ), + "EXTROOF2": LbwfElementMapping( + element=Element.ROOF, + aspect_type=AspectType.COVERING, + element_instance=2, + ), + "EXTROOF3": LbwfElementMapping( + element=Element.ROOF, + aspect_type=AspectType.COVERING, + element_instance=3, + ), + # ========================================================== + # EXTERNAL – DOORS & WINDOWS + # ========================================================== + "INTFRDOOR": LbwfElementMapping( + element=Element.EXTERNAL_DOOR, + aspect_type=AspectType.TYPE, + ), + "INTFRDRFRR": LbwfElementMapping( + element=Element.EXTERNAL_DOOR, + aspect_type=AspectType.FIRE_RATING, + ), + "EXTBKSDDR1": LbwfElementMapping( + element=Element.EXTERNAL_DOOR, + aspect_type=AspectType.TYPE, + element_instance=1, + ), + "EXTBKSDDR2": LbwfElementMapping( + element=Element.EXTERNAL_DOOR, + aspect_type=AspectType.TYPE, + element_instance=2, + ), + "INTWDWTYPE": LbwfElementMapping( + element=Element.EXTERNAL_WINDOWS, + aspect_type=AspectType.TYPE, + ), + "EXTWNDWS1": LbwfElementMapping( + element=Element.EXTERNAL_WINDOWS, + aspect_type=AspectType.TYPE, + element_instance=1, + ), + "EXTWNDWS2": LbwfElementMapping( + element=Element.EXTERNAL_WINDOWS, + aspect_type=AspectType.TYPE, + element_instance=2, + ), + # ========================================================== + # HHSRS – PHYSIOLOGICAL REQUIREMENTS + # ========================================================== + "HHSRSDAMP": LbwfElementMapping( + element=Element.HHSRS_DAMP_AND_MOULD, + aspect_type=AspectType.RISK, + ), + "HHSRSCOLD": LbwfElementMapping( + element=Element.HHSRS_EXCESS_COLD, + aspect_type=AspectType.RISK, + ), + "HHSRSHEAT": LbwfElementMapping( + element=Element.HHSRS_EXCESS_HEAT, + aspect_type=AspectType.RISK, + ), + "HHSRSASB": LbwfElementMapping( + element=Element.HHSRS_ASBESTOS_AND_MMF, + aspect_type=AspectType.RISK, + ), + # ========================================================== + # HHSRS – PSYCHOLOGICAL REQUIREMENTS + # ========================================================== + "HHSRSCROWD": LbwfElementMapping( + element=Element.HHSRS_CROWDING_AND_SPACE, + aspect_type=AspectType.RISK, + ), + "HHSRSENTRY": LbwfElementMapping( + element=Element.HHSRS_ENTRY_BY_INTRUDERS, + aspect_type=AspectType.RISK, + ), + "HHSRSENTRP": LbwfElementMapping( # collision / entrapment + element=Element.HHSRS_COLLISION_AND_ENTRAPMENT, + aspect_type=AspectType.RISK, + ), + "HHSRSLIGHT": LbwfElementMapping( + element=Element.HHSRS_LIGHTING, + aspect_type=AspectType.RISK, + ), + "HHSRSNOISE": LbwfElementMapping( + element=Element.HHSRS_NOISE, + aspect_type=AspectType.RISK, + ), + # ========================================================== + # HHSRS – PROTECTION AGAINST INFECTION + # ========================================================== + "HHSRSDOMES": LbwfElementMapping( + element=Element.HHSRS_DOMESTIC_HYGIENE_PESTS_REFUSE, + aspect_type=AspectType.RISK, + ), + "HHSRSFOOD": LbwfElementMapping( + element=Element.HHSRS_FOOD_SAFETY, + aspect_type=AspectType.RISK, + ), + "HHSRSPERS": LbwfElementMapping( + element=Element.HHSRS_PERSONAL_HYGIENE_SANITATION, + aspect_type=AspectType.RISK, + ), + "HHSRSWATER": LbwfElementMapping( + element=Element.HHSRS_WATER_SUPPLY, + aspect_type=AspectType.RISK, + ), + "HHSRSFBATH": LbwfElementMapping( + element=Element.HHSRS_FALLS_ASSOCIATED_WITH_BATHS, + aspect_type=AspectType.RISK, + ), + "HHSRSPOSI": LbwfElementMapping( + element=Element.HHSRS_SURFACES_MOULD, + aspect_type=AspectType.RISK, + ), + # ========================================================== + # HHSRS – PROTECTION AGAINST ACCIDENTS + # ========================================================== + "HHSRSFLEVE": LbwfElementMapping( + element=Element.HHSRS_FALLS_ON_LEVEL_SURFACES, + aspect_type=AspectType.RISK, + ), + "HHSRSFSTAI": LbwfElementMapping( + element=Element.HHSRS_FALLS_ON_STAIRS, + aspect_type=AspectType.RISK, + ), + "HHSRSFBETW": LbwfElementMapping( + element=Element.HHSRS_FALLS_BETWEEN_LEVELS, + aspect_type=AspectType.RISK, + ), + "HHSRSELEC": LbwfElementMapping( + element=Element.HHSRS_ELECTRICAL_HAZARDS, + aspect_type=AspectType.RISK, + ), + "HHSRSFIRE": LbwfElementMapping( + element=Element.HHSRS_FIRE, + aspect_type=AspectType.RISK, + ), + "HHSRSFLAME": LbwfElementMapping( + element=Element.HHSRS_FLAMES_HOT_SURFACES, + aspect_type=AspectType.RISK, + ), + "HHSRSEXPLO": LbwfElementMapping( + element=Element.HHSRS_EXPLOSION, + aspect_type=AspectType.RISK, + ), + "HHSRSSTRUC": LbwfElementMapping( + element=Element.HHSRS_STRUCTURAL_COLLAPSE, + aspect_type=AspectType.RISK, + ), + # ========================================================== + # HHSRS – PROTECTION AGAINST POLLUTION + # ========================================================== + "HHSRSCO": LbwfElementMapping( + element=Element.HHSRS_CARBON_MONOXIDE, + aspect_type=AspectType.RISK, + ), + "HHSRSFUEL": LbwfElementMapping( + element=Element.HHSRS_UNSAFE_GAS, + aspect_type=AspectType.RISK, + ), + "HHSRSNO2": LbwfElementMapping( + element=Element.HHSRS_UNCOMBUSTED_FUEL_GAS, + aspect_type=AspectType.RISK, + ), + "HHSRSSO2": LbwfElementMapping( + element=Element.HHSRS_UNCOMBUSTED_FUEL_GAS, + aspect_type=AspectType.RISK, + ), + "HHSRSLEAD": LbwfElementMapping( + element=Element.HHSRS_LEAD, + aspect_type=AspectType.RISK, + ), + "HHSRSRADIA": LbwfElementMapping( + element=Element.HHSRS_RADIATION, + aspect_type=AspectType.RISK, + ), + "HHSRSBIOC": LbwfElementMapping( + element=Element.HHSRS_UNCOMBUSTED_FUEL_GAS, + aspect_type=AspectType.RISK, + ), +} diff --git a/backend/condition/domain/mapping/lbwf/lbwf_mapper.py b/backend/condition/domain/mapping/lbwf/lbwf_mapper.py index dcd1d748..01f48a35 100644 --- a/backend/condition/domain/mapping/lbwf/lbwf_mapper.py +++ b/backend/condition/domain/mapping/lbwf/lbwf_mapper.py @@ -1,7 +1,11 @@ from typing import Any, List, Optional from backend.condition.domain.asset_condition import AssetCondition -from backend.condition.domain.lbwf_element import LbwfElement +from backend.condition.domain.element import Element +from backend.condition.domain.mapping.lbwf.lbwf_element_map import ( + LbwfElementMapping, + LBWF_ELEMENT_MAP, +) from backend.condition.domain.mapping.mapper import Mapper from backend.condition.parsing.records.lbwf.lbwf_asset_condition import ( LbwfAssetCondition, @@ -26,7 +30,9 @@ class LbwfMapper(Mapper): uprn: int = client_data.uprn for raw_asset in client_data.assets: try: - element: LbwfElement = LbwfMapper._map_element(raw_asset.element_code) + element_mapping: LbwfElementMapping = LbwfMapper._map_element( + raw_asset.element_code + ) except: logger.warning( f"Unrecognised LBWF Asset Element Code: {raw_asset.element_code}. Skipping record" @@ -36,22 +42,25 @@ class LbwfMapper(Mapper): mapped_assets.append( AssetCondition( uprn=uprn, - element=element, - condition_description=raw_asset.attribute_code_description, + element=element_mapping.element, + aspect_type=element_mapping.aspect_type, + value=raw_asset.attribute_code_description, quantity=raw_asset.quantity, + install_date=raw_asset.install_date, renewal_year=LbwfMapper._calculate_renewal_year( raw_asset, survey_year ), - source=raw_asset.element_comments, - install_date=raw_asset.install_date, + element_instance=element_mapping.element_instance, + source_system=None, # Once we know the system name we'll set it here + comments=raw_asset.element_comments, ) ) return mapped_assets @staticmethod - def _map_element(lbwf_element_code: LbwfAssetCondition) -> LbwfElement: - return LbwfElement[lbwf_element_code] + def _map_element(lbwf_element_code: str) -> LbwfElementMapping: + return LBWF_ELEMENT_MAP[lbwf_element_code] @staticmethod def _calculate_renewal_year( diff --git a/backend/condition/tests/mapping/test_lbwf_mapper.py b/backend/condition/tests/mapping/test_lbwf_mapper.py index f4266ac4..918b6fea 100644 --- a/backend/condition/tests/mapping/test_lbwf_mapper.py +++ b/backend/condition/tests/mapping/test_lbwf_mapper.py @@ -181,9 +181,30 @@ def test_lbwf_mapper_maps_house(): prop_sub_type="TERRACED", element_group="ASSETS", element_code="EXTWALLFN1", + element_code_description="Wall Finish 1 in External Area", + attribute_code="SMTHRENDER", + attribute_code_description="Render or Pebbledash in External Area", + element_date_value=None, + element_numerical_value=None, + element_text_value=None, + quantity=1, + install_date=date(2009, 4, 1), + remaining_life=26, + element_comments="Source of Data = Codeman", + ), + LbwfAssetCondition( + prop_ref=100, + domna=100, + address="123 Fake Street, London, A10 1AB", + ownership="LBWF_OWNED", + prop_status="OCCP", + prop_type="HOU", + prop_sub_type="TERRACED", + element_group="ASSETS", + element_code="EXTWALLFN2", element_code_description="Wall Finish 2 in External Area", attribute_code="SMTHRENDER", - attribute_code_description="Smooth Render Wall Finish 1 in External Area", + attribute_code_description="Smooth Render Wall Finish 2 in External Area", element_date_value=None, element_numerical_value=None, element_text_value=None, @@ -256,8 +277,8 @@ def test_lbwf_mapper_maps_house(): ), AssetCondition( uprn=1, - element=Element.HEATING_EXTENT, - aspect_type=AspectType.CONFIGURATION, + element=Element.HEATING_SYSTEM, + aspect_type=AspectType.EXTENT, element_instance=None, value="No Central Heating in Property", quantity=1, @@ -281,7 +302,7 @@ def test_lbwf_mapper_maps_house(): element=Element.EXTERNAL_WALL, aspect_type=AspectType.FINISH, element_instance=1, - value="Render or Pebbledash", + value="Render or Pebbledash in External Area", quantity=1, renewal_year=2052, install_date=date(2009, 4, 1), @@ -292,7 +313,7 @@ def test_lbwf_mapper_maps_house(): element=Element.EXTERNAL_WALL, aspect_type=AspectType.FINISH, element_instance=2, - value="Smooth Render Wall Finish 1 in External Area", + value="Smooth Render Wall Finish 2 in External Area", quantity=1, renewal_year=2052, install_date=date(2009, 4, 1), @@ -306,4 +327,7 @@ def test_lbwf_mapper_maps_house(): ) # assert - assert actual_assets == expected_assets + assert len(actual_assets) == len(expected_assets) + + for i, (actual, expected) in enumerate(zip(actual_assets, expected_assets)): + assert actual == expected, f"Mismatch at index {i}" From 7f74c892e65acfd524c837dae1fe8b36b0496c4c Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Thu, 22 Jan 2026 17:04:06 +0000 Subject: [PATCH 23/68] make a note of missing element codes and tidy up HHSRS --- backend/condition/domain/element.py | 26 ++-- .../domain/mapping/lbwf/lbwf_element_map.py | 133 +++++++++++------- 2 files changed, 96 insertions(+), 63 deletions(-) diff --git a/backend/condition/domain/element.py b/backend/condition/domain/element.py index e082bd4f..c8fb6167 100644 --- a/backend/condition/domain/element.py +++ b/backend/condition/domain/element.py @@ -115,27 +115,25 @@ class Element(str, Enum): # HHSRS – ALL 29 HAZARDS # ========================================================== - # --- Physiological requirements (4) HHSRS_DAMP_AND_MOULD = "hhsrs_damp_and_mould" HHSRS_EXCESS_COLD = "hhsrs_excess_cold" HHSRS_EXCESS_HEAT = "hhsrs_excess_heat" HHSRS_ASBESTOS_AND_MMF = "hhsrs_asbestos_and_mmf" - - # --- Psychological requirements (4) + HHSRS_BIOCIDES = "hhsrs_biocides" + HHSRS_CARBON_MONOXIDE = "hhsrs_carbon_monoxide" + HHSRS_LEAD = "hhsrs_lead" + HHSRS_RADIATION = "hhsrs_radiation" + HHSRS_UNCOMBUSTED_FUEL_GAS = "hhsrs_uncombusted_fuel_gas" + HHSRS_VOLATILE_ORGANIC_COMPOUNDS = "hhsrs_volatile_organic_compounds" HHSRS_CROWDING_AND_SPACE = "hhsrs_crowding_and_space" HHSRS_ENTRY_BY_INTRUDERS = "hhsrs_entry_by_intruders" HHSRS_LIGHTING = "hhsrs_lighting" HHSRS_NOISE = "hhsrs_noise" - - # --- Protection against infection (6) HHSRS_DOMESTIC_HYGIENE_PESTS_REFUSE = "hhsrs_domestic_hygiene_pests_refuse" HHSRS_FOOD_SAFETY = "hhsrs_food_safety" HHSRS_PERSONAL_HYGIENE_SANITATION = "hhsrs_personal_hygiene_sanitation" HHSRS_WATER_SUPPLY = "hhsrs_water_supply" HHSRS_FALLS_ASSOCIATED_WITH_BATHS = "hhsrs_falls_associated_with_baths" - HHSRS_SURFACES_MOULD = "hhsrs_surfaces_mould" - - # --- Protection against accidents (10) HHSRS_FALLS_ON_LEVEL_SURFACES = "hhsrs_falls_on_level_surfaces" HHSRS_FALLS_ON_STAIRS = "hhsrs_falls_on_stairs" HHSRS_FALLS_BETWEEN_LEVELS = "hhsrs_falls_between_levels" @@ -143,12 +141,8 @@ class Element(str, Enum): HHSRS_FIRE = "hhsrs_fire" HHSRS_FLAMES_HOT_SURFACES = "hhsrs_flames_hot_surfaces" HHSRS_COLLISION_AND_ENTRAPMENT = "hhsrs_collision_and_entrapment" - HHSRS_EXPLOSION = "hhsrs_explosion" + HHSRS_COLLISION_HAZARDS_LOW_HEADROOM = "hhsrs_collision_hazards_low_headroom" + HHSRS_EXPLOSIONS = "hhsrs_explosions" + HHSRS_ERGONOMICS = "hhsrs_ergonomics" HHSRS_STRUCTURAL_COLLAPSE = "hhsrs_structural_collapse" - HHSRS_UNSAFE_GAS = "hhsrs_unsafe_gas" - - # --- Protection against pollution (4) - HHSRS_CARBON_MONOXIDE = "hhsrs_carbon_monoxide" - HHSRS_LEAD = "hhsrs_lead" - HHSRS_RADIATION = "hhsrs_radiation" - HHSRS_UNCOMBUSTED_FUEL_GAS = "hhsrs_uncombusted_fuel_gas" + HHSRS_AMENITIES = "hhsrs_amenities" diff --git a/backend/condition/domain/mapping/lbwf/lbwf_element_map.py b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py index 6927e2fd..da13a6c8 100644 --- a/backend/condition/domain/mapping/lbwf/lbwf_element_map.py +++ b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py @@ -214,7 +214,7 @@ LBWF_ELEMENT_MAP: dict[str, LbwfElementMapping] = { element_instance=2, ), # ========================================================== - # HHSRS – PHYSIOLOGICAL REQUIREMENTS + # HHSRS # ========================================================== "HHSRSDAMP": LbwfElementMapping( element=Element.HHSRS_DAMP_AND_MOULD, @@ -232,9 +232,29 @@ LBWF_ELEMENT_MAP: dict[str, LbwfElementMapping] = { element=Element.HHSRS_ASBESTOS_AND_MMF, aspect_type=AspectType.RISK, ), - # ========================================================== - # HHSRS – PSYCHOLOGICAL REQUIREMENTS - # ========================================================== + "HHSRSBIOCIDES": LbwfElementMapping( + element=Element.HHSRS_BIOCIDES, + aspect_type=AspectType.RISK, + ), + "HHSRSCO": LbwfElementMapping( + element=Element.HHSRS_CARBON_MONOXIDE, + aspect_type=AspectType.RISK, + ), + "HHSRSLEAD": LbwfElementMapping( + element=Element.HHSRS_LEAD, + aspect_type=AspectType.RISK, + ), + "HHSRSRADIA": LbwfElementMapping( + element=Element.HHSRS_RADIATION, + aspect_type=AspectType.RISK, + ), + "HHSRSFUEL": LbwfElementMapping( + element=Element.HHSRS_UNCOMBUSTED_FUEL_GAS, + aspect_type=AspectType.RISK, + ), + "HHSRSORGAN": LbwfElementMapping( + element=Element.HHSRS_VOLATILE_ORGANIC_COMPOUNDS, aspect_type=AspectType.Risk + ), "HHSRSCROWD": LbwfElementMapping( element=Element.HHSRS_CROWDING_AND_SPACE, aspect_type=AspectType.RISK, @@ -243,10 +263,6 @@ LBWF_ELEMENT_MAP: dict[str, LbwfElementMapping] = { element=Element.HHSRS_ENTRY_BY_INTRUDERS, aspect_type=AspectType.RISK, ), - "HHSRSENTRP": LbwfElementMapping( # collision / entrapment - element=Element.HHSRS_COLLISION_AND_ENTRAPMENT, - aspect_type=AspectType.RISK, - ), "HHSRSLIGHT": LbwfElementMapping( element=Element.HHSRS_LIGHTING, aspect_type=AspectType.RISK, @@ -255,9 +271,6 @@ LBWF_ELEMENT_MAP: dict[str, LbwfElementMapping] = { element=Element.HHSRS_NOISE, aspect_type=AspectType.RISK, ), - # ========================================================== - # HHSRS – PROTECTION AGAINST INFECTION - # ========================================================== "HHSRSDOMES": LbwfElementMapping( element=Element.HHSRS_DOMESTIC_HYGIENE_PESTS_REFUSE, aspect_type=AspectType.RISK, @@ -278,13 +291,6 @@ LBWF_ELEMENT_MAP: dict[str, LbwfElementMapping] = { element=Element.HHSRS_FALLS_ASSOCIATED_WITH_BATHS, aspect_type=AspectType.RISK, ), - "HHSRSPOSI": LbwfElementMapping( - element=Element.HHSRS_SURFACES_MOULD, - aspect_type=AspectType.RISK, - ), - # ========================================================== - # HHSRS – PROTECTION AGAINST ACCIDENTS - # ========================================================== "HHSRSFLEVE": LbwfElementMapping( element=Element.HHSRS_FALLS_ON_LEVEL_SURFACES, aspect_type=AspectType.RISK, @@ -309,43 +315,76 @@ LBWF_ELEMENT_MAP: dict[str, LbwfElementMapping] = { element=Element.HHSRS_FLAMES_HOT_SURFACES, aspect_type=AspectType.RISK, ), + "HHSRSENTRP": LbwfElementMapping( + element=Element.HHSRS_COLLISION_AND_ENTRAPMENT, + aspect_type=AspectType.RISK, + ), "HHSRSEXPLO": LbwfElementMapping( - element=Element.HHSRS_EXPLOSION, + element=Element.HHSRS_EXPLOSIONS, aspect_type=AspectType.RISK, ), "HHSRSSTRUC": LbwfElementMapping( element=Element.HHSRS_STRUCTURAL_COLLAPSE, aspect_type=AspectType.RISK, ), - # ========================================================== - # HHSRS – PROTECTION AGAINST POLLUTION - # ========================================================== - "HHSRSCO": LbwfElementMapping( - element=Element.HHSRS_CARBON_MONOXIDE, - aspect_type=AspectType.RISK, + "HHSRSCLOW": LbwfElementMapping( + element=Element.HHSRS_COLLISION_AND_ENTRAPMENT, aspect_type=AspectType.Risk ), - "HHSRSFUEL": LbwfElementMapping( - element=Element.HHSRS_UNSAFE_GAS, - aspect_type=AspectType.RISK, - ), - "HHSRSNO2": LbwfElementMapping( - element=Element.HHSRS_UNCOMBUSTED_FUEL_GAS, - aspect_type=AspectType.RISK, - ), - "HHSRSSO2": LbwfElementMapping( - element=Element.HHSRS_UNCOMBUSTED_FUEL_GAS, - aspect_type=AspectType.RISK, - ), - "HHSRSLEAD": LbwfElementMapping( - element=Element.HHSRS_LEAD, - aspect_type=AspectType.RISK, - ), - "HHSRSRADIA": LbwfElementMapping( - element=Element.HHSRS_RADIATION, - aspect_type=AspectType.RISK, - ), - "HHSRSBIOC": LbwfElementMapping( - element=Element.HHSRS_UNCOMBUSTED_FUEL_GAS, + "HHSRSPOSI": LbwfElementMapping( + element=Element.HHSRS_AMENITIES, aspect_type=AspectType.RISK, ), } + +# Unhandled: +# DECNTHMINC +# EICINSFREQ +# EXTBALCONY +# EXTBPOINTG +# EXTCHIMNEY +# EXTDRPKERB +# EXTDWNPTYP +# EXTEXTDECS +# EXTFASOFBR +# EXTGARDOOR +# EXTGARROOF +# EXTGARSTDR +# EXTGARSTRF +# EXTGARSTWD +# EXTGARWDWS +# EXTGUTRTYP +# EXTHARDSTD +# EXTINTDWNP +# EXTLINTELS +# EXTOUTBOH +# EXTPARKING +# EXTPCHCNPY +# EXTPTFRDR1 +# EXTSTRDOOR +# EXTSTRINSP +# EXTSTRROOF +# EXTSTRWDWS +# FFHHDAMP +# FFHHDRNWC +# FFHHHCWAT +# FFHHNEGLC +# FFHHNONAT +# FFHHNOVEN +# FFHHPRPCK +# FFHHUNLAY +# FFHHUNSTA +# INTACCRAMP +# INTADDWCW +# INTBTHREML +# INTCOMHTG +# INTELECTRC +# INTFLRLVL +# INTGASAVAI +# INTHEATREC +# INTHTIMP +# INTKITREML +# INTLOFTINS +# INTNSEINSL +# INTPROGHTG +# INTSTEPSFD +# INTTNTINST From f4f2ffb59918febe163cc455bc2d8494956716ad Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Fri, 23 Jan 2026 09:58:27 +0000 Subject: [PATCH 24/68] function shell --- .idea/copilot.data.migration.agent.xml | 6 ++++++ recommendations/RoofRecommendations.py | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 .idea/copilot.data.migration.agent.xml diff --git a/.idea/copilot.data.migration.agent.xml b/.idea/copilot.data.migration.agent.xml new file mode 100644 index 00000000..4ea72a91 --- /dev/null +++ b/.idea/copilot.data.migration.agent.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/recommendations/RoofRecommendations.py b/recommendations/RoofRecommendations.py index 1e5636ff..7f7c334e 100644 --- a/recommendations/RoofRecommendations.py +++ b/recommendations/RoofRecommendations.py @@ -119,6 +119,24 @@ class RoofRecommendations: return (full_insulated_room_roof or room_roof_insulated_at_rafters) and not has_non_invasive_recommendation + def recommend_sloping_ceiling(self): + """ + Sloping ceiling insulation recommendations are different from other roof types, though + the description of the roof appears to be quite similar to a roof with a loft. In order to + deduce the roof type, we apply the following logic: + + 1) If the roof is descrbed as pitched, insulated, without a loft insulation thickness, it's + an insulated sloped ceiling + 2) If the roof insulation is assumed, it implies that the surveyor could not gain access to the + roof and therefore it's a loft + 3) If it's a pitched roof that is uninsulated and is NOT assumed, and there is not loft insulation + recommendation, this implies that the surveyor was able to gain access to the roof and there was no + loft insulation recommendation so it must be a sloping ceiling since loft insulation is a default + recommendation for an uninsualted loft + :return: + """ + pass + def recommend(self, phase, measures=None, default_u_values=False): if self.property.roof["has_dwelling_above"]: From 949c9f684de8555bdfae21fee0dbfeece29bef5b Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 23 Jan 2026 11:40:32 +0000 Subject: [PATCH 25/68] fix typos in lbwf element mapper --- backend/condition/domain/mapping/lbwf/lbwf_element_map.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/condition/domain/mapping/lbwf/lbwf_element_map.py b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py index da13a6c8..750a76c6 100644 --- a/backend/condition/domain/mapping/lbwf/lbwf_element_map.py +++ b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py @@ -253,7 +253,7 @@ LBWF_ELEMENT_MAP: dict[str, LbwfElementMapping] = { aspect_type=AspectType.RISK, ), "HHSRSORGAN": LbwfElementMapping( - element=Element.HHSRS_VOLATILE_ORGANIC_COMPOUNDS, aspect_type=AspectType.Risk + element=Element.HHSRS_VOLATILE_ORGANIC_COMPOUNDS, aspect_type=AspectType.RISK ), "HHSRSCROWD": LbwfElementMapping( element=Element.HHSRS_CROWDING_AND_SPACE, @@ -328,7 +328,7 @@ LBWF_ELEMENT_MAP: dict[str, LbwfElementMapping] = { aspect_type=AspectType.RISK, ), "HHSRSCLOW": LbwfElementMapping( - element=Element.HHSRS_COLLISION_AND_ENTRAPMENT, aspect_type=AspectType.Risk + element=Element.HHSRS_COLLISION_AND_ENTRAPMENT, aspect_type=AspectType.RISK ), "HHSRSPOSI": LbwfElementMapping( element=Element.HHSRS_AMENITIES, From d480339ba616ecd50d592b2481a012f1bedd49ce Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 23 Jan 2026 12:09:19 +0000 Subject: [PATCH 26/68] =?UTF-8?q?Map=20to=20dataclasses=20from=20Peabody?= =?UTF-8?q?=20objects=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/condition/domain/lbwf_element.py | 131 --------------- .../domain/mapping/element_mapping.py | 12 ++ .../domain/mapping/lbwf/lbwf_element_map.py | 153 +++++++++--------- .../domain/mapping/lbwf/lbwf_mapper.py | 12 +- backend/condition/domain/mapping/mapper.py | 9 +- .../mapping/peabody/peabody_element_map.py | 40 +++++ .../domain/mapping/peabody/peabody_mapper.py | 2 +- .../tests/mapping/test_peabody_mapper.py | 44 +++-- 8 files changed, 169 insertions(+), 234 deletions(-) delete mode 100644 backend/condition/domain/lbwf_element.py create mode 100644 backend/condition/domain/mapping/element_mapping.py create mode 100644 backend/condition/domain/mapping/peabody/peabody_element_map.py diff --git a/backend/condition/domain/lbwf_element.py b/backend/condition/domain/lbwf_element.py deleted file mode 100644 index 52928c72..00000000 --- a/backend/condition/domain/lbwf_element.py +++ /dev/null @@ -1,131 +0,0 @@ -from enum import StrEnum - - -class LbwfElement(StrEnum): - AHR_CAT = "Accessible Housing Register Category" - ASBESTOS = "Asbestos Present" - ASSETSAREA = "Assets Area for Decent Homes and Investment" - DECNTHMINC = "Include for Decent Homes Reporting - LBWF Stock" - EICINSFREQ = "EICR - Elec Install Conditions Report Inspection Frequency" - EXTBALCONY = "Private Balconies in External Area" - EXTBKSDDR1 = "Back and Side Doors 1 in External Area" - EXTBKSDDR2 = "Back and Side Doors 2 in External Area" - EXTBPOINTG = "Brickwork Pointing in External Area" - EXTCHIMNEY = "Chimneys in External Area" - EXTDWNPTYP = "Downpipes in External Area" - EXTDRPKERB = "Drop Kerb in External Area" - EXTEXTDECS = "External Decorations in External Area" - EXTFASOFBR = "Fascia / Soffit / Bargeboard in External Area" - EXTGARDOOR = "Garage Door in External Area" - EXTGARROOF = "Garage Roof in External Area" - EXTGARSTDR = "Garage and Store Doors in External Area" - EXTGARSTRF = "Garage and Store Roofs in External Area" - EXTGARSTWD = "Garage and Store Windows in External Area" - EXTGARWDWS = "Garage Windows in External Area" - EXTGUTRTYP = "Gutters in External Area" - EXTHARDSTD = "Hardstanding in External Area" - EXTINTDWNP = "Internal Downpipes in External Area" - EXTLINTELS = "Lintels in External Area" - EXTOUTBOH = "Overhaul of Outbuilding in External Area" - EXTPARKING = "Parking in External Area" - EXTPCHCNPY = "Porch and / or Canopy in External Area" - EXTPTFRDR1 = "Patio and French Doors 1 in External Area" - EXTROOF1 = "Roof Covering 1 in External Area" - EXTROOF2 = "Roof Covering 2 in External Area" - EXTROOF3 = "Roof Covering 3 in External Area" - EXTRFSTR1 = "Roof Structure 1 in External Area" - EXTRFSTR2 = "Roof Structure 2 in External Area" - EXTRFSTR3 = "Roof Structure 3 in External Area" - EXTSTOREY = "Number of Storeys within the Property or Block" - EXTSTRDOOR = "Store Door in External Area" - EXTSTRINSP = "Structural Defects in External Area" - EXTSTRROOF = "Store Roof in External Area" - EXTSTRWDWS = "Store Windows in External Area" - EXTWALLFN1 = "Wall Finish 1 in External Area" - EXTWALLFN2 = "Wall Finish 2 in External Area" - EXTWALLINS = "Wall Insulation Improvement in External Area" - EXTWALLSPL = "Wall Spalling in External Area" - EXTWALLSTR = "Wall Structure in External Area" - EXTWNDWS1 = "Windows 1 in External Area" - EXTWNDWS2 = "Windows 2 in External Area" - FFHHDAMP = "Fitness for Human Habitation - Serious problem with damp" - FFHHDRNWC = ( - "Fitness for Human Habitation - Problems with the drainage or the lavatories" - ) - FFHHHCWAT = ( - "Fitness for Human Habitation - Problem with the supply of hot and cold water" - ) - FFHHNEGLC = ( - "Fitness for Human Habitation - Building neglected and is in a bad condition" - ) - FFHHNONAT = "Fitness for Human Habitation - Not enough natural light" - FFHHNOVEN = "Fitness for Human Habitation - Not enough ventilation" - FFHHPRPCK = ( - "Fitness for Human Habitation - Difficult to prepare and cook food or wash up" - ) - FFHHUNLAY = "Fitness for Human Habitation - Unsafe layout" - FFHHUNSTA = "Fitness for Human Habitation - Building is unstable" - FRARISKRTG = "Fire Risk Assessment Rating" - FRAEVACSTR = "Fire Risk Assessment Evacuation Strategy" - FRATYPE = "Fire Risk Assessment Type" - FLVL = "Floor Level of Front Door" - HHSRSASB = "Asbestos (and MMF)" - HHSRSBIOC = "Biocides" - HHSRSCO = "Carbon monoxide" - HHSRSCOLD = "Excess cold" - HHSRSCLOW = "Collision hazards and low headroom" - HHSRSCROWD = "Crowding and space" - HHSRSDAMP = "Damp and mould growth" - HHSRSDOMES = "Domestic hygeine, Pests and Refuse" - HHSRSELEC = "Electrical hazards" - HHSRSENTRP = "Collision and entrapment" - HHSRSENTRY = "Entry by intruders" - HHSRSEXPLO = "Explosions" - HHSRSFBATH = "Falls associated with baths etc" - HHSRSFBETW = "Falling between levels" - HHSRSFIRE = "Fire" - HHSRSFLAME = "Flames, hot surfaces etc" - HHSRSFLEVE = "Falling on level surfaces etc" - HHSRSFOOD = "Food safety" - HHSRSFSTAI = "Falling on stairs etc" - HHSRSFUEL = "Uncombusted fuel gas" - HHSRSHEAT = "Excess heat" - HHSRSLEAD = "Lead" - HHSRSLIGHT = "Lighting" - HHSRSNO2 = "Nitrogen dioxide" - HHSRSNOISE = "Noise" - HHSRSORGAN = "Volatile organic compounds" - HHSRSPERS = "Personal hygeine, Sanitation and Drainage" - HHSRSPOSI = "Position and operability of amenities etc" - HHSRSRADIA = "Radiation" - HHSRSSO2 = "Sulphur dioxide and smoke" - HHSRSSTRUC = "Structural collapse and falling elements" - HHSRSWATER = "Water supply" - INTACCRAMP = "Access Ramp 1:12 Gradient to Property" - INTADDWCW = "Additional WCs and / or WHBs in Property" - INTBTHADEQ = "Adequacy of Bathroom Location in Property" - INTBTHREML = "Source of Bathroom Remaining Life in Property" - INTBTHRLOC = "Location of Bathroom in Property" - INTBOILERF = "Boiler Fuel in Property" - INTCHEXTNT = "Extent of Central Heating in Property" - INTCKRLOC = "Adequacy of Cooker Location in Property" - INTCOMHTG = "Community Heating in Property" - INTELECTRC = "Electrics Required in Property" - INTFLRLVL = "Floor Level Location for Property" - INTFRDOOR = "Type and Location of Front Door in Property" - INTFRDRFRR = "Front Door Fire Rating in Property" - INTGASAVAI = "Gas Available in Property" - INTHEATREC = "Heat Recovery Units in Property" - INTHTDISYS = "Heating Distribution System in Property" - INTHTIMP = "Heating Improvement Required in Property" - INTKITADEQ = "Adequacy of Kitchen and Type in Property" - INTKITREML = "Source of Kitchen Remaining Life in Property" - INTLOFTINS = "Size in mm of Loft Insulation Thickness in Property" - INTNSEINSL = "Adequacy of Noise Insulation in Property" - INTPROGHTG = "Programmable Heating in Property" - INTSMKDET = "Smoke Detectors in Property" - INTSTEPSFD = "Number of Steps to Front Door for Property" - INTTNTINST = "Tenant Installed Kitchen in Property" - INTWDWTYPE = "Windows in Property" - INTWTRHTNG = "Type of Water Heating in Property" - QUALITYSTD = "Quality standard" diff --git a/backend/condition/domain/mapping/element_mapping.py b/backend/condition/domain/mapping/element_mapping.py new file mode 100644 index 00000000..01e1f316 --- /dev/null +++ b/backend/condition/domain/mapping/element_mapping.py @@ -0,0 +1,12 @@ +from dataclasses import dataclass +from typing import Optional +from xml.dom.minidom import Element + +from backend.condition.domain.aspect_type import AspectType + + +@dataclass(frozen=True) +class ElementMapping: + element: Element + aspect_type: AspectType + element_instance: Optional[int] = None diff --git a/backend/condition/domain/mapping/lbwf/lbwf_element_map.py b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py index 750a76c6..047013f4 100644 --- a/backend/condition/domain/mapping/lbwf/lbwf_element_map.py +++ b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py @@ -1,179 +1,170 @@ -from dataclasses import dataclass -from typing import Optional - from backend.condition.domain.element import Element from backend.condition.domain.aspect_type import AspectType +from backend.condition.domain.mapping.element_mapping import ElementMapping -@dataclass(frozen=True) -class LbwfElementMapping: - element: Element - aspect_type: AspectType - element_instance: Optional[int] = None - - -LBWF_ELEMENT_MAP: dict[str, LbwfElementMapping] = { +LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { # ========================================================== # PROPERTY / GENERAL # ========================================================== - "AHR_CAT": LbwfElementMapping( + "AHR_CAT": ElementMapping( element=Element.ACCESSIBLE_HOUSING_REGISTER, aspect_type=AspectType.CATEGORY, ), - "ASSETSAREA": LbwfElementMapping( + "ASSETSAREA": ElementMapping( element=Element.PROPERTY, aspect_type=AspectType.AREA, ), - # "DECNTHMINC": LbwfElementMapping( + # "DECNTHMINC": ElementMapping( # element=Element.DECENT_HOMES, # aspect_type=AspectType.INCLUSION, # ), # Ignore this one - "QUALITYSTD": LbwfElementMapping( + "QUALITYSTD": ElementMapping( element=Element.QUALITY_STANDARD, aspect_type=AspectType.TYPE, ), - "EXTSTOREY": LbwfElementMapping( + "EXTSTOREY": ElementMapping( element=Element.PROPERTY, aspect_type=AspectType.CONFIGURATION, ), - "FLVL": LbwfElementMapping( + "FLVL": ElementMapping( element=Element.FLOOR_LEVEL_FRONT_DOOR, aspect_type=AspectType.LOCATION, ), # ========================================================== # ASBESTOS (NON-HHSRS RECORD) # ========================================================== - "ASBESTOS": LbwfElementMapping( + "ASBESTOS": ElementMapping( element=Element.ASBESTOS, aspect_type=AspectType.PRESENCE, ), # ========================================================== # INTERNAL – BATHROOMS & KITCHENS # ========================================================== - "INTBTHRLOC": LbwfElementMapping( + "INTBTHRLOC": ElementMapping( element=Element.BATHROOM, aspect_type=AspectType.LOCATION, ), - "INTBTHADEQ": LbwfElementMapping( + "INTBTHADEQ": ElementMapping( element=Element.BATHROOM, aspect_type=AspectType.ADEQUACY, ), - "INTKITADEQ": LbwfElementMapping( + "INTKITADEQ": ElementMapping( element=Element.KITCHEN, aspect_type=AspectType.ADEQUACY, ), - "INTCKRLOC": LbwfElementMapping( + "INTCKRLOC": ElementMapping( element=Element.KITCHEN, aspect_type=AspectType.LOCATION, ), # ========================================================== # INTERNAL – HEATING # ========================================================== - "INTCHEXTNT": LbwfElementMapping( + "INTCHEXTNT": ElementMapping( element=Element.HEATING_EXTENT, aspect_type=AspectType.CONFIGURATION, ), - "INTCHDIST": LbwfElementMapping( + "INTCHDIST": ElementMapping( element=Element.HEATING_DISTRIBUTION, aspect_type=AspectType.TYPE, ), - "INTCHBLR": LbwfElementMapping( + "INTCHBLR": ElementMapping( element=Element.HEATING_BOILER, aspect_type=AspectType.TYPE, ), # ========================================================== # INTERNAL – FIRE # ========================================================== - "FRARISKRTG": LbwfElementMapping( + "FRARISKRTG": ElementMapping( element=Element.FIRE_RISK_ASSESSMENT, aspect_type=AspectType.RATING, ), - "FRATYPE": LbwfElementMapping( + "FRATYPE": ElementMapping( element=Element.FIRE_RISK_ASSESSMENT, aspect_type=AspectType.TYPE, ), - "FRAEVACSTR": LbwfElementMapping( + "FRAEVACSTR": ElementMapping( element=Element.FIRE_RISK_ASSESSMENT, aspect_type=AspectType.STRATEGY, ), - "INTSMKDET": LbwfElementMapping( + "INTSMKDET": ElementMapping( element=Element.SMOKE_DETECTION, aspect_type=AspectType.PRESENCE, ), - "INTCHEXTNT": LbwfElementMapping( + "INTCHEXTNT": ElementMapping( element=Element.HEATING_SYSTEM, aspect_type=AspectType.EXTENT, ), # ========================================================== # HEATING & SERVICES # ========================================================== - "INTBOILERF": LbwfElementMapping( + "INTBOILERF": ElementMapping( element=Element.BOILER_FUEL, aspect_type=AspectType.TYPE, ), - "INTHTDISYS": LbwfElementMapping( + "INTHTDISYS": ElementMapping( element=Element.HEATING_SYSTEM, aspect_type=AspectType.DISTRIBUTION, ), - "INTWTRHTNG": LbwfElementMapping( + "INTWTRHTNG": ElementMapping( element=Element.WATER_HEATING, aspect_type=AspectType.TYPE, ), # ========================================================== # EXTERNAL – WALLS (INSTANCED) # ========================================================== - "EXTWALLSTR": LbwfElementMapping( + "EXTWALLSTR": ElementMapping( element=Element.EXTERNAL_WALL, aspect_type=AspectType.STRUCTURE, element_instance=1, ), - "EXTWALLFN1": LbwfElementMapping( + "EXTWALLFN1": ElementMapping( element=Element.EXTERNAL_WALL, aspect_type=AspectType.FINISH, element_instance=1, ), - "EXTWALLFN2": LbwfElementMapping( + "EXTWALLFN2": ElementMapping( element=Element.EXTERNAL_WALL, aspect_type=AspectType.FINISH, element_instance=2, ), - "EXTWALLINS": LbwfElementMapping( + "EXTWALLINS": ElementMapping( element=Element.EXTERNAL_WALL, aspect_type=AspectType.INSULATION, ), - "EXTWALLSPL": LbwfElementMapping( + "EXTWALLSPL": ElementMapping( element=Element.EXTERNAL_WALL, aspect_type=AspectType.CONDITION, ), # ========================================================== # EXTERNAL – ROOFS (INSTANCED) # ========================================================== - "EXTRFSTR1": LbwfElementMapping( + "EXTRFSTR1": ElementMapping( element=Element.ROOF, aspect_type=AspectType.STRUCTURE, element_instance=1, ), - "EXTRFSTR2": LbwfElementMapping( + "EXTRFSTR2": ElementMapping( element=Element.ROOF, aspect_type=AspectType.STRUCTURE, element_instance=2, ), - "EXTRFSTR3": LbwfElementMapping( + "EXTRFSTR3": ElementMapping( element=Element.ROOF, aspect_type=AspectType.STRUCTURE, element_instance=3, ), - "EXTROOF1": LbwfElementMapping( + "EXTROOF1": ElementMapping( element=Element.ROOF, aspect_type=AspectType.COVERING, element_instance=1, ), - "EXTROOF2": LbwfElementMapping( + "EXTROOF2": ElementMapping( element=Element.ROOF, aspect_type=AspectType.COVERING, element_instance=2, ), - "EXTROOF3": LbwfElementMapping( + "EXTROOF3": ElementMapping( element=Element.ROOF, aspect_type=AspectType.COVERING, element_instance=3, @@ -181,34 +172,34 @@ LBWF_ELEMENT_MAP: dict[str, LbwfElementMapping] = { # ========================================================== # EXTERNAL – DOORS & WINDOWS # ========================================================== - "INTFRDOOR": LbwfElementMapping( + "INTFRDOOR": ElementMapping( element=Element.EXTERNAL_DOOR, aspect_type=AspectType.TYPE, ), - "INTFRDRFRR": LbwfElementMapping( + "INTFRDRFRR": ElementMapping( element=Element.EXTERNAL_DOOR, aspect_type=AspectType.FIRE_RATING, ), - "EXTBKSDDR1": LbwfElementMapping( + "EXTBKSDDR1": ElementMapping( element=Element.EXTERNAL_DOOR, aspect_type=AspectType.TYPE, element_instance=1, ), - "EXTBKSDDR2": LbwfElementMapping( + "EXTBKSDDR2": ElementMapping( element=Element.EXTERNAL_DOOR, aspect_type=AspectType.TYPE, element_instance=2, ), - "INTWDWTYPE": LbwfElementMapping( + "INTWDWTYPE": ElementMapping( element=Element.EXTERNAL_WINDOWS, aspect_type=AspectType.TYPE, ), - "EXTWNDWS1": LbwfElementMapping( + "EXTWNDWS1": ElementMapping( element=Element.EXTERNAL_WINDOWS, aspect_type=AspectType.TYPE, element_instance=1, ), - "EXTWNDWS2": LbwfElementMapping( + "EXTWNDWS2": ElementMapping( element=Element.EXTERNAL_WINDOWS, aspect_type=AspectType.TYPE, element_instance=2, @@ -216,121 +207,121 @@ LBWF_ELEMENT_MAP: dict[str, LbwfElementMapping] = { # ========================================================== # HHSRS # ========================================================== - "HHSRSDAMP": LbwfElementMapping( + "HHSRSDAMP": ElementMapping( element=Element.HHSRS_DAMP_AND_MOULD, aspect_type=AspectType.RISK, ), - "HHSRSCOLD": LbwfElementMapping( + "HHSRSCOLD": ElementMapping( element=Element.HHSRS_EXCESS_COLD, aspect_type=AspectType.RISK, ), - "HHSRSHEAT": LbwfElementMapping( + "HHSRSHEAT": ElementMapping( element=Element.HHSRS_EXCESS_HEAT, aspect_type=AspectType.RISK, ), - "HHSRSASB": LbwfElementMapping( + "HHSRSASB": ElementMapping( element=Element.HHSRS_ASBESTOS_AND_MMF, aspect_type=AspectType.RISK, ), - "HHSRSBIOCIDES": LbwfElementMapping( + "HHSRSBIOCIDES": ElementMapping( element=Element.HHSRS_BIOCIDES, aspect_type=AspectType.RISK, ), - "HHSRSCO": LbwfElementMapping( + "HHSRSCO": ElementMapping( element=Element.HHSRS_CARBON_MONOXIDE, aspect_type=AspectType.RISK, ), - "HHSRSLEAD": LbwfElementMapping( + "HHSRSLEAD": ElementMapping( element=Element.HHSRS_LEAD, aspect_type=AspectType.RISK, ), - "HHSRSRADIA": LbwfElementMapping( + "HHSRSRADIA": ElementMapping( element=Element.HHSRS_RADIATION, aspect_type=AspectType.RISK, ), - "HHSRSFUEL": LbwfElementMapping( + "HHSRSFUEL": ElementMapping( element=Element.HHSRS_UNCOMBUSTED_FUEL_GAS, aspect_type=AspectType.RISK, ), - "HHSRSORGAN": LbwfElementMapping( + "HHSRSORGAN": ElementMapping( element=Element.HHSRS_VOLATILE_ORGANIC_COMPOUNDS, aspect_type=AspectType.RISK ), - "HHSRSCROWD": LbwfElementMapping( + "HHSRSCROWD": ElementMapping( element=Element.HHSRS_CROWDING_AND_SPACE, aspect_type=AspectType.RISK, ), - "HHSRSENTRY": LbwfElementMapping( + "HHSRSENTRY": ElementMapping( element=Element.HHSRS_ENTRY_BY_INTRUDERS, aspect_type=AspectType.RISK, ), - "HHSRSLIGHT": LbwfElementMapping( + "HHSRSLIGHT": ElementMapping( element=Element.HHSRS_LIGHTING, aspect_type=AspectType.RISK, ), - "HHSRSNOISE": LbwfElementMapping( + "HHSRSNOISE": ElementMapping( element=Element.HHSRS_NOISE, aspect_type=AspectType.RISK, ), - "HHSRSDOMES": LbwfElementMapping( + "HHSRSDOMES": ElementMapping( element=Element.HHSRS_DOMESTIC_HYGIENE_PESTS_REFUSE, aspect_type=AspectType.RISK, ), - "HHSRSFOOD": LbwfElementMapping( + "HHSRSFOOD": ElementMapping( element=Element.HHSRS_FOOD_SAFETY, aspect_type=AspectType.RISK, ), - "HHSRSPERS": LbwfElementMapping( + "HHSRSPERS": ElementMapping( element=Element.HHSRS_PERSONAL_HYGIENE_SANITATION, aspect_type=AspectType.RISK, ), - "HHSRSWATER": LbwfElementMapping( + "HHSRSWATER": ElementMapping( element=Element.HHSRS_WATER_SUPPLY, aspect_type=AspectType.RISK, ), - "HHSRSFBATH": LbwfElementMapping( + "HHSRSFBATH": ElementMapping( element=Element.HHSRS_FALLS_ASSOCIATED_WITH_BATHS, aspect_type=AspectType.RISK, ), - "HHSRSFLEVE": LbwfElementMapping( + "HHSRSFLEVE": ElementMapping( element=Element.HHSRS_FALLS_ON_LEVEL_SURFACES, aspect_type=AspectType.RISK, ), - "HHSRSFSTAI": LbwfElementMapping( + "HHSRSFSTAI": ElementMapping( element=Element.HHSRS_FALLS_ON_STAIRS, aspect_type=AspectType.RISK, ), - "HHSRSFBETW": LbwfElementMapping( + "HHSRSFBETW": ElementMapping( element=Element.HHSRS_FALLS_BETWEEN_LEVELS, aspect_type=AspectType.RISK, ), - "HHSRSELEC": LbwfElementMapping( + "HHSRSELEC": ElementMapping( element=Element.HHSRS_ELECTRICAL_HAZARDS, aspect_type=AspectType.RISK, ), - "HHSRSFIRE": LbwfElementMapping( + "HHSRSFIRE": ElementMapping( element=Element.HHSRS_FIRE, aspect_type=AspectType.RISK, ), - "HHSRSFLAME": LbwfElementMapping( + "HHSRSFLAME": ElementMapping( element=Element.HHSRS_FLAMES_HOT_SURFACES, aspect_type=AspectType.RISK, ), - "HHSRSENTRP": LbwfElementMapping( + "HHSRSENTRP": ElementMapping( element=Element.HHSRS_COLLISION_AND_ENTRAPMENT, aspect_type=AspectType.RISK, ), - "HHSRSEXPLO": LbwfElementMapping( + "HHSRSEXPLO": ElementMapping( element=Element.HHSRS_EXPLOSIONS, aspect_type=AspectType.RISK, ), - "HHSRSSTRUC": LbwfElementMapping( + "HHSRSSTRUC": ElementMapping( element=Element.HHSRS_STRUCTURAL_COLLAPSE, aspect_type=AspectType.RISK, ), - "HHSRSCLOW": LbwfElementMapping( + "HHSRSCLOW": ElementMapping( element=Element.HHSRS_COLLISION_AND_ENTRAPMENT, aspect_type=AspectType.RISK ), - "HHSRSPOSI": LbwfElementMapping( + "HHSRSPOSI": ElementMapping( element=Element.HHSRS_AMENITIES, aspect_type=AspectType.RISK, ), diff --git a/backend/condition/domain/mapping/lbwf/lbwf_mapper.py b/backend/condition/domain/mapping/lbwf/lbwf_mapper.py index 01f48a35..635e5898 100644 --- a/backend/condition/domain/mapping/lbwf/lbwf_mapper.py +++ b/backend/condition/domain/mapping/lbwf/lbwf_mapper.py @@ -2,10 +2,8 @@ from typing import Any, List, Optional from backend.condition.domain.asset_condition import AssetCondition from backend.condition.domain.element import Element -from backend.condition.domain.mapping.lbwf.lbwf_element_map import ( - LbwfElementMapping, - LBWF_ELEMENT_MAP, -) +from backend.condition.domain.mapping.element_mapping import ElementMapping +from backend.condition.domain.mapping.lbwf.lbwf_element_map import LBWF_ELEMENT_MAP from backend.condition.domain.mapping.mapper import Mapper from backend.condition.parsing.records.lbwf.lbwf_asset_condition import ( LbwfAssetCondition, @@ -19,7 +17,7 @@ logger = setup_logger() class LbwfMapper(Mapper): def map_asset_conditions_for_property( - self, client_data: Any, survey_year: Optional[int] + self, client_data: Any, survey_year: Optional[int] = None ) -> List[AssetCondition]: assert isinstance( client_data, LbwfHouse @@ -30,7 +28,7 @@ class LbwfMapper(Mapper): uprn: int = client_data.uprn for raw_asset in client_data.assets: try: - element_mapping: LbwfElementMapping = LbwfMapper._map_element( + element_mapping: ElementMapping = LbwfMapper._map_element( raw_asset.element_code ) except: @@ -59,7 +57,7 @@ class LbwfMapper(Mapper): return mapped_assets @staticmethod - def _map_element(lbwf_element_code: str) -> LbwfElementMapping: + def _map_element(lbwf_element_code: str) -> ElementMapping: return LBWF_ELEMENT_MAP[lbwf_element_code] @staticmethod diff --git a/backend/condition/domain/mapping/mapper.py b/backend/condition/domain/mapping/mapper.py index 4e51d46b..c0b07184 100644 --- a/backend/condition/domain/mapping/mapper.py +++ b/backend/condition/domain/mapping/mapper.py @@ -3,9 +3,12 @@ from typing import Any, List, Optional from backend.condition.domain.asset_condition import AssetCondition + class Mapper(ABC): @abstractmethod - def map_asset_conditions_for_property(self, client_data: Any, survey_year: Optional[int]) -> List[AssetCondition]: - #TODO: client_data should be properly typed - pass \ No newline at end of file + def map_asset_conditions_for_property( + self, client_data: Any, survey_year: Optional[int] = None + ) -> List[AssetCondition]: + # TODO: client_data should be properly typed + pass diff --git a/backend/condition/domain/mapping/peabody/peabody_element_map.py b/backend/condition/domain/mapping/peabody/peabody_element_map.py new file mode 100644 index 00000000..2a1203c0 --- /dev/null +++ b/backend/condition/domain/mapping/peabody/peabody_element_map.py @@ -0,0 +1,40 @@ +from backend.condition.domain.mapping.element_mapping import ElementMapping + + +PEABODY_ELEMENT_MAP = { + # -------------------- + # GENERAL + # -------------------- + ("100", "1"): ElementMapping(element="property", aspect="type"), + ("100", "3"): ElementMapping(element="property", aspect="age_band"), + ("100", "14"): ElementMapping(element="property", aspect="construction_type"), + # -------------------- + # EXTERNALS + # -------------------- + ("120", "1"): ElementMapping(element="external_wall", aspect="structure"), + ("120", "2"): ElementMapping(element="external_wall", aspect="finish"), + ("110", "1"): ElementMapping(element="roof", aspect="covering"), + # -------------------- + # INTERNALS + # -------------------- + ("160", "1"): ElementMapping(element="kitchen", aspect="condition"), + ("190", "1"): ElementMapping(element="bathroom", aspect="condition"), + # -------------------- + # HHSRS (PEABODY) + # -------------------- + ("54", "1"): ElementMapping( + element="hhsrs", aspect="risk", is_hhsrs=True, hhsrs_hazard_id=1 + ), + ("54", "2"): ElementMapping( + element="hhsrs", aspect="risk", is_hhsrs=True, hhsrs_hazard_id=2 + ), + ("54", "4"): ElementMapping( + element="hhsrs", aspect="risk", is_hhsrs=True, hhsrs_hazard_id=4 + ), + ("54", "24"): ElementMapping( + element="hhsrs", aspect="risk", is_hhsrs=True, hhsrs_hazard_id=24 + ), + ("54", "29"): ElementMapping( + element="hhsrs", aspect="risk", is_hhsrs=True, hhsrs_hazard_id=29 + ), +} diff --git a/backend/condition/domain/mapping/peabody/peabody_mapper.py b/backend/condition/domain/mapping/peabody/peabody_mapper.py index 8413b888..3f0ee931 100644 --- a/backend/condition/domain/mapping/peabody/peabody_mapper.py +++ b/backend/condition/domain/mapping/peabody/peabody_mapper.py @@ -13,6 +13,6 @@ logger = setup_logger() class PeabodyMapper(Mapper): def map_asset_conditions_for_property( - self, client_data: Any, survey_year: Optional[int] + self, client_data: Any, survey_year: Optional[int] = None ) -> List[AssetCondition]: raise NotImplementedError diff --git a/backend/condition/tests/mapping/test_peabody_mapper.py b/backend/condition/tests/mapping/test_peabody_mapper.py index de027fe7..ca3f78a4 100644 --- a/backend/condition/tests/mapping/test_peabody_mapper.py +++ b/backend/condition/tests/mapping/test_peabody_mapper.py @@ -1,5 +1,8 @@ from datetime import datetime +from typing import List +from backend.condition.domain.aspect_type import AspectType +from backend.condition.domain.element import Element from backend.condition.domain.mapping.peabody.peabody_mapper import PeabodyMapper from backend.condition.parsing.records.peabody.peabody_asset_condition import ( PeabodyAssetCondition, @@ -18,23 +21,42 @@ def test_peabody_mapper_maps_property(): full_address="FLAT 1 RANDOM SQUARE FAKE STREET LONDON E1 1EE", location_type_code=1, parent_lo_reference="RAND1000", - element_code=50, - element="Internal", - sub_element_code=3, - sub_element="CCU", - material_code=2, - material_or_answer="RCD/MCB CCU", - renewal_quantity=1, - renewal_year=2038, - renewal_cost=500, + element_code=130, + element="WINDOWS", + sub_element_code=1, + sub_element="Windows", + material_code=1, + material_or_answer="UPVC Double Glazed", + renewal_quantity=8, + renewal_year=2036, + renewal_cost=4800, cloned="N", lo_type_code=1, - condition_survey_date=datetime(2024, 2, 15, 0, 0, 0), + condition_survey_date=datetime(2024, 2, 15, 12, 47, 0), ) ], ) + mapper = PeabodyMapper() + expected_assets: List[AssetCondition] = [ + AssetCondition( + uprn=1, + element=Element.EXTERNAL_WINDOWS, + aspect_type=AspectType.MATERIAL, + value="UPVC Double Glazed", + quantity=8, + install_date=None, + renewal_year=2036, + element_instance=None, + source_system=None, + comments=None, + ) + ] # act + actual_assets = mapper.map_asset_conditions_for_property(peabody_property) # assert - assert False # temp + assert len(actual_assets) == len(expected_assets) + + for i, (actual, expected) in enumerate(zip(actual_assets, expected_assets)): + assert actual == expected, f"Mismatch at index {i}" From 42f9821f1b4a9f279e716cd9ed2725754d161b41 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 23 Jan 2026 12:50:48 +0000 Subject: [PATCH 27/68] =?UTF-8?q?Map=20to=20dataclasses=20from=20Peabody?= =?UTF-8?q?=20objects=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mapping/peabody/peabody_element_map.py | 70 +++++++++++-------- .../domain/mapping/peabody/peabody_mapper.py | 44 +++++++++++- 2 files changed, 85 insertions(+), 29 deletions(-) diff --git a/backend/condition/domain/mapping/peabody/peabody_element_map.py b/backend/condition/domain/mapping/peabody/peabody_element_map.py index 2a1203c0..5b89c578 100644 --- a/backend/condition/domain/mapping/peabody/peabody_element_map.py +++ b/backend/condition/domain/mapping/peabody/peabody_element_map.py @@ -1,40 +1,54 @@ +from backend.condition.domain.aspect_type import AspectType +from backend.condition.domain.element import Element from backend.condition.domain.mapping.element_mapping import ElementMapping PEABODY_ELEMENT_MAP = { - # -------------------- - # GENERAL - # -------------------- - ("100", "1"): ElementMapping(element="property", aspect="type"), - ("100", "3"): ElementMapping(element="property", aspect="age_band"), - ("100", "14"): ElementMapping(element="property", aspect="construction_type"), - # -------------------- - # EXTERNALS - # -------------------- - ("120", "1"): ElementMapping(element="external_wall", aspect="structure"), - ("120", "2"): ElementMapping(element="external_wall", aspect="finish"), - ("110", "1"): ElementMapping(element="roof", aspect="covering"), - # -------------------- - # INTERNALS - # -------------------- - ("160", "1"): ElementMapping(element="kitchen", aspect="condition"), - ("190", "1"): ElementMapping(element="bathroom", aspect="condition"), + # ========================================================== + # PROPERTY / GENERAL + # ========================================================== + (100, 1): ElementMapping(element=Element.PROPERTY, aspect_type=AspectType.TYPE), + # (100, 3): ElementMapping(element=Element.PROPERTY, aspect_type=AspectType.AGE), + # (100, 14): ElementMapping(element="property", aspect_type="construction_type"), + # ========================================================== + # EXTERNAL – WALLS + # ========================================================== + (120, 1): ElementMapping( + element=Element.EXTERNAL_WALL, aspect_type=AspectType.STRUCTURE + ), + (120, 2): ElementMapping( + element=Element.EXTERNAL_WALL, aspect_type=AspectType.FINISH + ), + # ========================================================== + # EXTERNAL – ROOFS + # ========================================================== + (110, 1): ElementMapping(element=Element.ROOF, aspect_type=AspectType.COVERING), + # ========================================================== + # EXTERNAL – DOORS & WINDOWS + # ========================================================== + (130, 1): ElementMapping( + element=Element.EXTERNAL_WINDOWS, aspect_type=AspectType.MATERIAL + ), + # ========================================================== + # INTERNAL – BATHROOMS & KITCHENS + # ========================================================== + (160, 1): ElementMapping(element=Element.KITCHEN, aspect_type=AspectType.CONDITION), + (190, 1): ElementMapping( + element=Element.BATHROOM, aspect_type=AspectType.CONDITION + ), # -------------------- # HHSRS (PEABODY) # -------------------- - ("54", "1"): ElementMapping( - element="hhsrs", aspect="risk", is_hhsrs=True, hhsrs_hazard_id=1 + (54, 1): ElementMapping( + element=Element.HHSRS_DAMP_AND_MOULD, aspect_type=AspectType.RISK ), - ("54", "2"): ElementMapping( - element="hhsrs", aspect="risk", is_hhsrs=True, hhsrs_hazard_id=2 + (54, 4): ElementMapping( + element=Element.HHSRS_ASBESTOS_AND_MMF, aspect_type=AspectType.RISK ), - ("54", "4"): ElementMapping( - element="hhsrs", aspect="risk", is_hhsrs=True, hhsrs_hazard_id=4 + (54, 15): ElementMapping( + element=Element.HHSRS_DOMESTIC_HYGIENE_PESTS_REFUSE, aspect_type=AspectType.RISK ), - ("54", "24"): ElementMapping( - element="hhsrs", aspect="risk", is_hhsrs=True, hhsrs_hazard_id=24 - ), - ("54", "29"): ElementMapping( - element="hhsrs", aspect="risk", is_hhsrs=True, hhsrs_hazard_id=29 + (54, 29): ElementMapping( + element=Element.HHSRS_STRUCTURAL_COLLAPSE, aspect_type=AspectType.RISK ), } diff --git a/backend/condition/domain/mapping/peabody/peabody_mapper.py b/backend/condition/domain/mapping/peabody/peabody_mapper.py index 3f0ee931..44dbd56e 100644 --- a/backend/condition/domain/mapping/peabody/peabody_mapper.py +++ b/backend/condition/domain/mapping/peabody/peabody_mapper.py @@ -1,6 +1,10 @@ from typing import Any, List, Optional from backend.condition.domain.asset_condition import AssetCondition +from backend.condition.domain.mapping.element_mapping import ElementMapping +from backend.condition.domain.mapping.peabody.peabody_element_map import ( + PEABODY_ELEMENT_MAP, +) from backend.condition.domain.mapping.mapper import Mapper from backend.condition.parsing.records.peabody.peabody_asset_condition import ( PeabodyAssetCondition, @@ -15,4 +19,42 @@ class PeabodyMapper(Mapper): def map_asset_conditions_for_property( self, client_data: Any, survey_year: Optional[int] = None ) -> List[AssetCondition]: - raise NotImplementedError + assert isinstance( + client_data, PeabodyProperty + ) # TODO: think of a better way to do this + + mapped_assets: List[AssetCondition] = [] + + uprn: int = client_data.uprn + for raw_asset in client_data.assets: + try: + element_mapping: ElementMapping = PeabodyMapper._map_element( + raw_asset.element_code, raw_asset.sub_element_code + ) + except: + logger.warning( + f"""Unrecognised Peabody Asset Element: {raw_asset.element} ({raw_asset.element_code}), + Sub-Element: {raw_asset.sub_element} ({raw_asset.sub_element_code}). Skipping record""" + ) + continue + + mapped_assets.append( + AssetCondition( + uprn=uprn, + element=element_mapping.element, + aspect_type=element_mapping.aspect_type, + value=raw_asset.material_or_answer, + quantity=raw_asset.renewal_quantity, + install_date=None, # Not available in peabody data + renewal_year=raw_asset.renewal_year, + element_instance=element_mapping.element_instance, + source_system=None, # Once we know the system name we'll set it here + comments=None, # Not available in peabody data + ) + ) + + return mapped_assets + + @staticmethod + def _map_element(element_code: int, sub_element_code: int) -> ElementMapping: + return PEABODY_ELEMENT_MAP[(element_code, sub_element_code)] From 4c16632b2f942eb426ccaeac768e8415cce629e9 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 23 Jan 2026 14:01:21 +0000 Subject: [PATCH 28/68] process both file types in local runner --- backend/condition/local_runner.py | 23 ++++++++++++++++------- backend/condition/parsing/factory.py | 4 ++++ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/backend/condition/local_runner.py b/backend/condition/local_runner.py index 28f9b06c..404f64d4 100644 --- a/backend/condition/local_runner.py +++ b/backend/condition/local_runner.py @@ -2,6 +2,7 @@ from pathlib import Path from backend.condition.processor import process_file + def main() -> None: try: # Works in scripts / debugger / pytest @@ -12,14 +13,22 @@ def main() -> None: path: Path = ROOT_DIR / "condition" / "sample_data" - lbwf_path: Path = path / "lbwf" / "LBWF - Example Asset Data September 2025.xlsx" # TODO: get this from s3 as part of devcontainer init + # TODO: get these from s3, maybe as part of devcontainer init + lbwf_path: Path = path / "lbwf" / "LBWF - Example Asset Data September 2025.xlsx" + peabody_path: Path = ( + path + / "peabody" + / "2026_01_06 - Peabody - Stock Condition Data - Survey Records - D Lower.xlsx" + ) + filepaths = [lbwf_path, peabody_path] + + for fp in filepaths: + with fp.open("rb") as f: + process_file( + file_stream=f, + source_key=fp.as_posix(), + ) - with lbwf_path.open("rb") as f: - process_file( - file_stream=f, - source_key=lbwf_path.as_posix(), - ) if __name__ == "__main__": main() - diff --git a/backend/condition/parsing/factory.py b/backend/condition/parsing/factory.py index 7233a1df..68ca0292 100644 --- a/backend/condition/parsing/factory.py +++ b/backend/condition/parsing/factory.py @@ -1,5 +1,6 @@ from backend.condition.domain.mapping.lbwf.lbwf_mapper import LbwfMapper from backend.condition.domain.mapping.mapper import Mapper +from backend.condition.domain.mapping.peabody.peabody_mapper import PeabodyMapper from backend.condition.file_type import FileType from backend.condition.parsing.parser import Parser from backend.condition.parsing.lbwf_parser import LbwfParser @@ -20,4 +21,7 @@ def select_mapper(file_type: FileType) -> Mapper: if file_type is FileType.LBWF: return LbwfMapper() + if file_type is FileType.Peabody: + return PeabodyMapper() + raise ValueError("Unrecognised file type, unable to instantiate Mapper") From 42cfcf604c5181ff64651acf93b6a3ae38cfcccc Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 23 Jan 2026 14:52:52 +0000 Subject: [PATCH 29/68] Extra test case, and note unhandled elements --- backend/condition/domain/aspect_type.py | 1 + .../mapping/peabody/peabody_element_map.py | 155 ++++++++++++++++++ .../domain/mapping/peabody/peabody_mapper.py | 3 - .../tests/mapping/test_peabody_mapper.py | 34 +++- 4 files changed, 188 insertions(+), 5 deletions(-) diff --git a/backend/condition/domain/aspect_type.py b/backend/condition/domain/aspect_type.py index 0f9a406a..d4db10bc 100644 --- a/backend/condition/domain/aspect_type.py +++ b/backend/condition/domain/aspect_type.py @@ -27,3 +27,4 @@ class AspectType(str, Enum): STRUCTURE = "structure" COVERING = "covering" FIRE_RATING = "fire_rating" + EXTERNAL_DECORATION = "external_decoration" diff --git a/backend/condition/domain/mapping/peabody/peabody_element_map.py b/backend/condition/domain/mapping/peabody/peabody_element_map.py index 5b89c578..80b3aa70 100644 --- a/backend/condition/domain/mapping/peabody/peabody_element_map.py +++ b/backend/condition/domain/mapping/peabody/peabody_element_map.py @@ -8,6 +8,9 @@ PEABODY_ELEMENT_MAP = { # PROPERTY / GENERAL # ========================================================== (100, 1): ElementMapping(element=Element.PROPERTY, aspect_type=AspectType.TYPE), + (100, 15): ElementMapping( + element=Element.PROPERTY, aspect_type=AspectType.EXTERNAL_DECORATION + ), # (100, 3): ElementMapping(element=Element.PROPERTY, aspect_type=AspectType.AGE), # (100, 14): ElementMapping(element="property", aspect_type="construction_type"), # ========================================================== @@ -52,3 +55,155 @@ PEABODY_ELEMENT_MAP = { element=Element.HHSRS_STRUCTURAL_COLLAPSE, aspect_type=AspectType.RISK ), } + + +# unhandled +# 'Element: ROOFS - Code: 110, Sub-Element: Chimney - Code: 3', +# 'Element: ROOFS - Code: 110, Sub-Element: Fascia - Code: 4', +# 'Element: ROOFS - Code: 110, Sub-Element: Rainwater Goods - Code: 6', +# 'Element: WINDOWS - Code: 130, Sub-Element: Communal Windows - Code: 2', +# 'Element: DOORS - Code: 140, Sub-Element: Main Doors - Code: 1', +# 'Element: DOORS - Code: 140, Sub-Element: Block Entrance Doors - Code: 4', +# 'Element: EXTERNAL AREAS - Code: 150, Sub-Element: Paving - Code: 1', +# 'Element: EXTERNAL AREAS - Code: 150, Sub-Element: Boundaries - Code: 4', +# 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Boiler - Code: 1', +# 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Heating - Code: 2', +# 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Electrics - Code: 3', +# 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Fire Detection - Code: 4', +# 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Floor Covering - Code: 6', +# 'Element: ROOFS - Code: 110, Sub-Element: Soffit - Code: 5', +# 'Element: External - Code: 53, Sub-Element: Window Type 01 - Code: 38', +# 'Element: ROOFS - Code: 110, Sub-Element: Secondary Roof - Code: 2', +# 'Element: HEATING - Code: 170, Sub-Element: Boiler - Code: 1', +# 'Element: HEATING - Code: 170, Sub-Element: Heating Distribution - Code: 2', +# 'Element: ELECTRICS - Code: 180, Sub-Element: Wiring - Code: 1', +# 'Element: ELECTRICS - Code: 180, Sub-Element: Consumer Unit - Code: 2', +# 'Element: ELECTRICS - Code: 180, Sub-Element: Smoke Detectors - Code: 3', +# 'Element: KITCHEN - Code: 160, Sub-Element: Kitchen space and layout - Code: 2', +# 'Element: HEATING - Code: 170, Sub-Element: Secondary Heating - Code: 3', +# 'Element: BATHROOM - Code: 190, Sub-Element: Secondary Toilet - Code: 2', +# 'Element: ELECTRICS - Code: 180, Sub-Element: Carbon Monoxide Alarms - Code: 4', +# 'Element: ROOFS - Code: 110, Sub-Element: Porch/Bay/Canopy - Code: 8', +# 'Element: HEATING - Code: 170, Sub-Element: Hot Water - Code: 5', +# 'Element: DOORS - Code: 140, Sub-Element: Garage Doors - Code: 3', +# 'Element: HEATING - Code: 170, Sub-Element: Cold Water - Code: 4', +# 'Element: DOORS - Code: 140, Sub-Element: Store Doors - Code: 2', +# 'Element: EXTERNAL AREAS - Code: 150, Sub-Element: Hardstanding - Code: 2', +# 'Element: WALLS - Code: 120, Sub-Element: Wall Insulation - Code: 3', +# 'Element: EXTERNAL AREAS - Code: 150, Sub-Element: Roads - Code: 3', +# 'Element: ROOFS - Code: 110, Sub-Element: Loft Insulation - Code: 7', +# 'Element: EXTERNAL AREAS - Code: 150, Sub-Element: Outbuilding - Code: 5', +# 'Element: Internal - Code: 50, Sub-Element: Additional WC - Code: 1', +# 'Element: Internal - Code: 50, Sub-Element: Carbon Monoxide Detector Type - Code: 2', +# 'Element: Internal - Code: 50, Sub-Element: CCU - Code: 3', +# 'Element: Internal - Code: 50, Sub-Element: Central Heating Boiler - Code: 4', +# 'Element: Internal - Code: 50, Sub-Element: Extractor Fan Bathroom - Code: 9', +# 'Element: Internal - Code: 50, Sub-Element: Extractor Fan Kitchen - Code: 10', +# 'Element: Internal - Code: 50, Sub-Element: Heat Detector Type - Code: 11', +# 'Element: Internal - Code: 50, Sub-Element: Kitchen Type - Code: 14', +# 'Element: Internal - Code: 50, Sub-Element: Primary Bathroom Type - Code: 18', +# 'Element: Internal - Code: 50, Sub-Element: Smoke Detector Type - Code: 21', +# 'Element: EXTERNAL AREAS - Code: 150, Sub-Element: Garage - Code: 6', +# 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Lifts - Code: 5', +# 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Kitchen - Code: 7', +# 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Bathroom - Code: 8', +# 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Toilets - Code: 9', +# 'Element: Internal - Code: 50, Sub-Element: Wiring - Code: 24', +# 'Element: External - Code: 53, Sub-Element: Front Door Material - Code: 8', +# 'Element: External - Code: 53, Sub-Element: Primary Wall Finish - Code: 23', +# 'Element: PASSENGER LIFTS - Code: 210, Sub-Element: Lift - Code: 2', +# 'Element: Internal - Code: 50, Sub-Element: Heating Distribution Type - Code: 12', +# 'Element: External - Code: 53, Sub-Element: Downpipes - Code: 3', +# 'Element: External - Code: 53, Sub-Element: Fascia/Soffits/Bargeboards - Code: 6', +# 'Element: External - Code: 53, Sub-Element: Gutters - Code: 15', +# 'Element: External - Code: 53, Sub-Element: Paths & Hardstandings - Code: 18', +# 'Element: External - Code: 53, Sub-Element: Pitched Roof Covering Material - Code: 21', +# 'Element: Internal - Code: 50, Sub-Element: Secondary Bathroom Type - Code: 20', +# 'Element: External - Code: 53, Sub-Element: Chimney - Code: 2', +# 'Element: External - Code: 53, Sub-Element: External Decoration - Code: 4', +# 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Gates - Code: 10', +# 'Element: GENERAL - Code: 100, Sub-Element: Property Age Band - Code: 3', +# 'Element: GENERAL - Code: 100, Sub-Element: Construction Type - Code: 14', +# 'Element: GENERAL - Code: 100, Sub-Element: Classification - Code: 16', +# 'Element: Communal - Code: 51, Sub-Element: Common Balcony/Walkway - Code: 3', +# 'Element: Communal - Code: 51, Sub-Element: Common Bin Store Doors - Code: 5', +# 'Element: Communal - Code: 51, Sub-Element: Common Bin Store Walls - Code: 7', +# 'Element: Communal - Code: 51, Sub-Element: Common Primary Entrance Material - Code: 28', +# 'Element: External - Code: 53, Sub-Element: Parking Areas - Code: 17', +# 'Element: External - Code: 53, Sub-Element: Front Fencing - Code: 9', +# 'Element: External - Code: 53, Sub-Element: Retaining Walls - Code: 28', +# 'Element: Communal - Code: 51, Sub-Element: Common Internal Decorations - Code: 20', +# 'Element: Communal - Code: 51, Sub-Element: Common Internal Floor Finish - Code: 22', +# 'Element: Communal - Code: 51, Sub-Element: Common Walkways Finish - Code: 36', +# 'Element: External - Code: 53, Sub-Element: Boundary Walls - Code: 1', +# 'Element: External - Code: 53, Sub-Element: Flat Roof Covering Material - Code: 7', +# 'Element: External - Code: 53, Sub-Element: Porch/Canopy - Code: 22', +# 'Element: External - Code: 53, Sub-Element: Private Balcony - Code: 24', +# 'Element: External - Code: 53, Sub-Element: Rear Gate - Code: 27', +# 'Element: Communal - Code: 51, Sub-Element: Common External Doors Other - Code: 17', +# 'Element: Communal - Code: 51, Sub-Element: Common Stair Finish - Code: 32', +# 'Element: External - Code: 53, Sub-Element: Front Gate - Code: 10', +# 'Element: External - Code: 53, Sub-Element: Rear Fencing - Code: 26', +# 'Element: External - Code: 53, Sub-Element: Side Fencing - Code: 31', +# 'Element: Communal - Code: 51, Sub-Element: Common Aerial - Code: 1', +# 'Element: Communal - Code: 51, Sub-Element: Common AOV - Code: 2', +# 'Element: Communal - Code: 51, Sub-Element: Common Door Entry System - Code: 14', +# 'Element: Communal - Code: 51, Sub-Element: Common Fire Alarm - Code: 19', +# 'Element: Communal - Code: 51, Sub-Element: Common Internal Doors - Code: 21', +# 'Element: External - Code: 53, Sub-Element: Store Door Material - Code: 35', +# 'Element: External - Code: 53, Sub-Element: Secondary Wall Finish - Code: 30', +# 'Element: Communal - Code: 51, Sub-Element: Common Emergency Lighting - Code: 16', +# 'Element: Communal - Code: 51, Sub-Element: Common Lateral Mains - Code: 24', +# 'Element: Communal - Code: 51, Sub-Element: Common Lighting - Code: 25', +# 'Element: Communal - Code: 51, Sub-Element: Common Store Roof - Code: 34', +# 'Element: Communal - Code: 51, Sub-Element: Common Store Walls - Code: 35', +# 'Element: External - Code: 53, Sub-Element: Cladding Material - Code: 41', +# 'Element: External - Code: 53, Sub-Element: Spandrel Panels - Code: 40', +# 'Element: Communal - Code: 51, Sub-Element: Common CCTV - Code: 11', +# 'Element: Communal - Code: 51, Sub-Element: Common Kitchen - Code: 23', +# 'Element: Communal - Code: 51, Sub-Element: Common Secondary Entrance Material - Code: 30', +# 'Element: Communal - Code: 51, Sub-Element: Common Warden Call System - Code: 37', +# 'Element: External - Code: 53, Sub-Element: Lintels - Code: 16', +# 'Element: Communal - Code: 51, Sub-Element: Common Boiler - Code: 9', +# 'Element: External - Code: 53, Sub-Element: Soil & Vent Material - Code: 32', +# 'Element: Communal - Code: 51, Sub-Element: Common Passenger Lift - Code: 27', +# 'Element: Communal - Code: 51, Sub-Element: Common Store Doors - Code: 33', +# 'Element: External - Code: 53, Sub-Element: Window Type 02 - Code: 39', +# 'Element: Communal - Code: 51, Sub-Element: Common BMS - Code: 8', +# 'Element: Communal - Code: 51, Sub-Element: Common Booster Pump - Code: 10', +# 'Element: Communal - Code: 51, Sub-Element: Common Dry Riser - Code: 15', +# 'Element: Communal - Code: 51, Sub-Element: Common Lightning Conductor - Code: 26', +# 'Element: Communal - Code: 51, Sub-Element: Common Bin Store Roof - Code: 6', +# 'Element: Communal - Code: 51, Sub-Element: Common Bathroom - Code: 4', +# 'Element: Communal - Code: 51, Sub-Element: Common WC - Code: 38', +# 'Element: External - Code: 53, Sub-Element: Wall Insulation - Code: 36', +# 'Element: External - Code: 53, Sub-Element: Garage Door - Code: 12', +# 'Element: Communal - Code: 51, Sub-Element: Common Cold Water Storage Tank - Code: 13', +# 'Element: Communal - Code: 51, Sub-Element: Common Sprinker - Code: 31', +# 'Element: External - Code: 53, Sub-Element: Garage Walls - Code: 14', +# 'Element: Communal - Code: 51, Sub-Element: Communal Plug Sockets - Code: 40', +# 'Element: Communal - Code: 51, Sub-Element: Common Wet Riser - Code: 39', +# 'Element: Communal - Code: 51, Sub-Element: Common Refuse Chute - Code: 29', +# 'Element: External - Code: 53, Sub-Element: Secondary Glazing - Code: 29', +# 'Element: External - Code: 53, Sub-Element: Solar Thermals - Code: 34', +# 'Element: External - Code: 53, Sub-Element: Garage Roof - Code: 13', +# 'Element: External - Code: 53, Sub-Element: Patio/French Door - Code: 19', +# 'Element: External - Code: 53, Sub-Element: Rear Door Material - Code: 25', +# 'Element: Internal - Code: 50, Sub-Element: Party Wall Fire Break - Code: 16', +# 'Element: Internal - Code: 50, Sub-Element: Boiler Type - Code: 25', +# 'Element: External - Code: 53, Sub-Element: Roof Structure - Code: 47', +# 'Element: External - Code: 53, Sub-Element: Front Door Type - Code: 43', +# 'Element: Communal - Code: 51, Sub-Element: Common Cirulation Space - Code: 12', +# 'Element: External - Code: 53, Sub-Element: External Noise Insulation - Code: 5', +# 'Element: Internal - Code: 50, Sub-Element: Door Entry Handset - Code: 8', +# 'Element: Internal - Code: 50, Sub-Element: Cold Water Storage Tank - Code: 6', +# 'Element: Internal - Code: 50, Sub-Element: Programmable Heating - Code: 19', +# 'Element: Internal - Code: 50, Sub-Element: Central Heating Extent - Code: 5', +# 'Element: Internal - Code: 50, Sub-Element: Kitchen Space & Layout - Code: 13', +# 'Element: Internal - Code: 50, Sub-Element: Loft Insulation - Code: 15', +# 'Element: Internal - Code: 50, Sub-Element: Stairlift - Code: 22', +# 'Element: Internal - Code: 50, Sub-Element: Primary Bathroom Location - Code: 17', +# 'Element: Internal - Code: 50, Sub-Element: Disabled Hoist Tracking - Code: 7', +# 'Element: External - Code: 53, Sub-Element: Garage Type - Code: 44', +# 'Element: External - Code: 53, Sub-Element: Private Balcony Balustrade Material - Code: 45', +# 'Element: Internal - Code: 50, Sub-Element: Disabled Facilities - Code: 26' diff --git a/backend/condition/domain/mapping/peabody/peabody_mapper.py b/backend/condition/domain/mapping/peabody/peabody_mapper.py index 44dbd56e..dea07756 100644 --- a/backend/condition/domain/mapping/peabody/peabody_mapper.py +++ b/backend/condition/domain/mapping/peabody/peabody_mapper.py @@ -6,9 +6,6 @@ from backend.condition.domain.mapping.peabody.peabody_element_map import ( PEABODY_ELEMENT_MAP, ) from backend.condition.domain.mapping.mapper import Mapper -from backend.condition.parsing.records.peabody.peabody_asset_condition import ( - PeabodyAssetCondition, -) from backend.condition.parsing.records.peabody.peabody_property import PeabodyProperty from utils.logger import setup_logger diff --git a/backend/condition/tests/mapping/test_peabody_mapper.py b/backend/condition/tests/mapping/test_peabody_mapper.py index ca3f78a4..7fad77f7 100644 --- a/backend/condition/tests/mapping/test_peabody_mapper.py +++ b/backend/condition/tests/mapping/test_peabody_mapper.py @@ -33,7 +33,25 @@ def test_peabody_mapper_maps_property(): cloned="N", lo_type_code=1, condition_survey_date=datetime(2024, 2, 15, 12, 47, 0), - ) + ), + PeabodyAssetCondition( + lo_reference="1000RAND0000", + full_address="FLAT 1 RANDOM SQUARE FAKE STREET LONDON E1 1EE", + location_type_code=1, + parent_lo_reference="RAND1000", + element_code=100, + element="GENERAL", + sub_element_code=15, + sub_element="External Decoration", + material_code=2, + material_or_answer="Normal", + renewal_quantity=1, + renewal_year=2029, + renewal_cost=1500, + cloned="N", + lo_type_code=1, + condition_survey_date=datetime(2024, 2, 15, 12, 47, 0), + ), ], ) mapper = PeabodyMapper() @@ -50,7 +68,19 @@ def test_peabody_mapper_maps_property(): element_instance=None, source_system=None, comments=None, - ) + ), + AssetCondition( + uprn=1, + element=Element.PROPERTY, + aspect_type=AspectType.EXTERNAL_DECORATION, + value="Normal", + quantity=1, + install_date=None, + renewal_year=2029, + element_instance=None, + source_system=None, + comments=None, + ), ] # act actual_assets = mapper.map_asset_conditions_for_property(peabody_property) From 7741373671093bc0c4bdaa2fd01aabc70dc9950d Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 23 Jan 2026 15:54:53 +0000 Subject: [PATCH 30/68] Map more peabody elements --- backend/condition/domain/aspect_type.py | 1 + backend/condition/domain/element.py | 8 ++ .../mapping/peabody/peabody_element_map.py | 104 +++++++++++++----- 3 files changed, 86 insertions(+), 27 deletions(-) diff --git a/backend/condition/domain/aspect_type.py b/backend/condition/domain/aspect_type.py index d4db10bc..94522b03 100644 --- a/backend/condition/domain/aspect_type.py +++ b/backend/condition/domain/aspect_type.py @@ -28,3 +28,4 @@ class AspectType(str, Enum): COVERING = "covering" FIRE_RATING = "fire_rating" EXTERNAL_DECORATION = "external_decoration" + WORK_REQUIRED = "work_required" diff --git a/backend/condition/domain/element.py b/backend/condition/domain/element.py index c8fb6167..1d109002 100644 --- a/backend/condition/domain/element.py +++ b/backend/condition/domain/element.py @@ -23,6 +23,9 @@ class Element(str, Enum): RAINWATER_GOODS = "rainwater_goods" LOFT_INSULATION = "loft_insulation" PORCH_CANOPY = "porch_canopy" + CHIMNEY = "chimney" + FASCIA = "fascia" + SOFFIT = "soffit" # ====================== # EXTERNAL – WALLS @@ -45,6 +48,8 @@ class Element(str, Enum): STORE_DOOR = "store_door" GARAGE_DOOR = "garage_door" COMMUNAL_ENTRANCE_DOOR = "communal_entrance_door" + MAIN_DOOR = "main_door" + BLOCK_ENTRANCE_DOOR = "block_entrance_door" # ====================== # EXTERNAL – AREAS @@ -59,6 +64,8 @@ class Element(str, Enum): BALCONY_BALUSTRADE = "balcony_balustrade" OUTBUILDINGS = "outbuildings" GARAGE_STRUCTURE = "garage_structure" + PAVING = "paving" + ROADS = "roads" # ====================== # INTERNAL – KITCHEN @@ -110,6 +117,7 @@ class Element(str, Enum): COMMUNAL_CCTV = "communal_cctv" COMMUNAL_BIN_STORE = "communal_bin_store" COMMUNAL_REFUSE_CHUTE = "communal_refuse_chute" + COMMUNAL_FLOOR_COVERING = "communal_floor_covering" # ========================================================== # HHSRS – ALL 29 HAZARDS diff --git a/backend/condition/domain/mapping/peabody/peabody_element_map.py b/backend/condition/domain/mapping/peabody/peabody_element_map.py index 80b3aa70..12c4bf66 100644 --- a/backend/condition/domain/mapping/peabody/peabody_element_map.py +++ b/backend/condition/domain/mapping/peabody/peabody_element_map.py @@ -25,13 +25,65 @@ PEABODY_ELEMENT_MAP = { # ========================================================== # EXTERNAL – ROOFS # ========================================================== - (110, 1): ElementMapping(element=Element.ROOF, aspect_type=AspectType.COVERING), + (110, 1): ElementMapping( + element=Element.ROOF, aspect_type=AspectType.MATERIAL, element_instance=1 + ), + (110, 2): ElementMapping( + element=Element.ROOF, aspect_type=AspectType.MATERIAL, element_instance=1 + ), + (110, 3): ElementMapping( + element=Element.CHIMNEY, aspect_type=AspectType.WORK_REQUIRED + ), + (110, 4): ElementMapping(element=Element.FASCIA, aspect_type=AspectType.MATERIAL), + (110, 5): ElementMapping(element=Element.SOFFIT, aspect_type=AspectType.MATERIAL), + (110, 6): ElementMapping( + element=Element.RAINWATER_GOODS, aspect_type=AspectType.MATERIAL + ), + (110, 7): ElementMapping( + element=Element.LOFT_INSULATION, + aspect_type=AspectType.WORK_REQUIRED, # possibly not the right aspect type + ), + (110, 8): ElementMapping( + element=Element.PORCH_CANOPY, aspect_type=AspectType.MATERIAL + ), # ========================================================== # EXTERNAL – DOORS & WINDOWS # ========================================================== (130, 1): ElementMapping( element=Element.EXTERNAL_WINDOWS, aspect_type=AspectType.MATERIAL ), + (130, 2): ElementMapping( + element=Element.COMMUNAL_WINDOWS, aspect_type=AspectType.MATERIAL + ), + (140, 1): ElementMapping( + element=Element.MAIN_DOOR, aspect_type=AspectType.MATERIAL + ), + (140, 2): ElementMapping( + element=Element.STORE_DOOR, aspect_type=AspectType.MATERIAL + ), + (140, 3): ElementMapping( + element=Element.GARAGE_DOOR, aspect_type=AspectType.MATERIAL + ), + (140, 4): ElementMapping( + element=Element.BLOCK_ENTRANCE_DOOR, aspect_type=AspectType.MATERIAL + ), + # ========================================================== + # EXTERNAL AREAS + # ========================================================== + (150, 1): ElementMapping( + element=Element.BLOCK_ENTRANCE_DOOR, aspect_type=AspectType.MATERIAL + ), + (150, 2): ElementMapping( + element=Element.PATHS_AND_HARDSTANDINGS, aspect_type=AspectType.MATERIAL + ), + (150, 3): ElementMapping(element=Element.ROADS, aspect_type=AspectType.MATERIAL), + (150, 4): ElementMapping( + element=Element.BOUNDARY_WALLS, aspect_type=AspectType.MATERIAL + ), + (150, 5): ElementMapping(element=Element.OUTBUILDINGS, aspect_type=AspectType.TYPE), + (150, 6): ElementMapping( + element=Element.GARAGE_STRUCTURE, aspect_type=AspectType.TYPE + ), # ========================================================== # INTERNAL – BATHROOMS & KITCHENS # ========================================================== @@ -39,9 +91,30 @@ PEABODY_ELEMENT_MAP = { (190, 1): ElementMapping( element=Element.BATHROOM, aspect_type=AspectType.CONDITION ), - # -------------------- - # HHSRS (PEABODY) - # -------------------- + # ========================================================== + # COMMUNAL SYSTEMS + # ========================================================== + (200, 1): ElementMapping( + element=Element.COMMUNAL_BOILER, aspect_type=AspectType.TYPE + ), + (200, 2): ElementMapping( + element=Element.COMMUNAL_HEATING, aspect_type=AspectType.TYPE + ), + (200, 3): ElementMapping( + element=Element.COMMUNAL_ELECTRICS, aspect_type=AspectType.TYPE + ), + (200, 4): ElementMapping( + element=Element.COMMUNAL_FIRE_ALARM, aspect_type=AspectType.TYPE + ), + (200, 5): ElementMapping( + element=Element.COMMUNAL_LIFT, aspect_type=AspectType.TYPE + ), + (200, 6): ElementMapping( + element=Element.COMMUNAL_FLOOR_COVERING, aspect_type=AspectType.MATERIAL + ), + # ========================================================== + # HHSRS + # ========================================================== (54, 1): ElementMapping( element=Element.HHSRS_DAMP_AND_MOULD, aspect_type=AspectType.RISK ), @@ -58,22 +131,7 @@ PEABODY_ELEMENT_MAP = { # unhandled -# 'Element: ROOFS - Code: 110, Sub-Element: Chimney - Code: 3', -# 'Element: ROOFS - Code: 110, Sub-Element: Fascia - Code: 4', -# 'Element: ROOFS - Code: 110, Sub-Element: Rainwater Goods - Code: 6', -# 'Element: WINDOWS - Code: 130, Sub-Element: Communal Windows - Code: 2', -# 'Element: DOORS - Code: 140, Sub-Element: Main Doors - Code: 1', -# 'Element: DOORS - Code: 140, Sub-Element: Block Entrance Doors - Code: 4', -# 'Element: EXTERNAL AREAS - Code: 150, Sub-Element: Paving - Code: 1', -# 'Element: EXTERNAL AREAS - Code: 150, Sub-Element: Boundaries - Code: 4', -# 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Boiler - Code: 1', -# 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Heating - Code: 2', -# 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Electrics - Code: 3', -# 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Fire Detection - Code: 4', -# 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Floor Covering - Code: 6', -# 'Element: ROOFS - Code: 110, Sub-Element: Soffit - Code: 5', # 'Element: External - Code: 53, Sub-Element: Window Type 01 - Code: 38', -# 'Element: ROOFS - Code: 110, Sub-Element: Secondary Roof - Code: 2', # 'Element: HEATING - Code: 170, Sub-Element: Boiler - Code: 1', # 'Element: HEATING - Code: 170, Sub-Element: Heating Distribution - Code: 2', # 'Element: ELECTRICS - Code: 180, Sub-Element: Wiring - Code: 1', @@ -83,16 +141,9 @@ PEABODY_ELEMENT_MAP = { # 'Element: HEATING - Code: 170, Sub-Element: Secondary Heating - Code: 3', # 'Element: BATHROOM - Code: 190, Sub-Element: Secondary Toilet - Code: 2', # 'Element: ELECTRICS - Code: 180, Sub-Element: Carbon Monoxide Alarms - Code: 4', -# 'Element: ROOFS - Code: 110, Sub-Element: Porch/Bay/Canopy - Code: 8', # 'Element: HEATING - Code: 170, Sub-Element: Hot Water - Code: 5', -# 'Element: DOORS - Code: 140, Sub-Element: Garage Doors - Code: 3', # 'Element: HEATING - Code: 170, Sub-Element: Cold Water - Code: 4', -# 'Element: DOORS - Code: 140, Sub-Element: Store Doors - Code: 2', -# 'Element: EXTERNAL AREAS - Code: 150, Sub-Element: Hardstanding - Code: 2', # 'Element: WALLS - Code: 120, Sub-Element: Wall Insulation - Code: 3', -# 'Element: EXTERNAL AREAS - Code: 150, Sub-Element: Roads - Code: 3', -# 'Element: ROOFS - Code: 110, Sub-Element: Loft Insulation - Code: 7', -# 'Element: EXTERNAL AREAS - Code: 150, Sub-Element: Outbuilding - Code: 5', # 'Element: Internal - Code: 50, Sub-Element: Additional WC - Code: 1', # 'Element: Internal - Code: 50, Sub-Element: Carbon Monoxide Detector Type - Code: 2', # 'Element: Internal - Code: 50, Sub-Element: CCU - Code: 3', @@ -103,7 +154,6 @@ PEABODY_ELEMENT_MAP = { # 'Element: Internal - Code: 50, Sub-Element: Kitchen Type - Code: 14', # 'Element: Internal - Code: 50, Sub-Element: Primary Bathroom Type - Code: 18', # 'Element: Internal - Code: 50, Sub-Element: Smoke Detector Type - Code: 21', -# 'Element: EXTERNAL AREAS - Code: 150, Sub-Element: Garage - Code: 6', # 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Lifts - Code: 5', # 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Kitchen - Code: 7', # 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Bathroom - Code: 8', From 8cdbe107e5f9c306efc1b6d0e12cf05e99057d0c Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 23 Jan 2026 16:21:33 +0000 Subject: [PATCH 31/68] add TODO to is_block_level --- .../records/peabody/peabody_asset_condition.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/backend/condition/parsing/records/peabody/peabody_asset_condition.py b/backend/condition/parsing/records/peabody/peabody_asset_condition.py index 01215a26..a74dc359 100644 --- a/backend/condition/parsing/records/peabody/peabody_asset_condition.py +++ b/backend/condition/parsing/records/peabody/peabody_asset_condition.py @@ -4,6 +4,7 @@ from dataclasses import dataclass from datetime import datetime from typing import Optional + @dataclass class PeabodyAssetCondition: lo_reference: str @@ -25,17 +26,19 @@ class PeabodyAssetCondition: @property def is_block_level(self) -> bool: + # TODO: maybe use block codes from other Peabody dataset to do this instead + if not self.full_address: return False address = self.full_address.upper() block_level_patterns = [ - r"\bBLOCK\b", # BLOCK MILNE HOUSE - r"\bFLATS\b", # FLATS A-D - r"\b\d+[A-Z]?-\d+[A-Z]?\b", # 1-80, 9A-9H - r"\b\d+[A-Z]-[A-Z]\b", # 81A-B - r"\b\d+\s*&\s*\d+\b", # 73 & 74 + r"\bBLOCK\b", # BLOCK MILNE HOUSE + r"\bFLATS\b", # FLATS A-D + r"\b\d+[A-Z]?-\d+[A-Z]?\b", # 1-80, 9A-9H + r"\b\d+[A-Z]-[A-Z]\b", # 81A-B + r"\b\d+\s*&\s*\d+\b", # 73 & 74 ] - return any(re.search(pattern, address) for pattern in block_level_patterns) \ No newline at end of file + return any(re.search(pattern, address) for pattern in block_level_patterns) From 2d77511650219f4a6f931c318a88107156a587ca Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Fri, 23 Jan 2026 17:11:11 +0000 Subject: [PATCH 32/68] Map remaining Peabody EXTERNAL elements --- backend/condition/domain/element.py | 23 ++- .../mapping/peabody/peabody_element_map.py | 160 +++++++++++++----- .../tests/mapping/test_peabody_mapper.py | 4 +- 3 files changed, 137 insertions(+), 50 deletions(-) diff --git a/backend/condition/domain/element.py b/backend/condition/domain/element.py index 1d109002..fed5ab3b 100644 --- a/backend/condition/domain/element.py +++ b/backend/condition/domain/element.py @@ -20,17 +20,30 @@ class Element(str, Enum): # EXTERNAL – ROOF # ====================== ROOF = "roof" + PITCHED_ROOF_COVERING = "pitched_roof_covering" + FLAT_ROOF_COVERING = "flat_roof_covering" RAINWATER_GOODS = "rainwater_goods" LOFT_INSULATION = "loft_insulation" PORCH_CANOPY = "porch_canopy" CHIMNEY = "chimney" FASCIA = "fascia" SOFFIT = "soffit" + FASCIA_SOFFIT_BARGEBOARDS = "fascia_soffit_bargeboards" + GUTTERS = "gutters" + GARAGE_ROOF = "garage_roof" # ====================== # EXTERNAL – WALLS # ====================== EXTERNAL_WALL = "external_wall" + EXTERNAL_NOISE_INSULATION = "external_noise_insulation" + PRIMARY_WALL = "primary_wall" + SECONDARY_WALL = "secondary_wall" + DOWNPIPES = "downpipes" + EXTERNAL_DECORATION = "external_decoration" + CLADDING = "cladding" + SPANDREL_PANELS = "spandrel_panels" + GARAGE_WALLS = "garage_walls" # ====================== # EXTERNAL – WINDOWS @@ -50,6 +63,8 @@ class Element(str, Enum): COMMUNAL_ENTRANCE_DOOR = "communal_entrance_door" MAIN_DOOR = "main_door" BLOCK_ENTRANCE_DOOR = "block_entrance_door" + LINTEL = "lintel" + PATIO_FRENCH_DOOR = "patio_french_door" # ====================== # EXTERNAL – AREAS @@ -57,7 +72,11 @@ class Element(str, Enum): PATHS_AND_HARDSTANDINGS = "paths_and_hardstandings" PARKING_AREAS = "parking_areas" BOUNDARY_WALLS = "boundary_walls" - FENCING = "fencing" + FRONT_FENCING = "front_fencing" + REAR_FENCING = "rear_fencing" + SIDE_FENCING = "side_fencing" + REAR_GATE = "rear_gate" + FRONT_GATE = "front_gate" GATES = "gates" RETAINING_WALLS = "retaining_walls" PRIVATE_BALCONY = "private_balcony" @@ -66,6 +85,8 @@ class Element(str, Enum): GARAGE_STRUCTURE = "garage_structure" PAVING = "paving" ROADS = "roads" + SOIL_AND_VENT = "soil_and_vent" + SOLAR_THERMALS = "solar_thermals" # ====================== # INTERNAL – KITCHEN diff --git a/backend/condition/domain/mapping/peabody/peabody_element_map.py b/backend/condition/domain/mapping/peabody/peabody_element_map.py index 12c4bf66..81aa8b9e 100644 --- a/backend/condition/domain/mapping/peabody/peabody_element_map.py +++ b/backend/condition/domain/mapping/peabody/peabody_element_map.py @@ -8,14 +8,39 @@ PEABODY_ELEMENT_MAP = { # PROPERTY / GENERAL # ========================================================== (100, 1): ElementMapping(element=Element.PROPERTY, aspect_type=AspectType.TYPE), - (100, 15): ElementMapping( - element=Element.PROPERTY, aspect_type=AspectType.EXTERNAL_DECORATION - ), # (100, 3): ElementMapping(element=Element.PROPERTY, aspect_type=AspectType.AGE), # (100, 14): ElementMapping(element="property", aspect_type="construction_type"), # ========================================================== # EXTERNAL – WALLS # ========================================================== + (53, 1): ElementMapping( + element=Element.BOUNDARY_WALLS, aspect_type=AspectType.PRESENCE + ), + (53, 4): ElementMapping( + element=Element.EXTERNAL_DECORATION, aspect_type=AspectType.PRESENCE + ), + (53, 4): ElementMapping( + element=Element.EXTERNAL_NOISE_INSULATION, aspect_type=AspectType.ADEQUACY + ), + (53, 14): ElementMapping( + element=Element.GARAGE_WALLS, aspect_type=AspectType.MATERIAL + ), + (53, 23): ElementMapping( + element=Element.PRIMARY_WALL, aspect_type=AspectType.FINISH + ), + (53, 30): ElementMapping( + element=Element.SECONDARY_WALL, aspect_type=AspectType.FINISH + ), # Should this be combined with primary wall, with different instance value? + (53, 36): ElementMapping( + element=Element.EXTERNAL_WALL, aspect_type=AspectType.INSULATION + ), + (53, 40): ElementMapping( + element=Element.SPANDREL_PANELS, aspect_type=AspectType.MATERIAL + ), + (53, 41): ElementMapping(element=Element.CLADDING, aspect_type=AspectType.MATERIAL), + (100, 15): ElementMapping( + element=Element.EXTERNAL_DECORATION, aspect_type=AspectType.CONDITION + ), (120, 1): ElementMapping( element=Element.EXTERNAL_WALL, aspect_type=AspectType.STRUCTURE ), @@ -25,6 +50,22 @@ PEABODY_ELEMENT_MAP = { # ========================================================== # EXTERNAL – ROOFS # ========================================================== + (53, 2): ElementMapping(element=Element.CHIMNEY, aspect_type=AspectType.PRESENCE), + (53, 6): ElementMapping( + element=Element.FASCIA_SOFFIT_BARGEBOARDS, aspect_type=AspectType.MATERIAL + ), + (53, 7): ElementMapping( + element=Element.FLAT_ROOF_COVERING, aspect_type=AspectType.MATERIAL + ), + (53, 13): ElementMapping( + element=Element.GARAGE_ROOF, aspect_type=AspectType.MATERIAL + ), + (53, 15): ElementMapping(element=Element.GUTTERS, aspect_type=AspectType.MATERIAL), + (53, 18): ElementMapping( + element=Element.PITCHED_ROOF_COVERING, aspect_type=AspectType.MATERIAL + ), + (53, 22): ElementMapping(element=Element.PORCH_CANOPY, aspect_type=AspectType.TYPE), + (53, 47): ElementMapping(element=Element.ROOF, aspect_type=AspectType.STRUCTURE), (110, 1): ElementMapping( element=Element.ROOF, aspect_type=AspectType.MATERIAL, element_instance=1 ), @@ -49,6 +90,36 @@ PEABODY_ELEMENT_MAP = { # ========================================================== # EXTERNAL – DOORS & WINDOWS # ========================================================== + (53, 8): ElementMapping( + element=Element.FRONT_DOOR, aspect_type=AspectType.MATERIAL + ), + (53, 12): ElementMapping( + element=Element.GARAGE_DOOR, aspect_type=AspectType.MATERIAL + ), + (53, 16): ElementMapping(element=Element.LINTEL, aspect_type=AspectType.PRESENCE), + (53, 19): ElementMapping( + element=Element.PATIO_FRENCH_DOOR, aspect_type=AspectType.MATERIAL + ), + (53, 25): ElementMapping( + element=Element.REAR_DOOR, aspect_type=AspectType.MATERIAL + ), + (53, 29): ElementMapping( + element=Element.SECONDARY_GLAZING, aspect_type=AspectType.PRESENCE + ), + (53, 35): ElementMapping( + element=Element.STORE_DOOR, aspect_type=AspectType.MATERIAL + ), + (53, 38): ElementMapping( + element=Element.EXTERNAL_WINDOWS, + aspect_type=AspectType.TYPE, + element_instance=1, + ), + (53, 39): ElementMapping( + element=Element.EXTERNAL_WINDOWS, + aspect_type=AspectType.TYPE, + element_instance=2, + ), + (53, 43): ElementMapping(element=Element.FRONT_DOOR, aspect_type=AspectType.TYPE), (130, 1): ElementMapping( element=Element.EXTERNAL_WINDOWS, aspect_type=AspectType.MATERIAL ), @@ -60,22 +131,58 @@ PEABODY_ELEMENT_MAP = { ), (140, 2): ElementMapping( element=Element.STORE_DOOR, aspect_type=AspectType.MATERIAL - ), + ), # Duplicate of (53, 35) (140, 3): ElementMapping( element=Element.GARAGE_DOOR, aspect_type=AspectType.MATERIAL - ), + ), # Duplicate of (53, 12) (140, 4): ElementMapping( element=Element.BLOCK_ENTRANCE_DOOR, aspect_type=AspectType.MATERIAL ), # ========================================================== # EXTERNAL AREAS # ========================================================== + (53, 3): ElementMapping(element=Element.DOWNPIPES, aspect_type=AspectType.MATERIAL), + (53, 9): ElementMapping( + element=Element.FRONT_FENCING, aspect_type=AspectType.MATERIAL + ), + (53, 10): ElementMapping(element=Element.FRONT_GATE, aspect_type=AspectType.TYPE), + (53, 17): ElementMapping( + element=Element.PARKING_AREAS, aspect_type=AspectType.MATERIAL + ), + (53, 18): ElementMapping( + element=Element.PATHS_AND_HARDSTANDINGS, aspect_type=AspectType.MATERIAL + ), + (53, 24): ElementMapping( + element=Element.PRIVATE_BALCONY, aspect_type=AspectType.PRESENCE + ), + (53, 26): ElementMapping( + element=Element.REAR_FENCING, aspect_type=AspectType.MATERIAL + ), + (53, 27): ElementMapping(element=Element.REAR_GATE, aspect_type=AspectType.TYPE), + (53, 28): ElementMapping( + element=Element.RETAINING_WALLS, aspect_type=AspectType.PRESENCE + ), + (53, 31): ElementMapping( + element=Element.SIDE_FENCING, aspect_type=AspectType.MATERIAL + ), + (53, 32): ElementMapping( + element=Element.SOIL_AND_VENT, aspect_type=AspectType.MATERIAL + ), + (53, 34): ElementMapping( + element=Element.SOLAR_THERMALS, aspect_type=AspectType.PRESENCE + ), + (53, 44): ElementMapping( + element=Element.GARAGE_STRUCTURE, aspect_type=AspectType.TYPE + ), + (53, 45): ElementMapping( + element=Element.BALCONY_BALUSTRADE, aspect_type=AspectType.MATERIAL + ), (150, 1): ElementMapping( element=Element.BLOCK_ENTRANCE_DOOR, aspect_type=AspectType.MATERIAL ), (150, 2): ElementMapping( element=Element.PATHS_AND_HARDSTANDINGS, aspect_type=AspectType.MATERIAL - ), + ), # Duplicate of (53, 18) - correct? (150, 3): ElementMapping(element=Element.ROADS, aspect_type=AspectType.MATERIAL), (150, 4): ElementMapping( element=Element.BOUNDARY_WALLS, aspect_type=AspectType.MATERIAL @@ -131,7 +238,6 @@ PEABODY_ELEMENT_MAP = { # unhandled -# 'Element: External - Code: 53, Sub-Element: Window Type 01 - Code: 38', # 'Element: HEATING - Code: 170, Sub-Element: Boiler - Code: 1', # 'Element: HEATING - Code: 170, Sub-Element: Heating Distribution - Code: 2', # 'Element: ELECTRICS - Code: 180, Sub-Element: Wiring - Code: 1', @@ -159,18 +265,9 @@ PEABODY_ELEMENT_MAP = { # 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Bathroom - Code: 8', # 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Toilets - Code: 9', # 'Element: Internal - Code: 50, Sub-Element: Wiring - Code: 24', -# 'Element: External - Code: 53, Sub-Element: Front Door Material - Code: 8', -# 'Element: External - Code: 53, Sub-Element: Primary Wall Finish - Code: 23', # 'Element: PASSENGER LIFTS - Code: 210, Sub-Element: Lift - Code: 2', # 'Element: Internal - Code: 50, Sub-Element: Heating Distribution Type - Code: 12', -# 'Element: External - Code: 53, Sub-Element: Downpipes - Code: 3', -# 'Element: External - Code: 53, Sub-Element: Fascia/Soffits/Bargeboards - Code: 6', -# 'Element: External - Code: 53, Sub-Element: Gutters - Code: 15', -# 'Element: External - Code: 53, Sub-Element: Paths & Hardstandings - Code: 18', -# 'Element: External - Code: 53, Sub-Element: Pitched Roof Covering Material - Code: 21', # 'Element: Internal - Code: 50, Sub-Element: Secondary Bathroom Type - Code: 20', -# 'Element: External - Code: 53, Sub-Element: Chimney - Code: 2', -# 'Element: External - Code: 53, Sub-Element: External Decoration - Code: 4', # 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Gates - Code: 10', # 'Element: GENERAL - Code: 100, Sub-Element: Property Age Band - Code: 3', # 'Element: GENERAL - Code: 100, Sub-Element: Construction Type - Code: 14', @@ -179,46 +276,28 @@ PEABODY_ELEMENT_MAP = { # 'Element: Communal - Code: 51, Sub-Element: Common Bin Store Doors - Code: 5', # 'Element: Communal - Code: 51, Sub-Element: Common Bin Store Walls - Code: 7', # 'Element: Communal - Code: 51, Sub-Element: Common Primary Entrance Material - Code: 28', -# 'Element: External - Code: 53, Sub-Element: Parking Areas - Code: 17', -# 'Element: External - Code: 53, Sub-Element: Front Fencing - Code: 9', -# 'Element: External - Code: 53, Sub-Element: Retaining Walls - Code: 28', # 'Element: Communal - Code: 51, Sub-Element: Common Internal Decorations - Code: 20', # 'Element: Communal - Code: 51, Sub-Element: Common Internal Floor Finish - Code: 22', # 'Element: Communal - Code: 51, Sub-Element: Common Walkways Finish - Code: 36', -# 'Element: External - Code: 53, Sub-Element: Boundary Walls - Code: 1', -# 'Element: External - Code: 53, Sub-Element: Flat Roof Covering Material - Code: 7', -# 'Element: External - Code: 53, Sub-Element: Porch/Canopy - Code: 22', -# 'Element: External - Code: 53, Sub-Element: Private Balcony - Code: 24', -# 'Element: External - Code: 53, Sub-Element: Rear Gate - Code: 27', # 'Element: Communal - Code: 51, Sub-Element: Common External Doors Other - Code: 17', # 'Element: Communal - Code: 51, Sub-Element: Common Stair Finish - Code: 32', -# 'Element: External - Code: 53, Sub-Element: Front Gate - Code: 10', -# 'Element: External - Code: 53, Sub-Element: Rear Fencing - Code: 26', -# 'Element: External - Code: 53, Sub-Element: Side Fencing - Code: 31', # 'Element: Communal - Code: 51, Sub-Element: Common Aerial - Code: 1', # 'Element: Communal - Code: 51, Sub-Element: Common AOV - Code: 2', # 'Element: Communal - Code: 51, Sub-Element: Common Door Entry System - Code: 14', # 'Element: Communal - Code: 51, Sub-Element: Common Fire Alarm - Code: 19', # 'Element: Communal - Code: 51, Sub-Element: Common Internal Doors - Code: 21', -# 'Element: External - Code: 53, Sub-Element: Store Door Material - Code: 35', -# 'Element: External - Code: 53, Sub-Element: Secondary Wall Finish - Code: 30', # 'Element: Communal - Code: 51, Sub-Element: Common Emergency Lighting - Code: 16', # 'Element: Communal - Code: 51, Sub-Element: Common Lateral Mains - Code: 24', # 'Element: Communal - Code: 51, Sub-Element: Common Lighting - Code: 25', # 'Element: Communal - Code: 51, Sub-Element: Common Store Roof - Code: 34', # 'Element: Communal - Code: 51, Sub-Element: Common Store Walls - Code: 35', -# 'Element: External - Code: 53, Sub-Element: Cladding Material - Code: 41', -# 'Element: External - Code: 53, Sub-Element: Spandrel Panels - Code: 40', # 'Element: Communal - Code: 51, Sub-Element: Common CCTV - Code: 11', # 'Element: Communal - Code: 51, Sub-Element: Common Kitchen - Code: 23', # 'Element: Communal - Code: 51, Sub-Element: Common Secondary Entrance Material - Code: 30', # 'Element: Communal - Code: 51, Sub-Element: Common Warden Call System - Code: 37', -# 'Element: External - Code: 53, Sub-Element: Lintels - Code: 16', # 'Element: Communal - Code: 51, Sub-Element: Common Boiler - Code: 9', -# 'Element: External - Code: 53, Sub-Element: Soil & Vent Material - Code: 32', # 'Element: Communal - Code: 51, Sub-Element: Common Passenger Lift - Code: 27', # 'Element: Communal - Code: 51, Sub-Element: Common Store Doors - Code: 33', -# 'Element: External - Code: 53, Sub-Element: Window Type 02 - Code: 39', # 'Element: Communal - Code: 51, Sub-Element: Common BMS - Code: 8', # 'Element: Communal - Code: 51, Sub-Element: Common Booster Pump - Code: 10', # 'Element: Communal - Code: 51, Sub-Element: Common Dry Riser - Code: 15', @@ -226,25 +305,14 @@ PEABODY_ELEMENT_MAP = { # 'Element: Communal - Code: 51, Sub-Element: Common Bin Store Roof - Code: 6', # 'Element: Communal - Code: 51, Sub-Element: Common Bathroom - Code: 4', # 'Element: Communal - Code: 51, Sub-Element: Common WC - Code: 38', -# 'Element: External - Code: 53, Sub-Element: Wall Insulation - Code: 36', -# 'Element: External - Code: 53, Sub-Element: Garage Door - Code: 12', # 'Element: Communal - Code: 51, Sub-Element: Common Cold Water Storage Tank - Code: 13', # 'Element: Communal - Code: 51, Sub-Element: Common Sprinker - Code: 31', -# 'Element: External - Code: 53, Sub-Element: Garage Walls - Code: 14', # 'Element: Communal - Code: 51, Sub-Element: Communal Plug Sockets - Code: 40', # 'Element: Communal - Code: 51, Sub-Element: Common Wet Riser - Code: 39', # 'Element: Communal - Code: 51, Sub-Element: Common Refuse Chute - Code: 29', -# 'Element: External - Code: 53, Sub-Element: Secondary Glazing - Code: 29', -# 'Element: External - Code: 53, Sub-Element: Solar Thermals - Code: 34', -# 'Element: External - Code: 53, Sub-Element: Garage Roof - Code: 13', -# 'Element: External - Code: 53, Sub-Element: Patio/French Door - Code: 19', -# 'Element: External - Code: 53, Sub-Element: Rear Door Material - Code: 25', # 'Element: Internal - Code: 50, Sub-Element: Party Wall Fire Break - Code: 16', # 'Element: Internal - Code: 50, Sub-Element: Boiler Type - Code: 25', -# 'Element: External - Code: 53, Sub-Element: Roof Structure - Code: 47', -# 'Element: External - Code: 53, Sub-Element: Front Door Type - Code: 43', # 'Element: Communal - Code: 51, Sub-Element: Common Cirulation Space - Code: 12', -# 'Element: External - Code: 53, Sub-Element: External Noise Insulation - Code: 5', # 'Element: Internal - Code: 50, Sub-Element: Door Entry Handset - Code: 8', # 'Element: Internal - Code: 50, Sub-Element: Cold Water Storage Tank - Code: 6', # 'Element: Internal - Code: 50, Sub-Element: Programmable Heating - Code: 19', @@ -254,6 +322,4 @@ PEABODY_ELEMENT_MAP = { # 'Element: Internal - Code: 50, Sub-Element: Stairlift - Code: 22', # 'Element: Internal - Code: 50, Sub-Element: Primary Bathroom Location - Code: 17', # 'Element: Internal - Code: 50, Sub-Element: Disabled Hoist Tracking - Code: 7', -# 'Element: External - Code: 53, Sub-Element: Garage Type - Code: 44', -# 'Element: External - Code: 53, Sub-Element: Private Balcony Balustrade Material - Code: 45', # 'Element: Internal - Code: 50, Sub-Element: Disabled Facilities - Code: 26' diff --git a/backend/condition/tests/mapping/test_peabody_mapper.py b/backend/condition/tests/mapping/test_peabody_mapper.py index 7fad77f7..a975a308 100644 --- a/backend/condition/tests/mapping/test_peabody_mapper.py +++ b/backend/condition/tests/mapping/test_peabody_mapper.py @@ -71,8 +71,8 @@ def test_peabody_mapper_maps_property(): ), AssetCondition( uprn=1, - element=Element.PROPERTY, - aspect_type=AspectType.EXTERNAL_DECORATION, + element=Element.EXTERNAL_DECORATION, + aspect_type=AspectType.CONDITION, value="Normal", quantity=1, install_date=None, From 793ae8098f69062291490c4bbc33ee5202188743 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Mon, 26 Jan 2026 10:25:03 +0000 Subject: [PATCH 33/68] More peabody -> domain mapping --- backend/condition/domain/element.py | 8 +- .../domain/mapping/lbwf/lbwf_element_map.py | 4 +- .../mapping/peabody/peabody_element_map.py | 101 +++++++++++++----- 3 files changed, 84 insertions(+), 29 deletions(-) diff --git a/backend/condition/domain/element.py b/backend/condition/domain/element.py index fed5ab3b..5fbd35dc 100644 --- a/backend/condition/domain/element.py +++ b/backend/condition/domain/element.py @@ -15,6 +15,7 @@ class Element(str, Enum): ACCESSIBLE_HOUSING_REGISTER = "accessible_housing_register" ASBESTOS = "asbestos" QUALITY_STANDARD = "quality_standard" + CCU = "ccu" # ====================== # EXTERNAL – ROOF @@ -94,18 +95,22 @@ class Element(str, Enum): KITCHEN = "kitchen" KITCHEN_SPACE_LAYOUT = "kitchen_space_layout" TENANT_INSTALLED_KITCHEN = "tenant_installed_kitchen" + KITCHEN_EXTRACTOR_FAN = "kitchen_extractor_fan" # ====================== # INTERNAL – BATHROOM # ====================== BATHROOM = "bathroom" + SECONDARY_BATHROOM = "secondary_bathroom" + SECONDARY_TOILET = "secondary_toilet" + BATHROOM_EXTRACTOR_FAN = "bathroom_extractor_fan" # ====================== # INTERNAL – HEATING / WATER # ====================== + CENTRAL_HEATING = "central_heating" HEATING_BOILER = "heating_boiler" HEATING_DISTRIBUTION = "heating_distribution" - HEATING_EXTENT = "heating_extent" SECONDARY_HEATING = "secondary_heating" HOT_WATER_SYSTEM = "hot_water_system" COLD_WATER_STORAGE = "cold_water_storage" @@ -113,6 +118,7 @@ class Element(str, Enum): HEATING_SYSTEM = "heating_system" BOILER_FUEL = "boiler_fuel" WATER_HEATING = "water_heating" + PROGRAMMABLE_HEATING = "programmable_heating" # ====================== # INTERNAL – ELECTRICS / FIRE diff --git a/backend/condition/domain/mapping/lbwf/lbwf_element_map.py b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py index 047013f4..be8a50b2 100644 --- a/backend/condition/domain/mapping/lbwf/lbwf_element_map.py +++ b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py @@ -61,8 +61,8 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { # INTERNAL – HEATING # ========================================================== "INTCHEXTNT": ElementMapping( - element=Element.HEATING_EXTENT, - aspect_type=AspectType.CONFIGURATION, + element=Element.CENTRAL_HEATING, + aspect_type=AspectType.EXTENT, ), "INTCHDIST": ElementMapping( element=Element.HEATING_DISTRIBUTION, diff --git a/backend/condition/domain/mapping/peabody/peabody_element_map.py b/backend/condition/domain/mapping/peabody/peabody_element_map.py index 81aa8b9e..8c29c60b 100644 --- a/backend/condition/domain/mapping/peabody/peabody_element_map.py +++ b/backend/condition/domain/mapping/peabody/peabody_element_map.py @@ -10,6 +10,16 @@ PEABODY_ELEMENT_MAP = { (100, 1): ElementMapping(element=Element.PROPERTY, aspect_type=AspectType.TYPE), # (100, 3): ElementMapping(element=Element.PROPERTY, aspect_type=AspectType.AGE), # (100, 14): ElementMapping(element="property", aspect_type="construction_type"), + (50, 2): ElementMapping( + element=Element.CARBON_MONOXIDE_DETECTION, aspect_type=AspectType.TYPE + ), + (50, 3): ElementMapping(element=Element.CCU, aspect_type=AspectType.TYPE), + (50, 11): ElementMapping( + element=Element.HEAT_DETECTION, aspect_type=AspectType.TYPE + ), + (50, 21): ElementMapping( + element=Element.SMOKE_DETECTION, aspect_type=AspectType.TYPE + ), # ========================================================== # EXTERNAL – WALLS # ========================================================== @@ -47,6 +57,9 @@ PEABODY_ELEMENT_MAP = { (120, 2): ElementMapping( element=Element.EXTERNAL_WALL, aspect_type=AspectType.FINISH ), + (120, 3): ElementMapping( + element=Element.PRIMARY_WALL, aspect_type=AspectType.INSULATION + ), # This code element code is actually "WALL" not "external wall" - correct? # ========================================================== # EXTERNAL – ROOFS # ========================================================== @@ -194,10 +207,36 @@ PEABODY_ELEMENT_MAP = { # ========================================================== # INTERNAL – BATHROOMS & KITCHENS # ========================================================== + (50, 1): ElementMapping( + element=Element.SECONDARY_TOILET, aspect_type=AspectType.PRESENCE + ), + (50, 9): ElementMapping( + element=Element.BATHROOM_EXTRACTOR_FAN, aspect_type=AspectType.PRESENCE + ), + (50, 9): ElementMapping(element=Element.KITCHEN, aspect_type=AspectType.TYPE), + (50, 10): ElementMapping( + element=Element.KITCHEN_EXTRACTOR_FAN, aspect_type=AspectType.PRESENCE + ), + (50, 13): ElementMapping( + element=Element.KITCHEN_SPACE_LAYOUT, aspect_type=AspectType.ADEQUACY + ), + (50, 17): ElementMapping(element=Element.BATHRROM, aspect_type=AspectType.LOCATION), + (50, 18): ElementMapping( + element=Element.BATHROOM, aspect_type=AspectType.TYPE + ), # Actually "Primary bathroom type" - ok like this? + (50, 20): ElementMapping( + element=Element.BATHROOM, aspect_type=AspectType.TYPE, element_instance=2 + ), # Actually "Secondary bathroom type" - ok like this? (160, 1): ElementMapping(element=Element.KITCHEN, aspect_type=AspectType.CONDITION), + (160, 2): ElementMapping( + element=Element.KITCHEN_SPACE_LAYOUT, aspect_type=AspectType.ADEQUACY + ), (190, 1): ElementMapping( element=Element.BATHROOM, aspect_type=AspectType.CONDITION ), + (190, 2): ElementMapping( + element=Element.SECONDARY_TOILET, aspect_type=AspectType.TYPE + ), # ========================================================== # COMMUNAL SYSTEMS # ========================================================== @@ -220,6 +259,42 @@ PEABODY_ELEMENT_MAP = { element=Element.COMMUNAL_FLOOR_COVERING, aspect_type=AspectType.MATERIAL ), # ========================================================== + # INTERNAL – HEATING + # ========================================================== + (50, 4): ElementMapping( + element=Element.HEATING_BOILER, aspect_type=AspectType.PRESENCE + ), # This is actually "Central heating boiler" - ok like this? + (50, 5): ElementMapping( + element=Element.CENTRAL_HEATING, aspect_type=AspectType.EXTENT + ), + (50, 6): ElementMapping( + element=Element.COLD_WATER_STORAGE, aspect_type=AspectType.PRESENCE + ), + (50, 12): ElementMapping( + element=Element.HEATING_DISTRIBUTION, aspect_type=AspectType.TYPE + ), + (50, 19): ElementMapping( + element=Element.PROGRAMMABLE_HEATING, aspect_type=AspectType.TYPE + ), + (50, 25): ElementMapping( + element=Element.HEATING_BOILER, aspect_type=AspectType.TYPE + ), + (170, 1): ElementMapping( + element=Element.HEATING_BOILER, aspect_type=AspectType.TYPE + ), # Duplicate of (50,25) - correct? + (170, 2): ElementMapping( + element=Element.HEATING_DISTRIBUTION, aspect_type=AspectType.TYPE + ), # Duplicate of (50,12) - correct? + (170, 3): ElementMapping( + element=Element.SECONDARY_HEATING, aspect_type=AspectType.TYPE + ), + (170, 4): ElementMapping( + element=Element.COLD_WATER_STORAGE, aspect_type=AspectType.TYPE + ), + (170, 5): ElementMapping( + element=Element.HOT_WATER_SYSTEM, aspect_type=AspectType.TYPE + ), + # ========================================================== # HHSRS # ========================================================== (54, 1): ElementMapping( @@ -238,36 +313,16 @@ PEABODY_ELEMENT_MAP = { # unhandled -# 'Element: HEATING - Code: 170, Sub-Element: Boiler - Code: 1', -# 'Element: HEATING - Code: 170, Sub-Element: Heating Distribution - Code: 2', # 'Element: ELECTRICS - Code: 180, Sub-Element: Wiring - Code: 1', # 'Element: ELECTRICS - Code: 180, Sub-Element: Consumer Unit - Code: 2', # 'Element: ELECTRICS - Code: 180, Sub-Element: Smoke Detectors - Code: 3', -# 'Element: KITCHEN - Code: 160, Sub-Element: Kitchen space and layout - Code: 2', -# 'Element: HEATING - Code: 170, Sub-Element: Secondary Heating - Code: 3', -# 'Element: BATHROOM - Code: 190, Sub-Element: Secondary Toilet - Code: 2', # 'Element: ELECTRICS - Code: 180, Sub-Element: Carbon Monoxide Alarms - Code: 4', -# 'Element: HEATING - Code: 170, Sub-Element: Hot Water - Code: 5', -# 'Element: HEATING - Code: 170, Sub-Element: Cold Water - Code: 4', -# 'Element: WALLS - Code: 120, Sub-Element: Wall Insulation - Code: 3', -# 'Element: Internal - Code: 50, Sub-Element: Additional WC - Code: 1', -# 'Element: Internal - Code: 50, Sub-Element: Carbon Monoxide Detector Type - Code: 2', -# 'Element: Internal - Code: 50, Sub-Element: CCU - Code: 3', -# 'Element: Internal - Code: 50, Sub-Element: Central Heating Boiler - Code: 4', -# 'Element: Internal - Code: 50, Sub-Element: Extractor Fan Bathroom - Code: 9', -# 'Element: Internal - Code: 50, Sub-Element: Extractor Fan Kitchen - Code: 10', -# 'Element: Internal - Code: 50, Sub-Element: Heat Detector Type - Code: 11', -# 'Element: Internal - Code: 50, Sub-Element: Kitchen Type - Code: 14', -# 'Element: Internal - Code: 50, Sub-Element: Primary Bathroom Type - Code: 18', -# 'Element: Internal - Code: 50, Sub-Element: Smoke Detector Type - Code: 21', # 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Lifts - Code: 5', # 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Kitchen - Code: 7', # 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Bathroom - Code: 8', # 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Toilets - Code: 9', # 'Element: Internal - Code: 50, Sub-Element: Wiring - Code: 24', # 'Element: PASSENGER LIFTS - Code: 210, Sub-Element: Lift - Code: 2', -# 'Element: Internal - Code: 50, Sub-Element: Heating Distribution Type - Code: 12', -# 'Element: Internal - Code: 50, Sub-Element: Secondary Bathroom Type - Code: 20', # 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Gates - Code: 10', # 'Element: GENERAL - Code: 100, Sub-Element: Property Age Band - Code: 3', # 'Element: GENERAL - Code: 100, Sub-Element: Construction Type - Code: 14', @@ -311,15 +366,9 @@ PEABODY_ELEMENT_MAP = { # 'Element: Communal - Code: 51, Sub-Element: Common Wet Riser - Code: 39', # 'Element: Communal - Code: 51, Sub-Element: Common Refuse Chute - Code: 29', # 'Element: Internal - Code: 50, Sub-Element: Party Wall Fire Break - Code: 16', -# 'Element: Internal - Code: 50, Sub-Element: Boiler Type - Code: 25', # 'Element: Communal - Code: 51, Sub-Element: Common Cirulation Space - Code: 12', # 'Element: Internal - Code: 50, Sub-Element: Door Entry Handset - Code: 8', -# 'Element: Internal - Code: 50, Sub-Element: Cold Water Storage Tank - Code: 6', -# 'Element: Internal - Code: 50, Sub-Element: Programmable Heating - Code: 19', -# 'Element: Internal - Code: 50, Sub-Element: Central Heating Extent - Code: 5', -# 'Element: Internal - Code: 50, Sub-Element: Kitchen Space & Layout - Code: 13', # 'Element: Internal - Code: 50, Sub-Element: Loft Insulation - Code: 15', # 'Element: Internal - Code: 50, Sub-Element: Stairlift - Code: 22', -# 'Element: Internal - Code: 50, Sub-Element: Primary Bathroom Location - Code: 17', # 'Element: Internal - Code: 50, Sub-Element: Disabled Hoist Tracking - Code: 7', # 'Element: Internal - Code: 50, Sub-Element: Disabled Facilities - Code: 26' From 0ad3f099026854c4cc28d3040d3fcee3bee68ef8 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 26 Jan 2026 12:25:51 +0000 Subject: [PATCH 34/68] refactoring roof recommendations logic --- asset_list/app.py | 10 +- etl/find_my_epc/RetrieveFindMyEpc.py | 2 +- recommendations/Costs.py | 31 +++- recommendations/RoofRecommendations.py | 154 +++++++++++++++--- .../tests/test_roof_recommendations.py | 40 +++++ 5 files changed, 199 insertions(+), 38 deletions(-) diff --git a/asset_list/app.py b/asset_list/app.py index 21a06a07..01906c5f 100644 --- a/asset_list/app.py +++ b/asset_list/app.py @@ -60,7 +60,7 @@ def app(): """ data_folder = "/Users/khalimconn-kowlessar/Documents/hestia/Customers/Hackney" - data_filename = "Domna SHF Wave 3.xlsx" + data_filename = "Domna SHF Wave 3 (3).xlsx" sheet_name = "Domna Wave 3" postcode_column = 'Postcode' address1_column = "Address 1" @@ -68,11 +68,11 @@ def app(): fulladdress_column = None address_cols_to_concat = ["Address 1"] missing_postcodes_method = None - landlord_year_built = None + landlord_year_built = "Construction Years" landlord_os_uprn = "UPRN" - landlord_property_type = None - landlord_built_form = None - landlord_wall_construction = None + landlord_property_type = "Type" + landlord_built_form = "Attachment" + landlord_wall_construction = "Wall type" landlord_roof_construction = None landlord_heating_system = None landlord_existing_pv = None diff --git a/etl/find_my_epc/RetrieveFindMyEpc.py b/etl/find_my_epc/RetrieveFindMyEpc.py index cf6659f9..82215443 100644 --- a/etl/find_my_epc/RetrieveFindMyEpc.py +++ b/etl/find_my_epc/RetrieveFindMyEpc.py @@ -665,7 +665,7 @@ class RetrieveFindMyEpc: ], "Change heating to gas condensing boiler": ["boiler_upgrade"], "Fan assisted storage heaters and dual immersion cylinder": ["high_heat_retention_storage_heaters"], - "Flat roof or sloping ceiling insulation": ["flat_roof_insulation"], + "Flat roof or sloping ceiling insulation": ["flat_roof_insulation", "sloping_ceiling_insulation"], "Heating controls (room thermostat)": [ "roomstat_programmer_trvs", "time_temperature_zone_control" ], diff --git a/recommendations/Costs.py b/recommendations/Costs.py index 60b1d8a2..3a65312e 100644 --- a/recommendations/Costs.py +++ b/recommendations/Costs.py @@ -160,6 +160,13 @@ class Costs: "low_energy_lighting": 0.26, "high_heat_retention_storage_heaters": 0.1, "windows_glazing": 0.15, + "boiler_upgrade": 0.26, + "time_and_temperature_zone_control": 0.1, + "roomstat_programmer_trvs": 0.1, + "room_roof_insulation": 0.26, + "heater_removal": 0.1, + "sealing_open_fireplace": 0.1, + "mechanical_ventilation": 0.26 } # Preliminaries are a percentage of the total cost of the work and covers the cost of site-specific costs @@ -664,10 +671,12 @@ class Costs: subtotal_before_vat = total_cost / (1 + self.VAT_RATE) vat = total_cost - subtotal_before_vat + contingency_rate = self.CONTINGENCIES["roomstat_programmer_trvs"] + return { "total": total_cost, - "contingency": total_cost * self.CONTINGENCY, - "contingency_rate": self.CONTINGENCY, + "contingency": total_cost * contingency_rate, + "contingency_rate": contingency_rate, "subtotal": subtotal_before_vat, "vat": vat, "labour_hours": labour_hours, @@ -698,10 +707,12 @@ class Costs: labour_days = np.ceil(labour_hours / 8) + contingency_rate = self.CONTINGENCIES["time_and_temperature_zone_control"] + return { "total": total_cost, - "contingency": total_cost * self.CONTINGENCY, - "contingency_rate": self.CONTINGENCY, + "contingency": total_cost * contingency_rate, + "contingency_rate": contingency_rate, "subtotal": subtotal_before_vat, "vat": vat, "labour_hours": labour_hours, @@ -752,10 +763,12 @@ class Costs: subtotal_before_vat = removal_cost total_cost = subtotal_before_vat + vat + contingency_rate = self.CONTINGENCIES["heater_removal"] + return { "total": total_cost, - "contingency": total_cost * self.CONTINGENCY, - "contingency_rate": self.CONTINGENCY, + "contingency": total_cost * contingency_rate, + "contingency_rate": contingency_rate, "subtotal": subtotal_before_vat, "vat": vat, "labour_hours": removal_labour_hours, @@ -858,10 +871,12 @@ class Costs: subtotal_before_vat += system_change_cost_before_vat vat += system_change_vat + contingency_rate = self.CONTINGENCIES["boiler_upgrade"] + return { "total": total_cost, - "contingency": total_cost * self.CONTINGENCY, - "contingency_rate": self.CONTINGENCY, + "contingency": total_cost * contingency_rate, + "contingency_rate": contingency_rate, "subtotal": subtotal_before_vat, "vat": vat, "labour_hours": labour_hours, diff --git a/recommendations/RoofRecommendations.py b/recommendations/RoofRecommendations.py index 7f7c334e..1d6fe06c 100644 --- a/recommendations/RoofRecommendations.py +++ b/recommendations/RoofRecommendations.py @@ -137,41 +137,127 @@ class RoofRecommendations: """ pass - def recommend(self, phase, measures=None, default_u_values=False): + @staticmethod + def is_sloping_ceiling_appropriate( + is_pitched: bool, is_loft: bool, is_assumed: bool, has_sloping_ceiling_recommendation: bool, + primary_roof_is_sloped: bool + ) -> bool: + """ + :param is_pitched: Boolean - indicates whether or not the roof is pitched + :param is_loft: Boolean - indicates whether or not the roof is described as a loft + :param is_assumed: Boolean - indiates if the assessment of the roof is assumed or actually confirmed + :param has_sloping_ceiling_recommendation: Boolean - indicates if the property has a sloping ceiling + recommendation + :param primary_roof_is_sloped: Boolean - indicates if the primary room is described a sloped (as opposed to + an extension) + :return: + """ + # We need to check: + # 1) If the property has a pitched roof + # 2) Does it have a recommendation for sloping ceiling + # 3) Is the insulation status NOT assumed + # 4) Is there a sloping ceiling recommendation (this may relate to the primary or secondary roof) + + # If we have a loft primary roof and sloping cei + + # The property is pitched, not a loft, not assumed and has a sloping ceiling rec + if (is_pitched and not is_loft and not is_assumed and has_sloping_ceiling_recommendation and + primary_roof_is_sloped): + return True + + return False + + @staticmethod + def is_loft_insulation_appropriate( + non_invasive_recommendations, measures, is_pitched, is_at_rafters, rir_over_loft + ) -> bool: + """ + Determine if loft insulation is appropriate + :param non_invasive_recommendations: List - list of non-invasive recommendations + :param measures: List - list of measures + :param is_pitched: Boolean - indicates whether or not the roof is pitched + :param is_at_rafters: Boolean - indicates whether or not the loft insulation is at rafters + :param rir_over_loft: Boolean - indicates whether or not there we should be doing RIR insulation + :return: + """ + + has_li_in_measures = "loft_insulation" in measures + has_li_non_invasive_recommendation = any( + x["type"] == "loft_insulation" for x in non_invasive_recommendations + ) + + return has_li_non_invasive_recommendation or ( + is_pitched and has_li_in_measures and not is_at_rafters + ) and not rir_over_loft + + @staticmethod + def is_flat_roof_insulation_appropriate( + is_flat: bool, measures: List, non_invasive_recommendations: List + ) -> bool: + """ + Determine if flat roof insulation is appropriate + :param is_flat: Boolean - indicates whether or not the roof is flat + :param measures: List - list of measures + :param non_invasive_recommendations: List - list of non-invasive recommendations + :return: + """ + + flat_roof_in_measures = "flat_roof_insulation" in measures + flat_roof_non_invasive_rec = has_li_non_invasive_recommendation = any( + x["type"] == "flat_roof_insulation" for x in non_invasive_recommendations + ) + + return (is_flat and flat_roof_in_measures) or flat_roof_non_invasive_rec + + def _does_roof_need_recommendation(self, measures: List | None = None, u_value: float | None = None): + """ + Utility function to recommend which contains the logic to determine whether the roof needs a recommendation + :return: + """ + # If there is a property above, nothing can be done if self.property.roof["has_dwelling_above"]: - return + return False - measures = MEASURE_MAP["roof_insulation"] if measures is None else measures - - u_value = self.property.roof["thermal_transmittance"] - - # If we have a flat roof but we don't have flat roof as a measure, we exit + # If we have a flat roof but not flat roof insulation recommendation if self.property.roof["is_flat"] and "flat_roof_insulation" not in measures: - return + return False - # We check if the roof is already insulated and if so, we exit - - # Building regulations part L recommend installing at least 270mm of insulation, however generally we - # experience diminishing returns in terms of SAP once we go beyond around 150mm of insulation - # This only holds true for pitched roofs. + # Logic to check if we have an already insulated loft if self.is_loft_already_insulated(measures): - return + return False + # Logic to check if we have an insulated flat roof if (self.insulation_thickness >= self.MINIMUM_FLAT_ROOF_ISULATION_MM) and self.property.roof["is_flat"]: - return + return False + # Logic to check if we have an already insulated room in roof if self.is_room_roof_insulated_or_unsuitable(measures): - return + return False if self.property.roof["is_thatched"]: - return + return False - # If we have a u-value and we don't have a non-invasive recommendation, we can't recommend anything if (u_value is not None) and not any( x in MEASURE_MAP["roof_insulation"] for x in [r["type"] for r in self.property.non_invasive_recommendations] ): - # We don't have enough information to provide a recommendation + return False + + def recommend(self, phase: int, measures: List | None = None, default_u_values: bool = False): + """ + Main method to recommend roof insulation measures + :param phase: Integer - phase of the recommendation, determines the order in which recommendations are + applied to the property + :param measures: List - list of measures to consider for recommendation + :param default_u_values: Boolean - whether or not to use default u-values for the recommendations + :return: + """ + + measures = MEASURE_MAP["roof_insulation"] if measures is None else measures + u_value = self.property.roof["thermal_transmittance"] + property_needs_roof_recommendation = self._does_roof_need_recommendation(measures, u_value) + + if not property_needs_roof_recommendation: return u_value = get_roof_u_value( @@ -200,17 +286,37 @@ class RoofRecommendations: # 1) We have an uninsulated loft (assumed) # 2) We have a non-intrusive recommendation for room in roof insulation + is_pitched = self.property.roof["is_pitched"] + is_loft = self.property.roof["is_loft"] + is_assumed = self.property.roof["is_assumed"] + is_at_rafters = self.property.roof["is_at_rafters"] + has_sloping_ceiling_recommendation = any( + x["type"] == "sloping_ceiling_insulation" for x in non_invasive_recommendations + ) + primary_roof_is_sloped = False # TODO + rir_over_loft = ( - self.property.roof["is_pitched"] and + is_pitched and self.property.roof["insulation_thickness"] == "none" and "room_in_roof_insulation" in [x["type"] for x in non_invasive_recommendations] ) + needs_sloping_ceiling = self.is_sloping_ceiling_appropriate( + is_pitched=is_pitched, is_loft=is_loft, is_assumed=is_assumed, + has_sloping_ceiling_recommendation=has_sloping_ceiling_recommendation, + primary_roof_is_sloped=primary_roof_is_sloped + ) + + needs_loft_insulation = self.is_loft_insulation_appropriate( + non_invasive_recommendations=non_invasive_recommendations, measures=measures, + is_pitched=is_pitched, is_at_rafters=is_at_rafters, rir_over_loft=rir_over_loft + ) + + ################################################## + # ~~~~~ Loft Insulation Recommendation Logic ~~~~~ + ################################################## # We firstly handle non-intrusive recommendations, which may override the normal roof insulation recommendations - if ("loft_insulation" in [x["type"] for x in non_invasive_recommendations]) or ( - self.property.roof["is_pitched"] and "loft_insulation" in measures and - not self.property.roof["is_at_rafters"] - ) and not rir_over_loft: + if needs_loft_insulation: self.recommend_roof_insulation( u_value=u_value, insulation_thickness=self.insulation_thickness, diff --git a/recommendations/tests/test_roof_recommendations.py b/recommendations/tests/test_roof_recommendations.py index 2241aeb7..b8cea10b 100644 --- a/recommendations/tests/test_roof_recommendations.py +++ b/recommendations/tests/test_roof_recommendations.py @@ -2,6 +2,7 @@ from backend.Property import Property from recommendations.RoofRecommendations import RoofRecommendations from recommendations.tests.test_data.materials import materials from etl.epc.Record import EPCRecord +import pytest class TestRoofRecommendations: @@ -402,3 +403,42 @@ class TestRoofRecommendations: roof_recommender14.recommend(phase=0) assert not roof_recommender14.recommendations + + # ~~~~~~~~~~~~ Sloping Ceiling Insulation ~~~~~~~~~~~~ + @pytest.mark.parameterize("roof", + [ + ( + # For this example, the roof is pitched, without insulation and the description + # isn't assumed + {'original_description': 'Pitched, no insulation', 'thermal_transmittance': None, + 'thermal_transmittance_unit': None, + 'is_pitched': True, 'is_roof_room': False, 'is_loft': False, 'is_flat': False, + 'is_thatched': False, + 'is_at_rafters': False, 'is_assumed': False, 'has_dwelling_above': False, + 'is_valid': True, + 'insulation_thickness': 'none'} + ) + ] + ) + def test_sloping_ceiling_valid(self, roof): + # All conditions are met and therefore we should produce a sloping ceiling recommendation + assert RoofRecommendations.is_sloping_ceiling_appropriate( + is_pitched=True, is_loft=False, is_assumed=False, has_sloping_ceiling_recommendation=True + ) + + # One condition not met - we cannot verify + assert not RoofRecommendations.is_sloping_ceiling_appropriate( + is_pitched=True, is_loft=True, is_assumed=False, has_sloping_ceiling_recommendation=True + ) + assert not RoofRecommendations.is_sloping_ceiling_appropriate( + is_pitched=False, is_loft=False, is_assumed=False, has_sloping_ceiling_recommendation=True, + primary_roof_is_sloped=True + ) + assert not RoofRecommendations.is_sloping_ceiling_appropriate( + is_pitched=True, is_loft=False, is_assumed=True, has_sloping_ceiling_recommendation=True, + primary_roof_is_sloped=True + ) + assert not RoofRecommendations.is_sloping_ceiling_appropriate( + is_pitched=True, is_loft=False, is_assumed=True, has_sloping_ceiling_recommendation=True, + primary_roof_is_sloped=True + ) From 3da9a643e0cdd014adf0bf633e69e75fbccdc443 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Mon, 26 Jan 2026 12:37:57 +0000 Subject: [PATCH 35/68] more peabody mappings --- backend/condition/domain/aspect_type.py | 4 + backend/condition/domain/element.py | 25 +++- .../mapping/peabody/peabody_element_map.py | 111 +++++++++++++----- 3 files changed, 110 insertions(+), 30 deletions(-) diff --git a/backend/condition/domain/aspect_type.py b/backend/condition/domain/aspect_type.py index 94522b03..2dc2be58 100644 --- a/backend/condition/domain/aspect_type.py +++ b/backend/condition/domain/aspect_type.py @@ -29,3 +29,7 @@ class AspectType(str, Enum): FIRE_RATING = "fire_rating" EXTERNAL_DECORATION = "external_decoration" WORK_REQUIRED = "work_required" + AGE_BAND = "age_band" + CONSTRUCTION_TYPE = "construction_type" + CLASSIFICATION = "classification" + SYSTEM = "system" diff --git a/backend/condition/domain/element.py b/backend/condition/domain/element.py index 5fbd35dc..b146d09b 100644 --- a/backend/condition/domain/element.py +++ b/backend/condition/domain/element.py @@ -16,6 +16,8 @@ class Element(str, Enum): ASBESTOS = "asbestos" QUALITY_STANDARD = "quality_standard" CCU = "ccu" + PASSENGER_LIFT = "passenger_lift" + STAIRLIFT = "stairlift" # ====================== # EXTERNAL – ROOF @@ -130,21 +132,40 @@ class Element(str, Enum): CARBON_MONOXIDE_DETECTION = "carbon_monoxide_detection" FIRE_DOOR_RATING = "fire_door_rating" FIRE_RISK_ASSESSMENT = "fire" + INTERNAL_WIRING = ( + "internal_wiring" # Is this definitely different from ELECTRICAL_WIRING? + ) # ====================== - # COMMUNAL SYSTEMS + # COMMUNAL # ====================== COMMUNAL_HEATING = "communal_heating" COMMUNAL_BOILER = "communal_boiler" COMMUNAL_ELECTRICS = "communal_electrics" COMMUNAL_FIRE_ALARM = "communal_fire_alarm" COMMUNAL_EMERGENCY_LIGHTING = "communal_emergency_lighting" - COMMUNAL_LIFT = "communal_lift" COMMUNAL_DOOR_ENTRY = "communal_door_entry" COMMUNAL_CCTV = "communal_cctv" COMMUNAL_BIN_STORE = "communal_bin_store" + COMMUNAL_BIN_STORE_DOORS = "communal_bin_store_doors" + COMMUNAL_BIN_STORE_WALLS = "communal_bin_store_wall" COMMUNAL_REFUSE_CHUTE = "communal_refuse_chute" COMMUNAL_FLOOR_COVERING = "communal_floor_covering" + COMMUNAL_KITCHEN = "communal_kitchen" + COMMUNAL_BATHROOM = "communal_bathroom" + COMMUNAL_TOILETS = "communal_toilets" + COMMUNAL_GATES = "communal_gates" + COMMUNAL_LIFT = "communal_lift" + COMMUNAL_PASSENGER_LIFT = "communal_passenger_lift" + COMMUNAL_BALCONY_WALKWAY = "communal_balcony_walkway" + COMMUNAL_PRIMARY_ENTRANCE = "communal_primary_entrance" + COMMUNAL_INTERNAL_DECORATIONS = "communal_internal_decorations" + COMMUNAL_INTERNAL_FLOOR = "communal_internal_floor" + COMMUNAL_WALKWAYS = "communal_walkways" + COMMUNAL_EXTERNAL_DOORS = "communal_external_doors" + COMMUNAL_STAIRS = "communal_stairs" + COMMUNAL_AERIAL = "communal_aerial" + COMMUNAL_AOV = "communal_aov" # ========================================================== # HHSRS – ALL 29 HAZARDS diff --git a/backend/condition/domain/mapping/peabody/peabody_element_map.py b/backend/condition/domain/mapping/peabody/peabody_element_map.py index 8c29c60b..08e63568 100644 --- a/backend/condition/domain/mapping/peabody/peabody_element_map.py +++ b/backend/condition/domain/mapping/peabody/peabody_element_map.py @@ -20,6 +20,19 @@ PEABODY_ELEMENT_MAP = { (50, 21): ElementMapping( element=Element.SMOKE_DETECTION, aspect_type=AspectType.TYPE ), + (50, 22): ElementMapping( + element=Element.STAIRLIFT, aspect_type=AspectType.PRESENCE + ), + (100, 3): ElementMapping(element=Element.PROPERTY, aspect_type=AspectType.AGE_BAND), + (100, 14): ElementMapping( + element=Element.PROPERTY, aspect_type=AspectType.CONSTRUCTION_TYPE + ), + (100, 16): ElementMapping( + element=Element.PROPERTY, aspect_type=AspectType.CLASSIFICATION + ), + (210, 2): ElementMapping( + element=Element.PASSENGER_LIFT, aspect_type=AspectType.TYPE + ), # ========================================================== # EXTERNAL – WALLS # ========================================================== @@ -238,8 +251,47 @@ PEABODY_ELEMENT_MAP = { element=Element.SECONDARY_TOILET, aspect_type=AspectType.TYPE ), # ========================================================== - # COMMUNAL SYSTEMS + # COMMUNAL # ========================================================== + (51, 1): ElementMapping( + element=Element.COMMUNAL_AERIAL, aspect_type=AspectType.PRESENCE + ), + (51, 2): ElementMapping( + element=Element.COMMUNAL_AOV, aspect_type=AspectType.PRESENCE + ), + (51, 3): ElementMapping( + element=Element.COMMUNAL_BALCONY_WALKWAY, aspect_type=AspectType.PRESENCE + ), + (51, 5): ElementMapping( + element=Element.COMMUNAL_BIN_STORE_DOORS, aspect_type=AspectType.PRESENCE + ), + (51, 7): ElementMapping( + element=Element.COMMUNAL_BIN_STORE_WALLS, aspect_type=AspectType.MATERIAL + ), + (51, 14): ElementMapping( + element=Element.COMMUNAL_DOOR_ENTRY, aspect_type=AspectType.SYSTEM + ), + (51, 17): ElementMapping( + element=Element.COMMUNAL_EXTERNAL_DOORS, aspect_type=AspectType.MATERIAL + ), + (51, 20): ElementMapping( + element=Element.COMMUNAL_INTERNAL_DECORATIONS, aspect_type=AspectType.PRESENCE + ), + (51, 22): ElementMapping( + element=Element.COMMUNAL_INTERNAL_FLOOR, aspect_type=AspectType.FINISH + ), + (51, 27): ElementMapping( + element=Element.COMMUNAL_PASSENGER_LIFT, aspect_type=AspectType.TYPE + ), + (51, 28): ElementMapping( + element=Element.COMMUNAL_PRIMARY_ENTRANCE, aspect_type=AspectType.MATERIAL + ), + (51, 32): ElementMapping( + element=Element.COMMUNAL_STAIRS, aspect_type=AspectType.FINISH + ), + (51, 36): ElementMapping( + element=Element.COMMUNAL_WALKWAYS, aspect_type=AspectType.FINISH + ), (200, 1): ElementMapping( element=Element.COMMUNAL_BOILER, aspect_type=AspectType.TYPE ), @@ -258,6 +310,18 @@ PEABODY_ELEMENT_MAP = { (200, 6): ElementMapping( element=Element.COMMUNAL_FLOOR_COVERING, aspect_type=AspectType.MATERIAL ), + (200, 7): ElementMapping( + element=Element.COMMUNAL_KITCHEN, aspect_type=AspectType.TYPE + ), + (200, 8): ElementMapping( + element=Element.COMMUNAL_BATHROOM, aspect_type=AspectType.TYPE + ), + (200, 9): ElementMapping( + element=Element.COMMUNAL_TOILETS, aspect_type=AspectType.TYPE + ), + (200, 10): ElementMapping( + element=Element.COMMUNAL_GATES, aspect_type=AspectType.TYPE + ), # ========================================================== # INTERNAL – HEATING # ========================================================== @@ -295,6 +359,24 @@ PEABODY_ELEMENT_MAP = { element=Element.HOT_WATER_SYSTEM, aspect_type=AspectType.TYPE ), # ========================================================== + # ELECTRICS + # ========================================================== + (50, 24): ElementMapping( + element=Element.INTERNAL_WIRING, aspect_type=AspectType.MATERIAL + ), + (180, 1): ElementMapping( + element=Element.ELECTRICAL_WIRING, aspect_type=AspectType.WORK_REQUIRED + ), # Not certain about the AspectType - only example in the sample data is "Full Rewire" + (180, 2): ElementMapping( + element=Element.CONSUMER_UNIT, aspect_type=AspectType.TYPE + ), + (180, 3): ElementMapping( + element=Element.SMOKE_DETECTION, aspect_type=AspectType.TYPE + ), # Duplicate of (50, 21) - correct? + (180, 4): ElementMapping( + element=Element.CARBON_MONOXIDE_DETECTION, aspect_type=AspectType.TYPE + ), # Duplicate of (50, 2) - correct? + # ========================================================== # HHSRS # ========================================================== (54, 1): ElementMapping( @@ -313,31 +395,6 @@ PEABODY_ELEMENT_MAP = { # unhandled -# 'Element: ELECTRICS - Code: 180, Sub-Element: Wiring - Code: 1', -# 'Element: ELECTRICS - Code: 180, Sub-Element: Consumer Unit - Code: 2', -# 'Element: ELECTRICS - Code: 180, Sub-Element: Smoke Detectors - Code: 3', -# 'Element: ELECTRICS - Code: 180, Sub-Element: Carbon Monoxide Alarms - Code: 4', -# 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Lifts - Code: 5', -# 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Kitchen - Code: 7', -# 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Bathroom - Code: 8', -# 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Toilets - Code: 9', -# 'Element: Internal - Code: 50, Sub-Element: Wiring - Code: 24', -# 'Element: PASSENGER LIFTS - Code: 210, Sub-Element: Lift - Code: 2', -# 'Element: COMMUNAL - Code: 200, Sub-Element: Communal Gates - Code: 10', -# 'Element: GENERAL - Code: 100, Sub-Element: Property Age Band - Code: 3', -# 'Element: GENERAL - Code: 100, Sub-Element: Construction Type - Code: 14', -# 'Element: GENERAL - Code: 100, Sub-Element: Classification - Code: 16', -# 'Element: Communal - Code: 51, Sub-Element: Common Balcony/Walkway - Code: 3', -# 'Element: Communal - Code: 51, Sub-Element: Common Bin Store Doors - Code: 5', -# 'Element: Communal - Code: 51, Sub-Element: Common Bin Store Walls - Code: 7', -# 'Element: Communal - Code: 51, Sub-Element: Common Primary Entrance Material - Code: 28', -# 'Element: Communal - Code: 51, Sub-Element: Common Internal Decorations - Code: 20', -# 'Element: Communal - Code: 51, Sub-Element: Common Internal Floor Finish - Code: 22', -# 'Element: Communal - Code: 51, Sub-Element: Common Walkways Finish - Code: 36', -# 'Element: Communal - Code: 51, Sub-Element: Common External Doors Other - Code: 17', -# 'Element: Communal - Code: 51, Sub-Element: Common Stair Finish - Code: 32', -# 'Element: Communal - Code: 51, Sub-Element: Common Aerial - Code: 1', -# 'Element: Communal - Code: 51, Sub-Element: Common AOV - Code: 2', # 'Element: Communal - Code: 51, Sub-Element: Common Door Entry System - Code: 14', # 'Element: Communal - Code: 51, Sub-Element: Common Fire Alarm - Code: 19', # 'Element: Communal - Code: 51, Sub-Element: Common Internal Doors - Code: 21', @@ -351,7 +408,6 @@ PEABODY_ELEMENT_MAP = { # 'Element: Communal - Code: 51, Sub-Element: Common Secondary Entrance Material - Code: 30', # 'Element: Communal - Code: 51, Sub-Element: Common Warden Call System - Code: 37', # 'Element: Communal - Code: 51, Sub-Element: Common Boiler - Code: 9', -# 'Element: Communal - Code: 51, Sub-Element: Common Passenger Lift - Code: 27', # 'Element: Communal - Code: 51, Sub-Element: Common Store Doors - Code: 33', # 'Element: Communal - Code: 51, Sub-Element: Common BMS - Code: 8', # 'Element: Communal - Code: 51, Sub-Element: Common Booster Pump - Code: 10', @@ -369,6 +425,5 @@ PEABODY_ELEMENT_MAP = { # 'Element: Communal - Code: 51, Sub-Element: Common Cirulation Space - Code: 12', # 'Element: Internal - Code: 50, Sub-Element: Door Entry Handset - Code: 8', # 'Element: Internal - Code: 50, Sub-Element: Loft Insulation - Code: 15', -# 'Element: Internal - Code: 50, Sub-Element: Stairlift - Code: 22', # 'Element: Internal - Code: 50, Sub-Element: Disabled Hoist Tracking - Code: 7', # 'Element: Internal - Code: 50, Sub-Element: Disabled Facilities - Code: 26' From e8b7a569ff2daca125fd830da926575704fe66e9 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 26 Jan 2026 13:25:15 +0000 Subject: [PATCH 36/68] working on rir insulation| --- .idea/copilot.data.migration.ask.xml | 6 ++ .idea/copilot.data.migration.edit.xml | 6 ++ recommendations/RoofRecommendations.py | 113 +++++++++++++++++++------ 3 files changed, 100 insertions(+), 25 deletions(-) create mode 100644 .idea/copilot.data.migration.ask.xml create mode 100644 .idea/copilot.data.migration.edit.xml diff --git a/.idea/copilot.data.migration.ask.xml b/.idea/copilot.data.migration.ask.xml new file mode 100644 index 00000000..7ef04e2e --- /dev/null +++ b/.idea/copilot.data.migration.ask.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/copilot.data.migration.edit.xml b/.idea/copilot.data.migration.edit.xml new file mode 100644 index 00000000..8648f940 --- /dev/null +++ b/.idea/copilot.data.migration.edit.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/recommendations/RoofRecommendations.py b/recommendations/RoofRecommendations.py index 1d6fe06c..6625aeb0 100644 --- a/recommendations/RoofRecommendations.py +++ b/recommendations/RoofRecommendations.py @@ -170,7 +170,13 @@ class RoofRecommendations: @staticmethod def is_loft_insulation_appropriate( - non_invasive_recommendations, measures, is_pitched, is_at_rafters, rir_over_loft + measures: List, + is_pitched: bool, + is_at_rafters: bool, + rir_over_loft: bool, + is_assumed: bool, + has_loft_insulation_recommendation: bool, + has_sloping_ceiling_recommendation: bool ) -> bool: """ Determine if loft insulation is appropriate @@ -179,36 +185,59 @@ class RoofRecommendations: :param is_pitched: Boolean - indicates whether or not the roof is pitched :param is_at_rafters: Boolean - indicates whether or not the loft insulation is at rafters :param rir_over_loft: Boolean - indicates whether or not there we should be doing RIR insulation + :param is_assumed: Boolean - indicates whether or not the roof insulation status is assumed + :param has_loft_insulation_recommendation: Boolean - indicates whether or not there + is a loft insulation non-invasive recommendation + :param has_sloping_ceiling_recommendation: Boolean - indicates whether or not there + is a sloping ceiling non-invasive recommendation :return: """ has_li_in_measures = "loft_insulation" in measures - has_li_non_invasive_recommendation = any( - x["type"] == "loft_insulation" for x in non_invasive_recommendations - ) - return has_li_non_invasive_recommendation or ( + # Key business logic: + # If we have a pitched roof, no insulation, it's not assumed and we have a sloping ceiling recommendation, + # we do NOT recommend loft insulation + if is_pitched and not is_assumed and has_sloping_ceiling_recommendation: + return False + + return has_loft_insulation_recommendation or ( is_pitched and has_li_in_measures and not is_at_rafters ) and not rir_over_loft @staticmethod def is_flat_roof_insulation_appropriate( - is_flat: bool, measures: List, non_invasive_recommendations: List + is_flat: bool, measures: List, has_flat_roof_recommendation: bool ) -> bool: """ Determine if flat roof insulation is appropriate :param is_flat: Boolean - indicates whether or not the roof is flat :param measures: List - list of measures - :param non_invasive_recommendations: List - list of non-invasive recommendations - :return: + :param has_flat_roof_recommendation: Boolean - indicates whether or not there is a flat roof non-invasive + recommendation + :return: Boolean """ flat_roof_in_measures = "flat_roof_insulation" in measures - flat_roof_non_invasive_rec = has_li_non_invasive_recommendation = any( - x["type"] == "flat_roof_insulation" for x in non_invasive_recommendations - ) - return (is_flat and flat_roof_in_measures) or flat_roof_non_invasive_rec + return (is_flat and flat_roof_in_measures) or has_flat_roof_recommendation + + @staticmethod + def is_room_roof_insulation_appropriate( + is_room_roof, measures, rir_over_loft, has_room_roof_recommendation + ): + """ + Determine if room roof insulation is appropriate + :param is_room_roof: Boolean - indicates whether or not the roof is a room roof + :param measures: List - list of measures + :param rir_over_loft: Boolean - indicates whether or not there we should be doing RIR insulation + :param has_room_roof_recommendation: Boolean - indicates whether or not there is a room roof non-invasive + recommendation + :return: + """ + return is_room_roof and ("room_roof_insulation" in measures) or ( + has_room_roof_recommendation or rir_over_loft + ) def _does_roof_need_recommendation(self, measures: List | None = None, u_value: float | None = None): """ @@ -243,6 +272,28 @@ class RoofRecommendations: ): return False + @staticmethod + def _is_primary_roof_sloped( + is_pitched: bool, is_loft: bool, is_assumed: bool + ): + """ + Determine if the primary roof is sloped + :param is_pitched: bool - is the roof pitched + :param is_loft: bool - is the roof a loft + :param is_assumed: bool - is the roof insulation status assumed + :return: + """ + # Conditions for this to be true + # Case 1 + # In the property roof description (primary roof) + # 1) Pitched Roof + # 2) Uninsulated + # 3) Not assumed + if is_pitched and not is_loft and not is_assumed: + return True + + return False + def recommend(self, phase: int, measures: List | None = None, default_u_values: bool = False): """ Main method to recommend roof insulation measures @@ -290,14 +341,21 @@ class RoofRecommendations: is_loft = self.property.roof["is_loft"] is_assumed = self.property.roof["is_assumed"] is_at_rafters = self.property.roof["is_at_rafters"] + is_flat = self.property.roof["is_flat"] + is_room_roof = self.property.roof["is_roof_room"] + has_sloping_ceiling_recommendation = any( x["type"] == "sloping_ceiling_insulation" for x in non_invasive_recommendations ) - primary_roof_is_sloped = False # TODO + has_loft_insulation_recommendation = any(x["type"] == "loft_insulation" for x in non_invasive_recommendations) + has_flat_roof_recommendation = any(x["type"] == "flat_roof_insulation" for x in non_invasive_recommendations) + has_room_roof_recommendation = any(x["type"] == "room_roof_insulation" for x in non_invasive_recommendations) + primary_roof_is_sloped = self._is_primary_roof_sloped( + is_pitched=is_pitched, is_loft=is_loft, is_assumed=is_assumed + ) rir_over_loft = ( - is_pitched and - self.property.roof["insulation_thickness"] == "none" and + is_pitched and self.property.roof["insulation_thickness"] == "none" and "room_in_roof_insulation" in [x["type"] for x in non_invasive_recommendations] ) @@ -308,8 +366,19 @@ class RoofRecommendations: ) needs_loft_insulation = self.is_loft_insulation_appropriate( - non_invasive_recommendations=non_invasive_recommendations, measures=measures, - is_pitched=is_pitched, is_at_rafters=is_at_rafters, rir_over_loft=rir_over_loft + measures=measures, is_pitched=is_pitched, is_at_rafters=is_at_rafters, + rir_over_loft=rir_over_loft, has_loft_insulation_recommendation=has_loft_insulation_recommendation, + is_assumed=is_assumed, has_sloping_ceiling_recommendation=has_sloping_ceiling_recommendation + ) + + needs_flat_roof_insulation = self.is_flat_roof_insulation_appropriate( + is_flat=is_flat, measures=measures, has_flat_roof_recommendation=has_flat_roof_recommendation + ) + needs_rir_insulation = self.is_room_roof_insulation_appropriate( + is_room_roof=is_room_roof, + measures=measures, + rir_over_loft=rir_over_loft, + has_room_roof_recommendation=has_room_roof_recommendation ) ################################################## @@ -327,10 +396,7 @@ class RoofRecommendations: ) return - if ( - (self.property.roof["is_flat"] and "flat_roof_insulation" in measures) or - "flat_roof_insulation" in [x["type"] for x in non_invasive_recommendations] - ): + if needs_flat_roof_insulation: self.recommend_roof_insulation( u_value=u_value, insulation_thickness=0, @@ -343,10 +409,7 @@ class RoofRecommendations: # There are cases where the property might have a room roof as the second roof, but we have a recommendation for # it, so we allow this override - if self.property.roof["is_roof_room"] and ("room_roof_insulation" in measures) or ( - "room_roof_insulation" in [x["type"] for x in non_invasive_recommendations] or - rir_over_loft - ): + if needs_rir_insulation: self.recommend_room_roof_insulation(u_value, phase, default_u_values) return From 1bd7117097682faafda932d289fa24c32487a2cd Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Mon, 26 Jan 2026 14:27:52 +0000 Subject: [PATCH 37/68] final peabody element mappings --- backend/condition/domain/element.py | 23 ++- .../mapping/peabody/peabody_element_map.py | 140 +++++++++++++----- 2 files changed, 123 insertions(+), 40 deletions(-) diff --git a/backend/condition/domain/element.py b/backend/condition/domain/element.py index b146d09b..72687aed 100644 --- a/backend/condition/domain/element.py +++ b/backend/condition/domain/element.py @@ -18,6 +18,8 @@ class Element(str, Enum): CCU = "ccu" PASSENGER_LIFT = "passenger_lift" STAIRLIFT = "stairlift" + DISABLED_HOIST_TRACKING = "disabled_hoist_tracking" + DISABLED_FACILITIES = "disabled_facilities" # ====================== # EXTERNAL – ROOF @@ -47,6 +49,7 @@ class Element(str, Enum): CLADDING = "cladding" SPANDREL_PANELS = "spandrel_panels" GARAGE_WALLS = "garage_walls" + PARTY_WALL_FIRE_BREAK = "party_wall_fire_break" # ====================== # EXTERNAL – WINDOWS @@ -68,6 +71,7 @@ class Element(str, Enum): BLOCK_ENTRANCE_DOOR = "block_entrance_door" LINTEL = "lintel" PATIO_FRENCH_DOOR = "patio_french_door" + DOOR_ENTRY_HANDSET = "door_entry_handset" # ====================== # EXTERNAL – AREAS @@ -149,6 +153,7 @@ class Element(str, Enum): COMMUNAL_BIN_STORE = "communal_bin_store" COMMUNAL_BIN_STORE_DOORS = "communal_bin_store_doors" COMMUNAL_BIN_STORE_WALLS = "communal_bin_store_wall" + COMMUNAL_BIN_STORE_ROOF = "communal_bin_store_roof" COMMUNAL_REFUSE_CHUTE = "communal_refuse_chute" COMMUNAL_FLOOR_COVERING = "communal_floor_covering" COMMUNAL_KITCHEN = "communal_kitchen" @@ -158,7 +163,7 @@ class Element(str, Enum): COMMUNAL_LIFT = "communal_lift" COMMUNAL_PASSENGER_LIFT = "communal_passenger_lift" COMMUNAL_BALCONY_WALKWAY = "communal_balcony_walkway" - COMMUNAL_PRIMARY_ENTRANCE = "communal_primary_entrance" + COMMUNAL_ENTRANCE = "communal_entrance" COMMUNAL_INTERNAL_DECORATIONS = "communal_internal_decorations" COMMUNAL_INTERNAL_FLOOR = "communal_internal_floor" COMMUNAL_WALKWAYS = "communal_walkways" @@ -166,6 +171,22 @@ class Element(str, Enum): COMMUNAL_STAIRS = "communal_stairs" COMMUNAL_AERIAL = "communal_aerial" COMMUNAL_AOV = "communal_aov" + COMMUNAL_INTERNAL_DOORS = "communal_internal_doors" + COMMUNAL_LATERAL_MAINS = "communal_lateral_mains" + COMMUNAL_LIGHTING = "communal_lighting" + COMMUNAL_LIGHTING_CONDUCTOR = "communal_lighting_conductor" + COMMUNAL_STORE_ROOF = "communal_store_roof" + COMMUNAL_STORE_WALLS = "communal_store_walls" + COMMUNAL_STORE_DOORS = "communal_store_doors" + COMMUNAL_WARDEN_CALL_SYSTEM = "communal_warden_call_system" + COMMUNAL_BMS = "communal_bms" + COMMUNAL_BOOSTER_PUMP = "communal_booster_pump" + COMMUNAL_DRY_RISER = "communal_dry_riser" + COMMUNAL_WET_RISER = "communal_wet_riser" + COMMUNAL_COLD_WATER_STORAGE = "communal_cold_water_storage" + COMMUNAL_SPRINKLER = "communal_sprinkler" + COMMUNAL_PLUG_SOCKETS = "communal_plug_sockets" + COMMUNAL_CIRCULATION_SPACE = "communal_circulation_space" # ========================================================== # HHSRS – ALL 29 HAZARDS diff --git a/backend/condition/domain/mapping/peabody/peabody_element_map.py b/backend/condition/domain/mapping/peabody/peabody_element_map.py index 08e63568..7a266a9f 100644 --- a/backend/condition/domain/mapping/peabody/peabody_element_map.py +++ b/backend/condition/domain/mapping/peabody/peabody_element_map.py @@ -14,6 +14,9 @@ PEABODY_ELEMENT_MAP = { element=Element.CARBON_MONOXIDE_DETECTION, aspect_type=AspectType.TYPE ), (50, 3): ElementMapping(element=Element.CCU, aspect_type=AspectType.TYPE), + (50, 7): ElementMapping( + element=Element.DISABLED_HOIST_TRACKING, aspect_type=AspectType.PRESENCE + ), (50, 11): ElementMapping( element=Element.HEAT_DETECTION, aspect_type=AspectType.TYPE ), @@ -23,6 +26,9 @@ PEABODY_ELEMENT_MAP = { (50, 22): ElementMapping( element=Element.STAIRLIFT, aspect_type=AspectType.PRESENCE ), + (50, 26): ElementMapping( + element=Element.DISABLED_FACILITIES, aspect_type=AspectType.TYPE + ), (100, 3): ElementMapping(element=Element.PROPERTY, aspect_type=AspectType.AGE_BAND), (100, 14): ElementMapping( element=Element.PROPERTY, aspect_type=AspectType.CONSTRUCTION_TYPE @@ -36,6 +42,9 @@ PEABODY_ELEMENT_MAP = { # ========================================================== # EXTERNAL – WALLS # ========================================================== + (50, 16): ElementMapping( + element=Element.PARTY_WALL_FIRE_BREAK, aspect_type=AspectType.PRESENCE + ), (53, 1): ElementMapping( element=Element.BOUNDARY_WALLS, aspect_type=AspectType.PRESENCE ), @@ -76,6 +85,9 @@ PEABODY_ELEMENT_MAP = { # ========================================================== # EXTERNAL – ROOFS # ========================================================== + (50, 15): ElementMapping( + element=Element.LOFT_INSULATION, aspect_type=AspectType.TYPE + ), (53, 2): ElementMapping(element=Element.CHIMNEY, aspect_type=AspectType.PRESENCE), (53, 6): ElementMapping( element=Element.FASCIA_SOFFIT_BARGEBOARDS, aspect_type=AspectType.MATERIAL @@ -116,6 +128,9 @@ PEABODY_ELEMENT_MAP = { # ========================================================== # EXTERNAL – DOORS & WINDOWS # ========================================================== + (50, 8): ElementMapping( + element=Element.DOOR_ENTRY_HANDSET, aspect_type=AspectType.PRESENCE + ), (53, 8): ElementMapping( element=Element.FRONT_DOOR, aspect_type=AspectType.MATERIAL ), @@ -262,39 +277,121 @@ PEABODY_ELEMENT_MAP = { (51, 3): ElementMapping( element=Element.COMMUNAL_BALCONY_WALKWAY, aspect_type=AspectType.PRESENCE ), + (51, 4): ElementMapping( + element=Element.COMMUNAL_BATHROOM, aspect_type=AspectType.TYPE + ), (51, 5): ElementMapping( element=Element.COMMUNAL_BIN_STORE_DOORS, aspect_type=AspectType.PRESENCE ), + (51, 6): ElementMapping( + element=Element.COMMUNAL_BIN_STORE_ROOF, aspect_type=AspectType.PRESENCE + ), (51, 7): ElementMapping( element=Element.COMMUNAL_BIN_STORE_WALLS, aspect_type=AspectType.MATERIAL ), + (51, 8): ElementMapping( + element=Element.COMMUNAL_BMS, aspect_type=AspectType.PRESENCE + ), + (51, 9): ElementMapping( + element=Element.COMMUNAL_BOILER, aspect_type=AspectType.TYPE + ), + (51, 10): ElementMapping( + element=Element.COMMUNAL_BOOSTER_PUMP, aspect_type=AspectType.PRESENCE + ), + (51, 11): ElementMapping( + element=Element.COMMUNAL_CCTV, aspect_type=AspectType.PRESENCE + ), + (51, 12): ElementMapping( + element=Element.COMMUNAL_CIRCULATION_SPACE, aspect_type=AspectType.ADEQUACY + ), + (51, 13): ElementMapping( + element=Element.COMMUNAL_COLD_WATER_STORAGE, aspect_type=AspectType.PRESENCE + ), (51, 14): ElementMapping( element=Element.COMMUNAL_DOOR_ENTRY, aspect_type=AspectType.SYSTEM ), + (51, 15): ElementMapping( + element=Element.COMMUNAL_DRY_RISER, aspect_type=AspectType.PRESENCE + ), + (51, 16): ElementMapping( + element=Element.COMMUNAL_EMERGENCY_LIGHTING, aspect_type=AspectType.PRESENCE + ), (51, 17): ElementMapping( element=Element.COMMUNAL_EXTERNAL_DOORS, aspect_type=AspectType.MATERIAL ), + (51, 19): ElementMapping( + element=Element.COMMUNAL_FIRE_ALARM, aspect_type=AspectType.TYPE + ), (51, 20): ElementMapping( element=Element.COMMUNAL_INTERNAL_DECORATIONS, aspect_type=AspectType.PRESENCE ), + (51, 21): ElementMapping( + element=Element.COMMUNAL_INTERNAL_DOORS, aspect_type=AspectType.MATERIAL + ), (51, 22): ElementMapping( element=Element.COMMUNAL_INTERNAL_FLOOR, aspect_type=AspectType.FINISH ), + (51, 23): ElementMapping( + element=Element.COMMUNAL_KITCHEN, aspect_type=AspectType.TYPE + ), + (51, 24): ElementMapping( + element=Element.COMMUNAL_LATERAL_MAINS, aspect_type=AspectType.PRESENCE + ), + (51, 25): ElementMapping( + element=Element.COMMUNAL_LIGHTING, aspect_type=AspectType.PRESENCE + ), + (51, 26): ElementMapping( + element=Element.COMMUNAL_LIGHTING_CONDUCTOR, aspect_type=AspectType.PRESENCE + ), (51, 27): ElementMapping( element=Element.COMMUNAL_PASSENGER_LIFT, aspect_type=AspectType.TYPE ), (51, 28): ElementMapping( - element=Element.COMMUNAL_PRIMARY_ENTRANCE, aspect_type=AspectType.MATERIAL + element=Element.COMMUNAL_ENTRANCE, + aspect_type=AspectType.MATERIAL, + element_instance=1, + ), + (51, 30): ElementMapping( + element=Element.COMMUNAL_ENTRANCE, + aspect_type=AspectType.FINISH, + element_instance=2, + ), + (51, 14): ElementMapping( + element=Element.COMMUNAL_SPRINKLER, aspect_type=AspectType.PRESENCE + ), + (51, 29): ElementMapping( + element=Element.COMMUNAL_REFUSE_CHUTE, aspect_type=AspectType.PRESENCE ), (51, 32): ElementMapping( element=Element.COMMUNAL_STAIRS, aspect_type=AspectType.FINISH ), + (51, 33): ElementMapping( + element=Element.COMMUNAL_STORE_DOORS, aspect_type=AspectType.MATERIAL + ), + (51, 34): ElementMapping( + element=Element.COMMUNAL_STORE_ROOF, aspect_type=AspectType.MATERIAL + ), + (51, 35): ElementMapping( + element=Element.COMMUNAL_STORE_WALLS, aspect_type=AspectType.MATERIAL + ), (51, 36): ElementMapping( element=Element.COMMUNAL_WALKWAYS, aspect_type=AspectType.FINISH ), + (51, 37): ElementMapping( + element=Element.COMMUNAL_WARDEN_CALL_SYSTEM, aspect_type=AspectType.PRESENCE + ), + (51, 38): ElementMapping( + element=Element.COMMUNAL_TOILETS, aspect_type=AspectType.TYPE + ), + (51, 39): ElementMapping( + element=Element.COMMUNAL_WET_RISER, aspect_type=AspectType.PRESENCE + ), + (51, 40): ElementMapping( + element=Element.COMMUNAL_PLUG_SOCKETS, aspect_type=AspectType.PRESENCE + ), (200, 1): ElementMapping( element=Element.COMMUNAL_BOILER, aspect_type=AspectType.TYPE - ), + ), # Duplicate of (51, 9) - correct? (200, 2): ElementMapping( element=Element.COMMUNAL_HEATING, aspect_type=AspectType.TYPE ), @@ -315,10 +412,10 @@ PEABODY_ELEMENT_MAP = { ), (200, 8): ElementMapping( element=Element.COMMUNAL_BATHROOM, aspect_type=AspectType.TYPE - ), + ), # Duplicate of (51, 4) - correct? (200, 9): ElementMapping( element=Element.COMMUNAL_TOILETS, aspect_type=AspectType.TYPE - ), + ), # Duplicate of (51, 38) - correct? (200, 10): ElementMapping( element=Element.COMMUNAL_GATES, aspect_type=AspectType.TYPE ), @@ -392,38 +489,3 @@ PEABODY_ELEMENT_MAP = { element=Element.HHSRS_STRUCTURAL_COLLAPSE, aspect_type=AspectType.RISK ), } - - -# unhandled -# 'Element: Communal - Code: 51, Sub-Element: Common Door Entry System - Code: 14', -# 'Element: Communal - Code: 51, Sub-Element: Common Fire Alarm - Code: 19', -# 'Element: Communal - Code: 51, Sub-Element: Common Internal Doors - Code: 21', -# 'Element: Communal - Code: 51, Sub-Element: Common Emergency Lighting - Code: 16', -# 'Element: Communal - Code: 51, Sub-Element: Common Lateral Mains - Code: 24', -# 'Element: Communal - Code: 51, Sub-Element: Common Lighting - Code: 25', -# 'Element: Communal - Code: 51, Sub-Element: Common Store Roof - Code: 34', -# 'Element: Communal - Code: 51, Sub-Element: Common Store Walls - Code: 35', -# 'Element: Communal - Code: 51, Sub-Element: Common CCTV - Code: 11', -# 'Element: Communal - Code: 51, Sub-Element: Common Kitchen - Code: 23', -# 'Element: Communal - Code: 51, Sub-Element: Common Secondary Entrance Material - Code: 30', -# 'Element: Communal - Code: 51, Sub-Element: Common Warden Call System - Code: 37', -# 'Element: Communal - Code: 51, Sub-Element: Common Boiler - Code: 9', -# 'Element: Communal - Code: 51, Sub-Element: Common Store Doors - Code: 33', -# 'Element: Communal - Code: 51, Sub-Element: Common BMS - Code: 8', -# 'Element: Communal - Code: 51, Sub-Element: Common Booster Pump - Code: 10', -# 'Element: Communal - Code: 51, Sub-Element: Common Dry Riser - Code: 15', -# 'Element: Communal - Code: 51, Sub-Element: Common Lightning Conductor - Code: 26', -# 'Element: Communal - Code: 51, Sub-Element: Common Bin Store Roof - Code: 6', -# 'Element: Communal - Code: 51, Sub-Element: Common Bathroom - Code: 4', -# 'Element: Communal - Code: 51, Sub-Element: Common WC - Code: 38', -# 'Element: Communal - Code: 51, Sub-Element: Common Cold Water Storage Tank - Code: 13', -# 'Element: Communal - Code: 51, Sub-Element: Common Sprinker - Code: 31', -# 'Element: Communal - Code: 51, Sub-Element: Communal Plug Sockets - Code: 40', -# 'Element: Communal - Code: 51, Sub-Element: Common Wet Riser - Code: 39', -# 'Element: Communal - Code: 51, Sub-Element: Common Refuse Chute - Code: 29', -# 'Element: Internal - Code: 50, Sub-Element: Party Wall Fire Break - Code: 16', -# 'Element: Communal - Code: 51, Sub-Element: Common Cirulation Space - Code: 12', -# 'Element: Internal - Code: 50, Sub-Element: Door Entry Handset - Code: 8', -# 'Element: Internal - Code: 50, Sub-Element: Loft Insulation - Code: 15', -# 'Element: Internal - Code: 50, Sub-Element: Disabled Hoist Tracking - Code: 7', -# 'Element: Internal - Code: 50, Sub-Element: Disabled Facilities - Code: 26' From eaf793011b43b4f0655a45d8526401a39bf0f114 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Mon, 26 Jan 2026 16:20:21 +0000 Subject: [PATCH 38/68] remaining lbwf mappings --- backend/condition/domain/element.py | 38 +- .../domain/mapping/lbwf/lbwf_element_map.py | 275 ++++++++--- .../mapping/peabody/peabody_element_map.py | 429 ++++++++++++------ 3 files changed, 548 insertions(+), 194 deletions(-) diff --git a/backend/condition/domain/element.py b/backend/condition/domain/element.py index 72687aed..f78f2d52 100644 --- a/backend/condition/domain/element.py +++ b/backend/condition/domain/element.py @@ -11,6 +11,7 @@ class Element(str, Enum): PROPERTY_CLASSIFICATION = "property_classification" PROPERTY_AGE_BAND = "property_age_band" STOREY_COUNT = "storey_count" + FLOOR_LEVEL = "floor_level" FLOOR_LEVEL_FRONT_DOOR = "floor_level_front_door" ACCESSIBLE_HOUSING_REGISTER = "accessible_housing_register" ASBESTOS = "asbestos" @@ -20,6 +21,7 @@ class Element(str, Enum): STAIRLIFT = "stairlift" DISABLED_HOIST_TRACKING = "disabled_hoist_tracking" DISABLED_FACILITIES = "disabled_facilities" + STEPS_TO_FRONT_DOOR = "steps_to_front_door" # ====================== # EXTERNAL – ROOF @@ -35,7 +37,9 @@ class Element(str, Enum): SOFFIT = "soffit" FASCIA_SOFFIT_BARGEBOARDS = "fascia_soffit_bargeboards" GUTTERS = "gutters" + STORE_ROOF = "store_roof" GARAGE_ROOF = "garage_roof" + GARAGE_AND_STORE_ROOF = "garage_and_store_roof" # ====================== # EXTERNAL – WALLS @@ -50,6 +54,8 @@ class Element(str, Enum): SPANDREL_PANELS = "spandrel_panels" GARAGE_WALLS = "garage_walls" PARTY_WALL_FIRE_BREAK = "party_wall_fire_break" + EXTERNAL_BRICKWORK_POINTING = "external_brickwork_pointing" + INTERNAL_DOWNPIPES_EXTERNAL_AREA = "internal_downpipes_in_external_area" # ====================== # EXTERNAL – WINDOWS @@ -57,6 +63,9 @@ class Element(str, Enum): EXTERNAL_WINDOWS = "external_windows" COMMUNAL_WINDOWS = "communal_windows" SECONDARY_GLAZING = "secondary_glazing" + STORE_WINDOWS = "store_windows" + GARAGE_WINDOWS = "garage_windows" + GARAGE_AND_STORE_WINDOWS = "garage_and_store_windows" # ====================== # EXTERNAL – DOORS @@ -66,6 +75,7 @@ class Element(str, Enum): REAR_DOOR = "rear_door" STORE_DOOR = "store_door" GARAGE_DOOR = "garage_door" + GARAGE_AND_STORE_DOOR = "garage_and_store_door" COMMUNAL_ENTRANCE_DOOR = "communal_entrance_door" MAIN_DOOR = "main_door" BLOCK_ENTRANCE_DOOR = "block_entrance_door" @@ -94,6 +104,10 @@ class Element(str, Enum): ROADS = "roads" SOIL_AND_VENT = "soil_and_vent" SOLAR_THERMALS = "solar_thermals" + DROP_KERB = "drop_kerb" + OUTBUILDING_OVERHAUL = "outbuilding_overhaul" + EXTERNAL_STRUCTURAL_DEFECTS = "external_structural_defects" + ACCESS_RAMP = "access_ramp" # ====================== # INTERNAL – KITCHEN @@ -110,6 +124,9 @@ class Element(str, Enum): SECONDARY_BATHROOM = "secondary_bathroom" SECONDARY_TOILET = "secondary_toilet" BATHROOM_EXTRACTOR_FAN = "bathroom_extractor_fan" + ADDITIONAL_WC_OR_WHB = "additional_wc_or_whb" + BATHROOM_REMAINING_LIFE_SOURCE = "bathroom_remaining_life_source" + KITCHEN_REMAINING_LIFE_SOURCE = "kitchen_remaining_life_source" # ====================== # INTERNAL – HEATING / WATER @@ -120,11 +137,16 @@ class Element(str, Enum): SECONDARY_HEATING = "secondary_heating" HOT_WATER_SYSTEM = "hot_water_system" COLD_WATER_STORAGE = "cold_water_storage" - PROGRAMMABLE_HEATING = "programmable_heating" HEATING_SYSTEM = "heating_system" BOILER_FUEL = "boiler_fuel" WATER_HEATING = "water_heating" PROGRAMMABLE_HEATING = "programmable_heating" + COMMUNITY_HEATING = ( + "community_heating" # Is this definitely different from COMMUNAL_HEATING? + ) + GAS_AVAILABLE = "gas_available" + HEAT_RECOVERY_UNITS = "heat_recovery_units" + HEATING_IMPROVEMENTS = "heating_improvements" # ====================== # INTERNAL – ELECTRICS / FIRE @@ -139,6 +161,7 @@ class Element(str, Enum): INTERNAL_WIRING = ( "internal_wiring" # Is this definitely different from ELECTRICAL_WIRING? ) + ELECTRICS = "electrics" # ====================== # COMMUNAL @@ -188,6 +211,19 @@ class Element(str, Enum): COMMUNAL_PLUG_SOCKETS = "communal_plug_sockets" COMMUNAL_CIRCULATION_SPACE = "communal_circulation_space" + # ====================== + # FITNESS FOR HUMAN HABITATION + # ====================== + FFHH_DAMP = "ffhh_damp" + FFHH_HOT_AND_COLD_WATER = "ffhh_hold_and_cold_water" + FFHH_DRAINAGE_LAVATORIES = "ffhh_drainage_or_lavatories" + FFHH_NEGLECTED = "ffhh_neglected_and_in_bad_condition" + FFHH_NATURAL_LIGHT = "ffhh_natural_light" + FFHH_VENTILATION = "ffhh_ventilation" + FFHH_FOOD_PREP_AND_WASHUP = "ffhh_prepare_and_cook_food_or_wash_up" + FFHH_UNSAFE_LAYOUT = "ffhh_unsafe_layout" + FFHH_UNSTABLE_BUILDING = "ffhh_unstable_building" + # ========================================================== # HHSRS – ALL 29 HAZARDS # ========================================================== diff --git a/backend/condition/domain/mapping/lbwf/lbwf_element_map.py b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py index be8a50b2..02722b11 100644 --- a/backend/condition/domain/mapping/lbwf/lbwf_element_map.py +++ b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py @@ -31,6 +31,18 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { element=Element.FLOOR_LEVEL_FRONT_DOOR, aspect_type=AspectType.LOCATION, ), + "INTFLRLVL": ElementMapping( + element=Element.FLOOR_LEVEL, + aspect_type=AspectType.LOCATION, + ), + "INTNSEINSL": ElementMapping( + element=Element.EXTERNAL_NOISE_INSULATION, # Maybe this shouldn't be "EXTERNAL_" + aspect_type=AspectType.ADEQUACY, + ), + "INTSTEPSFD": ElementMapping( + element=Element.STEPS_TO_FRONT_DOOR, + aspect_type=AspectType.QUANTITY, + ), # ========================================================== # ASBESTOS (NON-HHSRS RECORD) # ========================================================== @@ -57,21 +69,22 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { element=Element.KITCHEN, aspect_type=AspectType.LOCATION, ), - # ========================================================== - # INTERNAL – HEATING - # ========================================================== - "INTCHEXTNT": ElementMapping( - element=Element.CENTRAL_HEATING, - aspect_type=AspectType.EXTENT, + "INTADDWCW": ElementMapping( + element=Element.ADDITIONAL_WC_OR_WHB, + aspect_type=AspectType.PRESENCE, ), - "INTCHDIST": ElementMapping( - element=Element.HEATING_DISTRIBUTION, + "INTBTHREML": ElementMapping( + element=Element.BATHROOM_REMAINING_LIFE_SOURCE, aspect_type=AspectType.TYPE, ), - "INTCHBLR": ElementMapping( - element=Element.HEATING_BOILER, + "INTKITREML": ElementMapping( + element=Element.KITCHEN_REMAINING_LIFE_SOURCE, aspect_type=AspectType.TYPE, ), + "INTTNTINST": ElementMapping( + element=Element.TENANT_INSTALLED_KITCHEN, + aspect_type=AspectType.TYPE, # Not certain about this aspect type - need more data + ), # ========================================================== # INTERNAL – FIRE # ========================================================== @@ -98,6 +111,18 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { # ========================================================== # HEATING & SERVICES # ========================================================== + "INTCHEXTNT": ElementMapping( + element=Element.CENTRAL_HEATING, + aspect_type=AspectType.EXTENT, + ), + "INTCHDIST": ElementMapping( + element=Element.HEATING_DISTRIBUTION, + aspect_type=AspectType.TYPE, + ), + "INTCHBLR": ElementMapping( + element=Element.HEATING_BOILER, + aspect_type=AspectType.TYPE, + ), "INTBOILERF": ElementMapping( element=Element.BOILER_FUEL, aspect_type=AspectType.TYPE, @@ -110,6 +135,30 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { element=Element.WATER_HEATING, aspect_type=AspectType.TYPE, ), + "INTCOMHTG": ElementMapping( + element=Element.COMMUNITY_HEATING, + aspect_type=AspectType.TYPE, + ), + "INTELECTRC": ElementMapping( + element=Element.ELECTRICS, + aspect_type=AspectType.WORK_REQUIRED, # Not certain about this aspect type - need more data + ), + "INTGASAVAI": ElementMapping( + element=Element.GAS_AVAILABLE, + aspect_type=AspectType.PRESENCE, # Maybe should be AspectType.TYPE ? + ), + "INTHEATREC": ElementMapping( + element=Element.HEAT_RECOVERY_UNITS, + aspect_type=AspectType.PRESENCE, + ), + "INTHTIMP": ElementMapping( + element=Element.GAS_AVAILABLE, + aspect_type=AspectType.WORK_REQUIRED, + ), + "INTPROGHTG": ElementMapping( + element=Element.PROGRAMMABLE_HEATING, + aspect_type=AspectType.TYPE, # Should maybe be PRESENCE, but set to TYPE for consistency with Peabody data + ), # ========================================================== # EXTERNAL – WALLS (INSTANCED) # ========================================================== @@ -136,6 +185,14 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { element=Element.EXTERNAL_WALL, aspect_type=AspectType.CONDITION, ), + "EXTDWNPTYP": ElementMapping( + element=Element.DOWNPIPES, + aspect_type=AspectType.MATERIAL, + ), + "EXTGUTRTYP": ElementMapping( + element=Element.GUTTERS, + aspect_type=AspectType.MATERIAL, + ), # ========================================================== # EXTERNAL – ROOFS (INSTANCED) # ========================================================== @@ -169,6 +226,30 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { aspect_type=AspectType.COVERING, element_instance=3, ), + "EXTCHIMNEY": ElementMapping( + element=Element.CHIMNEY, + aspect_type=AspectType.WORK_REQUIRED, + ), + "EXTFASOFBR": ElementMapping( + element=Element.FASCIA_SOFFIT_BARGEBOARDS, + aspect_type=AspectType.MATERIAL, + ), + "EXTGARROOF": ElementMapping( + element=Element.GARAGE_ROOF, + aspect_type=AspectType.MATERIAL, + ), + "EXTGARSTRF": ElementMapping( + element=Element.GARAGE_AND_STORE_ROOF, + aspect_type=AspectType.MATERIAL, + ), + "EXTSTRROOF": ElementMapping( + element=Element.STORE_ROOF, + aspect_type=AspectType.MATERIAL, + ), + "INTLOFTINS": ElementMapping( + element=Element.LOFT_INSULATION, + aspect_type=AspectType.TYPE, + ), # ========================================================== # EXTERNAL – DOORS & WINDOWS # ========================================================== @@ -204,6 +285,125 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { aspect_type=AspectType.TYPE, element_instance=2, ), + "EXTGARDOOR": ElementMapping( + element=Element.GARAGE_DOOR, + aspect_type=AspectType.MATERIAL, + ), + "EXTGARSTDR": ElementMapping( + element=Element.GARAGE_AND_STORE_DOOR, + aspect_type=AspectType.MATERIAL, + ), + "EXTSTRDOOR": ElementMapping( + element=Element.STORE_DOOR, + aspect_type=AspectType.MATERIAL, + ), + "EXTGARWDWS": ElementMapping( + element=Element.GARAGE_WINDOWS, + aspect_type=AspectType.MATERIAL, + ), + "EXTSTRWDWS": ElementMapping( + element=Element.STORE_WINDOWS, + aspect_type=AspectType.MATERIAL, + ), + "EXTGARSTWD": ElementMapping( + element=Element.GARAGE_AND_STORE_WINDOWS, + aspect_type=AspectType.MATERIAL, + ), + "EXTLINTELS": ElementMapping( + element=Element.LINTEL, + aspect_type=AspectType.PRESENCE, + ), + "EXTPTFRDR1": ElementMapping( + element=Element.PATIO_FRENCH_DOOR, + aspect_type=AspectType.MATERIAL, + element_instance=1, + ), + # ========================================================== + # EXTERNAL AREAS + # ========================================================== + "EXTBALCONY": ElementMapping( + element=Element.PRIVATE_BALCONY, + aspect_type=AspectType.PRESENCE, + ), + "EXTBPOINTG": ElementMapping( + element=Element.EXTERNAL_BRICKWORK_POINTING, + aspect_type=AspectType.PRESENCE, + ), + "EXTDRPKERB": ElementMapping( + element=Element.DROP_KERB, + aspect_type=AspectType.PRESENCE, + ), + "EXTEXTDECS": ElementMapping( + element=Element.EXTERNAL_DECORATION, + aspect_type=AspectType.PRESENCE, + ), + "EXTHARDSTD": ElementMapping( + element=Element.PATHS_AND_HARDSTANDINGS, + aspect_type=AspectType.MATERIAL, + ), + "EXTINTDWNP": ElementMapping( + element=Element.INTERNAL_DOWNPIPES_EXTERNAL_AREA, + aspect_type=AspectType.MATERIAL, + ), + "EXTOUTBOH": ElementMapping( + element=Element.OUTBUILDING_OVERHAUL, + aspect_type=AspectType.TYPE, + ), + "EXTPARKING": ElementMapping( + element=Element.PARKING_AREAS, + aspect_type=AspectType.PRESENCE, + ), + "EXTPCHCNPY": ElementMapping( + element=Element.PORCH_CANOPY, + aspect_type=AspectType.TYPE, + ), + "EXTSTRINSP": ElementMapping( + element=Element.EXTERNAL_STRUCTURAL_DEFECTS, + aspect_type=AspectType.TYPE, # Need more sample data to know whether this is the correct aspect type + ), + "INTACCRAMP": ElementMapping( + element=Element.ACCESS_RAMP, + aspect_type=AspectType.TYPE, # # Need more sample data to know whether this is the correct aspect type + ), + # ====================== + # FITNESS FOR HUMAN HABITATION + # ====================== + "FFHHDAMP": ElementMapping( + element=Element.FFHH_DAMP, + aspect_type=AspectType.RISK, + ), + "FFHHHCWAT": ElementMapping( + element=Element.FFHH_HOT_AND_COLD_WATER, + aspect_type=AspectType.RISK, + ), + "FFHHDRNWC": ElementMapping( + element=Element.FFHH_DRAINAGE_LAVATORIES, + aspect_type=AspectType.RISK, + ), + "FFHHNEGLC": ElementMapping( + element=Element.FFHH_NEGLECTED, + aspect_type=AspectType.RISK, + ), + "FFHHNONAT": ElementMapping( + element=Element.FFHH_NATURAL_LIGHT, + aspect_type=AspectType.RISK, + ), + "FFHHNOVEN": ElementMapping( + element=Element.FFHH_VENTILATION, + aspect_type=AspectType.RISK, + ), + "FFHHPRPCK": ElementMapping( + element=Element.FFHH_FOOD_PREP_AND_WASHUP, + aspect_type=AspectType.RISK, + ), + "FFHHUNLAY": ElementMapping( + element=Element.FFHH_UNSAFE_LAYOUT, + aspect_type=AspectType.RISK, + ), + "FFHHUNSTA": ElementMapping( + element=Element.FFHH_UNSTABLE_BUILDING, + aspect_type=AspectType.RISK, + ), # ========================================================== # HHSRS # ========================================================== @@ -244,7 +444,8 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { aspect_type=AspectType.RISK, ), "HHSRSORGAN": ElementMapping( - element=Element.HHSRS_VOLATILE_ORGANIC_COMPOUNDS, aspect_type=AspectType.RISK + element=Element.HHSRS_VOLATILE_ORGANIC_COMPOUNDS, + aspect_type=AspectType.RISK, ), "HHSRSCROWD": ElementMapping( element=Element.HHSRS_CROWDING_AND_SPACE, @@ -319,7 +520,8 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { aspect_type=AspectType.RISK, ), "HHSRSCLOW": ElementMapping( - element=Element.HHSRS_COLLISION_AND_ENTRAPMENT, aspect_type=AspectType.RISK + element=Element.HHSRS_COLLISION_AND_ENTRAPMENT, + aspect_type=AspectType.RISK, ), "HHSRSPOSI": ElementMapping( element=Element.HHSRS_AMENITIES, @@ -330,52 +532,3 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { # Unhandled: # DECNTHMINC # EICINSFREQ -# EXTBALCONY -# EXTBPOINTG -# EXTCHIMNEY -# EXTDRPKERB -# EXTDWNPTYP -# EXTEXTDECS -# EXTFASOFBR -# EXTGARDOOR -# EXTGARROOF -# EXTGARSTDR -# EXTGARSTRF -# EXTGARSTWD -# EXTGARWDWS -# EXTGUTRTYP -# EXTHARDSTD -# EXTINTDWNP -# EXTLINTELS -# EXTOUTBOH -# EXTPARKING -# EXTPCHCNPY -# EXTPTFRDR1 -# EXTSTRDOOR -# EXTSTRINSP -# EXTSTRROOF -# EXTSTRWDWS -# FFHHDAMP -# FFHHDRNWC -# FFHHHCWAT -# FFHHNEGLC -# FFHHNONAT -# FFHHNOVEN -# FFHHPRPCK -# FFHHUNLAY -# FFHHUNSTA -# INTACCRAMP -# INTADDWCW -# INTBTHREML -# INTCOMHTG -# INTELECTRC -# INTFLRLVL -# INTGASAVAI -# INTHEATREC -# INTHTIMP -# INTKITREML -# INTLOFTINS -# INTNSEINSL -# INTPROGHTG -# INTSTEPSFD -# INTTNTINST diff --git a/backend/condition/domain/mapping/peabody/peabody_element_map.py b/backend/condition/domain/mapping/peabody/peabody_element_map.py index 7a266a9f..8fe2ccb9 100644 --- a/backend/condition/domain/mapping/peabody/peabody_element_map.py +++ b/backend/condition/domain/mapping/peabody/peabody_element_map.py @@ -86,69 +86,109 @@ PEABODY_ELEMENT_MAP = { # EXTERNAL – ROOFS # ========================================================== (50, 15): ElementMapping( - element=Element.LOFT_INSULATION, aspect_type=AspectType.TYPE + element=Element.LOFT_INSULATION, + aspect_type=AspectType.TYPE, + ), + (53, 2): ElementMapping( + element=Element.CHIMNEY, + aspect_type=AspectType.PRESENCE, ), - (53, 2): ElementMapping(element=Element.CHIMNEY, aspect_type=AspectType.PRESENCE), (53, 6): ElementMapping( - element=Element.FASCIA_SOFFIT_BARGEBOARDS, aspect_type=AspectType.MATERIAL + element=Element.FASCIA_SOFFIT_BARGEBOARDS, + aspect_type=AspectType.MATERIAL, ), (53, 7): ElementMapping( - element=Element.FLAT_ROOF_COVERING, aspect_type=AspectType.MATERIAL + element=Element.FLAT_ROOF_COVERING, + aspect_type=AspectType.MATERIAL, ), (53, 13): ElementMapping( - element=Element.GARAGE_ROOF, aspect_type=AspectType.MATERIAL + element=Element.GARAGE_ROOF, + aspect_type=AspectType.MATERIAL, + ), + (53, 15): ElementMapping( + element=Element.GUTTERS, + aspect_type=AspectType.MATERIAL, ), - (53, 15): ElementMapping(element=Element.GUTTERS, aspect_type=AspectType.MATERIAL), (53, 18): ElementMapping( - element=Element.PITCHED_ROOF_COVERING, aspect_type=AspectType.MATERIAL + element=Element.PITCHED_ROOF_COVERING, + aspect_type=AspectType.MATERIAL, + ), + (53, 22): ElementMapping( + element=Element.PORCH_CANOPY, + aspect_type=AspectType.TYPE, + ), + (53, 47): ElementMapping( + element=Element.ROOF, + aspect_type=AspectType.STRUCTURE, ), - (53, 22): ElementMapping(element=Element.PORCH_CANOPY, aspect_type=AspectType.TYPE), - (53, 47): ElementMapping(element=Element.ROOF, aspect_type=AspectType.STRUCTURE), (110, 1): ElementMapping( - element=Element.ROOF, aspect_type=AspectType.MATERIAL, element_instance=1 + element=Element.ROOF, + aspect_type=AspectType.MATERIAL, + element_instance=1, ), (110, 2): ElementMapping( - element=Element.ROOF, aspect_type=AspectType.MATERIAL, element_instance=1 + element=Element.ROOF, + aspect_type=AspectType.MATERIAL, + element_instance=1, ), (110, 3): ElementMapping( - element=Element.CHIMNEY, aspect_type=AspectType.WORK_REQUIRED + element=Element.CHIMNEY, + aspect_type=AspectType.WORK_REQUIRED, + ), + (110, 4): ElementMapping( + element=Element.FASCIA, + aspect_type=AspectType.MATERIAL, + ), + (110, 5): ElementMapping( + element=Element.SOFFIT, + aspect_type=AspectType.MATERIAL, ), - (110, 4): ElementMapping(element=Element.FASCIA, aspect_type=AspectType.MATERIAL), - (110, 5): ElementMapping(element=Element.SOFFIT, aspect_type=AspectType.MATERIAL), (110, 6): ElementMapping( - element=Element.RAINWATER_GOODS, aspect_type=AspectType.MATERIAL + element=Element.RAINWATER_GOODS, + aspect_type=AspectType.MATERIAL, ), (110, 7): ElementMapping( element=Element.LOFT_INSULATION, aspect_type=AspectType.WORK_REQUIRED, # possibly not the right aspect type ), (110, 8): ElementMapping( - element=Element.PORCH_CANOPY, aspect_type=AspectType.MATERIAL + element=Element.PORCH_CANOPY, + aspect_type=AspectType.MATERIAL, ), # ========================================================== # EXTERNAL – DOORS & WINDOWS # ========================================================== (50, 8): ElementMapping( - element=Element.DOOR_ENTRY_HANDSET, aspect_type=AspectType.PRESENCE + element=Element.DOOR_ENTRY_HANDSET, + aspect_type=AspectType.PRESENCE, ), (53, 8): ElementMapping( - element=Element.FRONT_DOOR, aspect_type=AspectType.MATERIAL + element=Element.FRONT_DOOR, + aspect_type=AspectType.MATERIAL, ), (53, 12): ElementMapping( - element=Element.GARAGE_DOOR, aspect_type=AspectType.MATERIAL + element=Element.GARAGE_DOOR, + aspect_type=AspectType.MATERIAL, + ), + (53, 16): ElementMapping( + element=Element.LINTEL, + aspect_type=AspectType.PRESENCE, ), - (53, 16): ElementMapping(element=Element.LINTEL, aspect_type=AspectType.PRESENCE), (53, 19): ElementMapping( - element=Element.PATIO_FRENCH_DOOR, aspect_type=AspectType.MATERIAL + element=Element.PATIO_FRENCH_DOOR, + aspect_type=AspectType.MATERIAL, ), (53, 25): ElementMapping( - element=Element.REAR_DOOR, aspect_type=AspectType.MATERIAL + element=Element.REAR_DOOR, + aspect_type=AspectType.MATERIAL, ), (53, 29): ElementMapping( - element=Element.SECONDARY_GLAZING, aspect_type=AspectType.PRESENCE + element=Element.SECONDARY_GLAZING, + aspect_type=AspectType.PRESENCE, ), (53, 35): ElementMapping( - element=Element.STORE_DOOR, aspect_type=AspectType.MATERIAL + element=Element.STORE_DOOR, + aspect_type=AspectType.MATERIAL, ), (53, 38): ElementMapping( element=Element.EXTERNAL_WINDOWS, @@ -160,191 +200,275 @@ PEABODY_ELEMENT_MAP = { aspect_type=AspectType.TYPE, element_instance=2, ), - (53, 43): ElementMapping(element=Element.FRONT_DOOR, aspect_type=AspectType.TYPE), + (53, 43): ElementMapping( + element=Element.FRONT_DOOR, + aspect_type=AspectType.TYPE, + ), (130, 1): ElementMapping( - element=Element.EXTERNAL_WINDOWS, aspect_type=AspectType.MATERIAL + element=Element.EXTERNAL_WINDOWS, + aspect_type=AspectType.MATERIAL, ), (130, 2): ElementMapping( - element=Element.COMMUNAL_WINDOWS, aspect_type=AspectType.MATERIAL + element=Element.COMMUNAL_WINDOWS, + aspect_type=AspectType.MATERIAL, ), (140, 1): ElementMapping( - element=Element.MAIN_DOOR, aspect_type=AspectType.MATERIAL + element=Element.MAIN_DOOR, + aspect_type=AspectType.MATERIAL, ), (140, 2): ElementMapping( - element=Element.STORE_DOOR, aspect_type=AspectType.MATERIAL + element=Element.STORE_DOOR, + aspect_type=AspectType.MATERIAL, ), # Duplicate of (53, 35) (140, 3): ElementMapping( - element=Element.GARAGE_DOOR, aspect_type=AspectType.MATERIAL + element=Element.GARAGE_DOOR, + aspect_type=AspectType.MATERIAL, ), # Duplicate of (53, 12) (140, 4): ElementMapping( - element=Element.BLOCK_ENTRANCE_DOOR, aspect_type=AspectType.MATERIAL + element=Element.BLOCK_ENTRANCE_DOOR, + aspect_type=AspectType.MATERIAL, ), # ========================================================== # EXTERNAL AREAS # ========================================================== - (53, 3): ElementMapping(element=Element.DOWNPIPES, aspect_type=AspectType.MATERIAL), - (53, 9): ElementMapping( - element=Element.FRONT_FENCING, aspect_type=AspectType.MATERIAL + (53, 3): ElementMapping( + element=Element.DOWNPIPES, + aspect_type=AspectType.MATERIAL, + ), + (53, 9): ElementMapping( + element=Element.FRONT_FENCING, + aspect_type=AspectType.MATERIAL, + ), + (53, 10): ElementMapping( + element=Element.FRONT_GATE, + aspect_type=AspectType.TYPE, ), - (53, 10): ElementMapping(element=Element.FRONT_GATE, aspect_type=AspectType.TYPE), (53, 17): ElementMapping( - element=Element.PARKING_AREAS, aspect_type=AspectType.MATERIAL + element=Element.PARKING_AREAS, + aspect_type=AspectType.MATERIAL, ), (53, 18): ElementMapping( - element=Element.PATHS_AND_HARDSTANDINGS, aspect_type=AspectType.MATERIAL + element=Element.PATHS_AND_HARDSTANDINGS, + aspect_type=AspectType.MATERIAL, ), (53, 24): ElementMapping( - element=Element.PRIVATE_BALCONY, aspect_type=AspectType.PRESENCE + element=Element.PRIVATE_BALCONY, + aspect_type=AspectType.PRESENCE, ), (53, 26): ElementMapping( - element=Element.REAR_FENCING, aspect_type=AspectType.MATERIAL + element=Element.REAR_FENCING, + aspect_type=AspectType.MATERIAL, + ), + (53, 27): ElementMapping( + element=Element.REAR_GATE, + aspect_type=AspectType.TYPE, ), - (53, 27): ElementMapping(element=Element.REAR_GATE, aspect_type=AspectType.TYPE), (53, 28): ElementMapping( - element=Element.RETAINING_WALLS, aspect_type=AspectType.PRESENCE + element=Element.RETAINING_WALLS, + aspect_type=AspectType.PRESENCE, ), (53, 31): ElementMapping( - element=Element.SIDE_FENCING, aspect_type=AspectType.MATERIAL + element=Element.SIDE_FENCING, + aspect_type=AspectType.MATERIAL, ), (53, 32): ElementMapping( - element=Element.SOIL_AND_VENT, aspect_type=AspectType.MATERIAL + element=Element.SOIL_AND_VENT, + aspect_type=AspectType.MATERIAL, ), (53, 34): ElementMapping( - element=Element.SOLAR_THERMALS, aspect_type=AspectType.PRESENCE + element=Element.SOLAR_THERMALS, + aspect_type=AspectType.PRESENCE, ), (53, 44): ElementMapping( - element=Element.GARAGE_STRUCTURE, aspect_type=AspectType.TYPE + element=Element.GARAGE_STRUCTURE, + aspect_type=AspectType.TYPE, ), (53, 45): ElementMapping( - element=Element.BALCONY_BALUSTRADE, aspect_type=AspectType.MATERIAL + element=Element.BALCONY_BALUSTRADE, + aspect_type=AspectType.MATERIAL, ), (150, 1): ElementMapping( - element=Element.BLOCK_ENTRANCE_DOOR, aspect_type=AspectType.MATERIAL + element=Element.BLOCK_ENTRANCE_DOOR, + aspect_type=AspectType.MATERIAL, ), (150, 2): ElementMapping( - element=Element.PATHS_AND_HARDSTANDINGS, aspect_type=AspectType.MATERIAL + element=Element.PATHS_AND_HARDSTANDINGS, + aspect_type=AspectType.MATERIAL, ), # Duplicate of (53, 18) - correct? - (150, 3): ElementMapping(element=Element.ROADS, aspect_type=AspectType.MATERIAL), - (150, 4): ElementMapping( - element=Element.BOUNDARY_WALLS, aspect_type=AspectType.MATERIAL + (150, 3): ElementMapping( + element=Element.ROADS, + aspect_type=AspectType.MATERIAL, + ), + (150, 4): ElementMapping( + element=Element.BOUNDARY_WALLS, + aspect_type=AspectType.MATERIAL, + ), + (150, 5): ElementMapping( + element=Element.OUTBUILDINGS, + aspect_type=AspectType.TYPE, ), - (150, 5): ElementMapping(element=Element.OUTBUILDINGS, aspect_type=AspectType.TYPE), (150, 6): ElementMapping( - element=Element.GARAGE_STRUCTURE, aspect_type=AspectType.TYPE + element=Element.GARAGE_STRUCTURE, + aspect_type=AspectType.TYPE, ), # ========================================================== # INTERNAL – BATHROOMS & KITCHENS # ========================================================== (50, 1): ElementMapping( - element=Element.SECONDARY_TOILET, aspect_type=AspectType.PRESENCE + element=Element.SECONDARY_TOILET, + aspect_type=AspectType.PRESENCE, ), (50, 9): ElementMapping( - element=Element.BATHROOM_EXTRACTOR_FAN, aspect_type=AspectType.PRESENCE + element=Element.BATHROOM_EXTRACTOR_FAN, + aspect_type=AspectType.PRESENCE, + ), + (50, 9): ElementMapping( + element=Element.KITCHEN, + aspect_type=AspectType.TYPE, ), - (50, 9): ElementMapping(element=Element.KITCHEN, aspect_type=AspectType.TYPE), (50, 10): ElementMapping( - element=Element.KITCHEN_EXTRACTOR_FAN, aspect_type=AspectType.PRESENCE + element=Element.KITCHEN_EXTRACTOR_FAN, + aspect_type=AspectType.PRESENCE, ), (50, 13): ElementMapping( - element=Element.KITCHEN_SPACE_LAYOUT, aspect_type=AspectType.ADEQUACY + element=Element.KITCHEN_SPACE_LAYOUT, + aspect_type=AspectType.ADEQUACY, + ), + (50, 17): ElementMapping( + element=Element.BATHRROM, + aspect_type=AspectType.LOCATION, ), - (50, 17): ElementMapping(element=Element.BATHRROM, aspect_type=AspectType.LOCATION), (50, 18): ElementMapping( - element=Element.BATHROOM, aspect_type=AspectType.TYPE + element=Element.BATHROOM, + aspect_type=AspectType.TYPE, ), # Actually "Primary bathroom type" - ok like this? (50, 20): ElementMapping( - element=Element.BATHROOM, aspect_type=AspectType.TYPE, element_instance=2 + element=Element.BATHROOM, + aspect_type=AspectType.TYPE, + element_instance=2, ), # Actually "Secondary bathroom type" - ok like this? - (160, 1): ElementMapping(element=Element.KITCHEN, aspect_type=AspectType.CONDITION), + (160, 1): ElementMapping( + element=Element.KITCHEN, + aspect_type=AspectType.CONDITION, + ), (160, 2): ElementMapping( - element=Element.KITCHEN_SPACE_LAYOUT, aspect_type=AspectType.ADEQUACY + element=Element.KITCHEN_SPACE_LAYOUT, + aspect_type=AspectType.ADEQUACY, ), (190, 1): ElementMapping( - element=Element.BATHROOM, aspect_type=AspectType.CONDITION + element=Element.BATHROOM, + aspect_type=AspectType.CONDITION, ), (190, 2): ElementMapping( - element=Element.SECONDARY_TOILET, aspect_type=AspectType.TYPE + element=Element.SECONDARY_TOILET, + aspect_type=AspectType.TYPE, ), # ========================================================== # COMMUNAL # ========================================================== (51, 1): ElementMapping( - element=Element.COMMUNAL_AERIAL, aspect_type=AspectType.PRESENCE + element=Element.COMMUNAL_AERIAL, + aspect_type=AspectType.PRESENCE, ), (51, 2): ElementMapping( - element=Element.COMMUNAL_AOV, aspect_type=AspectType.PRESENCE + element=Element.COMMUNAL_AOV, + aspect_type=AspectType.PRESENCE, ), (51, 3): ElementMapping( - element=Element.COMMUNAL_BALCONY_WALKWAY, aspect_type=AspectType.PRESENCE + element=Element.COMMUNAL_BALCONY_WALKWAY, + aspect_type=AspectType.PRESENCE, ), (51, 4): ElementMapping( - element=Element.COMMUNAL_BATHROOM, aspect_type=AspectType.TYPE + element=Element.COMMUNAL_BATHROOM, + aspect_type=AspectType.TYPE, ), (51, 5): ElementMapping( - element=Element.COMMUNAL_BIN_STORE_DOORS, aspect_type=AspectType.PRESENCE + element=Element.COMMUNAL_BIN_STORE_DOORS, + aspect_type=AspectType.PRESENCE, ), (51, 6): ElementMapping( - element=Element.COMMUNAL_BIN_STORE_ROOF, aspect_type=AspectType.PRESENCE + element=Element.COMMUNAL_BIN_STORE_ROOF, + aspect_type=AspectType.PRESENCE, ), (51, 7): ElementMapping( - element=Element.COMMUNAL_BIN_STORE_WALLS, aspect_type=AspectType.MATERIAL + element=Element.COMMUNAL_BIN_STORE_WALLS, + aspect_type=AspectType.MATERIAL, ), (51, 8): ElementMapping( - element=Element.COMMUNAL_BMS, aspect_type=AspectType.PRESENCE + element=Element.COMMUNAL_BMS, + aspect_type=AspectType.PRESENCE, ), (51, 9): ElementMapping( - element=Element.COMMUNAL_BOILER, aspect_type=AspectType.TYPE + element=Element.COMMUNAL_BOILER, + aspect_type=AspectType.TYPE, ), (51, 10): ElementMapping( - element=Element.COMMUNAL_BOOSTER_PUMP, aspect_type=AspectType.PRESENCE + element=Element.COMMUNAL_BOOSTER_PUMP, + aspect_type=AspectType.PRESENCE, ), (51, 11): ElementMapping( - element=Element.COMMUNAL_CCTV, aspect_type=AspectType.PRESENCE + element=Element.COMMUNAL_CCTV, + aspect_type=AspectType.PRESENCE, ), (51, 12): ElementMapping( - element=Element.COMMUNAL_CIRCULATION_SPACE, aspect_type=AspectType.ADEQUACY + element=Element.COMMUNAL_CIRCULATION_SPACE, + aspect_type=AspectType.ADEQUACY, ), (51, 13): ElementMapping( - element=Element.COMMUNAL_COLD_WATER_STORAGE, aspect_type=AspectType.PRESENCE + element=Element.COMMUNAL_COLD_WATER_STORAGE, + aspect_type=AspectType.PRESENCE, ), (51, 14): ElementMapping( - element=Element.COMMUNAL_DOOR_ENTRY, aspect_type=AspectType.SYSTEM + element=Element.COMMUNAL_DOOR_ENTRY, + aspect_type=AspectType.SYSTEM, ), (51, 15): ElementMapping( - element=Element.COMMUNAL_DRY_RISER, aspect_type=AspectType.PRESENCE + element=Element.COMMUNAL_DRY_RISER, + aspect_type=AspectType.PRESENCE, ), (51, 16): ElementMapping( - element=Element.COMMUNAL_EMERGENCY_LIGHTING, aspect_type=AspectType.PRESENCE + element=Element.COMMUNAL_EMERGENCY_LIGHTING, + aspect_type=AspectType.PRESENCE, ), (51, 17): ElementMapping( - element=Element.COMMUNAL_EXTERNAL_DOORS, aspect_type=AspectType.MATERIAL + element=Element.COMMUNAL_EXTERNAL_DOORS, + aspect_type=AspectType.MATERIAL, ), (51, 19): ElementMapping( - element=Element.COMMUNAL_FIRE_ALARM, aspect_type=AspectType.TYPE + element=Element.COMMUNAL_FIRE_ALARM, + aspect_type=AspectType.TYPE, ), (51, 20): ElementMapping( - element=Element.COMMUNAL_INTERNAL_DECORATIONS, aspect_type=AspectType.PRESENCE + element=Element.COMMUNAL_INTERNAL_DECORATIONS, + aspect_type=AspectType.PRESENCE, ), (51, 21): ElementMapping( - element=Element.COMMUNAL_INTERNAL_DOORS, aspect_type=AspectType.MATERIAL + element=Element.COMMUNAL_INTERNAL_DOORS, + aspect_type=AspectType.MATERIAL, ), (51, 22): ElementMapping( - element=Element.COMMUNAL_INTERNAL_FLOOR, aspect_type=AspectType.FINISH + element=Element.COMMUNAL_INTERNAL_FLOOR, + aspect_type=AspectType.FINISH, ), (51, 23): ElementMapping( - element=Element.COMMUNAL_KITCHEN, aspect_type=AspectType.TYPE + element=Element.COMMUNAL_KITCHEN, + aspect_type=AspectType.TYPE, ), (51, 24): ElementMapping( - element=Element.COMMUNAL_LATERAL_MAINS, aspect_type=AspectType.PRESENCE + element=Element.COMMUNAL_LATERAL_MAINS, + aspect_type=AspectType.PRESENCE, ), (51, 25): ElementMapping( - element=Element.COMMUNAL_LIGHTING, aspect_type=AspectType.PRESENCE + element=Element.COMMUNAL_LIGHTING, + aspect_type=AspectType.PRESENCE, ), (51, 26): ElementMapping( - element=Element.COMMUNAL_LIGHTING_CONDUCTOR, aspect_type=AspectType.PRESENCE + element=Element.COMMUNAL_LIGHTING_CONDUCTOR, + aspect_type=AspectType.PRESENCE, ), (51, 27): ElementMapping( - element=Element.COMMUNAL_PASSENGER_LIFT, aspect_type=AspectType.TYPE + element=Element.COMMUNAL_PASSENGER_LIFT, + aspect_type=AspectType.TYPE, ), (51, 28): ElementMapping( element=Element.COMMUNAL_ENTRANCE, @@ -357,135 +481,176 @@ PEABODY_ELEMENT_MAP = { element_instance=2, ), (51, 14): ElementMapping( - element=Element.COMMUNAL_SPRINKLER, aspect_type=AspectType.PRESENCE + element=Element.COMMUNAL_SPRINKLER, + aspect_type=AspectType.PRESENCE, ), (51, 29): ElementMapping( - element=Element.COMMUNAL_REFUSE_CHUTE, aspect_type=AspectType.PRESENCE + element=Element.COMMUNAL_REFUSE_CHUTE, + aspect_type=AspectType.PRESENCE, ), (51, 32): ElementMapping( - element=Element.COMMUNAL_STAIRS, aspect_type=AspectType.FINISH + element=Element.COMMUNAL_STAIRS, + aspect_type=AspectType.FINISH, ), (51, 33): ElementMapping( - element=Element.COMMUNAL_STORE_DOORS, aspect_type=AspectType.MATERIAL + element=Element.COMMUNAL_STORE_DOORS, + aspect_type=AspectType.MATERIAL, ), (51, 34): ElementMapping( - element=Element.COMMUNAL_STORE_ROOF, aspect_type=AspectType.MATERIAL + element=Element.COMMUNAL_STORE_ROOF, + aspect_type=AspectType.MATERIAL, ), (51, 35): ElementMapping( - element=Element.COMMUNAL_STORE_WALLS, aspect_type=AspectType.MATERIAL + element=Element.COMMUNAL_STORE_WALLS, + aspect_type=AspectType.MATERIAL, ), (51, 36): ElementMapping( - element=Element.COMMUNAL_WALKWAYS, aspect_type=AspectType.FINISH + element=Element.COMMUNAL_WALKWAYS, + aspect_type=AspectType.FINISH, ), (51, 37): ElementMapping( - element=Element.COMMUNAL_WARDEN_CALL_SYSTEM, aspect_type=AspectType.PRESENCE + element=Element.COMMUNAL_WARDEN_CALL_SYSTEM, + aspect_type=AspectType.PRESENCE, ), (51, 38): ElementMapping( - element=Element.COMMUNAL_TOILETS, aspect_type=AspectType.TYPE + element=Element.COMMUNAL_TOILETS, + aspect_type=AspectType.TYPE, ), (51, 39): ElementMapping( - element=Element.COMMUNAL_WET_RISER, aspect_type=AspectType.PRESENCE + element=Element.COMMUNAL_WET_RISER, + aspect_type=AspectType.PRESENCE, ), (51, 40): ElementMapping( - element=Element.COMMUNAL_PLUG_SOCKETS, aspect_type=AspectType.PRESENCE + element=Element.COMMUNAL_PLUG_SOCKETS, + aspect_type=AspectType.PRESENCE, ), (200, 1): ElementMapping( - element=Element.COMMUNAL_BOILER, aspect_type=AspectType.TYPE + element=Element.COMMUNAL_BOILER, + aspect_type=AspectType.TYPE, ), # Duplicate of (51, 9) - correct? (200, 2): ElementMapping( - element=Element.COMMUNAL_HEATING, aspect_type=AspectType.TYPE + element=Element.COMMUNAL_HEATING, + aspect_type=AspectType.TYPE, ), (200, 3): ElementMapping( - element=Element.COMMUNAL_ELECTRICS, aspect_type=AspectType.TYPE + element=Element.COMMUNAL_ELECTRICS, + aspect_type=AspectType.TYPE, ), (200, 4): ElementMapping( - element=Element.COMMUNAL_FIRE_ALARM, aspect_type=AspectType.TYPE + element=Element.COMMUNAL_FIRE_ALARM, + aspect_type=AspectType.TYPE, ), (200, 5): ElementMapping( - element=Element.COMMUNAL_LIFT, aspect_type=AspectType.TYPE + element=Element.COMMUNAL_LIFT, + aspect_type=AspectType.TYPE, ), (200, 6): ElementMapping( - element=Element.COMMUNAL_FLOOR_COVERING, aspect_type=AspectType.MATERIAL + element=Element.COMMUNAL_FLOOR_COVERING, + aspect_type=AspectType.MATERIAL, ), (200, 7): ElementMapping( - element=Element.COMMUNAL_KITCHEN, aspect_type=AspectType.TYPE + element=Element.COMMUNAL_KITCHEN, + aspect_type=AspectType.TYPE, ), (200, 8): ElementMapping( - element=Element.COMMUNAL_BATHROOM, aspect_type=AspectType.TYPE + element=Element.COMMUNAL_BATHROOM, + aspect_type=AspectType.TYPE, ), # Duplicate of (51, 4) - correct? (200, 9): ElementMapping( - element=Element.COMMUNAL_TOILETS, aspect_type=AspectType.TYPE + element=Element.COMMUNAL_TOILETS, + aspect_type=AspectType.TYPE, ), # Duplicate of (51, 38) - correct? (200, 10): ElementMapping( - element=Element.COMMUNAL_GATES, aspect_type=AspectType.TYPE + element=Element.COMMUNAL_GATES, + aspect_type=AspectType.TYPE, ), # ========================================================== # INTERNAL – HEATING # ========================================================== (50, 4): ElementMapping( - element=Element.HEATING_BOILER, aspect_type=AspectType.PRESENCE + element=Element.HEATING_BOILER, + aspect_type=AspectType.PRESENCE, ), # This is actually "Central heating boiler" - ok like this? (50, 5): ElementMapping( - element=Element.CENTRAL_HEATING, aspect_type=AspectType.EXTENT + element=Element.CENTRAL_HEATING, + aspect_type=AspectType.EXTENT, ), (50, 6): ElementMapping( - element=Element.COLD_WATER_STORAGE, aspect_type=AspectType.PRESENCE + element=Element.COLD_WATER_STORAGE, + aspect_type=AspectType.PRESENCE, ), (50, 12): ElementMapping( - element=Element.HEATING_DISTRIBUTION, aspect_type=AspectType.TYPE + element=Element.HEATING_DISTRIBUTION, + aspect_type=AspectType.TYPE, ), (50, 19): ElementMapping( - element=Element.PROGRAMMABLE_HEATING, aspect_type=AspectType.TYPE + element=Element.PROGRAMMABLE_HEATING, + aspect_type=AspectType.TYPE, ), (50, 25): ElementMapping( - element=Element.HEATING_BOILER, aspect_type=AspectType.TYPE + element=Element.HEATING_BOILER, + aspect_type=AspectType.TYPE, ), (170, 1): ElementMapping( - element=Element.HEATING_BOILER, aspect_type=AspectType.TYPE + element=Element.HEATING_BOILER, + aspect_type=AspectType.TYPE, ), # Duplicate of (50,25) - correct? (170, 2): ElementMapping( - element=Element.HEATING_DISTRIBUTION, aspect_type=AspectType.TYPE + element=Element.HEATING_DISTRIBUTION, + aspect_type=AspectType.TYPE, ), # Duplicate of (50,12) - correct? (170, 3): ElementMapping( - element=Element.SECONDARY_HEATING, aspect_type=AspectType.TYPE + element=Element.SECONDARY_HEATING, + aspect_type=AspectType.TYPE, ), (170, 4): ElementMapping( - element=Element.COLD_WATER_STORAGE, aspect_type=AspectType.TYPE + element=Element.COLD_WATER_STORAGE, + aspect_type=AspectType.TYPE, ), (170, 5): ElementMapping( - element=Element.HOT_WATER_SYSTEM, aspect_type=AspectType.TYPE + element=Element.HOT_WATER_SYSTEM, + aspect_type=AspectType.TYPE, ), # ========================================================== # ELECTRICS # ========================================================== (50, 24): ElementMapping( - element=Element.INTERNAL_WIRING, aspect_type=AspectType.MATERIAL + element=Element.INTERNAL_WIRING, + aspect_type=AspectType.MATERIAL, ), (180, 1): ElementMapping( - element=Element.ELECTRICAL_WIRING, aspect_type=AspectType.WORK_REQUIRED + element=Element.ELECTRICAL_WIRING, + aspect_type=AspectType.WORK_REQUIRED, ), # Not certain about the AspectType - only example in the sample data is "Full Rewire" (180, 2): ElementMapping( - element=Element.CONSUMER_UNIT, aspect_type=AspectType.TYPE + element=Element.CONSUMER_UNIT, + aspect_type=AspectType.TYPE, ), (180, 3): ElementMapping( - element=Element.SMOKE_DETECTION, aspect_type=AspectType.TYPE + element=Element.SMOKE_DETECTION, + aspect_type=AspectType.TYPE, ), # Duplicate of (50, 21) - correct? (180, 4): ElementMapping( - element=Element.CARBON_MONOXIDE_DETECTION, aspect_type=AspectType.TYPE + element=Element.CARBON_MONOXIDE_DETECTION, + aspect_type=AspectType.TYPE, ), # Duplicate of (50, 2) - correct? # ========================================================== # HHSRS # ========================================================== (54, 1): ElementMapping( - element=Element.HHSRS_DAMP_AND_MOULD, aspect_type=AspectType.RISK + element=Element.HHSRS_DAMP_AND_MOULD, + aspect_type=AspectType.RISK, ), (54, 4): ElementMapping( - element=Element.HHSRS_ASBESTOS_AND_MMF, aspect_type=AspectType.RISK + element=Element.HHSRS_ASBESTOS_AND_MMF, + aspect_type=AspectType.RISK, ), (54, 15): ElementMapping( - element=Element.HHSRS_DOMESTIC_HYGIENE_PESTS_REFUSE, aspect_type=AspectType.RISK + element=Element.HHSRS_DOMESTIC_HYGIENE_PESTS_REFUSE, + aspect_type=AspectType.RISK, ), (54, 29): ElementMapping( - element=Element.HHSRS_STRUCTURAL_COLLAPSE, aspect_type=AspectType.RISK + element=Element.HHSRS_STRUCTURAL_COLLAPSE, + aspect_type=AspectType.RISK, ), } From d2ce135f07f3b7fcda1f512b49b52c0e860c0de5 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Mon, 26 Jan 2026 16:45:36 +0000 Subject: [PATCH 39/68] final missing mappings --- .../domain/mapping/lbwf/lbwf_element_map.py | 14 ++++-- .../domain/mapping/lbwf/lbwf_mapper.py | 50 ++++++++++--------- .../mapping/peabody/peabody_element_map.py | 12 +++-- backend/condition/processor.py | 2 +- 4 files changed, 44 insertions(+), 34 deletions(-) diff --git a/backend/condition/domain/mapping/lbwf/lbwf_element_map.py b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py index 02722b11..8d6ea858 100644 --- a/backend/condition/domain/mapping/lbwf/lbwf_element_map.py +++ b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py @@ -423,7 +423,7 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { element=Element.HHSRS_ASBESTOS_AND_MMF, aspect_type=AspectType.RISK, ), - "HHSRSBIOCIDES": ElementMapping( + "HHSRSBIOC": ElementMapping( element=Element.HHSRS_BIOCIDES, aspect_type=AspectType.RISK, ), @@ -431,6 +431,14 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { element=Element.HHSRS_CARBON_MONOXIDE, aspect_type=AspectType.RISK, ), + "HHSRSNO2": ElementMapping( + element=Element.HHSRS_CARBON_MONOXIDE, + aspect_type=AspectType.RISK, + ), # Duplicate of HHSRSCO; I think they relate to the same HHSRS hazard + "HHSRSSO2": ElementMapping( + element=Element.HHSRS_CARBON_MONOXIDE, + aspect_type=AspectType.RISK, + ), # Duplicate of HHSRSCO; I think they relate to the same HHSRS hazard "HHSRSLEAD": ElementMapping( element=Element.HHSRS_LEAD, aspect_type=AspectType.RISK, @@ -528,7 +536,3 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { aspect_type=AspectType.RISK, ), } - -# Unhandled: -# DECNTHMINC -# EICINSFREQ diff --git a/backend/condition/domain/mapping/lbwf/lbwf_mapper.py b/backend/condition/domain/mapping/lbwf/lbwf_mapper.py index 635e5898..3d7b7349 100644 --- a/backend/condition/domain/mapping/lbwf/lbwf_mapper.py +++ b/backend/condition/domain/mapping/lbwf/lbwf_mapper.py @@ -27,32 +27,34 @@ class LbwfMapper(Mapper): uprn: int = client_data.uprn for raw_asset in client_data.assets: - try: - element_mapping: ElementMapping = LbwfMapper._map_element( - raw_asset.element_code - ) - except: - logger.warning( - f"Unrecognised LBWF Asset Element Code: {raw_asset.element_code}. Skipping record" - ) - continue + # Ignore metadata rows + if raw_asset.element_code not in ["EICINSFREQ", "DECNTHMINC"]: + try: + element_mapping: ElementMapping = LbwfMapper._map_element( + raw_asset.element_code + ) + except: + logger.warning( + f"Unrecognised LBWF Asset Element Code: {raw_asset.element_code}. Skipping record" + ) + continue - mapped_assets.append( - AssetCondition( - uprn=uprn, - element=element_mapping.element, - aspect_type=element_mapping.aspect_type, - value=raw_asset.attribute_code_description, - quantity=raw_asset.quantity, - install_date=raw_asset.install_date, - renewal_year=LbwfMapper._calculate_renewal_year( - raw_asset, survey_year - ), - element_instance=element_mapping.element_instance, - source_system=None, # Once we know the system name we'll set it here - comments=raw_asset.element_comments, + mapped_assets.append( + AssetCondition( + uprn=uprn, + element=element_mapping.element, + aspect_type=element_mapping.aspect_type, + value=raw_asset.attribute_code_description, + quantity=raw_asset.quantity, + install_date=raw_asset.install_date, + renewal_year=LbwfMapper._calculate_renewal_year( + raw_asset, survey_year + ), + element_instance=element_mapping.element_instance, + source_system=None, # Once we know the system name we'll set it here + comments=raw_asset.element_comments, + ) ) - ) return mapped_assets diff --git a/backend/condition/domain/mapping/peabody/peabody_element_map.py b/backend/condition/domain/mapping/peabody/peabody_element_map.py index 8fe2ccb9..2485136b 100644 --- a/backend/condition/domain/mapping/peabody/peabody_element_map.py +++ b/backend/condition/domain/mapping/peabody/peabody_element_map.py @@ -51,7 +51,7 @@ PEABODY_ELEMENT_MAP = { (53, 4): ElementMapping( element=Element.EXTERNAL_DECORATION, aspect_type=AspectType.PRESENCE ), - (53, 4): ElementMapping( + (53, 5): ElementMapping( element=Element.EXTERNAL_NOISE_INSULATION, aspect_type=AspectType.ADEQUACY ), (53, 14): ElementMapping( @@ -109,7 +109,7 @@ PEABODY_ELEMENT_MAP = { element=Element.GUTTERS, aspect_type=AspectType.MATERIAL, ), - (53, 18): ElementMapping( + (53, 21): ElementMapping( element=Element.PITCHED_ROOF_COVERING, aspect_type=AspectType.MATERIAL, ), @@ -334,8 +334,12 @@ PEABODY_ELEMENT_MAP = { element=Element.KITCHEN_SPACE_LAYOUT, aspect_type=AspectType.ADEQUACY, ), + (50, 14): ElementMapping( + element=Element.KITCHEN, + aspect_type=AspectType.TYPE, + ), (50, 17): ElementMapping( - element=Element.BATHRROM, + element=Element.BATHROOM, aspect_type=AspectType.LOCATION, ), (50, 18): ElementMapping( @@ -480,7 +484,7 @@ PEABODY_ELEMENT_MAP = { aspect_type=AspectType.FINISH, element_instance=2, ), - (51, 14): ElementMapping( + (51, 31): ElementMapping( element=Element.COMMUNAL_SPRINKLER, aspect_type=AspectType.PRESENCE, ), diff --git a/backend/condition/processor.py b/backend/condition/processor.py index a48e22f4..903c9f23 100644 --- a/backend/condition/processor.py +++ b/backend/condition/processor.py @@ -26,4 +26,4 @@ def process_file(file_stream: BinaryIO, source_key: str) -> None: for p in raw_properties: assets.extend(mapper.map_asset_conditions_for_property(p, survey_year)) - print(assets) # temp + print("done") # temp From cfc73d8f90ac429ef34590c08a0029dda4f14a9d Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Mon, 26 Jan 2026 17:07:22 +0000 Subject: [PATCH 40/68] fix broken test --- backend/condition/tests/mapping/test_lbwf_mapper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/condition/tests/mapping/test_lbwf_mapper.py b/backend/condition/tests/mapping/test_lbwf_mapper.py index 918b6fea..907bd250 100644 --- a/backend/condition/tests/mapping/test_lbwf_mapper.py +++ b/backend/condition/tests/mapping/test_lbwf_mapper.py @@ -277,7 +277,7 @@ def test_lbwf_mapper_maps_house(): ), AssetCondition( uprn=1, - element=Element.HEATING_SYSTEM, + element=Element.CENTRAL_HEATING, aspect_type=AspectType.EXTENT, element_instance=None, value="No Central Heating in Property", From 64eb2e2f204ee6f8d50dc7f4adb7646c23d4e6ff Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 26 Jan 2026 17:49:56 +0000 Subject: [PATCH 41/68] added in basic process for sloping ceiling --- backend/app/plan/schemas.py | 10 +++- recommendations/Costs.py | 68 ++++++++++++++++++++++- recommendations/RoofRecommendations.py | 76 +++++++++++++++++++++++--- 3 files changed, 143 insertions(+), 11 deletions(-) diff --git a/backend/app/plan/schemas.py b/backend/app/plan/schemas.py index edac31dc..7c352eba 100644 --- a/backend/app/plan/schemas.py +++ b/backend/app/plan/schemas.py @@ -9,7 +9,9 @@ TYPICAL_MEASURE_TYPES = [ ] WALL_INSULATION_MEASURES = ["internal_wall_insulation", "external_wall_insulation", "cavity_wall_insulation"] -ROOF_INSULATION_MEASURES = ["loft_insulation", "flat_roof_insulation", "room_roof_insulation"] +ROOF_INSULATION_MEASURES = [ + "loft_insulation", "flat_roof_insulation", "room_roof_insulation", "sloping_ceiling_insulation" +] # Both all and roof insulaiton measures are eligible for ECO4. These are the remaining fabric and heating measures # This is based on th measures we have recommendations for @@ -31,7 +33,7 @@ SPECIFIC_MEASURES = ( INSULATION_MEASURES = [ "internal_wall_insulation", "external_wall_insulation", "cavity_wall_insulation", - "loft_insulation", "flat_roof_insulation", "room_roof_insulation", + "loft_insulation", "flat_roof_insulation", "room_roof_insulation", "sloping_ceiling_insulation", "suspended_floor_insulation", "solid_floor_insulation", ] @@ -46,7 +48,9 @@ MEASURE_MAP = { "wall_insulation": [ "internal_wall_insulation", "external_wall_insulation", "cavity_wall_insulation", ], - "roof_insulation": ["loft_insulation", "flat_roof_insulation", "room_roof_insulation"], + "roof_insulation": [ + "loft_insulation", "flat_roof_insulation", "room_roof_insulation", "sloping_ceiling_insulation" + ], "floor_insulation": ["suspended_floor_insulation", "solid_floor_insulation"], "heating": ["boiler_upgrade", "high_heat_retention_storage_heaters", "air_source_heat_pump"], "windows": ["double_glazing", "secondary_glazing"], diff --git a/recommendations/Costs.py b/recommendations/Costs.py index 3a65312e..fd429afa 100644 --- a/recommendations/Costs.py +++ b/recommendations/Costs.py @@ -1,4 +1,6 @@ +from typing import Mapping, Any import numpy as np + from recommendations.county_to_region import county_to_region_map from utils.logger import setup_logger from backend.ml_models.AnnualBillSavings import AnnualBillSavings @@ -166,7 +168,8 @@ class Costs: "room_roof_insulation": 0.26, "heater_removal": 0.1, "sealing_open_fireplace": 0.1, - "mechanical_ventilation": 0.26 + "mechanical_ventilation": 0.26, + "sloping_ceiling_insulation": 0.26 # Similar to IWI so using the same contingency } # Preliminaries are a percentage of the total cost of the work and covers the cost of site-specific costs @@ -935,3 +938,66 @@ class Costs: "labour_hours": 80, "labour_days": 10, } + + @staticmethod + def _estimate_number_of_days_for_sloping_ceiling(insulation_roof_area: float) -> float: + """ + Estimate labour days required to insulate an existing sloping ceiling. + + Heuristic model based on retrofit guidance (Checkatrade, The Green Age) + and analogy with internal wall insulation. + + Assumptions: + - ~30 m² of sloping ceiling takes ~4 working days + - Small jobs still require multiple days (setup, stripping, reboarding) + - Larger areas benefit from economies of scale, but not linearly + + :param insulation_roof_area: m² of sloping ceiling to be insulated + """ + + base_days = 4 + base_area = 30 # m2 reference case + labour_exponent = 0.85 + min_days = 2 + + labour_days = max( + min_days, + base_days * (insulation_roof_area / base_area) ** labour_exponent + ) + + return labour_days + + @classmethod + def sloping_ceiling_insulation(cls, insulation_roof_area: float) -> Mapping[str, Any]: + """ + This costing for this is based on Checkatrade desktop research, since we are yet to receive installer quotes. + :param insulation_roof_area: Area of the sloping ceiling to be insulated + :return: + """ + ################ + # Assumptions + ################ + # Sources: + # https://www.checkatrade.com/blog/cost-guides/vaulted-ceiling-cost/ + # https://www.thegreenage.co.uk/can-i-insulate-my-sloping-ceiling/ + # These assumptions last updated 21/02/2026 + insulation_cost_per_m2 = 52 # The actual install process is quite similar to IWI + labour_rate = 250 # per day + contingency_rate = cls.CONTINGENCIES["sloping_ceiling_insulation"] + + labour_days = cls._estimate_number_of_days_for_sloping_ceiling(insulation_roof_area) + labour_hours = labour_days * 8 + + total = (insulation_cost_per_m2 * insulation_roof_area) + (labour_rate * labour_days) + + # Assume VAT included in the total => total is 120% of subtotal + vat = total - (total / 1.2) + + return { + "total": total, + "contingency": total * contingency_rate, + "contingency_rate": contingency_rate, + "vat": vat, + "labour_hours": labour_hours, + "labour_days": labour_days, + } diff --git a/recommendations/RoofRecommendations.py b/recommendations/RoofRecommendations.py index 6625aeb0..baaaa547 100644 --- a/recommendations/RoofRecommendations.py +++ b/recommendations/RoofRecommendations.py @@ -324,10 +324,11 @@ class RoofRecommendations: ) self.estimated_u_value = u_value + # The Roof is already compliant - in this case, the u-value is beyond the requirements for + # Building Regs Part L and so we don't recommend anything if (u_value <= self.BUILDING_REGULATIONS_PART_L_MAX_U_VALUE) or all( m not in measures for m in MEASURE_MAP["roof_insulation"] ): - # The Roof is already compliant return non_invasive_recommendations = self.property.non_invasive_recommendations @@ -381,14 +382,12 @@ class RoofRecommendations: has_room_roof_recommendation=has_room_roof_recommendation ) - ################################################## + ################################################################ # ~~~~~ Loft Insulation Recommendation Logic ~~~~~ - ################################################## - # We firstly handle non-intrusive recommendations, which may override the normal roof insulation recommendations + ################################################################ if needs_loft_insulation: self.recommend_roof_insulation( u_value=u_value, - insulation_thickness=self.insulation_thickness, phase=phase, is_flat=False, is_pitched=True, @@ -396,10 +395,12 @@ class RoofRecommendations: ) return + ################################################################ + # ~~~~~ Flat Roof Insulation Recommendation Logic ~~~~~ + ################################################################ if needs_flat_roof_insulation: self.recommend_roof_insulation( u_value=u_value, - insulation_thickness=0, phase=phase, is_flat=True, is_pitched=False, @@ -407,12 +408,21 @@ class RoofRecommendations: ) return + ################################################################ + # ~~~~~ Room Roof Insulation Recommendation Logic ~~~~~ + ################################################################ # There are cases where the property might have a room roof as the second roof, but we have a recommendation for # it, so we allow this override if needs_rir_insulation: self.recommend_room_roof_insulation(u_value, phase, default_u_values) return + #################################################################################################### + # ~~~~~ Sloping Ceiling Insulation Recommendation Logic ~~~~~ + #################################################################################################### + if needs_sloping_ceiling: + self.recommend_sloping_ceiling() + raise NotImplementedError("Implement me") @staticmethod @@ -432,7 +442,7 @@ class RoofRecommendations: raise ValueError("Invalid material type") def recommend_roof_insulation( - self, u_value, insulation_thickness, phase, is_pitched, is_flat, default_u_values + self, u_value, phase, is_pitched, is_flat, default_u_values ): """ @@ -773,3 +783,55 @@ class RoofRecommendations: ) self.recommendations = recommendations + + def recommend_sloping_ceiling(self, phase: int, u_value, sloping_ceiling_recommendation: dict = None): + """ + Recommend insulation for a sloping ceiling + Since we don't have any materials from installers for this specific recommendation, we + do not iterate through any materials. Instead, we provide a single recommendation, we estimated + prices based on desk research. + :return: + """ + + new_description = "Pitched, insulated" + new_efficiency = "Good" + + roof_ending_config = RoofAttributes(new_description).process() + roof_simulation_config = check_simulation_difference( + new_config=roof_ending_config, old_config=self.property.roof, prefix="roof_" + ) + + # We pull out new u-values, based on 75mm of insulation, with u-values defined from Elmhurst + new_u_value = 0.5 # This doesn't change, regardless of starting u-value + + simulation_config = { + **roof_simulation_config, + "roof_thermal_transmittance_ending": new_u_value, + "roof_energy_eff_ending": new_efficiency + } + + cost_result = self.costs.sloping_ceiling_insulation( + roof_area=self.property.roof_area # For a pitched roof, this is the pitched roof area + ) + + self.recommendations = [ + { + "phase": phase, + "parts": [], + "type": "sloping_ceiling_insulation", + "measure_type": "sloping_ceiling_insulation", + "description": "Insulate sloping ceilings at the rafters and re-decorate", + "starting_u_value": u_value, + "new_u_value": None, + "sap_points": sloping_ceiling_recommendation.get("sap_points", None), + "simulation_config": simulation_config, + "description_simulation": { + "roof-description": new_description, + "roof-energy-eff": new_efficiency + }, + **cost_result, + "already_installed": "sloping_ceiling_insulation" in self.property.already_installed, + "survey": sloping_ceiling_recommendation.get("survey", None), + "innovation_rate": 0 + } + ] From ac749e427e2f8724077489cf6bff4a6adc6f2c32 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 26 Jan 2026 19:06:50 +0000 Subject: [PATCH 42/68] first implementation for sloping ceiling insulation --- .idea/copilot.data.migration.ask2agent.xml | 6 ++ recommendations/Costs.py | 16 ++-- recommendations/RoofRecommendations.py | 59 ++++++++------- .../tests/test_roof_recommendations.py | 74 +++++++++---------- 4 files changed, 84 insertions(+), 71 deletions(-) create mode 100644 .idea/copilot.data.migration.ask2agent.xml diff --git a/.idea/copilot.data.migration.ask2agent.xml b/.idea/copilot.data.migration.ask2agent.xml new file mode 100644 index 00000000..1f2ea11e --- /dev/null +++ b/.idea/copilot.data.migration.ask2agent.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/recommendations/Costs.py b/recommendations/Costs.py index fd429afa..5f312f63 100644 --- a/recommendations/Costs.py +++ b/recommendations/Costs.py @@ -947,6 +947,10 @@ class Costs: Heuristic model based on retrofit guidance (Checkatrade, The Green Age) and analogy with internal wall insulation. + See _estimate_number_of_days_for_solid_floor for detailed explanation regarding assumptions + and methodology, however for the purpose of placeholder, this function mimics the approach + to that method but is detached to allow for future changes + Assumptions: - ~30 m² of sloping ceiling takes ~4 working days - Small jobs still require multiple days (setup, stripping, reboarding) @@ -968,7 +972,7 @@ class Costs: return labour_days @classmethod - def sloping_ceiling_insulation(cls, insulation_roof_area: float) -> Mapping[str, Any]: + def sloping_ceiling_insulation(cls, insulation_roof_area: float) -> Mapping[str, float]: """ This costing for this is based on Checkatrade desktop research, since we are yet to receive installer quotes. :param insulation_roof_area: Area of the sloping ceiling to be insulated @@ -994,10 +998,10 @@ class Costs: vat = total - (total / 1.2) return { - "total": total, - "contingency": total * contingency_rate, + "total": float(total), + "contingency": float(total * contingency_rate), "contingency_rate": contingency_rate, - "vat": vat, - "labour_hours": labour_hours, - "labour_days": labour_days, + "vat": float(vat), + "labour_hours": float(labour_hours), + "labour_days": float(labour_days), } diff --git a/recommendations/RoofRecommendations.py b/recommendations/RoofRecommendations.py index baaaa547..06ff306b 100644 --- a/recommendations/RoofRecommendations.py +++ b/recommendations/RoofRecommendations.py @@ -2,7 +2,7 @@ import math import pandas as pd from backend.Property import Property from backend.app.plan.schemas import MEASURE_MAP -from typing import List +from typing import List, Mapping, Any from datatypes.enums import QuantityUnits from recommendations.recommendation_utils import ( get_roof_u_value, r_value_per_mm_to_u_value, calculate_u_value_uplift, is_diminishing_returns, @@ -119,24 +119,6 @@ class RoofRecommendations: return (full_insulated_room_roof or room_roof_insulated_at_rafters) and not has_non_invasive_recommendation - def recommend_sloping_ceiling(self): - """ - Sloping ceiling insulation recommendations are different from other roof types, though - the description of the roof appears to be quite similar to a roof with a loft. In order to - deduce the roof type, we apply the following logic: - - 1) If the roof is descrbed as pitched, insulated, without a loft insulation thickness, it's - an insulated sloped ceiling - 2) If the roof insulation is assumed, it implies that the surveyor could not gain access to the - roof and therefore it's a loft - 3) If it's a pitched roof that is uninsulated and is NOT assumed, and there is not loft insulation - recommendation, this implies that the surveyor was able to gain access to the roof and there was no - loft insulation recommendation so it must be a sloping ceiling since loft insulation is a default - recommendation for an uninsualted loft - :return: - """ - pass - @staticmethod def is_sloping_ceiling_appropriate( is_pitched: bool, is_loft: bool, is_assumed: bool, has_sloping_ceiling_recommendation: bool, @@ -207,7 +189,7 @@ class RoofRecommendations: @staticmethod def is_flat_roof_insulation_appropriate( - is_flat: bool, measures: List, has_flat_roof_recommendation: bool + is_flat: bool, measures: List, has_flat_roof_recommendation: bool, primary_roof_is_sloped: bool ) -> bool: """ Determine if flat roof insulation is appropriate @@ -215,12 +197,17 @@ class RoofRecommendations: :param measures: List - list of measures :param has_flat_roof_recommendation: Boolean - indicates whether or not there is a flat roof non-invasive recommendation + :param primary_roof_is_sloped: Boolean - indicates if the primary roof is flat :return: Boolean + + When checking if has_flat_roof_recommendation and primary_roof_is_sloped, we need to check both + conditions. This is because within a default EPC recommendation, the EPC will pair these recommendations + together. Therefore, weneed to ensure the primary roof isn't sloped """ flat_roof_in_measures = "flat_roof_insulation" in measures - return (is_flat and flat_roof_in_measures) or has_flat_roof_recommendation + return (is_flat and flat_roof_in_measures) or (has_flat_roof_recommendation and not primary_roof_is_sloped) @staticmethod def is_room_roof_insulation_appropriate( @@ -272,6 +259,8 @@ class RoofRecommendations: ): return False + return True + @staticmethod def _is_primary_roof_sloped( is_pitched: bool, is_loft: bool, is_assumed: bool @@ -365,15 +354,15 @@ class RoofRecommendations: has_sloping_ceiling_recommendation=has_sloping_ceiling_recommendation, primary_roof_is_sloped=primary_roof_is_sloped ) - needs_loft_insulation = self.is_loft_insulation_appropriate( measures=measures, is_pitched=is_pitched, is_at_rafters=is_at_rafters, rir_over_loft=rir_over_loft, has_loft_insulation_recommendation=has_loft_insulation_recommendation, is_assumed=is_assumed, has_sloping_ceiling_recommendation=has_sloping_ceiling_recommendation ) - needs_flat_roof_insulation = self.is_flat_roof_insulation_appropriate( - is_flat=is_flat, measures=measures, has_flat_roof_recommendation=has_flat_roof_recommendation + is_flat=is_flat, measures=measures, + has_flat_roof_recommendation=has_flat_roof_recommendation, + primary_roof_is_sloped=primary_roof_is_sloped ) needs_rir_insulation = self.is_room_roof_insulation_appropriate( is_room_roof=is_room_roof, @@ -784,15 +773,31 @@ class RoofRecommendations: self.recommendations = recommendations - def recommend_sloping_ceiling(self, phase: int, u_value, sloping_ceiling_recommendation: dict = None): + def recommend_sloping_ceiling(self, phase: int, u_value, non_invasive_recommendations: List[Mapping[str, Any]]): """ - Recommend insulation for a sloping ceiling + Sloping ceiling insulation recommendations are different from other roof types, though + the description of the roof appears to be quite similar to a roof with a loft. In order to + deduce the roof type, we apply the following logic: + + 1) If the roof is descrbed as pitched, insulated, without a loft insulation thickness, it's + an insulated sloped ceiling + 2) If the roof insulation is assumed, it implies that the surveyor could not gain access to the + roof and therefore it's a loft + 3) If it's a pitched roof that is uninsulated and is NOT assumed, and there is not loft insulation + recommendation, this implies that the surveyor was able to gain access to the roof and there was no + loft insulation recommendation so it must be a sloping ceiling since loft insulation is a default + recommendation for an uninsualted loft + Since we don't have any materials from installers for this specific recommendation, we do not iterate through any materials. Instead, we provide a single recommendation, we estimated prices based on desk research. :return: """ + sloping_ceiling_recommendation = next( + (x for x in non_invasive_recommendations if ["type"] == "sloping_ceiling_insulation"), {} + ) + new_description = "Pitched, insulated" new_efficiency = "Good" @@ -811,7 +816,7 @@ class RoofRecommendations: } cost_result = self.costs.sloping_ceiling_insulation( - roof_area=self.property.roof_area # For a pitched roof, this is the pitched roof area + insulation_roof_area=self.property.roof_area # For a pitched roof, this is the pitched roof area ) self.recommendations = [ diff --git a/recommendations/tests/test_roof_recommendations.py b/recommendations/tests/test_roof_recommendations.py index b8cea10b..ef02d050 100644 --- a/recommendations/tests/test_roof_recommendations.py +++ b/recommendations/tests/test_roof_recommendations.py @@ -1,8 +1,8 @@ +import pytest from backend.Property import Property +from etl.epc.Record import EPCRecord from recommendations.RoofRecommendations import RoofRecommendations from recommendations.tests.test_data.materials import materials -from etl.epc.Record import EPCRecord -import pytest class TestRoofRecommendations: @@ -405,40 +405,38 @@ class TestRoofRecommendations: assert not roof_recommender14.recommendations # ~~~~~~~~~~~~ Sloping Ceiling Insulation ~~~~~~~~~~~~ - @pytest.mark.parameterize("roof", - [ - ( - # For this example, the roof is pitched, without insulation and the description - # isn't assumed - {'original_description': 'Pitched, no insulation', 'thermal_transmittance': None, - 'thermal_transmittance_unit': None, - 'is_pitched': True, 'is_roof_room': False, 'is_loft': False, 'is_flat': False, - 'is_thatched': False, - 'is_at_rafters': False, 'is_assumed': False, 'has_dwelling_above': False, - 'is_valid': True, - 'insulation_thickness': 'none'} - ) - ] - ) - def test_sloping_ceiling_valid(self, roof): - # All conditions are met and therefore we should produce a sloping ceiling recommendation + @pytest.mark.parametrize( + "roof, has_sloping_ceiling_recommendation, primary_roof_is_sloped, expected_result", + [ + ( + { + 'original_description': 'Pitched, no insulation', + 'thermal_transmittance': None, + 'thermal_transmittance_unit': None, + 'is_pitched': True, + 'is_roof_room': False, + 'is_loft': False, + 'is_flat': False, + 'is_thatched': False, + 'is_at_rafters': False, + 'is_assumed': False, + 'has_dwelling_above': False, + 'is_valid': True, + 'insulation_thickness': 'none' + }, + True, + True, + True, + ) + ] + ) + def test_sloping_ceiling_valid( + self, roof, has_sloping_ceiling_recommendation, primary_roof_is_sloped, expected_result + ): assert RoofRecommendations.is_sloping_ceiling_appropriate( - is_pitched=True, is_loft=False, is_assumed=False, has_sloping_ceiling_recommendation=True - ) - - # One condition not met - we cannot verify - assert not RoofRecommendations.is_sloping_ceiling_appropriate( - is_pitched=True, is_loft=True, is_assumed=False, has_sloping_ceiling_recommendation=True - ) - assert not RoofRecommendations.is_sloping_ceiling_appropriate( - is_pitched=False, is_loft=False, is_assumed=False, has_sloping_ceiling_recommendation=True, - primary_roof_is_sloped=True - ) - assert not RoofRecommendations.is_sloping_ceiling_appropriate( - is_pitched=True, is_loft=False, is_assumed=True, has_sloping_ceiling_recommendation=True, - primary_roof_is_sloped=True - ) - assert not RoofRecommendations.is_sloping_ceiling_appropriate( - is_pitched=True, is_loft=False, is_assumed=True, has_sloping_ceiling_recommendation=True, - primary_roof_is_sloped=True - ) + is_pitched=roof["is_pitched"], + is_loft=roof["is_loft"], + is_assumed=roof["is_assumed"], + has_sloping_ceiling_recommendation=has_sloping_ceiling_recommendation, + primary_roof_is_sloped=primary_roof_is_sloped + ) == expected_result From b81b04a6b891a03b93ce6e6a510bbbd4e65c93f8 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 27 Jan 2026 08:43:58 +0000 Subject: [PATCH 43/68] added initial test --- recommendations/RoofRecommendations.py | 10 ++-- .../tests/test_roof_recommendations.py | 50 ++++++++++++++++++- .../tests/test_wall_recommendations.py | 2 - 3 files changed, 56 insertions(+), 6 deletions(-) diff --git a/recommendations/RoofRecommendations.py b/recommendations/RoofRecommendations.py index 06ff306b..d7eb5fbb 100644 --- a/recommendations/RoofRecommendations.py +++ b/recommendations/RoofRecommendations.py @@ -410,7 +410,12 @@ class RoofRecommendations: # ~~~~~ Sloping Ceiling Insulation Recommendation Logic ~~~~~ #################################################################################################### if needs_sloping_ceiling: - self.recommend_sloping_ceiling() + self.recommend_sloping_ceiling( + phase=phase, + u_value=u_value, + non_invasive_recommendations=non_invasive_recommendations + ) + return raise NotImplementedError("Implement me") @@ -453,7 +458,6 @@ class RoofRecommendations: could be traditional roofing materials like bitumen-based felt, rubber membranes like EPDM, or fiberglass. :param u_value: U-value of the roof before any retrofit measures have been installed - :param insulation_thickness: Existing Insulation thickness of the loft :param phase: Phase of the recommendation :param is_pitched: Is the roof pitched :param is_flat: Is the roof flat @@ -799,7 +803,7 @@ class RoofRecommendations: ) new_description = "Pitched, insulated" - new_efficiency = "Good" + new_efficiency = "Average" # 75mm insulation only results in average performance category roof_ending_config = RoofAttributes(new_description).process() roof_simulation_config = check_simulation_difference( diff --git a/recommendations/tests/test_roof_recommendations.py b/recommendations/tests/test_roof_recommendations.py index ef02d050..aa11fd28 100644 --- a/recommendations/tests/test_roof_recommendations.py +++ b/recommendations/tests/test_roof_recommendations.py @@ -1,4 +1,5 @@ import pytest +from unittest.mock import Mock from backend.Property import Property from etl.epc.Record import EPCRecord from recommendations.RoofRecommendations import RoofRecommendations @@ -430,7 +431,7 @@ class TestRoofRecommendations: ) ] ) - def test_sloping_ceiling_valid( + def test_is_sloping_ceiling_appropriate( self, roof, has_sloping_ceiling_recommendation, primary_roof_is_sloped, expected_result ): assert RoofRecommendations.is_sloping_ceiling_appropriate( @@ -440,3 +441,50 @@ class TestRoofRecommendations: has_sloping_ceiling_recommendation=has_sloping_ceiling_recommendation, primary_roof_is_sloped=primary_roof_is_sloped ) == expected_result + + def test_sloping_ceiling_pitched_no_insulation(self): + property_instance = Mock( + id=0, + roof={ + 'original_description': 'Pitched, no insulation', 'clean_description': 'Pitched, no insulation', + 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': True, + 'is_roof_room': False, 'is_loft': False, 'is_flat': False, 'is_thatched': False, + 'is_at_rafters': False, 'is_assumed': False, 'has_dwelling_above': False, 'is_valid': True, + 'insulation_thickness': 'none' + }, + roof_area=64.085, + data={"county": None, "local-authority-label": "Manchester"} + ) + property_instance.age_band = "D" + property_instance.already_installed = [] + property_instance.non_invasive_recommendations = [ + {'type': 'flat_roof_insulation', 'sap_points': 9, 'survey': True}, + {'type': 'sloping_ceiling_insulation', 'sap_points': 9, 'survey': True}, + {'type': 'cavity_wall_insulation', 'sap_points': 6, 'survey': True}, + {'type': 'suspended_floor_insulation', 'sap_points': 2, 'survey': True}, + {'type': 'roomstat_programmer_trvs', 'sap_points': 3, 'survey': True}, + {'type': 'time_temperature_zone_control', 'sap_points': 3, 'survey': True}, + {'type': 'solar_pv', 'sap_points': 5, 'survey': True, 'suitable': True} + ] + + roof_recommender = RoofRecommendations(property_instance=property_instance, materials=[]) + assert not roof_recommender.recommendations + + roof_recommender.recommend(phase=0) + assert len(roof_recommender.recommendations) == 1 + + assert roof_recommender.recommendations[0]["type"] == "sloping_ceiling_insulation" + assert roof_recommender.recommendations[0]["measure_type"] == "sloping_ceiling_insulation" + assert ( + roof_recommender.recommendations[0]["description"] == + "Insulate sloping ceilings at the rafters and re-decorate" + ) + assert roof_recommender.recommendations[0]["simulation_config"] == { + 'roof_insulation_thickness_ending': 'average', + 'roof_thermal_transmittance_ending': 0.5, + 'roof_energy_eff_ending': 'Average' + } + + assert roof_recommender.recommendations[0]["description_simulation"] == { + 'roof-description': 'Pitched, insulated', 'roof-energy-eff': 'Good' + } diff --git a/recommendations/tests/test_wall_recommendations.py b/recommendations/tests/test_wall_recommendations.py index 18560118..c54582ad 100644 --- a/recommendations/tests/test_wall_recommendations.py +++ b/recommendations/tests/test_wall_recommendations.py @@ -1,6 +1,4 @@ -import os import pytest -import pickle import numpy as np from unittest.mock import Mock, MagicMock From 59c92574a3745196ffdd6a52fc39aa0c0fcdcbeb Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 27 Jan 2026 08:46:52 +0000 Subject: [PATCH 44/68] added costs unit test --- recommendations/tests/test_costs.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/recommendations/tests/test_costs.py b/recommendations/tests/test_costs.py index 752caf8c..10a63554 100644 --- a/recommendations/tests/test_costs.py +++ b/recommendations/tests/test_costs.py @@ -236,3 +236,11 @@ class TestCosts: ) assert result['total'] == pytest.approx(expected_cost, rel=0.01) + + def test_sloping_ceiling_insulation(self): + mock_property = Mock() + mock_property.data = {"county": "Mansfield"} + costs = Costs(mock_property) + res = costs.sloping_ceiling_insulation(insulation_roof_area=64.085) + assert res["total"] == 5238.713924924947 + assert res["contingency"] == 1362.0656204804861 From 5ab07d69030cb60cd62040ed82d68be3eb516ef5 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 27 Jan 2026 09:07:12 +0000 Subject: [PATCH 45/68] make element keys and values consistent --- backend/condition/domain/element.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/condition/domain/element.py b/backend/condition/domain/element.py index f78f2d52..2f6bac42 100644 --- a/backend/condition/domain/element.py +++ b/backend/condition/domain/element.py @@ -55,7 +55,7 @@ class Element(str, Enum): GARAGE_WALLS = "garage_walls" PARTY_WALL_FIRE_BREAK = "party_wall_fire_break" EXTERNAL_BRICKWORK_POINTING = "external_brickwork_pointing" - INTERNAL_DOWNPIPES_EXTERNAL_AREA = "internal_downpipes_in_external_area" + INTERNAL_DOWNPIPES_EXTERNAL_AREA = "internal_downpipes_external_area" # ====================== # EXTERNAL – WINDOWS @@ -157,7 +157,7 @@ class Element(str, Enum): HEAT_DETECTION = "heat_detection" CARBON_MONOXIDE_DETECTION = "carbon_monoxide_detection" FIRE_DOOR_RATING = "fire_door_rating" - FIRE_RISK_ASSESSMENT = "fire" + FIRE_RISK_ASSESSMENT = "fire_risk_assessment" INTERNAL_WIRING = ( "internal_wiring" # Is this definitely different from ELECTRICAL_WIRING? ) @@ -175,7 +175,7 @@ class Element(str, Enum): COMMUNAL_CCTV = "communal_cctv" COMMUNAL_BIN_STORE = "communal_bin_store" COMMUNAL_BIN_STORE_DOORS = "communal_bin_store_doors" - COMMUNAL_BIN_STORE_WALLS = "communal_bin_store_wall" + COMMUNAL_BIN_STORE_WALLS = "communal_bin_store_walls" COMMUNAL_BIN_STORE_ROOF = "communal_bin_store_roof" COMMUNAL_REFUSE_CHUTE = "communal_refuse_chute" COMMUNAL_FLOOR_COVERING = "communal_floor_covering" @@ -216,11 +216,11 @@ class Element(str, Enum): # ====================== FFHH_DAMP = "ffhh_damp" FFHH_HOT_AND_COLD_WATER = "ffhh_hold_and_cold_water" - FFHH_DRAINAGE_LAVATORIES = "ffhh_drainage_or_lavatories" - FFHH_NEGLECTED = "ffhh_neglected_and_in_bad_condition" + FFHH_DRAINAGE_LAVATORIES = "ffhh_drainage_lavatories" + FFHH_NEGLECTED = "ffhh_neglected" FFHH_NATURAL_LIGHT = "ffhh_natural_light" FFHH_VENTILATION = "ffhh_ventilation" - FFHH_FOOD_PREP_AND_WASHUP = "ffhh_prepare_and_cook_food_or_wash_up" + FFHH_FOOD_PREP_AND_WASHUP = "ffhh_food_prep_and_washup" FFHH_UNSAFE_LAYOUT = "ffhh_unsafe_layout" FFHH_UNSTABLE_BUILDING = "ffhh_unstable_building" From f8937c790aa98a3b63b071934699c5ef10f786c4 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 27 Jan 2026 09:29:56 +0000 Subject: [PATCH 46/68] added counter example --- recommendations/tests/test_roof_recommendations.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/recommendations/tests/test_roof_recommendations.py b/recommendations/tests/test_roof_recommendations.py index aa11fd28..7bafdffa 100644 --- a/recommendations/tests/test_roof_recommendations.py +++ b/recommendations/tests/test_roof_recommendations.py @@ -428,6 +428,18 @@ class TestRoofRecommendations: True, True, True, + ), + ( + { + 'original_description': 'Pitched, insulated (assumed)', 'clean_description': 'Pitched, insulated', + 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': True, + 'is_roof_room': False, 'is_loft': False, 'is_flat': False, 'is_thatched': False, + 'is_at_rafters': False, 'is_assumed': True, 'has_dwelling_above': False, 'is_valid': True, + 'insulation_thickness': 'average' + }, + False, + False, + False ) ] ) From f123a7ab8998ac200d68d80247ddf97b70a3999f Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 27 Jan 2026 09:31:18 +0000 Subject: [PATCH 47/68] expected performance for sloping ceiling updated to good --- recommendations/tests/test_roof_recommendations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/recommendations/tests/test_roof_recommendations.py b/recommendations/tests/test_roof_recommendations.py index 7bafdffa..e797892d 100644 --- a/recommendations/tests/test_roof_recommendations.py +++ b/recommendations/tests/test_roof_recommendations.py @@ -498,5 +498,5 @@ class TestRoofRecommendations: } assert roof_recommender.recommendations[0]["description_simulation"] == { - 'roof-description': 'Pitched, insulated', 'roof-energy-eff': 'Good' + 'roof-description': 'Pitched, insulated', 'roof-energy-eff': 'Average' } From 9509306e3d8621f5346daabdf94c5fa70cc552e3 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 27 Jan 2026 13:08:35 +0000 Subject: [PATCH 48/68] =?UTF-8?q?Add=20aspect=20instance=20to=20asset=20co?= =?UTF-8?q?ndition=20and=20modify=20how=20peabody=20walls=20are=20mapped?= =?UTF-8?q?=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/condition/domain/asset_condition.py | 4 +- .../domain/mapping/element_mapping.py | 1 + .../mapping/peabody/peabody_element_map.py | 2 +- .../tests/mapping/test_peabody_mapper.py | 114 ++++++++++++++++++ 4 files changed, 118 insertions(+), 3 deletions(-) diff --git a/backend/condition/domain/asset_condition.py b/backend/condition/domain/asset_condition.py index 1b157a6b..8b054f45 100644 --- a/backend/condition/domain/asset_condition.py +++ b/backend/condition/domain/asset_condition.py @@ -13,6 +13,8 @@ class AssetCondition: element: Element aspect_type: AspectType + element_instance: Optional[int] = None + aspect_instance: Optional[int] = None value: Optional[str] = None @@ -20,7 +22,5 @@ class AssetCondition: install_date: Optional[date] = None renewal_year: Optional[int] = None - element_instance: Optional[int] = None - source_system: Optional[str] = None comments: Optional[str] = None diff --git a/backend/condition/domain/mapping/element_mapping.py b/backend/condition/domain/mapping/element_mapping.py index 01e1f316..c93862c8 100644 --- a/backend/condition/domain/mapping/element_mapping.py +++ b/backend/condition/domain/mapping/element_mapping.py @@ -10,3 +10,4 @@ class ElementMapping: element: Element aspect_type: AspectType element_instance: Optional[int] = None + aspect_instance: Optional[int] = None diff --git a/backend/condition/domain/mapping/peabody/peabody_element_map.py b/backend/condition/domain/mapping/peabody/peabody_element_map.py index 2485136b..1f9cceee 100644 --- a/backend/condition/domain/mapping/peabody/peabody_element_map.py +++ b/backend/condition/domain/mapping/peabody/peabody_element_map.py @@ -58,7 +58,7 @@ PEABODY_ELEMENT_MAP = { element=Element.GARAGE_WALLS, aspect_type=AspectType.MATERIAL ), (53, 23): ElementMapping( - element=Element.PRIMARY_WALL, aspect_type=AspectType.FINISH + element=Element.EXTERNAL_WALL, aspect_type=AspectType.FINISH ), (53, 30): ElementMapping( element=Element.SECONDARY_WALL, aspect_type=AspectType.FINISH diff --git a/backend/condition/tests/mapping/test_peabody_mapper.py b/backend/condition/tests/mapping/test_peabody_mapper.py index a975a308..9997dfa8 100644 --- a/backend/condition/tests/mapping/test_peabody_mapper.py +++ b/backend/condition/tests/mapping/test_peabody_mapper.py @@ -90,3 +90,117 @@ def test_peabody_mapper_maps_property(): for i, (actual, expected) in enumerate(zip(actual_assets, expected_assets)): assert actual == expected, f"Mismatch at index {i}" + + +def test_wall_primary_and_secondary_wall_finish_map_correctly(): + # arrange + peabody_property = PeabodyProperty( + uprn=1, + assets=[ + PeabodyAssetCondition( + lo_reference="1000RAND0000", + full_address="FLAT 1 RANDOM SQUARE FAKE STREET LONDON E1 1EE", + location_type_code=1, + parent_lo_reference="RAND1000", + element_code=53, + element="External", + sub_element_code=23, + sub_element="Primary Wall Finish", + material_code=4, + material_or_answer="Pointed", + renewal_quantity=65, + renewal_year=2045, + renewal_cost=3835, + cloned="N", + lo_type_code=1, + condition_survey_date=datetime(2024, 2, 15, 12, 47, 0), + ), + PeabodyAssetCondition( + lo_reference="1000RAND0000", + full_address="FLAT 1 RANDOM SQUARE FAKE STREET LONDON E1 1EE", + location_type_code=1, + parent_lo_reference="RAND1000", + element_code=120, + element="WALLS", + sub_element_code=2, + sub_element="Wall Finish", + material_code=1, + material_or_answer="Pointing", + renewal_quantity=1, + renewal_year=2069, + renewal_cost=2450, + cloned="N", + lo_type_code=1, + condition_survey_date=datetime(2014, 2, 15, 12, 47, 0), + ), + PeabodyAssetCondition( + lo_reference="1000RAND0000", + full_address="FLAT 1 RANDOM SQUARE FAKE STREET LONDON E1 1EE", + location_type_code=1, + parent_lo_reference="RAND1000", + element_code=53, + element="External", + sub_element_code=30, + sub_element="Secondary Wall Finish", + material_code=8, + material_or_answer="Tile Hung", + renewal_quantity=8, + renewal_year=2049, + renewal_cost=472, + cloned="N", + lo_type_code=1, + condition_survey_date=datetime(2014, 2, 15, 12, 47, 0), + ), + ], + ) + mapper = PeabodyMapper() + + expected_assets: List[AssetCondition] = [ + AssetCondition( + uprn=1, + element=Element.EXTERNAL_WALLS, + aspect_type=AspectType.FINISH, + value="Pointed", + element_instance=1, + aspect_instance=1, + quantity=65, + install_date=None, + renewal_year=2045, + source_system=None, + comments=None, + ), + AssetCondition( + uprn=1, + element=Element.EXTERNAL_WALLS, + aspect_type=AspectType.FINISH, + value="Pointing", + element_instance=1, + aspect_instance=1, + quantity=1, + install_date=None, + renewal_year=2069, + source_system=None, + comments=None, + ), + AssetCondition( + uprn=1, + element=Element.EXTERNAL_WALLS, + aspect_type=AspectType.FINISH, + value="Tile Hung", + element_instance=1, + aspect_instance=2, + quantity=8, + install_date=None, + renewal_year=2049, + source_system=None, + comments=None, + ), + ] + # act + actual_assets = mapper.map_asset_conditions_for_property(peabody_property) + + # assert + assert len(actual_assets) == len(expected_assets) + + for i, (actual, expected) in enumerate(zip(actual_assets, expected_assets)): + assert actual == expected, f"Mismatch at index {i}" From 6a2bb26baeacee877bcd1477925ac0d4acb0d9a2 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 27 Jan 2026 13:29:06 +0000 Subject: [PATCH 49/68] Redefine conditions data structures. Mapping tests broken --- ...asset_condition.py => aspect_condition.py} | 15 +- backend/condition/domain/element.py | 269 +------------- backend/condition/domain/element_type.py | 261 ++++++++++++++ .../domain/mapping/lbwf/lbwf_element_map.py | 242 ++++++------- .../domain/mapping/lbwf/lbwf_mapper.py | 7 +- backend/condition/domain/mapping/mapper.py | 4 +- .../mapping/peabody/peabody_element_map.py | 330 +++++++++--------- .../domain/mapping/peabody/peabody_mapper.py | 8 +- .../domain/property_condition_survey.py | 14 + backend/condition/processor.py | 4 +- .../tests/mapping/test_lbwf_mapper.py | 44 +-- .../tests/mapping/test_peabody_mapper.py | 28 +- 12 files changed, 623 insertions(+), 603 deletions(-) rename backend/condition/domain/{asset_condition.py => aspect_condition.py} (57%) create mode 100644 backend/condition/domain/element_type.py create mode 100644 backend/condition/domain/property_condition_survey.py diff --git a/backend/condition/domain/asset_condition.py b/backend/condition/domain/aspect_condition.py similarity index 57% rename from backend/condition/domain/asset_condition.py rename to backend/condition/domain/aspect_condition.py index 8b054f45..75b46b09 100644 --- a/backend/condition/domain/asset_condition.py +++ b/backend/condition/domain/aspect_condition.py @@ -1,26 +1,17 @@ from dataclasses import dataclass -from datetime import date from typing import Optional -from xml.dom.minidom import Element +from datetime import date from backend.condition.domain.aspect_type import AspectType -from backend.condition.domain.element import Element @dataclass -class AssetCondition: - uprn: int - - element: Element +class AspectCondition: aspect_type: AspectType - element_instance: Optional[int] = None - aspect_instance: Optional[int] = None + aspect_instance: int value: Optional[str] = None - quantity: Optional[int] = None install_date: Optional[date] = None renewal_year: Optional[int] = None - - source_system: Optional[str] = None comments: Optional[str] = None diff --git a/backend/condition/domain/element.py b/backend/condition/domain/element.py index 2f6bac42..7aca11fd 100644 --- a/backend/condition/domain/element.py +++ b/backend/condition/domain/element.py @@ -1,261 +1,12 @@ -from enum import Enum +from dataclasses import dataclass +from typing import List + +from backend.condition.domain.aspect_condition import AspectCondition +from backend.condition.domain.element_type import ElementType -class Element(str, Enum): - - # ====================== - # PROPERTY / GENERAL - # ====================== - PROPERTY = "property" - PROPERTY_CONSTRUCTION_TYPE = "property_construction_type" - PROPERTY_CLASSIFICATION = "property_classification" - PROPERTY_AGE_BAND = "property_age_band" - STOREY_COUNT = "storey_count" - FLOOR_LEVEL = "floor_level" - FLOOR_LEVEL_FRONT_DOOR = "floor_level_front_door" - ACCESSIBLE_HOUSING_REGISTER = "accessible_housing_register" - ASBESTOS = "asbestos" - QUALITY_STANDARD = "quality_standard" - CCU = "ccu" - PASSENGER_LIFT = "passenger_lift" - STAIRLIFT = "stairlift" - DISABLED_HOIST_TRACKING = "disabled_hoist_tracking" - DISABLED_FACILITIES = "disabled_facilities" - STEPS_TO_FRONT_DOOR = "steps_to_front_door" - - # ====================== - # EXTERNAL – ROOF - # ====================== - ROOF = "roof" - PITCHED_ROOF_COVERING = "pitched_roof_covering" - FLAT_ROOF_COVERING = "flat_roof_covering" - RAINWATER_GOODS = "rainwater_goods" - LOFT_INSULATION = "loft_insulation" - PORCH_CANOPY = "porch_canopy" - CHIMNEY = "chimney" - FASCIA = "fascia" - SOFFIT = "soffit" - FASCIA_SOFFIT_BARGEBOARDS = "fascia_soffit_bargeboards" - GUTTERS = "gutters" - STORE_ROOF = "store_roof" - GARAGE_ROOF = "garage_roof" - GARAGE_AND_STORE_ROOF = "garage_and_store_roof" - - # ====================== - # EXTERNAL – WALLS - # ====================== - EXTERNAL_WALL = "external_wall" - EXTERNAL_NOISE_INSULATION = "external_noise_insulation" - PRIMARY_WALL = "primary_wall" - SECONDARY_WALL = "secondary_wall" - DOWNPIPES = "downpipes" - EXTERNAL_DECORATION = "external_decoration" - CLADDING = "cladding" - SPANDREL_PANELS = "spandrel_panels" - GARAGE_WALLS = "garage_walls" - PARTY_WALL_FIRE_BREAK = "party_wall_fire_break" - EXTERNAL_BRICKWORK_POINTING = "external_brickwork_pointing" - INTERNAL_DOWNPIPES_EXTERNAL_AREA = "internal_downpipes_external_area" - - # ====================== - # EXTERNAL – WINDOWS - # ====================== - EXTERNAL_WINDOWS = "external_windows" - COMMUNAL_WINDOWS = "communal_windows" - SECONDARY_GLAZING = "secondary_glazing" - STORE_WINDOWS = "store_windows" - GARAGE_WINDOWS = "garage_windows" - GARAGE_AND_STORE_WINDOWS = "garage_and_store_windows" - - # ====================== - # EXTERNAL – DOORS - # ====================== - EXTERNAL_DOOR = "external_door" - FRONT_DOOR = "front_door" - REAR_DOOR = "rear_door" - STORE_DOOR = "store_door" - GARAGE_DOOR = "garage_door" - GARAGE_AND_STORE_DOOR = "garage_and_store_door" - COMMUNAL_ENTRANCE_DOOR = "communal_entrance_door" - MAIN_DOOR = "main_door" - BLOCK_ENTRANCE_DOOR = "block_entrance_door" - LINTEL = "lintel" - PATIO_FRENCH_DOOR = "patio_french_door" - DOOR_ENTRY_HANDSET = "door_entry_handset" - - # ====================== - # EXTERNAL – AREAS - # ====================== - PATHS_AND_HARDSTANDINGS = "paths_and_hardstandings" - PARKING_AREAS = "parking_areas" - BOUNDARY_WALLS = "boundary_walls" - FRONT_FENCING = "front_fencing" - REAR_FENCING = "rear_fencing" - SIDE_FENCING = "side_fencing" - REAR_GATE = "rear_gate" - FRONT_GATE = "front_gate" - GATES = "gates" - RETAINING_WALLS = "retaining_walls" - PRIVATE_BALCONY = "private_balcony" - BALCONY_BALUSTRADE = "balcony_balustrade" - OUTBUILDINGS = "outbuildings" - GARAGE_STRUCTURE = "garage_structure" - PAVING = "paving" - ROADS = "roads" - SOIL_AND_VENT = "soil_and_vent" - SOLAR_THERMALS = "solar_thermals" - DROP_KERB = "drop_kerb" - OUTBUILDING_OVERHAUL = "outbuilding_overhaul" - EXTERNAL_STRUCTURAL_DEFECTS = "external_structural_defects" - ACCESS_RAMP = "access_ramp" - - # ====================== - # INTERNAL – KITCHEN - # ====================== - KITCHEN = "kitchen" - KITCHEN_SPACE_LAYOUT = "kitchen_space_layout" - TENANT_INSTALLED_KITCHEN = "tenant_installed_kitchen" - KITCHEN_EXTRACTOR_FAN = "kitchen_extractor_fan" - - # ====================== - # INTERNAL – BATHROOM - # ====================== - BATHROOM = "bathroom" - SECONDARY_BATHROOM = "secondary_bathroom" - SECONDARY_TOILET = "secondary_toilet" - BATHROOM_EXTRACTOR_FAN = "bathroom_extractor_fan" - ADDITIONAL_WC_OR_WHB = "additional_wc_or_whb" - BATHROOM_REMAINING_LIFE_SOURCE = "bathroom_remaining_life_source" - KITCHEN_REMAINING_LIFE_SOURCE = "kitchen_remaining_life_source" - - # ====================== - # INTERNAL – HEATING / WATER - # ====================== - CENTRAL_HEATING = "central_heating" - HEATING_BOILER = "heating_boiler" - HEATING_DISTRIBUTION = "heating_distribution" - SECONDARY_HEATING = "secondary_heating" - HOT_WATER_SYSTEM = "hot_water_system" - COLD_WATER_STORAGE = "cold_water_storage" - HEATING_SYSTEM = "heating_system" - BOILER_FUEL = "boiler_fuel" - WATER_HEATING = "water_heating" - PROGRAMMABLE_HEATING = "programmable_heating" - COMMUNITY_HEATING = ( - "community_heating" # Is this definitely different from COMMUNAL_HEATING? - ) - GAS_AVAILABLE = "gas_available" - HEAT_RECOVERY_UNITS = "heat_recovery_units" - HEATING_IMPROVEMENTS = "heating_improvements" - - # ====================== - # INTERNAL – ELECTRICS / FIRE - # ====================== - ELECTRICAL_WIRING = "electrical_wiring" - CONSUMER_UNIT = "consumer_unit" - SMOKE_DETECTION = "smoke_detection" - HEAT_DETECTION = "heat_detection" - CARBON_MONOXIDE_DETECTION = "carbon_monoxide_detection" - FIRE_DOOR_RATING = "fire_door_rating" - FIRE_RISK_ASSESSMENT = "fire_risk_assessment" - INTERNAL_WIRING = ( - "internal_wiring" # Is this definitely different from ELECTRICAL_WIRING? - ) - ELECTRICS = "electrics" - - # ====================== - # COMMUNAL - # ====================== - COMMUNAL_HEATING = "communal_heating" - COMMUNAL_BOILER = "communal_boiler" - COMMUNAL_ELECTRICS = "communal_electrics" - COMMUNAL_FIRE_ALARM = "communal_fire_alarm" - COMMUNAL_EMERGENCY_LIGHTING = "communal_emergency_lighting" - COMMUNAL_DOOR_ENTRY = "communal_door_entry" - COMMUNAL_CCTV = "communal_cctv" - COMMUNAL_BIN_STORE = "communal_bin_store" - COMMUNAL_BIN_STORE_DOORS = "communal_bin_store_doors" - COMMUNAL_BIN_STORE_WALLS = "communal_bin_store_walls" - COMMUNAL_BIN_STORE_ROOF = "communal_bin_store_roof" - COMMUNAL_REFUSE_CHUTE = "communal_refuse_chute" - COMMUNAL_FLOOR_COVERING = "communal_floor_covering" - COMMUNAL_KITCHEN = "communal_kitchen" - COMMUNAL_BATHROOM = "communal_bathroom" - COMMUNAL_TOILETS = "communal_toilets" - COMMUNAL_GATES = "communal_gates" - COMMUNAL_LIFT = "communal_lift" - COMMUNAL_PASSENGER_LIFT = "communal_passenger_lift" - COMMUNAL_BALCONY_WALKWAY = "communal_balcony_walkway" - COMMUNAL_ENTRANCE = "communal_entrance" - COMMUNAL_INTERNAL_DECORATIONS = "communal_internal_decorations" - COMMUNAL_INTERNAL_FLOOR = "communal_internal_floor" - COMMUNAL_WALKWAYS = "communal_walkways" - COMMUNAL_EXTERNAL_DOORS = "communal_external_doors" - COMMUNAL_STAIRS = "communal_stairs" - COMMUNAL_AERIAL = "communal_aerial" - COMMUNAL_AOV = "communal_aov" - COMMUNAL_INTERNAL_DOORS = "communal_internal_doors" - COMMUNAL_LATERAL_MAINS = "communal_lateral_mains" - COMMUNAL_LIGHTING = "communal_lighting" - COMMUNAL_LIGHTING_CONDUCTOR = "communal_lighting_conductor" - COMMUNAL_STORE_ROOF = "communal_store_roof" - COMMUNAL_STORE_WALLS = "communal_store_walls" - COMMUNAL_STORE_DOORS = "communal_store_doors" - COMMUNAL_WARDEN_CALL_SYSTEM = "communal_warden_call_system" - COMMUNAL_BMS = "communal_bms" - COMMUNAL_BOOSTER_PUMP = "communal_booster_pump" - COMMUNAL_DRY_RISER = "communal_dry_riser" - COMMUNAL_WET_RISER = "communal_wet_riser" - COMMUNAL_COLD_WATER_STORAGE = "communal_cold_water_storage" - COMMUNAL_SPRINKLER = "communal_sprinkler" - COMMUNAL_PLUG_SOCKETS = "communal_plug_sockets" - COMMUNAL_CIRCULATION_SPACE = "communal_circulation_space" - - # ====================== - # FITNESS FOR HUMAN HABITATION - # ====================== - FFHH_DAMP = "ffhh_damp" - FFHH_HOT_AND_COLD_WATER = "ffhh_hold_and_cold_water" - FFHH_DRAINAGE_LAVATORIES = "ffhh_drainage_lavatories" - FFHH_NEGLECTED = "ffhh_neglected" - FFHH_NATURAL_LIGHT = "ffhh_natural_light" - FFHH_VENTILATION = "ffhh_ventilation" - FFHH_FOOD_PREP_AND_WASHUP = "ffhh_food_prep_and_washup" - FFHH_UNSAFE_LAYOUT = "ffhh_unsafe_layout" - FFHH_UNSTABLE_BUILDING = "ffhh_unstable_building" - - # ========================================================== - # HHSRS – ALL 29 HAZARDS - # ========================================================== - - HHSRS_DAMP_AND_MOULD = "hhsrs_damp_and_mould" - HHSRS_EXCESS_COLD = "hhsrs_excess_cold" - HHSRS_EXCESS_HEAT = "hhsrs_excess_heat" - HHSRS_ASBESTOS_AND_MMF = "hhsrs_asbestos_and_mmf" - HHSRS_BIOCIDES = "hhsrs_biocides" - HHSRS_CARBON_MONOXIDE = "hhsrs_carbon_monoxide" - HHSRS_LEAD = "hhsrs_lead" - HHSRS_RADIATION = "hhsrs_radiation" - HHSRS_UNCOMBUSTED_FUEL_GAS = "hhsrs_uncombusted_fuel_gas" - HHSRS_VOLATILE_ORGANIC_COMPOUNDS = "hhsrs_volatile_organic_compounds" - HHSRS_CROWDING_AND_SPACE = "hhsrs_crowding_and_space" - HHSRS_ENTRY_BY_INTRUDERS = "hhsrs_entry_by_intruders" - HHSRS_LIGHTING = "hhsrs_lighting" - HHSRS_NOISE = "hhsrs_noise" - HHSRS_DOMESTIC_HYGIENE_PESTS_REFUSE = "hhsrs_domestic_hygiene_pests_refuse" - HHSRS_FOOD_SAFETY = "hhsrs_food_safety" - HHSRS_PERSONAL_HYGIENE_SANITATION = "hhsrs_personal_hygiene_sanitation" - HHSRS_WATER_SUPPLY = "hhsrs_water_supply" - HHSRS_FALLS_ASSOCIATED_WITH_BATHS = "hhsrs_falls_associated_with_baths" - HHSRS_FALLS_ON_LEVEL_SURFACES = "hhsrs_falls_on_level_surfaces" - HHSRS_FALLS_ON_STAIRS = "hhsrs_falls_on_stairs" - HHSRS_FALLS_BETWEEN_LEVELS = "hhsrs_falls_between_levels" - HHSRS_ELECTRICAL_HAZARDS = "hhsrs_electrical_hazards" - HHSRS_FIRE = "hhsrs_fire" - HHSRS_FLAMES_HOT_SURFACES = "hhsrs_flames_hot_surfaces" - HHSRS_COLLISION_AND_ENTRAPMENT = "hhsrs_collision_and_entrapment" - HHSRS_COLLISION_HAZARDS_LOW_HEADROOM = "hhsrs_collision_hazards_low_headroom" - HHSRS_EXPLOSIONS = "hhsrs_explosions" - HHSRS_ERGONOMICS = "hhsrs_ergonomics" - HHSRS_STRUCTURAL_COLLAPSE = "hhsrs_structural_collapse" - HHSRS_AMENITIES = "hhsrs_amenities" +@dataclass +class Element: + element: ElementType + element_instance: int + aspect_conditions: List[AspectCondition] diff --git a/backend/condition/domain/element_type.py b/backend/condition/domain/element_type.py new file mode 100644 index 00000000..32897895 --- /dev/null +++ b/backend/condition/domain/element_type.py @@ -0,0 +1,261 @@ +from enum import Enum + + +class ElementType(str, Enum): + + # ====================== + # PROPERTY / GENERAL + # ====================== + PROPERTY = "property" + PROPERTY_CONSTRUCTION_TYPE = "property_construction_type" + PROPERTY_CLASSIFICATION = "property_classification" + PROPERTY_AGE_BAND = "property_age_band" + STOREY_COUNT = "storey_count" + FLOOR_LEVEL = "floor_level" + FLOOR_LEVEL_FRONT_DOOR = "floor_level_front_door" + ACCESSIBLE_HOUSING_REGISTER = "accessible_housing_register" + ASBESTOS = "asbestos" + QUALITY_STANDARD = "quality_standard" + CCU = "ccu" + PASSENGER_LIFT = "passenger_lift" + STAIRLIFT = "stairlift" + DISABLED_HOIST_TRACKING = "disabled_hoist_tracking" + DISABLED_FACILITIES = "disabled_facilities" + STEPS_TO_FRONT_DOOR = "steps_to_front_door" + + # ====================== + # EXTERNAL – ROOF + # ====================== + ROOF = "roof" + PITCHED_ROOF_COVERING = "pitched_roof_covering" + FLAT_ROOF_COVERING = "flat_roof_covering" + RAINWATER_GOODS = "rainwater_goods" + LOFT_INSULATION = "loft_insulation" + PORCH_CANOPY = "porch_canopy" + CHIMNEY = "chimney" + FASCIA = "fascia" + SOFFIT = "soffit" + FASCIA_SOFFIT_BARGEBOARDS = "fascia_soffit_bargeboards" + GUTTERS = "gutters" + STORE_ROOF = "store_roof" + GARAGE_ROOF = "garage_roof" + GARAGE_AND_STORE_ROOF = "garage_and_store_roof" + + # ====================== + # EXTERNAL – WALLS + # ====================== + EXTERNAL_WALL = "external_wall" + EXTERNAL_NOISE_INSULATION = "external_noise_insulation" + PRIMARY_WALL = "primary_wall" + SECONDARY_WALL = "secondary_wall" + DOWNPIPES = "downpipes" + EXTERNAL_DECORATION = "external_decoration" + CLADDING = "cladding" + SPANDREL_PANELS = "spandrel_panels" + GARAGE_WALLS = "garage_walls" + PARTY_WALL_FIRE_BREAK = "party_wall_fire_break" + EXTERNAL_BRICKWORK_POINTING = "external_brickwork_pointing" + INTERNAL_DOWNPIPES_EXTERNAL_AREA = "internal_downpipes_external_area" + + # ====================== + # EXTERNAL – WINDOWS + # ====================== + EXTERNAL_WINDOWS = "external_windows" + COMMUNAL_WINDOWS = "communal_windows" + SECONDARY_GLAZING = "secondary_glazing" + STORE_WINDOWS = "store_windows" + GARAGE_WINDOWS = "garage_windows" + GARAGE_AND_STORE_WINDOWS = "garage_and_store_windows" + + # ====================== + # EXTERNAL – DOORS + # ====================== + EXTERNAL_DOOR = "external_door" + FRONT_DOOR = "front_door" + REAR_DOOR = "rear_door" + STORE_DOOR = "store_door" + GARAGE_DOOR = "garage_door" + GARAGE_AND_STORE_DOOR = "garage_and_store_door" + COMMUNAL_ENTRANCE_DOOR = "communal_entrance_door" + MAIN_DOOR = "main_door" + BLOCK_ENTRANCE_DOOR = "block_entrance_door" + LINTEL = "lintel" + PATIO_FRENCH_DOOR = "patio_french_door" + DOOR_ENTRY_HANDSET = "door_entry_handset" + + # ====================== + # EXTERNAL – AREAS + # ====================== + PATHS_AND_HARDSTANDINGS = "paths_and_hardstandings" + PARKING_AREAS = "parking_areas" + BOUNDARY_WALLS = "boundary_walls" + FRONT_FENCING = "front_fencing" + REAR_FENCING = "rear_fencing" + SIDE_FENCING = "side_fencing" + REAR_GATE = "rear_gate" + FRONT_GATE = "front_gate" + GATES = "gates" + RETAINING_WALLS = "retaining_walls" + PRIVATE_BALCONY = "private_balcony" + BALCONY_BALUSTRADE = "balcony_balustrade" + OUTBUILDINGS = "outbuildings" + GARAGE_STRUCTURE = "garage_structure" + PAVING = "paving" + ROADS = "roads" + SOIL_AND_VENT = "soil_and_vent" + SOLAR_THERMALS = "solar_thermals" + DROP_KERB = "drop_kerb" + OUTBUILDING_OVERHAUL = "outbuilding_overhaul" + EXTERNAL_STRUCTURAL_DEFECTS = "external_structural_defects" + ACCESS_RAMP = "access_ramp" + + # ====================== + # INTERNAL – KITCHEN + # ====================== + KITCHEN = "kitchen" + KITCHEN_SPACE_LAYOUT = "kitchen_space_layout" + TENANT_INSTALLED_KITCHEN = "tenant_installed_kitchen" + KITCHEN_EXTRACTOR_FAN = "kitchen_extractor_fan" + + # ====================== + # INTERNAL – BATHROOM + # ====================== + BATHROOM = "bathroom" + SECONDARY_BATHROOM = "secondary_bathroom" + SECONDARY_TOILET = "secondary_toilet" + BATHROOM_EXTRACTOR_FAN = "bathroom_extractor_fan" + ADDITIONAL_WC_OR_WHB = "additional_wc_or_whb" + BATHROOM_REMAINING_LIFE_SOURCE = "bathroom_remaining_life_source" + KITCHEN_REMAINING_LIFE_SOURCE = "kitchen_remaining_life_source" + + # ====================== + # INTERNAL – HEATING / WATER + # ====================== + CENTRAL_HEATING = "central_heating" + HEATING_BOILER = "heating_boiler" + HEATING_DISTRIBUTION = "heating_distribution" + SECONDARY_HEATING = "secondary_heating" + HOT_WATER_SYSTEM = "hot_water_system" + COLD_WATER_STORAGE = "cold_water_storage" + HEATING_SYSTEM = "heating_system" + BOILER_FUEL = "boiler_fuel" + WATER_HEATING = "water_heating" + PROGRAMMABLE_HEATING = "programmable_heating" + COMMUNITY_HEATING = ( + "community_heating" # Is this definitely different from COMMUNAL_HEATING? + ) + GAS_AVAILABLE = "gas_available" + HEAT_RECOVERY_UNITS = "heat_recovery_units" + HEATING_IMPROVEMENTS = "heating_improvements" + + # ====================== + # INTERNAL – ELECTRICS / FIRE + # ====================== + ELECTRICAL_WIRING = "electrical_wiring" + CONSUMER_UNIT = "consumer_unit" + SMOKE_DETECTION = "smoke_detection" + HEAT_DETECTION = "heat_detection" + CARBON_MONOXIDE_DETECTION = "carbon_monoxide_detection" + FIRE_DOOR_RATING = "fire_door_rating" + FIRE_RISK_ASSESSMENT = "fire_risk_assessment" + INTERNAL_WIRING = ( + "internal_wiring" # Is this definitely different from ELECTRICAL_WIRING? + ) + ELECTRICS = "electrics" + + # ====================== + # COMMUNAL + # ====================== + COMMUNAL_HEATING = "communal_heating" + COMMUNAL_BOILER = "communal_boiler" + COMMUNAL_ELECTRICS = "communal_electrics" + COMMUNAL_FIRE_ALARM = "communal_fire_alarm" + COMMUNAL_EMERGENCY_LIGHTING = "communal_emergency_lighting" + COMMUNAL_DOOR_ENTRY = "communal_door_entry" + COMMUNAL_CCTV = "communal_cctv" + COMMUNAL_BIN_STORE = "communal_bin_store" + COMMUNAL_BIN_STORE_DOORS = "communal_bin_store_doors" + COMMUNAL_BIN_STORE_WALLS = "communal_bin_store_walls" + COMMUNAL_BIN_STORE_ROOF = "communal_bin_store_roof" + COMMUNAL_REFUSE_CHUTE = "communal_refuse_chute" + COMMUNAL_FLOOR_COVERING = "communal_floor_covering" + COMMUNAL_KITCHEN = "communal_kitchen" + COMMUNAL_BATHROOM = "communal_bathroom" + COMMUNAL_TOILETS = "communal_toilets" + COMMUNAL_GATES = "communal_gates" + COMMUNAL_LIFT = "communal_lift" + COMMUNAL_PASSENGER_LIFT = "communal_passenger_lift" + COMMUNAL_BALCONY_WALKWAY = "communal_balcony_walkway" + COMMUNAL_ENTRANCE = "communal_entrance" + COMMUNAL_INTERNAL_DECORATIONS = "communal_internal_decorations" + COMMUNAL_INTERNAL_FLOOR = "communal_internal_floor" + COMMUNAL_WALKWAYS = "communal_walkways" + COMMUNAL_EXTERNAL_DOORS = "communal_external_doors" + COMMUNAL_STAIRS = "communal_stairs" + COMMUNAL_AERIAL = "communal_aerial" + COMMUNAL_AOV = "communal_aov" + COMMUNAL_INTERNAL_DOORS = "communal_internal_doors" + COMMUNAL_LATERAL_MAINS = "communal_lateral_mains" + COMMUNAL_LIGHTING = "communal_lighting" + COMMUNAL_LIGHTING_CONDUCTOR = "communal_lighting_conductor" + COMMUNAL_STORE_ROOF = "communal_store_roof" + COMMUNAL_STORE_WALLS = "communal_store_walls" + COMMUNAL_STORE_DOORS = "communal_store_doors" + COMMUNAL_WARDEN_CALL_SYSTEM = "communal_warden_call_system" + COMMUNAL_BMS = "communal_bms" + COMMUNAL_BOOSTER_PUMP = "communal_booster_pump" + COMMUNAL_DRY_RISER = "communal_dry_riser" + COMMUNAL_WET_RISER = "communal_wet_riser" + COMMUNAL_COLD_WATER_STORAGE = "communal_cold_water_storage" + COMMUNAL_SPRINKLER = "communal_sprinkler" + COMMUNAL_PLUG_SOCKETS = "communal_plug_sockets" + COMMUNAL_CIRCULATION_SPACE = "communal_circulation_space" + + # ====================== + # FITNESS FOR HUMAN HABITATION + # ====================== + FFHH_DAMP = "ffhh_damp" + FFHH_HOT_AND_COLD_WATER = "ffhh_hold_and_cold_water" + FFHH_DRAINAGE_LAVATORIES = "ffhh_drainage_lavatories" + FFHH_NEGLECTED = "ffhh_neglected" + FFHH_NATURAL_LIGHT = "ffhh_natural_light" + FFHH_VENTILATION = "ffhh_ventilation" + FFHH_FOOD_PREP_AND_WASHUP = "ffhh_food_prep_and_washup" + FFHH_UNSAFE_LAYOUT = "ffhh_unsafe_layout" + FFHH_UNSTABLE_BUILDING = "ffhh_unstable_building" + + # ========================================================== + # HHSRS – ALL 29 HAZARDS + # ========================================================== + + HHSRS_DAMP_AND_MOULD = "hhsrs_damp_and_mould" + HHSRS_EXCESS_COLD = "hhsrs_excess_cold" + HHSRS_EXCESS_HEAT = "hhsrs_excess_heat" + HHSRS_ASBESTOS_AND_MMF = "hhsrs_asbestos_and_mmf" + HHSRS_BIOCIDES = "hhsrs_biocides" + HHSRS_CARBON_MONOXIDE = "hhsrs_carbon_monoxide" + HHSRS_LEAD = "hhsrs_lead" + HHSRS_RADIATION = "hhsrs_radiation" + HHSRS_UNCOMBUSTED_FUEL_GAS = "hhsrs_uncombusted_fuel_gas" + HHSRS_VOLATILE_ORGANIC_COMPOUNDS = "hhsrs_volatile_organic_compounds" + HHSRS_CROWDING_AND_SPACE = "hhsrs_crowding_and_space" + HHSRS_ENTRY_BY_INTRUDERS = "hhsrs_entry_by_intruders" + HHSRS_LIGHTING = "hhsrs_lighting" + HHSRS_NOISE = "hhsrs_noise" + HHSRS_DOMESTIC_HYGIENE_PESTS_REFUSE = "hhsrs_domestic_hygiene_pests_refuse" + HHSRS_FOOD_SAFETY = "hhsrs_food_safety" + HHSRS_PERSONAL_HYGIENE_SANITATION = "hhsrs_personal_hygiene_sanitation" + HHSRS_WATER_SUPPLY = "hhsrs_water_supply" + HHSRS_FALLS_ASSOCIATED_WITH_BATHS = "hhsrs_falls_associated_with_baths" + HHSRS_FALLS_ON_LEVEL_SURFACES = "hhsrs_falls_on_level_surfaces" + HHSRS_FALLS_ON_STAIRS = "hhsrs_falls_on_stairs" + HHSRS_FALLS_BETWEEN_LEVELS = "hhsrs_falls_between_levels" + HHSRS_ELECTRICAL_HAZARDS = "hhsrs_electrical_hazards" + HHSRS_FIRE = "hhsrs_fire" + HHSRS_FLAMES_HOT_SURFACES = "hhsrs_flames_hot_surfaces" + HHSRS_COLLISION_AND_ENTRAPMENT = "hhsrs_collision_and_entrapment" + HHSRS_COLLISION_HAZARDS_LOW_HEADROOM = "hhsrs_collision_hazards_low_headroom" + HHSRS_EXPLOSIONS = "hhsrs_explosions" + HHSRS_ERGONOMICS = "hhsrs_ergonomics" + HHSRS_STRUCTURAL_COLLAPSE = "hhsrs_structural_collapse" + HHSRS_AMENITIES = "hhsrs_amenities" diff --git a/backend/condition/domain/mapping/lbwf/lbwf_element_map.py b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py index 8d6ea858..dfd9ca4e 100644 --- a/backend/condition/domain/mapping/lbwf/lbwf_element_map.py +++ b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py @@ -1,4 +1,4 @@ -from backend.condition.domain.element import Element +from backend.condition.domain.element_type import ElementType from backend.condition.domain.aspect_type import AspectType from backend.condition.domain.mapping.element_mapping import ElementMapping @@ -8,11 +8,11 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { # PROPERTY / GENERAL # ========================================================== "AHR_CAT": ElementMapping( - element=Element.ACCESSIBLE_HOUSING_REGISTER, + element=ElementType.ACCESSIBLE_HOUSING_REGISTER, aspect_type=AspectType.CATEGORY, ), "ASSETSAREA": ElementMapping( - element=Element.PROPERTY, + element=ElementType.PROPERTY, aspect_type=AspectType.AREA, ), # "DECNTHMINC": ElementMapping( @@ -20,301 +20,301 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { # aspect_type=AspectType.INCLUSION, # ), # Ignore this one "QUALITYSTD": ElementMapping( - element=Element.QUALITY_STANDARD, + element=ElementType.QUALITY_STANDARD, aspect_type=AspectType.TYPE, ), "EXTSTOREY": ElementMapping( - element=Element.PROPERTY, + element=ElementType.PROPERTY, aspect_type=AspectType.CONFIGURATION, ), "FLVL": ElementMapping( - element=Element.FLOOR_LEVEL_FRONT_DOOR, + element=ElementType.FLOOR_LEVEL_FRONT_DOOR, aspect_type=AspectType.LOCATION, ), "INTFLRLVL": ElementMapping( - element=Element.FLOOR_LEVEL, + element=ElementType.FLOOR_LEVEL, aspect_type=AspectType.LOCATION, ), "INTNSEINSL": ElementMapping( - element=Element.EXTERNAL_NOISE_INSULATION, # Maybe this shouldn't be "EXTERNAL_" + element=ElementType.EXTERNAL_NOISE_INSULATION, # Maybe this shouldn't be "EXTERNAL_" aspect_type=AspectType.ADEQUACY, ), "INTSTEPSFD": ElementMapping( - element=Element.STEPS_TO_FRONT_DOOR, + element=ElementType.STEPS_TO_FRONT_DOOR, aspect_type=AspectType.QUANTITY, ), # ========================================================== # ASBESTOS (NON-HHSRS RECORD) # ========================================================== "ASBESTOS": ElementMapping( - element=Element.ASBESTOS, + element=ElementType.ASBESTOS, aspect_type=AspectType.PRESENCE, ), # ========================================================== # INTERNAL – BATHROOMS & KITCHENS # ========================================================== "INTBTHRLOC": ElementMapping( - element=Element.BATHROOM, + element=ElementType.BATHROOM, aspect_type=AspectType.LOCATION, ), "INTBTHADEQ": ElementMapping( - element=Element.BATHROOM, + element=ElementType.BATHROOM, aspect_type=AspectType.ADEQUACY, ), "INTKITADEQ": ElementMapping( - element=Element.KITCHEN, + element=ElementType.KITCHEN, aspect_type=AspectType.ADEQUACY, ), "INTCKRLOC": ElementMapping( - element=Element.KITCHEN, + element=ElementType.KITCHEN, aspect_type=AspectType.LOCATION, ), "INTADDWCW": ElementMapping( - element=Element.ADDITIONAL_WC_OR_WHB, + element=ElementType.ADDITIONAL_WC_OR_WHB, aspect_type=AspectType.PRESENCE, ), "INTBTHREML": ElementMapping( - element=Element.BATHROOM_REMAINING_LIFE_SOURCE, + element=ElementType.BATHROOM_REMAINING_LIFE_SOURCE, aspect_type=AspectType.TYPE, ), "INTKITREML": ElementMapping( - element=Element.KITCHEN_REMAINING_LIFE_SOURCE, + element=ElementType.KITCHEN_REMAINING_LIFE_SOURCE, aspect_type=AspectType.TYPE, ), "INTTNTINST": ElementMapping( - element=Element.TENANT_INSTALLED_KITCHEN, + element=ElementType.TENANT_INSTALLED_KITCHEN, aspect_type=AspectType.TYPE, # Not certain about this aspect type - need more data ), # ========================================================== # INTERNAL – FIRE # ========================================================== "FRARISKRTG": ElementMapping( - element=Element.FIRE_RISK_ASSESSMENT, + element=ElementType.FIRE_RISK_ASSESSMENT, aspect_type=AspectType.RATING, ), "FRATYPE": ElementMapping( - element=Element.FIRE_RISK_ASSESSMENT, + element=ElementType.FIRE_RISK_ASSESSMENT, aspect_type=AspectType.TYPE, ), "FRAEVACSTR": ElementMapping( - element=Element.FIRE_RISK_ASSESSMENT, + element=ElementType.FIRE_RISK_ASSESSMENT, aspect_type=AspectType.STRATEGY, ), "INTSMKDET": ElementMapping( - element=Element.SMOKE_DETECTION, + element=ElementType.SMOKE_DETECTION, aspect_type=AspectType.PRESENCE, ), "INTCHEXTNT": ElementMapping( - element=Element.HEATING_SYSTEM, + element=ElementType.HEATING_SYSTEM, aspect_type=AspectType.EXTENT, ), # ========================================================== # HEATING & SERVICES # ========================================================== "INTCHEXTNT": ElementMapping( - element=Element.CENTRAL_HEATING, + element=ElementType.CENTRAL_HEATING, aspect_type=AspectType.EXTENT, ), "INTCHDIST": ElementMapping( - element=Element.HEATING_DISTRIBUTION, + element=ElementType.HEATING_DISTRIBUTION, aspect_type=AspectType.TYPE, ), "INTCHBLR": ElementMapping( - element=Element.HEATING_BOILER, + element=ElementType.HEATING_BOILER, aspect_type=AspectType.TYPE, ), "INTBOILERF": ElementMapping( - element=Element.BOILER_FUEL, + element=ElementType.BOILER_FUEL, aspect_type=AspectType.TYPE, ), "INTHTDISYS": ElementMapping( - element=Element.HEATING_SYSTEM, + element=ElementType.HEATING_SYSTEM, aspect_type=AspectType.DISTRIBUTION, ), "INTWTRHTNG": ElementMapping( - element=Element.WATER_HEATING, + element=ElementType.WATER_HEATING, aspect_type=AspectType.TYPE, ), "INTCOMHTG": ElementMapping( - element=Element.COMMUNITY_HEATING, + element=ElementType.COMMUNITY_HEATING, aspect_type=AspectType.TYPE, ), "INTELECTRC": ElementMapping( - element=Element.ELECTRICS, + element=ElementType.ELECTRICS, aspect_type=AspectType.WORK_REQUIRED, # Not certain about this aspect type - need more data ), "INTGASAVAI": ElementMapping( - element=Element.GAS_AVAILABLE, + element=ElementType.GAS_AVAILABLE, aspect_type=AspectType.PRESENCE, # Maybe should be AspectType.TYPE ? ), "INTHEATREC": ElementMapping( - element=Element.HEAT_RECOVERY_UNITS, + element=ElementType.HEAT_RECOVERY_UNITS, aspect_type=AspectType.PRESENCE, ), "INTHTIMP": ElementMapping( - element=Element.GAS_AVAILABLE, + element=ElementType.GAS_AVAILABLE, aspect_type=AspectType.WORK_REQUIRED, ), "INTPROGHTG": ElementMapping( - element=Element.PROGRAMMABLE_HEATING, + element=ElementType.PROGRAMMABLE_HEATING, aspect_type=AspectType.TYPE, # Should maybe be PRESENCE, but set to TYPE for consistency with Peabody data ), # ========================================================== # EXTERNAL – WALLS (INSTANCED) # ========================================================== "EXTWALLSTR": ElementMapping( - element=Element.EXTERNAL_WALL, + element=ElementType.EXTERNAL_WALL, aspect_type=AspectType.STRUCTURE, element_instance=1, ), "EXTWALLFN1": ElementMapping( - element=Element.EXTERNAL_WALL, + element=ElementType.EXTERNAL_WALL, aspect_type=AspectType.FINISH, element_instance=1, ), "EXTWALLFN2": ElementMapping( - element=Element.EXTERNAL_WALL, + element=ElementType.EXTERNAL_WALL, aspect_type=AspectType.FINISH, element_instance=2, ), "EXTWALLINS": ElementMapping( - element=Element.EXTERNAL_WALL, + element=ElementType.EXTERNAL_WALL, aspect_type=AspectType.INSULATION, ), "EXTWALLSPL": ElementMapping( - element=Element.EXTERNAL_WALL, + element=ElementType.EXTERNAL_WALL, aspect_type=AspectType.CONDITION, ), "EXTDWNPTYP": ElementMapping( - element=Element.DOWNPIPES, + element=ElementType.DOWNPIPES, aspect_type=AspectType.MATERIAL, ), "EXTGUTRTYP": ElementMapping( - element=Element.GUTTERS, + element=ElementType.GUTTERS, aspect_type=AspectType.MATERIAL, ), # ========================================================== # EXTERNAL – ROOFS (INSTANCED) # ========================================================== "EXTRFSTR1": ElementMapping( - element=Element.ROOF, + element=ElementType.ROOF, aspect_type=AspectType.STRUCTURE, element_instance=1, ), "EXTRFSTR2": ElementMapping( - element=Element.ROOF, + element=ElementType.ROOF, aspect_type=AspectType.STRUCTURE, element_instance=2, ), "EXTRFSTR3": ElementMapping( - element=Element.ROOF, + element=ElementType.ROOF, aspect_type=AspectType.STRUCTURE, element_instance=3, ), "EXTROOF1": ElementMapping( - element=Element.ROOF, + element=ElementType.ROOF, aspect_type=AspectType.COVERING, element_instance=1, ), "EXTROOF2": ElementMapping( - element=Element.ROOF, + element=ElementType.ROOF, aspect_type=AspectType.COVERING, element_instance=2, ), "EXTROOF3": ElementMapping( - element=Element.ROOF, + element=ElementType.ROOF, aspect_type=AspectType.COVERING, element_instance=3, ), "EXTCHIMNEY": ElementMapping( - element=Element.CHIMNEY, + element=ElementType.CHIMNEY, aspect_type=AspectType.WORK_REQUIRED, ), "EXTFASOFBR": ElementMapping( - element=Element.FASCIA_SOFFIT_BARGEBOARDS, + element=ElementType.FASCIA_SOFFIT_BARGEBOARDS, aspect_type=AspectType.MATERIAL, ), "EXTGARROOF": ElementMapping( - element=Element.GARAGE_ROOF, + element=ElementType.GARAGE_ROOF, aspect_type=AspectType.MATERIAL, ), "EXTGARSTRF": ElementMapping( - element=Element.GARAGE_AND_STORE_ROOF, + element=ElementType.GARAGE_AND_STORE_ROOF, aspect_type=AspectType.MATERIAL, ), "EXTSTRROOF": ElementMapping( - element=Element.STORE_ROOF, + element=ElementType.STORE_ROOF, aspect_type=AspectType.MATERIAL, ), "INTLOFTINS": ElementMapping( - element=Element.LOFT_INSULATION, + element=ElementType.LOFT_INSULATION, aspect_type=AspectType.TYPE, ), # ========================================================== # EXTERNAL – DOORS & WINDOWS # ========================================================== "INTFRDOOR": ElementMapping( - element=Element.EXTERNAL_DOOR, + element=ElementType.EXTERNAL_DOOR, aspect_type=AspectType.TYPE, ), "INTFRDRFRR": ElementMapping( - element=Element.EXTERNAL_DOOR, + element=ElementType.EXTERNAL_DOOR, aspect_type=AspectType.FIRE_RATING, ), "EXTBKSDDR1": ElementMapping( - element=Element.EXTERNAL_DOOR, + element=ElementType.EXTERNAL_DOOR, aspect_type=AspectType.TYPE, element_instance=1, ), "EXTBKSDDR2": ElementMapping( - element=Element.EXTERNAL_DOOR, + element=ElementType.EXTERNAL_DOOR, aspect_type=AspectType.TYPE, element_instance=2, ), "INTWDWTYPE": ElementMapping( - element=Element.EXTERNAL_WINDOWS, + element=ElementType.EXTERNAL_WINDOWS, aspect_type=AspectType.TYPE, ), "EXTWNDWS1": ElementMapping( - element=Element.EXTERNAL_WINDOWS, + element=ElementType.EXTERNAL_WINDOWS, aspect_type=AspectType.TYPE, element_instance=1, ), "EXTWNDWS2": ElementMapping( - element=Element.EXTERNAL_WINDOWS, + element=ElementType.EXTERNAL_WINDOWS, aspect_type=AspectType.TYPE, element_instance=2, ), "EXTGARDOOR": ElementMapping( - element=Element.GARAGE_DOOR, + element=ElementType.GARAGE_DOOR, aspect_type=AspectType.MATERIAL, ), "EXTGARSTDR": ElementMapping( - element=Element.GARAGE_AND_STORE_DOOR, + element=ElementType.GARAGE_AND_STORE_DOOR, aspect_type=AspectType.MATERIAL, ), "EXTSTRDOOR": ElementMapping( - element=Element.STORE_DOOR, + element=ElementType.STORE_DOOR, aspect_type=AspectType.MATERIAL, ), "EXTGARWDWS": ElementMapping( - element=Element.GARAGE_WINDOWS, + element=ElementType.GARAGE_WINDOWS, aspect_type=AspectType.MATERIAL, ), "EXTSTRWDWS": ElementMapping( - element=Element.STORE_WINDOWS, + element=ElementType.STORE_WINDOWS, aspect_type=AspectType.MATERIAL, ), "EXTGARSTWD": ElementMapping( - element=Element.GARAGE_AND_STORE_WINDOWS, + element=ElementType.GARAGE_AND_STORE_WINDOWS, aspect_type=AspectType.MATERIAL, ), "EXTLINTELS": ElementMapping( - element=Element.LINTEL, + element=ElementType.LINTEL, aspect_type=AspectType.PRESENCE, ), "EXTPTFRDR1": ElementMapping( - element=Element.PATIO_FRENCH_DOOR, + element=ElementType.PATIO_FRENCH_DOOR, aspect_type=AspectType.MATERIAL, element_instance=1, ), @@ -322,217 +322,217 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { # EXTERNAL AREAS # ========================================================== "EXTBALCONY": ElementMapping( - element=Element.PRIVATE_BALCONY, + element=ElementType.PRIVATE_BALCONY, aspect_type=AspectType.PRESENCE, ), "EXTBPOINTG": ElementMapping( - element=Element.EXTERNAL_BRICKWORK_POINTING, + element=ElementType.EXTERNAL_BRICKWORK_POINTING, aspect_type=AspectType.PRESENCE, ), "EXTDRPKERB": ElementMapping( - element=Element.DROP_KERB, + element=ElementType.DROP_KERB, aspect_type=AspectType.PRESENCE, ), "EXTEXTDECS": ElementMapping( - element=Element.EXTERNAL_DECORATION, + element=ElementType.EXTERNAL_DECORATION, aspect_type=AspectType.PRESENCE, ), "EXTHARDSTD": ElementMapping( - element=Element.PATHS_AND_HARDSTANDINGS, + element=ElementType.PATHS_AND_HARDSTANDINGS, aspect_type=AspectType.MATERIAL, ), "EXTINTDWNP": ElementMapping( - element=Element.INTERNAL_DOWNPIPES_EXTERNAL_AREA, + element=ElementType.INTERNAL_DOWNPIPES_EXTERNAL_AREA, aspect_type=AspectType.MATERIAL, ), "EXTOUTBOH": ElementMapping( - element=Element.OUTBUILDING_OVERHAUL, + element=ElementType.OUTBUILDING_OVERHAUL, aspect_type=AspectType.TYPE, ), "EXTPARKING": ElementMapping( - element=Element.PARKING_AREAS, + element=ElementType.PARKING_AREAS, aspect_type=AspectType.PRESENCE, ), "EXTPCHCNPY": ElementMapping( - element=Element.PORCH_CANOPY, + element=ElementType.PORCH_CANOPY, aspect_type=AspectType.TYPE, ), "EXTSTRINSP": ElementMapping( - element=Element.EXTERNAL_STRUCTURAL_DEFECTS, + element=ElementType.EXTERNAL_STRUCTURAL_DEFECTS, aspect_type=AspectType.TYPE, # Need more sample data to know whether this is the correct aspect type ), "INTACCRAMP": ElementMapping( - element=Element.ACCESS_RAMP, + element=ElementType.ACCESS_RAMP, aspect_type=AspectType.TYPE, # # Need more sample data to know whether this is the correct aspect type ), # ====================== # FITNESS FOR HUMAN HABITATION # ====================== "FFHHDAMP": ElementMapping( - element=Element.FFHH_DAMP, + element=ElementType.FFHH_DAMP, aspect_type=AspectType.RISK, ), "FFHHHCWAT": ElementMapping( - element=Element.FFHH_HOT_AND_COLD_WATER, + element=ElementType.FFHH_HOT_AND_COLD_WATER, aspect_type=AspectType.RISK, ), "FFHHDRNWC": ElementMapping( - element=Element.FFHH_DRAINAGE_LAVATORIES, + element=ElementType.FFHH_DRAINAGE_LAVATORIES, aspect_type=AspectType.RISK, ), "FFHHNEGLC": ElementMapping( - element=Element.FFHH_NEGLECTED, + element=ElementType.FFHH_NEGLECTED, aspect_type=AspectType.RISK, ), "FFHHNONAT": ElementMapping( - element=Element.FFHH_NATURAL_LIGHT, + element=ElementType.FFHH_NATURAL_LIGHT, aspect_type=AspectType.RISK, ), "FFHHNOVEN": ElementMapping( - element=Element.FFHH_VENTILATION, + element=ElementType.FFHH_VENTILATION, aspect_type=AspectType.RISK, ), "FFHHPRPCK": ElementMapping( - element=Element.FFHH_FOOD_PREP_AND_WASHUP, + element=ElementType.FFHH_FOOD_PREP_AND_WASHUP, aspect_type=AspectType.RISK, ), "FFHHUNLAY": ElementMapping( - element=Element.FFHH_UNSAFE_LAYOUT, + element=ElementType.FFHH_UNSAFE_LAYOUT, aspect_type=AspectType.RISK, ), "FFHHUNSTA": ElementMapping( - element=Element.FFHH_UNSTABLE_BUILDING, + element=ElementType.FFHH_UNSTABLE_BUILDING, aspect_type=AspectType.RISK, ), # ========================================================== # HHSRS # ========================================================== "HHSRSDAMP": ElementMapping( - element=Element.HHSRS_DAMP_AND_MOULD, + element=ElementType.HHSRS_DAMP_AND_MOULD, aspect_type=AspectType.RISK, ), "HHSRSCOLD": ElementMapping( - element=Element.HHSRS_EXCESS_COLD, + element=ElementType.HHSRS_EXCESS_COLD, aspect_type=AspectType.RISK, ), "HHSRSHEAT": ElementMapping( - element=Element.HHSRS_EXCESS_HEAT, + element=ElementType.HHSRS_EXCESS_HEAT, aspect_type=AspectType.RISK, ), "HHSRSASB": ElementMapping( - element=Element.HHSRS_ASBESTOS_AND_MMF, + element=ElementType.HHSRS_ASBESTOS_AND_MMF, aspect_type=AspectType.RISK, ), "HHSRSBIOC": ElementMapping( - element=Element.HHSRS_BIOCIDES, + element=ElementType.HHSRS_BIOCIDES, aspect_type=AspectType.RISK, ), "HHSRSCO": ElementMapping( - element=Element.HHSRS_CARBON_MONOXIDE, + element=ElementType.HHSRS_CARBON_MONOXIDE, aspect_type=AspectType.RISK, ), "HHSRSNO2": ElementMapping( - element=Element.HHSRS_CARBON_MONOXIDE, + element=ElementType.HHSRS_CARBON_MONOXIDE, aspect_type=AspectType.RISK, ), # Duplicate of HHSRSCO; I think they relate to the same HHSRS hazard "HHSRSSO2": ElementMapping( - element=Element.HHSRS_CARBON_MONOXIDE, + element=ElementType.HHSRS_CARBON_MONOXIDE, aspect_type=AspectType.RISK, ), # Duplicate of HHSRSCO; I think they relate to the same HHSRS hazard "HHSRSLEAD": ElementMapping( - element=Element.HHSRS_LEAD, + element=ElementType.HHSRS_LEAD, aspect_type=AspectType.RISK, ), "HHSRSRADIA": ElementMapping( - element=Element.HHSRS_RADIATION, + element=ElementType.HHSRS_RADIATION, aspect_type=AspectType.RISK, ), "HHSRSFUEL": ElementMapping( - element=Element.HHSRS_UNCOMBUSTED_FUEL_GAS, + element=ElementType.HHSRS_UNCOMBUSTED_FUEL_GAS, aspect_type=AspectType.RISK, ), "HHSRSORGAN": ElementMapping( - element=Element.HHSRS_VOLATILE_ORGANIC_COMPOUNDS, + element=ElementType.HHSRS_VOLATILE_ORGANIC_COMPOUNDS, aspect_type=AspectType.RISK, ), "HHSRSCROWD": ElementMapping( - element=Element.HHSRS_CROWDING_AND_SPACE, + element=ElementType.HHSRS_CROWDING_AND_SPACE, aspect_type=AspectType.RISK, ), "HHSRSENTRY": ElementMapping( - element=Element.HHSRS_ENTRY_BY_INTRUDERS, + element=ElementType.HHSRS_ENTRY_BY_INTRUDERS, aspect_type=AspectType.RISK, ), "HHSRSLIGHT": ElementMapping( - element=Element.HHSRS_LIGHTING, + element=ElementType.HHSRS_LIGHTING, aspect_type=AspectType.RISK, ), "HHSRSNOISE": ElementMapping( - element=Element.HHSRS_NOISE, + element=ElementType.HHSRS_NOISE, aspect_type=AspectType.RISK, ), "HHSRSDOMES": ElementMapping( - element=Element.HHSRS_DOMESTIC_HYGIENE_PESTS_REFUSE, + element=ElementType.HHSRS_DOMESTIC_HYGIENE_PESTS_REFUSE, aspect_type=AspectType.RISK, ), "HHSRSFOOD": ElementMapping( - element=Element.HHSRS_FOOD_SAFETY, + element=ElementType.HHSRS_FOOD_SAFETY, aspect_type=AspectType.RISK, ), "HHSRSPERS": ElementMapping( - element=Element.HHSRS_PERSONAL_HYGIENE_SANITATION, + element=ElementType.HHSRS_PERSONAL_HYGIENE_SANITATION, aspect_type=AspectType.RISK, ), "HHSRSWATER": ElementMapping( - element=Element.HHSRS_WATER_SUPPLY, + element=ElementType.HHSRS_WATER_SUPPLY, aspect_type=AspectType.RISK, ), "HHSRSFBATH": ElementMapping( - element=Element.HHSRS_FALLS_ASSOCIATED_WITH_BATHS, + element=ElementType.HHSRS_FALLS_ASSOCIATED_WITH_BATHS, aspect_type=AspectType.RISK, ), "HHSRSFLEVE": ElementMapping( - element=Element.HHSRS_FALLS_ON_LEVEL_SURFACES, + element=ElementType.HHSRS_FALLS_ON_LEVEL_SURFACES, aspect_type=AspectType.RISK, ), "HHSRSFSTAI": ElementMapping( - element=Element.HHSRS_FALLS_ON_STAIRS, + element=ElementType.HHSRS_FALLS_ON_STAIRS, aspect_type=AspectType.RISK, ), "HHSRSFBETW": ElementMapping( - element=Element.HHSRS_FALLS_BETWEEN_LEVELS, + element=ElementType.HHSRS_FALLS_BETWEEN_LEVELS, aspect_type=AspectType.RISK, ), "HHSRSELEC": ElementMapping( - element=Element.HHSRS_ELECTRICAL_HAZARDS, + element=ElementType.HHSRS_ELECTRICAL_HAZARDS, aspect_type=AspectType.RISK, ), "HHSRSFIRE": ElementMapping( - element=Element.HHSRS_FIRE, + element=ElementType.HHSRS_FIRE, aspect_type=AspectType.RISK, ), "HHSRSFLAME": ElementMapping( - element=Element.HHSRS_FLAMES_HOT_SURFACES, + element=ElementType.HHSRS_FLAMES_HOT_SURFACES, aspect_type=AspectType.RISK, ), "HHSRSENTRP": ElementMapping( - element=Element.HHSRS_COLLISION_AND_ENTRAPMENT, + element=ElementType.HHSRS_COLLISION_AND_ENTRAPMENT, aspect_type=AspectType.RISK, ), "HHSRSEXPLO": ElementMapping( - element=Element.HHSRS_EXPLOSIONS, + element=ElementType.HHSRS_EXPLOSIONS, aspect_type=AspectType.RISK, ), "HHSRSSTRUC": ElementMapping( - element=Element.HHSRS_STRUCTURAL_COLLAPSE, + element=ElementType.HHSRS_STRUCTURAL_COLLAPSE, aspect_type=AspectType.RISK, ), "HHSRSCLOW": ElementMapping( - element=Element.HHSRS_COLLISION_AND_ENTRAPMENT, + element=ElementType.HHSRS_COLLISION_AND_ENTRAPMENT, aspect_type=AspectType.RISK, ), "HHSRSPOSI": ElementMapping( - element=Element.HHSRS_AMENITIES, + element=ElementType.HHSRS_AMENITIES, aspect_type=AspectType.RISK, ), } diff --git a/backend/condition/domain/mapping/lbwf/lbwf_mapper.py b/backend/condition/domain/mapping/lbwf/lbwf_mapper.py index 3d7b7349..bb5f777d 100644 --- a/backend/condition/domain/mapping/lbwf/lbwf_mapper.py +++ b/backend/condition/domain/mapping/lbwf/lbwf_mapper.py @@ -1,6 +1,5 @@ from typing import Any, List, Optional -from backend.condition.domain.asset_condition import AssetCondition from backend.condition.domain.element import Element from backend.condition.domain.mapping.element_mapping import ElementMapping from backend.condition.domain.mapping.lbwf.lbwf_element_map import LBWF_ELEMENT_MAP @@ -18,12 +17,12 @@ class LbwfMapper(Mapper): def map_asset_conditions_for_property( self, client_data: Any, survey_year: Optional[int] = None - ) -> List[AssetCondition]: + ) -> List[Element]: assert isinstance( client_data, LbwfHouse ) # TODO: think of a better way to do this - mapped_assets: List[AssetCondition] = [] + mapped_assets: List[Element] = [] uprn: int = client_data.uprn for raw_asset in client_data.assets: @@ -40,7 +39,7 @@ class LbwfMapper(Mapper): continue mapped_assets.append( - AssetCondition( + Element( uprn=uprn, element=element_mapping.element, aspect_type=element_mapping.aspect_type, diff --git a/backend/condition/domain/mapping/mapper.py b/backend/condition/domain/mapping/mapper.py index c0b07184..ace6ad73 100644 --- a/backend/condition/domain/mapping/mapper.py +++ b/backend/condition/domain/mapping/mapper.py @@ -1,7 +1,7 @@ from abc import ABC, abstractmethod from typing import Any, List, Optional -from backend.condition.domain.asset_condition import AssetCondition +from backend.condition.domain.element import Element class Mapper(ABC): @@ -9,6 +9,6 @@ class Mapper(ABC): @abstractmethod def map_asset_conditions_for_property( self, client_data: Any, survey_year: Optional[int] = None - ) -> List[AssetCondition]: + ) -> List[Element]: # TODO: client_data should be properly typed pass diff --git a/backend/condition/domain/mapping/peabody/peabody_element_map.py b/backend/condition/domain/mapping/peabody/peabody_element_map.py index 1f9cceee..508b8968 100644 --- a/backend/condition/domain/mapping/peabody/peabody_element_map.py +++ b/backend/condition/domain/mapping/peabody/peabody_element_map.py @@ -1,5 +1,5 @@ from backend.condition.domain.aspect_type import AspectType -from backend.condition.domain.element import Element +from backend.condition.domain.element_type import ElementType from backend.condition.domain.mapping.element_mapping import ElementMapping @@ -7,654 +7,658 @@ PEABODY_ELEMENT_MAP = { # ========================================================== # PROPERTY / GENERAL # ========================================================== - (100, 1): ElementMapping(element=Element.PROPERTY, aspect_type=AspectType.TYPE), + (100, 1): ElementMapping(element=ElementType.PROPERTY, aspect_type=AspectType.TYPE), # (100, 3): ElementMapping(element=Element.PROPERTY, aspect_type=AspectType.AGE), # (100, 14): ElementMapping(element="property", aspect_type="construction_type"), (50, 2): ElementMapping( - element=Element.CARBON_MONOXIDE_DETECTION, aspect_type=AspectType.TYPE + element=ElementType.CARBON_MONOXIDE_DETECTION, aspect_type=AspectType.TYPE ), - (50, 3): ElementMapping(element=Element.CCU, aspect_type=AspectType.TYPE), + (50, 3): ElementMapping(element=ElementType.CCU, aspect_type=AspectType.TYPE), (50, 7): ElementMapping( - element=Element.DISABLED_HOIST_TRACKING, aspect_type=AspectType.PRESENCE + element=ElementType.DISABLED_HOIST_TRACKING, aspect_type=AspectType.PRESENCE ), (50, 11): ElementMapping( - element=Element.HEAT_DETECTION, aspect_type=AspectType.TYPE + element=ElementType.HEAT_DETECTION, aspect_type=AspectType.TYPE ), (50, 21): ElementMapping( - element=Element.SMOKE_DETECTION, aspect_type=AspectType.TYPE + element=ElementType.SMOKE_DETECTION, aspect_type=AspectType.TYPE ), (50, 22): ElementMapping( - element=Element.STAIRLIFT, aspect_type=AspectType.PRESENCE + element=ElementType.STAIRLIFT, aspect_type=AspectType.PRESENCE ), (50, 26): ElementMapping( - element=Element.DISABLED_FACILITIES, aspect_type=AspectType.TYPE + element=ElementType.DISABLED_FACILITIES, aspect_type=AspectType.TYPE + ), + (100, 3): ElementMapping( + element=ElementType.PROPERTY, aspect_type=AspectType.AGE_BAND ), - (100, 3): ElementMapping(element=Element.PROPERTY, aspect_type=AspectType.AGE_BAND), (100, 14): ElementMapping( - element=Element.PROPERTY, aspect_type=AspectType.CONSTRUCTION_TYPE + element=ElementType.PROPERTY, aspect_type=AspectType.CONSTRUCTION_TYPE ), (100, 16): ElementMapping( - element=Element.PROPERTY, aspect_type=AspectType.CLASSIFICATION + element=ElementType.PROPERTY, aspect_type=AspectType.CLASSIFICATION ), (210, 2): ElementMapping( - element=Element.PASSENGER_LIFT, aspect_type=AspectType.TYPE + element=ElementType.PASSENGER_LIFT, aspect_type=AspectType.TYPE ), # ========================================================== # EXTERNAL – WALLS # ========================================================== (50, 16): ElementMapping( - element=Element.PARTY_WALL_FIRE_BREAK, aspect_type=AspectType.PRESENCE + element=ElementType.PARTY_WALL_FIRE_BREAK, aspect_type=AspectType.PRESENCE ), (53, 1): ElementMapping( - element=Element.BOUNDARY_WALLS, aspect_type=AspectType.PRESENCE + element=ElementType.BOUNDARY_WALLS, aspect_type=AspectType.PRESENCE ), (53, 4): ElementMapping( - element=Element.EXTERNAL_DECORATION, aspect_type=AspectType.PRESENCE + element=ElementType.EXTERNAL_DECORATION, aspect_type=AspectType.PRESENCE ), (53, 5): ElementMapping( - element=Element.EXTERNAL_NOISE_INSULATION, aspect_type=AspectType.ADEQUACY + element=ElementType.EXTERNAL_NOISE_INSULATION, aspect_type=AspectType.ADEQUACY ), (53, 14): ElementMapping( - element=Element.GARAGE_WALLS, aspect_type=AspectType.MATERIAL + element=ElementType.GARAGE_WALLS, aspect_type=AspectType.MATERIAL ), (53, 23): ElementMapping( - element=Element.EXTERNAL_WALL, aspect_type=AspectType.FINISH + element=ElementType.EXTERNAL_WALL, aspect_type=AspectType.FINISH ), (53, 30): ElementMapping( - element=Element.SECONDARY_WALL, aspect_type=AspectType.FINISH + element=ElementType.SECONDARY_WALL, aspect_type=AspectType.FINISH ), # Should this be combined with primary wall, with different instance value? (53, 36): ElementMapping( - element=Element.EXTERNAL_WALL, aspect_type=AspectType.INSULATION + element=ElementType.EXTERNAL_WALL, aspect_type=AspectType.INSULATION ), (53, 40): ElementMapping( - element=Element.SPANDREL_PANELS, aspect_type=AspectType.MATERIAL + element=ElementType.SPANDREL_PANELS, aspect_type=AspectType.MATERIAL + ), + (53, 41): ElementMapping( + element=ElementType.CLADDING, aspect_type=AspectType.MATERIAL ), - (53, 41): ElementMapping(element=Element.CLADDING, aspect_type=AspectType.MATERIAL), (100, 15): ElementMapping( - element=Element.EXTERNAL_DECORATION, aspect_type=AspectType.CONDITION + element=ElementType.EXTERNAL_DECORATION, aspect_type=AspectType.CONDITION ), (120, 1): ElementMapping( - element=Element.EXTERNAL_WALL, aspect_type=AspectType.STRUCTURE + element=ElementType.EXTERNAL_WALL, aspect_type=AspectType.STRUCTURE ), (120, 2): ElementMapping( - element=Element.EXTERNAL_WALL, aspect_type=AspectType.FINISH + element=ElementType.EXTERNAL_WALL, aspect_type=AspectType.FINISH ), (120, 3): ElementMapping( - element=Element.PRIMARY_WALL, aspect_type=AspectType.INSULATION + element=ElementType.PRIMARY_WALL, aspect_type=AspectType.INSULATION ), # This code element code is actually "WALL" not "external wall" - correct? # ========================================================== # EXTERNAL – ROOFS # ========================================================== (50, 15): ElementMapping( - element=Element.LOFT_INSULATION, + element=ElementType.LOFT_INSULATION, aspect_type=AspectType.TYPE, ), (53, 2): ElementMapping( - element=Element.CHIMNEY, + element=ElementType.CHIMNEY, aspect_type=AspectType.PRESENCE, ), (53, 6): ElementMapping( - element=Element.FASCIA_SOFFIT_BARGEBOARDS, + element=ElementType.FASCIA_SOFFIT_BARGEBOARDS, aspect_type=AspectType.MATERIAL, ), (53, 7): ElementMapping( - element=Element.FLAT_ROOF_COVERING, + element=ElementType.FLAT_ROOF_COVERING, aspect_type=AspectType.MATERIAL, ), (53, 13): ElementMapping( - element=Element.GARAGE_ROOF, + element=ElementType.GARAGE_ROOF, aspect_type=AspectType.MATERIAL, ), (53, 15): ElementMapping( - element=Element.GUTTERS, + element=ElementType.GUTTERS, aspect_type=AspectType.MATERIAL, ), (53, 21): ElementMapping( - element=Element.PITCHED_ROOF_COVERING, + element=ElementType.PITCHED_ROOF_COVERING, aspect_type=AspectType.MATERIAL, ), (53, 22): ElementMapping( - element=Element.PORCH_CANOPY, + element=ElementType.PORCH_CANOPY, aspect_type=AspectType.TYPE, ), (53, 47): ElementMapping( - element=Element.ROOF, + element=ElementType.ROOF, aspect_type=AspectType.STRUCTURE, ), (110, 1): ElementMapping( - element=Element.ROOF, + element=ElementType.ROOF, aspect_type=AspectType.MATERIAL, element_instance=1, ), (110, 2): ElementMapping( - element=Element.ROOF, + element=ElementType.ROOF, aspect_type=AspectType.MATERIAL, element_instance=1, ), (110, 3): ElementMapping( - element=Element.CHIMNEY, + element=ElementType.CHIMNEY, aspect_type=AspectType.WORK_REQUIRED, ), (110, 4): ElementMapping( - element=Element.FASCIA, + element=ElementType.FASCIA, aspect_type=AspectType.MATERIAL, ), (110, 5): ElementMapping( - element=Element.SOFFIT, + element=ElementType.SOFFIT, aspect_type=AspectType.MATERIAL, ), (110, 6): ElementMapping( - element=Element.RAINWATER_GOODS, + element=ElementType.RAINWATER_GOODS, aspect_type=AspectType.MATERIAL, ), (110, 7): ElementMapping( - element=Element.LOFT_INSULATION, + element=ElementType.LOFT_INSULATION, aspect_type=AspectType.WORK_REQUIRED, # possibly not the right aspect type ), (110, 8): ElementMapping( - element=Element.PORCH_CANOPY, + element=ElementType.PORCH_CANOPY, aspect_type=AspectType.MATERIAL, ), # ========================================================== # EXTERNAL – DOORS & WINDOWS # ========================================================== (50, 8): ElementMapping( - element=Element.DOOR_ENTRY_HANDSET, + element=ElementType.DOOR_ENTRY_HANDSET, aspect_type=AspectType.PRESENCE, ), (53, 8): ElementMapping( - element=Element.FRONT_DOOR, + element=ElementType.FRONT_DOOR, aspect_type=AspectType.MATERIAL, ), (53, 12): ElementMapping( - element=Element.GARAGE_DOOR, + element=ElementType.GARAGE_DOOR, aspect_type=AspectType.MATERIAL, ), (53, 16): ElementMapping( - element=Element.LINTEL, + element=ElementType.LINTEL, aspect_type=AspectType.PRESENCE, ), (53, 19): ElementMapping( - element=Element.PATIO_FRENCH_DOOR, + element=ElementType.PATIO_FRENCH_DOOR, aspect_type=AspectType.MATERIAL, ), (53, 25): ElementMapping( - element=Element.REAR_DOOR, + element=ElementType.REAR_DOOR, aspect_type=AspectType.MATERIAL, ), (53, 29): ElementMapping( - element=Element.SECONDARY_GLAZING, + element=ElementType.SECONDARY_GLAZING, aspect_type=AspectType.PRESENCE, ), (53, 35): ElementMapping( - element=Element.STORE_DOOR, + element=ElementType.STORE_DOOR, aspect_type=AspectType.MATERIAL, ), (53, 38): ElementMapping( - element=Element.EXTERNAL_WINDOWS, + element=ElementType.EXTERNAL_WINDOWS, aspect_type=AspectType.TYPE, element_instance=1, ), (53, 39): ElementMapping( - element=Element.EXTERNAL_WINDOWS, + element=ElementType.EXTERNAL_WINDOWS, aspect_type=AspectType.TYPE, element_instance=2, ), (53, 43): ElementMapping( - element=Element.FRONT_DOOR, + element=ElementType.FRONT_DOOR, aspect_type=AspectType.TYPE, ), (130, 1): ElementMapping( - element=Element.EXTERNAL_WINDOWS, + element=ElementType.EXTERNAL_WINDOWS, aspect_type=AspectType.MATERIAL, ), (130, 2): ElementMapping( - element=Element.COMMUNAL_WINDOWS, + element=ElementType.COMMUNAL_WINDOWS, aspect_type=AspectType.MATERIAL, ), (140, 1): ElementMapping( - element=Element.MAIN_DOOR, + element=ElementType.MAIN_DOOR, aspect_type=AspectType.MATERIAL, ), (140, 2): ElementMapping( - element=Element.STORE_DOOR, + element=ElementType.STORE_DOOR, aspect_type=AspectType.MATERIAL, ), # Duplicate of (53, 35) (140, 3): ElementMapping( - element=Element.GARAGE_DOOR, + element=ElementType.GARAGE_DOOR, aspect_type=AspectType.MATERIAL, ), # Duplicate of (53, 12) (140, 4): ElementMapping( - element=Element.BLOCK_ENTRANCE_DOOR, + element=ElementType.BLOCK_ENTRANCE_DOOR, aspect_type=AspectType.MATERIAL, ), # ========================================================== # EXTERNAL AREAS # ========================================================== (53, 3): ElementMapping( - element=Element.DOWNPIPES, + element=ElementType.DOWNPIPES, aspect_type=AspectType.MATERIAL, ), (53, 9): ElementMapping( - element=Element.FRONT_FENCING, + element=ElementType.FRONT_FENCING, aspect_type=AspectType.MATERIAL, ), (53, 10): ElementMapping( - element=Element.FRONT_GATE, + element=ElementType.FRONT_GATE, aspect_type=AspectType.TYPE, ), (53, 17): ElementMapping( - element=Element.PARKING_AREAS, + element=ElementType.PARKING_AREAS, aspect_type=AspectType.MATERIAL, ), (53, 18): ElementMapping( - element=Element.PATHS_AND_HARDSTANDINGS, + element=ElementType.PATHS_AND_HARDSTANDINGS, aspect_type=AspectType.MATERIAL, ), (53, 24): ElementMapping( - element=Element.PRIVATE_BALCONY, + element=ElementType.PRIVATE_BALCONY, aspect_type=AspectType.PRESENCE, ), (53, 26): ElementMapping( - element=Element.REAR_FENCING, + element=ElementType.REAR_FENCING, aspect_type=AspectType.MATERIAL, ), (53, 27): ElementMapping( - element=Element.REAR_GATE, + element=ElementType.REAR_GATE, aspect_type=AspectType.TYPE, ), (53, 28): ElementMapping( - element=Element.RETAINING_WALLS, + element=ElementType.RETAINING_WALLS, aspect_type=AspectType.PRESENCE, ), (53, 31): ElementMapping( - element=Element.SIDE_FENCING, + element=ElementType.SIDE_FENCING, aspect_type=AspectType.MATERIAL, ), (53, 32): ElementMapping( - element=Element.SOIL_AND_VENT, + element=ElementType.SOIL_AND_VENT, aspect_type=AspectType.MATERIAL, ), (53, 34): ElementMapping( - element=Element.SOLAR_THERMALS, + element=ElementType.SOLAR_THERMALS, aspect_type=AspectType.PRESENCE, ), (53, 44): ElementMapping( - element=Element.GARAGE_STRUCTURE, + element=ElementType.GARAGE_STRUCTURE, aspect_type=AspectType.TYPE, ), (53, 45): ElementMapping( - element=Element.BALCONY_BALUSTRADE, + element=ElementType.BALCONY_BALUSTRADE, aspect_type=AspectType.MATERIAL, ), (150, 1): ElementMapping( - element=Element.BLOCK_ENTRANCE_DOOR, + element=ElementType.BLOCK_ENTRANCE_DOOR, aspect_type=AspectType.MATERIAL, ), (150, 2): ElementMapping( - element=Element.PATHS_AND_HARDSTANDINGS, + element=ElementType.PATHS_AND_HARDSTANDINGS, aspect_type=AspectType.MATERIAL, ), # Duplicate of (53, 18) - correct? (150, 3): ElementMapping( - element=Element.ROADS, + element=ElementType.ROADS, aspect_type=AspectType.MATERIAL, ), (150, 4): ElementMapping( - element=Element.BOUNDARY_WALLS, + element=ElementType.BOUNDARY_WALLS, aspect_type=AspectType.MATERIAL, ), (150, 5): ElementMapping( - element=Element.OUTBUILDINGS, + element=ElementType.OUTBUILDINGS, aspect_type=AspectType.TYPE, ), (150, 6): ElementMapping( - element=Element.GARAGE_STRUCTURE, + element=ElementType.GARAGE_STRUCTURE, aspect_type=AspectType.TYPE, ), # ========================================================== # INTERNAL – BATHROOMS & KITCHENS # ========================================================== (50, 1): ElementMapping( - element=Element.SECONDARY_TOILET, + element=ElementType.SECONDARY_TOILET, aspect_type=AspectType.PRESENCE, ), (50, 9): ElementMapping( - element=Element.BATHROOM_EXTRACTOR_FAN, + element=ElementType.BATHROOM_EXTRACTOR_FAN, aspect_type=AspectType.PRESENCE, ), (50, 9): ElementMapping( - element=Element.KITCHEN, + element=ElementType.KITCHEN, aspect_type=AspectType.TYPE, ), (50, 10): ElementMapping( - element=Element.KITCHEN_EXTRACTOR_FAN, + element=ElementType.KITCHEN_EXTRACTOR_FAN, aspect_type=AspectType.PRESENCE, ), (50, 13): ElementMapping( - element=Element.KITCHEN_SPACE_LAYOUT, + element=ElementType.KITCHEN_SPACE_LAYOUT, aspect_type=AspectType.ADEQUACY, ), (50, 14): ElementMapping( - element=Element.KITCHEN, + element=ElementType.KITCHEN, aspect_type=AspectType.TYPE, ), (50, 17): ElementMapping( - element=Element.BATHROOM, + element=ElementType.BATHROOM, aspect_type=AspectType.LOCATION, ), (50, 18): ElementMapping( - element=Element.BATHROOM, + element=ElementType.BATHROOM, aspect_type=AspectType.TYPE, ), # Actually "Primary bathroom type" - ok like this? (50, 20): ElementMapping( - element=Element.BATHROOM, + element=ElementType.BATHROOM, aspect_type=AspectType.TYPE, element_instance=2, ), # Actually "Secondary bathroom type" - ok like this? (160, 1): ElementMapping( - element=Element.KITCHEN, + element=ElementType.KITCHEN, aspect_type=AspectType.CONDITION, ), (160, 2): ElementMapping( - element=Element.KITCHEN_SPACE_LAYOUT, + element=ElementType.KITCHEN_SPACE_LAYOUT, aspect_type=AspectType.ADEQUACY, ), (190, 1): ElementMapping( - element=Element.BATHROOM, + element=ElementType.BATHROOM, aspect_type=AspectType.CONDITION, ), (190, 2): ElementMapping( - element=Element.SECONDARY_TOILET, + element=ElementType.SECONDARY_TOILET, aspect_type=AspectType.TYPE, ), # ========================================================== # COMMUNAL # ========================================================== (51, 1): ElementMapping( - element=Element.COMMUNAL_AERIAL, + element=ElementType.COMMUNAL_AERIAL, aspect_type=AspectType.PRESENCE, ), (51, 2): ElementMapping( - element=Element.COMMUNAL_AOV, + element=ElementType.COMMUNAL_AOV, aspect_type=AspectType.PRESENCE, ), (51, 3): ElementMapping( - element=Element.COMMUNAL_BALCONY_WALKWAY, + element=ElementType.COMMUNAL_BALCONY_WALKWAY, aspect_type=AspectType.PRESENCE, ), (51, 4): ElementMapping( - element=Element.COMMUNAL_BATHROOM, + element=ElementType.COMMUNAL_BATHROOM, aspect_type=AspectType.TYPE, ), (51, 5): ElementMapping( - element=Element.COMMUNAL_BIN_STORE_DOORS, + element=ElementType.COMMUNAL_BIN_STORE_DOORS, aspect_type=AspectType.PRESENCE, ), (51, 6): ElementMapping( - element=Element.COMMUNAL_BIN_STORE_ROOF, + element=ElementType.COMMUNAL_BIN_STORE_ROOF, aspect_type=AspectType.PRESENCE, ), (51, 7): ElementMapping( - element=Element.COMMUNAL_BIN_STORE_WALLS, + element=ElementType.COMMUNAL_BIN_STORE_WALLS, aspect_type=AspectType.MATERIAL, ), (51, 8): ElementMapping( - element=Element.COMMUNAL_BMS, + element=ElementType.COMMUNAL_BMS, aspect_type=AspectType.PRESENCE, ), (51, 9): ElementMapping( - element=Element.COMMUNAL_BOILER, + element=ElementType.COMMUNAL_BOILER, aspect_type=AspectType.TYPE, ), (51, 10): ElementMapping( - element=Element.COMMUNAL_BOOSTER_PUMP, + element=ElementType.COMMUNAL_BOOSTER_PUMP, aspect_type=AspectType.PRESENCE, ), (51, 11): ElementMapping( - element=Element.COMMUNAL_CCTV, + element=ElementType.COMMUNAL_CCTV, aspect_type=AspectType.PRESENCE, ), (51, 12): ElementMapping( - element=Element.COMMUNAL_CIRCULATION_SPACE, + element=ElementType.COMMUNAL_CIRCULATION_SPACE, aspect_type=AspectType.ADEQUACY, ), (51, 13): ElementMapping( - element=Element.COMMUNAL_COLD_WATER_STORAGE, + element=ElementType.COMMUNAL_COLD_WATER_STORAGE, aspect_type=AspectType.PRESENCE, ), (51, 14): ElementMapping( - element=Element.COMMUNAL_DOOR_ENTRY, + element=ElementType.COMMUNAL_DOOR_ENTRY, aspect_type=AspectType.SYSTEM, ), (51, 15): ElementMapping( - element=Element.COMMUNAL_DRY_RISER, + element=ElementType.COMMUNAL_DRY_RISER, aspect_type=AspectType.PRESENCE, ), (51, 16): ElementMapping( - element=Element.COMMUNAL_EMERGENCY_LIGHTING, + element=ElementType.COMMUNAL_EMERGENCY_LIGHTING, aspect_type=AspectType.PRESENCE, ), (51, 17): ElementMapping( - element=Element.COMMUNAL_EXTERNAL_DOORS, + element=ElementType.COMMUNAL_EXTERNAL_DOORS, aspect_type=AspectType.MATERIAL, ), (51, 19): ElementMapping( - element=Element.COMMUNAL_FIRE_ALARM, + element=ElementType.COMMUNAL_FIRE_ALARM, aspect_type=AspectType.TYPE, ), (51, 20): ElementMapping( - element=Element.COMMUNAL_INTERNAL_DECORATIONS, + element=ElementType.COMMUNAL_INTERNAL_DECORATIONS, aspect_type=AspectType.PRESENCE, ), (51, 21): ElementMapping( - element=Element.COMMUNAL_INTERNAL_DOORS, + element=ElementType.COMMUNAL_INTERNAL_DOORS, aspect_type=AspectType.MATERIAL, ), (51, 22): ElementMapping( - element=Element.COMMUNAL_INTERNAL_FLOOR, + element=ElementType.COMMUNAL_INTERNAL_FLOOR, aspect_type=AspectType.FINISH, ), (51, 23): ElementMapping( - element=Element.COMMUNAL_KITCHEN, + element=ElementType.COMMUNAL_KITCHEN, aspect_type=AspectType.TYPE, ), (51, 24): ElementMapping( - element=Element.COMMUNAL_LATERAL_MAINS, + element=ElementType.COMMUNAL_LATERAL_MAINS, aspect_type=AspectType.PRESENCE, ), (51, 25): ElementMapping( - element=Element.COMMUNAL_LIGHTING, + element=ElementType.COMMUNAL_LIGHTING, aspect_type=AspectType.PRESENCE, ), (51, 26): ElementMapping( - element=Element.COMMUNAL_LIGHTING_CONDUCTOR, + element=ElementType.COMMUNAL_LIGHTING_CONDUCTOR, aspect_type=AspectType.PRESENCE, ), (51, 27): ElementMapping( - element=Element.COMMUNAL_PASSENGER_LIFT, + element=ElementType.COMMUNAL_PASSENGER_LIFT, aspect_type=AspectType.TYPE, ), (51, 28): ElementMapping( - element=Element.COMMUNAL_ENTRANCE, + element=ElementType.COMMUNAL_ENTRANCE, aspect_type=AspectType.MATERIAL, element_instance=1, ), (51, 30): ElementMapping( - element=Element.COMMUNAL_ENTRANCE, + element=ElementType.COMMUNAL_ENTRANCE, aspect_type=AspectType.FINISH, element_instance=2, ), (51, 31): ElementMapping( - element=Element.COMMUNAL_SPRINKLER, + element=ElementType.COMMUNAL_SPRINKLER, aspect_type=AspectType.PRESENCE, ), (51, 29): ElementMapping( - element=Element.COMMUNAL_REFUSE_CHUTE, + element=ElementType.COMMUNAL_REFUSE_CHUTE, aspect_type=AspectType.PRESENCE, ), (51, 32): ElementMapping( - element=Element.COMMUNAL_STAIRS, + element=ElementType.COMMUNAL_STAIRS, aspect_type=AspectType.FINISH, ), (51, 33): ElementMapping( - element=Element.COMMUNAL_STORE_DOORS, + element=ElementType.COMMUNAL_STORE_DOORS, aspect_type=AspectType.MATERIAL, ), (51, 34): ElementMapping( - element=Element.COMMUNAL_STORE_ROOF, + element=ElementType.COMMUNAL_STORE_ROOF, aspect_type=AspectType.MATERIAL, ), (51, 35): ElementMapping( - element=Element.COMMUNAL_STORE_WALLS, + element=ElementType.COMMUNAL_STORE_WALLS, aspect_type=AspectType.MATERIAL, ), (51, 36): ElementMapping( - element=Element.COMMUNAL_WALKWAYS, + element=ElementType.COMMUNAL_WALKWAYS, aspect_type=AspectType.FINISH, ), (51, 37): ElementMapping( - element=Element.COMMUNAL_WARDEN_CALL_SYSTEM, + element=ElementType.COMMUNAL_WARDEN_CALL_SYSTEM, aspect_type=AspectType.PRESENCE, ), (51, 38): ElementMapping( - element=Element.COMMUNAL_TOILETS, + element=ElementType.COMMUNAL_TOILETS, aspect_type=AspectType.TYPE, ), (51, 39): ElementMapping( - element=Element.COMMUNAL_WET_RISER, + element=ElementType.COMMUNAL_WET_RISER, aspect_type=AspectType.PRESENCE, ), (51, 40): ElementMapping( - element=Element.COMMUNAL_PLUG_SOCKETS, + element=ElementType.COMMUNAL_PLUG_SOCKETS, aspect_type=AspectType.PRESENCE, ), (200, 1): ElementMapping( - element=Element.COMMUNAL_BOILER, + element=ElementType.COMMUNAL_BOILER, aspect_type=AspectType.TYPE, ), # Duplicate of (51, 9) - correct? (200, 2): ElementMapping( - element=Element.COMMUNAL_HEATING, + element=ElementType.COMMUNAL_HEATING, aspect_type=AspectType.TYPE, ), (200, 3): ElementMapping( - element=Element.COMMUNAL_ELECTRICS, + element=ElementType.COMMUNAL_ELECTRICS, aspect_type=AspectType.TYPE, ), (200, 4): ElementMapping( - element=Element.COMMUNAL_FIRE_ALARM, + element=ElementType.COMMUNAL_FIRE_ALARM, aspect_type=AspectType.TYPE, ), (200, 5): ElementMapping( - element=Element.COMMUNAL_LIFT, + element=ElementType.COMMUNAL_LIFT, aspect_type=AspectType.TYPE, ), (200, 6): ElementMapping( - element=Element.COMMUNAL_FLOOR_COVERING, + element=ElementType.COMMUNAL_FLOOR_COVERING, aspect_type=AspectType.MATERIAL, ), (200, 7): ElementMapping( - element=Element.COMMUNAL_KITCHEN, + element=ElementType.COMMUNAL_KITCHEN, aspect_type=AspectType.TYPE, ), (200, 8): ElementMapping( - element=Element.COMMUNAL_BATHROOM, + element=ElementType.COMMUNAL_BATHROOM, aspect_type=AspectType.TYPE, ), # Duplicate of (51, 4) - correct? (200, 9): ElementMapping( - element=Element.COMMUNAL_TOILETS, + element=ElementType.COMMUNAL_TOILETS, aspect_type=AspectType.TYPE, ), # Duplicate of (51, 38) - correct? (200, 10): ElementMapping( - element=Element.COMMUNAL_GATES, + element=ElementType.COMMUNAL_GATES, aspect_type=AspectType.TYPE, ), # ========================================================== # INTERNAL – HEATING # ========================================================== (50, 4): ElementMapping( - element=Element.HEATING_BOILER, + element=ElementType.HEATING_BOILER, aspect_type=AspectType.PRESENCE, ), # This is actually "Central heating boiler" - ok like this? (50, 5): ElementMapping( - element=Element.CENTRAL_HEATING, + element=ElementType.CENTRAL_HEATING, aspect_type=AspectType.EXTENT, ), (50, 6): ElementMapping( - element=Element.COLD_WATER_STORAGE, + element=ElementType.COLD_WATER_STORAGE, aspect_type=AspectType.PRESENCE, ), (50, 12): ElementMapping( - element=Element.HEATING_DISTRIBUTION, + element=ElementType.HEATING_DISTRIBUTION, aspect_type=AspectType.TYPE, ), (50, 19): ElementMapping( - element=Element.PROGRAMMABLE_HEATING, + element=ElementType.PROGRAMMABLE_HEATING, aspect_type=AspectType.TYPE, ), (50, 25): ElementMapping( - element=Element.HEATING_BOILER, + element=ElementType.HEATING_BOILER, aspect_type=AspectType.TYPE, ), (170, 1): ElementMapping( - element=Element.HEATING_BOILER, + element=ElementType.HEATING_BOILER, aspect_type=AspectType.TYPE, ), # Duplicate of (50,25) - correct? (170, 2): ElementMapping( - element=Element.HEATING_DISTRIBUTION, + element=ElementType.HEATING_DISTRIBUTION, aspect_type=AspectType.TYPE, ), # Duplicate of (50,12) - correct? (170, 3): ElementMapping( - element=Element.SECONDARY_HEATING, + element=ElementType.SECONDARY_HEATING, aspect_type=AspectType.TYPE, ), (170, 4): ElementMapping( - element=Element.COLD_WATER_STORAGE, + element=ElementType.COLD_WATER_STORAGE, aspect_type=AspectType.TYPE, ), (170, 5): ElementMapping( - element=Element.HOT_WATER_SYSTEM, + element=ElementType.HOT_WATER_SYSTEM, aspect_type=AspectType.TYPE, ), # ========================================================== # ELECTRICS # ========================================================== (50, 24): ElementMapping( - element=Element.INTERNAL_WIRING, + element=ElementType.INTERNAL_WIRING, aspect_type=AspectType.MATERIAL, ), (180, 1): ElementMapping( - element=Element.ELECTRICAL_WIRING, + element=ElementType.ELECTRICAL_WIRING, aspect_type=AspectType.WORK_REQUIRED, ), # Not certain about the AspectType - only example in the sample data is "Full Rewire" (180, 2): ElementMapping( - element=Element.CONSUMER_UNIT, + element=ElementType.CONSUMER_UNIT, aspect_type=AspectType.TYPE, ), (180, 3): ElementMapping( - element=Element.SMOKE_DETECTION, + element=ElementType.SMOKE_DETECTION, aspect_type=AspectType.TYPE, ), # Duplicate of (50, 21) - correct? (180, 4): ElementMapping( - element=Element.CARBON_MONOXIDE_DETECTION, + element=ElementType.CARBON_MONOXIDE_DETECTION, aspect_type=AspectType.TYPE, ), # Duplicate of (50, 2) - correct? # ========================================================== # HHSRS # ========================================================== (54, 1): ElementMapping( - element=Element.HHSRS_DAMP_AND_MOULD, + element=ElementType.HHSRS_DAMP_AND_MOULD, aspect_type=AspectType.RISK, ), (54, 4): ElementMapping( - element=Element.HHSRS_ASBESTOS_AND_MMF, + element=ElementType.HHSRS_ASBESTOS_AND_MMF, aspect_type=AspectType.RISK, ), (54, 15): ElementMapping( - element=Element.HHSRS_DOMESTIC_HYGIENE_PESTS_REFUSE, + element=ElementType.HHSRS_DOMESTIC_HYGIENE_PESTS_REFUSE, aspect_type=AspectType.RISK, ), (54, 29): ElementMapping( - element=Element.HHSRS_STRUCTURAL_COLLAPSE, + element=ElementType.HHSRS_STRUCTURAL_COLLAPSE, aspect_type=AspectType.RISK, ), } diff --git a/backend/condition/domain/mapping/peabody/peabody_mapper.py b/backend/condition/domain/mapping/peabody/peabody_mapper.py index dea07756..8c8a103b 100644 --- a/backend/condition/domain/mapping/peabody/peabody_mapper.py +++ b/backend/condition/domain/mapping/peabody/peabody_mapper.py @@ -1,6 +1,6 @@ from typing import Any, List, Optional -from backend.condition.domain.asset_condition import AssetCondition +from backend.condition.domain.element import Element from backend.condition.domain.mapping.element_mapping import ElementMapping from backend.condition.domain.mapping.peabody.peabody_element_map import ( PEABODY_ELEMENT_MAP, @@ -15,12 +15,12 @@ logger = setup_logger() class PeabodyMapper(Mapper): def map_asset_conditions_for_property( self, client_data: Any, survey_year: Optional[int] = None - ) -> List[AssetCondition]: + ) -> List[Element]: assert isinstance( client_data, PeabodyProperty ) # TODO: think of a better way to do this - mapped_assets: List[AssetCondition] = [] + mapped_assets: List[Element] = [] uprn: int = client_data.uprn for raw_asset in client_data.assets: @@ -36,7 +36,7 @@ class PeabodyMapper(Mapper): continue mapped_assets.append( - AssetCondition( + Element( uprn=uprn, element=element_mapping.element, aspect_type=element_mapping.aspect_type, diff --git a/backend/condition/domain/property_condition_survey.py b/backend/condition/domain/property_condition_survey.py new file mode 100644 index 00000000..6955e5fa --- /dev/null +++ b/backend/condition/domain/property_condition_survey.py @@ -0,0 +1,14 @@ +from dataclasses import dataclass +from typing import List +from datetime import date + +from backend.condition.domain.element import Element + + +@dataclass +class PropertyConditionSurvey: + uprn: int + elements: List[Element] + + date: date + source: str # TODO: make enum diff --git a/backend/condition/processor.py b/backend/condition/processor.py index 903c9f23..3ed0904a 100644 --- a/backend/condition/processor.py +++ b/backend/condition/processor.py @@ -1,7 +1,7 @@ from typing import Any, BinaryIO, List from datetime import datetime -from backend.condition.domain.asset_condition import AssetCondition +from backend.condition.domain.element import Element from backend.condition.domain.mapping.mapper import Mapper from backend.condition.parsing.parser import Parser from utils.logger import setup_logger @@ -22,7 +22,7 @@ def process_file(file_stream: BinaryIO, source_key: str) -> None: survey_year = datetime.now().year # TODO: get this from filepath or elsewhere - assets: List[AssetCondition] = [] + assets: List[Element] = [] for p in raw_properties: assets.extend(mapper.map_asset_conditions_for_property(p, survey_year)) diff --git a/backend/condition/tests/mapping/test_lbwf_mapper.py b/backend/condition/tests/mapping/test_lbwf_mapper.py index 907bd250..dab9c7a1 100644 --- a/backend/condition/tests/mapping/test_lbwf_mapper.py +++ b/backend/condition/tests/mapping/test_lbwf_mapper.py @@ -4,13 +4,13 @@ import pytest from datetime import date from backend.condition.domain.aspect_type import AspectType -from backend.condition.domain.element import Element +from backend.condition.domain.element_type import ElementType from backend.condition.domain.mapping.lbwf.lbwf_mapper import LbwfMapper from backend.condition.parsing.records.lbwf.lbwf_house import LbwfHouse from backend.condition.parsing.records.lbwf.lbwf_asset_condition import ( LbwfAssetCondition, ) -from backend.condition.domain.asset_condition import AssetCondition +from backend.condition.domain.element import Element def test_lbwf_mapper_maps_house(): @@ -219,10 +219,10 @@ def test_lbwf_mapper_maps_house(): survey_year = 2026 - expected_assets: List[AssetCondition] = [ - AssetCondition( + expected_assets: List[Element] = [ + Element( uprn=1, - element=Element.ACCESSIBLE_HOUSING_REGISTER, + element=ElementType.ACCESSIBLE_HOUSING_REGISTER, aspect_type=AspectType.CATEGORY, element_instance=None, value="General Needs", @@ -231,9 +231,9 @@ def test_lbwf_mapper_maps_house(): install_date=None, comments=None, ), - AssetCondition( + Element( uprn=1, - element=Element.FLOOR_LEVEL_FRONT_DOOR, + element=ElementType.FLOOR_LEVEL_FRONT_DOOR, aspect_type=AspectType.LOCATION, element_instance=None, value="Ground Floor", @@ -242,9 +242,9 @@ def test_lbwf_mapper_maps_house(): install_date=None, comments=None, ), - AssetCondition( + Element( uprn=1, - element=Element.ASBESTOS, + element=ElementType.ASBESTOS, aspect_type=AspectType.PRESENCE, element_instance=None, value="Yes", @@ -253,9 +253,9 @@ def test_lbwf_mapper_maps_house(): install_date=None, comments="Source of Data = ACT", ), - AssetCondition( + Element( uprn=1, - element=Element.HHSRS_ASBESTOS_AND_MMF, + element=ElementType.HHSRS_ASBESTOS_AND_MMF, aspect_type=AspectType.RISK, element_instance=None, value="Category 4 - Typical Risk", @@ -264,9 +264,9 @@ def test_lbwf_mapper_maps_house(): install_date=None, comments="Source of Data = ACT", ), - AssetCondition( + Element( uprn=1, - element=Element.BATHROOM, + element=ElementType.BATHROOM, aspect_type=AspectType.LOCATION, element_instance=None, value="Bathroom on Entrance Level in Property", @@ -275,9 +275,9 @@ def test_lbwf_mapper_maps_house(): install_date=None, comments="Source of Data = Codeman", ), - AssetCondition( + Element( uprn=1, - element=Element.CENTRAL_HEATING, + element=ElementType.CENTRAL_HEATING, aspect_type=AspectType.EXTENT, element_instance=None, value="No Central Heating in Property", @@ -286,9 +286,9 @@ def test_lbwf_mapper_maps_house(): install_date=None, comments="Source of Data = Codeman", ), - AssetCondition( + Element( uprn=1, - element=Element.HHSRS_FIRE, + element=ElementType.HHSRS_FIRE, aspect_type=AspectType.RISK, element_instance=None, value="Category 4 - Typical Risk", @@ -297,9 +297,9 @@ def test_lbwf_mapper_maps_house(): install_date=None, comments="Source of Data = Morgan Sindall", ), - AssetCondition( + Element( uprn=1, - element=Element.EXTERNAL_WALL, + element=ElementType.EXTERNAL_WALL, aspect_type=AspectType.FINISH, element_instance=1, value="Render or Pebbledash in External Area", @@ -308,9 +308,9 @@ def test_lbwf_mapper_maps_house(): install_date=date(2009, 4, 1), comments="Source of Data = Codeman", ), - AssetCondition( + Element( uprn=1, - element=Element.EXTERNAL_WALL, + element=ElementType.EXTERNAL_WALL, aspect_type=AspectType.FINISH, element_instance=2, value="Smooth Render Wall Finish 2 in External Area", @@ -322,7 +322,7 @@ def test_lbwf_mapper_maps_house(): ] # act - actual_assets: List[AssetCondition] = mapper.map_asset_conditions_for_property( + actual_assets: List[Element] = mapper.map_asset_conditions_for_property( lbwf_house, survey_year ) diff --git a/backend/condition/tests/mapping/test_peabody_mapper.py b/backend/condition/tests/mapping/test_peabody_mapper.py index 9997dfa8..75e03016 100644 --- a/backend/condition/tests/mapping/test_peabody_mapper.py +++ b/backend/condition/tests/mapping/test_peabody_mapper.py @@ -2,13 +2,13 @@ from datetime import datetime from typing import List from backend.condition.domain.aspect_type import AspectType -from backend.condition.domain.element import Element +from backend.condition.domain.element_type import ElementType from backend.condition.domain.mapping.peabody.peabody_mapper import PeabodyMapper from backend.condition.parsing.records.peabody.peabody_asset_condition import ( PeabodyAssetCondition, ) from backend.condition.parsing.records.peabody.peabody_property import PeabodyProperty -from backend.condition.domain.asset_condition import AssetCondition +from backend.condition.domain.element import Element def test_peabody_mapper_maps_property(): @@ -56,10 +56,10 @@ def test_peabody_mapper_maps_property(): ) mapper = PeabodyMapper() - expected_assets: List[AssetCondition] = [ - AssetCondition( + expected_assets: List[Element] = [ + Element( uprn=1, - element=Element.EXTERNAL_WINDOWS, + element=ElementType.EXTERNAL_WINDOWS, aspect_type=AspectType.MATERIAL, value="UPVC Double Glazed", quantity=8, @@ -69,9 +69,9 @@ def test_peabody_mapper_maps_property(): source_system=None, comments=None, ), - AssetCondition( + Element( uprn=1, - element=Element.EXTERNAL_DECORATION, + element=ElementType.EXTERNAL_DECORATION, aspect_type=AspectType.CONDITION, value="Normal", quantity=1, @@ -155,10 +155,10 @@ def test_wall_primary_and_secondary_wall_finish_map_correctly(): ) mapper = PeabodyMapper() - expected_assets: List[AssetCondition] = [ - AssetCondition( + expected_assets: List[Element] = [ + Element( uprn=1, - element=Element.EXTERNAL_WALLS, + element=ElementType.EXTERNAL_WALLS, aspect_type=AspectType.FINISH, value="Pointed", element_instance=1, @@ -169,9 +169,9 @@ def test_wall_primary_and_secondary_wall_finish_map_correctly(): source_system=None, comments=None, ), - AssetCondition( + Element( uprn=1, - element=Element.EXTERNAL_WALLS, + element=ElementType.EXTERNAL_WALLS, aspect_type=AspectType.FINISH, value="Pointing", element_instance=1, @@ -182,9 +182,9 @@ def test_wall_primary_and_secondary_wall_finish_map_correctly(): source_system=None, comments=None, ), - AssetCondition( + Element( uprn=1, - element=Element.EXTERNAL_WALLS, + element=ElementType.EXTERNAL_WALLS, aspect_type=AspectType.FINISH, value="Tile Hung", element_instance=1, From a0fa676230e5be1a34aac8d463a68dcca8090799 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 27 Jan 2026 13:53:09 +0000 Subject: [PATCH 50/68] =?UTF-8?q?Map=20peabody=20data=20to=20new=20structu?= =?UTF-8?q?re=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/condition/domain/element.py | 2 +- .../domain/mapping/lbwf/lbwf_mapper.py | 15 +- backend/condition/domain/mapping/mapper.py | 5 +- .../domain/mapping/peabody/peabody_mapper.py | 15 +- backend/condition/processor.py | 9 +- .../tests/mapping/test_lbwf_mapper.py | 18 +- .../tests/mapping/test_peabody_mapper.py | 169 ++++++++++-------- 7 files changed, 127 insertions(+), 106 deletions(-) diff --git a/backend/condition/domain/element.py b/backend/condition/domain/element.py index 7aca11fd..4a154815 100644 --- a/backend/condition/domain/element.py +++ b/backend/condition/domain/element.py @@ -7,6 +7,6 @@ from backend.condition.domain.element_type import ElementType @dataclass class Element: - element: ElementType + element_type: ElementType element_instance: int aspect_conditions: List[AspectCondition] diff --git a/backend/condition/domain/mapping/lbwf/lbwf_mapper.py b/backend/condition/domain/mapping/lbwf/lbwf_mapper.py index bb5f777d..fa61abf0 100644 --- a/backend/condition/domain/mapping/lbwf/lbwf_mapper.py +++ b/backend/condition/domain/mapping/lbwf/lbwf_mapper.py @@ -4,6 +4,7 @@ from backend.condition.domain.element import Element from backend.condition.domain.mapping.element_mapping import ElementMapping from backend.condition.domain.mapping.lbwf.lbwf_element_map import LBWF_ELEMENT_MAP from backend.condition.domain.mapping.mapper import Mapper +from backend.condition.domain.property_condition_survey import PropertyConditionSurvey from backend.condition.parsing.records.lbwf.lbwf_asset_condition import ( LbwfAssetCondition, ) @@ -16,16 +17,18 @@ logger = setup_logger() class LbwfMapper(Mapper): def map_asset_conditions_for_property( - self, client_data: Any, survey_year: Optional[int] = None - ) -> List[Element]: + self, client_property_data: Any, survey_year: Optional[int] = None + ) -> PropertyConditionSurvey: + raise NotImplementedError + assert isinstance( - client_data, LbwfHouse + client_property_data, LbwfHouse ) # TODO: think of a better way to do this mapped_assets: List[Element] = [] - uprn: int = client_data.uprn - for raw_asset in client_data.assets: + uprn: int = client_property_data.uprn + for raw_asset in client_property_data.assets: # Ignore metadata rows if raw_asset.element_code not in ["EICINSFREQ", "DECNTHMINC"]: try: @@ -41,7 +44,7 @@ class LbwfMapper(Mapper): mapped_assets.append( Element( uprn=uprn, - element=element_mapping.element, + element_type=element_mapping.element, aspect_type=element_mapping.aspect_type, value=raw_asset.attribute_code_description, quantity=raw_asset.quantity, diff --git a/backend/condition/domain/mapping/mapper.py b/backend/condition/domain/mapping/mapper.py index ace6ad73..3479668a 100644 --- a/backend/condition/domain/mapping/mapper.py +++ b/backend/condition/domain/mapping/mapper.py @@ -2,13 +2,14 @@ from abc import ABC, abstractmethod from typing import Any, List, Optional from backend.condition.domain.element import Element +from backend.condition.domain.property_condition_survey import PropertyConditionSurvey class Mapper(ABC): @abstractmethod def map_asset_conditions_for_property( - self, client_data: Any, survey_year: Optional[int] = None - ) -> List[Element]: + self, client_property_data: Any, survey_year: Optional[int] = None + ) -> PropertyConditionSurvey: # TODO: client_data should be properly typed pass diff --git a/backend/condition/domain/mapping/peabody/peabody_mapper.py b/backend/condition/domain/mapping/peabody/peabody_mapper.py index 8c8a103b..e9ee99a9 100644 --- a/backend/condition/domain/mapping/peabody/peabody_mapper.py +++ b/backend/condition/domain/mapping/peabody/peabody_mapper.py @@ -6,6 +6,7 @@ from backend.condition.domain.mapping.peabody.peabody_element_map import ( PEABODY_ELEMENT_MAP, ) from backend.condition.domain.mapping.mapper import Mapper +from backend.condition.domain.property_condition_survey import PropertyConditionSurvey from backend.condition.parsing.records.peabody.peabody_property import PeabodyProperty from utils.logger import setup_logger @@ -14,16 +15,18 @@ logger = setup_logger() class PeabodyMapper(Mapper): def map_asset_conditions_for_property( - self, client_data: Any, survey_year: Optional[int] = None - ) -> List[Element]: + self, client_property_data: Any, survey_year: Optional[int] = None + ) -> PropertyConditionSurvey: + raise NotImplementedError + assert isinstance( - client_data, PeabodyProperty + client_property_data, PeabodyProperty ) # TODO: think of a better way to do this mapped_assets: List[Element] = [] - uprn: int = client_data.uprn - for raw_asset in client_data.assets: + uprn: int = client_property_data.uprn + for raw_asset in client_property_data.assets: try: element_mapping: ElementMapping = PeabodyMapper._map_element( raw_asset.element_code, raw_asset.sub_element_code @@ -38,7 +41,7 @@ class PeabodyMapper(Mapper): mapped_assets.append( Element( uprn=uprn, - element=element_mapping.element, + element_type=element_mapping.element, aspect_type=element_mapping.aspect_type, value=raw_asset.material_or_answer, quantity=raw_asset.renewal_quantity, diff --git a/backend/condition/processor.py b/backend/condition/processor.py index 3ed0904a..3135d8a5 100644 --- a/backend/condition/processor.py +++ b/backend/condition/processor.py @@ -1,8 +1,8 @@ from typing import Any, BinaryIO, List from datetime import datetime -from backend.condition.domain.element import Element from backend.condition.domain.mapping.mapper import Mapper +from backend.condition.domain.property_condition_survey import PropertyConditionSurvey from backend.condition.parsing.parser import Parser from utils.logger import setup_logger from backend.condition.file_type import FileType, detect_file_type @@ -22,8 +22,11 @@ def process_file(file_stream: BinaryIO, source_key: str) -> None: survey_year = datetime.now().year # TODO: get this from filepath or elsewhere - assets: List[Element] = [] + property_condition_surveys: List[PropertyConditionSurvey] = [] + for p in raw_properties: - assets.extend(mapper.map_asset_conditions_for_property(p, survey_year)) + property_condition_surveys.push( + mapper.map_asset_conditions_for_property(p, survey_year) + ) print("done") # temp diff --git a/backend/condition/tests/mapping/test_lbwf_mapper.py b/backend/condition/tests/mapping/test_lbwf_mapper.py index dab9c7a1..8c92c029 100644 --- a/backend/condition/tests/mapping/test_lbwf_mapper.py +++ b/backend/condition/tests/mapping/test_lbwf_mapper.py @@ -222,7 +222,7 @@ def test_lbwf_mapper_maps_house(): expected_assets: List[Element] = [ Element( uprn=1, - element=ElementType.ACCESSIBLE_HOUSING_REGISTER, + element_type=ElementType.ACCESSIBLE_HOUSING_REGISTER, aspect_type=AspectType.CATEGORY, element_instance=None, value="General Needs", @@ -233,7 +233,7 @@ def test_lbwf_mapper_maps_house(): ), Element( uprn=1, - element=ElementType.FLOOR_LEVEL_FRONT_DOOR, + element_type=ElementType.FLOOR_LEVEL_FRONT_DOOR, aspect_type=AspectType.LOCATION, element_instance=None, value="Ground Floor", @@ -244,7 +244,7 @@ def test_lbwf_mapper_maps_house(): ), Element( uprn=1, - element=ElementType.ASBESTOS, + element_type=ElementType.ASBESTOS, aspect_type=AspectType.PRESENCE, element_instance=None, value="Yes", @@ -255,7 +255,7 @@ def test_lbwf_mapper_maps_house(): ), Element( uprn=1, - element=ElementType.HHSRS_ASBESTOS_AND_MMF, + element_type=ElementType.HHSRS_ASBESTOS_AND_MMF, aspect_type=AspectType.RISK, element_instance=None, value="Category 4 - Typical Risk", @@ -266,7 +266,7 @@ def test_lbwf_mapper_maps_house(): ), Element( uprn=1, - element=ElementType.BATHROOM, + element_type=ElementType.BATHROOM, aspect_type=AspectType.LOCATION, element_instance=None, value="Bathroom on Entrance Level in Property", @@ -277,7 +277,7 @@ def test_lbwf_mapper_maps_house(): ), Element( uprn=1, - element=ElementType.CENTRAL_HEATING, + element_type=ElementType.CENTRAL_HEATING, aspect_type=AspectType.EXTENT, element_instance=None, value="No Central Heating in Property", @@ -288,7 +288,7 @@ def test_lbwf_mapper_maps_house(): ), Element( uprn=1, - element=ElementType.HHSRS_FIRE, + element_type=ElementType.HHSRS_FIRE, aspect_type=AspectType.RISK, element_instance=None, value="Category 4 - Typical Risk", @@ -299,7 +299,7 @@ def test_lbwf_mapper_maps_house(): ), Element( uprn=1, - element=ElementType.EXTERNAL_WALL, + element_type=ElementType.EXTERNAL_WALL, aspect_type=AspectType.FINISH, element_instance=1, value="Render or Pebbledash in External Area", @@ -310,7 +310,7 @@ def test_lbwf_mapper_maps_house(): ), Element( uprn=1, - element=ElementType.EXTERNAL_WALL, + element_type=ElementType.EXTERNAL_WALL, aspect_type=AspectType.FINISH, element_instance=2, value="Smooth Render Wall Finish 2 in External Area", diff --git a/backend/condition/tests/mapping/test_peabody_mapper.py b/backend/condition/tests/mapping/test_peabody_mapper.py index 75e03016..63cd19c9 100644 --- a/backend/condition/tests/mapping/test_peabody_mapper.py +++ b/backend/condition/tests/mapping/test_peabody_mapper.py @@ -1,9 +1,10 @@ -from datetime import datetime -from typing import List +from datetime import datetime, date +from backend.condition.domain.aspect_condition import AspectCondition from backend.condition.domain.aspect_type import AspectType from backend.condition.domain.element_type import ElementType from backend.condition.domain.mapping.peabody.peabody_mapper import PeabodyMapper +from backend.condition.domain.property_condition_survey import PropertyConditionSurvey from backend.condition.parsing.records.peabody.peabody_asset_condition import ( PeabodyAssetCondition, ) @@ -56,40 +57,51 @@ def test_peabody_mapper_maps_property(): ) mapper = PeabodyMapper() - expected_assets: List[Element] = [ - Element( - uprn=1, - element=ElementType.EXTERNAL_WINDOWS, - aspect_type=AspectType.MATERIAL, - value="UPVC Double Glazed", - quantity=8, - install_date=None, - renewal_year=2036, - element_instance=None, - source_system=None, - comments=None, - ), - Element( - uprn=1, - element=ElementType.EXTERNAL_DECORATION, - aspect_type=AspectType.CONDITION, - value="Normal", - quantity=1, - install_date=None, - renewal_year=2029, - element_instance=None, - source_system=None, - comments=None, - ), - ] + expected_condition_survey = PropertyConditionSurvey( + uprn=1, + elements=[ + Element( + element_type=ElementType.EXTERNAL_WINDOWS, + element_instance=1, + aspect_conditions=[ + AspectCondition( + aspect_type=AspectType.MATERIAL, + aspect_instance=1, + value="UPVC Double Glazed", + quantity=8, + install_date=None, + renewal_year=2036, + comments=None, + ), + ], + ), + Element( + element_type=ElementType.EXTERNAL_DECORATION, + element_instance=1, + aspect_conditions=[ + AspectCondition( + aspect_type=AspectType.CONDITION, + aspect_instance=1, + value="Normal", + quantity=1, + install_date=None, + renewal_year=2029, + comments=None, + ) + ], + ), + ], + date=date(2000, 1, 1), # what should this be? + source="Peabody", + ) + # act - actual_assets = mapper.map_asset_conditions_for_property(peabody_property) + actual_condition_survey: PropertyConditionSurvey = ( + mapper.map_asset_conditions_for_property(peabody_property) + ) # assert - assert len(actual_assets) == len(expected_assets) - - for i, (actual, expected) in enumerate(zip(actual_assets, expected_assets)): - assert actual == expected, f"Mismatch at index {i}" + assert actual_condition_survey == expected_condition_survey def test_wall_primary_and_secondary_wall_finish_map_correctly(): @@ -155,52 +167,51 @@ def test_wall_primary_and_secondary_wall_finish_map_correctly(): ) mapper = PeabodyMapper() - expected_assets: List[Element] = [ - Element( - uprn=1, - element=ElementType.EXTERNAL_WALLS, - aspect_type=AspectType.FINISH, - value="Pointed", - element_instance=1, - aspect_instance=1, - quantity=65, - install_date=None, - renewal_year=2045, - source_system=None, - comments=None, - ), - Element( - uprn=1, - element=ElementType.EXTERNAL_WALLS, - aspect_type=AspectType.FINISH, - value="Pointing", - element_instance=1, - aspect_instance=1, - quantity=1, - install_date=None, - renewal_year=2069, - source_system=None, - comments=None, - ), - Element( - uprn=1, - element=ElementType.EXTERNAL_WALLS, - aspect_type=AspectType.FINISH, - value="Tile Hung", - element_instance=1, - aspect_instance=2, - quantity=8, - install_date=None, - renewal_year=2049, - source_system=None, - comments=None, - ), - ] + expected_condition_survey = PropertyConditionSurvey( + uprn=1, + elements=[ + Element( + element_type=ElementType.EXTERNAL_WALL, + element_instance=1, + aspect_conditions=[ + AspectCondition( + aspect_type=AspectType.FINISH, + aspect_instance=1, + value="Pointed", + quantity=65, + install_date=None, + renewal_year=2045, + comments=None, + ), + AspectCondition( + aspect_type=AspectType.FINISH, + aspect_instance=1, + value="Pointing", + quantity=1, + install_date=None, + renewal_year=2069, + comments=None, + ), + AspectCondition( + aspect_type=AspectType.FINISH, + aspect_instance=2, + value="Tile Hung", + quantity=8, + install_date=None, + renewal_year=2049, + comments=None, + ), + ], + ), + ], + date=date(2000, 1, 1), # what should this be? + source="Peabody", + ) + # act - actual_assets = mapper.map_asset_conditions_for_property(peabody_property) + actual_condition_survey: PropertyConditionSurvey = ( + mapper.map_asset_conditions_for_property(peabody_property) + ) # assert - assert len(actual_assets) == len(expected_assets) - - for i, (actual, expected) in enumerate(zip(actual_assets, expected_assets)): - assert actual == expected, f"Mismatch at index {i}" + assert actual_condition_survey == expected_condition_survey From 0bd5106cb4535ba46a657d8777ac310a53e4c563 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 27 Jan 2026 14:30:23 +0000 Subject: [PATCH 51/68] =?UTF-8?q?Map=20peabody=20data=20to=20new=20structu?= =?UTF-8?q?re=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mapping/peabody/peabody_element_map.py | 10 +-- .../domain/mapping/peabody/peabody_mapper.py | 63 ++++++++++++++----- 2 files changed, 52 insertions(+), 21 deletions(-) diff --git a/backend/condition/domain/mapping/peabody/peabody_element_map.py b/backend/condition/domain/mapping/peabody/peabody_element_map.py index 508b8968..62cb2fc3 100644 --- a/backend/condition/domain/mapping/peabody/peabody_element_map.py +++ b/backend/condition/domain/mapping/peabody/peabody_element_map.py @@ -63,8 +63,10 @@ PEABODY_ELEMENT_MAP = { element=ElementType.EXTERNAL_WALL, aspect_type=AspectType.FINISH ), (53, 30): ElementMapping( - element=ElementType.SECONDARY_WALL, aspect_type=AspectType.FINISH - ), # Should this be combined with primary wall, with different instance value? + element=ElementType.EXTERNAL_WALL, + aspect_type=AspectType.FINISH, + aspect_instance=2, + ), (53, 36): ElementMapping( element=ElementType.EXTERNAL_WALL, aspect_type=AspectType.INSULATION ), @@ -84,8 +86,8 @@ PEABODY_ELEMENT_MAP = { element=ElementType.EXTERNAL_WALL, aspect_type=AspectType.FINISH ), (120, 3): ElementMapping( - element=ElementType.PRIMARY_WALL, aspect_type=AspectType.INSULATION - ), # This code element code is actually "WALL" not "external wall" - correct? + element=ElementType.EXTERNAL_WALL, aspect_type=AspectType.INSULATION + ), # ========================================================== # EXTERNAL – ROOFS # ========================================================== diff --git a/backend/condition/domain/mapping/peabody/peabody_mapper.py b/backend/condition/domain/mapping/peabody/peabody_mapper.py index e9ee99a9..37bb3b55 100644 --- a/backend/condition/domain/mapping/peabody/peabody_mapper.py +++ b/backend/condition/domain/mapping/peabody/peabody_mapper.py @@ -1,6 +1,9 @@ from typing import Any, List, Optional +from datetime import date +from backend.condition.domain.aspect_condition import AspectCondition from backend.condition.domain.element import Element +from backend.condition.domain.element_type import ElementType from backend.condition.domain.mapping.element_mapping import ElementMapping from backend.condition.domain.mapping.peabody.peabody_element_map import ( PEABODY_ELEMENT_MAP, @@ -17,15 +20,12 @@ class PeabodyMapper(Mapper): def map_asset_conditions_for_property( self, client_property_data: Any, survey_year: Optional[int] = None ) -> PropertyConditionSurvey: - raise NotImplementedError - assert isinstance( client_property_data, PeabodyProperty ) # TODO: think of a better way to do this - mapped_assets: List[Element] = [] + mapped_elements: List[Element] = [] - uprn: int = client_property_data.uprn for raw_asset in client_property_data.assets: try: element_mapping: ElementMapping = PeabodyMapper._map_element( @@ -38,22 +38,51 @@ class PeabodyMapper(Mapper): ) continue - mapped_assets.append( - Element( - uprn=uprn, - element_type=element_mapping.element, - aspect_type=element_mapping.aspect_type, - value=raw_asset.material_or_answer, - quantity=raw_asset.renewal_quantity, - install_date=None, # Not available in peabody data - renewal_year=raw_asset.renewal_year, - element_instance=element_mapping.element_instance, - source_system=None, # Once we know the system name we'll set it here - comments=None, # Not available in peabody data + aspect_condition = AspectCondition( + aspect_type=element_mapping.aspect_type, + aspect_instance=element_mapping.aspect_instance or 1, + value=raw_asset.material_or_answer, + quantity=raw_asset.renewal_quantity, + install_date=None, # Not available in peabody data + renewal_year=raw_asset.renewal_year, + comments=None, # Not available in peabody data + ) + matching_element_type_instance: Optional[Element] = ( + PeabodyMapper._check_for_element_type_and_instance( + mapped_elements, + element_mapping.element, + element_mapping.element_instance or 1, ) ) - return mapped_assets + if not matching_element_type_instance: + mapped_elements.append( + Element( + element_type=element_mapping.element, + element_instance=element_mapping.element_instance or 1, + aspect_conditions=[aspect_condition], + ) + ) + else: + matching_element_type_instance.aspect_conditions.append( + aspect_condition + ) + + return PropertyConditionSurvey( + uprn=client_property_data.uprn, + elements=mapped_elements, + date=date(2000, 1, 1), # Temp. Not sure how to get this + source="Peabody", # TODO: Make this the system, not the client + ) + + @staticmethod + def _check_for_element_type_and_instance( + elements: List[Element], type: ElementType, instance: int + ) -> Optional[Element]: + for e in elements: + if e.element_type == type and e.element_instance == instance: + return e + return None @staticmethod def _map_element(element_code: int, sub_element_code: int) -> ElementMapping: From dc5b43d4539ebbda6588dafe5a7bae28e67def7f Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 27 Jan 2026 14:47:17 +0000 Subject: [PATCH 52/68] =?UTF-8?q?Map=20peabody=20data=20to=20new=20structu?= =?UTF-8?q?re=20=F0=9F=9F=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/mapping/peabody/peabody_mapper.py | 114 +++++++++++------- 1 file changed, 69 insertions(+), 45 deletions(-) diff --git a/backend/condition/domain/mapping/peabody/peabody_mapper.py b/backend/condition/domain/mapping/peabody/peabody_mapper.py index 37bb3b55..e052249b 100644 --- a/backend/condition/domain/mapping/peabody/peabody_mapper.py +++ b/backend/condition/domain/mapping/peabody/peabody_mapper.py @@ -1,4 +1,4 @@ -from typing import Any, List, Optional +from typing import Any, Dict, List, Optional, Tuple from datetime import date from backend.condition.domain.aspect_condition import AspectCondition @@ -24,57 +24,85 @@ class PeabodyMapper(Mapper): client_property_data, PeabodyProperty ) # TODO: think of a better way to do this - mapped_elements: List[Element] = [] + elements_by_key: dict[tuple[ElementType, int], Element] = {} for raw_asset in client_property_data.assets: - try: - element_mapping: ElementMapping = PeabodyMapper._map_element( - raw_asset.element_code, raw_asset.sub_element_code - ) - except: - logger.warning( - f"""Unrecognised Peabody Asset Element: {raw_asset.element} ({raw_asset.element_code}), - Sub-Element: {raw_asset.sub_element} ({raw_asset.sub_element_code}). Skipping record""" - ) - continue + element_mapping = PeabodyMapper._safe_map_element(raw_asset) - aspect_condition = AspectCondition( - aspect_type=element_mapping.aspect_type, - aspect_instance=element_mapping.aspect_instance or 1, - value=raw_asset.material_or_answer, - quantity=raw_asset.renewal_quantity, - install_date=None, # Not available in peabody data - renewal_year=raw_asset.renewal_year, - comments=None, # Not available in peabody data - ) - matching_element_type_instance: Optional[Element] = ( - PeabodyMapper._check_for_element_type_and_instance( - mapped_elements, - element_mapping.element, - element_mapping.element_instance or 1, - ) + aspect_condition = PeabodyMapper._build_aspect_condition( + raw_asset, element_mapping ) - if not matching_element_type_instance: - mapped_elements.append( - Element( - element_type=element_mapping.element, - element_instance=element_mapping.element_instance or 1, - aspect_conditions=[aspect_condition], - ) - ) - else: - matching_element_type_instance.aspect_conditions.append( - aspect_condition - ) + element_key = ( + element_mapping.element, + element_mapping.element_instance or 1, + ) + + PeabodyMapper._attach_aspect_condition_to_element( + elements_by_key, + element_key, + aspect_condition, + ) return PropertyConditionSurvey( uprn=client_property_data.uprn, - elements=mapped_elements, - date=date(2000, 1, 1), # Temp. Not sure how to get this + elements=list(elements_by_key.values()), + date=date(2000, 1, 1), # Temp - not sure how to get this source="Peabody", # TODO: Make this the system, not the client ) + @staticmethod + def _safe_map_element(raw_asset) -> Optional[ElementMapping]: + try: + return PeabodyMapper._map_element( + raw_asset.element_code, + raw_asset.sub_element_code, + ) + except KeyError: + logger.warning( + f"Unrecognised Peabody Asset Element: " + f"{raw_asset.element} ({raw_asset.element_code}), " + f"Sub-Element: {raw_asset.sub_element} ({raw_asset.sub_element_code}). " + "Skipping record" + ) + return None + + @staticmethod + def _map_element(element_code: int, sub_element_code: int) -> ElementMapping: + return PEABODY_ELEMENT_MAP[(element_code, sub_element_code)] + + @staticmethod + def _attach_aspect_condition_to_element( + elements_by_key: Dict[Tuple[ElementType, int], Element], + element_key: Tuple[ElementType, int], + aspect_condition: AspectCondition, + ) -> None: + element = elements_by_key.get(element_key) + + if element is None: + element = Element( + element_type=element_key[0], + element_instance=element_key[1], + aspect_conditions=[], + ) + elements_by_key[element_key] = element + + element.aspect_conditions.append(aspect_condition) + + @staticmethod + def _build_aspect_condition( + raw_asset, element_mapping: ElementMapping + ) -> AspectCondition: + return AspectCondition( + aspect_type=element_mapping.aspect_type, + aspect_instance=element_mapping.aspect_instance or 1, + value=raw_asset.material_or_answer, + quantity=raw_asset.renewal_quantity, + install_date=None, + renewal_year=raw_asset.renewal_year, + comments=None, + ) + @staticmethod def _check_for_element_type_and_instance( elements: List[Element], type: ElementType, instance: int @@ -83,7 +111,3 @@ class PeabodyMapper(Mapper): if e.element_type == type and e.element_instance == instance: return e return None - - @staticmethod - def _map_element(element_code: int, sub_element_code: int) -> ElementMapping: - return PEABODY_ELEMENT_MAP[(element_code, sub_element_code)] From 61caf8c495baaa7b4a7cdda4fed8e3027b4dc055 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 27 Jan 2026 14:48:05 +0000 Subject: [PATCH 53/68] =?UTF-8?q?Map=20peabody=20data=20to=20new=20structu?= =?UTF-8?q?re=20=F0=9F=9F=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../condition/domain/mapping/peabody/peabody_mapper.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/backend/condition/domain/mapping/peabody/peabody_mapper.py b/backend/condition/domain/mapping/peabody/peabody_mapper.py index e052249b..41b2ff39 100644 --- a/backend/condition/domain/mapping/peabody/peabody_mapper.py +++ b/backend/condition/domain/mapping/peabody/peabody_mapper.py @@ -102,12 +102,3 @@ class PeabodyMapper(Mapper): renewal_year=raw_asset.renewal_year, comments=None, ) - - @staticmethod - def _check_for_element_type_and_instance( - elements: List[Element], type: ElementType, instance: int - ) -> Optional[Element]: - for e in elements: - if e.element_type == type and e.element_instance == instance: - return e - return None From d6112f3dc8f45aabd63f789f475c09929bafb56d Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 27 Jan 2026 15:20:13 +0000 Subject: [PATCH 54/68] =?UTF-8?q?Map=20lbwf=20data=20to=20new=20structure?= =?UTF-8?q?=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/mapping/peabody/peabody_mapper.py | 2 +- .../tests/mapping/test_lbwf_mapper.py | 247 ++++++++++-------- 2 files changed, 141 insertions(+), 108 deletions(-) diff --git a/backend/condition/domain/mapping/peabody/peabody_mapper.py b/backend/condition/domain/mapping/peabody/peabody_mapper.py index 41b2ff39..7749b024 100644 --- a/backend/condition/domain/mapping/peabody/peabody_mapper.py +++ b/backend/condition/domain/mapping/peabody/peabody_mapper.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, Dict, Optional, Tuple from datetime import date from backend.condition.domain.aspect_condition import AspectCondition diff --git a/backend/condition/tests/mapping/test_lbwf_mapper.py b/backend/condition/tests/mapping/test_lbwf_mapper.py index 8c92c029..064e06f7 100644 --- a/backend/condition/tests/mapping/test_lbwf_mapper.py +++ b/backend/condition/tests/mapping/test_lbwf_mapper.py @@ -3,9 +3,11 @@ from xml.dom.minidom import Element import pytest from datetime import date +from backend.condition.domain.aspect_condition import AspectCondition from backend.condition.domain.aspect_type import AspectType from backend.condition.domain.element_type import ElementType from backend.condition.domain.mapping.lbwf.lbwf_mapper import LbwfMapper +from backend.condition.domain.property_condition_survey import PropertyConditionSurvey from backend.condition.parsing.records.lbwf.lbwf_house import LbwfHouse from backend.condition.parsing.records.lbwf.lbwf_asset_condition import ( LbwfAssetCondition, @@ -219,115 +221,146 @@ def test_lbwf_mapper_maps_house(): survey_year = 2026 - expected_assets: List[Element] = [ - Element( - uprn=1, - element_type=ElementType.ACCESSIBLE_HOUSING_REGISTER, - aspect_type=AspectType.CATEGORY, - element_instance=None, - value="General Needs", - quantity=1, - renewal_year=None, - install_date=None, - comments=None, - ), - Element( - uprn=1, - element_type=ElementType.FLOOR_LEVEL_FRONT_DOOR, - aspect_type=AspectType.LOCATION, - element_instance=None, - value="Ground Floor", - quantity=1, - renewal_year=None, - install_date=None, - comments=None, - ), - Element( - uprn=1, - element_type=ElementType.ASBESTOS, - aspect_type=AspectType.PRESENCE, - element_instance=None, - value="Yes", - quantity=None, - renewal_year=None, - install_date=None, - comments="Source of Data = ACT", - ), - Element( - uprn=1, - element_type=ElementType.HHSRS_ASBESTOS_AND_MMF, - aspect_type=AspectType.RISK, - element_instance=None, - value="Category 4 - Typical Risk", - quantity=None, - renewal_year=None, - install_date=None, - comments="Source of Data = ACT", - ), - Element( - uprn=1, - element_type=ElementType.BATHROOM, - aspect_type=AspectType.LOCATION, - element_instance=None, - value="Bathroom on Entrance Level in Property", - quantity=1, - renewal_year=None, - install_date=None, - comments="Source of Data = Codeman", - ), - Element( - uprn=1, - element_type=ElementType.CENTRAL_HEATING, - aspect_type=AspectType.EXTENT, - element_instance=None, - value="No Central Heating in Property", - quantity=1, - renewal_year=None, - install_date=None, - comments="Source of Data = Codeman", - ), - Element( - uprn=1, - element_type=ElementType.HHSRS_FIRE, - aspect_type=AspectType.RISK, - element_instance=None, - value="Category 4 - Typical Risk", - quantity=1, - renewal_year=None, - install_date=None, - comments="Source of Data = Morgan Sindall", - ), - Element( - uprn=1, - element_type=ElementType.EXTERNAL_WALL, - aspect_type=AspectType.FINISH, - element_instance=1, - value="Render or Pebbledash in External Area", - quantity=1, - renewal_year=2052, - install_date=date(2009, 4, 1), - comments="Source of Data = Codeman", - ), - Element( - uprn=1, - element_type=ElementType.EXTERNAL_WALL, - aspect_type=AspectType.FINISH, - element_instance=2, - value="Smooth Render Wall Finish 2 in External Area", - quantity=1, - renewal_year=2052, - install_date=date(2009, 4, 1), - comments="Source of Data = Codeman", - ), - ] + expected_condition_survey = PropertyConditionSurvey( + uprn=1, + elements=[ + Element( + element_type=ElementType.ACCESSIBLE_HOUSING_REGISTER, + element_instance=1, + aspect_conditions=[ + AspectCondition( + aspect_type=AspectType.CATEGORY, + aspect_instance=1, + value="General Needs", + quantity=1, + install_date=None, + renewal_year=None, + comments=None, + ) + ], + ), + Element( + element_type=ElementType.FLOOR_LEVEL_FRONT_DOOR, + element_instance=1, + aspect_conditions=[ + AspectCondition( + aspect_type=AspectType.LOCATION, + aspect_instance=1, + value="Ground Floor", + quantity=1, + install_date=None, + renewal_year=None, + comments=None, + ) + ], + ), + Element( + element_type=ElementType.ASBESTOS, + element_instance=1, + aspect_conditions=[ + AspectCondition( + aspect_type=AspectType.PRESENCE, + aspect_instance=1, + value="Yes", + quantity=None, + install_date=None, + renewal_year=None, + comments=None, + ) + ], + ), + Element( + element_type=ElementType.HHSRS_ASBESTOS_AND_MMF, + element_instance=1, + aspect_conditions=[ + AspectCondition( + aspect_type=AspectType.RISK, + aspect_instance=1, + value="Category 4 - Typical Risk", + quantity=None, + renewal_year=None, + comments="Source of Data = ACT", + ) + ], + ), + Element( + element_type=ElementType.BATHROOM, + element_instance=1, + aspect_conditions=[ + AspectCondition( + aspect_type=AspectType.LOCATION, + aspect_instance=1, + value="Bathroom on Entrance Level in Property", + quantity=1, + install_date=None, + renewal_year=None, + comments="Source of Data = Codeman", + ) + ], + ), + Element( + element_type=ElementType.CENTRAL_HEATING, + element_instance=1, + aspect_conditions=[ + AspectCondition( + aspect_type=AspectType.EXTENT, + aspect_instance=1, + value="No Central Heating in Property", + quantity=1, + install_date=None, + renewal_year=None, + comments="Source of Data = Codeman", + ) + ], + ), + Element( + element_type=ElementType.HHSRS_FIRE, + element_instance=1, + aspect_conditions=[ + AspectCondition( + aspect_type=AspectType.RISK, + aspect_instance=1, + value="Category 4 - Typical Risk", + quantity=1, + install_date=None, + renewal_year=None, + comments="Source of Data = Morgan Sindall", + ) + ], + ), + Element( + element_type=ElementType.EXTERNAL_WALL, + element_instance=1, + aspect_conditions=[ + AspectCondition( + aspect_type=AspectType.FINISH, + aspect_instance=1, + value="Render or Pebbledash in External Area", + quantity=1, + install_date=date(2009, 4, 1), + renewal_year=2052, + comments="Source of Data = Codeman", + ), + AspectCondition( + aspect_type=AspectType.FINISH, + aspect_instance=2, + value="Smooth Render Wall Finish 2 in External Area", + quantity=1, + install_date=date(2009, 4, 1), + renewal_year=2052, + comments="Source of Data = Codeman", + ), + ], + ), + ], + date=date(2000, 1, 1), # what should this be? + source="LBWF", + ) # act - actual_assets: List[Element] = mapper.map_asset_conditions_for_property( - lbwf_house, survey_year + actual_condition_survey: PropertyConditionSurvey = ( + mapper.map_asset_conditions_for_property(lbwf_house) ) # assert - assert len(actual_assets) == len(expected_assets) - - for i, (actual, expected) in enumerate(zip(actual_assets, expected_assets)): - assert actual == expected, f"Mismatch at index {i}" + assert actual_condition_survey == expected_condition_survey From 803484defd8319e1d893f23e65c53b7cd97f0a88 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 27 Jan 2026 15:59:06 +0000 Subject: [PATCH 55/68] =?UTF-8?q?Map=20lbwf=20data=20to=20new=20structure?= =?UTF-8?q?=20=F0=9F=9F=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/mapping/lbwf/lbwf_element_map.py | 3 +- .../domain/mapping/lbwf/lbwf_mapper.py | 103 ++++++++++++------ .../domain/mapping/peabody/peabody_mapper.py | 7 +- backend/condition/tests/custom_asserts.py | 74 +++++++++++++ .../tests/mapping/test_lbwf_mapper.py | 12 +- .../tests/mapping/test_peabody_mapper.py | 5 +- 6 files changed, 161 insertions(+), 43 deletions(-) create mode 100644 backend/condition/tests/custom_asserts.py diff --git a/backend/condition/domain/mapping/lbwf/lbwf_element_map.py b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py index dfd9ca4e..96f21b84 100644 --- a/backend/condition/domain/mapping/lbwf/lbwf_element_map.py +++ b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py @@ -175,7 +175,8 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { "EXTWALLFN2": ElementMapping( element=ElementType.EXTERNAL_WALL, aspect_type=AspectType.FINISH, - element_instance=2, + element_instance=1, + aspect_instance=2, ), "EXTWALLINS": ElementMapping( element=ElementType.EXTERNAL_WALL, diff --git a/backend/condition/domain/mapping/lbwf/lbwf_mapper.py b/backend/condition/domain/mapping/lbwf/lbwf_mapper.py index fa61abf0..f11133bf 100644 --- a/backend/condition/domain/mapping/lbwf/lbwf_mapper.py +++ b/backend/condition/domain/mapping/lbwf/lbwf_mapper.py @@ -1,6 +1,9 @@ -from typing import Any, List, Optional +from typing import Any, Dict, List, Optional, Tuple +from datetime import date +from backend.condition.domain.aspect_condition import AspectCondition from backend.condition.domain.element import Element +from backend.condition.domain.element_type import ElementType from backend.condition.domain.mapping.element_mapping import ElementMapping from backend.condition.domain.mapping.lbwf.lbwf_element_map import LBWF_ELEMENT_MAP from backend.condition.domain.mapping.mapper import Mapper @@ -19,51 +22,85 @@ class LbwfMapper(Mapper): def map_asset_conditions_for_property( self, client_property_data: Any, survey_year: Optional[int] = None ) -> PropertyConditionSurvey: - raise NotImplementedError - assert isinstance( client_property_data, LbwfHouse ) # TODO: think of a better way to do this - mapped_assets: List[Element] = [] + elements_by_key: dict[tuple[ElementType, int], Element] = {} - uprn: int = client_property_data.uprn for raw_asset in client_property_data.assets: - # Ignore metadata rows - if raw_asset.element_code not in ["EICINSFREQ", "DECNTHMINC"]: - try: - element_mapping: ElementMapping = LbwfMapper._map_element( - raw_asset.element_code - ) - except: - logger.warning( - f"Unrecognised LBWF Asset Element Code: {raw_asset.element_code}. Skipping record" - ) - continue + element_mapping = LbwfMapper._safe_map_element(raw_asset) - mapped_assets.append( - Element( - uprn=uprn, - element_type=element_mapping.element, - aspect_type=element_mapping.aspect_type, - value=raw_asset.attribute_code_description, - quantity=raw_asset.quantity, - install_date=raw_asset.install_date, - renewal_year=LbwfMapper._calculate_renewal_year( - raw_asset, survey_year - ), - element_instance=element_mapping.element_instance, - source_system=None, # Once we know the system name we'll set it here - comments=raw_asset.element_comments, - ) + aspect_condition = LbwfMapper._build_aspect_condition( + raw_asset, element_mapping, survey_year + ) + + element_key = ( + element_mapping.element, + element_mapping.element_instance or 1, + ) + + LbwfMapper._attach_aspect_condition_to_element( + elements_by_key, element_key, aspect_condition + ) + + return PropertyConditionSurvey( + uprn=client_property_data.uprn, + elements=list(elements_by_key.values()), + date=date(2000, 1, 1), # Temp - not sure how to get this + source="LBWF", # TODO: Make this the system, not the client + ) + + @staticmethod + def _safe_map_element(raw_asset: LbwfAssetCondition) -> Optional[ElementMapping]: + try: + return LbwfMapper._map_element(raw_asset.element_code) + except KeyError: + logger.warning( + logger.warning( + f"Unrecognised LBWF Asset Element: " + f"{raw_asset.element_code} ({raw_asset.element_code_description})). " + "Skipping record" ) - - return mapped_assets + ) + return None @staticmethod def _map_element(lbwf_element_code: str) -> ElementMapping: return LBWF_ELEMENT_MAP[lbwf_element_code] + @staticmethod + def _build_aspect_condition( + raw_asset, element_mapping: ElementMapping, survey_year: int + ) -> AspectCondition: + return AspectCondition( + aspect_type=element_mapping.aspect_type, + aspect_instance=element_mapping.aspect_instance or 1, + value=raw_asset.attribute_code_description, + quantity=raw_asset.quantity, + install_date=raw_asset.install_date, + renewal_year=LbwfMapper._calculate_renewal_year(raw_asset, survey_year), + comments=raw_asset.element_comments, + ) + + @staticmethod + def _attach_aspect_condition_to_element( + elements_by_key: Dict[Tuple[ElementType, int], Element], + element_key: Tuple[ElementType, int], + aspect_condition: AspectCondition, + ) -> None: + element = elements_by_key.get(element_key) + + if element is None: + element = Element( + element_type=element_key[0], + element_instance=element_key[1], + aspect_conditions=[], + ) + elements_by_key[element_key] = element + + element.aspect_conditions.append(aspect_condition) + @staticmethod def _calculate_renewal_year( lbwf_asset: LbwfAssetCondition, survey_year: Optional[int] diff --git a/backend/condition/domain/mapping/peabody/peabody_mapper.py b/backend/condition/domain/mapping/peabody/peabody_mapper.py index 7749b024..184b2898 100644 --- a/backend/condition/domain/mapping/peabody/peabody_mapper.py +++ b/backend/condition/domain/mapping/peabody/peabody_mapper.py @@ -10,6 +10,9 @@ from backend.condition.domain.mapping.peabody.peabody_element_map import ( ) from backend.condition.domain.mapping.mapper import Mapper from backend.condition.domain.property_condition_survey import PropertyConditionSurvey +from backend.condition.parsing.records.peabody.peabody_asset_condition import ( + PeabodyAssetCondition, +) from backend.condition.parsing.records.peabody.peabody_property import PeabodyProperty from utils.logger import setup_logger @@ -52,7 +55,7 @@ class PeabodyMapper(Mapper): ) @staticmethod - def _safe_map_element(raw_asset) -> Optional[ElementMapping]: + def _safe_map_element(raw_asset: PeabodyAssetCondition) -> Optional[ElementMapping]: try: return PeabodyMapper._map_element( raw_asset.element_code, @@ -98,7 +101,7 @@ class PeabodyMapper(Mapper): aspect_instance=element_mapping.aspect_instance or 1, value=raw_asset.material_or_answer, quantity=raw_asset.renewal_quantity, - install_date=None, + install_date=None, # Not available in peabody data renewal_year=raw_asset.renewal_year, comments=None, ) diff --git a/backend/condition/tests/custom_asserts.py b/backend/condition/tests/custom_asserts.py new file mode 100644 index 00000000..9e3abd7f --- /dev/null +++ b/backend/condition/tests/custom_asserts.py @@ -0,0 +1,74 @@ +from backend.condition.domain.property_condition_survey import PropertyConditionSurvey + + +class CustomAsserts: + def assert_property_condition_surveys_equal( + actual: PropertyConditionSurvey, + expected: PropertyConditionSurvey, + ) -> bool: + assert actual.uprn == expected.uprn, "UPRN differs" + assert actual.source == expected.source, "Source differs" + assert actual.date == expected.date, "Date differs" + + assert len(actual.elements) == len(expected.elements), ( + f"Expected {len(expected.elements)} elements, " + f"got {len(actual.elements)}" + ) + + for i, (actual_element, expected_element) in enumerate( + zip(actual.elements, expected.elements) + ): + assert actual_element.element_type == expected_element.element_type, ( + f"Element[{i}] type differs: " + f"{actual_element.element_type} != {expected_element.element_type}" + ) + assert ( + actual_element.element_instance == expected_element.element_instance + ), ( + f"Element[{i}] instance differs: " + f"{actual_element.element_instance} != {expected_element.element_instance}" + ) + + assert len(actual_element.aspect_conditions) == len( + expected_element.aspect_conditions + ), f"Element[{i}] aspect count differs" + + for j, (actual_aspect, expected_aspect) in enumerate( + zip( + actual_element.aspect_conditions, + expected_element.aspect_conditions, + ) + ): + prefix = f"Element[{i}].Aspect[{j}]" + + assert actual_aspect.aspect_type == expected_aspect.aspect_type, ( + f"{prefix}.aspect_type differs: " + f"{actual_aspect.aspect_type} != {expected_aspect.aspect_type}" + ) + assert ( + actual_aspect.aspect_instance == expected_aspect.aspect_instance + ), ( + f"{prefix}.aspect_instance differs: " + f"{actual_aspect.aspect_instance} != {expected_aspect.aspect_instance}" + ) + assert actual_aspect.value == expected_aspect.value, ( + f"{prefix}.value differs: " + f"{actual_aspect.value} != {expected_aspect.value}" + ) + assert actual_aspect.quantity == expected_aspect.quantity, ( + f"{prefix}.quantity differs: " + f"{actual_aspect.quantity} != {expected_aspect.quantity}" + ) + assert actual_aspect.install_date == expected_aspect.install_date, ( + f"{prefix}.install_date differs: " + f"{actual_aspect.install_date} != {expected_aspect.install_date}" + ) + assert actual_aspect.renewal_year == expected_aspect.renewal_year, ( + f"{prefix}.renewal_year differs: " + f"{actual_aspect.renewal_year} != {expected_aspect.renewal_year}" + ) + assert actual_aspect.comments == expected_aspect.comments, ( + f"{prefix}.comments differs: " + f"{actual_aspect.comments} != {expected_aspect.comments}" + ) + return True diff --git a/backend/condition/tests/mapping/test_lbwf_mapper.py b/backend/condition/tests/mapping/test_lbwf_mapper.py index 064e06f7..77890155 100644 --- a/backend/condition/tests/mapping/test_lbwf_mapper.py +++ b/backend/condition/tests/mapping/test_lbwf_mapper.py @@ -1,6 +1,3 @@ -from typing import List -from xml.dom.minidom import Element -import pytest from datetime import date from backend.condition.domain.aspect_condition import AspectCondition @@ -13,6 +10,7 @@ from backend.condition.parsing.records.lbwf.lbwf_asset_condition import ( LbwfAssetCondition, ) from backend.condition.domain.element import Element +from backend.condition.tests.custom_asserts import CustomAsserts def test_lbwf_mapper_maps_house(): @@ -265,7 +263,7 @@ def test_lbwf_mapper_maps_house(): quantity=None, install_date=None, renewal_year=None, - comments=None, + comments="Source of Data = ACT", ) ], ), @@ -359,8 +357,10 @@ def test_lbwf_mapper_maps_house(): # act actual_condition_survey: PropertyConditionSurvey = ( - mapper.map_asset_conditions_for_property(lbwf_house) + mapper.map_asset_conditions_for_property(lbwf_house, survey_year) ) # assert - assert actual_condition_survey == expected_condition_survey + assert CustomAsserts.assert_property_condition_surveys_equal( + actual_condition_survey, expected_condition_survey + ) diff --git a/backend/condition/tests/mapping/test_peabody_mapper.py b/backend/condition/tests/mapping/test_peabody_mapper.py index 63cd19c9..979258b0 100644 --- a/backend/condition/tests/mapping/test_peabody_mapper.py +++ b/backend/condition/tests/mapping/test_peabody_mapper.py @@ -10,6 +10,7 @@ from backend.condition.parsing.records.peabody.peabody_asset_condition import ( ) from backend.condition.parsing.records.peabody.peabody_property import PeabodyProperty from backend.condition.domain.element import Element +from backend.condition.tests.custom_asserts import CustomAsserts def test_peabody_mapper_maps_property(): @@ -214,4 +215,6 @@ def test_wall_primary_and_secondary_wall_finish_map_correctly(): ) # assert - assert actual_condition_survey == expected_condition_survey + assert CustomAsserts.assert_property_condition_surveys_equal( + actual_condition_survey, expected_condition_survey + ) From 32f9850a27e101503d819c7b3abda5d5eeb251f6 Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 27 Jan 2026 16:08:20 +0000 Subject: [PATCH 56/68] minor fixes and TODO --- backend/condition/domain/element_type.py | 2 + .../domain/mapping/element_mapping.py | 4 +- .../domain/mapping/lbwf/lbwf_element_map.py | 240 ++++++------- .../domain/mapping/lbwf/lbwf_mapper.py | 2 +- .../mapping/peabody/peabody_element_map.py | 327 +++++++++--------- .../domain/mapping/peabody/peabody_mapper.py | 2 +- 6 files changed, 291 insertions(+), 286 deletions(-) diff --git a/backend/condition/domain/element_type.py b/backend/condition/domain/element_type.py index 32897895..bc2aa2d6 100644 --- a/backend/condition/domain/element_type.py +++ b/backend/condition/domain/element_type.py @@ -228,6 +228,8 @@ class ElementType(str, Enum): # HHSRS – ALL 29 HAZARDS # ========================================================== + # TODO: In order to group HHSRS, should there be a single HHSRS element type, and each of the below is an AspectType? + HHSRS_DAMP_AND_MOULD = "hhsrs_damp_and_mould" HHSRS_EXCESS_COLD = "hhsrs_excess_cold" HHSRS_EXCESS_HEAT = "hhsrs_excess_heat" diff --git a/backend/condition/domain/mapping/element_mapping.py b/backend/condition/domain/mapping/element_mapping.py index c93862c8..95fd08b9 100644 --- a/backend/condition/domain/mapping/element_mapping.py +++ b/backend/condition/domain/mapping/element_mapping.py @@ -1,13 +1,13 @@ from dataclasses import dataclass from typing import Optional -from xml.dom.minidom import Element from backend.condition.domain.aspect_type import AspectType +from backend.condition.domain.element_type import ElementType @dataclass(frozen=True) class ElementMapping: - element: Element + elementType: ElementType aspect_type: AspectType element_instance: Optional[int] = None aspect_instance: Optional[int] = None diff --git a/backend/condition/domain/mapping/lbwf/lbwf_element_map.py b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py index 96f21b84..a547fe5c 100644 --- a/backend/condition/domain/mapping/lbwf/lbwf_element_map.py +++ b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py @@ -8,11 +8,11 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { # PROPERTY / GENERAL # ========================================================== "AHR_CAT": ElementMapping( - element=ElementType.ACCESSIBLE_HOUSING_REGISTER, + elementType=ElementType.ACCESSIBLE_HOUSING_REGISTER, aspect_type=AspectType.CATEGORY, ), "ASSETSAREA": ElementMapping( - element=ElementType.PROPERTY, + elementType=ElementType.PROPERTY, aspect_type=AspectType.AREA, ), # "DECNTHMINC": ElementMapping( @@ -20,302 +20,302 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { # aspect_type=AspectType.INCLUSION, # ), # Ignore this one "QUALITYSTD": ElementMapping( - element=ElementType.QUALITY_STANDARD, + elementType=ElementType.QUALITY_STANDARD, aspect_type=AspectType.TYPE, ), "EXTSTOREY": ElementMapping( - element=ElementType.PROPERTY, + elementType=ElementType.PROPERTY, aspect_type=AspectType.CONFIGURATION, ), "FLVL": ElementMapping( - element=ElementType.FLOOR_LEVEL_FRONT_DOOR, + elementType=ElementType.FLOOR_LEVEL_FRONT_DOOR, aspect_type=AspectType.LOCATION, ), "INTFLRLVL": ElementMapping( - element=ElementType.FLOOR_LEVEL, + elementType=ElementType.FLOOR_LEVEL, aspect_type=AspectType.LOCATION, ), "INTNSEINSL": ElementMapping( - element=ElementType.EXTERNAL_NOISE_INSULATION, # Maybe this shouldn't be "EXTERNAL_" + elementType=ElementType.EXTERNAL_NOISE_INSULATION, # Maybe this shouldn't be "EXTERNAL_" aspect_type=AspectType.ADEQUACY, ), "INTSTEPSFD": ElementMapping( - element=ElementType.STEPS_TO_FRONT_DOOR, + elementType=ElementType.STEPS_TO_FRONT_DOOR, aspect_type=AspectType.QUANTITY, ), # ========================================================== # ASBESTOS (NON-HHSRS RECORD) # ========================================================== "ASBESTOS": ElementMapping( - element=ElementType.ASBESTOS, + elementType=ElementType.ASBESTOS, aspect_type=AspectType.PRESENCE, ), # ========================================================== # INTERNAL – BATHROOMS & KITCHENS # ========================================================== "INTBTHRLOC": ElementMapping( - element=ElementType.BATHROOM, + elementType=ElementType.BATHROOM, aspect_type=AspectType.LOCATION, ), "INTBTHADEQ": ElementMapping( - element=ElementType.BATHROOM, + elementType=ElementType.BATHROOM, aspect_type=AspectType.ADEQUACY, ), "INTKITADEQ": ElementMapping( - element=ElementType.KITCHEN, + elementType=ElementType.KITCHEN, aspect_type=AspectType.ADEQUACY, ), "INTCKRLOC": ElementMapping( - element=ElementType.KITCHEN, + elementType=ElementType.KITCHEN, aspect_type=AspectType.LOCATION, ), "INTADDWCW": ElementMapping( - element=ElementType.ADDITIONAL_WC_OR_WHB, + elementType=ElementType.ADDITIONAL_WC_OR_WHB, aspect_type=AspectType.PRESENCE, ), "INTBTHREML": ElementMapping( - element=ElementType.BATHROOM_REMAINING_LIFE_SOURCE, + elementType=ElementType.BATHROOM_REMAINING_LIFE_SOURCE, aspect_type=AspectType.TYPE, ), "INTKITREML": ElementMapping( - element=ElementType.KITCHEN_REMAINING_LIFE_SOURCE, + elementType=ElementType.KITCHEN_REMAINING_LIFE_SOURCE, aspect_type=AspectType.TYPE, ), "INTTNTINST": ElementMapping( - element=ElementType.TENANT_INSTALLED_KITCHEN, + elementType=ElementType.TENANT_INSTALLED_KITCHEN, aspect_type=AspectType.TYPE, # Not certain about this aspect type - need more data ), # ========================================================== # INTERNAL – FIRE # ========================================================== "FRARISKRTG": ElementMapping( - element=ElementType.FIRE_RISK_ASSESSMENT, + elementType=ElementType.FIRE_RISK_ASSESSMENT, aspect_type=AspectType.RATING, ), "FRATYPE": ElementMapping( - element=ElementType.FIRE_RISK_ASSESSMENT, + elementType=ElementType.FIRE_RISK_ASSESSMENT, aspect_type=AspectType.TYPE, ), "FRAEVACSTR": ElementMapping( - element=ElementType.FIRE_RISK_ASSESSMENT, + elementType=ElementType.FIRE_RISK_ASSESSMENT, aspect_type=AspectType.STRATEGY, ), "INTSMKDET": ElementMapping( - element=ElementType.SMOKE_DETECTION, + elementType=ElementType.SMOKE_DETECTION, aspect_type=AspectType.PRESENCE, ), "INTCHEXTNT": ElementMapping( - element=ElementType.HEATING_SYSTEM, + elementType=ElementType.HEATING_SYSTEM, aspect_type=AspectType.EXTENT, ), # ========================================================== # HEATING & SERVICES # ========================================================== "INTCHEXTNT": ElementMapping( - element=ElementType.CENTRAL_HEATING, + elementType=ElementType.CENTRAL_HEATING, aspect_type=AspectType.EXTENT, ), "INTCHDIST": ElementMapping( - element=ElementType.HEATING_DISTRIBUTION, + elementType=ElementType.HEATING_DISTRIBUTION, aspect_type=AspectType.TYPE, ), "INTCHBLR": ElementMapping( - element=ElementType.HEATING_BOILER, + elementType=ElementType.HEATING_BOILER, aspect_type=AspectType.TYPE, ), "INTBOILERF": ElementMapping( - element=ElementType.BOILER_FUEL, + elementType=ElementType.BOILER_FUEL, aspect_type=AspectType.TYPE, ), "INTHTDISYS": ElementMapping( - element=ElementType.HEATING_SYSTEM, + elementType=ElementType.HEATING_SYSTEM, aspect_type=AspectType.DISTRIBUTION, ), "INTWTRHTNG": ElementMapping( - element=ElementType.WATER_HEATING, + elementType=ElementType.WATER_HEATING, aspect_type=AspectType.TYPE, ), "INTCOMHTG": ElementMapping( - element=ElementType.COMMUNITY_HEATING, + elementType=ElementType.COMMUNITY_HEATING, aspect_type=AspectType.TYPE, ), "INTELECTRC": ElementMapping( - element=ElementType.ELECTRICS, + elementType=ElementType.ELECTRICS, aspect_type=AspectType.WORK_REQUIRED, # Not certain about this aspect type - need more data ), "INTGASAVAI": ElementMapping( - element=ElementType.GAS_AVAILABLE, + elementType=ElementType.GAS_AVAILABLE, aspect_type=AspectType.PRESENCE, # Maybe should be AspectType.TYPE ? ), "INTHEATREC": ElementMapping( - element=ElementType.HEAT_RECOVERY_UNITS, + elementType=ElementType.HEAT_RECOVERY_UNITS, aspect_type=AspectType.PRESENCE, ), "INTHTIMP": ElementMapping( - element=ElementType.GAS_AVAILABLE, + elementType=ElementType.GAS_AVAILABLE, aspect_type=AspectType.WORK_REQUIRED, ), "INTPROGHTG": ElementMapping( - element=ElementType.PROGRAMMABLE_HEATING, + elementType=ElementType.PROGRAMMABLE_HEATING, aspect_type=AspectType.TYPE, # Should maybe be PRESENCE, but set to TYPE for consistency with Peabody data ), # ========================================================== # EXTERNAL – WALLS (INSTANCED) # ========================================================== "EXTWALLSTR": ElementMapping( - element=ElementType.EXTERNAL_WALL, + elementType=ElementType.EXTERNAL_WALL, aspect_type=AspectType.STRUCTURE, element_instance=1, ), "EXTWALLFN1": ElementMapping( - element=ElementType.EXTERNAL_WALL, + elementType=ElementType.EXTERNAL_WALL, aspect_type=AspectType.FINISH, element_instance=1, ), "EXTWALLFN2": ElementMapping( - element=ElementType.EXTERNAL_WALL, + elementType=ElementType.EXTERNAL_WALL, aspect_type=AspectType.FINISH, element_instance=1, aspect_instance=2, ), "EXTWALLINS": ElementMapping( - element=ElementType.EXTERNAL_WALL, + elementType=ElementType.EXTERNAL_WALL, aspect_type=AspectType.INSULATION, ), "EXTWALLSPL": ElementMapping( - element=ElementType.EXTERNAL_WALL, + elementType=ElementType.EXTERNAL_WALL, aspect_type=AspectType.CONDITION, ), "EXTDWNPTYP": ElementMapping( - element=ElementType.DOWNPIPES, + elementType=ElementType.DOWNPIPES, aspect_type=AspectType.MATERIAL, ), "EXTGUTRTYP": ElementMapping( - element=ElementType.GUTTERS, + elementType=ElementType.GUTTERS, aspect_type=AspectType.MATERIAL, ), # ========================================================== # EXTERNAL – ROOFS (INSTANCED) # ========================================================== "EXTRFSTR1": ElementMapping( - element=ElementType.ROOF, + elementType=ElementType.ROOF, aspect_type=AspectType.STRUCTURE, element_instance=1, ), "EXTRFSTR2": ElementMapping( - element=ElementType.ROOF, + elementType=ElementType.ROOF, aspect_type=AspectType.STRUCTURE, element_instance=2, ), "EXTRFSTR3": ElementMapping( - element=ElementType.ROOF, + elementType=ElementType.ROOF, aspect_type=AspectType.STRUCTURE, element_instance=3, ), "EXTROOF1": ElementMapping( - element=ElementType.ROOF, + elementType=ElementType.ROOF, aspect_type=AspectType.COVERING, element_instance=1, ), "EXTROOF2": ElementMapping( - element=ElementType.ROOF, + elementType=ElementType.ROOF, aspect_type=AspectType.COVERING, element_instance=2, ), "EXTROOF3": ElementMapping( - element=ElementType.ROOF, + elementType=ElementType.ROOF, aspect_type=AspectType.COVERING, element_instance=3, ), "EXTCHIMNEY": ElementMapping( - element=ElementType.CHIMNEY, + elementType=ElementType.CHIMNEY, aspect_type=AspectType.WORK_REQUIRED, ), "EXTFASOFBR": ElementMapping( - element=ElementType.FASCIA_SOFFIT_BARGEBOARDS, + elementType=ElementType.FASCIA_SOFFIT_BARGEBOARDS, aspect_type=AspectType.MATERIAL, ), "EXTGARROOF": ElementMapping( - element=ElementType.GARAGE_ROOF, + elementType=ElementType.GARAGE_ROOF, aspect_type=AspectType.MATERIAL, ), "EXTGARSTRF": ElementMapping( - element=ElementType.GARAGE_AND_STORE_ROOF, + elementType=ElementType.GARAGE_AND_STORE_ROOF, aspect_type=AspectType.MATERIAL, ), "EXTSTRROOF": ElementMapping( - element=ElementType.STORE_ROOF, + elementType=ElementType.STORE_ROOF, aspect_type=AspectType.MATERIAL, ), "INTLOFTINS": ElementMapping( - element=ElementType.LOFT_INSULATION, + elementType=ElementType.LOFT_INSULATION, aspect_type=AspectType.TYPE, ), # ========================================================== # EXTERNAL – DOORS & WINDOWS # ========================================================== "INTFRDOOR": ElementMapping( - element=ElementType.EXTERNAL_DOOR, + elementType=ElementType.EXTERNAL_DOOR, aspect_type=AspectType.TYPE, ), "INTFRDRFRR": ElementMapping( - element=ElementType.EXTERNAL_DOOR, + elementType=ElementType.EXTERNAL_DOOR, aspect_type=AspectType.FIRE_RATING, ), "EXTBKSDDR1": ElementMapping( - element=ElementType.EXTERNAL_DOOR, + elementType=ElementType.EXTERNAL_DOOR, aspect_type=AspectType.TYPE, element_instance=1, ), "EXTBKSDDR2": ElementMapping( - element=ElementType.EXTERNAL_DOOR, + elementType=ElementType.EXTERNAL_DOOR, aspect_type=AspectType.TYPE, element_instance=2, ), "INTWDWTYPE": ElementMapping( - element=ElementType.EXTERNAL_WINDOWS, + elementType=ElementType.EXTERNAL_WINDOWS, aspect_type=AspectType.TYPE, ), "EXTWNDWS1": ElementMapping( - element=ElementType.EXTERNAL_WINDOWS, + elementType=ElementType.EXTERNAL_WINDOWS, aspect_type=AspectType.TYPE, element_instance=1, ), "EXTWNDWS2": ElementMapping( - element=ElementType.EXTERNAL_WINDOWS, + elementType=ElementType.EXTERNAL_WINDOWS, aspect_type=AspectType.TYPE, element_instance=2, ), "EXTGARDOOR": ElementMapping( - element=ElementType.GARAGE_DOOR, + elementType=ElementType.GARAGE_DOOR, aspect_type=AspectType.MATERIAL, ), "EXTGARSTDR": ElementMapping( - element=ElementType.GARAGE_AND_STORE_DOOR, + elementType=ElementType.GARAGE_AND_STORE_DOOR, aspect_type=AspectType.MATERIAL, ), "EXTSTRDOOR": ElementMapping( - element=ElementType.STORE_DOOR, + elementType=ElementType.STORE_DOOR, aspect_type=AspectType.MATERIAL, ), "EXTGARWDWS": ElementMapping( - element=ElementType.GARAGE_WINDOWS, + elementType=ElementType.GARAGE_WINDOWS, aspect_type=AspectType.MATERIAL, ), "EXTSTRWDWS": ElementMapping( - element=ElementType.STORE_WINDOWS, + elementType=ElementType.STORE_WINDOWS, aspect_type=AspectType.MATERIAL, ), "EXTGARSTWD": ElementMapping( - element=ElementType.GARAGE_AND_STORE_WINDOWS, + elementType=ElementType.GARAGE_AND_STORE_WINDOWS, aspect_type=AspectType.MATERIAL, ), "EXTLINTELS": ElementMapping( - element=ElementType.LINTEL, + elementType=ElementType.LINTEL, aspect_type=AspectType.PRESENCE, ), "EXTPTFRDR1": ElementMapping( - element=ElementType.PATIO_FRENCH_DOOR, + elementType=ElementType.PATIO_FRENCH_DOOR, aspect_type=AspectType.MATERIAL, element_instance=1, ), @@ -323,217 +323,217 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { # EXTERNAL AREAS # ========================================================== "EXTBALCONY": ElementMapping( - element=ElementType.PRIVATE_BALCONY, + elementType=ElementType.PRIVATE_BALCONY, aspect_type=AspectType.PRESENCE, ), "EXTBPOINTG": ElementMapping( - element=ElementType.EXTERNAL_BRICKWORK_POINTING, + elementType=ElementType.EXTERNAL_BRICKWORK_POINTING, aspect_type=AspectType.PRESENCE, ), "EXTDRPKERB": ElementMapping( - element=ElementType.DROP_KERB, + elementType=ElementType.DROP_KERB, aspect_type=AspectType.PRESENCE, ), "EXTEXTDECS": ElementMapping( - element=ElementType.EXTERNAL_DECORATION, + elementType=ElementType.EXTERNAL_DECORATION, aspect_type=AspectType.PRESENCE, ), "EXTHARDSTD": ElementMapping( - element=ElementType.PATHS_AND_HARDSTANDINGS, + elementType=ElementType.PATHS_AND_HARDSTANDINGS, aspect_type=AspectType.MATERIAL, ), "EXTINTDWNP": ElementMapping( - element=ElementType.INTERNAL_DOWNPIPES_EXTERNAL_AREA, + elementType=ElementType.INTERNAL_DOWNPIPES_EXTERNAL_AREA, aspect_type=AspectType.MATERIAL, ), "EXTOUTBOH": ElementMapping( - element=ElementType.OUTBUILDING_OVERHAUL, + elementType=ElementType.OUTBUILDING_OVERHAUL, aspect_type=AspectType.TYPE, ), "EXTPARKING": ElementMapping( - element=ElementType.PARKING_AREAS, + elementType=ElementType.PARKING_AREAS, aspect_type=AspectType.PRESENCE, ), "EXTPCHCNPY": ElementMapping( - element=ElementType.PORCH_CANOPY, + elementType=ElementType.PORCH_CANOPY, aspect_type=AspectType.TYPE, ), "EXTSTRINSP": ElementMapping( - element=ElementType.EXTERNAL_STRUCTURAL_DEFECTS, + elementType=ElementType.EXTERNAL_STRUCTURAL_DEFECTS, aspect_type=AspectType.TYPE, # Need more sample data to know whether this is the correct aspect type ), "INTACCRAMP": ElementMapping( - element=ElementType.ACCESS_RAMP, + elementType=ElementType.ACCESS_RAMP, aspect_type=AspectType.TYPE, # # Need more sample data to know whether this is the correct aspect type ), # ====================== # FITNESS FOR HUMAN HABITATION # ====================== "FFHHDAMP": ElementMapping( - element=ElementType.FFHH_DAMP, + elementType=ElementType.FFHH_DAMP, aspect_type=AspectType.RISK, ), "FFHHHCWAT": ElementMapping( - element=ElementType.FFHH_HOT_AND_COLD_WATER, + elementType=ElementType.FFHH_HOT_AND_COLD_WATER, aspect_type=AspectType.RISK, ), "FFHHDRNWC": ElementMapping( - element=ElementType.FFHH_DRAINAGE_LAVATORIES, + elementType=ElementType.FFHH_DRAINAGE_LAVATORIES, aspect_type=AspectType.RISK, ), "FFHHNEGLC": ElementMapping( - element=ElementType.FFHH_NEGLECTED, + elementType=ElementType.FFHH_NEGLECTED, aspect_type=AspectType.RISK, ), "FFHHNONAT": ElementMapping( - element=ElementType.FFHH_NATURAL_LIGHT, + elementType=ElementType.FFHH_NATURAL_LIGHT, aspect_type=AspectType.RISK, ), "FFHHNOVEN": ElementMapping( - element=ElementType.FFHH_VENTILATION, + elementType=ElementType.FFHH_VENTILATION, aspect_type=AspectType.RISK, ), "FFHHPRPCK": ElementMapping( - element=ElementType.FFHH_FOOD_PREP_AND_WASHUP, + elementType=ElementType.FFHH_FOOD_PREP_AND_WASHUP, aspect_type=AspectType.RISK, ), "FFHHUNLAY": ElementMapping( - element=ElementType.FFHH_UNSAFE_LAYOUT, + elementType=ElementType.FFHH_UNSAFE_LAYOUT, aspect_type=AspectType.RISK, ), "FFHHUNSTA": ElementMapping( - element=ElementType.FFHH_UNSTABLE_BUILDING, + elementType=ElementType.FFHH_UNSTABLE_BUILDING, aspect_type=AspectType.RISK, ), # ========================================================== # HHSRS # ========================================================== "HHSRSDAMP": ElementMapping( - element=ElementType.HHSRS_DAMP_AND_MOULD, + elementType=ElementType.HHSRS_DAMP_AND_MOULD, aspect_type=AspectType.RISK, ), "HHSRSCOLD": ElementMapping( - element=ElementType.HHSRS_EXCESS_COLD, + elementType=ElementType.HHSRS_EXCESS_COLD, aspect_type=AspectType.RISK, ), "HHSRSHEAT": ElementMapping( - element=ElementType.HHSRS_EXCESS_HEAT, + elementType=ElementType.HHSRS_EXCESS_HEAT, aspect_type=AspectType.RISK, ), "HHSRSASB": ElementMapping( - element=ElementType.HHSRS_ASBESTOS_AND_MMF, + elementType=ElementType.HHSRS_ASBESTOS_AND_MMF, aspect_type=AspectType.RISK, ), "HHSRSBIOC": ElementMapping( - element=ElementType.HHSRS_BIOCIDES, + elementType=ElementType.HHSRS_BIOCIDES, aspect_type=AspectType.RISK, ), "HHSRSCO": ElementMapping( - element=ElementType.HHSRS_CARBON_MONOXIDE, + elementType=ElementType.HHSRS_CARBON_MONOXIDE, aspect_type=AspectType.RISK, ), "HHSRSNO2": ElementMapping( - element=ElementType.HHSRS_CARBON_MONOXIDE, + elementType=ElementType.HHSRS_CARBON_MONOXIDE, aspect_type=AspectType.RISK, ), # Duplicate of HHSRSCO; I think they relate to the same HHSRS hazard "HHSRSSO2": ElementMapping( - element=ElementType.HHSRS_CARBON_MONOXIDE, + elementType=ElementType.HHSRS_CARBON_MONOXIDE, aspect_type=AspectType.RISK, ), # Duplicate of HHSRSCO; I think they relate to the same HHSRS hazard "HHSRSLEAD": ElementMapping( - element=ElementType.HHSRS_LEAD, + elementType=ElementType.HHSRS_LEAD, aspect_type=AspectType.RISK, ), "HHSRSRADIA": ElementMapping( - element=ElementType.HHSRS_RADIATION, + elementType=ElementType.HHSRS_RADIATION, aspect_type=AspectType.RISK, ), "HHSRSFUEL": ElementMapping( - element=ElementType.HHSRS_UNCOMBUSTED_FUEL_GAS, + elementType=ElementType.HHSRS_UNCOMBUSTED_FUEL_GAS, aspect_type=AspectType.RISK, ), "HHSRSORGAN": ElementMapping( - element=ElementType.HHSRS_VOLATILE_ORGANIC_COMPOUNDS, + elementType=ElementType.HHSRS_VOLATILE_ORGANIC_COMPOUNDS, aspect_type=AspectType.RISK, ), "HHSRSCROWD": ElementMapping( - element=ElementType.HHSRS_CROWDING_AND_SPACE, + elementType=ElementType.HHSRS_CROWDING_AND_SPACE, aspect_type=AspectType.RISK, ), "HHSRSENTRY": ElementMapping( - element=ElementType.HHSRS_ENTRY_BY_INTRUDERS, + elementType=ElementType.HHSRS_ENTRY_BY_INTRUDERS, aspect_type=AspectType.RISK, ), "HHSRSLIGHT": ElementMapping( - element=ElementType.HHSRS_LIGHTING, + elementType=ElementType.HHSRS_LIGHTING, aspect_type=AspectType.RISK, ), "HHSRSNOISE": ElementMapping( - element=ElementType.HHSRS_NOISE, + elementType=ElementType.HHSRS_NOISE, aspect_type=AspectType.RISK, ), "HHSRSDOMES": ElementMapping( - element=ElementType.HHSRS_DOMESTIC_HYGIENE_PESTS_REFUSE, + elementType=ElementType.HHSRS_DOMESTIC_HYGIENE_PESTS_REFUSE, aspect_type=AspectType.RISK, ), "HHSRSFOOD": ElementMapping( - element=ElementType.HHSRS_FOOD_SAFETY, + elementType=ElementType.HHSRS_FOOD_SAFETY, aspect_type=AspectType.RISK, ), "HHSRSPERS": ElementMapping( - element=ElementType.HHSRS_PERSONAL_HYGIENE_SANITATION, + elementType=ElementType.HHSRS_PERSONAL_HYGIENE_SANITATION, aspect_type=AspectType.RISK, ), "HHSRSWATER": ElementMapping( - element=ElementType.HHSRS_WATER_SUPPLY, + elementType=ElementType.HHSRS_WATER_SUPPLY, aspect_type=AspectType.RISK, ), "HHSRSFBATH": ElementMapping( - element=ElementType.HHSRS_FALLS_ASSOCIATED_WITH_BATHS, + elementType=ElementType.HHSRS_FALLS_ASSOCIATED_WITH_BATHS, aspect_type=AspectType.RISK, ), "HHSRSFLEVE": ElementMapping( - element=ElementType.HHSRS_FALLS_ON_LEVEL_SURFACES, + elementType=ElementType.HHSRS_FALLS_ON_LEVEL_SURFACES, aspect_type=AspectType.RISK, ), "HHSRSFSTAI": ElementMapping( - element=ElementType.HHSRS_FALLS_ON_STAIRS, + elementType=ElementType.HHSRS_FALLS_ON_STAIRS, aspect_type=AspectType.RISK, ), "HHSRSFBETW": ElementMapping( - element=ElementType.HHSRS_FALLS_BETWEEN_LEVELS, + elementType=ElementType.HHSRS_FALLS_BETWEEN_LEVELS, aspect_type=AspectType.RISK, ), "HHSRSELEC": ElementMapping( - element=ElementType.HHSRS_ELECTRICAL_HAZARDS, + elementType=ElementType.HHSRS_ELECTRICAL_HAZARDS, aspect_type=AspectType.RISK, ), "HHSRSFIRE": ElementMapping( - element=ElementType.HHSRS_FIRE, + elementType=ElementType.HHSRS_FIRE, aspect_type=AspectType.RISK, ), "HHSRSFLAME": ElementMapping( - element=ElementType.HHSRS_FLAMES_HOT_SURFACES, + elementType=ElementType.HHSRS_FLAMES_HOT_SURFACES, aspect_type=AspectType.RISK, ), "HHSRSENTRP": ElementMapping( - element=ElementType.HHSRS_COLLISION_AND_ENTRAPMENT, + elementType=ElementType.HHSRS_COLLISION_AND_ENTRAPMENT, aspect_type=AspectType.RISK, ), "HHSRSEXPLO": ElementMapping( - element=ElementType.HHSRS_EXPLOSIONS, + elementType=ElementType.HHSRS_EXPLOSIONS, aspect_type=AspectType.RISK, ), "HHSRSSTRUC": ElementMapping( - element=ElementType.HHSRS_STRUCTURAL_COLLAPSE, + elementType=ElementType.HHSRS_STRUCTURAL_COLLAPSE, aspect_type=AspectType.RISK, ), "HHSRSCLOW": ElementMapping( - element=ElementType.HHSRS_COLLISION_AND_ENTRAPMENT, + elementType=ElementType.HHSRS_COLLISION_AND_ENTRAPMENT, aspect_type=AspectType.RISK, ), "HHSRSPOSI": ElementMapping( - element=ElementType.HHSRS_AMENITIES, + elementType=ElementType.HHSRS_AMENITIES, aspect_type=AspectType.RISK, ), } diff --git a/backend/condition/domain/mapping/lbwf/lbwf_mapper.py b/backend/condition/domain/mapping/lbwf/lbwf_mapper.py index f11133bf..09109ef9 100644 --- a/backend/condition/domain/mapping/lbwf/lbwf_mapper.py +++ b/backend/condition/domain/mapping/lbwf/lbwf_mapper.py @@ -36,7 +36,7 @@ class LbwfMapper(Mapper): ) element_key = ( - element_mapping.element, + element_mapping.elementType, element_mapping.element_instance or 1, ) diff --git a/backend/condition/domain/mapping/peabody/peabody_element_map.py b/backend/condition/domain/mapping/peabody/peabody_element_map.py index 62cb2fc3..2281a17c 100644 --- a/backend/condition/domain/mapping/peabody/peabody_element_map.py +++ b/backend/condition/domain/mapping/peabody/peabody_element_map.py @@ -7,660 +7,663 @@ PEABODY_ELEMENT_MAP = { # ========================================================== # PROPERTY / GENERAL # ========================================================== - (100, 1): ElementMapping(element=ElementType.PROPERTY, aspect_type=AspectType.TYPE), + (100, 1): ElementMapping( + elementType=ElementType.PROPERTY, aspect_type=AspectType.TYPE + ), # (100, 3): ElementMapping(element=Element.PROPERTY, aspect_type=AspectType.AGE), # (100, 14): ElementMapping(element="property", aspect_type="construction_type"), (50, 2): ElementMapping( - element=ElementType.CARBON_MONOXIDE_DETECTION, aspect_type=AspectType.TYPE + elementType=ElementType.CARBON_MONOXIDE_DETECTION, aspect_type=AspectType.TYPE ), - (50, 3): ElementMapping(element=ElementType.CCU, aspect_type=AspectType.TYPE), + (50, 3): ElementMapping(elementType=ElementType.CCU, aspect_type=AspectType.TYPE), (50, 7): ElementMapping( - element=ElementType.DISABLED_HOIST_TRACKING, aspect_type=AspectType.PRESENCE + elementType=ElementType.DISABLED_HOIST_TRACKING, aspect_type=AspectType.PRESENCE ), (50, 11): ElementMapping( - element=ElementType.HEAT_DETECTION, aspect_type=AspectType.TYPE + elementType=ElementType.HEAT_DETECTION, aspect_type=AspectType.TYPE ), (50, 21): ElementMapping( - element=ElementType.SMOKE_DETECTION, aspect_type=AspectType.TYPE + elementType=ElementType.SMOKE_DETECTION, aspect_type=AspectType.TYPE ), (50, 22): ElementMapping( - element=ElementType.STAIRLIFT, aspect_type=AspectType.PRESENCE + elementType=ElementType.STAIRLIFT, aspect_type=AspectType.PRESENCE ), (50, 26): ElementMapping( - element=ElementType.DISABLED_FACILITIES, aspect_type=AspectType.TYPE + elementType=ElementType.DISABLED_FACILITIES, aspect_type=AspectType.TYPE ), (100, 3): ElementMapping( - element=ElementType.PROPERTY, aspect_type=AspectType.AGE_BAND + elementType=ElementType.PROPERTY, aspect_type=AspectType.AGE_BAND ), (100, 14): ElementMapping( - element=ElementType.PROPERTY, aspect_type=AspectType.CONSTRUCTION_TYPE + elementType=ElementType.PROPERTY, aspect_type=AspectType.CONSTRUCTION_TYPE ), (100, 16): ElementMapping( - element=ElementType.PROPERTY, aspect_type=AspectType.CLASSIFICATION + elementType=ElementType.PROPERTY, aspect_type=AspectType.CLASSIFICATION ), (210, 2): ElementMapping( - element=ElementType.PASSENGER_LIFT, aspect_type=AspectType.TYPE + elementType=ElementType.PASSENGER_LIFT, aspect_type=AspectType.TYPE ), # ========================================================== # EXTERNAL – WALLS # ========================================================== (50, 16): ElementMapping( - element=ElementType.PARTY_WALL_FIRE_BREAK, aspect_type=AspectType.PRESENCE + elementType=ElementType.PARTY_WALL_FIRE_BREAK, aspect_type=AspectType.PRESENCE ), (53, 1): ElementMapping( - element=ElementType.BOUNDARY_WALLS, aspect_type=AspectType.PRESENCE + elementType=ElementType.BOUNDARY_WALLS, aspect_type=AspectType.PRESENCE ), (53, 4): ElementMapping( - element=ElementType.EXTERNAL_DECORATION, aspect_type=AspectType.PRESENCE + elementType=ElementType.EXTERNAL_DECORATION, aspect_type=AspectType.PRESENCE ), (53, 5): ElementMapping( - element=ElementType.EXTERNAL_NOISE_INSULATION, aspect_type=AspectType.ADEQUACY + elementType=ElementType.EXTERNAL_NOISE_INSULATION, + aspect_type=AspectType.ADEQUACY, ), (53, 14): ElementMapping( - element=ElementType.GARAGE_WALLS, aspect_type=AspectType.MATERIAL + elementType=ElementType.GARAGE_WALLS, aspect_type=AspectType.MATERIAL ), (53, 23): ElementMapping( - element=ElementType.EXTERNAL_WALL, aspect_type=AspectType.FINISH + elementType=ElementType.EXTERNAL_WALL, aspect_type=AspectType.FINISH ), (53, 30): ElementMapping( - element=ElementType.EXTERNAL_WALL, + elementType=ElementType.EXTERNAL_WALL, aspect_type=AspectType.FINISH, aspect_instance=2, ), (53, 36): ElementMapping( - element=ElementType.EXTERNAL_WALL, aspect_type=AspectType.INSULATION + elementType=ElementType.EXTERNAL_WALL, aspect_type=AspectType.INSULATION ), (53, 40): ElementMapping( - element=ElementType.SPANDREL_PANELS, aspect_type=AspectType.MATERIAL + elementType=ElementType.SPANDREL_PANELS, aspect_type=AspectType.MATERIAL ), (53, 41): ElementMapping( - element=ElementType.CLADDING, aspect_type=AspectType.MATERIAL + elementType=ElementType.CLADDING, aspect_type=AspectType.MATERIAL ), (100, 15): ElementMapping( - element=ElementType.EXTERNAL_DECORATION, aspect_type=AspectType.CONDITION + elementType=ElementType.EXTERNAL_DECORATION, aspect_type=AspectType.CONDITION ), (120, 1): ElementMapping( - element=ElementType.EXTERNAL_WALL, aspect_type=AspectType.STRUCTURE + elementType=ElementType.EXTERNAL_WALL, aspect_type=AspectType.STRUCTURE ), (120, 2): ElementMapping( - element=ElementType.EXTERNAL_WALL, aspect_type=AspectType.FINISH + elementType=ElementType.EXTERNAL_WALL, aspect_type=AspectType.FINISH ), (120, 3): ElementMapping( - element=ElementType.EXTERNAL_WALL, aspect_type=AspectType.INSULATION + elementType=ElementType.EXTERNAL_WALL, aspect_type=AspectType.INSULATION ), # ========================================================== # EXTERNAL – ROOFS # ========================================================== (50, 15): ElementMapping( - element=ElementType.LOFT_INSULATION, + elementType=ElementType.LOFT_INSULATION, aspect_type=AspectType.TYPE, ), (53, 2): ElementMapping( - element=ElementType.CHIMNEY, + elementType=ElementType.CHIMNEY, aspect_type=AspectType.PRESENCE, ), (53, 6): ElementMapping( - element=ElementType.FASCIA_SOFFIT_BARGEBOARDS, + elementType=ElementType.FASCIA_SOFFIT_BARGEBOARDS, aspect_type=AspectType.MATERIAL, ), (53, 7): ElementMapping( - element=ElementType.FLAT_ROOF_COVERING, + elementType=ElementType.FLAT_ROOF_COVERING, aspect_type=AspectType.MATERIAL, ), (53, 13): ElementMapping( - element=ElementType.GARAGE_ROOF, + elementType=ElementType.GARAGE_ROOF, aspect_type=AspectType.MATERIAL, ), (53, 15): ElementMapping( - element=ElementType.GUTTERS, + elementType=ElementType.GUTTERS, aspect_type=AspectType.MATERIAL, ), (53, 21): ElementMapping( - element=ElementType.PITCHED_ROOF_COVERING, + elementType=ElementType.PITCHED_ROOF_COVERING, aspect_type=AspectType.MATERIAL, ), (53, 22): ElementMapping( - element=ElementType.PORCH_CANOPY, + elementType=ElementType.PORCH_CANOPY, aspect_type=AspectType.TYPE, ), (53, 47): ElementMapping( - element=ElementType.ROOF, + elementType=ElementType.ROOF, aspect_type=AspectType.STRUCTURE, ), (110, 1): ElementMapping( - element=ElementType.ROOF, + elementType=ElementType.ROOF, aspect_type=AspectType.MATERIAL, element_instance=1, ), (110, 2): ElementMapping( - element=ElementType.ROOF, + elementType=ElementType.ROOF, aspect_type=AspectType.MATERIAL, element_instance=1, ), (110, 3): ElementMapping( - element=ElementType.CHIMNEY, + elementType=ElementType.CHIMNEY, aspect_type=AspectType.WORK_REQUIRED, ), (110, 4): ElementMapping( - element=ElementType.FASCIA, + elementType=ElementType.FASCIA, aspect_type=AspectType.MATERIAL, ), (110, 5): ElementMapping( - element=ElementType.SOFFIT, + elementType=ElementType.SOFFIT, aspect_type=AspectType.MATERIAL, ), (110, 6): ElementMapping( - element=ElementType.RAINWATER_GOODS, + elementType=ElementType.RAINWATER_GOODS, aspect_type=AspectType.MATERIAL, ), (110, 7): ElementMapping( - element=ElementType.LOFT_INSULATION, + elementType=ElementType.LOFT_INSULATION, aspect_type=AspectType.WORK_REQUIRED, # possibly not the right aspect type ), (110, 8): ElementMapping( - element=ElementType.PORCH_CANOPY, + elementType=ElementType.PORCH_CANOPY, aspect_type=AspectType.MATERIAL, ), # ========================================================== # EXTERNAL – DOORS & WINDOWS # ========================================================== (50, 8): ElementMapping( - element=ElementType.DOOR_ENTRY_HANDSET, + elementType=ElementType.DOOR_ENTRY_HANDSET, aspect_type=AspectType.PRESENCE, ), (53, 8): ElementMapping( - element=ElementType.FRONT_DOOR, + elementType=ElementType.FRONT_DOOR, aspect_type=AspectType.MATERIAL, ), (53, 12): ElementMapping( - element=ElementType.GARAGE_DOOR, + elementType=ElementType.GARAGE_DOOR, aspect_type=AspectType.MATERIAL, ), (53, 16): ElementMapping( - element=ElementType.LINTEL, + elementType=ElementType.LINTEL, aspect_type=AspectType.PRESENCE, ), (53, 19): ElementMapping( - element=ElementType.PATIO_FRENCH_DOOR, + elementType=ElementType.PATIO_FRENCH_DOOR, aspect_type=AspectType.MATERIAL, ), (53, 25): ElementMapping( - element=ElementType.REAR_DOOR, + elementType=ElementType.REAR_DOOR, aspect_type=AspectType.MATERIAL, ), (53, 29): ElementMapping( - element=ElementType.SECONDARY_GLAZING, + elementType=ElementType.SECONDARY_GLAZING, aspect_type=AspectType.PRESENCE, ), (53, 35): ElementMapping( - element=ElementType.STORE_DOOR, + elementType=ElementType.STORE_DOOR, aspect_type=AspectType.MATERIAL, ), (53, 38): ElementMapping( - element=ElementType.EXTERNAL_WINDOWS, + elementType=ElementType.EXTERNAL_WINDOWS, aspect_type=AspectType.TYPE, element_instance=1, ), (53, 39): ElementMapping( - element=ElementType.EXTERNAL_WINDOWS, + elementType=ElementType.EXTERNAL_WINDOWS, aspect_type=AspectType.TYPE, element_instance=2, ), (53, 43): ElementMapping( - element=ElementType.FRONT_DOOR, + elementType=ElementType.FRONT_DOOR, aspect_type=AspectType.TYPE, ), (130, 1): ElementMapping( - element=ElementType.EXTERNAL_WINDOWS, + elementType=ElementType.EXTERNAL_WINDOWS, aspect_type=AspectType.MATERIAL, ), (130, 2): ElementMapping( - element=ElementType.COMMUNAL_WINDOWS, + elementType=ElementType.COMMUNAL_WINDOWS, aspect_type=AspectType.MATERIAL, ), (140, 1): ElementMapping( - element=ElementType.MAIN_DOOR, + elementType=ElementType.MAIN_DOOR, aspect_type=AspectType.MATERIAL, ), (140, 2): ElementMapping( - element=ElementType.STORE_DOOR, + elementType=ElementType.STORE_DOOR, aspect_type=AspectType.MATERIAL, ), # Duplicate of (53, 35) (140, 3): ElementMapping( - element=ElementType.GARAGE_DOOR, + elementType=ElementType.GARAGE_DOOR, aspect_type=AspectType.MATERIAL, ), # Duplicate of (53, 12) (140, 4): ElementMapping( - element=ElementType.BLOCK_ENTRANCE_DOOR, + elementType=ElementType.BLOCK_ENTRANCE_DOOR, aspect_type=AspectType.MATERIAL, ), # ========================================================== # EXTERNAL AREAS # ========================================================== (53, 3): ElementMapping( - element=ElementType.DOWNPIPES, + elementType=ElementType.DOWNPIPES, aspect_type=AspectType.MATERIAL, ), (53, 9): ElementMapping( - element=ElementType.FRONT_FENCING, + elementType=ElementType.FRONT_FENCING, aspect_type=AspectType.MATERIAL, ), (53, 10): ElementMapping( - element=ElementType.FRONT_GATE, + elementType=ElementType.FRONT_GATE, aspect_type=AspectType.TYPE, ), (53, 17): ElementMapping( - element=ElementType.PARKING_AREAS, + elementType=ElementType.PARKING_AREAS, aspect_type=AspectType.MATERIAL, ), (53, 18): ElementMapping( - element=ElementType.PATHS_AND_HARDSTANDINGS, + elementType=ElementType.PATHS_AND_HARDSTANDINGS, aspect_type=AspectType.MATERIAL, ), (53, 24): ElementMapping( - element=ElementType.PRIVATE_BALCONY, + elementType=ElementType.PRIVATE_BALCONY, aspect_type=AspectType.PRESENCE, ), (53, 26): ElementMapping( - element=ElementType.REAR_FENCING, + elementType=ElementType.REAR_FENCING, aspect_type=AspectType.MATERIAL, ), (53, 27): ElementMapping( - element=ElementType.REAR_GATE, + elementType=ElementType.REAR_GATE, aspect_type=AspectType.TYPE, ), (53, 28): ElementMapping( - element=ElementType.RETAINING_WALLS, + elementType=ElementType.RETAINING_WALLS, aspect_type=AspectType.PRESENCE, ), (53, 31): ElementMapping( - element=ElementType.SIDE_FENCING, + elementType=ElementType.SIDE_FENCING, aspect_type=AspectType.MATERIAL, ), (53, 32): ElementMapping( - element=ElementType.SOIL_AND_VENT, + elementType=ElementType.SOIL_AND_VENT, aspect_type=AspectType.MATERIAL, ), (53, 34): ElementMapping( - element=ElementType.SOLAR_THERMALS, + elementType=ElementType.SOLAR_THERMALS, aspect_type=AspectType.PRESENCE, ), (53, 44): ElementMapping( - element=ElementType.GARAGE_STRUCTURE, + elementType=ElementType.GARAGE_STRUCTURE, aspect_type=AspectType.TYPE, ), (53, 45): ElementMapping( - element=ElementType.BALCONY_BALUSTRADE, + elementType=ElementType.BALCONY_BALUSTRADE, aspect_type=AspectType.MATERIAL, ), (150, 1): ElementMapping( - element=ElementType.BLOCK_ENTRANCE_DOOR, + elementType=ElementType.BLOCK_ENTRANCE_DOOR, aspect_type=AspectType.MATERIAL, ), (150, 2): ElementMapping( - element=ElementType.PATHS_AND_HARDSTANDINGS, + elementType=ElementType.PATHS_AND_HARDSTANDINGS, aspect_type=AspectType.MATERIAL, ), # Duplicate of (53, 18) - correct? (150, 3): ElementMapping( - element=ElementType.ROADS, + elementType=ElementType.ROADS, aspect_type=AspectType.MATERIAL, ), (150, 4): ElementMapping( - element=ElementType.BOUNDARY_WALLS, + elementType=ElementType.BOUNDARY_WALLS, aspect_type=AspectType.MATERIAL, ), (150, 5): ElementMapping( - element=ElementType.OUTBUILDINGS, + elementType=ElementType.OUTBUILDINGS, aspect_type=AspectType.TYPE, ), (150, 6): ElementMapping( - element=ElementType.GARAGE_STRUCTURE, + elementType=ElementType.GARAGE_STRUCTURE, aspect_type=AspectType.TYPE, ), # ========================================================== # INTERNAL – BATHROOMS & KITCHENS # ========================================================== (50, 1): ElementMapping( - element=ElementType.SECONDARY_TOILET, + elementType=ElementType.SECONDARY_TOILET, aspect_type=AspectType.PRESENCE, ), (50, 9): ElementMapping( - element=ElementType.BATHROOM_EXTRACTOR_FAN, + elementType=ElementType.BATHROOM_EXTRACTOR_FAN, aspect_type=AspectType.PRESENCE, ), (50, 9): ElementMapping( - element=ElementType.KITCHEN, + elementType=ElementType.KITCHEN, aspect_type=AspectType.TYPE, ), (50, 10): ElementMapping( - element=ElementType.KITCHEN_EXTRACTOR_FAN, + elementType=ElementType.KITCHEN_EXTRACTOR_FAN, aspect_type=AspectType.PRESENCE, ), (50, 13): ElementMapping( - element=ElementType.KITCHEN_SPACE_LAYOUT, + elementType=ElementType.KITCHEN_SPACE_LAYOUT, aspect_type=AspectType.ADEQUACY, ), (50, 14): ElementMapping( - element=ElementType.KITCHEN, + elementType=ElementType.KITCHEN, aspect_type=AspectType.TYPE, ), (50, 17): ElementMapping( - element=ElementType.BATHROOM, + elementType=ElementType.BATHROOM, aspect_type=AspectType.LOCATION, ), (50, 18): ElementMapping( - element=ElementType.BATHROOM, + elementType=ElementType.BATHROOM, aspect_type=AspectType.TYPE, ), # Actually "Primary bathroom type" - ok like this? (50, 20): ElementMapping( - element=ElementType.BATHROOM, + elementType=ElementType.BATHROOM, aspect_type=AspectType.TYPE, element_instance=2, ), # Actually "Secondary bathroom type" - ok like this? (160, 1): ElementMapping( - element=ElementType.KITCHEN, + elementType=ElementType.KITCHEN, aspect_type=AspectType.CONDITION, ), (160, 2): ElementMapping( - element=ElementType.KITCHEN_SPACE_LAYOUT, + elementType=ElementType.KITCHEN_SPACE_LAYOUT, aspect_type=AspectType.ADEQUACY, ), (190, 1): ElementMapping( - element=ElementType.BATHROOM, + elementType=ElementType.BATHROOM, aspect_type=AspectType.CONDITION, ), (190, 2): ElementMapping( - element=ElementType.SECONDARY_TOILET, + elementType=ElementType.SECONDARY_TOILET, aspect_type=AspectType.TYPE, ), # ========================================================== # COMMUNAL # ========================================================== (51, 1): ElementMapping( - element=ElementType.COMMUNAL_AERIAL, + elementType=ElementType.COMMUNAL_AERIAL, aspect_type=AspectType.PRESENCE, ), (51, 2): ElementMapping( - element=ElementType.COMMUNAL_AOV, + elementType=ElementType.COMMUNAL_AOV, aspect_type=AspectType.PRESENCE, ), (51, 3): ElementMapping( - element=ElementType.COMMUNAL_BALCONY_WALKWAY, + elementType=ElementType.COMMUNAL_BALCONY_WALKWAY, aspect_type=AspectType.PRESENCE, ), (51, 4): ElementMapping( - element=ElementType.COMMUNAL_BATHROOM, + elementType=ElementType.COMMUNAL_BATHROOM, aspect_type=AspectType.TYPE, ), (51, 5): ElementMapping( - element=ElementType.COMMUNAL_BIN_STORE_DOORS, + elementType=ElementType.COMMUNAL_BIN_STORE_DOORS, aspect_type=AspectType.PRESENCE, ), (51, 6): ElementMapping( - element=ElementType.COMMUNAL_BIN_STORE_ROOF, + elementType=ElementType.COMMUNAL_BIN_STORE_ROOF, aspect_type=AspectType.PRESENCE, ), (51, 7): ElementMapping( - element=ElementType.COMMUNAL_BIN_STORE_WALLS, + elementType=ElementType.COMMUNAL_BIN_STORE_WALLS, aspect_type=AspectType.MATERIAL, ), (51, 8): ElementMapping( - element=ElementType.COMMUNAL_BMS, + elementType=ElementType.COMMUNAL_BMS, aspect_type=AspectType.PRESENCE, ), (51, 9): ElementMapping( - element=ElementType.COMMUNAL_BOILER, + elementType=ElementType.COMMUNAL_BOILER, aspect_type=AspectType.TYPE, ), (51, 10): ElementMapping( - element=ElementType.COMMUNAL_BOOSTER_PUMP, + elementType=ElementType.COMMUNAL_BOOSTER_PUMP, aspect_type=AspectType.PRESENCE, ), (51, 11): ElementMapping( - element=ElementType.COMMUNAL_CCTV, + elementType=ElementType.COMMUNAL_CCTV, aspect_type=AspectType.PRESENCE, ), (51, 12): ElementMapping( - element=ElementType.COMMUNAL_CIRCULATION_SPACE, + elementType=ElementType.COMMUNAL_CIRCULATION_SPACE, aspect_type=AspectType.ADEQUACY, ), (51, 13): ElementMapping( - element=ElementType.COMMUNAL_COLD_WATER_STORAGE, + elementType=ElementType.COMMUNAL_COLD_WATER_STORAGE, aspect_type=AspectType.PRESENCE, ), (51, 14): ElementMapping( - element=ElementType.COMMUNAL_DOOR_ENTRY, + elementType=ElementType.COMMUNAL_DOOR_ENTRY, aspect_type=AspectType.SYSTEM, ), (51, 15): ElementMapping( - element=ElementType.COMMUNAL_DRY_RISER, + elementType=ElementType.COMMUNAL_DRY_RISER, aspect_type=AspectType.PRESENCE, ), (51, 16): ElementMapping( - element=ElementType.COMMUNAL_EMERGENCY_LIGHTING, + elementType=ElementType.COMMUNAL_EMERGENCY_LIGHTING, aspect_type=AspectType.PRESENCE, ), (51, 17): ElementMapping( - element=ElementType.COMMUNAL_EXTERNAL_DOORS, + elementType=ElementType.COMMUNAL_EXTERNAL_DOORS, aspect_type=AspectType.MATERIAL, ), (51, 19): ElementMapping( - element=ElementType.COMMUNAL_FIRE_ALARM, + elementType=ElementType.COMMUNAL_FIRE_ALARM, aspect_type=AspectType.TYPE, ), (51, 20): ElementMapping( - element=ElementType.COMMUNAL_INTERNAL_DECORATIONS, + elementType=ElementType.COMMUNAL_INTERNAL_DECORATIONS, aspect_type=AspectType.PRESENCE, ), (51, 21): ElementMapping( - element=ElementType.COMMUNAL_INTERNAL_DOORS, + elementType=ElementType.COMMUNAL_INTERNAL_DOORS, aspect_type=AspectType.MATERIAL, ), (51, 22): ElementMapping( - element=ElementType.COMMUNAL_INTERNAL_FLOOR, + elementType=ElementType.COMMUNAL_INTERNAL_FLOOR, aspect_type=AspectType.FINISH, ), (51, 23): ElementMapping( - element=ElementType.COMMUNAL_KITCHEN, + elementType=ElementType.COMMUNAL_KITCHEN, aspect_type=AspectType.TYPE, ), (51, 24): ElementMapping( - element=ElementType.COMMUNAL_LATERAL_MAINS, + elementType=ElementType.COMMUNAL_LATERAL_MAINS, aspect_type=AspectType.PRESENCE, ), (51, 25): ElementMapping( - element=ElementType.COMMUNAL_LIGHTING, + elementType=ElementType.COMMUNAL_LIGHTING, aspect_type=AspectType.PRESENCE, ), (51, 26): ElementMapping( - element=ElementType.COMMUNAL_LIGHTING_CONDUCTOR, + elementType=ElementType.COMMUNAL_LIGHTING_CONDUCTOR, aspect_type=AspectType.PRESENCE, ), (51, 27): ElementMapping( - element=ElementType.COMMUNAL_PASSENGER_LIFT, + elementType=ElementType.COMMUNAL_PASSENGER_LIFT, aspect_type=AspectType.TYPE, ), (51, 28): ElementMapping( - element=ElementType.COMMUNAL_ENTRANCE, + elementType=ElementType.COMMUNAL_ENTRANCE, aspect_type=AspectType.MATERIAL, element_instance=1, ), (51, 30): ElementMapping( - element=ElementType.COMMUNAL_ENTRANCE, + elementType=ElementType.COMMUNAL_ENTRANCE, aspect_type=AspectType.FINISH, element_instance=2, ), (51, 31): ElementMapping( - element=ElementType.COMMUNAL_SPRINKLER, + elementType=ElementType.COMMUNAL_SPRINKLER, aspect_type=AspectType.PRESENCE, ), (51, 29): ElementMapping( - element=ElementType.COMMUNAL_REFUSE_CHUTE, + elementType=ElementType.COMMUNAL_REFUSE_CHUTE, aspect_type=AspectType.PRESENCE, ), (51, 32): ElementMapping( - element=ElementType.COMMUNAL_STAIRS, + elementType=ElementType.COMMUNAL_STAIRS, aspect_type=AspectType.FINISH, ), (51, 33): ElementMapping( - element=ElementType.COMMUNAL_STORE_DOORS, + elementType=ElementType.COMMUNAL_STORE_DOORS, aspect_type=AspectType.MATERIAL, ), (51, 34): ElementMapping( - element=ElementType.COMMUNAL_STORE_ROOF, + elementType=ElementType.COMMUNAL_STORE_ROOF, aspect_type=AspectType.MATERIAL, ), (51, 35): ElementMapping( - element=ElementType.COMMUNAL_STORE_WALLS, + elementType=ElementType.COMMUNAL_STORE_WALLS, aspect_type=AspectType.MATERIAL, ), (51, 36): ElementMapping( - element=ElementType.COMMUNAL_WALKWAYS, + elementType=ElementType.COMMUNAL_WALKWAYS, aspect_type=AspectType.FINISH, ), (51, 37): ElementMapping( - element=ElementType.COMMUNAL_WARDEN_CALL_SYSTEM, + elementType=ElementType.COMMUNAL_WARDEN_CALL_SYSTEM, aspect_type=AspectType.PRESENCE, ), (51, 38): ElementMapping( - element=ElementType.COMMUNAL_TOILETS, + elementType=ElementType.COMMUNAL_TOILETS, aspect_type=AspectType.TYPE, ), (51, 39): ElementMapping( - element=ElementType.COMMUNAL_WET_RISER, + elementType=ElementType.COMMUNAL_WET_RISER, aspect_type=AspectType.PRESENCE, ), (51, 40): ElementMapping( - element=ElementType.COMMUNAL_PLUG_SOCKETS, + elementType=ElementType.COMMUNAL_PLUG_SOCKETS, aspect_type=AspectType.PRESENCE, ), (200, 1): ElementMapping( - element=ElementType.COMMUNAL_BOILER, + elementType=ElementType.COMMUNAL_BOILER, aspect_type=AspectType.TYPE, ), # Duplicate of (51, 9) - correct? (200, 2): ElementMapping( - element=ElementType.COMMUNAL_HEATING, + elementType=ElementType.COMMUNAL_HEATING, aspect_type=AspectType.TYPE, ), (200, 3): ElementMapping( - element=ElementType.COMMUNAL_ELECTRICS, + elementType=ElementType.COMMUNAL_ELECTRICS, aspect_type=AspectType.TYPE, ), (200, 4): ElementMapping( - element=ElementType.COMMUNAL_FIRE_ALARM, + elementType=ElementType.COMMUNAL_FIRE_ALARM, aspect_type=AspectType.TYPE, ), (200, 5): ElementMapping( - element=ElementType.COMMUNAL_LIFT, + elementType=ElementType.COMMUNAL_LIFT, aspect_type=AspectType.TYPE, ), (200, 6): ElementMapping( - element=ElementType.COMMUNAL_FLOOR_COVERING, + elementType=ElementType.COMMUNAL_FLOOR_COVERING, aspect_type=AspectType.MATERIAL, ), (200, 7): ElementMapping( - element=ElementType.COMMUNAL_KITCHEN, + elementType=ElementType.COMMUNAL_KITCHEN, aspect_type=AspectType.TYPE, ), (200, 8): ElementMapping( - element=ElementType.COMMUNAL_BATHROOM, + elementType=ElementType.COMMUNAL_BATHROOM, aspect_type=AspectType.TYPE, ), # Duplicate of (51, 4) - correct? (200, 9): ElementMapping( - element=ElementType.COMMUNAL_TOILETS, + elementType=ElementType.COMMUNAL_TOILETS, aspect_type=AspectType.TYPE, ), # Duplicate of (51, 38) - correct? (200, 10): ElementMapping( - element=ElementType.COMMUNAL_GATES, + elementType=ElementType.COMMUNAL_GATES, aspect_type=AspectType.TYPE, ), # ========================================================== # INTERNAL – HEATING # ========================================================== (50, 4): ElementMapping( - element=ElementType.HEATING_BOILER, + elementType=ElementType.HEATING_BOILER, aspect_type=AspectType.PRESENCE, ), # This is actually "Central heating boiler" - ok like this? (50, 5): ElementMapping( - element=ElementType.CENTRAL_HEATING, + elementType=ElementType.CENTRAL_HEATING, aspect_type=AspectType.EXTENT, ), (50, 6): ElementMapping( - element=ElementType.COLD_WATER_STORAGE, + elementType=ElementType.COLD_WATER_STORAGE, aspect_type=AspectType.PRESENCE, ), (50, 12): ElementMapping( - element=ElementType.HEATING_DISTRIBUTION, + elementType=ElementType.HEATING_DISTRIBUTION, aspect_type=AspectType.TYPE, ), (50, 19): ElementMapping( - element=ElementType.PROGRAMMABLE_HEATING, + elementType=ElementType.PROGRAMMABLE_HEATING, aspect_type=AspectType.TYPE, ), (50, 25): ElementMapping( - element=ElementType.HEATING_BOILER, + elementType=ElementType.HEATING_BOILER, aspect_type=AspectType.TYPE, ), (170, 1): ElementMapping( - element=ElementType.HEATING_BOILER, + elementType=ElementType.HEATING_BOILER, aspect_type=AspectType.TYPE, ), # Duplicate of (50,25) - correct? (170, 2): ElementMapping( - element=ElementType.HEATING_DISTRIBUTION, + elementType=ElementType.HEATING_DISTRIBUTION, aspect_type=AspectType.TYPE, ), # Duplicate of (50,12) - correct? (170, 3): ElementMapping( - element=ElementType.SECONDARY_HEATING, + elementType=ElementType.SECONDARY_HEATING, aspect_type=AspectType.TYPE, ), (170, 4): ElementMapping( - element=ElementType.COLD_WATER_STORAGE, + elementType=ElementType.COLD_WATER_STORAGE, aspect_type=AspectType.TYPE, ), (170, 5): ElementMapping( - element=ElementType.HOT_WATER_SYSTEM, + elementType=ElementType.HOT_WATER_SYSTEM, aspect_type=AspectType.TYPE, ), # ========================================================== # ELECTRICS # ========================================================== (50, 24): ElementMapping( - element=ElementType.INTERNAL_WIRING, + elementType=ElementType.INTERNAL_WIRING, aspect_type=AspectType.MATERIAL, ), (180, 1): ElementMapping( - element=ElementType.ELECTRICAL_WIRING, + elementType=ElementType.ELECTRICAL_WIRING, aspect_type=AspectType.WORK_REQUIRED, ), # Not certain about the AspectType - only example in the sample data is "Full Rewire" (180, 2): ElementMapping( - element=ElementType.CONSUMER_UNIT, + elementType=ElementType.CONSUMER_UNIT, aspect_type=AspectType.TYPE, ), (180, 3): ElementMapping( - element=ElementType.SMOKE_DETECTION, + elementType=ElementType.SMOKE_DETECTION, aspect_type=AspectType.TYPE, ), # Duplicate of (50, 21) - correct? (180, 4): ElementMapping( - element=ElementType.CARBON_MONOXIDE_DETECTION, + elementType=ElementType.CARBON_MONOXIDE_DETECTION, aspect_type=AspectType.TYPE, ), # Duplicate of (50, 2) - correct? # ========================================================== # HHSRS # ========================================================== (54, 1): ElementMapping( - element=ElementType.HHSRS_DAMP_AND_MOULD, + elementType=ElementType.HHSRS_DAMP_AND_MOULD, aspect_type=AspectType.RISK, ), (54, 4): ElementMapping( - element=ElementType.HHSRS_ASBESTOS_AND_MMF, + elementType=ElementType.HHSRS_ASBESTOS_AND_MMF, aspect_type=AspectType.RISK, ), (54, 15): ElementMapping( - element=ElementType.HHSRS_DOMESTIC_HYGIENE_PESTS_REFUSE, + elementType=ElementType.HHSRS_DOMESTIC_HYGIENE_PESTS_REFUSE, aspect_type=AspectType.RISK, ), (54, 29): ElementMapping( - element=ElementType.HHSRS_STRUCTURAL_COLLAPSE, + elementType=ElementType.HHSRS_STRUCTURAL_COLLAPSE, aspect_type=AspectType.RISK, ), } diff --git a/backend/condition/domain/mapping/peabody/peabody_mapper.py b/backend/condition/domain/mapping/peabody/peabody_mapper.py index 184b2898..92f1687f 100644 --- a/backend/condition/domain/mapping/peabody/peabody_mapper.py +++ b/backend/condition/domain/mapping/peabody/peabody_mapper.py @@ -37,7 +37,7 @@ class PeabodyMapper(Mapper): ) element_key = ( - element_mapping.element, + element_mapping.elementType, element_mapping.element_instance or 1, ) From 0d9ee79c40a60a32f94fba86c634a4bc13a64e6e Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 27 Jan 2026 16:25:21 +0000 Subject: [PATCH 57/68] adjust some element mappings for consistency between systems --- .../domain/mapping/lbwf/lbwf_element_map.py | 26 +++--- .../mapping/peabody/peabody_element_map.py | 80 ++++++++++++------- 2 files changed, 61 insertions(+), 45 deletions(-) diff --git a/backend/condition/domain/mapping/lbwf/lbwf_element_map.py b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py index a547fe5c..bf54c5bb 100644 --- a/backend/condition/domain/mapping/lbwf/lbwf_element_map.py +++ b/backend/condition/domain/mapping/lbwf/lbwf_element_map.py @@ -165,17 +165,14 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { "EXTWALLSTR": ElementMapping( elementType=ElementType.EXTERNAL_WALL, aspect_type=AspectType.STRUCTURE, - element_instance=1, ), "EXTWALLFN1": ElementMapping( elementType=ElementType.EXTERNAL_WALL, aspect_type=AspectType.FINISH, - element_instance=1, ), "EXTWALLFN2": ElementMapping( elementType=ElementType.EXTERNAL_WALL, aspect_type=AspectType.FINISH, - element_instance=1, aspect_instance=2, ), "EXTWALLINS": ElementMapping( @@ -200,32 +197,30 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { "EXTRFSTR1": ElementMapping( elementType=ElementType.ROOF, aspect_type=AspectType.STRUCTURE, - element_instance=1, ), "EXTRFSTR2": ElementMapping( elementType=ElementType.ROOF, aspect_type=AspectType.STRUCTURE, - element_instance=2, + aspect_instance=2, ), "EXTRFSTR3": ElementMapping( elementType=ElementType.ROOF, aspect_type=AspectType.STRUCTURE, - element_instance=3, + aspect_instance=3, ), "EXTROOF1": ElementMapping( elementType=ElementType.ROOF, - aspect_type=AspectType.COVERING, - element_instance=1, + aspect_type=AspectType.MATERIAL, ), "EXTROOF2": ElementMapping( elementType=ElementType.ROOF, - aspect_type=AspectType.COVERING, - element_instance=2, + aspect_type=AspectType.MATERIAL, + aspect_instance=2, ), "EXTROOF3": ElementMapping( elementType=ElementType.ROOF, - aspect_type=AspectType.COVERING, - element_instance=3, + aspect_type=AspectType.MATERIAL, + aspect_instance=3, ), "EXTCHIMNEY": ElementMapping( elementType=ElementType.CHIMNEY, @@ -265,12 +260,11 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { "EXTBKSDDR1": ElementMapping( elementType=ElementType.EXTERNAL_DOOR, aspect_type=AspectType.TYPE, - element_instance=1, ), "EXTBKSDDR2": ElementMapping( elementType=ElementType.EXTERNAL_DOOR, aspect_type=AspectType.TYPE, - element_instance=2, + aspect_instance=2, ), "INTWDWTYPE": ElementMapping( elementType=ElementType.EXTERNAL_WINDOWS, @@ -279,12 +273,11 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { "EXTWNDWS1": ElementMapping( elementType=ElementType.EXTERNAL_WINDOWS, aspect_type=AspectType.TYPE, - element_instance=1, ), "EXTWNDWS2": ElementMapping( elementType=ElementType.EXTERNAL_WINDOWS, aspect_type=AspectType.TYPE, - element_instance=2, + aspect_instance=2, ), "EXTGARDOOR": ElementMapping( elementType=ElementType.GARAGE_DOOR, @@ -317,7 +310,6 @@ LBWF_ELEMENT_MAP: dict[str, ElementMapping] = { "EXTPTFRDR1": ElementMapping( elementType=ElementType.PATIO_FRENCH_DOOR, aspect_type=AspectType.MATERIAL, - element_instance=1, ), # ========================================================== # EXTERNAL AREAS diff --git a/backend/condition/domain/mapping/peabody/peabody_element_map.py b/backend/condition/domain/mapping/peabody/peabody_element_map.py index 2281a17c..ce344b9a 100644 --- a/backend/condition/domain/mapping/peabody/peabody_element_map.py +++ b/backend/condition/domain/mapping/peabody/peabody_element_map.py @@ -8,62 +8,81 @@ PEABODY_ELEMENT_MAP = { # PROPERTY / GENERAL # ========================================================== (100, 1): ElementMapping( - elementType=ElementType.PROPERTY, aspect_type=AspectType.TYPE + elementType=ElementType.PROPERTY, + aspect_type=AspectType.TYPE, ), # (100, 3): ElementMapping(element=Element.PROPERTY, aspect_type=AspectType.AGE), # (100, 14): ElementMapping(element="property", aspect_type="construction_type"), (50, 2): ElementMapping( - elementType=ElementType.CARBON_MONOXIDE_DETECTION, aspect_type=AspectType.TYPE + elementType=ElementType.CARBON_MONOXIDE_DETECTION, + aspect_type=AspectType.TYPE, + ), + (50, 3): ElementMapping( + elementType=ElementType.CCU, + aspect_type=AspectType.TYPE, ), - (50, 3): ElementMapping(elementType=ElementType.CCU, aspect_type=AspectType.TYPE), (50, 7): ElementMapping( - elementType=ElementType.DISABLED_HOIST_TRACKING, aspect_type=AspectType.PRESENCE + elementType=ElementType.DISABLED_HOIST_TRACKING, + aspect_type=AspectType.PRESENCE, ), (50, 11): ElementMapping( - elementType=ElementType.HEAT_DETECTION, aspect_type=AspectType.TYPE + elementType=ElementType.HEAT_DETECTION, + aspect_type=AspectType.TYPE, ), (50, 21): ElementMapping( - elementType=ElementType.SMOKE_DETECTION, aspect_type=AspectType.TYPE + elementType=ElementType.SMOKE_DETECTION, + aspect_type=AspectType.TYPE, ), (50, 22): ElementMapping( - elementType=ElementType.STAIRLIFT, aspect_type=AspectType.PRESENCE + elementType=ElementType.STAIRLIFT, + aspect_type=AspectType.PRESENCE, ), (50, 26): ElementMapping( - elementType=ElementType.DISABLED_FACILITIES, aspect_type=AspectType.TYPE + elementType=ElementType.DISABLED_FACILITIES, + aspect_type=AspectType.TYPE, ), (100, 3): ElementMapping( - elementType=ElementType.PROPERTY, aspect_type=AspectType.AGE_BAND + elementType=ElementType.PROPERTY, + aspect_type=AspectType.AGE_BAND, ), (100, 14): ElementMapping( - elementType=ElementType.PROPERTY, aspect_type=AspectType.CONSTRUCTION_TYPE + elementType=ElementType.PROPERTY, + aspect_type=AspectType.CONSTRUCTION_TYPE, ), (100, 16): ElementMapping( - elementType=ElementType.PROPERTY, aspect_type=AspectType.CLASSIFICATION + elementType=ElementType.PROPERTY, + aspect_type=AspectType.CLASSIFICATION, ), (210, 2): ElementMapping( - elementType=ElementType.PASSENGER_LIFT, aspect_type=AspectType.TYPE + elementType=ElementType.PASSENGER_LIFT, + aspect_type=AspectType.TYPE, ), # ========================================================== # EXTERNAL – WALLS # ========================================================== (50, 16): ElementMapping( - elementType=ElementType.PARTY_WALL_FIRE_BREAK, aspect_type=AspectType.PRESENCE + elementType=ElementType.PARTY_WALL_FIRE_BREAK, + aspect_type=AspectType.PRESENCE, ), (53, 1): ElementMapping( - elementType=ElementType.BOUNDARY_WALLS, aspect_type=AspectType.PRESENCE + elementType=ElementType.BOUNDARY_WALLS, + aspect_type=AspectType.PRESENCE, ), (53, 4): ElementMapping( - elementType=ElementType.EXTERNAL_DECORATION, aspect_type=AspectType.PRESENCE + elementType=ElementType.EXTERNAL_DECORATION, + aspect_type=AspectType.PRESENCE, ), (53, 5): ElementMapping( elementType=ElementType.EXTERNAL_NOISE_INSULATION, aspect_type=AspectType.ADEQUACY, ), (53, 14): ElementMapping( - elementType=ElementType.GARAGE_WALLS, aspect_type=AspectType.MATERIAL + elementType=ElementType.GARAGE_WALLS, + aspect_type=AspectType.MATERIAL, ), (53, 23): ElementMapping( - elementType=ElementType.EXTERNAL_WALL, aspect_type=AspectType.FINISH + elementType=ElementType.EXTERNAL_WALL, + aspect_type=AspectType.FINISH, ), (53, 30): ElementMapping( elementType=ElementType.EXTERNAL_WALL, @@ -71,25 +90,32 @@ PEABODY_ELEMENT_MAP = { aspect_instance=2, ), (53, 36): ElementMapping( - elementType=ElementType.EXTERNAL_WALL, aspect_type=AspectType.INSULATION + elementType=ElementType.EXTERNAL_WALL, + aspect_type=AspectType.INSULATION, ), (53, 40): ElementMapping( - elementType=ElementType.SPANDREL_PANELS, aspect_type=AspectType.MATERIAL + elementType=ElementType.SPANDREL_PANELS, + aspect_type=AspectType.MATERIAL, ), (53, 41): ElementMapping( - elementType=ElementType.CLADDING, aspect_type=AspectType.MATERIAL + elementType=ElementType.CLADDING, + aspect_type=AspectType.MATERIAL, ), (100, 15): ElementMapping( - elementType=ElementType.EXTERNAL_DECORATION, aspect_type=AspectType.CONDITION + elementType=ElementType.EXTERNAL_DECORATION, + aspect_type=AspectType.CONDITION, ), (120, 1): ElementMapping( - elementType=ElementType.EXTERNAL_WALL, aspect_type=AspectType.STRUCTURE + elementType=ElementType.EXTERNAL_WALL, + aspect_type=AspectType.STRUCTURE, ), (120, 2): ElementMapping( - elementType=ElementType.EXTERNAL_WALL, aspect_type=AspectType.FINISH + elementType=ElementType.EXTERNAL_WALL, + aspect_type=AspectType.FINISH, ), (120, 3): ElementMapping( - elementType=ElementType.EXTERNAL_WALL, aspect_type=AspectType.INSULATION + elementType=ElementType.EXTERNAL_WALL, + aspect_type=AspectType.INSULATION, ), # ========================================================== # EXTERNAL – ROOFS @@ -133,12 +159,11 @@ PEABODY_ELEMENT_MAP = { (110, 1): ElementMapping( elementType=ElementType.ROOF, aspect_type=AspectType.MATERIAL, - element_instance=1, ), (110, 2): ElementMapping( elementType=ElementType.ROOF, aspect_type=AspectType.MATERIAL, - element_instance=1, + aspect_instance=1, ), (110, 3): ElementMapping( elementType=ElementType.CHIMNEY, @@ -202,12 +227,11 @@ PEABODY_ELEMENT_MAP = { (53, 38): ElementMapping( elementType=ElementType.EXTERNAL_WINDOWS, aspect_type=AspectType.TYPE, - element_instance=1, ), (53, 39): ElementMapping( elementType=ElementType.EXTERNAL_WINDOWS, aspect_type=AspectType.TYPE, - element_instance=2, + aspect_instance=2, ), (53, 43): ElementMapping( elementType=ElementType.FRONT_DOOR, From 60241f947e6991559d7d4f2adfc929dc2becc02a Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Tue, 27 Jan 2026 16:57:59 +0000 Subject: [PATCH 58/68] add readme --- backend/condition/README.md | 75 +++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 backend/condition/README.md diff --git a/backend/condition/README.md b/backend/condition/README.md new file mode 100644 index 00000000..140d4585 --- /dev/null +++ b/backend/condition/README.md @@ -0,0 +1,75 @@ +# Condition Data Processor + +The Condition Data Processor performs the following steps: + +- **Extract** + - Ingest client Condition Survey data files (currently from local files; future support planned for S3 and internal survey sources) + - Parse input files into Data Transfer Objects (DTOs) + +- **Transform** + - Map source data into the internal domain data model + +- **Load** + - Persist transformed data into the ARA database (not yet implemented) + +The processor currently supports file formats provided by **Peabody** and **LBWF**. + +--- + +## Running Locally + +The `local_runner` script allows the processor to be executed in a local environment. + +1. Copy a sample input file into the `sample_data/` directory. +2. Update `local_runner.py` as required, specifically the definitions of: + - `lbwf_path` + - `peabody_path` + - `file_paths` +3. Run `local_runner.py`. + Breakpoints may be added and the script run in debug mode if required. + +--- + +## Known Data Issues + +Some inconsistencies exist in the source datasets, primarily involving multiple representations of the same logical element within a single file. In these cases, assumptions have been made in order to normalise the data into the internal domain model. + +### Peabody Data – Wall Finish Mapping + +In the original Peabody sample dataset, multiple Element/Sub-Element combinations correspond to wall finishes: + +| Element_Code | Element | Sub_Element_Code | Sub_Element | +|--------------|----------|------------------|-----------------------| +| 53 | External | 23 | Primary Wall Finish | +| 53 | External | 30 | Secondary Wall Finish | +| 120 | WALLS | 2 | Wall Finish | + +A single property may contain records for all three combinations, and each combination may appear multiple times. + +For example, the property at **55 Burnaby Street, London** contains entries for all three of the above combinations. However, it contains only a single entry for *“WALLS: Wall structure”*, indicating that the property has only one structure rather than multiple. + +This pattern is also observed in other sampled properties. Based on this, the following assumption is applied: + +- “Secondary” refers to a secondary **finish**, not a secondary **wall**. + +As a result: +- The property is mapped to a single Wall element. +- That Wall element is assigned three Finish aspects: + - Two with `aspect_instance = 1` + - One with `aspect_instance = 2` + +This means that the combination of +`UPRN / ElementType / ElementInstance / AspectType / AspectInstance` +is **not guaranteed to be unique**. + +### LBWF Data – Wall Finish Mapping + +In the LBWF dataset, the following element codes map to wall finishes: + +- `EXTWALLFN1` +- `EXTWALLFN2` + +These are similarly mapped as multiple instances of the **Finish** aspect for a single Wall element. + +--- + From 79ef0805c3d8e12082ff06b295949ac1ec707c6a Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 27 Jan 2026 19:04:37 +0000 Subject: [PATCH 59/68] handling ambiguous cases for sloping ceiling vs loft insulation --- backend/Property.py | 2 + backend/engine/engine.py | 5 +- etl/find_my_epc/RetrieveFindMyEpc.py | 59 ++++- recommendations/RoofRecommendations.py | 217 ++++++++++++++-- .../tests/test_roof_recommendations.py | 241 ++++++++++++++++-- 5 files changed, 477 insertions(+), 47 deletions(-) diff --git a/backend/Property.py b/backend/Property.py index fa607cfd..c320fdd8 100644 --- a/backend/Property.py +++ b/backend/Property.py @@ -84,6 +84,7 @@ class Property: uprn=None, # Pass as an optional input property_valuation=None, already_installed=None, + find_my_epc_components=None, non_invasive_recommendations=None, measures=None, energy_assessment=None, @@ -114,6 +115,7 @@ class Property: non_invasive_recommendations['recommendations'] if non_invasive_recommendations else [] ) + self.find_my_epc_components = find_my_epc_components # Store the find my epc components # This is a list of measures that have been recommended for the property if isinstance(measures, list): self.measures = measures diff --git a/backend/engine/engine.py b/backend/engine/engine.py index a9156078..67353b7a 100644 --- a/backend/engine/engine.py +++ b/backend/engine/engine.py @@ -796,9 +796,9 @@ async def model_engine(body: PlanTriggerRequest): property_non_invasive_recommendations, patch = req_data.non_invasive_recommendations, req_data.patch # if we have a remote assment data type, we pull the additional data and include it - epc_page_source = {} + epc_page_source, find_my_epc_components = {}, [] if (body.event_type == "remote_assessment") and not (epc_searcher.newest_epc.get("estimated")): - property_non_invasive_recommendations, patch, epc_page_source = ( + property_non_invasive_recommendations, patch, epc_page_source, find_my_epc_components = ( RetrieveFindMyEpc.get_from_epc_with_fallback( epc=epc_searcher.newest_epc, epc_page=epc_page, @@ -834,6 +834,7 @@ async def model_engine(body: PlanTriggerRequest): postcode=epc_searcher.postcode_clean, epc_record=prepared_epc, already_installed=property_already_installed + eco_packages.get(property_id)[3], + find_my_epc_components=find_my_epc_components, property_valuation=req_data.valuation, non_invasive_recommendations=property_non_invasive_recommendations, energy_assessment=energy_assessment, diff --git a/etl/find_my_epc/RetrieveFindMyEpc.py b/etl/find_my_epc/RetrieveFindMyEpc.py index 82215443..8bdc45c5 100644 --- a/etl/find_my_epc/RetrieveFindMyEpc.py +++ b/etl/find_my_epc/RetrieveFindMyEpc.py @@ -36,6 +36,9 @@ class RetrieveFindMyEpc: self.rrn = rrn self.address_cleaned = self.address.replace(",", "").replace(" ", "").lower() + + # Containers for the extracted components + self.property_components = [] self.walls = [] self.address_postal_town = address_postal_town @@ -256,12 +259,10 @@ class RetrieveFindMyEpc: property_features_table = soup.find("tbody", class_="govuk-table__body") property_features_table = property_features_table.find_all("tr") - # Extract wall types - self.walls = [] - for row in property_features_table: - cells = row.find_all("td") - if row.find("th").text.strip() == "Wall": - self.walls.append(cells[0].text.strip()) + self.extract_property_components(property_features_table) + + # Extract walls + self.walls = [x["description"] for x in self.property_components if x["component_name"] == "Wall"] # Finally, we format the recommendations recommendations = self.format_recommendations(recommendations, assessment_data, sap_2012_date) @@ -424,6 +425,37 @@ class RetrieveFindMyEpc: return chosen_epc, epc_certificate + @staticmethod + def extract_property_components(property_features_table: list): + """ + Function to pull out a table for property components, marking their appearance index + :param property_features_table: The table of property features, as extracted by BeautifulSoup + :return: List of property components with appearance index + """ + property_components = [] + for row in property_features_table: + cells = row.find_all("td") + component_name = row.find("th").text.strip() + property_components.append( + { + "component_name": component_name, + "description": cells[0].text.strip(), + "efficiency": cells[1].text.strip(), + } + ) + # Add an appearance index, which will indicate if the component appears multiple times, so this + # becomes a reference for the building part the component is associated to (main, extensions, etc) + # We want to inject this appearance index into the component dictionaries + component_count = {} + for component in property_components: + name = component['component_name'] + if name not in component_count: + component_count[name] = 0 + component['appearance_index'] = component_count[name] + component_count[name] += 1 + + return property_components + def retrieve_newest_find_my_epc_data( self, sap_2012_date=None, return_page=False, epc_page_source=None, rrn=None ): @@ -577,12 +609,10 @@ class RetrieveFindMyEpc: property_features_table = address_res.find("tbody", class_="govuk-table__body") property_features_table = property_features_table.find_all("tr") - # Extract wall types - self.walls = [] - for row in property_features_table: - cells = row.find_all("td") - if row.find("th").text.strip() == "Wall": - self.walls.append(cells[0].text.strip()) + property_components = self.extract_property_components(property_features_table) + + # Extract walls + self.walls = [x["description"] for x in self.property_components if x["component_name"] == "Wall"] # Finally, we format the recommendations recommendations = self.format_recommendations(recommendations, assessment_data, sap_2012_date) @@ -615,6 +645,7 @@ class RetrieveFindMyEpc: "heating_text": heating_text, "hot_water_text": hot_water_text, "recommendations": recommendations, + "property_components": property_components, "epc_data": epc_data, **assessment_data, **low_carbon_energy_sources, @@ -804,7 +835,9 @@ class RetrieveFindMyEpc: "page_source": find_epc_data.get("page_source") } - return non_invasive_recommendations, patch, page_source + property_components = find_epc_data.get("property_components", []) + + return non_invasive_recommendations, patch, page_source, property_components @classmethod def get_from_epc_with_fallback( diff --git a/recommendations/RoofRecommendations.py b/recommendations/RoofRecommendations.py index d7eb5fbb..bcaea687 100644 --- a/recommendations/RoofRecommendations.py +++ b/recommendations/RoofRecommendations.py @@ -11,6 +11,7 @@ from recommendations.recommendation_utils import ( ) from recommendations.Costs import Costs from etl.epc_clean.epc_attributes.RoofAttributes import RoofAttributes +from backend.app.plan.schemas import ROOF_INSULATION_MEASURES class RoofRecommendations: @@ -122,7 +123,7 @@ class RoofRecommendations: @staticmethod def is_sloping_ceiling_appropriate( is_pitched: bool, is_loft: bool, is_assumed: bool, has_sloping_ceiling_recommendation: bool, - primary_roof_is_sloped: bool + primary_roof_is_sloped: bool, insulation_thickness: str ) -> bool: """ @@ -133,6 +134,7 @@ class RoofRecommendations: recommendation :param primary_roof_is_sloped: Boolean - indicates if the primary room is described a sloped (as opposed to an extension) + :param insulation_thickness: String - insulation thickness of the roof :return: """ # We need to check: @@ -141,11 +143,27 @@ class RoofRecommendations: # 3) Is the insulation status NOT assumed # 4) Is there a sloping ceiling recommendation (this may relate to the primary or secondary roof) - # If we have a loft primary roof and sloping cei + # If we have a loft primary roof and sloping ceiling + + has_suitable_features = ( + is_pitched and not is_loft and not is_assumed and primary_roof_is_sloped + ) + + # Check if it needs a recommendation + needs_recommendation_condition1 = has_sloping_ceiling_recommendation | ( + insulation_thickness in ["below average"] + ) + + needs_recommendation_condition2 = has_sloping_ceiling_recommendation & ( + insulation_thickness in ["none"] + ) + + # If the insulation thickness is 'none' this isn't alone conclusive for us to determine if it's + # a sloped ceiling + needs_recommendation = needs_recommendation_condition1 | needs_recommendation_condition2 # The property is pitched, not a loft, not assumed and has a sloping ceiling rec - if (is_pitched and not is_loft and not is_assumed and has_sloping_ceiling_recommendation and - primary_roof_is_sloped): + if has_suitable_features and needs_recommendation: return True return False @@ -157,17 +175,18 @@ class RoofRecommendations: is_at_rafters: bool, rir_over_loft: bool, is_assumed: bool, + insulation_thickness: str, has_loft_insulation_recommendation: bool, has_sloping_ceiling_recommendation: bool ) -> bool: """ Determine if loft insulation is appropriate - :param non_invasive_recommendations: List - list of non-invasive recommendations :param measures: List - list of measures :param is_pitched: Boolean - indicates whether or not the roof is pitched :param is_at_rafters: Boolean - indicates whether or not the loft insulation is at rafters :param rir_over_loft: Boolean - indicates whether or not there we should be doing RIR insulation :param is_assumed: Boolean - indicates whether or not the roof insulation status is assumed + :param insulation_thickness: String - insulation thickness of the roof :param has_loft_insulation_recommendation: Boolean - indicates whether or not there is a loft insulation non-invasive recommendation :param has_sloping_ceiling_recommendation: Boolean - indicates whether or not there @@ -183,6 +202,15 @@ class RoofRecommendations: if is_pitched and not is_assumed and has_sloping_ceiling_recommendation: return False + # We check the insulation thickness. If it's one of the "average", "below average", "none" values, + + if ( + is_assumed and is_pitched and insulation_thickness in ["average", "below average", "above average"] + and not has_sloping_ceiling_recommendation and not has_loft_insulation_recommendation + ): + # This is a pitched roof, without access to the loft, with unknown insulation status + return True + return has_loft_insulation_recommendation or ( is_pitched and has_li_in_measures and not is_at_rafters ) and not rir_over_loft @@ -283,6 +311,146 @@ class RoofRecommendations: return False + @staticmethod + def _deduce_primary_roof(component_needs: dict) -> str: + """ + Helper function for deducing the primary roof type used by _handle_multi_roof_types + """ + + # Can a non-primary part satisfy loft insulation? + primary_needs_loft = component_needs[1]["needs_loft_insulation"] + secondary_needs_loft = any( + p['needs_loft_insulation'] for idx, p in component_needs.items() if idx != 1 + ) + + if primary_needs_loft and not secondary_needs_loft: + # Only option is loft + return "loft" + + primary_needs_sloping = component_needs[1]["needs_sloping_ceiling"] + secondary_needs_sloping = any( + p['needs_sloping_ceiling'] for idx, p in component_needs.items() if idx != 1 + ) + + if primary_needs_sloping and not secondary_needs_sloping: + # Only option is sloping ceiling + return "sloping_ceiling" + + return "loft_insulation" # Defer to the cheaper option + + def _handle_multi_roof_types( + self, + measures: List, + find_my_epc_components: List[Mapping[str, Any]], + non_invasive_recommendations: List[Mapping[str, Any]], + has_sloping_ceiling_recommendation: bool, + has_loft_insulation_recommendation: bool, + rir_over_loft: bool + ) -> tuple[bool, bool]: + """ + This is a rough function to handle some edge cases, where we have two roof descriptions where + both look like they could be sloping ceilings or lofts. In this case, we need to deduce + which roof is the primary roof, and therefore whether or not we should recommend sloping ceiling insulation + :param measures: List - list of measures + :param find_my_epc_components: List - list of components from find my epc + :param non_invasive_recommendations: List - list of non-invasive recommendations + :param has_sloping_ceiling_recommendation: Boolean - indicates whether or not there is a sloping ceiling + recommendation + :param has_loft_insulation_recommendation: Boolean - indicates whether or not there is a loft insulation + recommendation + :param rir_over_loft: Boolean - indicates whether or not there we should be doing RIR insulation + :return: tuple[bool, bool] - (needs_sloping_ceiling, needs_loft_insulation) + """ + + # We utilise the find my EPC data to solve cases where the primary roof and secondary roof + # being loft and sloped ceiling is ambiguous + # We need to: + # 1) Check if we have two roof types + # 2) check if both could be considered sloped + # 3) Check if we have two non-invasive recommendations for both roof types + # 4) Determine which roof is the primary roof + + # We check a specific condition - which will imply loft insulation isn't appropriate but room in roof + # insulation is + # 1) We have an uninsulated loft (assumed) + # 2) We have a non-intrusive recommendation for room in roof insulation + + # We only use this when we have sloping ceiling and loft insulation recommendations + # Components are indexed from 0 + roof_count = max( + x["appearance_index"] for x in find_my_epc_components if x["component_name"] == "Roof" + ) + 1 + + roof_non_invasive_recommendations = [ + x["type"] for x in non_invasive_recommendations if x['type'] in ROOF_INSULATION_MEASURES + ] + + has_both_recommendations = ( + "loft_insulation" in roof_non_invasive_recommendations and \ + "sloping_ceiling_insulation" in roof_non_invasive_recommendations + ) + + if (roof_count <= 1) or not has_both_recommendations: + if roof_count > 1: + if "loft_insulation" in roof_non_invasive_recommendations: + return False, True + + if "sloping_ceiling_insulation" in roof_non_invasive_recommendations: + return True, False + + return True, False # Indicates that the property needs sloping ceiling as we only run this in that case + + extracted_roof_descriptions = { + idx: { + "description": component["description"], + **RoofAttributes(component["description"]).process() + } for idx, component in enumerate(find_my_epc_components) if component["component_name"] == "Roof" + } + + component_needs = {} + for component_idx, mapped in extracted_roof_descriptions.items(): + is_pitched = mapped["is_pitched"] + is_loft = mapped["is_loft"] + is_assumed = mapped["is_assumed"] + insulation_thickness = mapped["insulation_thickness"] + is_at_rafters = mapped["is_at_rafters"] + + needs_sloping_ceiling = self.is_sloping_ceiling_appropriate( + is_pitched=is_pitched, + is_loft=is_loft, + is_assumed=is_assumed, + has_sloping_ceiling_recommendation=has_sloping_ceiling_recommendation, + primary_roof_is_sloped=True, + insulation_thickness=insulation_thickness + ) + # If the roof has some form of insulation already but isn't a loft, it's + # not a loft. E.g. "pitched, limited insulation" is for sloping ceiling, not loft + needs_loft_insulation = self.is_loft_insulation_appropriate( + measures=measures, + is_pitched=is_pitched, + is_at_rafters=is_at_rafters, + rir_over_loft=rir_over_loft, + insulation_thickness=insulation_thickness, + has_loft_insulation_recommendation=has_loft_insulation_recommendation, + is_assumed=is_assumed, + has_sloping_ceiling_recommendation=False + ) + + component_needs[component_idx] = { + "needs_sloping_ceiling": needs_sloping_ceiling, + "needs_loft_insulation": needs_loft_insulation + } + + # Given the results we determine if the primary roof is sloped. The situation we may be in is + # one where the only otion is to assign one of the primary or secondary roof as a loft or sloped ceiling + # forcing our hand on whether the primary roof is sloped + primary_roof_type = self._deduce_primary_roof(component_needs) + + if primary_roof_type in ["ambiguous", "sloping_ceiling"]: + return True, False # Set sloping ceiling to true, loft to false + + return False, True # Set sloping ceiling to false, loft to true + def recommend(self, phase: int, measures: List | None = None, default_u_values: bool = False): """ Main method to recommend roof insulation measures @@ -322,17 +490,13 @@ class RoofRecommendations: non_invasive_recommendations = self.property.non_invasive_recommendations - # We check a specific condition - which will imply loft insulation isn't appropriate but room in roof - # insulation is - # 1) We have an uninsulated loft (assumed) - # 2) We have a non-intrusive recommendation for room in roof insulation - is_pitched = self.property.roof["is_pitched"] is_loft = self.property.roof["is_loft"] is_assumed = self.property.roof["is_assumed"] is_at_rafters = self.property.roof["is_at_rafters"] is_flat = self.property.roof["is_flat"] is_room_roof = self.property.roof["is_roof_room"] + insulation_thickness = self.property.roof["insulation_thickness"] has_sloping_ceiling_recommendation = any( x["type"] == "sloping_ceiling_insulation" for x in non_invasive_recommendations @@ -343,24 +507,32 @@ class RoofRecommendations: primary_roof_is_sloped = self._is_primary_roof_sloped( is_pitched=is_pitched, is_loft=is_loft, is_assumed=is_assumed ) - rir_over_loft = ( is_pitched and self.property.roof["insulation_thickness"] == "none" and "room_in_roof_insulation" in [x["type"] for x in non_invasive_recommendations] ) needs_sloping_ceiling = self.is_sloping_ceiling_appropriate( - is_pitched=is_pitched, is_loft=is_loft, is_assumed=is_assumed, + is_pitched=is_pitched, + is_loft=is_loft, + is_assumed=is_assumed, has_sloping_ceiling_recommendation=has_sloping_ceiling_recommendation, - primary_roof_is_sloped=primary_roof_is_sloped + primary_roof_is_sloped=primary_roof_is_sloped, + insulation_thickness=insulation_thickness ) needs_loft_insulation = self.is_loft_insulation_appropriate( - measures=measures, is_pitched=is_pitched, is_at_rafters=is_at_rafters, - rir_over_loft=rir_over_loft, has_loft_insulation_recommendation=has_loft_insulation_recommendation, - is_assumed=is_assumed, has_sloping_ceiling_recommendation=has_sloping_ceiling_recommendation + measures=measures, + is_pitched=is_pitched, + is_at_rafters=is_at_rafters, + rir_over_loft=rir_over_loft, + insulation_thickness=insulation_thickness, + has_loft_insulation_recommendation=has_loft_insulation_recommendation, + is_assumed=is_assumed, + has_sloping_ceiling_recommendation=has_sloping_ceiling_recommendation ) needs_flat_roof_insulation = self.is_flat_roof_insulation_appropriate( - is_flat=is_flat, measures=measures, + is_flat=is_flat, + measures=measures, has_flat_roof_recommendation=has_flat_roof_recommendation, primary_roof_is_sloped=primary_roof_is_sloped ) @@ -371,6 +543,17 @@ class RoofRecommendations: has_room_roof_recommendation=has_room_roof_recommendation ) + # We handle possible multi roof types + if needs_sloping_ceiling: + needs_sloping_ceiling, needs_loft_insulation = self._handle_multi_roof_types( + measures=measures, + find_my_epc_components=self.property.find_my_epc_components, + non_invasive_recommendations=non_invasive_recommendations, + has_sloping_ceiling_recommendation=has_sloping_ceiling_recommendation, + has_loft_insulation_recommendation=has_loft_insulation_recommendation, + rir_over_loft=rir_over_loft + ) + ################################################################ # ~~~~~ Loft Insulation Recommendation Logic ~~~~~ ################################################################ diff --git a/recommendations/tests/test_roof_recommendations.py b/recommendations/tests/test_roof_recommendations.py index e797892d..48e96af7 100644 --- a/recommendations/tests/test_roof_recommendations.py +++ b/recommendations/tests/test_roof_recommendations.py @@ -1,6 +1,7 @@ import pytest from unittest.mock import Mock from backend.Property import Property +from etl.customers.immo.pilot.asset_list import non_invasive_recommendations from etl.epc.Record import EPCRecord from recommendations.RoofRecommendations import RoofRecommendations from recommendations.tests.test_data.materials import materials @@ -407,7 +408,7 @@ class TestRoofRecommendations: # ~~~~~~~~~~~~ Sloping Ceiling Insulation ~~~~~~~~~~~~ @pytest.mark.parametrize( - "roof, has_sloping_ceiling_recommendation, primary_roof_is_sloped, expected_result", + "roof, has_sloping_ceiling_recommendation, primary_roof_is_sloped, insulation_thickness, expected_result", [ ( { @@ -427,6 +428,7 @@ class TestRoofRecommendations: }, True, True, + "none", True, ), ( @@ -439,19 +441,22 @@ class TestRoofRecommendations: }, False, False, + "average", False ) ] ) def test_is_sloping_ceiling_appropriate( - self, roof, has_sloping_ceiling_recommendation, primary_roof_is_sloped, expected_result + self, roof, has_sloping_ceiling_recommendation, primary_roof_is_sloped, + insulation_thickness, expected_result ): assert RoofRecommendations.is_sloping_ceiling_appropriate( is_pitched=roof["is_pitched"], is_loft=roof["is_loft"], is_assumed=roof["is_assumed"], has_sloping_ceiling_recommendation=has_sloping_ceiling_recommendation, - primary_roof_is_sloped=primary_roof_is_sloped + primary_roof_is_sloped=primary_roof_is_sloped, + insulation_thickness=insulation_thickness ) == expected_result def test_sloping_ceiling_pitched_no_insulation(self): @@ -465,19 +470,42 @@ class TestRoofRecommendations: 'insulation_thickness': 'none' }, roof_area=64.085, - data={"county": None, "local-authority-label": "Manchester"} + data={"county": None, "local-authority-label": "Manchester"}, + age_band="D", + already_installed=[], + non_invasive_recommendations=[ + {'type': 'flat_roof_insulation', 'sap_points': 9, 'survey': True}, + {'type': 'sloping_ceiling_insulation', 'sap_points': 9, 'survey': True}, + {'type': 'cavity_wall_insulation', 'sap_points': 6, 'survey': True}, + {'type': 'suspended_floor_insulation', 'sap_points': 2, 'survey': True}, + {'type': 'roomstat_programmer_trvs', 'sap_points': 3, 'survey': True}, + {'type': 'time_temperature_zone_control', 'sap_points': 3, 'survey': True}, + {'type': 'solar_pv', 'sap_points': 5, 'survey': True, 'suitable': True} + ], + find_my_epc_components=[ + {'component_name': 'Wall', 'description': 'Solid brick, as built, no insulation (assumed)', + 'efficiency': 'Very poor', 'appearance_index': 0}, + {'component_name': 'Roof', 'description': 'Pitched, no insulation', 'efficiency': 'Very poor', + 'appearance_index': 0}, + {'component_name': 'Roof', 'description': 'Pitched, limited insulation', 'efficiency': 'Very poor', + 'appearance_index': 1}, + {'component_name': 'Window', 'description': 'Some multiple glazing', 'efficiency': 'Very poor', + 'appearance_index': 0}, + {'component_name': 'Main heating', 'description': 'Boiler and radiators, mains gas', + 'efficiency': 'Good', 'appearance_index': 0}, + {'component_name': 'Main heating control', 'description': 'Programmer, room thermostat and TRVs', + 'efficiency': 'Good', 'appearance_index': 0}, + {'component_name': 'Hot water', 'description': 'From main system', 'efficiency': 'Good', + 'appearance_index': 0}, + {'component_name': 'Lighting', 'description': 'Low energy lighting in 28% of fixed outlets', + 'efficiency': 'Average', 'appearance_index': 0}, + {'component_name': 'Floor', 'description': 'Solid, no insulation (assumed)', 'efficiency': 'N/A', + 'appearance_index': 0}, + {'component_name': 'Secondary heating', 'description': 'None', 'efficiency': 'N/A', + 'appearance_index': 0} + ] + ) - property_instance.age_band = "D" - property_instance.already_installed = [] - property_instance.non_invasive_recommendations = [ - {'type': 'flat_roof_insulation', 'sap_points': 9, 'survey': True}, - {'type': 'sloping_ceiling_insulation', 'sap_points': 9, 'survey': True}, - {'type': 'cavity_wall_insulation', 'sap_points': 6, 'survey': True}, - {'type': 'suspended_floor_insulation', 'sap_points': 2, 'survey': True}, - {'type': 'roomstat_programmer_trvs', 'sap_points': 3, 'survey': True}, - {'type': 'time_temperature_zone_control', 'sap_points': 3, 'survey': True}, - {'type': 'solar_pv', 'sap_points': 5, 'survey': True, 'suitable': True} - ] roof_recommender = RoofRecommendations(property_instance=property_instance, materials=[]) assert not roof_recommender.recommendations @@ -500,3 +528,186 @@ class TestRoofRecommendations: assert roof_recommender.recommendations[0]["description_simulation"] == { 'roof-description': 'Pitched, insulated', 'roof-energy-eff': 'Average' } + + def test_ambiguous_sloping_ceiling_or_loft(self): + # In this case, we actually expect loft insulation to be recommended + property_instance = Mock( + id=0, + roof={ + # Roof looks like it could be a sloping ceiling but it's actually a loft + 'original_description': 'Pitched, no insulation', 'clean_description': 'Pitched, no insulation', + 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': True, + 'is_roof_room': False, 'is_loft': False, 'is_flat': False, 'is_thatched': False, + 'is_at_rafters': False, 'is_assumed': False, 'has_dwelling_above': False, 'is_valid': True, + 'insulation_thickness': 'none' + }, + roof_area=197.748, + data={"county": None, "local-authority-label": "Manchester"}, + already_installed=[], + find_my_epc_components=[ + {'component_name': 'Wall', 'description': 'Solid brick, as built, no insulation (assumed)', + 'efficiency': 'Very poor', 'appearance_index': 0}, + {'component_name': 'Roof', 'description': 'Pitched, no insulation', 'efficiency': 'Very poor', + 'appearance_index': 0}, + {'component_name': 'Roof', 'description': 'Pitched, limited insulation', 'efficiency': 'Very poor', + 'appearance_index': 1}, + {'component_name': 'Window', 'description': 'Some multiple glazing', 'efficiency': 'Very poor', + 'appearance_index': 0}, + {'component_name': 'Main heating', 'description': 'Boiler and radiators, mains gas', + 'efficiency': 'Good', 'appearance_index': 0}, + {'component_name': 'Main heating control', 'description': 'Programmer, room thermostat and TRVs', + 'efficiency': 'Good', 'appearance_index': 0}, + {'component_name': 'Hot water', 'description': 'From main system', 'efficiency': 'Good', + 'appearance_index': 0}, + {'component_name': 'Lighting', 'description': 'Low energy lighting in 28% of fixed outlets', + 'efficiency': 'Average', 'appearance_index': 0}, + {'component_name': 'Floor', 'description': 'Solid, no insulation (assumed)', 'efficiency': 'N/A', + 'appearance_index': 0}, + {'component_name': 'Secondary heating', 'description': 'None', 'efficiency': 'N/A', + 'appearance_index': 0} + ], + age_band="B", + non_invasive_recommendations=[ + {'type': 'loft_insulation', 'sap_points': 3, 'survey': True}, + {'type': 'flat_roof_insulation', 'sap_points': 2, 'survey': True}, + {'type': 'sloping_ceiling_insulation', 'sap_points': 2, 'survey': True}, + {'type': 'internal_wall_insulation', 'sap_points': 9, 'survey': True}, + {'type': 'draught_proofing', 'sap_points': 1, 'survey': True}, + {'type': 'low_energy_lighting', 'sap_points': 1, 'survey': True}, + {'type': 'solar_water_heating', 'sap_points': 1, 'survey': True}, + {'type': 'double_glazing', 'sap_points': 3, 'survey': True}, + {'type': 'solar_pv', 'sap_points': 4, 'survey': True, 'suitable': True} + ], + insulation_floor_area=162 + ) + + roof_recommender = RoofRecommendations(property_instance=property_instance, materials=materials) + assert not roof_recommender.recommendations + + roof_recommender.recommend(phase=0) + assert len(roof_recommender.recommendations) == 3 + + # Should all be loft insulation recommendations + assert all( + rec["type"] == "loft_insulation" for rec in roof_recommender.recommendations + ) + + def test_no_access_pitched_roof_assumed(self): + """ + In this case, the roof will have been surveyed as pitched, but the surveyor won't + have gotten access to the property to check the insulation. Therefore, we + recommend loft insulation. We assume that the roof is a locked off loft + :return: + """ + + property_instance = Mock( + id=0, + roof={ + 'original_description': 'Pitched, limited insulation (assumed)', + 'clean_description': 'Pitched, limited insulation', 'thermal_transmittance': None, + 'thermal_transmittance_unit': None, 'is_pitched': True, 'is_roof_room': False, 'is_loft': False, + 'is_flat': False, 'is_thatched': False, 'is_at_rafters': False, 'is_assumed': True, + 'has_dwelling_above': False, 'is_valid': True, 'insulation_thickness': 'below average' + }, + roof_area=73.24, + data={"county": None, "local-authority-label": "Manchester"}, + already_installed=[], + find_my_epc_components=[ + {'component_name': 'Wall', 'description': 'Solid brick, as built, no insulation (assumed)', + 'efficiency': 'Very poor', 'appearance_index': 0}, + {'component_name': 'Wall', 'description': 'System built, as built, no insulation (assumed)', + 'efficiency': 'Poor', 'appearance_index': 1}, + {'component_name': 'Wall', 'description': 'Cavity wall, filled cavity', 'efficiency': 'Average', + 'appearance_index': 2}, + {'component_name': 'Roof', 'description': 'Pitched, limited insulation (assumed)', + 'efficiency': 'Very poor', 'appearance_index': 0}, + {'component_name': 'Window', 'description': 'Fully double glazed', 'efficiency': 'Average', + 'appearance_index': 0}, + {'component_name': 'Main heating', 'description': 'Boiler and radiators, mains gas', + 'efficiency': 'Good', 'appearance_index': 0}, + {'component_name': 'Main heating control', 'description': 'Programmer and room thermostat', + 'efficiency': 'Average', 'appearance_index': 0}, + {'component_name': 'Hot water', 'description': 'From main system', 'efficiency': 'Good', + 'appearance_index': 0}, + {'component_name': 'Lighting', 'description': 'Low energy lighting in 75% of fixed outlets', + 'efficiency': 'Very good', 'appearance_index': 0}, + {'component_name': 'Roof', 'description': '(another dwelling above)', 'efficiency': 'N/A', + 'appearance_index': 1}, + {'component_name': 'Floor', 'description': 'Suspended, no insulation (assumed)', 'efficiency': 'N/A', + 'appearance_index': 0}, + {'component_name': 'Floor', 'description': 'Solid, no insulation (assumed)', 'efficiency': 'N/A', + 'appearance_index': 1}, + {'component_name': 'Secondary heating', 'description': 'None', 'efficiency': 'N/A', + 'appearance_index': 0} + ], + age_band="B", + non_invasive_recommendations=[ + {'type': 'internal_wall_insulation', 'sap_points': 2, 'survey': True}, + {'type': 'suspended_floor_insulation', 'sap_points': 2, 'survey': True}, + {'type': 'solid_floor_insulation', 'sap_points': 1, 'survey': True}, + {'type': 'low_energy_lighting', 'sap_points': 0, 'survey': True} + ], + insulation_floor_area=60 + ) + + roof_recommender = RoofRecommendations(property_instance=property_instance, materials=materials) + assert not roof_recommender.recommendations + + roof_recommender.recommend(phase=0) + assert len(roof_recommender.recommendations) == 3 + + # Should all be loft insulation recommendations + assert all( + rec["type"] == "loft_insulation" for rec in roof_recommender.recommendations + ) + + def test_traditional_loft_insulation(self): + property_instance = Mock( + id=0, + roof={ + 'original_description': 'Pitched, no insulation', 'clean_description': 'Pitched, no insulation', + 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': True, + 'is_roof_room': False, 'is_loft': False, 'is_flat': False, 'is_thatched': False, + 'is_at_rafters': False, 'is_assumed': False, 'has_dwelling_above': False, 'is_valid': True, + 'insulation_thickness': 'none' + }, + roof_area=48.82666666666667, + data={"county": None, "local-authority-label": "Manchester"}, + already_installed=[], + find_my_epc_components=[ + {'component_name': 'Wall', 'description': 'Cavity wall, filled cavity', 'efficiency': 'Good', + 'appearance_index': 0}, + {'component_name': 'Roof', 'description': 'Pitched, no insulation', 'efficiency': 'Very poor', + 'appearance_index': 0}, + {'component_name': 'Window', 'description': 'Fully double glazed', 'efficiency': 'Good', + 'appearance_index': 0}, + {'component_name': 'Main heating', 'description': 'Boiler and radiators, mains gas', + 'efficiency': 'Good', 'appearance_index': 0}, + {'component_name': 'Main heating control', 'description': 'TRVs and bypass', 'efficiency': 'Average', + 'appearance_index': 0}, + {'component_name': 'Hot water', 'description': 'From main system', 'efficiency': 'Good', + 'appearance_index': 0}, + {'component_name': 'Lighting', 'description': 'Low energy lighting in all fixed outlets', + 'efficiency': 'Very good', 'appearance_index': 0}, + {'component_name': 'Floor', 'description': 'Solid, no insulation (assumed)', 'efficiency': 'N/A', + 'appearance_index': 0}, + {'component_name': 'Secondary heating', 'description': 'Room heaters, electric', 'efficiency': 'N/A', + 'appearance_index': 0} + ], + age_band="F", + non_invasive_recommendations=[ + {'type': 'loft_insulation', 'sap_points': 9, 'survey': True}, + {'type': 'solid_floor_insulation', 'sap_points': 2, 'survey': True}, + {'type': 'solar_water_heating', 'sap_points': 1, 'survey': True}, + {'type': 'solar_pv', 'sap_points': 11, 'survey': True, 'suitable': True} + ], + insulation_floor_area=40.0 + ) + + roof_recommender = RoofRecommendations(property_instance=property_instance, materials=materials) + assert not roof_recommender.recommendations + + roof_recommender.recommend(0) + assert len(roof_recommender.recommendations) == 3 + # should all be loft insulation recommendations + assert all(rec["type"] == "loft_insulation" for rec in roof_recommender.recommendations) From 81fc264afec0f1b128262a8590e46da3e3988b4b Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 28 Jan 2026 09:32:32 +0000 Subject: [PATCH 60/68] handling ambiguous cases for sloping ceiling vs loft insulation --- recommendations/RoofRecommendations.py | 15 ++++- .../tests/test_roof_recommendations.py | 61 ++++++++++++++++++- 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/recommendations/RoofRecommendations.py b/recommendations/RoofRecommendations.py index bcaea687..578173bb 100644 --- a/recommendations/RoofRecommendations.py +++ b/recommendations/RoofRecommendations.py @@ -122,12 +122,16 @@ class RoofRecommendations: @staticmethod def is_sloping_ceiling_appropriate( - is_pitched: bool, is_loft: bool, is_assumed: bool, has_sloping_ceiling_recommendation: bool, - primary_roof_is_sloped: bool, insulation_thickness: str + is_pitched: bool, is_loft: bool, is_assumed: bool, + is_flat: bool, + has_sloping_ceiling_recommendation: bool, + primary_roof_is_sloped: bool, insulation_thickness: str, + has_loft_insulation_recommendation: bool ) -> bool: """ :param is_pitched: Boolean - indicates whether or not the roof is pitched + :param is_flat: Boolean - indicates whether or not the roof is flat :param is_loft: Boolean - indicates whether or not the roof is described as a loft :param is_assumed: Boolean - indiates if the assessment of the roof is assumed or actually confirmed :param has_sloping_ceiling_recommendation: Boolean - indicates if the property has a sloping ceiling @@ -135,6 +139,7 @@ class RoofRecommendations: :param primary_roof_is_sloped: Boolean - indicates if the primary room is described a sloped (as opposed to an extension) :param insulation_thickness: String - insulation thickness of the roof + :param has_loft_insulation_recommendation: Boolean - indicates whether or not there :return: """ # We need to check: @@ -166,6 +171,11 @@ class RoofRecommendations: if has_suitable_features and needs_recommendation: return True + # In this case, we have an assumed pitched roof with average or below average insulation + # but a sloping ceiling insulation without loft + if has_sloping_ceiling_recommendation and not has_loft_insulation_recommendation and not is_flat: + return True + return False @staticmethod @@ -504,6 +514,7 @@ class RoofRecommendations: has_loft_insulation_recommendation = any(x["type"] == "loft_insulation" for x in non_invasive_recommendations) has_flat_roof_recommendation = any(x["type"] == "flat_roof_insulation" for x in non_invasive_recommendations) has_room_roof_recommendation = any(x["type"] == "room_roof_insulation" for x in non_invasive_recommendations) + # Very naive condition primary_roof_is_sloped = self._is_primary_roof_sloped( is_pitched=is_pitched, is_loft=is_loft, is_assumed=is_assumed ) diff --git a/recommendations/tests/test_roof_recommendations.py b/recommendations/tests/test_roof_recommendations.py index 48e96af7..f022f9b7 100644 --- a/recommendations/tests/test_roof_recommendations.py +++ b/recommendations/tests/test_roof_recommendations.py @@ -1,7 +1,6 @@ import pytest from unittest.mock import Mock from backend.Property import Property -from etl.customers.immo.pilot.asset_list import non_invasive_recommendations from etl.epc.Record import EPCRecord from recommendations.RoofRecommendations import RoofRecommendations from recommendations.tests.test_data.materials import materials @@ -711,3 +710,63 @@ class TestRoofRecommendations: assert len(roof_recommender.recommendations) == 3 # should all be loft insulation recommendations assert all(rec["type"] == "loft_insulation" for rec in roof_recommender.recommendations) + + def sloping_ceiling_limited_insulation(self): + property_instance = Mock( + id=0, + roof={ + "original_description": 'Pitched, limited insulation (assumed)', + 'clean_description': 'Pitched, limited insulation', + 'thermal_transmittance': None, 'thermal_transmittance_unit': None, 'is_pitched': True, + 'is_roof_room': False, 'is_loft': False, 'is_flat': False, 'is_thatched': False, 'is_at_rafters': False, + 'is_assumed': True, 'has_dwelling_above': False, 'is_valid': True, + 'insulation_thickness': 'below average' + }, + roof_area=35, + data={"county": None, "local-authority-label": "Manchester"}, + already_installed=[], + find_my_epc_components=[ + {'component_name': 'Wall', 'description': 'Cavity wall, as built, no insulation (assumed)', + 'efficiency': 'poor', 'appearance_index': 0}, + {'component_name': 'Roof', 'description': 'Pitched, limited insulation (assumed)', + 'efficiency': 'Very poor', 'appearance_index': 0}, + {'component_name': 'Window', 'description': 'Fully double glazed', 'efficiency': 'Average', + 'appearance_index': 0}, + {'component_name': 'Main heating', 'description': 'Boiler and radiators, mains gas', + 'efficiency': 'Good', 'appearance_index': 0}, + {'component_name': 'Main heating control', 'description': 'TRVs and bypass', + 'efficiency': 'Average', 'appearance_index': 0}, + {'component_name': 'Hot water', 'description': 'From main system', 'efficiency': 'Good', + 'appearance_index': 0}, + {'component_name': 'Lighting', 'description': 'Low energy lighting in all fixed outlets', + 'efficiency': 'Very good', 'appearance_index': 0}, + {'component_name': 'Floor', 'description': '(another dwelling below)', 'efficiency': 'N/A', + 'appearance_index': 0}, + {'component_name': 'Secondary heating', 'description': 'None', 'efficiency': 'N/A', + 'appearance_index': 0} + ], + age_band="B", + non_invasive_recommendations=[ + {'type': 'sloping_ceiling_insulation', 'sap_points': 2, 'survey': True}, + {'type': 'flat_roof_insulation', 'sap_points': 2, 'survey': True}, + ], + ) + + # We expect a sloping ceiling insulation recommendation + roof_recommender = RoofRecommendations(property_instance=property_instance, materials=materials) + assert not roof_recommender.recommendations + + roof_recommender.recommend(phase=0) + assert len(roof_recommender.recommendations) == 1 + assert roof_recommender.recommendations[0]["type"] == "sloping_ceiling_insulation" + assert roof_recommender.recommendations[0]["measure_type"] == "sloping_ceiling_insulation" + assert roof_recommender.recommendations[0]["description"] == \ + "Insulate sloping ceilings at the rafters and re-decorate" + assert roof_recommender.recommendations[0]["simulation_config"] == { + 'roof_insulation_thickness_ending': 'average', + 'roof_thermal_transmittance_ending': 0.5, + 'roof_energy_eff_ending': 'Average' + } + assert roof_recommender.recommendations[0]["description_simulation"] == { + 'roof-description': 'Pitched, insulated', 'roof-energy-eff': 'Average' + } From 5f463efe7d73f87805ec4642afe4cb0d6f25538a Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 28 Jan 2026 09:36:16 +0000 Subject: [PATCH 61/68] fixed last sloping ceiling recommendation with limited insulation --- recommendations/RoofRecommendations.py | 9 +++++++-- recommendations/tests/test_roof_recommendations.py | 11 ++++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/recommendations/RoofRecommendations.py b/recommendations/RoofRecommendations.py index 578173bb..08fceb32 100644 --- a/recommendations/RoofRecommendations.py +++ b/recommendations/RoofRecommendations.py @@ -424,14 +424,17 @@ class RoofRecommendations: is_assumed = mapped["is_assumed"] insulation_thickness = mapped["insulation_thickness"] is_at_rafters = mapped["is_at_rafters"] + is_flat = mapped["is_flat"] needs_sloping_ceiling = self.is_sloping_ceiling_appropriate( + is_flat=is_flat, is_pitched=is_pitched, is_loft=is_loft, is_assumed=is_assumed, has_sloping_ceiling_recommendation=has_sloping_ceiling_recommendation, primary_roof_is_sloped=True, - insulation_thickness=insulation_thickness + insulation_thickness=insulation_thickness, + has_loft_insulation_recommendation=has_loft_insulation_recommendation ) # If the roof has some form of insulation already but isn't a loft, it's # not a loft. E.g. "pitched, limited insulation" is for sloping ceiling, not loft @@ -525,11 +528,13 @@ class RoofRecommendations: needs_sloping_ceiling = self.is_sloping_ceiling_appropriate( is_pitched=is_pitched, + is_flat=is_flat, is_loft=is_loft, is_assumed=is_assumed, has_sloping_ceiling_recommendation=has_sloping_ceiling_recommendation, primary_roof_is_sloped=primary_roof_is_sloped, - insulation_thickness=insulation_thickness + insulation_thickness=insulation_thickness, + has_loft_insulation_recommendation=has_loft_insulation_recommendation ) needs_loft_insulation = self.is_loft_insulation_appropriate( measures=measures, diff --git a/recommendations/tests/test_roof_recommendations.py b/recommendations/tests/test_roof_recommendations.py index f022f9b7..9f39779c 100644 --- a/recommendations/tests/test_roof_recommendations.py +++ b/recommendations/tests/test_roof_recommendations.py @@ -407,7 +407,8 @@ class TestRoofRecommendations: # ~~~~~~~~~~~~ Sloping Ceiling Insulation ~~~~~~~~~~~~ @pytest.mark.parametrize( - "roof, has_sloping_ceiling_recommendation, primary_roof_is_sloped, insulation_thickness, expected_result", + "roof, has_sloping_ceiling_recommendation, primary_roof_is_sloped, insulation_thickness, " + "has_loft_insulation_recommendation, expected_result", [ ( { @@ -428,6 +429,7 @@ class TestRoofRecommendations: True, True, "none", + False, True, ), ( @@ -441,21 +443,24 @@ class TestRoofRecommendations: False, False, "average", + False, False ) ] ) def test_is_sloping_ceiling_appropriate( self, roof, has_sloping_ceiling_recommendation, primary_roof_is_sloped, - insulation_thickness, expected_result + insulation_thickness, has_loft_insulation_recommendation, expected_result ): assert RoofRecommendations.is_sloping_ceiling_appropriate( + is_flat=roof["is_flat"], is_pitched=roof["is_pitched"], is_loft=roof["is_loft"], is_assumed=roof["is_assumed"], has_sloping_ceiling_recommendation=has_sloping_ceiling_recommendation, primary_roof_is_sloped=primary_roof_is_sloped, - insulation_thickness=insulation_thickness + insulation_thickness=insulation_thickness, + has_loft_insulation_recommendation=has_loft_insulation_recommendation ) == expected_result def test_sloping_ceiling_pitched_no_insulation(self): From 751032e6669857ce75e6af9621a7949462a4c17d Mon Sep 17 00:00:00 2001 From: Daniel Roth Date: Wed, 28 Jan 2026 09:54:33 +0000 Subject: [PATCH 62/68] fixes so it actually runs --- .../domain/mapping/lbwf/lbwf_mapper.py | 7 +++++ backend/condition/parsing/lbwf_parser.py | 31 +++++++++++-------- backend/condition/processor.py | 2 +- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/backend/condition/domain/mapping/lbwf/lbwf_mapper.py b/backend/condition/domain/mapping/lbwf/lbwf_mapper.py index 09109ef9..60c8b1ac 100644 --- a/backend/condition/domain/mapping/lbwf/lbwf_mapper.py +++ b/backend/condition/domain/mapping/lbwf/lbwf_mapper.py @@ -29,8 +29,15 @@ class LbwfMapper(Mapper): elements_by_key: dict[tuple[ElementType, int], Element] = {} for raw_asset in client_property_data.assets: + if raw_asset.element_code in ["DECNTHMINC", "EICINSFREQ"]: + # skip metadata rows + continue + element_mapping = LbwfMapper._safe_map_element(raw_asset) + if not element_mapping: + continue + aspect_condition = LbwfMapper._build_aspect_condition( raw_asset, element_mapping, survey_year ) diff --git a/backend/condition/parsing/lbwf_parser.py b/backend/condition/parsing/lbwf_parser.py index 63512c41..14d2efe4 100644 --- a/backend/condition/parsing/lbwf_parser.py +++ b/backend/condition/parsing/lbwf_parser.py @@ -3,18 +3,23 @@ from openpyxl import Workbook, load_workbook from collections import defaultdict from backend.condition.parsing.parser import Parser -from backend.condition.parsing.records.lbwf.lbwf_asset_condition import LbwfAssetCondition +from backend.condition.parsing.records.lbwf.lbwf_asset_condition import ( + LbwfAssetCondition, +) from backend.condition.parsing.records.lbwf.lbwf_house import LbwfHouse from backend.condition.utils.date_utils import normalise_date from utils.logger import setup_logger logger = setup_logger() + class LbwfParser(Parser): def parse(self, file_stream: BinaryIO) -> Any: wb: Workbook = load_workbook(file_stream) - address_to_uprn_map: Dict[str, int] = LbwfParser._generate_address_to_uprn_dict(wb) + address_to_uprn_map: Dict[str, int] = LbwfParser._generate_address_to_uprn_dict( + wb + ) assets = self._parse_assets(wb) houses = self._parse_houses(wb, address_to_uprn_map) @@ -82,7 +87,6 @@ class LbwfParser(Parser): for house in houses: house.assets = assets_by_ref.get(house.reference, []) - @staticmethod def _map_row_to_house_record( row: Any | Tuple[object | None, ...], @@ -100,8 +104,8 @@ class LbwfParser(Parser): house=row[header_indexes["HOSUE"]], fail_decency=row[header_indexes["Fail Decency"]], assets=[], - ) - + ) + @staticmethod def _map_row_to_asset_record( row: Any | Tuple[object | None, ...], @@ -119,7 +123,9 @@ class LbwfParser(Parser): element_code=row[header_indexes["ELEMENT CODE"]], element_code_description=row[header_indexes["ELEMENT CODE DESCRIPTION"]], attribute_code=row[header_indexes["ATTRIBUTE CODE"]], - attribute_code_description=row[header_indexes["ATTRIBUTE CODE DESCRIPTION"]], + attribute_code_description=row[ + header_indexes["ATTRIBUTE CODE DESCRIPTION"] + ], element_date_value=row[header_indexes["ELEMENT DATE VALUE"]], element_numerical_value=row[header_indexes["ELEMENT NUMERIC VALUE"]], element_text_value=row[header_indexes["ELEMENT TEXT VALUE"]], @@ -128,7 +134,6 @@ class LbwfParser(Parser): remaining_life=row[header_indexes["REMAINING LIFE"]], element_comments=row[header_indexes["ELEMENT COMMENTS"]], ) - @staticmethod def _generate_address_to_uprn_dict(wb: Workbook) -> Dict[str, int | None]: @@ -158,10 +163,9 @@ class LbwfParser(Parser): return mapping - @staticmethod def _get_column_indexes_by_name( - headers: Tuple[object | None, ...] + headers: Tuple[object | None, ...], ) -> Dict[str, int]: index: Dict[str, int] = {} @@ -170,13 +174,14 @@ class LbwfParser(Parser): index[header] = i return index - + @staticmethod - def _get_uprn_from_address(address: str, address_to_uprn_map: Dict[str, int]) -> int | None: + def _get_uprn_from_address( + address: str, address_to_uprn_map: Dict[str, int] + ) -> int | None: pseudo_name = address.split(",")[0] if pseudo_name.lower() in (k.lower() for k in address_to_uprn_map.keys()): return address_to_uprn_map[pseudo_name.upper()] - + return None - diff --git a/backend/condition/processor.py b/backend/condition/processor.py index 3135d8a5..3cbff498 100644 --- a/backend/condition/processor.py +++ b/backend/condition/processor.py @@ -25,7 +25,7 @@ def process_file(file_stream: BinaryIO, source_key: str) -> None: property_condition_surveys: List[PropertyConditionSurvey] = [] for p in raw_properties: - property_condition_surveys.push( + property_condition_surveys.append( mapper.map_asset_conditions_for_property(p, survey_year) ) From c1782e6e310282e3c7c8a719f684901bb75465f1 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 28 Jan 2026 10:00:20 +0000 Subject: [PATCH 63/68] add specific return names in handling multi roof types --- recommendations/RoofRecommendations.py | 78 +++++++++++++------ .../tests/test_roof_recommendations.py | 6 +- 2 files changed, 59 insertions(+), 25 deletions(-) diff --git a/recommendations/RoofRecommendations.py b/recommendations/RoofRecommendations.py index 08fceb32..5eb81439 100644 --- a/recommendations/RoofRecommendations.py +++ b/recommendations/RoofRecommendations.py @@ -122,10 +122,13 @@ class RoofRecommendations: @staticmethod def is_sloping_ceiling_appropriate( - is_pitched: bool, is_loft: bool, is_assumed: bool, + is_pitched: bool, + is_loft: bool, + is_assumed: bool, is_flat: bool, has_sloping_ceiling_recommendation: bool, - primary_roof_is_sloped: bool, insulation_thickness: str, + primary_roof_looks_sloped: bool, + insulation_thickness: str, has_loft_insulation_recommendation: bool ) -> bool: """ @@ -136,7 +139,7 @@ class RoofRecommendations: :param is_assumed: Boolean - indiates if the assessment of the roof is assumed or actually confirmed :param has_sloping_ceiling_recommendation: Boolean - indicates if the property has a sloping ceiling recommendation - :param primary_roof_is_sloped: Boolean - indicates if the primary room is described a sloped (as opposed to + :param primary_roof_looks_sloped: Boolean - indicates if the primary room is described a sloped (as opposed to an extension) :param insulation_thickness: String - insulation thickness of the roof :param has_loft_insulation_recommendation: Boolean - indicates whether or not there @@ -151,7 +154,7 @@ class RoofRecommendations: # If we have a loft primary roof and sloping ceiling has_suitable_features = ( - is_pitched and not is_loft and not is_assumed and primary_roof_is_sloped + is_pitched and not is_loft and not is_assumed and primary_roof_looks_sloped ) # Check if it needs a recommendation @@ -227,7 +230,7 @@ class RoofRecommendations: @staticmethod def is_flat_roof_insulation_appropriate( - is_flat: bool, measures: List, has_flat_roof_recommendation: bool, primary_roof_is_sloped: bool + is_flat: bool, measures: List, has_flat_roof_recommendation: bool, primary_roof_looks_sloped: bool ) -> bool: """ Determine if flat roof insulation is appropriate @@ -235,17 +238,17 @@ class RoofRecommendations: :param measures: List - list of measures :param has_flat_roof_recommendation: Boolean - indicates whether or not there is a flat roof non-invasive recommendation - :param primary_roof_is_sloped: Boolean - indicates if the primary roof is flat + :param primary_roof_looks_sloped: Boolean - indicates if the primary roof looks like a sloped roof :return: Boolean - When checking if has_flat_roof_recommendation and primary_roof_is_sloped, we need to check both + When checking if has_flat_roof_recommendation and primary_roof_looks_sloped, we need to check both conditions. This is because within a default EPC recommendation, the EPC will pair these recommendations together. Therefore, weneed to ensure the primary roof isn't sloped """ flat_roof_in_measures = "flat_roof_insulation" in measures - return (is_flat and flat_roof_in_measures) or (has_flat_roof_recommendation and not primary_roof_is_sloped) + return (is_flat and flat_roof_in_measures) or (has_flat_roof_recommendation and not primary_roof_looks_sloped) @staticmethod def is_room_roof_insulation_appropriate( @@ -300,7 +303,7 @@ class RoofRecommendations: return True @staticmethod - def _is_primary_roof_sloped( + def _does_primary_roof_look_sloped( is_pitched: bool, is_loft: bool, is_assumed: bool ): """ @@ -387,6 +390,10 @@ class RoofRecommendations: # We only use this when we have sloping ceiling and loft insulation recommendations # Components are indexed from 0 + + needs_sloping = True + needs_loft = True + roof_count = max( x["appearance_index"] for x in find_my_epc_components if x["component_name"] == "Roof" ) + 1 @@ -403,12 +410,13 @@ class RoofRecommendations: if (roof_count <= 1) or not has_both_recommendations: if roof_count > 1: if "loft_insulation" in roof_non_invasive_recommendations: - return False, True + return not needs_sloping, needs_loft if "sloping_ceiling_insulation" in roof_non_invasive_recommendations: - return True, False + return needs_sloping, not needs_loft - return True, False # Indicates that the property needs sloping ceiling as we only run this in that case + return needs_sloping, not needs_loft # Indicates that the property needs sloping ceiling as we only run + # this in that case extracted_roof_descriptions = { idx: { @@ -432,7 +440,7 @@ class RoofRecommendations: is_loft=is_loft, is_assumed=is_assumed, has_sloping_ceiling_recommendation=has_sloping_ceiling_recommendation, - primary_roof_is_sloped=True, + primary_roof_looks_sloped=True, insulation_thickness=insulation_thickness, has_loft_insulation_recommendation=has_loft_insulation_recommendation ) @@ -460,9 +468,9 @@ class RoofRecommendations: primary_roof_type = self._deduce_primary_roof(component_needs) if primary_roof_type in ["ambiguous", "sloping_ceiling"]: - return True, False # Set sloping ceiling to true, loft to false + return needs_sloping, not needs_loft # Set sloping ceiling to true, loft to false - return False, True # Set sloping ceiling to false, loft to true + return not needs_sloping, needs_loft # Set sloping ceiling to false, loft to true def recommend(self, phase: int, measures: List | None = None, default_u_values: bool = False): """ @@ -479,6 +487,10 @@ class RoofRecommendations: property_needs_roof_recommendation = self._does_roof_need_recommendation(measures, u_value) if not property_needs_roof_recommendation: + # Roof is either: + # - already sufficiently insulated + # - unsuitable (dwelling above, thatched, etc.) + # - not matching available measures return u_value = get_roof_u_value( @@ -517,13 +529,14 @@ class RoofRecommendations: has_loft_insulation_recommendation = any(x["type"] == "loft_insulation" for x in non_invasive_recommendations) has_flat_roof_recommendation = any(x["type"] == "flat_roof_insulation" for x in non_invasive_recommendations) has_room_roof_recommendation = any(x["type"] == "room_roof_insulation" for x in non_invasive_recommendations) - # Very naive condition - primary_roof_is_sloped = self._is_primary_roof_sloped( + + primary_roof_looks_sloped = self._does_primary_roof_look_sloped( is_pitched=is_pitched, is_loft=is_loft, is_assumed=is_assumed ) rir_over_loft = ( - is_pitched and self.property.roof["insulation_thickness"] == "none" and - "room_in_roof_insulation" in [x["type"] for x in non_invasive_recommendations] + is_pitched and + self.property.roof["insulation_thickness"] == "none" and + has_room_roof_recommendation ) needs_sloping_ceiling = self.is_sloping_ceiling_appropriate( @@ -532,7 +545,7 @@ class RoofRecommendations: is_loft=is_loft, is_assumed=is_assumed, has_sloping_ceiling_recommendation=has_sloping_ceiling_recommendation, - primary_roof_is_sloped=primary_roof_is_sloped, + primary_roof_looks_sloped=primary_roof_looks_sloped, insulation_thickness=insulation_thickness, has_loft_insulation_recommendation=has_loft_insulation_recommendation ) @@ -550,7 +563,7 @@ class RoofRecommendations: is_flat=is_flat, measures=measures, has_flat_roof_recommendation=has_flat_roof_recommendation, - primary_roof_is_sloped=primary_roof_is_sloped + primary_roof_looks_sloped=primary_roof_looks_sloped ) needs_rir_insulation = self.is_room_roof_insulation_appropriate( is_room_roof=is_room_roof, @@ -561,6 +574,9 @@ class RoofRecommendations: # We handle possible multi roof types if needs_sloping_ceiling: + # Multi-roof override: + # In ambiguous cases (extensions, mixed descriptions), EPC component analysis + # may force us to choose between loft vs sloping ceiling. needs_sloping_ceiling, needs_loft_insulation = self._handle_multi_roof_types( measures=measures, find_my_epc_components=self.property.find_my_epc_components, @@ -569,6 +585,17 @@ class RoofRecommendations: has_loft_insulation_recommendation=has_loft_insulation_recommendation, rir_over_loft=rir_over_loft ) + # Explicit override + needs_flat_roof_insulation = False + needs_rir_insulation = False + if needs_sloping_ceiling and needs_loft_insulation: + raise RuntimeError( + "Multi-roof resolution produced conflicting outcomes: " + "both sloping ceiling and loft insulation required" + ) + + # Retrofit precedence (least → most invasive): + # Loft > Flat roof > Room in roof > Sloping ceiling ################################################################ # ~~~~~ Loft Insulation Recommendation Logic ~~~~~ @@ -616,7 +643,14 @@ class RoofRecommendations: ) return - raise NotImplementedError("Implement me") + raise RuntimeError( + "Roof recommendation undecidable. " + f"needs_loft={needs_loft_insulation}, " + f"needs_flat={needs_flat_roof_insulation}, " + f"needs_rir={needs_rir_insulation}, " + f"needs_sloping={needs_sloping_ceiling}, " + f"roof={self.property.roof}" + ) @staticmethod def make_roof_insulation_description(material): diff --git a/recommendations/tests/test_roof_recommendations.py b/recommendations/tests/test_roof_recommendations.py index 9f39779c..0879757f 100644 --- a/recommendations/tests/test_roof_recommendations.py +++ b/recommendations/tests/test_roof_recommendations.py @@ -407,7 +407,7 @@ class TestRoofRecommendations: # ~~~~~~~~~~~~ Sloping Ceiling Insulation ~~~~~~~~~~~~ @pytest.mark.parametrize( - "roof, has_sloping_ceiling_recommendation, primary_roof_is_sloped, insulation_thickness, " + "roof, has_sloping_ceiling_recommendation, primary_roof_looks_sloped, insulation_thickness, " "has_loft_insulation_recommendation, expected_result", [ ( @@ -449,7 +449,7 @@ class TestRoofRecommendations: ] ) def test_is_sloping_ceiling_appropriate( - self, roof, has_sloping_ceiling_recommendation, primary_roof_is_sloped, + self, roof, has_sloping_ceiling_recommendation, primary_roof_looks_sloped, insulation_thickness, has_loft_insulation_recommendation, expected_result ): assert RoofRecommendations.is_sloping_ceiling_appropriate( @@ -458,7 +458,7 @@ class TestRoofRecommendations: is_loft=roof["is_loft"], is_assumed=roof["is_assumed"], has_sloping_ceiling_recommendation=has_sloping_ceiling_recommendation, - primary_roof_is_sloped=primary_roof_is_sloped, + primary_roof_looks_sloped=primary_roof_looks_sloped, insulation_thickness=insulation_thickness, has_loft_insulation_recommendation=has_loft_insulation_recommendation ) == expected_result From fad9512fd70648b15b79aac08d7c6067eb3b9712 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 28 Jan 2026 12:08:31 +0000 Subject: [PATCH 64/68] removed self.property_components from find my epc --- etl/find_my_epc/RetrieveFindMyEpc.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/etl/find_my_epc/RetrieveFindMyEpc.py b/etl/find_my_epc/RetrieveFindMyEpc.py index 8bdc45c5..392e6aaa 100644 --- a/etl/find_my_epc/RetrieveFindMyEpc.py +++ b/etl/find_my_epc/RetrieveFindMyEpc.py @@ -38,7 +38,6 @@ class RetrieveFindMyEpc: self.address_cleaned = self.address.replace(",", "").replace(" ", "").lower() # Containers for the extracted components - self.property_components = [] self.walls = [] self.address_postal_town = address_postal_town @@ -259,10 +258,10 @@ class RetrieveFindMyEpc: property_features_table = soup.find("tbody", class_="govuk-table__body") property_features_table = property_features_table.find_all("tr") - self.extract_property_components(property_features_table) + property_components = self.extract_property_components(property_features_table) # Extract walls - self.walls = [x["description"] for x in self.property_components if x["component_name"] == "Wall"] + self.walls = [x["description"] for x in property_components if x["component_name"] == "Wall"] # Finally, we format the recommendations recommendations = self.format_recommendations(recommendations, assessment_data, sap_2012_date) @@ -612,7 +611,7 @@ class RetrieveFindMyEpc: property_components = self.extract_property_components(property_features_table) # Extract walls - self.walls = [x["description"] for x in self.property_components if x["component_name"] == "Wall"] + self.walls = [x["description"] for x in property_components if x["component_name"] == "Wall"] # Finally, we format the recommendations recommendations = self.format_recommendations(recommendations, assessment_data, sap_2012_date) From 491ef6c2e89ba8030cf0213c576c85b9c9e64bac Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 28 Jan 2026 12:10:18 +0000 Subject: [PATCH 65/68] added sloping ceiling insulation into create recommendation scoring data --- backend/Property.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/Property.py b/backend/Property.py index c320fdd8..09c1d8ed 100644 --- a/backend/Property.py +++ b/backend/Property.py @@ -576,7 +576,7 @@ class Property: "solid_floor_insulation", "suspended_floor_insulation", "windows_glazing", "solar_pv", "heating", "hot_water_tank_insulation", "heating_control", "secondary_heating", "cylinder_thermostat", "mixed_glazing", - "extension_cavity_wall_insulation", "mechanical_ventilation", + "extension_cavity_wall_insulation", "mechanical_ventilation", "sloping_ceiling_insulation" ]: raise NotImplementedError( "Implement me, given type %s" % recommendation["type"] From 89262ff3dd09a887602e9c0821a670e751355ad9 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 28 Jan 2026 12:35:25 +0000 Subject: [PATCH 66/68] added missing sloiping ceiling --- backend/Property.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/Property.py b/backend/Property.py index 09c1d8ed..14f7e03f 100644 --- a/backend/Property.py +++ b/backend/Property.py @@ -553,7 +553,7 @@ class Property: "internal_wall_insulation", "external_wall_insulation", "cavity_wall_insulation", "cylinder_thermostat", "loft_insulation", "room_roof_insulation", "flat_roof_insulation", "solid_floor_insulation", "suspended_floor_insulation", "mixed_glazing", - "windows_glazing", "mechanical_ventilation", "solar_pv" + "windows_glazing", "mechanical_ventilation", "solar_pv", "sloping_ceiling_insulation" ]: # We update the data, as defined in the recommendaton for prefix in ["walls", "roof", "floor"]: From 71a2a2357aa71e281064c3b8dd7d7ce857241a6b Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 28 Jan 2026 12:45:42 +0000 Subject: [PATCH 67/68] fixed bug not pulling out sloping ceiling rec --- recommendations/RoofRecommendations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/recommendations/RoofRecommendations.py b/recommendations/RoofRecommendations.py index 5eb81439..71e47ba6 100644 --- a/recommendations/RoofRecommendations.py +++ b/recommendations/RoofRecommendations.py @@ -1032,7 +1032,7 @@ class RoofRecommendations: """ sloping_ceiling_recommendation = next( - (x for x in non_invasive_recommendations if ["type"] == "sloping_ceiling_insulation"), {} + (x for x in non_invasive_recommendations if x["type"] == "sloping_ceiling_insulation"), {} ) new_description = "Pitched, insulated" From a8d772abfc0733024222a4e8b23f85349ce344c4 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 28 Jan 2026 18:44:03 +0000 Subject: [PATCH 68/68] minor handling for when a user switches off ventilation as an option --- backend/engine/engine.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/backend/engine/engine.py b/backend/engine/engine.py index 67353b7a..e833eb89 100644 --- a/backend/engine/engine.py +++ b/backend/engine/engine.py @@ -1051,11 +1051,14 @@ async def model_engine(body: PlanTriggerRequest): property_required_measures = [m for m in recommendations[p.id] if m[0]["type"] in body.required_measures] measures_to_optimise = [m for m in recommendations[p.id] if m[0]["type"] not in body.required_measures] + ventilation_included = "ventilation" in property_measure_types + # If a measure requiring ventilation is selected, and the property does not have ventilation, we enfore # its inclusion + needs_ventilation = any( x in property_measure_types for x in assumptions.measures_needing_ventilation - ) and not p.has_ventilation + ) and not p.has_ventilation and ventilation_included if not measures_to_optimise: # Nothing to do, we just reshape the recommendations