carconnectivity-connector-skoda 0.3__tar.gz → 0.4__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 (41) hide show
  1. {carconnectivity_connector_skoda-0.3 → carconnectivity_connector_skoda-0.4}/CHANGELOG.md +16 -1
  2. {carconnectivity_connector_skoda-0.3 → carconnectivity_connector_skoda-0.4}/PKG-INFO +2 -2
  3. {carconnectivity_connector_skoda-0.3 → carconnectivity_connector_skoda-0.4}/pyproject.toml +1 -1
  4. {carconnectivity_connector_skoda-0.3 → carconnectivity_connector_skoda-0.4}/src/carconnectivity_connector_skoda.egg-info/PKG-INFO +2 -2
  5. {carconnectivity_connector_skoda-0.3 → carconnectivity_connector_skoda-0.4}/src/carconnectivity_connector_skoda.egg-info/requires.txt +1 -1
  6. {carconnectivity_connector_skoda-0.3 → carconnectivity_connector_skoda-0.4}/src/carconnectivity_connectors/skoda/_version.py +9 -4
  7. {carconnectivity_connector_skoda-0.3 → carconnectivity_connector_skoda-0.4}/src/carconnectivity_connectors/skoda/auth/skoda_web_session.py +2 -0
  8. {carconnectivity_connector_skoda-0.3 → carconnectivity_connector_skoda-0.4}/src/carconnectivity_connectors/skoda/climatization.py +8 -6
  9. {carconnectivity_connector_skoda-0.3 → carconnectivity_connector_skoda-0.4}/src/carconnectivity_connectors/skoda/command_impl.py +4 -2
  10. {carconnectivity_connector_skoda-0.3 → carconnectivity_connector_skoda-0.4}/src/carconnectivity_connectors/skoda/connector.py +336 -84
  11. {carconnectivity_connector_skoda-0.3 → carconnectivity_connector_skoda-0.4}/src/carconnectivity_connectors/skoda/mqtt_client.py +14 -3
  12. {carconnectivity_connector_skoda-0.3 → carconnectivity_connector_skoda-0.4}/src/carconnectivity_connectors/skoda/vehicle.py +6 -0
  13. {carconnectivity_connector_skoda-0.3 → carconnectivity_connector_skoda-0.4}/.flake8 +0 -0
  14. {carconnectivity_connector_skoda-0.3 → carconnectivity_connector_skoda-0.4}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  15. {carconnectivity_connector_skoda-0.3 → carconnectivity_connector_skoda-0.4}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  16. {carconnectivity_connector_skoda-0.3 → carconnectivity_connector_skoda-0.4}/.github/dependabot.yml +0 -0
  17. {carconnectivity_connector_skoda-0.3 → carconnectivity_connector_skoda-0.4}/.github/workflows/build.yml +0 -0
  18. {carconnectivity_connector_skoda-0.3 → carconnectivity_connector_skoda-0.4}/.github/workflows/build_and_publish.yml +0 -0
  19. {carconnectivity_connector_skoda-0.3 → carconnectivity_connector_skoda-0.4}/.github/workflows/codeql-analysis.yml +0 -0
  20. {carconnectivity_connector_skoda-0.3 → carconnectivity_connector_skoda-0.4}/.gitignore +0 -0
  21. {carconnectivity_connector_skoda-0.3 → carconnectivity_connector_skoda-0.4}/LICENSE +0 -0
  22. {carconnectivity_connector_skoda-0.3 → carconnectivity_connector_skoda-0.4}/Makefile +0 -0
  23. {carconnectivity_connector_skoda-0.3 → carconnectivity_connector_skoda-0.4}/README.md +0 -0
  24. {carconnectivity_connector_skoda-0.3 → carconnectivity_connector_skoda-0.4}/doc/Config.md +0 -0
  25. {carconnectivity_connector_skoda-0.3 → carconnectivity_connector_skoda-0.4}/setup.cfg +0 -0
  26. {carconnectivity_connector_skoda-0.3 → carconnectivity_connector_skoda-0.4}/setup_requirements.txt +0 -0
  27. {carconnectivity_connector_skoda-0.3 → carconnectivity_connector_skoda-0.4}/src/carconnectivity_connector_skoda.egg-info/SOURCES.txt +0 -0
  28. {carconnectivity_connector_skoda-0.3 → carconnectivity_connector_skoda-0.4}/src/carconnectivity_connector_skoda.egg-info/dependency_links.txt +0 -0
  29. {carconnectivity_connector_skoda-0.3 → carconnectivity_connector_skoda-0.4}/src/carconnectivity_connector_skoda.egg-info/top_level.txt +0 -0
  30. {carconnectivity_connector_skoda-0.3 → carconnectivity_connector_skoda-0.4}/src/carconnectivity_connectors/skoda/__init__.py +0 -0
  31. {carconnectivity_connector_skoda-0.3 → carconnectivity_connector_skoda-0.4}/src/carconnectivity_connectors/skoda/auth/__init__.py +0 -0
  32. {carconnectivity_connector_skoda-0.3 → carconnectivity_connector_skoda-0.4}/src/carconnectivity_connectors/skoda/auth/auth_util.py +0 -0
  33. {carconnectivity_connector_skoda-0.3 → carconnectivity_connector_skoda-0.4}/src/carconnectivity_connectors/skoda/auth/helpers/blacklist_retry.py +0 -0
  34. {carconnectivity_connector_skoda-0.3 → carconnectivity_connector_skoda-0.4}/src/carconnectivity_connectors/skoda/auth/my_skoda_session.py +0 -0
  35. {carconnectivity_connector_skoda-0.3 → carconnectivity_connector_skoda-0.4}/src/carconnectivity_connectors/skoda/auth/openid_session.py +0 -0
  36. {carconnectivity_connector_skoda-0.3 → carconnectivity_connector_skoda-0.4}/src/carconnectivity_connectors/skoda/auth/session_manager.py +0 -0
  37. {carconnectivity_connector_skoda-0.3 → carconnectivity_connector_skoda-0.4}/src/carconnectivity_connectors/skoda/capability.py +0 -0
  38. {carconnectivity_connector_skoda-0.3 → carconnectivity_connector_skoda-0.4}/src/carconnectivity_connectors/skoda/charging.py +0 -0
  39. {carconnectivity_connector_skoda-0.3 → carconnectivity_connector_skoda-0.4}/src/carconnectivity_connectors/skoda/error.py +0 -0
  40. {carconnectivity_connector_skoda-0.3 → carconnectivity_connector_skoda-0.4}/src/carconnectivity_connectors/skoda/ui/connector_ui.py +0 -0
  41. {carconnectivity_connector_skoda-0.3 → carconnectivity_connector_skoda-0.4}/test/integration_test/carConnectivity.json +0 -0
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
5
5
  ## [Unreleased]
6
6
  - No unreleased changes so far
7
7
 
8
+ ## [0.4] - 2025-03-02
9
+ ### Added
10
+ - Added better feedback when consent is needed
11
+ - added better access to connection state
12
+ - added better access to health state
13
+ - added attribute for vehicle in_motion
14
+ - added possibility to online change interval
15
+ - added named threads for better debugging
16
+ - added vehcile connection state
17
+ - added global vehicle state
18
+ - added maintainance objects
19
+ - added checks for min/max values with climatization temperatures
20
+ - improved error handling with commands
21
+
8
22
  ## [0.3] - 2025-02-19
9
23
  ### Added
10
24
  - Added support for images
@@ -19,7 +33,8 @@ All notable changes to this project will be documented in this file.
19
33
  Initial release, let's go and give this to the public to try out...
20
34
  The API is not yet implemented completely but most functions already work
21
35
 
22
- [unreleased]: https://github.com/tillsteinbach/CarConnectivity-connector-skoda/compare/v0.3...HEAD
36
+ [unreleased]: https://github.com/tillsteinbach/CarConnectivity-connector-skoda/compare/v0.4...HEAD
37
+ [0.4]: https://github.com/tillsteinbach/CarConnectivity-connector-skoda/releases/tag/v0.4
23
38
  [0.3]: https://github.com/tillsteinbach/CarConnectivity-connector-skoda/releases/tag/v0.3
24
39
  [0.2]: https://github.com/tillsteinbach/CarConnectivity-connector-skoda/releases/tag/v0.2
25
40
  [0.1]: https://github.com/tillsteinbach/CarConnectivity-connector-skoda/releases/tag/v0.1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: carconnectivity-connector-skoda
