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.
- volkswagencarnet/version.py +1 -1
- volkswagencarnet/vw_connection.py +489 -398
- volkswagencarnet/vw_const.py +1 -1
- volkswagencarnet/vw_dashboard.py +548 -139
- volkswagencarnet/vw_utilities.py +24 -40
- volkswagencarnet/vw_vehicle.py +1712 -687
- {volkswagencarnet-5.0.0b4.dist-info → volkswagencarnet-5.0.0b5.dist-info}/METADATA +5 -5
- volkswagencarnet-5.0.0b5.dist-info/RECORD +12 -0
- volkswagencarnet/vw_exceptions.py +0 -7
- volkswagencarnet-5.0.0b4.dist-info/RECORD +0 -13
- {volkswagencarnet-5.0.0b4.dist-info → volkswagencarnet-5.0.0b5.dist-info}/LICENSE.txt +0 -0
- {volkswagencarnet-5.0.0b4.dist-info → volkswagencarnet-5.0.0b5.dist-info}/WHEEL +0 -0
- {volkswagencarnet-5.0.0b4.dist-info → volkswagencarnet-5.0.0b5.dist-info}/top_level.txt +0 -0
|
@@ -1,46 +1,40 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Communicate with
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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__(
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
-
#
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
#
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
274
|
-
|
|
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
|
-
|
|
281
|
-
#
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
self._session_tokens[client] =
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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"] =
|
|
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
|
-
|
|
367
|
-
return
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
395
|
+
_LOGGER.debug('HTTP %s "%s"', method, url)
|
|
400
396
|
if kwargs.get("json", None):
|
|
401
|
-
_LOGGER.debug(
|
|
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(
|
|
434
|
-
|
|
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(
|
|
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
|
-
|
|
440
|
-
return res
|
|
443
|
+
return res
|
|
441
444
|
|
|
442
445
|
if self._session_fulldebug:
|
|
443
446
|
_LOGGER.debug(
|
|
444
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
495
|
+
_LOGGER.info(
|
|
496
|
+
"Got HTTP 500 from server, service might be temporarily unavailable"
|
|
497
|
+
)
|
|
482
498
|
elif error.status == 502:
|
|
483
|
-
_LOGGER.info(
|
|
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(
|
|
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(
|
|
493
|
-
|
|
494
|
-
|
|
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(
|
|
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(
|
|
501
|
-
|
|
502
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
517
|
-
|
|
518
|
-
|
|
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(
|
|
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(
|
|
553
|
+
_LOGGER.info(
|
|
554
|
+
"Session expired. Initiating new login for %s account", BRAND
|
|
555
|
+
)
|
|
530
556
|
if not await self.doLogin():
|
|
531
|
-
_LOGGER.warning(
|
|
532
|
-
raise Exception(f"Login for {BRAND} account failed")
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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(
|
|
576
|
+
response = await self.get(
|
|
577
|
+
f"{BASE_API}/vehicle/v1/vehicles/{vin}/pendingrequests"
|
|
578
|
+
)
|
|
553
579
|
|
|
554
580
|
if response:
|
|
555
|
-
response
|
|
556
|
-
|
|
557
|
-
return response
|
|
581
|
+
response["refreshTimestamp"] = datetime.now(UTC)
|
|
582
|
+
return response
|
|
558
583
|
|
|
559
|
-
except Exception as error:
|
|
560
|
-
_LOGGER.warning(
|
|
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(
|
|
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(
|
|
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(
|
|
607
|
+
_LOGGER.info("Could not fetch operation list: %s", response)
|
|
576
608
|
data = {"error": "unknown"}
|
|
577
|
-
except Exception as error:
|
|
578
|
-
_LOGGER.warning(
|
|
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
|
-
|
|
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(
|
|
599
|
-
|
|
600
|
-
return response
|
|
632
|
+
response.update({"refreshTimestamp": datetime.now(UTC)})
|
|
633
|
+
return response
|
|
601
634
|
|
|
602
|
-
except Exception as error:
|
|
603
|
-
_LOGGER.warning(
|
|
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
|
-
|
|
616
|
-
return data
|
|
648
|
+
return {"vehicle": vehicle}
|
|
617
649
|
|
|
618
|
-
_LOGGER.warning(
|
|
650
|
+
_LOGGER.warning("Could not fetch vehicle data for vin %s", vin)
|
|
619
651
|
|
|
620
|
-
except Exception as error:
|
|
621
|
-
_LOGGER.warning(
|
|
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(
|
|
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
|
-
|
|
667
|
+
if response.get("status_code", {}):
|
|
634
668
|
if response.get("status_code", 0) == 204:
|
|
635
|
-
_LOGGER.debug(
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
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(
|
|
642
|
-
|
|
643
|
-
|
|
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(
|
|
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
|
-
|
|
658
|
-
|
|
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
|
-
|
|
667
|
-
f"{BASE_API}/vehicle/v1/vehicles/{vin}/vehiclewakeuptrigger",
|
|
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(
|
|
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(
|
|
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(
|
|
728
|
+
_LOGGER.info(
|
|
729
|
+
"Session expired. Initiating new login for %s account", BRAND
|
|
730
|
+
)
|
|
684
731
|
if not await self.doLogin():
|
|
685
|
-
_LOGGER.warning(
|
|
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
|
|
744
|
+
if result in ("in_progress", "queued", "fetched"):
|
|
698
745
|
status = "In Progress"
|
|
699
|
-
elif result
|
|
746
|
+
elif result in ("request_fail", "failed"):
|
|
700
747
|
status = "Failed"
|
|
701
748
|
elif result == "unfetched":
|
|
702
749
|
status = "No response"
|
|
703
|
-
elif result
|
|
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(
|
|
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
|
-
|
|
725
|
-
|
|
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}",
|
|
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",
|
|
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}",
|
|
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}",
|
|
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}",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
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}",
|
|
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,
|
|
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,
|
|
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
|
-
|
|
925
|
-
|
|
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",
|
|
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"] =
|
|
1036
|
+
self._session_headers["Authorization"] = (
|
|
1037
|
+
"Bearer " + self._session_tokens["identity"]["access_token"]
|
|
1038
|
+
)
|
|
956
1039
|
else:
|
|
957
|
-
_LOGGER.warning(
|
|
1040
|
+
_LOGGER.warning(
|
|
1041
|
+
"Something went wrong when refreshing %s account tokens", BRAND
|
|
1042
|
+
)
|
|
958
1043
|
return False
|
|
959
|
-
|
|
960
|
-
|
|
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(
|
|
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(
|
|
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
|