carconnectivity-connector-skoda 0.2a5__py3-none-any.whl → 0.4__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.2
2
2
  Name: carconnectivity-connector-skoda
3
- Version: 0.2a5
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.3a2
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
@@ -0,0 +1,23 @@
1
+ carconnectivity_connectors/skoda/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ carconnectivity_connectors/skoda/_version.py,sha256=z4A7Ai6QyXWBOpaj5g4Fi0n9CYi27WfJ6gm02OiXBxE,506
3
+ carconnectivity_connectors/skoda/capability.py,sha256=vbAKK8KKre-CndLF6_5qyWLpfa4KZHk1U-hpb6nCL5w,4225
4
+ carconnectivity_connectors/skoda/charging.py,sha256=CoUOYHHUPPPldKQvv0h-qrUsoEtstR3iUx-l0IU5rNM,6798
5
+ carconnectivity_connectors/skoda/climatization.py,sha256=Jut468SkxjPBDTqroWFvDifVPfJBxGjsFed5pc4kZkg,1768
6
+ carconnectivity_connectors/skoda/command_impl.py,sha256=vgno5Qb5To0hCHOEBWSG8UOwCY9kT5fz1e2y0b6zF7U,3047
7
+ carconnectivity_connectors/skoda/connector.py,sha256=cwI-7AAwzKODZgyZtk2qL-HxBdEpEvHE2riVLTePXw4,128467
8
+ carconnectivity_connectors/skoda/error.py,sha256=ffxzvjmci7vtp9Q1K4DR1kBF0kTJxN5Gluci3kDBkEI,2459
9
+ carconnectivity_connectors/skoda/mqtt_client.py,sha256=RkZ43NG1Z_TUmc2hUZS0yYUfwewzfut63zZUhQR1xug,39101
10
+ carconnectivity_connectors/skoda/vehicle.py,sha256=TeY3qKWbBfxFxt6UzSDrB-YZ4L8GURAvINzBiFm9Y9E,3819
11
+ carconnectivity_connectors/skoda/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ carconnectivity_connectors/skoda/auth/auth_util.py,sha256=dGLUbUre0HBsTg_Ii5vW34f8DLrCykYJYCyzEvUBBEE,4434
13
+ carconnectivity_connectors/skoda/auth/my_skoda_session.py,sha256=lSh23SFJs8opjmPwHTv-KNIKDep_WY4aItSP4Zq7bT8,10396
14
+ carconnectivity_connectors/skoda/auth/openid_session.py,sha256=5JfR-gS1uKpE8DD-sx5Qvw6zv-OJhzcRlt0D-cm38-Y,16832
15
+ carconnectivity_connectors/skoda/auth/session_manager.py,sha256=Uf1vujuDBYUCAXhYToOsZkgbTtfmY3Qe0ICTfwomBpI,2899
16
+ carconnectivity_connectors/skoda/auth/skoda_web_session.py,sha256=tapjCRRPBu3tHrDoKmtuAlQhgxktib3oWTB8MHEzZTY,10816
17
+ carconnectivity_connectors/skoda/auth/helpers/blacklist_retry.py,sha256=f3wsiY5bpHDBxp7Va1Mv9nKJ4u3qnCHZZmDu78_AhMk,1251
18
+ carconnectivity_connectors/skoda/ui/connector_ui.py,sha256=2Gywhyki52IxIZXV6DhWhzrBLn2293LlUMhK1Rxnw9w,1376
19
+ carconnectivity_connector_skoda-0.4.dist-info/LICENSE,sha256=PIwI1alwDyOfvEQHdGCm2u9uf_mGE8030xZDfun0xTo,1071
20
+ carconnectivity_connector_skoda-0.4.dist-info/METADATA,sha256=mgi4p5xjlB5jJxeBIm3W3vZnkBjXuaCT8ZdRIDFA--g,5361
21
+ carconnectivity_connector_skoda-0.4.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
22
+ carconnectivity_connector_skoda-0.4.dist-info/top_level.txt,sha256=KqA8GviZsDH4PtmnwSQsz0HB_w-TWkeEHLIRNo5dTaI,27
23
+ carconnectivity_connector_skoda-0.4.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.8.0)
2
+ Generator: setuptools (75.8.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -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.2a5'
16
- __version_tuple__ = version_tuple = (0, 2)
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
@@ -15,20 +16,21 @@ import requests
15
16
  from carconnectivity.garage import Garage
16
17
  from carconnectivity.vehicle import GenericVehicle
17
18
  from carconnectivity.errors import AuthenticationError, TooManyRequestsError, RetrievalError, APIError, APICompatibilityError, \
18
- TemporaryAuthenticationError, ConfigurationError, SetterError, CommandError
19
+ TemporaryAuthenticationError, SetterError, CommandError
19
20
  from carconnectivity.util import robust_time_parse, log_extra_keys, config_remove_credentials
20
21
  from carconnectivity.units import Length, Speed, Power, Temperature
21
22
  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
@@ -71,7 +73,7 @@ class Connector(BaseConnector):
71
73
  max_age (Optional[int]): Maximum age for cached data in seconds.
72
74
  """
73
75
  def __init__(self, connector_id: str, car_connectivity: CarConnectivity, config: Dict) -> None:
74
- BaseConnector.__init__(self, connector_id=connector_id, car_connectivity=car_connectivity, config=config)
76
+ BaseConnector.__init__(self, connector_id=connector_id, car_connectivity=car_connectivity, config=config, log=LOG, api_log=LOG_API)
75
77
 
76
78
  self._mqtt_client: SkodaMQTTClient = SkodaMQTTClient(skoda_connector=self)
77
79
 
@@ -79,83 +81,74 @@ 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
87
95
 
88
- # Configure logging
89
- if 'log_level' in config and config['log_level'] is not None:
90
- config['log_level'] = config['log_level'].upper()
91
- if config['log_level'] in logging._nameToLevel:
92
- LOG.setLevel(config['log_level'])
93
- self.log_level._set_value(config['log_level']) # pylint: disable=protected-access
94
- logging.getLogger('requests').setLevel(config['log_level'])
95
- logging.getLogger('urllib3').setLevel(config['log_level'])
96
- logging.getLogger('oauthlib').setLevel(config['log_level'])
97
- else:
98
- raise ConfigurationError(f'Invalid log level: "{config["log_level"]}" not in {list(logging._nameToLevel.keys())}')
99
- if 'api_log_level' in config and config['api_log_level'] is not None:
100
- config['api_log_level'] = config['api_log_level'].upper()
101
- if config['api_log_level'] in logging._nameToLevel:
102
- LOG_API.setLevel(config['api_log_level'])
103
- else:
104
- raise ConfigurationError(f'Invalid log level: "{config["log_level"]}" not in {list(logging._nameToLevel.keys())}')
105
- LOG.info("Loading skoda connector with config %s", config_remove_credentials(self.config))
96
+ LOG.info("Loading skoda connector with config %s", config_remove_credentials(config))
106
97
 
107
98
  if 'spin' in config and config['spin'] is not None:
108
- self._spin: Optional[str] = config['spin']
99
+ self.active_config['spin'] = config['spin']
109
100
  else:
110
- self._spin = None
101
+ self.active_config['spin'] = None
111
102
 
112
- username: Optional[str] = None
113
- password: Optional[str] = None
114
- if 'username' in self.config and 'password' in self.config:
115
- username = self.config['username']
116
- password = self.config['password']
103
+ self.active_config['username'] = None
104
+ self.active_config['password'] = None
105
+ if 'username' in config and 'password' in config:
106
+ self.active_config['username'] = config['username']
107
+ self.active_config['password'] = config['password']
117
108
  else:
118
- if 'netrc' in self.config:
119
- netrc_filename: str = self.config['netrc']
109
+ if 'netrc' in config:
110
+ self.active_config['netrc'] = config['netrc']
120
111
  else:
121
- netrc_filename = os.path.join(os.path.expanduser("~"), ".netrc")
112
+ self.active_config['netrc'] = os.path.join(os.path.expanduser("~"), ".netrc")
122
113
  try:
123
- secrets = netrc.netrc(file=netrc_filename)
114
+ secrets = netrc.netrc(file=self.active_config['netrc'])
124
115
  secret: tuple[str, str, str] | None = secrets.authenticators("skoda")
125
116
  if secret is None:
126
- raise AuthenticationError(f'Authentication using {netrc_filename} failed: skoda not found in netrc')
127
- username, account, password = secret
117
+ raise AuthenticationError(f'Authentication using {self.active_config["netrc"]} failed: skoda not found in netrc')
118
+ self.active_config['username'], account, self.active_config['password'] = secret
128
119
 
129
- if self._spin is None and account is not None:
120
+ if self.active_config['spin'] is None and account is not None:
130
121
  try:
131
- self._spin = account
122
+ self.active_config['spin'] = account
132
123
  except ValueError as err:
133
124
  LOG.error('Could not parse spin from netrc: %s', err)
134
125
  except netrc.NetrcParseError as err:
135
- LOG.error('Authentification using %s failed: %s', netrc_filename, err)
136
- raise AuthenticationError(f'Authentication using {netrc_filename} failed: {err}') from err
126
+ LOG.error('Authentification using %s failed: %s', self.active_config['netrc'], err)
127
+ raise AuthenticationError(f'Authentication using {self.active_config["netrc"]} failed: {err}') from err
137
128
  except TypeError as err:
138
- if 'username' not in self.config:
139
- raise AuthenticationError(f'"skoda" entry was not found in {netrc_filename} netrc-file.'
129
+ if 'username' not in config:
130
+ raise AuthenticationError(f'"skoda" entry was not found in {self.active_config["netrc"]} netrc-file.'
140
131
  ' Create it or provide username and password in config') from err
141
132
  except FileNotFoundError as err:
142
- raise AuthenticationError(f'{netrc_filename} netrc-file was not found. Create it or provide username and password in config') from err
143
-
144
- interval: int = 300
145
- if 'interval' in self.config:
146
- interval = self.config['interval']
147
- if interval < 300:
148
- raise ValueError('Intervall must be at least 300 seconds')
149
- self.max_age: int = interval - 1
150
- if 'max_age' in self.config:
151
- self.max_age = self.config['max_age']
152
- self.interval._set_value(timedelta(seconds=interval)) # pylint: disable=protected-access
153
-
154
- if username is None or password is None:
133
+ raise AuthenticationError(f'{self.active_config["netrc"]} netrc-file was not found. Create it or provide username and password in config') \
134
+ from err
135
+
136
+ self.active_config['interval'] = 300
137
+ if 'interval' in config:
138
+ self.active_config['interval'] = config['interval']
139
+ if self.active_config['interval'] < 180:
140
+ raise ValueError('Intervall must be at least 180 seconds')
141
+ self.active_config['max_age'] = self.active_config['interval'] - 1
142
+ if 'max_age' in config:
143
+ self.active_config['max_age'] = config['max_age']
144
+ self.interval._set_value(timedelta(seconds=self.active_config['interval'])) # pylint: disable=protected-access
145
+
146
+ if self.active_config['username'] is None or self.active_config['password'] is None:
155
147
  raise AuthenticationError('Username or password not provided')
156
148
 
157
149
  self._manager: SessionManager = SessionManager(tokenstore=car_connectivity.get_tokenstore(), cache=car_connectivity.get_cache())
158
- session: requests.Session = self._manager.get_session(Service.MY_SKODA, SessionUser(username=username, password=password))
150
+ session: requests.Session = self._manager.get_session(Service.MY_SKODA, SessionUser(username=self.active_config['username'],
151
+ password=self.active_config['password']))
159
152
  if not isinstance(session, MySkodaSession):
160
153
  raise AuthenticationError('Could not create session')
161
154
  self.session: MySkodaSession = session
@@ -169,12 +162,15 @@ class Connector(BaseConnector):
169
162
  self._stop_event.clear()
170
163
  # Start background thread for Rest API polling
171
164
  self._background_thread = threading.Thread(target=self._background_loop, daemon=False)
165
+ self._background_thread.name = 'carconnectivity.connectors.skoda-background'
172
166
  self._background_thread.start()
173
167
  # Start background thread for MQTT connection
174
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'
175
170
  self._background_connect_thread.start()
176
171
  # Start MQTT thread
177
172
  self._mqtt_client.loop_start()
173
+ self.healthy._set_value(value=True) # pylint: disable=protected-access
178
174
 
179
175
  def _background_connect_loop(self) -> None:
180
176
  while not self._stop_event.is_set():
@@ -188,6 +184,7 @@ class Connector(BaseConnector):
188
184
  def _background_loop(self) -> None:
189
185
  self._stop_event.clear()
190
186
  fetch: bool = True
187
+ self.connection_state._set_value(value=ConnectionState.CONNECTING) # pylint: disable=protected-access
191
188
  while not self._stop_event.is_set():
192
189
  interval = 300
193
190
  try:
@@ -206,21 +203,43 @@ class Connector(BaseConnector):
206
203
  raise
207
204
  except TooManyRequestsError as err:
208
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
209
208
  self._stop_event.wait(900)
210
209
  except RetrievalError as err:
211
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
212
213
  self._stop_event.wait(interval)
213
214
  except APIError as err:
214
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
215
218
  self._stop_event.wait(interval)
216
219
  except APICompatibilityError as err:
217
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
218
223
  self._stop_event.wait(interval)
219
224
  except TemporaryAuthenticationError as err:
220
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
221
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
222
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
223
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
224
243
 
225
244
  def persist(self) -> None:
226
245
  """
@@ -251,12 +270,12 @@ class Connector(BaseConnector):
251
270
  self.car_connectivity.garage.remove_vehicle(vehicle.id)
252
271
  vehicle.enabled = False
253
272
  self._stop_event.set()
273
+ self.session.close()
254
274
  if self._background_thread is not None:
255
275
  self._background_thread.join()
256
276
  if self._background_connect_thread is not None:
257
277
  self._background_connect_thread.join()
258
278
  self.persist()
259
- self.session.close()
260
279
  return super().shutdown()
261
280
 
262
281
  def fetch_all(self) -> None:
@@ -354,12 +373,38 @@ class Connector(BaseConnector):
354
373
  vehicle_to_update = self.fetch_vehicle_status(vehicle_to_update)
355
374
  vehicle_to_update = self.fetch_driving_range(vehicle_to_update)
356
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)
357
378
  if vehicle_to_update.capabilities.has_capability('PARKING_POSITION'):
