carconnectivity-connector-skoda 0.1a7__tar.gz → 0.1a9__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of carconnectivity-connector-skoda might be problematic. Click here for more details.

Files changed (35) hide show
  1. {carconnectivity_connector_skoda-0.1a7 → carconnectivity_connector_skoda-0.1a9}/PKG-INFO +1 -1
  2. {carconnectivity_connector_skoda-0.1a7 → carconnectivity_connector_skoda-0.1a9}/src/carconnectivity_connector_skoda.egg-info/PKG-INFO +1 -1
  3. {carconnectivity_connector_skoda-0.1a7 → carconnectivity_connector_skoda-0.1a9}/src/carconnectivity_connector_skoda.egg-info/SOURCES.txt +1 -0
  4. {carconnectivity_connector_skoda-0.1a7 → carconnectivity_connector_skoda-0.1a9}/src/carconnectivity_connectors/skoda/_version.py +1 -1
  5. carconnectivity_connector_skoda-0.1a9/src/carconnectivity_connectors/skoda/charging.py +67 -0
  6. {carconnectivity_connector_skoda-0.1a7 → carconnectivity_connector_skoda-0.1a9}/src/carconnectivity_connectors/skoda/connector.py +143 -9
  7. {carconnectivity_connector_skoda-0.1a7 → carconnectivity_connector_skoda-0.1a9}/src/carconnectivity_connectors/skoda/mqtt_client.py +48 -16
  8. {carconnectivity_connector_skoda-0.1a7 → carconnectivity_connector_skoda-0.1a9}/.flake8 +0 -0
  9. {carconnectivity_connector_skoda-0.1a7 → carconnectivity_connector_skoda-0.1a9}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  10. {carconnectivity_connector_skoda-0.1a7 → carconnectivity_connector_skoda-0.1a9}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  11. {carconnectivity_connector_skoda-0.1a7 → carconnectivity_connector_skoda-0.1a9}/.github/dependabot.yml +0 -0
  12. {carconnectivity_connector_skoda-0.1a7 → carconnectivity_connector_skoda-0.1a9}/.github/workflows/build.yml +0 -0
  13. {carconnectivity_connector_skoda-0.1a7 → carconnectivity_connector_skoda-0.1a9}/.github/workflows/build_and_publish.yml +0 -0
  14. {carconnectivity_connector_skoda-0.1a7 → carconnectivity_connector_skoda-0.1a9}/.github/workflows/codeql-analysis.yml +0 -0
  15. {carconnectivity_connector_skoda-0.1a7 → carconnectivity_connector_skoda-0.1a9}/.gitignore +0 -0
  16. {carconnectivity_connector_skoda-0.1a7 → carconnectivity_connector_skoda-0.1a9}/LICENSE +0 -0
  17. {carconnectivity_connector_skoda-0.1a7 → carconnectivity_connector_skoda-0.1a9}/Makefile +0 -0
  18. {carconnectivity_connector_skoda-0.1a7 → carconnectivity_connector_skoda-0.1a9}/README.md +0 -0
  19. {carconnectivity_connector_skoda-0.1a7 → carconnectivity_connector_skoda-0.1a9}/doc/Config.md +0 -0
  20. {carconnectivity_connector_skoda-0.1a7 → carconnectivity_connector_skoda-0.1a9}/pyproject.toml +0 -0
  21. {carconnectivity_connector_skoda-0.1a7 → carconnectivity_connector_skoda-0.1a9}/setup.cfg +0 -0
  22. {carconnectivity_connector_skoda-0.1a7 → carconnectivity_connector_skoda-0.1a9}/setup_requirements.txt +0 -0
  23. {carconnectivity_connector_skoda-0.1a7 → carconnectivity_connector_skoda-0.1a9}/src/carconnectivity_connector_skoda.egg-info/dependency_links.txt +0 -0
  24. {carconnectivity_connector_skoda-0.1a7 → carconnectivity_connector_skoda-0.1a9}/src/carconnectivity_connector_skoda.egg-info/requires.txt +0 -0
  25. {carconnectivity_connector_skoda-0.1a7 → carconnectivity_connector_skoda-0.1a9}/src/carconnectivity_connector_skoda.egg-info/top_level.txt +0 -0
  26. {carconnectivity_connector_skoda-0.1a7 → carconnectivity_connector_skoda-0.1a9}/src/carconnectivity_connectors/skoda/__init__.py +0 -0
  27. {carconnectivity_connector_skoda-0.1a7 → carconnectivity_connector_skoda-0.1a9}/src/carconnectivity_connectors/skoda/auth/__init__.py +0 -0
  28. {carconnectivity_connector_skoda-0.1a7 → carconnectivity_connector_skoda-0.1a9}/src/carconnectivity_connectors/skoda/auth/auth_util.py +0 -0
  29. {carconnectivity_connector_skoda-0.1a7 → carconnectivity_connector_skoda-0.1a9}/src/carconnectivity_connectors/skoda/auth/helpers/blacklist_retry.py +0 -0
  30. {carconnectivity_connector_skoda-0.1a7 → carconnectivity_connector_skoda-0.1a9}/src/carconnectivity_connectors/skoda/auth/my_skoda_session.py +0 -0
  31. {carconnectivity_connector_skoda-0.1a7 → carconnectivity_connector_skoda-0.1a9}/src/carconnectivity_connectors/skoda/auth/openid_session.py +0 -0
  32. {carconnectivity_connector_skoda-0.1a7 → carconnectivity_connector_skoda-0.1a9}/src/carconnectivity_connectors/skoda/auth/session_manager.py +0 -0
  33. {carconnectivity_connector_skoda-0.1a7 → carconnectivity_connector_skoda-0.1a9}/src/carconnectivity_connectors/skoda/auth/skoda_web_session.py +0 -0
  34. {carconnectivity_connector_skoda-0.1a7 → carconnectivity_connector_skoda-0.1a9}/src/carconnectivity_connectors/skoda/capability.py +0 -0
  35. {carconnectivity_connector_skoda-0.1a7 → carconnectivity_connector_skoda-0.1a9}/src/carconnectivity_connectors/skoda/vehicle.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: carconnectivity-connector-skoda
