volkswagencarnet 5.0.0b2__py3-none-any.whl → 5.0.0b4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of volkswagencarnet might be problematic. Click here for more details.

@@ -1,40 +1,46 @@
1
1
  #!/usr/bin/env python3
2
- """Communicate with Volkswagen Connect services."""
3
-
2
+ """Communicate with We Connect services."""
4
3
  from __future__ import annotations
5
4
 
6
- import asyncio
7
- from datetime import UTC, datetime, timedelta
8
5
  import hashlib
9
- from json import dumps as to_json
10
- import logging
11
- from random import randint, random
12
6
  import re
13
- from urllib.parse import parse_qs, urljoin, urlparse
7
+ import secrets
8
+ import time
9
+ from base64 import b64encode, urlsafe_b64encode
10
+ from datetime import timedelta, datetime, timezone
11
+ from random import random, randint
12
+ from sys import version_info
14
13
 
14
+ import asyncio
15
+ import jwt
16
+ import logging
15
17
  from aiohttp import ClientTimeout, client_exceptions
16
18
  from aiohttp.hdrs import METH_GET, METH_POST, METH_PUT
17
19
  from bs4 import BeautifulSoup
18
- import jwt
20
+ from json import dumps as to_json
21
+ from urllib.parse import urljoin, parse_qs, urlparse
19
22
 
23
+ from volkswagencarnet.vw_exceptions import AuthenticationException
20
24
  from .vw_const import (
21
- APP_URI,
22
- BASE_API,
23
- BASE_AUTH,
24
- BASE_SESSION,
25
25
  BRAND,
26
- CLIENT,
27
26
  COUNTRY,
28
- HEADERS_AUTH,
29
27
  HEADERS_SESSION,
28
+ HEADERS_AUTH,
29
+ BASE_SESSION,
30
+ BASE_API,
31
+ BASE_AUTH,
32
+ CLIENT,
30
33
  USER_AGENT,
34
+ APP_URI,
31
35
  )
32
36
  from .vw_utilities import json_loads
33
37
  from .vw_vehicle import Vehicle
34
38
 
35
39
  MAX_RETRIES_ON_RATE_LIMIT = 3
36
40
 
37
- _LOGGER = logging.getLogger(__name__) # pylint: disable=unreachable
41
+ version_info >= (3, 7) or exit("Python 3.7+ required")
42
+
43
+ _LOGGER = logging.getLogger(__name__)
38
44
 
39
45
  TIMEOUT = timedelta(seconds=30)
40
46
  JWT_ALGORITHMS = ["RS256"]
@@ -47,15 +53,7 @@ class Connection:
47
53
  _login_lock = asyncio.Lock()
48
54
 
49
55
  # Init connection class
50
- def __init__(
51
- self,
52
- session,
53
- username,
54
- password,
55
- fulldebug=False,
56
- country=COUNTRY,
57
- interval=timedelta(minutes=5),
58
- ) -> None:
56
+ def __init__(self, session, username, password, fulldebug=False, country=COUNTRY, interval=timedelta(minutes=5)):
59
57
  """Initialize."""
60
58
  self._x_client_id = None
61
59
  self._session = session
@@ -78,7 +76,7 @@ class Connection:
78
76
 
79
77
  self._vehicles = []
80
78
 
81
- _LOGGER.debug("Using service %s", self._session_base)
79
+ _LOGGER.debug(f"Using service {self._session_base}")
82
80
 
83
81
  self._jarCookie = ""
84
82
  self._state = {}
@@ -86,7 +84,7 @@ class Connection:
86
84
  self._service_status = {}
87
85
 
88
86
  def _clear_cookies(self):
89
- self._session._cookie_jar._cookies.clear() # pylint: disable=protected-access
87
+ self._session._cookie_jar._cookies.clear()
90
88
 
91
89
  # API Login
92
90
  async def doLogin(self, tries: int = 1):
@@ -98,9 +96,7 @@ class Connection:
98
96
  self._session_logged_in = await self._login("Legacy")
99
97
  if self._session_logged_in:
100
98
  break
101
- if i > tries:
102
- _LOGGER.error("Login failed after %s tries", tries)
103
- return False
99
+ _LOGGER.info("Something failed")
104
100
  await asyncio.sleep(random() * 5)
105
101
 
106
102
  if not self._session_logged_in:
@@ -115,12 +111,12 @@ class Connection:
115
111
  loaded_vehicles = await self.get(url=f"{BASE_API}/vehicle/v2/vehicles")
116
112
  # Add Vehicle class object for all VIN-numbers from account
117
113
  if loaded_vehicles.get("data") is not None:
118
- _LOGGER.debug("Found vehicle(s) associated with account")
114
+ _LOGGER.debug("Found vehicle(s) associated with account.")
119
115
  self._vehicles = []
120
116
  for vehicle in loaded_vehicles.get("data"):
121
117
  self._vehicles.append(Vehicle(self, vehicle.get("vin")))
122
118
  else:
123
- _LOGGER.warning("Failed to login to Volkswagen Connect API")
119
+ _LOGGER.warning("Failed to login to We Connect API.")
124
120
  self._session_logged_in = False
125
121
  return False
126
122
 
@@ -128,246 +124,251 @@ class Connection:
128
124
  await self.update()
129
125
  return True
130
126
 
131
- async def get_openid_config(self):
132
- """Get OpenID config."""
133
- if self._session_fulldebug:
134
- _LOGGER.debug("Requesting openid config")
135
- req = await self._session.get(
136
- url=f"{BASE_API}/login/v1/idk/openid-configuration"
137
- )
138
- if req.status != 200:
139
- _LOGGER.error("Failed to get OpenID configuration, status: %s", req.status)
140
- raise Exception("OpenID configuration error") # pylint: disable=broad-exception-raised
141
- return await req.json()
142
-
143
- async def get_authorization_page(self, authorization_endpoint, client):
144
- """Get authorization page (login page)."""
145
- # https://identity.vwgroup.io/oidc/v1/authorize?nonce={NONCE}&state={STATE}&response_type={TOKEN_TYPES}&scope={SCOPE}&redirect_uri={APP_URI}&client_id={CLIENT_ID}
146
- # https://identity.vwgroup.io/oidc/v1/authorize?client_id={CLIENT_ID}&scope={SCOPE}&response_type={TOKEN_TYPES}&redirect_uri={APP_URI}
147
- if self._session_fulldebug:
148
- _LOGGER.debug(
149
- 'Requesting authorization page from "%s"', authorization_endpoint
150
- )
151
- self._session_auth_headers.pop("Referer", None)
152
- self._session_auth_headers.pop("Origin", None)
153
- _LOGGER.debug('Request headers: "%s"', self._session_auth_headers)
154
-
155
- try:
156
- req = await self._session.get(
157
- url=authorization_endpoint,
158
- headers=self._session_auth_headers,
159
- allow_redirects=False,
160
- params={
161
- "redirect_uri": APP_URI,
162
- "response_type": CLIENT[client].get("TOKEN_TYPES"),
163
- "client_id": CLIENT[client].get("CLIENT_ID"),
164
- "scope": CLIENT[client].get("SCOPE"),
165
- },
166
- )
167
-
168
- # Check if the response contains a redirect location
169
- location = req.headers.get("Location")
170
- if not location:
171
- # pylint: disable=broad-exception-raised
172
- raise Exception(
173
- f"Missing 'Location' header, payload returned: {await req.content.read()}"
174
- )
175
-
176
- ref = urljoin(authorization_endpoint, location)
177
- if "error" in ref:
178
- parsed_query = parse_qs(urlparse(ref).query)
179
- error_msg = parsed_query.get("error", ["Unknown error"])[0]
180
- error_description = parsed_query.get(
181
- "error_description", ["No description"]
182
- )[0]
183
- _LOGGER.info("Authorization error: %s", error_description)
184
- raise Exception(error_msg) # pylint: disable=broad-exception-raised
185
-
186
- # If redirected, fetch the new location
187
- req = await self._session.get(
188
- url=ref, headers=self._session_auth_headers, allow_redirects=False
189
- )
190
-
191
- if req.status != 200:
192
- raise Exception("Failed to fetch authorization endpoint") # pylint: disable=broad-exception-raised
193
-
194
- return await req.text()
195
-
196
- except Exception as e:
197
- _LOGGER.warning("Error during fetching authorization page: %s", str(e))
198
- raise
199
-
200
- def extract_form_data(self, page_content, form_id):
201
- """Extract form data from a page."""
202
- soup = BeautifulSoup(page_content, "html.parser")
203
- form = soup.find("form", id=form_id)
204
- if form is None:
205
- raise Exception(f"Form with ID '{form_id}' not found.") # pylint: disable=broad-exception-raised
206
- return {
207
- input_field["name"]: input_field["value"]
208
- for input_field in form.find_all("input", type="hidden")
209
- }
210
-
211
- def extract_password_form_data(self, soup):
212
- """Extract password form data from a page."""
213
- pw_form = {}
214
- for script in soup.find_all("script"):
215
- if "src" in script.attrs or not script.string:
216
- continue
217
- script_text = script.string
218
-
219
- if "window._IDK" not in script_text:
220
- continue # Skip scripts that don't contain relevant data
221
- if re.match('"errorCode":"', script_text):
222
- raise Exception("Error code found in script data.") # pylint: disable=broad-exception-raised
223
-
224
- pw_form["relayState"] = re.search(
225
- '"relayState":"([a-f0-9]*)"', script_text
226
- )[1]
227
- pw_form["hmac"] = re.search('"hmac":"([a-f0-9]*)"', script_text)[1]
228
- pw_form["email"] = re.search('"email":"([^"]*)"', script_text)[1]
229
- pw_form["_csrf"] = re.search("csrf_token:\\s*'([^\"']*)'", script_text)[1]
230
-
231
- post_action = re.search('"postAction":\\s*"([^"\']*)"', script_text)[1]
232
- client_id = re.search('"clientId":\\s*"([^"\']*)"', script_text)[1]
233
- return pw_form, post_action, client_id
234
-
235
- raise Exception("Password form data not found in script.") # pylint: disable=broad-exception-raised
236
-
237
- async def post_form(self, session, url, headers, form_data, redirect=True):
238
- """Post a form and check for success."""
239
- req = await session.post(
240
- url, headers=headers, data=form_data, allow_redirects=redirect
241
- )
242
- if not redirect and req.status == 302:
243
- return req.headers["Location"]
244
- if req.status != 200:
245
- raise Exception("Form POST request failed.") # pylint: disable=broad-exception-raised
246
- return await req.text()
247
-
248
- async def handle_login_with_password(self, session, url, auth_headers, form_data):
249
- """Handle login with email and password."""
250
- return await self.post_form(session, url, auth_headers, form_data, False)
251
-
252
- async def follow_redirects(self, session, pw_url, redirect_location):
253
- """Handle redirects."""
254
- ref = urljoin(pw_url, redirect_location)
255
- max_depth = 10
256
- while not ref.startswith(APP_URI):
257
- if max_depth == 0:
258
- raise Exception("Too many redirects") # pylint: disable=broad-exception-raised
259
- response = await session.get(
260
- url=ref, headers=self._session_auth_headers, allow_redirects=False
261
- )
262
- if "Location" not in response.headers:
263
- _LOGGER.warning("Failed to find next redirect location")
264
- raise Exception("Redirect error") # pylint: disable=broad-exception-raised
265
- ref = urljoin(ref, response.headers["Location"])
266
- max_depth -= 1
267
- return ref
268
-
269
127
  async def _login(self, client="Legacy"):
270
128
  """Login function."""
271
129
 
130
+ # Helper functions
131
+ def getNonce():
132
+ """
133
+ Get a random nonce.
134
+
135
+ :return:
136
+ """
137
+ ts = "%d" % (time.time())
138
+ sha256 = hashlib.sha256()
139
+ sha256.update(ts.encode())
140
+ sha256.update(secrets.token_bytes(16))
141
+ return b64encode(sha256.digest()).decode("utf-8")[:-1]
142
+
143
+ def base64URLEncode(s):
144
+ """
145
+ Encode string as Base 64 in a URL safe way, stripping trailing '='.
146
+
147
+ :param s:
148
+ :return:
149
+ """
150
+ return urlsafe_b64encode(s).rstrip(b"=")
151
+
152
+ # Login starts here
272
153
  try:
273
- # Clear cookies and reset headers
154
+ # Get OpenID config:
274
155
  self._clear_cookies()
275
156
  self._session_headers = HEADERS_SESSION.copy()
276
157
  self._session_auth_headers = HEADERS_AUTH.copy()
277
-
278
- # Get OpenID configuration
279
- openid_config = await self.get_openid_config()
280
- authorization_endpoint = openid_config["authorization_endpoint"]
281
- token_endpoint = openid_config["token_endpoint"]
282
- auth_issuer = openid_config["issuer"]
283
-
284
- # Get authorization page
285
- authorization_page = await self.get_authorization_page(
286
- authorization_endpoint, client
287
- )
288
-
289
- # Extract form data
290
- mailform = self.extract_form_data(authorization_page, "emailPasswordForm")
291
- mailform["email"] = self._session_auth_username
292
- pe_url = auth_issuer + BeautifulSoup(
293
- authorization_page, "html.parser"
294
- ).find("form", id="emailPasswordForm").get("action")
158
+ if self._session_fulldebug:
159
+ _LOGGER.debug("Requesting openid config")
160
+ req = await self._session.get(url=f"{BASE_API}/login/v1/idk/openid-configuration")
161
+ if req.status != 200:
162
+ _LOGGER.debug("OpenId config error")
163
+ return False
164
+ response_data = await req.json()
165
+ authorization_endpoint = response_data["authorization_endpoint"]
166
+ token_endpoint = response_data["token_endpoint"]
167
+ auth_issuer = response_data["issuer"]
168
+
169
+ # Get authorization page (login page)
170
+ # https://identity.vwgroup.io/oidc/v1/authorize?nonce={NONCE}&state={STATE}&response_type={TOKEN_TYPES}&scope={SCOPE}&redirect_uri={APP_URI}&client_id={CLIENT_ID}
171
+ # https://identity.vwgroup.io/oidc/v1/authorize?client_id={CLIENT_ID}&scope={SCOPE}&response_type={TOKEN_TYPES}&redirect_uri={APP_URI}
172
+ if self._session_fulldebug:
173
+ _LOGGER.debug(f'Get authorization page from "{authorization_endpoint}"')
174
+ self._session_auth_headers.pop("Referer", None)
175
+ self._session_auth_headers.pop("Origin", None)
176
+ _LOGGER.debug(f'Request headers: "{self._session_auth_headers}"')
177
+ try:
178
+ code_verifier = base64URLEncode(secrets.token_bytes(32))
179
+ if len(code_verifier) < 43:
180
+ raise ValueError("Verifier too short. n_bytes must be > 30.")
181
+ elif len(code_verifier) > 128:
182
+ raise ValueError("Verifier too long. n_bytes must be < 97.")
183
+
184
+ req = await self._session.get(
185
+ url=authorization_endpoint,
186
+ headers=self._session_auth_headers,
187
+ allow_redirects=False,
188
+ params={
189
+ "redirect_uri": APP_URI,
190
+ "response_type": CLIENT[client].get("TOKEN_TYPES"),
191
+ "client_id": CLIENT[client].get("CLIENT_ID"),
192
+ "scope": CLIENT[client].get("SCOPE"),
193
+ },
194
+ )
195
+ if req.headers.get("Location", False):
196
+ ref = urljoin(authorization_endpoint, req.headers.get("Location", ""))
197
+ if "error" in ref:
198
+ error = parse_qs(urlparse(ref).query).get("error", "")[0]
199
+ if "error_description" in ref:
200
+ error_description = parse_qs(urlparse(ref).query).get("error_description", "")[0]
201
+ _LOGGER.info(f"Unable to login, {error_description}")
202
+ else:
203
+ _LOGGER.info("Unable to login.")
204
+ raise Exception(error)
205
+ else:
206
+ if self._session_fulldebug:
207
+ _LOGGER.debug(f'Got redirect to "{ref}"')
208
+ req = await self._session.get(
209
+ url=ref, headers=self._session_auth_headers, allow_redirects=False
210
+ )
211
+ else:
212
+ _LOGGER.warning("Unable to fetch authorization endpoint.")
213
+ raise Exception(f'Missing "location" header, payload returned: {await req.content.read()}')
214
+ except Exception as error:
215
+ _LOGGER.warning("Failed to get authorization endpoint")
216
+ raise error
217
+ if req.status != 200:
218
+ raise Exception("Fetching authorization endpoint failed")
219
+ else:
220
+ _LOGGER.debug("Got authorization endpoint")
221
+ try:
222
+ response_data = await req.text()
223
+ response_soup = BeautifulSoup(response_data, "html.parser")
224
+ mailform = {
225
+ t["name"]: t["value"]
226
+ for t in response_soup.find("form", id="emailPasswordForm").find_all("input", type="hidden")
227
+ }
228
+ mailform["email"] = self._session_auth_username
229
+ pe_url = auth_issuer + response_soup.find("form", id="emailPasswordForm").get("action")
230
+ except Exception as e:
231
+ _LOGGER.error("Failed to extract user login form.")
232
+ raise e
295
233
 
