carconnectivity-connector-skoda 0.1a9__tar.gz → 0.1a11__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.1a9 → carconnectivity_connector_skoda-0.1a11}/PKG-INFO +2 -2
  2. {carconnectivity_connector_skoda-0.1a9 → carconnectivity_connector_skoda-0.1a11}/setup_requirements.txt +1 -1
  3. {carconnectivity_connector_skoda-0.1a9 → carconnectivity_connector_skoda-0.1a11}/src/carconnectivity_connector_skoda.egg-info/PKG-INFO +2 -2
  4. {carconnectivity_connector_skoda-0.1a9 → carconnectivity_connector_skoda-0.1a11}/src/carconnectivity_connectors/skoda/_version.py +1 -1
  5. {carconnectivity_connector_skoda-0.1a9 → carconnectivity_connector_skoda-0.1a11}/src/carconnectivity_connectors/skoda/auth/openid_session.py +4 -0
  6. {carconnectivity_connector_skoda-0.1a9 → carconnectivity_connector_skoda-0.1a11}/src/carconnectivity_connectors/skoda/connector.py +129 -19
  7. {carconnectivity_connector_skoda-0.1a9 → carconnectivity_connector_skoda-0.1a11}/src/carconnectivity_connectors/skoda/mqtt_client.py +88 -5
  8. {carconnectivity_connector_skoda-0.1a9 → carconnectivity_connector_skoda-0.1a11}/.flake8 +0 -0
  9. {carconnectivity_connector_skoda-0.1a9 → carconnectivity_connector_skoda-0.1a11}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  10. {carconnectivity_connector_skoda-0.1a9 → carconnectivity_connector_skoda-0.1a11}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  11. {carconnectivity_connector_skoda-0.1a9 → carconnectivity_connector_skoda-0.1a11}/.github/dependabot.yml +0 -0
  12. {carconnectivity_connector_skoda-0.1a9 → carconnectivity_connector_skoda-0.1a11}/.github/workflows/build.yml +0 -0
  13. {carconnectivity_connector_skoda-0.1a9 → carconnectivity_connector_skoda-0.1a11}/.github/workflows/build_and_publish.yml +0 -0
  14. {carconnectivity_connector_skoda-0.1a9 → carconnectivity_connector_skoda-0.1a11}/.github/workflows/codeql-analysis.yml +0 -0
  15. {carconnectivity_connector_skoda-0.1a9 → carconnectivity_connector_skoda-0.1a11}/.gitignore +0 -0
  16. {carconnectivity_connector_skoda-0.1a9 → carconnectivity_connector_skoda-0.1a11}/LICENSE +0 -0
  17. {carconnectivity_connector_skoda-0.1a9 → carconnectivity_connector_skoda-0.1a11}/Makefile +0 -0
  18. {carconnectivity_connector_skoda-0.1a9 → carconnectivity_connector_skoda-0.1a11}/README.md +0 -0
  19. {carconnectivity_connector_skoda-0.1a9 → carconnectivity_connector_skoda-0.1a11}/doc/Config.md +0 -0
  20. {carconnectivity_connector_skoda-0.1a9 → carconnectivity_connector_skoda-0.1a11}/pyproject.toml +0 -0
  21. {carconnectivity_connector_skoda-0.1a9 → carconnectivity_connector_skoda-0.1a11}/setup.cfg +0 -0
  22. {carconnectivity_connector_skoda-0.1a9 → carconnectivity_connector_skoda-0.1a11}/src/carconnectivity_connector_skoda.egg-info/SOURCES.txt +0 -0
  23. {carconnectivity_connector_skoda-0.1a9 → carconnectivity_connector_skoda-0.1a11}/src/carconnectivity_connector_skoda.egg-info/dependency_links.txt +0 -0
  24. {carconnectivity_connector_skoda-0.1a9 → carconnectivity_connector_skoda-0.1a11}/src/carconnectivity_connector_skoda.egg-info/requires.txt +0 -0
  25. {carconnectivity_connector_skoda-0.1a9 → carconnectivity_connector_skoda-0.1a11}/src/carconnectivity_connector_skoda.egg-info/top_level.txt +0 -0
  26. {carconnectivity_connector_skoda-0.1a9 → carconnectivity_connector_skoda-0.1a11}/src/carconnectivity_connectors/skoda/__init__.py +0 -0
  27. {carconnectivity_connector_skoda-0.1a9 → carconnectivity_connector_skoda-0.1a11}/src/carconnectivity_connectors/skoda/auth/__init__.py +0 -0
  28. {carconnectivity_connector_skoda-0.1a9 → carconnectivity_connector_skoda-0.1a11}/src/carconnectivity_connectors/skoda/auth/auth_util.py +0 -0
  29. {carconnectivity_connector_skoda-0.1a9 → carconnectivity_connector_skoda-0.1a11}/src/carconnectivity_connectors/skoda/auth/helpers/blacklist_retry.py +0 -0
  30. {carconnectivity_connector_skoda-0.1a9 → carconnectivity_connector_skoda-0.1a11}/src/carconnectivity_connectors/skoda/auth/my_skoda_session.py +0 -0
  31. {carconnectivity_connector_skoda-0.1a9 → carconnectivity_connector_skoda-0.1a11}/src/carconnectivity_connectors/skoda/auth/session_manager.py +0 -0
  32. {carconnectivity_connector_skoda-0.1a9 → carconnectivity_connector_skoda-0.1a11}/src/carconnectivity_connectors/skoda/auth/skoda_web_session.py +0 -0
  33. {carconnectivity_connector_skoda-0.1a9 → carconnectivity_connector_skoda-0.1a11}/src/carconnectivity_connectors/skoda/capability.py +0 -0
  34. {carconnectivity_connector_skoda-0.1a9 → carconnectivity_connector_skoda-0.1a11}/src/carconnectivity_connectors/skoda/charging.py +0 -0
  35. {carconnectivity_connector_skoda-0.1a9 → carconnectivity_connector_skoda-0.1a11}/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.1a9
3
+ Version: 0.1a11
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.1a9
3
+ Version: 0.1a11
4
4
  Summary: CarConnectivity connector for Skoda services
5
5
  Author: Till Steinbach
6
6
  License: MIT License
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '0.1a9'
15
+ __version__ = version = '0.1a11'
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],
@@ -14,7 +14,7 @@ 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
@@ -22,6 +22,7 @@ from carconnectivity.drive import GenericDrive, ElectricDrive, CombustionDrive
22
22
  from carconnectivity.attributes import BooleanAttribute, DurationAttribute
23
23
  from carconnectivity.charging import Charging
24
24
  from carconnectivity.position import Position
25
+ from carconnectivity.climatization import Climatization
25
26
 
26
27
  from carconnectivity_connectors.base.connector import BaseConnector
27
28
  from carconnectivity_connectors.skoda.auth.session_manager import SessionManager, SessionUser, Service
@@ -303,11 +304,15 @@ class Connector(BaseConnector):
303
304
  if vehicle_to_update is not None and isinstance(vehicle_to_update, SkodaVehicle) and vehicle_to_update.is_managed_by_connector(self):
304
305
  vehicle_to_update = self.fetch_vehicle_status_second_api(vehicle_to_update)
305
306
  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)
309
-
310
- def fetch_charging(self, vehicle: SkodaElectricVehicle) -> SkodaElectricVehicle:
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)
314
+
315
+ def fetch_charging(self, vehicle: SkodaElectricVehicle, no_cache: bool = False) -> SkodaElectricVehicle:
311
316
  """
312
317
  Fetches the charging information for a given Skoda electric vehicle.
313
318
 
@@ -324,7 +329,7 @@ class Connector(BaseConnector):
324
329
  if vehicle.charging is None:
325
330
  raise ValueError('Vehicle has no charging object')
326
331
  url = f'https://mysmob.api.connect.skoda-auto.cz/api/v1/charging/{vin}'
327
- data: Dict[str, Any] | None = self._fetch_data(url, session=self.session)
332
+ data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
328
333
  if data is not None:
329
334
  if 'carCapturedTimestamp' in data and data['carCapturedTimestamp'] is not None:
330
335
  captured_at: datetime = robust_time_parse(data['carCapturedTimestamp'])
@@ -355,10 +360,11 @@ class Connector(BaseConnector):
355
360
  vehicle.charging.power._set_value(None, measured=captured_at, unit=Power.KW) # pylint: disable=protected-access
356
361
  if 'remainingTimeToFullyChargedInMinutes' in data['status'] and data['status']['remainingTimeToFullyChargedInMinutes'] is not None:
357
362
  remaining_duration: timedelta = timedelta(minutes=data['status']['remainingTimeToFullyChargedInMinutes'])
363
+ estimated_date_reached: datetime = captured_at + remaining_duration
358
364
  # pylint: disable-next=protected-access
359
- 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)
360
366
  else:
361
- 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
362
368
  log_extra_keys(LOG_API, 'status', data['status'], {'chargingRateInKilometersPerHour',
363
369
  'chargePowerInKw',
364
370
  'remainingTimeToFullyChargedInMinutes',
@@ -366,7 +372,7 @@ class Connector(BaseConnector):
366
372
  log_extra_keys(LOG_API, 'charging data', data, {'carCapturedTimestamp', 'status'})
367
373
  return vehicle
368
374
 
369
- def fetch_position(self, vehicle: SkodaVehicle) -> SkodaVehicle:
375
+ def fetch_position(self, vehicle: SkodaVehicle, no_cache: bool = False) -> SkodaVehicle:
370
376
  """