3
- Version: 0.1a7
3
+ Version: 0.1a9
4
4
  Summary: CarConnectivity connector for Skoda services
5
5
  Author: Till Steinbach
6
6
  License: MIT License
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: carconnectivity-connector-skoda
3
- Version: 0.1a7
3
+ Version: 0.1a9
4
4
  Summary: CarConnectivity connector for Skoda services
5
5
  Author: Till Steinbach
6
6
  License: MIT License
@@ -20,6 +20,7 @@ src/carconnectivity_connector_skoda.egg-info/top_level.txt
20
20
  src/carconnectivity_connectors/skoda/__init__.py
21
21
  src/carconnectivity_connectors/skoda/_version.py
22
22
  src/carconnectivity_connectors/skoda/capability.py
23
+ src/carconnectivity_connectors/skoda/charging.py
23
24
  src/carconnectivity_connectors/skoda/connector.py
24
25
  src/carconnectivity_connectors/skoda/mqtt_client.py
25
26
  src/carconnectivity_connectors/skoda/vehicle.py
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '0.1a7'
15
+ __version__ = version = '0.1a9'
16
16
  __version_tuple__ = version_tuple = (0, 1)
@@ -0,0 +1,67 @@
1
+ """
2
+ Module for charging for skoda vehicles.
3
+ """
4
+ from __future__ import annotations
5
+ from typing import TYPE_CHECKING
6
+
7
+ from enum import Enum
8
+
9
+ from carconnectivity.charging import Charging
10
+
11
+ if TYPE_CHECKING:
12
+ from typing import Dict
13
+
14
+
15
+ class SkodaCharging(Charging): # pylint: disable=too-many-instance-attributes
16
+ """
17
+ SkodaCharging class for handling Skoda vehicle charging information.
18
+
19
+ This class extends the Charging class and includes an enumeration of various
20
+ charging states specific to Skoda vehicles.
21
+ """
22
+ class SkodaChargingState(Enum,):
23
+ """
24
+ Enum representing the various charging states for a Skoda vehicle.
25
+
26
+ Attributes:
27
+ OFF: The vehicle is not charging.
28
+ READY_FOR_CHARGING: The vehicle is ready to start charging.
29
+ NOT_READY_FOR_CHARGING: The vehicle is not ready to start charging.
30
+ CONSERVATION: The vehicle is in conservation mode.
31
+ CHARGE_PURPOSE_REACHED_NOT_CONSERVATION_CHARGING: The vehicle has reached its charging purpose and is not in conservation charging mode.
32
+ CHARGE_PURPOSE_REACHED_CONSERVATION: The vehicle has reached its charging purpose and is in conservation charging mode.
33
+ CHARGING: The vehicle is currently charging.
34
+ ERROR: There is an error in the charging process.
35
+ UNSUPPORTED: The charging state is unsupported.
36
+ DISCHARGING: The vehicle is discharging.
37
+ UNKNOWN: The charging state is unknown.
38
+ """
39
+ OFF = 'off'
40
+ CONNECT_CABLE = 'connectCable'
41
+ READY_FOR_CHARGING = 'readyForCharging'
42
+ NOT_READY_FOR_CHARGING = 'notReadyForCharging'
43
+ CONSERVATION = 'conservation'
44
+ CHARGE_PURPOSE_REACHED_NOT_CONSERVATION_CHARGING = 'chargePurposeReachedAndNotConservationCharging'
45
+ CHARGE_PURPOSE_REACHED_CONSERVATION = 'chargePurposeReachedAndConservation'
46
+ CHARGING = 'charging'
47
+ ERROR = 'error'
48
+ UNSUPPORTED = 'unsupported'
49
+ DISCHARGING = 'discharging'
50
+ UNKNOWN = 'unknown charging state'
51
+
52
+
53
+ # Mapping of Skoda charging states to generic charging states
54
+ mapping_skoda_charging_state: Dict[SkodaCharging.SkodaChargingState, Charging.ChargingState] = {
55
+ SkodaCharging.SkodaChargingState.OFF: Charging.ChargingState.OFF,
56
+ SkodaCharging.SkodaChargingState.CONNECT_CABLE: Charging.ChargingState.OFF,
57
+ SkodaCharging.SkodaChargingState.READY_FOR_CHARGING: Charging.ChargingState.READY_FOR_CHARGING,
58
+ SkodaCharging.SkodaChargingState.NOT_READY_FOR_CHARGING: Charging.ChargingState.OFF,
59
+ SkodaCharging.SkodaChargingState.CONSERVATION: Charging.ChargingState.CONSERVATION,
60
+ SkodaCharging.SkodaChargingState.CHARGE_PURPOSE_REACHED_NOT_CONSERVATION_CHARGING: Charging.ChargingState.READY_FOR_CHARGING,
61
+ SkodaCharging.SkodaChargingState.CHARGE_PURPOSE_REACHED_CONSERVATION: Charging.ChargingState.CONSERVATION,
62
+ SkodaCharging.SkodaChargingState.CHARGING: Charging.ChargingState.CHARGING,
63
+ SkodaCharging.SkodaChargingState.ERROR: Charging.ChargingState.ERROR,
64
+ SkodaCharging.SkodaChargingState.UNSUPPORTED: Charging.ChargingState.UNSUPPORTED,
65
+ SkodaCharging.SkodaChargingState.DISCHARGING: Charging.ChargingState.DISCHARGING,
66
+ SkodaCharging.SkodaChargingState.UNKNOWN: Charging.ChargingState.UNKNOWN
67
+ }
@@ -6,7 +6,7 @@ import threading
6
6
  import os
7
7
  import logging
8
8
  import netrc
9
- from datetime import datetime, timedelta
9
+ from datetime import datetime, timedelta, timezone
10
10
  import requests
11
11
 
12
12
  from carconnectivity.garage import Garage
@@ -20,12 +20,15 @@ from carconnectivity.windows import Windows
20
20
  from carconnectivity.lights import Lights
21
21
  from carconnectivity.drive import GenericDrive, ElectricDrive, CombustionDrive
22
22
  from carconnectivity.attributes import BooleanAttribute, DurationAttribute
23
+ from carconnectivity.charging import Charging
24
+ from carconnectivity.position import Position
23
25
 
24
26
  from carconnectivity_connectors.base.connector import BaseConnector
25
27
  from carconnectivity_connectors.skoda.auth.session_manager import SessionManager, SessionUser, Service
26
28
  from carconnectivity_connectors.skoda.auth.my_skoda_session import MySkodaSession
27
29
  from carconnectivity_connectors.skoda.vehicle import SkodaVehicle, SkodaElectricVehicle, SkodaCombustionVehicle, SkodaHybridVehicle
28
30
  from carconnectivity_connectors.skoda.capability import Capability
31
+ from carconnectivity_connectors.skoda.charging import SkodaCharging, mapping_skoda_charging_state
29
32
  from carconnectivity_connectors.skoda._version import __version__