358
379
  vehicle_to_update = self.fetch_position(vehicle_to_update)
359
380
  if vehicle_to_update.capabilities.has_capability('CHARGING') and isinstance(vehicle_to_update, SkodaElectricVehicle):
360
381
  vehicle_to_update = self.fetch_charging(vehicle_to_update)
361
382
  if vehicle_to_update.capabilities.has_capability('AIR_CONDITIONING'):
362
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
363
408
 
364
409
  def fetch_charging(self, vehicle: SkodaElectricVehicle, no_cache: bool = False) -> SkodaElectricVehicle:
365
410
  """
@@ -568,6 +613,41 @@ class Connector(BaseConnector):
568
613
  vehicle.charging.errors.clear()
569
614
  log_extra_keys(LOG_API, 'charging data', data, {'carCapturedTimestamp', 'status', 'isVehicleInSavedLocation', 'errors', 'settings'})
570
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
571
651
 
572
652
  def fetch_position(self, vehicle: SkodaVehicle, no_cache: bool = False) -> SkodaVehicle:
573
653
  """
@@ -589,7 +669,7 @@ class Connector(BaseConnector):
589
669
  if vehicle.position is None:
590
670
  raise ValueError('Vehicle has no charging object')
591
671
  url = f'https://mysmob.api.connect.skoda-auto.cz/api/v1/maps/positions?vin={vin}'
