diff --git a/backend/app/plan/router.py b/backend/app/plan/router.py index 7a0bba2a..e4fc508c 100644 --- a/backend/app/plan/router.py +++ b/backend/app/plan/router.py @@ -380,6 +380,7 @@ async def trigger_plan(body: PlanTriggerRequest): target_rating=body.goal_value, current_consumption=p.current_adjusted_energy ), + "property_id": p.id } for p in input_properties if p.building_id is not None ] if building_ids: @@ -419,7 +420,13 @@ async def trigger_plan(body: PlanTriggerRequest): # Insert this into the properties that have this building id for p in input_properties: if p.building_id == building_id: - p.set_solar_panel_configuration(solar_panel_configuration[building_id]) + unit_solar_panel_configuration = solar_panel_configuration[building_id].copy() + + unit_solar_panel_configuration["unit_share_of_energy"] = ( + [x for x in building_ids if x["property_id"] == p.id][0]["energy_consumption"] / + energy_consumption + ) + p.set_solar_panel_configuration(unit_solar_panel_configuration) else: # Model the solar potential at the property level diff --git a/backend/ml_models/AnnualBillSavings.py b/backend/ml_models/AnnualBillSavings.py index a0c426bb..e4d9d143 100644 --- a/backend/ml_models/AnnualBillSavings.py +++ b/backend/ml_models/AnnualBillSavings.py @@ -28,8 +28,10 @@ class AnnualBillSavings: # https://www.ofgem.gov.uk/energy-price-cap ELECTRICITY_PRICE_CAP = 0.2236 GAS_PRICE_CAP = 0.0548 - # This is the most recent export payment figure, at 12p per kwh - ELECTRICITY_EXPORT_PAYMENT = 0.12 + # This is the most recent export payment figure, at 9.28p/kWh + # Smart export guarantee rates can be found here: + # https://www.sunsave.energy/solar-panels-advice/exporting-to-the-grid/best-seg-rates + ELECTRICITY_EXPORT_PAYMENT = 0.0928 # This is a weighted mean of the price caps, using the consumption figures above as weights PRICE_FACTOR = 0.09549999999999999 diff --git a/recommendations/Recommendations.py b/recommendations/Recommendations.py index 806e4d23..6e17ef54 100644 --- a/recommendations/Recommendations.py +++ b/recommendations/Recommendations.py @@ -14,6 +14,7 @@ from recommendations.HeatingRecommender import HeatingRecommender from recommendations.HotwaterRecommendations import HotwaterRecommendations from recommendations.SecondaryHeating import SecondaryHeating from backend.ml_models.AnnualBillSavings import AnnualBillSavings +from backend.apis.GoogleSolarApi import GoogleSolarApi class Recommendations: @@ -374,6 +375,8 @@ class Recommendations: # # TODO: We should determine if the home is gas & electricity or just electricity # expected_energy_bill = AnnualBillSavings.calculate_annual_bill(expected_adjusted_energy) + phase_lighting_costs = {} + phase_kwh_figures = {} for recommendations_by_type in property_recommendations: for rec in recommendations_by_type: @@ -422,23 +425,52 @@ class Recommendations: float(property_instance.data["energy-consumption-current"]) - new_heat_demand ) + if rec["type"] == "lighting": + new_heating_cost = property_instance.energy_cost_estimates["adjusted"]["heating"] + new_hot_water_cost = property_instance.energy_cost_estimates["adjusted"]["hot_water"] + new_lighting_cost = min( + new_lighting_cost, property_instance.energy_cost_estimates["adjusted"]["lighting"] + ) + scoring_heating_cost = property_instance.energy_cost_estimates["unadjusted"]["heating"] + scoring_hot_water_cost = property_instance.energy_cost_estimates["unadjusted"]["hot_water"] + scoring_lighting_cost = min( + property_instance.energy_cost_estimates["unadjusted"]["lighting"], + new_lighting_cost_unadjusted + ) + else: + new_heating_cost = min( + new_heating_cost, property_instance.energy_cost_estimates["adjusted"]["heating"] + ) + new_hot_water_cost = min( + new_hot_water_cost, property_instance.energy_cost_estimates["adjusted"]["hot_water"] + ) + new_lighting_cost = property_instance.energy_cost_estimates["adjusted"]["lighting"] + + scoring_heating_cost = min( + property_instance.energy_cost_estimates["unadjusted"]["heating"], + new_heating_cost_unadjusted + ) + scoring_hot_water_cost = min( + property_instance.energy_cost_estimates["unadjusted"]["hot_water"], + new_hot_water_cost_unadjusted + ) + scoring_lighting_cost = property_instance.energy_cost_estimates["unadjusted"]["lighting"] + predicted_heating_cost_reduction = ( - float(property_instance.energy_cost_estimates["adjusted"]["heating"]) - new_heating_cost + property_instance.energy_cost_estimates["adjusted"]["heating"] - new_heating_cost ) predicted_hot_water_cost_reduction = ( - float(property_instance.energy_cost_estimates["adjusted"]["hot_water"]) - new_hot_water_cost - ) - predicted_heating_cost_reduction = ( - 0 if predicted_heating_cost_reduction < 0 else predicted_heating_cost_reduction - ) - predicted_hot_water_cost_reduction = ( - 0 if predicted_hot_water_cost_reduction < 0 else predicted_hot_water_cost_reduction + property_instance.energy_cost_estimates["adjusted"]["hot_water"] - new_hot_water_cost ) - # Only lighting recommendations can have an impact here predicted_lighting_cost_reduction = 0 if rec["type"] != "lighting" else ( - float(property_instance.energy_cost_estimates["adjusted"]["lighting"]) - new_lighting_cost + property_instance.energy_cost_estimates["adjusted"]["lighting"] - new_lighting_cost ) + # We store this value for later + phase_lighting_costs[rec["phase"]] = { + "adjusted": new_lighting_cost, + "unadjusted": scoring_lighting_cost + } # This is the total bill savings for the recommendation if rec["type"] == "solar_pv": @@ -456,17 +488,6 @@ class Recommendations: ) # We now predict the kwh savings using the xgb model - scoring_heating_cost = min( - property_instance.energy_cost_estimates["unadjusted"]["heating"], new_heating_cost_unadjusted - ) - scoring_hot_water_cost = min( - property_instance.energy_cost_estimates["unadjusted"]["hot_water"], - new_hot_water_cost_unadjusted - ) - scoring_lighting_cost = min( - property_instance.energy_cost_estimates["unadjusted"]["lighting"], new_lighting_cost_unadjusted - ) if rec["type"] == "lighting" \ - else property_instance.energy_cost_estimates["unadjusted"]["lighting"] simulation_epc = property_instance.simulation_epcs[rec["phase"]].copy() # The current heating, hot water and energy kwh should be based on the new, unadjusted @@ -513,6 +534,17 @@ class Recommendations: kwh_reduction = heating_kwh_reduction + hot_water_kwh_reduction + lighting_kwh_reduction + phase_kwh_figures[rec["phase"]] = { + "adjusted": { + "heating": new_heating_kwh_adjusted, + "hot_water": new_hot_water_kwh_adjusted + }, + "unadjusted": { + "heating": new_heating_kwh, + "hot_water": new_hot_water_kwh + } + } + else: previous_phase = rec["phase"] - 1 predicted_sap_points = ( @@ -527,30 +559,177 @@ class Recommendations: new_heat_demand ) + if rec["type"] == "lighting": + # If we have a lighting recommendation, the heating, hot water and lighting costs will + # be from the previous phase - nothing will change + new_heating_cost = heating_cost_phase_impact[ + heating_cost_phase_impact["phase"] == previous_phase + ]["adjusted_cost"].values[0] + new_hot_water_cost = hot_water_cost_phase_impact[ + hot_water_cost_phase_impact["phase"] == previous_phase + ]["adjusted_cost"].values[0] + + new_lighting_cost = min( + new_lighting_cost, phase_lighting_costs[previous_phase]["adjusted"] + ) + # We also use the unadjusted costs for the scoring from the previous phase + scoring_heating_cost = heating_cost_phase_impact[ + heating_cost_phase_impact["phase"] == previous_phase + ]["predictions"].values[0] + scoring_hot_water_cost = hot_water_cost_phase_impact[ + hot_water_cost_phase_impact["phase"] == previous_phase + ]["predictions"].values[0] + scoring_lighting_cost = min( + new_lighting_cost_unadjusted, + phase_lighting_costs[previous_phase]["unadjusted"] + ) + else: + # Whereas for other recommendations, we use the new costs + new_heating_cost = min( + new_heating_cost, + heating_cost_phase_impact[ + heating_cost_phase_impact["phase"] == previous_phase + ]["adjusted_cost"].values[0] + ) + new_hot_water_cost = min( + new_hot_water_cost, + hot_water_cost_phase_impact[ + hot_water_cost_phase_impact["phase"] == previous_phase + ]["adjusted_cost"].values[0] + ) + new_lighting_cost = phase_lighting_costs[previous_phase]["adjusted"] + + scoring_heating_cost = min( + new_heating_cost_unadjusted, + heating_cost_phase_impact[ + heating_cost_phase_impact["phase"] == previous_phase + ]["predictions"].values[0] + ) + scoring_hot_water_cost = min( + new_hot_water_cost_unadjusted, + hot_water_cost_phase_impact[ + hot_water_cost_phase_impact["phase"] == previous_phase + ]["predictions"].values[0] + ) + scoring_lighting_cost = phase_lighting_costs[previous_phase]["unadjusted"] + # We now estimate the adjusted cost savings for the recommendation predicted_heating_cost_reduction = ( heating_cost_phase_impact[heating_cost_phase_impact["phase"] == previous_phase][ "adjusted_cost" ].values[0] - new_heating_cost ) - predicted_heating_cost_reduction = ( - 0 if predicted_heating_cost_reduction < 0 else predicted_heating_cost_reduction - ) predicted_hot_water_cost_reduction = ( hot_water_cost_phase_impact[hot_water_cost_phase_impact["phase"] == previous_phase][ "adjusted_cost" ].values[0] - new_hot_water_cost ) - predicted_hot_water_cost_reduction = ( - 0 if predicted_hot_water_cost_reduction < 0 else predicted_hot_water_cost_reduction - ) # Only lighting recommendations can have an impact here - predicted_lighting_cost_reduction = 0 if rec["type"] != "lighting" else ( - lighting_cost_phase_impact[lighting_cost_phase_impact["phase"] == previous_phase][ - "adjusted_cost" - ].values[0] - new_lighting_cost + predicted_lighting_cost_reduction = ( + phase_lighting_costs[previous_phase]["adjusted"] - new_lighting_cost + ) + + # We now predict the kwh savings using the xgb model - this is based on + # the new costs at this phase + + simulation_epc = property_instance.simulation_epcs[rec["phase"]].copy() + # The current heating, hot water and energy kwh should be based on the new, unadjusted + # costs for lighting, heating, hot water + simulation_epc["heating-cost-current"] = int(scoring_heating_cost) + simulation_epc["hot-water-cost-current"] = int(scoring_hot_water_cost) + simulation_epc["lighting-cost-current"] = int(scoring_lighting_cost) + # We predict with the energy consumption model + scoring_df = pd.DataFrame([simulation_epc]) + # Change columns from underscores to hyphens + scoring_df.columns = [ + x.lower().replace("_", "-") for x in scoring_df.columns + ] + for col in ["heating_kwh", "hot_water_kwh"]: + scoring_df[col] = None + + energy_consumption_client.data = None + new_heating_kwh = energy_consumption_client.score_new_data( + new_data=scoring_df, target="heating_kwh" + )[0] + + new_hot_water_kwh = energy_consumption_client.score_new_data( + new_data=scoring_df, target="hot_water_kwh" + )[0] + + # Adjust these figures + new_heating_kwh_adjusted = AnnualBillSavings.adjust_energy_to_metered( + new_heating_kwh, current_epc_rating=property_instance.data["current-energy-rating"] + ) + new_hot_water_kwh_adjusted = AnnualBillSavings.adjust_energy_to_metered( + new_hot_water_kwh, current_epc_rating=property_instance.data["current-energy-rating"] + ) + + heating_kwh_reduction = 0 if predicted_heating_cost_reduction == 0 else ( + phase_kwh_figures[previous_phase]["adjusted"]["heating"] - new_heating_kwh_adjusted + ) + + hot_water_kwh_reduction = 0 if predicted_hot_water_cost_reduction == 0 else ( + phase_kwh_figures[previous_phase]["adjusted"]["hot_water"] - new_hot_water_kwh_adjusted + ) + + lighting_kwh_reduction = predicted_lighting_cost_reduction / AnnualBillSavings.ELECTRICITY_PRICE_CAP + + # This is the total bill savings for the recommendation + + predicted_appliances_cost_reduction = 0 + predicted_appliances_kwh_reduction = 0 + if rec["type"] == "solar_pv": + # Calulate the amount of energy the solar panel array will generate for this unit + unit_energy_consumption = ( + rec["initial_ac_kwh_per_year"] * + property_instance.solar_panel_configuration["unit_share_of_energy"] + ) + + unit_energy_utilised = unit_energy_consumption * GoogleSolarApi.SOLAR_CONSUMPTION_PROPORTION + unit_energy_exported = unit_energy_consumption - unit_energy_utilised + unit_energy_exported_value = unit_energy_exported * AnnualBillSavings.ELECTRICITY_EXPORT_PAYMENT + + # We assume that 50% of the energy generated will be used by the property without a battery + # to be conservative + + # of the energy utilised, some of it is used by heating, hot water and lighting so we + # remove that from the total + unit_energy_utilised -= ( + heating_kwh_reduction + hot_water_kwh_reduction + lighting_kwh_reduction + ) + unit_energy_utilised = 0 if unit_energy_utilised < 0 else unit_energy_utilised + + # This is how much energy the appliances will use after install + post_install_appliance_kwh = ( + property_instance.energy_consumption_estimates["adjusted"]["appliances"] - + unit_energy_utilised + ) + post_install_appliance_kwh = ( + 0 if post_install_appliance_kwh < 0 else post_install_appliance_kwh + ) + + predicted_appliances_kwh_reduction = ( + property_instance.energy_consumption_estimates["adjusted"]["appliances"] - + post_install_appliance_kwh + ) + + predicted_appliances_cost_reduction = unit_energy_exported_value + ( + predicted_appliances_kwh_reduction * AnnualBillSavings.ELECTRICITY_PRICE_CAP + ) + + # We now calculate the predicted_bill_savings + predicted_bill_savings = ( + predicted_heating_cost_reduction + predicted_hot_water_cost_reduction + + predicted_lighting_cost_reduction + predicted_appliances_cost_reduction + ) + + kwh_reduction = ( + heating_kwh_reduction + + hot_water_kwh_reduction + + lighting_kwh_reduction + + predicted_appliances_kwh_reduction ) if rec["type"] == "low_energy_lighting": diff --git a/recommendations/SolarPvRecommendations.py b/recommendations/SolarPvRecommendations.py index 18bfdced..596b9290 100644 --- a/recommendations/SolarPvRecommendations.py +++ b/recommendations/SolarPvRecommendations.py @@ -113,13 +113,15 @@ class SolarPvRecommendations: for rank, recommendation_config in best_configurations.iterrows(): roof_coverage_percent = round(recommendation_config["panneled_roof_area"] / total_roof_area * 100) - # Spread the cost to the individual units + # 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_warrage"] / 100) / 10 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, @@ -135,6 +137,7 @@ class SolarPvRecommendations: # 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 }