30
33
  from carconnectivity_connectors.skoda.mqtt_client import SkodaMQTTClient
31
34
 
@@ -150,12 +153,17 @@ class Connector(BaseConnector):
150
153
 
151
154
  def _background_loop(self) -> None:
152
155
  self._stop_event.clear()
156
+ fetch: bool = True
153
157
  while not self._stop_event.is_set():
154
158
  interval = 300
155
159
  try:
156
160
  try:
157
- self.fetch_all()
158
- self.last_update._set_value(value=datetime.now()) # pylint: disable=protected-access
161
+ if fetch:
162
+ self.fetch_all()
163
+ fetch = False
164
+ else:
165
+ self.update_vehicles()
166
+ self.last_update._set_value(value=datetime.now(tz=timezone.utc)) # pylint: disable=protected-access
159
167
  if self.interval.value is not None:
160
168
  interval: int = self.interval.value.total_seconds()
161
169
  except Exception:
@@ -267,15 +275,37 @@ class Connector(BaseConnector):
267
275
  vehicle.license_plate._set_value(None) # pylint: disable=protected-access
268
276
 
269
277
  log_extra_keys(LOG_API, 'vehicles', vehicle_dict, {'vin', 'licensePlate'})
270
- vehicle = self.fetch_vehicle_status(vehicle)
271
- if isinstance(vehicle, SkodaElectricVehicle):
272
- vehicle = self.fetch_charging(vehicle)
278
+
279
+ vehicle = self.fetch_vehicle_details(vehicle)
273
280
  else:
274
281
  raise APIError('Could not parse vehicle, vin missing')
275
282
  for vin in set(garage.list_vehicle_vins()) - seen_vehicle_vins:
276
283
  vehicle_to_remove = garage.get_vehicle(vin)
277
284
  if vehicle_to_remove is not None and vehicle_to_remove.is_managed_by_connector(self):
278
285
  garage.remove_vehicle(vin)
286
+ self.update_vehicles()
287
+
288
+ def update_vehicles(self) -> None:
289
+ """
290
+ Updates the status of all vehicles in the garage managed by this connector.
291
+
292
+ This method iterates through all vehicle VINs in the garage, and for each vehicle that is
293
+ managed by this connector and is an instance of SkodaVehicle, it updates the vehicle's status
294
+ by fetching data from various APIs. If the vehicle is an instance of SkodaElectricVehicle,
295
+ it also fetches charging information.
296
+
297
+ Returns:
298
+ None
299
+ """
300
+ garage: Garage = self.car_connectivity.garage
301
+ for vin in set(garage.list_vehicle_vins()):
302
+ vehicle_to_update: Optional[GenericVehicle] = garage.get_vehicle(vin)
303
+ if vehicle_to_update is not None and isinstance(vehicle_to_update, SkodaVehicle) and vehicle_to_update.is_managed_by_connector(self):
304
+ vehicle_to_update = self.fetch_vehicle_status_second_api(vehicle_to_update)
305
+ vehicle_to_update = self.fetch_driving_range(vehicle_to_update)
306
+ vehicle_to_update = self.fetch_position(vehicle_to_update)
307
+ if isinstance(vehicle_to_update, SkodaElectricVehicle):
308
+ vehicle_to_update = self.fetch_charging(vehicle_to_update)
279
309
 
