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.
@@ -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.7.1
41
+ Requires-Dist: carconnectivity>=0.8.1
41
42
  Requires-Dist: oauthlib~=3.3.1
42
- Requires-Dist: requests~=2.32.3
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=i6raU8IDbjX2_qal-So090T9KiZO76P0n85Br_T16qg,506
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=Nj1wkA5SI70Ivvh_UVvTxQVSu2oE9xpk3UqxJfwf1gQ,147265
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=D9_e_Bz842ULYKlRWd4JmosalhTtYr9DtCuvWrT3WQw,39126
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=lSh23SFJs8opjmPwHTv-KNIKDep_WY4aItSP4Zq7bT8,10396
15
- carconnectivity_connectors/skoda/auth/openid_session.py,sha256=jlRBN6A1Z-U_YrgdD-X1itRG-aelnMGPQe_dg_XOmCs,16789
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=vX69lDP8qJUdrDCTtkAicZbgbN3c_JAHkw_XT85O5p8,5381
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__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
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
- verifier = "".join(random.choices(string.ascii_uppercase + string.digits, k=16))
62
- verifier_hash = hashlib.sha256(verifier.encode("utf-8")).digest()
63
- code_challenge = base64.b64encode(verifier_hash).decode("utf-8").replace("+", "-").replace("/", "_").rstrip("=")
64
- # retrieve authorization URL
65
- authorization_url = self.authorization_url(url='https://identity.vwgroup.io/oidc/v1/authorize', prompt='login', code_challenge=code_challenge,
66
- code_challenge_method='s256')
67
- # perform web authentication
68
- response = self.do_web_auth(authorization_url)
69
- # fetch tokens from web authentication response
70
- self.fetch_tokens('https://mysmob.api.connect.skoda-auto.cz/api/v1/authentication/exchange-authorization-code?tokenType=CONNECT',
71
- authorization_response=response, verifier=verifier)
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 WeConnect failure: {token_response.status_code}')
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 WeConnect failure: json could not be decoded') from err
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 self._token is not None and 'expires_in' in self._token:
153
- new_token['expires_in'] = self._token['expires_in']
154
- else:
155
- if 'id_token' in new_token:
156
- meta_data = jwt.decode(new_token['id_token'], options={"verify_signature": False})
157
- if 'exp' in meta_data:
158
- new_token['expires_at'] = meta_data['exp']
159
- expires_at = datetime.fromtimestamp(meta_data['exp'], tz=timezone.utc)
160
- new_token['expires_in'] = (expires_at - datetime.now(tz=timezone.utc)).total_seconds()
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
- self._skoda_connector.session.refresh()
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: