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
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Module for Skoda vehicle capability class."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from enum import Enum
|
|
6
|
+
|
|
7
|
+
from carconnectivity.objects import GenericObject
|
|
8
|
+
from carconnectivity.attributes import EnumAttribute, StringAttribute
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Error(GenericObject):
|
|
15
|
+
"""
|
|
16
|
+
Represents an error object in the car connectivity context.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, object_id, parent: Optional[GenericObject] = None) -> None:
|
|
20
|
+
super().__init__(object_id, parent=parent)
|
|
21
|
+
self.type: EnumAttribute = EnumAttribute("type", parent=self, tags={'connector_custom'})
|
|
22
|
+
self.description: StringAttribute = StringAttribute("description", parent=self, tags={'connector_custom'})
|
|
23
|
+
|
|
24
|
+
class ChargingError(Enum):
|
|
25
|
+
"""
|
|
26
|
+
Enum representing various charging errors for Skoda car connectivity.
|
|
27
|
+
|
|
28
|
+
Attributes:
|
|
29
|
+
STATUS_OF_CHARGING_NOT_AVAILABLE: Indicates that the status of charging is not available.
|
|
30
|
+
STATUS_OF_CONNECTION_NOT_AVAILABLE: Indicates that the status of connection is not available.
|
|
31
|
+
CARE_MODE_IS_NOT_AVAILABLE: Indicates that the care mode is not available.
|
|
32
|
+
AUTO_UNLOCK_IS_NOT_AVAILABLE: Indicates that the auto unlock feature is not available.
|
|
33
|
+
MAX_CHARGE_CURRENT_IS_NOT_AVAILABLE: Indicates that the maximum charge current setting is not available.
|
|
34
|
+
CHARGE_LIMIT_IS_NOT_AVAILABLE: Indicates that the charge limit setting is not available.
|
|
35
|
+
"""
|
|
36
|
+
STATUS_OF_CHARGING_NOT_AVAILABLE = 'STATUS_OF_CHARGING_NOT_AVAILABLE'
|
|
37
|
+
STATUS_OF_CONNECTION_NOT_AVAILABLE = 'STATUS_OF_CONNECTION_NOT_AVAILABLE'
|
|
38
|
+
CARE_MODE_IS_NOT_AVAILABLE = 'CARE_MODE_IS_NOT_AVAILABLE'
|
|
39
|
+
AUTO_UNLOCK_IS_NOT_AVAILABLE = 'AUTO_UNLOCK_IS_NOT_AVAILABLE'
|
|
40
|
+
MAX_CHARGE_CURRENT_IS_NOT_AVAILABLE = 'MAX_CHARGE_CURRENT_IS_NOT_AVAILABLE'
|
|
41
|
+
CHARGE_LIMIT_IS_NOT_AVAILABLE = 'CHARGE_LIMIT_IS_NOT_AVAILABLE'
|
|
42
|
+
UNKNOWN = 'UNKNOWN'
|
|
43
|
+
|
|
44
|
+
class ClimatizationError(Enum):
|
|
45
|
+
"""
|
|
46
|
+
ClimatizationError is an enumeration for representing various errors
|
|
47
|
+
related to the climatization system in a Skoda car.
|
|
48
|
+
|
|
49
|
+
This enum can be extended to include specific error codes and messages
|
|
50
|
+
that correspond to different climatization issues.
|
|
51
|
+
"""
|
|
52
|
+
UNAVAILABLE_CHARGING_INFORMATION = 'UNAVAILABLE_CHARGING_INFORMATION'
|
|
53
|
+
UNKNOWN = 'UNKNOWN'
|
|
@@ -13,22 +13,31 @@ from datetime import timedelta, timezone
|
|
|
13
13
|
from paho.mqtt.client import Client
|
|
14
14
|
from paho.mqtt.enums import MQTTProtocolVersion, CallbackAPIVersion, MQTTErrorCode
|
|
15
15
|
|
|
16
|
-
from carconnectivity.errors import CarConnectivityError
|
|
16
|
+
from carconnectivity.errors import CarConnectivityError, TemporaryAuthenticationError
|
|
17
17
|
from carconnectivity.observable import Observable
|
|
18
18
|
from carconnectivity.vehicle import GenericVehicle
|
|
19
19
|
|
|
20
20
|
from carconnectivity.drive import ElectricDrive
|
|
21
21
|
from carconnectivity.util import robust_time_parse, log_extra_keys
|
|
22
22
|
from carconnectivity.charging import Charging
|
|
23
|
+
from carconnectivity.climatization import Climatization
|
|
24
|
+
from carconnectivity.units import Speed, Power, Length
|
|
25
|
+
from carconnectivity.enums import ConnectionState
|
|
23
26
|
|
|
24
27
|
from carconnectivity_connectors.skoda.vehicle import SkodaVehicle, SkodaElectricVehicle
|
|
25
28
|
from carconnectivity_connectors.skoda.charging import SkodaCharging, mapping_skoda_charging_state
|
|
26
29
|
|
|
27
30
|
|
|
28
31
|
if TYPE_CHECKING:
|
|
29
|
-
from typing import Set, Dict, Any, Optional
|
|
32
|
+
from typing import Set, Dict, Any, Optional, List
|
|
30
33
|
from datetime import datetime
|
|
31
34
|
|
|
35
|
+
from paho.mqtt.client import MQTTMessage, DisconnectFlags, ConnectFlags
|
|
36
|
+
from paho.mqtt.reasoncodes import ReasonCode
|
|
37
|
+
from paho.mqtt.properties import Properties
|
|
38
|
+
|
|
39
|
+
from carconnectivity.attributes import GenericAttribute
|
|
40
|
+
|
|
32
41
|
from carconnectivity_connectors.skoda.connector import Connector
|
|
33
42
|
|
|
34
43
|
|
|
@@ -61,6 +70,8 @@ class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
|
|
|
61
70
|
self.delayed_access_function_timers: Dict[str, threading.Timer] = {}
|
|
62
71
|
|
|
63
72
|
self.tls_set(cert_reqs=ssl.CERT_NONE)
|
|
73
|
+
|
|
74
|
+
self._retry_refresh_login_once = True
|
|
64
75
|
|
|
65
76
|
def connect(self, *args, **kwargs) -> MQTTErrorCode:
|
|
66
77
|
"""
|
|
@@ -69,9 +80,10 @@ class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
|
|
|
69
80
|
Returns:
|
|
70
81
|
MQTTErrorCode: The result of the connection attempt.
|
|
71
82
|
"""
|
|
83
|
+
self._skoda_connector.connection_state._set_value(value=ConnectionState.CONNECTING) # pylint: disable=protected-access
|
|
72
84
|
return super().connect(*args, host='mqtt.messagehub.de', port=8883, keepalive=60, **kwargs)
|
|
73
85
|
|
|
74
|
-
def _on_pre_connect_callback(self, client, userdata) -> None:
|
|
86
|
+
def _on_pre_connect_callback(self, client: Client, userdata: Any) -> None:
|
|
75
87
|
"""
|
|
76
88
|
Callback function that is called before the MQTT client connects to the broker.
|
|
77
89
|
|
|
@@ -88,12 +100,17 @@ class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
|
|
|
88
100
|
del userdata
|
|
89
101
|
|
|
90
102
|
if self._skoda_connector.session.expired or self._skoda_connector.session.access_token is None:
|
|
91
|
-
|
|
103
|
+
try:
|
|
104
|
+
self._skoda_connector.session.refresh()
|
|
105
|
+
except ConnectionError as exc:
|
|
106
|
+
LOG.error('Token refresh failed due to connection error: %s', exc)
|
|
107
|
+
except TemporaryAuthenticationError as exc:
|
|
108
|
+
LOG.error('Token refresh failed due to temporary MySkoda error: %s', exc)
|
|
92
109
|
if not self._skoda_connector.session.expired and self._skoda_connector.session.access_token is not None:
|
|
93
110
|
# pylint: disable-next=attribute-defined-outside-init # this is a false positive, password has a setter in super class
|
|
94
111
|
self._password = self._skoda_connector.session.access_token # This is a bit hacky but if password attribute is used here there is an Exception
|
|
95
112
|
|
|
96
|
-
def _on_carconnectivity_vehicle_enabled(self, element, flags):
|
|
113
|
+
def _on_carconnectivity_vehicle_enabled(self, element: GenericAttribute, flags: Observable.ObserverEvent) -> None:
|
|
97
114
|
"""
|
|
98
115
|
Handles the event when a vehicle is enabled or disabled in the car connectivity system.
|
|
99
116
|
|
|
@@ -171,7 +188,10 @@ class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
|
|
|
171
188
|
vin: str = vehicle.vin.value
|
|
172
189
|
# If the skoda connector is managing this vehicle
|
|
173
190
|
if self._skoda_connector in vehicle.managing_connectors:
|
|
174
|
-
account_events: Set[str] = {'privacy'
|
|
191
|
+
account_events: Set[str] = {'privacy',
|
|
192
|
+
'guest-user-nomination',
|
|
193
|
+
'primary-user-nomination'}
|
|
194
|
+
vehicle_status_events: Set[str] = {'vehicle-connection-status'}
|
|
175
195
|
operation_requests: Set[str] = {
|
|
176
196
|
'air-conditioning/set-air-conditioning-at-unlock',
|
|
177
197
|
'air-conditioning/set-air-conditioning-seats-heating',
|
|
@@ -208,6 +228,8 @@ class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
|
|
|
208
228
|
# Compile all possible topics
|
|
209
229
|
for event in account_events:
|
|
210
230
|
possible_topics.add(f'{user_id}/{vin}/account-event/{event}')
|
|
231
|
+
for event in vehicle_status_events:
|
|
232
|
+
possible_topics.add(f'{user_id}/{vin}/vehicle-status/{event}')
|
|
211
233
|
for event in operation_requests:
|
|
212
234
|
possible_topics.add(f'{user_id}/{vin}/operation-request/{event}')
|
|
213
235
|
for event in service_events:
|
|
@@ -251,7 +273,7 @@ class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
|
|
|
251
273
|
self.subscribed_topics.remove(topic)
|
|
252
274
|
LOG.debug('Unsubscribed from topic %s', topic)
|
|
253
275
|
|
|
254
|
-
def _on_connect_callback(self,
|
|
276
|
+
def _on_connect_callback(self, client: Client, obj: Any, flags: ConnectFlags, reason_code: ReasonCode, properties: Optional[Properties]) -> None:
|
|
255
277
|
"""
|
|
256
278
|
Callback function that is called when the MQTT client connects to the broker.
|
|
257
279
|
|
|
@@ -292,18 +314,21 @@ class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
|
|
|
292
314
|
- 159: Connection rate exceeded.
|
|
293
315
|
- Other: Generic connection error.
|
|
294
316
|
"""
|
|
295
|
-
del
|
|
317
|
+
del client # unused
|
|
296
318
|
del obj # unused
|
|
297
319
|
del flags # unused
|
|
298
320
|
del properties
|
|
299
321
|
# reason_code 0 means success
|
|
300
322
|
if reason_code == 0:
|
|
301
323
|
LOG.info('Connected to Skoda MQTT server')
|
|
302
|
-
self._skoda_connector.
|
|
324
|
+
if self._skoda_connector.rest_connected:
|
|
325
|
+
self._skoda_connector.connection_state._set_value(value=ConnectionState.CONNECTED) # pylint: disable=protected-access
|
|
326
|
+
self._skoda_connector.mqtt_connected = True
|
|
303
327
|
observer_flags: Observable.ObserverEvent = Observable.ObserverEvent.ENABLED | Observable.ObserverEvent.DISABLED
|
|
304
328
|
self._skoda_connector.car_connectivity.garage.add_observer(observer=self._on_carconnectivity_vehicle_enabled,
|
|
305
329
|
flag=observer_flags,
|
|
306
330
|
priority=Observable.ObserverPriority.USER_MID)
|
|
331
|
+
self._retry_refresh_login_once = True
|
|
307
332
|
self._subscribe_vehicles()
|
|
308
333
|
|
|
309
334
|
# Handle different reason codes
|
|
@@ -321,6 +346,15 @@ class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
|
|
|
321
346
|
LOG.error('Could not connect (%s): Client identifier not valid', reason_code)
|
|
322
347
|
elif reason_code == 134:
|
|
323
348
|
LOG.error('Could not connect (%s): Bad user name or password', reason_code)
|
|
349
|
+
if self._retry_refresh_login_once == True:
|
|
350
|
+
self._retry_refresh_login_once = False
|
|
351
|
+
LOG.info('trying a relogin once to resolve the error')
|
|
352
|
+
try:
|
|
353
|
+
self._skoda_connector.session.login()
|
|
354
|
+
except TemporaryAuthenticationError as exc:
|
|
355
|
+
LOG.error('Login failed due to temporary MySkoda error: %s', exc)
|
|
356
|
+
except ConnectionError as exc:
|
|
357
|
+
LOG.error('Login failed due to connection error: %s', exc)
|
|
324
358
|
elif reason_code == 135:
|
|
325
359
|
LOG.error('Could not connect (%s): Not authorized', reason_code)
|
|
326
360
|
elif reason_code == 136:
|
|
@@ -350,8 +384,8 @@ class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
|
|
|
350
384
|
else:
|
|
351
385
|
LOG.error('Could not connect (%s)', reason_code)
|
|
352
386
|
|
|
353
|
-
def _on_disconnect_callback(self, client, userdata, flags, reason_code, properties) -> None:
|
|
354
|
-
"""
|
|
387
|
+
def _on_disconnect_callback(self, client: Client, userdata, flags: DisconnectFlags, reason_code: ReasonCode, properties: Optional[Properties]) -> None:
|
|
388
|
+
"""["Client", Any, DisconnectFlags, ReasonCode, Union[Properties, None]
|
|
355
389
|
Callback function that is called when the MQTT client disconnects.
|
|
356
390
|
|
|
357
391
|
This function handles the disconnection of the MQTT client and logs the appropriate
|
|
@@ -372,7 +406,8 @@ class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
|
|
|
372
406
|
del properties
|
|
373
407
|
del flags
|
|
374
408
|
|
|
375
|
-
self._skoda_connector.
|
|
409
|
+
self._skoda_connector.connection_state._set_value(value=ConnectionState.DISCONNECTED) # pylint: disable=protected-access
|
|
410
|
+
self._skoda_connector.mqtt_connected = False
|
|
376
411
|
self._skoda_connector.car_connectivity.garage.remove_observer(observer=self._on_carconnectivity_vehicle_enabled)
|
|
377
412
|
|
|
378
413
|
self.subscribed_topics.clear()
|
|
@@ -390,9 +425,9 @@ class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
|
|
|
390
425
|
elif reason_code == 160:
|
|
391
426
|
LOG.error('Client disconnected: Maximum connect time')
|
|
392
427
|
else:
|
|
393
|
-
LOG.error('Client unexpectedly disconnected (%d: %s), trying to reconnect', reason_code, reason_code)
|
|
428
|
+
LOG.error('Client unexpectedly disconnected (%d: %s), trying to reconnect', reason_code.value, reason_code.getName())
|
|
394
429
|
|
|
395
|
-
def _on_subscribe_callback(self,
|
|
430
|
+
def _on_subscribe_callback(self, client: Client, obj: Any, mid: int, reason_codes: List[ReasonCode], properties: Optional[Properties]) -> None:
|
|
396
431
|
"""
|
|
397
432
|
Callback function for MQTT subscription.
|
|
398
433
|
|
|
@@ -409,15 +444,15 @@ class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
|
|
|
409
444
|
Returns:
|
|
410
445
|
None
|
|
411
446
|
"""
|
|
412
|
-
del
|
|
447
|
+
del client # unused
|
|
413
448
|
del obj # unused
|
|
414
449
|
del properties # unused
|
|
415
450
|
if any(x in [0, 1, 2] for x in reason_codes):
|
|
416
451
|
LOG.debug('sucessfully subscribed to topic of mid %d', mid)
|
|
417
452
|
else:
|
|
418
|
-
LOG.error('Subscribe was not successfull (%s)', ', '.join(reason_codes))
|
|
453
|
+
LOG.error('Subscribe was not successfull (%s)', ', '.join([reason_code.getName() for reason_code in reason_codes]))
|
|
419
454
|
|
|
420
|
-
def _on_message_callback(self,
|
|
455
|
+
def _on_message_callback(self, client: Client, obj: Any, msg: MQTTMessage) -> None: # noqa: C901
|
|
421
456
|
"""
|
|
422
457
|
Callback function for handling incoming MQTT messages.
|
|
423
458
|
|
|
@@ -433,7 +468,7 @@ class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
|
|
|
433
468
|
Returns:
|
|
434
469
|
None
|
|
435
470
|
"""
|
|
436
|
-
del
|
|
471
|
+
del client # unused
|
|
437
472
|
del obj # unused
|
|
438
473
|
if len(msg.payload) == 0:
|
|
439
474
|
LOG_API.debug('MQTT topic %s: ignoring empty message', msg.topic)
|
|
@@ -456,10 +491,19 @@ class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
|
|
|
456
491
|
if 'data' in data and data['data'] is not None:
|
|
457
492
|
vehicle: Optional[GenericVehicle] = self._skoda_connector.car_connectivity.garage.get_vehicle(vin)
|
|
458
493
|
if isinstance(vehicle, SkodaElectricVehicle):
|
|
459
|
-
electric_drive: ElectricDrive = vehicle.get_electric_drive()
|
|
494
|
+
electric_drive: Optional[ElectricDrive] = vehicle.get_electric_drive()
|
|
460
495
|
if electric_drive is not None:
|
|
461
496
|
charging_state: Optional[Charging.ChargingState] = vehicle.charging.state.value
|
|
462
497
|
old_charging_state: Optional[Charging.ChargingState] = charging_state
|
|
498
|
+
if 'mode' in data['data'] and data['data']['mode'] is not None \
|
|
499
|
+
and vehicle.charging is not None and isinstance(vehicle.charging.settings, SkodaCharging.Settings):
|
|
500
|
+
if data['data']['mode'] in [item.value for item in SkodaCharging.SkodaChargeMode]:
|
|
501
|
+
skoda_charging_mode = SkodaCharging.SkodaChargeMode(data['data']['mode'])
|
|
502
|
+
else:
|
|
503
|
+
LOG_API.info('Unkown charging mode %s not in %s', data['data']['mode'], str(SkodaCharging.SkodaChargeMode))
|
|
504
|
+
skoda_charging_mode = Charging.ChargingState.UNKNOWN
|
|
505
|
+
# pylint: disable-next=protected-access
|
|
506
|
+
vehicle.charging.settings.preferred_charge_mode._set_value(value=skoda_charging_mode, measured=measured_at)
|
|
463
507
|
if 'state' in data['data'] and data['data']['state'] is not None:
|
|
464
508
|
if data['data']['state'] in [item.value for item in SkodaCharging.SkodaChargingState]:
|
|
465
509
|
skoda_charging_state = SkodaCharging.SkodaChargingState(data['data']['state'])
|
|
@@ -473,18 +517,21 @@ class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
|
|
|
473
517
|
# pylint: disable-next=protected-access
|
|
474
518
|
vehicle.charging.type._set_value(value=Charging.ChargingType.OFF, measured=measured_at)
|
|
475
519
|
# pylint: disable-next=protected-access
|
|
476
|
-
vehicle.charging.rate._set_value(value=0, measured=measured_at)
|
|
520
|
+
vehicle.charging.rate._set_value(value=0, measured=measured_at, unit=Speed.KMH)
|
|
477
521
|
# pylint: disable-next=protected-access
|
|
478
|
-
vehicle.charging.power._set_value(value=0, measured=measured_at)
|
|
522
|
+
vehicle.charging.power._set_value(value=0, measured=measured_at, unit=Power.KW)
|
|
479
523
|
if 'soc' in data['data'] and data['data']['soc'] is not None:
|
|
524
|
+
if isinstance(data['data']['soc'], str):
|
|
525
|
+
data['data']['soc'] = int(data['data']['soc'])
|
|
480
526
|
electric_drive.level._set_value(measured=measured_at, value=data['data']['soc']) # pylint: disable=protected-access
|
|
481
527
|
if 'chargedRange' in data['data'] and data['data']['chargedRange'] is not None:
|
|
482
528
|
# pylint: disable-next=protected-access
|
|
483
|
-
electric_drive.range._set_value(measured=measured_at, value=data['data']['chargedRange'])
|
|
529
|
+
electric_drive.range._set_value(measured=measured_at, value=data['data']['chargedRange'], unit=Length.KM)
|
|
484
530
|
# If charging state changed, fetch charging again
|
|
485
531
|
if old_charging_state != charging_state:
|
|
486
532
|
try:
|
|
487
533
|
self._skoda_connector.fetch_charging(vehicle, no_cache=True)
|
|
534
|
+
self._skoda_connector.car_connectivity.transaction_end()
|
|
488
535
|
except CarConnectivityError as e:
|
|
489
536
|
LOG.error('Error while fetching charging: %s', e)
|
|
490
537
|
if 'timeToFinish' in data['data'] and data['data']['timeToFinish'] is not None \
|
|
@@ -492,11 +539,12 @@ class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
|
|
|
492
539
|
try:
|
|
493
540
|
remaining_duration: Optional[timedelta] = timedelta(minutes=int(data['data']['timeToFinish']))
|
|
494
541
|
estimated_date_reached: Optional[datetime] = measured_at + remaining_duration
|
|
542
|
+
estimated_date_reached = estimated_date_reached.replace(second=0, microsecond=0)
|
|
495
543
|
except ValueError:
|
|
496
544
|
estimated_date_reached: Optional[datetime] = None
|
|
497
545
|
# pylint: disable-next=protected-access
|
|
498
546
|
vehicle.charging.estimated_date_reached._set_value(measured=measured_at, value=estimated_date_reached)
|
|
499
|
-
log_extra_keys(LOG_API, 'data', data['data'], {'vin', 'userId', 'soc', 'chargedRange', 'timeToFinish', 'state'})
|
|
547
|
+
log_extra_keys(LOG_API, 'data', data['data'], {'vin', 'userId', 'soc', 'chargedRange', 'timeToFinish', 'state', 'mode'})
|
|
500
548
|
LOG.debug('Received %s event for vehicle %s from user %s', data['name'], vin, user_id)
|
|
501
549
|
return
|
|
502
550
|
else:
|
|
@@ -511,8 +559,17 @@ class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
|
|
|
511
559
|
if isinstance(vehicle, SkodaVehicle):
|
|
512
560
|
try:
|
|
513
561
|
self._skoda_connector.fetch_air_conditioning(vehicle, no_cache=True)
|
|
562
|
+
self._skoda_connector.car_connectivity.transaction_end()
|
|
514
563
|
except CarConnectivityError as e:
|
|
515
|
-
LOG.error('Error while fetching
|
|
564
|
+
LOG.error('Error while fetching air conditioning: %s', e)
|
|
565
|
+
elif 'name' in data and data['name'] == 'climatisation-completed':
|
|
566
|
+
if 'data' in data and data['data'] is not None:
|
|
567
|
+
vehicle: Optional[GenericVehicle] = self._skoda_connector.car_connectivity.garage.get_vehicle(vin)
|
|
568
|
+
if vehicle is not None and vehicle.climatization is not None:
|
|
569
|
+
# pylint: disable-next=protected-access
|
|
570
|
+
vehicle.climatization.state._set_value(value=Climatization.ClimatizationState.OFF, measured=measured_at)
|
|
571
|
+
# pylint: disable-next=protected-access
|
|
572
|
+
vehicle.climatization.estimated_date_reached._set_value(value=measured_at, measured=measured_at)
|
|
516
573
|
LOG_API.info('Received event name %s service event %s for vehicle %s from user %s: %s', data['name'],
|
|
517
574
|
service_event, vin, user_id, msg.payload)
|
|
518
575
|
return
|
|
@@ -527,6 +584,10 @@ class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
|
|
|
527
584
|
"""
|
|
528
585
|
vin = vehicle.id
|
|
529
586
|
self.delayed_access_function_timers.pop(vin)
|
|
587
|
+
try:
|
|
588
|
+
self._skoda_connector.fetch_vehicle_status(vehicle, no_cache=True)
|
|
589
|
+
except CarConnectivityError as e:
|
|
590
|
+
LOG.error('Error while fetching vehicle status: %s', e)
|
|
530
591
|
if vehicle.capabilities is not None and vehicle.capabilities.enabled \
|
|
531
592
|
and vehicle.capabilities.has_capability('CHARGING') and isinstance(vehicle, SkodaElectricVehicle):
|
|
532
593
|
try:
|
|
@@ -545,26 +606,33 @@ class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
|
|
|
545
606
|
self._skoda_connector.fetch_air_conditioning(vehicle, no_cache=True)
|
|
546
607
|
except CarConnectivityError as e:
|
|
547
608
|
LOG.error('Error while fetching air conditioning: %s', e)
|
|
548
|
-
|
|
549
|
-
self._skoda_connector.fetch_vehicle_status_second_api(vehicle, no_cache=True)
|
|
550
|
-
except CarConnectivityError as e:
|
|
551
|
-
LOG.error('Error while fetching status second API: %s', e)
|
|
552
|
-
try:
|
|
553
|
-
self._skoda_connector.fetch_driving_range(vehicle, no_cache=True)
|
|
554
|
-
except CarConnectivityError as e:
|
|
555
|
-
LOG.error('Error while fetching driving range: %s', e)
|
|
609
|
+
self._skoda_connector.car_connectivity.transaction_end()
|
|
556
610
|
|
|
557
611
|
if vin in self.delayed_access_function_timers:
|
|
558
612
|
self.delayed_access_function_timers[vin].cancel()
|
|
559
613
|
self.delayed_access_function_timers[vin] = threading.Timer(2.0, delayed_access_function, kwargs={'vehicle': vehicle})
|
|
560
614
|
self.delayed_access_function_timers[vin].start()
|
|
561
615
|
|
|
616
|
+
LOG_API.info('Received event name %s service event %s for vehicle %s from user %s: %s', data['name'],
|
|
617
|
+
service_event, vin, user_id, msg.payload)
|
|
618
|
+
return
|
|
619
|
+
elif service_event == 'vehicle-status/lights':
|
|
620
|
+
if 'name' in data and data['name'] == 'change-lights':
|
|
621
|
+
if 'data' in data and data['data'] is not None:
|
|
622
|
+
vehicle: Optional[GenericVehicle] = self._skoda_connector.car_connectivity.garage.get_vehicle(vin)
|
|
623
|
+
if isinstance(vehicle, SkodaVehicle):
|
|
624
|
+
try:
|
|
625
|
+
self._skoda_connector.fetch_vehicle_status(vehicle, no_cache=True)
|
|
626
|
+
self._skoda_connector.car_connectivity.transaction_end()
|
|
627
|
+
except CarConnectivityError as e:
|
|
628
|
+
LOG.error('Error while fetching vehicle status: %s', e)
|
|
629
|
+
|
|
562
630
|
LOG_API.info('Received event name %s service event %s for vehicle %s from user %s: %s', data['name'],
|
|
563
631
|
service_event, vin, user_id, msg.payload)
|
|
564
632
|
return
|
|
565
633
|
LOG_API.info('Received unknown service event %s for vehicle %s from user %s: %s', service_event, vin, user_id, msg.payload)
|
|
566
634
|
return
|
|
567
|
-
#
|
|
635
|
+
# operation-requests
|
|
568
636
|
match = re.match(r'^(?P<user_id>[0-9a-fA-F-]+)/(?P<vin>[A-Z0-9]+)/operation-request/(?P<operation_request>[a-zA-Z0-9-_/]+)$', msg.topic)
|
|
569
637
|
if match:
|
|
570
638
|
user_id: str = match.group('user_id')
|
|
@@ -572,17 +640,49 @@ class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
|
|
|
572
640
|
operation_request: str = match.group('operation_request')
|
|
573
641
|
data: Dict[str, Any] = json.loads(msg.payload)
|
|
574
642
|
if data is not None:
|
|
575
|
-
|
|
576
|
-
|
|
643
|
+
vehicle: Optional[GenericVehicle] = self._skoda_connector.car_connectivity.garage.get_vehicle(vin)
|
|
644
|
+
if operation_request == 'air-conditioning/set-air-conditioning-at-unlock' \
|
|
645
|
+
or operation_request == 'air-conditioning/set-air-conditioning-seats-heating' \
|
|
646
|
+
or operation_request == 'air-conditioning/set-air-conditioning-timers' \
|
|
647
|
+
or operation_request == 'air-conditioning/set-air-conditioning-without-external-power' \
|
|
648
|
+
or operation_request == 'air-conditioning/set-target-temperature' \
|
|
649
|
+
or operation_request == 'air-conditioning/start-stop-air-conditioning' \
|
|
650
|
+
or operation_request == 'air-conditioning/start-stop-window-heating' \
|
|
651
|
+
or operation_request == 'air-conditioning/windows-heating':
|
|
577
652
|
if isinstance(vehicle, SkodaVehicle):
|
|
578
653
|
if 'status' in data and data['status'] is not None:
|
|
579
654
|
if data['status'] == 'COMPLETED_SUCCESS':
|
|
580
655
|
LOG.debug('Received %s operation request for vehicle %s from user %s', operation_request, vin, user_id)
|
|
581
656
|
try:
|
|
582
657
|
self._skoda_connector.fetch_air_conditioning(vehicle, no_cache=True)
|
|
658
|
+
self._skoda_connector.car_connectivity.transaction_end()
|
|
583
659
|
except CarConnectivityError as e:
|
|
584
660
|
LOG.error('Error while fetching air-conditioning: %s', e)
|
|
585
661
|
return
|
|
586
|
-
|
|
587
|
-
|
|
662
|
+
elif data['status'] == 'IN_PROGRESS':
|
|
663
|
+
LOG.debug('Received %s operation request for vehicle %s from user %s', operation_request, vin, user_id)
|
|
664
|
+
return
|
|
665
|
+
elif operation_request == 'charging/start-stop-charging' \
|
|
666
|
+
or operation_request == 'charging/update-battery-support' \
|
|
667
|
+
or operation_request == 'charging/update-auto-unlock-plug' \
|
|
668
|
+
or operation_request == 'charging/update-care-mode' \
|
|
669
|
+
or operation_request == 'charging/update-charge-limit' \
|
|
670
|
+
or operation_request == 'charging/update-charge-mode' \
|
|
671
|
+
or operation_request == 'charging/update-charging-profiles' \
|
|
672
|
+
or operation_request == 'charging/update-charging-current':
|
|
673
|
+
if isinstance(vehicle, SkodaElectricVehicle):
|
|
674
|
+
if 'status' in data and data['status'] is not None:
|
|
675
|
+
if data['status'] == 'COMPLETED_SUCCESS':
|
|
676
|
+
LOG.debug('Received %s operation request for vehicle %s from user %s', operation_request, vin, user_id)
|
|
677
|
+
try:
|
|
678
|
+
self._skoda_connector.fetch_charging(vehicle, no_cache=True)
|
|
679
|
+
self._skoda_connector.car_connectivity.transaction_end()
|
|
680
|
+
except CarConnectivityError as e:
|
|
681
|
+
LOG.error('Error while fetching charging: %s', e)
|
|
682
|
+
return
|
|
683
|
+
elif data['status'] == 'IN_PROGRESS':
|
|
684
|
+
LOG.debug('Received %s operation request for vehicle %s from user %s', operation_request, vin, user_id)
|
|
685
|
+
return
|
|
686
|
+
LOG_API.info('Received unknown operation request %s for vehicle %s from user %s: %s', operation_request, vin, user_id, msg.payload)
|
|
687
|
+
return
|
|
588
688
|
LOG_API.info('I don\'t understand message %s: %s', msg.topic, msg.payload)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
""" User interface for the Skoda connector in the Car Connectivity application. """
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
import flask
|
|
8
|
+
|
|
9
|
+
from carconnectivity_connectors.base.ui.connector_ui import BaseConnectorUI
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from typing import Optional, List, Dict, Union, Literal
|
|
13
|
+
|
|
14
|
+
from carconnectivity_connectors.base.connector import BaseConnector
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ConnectorUI(BaseConnectorUI):
|
|
18
|
+
"""
|
|
19
|
+
A user interface class for the Skoda connector in the Car Connectivity application.
|
|
20
|
+
"""
|
|
21
|
+
def __init__(self, connector: BaseConnector):
|
|
22
|
+
blueprint: Optional[flask.Blueprint] = flask.Blueprint(name=connector.id, import_name='carconnectivity-connector-skoda', url_prefix=f'/{connector.id}',
|
|
23
|
+
template_folder=os.path.dirname(__file__) + '/templates')
|
|
24
|
+
super().__init__(connector, blueprint=blueprint)
|
|
25
|
+
|
|
26
|
+
def get_nav_items(self) -> List[Dict[Literal['text', 'url', 'sublinks', 'divider'], Union[str, List]]]:
|
|
27
|
+
"""
|
|
28
|
+
Generates a list of navigation items for the Skoda connector UI.
|
|
29
|
+
"""
|
|
30
|
+
return super().get_nav_items()
|
|
31
|
+
|
|
32
|
+
def get_title(self) -> str:
|
|
33
|
+
"""
|
|
34
|
+
Returns the title of the connector.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
str: The title of the connector, which is "Skoda".
|
|
38
|
+
"""
|
|
39
|
+
return "Skoda"
|
|
@@ -3,11 +3,22 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
5
|
from carconnectivity.vehicle import GenericVehicle, ElectricVehicle, CombustionVehicle, HybridVehicle
|
|
6
|
+
from carconnectivity.charging import Charging
|
|
7
|
+
from carconnectivity.attributes import BooleanAttribute
|
|
6
8
|
|
|
7
9
|
from carconnectivity_connectors.skoda.capability import Capabilities
|
|
10
|
+
from carconnectivity_connectors.skoda.charging import SkodaCharging
|
|
11
|
+
from carconnectivity_connectors.skoda.climatization import SkodaClimatization
|
|
12
|
+
|
|
13
|
+
SUPPORT_IMAGES = False
|
|
14
|
+
try:
|
|
15
|
+
from PIL import Image
|
|
16
|
+
SUPPORT_IMAGES = True
|
|
17
|
+
except ImportError:
|
|
18
|
+
pass
|
|
8
19
|
|
|
9
20
|
if TYPE_CHECKING:
|
|
10
|
-
from typing import Optional
|
|
21
|
+
from typing import Optional, Dict
|
|
11
22
|
from carconnectivity.garage import Garage
|
|
12
23
|
from carconnectivity_connectors.base.connector import BaseConnector
|
|
13
24
|
|
|
@@ -19,16 +30,22 @@ class SkodaVehicle(GenericVehicle): # pylint: disable=too-many-instance-attribu
|
|
|
19
30
|
def __init__(self, vin: Optional[str] = None, garage: Optional[Garage] = None, managing_connector: Optional[BaseConnector] = None,
|
|
20
31
|
origin: Optional[SkodaVehicle] = None) -> None:
|
|
21
32
|
if origin is not None:
|
|
22
|
-
super().__init__(origin=origin)
|
|
33
|
+
super().__init__(garage=garage, origin=origin)
|
|
23
34
|
self.capabilities: Capabilities = origin.capabilities
|
|
24
35
|
self.capabilities.parent = self
|
|
36
|
+
self.in_motion: BooleanAttribute = origin.in_motion
|
|
37
|
+
self.in_motion.parent = self
|
|
38
|
+
if SUPPORT_IMAGES:
|
|
39
|
+
self._car_images = origin._car_images
|
|
40
|
+
|
|
25
41
|
else:
|
|
26
42
|
super().__init__(vin=vin, garage=garage, managing_connector=managing_connector)
|
|
43
|
+
self.climatization = SkodaClimatization(vehicle=self, origin=self.climatization)
|
|
27
44
|
self.capabilities = Capabilities(vehicle=self)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
45
|
+
self.in_motion = BooleanAttribute(name='in_motion', parent=self, tags={'connector_custom'})
|
|
46
|
+
if SUPPORT_IMAGES:
|
|
47
|
+
self._car_images: Dict[str, Image.Image] = {}
|
|
48
|
+
self.manufacturer._set_value(value='Škoda') # pylint: disable=protected-access
|
|
32
49
|
|
|
33
50
|
|
|
34
51
|
class SkodaElectricVehicle(ElectricVehicle, SkodaVehicle):
|
|
@@ -38,9 +55,14 @@ class SkodaElectricVehicle(ElectricVehicle, SkodaVehicle):
|
|
|
38
55
|
def __init__(self, vin: Optional[str] = None, garage: Optional[Garage] = None, managing_connector: Optional[BaseConnector] = None,
|
|
39
56
|
origin: Optional[SkodaVehicle] = None) -> None:
|
|
40
57
|
if origin is not None:
|
|
41
|
-
super().__init__(origin=origin)
|
|
58
|
+
super().__init__(garage=garage, origin=origin)
|
|
59
|
+
if isinstance(origin, ElectricVehicle):
|
|
60
|
+
self.charging: Charging = SkodaCharging(vehicle=self, origin=origin.charging)
|
|
61
|
+
else:
|
|
62
|
+
self.charging: Charging = SkodaCharging(vehicle=self, origin=self.charging)
|
|
42
63
|
else:
|
|
43
64
|
super().__init__(vin=vin, garage=garage, managing_connector=managing_connector)
|
|
65
|
+
self.charging: Charging = SkodaCharging(vehicle=self, origin=self.charging)
|
|
44
66
|
|
|
45
67
|
|
|
46
68
|
class SkodaCombustionVehicle(CombustionVehicle, SkodaVehicle):
|
|
@@ -50,7 +72,7 @@ class SkodaCombustionVehicle(CombustionVehicle, SkodaVehicle):
|
|
|
50
72
|
def __init__(self, vin: Optional[str] = None, garage: Optional[Garage] = None, managing_connector: Optional[BaseConnector] = None,
|
|
51
73
|
origin: Optional[SkodaVehicle] = None) -> None:
|
|
52
74
|
if origin is not None:
|
|
53
|
-
super().__init__(origin=origin)
|
|
75
|
+
super().__init__(garage=garage, origin=origin)
|
|
54
76
|
else:
|
|
55
77
|
super().__init__(vin=vin, garage=garage, managing_connector=managing_connector)
|
|
56
78
|
|
|
@@ -62,6 +84,6 @@ class SkodaHybridVehicle(HybridVehicle, SkodaVehicle):
|
|
|
62
84
|
def __init__(self, vin: Optional[str] = None, garage: Optional[Garage] = None, managing_connector: Optional[BaseConnector] = None,
|
|
63
85
|
origin: Optional[SkodaVehicle] = None) -> None:
|
|
64
86
|
if origin is not None:
|
|
65
|
-
super().__init__(origin=origin)
|
|
87
|
+
super().__init__(garage=garage, origin=origin)
|
|
66
88
|
else:
|
|
67
89
|
super().__init__(vin=vin, garage=garage, managing_connector=managing_connector)
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
carconnectivity_connectors/skoda/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
carconnectivity_connectors/skoda/_version.py,sha256=lXj09wNlQE2ygonVVc_Fk2FN_1_4nx6sHnElAwbvzHM,409
|
|
3
|
-
carconnectivity_connectors/skoda/capability.py,sha256=JlNEaisVYF8qWv0wNDHTaas36uIpTIQ3NVR69wesiYQ,4513
|
|
4
|
-
carconnectivity_connectors/skoda/charging.py,sha256=oDHxZxrfTMvtYCJxmGfKFeWVMH4ceQ5HTKRAspnsunU,3312
|
|
5
|
-
carconnectivity_connectors/skoda/connector.py,sha256=xiPPT6XTVWsLvu0iqZvSPpgtHm3DrR1TKUL4d4NRz2Y,62158
|
|
6
|
-
carconnectivity_connectors/skoda/mqtt_client.py,sha256=ZB56Za1k5vbxQPl5qCvukE46Tq1O0-Cn19kBz_bDJZM,31782
|
|
7
|
-
carconnectivity_connectors/skoda/vehicle.py,sha256=H3GRDNimMghFwFi--y9BsgoSK3pMibNf_l6SsDN6gvQ,2759
|
|
8
|
-
carconnectivity_connectors/skoda/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
-
carconnectivity_connectors/skoda/auth/auth_util.py,sha256=dGLUbUre0HBsTg_Ii5vW34f8DLrCykYJYCyzEvUBBEE,4434
|
|
10
|
-
carconnectivity_connectors/skoda/auth/my_skoda_session.py,sha256=lSh23SFJs8opjmPwHTv-KNIKDep_WY4aItSP4Zq7bT8,10396
|
|
11
|
-
carconnectivity_connectors/skoda/auth/openid_session.py,sha256=LusWi2FZZIL3buodGXZKUR0naLhhqeYv0uRW4V3wI2w,16842
|
|
12
|
-
carconnectivity_connectors/skoda/auth/session_manager.py,sha256=Uf1vujuDBYUCAXhYToOsZkgbTtfmY3Qe0ICTfwomBpI,2899
|
|
13
|
-
carconnectivity_connectors/skoda/auth/skoda_web_session.py,sha256=cjzMkzx473Sh-4RgZAQULeRRcxB1MboddldCVM_y5LE,10640
|
|
14
|
-
carconnectivity_connectors/skoda/auth/helpers/blacklist_retry.py,sha256=f3wsiY5bpHDBxp7Va1Mv9nKJ4u3qnCHZZmDu78_AhMk,1251
|
|
15
|
-
carconnectivity_connector_skoda-0.1a11.dist-info/LICENSE,sha256=PIwI1alwDyOfvEQHdGCm2u9uf_mGE8030xZDfun0xTo,1071
|
|
16
|
-
carconnectivity_connector_skoda-0.1a11.dist-info/METADATA,sha256=tS2ccqTBaAHPxFbNRA7wlbJQPiqLDU8J9h62FQcXvb8,5327
|
|
17
|
-
carconnectivity_connector_skoda-0.1a11.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
|
18
|
-
carconnectivity_connector_skoda-0.1a11.dist-info/top_level.txt,sha256=KqA8GviZsDH4PtmnwSQsz0HB_w-TWkeEHLIRNo5dTaI,27
|
|
19
|
-
carconnectivity_connector_skoda-0.1a11.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|