volkswagencarnet 5.0.0b4__py3-none-any.whl → 5.0.0b5__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,46 +1,40 @@
1
1
  #!/usr/bin/env python3
2
- """Communicate with We Connect services."""
2
+ """Communicate with Volkswagen Connect services."""
3
+
3
4
  from __future__ import annotations
4
5
 
6
+ import asyncio
7
+ from datetime import UTC, datetime, timedelta
5
8
  import hashlib
9
+ from json import dumps as to_json
10
+ import logging
11
+ from random import randint, random
6
12
  import re
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
13
+ from urllib.parse import parse_qs, urljoin, urlparse
13
14
 
14
- import asyncio
15
- import jwt
16
- import logging
17
15
  from aiohttp import ClientTimeout, client_exceptions
18
16
  from aiohttp.hdrs import METH_GET, METH_POST, METH_PUT
19
17
  from bs4 import BeautifulSoup
20
- from json import dumps as to_json
21
- from urllib.parse import urljoin, parse_qs, urlparse
18
+ import jwt
22
19
 
23
- from volkswagencarnet.vw_exceptions import AuthenticationException
24
20
  from .vw_const import (
25
- BRAND,
26
- COUNTRY,
27
- HEADERS_SESSION,
28
- HEADERS_AUTH,
29
- BASE_SESSION,
21
+ APP_URI,
30
22
  BASE_API,
31
23
  BASE_AUTH,
24
+ BASE_SESSION,
25
+ BRAND,
32
26
  CLIENT,
27
+ COUNTRY,
28
+ HEADERS_AUTH,
29
+ HEADERS_SESSION,
33
30
  USER_AGENT,
34
- APP_URI,
35
31
  )
36
32
  from .vw_utilities import json_loads
37
33
  from .vw_vehicle import Vehicle
38
34
 
39
35
  MAX_RETRIES_ON_RATE_LIMIT = 3
40
36
 
41
- version_info >= (3, 7) or exit("Python 3.7+ required")
42
-
43
- _LOGGER = logging.getLogger(__name__)
37
+ _LOGGER = logging.getLogger(__name__) # pylint: disable=unreachable
44
38
 
45
39
  TIMEOUT = timedelta(seconds=30)
46
40
  JWT_ALGORITHMS = ["RS256"]
@@ -53,7 +47,15 @@ class Connection:
53
47
  _login_lock = asyncio.Lock()
54
48
 
55
49
  # Init connection class
56
- def __init__(self, session, username, password, fulldebug=False, country=COUNTRY, interval=timedelta(minutes=5)):
50
+ def __init__(
51
+ self,
52
+ session,
53
+ username,
54
+ password,
55
+ fulldebug=False,
56
+ country=COUNTRY,
57
+ interval=timedelta(minutes=5),
58
+ ) -> None:
57
59
  """Initialize."""
58
60
  self._x_client_id = None
59
61
  self._session = session
@@ -76,7 +78,7 @@ class Connection:
76
78
 
77
79
  self._vehicles = []
78
80
 
79
- _LOGGER.debug(f"Using service {self._session_base}")
81
+ _LOGGER.debug("Using service %s", self._session_base)
80
82
 
81
83
  self._jarCookie = ""
82
84
  self._state = {}
@@ -84,7 +86,7 @@ class Connection:
84
86
  self._service_status = {}
85
87
 
86
88
  def _clear_cookies(self):
87
- self._session._cookie_jar._cookies.clear()
89
+ self._session._cookie_jar._cookies.clear() # pylint: disable=protected-access
88
90
 
89
91
  # API Login
90
92
  async def doLogin(self, tries: int = 1):
@@ -96,7 +98,9 @@ class Connection:
96
98
  self._session_logged_in = await self._login("Legacy")
97
99
  if self._session_logged_in:
98
100
  break
99
- _LOGGER.info("Something failed")
101
+ if i > tries:
102
+ _LOGGER.error("Login failed after %s tries", tries)
103
+ return False
100
104
  await asyncio.sleep(random() * 5)
101
105
 
102
106
  if not self._session_logged_in:
@@ -111,12 +115,12 @@ class Connection:
111
115
  loaded_vehicles = await self.get(url=f"{BASE_API}/vehicle/v2/vehicles")
112
116
  # Add Vehicle class object for all VIN-numbers from account
113
117
  if loaded_vehicles.get("data") is not None:
114
- _LOGGER.debug("Found vehicle(s) associated with account.")
118
+ _LOGGER.debug("Found vehicle(s) associated with account")
115
119
  self._vehicles = []
116
120
  for vehicle in loaded_vehicles.get("data"):
117
121
  self._vehicles.append(Vehicle(self, vehicle.get("vin")))
118
122
  else:
119
- _LOGGER.warning("Failed to login to We Connect API.")
123
+ _LOGGER.warning("Failed to login to Volkswagen Connect API")
120
124
  self._session_logged_in = False
121
125
  return False
122
126
 
@@ -124,251 +128,246 @@ class Connection:
124
128
  await self.update()
125
129
  return True
126
130
 
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
+
127
269
  async def _login(self, client="Legacy"):
128
270
  """Login function."""
129
271
 
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
153
272
  try:
