carconnectivity-connector-skoda 0.1a11__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.1a11.dist-info → carconnectivity_connector_skoda-0.8.2.dist-info}/METADATA +10 -7
- carconnectivity_connector_skoda-0.8.2.dist-info/RECORD +23 -0
- {carconnectivity_connector_skoda-0.1a11.dist-info → carconnectivity_connector_skoda-0.8.2.dist-info}/WHEEL +1 -1
- carconnectivity_connectors/skoda/_version.py +22 -4
- carconnectivity_connectors/skoda/auth/my_skoda_session.py +24 -13
- carconnectivity_connectors/skoda/auth/openid_session.py +16 -15
- carconnectivity_connectors/skoda/auth/skoda_web_session.py +2 -0
- carconnectivity_connectors/skoda/capability.py +13 -16
- carconnectivity_connectors/skoda/charging.py +74 -4
- carconnectivity_connectors/skoda/climatization.py +43 -0
- carconnectivity_connectors/skoda/command_impl.py +78 -0
- carconnectivity_connectors/skoda/connector.py +1354 -233
- carconnectivity_connectors/skoda/error.py +53 -0
- carconnectivity_connectors/skoda/mqtt_client.py +137 -37
- carconnectivity_connectors/skoda/ui/connector_ui.py +39 -0
- carconnectivity_connectors/skoda/vehicle.py +31 -9
- carconnectivity_connector_skoda-0.1a11.dist-info/RECORD +0 -19
- {carconnectivity_connector_skoda-0.1a11.dist-info → carconnectivity_connector_skoda-0.8.2.dist-info/licenses}/LICENSE +0 -0
- {carconnectivity_connector_skoda-0.1a11.dist-info → carconnectivity_connector_skoda-0.8.2.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: carconnectivity-connector-skoda
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.2
|
|
4
4
|
Summary: CarConnectivity connector for Skoda services
|
|
5
5
|
Author: Till Steinbach
|
|
6
6
|
License: MIT License
|
|
@@ -33,18 +33,21 @@ 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
|
|
41
|
-
Requires-Dist: oauthlib~=3.
|
|
42
|
-
Requires-Dist: requests~=2.32.
|
|
43
|
-
Requires-Dist:
|
|
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
|
|
45
|
+
Requires-Dist: paho-mqtt~=2.1.0
|
|
46
|
+
Dynamic: license-file
|
|
44
47
|
|
|
45
48
|
|
|
46
49
|
|
|
47
|
-
# CarConnectivity Connector for
|
|
50
|
+
# CarConnectivity Connector for Skoda Vehicles
|
|
48
51
|
[](https://github.com/tillsteinbach/CarConnectivity-connector-skoda/)
|
|
49
52
|
[](https://github.com/tillsteinbach/CarConnectivity-connector-skoda/releases/latest)
|
|
50
53
|
[](https://github.com/tillsteinbach/CarConnectivity-connector-skoda/blob/master/LICENSE)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
carconnectivity_connector_skoda-0.8.2.dist-info/licenses/LICENSE,sha256=PIwI1alwDyOfvEQHdGCm2u9uf_mGE8030xZDfun0xTo,1071
|
|
2
|
+
carconnectivity_connectors/skoda/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
carconnectivity_connectors/skoda/_version.py,sha256=K6dg_KQgkOH-DF8J9hcSjz3upL94O2YIVOO7FP9tPpk,704
|
|
4
|
+
carconnectivity_connectors/skoda/capability.py,sha256=TC8-yC23UUrf0faePdbZL0802DHXbtGDcSlt3vj5ltg,4770
|
|
5
|
+
carconnectivity_connectors/skoda/charging.py,sha256=7DPNiTWFhxiiEFKVnbIIU2TCmkpmcMWx_zsHXGXFpAQ,6856
|
|
6
|
+
carconnectivity_connectors/skoda/climatization.py,sha256=Jut468SkxjPBDTqroWFvDifVPfJBxGjsFed5pc4kZkg,1768
|
|
7
|
+
carconnectivity_connectors/skoda/command_impl.py,sha256=wDCI3Bka5pXlbyI4yVFS353TgFGyiBHBkERpP2g0A9w,3230
|
|
8
|
+
carconnectivity_connectors/skoda/connector.py,sha256=y25ZY1jCH-8IXSK847Stu4GbRK6j35xwZJKU9C7TSSU,147301
|
|
9
|
+
carconnectivity_connectors/skoda/error.py,sha256=ffxzvjmci7vtp9Q1K4DR1kBF0kTJxN5Gluci3kDBkEI,2459
|
|
10
|
+
carconnectivity_connectors/skoda/mqtt_client.py,sha256=f4fRFeI1VUCGm9ZzGj_vnctiSIiMAoolYLAwKx4apqA,40086
|
|
11
|
+
carconnectivity_connectors/skoda/vehicle.py,sha256=q5gwe-_yPfE_-aEc17UQ-Q0Z46IN7PCpNG5jLw5PZl0,3981
|
|
12
|
+
carconnectivity_connectors/skoda/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
carconnectivity_connectors/skoda/auth/auth_util.py,sha256=dGLUbUre0HBsTg_Ii5vW34f8DLrCykYJYCyzEvUBBEE,4434
|
|
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
|
+
carconnectivity_connectors/skoda/auth/session_manager.py,sha256=Uf1vujuDBYUCAXhYToOsZkgbTtfmY3Qe0ICTfwomBpI,2899
|
|
17
|
+
carconnectivity_connectors/skoda/auth/skoda_web_session.py,sha256=tapjCRRPBu3tHrDoKmtuAlQhgxktib3oWTB8MHEzZTY,10816
|
|
18
|
+
carconnectivity_connectors/skoda/auth/helpers/blacklist_retry.py,sha256=f3wsiY5bpHDBxp7Va1Mv9nKJ4u3qnCHZZmDu78_AhMk,1251
|
|
19
|
+
carconnectivity_connectors/skoda/ui/connector_ui.py,sha256=lLjwoakRaU0S80hAVwVi4JA1wtHycGHcoM2-7S9qsqI,1386
|
|
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,16 +1,34 @@
|
|
|
1
|
-
# file generated by
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
2
|
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
12
|
+
|
|
3
13
|
TYPE_CHECKING = False
|
|
4
14
|
if TYPE_CHECKING:
|
|
5
|
-
from typing import Tuple
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
from typing import Union
|
|
17
|
+
|
|
6
18
|
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
7
20
|
else:
|
|
8
21
|
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
9
23
|
|
|
10
24
|
version: str
|
|
11
25
|
__version__: str
|
|
12
26
|
__version_tuple__: VERSION_TUPLE
|
|
13
27
|
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
30
|
+
|
|
31
|
+
__version__ = version = '0.8.2'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 8, 2)
|
|
14
33
|
|
|
15
|
-
|
|
16
|
-
__version_tuple__ = version_tuple = (0, 1)
|
|
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
|
|
@@ -7,7 +7,7 @@ import time
|
|
|
7
7
|
from datetime import datetime, timezone
|
|
8
8
|
import logging
|
|
9
9
|
import requests
|
|
10
|
-
|
|
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
|
|
@@ -18,8 +18,8 @@ from requests.adapters import HTTPAdapter
|
|
|
18
18
|
|
|
19
19
|
from carconnectivity.errors import AuthenticationError, RetrievalError
|
|
20
20
|
|
|
21
|
-
from carconnectivity_connectors.
|
|
22
|
-
from carconnectivity_connectors.
|
|
21
|
+
from carconnectivity_connectors.skoda.auth.auth_util import add_bearer_auth_header
|
|
22
|
+
from carconnectivity_connectors.skoda.auth.helpers.blacklist_retry import BlacklistRetry
|
|
23
23
|
|
|
24
24
|
if TYPE_CHECKING:
|
|
25
25
|
from typing import Dict
|
|
@@ -149,23 +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
|
-
new_token['expires_in'] = (expires_at - datetime.now(tz=timezone.utc)).total_seconds()
|
|
162
|
-
else:
|
|
163
|
-
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']
|
|
164
161
|
else:
|
|
165
162
|
new_token['expires_in'] = 3600
|
|
166
163
|
# If expires_in is set and expires_at is not set we calculate expires_at from expires_in using the current time
|
|
167
164
|
if 'expires_in' in new_token and 'expires_at' not in new_token:
|
|
168
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')
|
|
169
170
|
self._token = new_token
|
|
170
171
|
|
|
171
172
|
@property
|
|
@@ -121,6 +121,8 @@ class SkodaWebSession(OpenIDSession):
|
|
|
121
121
|
raise RetrievalError('Temporary server error during login')
|
|
122
122
|
|
|
123
123
|
if 'Location' not in response.headers:
|
|
124
|
+
if 'consent' in url:
|
|
125
|
+
raise AuthenticationError('Could not find Location in headers, probably due to missing consent. Try visiting: ' + url)
|
|
124
126
|
raise APICompatibilityError('Forwarding without Location in headers')
|
|
125
127
|
|
|
126
128
|
url = response.headers['Location']
|
|
@@ -5,7 +5,7 @@ from typing import TYPE_CHECKING
|
|
|
5
5
|
from enum import IntEnum
|
|
6
6
|
|
|
7
7
|
from carconnectivity.objects import GenericObject
|
|
8
|
-
from carconnectivity.attributes import StringAttribute
|
|
8
|
+
from carconnectivity.attributes import StringAttribute, GenericAttribute
|
|
9
9
|
|
|
10
10
|
if TYPE_CHECKING:
|
|
11
11
|
from typing import Dict, Optional
|
|
@@ -77,7 +77,7 @@ class Capabilities(GenericObject):
|
|
|
77
77
|
"""
|
|
78
78
|
return self.__capabilities.get(capability_id)
|
|
79
79
|
|
|
80
|
-
def has_capability(self, capability_id: str) -> bool:
|
|
80
|
+
def has_capability(self, capability_id: str, check_status_ok=False) -> bool:
|
|
81
81
|
"""
|
|
82
82
|
Check if the Capabilities contains a capability with the specified ID.
|
|
83
83
|
|
|
@@ -87,13 +87,14 @@ class Capabilities(GenericObject):
|
|
|
87
87
|
Returns:
|
|
88
88
|
bool: True if the capability exists, otherwise False.
|
|
89
89
|
"""
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
90
|
+
if check_status_ok:
|
|
91
|
+
if capability_id in self.__capabilities and self.__capabilities[capability_id].enabled:
|
|
92
|
+
capability: Capability = self.__capabilities[capability_id]
|
|
93
|
+
if capability.status.enabled and capability.status.value is not None and len(capability.status.value) > 0:
|
|
94
|
+
return False
|
|
95
|
+
return True
|
|
96
|
+
return False
|
|
97
|
+
return capability_id in self.__capabilities and self.__capabilities[capability_id].enabled
|
|
97
98
|
|
|
98
99
|
|
|
99
100
|
class Capability(GenericObject):
|
|
@@ -108,15 +109,11 @@ class Capability(GenericObject):
|
|
|
108
109
|
raise ValueError('Capability ID cannot be None')
|
|
109
110
|
super().__init__(object_id=capability_id, parent=capabilities)
|
|
110
111
|
self.delay_notifications = True
|
|
111
|
-
self.capability_id = StringAttribute("id", self, capability_id)
|
|
112
|
-
self.
|
|
112
|
+
self.capability_id = StringAttribute("id", self, capability_id, tags={'connector_custom'})
|
|
113
|
+
self.status = GenericAttribute("status", self, value=[], tags={'connector_custom'})
|
|
113
114
|
self.enabled = True
|
|
114
115
|
self.delay_notifications = False
|
|
115
116
|
|
|
116
|
-
def __str__(self) -> str:
|
|
117
|
-
return_string = f'{self.capability_id}'
|
|
118
|
-
return return_string
|
|
119
|
-
|
|
120
117
|
class Status(IntEnum):
|
|
121
118
|
"""
|
|
122
119
|
Enum for capability status.
|
|
@@ -135,7 +132,7 @@ class Capability(GenericObject):
|
|
|
135
132
|
LOCATION_DATA_DISABLED = 1013
|
|
136
133
|
LICENSE_INACTIVE = 2001
|
|
137
134
|
LICENSE_EXPIRED = 2002
|
|
138
|
-
|
|
135
|
+
LICENSE_MISSING = 2003
|
|
139
136
|
USER_NOT_VERIFIED = 3001
|
|
140
137
|
TERMS_AND_CONDITIONS_NOT_ACCEPTED = 3002
|
|
141
138
|
INSUFFICIENT_RIGHTS = 3003
|
|
@@ -7,18 +7,47 @@ from typing import TYPE_CHECKING
|
|
|
7
7
|
from enum import Enum
|
|
8
8
|
|
|
9
9
|
from carconnectivity.charging import Charging
|
|
10
|
+
from carconnectivity.objects import GenericObject
|
|
11
|
+
from carconnectivity.vehicle import ElectricVehicle
|
|
12
|
+
from carconnectivity.attributes import BooleanAttribute, EnumAttribute, StringAttribute
|
|
13
|
+
|
|
14
|
+
from carconnectivity_connectors.skoda.error import Error
|
|
10
15
|
|
|
11
16
|
if TYPE_CHECKING:
|
|
12
|
-
from typing import Dict
|
|
17
|
+
from typing import Optional, Dict
|
|
13
18
|
|
|
14
19
|
|
|
15
20
|
class SkodaCharging(Charging): # pylint: disable=too-many-instance-attributes
|
|
16
21
|
"""
|
|
17
22
|
SkodaCharging class for handling Skoda vehicle charging information.
|
|
18
23
|
|
|
19
|
-
This class extends the Charging class and includes an enumeration of various
|
|
24
|
+
This class extends the Charging class and includes an enumeration of various
|
|
20
25
|
charging states specific to Skoda vehicles.
|
|
21
26
|
"""
|
|
27
|
+
def __init__(self, vehicle: ElectricVehicle | None = None, origin: Optional[Charging] = None) -> None:
|
|
28
|
+
if origin is not None:
|
|
29
|
+
super().__init__(vehicle=vehicle, origin=origin)
|
|
30
|
+
self.settings: Charging.Settings = SkodaCharging.Settings(parent=self, origin=origin.settings)
|
|
31
|
+
else:
|
|
32
|
+
super().__init__(vehicle=vehicle)
|
|
33
|
+
self.settings: Charging.Settings = SkodaCharging.Settings(parent=self, origin=self.settings)
|
|
34
|
+
self.errors: Dict[str, Error] = {}
|
|
35
|
+
self.is_in_saved_location: BooleanAttribute = BooleanAttribute("is_in_saved_location", parent=self, tags={'connector_custom'})
|
|
36
|
+
|
|
37
|
+
class Settings(Charging.Settings):
|
|
38
|
+
"""
|
|
39
|
+
This class represents the settings for a skoda car charging.
|
|
40
|
+
"""
|
|
41
|
+
def __init__(self, parent: Optional[GenericObject] = None, origin: Optional[Charging.Settings] = None) -> None:
|
|
42
|
+
if origin is not None:
|
|
43
|
+
super().__init__(parent=parent, origin=origin)
|
|
44
|
+
else:
|
|
45
|
+
super().__init__(parent=parent)
|
|
46
|
+
self.preferred_charge_mode: EnumAttribute = EnumAttribute("preferred_charge_mode", parent=self, tags={'connector_custom'})
|
|
47
|
+
self.available_charge_modes: StringAttribute = StringAttribute("available_charge_modes", parent=self, tags={'connector_custom'})
|
|
48
|
+
self.charging_care_mode: EnumAttribute = EnumAttribute("charging_care_mode", parent=self, tags={'connector_custom'})
|
|
49
|
+
self.battery_support: EnumAttribute = EnumAttribute("battery_support", parent=self, tags={'connector_custom'})
|
|
50
|
+
|
|
22
51
|
class SkodaChargingState(Enum,):
|
|
23
52
|
"""
|
|
24
53
|
Enum representing the various charging states for a Skoda vehicle.
|
|
@@ -40,7 +69,7 @@ class SkodaCharging(Charging): # pylint: disable=too-many-instance-attributes
|
|
|
40
69
|
CONNECT_CABLE = 'connectCable'
|
|
41
70
|
READY_FOR_CHARGING = 'readyForCharging'
|
|
42
71
|
NOT_READY_FOR_CHARGING = 'notReadyForCharging'
|
|
43
|
-
|
|
72
|
+
CONSERVING = 'conserving'
|
|
44
73
|
CHARGE_PURPOSE_REACHED_NOT_CONSERVATION_CHARGING = 'chargePurposeReachedAndNotConservationCharging'
|
|
45
74
|
CHARGE_PURPOSE_REACHED_CONSERVATION = 'chargePurposeReachedAndConservation'
|
|
46
75
|
CHARGING = 'charging'
|
|
@@ -49,6 +78,47 @@ class SkodaCharging(Charging): # pylint: disable=too-many-instance-attributes
|
|
|
49
78
|
DISCHARGING = 'discharging'
|
|
50
79
|
UNKNOWN = 'unknown charging state'
|
|
51
80
|
|
|
81
|
+
class SkodaChargeMode(Enum,):
|
|
82
|
+
"""
|
|
83
|
+
Enum class representing different Skoda charge modes.
|
|
84
|
+
|
|
85
|
+
Attributes:
|
|
86
|
+
HOME_STORAGE_CHARGING (str): Charge mode for home storage charging.
|
|
87
|
+
IMMEDIATE_DISCHARGING (str): Charge mode for immediate discharging.
|
|
88
|
+
ONLY_OWN_CURRENT (str): Charge mode for using only own current.
|
|
89
|
+
PREFERRED_CHARGING_TIMES (str): Charge mode for preferred charging times.
|
|
90
|
+
TIMER_CHARGING_WITH_CLIMATISATION (str): Charge mode for timer charging with climatisation.
|
|
91
|
+
TIMER (str): Charge mode for timer-based charging.
|
|
92
|
+
MANUAL (str): Charge mode for manual charging.
|
|
93
|
+
OFF (str): Charge mode for turning off charging.
|
|
94
|
+
"""
|
|
95
|
+
HOME_STORAGE_CHARGING = 'HOME_STORAGE_CHARGING'
|
|
96
|
+
IMMEDIATE_DISCHARGING = 'IMMEDIATE_DISCHARGING'
|
|
97
|
+
ONLY_OWN_CURRENT = 'ONLY_OWN_CURRENT'
|
|
98
|
+
PREFERRED_CHARGING_TIMES = 'PREFERRED_CHARGING_TIMES'
|
|
99
|
+
TIMER_CHARGING_WITH_CLIMATISATION = 'TIMER_CHARGING_WITH_CLIMATISATION'
|
|
100
|
+
TIMER = 'timer'
|
|
101
|
+
MANUAL = 'manual'
|
|
102
|
+
OFF = 'off'
|
|
103
|
+
UNKNOWN = 'unknown charge mode'
|
|
104
|
+
|
|
105
|
+
class SkodaChargingCareMode(Enum,):
|
|
106
|
+
"""
|
|
107
|
+
Enum representing the charging care mode for Skoda vehicles.
|
|
108
|
+
"""
|
|
109
|
+
ACTIVATED = 'ACTIVATED'
|
|
110
|
+
DEACTIVATED = 'DEACTIVATED'
|
|
111
|
+
UNKNOWN = 'UNKNOWN'
|
|
112
|
+
|
|
113
|
+
class SkodaBatterySupport(Enum,):
|
|
114
|
+
"""
|
|
115
|
+
SkodaBatterySupport is an enumeration that represents the different states of battery support for Skoda vehicles.
|
|
116
|
+
"""
|
|
117
|
+
ENABLED = 'ENABLED'
|
|
118
|
+
DISABLED = 'DISABLED'
|
|
119
|
+
NOT_ALLOWED = 'NOT_ALLOWED'
|
|
120
|
+
UNKNOWN = 'UNKNOWN'
|
|
121
|
+
|
|
52
122
|
|
|
53
123
|
# Mapping of Skoda charging states to generic charging states
|
|
54
124
|
mapping_skoda_charging_state: Dict[SkodaCharging.SkodaChargingState, Charging.ChargingState] = {
|
|
@@ -56,7 +126,7 @@ mapping_skoda_charging_state: Dict[SkodaCharging.SkodaChargingState, Charging.Ch
|
|
|
56
126
|
SkodaCharging.SkodaChargingState.CONNECT_CABLE: Charging.ChargingState.OFF,
|
|
57
127
|
SkodaCharging.SkodaChargingState.READY_FOR_CHARGING: Charging.ChargingState.READY_FOR_CHARGING,
|
|
58
128
|
SkodaCharging.SkodaChargingState.NOT_READY_FOR_CHARGING: Charging.ChargingState.OFF,
|
|
59
|
-
SkodaCharging.SkodaChargingState.
|
|
129
|
+
SkodaCharging.SkodaChargingState.CONSERVING: Charging.ChargingState.CONSERVATION,
|
|
60
130
|
SkodaCharging.SkodaChargingState.CHARGE_PURPOSE_REACHED_NOT_CONSERVATION_CHARGING: Charging.ChargingState.READY_FOR_CHARGING,
|
|
61
131
|
SkodaCharging.SkodaChargingState.CHARGE_PURPOSE_REACHED_CONSERVATION: Charging.ChargingState.CONSERVATION,
|
|
62
132
|
SkodaCharging.SkodaChargingState.CHARGING: Charging.ChargingState.CHARGING,
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module for climatization for skoda vehicles.
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from carconnectivity.climatization import Climatization
|
|
8
|
+
from carconnectivity.objects import GenericObject
|
|
9
|
+
from carconnectivity.vehicle import GenericVehicle
|
|
10
|
+
|
|
11
|
+
from carconnectivity_connectors.skoda.error import Error
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from typing import Optional, Dict
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SkodaClimatization(Climatization): # pylint: disable=too-many-instance-attributes
|
|
18
|
+
"""
|
|
19
|
+
SkodaClimatization class for handling Skoda vehicle climatization information.
|
|
20
|
+
|
|
21
|
+
This class extends the Climatization class and includes an enumeration of various
|
|
22
|
+
charging states specific to Skoda vehicles.
|
|
23
|
+
"""
|
|
24
|
+
def __init__(self, vehicle: GenericVehicle | None = None, origin: Optional[Climatization] = None) -> None:
|
|
25
|
+
if origin is not None:
|
|
26
|
+
super().__init__(origin=origin)
|
|
27
|
+
if not isinstance(self.settings, SkodaClimatization.Settings):
|
|
28
|
+
self.settings: Climatization.Settings = SkodaClimatization.Settings(parent=self, origin=origin.settings)
|
|
29
|
+
self.settings.parent = self
|
|
30
|
+
else:
|
|
31
|
+
super().__init__(vehicle=vehicle)
|
|
32
|
+
self.settings: Climatization.Settings = SkodaClimatization.Settings(parent=self)
|
|
33
|
+
self.errors: Dict[str, Error] = {}
|
|
34
|
+
|
|
35
|
+
class Settings(Climatization.Settings):
|
|
36
|
+
"""
|
|
37
|
+
This class represents the settings for a skoda car climatiation.
|
|
38
|
+
"""
|
|
39
|
+
def __init__(self, parent: Optional[GenericObject] = None, origin: Optional[Climatization.Settings] = None) -> None:
|
|
40
|
+
if origin is not None:
|
|
41
|
+
super().__init__(parent=parent, origin=origin)
|
|
42
|
+
else:
|
|
43
|
+
super().__init__(parent=parent)
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""This module defines the classes that represent attributes in the CarConnectivity system."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from typing import TYPE_CHECKING, Dict, Union
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
import argparse
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
from carconnectivity.commands import GenericCommand
|
|
10
|
+
from carconnectivity.objects import GenericObject
|
|
11
|
+
from carconnectivity.errors import SetterError
|
|
12
|
+
from carconnectivity.util import ThrowingArgumentParser
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from carconnectivity.objects import Optional
|
|
16
|
+
|
|
17
|
+
LOG: logging.Logger = logging.getLogger("carconnectivity.connectors.skoda")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SpinCommand(GenericCommand):
|
|
21
|
+
"""
|
|
22
|
+
SpinCommand is a command class for verifying the spin
|
|
23
|
+
|
|
24
|
+
"""
|
|
25
|
+
def __init__(self, name: str = 'spin', parent: Optional[GenericObject] = None) -> None:
|
|
26
|
+
super().__init__(name=name, parent=parent)
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def value(self) -> Optional[Union[str, Dict]]:
|
|
30
|
+
return super().value
|
|
31
|
+
|
|
32
|
+
@value.setter
|
|
33
|
+
def value(self, new_value: Optional[Union[str, Dict]]) -> None:
|
|
34
|
+
# Execute early hooks before parsing the value
|
|
35
|
+
new_value = self._execute_on_set_hook(new_value, early_hook=True)
|
|
36
|
+
if isinstance(new_value, SpinCommand.Command):
|
|
37
|
+
newvalue_dict = {}
|
|
38
|
+
newvalue_dict['command'] = new_value
|
|
39
|
+
new_value = newvalue_dict
|
|
40
|
+
elif isinstance(new_value, str):
|
|
41
|
+
parser = ThrowingArgumentParser(prog='', add_help=False, exit_on_error=False)
|
|
42
|
+
parser.add_argument('command', help='Command to execute', type=SpinCommand.Command,
|
|
43
|
+
choices=list(SpinCommand.Command))
|
|
44
|
+
parser.add_argument('--spin', dest='spin', help='Spin to be used instead of spin from config or .netrc', type=str, required=False,
|
|
45
|
+
default=None)
|
|
46
|
+
try:
|
|
47
|
+
args = parser.parse_args(new_value.strip().split(sep=' '))
|
|
48
|
+
except argparse.ArgumentError as e:
|
|
49
|
+
raise SetterError(f'Invalid format for SpinCommand: {e.message} {parser.format_usage()}') from e
|
|
50
|
+
|
|
51
|
+
newvalue_dict = {}
|
|
52
|
+
newvalue_dict['command'] = args.command
|
|
53
|
+
if args.spin is not None:
|
|
54
|
+
newvalue_dict['spin'] = args.spin
|
|
55
|
+
new_value = newvalue_dict
|
|
56
|
+
elif isinstance(new_value, dict):
|
|
57
|
+
if 'command' in new_value and isinstance(new_value['command'], str):
|
|
58
|
+
if new_value['command'] in SpinCommand.Command:
|
|
59
|
+
new_value['command'] = SpinCommand.Command(new_value['command'])
|
|
60
|
+
else:
|
|
61
|
+
raise ValueError('Invalid value for SpinCommand. '
|
|
62
|
+
f'Command must be one of {SpinCommand.Command}')
|
|
63
|
+
if self._is_changeable:
|
|
64
|
+
# Execute late hooks before setting the value
|
|
65
|
+
new_value = self._execute_on_set_hook(new_value, early_hook=False)
|
|
66
|
+
self._set_value(new_value)
|
|
67
|
+
else:
|
|
68
|
+
raise TypeError('You cannot set this attribute. Attribute is not mutable.')
|
|
69
|
+
|
|
70
|
+
class Command(Enum):
|
|
71
|
+
"""
|
|
72
|
+
Enum class representing different commands for SPIN.
|
|
73
|
+
|
|
74
|
+
"""
|
|
75
|
+
VERIFY = 'verify'
|
|
76
|
+
|
|
77
|
+
def __str__(self) -> str:
|
|
78
|
+
return self.value
|