592
- 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)
593
673
  if data is not None:
594
674
  if 'positions' in data and data['positions'] is not None:
595
675
  for position_dict in data['positions']:
@@ -623,6 +703,53 @@ class Connector(BaseConnector):
623
703
  vehicle.position.position_type._set_value(None) # pylint: disable=protected-access
624
704
  return vehicle
625
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
+
626
753
  def fetch_air_conditioning(self, vehicle: SkodaVehicle, no_cache: bool = False) -> SkodaVehicle:
627
754
  """
628
755
  Fetches the air conditioning data for a given Skoda vehicle and updates the vehicle object with the retrieved data.
@@ -682,12 +809,19 @@ class Connector(BaseConnector):
682
809
  # pylint: disable-next=protected-access
683
810
  vehicle.climatization.settings.target_temperature._add_on_set_hook(self.__on_air_conditioning_target_temperature_change)
684
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
685
815
  unit: Temperature = Temperature.UNKNOWN
686
816
  if 'unitInCar' in data['targetTemperature'] and data['targetTemperature']['unitInCar'] is not None:
687
817
  if data['targetTemperature']['unitInCar'] == 'CELSIUS':
688
818
  unit = Temperature.C
819
+ min_temperature: Optional[float] = 16
820
+ max_temperature: Optional[float] = 29.5
689
821
  elif data['targetTemperature']['unitInCar'] == 'FAHRENHEIT':
690
822
  unit = Temperature.F
823
+ min_temperature: Optional[float] = 61
824
+ max_temperature: Optional[float] = 85
691
825
  elif data['targetTemperature']['unitInCar'] == 'KELVIN':
692
826
  unit = Temperature.K
693
827
  else:
@@ -697,6 +831,10 @@ class Connector(BaseConnector):
697
831
  vehicle.climatization.settings.target_temperature._set_value(value=data['targetTemperature']['temperatureValue'],
698
832
  measured=captured_at,
699
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
+
700
838
  else:
701
839
  # pylint: disable-next=protected-access
702
840
  vehicle.climatization.settings.target_temperature._set_value(value=None, measured=captured_at, unit=unit)
@@ -948,7 +1086,7 @@ class Connector(BaseConnector):
948
1086
  data = self._fetch_data(url, session=self.session, allow_http_error=True)
949
1087
  if data is not None and 'compositeRenders' in data: # pylint: disable=too-many-nested-blocks
950
1088
  for image in data['compositeRenders']:
951
- if not 'layers' in image or image['layers'] is None or len(image['layers']) == 0:
1089
+ if 'layers' not in image or image['layers'] is None or len(image['layers']) == 0:
952
1090
  continue
953
1091
  image_url: Optional[str] = None
954
1092
  for layer in image['layers']:
@@ -959,13 +1097,13 @@ class Connector(BaseConnector):
959
1097
  continue
960
1098
  img = None
961
1099
  cache_date = None
962
- if self.max_age is not None and self.session.cache is not None and image_url in self.session.cache:
1100
+ if self.active_config['max_age'] is not None and self.session.cache is not None and image_url in self.session.cache:
963
1101
  img, cache_date_string = self.session.cache[image_url]
964
1102
  img = base64.b64decode(img) # pyright: ignore[reportPossiblyUnboundVariable]
965
1103
  img = Image.open(io.BytesIO(img)) # pyright: ignore[reportPossiblyUnboundVariable]
966
1104
  cache_date = datetime.fromisoformat(cache_date_string)
967
- if img is None or self.max_age is None \
968
- or (cache_date is not None and cache_date < (datetime.utcnow() - timedelta(seconds=self.max_age))):
1105
+ if img is None or self.active_config['max_age'] is None \
1106
+ or (cache_date is not None and cache_date < (datetime.utcnow() - timedelta(seconds=self.active_config['max_age']))):
969
1107
  try:
970
1108
  image_download_response = requests.get(image_url, stream=True)
971
1109
  if image_download_response.status_code == requests.codes['ok']:
@@ -1000,8 +1138,8 @@ class Connector(BaseConnector):
1000
1138
  if 'car_picture' in vehicle.images.images:
1001
1139
  vehicle.images.images['car_picture']._set_value(img) # pylint: disable=protected-access
1002
1140
  else:
1003
- vehicle.images.images['car_picture']: ImageAttribute = ImageAttribute(name="car_picture", parent=vehicle.images,
1004
- value=img, tags={'carconnectivity'})
1141
+ vehicle.images.images['car_picture'] = ImageAttribute(name="car_picture", parent=vehicle.images,
1142
+ value=img, tags={'carconnectivity'})
1005
1143
  return vehicle
1006
1144
 
1007
1145
  def fetch_driving_range(self, vehicle: SkodaVehicle, no_cache: bool = False) -> SkodaVehicle:
@@ -1224,11 +1362,11 @@ class Connector(BaseConnector):
1224
1362
  allowed_errors=None) -> Optional[Dict[str, Any]]: # noqa: C901
1225
1363
  data: Optional[Dict[str, Any]] = None
1226
1364
  cache_date: Optional[datetime] = None
1227
- if not no_cache and (self.max_age is not None and session.cache is not None and url in session.cache):
1365
+ if not no_cache and (self.active_config['max_age'] is not None and session.cache is not None and url in session.cache):
1228
1366
  data, cache_date_string = session.cache[url]
1229
1367
  cache_date = datetime.fromisoformat(cache_date_string)
1230
- if data is None or self.max_age is None \
1231
- or (cache_date is not None and cache_date < (datetime.utcnow() - timedelta(seconds=self.max_age))):
1368
+ if data is None or self.active_config['max_age'] is None \
1369
+ or (cache_date is not None and cache_date < (datetime.utcnow() - timedelta(seconds=self.active_config['max_age']))):
1232
1370
  try:
1233
1371
  status_response: requests.Response = session.get(url, allow_redirects=False)
1234
1372
  self._record_elapsed(status_response.elapsed)
@@ -1236,6 +1374,8 @@ class Connector(BaseConnector):
1236
1374
  data = status_response.json()
1237
1375
  if session.cache is not None:
1238
1376
  session.cache[url] = (data, str(datetime.utcnow()))
1377
+ elif status_response.status_code == requests.codes['no_content'] and allow_empty:
1378
+ data = None
1239
1379
  elif status_response.status_code == requests.codes['too_many_requests']:
1240
1380
  raise TooManyRequestsError('Could not fetch data due to too many requests from your account. '
1241
1381
  f'Status Code was: {status_response.status_code}')
@@ -1302,10 +1442,20 @@ class Connector(BaseConnector):
1302
1442
  raise SetterError(f'Unknown temperature unit {temperature_attribute.unit}')
1303
1443
 
1304
1444
  url = f'https://mysmob.api.connect.skoda-auto.cz/api/v2/air-conditioning/{vin}/settings/target-temperature'
1305
- settings_response: requests.Response = self.session.post(url, data=json.dumps(setting_dict), allow_redirects=True)
1306
- if settings_response.status_code != requests.codes['accepted']:
1307
- LOG.error('Could not set target temperature (%s)', settings_response.status_code)
1308
- 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
1309
1459
  return target_temperature
1310
1460
 
1311
1461
  def __on_air_conditioning_at_unlock_change(self, at_unlock_attribute: BooleanAttribute, at_unlock_value: bool) -> bool:
@@ -1321,10 +1471,20 @@ class Connector(BaseConnector):
1321
1471
  setting_dict['airConditioningAtUnlockEnabled'] = at_unlock_value
1322
1472
 
1323
1473
  url = f'https://mysmob.api.connect.skoda-auto.cz/api/v2/air-conditioning/{vin}/settings/ac-at-unlock'
1324
- settings_response: requests.Response = self.session.post(url, data=json.dumps(setting_dict), allow_redirects=True)
1325
- if settings_response.status_code != requests.codes['accepted']:
1326
- LOG.error('Could not set air conditioning at unlock (%s)', settings_response.status_code)
1327
- 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
1328
1488
  return at_unlock_value
1329
1489
 
1330
1490
  def __on_air_conditioning_window_heating_change(self, window_heating_attribute: BooleanAttribute, window_heating_value: bool) -> bool:
@@ -1340,10 +1500,20 @@ class Connector(BaseConnector):
1340
1500
  setting_dict['windowHeatingEnabled'] = window_heating_value
1341
1501
 
1342
1502
  url = f'https://mysmob.api.connect.skoda-auto.cz/api/v2/air-conditioning/{vin}/settings/ac-at-unlock'
1343
- settings_response: requests.Response = self.session.post(url, data=json.dumps(setting_dict), allow_redirects=True)
1344
- if settings_response.status_code != requests.codes['accepted']:
1345
- LOG.error('Could not set air conditioning window heating (%s)', settings_response.status_code)
1346
- 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
1347
1517
  return window_heating_value
1348
1518
 
1349
1519
  def __on_air_conditioning_start_stop(self, start_stop_command: ClimatizationStartStopCommand, command_arguments: Union[str, Dict[str, Any]]) \
@@ -1360,53 +1530,66 @@ class Connector(BaseConnector):
1360
1530
  if 'command' not in command_arguments:
1361
1531
  raise CommandError('Command argument missing')
1362
1532
  command_dict = {}
1363
- if command_arguments['command'] == ClimatizationStartStopCommand.Command.START:
1364
- command_dict['heaterSource'] = 'ELECTRIC'
1365
- command_dict['targetTemperature'] = {}
1366
- if 'target_temperature' in command_arguments:
1367
- # Round target temperature to nearest 0.5
1368
- command_dict['targetTemperature']['temperatureValue'] = round(command_arguments['target_temperature'] * 2) / 2
1369
- if 'target_temperature_unit' in command_arguments:
1370
- if not isinstance(command_arguments['target_temperature_unit'], Temperature):
1371
- raise CommandError('Temperature unit is not of type Temperature')
1372
- 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:
1373
1553
  command_dict['targetTemperature']['unitInCar'] = 'CELSIUS'
1374
- 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:
1375
1565
  command_dict['targetTemperature']['unitInCar'] = 'FAHRENHEIT'
1376
- elif command_arguments['target_temperature_unit'] == Temperature.K:
1566
+ elif climatization.settings.target_temperature.unit == Temperature.K:
1377
1567
  command_dict['targetTemperature']['unitInCar'] = 'KELVIN'
1378
1568
  else:
1379
- raise CommandError(f'Unknown temperature unit {command_arguments["target_temperature_unit"]}')
1569
+ raise CommandError(f'Unknown temperature unit {climatization.settings.target_temperature.unit}')
1380
1570
  else:
1571
+ command_dict['targetTemperature']['temperatureValue'] = 25.0
1381
1572
  command_dict['targetTemperature']['unitInCar'] = 'CELSIUS'
1382
- elif start_stop_command.parent is not None and (climatization := start_stop_command.parent.parent) is not None \
1383
- and isinstance(climatization, Climatization) and climatization.settings is not None \
1384
- and climatization.settings.target_temperature is not None and climatization.settings.target_temperature.enabled \
1385
- and climatization.settings.target_temperature.value is not None: # pylint: disable=too-many-boolean-expressions
1386
- # Round target temperature to nearest 0.5
1387
- command_dict['targetTemperature']['temperatureValue'] = round(climatization.settings.target_temperature.value * 2) / 2
1388
- if climatization.settings.target_temperature.unit == Temperature.C:
1389
- command_dict['targetTemperature']['unitInCar'] = 'CELSIUS'
1390
- elif climatization.settings.target_temperature.unit == Temperature.F:
1391
- command_dict['targetTemperature']['unitInCar'] = 'FAHRENHEIT'
1392
- elif climatization.settings.target_temperature.unit == Temperature.K:
1393
- command_dict['targetTemperature']['unitInCar'] = 'KELVIN'
1394
- else:
1395
- 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)
1396
1578
  else:
1397
- command_dict['targetTemperature']['temperatureValue'] = 25.0
1398
- command_dict['targetTemperature']['unitInCar'] = 'CELSIUS'
1399
- url = f'https://mysmob.api.connect.skoda-auto.cz/api/v2/air-conditioning/{vin}/start'
1400
- command_response: requests.Response = self.session.post(url, data=json.dumps(command_dict), allow_redirects=True)
1401
- elif command_arguments['command'] == ClimatizationStartStopCommand.Command.STOP:
1402
- url = f'https://mysmob.api.connect.skoda-auto.cz/api/v2/air-conditioning/{vin}/stop'
1403
- command_response: requests.Response = self.session.post(url, allow_redirects=True)
1404
- else:
1405
- raise CommandError(f'Unknown command {command_arguments["command"]}')
1579
+ raise CommandError(f'Unknown command {command_arguments["command"]}')
1406
1580
 
1407
- if command_response.status_code != requests.codes['accepted']:
1408
- LOG.error('Could not start/stop air conditioning (%s: %s)', command_response.status_code, command_response.text)
1409
- 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
1410
1593
  return command_arguments
1411
1594
 
1412
1595
  def __on_charging_start_stop(self, start_stop_command: ChargingStartStopCommand, command_arguments: Union[str, Dict[str, Any]]) \
@@ -1422,18 +1605,29 @@ class Connector(BaseConnector):
1422
1605
  raise CommandError('VIN in object hierarchy missing')
1423
1606
  if 'command' not in command_arguments:
1424
1607
  raise CommandError('Command argument missing')
1425
- if command_arguments['command'] == ChargingStartStopCommand.Command.START:
1426
- url = f'https://mysmob.api.connect.skoda-auto.cz/api/v1/charging/{vin}/start'
1427
- command_response: requests.Response = self.session.post(url, allow_redirects=True)
1428
- elif command_arguments['command'] == ChargingStartStopCommand.Command.STOP:
1429
- url = f'https://mysmob.api.connect.skoda-auto.cz/api/v1/charging/{vin}/stop'
1430
- command_response: requests.Response = self.session.post(url, allow_redirects=True)
1431
- else:
1432
- 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"]}')
1433
1618
 
1434
- if command_response.status_code != requests.codes['accepted']:
1435
- LOG.error('Could not start/stop charging (%s: %s)', command_response.status_code, command_response.text)
1436
- 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
1437
1631
  return command_arguments
1438
1632
 
1439
1633
  def __on_honk_flash(self, honk_flash_command: HonkAndFlashCommand, command_arguments: Union[str, Dict[str, Any]]) \
@@ -1463,10 +1657,20 @@ class Connector(BaseConnector):
1463
1657
  command_dict['vehiclePosition']['longitude'] = vehicle.position.longitude.value
1464
1658
 
1465
1659
  url = f'https://mysmob.api.connect.skoda-auto.cz/api/v1/vehicle-access/{vin}/honk-and-flash'
1466
- command_response: requests.Response = self.session.post(url, data=json.dumps(command_dict), allow_redirects=True)
1467
- if command_response.status_code != requests.codes['accepted']:
1468
- LOG.error('Could not execute honk or flash command (%s: %s)', command_response.status_code, command_response.text)
1469
- 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
1470
1674
  else:
1471
1675
  raise CommandError(f'Unknown command {command_arguments["command"]}')
1472
1676
  return command_arguments
@@ -1488,19 +1692,29 @@ class Connector(BaseConnector):
1488
1692
  if 'spin' in command_arguments:
1489
1693
  command_dict['currentSpin'] = command_arguments['spin']
1490
1694
  else:
1491
- if self._spin is None:
1695
+ if self.active_config['spin'] is None:
1492
1696
  raise CommandError('S-PIN is missing, please add S-PIN to your configuration or .netrc file')
1493
- command_dict['currentSpin'] = self._spin
1697
+ command_dict['currentSpin'] = self.active_config['spin']
1494
1698
  if command_arguments['command'] == LockUnlockCommand.Command.LOCK:
1495
1699
  url = f'https://mysmob.api.connect.skoda-auto.cz/api/v1/vehicle-access/{vin}/lock'
1496
1700
  elif command_arguments['command'] == LockUnlockCommand.Command.UNLOCK:
1497
1701
  url = f'https://mysmob.api.connect.skoda-auto.cz/api/v1/vehicle-access/{vin}/unlock'
1498
1702
  else:
1499
1703
  raise CommandError(f'Unknown command {command_arguments["command"]}')
1500
- command_response: requests.Response = self.session.post(url, data=json.dumps(command_dict), allow_redirects=True)
1501
- if command_response.status_code != requests.codes['accepted']:
1502
- LOG.error('Could not execute locking command (%s: %s)', command_response.status_code, command_response.text)
1503
- 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
1504
1718
  return command_arguments
1505
1719
 
1506
1720
  def __on_wake_sleep(self, wake_sleep_command: WakeSleepCommand, command_arguments: Union[str, Dict[str, Any]]) \
@@ -1519,10 +1733,20 @@ class Connector(BaseConnector):
1519
1733
  if command_arguments['command'] == WakeSleepCommand.Command.WAKE:
1520
1734
  url = f'https://mysmob.api.connect.skoda-auto.cz/api/v1/vehicle-wakeup/{vin}?applyRequestLimiter=true'
1521
1735
 
1522
- command_response: requests.Response = self.session.post(url, data='{}', allow_redirects=True)
1523
- if command_response.status_code != requests.codes['accepted']:
1524
- LOG.error('Could not execute wake command (%s: %s)', command_response.status_code, command_response.text)
1525
- 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
1526
1750
  elif command_arguments['command'] == WakeSleepCommand.Command.SLEEP:
1527
1751
  raise CommandError('Sleep command not supported by vehicle. Vehicle will put itself to sleep')
1528
1752
  else:
@@ -1537,22 +1761,35 @@ class Connector(BaseConnector):
1537
1761
  if 'command' not in command_arguments:
1538
1762
  raise CommandError('Command argument missing')
1539
1763
  command_dict = {}
1540
- if self._spin is None:
1764
+ if self.active_config['spin'] is None:
1541
1765
  raise CommandError('S-PIN is missing, please add S-PIN to your configuration or .netrc file')
1542
1766
  if 'spin' in command_arguments:
1543
1767
  command_dict['currentSpin'] = command_arguments['spin']
1544
1768
  else:
1545
- if self._spin is None or self._spin == '':
1769
+ if self.active_config['spin'] is None or self.active_config['spin'] == '':
1546
1770
  raise CommandError('S-PIN is missing, please add S-PIN to your configuration or .netrc file')
1547
- command_dict['currentSpin'] = self._spin
1771
+ command_dict['currentSpin'] = self.active_config['spin']
1548
1772
  if command_arguments['command'] == SpinCommand.Command.VERIFY:
1549
1773
  url = 'https://mysmob.api.connect.skoda-auto.cz/api/v1/spin/verify'
1550
1774
  else:
1551
1775
  raise CommandError(f'Unknown command {command_arguments["command"]}')
1552
- command_response: requests.Response = self.session.post(url, data=json.dumps(command_dict), allow_redirects=True)
1553
- if command_response.status_code != requests.codes['ok']:
1554
- LOG.error('Could not execute spin command (%s: %s)', command_response.status_code, command_response.text)
1555
- raise CommandError(f'Could not execute spin command ({command_response.status_code}: {command_response.text})')
1556
- else:
1557
- 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
1558
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
@@ -0,0 +1,39 @@
1
+ """ User interface for the Skoda connector in the Car Connectivity application. """
2
+ from __future__ import annotations
3
+ from typing import TYPE_CHECKING
4
+
5
+ import os
6
+
7
+ import flask
8
+
9
+ from carconnectivity_connectors.base.ui.connector_ui import BaseConnectorUI
10
+
11
+ if TYPE_CHECKING:
12
+ from typing import Optional, List, Dict, Union, Literal
13
+
14
+ from carconnectivity_connectors.base.connector import BaseConnector
15
+
16
+
17
+ class ConnectorUI(BaseConnectorUI):
18
+ """
19
+ A user interface class for the Skoda connector in the Car Connectivity application.
20
+ """
21
+ def __init__(self, connector: BaseConnector):
22
+ blueprint: Optional[flask.Blueprint] = flask.Blueprint(name='skoda', import_name='carconnectivity-connector-skoda', url_prefix='/skoda',
23
+ template_folder=os.path.dirname(__file__) + '/templates')
24
+ super().__init__(connector, blueprint=blueprint)
25
+
26
+ def get_nav_items(self) -> List[Dict[Literal['text', 'url', 'sublinks', 'divider'], Union[str, List]]]:
27
+ """
28
+ Generates a list of navigation items for the Skoda connector UI.
29
+ """
30
+ return super().get_nav_items()
31
+
32
+ def get_title(self) -> str:
33
+ """
34
+ Returns the title of the connector.
35
+
36
+ Returns:
37
+ str: The title of the connector, which is "Skoda".
38
+ """
39
+ return "Skoda"
@@ -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
@@ -1,22 +0,0 @@
1
- carconnectivity_connectors/skoda/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- carconnectivity_connectors/skoda/_version.py,sha256=a3v4YSZuX3dc1rsmKxffi4xGREQvzv2JwGo2cmifdH4,408
3
- carconnectivity_connectors/skoda/capability.py,sha256=vbAKK8KKre-CndLF6_5qyWLpfa4KZHk1U-hpb6nCL5w,4225
4
- carconnectivity_connectors/skoda/charging.py,sha256=CoUOYHHUPPPldKQvv0h-qrUsoEtstR3iUx-l0IU5rNM,6798
5
- carconnectivity_connectors/skoda/climatization.py,sha256=-Nk4tO5C5_YYNQfUIUWBL7mGgR6-J0_pOZplLK8p_ms,1627
6
- carconnectivity_connectors/skoda/command_impl.py,sha256=WdgxWPgi82-UgmyFpiSZE-KHRtRjqn7CH-YX9N3bAoI,2875
7
- carconnectivity_connectors/skoda/connector.py,sha256=87k1cfXCQt8vqfc8OTudTjWQ3-nyPbwZTityRmUaFfQ,111127
8
- carconnectivity_connectors/skoda/error.py,sha256=ffxzvjmci7vtp9Q1K4DR1kBF0kTJxN5Gluci3kDBkEI,2459
9
- carconnectivity_connectors/skoda/mqtt_client.py,sha256=PHvMkNhmkP_FxHvVlXzFLJA6Q3vkFCK8jZHkfII_j74,38123
10
- carconnectivity_connectors/skoda/vehicle.py,sha256=EhrhAY41A05S2yf5YoU-uvo_alAiSdjFuAyLW318DJw,3383
11
- carconnectivity_connectors/skoda/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
- carconnectivity_connectors/skoda/auth/auth_util.py,sha256=dGLUbUre0HBsTg_Ii5vW34f8DLrCykYJYCyzEvUBBEE,4434
13
- carconnectivity_connectors/skoda/auth/my_skoda_session.py,sha256=lSh23SFJs8opjmPwHTv-KNIKDep_WY4aItSP4Zq7bT8,10396
14
- carconnectivity_connectors/skoda/auth/openid_session.py,sha256=5JfR-gS1uKpE8DD-sx5Qvw6zv-OJhzcRlt0D-cm38-Y,16832
15
- carconnectivity_connectors/skoda/auth/session_manager.py,sha256=Uf1vujuDBYUCAXhYToOsZkgbTtfmY3Qe0ICTfwomBpI,2899
16
- carconnectivity_connectors/skoda/auth/skoda_web_session.py,sha256=cjzMkzx473Sh-4RgZAQULeRRcxB1MboddldCVM_y5LE,10640
17
- carconnectivity_connectors/skoda/auth/helpers/blacklist_retry.py,sha256=f3wsiY5bpHDBxp7Va1Mv9nKJ4u3qnCHZZmDu78_AhMk,1251
18
- carconnectivity_connector_skoda-0.2a5.dist-info/LICENSE,sha256=PIwI1alwDyOfvEQHdGCm2u9uf_mGE8030xZDfun0xTo,1071
19
- carconnectivity_connector_skoda-0.2a5.dist-info/METADATA,sha256=BveQjEfxoeoepVP7gk4EOLaadbA8c1E0HvToCb-nzj0,5365
20
- carconnectivity_connector_skoda-0.2a5.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
21
- carconnectivity_connector_skoda-0.2a5.dist-info/top_level.txt,sha256=KqA8GviZsDH4PtmnwSQsz0HB_w-TWkeEHLIRNo5dTaI,27
22
- carconnectivity_connector_skoda-0.2a5.dist-info/RECORD,,