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