From 00f67f6d419976da63abb9e477dd0bfc3bf7cf75 Mon Sep 17 00:00:00 2001 From: vuffiraa72 Date: Sat, 23 May 2026 09:59:27 +0200 Subject: [PATCH 01/15] cupra: dynamic token auth header and central user-agent --- packages/modules/vehicles/cupra/libcupra.py | 28 ++++++++++----------- packages/modules/vehicles/cupra/soc_test.py | 16 ++++++++++-- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/packages/modules/vehicles/cupra/libcupra.py b/packages/modules/vehicles/cupra/libcupra.py index 9332ff2acd..45f94d27a0 100755 --- a/packages/modules/vehicles/cupra/libcupra.py +++ b/packages/modules/vehicles/cupra/libcupra.py @@ -17,12 +17,15 @@ LOGIN_HANDLER_BASE = "https://identity.vwgroup.io" API_BASE = "https://ola.prod.code.seat.cloud.vwgroup.com" CLIENT_ID = "99a5b77d-bd88-4d53-b4e5-a539c60694a3@apps_vw-dilab_com" +USER_AGENT = "OLACupra/2.16.0 (Android 14; Pixel 8; Google) Mobile" class cupra: def __init__(self, session): self.session = session - self.headers = {} + self.headers = { + 'app-market': 'android' + } self.log = logging.getLogger(__name__) self.jobs_string = 'all' @@ -97,6 +100,14 @@ def to_camel_case(s): return {to_camel_case(k): v for k, v in json.items()} + def token_headers(self): + basic_auth = base64.b64encode(f"{CLIENT_ID}:".encode('utf-8')).decode('utf-8') + return { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Authorization': f'BASIC {basic_auth}', + 'User-Agent': USER_AGENT, + } + async def connect(self, username, password): self.set_credentials(username, password) return (await self.reconnect()) @@ -200,13 +211,7 @@ async def reconnect(self): form['redirect_uri'] = "seat://oauth-callback" form['grant_type'] = 'authorization_code' form['code_verifier'] = code_verifier - headers = {} - headers['Content-Type'] = 'application/x-www-form-urlencoded' - headers['Authorization'] = 'BASIC OTlhNWI3N2QtYmQ4OC00ZDUzLWI0ZTUtYTUzOWM2MDY5NGEzQGFwcHNfdnctZGlsYWJfY29tOg==' - headers['User-Agent'] = ( - 'SEATApp/2.5.0 (com.seat.myseat.ola; build:202410171614; ' - 'iOS 15.8.3) Alamofire/5.7.0 Mobile' - ) + headers = self.token_headers() response = await self.session.post(API_BASE + '/authorization/api/v1/token', headers=headers, data=form) @@ -233,12 +238,7 @@ async def refresh_tokens(self): form['client_id'] = CLIENT_ID form['grant_type'] = 'refresh_token' form['refresh_token'] = self.tokens["refreshToken"] - headers = {} - headers['Content-Type'] = 'application/x-www-form-urlencoded' - headers['User-Agent'] = ( - 'SEATApp/2.5.0 (com.seat.myseat.ola; build:202410171614; ' - 'iOS 15.8.3) Alamofire/5.7.0 Mobile' - ) + headers = self.token_headers() response = await self.session.post(API_BASE + '/authorization/api/v1/token', headers=headers, data=form) diff --git a/packages/modules/vehicles/cupra/soc_test.py b/packages/modules/vehicles/cupra/soc_test.py index ade6ce013c..defbad2fa3 100644 --- a/packages/modules/vehicles/cupra/soc_test.py +++ b/packages/modules/vehicles/cupra/soc_test.py @@ -7,7 +7,8 @@ from modules.vehicles.cupra import api from modules.vehicles.cupra.soc import create_vehicle from modules.vehicles.cupra.config import Cupra, CupraConfiguration -from modules.vehicles.cupra.libcupra import cupra as CupraApi +from modules.vehicles.cupra.libcupra import cupra as CupraApi, CLIENT_ID, USER_AGENT +import base64 class TestCupra: @@ -93,9 +94,20 @@ async def get_side_effect(*args, **kwargs): def cupra_instance(self, mock_session): instance = CupraApi(mock_session) instance.set_vin("test_vin") - instance.headers = {"Authorization": "Bearer test_token"} + instance.headers = { + "Authorization": "Bearer test_token", + "app-market": "android", + } return instance + def test_token_headers_use_dynamic_basic_auth_and_user_agent(self, cupra_instance): + headers = cupra_instance.token_headers() + expected_basic = base64.b64encode(f"{CLIENT_ID}:".encode("utf-8")).decode("utf-8") + + assert headers["Content-Type"] == "application/x-www-form-urlencoded" + assert headers["Authorization"] == f"BASIC {expected_basic}" + assert headers["User-Agent"] == USER_AGENT + def test_get_status_success(self, cupra_instance, mock_session): # setup status_response_data = { From 4331f44e7a630294ac196b139d5a9b985c9f1d8c Mon Sep 17 00:00:00 2001 From: vuffiraa72 Date: Mon, 25 May 2026 10:29:30 +0200 Subject: [PATCH 02/15] cupra: harden token exchange and api headers --- packages/modules/vehicles/cupra/libcupra.py | 20 ++++++++++++++------ packages/modules/vehicles/cupra/soc_test.py | 11 ++++++++--- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/packages/modules/vehicles/cupra/libcupra.py b/packages/modules/vehicles/cupra/libcupra.py index 45f94d27a0..3fc185a5f1 100755 --- a/packages/modules/vehicles/cupra/libcupra.py +++ b/packages/modules/vehicles/cupra/libcupra.py @@ -24,7 +24,9 @@ class cupra: def __init__(self, session): self.session = session self.headers = { - 'app-market': 'android' + 'app-market': 'android', + 'app-version': '2.16.0', + 'origin': 'app', } self.log = logging.getLogger(__name__) self.jobs_string = 'all' @@ -79,6 +81,7 @@ def password_form(self, text): def set_vin(self, vin): self.vin = vin + self.headers['VIN'] = vin def set_credentials(self, username, password): self.username = username @@ -103,9 +106,11 @@ def to_camel_case(s): def token_headers(self): basic_auth = base64.b64encode(f"{CLIENT_ID}:".encode('utf-8')).decode('utf-8') return { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Authorization': f'BASIC {basic_auth}', - 'User-Agent': USER_AGENT, + 'accept': '*/*', + 'content-type': 'application/x-www-form-urlencoded; charset=utf-8', + 'authorization': f'Basic {basic_auth}', + 'user-agent': USER_AGENT, + 'accept-language': 'de-de', } async def connect(self, username, password): @@ -118,7 +123,7 @@ async def reconnect(self): self.log.debug("Starting Cupra reconnect/auth flow") # Get authorize page - _scope = 'openid profile nickname birthdate phone' + _scope = 'openid profile nickname birthdate phone mbb cars address nationalIdentifier nationality profession email' payload = { 'client_id': CLIENT_ID, 'scope': _scope, @@ -217,7 +222,8 @@ async def reconnect(self): headers=headers, data=form) self.log.debug("Authorization code exchange status=%s", response.status) if response.status >= 400: - self.log.error("Login: Non-2xx response") + self.log.error("Login: Non-2xx response (%s), body=%s", + response.status, await response.text()) # Non 2xx response, failed return False self.tokens = self.convert_to_camel_case(await response.json()) @@ -243,6 +249,8 @@ async def refresh_tokens(self): response = await self.session.post(API_BASE + '/authorization/api/v1/token', headers=headers, data=form) if response.status >= 400: + self.log.error("Refresh token exchange failed (%s), body=%s", + response.status, await response.text()) return False self.tokens = self.convert_to_camel_case(await response.json()) diff --git a/packages/modules/vehicles/cupra/soc_test.py b/packages/modules/vehicles/cupra/soc_test.py index defbad2fa3..4c0398879a 100644 --- a/packages/modules/vehicles/cupra/soc_test.py +++ b/packages/modules/vehicles/cupra/soc_test.py @@ -97,6 +97,9 @@ def cupra_instance(self, mock_session): instance.headers = { "Authorization": "Bearer test_token", "app-market": "android", + "app-version": "2.16.0", + "origin": "app", + "VIN": "test_vin", } return instance @@ -104,9 +107,11 @@ def test_token_headers_use_dynamic_basic_auth_and_user_agent(self, cupra_instanc headers = cupra_instance.token_headers() expected_basic = base64.b64encode(f"{CLIENT_ID}:".encode("utf-8")).decode("utf-8") - assert headers["Content-Type"] == "application/x-www-form-urlencoded" - assert headers["Authorization"] == f"BASIC {expected_basic}" - assert headers["User-Agent"] == USER_AGENT + assert headers["accept"] == "*/*" + assert headers["content-type"] == "application/x-www-form-urlencoded; charset=utf-8" + assert headers["authorization"] == f"Basic {expected_basic}" + assert headers["user-agent"] == USER_AGENT + assert headers["accept-language"] == "de-de" def test_get_status_success(self, cupra_instance, mock_session): # setup From 95b6e0e522deddb46f8a048bd714ba98018281c4 Mon Sep 17 00:00:00 2001 From: vuffiraa72 Date: Mon, 25 May 2026 10:34:48 +0200 Subject: [PATCH 03/15] cupra: wrap scope string to satisfy flake8 --- packages/modules/vehicles/cupra/libcupra.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/modules/vehicles/cupra/libcupra.py b/packages/modules/vehicles/cupra/libcupra.py index 3fc185a5f1..209b8733c9 100755 --- a/packages/modules/vehicles/cupra/libcupra.py +++ b/packages/modules/vehicles/cupra/libcupra.py @@ -123,7 +123,10 @@ async def reconnect(self): self.log.debug("Starting Cupra reconnect/auth flow") # Get authorize page - _scope = 'openid profile nickname birthdate phone mbb cars address nationalIdentifier nationality profession email' + _scope = ( + 'openid profile nickname birthdate phone mbb cars address ' + 'nationalIdentifier nationality profession email' + ) payload = { 'client_id': CLIENT_ID, 'scope': _scope, From 406c2df50388b62ded0a16519d09cec54e18dc20 Mon Sep 17 00:00:00 2001 From: vuffiraa72 Date: Mon, 25 May 2026 11:45:04 +0200 Subject: [PATCH 04/15] cupra: minimal reset to working auth profile --- packages/modules/vehicles/cupra/libcupra.py | 18 +++++------------- packages/modules/vehicles/cupra/soc_test.py | 11 +++-------- 2 files changed, 8 insertions(+), 21 deletions(-) diff --git a/packages/modules/vehicles/cupra/libcupra.py b/packages/modules/vehicles/cupra/libcupra.py index 209b8733c9..4183121331 100755 --- a/packages/modules/vehicles/cupra/libcupra.py +++ b/packages/modules/vehicles/cupra/libcupra.py @@ -24,9 +24,7 @@ class cupra: def __init__(self, session): self.session = session self.headers = { - 'app-market': 'android', - 'app-version': '2.16.0', - 'origin': 'app', + 'app-market': 'android' } self.log = logging.getLogger(__name__) self.jobs_string = 'all' @@ -81,7 +79,6 @@ def password_form(self, text): def set_vin(self, vin): self.vin = vin - self.headers['VIN'] = vin def set_credentials(self, username, password): self.username = username @@ -106,11 +103,9 @@ def to_camel_case(s): def token_headers(self): basic_auth = base64.b64encode(f"{CLIENT_ID}:".encode('utf-8')).decode('utf-8') return { - 'accept': '*/*', - 'content-type': 'application/x-www-form-urlencoded; charset=utf-8', - 'authorization': f'Basic {basic_auth}', - 'user-agent': USER_AGENT, - 'accept-language': 'de-de', + 'Content-Type': 'application/x-www-form-urlencoded', + 'Authorization': f'Basic {basic_auth}', + 'User-Agent': USER_AGENT, } async def connect(self, username, password): @@ -123,10 +118,7 @@ async def reconnect(self): self.log.debug("Starting Cupra reconnect/auth flow") # Get authorize page - _scope = ( - 'openid profile nickname birthdate phone mbb cars address ' - 'nationalIdentifier nationality profession email' - ) + _scope = 'openid profile nickname birthdate phone' payload = { 'client_id': CLIENT_ID, 'scope': _scope, diff --git a/packages/modules/vehicles/cupra/soc_test.py b/packages/modules/vehicles/cupra/soc_test.py index 4c0398879a..34f80352e3 100644 --- a/packages/modules/vehicles/cupra/soc_test.py +++ b/packages/modules/vehicles/cupra/soc_test.py @@ -97,9 +97,6 @@ def cupra_instance(self, mock_session): instance.headers = { "Authorization": "Bearer test_token", "app-market": "android", - "app-version": "2.16.0", - "origin": "app", - "VIN": "test_vin", } return instance @@ -107,11 +104,9 @@ def test_token_headers_use_dynamic_basic_auth_and_user_agent(self, cupra_instanc headers = cupra_instance.token_headers() expected_basic = base64.b64encode(f"{CLIENT_ID}:".encode("utf-8")).decode("utf-8") - assert headers["accept"] == "*/*" - assert headers["content-type"] == "application/x-www-form-urlencoded; charset=utf-8" - assert headers["authorization"] == f"Basic {expected_basic}" - assert headers["user-agent"] == USER_AGENT - assert headers["accept-language"] == "de-de" + assert headers["Content-Type"] == "application/x-www-form-urlencoded" + assert headers["Authorization"] == f"Basic {expected_basic}" + assert headers["User-Agent"] == USER_AGENT def test_get_status_success(self, cupra_instance, mock_session): # setup From c7656a77e28510063511d9e6b3a086b161601fec Mon Sep 17 00:00:00 2001 From: vuffiraa72 Date: Mon, 25 May 2026 11:49:04 +0200 Subject: [PATCH 05/15] cupra: restore legacy seatapp user-agent --- packages/modules/vehicles/cupra/libcupra.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/modules/vehicles/cupra/libcupra.py b/packages/modules/vehicles/cupra/libcupra.py index 4183121331..55794f6336 100755 --- a/packages/modules/vehicles/cupra/libcupra.py +++ b/packages/modules/vehicles/cupra/libcupra.py @@ -17,7 +17,10 @@ LOGIN_HANDLER_BASE = "https://identity.vwgroup.io" API_BASE = "https://ola.prod.code.seat.cloud.vwgroup.com" CLIENT_ID = "99a5b77d-bd88-4d53-b4e5-a539c60694a3@apps_vw-dilab_com" -USER_AGENT = "OLACupra/2.16.0 (Android 14; Pixel 8; Google) Mobile" +USER_AGENT = ( + "SEATApp/2.5.0 (com.seat.myseat.ola; build:202410171614; " + "iOS 15.8.3) Alamofire/5.7.0 Mobile" +) class cupra: From 5780d928a2678b078f44eb0e53a6537f26c7d761 Mon Sep 17 00:00:00 2001 From: vuffiraa72 Date: Mon, 25 May 2026 21:59:42 +0200 Subject: [PATCH 06/15] use cupra login flow --- packages/modules/vehicles/cupra/libcupra.py | 25 ++++++++++++--------- packages/modules/vehicles/cupra/soc_test.py | 2 +- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/packages/modules/vehicles/cupra/libcupra.py b/packages/modules/vehicles/cupra/libcupra.py index 55794f6336..0c4ec580fb 100755 --- a/packages/modules/vehicles/cupra/libcupra.py +++ b/packages/modules/vehicles/cupra/libcupra.py @@ -16,18 +16,18 @@ LOGIN_BASE = "https://identity.vwgroup.io/oidc/v1" LOGIN_HANDLER_BASE = "https://identity.vwgroup.io" API_BASE = "https://ola.prod.code.seat.cloud.vwgroup.com" -CLIENT_ID = "99a5b77d-bd88-4d53-b4e5-a539c60694a3@apps_vw-dilab_com" -USER_AGENT = ( - "SEATApp/2.5.0 (com.seat.myseat.ola; build:202410171614; " - "iOS 15.8.3) Alamofire/5.7.0 Mobile" -) +CLIENT_ID = "3c756d46-f1ba-4d78-9f9a-cff0d5292d51@apps_vw-dilab_com" +USER_AGENT = "OLACupra/2.16.0 (Android 14; Pixel 8; Google) Mobile" class cupra: def __init__(self, session): self.session = session self.headers = { - 'app-market': 'android' + 'user-agent': USER_AGENT, + 'app-brand': 'cupra', + 'app-market': 'android', + 'app-version': '2.16.0', } self.log = logging.getLogger(__name__) self.jobs_string = 'all' @@ -121,13 +121,16 @@ async def reconnect(self): self.log.debug("Starting Cupra reconnect/auth flow") # Get authorize page - _scope = 'openid profile nickname birthdate phone' + _scope = ( + 'openid profile nickname birthdate phone mbb cars address ' + 'nationalIdentifier nationality profession badge driversLicense' + ) payload = { 'client_id': CLIENT_ID, 'scope': _scope, 'response_type': 'code', 'nonce': secrets.token_urlsafe(12), - 'redirect_uri': 'seat://oauth-callback', + 'redirect_uri': 'cupra://oauth-callback', 'state': str(uuid.uuid4()), 'code_challenge': code_challenge, 'code_challenge_method': 'S256' @@ -191,7 +194,7 @@ async def reconnect(self): url = response.headers['Location'] self.log.debug("Redirect: status=%s location=%s", response.status, url) - if (url.split(':')[0] == "seat"): + if (url.split(':')[0] == "cupra"): if not ('code' in url): self.log.error("Missing authorization code") return False @@ -211,7 +214,7 @@ async def reconnect(self): form = {} form['code'] = query['code'] form['client_id'] = CLIENT_ID - form['redirect_uri'] = "seat://oauth-callback" + form['redirect_uri'] = "cupra://oauth-callback" form['grant_type'] = 'authorization_code' form['code_verifier'] = code_verifier headers = self.token_headers() @@ -258,7 +261,7 @@ async def refresh_tokens(self): return True async def get_status(self): - status_url = f"{API_BASE}/vehicles/{self.vin}/charging/status" + status_url = f"{API_BASE}/v1/vehicles/{self.vin}/charging/status" mileage_url = f"{API_BASE}/v1/vehicles/{self.vin}/mileage" response = await self.session.get(status_url, headers=self.headers) diff --git a/packages/modules/vehicles/cupra/soc_test.py b/packages/modules/vehicles/cupra/soc_test.py index 34f80352e3..012e0e497e 100644 --- a/packages/modules/vehicles/cupra/soc_test.py +++ b/packages/modules/vehicles/cupra/soc_test.py @@ -141,7 +141,7 @@ async def side_effect_func(*args, **kwargs): assert status['charging']['batteryStatus']['value']['odometer'] == 77889 assert mock_session.get.call_count == 2 mock_session.get.assert_any_call( - "https://ola.prod.code.seat.cloud.vwgroup.com/vehicles/test_vin/charging/status", + "https://ola.prod.code.seat.cloud.vwgroup.com/v1/vehicles/test_vin/charging/status", headers=cupra_instance.headers ) mock_session.get.assert_any_call( From 484872f2a6f0d746baae85e4b21126cb62502b33 Mon Sep 17 00:00:00 2001 From: vuffiraa72 Date: Wed, 27 May 2026 07:11:10 +0200 Subject: [PATCH 07/15] initialize headers before status update --- packages/modules/vehicles/cupra/libcupra.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/modules/vehicles/cupra/libcupra.py b/packages/modules/vehicles/cupra/libcupra.py index 0c4ec580fb..427ce9e34b 100755 --- a/packages/modules/vehicles/cupra/libcupra.py +++ b/packages/modules/vehicles/cupra/libcupra.py @@ -23,12 +23,7 @@ class cupra: def __init__(self, session): self.session = session - self.headers = { - 'user-agent': USER_AGENT, - 'app-brand': 'cupra', - 'app-market': 'android', - 'app-version': '2.16.0', - } + self.headers = {} self.log = logging.getLogger(__name__) self.jobs_string = 'all' @@ -261,6 +256,10 @@ async def refresh_tokens(self): return True async def get_status(self): + self.headers['user-agent'] = USER_AGENT + self.headers['app-brand'] = 'cupra' + self.headers['app-market'] = 'android' + self.headers['app-version'] = '2.16.0' status_url = f"{API_BASE}/v1/vehicles/{self.vin}/charging/status" mileage_url = f"{API_BASE}/v1/vehicles/{self.vin}/mileage" response = await self.session.get(status_url, headers=self.headers) From 345f7d94fb32b05b21afa1741201d48b129b55bf Mon Sep 17 00:00:00 2001 From: vuffiraa72 Date: Wed, 27 May 2026 18:30:09 +0200 Subject: [PATCH 08/15] allign to json response --- packages/modules/vehicles/cupra/libcupra.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/modules/vehicles/cupra/libcupra.py b/packages/modules/vehicles/cupra/libcupra.py index 427ce9e34b..b51e663e95 100755 --- a/packages/modules/vehicles/cupra/libcupra.py +++ b/packages/modules/vehicles/cupra/libcupra.py @@ -297,9 +297,9 @@ async def get_status(self): odometer = mileage_data.get('mileageKm', None) battery_value = { - 'currentSOC_pct': status_data['status']['battery']['currentSOC_pct'], - 'cruisingRangeElectric_km': status_data['status']['battery']['cruisingRangeElectric_km'], - 'carCapturedTimestamp': status_data['status']['battery']['carCapturedTimestamp'], + 'currentSOC_pct': status_data['battery']['currentSocPercentage'], + 'cruisingRangeElectric_km': status_data['battery']['estimatedRangeInKm'], + # 'carCapturedTimestamp': status_data['status']['battery']['carCapturedTimestamp'], 'odometer': odometer, } From e391c5410e0392a5ecc9a35d102f0342d1e31053 Mon Sep 17 00:00:00 2001 From: vuffiraa72 Date: Wed, 27 May 2026 19:17:11 +0200 Subject: [PATCH 09/15] current timestamp as fallback --- packages/modules/vehicles/cupra/libcupra.py | 6 +++++- packages/modules/vehicles/cupra/soc_test.py | 20 ++++++++------------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/packages/modules/vehicles/cupra/libcupra.py b/packages/modules/vehicles/cupra/libcupra.py index b51e663e95..66adfb0460 100755 --- a/packages/modules/vehicles/cupra/libcupra.py +++ b/packages/modules/vehicles/cupra/libcupra.py @@ -7,6 +7,7 @@ import uuid import base64 import hashlib +import datetime from helpermodules.utils.error_handling import ImportErrorContext with ImportErrorContext(): @@ -299,7 +300,10 @@ async def get_status(self): battery_value = { 'currentSOC_pct': status_data['battery']['currentSocPercentage'], 'cruisingRangeElectric_km': status_data['battery']['estimatedRangeInKm'], - # 'carCapturedTimestamp': status_data['status']['battery']['carCapturedTimestamp'], + # use current timestamp as a fallback, as the API field is missing + 'carCapturedTimestamp': status_data['battery'].get( + 'carCapturedTimestamp', + datetime.datetime.now(datetime.UTC).isoformat() + 'Z'), 'odometer': odometer, } diff --git a/packages/modules/vehicles/cupra/soc_test.py b/packages/modules/vehicles/cupra/soc_test.py index 012e0e497e..2fd26501d0 100644 --- a/packages/modules/vehicles/cupra/soc_test.py +++ b/packages/modules/vehicles/cupra/soc_test.py @@ -111,12 +111,10 @@ def test_token_headers_use_dynamic_basic_auth_and_user_agent(self, cupra_instanc def test_get_status_success(self, cupra_instance, mock_session): # setup status_response_data = { - "status": { - "battery": { - "currentSOC_pct": 67, - "cruisingRangeElectric_km": 305, - "carCapturedTimestamp": "2026-03-29T11:20:00Z" - } + "battery": { + "currentSocPercentage": 67, + "estimatedRangeInKm": 305, + "carCapturedTimestamp": "2026-03-29T11:20:00Z" } } mileage_response_data = { @@ -152,12 +150,10 @@ async def side_effect_func(*args, **kwargs): def test_get_status_mileage_error_sets_odometer_none(self, cupra_instance, mock_session): # setup status_response_data = { - "status": { - "battery": { - "currentSOC_pct": 67, - "cruisingRangeElectric_km": 305, - "carCapturedTimestamp": "2026-03-29T11:20:00Z" - } + "battery": { + "currentSocPercentage": 67, + "estimatedRangeInKm": 305, + "carCapturedTimestamp": "2026-03-29T11:20:00Z" } } mileage_error_response = { From 60920c02ce1f961378138369b1303d108049fe9f Mon Sep 17 00:00:00 2001 From: vuffiraa72 Date: Wed, 27 May 2026 19:29:18 +0200 Subject: [PATCH 10/15] fix timestamp --- packages/modules/vehicles/cupra/libcupra.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/modules/vehicles/cupra/libcupra.py b/packages/modules/vehicles/cupra/libcupra.py index 66adfb0460..b4e0c30e2e 100755 --- a/packages/modules/vehicles/cupra/libcupra.py +++ b/packages/modules/vehicles/cupra/libcupra.py @@ -7,8 +7,8 @@ import uuid import base64 import hashlib -import datetime +from datetime import datetime, timezone from helpermodules.utils.error_handling import ImportErrorContext with ImportErrorContext(): import lxml.html @@ -303,7 +303,7 @@ async def get_status(self): # use current timestamp as a fallback, as the API field is missing 'carCapturedTimestamp': status_data['battery'].get( 'carCapturedTimestamp', - datetime.datetime.now(datetime.UTC).isoformat() + 'Z'), + datetime.now(timezone.utc).isoformat() + 'Z'), 'odometer': odometer, } From 6447a0ff586118b07221a376ab0b800c89ff722b Mon Sep 17 00:00:00 2001 From: vuffiraa72 Date: Wed, 27 May 2026 19:54:22 +0200 Subject: [PATCH 11/15] fix timestamp --- packages/modules/vehicles/cupra/libcupra.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/modules/vehicles/cupra/libcupra.py b/packages/modules/vehicles/cupra/libcupra.py index b4e0c30e2e..4346835d3e 100755 --- a/packages/modules/vehicles/cupra/libcupra.py +++ b/packages/modules/vehicles/cupra/libcupra.py @@ -303,7 +303,7 @@ async def get_status(self): # use current timestamp as a fallback, as the API field is missing 'carCapturedTimestamp': status_data['battery'].get( 'carCapturedTimestamp', - datetime.now(timezone.utc).isoformat() + 'Z'), + datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")), 'odometer': odometer, } From 3a0d10cf2ec9202d013ad96793ad520dc5b84ba3 Mon Sep 17 00:00:00 2001 From: vuffiraa72 Date: Wed, 27 May 2026 22:10:36 +0200 Subject: [PATCH 12/15] fetch capture timestamp from other endpoint --- packages/modules/vehicles/cupra/libcupra.py | 18 ++++++++++++---- packages/modules/vehicles/cupra/soc_test.py | 23 +++++++++++++++------ 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/packages/modules/vehicles/cupra/libcupra.py b/packages/modules/vehicles/cupra/libcupra.py index 4346835d3e..9ef715a80e 100755 --- a/packages/modules/vehicles/cupra/libcupra.py +++ b/packages/modules/vehicles/cupra/libcupra.py @@ -262,6 +262,7 @@ async def get_status(self): self.headers['app-market'] = 'android' self.headers['app-version'] = '2.16.0' status_url = f"{API_BASE}/v1/vehicles/{self.vin}/charging/status" + statusv2_url = f"{API_BASE}/v2/vehicles/{self.vin}/status" mileage_url = f"{API_BASE}/v1/vehicles/{self.vin}/mileage" response = await self.session.get(status_url, headers=self.headers) @@ -297,13 +298,22 @@ async def get_status(self): self.log.debug(f"Mileage data from Cupra API: {mileage_data}") odometer = mileage_data.get('mileageKm', None) + # Fetch additional status data from v2 endpoint + response = await self.session.get(statusv2_url, headers=self.headers) + if response.status >= 400: + self.log.error("Get status v2 failed") + else: + statusv2_data = await response.json() + self.log.debug(f"Status v2 data from Cupra API: {statusv2_data}") + # use current timestamp as a fallback, as the API field is missing + carCapturedTimestamp = statusv2_data.get( + 'updatedAt', + datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")) + battery_value = { 'currentSOC_pct': status_data['battery']['currentSocPercentage'], 'cruisingRangeElectric_km': status_data['battery']['estimatedRangeInKm'], - # use current timestamp as a fallback, as the API field is missing - 'carCapturedTimestamp': status_data['battery'].get( - 'carCapturedTimestamp', - datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")), + 'carCapturedTimestamp': status_data['battery'].get('updatedAt', carCapturedTimestamp), 'odometer': odometer, } diff --git a/packages/modules/vehicles/cupra/soc_test.py b/packages/modules/vehicles/cupra/soc_test.py index 2fd26501d0..6b4ca994a9 100644 --- a/packages/modules/vehicles/cupra/soc_test.py +++ b/packages/modules/vehicles/cupra/soc_test.py @@ -113,16 +113,19 @@ def test_get_status_success(self, cupra_instance, mock_session): status_response_data = { "battery": { "currentSocPercentage": 67, - "estimatedRangeInKm": 305, - "carCapturedTimestamp": "2026-03-29T11:20:00Z" + "estimatedRangeInKm": 305 } } mileage_response_data = { "mileageKm": 77889 } + statusv2_response_data = { + "updatedAt": "2026-03-29T11:20:00Z" + } responses = [ MockAiohttpResponse(status_response_data, 200), - MockAiohttpResponse(mileage_response_data, 200) + MockAiohttpResponse(mileage_response_data, 200), + MockAiohttpResponse(statusv2_response_data, 200) ] async def side_effect_func(*args, **kwargs): @@ -137,7 +140,7 @@ async def side_effect_func(*args, **kwargs): assert status['charging']['batteryStatus']['value']['cruisingRangeElectric_km'] == 305 assert status['charging']['batteryStatus']['value']['carCapturedTimestamp'] == "2026-03-29T11:20:00Z" assert status['charging']['batteryStatus']['value']['odometer'] == 77889 - assert mock_session.get.call_count == 2 + assert mock_session.get.call_count == 3 mock_session.get.assert_any_call( "https://ola.prod.code.seat.cloud.vwgroup.com/v1/vehicles/test_vin/charging/status", headers=cupra_instance.headers @@ -146,6 +149,10 @@ async def side_effect_func(*args, **kwargs): "https://ola.prod.code.seat.cloud.vwgroup.com/v1/vehicles/test_vin/mileage", headers=cupra_instance.headers ) + mock_session.get.assert_any_call( + "https://ola.prod.code.seat.cloud.vwgroup.com/v2/vehicles/test_vin/status", + headers=cupra_instance.headers + ) def test_get_status_mileage_error_sets_odometer_none(self, cupra_instance, mock_session): # setup @@ -153,15 +160,19 @@ def test_get_status_mileage_error_sets_odometer_none(self, cupra_instance, mock_ "battery": { "currentSocPercentage": 67, "estimatedRangeInKm": 305, - "carCapturedTimestamp": "2026-03-29T11:20:00Z" + "updatedAt": "2026-03-29T11:20:00Z" } } mileage_error_response = { "error": "service_unavailable" } + statusv2_response_data = { + "error": "service_unavailable" + } responses = [ MockAiohttpResponse(status_response_data, 200), - MockAiohttpResponse(mileage_error_response, 503) + MockAiohttpResponse(mileage_error_response, 503), + MockAiohttpResponse(statusv2_response_data, 200) ] async def side_effect_func(*args, **kwargs): From d8e14b07b8339c62e74686631a0329d2bce9afeb Mon Sep 17 00:00:00 2001 From: vuffiraa72 Date: Wed, 27 May 2026 22:17:18 +0200 Subject: [PATCH 13/15] remove millisecs from timestamp --- packages/modules/vehicles/cupra/libcupra.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/modules/vehicles/cupra/libcupra.py b/packages/modules/vehicles/cupra/libcupra.py index 9ef715a80e..c081e7fe88 100755 --- a/packages/modules/vehicles/cupra/libcupra.py +++ b/packages/modules/vehicles/cupra/libcupra.py @@ -309,6 +309,9 @@ async def get_status(self): carCapturedTimestamp = statusv2_data.get( 'updatedAt', datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")) + carCapturedTimestamp = carCapturedTimestamp.split('.')[0] + if not carCapturedTimestamp.endswith('Z'): + carCapturedTimestamp += 'Z' battery_value = { 'currentSOC_pct': status_data['battery']['currentSocPercentage'], From 09e1a179acba046aa47aa9348b62be3d1c4138f2 Mon Sep 17 00:00:00 2001 From: vuffiraa72 Date: Wed, 27 May 2026 22:21:25 +0200 Subject: [PATCH 14/15] simplify timestamp --- packages/modules/vehicles/cupra/libcupra.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/modules/vehicles/cupra/libcupra.py b/packages/modules/vehicles/cupra/libcupra.py index c081e7fe88..aff42833a7 100755 --- a/packages/modules/vehicles/cupra/libcupra.py +++ b/packages/modules/vehicles/cupra/libcupra.py @@ -306,10 +306,7 @@ async def get_status(self): statusv2_data = await response.json() self.log.debug(f"Status v2 data from Cupra API: {statusv2_data}") # use current timestamp as a fallback, as the API field is missing - carCapturedTimestamp = statusv2_data.get( - 'updatedAt', - datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")) - carCapturedTimestamp = carCapturedTimestamp.split('.')[0] + carCapturedTimestamp = statusv2_data.get('updatedAt', datetime.now(timezone.utc).isoformat()).split('.')[0] if not carCapturedTimestamp.endswith('Z'): carCapturedTimestamp += 'Z' From 2494ea38aefce49697ed261752f4dafc7276f4cb Mon Sep 17 00:00:00 2001 From: vuffiraa72 Date: Thu, 28 May 2026 07:07:56 +0200 Subject: [PATCH 15/15] fix flake8 --- packages/modules/vehicles/cupra/libcupra.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/modules/vehicles/cupra/libcupra.py b/packages/modules/vehicles/cupra/libcupra.py index aff42833a7..93589b219e 100755 --- a/packages/modules/vehicles/cupra/libcupra.py +++ b/packages/modules/vehicles/cupra/libcupra.py @@ -309,7 +309,7 @@ async def get_status(self): carCapturedTimestamp = statusv2_data.get('updatedAt', datetime.now(timezone.utc).isoformat()).split('.')[0] if not carCapturedTimestamp.endswith('Z'): carCapturedTimestamp += 'Z' - + battery_value = { 'currentSOC_pct': status_data['battery']['currentSocPercentage'], 'cruisingRangeElectric_km': status_data['battery']['estimatedRangeInKm'],