carconnectivity-connector-skoda 0.1a5__py3-none-any.whl → 0.1a7__py3-none-any.whl

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

Potentially problematic release.


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

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: carconnectivity-connector-skoda
3
- Version: 0.1a5
3
+ Version: 0.1a7
4
4
  Summary: CarConnectivity connector for Skoda services
5
5
  Author: Till Steinbach
6
6
  License: MIT License
@@ -1,18 +1,18 @@
1
1
  carconnectivity_connectors/skoda/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- carconnectivity_connectors/skoda/_version.py,sha256=esoDEDYz4YIDGl5EqkY9II8SqHmik1MTCaJ8ITyiktI,408
2
+ carconnectivity_connectors/skoda/_version.py,sha256=MnQaOHoAdWTpzV0qGoSHh2fAR79T8Wl9rWwKuQBvlj8,408
3
3
  carconnectivity_connectors/skoda/capability.py,sha256=JlNEaisVYF8qWv0wNDHTaas36uIpTIQ3NVR69wesiYQ,4513
4
- carconnectivity_connectors/skoda/connector.py,sha256=uAf1WcSQ68fkMF_R2zPjLP5vJYjk1QzGcg-ZdX4mPZo,42473
5
- carconnectivity_connectors/skoda/mqtt_client.py,sha256=7Hn-TqBl7VsN7e7DoPLvXZQ2UsSaOl1P0bMhbQQPX7k,18933
4
+ carconnectivity_connectors/skoda/connector.py,sha256=MTAVR9ysRBoEvOvPyXw75HALouLeMvRhAvC1lbvPBvI,45851
5
+ carconnectivity_connectors/skoda/mqtt_client.py,sha256=ENAsoMrVbROKxKnbc2-Bub8iAoxJk0LF4S3oKabmQII,22542
6
6
  carconnectivity_connectors/skoda/vehicle.py,sha256=H3GRDNimMghFwFi--y9BsgoSK3pMibNf_l6SsDN6gvQ,2759
7
7
  carconnectivity_connectors/skoda/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  carconnectivity_connectors/skoda/auth/auth_util.py,sha256=dGLUbUre0HBsTg_Ii5vW34f8DLrCykYJYCyzEvUBBEE,4434
9
- carconnectivity_connectors/skoda/auth/my_skoda_session.py,sha256=UwETob0JpDMolf29h0FZgZMtKpw9i57qetiZj4sdR3w,10395
9
+ carconnectivity_connectors/skoda/auth/my_skoda_session.py,sha256=lSh23SFJs8opjmPwHTv-KNIKDep_WY4aItSP4Zq7bT8,10396
10
10
  carconnectivity_connectors/skoda/auth/openid_session.py,sha256=PLWSSKw9Dg7hBbhzJ_nEycNrqiG6GiEM15h2wduL8jI,16592
11
11
  carconnectivity_connectors/skoda/auth/session_manager.py,sha256=Uf1vujuDBYUCAXhYToOsZkgbTtfmY3Qe0ICTfwomBpI,2899
12
12
  carconnectivity_connectors/skoda/auth/skoda_web_session.py,sha256=cjzMkzx473Sh-4RgZAQULeRRcxB1MboddldCVM_y5LE,10640
13
13
  carconnectivity_connectors/skoda/auth/helpers/blacklist_retry.py,sha256=f3wsiY5bpHDBxp7Va1Mv9nKJ4u3qnCHZZmDu78_AhMk,1251
