carconnectivity-connector-skoda 0.6__tar.gz → 0.8.1__tar.gz

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 carconnectivity-connector-skoda might be problematic. Click here for more details.

Files changed (43) hide show
  1. {carconnectivity_connector_skoda-0.6 → carconnectivity_connector_skoda-0.8.1}/.github/workflows/build.yml +3 -3
  2. {carconnectivity_connector_skoda-0.6 → carconnectivity_connector_skoda-0.8.1}/.github/workflows/build_and_publish.yml +2 -2
  3. {carconnectivity_connector_skoda-0.6 → carconnectivity_connector_skoda-0.8.1}/.github/workflows/codeql-analysis.yml +4 -4
  4. {carconnectivity_connector_skoda-0.6 → carconnectivity_connector_skoda-0.8.1}/CHANGELOG.md +47 -1
  5. {carconnectivity_connector_skoda-0.6 → carconnectivity_connector_skoda-0.8.1}/PKG-INFO +6 -5
  6. {carconnectivity_connector_skoda-0.6 → carconnectivity_connector_skoda-0.8.1}/doc/Config.md +2 -1
  7. {carconnectivity_connector_skoda-0.6 → carconnectivity_connector_skoda-0.8.1}/pyproject.toml +6 -5
  8. carconnectivity_connector_skoda-0.8.1/setup_requirements.txt +6 -0
  9. {carconnectivity_connector_skoda-0.6 → carconnectivity_connector_skoda-0.8.1}/src/carconnectivity_connector_skoda.egg-info/PKG-INFO +6 -5
  10. carconnectivity_connector_skoda-0.8.1/src/carconnectivity_connector_skoda.egg-info/requires.txt +5 -0
  11. {carconnectivity_connector_skoda-0.6 → carconnectivity_connector_skoda-0.8.1}/src/carconnectivity_connectors/skoda/_version.py +16 -3
  12. {carconnectivity_connector_skoda-0.6 → carconnectivity_connector_skoda-0.8.1}/src/carconnectivity_connectors/skoda/auth/my_skoda_session.py +19 -13
  13. {carconnectivity_connector_skoda-0.6 → carconnectivity_connector_skoda-0.8.1}/src/carconnectivity_connectors/skoda/auth/openid_session.py +6 -3
  14. {carconnectivity_connector_skoda-0.6 → carconnectivity_connector_skoda-0.8.1}/src/carconnectivity_connectors/skoda/capability.py +1 -1
  15. {carconnectivity_connector_skoda-0.6 → carconnectivity_connector_skoda-0.8.1}/src/carconnectivity_connectors/skoda/command_impl.py +1 -1
  16. {carconnectivity_connector_skoda-0.6 → carconnectivity_connector_skoda-0.8.1}/src/carconnectivity_connectors/skoda/connector.py +33 -6
  17. {carconnectivity_connector_skoda-0.6 → carconnectivity_connector_skoda-0.8.1}/src/carconnectivity_connectors/skoda/mqtt_client.py +20 -3
  18. carconnectivity_connector_skoda-0.6/setup_requirements.txt +0 -6
  19. carconnectivity_connector_skoda-0.6/src/carconnectivity_connector_skoda.egg-info/requires.txt +0 -5
  20. {carconnectivity_connector_skoda-0.6 → carconnectivity_connector_skoda-0.8.1}/.flake8 +0 -0
  21. {carconnectivity_connector_skoda-0.6 → carconnectivity_connector_skoda-0.8.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  22. {carconnectivity_connector_skoda-0.6 → carconnectivity_connector_skoda-0.8.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  23. {carconnectivity_connector_skoda-0.6 → carconnectivity_connector_skoda-0.8.1}/.github/dependabot.yml +0 -0
  24. {carconnectivity_connector_skoda-0.6 → carconnectivity_connector_skoda-0.8.1}/.gitignore +0 -0
  25. {carconnectivity_connector_skoda-0.6 → carconnectivity_connector_skoda-0.8.1}/LICENSE +0 -0
  26. {carconnectivity_connector_skoda-0.6 → carconnectivity_connector_skoda-0.8.1}/Makefile +0 -0
  27. {carconnectivity_connector_skoda-0.6 → carconnectivity_connector_skoda-0.8.1}/README.md +0 -0
  28. {carconnectivity_connector_skoda-0.6 → carconnectivity_connector_skoda-0.8.1}/setup.cfg +0 -0
  29. {carconnectivity_connector_skoda-0.6 → carconnectivity_connector_skoda-0.8.1}/src/carconnectivity_connector_skoda.egg-info/SOURCES.txt +0 -0
  30. {carconnectivity_connector_skoda-0.6 → carconnectivity_connector_skoda-0.8.1}/src/carconnectivity_connector_skoda.egg-info/dependency_links.txt +0 -0
  31. {carconnectivity_connector_skoda-0.6 → carconnectivity_connector_skoda-0.8.1}/src/carconnectivity_connector_skoda.egg-info/top_level.txt +0 -0
  32. {carconnectivity_connector_skoda-0.6 → carconnectivity_connector_skoda-0.8.1}/src/carconnectivity_connectors/skoda/__init__.py +0 -0
  33. {carconnectivity_connector_skoda-0.6 → carconnectivity_connector_skoda-0.8.1}/src/carconnectivity_connectors/skoda/auth/__init__.py +0 -0
  34. {carconnectivity_connector_skoda-0.6 → carconnectivity_connector_skoda-0.8.1}/src/carconnectivity_connectors/skoda/auth/auth_util.py +0 -0
  35. {carconnectivity_connector_skoda-0.6 → carconnectivity_connector_skoda-0.8.1}/src/carconnectivity_connectors/skoda/auth/helpers/blacklist_retry.py +0 -0
  36. {carconnectivity_connector_skoda-0.6 → carconnectivity_connector_skoda-0.8.1}/src/carconnectivity_connectors/skoda/auth/session_manager.py +0 -0
  37. {carconnectivity_connector_skoda-0.6 → carconnectivity_connector_skoda-0.8.1}/src/carconnectivity_connectors/skoda/auth/skoda_web_session.py +0 -0
  38. {carconnectivity_connector_skoda-0.6 → carconnectivity_connector_skoda-0.8.1}/src/carconnectivity_connectors/skoda/charging.py +0 -0
  39. {carconnectivity_connector_skoda-0.6 → carconnectivity_connector_skoda-0.8.1}/src/carconnectivity_connectors/skoda/climatization.py +0 -0
  40. {carconnectivity_connector_skoda-0.6 → carconnectivity_connector_skoda-0.8.1}/src/carconnectivity_connectors/skoda/error.py +0 -0
  41. {carconnectivity_connector_skoda-0.6 → carconnectivity_connector_skoda-0.8.1}/src/carconnectivity_connectors/skoda/ui/connector_ui.py +0 -0
  42. {carconnectivity_connector_skoda-0.6 → carconnectivity_connector_skoda-0.8.1}/src/carconnectivity_connectors/skoda/vehicle.py +0 -0
  43. {carconnectivity_connector_skoda-0.6 → carconnectivity_connector_skoda-0.8.1}/test/integration_test/carConnectivity.json +0 -0
@@ -22,12 +22,12 @@ jobs:
22
22
  runs-on: ubuntu-latest
23
23
  strategy:
24
24
  matrix:
25
- python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
25
+ python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
26
26
 
27
27
  steps:
28
- - uses: actions/checkout@v4
28
+ - uses: actions/checkout@v5
29
29
  - name: Set up Python ${{ matrix.python-version }}
30
- uses: actions/setup-python@v5
30
+ uses: actions/setup-python@v6
31
31
  with:
32
32
  python-version: ${{ matrix.python-version }}
33
33
  - name: Install dependencies
@@ -15,11 +15,11 @@ jobs:
15
15
  id-token: write
16
16
 
17
17
  steps:
18
- - uses: actions/checkout@v4
18
+ - uses: actions/checkout@v5
19
19
  with:
20
20
  fetch-depth: 0
21
21
  - name: Set up Python
22
- uses: actions/setup-python@v5
22
+ uses: actions/setup-python@v6
23
23
  with:
24
24
  python-version: "3.x"
25
25
  - name: Install dependencies
@@ -26,11 +26,11 @@ jobs:
26
26
 
27
27
  steps:
28
28
  - name: Checkout repository
29
- uses: actions/checkout@v4
29
+ uses: actions/checkout@v5
30
30
 
31
31
  # Initializes the CodeQL tools for scanning.
32
32
  - name: Initialize CodeQL
33
- uses: github/codeql-action/init@v3
33
+ uses: github/codeql-action/init@v4
34
34
  with:
35
35
  languages: ${{ matrix.language }}
36
36
  # If you wish to specify custom queries, you can do so here or in a config file.
@@ -41,7 +41,7 @@ jobs:
41
41
  # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
42
42
  # If this step fails, then you should remove it and run the build manually (see below)
43
43
  - name: Autobuild
44
- uses: github/codeql-action/autobuild@v3
44
+ uses: github/codeql-action/autobuild@v4
45
45
 
46
46
  # ℹ️ Command-line programs to run using the OS shell.
47
47
  # 📚 https://git.io/JvXDl
@@ -55,4 +55,4 @@ jobs:
55
55
  # make release
56
56
 
57
57
  - name: Perform CodeQL Analysis
58
- uses: github/codeql-action/analyze@v3
58
+ uses: github/codeql-action/analyze@v4
@@ -5,6 +5,46 @@ All notable changes to this project will be documented in this file.
5
5
  ## [Unreleased]
6
6
  - No unreleased changes so far
7
7
 
8
+ ## [0.8.1] - 2025-11-02
9
+ ### Fixed
10
+ - Relogin when MQTT client receives a bad username or password error
11
+
12
+ ### Added
13
+ - Warning when connector detects suspicious token expiry
14
+
15
+ ### Changed
16
+ - Updated some dependencies
17
+
18
+ ## [0.8] - 2025-06-27
19
+ ### Added
20
+ - Support for adblue range
21
+
22
+ ## [0.7.3] - 2025-06-20
23
+ ### Fixed
24
+ - Fixes bug that registers hooks several times, causing multiple calls to the servers
25
+
26
+ ### Changed
27
+ - Updated dependencies
28
+
29
+ ## [0.7.2] - 2025-04-19
30
+ ### Fixed
31
+ - Fix for problems introduced with PyJWT
32
+
33
+ ## [0.7.1] - 2025-04-18
34
+ ### Fixed
35
+ - Fixed problem where driving range was not fetched when measurements capability had an expired license. Now it will be also fetched when charging license is active.
36
+
37
+ ## [0.7] - 2025-04-17
38
+ ### Fixed
39
+ - Bug in mode attribute that caused the connector to crash
40
+
41
+ ### Changed
42
+ - Updated dependencies
43
+ - stripping of leading and trailing spaces in commands
44
+
45
+ ### Added
46
+ - Precision for all attributes is now used when displaying values
47
+
8
48
  ## [0.6] - 2025-04-02
9
49
  ### Fixed
10
50
  - Only fetch range when measurement capability is available (fix for older cars)
@@ -51,7 +91,13 @@ All notable changes to this project will be documented in this file.
51
91
  Initial release, let's go and give this to the public to try out...
52
92
  The API is not yet implemented completely but most functions already work
53
93
 
54
- [unreleased]: https://github.com/tillsteinbach/CarConnectivity-connector-skoda/compare/v0.6...HEAD
94
+ [unreleased]: https://github.com/tillsteinbach/CarConnectivity-connector-skoda/compare/v0.8.1...HEAD
95
+ [0.8.1]: https://github.com/tillsteinbach/CarConnectivity-connector-skoda/releases/tag/v0.8.1
96
+ [0.8]: https://github.com/tillsteinbach/CarConnectivity-connector-skoda/releases/tag/v0.8
97
+ [0.7.3]: https://github.com/tillsteinbach/CarConnectivity-connector-skoda/releases/tag/v0.7.3
98
+ [0.7.2]: https://github.com/tillsteinbach/CarConnectivity-connector-skoda/releases/tag/v0.7.2
99
+ [0.7.1]: https://github.com/tillsteinbach/CarConnectivity-connector-skoda/releases/tag/v0.7.1
100
+ [0.7]: https://github.com/tillsteinbach/CarConnectivity-connector-skoda/releases/tag/v0.7
55
101
  [0.6]: https://github.com/tillsteinbach/CarConnectivity-connector-skoda/releases/tag/v0.6
56
102
  [0.5]: https://github.com/tillsteinbach/CarConnectivity-connector-skoda/releases/tag/v0.5
57
103
  [0.4.1]: https://github.com/tillsteinbach/CarConnectivity-connector-skoda/releases/tag/v0.4.1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: carconnectivity-connector-skoda
3
- Version: 0.6
3
+ Version: 0.8.1
4
4
  Summary: CarConnectivity connector for Skoda services
5
5
  Author: Till Steinbach
6
6
  License: MIT License
@@ -33,14 +33,15 @@ 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.6
41
- Requires-Dist: oauthlib~=3.2.2
42
- Requires-Dist: requests~=2.32.3
43
- Requires-Dist: jwt~=1.3.1
41
+ Requires-Dist: carconnectivity>=0.8.1
42
+ Requires-Dist: oauthlib~=3.3.1
43
+ Requires-Dist: requests~=2.32.5
44
+ Requires-Dist: pyjwt~=2.10
44
45
  Requires-Dist: paho-mqtt~=2.1.0
45
46
  Dynamic: license-file
46
47
 
@@ -19,7 +19,8 @@ These are the valid options for the MySkoda Connector
19
19
  "spin": 1234, //S-Pin used for some special commands like locking/unlocking
20
20
  "netrc": "~/.netr", // netrc file if to be used for passwords
21
21
  "api_log_level": "debug", // Show debug information regarding the API
22
- "max_age": 300 //Cache requests to the server vor MAX_AGE seconds
22
+ "max_age": 300, //Cache requests to the server vor MAX_AGE seconds
23
+ "hide_vins": ["19XFB2F90CE040211", "1G2ZH35N074252067"] // Don't fetch these vins
23
24
  }
24
25
  }
25
26
  ],
@@ -14,10 +14,10 @@ authors = [
14
14
  { name = "Till Steinbach" }
15
15
  ]
16
16
  dependencies = [
17
- "carconnectivity>=0.6",
18
- "oauthlib~=3.2.2",
19
- "requests~=2.32.3",
20
- "jwt~=1.3.1",
17
+ "carconnectivity>=0.8.1",
18
+ "oauthlib~=3.3.1",
19
+ "requests~=2.32.5",
20
+ "pyjwt~=2.10",
21
21
  "paho-mqtt~=2.1.0",
22
22
  ]
23
23
  readme = "README.md"
@@ -31,6 +31,7 @@ classifiers = [
31
31
  "Programming Language :: Python :: 3.11",
32
32
  "Programming Language :: Python :: 3.12",
33
33
  "Programming Language :: Python :: 3.13",
34
+ "Programming Language :: Python :: 3.14",
34
35
  "Topic :: Software Development :: Libraries"
35
36
  ]
36
37
 
@@ -44,4 +45,4 @@ max-line-length = 160
44
45
  ignore-patterns= "_version.py"
45
46
 
46
47
  [tool.bandit]
47
- targets = "carconnectivity_cli"
48
+ targets = "carconnectivity_cli"
@@ -0,0 +1,6 @@
1
+ flake8~=7.3.0
2
+ pylint~=4.0.2
3
+ bandit~=1.8.6
4
+
5
+ # For UI only
6
+ Flask~=3.1.2
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: carconnectivity-connector-skoda
3
- Version: 0.6
3
+ Version: 0.8.1
4
4
  Summary: CarConnectivity connector for Skoda services
5
5
  Author: Till Steinbach
6
6
  License: MIT License
@@ -33,14 +33,15 @@ 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.6
41
- Requires-Dist: oauthlib~=3.2.2
42
- Requires-Dist: requests~=2.32.3
43
- Requires-Dist: jwt~=1.3.1
41
+ Requires-Dist: carconnectivity>=0.8.1
42
+ Requires-Dist: oauthlib~=3.3.1
43
+ Requires-Dist: requests~=2.32.5
44
+ Requires-Dist: pyjwt~=2.10
44
45
  Requires-Dist: paho-mqtt~=2.1.0
45
46
  Dynamic: license-file
46
47
 
@@ -0,0 +1,5 @@
1
+ carconnectivity>=0.8.1
2
+ oauthlib~=3.3.1
3
+ requests~=2.32.5
4
+ pyjwt~=2.10
5
+ paho-mqtt~=2.1.0
@@ -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.6'
21
- __version_tuple__ = version_tuple = (0, 6)
31
+ __version__ = version = '0.8.1'
32
+ __version_tuple__ = version_tuple = (0, 8, 1)
33
+
34
+ __commit_id__ = commit_id = 'g5123c7f6f'
@@ -15,6 +15,7 @@ from urllib.parse import parse_qsl, urlparse
15
15
 
16
16
  import requests
17
17
  from requests.models import CaseInsensitiveDict
18
+ from requests.exceptions import ReadTimeout, ConnectionError
18
19
 
19
20
  from oauthlib.common import add_params_to_uri, generate_nonce, to_unicode
20
21
  from oauthlib.oauth2 import InsecureTransportError
@@ -58,17 +59,22 @@ class MySkodaSession(SkodaWebSession):
58
59
  def login(self):
59
60
  super(MySkodaSession, self).login()
60
61
 
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)
62
+ try:
63
+ verifier = "".join(random.choices(string.ascii_uppercase + string.digits, k=16))
64
+ verifier_hash = hashlib.sha256(verifier.encode("utf-8")).digest()
65
+ code_challenge = base64.b64encode(verifier_hash).decode("utf-8").replace("+", "-").replace("/", "_").rstrip("=")
66
+ # retrieve authorization URL
67
+ authorization_url = self.authorization_url(url='https://identity.vwgroup.io/oidc/v1/authorize', prompt='login', code_challenge=code_challenge,
68
+ code_challenge_method='s256')
69
+ # perform web authentication
70
+ response = self.do_web_auth(authorization_url)
71
+ # fetch tokens from web authentication response
72
+ self.fetch_tokens('https://mysmob.api.connect.skoda-auto.cz/api/v1/authentication/exchange-authorization-code?tokenType=CONNECT',
73
+ authorization_response=response, verifier=verifier)
74
+ except ReadTimeout as exc:
75
+ raise TemporaryAuthenticationError('Login timed out (Read timeout)') from exc
76
+ except ConnectionError as exc:
77
+ raise TemporaryAuthenticationError('Login failed due to connection error') from exc
72
78
 
