carconnectivity-connector-skoda 0.1a8__tar.gz → 0.1a10__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.1a8 → carconnectivity_connector_skoda-0.1a10}/PKG-INFO +2 -2
  2. {carconnectivity_connector_skoda-0.1a8 → carconnectivity_connector_skoda-0.1a10}/setup_requirements.txt +1 -1
  3. {carconnectivity_connector_skoda-0.1a8 → carconnectivity_connector_skoda-0.1a10}/src/carconnectivity_connector_skoda.egg-info/PKG-INFO +2 -2
  4. {carconnectivity_connector_skoda-0.1a8 → carconnectivity_connector_skoda-0.1a10}/src/carconnectivity_connector_skoda.egg-info/SOURCES.txt +1 -0
  5. {carconnectivity_connector_skoda-0.1a8 → carconnectivity_connector_skoda-0.1a10}/src/carconnectivity_connectors/skoda/_version.py +1 -1
  6. {carconnectivity_connector_skoda-0.1a8 → carconnectivity_connector_skoda-0.1a10}/src/carconnectivity_connectors/skoda/auth/openid_session.py +4 -0
  7. carconnectivity_connector_skoda-0.1a10/src/carconnectivity_connectors/skoda/charging.py +67 -0
  8. {carconnectivity_connector_skoda-0.1a8 → carconnectivity_connector_skoda-0.1a10}/src/carconnectivity_connectors/skoda/connector.py +255 -12
  9. {carconnectivity_connector_skoda-0.1a8 → carconnectivity_connector_skoda-0.1a10}/src/carconnectivity_connectors/skoda/mqtt_client.py +84 -18
  10. {carconnectivity_connector_skoda-0.1a8 → carconnectivity_connector_skoda-0.1a10}/.flake8 +0 -0
  11. {carconnectivity_connector_skoda-0.1a8 → carconnectivity_connector_skoda-0.1a10}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  12. {carconnectivity_connector_skoda-0.1a8 → carconnectivity_connector_skoda-0.1a10}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  13. {carconnectivity_connector_skoda-0.1a8 → carconnectivity_connector_skoda-0.1a10}/.github/dependabot.yml +0 -0
  14. {carconnectivity_connector_skoda-0.1a8 → carconnectivity_connector_skoda-0.1a10}/.github/workflows/build.yml +0 -0
  15. {carconnectivity_connector_skoda-0.1a8 → carconnectivity_connector_skoda-0.1a10}/.github/workflows/build_and_publish.yml +0 -0
  16. {carconnectivity_connector_skoda-0.1a8 → carconnectivity_connector_skoda-0.1a10}/.github/workflows/codeql-analysis.yml +0 -0
  17. {carconnectivity_connector_skoda-0.1a8 → carconnectivity_connector_skoda-0.1a10}/.gitignore +0 -0
  18. {carconnectivity_connector_skoda-0.1a8 → carconnectivity_connector_skoda-0.1a10}/LICENSE +0 -0
  19. {carconnectivity_connector_skoda-0.1a8 → carconnectivity_connector_skoda-0.1a10}/Makefile +0 -0
  20. {carconnectivity_connector_skoda-0.1a8 → carconnectivity_connector_skoda-0.1a10}/README.md +0 -0
  21. {carconnectivity_connector_skoda-0.1a8 → carconnectivity_connector_skoda-0.1a10}/doc/Config.md +0 -0
  22. {carconnectivity_connector_skoda-0.1a8 → carconnectivity_connector_skoda-0.1a10}/pyproject.toml +0 -0
  23. {carconnectivity_connector_skoda-0.1a8 → carconnectivity_connector_skoda-0.1a10}/setup.cfg +0 -0
  24. {carconnectivity_connector_skoda-0.1a8 → carconnectivity_connector_skoda-0.1a10}/src/carconnectivity_connector_skoda.egg-info/dependency_links.txt +0 -0
  25. {carconnectivity_connector_skoda-0.1a8 → carconnectivity_connector_skoda-0.1a10}/src/carconnectivity_connector_skoda.egg-info/requires.txt +0 -0
  26. {carconnectivity_connector_skoda-0.1a8 → carconnectivity_connector_skoda-0.1a10}/src/carconnectivity_connector_skoda.egg-info/top_level.txt +0 -0
  27. {carconnectivity_connector_skoda-0.1a8 → carconnectivity_connector_skoda-0.1a10}/src/carconnectivity_connectors/skoda/__init__.py +0 -0
  28. {carconnectivity_connector_skoda-0.1a8 → carconnectivity_connector_skoda-0.1a10}/src/carconnectivity_connectors/skoda/auth/__init__.py +0 -0
  29. {carconnectivity_connector_skoda-0.1a8 → carconnectivity_connector_skoda-0.1a10}/src/carconnectivity_connectors/skoda/auth/auth_util.py +0 -0
  30. {carconnectivity_connector_skoda-0.1a8 → carconnectivity_connector_skoda-0.1a10}/src/carconnectivity_connectors/skoda/auth/helpers/blacklist_retry.py +0 -0
  31. {carconnectivity_connector_skoda-0.1a8 → carconnectivity_connector_skoda-0.1a10}/src/carconnectivity_connectors/skoda/auth/my_skoda_session.py +0 -0
  32. {carconnectivity_connector_skoda-0.1a8 → carconnectivity_connector_skoda-0.1a10}/src/carconnectivity_connectors/skoda/auth/session_manager.py +0 -0
  33. {carconnectivity_connector_skoda-0.1a8 → carconnectivity_connector_skoda-0.1a10}/src/carconnectivity_connectors/skoda/auth/skoda_web_session.py +0 -0
  34. {carconnectivity_connector_skoda-0.1a8 → carconnectivity_connector_skoda-0.1a10}/src/carconnectivity_connectors/skoda/capability.py +0 -0
  35. {carconnectivity_connector_skoda-0.1a8 → carconnectivity_connector_skoda-0.1a10}/src/carconnectivity_connectors/skoda/vehicle.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: carconnectivity-connector-skoda