296
234
  # POST email
297
235
  # https://identity.vwgroup.io/signin-service/v1/{CLIENT_ID}/login/identifier
298
236
  self._session_auth_headers["Referer"] = authorization_endpoint
299
237
  self._session_auth_headers["Origin"] = auth_issuer
300
- response_text = await self.post_form(
301
- self._session, pe_url, self._session_auth_headers, mailform
302
- )
303
-
304
- # Extract password form data
305
- response_soup = BeautifulSoup(response_text, "html.parser")
306
- pw_form, post_action, client_id = self.extract_password_form_data(
307
- response_soup
308
- )
309
-
310
- # Add password to form data
311
- pw_form["password"] = self._session_auth_password
312
- pw_url = f"{auth_issuer}/signin-service/v1/{client_id}/{post_action}"
238
+ req = await self._session.post(url=pe_url, headers=self._session_auth_headers, data=mailform)
239
+ if req.status != 200:
240
+ raise Exception("POST password request failed")
241
+ try:
242
+ response_data = await req.text()
243
+ response_soup = BeautifulSoup(response_data, "html.parser")
244
+ pw_form: dict[str, str] = {}
245
+ post_action = None
246
+ client_id = None
247
+ for d in response_soup.find_all("script"):
248
+ if "src" in d.attrs:
249
+ continue
250
+ if "window._IDK" in d.string:
251
+ if re.match('"errorCode":"', d.string) is not None:
252
+ raise Exception("Error code in response")
253
+ pw_form["relayState"] = re.search('"relayState":"([a-f0-9]*)"', d.string)[1]
254
+ pw_form["hmac"] = re.search('"hmac":"([a-f0-9]*)"', d.string)[1]
255
+ pw_form["email"] = re.search('"email":"([^"]*)"', d.string)[1]
256
+ pw_form["_csrf"] = re.search("csrf_token:\\s*'([^\"']*)'", d.string)[1]
257
+ post_action = re.search('"postAction":\\s*"([^"\']*)"', d.string)[1]
258
+ client_id = re.search('"clientId":\\s*"([^"\']*)"', d.string)[1]
259
+ break
260
+ if pw_form["hmac"] is None or post_action is None:
261
+ raise Exception("Failed to find authentication data in response")
262
+ pw_form["password"] = self._session_auth_password
263
+ pw_url = "{host}/signin-service/v1/{clientId}/{postAction}".format(
264
+ host=auth_issuer, clientId=client_id, postAction=post_action
265
+ )
266
+ except Exception as e:
267
+ _LOGGER.error("Failed to extract password login form.")
268
+ raise e
313
269
 
314
270
  # POST password
271
+ # https://identity.vwgroup.io/signin-service/v1/{CLIENT_ID}/login/authenticate
315
272
  self._session_auth_headers["Referer"] = pe_url
