diff --git a/backend/apis/GoogleSolarApi.py b/backend/apis/GoogleSolarApi.py index a99d96c9..f1e3d2e9 100644 --- a/backend/apis/GoogleSolarApi.py +++ b/backend/apis/GoogleSolarApi.py @@ -42,6 +42,9 @@ class GoogleSolarApi: # your area installation_life_span = 20 + MIN_UNIT_PANELS = 4 # Minimum number of panels we allow for a domestic building + MIN_BUILDING_PANELS = 10 # Minimum number of panels we allow for a block of flats + def __init__(self, api_key, max_retries=5): """ Initialize the GoogleSolarApi class with the provided API key and maximum retries. @@ -250,6 +253,9 @@ class GoogleSolarApi: Optimise the solar panel configuration for the building. :return: """ + # If we look at the building level, we don't include any projects fewer than 10 panels, otherwise the + # minimum is 4 + min_panels = self.MIN_BUILDING_PANELS if is_building else self.MIN_UNIT_PANELS cost_instance = Costs(property_instance=property_instance) if property_instance is not None else None @@ -264,6 +270,10 @@ class GoogleSolarApi: roi_summary = [] for segment in roof_segment_summaries: + + if segment["panelsCount"] < min_panels: + continue + wattage = segment["panelsCount"] * self.insights_data["solarPotential"]["panelCapacityWatts"] generated_dc_energy = segment["yearlyEnergyDcKwh"] ratio = generated_dc_energy / wattage @@ -272,7 +282,9 @@ class GoogleSolarApi: cost = MCS_SOLAR_PV_COST_DATA["average_cost_per_kwh"] * (wattage / 1000) else: cost = cost_instance.solar_pv( - wattage=wattage, has_battery=False + n_panels=segment["panelsCount"], + has_battery=False, + n_floors=property_instance.number_of_floors, )["total"] roi_summary.append( @@ -330,10 +342,6 @@ class GoogleSolarApi: # We can have duplicate configurations panel_performance = panel_performance.drop_duplicates() - # If we look at the building level, we don't include any projects fewer than 10 panels, otherwise the - # minimum is 4 - min_panels = 10 if is_building else 4 - panel_performance = panel_performance[panel_performance["n_panels"] >= min_panels] if panel_performance.empty: self.panel_performance = pd.DataFrame( diff --git a/recommendations/Costs.py b/recommendations/Costs.py index 671c4db7..c71316ad 100644 --- a/recommendations/Costs.py +++ b/recommendations/Costs.py @@ -54,6 +54,8 @@ INSTALLER_SOLAR_COSTS = [ {'n_panels': 17, 'array_kwp': 7.0, 'cost': 5500.00, 'installer': 'CEG'}, {'n_panels': 18, 'array_kwp': 7.4, 'cost': 6021.00, 'installer': 'CEG'} ] +# This is the maximum number of panels that we have a cost from the installers for +INSTALLER_MAX_PANELS = 18 # CEG uses use Solshare as an inverter to provide solar PV to multiple flats. This costs £7500 for the inverter alone # https://midsummerwholesale.co.uk/buy/solshare @@ -362,7 +364,7 @@ class Costs: "labour_days": labour_days } - def internal_wall_insulation(self, wall_area, material, non_insulation_materials): + def internal_wall_insulation(self, wall_area, material): """ Broadly speaking, the high level steps to an internal wall insulation job are the following: @@ -401,74 +403,25 @@ class Costs: "labour_days": labour_days, } - # Extract and check the different types of data we'll need - demolition_data = [x for x in non_insulation_materials if x["type"] == "iwi_wall_demolition"] - vapour_barrier_data = [x for x in non_insulation_materials if x["type"] == "iwi_vapour_barrier"] - redecoration_data = [x for x in non_insulation_materials if x["type"] == "iwi_redecoration"] - if not demolition_data: - raise ValueError("No data found for iwi_wall_demolition") - - if (len(vapour_barrier_data) != 1) or (len(redecoration_data) != 3): - raise ValueError("Incorrect number of data entries for non-insulation materials") - # Break out the individual material costs # Since we don't know the exact wall construction, we take an average for demolition costs, since # the cost will depend on the type of wall construction - demolition_material_costs = np.mean([x["material_cost"] * wall_area for x in demolition_data]) - insulation_material_costs = material["material_cost"] * wall_area - vapour_barrier_material_costs = vapour_barrier_data[0]["material_cost"] * wall_area - redecoration_material_costs = sum([x["material_cost"] * wall_area for x in redecoration_data]) - demolition_plant_costs = np.mean([x["plant_cost"] * wall_area for x in demolition_data]) - - # Again for demolition, we average since we aren't sure which demolition process will be used - demolition_labour_costs = np.mean([x["labour_cost"] * wall_area for x in demolition_data]) - insulation_labour_costs = material["labour_cost"] * wall_area - vapour_barrier_labour_costs = vapour_barrier_data[0]["labour_cost"] * wall_area - redecoration_labour_costs = sum([x["labour_cost"] * wall_area for x in redecoration_data]) - - labour_costs = (demolition_labour_costs + insulation_labour_costs + vapour_barrier_labour_costs + - redecoration_labour_costs) - - labour_costs = labour_costs * self.labour_adjustment_factor - - materials_costs = (demolition_material_costs + insulation_material_costs + vapour_barrier_material_costs + - redecoration_material_costs) - - subtotal_before_profit = labour_costs + materials_costs + demolition_plant_costs - - contingency_cost = subtotal_before_profit * self.IWI_CONTINGENCY - preliminaries_cost = subtotal_before_profit * self.PRELIMINARIES - profit_cost = subtotal_before_profit * self.PROFIT_MARGIN - - subtotal_before_vat = subtotal_before_profit + contingency_cost + preliminaries_cost + profit_cost - - vat_cost = subtotal_before_vat * self.VAT_RATE - - total_cost = subtotal_before_vat + vat_cost - - demolition_labour_hours = np.mean([x["labour_hours_per_unit"] * wall_area for x in demolition_data]) - insulation_labour_hours = material["labour_hours_per_unit"] * wall_area - vapour_barrier_labour_hours = vapour_barrier_data[0]["labour_hours_per_unit"] * wall_area - redecoration_labour_hours = sum([x["labour_hours_per_unit"] * wall_area for x in redecoration_data]) - - labour_hours = (demolition_labour_hours + insulation_labour_hours + vapour_barrier_labour_hours + - redecoration_labour_hours) + total_including_vat = material["total_cost"] * wall_area + total_excluding_vat = total_including_vat / (1 + self.VAT_RATE) + vat_cost = total_including_vat - total_excluding_vat + # We estimate 1 weeks worth of work + labour_hours = 160 # To install internal wall insulation, a small to medium size project might be conducted by a team of 3-5 people labour_days = (labour_hours / 8) / 4 return { - "total": total_cost, - "subtotal": subtotal_before_vat, + "total": total_including_vat, + "subtotal": total_excluding_vat, "vat": vat_cost, - "contingency": contingency_cost, - "preliminaries": preliminaries_cost, - "material": materials_costs, - "profit": profit_cost, "labour_hours": labour_hours, "labour_days": labour_days, - "labour_cost": labour_costs } def suspended_floor_insulation(self, insulation_floor_area, material, non_insulation_materials): @@ -1088,7 +1041,15 @@ class Costs: units """ - system_cost = [c for c in INSTALLER_SOLAR_COSTS if c["n_panels"] == n_panels][0]["cost"] + if n_panels > INSTALLER_MAX_PANELS: + base_cost = [c for c in INSTALLER_SOLAR_COSTS if c["n_panels"] == INSTALLER_MAX_PANELS][0]["cost"] + cost_per_panel = [ + c for c in INSTALLER_SOLAR_COSTS if c["n_panels"] == (INSTALLER_MAX_PANELS - 1) + ][0]["cost"] + cost_per_panel = base_cost - cost_per_panel + system_cost = base_cost + (n_panels - INSTALLER_MAX_PANELS) * cost_per_panel + else: + system_cost = [c for c in INSTALLER_SOLAR_COSTS if c["n_panels"] == n_panels][0]["cost"] total_cost = array_cost if array_cost is not None else system_cost diff --git a/recommendations/WallRecommendations.py b/recommendations/WallRecommendations.py index a0c71860..0ddd7b0b 100644 --- a/recommendations/WallRecommendations.py +++ b/recommendations/WallRecommendations.py @@ -106,23 +106,10 @@ class WallRecommendations(Definitions): part for part in materials if part["type"] == "internal_wall_insulation" ] - self.internal_wall_non_insulation_materials = [ - part - for part in materials - if part["type"] - in ["iwi_wall_demolition", "iwi_vapour_barrier", "iwi_redecoration"] - ] - self.external_wall_insulation_materials = [ part for part in materials if part["type"] == "external_wall_insulation" ] - self.external_wall_non_insulation_materials = [ - part - for part in materials - if part["type"] in ["ewi_wall_demolition", "ewi_wall_preparation", "ewi_wall_redecoration"] - ] - def ewi_valid(self): """ This method check available data, to determine if a property is suitable for external wall insulation @@ -508,7 +495,6 @@ class WallRecommendations(Definitions): cost_result = self.costs.internal_wall_insulation( wall_area=self.property.insulation_wall_area, material=material.to_dict(), - non_insulation_materials=non_insulation_materials, ) already_installed = ( "internal_wall_insulation" @@ -617,7 +603,6 @@ class WallRecommendations(Definitions): iwi_recommendations = self._find_insulation( u_value=u_value, insulation_materials=pd.DataFrame(self.internal_wall_insulation_materials), - non_insulation_materials=self.internal_wall_non_insulation_materials, phase=phase, )