3
- Version: 0.1a8
3
+ Version: 0.1a10
4
4
  Summary: CarConnectivity connector for Skoda services
5
5
  Author: Till Steinbach
6
6
  License: MIT License
@@ -1,3 +1,3 @@
1
1
  flake8~=7.1.1
2
- pylint~=3.3.2
2
+ pylint~=3.3.3
3
3
  bandit~=1.8.0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: carconnectivity-connector-skoda
3
- Version: 0.1a8
3
+ Version: 0.1a10
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.1a8'
15
+ __version__ = version = '0.1a10'
16
16
  __version_tuple__ = version_tuple = (0, 1)
@@ -112,6 +112,10 @@ class OpenIDSession(requests.Session):
112
112
  if new_retries_value:
113
113
  # Retry on internal server error (500)
114
114
  retries = BlacklistRetry(total=new_retries_value,
115
+ connect=new_retries_value,
116
+ read=new_retries_value,
117
+ status=new_retries_value,
118
+ other=new_retries_value,
115
119
  backoff_factor=0.1,
116
120
  status_forcelist=[500],
117
121
  status_blacklist=[429],
@@ -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
@@ -14,18 +14,22 @@ from carconnectivity.vehicle import GenericVehicle
14
14
  from carconnectivity.errors import AuthenticationError, TooManyRequestsError, RetrievalError, APIError, APICompatibilityError, \
15
15
  TemporaryAuthenticationError, ConfigurationError
16
16
  from carconnectivity.util import robust_time_parse, log_extra_keys, config_remove_credentials
17
- from carconnectivity.units import Length, Speed, Power
17
+ from carconnectivity.units import Length, Speed, Power, Temperature
18
18
  from carconnectivity.doors import Doors
19
19
  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
25
+ from carconnectivity.climatization import Climatization
23
26
 
24
27
  from carconnectivity_connectors.base.connector import BaseConnector
25
28
  from carconnectivity_connectors.skoda.auth.session_manager import SessionManager, SessionUser, Service
26
29
  from carconnectivity_connectors.skoda.auth.my_skoda_session import MySkodaSession
27
30
  from carconnectivity_connectors.skoda.vehicle import SkodaVehicle, SkodaElectricVehicle, SkodaCombustionVehicle, SkodaHybridVehicle
28
31
  from carconnectivity_connectors.skoda.capability import Capability
32
+ from carconnectivity_connectors.skoda.charging import SkodaCharging, mapping_skoda_charging_state
29
33
  from carconnectivity_connectors.skoda._version import __version__
30
34
  from carconnectivity_connectors.skoda.mqtt_client import SkodaMQTTClient
31
35
 
@@ -150,12 +154,17 @@ class Connector(BaseConnector):
150
154
 
151
155
  def _background_loop(self) -> None:
152
156
  self._stop_event.clear()
157
+ fetch: bool = True
153
158
  while not self._stop_event.is_set():
154
159
  interval = 300
155
160
  try:
156
161
  try:
157
- self.fetch_all()
158
- self.last_update._set_value(value=datetime.now()) # pylint: disable=protected-access
162
+ if fetch:
163
+ self.fetch_all()
164
+ fetch = False
165
+ else:
166
+ self.update_vehicles()
167
+ self.last_update._set_value(value=datetime.now(tz=timezone.utc)) # pylint: disable=protected-access
159
168
  if self.interval.value is not None:
160
169
  interval: int = self.interval.value.total_seconds()
161
170
  except Exception:
@@ -267,15 +276,41 @@ class Connector(BaseConnector):
267
276
  vehicle.license_plate._set_value(None) # pylint: disable=protected-access
268
277
 
269
278
  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)
