From 2c931b438367f63997760b56de3b64913727d530 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Fri, 26 Jul 2024 15:39:47 +0100 Subject: [PATCH] Updating logic for extracting heat loss perimeter and party walls from xml data --- backend/Property.py | 21 ++++++++++++--- backend/app/db/models/energy_assessments.py | 6 ++--- backend/app/plan/router.py | 24 ++++++++++++++++- etl/xml_survey_extraction/XmlParser.py | 30 ++++++++++++--------- etl/xml_survey_extraction/app.py | 12 +++++++++ 5 files changed, 74 insertions(+), 19 deletions(-) diff --git a/backend/Property.py b/backend/Property.py index 4d5a93a7..4f508b9a 100644 --- a/backend/Property.py +++ b/backend/Property.py @@ -76,6 +76,7 @@ class Property: already_installed=None, non_invasive_recommendations=None, measures=None, + energy_assessment=None, **kwargs ): @@ -178,6 +179,11 @@ class Property: self.recommendations_scoring_data = [] self.simulation_epcs = {} + # This additional condition data should change how we pass kwargs to this. We should no longer need to pass + # kwargs to this class, but instead, we should pass the energy assessment condition data + self.energy_assessment_condition_data = energy_assessment["condition"] + + # TODO: We keep this but only temporarily until we add bathrooms, bedrooms, building id to the condition data self.parse_kwargs(kwargs) @classmethod @@ -188,6 +194,10 @@ class Property: :param kwargs: :return: """ + + # Note - none of this data is contained in an energy asssessment, but we should consider how this is done + # as we collect more data from the energy assessment + n_bathrooms = kwargs.get("n_bathrooms", None) if n_bathrooms not in [None, ""]: # We add on a small value to ensure that the number of bathrooms is rounded up, in case the value is 0.5 @@ -1034,9 +1044,14 @@ class Property: # 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 - self.perimeter = estimate_perimeter( - self.floor_area / self.number_of_floors, - self.number_of_rooms / self.number_of_floors, + # Many of these pieces of information are now contained in the condition data + condition_data = self.energy_assessment_condition_data.copy() + + self.perimeter = float(self.energy_assessment_condition_data["perimeter"]) \ + if condition_data["perimeter"] is not None \ + else estimate_perimeter( + floor_area=self.floor_area / self.number_of_floors, + num_rooms=self.number_of_rooms / self.number_of_floors ) self.insulation_wall_area = estimate_external_wall_area( diff --git a/backend/app/db/models/energy_assessments.py b/backend/app/db/models/energy_assessments.py index efcbc26c..f89cccb7 100644 --- a/backend/app/db/models/energy_assessments.py +++ b/backend/app/db/models/energy_assessments.py @@ -150,13 +150,13 @@ class EnergyAssessment(Base): epc = {key.replace("_", "-"): getattr(self, key) for key in self.EPC_KEYS} # Get everything else - additional = { + condition = { column.name: getattr(self, column.name) for column in self.__table__.columns if column.name not in self.EPC_KEYS } - return {"epc": epc, "additional": additional} + return {"epc": epc, "condition": condition} @staticmethod def empty_response(): - return {"epc": {}, "additional": {}} + return {"epc": {}, "condition": {}} diff --git a/backend/app/plan/router.py b/backend/app/plan/router.py index 175561e4..2ed19880 100644 --- a/backend/app/plan/router.py +++ b/backend/app/plan/router.py @@ -360,6 +360,27 @@ async def trigger_plan(body: PlanTriggerRequest): # Otherwise, we use the newest EPC epc_records = create_epc_records(epc_searcher, energy_assessment) + patch = next(( + x for x in patches if (x["address"] == config["address"]) and (x["postcode"] == config["postcode"]) + ), {}) + epc_records = patch_epc(patch, epc_records) + + prepared_epc = EPCRecord( + epc_records=epc_records, + run_mode="newdata", + cleaning_data=cleaning_data + ) + + property_already_installed = next(( + x for x in already_installed if + (x["address"] == config["address"]) and (x["postcode"] == config["postcode"]) + ), {}) + + property_non_invasive_recommendations = next(( + x for x in non_invasive_recommendations if + (x["address"] == config["address"]) and (x["postcode"] == config["postcode"]) + ), {}) + input_properties.append( Property( id=property_id, @@ -368,7 +389,8 @@ async def trigger_plan(body: PlanTriggerRequest): epc_record=prepared_epc, already_installed=property_already_installed, non_invasive_recommendations=property_non_invasive_recommendations, - **Property.extract_kwargs(config) + energy_assessment=energy_assessment, + **Property.extract_kwargs(config), # TODO: Depraecate this ) ) diff --git a/etl/xml_survey_extraction/XmlParser.py b/etl/xml_survey_extraction/XmlParser.py index 522cb899..3301b0be 100644 --- a/etl/xml_survey_extraction/XmlParser.py +++ b/etl/xml_survey_extraction/XmlParser.py @@ -645,19 +645,25 @@ class XmlParser: self.number_of_floors = len( [f for f in self.floor_dimensions if f["building_part_identifier"] == "Main Dwelling"] ) - self.heat_loss_perimeter = max( - [ - float(f["heat_loss_perimeter"]) for f in self.floor_dimensions - if f["building_part_identifier"] == "Main Dwelling" and not f["room_roof"] - ] - ) - self.party_wall_length = max( - [ - float(f["party_wall_length"]) for f in self.floor_dimensions - if f["building_part_identifier"] == "Main Dwelling" and not f["room_roof"] - ] - ) + # We extract the maximum heat loss perimeter, per building part + max_heat_loss_perimeters = {d['building_part_identifier']: max( + (float(x['heat_loss_perimeter']) for x in self.floor_dimensions if + x['building_part_identifier'] == d['building_part_identifier'] and x['heat_loss_perimeter']), + default=float('-inf') + ) for d in self.floor_dimensions} + + self.heat_loss_perimeter = sum(max_heat_loss_perimeters.values()) + + max_party_walls = { + d['building_part_identifier']: max( + (float(x['party_wall_length']) for x in self.floor_dimensions if + x['building_part_identifier'] == d['building_part_identifier'] and x['party_wall_length']), + default=float('-inf') + ) for d in self.floor_dimensions + } + + self.party_wall_length = sum(max_party_walls.values()) self.perimeter = self.heat_loss_perimeter + self.party_wall_length diff --git a/etl/xml_survey_extraction/app.py b/etl/xml_survey_extraction/app.py index beb47454..7f4e679c 100644 --- a/etl/xml_survey_extraction/app.py +++ b/etl/xml_survey_extraction/app.py @@ -48,6 +48,9 @@ def main(): # TODO: IF we have many uploads, we can do them in a batch so we don't try and upload huge amounts of data to # the database at onece + # TODO: We now have detailed information about primary and secondary walls, so we should use this information + # in our recommendations when we have it + # For each property, we download the xmls and extract the data database_data = [] for uprn, xmls in assessments_map.items(): @@ -117,3 +120,12 @@ def main(): # https://www.ncm-pcdb.org.uk/sap/download # However retrieving this data is not a priority, so we can leave this for now as parsing the database # is a non-trivial task + + # TODO: The condition report contains additional data such as the number of bedrooms and the number of bathrooms + # We can extract this data and store it in the database as well. We can then update our kwargs methodology + # that is passed to the property class, where instead we store this additional data in our database (it could + # be stored in the energy assessment table, or in a separate table) and then when we're passed additional data + # we can query the database for this data and use it to update the property object, instead of storing it + # in the asset list and pulling it out of the asset list + # 1) Bathrooms + # 2) Bedrooms