carconnectivity-connector-skoda 0.8__py3-none-any.whl → 0.8.2__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.
- {carconnectivity_connector_skoda-0.8.dist-info → carconnectivity_connector_skoda-0.8.2.dist-info}/METADATA +4 -3
- {carconnectivity_connector_skoda-0.8.dist-info → carconnectivity_connector_skoda-0.8.2.dist-info}/RECORD +10 -10
- carconnectivity_connectors/skoda/_version.py +16 -3
- carconnectivity_connectors/skoda/auth/my_skoda_session.py +24 -13
- carconnectivity_connectors/skoda/auth/openid_session.py +13 -11
- carconnectivity_connectors/skoda/connector.py +1 -1
- carconnectivity_connectors/skoda/mqtt_client.py +19 -2
- {carconnectivity_connector_skoda-0.8.dist-info → carconnectivity_connector_skoda-0.8.2.dist-info}/WHEEL +0 -0
- {carconnectivity_connector_skoda-0.8.dist-info → carconnectivity_connector_skoda-0.8.2.dist-info}/licenses/LICENSE +0 -0
- {carconnectivity_connector_skoda-0.8.dist-info → carconnectivity_connector_skoda-0.8.2.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: carconnectivity-connector-skoda
|
|
3
|
-
Version: 0.8
|
|
3
|
+
Version: 0.8.2
|
|
4
4
|
Summary: CarConnectivity connector for Skoda services
|
|
5
5
|
Author: Till Steinbach
|
|
6
6
|
License: MIT License
|
|
@@ -33,13 +33,14 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
33
33
|
Classifier: Programming Language :: Python :: 3.11
|
|
34
34
|
Classifier: Programming Language :: Python :: 3.12
|
|
35
35
|
Classifier: Programming Language :: Python :: 3.13
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
36
37
|
Classifier: Topic :: Software Development :: Libraries
|
|
37
38
|
Requires-Python: >=3.8
|
|
38
39
|
Description-Content-Type: text/markdown
|
|
39
40
|
License-File: LICENSE
|
|
40
|
-
Requires-Dist: carconnectivity>=0.
|
|
41
|
+
Requires-Dist: carconnectivity>=0.8.1
|
|
41
42
|
Requires-Dist: oauthlib~=3.3.1
|
|
42
|
-
Requires-Dist: requests~=2.32.
|
|
43
|
+
Requires-Dist: requests~=2.32.5
|
|
43
44
|
Requires-Dist: pyjwt~=2.10
|
|
44
45
|
Requires-Dist: paho-mqtt~=2.1.0
|
|
45
46
|
Dynamic: license-file
|
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
carconnectivity_connector_skoda-0.8.dist-info/licenses/LICENSE,sha256=PIwI1alwDyOfvEQHdGCm2u9uf_mGE8030xZDfun0xTo,1071
|
|
1
|
+
carconnectivity_connector_skoda-0.8.2.dist-info/licenses/LICENSE,sha256=PIwI1alwDyOfvEQHdGCm2u9uf_mGE8030xZDfun0xTo,1071
|
|
2
2
|
carconnectivity_connectors/skoda/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
-
carconnectivity_connectors/skoda/_version.py,sha256=
|
|
3
|
+
carconnectivity_connectors/skoda/_version.py,sha256=K6dg_KQgkOH-DF8J9hcSjz3upL94O2YIVOO7FP9tPpk,704
|
|
4
4
|
carconnectivity_connectors/skoda/capability.py,sha256=TC8-yC23UUrf0faePdbZL0802DHXbtGDcSlt3vj5ltg,4770
|
|
5
5
|
carconnectivity_connectors/skoda/charging.py,sha256=7DPNiTWFhxiiEFKVnbIIU2TCmkpmcMWx_zsHXGXFpAQ,6856
|
|
6
6
|
carconnectivity_connectors/skoda/climatization.py,sha256=Jut468SkxjPBDTqroWFvDifVPfJBxGjsFed5pc4kZkg,1768
|
|
7
7
|
carconnectivity_connectors/skoda/command_impl.py,sha256=wDCI3Bka5pXlbyI4yVFS353TgFGyiBHBkERpP2g0A9w,3230
|
|
8
|
-
carconnectivity_connectors/skoda/connector.py,sha256=
|
|
8
|
+
carconnectivity_connectors/skoda/connector.py,sha256=y25ZY1jCH-8IXSK847Stu4GbRK6j35xwZJKU9C7TSSU,147301
|
|
9
9
|
carconnectivity_connectors/skoda/error.py,sha256=ffxzvjmci7vtp9Q1K4DR1kBF0kTJxN5Gluci3kDBkEI,2459
|
|
10
|
-
carconnectivity_connectors/skoda/mqtt_client.py,sha256=
|
|
10
|
+
carconnectivity_connectors/skoda/mqtt_client.py,sha256=f4fRFeI1VUCGm9ZzGj_vnctiSIiMAoolYLAwKx4apqA,40086
|
|
11
11
|
carconnectivity_connectors/skoda/vehicle.py,sha256=q5gwe-_yPfE_-aEc17UQ-Q0Z46IN7PCpNG5jLw5PZl0,3981
|
|
12
12
|
carconnectivity_connectors/skoda/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
13
|
carconnectivity_connectors/skoda/auth/auth_util.py,sha256=dGLUbUre0HBsTg_Ii5vW34f8DLrCykYJYCyzEvUBBEE,4434
|
|
14
|
-
carconnectivity_connectors/skoda/auth/my_skoda_session.py,sha256=
|
|
15
|
-
carconnectivity_connectors/skoda/auth/openid_session.py,sha256=
|
|
14
|
+
carconnectivity_connectors/skoda/auth/my_skoda_session.py,sha256=5olw2TsId63BbJxmMk7i193Vww7RlBo9mpE3Ucl5fhw,11207
|
|
15
|
+
carconnectivity_connectors/skoda/auth/openid_session.py,sha256=CiHKXWpZqlhjzKNCKQ_Y5f3RJgw4gEKbWp_I5_fXa7Y,16984
|
|
16
16
|
carconnectivity_connectors/skoda/auth/session_manager.py,sha256=Uf1vujuDBYUCAXhYToOsZkgbTtfmY3Qe0ICTfwomBpI,2899
|
|
17
17
|
carconnectivity_connectors/skoda/auth/skoda_web_session.py,sha256=tapjCRRPBu3tHrDoKmtuAlQhgxktib3oWTB8MHEzZTY,10816
|
|
18
18
|
carconnectivity_connectors/skoda/auth/helpers/blacklist_retry.py,sha256=f3wsiY5bpHDBxp7Va1Mv9nKJ4u3qnCHZZmDu78_AhMk,1251
|
|
19
19
|
carconnectivity_connectors/skoda/ui/connector_ui.py,sha256=lLjwoakRaU0S80hAVwVi4JA1wtHycGHcoM2-7S9qsqI,1386
|
|
20
|
-
carconnectivity_connector_skoda-0.8.dist-info/METADATA,sha256=
|
|
21
|
-
carconnectivity_connector_skoda-0.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
22
|
-
carconnectivity_connector_skoda-0.8.dist-info/top_level.txt,sha256=KqA8GviZsDH4PtmnwSQsz0HB_w-TWkeEHLIRNo5dTaI,27
|
|
23
|
-
carconnectivity_connector_skoda-0.8.dist-info/RECORD,,
|
|
20
|
+
carconnectivity_connector_skoda-0.8.2.dist-info/METADATA,sha256=LP-Xq58069bnrLJeiyEC8yg0zJGgGQGLt3AIuKStJes,5434
|
|
21
|
+
carconnectivity_connector_skoda-0.8.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
22
|
+
carconnectivity_connector_skoda-0.8.2.dist-info/top_level.txt,sha256=KqA8GviZsDH4PtmnwSQsz0HB_w-TWkeEHLIRNo5dTaI,27
|
|
23
|
+
carconnectivity_connector_skoda-0.8.2.dist-info/RECORD,,
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
# file generated by setuptools-scm
|
|
2
2
|
# don't change, don't track in version control
|
|
3
3
|
|
|
4
|
-
__all__ = [
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
5
12
|
|
|
6
13
|
TYPE_CHECKING = False
|
|
7
14
|
if TYPE_CHECKING:
|
|
@@ -9,13 +16,19 @@ if TYPE_CHECKING:
|
|
|
9
16
|
from typing import Union
|
|
10
17
|
|
|
11
18
|
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
12
20
|
else:
|
|
13
21
|
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
14
23
|
|
|
15
24
|
version: str
|
|
16
25
|
__version__: str
|
|
17
26
|
__version_tuple__: VERSION_TUPLE
|
|
18
27
|
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
19
30
|
|
|
20
|
-
__version__ = version = '0.8'
|
|
21
|
-
__version_tuple__ = version_tuple = (0, 8)
|
|
31
|
+
__version__ = version = '0.8.2'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 8, 2)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = None
|
|
@@ -12,9 +12,11 @@ import random
|
|
|
12
12
|
import string
|
|
13
13
|
|
|
14
14
|
from urllib.parse import parse_qsl, urlparse
|
|
15
|
+
from urllib3.exceptions import NameResolutionError
|
|
15
16
|
|
|
16
17
|
import requests
|
|
17
18
|
from requests.models import CaseInsensitiveDict
|
|
19
|
+
from requests.exceptions import ReadTimeout, ConnectionError
|
|
18
20
|
|
|
19
21
|
from oauthlib.common import add_params_to_uri, generate_nonce, to_unicode
|
|
20
22
|
from oauthlib.oauth2 import InsecureTransportError
|
|
@@ -58,17 +60,24 @@ class MySkodaSession(SkodaWebSession):
|
|
|
58
60
|
def login(self):
|
|
59
61
|
super(MySkodaSession, self).login()
|
|
60
62
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
63
|
+
try:
|
|
64
|
+
verifier = "".join(random.choices(string.ascii_uppercase + string.digits, k=16))
|
|
65
|
+
verifier_hash = hashlib.sha256(verifier.encode("utf-8")).digest()
|
|
66
|
+
code_challenge = base64.b64encode(verifier_hash).decode("utf-8").replace("+", "-").replace("/", "_").rstrip("=")
|
|
67
|
+
# retrieve authorization URL
|
|
68
|
+
authorization_url = self.authorization_url(url='https://identity.vwgroup.io/oidc/v1/authorize', prompt='login', code_challenge=code_challenge,
|
|
69
|
+
code_challenge_method='s256')
|
|
70
|
+
# perform web authentication
|
|
71
|
+
response = self.do_web_auth(authorization_url)
|
|
72
|
+
# fetch tokens from web authentication response
|
|
73
|
+
self.fetch_tokens('https://mysmob.api.connect.skoda-auto.cz/api/v1/authentication/exchange-authorization-code?tokenType=CONNECT',
|
|
74
|
+
authorization_response=response, verifier=verifier)
|
|
75
|
+
except ReadTimeout as exc:
|
|
76
|
+
raise TemporaryAuthenticationError('Login timed out (Read timeout)') from exc
|
|
77
|
+
except ConnectionError as exc:
|
|
78
|
+
raise TemporaryAuthenticationError('Login failed due to connection error') from exc
|
|
79
|
+
except NameResolutionError as exc:
|
|
80
|
+
raise TemporaryAuthenticationError('Token could not be refreshed due to Name resolution error, probably no internet connection') from exc
|
|
72
81
|
|
|
73
82
|
def refresh(self) -> None:
|
|
74
83
|
# refresh tokens from refresh endpoint
|
|
@@ -118,7 +127,7 @@ class MySkodaSession(SkodaWebSession):
|
|
|
118
127
|
token_response = self.post(token_url, headers=request_headers, data=body, allow_redirects=False,
|
|
119
128
|
access_type=AccessType.NONE) # pyright: ignore reportCallIssue
|
|
120
129
|
if token_response.status_code != requests.codes['ok']:
|
|
121
|
-
raise TemporaryAuthenticationError(f'Token could not be fetched due to temporary
|
|
130
|
+
raise TemporaryAuthenticationError(f'Token could not be fetched due to temporary MySkoda failure: {token_response.status_code}')
|
|
122
131
|
# parse token from response body
|
|
123
132
|
token = self.parse_from_body(token_response.text)
|
|
124
133
|
return token
|
|
@@ -132,7 +141,7 @@ class MySkodaSession(SkodaWebSession):
|
|
|
132
141
|
# Tokens are in body of response in json format
|
|
133
142
|
token = json.loads(token_response)
|
|
134
143
|
except json.decoder.JSONDecodeError as err:
|
|
135
|
-
raise TemporaryAuthenticationError('Token could not be refreshed due to temporary
|
|
144
|
+
raise TemporaryAuthenticationError('Token could not be refreshed due to temporary MySkoda failure: json could not be decoded') from err
|
|
136
145
|
found_tokens: Set[str] = set()
|
|
137
146
|
# Fix token keys, we want access_token instead of accessToken
|
|
138
147
|
if 'accessToken' in token:
|
|
@@ -222,3 +231,5 @@ class MySkodaSession(SkodaWebSession):
|
|
|
222
231
|
except ConnectionError:
|
|
223
232
|
self.login()
|
|
224
233
|
return self.token
|
|
234
|
+
except NameResolutionError as exc:
|
|
235
|
+
raise TemporaryAuthenticationError('Token could not be refreshed due to Name resolution error, probably no internet connection') from exc
|
|
@@ -149,22 +149,24 @@ class OpenIDSession(requests.Session):
|
|
|
149
149
|
if new_token is not None:
|
|
150
150
|
# If new token e.g. after refresh is missing expires_in we assume it is the same than before
|
|
151
151
|
if 'expires_in' not in new_token:
|
|
152
|
-
if
|
|
153
|
-
new_token['
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
else:
|
|
162
|
-
new_token['expires_in'] = 3600
|
|
152
|
+
if 'id_token' in new_token:
|
|
153
|
+
meta_data = jwt.decode(new_token['id_token'], options={"verify_signature": False})
|
|
154
|
+
if 'exp' in meta_data:
|
|
155
|
+
new_token['expires_at'] = meta_data['exp']
|
|
156
|
+
expires_at = datetime.fromtimestamp(meta_data['exp'], tz=timezone.utc)
|
|
157
|
+
new_token['expires_in'] = (expires_at - datetime.now(tz=timezone.utc)).total_seconds()
|
|
158
|
+
if 'expires_in' not in new_token:
|
|
159
|
+
if self._token is not None and 'expires_in' in self._token:
|
|
160
|
+
new_token['expires_in'] = self._token['expires_in']
|
|
163
161
|
else:
|
|
164
162
|
new_token['expires_in'] = 3600
|
|
165
163
|
# If expires_in is set and expires_at is not set we calculate expires_at from expires_in using the current time
|
|
166
164
|
if 'expires_in' in new_token and 'expires_at' not in new_token:
|
|
167
165
|
new_token['expires_at'] = time.time() + int(new_token.get('expires_in'))
|
|
166
|
+
if new_token['expires_in'] > 3600:
|
|
167
|
+
LOG.warning('unexpected Token expires_in > 3600s (%d)', new_token['expires_in'])
|
|
168
|
+
if new_token['expires_at'] > (time.time() + 3600):
|
|
169
|
+
LOG.warning('unexpected Token expires_at after more than 3600s')
|
|
168
170
|
self._token = new_token
|
|
169
171
|
|
|
170
172
|
@property
|
|
@@ -407,7 +407,7 @@ class Connector(BaseConnector):
|
|
|
407
407
|
if vehicle is not None:
|
|
408
408
|
if vehicle.connection_state is not None and vehicle.connection_state.enabled \
|
|
409
409
|
and vehicle.connection_state.value == GenericVehicle.ConnectionState.OFFLINE:
|
|
410
|
-
vehicle.state._set_value(GenericVehicle.State.OFFLINE)
|
|
410
|
+
vehicle.state._set_value(GenericVehicle.State.OFFLINE) # pylint: disable=protected-access
|
|
411
411
|
elif vehicle.in_motion is not None and vehicle.in_motion.enabled and vehicle.in_motion.value:
|
|
412
412
|
vehicle.state._set_value(GenericVehicle.State.IGNITION_ON) # pylint: disable=protected-access
|
|
413
413
|
elif vehicle.position is not None and vehicle.position.enabled and vehicle.position.position_type is not None \
|
|
@@ -13,7 +13,7 @@ from datetime import timedelta, timezone
|
|
|
13
13
|
from paho.mqtt.client import Client
|
|
14
14
|
from paho.mqtt.enums import MQTTProtocolVersion, CallbackAPIVersion, MQTTErrorCode
|
|
15
15
|
|
|
16
|
-
from carconnectivity.errors import CarConnectivityError
|
|
16
|
+
from carconnectivity.errors import CarConnectivityError, TemporaryAuthenticationError
|
|
17
17
|
from carconnectivity.observable import Observable
|
|
18
18
|
from carconnectivity.vehicle import GenericVehicle
|
|
19
19
|
|
|
@@ -70,6 +70,8 @@ class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
|
|
|
70
70
|
self.delayed_access_function_timers: Dict[str, threading.Timer] = {}
|
|
71
71
|
|
|
72
72
|
self.tls_set(cert_reqs=ssl.CERT_NONE)
|
|
73
|
+
|
|
74
|
+
self._retry_refresh_login_once = True
|
|
73
75
|
|
|
74
76
|
def connect(self, *args, **kwargs) -> MQTTErrorCode:
|
|
75
77
|
"""
|
|
@@ -98,7 +100,12 @@ class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
|
|
|
98
100
|
del userdata
|
|
99
101
|
|
|
100
102
|
if self._skoda_connector.session.expired or self._skoda_connector.session.access_token is None:
|
|
101
|
-
|
|
103
|
+
try:
|
|
104
|
+
self._skoda_connector.session.refresh()
|
|
105
|
+
except ConnectionError as exc:
|
|
106
|
+
LOG.error('Token refresh failed due to connection error: %s', exc)
|
|
107
|
+
except TemporaryAuthenticationError as exc:
|
|
108
|
+
LOG.error('Token refresh failed due to temporary MySkoda error: %s', exc)
|
|
102
109
|
if not self._skoda_connector.session.expired and self._skoda_connector.session.access_token is not None:
|
|
103
110
|
# pylint: disable-next=attribute-defined-outside-init # this is a false positive, password has a setter in super class
|
|
104
111
|
self._password = self._skoda_connector.session.access_token # This is a bit hacky but if password attribute is used here there is an Exception
|
|
@@ -321,6 +328,7 @@ class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
|
|
|
321
328
|
self._skoda_connector.car_connectivity.garage.add_observer(observer=self._on_carconnectivity_vehicle_enabled,
|
|
322
329
|
flag=observer_flags,
|
|
323
330
|
priority=Observable.ObserverPriority.USER_MID)
|
|
331
|
+
self._retry_refresh_login_once = True
|
|
324
332
|
self._subscribe_vehicles()
|
|
325
333
|
|
|
326
334
|
# Handle different reason codes
|
|
@@ -338,6 +346,15 @@ class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
|
|
|
338
346
|
LOG.error('Could not connect (%s): Client identifier not valid', reason_code)
|
|
339
347
|
elif reason_code == 134:
|
|
340
348
|
LOG.error('Could not connect (%s): Bad user name or password', reason_code)
|
|
349
|
+
if self._retry_refresh_login_once == True:
|
|
350
|
+
self._retry_refresh_login_once = False
|
|
351
|
+
LOG.info('trying a relogin once to resolve the error')
|
|
352
|
+
try:
|
|
353
|
+
self._skoda_connector.session.login()
|
|
354
|
+
except TemporaryAuthenticationError as exc:
|
|
355
|
+
LOG.error('Login failed due to temporary MySkoda error: %s', exc)
|
|
356
|
+
except ConnectionError as exc:
|
|
357
|
+
LOG.error('Login failed due to connection error: %s', exc)
|
|
341
358
|
elif reason_code == 135:
|
|
342
359
|
LOG.error('Could not connect (%s): Not authorized', reason_code)
|
|
343
360
|
elif reason_code == 136:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|