mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
integrating solar api to router
This commit is contained in:
parent
9781b08478
commit
01c50eb5cb
6 changed files with 51 additions and 127 deletions
2
.idea/Model.iml
generated
2
.idea/Model.iml
generated
|
|
@ -7,7 +7,7 @@
|
||||||
<sourceFolder url="file://$MODULE_DIR$/open_uprn" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/open_uprn" isTestSource="false" />
|
||||||
<sourceFolder url="file://$MODULE_DIR$/recommendations" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/recommendations" isTestSource="false" />
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="jdk" jdkName="Python 3.10 (model_data)" jdkType="Python SDK" />
|
<orderEntry type="jdk" jdkName="Python 3.10 (backend)" jdkType="Python SDK" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
</component>
|
</component>
|
||||||
<component name="PyNamespacePackagesService">
|
<component name="PyNamespacePackagesService">
|
||||||
|
|
|
||||||
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
|
|
@ -3,7 +3,7 @@
|
||||||
<component name="Black">
|
<component name="Black">
|
||||||
<option name="sdkName" value="Python 3.10 (backend)" />
|
<option name="sdkName" value="Python 3.10 (backend)" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (model_data)" project-jdk-type="Python SDK" />
|
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (backend)" project-jdk-type="Python SDK" />
|
||||||
<component name="PythonCompatibilityInspectionAdvertiser">
|
<component name="PythonCompatibilityInspectionAdvertiser">
|
||||||
<option name="version" value="3" />
|
<option name="version" value="3" />
|
||||||
</component>
|
</component>
|
||||||
|
|
|
||||||
|
|
@ -1,136 +1,19 @@
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from recommendations.Costs import MCS_SOLAR_PV_COST_DATA
|
from recommendations.Costs import MCS_SOLAR_PV_COST_DATA
|
||||||
|
from backend.ml_models.AnnualBillSavings import AnnualBillSavings
|
||||||
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
|
|
||||||
import requests
|
import requests
|
||||||
import msgpack
|
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
import time
|
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:
|
class GoogleSolarApi:
|
||||||
NORTH_FACING_AZIMUTH_RANGE = (-30, 30)
|
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):
|
def __init__(self, api_key, max_retries=5):
|
||||||
"""
|
"""
|
||||||
Initialize the GoogleSolarApi class with the provided API key and maximum retries.
|
Initialize the GoogleSolarApi class with the provided API key and maximum retries.
|
||||||
|
|
@ -150,6 +33,8 @@ class GoogleSolarApi:
|
||||||
self.roof_area = None
|
self.roof_area = None
|
||||||
self.roof_segment_indexes = None
|
self.roof_segment_indexes = None
|
||||||
self.panel_area = 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):
|
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.
|
: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)
|
self.insights_data = self.get_building_insights(longitude, latitude, required_quality)
|
||||||
|
|
||||||
# Extract key data from the insights response
|
# Extract key data from the insights response
|
||||||
|
|
@ -209,6 +93,7 @@ class GoogleSolarApi:
|
||||||
self.insights_data["solarPotential"]["panelHeightMeters"] *
|
self.insights_data["solarPotential"]["panelHeightMeters"] *
|
||||||
self.insights_data["solarPotential"]["panelWidthMeters"]
|
self.insights_data["solarPotential"]["panelWidthMeters"]
|
||||||
)
|
)
|
||||||
|
self.panel_wattage = self.insights_data["solarPotential"]["panelCapacityWatts"]
|
||||||
|
|
||||||
# Automatically exclude north-facing segments
|
# Automatically exclude north-facing segments
|
||||||
self.exclude_north_facing_segments()
|
self.exclude_north_facing_segments()
|
||||||
|
|
@ -246,7 +131,8 @@ class GoogleSolarApi:
|
||||||
"generatedEnergy": generated_energy,
|
"generatedEnergy": generated_energy,
|
||||||
"ratio": ratio,
|
"ratio": ratio,
|
||||||
"n_panels": segment["panelsCount"],
|
"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(),
|
"n_panels": roi_summary["n_panels"].sum(),
|
||||||
"total_energy": total_energy,
|
"total_energy": total_energy,
|
||||||
"total_cost": total_cost,
|
"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)
|
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)
|
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):
|
def exclude_north_facing_segments(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ class Settings(BaseSettings):
|
||||||
PLAN_TRIGGER_BUCKET: str
|
PLAN_TRIGGER_BUCKET: str
|
||||||
EPC_AUTH_TOKEN: str
|
EPC_AUTH_TOKEN: str
|
||||||
ORDNANCE_SURVEY_API_KEY: str
|
ORDNANCE_SURVEY_API_KEY: str
|
||||||
|
GOOGLE_SOLAR_API_KEY: str
|
||||||
DB_HOST: str
|
DB_HOST: str
|
||||||
DB_PASSWORD: str
|
DB_PASSWORD: str
|
||||||
DB_USERNAME: str
|
DB_USERNAME: str
|
||||||
|
|
|
||||||
|
|
@ -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.ml_models.api import ModelApi
|
||||||
from backend.Property import Property
|
from backend.Property import Property
|
||||||
|
from backend.apis.GoogleSolarApi import GoogleSolarApi
|
||||||
from etl.solar.SolarPhotoSupply import SolarPhotoSupply
|
from etl.solar.SolarPhotoSupply import SolarPhotoSupply
|
||||||
|
|
||||||
from recommendations.optimiser.CostOptimiser import CostOptimiser
|
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"
|
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)
|
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")
|
logger.info("Getting spatial data")
|
||||||
for p in input_properties:
|
for p in input_properties:
|
||||||
p.get_spatial_data(uprn_filenames)
|
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")
|
logger.info("Getting components and epc recommendations")
|
||||||
recommendations = {}
|
recommendations = {}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ class AnnualBillSavings:
|
||||||
# https://www.ofgem.gov.uk/publications/new-energy-price-cap-level-april-june-2024-starts-today
|
# https://www.ofgem.gov.uk/publications/new-energy-price-cap-level-april-june-2024-starts-today
|
||||||
ELECTRICITY_PRICE_CAP = 0.245
|
ELECTRICITY_PRICE_CAP = 0.245
|
||||||
GAS_PRICE_CAP = 0.0604
|
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
|
# This is a weighted mean of the price caps, using the consumption figures above as weights
|
||||||
PRICE_FACTOR = 0.09549999999999999
|
PRICE_FACTOR = 0.09549999999999999
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue