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,28 +1,39 @@
|
|
|
1
|
-
"""Module implements the connector to interact with the Skoda API."""
|
|
1
|
+
"""Module implements the connector to interact with the Skoda API.""" # pylint: disable=too-many-lines
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
5
|
import threading
|
|
6
6
|
import os
|
|
7
|
+
import traceback
|
|
7
8
|
import logging
|
|
8
9
|
import netrc
|
|
9
10
|
from datetime import datetime, timedelta, timezone
|
|
11
|
+
import json
|
|
12
|
+
|
|
10
13
|
import requests
|
|
11
14
|
|
|
15
|
+
|
|
12
16
|
from carconnectivity.garage import Garage
|
|
13
17
|
from carconnectivity.vehicle import GenericVehicle
|
|
14
18
|
from carconnectivity.errors import AuthenticationError, TooManyRequestsError, RetrievalError, APIError, APICompatibilityError, \
|
|
15
|
-
TemporaryAuthenticationError,
|
|
19
|
+
TemporaryAuthenticationError, SetterError, CommandError
|
|
16
20
|
from carconnectivity.util import robust_time_parse, log_extra_keys, config_remove_credentials
|
|
17
21
|
from carconnectivity.units import Length, Speed, Power, Temperature
|
|
18
22
|
from carconnectivity.doors import Doors
|
|
19
23
|
from carconnectivity.windows import Windows
|
|
20
24
|
from carconnectivity.lights import Lights
|
|
21
|
-
from carconnectivity.drive import GenericDrive, ElectricDrive, CombustionDrive
|
|
22
|
-
from carconnectivity.attributes import BooleanAttribute, DurationAttribute
|
|
25
|
+
from carconnectivity.drive import GenericDrive, ElectricDrive, CombustionDrive, DieselDrive
|
|
26
|
+
from carconnectivity.attributes import BooleanAttribute, DurationAttribute, TemperatureAttribute, EnumAttribute, LevelAttribute, \
|
|
27
|
+
CurrentAttribute
|
|
23
28
|
from carconnectivity.charging import Charging
|
|
24
29
|
from carconnectivity.position import Position
|
|
25
30
|
from carconnectivity.climatization import Climatization
|
|
31
|
+
from carconnectivity.charging_connector import ChargingConnector
|
|
32
|
+
from carconnectivity.commands import Commands
|
|
33
|
+
from carconnectivity.command_impl import ClimatizationStartStopCommand, ChargingStartStopCommand, HonkAndFlashCommand, LockUnlockCommand, WakeSleepCommand, \
|
|
34
|
+
WindowHeatingStartStopCommand
|
|
35
|
+
from carconnectivity.enums import ConnectionState
|
|
36
|
+
from carconnectivity.window_heating import WindowHeatings
|
|
26
37
|
|
|
27
38
|
from carconnectivity_connectors.base.connector import BaseConnector
|
|
28
39
|
from carconnectivity_connectors.skoda.auth.session_manager import SessionManager, SessionUser, Service
|
|
@@ -30,11 +41,24 @@ from carconnectivity_connectors.skoda.auth.my_skoda_session import MySkodaSessio
|
|
|
30
41
|
from carconnectivity_connectors.skoda.vehicle import SkodaVehicle, SkodaElectricVehicle, SkodaCombustionVehicle, SkodaHybridVehicle
|
|
31
42
|
from carconnectivity_connectors.skoda.capability import Capability
|
|
32
43
|
from carconnectivity_connectors.skoda.charging import SkodaCharging, mapping_skoda_charging_state
|
|
44
|
+
from carconnectivity_connectors.skoda.climatization import SkodaClimatization
|
|
45
|
+
from carconnectivity_connectors.skoda.error import Error
|
|
33
46
|
from carconnectivity_connectors.skoda._version import __version__
|
|
34
47
|
from carconnectivity_connectors.skoda.mqtt_client import SkodaMQTTClient
|
|
48
|
+
from carconnectivity_connectors.skoda.command_impl import SpinCommand
|
|
49
|
+
|
|
50
|
+
SUPPORT_IMAGES = False
|
|
51
|
+
try:
|
|
52
|
+
from PIL import Image
|
|
53
|
+
import base64
|
|
54
|
+
import io
|
|
55
|
+
SUPPORT_IMAGES = True
|
|
56
|
+
from carconnectivity.attributes import ImageAttribute
|
|
57
|
+
except ImportError:
|
|
58
|
+
pass
|
|
35
59
|
|
|
36
60
|
if TYPE_CHECKING:
|
|
37
|
-
from typing import Dict, List, Optional, Any
|
|
61
|
+
from typing import Dict, List, Optional, Any, Set, Union
|
|
38
62
|
|
|
39
63
|
from carconnectivity.carconnectivity import CarConnectivity
|
|
40
64
|
|
|
@@ -52,7 +76,7 @@ class Connector(BaseConnector):
|
|
|
52
76
|
max_age (Optional[int]): Maximum age for cached data in seconds.
|
|
53
77
|
"""
|
|
54
78
|
def __init__(self, connector_id: str, car_connectivity: CarConnectivity, config: Dict) -> None:
|
|
55
|
-
BaseConnector.__init__(self, connector_id=connector_id, car_connectivity=car_connectivity, config=config)
|
|
79
|
+
BaseConnector.__init__(self, connector_id=connector_id, car_connectivity=car_connectivity, config=config, log=LOG, api_log=LOG_API)
|
|
56
80
|
|
|
57
81
|
self._mqtt_client: SkodaMQTTClient = SkodaMQTTClient(skoda_connector=self)
|
|
58
82
|
|
|
@@ -60,74 +84,79 @@ class Connector(BaseConnector):
|
|
|
60
84
|
self._background_connect_thread: Optional[threading.Thread] = None
|
|
61
85
|
self._stop_event = threading.Event()
|
|
62
86
|
|
|
63
|
-
self.
|
|
64
|
-
|
|
87
|
+
self.connection_state: EnumAttribute = EnumAttribute(name="connection_state", parent=self, value_type=ConnectionState,
|
|
88
|
+
value=ConnectionState.DISCONNECTED, tags={'connector_custom'})
|
|
89
|
+
self.rest_connected: bool = False
|
|
90
|
+
self.mqtt_connected: bool = False
|
|
91
|
+
self.interval: DurationAttribute = DurationAttribute(name="interval", parent=self, tags={'connector_custom'})
|
|
92
|
+
self.interval.minimum = timedelta(seconds=180)
|
|
93
|
+
self.interval._is_changeable = True # pylint: disable=protected-access
|
|
94
|
+
|
|
95
|
+
self.commands: Commands = Commands(parent=self)
|
|
65
96
|
|
|
66
97
|
self.user_id: Optional[str] = None
|
|
67
98
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
config['api_log_level'] = config['api_log_level'].upper()
|
|
81
|
-
if config['api_log_level'] in logging.getLevelNamesMapping():
|
|
82
|
-
LOG_API.setLevel(config['api_log_level'])
|
|
83
|
-
else:
|
|
84
|
-
raise ConfigurationError(f'Invalid log level: "{config["log_level"]}" not in {list(logging.getLevelNamesMapping().keys())}')
|
|
85
|
-
LOG.info("Loading skoda connector with config %s", config_remove_credentials(self.config))
|
|
86
|
-
|
|
87
|
-
username: Optional[str] = None
|
|
88
|
-
password: Optional[str] = None
|
|
89
|
-
if 'username' in self.config and 'password' in self.config:
|
|
90
|
-
username = self.config['username']
|
|
91
|
-
password = self.config['password']
|
|
99
|
+
LOG.info("Loading skoda connector with config %s", config_remove_credentials(config))
|
|
100
|
+
|
|
101
|
+
if 'spin' in config and config['spin'] is not None:
|
|
102
|
+
self.active_config['spin'] = config['spin']
|
|
103
|
+
else:
|
|
104
|
+
self.active_config['spin'] = None
|
|
105
|
+
|
|
106
|
+
self.active_config['username'] = None
|
|
107
|
+
self.active_config['password'] = None
|
|
108
|
+
if 'username' in config and 'password' in config:
|
|
109
|
+
self.active_config['username'] = config['username']
|
|
110
|
+
self.active_config['password'] = config['password']
|
|
92
111
|
else:
|
|
93
|
-
if 'netrc' in
|
|
94
|
-
|
|
112
|
+
if 'netrc' in config:
|
|
113
|
+
self.active_config['netrc'] = config['netrc']
|
|
95
114
|
else:
|
|
96
|
-
|
|
115
|
+
self.active_config['netrc'] = os.path.join(os.path.expanduser("~"), ".netrc")
|
|
97
116
|
try:
|
|
98
|
-
secrets = netrc.netrc(file=
|
|
117
|
+
secrets = netrc.netrc(file=self.active_config['netrc'])
|
|
99
118
|
secret: tuple[str, str, str] | None = secrets.authenticators("skoda")
|
|
100
119
|
if secret is None:
|
|
101
|
-
raise AuthenticationError(f'Authentication using {
|
|
102
|
-
username,
|
|
120
|
+
raise AuthenticationError(f'Authentication using {self.active_config["netrc"]} failed: skoda not found in netrc')
|
|
121
|
+
self.active_config['username'], account, self.active_config['password'] = secret
|
|
122
|
+
|
|
123
|
+
if self.active_config['spin'] is None and account is not None:
|
|
124
|
+
try:
|
|
125
|
+
self.active_config['spin'] = account
|
|
126
|
+
except ValueError as err:
|
|
127
|
+
LOG.error('Could not parse spin from netrc: %s', err)
|
|
103
128
|
except netrc.NetrcParseError as err:
|
|
104
|
-
LOG.error('Authentification using %s failed: %s',
|
|
105
|
-
raise AuthenticationError(f'Authentication using {
|
|
129
|
+
LOG.error('Authentification using %s failed: %s', self.active_config['netrc'], err)
|
|
130
|
+
raise AuthenticationError(f'Authentication using {self.active_config["netrc"]} failed: {err}') from err
|
|
106
131
|
except TypeError as err:
|
|
107
|
-
if 'username' not in
|
|
108
|
-
raise AuthenticationError(f'"skoda" entry was not found in {
|
|
132
|
+
if 'username' not in config:
|
|
133
|
+
raise AuthenticationError(f'"skoda" entry was not found in {self.active_config["netrc"]} netrc-file.'
|
|
109
134
|
' Create it or provide username and password in config') from err
|
|
110
135
|
except FileNotFoundError as err:
|
|
111
|
-
raise AuthenticationError(f'{
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
136
|
+
raise AuthenticationError(f'{self.active_config["netrc"]} netrc-file was not found. Create it or provide username and password in config') \
|
|
137
|
+
from err
|
|
138
|
+
|
|
139
|
+
self.active_config['interval'] = 300
|
|
140
|
+
if 'interval' in config:
|
|
141
|
+
self.active_config['interval'] = config['interval']
|
|
142
|
+
if self.active_config['interval'] < 180:
|
|
143
|
+
raise ValueError('Intervall must be at least 180 seconds')
|
|
144
|
+
self.active_config['max_age'] = self.active_config['interval'] - 1
|
|
145
|
+
if 'max_age' in config:
|
|
146
|
+
self.active_config['max_age'] = config['max_age']
|
|
147
|
+
self.interval._set_value(timedelta(seconds=self.active_config['interval'])) # pylint: disable=protected-access
|
|
148
|
+
|
|
149
|
+
if self.active_config['username'] is None or self.active_config['password'] is None:
|
|
124
150
|
raise AuthenticationError('Username or password not provided')
|
|
125
151
|
|
|
126
152
|
self._manager: SessionManager = SessionManager(tokenstore=car_connectivity.get_tokenstore(), cache=car_connectivity.get_cache())
|
|
127
|
-
session: requests.Session = self._manager.get_session(Service.MY_SKODA, SessionUser(username=username,
|
|
153
|
+
session: requests.Session = self._manager.get_session(Service.MY_SKODA, SessionUser(username=self.active_config['username'],
|
|
154
|
+
password=self.active_config['password']))
|
|
128
155
|
if not isinstance(session, MySkodaSession):
|
|
129
156
|
raise AuthenticationError('Could not create session')
|
|
130
157
|
self.session: MySkodaSession = session
|
|
158
|
+
self.session.retries = 3
|
|
159
|
+
self.session.timeout = 180
|
|
131
160
|
self.session.refresh()
|
|
132
161
|
|
|
133
162
|
self._elapsed: List[timedelta] = []
|
|
@@ -136,12 +165,15 @@ class Connector(BaseConnector):
|
|
|
136
165
|
self._stop_event.clear()
|
|
137
166
|
# Start background thread for Rest API polling
|
|
138
167
|
self._background_thread = threading.Thread(target=self._background_loop, daemon=False)
|
|
168
|
+
self._background_thread.name = 'carconnectivity.connectors.skoda-background'
|
|
139
169
|
self._background_thread.start()
|
|
140
170
|
# Start background thread for MQTT connection
|
|
141
171
|
self._background_connect_thread = threading.Thread(target=self._background_connect_loop, daemon=False)
|
|
172
|
+
self._background_connect_thread.name = 'carconnectivity.connectors.skoda-background_connect'
|
|
142
173
|
self._background_connect_thread.start()
|
|
143
174
|
# Start MQTT thread
|
|
144
175
|
self._mqtt_client.loop_start()
|
|
176
|
+
self.healthy._set_value(value=True) # pylint: disable=protected-access
|
|
145
177
|
|
|
146
178
|
def _background_connect_loop(self) -> None:
|
|
147
179
|
while not self._stop_event.is_set():
|
|
@@ -155,6 +187,7 @@ class Connector(BaseConnector):
|
|
|
155
187
|
def _background_loop(self) -> None:
|
|
156
188
|
self._stop_event.clear()
|
|
157
189
|
fetch: bool = True
|
|
190
|
+
self.connection_state._set_value(value=ConnectionState.CONNECTING) # pylint: disable=protected-access
|
|
158
191
|
while not self._stop_event.is_set():
|
|
159
192
|
interval = 300
|
|
160
193
|
try:
|
|
@@ -166,25 +199,50 @@ class Connector(BaseConnector):
|
|
|
166
199
|
self.update_vehicles()
|
|
167
200
|
self.last_update._set_value(value=datetime.now(tz=timezone.utc)) # pylint: disable=protected-access
|
|
168
201
|
if self.interval.value is not None:
|
|
169
|
-
interval:
|
|
202
|
+
interval: float = self.interval.value.total_seconds()
|
|
170
203
|
except Exception:
|
|
171
204
|
if self.interval.value is not None:
|
|
172
|
-
interval:
|
|
205
|
+
interval: float = self.interval.value.total_seconds()
|
|
173
206
|
raise
|
|
174
207
|
except TooManyRequestsError as err:
|
|
175
208
|
LOG.error('Retrieval error during update. Too many requests from your account (%s). Will try again after 15 minutes', str(err))
|
|
209
|
+
self.connection_state._set_value(value=ConnectionState.ERROR) # pylint: disable=protected-access
|
|
210
|
+
self.rest_connected = False
|
|
176
211
|
self._stop_event.wait(900)
|
|
177
212
|
except RetrievalError as err:
|
|
178
213
|
LOG.error('Retrieval error during update (%s). Will try again after configured interval of %ss', str(err), interval)
|
|
214
|
+
self.connection_state._set_value(value=ConnectionState.ERROR) # pylint: disable=protected-access
|
|
215
|
+
self.rest_connected = False
|
|
216
|
+
self._stop_event.wait(interval)
|
|
217
|
+
except APIError as err:
|
|
218
|
+
LOG.error('API error during update (%s). Will try again after configured interval of %ss', str(err), interval)
|
|
219
|
+
self.connection_state._set_value(value=ConnectionState.ERROR) # pylint: disable=protected-access
|
|
220
|
+
self.rest_connected = False
|
|
179
221
|
self._stop_event.wait(interval)
|
|
180
222
|
except APICompatibilityError as err:
|
|
181
223
|
LOG.error('API compatability error during update (%s). Will try again after configured interval of %ss', str(err), interval)
|
|
224
|
+
self.connection_state._set_value(value=ConnectionState.ERROR) # pylint: disable=protected-access
|
|
225
|
+
self.rest_connected = False
|
|
182
226
|
self._stop_event.wait(interval)
|
|
183
227
|
except TemporaryAuthenticationError as err:
|
|
184
228
|
LOG.error('Temporary authentification error during update (%s). Will try again after configured interval of %ss', str(err), interval)
|
|
229
|
+
self.connection_state._set_value(value=ConnectionState.ERROR) # pylint: disable=protected-access
|
|
230
|
+
self.rest_connected = False
|
|
185
231
|
self._stop_event.wait(interval)
|
|
232
|
+
except Exception as err:
|
|
233
|
+
LOG.critical('Critical error during update: %s', traceback.format_exc())
|
|
234
|
+
self.connection_state._set_value(value=ConnectionState.ERROR) # pylint: disable=protected-access
|
|
235
|
+
self.rest_connected = False
|
|
236
|
+
self.healthy._set_value(value=False) # pylint: disable=protected-access
|
|
237
|
+
raise err
|
|
186
238
|
else:
|
|
239
|
+
self.rest_connected = True
|
|
240
|
+
if self.mqtt_connected:
|
|
241
|
+
self.connection_state._set_value(value=ConnectionState.CONNECTED) # pylint: disable=protected-access
|
|
187
242
|
self._stop_event.wait(interval)
|
|
243
|
+
# When leaving the loop, set the connection state to disconnected
|
|
244
|
+
self.connection_state._set_value(value=ConnectionState.DISCONNECTED) # pylint: disable=protected-access
|
|
245
|
+
self.rest_connected = False
|
|
188
246
|
|
|
189
247
|
def persist(self) -> None:
|
|
190
248
|
"""
|
|
@@ -215,12 +273,12 @@ class Connector(BaseConnector):
|
|
|
215
273
|
self.car_connectivity.garage.remove_vehicle(vehicle.id)
|
|
216
274
|
vehicle.enabled = False
|
|
217
275
|
self._stop_event.set()
|
|
276
|
+
self.session.close()
|
|
218
277
|
if self._background_thread is not None:
|
|
219
278
|
self._background_thread.join()
|
|
220
279
|
if self._background_connect_thread is not None:
|
|
221
280
|
self._background_connect_thread.join()
|
|
222
281
|
self.persist()
|
|
223
|
-
self.session.close()
|
|
224
282
|
return super().shutdown()
|
|
225
283
|
|
|
226
284
|
def fetch_all(self) -> None:
|
|
@@ -229,6 +287,12 @@ class Connector(BaseConnector):
|
|
|
229
287
|
|
|
230
288
|
This method calls the `fetch_vehicles` method to retrieve vehicle data.
|
|
231
289
|
"""
|
|
290
|
+
# Add spin command
|
|
291
|
+
if self.commands is not None and not self.commands.contains_command('spin'):
|
|
292
|
+
spin_command = SpinCommand(parent=self.commands)
|
|
293
|
+
spin_command._add_on_set_hook(self.__on_spin) # pylint: disable=protected-access
|
|
294
|
+
spin_command.enabled = True
|
|
295
|
+
self.commands.add_command(spin_command)
|
|
232
296
|
self.fetch_vehicles()
|
|
233
297
|
self.car_connectivity.transaction_end()
|
|
234
298
|
|
|
@@ -264,6 +328,9 @@ class Connector(BaseConnector):
|
|
|
264
328
|
if 'vehicles' in data and data['vehicles'] is not None:
|
|
265
329
|
for vehicle_dict in data['vehicles']:
|
|
266
330
|
if 'vin' in vehicle_dict and vehicle_dict['vin'] is not None:
|
|
331
|
+
if vehicle_dict['vin'] in self.active_config['hide_vins']:
|
|
332
|
+
LOG.info('Vehicle %s filtered out due to configuration', vehicle_dict['vin'])
|
|
333
|
+
continue
|
|
267
334
|
seen_vehicle_vins.add(vehicle_dict['vin'])
|
|
268
335
|
vehicle: Optional[SkodaVehicle] = garage.get_vehicle(vehicle_dict['vin']) # pyright: ignore[reportAssignmentType]
|
|
269
336
|
if not vehicle:
|
|
@@ -275,9 +342,16 @@ class Connector(BaseConnector):
|
|
|
275
342
|
else:
|
|
276
343
|
vehicle.license_plate._set_value(None) # pylint: disable=protected-access
|
|
277
344
|
|
|
278
|
-
|
|
345
|
+
if 'name' in vehicle_dict and vehicle_dict['name'] is not None:
|
|
346
|
+
vehicle.name._set_value(vehicle_dict['name']) # pylint: disable=protected-access
|
|
347
|
+
else:
|
|
348
|
+
vehicle.name._set_value(None) # pylint: disable=protected-access
|
|
349
|
+
|
|
350
|
+
log_extra_keys(LOG_API, 'vehicles', vehicle_dict, {'vin', 'licensePlate', 'name'})
|
|
279
351
|
|
|
280
352
|
vehicle = self.fetch_vehicle_details(vehicle)
|
|
353
|
+
if SUPPORT_IMAGES:
|
|
354
|
+
vehicle = self.fetch_vehicle_images(vehicle)
|
|
281
355
|
else:
|
|
282
356
|
raise APIError('Could not parse vehicle, vin missing')
|
|
283
357
|
for vin in set(garage.list_vehicle_vins()) - seen_vehicle_vins:
|
|
@@ -302,15 +376,46 @@ class Connector(BaseConnector):
|
|
|
302
376
|
for vin in set(garage.list_vehicle_vins()):
|
|
303
377
|
vehicle_to_update: Optional[GenericVehicle] = garage.get_vehicle(vin)
|
|
304
378
|
if vehicle_to_update is not None and isinstance(vehicle_to_update, SkodaVehicle) and vehicle_to_update.is_managed_by_connector(self):
|
|
305
|
-
vehicle_to_update = self.
|
|
306
|
-
vehicle_to_update = self.fetch_driving_range(vehicle_to_update)
|
|
379
|
+
vehicle_to_update = self.fetch_vehicle_status(vehicle_to_update)
|
|
307
380
|
if vehicle_to_update.capabilities is not None and vehicle_to_update.capabilities.enabled:
|
|
308
|
-
if vehicle_to_update.capabilities.has_capability('
|
|
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):
|
|
383
|
+
vehicle_to_update = self.fetch_driving_range(vehicle_to_update)
|
|
384
|
+
if vehicle_to_update.capabilities.has_capability('READINESS', check_status_ok=True):
|
|
385
|
+
vehicle_to_update = self.fetch_connection_status(vehicle_to_update)
|
|
386
|
+
if vehicle_to_update.capabilities.has_capability('PARKING_POSITION', check_status_ok=True):
|
|
309
387
|
vehicle_to_update = self.fetch_position(vehicle_to_update)
|
|
310
|
-
if vehicle_to_update.capabilities.has_capability('CHARGING') and isinstance(vehicle_to_update, SkodaElectricVehicle):
|
|
388
|
+
if vehicle_to_update.capabilities.has_capability('CHARGING', check_status_ok=True) and isinstance(vehicle_to_update, SkodaElectricVehicle):
|
|
311
389
|
vehicle_to_update = self.fetch_charging(vehicle_to_update)
|
|
312
|
-
if vehicle_to_update.capabilities.has_capability('AIR_CONDITIONING'):
|
|
390
|
+
if vehicle_to_update.capabilities.has_capability('AIR_CONDITIONING', check_status_ok=True):
|
|
313
391
|
vehicle_to_update = self.fetch_air_conditioning(vehicle_to_update)
|
|
392
|
+
if vehicle_to_update.capabilities.has_capability('VEHICLE_HEALTH_INSPECTION', check_status_ok=True):
|
|
393
|
+
vehicle_to_update = self.fetch_maintenance(vehicle_to_update)
|
|
394
|
+
vehicle_to_update = self.decide_state(vehicle_to_update)
|
|
395
|
+
self.car_connectivity.transaction_end()
|
|
396
|
+
|
|
397
|
+
def decide_state(self, vehicle: SkodaVehicle) -> SkodaVehicle:
|
|
398
|
+
"""
|
|
399
|
+
Decides the state of the vehicle based on the current data.
|
|
400
|
+
|
|
401
|
+
Args:
|
|
402
|
+
vehicle (SkodaVehicle): The Skoda vehicle object.
|
|
403
|
+
|
|
404
|
+
Returns:
|
|
405
|
+
SkodaVehicle: The Skoda vehicle object with the updated state.
|
|
406
|
+
"""
|
|
407
|
+
if vehicle is not None:
|
|
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:
|
|
412
|
+
vehicle.state._set_value(GenericVehicle.State.IGNITION_ON) # pylint: disable=protected-access
|
|
413
|
+
elif vehicle.position is not None and vehicle.position.enabled and vehicle.position.position_type is not None \
|
|
414
|
+
and vehicle.position.position_type.enabled and vehicle.position.position_type.value == Position.PositionType.PARKING:
|
|
415
|
+
vehicle.state._set_value(GenericVehicle.State.PARKED) # pylint: disable=protected-access
|
|
416
|
+
else:
|
|
417
|
+
vehicle.state._set_value(GenericVehicle.State.UNKNOWN) # pylint: disable=protected-access
|
|
418
|
+
return vehicle
|
|
314
419
|
|
|
315
420
|
def fetch_charging(self, vehicle: SkodaElectricVehicle, no_cache: bool = False) -> SkodaElectricVehicle:
|
|
316
421
|
"""
|
|
@@ -331,10 +436,21 @@ class Connector(BaseConnector):
|
|
|
331
436
|
url = f'https://mysmob.api.connect.skoda-auto.cz/api/v1/charging/{vin}'
|
|
332
437
|
data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
|
|
333
438
|
if data is not None:
|
|
439
|
+
if not vehicle.charging.commands.contains_command('start-stop'):
|
|
440
|
+
start_stop_command: ChargingStartStopCommand = ChargingStartStopCommand(parent=vehicle.charging.commands)
|
|
441
|
+
start_stop_command._add_on_set_hook(self.__on_charging_start_stop) # pylint: disable=protected-access
|
|
442
|
+
start_stop_command.enabled = True
|
|
443
|
+
vehicle.charging.commands.add_command(start_stop_command)
|
|
334
444
|
if 'carCapturedTimestamp' in data and data['carCapturedTimestamp'] is not None:
|
|
335
445
|
captured_at: datetime = robust_time_parse(data['carCapturedTimestamp'])
|
|
336
446
|
else:
|
|
337
447
|
raise APIError('Could not fetch charging, carCapturedTimestamp missing')
|
|
448
|
+
if 'isVehicleInSavedLocation' in data and data['isVehicleInSavedLocation'] is not None:
|
|
449
|
+
if vehicle.charging is not None:
|
|
450
|
+
if not isinstance(vehicle.charging, SkodaCharging):
|
|
451
|
+
vehicle.charging = SkodaCharging(origin=vehicle.charging)
|
|
452
|
+
# pylint: disable-next=protected-access
|
|
453
|
+
vehicle.charging.is_in_saved_location._set_value(data['isVehicleInSavedLocation'], measured=captured_at)
|
|
338
454
|
if 'status' in data and data['status'] is not None:
|
|
339
455
|
if 'state' in data['status'] and data['status']['state'] is not None:
|
|
340
456
|
if data['status']['state'] in [item.name for item in SkodaCharging.SkodaChargingState]:
|
|
@@ -361,15 +477,201 @@ class Connector(BaseConnector):
|
|
|
361
477
|
if 'remainingTimeToFullyChargedInMinutes' in data['status'] and data['status']['remainingTimeToFullyChargedInMinutes'] is not None:
|
|
362
478
|
remaining_duration: timedelta = timedelta(minutes=data['status']['remainingTimeToFullyChargedInMinutes'])
|
|
363
479
|
estimated_date_reached: datetime = captured_at + remaining_duration
|
|
480
|
+
estimated_date_reached = estimated_date_reached.replace(second=0, microsecond=0)
|
|
364
481
|
# pylint: disable-next=protected-access
|
|
365
482
|
vehicle.charging.estimated_date_reached._set_value(value=estimated_date_reached, measured=captured_at)
|
|
366
483
|
else:
|
|
367
484
|
vehicle.charging.estimated_date_reached._set_value(None, measured=captured_at) # pylint: disable=protected-access
|
|
485
|
+
if 'chargeType' in data['status'] and data['status']['chargeType'] is not None:
|
|
486
|
+
if data['status']['chargeType'] in [item.name for item in Charging.ChargingType]:
|
|
487
|
+
charge_type: Charging.ChargingType = Charging.ChargingType[data['status']['chargeType']]
|
|
488
|
+
else:
|
|
489
|
+
LOG_API.info('Unknown charge type %s not in %s', data['status']['chargeType'], str(Charging.ChargingType))
|
|
490
|
+
charge_type = Charging.ChargingType.UNKNOWN
|
|
491
|
+
# pylint: disable-next=protected-access
|
|
492
|
+
vehicle.charging.type._set_value(value=charge_type, measured=captured_at)
|
|
493
|
+
else:
|
|
494
|
+
# pylint: disable-next=protected-access
|
|
495
|
+
vehicle.charging.type._set_value(None, measured=captured_at)
|
|
496
|
+
if 'battery' in data['status'] and data['status']['battery'] is not None:
|
|
497
|
+
for drive in vehicle.drives.drives.values():
|
|
498
|
+
# Assume first electric drive is the right one
|
|
499
|
+
if isinstance(drive, ElectricDrive):
|
|
500
|
+
if 'remainingCruisingRangeInMeters' in data['status']['battery'] \
|
|
501
|
+
and data['status']['battery']['remainingCruisingRangeInMeters'] is not None:
|
|
502
|
+
cruising_range_in_km: float = data['status']['battery']['remainingCruisingRangeInMeters'] / 1000
|
|
503
|
+
# pylint: disable-next=protected-access
|
|
504
|
+
drive.range._set_value(value=cruising_range_in_km, measured=captured_at, unit=Length.KM)
|
|
505
|
+
drive.range.precision = 1
|
|
506
|
+
if 'stateOfChargeInPercent' in data['status']['battery'] \
|
|
507
|
+
and data['status']['battery']['stateOfChargeInPercent'] is not None:
|
|
508
|
+
# pylint: disable-next=protected-access
|
|
509
|
+
drive.level._set_value(value=data['status']['battery']['stateOfChargeInPercent'], measured=captured_at)
|
|
510
|
+
drive.level.precision = 1
|
|
511
|
+
log_extra_keys(LOG_API, 'status', data['status']['battery'], {'remainingCruisingRangeInMeters',
|
|
512
|
+
'stateOfChargeInPercent'})
|
|
513
|
+
break
|
|
368
514
|
log_extra_keys(LOG_API, 'status', data['status'], {'chargingRateInKilometersPerHour',
|
|
369
515
|
'chargePowerInKw',
|
|
370
516
|
'remainingTimeToFullyChargedInMinutes',
|
|
371
|
-
'state'
|
|
372
|
-
|
|
517
|
+
'state',
|
|
518
|
+
'chargeType',
|
|
519
|
+
'battery'})
|
|
520
|
+
if 'settings' in data and data['settings'] is not None:
|
|
521
|
+
if 'targetStateOfChargeInPercent' in data['settings'] and data['settings']['targetStateOfChargeInPercent'] is not None \
|
|
522
|
+
and vehicle.charging is not None and vehicle.charging.settings is not None:
|
|
523
|
+
vehicle.charging.settings.target_level.minimum = 50.0
|
|
524
|
+
vehicle.charging.settings.target_level.maximum = 100.0
|
|
525
|
+
vehicle.charging.settings.target_level.precision = 10.0
|
|
526
|
+
vehicle.charging.settings.target_level._add_on_set_hook(self.__on_charging_target_level_change) # pylint: disable=protected-access
|
|
527
|
+
vehicle.charging.settings.target_level._is_changeable = True # pylint: disable=protected-access
|
|
528
|
+
# pylint: disable-next=protected-access
|
|
529
|
+
vehicle.charging.settings.target_level._set_value(value=data['settings']['targetStateOfChargeInPercent'], measured=captured_at)
|
|
530
|
+
else:
|
|
531
|
+
vehicle.charging.settings.target_level._set_value(None, measured=captured_at) # pylint: disable=protected-access
|
|
532
|
+
if 'maxChargeCurrentAc' in data['settings'] and data['settings']['maxChargeCurrentAc'] is not None \
|
|
533
|
+
and vehicle.charging is not None and vehicle.charging.settings is not None:
|
|
534
|
+
vehicle.charging.settings.maximum_current.minimum = 6.0
|
|
535
|
+
vehicle.charging.settings.maximum_current.maximum = 16.0
|
|
536
|
+
vehicle.charging.settings.maximum_current.precision = 1.0
|
|
537
|
+
vehicle.charging.settings.maximum_current._add_on_set_hook(self.__on_charging_maximum_current_change) # pylint: disable=protected-access
|
|
538
|
+
vehicle.charging.settings.maximum_current._is_changeable = True # pylint: disable=protected-access
|
|
539
|
+
if data['settings']['maxChargeCurrentAc'] == 'MAXIMUM':
|
|
540
|
+
vehicle.charging.settings.maximum_current._set_value(value=16, measured=captured_at) # pylint: disable=protected-access
|
|
541
|
+
elif data['settings']['maxChargeCurrentAc'] == 'REDUCED':
|
|
542
|
+
vehicle.charging.settings.maximum_current._set_value(value=6, measured=captured_at) # pylint: disable=protected-access
|
|
543
|
+
else:
|
|
544
|
+
LOG_API.info('Unknown maxChargeCurrentAc %s not in %s', data['settings']['maxChargeCurrentAc'], ['MAXIMUM', 'REDUCED'])
|
|
545
|
+
vehicle.charging.settings.maximum_current._set_value(None, measured=captured_at) # pylint: disable=protected-access
|
|
546
|
+
else:
|
|
547
|
+
vehicle.charging.settings.maximum_current._set_value(None, measured=captured_at) # pylint: disable=protected-access
|
|
548
|
+
if 'autoUnlockPlugWhenCharged' in data['settings'] and data['settings']['autoUnlockPlugWhenCharged'] is not None:
|
|
549
|
+
vehicle.charging.settings.auto_unlock._add_on_set_hook(self.__on_charging_auto_unlock_change) # pylint: disable=protected-access
|
|
550
|
+
vehicle.charging.settings.auto_unlock._is_changeable = True # pylint: disable=protected-access
|
|
551
|
+
if data['settings']['autoUnlockPlugWhenCharged'] in ['ON', 'PERMANENT']:
|
|
552
|
+
vehicle.charging.settings.auto_unlock._set_value(True, measured=captured_at) # pylint: disable=protected-access
|
|
553
|
+
elif data['settings']['autoUnlockPlugWhenCharged'] == 'OFF':
|
|
554
|
+
vehicle.charging.settings.auto_unlock._set_value(False, measured=captured_at) # pylint: disable=protected-access
|
|
555
|
+
else:
|
|
556
|
+
LOG_API.info('Unknown autoUnlockPlugWhenCharged %s not in %s', data['settings']['autoUnlockPlugWhenCharged'],
|
|
557
|
+
['ON', 'PERMANENT', 'OFF'])
|
|
558
|
+
vehicle.charging.settings.auto_unlock._set_value(None, measured=captured_at) # pylint: disable=protected-access
|
|
559
|
+
if 'preferredChargeMode' in data['settings'] and data['settings']['preferredChargeMode'] is not None:
|
|
560
|
+
if not isinstance(vehicle.charging, SkodaCharging):
|
|
561
|
+
vehicle.charging = SkodaCharging(origin=vehicle.charging)
|
|
562
|
+
if data['settings']['preferredChargeMode'] in [item.name for item in SkodaCharging.SkodaChargeMode]:
|
|
563
|
+
preferred_charge_mode: SkodaCharging.SkodaChargeMode = SkodaCharging.SkodaChargeMode[data['settings']['preferredChargeMode']]
|
|
564
|
+
else:
|
|
565
|
+
LOG_API.info('Unkown charge mode %s not in %s', data['settings']['preferredChargeMode'], str(SkodaCharging.SkodaChargeMode))
|
|
566
|
+
preferred_charge_mode = SkodaCharging.SkodaChargeMode.UNKNOWN
|
|
567
|
+
|
|
568
|
+
if isinstance(vehicle.charging.settings, SkodaCharging.Settings):
|
|
569
|
+
# pylint: disable-next=protected-access
|
|
570
|
+
vehicle.charging.settings.preferred_charge_mode._set_value(value=preferred_charge_mode, measured=captured_at)
|
|
571
|
+
else:
|
|
572
|
+
if vehicle.charging is not None and isinstance(vehicle.charging.settings, SkodaCharging.Settings):
|
|
573
|
+
vehicle.charging.settings.preferred_charge_mode._set_value(None, measured=captured_at) # pylint: disable=protected-access
|
|
574
|
+
if 'availableChargeModes' in data['settings'] and data['settings']['availableChargeModes'] is not None:
|
|
575
|
+
if not isinstance(vehicle.charging, SkodaCharging):
|
|
576
|
+
vehicle.charging = SkodaCharging(origin=vehicle.charging)
|
|
577
|
+
available_charge_modes: list[str] = data['settings']['availableChargeModes']
|
|
578
|
+
if vehicle.charging is not None and isinstance(vehicle.charging.settings, SkodaCharging.Settings):
|
|
579
|
+
# pylint: disable-next=protected-access
|
|
580
|
+
vehicle.charging.settings.available_charge_modes._set_value('.'.join(available_charge_modes), measured=captured_at)
|
|
581
|
+
else:
|
|
582
|
+
if vehicle.charging is not None and isinstance(vehicle.charging.settings, SkodaCharging.Settings):
|
|
583
|
+
vehicle.charging.settings.available_charge_modes._set_value(None, measured=captured_at) # pylint: disable=protected-access
|
|
584
|
+
if 'chargingCareMode' in data['settings'] and data['settings']['chargingCareMode'] is not None:
|
|
585
|
+
if not isinstance(vehicle.charging, SkodaCharging):
|
|
586
|
+
vehicle.charging = SkodaCharging(origin=vehicle.charging)
|
|
587
|
+
if data['settings']['chargingCareMode'] in [item.name for item in SkodaCharging.SkodaChargingCareMode]:
|
|
588
|
+
charge_mode: SkodaCharging.SkodaChargingCareMode = SkodaCharging.SkodaChargingCareMode[data['settings']['chargingCareMode']]
|
|
589
|
+
else:
|
|
590
|
+
LOG_API.info('Unknown charging care mode %s not in %s', data['settings']['chargingCareMode'], str(SkodaCharging.SkodaChargingCareMode))
|
|
591
|
+
charge_mode = SkodaCharging.SkodaChargingCareMode.UNKNOWN
|
|
592
|
+
if vehicle.charging is not None and isinstance(vehicle.charging.settings, SkodaCharging.Settings):
|
|
593
|
+
# pylint: disable-next=protected-access
|
|
594
|
+
vehicle.charging.settings.charging_care_mode._set_value(value=charge_mode, measured=captured_at)
|
|
595
|
+
else:
|
|
596
|
+
if vehicle.charging is not None and isinstance(vehicle.charging.settings, SkodaCharging.Settings):
|
|
597
|
+
vehicle.charging.settings.charging_care_mode._set_value(None, measured=captured_at) # pylint: disable=protected-access
|
|
598
|
+
if 'batterySupport' in data['settings'] and data['settings']['batterySupport'] is not None:
|
|
599
|
+
if not isinstance(vehicle.charging, SkodaCharging):
|
|
600
|
+
vehicle.charging = SkodaCharging(origin=vehicle.charging)
|
|
601
|
+
if data['settings']['batterySupport'] in [item.name for item in SkodaCharging.SkodaBatterySupport]:
|
|
602
|
+
battery_support: SkodaCharging.SkodaBatterySupport = SkodaCharging.SkodaBatterySupport[data['settings']['batterySupport']]
|
|
603
|
+
else:
|
|
604
|
+
LOG_API.info('Unknown battery support %s not in %s', data['settings']['batterySupport'], str(SkodaCharging.SkodaBatterySupport))
|
|
605
|
+
battery_support = SkodaCharging.SkodaBatterySupport.UNKNOWN
|
|
606
|
+
if vehicle.charging is not None and isinstance(vehicle.charging.settings, SkodaCharging.Settings):
|
|
607
|
+
# pylint: disable-next=protected-access
|
|
608
|
+
vehicle.charging.settings.battery_support._set_value(value=battery_support, measured=captured_at)
|
|
609
|
+
else:
|
|
610
|
+
if vehicle.charging is not None and isinstance(vehicle.charging.settings, SkodaCharging.Settings):
|
|
611
|
+
vehicle.charging.settings.battery_support._set_value(None, measured=captured_at) # pylint: disable=protected-access
|
|
612
|
+
log_extra_keys(LOG_API, 'settings', data['settings'], {'targetStateOfChargeInPercent', 'maxChargeCurrentAc', 'autoUnlockPlugWhenCharged',
|
|
613
|
+
'preferredChargeMode', 'availableChargeModes', 'chargingCareMode', 'batterySupport'})
|
|
614
|
+
if 'errors' in data and data['errors'] is not None:
|
|
615
|
+
found_errors: Set[str] = set()
|
|
616
|
+
if not isinstance(vehicle.charging, SkodaCharging):
|
|
617
|
+
vehicle.charging = SkodaCharging(origin=vehicle.charging)
|
|
618
|
+
for error_dict in data['errors']:
|
|
619
|
+
if 'type' in error_dict and error_dict['type'] is not None:
|
|
620
|
+
if error_dict['type'] not in vehicle.charging.errors:
|
|
621
|
+
error: Error = Error(object_id=error_dict['type'])
|
|
622
|
+
else:
|
|
623
|
+
error = vehicle.charging.errors[error_dict['type']]
|
|
624
|
+
if error_dict['type'] in [item.name for item in Error.ChargingError]:
|
|
625
|
+
error_type: Error.ChargingError = Error.ChargingError[error_dict['type']]
|
|
626
|
+
else:
|
|
627
|
+
LOG_API.info('Unknown charging error type %s not in %s', error_dict['type'], str(Error.ChargingError))
|
|
628
|
+
error_type = Error.ChargingError.UNKNOWN
|
|
629
|
+
error.type._set_value(error_type, measured=captured_at) # pylint: disable=protected-access
|
|
630
|
+
if 'description' in error_dict and error_dict['description'] is not None:
|
|
631
|
+
error.description._set_value(error_dict['description'], measured=captured_at) # pylint: disable=protected-access
|
|
632
|
+
log_extra_keys(LOG_API, 'errors', error_dict, {'type', 'description'})
|
|
633
|
+
if vehicle.charging is not None and vehicle.charging.errors is not None and len(vehicle.charging.errors) > 0:
|
|
634
|
+
for error_id in vehicle.charging.errors.keys()-found_errors:
|
|
635
|
+
vehicle.charging.errors.pop(error_id)
|
|
636
|
+
else:
|
|
637
|
+
if isinstance(vehicle.charging, SkodaCharging):
|
|
638
|
+
vehicle.charging.errors.clear()
|
|
639
|
+
log_extra_keys(LOG_API, 'charging data', data, {'carCapturedTimestamp', 'status', 'isVehicleInSavedLocation', 'errors', 'settings'})
|
|
640
|
+
return vehicle
|
|
641
|
+
|
|
642
|
+
def fetch_connection_status(self, vehicle: SkodaVehicle, no_cache: bool = False) -> SkodaVehicle:
|
|
643
|
+
"""
|
|
644
|
+
Fetches the connection status of the given Skoda vehicle and updates its connection attributes.
|
|
645
|
+
|
|
646
|
+
Args:
|
|
647
|
+
vehicle (SkodaVehicle): The Skoda vehicle object containing the VIN and connection attributes.
|
|
648
|
+
|
|
649
|
+
Returns:
|
|
650
|
+
SkodaVehicle: The updated Skoda vehicle object with the fetched connection data.
|
|
651
|
+
|
|
652
|
+
Raises:
|
|
653
|
+
APIError: If the VIN is missing.
|
|
654
|
+
ValueError: If the vehicle has no connection object.
|
|
655
|
+
"""
|
|
656
|
+
vin = vehicle.vin.value
|
|
657
|
+
if vin is None:
|
|
658
|
+
raise APIError('VIN is missing')
|
|
659
|
+
url = f'https://mysmob.api.connect.skoda-auto.cz/api/v2/connection-status/{vin}/readiness'
|
|
660
|
+
data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
|
|
661
|
+
# {'unreachable': False, 'inMotion': False, 'batteryProtectionLimitOn': False}
|
|
662
|
+
if data is not None:
|
|
663
|
+
if 'unreachable' in data and data['unreachable'] is not None:
|
|
664
|
+
if data['unreachable']:
|
|
665
|
+
vehicle.connection_state._set_value(vehicle.ConnectionState.OFFLINE) # pylint: disable=protected-access
|
|
666
|
+
else:
|
|
667
|
+
vehicle.connection_state._set_value(vehicle.ConnectionState.REACHABLE) # pylint: disable=protected-access
|
|
668
|
+
else:
|
|
669
|
+
vehicle.connection_state._set_value(None) # pylint: disable=protected-access
|
|
670
|
+
if 'inMotion' in data and data['inMotion'] is not None:
|
|
671
|
+
vehicle.in_motion._set_value(data['inMotion']) # pylint: disable=protected-access
|
|
672
|
+
else:
|
|
673
|
+
vehicle.in_motion._set_value(None) # pylint: disable=protected-access
|
|
674
|
+
log_extra_keys(LOG_API, 'connection status', data, {'unreachable', 'inMotion'})
|
|
373
675
|
return vehicle
|
|
374
676
|
|
|
375
677
|
def fetch_position(self, vehicle: SkodaVehicle, no_cache: bool = False) -> SkodaVehicle:
|
|
@@ -392,7 +694,7 @@ class Connector(BaseConnector):
|
|
|
392
694
|
if vehicle.position is None:
|
|
393
695
|
raise ValueError('Vehicle has no charging object')
|
|
394
696
|
url = f'https://mysmob.api.connect.skoda-auto.cz/api/v1/maps/positions?vin={vin}'
|
|
395
|
-
data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
|
|
697
|
+
data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache, allow_empty=True)
|
|
396
698
|
if data is not None:
|
|
397
699
|
if 'positions' in data and data['positions'] is not None:
|
|
398
700
|
for position_dict in data['positions']:
|
|
@@ -407,7 +709,9 @@ class Connector(BaseConnector):
|
|
|
407
709
|
else:
|
|
408
710
|
longitude = None
|
|
409
711
|
vehicle.position.latitude._set_value(latitude) # pylint: disable=protected-access
|
|
712
|
+
vehicle.position.latitude.precision = 0.000001
|
|
410
713
|
vehicle.position.longitude._set_value(longitude) # pylint: disable=protected-access
|
|
714
|
+
vehicle.position.longitude.precision = 0.000001
|
|
411
715
|
vehicle.position.position_type._set_value(Position.PositionType.PARKING) # pylint: disable=protected-access
|
|
412
716
|
else:
|
|
413
717
|
vehicle.position.latitude._set_value(None) # pylint: disable=protected-access
|
|
@@ -424,6 +728,58 @@ class Connector(BaseConnector):
|
|
|
424
728
|
vehicle.position.latitude._set_value(None) # pylint: disable=protected-access
|
|
425
729
|
vehicle.position.longitude._set_value(None) # pylint: disable=protected-access
|
|
426
730
|
vehicle.position.position_type._set_value(None) # pylint: disable=protected-access
|
|
731
|
+
else:
|
|
732
|
+
vehicle.position.latitude._set_value(None) # pylint: disable=protected-access
|
|
733
|
+
vehicle.position.longitude._set_value(None) # pylint: disable=protected-access
|
|
734
|
+
vehicle.position.position_type._set_value(None) # pylint: disable=protected-access
|
|
735
|
+
return vehicle
|
|
736
|
+
|
|
737
|
+
def fetch_maintenance(self, vehicle: SkodaVehicle, no_cache: bool = False) -> SkodaVehicle:
|
|
738
|
+
"""
|
|
739
|
+
Fetches the maintenance information for a given Skoda vehicle.
|
|
740
|
+
|
|
741
|
+
Args:
|
|
742
|
+
vehicle (SkodaVehicle): The vehicle object for which maintenance information is to be fetched.
|
|
743
|
+
no_cache (bool, optional): If True, bypasses the cache and fetches fresh data. Defaults to False.
|
|
744
|
+
|
|
745
|
+
Returns:
|
|
746
|
+
SkodaVehicle: The vehicle object with updated maintenance information.
|
|
747
|
+
|
|
748
|
+
Raises:
|
|
749
|
+
APIError: If the VIN is missing or if the 'capturedAt' field is missing in the fetched data.
|
|
750
|
+
ValueError: If the vehicle has no charging object.
|
|
751
|
+
"""
|
|
752
|
+
vin = vehicle.vin.value
|
|
753
|
+
if vin is None:
|
|
754
|
+
raise APIError('VIN is missing')
|
|
755
|
+
if vehicle.position is None:
|
|
756
|
+
raise ValueError('Vehicle has no charging object')
|
|
757
|
+
url = f'https://mysmob.api.connect.skoda-auto.cz/api/v3/vehicle-maintenance/vehicles/{vin}/report'
|
|
758
|
+
data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
|
|
759
|
+
#{'capturedAt': '2025-02-24T19:54:32.728Z', 'inspectionDueInDays': 620, 'mileageInKm': 2512}
|
|
760
|
+
if data is not None:
|
|
761
|
+
if 'capturedAt' in data and data['capturedAt'] is not None:
|
|
762
|
+
captured_at: datetime = robust_time_parse(data['capturedAt'])
|
|
763
|
+
else:
|
|
764
|
+
raise APIError('Could not fetch maintenance, capturedAt missing')
|
|
765
|
+
if 'mileageInKm' in data and data['mileageInKm'] is not None:
|
|
766
|
+
vehicle.odometer._set_value(value=data['mileageInKm'], measured=captured_at, unit=Length.KM) # pylint: disable=protected-access
|
|
767
|
+
vehicle.odometer.precision = 1
|
|
768
|
+
else:
|
|
769
|
+
vehicle.odometer._set_value(None) # pylint: disable=protected-access
|
|
770
|
+
if 'inspectionDueInDays' in data and data['inspectionDueInDays'] is not None:
|
|
771
|
+
inspection_due: timedelta = timedelta(days=data['inspectionDueInDays'])
|
|
772
|
+
inspection_date: datetime = captured_at + inspection_due
|
|
773
|
+
inspection_date = inspection_date.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
774
|
+
# pylint: disable-next=protected-access
|
|
775
|
+
vehicle.maintenance.inspection_due_at._set_value(value=inspection_date, measured=captured_at)
|
|
776
|
+
else:
|
|
777
|
+
vehicle.maintenance.inspection_due_at._set_value(None) # pylint: disable=protected-access
|
|
778
|
+
log_extra_keys(LOG_API, 'maintenance', data, {'capturedAt', 'mileageInKm', 'inspectionDueInDays'})
|
|
779
|
+
|
|
780
|
+
#url = f'https://mysmob.api.connect.skoda-auto.cz/api/v1/vehicle-health-report/warning-lights/{vin}'
|
|
781
|
+
#data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
|
|
782
|
+
#{'capturedAt': '2025-02-24T15:32:35.032Z', 'mileageInKm': 2512, 'warningLights': [{'category': 'ASSISTANCE', 'defects': []}, {'category': 'COMFORT', 'defects': []}, {'category': 'BRAKE', 'defects': []}, {'category': 'ELECTRIC_ENGINE', 'defects': []}, {'category': 'LIGHTING', 'defects': []}, {'category': 'TIRE', 'defects': []}, {'category': 'OTHER', 'defects': []}]}
|
|
427
783
|
return vehicle
|
|
428
784
|
|
|
429
785
|
def fetch_air_conditioning(self, vehicle: SkodaVehicle, no_cache: bool = False) -> SkodaVehicle:
|
|
@@ -453,6 +809,13 @@ class Connector(BaseConnector):
|
|
|
453
809
|
url = f'https://mysmob.api.connect.skoda-auto.cz/api/v2/air-conditioning/{vin}'
|
|
454
810
|
data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
|
|
455
811
|
if data is not None:
|
|
812
|
+
if vehicle.climatization is not None and vehicle.climatization.commands is not None \
|
|
813
|
+
and not vehicle.climatization.commands.contains_command('start-stop'):
|
|
814
|
+
start_stop_command = ClimatizationStartStopCommand(parent=vehicle.climatization.commands)
|
|
815
|
+
start_stop_command._add_on_set_hook(self.__on_air_conditioning_start_stop) # pylint: disable=protected-access
|
|
816
|
+
start_stop_command.enabled = True
|
|
817
|
+
vehicle.climatization.commands.add_command(start_stop_command)
|
|
818
|
+
|
|
456
819
|
if 'carCapturedTimestamp' in data and data['carCapturedTimestamp'] is not None:
|
|
457
820
|
captured_at: datetime = robust_time_parse(data['carCapturedTimestamp'])
|
|
458
821
|
else:
|
|
@@ -475,27 +838,42 @@ class Connector(BaseConnector):
|
|
|
475
838
|
else:
|
|
476
839
|
vehicle.climatization.estimated_date_reached._set_value(value=None, measured=captured_at) # pylint: disable=protected-access
|
|
477
840
|
if 'targetTemperature' in data and data['targetTemperature'] is not None:
|
|
841
|
+
# pylint: disable-next=protected-access
|
|
842
|
+
vehicle.climatization.settings.target_temperature._add_on_set_hook(self.__on_air_conditioning_target_temperature_change)
|
|
843
|
+
vehicle.climatization.settings.target_temperature._is_changeable = True # pylint: disable=protected-access
|
|
844
|
+
precision: float = 0.5
|
|
845
|
+
min_temperature: Optional[float] = None
|
|
846
|
+
max_temperature: Optional[float] = None
|
|
478
847
|
unit: Temperature = Temperature.UNKNOWN
|
|
479
848
|
if 'unitInCar' in data['targetTemperature'] and data['targetTemperature']['unitInCar'] is not None:
|
|
480
849
|
if data['targetTemperature']['unitInCar'] == 'CELSIUS':
|
|
481
850
|
unit = Temperature.C
|
|
851
|
+
min_temperature: Optional[float] = 16
|
|
852
|
+
max_temperature: Optional[float] = 29.5
|
|
482
853
|
elif data['targetTemperature']['unitInCar'] == 'FAHRENHEIT':
|
|
483
854
|
unit = Temperature.F
|
|
855
|
+
min_temperature: Optional[float] = 61
|
|
856
|
+
max_temperature: Optional[float] = 85
|
|
484
857
|
elif data['targetTemperature']['unitInCar'] == 'KELVIN':
|
|
485
858
|
unit = Temperature.K
|
|
486
859
|
else:
|
|
487
860
|
LOG_API.info('Unknown temperature unit for targetTemperature in air-conditioning %s', data['targetTemperature']['unitInCar'])
|
|
488
861
|
if 'temperatureValue' in data['targetTemperature'] and data['targetTemperature']['temperatureValue'] is not None:
|
|
489
862
|
# pylint: disable-next=protected-access
|
|
490
|
-
vehicle.climatization.target_temperature._set_value(value=data['targetTemperature']['temperatureValue'],
|
|
491
|
-
|
|
492
|
-
|
|
863
|
+
vehicle.climatization.settings.target_temperature._set_value(value=data['targetTemperature']['temperatureValue'],
|
|
864
|
+
measured=captured_at,
|
|
865
|
+
unit=unit)
|
|
866
|
+
vehicle.climatization.settings.target_temperature.precision = precision
|
|
867
|
+
vehicle.climatization.settings.target_temperature.minimum = min_temperature
|
|
868
|
+
vehicle.climatization.settings.target_temperature.maximum = max_temperature
|
|
869
|
+
|
|
493
870
|
else:
|
|
494
|
-
|
|
871
|
+
# pylint: disable-next=protected-access
|
|
872
|
+
vehicle.climatization.settings.target_temperature._set_value(value=None, measured=captured_at, unit=unit)
|
|
495
873
|
log_extra_keys(LOG_API, 'targetTemperature', data['targetTemperature'], {'unitInCar', 'temperatureValue'})
|
|
496
874
|
else:
|
|
497
875
|
# pylint: disable-next=protected-access
|
|
498
|
-
vehicle.climatization.target_temperature._set_value(value=None, measured=captured_at, unit=Temperature.UNKNOWN)
|
|
876
|
+
vehicle.climatization.settings.target_temperature._set_value(value=None, measured=captured_at, unit=Temperature.UNKNOWN)
|
|
499
877
|
if 'outsideTemperature' in data and data['outsideTemperature'] is not None:
|
|
500
878
|
if 'carCapturedTimestamp' in data['outsideTemperature'] and data['outsideTemperature']['carCapturedTimestamp'] is not None:
|
|
501
879
|
outside_captured_at: datetime = robust_time_parse(data['outsideTemperature']['carCapturedTimestamp'])
|
|
@@ -510,7 +888,7 @@ class Connector(BaseConnector):
|
|
|
510
888
|
elif data['outsideTemperature']['temperatureUnit'] == 'KELVIN':
|
|
511
889
|
unit = Temperature.K
|
|
512
890
|
else:
|
|
513
|
-
LOG_API.info('Unknown temperature unit for outsideTemperature in air-conditioning %s', data['
|
|
891
|
+
LOG_API.info('Unknown temperature unit for outsideTemperature in air-conditioning %s', data['outsideTemperature']['temperatureUnit'])
|
|
514
892
|
if 'temperatureValue' in data['outsideTemperature'] and data['outsideTemperature']['temperatureValue'] is not None:
|
|
515
893
|
# pylint: disable-next=protected-access
|
|
516
894
|
vehicle.outside_temperature._set_value(value=data['outsideTemperature']['temperatureValue'],
|
|
@@ -525,8 +903,171 @@ class Connector(BaseConnector):
|
|
|
525
903
|
log_extra_keys(LOG_API, 'targetTemperature', data['outsideTemperature'], {'carCapturedTimestamp', 'temperatureUnit', 'temperatureValue'})
|
|
526
904
|
else:
|
|
527
905
|
vehicle.outside_temperature._set_value(value=None, measured=None, unit=Temperature.UNKNOWN) # pylint: disable=protected-access
|
|
528
|
-
|
|
529
|
-
|
|
906
|
+
if 'airConditioningAtUnlock' in data and data['airConditioningAtUnlock'] is not None:
|
|
907
|
+
if vehicle.climatization is not None and vehicle.climatization.settings is not None:
|
|
908
|
+
# pylint: disable-next=protected-access
|
|
909
|
+
vehicle.climatization.settings.climatization_at_unlock._add_on_set_hook(self.__on_air_conditioning_at_unlock_change)
|
|
910
|
+
vehicle.climatization.settings.climatization_at_unlock._is_changeable = True # pylint: disable=protected-access
|
|
911
|
+
if data['airConditioningAtUnlock'] is True:
|
|
912
|
+
# pylint: disable-next=protected-access
|
|
913
|
+
vehicle.climatization.settings.climatization_at_unlock._set_value(True, measured=captured_at)
|
|
914
|
+
elif data['airConditioningAtUnlock'] is False:
|
|
915
|
+
# pylint: disable-next=protected-access
|
|
916
|
+
vehicle.climatization.settings.climatization_at_unlock._set_value(False, measured=captured_at)
|
|
917
|
+
else:
|
|
918
|
+
# pylint: disable-next=protected-access
|
|
919
|
+
vehicle.climatization.settings.climatization_at_unlock._set_value(None, measured=captured_at)
|
|
920
|
+
else:
|
|
921
|
+
if vehicle.climatization is not None and vehicle.climatization.settings is not None:
|
|
922
|
+
# pylint: disable-next=protected-access
|
|
923
|
+
vehicle.climatization.settings.climatization_at_unlock._set_value(None, measured=captured_at)
|
|
924
|
+
if 'steeringWheelPosition' in data and data['steeringWheelPosition'] is not None:
|
|
925
|
+
if vehicle.specification is not None:
|
|
926
|
+
if data['steeringWheelPosition'] in [item.name for item in GenericVehicle.VehicleSpecification.SteeringPosition]:
|
|
927
|
+
steering_wheel_position: GenericVehicle.VehicleSpecification.SteeringPosition = \
|
|
928
|
+
GenericVehicle.VehicleSpecification.SteeringPosition[data['steeringWheelPosition']]
|
|
929
|
+
else:
|
|
930
|
+
LOG_API.info('Unknown steering wheel position %s not in %s', data['steeringWheelPosition'],
|
|
931
|
+
str(GenericVehicle.VehicleSpecification.SteeringPosition))
|
|
932
|
+
steering_wheel_position = GenericVehicle.VehicleSpecification.SteeringPosition.UNKNOWN
|
|
933
|
+
# pylint: disable-next=protected-access
|
|
934
|
+
vehicle.specification.steering_wheel_position._set_value(value=steering_wheel_position, measured=captured_at)
|
|
935
|
+
else:
|
|
936
|
+
if vehicle.specification is not None:
|
|
937
|
+
# pylint: disable-next=protected-access
|
|
938
|
+
vehicle.specification.steering_wheel_position._set_value(None, measured=captured_at)
|
|
939
|
+
if 'windowHeatingEnabled' in data and data['windowHeatingEnabled'] is not None:
|
|
940
|
+
if vehicle.climatization is not None and vehicle.climatization.settings is not None:
|
|
941
|
+
# pylint: disable-next=protected-access
|
|
942
|
+
vehicle.climatization.settings.window_heating._add_on_set_hook(self.__on_air_conditioning_window_heating_change)
|
|
943
|
+
vehicle.climatization.settings.window_heating._is_changeable = True # pylint: disable=protected-access
|
|
944
|
+
if data['windowHeatingEnabled'] is True:
|
|
945
|
+
# pylint: disable-next=protected-access
|
|
946
|
+
vehicle.climatization.settings.window_heating._set_value(True, measured=captured_at)
|
|
947
|
+
elif data['windowHeatingEnabled'] is False:
|
|
948
|
+
# pylint: disable-next=protected-access
|
|
949
|
+
vehicle.climatization.settings.window_heating._set_value(False, measured=captured_at)
|
|
950
|
+
else:
|
|
951
|
+
# pylint: disable-next=protected-access
|
|
952
|
+
vehicle.climatization.settings.window_heating._set_value(None, measured=captured_at)
|
|
953
|
+
else:
|
|
954
|
+
if vehicle.climatization is not None and vehicle.climatization.settings is not None:
|
|
955
|
+
# pylint: disable-next=protected-access
|
|
956
|
+
vehicle.climatization.settings.window_heating._set_value(None, measured=captured_at)
|
|
957
|
+
if 'seatHeatingActivated' in data and data['seatHeatingActivated'] is not None:
|
|
958
|
+
if vehicle.climatization is not None and vehicle.climatization.settings is not None:
|
|
959
|
+
if data['seatHeatingActivated'] is True:
|
|
960
|
+
# pylint: disable-next=protected-access
|
|
961
|
+
vehicle.climatization.settings.seat_heating._set_value(True, measured=captured_at)
|
|
962
|
+
elif data['seatHeatingActivated'] is False:
|
|
963
|
+
# pylint: disable-next=protected-access
|
|
964
|
+
vehicle.climatization.settings.seat_heating._set_value(False, measured=captured_at)
|
|
965
|
+
else:
|
|
966
|
+
# pylint: disable-next=protected-access
|
|
967
|
+
vehicle.climatization.settings.seat_heating._set_value(None, measured=captured_at)
|
|
968
|
+
else:
|
|
969
|
+
if vehicle.climatization is not None and vehicle.climatization.settings is not None:
|
|
970
|
+
# pylint: disable-next=protected-access
|
|
971
|
+
vehicle.climatization.settings.seat_heating._set_value(None, measured=captured_at)
|
|
972
|
+
if isinstance(vehicle, SkodaElectricVehicle):
|
|
973
|
+
if 'chargerConnectionState' in data and data['chargerConnectionState'] is not None \
|
|
974
|
+
and vehicle.charging is not None and vehicle.charging.connector is not None:
|
|
975
|
+
if data['chargerConnectionState'] in [item.name for item in ChargingConnector.ChargingConnectorConnectionState]:
|
|
976
|
+
charging_connector_state: ChargingConnector.ChargingConnectorConnectionState = \
|
|
977
|
+
ChargingConnector.ChargingConnectorConnectionState[data['chargerConnectionState']]
|
|
978
|
+
# pylint: disable-next=protected-access
|
|
979
|
+
vehicle.charging.connector.connection_state._set_value(value=charging_connector_state, measured=captured_at)
|
|
980
|
+
else:
|
|
981
|
+
LOG_API.info('Unkown connector state %s not in %s', data['chargerConnectionState'],
|
|
982
|
+
str(ChargingConnector.ChargingConnectorConnectionState))
|
|
983
|
+
# pylint: disable-next=protected-access
|
|
984
|
+
vehicle.charging.connector.connection_state._set_value(value=SkodaCharging.SkodaChargingState.UNKNOWN, measured=captured_at)
|
|
985
|
+
else:
|
|
986
|
+
# pylint: disable-next=protected-access
|
|
987
|
+
vehicle.charging.connector.connection_state._set_value(value=None, measured=captured_at)
|
|
988
|
+
if 'chargerLockState' in data and data['chargerLockState'] is not None \
|
|
989
|
+
and vehicle.charging is not None and vehicle.charging.connector is not None:
|
|
990
|
+
if data['chargerLockState'] in [item.name for item in ChargingConnector.ChargingConnectorLockState]:
|
|
991
|
+
charging_connector_lockstate: ChargingConnector.ChargingConnectorLockState = \
|
|
992
|
+
ChargingConnector.ChargingConnectorLockState[data['chargerLockState']]
|
|
993
|
+
# pylint: disable-next=protected-access
|
|
994
|
+
vehicle.charging.connector.lock_state._set_value(value=charging_connector_lockstate, measured=captured_at)
|
|
995
|
+
else:
|
|
996
|
+
LOG_API.info('Unkown connector lock state %s not in %s', data['chargerLockState'],
|
|
997
|
+
str(ChargingConnector.ChargingConnectorLockState))
|
|
998
|
+
# pylint: disable-next=protected-access
|
|
999
|
+
vehicle.charging.connector.lock_state._set_value(value=SkodaCharging.SkodaChargingState.UNKNOWN, measured=captured_at)
|
|
1000
|
+
else:
|
|
1001
|
+
# pylint: disable-next=protected-access
|
|
1002
|
+
vehicle.charging.connector.lock_state._set_value(value=None, measured=captured_at)
|
|
1003
|
+
if 'windowHeatingState' in data and data['windowHeatingState'] is not None:
|
|
1004
|
+
heating_on: bool = False
|
|
1005
|
+
all_heating_invalid: bool = True
|
|
1006
|
+
for window_id, state in data['windowHeatingState'].items():
|
|
1007
|
+
if window_id != 'unspecified':
|
|
1008
|
+
if window_id in vehicle.window_heatings.windows:
|
|
1009
|
+
window: WindowHeatings.WindowHeating = vehicle.window_heatings.windows[window_id]
|
|
1010
|
+
else:
|
|
1011
|
+
window = WindowHeatings.WindowHeating(window_id=window_id, window_heatings=vehicle.window_heatings)
|
|
1012
|
+
vehicle.window_heatings.windows[window_id] = window
|
|
1013
|
+
|
|
1014
|
+
if state.lower() in [item.value for item in WindowHeatings.HeatingState]:
|
|
1015
|
+
window_heating_state: WindowHeatings.HeatingState = WindowHeatings.HeatingState(state.lower())
|
|
1016
|
+
if window_heating_state == WindowHeatings.HeatingState.ON:
|
|
1017
|
+
heating_on = True
|
|
1018
|
+
if window_heating_state in [WindowHeatings.HeatingState.ON,
|
|
1019
|
+
WindowHeatings.HeatingState.OFF]:
|
|
1020
|
+
all_heating_invalid = False
|
|
1021
|
+
window.heating_state._set_value(window_heating_state, measured=captured_at) # pylint: disable=protected-access
|
|
1022
|
+
else:
|
|
1023
|
+
LOG_API.info('Unknown window heating state %s not in %s', state.lower(), str(WindowHeatings.HeatingState))
|
|
1024
|
+
# pylint: disable-next=protected-access
|
|
1025
|
+
window.heating_state._set_value(WindowHeatings.HeatingState.UNKNOWN, measured=captured_at)
|
|
1026
|
+
if all_heating_invalid:
|
|
1027
|
+
# pylint: disable-next=protected-access
|
|
1028
|
+
vehicle.window_heatings.heating_state._set_value(WindowHeatings.HeatingState.INVALID, measured=captured_at)
|
|
1029
|
+
else:
|
|
1030
|
+
if heating_on:
|
|
1031
|
+
# pylint: disable-next=protected-access
|
|
1032
|
+
vehicle.window_heatings.heating_state._set_value(WindowHeatings.HeatingState.ON, measured=captured_at)
|
|
1033
|
+
else:
|
|
1034
|
+
# pylint: disable-next=protected-access
|
|
1035
|
+
vehicle.window_heatings.heating_state._set_value(WindowHeatings.HeatingState.OFF, measured=captured_at)
|
|
1036
|
+
if vehicle.window_heatings is not None and vehicle.window_heatings.commands is not None \
|
|
1037
|
+
and not vehicle.window_heatings.commands.contains_command('start-stop'):
|
|
1038
|
+
start_stop_command = WindowHeatingStartStopCommand(parent=vehicle.window_heatings.commands)
|
|
1039
|
+
start_stop_command._add_on_set_hook(self.__on_window_heating_start_stop) # pylint: disable=protected-access
|
|
1040
|
+
start_stop_command.enabled = True
|
|
1041
|
+
vehicle.window_heatings.commands.add_command(start_stop_command)
|
|
1042
|
+
if 'errors' in data and data['errors'] is not None:
|
|
1043
|
+
found_errors: Set[str] = set()
|
|
1044
|
+
if not isinstance(vehicle.climatization, SkodaClimatization):
|
|
1045
|
+
vehicle.climatization = SkodaClimatization(origin=vehicle.climatization)
|
|
1046
|
+
for error_dict in data['errors']:
|
|
1047
|
+
if 'type' in error_dict and error_dict['type'] is not None:
|
|
1048
|
+
if error_dict['type'] not in vehicle.climatization.errors:
|
|
1049
|
+
error: Error = Error(object_id=error_dict['type'])
|
|
1050
|
+
else:
|
|
1051
|
+
error = vehicle.climatization.errors[error_dict['type']]
|
|
1052
|
+
if error_dict['type'] in [item.name for item in Error.ClimatizationError]:
|
|
1053
|
+
error_type: Error.ClimatizationError = Error.ClimatizationError[error_dict['type']]
|
|
1054
|
+
else:
|
|
1055
|
+
LOG_API.info('Unknown climatization error type %s not in %s', error_dict['type'], str(Error.ClimatizationError))
|
|
1056
|
+
error_type = Error.ClimatizationError.UNKNOWN
|
|
1057
|
+
error.type._set_value(error_type, measured=captured_at) # pylint: disable=protected-access
|
|
1058
|
+
if 'description' in error_dict and error_dict['description'] is not None:
|
|
1059
|
+
error.description._set_value(error_dict['description'], measured=captured_at) # pylint: disable=protected-access
|
|
1060
|
+
log_extra_keys(LOG_API, 'errors', error_dict, {'type', 'description'})
|
|
1061
|
+
if vehicle.climatization is not None and vehicle.climatization.errors is not None and len(vehicle.climatization.errors) > 0:
|
|
1062
|
+
for error_id in vehicle.climatization.errors.keys()-found_errors:
|
|
1063
|
+
vehicle.climatization.errors.pop(error_id)
|
|
1064
|
+
else:
|
|
1065
|
+
if isinstance(vehicle.climatization, SkodaClimatization):
|
|
1066
|
+
vehicle.climatization.errors.clear()
|
|
1067
|
+
log_extra_keys(LOG_API, 'air-condition', data, {'carCapturedTimestamp', 'state', 'estimatedDateTimeToReachTargetTemperature',
|
|
1068
|
+
'targetTemperature', 'outsideTemperature', 'chargerConnectionState',
|
|
1069
|
+
'chargerLockState', 'airConditioningAtUnlock', 'steeringWheelPosition',
|
|
1070
|
+
'windowHeatingEnabled', 'seatHeatingActivated', 'windowHeatingState', 'errors'})
|
|
530
1071
|
return vehicle
|
|
531
1072
|
|
|
532
1073
|
def fetch_vehicle_details(self, vehicle: SkodaVehicle, no_cache: bool = False) -> SkodaVehicle:
|
|
@@ -562,6 +1103,20 @@ class Connector(BaseConnector):
|
|
|
562
1103
|
else:
|
|
563
1104
|
capability = Capability(capability_id=capability_id, capabilities=vehicle.capabilities)
|
|
564
1105
|
vehicle.capabilities.add_capability(capability_id, capability)
|
|
1106
|
+
if 'statuses' in capability_dict and capability_dict['statuses'] is not None:
|
|
1107
|
+
statuses = capability_dict['statuses']
|
|
1108
|
+
if isinstance(statuses, list):
|
|
1109
|
+
for status in statuses:
|
|
1110
|
+
if status in [item.name for item in Capability.Status]:
|
|
1111
|
+
capability.status.value.append(Capability.Status[status])
|
|
1112
|
+
else:
|
|
1113
|
+
LOG_API.warning('Capability status unkown %s', status)
|
|
1114
|
+
capability.status.value.append(Capability.Status.UNKNOWN)
|
|
1115
|
+
else:
|
|
1116
|
+
LOG_API.warning('Capability status not a list in %s', statuses)
|
|
1117
|
+
else:
|
|
1118
|
+
capability.status.value.clear()
|
|
1119
|
+
log_extra_keys(LOG_API, 'capability', capability_dict, {'id', 'statuses'})
|
|
565
1120
|
else:
|
|
566
1121
|
raise APIError('Could not parse capability, id missing')
|
|
567
1122
|
for capability_id in vehicle.capabilities.capabilities.keys() - found_capabilities:
|
|
@@ -571,6 +1126,33 @@ class Connector(BaseConnector):
|
|
|
571
1126
|
else:
|
|
572
1127
|
vehicle.capabilities.clear_capabilities()
|
|
573
1128
|
|
|
1129
|
+
if vehicle.capabilities.has_capability('VEHICLE_WAKE_UP_TRIGGER', check_status_ok=True):
|
|
1130
|
+
if vehicle.commands is not None and vehicle.commands.commands is not None \
|
|
1131
|
+
and not vehicle.commands.contains_command('wake-sleep'):
|
|
1132
|
+
wake_sleep_command = WakeSleepCommand(parent=vehicle.commands)
|
|
1133
|
+
wake_sleep_command._add_on_set_hook(self.__on_wake_sleep) # pylint: disable=protected-access
|
|
1134
|
+
wake_sleep_command.enabled = True
|
|
1135
|
+
vehicle.commands.add_command(wake_sleep_command)
|
|
1136
|
+
|
|
1137
|
+
# Add HONK_AND_FLASH command if necessary capabilities are available
|
|
1138
|
+
if vehicle.capabilities.has_capability('HONK_AND_FLASH', check_status_ok=True) \
|
|
1139
|
+
and vehicle.capabilities.has_capability('PARKING_POSITION', check_status_ok=True):
|
|
1140
|
+
if vehicle.commands is not None and vehicle.commands.commands is not None \
|
|
1141
|
+
and not vehicle.commands.contains_command('honk-flash'):
|
|
1142
|
+
honk_flash_command = HonkAndFlashCommand(parent=vehicle.commands)
|
|
1143
|
+
honk_flash_command._add_on_set_hook(self.__on_honk_flash) # pylint: disable=protected-access
|
|
1144
|
+
honk_flash_command.enabled = True
|
|
1145
|
+
vehicle.commands.add_command(honk_flash_command)
|
|
1146
|
+
|
|
1147
|
+
# Add lock and unlock command
|
|
1148
|
+
if vehicle.capabilities.has_capability('ACCESS', check_status_ok=True):
|
|
1149
|
+
if vehicle.doors is not None and vehicle.doors.commands is not None and vehicle.doors.commands.commands is not None \
|
|
1150
|
+
and not vehicle.doors.commands.contains_command('lock-unlock'):
|
|
1151
|
+
lock_unlock_command = LockUnlockCommand(parent=vehicle.doors.commands)
|
|
1152
|
+
lock_unlock_command._add_on_set_hook(self.__on_lock_unlock) # pylint: disable=protected-access
|
|
1153
|
+
lock_unlock_command.enabled = True
|
|
1154
|
+
vehicle.doors.commands.add_command(lock_unlock_command)
|
|
1155
|
+
|
|
574
1156
|
if 'specification' in vehicle_data and vehicle_data['specification'] is not None:
|
|
575
1157
|
if 'model' in vehicle_data['specification'] and vehicle_data['specification']['model'] is not None:
|
|
576
1158
|
vehicle.model._set_value(vehicle_data['specification']['model']) # pylint: disable=protected-access
|
|
@@ -582,6 +1164,68 @@ class Connector(BaseConnector):
|
|
|
582
1164
|
log_extra_keys(LOG_API, 'api/v2/garage/vehicles/VIN', vehicle_data, {'softwareVersion'})
|
|
583
1165
|
return vehicle
|
|
584
1166
|
|
|
1167
|
+
def fetch_vehicle_images(self, vehicle: SkodaVehicle, no_cache: bool = False) -> SkodaVehicle:
|
|
1168
|
+
if SUPPORT_IMAGES:
|
|
1169
|
+
url: str = f'https://mysmob.api.connect.skoda-auto.cz/api/v1/vehicle-information/{vehicle.vin.value}/renders'
|
|
1170
|
+
data = self._fetch_data(url, session=self.session, allow_http_error=True)
|
|
1171
|
+
if data is not None and 'compositeRenders' in data: # pylint: disable=too-many-nested-blocks
|
|
1172
|
+
for image in data['compositeRenders']:
|
|
1173
|
+
if 'layers' not in image or image['layers'] is None or len(image['layers']) == 0:
|
|
1174
|
+
continue
|
|
1175
|
+
image_url: Optional[str] = None
|
|
1176
|
+
for layer in image['layers']:
|
|
1177
|
+
if 'url' in layer and layer['url'] is not None:
|
|
1178
|
+
image_url = layer['url']
|
|
1179
|
+
break
|
|
1180
|
+
if image_url is None:
|
|
1181
|
+
continue
|
|
1182
|
+
img = None
|
|
1183
|
+
cache_date = None
|
|
1184
|
+
if self.active_config['max_age'] is not None and self.session.cache is not None and image_url in self.session.cache:
|
|
1185
|
+
img, cache_date_string = self.session.cache[image_url]
|
|
1186
|
+
img = base64.b64decode(img) # pyright: ignore[reportPossiblyUnboundVariable]
|
|
1187
|
+
img = Image.open(io.BytesIO(img)) # pyright: ignore[reportPossiblyUnboundVariable]
|
|
1188
|
+
cache_date = datetime.fromisoformat(cache_date_string)
|
|
1189
|
+
if img is None or self.active_config['max_age'] is None \
|
|
1190
|
+
or (cache_date is not None and cache_date < (datetime.utcnow() - timedelta(seconds=self.active_config['max_age']))):
|
|
1191
|
+
try:
|
|
1192
|
+
image_download_response = requests.get(image_url, stream=True)
|
|
1193
|
+
if image_download_response.status_code == requests.codes['ok']:
|
|
1194
|
+
img = Image.open(image_download_response.raw) # pyright: ignore[reportPossiblyUnboundVariable]
|
|
1195
|
+
if self.session.cache is not None:
|
|
1196
|
+
buffered = io.BytesIO() # pyright: ignore[reportPossiblyUnboundVariable]
|
|
1197
|
+
img.save(buffered, format="PNG")
|
|
1198
|
+
img_str = base64.b64encode(buffered.getvalue()).decode("utf-8") # pyright: ignore[reportPossiblyUnboundVariable]
|
|
1199
|
+
self.session.cache[image_url] = (img_str, str(datetime.utcnow()))
|
|
1200
|
+
elif image_download_response.status_code == requests.codes['unauthorized']:
|
|
1201
|
+
LOG.info('Server asks for new authorization')
|
|
1202
|
+
self.session.login()
|
|
1203
|
+
image_download_response = self.session.get(image_url, stream=True)
|
|
1204
|
+
if image_download_response.status_code == requests.codes['ok']:
|
|
1205
|
+
img = Image.open(image_download_response.raw) # pyright: ignore[reportPossiblyUnboundVariable]
|
|
1206
|
+
if self.session.cache is not None:
|
|
1207
|
+
buffered = io.BytesIO() # pyright: ignore[reportPossiblyUnboundVariable]
|
|
1208
|
+
img.save(buffered, format="PNG")
|
|
1209
|
+
img_str = base64.b64encode(buffered.getvalue()).decode("utf-8") # pyright: ignore[reportPossiblyUnboundVariable]
|
|
1210
|
+
self.session.cache[image_url] = (img_str, str(datetime.utcnow()))
|
|
1211
|
+
except requests.exceptions.ConnectionError as connection_error:
|
|
1212
|
+
raise RetrievalError(f'Connection error: {connection_error}') from connection_error
|
|
1213
|
+
except requests.exceptions.ChunkedEncodingError as chunked_encoding_error:
|
|
1214
|
+
raise RetrievalError(f'Error: {chunked_encoding_error}') from chunked_encoding_error
|
|
1215
|
+
except requests.exceptions.ReadTimeout as timeout_error:
|
|
1216
|
+
raise RetrievalError(f'Timeout during read: {timeout_error}') from timeout_error
|
|
1217
|
+
except requests.exceptions.RetryError as retry_error:
|
|
1218
|
+
raise RetrievalError(f'Retrying failed: {retry_error}') from retry_error
|
|
1219
|
+
if img is not None:
|
|
1220
|
+
vehicle._car_images[image['viewType']] = img # pylint: disable=protected-access
|
|
1221
|
+
if image['viewType'] == 'UNMODIFIED_EXTERIOR_FRONT':
|
|
1222
|
+
if 'car_picture' in vehicle.images.images:
|
|
1223
|
+
vehicle.images.images['car_picture']._set_value(img) # pylint: disable=protected-access
|
|
1224
|
+
else:
|
|
1225
|
+
vehicle.images.images['car_picture'] = ImageAttribute(name="car_picture", parent=vehicle.images,
|
|
1226
|
+
value=img, tags={'carconnectivity'})
|
|
1227
|
+
return vehicle
|
|
1228
|
+
|
|
585
1229
|
def fetch_driving_range(self, vehicle: SkodaVehicle, no_cache: bool = False) -> SkodaVehicle:
|
|
586
1230
|
"""
|
|
587
1231
|
Fetches the driving range data for a given Skoda vehicle and updates the vehicle object accordingly.
|
|
@@ -607,6 +1251,8 @@ class Connector(BaseConnector):
|
|
|
607
1251
|
url = f'https://mysmob.api.connect.skoda-auto.cz/api/v2/vehicle-status/{vin}/driving-range'
|
|
608
1252
|
range_data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
|
|
609
1253
|
if range_data:
|
|
1254
|
+
if 'carCapturedTimestamp' not in range_data or range_data['carCapturedTimestamp'] is None:
|
|
1255
|
+
raise APIError('Could not fetch driving range, carCapturedTimestamp missing')
|
|
610
1256
|
captured_at: datetime = robust_time_parse(range_data['carCapturedTimestamp'])
|
|
611
1257
|
# Check vehicle type and if it does not match the current vehicle type, create a new vehicle object using copy constructor
|
|
612
1258
|
if 'carType' in range_data and range_data['carType'] is not None:
|
|
@@ -614,7 +1260,7 @@ class Connector(BaseConnector):
|
|
|
614
1260
|
car_type = GenericVehicle.Type(range_data['carType'])
|
|
615
1261
|
if car_type == GenericVehicle.Type.ELECTRIC and not isinstance(vehicle, SkodaElectricVehicle):
|
|
616
1262
|
LOG.debug('Promoting %s to SkodaElectricVehicle object for %s', vehicle.__class__.__name__, vin)
|
|
617
|
-
vehicle = SkodaElectricVehicle(origin=vehicle)
|
|
1263
|
+
vehicle = SkodaElectricVehicle(garage=self.car_connectivity.garage, origin=vehicle)
|
|
618
1264
|
self.car_connectivity.garage.replace_vehicle(vin, vehicle)
|
|
619
1265
|
elif car_type in [GenericVehicle.Type.FUEL,
|
|
620
1266
|
GenericVehicle.Type.GASOLINE,
|
|
@@ -624,11 +1270,11 @@ class Connector(BaseConnector):
|
|
|
624
1270
|
GenericVehicle.Type.LPG] \
|
|
625
1271
|
and not isinstance(vehicle, SkodaCombustionVehicle):
|
|
626
1272
|
LOG.debug('Promoting %s to SkodaCombustionVehicle object for %s', vehicle.__class__.__name__, vin)
|
|
627
|
-
vehicle = SkodaCombustionVehicle(origin=vehicle)
|
|
1273
|
+
vehicle = SkodaCombustionVehicle(garage=self.car_connectivity.garage, origin=vehicle)
|
|
628
1274
|
self.car_connectivity.garage.replace_vehicle(vin, vehicle)
|
|
629
1275
|
elif car_type == GenericVehicle.Type.HYBRID and not isinstance(vehicle, SkodaHybridVehicle):
|
|
630
1276
|
LOG.debug('Promoting %s to SkodaHybridVehicle object for %s', vehicle.__class__.__name__, vin)
|
|
631
|
-
vehicle = SkodaHybridVehicle(origin=vehicle)
|
|
1277
|
+
vehicle = SkodaHybridVehicle(garage=self.car_connectivity.garage, origin=vehicle)
|
|
632
1278
|
self.car_connectivity.garage.replace_vehicle(vin, vehicle)
|
|
633
1279
|
except ValueError:
|
|
634
1280
|
LOG_API.warning('Unknown car type %s', range_data['carType'])
|
|
@@ -637,6 +1283,7 @@ class Connector(BaseConnector):
|
|
|
637
1283
|
if 'totalRangeInKm' in range_data and range_data['totalRangeInKm'] is not None:
|
|
638
1284
|
# pylint: disable-next=protected-access
|
|
639
1285
|
vehicle.drives.total_range._set_value(value=range_data['totalRangeInKm'], measured=captured_at, unit=Length.KM)
|
|
1286
|
+
vehicle.drives.total_range.precision = 1
|
|
640
1287
|
else:
|
|
641
1288
|
vehicle.drives.total_range._set_value(None, measured=captured_at, unit=Length.KM) # pylint: disable=protected-access
|
|
642
1289
|
|
|
@@ -654,10 +1301,11 @@ class Connector(BaseConnector):
|
|
|
654
1301
|
else:
|
|
655
1302
|
if engine_type == GenericDrive.Type.ELECTRIC:
|
|
656
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)
|
|
657
1306
|
elif engine_type in [GenericDrive.Type.FUEL,
|
|
658
1307
|
GenericDrive.Type.GASOLINE,
|
|
659
1308
|
GenericDrive.Type.PETROL,
|
|
660
|
-
GenericDrive.Type.DIESEL,
|
|
661
1309
|
GenericDrive.Type.CNG,
|
|
662
1310
|
GenericDrive.Type.LPG]:
|
|
663
1311
|
drive = CombustionDrive(drive_id=drive_id, drives=vehicle.drives)
|
|
@@ -669,15 +1317,18 @@ class Connector(BaseConnector):
|
|
|
669
1317
|
and range_data[f'{drive_id}EngineRange']['currentSoCInPercent'] is not None:
|
|
670
1318
|
# pylint: disable-next=protected-access
|
|
671
1319
|
drive.level._set_value(value=range_data[f'{drive_id}EngineRange']['currentSoCInPercent'], measured=captured_at)
|
|
1320
|
+
drive.level.precision = 1
|
|
672
1321
|
elif 'currentFuelLevelInPercent' in range_data[f'{drive_id}EngineRange'] \
|
|
673
1322
|
and range_data[f'{drive_id}EngineRange']['currentFuelLevelInPercent'] is not None:
|
|
674
1323
|
# pylint: disable-next=protected-access
|
|
675
1324
|
drive.level._set_value(value=range_data[f'{drive_id}EngineRange']['currentFuelLevelInPercent'], measured=captured_at)
|
|
1325
|
+
drive.level.precision = 1
|
|
676
1326
|
else:
|
|
677
1327
|
drive.level._set_value(None, measured=captured_at) # pylint: disable=protected-access
|
|
678
1328
|
if 'remainingRangeInKm' in range_data[f'{drive_id}EngineRange'] and range_data[f'{drive_id}EngineRange']['remainingRangeInKm'] is not None:
|
|
679
1329
|
# pylint: disable-next=protected-access
|
|
680
1330
|
drive.range._set_value(value=range_data[f'{drive_id}EngineRange']['remainingRangeInKm'], measured=captured_at, unit=Length.KM)
|
|
1331
|
+
drive.range.precision = 1
|
|
681
1332
|
else:
|
|
682
1333
|
drive.range._set_value(None, measured=captured_at, unit=Length.KM) # pylint: disable=protected-access
|
|
683
1334
|
|
|
@@ -685,14 +1336,27 @@ class Connector(BaseConnector):
|
|
|
685
1336
|
'currentSoCInPercent',
|
|
686
1337
|
'currentFuelLevelInPercent',
|
|
687
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)
|
|
688
1351
|
log_extra_keys(LOG_API, '/api/v2/vehicle-status/{vin}/driving-range', range_data, {'carCapturedTimestamp',
|
|
689
1352
|
'carType',
|
|
690
1353
|
'totalRangeInKm',
|
|
1354
|
+
'adBlueRange',
|
|
691
1355
|
'primaryEngineRange',
|
|
692
1356
|
'secondaryEngineRange'})
|
|
693
1357
|
return vehicle
|
|
694
1358
|
|
|
695
|
-
def
|
|
1359
|
+
def fetch_vehicle_status(self, vehicle: SkodaVehicle, no_cache: bool = False) -> SkodaVehicle:
|
|
696
1360
|
"""
|
|
697
1361
|
Fetches the status of a vehicle from other Skoda API.
|
|
698
1362
|
|
|
@@ -705,171 +1369,88 @@ class Connector(BaseConnector):
|
|
|
705
1369
|
vin = vehicle.vin.value
|
|
706
1370
|
if vin is None:
|
|
707
1371
|
raise APIError('VIN is missing')
|
|
708
|
-
url = f'https://api.connect.skoda-auto.cz/api/v2/vehicle-status/{vin}'
|
|
1372
|
+
url = f'https://mysmob.api.connect.skoda-auto.cz/api/v2/vehicle-status/{vin}'
|
|
709
1373
|
vehicle_status_data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
|
|
710
1374
|
if vehicle_status_data:
|
|
711
|
-
if '
|
|
712
|
-
|
|
713
|
-
if vehicle_status_data:
|
|
714
|
-
if 'capturedAt' in vehicle_status_data and vehicle_status_data['capturedAt'] is not None:
|
|
715
|
-
captured_at: datetime = robust_time_parse(vehicle_status_data['capturedAt'])
|
|
1375
|
+
if 'carCapturedTimestamp' in vehicle_status_data and vehicle_status_data['carCapturedTimestamp'] is not None:
|
|
1376
|
+
captured_at: Optional[datetime] = robust_time_parse(vehicle_status_data['carCapturedTimestamp'])
|
|
716
1377
|
else:
|
|
717
|
-
|
|
718
|
-
if '
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
1378
|
+
captured_at: Optional[datetime] = None
|
|
1379
|
+
if 'overall' in vehicle_status_data and vehicle_status_data['overall'] is not None:
|
|
1380
|
+
if 'doorsLocked' in vehicle_status_data['overall'] and vehicle_status_data['overall']['doorsLocked'] is not None \
|
|
1381
|
+
and vehicle.doors is not None:
|
|
1382
|
+
if vehicle_status_data['overall']['doorsLocked'] == 'YES':
|
|
1383
|
+
vehicle.doors.lock_state._set_value(Doors.LockState.LOCKED, measured=captured_at) # pylint: disable=protected-access
|
|
1384
|
+
vehicle.doors.open_state._set_value(Doors.OpenState.CLOSED, measured=captured_at) # pylint: disable=protected-access
|
|
1385
|
+
elif vehicle_status_data['overall']['doorsLocked'] == 'NO':
|
|
1386
|
+
vehicle.doors.lock_state._set_value(Doors.LockState.UNLOCKED, measured=captured_at) # pylint: disable=protected-access
|
|
1387
|
+
vehicle.doors.open_state._set_value(Doors.OpenState.UNKNOWN, measured=captured_at) # pylint: disable=protected-access
|
|
1388
|
+
elif vehicle_status_data['overall']['doorsLocked'] == 'OPENED':
|
|
1389
|
+
vehicle.doors.lock_state._set_value(Doors.LockState.UNLOCKED, measured=captured_at) # pylint: disable=protected-access
|
|
725
1390
|
vehicle.doors.open_state._set_value(Doors.OpenState.OPEN, measured=captured_at) # pylint: disable=protected-access
|
|
726
|
-
elif vehicle_status_data['
|
|
1391
|
+
elif vehicle_status_data['overall']['doorsLocked'] == 'UNLOCKED':
|
|
1392
|
+
vehicle.doors.lock_state._set_value(Doors.LockState.UNLOCKED, measured=captured_at) # pylint: disable=protected-access
|
|
727
1393
|
vehicle.doors.open_state._set_value(Doors.OpenState.CLOSED, measured=captured_at) # pylint: disable=protected-access
|
|
1394
|
+
elif vehicle_status_data['overall']['doorsLocked'] == 'TRUNK_OPENED':
|
|
1395
|
+
vehicle.doors.lock_state._set_value(Doors.LockState.UNLOCKED, measured=captured_at) # pylint: disable=protected-access
|
|
1396
|
+
vehicle.doors.open_state._set_value(Doors.OpenState.OPEN, measured=captured_at) # pylint: disable=protected-access
|
|
1397
|
+
elif vehicle_status_data['overall']['doorsLocked'] == 'UNKNOWN':
|
|
1398
|
+
vehicle.doors.lock_state._set_value(Doors.LockState.UNKNOWN, measured=captured_at) # pylint: disable=protected-access
|
|
1399
|
+
vehicle.doors.open_state._set_value(Doors.OpenState.UNKNOWN, measured=captured_at) # pylint: disable=protected-access
|
|
728
1400
|
else:
|
|
1401
|
+
LOG_API.info('Unknown doorsLocked state %s', vehicle_status_data['overall']['doorsLocked'])
|
|
1402
|
+
vehicle.doors.lock_state._set_value(Doors.LockState.UNKNOWN, measured=captured_at) # pylint: disable=protected-access
|
|
729
1403
|
vehicle.doors.open_state._set_value(Doors.OpenState.UNKNOWN, measured=captured_at) # pylint: disable=protected-access
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
vehicle.doors.open_state._set_value(None, measured=captured_at) # pylint: disable=protected-access
|
|
733
|
-
if 'locked' in vehicle_status_data['status'] and vehicle_status_data['status']['locked'] is not None:
|
|
734
|
-
if vehicle_status_data['status']['locked'] == 'YES':
|
|
1404
|
+
if 'locked' in vehicle_status_data['overall'] and vehicle_status_data['overall']['locked'] is not None:
|
|
1405
|
+
if vehicle_status_data['overall']['locked'] == 'YES':
|
|
735
1406
|
vehicle.doors.lock_state._set_value(Doors.LockState.LOCKED, measured=captured_at) # pylint: disable=protected-access
|
|
736
|
-
elif vehicle_status_data['
|
|
1407
|
+
elif vehicle_status_data['overall']['locked'] == 'NO':
|
|
737
1408
|
vehicle.doors.lock_state._set_value(Doors.LockState.UNLOCKED, measured=captured_at) # pylint: disable=protected-access
|
|
1409
|
+
elif vehicle_status_data['overall']['locked'] == 'UNKNOWN':
|
|
1410
|
+
vehicle.doors.lock_state._set_value(Doors.LockState.UNKNOWN, measured=captured_at) # pylint: disable=protected-access
|
|
738
1411
|
else:
|
|
1412
|
+
LOG_API.info('Unknown locked state %s', vehicle_status_data['overall']['locked'])
|
|
739
1413
|
vehicle.doors.lock_state._set_value(Doors.LockState.UNKNOWN, measured=captured_at) # pylint: disable=protected-access
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
if 'name' in door_status and door_status['name'] is not None:
|
|
750
|
-
door_id = door_status['name']
|
|
751
|
-
seen_door_ids.add(door_id)
|
|
752
|
-
if door_id in vehicle.doors.doors:
|
|
753
|
-
door: Doors.Door = vehicle.doors.doors[door_id]
|
|
754
|
-
else:
|
|
755
|
-
door = Doors.Door(door_id=door_id, doors=vehicle.doors)
|
|
756
|
-
vehicle.doors.doors[door_id] = door
|
|
757
|
-
if 'status' in door_status and door_status['status'] is not None:
|
|
758
|
-
if door_status['status'] == 'OPEN':
|
|
759
|
-
door.lock_state._set_value(Doors.LockState.UNLOCKED, measured=captured_at) # pylint: disable=protected-access
|
|
760
|
-
door.open_state._set_value(Doors.OpenState.OPEN, measured=captured_at) # pylint: disable=protected-access
|
|
761
|
-
elif door_status['status'] == 'CLOSED':
|
|
762
|
-
door.lock_state._set_value(Doors.LockState.UNKNOWN, measured=captured_at) # pylint: disable=protected-access
|
|
763
|
-
door.open_state._set_value(Doors.OpenState.CLOSED, measured=captured_at) # pylint: disable=protected-access
|
|
764
|
-
elif door_status['status'] == 'LOCKED':
|
|
765
|
-
door.lock_state._set_value(Doors.LockState.LOCKED, measured=captured_at) # pylint: disable=protected-access
|
|
766
|
-
door.open_state._set_value(Doors.OpenState.CLOSED, measured=captured_at) # pylint: disable=protected-access
|
|
767
|
-
elif door_status['status'] == 'UNSUPPORTED':
|
|
768
|
-
door.lock_state._set_value(Doors.LockState.UNKNOWN, measured=captured_at) # pylint: disable=protected-access
|
|
769
|
-
door.open_state._set_value(Doors.OpenState.UNKNOWN, measured=captured_at) # pylint: disable=protected-access
|
|
770
|
-
else:
|
|
771
|
-
LOG_API.info('Unknown door status %s', door_status['status'])
|
|
772
|
-
door.lock_state._set_value(Doors.LockState.UNKNOWN, measured=captured_at) # pylint: disable=protected-access
|
|
773
|
-
door.open_state._set_value(Doors.OpenState.UNKNOWN, measured=captured_at) # pylint: disable=protected-access
|
|
774
|
-
else:
|
|
775
|
-
door.lock_state._set_value(None, measured=captured_at) # pylint: disable=protected-access
|
|
776
|
-
door.open_state._set_value(None, measured=captured_at) # pylint: disable=protected-access
|
|
1414
|
+
if 'doors' in vehicle_status_data['overall'] and vehicle_status_data['overall']['doors'] is not None:
|
|
1415
|
+
if vehicle_status_data['overall']['doors'] == 'CLOSED':
|
|
1416
|
+
vehicle.doors.open_state._set_value(Doors.OpenState.CLOSED, measured=captured_at) # pylint: disable=protected-access
|
|
1417
|
+
elif vehicle_status_data['overall']['doors'] == 'OPEN':
|
|
1418
|
+
vehicle.doors.open_state._set_value(Doors.OpenState.OPEN, measured=captured_at) # pylint: disable=protected-access
|
|
1419
|
+
elif vehicle_status_data['overall']['doors'] == 'UNSUPPORTED':
|
|
1420
|
+
vehicle.doors.open_state._set_value(Doors.OpenState.UNSUPPORTED, measured=captured_at) # pylint: disable=protected-access
|
|
1421
|
+
elif vehicle_status_data['overall']['doors'] == 'UNKNOWN':
|
|
1422
|
+
vehicle.doors.open_state._set_value(Doors.OpenState.UNKNOWN, measured=captured_at) # pylint: disable=protected-access
|
|
777
1423
|
else:
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
all_windows_closed: bool = True
|
|
790
|
-
for window_status in vehicle_status_data['windows']:
|
|
791
|
-
if 'name' in window_status and window_status['name'] is not None:
|
|
792
|
-
window_id = window_status['name']
|
|
793
|
-
seen_window_ids.add(window_id)
|
|
794
|
-
if window_id in vehicle.windows.windows:
|
|
795
|
-
window: Windows.Window = vehicle.windows.windows[window_id]
|
|
796
|
-
else:
|
|
797
|
-
window = Windows.Window(window_id=window_id, windows=vehicle.windows)
|
|
798
|
-
vehicle.windows.windows[window_id] = window
|
|
799
|
-
if 'status' in window_status and window_status['status'] is not None:
|
|
800
|
-
if window_status['status'] == 'OPEN':
|
|
801
|
-
all_windows_closed = False
|
|
802
|
-
window.open_state._set_value(Windows.OpenState.OPEN, measured=captured_at) # pylint: disable=protected-access
|
|
803
|
-
elif window_status['status'] == 'CLOSED':
|
|
804
|
-
window.open_state._set_value(Windows.OpenState.CLOSED, measured=captured_at) # pylint: disable=protected-access
|
|
805
|
-
elif window_status['status'] == 'UNSUPPORTED':
|
|
806
|
-
window.open_state._set_value(Windows.OpenState.UNSUPPORTED, measured=captured_at) # pylint: disable=protected-access
|
|
807
|
-
elif window_status['status'] == 'INVALID':
|
|
808
|
-
window.open_state._set_value(Windows.OpenState.INVALID, measured=captured_at) # pylint: disable=protected-access
|
|
809
|
-
else:
|
|
810
|
-
LOG_API.info('Unknown window status %s', window_status['status'])
|
|
811
|
-
window.open_state._set_value(Windows.OpenState.UNKNOWN, measured=captured_at) # pylint: disable=protected-access
|
|
812
|
-
else:
|
|
813
|
-
window.open_state._set_value(None, measured=captured_at) # pylint: disable=protected-access
|
|
1424
|
+
LOG_API.info('Unknown doors state %s', vehicle_status_data['overall']['doors'])
|
|
1425
|
+
vehicle.doors.open_state._set_value(Doors.OpenState.UNKNOWN, measured=captured_at) # pylint: disable=protected-access
|
|
1426
|
+
if 'windows' in vehicle_status_data['overall'] and vehicle_status_data['overall']['windows'] is not None:
|
|
1427
|
+
if vehicle_status_data['overall']['windows'] == 'CLOSED':
|
|
1428
|
+
vehicle.windows.open_state._set_value(Windows.OpenState.CLOSED, measured=captured_at) # pylint: disable=protected-access
|
|
1429
|
+
elif vehicle_status_data['overall']['windows'] == 'OPEN':
|
|
1430
|
+
vehicle.windows.open_state._set_value(Windows.OpenState.OPEN, measured=captured_at) # pylint: disable=protected-access
|
|
1431
|
+
elif vehicle_status_data['overall']['windows'] == 'UNKNOWN':
|
|
1432
|
+
vehicle.windows.open_state._set_value(Windows.OpenState.UNKNOWN, measured=captured_at) # pylint: disable=protected-access
|
|
1433
|
+
elif vehicle_status_data['overall']['windows'] == 'UNSUPPORTED':
|
|
1434
|
+
vehicle.windows.open_state._set_value(Windows.OpenState.UNSUPPORTED, measured=captured_at) # pylint: disable=protected-access
|
|
814
1435
|
else:
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
vehicle.windows.windows.pop(window_to_remove)
|
|
820
|
-
if all_windows_closed:
|
|
821
|
-
vehicle.windows.open_state._set_value(Windows.OpenState.CLOSED, measured=captured_at) # pylint: disable=protected-access
|
|
822
|
-
else:
|
|
823
|
-
vehicle.windows.open_state._set_value(Windows.OpenState.OPEN, measured=captured_at) # pylint: disable=protected-access
|
|
824
|
-
else:
|
|
825
|
-
vehicle.windows.open_state._set_value(None, measured=captured_at) # pylint: disable=protected-access
|
|
826
|
-
vehicle.windows.windows = {}
|
|
827
|
-
if 'lights' in vehicle_status_data and vehicle_status_data['lights'] is not None:
|
|
828
|
-
seen_light_ids: set[str] = set()
|
|
829
|
-
if 'overallStatus' in vehicle_status_data['lights'] and vehicle_status_data['lights']['overallStatus'] is not None:
|
|
830
|
-
if vehicle_status_data['lights']['overallStatus'] == 'ON':
|
|
1436
|
+
LOG_API.info('Unknown windows state %s', vehicle_status_data['overall']['windows'])
|
|
1437
|
+
vehicle.windows.open_state._set_value(Windows.OpenState.UNKNOWN, measured=captured_at) # pylint: disable=protected-access
|
|
1438
|
+
if 'lights' in vehicle_status_data['overall'] and vehicle_status_data['overall']['lights'] is not None:
|
|
1439
|
+
if vehicle_status_data['overall']['lights'] == 'ON':
|
|
831
1440
|
vehicle.lights.light_state._set_value(Lights.LightState.ON, measured=captured_at) # pylint: disable=protected-access
|
|
832
|
-
elif vehicle_status_data['
|
|
1441
|
+
elif vehicle_status_data['overall']['lights'] == 'OFF':
|
|
833
1442
|
vehicle.lights.light_state._set_value(Lights.LightState.OFF, measured=captured_at) # pylint: disable=protected-access
|
|
834
|
-
elif vehicle_status_data['
|
|
835
|
-
vehicle.lights.light_state._set_value(Lights.LightState.
|
|
1443
|
+
elif vehicle_status_data['overall']['lights'] == 'UNKNOWN':
|
|
1444
|
+
vehicle.lights.light_state._set_value(Lights.LightState.UNKNOWN, measured=captured_at) # pylint: disable=protected-access
|
|
836
1445
|
else:
|
|
837
|
-
LOG_API.info('Unknown
|
|
1446
|
+
LOG_API.info('Unknown lights state %s', vehicle_status_data['overall']['lights'])
|
|
838
1447
|
vehicle.lights.light_state._set_value(Lights.LightState.UNKNOWN, measured=captured_at) # pylint: disable=protected-access
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
seen_light_ids.add(light_id)
|
|
846
|
-
if light_id in vehicle.lights.lights:
|
|
847
|
-
light: Lights.Light = vehicle.lights.lights[light_id]
|
|
848
|
-
else:
|
|
849
|
-
light = Lights.Light(light_id=light_id, lights=vehicle.lights)
|
|
850
|
-
vehicle.lights.lights[light_id] = light
|
|
851
|
-
if 'status' in light_status and light_status['status'] is not None:
|
|
852
|
-
if light_status['status'] == 'ON':
|
|
853
|
-
light.light_state._set_value(Lights.LightState.ON, measured=captured_at) # pylint: disable=protected-access
|
|
854
|
-
elif light_status['status'] == 'OFF':
|
|
855
|
-
light.light_state._set_value(Lights.LightState.OFF, measured=captured_at) # pylint: disable=protected-access
|
|
856
|
-
elif light_status['status'] == 'INVALID':
|
|
857
|
-
light.light_state._set_value(Lights.LightState.INVALID, measured=captured_at) # pylint: disable=protected-access
|
|
858
|
-
else:
|
|
859
|
-
LOG_API.info('Unknown light status %s', light_status['status'])
|
|
860
|
-
light.light_state._set_value(Lights.LightState.UNKNOWN, measured=captured_at) # pylint: disable=protected-access
|
|
861
|
-
else:
|
|
862
|
-
light.light_state._set_value(None, measured=captured_at) # pylint: disable=protected-access
|
|
863
|
-
else:
|
|
864
|
-
raise APIError('Could not parse light, name missing')
|
|
865
|
-
log_extra_keys(LOG_API, 'lights', light_status, {'name', 'status'})
|
|
866
|
-
for light_to_remove in set(vehicle.lights.lights) - seen_light_ids:
|
|
867
|
-
vehicle.lights.lights[light_to_remove].enabled = False
|
|
868
|
-
vehicle.lights.lights.pop(light_to_remove)
|
|
869
|
-
else:
|
|
870
|
-
vehicle.lights.lights = {}
|
|
871
|
-
log_extra_keys(LOG_API, 'lights', vehicle_status_data['lights'], {'overallStatus', 'lightsStatus'})
|
|
872
|
-
log_extra_keys(LOG_API, 'vehicles', vehicle_status_data, {'capturedAt', 'mileageInKm', 'status', 'doors', 'windows', 'lights'})
|
|
1448
|
+
log_extra_keys(LOG_API, 'status overall', vehicle_status_data['overall'], {'doorsLocked',
|
|
1449
|
+
'locked',
|
|
1450
|
+
'doors',
|
|
1451
|
+
'windows',
|
|
1452
|
+
'lights'})
|
|
1453
|
+
log_extra_keys(LOG_API, f'/api/v2/vehicle-status/{vin}', vehicle_status_data, {'overall', 'carCapturedTimestamp'})
|
|
873
1454
|
return vehicle
|
|
874
1455
|
|
|
875
1456
|
def _record_elapsed(self, elapsed: timedelta) -> None:
|
|
@@ -885,11 +1466,11 @@ class Connector(BaseConnector):
|
|
|
885
1466
|
allowed_errors=None) -> Optional[Dict[str, Any]]: # noqa: C901
|
|
886
1467
|
data: Optional[Dict[str, Any]] = None
|
|
887
1468
|
cache_date: Optional[datetime] = None
|
|
888
|
-
if not no_cache and (self.max_age is not None and session.cache is not None and url in session.cache):
|
|
1469
|
+
if not no_cache and (self.active_config['max_age'] is not None and session.cache is not None and url in session.cache):
|
|
889
1470
|
data, cache_date_string = session.cache[url]
|
|
890
1471
|
cache_date = datetime.fromisoformat(cache_date_string)
|
|
891
|
-
if data is None or self.max_age is None \
|
|
892
|
-
or (cache_date is not None and cache_date < (datetime.utcnow() - timedelta(seconds=self.max_age))):
|
|
1472
|
+
if data is None or self.active_config['max_age'] is None \
|
|
1473
|
+
or (cache_date is not None and cache_date < (datetime.utcnow() - timedelta(seconds=self.active_config['max_age']))):
|
|
893
1474
|
try:
|
|
894
1475
|
status_response: requests.Response = session.get(url, allow_redirects=False)
|
|
895
1476
|
self._record_elapsed(status_response.elapsed)
|
|
@@ -897,6 +1478,8 @@ class Connector(BaseConnector):
|
|
|
897
1478
|
data = status_response.json()
|
|
898
1479
|
if session.cache is not None:
|
|
899
1480
|
session.cache[url] = (data, str(datetime.utcnow()))
|
|
1481
|
+
elif status_response.status_code == requests.codes['no_content'] and allow_empty:
|
|
1482
|
+
data = None
|
|
900
1483
|
elif status_response.status_code == requests.codes['too_many_requests']:
|
|
901
1484
|
raise TooManyRequestsError('Could not fetch data due to too many requests from your account. '
|
|
902
1485
|
f'Status Code was: {status_response.status_code}')
|
|
@@ -912,7 +1495,7 @@ class Connector(BaseConnector):
|
|
|
912
1495
|
elif not allow_http_error or (allowed_errors is not None and status_response.status_code not in allowed_errors):
|
|
913
1496
|
raise RetrievalError(f'Could not fetch data even after re-authorization. Status Code was: {status_response.status_code}')
|
|
914
1497
|
elif not allow_http_error or (allowed_errors is not None and status_response.status_code not in allowed_errors):
|
|
915
|
-
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}')
|
|
916
1499
|
except requests.exceptions.ConnectionError as connection_error:
|
|
917
1500
|
raise RetrievalError(f'Connection error: {connection_error}.'
|
|
918
1501
|
' If this happens frequently, please check if other applications communicate with the Skoda server.') from connection_error
|
|
@@ -931,3 +1514,541 @@ class Connector(BaseConnector):
|
|
|
931
1514
|
|
|
932
1515
|
def get_version(self) -> str:
|
|
933
1516
|
return __version__
|
|
1517
|
+
|
|
1518
|
+
def get_type(self) -> str:
|
|
1519
|
+
return "carconnectivity-connector-skoda"
|
|
1520
|
+
|
|
1521
|
+
def __on_air_conditioning_target_temperature_change(self, temperature_attribute: TemperatureAttribute, target_temperature: float) -> float:
|
|
1522
|
+
"""
|
|
1523
|
+
Callback for the climatization target temperature change.
|
|
1524
|
+
|
|
1525
|
+
Args:
|
|
1526
|
+
temperature_attribute (TemperatureAttribute): The temperature attribute that changed.
|
|
1527
|
+
target_temperature (float): The new target temperature.
|
|
1528
|
+
"""
|
|
1529
|
+
if temperature_attribute.parent is None or temperature_attribute.parent.parent is None \
|
|
1530
|
+
or temperature_attribute.parent.parent.parent is None or not isinstance(temperature_attribute.parent.parent.parent, SkodaVehicle):
|
|
1531
|
+
raise SetterError('Object hierarchy is not as expected')
|
|
1532
|
+
vehicle: SkodaVehicle = temperature_attribute.parent.parent.parent
|
|
1533
|
+
vin: Optional[str] = vehicle.vin.value
|
|
1534
|
+
if vin is None:
|
|
1535
|
+
raise SetterError('VIN in object hierarchy missing')
|
|
1536
|
+
setting_dict = {}
|
|
1537
|
+
# Round target temperature to nearest 0.5
|
|
1538
|
+
setting_dict['temperatureValue'] = round(target_temperature * 2) / 2
|
|
1539
|
+
if temperature_attribute.unit == Temperature.C:
|
|
1540
|
+
setting_dict['unitInCar'] = 'CELSIUS'
|
|
1541
|
+
elif temperature_attribute.unit == Temperature.F:
|
|
1542
|
+
setting_dict['unitInCar'] = 'FAHRENHEIT'
|
|
1543
|
+
elif temperature_attribute.unit == Temperature.K:
|
|
1544
|
+
setting_dict['unitInCar'] = 'KELVIN'
|
|
1545
|
+
else:
|
|
1546
|
+
raise SetterError(f'Unknown temperature unit {temperature_attribute.unit}')
|
|
1547
|
+
|
|
1548
|
+
url = f'https://mysmob.api.connect.skoda-auto.cz/api/v2/air-conditioning/{vin}/settings/target-temperature'
|
|
1549
|
+
try:
|
|
1550
|
+
settings_response: requests.Response = self.session.post(url, data=json.dumps(setting_dict), allow_redirects=True)
|
|
1551
|
+
if settings_response.status_code != requests.codes['accepted']:
|
|
1552
|
+
LOG.error('Could not set target temperature (%s)', settings_response.status_code)
|
|
1553
|
+
raise SetterError(f'Could not set value ({settings_response.status_code})')
|
|
1554
|
+
except requests.exceptions.ConnectionError as connection_error:
|
|
1555
|
+
raise SetterError(f'Connection error: {connection_error}.'
|
|
1556
|
+
' If this happens frequently, please check if other applications communicate with the Skoda server.') from connection_error
|
|
1557
|
+
except requests.exceptions.ChunkedEncodingError as chunked_encoding_error:
|
|
1558
|
+
raise SetterError(f'Error: {chunked_encoding_error}') from chunked_encoding_error
|
|
1559
|
+
except requests.exceptions.ReadTimeout as timeout_error:
|
|
1560
|
+
raise SetterError(f'Timeout during read: {timeout_error}') from timeout_error
|
|
1561
|
+
except requests.exceptions.RetryError as retry_error:
|
|
1562
|
+
raise SetterError(f'Retrying failed: {retry_error}') from retry_error
|
|
1563
|
+
return target_temperature
|
|
1564
|
+
|
|
1565
|
+
def __on_air_conditioning_at_unlock_change(self, at_unlock_attribute: BooleanAttribute, at_unlock_value: bool) -> bool:
|
|
1566
|
+
if at_unlock_attribute.parent is None or at_unlock_attribute.parent.parent is None \
|
|
1567
|
+
or at_unlock_attribute.parent.parent.parent is None or not isinstance(at_unlock_attribute.parent.parent.parent, SkodaVehicle):
|
|
1568
|
+
raise SetterError('Object hierarchy is not as expected')
|
|
1569
|
+
vehicle: SkodaVehicle = at_unlock_attribute.parent.parent.parent
|
|
1570
|
+
vin: Optional[str] = vehicle.vin.value
|
|
1571
|
+
if vin is None:
|
|
1572
|
+
raise SetterError('VIN in object hierarchy missing')
|
|
1573
|
+
setting_dict = {}
|
|
1574
|
+
# Round target temperature to nearest 0.5
|
|
1575
|
+
setting_dict['airConditioningAtUnlockEnabled'] = at_unlock_value
|
|
1576
|
+
|
|
1577
|
+
url = f'https://mysmob.api.connect.skoda-auto.cz/api/v2/air-conditioning/{vin}/settings/ac-at-unlock'
|
|
1578
|
+
try:
|
|
1579
|
+
settings_response: requests.Response = self.session.post(url, data=json.dumps(setting_dict), allow_redirects=True)
|
|
1580
|
+
if settings_response.status_code != requests.codes['accepted']:
|
|
1581
|
+
LOG.error('Could not set air conditioning at unlock (%s)', settings_response.status_code)
|
|
1582
|
+
raise SetterError(f'Could not set value ({settings_response.status_code})')
|
|
1583
|
+
except requests.exceptions.ConnectionError as connection_error:
|
|
1584
|
+
raise SetterError(f'Connection error: {connection_error}.'
|
|
1585
|
+
' If this happens frequently, please check if other applications communicate with the Skoda server.') from connection_error
|
|
1586
|
+
except requests.exceptions.ChunkedEncodingError as chunked_encoding_error:
|
|
1587
|
+
raise SetterError(f'Error: {chunked_encoding_error}') from chunked_encoding_error
|
|
1588
|
+
except requests.exceptions.ReadTimeout as timeout_error:
|
|
1589
|
+
raise SetterError(f'Timeout during read: {timeout_error}') from timeout_error
|
|
1590
|
+
except requests.exceptions.RetryError as retry_error:
|
|
1591
|
+
raise SetterError(f'Retrying failed: {retry_error}') from retry_error
|
|
1592
|
+
return at_unlock_value
|
|
1593
|
+
|
|
1594
|
+
def __on_air_conditioning_window_heating_change(self, window_heating_attribute: BooleanAttribute, window_heating_value: bool) -> bool:
|
|
1595
|
+
if window_heating_attribute.parent is None or window_heating_attribute.parent.parent is None \
|
|
1596
|
+
or window_heating_attribute.parent.parent.parent is None or not isinstance(window_heating_attribute.parent.parent.parent, SkodaVehicle):
|
|
1597
|
+
raise SetterError('Object hierarchy is not as expected')
|
|
1598
|
+
vehicle: SkodaVehicle = window_heating_attribute.parent.parent.parent
|
|
1599
|
+
vin: Optional[str] = vehicle.vin.value
|
|
1600
|
+
if vin is None:
|
|
1601
|
+
raise SetterError('VIN in object hierarchy missing')
|
|
1602
|
+
setting_dict = {}
|
|
1603
|
+
# Round target temperature to nearest 0.5
|
|
1604
|
+
setting_dict['windowHeatingEnabled'] = window_heating_value
|
|
1605
|
+
|
|
1606
|
+
url = f'https://mysmob.api.connect.skoda-auto.cz/api/v2/air-conditioning/{vin}/settings/ac-at-unlock'
|
|
1607
|
+
try:
|
|
1608
|
+
settings_response: requests.Response = self.session.post(url, data=json.dumps(setting_dict), allow_redirects=True)
|
|
1609
|
+
if settings_response.status_code != requests.codes['accepted']:
|
|
1610
|
+
LOG.error('Could not set air conditioning window heating (%s)', settings_response.status_code)
|
|
1611
|
+
raise SetterError(f'Could not set value ({settings_response.status_code})')
|
|
1612
|
+
except requests.exceptions.ConnectionError as connection_error:
|
|
1613
|
+
raise SetterError(f'Connection error: {connection_error}.'
|
|
1614
|
+
' If this happens frequently, please check if other applications communicate with the Skoda server.') from connection_error
|
|
1615
|
+
except requests.exceptions.ChunkedEncodingError as chunked_encoding_error:
|
|
1616
|
+
raise SetterError(f'Error: {chunked_encoding_error}') from chunked_encoding_error
|
|
1617
|
+
except requests.exceptions.ReadTimeout as timeout_error:
|
|
1618
|
+
raise SetterError(f'Timeout during read: {timeout_error}') from timeout_error
|
|
1619
|
+
except requests.exceptions.RetryError as retry_error:
|
|
1620
|
+
raise SetterError(f'Retrying failed: {retry_error}') from retry_error
|
|
1621
|
+
return window_heating_value
|
|
1622
|
+
|
|
1623
|
+
def __on_air_conditioning_start_stop(self, start_stop_command: ClimatizationStartStopCommand, command_arguments: Union[str, Dict[str, Any]]) \
|
|
1624
|
+
-> Union[str, Dict[str, Any]]:
|
|
1625
|
+
if start_stop_command.parent is None or start_stop_command.parent.parent is None \
|
|
1626
|
+
or start_stop_command.parent.parent.parent is None or not isinstance(start_stop_command.parent.parent.parent, SkodaVehicle):
|
|
1627
|
+
raise CommandError('Object hierarchy is not as expected')
|
|
1628
|
+
if not isinstance(command_arguments, dict):
|
|
1629
|
+
raise CommandError('Command arguments are not a dictionary')
|
|
1630
|
+
vehicle: SkodaVehicle = start_stop_command.parent.parent.parent
|
|
1631
|
+
vin: Optional[str] = vehicle.vin.value
|
|
1632
|
+
if vin is None:
|
|
1633
|
+
raise CommandError('VIN in object hierarchy missing')
|
|
1634
|
+
if 'command' not in command_arguments:
|
|
1635
|
+
raise CommandError('Command argument missing')
|
|
1636
|
+
command_dict = {}
|
|
1637
|
+
try:
|
|
1638
|
+
if command_arguments['command'] == ClimatizationStartStopCommand.Command.START:
|
|
1639
|
+
command_dict['heaterSource'] = 'ELECTRIC'
|
|
1640
|
+
command_dict['targetTemperature'] = {}
|
|
1641
|
+
precision: float = 0.5
|
|
1642
|
+
if 'target_temperature' in command_arguments:
|
|
1643
|
+
# Round target temperature to nearest 0.5
|
|
1644
|
+
command_dict['targetTemperature']['temperatureValue'] = round(command_arguments['target_temperature'] / precision) * precision
|
|
1645
|
+
if 'target_temperature_unit' in command_arguments:
|
|
1646
|
+
if not isinstance(command_arguments['target_temperature_unit'], Temperature):
|
|
1647
|
+
raise CommandError('Temperature unit is not of type Temperature')
|
|
1648
|
+
if command_arguments['target_temperature_unit'] == Temperature.C:
|
|
1649
|
+
command_dict['targetTemperature']['unitInCar'] = 'CELSIUS'
|
|
1650
|
+
elif command_arguments['target_temperature_unit'] == Temperature.F:
|
|
1651
|
+
command_dict['targetTemperature']['unitInCar'] = 'FAHRENHEIT'
|
|
1652
|
+
elif command_arguments['target_temperature_unit'] == Temperature.K:
|
|
1653
|
+
command_dict['targetTemperature']['unitInCar'] = 'KELVIN'
|
|
1654
|
+
else:
|
|
1655
|
+
raise CommandError(f'Unknown temperature unit {command_arguments["target_temperature_unit"]}')
|
|
1656
|
+
else:
|
|
1657
|
+
command_dict['targetTemperature']['unitInCar'] = 'CELSIUS'
|
|
1658
|
+
elif start_stop_command.parent is not None and (climatization := start_stop_command.parent.parent) is not None \
|
|
1659
|
+
and isinstance(climatization, Climatization) and climatization.settings is not None \
|
|
1660
|
+
and climatization.settings.target_temperature is not None and climatization.settings.target_temperature.enabled \
|
|
1661
|
+
and climatization.settings.target_temperature.value is not None: # pylint: disable=too-many-boolean-expressions
|
|
1662
|
+
if climatization.settings.target_temperature.precision is not None:
|
|
1663
|
+
precision = climatization.settings.target_temperature.precision
|
|
1664
|
+
# Round target temperature to nearest 0.5
|
|
1665
|
+
command_dict['targetTemperature']['temperatureValue'] = round(climatization.settings.target_temperature.value / precision) * precision
|
|
1666
|
+
if climatization.settings.target_temperature.unit == Temperature.C:
|
|
1667
|
+
command_dict['targetTemperature']['unitInCar'] = 'CELSIUS'
|
|
1668
|
+
elif climatization.settings.target_temperature.unit == Temperature.F:
|
|
1669
|
+
command_dict['targetTemperature']['unitInCar'] = 'FAHRENHEIT'
|
|
1670
|
+
elif climatization.settings.target_temperature.unit == Temperature.K:
|
|
1671
|
+
command_dict['targetTemperature']['unitInCar'] = 'KELVIN'
|
|
1672
|
+
else:
|
|
1673
|
+
raise CommandError(f'Unknown temperature unit {climatization.settings.target_temperature.unit}')
|
|
1674
|
+
else:
|
|
1675
|
+
command_dict['targetTemperature']['temperatureValue'] = 25.0
|
|
1676
|
+
command_dict['targetTemperature']['unitInCar'] = 'CELSIUS'
|
|
1677
|
+
url = f'https://mysmob.api.connect.skoda-auto.cz/api/v2/air-conditioning/{vin}/start'
|
|
1678
|
+
command_response: requests.Response = self.session.post(url, data=json.dumps(command_dict), allow_redirects=True)
|
|
1679
|
+
elif command_arguments['command'] == ClimatizationStartStopCommand.Command.STOP:
|
|
1680
|
+
url = f'https://mysmob.api.connect.skoda-auto.cz/api/v2/air-conditioning/{vin}/stop'
|
|
1681
|
+
command_response: requests.Response = self.session.post(url, allow_redirects=True)
|
|
1682
|
+
else:
|
|
1683
|
+
raise CommandError(f'Unknown command {command_arguments["command"]}')
|
|
1684
|
+
|
|
1685
|
+
if command_response.status_code != requests.codes['accepted']:
|
|
1686
|
+
LOG.error('Could not start/stop air conditioning (%s: %s)', command_response.status_code, command_response.text)
|
|
1687
|
+
raise CommandError(f'Could not start/stop air conditioning ({command_response.status_code}: {command_response.text})')
|
|
1688
|
+
except requests.exceptions.ConnectionError as connection_error:
|
|
1689
|
+
raise CommandError(f'Connection error: {connection_error}.'
|
|
1690
|
+
' If this happens frequently, please check if other applications communicate with the Skoda server.') from connection_error
|
|
1691
|
+
except requests.exceptions.ChunkedEncodingError as chunked_encoding_error:
|
|
1692
|
+
raise CommandError(f'Error: {chunked_encoding_error}') from chunked_encoding_error
|
|
1693
|
+
except requests.exceptions.ReadTimeout as timeout_error:
|
|
1694
|
+
raise CommandError(f'Timeout during read: {timeout_error}') from timeout_error
|
|
1695
|
+
except requests.exceptions.RetryError as retry_error:
|
|
1696
|
+
raise CommandError(f'Retrying failed: {retry_error}') from retry_error
|
|
1697
|
+
return command_arguments
|
|
1698
|
+
|
|
1699
|
+
def __on_charging_start_stop(self, start_stop_command: ChargingStartStopCommand, command_arguments: Union[str, Dict[str, Any]]) \
|
|
1700
|
+
-> Union[str, Dict[str, Any]]:
|
|
1701
|
+
if start_stop_command.parent is None or start_stop_command.parent.parent is None \
|
|
1702
|
+
or start_stop_command.parent.parent.parent is None or not isinstance(start_stop_command.parent.parent.parent, SkodaVehicle):
|
|
1703
|
+
raise CommandError('Object hierarchy is not as expected')
|
|
1704
|
+
if not isinstance(command_arguments, dict):
|
|
1705
|
+
raise CommandError('Command arguments are not a dictionary')
|
|
1706
|
+
vehicle: SkodaVehicle = start_stop_command.parent.parent.parent
|
|
1707
|
+
vin: Optional[str] = vehicle.vin.value
|
|
1708
|
+
if vin is None:
|
|
1709
|
+
raise CommandError('VIN in object hierarchy missing')
|
|
1710
|
+
if 'command' not in command_arguments:
|
|
1711
|
+
raise CommandError('Command argument missing')
|
|
1712
|
+
try:
|
|
1713
|
+
if command_arguments['command'] == ChargingStartStopCommand.Command.START:
|
|
1714
|
+
url = f'https://mysmob.api.connect.skoda-auto.cz/api/v1/charging/{vin}/start'
|
|
1715
|
+
command_response: requests.Response = self.session.post(url, allow_redirects=True)
|
|
1716
|
+
elif command_arguments['command'] == ChargingStartStopCommand.Command.STOP:
|
|
1717
|
+
url = f'https://mysmob.api.connect.skoda-auto.cz/api/v1/charging/{vin}/stop'
|
|
1718
|
+
|
|
1719
|
+
command_response: requests.Response = self.session.post(url, allow_redirects=True)
|
|
1720
|
+
else:
|
|
1721
|
+
raise CommandError(f'Unknown command {command_arguments["command"]}')
|
|
1722
|
+
|
|
1723
|
+
if command_response.status_code != requests.codes['accepted']:
|
|
1724
|
+
LOG.error('Could not start/stop charging (%s: %s)', command_response.status_code, command_response.text)
|
|
1725
|
+
raise CommandError(f'Could not start/stop charging ({command_response.status_code}: {command_response.text})')
|
|
1726
|
+
except requests.exceptions.ConnectionError as connection_error:
|
|
1727
|
+
raise CommandError(f'Connection error: {connection_error}.'
|
|
1728
|
+
' If this happens frequently, please check if other applications communicate with the Skoda server.') from connection_error
|
|
1729
|
+
except requests.exceptions.ChunkedEncodingError as chunked_encoding_error:
|
|
1730
|
+
raise CommandError(f'Error: {chunked_encoding_error}') from chunked_encoding_error
|
|
1731
|
+
except requests.exceptions.ReadTimeout as timeout_error:
|
|
1732
|
+
raise CommandError(f'Timeout during read: {timeout_error}') from timeout_error
|
|
1733
|
+
except requests.exceptions.RetryError as retry_error:
|
|
1734
|
+
raise CommandError(f'Retrying failed: {retry_error}') from retry_error
|
|
1735
|
+
return command_arguments
|
|
1736
|
+
|
|
1737
|
+
def __on_honk_flash(self, honk_flash_command: HonkAndFlashCommand, command_arguments: Union[str, Dict[str, Any]]) \
|
|
1738
|
+
-> Union[str, Dict[str, Any]]:
|
|
1739
|
+
if honk_flash_command.parent is None or honk_flash_command.parent.parent is None \
|
|
1740
|
+
or not isinstance(honk_flash_command.parent.parent, SkodaVehicle):
|
|
1741
|
+
raise CommandError('Object hierarchy is not as expected')
|
|
1742
|
+
if not isinstance(command_arguments, dict):
|
|
1743
|
+
raise CommandError('Command arguments are not a dictionary')
|
|
1744
|
+
vehicle: SkodaVehicle = honk_flash_command.parent.parent
|
|
1745
|
+
vin: Optional[str] = vehicle.vin.value
|
|
1746
|
+
if vin is None:
|
|
1747
|
+
raise CommandError('VIN in object hierarchy missing')
|
|
1748
|
+
if 'command' not in command_arguments:
|
|
1749
|
+
raise CommandError('Command argument missing')
|
|
1750
|
+
if 'duration' in command_arguments:
|
|
1751
|
+
LOG.warning('Duration argument is not supported by the Skoda API')
|
|
1752
|
+
command_dict = {}
|
|
1753
|
+
if command_arguments['command'] in [HonkAndFlashCommand.Command.FLASH, HonkAndFlashCommand.Command.HONK_AND_FLASH]:
|
|
1754
|
+
command_dict['mode'] = command_arguments['command'].name
|
|
1755
|
+
command_dict['vehiclePosition'] = {}
|
|
1756
|
+
if vehicle.position is None or vehicle.position.latitude is None or vehicle.position.longitude is None \
|
|
1757
|
+
or vehicle.position.latitude.value is None or vehicle.position.longitude.value is None \
|
|
1758
|
+
or not vehicle.position.latitude.enabled or not vehicle.position.longitude.enabled:
|
|
1759
|
+
raise CommandError('Can only execute honk and flash commands if vehicle position is known')
|
|
1760
|
+
command_dict['vehiclePosition']['latitude'] = vehicle.position.latitude.value
|
|
1761
|
+
command_dict['vehiclePosition']['longitude'] = vehicle.position.longitude.value
|
|
1762
|
+
|
|
1763
|
+
url = f'https://mysmob.api.connect.skoda-auto.cz/api/v1/vehicle-access/{vin}/honk-and-flash'
|
|
1764
|
+
try:
|
|
1765
|
+
command_response: requests.Response = self.session.post(url, data=json.dumps(command_dict), allow_redirects=True)
|
|
1766
|
+
if command_response.status_code != requests.codes['accepted']:
|
|
1767
|
+
LOG.error('Could not execute honk or flash command (%s: %s)', command_response.status_code, command_response.text)
|
|
1768
|
+
raise CommandError(f'Could not execute honk or flash command ({command_response.status_code}: {command_response.text})')
|
|
1769
|
+
except requests.exceptions.ConnectionError as connection_error:
|
|
1770
|
+
raise CommandError(f'Connection error: {connection_error}.'
|
|
1771
|
+
' If this happens frequently, please check if other applications communicate with the Skoda server.') from connection_error
|
|
1772
|
+
except requests.exceptions.ChunkedEncodingError as chunked_encoding_error:
|
|
1773
|
+
raise SetterError(f'Error: {chunked_encoding_error}') from chunked_encoding_error
|
|
1774
|
+
except requests.exceptions.ReadTimeout as timeout_error:
|
|
1775
|
+
raise CommandError(f'Timeout during read: {timeout_error}') from timeout_error
|
|
1776
|
+
except requests.exceptions.RetryError as retry_error:
|
|
1777
|
+
raise CommandError(f'Retrying failed: {retry_error}') from retry_error
|
|
1778
|
+
else:
|
|
1779
|
+
raise CommandError(f'Unknown command {command_arguments["command"]}')
|
|
1780
|
+
return command_arguments
|
|
1781
|
+
|
|
1782
|
+
def __on_lock_unlock(self, lock_unlock_command: LockUnlockCommand, command_arguments: Union[str, Dict[str, Any]]) \
|
|
1783
|
+
-> Union[str, Dict[str, Any]]:
|
|
1784
|
+
if lock_unlock_command.parent is None or lock_unlock_command.parent.parent is None \
|
|
1785
|
+
or lock_unlock_command.parent.parent.parent is None or not isinstance(lock_unlock_command.parent.parent.parent, SkodaVehicle):
|
|
1786
|
+
raise CommandError('Object hierarchy is not as expected')
|
|
1787
|
+
if not isinstance(command_arguments, dict):
|
|
1788
|
+
raise SetterError('Command arguments are not a dictionary')
|
|
1789
|
+
vehicle: SkodaVehicle = lock_unlock_command.parent.parent.parent
|
|
1790
|
+
vin: Optional[str] = vehicle.vin.value
|
|
1791
|
+
if vin is None:
|
|
1792
|
+
raise CommandError('VIN in object hierarchy missing')
|
|
1793
|
+
if 'command' not in command_arguments:
|
|
1794
|
+
raise CommandError('Command argument missing')
|
|
1795
|
+
command_dict = {}
|
|
1796
|
+
if 'spin' in command_arguments:
|
|
1797
|
+
command_dict['currentSpin'] = command_arguments['spin']
|
|
1798
|
+
else:
|
|
1799
|
+
if self.active_config['spin'] is None:
|
|
1800
|
+
raise CommandError('S-PIN is missing, please add S-PIN to your configuration or .netrc file')
|
|
1801
|
+
command_dict['currentSpin'] = self.active_config['spin']
|
|
1802
|
+
if command_arguments['command'] == LockUnlockCommand.Command.LOCK:
|
|
1803
|
+
url = f'https://mysmob.api.connect.skoda-auto.cz/api/v1/vehicle-access/{vin}/lock'
|
|
1804
|
+
elif command_arguments['command'] == LockUnlockCommand.Command.UNLOCK:
|
|
1805
|
+
url = f'https://mysmob.api.connect.skoda-auto.cz/api/v1/vehicle-access/{vin}/unlock'
|
|
1806
|
+
else:
|
|
1807
|
+
raise CommandError(f'Unknown command {command_arguments["command"]}')
|
|
1808
|
+
try:
|
|
1809
|
+
command_response: requests.Response = self.session.post(url, data=json.dumps(command_dict), allow_redirects=True)
|
|
1810
|
+
if command_response.status_code != requests.codes['accepted']:
|
|
1811
|
+
LOG.error('Could not execute locking command (%s: %s)', command_response.status_code, command_response.text)
|
|
1812
|
+
raise CommandError(f'Could not execute locking command ({command_response.status_code}: {command_response.text})')
|
|
1813
|
+
except requests.exceptions.ConnectionError as connection_error:
|
|
1814
|
+
raise CommandError(f'Connection error: {connection_error}.'
|
|
1815
|
+
' If this happens frequently, please check if other applications communicate with the Skoda server.') from connection_error
|
|
1816
|
+
except requests.exceptions.ChunkedEncodingError as chunked_encoding_error:
|
|
1817
|
+
raise CommandError(f'Error: {chunked_encoding_error}') from chunked_encoding_error
|
|
1818
|
+
except requests.exceptions.ReadTimeout as timeout_error:
|
|
1819
|
+
raise CommandError(f'Timeout during read: {timeout_error}') from timeout_error
|
|
1820
|
+
except requests.exceptions.RetryError as retry_error:
|
|
1821
|
+
raise CommandError(f'Retrying failed: {retry_error}') from retry_error
|
|
1822
|
+
return command_arguments
|
|
1823
|
+
|
|
1824
|
+
def __on_wake_sleep(self, wake_sleep_command: WakeSleepCommand, command_arguments: Union[str, Dict[str, Any]]) \
|
|
1825
|
+
-> Union[str, Dict[str, Any]]:
|
|
1826
|
+
if wake_sleep_command.parent is None or wake_sleep_command.parent.parent is None \
|
|
1827
|
+
or not isinstance(wake_sleep_command.parent.parent, GenericVehicle):
|
|
1828
|
+
raise CommandError('Object hierarchy is not as expected')
|
|
1829
|
+
if not isinstance(command_arguments, dict):
|
|
1830
|
+
raise CommandError('Command arguments are not a dictionary')
|
|
1831
|
+
vehicle: GenericVehicle = wake_sleep_command.parent.parent
|
|
1832
|
+
vin: Optional[str] = vehicle.vin.value
|
|
1833
|
+
if vin is None:
|
|
1834
|
+
raise CommandError('VIN in object hierarchy missing')
|
|
1835
|
+
if 'command' not in command_arguments:
|
|
1836
|
+
raise CommandError('Command argument missing')
|
|
1837
|
+
if command_arguments['command'] == WakeSleepCommand.Command.WAKE:
|
|
1838
|
+
url = f'https://mysmob.api.connect.skoda-auto.cz/api/v1/vehicle-wakeup/{vin}?applyRequestLimiter=true'
|
|
1839
|
+
|
|
1840
|
+
try:
|
|
1841
|
+
command_response: requests.Response = self.session.post(url, data='{}', allow_redirects=True)
|
|
1842
|
+
if command_response.status_code != requests.codes['accepted']:
|
|
1843
|
+
LOG.error('Could not execute wake command (%s: %s)', command_response.status_code, command_response.text)
|
|
1844
|
+
raise CommandError(f'Could not execute wake command ({command_response.status_code}: {command_response.text})')
|
|
1845
|
+
except requests.exceptions.ConnectionError as connection_error:
|
|
1846
|
+
raise CommandError(f'Connection error: {connection_error}.'
|
|
1847
|
+
' If this happens frequently, please check if other applications communicate with the Skoda server.') from connection_error
|
|
1848
|
+
except requests.exceptions.ChunkedEncodingError as chunked_encoding_error:
|
|
1849
|
+
raise CommandError(f'Error: {chunked_encoding_error}') from chunked_encoding_error
|
|
1850
|
+
except requests.exceptions.ReadTimeout as timeout_error:
|
|
1851
|
+
raise CommandError(f'Timeout during read: {timeout_error}') from timeout_error
|
|
1852
|
+
except requests.exceptions.RetryError as retry_error:
|
|
1853
|
+
raise CommandError(f'Retrying failed: {retry_error}') from retry_error
|
|
1854
|
+
elif command_arguments['command'] == WakeSleepCommand.Command.SLEEP:
|
|
1855
|
+
raise CommandError('Sleep command not supported by vehicle. Vehicle will put itself to sleep')
|
|
1856
|
+
else:
|
|
1857
|
+
raise CommandError(f'Unknown command {command_arguments["command"]}')
|
|
1858
|
+
return command_arguments
|
|
1859
|
+
|
|
1860
|
+
def __on_spin(self, spin_command: SpinCommand, command_arguments: Union[str, Dict[str, Any]]) \
|
|
1861
|
+
-> Union[str, Dict[str, Any]]:
|
|
1862
|
+
del spin_command
|
|
1863
|
+
if not isinstance(command_arguments, dict):
|
|
1864
|
+
raise CommandError('Command arguments are not a dictionary')
|
|
1865
|
+
if 'command' not in command_arguments:
|
|
1866
|
+
raise CommandError('Command argument missing')
|
|
1867
|
+
command_dict = {}
|
|
1868
|
+
if self.active_config['spin'] is None:
|
|
1869
|
+
raise CommandError('S-PIN is missing, please add S-PIN to your configuration or .netrc file')
|
|
1870
|
+
if 'spin' in command_arguments:
|
|
1871
|
+
command_dict['currentSpin'] = command_arguments['spin']
|
|
1872
|
+
else:
|
|
1873
|
+
if self.active_config['spin'] is None or self.active_config['spin'] == '':
|
|
1874
|
+
raise CommandError('S-PIN is missing, please add S-PIN to your configuration or .netrc file')
|
|
1875
|
+
command_dict['currentSpin'] = self.active_config['spin']
|
|
1876
|
+
if command_arguments['command'] == SpinCommand.Command.VERIFY:
|
|
1877
|
+
url = 'https://mysmob.api.connect.skoda-auto.cz/api/v1/spin/verify'
|
|
1878
|
+
else:
|
|
1879
|
+
raise CommandError(f'Unknown command {command_arguments["command"]}')
|
|
1880
|
+
try:
|
|
1881
|
+
command_response: requests.Response = self.session.post(url, data=json.dumps(command_dict), allow_redirects=True)
|
|
1882
|
+
if command_response.status_code != requests.codes['ok']:
|
|
1883
|
+
LOG.error('Could not execute spin command (%s: %s)', command_response.status_code, command_response.text)
|
|
1884
|
+
raise CommandError(f'Could not execute spin command ({command_response.status_code}: {command_response.text})')
|
|
1885
|
+
else:
|
|
1886
|
+
LOG.info('Spin verify command executed successfully')
|
|
1887
|
+
except requests.exceptions.ConnectionError as connection_error:
|
|
1888
|
+
raise CommandError(f'Connection error: {connection_error}.'
|
|
1889
|
+
' If this happens frequently, please check if other applications communicate with the Skoda server.') from connection_error
|
|
1890
|
+
except requests.exceptions.ChunkedEncodingError as chunked_encoding_error:
|
|
1891
|
+
raise CommandError(f'Error: {chunked_encoding_error}') from chunked_encoding_error
|
|
1892
|
+
except requests.exceptions.ReadTimeout as timeout_error:
|
|
1893
|
+
raise CommandError(f'Timeout during read: {timeout_error}') from timeout_error
|
|
1894
|
+
except requests.exceptions.RetryError as retry_error:
|
|
1895
|
+
raise CommandError(f'Retrying failed: {retry_error}') from retry_error
|
|
1896
|
+
return command_arguments
|
|
1897
|
+
|
|
1898
|
+
def __on_window_heating_start_stop(self, start_stop_command: WindowHeatingStartStopCommand, command_arguments: Union[str, Dict[str, Any]]) \
|
|
1899
|
+
-> Union[str, Dict[str, Any]]:
|
|
1900
|
+
if start_stop_command.parent is None or start_stop_command.parent.parent is None \
|
|
1901
|
+
or start_stop_command.parent.parent.parent is None or not isinstance(start_stop_command.parent.parent.parent, SkodaVehicle):
|
|
1902
|
+
raise CommandError('Object hierarchy is not as expected')
|
|
1903
|
+
if not isinstance(command_arguments, dict):
|
|
1904
|
+
raise CommandError('Command arguments are not a dictionary')
|
|
1905
|
+
vehicle: SkodaVehicle = start_stop_command.parent.parent.parent
|
|
1906
|
+
vin: Optional[str] = vehicle.vin.value
|
|
1907
|
+
if vin is None:
|
|
1908
|
+
raise CommandError('VIN in object hierarchy missing')
|
|
1909
|
+
if 'command' not in command_arguments:
|
|
1910
|
+
raise CommandError('Command argument missing')
|
|
1911
|
+
try:
|
|
1912
|
+
if command_arguments['command'] == WindowHeatingStartStopCommand.Command.START:
|
|
1913
|
+
url = f'https://mysmob.api.connect.skoda-auto.cz/api/v2/air-conditioning/{vin}/start-window-heating'
|
|
1914
|
+
command_response: requests.Response = self.session.post(url, allow_redirects=True)
|
|
1915
|
+
elif command_arguments['command'] == WindowHeatingStartStopCommand.Command.STOP:
|
|
1916
|
+
url = f'https://mysmob.api.connect.skoda-auto.cz/api/v2/air-conditioning/{vin}/stop-window-heating'
|
|
1917
|
+
|
|
1918
|
+
command_response: requests.Response = self.session.post(url, allow_redirects=True)
|
|
1919
|
+
else:
|
|
1920
|
+
raise CommandError(f'Unknown command {command_arguments["command"]}')
|
|
1921
|
+
|
|
1922
|
+
if command_response.status_code != requests.codes['accepted']:
|
|
1923
|
+
LOG.error('Could not start/stop window heating (%s: %s)', command_response.status_code, command_response.text)
|
|
1924
|
+
raise CommandError(f'Could not start/stop window heating ({command_response.status_code}: {command_response.text})')
|
|
1925
|
+
except requests.exceptions.ConnectionError as connection_error:
|
|
1926
|
+
raise CommandError(f'Connection error: {connection_error}.'
|
|
1927
|
+
' If this happens frequently, please check if other applications communicate with the Skoda server.') from connection_error
|
|
1928
|
+
except requests.exceptions.ChunkedEncodingError as chunked_encoding_error:
|
|
1929
|
+
raise CommandError(f'Error: {chunked_encoding_error}') from chunked_encoding_error
|
|
1930
|
+
except requests.exceptions.ReadTimeout as timeout_error:
|
|
1931
|
+
raise CommandError(f'Timeout during read: {timeout_error}') from timeout_error
|
|
1932
|
+
except requests.exceptions.RetryError as retry_error:
|
|
1933
|
+
raise CommandError(f'Retrying failed: {retry_error}') from retry_error
|
|
1934
|
+
return command_arguments
|
|
1935
|
+
|
|
1936
|
+
def __on_charging_target_level_change(self, level_attribute: LevelAttribute, target_level: float) -> float:
|
|
1937
|
+
"""
|
|
1938
|
+
Callback for the charging target level change.
|
|
1939
|
+
|
|
1940
|
+
Args:
|
|
1941
|
+
level_attribute (LevelAttribute): The level attribute that changed.
|
|
1942
|
+
target_level (float): The new target level.
|
|
1943
|
+
"""
|
|
1944
|
+
if level_attribute.parent is None or level_attribute.parent.parent is None \
|
|
1945
|
+
or level_attribute.parent.parent.parent is None or not isinstance(level_attribute.parent.parent.parent, SkodaVehicle):
|
|
1946
|
+
raise SetterError('Object hierarchy is not as expected')
|
|
1947
|
+
vehicle: SkodaVehicle = level_attribute.parent.parent.parent
|
|
1948
|
+
vin: Optional[str] = vehicle.vin.value
|
|
1949
|
+
if vin is None:
|
|
1950
|
+
raise SetterError('VIN in object hierarchy missing')
|
|
1951
|
+
precision: float = level_attribute.precision if level_attribute.precision is not None else 10.0
|
|
1952
|
+
target_level = round(target_level / precision) * precision
|
|
1953
|
+
setting_dict = {}
|
|
1954
|
+
setting_dict['targetSOCInPercent'] = target_level
|
|
1955
|
+
|
|
1956
|
+
url = f'https://mysmob.api.connect.skoda-auto.cz/api/v1/charging/{vin}/set-charge-limit'
|
|
1957
|
+
try:
|
|
1958
|
+
settings_response: requests.Response = self.session.put(url, data=json.dumps(setting_dict), allow_redirects=True)
|
|
1959
|
+
if settings_response.status_code != requests.codes['accepted']:
|
|
1960
|
+
LOG.error('Could not set target level (%s)', settings_response.status_code)
|
|
1961
|
+
raise SetterError(f'Could not set value ({settings_response.status_code})')
|
|
1962
|
+
except requests.exceptions.ConnectionError as connection_error:
|
|
1963
|
+
raise SetterError(f'Connection error: {connection_error}.'
|
|
1964
|
+
' If this happens frequently, please check if other applications communicate with the Skoda server.') from connection_error
|
|
1965
|
+
except requests.exceptions.ChunkedEncodingError as chunked_encoding_error:
|
|
1966
|
+
raise SetterError(f'Error: {chunked_encoding_error}') from chunked_encoding_error
|
|
1967
|
+
except requests.exceptions.ReadTimeout as timeout_error:
|
|
1968
|
+
raise SetterError(f'Timeout during read: {timeout_error}') from timeout_error
|
|
1969
|
+
except requests.exceptions.RetryError as retry_error:
|
|
1970
|
+
raise SetterError(f'Retrying failed: {retry_error}') from retry_error
|
|
1971
|
+
return target_level
|
|
1972
|
+
|
|
1973
|
+
def __on_charging_maximum_current_change(self, current_attribute: CurrentAttribute, maximum_current: float) -> float:
|
|
1974
|
+
"""
|
|
1975
|
+
Callback for the charging target level change.
|
|
1976
|
+
|
|
1977
|
+
Args:
|
|
1978
|
+
current_attribute (CurrentAttribute): The current attribute that changed.
|
|
1979
|
+
maximum_current (float): The new maximum current.
|
|
1980
|
+
"""
|
|
1981
|
+
if current_attribute.parent is None or current_attribute.parent.parent is None \
|
|
1982
|
+
or current_attribute.parent.parent.parent is None or not isinstance(current_attribute.parent.parent.parent, SkodaVehicle):
|
|
1983
|
+
raise SetterError('Object hierarchy is not as expected')
|
|
1984
|
+
vehicle: SkodaVehicle = current_attribute.parent.parent.parent
|
|
1985
|
+
vin: Optional[str] = vehicle.vin.value
|
|
1986
|
+
if vin is None:
|
|
1987
|
+
raise SetterError('VIN in object hierarchy missing')
|
|
1988
|
+
setting_dict = {}
|
|
1989
|
+
precision: float = current_attribute.precision if current_attribute.precision is not None else 1.0
|
|
1990
|
+
maximum_current = round(maximum_current / precision) * precision
|
|
1991
|
+
if maximum_current < 16:
|
|
1992
|
+
setting_dict['chargingCurrent'] = "REDUCED"
|
|
1993
|
+
maximum_current = 6.0
|
|
1994
|
+
else:
|
|
1995
|
+
setting_dict['chargingCurrent'] = "MAXIMUM"
|
|
1996
|
+
maximum_current = 16.0
|
|
1997
|
+
|
|
1998
|
+
url = f'https://mysmob.api.connect.skoda-auto.cz/api/v1/charging/{vin}/set-charging-current'
|
|
1999
|
+
try:
|
|
2000
|
+
settings_response: requests.Response = self.session.put(url, data=json.dumps(setting_dict), allow_redirects=True)
|
|
2001
|
+
if settings_response.status_code != requests.codes['accepted']:
|
|
2002
|
+
LOG.error('Could not set target charging current (%s)', settings_response.status_code)
|
|
2003
|
+
raise SetterError(f'Could not set value ({settings_response.status_code})')
|
|
2004
|
+
except requests.exceptions.ConnectionError as connection_error:
|
|
2005
|
+
raise SetterError(f'Connection error: {connection_error}.'
|
|
2006
|
+
' If this happens frequently, please check if other applications communicate with the Skoda server.') from connection_error
|
|
2007
|
+
except requests.exceptions.ChunkedEncodingError as chunked_encoding_error:
|
|
2008
|
+
raise SetterError(f'Error: {chunked_encoding_error}') from chunked_encoding_error
|
|
2009
|
+
except requests.exceptions.ReadTimeout as timeout_error:
|
|
2010
|
+
raise SetterError(f'Timeout during read: {timeout_error}') from timeout_error
|
|
2011
|
+
except requests.exceptions.RetryError as retry_error:
|
|
2012
|
+
raise SetterError(f'Retrying failed: {retry_error}') from retry_error
|
|
2013
|
+
return maximum_current
|
|
2014
|
+
|
|
2015
|
+
def __on_charging_auto_unlock_change(self, boolean_attribute: BooleanAttribute, auto_unlock: bool) -> bool:
|
|
2016
|
+
"""
|
|
2017
|
+
Callback for the charging target level change.
|
|
2018
|
+
|
|
2019
|
+
Args:
|
|
2020
|
+
boolean_attribute (BooleanAttribute): The boolean attribute that changed.
|
|
2021
|
+
auto_unlock (float): The new auto_unlock setting.
|
|
2022
|
+
"""
|
|
2023
|
+
if boolean_attribute.parent is None or boolean_attribute.parent.parent is None \
|
|
2024
|
+
or boolean_attribute.parent.parent.parent is None or not isinstance(boolean_attribute.parent.parent.parent, SkodaVehicle):
|
|
2025
|
+
raise SetterError('Object hierarchy is not as expected')
|
|
2026
|
+
vehicle: SkodaVehicle = boolean_attribute.parent.parent.parent
|
|
2027
|
+
vin: Optional[str] = vehicle.vin.value
|
|
2028
|
+
if vin is None:
|
|
2029
|
+
raise SetterError('VIN in object hierarchy missing')
|
|
2030
|
+
setting_dict = {}
|
|
2031
|
+
if auto_unlock:
|
|
2032
|
+
setting_dict['autoUnlockPlug'] = "PERMANENT"
|
|
2033
|
+
else:
|
|
2034
|
+
setting_dict['autoUnlockPlug'] = "OFF"
|
|
2035
|
+
|
|
2036
|
+
url = f'https://mysmob.api.connect.skoda-auto.cz/api/v1/charging/{vin}/set-auto-unlock-plug'
|
|
2037
|
+
try:
|
|
2038
|
+
settings_response: requests.Response = self.session.put(url, data=json.dumps(setting_dict), allow_redirects=True)
|
|
2039
|
+
if settings_response.status_code != requests.codes['accepted']:
|
|
2040
|
+
LOG.error('Could not set auto unlock setting (%s)', settings_response.status_code)
|
|
2041
|
+
raise SetterError(f'Could not set value ({settings_response.status_code})')
|
|
2042
|
+
except requests.exceptions.ConnectionError as connection_error:
|
|
2043
|
+
raise SetterError(f'Connection error: {connection_error}.'
|
|
2044
|
+
' If this happens frequently, please check if other applications communicate with the Skoda server.') from connection_error
|
|
2045
|
+
except requests.exceptions.ChunkedEncodingError as chunked_encoding_error:
|
|
2046
|
+
raise SetterError(f'Error: {chunked_encoding_error}') from chunked_encoding_error
|
|
2047
|
+
except requests.exceptions.ReadTimeout as timeout_error:
|
|
2048
|
+
raise SetterError(f'Timeout during read: {timeout_error}') from timeout_error
|
|
2049
|
+
except requests.exceptions.RetryError as retry_error:
|
|
2050
|
+
raise SetterError(f'Retrying failed: {retry_error}') from retry_error
|
|
2051
|
+
return auto_unlock
|
|
2052
|
+
|
|
2053
|
+
def get_name(self) -> str:
|
|
2054
|
+
return "Skoda Connector"
|