Skip to content

Commit 0492414

Browse files
authored
Add JWT access to api (#41)
* add jwt header option * update readme for jwt * fixes for jwt * change nodeType to int * add refresh token * switch response error to warning * lint
1 parent 32109b0 commit 0492414

7 files changed

Lines changed: 133 additions & 22 deletions

File tree

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
11
# Fronius Solar.web
22

3-
Python client for the Fronius [Solar.web API](https://www.fronius.com/~/downloads/Solar%20Energy/User%20Information/SE_UI_API_InterfaceDocumentation_EN.pdf).
3+
Python client for the Fronius [Solar.web API documentation](https://www.fronius.com/~/downloads/Solar%20Energy/User%20Information/SE_UI_API_InterfaceDocumentation_EN.pdf).
4+
Fronius [test site](https://api.solarweb.com/swqapi/index.html).
45

56
## Features
67

78
- Talks to your Fronius Solar.web PV system via Cloud API
89
- Automatic retries with exponential backoff
910
- Optionally pass in a `httpx` client
11+
- If a login and password is provided login with a bearer token can be used
1012

1113
## Usage
1214

13-
Although intended as a library [`fronius_sw_example.py`](https://github.com/drc38/python-fronius-web/blob/main/examples/fronius_sw_example.py) is provided for testing purposes.
15+
Although intended as a library [`fronius_key_example.py`](https://github.com/drc38/python-fronius-web/blob/main/examples/fronius_key_example.py) is provided for testing purposes.
1416

1517
Authentication and PV system id for the example is provided via environment variables, e.g. on nix systems:
1618

1719
```
1820
export ACCESS_KEY_ID=FKIAFEF58CFEFA94486F9C804CF6077A01AB
1921
export ACCESS_KEY_VALUE=47c076bc-23e5-4949-37a6-4bcfcf8d21d6
22+
export LOGIN_NAME=abc@email.com
23+
export LOGIN_PASSWORD=xxxxx
2024
export PV_SYSTEM_ID=20bb600e-019b-4e03-9df3-a0a900cda689
2125
```
2.93 KB
Binary file not shown.

examples/fronius_login_example.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import asyncio
2+
from pydantic import SecretStr
3+
from pydantic_settings import BaseSettings
4+
from fronius_solarweb.api import Fronius_Solarweb
5+
6+
7+
class AuthDetails(BaseSettings):
8+
ACCESS_KEY_ID: SecretStr
9+
ACCESS_KEY_VALUE: SecretStr
10+
LOGIN_NAME: SecretStr
11+
LOGIN_PASSWORD: SecretStr
12+
PV_SYSTEM_ID: str
13+
14+
15+
async def main():
16+
creds = AuthDetails()
17+
fronius = Fronius_Solarweb(
18+
access_key_id=creds.ACCESS_KEY_ID.get_secret_value(),
19+
access_key_value=creds.ACCESS_KEY_VALUE.get_secret_value(),
20+
login_name=creds.LOGIN_NAME.get_secret_value(),
21+
login_password=creds.LOGIN_PASSWORD.get_secret_value(),
22+
pv_system_id=creds.PV_SYSTEM_ID,
23+
)
24+
await fronius.login()
25+
26+
print("Getting SolarWeb api release info:")
27+
release_info = await fronius.get_api_release_info()
28+
print(f"{release_info}\n")
29+
30+
print("Getting PV systems meta data:")
31+
pv_systems_data = await fronius.get_pvsystems_meta_data()
32+
print(f"{pv_systems_data}\n")
33+
34+
print(f"Getting PV system meta data for {creds.PV_SYSTEM_ID}:")
35+
pv_system_data = await fronius.get_pvsystem_meta_data()
36+
print(f"{pv_system_data}\n")
37+
38+
print("Getting Devices meta data:")
39+
devices_data = await fronius.get_devices_meta_data()
40+
print(f"{devices_data}\n")
41+
42+
print("Getting power flow data:")
43+
flow_data = await fronius.get_system_flow_data()
44+
print(f"{flow_data}\n")
45+
46+
print("Getting aggregated V2 data:")
47+
aggr_data = await fronius.get_system_aggr_data_v2()
48+
print(f"{aggr_data}\n")
49+
50+
51+
if __name__ == "__main__":
52+
asyncio.run(main())

fronius_solarweb/api.py

Lines changed: 74 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -31,50 +31,107 @@
3131
class Fronius_Solarweb:
3232
def __init__(
3333
self,
34-
access_key_id: str,
35-
access_key_value: str,
34+
access_key_id: str = None,
35+
access_key_value: str = None,
36+
login_name: str = None,
37+
login_password: str = None,
3638
pv_system_id: str = None,
3739
httpx_client: AsyncClient = None,
3840
):
3941
"""
40-
Create an Fronius Solarweb API client.
42+
Create an Fronius Solarweb API client from either key/id or login/password.
4143
4244
:param access_key_id: unique ID for the API key, e.g.
4345
"FKIAFEF58CFEFA94486F9C804CF6077A01AB". Access keys are 36
4446
characters long and start with the "FKIA" prefix.
4547
:param access_key_value: A secret value (GUID),
4648
e.g. "47c076bc-23e5-4949-37a6-4bcfcf8d21d6", which
4749
you need to know for authorization of API calls.
50+
:param login_name: Solar.web email / login name.
51+
:param login_password: Solar.web password.
4852
:param pv_system_id (optional): Unique PV system ID,
4953
this can be provided or determined from a call to
5054
get_pvsystems_meta_data()
5155
:param httpx_client (optional)
5256
"""
5357
self.access_key_id = access_key_id
5458
self.access_key_value = access_key_value
59+
self.login_name = login_name
60+
self.login_password = login_password
5561
self.pv_system_id = pv_system_id
5662
self.httpx_client = httpx_client or AsyncClient()
57-
58-
@property
59-
def _common_headers(self):
60-
return {
61-
"Accept": "application/json",
63+
self.jwt_data: dict = None
64+
self._jwt_base_header = {
65+
"Content-Type": "application/json-patch+json",
6266
"AccessKeyId": self.access_key_id,
6367
"AccessKeyValue": self.access_key_value,
68+
"Accept": "application/json",
69+
# "Accept-Language": "de-de",
70+
"User-Agent": "Solar.web/921 CFNetwork/1410.0.3 Darwin/22.6.0",
6471
}
6572

73+
@property
74+
def _common_headers(self):
75+
if self._jwt_headers.get("Authorization"):
76+
return self._jwt_headers
77+
else:
78+
return {
79+
"Accept": "application/json",
80+
"AccessKeyId": self.access_key_id,
81+
"AccessKeyValue": self.access_key_value,
82+
}
83+
84+
@property
85+
def _jwt_headers(self):
86+
return self._jwt_base_header
87+
88+
@_jwt_headers.setter
89+
def _jwt_headers(self, new: dict):
90+
return self._jwt_base_header.update(new)
91+
92+
def _jwt_del_header(self, key: str):
93+
self._jwt_base_header.pop(key, None)
94+
6695
async def _check_api_response(self, response):
6796
if response.status_code == 401:
68-
_LOGGER.debug("Access unauthorised check solar.web access key values")
97+
_LOGGER.warning(
98+
"Access unauthorised check solar.web access key values or login password"
99+
)
69100
raise NotAuthorizedException()
70101
if response.status_code == 404:
71-
_LOGGER.debug("Item not found check your PV system ID")
102+
_LOGGER.warning("Item not found check your PV system ID")
72103
raise NotFoundException()
73104

74105
response.raise_for_status()
75106
# returns dict type not string
76107
return response.json()
77108

109+
async def login(self):
110+
self._jwt_del_header("Authorization")
111+
_LOGGER.debug("Obtaining JSON web token")
112+
r = await self.httpx_client.post(
113+
f"{SW_BASE_URL}/iam/jwt",
114+
headers=self._jwt_headers,
115+
json={
116+
"userId": self.login_name,
117+
"password": self.login_password,
118+
},
119+
)
120+
self.jwt_data = await self._check_api_response(r)
121+
_LOGGER.debug(f"JWT data returned: {self.jwt_data}")
122+
self._jwt_headers = {"Authorization": "Bearer " + self.jwt_data.get("jwtToken")}
123+
124+
async def refresh_token(self, token: str = None):
125+
refresh = self.jwt_data.get("refreshToken", token)
126+
self._jwt_del_header("Authorization")
127+
_LOGGER.debug(f"Obtaining JSON web token using refresh token: {refresh}")
128+
r = await self.httpx_client.patch(
129+
f"{SW_BASE_URL}/iam/jwt/{refresh}", headers={"accept": "application/json"}
130+
)
131+
self.jwt_data = await self._check_api_response(r)
132+
_LOGGER.debug(f"JWT data returned: {self.jwt_data}")
133+
self._jwt_headers = {"Authorization": "Bearer " + self.jwt_data.get("jwtToken")}
134+
78135
@retry(
79136
wait=wait_random_exponential(multiplier=2, max=60),
80137
retry=retry_if_not_exception_type(
@@ -93,7 +150,7 @@ async def get_api_release_info(self) -> ReleaseInfo:
93150
model_data = ReleaseInfo.model_validate(json_data)
94151
except ValidationError as e:
95152
_LOGGER.error(
96-
f"Unable to validate data receieved from SolarWeb api: '{json_data}'"
153+
f"Unable to validate data receieved from SolarWeb api: {json_data}"
97154
)
98155
_LOGGER.error(e)
99156
return model_data
@@ -116,7 +173,7 @@ async def get_pvsystems_meta_data(self) -> list[PvSystemMetaData]:
116173
model_data = PvSystemsMetaData.model_validate(json_data).pvSystems
117174
except ValidationError as e:
118175
_LOGGER.error(
119-
f"Unable to validate data receieved from SolarWeb api: '{json_data}'"
176+
f"Unable to validate data receieved from SolarWeb api: {json_data}"
120177
)
121178
_LOGGER.error(e)
122179
return model_data
@@ -139,7 +196,7 @@ async def get_pvsystem_meta_data(self) -> PvSystemMetaData:
139196
model_data = PvSystemMetaData.model_validate(json_data)
140197
except ValidationError as e:
141198
_LOGGER.error(
142-
f"Unable to validate data receieved from SolarWeb api: '{json_data}'"
199+
f"Unable to validate data receieved from SolarWeb api: {json_data}"
143200
)
144201
_LOGGER.error(e)
145202
return model_data
@@ -162,7 +219,7 @@ async def get_devices_meta_data(self) -> list[DeviceMetaData]:
162219
model_data = DevicesMetaData.model_validate(json_data).devices
163220
except ValidationError as e:
164221
_LOGGER.error(
165-
f"Unable to validate data receieved from SolarWeb api: '{json_data}'"
222+
f"Unable to validate data receieved from SolarWeb api: {json_data}"
166223
)
167224
_LOGGER.error(e)
168225
return model_data
@@ -185,7 +242,7 @@ async def get_system_flow_data(self, tz: str = "zulu") -> PvSystemFlowData:
185242
model_data = PvSystemFlowData.model_validate(json_data)
186243
except ValidationError as e:
187244
_LOGGER.error(
188-
f"Unable to validate data receieved from SolarWeb api: '{json_data}'"
245+
f"Unable to validate data receieved from SolarWeb api: {json_data}"
189246
)
190247
_LOGGER.error(e)
191248
return model_data
@@ -213,7 +270,7 @@ async def get_system_aggr_data_v2(
213270
model_data = PvSystemAggrDataV2.model_validate(json_data)
214271
except ValidationError as e:
215272
_LOGGER.error(
216-
f"Unable to validate data receieved from SolarWeb api: '{json_data}'"
273+
f"Unable to validate data receieved from SolarWeb api: {json_data}"
217274
)
218275
_LOGGER.error(e)
219276
return model_data
@@ -241,7 +298,7 @@ async def get_hist_data(
241298
model_data = HistoricalValues.model_validate(json_data)
242299
except ValidationError as e:
243300
_LOGGER.error(
244-
f"Unable to validate data receieved from SolarWeb api: '{json_data}'"
301+
f"Unable to validate data receieved from SolarWeb api: {json_data}"
245302
)
246303
_LOGGER.error(e)
247304
return model_data

fronius_solarweb/schema/device.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class DeviceMetaData(BaseModel):
4343
serialnumber: Optional[str] = None
4444
deviceTypeDetails: Optional[str] = None
4545
dataloggerId: Optional[str] = None
46-
nodeType: Optional[str] = None
46+
nodeType: Optional[int] = None
4747
numberMPPTrackers: Optional[int] = None
4848
numberPhases: Optional[int] = None
4949
peakPower: Optional[Power] = None

fronius_solarweb/schema/error.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
from datetime import datetime
2-
31
from pydantic import BaseModel
42

53

0 commit comments

Comments
 (0)