Skip to content

Commit 7989247

Browse files
authored
solis (#162)
* solis * solis api * solis inverter_example * rm solaredge * refactored * coroutines * load env * rm unnecessary funcs * dashboard update with solis * rm code * async rm * asyncio * rm async def main * rm unnecessary vars * rm unnecessary var * rm asyncio ex
1 parent a53c802 commit 7989247

File tree

10 files changed

+453
-117
lines changed

10 files changed

+453
-117
lines changed

.env.example

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ ENPHASE_API_KEY = 'user_enphase_api_key'
77
# Replace ENPHASE_CLIENT_ID below with the actual client id
88
AUTHORIZATION_URL = 'https://api.enphaseenergy.com/oauth/authorize?response_type=code&client_id=ENPHASE_CLIENT_ID'
99

10-
# User needs to add their SolarEdge API details
10+
# User needs to add their Solis Cloud API details
1111

12-
SOLAREDGE_ACCOUNT_KEY='user_solaredge_account_key'
13-
SOLAREDGE_USER_KEY='user_solaredge_user_key'
12+
SOLIS_CLOUD_API_KEY = 'user_solis_account_key'
13+
SOLIS_CLOUD_API_KEY_SECRET = 'user_solis_user_key'
14+
SOLIS_CLOUD_API_URL = 'https://www.soliscloud.com'
15+
SOLIS_CLOUD_API_PORT = '13333'
1416

1517
# This section is for OpenMeteo setup
1618

api/app/api.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
from fastapi.middleware.cors import CORSMiddleware
33
from quartz_solar_forecast.pydantic_models import PVSite
44
from quartz_solar_forecast.forecast import run_forecast
5-
from datetime import datetime
65

76
app = FastAPI()
87

dashboards/dashboard_2/app.py

Lines changed: 36 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@
1212
import json
1313
from urllib.parse import urlencode
1414
from PIL import Image
15+
import asyncio
16+
17+
from quartz_solar_forecast.pydantic_models import PVSite
18+
from quartz_solar_forecast.forecasts import forecast_v1_tilt_orientation
19+
from quartz_solar_forecast.forecast import predict_tryolabs
20+
from quartz_solar_forecast.data import get_nwp, process_pv_data
21+
from quartz_solar_forecast.inverters.enphase import process_enphase_data
22+
from quartz_solar_forecast.inverters.solis import SolisData, get_solis_data
1523

1624
# Load environment variables
1725
load_dotenv()
@@ -23,12 +31,6 @@
2331
# Add the parent directory to the Python path
2432
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
2533

26-
from quartz_solar_forecast.pydantic_models import PVSite
27-
from quartz_solar_forecast.forecasts import forecast_v1_tilt_orientation
28-
from quartz_solar_forecast.forecast import predict_tryolabs
29-
from quartz_solar_forecast.data import get_nwp, process_pv_data
30-
from quartz_solar_forecast.inverters.enphase import process_enphase_data
31-
3234
# Get the directory of the current script
3335
script_dir = os.path.dirname(os.path.abspath(__file__))
3436

@@ -144,11 +146,14 @@ def make_pv_data(
144146
ts: pd.Timestamp,
145147
access_token: str = None,
146148
enphase_system_id: str = None,
149+
solis_data: pd.DataFrame = None
147150
) -> xr.Dataset:
148151
live_generation_kw = None
149152

150153
if site.inverter_type == "enphase" and access_token and enphase_system_id:
151154
live_generation_kw = get_enphase_data(enphase_system_id, access_token)
155+
elif site.inverter_type == "solis" and solis_data is not None:
156+
live_generation_kw = solis_data
152157

153158
da = process_pv_data(live_generation_kw, ts, site)
154159
return da
@@ -160,6 +165,7 @@ def predict_ocf(
160165
nwp_source: str = "icon",
161166
access_token: str = None,
162167
enphase_system_id: str = None,
168+
solis_data: pd.DataFrame = None
163169
):
164170
if ts is None:
165171
ts = pd.Timestamp.now().round("15min")
@@ -168,7 +174,7 @@ def predict_ocf(
168174

169175
nwp_xr = get_nwp(site=site, ts=ts, nwp_source=nwp_source)
170176
pv_xr = make_pv_data(
171-
site=site, ts=ts, access_token=access_token, enphase_system_id=enphase_system_id
177+
site=site, ts=ts, access_token=access_token, enphase_system_id=enphase_system_id, solis_data=solis_data
172178
)
173179

174180
pred_df = forecast_v1_tilt_orientation(nwp_source, nwp_xr, pv_xr, ts, model=model)
@@ -181,9 +187,10 @@ def run_forecast(
181187
nwp_source: str = "icon",
182188
access_token: str = None,
183189
enphase_system_id: str = None,
190+
solis_data: pd.DataFrame = None
184191
) -> pd.DataFrame:
185192
if model == "gb":
186-
return predict_ocf(site, None, ts, nwp_source, access_token, enphase_system_id)
193+
return predict_ocf(site, None, ts, nwp_source, access_token, enphase_system_id, solis_data)
187194
elif model == "xgb":
188195
return predict_tryolabs(site, ts)
189196
else:
@@ -192,7 +199,8 @@ def run_forecast(
192199
def fetch_data_and_run_forecast(
193200
site: PVSite,
194201
access_token: str = None,
195-
enphase_system_id: str = None
202+
enphase_system_id: str = None,
203+
solis_data: pd.DataFrame = None
196204
):
197205
with st.spinner("Running forecast..."):
198206
try:
@@ -208,6 +216,7 @@ def fetch_data_and_run_forecast(
208216
ts=ts,
209217
access_token=access_token,
210218
enphase_system_id=enphase_system_id,
219+
solis_data=solis_data
211220
)
212221

213222
# Create a site without inverter for comparison
@@ -246,10 +255,11 @@ def fetch_data_and_run_forecast(
246255
longitude = st.sidebar.number_input("Longitude", min_value=-180.0, max_value=180.0, value=-1.25, step=0.01)
247256
capacity_kwp = st.sidebar.number_input("Capacity (kWp)", min_value=0.1, value=1.25, step=0.01)
248257

249-
inverter_type = st.sidebar.selectbox("Select Inverter", ["No Inverter", "Enphase"])
258+
inverter_type = st.sidebar.selectbox("Select Inverter", ["No Inverter", "Enphase", "Solis"])
250259

251260
access_token = None
252261
enphase_system_id = None
262+
solis_data = None
253263

254264
if inverter_type == "Enphase":
255265
if "access_token" not in st.session_state:
@@ -258,6 +268,8 @@ def fetch_data_and_run_forecast(
258268
access_token, enphase_system_id = st.session_state["access_token"], os.getenv(
259269
"ENPHASE_SYSTEM_ID"
260270
)
271+
elif inverter_type == "Solis":
272+
solis_data = SolisData()
261273

262274
if st.sidebar.button("Run Forecast"):
263275
if inverter_type == "Enphase" and (access_token is None or enphase_system_id is None):
@@ -270,12 +282,21 @@ def fetch_data_and_run_forecast(
270282
latitude=latitude,
271283
longitude=longitude,
272284
capacity_kwp=capacity_kwp,
273-
inverter_type="enphase" if inverter_type == "Enphase" else "none" # Changed this line
285+
inverter_type=inverter_type.lower()
274286
)
275287

276-
predictions_df, ts = fetch_data_and_run_forecast(
277-
site, access_token, enphase_system_id
278-
)
288+
# Fetch data based on the selected inverter type
289+
if inverter_type == "Enphase":
290+
predictions_df, ts = fetch_data_and_run_forecast(
291+
site, access_token, enphase_system_id
292+
)
293+
elif inverter_type == "Solis":
294+
solis_df = asyncio.run(get_solis_data())
295+
predictions_df, ts = fetch_data_and_run_forecast(
296+
site, solis_data=solis_df
297+
)
298+
else:
299+
predictions_df, ts = fetch_data_and_run_forecast(site)
279300

280301
if predictions_df is not None:
281302
st.success("Forecast completed successfully!")
@@ -308,7 +329,7 @@ def fetch_data_and_run_forecast(
308329
y=["power_kw", "power_kw_no_live_pv"],
309330
title="Forecasted Power Generation Comparison",
310331
labels={
311-
"power_kw": "Forecast with selected inverter type",
332+
"power_kw": f"Forecast with {inverter_type}",
312333
"power_kw_no_live_pv": "Forecast without recent PV data"
313334
}
314335
)

examples/example.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,5 @@ def main():
1313
print(predictions_df)
1414
print(f"Max: {predictions_df['power_kw'].max()}")
1515

16-
1716
if __name__ == "__main__":
1817
main()

examples/inverter_example.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ def main(save_outputs: bool = False):
1313
timestamp_str = datetime.fromtimestamp(timestamp, tz=timezone.utc).strftime('%Y-%m-%d %H:%M:%S')
1414
ts = pd.to_datetime(timestamp_str)
1515

16-
# make input data with live enphase data
17-
site_live = PVSite(latitude=51.75, longitude=-1.25, capacity_kwp=1.25, inverter_type="enphase")
16+
# make input data with live enphase or solis data
17+
site_live = PVSite(latitude=51.75, longitude=-1.25, capacity_kwp=1.25, inverter_type="solis") # inverter_type="enphase" or "solis"
1818

1919
# make input data with nan data
2020
site_no_live = PVSite(latitude=51.75, longitude=-1.25, capacity_kwp=1.25)

quartz_solar_forecast/data.py

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,26 @@
11
""" Function to get NWP data and create fake PV dataset"""
2-
import json
32
import ssl
43
from datetime import datetime
54
import os
6-
75
import numpy as np
86
import pandas as pd
9-
import requests
107
import xarray as xr
11-
128
import openmeteo_requests
139
import requests_cache
10+
import asyncio
11+
1412
from retry_requests import retry
13+
from typing import Optional
1514

1615
from quartz_solar_forecast.pydantic_models import PVSite
1716
from quartz_solar_forecast.inverters.enphase import get_enphase_data
18-
from quartz_solar_forecast.inverters.solaredge import get_site_coordinates, get_site_list, get_solaredge_data
17+
from quartz_solar_forecast.inverters.solis import get_solis_data
1918

2019
ssl._create_default_https_context = ssl._create_unverified_context
2120

2221
from dotenv import load_dotenv
2322

24-
system_id = os.getenv('ENPHASE_SYSTEM_ID')
23+
load_dotenv()
2524

2625
def get_nwp(site: PVSite, ts: datetime, nwp_source: str = "icon") -> xr.Dataset:
2726
"""
@@ -143,7 +142,7 @@ def format_nwp_data(df: pd.DataFrame, nwp_source:str, site: PVSite):
143142
)
144143
return data_xr
145144

146-
def process_pv_data(live_generation_kw: pd.DataFrame, ts: pd.Timestamp, site: PVSite) -> xr.Dataset:
145+
def process_pv_data(live_generation_kw: Optional[pd.DataFrame], ts: pd.Timestamp, site: PVSite) -> xr.Dataset:
147146
"""
148147
Process PV data and create an xarray Dataset.
149148
@@ -152,7 +151,7 @@ def process_pv_data(live_generation_kw: pd.DataFrame, ts: pd.Timestamp, site: PV
152151
:param site: PV site information
153152
:return: xarray Dataset containing processed PV data
154153
"""
155-
if live_generation_kw is not None:
154+
if live_generation_kw is not None and not live_generation_kw.empty:
156155
# get the most recent data
157156
recent_pv_data = live_generation_kw[live_generation_kw['timestamp'] <= ts]
158157
power_kw = np.array([np.array(recent_pv_data["power_kw"].values, dtype=np.float64)])
@@ -181,7 +180,7 @@ def process_pv_data(live_generation_kw: pd.DataFrame, ts: pd.Timestamp, site: PV
181180

182181
def make_pv_data(site: PVSite, ts: pd.Timestamp) -> xr.Dataset:
183182
"""
184-
Make PV data by combining live data from SolarEdge or Enphase and fake PV data.
183+
Make PV data by combining live data from Enphase or Solis and fake PV data.
185184
Later we could add PV history here.
186185
:param site: the PV site
187186
:param ts: the timestamp of the site
@@ -191,20 +190,16 @@ def make_pv_data(site: PVSite, ts: pd.Timestamp) -> xr.Dataset:
191190
live_generation_kw = None
192191

193192
# Check if the site has an inverter type specified
194-
if site.inverter_type == 'solaredge':
195-
# Fetch the list of site IDs associated with the SolarEdge account
196-
site_ids = get_site_list()
197-
# Find the site ID that matches the site's latitude and longitude
198-
matching_site_ids = [s_id for s_id in site_ids if abs(site.latitude - lat) < 1e-6 and abs(site.longitude - lon) < 1e-6 for lat, lon in get_site_coordinates(s_id)]
199-
if not matching_site_ids:
200-
raise ValueError("Site not found in the list of associated sites.")
201-
elif len(matching_site_ids) > 1:
202-
raise ValueError("Multiple sites found matching the given latitude and longitude.")
193+
if site.inverter_type == 'enphase':
194+
system_id = os.getenv('ENPHASE_SYSTEM_ID')
195+
if system_id:
196+
live_generation_kw = get_enphase_data(system_id)
203197
else:
204-
site_id = matching_site_ids[0]
205-
live_generation_kw = get_solaredge_data(site_id)
206-
elif site.inverter_type == 'enphase':
207-
live_generation_kw = get_enphase_data(system_id)
198+
print("Error: Enphase inverter ID is not provided in the environment variables.")
199+
elif site.inverter_type == 'solis':
200+
live_generation_kw = asyncio.run(get_solis_data())
201+
if live_generation_kw is None:
202+
print("Error: Failed to retrieve Solis inverter data.")
208203
else:
209204
# If no inverter type is specified or not recognized, set live_generation_kw to None
210205
live_generation_kw = None

quartz_solar_forecast/inverters/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,5 +62,5 @@ Open-Source-Quartz-Solar-Forecast/
6262
4. Install the requirements by entering `pip install -r requirements.txt` and `pip install -e .`
6363
5. Install `plotly` by entering `pip install plotly`
6464
6. Create a `.env` file in the root directory, i.e. `Open-Source-Quartz-Solar-Forecast`
65-
7. Add your Solar Inverter's user credentials along with environment variables in the `.env` file, refer to the `.env.example` file for Enphase & SolarEdge credential examples
65+
7. Add your Solar Inverter's user credentials along with environment variables in the `.env` file, refer to the `.env.example` file for Enphase & Solis credential examples
6666
8. Run the `inverter_example.py` file by entering `python examples/inverter_example.py`

quartz_solar_forecast/inverters/solaredge.py

Lines changed: 0 additions & 70 deletions
This file was deleted.

0 commit comments

Comments
 (0)