73
79
  def refresh(self) -> None:
74
80
  # refresh tokens from refresh endpoint
@@ -118,7 +124,7 @@ class MySkodaSession(SkodaWebSession):
118
124
  token_response = self.post(token_url, headers=request_headers, data=body, allow_redirects=False,
119
125
  access_type=AccessType.NONE) # pyright: ignore reportCallIssue
120
126
  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}')
127
+ raise TemporaryAuthenticationError(f'Token could not be fetched due to temporary MySkoda failure: {token_response.status_code}')
122
128
  # parse token from response body
123
129
  token = self.parse_from_body(token_response.text)
124
130
  return token
@@ -132,7 +138,7 @@ class MySkodaSession(SkodaWebSession):
132
138
  # Tokens are in body of response in json format
133
139
  token = json.loads(token_response)
134
140
  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
141
+ raise TemporaryAuthenticationError('Token could not be refreshed due to temporary MySkoda failure: json could not be decoded') from err
136
142
  found_tokens: Set[str] = set()
137
143
  # Fix token keys, we want access_token instead of accessToken
138
144
  if 'accessToken' in token:
@@ -7,7 +7,7 @@ import time
7
7
  from datetime import datetime, timezone
8
8
  import logging
9
9
  import requests
10
- from jwt import JWT
10
+ import jwt
11
11
 
12
12
  from oauthlib.common import UNICODE_ASCII_CHARACTER_SET, generate_nonce, generate_token
13
13
  from oauthlib.oauth2.rfc6749.parameters import parse_authorization_code_response, parse_token_response, prepare_grant_uri
@@ -153,8 +153,7 @@ class OpenIDSession(requests.Session):
153
153
  new_token['expires_in'] = self._token['expires_in']
154
154
  else:
155
155
  if 'id_token' in new_token:
156
- jwt_instance = JWT()
157
- meta_data = jwt_instance.decode(new_token['id_token'], do_verify=False)
156
+ meta_data = jwt.decode(new_token['id_token'], options={"verify_signature": False})
158
157
  if 'exp' in meta_data:
159
158
  new_token['expires_at'] = meta_data['exp']
160
159
  expires_at = datetime.fromtimestamp(meta_data['exp'], tz=timezone.utc)
@@ -166,6 +165,10 @@ class OpenIDSession(requests.Session):
166
165
  # If expires_in is set and expires_at is not set we calculate expires_at from expires_in using the current time
167
166
  if 'expires_in' in new_token and 'expires_at' not in new_token:
168
167
  new_token['expires_at'] = time.time() + int(new_token.get('expires_in'))
168
+ if new_token['expires_in'] > 3600:
169
+ LOG.warning('unexpected Token expires_in > 3600s (%d)', new_token['expires_in'])
170
+ if new_token['expires_at'] > (time.time() + 3600):
171
+ LOG.warning('unexpected Token expires_at after more than 3600s')
169
172
  self._token = new_token
170
173
 
171
174
  @property
@@ -132,7 +132,7 @@ class Capability(GenericObject):
132
132
  LOCATION_DATA_DISABLED = 1013