371
377
  Fetches the position of the given Skoda vehicle and updates its position attributes.
372
378
 
@@ -386,7 +392,7 @@ class Connector(BaseConnector):
386
392
  if vehicle.position is None:
387
393
  raise ValueError('Vehicle has no charging object')
388
394
  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)
395
+ data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
390
396
  if data is not None:
391
397
  if 'positions' in data and data['positions'] is not None:
392
398
  for position_dict in data['positions']:
@@ -420,7 +426,110 @@ class Connector(BaseConnector):
420
426
  vehicle.position.position_type._set_value(None) # pylint: disable=protected-access
421
427
  return vehicle
422
428
 
423
- def fetch_vehicle_details(self, vehicle: SkodaVehicle) -> SkodaVehicle:
429
+ def fetch_air_conditioning(self, vehicle: SkodaVehicle, no_cache: bool = False) -> SkodaVehicle:
430
+ """
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=url, session=self.session, no_cache=no_cache)
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, no_cache: bool = False) -> SkodaVehicle:
424
533
  """
425
534
  Fetches the details of a vehicle from the Skoda API.
426
535
 
@@ -435,7 +544,7 @@ class Connector(BaseConnector):
435
544
  raise APIError('VIN is missing')
436
545
  url = f'https://mysmob.api.connect.skoda-auto.cz/api/v2/garage/vehicles/{vin}?' \
437
546
  'connectivityGenerations=MOD1&connectivityGenerations=MOD2&connectivityGenerations=MOD3&connectivityGenerations=MOD4'
438
- vehicle_data: Dict[str, Any] | None = self._fetch_data(url, self.session)
547
+ vehicle_data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
439
548
  if vehicle_data:
440
549
  if 'softwareVersion' in vehicle_data and vehicle_data['softwareVersion'] is not None:
441
550
  vehicle.software.version._set_value(vehicle_data['softwareVersion']) # pylint: disable=protected-access
@@ -473,7 +582,7 @@ class Connector(BaseConnector):
473
582
  log_extra_keys(LOG_API, 'api/v2/garage/vehicles/VIN', vehicle_data, {'softwareVersion'})
474
583
  return vehicle
475
584
 
476
- def fetch_driving_range(self, vehicle: SkodaVehicle) -> SkodaVehicle:
585
+ def fetch_driving_range(self, vehicle: SkodaVehicle, no_cache: bool = False) -> SkodaVehicle:
477
586
  """
478
587
  Fetches the driving range data for a given Skoda vehicle and updates the vehicle object accordingly.
479
588
 
@@ -496,7 +605,7 @@ class Connector(BaseConnector):
496
605
  if vin is None:
497
606
  raise APIError('VIN is missing')
498
607
  url = f'https://mysmob.api.connect.skoda-auto.cz/api/v2/vehicle-status/{vin}/driving-range'
499
- range_data: Dict[str, Any] | None = self._fetch_data(url, self.session)
608
+ range_data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
500
609
  if range_data:
501
610
  captured_at: datetime = robust_time_parse(range_data['carCapturedTimestamp'])
502
611
  # Check vehicle type and if it does not match the current vehicle type, create a new vehicle object using copy constructor
@@ -583,7 +692,7 @@ class Connector(BaseConnector):
583
692
  'secondaryEngineRange'})
584
693
  return vehicle
585
694
 
586
- def fetch_vehicle_status_second_api(self, vehicle: SkodaVehicle) -> SkodaVehicle:
695
+ def fetch_vehicle_status_second_api(self, vehicle: SkodaVehicle, no_cache: bool = False) -> SkodaVehicle:
587
696
  """
588
697
  Fetches the status of a vehicle from other Skoda API.
589
698
 
@@ -597,7 +706,7 @@ class Connector(BaseConnector):
597
706
  if vin is None:
598
707
  raise APIError('VIN is missing')
599
708
  url = f'https://api.connect.skoda-auto.cz/api/v2/vehicle-status/{vin}'
600
- vehicle_status_data: Dict[str, Any] | None = self._fetch_data(url, self.session)
709
+ vehicle_status_data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
601
710
  if vehicle_status_data:
