From 0c079647d2ac95c178f91090fa2293ff59c86c88 Mon Sep 17 00:00:00 2001 From: drc38 <20024196+drc38@users.noreply.github.com> Date: Wed, 12 Feb 2025 20:54:59 +0000 Subject: [PATCH 1/7] add jwt header option --- .../fronius_login_example.cpython-312.pyc | Bin 0 -> 2760 bytes ...s_sw_example.py => fronius_key_example.py} | 0 examples/fronius_login_example.py | 48 +++++++++++ fronius_solarweb/api.py | 78 ++++++++++++++---- fronius_solarweb/schema/error.py | 2 - 5 files changed, 110 insertions(+), 18 deletions(-) create mode 100644 examples/__pycache__/fronius_login_example.cpython-312.pyc rename examples/{fronius_sw_example.py => fronius_key_example.py} (100%) create mode 100644 examples/fronius_login_example.py diff --git a/examples/__pycache__/fronius_login_example.cpython-312.pyc b/examples/__pycache__/fronius_login_example.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a1f14145762dfb43b5b907faa001a4dea169c3c7 GIT binary patch literal 2760 zcmb7G%Wo4$7@zTupD|9rv5jLVETND%&12osWQ@h^S$Q#%{M#q z?fkNBn+@RBy?A#vZUo?WBIpgvYFoRChns){9E^bo84WQQH$)7O@KlV7qX<%Y6f?$6 z5fcOkFabE@6~LKzbcfQyM9iG|3W!*ED`&ab3V^WF0C+H0uURW8)?4nB6nCJ;K#+!M z7_k0D#83b#)-XRU@KRV3uwWl!MLx_+QZz9uV!19TBoonjkqIYbtgy(RpDu+{hth(; zT00hR;zdLt2O@^kfTwuG!C|abIfKd>v5q2K8U>6>&FH*zVVsxPXiQXW$4^ci3o)V5 z6Gs(?#!rog!)H&P8dvO7XPEH0@ViG(FvrG20h3~3m;@W=8Ah=&OgzcW#|Up{m=EUJ zm}U{k%9L7$VH1g@#7fa*Lc~6lC<#A+HEaXxN4gf1!bLH~PV-_{DlJ_|COT%cWIJ%4 zUHpe^JQc&Xl9GufXQK(Gr0z_m1v3t#62gdN`#%T8I&U6%i}j6p)L7i%Dj=7zU3aV6 zI&vm1evSlR~5!GtX zEU>Y8Ua%4yInatx-3Y~)5~2x7sVAN|SEiIJLvmF-M4MVDrO1#J8Iq#nt~6BWT}01m z300`o=%rmI$!I~;=p`*lN7XV5Jpqei#6KubRLrGPtk|oTRr~6jiCQGbD}^T8^(Cl_ zwU!~RuW&@NL3F7JJIIcpI2%m}ZI~q$PrQn&^Pk{F20XQUuARsned=f`I(N#>Jvrwd z+1ZF~kTq-TETFEUmzKSKId5Oy+b=u&GsmAI>&km8plq8!Mo9ioRyqcPQsO zl=ltEuAxkb%zQs<_um@J+IAHXtzzsepcZv82Xo$odGCPi9C-a=-YTHIMR%j@?#a1( z^6p-_t~Yaxggm!0c|*wB8Vbm#MrfhW zRSWQpyuj2wOlbWfLbvy+bYNZl7=Mq?M)w7K;JsEDY)1D2&6vMm1A}|feS0_IhheZ4 z-9J2n`3FNV*oGbq6aB+>7;HxmI|%=%5e7Taqb9;XcEDgadR$vi_`Q%&92wI#mEu{L z?^J0wq2vuhjt9d8Y7{dorW4cAq+&^>IW{3hr?uzDc3s4GPJ2)Zn{qi^rq^> zH%(BlsrsZKM^30Ao|G`35bB7iJ|f7mT(=BsPo5Fs02axUK^((mm4Xod1{}{o(=*Wa z3> ReleaseInfo: model_data = ReleaseInfo.model_validate(json_data) except ValidationError as e: _LOGGER.error( - f"Unable to validate data receieved from SolarWeb api: '{json_data}'" + f"Unable to validate data receieved from SolarWeb api: {json_data}" ) _LOGGER.error(e) return model_data @@ -116,7 +162,7 @@ async def get_pvsystems_meta_data(self) -> list[PvSystemMetaData]: model_data = PvSystemsMetaData.model_validate(json_data).pvSystems except ValidationError as e: _LOGGER.error( - f"Unable to validate data receieved from SolarWeb api: '{json_data}'" + f"Unable to validate data receieved from SolarWeb api: {json_data}" ) _LOGGER.error(e) return model_data @@ -139,7 +185,7 @@ async def get_pvsystem_meta_data(self) -> PvSystemMetaData: model_data = PvSystemMetaData.model_validate(json_data) except ValidationError as e: _LOGGER.error( - f"Unable to validate data receieved from SolarWeb api: '{json_data}'" + f"Unable to validate data receieved from SolarWeb api: {json_data}" ) _LOGGER.error(e) return model_data @@ -162,7 +208,7 @@ async def get_devices_meta_data(self) -> list[DeviceMetaData]: model_data = DevicesMetaData.model_validate(json_data).devices except ValidationError as e: _LOGGER.error( - f"Unable to validate data receieved from SolarWeb api: '{json_data}'" + f"Unable to validate data receieved from SolarWeb api: {json_data}" ) _LOGGER.error(e) return model_data @@ -185,7 +231,7 @@ async def get_system_flow_data(self, tz: str = "zulu") -> PvSystemFlowData: model_data = PvSystemFlowData.model_validate(json_data) except ValidationError as e: _LOGGER.error( - f"Unable to validate data receieved from SolarWeb api: '{json_data}'" + f"Unable to validate data receieved from SolarWeb api: {json_data}" ) _LOGGER.error(e) return model_data @@ -213,7 +259,7 @@ async def get_system_aggr_data_v2( model_data = PvSystemAggrDataV2.model_validate(json_data) except ValidationError as e: _LOGGER.error( - f"Unable to validate data receieved from SolarWeb api: '{json_data}'" + f"Unable to validate data receieved from SolarWeb api: {json_data}" ) _LOGGER.error(e) return model_data @@ -241,7 +287,7 @@ async def get_hist_data( model_data = HistoricalValues.model_validate(json_data) except ValidationError as e: _LOGGER.error( - f"Unable to validate data receieved from SolarWeb api: '{json_data}'" + f"Unable to validate data receieved from SolarWeb api: {json_data}" ) _LOGGER.error(e) return model_data diff --git a/fronius_solarweb/schema/error.py b/fronius_solarweb/schema/error.py index 84d3b9e..b58f01f 100644 --- a/fronius_solarweb/schema/error.py +++ b/fronius_solarweb/schema/error.py @@ -1,5 +1,3 @@ -from datetime import datetime - from pydantic import BaseModel From a9fdc97b6901f5ab71d314095901897dc14e0301 Mon Sep 17 00:00:00 2001 From: drc38 <20024196+drc38@users.noreply.github.com> Date: Wed, 12 Feb 2025 21:02:30 +0000 Subject: [PATCH 2/7] update readme for jwt --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 65e42db..62af026 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,25 @@ # Fronius Solar.web -Python client for the Fronius [Solar.web API](https://www.fronius.com/~/downloads/Solar%20Energy/User%20Information/SE_UI_API_InterfaceDocumentation_EN.pdf). +Python client for the Fronius [Solar.web API documentation](https://www.fronius.com/~/downloads/Solar%20Energy/User%20Information/SE_UI_API_InterfaceDocumentation_EN.pdf). +Fronius [test site](https://api.solarweb.com/swqapi/index.html). ## Features - Talks to your Fronius Solar.web PV system via Cloud API - Automatic retries with exponential backoff - Optionally pass in a `httpx` client +- If a login and password is provided login with a bearer token can be used ## Usage -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. +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. Authentication and PV system id for the example is provided via environment variables, e.g. on nix systems: ``` export ACCESS_KEY_ID=FKIAFEF58CFEFA94486F9C804CF6077A01AB export ACCESS_KEY_VALUE=47c076bc-23e5-4949-37a6-4bcfcf8d21d6 +export LOGIN_NAME=abc@email.com +export LOGIN_PASSWORD=xxxxx export PV_SYSTEM_ID=20bb600e-019b-4e03-9df3-a0a900cda689 ``` \ No newline at end of file From 7d477bdfd9fb81be4f13ce11c34ecedab29a2fe1 Mon Sep 17 00:00:00 2001 From: drc38 <20024196+drc38@users.noreply.github.com> Date: Wed, 12 Feb 2025 23:22:05 +0000 Subject: [PATCH 3/7] fixes for jwt --- .../fronius_login_example.cpython-312.pyc | Bin 2760 -> 3004 bytes examples/fronius_login_example.py | 4 ++++ fronius_solarweb/api.py | 5 ++--- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/examples/__pycache__/fronius_login_example.cpython-312.pyc b/examples/__pycache__/fronius_login_example.cpython-312.pyc index a1f14145762dfb43b5b907faa001a4dea169c3c7..e62f4559e4f166284c0a712e0f12c6767d9eaef7 100644 GIT binary patch delta 803 zcmZva&1(~35XN_QXE(8FHfft|+$PP&P?5B0q6ZJHHKZS5l`0;Jf_R9b*=jVcut|hM ziv{uIMST$j@!-Krqo-cgLq7$32~n~v{R0$wX#D{3Ng^eY(VP@>*shUE`R2G$&J~I!+dVQi zmYJN)PGqiT&yU;QmiEfXh07U|g0)P9u0G(bu23CAmZH^Bv%G2@Rm;~?#~!QG{|WtO z7ZsTfTBc|o1RLC^`F)F=;a`#^UW0b>j6bEx2a*GL4OMZ!Spbp{5;AaQ=Gu%rPY?$M zzT8Ew3qjZ69LY2e4hVuxojbCdt2JO zxnjj2Rq-o$$y2G{VY=uQX8ZCxsRJf_lx>{J6=ux@MGwu=5`a3vUJd=~TouB1*sDH@?;y*b0eDfZI(C#_%s!chJwS4$feQGsqQMwORJFFJjM)4f>Y2ot@j z_*abklT`3kf`=5t!->71_`N3Tfe+Cg*k)(72Gn6aBHC;LO!0|XqQ&N~HUd@i+*%L3 zQ#|%QunlJ!P^zZ5h8A2PQ##XNBT_alJ#XXjy0RV;ZA^-9%&~FAh2t&(+!A-tN%0XK zAJ+}LYhnv?EJ&km@yq&fcyB`JJjYbZ>%jsr@g#TxhFDSOcU!6fH>apUAIqzyO+QUE zLQ#1|i~dmlHkISB_4+~c{;@G5PPB}sq8V&1pN4kriDh`cbi=(3dvoB<;$CW&ct-~} ziJfp(@;tb6*txezeE%-EY244teAfZj!R~_O2MKU9crYXRp#g3d56zh5NsvgL3DW~4 z7xByhvl9E7_#RqG$&r0suE6}-rXQ8h!S~Cx{%QI0W3RSRT=Z{=l5S5jxY%<3D0KY_ O+i>NyAl~XWO8tK%7>7jw diff --git a/examples/fronius_login_example.py b/examples/fronius_login_example.py index a15b502..377f751 100644 --- a/examples/fronius_login_example.py +++ b/examples/fronius_login_example.py @@ -5,6 +5,8 @@ class AuthDetails(BaseSettings): + ACCESS_KEY_ID: SecretStr + ACCESS_KEY_VALUE: SecretStr LOGIN_NAME: SecretStr LOGIN_PASSWORD: SecretStr PV_SYSTEM_ID: str @@ -13,6 +15,8 @@ class AuthDetails(BaseSettings): async def main(): creds = AuthDetails() fronius = Fronius_Solarweb( + access_key_id=creds.ACCESS_KEY_ID.get_secret_value(), + access_key_value=creds.ACCESS_KEY_VALUE.get_secret_value(), login_name=creds.LOGIN_NAME.get_secret_value(), login_password=creds.LOGIN_PASSWORD.get_secret_value(), pv_system_id=creds.PV_SYSTEM_ID, diff --git a/fronius_solarweb/api.py b/fronius_solarweb/api.py index 54853ad..1dd88cc 100644 --- a/fronius_solarweb/api.py +++ b/fronius_solarweb/api.py @@ -25,7 +25,6 @@ _LOGGER = logging.getLogger(__name__) SW_BASE_URL = "https://api.solarweb.com/swqapi" -JWT_BASE_URL = "https://swqapi.solarweb.com/iam/jwt" MAX_ATTEMPTS = 5 @@ -96,9 +95,9 @@ async def login(self): self._jwt_del_header("Authorization") _LOGGER.debug("Obtaining JSON web token") r = await self.httpx_client.post( - JWT_BASE_URL, + f"{SW_BASE_URL}/iam/jwt", headers=self._jwt_headers, - data={ + json={ "userId": self.login_name, "password": self.login_password, }, From 8e4784441b4b5e5162b227842d3e16f196fc240c Mon Sep 17 00:00:00 2001 From: drc38 <20024196+drc38@users.noreply.github.com> Date: Thu, 13 Feb 2025 08:38:01 +0000 Subject: [PATCH 4/7] change nodeType to int --- fronius_solarweb/schema/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fronius_solarweb/schema/device.py b/fronius_solarweb/schema/device.py index 045dd10..da55231 100644 --- a/fronius_solarweb/schema/device.py +++ b/fronius_solarweb/schema/device.py @@ -43,7 +43,7 @@ class DeviceMetaData(BaseModel): serialnumber: Optional[str] = None deviceTypeDetails: Optional[str] = None dataloggerId: Optional[str] = None - nodeType: Optional[str] = None + nodeType: Optional[int] = None numberMPPTrackers: Optional[int] = None numberPhases: Optional[int] = None peakPower: Optional[Power] = None From a1a79487ffe6fd719cc12a42ff4813bc23a11b47 Mon Sep 17 00:00:00 2001 From: drc38 <20024196+drc38@users.noreply.github.com> Date: Thu, 13 Feb 2025 09:01:03 +0000 Subject: [PATCH 5/7] add refresh token --- fronius_solarweb/api.py | 45 ++++++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/fronius_solarweb/api.py b/fronius_solarweb/api.py index 1dd88cc..e92ee31 100644 --- a/fronius_solarweb/api.py +++ b/fronius_solarweb/api.py @@ -60,12 +60,13 @@ def __init__( self.login_password = login_password self.pv_system_id = pv_system_id self.httpx_client = httpx_client or AsyncClient() + self.jwt_data: dict = None self._jwt_base_header = { "Content-Type": "application/json-patch+json", "AccessKeyId": self.access_key_id, "AccessKeyValue": self.access_key_value, "Accept": "application/json", - "Accept-Language": "de-de", + # "Accept-Language": "de-de", "User-Agent": "Solar.web/921 CFNetwork/1410.0.3 Darwin/22.6.0", } @@ -91,21 +92,6 @@ def _jwt_headers(self, new: dict): def _jwt_del_header(self, key: str): self._jwt_base_header.pop(key, None) - async def login(self): - self._jwt_del_header("Authorization") - _LOGGER.debug("Obtaining JSON web token") - r = await self.httpx_client.post( - f"{SW_BASE_URL}/iam/jwt", - headers=self._jwt_headers, - json={ - "userId": self.login_name, - "password": self.login_password, - }, - ) - json_data = await self._check_api_response(r) - _LOGGER.debug(f"JWT data returned: {json_data}") - self._jwt_headers = {"Authorization": "Bearer " + json_data.get("jwtToken")} - async def _check_api_response(self, response): if response.status_code == 401: _LOGGER.debug( @@ -120,6 +106,33 @@ async def _check_api_response(self, response): # returns dict type not string return response.json() + async def login(self): + self._jwt_del_header("Authorization") + _LOGGER.debug("Obtaining JSON web token") + r = await self.httpx_client.post( + f"{SW_BASE_URL}/iam/jwt", + headers=self._jwt_headers, + json={ + "userId": self.login_name, + "password": self.login_password, + }, + ) + self.jwt_data = await self._check_api_response(r) + _LOGGER.debug(f"JWT data returned: {self.jwt_data}") + self._jwt_headers = {"Authorization": "Bearer " + self.jwt_data.get("jwtToken")} + + async def refresh_token(self, token: str = None): + refresh=self.jwt_data.get("refreshToken",token) + self._jwt_del_header("Authorization") + _LOGGER.debug(f"Obtaining JSON web token using refresh token: {refresh}") + r = await self.httpx_client.patch( + f"{SW_BASE_URL}/iam/jwt/{refresh}", + headers={"accept": "application/json"} + ) + self.jwt_data = await self._check_api_response(r) + _LOGGER.debug(f"JWT data returned: {self.jwt_data}") + self._jwt_headers = {"Authorization": "Bearer " + self.jwt_data.get("jwtToken")} + @retry( wait=wait_random_exponential(multiplier=2, max=60), retry=retry_if_not_exception_type( From 59a1cdfc7779d1939108632eb5c6e5fe4d6dc602 Mon Sep 17 00:00:00 2001 From: drc38 <20024196+drc38@users.noreply.github.com> Date: Thu, 13 Feb 2025 09:04:09 +0000 Subject: [PATCH 6/7] switch response error to warning --- fronius_solarweb/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fronius_solarweb/api.py b/fronius_solarweb/api.py index e92ee31..74c66d7 100644 --- a/fronius_solarweb/api.py +++ b/fronius_solarweb/api.py @@ -94,12 +94,12 @@ def _jwt_del_header(self, key: str): async def _check_api_response(self, response): if response.status_code == 401: - _LOGGER.debug( + _LOGGER.warning( "Access unauthorised check solar.web access key values or login password" ) raise NotAuthorizedException() if response.status_code == 404: - _LOGGER.debug("Item not found check your PV system ID") + _LOGGER.warning("Item not found check your PV system ID") raise NotFoundException() response.raise_for_status() From 9f688519bc248e1367c8e3e926adc346d22f2352 Mon Sep 17 00:00:00 2001 From: drc38 <20024196+drc38@users.noreply.github.com> Date: Thu, 13 Feb 2025 09:06:44 +0000 Subject: [PATCH 7/7] lint --- fronius_solarweb/api.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/fronius_solarweb/api.py b/fronius_solarweb/api.py index 74c66d7..5b4dd45 100644 --- a/fronius_solarweb/api.py +++ b/fronius_solarweb/api.py @@ -122,12 +122,11 @@ async def login(self): self._jwt_headers = {"Authorization": "Bearer " + self.jwt_data.get("jwtToken")} async def refresh_token(self, token: str = None): - refresh=self.jwt_data.get("refreshToken",token) + refresh = self.jwt_data.get("refreshToken", token) self._jwt_del_header("Authorization") _LOGGER.debug(f"Obtaining JSON web token using refresh token: {refresh}") r = await self.httpx_client.patch( - f"{SW_BASE_URL}/iam/jwt/{refresh}", - headers={"accept": "application/json"} + f"{SW_BASE_URL}/iam/jwt/{refresh}", headers={"accept": "application/json"} ) self.jwt_data = await self._check_api_response(r) _LOGGER.debug(f"JWT data returned: {self.jwt_data}")