import numpy as np import pandas as pd from recommendations.Costs import Costs from recommendations.recommendation_utils import override_costs, esimtate_pitched_roof_area class SolarPvRecommendations: # Solar panel specs based on Eurener 400s solar panels # https://midsummerwholesale.co.uk/buy/eurener/eurener-400w-mepv-zebra-ab-half-cut-mono # Approximate area of the solar panels SOLAR_PANEL_AREA = 1.79 # Wattage per panel - this is based on the average wattage of a solar panel being between 250w and 420w # This was previously set to 250w, but has been upped to 400 based on the systems used by Cotswolrd Energy Group SOLAR_PANEL_WATTAGE = 400 MAX_SYSTEM_WATTAGE = 6000 MIN_SYSTEM_WATTAGE = 1000 def __init__(self, property_instance): """ :param property_instance: Instance of the Property class, for the home associated to property_id """ self.property = property_instance self.costs = Costs(self.property) self.recommendation = [] @staticmethod def trim_solar_wattage_options(scenarios_with_wattage): # Initialize the list with the first element, assuming the list is not empty trimmed_list = [scenarios_with_wattage[0]] # Iterate over the list starting from the second element for scenario in scenarios_with_wattage[1:]: # Compare the second element (index 1) of the current tuple with the last tuple in the trimmed list if scenario[1] > trimmed_list[-1][1]: trimmed_list.append(scenario) return trimmed_list def mds_recommend(self, phase=None, solar_pv_percentage=0.5): # For specific usage within the mds report solar_pv_roof_area = self.property.get_solar_pv_roof_area(solar_pv_percentage) number_solar_panels = np.floor(solar_pv_roof_area / self.SOLAR_PANEL_AREA) solar_panel_wattage = number_solar_panels * self.SOLAR_PANEL_WATTAGE solar_panel_wattage = np.clip( a=solar_panel_wattage, a_min=self.MIN_SYSTEM_WATTAGE, a_max=self.MAX_SYSTEM_WATTAGE ) # We now have a property which is potentially suitable for solar PV roof_coverage_percent = round(solar_pv_percentage * 100) # Given the wattage, we estimate the cost of the solar PV system. This is based on the MCS database # of solar PV installations cost_result = self.costs.solar_pv(wattage=solar_panel_wattage, has_battery=False) kw = np.floor(solar_panel_wattage / 100) / 10 description = (f"Install a {kw} kilowatt-peak (kWp) solar photovoltaic (PV) p" f"anel system on {round(roof_coverage_percent)}% the roof.") return [ { "phase": phase, "parts": [], "type": "solar_pv", "description": description, "starting_u_value": None, "new_u_value": None, "sap_points": None, "already_installed": False, **cost_result, # This is required for simulating the SAP impact. solar_pv_percentage is between 0 & 1 so we scale # back up here "photo_supply": roof_coverage_percent, "has_battery": False } ] def recommend_building_analysis(self, phase): """ This recommendation approach handles the case of producing solar PV recommendations at the building level, across multiple flats. For these recommendations, we don't include the battery option since it's impractical from a space perspective. :return: """ panel_performance = self.property.solar_panel_configuration["panel_performance"] total_roof_area = ( self.property.solar_panel_configuration["insights_data"]["solarPotential"]["wholeRoofStats"]["areaMeters2"] ) n_units = self.property.solar_panel_configuration["n_units"] # At a building level, we take a single configuration so that all properties a guaranteed to use # the same configuration best_configurations = panel_performance.head(1).reset_index(drop=True) for rank, recommendation_config in best_configurations.iterrows(): # If we dont have the panneled_roof_area in the recommendation_config we calculate it if recommendation_config.get("panneled_roof_area", None): roof_coverage_percent = round(recommendation_config["panneled_roof_area"] / total_roof_area * 100) else: raise Exception("IMPLEMENT ME") # Spread the cost to the individual units - adding a 20% contingency total_cost = recommendation_config["total_cost"] / n_units kw = np.floor(recommendation_config["array_wattage"] / 100) / 10 # Default to a weeks work for a team of 3 people doing 8 hour days labour_days = 5 labour_hours = 3 * 8 * labour_days description = (f"Install a {kw} kilowatt-peak (kWp) solar photovoltaic (PV) panel system on the roof " "of the building") initial_ac_kwh_per_year = recommendation_config["initial_ac_kwh_per_year"] self.recommendation.append( { "phase": phase, "parts": [], "type": "solar_pv", "description": description, "starting_u_value": None, "new_u_value": None, "sap_points": None, "already_installed": False, "total": total_cost, "labour_days": labour_days, "labour_hours": labour_hours, # This is required for simulating the SAP impact. solar_pv_percentage is between 0 & 1 so we scale # back up here "photo_supply": roof_coverage_percent, "has_battery": False, "initial_ac_kwh_per_year": initial_ac_kwh_per_year, "description_simulation": {"photo-supply": roof_coverage_percent}, "rank": rank # Rank is used to get the representative recommendation - rank 0 will be chosen } ) def recommend(self, phase): """ We check if a property is potentially suitable for solar PV based on the following criteria: - The property is a house or bungalow - The property has a flat or pitched roof - The property does not have existing solar pv :return: """ if not self.property.is_solar_pv_valid(): return # If we have a buiilding level analysis, we implement separate logic if self.property.building_id is not None: self.recommend_building_analysis(phase) return non_invasive_recommendation = next( (r for r in self.property.non_invasive_recommendations if r["type"] == "solar_pv"), {"suitable": True} ) # We allow for the non-invasive recommendation to be that solar PV is not suitable if not non_invasive_recommendation["suitable"]: return if non_invasive_recommendation.get("array_wattage") is not None: if self.property.roof["is_flat"]: roof_area = self.property.insulation_floor_area else: roof_area = esimtate_pitched_roof_area( floor_area=self.property.insulation_floor_area, floor_height=self.property.data["floor-height"] ) solar_configurations = pd.DataFrame( [ { "array_wattage": non_invasive_recommendation["array_wattage"], "initial_ac_kwh_per_year": non_invasive_recommendation["initial_ac_kwh_per_year"], "panneled_roof_area": non_invasive_recommendation["panneled_roof_area"] } ] ) else: # TODO: There may be some instances where we don't want to use the solar API so we should cover for them panel_performance = self.property.solar_panel_configuration["panel_performance"] roof_area = self.property.roof_area solar_configurations = panel_performance.head(3).reset_index(drop=True) # We combine each of these configurations with estimates with and without a battery for rank, recommendation_config in solar_configurations.iterrows(): roof_coverage_percent = round(recommendation_config["panneled_roof_area"] / roof_area * 100) # We round up to the nearest 10 roof_coverage_percent = np.ceil(roof_coverage_percent / 10) * 10 for has_battery in [False, True]: cost_result = self.costs.solar_pv( wattage=recommendation_config["array_wattage"], has_battery=has_battery, array_cost=non_invasive_recommendation.get("cost", None) ) kw = np.floor(recommendation_config["array_wattage"] / 100) / 10 if has_battery: description = (f"Install a {kw} kilowatt-peak (kWp) solar photovoltaic (PV) panel system on " f"{round(roof_coverage_percent)}% the roof, with a battery storage system.") else: description = (f"Install a {kw} kilowatt-peak (kWp) solar photovoltaic (PV) p" f"anel system on {round(roof_coverage_percent)}% the roof.") already_installed = "solar_pv" in self.property.already_installed if already_installed: cost_result = override_costs(cost_result) self.recommendation.append( { "phase": phase, "parts": [], "type": "solar_pv", "description": description, "starting_u_value": None, "new_u_value": None, "sap_points": None, "already_installed": already_installed, **cost_result, # This is required for simulating the SAP impact. solar_pv_percentage is between 0 & 1 so we # scale # back up here "photo_supply": roof_coverage_percent, "has_battery": has_battery, "initial_ac_kwh_per_year": recommendation_config["initial_ac_kwh_per_year"], "description_simulation": {"photo-supply": roof_coverage_percent}, } )