279
+
280
+ vehicle = self.fetch_vehicle_details(vehicle)
273
281
  else:
274
282
  raise APIError('Could not parse vehicle, vin missing')
275
283
  for vin in set(garage.list_vehicle_vins()) - seen_vehicle_vins:
276
284
  vehicle_to_remove = garage.get_vehicle(vin)
277
285
  if vehicle_to_remove is not None and vehicle_to_remove.is_managed_by_connector(self):
278
286
  garage.remove_vehicle(vin)
287
+ self.update_vehicles()
288
+
289
+ def update_vehicles(self) -> None:
290
+ """
291
+ Updates the status of all vehicles in the garage managed by this connector.
292
+
293
+ This method iterates through all vehicle VINs in the garage, and for each vehicle that is
294
+ managed by this connector and is an instance of SkodaVehicle, it updates the vehicle's status
295
+ by fetching data from various APIs. If the vehicle is an instance of SkodaElectricVehicle,
296
+ it also fetches charging information.
297
+
298
+ Returns:
299
+ None
300
+ """
301
+ garage: Garage = self.car_connectivity.garage
302
+ for vin in set(garage.list_vehicle_vins()):
303
+ vehicle_to_update: Optional[GenericVehicle] = garage.get_vehicle(vin)
304
+ 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.fetch_vehicle_status_second_api(vehicle_to_update)
306
+ vehicle_to_update = self.fetch_driving_range(vehicle_to_update)
307
+ if vehicle_to_update.capabilities is not None and vehicle_to_update.capabilities.enabled:
308
+ if vehicle_to_update.capabilities.has_capability('PARKING_POSITION'):
309
+ vehicle_to_update = self.fetch_position(vehicle_to_update)
310
+ if vehicle_to_update.capabilities.has_capability('CHARGING') and isinstance(vehicle_to_update, SkodaElectricVehicle):
311
+ vehicle_to_update = self.fetch_charging(vehicle_to_update)
312
+ if vehicle_to_update.capabilities.has_capability('AIR_CONDITIONING'):
313
+ vehicle_to_update = self.fetch_air_conditioning(vehicle_to_update)
279
314
 
280
315
  def fetch_charging(self, vehicle: SkodaElectricVehicle) -> SkodaElectricVehicle:
