carconnectivity-connector-skoda 0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of carconnectivity-connector-skoda might be problematic. Click here for more details.
- carconnectivity_connector_skoda-0.1.dist-info/LICENSE +21 -0
- carconnectivity_connector_skoda-0.1.dist-info/METADATA +123 -0
- carconnectivity_connector_skoda-0.1.dist-info/RECORD +22 -0
- carconnectivity_connector_skoda-0.1.dist-info/WHEEL +5 -0
- carconnectivity_connector_skoda-0.1.dist-info/top_level.txt +1 -0
- carconnectivity_connectors/skoda/__init__.py +0 -0
- carconnectivity_connectors/skoda/_version.py +16 -0
- carconnectivity_connectors/skoda/auth/__init__.py +0 -0
- carconnectivity_connectors/skoda/auth/auth_util.py +141 -0
- carconnectivity_connectors/skoda/auth/helpers/blacklist_retry.py +29 -0
- carconnectivity_connectors/skoda/auth/my_skoda_session.py +224 -0
- carconnectivity_connectors/skoda/auth/openid_session.py +449 -0
- carconnectivity_connectors/skoda/auth/session_manager.py +82 -0
- carconnectivity_connectors/skoda/auth/skoda_web_session.py +239 -0
- carconnectivity_connectors/skoda/capability.py +135 -0
- carconnectivity_connectors/skoda/charging.py +137 -0
- carconnectivity_connectors/skoda/climatization.py +41 -0
- carconnectivity_connectors/skoda/command_impl.py +74 -0
- carconnectivity_connectors/skoda/connector.py +1624 -0
- carconnectivity_connectors/skoda/error.py +53 -0
- carconnectivity_connectors/skoda/mqtt_client.py +655 -0
- carconnectivity_connectors/skoda/vehicle.py +70 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module implements a VW Web session.
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
from urllib.parse import parse_qsl, urlparse, urlsplit, urljoin
|
|
9
|
+
|
|
10
|
+
from urllib3.util.retry import Retry
|
|
11
|
+
|
|
12
|
+
import requests
|
|
13
|
+
from requests.adapters import HTTPAdapter
|
|
14
|
+
from requests.models import CaseInsensitiveDict
|
|
15
|
+
|
|
16
|
+
from carconnectivity.errors import APICompatibilityError, AuthenticationError, RetrievalError
|
|
17
|
+
|
|
18
|
+
from carconnectivity_connectors.skoda.auth.auth_util import CredentialsFormParser, HTMLFormParser, TermsAndConditionsFormParser
|
|
19
|
+
from carconnectivity_connectors.skoda.auth.openid_session import OpenIDSession
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from typing import Any, Dict
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SkodaWebSession(OpenIDSession):
|
|
26
|
+
"""
|
|
27
|
+
SkodaWebSession handles the web authentication process for Skoda's web services.
|
|
28
|
+
"""
|
|
29
|
+
def __init__(self, session_user, cache, accept_terms_on_login=False, **kwargs):
|
|
30
|
+
super(SkodaWebSession, self).__init__(**kwargs)
|
|
31
|
+
self.session_user = session_user
|
|
32
|
+
self.cache = cache
|
|
33
|
+
self.accept_terms_on_login: bool = accept_terms_on_login
|
|
34
|
+
|
|
35
|
+
# Set up the web session
|
|
36
|
+
retries = Retry(
|
|
37
|
+
total=self.retries,
|
|
38
|
+
backoff_factor=0.1,
|
|
39
|
+
status_forcelist=[500],
|
|
40
|
+
raise_on_status=False
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
self.websession: requests.Session = requests.Session()
|
|
44
|
+
self.websession.proxies.update(self.proxies)
|
|
45
|
+
self.websession.mount('https://', HTTPAdapter(max_retries=retries))
|
|
46
|
+
self.websession.headers = CaseInsensitiveDict({
|
|
47
|
+
'user-agent': 'Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 '
|
|
48
|
+
'Chrome/74.0.3729.185 Mobile Safari/537.36',
|
|
49
|
+
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,'
|
|
50
|
+
'application/signed-exchange;v=b3',
|
|
51
|
+
'accept-language': 'en-US,en;q=0.9',
|
|
52
|
+
'accept-encoding': 'gzip, deflate',
|
|
53
|
+
'x-requested-with': 'cz.skodaauto.connect',
|
|
54
|
+
'upgrade-insecure-requests': '1',
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
def do_web_auth(self, url: str) -> str:
|
|
58
|
+
"""
|
|
59
|
+
Perform web authentication using the provided URL.
|
|
60
|
+
|
|
61
|
+
This method handles the web authentication process by:
|
|
62
|
+
1. Retrieving the login form.
|
|
63
|
+
2. Setting the email to the provided username.
|
|
64
|
+
3. Retrieving the password form.
|
|
65
|
+
4. Setting the credentials (email and password).
|
|
66
|
+
5. Logging in and getting the redirect URL.
|
|
67
|
+
6. Checking the URL for terms and conditions and handling consent if required.
|
|
68
|
+
7. Following redirects until the final URL is reached.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
url (str): The URL to start the authentication process.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
str: The final URL after successful authentication.
|
|
75
|
+
|
|
76
|
+
Raises:
|
|
77
|
+
AuthenticationError: If terms and conditions need to be accepted.
|
|
78
|
+
RetrievalError: If there is a temporary server error during login.
|
|
79
|
+
APICompatibilityError: If forwarding occurs without 'Location' in headers.
|
|
80
|
+
"""
|
|
81
|
+
# Get the login form
|
|
82
|
+
email_form: HTMLFormParser = self._get_login_form(url)
|
|
83
|
+
|
|
84
|
+
# Set email to the provided username
|
|
85
|
+
email_form.data['email'] = self.session_user.username
|
|
86
|
+
|
|
87
|
+
# Get password form
|
|
88
|
+
password_form = self._get_password_form(
|
|
89
|
+
urljoin('https://identity.vwgroup.io', email_form.target),
|
|
90
|
+
email_form.data
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Set credentials
|
|
94
|
+
password_form.data['email'] = self.session_user.username
|
|
95
|
+
password_form.data['password'] = self.session_user.password
|
|
96
|
+
|
|
97
|
+
# Log in and get the redirect URL
|
|
98
|
+
url = self._handle_login(
|
|
99
|
+
f'https://identity.vwgroup.io/signin-service/v1/{self.client_id}/{password_form.target}',
|
|
100
|
+
password_form.data
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
if self.redirect_uri is None:
|
|
104
|
+
raise ValueError('Redirect URI is not set')
|
|
105
|
+
# Check URL for terms and conditions
|
|
106
|
+
while True:
|
|
107
|
+
if url.startswith(self.redirect_uri):
|
|
108
|
+
break
|
|
109
|
+
|
|
110
|
+
url = urljoin('https://identity.vwgroup.io', url)
|
|
111
|
+
|
|
112
|
+
if 'terms-and-conditions' in url:
|
|
113
|
+
if self.accept_terms_on_login:
|
|
114
|
+
url = self._handle_consent_form(url)
|
|
115
|
+
else:
|
|
116
|
+
raise AuthenticationError(f'It seems like you need to accept the terms and conditions. '
|
|
117
|
+
f'Try to visit the URL "{url}" or log into smartphone app.')
|
|
118
|
+
|
|
119
|
+
response = self.websession.get(url, allow_redirects=False)
|
|
120
|
+
if response.status_code == requests.codes['internal_server_error']:
|
|
121
|
+
raise RetrievalError('Temporary server error during login')
|
|
122
|
+
|
|
123
|
+
if 'Location' not in response.headers:
|
|
124
|
+
raise APICompatibilityError('Forwarding without Location in headers')
|
|
125
|
+
|
|
126
|
+
url = response.headers['Location']
|
|
127
|
+
|
|
128
|
+
return url.replace(self.redirect_uri + '#', 'https://egal?')
|
|
129
|
+
|
|
130
|
+
def _get_login_form(self, url: str) -> HTMLFormParser:
|
|
131
|
+
while True:
|
|
132
|
+
response = self.websession.get(url, allow_redirects=False)
|
|
133
|
+
if response.status_code == requests.codes['ok']:
|
|
134
|
+
break
|
|
135
|
+
|
|
136
|
+
if response.status_code in (requests.codes['found'], requests.codes['see_other']):
|
|
137
|
+
if 'Location' not in response.headers:
|
|
138
|
+
raise APICompatibilityError('Forwarding without Location in headers')
|
|
139
|
+
|
|
140
|
+
url = response.headers['Location']
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
raise APICompatibilityError(f'Retrieving login page was not successful, '
|
|
144
|
+
f'status code: {response.status_code}')
|
|
145
|
+
|
|
146
|
+
# Find login form on page to obtain inputs
|
|
147
|
+
email_form = HTMLFormParser(form_id='emailPasswordForm')
|
|
148
|
+
email_form.feed(response.text)
|
|
149
|
+
|
|
150
|
+
if not email_form.target or not all(x in email_form.data for x in ['_csrf', 'relayState', 'hmac', 'email']):
|
|
151
|
+
raise APICompatibilityError('Could not find all required input fields on login page')
|
|
152
|
+
|
|
153
|
+
return email_form
|
|
154
|
+
|
|
155
|
+
def _get_password_form(self, url: str, data: Dict[str, Any]) -> CredentialsFormParser:
|
|
156
|
+
response = self.websession.post(url, data=data, allow_redirects=True)
|
|
157
|
+
if response.status_code != requests.codes['ok']:
|
|
158
|
+
raise APICompatibilityError(f'Retrieving credentials page was not successful, '
|
|
159
|
+
f'status code: {response.status_code}')
|
|
160
|
+
|
|
161
|
+
# Find login form on page to obtain inputs
|
|
162
|
+
credentials_form = CredentialsFormParser()
|
|
163
|
+
credentials_form.feed(response.text)
|
|
164
|
+
|
|
165
|
+
if not credentials_form.target or not all(x in credentials_form.data for x in ['relayState', 'hmac', '_csrf']):
|
|
166
|
+
raise APICompatibilityError('Could not find all required input fields on credentials page')
|
|
167
|
+
|
|
168
|
+
if credentials_form.data.get('error', None) is not None:
|
|
169
|
+
if credentials_form.data['error'] == 'validator.email.invalid':
|
|
170
|
+
raise AuthenticationError('Error during login, email invalid')
|
|
171
|
+
raise AuthenticationError(f'Error during login: {credentials_form.data["error"]}')
|
|
172
|
+
|
|
173
|
+
if 'errorCode' in credentials_form.data:
|
|
174
|
+
raise AuthenticationError('Error during login, is the username correct?')
|
|
175
|
+
|
|
176
|
+
if credentials_form.data.get('registerCredentialsPath', None) == 'register':
|
|
177
|
+
raise AuthenticationError(f'Error during login, account {self.session_user.username} does not exist')
|
|
178
|
+
|
|
179
|
+
return credentials_form
|
|
180
|
+
|
|
181
|
+
def _handle_login(self, url: str, data: Dict[str, Any]) -> str:
|
|
182
|
+
response: requests.Response = self.websession.post(url, data=data, allow_redirects=False)
|
|
183
|
+
|
|
184
|
+
if response.status_code == requests.codes['internal_server_error']:
|
|
185
|
+
raise RetrievalError('Temporary server error during login')
|
|
186
|
+
|
|
187
|
+
if response.status_code not in (requests.codes['found'], requests.codes['see_other']):
|
|
188
|
+
raise APICompatibilityError(f'Forwarding expected (status code 302), '
|
|
189
|
+
f'but got status code {response.status_code}')
|
|
190
|
+
|
|
191
|
+
if 'Location' not in response.headers:
|
|
192
|
+
raise APICompatibilityError('Forwarding without Location in headers')
|
|
193
|
+
|
|
194
|
+
# Parse parameters from forwarding url
|
|
195
|
+
params: Dict[str, str] = dict(parse_qsl(urlsplit(response.headers['Location']).query))
|
|
196
|
+
|
|
197
|
+
# Check for login error
|
|
198
|
+
if 'error' in params and params['error']:
|
|
199
|
+
error_messages: Dict[str, str] = {
|
|
200
|
+
'login.errors.password_invalid': 'Password is invalid',
|
|
201
|
+
'login.error.throttled': 'Login throttled, probably too many wrong logins. You have to wait '
|
|
202
|
+
'a few minutes until a new login attempt is possible'
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
raise AuthenticationError(error_messages.get(params['error'], params['error']))
|
|
206
|
+
|
|
207
|
+
# Check for user ID
|
|
208
|
+
if 'userId' not in params or not params['userId']:
|
|
209
|
+
if 'updated' in params and params['updated'] == 'dataprivacy':
|
|
210
|
+
raise AuthenticationError('You have to login at myvolkswagen.de and accept the terms and conditions')
|
|
211
|
+
raise APICompatibilityError('No user ID provided')
|
|
212
|
+
|
|
213
|
+
self.user_id = params['userId'] # pylint: disable=unused-private-member
|
|
214
|
+
return response.headers['Location']
|
|
215
|
+
|
|
216
|
+
def _handle_consent_form(self, url: str) -> str:
|
|
217
|
+
response = self.websession.get(url, allow_redirects=False)
|
|
218
|
+
if response.status_code == requests.codes['internal_server_error']:
|
|
219
|
+
raise RetrievalError('Temporary server error during login')
|
|
220
|
+
|
|
221
|
+
# Find form on page to obtain inputs
|
|
222
|
+
tc_form = TermsAndConditionsFormParser()
|
|
223
|
+
tc_form.feed(response.text)
|
|
224
|
+
|
|
225
|
+
# Remove query from URL
|
|
226
|
+
url = urlparse(response.url)._replace(query='').geturl()
|
|
227
|
+
|
|
228
|
+
response = self.websession.post(url, data=tc_form.data, allow_redirects=False)
|
|
229
|
+
if response.status_code == requests.codes['internal_server_error']:
|
|
230
|
+
raise RetrievalError('Temporary server error during login')
|
|
231
|
+
|
|
232
|
+
if response.status_code not in (requests.codes['found'], requests.codes['see_other']):
|
|
233
|
+
raise APICompatibilityError('Forwarding expected (status code 302), '
|
|
234
|
+
f'but got status code {response.status_code}')
|
|
235
|
+
|
|
236
|
+
if 'Location' not in response.headers:
|
|
237
|
+
raise APICompatibilityError('Forwarding without Location in headers')
|
|
238
|
+
|
|
239
|
+
return response.headers['Location']
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Module for Skoda vehicle capability class."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from enum import IntEnum
|
|
6
|
+
|
|
7
|
+
from carconnectivity.objects import GenericObject
|
|
8
|
+
from carconnectivity.attributes import StringAttribute
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from typing import Dict, Optional
|
|
12
|
+
from carconnectivity_connectors.skoda.vehicle import SkodaVehicle
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Capabilities(GenericObject):
|
|
16
|
+
"""
|
|
17
|
+
Represents the capabilities of a Skoda vehicle.
|
|
18
|
+
"""
|
|
19
|
+
def __init__(self, vehicle: SkodaVehicle) -> None:
|
|
20
|
+
super().__init__(object_id='capabilities', parent=vehicle)
|
|
21
|
+
self.__capabilities: Dict[str, Capability] = {}
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def capabilities(self) -> Dict[str, Capability]:
|
|
25
|
+
"""
|
|
26
|
+
Retrieve the capabilities of the vehicle.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
Dict[str, Capability]: A dictionary of capabilities.
|
|
30
|
+
"""
|
|
31
|
+
return self.__capabilities
|
|
32
|
+
|
|
33
|
+
def add_capability(self, capability_id: str, capability: Capability) -> None:
|
|
34
|
+
"""
|
|
35
|
+
Adds a capability to the Capabilities of the vehicle.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
capability_id (str): The unique identifier of the capability.
|
|
39
|
+
capability (Capability): The capability object to be added.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
None
|
|
43
|
+
"""
|
|
44
|
+
self.__capabilities[capability_id] = capability
|
|
45
|
+
|
|
46
|
+
def remove_capability(self, capability_id: str) -> None:
|
|
47
|
+
"""
|
|
48
|
+
Remove a capability from the Capabilities by its capability ID.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
capability_id (str): The ID of the capability to be removed.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
None
|
|
55
|
+
"""
|
|
56
|
+
if capability_id in self.__capabilities:
|
|
57
|
+
del self.__capabilities[capability_id]
|
|
58
|
+
|
|
59
|
+
def clear_capabilities(self) -> None:
|
|
60
|
+
"""
|
|
61
|
+
Remove all capabilities from the Capabilities.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
None
|
|
65
|
+
"""
|
|
66
|
+
self.__capabilities.clear()
|
|
67
|
+
|
|
68
|
+
def get_capability(self, capability_id: str) -> Optional[Capability]:
|
|
69
|
+
"""
|
|
70
|
+
Retrieve a capability from the Capabilities by its capability ID.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
capability_id (str): The unique identifier of the capability to retrieve.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Capability: The capability object if found, otherwise None.
|
|
77
|
+
"""
|
|
78
|
+
return self.__capabilities.get(capability_id)
|
|
79
|
+
|
|
80
|
+
def has_capability(self, capability_id: str) -> bool:
|
|
81
|
+
"""
|
|
82
|
+
Check if the Capabilities contains a capability with the specified ID.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
capability_id (str): The unique identifier of the capability to check.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
bool: True if the capability exists, otherwise False.
|
|
89
|
+
"""
|
|
90
|
+
return capability_id in self.__capabilities
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class Capability(GenericObject):
|
|
94
|
+
"""
|
|
95
|
+
Represents a capability of a Skoda vehicle.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
def __init__(self, capability_id: str, capabilities: Capabilities) -> None:
|
|
99
|
+
if capabilities is None:
|
|
100
|
+
raise ValueError('Cannot create capability without Capabilities')
|
|
101
|
+
if id is None:
|
|
102
|
+
raise ValueError('Capability ID cannot be None')
|
|
103
|
+
super().__init__(object_id=capability_id, parent=capabilities)
|
|
104
|
+
self.delay_notifications = True
|
|
105
|
+
self.capability_id = StringAttribute("id", self, capability_id)
|
|
106
|
+
self.statuses = list[Capability.Status]
|
|
107
|
+
self.enabled = True
|
|
108
|
+
self.delay_notifications = False
|
|
109
|
+
|
|
110
|
+
class Status(IntEnum):
|
|
111
|
+
"""
|
|
112
|
+
Enum for capability status.
|
|
113
|
+
"""
|
|
114
|
+
UNKNOWN = 0
|
|
115
|
+
DEACTIVATED = 1001
|
|
116
|
+
INITIALLY_DISABLED = 1003
|
|
117
|
+
DISABLED_BY_USER = 1004
|
|
118
|
+
OFFLINE_MODE = 1005
|
|
119
|
+
WORKSHOP_MODE = 1006
|
|
120
|
+
MISSING_OPERATION = 1007
|
|
121
|
+
MISSING_SERVICE = 1008
|
|
122
|
+
PLAY_PROTECTION = 1009
|
|
123
|
+
POWER_BUDGET_REACHED = 1010
|
|
124
|
+
DEEP_SLEEP = 1011
|
|
125
|
+
LOCATION_DATA_DISABLED = 1013
|
|
126
|
+
LICENSE_INACTIVE = 2001
|
|
127
|
+
LICENSE_EXPIRED = 2002
|
|
128
|
+
MISSING_LICENSE = 2003
|
|
129
|
+
USER_NOT_VERIFIED = 3001
|
|
130
|
+
TERMS_AND_CONDITIONS_NOT_ACCEPTED = 3002
|
|
131
|
+
INSUFFICIENT_RIGHTS = 3003
|
|
132
|
+
CONSENT_MISSING = 3004
|
|
133
|
+
LIMITED_FEATURE = 3005
|
|
134
|
+
AUTH_APP_CERT_ERROR = 3006
|
|
135
|
+
STATUS_UNSUPPORTED = 4001
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module for charging for skoda vehicles.
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from enum import Enum
|
|
8
|
+
|
|
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
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from typing import Optional, Dict
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SkodaCharging(Charging): # pylint: disable=too-many-instance-attributes
|
|
21
|
+
"""
|
|
22
|
+
SkodaCharging class for handling Skoda vehicle charging information.
|
|
23
|
+
|
|
24
|
+
This class extends the Charging class and includes an enumeration of various
|
|
25
|
+
charging states specific to Skoda vehicles.
|
|
26
|
+
"""
|
|
27
|
+
def __init__(self, vehicle: ElectricVehicle | None = None, origin: Optional[Charging] = None) -> None:
|
|
28
|
+
if origin is not None:
|
|
29
|
+
super().__init__(origin=origin)
|
|
30
|
+
self.settings: Charging.Settings = SkodaCharging.Settings(origin=origin.settings)
|
|
31
|
+
else:
|
|
32
|
+
super().__init__(vehicle=vehicle)
|
|
33
|
+
self.settings: Charging.Settings = SkodaCharging.Settings(origin=self.settings)
|
|
34
|
+
self.errors: Dict[str, Error] = {}
|
|
35
|
+
self.is_in_saved_location: BooleanAttribute = BooleanAttribute("is_in_saved_location", parent=self)
|
|
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__(origin=origin)
|
|
44
|
+
else:
|
|
45
|
+
super().__init__(parent=parent)
|
|
46
|
+
self.preferred_charge_mode: EnumAttribute = EnumAttribute("preferred_charge_mode", parent=self)
|
|
47
|
+
self.available_charge_modes: StringAttribute = StringAttribute("available_charge_modes", parent=self)
|
|
48
|
+
self.charging_care_mode: EnumAttribute = EnumAttribute("charging_care_mode", parent=self)
|
|
49
|
+
self.battery_support: EnumAttribute = EnumAttribute("battery_support", parent=self)
|
|
50
|
+
|
|
51
|
+
class SkodaChargingState(Enum,):
|
|
52
|
+
"""
|
|
53
|
+
Enum representing the various charging states for a Skoda vehicle.
|
|
54
|
+
|
|
55
|
+
Attributes:
|
|
56
|
+
OFF: The vehicle is not charging.
|
|
57
|
+
READY_FOR_CHARGING: The vehicle is ready to start charging.
|
|
58
|
+
NOT_READY_FOR_CHARGING: The vehicle is not ready to start charging.
|
|
59
|
+
CONSERVATION: The vehicle is in conservation mode.
|
|
60
|
+
CHARGE_PURPOSE_REACHED_NOT_CONSERVATION_CHARGING: The vehicle has reached its charging purpose and is not in conservation charging mode.
|
|
61
|
+
CHARGE_PURPOSE_REACHED_CONSERVATION: The vehicle has reached its charging purpose and is in conservation charging mode.
|
|
62
|
+
CHARGING: The vehicle is currently charging.
|
|
63
|
+
ERROR: There is an error in the charging process.
|
|
64
|
+
UNSUPPORTED: The charging state is unsupported.
|
|
65
|
+
DISCHARGING: The vehicle is discharging.
|
|
66
|
+
UNKNOWN: The charging state is unknown.
|
|
67
|
+
"""
|
|
68
|
+
OFF = 'off'
|
|
69
|
+
CONNECT_CABLE = 'connectCable'
|
|
70
|
+
READY_FOR_CHARGING = 'readyForCharging'
|
|
71
|
+
NOT_READY_FOR_CHARGING = 'notReadyForCharging'
|
|
72
|
+
CONSERVING = 'conserving'
|
|
73
|
+
CHARGE_PURPOSE_REACHED_NOT_CONSERVATION_CHARGING = 'chargePurposeReachedAndNotConservationCharging'
|
|
74
|
+
CHARGE_PURPOSE_REACHED_CONSERVATION = 'chargePurposeReachedAndConservation'
|
|
75
|
+
CHARGING = 'charging'
|
|
76
|
+
ERROR = 'error'
|
|
77
|
+
UNSUPPORTED = 'unsupported'
|
|
78
|
+
DISCHARGING = 'discharging'
|
|
79
|
+
UNKNOWN = 'unknown charging state'
|
|
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
|
+
|
|
122
|
+
|
|
123
|
+
# Mapping of Skoda charging states to generic charging states
|
|
124
|
+
mapping_skoda_charging_state: Dict[SkodaCharging.SkodaChargingState, Charging.ChargingState] = {
|
|
125
|
+
SkodaCharging.SkodaChargingState.OFF: Charging.ChargingState.OFF,
|
|
126
|
+
SkodaCharging.SkodaChargingState.CONNECT_CABLE: Charging.ChargingState.OFF,
|
|
127
|
+
SkodaCharging.SkodaChargingState.READY_FOR_CHARGING: Charging.ChargingState.READY_FOR_CHARGING,
|
|
128
|
+
SkodaCharging.SkodaChargingState.NOT_READY_FOR_CHARGING: Charging.ChargingState.OFF,
|
|
129
|
+
SkodaCharging.SkodaChargingState.CONSERVING: Charging.ChargingState.CONSERVATION,
|
|
130
|
+
SkodaCharging.SkodaChargingState.CHARGE_PURPOSE_REACHED_NOT_CONSERVATION_CHARGING: Charging.ChargingState.READY_FOR_CHARGING,
|
|
131
|
+
SkodaCharging.SkodaChargingState.CHARGE_PURPOSE_REACHED_CONSERVATION: Charging.ChargingState.CONSERVATION,
|
|
132
|
+
SkodaCharging.SkodaChargingState.CHARGING: Charging.ChargingState.CHARGING,
|
|
133
|
+
SkodaCharging.SkodaChargingState.ERROR: Charging.ChargingState.ERROR,
|
|
134
|
+
SkodaCharging.SkodaChargingState.UNSUPPORTED: Charging.ChargingState.UNSUPPORTED,
|
|
135
|
+
SkodaCharging.SkodaChargingState.DISCHARGING: Charging.ChargingState.DISCHARGING,
|
|
136
|
+
SkodaCharging.SkodaChargingState.UNKNOWN: Charging.ChargingState.UNKNOWN
|
|
137
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module for charging 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 ElectricVehicle
|
|
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: ElectricVehicle | None = None, origin: Optional[Climatization] = None) -> None:
|
|
25
|
+
if origin is not None:
|
|
26
|
+
super().__init__(origin=origin)
|
|
27
|
+
self.settings: Climatization.Settings = SkodaClimatization.Settings(origin=origin.settings)
|
|
28
|
+
else:
|
|
29
|
+
super().__init__(vehicle=vehicle)
|
|
30
|
+
self.settings: Climatization.Settings = SkodaClimatization.Settings(origin=self.settings)
|
|
31
|
+
self.errors: Dict[str, Error] = {}
|
|
32
|
+
|
|
33
|
+
class Settings(Climatization.Settings):
|
|
34
|
+
"""
|
|
35
|
+
This class represents the settings for a skoda car climatiation.
|
|
36
|
+
"""
|
|
37
|
+
def __init__(self, parent: Optional[GenericObject] = None, origin: Optional[Climatization.Settings] = None) -> None:
|
|
38
|
+
if origin is not None:
|
|
39
|
+
super().__init__(origin=origin)
|
|
40
|
+
else:
|
|
41
|
+
super().__init__(parent=parent)
|
|
@@ -0,0 +1,74 @@
|
|
|
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")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SpinCommand(GenericCommand):
|
|
21
|
+
"""
|
|
22
|
+
LockUnlockCommand is a command class for locking and unlocking the vehicle.
|
|
23
|
+
|
|
24
|
+
Command (Enum): Enum class representing different commands for locking.
|
|
25
|
+
|
|
26
|
+
"""
|
|
27
|
+
def __init__(self, name: str = 'spin', parent: Optional[GenericObject] = None) -> None:
|
|
28
|
+
super().__init__(name=name, parent=parent)
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def value(self) -> Optional[Union[str, Dict]]:
|
|
32
|
+
return super().value
|
|
33
|
+
|
|
34
|
+
@value.setter
|
|
35
|
+
def value(self, new_value: Optional[Union[str, Dict]]) -> None:
|
|
36
|
+
if isinstance(new_value, str):
|
|
37
|
+
parser = ThrowingArgumentParser(prog='', add_help=False, exit_on_error=False)
|
|
38
|
+
parser.add_argument('command', help='Command to execute', type=SpinCommand.Command,
|
|
39
|
+
choices=list(SpinCommand.Command))
|
|
40
|
+
parser.add_argument('--spin', dest='spin', help='Spin to be used instead of spin from config or .netrc', type=str, required=False,
|
|
41
|
+
default=None)
|
|
42
|
+
try:
|
|
43
|
+
args = parser.parse_args(new_value.split(sep=' '))
|
|
44
|
+
except argparse.ArgumentError as e:
|
|
45
|
+
raise SetterError(f'Invalid format for SpinCommand: {e.message} {parser.format_usage()}') from e
|
|
46
|
+
|
|
47
|
+
newvalue_dict = {}
|
|
48
|
+
newvalue_dict['command'] = args.command
|
|
49
|
+
if args.spin is not None:
|
|
50
|
+
newvalue_dict['spin'] = args.spin
|
|
51
|
+
new_value = newvalue_dict
|
|
52
|
+
elif isinstance(new_value, dict):
|
|
53
|
+
if 'command' in new_value and isinstance(new_value['command'], str):
|
|
54
|
+
if new_value['command'] in SpinCommand.Command:
|
|
55
|
+
new_value['command'] = SpinCommand.Command(new_value['command'])
|
|
56
|
+
else:
|
|
57
|
+
raise ValueError('Invalid value for SpinCommand. '
|
|
58
|
+
f'Command must be one of {SpinCommand.Command}')
|
|
59
|
+
if self._is_changeable:
|
|
60
|
+
for hook in self._on_set_hooks:
|
|
61
|
+
new_value = hook(self, new_value)
|
|
62
|
+
self._set_value(new_value)
|
|
63
|
+
else:
|
|
64
|
+
raise TypeError('You cannot set this attribute. Attribute is not mutable.')
|
|
65
|
+
|
|
66
|
+
class Command(Enum):
|
|
67
|
+
"""
|
|
68
|
+
Enum class representing different commands for SPIN.
|
|
69
|
+
|
|
70
|
+
"""
|
|
71
|
+
VERIFY = 'verify'
|
|
72
|
+
|
|
73
|
+
def __str__(self) -> str:
|
|
74
|
+
return self.value
|