Skip to content

Commit 32ab2fa

Browse files
committed
frontend
1 parent 31b70ec commit 32ab2fa

File tree

3 files changed

+258
-1
lines changed

3 files changed

+258
-1
lines changed

frontend/app.py

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
import streamlit as st
2+
import http.client
3+
import pandas as pd
4+
import plotly.express as px
5+
from datetime import datetime, timezone, timedelta
6+
import sys
7+
import os
8+
import logging
9+
import numpy as np
10+
import xarray as xr
11+
from dotenv import load_dotenv
12+
import base64
13+
import json
14+
import requests
15+
from urllib.parse import urlencode
16+
from PIL import Image
17+
18+
# Load environment variables
19+
load_dotenv()
20+
21+
# Set up logging
22+
logging.basicConfig(level=logging.INFO)
23+
logger = logging.getLogger(__name__)
24+
25+
# Add the parent directory to the Python path
26+
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
27+
28+
from quartz_solar_forecast.pydantic_models import PVSite
29+
from quartz_solar_forecast.forecasts import forecast_v1_tilt_orientation, TryolabsSolarPowerPredictor
30+
from quartz_solar_forecast.data import get_nwp, process_pv_data
31+
from quartz_solar_forecast.inverters.enphase import process_enphase_data
32+
33+
# Get the directory of the current script
34+
script_dir = os.path.dirname(os.path.abspath(__file__))
35+
36+
# Construct the path to logo.png
37+
logo_path = os.path.join(script_dir, "logo.png")
38+
im = Image.open(logo_path)
39+
40+
st.set_page_config(page_title="Open Source Quartz Solar Forecast | Open Climate Fix", layout="wide", page_icon=im)
41+
42+
st.title("☀️ Open Source Quartz Solar Forecast")
43+
44+
def get_enphase_auth_url():
45+
client_id = os.getenv('ENPHASE_CLIENT_ID')
46+
redirect_uri = "https://api.enphaseenergy.com/oauth/redirect_uri"
47+
params = {
48+
"response_type": "code",
49+
"client_id": client_id,
50+
"redirect_uri": redirect_uri,
51+
}
52+
auth_url = f"https://api.enphaseenergy.com/oauth/authorize?{urlencode(params)}"
53+
return auth_url
54+
55+
def get_enphase_access_token(auth_code):
56+
client_id = os.getenv('ENPHASE_CLIENT_ID')
57+
client_secret = os.getenv('ENPHASE_CLIENT_SECRET')
58+
59+
credentials = f"{client_id}:{client_secret}"
60+
credentials_bytes = credentials.encode("utf-8")
61+
encoded_credentials = base64.b64encode(credentials_bytes).decode("utf-8")
62+
conn = http.client.HTTPSConnection("api.enphaseenergy.com")
63+
payload = ""
64+
headers = {
65+
"Authorization": f"Basic {encoded_credentials}"
66+
}
67+
conn.request(
68+
"POST",
69+
f"/oauth/token?grant_type=authorization_code&redirect_uri=https://api.enphaseenergy.com/oauth/redirect_uri&code={auth_code}",
70+
payload,
71+
headers,
72+
)
73+
res = conn.getresponse()
74+
data = res.read()
75+
76+
# Decode the data read from the response
77+
decoded_data = data.decode("utf-8")
78+
79+
# Convert the decoded data into JSON format
80+
data_json = json.loads(decoded_data)
81+
access_token = data_json["access_token"]
82+
83+
return access_token
84+
85+
def get_enphase_data(enphase_system_id: str, access_token: str) -> pd.DataFrame:
86+
api_key = os.getenv('ENPHASE_API_KEY')
87+
start_at = int((datetime.now() - timedelta(weeks=1)).timestamp())
88+
granularity = "week"
89+
90+
conn = http.client.HTTPSConnection("api.enphaseenergy.com")
91+
headers = {
92+
"Authorization": f"Bearer {str(access_token)}",
93+
"key": str(api_key)
94+
}
95+
96+
url = f"/api/v4/systems/{enphase_system_id}/telemetry/production_micro?start_at={start_at}&granularity={granularity}"
97+
conn.request("GET", url, headers=headers)
98+
res = conn.getresponse()
99+
data = res.read()
100+
decoded_data = data.decode("utf-8")
101+
data_json = json.loads(decoded_data)
102+
103+
return process_enphase_data(data_json, start_at)
104+
105+
def enphase_authorization():
106+
auth_url = get_enphase_auth_url()
107+
st.write("Please visit the following URL to authorize the application:")
108+
st.markdown(f"[Enphase Authorization URL]({auth_url})")
109+
st.write("After authorization, you will be redirected to a URL. Please copy the entire URL and paste it below:")
110+
111+
redirect_url = st.text_input("Enter the redirect URL:")
112+
113+
if redirect_url:
114+
auth_code = redirect_url.split("?code=")[1] if "?code=" in redirect_url else None
115+
116+
if auth_code:
117+
access_token = get_enphase_access_token(auth_code)
118+
return access_token, os.getenv('ENPHASE_SYSTEM_ID')
119+
120+
return None, None
121+
122+
def make_pv_data(site: PVSite, ts: pd.Timestamp, access_token: str = None, enphase_system_id: str = None) -> xr.Dataset:
123+
live_generation_kw = None
124+
125+
if site.inverter_type == 'enphase' and access_token and enphase_system_id:
126+
live_generation_kw = get_enphase_data(enphase_system_id, access_token)
127+
128+
da = process_pv_data(live_generation_kw, ts, site)
129+
return da
130+
131+
def predict_ocf(site: PVSite, model=None, ts: datetime | str = None, nwp_source: str = "icon", access_token: str = None, enphase_system_id: str = None):
132+
if ts is None:
133+
ts = pd.Timestamp.now().round("15min")
134+
if isinstance(ts, str):
135+
ts = datetime.fromisoformat(ts)
136+
137+
nwp_xr = get_nwp(site=site, ts=ts, nwp_source=nwp_source)
138+
pv_xr = make_pv_data(site=site, ts=ts, access_token=access_token, enphase_system_id=enphase_system_id)
139+
140+
pred_df = forecast_v1_tilt_orientation(nwp_source, nwp_xr, pv_xr, ts, model=model)
141+
return pred_df
142+
143+
def predict_tryolabs(site: PVSite, ts: datetime | str = None):
144+
solar_power_predictor = TryolabsSolarPowerPredictor()
145+
146+
if ts is None:
147+
start_date = pd.Timestamp.now().strftime("%Y-%m-%d")
148+
start_time = pd.Timestamp.now().round(freq='h')
149+
else:
150+
start_date = pd.Timestamp(ts).strftime("%Y-%m-%d")
151+
start_time = pd.Timestamp(ts).round(freq='h')
152+
153+
end_time = start_time + pd.Timedelta(hours=48)
154+
155+
solar_power_predictor.load_model()
156+
predictions = solar_power_predictor.predict_power_output(
157+
latitude=site.latitude,
158+
longitude=site.longitude,
159+
start_date=start_date,
160+
kwp=site.capacity_kwp,
161+
orientation=site.orientation,
162+
tilt=site.tilt,
163+
)
164+
165+
predictions = predictions[
166+
(predictions["date"] >= start_time) & (predictions["date"] < end_time)
167+
]
168+
predictions = predictions.reset_index(drop=True)
169+
predictions.set_index("date", inplace=True)
170+
return predictions
171+
172+
def run_forecast(site: PVSite, model: str = "gb", ts: datetime | str = None, nwp_source: str = "icon", access_token: str = None, enphase_system_id: str = None) -> pd.DataFrame:
173+
if model == "gb":
174+
return predict_ocf(site, None, ts, nwp_source, access_token, enphase_system_id)
175+
elif model == "xgb":
176+
return predict_tryolabs(site, ts)
177+
else:
178+
raise ValueError(f"Unsupported model: {model}. Choose between 'xgb' and 'gb'")
179+
180+
def fetch_data_and_run_forecast(access_token: str = None, enphase_system_id: str = None):
181+
with st.spinner("Running forecast..."):
182+
try:
183+
timestamp = datetime.now().timestamp()
184+
timestamp_str = datetime.fromtimestamp(timestamp, tz=timezone.utc).strftime('%Y-%m-%d %H:%M:%S')
185+
ts = pd.to_datetime(timestamp_str)
186+
187+
site_live = PVSite(latitude=51.75, longitude=-1.25, capacity_kwp=1.25, inverter_type="enphase")
188+
site_no_live = PVSite(latitude=51.75, longitude=-1.25, capacity_kwp=1.25)
189+
190+
predictions_with_recent_pv_df = run_forecast(site=site_live, ts=ts, access_token=access_token, enphase_system_id=enphase_system_id)
191+
predictions_df = run_forecast(site=site_no_live, ts=ts)
192+
193+
predictions_with_recent_pv_df["power_kw_no_live_pv"] = predictions_df["power_kw"]
194+
195+
return predictions_with_recent_pv_df, ts
196+
197+
except Exception as e:
198+
logger.error(f"An error occurred: {str(e)}")
199+
st.error(f"An error occurred: {str(e)}")
200+
return None, None
201+
202+
# Main app logic
203+
access_token, enphase_system_id = enphase_authorization()
204+
205+
if st.button("Run Forecast"):
206+
if access_token:
207+
predictions_with_recent_pv_df, ts = fetch_data_and_run_forecast(access_token, enphase_system_id)
208+
209+
if predictions_with_recent_pv_df is not None:
210+
st.success("Forecast completed successfully!")
211+
212+
# Display current timestamp
213+
st.subheader(f"Forecast generated at: {ts}")
214+
215+
# Create three columns
216+
col1, col2, col3 = st.columns(3)
217+
218+
with col1:
219+
st.metric("Current Power", f"{predictions_with_recent_pv_df['power_kw'].iloc[-1]:.2f} kW")
220+
221+
with col2:
222+
total_energy = predictions_with_recent_pv_df['power_kw'].sum() * 0.25 # Assuming 15-minute intervals
223+
st.metric("Total Forecasted Energy", f"{total_energy:.2f} kWh")
224+
225+
with col3:
226+
peak_power = predictions_with_recent_pv_df['power_kw'].max()
227+
st.metric("Peak Forecasted Power", f"{peak_power:.2f} kW")
228+
229+
# Create a line chart of power generation
230+
fig = px.line(predictions_with_recent_pv_df.reset_index(),
231+
x='index', y=['power_kw', 'power_kw_no_live_pv'],
232+
title='Forecasted Power Generation Comparison')
233+
fig.update_layout(xaxis_title="Time", yaxis_title="Power (kW)")
234+
st.plotly_chart(fig, use_container_width=True)
235+
236+
# Display raw data
237+
st.subheader("Raw Forecast Data")
238+
st.dataframe(predictions_with_recent_pv_df.reset_index())
239+
240+
# Some information about the app
241+
242+
st.sidebar.info(
243+
"""
244+
This dashboard runs
245+
246+
[Open Climate Fix](https://openclimatefix.org/)'s
247+
248+
[Open Source Quartz Solar Forecast](https://github.com/openclimatefix/Open-Source-Quartz-Solar-Forecast/).
249+
250+
Click 'Run Forecast' and add the Home-Owner approved authentication URL to see the results.
251+
"""
252+
)
253+
254+
# Footer
255+
st.markdown("---")
256+
st.markdown(f"Created with ❤️ by [Open Climate Fix](https://openclimatefix.org/)")

frontend/logo.png

3.09 KB
Loading

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@ retry-requests==2.0.0
1010
gdown==5.1.0
1111
xgboost==2.0.3
1212
plotly
13-
typer
13+
typer
14+
streamlit

0 commit comments

Comments
 (0)