281
316
  """
@@ -301,6 +336,18 @@ class Connector(BaseConnector):
301
336
  else:
302
337
  raise APIError('Could not fetch charging, carCapturedTimestamp missing')
303
338
  if 'status' in data and data['status'] is not None:
339
+ if 'state' in data['status'] and data['status']['state'] is not None:
340
+ if data['status']['state'] in [item.name for item in SkodaCharging.SkodaChargingState]:
341
+ skoda_charging_state = SkodaCharging.SkodaChargingState[data['status']['state']]
342
+ charging_state: Charging.ChargingState = mapping_skoda_charging_state[skoda_charging_state]
343
+ else:
344
+ LOG_API.info('Unkown charging state %s not in %s', data['status']['state'], str(SkodaCharging.SkodaChargingState))
345
+ charging_state = Charging.ChargingState.UNKNOWN
346
+
347
+ # pylint: disable-next=protected-access
348
+ vehicle.charging.state._set_value(value=charging_state, measured=captured_at)
349
+ else:
350
+ vehicle.charging.state._set_value(None, measured=captured_at) # pylint: disable=protected-access
304
351
  if 'chargingRateInKilometersPerHour' in data['status'] and data['status']['chargingRateInKilometersPerHour'] is not None:
305
352
  # pylint: disable-next=protected-access
306
353
  vehicle.charging.rate._set_value(value=data['status']['chargingRateInKilometersPerHour'], measured=captured_at, unit=Speed.KMH)
@@ -313,19 +360,178 @@ class Connector(BaseConnector):
313
360
  vehicle.charging.power._set_value(None, measured=captured_at, unit=Power.KW) # pylint: disable=protected-access
314
361
  if 'remainingTimeToFullyChargedInMinutes' in data['status'] and data['status']['remainingTimeToFullyChargedInMinutes'] is not None:
315
362
  remaining_duration: timedelta = timedelta(minutes=data['status']['remainingTimeToFullyChargedInMinutes'])
363
+ estimated_date_reached: datetime = captured_at + remaining_duration
316
364
  # pylint: disable-next=protected-access
317
- vehicle.charging.remaining_duration._set_value(value=remaining_duration, measured=captured_at)
365
+ vehicle.charging.estimated_date_reached._set_value(value=estimated_date_reached, measured=captured_at)
318
366
  else:
319
- vehicle.charging.remaining_duration._set_value(None, measured=captured_at) # pylint: disable=protected-access
367
+ vehicle.charging.estimated_date_reached._set_value(None, measured=captured_at) # pylint: disable=protected-access
320
368
  log_extra_keys(LOG_API, 'status', data['status'], {'chargingRateInKilometersPerHour',
321
369
  'chargePowerInKw',
322
- 'remainingTimeToFullyChargedInMinutes'})
370
+ 'remainingTimeToFullyChargedInMinutes',
371
+ 'state'})
323
372
  log_extra_keys(LOG_API, 'charging data', data, {'carCapturedTimestamp', 'status'})
324
373
  return vehicle
325
374
 
326
- def fetch_vehicle_status(self, vehicle: SkodaVehicle) -> SkodaVehicle:
375
+ def fetch_position(self, vehicle: SkodaVehicle) -> SkodaVehicle:
376
+ """
377
+ Fetches the position of the given Skoda vehicle and updates its position attributes.
378
+
379
+ Args:
380
+ vehicle (SkodaVehicle): The Skoda vehicle object containing the VIN and position attributes.
381
+
382
+ Returns:
383
+ SkodaVehicle: The updated Skoda vehicle object with the fetched position data.
384
+
385
+ Raises:
386
+ APIError: If the VIN is missing.
387
+ ValueError: If the vehicle has no position object.
388
+ """
389
+ vin = vehicle.vin.value
390
+ if vin is None:
391
+ raise APIError('VIN is missing')
392
+ if vehicle.position is None:
393
+ raise ValueError('Vehicle has no charging object')
394
+ 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, session=self.session)
396
+ if data is not None:
397
+ if 'positions' in data and data['positions'] is not None:
398
+ for position_dict in data['positions']:
399
+ if 'type' in position_dict and position_dict['type'] == 'VEHICLE':
400
+ if 'gpsCoordinates' in position_dict and position_dict['gpsCoordinates'] is not None:
401
+ if 'latitude' in position_dict['gpsCoordinates'] and position_dict['gpsCoordinates']['latitude'] is not None:
402
+ latitude: Optional[float] = position_dict['gpsCoordinates']['latitude']
403
+ else:
404
+ latitude = None
405
+ if 'longitude' in position_dict['gpsCoordinates'] and position_dict['gpsCoordinates']['longitude'] is not None:
406
+ longitude: Optional[float] = position_dict['gpsCoordinates']['longitude']
407
+ else:
408
+ longitude = None
409
+ vehicle.position.latitude._set_value(latitude) # pylint: disable=protected-access
410
+ vehicle.position.longitude._set_value(longitude) # pylint: disable=protected-access
411
+ vehicle.position.position_type._set_value(Position.PositionType.PARKING) # pylint: disable=protected-access
412
+ else:
413
+ vehicle.position.latitude._set_value(None) # pylint: disable=protected-access
414
+ vehicle.position.longitude._set_value(None) # pylint: disable=protected-access
415
+ vehicle.position.position_type._set_value(None) # pylint: disable=protected-access
416
+ else:
417
+ vehicle.position.latitude._set_value(None) # pylint: disable=protected-access
418
+ vehicle.position.longitude._set_value(None) # pylint: disable=protected-access
419
+ vehicle.position.position_type._set_value(None) # pylint: disable=protected-access
420
+ log_extra_keys(LOG_API, 'positions', position_dict, {'type',
421
+ 'gpsCoordinates',
422
+ 'address'})
423
+ else:
424
+ vehicle.position.latitude._set_value(None) # pylint: disable=protected-access
425
+ vehicle.position.longitude._set_value(None) # pylint: disable=protected-access
426
+ vehicle.position.position_type._set_value(None) # pylint: disable=protected-access
427
+ return vehicle
428
+
429
+ def fetch_air_conditioning(self, vehicle: SkodaVehicle) -> SkodaVehicle:
327
430
  """
