From 6933678810fe74f23c1d6c2dea71420f06f9b419 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 20 Feb 2024 20:16:56 +0000 Subject: [PATCH] Created high heat retention storage heaters recommendations + set up HotwaterRecommendations class --- backend/app/plan/router.py | 25 ++++- etl/customers/urban_splash.py | 30 +++--- etl/epc_clean/app.py | 2 - recommendations/Costs.py | 4 +- recommendations/HeatingControlRecommender.py | 20 ++-- recommendations/HeatingRecommender.py | 100 ++++--------------- recommendations/HotwaterRecommendations.py | 13 +++ recommendations/WallRecommendations.py | 13 +++ 8 files changed, 90 insertions(+), 117 deletions(-) create mode 100644 recommendations/HotwaterRecommendations.py diff --git a/backend/app/plan/router.py b/backend/app/plan/router.py index 375a551a..a75da345 100644 --- a/backend/app/plan/router.py +++ b/backend/app/plan/router.py @@ -42,6 +42,25 @@ logger = setup_logger() BATCH_SIZE = 5 + +def patch_epc(config, epc_records): + """ + This utility function is useful to patch the epc data if we have data from the customer + :return: + """ + + number_habitable_rooms = config.get("number-habitable-rooms", None) + number_heated_rooms = config.get("number-heated-rooms", None) + + if number_habitable_rooms is not None: + epc_records["original_epc"]["number-habitable-rooms"] = int(number_habitable_rooms) + + if number_heated_rooms is not None: + epc_records["original_epc"]["number-heated-rooms"] = int(number_heated_rooms) + + return epc_records + + router = APIRouter( prefix="/plan", tags=["plan"], @@ -56,6 +75,9 @@ async def trigger_plan(body: PlanTriggerRequest): session = sessionmaker(bind=db_engine)() created_at = datetime.now().isoformat() + # TODO: We should store the trigger file path in the database with the plan so we can track the file that + # triggered the plan + try: session.begin() logger.info("Getting the inputs") @@ -96,8 +118,7 @@ async def trigger_plan(body: PlanTriggerRequest): 'full_sap_epc': epc_searcher.full_sap_epc.copy(), 'old_data': epc_searcher.older_epcs.copy(), } - - # We can patch the data if we are provided data from the customer + epc_records = patch_epc(config, epc_records) prepared_epc = EPCRecord( epc_records=epc_records, diff --git a/etl/customers/urban_splash.py b/etl/customers/urban_splash.py index 85097ef7..96aad007 100644 --- a/etl/customers/urban_splash.py +++ b/etl/customers/urban_splash.py @@ -73,15 +73,26 @@ def app(): if newest_epc is None: raise Exception("FX ME") + if row["Beds"] == "Studio": + number_heated_rooms = 2 + number_habitable_rooms = 2 + else: + # Assume one room for communal space, one room for bathroom + number_heated_rooms = row["Beds"] + 2 + number_habitable_rooms = row["Beds"] + 2 + to_append = { **row.to_dict(), "uprn": newest_epc["uprn"], - "postcode to check": newest_epc["postcode"], + "address": newest_epc["address1"], + "postcode": newest_epc["postcode"], # "walls-description": newest_epc["walls-description"], # "roof-description": newest_epc["roof-description"], # "floor-description": newest_epc["floor-description"], # "total-floor-area": newest_epc["total-floor-area"], "full-address": newest_epc["address"], + "number-heated-rooms": number_heated_rooms, + "number-habitable-rooms": number_habitable_rooms, } processed_asset_list.append(to_append) @@ -92,23 +103,6 @@ def app(): epc_data_df = pd.DataFrame(epc_data) - example = epc_data_df.iloc[11, :] - rest = epc_data_df[epc_data_df["address1"] != example["address1"]] - z = rest[ - (rest["total-floor-area"] == example["total-floor-area"]) & - (rest["current-energy-rating"] == "C") - ] - # Walls better in the example - z["walls-description"] - example["walls-description"] - - # Example has a property above - z["roof-description"] - example["roof-description"] - - compare = pd.concat([pd.DataFrame(example).T, z]) - compare["mainheat-description"] - # We store this data # Store the data in s3 filename = f"{USER_ID}/{PORTFOLIO_ID}/test_inputs.csv" diff --git a/etl/epc_clean/app.py b/etl/epc_clean/app.py index bbb12d31..cf75f24a 100644 --- a/etl/epc_clean/app.py +++ b/etl/epc_clean/app.py @@ -36,9 +36,7 @@ def app(): cleaned_data = {} epc_directories = [entry for entry in EPC_DIRECTORY.iterdir() if entry.is_dir()] for directory in tqdm(epc_directories): - data = pd.read_csv(directory / "certificates.csv", low_memory=False) - # Rename the columns to the same format as the api returns data.columns = [c.replace("_", "-").lower() for c in data.columns] # Take just date before the date threshold diff --git a/recommendations/Costs.py b/recommendations/Costs.py index 1ae194a9..c94eb79c 100644 --- a/recommendations/Costs.py +++ b/recommendations/Costs.py @@ -935,7 +935,7 @@ class Costs: "labour_days": labour_days, } - def electric_storage_heaters(self, number_heated_rooms): + def high_heat_electric_storage_heaters(self, number_heated_rooms): """ We base the estimates for the cost of electric storage heaters on the cost per room as estimated by the @@ -946,7 +946,7 @@ class Costs: :param number_heated_rooms: int, number of rooms to be heated """ - total_cost = 1000 * number_heated_rooms + total_cost = 1500 * number_heated_rooms subtotal_before_vat = total_cost / (1 + self.VAT_RATE) vat = total_cost - subtotal_before_vat diff --git a/recommendations/HeatingControlRecommender.py b/recommendations/HeatingControlRecommender.py index 3e9b9dbc..81597f61 100644 --- a/recommendations/HeatingControlRecommender.py +++ b/recommendations/HeatingControlRecommender.py @@ -19,14 +19,12 @@ class HeatingControlRecommender: # This first iteration of the recommender will provide very basic recommendation # We recommend heating controls based on the main heating system - if heating_description in [ - "Room heaters, electric", "Electric storage heaters" - ]: + if heating_description in ["Room heaters, electric"]: self.recommend_room_heaters_electric_controls() return - if heating_description in ["Electric storage heaters, radiators"]: - self.recommend_celect_type_controls() + if heating_description in ["Electric storage heaters", "Electric storage heaters, radiators"]: + self.recommend_high_heat_retention_controls() return def recommend_room_heaters_electric_controls(self): @@ -78,18 +76,18 @@ class HeatingControlRecommender: # We don't implement any other recommendations right now return - def recommend_celect_type_controls(self): + def recommend_high_heat_retention_controls(self): """ - If the home has Electric storage heaters, radiators, we start by identifying potential heating controls that - could - be upgraded, that would provide a practical impact. This will be the least invasive improvement. + When applicable, we recommend upgrading the heating controls to high heat retention controls. This is a + specific type of control system that is designed to work with electric storage heaters. It is a more + efficient control system than the standard controls that come with electric storage heaters. We can then consider the heating system itself :return: """ # We recommend upgrading to Celect type controls - ending_config = MainheatControlAttributes("Celect-type controls").process() + ending_config = MainheatControlAttributes("Controls for high heat retention storage heaters").process() # We look at what has changed in the ending config, and compare it to the current config simulation_config = check_simulation_difference( new_config=ending_config, old_config=self.property.main_heating_controls @@ -99,7 +97,7 @@ class HeatingControlRecommender: self.recommendation.append( { - "description": "upgrade heating controls to Celect type controls", + "description": "upgrade heating controls to High Heat Retention Storage Heater Controls", **self.costs.celect_type_controls(), "simulation_config": simulation_config } diff --git a/recommendations/HeatingRecommender.py b/recommendations/HeatingRecommender.py index d77e6b16..11ae3da6 100644 --- a/recommendations/HeatingRecommender.py +++ b/recommendations/HeatingRecommender.py @@ -1,3 +1,5 @@ +import pandas as pd + from recommendations.Costs import Costs from recommendations.recommendation_utils import check_simulation_difference from backend.Property import Property @@ -17,14 +19,11 @@ class HeatingRecommender: self.recommendations = [] # This first iteration of the recommender will provide very basic recommendation # We recommend heating controls based on the main heating system - if self.property.main_heating["clean_description"] == "Room heaters, electric": + if self.property.main_heating["clean_description"] in [ + "Room heaters, electric", "Electric storage heaters", "Electric storage heaters, radiators" + ]: # Recommend high heat retention storage heaters - # self.recommend_room_heaters_electric(phase=phase, system_change=False, heating_controls_only=True) - # self.recommend_electric_storage_heaters(phase=phase, system_change=True, heating_controls_only=False) - return - - if self.property.main_heating["clean_description"] == "Electric storage heaters, radiators": - self.recommend_electric_storage_heaters(phase=phase, system_change=False, heating_controls_only=True) + self.recommend_electric_storage_heaters(phase=phase, system_change=True, heating_controls_only=False) return @staticmethod @@ -130,6 +129,9 @@ class HeatingRecommender: def recommend_electric_storage_heaters(self, phase, system_change, heating_controls_only): """ We recommend electric storage heaters as an upgrade to the heating system. + We will recommend upgrading to a high heat retention storage system, if the current system is not already + high heat retention storage + :param phase: The phase of the recommendation :param system_change: Indicates if we are recommending a different type of heating system, compared to the current system @@ -139,36 +141,20 @@ class HeatingRecommender: controls_recommender = HeatingControlRecommender(self.property) # The heating controls we're recommending for are based on the recommended heating system - + high_heat_retention_contols_desc = "Controls for high heat retention storage heaters" # We only recommend Celect-type controls if the current heating system is not Celect-type controls - if self.property.main_heating_controls["clean_description"] != "Celect-type controls": + if self.property.main_heating_controls["clean_description"] != high_heat_retention_contols_desc: controls_recommender.recommend(heating_description="Electric storage heaters, radiators") # Conditions for not needing this recommendation - efficient_room_heaters = self.property.main_heating["clean_description"] == "Room heaters, electric" and ( - self.property.data["mainheat-energy-eff"] not in ["Poor", "Very Poor", "Average"] - ) - - efficient_storage_heaters = ( - (self.property.main_heating["clean_description"] in [ - "Electric storage heaters, radiators", "Electric storage heaters" - ]) and self.property.data["mainheat-energy-eff"] not in ["Poor", "Very Poor", "Average"] + already_installed_hh_retention = ( + "Electric storage heaters" in self.property.main_heating["clean_description"] and + self.property.main_heating_controls["clean_description"].lower() == high_heat_retention_contols_desc.lower() ) # Conditions for not recommending electric storage heaters - if efficient_room_heaters or efficient_storage_heaters: - # We do just heating controls - self.recommendations.extend( - self.combine_heating_and_controls( - controls_recommendations=controls_recommender.recommendation, - heating_simulation_config={}, - costs={}, - description="", - phase=phase, - heating_controls_only=heating_controls_only, - system_change=system_change - ) - ) + if already_installed_hh_retention: + # No recommendation needed return # Set up artefacts, suitable for the simulation and regardless of controls @@ -180,60 +166,10 @@ class HeatingRecommender: heating_simulation_config["mainheat_energy_eff_ending"] = "Average" # Upgrade to electric storage heaters - costs = self.costs.electric_storage_heaters( + costs = self.costs.high_heat_electric_storage_heaters( number_heated_rooms=self.property.data["number-heated-rooms"] ) - description = "Install electric storage heaters" - - recommendations = self.combine_heating_and_controls( - controls_recommendations=controls_recommender.recommendation, - heating_simulation_config=heating_simulation_config, - costs=costs, - description=description, - phase=phase, - heating_controls_only=heating_controls_only, - system_change=system_change - ) - - self.recommendations.extend(recommendations) - - def recommend_room_heaters_electric(self, phase, system_change, heating_controls_only): - """ - If the home has Room heaters, electric, we start by identifying potential heating controls that could - be upgraded, that would provide a practical impact. This will be the least invasive improvement. - - We can then consider the heating system itself - :param phase: The phase of the recommendation - :param system_change: Indicates if we are recommending a different type of heating system, compared to the - current system - :param heating_controls_only: Indicates if we should include a recommendation for just heating controls - :return: - """ - - controls_recommender = HeatingControlRecommender(self.property) - if self.property.main_heating_controls["clean_description"] != "Programmer and appliance thermostats": - controls_recommender.recommend(heating_description='Room heaters, electric') - - if self.property.data["mainheat-energy-eff"] not in ["Poor", "Very Poor"]: - # We do just heating controls - self.recommendations.extend( - self.combine_heating_and_controls( - controls_recommendations=controls_recommender.recommendation, - heating_simulation_config={}, - costs={}, - description="", - phase=phase, - heating_controls_only=heating_controls_only, - system_change=system_change - ) - ) - return - - costs = self.costs.electric_room_heaters( - number_heated_rooms=self.property.data["number-heated-rooms"] - ) - description = "Upgrade electric room heaters to efficient electric radiators" - heating_simulation_config = {"mainheat_energy_eff_ending": "Average"} + description = "Install high heat retention electric storage heaters" recommendations = self.combine_heating_and_controls( controls_recommendations=controls_recommender.recommendation, diff --git a/recommendations/HotwaterRecommendations.py b/recommendations/HotwaterRecommendations.py new file mode 100644 index 00000000..b628f9a2 --- /dev/null +++ b/recommendations/HotwaterRecommendations.py @@ -0,0 +1,13 @@ +from backend.Property import Property +from recommendations.Costs import Costs + + +class HotwaterRecommendations: + def __init__(self, property_instance: Property): + self.property = property_instance + self.costs = Costs(self.property) + + self.recommendation = [] + + def recommend(self): + pass diff --git a/recommendations/WallRecommendations.py b/recommendations/WallRecommendations.py index 9b731af4..6b59c148 100644 --- a/recommendations/WallRecommendations.py +++ b/recommendations/WallRecommendations.py @@ -47,6 +47,12 @@ class WallRecommendations(Definitions): # we still consider it as an option U_VALUE_ERROR = 0.01 + # Typically when the U-value is around 0.75 and below, and the home is a new build, this is a good indication + # that the home is already insulated with at least some partial insulation. We don't recommend insulation + # in this case. This estimate was verified with the Warmfront team and 0.75 has been used as a conservative + # threshold + NEW_BUILD_INSULATED = 0.75 + def __init__( self, property_instance: Property, @@ -114,6 +120,13 @@ class WallRecommendations(Definitions): if self.property.walls["thermal_transmittance_unit"] != self.U_VALUE_UNIT: raise NotImplementedError("Haven't handled the case of other u value units yet") + + # If the property is a new build and the U-value is below 0.75, we don't recommend insulation because it's + # not practical + if (self.property.data["transaction-type"] == "new dwelling") and (u_value <= self.NEW_BUILD_INSULATED): + # Recommend nothing + return + # We can't detect it's a cavity wall, but it was built after 1990 so likely built with insulation already # + it already has a U-value WORSE than the building regulations, so we recommend either internal or # external wall insulation