280
310
  def fetch_charging(self, vehicle: SkodaElectricVehicle) -> SkodaElectricVehicle:
281
311
  """
@@ -301,6 +331,18 @@ class Connector(BaseConnector):
301
331
  else:
302
332
  raise APIError('Could not fetch charging, carCapturedTimestamp missing')
303
333
  if 'status' in data and data['status'] is not None:
334
+ if 'state' in data['status'] and data['status']['state'] is not None:
335
+ if data['status']['state'] in [item.name for item in SkodaCharging.SkodaChargingState]:
336
+ skoda_charging_state = SkodaCharging.SkodaChargingState[data['status']['state']]
337
+ charging_state: Charging.ChargingState = mapping_skoda_charging_state[skoda_charging_state]
338
+ else:
339
+ LOG_API.info('Unkown charging state %s not in %s', data['status']['state'], str(SkodaCharging.SkodaChargingState))
340
+ charging_state = Charging.ChargingState.UNKNOWN
341
+
342
+ # pylint: disable-next=protected-access
343
+ vehicle.charging.state._set_value(value=charging_state, measured=captured_at)
344
+ else:
345
+ vehicle.charging.state._set_value(None, measured=captured_at) # pylint: disable=protected-access
304
346
  if 'chargingRateInKilometersPerHour' in data['status'] and data['status']['chargingRateInKilometersPerHour'] is not None:
305
347
  # pylint: disable-next=protected-access
306
348
  vehicle.charging.rate._set_value(value=data['status']['chargingRateInKilometersPerHour'], measured=captured_at, unit=Speed.KMH)
@@ -319,13 +361,68 @@ class Connector(BaseConnector):
319
361
  vehicle.charging.remaining_duration._set_value(None, measured=captured_at) # pylint: disable=protected-access
320
362
  log_extra_keys(LOG_API, 'status', data['status'], {'chargingRateInKilometersPerHour',
321
363
  'chargePowerInKw',
322
- 'remainingTimeToFullyChargedInMinutes'})
364
+ 'remainingTimeToFullyChargedInMinutes',
365
+ 'state'})
323
366
  log_extra_keys(LOG_API, 'charging data', data, {'carCapturedTimestamp', 'status'})
324
367
  return vehicle
325
368
 
326
- def fetch_vehicle_status(self, vehicle: SkodaVehicle) -> SkodaVehicle:
369
+ def fetch_position(self, vehicle: SkodaVehicle) -> SkodaVehicle:
370
+ """
371
+ Fetches the position of the given Skoda vehicle and updates its position attributes.
372
+
373
+ Args:
374
+ vehicle (SkodaVehicle): The Skoda vehicle object containing the VIN and position attributes.
375
+
376
+ Returns:
377
+ SkodaVehicle: The updated Skoda vehicle object with the fetched position data.
378
+
379
+ Raises:
380
+ APIError: If the VIN is missing.
381
+ ValueError: If the vehicle has no position object.
382
+ """
383
+ vin = vehicle.vin.value
384
+ if vin is None:
385
+ raise APIError('VIN is missing')
386
+ if vehicle.position is None:
387
+ raise ValueError('Vehicle has no charging object')
388
+ url = f'https://mysmob.api.connect.skoda-auto.cz/api/v1/maps/positions?vin={vin}'
389
+ data: Dict[str, Any] | None = self._fetch_data(url, session=self.session)
390
+ if data is not None:
391
+ if 'positions' in data and data['positions'] is not None:
392
+ for position_dict in data['positions']:
393
+ if 'type' in position_dict and position_dict['type'] == 'VEHICLE':
394
+ if 'gpsCoordinates' in position_dict and position_dict['gpsCoordinates'] is not None:
395
+ if 'latitude' in position_dict['gpsCoordinates'] and position_dict['gpsCoordinates']['latitude'] is not None:
396
+ latitude: Optional[float] = position_dict['gpsCoordinates']['latitude']
397
+ else:
398
+ latitude = None
399
+ if 'longitude' in position_dict['gpsCoordinates'] and position_dict['gpsCoordinates']['longitude'] is not None:
400
+ longitude: Optional[float] = position_dict['gpsCoordinates']['longitude']
401
+ else:
402
+ longitude = None
403
+ vehicle.position.latitude._set_value(latitude) # pylint: disable=protected-access
404
+ vehicle.position.longitude._set_value(longitude) # pylint: disable=protected-access
405
+ vehicle.position.position_type._set_value(Position.PositionType.PARKING) # pylint: disable=protected-access
406
+ else:
407
+ vehicle.position.latitude._set_value(None) # pylint: disable=protected-access
408
+ vehicle.position.longitude._set_value(None) # pylint: disable=protected-access
409
+ vehicle.position.position_type._set_value(None) # pylint: disable=protected-access
410
+ else:
411
+ vehicle.position.latitude._set_value(None) # pylint: disable=protected-access
412
+ vehicle.position.longitude._set_value(None) # pylint: disable=protected-access
413
+ vehicle.position.position_type._set_value(None) # pylint: disable=protected-access
414
+ log_extra_keys(LOG_API, 'positions', position_dict, {'type',
415
+ 'gpsCoordinates',
416
+ 'address'})
417
+ else:
418
+ vehicle.position.latitude._set_value(None) # pylint: disable=protected-access
419
+ vehicle.position.longitude._set_value(None) # pylint: disable=protected-access
420
+ vehicle.position.position_type._set_value(None) # pylint: disable=protected-access
421
+ return vehicle
422
+
423
+ def fetch_vehicle_details(self, vehicle: SkodaVehicle) -> SkodaVehicle:
327
424
  """