133
133
  LICENSE_INACTIVE = 2001
134
134
  LICENSE_EXPIRED = 2002
135
- MISSING_LICENSE = 2003
135
+ LICENSE_MISSING = 2003
136
136
  USER_NOT_VERIFIED = 3001
137
137
  TERMS_AND_CONDITIONS_NOT_ACCEPTED = 3002
138
138
  INSUFFICIENT_RIGHTS = 3003
@@ -44,7 +44,7 @@ class SpinCommand(GenericCommand):
44
44
  parser.add_argument('--spin', dest='spin', help='Spin to be used instead of spin from config or .netrc', type=str, required=False,
45
45
  default=None)
46
46
  try:
47
- args = parser.parse_args(new_value.split(sep=' '))
47
+ args = parser.parse_args(new_value.strip().split(sep=' '))
48
48
  except argparse.ArgumentError as e:
49
49
  raise SetterError(f'Invalid format for SpinCommand: {e.message} {parser.format_usage()}') from e
50
50
 
@@ -22,7 +22,7 @@ from carconnectivity.units import Length, Speed, Power, Temperature
22
22
  from carconnectivity.doors import Doors
23
23
  from carconnectivity.windows import Windows
24
24
  from carconnectivity.lights import Lights
25
- from carconnectivity.drive import GenericDrive, ElectricDrive, CombustionDrive
25
+ from carconnectivity.drive import GenericDrive, ElectricDrive, CombustionDrive, DieselDrive
26
26
  from carconnectivity.attributes import BooleanAttribute, DurationAttribute, TemperatureAttribute, EnumAttribute, LevelAttribute, \
27
27
  CurrentAttribute
28
28
  from carconnectivity.charging import Charging
@@ -378,7 +378,8 @@ class Connector(BaseConnector):
378
378
  if vehicle_to_update is not None and isinstance(vehicle_to_update, SkodaVehicle) and vehicle_to_update.is_managed_by_connector(self):
379
379
  vehicle_to_update = self.fetch_vehicle_status(vehicle_to_update)
380
380
  if vehicle_to_update.capabilities is not None and vehicle_to_update.capabilities.enabled:
381
- if vehicle_to_update.capabilities.has_capability('MEASUREMENTS', check_status_ok=True):
381
+ if vehicle_to_update.capabilities.has_capability('MEASUREMENTS', check_status_ok=True) or \
382
+ vehicle_to_update.capabilities.has_capability('CHARGING', check_status_ok=True):
382
383
  vehicle_to_update = self.fetch_driving_range(vehicle_to_update)
383
384
  if vehicle_to_update.capabilities.has_capability('READINESS', check_status_ok=True):
384
385
  vehicle_to_update = self.fetch_connection_status(vehicle_to_update)
@@ -404,13 +405,16 @@ class Connector(BaseConnector):
404
405
  SkodaVehicle: The Skoda vehicle object with the updated state.
