From b8dafdda3fc85452e07aa40ee2c9fd46cb397b64 Mon Sep 17 00:00:00 2001 From: Aryan Bhosale <36108149+aryanbhosale@users.noreply.github.com> Date: Mon, 29 Jul 2024 16:32:47 +0530 Subject: [PATCH] givenergy (#165) * givenergy * run fc update * latest inv * refactor * use serial number from api --- .env.example | 5 +- dashboards/dashboard_2/app.py | 33 +++++--- examples/inverter_example.py | 4 +- quartz_solar_forecast/data.py | 6 ++ quartz_solar_forecast/inverters/givenergy.py | 79 ++++++++++++++++++++ quartz_solar_forecast/pydantic_models.py | 2 +- requirements.txt | 3 +- 7 files changed, 116 insertions(+), 16 deletions(-) create mode 100644 quartz_solar_forecast/inverters/givenergy.py diff --git a/.env.example b/.env.example index f181c34e..ec52b660 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,4 @@ # User needs to add their Enphase API details - ENPHASE_SYSTEM_ID = 'user_enphase_system_id' ENPHASE_CLIENT_ID = 'user_enphase_client_id' ENPHASE_CLIENT_SECRET = 'user_enphase_client_secret' @@ -8,12 +7,14 @@ ENPHASE_API_KEY = 'user_enphase_api_key' AUTHORIZATION_URL = 'https://api.enphaseenergy.com/oauth/authorize?response_type=code&client_id=ENPHASE_CLIENT_ID' # User needs to add their Solis Cloud API details - 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' +# User needs to add their GivEnergy API details +GIVENERGY_API_KEY = 'user_givenergy_api_key' + # This section is for OpenMeteo setup # Docker is used to fetch and store OpenMeteo's open data, targeting temperature_2m, precipitation, diff --git a/dashboards/dashboard_2/app.py b/dashboards/dashboard_2/app.py index d6486a53..78d529f7 100644 --- a/dashboards/dashboard_2/app.py +++ b/dashboards/dashboard_2/app.py @@ -20,6 +20,7 @@ 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 +from quartz_solar_forecast.inverters.givenergy import get_givenergy_data # Load environment variables load_dotenv() @@ -146,7 +147,8 @@ def make_pv_data( ts: pd.Timestamp, access_token: str = None, enphase_system_id: str = None, - solis_data: pd.DataFrame = None + solis_data: pd.DataFrame = None, + givenergy_data: pd.DataFrame = None ) -> xr.Dataset: live_generation_kw = None @@ -154,6 +156,8 @@ def make_pv_data( 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 + elif site.inverter_type == "givenergy" and givenergy_data is not None: + live_generation_kw = givenergy_data da = process_pv_data(live_generation_kw, ts, site) return da @@ -165,7 +169,8 @@ def predict_ocf( nwp_source: str = "icon", access_token: str = None, enphase_system_id: str = None, - solis_data: pd.DataFrame = None + solis_data: pd.DataFrame = None, + givenergy_data: pd.DataFrame = None ): if ts is None: ts = pd.Timestamp.now().round("15min") @@ -174,7 +179,8 @@ 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, solis_data=solis_data + site=site, ts=ts, access_token=access_token, enphase_system_id=enphase_system_id, + solis_data=solis_data, givenergy_data=givenergy_data ) pred_df = forecast_v1_tilt_orientation(nwp_source, nwp_xr, pv_xr, ts, model=model) @@ -187,10 +193,11 @@ def run_forecast( nwp_source: str = "icon", access_token: str = None, enphase_system_id: str = None, - solis_data: pd.DataFrame = None + solis_data: pd.DataFrame = None, + givenergy_data: pd.DataFrame = None ) -> pd.DataFrame: if model == "gb": - return predict_ocf(site, None, ts, nwp_source, access_token, enphase_system_id, solis_data) + return predict_ocf(site, None, ts, nwp_source, access_token, enphase_system_id, solis_data, givenergy_data) elif model == "xgb": return predict_tryolabs(site, ts) else: @@ -200,7 +207,8 @@ def fetch_data_and_run_forecast( site: PVSite, access_token: str = None, enphase_system_id: str = None, - solis_data: pd.DataFrame = None + solis_data: pd.DataFrame = None, + givenergy_data: pd.DataFrame = None ): with st.spinner("Running forecast..."): try: @@ -216,7 +224,8 @@ def fetch_data_and_run_forecast( ts=ts, access_token=access_token, enphase_system_id=enphase_system_id, - solis_data=solis_data + solis_data=solis_data, + givenergy_data=givenergy_data ) # Create a site without inverter for comparison @@ -255,11 +264,12 @@ 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", "Solis"]) +inverter_type = st.sidebar.selectbox("Select Inverter", ["No Inverter", "Enphase", "Solis", "GivEnergy"]) access_token = None enphase_system_id = None solis_data = None +givenergy_data = None if inverter_type == "Enphase": if "access_token" not in st.session_state: @@ -268,8 +278,6 @@ 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): @@ -295,6 +303,11 @@ def fetch_data_and_run_forecast( predictions_df, ts = fetch_data_and_run_forecast( site, solis_data=solis_df ) + elif inverter_type == "GivEnergy": + givenergy_df = get_givenergy_data() + predictions_df, ts = fetch_data_and_run_forecast( + site, givenergy_data=givenergy_df + ) else: predictions_df, ts = fetch_data_and_run_forecast(site) diff --git a/examples/inverter_example.py b/examples/inverter_example.py index af38932e..21c0dd1b 100644 --- a/examples/inverter_example.py +++ b/examples/inverter_example.py @@ -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 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 live enphase, solis, or givenergy data + site_live = PVSite(latitude=51.75, longitude=-1.25, capacity_kwp=1.25, inverter_type="givenergy") # inverter_type="enphase", "solis", or "givenergy" # make input data with nan data site_no_live = PVSite(latitude=51.75, longitude=-1.25, capacity_kwp=1.25) diff --git a/quartz_solar_forecast/data.py b/quartz_solar_forecast/data.py index 5ec54892..38b8b1d3 100644 --- a/quartz_solar_forecast/data.py +++ b/quartz_solar_forecast/data.py @@ -15,6 +15,7 @@ from quartz_solar_forecast.pydantic_models import PVSite from quartz_solar_forecast.inverters.enphase import get_enphase_data from quartz_solar_forecast.inverters.solis import get_solis_data +from quartz_solar_forecast.inverters.givenergy import get_givenergy_data ssl._create_default_https_context = ssl._create_unverified_context @@ -200,6 +201,11 @@ def make_pv_data(site: PVSite, ts: pd.Timestamp) -> xr.Dataset: live_generation_kw = asyncio.run(get_solis_data()) if live_generation_kw is None: print("Error: Failed to retrieve Solis inverter data.") + elif site.inverter_type == 'givenergy': + try: + live_generation_kw = get_givenergy_data() + except Exception as e: + print(f"Error retrieving GivEnergy data: {str(e)}") else: # If no inverter type is specified or not recognized, set live_generation_kw to None live_generation_kw = None diff --git a/quartz_solar_forecast/inverters/givenergy.py b/quartz_solar_forecast/inverters/givenergy.py new file mode 100644 index 00000000..e289b44b --- /dev/null +++ b/quartz_solar_forecast/inverters/givenergy.py @@ -0,0 +1,79 @@ +import os +import requests +import pandas as pd +from datetime import datetime +from dotenv import load_dotenv + +# Load environment variables +load_dotenv() + +def get_inverter_serial_number(): + """ + Fetch the inverter serial number from the GivEnergy communication device API. + + :return: Inverter serial number as a string + """ + api_key = os.getenv('GIVENERGY_API_KEY') + + if not api_key: + raise ValueError("GIVENERGY_API_KEY not set in environment variables") + + url = 'https://api.givenergy.cloud/v1/communication-device' + + headers = { + 'Authorization': f'Bearer {api_key}', + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + + response = requests.get(url, headers=headers) + + if response.status_code != 200: + raise Exception(f"Communication device API request failed with status code {response.status_code}") + + data = response.json()['data'] + if not data: + raise ValueError("No communication devices found") + + inverter_serial_number = data[0]['inverter']['serial'] + return inverter_serial_number + +def get_givenergy_data(): + """ + Fetch the latest data from the GivEnergy API and return a DataFrame. + + :return: DataFrame with timestamp and power_kw columns + """ + api_key = os.getenv('GIVENERGY_API_KEY') + + if not api_key: + raise ValueError("GIVENERGY_API_KEY not set in environment variables") + + inverter_serial_number = get_inverter_serial_number() + + url = f'https://api.givenergy.cloud/v1/inverter/{inverter_serial_number}/system-data/latest' + + headers = { + 'Authorization': f'Bearer {api_key}', + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + + response = requests.get(url, headers=headers) + + if response.status_code != 200: + raise Exception(f"System data API request failed with status code {response.status_code}") + + data = response.json()['data'] + + # Process the data + timestamp = datetime.strptime(data['time'], "%Y-%m-%dT%H:%M:%SZ") + power_kw = data['solar']['power'] / 1000 # Convert W to kW + + # Create DataFrame + df = pd.DataFrame({ + 'timestamp': [timestamp], + 'power_kw': [power_kw] + }) + print(df) + return df \ No newline at end of file diff --git a/quartz_solar_forecast/pydantic_models.py b/quartz_solar_forecast/pydantic_models.py index 71e801d3..0861b43c 100644 --- a/quartz_solar_forecast/pydantic_models.py +++ b/quartz_solar_forecast/pydantic_models.py @@ -22,5 +22,5 @@ class PVSite(BaseModel): inverter_type: str = Field( default=None, description="The type of inverter used", - json_schema_extra=["enphase", "solis", None], + json_schema_extra=["enphase", "solis", "givenergy", None], ) diff --git a/requirements.txt b/requirements.txt index 586673b2..dbe45550 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,4 +11,5 @@ gdown==5.1.0 xgboost==2.0.3 plotly typer -streamlit \ No newline at end of file +streamlit +async_timeout \ No newline at end of file