328
- Fetches the status of a vehicle from the Skoda API.
425
+ Fetches the details of a vehicle from the Skoda API.
329
426
 
330
427
  Args:
331
428
  vehicle (GenericVehicle): The vehicle object containing the VIN.
@@ -374,7 +471,30 @@ class Connector(BaseConnector):
374
471
  else:
375
472
  vehicle.model._set_value(None) # pylint: disable=protected-access
376
473
  log_extra_keys(LOG_API, 'api/v2/garage/vehicles/VIN', vehicle_data, {'softwareVersion'})
474
+ return vehicle
475
+
476
+ def fetch_driving_range(self, vehicle: SkodaVehicle) -> SkodaVehicle:
477
+ """
478
+ Fetches the driving range data for a given Skoda vehicle and updates the vehicle object accordingly.
377
479
 
480
+ Args:
481
+ vehicle (SkodaVehicle): The Skoda vehicle object for which to fetch the driving range data.
482
+
483
+ Returns:
484
+ SkodaVehicle: The updated Skoda vehicle object with the fetched driving range data.
485
+
486
+ Raises:
487
+ APIError: If the vehicle's VIN is missing.
488
+
489
+ Notes:
490
+ - The method fetches data from the Skoda API using the vehicle's VIN.
491
+ - It updates the vehicle's type if the fetched data indicates a different type (e.g., electric, combustion, hybrid).
492
+ - It updates the vehicle's total range and individual drive ranges (primary and secondary) based on the fetched data.
493
+ - It logs warnings for unknown car types and engine types.
494
+ """
495
+ vin = vehicle.vin.value
496
+ if vin is None:
497
+ raise APIError('VIN is missing')
378
498
  url = f'https://mysmob.api.connect.skoda-auto.cz/api/v2/vehicle-status/{vin}/driving-range'
379
499
  range_data: Dict[str, Any] | None = self._fetch_data(url, self.session)
380
500
  if range_data:
@@ -461,7 +581,21 @@ class Connector(BaseConnector):
461
581
  'totalRangeInKm',
