From b8157976de1288dad25cdf5b84f78768b578ced1 Mon Sep 17 00:00:00 2001 From: Giovanni Condello Date: Sat, 31 May 2025 11:30:13 +0200 Subject: [PATCH 1/4] FIx compatibility with new app release --- src/saic_ismart_client_ng/api/base.py | 146 ++++++++++++------------ src/saic_ismart_client_ng/net/crypto.py | 2 +- 2 files changed, 75 insertions(+), 73 deletions(-) diff --git a/src/saic_ismart_client_ng/api/base.py b/src/saic_ismart_client_ng/api/base.py index 4429824..b4e38a2 100644 --- a/src/saic_ismart_client_ng/api/base.py +++ b/src/saic_ismart_client_ng/api/base.py @@ -1,8 +1,8 @@ from __future__ import annotations -from dataclasses import asdict import datetime import logging +from dataclasses import asdict from typing import ( TYPE_CHECKING, Any, @@ -33,11 +33,13 @@ from saic_ismart_client_ng.listener import SaicApiListener from saic_ismart_client_ng.model import SaicApiConfiguration + class IsDataclass(Protocol): # as already noted in comments, checking for this attribute is currently # the most reliable way to ascertain that something is a dataclass __dataclass_fields__: ClassVar[dict[str, Any]] + T = TypeVar("T", bound=IsDataclass) logger = logging.getLogger(__name__) @@ -45,9 +47,9 @@ class IsDataclass(Protocol): class AbstractSaicApi: def __init__( - self, - configuration: SaicApiConfiguration, - listener: SaicApiListener | None = None, + self, + configuration: SaicApiConfiguration, + listener: SaicApiListener | None = None, ) -> None: self.__configuration = configuration self.__api_client = SaicApiClient(configuration, listener=listener) @@ -59,16 +61,16 @@ async def login(self) -> LoginResp: "Accept": "application/json", "Authorization": "Basic c3dvcmQ6c3dvcmRfc2VjcmV0", } - firebase_device_id = "cqSHOMG1SmK4k-fzAeK6hr:APA91bGtGihOG5SEQ9hPx3Dtr9o9mQguNiKZrQzboa-1C_UBlRZYdFcMmdfLvh9Q_xA8A0dGFIjkMhZbdIXOYnKfHCeWafAfLXOrxBS3N18T4Slr-x9qpV6FHLMhE9s7I6s89k9lU7DD" + firebase_device_id = "simulator*********************************************" + str(int(datetime.datetime.now().timestamp())) form_body = { "grant_type": "password", "username": self.__configuration.username, "password": sha1_hex_digest(self.__configuration.password), "scope": "all", - "deviceId": f"{firebase_device_id}###europecar", - "deviceType": "1", # 2 for huawei + "deviceId": f"{firebase_device_id}###com.saicmotor.europecar", + "deviceType": "0", # 2 for huawei "loginType": "2" if self.__configuration.username_is_email else "1", - "countryCode": "" + "language": "EN" if self.__configuration.username_is_email else self.__configuration.phone_country_code, } @@ -82,7 +84,7 @@ async def login(self) -> LoginResp: ) # Update the user token if not (access_token := result.access_token) or not ( - expiration := result.expires_in + expiration := result.expires_in ): raise SaicApiException( "Failed to get an access token, please check your credentials" @@ -95,16 +97,16 @@ async def login(self) -> LoginResp: return result async def execute_api_call( - self, - method: str, - path: str, - *, - body: Any | None = None, - form_body: Any | None = None, - out_type: type[T], - params: QueryParamTypes | None = None, - headers: HeaderTypes | None = None, - allow_null_body: bool = False, + self, + method: str, + path: str, + *, + body: Any | None = None, + form_body: Any | None = None, + out_type: type[T], + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + allow_null_body: bool = False, ) -> T: result = await self.__execute_api_call( method, @@ -122,15 +124,15 @@ async def execute_api_call( return result async def execute_api_call_no_result( - self, - method: str, - path: str, - *, - body: Any | None = None, - form_body: Any | None = None, - params: QueryParamTypes | None = None, - headers: HeaderTypes | None = None, - allow_null_body: bool = False, + self, + method: str, + path: str, + *, + body: Any | None = None, + form_body: Any | None = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + allow_null_body: bool = False, ) -> None: await self.__execute_api_call( method, @@ -143,16 +145,16 @@ async def execute_api_call_no_result( ) async def __execute_api_call( - self, - method: str, - path: str, - *, - body: Any | None = None, - form_body: Any | None = None, - out_type: type[T] | None = None, - params: QueryParamTypes | None = None, - headers: HeaderTypes | None = None, - allow_null_body: bool = False, + self, + method: str, + path: str, + *, + body: Any | None = None, + form_body: Any | None = None, + out_type: type[T] | None = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + allow_null_body: bool = False, ) -> T | None: try: url = f"{self.__configuration.base_uri}{path.removeprefix('/')}" @@ -174,15 +176,15 @@ async def __execute_api_call( raise SaicApiException(msg, return_code=500) from e async def execute_api_call_with_event_id( - self, - method: str, - path: str, - *, - body: Any | None = None, - out_type: type[T], - params: QueryParamTypes | None = None, - headers: MutableMapping[str, str] | None = None, - delay: tenacity.wait.WaitBaseT | None = None, + self, + method: str, + path: str, + *, + body: Any | None = None, + out_type: type[T], + params: QueryParamTypes | None = None, + headers: MutableMapping[str, str] | None = None, + delay: tenacity.wait.WaitBaseT | None = None, ) -> T: result = await self.__execute_api_call_with_event_id( method, @@ -199,14 +201,14 @@ async def execute_api_call_with_event_id( return result async def execute_api_call_with_event_id_no_result( - self, - method: str, - path: str, - *, - body: Any | None = None, - params: QueryParamTypes | None = None, - headers: MutableMapping[str, str] | None = None, - delay: tenacity.wait.WaitBaseT | None = None, + self, + method: str, + path: str, + *, + body: Any | None = None, + params: QueryParamTypes | None = None, + headers: MutableMapping[str, str] | None = None, + delay: tenacity.wait.WaitBaseT | None = None, ) -> None: await self.__execute_api_call_with_event_id( method, @@ -218,15 +220,15 @@ async def execute_api_call_with_event_id_no_result( ) async def __execute_api_call_with_event_id( - self, - method: str, - path: str, - *, - body: Any | None = None, - out_type: type[T] | None = None, - params: QueryParamTypes | None = None, - headers: MutableMapping[str, str] | None = None, - delay: tenacity.wait.WaitBaseT | None = None, + self, + method: str, + path: str, + *, + body: Any | None = None, + out_type: type[T] | None = None, + params: QueryParamTypes | None = None, + headers: MutableMapping[str, str] | None = None, + delay: tenacity.wait.WaitBaseT | None = None, ) -> T | None: @tenacity.retry( stop=tenacity.stop_after_delay(30), @@ -251,11 +253,11 @@ async def execute_api_call_with_event_id_inner(*, event_id: str) -> T | None: # pylint: disable=too-many-branches async def __deserialize( - self, - request: httpx.Request, - response: httpx.Response, - data_class: type[T] | None, - allow_null_body: bool, + self, + request: httpx.Request, + response: httpx.Response, + data_class: type[T] | None, + allow_null_body: bool, ) -> T | None: try: request_event_id = request.headers.get("event-id") @@ -348,9 +350,9 @@ def logout(self) -> None: @property def is_logged_in(self) -> bool: return ( - self.__api_client.user_token is not None - and self.__token_expiration is not None - and self.__token_expiration > datetime.datetime.now() + self.__api_client.user_token is not None + and self.__token_expiration is not None + and self.__token_expiration > datetime.datetime.now() ) @property diff --git a/src/saic_ismart_client_ng/net/crypto.py b/src/saic_ismart_client_ng/net/crypto.py index 1aa9790..1370026 100644 --- a/src/saic_ismart_client_ng/net/crypto.py +++ b/src/saic_ismart_client_ng/net/crypto.py @@ -111,7 +111,7 @@ def encrypt_request( request_content, key_hex, iv_hex ).encode("utf-8") - original_request_headers["User-Agent"] = "okhttp/3.14.9" + original_request_headers["User-Agent"] = "Europe/2.1.0 (iPad; iOS 18.5; Scale/2.00)" original_request_headers["Content-Type"] = f"{modified_content_type};charset=utf-8" original_request_headers["Accept"] = "application/json" original_request_headers["Accept-Encoding"] = "gzip" From 8fcc2d9e7c08dd9fd75bb52d2c61eef88b15c59d Mon Sep 17 00:00:00 2001 From: Giovanni Condello Date: Sat, 31 May 2025 11:30:13 +0200 Subject: [PATCH 2/4] FIx compatibility with new app release --- src/saic_ismart_client_ng/api/base.py | 146 ++++++++++++------------ src/saic_ismart_client_ng/net/crypto.py | 2 +- 2 files changed, 75 insertions(+), 73 deletions(-) diff --git a/src/saic_ismart_client_ng/api/base.py b/src/saic_ismart_client_ng/api/base.py index 4429824..b4e38a2 100644 --- a/src/saic_ismart_client_ng/api/base.py +++ b/src/saic_ismart_client_ng/api/base.py @@ -1,8 +1,8 @@ from __future__ import annotations -from dataclasses import asdict import datetime import logging +from dataclasses import asdict from typing import ( TYPE_CHECKING, Any, @@ -33,11 +33,13 @@ from saic_ismart_client_ng.listener import SaicApiListener from saic_ismart_client_ng.model import SaicApiConfiguration + class IsDataclass(Protocol): # as already noted in comments, checking for this attribute is currently # the most reliable way to ascertain that something is a dataclass __dataclass_fields__: ClassVar[dict[str, Any]] + T = TypeVar("T", bound=IsDataclass) logger = logging.getLogger(__name__) @@ -45,9 +47,9 @@ class IsDataclass(Protocol): class AbstractSaicApi: def __init__( - self, - configuration: SaicApiConfiguration, - listener: SaicApiListener | None = None, + self, + configuration: SaicApiConfiguration, + listener: SaicApiListener | None = None, ) -> None: self.__configuration = configuration self.__api_client = SaicApiClient(configuration, listener=listener) @@ -59,16 +61,16 @@ async def login(self) -> LoginResp: "Accept": "application/json", "Authorization": "Basic c3dvcmQ6c3dvcmRfc2VjcmV0", } - firebase_device_id = "cqSHOMG1SmK4k-fzAeK6hr:APA91bGtGihOG5SEQ9hPx3Dtr9o9mQguNiKZrQzboa-1C_UBlRZYdFcMmdfLvh9Q_xA8A0dGFIjkMhZbdIXOYnKfHCeWafAfLXOrxBS3N18T4Slr-x9qpV6FHLMhE9s7I6s89k9lU7DD" + firebase_device_id = "simulator*********************************************" + str(int(datetime.datetime.now().timestamp())) form_body = { "grant_type": "password", "username": self.__configuration.username, "password": sha1_hex_digest(self.__configuration.password), "scope": "all", - "deviceId": f"{firebase_device_id}###europecar", - "deviceType": "1", # 2 for huawei + "deviceId": f"{firebase_device_id}###com.saicmotor.europecar", + "deviceType": "0", # 2 for huawei "loginType": "2" if self.__configuration.username_is_email else "1", - "countryCode": "" + "language": "EN" if self.__configuration.username_is_email else self.__configuration.phone_country_code, } @@ -82,7 +84,7 @@ async def login(self) -> LoginResp: ) # Update the user token if not (access_token := result.access_token) or not ( - expiration := result.expires_in + expiration := result.expires_in ): raise SaicApiException( "Failed to get an access token, please check your credentials" @@ -95,16 +97,16 @@ async def login(self) -> LoginResp: return result async def execute_api_call( - self, - method: str, - path: str, - *, - body: Any | None = None, - form_body: Any | None = None, - out_type: type[T], - params: QueryParamTypes | None = None, - headers: HeaderTypes | None = None, - allow_null_body: bool = False, + self, + method: str, + path: str, + *, + body: Any | None = None, + form_body: Any | None = None, + out_type: type[T], + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + allow_null_body: bool = False, ) -> T: result = await self.__execute_api_call( method, @@ -122,15 +124,15 @@ async def execute_api_call( return result async def execute_api_call_no_result( - self, - method: str, - path: str, - *, - body: Any | None = None, - form_body: Any | None = None, - params: QueryParamTypes | None = None, - headers: HeaderTypes | None = None, - allow_null_body: bool = False, + self, + method: str, + path: str, + *, + body: Any | None = None, + form_body: Any | None = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + allow_null_body: bool = False, ) -> None: await self.__execute_api_call( method, @@ -143,16 +145,16 @@ async def execute_api_call_no_result( ) async def __execute_api_call( - self, - method: str, - path: str, - *, - body: Any | None = None, - form_body: Any | None = None, - out_type: type[T] | None = None, - params: QueryParamTypes | None = None, - headers: HeaderTypes | None = None, - allow_null_body: bool = False, + self, + method: str, + path: str, + *, + body: Any | None = None, + form_body: Any | None = None, + out_type: type[T] | None = None, + params: QueryParamTypes | None = None, + headers: HeaderTypes | None = None, + allow_null_body: bool = False, ) -> T | None: try: url = f"{self.__configuration.base_uri}{path.removeprefix('/')}" @@ -174,15 +176,15 @@ async def __execute_api_call( raise SaicApiException(msg, return_code=500) from e async def execute_api_call_with_event_id( - self, - method: str, - path: str, - *, - body: Any | None = None, - out_type: type[T], - params: QueryParamTypes | None = None, - headers: MutableMapping[str, str] | None = None, - delay: tenacity.wait.WaitBaseT | None = None, + self, + method: str, + path: str, + *, + body: Any | None = None, + out_type: type[T], + params: QueryParamTypes | None = None, + headers: MutableMapping[str, str] | None = None, + delay: tenacity.wait.WaitBaseT | None = None, ) -> T: result = await self.__execute_api_call_with_event_id( method, @@ -199,14 +201,14 @@ async def execute_api_call_with_event_id( return result async def execute_api_call_with_event_id_no_result( - self, - method: str, - path: str, - *, - body: Any | None = None, - params: QueryParamTypes | None = None, - headers: MutableMapping[str, str] | None = None, - delay: tenacity.wait.WaitBaseT | None = None, + self, + method: str, + path: str, + *, + body: Any | None = None, + params: QueryParamTypes | None = None, + headers: MutableMapping[str, str] | None = None, + delay: tenacity.wait.WaitBaseT | None = None, ) -> None: await self.__execute_api_call_with_event_id( method, @@ -218,15 +220,15 @@ async def execute_api_call_with_event_id_no_result( ) async def __execute_api_call_with_event_id( - self, - method: str, - path: str, - *, - body: Any | None = None, - out_type: type[T] | None = None, - params: QueryParamTypes | None = None, - headers: MutableMapping[str, str] | None = None, - delay: tenacity.wait.WaitBaseT | None = None, + self, + method: str, + path: str, + *, + body: Any | None = None, + out_type: type[T] | None = None, + params: QueryParamTypes | None = None, + headers: MutableMapping[str, str] | None = None, + delay: tenacity.wait.WaitBaseT | None = None, ) -> T | None: @tenacity.retry( stop=tenacity.stop_after_delay(30), @@ -251,11 +253,11 @@ async def execute_api_call_with_event_id_inner(*, event_id: str) -> T | None: # pylint: disable=too-many-branches async def __deserialize( - self, - request: httpx.Request, - response: httpx.Response, - data_class: type[T] | None, - allow_null_body: bool, + self, + request: httpx.Request, + response: httpx.Response, + data_class: type[T] | None, + allow_null_body: bool, ) -> T | None: try: request_event_id = request.headers.get("event-id") @@ -348,9 +350,9 @@ def logout(self) -> None: @property def is_logged_in(self) -> bool: return ( - self.__api_client.user_token is not None - and self.__token_expiration is not None - and self.__token_expiration > datetime.datetime.now() + self.__api_client.user_token is not None + and self.__token_expiration is not None + and self.__token_expiration > datetime.datetime.now() ) @property diff --git a/src/saic_ismart_client_ng/net/crypto.py b/src/saic_ismart_client_ng/net/crypto.py index 1aa9790..1370026 100644 --- a/src/saic_ismart_client_ng/net/crypto.py +++ b/src/saic_ismart_client_ng/net/crypto.py @@ -111,7 +111,7 @@ def encrypt_request( request_content, key_hex, iv_hex ).encode("utf-8") - original_request_headers["User-Agent"] = "okhttp/3.14.9" + original_request_headers["User-Agent"] = "Europe/2.1.0 (iPad; iOS 18.5; Scale/2.00)" original_request_headers["Content-Type"] = f"{modified_content_type};charset=utf-8" original_request_headers["Accept"] = "application/json" original_request_headers["Accept-Encoding"] = "gzip" From e4ae00ded50803afa140d33c779f636ce6ff1d6c Mon Sep 17 00:00:00 2001 From: Giovanni Condello Date: Sat, 31 May 2025 11:31:38 +0200 Subject: [PATCH 3/4] Version bump to 0.8.2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 677278d..7c604ef 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "saic_ismart_client_ng" -version = "0.8.1" +version = "0.8.2" description = "SAIC next gen client library (MG iSMART)" authors = [ { name = "Giovanni Condello", email = "saic-python-client@nanomad.net" }, From 0547ad8e36829effdb82f9872d62c4b24905fe6c Mon Sep 17 00:00:00 2001 From: Giovanni Condello Date: Sat, 31 May 2025 11:34:18 +0200 Subject: [PATCH 4/4] Fix poetry errors --- src/saic_ismart_client_ng/api/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/saic_ismart_client_ng/api/base.py b/src/saic_ismart_client_ng/api/base.py index b4e38a2..000c542 100644 --- a/src/saic_ismart_client_ng/api/base.py +++ b/src/saic_ismart_client_ng/api/base.py @@ -1,8 +1,8 @@ from __future__ import annotations +from dataclasses import asdict import datetime import logging -from dataclasses import asdict from typing import ( TYPE_CHECKING, Any,