From c8b6eab3fd5ead7edc03272594a71f2ca02bcabc Mon Sep 17 00:00:00 2001 From: seaspotter Date: Mon, 2 Nov 2026 14:55:37 +0100 Subject: [PATCH 1/3] =?UTF-8?q?SAIC=20iSMART:=20HTTP-401/403=20l=C3=B6st?= =?UTF-8?q?=20wie=20code=3D401/403=20einen=20Relogin=20aus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/modules/vehicles/saic_ismart/api.py | 6 ++++++ .../modules/vehicles/saic_ismart/api_test.py | 17 +++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/packages/modules/vehicles/saic_ismart/api.py b/packages/modules/vehicles/saic_ismart/api.py index 97da753d08..23a5fb0173 100644 --- a/packages/modules/vehicles/saic_ismart/api.py +++ b/packages/modules/vehicles/saic_ismart/api.py @@ -97,6 +97,12 @@ def _send(method: str, path_with_query: str, config: SaicIsmartConfiguration, response = session.request( method, f"{BASE_URL}{path_with_query}", data=encrypted_body if encrypted_body else None, headers=headers, timeout=15) + if response.status_code in (401, 403): + # Die Cloud kann eine ungültige Session auch schon auf HTTP-Ebene ablehnen (zB nach Login in + # der iSMART-App auf dem Handy), nicht nur über das eigene code-Feld in einer 200-Antwort - + # muss hier genauso wie code=401/403 (_check_code) eine SaicAuthenticationError auslösen, + # sonst greift der automatische Relogin in _call_with_relogin() nicht. + raise SaicAuthenticationError(f"{path_with_query}: Session abgelaufen (HTTP {response.status_code})") response.raise_for_status() body_text = response.text diff --git a/packages/modules/vehicles/saic_ismart/api_test.py b/packages/modules/vehicles/saic_ismart/api_test.py index a70a87a348..65f349665e 100644 --- a/packages/modules/vehicles/saic_ismart/api_test.py +++ b/packages/modules/vehicles/saic_ismart/api_test.py @@ -95,3 +95,20 @@ def test_wake_vehicle_auth_error_triggers_relogin(self, monkeypatch): assert result.soc == 55.5 assert wake_mock.call_count == 2 + + +class TestSendHttpLevelAuthError: + def test_http_401_raises_authentication_error_not_http_error(self, monkeypatch): + # Die Cloud lehnt eine ungueltige Session manchmal schon auf HTTP-Ebene ab (zB nach + # Login in der iSMART-App auf dem Handy), nicht nur ueber das eigene code-Feld in einer + # 200-Antwort - muss als SaicAuthenticationError erkannt werden, damit der automatische + # Relogin in _call_with_relogin() greift (siehe Discussion zu wiederholtem 401 ohne Relogin). + response = Mock(status_code=401, headers={}) + response.raise_for_status = Mock(side_effect=AssertionError("raise_for_status() haette nicht " + "aufgerufen werden duerfen")) + session = Mock() + session.request = Mock(return_value=response) + monkeypatch.setattr(api.req, "get_http_session", Mock(return_value=session)) + + with pytest.raises(api.SaicAuthenticationError): + api._send("GET", "/vehicle/charging/mgmtData?vin=xxx", _config(), "token") From e41da1927c6025aac7cfd311e97c4580883dd727 Mon Sep 17 00:00:00 2001 From: seaspotter Date: Tue, 3 Nov 2026 16:33:21 +0100 Subject: [PATCH 2/3] SAIC iSMART: catch HTTPError from raise_for_status instead of manual status check --- packages/modules/vehicles/saic_ismart/api.py | 17 ++++++++++------- .../modules/vehicles/saic_ismart/api_test.py | 4 ++-- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/modules/vehicles/saic_ismart/api.py b/packages/modules/vehicles/saic_ismart/api.py index 23a5fb0173..158b60a04b 100644 --- a/packages/modules/vehicles/saic_ismart/api.py +++ b/packages/modules/vehicles/saic_ismart/api.py @@ -24,6 +24,8 @@ from typing import Any, Dict, Optional, Tuple from urllib.parse import urlencode +from requests.exceptions import HTTPError + from modules.common import req from modules.common.component_state import CarState from modules.vehicles.saic_ismart import crypto @@ -97,13 +99,14 @@ def _send(method: str, path_with_query: str, config: SaicIsmartConfiguration, response = session.request( method, f"{BASE_URL}{path_with_query}", data=encrypted_body if encrypted_body else None, headers=headers, timeout=15) - if response.status_code in (401, 403): - # Die Cloud kann eine ungültige Session auch schon auf HTTP-Ebene ablehnen (zB nach Login in - # der iSMART-App auf dem Handy), nicht nur über das eigene code-Feld in einer 200-Antwort - - # muss hier genauso wie code=401/403 (_check_code) eine SaicAuthenticationError auslösen, - # sonst greift der automatische Relogin in _call_with_relogin() nicht. - raise SaicAuthenticationError(f"{path_with_query}: Session abgelaufen (HTTP {response.status_code})") - response.raise_for_status() + try: + response.raise_for_status() + except HTTPError as exc: + if response.status_code in (401, 403): + # Session kann auch schon auf HTTP-Ebene abgelehnt werden, nicht nur über code= in der Antwort. + raise SaicAuthenticationError( + f"{path_with_query}: Session abgelaufen (HTTP {response.status_code})") from exc + raise body_text = response.text resp_ts = response.headers.get("APP-SEND-DATE") diff --git a/packages/modules/vehicles/saic_ismart/api_test.py b/packages/modules/vehicles/saic_ismart/api_test.py index 65f349665e..6404ae3d17 100644 --- a/packages/modules/vehicles/saic_ismart/api_test.py +++ b/packages/modules/vehicles/saic_ismart/api_test.py @@ -3,6 +3,7 @@ from unittest.mock import Mock import pytest +from requests.exceptions import HTTPError from modules.vehicles.saic_ismart import api from modules.vehicles.saic_ismart.config import SaicIsmartConfiguration @@ -104,8 +105,7 @@ def test_http_401_raises_authentication_error_not_http_error(self, monkeypatch): # 200-Antwort - muss als SaicAuthenticationError erkannt werden, damit der automatische # Relogin in _call_with_relogin() greift (siehe Discussion zu wiederholtem 401 ohne Relogin). response = Mock(status_code=401, headers={}) - response.raise_for_status = Mock(side_effect=AssertionError("raise_for_status() haette nicht " - "aufgerufen werden duerfen")) + response.raise_for_status = Mock(side_effect=HTTPError("401 Client Error")) session = Mock() session.request = Mock(return_value=response) monkeypatch.setattr(api.req, "get_http_session", Mock(return_value=session)) From af4c64998d558387068aea724cd0e700bd25c309 Mon Sep 17 00:00:00 2001 From: seaspotter Date: Wed, 4 Nov 2026 10:32:14 +0100 Subject: [PATCH 3/3] SAIC iSMART: catch HTTPError from session.request, not a dead raise_for_status call --- packages/modules/vehicles/saic_ismart/api.py | 11 +++++------ packages/modules/vehicles/saic_ismart/api_test.py | 4 ++-- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/modules/vehicles/saic_ismart/api.py b/packages/modules/vehicles/saic_ismart/api.py index 158b60a04b..b0fff800b9 100644 --- a/packages/modules/vehicles/saic_ismart/api.py +++ b/packages/modules/vehicles/saic_ismart/api.py @@ -96,16 +96,15 @@ def _send(method: str, path_with_query: str, config: SaicIsmartConfiguration, headers["event-id"] = event_id session = req.get_http_session() - response = session.request( - method, f"{BASE_URL}{path_with_query}", - data=encrypted_body if encrypted_body else None, headers=headers, timeout=15) try: - response.raise_for_status() + response = session.request( + method, f"{BASE_URL}{path_with_query}", + data=encrypted_body if encrypted_body else None, headers=headers, timeout=15) except HTTPError as exc: - if response.status_code in (401, 403): + if exc.response is not None and exc.response.status_code in (401, 403): # Session kann auch schon auf HTTP-Ebene abgelehnt werden, nicht nur über code= in der Antwort. raise SaicAuthenticationError( - f"{path_with_query}: Session abgelaufen (HTTP {response.status_code})") from exc + f"{path_with_query}: Session abgelaufen (HTTP {exc.response.status_code})") from exc raise body_text = response.text diff --git a/packages/modules/vehicles/saic_ismart/api_test.py b/packages/modules/vehicles/saic_ismart/api_test.py index 6404ae3d17..87e14c7866 100644 --- a/packages/modules/vehicles/saic_ismart/api_test.py +++ b/packages/modules/vehicles/saic_ismart/api_test.py @@ -104,10 +104,10 @@ def test_http_401_raises_authentication_error_not_http_error(self, monkeypatch): # Login in der iSMART-App auf dem Handy), nicht nur ueber das eigene code-Feld in einer # 200-Antwort - muss als SaicAuthenticationError erkannt werden, damit der automatische # Relogin in _call_with_relogin() greift (siehe Discussion zu wiederholtem 401 ohne Relogin). + # Der Session-Hook in req.py ruft raise_for_status() bereits in session.request() auf. response = Mock(status_code=401, headers={}) - response.raise_for_status = Mock(side_effect=HTTPError("401 Client Error")) session = Mock() - session.request = Mock(return_value=response) + session.request = Mock(side_effect=HTTPError("401 Client Error", response=response)) monkeypatch.setattr(api.req, "get_http_session", Mock(return_value=session)) with pytest.raises(api.SaicAuthenticationError):