diff --git a/asset_list/AssetList.py b/asset_list/AssetList.py index e1df5342..5ae3029f 100644 --- a/asset_list/AssetList.py +++ b/asset_list/AssetList.py @@ -1351,7 +1351,8 @@ class AssetList: # Check 1: Does the property have a valid heating system? self.standardised_asset_list["solar_landlord_data_indicates_correct_heating_system"] = ( self.standardised_asset_list[self.STANDARD_HEATING_SYSTEM].isin( - ["air source heat pump", "ground source heat pump", "high heat retention storage heaters"] + ["air source heat pump", "ground source heat pump", "high heat retention storage heaters", + "electric boiler"] ) ) self.standardised_asset_list["solar_landlord_data_indicates_needs_heating_upgrade"] = ( @@ -1363,7 +1364,7 @@ class AssetList: self.standardised_asset_list["solar_epc_data_indicates_correct_heating_system"] = ( ( self.standardised_asset_list[self.EPC_API_DATA_NAMES["mainheat-description"]] - .str.lower().str.contains("air source heat pump|ground source heat pump") + .str.lower().str.contains("air source heat pump|ground source heat pump|boiler and radiators, electric") ) | ( self.standardised_asset_list[ self.EPC_API_DATA_NAMES["mainheat-description"]].str.lower().str.contains( diff --git a/asset_list/app.py b/asset_list/app.py index 2925e82f..67e18dac 100644 --- a/asset_list/app.py +++ b/asset_list/app.py @@ -111,6 +111,7 @@ def app(): outcomes_postcode = None outcomes_houseno = None outcomes_id = None + outcomes_address = None master_filepaths = [] master_to_asset_list_filepath = None diff --git a/asset_list/mappings/built_form.py b/asset_list/mappings/built_form.py index dbb25e9b..aad36fce 100644 --- a/asset_list/mappings/built_form.py +++ b/asset_list/mappings/built_form.py @@ -63,6 +63,22 @@ BUILT_FORM_MAPPINGS = { np.nan: 'unknown', 'Third Floor Flat': 'mid-floor', '2 Ext. Wall Flat': 'mid-terrace', - 'Hostel': 'unknown' - + 'Hostel': 'unknown', + 'Flat: Mid Terrace: Mid Floor': 'mid-terrace', + 'Bungalow: SemiDetached': 'semi-detached', + 'Flat: End Terrace: Top Floor': 'end-terrace', + 'Flat: Enclosed End Terrace: Top Floor': 'end-terrace', + 'Maisonette: End Terrace: Ground Floor': 'end-terrace', + 'Flat: End Terrace: Ground Floor': 'end-terrace', + 'Flat: Mid Terrace: Top Floor': 'mid-terrace', + 'House: Detached': 'detached', + 'Flat: End Terrace: Mid Floor': 'end-terrace', + 'House: SemiDetached': 'semi-detached', + 'Flat: Semi Detached: Ground Floor': 'semi-detached', + 'Flat: Semi Detached: Top Floor': 'semi-detached', + 'Flat: Mid Terrace: Ground Floor': 'mid-terrace', + 'House: MidTerrace': 'mid-terrace', + 'House: EndTerrace': 'end-terrace', + 'Bungalow: EndTerrace': 'end-terrace', + 'Bungalow: MidTerrace': 'mid-terrace' } diff --git a/asset_list/mappings/heating_systems.py b/asset_list/mappings/heating_systems.py index f6b0d0ea..714f5434 100644 --- a/asset_list/mappings/heating_systems.py +++ b/asset_list/mappings/heating_systems.py @@ -122,12 +122,26 @@ HEATING_MAPPINGS = { 'SOLID FUEL': 'solid fuel', 'GAS': 'gas combi boiler', 'DO NOT SURVEY': 'unknown', - 'Gas Boiler': 'gas combi boiler', 'Communal Gas ': 'communal gas boiler', 'Communal': 'communal gas boiler', 'Communal Gas': 'communal gas boiler', 'Wood Burning Boiler': "boiler - other fuel", - 'Oil Fired Boiler': 'oil boiler' - + 'Oil Fired Boiler': 'oil boiler', + 'Electric (direct acting) room heaters: Panel, convector or radiant heaters Electricity: Electricity': 'room ' + 'heaters', + 'Electric Storage Systems: Integrated storage+direct-acting heater Electricity: Electricity': 'electric storage ' + 'heaters', + 'Community Heating Systems: Community CHP and boilers (RdSAP) Gas: Mains Gas (Community)': 'communal gas boiler', + 'Boiler: D rated Regular Boiler Gas: Mains Gas': 'gas boiler', + 'Boiler: C rated Combi Gas: Mains Gas': 'gas combi boiler', + 'Electric Storage Systems: Fan storage heaters Electricity: Electricity': 'electric storage heaters', + ' ': 'unknown', + 'Boiler: G rated Regular Boiler Gas: Mains Gas': 'gas boiler', + 'Electric Storage Systems: Modern (slimline) storage heaters Electricity: Electricity': 'electric storage heaters', + 'Boiler: E rated Regular Boiler Gas: Mains Gas': 'gas boiler', + 'Boiler: A rated Regular Boiler Electricity: Electricity': 'electric boiler', + 'Community Heating Systems: Community boilers only (RdSAP) Gas: Mains Gas (Community)': 'communal gas boiler', + 'Boiler: A rated Combi Gas: Mains Gas': 'gas condensing combi', + 'Boiler: A rated CPSU Electricity: Electricity': 'electric boiler' } diff --git a/asset_list/mappings/property_type.py b/asset_list/mappings/property_type.py index be4aa797..139b1622 100644 --- a/asset_list/mappings/property_type.py +++ b/asset_list/mappings/property_type.py @@ -119,6 +119,22 @@ PROPERTY_MAPPING = { 'Studio (2nd floor)': 'flat', 'Third Floor Flat': 'flat', '2 Ext. Wall Flat': 'flat', - 'Hostel': 'other' - + 'Hostel': 'other', + 'House: MidTerrace': 'house', + 'House: EndTerrace': 'house', + 'Flat: Mid Terrace: Mid Floor': 'flat', + 'Bungalow: SemiDetached': 'bungalow', + 'Bungalow: EndTerrace': 'bungalow', + 'Flat: End Terrace: Top Floor': 'flat', + 'Maisonette: End Terrace: Ground Floor': 'maisonette', + 'Flat: End Terrace: Ground Floor': 'flat', + 'Flat: Mid Terrace: Top Floor': 'flat', + 'House: Detached': 'house', + 'Flat: End Terrace: Mid Floor': 'flat', + 'House: SemiDetached': 'house', + 'Flat: Semi Detached: Ground Floor': 'flat', + 'Flat: Semi Detached: Top Floor': 'flat', + 'Flat: Mid Terrace: Ground Floor': 'flat', + 'Bungalow: MidTerrace': 'bungalow', + 'Flat: Enclosed End Terrace: Top Floor': 'flat' } diff --git a/asset_list/mappings/walls.py b/asset_list/mappings/walls.py index 065aa988..e5f22f13 100644 --- a/asset_list/mappings/walls.py +++ b/asset_list/mappings/walls.py @@ -137,4 +137,15 @@ WALL_CONSTRUCTION_MAPPINGS = { 'Cavity CWI Completed by Dyson': 'filled cavity', None: "unknown", "Cavity": "cavity unknown insulation", + 'SolidBrick: Unknown': 'solid brick unknown insulation', + 'Cavity: Unknown': 'cavity unknown insulation', + 'Cavity: AsBuilt (Post 1995)': 'filled cavity', + 'Cavity: AsBuilt (1976-1982)': 'cavity unknown insulation', + 'SystemBuilt: AsBuilt': 'system built', + 'TimberFrame: AsBuilt': "timber frame unknown insulation", + 'Cavity: AsBuilt (1983-1995)': 'cavity unknown insulation', + 'Cavity: AsBuilt (1983-1995), Cavity: FilledCavity': 'filled cavity', + 'SolidBrick: AsBuilt': 'solid brick unknown insulation', + 'Cavity: FilledCavity': 'filled cavity', + 'SolidBrick: Internal': 'insulated solid brick' } diff --git a/backend/app/plan/router.py b/backend/app/plan/router.py index cce47566..45c19484 100644 --- a/backend/app/plan/router.py +++ b/backend/app/plan/router.py @@ -44,6 +44,7 @@ from backend.ml_models.Valuation import PropertyValuation from etl.bill_savings.KwhData import KwhData from etl.spatial.OpenUprnClient import OpenUprnClient +from etl.find_my_epc.RetrieveFindMyEpc import RetrieveFindMyEpc logger = setup_logger() @@ -514,6 +515,13 @@ async def trigger_plan(body: PlanTriggerRequest): ) ) + # if we have a remote assment data type, we pull the additional data and include it + if body.event_type == "remote_assessment": + logger.info("Retrieving find my epc data") + property_non_invasive_recommendations = RetrieveFindMyEpc.get_from_epc( + epc_searcher.newest_epc + ) + epc_records = patch_epc(patch, epc_records) prepared_epc = EPCRecord( diff --git a/backend/app/plan/schemas.py b/backend/app/plan/schemas.py index 7db0f16f..4237472d 100644 --- a/backend/app/plan/schemas.py +++ b/backend/app/plan/schemas.py @@ -37,6 +37,7 @@ MEASURE_MAP = { VALID_GOALS = ["Increasing EPC"] VALID_HOUSING_TYPES = ["Social", "Private"] +VALID_EVENT_TYPES = ["remote_assessment"] # Define the validation function for inclusions/exclusions @@ -56,10 +57,16 @@ def check_housing_type(value: str) -> str: return value +def check_event_type(value: str) -> str: + assert value in VALID_EVENT_TYPES, f"{value} is not a valid event type" + return value + + # Use Annotated with BeforeValidator for each list item validation InclusionOrExclusionItem = Annotated[str, BeforeValidator(check_inclusion_or_exclusion)] Goal = Annotated[str, BeforeValidator(check_goals)] HousingType = Annotated[str, BeforeValidator(check_housing_type)] +EventType = Annotated[str, BeforeValidator(check_event_type)] class PlanTriggerRequest(BaseModel): @@ -84,3 +91,7 @@ class PlanTriggerRequest(BaseModel): default_u_values: Optional[bool] = True ashp_cop: Optional[float] = 2.8 + + # When performing a remote assessment, if this has been set, it will allow the engine to + # pull data from the find my epc website, to utilise as part of a remote assessment + event_type: Optional[float] = "remote_assessment", diff --git a/etl/find_my_epc/RetrieveFindMyEpc.py b/etl/find_my_epc/RetrieveFindMyEpc.py index 7da21012..5e05d56f 100644 --- a/etl/find_my_epc/RetrieveFindMyEpc.py +++ b/etl/find_my_epc/RetrieveFindMyEpc.py @@ -3,6 +3,10 @@ import requests from bs4 import BeautifulSoup from datetime import datetime +from utils.logger import setup_logger + +logger = setup_logger() + class RetrieveFindMyEpc: SEARCH_POSTCODE_URL = ( @@ -366,3 +370,24 @@ class RetrieveFindMyEpc: formatted_recommendations.append(to_append) return formatted_recommendations + + @classmethod + def get_from_epc(cls, epc): + # Attempt both methods: + try: + searcher = cls(address=epc["address"], postcode=epc["postcode"]) + find_epc_data = searcher.retrieve_newest_find_my_epc_data() + except Exception as e: + logger.error(f"Error retrieving find my epc data: {e}") + # We attempt with the backup add + searcher = cls(address=epc["address1"], postcode=epc["postcode"]) + find_epc_data = searcher.retrieve_newest_find_my_epc_data() + + non_invasive_recommendations = { + "uprn": epc["uprn"], + "address": epc["address"], + "postcode": epc["postcode"], + "recommendations": find_epc_data["recommendations"], + } + + return non_invasive_recommendations