14
- carconnectivity_connector_skoda-0.1a5.dist-info/LICENSE,sha256=PIwI1alwDyOfvEQHdGCm2u9uf_mGE8030xZDfun0xTo,1071
15
- carconnectivity_connector_skoda-0.1a5.dist-info/METADATA,sha256=bmcWfKYA5sYKG2WGsINKuKehGOF4ZVx2HCTedWfTGqQ,5326
16
- carconnectivity_connector_skoda-0.1a5.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
17
- carconnectivity_connector_skoda-0.1a5.dist-info/top_level.txt,sha256=KqA8GviZsDH4PtmnwSQsz0HB_w-TWkeEHLIRNo5dTaI,27
18
- carconnectivity_connector_skoda-0.1a5.dist-info/RECORD,,
14
+ carconnectivity_connector_skoda-0.1a7.dist-info/LICENSE,sha256=PIwI1alwDyOfvEQHdGCm2u9uf_mGE8030xZDfun0xTo,1071
15
+ carconnectivity_connector_skoda-0.1a7.dist-info/METADATA,sha256=MrhI9oLsUCPdUXlyAm5irYr7351ViguoW2N6LH-2irI,5326
16
+ carconnectivity_connector_skoda-0.1a7.dist-info/WHEEL,sha256=A3WOREP4zgxI0fKrHUG8DC8013e3dK3n7a6HDbcEIwE,91
17
+ carconnectivity_connector_skoda-0.1a7.dist-info/top_level.txt,sha256=KqA8GviZsDH4PtmnwSQsz0HB_w-TWkeEHLIRNo5dTaI,27
18
+ carconnectivity_connector_skoda-0.1a7.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.6.0)
2
+ Generator: setuptools (75.7.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '0.1a5'
15
+ __version__ = version = '0.1a7'
16
16
  __version_tuple__ = version_tuple = (0, 1)
@@ -146,7 +146,7 @@ class MySkodaSession(SkodaWebSession):
146
146
  if 'refreshToken' in token:
147
147
  found_tokens.add('refreshToken')
148
148
  token['refresh_token'] = token.pop('refreshToken')
149
- LOG.info(f'Found tokens in answer: {found_tokens}')
149
+ LOG.debug(f'Found tokens in answer: {found_tokens}')
150
150
  # generate json from fixed dict
151
151
  fixed_token_response = to_unicode(json.dumps(token)).encode("utf-8")
152
152
  # Let OAuthlib parse the token
@@ -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
17
+ from carconnectivity.units import Length, Speed, Power
18
18
  from carconnectivity.doors import Doors
19
19
  from carconnectivity.windows import Windows
20
20
  from carconnectivity.lights import Lights
@@ -267,7 +267,9 @@ class Connector(BaseConnector):
267
267
  vehicle.license_plate._set_value(None) # pylint: disable=protected-access
268
268
 
269
269
  log_extra_keys(LOG_API, 'vehicles', vehicle_dict, {'vin', 'licensePlate'})
270
- self.fetch_vehicle_status(vehicle)
270
+ vehicle = self.fetch_vehicle_status(vehicle)
271
+ if isinstance(vehicle, SkodaElectricVehicle):
272
+ vehicle = self.fetch_charging(vehicle)
271
273
  else:
272
274
  raise APIError('Could not parse vehicle, vin missing')
273
275
  for vin in set(garage.list_vehicle_vins()) - seen_vehicle_vins:
@@ -275,7 +277,53 @@ class Connector(BaseConnector):
275
277
  if vehicle_to_remove is not None and vehicle_to_remove.is_managed_by_connector(self):
276
278
  garage.remove_vehicle(vin)
277
279
 
278
- def fetch_vehicle_status(self, vehicle: SkodaVehicle) -> None:
280
+ def fetch_charging(self, vehicle: SkodaElectricVehicle) -> SkodaElectricVehicle:
281
+ """
282
+ Fetches the charging information for a given Skoda electric vehicle.
283
+
284
+ Args:
285
+ vehicle (SkodaElectricVehicle): The Skoda electric vehicle object.
286
+
287
+ Raises:
288
+ APIError: If the VIN is missing or if the carCapturedTimestamp is missing in the response data.
289
+ ValueError: If the vehicle has no charging object.
290
+ """
291
+ vin = vehicle.vin.value
292
+ if vin is None:
293
+ raise APIError('VIN is missing')
294
+ if vehicle.charging is None:
295
+ raise ValueError('Vehicle has no charging object')
296
+ url = f'https://mysmob.api.connect.skoda-auto.cz/api/v1/charging/{vin}'
297
+ data: Dict[str, Any] | None = self._fetch_data(url, session=self.session)
298
+ if data is not None:
299
+ if 'carCapturedTimestamp' in data and data['carCapturedTimestamp'] is not None:
300
+ captured_at: datetime = robust_time_parse(data['carCapturedTimestamp'])
301
+ else:
302
+ raise APIError('Could not fetch charging, carCapturedTimestamp missing')
303
+ if 'status' in data and data['status'] is not None:
304
+ if 'chargingRateInKilometersPerHour' in data['status'] and data['status']['chargingRateInKilometersPerHour'] is not None:
305
+ # pylint: disable-next=protected-access
306
+ vehicle.charging.rate._set_value(value=data['status']['chargingRateInKilometersPerHour'], measured=captured_at, unit=Speed.KMH)
307
+ else:
308
+ vehicle.charging.rate._set_value(None, measured=captured_at, unit=Speed.KMH) # pylint: disable=protected-access
309
+ if 'chargePowerInKw' in data['status'] and data['status']['chargePowerInKw'] is not None:
310
+ # pylint: disable-next=protected-access
311
+ vehicle.charging.power._set_value(value=data['status']['chargePowerInKw'], measured=captured_at, unit=Power.KW)
312
+ else:
313
+ vehicle.charging.power._set_value(None, measured=captured_at, unit=Power.KW) # pylint: disable=protected-access
314
+ if 'remainingTimeToFullyChargedInMinutes' in data['status'] and data['status']['remainingTimeToFullyChargedInMinutes'] is not None:
315
+ remaining_duration: timedelta = timedelta(minutes=data['status']['remainingTimeToFullyChargedInMinutes'])
316
+ # pylint: disable-next=protected-access
317
+ vehicle.charging.remaining_duration._set_value(value=remaining_duration, measured=captured_at)
318
+ else:
319
+ vehicle.charging.remaining_duration._set_value(None, measured=captured_at) # pylint: disable=protected-access
320
+ log_extra_keys(LOG_API, 'status', data['status'], {'chargingRateInKilometersPerHour',
321
+ 'chargePowerInKw',
322
+ 'remainingTimeToFullyChargedInMinutes'})
323
+ log_extra_keys(LOG_API, 'charging data', data, {'carCapturedTimestamp', 'status'})
324
+ return vehicle
325
+
326
+ def fetch_vehicle_status(self, vehicle: SkodaVehicle) -> SkodaVehicle:
279
327
  """
280
328
  Fetches the status of a vehicle from the Skoda API.
281
329
 
@@ -579,6 +627,7 @@ class Connector(BaseConnector):
579
627
  vehicle.lights.lights = {}
580
628
  log_extra_keys(LOG_API, 'lights', vehicle_status_data['lights'], {'overallStatus', 'lightsStatus'})
581
629
  log_extra_keys(LOG_API, 'vehicles', vehicle_status_data, {'capturedAt', 'mileageInKm', 'status', 'doors', 'windows', 'lights'})
630
+ return vehicle
582
631
 
583
632
  def _record_elapsed(self, elapsed: timedelta) -> None:
584
633
  """
@@ -2,24 +2,31 @@
2
2
  from __future__ import annotations
3
3
  from typing import TYPE_CHECKING
4
4
 
5
+ import re
5
6
  import logging
6
7
  import uuid
7
8
  import ssl
9
+ import json
10
+ from datetime import timedelta
8
11
 
9
12
  from paho.mqtt.client import Client
10
13
  from paho.mqtt.enums import MQTTProtocolVersion, CallbackAPIVersion, MQTTErrorCode
11
14
 
12
15
  from carconnectivity.observable import Observable
13
- from carconnectivity.vehicle import GenericVehicle
16
+ from carconnectivity.vehicle import GenericVehicle, ElectricVehicle
17
+ from carconnectivity.drive import ElectricDrive
18
+ from carconnectivity.util import robust_time_parse, log_extra_keys
14
19
 
15
20
 
16
21
  if TYPE_CHECKING:
17
- from typing import Set
22
+ from typing import Set, Dict, Any, Optional
23
+ from datetime import datetime
18
24
 
19
25
  from carconnectivity_connectors.skoda.connector import Connector
20
26
 
21
27
 
22
28
  LOG: logging.Logger = logging.getLogger("carconnectivity.connectors.skoda.mqtt")
29
+ LOG_API: logging.Logger = logging.getLogger("carconnectivity.connectors.skoda-api-debug")
23
30
 
24
31
 
25
32
  class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
@@ -362,10 +369,14 @@ class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
362
369
  self._skoda_connector.connected._set_value(value=False) # pylint: disable=protected-access
363
370
  self._skoda_connector.car_connectivity.garage.remove_observer(observer=self._on_carconnectivity_vehicle_enabled)
364
371
 
372
+ self.subscribed_topics.clear()
373
+
365
374
  if reason_code == 0:
366
375
  LOG.info('Client successfully disconnected')
367
376
  elif reason_code == 4:
368
377
  LOG.info('Client successfully disconnected: %s', userdata)
378
+ elif reason_code == 128:
379
+ LOG.info('Client disconnected: Needs new access token, trying to reconnect')
369
380
  elif reason_code == 137:
370
381
  LOG.error('Client disconnected: Server busy')
371
382
  elif reason_code == 139:
@@ -373,7 +384,7 @@ class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
373
384
  elif reason_code == 160:
374
385
  LOG.error('Client disconnected: Maximum connect time')
375
386
  else:
376
- LOG.error('Client unexpectedly disconnected (%s), trying to reconnect', reason_code)
387
+ LOG.error('Client unexpectedly disconnected (%d: %s), trying to reconnect', reason_code, reason_code)
377
388
 
378
389
  def _on_subscribe_callback(self, mqttc, obj, mid, reason_codes, properties) -> None:
379
390
  """
@@ -418,5 +429,45 @@ class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
418
429
  """
419
430
  del mqttc # unused
420
431
  del obj # unused
421
- error_message = f'I don\'t understand message {msg.topic}: {msg.payload}'
422
- LOG.info(error_message)
432
+ if len(msg.payload) == 0:
433
+ LOG_API.debug('MQTT topic %s: ignoring empty message', msg.topic)
434
+ return
435
+
436
+ # 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)
438
+ if match:
439
+ user_id: str = match.group('user_id')
440
+ vin: str = match.group('vin')
441
+ service_event: str = match.group('service_event')
442
+ data: Dict[str, Any] = json.loads(msg.payload)
443
+ if data is not None:
444
+ if 'timestamp' in data and data['timestamp'] is not None:
445
+ measured_at: datetime = robust_time_parse(data['timestamp'])
446
+ else:
447
+ measured_at: datetime = datetime.now()
448
+ if service_event == 'charging':
449
+ if 'name' in data and data['name'] == 'change-charge-mode' or data['name'] == 'change-soc':
450
+ if 'data' in data and data['data'] is not None:
451
+ vehicle: Optional[GenericVehicle] = self._skoda_connector.car_connectivity.garage.get_vehicle(vin)
452
+ if isinstance(vehicle, ElectricVehicle):
453
+ electric_drive: ElectricDrive = vehicle.get_electric_drive()
454
+ if electric_drive is not None:
455
+ if 'soc' in data['data'] and data['data']['soc'] is not None:
456
+ electric_drive.level._set_value(measured=measured_at, value=data['data']['soc']) # pylint: disable=protected-access
457
+ if 'chargedRange' in data['data'] and data['data']['chargedRange'] is not None:
458
+ # pylint: disable-next=protected-access
459
+ electric_drive.range._set_value(measured=measured_at, value=data['data']['chargedRange'])
460
+ if 'timeToFinish' in data['data'] and data['data']['timeToFinish'] is not None \
461
+ and vehicle.charging is not None:
462
+ remaining_duration: timedelta = timedelta(minutes=data['data']['timeToFinish'])
463
+ # 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'})
466
+ LOG.debug('Received %s event for vehicle %s from user %s', data['name'], vin, user_id)
467
+ return
468
+ LOG_API.info('Received event name %s service event %s for vehicle %s from user %s: %s', data['name'],
469
+ service_event, vin, user_id, msg.payload)
470
+ return
471
+ LOG_API.info('Received unknown service event %s for vehicle %s from user %s: %s', service_event, vin, user_id, msg.payload)
472
+ return
473
+ LOG_API.info('I don\'t understand message %s: %s', msg.topic, msg.payload)