154
- # Get OpenID config:
273
+ # Clear cookies and reset headers
155
274
  self._clear_cookies()
156
275
  self._session_headers = HEADERS_SESSION.copy()
157
276
  self._session_auth_headers = HEADERS_AUTH.copy()
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
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")
233
295
 
234
296
  # POST email
235
297
  # https://identity.vwgroup.io/signin-service/v1/{CLIENT_ID}/login/identifier
236
298
  self._session_auth_headers["Referer"] = authorization_endpoint
237
299
  self._session_auth_headers["Origin"] = auth_issuer
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
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}"
269
313
 
270
314
  # POST password
271
- # https://identity.vwgroup.io/signin-service/v1/{CLIENT_ID}/login/authenticate
272
315
  self._session_auth_headers["Referer"] = pe_url
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
316
+ redirect_location = await self.handle_login_with_password(
317
+ self._session, pw_url, self._session_auth_headers, pw_form
279
318
  )
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)
319
+
320
+ # Handle redirects and extract tokens
321
+ redirect_response = await self.follow_redirects(
322
+ self._session, pw_url, redirect_location
323
+ )
324
+ jwt_auth_code = parse_qs(urlparse(redirect_response).query)["code"][0]
325
+
326
+ # Exchange authorization code for tokens
324
327
  token_body = {
325
328
  "client_id": CLIENT[client].get("CLIENT_ID"),
326
329
  "grant_type": "authorization_code",
327
330
  "code": jwt_auth_code,
328
331
  "redirect_uri": APP_URI,
329
- # "brand": BRAND,
330
332
  }
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
333
+
334
+ # Token endpoint
335
+ token_response = await self.post_form(
336
+ self._session, token_endpoint, self._session_auth_headers, token_body
335
337
  )
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"):
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
+ ):
350
346
  _LOGGER.warning("User identity token could not be verified!")
351
347
  else:
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)
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)
357
355
  self._session_logged_in = False
358
356
  return False
359
- self._session_headers["Authorization"] = "Bearer " + self._session_tokens[client]["access_token"]
357
+ self._session_headers["Authorization"] = (
358
+ "Bearer " + self._session_tokens[client]["access_token"]
359
+ )
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")
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)})
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)}
372
371
 
373
372
  async def terminate(self):
374
373
  """Log out from connect services."""
@@ -377,28 +376,25 @@ class Connection:
377
376
 
378
377
  async def logout(self):
379
378
  """Logout, revoke tokens."""
380
- # TODO: not tested yet
381
379
  self._session_headers.pop("Authorization", None)
382
380
 
383
381
  if self._session_logged_in:
384
382
  if self._session_headers.get("identity", {}).get("identity_token"):
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)
383
+ _LOGGER.info("Revoking Identity Access Token")
384
+
391
385
  if self._session_headers.get("identity", {}).get("refresh_token"):
392
- _LOGGER.info("Revoking Identity Refresh Token...")
386
+ _LOGGER.info("Revoking Identity Refresh Token")
393
387
  params = {"token": self._session_tokens["identity"]["refresh_token"]}
394
- await self.post("https://emea.bff.cariad.digital/login/v1/idk/revoke", data=params)
388
+ await self.post(
389
+ "https://emea.bff.cariad.digital/login/v1/idk/revoke", data=params
390
+ )
395
391
 
396
392
  # HTTP methods to API
397
393
  async def _request(self, method, url, return_raw=False, **kwargs):
398
394
  """Perform a query to the VW-Group API."""
399
- _LOGGER.debug(f'HTTP {method} "{url}"')
395
+ _LOGGER.debug('HTTP %s "%s"', method, url)
400
396
  if kwargs.get("json", None):
401
- _LOGGER.debug(f'Request payload: {kwargs.get("json", None)}')
397
+ _LOGGER.debug("Request payload: %s", kwargs.get("json", None))
402
398
  try:
403
399
  async with self._session.request(
404
400
  method,
@@ -430,21 +426,36 @@ class Connection:
430
426
  res = await response.json(loads=json_loads)
431
427
  else:
432
428
  res = {}
433
- _LOGGER.debug(f"Not success status code [{response.status}] response: {response.text}")
434
- except Exception:
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
435
435
  res = {}
436
- _LOGGER.debug(f"Something went wrong [{response.status}] response: {response.text}")
436
+ _LOGGER.debug(
437
+ "Something went wrong [%s] response: %s",
438
+ response.status,
439
+ response.text,
440
+ )
437
441
  if return_raw:
438
442
  return response
439
- else:
440
- return res
443
+ return res
441
444
 
442
445
  if self._session_fulldebug:
443
446
  _LOGGER.debug(
444
- f'Request for "{url}" returned with status code [{response.status}], headers: {response.headers}, response: {res}'
447
+ 'Request for "%s" returned with status code [%s], headers: %s, response: %s',
448
+ url,
449
+ response.status,
450
+ response.headers,
451
+ res,
445
452
  )
446
453
  else:
447
- _LOGGER.debug(f'Request for "{url}" returned with status code [{response.status}]')
454
+ _LOGGER.debug(
455
+ 'Request for "%s" returned with status code [%s]',
456
+ url,
457
+ response.status,
458
+ )
448
459
 
449
460
  if return_raw:
450
461
  res = response
@@ -461,8 +472,7 @@ class Connection:
461
472
  async def get(self, url, vin="", tries=0):
462
473
  """Perform a get query."""
463
474
  try:
464
- response = await self._request(METH_GET, url)
465
- return response
475
+ return await self._request(METH_GET, url)
466
476
  except client_exceptions.ClientResponseError as error:
467
477
  if error.status == 400:
468
478
  _LOGGER.error(
@@ -470,78 +480,92 @@ class Connection:
470
480
  " correctly for this vehicle"
471
481
  )
472
482
  elif error.status == 401:
473
- _LOGGER.warning(f'Received "unauthorized" error while fetching data: {error}')
483
+ _LOGGER.warning(
484
+ 'Received "unauthorized" error while fetching data: %s', error
485
+ )
474
486
  self._session_logged_in = False
475
487
  elif error.status == 429 and tries < MAX_RETRIES_ON_RATE_LIMIT:
476
488
  delay = randint(1, 3 + tries * 2)
477
- _LOGGER.debug(f"Server side throttled. Waiting {delay}, try {tries + 1}")
489
+ _LOGGER.debug(
490
+ "Server side throttled. Waiting %s, try %s", delay, tries + 1
491
+ )
478
492
  await asyncio.sleep(delay)
479
493
  return await self.get(url, vin, tries + 1)
480
494
  elif error.status == 500:
481
- _LOGGER.info("Got HTTP 500 from server, service might be temporarily unavailable")
495
+ _LOGGER.info(
496
+ "Got HTTP 500 from server, service might be temporarily unavailable"
497
+ )
482
498
  elif error.status == 502:
483
- _LOGGER.info("Got HTTP 502 from server, this request might not be supported for this vehicle")
499
+ _LOGGER.info(
500
+ "Got HTTP 502 from server, this request might not be supported for this vehicle"
501
+ )
484
502
  else:
485
- _LOGGER.error(f"Got unhandled error from server: {error.status}")
503
+ _LOGGER.error("Got unhandled error from server: %s", error.status)
486
504
  return {"status_code": error.status}
487
505
 
488
506
  async def post(self, url, vin="", tries=0, return_raw=False, **data):
489
507
  """Perform a post query."""
490
508
  try:
491
509
  if data:
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)
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)
495
514
  except client_exceptions.ClientResponseError as error:
496
515
  if error.status == 429 and tries < MAX_RETRIES_ON_RATE_LIMIT:
497
516
  delay = randint(1, 3 + tries * 2)
498
- _LOGGER.debug(f"Server side throttled. Waiting {delay}, try {tries + 1}")
517
+ _LOGGER.debug(
518
+ "Server side throttled. Waiting %s, try %s", delay, tries + 1
519
+ )
499
520
  await asyncio.sleep(delay)
500
- return await self.post(url, vin, tries + 1, return_raw=return_raw, **data)
501
- else:
502
- raise
521
+ return await self.post(
522
+ url, vin, tries + 1, return_raw=return_raw, **data
523
+ )
524
+ raise
503
525
 
504
526
  async def put(self, url, vin="", tries=0, return_raw=False, **data):
505
527
  """Perform a put query."""
506
528
  try:
507
529
  if data:
508
530
  return await self._request(METH_PUT, url, return_raw=return_raw, **data)
509
- else:
510
- return await self._request(METH_PUT, url, return_raw=return_raw)
531
+ return await self._request(METH_PUT, url, return_raw=return_raw)
511
532
  except client_exceptions.ClientResponseError as error:
512
533
  if error.status == 429 and tries < MAX_RETRIES_ON_RATE_LIMIT:
513
534
  delay = randint(1, 3 + tries * 2)
514
- _LOGGER.debug(f"Server side throttled. Waiting {delay}, try {tries + 1}")
535
+ _LOGGER.debug(
536
+ "Server side throttled. Waiting %s, try %s", delay, tries + 1
537
+ )
515
538
  await asyncio.sleep(delay)
516
- return await self.put(url, vin, tries + 1, return_raw=return_raw, **data)
517
- else:
518
- raise
539
+ return await self.put(
540
+ url, vin, tries + 1, return_raw=return_raw, **data
541
+ )
542
+ raise
519
543
 
520
544
  # Update data for all Vehicles
521
545
  async def update(self):
522
546
  """Update status."""
523
547
  if not self.logged_in:
524
548
  if not await self._login():
525
- _LOGGER.warning(f"Login for {BRAND} account failed!")
549
+ _LOGGER.warning("Login for %s account failed!", BRAND)
526
550
  return False
527
551
  try:
528
552
  if not await self.validate_tokens:
529
- _LOGGER.info(f"Session expired. Initiating new login for {BRAND} account.")
553
+ _LOGGER.info(
554
+ "Session expired. Initiating new login for %s account", BRAND
555
+ )
530
556
  if not await self.doLogin():
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}")
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)
545
569
  return False
546
570
 
547
571
  async def getPendingRequests(self, vin):
@@ -549,15 +573,18 @@ class Connection:
549
573
  if not await self.validate_tokens:
550
574
  return False
551
575
  try:
552
- response = await self.get(f"{BASE_API}/vehicle/v1/vehicles/{vin}/pendingrequests")
576
+ response = await self.get(
577
+ f"{BASE_API}/vehicle/v1/vehicles/{vin}/pendingrequests"
578
+ )
553
579
 
554
580
  if response:
555
- response.update({"refreshTimestamp": datetime.now(timezone.utc)})
556
-
557
- return response
581
+ response["refreshTimestamp"] = datetime.now(UTC)
582
+ return response
558
583
 
559
- except Exception as error:
560
- _LOGGER.warning(f"Could not fetch information for pending requests, error: {error}")
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
+ )
561
588
  return False
562
589
 
563
590
  async def getOperationList(self, vin):
@@ -565,17 +592,22 @@ class Connection:
565
592
  if not await self.validate_tokens:
566
593
  return False
567
594
  try:
568
- response = await self.get(f"{BASE_API}/vehicle/v1/vehicles/{vin}/capabilities", "")
595
+ response = await self.get(
596
+ f"{BASE_API}/vehicle/v1/vehicles/{vin}/capabilities", ""
597
+ )
569
598
  if response.get("capabilities", False):
570
599
  data = response
571
600
  elif response.get("status_code", {}):
572
- _LOGGER.warning(f'Could not fetch operation list, HTTP status code: {response.get("status_code")}')
601
+ _LOGGER.warning(
602
+ "Could not fetch operation list, HTTP status code: %s",
603
+ response.get("status_code"),
604
+ )
573
605
  data = response
574
606
  else:
575
- _LOGGER.info(f"Could not fetch operation list: {response}")
607
+ _LOGGER.info("Could not fetch operation list: %s", response)
576
608
  data = {"error": "unknown"}
577
- except Exception as error:
578
- _LOGGER.warning(f"Could not fetch operation list, error: {error}")
609
+ except Exception as error: # pylint: disable=broad-exception-caught
610
+ _LOGGER.warning("Could not fetch operation list, error: %s", error)
579
611
  data = {"error": "unknown"}
580
612
  return data
581
613
 
@@ -585,22 +617,23 @@ class Connection:
585
617
  return False
586
618
  try:
587
619
  response = await self.get(
588
- f"{BASE_API}/vehicle/v1/vehicles/{vin}/selectivestatus?jobs={','.join(services)}", ""
620
+ f"{BASE_API}/vehicle/v1/vehicles/{vin}/selectivestatus?jobs={','.join(services)}",
621
+ "",
589
622
  )
590
623
 
591
624
  for service in services:
592
625
  if not response.get(service):
593
626
  _LOGGER.debug(
594
- f"Did not receive return data for requested service {service}. (This is expected for several service/car combinations)"
627
+ "Did not receive return data for requested service %s. (This is expected for several service/car combinations)",
628
+ service,
595
629
  )
596
630
 
597
631
  if response:
598
- response.update({"refreshTimestamp": datetime.now(timezone.utc)})
599
-
600
- return response
632
+ response.update({"refreshTimestamp": datetime.now(UTC)})
633
+ return response
601
634
 
602
- except Exception as error:
603
- _LOGGER.warning(f"Could not fetch selectivestatus, error: {error}")
635
+ except Exception as error: # pylint: disable=broad-exception-caught
636
+ _LOGGER.warning("Could not fetch selectivestatus, error: %s", error)
604
637
  return False
605
638
 
606
639
  async def getVehicleData(self, vin):
@@ -612,13 +645,12 @@ class Connection:
612
645
 
613
646
  for vehicle in response.get("data"):
614
647
  if vehicle.get("vin") == vin:
615
- data = {"vehicle": vehicle}
616
- return data
648
+ return {"vehicle": vehicle}
617
649
 
618
- _LOGGER.warning(f"Could not fetch vehicle data for vin {vin}")
650
+ _LOGGER.warning("Could not fetch vehicle data for vin %s", vin)
619
651
 
620
- except Exception as error:
621
- _LOGGER.warning(f"Could not fetch vehicle data, error: {error}")
652
+ except Exception as error: # pylint: disable=broad-exception-caught
653
+ _LOGGER.warning("Could not fetch vehicle data, error: %s", error)
622
654
  return False
623
655
 
624
656
  async def getParkingPosition(self, vin):
@@ -626,21 +658,29 @@ class Connection:
626
658
  if not await self.validate_tokens:
627
659
  return False
628
660
  try:
629
- response = await self.get(f"{BASE_API}/vehicle/v1/vehicles/{vin}/parkingposition", "")
661
+ response = await self.get(
662
+ f"{BASE_API}/vehicle/v1/vehicles/{vin}/parkingposition", ""
663
+ )
630
664
 
631
665
  if "data" in response:
632
666
  return {"isMoving": False, "parkingposition": response["data"]}
633
- elif response.get("status_code", {}):
667
+ if response.get("status_code", {}):
634
668
  if response.get("status_code", 0) == 204:
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")}')
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
+ )
640
678
  else:
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}")
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)
644
684
  return False
645
685
 
646
686
  async def getTripLast(self, vin):
@@ -648,14 +688,18 @@ class Connection:
648
688
  if not await self.validate_tokens:
649
689
  return False
650
690
  try:
651
- response = await self.get(f"{BASE_API}/vehicle/v1/trips/{vin}/shortterm/last", "")
691
+ response = await self.get(
692
+ f"{BASE_API}/vehicle/v1/trips/{vin}/shortterm/last", ""
693
+ )
652
694
  if "data" in response:
653
695
  return {"trip_last": response["data"]}
654
- else:
655
- _LOGGER.warning(f"Could not fetch last trip data, server response: {response}")
656
696
 
657
- except Exception as error:
658
- _LOGGER.warning(f"Could not fetch last trip data, error: {error}")
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)
659
703
  return False
660
704
 
661
705
  async def wakeUpVehicle(self, vin):
@@ -663,27 +707,30 @@ class Connection:
663
707
  if not await self.validate_tokens:
664
708
  return False
665
709
  try:
666
- response = await self.post(
667
- f"{BASE_API}/vehicle/v1/vehicles/{vin}/vehiclewakeuptrigger", json={}, return_raw=True
710
+ return await self.post(
711
+ f"{BASE_API}/vehicle/v1/vehicles/{vin}/vehiclewakeuptrigger",
712
+ json={},
713
+ return_raw=True,
668
714
  )
669
- return response
670
715
 
671
- except Exception as error:
672
- _LOGGER.warning(f"Could not refresh the data, error: {error}")
716
+ except Exception as error: # pylint: disable=broad-exception-caught
717
+ _LOGGER.warning("Could not refresh the data, error: %s", error)
673
718
  return False
674
719
 
675
720
  async def get_request_status(self, vin, requestId, actionId=""):
676
721
  """Return status of a request ID for a given section ID."""
677
722
  if self.logged_in is False:
678
723
  if not await self.doLogin():
679
- _LOGGER.warning(f"Login for {BRAND} account failed!")
680
- raise Exception(f"Login for {BRAND} account failed")
724
+ _LOGGER.warning("Login for %s account failed!", BRAND)
725
+ raise Exception(f"Login for {BRAND} account failed") # pylint: disable=broad-exception-raised
681
726
  try:
682
727
  if not await self.validate_tokens:
683
- _LOGGER.info(f"Session expired. Initiating new login for {BRAND} account.")
728
+ _LOGGER.info(
729
+ "Session expired. Initiating new login for %s account", BRAND
730
+ )
684
731
  if not await self.doLogin():
685
- _LOGGER.warning(f"Login for {BRAND} account failed!")
686
- raise Exception(f"Login for {BRAND} account failed")
732
+ _LOGGER.warning("Login for %s account failed!", BRAND)
733
+ raise Exception(f"Login for {BRAND} account failed") # pylint: disable=broad-exception-raised
687
734
 
688
735
  response = await self.getPendingRequests(vin)
689
736
 
@@ -694,35 +741,37 @@ class Connection:
694
741
  result = request.get("status")
695
742
 
696
743
  # Translate status messages to meaningful info
697
- if result == "in_progress" or result == "queued" or result == "fetched":
744
+ if result in ("in_progress", "queued", "fetched"):
698
745
  status = "In Progress"
699
- elif result == "request_fail" or result == "failed":
746
+ elif result in ("request_fail", "failed"):
700
747
  status = "Failed"
701
748
  elif result == "unfetched":
702
749
  status = "No response"
703
- elif result == "request_successful" or result == "successful":
750
+ elif result in ("request_successful", "successful"):
704
751
  status = "Success"
705
752
  elif result == "fail_ignition_on":
706
753
  status = "Failed because ignition is on"
707
754
  else:
708
755
  status = result
709
- return status
710
756
  except Exception as error:
711
- _LOGGER.warning(f"Failure during get request status: {error}")
712
- raise Exception(f"Failure during get request status: {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
+ return status
713
761
 
714
762
  async def check_spin_state(self):
715
763
  """Determine SPIN state to prevent lockout due to wrong SPIN."""
716
764
  result = await self.get(f"{BASE_API}/vehicle/v1/spin/state")
717
765
  remainingTries = result.get("remainingTries", None)
718
766
  if remainingTries is None:
719
- raise Exception("Couldn't determine S-PIN state.")
767
+ raise Exception("Couldn't determine S-PIN state.") # pylint: disable=broad-exception-raised
720
768
 
721
769
  if remainingTries < 3:
770
+ # pylint: disable=broad-exception-raised
722
771
  raise Exception(
723
772
  "Remaining tries for S-PIN is < 3. Bailing out for security reasons. "
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."
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."
726
775
  )
727
776
 
728
777
  return True
@@ -732,124 +781,148 @@ class Connection:
732
781
  action = "start" if action else "stop"
733
782
  try:
734
783
  response_raw = await self.post(
735
- f"{BASE_API}/vehicle/v1/vehicles/{vin}/climatisation/{action}", json=data, return_raw=True
784
+ f"{BASE_API}/vehicle/v1/vehicles/{vin}/climatisation/{action}",
785
+ json=data,
786
+ return_raw=True,
736
787
  )
737
788
  return await self._handle_action_result(response_raw)
738
789
  except Exception as e:
739
- raise Exception("Unknown error during setClimater") from e
790
+ raise Exception("Unknown error during setClimater") from e # pylint: disable=broad-exception-raised
740
791
 
741
792
  async def setClimaterSettings(self, vin, data):
742
793
  """Execute climatisation settings."""
743
794
  try:
744
795
  response_raw = await self.put(
745
- f"{BASE_API}/vehicle/v1/vehicles/{vin}/climatisation/settings", json=data, return_raw=True
796
+ f"{BASE_API}/vehicle/v1/vehicles/{vin}/climatisation/settings",
797
+ json=data,
798
+ return_raw=True,
746
799
  )
747
800
  return await self._handle_action_result(response_raw)
748
801
  except Exception as e:
749
- raise Exception("Unknown error during setClimaterSettings") from e
802
+ raise Exception("Unknown error during setClimaterSettings") from e # pylint: disable=broad-exception-raised
750
803
 
751
804
  async def setAuxiliary(self, vin, data, action):
752
805
  """Execute auxiliary climatisation actions."""
753
806
  action = "start" if action else "stop"
754
807
  try:
755
808
  response_raw = await self.post(
756
- f"{BASE_API}/vehicle/v1/vehicles/{vin}/auxiliaryheating/{action}", json=data, return_raw=True
809
+ f"{BASE_API}/vehicle/v1/vehicles/{vin}/auxiliaryheating/{action}",
810
+ json=data,
811
+ return_raw=True,
757
812
  )
758
813
  return await self._handle_action_result(response_raw)
759
814
  except Exception as e:
760
- raise Exception("Unknown error during setAuxiliary") from e
815
+ raise Exception("Unknown error during setAuxiliary") from e # pylint: disable=broad-exception-raised
761
816
 
762
817
  async def setWindowHeater(self, vin, action):
763
818
  """Execute window heating actions."""
764
819
  action = "start" if action else "stop"
765
820
  try:
766
821
  response_raw = await self.post(
767
- f"{BASE_API}/vehicle/v1/vehicles/{vin}/windowheating/{action}", json={}, return_raw=True
822
+ f"{BASE_API}/vehicle/v1/vehicles/{vin}/windowheating/{action}",
823
+ json={},
824
+ return_raw=True,
768
825
  )
769
826
  return await self._handle_action_result(response_raw)
770
827
  except Exception as e:
771
- raise Exception("Unknown error during setWindowHeater") from e
828
+ raise Exception("Unknown error during setWindowHeater") from e # pylint: disable=broad-exception-raised
772
829
 
773
830
  async def setCharging(self, vin, action):
774
831
  """Execute charging actions."""
775
832
  action = "start" if action else "stop"
776
833
  try:
777
834
  response_raw = await self.post(
778
- f"{BASE_API}/vehicle/v1/vehicles/{vin}/charging/{action}", json={}, return_raw=True
835
+ f"{BASE_API}/vehicle/v1/vehicles/{vin}/charging/{action}",
836
+ json={},
837
+ return_raw=True,
779
838
  )
780
839
  return await self._handle_action_result(response_raw)
781
840
  except Exception as e:
782
- raise Exception("Unknown error during setCharging") from e
841
+ raise Exception("Unknown error during setCharging") from e # pylint: disable=broad-exception-raised
783
842
 
784
843
  async def setChargingSettings(self, vin, data):
785
844
  """Execute charging actions."""
786
845
  try:
787
846
  response_raw = await self.put(
788
- f"{BASE_API}/vehicle/v1/vehicles/{vin}/charging/settings", json=data, return_raw=True
847
+ f"{BASE_API}/vehicle/v1/vehicles/{vin}/charging/settings",
848
+ json=data,
849
+ return_raw=True,
789
850
  )
790
851
  return await self._handle_action_result(response_raw)
791
852
  except Exception as e:
792
- raise Exception("Unknown error during setChargingSettings") from e
853
+ raise Exception("Unknown error during setChargingSettings") from e # pylint: disable=broad-exception-raised
793
854
 
794
855
  async def setChargingCareModeSettings(self, vin, data):
795
856
  """Execute battery care mode actions."""
796
857
  try:
797
858
  response_raw = await self.put(
798
- f"{BASE_API}/vehicle/v1/vehicles/{vin}/charging/care/settings", json=data, return_raw=True
859
+ f"{BASE_API}/vehicle/v1/vehicles/{vin}/charging/care/settings",
860
+ json=data,
861
+ return_raw=True,
799
862
  )
800
863
  return await self._handle_action_result(response_raw)
801
864
  except Exception as e:
802
- raise Exception("Unknown error during setChargingCareModeSettings") from e
865
+ raise Exception("Unknown error during setChargingCareModeSettings") from e # pylint: disable=broad-exception-raised
803
866
 
804
867
  async def setReadinessBatterySupport(self, vin, data):
805
868
  """Execute readiness battery support actions."""
806
869
  try:
807
870
  response_raw = await self.put(
808
- f"{BASE_API}/vehicle/v1/vehicles/{vin}/readiness/batterysupport", json=data, return_raw=True
871
+ f"{BASE_API}/vehicle/v1/vehicles/{vin}/readiness/batterysupport",
872
+ json=data,
873
+ return_raw=True,
809
874
  )
810
875
  return await self._handle_action_result(response_raw)
811
876
  except Exception as e:
812
- raise Exception("Unknown error during setReadinessBatterySupport") from e
877
+ raise Exception("Unknown error during setReadinessBatterySupport") from e # pylint: disable=broad-exception-raised
813
878
 
814
879
  async def setDepartureProfiles(self, vin, data):
815
880
  """Execute departure timers actions."""
816
881
  try:
817
882
  response_raw = await self.put(
818
- f"{BASE_API}/vehicle/v1/vehicles/{vin}/departure/profiles", json=data, return_raw=True
883
+ f"{BASE_API}/vehicle/v1/vehicles/{vin}/departure/profiles",
884
+ json=data,
885
+ return_raw=True,
819
886
  )
820
887
  return await self._handle_action_result(response_raw)
821
888
  except Exception as e:
822
- raise Exception("Unknown error during setDepartureProfiles") from e
889
+ raise Exception("Unknown error during setDepartureProfiles") from e # pylint: disable=broad-exception-raised
823
890
 
824
891
  async def setClimatisationTimers(self, vin, data):
825
892
  """Execute climatisation timers actions."""
826
893
  try:
827
894
  response_raw = await self.put(
828
- f"{BASE_API}/vehicle/v1/vehicles/{vin}/climatisation/timers", json=data, return_raw=True
895
+ f"{BASE_API}/vehicle/v1/vehicles/{vin}/climatisation/timers",
896
+ json=data,
897
+ return_raw=True,
829
898
  )
830
899
  return await self._handle_action_result(response_raw)
831
900
  except Exception as e:
832
- raise Exception("Unknown error during setClimatisationTimers") from e
901
+ raise Exception("Unknown error during setClimatisationTimers") from e # pylint: disable=broad-exception-raised
833
902
 
834
903
  async def setAuxiliaryHeatingTimers(self, vin, data):
835
904
  """Execute auxiliary heating timers actions."""
836
905
  try:
837
906
  response_raw = await self.put(
838
- f"{BASE_API}/vehicle/v1/vehicles/{vin}/auxiliaryheating/timers", json=data, return_raw=True
907
+ f"{BASE_API}/vehicle/v1/vehicles/{vin}/auxiliaryheating/timers",
908
+ json=data,
909
+ return_raw=True,
839
910
  )
840
911
  return await self._handle_action_result(response_raw)
841
912
  except Exception as e:
842
- raise Exception("Unknown error during setAuxiliaryHeatingTimers") from e
913
+ raise Exception("Unknown error during setAuxiliaryHeatingTimers") from e # pylint: disable=broad-exception-raised
843
914
 
844
915
  async def setDepartureTimers(self, vin, data):
845
916
  """Execute departure timers actions."""
846
917
  try:
847
918
  response_raw = await self.put(
848
- f"{BASE_API}/vehicle/v1/vehicles/{vin}/departure/timers", json=data, return_raw=True
919
+ f"{BASE_API}/vehicle/v1/vehicles/{vin}/departure/timers",
920
+ json=data,
921
+ return_raw=True,
849
922
  )
850
923
  return await self._handle_action_result(response_raw)
851
924
  except Exception as e:
852
- raise Exception("Unknown error during setDepartureTimers") from e
925
+ raise Exception("Unknown error during setDepartureTimers") from e # pylint: disable=broad-exception-raised
853
926
 
854
927
  async def setLock(self, vin, lock, spin):
855
928
  """Remote lock and unlock actions."""
@@ -857,11 +930,13 @@ class Connection:
857
930
  action = "lock" if lock else "unlock"
858
931
  try:
859
932
  response_raw = await self.post(
860
- f"{BASE_API}/vehicle/v1/vehicles/{vin}/access/{action}", json={"spin": spin}, return_raw=True
933
+ f"{BASE_API}/vehicle/v1/vehicles/{vin}/access/{action}",
934
+ json={"spin": spin},
935
+ return_raw=True,
861
936
  )
862
937
  return await self._handle_action_result(response_raw)
863
938
  except Exception as e:
864
- raise Exception("Unknown error during setLock") from e
939
+ raise Exception("Unknown error during setLock") from e # pylint: disable=broad-exception-raised
865
940
 
866
941
  # Token handling #
867
942
  @property
@@ -870,10 +945,14 @@ class Connection:
870
945
  idtoken = self._session_tokens["identity"]["id_token"]
871
946
  atoken = self._session_tokens["identity"]["access_token"]
872
947
  id_exp = jwt.decode(
873
- idtoken, options={"verify_signature": False, "verify_aud": False}, algorithms=JWT_ALGORITHMS
948
+ idtoken,
949
+ options={"verify_signature": False, "verify_aud": False},
950
+ algorithms=JWT_ALGORITHMS,
874
951
  ).get("exp", None)
875
952
  at_exp = jwt.decode(
876
- atoken, options={"verify_signature": False, "verify_aud": False}, algorithms=JWT_ALGORITHMS
953
+ atoken,
954
+ options={"verify_signature": False, "verify_aud": False},
955
+ algorithms=JWT_ALGORITHMS,
877
956
  ).get("exp", None)
878
957
  id_dt = datetime.fromtimestamp(int(id_exp))
879
958
  at_dt = datetime.fromtimestamp(int(at_exp))
@@ -882,14 +961,14 @@ class Connection:
882
961
 
883
962
  # Check if tokens have expired, or expires now
884
963
  if now >= id_dt or now >= at_dt:
885
- _LOGGER.debug("Tokens have expired. Try to fetch new tokens.")
964
+ _LOGGER.debug("Tokens have expired. Try to fetch new tokens")
886
965
  if await self.refresh_tokens():
887
966
  _LOGGER.debug("Successfully refreshed tokens")
888
967
  else:
889
968
  return False
890
969
  # Check if tokens expires before next update
891
970
  elif later >= id_dt or later >= at_dt:
892
- _LOGGER.debug("Tokens about to expire. Try to fetch new tokens.")
971
+ _LOGGER.debug("Tokens about to expire. Try to fetch new tokens")
893
972
  if await self.refresh_tokens():
894
973
  _LOGGER.debug("Successfully refreshed tokens")
895
974
  else:
@@ -921,10 +1000,10 @@ class Connection:
921
1000
 
922
1001
  pubkey = pubkeys[token_kid]
923
1002
  jwt.decode(token, key=pubkey, algorithms=JWT_ALGORITHMS, audience=audience)
924
- return True
925
- except Exception as error:
926
- _LOGGER.debug(f"Failed to verify token, error: {error}")
1003
+ except Exception as error: # pylint: disable=broad-exception-caught
1004
+ _LOGGER.debug("Failed to verify token, error: %s", error)
927
1005
  return False
1006
+ return True
928
1007
 
929
1008
  async def refresh_tokens(self):
930
1009
  """Refresh tokens."""
@@ -942,7 +1021,9 @@ class Connection:
942
1021
  "client_id": CLIENT["Legacy"]["CLIENT_ID"],
943
1022
  }
944
1023
  response = await self._session.post(
945
- url="https://emea.bff.cariad.digital/login/v1/idk/token", headers=tHeaders, data=body
1024
+ url="https://emea.bff.cariad.digital/login/v1/idk/token",
1025
+ headers=tHeaders,
1026
+ data=body,
946
1027
  )
947
1028
  await self.update_service_status("token", response.status)
948
1029
  if response.status == 200:
@@ -952,15 +1033,19 @@ class Connection:
952
1033
  _LOGGER.warning("Token could not be verified!")
953
1034
  for token in tokens:
954
1035
  self._session_tokens["identity"][token] = tokens[token]
955
- self._session_headers["Authorization"] = "Bearer " + self._session_tokens["identity"]["access_token"]
1036
+ self._session_headers["Authorization"] = (
1037
+ "Bearer " + self._session_tokens["identity"]["access_token"]
1038
+ )
956
1039
  else:
957
- _LOGGER.warning(f"Something went wrong when refreshing {BRAND} account tokens.")
1040
+ _LOGGER.warning(
1041
+ "Something went wrong when refreshing %s account tokens", BRAND
1042
+ )
958
1043
  return False
959
-
960
- return True
961
- except Exception as error:
962
- _LOGGER.warning(f"Could not refresh tokens: {error}")
1044
+ except Exception as error: # pylint: disable=broad-exception-caught
1045
+ _LOGGER.warning("Could not refresh tokens: %s", error)
963
1046
  return False
1047
+ else:
1048
+ return True
964
1049
 
965
1050
  async def update_service_status(self, url, response_code):
966
1051
  """Update service status."""
@@ -990,7 +1075,7 @@ class Connection:
990
1075
  elif "token" in url:
991
1076
  self._service_status["token"] = status
992
1077
  else:
993
- _LOGGER.debug(f'Unhandled API URL: "{url}"')
1078
+ _LOGGER.debug('Unhandled API URL: "%s"', url)
994
1079
 
995
1080
  async def get_service_status(self):
996
1081
  """Return list of service statuses."""
@@ -1005,8 +1090,7 @@ class Connection:
1005
1090
 
1006
1091
  @property
1007
1092
  def logged_in(self):
1008
- """
1009
- Return cached logged in state.
1093
+ """Return cached logged in state.
1010
1094
 
1011
1095
  Not actually checking anything.
1012
1096
  """
@@ -1014,7 +1098,14 @@ class Connection:
1014
1098
 
1015
1099
  def vehicle(self, vin):
1016
1100
  """Return vehicle object for given vin."""
1017
- return next((vehicle for vehicle in self.vehicles if vehicle.unique_id.lower() == vin.lower()), None)
1101
+ return next(
1102
+ (
1103
+ vehicle
1104
+ for vehicle in self.vehicles
1105
+ if vehicle.unique_id.lower() == vin.lower()
1106
+ ),
1107
+ None,
1108
+ )
1018
1109
 
1019
1110
  def hash_spin(self, challenge, spin):
1020
1111
  """Convert SPIN and challenge to hash."""
@@ -1029,8 +1120,8 @@ class Connection:
1029
1120
  try:
1030
1121
  if not await self.validate_tokens:
1031
1122
  return False
1032
-
1033
- return True
1034
1123
  except OSError as error:
1035
1124
  _LOGGER.warning("Could not validate login: %s", error)
1036
1125
  return False
1126
+ else:
1127
+ return True