328
- Fetches the status of a vehicle from the Skoda API.
431
+ Fetches the air conditioning data for a given Skoda vehicle and updates the vehicle object with the retrieved data.
432
+
433
+ Args:
434
+ vehicle (SkodaVehicle): The vehicle object for which to fetch air conditioning data.
435
+
436
+ Returns:
437
+ SkodaVehicle: The updated vehicle object with the fetched air conditioning data.
438
+
439
+ Raises:
440
+ APIError: If the VIN is missing or if the carCapturedTimestamp is missing in the response data.
441
+ ValueError: If the vehicle has no charging object.
442
+
443
+ Notes:
444
+ - The method fetches data from the Skoda API using the vehicle's VIN.
445
+ - It updates the vehicle's climatization state, estimated date to reach target temperature, target temperature, and outside temperature.
446
+ - Logs additional keys found in the response data for debugging purposes.
447
+ """
448
+ vin = vehicle.vin.value
449
+ if vin is None:
450
+ raise APIError('VIN is missing')
451
+ if vehicle.position is None:
452
+ raise ValueError('Vehicle has no charging object')
453
+ url = f'https://mysmob.api.connect.skoda-auto.cz/api/v2/air-conditioning/{vin}'
454
+ data: Dict[str, Any] | None = self._fetch_data(url, session=self.session)
455
+ if data is not None:
456
+ if 'carCapturedTimestamp' in data and data['carCapturedTimestamp'] is not None:
457
+ captured_at: datetime = robust_time_parse(data['carCapturedTimestamp'])
458
+ else:
459
+ raise APIError('Could not fetch air conditioning, carCapturedTimestamp missing')
460
+ if 'state' in data and data['state'] is not None:
461
+ if data['state'] in [item.name for item in Climatization.ClimatizationState]:
462
+ climatization_state: Climatization.ClimatizationState = Climatization.ClimatizationState[data['state']]
463
+ else:
464
+ LOG_API.info('Unknown climatization state %s not in %s', data['state'], str(Climatization.ClimatizationState))
465
+ climatization_state = Climatization.ClimatizationState.UNKNOWN
466
+ vehicle.climatization.state._set_value(value=climatization_state, measured=captured_at) # pylint: disable=protected-access
467
+ else:
468
+ vehicle.climatization.state._set_value(None, measured=captured_at) # pylint: disable=protected-access
469
+ if 'estimatedDateTimeToReachTargetTemperature' in data and data['estimatedDateTimeToReachTargetTemperature'] is not None:
470
+ estimated_reach: datetime = robust_time_parse(data['estimatedDateTimeToReachTargetTemperature'])
471
+ if estimated_reach is not None:
472
+ vehicle.climatization.estimated_date_reached._set_value(value=estimated_reach, measured=captured_at) # pylint: disable=protected-access
473
+ else:
474
+ vehicle.climatization.estimated_date_reached._set_value(value=None, measured=captured_at) # pylint: disable=protected-access
475
+ else:
476
+ vehicle.climatization.estimated_date_reached._set_value(value=None, measured=captured_at) # pylint: disable=protected-access
477
+ if 'targetTemperature' in data and data['targetTemperature'] is not None:
478
+ unit: Temperature = Temperature.UNKNOWN
479
+ if 'unitInCar' in data['targetTemperature'] and data['targetTemperature']['unitInCar'] is not None:
480
+ if data['targetTemperature']['unitInCar'] == 'CELSIUS':
481
+ unit = Temperature.C
482
+ elif data['targetTemperature']['unitInCar'] == 'FAHRENHEIT':
483
+ unit = Temperature.F
484
+ elif data['targetTemperature']['unitInCar'] == 'KELVIN':
485
+ unit = Temperature.K
486
+ else:
487
+ LOG_API.info('Unknown temperature unit for targetTemperature in air-conditioning %s', data['targetTemperature']['unitInCar'])
488
+ if 'temperatureValue' in data['targetTemperature'] and data['targetTemperature']['temperatureValue'] is not None:
489
+ # pylint: disable-next=protected-access
490
+ vehicle.climatization.target_temperature._set_value(value=data['targetTemperature']['temperatureValue'],
491
+ measured=captured_at,
492
+ unit=unit)
493
+ else:
494
+ vehicle.climatization.target_temperature._set_value(value=None, measured=captured_at, unit=unit) # pylint: disable=protected-access
495
+ log_extra_keys(LOG_API, 'targetTemperature', data['targetTemperature'], {'unitInCar', 'temperatureValue'})
496
+ else:
497
+ # pylint: disable-next=protected-access
498
+ vehicle.climatization.target_temperature._set_value(value=None, measured=captured_at, unit=Temperature.UNKNOWN)
499
+ if 'outsideTemperature' in data and data['outsideTemperature'] is not None:
500
+ if 'carCapturedTimestamp' in data['outsideTemperature'] and data['outsideTemperature']['carCapturedTimestamp'] is not None:
501
+ outside_captured_at: datetime = robust_time_parse(data['outsideTemperature']['carCapturedTimestamp'])
502
+ else:
503
+ outside_captured_at = captured_at
504
+ if 'temperatureUnit' in data['outsideTemperature'] and data['outsideTemperature']['temperatureUnit'] is not None:
505
+ unit: Temperature = Temperature.UNKNOWN
506
+ if data['outsideTemperature']['temperatureUnit'] == 'CELSIUS':
507
+ unit = Temperature.C
508
+ elif data['outsideTemperature']['temperatureUnit'] == 'FAHRENHEIT':
509
+ unit = Temperature.F
510
+ elif data['outsideTemperature']['temperatureUnit'] == 'KELVIN':
511
+ unit = Temperature.K
512
+ else:
513
+ LOG_API.info('Unknown temperature unit for outsideTemperature in air-conditioning %s', data['targetTemperature']['temperatureUnit'])
514
+ if 'temperatureValue' in data['outsideTemperature'] and data['outsideTemperature']['temperatureValue'] is not None:
515
+ # pylint: disable-next=protected-access
516
+ vehicle.outside_temperature._set_value(value=data['outsideTemperature']['temperatureValue'],
517
+ measured=outside_captured_at,
518
+ unit=unit)
519
+ else:
520
+ # pylint: disable-next=protected-access
521
+ vehicle.outside_temperature._set_value(value=None, measured=outside_captured_at, unit=Temperature.UNKNOWN)
522
+ else:
523
+ # pylint: disable-next=protected-access
524
+ vehicle.outside_temperature._set_value(value=None, measured=outside_captured_at, unit=Temperature.UNKNOWN)
525
+ log_extra_keys(LOG_API, 'targetTemperature', data['outsideTemperature'], {'carCapturedTimestamp', 'temperatureUnit', 'temperatureValue'})
526
+ else:
527
+ vehicle.outside_temperature._set_value(value=None, measured=None, unit=Temperature.UNKNOWN) # pylint: disable=protected-access
528
+ log_extra_keys(LOG_API, 'air-condition', data, {'carCapturedTimestamp', 'state', 'estimatedDateTimeToReachTargetTemperature'
529
+ 'targetTemperature', 'outsideTemperature'})
530
+ return vehicle
531
+
532
+ def fetch_vehicle_details(self, vehicle: SkodaVehicle) -> SkodaVehicle:
533
+ """
534
+ Fetches the details of a vehicle from the Skoda API.
329
535
 
