From 24508b2a84cbbcb33bf5f7feff5ba217d69fe3b1 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Fri, 26 Jul 2024 16:41:32 +0100 Subject: [PATCH] added condition data to router --- backend/Property.py | 56 +++++++++++++++------ backend/app/db/models/energy_assessments.py | 3 ++ backend/app/plan/router.py | 2 + etl/bill_savings/EnergyConsumptionModel.py | 1 + etl/xml_survey_extraction/XmlParser.py | 13 +++++ etl/xml_survey_extraction/app.py | 5 ++ recommendations/WindowsRecommendations.py | 4 ++ 7 files changed, 68 insertions(+), 16 deletions(-) diff --git a/backend/Property.py b/backend/Property.py index 4f508b9a..6365bb0b 100644 --- a/backend/Property.py +++ b/backend/Property.py @@ -166,6 +166,7 @@ class Property: ) self.floor_level = None self.number_of_windows = None + self.windows_area = None self.solar_pv_percentage = None self.current_adjusted_energy = None @@ -707,17 +708,20 @@ class Property: # Today's costs todays_heating_cost = energy_consumption_client.convert_cost_to_today( original_cost=float(self.data["heating-cost-current"]), - lodgement_date=pd.Timestamp(self.epc_record.prepared_epc["lodgement_date"]) + lodgement_date=pd.Timestamp(self.epc_record.prepared_epc["lodgement_date"]).tz_localize(None) ) todays_hot_water_cost = energy_consumption_client.convert_cost_to_today( original_cost=float(self.data["hot-water-cost-current"]), - lodgement_date=pd.Timestamp(self.epc_record.prepared_epc["lodgement_date"]) + lodgement_date=pd.Timestamp(self.epc_record.prepared_epc["lodgement_date"]).tz_localize(None) ) todays_lighting_cost = energy_consumption_client.convert_cost_to_today( original_cost=float(self.data["lighting-cost-current"]), - lodgement_date=pd.Timestamp(self.epc_record.prepared_epc["lodgement_date"]) + lodgement_date=pd.Timestamp(self.epc_record.prepared_epc["lodgement_date"]).tz_localize(None) ) + # If we have the kwh figures, we don't need to predict them + condition_data = self.energy_assessment_condition_data.copy() + scoring_df = pd.DataFrame([self.epc_record.prepared_epc]) # Change columns from underscores to hyphens scoring_df.columns = [ @@ -727,13 +731,20 @@ class Property: scoring_df[col] = None energy_consumption_client.data = None - heating_prediction = energy_consumption_client.score_new_data( - new_data=scoring_df, target="heating_kwh" - )[0] - hot_water_prediction = energy_consumption_client.score_new_data( - new_data=scoring_df, target="hot_water_kwh" - )[0] + heating_prediction = ( + float(condition_data["space_heating_kwh"]) if condition_data["space_heating_kwh"] + else energy_consumption_client.score_new_data( + new_data=scoring_df, target="heating_kwh" + )[0] + ) + + hot_water_prediction = ( + float(condition_data["water_heating_kwh"]) if condition_data["water_heating_kwh"] + else energy_consumption_client.score_new_data( + new_data=scoring_df, target="hot_water_kwh" + )[0] + ) # We convert the lighting cost into kwh, just using the price cap lighting_kwh = float(self.data["lighting-cost-current"]) / AnnualBillSavings.ELECTRICITY_PRICE_CAP @@ -1040,13 +1051,14 @@ class Property: medians across the EPC data :return: """ - - # TODO: These functions should work on an EPCRecord object, so that the format is more standardised. - # They could also be added as attributes to the EPC Record - # Many of these pieces of information are now contained in the condition data condition_data = self.energy_assessment_condition_data.copy() + # We can update the number of floors if we have this information in the condition data + self.number_of_floors = int(self.energy_assessment_condition_data["number_of_floors"]) \ + if condition_data["number_of_floors"] is not None \ + else self.number_of_floors + self.perimeter = float(self.energy_assessment_condition_data["perimeter"]) \ if condition_data["perimeter"] is not None \ else estimate_perimeter( @@ -1054,14 +1066,18 @@ class Property: num_rooms=self.number_of_rooms / self.number_of_floors ) - self.insulation_wall_area = estimate_external_wall_area( + self.insulation_wall_area = float(self.energy_assessment_condition_data["insulation_wall_area"]) \ + if condition_data["insulation_wall_area"] is not None \ + else estimate_external_wall_area( num_floors=self.number_of_floors, floor_height=self.floor_height, perimeter=self.perimeter, built_form=self.data["built-form"], ) - self.insulation_floor_area = self.floor_area / self.number_of_floors + self.insulation_floor_area = float(self.energy_assessment_condition_data["main_dwelling_ground_floor_area"]) \ + if condition_data["main_dwelling_ground_floor_area"] is not None \ + else self.floor_area / self.number_of_floors self.pitched_roof_area = esimtate_pitched_roof_area( floor_area=self.insulation_floor_area, floor_height=self.floor_height @@ -1163,7 +1179,11 @@ class Property: :return: """ - self.number_of_windows = estimate_windows( + condition_data = self.energy_assessment_condition_data.copy() + + self.number_of_windows = int(condition_data["number_of_windows"]) \ + if condition_data["number_of_windows"] is not None \ + else estimate_windows( property_type=self.data["property-type"], built_form=self.data["built-form"], construction_age_band=self.construction_age_band, @@ -1171,6 +1191,10 @@ class Property: number_habitable_rooms=self.number_of_rooms, ) + self.windows_area = float(condition_data["windows_area"]) \ + if condition_data["windows_area"] is not None \ + else None + def set_solar_panel_area(self, photo_supply_lookup, floor_area_decile_thresholds): """ Sets the approximate area of the solar panels diff --git a/backend/app/db/models/energy_assessments.py b/backend/app/db/models/energy_assessments.py index f89cccb7..2c3cc144 100644 --- a/backend/app/db/models/energy_assessments.py +++ b/backend/app/db/models/energy_assessments.py @@ -119,6 +119,9 @@ class EnergyAssessment(Base): cylinder_insulation_type = Column(Text) cylinder_insulation_thickness = Column(Integer) cylinder_thermostat = Column(Boolean) + main_dwelling_ground_floor_area = Column(Float) + number_of_windows = Column(Integer) + windows_area = Column(Float) EPC_KEYS = [ 'low_energy_fixed_light_count', 'address', 'uprn_source', 'floor_height', 'heating_cost_potential', diff --git a/backend/app/plan/router.py b/backend/app/plan/router.py index 2ed19880..e76d4430 100644 --- a/backend/app/plan/router.py +++ b/backend/app/plan/router.py @@ -515,6 +515,8 @@ async def trigger_plan(body: PlanTriggerRequest): # ) print("Implement me") + # TODO: We can set the pitched roof area based on the results of the solar api! + logger.info("Getting components and epc recommendations") recommendations = {} recommendations_scoring_data = [] diff --git a/etl/bill_savings/EnergyConsumptionModel.py b/etl/bill_savings/EnergyConsumptionModel.py index 9a7d6523..dfb0e574 100644 --- a/etl/bill_savings/EnergyConsumptionModel.py +++ b/etl/bill_savings/EnergyConsumptionModel.py @@ -102,6 +102,7 @@ class EnergyConsumptionModel: # We also retrieve the newest retail price comparison data which comes from Ofgem: # https://www.ofgem.gov.uk/energy-data-and-research/data-portal/retail-market-indicators # We use the detail price comparison by company and tariff type data + print("Reading retail price comparison - make sure this is up-to-date") self.read_retail_price_comparison() def read_retail_price_comparison(self): diff --git a/etl/xml_survey_extraction/XmlParser.py b/etl/xml_survey_extraction/XmlParser.py index 8391314a..0bc3d56b 100644 --- a/etl/xml_survey_extraction/XmlParser.py +++ b/etl/xml_survey_extraction/XmlParser.py @@ -366,6 +366,16 @@ class XmlParser: self.insulation_wall_area = self.get_insulation_wall_area() + # We pull this out which is used as the insulation floor area + main_dwelling_ground_floor_area = [ + f for f in self.floor_dimensions if f["building_part_identifier"] == "Main Dwelling" and f["floor"] == "0" + ][0]["total_floor_area"] + + main_dwelling_windows = [w for w in self.windows if w["window_location"] == "0"] + + number_of_windows = len(main_dwelling_windows) + windows_area = sum([float(w["window_area"]) for w in main_dwelling_windows]) + boolean_lookup = { "true": True, "false": False, @@ -400,6 +410,9 @@ class XmlParser: "cylinder_insulation_type": cylinder_insulation_type[self.get_node_value('Cylinder-Insulation-Type')], "cylinder_insulation_thickness": int(self.get_node_value('Cylinder-Insulation-Thickness')), "cylinder_thermostat": boolean_lookup[self.get_node_value('Cylinder-Thermostat')], + "main_dwelling_ground_floor_area": float(main_dwelling_ground_floor_area), + "number_of_windows": int(number_of_windows), + "windows_area": float(windows_area), } def get_node_value(self, tag_name): diff --git a/etl/xml_survey_extraction/app.py b/etl/xml_survey_extraction/app.py index 9a813216..c4f6091f 100644 --- a/etl/xml_survey_extraction/app.py +++ b/etl/xml_survey_extraction/app.py @@ -58,6 +58,11 @@ def main(): # Idea: We can collect all of this information by building part and store it separately in the database # against the uprn. We can have key data for the EPC, but then also additional data for each building # part. We can then use this data to make recommendations that are specific to each building part + # We should probably re-think this data model, so we break up the data in a more considered fasion and produce + # the underlying EPC data as a summary of the building parts. Not only do we have data against the main + # dwelling and extensions, but we also have multiple windows with individiaul pieces of information that + # we can use to make recommendations. We should store this data in a way that we can easily access it and + # use it to make recommendations (e.g. we should have a Windows table) # For each property, we download the xmls and extract the data database_data = [] diff --git a/recommendations/WindowsRecommendations.py b/recommendations/WindowsRecommendations.py index 29c75989..9a30cd2e 100644 --- a/recommendations/WindowsRecommendations.py +++ b/recommendations/WindowsRecommendations.py @@ -48,10 +48,14 @@ class WindowsRecommendations: is_secondary_glazing = self.property.restricted_measures or ( self.property.windows["glazing_type"] == "secondary" ) + windows_area = self.property.windows_area if not number_of_windows: raise ValueError("Number of windows not specified") + if windows_area is not None: + raise Exception("We have windows area, we should use this data for our recommendations!!!") + if self.property.windows["has_glazing"] & ( self.property.windows["glazing_coverage"] == "full" ):