From 5df0fb2d1bacf31d194dc2789c60e83b278095e3 Mon Sep 17 00:00:00 2001 From: Jan Weltmeyer <1668465+weltmeyer@users.noreply.github.com> Date: Sun, 21 Sep 2025 23:48:13 +0200 Subject: [PATCH 1/9] possibly fix the new login possibly fix the new login variant of modern sonnenbatterie --- sonnenbatterie/sonnenbatterie.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/sonnenbatterie/sonnenbatterie.py b/sonnenbatterie/sonnenbatterie.py index c2ca862..11215df 100644 --- a/sonnenbatterie/sonnenbatterie.py +++ b/sonnenbatterie/sonnenbatterie.py @@ -36,8 +36,17 @@ def login(self): password_sha512 = hashlib.sha512(self.password.encode('utf-8')).hexdigest() req_challenge=requests.get(self.baseurl+'challenge', timeout=self._batteryLoginTimeout) req_challenge.raise_for_status() + + salt=requests.get(self.baseurl+'salt/'+self.username, timeout=self._batteryLoginTimeout, allow_redirects=False)##returns a solt after battery got updated to a certatin version (>1.18??) + saltResponseCode=salt.status_code + challenge=req_challenge.json() - response=hashlib.pbkdf2_hmac('sha512',password_sha512.encode('utf-8'),challenge.encode('utf-8'),7500,64).hex() + if saltResponseCode!=200: #use old variant where no salt is availlable + response=hashlib.pbkdf2_hmac('sha512',password_sha512.encode('utf-8'),challenge.encode('utf-8'),7500,64).hex() + else: + salt=salt.json() + response=hashlib.pbkdf2_hmac('sha512',password_sha512.encode('utf-8'),salt.encode('utf-8'),7500,64).hex() + getsession=requests.post(self.baseurl+'session',{"user":self.username,"challenge":challenge,"response":response}, timeout=self._batteryLoginTimeout) getsession.raise_for_status() From 89027f5c523c8fc688903877c9c802a563e0c55b Mon Sep 17 00:00:00 2001 From: Jan Weltmeyer <1668465+weltmeyer@users.noreply.github.com> Date: Mon, 22 Sep 2025 00:09:51 +0200 Subject: [PATCH 2/9] Update sonnenbatterie.py more correct way of new login auth with salt, add description of difference between old and new method --- sonnenbatterie/sonnenbatterie.py | 37 +++++++++++++++++++++++++++----- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/sonnenbatterie/sonnenbatterie.py b/sonnenbatterie/sonnenbatterie.py index 11215df..a92667a 100644 --- a/sonnenbatterie/sonnenbatterie.py +++ b/sonnenbatterie/sonnenbatterie.py @@ -1,5 +1,6 @@ import json import sys +import base64, binascii, hashlib, hmac, string from sonnenbatterie2.sonnenbatterie2 import AsyncSonnenBatterieV2 @@ -40,13 +41,39 @@ def login(self): salt=requests.get(self.baseurl+'salt/'+self.username, timeout=self._batteryLoginTimeout, allow_redirects=False)##returns a solt after battery got updated to a certatin version (>1.18??) saltResponseCode=salt.status_code + + + + + challenge=req_challenge.json() - if saltResponseCode!=200: #use old variant where no salt is availlable + if saltResponseCode!=200: #use old variant where no salt is availlable + #Old path (no salt): PBKDF2 over sha512(password) + challenge → hex response=hashlib.pbkdf2_hmac('sha512',password_sha512.encode('utf-8'),challenge.encode('utf-8'),7500,64).hex() - else: - salt=salt.json() - response=hashlib.pbkdf2_hmac('sha512',password_sha512.encode('utf-8'),salt.encode('utf-8'),7500,64).hex() - + else:#New path (with salt): PBKDF2 over password + salt → HMAC-SHA256(challenge) → hex. + salt_payload=salt.json() + salt_str = salt_payload["salt"] + try: + if len(salt_str) % 2 == 0 and all(c in string.hexdigits for c in salt_str): + salt_bytes = bytes.fromhex(salt_str) + else: + raise ValueError + except Exception: + try: + salt_bytes = base64.b64decode(salt_str, validate=True) + except Exception: + salt_bytes = salt_str.encode("utf-8") + + + dk = hashlib.pbkdf2_hmac( + "sha512", + self.password.encode("utf-8"), # raw password (not pre-hashed) + salt_bytes, + 7500, + dklen=64 + ) + response = hmac.new(dk, challenge.encode("utf-8"), hashlib.sha256).hexdigest() + getsession=requests.post(self.baseurl+'session',{"user":self.username,"challenge":challenge,"response":response}, timeout=self._batteryLoginTimeout) getsession.raise_for_status() From 5f491eeb1e6cdc1d7feb1fe1e01eb09c9d854f89 Mon Sep 17 00:00:00 2001 From: Jan Weltmeyer <1668465+weltmeyer@users.noreply.github.com> Date: Wed, 24 Sep 2025 20:31:32 +0200 Subject: [PATCH 3/9] Update Async V1 for new login --- .gitignore | 3 +++ setup.cfg | 2 +- sonnenbatterie/sonnenbatterie.py | 43 ++++++++++++++++++++++++++------ 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index d2aee62..742727d 100644 --- a/.gitignore +++ b/.gitignore @@ -130,3 +130,6 @@ dmypy.json .vscode/ .idea/ +pyvenv.cfg +lib64 +bin diff --git a/setup.cfg b/setup.cfg index 1a3815a..74447c7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = sonnenbatterie -version = 0.6.1 +version = 0.7.dev2 author = Jan Weltmeyer description = "Access Sonnenbatterie REST API" long_description = file: README.md diff --git a/sonnenbatterie/sonnenbatterie.py b/sonnenbatterie/sonnenbatterie.py index a92667a..eaeb843 100644 --- a/sonnenbatterie/sonnenbatterie.py +++ b/sonnenbatterie/sonnenbatterie.py @@ -284,15 +284,42 @@ async def login(self): timeout=self._timeout, ) req_challenge.raise_for_status() - challenge = await req_challenge.json() - response = hashlib.pbkdf2_hmac( - 'sha512', - pw_sha512.encode('utf-8'), - challenge.encode('utf-8'), - 7500, - 64 - ).hex() + + salt = await self._session.get(self.baseurl+'salt/'+self.username, timeout=self._timeout, allow_redirects=False)##returns a solt after battery got updated to a certatin version (>1.18??) + saltResponseCode = salt.status + + + + + if saltResponseCode!=200: #use old variant where no salt is availlable + #Old path (no salt): PBKDF2 over sha512(password) + challenge → hex + response=hashlib.pbkdf2_hmac('sha512',pw_sha512.encode('utf-8'),challenge.encode('utf-8'),7500,64).hex() + else:#New path (with salt): PBKDF2 over password + salt → HMAC-SHA256(challenge) → hex. + salt_payload=salt.json() + salt_str = salt_payload["salt"] + try: + if len(salt_str) % 2 == 0 and all(c in string.hexdigits for c in salt_str): + salt_bytes = bytes.fromhex(salt_str) + else: + raise ValueError + except Exception: + try: + salt_bytes = base64.b64decode(salt_str, validate=True) + except Exception: + salt_bytes = salt_str.encode("utf-8") + + + dk = hashlib.pbkdf2_hmac( + "sha512", + self.password.encode("utf-8"), # raw password (not pre-hashed) + salt_bytes, + 7500, + dklen=64 + ) + response = hmac.new(dk, challenge.encode("utf-8"), hashlib.sha256).hexdigest() + + session = await self._session.post( url = self.baseurl+'session', From 35b98d6cba278d5a956d1c0ab8149863a81a2de7 Mon Sep 17 00:00:00 2001 From: Jan Weltmeyer <1668465+weltmeyer@users.noreply.github.com> Date: Thu, 25 Sep 2025 00:10:42 +0200 Subject: [PATCH 4/9] another small fix thats real blind debugging :) --- setup.cfg | 2 +- sonnenbatterie/sonnenbatterie.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 74447c7..2b10bb1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = sonnenbatterie -version = 0.7.dev2 +version = 0.7.dev3 author = Jan Weltmeyer description = "Access Sonnenbatterie REST API" long_description = file: README.md diff --git a/sonnenbatterie/sonnenbatterie.py b/sonnenbatterie/sonnenbatterie.py index eaeb843..99bd972 100644 --- a/sonnenbatterie/sonnenbatterie.py +++ b/sonnenbatterie/sonnenbatterie.py @@ -296,7 +296,7 @@ async def login(self): #Old path (no salt): PBKDF2 over sha512(password) + challenge → hex response=hashlib.pbkdf2_hmac('sha512',pw_sha512.encode('utf-8'),challenge.encode('utf-8'),7500,64).hex() else:#New path (with salt): PBKDF2 over password + salt → HMAC-SHA256(challenge) → hex. - salt_payload=salt.json() + salt_payload = await salt.json() salt_str = salt_payload["salt"] try: if len(salt_str) % 2 == 0 and all(c in string.hexdigits for c in salt_str): From 9e9def9b8d0109c26a409e85de4580707ff3b097 Mon Sep 17 00:00:00 2001 From: Jan Weltmeyer <1668465+weltmeyer@users.noreply.github.com> Date: Thu, 25 Sep 2025 00:40:29 +0200 Subject: [PATCH 5/9] more login trying --- setup.cfg | 2 +- sonnenbatterie/sonnenbatterie.py | 85 +++++++++++++++++++++----------- 2 files changed, 56 insertions(+), 31 deletions(-) diff --git a/setup.cfg b/setup.cfg index 2b10bb1..effedf1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = sonnenbatterie -version = 0.7.dev3 +version = 0.7.dev4 author = Jan Weltmeyer description = "Access Sonnenbatterie REST API" long_description = file: README.md diff --git a/sonnenbatterie/sonnenbatterie.py b/sonnenbatterie/sonnenbatterie.py index 99bd972..9aa148a 100644 --- a/sonnenbatterie/sonnenbatterie.py +++ b/sonnenbatterie/sonnenbatterie.py @@ -295,46 +295,71 @@ async def login(self): if saltResponseCode!=200: #use old variant where no salt is availlable #Old path (no salt): PBKDF2 over sha512(password) + challenge → hex response=hashlib.pbkdf2_hmac('sha512',pw_sha512.encode('utf-8'),challenge.encode('utf-8'),7500,64).hex() - else:#New path (with salt): PBKDF2 over password + salt → HMAC-SHA256(challenge) → hex. - salt_payload = await salt.json() - salt_str = salt_payload["salt"] - try: - if len(salt_str) % 2 == 0 and all(c in string.hexdigits for c in salt_str): - salt_bytes = bytes.fromhex(salt_str) - else: - raise ValueError - except Exception: - try: - salt_bytes = base64.b64decode(salt_str, validate=True) - except Exception: - salt_bytes = salt_str.encode("utf-8") - - - dk = hashlib.pbkdf2_hmac( - "sha512", - self.password.encode("utf-8"), # raw password (not pre-hashed) - salt_bytes, - 7500, - dklen=64 + session = await self._session.post( + url = self.baseurl+'session', + data = {"user":self.username,"challenge":challenge,"response":response}, + timeout=self._timeout, ) - response = hmac.new(dk, challenge.encode("utf-8"), hashlib.sha256).hexdigest() + session.raise_for_status() + token = await session.json() + self.token = token['authentication_token'] + else:#New path (with salt): PBKDF2 over password + salt → HMAC-SHA256(challenge) → hex. + await create_session_token_new() - session = await self._session.post( - url = self.baseurl+'session', - data = {"user":self.username,"challenge":challenge,"response":response}, - timeout=self._timeout, - ) - session.raise_for_status() - token = await session.json() - self.token = token['authentication_token'] # Inisitalite async API v2 if self.sb2 is None: self.sb2 = AsyncSonnenBatterieV2(ip_address=self.ipaddress, api_token=self.token) + async def create_session_token_new(self): + # Step 1: challenge + async with self._session.get(f"{self.baseurl}challenge") as r: + r.raise_for_status() + try: + challenge = await r.json() + if isinstance(challenge, dict): + challenge = challenge.get("challenge") or next(iter(challenge.values())) + except Exception: + challenge = (await r.text()).strip() + + # Step 2: salt + async with self._session.get(f"{self.baseurl}salt/{self.username}") as r: + r.raise_for_status() + salt = (await r.json())["salt"] + + # Step 3: derive key + pw_sha512_hex = hashlib.sha512(self.password.encode("utf-8")).hexdigest() + pw_bytes = pw_sha512_hex.encode("utf-8") + dk = hashlib.pbkdf2_hmac("sha512", pw_bytes, salt.encode("utf-8"), 7500, dklen=64) + derived_hex = dk.hex() + + # Step 4: loop: POST /session, maybe retry with new_challenge + for _ in range(2): + key = derived_hex.encode("utf-8") + response = hmac.new(key, challenge.encode("utf-8"), hashlib.sha256).hexdigest() + payload = {"user": self.username, "challenge": challenge, "response": response} + + async with self._session.post(f"{self.baseurl}session", json=payload) as r: + r.raise_for_status() + data = await r.json() + + if "new_challenge" in data and data["new_challenge"]: + challenge = data["new_challenge"] + continue + + if "authentication_token" in data and data["authentication_token"]: + self.token = data["authentication_token"] + + raise RuntimeError(f"Unexpected /session response: {json.dumps(data)}") + + raise RuntimeError("Failed to obtain authentication_token") + + + + """ Base functions """ From 90ad36db9973b6432f8f72156d8171afe93a101d Mon Sep 17 00:00:00 2001 From: Jan Weltmeyer <1668465+weltmeyer@users.noreply.github.com> Date: Thu, 25 Sep 2025 00:56:28 +0200 Subject: [PATCH 6/9] Update sonnenbatterie.py --- sonnenbatterie/sonnenbatterie.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sonnenbatterie/sonnenbatterie.py b/sonnenbatterie/sonnenbatterie.py index 9aa148a..d483029 100644 --- a/sonnenbatterie/sonnenbatterie.py +++ b/sonnenbatterie/sonnenbatterie.py @@ -304,7 +304,7 @@ async def login(self): token = await session.json() self.token = token['authentication_token'] else:#New path (with salt): PBKDF2 over password + salt → HMAC-SHA256(challenge) → hex. - await create_session_token_new() + await self.create_session_token_new() From 3997ff08d9a8ca15a3bb6b1843ddc7871d6d1707 Mon Sep 17 00:00:00 2001 From: Jan Weltmeyer <1668465+weltmeyer@users.noreply.github.com> Date: Thu, 25 Sep 2025 00:56:48 +0200 Subject: [PATCH 7/9] Update setup.cfg --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index effedf1..2036f62 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = sonnenbatterie -version = 0.7.dev4 +version = 0.7.dev5 author = Jan Weltmeyer description = "Access Sonnenbatterie REST API" long_description = file: README.md From ef2424fc379e07015bf59cee463dd5beb0bf2662 Mon Sep 17 00:00:00 2001 From: Jan Weltmeyer <1668465+weltmeyer@users.noreply.github.com> Date: Thu, 25 Sep 2025 18:58:17 +0200 Subject: [PATCH 8/9] new login next try --- setup.cfg | 2 +- sonnenbatterie/sonnenbatterie.py | 33 +++++++++++++++------- test/test_login_new.py | 48 ++++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 11 deletions(-) create mode 100644 test/test_login_new.py diff --git a/setup.cfg b/setup.cfg index 2036f62..4dd3cad 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = sonnenbatterie -version = 0.7.dev5 +version = 0.7.dev8 author = Jan Weltmeyer description = "Access Sonnenbatterie REST API" long_description = file: README.md diff --git a/sonnenbatterie/sonnenbatterie.py b/sonnenbatterie/sonnenbatterie.py index d483029..3ddefc6 100644 --- a/sonnenbatterie/sonnenbatterie.py +++ b/sonnenbatterie/sonnenbatterie.py @@ -290,8 +290,6 @@ async def login(self): saltResponseCode = salt.status - - if saltResponseCode!=200: #use old variant where no salt is availlable #Old path (no salt): PBKDF2 over sha512(password) + challenge → hex response=hashlib.pbkdf2_hmac('sha512',pw_sha512.encode('utf-8'),challenge.encode('utf-8'),7500,64).hex() @@ -314,6 +312,16 @@ async def login(self): self.sb2 = AsyncSonnenBatterieV2(ip_address=self.ipaddress, api_token=self.token) + + def create_response_from_values(self, username, password, challenge, salt): + pw_sha512_hex = hashlib.sha512(password.encode("utf-8")).hexdigest() + pw_bytes = pw_sha512_hex.encode("utf-8") + dk = hashlib.pbkdf2_hmac("sha512", pw_bytes, salt.encode("utf-8"), 7500, dklen=64) + derived_hex = dk.hex() + key = derived_hex.encode("utf-8") + response = hmac.new(key, challenge.encode("utf-8"), hashlib.sha256).hexdigest() + return response + async def create_session_token_new(self): # Step 1: challenge async with self._session.get(f"{self.baseurl}challenge") as r: @@ -330,19 +338,24 @@ async def create_session_token_new(self): r.raise_for_status() salt = (await r.json())["salt"] - # Step 3: derive key - pw_sha512_hex = hashlib.sha512(self.password.encode("utf-8")).hexdigest() - pw_bytes = pw_sha512_hex.encode("utf-8") - dk = hashlib.pbkdf2_hmac("sha512", pw_bytes, salt.encode("utf-8"), 7500, dklen=64) - derived_hex = dk.hex() - + # Step 4: loop: POST /session, maybe retry with new_challenge for _ in range(2): - key = derived_hex.encode("utf-8") - response = hmac.new(key, challenge.encode("utf-8"), hashlib.sha256).hexdigest() + response = self.create_response_from_values(self.username,self.password,challenge,salt) payload = {"user": self.username, "challenge": challenge, "response": response} async with self._session.post(f"{self.baseurl}session", json=payload) as r: + txt = await r.text() + if r.status >= 400: + raise RuntimeError( + f"Login failed with HTTP {r.status}\n" + f"URL: {r.url}\n" + f"Payload: {json.dumps(payload)}\n" + f"challenge: {challenge}\n" + f"salt: {salt}\n" + f"response: {response}\n" + f"Server reply: {txt}" + ) r.raise_for_status() data = await r.json() diff --git a/test/test_login_new.py b/test/test_login_new.py new file mode 100644 index 0000000..f95a4e6 --- /dev/null +++ b/test/test_login_new.py @@ -0,0 +1,48 @@ + +#!/usr/bin/env python3 +# I suspect that I haven't got to grips with the way phthon does things, but soppusely this will setup the path to allod for the sonnen batteri moduel to be in a separate location +# To me having to do this for testing seems a horrendous hack +import asyncio +import os +import sys +import time +import json +import sys +import base64, binascii, hashlib, hmac, string +script_path = os.path.realpath(os.path.dirname(__name__)) +os.chdir(script_path) +sys.path.append("..") +from pprint import pprint +import hashlib + +# this is based on the test code by rust dust + +def main(): + challenge="39bdd8a304b84c76" + username="User" + password="sonnenUser3552!" + salt="04f8996da763b7a969b1028ee3007569eaf3a635486ddab211d512c85b9df8fb_02bbe1e15021947b232050cc88b07772" + testResponse=create_response_from_values(username,password,challenge,salt) + wantedResponse="39b5cb8cc21772b482cdbbcbd135f4a05af4974f3e1b9e4c8aa846d6e64bcc1a" + + + if testResponse==wantedResponse: + print("It works!") + else: + print("doesnt work :(") + + print("Response: "+testResponse) + + +def create_response_from_values(username, password, challenge, salt): + pw_sha512_hex = hashlib.sha512(password.encode("utf-8")).hexdigest() + pw_bytes = pw_sha512_hex.encode("utf-8") + dk = hashlib.pbkdf2_hmac("sha512", pw_bytes, salt.encode("utf-8"), 7500, dklen=64) + derived_hex = dk.hex() + key = derived_hex.encode("utf-8") + response = hmac.new(key, challenge.encode("utf-8"), hashlib.sha256).hexdigest() + return response + + +if __name__ == '__main__': + main() From a76d7081a7a456fd8bcab7de9b35e40a3930355c Mon Sep 17 00:00:00 2001 From: Jan Weltmeyer <1668465+weltmeyer@users.noreply.github.com> Date: Thu, 25 Sep 2025 21:09:53 +0200 Subject: [PATCH 9/9] new login method done? --- setup.cfg | 2 +- sonnenbatterie/sonnenbatterie.py | 156 ++++++++++++++++++++----------- test/test_login_new.py | 1 + 3 files changed, 103 insertions(+), 56 deletions(-) diff --git a/setup.cfg b/setup.cfg index 4dd3cad..04fa4ac 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = sonnenbatterie -version = 0.7.dev8 +version = 0.7.1 author = Jan Weltmeyer description = "Access Sonnenbatterie REST API" long_description = file: README.md diff --git a/sonnenbatterie/sonnenbatterie.py b/sonnenbatterie/sonnenbatterie.py index 3ddefc6..dfa1d80 100644 --- a/sonnenbatterie/sonnenbatterie.py +++ b/sonnenbatterie/sonnenbatterie.py @@ -34,51 +34,92 @@ def __init__(self,username,password,ipaddress): def login(self): - password_sha512 = hashlib.sha512(self.password.encode('utf-8')).hexdigest() - req_challenge=requests.get(self.baseurl+'challenge', timeout=self._batteryLoginTimeout) - req_challenge.raise_for_status() - salt=requests.get(self.baseurl+'salt/'+self.username, timeout=self._batteryLoginTimeout, allow_redirects=False)##returns a solt after battery got updated to a certatin version (>1.18??) - saltResponseCode=salt.status_code + salt = requests.get(self.baseurl+'salt/'+self.username, timeout=self._batteryLoginTimeout, allow_redirects=False)##returns a solt after battery got updated to a certatin version (>1.18??) + if salt.status_code==200: + self.create_session_token_new() + else: + password_sha512 = hashlib.sha512(self.password.encode('utf-8')).hexdigest() + req_challenge=requests.get(self.baseurl+'challenge', timeout=self._batteryLoginTimeout) + req_challenge.raise_for_status() + challenge=req_challenge.json() + response=hashlib.pbkdf2_hmac('sha512',password_sha512.encode('utf-8'),challenge.encode('utf-8'),7500,64).hex() + + getsession=requests.post(self.baseurl+'session',{"user":self.username,"challenge":challenge,"response":response}, timeout=self._batteryLoginTimeout) + getsession.raise_for_status() + token=getsession.json()['authentication_token'] + self.token=token - - challenge=req_challenge.json() - if saltResponseCode!=200: #use old variant where no salt is availlable - #Old path (no salt): PBKDF2 over sha512(password) + challenge → hex - response=hashlib.pbkdf2_hmac('sha512',password_sha512.encode('utf-8'),challenge.encode('utf-8'),7500,64).hex() - else:#New path (with salt): PBKDF2 over password + salt → HMAC-SHA256(challenge) → hex. - salt_payload=salt.json() - salt_str = salt_payload["salt"] - try: - if len(salt_str) % 2 == 0 and all(c in string.hexdigits for c in salt_str): - salt_bytes = bytes.fromhex(salt_str) - else: - raise ValueError - except Exception: - try: - salt_bytes = base64.b64decode(salt_str, validate=True) - except Exception: - salt_bytes = salt_str.encode("utf-8") + def create_response_from_values(self, username, password, challenge, salt): + pw_sha512_hex = hashlib.sha512(password.encode("utf-8")).hexdigest() + pw_bytes = pw_sha512_hex.encode("utf-8") + dk = hashlib.pbkdf2_hmac("sha512", pw_bytes, salt.encode("utf-8"), 7500, dklen=64) + derived_hex = dk.hex() + key = derived_hex.encode("utf-8") + response = hmac.new(key, challenge.encode("utf-8"), hashlib.sha256).hexdigest() + return response - - dk = hashlib.pbkdf2_hmac( - "sha512", - self.password.encode("utf-8"), # raw password (not pre-hashed) - salt_bytes, - 7500, - dklen=64 + def create_session_token_new(self): + session = requests.Session() + + # Step 1: challenge + r = session.get(f"{self.baseurl}challenge", timeout=self._timeout) + r.raise_for_status() + try: + challenge = r.json() + if isinstance(challenge, dict): + challenge = challenge.get("challenge") or next(iter(challenge.values())) + except Exception: + challenge = r.text.strip() + + # Step 2: salt + r = session.get(f"{self.baseurl}salt/{self.username}", timeout=self._timeout) + r.raise_for_status() + salt = r.json()["salt"] + + # Step 4: loop: POST /session, maybe retry with new_challenge + for _ in range(2): + response = self.create_response_from_values(self.username, self.password, challenge, salt) + payload = { + "user": self.username, + "challenge": challenge, + "response": response, + } + + r = session.post( + url=f"{self.baseurl}session", + data=payload, + timeout=self._timeout, ) - response = hmac.new(dk, challenge.encode("utf-8"), hashlib.sha256).hexdigest() + txt = r.text + if r.status_code >= 400: + raise RuntimeError( + f"Login failed with HTTP {r.status_code}\n" + f"URL: {r.url}\n" + f"Payload: {json.dumps(payload)}\n" + f"challenge: {challenge}\n" + f"salt: {salt}\n" + f"response: {response}\n" + f"Server reply: {txt}" + ) + r.raise_for_status() + data = r.json() - - getsession=requests.post(self.baseurl+'session',{"user":self.username,"challenge":challenge,"response":response}, timeout=self._batteryLoginTimeout) - getsession.raise_for_status() - token=getsession.json()['authentication_token'] - self.token=token + if "new_challenge" in data and data["new_challenge"]: + challenge = data["new_challenge"] + continue + + if "authentication_token" in data and data["authentication_token"]: + self.token = data["authentication_token"] + return + + raise RuntimeError(f"Unexpected /session response: {json.dumps(data)}") + + raise RuntimeError("Failed to obtain authentication_token") def set_login_timeout(self, timeout:int = 120): self._batteryLoginTimeout = timeout @@ -278,21 +319,26 @@ async def login(self): if self._session is None: self._session = aiohttp.ClientSession() - pw_sha512 = hashlib.sha512(self.password.encode('utf-8')).hexdigest() - req_challenge = await self._session.get( - self.baseurl+'challenge', - timeout=self._timeout, - ) - req_challenge.raise_for_status() - challenge = await req_challenge.json() - salt = await self._session.get(self.baseurl+'salt/'+self.username, timeout=self._timeout, allow_redirects=False)##returns a solt after battery got updated to a certatin version (>1.18??) - saltResponseCode = salt.status + if salt.status==200: + await self.create_session_token_new() + else: + pw_sha512 = hashlib.sha512(self.password.encode('utf-8')).hexdigest() + req_challenge = await self._session.get( + self.baseurl+'challenge', + timeout=self._timeout, + ) + req_challenge.raise_for_status() + challenge = await req_challenge.json() + response = hashlib.pbkdf2_hmac( + 'sha512', + pw_sha512.encode('utf-8'), + challenge.encode('utf-8'), + 7500, + 64 + ).hex() - if saltResponseCode!=200: #use old variant where no salt is availlable - #Old path (no salt): PBKDF2 over sha512(password) + challenge → hex - response=hashlib.pbkdf2_hmac('sha512',pw_sha512.encode('utf-8'),challenge.encode('utf-8'),7500,64).hex() session = await self._session.post( url = self.baseurl+'session', data = {"user":self.username,"challenge":challenge,"response":response}, @@ -301,11 +347,6 @@ async def login(self): session.raise_for_status() token = await session.json() self.token = token['authentication_token'] - else:#New path (with salt): PBKDF2 over password + salt → HMAC-SHA256(challenge) → hex. - await self.create_session_token_new() - - - # Inisitalite async API v2 if self.sb2 is None: @@ -342,9 +383,13 @@ async def create_session_token_new(self): # Step 4: loop: POST /session, maybe retry with new_challenge for _ in range(2): response = self.create_response_from_values(self.username,self.password,challenge,salt) - payload = {"user": self.username, "challenge": challenge, "response": response} + async with self._session.post( + + url = self.baseurl+'session', + data = {"user":self.username,"challenge":challenge,"response":response}, + timeout=self._timeout, - async with self._session.post(f"{self.baseurl}session", json=payload) as r: + ) as r: txt = await r.text() if r.status >= 400: raise RuntimeError( @@ -365,6 +410,7 @@ async def create_session_token_new(self): if "authentication_token" in data and data["authentication_token"]: self.token = data["authentication_token"] + return raise RuntimeError(f"Unexpected /session response: {json.dumps(data)}") diff --git a/test/test_login_new.py b/test/test_login_new.py index f95a4e6..c8bdbf0 100644 --- a/test/test_login_new.py +++ b/test/test_login_new.py @@ -19,6 +19,7 @@ def main(): challenge="39bdd8a304b84c76" + challenge="d43aeb06a6394daa" username="User" password="sonnenUser3552!" salt="04f8996da763b7a969b1028ee3007569eaf3a635486ddab211d512c85b9df8fb_02bbe1e15021947b232050cc88b07772"