330
536
  Args:
331
537
  vehicle (GenericVehicle): The vehicle object containing the VIN.
@@ -374,7 +580,30 @@ class Connector(BaseConnector):
374
580
  else:
375
581
  vehicle.model._set_value(None) # pylint: disable=protected-access
376
582
  log_extra_keys(LOG_API, 'api/v2/garage/vehicles/VIN', vehicle_data, {'softwareVersion'})
583
+ return vehicle
377
584
 
585
+ def fetch_driving_range(self, vehicle: SkodaVehicle) -> SkodaVehicle:
586
+ """
587
+ Fetches the driving range data for a given Skoda vehicle and updates the vehicle object accordingly.
588
+
589
+ Args:
590
+ vehicle (SkodaVehicle): The Skoda vehicle object for which to fetch the driving range data.
591
+
592
+ Returns:
593
+ SkodaVehicle: The updated Skoda vehicle object with the fetched driving range data.
594
+
595
+ Raises:
596
+ APIError: If the vehicle's VIN is missing.
597
+
598
+ Notes:
599
+ - The method fetches data from the Skoda API using the vehicle's VIN.
600
+ - It updates the vehicle's type if the fetched data indicates a different type (e.g., electric, combustion, hybrid).
601
+ - It updates the vehicle's total range and individual drive ranges (primary and secondary) based on the fetched data.
602
+ - It logs warnings for unknown car types and engine types.
603
+ """
604
+ vin = vehicle.vin.value
605
+ if vin is None:
606
+ raise APIError('VIN is missing')
378
607
  url = f'https://mysmob.api.connect.skoda-auto.cz/api/v2/vehicle-status/{vin}/driving-range'
