diff --git a/backend/Property.py b/backend/Property.py index f5325722..0cb295a7 100644 --- a/backend/Property.py +++ b/backend/Property.py @@ -221,11 +221,15 @@ class Property(Definitions): setattr(self, attribute, value) - def get_components(self, cleaned): + def get_components(self, cleaned, photo_supply_lookup, floor_area_decile_thresholds): """ Given the cleaning that has been performed, we'll use this to identify the property components, from roof to walls to windows, heating and hot water :param cleaned: This is the dictionary of components found in cleaner.cleaned + :param photo_supply_lookup: This is the lookup table for the photo supply, used to estimate the percentage + of the roof that is suitable for solar panels + :param floor_area_decile_thresholds: This is the decile thresholds for the floor area, used in estimating the + solar pv roof area :return: """ @@ -295,6 +299,9 @@ class Property(Definitions): self.set_floor_type() self.set_floor_level() self.set_windows_count() + self.set_solar_panel_area( + photo_supply_lookup=photo_supply_lookup, floor_area_decile_thresholds=floor_area_decile_thresholds + ) def set_age_band(self): """ @@ -849,7 +856,9 @@ class Property(Definitions): return i # Returns the decile index (0 to 9) return len(thresholds) - floor_area_decile = classify_floor_area(self.floor_area, floor_area_decile_thresholds) + floor_area_decile = classify_floor_area( + self.floor_area, floor_area_decile_thresholds["floor_area_decile_thresholds"].values + ) # Given the photo_supply_lookup, we esimate the percentage of the roof that is suitable for solar panels diff --git a/backend/app/plan/router.py b/backend/app/plan/router.py index 89347be2..77ee9869 100644 --- a/backend/app/plan/router.py +++ b/backend/app/plan/router.py @@ -67,6 +67,12 @@ async def trigger_plan(body: PlanTriggerRequest): cleaning_data = read_parquet_from_s3( bucket_name=get_settings().DATA_BUCKET, file_key="sap_change_model/cleaning_dataset.parquet", ) + photo_supply_lookup = read_parquet_from_s3( + bucket_name=get_settings().DATA_BUCKET, file_key="solar_pv_supply/photo_supply_lookup.parquet", + ) + floor_area_decile_thresholds = read_parquet_from_s3( + bucket_name=get_settings().DATA_BUCKET, file_key="solar_pv_supply/floor_area_decile_thresholds.parquet", + ) input_properties = [] for config in plan_input: @@ -129,7 +135,7 @@ async def trigger_plan(body: PlanTriggerRequest): for p in input_properties: # Property recommendations - p.get_components(cleaned) + p.get_components(cleaned, photo_supply_lookup, floor_area_decile_thresholds) # This is temp - this should happen after scoring cleaned_property_data = DataProcessor.apply_averages_cleaning( diff --git a/etl/testing_data/solar_research.py b/etl/testing_data/solar_research.py index 4e60fa7a..8ce8a6ac 100644 --- a/etl/testing_data/solar_research.py +++ b/etl/testing_data/solar_research.py @@ -101,7 +101,7 @@ def app(): save_dataframe_to_s3_parquet( df=aggregated, bucket_name="retrofit-data-dev", - file_key=f"solar_pv_supply/photo_supply_lookup.parquet", + file_key="solar_pv_supply/photo_supply_lookup.parquet", ) floor_area_decile_thresholds = pd.DataFrame(decile_thresholds, columns=["floor_area_decile_thresholds"]) diff --git a/recommendations/Costs.py b/recommendations/Costs.py index cc993143..654dd7a8 100644 --- a/recommendations/Costs.py +++ b/recommendations/Costs.py @@ -831,5 +831,35 @@ class Costs: "labour_days": labour_days } - def solar_pv(self, wattage): - pass + def solar_pv(self, wattage: float): + + """ + Calculates the total cost for solar PV based data provided by the MCS dashboard, which contains + costing data for installations of renewable and clean energy measures. + + The data in the dashboard is filtered on domestic building installations and then the data across the + various regions is manually collected. There is currently no automated way to get the data from the MCS + dashboard + :param wattage: + :return: + """ + + # Get the cost data relevant to the region + regional_cost = MCS_SOLAR_PV_COST_DATA["-".join(["average_cost_per_kwh", self.region])] + + kw = wattage / 1000 + total_cost = kw * regional_cost + + subtotal_before_vat = total_cost / (1 + self.VAT_RATE) + vat = total_cost - subtotal_before_vat + + # Labour hours are based on estimates from online research but an average team seems to consist of 3 people + # and most jobs take around 2 days. Assuming an 8 hour day for 3 people across 2 days, gives us 72 hours of + # labour + return { + "total": total_cost, + "subtotal": subtotal_before_vat, + "vat": vat, + "labour_hours": 72, + "labour_days": 2, + } diff --git a/recommendations/SolarPvRecommendations.py b/recommendations/SolarPvRecommendations.py index e8b76988..ebe774bf 100644 --- a/recommendations/SolarPvRecommendations.py +++ b/recommendations/SolarPvRecommendations.py @@ -16,7 +16,7 @@ class SolarPvRecommendations: self.property = property_instance self.costs = Costs(self.property) - self.recommendations = [] + self.recommendation = [] def recommend(self): """ @@ -35,12 +35,27 @@ class SolarPvRecommendations: None, 0, self.property.DATA_ANOMALY_MATCHES ] - if not is_valid_property_type or not is_valid_roof_type or has_no_existing_solar_pv: + if not is_valid_property_type or not is_valid_roof_type or not has_no_existing_solar_pv: return # We now have a property which is potentially suitable for solar PV number_solar_panels = np.floor(self.property.solar_pv_roof_area / self.SOLAR_PANEL_AREA) - solar_panel_capacity = number_solar_panels * self.SOLAR_PANEL_WATTAGE + solar_panel_wattage = number_solar_panels * self.SOLAR_PANEL_WATTAGE # 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) + + kw = int(np.round(solar_panel_wattage / 1000)) + + self.recommendation = [ + { + "parts": [], + "type": "solar_pv", + "description": f"Install a {kw} kilowatt-peak (kWp) solar photovoltaic (PV) panel system on the roof", + "starting_u_value": None, + "new_u_value": None, + "sap_points": None, + **cost_result, + } + ]