602
711
  if 'remote' in vehicle_status_data and vehicle_status_data['remote'] is not None:
603
712
  vehicle_status_data = vehicle_status_data['remote']
@@ -772,10 +881,11 @@ class Connector(BaseConnector):
772
881
  """
773
882
  self._elapsed.append(elapsed)
774
883
 
775
- def _fetch_data(self, url, session, force=False, allow_empty=False, allow_http_error=False, allowed_errors=None) -> Optional[Dict[str, Any]]: # noqa: C901
884
+ def _fetch_data(self, url, session, no_cache=False, allow_empty=False, allow_http_error=False,
885
+ allowed_errors=None) -> Optional[Dict[str, Any]]: # noqa: C901
776
886
  data: Optional[Dict[str, Any]] = None
777
887
  cache_date: Optional[datetime] = None
778
- if not force and (self.max_age is not None and session.cache is not None and url in session.cache):
888
+ if not no_cache and (self.max_age is not None and session.cache is not None and url in session.cache):
779
889
  data, cache_date_string = session.cache[url]
780
890
  cache_date = datetime.fromisoformat(cache_date_string)
781
891
  if data is None or self.max_age is None \
@@ -7,6 +7,7 @@ import logging
7
7
  import uuid
8
8
  import ssl
9
9
  import json
10
+ import threading
10
11
  from datetime import timedelta, timezone
11
12
 
12
13
  from paho.mqtt.client import Client
@@ -20,7 +21,7 @@ from carconnectivity.drive import ElectricDrive
20
21
  from carconnectivity.util import robust_time_parse, log_extra_keys
21
22
  from carconnectivity.charging import Charging
22
23
 
23
- from carconnectivity_connectors.skoda.vehicle import SkodaElectricVehicle
24
+ from carconnectivity_connectors.skoda.vehicle import SkodaVehicle, SkodaElectricVehicle
24
25
  from carconnectivity_connectors.skoda.charging import SkodaCharging, mapping_skoda_charging_state
25
26
 
26
27
 
@@ -57,6 +58,8 @@ class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
57
58
  self.on_subscribe = self._on_subscribe_callback
58
59
  self.subscribed_topics: Set[str] = set()
59
60
 
61
+ self.delayed_access_function_timers: Dict[str, threading.Timer] = {}
62
+
60
63
  self.tls_set(cert_reqs=ssl.CERT_NONE)
61
64
 
62
65
  def connect(self, *args, **kwargs) -> MQTTErrorCode:
@@ -437,7 +440,7 @@ class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
437
440
  return
438
441
 
439
442
  # service_events
440
- match = re.match(r'^(?P<user_id>[0-9a-fA-F-]+)/(?P<vin>[A-Z0-9]+)/service-event/(?P<service_event>\w+)$', msg.topic)
443
+ 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)
441
444
  if match:
442
445
  user_id: str = match.group('user_id')
443
446
  vin: str = match.group('vin')
@@ -481,17 +484,18 @@ class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
481
484
  # If charging state changed, fetch charging again
482
485
  if old_charging_state != charging_state:
483
486
  try:
484
- self._skoda_connector.fetch_charging(vehicle)
487
+ self._skoda_connector.fetch_charging(vehicle, no_cache=True)
485
488
  except CarConnectivityError as e:
486
489
  LOG.error('Error while fetching charging: %s', e)
487
490
  if 'timeToFinish' in data['data'] and data['data']['timeToFinish'] is not None \
488
491
  and vehicle.charging is not None:
489
492
  try:
490
493
  remaining_duration: Optional[timedelta] = timedelta(minutes=int(data['data']['timeToFinish']))
494
+ estimated_date_reached: Optional[datetime] = measured_at + remaining_duration
491
495
  except ValueError:
492
- remaining_duration: Optional[timedelta] = None
496
+ estimated_date_reached: Optional[datetime] = None
493
497
  # pylint: disable-next=protected-access
494
- vehicle.charging.remaining_duration._set_value(measured=measured_at, value=remaining_duration)
498
+ vehicle.charging.estimated_date_reached._set_value(measured=measured_at, value=estimated_date_reached)
495
499
  log_extra_keys(LOG_API, 'data', data['data'], {'vin', 'userId', 'soc', 'chargedRange', 'timeToFinish', 'state'})
496
500
  LOG.debug('Received %s event for vehicle %s from user %s', data['name'], vin, user_id)
497
501
  return
@@ -500,6 +504,85 @@ class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
500
504
  LOG_API.info('Received event name %s service event %s for vehicle %s from user %s: %s', data['name'],
501
505
  service_event, vin, user_id, msg.payload)
502
506
  return
507
+ elif service_event == 'air-conditioning':
508
+ if 'name' in data and data['name'] == 'change-remaining-time':
509
+ if 'data' in data and data['data'] is not None:
510
+ vehicle: Optional[GenericVehicle] = self._skoda_connector.car_connectivity.garage.get_vehicle(vin)
511
+ if isinstance(vehicle, SkodaVehicle):
512
+ try:
513
+ self._skoda_connector.fetch_air_conditioning(vehicle, no_cache=True)
514
+ except CarConnectivityError as e:
515
+ LOG.error('Error while fetching charging: %s', e)
516
+ LOG_API.info('Received event name %s service event %s for vehicle %s from user %s: %s', data['name'],
517
+ service_event, vin, user_id, msg.payload)
518
+ return
519
+ elif service_event == 'vehicle-status/access':
520
+ if 'name' in data and data['name'] == 'change-access':
521
+ if 'data' in data and data['data'] is not None:
522
+ vehicle: Optional[GenericVehicle] = self._skoda_connector.car_connectivity.garage.get_vehicle(vin)
523
+ if isinstance(vehicle, SkodaVehicle):
524
+ def delayed_access_function(vehicle: SkodaVehicle):
525
+ """
526
+ Function to be executed after a delay of two seconds.
527
+ """
528
+ vin = vehicle.id
529
+ self.delayed_access_function_timers.pop(vin)
530
+ if vehicle.capabilities is not None and vehicle.capabilities.enabled \
531
+ and vehicle.capabilities.has_capability('CHARGING') and isinstance(vehicle, SkodaElectricVehicle):
532
+ try:
533
+ self._skoda_connector.fetch_charging(vehicle, no_cache=True)
534
+ except CarConnectivityError as e:
535
+ LOG.error('Error while fetching charging: %s', e)
536
+ if vehicle.capabilities is not None and vehicle.capabilities.enabled \
537
+ and vehicle.capabilities.has_capability('PARKING_POSITION'):
538
+ try:
539
+ self._skoda_connector.fetch_position(vehicle, no_cache=True)
540
+ except CarConnectivityError as e:
541
+ LOG.error('Error while fetching position: %s', e)
542
+ if vehicle.capabilities is not None and vehicle.capabilities.enabled \
543
+ and vehicle.capabilities.has_capability('AIR_CONDITIONING'):
544
+ try:
545
+ self._skoda_connector.fetch_air_conditioning(vehicle, no_cache=True)
546
+ except CarConnectivityError as e:
547
+ LOG.error('Error while fetching air conditioning: %s', e)
548
+ try:
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)
556
+
557
+ if vin in self.delayed_access_function_timers:
558
+ self.delayed_access_function_timers[vin].cancel()
559
+ self.delayed_access_function_timers[vin] = threading.Timer(2.0, delayed_access_function, kwargs={'vehicle': vehicle})
560
+ self.delayed_access_function_timers[vin].start()
561
+
562
+ LOG_API.info('Received event name %s service event %s for vehicle %s from user %s: %s', data['name'],
563
+ service_event, vin, user_id, msg.payload)
564
+ return
503
565
  LOG_API.info('Received unknown service event %s for vehicle %s from user %s: %s', service_event, vin, user_id, msg.payload)
504
566
  return
567
+ # service_events
568
+ 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
+ if match:
570
+ user_id: str = match.group('user_id')
571
+ vin: str = match.group('vin')
572
+ operation_request: str = match.group('operation_request')
573
+ data: Dict[str, Any] = json.loads(msg.payload)
574
+ if data is not None:
575
+ if operation_request == 'air-conditioning/start-stop-air-conditioning':
576
+ vehicle: Optional[GenericVehicle] = self._skoda_connector.car_connectivity.garage.get_vehicle(vin)
577
+ if isinstance(vehicle, SkodaVehicle):
578
+ if 'status' in data and data['status'] is not None:
579
+ if data['status'] == 'COMPLETED_SUCCESS':
580
+ LOG.debug('Received %s operation request for vehicle %s from user %s', operation_request, vin, user_id)
581
+ try:
582
+ self._skoda_connector.fetch_air_conditioning(vehicle, no_cache=True)
583
+ except CarConnectivityError as e:
584
+ LOG.error('Error while fetching air-conditioning: %s', e)
585
+ return
586
+ LOG_API.info('Received unknown operation request %s for vehicle %s from user %s: %s', operation_request, vin, user_id, msg.payload)
587
+ return
505
588
  LOG_API.info('I don\'t understand message %s: %s', msg.topic, msg.payload)