Skip to content

Commit

Permalink
solis (#162)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
aryanbhosale authored Jul 25, 2024
1 parent a53c802 commit 7989247
Show file tree
Hide file tree
Showing 10 changed files with 453 additions and 117 deletions.
8 changes: 5 additions & 3 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ ENPHASE_API_KEY = 'user_enphase_api_key'
# Replace ENPHASE_CLIENT_ID below with the actual client id
AUTHORIZATION_URL = 'https://api.enphaseenergy.com/oauth/authorize?response_type=code&client_id=ENPHASE_CLIENT_ID'

# User needs to add their SolarEdge API details
# User needs to add their Solis Cloud API details

SOLAREDGE_ACCOUNT_KEY='user_solaredge_account_key'
SOLAREDGE_USER_KEY='user_solaredge_user_key'
SOLIS_CLOUD_API_KEY = 'user_solis_account_key'
SOLIS_CLOUD_API_KEY_SECRET = 'user_solis_user_key'
SOLIS_CLOUD_API_URL = 'https://www.soliscloud.com'
SOLIS_CLOUD_API_PORT = '13333'

# This section is for OpenMeteo setup

Expand Down
1 change: 0 additions & 1 deletion api/app/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from fastapi.middleware.cors import CORSMiddleware
from quartz_solar_forecast.pydantic_models import PVSite
from quartz_solar_forecast.forecast import run_forecast
from datetime import datetime

app = FastAPI()

Expand Down
51 changes: 36 additions & 15 deletions dashboards/dashboard_2/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@
import json
from urllib.parse import urlencode
from PIL import Image
import asyncio

from quartz_solar_forecast.pydantic_models import PVSite
from quartz_solar_forecast.forecasts import forecast_v1_tilt_orientation
from quartz_solar_forecast.forecast import predict_tryolabs
from quartz_solar_forecast.data import get_nwp, process_pv_data
from quartz_solar_forecast.inverters.enphase import process_enphase_data
from quartz_solar_forecast.inverters.solis import SolisData, get_solis_data

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

from quartz_solar_forecast.pydantic_models import PVSite
from quartz_solar_forecast.forecasts import forecast_v1_tilt_orientation
from quartz_solar_forecast.forecast import predict_tryolabs
from quartz_solar_forecast.data import get_nwp, process_pv_data
from quartz_solar_forecast.inverters.enphase import process_enphase_data

# Get the directory of the current script
script_dir = os.path.dirname(os.path.abspath(__file__))

Expand Down Expand Up @@ -144,11 +146,14 @@ def make_pv_data(
ts: pd.Timestamp,
access_token: str = None,
enphase_system_id: str = None,
solis_data: pd.DataFrame = None
) -> xr.Dataset:
live_generation_kw = None

if site.inverter_type == "enphase" and access_token and enphase_system_id:
live_generation_kw = get_enphase_data(enphase_system_id, access_token)
elif site.inverter_type == "solis" and solis_data is not None:
live_generation_kw = solis_data

da = process_pv_data(live_generation_kw, ts, site)
return da
Expand All @@ -160,6 +165,7 @@ def predict_ocf(
nwp_source: str = "icon",
access_token: str = None,
enphase_system_id: str = None,
solis_data: pd.DataFrame = None
):
if ts is None:
ts = pd.Timestamp.now().round("15min")
Expand All @@ -168,7 +174,7 @@ def predict_ocf(

nwp_xr = get_nwp(site=site, ts=ts, nwp_source=nwp_source)
pv_xr = make_pv_data(
site=site, ts=ts, access_token=access_token, enphase_system_id=enphase_system_id
site=site, ts=ts, access_token=access_token, enphase_system_id=enphase_system_id, solis_data=solis_data
)

pred_df = forecast_v1_tilt_orientation(nwp_source, nwp_xr, pv_xr, ts, model=model)
Expand All @@ -181,9 +187,10 @@ def run_forecast(
nwp_source: str = "icon",
access_token: str = None,
enphase_system_id: str = None,
solis_data: pd.DataFrame = None
) -> pd.DataFrame:
if model == "gb":
return predict_ocf(site, None, ts, nwp_source, access_token, enphase_system_id)
return predict_ocf(site, None, ts, nwp_source, access_token, enphase_system_id, solis_data)
elif model == "xgb":
return predict_tryolabs(site, ts)
else:
Expand All @@ -192,7 +199,8 @@ def run_forecast(
def fetch_data_and_run_forecast(
site: PVSite,
access_token: str = None,
enphase_system_id: str = None
enphase_system_id: str = None,
solis_data: pd.DataFrame = None
):
with st.spinner("Running forecast..."):
try:
Expand All @@ -208,6 +216,7 @@ def fetch_data_and_run_forecast(
ts=ts,
access_token=access_token,
enphase_system_id=enphase_system_id,
solis_data=solis_data
)

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

inverter_type = st.sidebar.selectbox("Select Inverter", ["No Inverter", "Enphase"])
inverter_type = st.sidebar.selectbox("Select Inverter", ["No Inverter", "Enphase", "Solis"])

access_token = None
enphase_system_id = None
solis_data = None

if inverter_type == "Enphase":
if "access_token" not in st.session_state:
Expand All @@ -258,6 +268,8 @@ def fetch_data_and_run_forecast(
access_token, enphase_system_id = st.session_state["access_token"], os.getenv(
"ENPHASE_SYSTEM_ID"
)
elif inverter_type == "Solis":
solis_data = SolisData()

if st.sidebar.button("Run Forecast"):
if inverter_type == "Enphase" and (access_token is None or enphase_system_id is None):
Expand All @@ -270,12 +282,21 @@ def fetch_data_and_run_forecast(
latitude=latitude,
longitude=longitude,
capacity_kwp=capacity_kwp,
inverter_type="enphase" if inverter_type == "Enphase" else "none" # Changed this line
inverter_type=inverter_type.lower()
)

predictions_df, ts = fetch_data_and_run_forecast(
site, access_token, enphase_system_id
)
# Fetch data based on the selected inverter type
if inverter_type == "Enphase":
predictions_df, ts = fetch_data_and_run_forecast(
site, access_token, enphase_system_id
)
elif inverter_type == "Solis":
solis_df = asyncio.run(get_solis_data())
predictions_df, ts = fetch_data_and_run_forecast(
site, solis_data=solis_df
)
else:
predictions_df, ts = fetch_data_and_run_forecast(site)

if predictions_df is not None:
st.success("Forecast completed successfully!")
Expand Down Expand Up @@ -308,7 +329,7 @@ def fetch_data_and_run_forecast(
y=["power_kw", "power_kw_no_live_pv"],
title="Forecasted Power Generation Comparison",
labels={
"power_kw": "Forecast with selected inverter type",
"power_kw": f"Forecast with {inverter_type}",
"power_kw_no_live_pv": "Forecast without recent PV data"
}
)
Expand Down
1 change: 0 additions & 1 deletion examples/example.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,5 @@ def main():
print(predictions_df)
print(f"Max: {predictions_df['power_kw'].max()}")


if __name__ == "__main__":
main()
4 changes: 2 additions & 2 deletions examples/inverter_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ def main(save_outputs: bool = False):
timestamp_str = datetime.fromtimestamp(timestamp, tz=timezone.utc).strftime('%Y-%m-%d %H:%M:%S')
ts = pd.to_datetime(timestamp_str)

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

# make input data with nan data
site_no_live = PVSite(latitude=51.75, longitude=-1.25, capacity_kwp=1.25)
Expand Down
39 changes: 17 additions & 22 deletions quartz_solar_forecast/data.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,26 @@
""" Function to get NWP data and create fake PV dataset"""
import json
import ssl
from datetime import datetime
import os

import numpy as np
import pandas as pd
import requests
import xarray as xr

import openmeteo_requests
import requests_cache
import asyncio

from retry_requests import retry
from typing import Optional

from quartz_solar_forecast.pydantic_models import PVSite
from quartz_solar_forecast.inverters.enphase import get_enphase_data
from quartz_solar_forecast.inverters.solaredge import get_site_coordinates, get_site_list, get_solaredge_data
from quartz_solar_forecast.inverters.solis import get_solis_data

ssl._create_default_https_context = ssl._create_unverified_context

from dotenv import load_dotenv

system_id = os.getenv('ENPHASE_SYSTEM_ID')
load_dotenv()

def get_nwp(site: PVSite, ts: datetime, nwp_source: str = "icon") -> xr.Dataset:
"""
Expand Down Expand Up @@ -143,7 +142,7 @@ def format_nwp_data(df: pd.DataFrame, nwp_source:str, site: PVSite):
)
return data_xr

def process_pv_data(live_generation_kw: pd.DataFrame, ts: pd.Timestamp, site: PVSite) -> xr.Dataset:
def process_pv_data(live_generation_kw: Optional[pd.DataFrame], ts: pd.Timestamp, site: PVSite) -> xr.Dataset:
"""
Process PV data and create an xarray Dataset.
Expand All @@ -152,7 +151,7 @@ def process_pv_data(live_generation_kw: pd.DataFrame, ts: pd.Timestamp, site: PV
:param site: PV site information
:return: xarray Dataset containing processed PV data
"""
if live_generation_kw is not None:
if live_generation_kw is not None and not live_generation_kw.empty:
# get the most recent data
recent_pv_data = live_generation_kw[live_generation_kw['timestamp'] <= ts]
power_kw = np.array([np.array(recent_pv_data["power_kw"].values, dtype=np.float64)])
Expand Down Expand Up @@ -181,7 +180,7 @@ def process_pv_data(live_generation_kw: pd.DataFrame, ts: pd.Timestamp, site: PV

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

# Check if the site has an inverter type specified
if site.inverter_type == 'solaredge':
# Fetch the list of site IDs associated with the SolarEdge account
site_ids = get_site_list()
# Find the site ID that matches the site's latitude and longitude
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)]
if not matching_site_ids:
raise ValueError("Site not found in the list of associated sites.")
elif len(matching_site_ids) > 1:
raise ValueError("Multiple sites found matching the given latitude and longitude.")
if site.inverter_type == 'enphase':
system_id = os.getenv('ENPHASE_SYSTEM_ID')
if system_id:
live_generation_kw = get_enphase_data(system_id)
else:
site_id = matching_site_ids[0]
live_generation_kw = get_solaredge_data(site_id)
elif site.inverter_type == 'enphase':
live_generation_kw = get_enphase_data(system_id)
print("Error: Enphase inverter ID is not provided in the environment variables.")
elif site.inverter_type == 'solis':
live_generation_kw = asyncio.run(get_solis_data())
if live_generation_kw is None:
print("Error: Failed to retrieve Solis inverter data.")
else:
# If no inverter type is specified or not recognized, set live_generation_kw to None
live_generation_kw = None
Expand Down
2 changes: 1 addition & 1 deletion quartz_solar_forecast/inverters/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,5 @@ Open-Source-Quartz-Solar-Forecast/
4. Install the requirements by entering `pip install -r requirements.txt` and `pip install -e .`
5. Install `plotly` by entering `pip install plotly`
6. Create a `.env` file in the root directory, i.e. `Open-Source-Quartz-Solar-Forecast`
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
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
8. Run the `inverter_example.py` file by entering `python examples/inverter_example.py`
70 changes: 0 additions & 70 deletions quartz_solar_forecast/inverters/solaredge.py

This file was deleted.

Loading

0 comments on commit 7989247

Please sign in to comment.