462
582
  'primaryEngineRange',
463
583
  'secondaryEngineRange'})
584
+ return vehicle
464
585
 
586
+ def fetch_vehicle_status_second_api(self, vehicle: SkodaVehicle) -> SkodaVehicle:
587
+ """
588
+ Fetches the status of a vehicle from other Skoda API.
589
+
590
+ Args:
591
+ vehicle (GenericVehicle): The vehicle object containing the VIN.
592
+
593
+ Returns:
594
+ None
595
+ """
596
+ vin = vehicle.vin.value
597
+ if vin is None:
598
+ raise APIError('VIN is missing')
465
599
  url = f'https://api.connect.skoda-auto.cz/api/v2/vehicle-status/{vin}'
466
600
  vehicle_status_data: Dict[str, Any] | None = self._fetch_data(url, self.session)
467
601
  if vehicle_status_data:
@@ -7,15 +7,21 @@ import logging
7
7
  import uuid
8
8
  import ssl
9
9
  import json
10
- from datetime import timedelta
10
+ from datetime import timedelta, timezone
11
11
 
12
12
  from paho.mqtt.client import Client
13
13
  from paho.mqtt.enums import MQTTProtocolVersion, CallbackAPIVersion, MQTTErrorCode
14
14
 
15
+ from carconnectivity.errors import CarConnectivityError
15
16
  from carconnectivity.observable import Observable
16
- from carconnectivity.vehicle import GenericVehicle, ElectricVehicle
17
+ from carconnectivity.vehicle import GenericVehicle
18
+
17
19
  from carconnectivity.drive import ElectricDrive
18
20
  from carconnectivity.util import robust_time_parse, log_extra_keys
21
+ from carconnectivity.charging import Charging
22
+
23
+ from carconnectivity_connectors.skoda.vehicle import SkodaElectricVehicle
24
+ from carconnectivity_connectors.skoda.charging import SkodaCharging, mapping_skoda_charging_state
19
25
 
20
26
 
21
27
  if TYPE_CHECKING:
@@ -101,7 +107,7 @@ class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
101
107
  if (flags & Observable.ObserverEvent.ENABLED) and isinstance(element, GenericVehicle):
102
108
  self._subscribe_vehicle(element)
103
109
  elif (flags & Observable.ObserverEvent.DISABLED) and isinstance(element, GenericVehicle):
104
- self._subscribe_vehicle(element)
110
+ self._unsubscribe_vehicle(element)
105
111
 
106
112
  def _subscribe_vehicles(self) -> None:
107
113
  """
@@ -235,15 +241,12 @@ class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
235
241
  - Warning if the vehicle's VIN is not enabled or is None.
236
242
  - Info for each topic successfully unsubscribed.
237
243
  """
238
- if not vehicle.vin.enabled or vehicle.vin.value is None:
239
- LOG.warning('Could not unsubscribe to vehicle without vin')
240
- else:
241
- vin: str = vehicle.vin.value
242
- for topic in self.subscribed_topics:
243
- if vin in topic:
244
- self.unsubscribe(topic)
245
- self.subscribed_topics.remove(topic)
246
- LOG.debug('Unsubscribed from topic %s', topic)
244
+ vin: str = vehicle.id
245
+ for topic in self.subscribed_topics:
246
+ if vin in topic:
247
+ self.unsubscribe(topic)
248
+ self.subscribed_topics.remove(topic)
249
+ LOG.debug('Unsubscribed from topic %s', topic)
247
250
 
248
251
  def _on_connect_callback(self, mqttc, obj, flags, reason_code, properties) -> None:
249
252
  """
@@ -444,27 +447,56 @@ class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
444
447
  if 'timestamp' in data and data['timestamp'] is not None:
445
448
  measured_at: datetime = robust_time_parse(data['timestamp'])
446
449
  else:
447
- measured_at: datetime = datetime.now()
450
+ measured_at: datetime = datetime.now(tz=timezone.utc)
448
451
  if service_event == 'charging':
449
452
  if 'name' in data and data['name'] == 'change-charge-mode' or data['name'] == 'change-soc':
450
453
  if 'data' in data and data['data'] is not None:
451
454
  vehicle: Optional[GenericVehicle] = self._skoda_connector.car_connectivity.garage.get_vehicle(vin)
452
- if isinstance(vehicle, ElectricVehicle):
455
+ if isinstance(vehicle, SkodaElectricVehicle):
453
456
  electric_drive: ElectricDrive = vehicle.get_electric_drive()
454
457
  if electric_drive is not None:
458
+ charging_state: Optional[Charging.ChargingState] = vehicle.charging.state.value
459
+ old_charging_state: Optional[Charging.ChargingState] = charging_state
460
+ if 'state' in data['data'] and data['data']['state'] is not None:
461
+ if data['data']['state'] in [item.value for item in SkodaCharging.SkodaChargingState]:
462
+ skoda_charging_state = SkodaCharging.SkodaChargingState(data['data']['state'])
463
+ charging_state = mapping_skoda_charging_state[skoda_charging_state]
464
+ else:
465
+ LOG_API.info('Unkown charging state %s not in %s', data['data']['state'], str(SkodaCharging.SkodaChargingState))
466
+ charging_state = Charging.ChargingState.UNKNOWN
467
+ # pylint: disable-next=protected-access
468
+ vehicle.charging.state._set_value(value=charging_state, measured=measured_at)
469
+ if charging_state == Charging.ChargingState.OFF:
470
+ # pylint: disable-next=protected-access
471
+ vehicle.charging.type._set_value(value=Charging.ChargingType.OFF, measured=measured_at)
472
+ # pylint: disable-next=protected-access
473
+ vehicle.charging.rate._set_value(value=0, measured=measured_at)
474
+ # pylint: disable-next=protected-access
475
+ vehicle.charging.power._set_value(value=0, measured=measured_at)
455
476
  if 'soc' in data['data'] and data['data']['soc'] is not None:
456
477
  electric_drive.level._set_value(measured=measured_at, value=data['data']['soc']) # pylint: disable=protected-access
457
478
  if 'chargedRange' in data['data'] and data['data']['chargedRange'] is not None:
458
479
  # pylint: disable-next=protected-access
459
480
  electric_drive.range._set_value(measured=measured_at, value=data['data']['chargedRange'])
481
+ # If charging state changed, fetch charging again
482
+ if old_charging_state != charging_state:
483
+ try:
484
+ self._skoda_connector.fetch_charging(vehicle)
485
+ except CarConnectivityError as e:
486
+ LOG.error('Error while fetching charging: %s', e)
460
487
  if 'timeToFinish' in data['data'] and data['data']['timeToFinish'] is not None \
461
488
  and vehicle.charging is not None:
462
- remaining_duration: timedelta = timedelta(minutes=data['data']['timeToFinish'])
489
+ try:
490
+ remaining_duration: Optional[timedelta] = timedelta(minutes=int(data['data']['timeToFinish']))
491
+ except ValueError:
492
+ remaining_duration: Optional[timedelta] = None
463
493
  # pylint: disable-next=protected-access
464
494
  vehicle.charging.remaining_duration._set_value(measured=measured_at, value=remaining_duration)
465
- log_extra_keys(LOG_API, 'data', data['data'], {'vin', 'userId', 'soc', 'chargedRange', 'timeToFinish'})
495
+ log_extra_keys(LOG_API, 'data', data['data'], {'vin', 'userId', 'soc', 'chargedRange', 'timeToFinish', 'state'})
466
496
  LOG.debug('Received %s event for vehicle %s from user %s', data['name'], vin, user_id)
467
497
  return
498
+ else:
499
+ LOG.debug('Discarded %s event for vehicle %s from user %s: vehicle is not an electric vehicle', data['name'], vin, user_id)
468
500
  LOG_API.info('Received event name %s service event %s for vehicle %s from user %s: %s', data['name'],
469
501
  service_event, vin, user_id, msg.payload)
470
502
  return