405
406
  """
406
407
  if vehicle is not None:
407
- if vehicle.in_motion is not None and vehicle.in_motion.enabled and vehicle.in_motion.value:
408
+ if vehicle.connection_state is not None and vehicle.connection_state.enabled \
409
+ and vehicle.connection_state.value == GenericVehicle.ConnectionState.OFFLINE:
410
+ vehicle.state._set_value(GenericVehicle.State.OFFLINE) # pylint: disable=protected-access
411
+ elif vehicle.in_motion is not None and vehicle.in_motion.enabled and vehicle.in_motion.value:
408
412
  vehicle.state._set_value(GenericVehicle.State.IGNITION_ON) # pylint: disable=protected-access
409
413
  elif vehicle.position is not None and vehicle.position.enabled and vehicle.position.position_type is not None \
410
414
  and vehicle.position.position_type.enabled and vehicle.position.position_type.value == Position.PositionType.PARKING:
411
415
  vehicle.state._set_value(GenericVehicle.State.PARKED) # pylint: disable=protected-access
412
416
  else:
413
- vehicle.state._set_value(None) # pylint: disable=protected-access
417
+ vehicle.state._set_value(GenericVehicle.State.UNKNOWN) # pylint: disable=protected-access
414
418
  return vehicle
415
419
 
416
420
  def fetch_charging(self, vehicle: SkodaElectricVehicle, no_cache: bool = False) -> SkodaElectricVehicle:
@@ -498,10 +502,12 @@ class Connector(BaseConnector):
498
502
  cruising_range_in_km: float = data['status']['battery']['remainingCruisingRangeInMeters'] / 1000
499
503
  # pylint: disable-next=protected-access
500
504
  drive.range._set_value(value=cruising_range_in_km, measured=captured_at, unit=Length.KM)
505
+ drive.range.precision = 1
501
506
  if 'stateOfChargeInPercent' in data['status']['battery'] \
502
507
  and data['status']['battery']['stateOfChargeInPercent'] is not None:
503
508
  # pylint: disable-next=protected-access
504
509
  drive.level._set_value(value=data['status']['battery']['stateOfChargeInPercent'], measured=captured_at)
510
+ drive.level.precision = 1
505
511
  log_extra_keys(LOG_API, 'status', data['status']['battery'], {'remainingCruisingRangeInMeters',
506
512
  'stateOfChargeInPercent'})
507
513
  break
@@ -703,7 +709,9 @@ class Connector(BaseConnector):
703
709
  else:
704
710
  longitude = None
705
711
  vehicle.position.latitude._set_value(latitude) # pylint: disable=protected-access
712
+ vehicle.position.latitude.precision = 0.000001
706
713
  vehicle.position.longitude._set_value(longitude) # pylint: disable=protected-access
714
+ vehicle.position.longitude.precision = 0.000001
707
715
  vehicle.position.position_type._set_value(Position.PositionType.PARKING) # pylint: disable=protected-access
708
716
  else:
709
717
  vehicle.position.latitude._set_value(None) # pylint: disable=protected-access
@@ -756,6 +764,7 @@ class Connector(BaseConnector):
756
764
  raise APIError('Could not fetch maintenance, capturedAt missing')
757
765
  if 'mileageInKm' in data and data['mileageInKm'] is not None:
758
766
  vehicle.odometer._set_value(value=data['mileageInKm'], measured=captured_at, unit=Length.KM) # pylint: disable=protected-access
767
+ vehicle.odometer.precision = 1
759
768
  else:
760
769
  vehicle.odometer._set_value(None) # pylint: disable=protected-access
761
770
  if 'inspectionDueInDays' in data and data['inspectionDueInDays'] is not None:
@@ -1274,6 +1283,7 @@ class Connector(BaseConnector):
1274
1283
  if 'totalRangeInKm' in range_data and range_data['totalRangeInKm'] is not None:
1275
1284
  # pylint: disable-next=protected-access
1276
1285
  vehicle.drives.total_range._set_value(value=range_data['totalRangeInKm'], measured=captured_at, unit=Length.KM)
1286
+ vehicle.drives.total_range.precision = 1
1277
1287
  else:
1278
1288
  vehicle.drives.total_range._set_value(None, measured=captured_at, unit=Length.KM) # pylint: disable=protected-access
1279
1289
 
@@ -1291,10 +1301,11 @@ class Connector(BaseConnector):
1291
1301
  else:
1292
1302
  if engine_type == GenericDrive.Type.ELECTRIC:
1293
1303
  drive = ElectricDrive(drive_id=drive_id, drives=vehicle.drives)
1304
+ elif engine_type == GenericDrive.Type.DIESEL:
1305
+ drive = DieselDrive(drive_id=drive_id, drives=vehicle.drives)
1294
1306
  elif engine_type in [GenericDrive.Type.FUEL,
1295
1307
  GenericDrive.Type.GASOLINE,
1296
1308
  GenericDrive.Type.PETROL,
1297
- GenericDrive.Type.DIESEL,
1298
1309
  GenericDrive.Type.CNG,
1299
1310
  GenericDrive.Type.LPG]:
1300
1311
  drive = CombustionDrive(drive_id=drive_id, drives=vehicle.drives)
@@ -1306,15 +1317,18 @@ class Connector(BaseConnector):
1306
1317
  and range_data[f'{drive_id}EngineRange']['currentSoCInPercent'] is not None:
1307
1318
  # pylint: disable-next=protected-access
1308
1319
  drive.level._set_value(value=range_data[f'{drive_id}EngineRange']['currentSoCInPercent'], measured=captured_at)
1320
+ drive.level.precision = 1
1309
1321
  elif 'currentFuelLevelInPercent' in range_data[f'{drive_id}EngineRange'] \
1310
1322
  and range_data[f'{drive_id}EngineRange']['currentFuelLevelInPercent'] is not None:
1311
1323
  # pylint: disable-next=protected-access
1312
1324
  drive.level._set_value(value=range_data[f'{drive_id}EngineRange']['currentFuelLevelInPercent'], measured=captured_at)
1325
+ drive.level.precision = 1
1313
1326
  else:
1314
1327
  drive.level._set_value(None, measured=captured_at) # pylint: disable=protected-access
1315
1328
  if 'remainingRangeInKm' in range_data[f'{drive_id}EngineRange'] and range_data[f'{drive_id}EngineRange']['remainingRangeInKm'] is not None:
1316
1329
  # pylint: disable-next=protected-access
1317
1330
  drive.range._set_value(value=range_data[f'{drive_id}EngineRange']['remainingRangeInKm'], measured=captured_at, unit=Length.KM)
1331
+ drive.range.precision = 1
1318
1332
  else:
1319
1333
  drive.range._set_value(None, measured=captured_at, unit=Length.KM) # pylint: disable=protected-access
1320
1334
 
@@ -1322,9 +1336,22 @@ class Connector(BaseConnector):
1322
1336
  'currentSoCInPercent',
1323
1337
  'currentFuelLevelInPercent',
1324
1338
  'remainingRangeInKm'})
1339
+ if 'adBlueRange' in range_data and range_data['adBlueRange'] is not None:
1340
+ # pylint: disable-next=protected-access
1341
+ for drive in vehicle.drives.drives.values():
1342
+ if isinstance(drive, DieselDrive):
1343
+ # pylint: disable-next=protected-access
1344
+ drive.adblue_range._set_value(value=range_data['adBlueRange'], measured=captured_at, unit=Length.KM)
1345
+ drive.adblue_range.precision = 1
1346
+ else:
1347
+ for drive in vehicle.drives.drives.values():
1348
+ if isinstance(drive, DieselDrive):
1349
+ # pylint: disable-next=protected-access
1350
+ drive.adblue_range._set_value(value=None, measured=captured_at, unit=Length.KM)
1325
1351
  log_extra_keys(LOG_API, '/api/v2/vehicle-status/{vin}/driving-range', range_data, {'carCapturedTimestamp',
1326
1352
  'carType',
1327
1353
  'totalRangeInKm',
1354
+ 'adBlueRange',
1328
1355
  'primaryEngineRange',
1329
1356
  'secondaryEngineRange'})
1330
1357
  return vehicle
@@ -1468,7 +1495,7 @@ class Connector(BaseConnector):
1468
1495
  elif not allow_http_error or (allowed_errors is not None and status_response.status_code not in allowed_errors):
1469
1496
  raise RetrievalError(f'Could not fetch data even after re-authorization. Status Code was: {status_response.status_code}')
1470
1497
  elif not allow_http_error or (allowed_errors is not None and status_response.status_code not in allowed_errors):
1471
- raise RetrievalError(f'Could not fetch data. Status Code was: {status_response.status_code}')
1498
+ raise RetrievalError(f'Could not fetch data for {url}. Status Code was: {status_response.status_code}')
1472
1499
  except requests.exceptions.ConnectionError as connection_error:
1473
1500
  raise RetrievalError(f'Connection error: {connection_error}.'
1474
1501
  ' If this happens frequently, please check if other applications communicate with the Skoda server.') from connection_error
@@ -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:
@@ -480,7 +497,7 @@ class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
480
497
  old_charging_state: Optional[Charging.ChargingState] = charging_state
481
498
  if 'mode' in data['data'] and data['data']['mode'] is not None \
482
499
  and vehicle.charging is not None and isinstance(vehicle.charging.settings, SkodaCharging.Settings):
483
- if data['data']['mode'] in SkodaCharging.SkodaChargeMode:
500
+ if data['data']['mode'] in [item.value for item in SkodaCharging.SkodaChargeMode]:
484
501
  skoda_charging_mode = SkodaCharging.SkodaChargeMode(data['data']['mode'])
485
502
  else:
486
503
  LOG_API.info('Unkown charging mode %s not in %s', data['data']['mode'], str(SkodaCharging.SkodaChargeMode))
@@ -1,6 +0,0 @@
1
- flake8~=7.2.0
2
- pylint~=3.3.6
3
- bandit~=1.8.3
4
-
5
- # For UI only
6
- Flask~=3.1.0
@@ -1,5 +0,0 @@
1
- carconnectivity>=0.6
2
- oauthlib~=3.2.2
3
- requests~=2.32.3
4
- jwt~=1.3.1
5
- paho-mqtt~=2.1.0