3
- Version: 0.3
3
+ Version: 0.4
4
4
  Summary: CarConnectivity connector for Skoda services
5
5
  Author: Till Steinbach
6
6
  License: MIT License
@@ -37,7 +37,7 @@ Classifier: Topic :: Software Development :: Libraries
37
37
  Requires-Python: >=3.8
38
38
  Description-Content-Type: text/markdown
39
39
  License-File: LICENSE
40
- Requires-Dist: carconnectivity>=0.3
40
+ Requires-Dist: carconnectivity>=0.4
41
41
  Requires-Dist: oauthlib~=3.2.2
42
42
  Requires-Dist: requests~=2.32.3
43
43
  Requires-Dist: jwt~=1.3.1
@@ -14,7 +14,7 @@ authors = [
14
14
  { name = "Till Steinbach" }
15
15
  ]
16
16
  dependencies = [
17
- "carconnectivity>=0.3",
17
+ "carconnectivity>=0.4",
18
18
  "oauthlib~=3.2.2",
19
19
  "requests~=2.32.3",
20
20
  "jwt~=1.3.1",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: carconnectivity-connector-skoda
3
- Version: 0.3
3
+ Version: 0.4
4
4
  Summary: CarConnectivity connector for Skoda services
5
5
  Author: Till Steinbach
6
6
  License: MIT License
@@ -37,7 +37,7 @@ Classifier: Topic :: Software Development :: Libraries
37
37
  Requires-Python: >=3.8
38
38
  Description-Content-Type: text/markdown
39
39
  License-File: LICENSE
40
- Requires-Dist: carconnectivity>=0.3
40
+ Requires-Dist: carconnectivity>=0.4
41
41
  Requires-Dist: oauthlib~=3.2.2
42
42
  Requires-Dist: requests~=2.32.3
43
43
  Requires-Dist: jwt~=1.3.1
@@ -1,4 +1,4 @@
1
- carconnectivity>=0.3
1
+ carconnectivity>=0.4
2
2
  oauthlib~=3.2.2
3
3
  requests~=2.32.3
4
4
  jwt~=1.3.1
@@ -1,8 +1,13 @@
1
- # file generated by setuptools_scm
1
+ # file generated by setuptools-scm
2
2
  # don't change, don't track in version control
3
+
4
+ __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
5
+
3
6
  TYPE_CHECKING = False
4
7
  if TYPE_CHECKING:
5
- from typing import Tuple, Union
8
+ from typing import Tuple
9
+ from typing import Union
10
+
6
11
  VERSION_TUPLE = Tuple[Union[int, str], ...]
7
12
  else:
8
13
  VERSION_TUPLE = object
@@ -12,5 +17,5 @@ __version__: str
12
17
  __version_tuple__: VERSION_TUPLE
13
18
  version_tuple: VERSION_TUPLE
14
19
 
15
- __version__ = version = '0.3'
16
- __version_tuple__ = version_tuple = (0, 3)
20
+ __version__ = version = '0.4'
21
+ __version_tuple__ = version_tuple = (0, 4)
@@ -121,6 +121,8 @@ class SkodaWebSession(OpenIDSession):
121
121
  raise RetrievalError('Temporary server error during login')
122
122
 
123
123
  if 'Location' not in response.headers:
124
+ if 'consent' in url:
125
+ raise AuthenticationError('Could not find Location in headers, probably due to missing consent. Try visiting: ' + url)
124
126
  raise APICompatibilityError('Forwarding without Location in headers')
125
127
 
126
128
  url = response.headers['Location']
@@ -1,12 +1,12 @@
1
1
  """
2
- Module for charging for skoda vehicles.
2
+ Module for climatization for skoda vehicles.
3
3
  """
4
4
  from __future__ import annotations
5
5
  from typing import TYPE_CHECKING
6
6
 
7
7
  from carconnectivity.climatization import Climatization
8
8
  from carconnectivity.objects import GenericObject
9
- from carconnectivity.vehicle import ElectricVehicle
9
+ from carconnectivity.vehicle import GenericVehicle
10
10
 
11
11
  from carconnectivity_connectors.skoda.error import Error
12
12
 
@@ -21,13 +21,15 @@ class SkodaClimatization(Climatization): # pylint: disable=too-many-instance-at
21
21
  This class extends the Climatization class and includes an enumeration of various
22
22
  charging states specific to Skoda vehicles.
23
23
  """
24
- def __init__(self, vehicle: ElectricVehicle | None = None, origin: Optional[Climatization] = None) -> None:
24
+ def __init__(self, vehicle: GenericVehicle | None = None, origin: Optional[Climatization] = None) -> None:
25
25
  if origin is not None:
26
26
  super().__init__(origin=origin)
27
- self.settings: Climatization.Settings = SkodaClimatization.Settings(origin=origin.settings)
27
+ if not isinstance(self.settings, SkodaClimatization.Settings):
28
+ self.settings: Climatization.Settings = SkodaClimatization.Settings(parent=self, origin=origin.settings)
29
+ self.settings.parent = self
28
30
  else:
29
31
  super().__init__(vehicle=vehicle)
30
- self.settings: Climatization.Settings = SkodaClimatization.Settings(origin=self.settings)
32
+ self.settings: Climatization.Settings = SkodaClimatization.Settings(parent=self)
31
33
  self.errors: Dict[str, Error] = {}
32
34
 
33
35
  class Settings(Climatization.Settings):
@@ -36,6 +38,6 @@ class SkodaClimatization(Climatization): # pylint: disable=too-many-instance-at
36
38
  """
37
39
  def __init__(self, parent: Optional[GenericObject] = None, origin: Optional[Climatization.Settings] = None) -> None:
38
40
  if origin is not None:
39
- super().__init__(origin=origin)
41
+ super().__init__(parent=parent, origin=origin)
40
42
  else:
41
43
  super().__init__(parent=parent)
@@ -31,6 +31,8 @@ class SpinCommand(GenericCommand):
31
31
 
32
32
  @value.setter
33
33
  def value(self, new_value: Optional[Union[str, Dict]]) -> None:
34
+ # Execute early hooks before parsing the value
35
+ new_value = self._execute_on_set_hook(new_value, early_hook=True)
34
36
  if isinstance(new_value, str):
35
37
  parser = ThrowingArgumentParser(prog='', add_help=False, exit_on_error=False)
36
38
  parser.add_argument('command', help='Command to execute', type=SpinCommand.Command,
@@ -55,8 +57,8 @@ class SpinCommand(GenericCommand):
55
57
  raise ValueError('Invalid value for SpinCommand. '
56
58
  f'Command must be one of {SpinCommand.Command}')
57
59
  if self._is_changeable:
58
- for hook in self._on_set_hooks:
59
- new_value = hook(self, new_value)
60
+ # Execute late hooks before setting the value
61
+ new_value = self._execute_on_set_hook(new_value, early_hook=False)
60
62
  self._set_value(new_value)
61
63
  else:
62
64
  raise TypeError('You cannot set this attribute. Attribute is not mutable.')
@@ -4,6 +4,7 @@ from typing import TYPE_CHECKING
4
4
 
5
5
  import threading
6
6
  import os
7
+ import traceback
7
8
  import logging
8
9
  import netrc
9
10
  from datetime import datetime, timedelta, timezone
@@ -22,13 +23,14 @@ from carconnectivity.doors import Doors
22
23
  from carconnectivity.windows import Windows
23
24
  from carconnectivity.lights import Lights
24
25
  from carconnectivity.drive import GenericDrive, ElectricDrive, CombustionDrive
25
- from carconnectivity.attributes import BooleanAttribute, DurationAttribute, TemperatureAttribute
26
+ from carconnectivity.attributes import GenericAttribute, BooleanAttribute, DurationAttribute, TemperatureAttribute, EnumAttribute
26
27
  from carconnectivity.charging import Charging
27
28
  from carconnectivity.position import Position
28
29
  from carconnectivity.climatization import Climatization
29
30
  from carconnectivity.charging_connector import ChargingConnector
30
31
  from carconnectivity.commands import Commands
31
32
  from carconnectivity.command_impl import ClimatizationStartStopCommand, ChargingStartStopCommand, HonkAndFlashCommand, LockUnlockCommand, WakeSleepCommand
33
+ from carconnectivity.enums import ConnectionState
32
34
 
33
35
  from carconnectivity_connectors.base.connector import BaseConnector
34
36
  from carconnectivity_connectors.skoda.auth.session_manager import SessionManager, SessionUser, Service
@@ -79,8 +81,14 @@ class Connector(BaseConnector):
79
81
  self._background_connect_thread: Optional[threading.Thread] = None
80
82
  self._stop_event = threading.Event()
81
83
 
82
- self.connected: BooleanAttribute = BooleanAttribute(name="connected", parent=self, tags={'connector_custom'})
84
+ self.connection_state: EnumAttribute = EnumAttribute(name="connection_state", parent=self, value_type=ConnectionState,
85
+ value=ConnectionState.DISCONNECTED, tags={'connector_custom'})
86
+ self.rest_connected: bool = False
87
+ self.mqtt_connected: bool = False
83
88
  self.interval: DurationAttribute = DurationAttribute(name="interval", parent=self, tags={'connector_custom'})
89
+ self.interval.minimum = timedelta(seconds=180)
90
+ self.interval._is_changeable = True # pylint: disable=protected-access
91
+
84
92
  self.commands: Commands = Commands(parent=self)
85
93
 
86
94
  self.user_id: Optional[str] = None
@@ -154,12 +162,15 @@ class Connector(BaseConnector):
154
162
  self._stop_event.clear()
155
163
  # Start background thread for Rest API polling
156
164
  self._background_thread = threading.Thread(target=self._background_loop, daemon=False)
165
+ self._background_thread.name = 'carconnectivity.connectors.skoda-background'
157
166
  self._background_thread.start()
158
167
  # Start background thread for MQTT connection
159
168
  self._background_connect_thread = threading.Thread(target=self._background_connect_loop, daemon=False)
169
+ self._background_connect_thread.name = 'carconnectivity.connectors.skoda-background_connect'
160
170
  self._background_connect_thread.start()
161
171
  # Start MQTT thread
162
172
  self._mqtt_client.loop_start()
173
+ self.healthy._set_value(value=True) # pylint: disable=protected-access
163
174
 
164
175
  def _background_connect_loop(self) -> None:
165
176
  while not self._stop_event.is_set():
@@ -173,6 +184,7 @@ class Connector(BaseConnector):
173
184
  def _background_loop(self) -> None:
174
185
  self._stop_event.clear()
175
186
  fetch: bool = True
187
+ self.connection_state._set_value(value=ConnectionState.CONNECTING) # pylint: disable=protected-access
176
188
  while not self._stop_event.is_set():
177
189
  interval = 300
178
190
  try:
@@ -191,21 +203,43 @@ class Connector(BaseConnector):
191
203
  raise
192
204
  except TooManyRequestsError as err:
193
205
  LOG.error('Retrieval error during update. Too many requests from your account (%s). Will try again after 15 minutes', str(err))
206
+ self.connection_state._set_value(value=ConnectionState.ERROR) # pylint: disable=protected-access
207
+ self.rest_connected = False
194
208
  self._stop_event.wait(900)
195
209
  except RetrievalError as err:
196
210
  LOG.error('Retrieval error during update (%s). Will try again after configured interval of %ss', str(err), interval)
211
+ self.connection_state._set_value(value=ConnectionState.ERROR) # pylint: disable=protected-access
212
+ self.rest_connected = False
197
213
  self._stop_event.wait(interval)
198
214
  except APIError as err:
199
215
  LOG.error('API error during update (%s). Will try again after configured interval of %ss', str(err), interval)
216
+ self.connection_state._set_value(value=ConnectionState.ERROR) # pylint: disable=protected-access
217
+ self.rest_connected = False
200
218
  self._stop_event.wait(interval)
201
219
  except APICompatibilityError as err:
202
220
  LOG.error('API compatability error during update (%s). Will try again after configured interval of %ss', str(err), interval)
221
+ self.connection_state._set_value(value=ConnectionState.ERROR) # pylint: disable=protected-access
222
+ self.rest_connected = False
203
223
  self._stop_event.wait(interval)
204
224
  except TemporaryAuthenticationError as err:
205
225
  LOG.error('Temporary authentification error during update (%s). Will try again after configured interval of %ss', str(err), interval)
226
+ self.connection_state._set_value(value=ConnectionState.ERROR) # pylint: disable=protected-access
227
+ self.rest_connected = False
206
228
  self._stop_event.wait(interval)
229
+ except Exception as err:
230
+ LOG.critical('Critical error during update: %s', traceback.format_exc())
231
+ self.connection_state._set_value(value=ConnectionState.ERROR) # pylint: disable=protected-access
232
+ self.rest_connected = False
233
+ self.healthy._set_value(value=False) # pylint: disable=protected-access
234
+ raise err
207
235
  else:
236
+ self.rest_connected = True
237
+ if self.mqtt_connected:
238
+ self.connection_state._set_value(value=ConnectionState.CONNECTED) # pylint: disable=protected-access
208
239
  self._stop_event.wait(interval)
240
+ # When leaving the loop, set the connection state to disconnected
241
+ self.connection_state._set_value(value=ConnectionState.DISCONNECTED) # pylint: disable=protected-access
242
+ self.rest_connected = False
209
243
 
210
244
  def persist(self) -> None:
211
245
  """
@@ -236,12 +270,12 @@ class Connector(BaseConnector):
236
270
  self.car_connectivity.garage.remove_vehicle(vehicle.id)
237
271
  vehicle.enabled = False
238
272
  self._stop_event.set()
273
+ self.session.close()
239
274
  if self._background_thread is not None:
240
275
  self._background_thread.join()
241
276
  if self._background_connect_thread is not None:
242
277
  self._background_connect_thread.join()
243
278
  self.persist()
244
- self.session.close()
245
279
  return super().shutdown()
246
280
 
247
281
  def fetch_all(self) -> None:
@@ -339,12 +373,38 @@ class Connector(BaseConnector):
339
373
  vehicle_to_update = self.fetch_vehicle_status(vehicle_to_update)
340
374
  vehicle_to_update = self.fetch_driving_range(vehicle_to_update)
341
375
  if vehicle_to_update.capabilities is not None and vehicle_to_update.capabilities.enabled:
376
+ if vehicle_to_update.capabilities.has_capability('READINESS'):
377
+ vehicle_to_update = self.fetch_connection_status(vehicle_to_update)
342
378
  if vehicle_to_update.capabilities.has_capability('PARKING_POSITION'):
343
379
  vehicle_to_update = self.fetch_position(vehicle_to_update)
344
380
  if vehicle_to_update.capabilities.has_capability('CHARGING') and isinstance(vehicle_to_update, SkodaElectricVehicle):
345
381
  vehicle_to_update = self.fetch_charging(vehicle_to_update)
346
382
  if vehicle_to_update.capabilities.has_capability('AIR_CONDITIONING'):
347
383
  vehicle_to_update = self.fetch_air_conditioning(vehicle_to_update)
384
+ if vehicle_to_update.capabilities.has_capability('VEHICLE_HEALTH_INSPECTION'):
385
+ vehicle_to_update = self.fetch_maintenance(vehicle_to_update)
386
+ vehicle_to_update = self.decide_state(vehicle_to_update)
387
+ self.car_connectivity.transaction_end()
388
+
389
+ def decide_state(self, vehicle: SkodaVehicle) -> SkodaVehicle:
390
+ """
391
+ Decides the state of the vehicle based on the current data.
392
+
393
+ Args:
394
+ vehicle (SkodaVehicle): The Skoda vehicle object.
395
+
396
+ Returns:
397
+ SkodaVehicle: The Skoda vehicle object with the updated state.
398
+ """
399
+ if vehicle is not None:
400
+ if vehicle.in_motion is not None and vehicle.in_motion.enabled and vehicle.in_motion.value:
401
+ vehicle.state._set_value(GenericVehicle.State.IGNITION_ON) # pylint: disable=protected-access
402
+ elif vehicle.position is not None and vehicle.position.enabled and vehicle.position.position_type is not None \
403
+ and vehicle.position.position_type.enabled and vehicle.position.position_type.value == Position.PositionType.PARKING:
404
+ vehicle.state._set_value(GenericVehicle.State.PARKED) # pylint: disable=protected-access
405
+ else:
406
+ vehicle.state._set_value(None) # pylint: disable=protected-access
407
+ return vehicle
348
408
 
349
409
  def fetch_charging(self, vehicle: SkodaElectricVehicle, no_cache: bool = False) -> SkodaElectricVehicle:
350
410
  """
@@ -553,6 +613,41 @@ class Connector(BaseConnector):
553
613
  vehicle.charging.errors.clear()
554
614
  log_extra_keys(LOG_API, 'charging data', data, {'carCapturedTimestamp', 'status', 'isVehicleInSavedLocation', 'errors', 'settings'})
555
615
  return vehicle
616
+
617
+ def fetch_connection_status(self, vehicle: SkodaVehicle, no_cache: bool = False) -> SkodaVehicle:
618
+ """
619
+ Fetches the connection status of the given Skoda vehicle and updates its connection attributes.
620
+
621
+ Args:
622
+ vehicle (SkodaVehicle): The Skoda vehicle object containing the VIN and connection attributes.
623
+
624
+ Returns:
625
+ SkodaVehicle: The updated Skoda vehicle object with the fetched connection data.
626
+
627
+ Raises:
628
+ APIError: If the VIN is missing.
629
+ ValueError: If the vehicle has no connection object.
630
+ """
631
+ vin = vehicle.vin.value
632
+ if vin is None:
633
+ raise APIError('VIN is missing')
634
+ url = f'https://mysmob.api.connect.skoda-auto.cz/api/v2/connection-status/{vin}/readiness'
635
+ data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
636
+ # {'unreachable': False, 'inMotion': False, 'batteryProtectionLimitOn': False}
637
+ if data is not None:
638
+ if 'unreachable' in data and data['unreachable'] is not None:
639
+ if data['unreachable']:
640
+ vehicle.connection_state._set_value(vehicle.ConnectionState.OFFLINE) # pylint: disable=protected-access
641
+ else:
642
+ vehicle.connection_state._set_value(vehicle.ConnectionState.REACHABLE) # pylint: disable=protected-access
643
+ else:
644
+ vehicle.connection_state._set_value(None) # pylint: disable=protected-access
645
+ if 'inMotion' in data and data['inMotion'] is not None:
646
+ vehicle.in_motion._set_value(data['inMotion']) # pylint: disable=protected-access
647
+ else:
648
+ vehicle.in_motion._set_value(None) # pylint: disable=protected-access
649
+ log_extra_keys(LOG_API, 'connection status', data, {'unreachable'})
650
+ return vehicle
556
651
 
557
652
  def fetch_position(self, vehicle: SkodaVehicle, no_cache: bool = False) -> SkodaVehicle:
558
653
  """
@@ -574,7 +669,7 @@ class Connector(BaseConnector):
574
669
  if vehicle.position is None:
575
670
  raise ValueError('Vehicle has no charging object')
576
671
  url = f'https://mysmob.api.connect.skoda-auto.cz/api/v1/maps/positions?vin={vin}'
577
- data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
672
+ data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache, allow_empty=True)
578
673
  if data is not None:
579
674
  if 'positions' in data and data['positions'] is not None:
580
675
  for position_dict in data['positions']:
@@ -608,6 +703,53 @@ class Connector(BaseConnector):
608
703
  vehicle.position.position_type._set_value(None) # pylint: disable=protected-access
609
704
  return vehicle
610
705
 
706
+ def fetch_maintenance(self, vehicle: SkodaVehicle, no_cache: bool = False) -> SkodaVehicle:
707
+ """
708
+ Fetches the maintenance information for a given Skoda vehicle.
709
+
710
+ Args:
711
+ vehicle (SkodaVehicle): The vehicle object for which maintenance information is to be fetched.
712
+ no_cache (bool, optional): If True, bypasses the cache and fetches fresh data. Defaults to False.
713
+
714
+ Returns:
715
+ SkodaVehicle: The vehicle object with updated maintenance information.
716
+
717
+ Raises:
718
+ APIError: If the VIN is missing or if the 'capturedAt' field is missing in the fetched data.
719
+ ValueError: If the vehicle has no charging object.
720
+ """
721
+ vin = vehicle.vin.value
722
+ if vin is None:
723
+ raise APIError('VIN is missing')
724
+ if vehicle.position is None:
725
+ raise ValueError('Vehicle has no charging object')
726
+ url = f'https://mysmob.api.connect.skoda-auto.cz/api/v3/vehicle-maintenance/vehicles/{vin}/report'
727
+ data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
728
+ #{'capturedAt': '2025-02-24T19:54:32.728Z', 'inspectionDueInDays': 620, 'mileageInKm': 2512}
729
+ if data is not None:
730
+ if 'capturedAt' in data and data['capturedAt'] is not None:
731
+ captured_at: datetime = robust_time_parse(data['capturedAt'])
732
+ else:
733
+ raise APIError('Could not fetch maintenance, capturedAt missing')
734
+ if 'mileageInKm' in data and data['mileageInKm'] is not None:
735
+ vehicle.odometer._set_value(value=data['mileageInKm'], measured=captured_at, unit=Length.KM) # pylint: disable=protected-access
736
+ else:
737
+ vehicle.odometer._set_value(None) # pylint: disable=protected-access
738
+ if 'inspectionDueInDays' in data and data['inspectionDueInDays'] is not None:
739
+ inspection_due: timedelta = timedelta(days=data['inspectionDueInDays'])
740
+ inspection_date: datetime = captured_at + inspection_due
741
+ inspection_date = inspection_date.replace(hour=0, minute=0, second=0, microsecond=0)
742
+ # pylint: disable-next=protected-access
743
+ vehicle.maintenance.inspection_due_at._set_value(value=inspection_date, measured=captured_at)
744
+ else:
745
+ vehicle.maintenance.inspection_due_at._set_value(None) # pylint: disable=protected-access
746
+ log_extra_keys(LOG_API, 'maintenance', data, {'capturedAt', 'mileageInKm', 'inspectionDueInDays'})
747
+
748
+ #url = f'https://mysmob.api.connect.skoda-auto.cz/api/v1/vehicle-health-report/warning-lights/{vin}'
749
+ #data: Dict[str, Any] | None = self._fetch_data(url=url, session=self.session, no_cache=no_cache)
750
+ #{'capturedAt': '2025-02-24T15:32:35.032Z', 'mileageInKm': 2512, 'warningLights': [{'category': 'ASSISTANCE', 'defects': []}, {'category': 'COMFORT', 'defects': []}, {'category': 'BRAKE', 'defects': []}, {'category': 'ELECTRIC_ENGINE', 'defects': []}, {'category': 'LIGHTING', 'defects': []}, {'category': 'TIRE', 'defects': []}, {'category': 'OTHER', 'defects': []}]}
751
+ return vehicle
752
+
611
753
  def fetch_air_conditioning(self, vehicle: SkodaVehicle, no_cache: bool = False) -> SkodaVehicle:
612
754
  """
613
755
  Fetches the air conditioning data for a given Skoda vehicle and updates the vehicle object with the retrieved data.
@@ -667,12 +809,19 @@ class Connector(BaseConnector):
667
809
  # pylint: disable-next=protected-access
668
810
  vehicle.climatization.settings.target_temperature._add_on_set_hook(self.__on_air_conditioning_target_temperature_change)
669
811
  vehicle.climatization.settings.target_temperature._is_changeable = True # pylint: disable=protected-access
812
+ precision: float = 0.5
813
+ min_temperature: Optional[float] = None
814
+ max_temperature: Optional[float] = None
670
815
  unit: Temperature = Temperature.UNKNOWN
671
816
  if 'unitInCar' in data['targetTemperature'] and data['targetTemperature']['unitInCar'] is not None:
672
817
  if data['targetTemperature']['unitInCar'] == 'CELSIUS':
673
818
  unit = Temperature.C
819
+ min_temperature: Optional[float] = 16
820
+ max_temperature: Optional[float] = 29.5
674
821
  elif data['targetTemperature']['unitInCar'] == 'FAHRENHEIT':
675
822
  unit = Temperature.F
823
+ min_temperature: Optional[float] = 61
824
+ max_temperature: Optional[float] = 85
676
825
  elif data['targetTemperature']['unitInCar'] == 'KELVIN':
677
826
  unit = Temperature.K
678
827
  else:
@@ -682,6 +831,10 @@ class Connector(BaseConnector):
682
831
  vehicle.climatization.settings.target_temperature._set_value(value=data['targetTemperature']['temperatureValue'],
683
832
  measured=captured_at,
684
833
  unit=unit)
834
+ vehicle.climatization.settings.target_temperature.precision = precision
835
+ vehicle.climatization.settings.target_temperature.minimum = min_temperature
836
+ vehicle.climatization.settings.target_temperature.maximum = max_temperature
837
+
685
838
  else:
686
839
  # pylint: disable-next=protected-access
687
840
  vehicle.climatization.settings.target_temperature._set_value(value=None, measured=captured_at, unit=unit)
@@ -1221,6 +1374,8 @@ class Connector(BaseConnector):
1221
1374
  data = status_response.json()
1222
1375
  if session.cache is not None:
1223
1376
  session.cache[url] = (data, str(datetime.utcnow()))
1377
+ elif status_response.status_code == requests.codes['no_content'] and allow_empty:
1378
+ data = None
1224
1379
  elif status_response.status_code == requests.codes['too_many_requests']:
1225
1380
  raise TooManyRequestsError('Could not fetch data due to too many requests from your account. '
1226
1381
  f'Status Code was: {status_response.status_code}')
@@ -1287,10 +1442,20 @@ class Connector(BaseConnector):
1287
1442
  raise SetterError(f'Unknown temperature unit {temperature_attribute.unit}')
1288
1443
 
1289
1444
  url = f'https://mysmob.api.connect.skoda-auto.cz/api/v2/air-conditioning/{vin}/settings/target-temperature'
1290
- settings_response: requests.Response = self.session.post(url, data=json.dumps(setting_dict), allow_redirects=True)
1291
- if settings_response.status_code != requests.codes['accepted']:
1292
- LOG.error('Could not set target temperature (%s)', settings_response.status_code)
1293
- raise SetterError(f'Could not set value ({settings_response.status_code})')
1445
+ try:
1446
+ settings_response: requests.Response = self.session.post(url, data=json.dumps(setting_dict), allow_redirects=True)
1447
+ if settings_response.status_code != requests.codes['accepted']:
1448
+ LOG.error('Could not set target temperature (%s)', settings_response.status_code)
1449
+ raise SetterError(f'Could not set value ({settings_response.status_code})')
1450
+ except requests.exceptions.ConnectionError as connection_error:
1451
+ raise SetterError(f'Connection error: {connection_error}.'
1452
+ ' If this happens frequently, please check if other applications communicate with the Skoda server.') from connection_error
1453
+ except requests.exceptions.ChunkedEncodingError as chunked_encoding_error:
1454
+ raise SetterError(f'Error: {chunked_encoding_error}') from chunked_encoding_error
1455
+ except requests.exceptions.ReadTimeout as timeout_error:
1456
+ raise SetterError(f'Timeout during read: {timeout_error}') from timeout_error
1457
+ except requests.exceptions.RetryError as retry_error:
1458
+ raise SetterError(f'Retrying failed: {retry_error}') from retry_error
1294
1459
  return target_temperature
1295
1460
 
1296
1461
  def __on_air_conditioning_at_unlock_change(self, at_unlock_attribute: BooleanAttribute, at_unlock_value: bool) -> bool:
@@ -1306,10 +1471,20 @@ class Connector(BaseConnector):
1306
1471
  setting_dict['airConditioningAtUnlockEnabled'] = at_unlock_value
1307
1472
 
1308
1473
  url = f'https://mysmob.api.connect.skoda-auto.cz/api/v2/air-conditioning/{vin}/settings/ac-at-unlock'
1309
- settings_response: requests.Response = self.session.post(url, data=json.dumps(setting_dict), allow_redirects=True)
1310
- if settings_response.status_code != requests.codes['accepted']:
1311
- LOG.error('Could not set air conditioning at unlock (%s)', settings_response.status_code)
1312
- raise SetterError(f'Could not set value ({settings_response.status_code})')
1474
+ try:
1475
+ settings_response: requests.Response = self.session.post(url, data=json.dumps(setting_dict), allow_redirects=True)
1476
+ if settings_response.status_code != requests.codes['accepted']:
1477
+ LOG.error('Could not set air conditioning at unlock (%s)', settings_response.status_code)
1478
+ raise SetterError(f'Could not set value ({settings_response.status_code})')
1479
+ except requests.exceptions.ConnectionError as connection_error:
1480
+ raise SetterError(f'Connection error: {connection_error}.'
1481
+ ' If this happens frequently, please check if other applications communicate with the Skoda server.') from connection_error
1482
+ except requests.exceptions.ChunkedEncodingError as chunked_encoding_error:
1483
+ raise SetterError(f'Error: {chunked_encoding_error}') from chunked_encoding_error
1484
+ except requests.exceptions.ReadTimeout as timeout_error:
1485
+ raise SetterError(f'Timeout during read: {timeout_error}') from timeout_error
1486
+ except requests.exceptions.RetryError as retry_error:
1487
+ raise SetterError(f'Retrying failed: {retry_error}') from retry_error
1313
1488
  return at_unlock_value
1314
1489
 
1315
1490
  def __on_air_conditioning_window_heating_change(self, window_heating_attribute: BooleanAttribute, window_heating_value: bool) -> bool:
@@ -1325,10 +1500,20 @@ class Connector(BaseConnector):
1325
1500
  setting_dict['windowHeatingEnabled'] = window_heating_value
1326
1501
 
1327
1502
  url = f'https://mysmob.api.connect.skoda-auto.cz/api/v2/air-conditioning/{vin}/settings/ac-at-unlock'
1328
- settings_response: requests.Response = self.session.post(url, data=json.dumps(setting_dict), allow_redirects=True)
1329
- if settings_response.status_code != requests.codes['accepted']:
1330
- LOG.error('Could not set air conditioning window heating (%s)', settings_response.status_code)
1331
- raise SetterError(f'Could not set value ({settings_response.status_code})')
1503
+ try:
1504
+ settings_response: requests.Response = self.session.post(url, data=json.dumps(setting_dict), allow_redirects=True)
1505
+ if settings_response.status_code != requests.codes['accepted']:
1506
+ LOG.error('Could not set air conditioning window heating (%s)', settings_response.status_code)
1507
+ raise SetterError(f'Could not set value ({settings_response.status_code})')
1508
+ except requests.exceptions.ConnectionError as connection_error:
1509
+ raise SetterError(f'Connection error: {connection_error}.'
1510
+ ' If this happens frequently, please check if other applications communicate with the Skoda server.') from connection_error
1511
+ except requests.exceptions.ChunkedEncodingError as chunked_encoding_error:
1512
+ raise SetterError(f'Error: {chunked_encoding_error}') from chunked_encoding_error
1513
+ except requests.exceptions.ReadTimeout as timeout_error:
1514
+ raise SetterError(f'Timeout during read: {timeout_error}') from timeout_error
1515
+ except requests.exceptions.RetryError as retry_error:
1516
+ raise SetterError(f'Retrying failed: {retry_error}') from retry_error
1332
1517
  return window_heating_value
1333
1518
 
1334
1519
  def __on_air_conditioning_start_stop(self, start_stop_command: ClimatizationStartStopCommand, command_arguments: Union[str, Dict[str, Any]]) \
@@ -1345,53 +1530,66 @@ class Connector(BaseConnector):
1345
1530
  if 'command' not in command_arguments:
1346
1531
  raise CommandError('Command argument missing')
1347
1532
  command_dict = {}
1348
- if command_arguments['command'] == ClimatizationStartStopCommand.Command.START:
1349
- command_dict['heaterSource'] = 'ELECTRIC'
1350
- command_dict['targetTemperature'] = {}
1351
- if 'target_temperature' in command_arguments:
1352
- # Round target temperature to nearest 0.5
1353
- command_dict['targetTemperature']['temperatureValue'] = round(command_arguments['target_temperature'] * 2) / 2
1354
- if 'target_temperature_unit' in command_arguments:
1355
- if not isinstance(command_arguments['target_temperature_unit'], Temperature):
1356
- raise CommandError('Temperature unit is not of type Temperature')
1357
- if command_arguments['target_temperature_unit'] == Temperature.C:
1533
+ try:
1534
+ if command_arguments['command'] == ClimatizationStartStopCommand.Command.START:
1535
+ command_dict['heaterSource'] = 'ELECTRIC'
1536
+ command_dict['targetTemperature'] = {}
1537
+ precision: float = 0.5
1538
+ if 'target_temperature' in command_arguments:
1539
+ # Round target temperature to nearest 0.5
1540
+ command_dict['targetTemperature']['temperatureValue'] = round(command_arguments['target_temperature'] / precision) * precision
1541
+ if 'target_temperature_unit' in command_arguments:
1542
+ if not isinstance(command_arguments['target_temperature_unit'], Temperature):
1543
+ raise CommandError('Temperature unit is not of type Temperature')
1544
+ if command_arguments['target_temperature_unit'] == Temperature.C:
1545
+ command_dict['targetTemperature']['unitInCar'] = 'CELSIUS'
1546
+ elif command_arguments['target_temperature_unit'] == Temperature.F:
1547
+ command_dict['targetTemperature']['unitInCar'] = 'FAHRENHEIT'
1548
+ elif command_arguments['target_temperature_unit'] == Temperature.K:
1549
+ command_dict['targetTemperature']['unitInCar'] = 'KELVIN'
1550
+ else:
1551
+ raise CommandError(f'Unknown temperature unit {command_arguments["target_temperature_unit"]}')
1552
+ else:
1358
1553
  command_dict['targetTemperature']['unitInCar'] = 'CELSIUS'
1359
- elif command_arguments['target_temperature_unit'] == Temperature.F:
1554
+ elif start_stop_command.parent is not None and (climatization := start_stop_command.parent.parent) is not None \
1555
+ and isinstance(climatization, Climatization) and climatization.settings is not None \
1556
+ and climatization.settings.target_temperature is not None and climatization.settings.target_temperature.enabled \
1557
+ and climatization.settings.target_temperature.value is not None: # pylint: disable=too-many-boolean-expressions
1558
+ if climatization.settings.target_temperature.precision is not None:
1559
+ precision = climatization.settings.target_temperature.precision
1560
+ # Round target temperature to nearest 0.5
1561
+ command_dict['targetTemperature']['temperatureValue'] = round(climatization.settings.target_temperature.value / precision) * precision
1562
+ if climatization.settings.target_temperature.unit == Temperature.C:
1563
+ command_dict['targetTemperature']['unitInCar'] = 'CELSIUS'
1564
+ elif climatization.settings.target_temperature.unit == Temperature.F:
1360
1565
  command_dict['targetTemperature']['unitInCar'] = 'FAHRENHEIT'
1361
- elif command_arguments['target_temperature_unit'] == Temperature.K:
1566
+ elif climatization.settings.target_temperature.unit == Temperature.K:
1362
1567
  command_dict['targetTemperature']['unitInCar'] = 'KELVIN'
1363
1568
  else:
1364
- raise CommandError(f'Unknown temperature unit {command_arguments["target_temperature_unit"]}')
1569
+ raise CommandError(f'Unknown temperature unit {climatization.settings.target_temperature.unit}')
1365
1570
  else:
1571
+ command_dict['targetTemperature']['temperatureValue'] = 25.0
1366
1572
  command_dict['targetTemperature']['unitInCar'] = 'CELSIUS'
1367
- elif start_stop_command.parent is not None and (climatization := start_stop_command.parent.parent) is not None \
1368
- and isinstance(climatization, Climatization) and climatization.settings is not None \
1369
- and climatization.settings.target_temperature is not None and climatization.settings.target_temperature.enabled \
1370
- and climatization.settings.target_temperature.value is not None: # pylint: disable=too-many-boolean-expressions
1371
- # Round target temperature to nearest 0.5
1372
- command_dict['targetTemperature']['temperatureValue'] = round(climatization.settings.target_temperature.value * 2) / 2
1373
- if climatization.settings.target_temperature.unit == Temperature.C:
1374
- command_dict['targetTemperature']['unitInCar'] = 'CELSIUS'
1375
- elif climatization.settings.target_temperature.unit == Temperature.F:
1376
- command_dict['targetTemperature']['unitInCar'] = 'FAHRENHEIT'
1377
- elif climatization.settings.target_temperature.unit == Temperature.K:
1378
- command_dict['targetTemperature']['unitInCar'] = 'KELVIN'
1379
- else:
1380
- raise CommandError(f'Unknown temperature unit {climatization.settings.target_temperature.unit}')
1573
+ url = f'https://mysmob.api.connect.skoda-auto.cz/api/v2/air-conditioning/{vin}/start'
1574
+ command_response: requests.Response = self.session.post(url, data=json.dumps(command_dict), allow_redirects=True)
1575
+ elif command_arguments['command'] == ClimatizationStartStopCommand.Command.STOP:
1576
+ url = f'https://mysmob.api.connect.skoda-auto.cz/api/v2/air-conditioning/{vin}/stop'
1577
+ command_response: requests.Response = self.session.post(url, allow_redirects=True)
1381
1578
  else:
1382
- command_dict['targetTemperature']['temperatureValue'] = 25.0
1383
- command_dict['targetTemperature']['unitInCar'] = 'CELSIUS'
1384
- url = f'https://mysmob.api.connect.skoda-auto.cz/api/v2/air-conditioning/{vin}/start'
1385
- command_response: requests.Response = self.session.post(url, data=json.dumps(command_dict), allow_redirects=True)
1386
- elif command_arguments['command'] == ClimatizationStartStopCommand.Command.STOP:
1387
- url = f'https://mysmob.api.connect.skoda-auto.cz/api/v2/air-conditioning/{vin}/stop'
1388
- command_response: requests.Response = self.session.post(url, allow_redirects=True)
1389
- else:
1390
- raise CommandError(f'Unknown command {command_arguments["command"]}')
1579
+ raise CommandError(f'Unknown command {command_arguments["command"]}')
1391
1580
 
1392
- if command_response.status_code != requests.codes['accepted']:
1393
- LOG.error('Could not start/stop air conditioning (%s: %s)', command_response.status_code, command_response.text)
1394
- raise CommandError(f'Could not start/stop air conditioning ({command_response.status_code}: {command_response.text})')
1581
+ if command_response.status_code != requests.codes['accepted']:
1582
+ LOG.error('Could not start/stop air conditioning (%s: %s)', command_response.status_code, command_response.text)
1583
+ raise CommandError(f'Could not start/stop air conditioning ({command_response.status_code}: {command_response.text})')
1584
+ except requests.exceptions.ConnectionError as connection_error:
1585
+ raise CommandError(f'Connection error: {connection_error}.'
1586
+ ' If this happens frequently, please check if other applications communicate with the Skoda server.') from connection_error
1587
+ except requests.exceptions.ChunkedEncodingError as chunked_encoding_error:
1588
+ raise CommandError(f'Error: {chunked_encoding_error}') from chunked_encoding_error
1589
+ except requests.exceptions.ReadTimeout as timeout_error:
1590
+ raise CommandError(f'Timeout during read: {timeout_error}') from timeout_error
1591
+ except requests.exceptions.RetryError as retry_error:
1592
+ raise CommandError(f'Retrying failed: {retry_error}') from retry_error
1395
1593
  return command_arguments
1396
1594
 
1397
1595
  def __on_charging_start_stop(self, start_stop_command: ChargingStartStopCommand, command_arguments: Union[str, Dict[str, Any]]) \
@@ -1407,18 +1605,29 @@ class Connector(BaseConnector):
1407
1605
  raise CommandError('VIN in object hierarchy missing')
1408
1606
  if 'command' not in command_arguments:
1409
1607
  raise CommandError('Command argument missing')
1410
- if command_arguments['command'] == ChargingStartStopCommand.Command.START:
1411
- url = f'https://mysmob.api.connect.skoda-auto.cz/api/v1/charging/{vin}/start'
1412
- command_response: requests.Response = self.session.post(url, allow_redirects=True)
1413
- elif command_arguments['command'] == ChargingStartStopCommand.Command.STOP:
1414
- url = f'https://mysmob.api.connect.skoda-auto.cz/api/v1/charging/{vin}/stop'
1415
- command_response: requests.Response = self.session.post(url, allow_redirects=True)
1416
- else:
1417
- raise CommandError(f'Unknown command {command_arguments["command"]}')
1608
+ try:
1609
+ if command_arguments['command'] == ChargingStartStopCommand.Command.START:
1610
+ url = f'https://mysmob.api.connect.skoda-auto.cz/api/v1/charging/{vin}/start'
1611
+ command_response: requests.Response = self.session.post(url, allow_redirects=True)
1612
+ elif command_arguments['command'] == ChargingStartStopCommand.Command.STOP:
1613
+ url = f'https://mysmob.api.connect.skoda-auto.cz/api/v1/charging/{vin}/stop'
1614
+
1615
+ command_response: requests.Response = self.session.post(url, allow_redirects=True)
1616
+ else:
1617
+ raise CommandError(f'Unknown command {command_arguments["command"]}')
1418
1618
 
1419
- if command_response.status_code != requests.codes['accepted']:
1420
- LOG.error('Could not start/stop charging (%s: %s)', command_response.status_code, command_response.text)
1421
- raise CommandError(f'Could not start/stop charging ({command_response.status_code}: {command_response.text})')
1619
+ if command_response.status_code != requests.codes['accepted']:
1620
+ LOG.error('Could not start/stop charging (%s: %s)', command_response.status_code, command_response.text)
1621
+ raise CommandError(f'Could not start/stop charging ({command_response.status_code}: {command_response.text})')
1622
+ except requests.exceptions.ConnectionError as connection_error:
1623
+ raise CommandError(f'Connection error: {connection_error}.'
1624
+ ' If this happens frequently, please check if other applications communicate with the Skoda server.') from connection_error
1625
+ except requests.exceptions.ChunkedEncodingError as chunked_encoding_error:
1626
+ raise CommandError(f'Error: {chunked_encoding_error}') from chunked_encoding_error
1627
+ except requests.exceptions.ReadTimeout as timeout_error:
1628
+ raise CommandError(f'Timeout during read: {timeout_error}') from timeout_error
1629
+ except requests.exceptions.RetryError as retry_error:
1630
+ raise CommandError(f'Retrying failed: {retry_error}') from retry_error
1422
1631
  return command_arguments
1423
1632
 
1424
1633
  def __on_honk_flash(self, honk_flash_command: HonkAndFlashCommand, command_arguments: Union[str, Dict[str, Any]]) \
@@ -1448,10 +1657,20 @@ class Connector(BaseConnector):
1448
1657
  command_dict['vehiclePosition']['longitude'] = vehicle.position.longitude.value
1449
1658
 
1450
1659
  url = f'https://mysmob.api.connect.skoda-auto.cz/api/v1/vehicle-access/{vin}/honk-and-flash'
1451
- command_response: requests.Response = self.session.post(url, data=json.dumps(command_dict), allow_redirects=True)
1452
- if command_response.status_code != requests.codes['accepted']:
1453
- LOG.error('Could not execute honk or flash command (%s: %s)', command_response.status_code, command_response.text)
1454
- raise CommandError(f'Could not execute honk or flash command ({command_response.status_code}: {command_response.text})')
1660
+ try:
1661
+ command_response: requests.Response = self.session.post(url, data=json.dumps(command_dict), allow_redirects=True)
1662
+ if command_response.status_code != requests.codes['accepted']:
1663
+ LOG.error('Could not execute honk or flash command (%s: %s)', command_response.status_code, command_response.text)
1664
+ raise CommandError(f'Could not execute honk or flash command ({command_response.status_code}: {command_response.text})')
1665
+ except requests.exceptions.ConnectionError as connection_error:
1666
+ raise CommandError(f'Connection error: {connection_error}.'
1667
+ ' If this happens frequently, please check if other applications communicate with the Skoda server.') from connection_error
1668
+ except requests.exceptions.ChunkedEncodingError as chunked_encoding_error:
1669
+ raise SetterError(f'Error: {chunked_encoding_error}') from chunked_encoding_error
1670
+ except requests.exceptions.ReadTimeout as timeout_error:
1671
+ raise CommandError(f'Timeout during read: {timeout_error}') from timeout_error
1672
+ except requests.exceptions.RetryError as retry_error:
1673
+ raise CommandError(f'Retrying failed: {retry_error}') from retry_error
1455
1674
  else:
1456
1675
  raise CommandError(f'Unknown command {command_arguments["command"]}')
1457
1676
  return command_arguments
@@ -1482,10 +1701,20 @@ class Connector(BaseConnector):
1482
1701
  url = f'https://mysmob.api.connect.skoda-auto.cz/api/v1/vehicle-access/{vin}/unlock'
1483
1702
  else:
1484
1703
  raise CommandError(f'Unknown command {command_arguments["command"]}')
1485
- command_response: requests.Response = self.session.post(url, data=json.dumps(command_dict), allow_redirects=True)
1486
- if command_response.status_code != requests.codes['accepted']:
1487
- LOG.error('Could not execute locking command (%s: %s)', command_response.status_code, command_response.text)
1488
- raise CommandError(f'Could not execute locking command ({command_response.status_code}: {command_response.text})')
1704
+ try:
1705
+ command_response: requests.Response = self.session.post(url, data=json.dumps(command_dict), allow_redirects=True)
1706
+ if command_response.status_code != requests.codes['accepted']:
1707
+ LOG.error('Could not execute locking command (%s: %s)', command_response.status_code, command_response.text)
1708
+ raise CommandError(f'Could not execute locking command ({command_response.status_code}: {command_response.text})')
1709
+ except requests.exceptions.ConnectionError as connection_error:
1710
+ raise CommandError(f'Connection error: {connection_error}.'
1711
+ ' If this happens frequently, please check if other applications communicate with the Skoda server.') from connection_error
1712
+ except requests.exceptions.ChunkedEncodingError as chunked_encoding_error:
1713
+ raise CommandError(f'Error: {chunked_encoding_error}') from chunked_encoding_error
1714
+ except requests.exceptions.ReadTimeout as timeout_error:
1715
+ raise CommandError(f'Timeout during read: {timeout_error}') from timeout_error
1716
+ except requests.exceptions.RetryError as retry_error:
1717
+ raise CommandError(f'Retrying failed: {retry_error}') from retry_error
1489
1718
  return command_arguments
1490
1719
 
1491
1720
  def __on_wake_sleep(self, wake_sleep_command: WakeSleepCommand, command_arguments: Union[str, Dict[str, Any]]) \
@@ -1504,10 +1733,20 @@ class Connector(BaseConnector):
1504
1733
  if command_arguments['command'] == WakeSleepCommand.Command.WAKE:
1505
1734
  url = f'https://mysmob.api.connect.skoda-auto.cz/api/v1/vehicle-wakeup/{vin}?applyRequestLimiter=true'
1506
1735
 
1507
- command_response: requests.Response = self.session.post(url, data='{}', allow_redirects=True)
1508
- if command_response.status_code != requests.codes['accepted']:
1509
- LOG.error('Could not execute wake command (%s: %s)', command_response.status_code, command_response.text)
1510
- raise CommandError(f'Could not execute wake command ({command_response.status_code}: {command_response.text})')
1736
+ try:
1737
+ command_response: requests.Response = self.session.post(url, data='{}', allow_redirects=True)
1738
+ if command_response.status_code != requests.codes['accepted']:
1739
+ LOG.error('Could not execute wake command (%s: %s)', command_response.status_code, command_response.text)
1740
+ raise CommandError(f'Could not execute wake command ({command_response.status_code}: {command_response.text})')
1741
+ except requests.exceptions.ConnectionError as connection_error:
1742
+ raise CommandError(f'Connection error: {connection_error}.'
1743
+ ' If this happens frequently, please check if other applications communicate with the Skoda server.') from connection_error
1744
+ except requests.exceptions.ChunkedEncodingError as chunked_encoding_error:
1745
+ raise CommandError(f'Error: {chunked_encoding_error}') from chunked_encoding_error
1746
+ except requests.exceptions.ReadTimeout as timeout_error:
1747
+ raise CommandError(f'Timeout during read: {timeout_error}') from timeout_error
1748
+ except requests.exceptions.RetryError as retry_error:
1749
+ raise CommandError(f'Retrying failed: {retry_error}') from retry_error
1511
1750
  elif command_arguments['command'] == WakeSleepCommand.Command.SLEEP:
1512
1751
  raise CommandError('Sleep command not supported by vehicle. Vehicle will put itself to sleep')
1513
1752
  else:
@@ -1534,10 +1773,23 @@ class Connector(BaseConnector):
1534
1773
  url = 'https://mysmob.api.connect.skoda-auto.cz/api/v1/spin/verify'
1535
1774
  else:
1536
1775
  raise CommandError(f'Unknown command {command_arguments["command"]}')
1537
- command_response: requests.Response = self.session.post(url, data=json.dumps(command_dict), allow_redirects=True)
1538
- if command_response.status_code != requests.codes['ok']:
1539
- LOG.error('Could not execute spin command (%s: %s)', command_response.status_code, command_response.text)
1540
- raise CommandError(f'Could not execute spin command ({command_response.status_code}: {command_response.text})')
1541
- else:
1542
- LOG.info('Spin verify command executed successfully')
1776
+ try:
1777
+ command_response: requests.Response = self.session.post(url, data=json.dumps(command_dict), allow_redirects=True)
1778
+ if command_response.status_code != requests.codes['ok']:
1779
+ LOG.error('Could not execute spin command (%s: %s)', command_response.status_code, command_response.text)
1780
+ raise CommandError(f'Could not execute spin command ({command_response.status_code}: {command_response.text})')
1781
+ else:
1782
+ LOG.info('Spin verify command executed successfully')
1783
+ except requests.exceptions.ConnectionError as connection_error:
1784
+ raise CommandError(f'Connection error: {connection_error}.'
1785
+ ' If this happens frequently, please check if other applications communicate with the Skoda server.') from connection_error
1786
+ except requests.exceptions.ChunkedEncodingError as chunked_encoding_error:
1787
+ raise CommandError(f'Error: {chunked_encoding_error}') from chunked_encoding_error
1788
+ except requests.exceptions.ReadTimeout as timeout_error:
1789
+ raise CommandError(f'Timeout during read: {timeout_error}') from timeout_error
1790
+ except requests.exceptions.RetryError as retry_error:
1791
+ raise CommandError(f'Retrying failed: {retry_error}') from retry_error
1543
1792
  return command_arguments
1793
+
1794
+ def get_name(self) -> str:
1795
+ return "Skoda Connector"
@@ -22,6 +22,7 @@ from carconnectivity.util import robust_time_parse, log_extra_keys
22
22
  from carconnectivity.charging import Charging
23
23
  from carconnectivity.climatization import Climatization
24
24
  from carconnectivity.units import Speed, Power, Length
25
+ from carconnectivity.enums import ConnectionState
25
26
 
26
27
  from carconnectivity_connectors.skoda.vehicle import SkodaVehicle, SkodaElectricVehicle
27
28
  from carconnectivity_connectors.skoda.charging import SkodaCharging, mapping_skoda_charging_state
@@ -77,6 +78,7 @@ class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
77
78
  Returns:
78
79
  MQTTErrorCode: The result of the connection attempt.
79
80
  """
81
+ self._skoda_connector.connection_state._set_value(value=ConnectionState.CONNECTING) # pylint: disable=protected-access
80
82
  return super().connect(*args, host='mqtt.messagehub.de', port=8883, keepalive=60, **kwargs)
81
83
 
82
84
  def _on_pre_connect_callback(self, client: Client, userdata: Any) -> None:
@@ -312,7 +314,9 @@ class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
312
314
  # reason_code 0 means success
313
315
  if reason_code == 0:
314
316
  LOG.info('Connected to Skoda MQTT server')
315
- self._skoda_connector.connected._set_value(value=True) # pylint: disable=protected-access
317
+ if self._skoda_connector.rest_connected:
318
+ self._skoda_connector.connection_state._set_value(value=ConnectionState.CONNECTED) # pylint: disable=protected-access
319
+ self._skoda_connector.mqtt_connected = True
316
320
  observer_flags: Observable.ObserverEvent = Observable.ObserverEvent.ENABLED | Observable.ObserverEvent.DISABLED
317
321
  self._skoda_connector.car_connectivity.garage.add_observer(observer=self._on_carconnectivity_vehicle_enabled,
318
322
  flag=observer_flags,
@@ -385,7 +389,8 @@ class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
385
389
  del properties
386
390
  del flags
387
391
 
388
- self._skoda_connector.connected._set_value(value=False) # pylint: disable=protected-access
392
+ self._skoda_connector.connection_state._set_value(value=ConnectionState.DISCONNECTED) # pylint: disable=protected-access
393
+ self._skoda_connector.mqtt_connected = False
389
394
  self._skoda_connector.car_connectivity.garage.remove_observer(observer=self._on_carconnectivity_vehicle_enabled)
390
395
 
391
396
  self.subscribed_topics.clear()
@@ -469,7 +474,7 @@ class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
469
474
  if 'data' in data and data['data'] is not None:
470
475
  vehicle: Optional[GenericVehicle] = self._skoda_connector.car_connectivity.garage.get_vehicle(vin)
471
476
  if isinstance(vehicle, SkodaElectricVehicle):
472
- electric_drive: ElectricDrive = vehicle.get_electric_drive()
477
+ electric_drive: Optional[ElectricDrive] = vehicle.get_electric_drive()
473
478
  if electric_drive is not None:
474
479
  charging_state: Optional[Charging.ChargingState] = vehicle.charging.state.value
475
480
  old_charging_state: Optional[Charging.ChargingState] = charging_state
@@ -509,6 +514,7 @@ class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
509
514
  if old_charging_state != charging_state:
510
515
  try:
511
516
  self._skoda_connector.fetch_charging(vehicle, no_cache=True)
517
+ self._skoda_connector.car_connectivity.transaction_end()
512
518
  except CarConnectivityError as e:
513
519
  LOG.error('Error while fetching charging: %s', e)
514
520
  if 'timeToFinish' in data['data'] and data['data']['timeToFinish'] is not None \
@@ -536,6 +542,7 @@ class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
536
542
  if isinstance(vehicle, SkodaVehicle):
537
543
  try:
538
544
  self._skoda_connector.fetch_air_conditioning(vehicle, no_cache=True)
545
+ self._skoda_connector.car_connectivity.transaction_end()
539
546
  except CarConnectivityError as e:
540
547
  LOG.error('Error while fetching air conditioning: %s', e)
541
548
  elif 'name' in data and data['name'] == 'climatisation-completed':
@@ -582,6 +589,7 @@ class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
582
589
  self._skoda_connector.fetch_air_conditioning(vehicle, no_cache=True)
583
590
  except CarConnectivityError as e:
584
591
  LOG.error('Error while fetching air conditioning: %s', e)
592
+ self._skoda_connector.car_connectivity.transaction_end()
585
593
 
586
594
  if vin in self.delayed_access_function_timers:
587
595
  self.delayed_access_function_timers[vin].cancel()
@@ -598,6 +606,7 @@ class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
598
606
  if isinstance(vehicle, SkodaVehicle):
599
607
  try:
600
608
  self._skoda_connector.fetch_vehicle_status(vehicle, no_cache=True)
609
+ self._skoda_connector.car_connectivity.transaction_end()
601
610
  except CarConnectivityError as e:
602
611
  LOG.error('Error while fetching vehicle status: %s', e)
603
612
 
@@ -629,6 +638,7 @@ class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
629
638
  LOG.debug('Received %s operation request for vehicle %s from user %s', operation_request, vin, user_id)
630
639
  try:
631
640
  self._skoda_connector.fetch_air_conditioning(vehicle, no_cache=True)
641
+ self._skoda_connector.car_connectivity.transaction_end()
632
642
  except CarConnectivityError as e:
633
643
  LOG.error('Error while fetching air-conditioning: %s', e)
634
644
  return
@@ -649,6 +659,7 @@ class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
649
659
  LOG.debug('Received %s operation request for vehicle %s from user %s', operation_request, vin, user_id)
650
660
  try:
651
661
  self._skoda_connector.fetch_charging(vehicle, no_cache=True)
662
+ self._skoda_connector.car_connectivity.transaction_end()
652
663
  except CarConnectivityError as e:
653
664
  LOG.error('Error while fetching charging: %s', e)
654
665
  return
@@ -4,9 +4,11 @@ from typing import TYPE_CHECKING
4
4
 
5
5
  from carconnectivity.vehicle import GenericVehicle, ElectricVehicle, CombustionVehicle, HybridVehicle
6
6
  from carconnectivity.charging import Charging
7
+ from carconnectivity.attributes import BooleanAttribute
7
8
 
8
9
  from carconnectivity_connectors.skoda.capability import Capabilities
9
10
  from carconnectivity_connectors.skoda.charging import SkodaCharging
11
+ from carconnectivity_connectors.skoda.climatization import SkodaClimatization
10
12
 
11
13
  SUPPORT_IMAGES = False
12
14
  try:
@@ -31,12 +33,16 @@ class SkodaVehicle(GenericVehicle): # pylint: disable=too-many-instance-attribu
31
33
  super().__init__(origin=origin)
32
34
  self.capabilities: Capabilities = origin.capabilities
33
35
  self.capabilities.parent = self
36
+ self.in_motion: BooleanAttribute = origin.in_motion
37
+ self.in_motion.parent = self
34
38
  if SUPPORT_IMAGES:
35
39
  self._car_images = origin._car_images
36
40
 
37
41
  else:
38
42
  super().__init__(vin=vin, garage=garage, managing_connector=managing_connector)
43
+ self.climatization = SkodaClimatization(vehicle=self, origin=self.climatization)
39
44
  self.capabilities = Capabilities(vehicle=self)
45
+ self.in_motion = BooleanAttribute(name='in_motion', parent=self, tags={'connector_custom'})
40
46
  if SUPPORT_IMAGES:
41
47
  self._car_images: Dict[str, Image.Image] = {}
42
48
  self.manufacturer._set_value(value='Škoda') # pylint: disable=protected-access