379
608
  range_data: Dict[str, Any] | None = self._fetch_data(url, self.session)
380
609
  if range_data:
@@ -461,7 +690,21 @@ class Connector(BaseConnector):
461
690
  'totalRangeInKm',
462
691
  'primaryEngineRange',
463
692
  'secondaryEngineRange'})
693
+ return vehicle
694
+
695
+ def fetch_vehicle_status_second_api(self, vehicle: SkodaVehicle) -> SkodaVehicle:
696
+ """
697
+ Fetches the status of a vehicle from other Skoda API.
698
+
699
+ Args:
700
+ vehicle (GenericVehicle): The vehicle object containing the VIN.
464
701
 
702
+ Returns:
703
+ None
704
+ """
705
+ vin = vehicle.vin.value
706
+ if vin is None:
707
+ raise APIError('VIN is missing')
465
708
  url = f'https://api.connect.skoda-auto.cz/api/v2/vehicle-status/{vin}'
466
709
  vehicle_status_data: Dict[str, Any] | None = self._fetch_data(url, self.session)
467
710
  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 SkodaVehicle, 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
  """
@@ -434,7 +437,7 @@ class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
434
437
  return
435
438
 
436
439
  # service_events
437
- match = re.match(r'^(?P<user_id>[0-9a-fA-F-]+)/(?P<vin>[A-Z0-9]+)/service-event/(?P<service_event>\w+)$', msg.topic)
440
+ match = re.match(r'^(?P<user_id>[0-9a-fA-F-]+)/(?P<vin>[A-Z0-9]+)/service-event/(?P<service_event>[a-zA-Z0-9-_]+)$', msg.topic)
438
441
  if match:
439
442
  user_id: str = match.group('user_id')
440
443
  vin: str = match.group('vin')
@@ -444,30 +447,93 @@ 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=int(data['data']['timeToFinish']))
489
+ try:
490
+ remaining_duration: Optional[timedelta] = timedelta(minutes=int(data['data']['timeToFinish']))
491
+ estimated_date_reached: Optional[datetime] = measured_at + remaining_duration
492
+ except ValueError:
493
+ estimated_date_reached: Optional[datetime] = None
463
494
  # pylint: disable-next=protected-access
464
- 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
+ vehicle.charging.estimated_date_reached._set_value(measured=measured_at, value=estimated_date_reached)
496
+ log_extra_keys(LOG_API, 'data', data['data'], {'vin', 'userId', 'soc', 'chargedRange', 'timeToFinish', 'state'})
466
497
  LOG.debug('Received %s event for vehicle %s from user %s', data['name'], vin, user_id)
467
498
  return
499
+ else:
500
+ LOG.debug('Discarded %s event for vehicle %s from user %s: vehicle is not an electric vehicle', data['name'], vin, user_id)
501
+ LOG_API.info('Received event name %s service event %s for vehicle %s from user %s: %s', data['name'],
502
+ service_event, vin, user_id, msg.payload)
503
+ return
504
+ elif service_event == 'air-conditioning':
505
+ if 'name' in data and data['name'] == 'change-remaining-time':
506
+ if 'data' in data and data['data'] is not None:
507
+ vehicle: Optional[GenericVehicle] = self._skoda_connector.car_connectivity.garage.get_vehicle(vin)
508
+ if isinstance(vehicle, SkodaVehicle):
509
+ try:
510
+ self._skoda_connector.fetch_air_conditioning(vehicle)
511
+ except CarConnectivityError as e:
512
+ LOG.error('Error while fetching charging: %s', e)
468
513
  LOG_API.info('Received event name %s service event %s for vehicle %s from user %s: %s', data['name'],
469
514
  service_event, vin, user_id, msg.payload)
470
515
  return
471
516
  LOG_API.info('Received unknown service event %s for vehicle %s from user %s: %s', service_event, vin, user_id, msg.payload)
472
517
  return
518
+ # service_events
519
+ 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)
520
+ if match:
521
+ user_id: str = match.group('user_id')
522
+ vin: str = match.group('vin')
523
+ operation_request: str = match.group('operation_request')
524
+ data: Dict[str, Any] = json.loads(msg.payload)
525
+ if data is not None:
526
+ if operation_request == 'air-conditioning/start-stop-air-conditioning':
527
+ vehicle: Optional[GenericVehicle] = self._skoda_connector.car_connectivity.garage.get_vehicle(vin)
528
+ if isinstance(vehicle, SkodaVehicle):
529
+ if 'status' in data and data['status'] is not None:
530
+ if data['status'] == 'COMPLETED_SUCCESS':
531
+ LOG.debug('Received %s operation request for vehicle %s from user %s', operation_request, vin, user_id)
532
+ try:
533
+ self._skoda_connector.fetch_air_conditioning(vehicle)
534
+ except CarConnectivityError as e:
535
+ LOG.error('Error while fetching air-conditioning: %s', e)
536
+ return
537
+ LOG_API.info('Received unknown operation request %s for vehicle %s from user %s: %s', operation_request, vin, user_id, msg.payload)
538
+ return
473
539
  LOG_API.info('I don\'t understand message %s: %s', msg.topic, msg.payload)