1
1
import http .client
2
2
import os
3
+ from typing import Optional
4
+
3
5
import pandas as pd
4
6
import json
5
7
import base64
6
8
from datetime import datetime , timedelta , timezone
7
9
8
- from dotenv import load_dotenv
10
+ from urllib .parse import urlencode
11
+
12
+ from quartz_solar_forecast .inverters .inverter import AbstractInverter
13
+ from pydantic import Field
14
+ from pydantic_settings import BaseSettings , SettingsConfigDict
9
15
10
- load_dotenv ()
11
16
12
- from urllib .parse import urlencode
17
+ class EnphaseSettings (BaseSettings ):
18
+ model_config = SettingsConfigDict (env_file = '.env' , extra = 'ignore' )
19
+
20
+ client_id : str = Field (alias = "ENPHASE_CLIENT_ID" )
21
+ system_id : str = Field (alias = "ENPHASE_SYSTEM_ID" )
22
+ api_key : str = Field (alias = "ENPHASE_API_KEY" )
23
+ client_secret : str = Field (alias = "ENPHASE_CLIENT_SECRET" )
24
+
25
+
26
+ class EnphaseInverter (AbstractInverter ):
13
27
14
- def get_enphase_auth_url ():
28
+ def __init__ (self , settings : EnphaseSettings ):
29
+ self .__settings = settings
30
+
31
+ def get_data (self , ts : pd .Timestamp ) -> Optional [pd .DataFrame ]:
32
+ return get_enphase_data (self .__settings )
33
+
34
+
35
+ def get_enphase_auth_url (settings : Optional [EnphaseSettings ] = None ):
15
36
"""
16
37
Generate the authorization URL for the Enphase API.
17
38
18
- :param None
39
+ :param settings: the Enphase settings
19
40
:return: Authentication URL
20
41
"""
21
- client_id = os .getenv ('ENPHASE_CLIENT_ID' )
42
+ if settings is None :
43
+ # Because this uses env variables we don't want to set it as a default argument, otherwise it will be evaluated
44
+ # even if the method is not called
45
+ settings = EnphaseSettings ()
46
+
47
+ client_id = settings .client_id
22
48
23
49
redirect_uri = (
24
50
"https://api.enphaseenergy.com/oauth/redirect_uri" # Or your own redirect URI
@@ -51,17 +77,23 @@ def get_enphase_authorization_code(auth_url):
51
77
return code
52
78
53
79
54
- def get_enphase_access_token (auth_code = None ):
80
+ def get_enphase_access_token (auth_code : Optional [ str ] = None , settings : Optional [ EnphaseSettings ] = None ):
55
81
"""
56
82
Obtain an access token for the Enphase API using the Authorization Code Grant flow.
57
83
:param auth_code: Optional authorization code. If not provided, it will be obtained.
84
+ :param settings: Optional Enphase settings
58
85
:return: Access Token
59
86
"""
60
- client_id = os .getenv ('ENPHASE_CLIENT_ID' )
61
- client_secret = os .getenv ('ENPHASE_CLIENT_SECRET' )
87
+ if settings is None :
88
+ # Because this uses env variables we don't want to set it as a default argument, otherwise it will be evaluated
89
+ # even if the method is not called
90
+ settings = EnphaseSettings ()
91
+
92
+ client_id = settings .client_id
93
+ client_secret = settings .client_secret
62
94
63
95
if auth_code is None :
64
- auth_url = get_enphase_auth_url ()
96
+ auth_url = get_enphase_auth_url (settings )
65
97
auth_code = get_enphase_authorization_code (auth_url )
66
98
67
99
credentials = f"{ client_id } :{ client_secret } "
@@ -90,7 +122,7 @@ def get_enphase_access_token(auth_code=None):
90
122
return access_token
91
123
92
124
93
- def process_enphase_data (data_json : dict , start_at : int ) -> pd .DataFrame :
125
+ def process_enphase_data (data_json : dict , start_at : int ) -> pd .DataFrame :
94
126
# Check if 'intervals' key exists in the response
95
127
if 'intervals' not in data_json :
96
128
return pd .DataFrame (columns = ["timestamp" , "power_kw" ])
@@ -106,7 +138,7 @@ def process_enphase_data(data_json: dict, start_at: int) -> pd.DataFrame:
106
138
timestamp = datetime .fromtimestamp (end_at , tz = timezone .utc ).strftime ('%Y-%m-%d %H:%M:%S' )
107
139
108
140
# Append the data to the list
109
- data_list .append ({"timestamp" : timestamp , "power_kw" : interval ['powr' ]/ 1000 })
141
+ data_list .append ({"timestamp" : timestamp , "power_kw" : interval ['powr' ] / 1000 })
110
142
111
143
# Convert the list to a DataFrame
112
144
live_generation_kw = pd .DataFrame (data_list )
@@ -120,18 +152,19 @@ def process_enphase_data(data_json: dict, start_at: int) -> pd.DataFrame:
120
152
121
153
return live_generation_kw
122
154
123
- def get_enphase_data (enphase_system_id : str ) -> pd .DataFrame :
155
+
156
+ def get_enphase_data (settings : EnphaseSettings ) -> pd .DataFrame :
124
157
"""
125
158
Get live PV generation data from Enphase API v4
159
+ :param settings: the Enphase settings
126
160
:param enphase_system_id: System ID for Enphase API
127
161
:return: Live PV generation in Watt-hours, assumes to be a floating-point number
128
162
"""
129
- api_key = os .getenv ('ENPHASE_API_KEY' )
130
163
access_token = os .getenv ('ENPHASE_ACCESS_TOKEN' )
131
164
132
165
# If access token is not in environment variables, get a new one
133
166
if not access_token :
134
- access_token = get_enphase_access_token ()
167
+ access_token = get_enphase_access_token (settings = settings )
135
168
136
169
# Set the start time to 1 week ago
137
170
start_at = int ((datetime .now () - timedelta (weeks = 1 )).timestamp ())
@@ -142,11 +175,11 @@ def get_enphase_data(enphase_system_id: str) -> pd.DataFrame:
142
175
conn = http .client .HTTPSConnection ("api.enphaseenergy.com" )
143
176
headers = {
144
177
"Authorization" : f"Bearer { access_token } " ,
145
- "key" : api_key
178
+ "key" : settings . api_key
146
179
}
147
180
148
181
# Add the system_id and duration parameters to the URL
149
- url = f"/api/v4/systems/{ enphase_system_id } /telemetry/production_micro?start_at={ start_at } &granularity={ granularity } "
182
+ url = f"/api/v4/systems/{ settings . system_id } /telemetry/production_micro?start_at={ start_at } &granularity={ granularity } "
150
183
conn .request ("GET" , url , headers = headers )
151
184
152
185
res = conn .getresponse ()
@@ -161,4 +194,4 @@ def get_enphase_data(enphase_system_id: str) -> pd.DataFrame:
161
194
# Process the data using the new function
162
195
live_generation_kw = process_enphase_data (data_json , start_at )
163
196
164
- return live_generation_kw
197
+ return live_generation_kw
0 commit comments