From 8a09a29956f2294d9d25855a378086ca682f9795 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Wed, 18 Sep 2024 18:53:04 +0100 Subject: [PATCH] adding new installer costs for solar pv to cost class --- backend/app/plan/schemas.py | 15 ++++- recommendations/Costs.py | 77 +++++++++++++++++++---- recommendations/Recommendations.py | 27 ++++---- recommendations/SolarPvRecommendations.py | 3 +- 4 files changed, 96 insertions(+), 26 deletions(-) diff --git a/backend/app/plan/schemas.py b/backend/app/plan/schemas.py index 2968babf..68f8bbf5 100644 --- a/backend/app/plan/schemas.py +++ b/backend/app/plan/schemas.py @@ -20,7 +20,7 @@ SPECIFIC_MEASURES = [ # Walls "internal_wall_insulation", "external_wall_insulation", - "cavity_wall_insulation" + "cavity_wall_insulation", # Roof "loft_insulation", "flat_roof_insulation", @@ -32,7 +32,20 @@ SPECIFIC_MEASURES = [ "boiler_upgrade", "high_heat_retention_storage_heater", "air_source_heat_pump", + "secondary_heating", + # Solar + "solar_pv", + # Windows Glazing + "windows", + # Mechanical ventilation + "ventilation", + # Other + "low_energy_lighting", + "fireplace", + "hot_water", +] +NON_INVASIVE_SPECIFIC_MEASURES = [ # Specific measures that will typically come from an energy assessment "trickle_vents", "draught_proofing", diff --git a/recommendations/Costs.py b/recommendations/Costs.py index 908a409a..71d20855 100644 --- a/recommendations/Costs.py +++ b/recommendations/Costs.py @@ -37,6 +37,30 @@ MCS_SOLAR_PV_COST_DATA = { "average_cost_per_kwh-Northern Ireland": 1347, } +INSTALLER_SOLAR_COSTS = [ + {'n_panels': 4, 'array_kwp': 1.6, 'cost': 3040.00, 'installer': 'CEG'}, + {'n_panels': 5, 'array_kwp': 2.1, 'cost': 3201.00, 'installer': 'CEG'}, + {'n_panels': 6, 'array_kwp': 2.5, 'cost': 3363.00, 'installer': 'CEG'}, + {'n_panels': 7, 'array_kwp': 2.9, 'cost': 3524.00, 'installer': 'CEG'}, + {'n_panels': 8, 'array_kwp': 3.3, 'cost': 3686.00, 'installer': 'CEG'}, + {'n_panels': 9, 'array_kwp': 3.7, 'cost': 3847.00, 'installer': 'CEG'}, + {'n_panels': 10, 'array_kwp': 4.1, 'cost': 4009.00, 'installer': 'CEG'}, + {'n_panels': 11, 'array_kwp': 4.5, 'cost': 4170.00, 'installer': 'CEG'}, + {'n_panels': 12, 'array_kwp': 4.9, 'cost': 4332.00, 'installer': 'CEG'}, + {'n_panels': 13, 'array_kwp': 5.3, 'cost': 4835.00, 'installer': 'CEG'}, + {'n_panels': 14, 'array_kwp': 5.7, 'cost': 5015.00, 'installer': 'CEG'}, + {'n_panels': 15, 'array_kwp': 6.2, 'cost': 5176.00, 'installer': 'CEG'}, + {'n_panels': 16, 'array_kwp': 6.6, 'cost': 5338.00, 'installer': 'CEG'}, + {'n_panels': 17, 'array_kwp': 7.0, 'cost': 5500.00, 'installer': 'CEG'}, + {'n_panels': 18, 'array_kwp': 7.4, 'cost': 6021.00, 'installer': 'CEG'} +] + +INSTALLER_SCAFFOLDING_COSTS = [ + {'stories': 1, 'description': '1 Story Scaffold', 'cost': 531.00, 'installer': 'CEG'}, + {'stories': 2, 'description': '2 Story Scaffold', 'cost': 841.00, 'installer': 'CEG'}, + {'stories': 3, 'description': '3 Story Scaffold', 'cost': 1077.00, 'installer': 'CEG'} +] + # This data is based on the MCS database, We use the larger figure between the 2023 and 2024 average, # to be conservative MCS_AIR_SOURCE_HEAT_PUMP_COST_DATA = { @@ -54,10 +78,27 @@ MCS_AIR_SOURCE_HEAT_PUMP_COST_DATA = { "Scotland": 12586, "Northern Ireland": 12000, # There are hardly any air source heat pump installs going on in Northern Ireland } + +INSTALLER_ASHP_COSTS = [ + {'capacity_kw': 5.0, 'brand': 'Mitsubishi', 'tank_size_liters': 150, 'cost': 10149.53, 'installer': 'CEG'}, + {'capacity_kw': 6.0, 'brand': 'Mitsubishi', 'tank_size_liters': 170, 'cost': 10823.48, 'installer': 'CEG'}, + {'capacity_kw': 8.5, 'brand': 'Mitsubishi', 'tank_size_liters': 200, 'cost': 11312.43, 'installer': 'CEG'}, + {'capacity_kw': 11.2, 'brand': 'Mitsubishi', 'tank_size_liters': 250, 'cost': 12156.75, 'installer': 'CEG'}, + {'capacity_kw': 14.0, 'brand': 'Mitsubishi', 'tank_size_liters': 300, 'cost': 14405.54, 'installer': 'CEG'}, + {'capacity_kw': 14.0, 'brand': 'Mitsubishi', 'tank_size_liters': 300, 'cost': 14405.54, 'installer': 'CEG'}, + {'capacity_kw': 17.0, 'brand': 'Grant', 'tank_size_liters': 300, 'cost': 14445.00, 'installer': 'CEG'}, + {'capacity_kw': 20.0, 'brand': 'Ecoforest', 'tank_size_liters': 400, 'cost': 21189.41, 'installer': 'CEG'}, + {'capacity_kw': None, 'brand': '2 x cascaded ASHPs', 'tank_size_liters': 500, 'cost': 22950.00, 'installer': 'CEG'} +] + BOILER_UPGRADE_SCHEME_ASHP_VALUE = 7500 -# This is based on quotes from installers -BATTERY_COST = 3500 +INSTALLER_SOLAR_BATTERY_COSTS = [ + {'capacity_kwh': 5, 'description': 'Battery Add on', 'cost': 2700.00, 'installer': 'CEG'}, + {'capacity_kwh': 10, 'description': 'Battery Add on', 'cost': 4300.00, 'installer': 'CEG'}, + {'capacity_kwh': 5, 'description': 'Battery Retrofit existing system', 'cost': 4250.00, 'installer': 'CEG'}, + {'capacity_kwh': 10, 'description': 'Battery Retrofit Existing system', 'cost': 5950.00, 'installer': 'CEG'} +] # This is based on https://www.checkatrade.com/blog/cost-guides/cost-smart-thermostat/ SMART_APPLIANCE_THERMOSTAT_COST = 400 @@ -1013,7 +1054,14 @@ class Costs: "labour_days": labour_days } - def solar_pv(self, wattage: float, has_battery: bool = False, array_cost=None): + def solar_pv( + self, wattage: float, + n_panels: int | float, + has_battery: bool = False, + array_cost=None, + n_floors: int = 1, + battery_kwh: int = 5, + ): """ Calculates the total cost for solar PV based data provided by the MCS dashboard, which contains @@ -1025,23 +1073,26 @@ class Costs: Price can also be benchmarked against this checkatrade article: https://www.checkatrade.com/blog/cost-guides/cost-of-solar-panel-installation/ - :param wattage: Peak wattage of the solar PV system] + :param wattage: Peak wattage of the solar PV system + :param n_panels: Number of solar panels :param has_battery: Bool, whether the system includes a battery :param array_cost: float, containing the cost of the solar PV array + :param n_floors: int, number of floors in the property, used to estimate the cost of scaffolding + :param battery_kwh: int, capacity of the battery in kWh. Defaulted to 5 """ - # Get the cost data relevant to the region - regional_cost = MCS_SOLAR_PV_COST_DATA["-".join(["average_cost_per_kwh", self.region])] + system_cost = [c for c in INSTALLER_SOLAR_COSTS if c["n_panels"] == n_panels][0]["cost"] - if array_cost is not None: - total_cost = array_cost - else: - kw = wattage / 1000 - total_cost = kw * regional_cost + total_cost = array_cost if array_cost is not None else system_cost if has_battery: - # The battery cost is based on the £3500 quote, recieved from installers - total_cost += BATTERY_COST + battery_cost = [c for c in INSTALLER_SOLAR_BATTERY_COSTS if c["capacity_kwh"] == battery_kwh][0]["cost"] + total_cost += battery_cost + + scaffolding_cost = [c for c in INSTALLER_SCAFFOLDING_COSTS if c["stories"] == n_floors][0]["cost"] + total_cost += scaffolding_cost + + # We add an additional cost for scaffolding subtotal_before_vat = total_cost / (1 + self.VAT_RATE) diff --git a/recommendations/Recommendations.py b/recommendations/Recommendations.py index fbaf0f9b..5037f450 100644 --- a/recommendations/Recommendations.py +++ b/recommendations/Recommendations.py @@ -18,9 +18,8 @@ from recommendations.DraughtProofingRecommendations import DraughtProofingRecomm from backend.ml_models.AnnualBillSavings import AnnualBillSavings from backend.apis.GoogleSolarApi import GoogleSolarApi import backend.app.assumptions as assumptions -from backend.app.plan.schemas import TYPICAL_MEASURE_TYPES, SPECIFIC_MEASURES, MEASURE_MAP +from backend.app.plan.schemas import SPECIFIC_MEASURES, MEASURE_MAP, NON_INVASIVE_SPECIFIC_MEASURES -ASHP_COP = 3 STARTING_DUMMY_ID_VALUE = -9999 @@ -50,8 +49,11 @@ class Recommendations: self.exclusions = exclusions if exclusions else [] self.inclusions = inclusions if inclusions else [] - self.all_typical_measures = TYPICAL_MEASURE_TYPES self.all_specific_measures = SPECIFIC_MEASURES + self.all_non_invase_measures = NON_INVASIVE_SPECIFIC_MEASURES + self.non_invasive_recommendation_types = [ + r["type"] for r in self.property_instance.non_invasive_recommendations + ] self.floor_recommender = FloorRecommendations(property_instance=property_instance, materials=materials) self.wall_recomender = WallRecommendations(property_instance=property_instance, materials=materials) @@ -82,14 +84,18 @@ class Recommendations: # If inclusions and exclusions are empty, it means that nothing was specified, so we allow # all recommendation types if not inclusions_full and not exclusions_full: - # All typical measures - return self.all_specific_measures + # All typical measures - this does not include non-invasive measures inless they are specified + return self.all_specific_measures + self.non_invasive_recommendation_types if inclusions_full: return inclusions_full if exclusions_full: - return [m for m in self.all_specific_measures if m not in exclusions_full] + measures = [ + m for m in self.all_specific_measures + self.non_invasive_recommendation_types + if m not in exclusions_full + ] + return measures def recommend(self): @@ -146,11 +152,10 @@ class Recommendations: if self.draught_proofing_recommender.recommendation: property_recommendations.append(self.draught_proofing_recommender.recommendation) - if "floor_insulation" in measures: - self.floor_recommender.recommend(phase=phase, measures=measures) - if self.floor_recommender.recommendations: - property_recommendations.append(self.floor_recommender.recommendations) - phase += 1 + self.floor_recommender.recommend(phase=phase, measures=measures) + if self.floor_recommender.recommendations: + property_recommendations.append(self.floor_recommender.recommendations) + phase += 1 if "windows" in measures and "mixed_glazing" not in non_invasive_recommendation_types: # If we have a mixed glazing recommendation, we prioritise this over the windows recommendation diff --git a/recommendations/SolarPvRecommendations.py b/recommendations/SolarPvRecommendations.py index d0d555c9..bbaffdda 100644 --- a/recommendations/SolarPvRecommendations.py +++ b/recommendations/SolarPvRecommendations.py @@ -196,7 +196,8 @@ class SolarPvRecommendations: cost_result = self.costs.solar_pv( wattage=recommendation_config["array_wattage"], has_battery=has_battery, - array_cost=non_invasive_recommendation.get("cost", None) + array_cost=non_invasive_recommendation.get("cost", None), + n_panels=recommendation_config["n_panels"], ) kw = np.floor(recommendation_config["array_wattage"] / 100) / 10 if has_battery: