diff --git a/.idea/Model.iml b/.idea/Model.iml index b0f9c00d..4413bb06 100644 --- a/.idea/Model.iml +++ b/.idea/Model.iml @@ -7,7 +7,7 @@ - + diff --git a/.idea/misc.xml b/.idea/misc.xml index 1122b380..6f308057 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,7 +3,7 @@ - + diff --git a/backend/apis/GoogleSolarApi.py b/backend/apis/GoogleSolarApi.py index 0afa0b26..cac82f4b 100644 --- a/backend/apis/GoogleSolarApi.py +++ b/backend/apis/GoogleSolarApi.py @@ -1,136 +1,19 @@ import pandas as pd import numpy as np from recommendations.Costs import MCS_SOLAR_PV_COST_DATA - -from backend.Property import Property -from backend.SearchEpc import SearchEpc -from etl.epc.Record import EPCRecord -from dotenv import load_dotenv -from utils.s3 import read_dataframe_from_s3_parquet, read_from_s3 -import os +from backend.ml_models.AnnualBillSavings import AnnualBillSavings import requests -import msgpack from functools import lru_cache import time -load_dotenv(dotenv_path="backend/.env") -EPC_AUTH_TOKEN = os.getenv("EPC_AUTH_TOKEN") - -# This is for 6 Laura Close, Tintagel, PL34 0EB (same property that Cotswolrd energy used) -uprn = 100040099104 -# This is for 353A, Hermitage Lane, ME16 9NT (one of the e.on properties) -uprn = 200000964454 -# This is for 14 Victoria Road, Cross Hills, KEIGHLEY, North Yorkshire, ENGLAND, BD20 8SY -uprn = 100050346517 - -cleaning_data = read_dataframe_from_s3_parquet( - bucket_name="retrofit-data-dev", file_key="sap_change_model/cleaning_dataset.parquet", -) - -searcher = SearchEpc(address1="", postcode="", uprn=uprn, auth_token=EPC_AUTH_TOKEN, os_api_key="") - -searcher.find_property(skip_os=True) - -epc_records = { - 'original_epc': searcher.newest_epc.copy(), - 'full_sap_epc': searcher.full_sap_epc.copy(), - 'old_data': searcher.older_epcs.copy(), -} - -epc = EPCRecord( - epc_records=epc_records, - run_mode="newdata", - cleaning_data=cleaning_data -) - -uprn_filenames = read_dataframe_from_s3_parquet( - bucket_name="retrofit-data-dev", file_key="spatial/filename_meta.parquet" -) - -p = Property( - id=0, - address=searcher.address_clean, - postcode=searcher.postcode_clean, - epc_record=epc, - already_installed={}, - non_invasive_recommendations={}, -) - -p.get_spatial_data(uprn_filenames) - -cleaned = read_from_s3( - s3_file_name="cleaned_epc_data/cleaned.bson", - bucket_name="retrofit-data-dev" -) - -cleaned = msgpack.unpackb(cleaned, raw=False) - -from etl.solar.SolarPhotoSupply import SolarPhotoSupply - -photo_supply_lookup, floor_area_decile_thresholds = SolarPhotoSupply.load(bucket="retrofit-data-dev") - -p.get_components( - cleaned=cleaned, - photo_supply_lookup=photo_supply_lookup, - floor_area_decile_thresholds=floor_area_decile_thresholds -) -p.hot_water_energy_source -p.heating_energy_source - -longitude = p.spatial["longitude"] -latitude = p.spatial["latitude"] - -api_key = "AIzaSyCIz8Psu5h-1txuDX0rQpUTgkvdj8yohqU" -url = 'https://solar.googleapis.com/v1/solarPotential' -params = { - 'location.latitude': f'{latitude:.5f}', - 'location.longitude': f'{longitude:.5f}', - 'requiredQuality': "MEDIUM", - 'key': api_key -} - -insights_url = 'https://solar.googleapis.com/v1/buildingInsights:findClosest' - -# Make the GET request to the Solar API -insights_response = requests.get(insights_url, params=params) -insights_data = insights_response.json() - -solar_potential = insights_data["solarPotential"] - -from pprint import pprint - -pprint(solar_potential) - -# This is the maximum number of panels that can be installed -solar_potential["maxArrayPanelsCount"] - -# This is the size of the panels used in the calculation - 400 watt -solar_potential["panelCapacityWatts"] - -# Height of the panels used -solar_potential["panelHeightMeters"] - -# Width of the panels used -solar_potential["panelWidthMeters"] - -# This is the maximum area that can be covered by the panels -solar_potential["maxArrayAreaMeters2"] - -# This is the area of the roof -solar_potential["wholeRoofStats"]["areaMeters2"] - -# This is the area of the floor -solar_potential["wholeRoofStats"]["groundAreaMeters2"] - -solar_potential["solarPanelConfigs"][0] -solar_potential["solarPanelConfigs"][1] - -self = GoogleSolarApi(api_key=api_key) - class GoogleSolarApi: NORTH_FACING_AZIMUTH_RANGE = (-30, 30) + # Conservative estimate of the proportion of electricity that will be consumed, whereas the rest will + # be exported + SOLAR_CONSUMPTION_PROPORTION = 0.5 + def __init__(self, api_key, max_retries=5): """ Initialize the GoogleSolarApi class with the provided API key and maximum retries. @@ -150,6 +33,8 @@ class GoogleSolarApi: self.roof_area = None self.roof_segment_indexes = None self.panel_area = None + self.panel_wattage = None + self.panel_performance = None def get_building_insights(self, longitude, latitude, required_quality="MEDIUM", max_retries=None): """ @@ -198,7 +83,6 @@ class GoogleSolarApi: :return: The JSON response containing the building insights data. """ - # TODO - can we make a request which includes the 30cm buffer from the edge of the roof? self.insights_data = self.get_building_insights(longitude, latitude, required_quality) # Extract key data from the insights response @@ -209,6 +93,7 @@ class GoogleSolarApi: self.insights_data["solarPotential"]["panelHeightMeters"] * self.insights_data["solarPotential"]["panelWidthMeters"] ) + self.panel_wattage = self.insights_data["solarPotential"]["panelCapacityWatts"] # Automatically exclude north-facing segments self.exclude_north_facing_segments() @@ -246,7 +131,8 @@ class GoogleSolarApi: "generatedEnergy": generated_energy, "ratio": ratio, "n_panels": segment["panelsCount"], - "cost": cost + "cost": cost, + "panneled_roof_area": self.panel_area * int(segment["panelsCount"]) } ) @@ -263,12 +149,43 @@ class GoogleSolarApi: "n_panels": roi_summary["n_panels"].sum(), "total_energy": total_energy, "total_cost": total_cost, - "weighted_ratio": weighted_ratio + "weighted_ratio": weighted_ratio, + "panneled_roof_area": roi_summary["panneled_roof_area"].sum(), + "array_warrage": roi_summary["n_panels"].sum() * self.panel_wattage } ) panel_performance = pd.DataFrame(panel_performance) + # We can have duplicate configurations + panel_performance = panel_performance.drop_duplicates() + # Ensure more than 4 panels + panel_performance = panel_performance[panel_performance["n_panels"] >= 4] + # Remove anything where the total energy is less than half of the array wattage + panel_performance = panel_performance[ + (panel_performance["total_energy"] / panel_performance["array_warrage"]) >= 0.5 + ] + + # This first bracket is the value of the energy bill savings + panel_performance["bill_savings"] = ( + self.SOLAR_CONSUMPTION_PROPORTION * + panel_performance["total_energy"] * + AnnualBillSavings.ELECTRICITY_PRICE_CAP + ) + # This is the amount of energy exported + panel_performance["export_value"] = ( + (1 - self.SOLAR_CONSUMPTION_PROPORTION) * + panel_performance["total_energy"] * + AnnualBillSavings.ELECTRICITY_EXPORT_PAYMENT + ) + panel_performance["energy_value"] = panel_performance["bill_savings"] + panel_performance["export_value"] + panel_performance["payback_years"] = panel_performance["total_cost"] / panel_performance["energy_value"] + panel_performance = panel_performance.sort_values("weighted_ratio", ascending=False) + # TODO: Finish this!! + + panel_performance["roof_area_percentage"] = panel_performance["panneled_roof_area"] / self.roof_area + + self.panel_performance = panel_performance def exclude_north_facing_segments(self): """ diff --git a/backend/app/config.py b/backend/app/config.py index 764bddf5..6f2e405b 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -14,6 +14,7 @@ class Settings(BaseSettings): PLAN_TRIGGER_BUCKET: str EPC_AUTH_TOKEN: str ORDNANCE_SURVEY_API_KEY: str + GOOGLE_SOLAR_API_KEY: str DB_HOST: str DB_PASSWORD: str DB_USERNAME: str diff --git a/backend/app/plan/router.py b/backend/app/plan/router.py index 9caab324..54e02766 100644 --- a/backend/app/plan/router.py +++ b/backend/app/plan/router.py @@ -29,6 +29,7 @@ from backend.app.utils import epc_to_sap_lower_bound, sap_to_epc from backend.ml_models.api import ModelApi from backend.Property import Property +from backend.apis.GoogleSolarApi import GoogleSolarApi from etl.solar.SolarPhotoSupply import SolarPhotoSupply from recommendations.optimiser.CostOptimiser import CostOptimiser @@ -347,10 +348,13 @@ async def trigger_plan(body: PlanTriggerRequest): bucket_name=get_settings().DATA_BUCKET, file_key="spatial/filename_meta.parquet" ) photo_supply_lookup, floor_area_decile_thresholds = SolarPhotoSupply.load(bucket=get_settings().DATA_BUCKET) + solar_api_client = GoogleSolarApi(api_key=get_settings().GOOGLE_SOLAR_API_KEY) logger.info("Getting spatial data") for p in input_properties: p.get_spatial_data(uprn_filenames) + # Call Google Solar API + solar_api_client.get(longitude=p.spatial["longitude"], latitude=p.spatial["latitude"]) logger.info("Getting components and epc recommendations") recommendations = {} diff --git a/backend/ml_models/AnnualBillSavings.py b/backend/ml_models/AnnualBillSavings.py index b92077e4..d88fe677 100644 --- a/backend/ml_models/AnnualBillSavings.py +++ b/backend/ml_models/AnnualBillSavings.py @@ -14,6 +14,8 @@ class AnnualBillSavings: # https://www.ofgem.gov.uk/publications/new-energy-price-cap-level-april-june-2024-starts-today ELECTRICITY_PRICE_CAP = 0.245 GAS_PRICE_CAP = 0.0604 + # This is the most recent export payment figure, at 12p per kwh + ELECTRICITY_EXPORT_PAYMENT = 0.12 # This is a weighted mean of the price caps, using the consumption figures above as weights PRICE_FACTOR = 0.09549999999999999