316
- redirect_location = await self.handle_login_with_password(
317
- self._session, pw_url, self._session_auth_headers, pw_form
318
- )
319
-
320
- # Handle redirects and extract tokens
321
- redirect_response = await self.follow_redirects(
322
- self._session, pw_url, redirect_location
273
+ self._session_auth_headers["Origin"] = auth_issuer
274
+ _LOGGER.debug("Authenticating with email and password.")
275
+ if self._session_fulldebug:
276
+ _LOGGER.debug(f'Using login action url: "{pw_url}"')
277
+ req = await self._session.post(
278
+ url=pw_url, headers=self._session_auth_headers, data=pw_form, allow_redirects=False
323
279
  )
324
- jwt_auth_code = parse_qs(urlparse(redirect_response).query)["code"][0]
325
-
326
- # Exchange authorization code for tokens
280
+ _LOGGER.debug("Parsing login response.")
281
+ # Follow all redirects until we get redirected back to "our app"
282
+ try:
283
+ max_depth = 10
284
+ ref = urljoin(pw_url, req.headers["Location"])
285
+ while not ref.startswith(APP_URI):
286
+ if self._session_fulldebug:
287
+ _LOGGER.debug(f'Following redirect to "{ref}"')
288
+ response = await self._session.get(
289
+ url=ref, headers=self._session_auth_headers, allow_redirects=False
290
+ )
291
+ if not response.headers.get("Location", False):
292
+ _LOGGER.info("Login failed, does this account have any vehicle with connect services enabled?")
293
+ raise Exception("User appears unauthorized")
294
+ ref = urljoin(ref, response.headers["Location"])
295
+ # Set a max limit on requests to prevent forever loop
296
+ max_depth -= 1
297
+ if max_depth == 0:
298
+ _LOGGER.warning("Should have gotten a token by now.")
299
+ raise Exception("Too many redirects")
300
+ except Exception as e:
301
+ # If we get excepted it should be because we can't redirect to the APP_URI URL
302
+ if "error" in ref:
303
+ error_msg = parse_qs(urlparse(ref).query).get("error", "")[0]
304
+ if error_msg == "login.error.throttled":
305
+ timeout = parse_qs(urlparse(ref).query).get("enableNextButtonAfterSeconds", "")[0]
306
+ _LOGGER.warning(f"Login failed, login is disabled for another {timeout} seconds")
307
+ elif error_msg == "login.errors.password_invalid":
308
+ _LOGGER.warning("Login failed, invalid password")
309
+ else:
310
+ _LOGGER.warning(f"Login failed: {error_msg}")
311
+ raise AuthenticationException(error_msg)
312
+ if "code" in ref:
313
+ _LOGGER.debug("Got code: %s" % ref)
314
+ else:
315
+ _LOGGER.debug("Exception occurred while logging in.")
316
+ raise e
317
+ _LOGGER.debug("Login successful, received authorization code.")
318
+
319
+ # Extract code and tokens
320
+ parsed_qs = parse_qs(urlparse(ref).query)
321
+ jwt_auth_code = parsed_qs["code"][0]
322
+ # jwt_id_token = parsed_qs["id_token"][0]
323
+ # Exchange Auth code and id_token for new tokens with refresh_token (so we can easier fetch new ones later)
327
324
  token_body = {
328
325
  "client_id": CLIENT[client].get("CLIENT_ID"),
329
326
  "grant_type": "authorization_code",
330
327
  "code": jwt_auth_code,
331
328
  "redirect_uri": APP_URI,
329
+ # "brand": BRAND,
332
330
  }
333
-
334
- # Token endpoint
335
- token_response = await self.post_form(
336
- self._session, token_endpoint, self._session_auth_headers, token_body
331
+ _LOGGER.debug("Trying to fetch user identity tokens.")
332
+ token_url = token_endpoint
333
+ req = await self._session.post(
334
+ url=token_url, headers=self._session_auth_headers, data=token_body, allow_redirects=False
337
335
  )
338
-
339
- # Store session tokens
340
- self._session_tokens[client] = json_loads(token_response)
341
-
342
- # Verify tokens
343
- if not await self.verify_tokens(
344
- self._session_tokens[client].get("id_token", ""), "identity"
345
- ):
336
+ if req.status != 200:
337
+ raise Exception(f"Token exchange failed. Received message: {await req.content.read()}")
338
+ self._session_tokens[client] = await req.json()
339
+ if "error" in self._session_tokens[client]:
340
+ error_msg = self._session_tokens[client].get("error", "")
341
+ if "error_description" in self._session_tokens[client]:
342
+ error_description = self._session_tokens[client].get("error_description", "")
343
+ raise Exception(f"{error_msg} - {error_description}")
344
+ else:
345
+ raise Exception(error_msg)
346
+ if self._session_fulldebug:
347
+ for token in self._session_tokens.get(client, {}):
348
+ _LOGGER.debug(f"Got token {token}")
349
+ if not await self.verify_tokens(self._session_tokens[client].get("id_token", ""), "identity"):
346
350
  _LOGGER.warning("User identity token could not be verified!")
347
351
  else:
348
- _LOGGER.debug("User identity token verified successfully")
349
-
350
- # Mark session as logged in
351
- self._session_logged_in = True
352
-
353
- except Exception as error: # pylint: disable=broad-exception-caught
354
- _LOGGER.error("Login failed: %s", error)
352
+ _LOGGER.debug("User identity token verified OK.")
353
+ self._session_logged_in = True
354
+ except Exception as error:
355
+ _LOGGER.error(f"Login failed for {BRAND} account, {error}")
356
+ _LOGGER.exception(error)
355
357
  self._session_logged_in = False
356
358
  return False
357
- self._session_headers["Authorization"] = (
358
- "Bearer " + self._session_tokens[client]["access_token"]
359
- )
359
+ self._session_headers["Authorization"] = "Bearer " + self._session_tokens[client]["access_token"]
360
360
  return True
361
361
 
362
362
  async def _handle_action_result(self, response_raw):
363
363
  response = await response_raw.json(loads=json_loads)
364
364
  if not response:
365
- raise Exception("Invalid or no response") # pylint: disable=broad-exception-raised
366
- if response == 429:
367
- return {"id": None, "state": "Throttled"}
368
- request_id = response.get("data", {}).get("requestID", 0)
369
- _LOGGER.debug("Request returned with request id: %s", request_id)
370
- return {"id": str(request_id)}
365
+ raise Exception("Invalid or no response")
366
+ elif response == 429:
367
+ return dict({"id": None, "state": "Throttled"})
368
+ else:
369
+ request_id = response.get("data", {}).get("requestID", 0)
370
+ _LOGGER.debug(f"Request returned with request id: {request_id}")
371
+ return dict({"id": str(request_id)})
371
372
 
372
373
  async def terminate(self):
373
374
  """Log out from connect services."""
@@ -376,25 +377,28 @@ class Connection:
376
377
 
377
378
  async def logout(self):
378
379
  """Logout, revoke tokens."""
380
+ # TODO: not tested yet
379
381
  self._session_headers.pop("Authorization", None)
380
382
 
381
383
  if self._session_logged_in:
382
384
  if self._session_headers.get("identity", {}).get("identity_token"):
383
- _LOGGER.info("Revoking Identity Access Token")
384
-
385
+ _LOGGER.info("Revoking Identity Access Token...")
386
+ # params = {
387
+ # "token": self._session_tokens['identity']['access_token'],
388
+ # "brand": BRAND
389
+ # }
390
+ # revoke_at = await self.post('https://emea.bff.cariad.digital/login/v1/idk/revoke', data = params)
385
391
  if self._session_headers.get("identity", {}).get("refresh_token"):
386
- _LOGGER.info("Revoking Identity Refresh Token")
392
+ _LOGGER.info("Revoking Identity Refresh Token...")
387
393
  params = {"token": self._session_tokens["identity"]["refresh_token"]}
388
- await self.post(
389
- "https://emea.bff.cariad.digital/login/v1/idk/revoke", data=params
390
- )
394
+ await self.post("https://emea.bff.cariad.digital/login/v1/idk/revoke", data=params)
391
395
 
392
396
  # HTTP methods to API
393
397
  async def _request(self, method, url, return_raw=False, **kwargs):
394
398
  """Perform a query to the VW-Group API."""
395
- _LOGGER.debug('HTTP %s "%s"', method, url)
399
+ _LOGGER.debug(f'HTTP {method} "{url}"')
396
400
  if kwargs.get("json", None):
397
- _LOGGER.debug("Request payload: %s", kwargs.get("json", None))
401
+ _LOGGER.debug(f'Request payload: {kwargs.get("json", None)}')
398
402
  try:
399
403
  async with self._session.request(
400
404
  method,
@@ -426,36 +430,21 @@ class Connection:
426
430
  res = await response.json(loads=json_loads)
427
431
  else:
428
432
  res = {}
429
- _LOGGER.debug(
430
- "Not success status code [%s] response: %s",
431
- response.status,
432
- response.text,
433
- )
434
- except Exception: # pylint: disable=broad-exception-caught
433
+ _LOGGER.debug(f"Not success status code [{response.status}] response: {response.text}")
434
+ except Exception:
435
435
  res = {}
436
- _LOGGER.debug(
437
- "Something went wrong [%s] response: %s",
438
- response.status,
439
- response.text,
440
- )
436
+ _LOGGER.debug(f"Something went wrong [{response.status}] response: {response.text}")
441
437
  if return_raw:
442
438
  return response
443
- return res
439
+ else:
440
+ return res
444
441
 
445
442
  if self._session_fulldebug:
446
443
  _LOGGER.debug(
447
- 'Request for "%s" returned with status code [%s], headers: %s, response: %s',
448
- url,
449
- response.status,
450
- response.headers,
451
- res,
444
+ f'Request for "{url}" returned with status code [{response.status}], headers: {response.headers}, response: {res}'
452
445
  )
453
446
  else:
454
- _LOGGER.debug(
455
- 'Request for "%s" returned with status code [%s]',
456
- url,
457
- response.status,
458
- )
447
+ _LOGGER.debug(f'Request for "{url}" returned with status code [{response.status}]')
459
448
 
460
449
  if return_raw:
461
450
  res = response
@@ -472,7 +461,8 @@ class Connection:
472
461
  async def get(self, url, vin="", tries=0):
473
462
  """Perform a get query."""
474
463
  try:
475
- return await self._request(METH_GET, url)
464
+ response = await self._request(METH_GET, url)
465
+ return response
476
466
  except client_exceptions.ClientResponseError as error:
477
467
  if error.status == 400:
478
468
  _LOGGER.error(
@@ -480,92 +470,78 @@ class Connection:
480
470
  " correctly for this vehicle"
481
471
  )
482
472
  elif error.status == 401:
483
- _LOGGER.warning(
484
- 'Received "unauthorized" error while fetching data: %s', error
485
- )
473
+ _LOGGER.warning(f'Received "unauthorized" error while fetching data: {error}')
486
474
  self._session_logged_in = False
487
475
  elif error.status == 429 and tries < MAX_RETRIES_ON_RATE_LIMIT:
488
476
  delay = randint(1, 3 + tries * 2)
489
- _LOGGER.debug(
490
- "Server side throttled. Waiting %s, try %s", delay, tries + 1
491
- )
477
+ _LOGGER.debug(f"Server side throttled. Waiting {delay}, try {tries + 1}")
492
478
  await asyncio.sleep(delay)
493
479
  return await self.get(url, vin, tries + 1)
494
480
  elif error.status == 500:
495
- _LOGGER.info(
496
- "Got HTTP 500 from server, service might be temporarily unavailable"
497
- )
481
+ _LOGGER.info("Got HTTP 500 from server, service might be temporarily unavailable")
498
482
  elif error.status == 502:
499
- _LOGGER.info(
500
- "Got HTTP 502 from server, this request might not be supported for this vehicle"
501
- )
483
+ _LOGGER.info("Got HTTP 502 from server, this request might not be supported for this vehicle")
502
484
  else:
503
- _LOGGER.error("Got unhandled error from server: %s", error.status)
485
+ _LOGGER.error(f"Got unhandled error from server: {error.status}")
504
486
  return {"status_code": error.status}
505
487
 
506
488
  async def post(self, url, vin="", tries=0, return_raw=False, **data):
507
489
  """Perform a post query."""
508
490
  try:
509
491
  if data:
510
- return await self._request(
511
- METH_POST, url, return_raw=return_raw, **data
512
- )
513
- return await self._request(METH_POST, url, return_raw=return_raw)
492
+ return await self._request(METH_POST, url, return_raw=return_raw, **data)
493
+ else:
494
+ return await self._request(METH_POST, url, return_raw=return_raw)
514
495
  except client_exceptions.ClientResponseError as error:
515
496
  if error.status == 429 and tries < MAX_RETRIES_ON_RATE_LIMIT:
516
497
  delay = randint(1, 3 + tries * 2)
517
- _LOGGER.debug(
518
- "Server side throttled. Waiting %s, try %s", delay, tries + 1
519
- )
498
+ _LOGGER.debug(f"Server side throttled. Waiting {delay}, try {tries + 1}")
520
499
  await asyncio.sleep(delay)
521
- return await self.post(
522
- url, vin, tries + 1, return_raw=return_raw, **data
523
- )
524
- raise
500
+ return await self.post(url, vin, tries + 1, return_raw=return_raw, **data)
501
+ else:
502
+ raise
525
503
 
526
504
  async def put(self, url, vin="", tries=0, return_raw=False, **data):
527
505
  """Perform a put query."""
528
506
  try:
529
507
  if data:
530
508
  return await self._request(METH_PUT, url, return_raw=return_raw, **data)
531
- return await self._request(METH_PUT, url, return_raw=return_raw)
509
+ else:
510
+ return await self._request(METH_PUT, url, return_raw=return_raw)
532
511
  except client_exceptions.ClientResponseError as error:
533
512
  if error.status == 429 and tries < MAX_RETRIES_ON_RATE_LIMIT:
534
513
  delay = randint(1, 3 + tries * 2)
535
- _LOGGER.debug(
536
- "Server side throttled. Waiting %s, try %s", delay, tries + 1
537
- )
514
+ _LOGGER.debug(f"Server side throttled. Waiting {delay}, try {tries + 1}")
538
515
  await asyncio.sleep(delay)
539
- return await self.put(
540
- url, vin, tries + 1, return_raw=return_raw, **data
541
- )
542
- raise
516
+ return await self.put(url, vin, tries + 1, return_raw=return_raw, **data)
517
+ else:
518
+ raise
543
519
 
544
520
  # Update data for all Vehicles
545
521
  async def update(self):
546
522
  """Update status."""
547
523
  if not self.logged_in:
548
524
  if not await self._login():
549
- _LOGGER.warning("Login for %s account failed!", BRAND)
525
+ _LOGGER.warning(f"Login for {BRAND} account failed!")
550
526
  return False
551
527
  try:
552
528
  if not await self.validate_tokens:
553
- _LOGGER.info(
554
- "Session expired. Initiating new login for %s account", BRAND
555
- )
529
+ _LOGGER.info(f"Session expired. Initiating new login for {BRAND} account.")
556
530
  if not await self.doLogin():
557
- _LOGGER.warning("Login for %s account failed!", BRAND)
558
- raise Exception(f"Login for {BRAND} account failed") # pylint: disable=broad-exception-raised
559
- else:
560
- _LOGGER.debug("Going to call vehicle updates")
561
- # Get all Vehicle objects and update in parallell
562
- updatelist = [vehicle.update() for vehicle in self.vehicles]
563
- # Wait for all data updates to complete
564
- await asyncio.gather(*updatelist)
565
-
566
- return True
567
- except (OSError, LookupError, Exception) as error: # pylint: disable=broad-exception-caught
568
- _LOGGER.warning("Could not update information: %s", error)
531
+ _LOGGER.warning(f"Login for {BRAND} account failed!")
532
+ raise Exception(f"Login for {BRAND} account failed")
533
+
534
+ _LOGGER.debug("Going to call vehicle updates")
535
+ # Get all Vehicle objects and update in parallell
536
+ updatelist = []
537
+ for vehicle in self.vehicles:
538
+ updatelist.append(vehicle.update())
539
+ # Wait for all data updates to complete
540
+ await asyncio.gather(*updatelist)
541
+
542
+ return True
543
+ except (OSError, LookupError, Exception) as error:
544
+ _LOGGER.warning(f"Could not update information: {error}")
569
545
  return False
570
546
 
571
547
  async def getPendingRequests(self, vin):
@@ -573,18 +549,15 @@ class Connection:
573
549
  if not await self.validate_tokens:
574
550
  return False
575
551
  try:
576
- response = await self.get(
577
- f"{BASE_API}/vehicle/v1/vehicles/{vin}/pendingrequests"
578
- )
552
+ response = await self.get(f"{BASE_API}/vehicle/v1/vehicles/{vin}/pendingrequests")
579
553
 
580
554
  if response:
581
- response["refreshTimestamp"] = datetime.now(UTC)
582
- return response
555
+ response.update({"refreshTimestamp": datetime.now(timezone.utc)})
583
556
 
584
- except Exception as error: # pylint: disable=broad-exception-caught
585
- _LOGGER.warning(
586
- "Could not fetch information for pending requests, error: %s", error
587
- )
557
+ return response
558
+
559
+ except Exception as error:
560
+ _LOGGER.warning(f"Could not fetch information for pending requests, error: {error}")
588
561
  return False
589
562
 
590
563
  async def getOperationList(self, vin):
@@ -592,22 +565,17 @@ class Connection:
592
565
  if not await self.validate_tokens:
593
566
  return False
594
567
  try:
595
- response = await self.get(
596
- f"{BASE_API}/vehicle/v1/vehicles/{vin}/capabilities", ""
597
- )
568
+ response = await self.get(f"{BASE_API}/vehicle/v1/vehicles/{vin}/capabilities", "")
598
569
  if response.get("capabilities", False):
599
570
  data = response
600
571
  elif response.get("status_code", {}):
601
- _LOGGER.warning(
602
- "Could not fetch operation list, HTTP status code: %s",
603
- response.get("status_code"),
604
- )
572
+ _LOGGER.warning(f'Could not fetch operation list, HTTP status code: {response.get("status_code")}')
605
573
  data = response
606
574
  else:
607
- _LOGGER.info("Could not fetch operation list: %s", response)
575
+ _LOGGER.info(f"Could not fetch operation list: {response}")
608
576
  data = {"error": "unknown"}
609
- except Exception as error: # pylint: disable=broad-exception-caught
610
- _LOGGER.warning("Could not fetch operation list, error: %s", error)
577
+ except Exception as error:
578
+ _LOGGER.warning(f"Could not fetch operation list, error: {error}")
611
579
  data = {"error": "unknown"}
612
580
  return data
613
581
 
@@ -617,23 +585,22 @@ class Connection:
617
585
  return False
618
586
  try:
619
587
  response = await self.get(
620
- f"{BASE_API}/vehicle/v1/vehicles/{vin}/selectivestatus?jobs={','.join(services)}",
621
- "",
588
+ f"{BASE_API}/vehicle/v1/vehicles/{vin}/selectivestatus?jobs={','.join(services)}", ""
622
589
  )
623
590
 
624
591
  for service in services:
625
592
  if not response.get(service):
626
593
  _LOGGER.debug(
627
- "Did not receive return data for requested service %s. (This is expected for several service/car combinations)",
628
- service,
594
+ f"Did not receive return data for requested service {service}. (This is expected for several service/car combinations)"
629
595
  )
630
596
 
631
597
  if response:
632
- response.update({"refreshTimestamp": datetime.now(UTC)})
633
- return response
598
+ response.update({"refreshTimestamp": datetime.now(timezone.utc)})
599
+
600
+ return response
634
601
 
635
- except Exception as error: # pylint: disable=broad-exception-caught
636
- _LOGGER.warning("Could not fetch selectivestatus, error: %s", error)
602
+ except Exception as error:
603
+ _LOGGER.warning(f"Could not fetch selectivestatus, error: {error}")
637
604
  return False
638
605
 
639
606
  async def getVehicleData(self, vin):
@@ -645,12 +612,13 @@ class Connection:
645
612
 
646
613
  for vehicle in response.get("data"):
647
614
  if vehicle.get("vin") == vin:
648
- return {"vehicle": vehicle}
615
+ data = {"vehicle": vehicle}
616
+ return data
649
617
 
650
- _LOGGER.warning("Could not fetch vehicle data for vin %s", vin)
618
+ _LOGGER.warning(f"Could not fetch vehicle data for vin {vin}")
651
619
 
652
- except Exception as error: # pylint: disable=broad-exception-caught
653
- _LOGGER.warning("Could not fetch vehicle data, error: %s", error)
620
+ except Exception as error:
621
+ _LOGGER.warning(f"Could not fetch vehicle data, error: {error}")
654
622
  return False
655
623
 
656
624
  async def getParkingPosition(self, vin):
@@ -658,29 +626,21 @@ class Connection:
658
626
  if not await self.validate_tokens:
659
627
  return False
660
628
  try:
661
- response = await self.get(
662
- f"{BASE_API}/vehicle/v1/vehicles/{vin}/parkingposition", ""
663
- )
629
+ response = await self.get(f"{BASE_API}/vehicle/v1/vehicles/{vin}/parkingposition", "")
664
630
 
665
631
  if "data" in response:
666
632
  return {"isMoving": False, "parkingposition": response["data"]}
667
- if response.get("status_code", {}):
633
+ elif response.get("status_code", {}):
668
634
  if response.get("status_code", 0) == 204:
669
- _LOGGER.debug(
670
- "Seems car is moving, HTTP 204 received from parkingposition"
671
- )
672
- return {"isMoving": True, "parkingposition": {}}
673
-
674
- _LOGGER.warning(
675
- "Could not fetch parkingposition, HTTP status code: %s",
676
- response.get("status_code"),
677
- )
635
+ _LOGGER.debug("Seems car is moving, HTTP 204 received from parkingposition")
636
+ data = {"isMoving": True, "parkingposition": {}}
637
+ return data
638
+ else:
639
+ _LOGGER.warning(f'Could not fetch parkingposition, HTTP status code: {response.get("status_code")}')
678
640
  else:
679
- _LOGGER.info(
680
- "Unhandled error while trying to fetch parkingposition data"
681
- )
682
- except Exception as error: # pylint: disable=broad-exception-caught
683
- _LOGGER.warning("Could not fetch parkingposition, error: %s", error)
641
+ _LOGGER.info("Unhandled error while trying to fetch parkingposition data")
642
+ except Exception as error:
643
+ _LOGGER.warning(f"Could not fetch parkingposition, error: {error}")
684
644
  return False
685
645
 
686
646
  async def getTripLast(self, vin):
@@ -688,18 +648,14 @@ class Connection:
688
648
  if not await self.validate_tokens:
689
649
  return False
690
650
  try:
691
- response = await self.get(
692
- f"{BASE_API}/vehicle/v1/trips/{vin}/shortterm/last", ""
693
- )
651
+ response = await self.get(f"{BASE_API}/vehicle/v1/trips/{vin}/shortterm/last", "")
694
652
  if "data" in response:
695
653
  return {"trip_last": response["data"]}
654
+ else:
655
+ _LOGGER.warning(f"Could not fetch last trip data, server response: {response}")
696
656
 
697
- _LOGGER.warning(
698
- "Could not fetch last trip data, server response: %s", response
699
- )
700
-
701
- except Exception as error: # pylint: disable=broad-exception-caught
702
- _LOGGER.warning("Could not fetch last trip data, error: %s", error)
657
+ except Exception as error:
658
+ _LOGGER.warning(f"Could not fetch last trip data, error: {error}")
703
659
  return False
704
660
 
705
661
  async def wakeUpVehicle(self, vin):
@@ -707,30 +663,27 @@ class Connection:
707
663
  if not await self.validate_tokens:
708
664
  return False
709
665
  try:
710
- return await self.post(
711
- f"{BASE_API}/vehicle/v1/vehicles/{vin}/vehiclewakeuptrigger",
712
- json={},
713
- return_raw=True,
666
+ response = await self.post(
667
+ f"{BASE_API}/vehicle/v1/vehicles/{vin}/vehiclewakeuptrigger", json={}, return_raw=True
714
668
  )
669
+ return response
715
670
 
716
- except Exception as error: # pylint: disable=broad-exception-caught
717
- _LOGGER.warning("Could not refresh the data, error: %s", error)
671
+ except Exception as error:
672
+ _LOGGER.warning(f"Could not refresh the data, error: {error}")
718
673
  return False
719
674
 
720
675
  async def get_request_status(self, vin, requestId, actionId=""):
721
676
  """Return status of a request ID for a given section ID."""
722
677
  if self.logged_in is False:
723
678
  if not await self.doLogin():
724
- _LOGGER.warning("Login for %s account failed!", BRAND)
725
- raise Exception(f"Login for {BRAND} account failed") # pylint: disable=broad-exception-raised
679
+ _LOGGER.warning(f"Login for {BRAND} account failed!")
680
+ raise Exception(f"Login for {BRAND} account failed")
726
681
  try:
727
682
  if not await self.validate_tokens:
728
- _LOGGER.info(
729
- "Session expired. Initiating new login for %s account", BRAND
730
- )
683
+ _LOGGER.info(f"Session expired. Initiating new login for {BRAND} account.")
731
684
  if not await self.doLogin():
732
- _LOGGER.warning("Login for %s account failed!", BRAND)
733
- raise Exception(f"Login for {BRAND} account failed") # pylint: disable=broad-exception-raised
685
+ _LOGGER.warning(f"Login for {BRAND} account failed!")
686
+ raise Exception(f"Login for {BRAND} account failed")
734
687
 
735
688
  response = await self.getPendingRequests(vin)
736
689
 
@@ -741,37 +694,35 @@ class Connection:
741
694
  result = request.get("status")
742
695
 
743
696
  # Translate status messages to meaningful info
744
- if result in ("in_progress", "queued", "fetched"):
697
+ if result == "in_progress" or result == "queued" or result == "fetched":
745
698
  status = "In Progress"
746
- elif result in ("request_fail", "failed"):
699
+ elif result == "request_fail" or result == "failed":
747
700
  status = "Failed"
748
701
  elif result == "unfetched":
749
702
  status = "No response"
750
- elif result in ("request_successful", "successful"):
703
+ elif result == "request_successful" or result == "successful":
751
704
  status = "Success"
752
705
  elif result == "fail_ignition_on":
753
706
  status = "Failed because ignition is on"
754
707
  else:
755
708
  status = result
756
- except Exception as error:
757
- _LOGGER.warning("Failure during get request status: %s", error)
758
- raise Exception(f"Failure during get request status: {error}") from error # pylint: disable=broad-exception-raised
759
- else:
760
709
  return status
710
+ except Exception as error:
711
+ _LOGGER.warning(f"Failure during get request status: {error}")
712
+ raise Exception(f"Failure during get request status: {error}")
761
713
 
762
714
  async def check_spin_state(self):
763
715
  """Determine SPIN state to prevent lockout due to wrong SPIN."""
764
716
  result = await self.get(f"{BASE_API}/vehicle/v1/spin/state")
765
717
  remainingTries = result.get("remainingTries", None)
766
718
  if remainingTries is None:
767
- raise Exception("Couldn't determine S-PIN state.") # pylint: disable=broad-exception-raised
719
+ raise Exception("Couldn't determine S-PIN state.")
768
720
 
769
721
  if remainingTries < 3:
770
- # pylint: disable=broad-exception-raised
771
722
  raise Exception(
772
723
  "Remaining tries for S-PIN is < 3. Bailing out for security reasons. "
773
- "To resume operation, please make sure the correct S-PIN has been set in the integration "
774
- "and then use the correct S-PIN once via the Volkswagen app."
724
+ + "To resume operation, please make sure the correct S-PIN has been set in the integration "
725
+ + "and then use the correct S-PIN once via the Volkswagen app."
775
726
  )
776
727
 
777
728
  return True
@@ -781,148 +732,124 @@ class Connection:
781
732
  action = "start" if action else "stop"
782
733
  try:
783
734
  response_raw = await self.post(
784
- f"{BASE_API}/vehicle/v1/vehicles/{vin}/climatisation/{action}",
785
- json=data,
786
- return_raw=True,
735
+ f"{BASE_API}/vehicle/v1/vehicles/{vin}/climatisation/{action}", json=data, return_raw=True
787
736
  )
788
737
  return await self._handle_action_result(response_raw)
789
738
  except Exception as e:
790
- raise Exception("Unknown error during setClimater") from e # pylint: disable=broad-exception-raised
739
+ raise Exception("Unknown error during setClimater") from e
791
740
 
792
741
  async def setClimaterSettings(self, vin, data):
793
742
  """Execute climatisation settings."""
794
743
  try:
795
744
  response_raw = await self.put(
796
- f"{BASE_API}/vehicle/v1/vehicles/{vin}/climatisation/settings",
797
- json=data,
798
- return_raw=True,
745
+ f"{BASE_API}/vehicle/v1/vehicles/{vin}/climatisation/settings", json=data, return_raw=True
799
746
  )
800
747
  return await self._handle_action_result(response_raw)
801
748
  except Exception as e:
802
- raise Exception("Unknown error during setClimaterSettings") from e # pylint: disable=broad-exception-raised
749
+ raise Exception("Unknown error during setClimaterSettings") from e
803
750
 
804
751
  async def setAuxiliary(self, vin, data, action):
805
752
  """Execute auxiliary climatisation actions."""
806
753
  action = "start" if action else "stop"
807
754
  try:
808
755
  response_raw = await self.post(
809
- f"{BASE_API}/vehicle/v1/vehicles/{vin}/auxiliaryheating/{action}",
810
- json=data,
811
- return_raw=True,
756
+ f"{BASE_API}/vehicle/v1/vehicles/{vin}/auxiliaryheating/{action}", json=data, return_raw=True
812
757
  )
813
758
  return await self._handle_action_result(response_raw)
814
759
  except Exception as e:
815
- raise Exception("Unknown error during setAuxiliary") from e # pylint: disable=broad-exception-raised
760
+ raise Exception("Unknown error during setAuxiliary") from e
816
761
 
817
762
  async def setWindowHeater(self, vin, action):
818
763
  """Execute window heating actions."""
819
764
  action = "start" if action else "stop"
820
765
  try:
821
766
  response_raw = await self.post(
822
- f"{BASE_API}/vehicle/v1/vehicles/{vin}/windowheating/{action}",
823
- json={},
824
- return_raw=True,
767
+ f"{BASE_API}/vehicle/v1/vehicles/{vin}/windowheating/{action}", json={}, return_raw=True
825
768
  )
826
769
  return await self._handle_action_result(response_raw)
827
770
  except Exception as e:
828
- raise Exception("Unknown error during setWindowHeater") from e # pylint: disable=broad-exception-raised
771
+ raise Exception("Unknown error during setWindowHeater") from e
829
772
 
830
773
  async def setCharging(self, vin, action):
831
774
  """Execute charging actions."""
832
775
  action = "start" if action else "stop"
833
776
  try:
834
777
  response_raw = await self.post(
835
- f"{BASE_API}/vehicle/v1/vehicles/{vin}/charging/{action}",
836
- json={},
837
- return_raw=True,
778
+ f"{BASE_API}/vehicle/v1/vehicles/{vin}/charging/{action}", json={}, return_raw=True
838
779
  )
839
780
  return await self._handle_action_result(response_raw)
840
781
  except Exception as e:
841
- raise Exception("Unknown error during setCharging") from e # pylint: disable=broad-exception-raised
782
+ raise Exception("Unknown error during setCharging") from e
842
783
 
843
784
  async def setChargingSettings(self, vin, data):
844
785
  """Execute charging actions."""
845
786
  try:
846
787
  response_raw = await self.put(
847
- f"{BASE_API}/vehicle/v1/vehicles/{vin}/charging/settings",
848
- json=data,
849
- return_raw=True,
788
+ f"{BASE_API}/vehicle/v1/vehicles/{vin}/charging/settings", json=data, return_raw=True
850
789
  )
851
790
  return await self._handle_action_result(response_raw)
852
791
  except Exception as e:
853
- raise Exception("Unknown error during setChargingSettings") from e # pylint: disable=broad-exception-raised
792
+ raise Exception("Unknown error during setChargingSettings") from e
854
793
 
855
794
  async def setChargingCareModeSettings(self, vin, data):
856
795
  """Execute battery care mode actions."""
857
796
  try:
858
797
  response_raw = await self.put(
859
- f"{BASE_API}/vehicle/v1/vehicles/{vin}/charging/care/settings",
860
- json=data,
861
- return_raw=True,
798
+ f"{BASE_API}/vehicle/v1/vehicles/{vin}/charging/care/settings", json=data, return_raw=True
862
799
  )
863
800
  return await self._handle_action_result(response_raw)
864
801
  except Exception as e:
865
- raise Exception("Unknown error during setChargingCareModeSettings") from e # pylint: disable=broad-exception-raised
802
+ raise Exception("Unknown error during setChargingCareModeSettings") from e
866
803
 
867
804
  async def setReadinessBatterySupport(self, vin, data):
868
805
  """Execute readiness battery support actions."""
869
806
  try:
870
807
  response_raw = await self.put(
871
- f"{BASE_API}/vehicle/v1/vehicles/{vin}/readiness/batterysupport",
872
- json=data,
873
- return_raw=True,
808
+ f"{BASE_API}/vehicle/v1/vehicles/{vin}/readiness/batterysupport", json=data, return_raw=True
874
809
  )
875
810
  return await self._handle_action_result(response_raw)
876
811
  except Exception as e:
877
- raise Exception("Unknown error during setReadinessBatterySupport") from e # pylint: disable=broad-exception-raised
812
+ raise Exception("Unknown error during setReadinessBatterySupport") from e
878
813
 
879
814
  async def setDepartureProfiles(self, vin, data):
880
815
  """Execute departure timers actions."""
881
816
  try:
882
817
  response_raw = await self.put(
883
- f"{BASE_API}/vehicle/v1/vehicles/{vin}/departure/profiles",
884
- json=data,
885
- return_raw=True,
818
+ f"{BASE_API}/vehicle/v1/vehicles/{vin}/departure/profiles", json=data, return_raw=True
886
819
  )
887
820
  return await self._handle_action_result(response_raw)
888
821
  except Exception as e:
889
- raise Exception("Unknown error during setDepartureProfiles") from e # pylint: disable=broad-exception-raised
822
+ raise Exception("Unknown error during setDepartureProfiles") from e
890
823
 
891
824
  async def setClimatisationTimers(self, vin, data):
892
825
  """Execute climatisation timers actions."""
893
826
  try:
894
827
  response_raw = await self.put(
895
- f"{BASE_API}/vehicle/v1/vehicles/{vin}/climatisation/timers",
896
- json=data,
897
- return_raw=True,
828
+ f"{BASE_API}/vehicle/v1/vehicles/{vin}/climatisation/timers", json=data, return_raw=True
898
829
  )
899
830
  return await self._handle_action_result(response_raw)
900
831
  except Exception as e:
901
- raise Exception("Unknown error during setClimatisationTimers") from e # pylint: disable=broad-exception-raised
832
+ raise Exception("Unknown error during setClimatisationTimers") from e
902
833
 
903
834
  async def setAuxiliaryHeatingTimers(self, vin, data):
904
835
  """Execute auxiliary heating timers actions."""
905
836
  try:
906
837
  response_raw = await self.put(
907
- f"{BASE_API}/vehicle/v1/vehicles/{vin}/auxiliaryheating/timers",
908
- json=data,
909
- return_raw=True,
838
+ f"{BASE_API}/vehicle/v1/vehicles/{vin}/auxiliaryheating/timers", json=data, return_raw=True
910
839
  )
911
840
  return await self._handle_action_result(response_raw)
912
841
  except Exception as e:
913
- raise Exception("Unknown error during setAuxiliaryHeatingTimers") from e # pylint: disable=broad-exception-raised
842
+ raise Exception("Unknown error during setAuxiliaryHeatingTimers") from e
914
843
 
915
844
  async def setDepartureTimers(self, vin, data):
916
845
  """Execute departure timers actions."""
917
846
  try:
918
847
  response_raw = await self.put(
919
- f"{BASE_API}/vehicle/v1/vehicles/{vin}/departure/timers",
920
- json=data,
921
- return_raw=True,
848
+ f"{BASE_API}/vehicle/v1/vehicles/{vin}/departure/timers", json=data, return_raw=True
922
849
  )
923
850
  return await self._handle_action_result(response_raw)
924
851
  except Exception as e:
925
- raise Exception("Unknown error during setDepartureTimers") from e # pylint: disable=broad-exception-raised
852
+ raise Exception("Unknown error during setDepartureTimers") from e
926
853
 
927
854
  async def setLock(self, vin, lock, spin):
928
855
  """Remote lock and unlock actions."""
@@ -930,13 +857,11 @@ class Connection:
930
857
  action = "lock" if lock else "unlock"
931
858
  try:
932
859
  response_raw = await self.post(
933
- f"{BASE_API}/vehicle/v1/vehicles/{vin}/access/{action}",
934
- json={"spin": spin},
935
- return_raw=True,
860
+ f"{BASE_API}/vehicle/v1/vehicles/{vin}/access/{action}", json={"spin": spin}, return_raw=True
936
861
  )
937
862
  return await self._handle_action_result(response_raw)
938
863
  except Exception as e:
939
- raise Exception("Unknown error during setLock") from e # pylint: disable=broad-exception-raised
864
+ raise Exception("Unknown error during setLock") from e
940
865
 
941
866
  # Token handling #
942
867
  @property
@@ -945,14 +870,10 @@ class Connection:
945
870
  idtoken = self._session_tokens["identity"]["id_token"]
946
871
  atoken = self._session_tokens["identity"]["access_token"]
947
872
  id_exp = jwt.decode(
948
- idtoken,
949
- options={"verify_signature": False, "verify_aud": False},
950
- algorithms=JWT_ALGORITHMS,
873
+ idtoken, options={"verify_signature": False, "verify_aud": False}, algorithms=JWT_ALGORITHMS
951
874
  ).get("exp", None)
952
875
  at_exp = jwt.decode(
953
- atoken,
954
- options={"verify_signature": False, "verify_aud": False},
955
- algorithms=JWT_ALGORITHMS,
876
+ atoken, options={"verify_signature": False, "verify_aud": False}, algorithms=JWT_ALGORITHMS
956
877
  ).get("exp", None)
957
878
  id_dt = datetime.fromtimestamp(int(id_exp))
958
879
  at_dt = datetime.fromtimestamp(int(at_exp))
@@ -961,14 +882,14 @@ class Connection:
961
882
 
962
883
  # Check if tokens have expired, or expires now
963
884
  if now >= id_dt or now >= at_dt:
964
- _LOGGER.debug("Tokens have expired. Try to fetch new tokens")
885
+ _LOGGER.debug("Tokens have expired. Try to fetch new tokens.")
965
886
  if await self.refresh_tokens():
966
887
  _LOGGER.debug("Successfully refreshed tokens")
967
888
  else:
968
889
  return False
969
890
  # Check if tokens expires before next update
970
891
  elif later >= id_dt or later >= at_dt:
971
- _LOGGER.debug("Tokens about to expire. Try to fetch new tokens")
892
+ _LOGGER.debug("Tokens about to expire. Try to fetch new tokens.")
972
893
  if await self.refresh_tokens():
973
894
  _LOGGER.debug("Successfully refreshed tokens")
974
895
  else:
@@ -1000,10 +921,10 @@ class Connection:
1000
921
 
1001
922
  pubkey = pubkeys[token_kid]
1002
923
  jwt.decode(token, key=pubkey, algorithms=JWT_ALGORITHMS, audience=audience)
1003
- except Exception as error: # pylint: disable=broad-exception-caught
1004
- _LOGGER.debug("Failed to verify token, error: %s", error)
924
+ return True
925
+ except Exception as error:
926
+ _LOGGER.debug(f"Failed to verify token, error: {error}")
1005
927
  return False
1006
- return True
1007
928
 
1008
929
  async def refresh_tokens(self):
1009
930
  """Refresh tokens."""
@@ -1021,9 +942,7 @@ class Connection:
1021
942
  "client_id": CLIENT["Legacy"]["CLIENT_ID"],
1022
943
  }
1023
944
  response = await self._session.post(
1024
- url="https://emea.bff.cariad.digital/login/v1/idk/token",
1025
- headers=tHeaders,
1026
- data=body,
945
+ url="https://emea.bff.cariad.digital/login/v1/idk/token", headers=tHeaders, data=body
1027
946
  )
1028
947
  await self.update_service_status("token", response.status)
1029
948
  if response.status == 200:
@@ -1033,19 +952,15 @@ class Connection:
1033
952
  _LOGGER.warning("Token could not be verified!")
1034
953
  for token in tokens:
1035
954
  self._session_tokens["identity"][token] = tokens[token]
1036
- self._session_headers["Authorization"] = (
1037
- "Bearer " + self._session_tokens["identity"]["access_token"]
1038
- )
955
+ self._session_headers["Authorization"] = "Bearer " + self._session_tokens["identity"]["access_token"]
1039
956
  else:
1040
- _LOGGER.warning(
1041
- "Something went wrong when refreshing %s account tokens", BRAND
1042
- )
957
+ _LOGGER.warning(f"Something went wrong when refreshing {BRAND} account tokens.")
1043
958
  return False
1044
- except Exception as error: # pylint: disable=broad-exception-caught
1045
- _LOGGER.warning("Could not refresh tokens: %s", error)
1046
- return False
1047
- else:
959
+
1048
960
  return True
961
+ except Exception as error:
962
+ _LOGGER.warning(f"Could not refresh tokens: {error}")
963
+ return False
1049
964
 
1050
965
  async def update_service_status(self, url, response_code):
1051
966
  """Update service status."""
@@ -1075,7 +990,7 @@ class Connection:
1075
990
  elif "token" in url:
1076
991
  self._service_status["token"] = status
1077
992
  else:
1078
- _LOGGER.debug('Unhandled API URL: "%s"', url)
993
+ _LOGGER.debug(f'Unhandled API URL: "{url}"')
1079
994
 
1080
995
  async def get_service_status(self):
1081
996
  """Return list of service statuses."""
@@ -1090,7 +1005,8 @@ class Connection:
1090
1005
 
1091
1006
  @property
1092
1007
  def logged_in(self):
1093
- """Return cached logged in state.
1008
+ """
1009
+ Return cached logged in state.
1094
1010
 
1095
1011
  Not actually checking anything.
1096
1012
  """
@@ -1098,14 +1014,7 @@ class Connection:
1098
1014
 
1099
1015
  def vehicle(self, vin):
1100
1016
  """Return vehicle object for given vin."""
1101
- return next(
1102
- (
1103
- vehicle
1104
- for vehicle in self.vehicles
1105
- if vehicle.unique_id.lower() == vin.lower()
1106
- ),
1107
- None,
1108
- )
1017
+ return next((vehicle for vehicle in self.vehicles if vehicle.unique_id.lower() == vin.lower()), None)
1109
1018
 
1110
1019
  def hash_spin(self, challenge, spin):
1111
1020
  """Convert SPIN and challenge to hash."""
@@ -1120,8 +1029,8 @@ class Connection:
1120
1029
  try:
1121
1030
  if not await self.validate_tokens:
1122
1031
  return False
1032
+
1033
+ return True
1123
1034
  except OSError as error:
1124
1035
  _LOGGER.warning("Could not validate login: %s", error)
1125
1036
  return False
1126
- else:
1127
- return True