From 68fc8b8cbbba3f12e9081af92c2fd9ad0c4f0a94 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 17 Sep 2024 09:15:31 +0100 Subject: [PATCH] Added dual heating combined recommendation --- recommendations/HeatingControlRecommender.py | 38 ++- recommendations/HeatingRecommender.py | 224 +++++++++++++++--- recommendations/recommendation_utils.py | 41 ++++ .../test_data/heating_recommendations_data.py | 97 +++++++- .../tests/test_heating_recommendations.py | 2 + 5 files changed, 360 insertions(+), 42 deletions(-) diff --git a/recommendations/HeatingControlRecommender.py b/recommendations/HeatingControlRecommender.py index 6f848441..62e292df 100644 --- a/recommendations/HeatingControlRecommender.py +++ b/recommendations/HeatingControlRecommender.py @@ -12,8 +12,11 @@ class HeatingControlRecommender: self.recommendation = [] - def recommend(self, heating_description): + def recommend(self, heating_description, description_prefix="", description_suffix=""): + # TODO: Many of these functions are quite similar. We can possibly create a single wrapper function that + # takes in the heating description and the description prefix/suffix, and then creates the appropriate + # output # Reset the recommendations self.recommendation = [] @@ -24,14 +27,14 @@ class HeatingControlRecommender: return if heating_description in ["Electric storage heaters", "Electric storage heaters, radiators"]: - self.recommend_high_heat_retention_controls() + self.recommend_high_heat_retention_controls(description_prefix=description_prefix) return if heating_description in ["Boiler and radiators, mains gas"]: # We can recommend roomstat programmer trvs - self.recommend_roomstat_programmer_trvs() + self.recommend_roomstat_programmer_trvs(description_suffix=description_suffix) # We can also recommend time and temperature zone controls - self.recommend_time_temperature_zone_controls() + self.recommend_time_temperature_zone_controls(description_suffix=description_suffix) return @@ -94,16 +97,22 @@ class HeatingControlRecommender: # We don't implement any other recommendations right now return - def recommend_high_heat_retention_controls(self): + def recommend_high_heat_retention_controls(self, description_prefix=""): """ 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 + + If there is a description prefix, this means there is a dual heating system and so we need to add this to the + description + :return: """ new_description = "Controls for high heat retention storage heaters" + if description_prefix: + new_description = f"{description_prefix}, {new_description}" # We recommend upgrading to Celect type controls ending_config = MainheatControlAttributes(new_description).process() @@ -112,7 +121,10 @@ class HeatingControlRecommender: new_config=ending_config, old_config=self.property.main_heating_controls ) # This upgrade will only take the heating system to average energy efficiency - simulation_config["mainheatc_energy_eff_ending"] = "Good" + if self.property.data["mainheatc-energy-eff"] in ["Poor", "Very Poor", "Average"]: + simulation_config["mainheatc_energy_eff_ending"] = "Good" + else: + simulation_config["mainheatc_energy_eff_ending"] = self.property.data["mainheatc-energy-eff"] description_simulation = { "mainheatcont-description": new_description, @@ -131,7 +143,7 @@ class HeatingControlRecommender: # We don't implement any other recommendations right now return - def recommend_roomstat_programmer_trvs(self): + def recommend_roomstat_programmer_trvs(self, description_suffix=""): """ If the home has a boiler and radiators, mains gas, we start by identifying potential heating controls that could be upgraded, that would provide a practical impact. @@ -163,6 +175,8 @@ class HeatingControlRecommender: return new_controls_description = "Programmer, room thermostat and TRVS" + if description_suffix: + new_controls_description = f"{new_controls_description}, {description_suffix}" ending_config = MainheatControlAttributes(new_controls_description).process() # We use this to determine how we should be updating the config @@ -216,7 +230,7 @@ class HeatingControlRecommender: return - def recommend_time_temperature_zone_controls(self): + def recommend_time_temperature_zone_controls(self, description_suffix=""): """ If the home has a boiler, we can recommend time and temperature zone controls. This is a more advanced and more efficient control system than the standard controls that come with a boiler. However, it may come @@ -238,6 +252,8 @@ class HeatingControlRecommender: return new_controls_description = "Time and temperature zone control" + if description_suffix: + new_controls_description = f"{new_controls_description}, {description_suffix}" ending_config = MainheatControlAttributes(new_controls_description).process() @@ -260,8 +276,10 @@ class HeatingControlRecommender: number_heated_rooms=int(self.property.data["number-heated-rooms"]) ) - description = ("Upgrade heating controls to Smart Thermostats, room sensors and smart radiator valves (time & " - "temperature zone control)") + description = ( + "Upgrade heating controls to Smart Thermostats, room sensors and smart radiator valves (time & " + "temperature zone control)" + ) already_installed = "heating_control" in self.property.already_installed if already_installed: diff --git a/recommendations/HeatingRecommender.py b/recommendations/HeatingRecommender.py index bb074407..23b9bf7d 100644 --- a/recommendations/HeatingRecommender.py +++ b/recommendations/HeatingRecommender.py @@ -1,5 +1,7 @@ from recommendations.Costs import Costs, BOILER_UPGRADE_SCHEME_ASHP_VALUE -from recommendations.recommendation_utils import check_simulation_difference, override_costs +from recommendations.recommendation_utils import ( + check_simulation_difference, override_costs, combine_recommendation_configs +) from backend.Property import Property from backend.app.plan.schemas import MEASURE_MAP from etl.epc_clean.epc_attributes.MainheatAttributes import MainHeatAttributes @@ -11,6 +13,37 @@ from recommendations.HeatingControlRecommender import HeatingControlRecommender class HeatingRecommender: high_heat_retention_contols_desc = "Controls for high heat retention storage heaters" + DUAL_HEATING_DESCRIPTIONS = { + "Boiler and radiators, mains gas, electric storage heaters": { + "hhr": { + "mainheating_description": "Boiler and radiators, mains gas, Electric storage heaters", + "recommendation_description": "Install high heat retention electric storage heaters alongside the " + "boiler. The current electric heaters may be retrofit with high heat " + "retention storage controls" + " however this is dependent on the existing system and may not be " + "possible.", + "controls_prefix": "current_controls" + }, + "boiler": { + "mainheating_description": "Boiler and radiators, mains gas, electric storage heaters", + "recommendation_description": "Upgrade the existing boiler to a new, more efficient condensing " + "boiler. ", + "controls_suffix": "Manual charge controls" + }, + # These are the heating types we need to produce a dual heating recommendation + "dual": { + "recommendation_description": "Upgrade both the existing boiler to a new condensing boiler and" + "upgrade storage heaters to high heat retention storage heaters.", + "types": [ + # type 1 + "boiler_upgrade", + # type 2 + "high_heat_retention_storage_heater", + ] + } + } + } + def __init__(self, property_instance: Property): self.property = property_instance self.costs = Costs(self.property) @@ -20,10 +53,34 @@ class HeatingRecommender: self.has_electric_heating_description = self.property.main_heating["has_electric"] self.has_ashp = self.property.main_heating["has_air_source_heat_pump"] + self.has_room_heaters = ( + self.property.main_heating["has_room_heaters"] or + self.property.main_heating["has_portable_electric_heaters"] + ) + self.has_boiler = self.property.main_heating["has_boiler"] + + self.dual_heating = self.identify_dual_heating() + + def identify_dual_heating(self): + # All heat systems are in here so we identify whether two of these are true + # MainHeatAttributes.HEAT_SYSTEMS + + n_trues = 0 + for heat_system in MainHeatAttributes.HEAT_SYSTEMS: + if self.property.main_heating[f"has_{heat_system.replace(' ', '_')}"]: + n_trues += 1 + + if n_trues > 2 or n_trues == 0: + raise Exception("Implement me") + if n_trues == 1: + return False + + return True def is_high_heat_retention_valid(self, ashp_only_heating_recommendation, measures): """ Check conditions if high heat retention storage is valid + If there's already an ASHP in place, we don't recommend HHR :return: """ @@ -32,8 +89,6 @@ class HeatingRecommender: hhr_suitable = no_mains or self.has_electric_heating_description - # If there's already an ASHP in place, we don't recommend HHR - return ( hhr_suitable and (not ashp_only_heating_recommendation) and not self.has_ashp and ("high_heat_retention_storage_heater" in measures) @@ -47,7 +102,7 @@ class HeatingRecommender: # 1) if the property has mains heating with boiler and radiators, we recommend optimal heating controls # If it's NOT a gas boiler, we'll potentially recommend a boiler - has_gas_boiler = self.property.main_heating["has_boiler"] and self.property.main_heating["has_mains_gas"] + has_gas_boiler = self.has_boiler and self.property.main_heating["has_mains_gas"] # 2) If the property doesn't have a heating system, but it has access to the mains gas no_heating_has_mains = self.property.main_heating["clean_description"] in [ @@ -55,21 +110,13 @@ class HeatingRecommender: ] and self.property.data["mains-gas-flag"] # The property is using portable heaters and has access to gas mains - has_room_heaters = ( - ( - self.property.main_heating["has_room_heaters"] or - self.property.main_heating["has_portable_electric_heaters"] - ) and - self.property.data["mains-gas-flag"] - ) + has_room_heaters = self.has_room_heaters and self.property.data["mains-gas-flag"] # We also check if the property has electric heating, but it has access to the mains gas electic_heating_has_mains = self.has_electric_heating_description and self.property.data["mains-gas-flag"] portable_heaters_has_mains = ( - self.property.main_heating["clean_description"] in ["Portable electric heaters assumed for most rooms"] - and - self.property.data["mains-gas-flag"] + self.property.main_heating["has_portable_electric_heaters"] and self.property.data["mains-gas-flag"] ) # The next condition is if the home has a non-gas boiler, such as an oil boiler, with a mains gas connection @@ -95,6 +142,55 @@ class HeatingRecommender: return is_valid, has_gas_boiler + def recommend_dual_heating(self): + + if self.property.main_heating["clean_description"] not in self.DUAL_HEATING_DESCRIPTIONS: + return + + dual_heating_description = self.DUAL_HEATING_DESCRIPTIONS[ + self.property.main_heating["clean_description"] + ]["dual"]["types"] + + recommendation_system_types = list(set([x["system_type"] for x in self.heating_recommendations])) + + # We check if we have the required type + if not any([x in recommendation_system_types for x in dual_heating_description]): + return + + type_1_recommendations = [ + x for x in self.heating_recommendations if x["system_type"] == dual_heating_description[0] + ] + type_2_recommendations = [ + x for x in self.heating_recommendations if x["system_type"] == dual_heating_description[1] + ] + # we combine the two recommendations + combined_recommendations = [] + for rec in type_1_recommendations: + for rec2 in type_2_recommendations: + combined_rec = rec.copy() + # Update the description + combined_rec["description"] = self.DUAL_HEATING_DESCRIPTIONS[ + self.property.main_heating["clean_description"] + ]["dual"]["recommendation_description"] + + # Combine simulation_config + # Make sure we end up with the best efficiecy values + combined_rec["simulation_config"] = combine_recommendation_configs( + rec["simulation_config"], rec2["simulation_config"] + ) + # Combine description_simulation + combined_rec["description_simulation"] = combine_recommendation_configs( + rec["description_simulation"], rec2["description_simulation"] + ) + + # Combine costs + for k in ["total", "subtotal", "vat", "labour_hours", "labour_days"]: + combined_rec[k] = rec[k] + rec2[k] + + combined_recommendations.append(combined_rec) + + self.heating_recommendations.extend(combined_recommendations) + def recommend(self, has_cavity_or_loft_recommendations, phase=0, measures=None): """ Produces heating recommendations @@ -144,14 +240,16 @@ class HeatingRecommender: # This indicates that the home previously did not have a boiler in place and so would require # an overhaul to the system - right now, this is all reasons, apart from if there is an existing boiler system_change = not has_gas_boiler - exising_room_heaters = self.property.main_heating["clean_description"] in [ - "Room heaters, electric", "Room heaters, mains gas" - ] + exising_room_heaters = self.property.main_heating["has_room_heaters"] self.recommend_boiler_upgrades( phase=phase, system_change=system_change, exising_room_heaters=exising_room_heaters ) + # If we have dual heating and we allow for a combined recommendation, to upgrade both systems + if self.dual_heating: + self.recommend_dual_heating(phase=phase, measures=measures) + # We recommend air source heat pumps # Heat pumps are suitable for all property types: # https://energysavingtrust.org.uk/from-flats-to-terraced-houses-heat-pumps-are-suitable-for-all-property-types/ @@ -424,7 +522,8 @@ class HeatingRecommender: description, phase, heating_controls_only, - system_change + system_change, + system_type ): """ Given a recommendation for heating controls, and a recommendation for the heating system, we combine the two @@ -439,6 +538,7 @@ class HeatingRecommender: :param system_change: Indicates if we are recommending a different type of heating system, compared to the current system. If we have a system change and we have a heat control recommendation, we only recommend both heating and controls together + :param system_type: The type of heating system we are recommending :return: """ @@ -494,7 +594,9 @@ class HeatingRecommender: "already_installed": already_installed, **total_costs, "simulation_config": recommendation_simulation_config, - "description_simulation": recommendation_description_simulation + "description_simulation": recommendation_description_simulation, + # We insert the heating system type here + "system_type": system_type } output.append(recommendation) @@ -572,7 +674,19 @@ class HeatingRecommender: # We only recommend Celect-type controls if the current heating system is not Celect-type controls if self.property.main_heating_controls["clean_description"] != self.high_heat_retention_contols_desc: - controls_recommender.recommend(heating_description="Electric storage heaters, radiators") + if self.dual_heating: + if self.DUAL_HEATING_DESCRIPTIONS[self.property.main_heating["clean_description"]]["hhr"][ + "controls_prefix" + ] == "current_controls": + description_prefix = self.property.main_heating_controls["clean_description"] + else: + raise NotImplementedError("Implement me") + else: + description_prefix = "" + + controls_recommender.recommend( + heating_description="Electric storage heaters, radiators", description_prefix=description_prefix + ) has_hhr = self.is_hhr_already_installed() # Conditions for not recommending electric storage heaters @@ -580,7 +694,13 @@ class HeatingRecommender: # No recommendation needed return - new_heating_description = "Electric storage heaters, radiators" + # We check if the property has dual heating in place with a boiler and storage heaters + if self.dual_heating: + new_heating_description = self.DUAL_HEATING_DESCRIPTIONS[ + self.property.main_heating["clean_description"] + ]["hhr"]["mainheating_description"] + else: + new_heating_description = "Electric storage heaters, radiators" # Set up artefacts, suitable for the simulation and regardless of controls heating_ending_config = MainHeatAttributes(new_heating_description).process() @@ -588,7 +708,10 @@ class HeatingRecommender: new_config=heating_ending_config, old_config=self.property.main_heating ) # This upgrade will only take the heating system to average energy efficiency - heating_simulation_config["mainheat_energy_eff_ending"] = "Average" + if self.property.data["mainheat-energy-eff"] in ["Very Poor", "Poor"]: + heating_simulation_config["mainheat_energy_eff_ending"] = "Average" + else: + heating_simulation_config["mainheat_energy_eff_ending"] = self.property.data["mainheat-energy-eff"] # If the property is off-gas and has no heating system in place, the number of heated rooms will actually # be 0, so we use the number of rooms as the figure @@ -603,7 +726,13 @@ class HeatingRecommender: costs = self.costs.high_heat_electric_storage_heaters( number_heated_rooms=number_heated_rooms ) - description = "Install high heat retention electric storage heaters." + if self.dual_heating: + description = self.DUAL_HEATING_DESCRIPTIONS[ + self.property.main_heating["clean_description"] + ]["hhr"]["recommendation_description"] + + else: + description = "Install high heat retention electric storage heaters." # We check the existing heating system and controls if ( @@ -627,7 +756,8 @@ class HeatingRecommender: description=description, phase=phase, heating_controls_only=heating_controls_only, - system_change=system_change + system_change=system_change, + system_type="high_heat_retention_storage_heater" ) if _return: return recommendations @@ -721,11 +851,26 @@ class HeatingRecommender: num_heated_rooms=self.property.data["number-heated-rooms"], ) - description = "Upgrade to a new condensing boiler" + if self.dual_heating: + description = self.DUAL_HEATING_DESCRIPTIONS[ + self.property.main_heating["clean_description"] + ]["boiler"]["recommendation_description"] + else: + description = "Upgrade to a new condensing boiler" + + new_heating_eff = ( + "Good" if self.property.data["mainheat-energy-eff"] in ["Very Poor", "Poor", "Average"] + else self.property.data["mainheat-energy-eff"] + ) + + new_hotwater_eff = ( + "Good" if self.property.data["hot-water-energy-eff"] in ["Very Poor", "Poor", "Average"] + else self.property.data["hot-water-energy-eff"] + ) simulation_config = { - "mainheat_energy_eff_ending": "Good", - "hot_water_energy_eff_ending": "Good" + "mainheat_energy_eff_ending": new_heating_eff, + "hot_water_energy_eff_ending": new_hotwater_eff } description_simulation = { @@ -736,7 +881,13 @@ class HeatingRecommender: if system_change: # Installation of a boiler improves the hot water system so we need to reflect this in # the outcome of the recommendation - new_heating_description = "Boiler and radiators, mains gas" + if self.dual_heating: + new_heating_description = self.DUAL_HEATING_DESCRIPTIONS[ + self.property.main_heating["clean_description"] + ]["boiler"]["mainheating_description"] + else: + new_heating_description = "Boiler and radiators, mains gas" + new_hotwater_description = "From main system" new_fuel_description = "mains gas (not community)" @@ -794,13 +945,23 @@ class HeatingRecommender: "already_installed": already_installed, "simulation_config": simulation_config, "description_simulation": description_simulation, - **boiler_costs + **boiler_costs, + "system_type": "boiler_upgrade", } # We recommend the heating controls # If the property did not previously have a boiler, we combine controls_recommender = HeatingControlRecommender(self.property) - controls_recommender.recommend(heating_description="Boiler and radiators, mains gas") + if self.dual_heating: + description_suffix = self.DUAL_HEATING_DESCRIPTIONS[ + self.property.main_heating["clean_description"] + ]["boiler"]["controls_suffix"] + else: + description_suffix = "" + controls_recommender.recommend( + heating_description="Boiler and radiators, mains gas", + description_suffix=description_suffix + ) # We may have 2 recommendations from the heating controls if not controls_recommender.recommendation and not boiler_recommendation: @@ -822,7 +983,8 @@ class HeatingRecommender: description=boiler_recommendation["description"], phase=recommendation_phase, heating_controls_only=False, - system_change=True + system_change=True, + system_type="boiler_upgrade" ) combined_recommendations.extend(combined_recommendation) diff --git a/recommendations/recommendation_utils.py b/recommendations/recommendation_utils.py index ce32e061..883a387b 100644 --- a/recommendations/recommendation_utils.py +++ b/recommendations/recommendation_utils.py @@ -800,3 +800,44 @@ def override_costs(costs): costs[k] = 0 return costs + + +def combine_recommendation_configs(recommendation_config1, recommendation_config2): + """ + Given two simulation configs, this function will combine them into one + :param recommendation_config1: + :param recommendation_config2: + :return: + """ + # Efficiency values - keys which contain _energy_eff_ending + eff_1 = { + k: v for k, v in recommendation_config1.items() if ("_energy_eff_ending" in k) or ("-energy-eff" in k) + } + eff_2 = { + k: v for k, v in recommendation_config2.items() if ("_energy_eff_ending" in k) or ("-energy-eff" in k) + } + + # We combine the simulation configs + combined = { + **recommendation_config1, + **recommendation_config2 + } + + # Find overlapping keys + overlapping_keys = set(eff_1.keys()).intersection(set(eff_2.keys())) + if overlapping_keys: + # We make sure we take the best value - map efficiency values to numbers + numerical_embedding = { + "Very poor": 1, + "Poor": 2, + "Average": 3, + "Good": 4, + "Very good": 5, + } + for key in overlapping_keys: + if numerical_embedding[eff_1[key]] >= numerical_embedding[eff_2[key]]: + combined[key] = eff_1[key] + else: + combined[key] = eff_2[key] + + return combined diff --git a/recommendations/tests/test_data/heating_recommendations_data.py b/recommendations/tests/test_data/heating_recommendations_data.py index 51d0636e..821e79c6 100644 --- a/recommendations/tests/test_data/heating_recommendations_data.py +++ b/recommendations/tests/test_data/heating_recommendations_data.py @@ -1066,6 +1066,99 @@ testing_examples = [ "heating_recommendation_descriptions": [], "heating_controls_recommendation_descriptions": [], "notes": "This property already has an ashp. We don't recommend any heating upgrades" + }, + { + "epc": { + 'lmk-key': '1dd9aa80d6e5bae3e0e4892d9ed1a83b53f3af848568f4a928c9f7a63d8825ea', + 'address1': '49 Ridgeway Road', 'address2': 'Wordsley', 'address3': None, 'postcode': 'DY8 5UD', + 'building-reference-number': 10003464876, 'current-energy-rating': 'F', 'potential-energy-rating': 'D', + 'current-energy-efficiency': 35, 'potential-energy-efficiency': 64, 'property-type': 'House', + 'built-form': 'Semi-Detached', 'inspection-date': '2021-11-17', 'local-authority': 'E08000027', + 'constituency': 'E14000672', 'county': None, 'lodgement-date': '2022-10-10', 'transaction-type': 'rental', + 'environment-impact-current': 41, 'environment-impact-potential': 67, 'energy-consumption-current': 401, + 'energy-consumption-potential': 207, 'co2-emissions-current': 6.1, 'co2-emiss-curr-per-floor-area': 69, + 'co2-emissions-potential': 3.2, 'lighting-cost-current': 61, 'lighting-cost-potential': 61, + 'heating-cost-current': 1488, 'heating-cost-potential': 1015, 'hot-water-cost-current': 114, + 'hot-water-cost-potential': 77, 'total-floor-area': 89.0, 'energy-tariff': 'Single', 'mains-gas-flag': 'Y', + 'floor-level': None, 'flat-top-storey': None, 'flat-storey-count': None, 'main-heating-controls': None, + 'multi-glaze-proportion': 100.0, 'glazed-type': 'double glazing installed during or after 2002', + 'glazed-area': 'Normal', 'extension-count': 2, 'number-habitable-rooms': 5, 'number-heated-rooms': 5, + 'low-energy-lighting': 91, 'number-open-fireplaces': 0, 'hotwater-description': 'From main system', + 'hot-water-energy-eff': 'Good', 'hot-water-env-eff': 'Good', + 'floor-description': 'Solid, no insulation (assumed)', 'floor-energy-eff': None, + 'windows-description': 'Fully double glazed', 'windows-energy-eff': 'Good', 'windows-env-eff': 'Good', + 'walls-description': 'Cavity wall, as built, no insulation (assumed)', 'walls-energy-eff': 'Poor', + 'walls-env-eff': 'Poor', 'secondheat-description': 'Room heaters, electric', + 'roof-description': 'Pitched, 200 mm loft insulation', 'roof-energy-eff': 'Good', 'roof-env-eff': 'Good', + 'mainheat-description': 'Boiler and radiators, mains gas, Electric storage heaters', + 'mainheat-energy-eff': 'Good', 'mainheat-env-eff': 'Good', + 'mainheatcont-description': 'Programmer, room thermostat and TRVs', 'mainheatc-energy-eff': 'Good', + 'mainheatc-env-eff': 'Good', 'lighting-description': 'Low energy lighting in 91% of fixed outlets', + 'lighting-energy-eff': 'Very Good', 'lighting-env-eff': 'Very Good', + 'main-fuel': 'mains gas (not community)', 'wind-turbine-count': 0, 'heat-loss-corridor': None, + 'unheated-corridor-length': None, 'floor-height': 2.53, 'photo-supply': 0.0, + 'solar-water-heating-flag': 'N', + 'mechanical-ventilation': 'natural', 'address': '49 Ridgeway Road, Wordsley', + 'local-authority-label': 'Dudley', 'constituency-label': 'Dudley South', 'posttown': 'Stourbridge', + 'construction-age-band': 'England and Wales: 1950-1966', 'lodgement-datetime': '2022-10-10 16:41:36', + 'tenure': 'Rented (social)', 'fixed-lighting-outlets-count': 11.0, 'low-energy-fixed-light-count': None, + 'uprn': 90041166, 'uprn-source': 'Energy Assessor', 'sheating-energy-eff': None, 'sheating-env-eff': None + }, + "heating_recommendation_descriptions": [ + 'Install an air source heat pump, and upgrade heating controls to Smart Thermostats, room sensors and ' + 'smart radiator valves (time & temperature zone control). The cost includes the £7500 boiler upgrade ' + 'scheme grant', + 'Install high heat retention electric storage heaters alongside the boiler. The current electric heaters ' + 'may be retrofit with high heat retention storage controls however this is dependent on the existing ' + 'system and may not be possible. Upgrade heating controls to High Heat Retention Storage Heater Controls' + ], + "heating_controls_recommendation_descriptions": [], + "notes": "This property has dual heating. A boiler and electric storage heaters. The heating is efficient so" + "we recommend ASHP and HHR" + }, + { + "epc": { + 'lmk-key': '1dd9aa80d6e5bae3e0e4892d9ed1a83b53f3af848568f4a928c9f7a63d8825ea', + 'address1': '49 Ridgeway Road', 'address2': 'Wordsley', 'address3': None, 'postcode': 'DY8 5UD', + 'building-reference-number': 10003464876, 'current-energy-rating': 'F', 'potential-energy-rating': 'D', + 'current-energy-efficiency': 35, 'potential-energy-efficiency': 64, 'property-type': 'House', + 'built-form': 'Semi-Detached', 'inspection-date': '2021-11-17', 'local-authority': 'E08000027', + 'constituency': 'E14000672', 'county': None, 'lodgement-date': '2022-10-10', 'transaction-type': 'rental', + 'environment-impact-current': 41, 'environment-impact-potential': 67, 'energy-consumption-current': 401, + 'energy-consumption-potential': 207, 'co2-emissions-current': 6.1, 'co2-emiss-curr-per-floor-area': 69, + 'co2-emissions-potential': 3.2, 'lighting-cost-current': 61, 'lighting-cost-potential': 61, + 'heating-cost-current': 1488, 'heating-cost-potential': 1015, 'hot-water-cost-current': 114, + 'hot-water-cost-potential': 77, 'total-floor-area': 89.0, 'energy-tariff': 'Single', 'mains-gas-flag': 'Y', + 'floor-level': None, 'flat-top-storey': None, 'flat-storey-count': None, 'main-heating-controls': None, + 'multi-glaze-proportion': 100.0, 'glazed-type': 'double glazing installed during or after 2002', + 'glazed-area': 'Normal', 'extension-count': 2, 'number-habitable-rooms': 5, 'number-heated-rooms': 5, + 'low-energy-lighting': 91, 'number-open-fireplaces': 0, 'hotwater-description': 'From main system', + 'hot-water-energy-eff': 'Good', 'hot-water-env-eff': 'Good', + 'floor-description': 'Solid, no insulation (assumed)', 'floor-energy-eff': None, + 'windows-description': 'Fully double glazed', 'windows-energy-eff': 'Good', 'windows-env-eff': 'Good', + 'walls-description': 'Cavity wall, as built, no insulation (assumed)', 'walls-energy-eff': 'Poor', + 'walls-env-eff': 'Poor', 'secondheat-description': 'Room heaters, electric', + 'roof-description': 'Pitched, 200 mm loft insulation', 'roof-energy-eff': 'Good', 'roof-env-eff': 'Good', + 'mainheat-description': 'Boiler and radiators, mains gas, Electric storage heaters', + 'mainheat-energy-eff': 'Average', 'mainheat-env-eff': 'Good', + 'mainheatcont-description': 'Programmer, room thermostat and TRVs', 'mainheatc-energy-eff': 'Good', + 'mainheatc-env-eff': 'Good', 'lighting-description': 'Low energy lighting in 91% of fixed outlets', + 'lighting-energy-eff': 'Very Good', 'lighting-env-eff': 'Very Good', + 'main-fuel': 'mains gas (not community)', 'wind-turbine-count': 0, 'heat-loss-corridor': None, + 'unheated-corridor-length': None, 'floor-height': 2.53, 'photo-supply': 0.0, + 'solar-water-heating-flag': 'N', + 'mechanical-ventilation': 'natural', 'address': '49 Ridgeway Road, Wordsley', + 'local-authority-label': 'Dudley', 'constituency-label': 'Dudley South', 'posttown': 'Stourbridge', + 'construction-age-band': 'England and Wales: 1950-1966', 'lodgement-datetime': '2022-10-10 16:41:36', + 'tenure': 'Rented (social)', 'fixed-lighting-outlets-count': 11.0, 'low-energy-fixed-light-count': None, + 'uprn': 90041166, 'uprn-source': 'Energy Assessor', 'sheating-energy-eff': None, 'sheating-env-eff': None + }, + "heating_recommendation_descriptions": [ + ], + "heating_controls_recommendation_descriptions": [], + "notes": "This property is a modified version of the previous dual heating property, where we lower the" + "starting heating efficiency so that we a combined heating upgrade to both the boiler and the electric" + "storage heaters" } ] @@ -1125,6 +1218,8 @@ completed_descriptions = [ "Boiler and radiators, smokeless fuel", "Boiler and radiators, wood pellets", "Room heaters, dual fuel (mineral and wood)", + "Air source heat pump, radiators, electric", + "Portable electric heaters assumed for most rooms, Room heaters, electric", ] portfolio = pd.read_excel( @@ -1139,7 +1234,7 @@ portfolio["lodgement-datetime"] = portfolio["lodgement-datetime"].astype(str) print(portfolio["mainheat-description"].value_counts()) eg = portfolio[ - (portfolio["mainheat-description"] == "Air source heat pump, radiators, electric") + (portfolio["mainheat-description"] == "Boiler and radiators, mains gas, Electric storage heaters") ].sample(1) eg = eg.squeeze().to_dict() print(eg) diff --git a/recommendations/tests/test_heating_recommendations.py b/recommendations/tests/test_heating_recommendations.py index 35373729..b80780d9 100644 --- a/recommendations/tests/test_heating_recommendations.py +++ b/recommendations/tests/test_heating_recommendations.py @@ -53,6 +53,8 @@ class TestHeatingRecommendations: we retrieve alongside them :return: """ + if test_case["epc"]["uprn"] == 90041166: + raise Exception("Finish the second test case with this uprn") epc_records = {"original_epc": test_case["epc"].copy(), "full_sap_epc": {}, "old_data": []}