diff --git a/recommendations/Costs.py b/recommendations/Costs.py index 47844657..e5ceb0c0 100644 --- a/recommendations/Costs.py +++ b/recommendations/Costs.py @@ -59,6 +59,26 @@ TTZC_ROOM_TEMPERATURE_SENSOR_LABOUR_HOURS = 0.17 # (Assume ~ 10 mins install pe TTZC_SMART_RADIATOR_VALUES = 50 TTZC_SMART_RADIATOR_VALUES_LABOUR_HOURS = 0.37 # (Assume ~ 15-30 mins install per valve) +# Low carbon combi boiler - median value based on £2200 - £3000 range +LOW_CARBON_COMBI_BOILER = 2200 + +# boiler prices based on +# https://www.greenmatch.co.uk/boilers/30kw-boiler +# https://www.greenmatch.co.uk/boilers/35kw-boiler +# https://www.greenmatch.co.uk/boilers/40kw-boiler +# These are exclusive of installation costs +COMBI_BOILER_COSTS = { + "30kw": 1550, + "35kw": 1610, + "40kw": 1625 +} + +CONVENTIONAL_BOILER_COSTS = { + "30kw": 1117, + "35kw": 1546, + "40kw": 1776 +} + class Costs: """ @@ -1079,3 +1099,34 @@ class Costs: "labour_hours": labour_hours, "labour_days": labour_days, } + + def low_carbon_boiler(self, is_combi, size): + """ + Based on a basic estimate of median value £2600 to install a low carbon combi boiler + :return: + """ + + unit_cost = COMBI_BOILER_COSTS[size] if is_combi else CONVENTIONAL_BOILER_COSTS[size] + # The unit cost is the cost without VAT + # We now need to estimate the cost of the works + labour_days = 2 + labour_rate = 500 + + # Average cost of installation is 1 (maybe 2days) at £300 per day + # https://www.checkatrade.com/blog/cost-guides/new-boiler-cost/ + # To be pessimistic, assume 2 days work and £500 day rate + labour_cost = labour_rate * self.labour_adjustment_factor * labour_days + # Add contingency and preliminaries + labour_cost = labour_cost * (1 + self.CONTINGENCY + self.PRELIMINARIES) + vat = labour_cost * self.VAT_RATE + + subtotal_before_vat = unit_cost + labour_cost + total_cost = subtotal_before_vat + vat + + return { + "total": total_cost, + "subtotal": subtotal_before_vat, + "vat": vat, + "labour_hours": labour_days * 8, + "labour_days": labour_days, + } diff --git a/recommendations/HeatingControlRecommender.py b/recommendations/HeatingControlRecommender.py index 99b41469..547ea497 100644 --- a/recommendations/HeatingControlRecommender.py +++ b/recommendations/HeatingControlRecommender.py @@ -189,7 +189,7 @@ class HeatingControlRecommender: # 2) The current heating controls are not already at 'Very Good' or above if ( - (self.property["thermostatic_control"] == "time and temperature zone control") or + (self.property.main_heating_controls["thermostatic_control"] == "time and temperature zone control") or (self.property.data["mainheatc-energy-eff"] in ["Very Good"]) ): # No recommendation needed @@ -209,7 +209,9 @@ class HeatingControlRecommender: self.recommendation.append( { "description": "upgrade heating controls to Room thermostat, programmer and TRVs", - **self.costs.time_and_temperature_zone_control(), + **self.costs.time_and_temperature_zone_control( + number_heated_rooms=int(self.property.data["number-heated-rooms"]) + ), "simulation_config": simulation_config } ) diff --git a/recommendations/HeatingRecommender.py b/recommendations/HeatingRecommender.py index 6467bd2f..c7064274 100644 --- a/recommendations/HeatingRecommender.py +++ b/recommendations/HeatingRecommender.py @@ -28,7 +28,7 @@ class HeatingRecommender: # if the property has mains heating with boiler and radiators, we recommend optimal heating controls if self.property.main_heating["clean_description"] in ["Boiler and radiators, mains gas"]: - self.recommend_roomstat_programmer_trvs(phase=phase) + self.recommend_boiler_upgrades(phase=phase) return @staticmethod @@ -188,14 +188,89 @@ class HeatingRecommender: self.recommendations.extend(recommendations) - def recommend_roomstat_programmer_trvs(self, phase): - """ + @staticmethod + def estimate_boiler_size(property_type, built_form, floor_area, floor_height, num_heated_rooms): + # Step 1: Base size estimation based on property type (as a starting point) + base_size = { + 'Flat': 25, + 'House': 30, + 'Maisonette': 28, + 'Bungalow': 27 + } + # Step 2: Calculate the volume of the property + volume = floor_area * floor_height + + # Step 3: Adjust base size for built form (to account for heat retention) + form_adjustment = { + 'Mid-Terrace': 0, + 'End-Terrace': 2, + 'Semi-Detached': 4, + 'Detached': 6 + } + + # Step 4: Further adjust for the total volume and number of heated rooms + volume_adjustment = (volume / 100) # Simplified adjustment factor for volume + rooms_adjustment = (num_heated_rooms - 5) * 0.5 # Assuming base case of 5 rooms + + # Calculate the estimated boiler size + estimated_size = base_size[property_type] + form_adjustment[built_form] + volume_adjustment + rooms_adjustment + + # Step 5: Align with available boiler sizes and ensure it does not exceed 35kW, as it's rare to need more + available_sizes = [30, 35, 40, 45, 50] + estimated_size = min(max(estimated_size, 30), 40) # Ensure within 30kW to 35kW range + + # Find the closest available size (in this case, either rounding up or down to align with 30 or 35) + closest_size = min(available_sizes, key=lambda x: abs(x - estimated_size)) + + return closest_size + + def recommend_boiler_upgrades(self, phase): + """ + This boiler recommendation will only recommend a like-for-like upgrade, since changing the system + is generally more expensive :param phase: :return: """ + + # We now recommend boiler upgrades, if applicable + if self.property.data["mainheat-energy-eff"] in ["Very Poor", "Poor", "Average"]: + boiler_size = self.estimate_boiler_size( + property_type=self.property.data["property-type"], + built_form=self.property.data["built-form"], + floor_area=self.property.floor_area, + floor_height=self.property.floor_height, + num_heated_rooms=self.property.data["number-heated-rooms"], + ) + + # If heating and hot water come from the mains, we need a combi boiler, otherwise we need a regular boiler + is_combi = self.property.hotwater["clean_description"] in ["From main system"] + if is_combi: + description = "Upgrade to a low carbon combi boiler" + else: + description = "Upgrade to a low carbon boiler" + + self.recommendations.append( + { + "phase": phase, + "parts": [ + # TODO + ], + "type": "heating", + "description": description, + "starting_u_value": None, + "new_u_value": None, + "sap_points": None, + **self.costs.low_carbon_boiler(is_combi=is_combi, size=f"{boiler_size}kw") + } + ) + # We recommend the heating controls controls_recommender = HeatingControlRecommender(self.property) controls_recommender.recommend(heating_description="Boiler and radiators, mains gas") + # We may have 2 recommendations from the heating controls - controls_recommender.recommendation + # The heating controls recommendation is distrinct from the boiler upgrade recommendation + # We insert phase into the recommendations for heating controls + for recommendation in controls_recommender.recommendation: + recommendation["phase"] = phase diff --git a/recommendations/Recommendations.py b/recommendations/Recommendations.py index 944fec7a..d9a0a0fd 100644 --- a/recommendations/Recommendations.py +++ b/recommendations/Recommendations.py @@ -110,6 +110,9 @@ class Recommendations: self.heating_recommender.recommend(phase=phase) if self.heating_recommender.recommendations: property_recommendations.append(self.heating_recommender.recommendations) + # We check if we have distinct heating and heating controls recommendations + # If so, we increment by 2 (one of the heating system, one for the heating controls) + # otherwise we incremenet by 1 phase += 1 # Hot water