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.
- volkswagencarnet/version.py +1 -1
- volkswagencarnet/vw_connection.py +398 -489
- volkswagencarnet/vw_const.py +1 -1
- volkswagencarnet/vw_dashboard.py +139 -450
- volkswagencarnet/vw_exceptions.py +7 -0
- volkswagencarnet/vw_utilities.py +40 -24
- volkswagencarnet/vw_vehicle.py +678 -1646
- {volkswagencarnet-5.0.0b2.dist-info → volkswagencarnet-5.0.0b4.dist-info}/METADATA +5 -5
- volkswagencarnet-5.0.0b4.dist-info/RECORD +13 -0
- volkswagencarnet-5.0.0b2.dist-info/RECORD +0 -12
- {volkswagencarnet-5.0.0b2.dist-info → volkswagencarnet-5.0.0b4.dist-info}/LICENSE.txt +0 -0
- {volkswagencarnet-5.0.0b2.dist-info → volkswagencarnet-5.0.0b4.dist-info}/WHEEL +0 -0
- {volkswagencarnet-5.0.0b2.dist-info → volkswagencarnet-5.0.0b4.dist-info}/top_level.txt +0 -0
|
@@ -1,40 +1,46 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Communicate with
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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()
|
|
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
|
-
|
|
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
|
|
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
|
-
#
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
#
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
340
|
-
self._session_tokens[client] =
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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")
|
|
366
|
-
|
|
367
|
-
return {"id": None, "state": "Throttled"}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
|
399
|
+
_LOGGER.debug(f'HTTP {method} "{url}"')
|
|
396
400
|
if kwargs.get("json", None):
|
|
397
|
-
_LOGGER.debug(
|
|
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
|
-
|
|
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
|
-
|
|
439
|
+
else:
|
|
440
|
+
return res
|
|
444
441
|
|
|
445
442
|
if self._session_fulldebug:
|
|
446
443
|
_LOGGER.debug(
|
|
447
|
-
'Request for "
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
558
|
-
raise Exception(f"Login for {BRAND} account failed")
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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
|
|
582
|
-
return response
|
|
555
|
+
response.update({"refreshTimestamp": datetime.now(timezone.utc)})
|
|
583
556
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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:
|
|
575
|
+
_LOGGER.info(f"Could not fetch operation list: {response}")
|
|
608
576
|
data = {"error": "unknown"}
|
|
609
|
-
except Exception as error:
|
|
610
|
-
_LOGGER.warning("Could not fetch operation list, 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
|
|
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(
|
|
633
|
-
|
|
598
|
+
response.update({"refreshTimestamp": datetime.now(timezone.utc)})
|
|
599
|
+
|
|
600
|
+
return response
|
|
634
601
|
|
|
635
|
-
except Exception as error:
|
|
636
|
-
_LOGGER.warning("Could not fetch selectivestatus, 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
|
-
|
|
615
|
+
data = {"vehicle": vehicle}
|
|
616
|
+
return data
|
|
649
617
|
|
|
650
|
-
_LOGGER.warning("Could not fetch vehicle data for vin
|
|
618
|
+
_LOGGER.warning(f"Could not fetch vehicle data for vin {vin}")
|
|
651
619
|
|
|
652
|
-
except Exception as error:
|
|
653
|
-
_LOGGER.warning("Could not fetch vehicle data, 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
|
-
|
|
633
|
+
elif response.get("status_code", {}):
|
|
668
634
|
if response.get("status_code", 0) == 204:
|
|
669
|
-
_LOGGER.debug(
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
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
|
-
|
|
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
|
-
|
|
698
|
-
|
|
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
|
-
|
|
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:
|
|
717
|
-
_LOGGER.warning("Could not refresh the data, 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
|
|
725
|
-
raise Exception(f"Login for {BRAND} account failed")
|
|
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
|
|
733
|
-
raise Exception(f"Login for {BRAND} account failed")
|
|
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
|
|
697
|
+
if result == "in_progress" or result == "queued" or result == "fetched":
|
|
745
698
|
status = "In Progress"
|
|
746
|
-
elif result
|
|
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
|
|
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.")
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
1004
|
-
|
|
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
|
-
|
|
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: "
|
|
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
|
-
"""
|
|
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
|