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.
- {carconnectivity_connector_skoda-0.2a5.dist-info → carconnectivity_connector_skoda-0.4.dist-info}/METADATA +2 -2
- carconnectivity_connector_skoda-0.4.dist-info/RECORD +23 -0
- {carconnectivity_connector_skoda-0.2a5.dist-info → carconnectivity_connector_skoda-0.4.dist-info}/WHEEL +1 -1
- carconnectivity_connectors/skoda/_version.py +9 -4
- carconnectivity_connectors/skoda/auth/skoda_web_session.py +2 -0
- carconnectivity_connectors/skoda/climatization.py +8 -6
- carconnectivity_connectors/skoda/command_impl.py +4 -2
- carconnectivity_connectors/skoda/connector.py +388 -151
- carconnectivity_connectors/skoda/mqtt_client.py +14 -3
- carconnectivity_connectors/skoda/ui/connector_ui.py +39 -0
- carconnectivity_connectors/skoda/vehicle.py +6 -0
- carconnectivity_connector_skoda-0.2a5.dist-info/RECORD +0 -22
- {carconnectivity_connector_skoda-0.2a5.dist-info → carconnectivity_connector_skoda-0.4.dist-info}/LICENSE +0 -0
- {carconnectivity_connector_skoda-0.2a5.dist-info → carconnectivity_connector_skoda-0.4.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: carconnectivity-connector-skoda
|
|
3
|
-
Version: 0.
|
|
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.
|
|
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,8 +1,13 @@
|
|
|
1
|
-
# file generated by
|
|
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
|
|
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.
|
|
16
|
-
__version_tuple__ = version_tuple = (0,
|
|
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
|
|
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
|
|
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:
|
|
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
|
|
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(
|
|
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
|
-
|
|
59
|
-
|
|
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,
|
|
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.
|
|
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
|
-
|
|
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.
|
|
99
|
+
self.active_config['spin'] = config['spin']
|
|
109
100
|
else:
|
|
110
|
-
self.
|
|
101
|
+
self.active_config['spin'] = None
|
|
111
102
|
|
|
112
|
-
username
|
|
113
|
-
password
|
|
114
|
-
if 'username' in
|
|
115
|
-
username =
|
|
116
|
-
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
|
|
119
|
-
|
|
109
|
+
if 'netrc' in config:
|
|
110
|
+
self.active_config['netrc'] = config['netrc']
|
|
120
111
|
else:
|
|
121
|
-
|
|
112
|
+
self.active_config['netrc'] = os.path.join(os.path.expanduser("~"), ".netrc")
|
|
122
113
|
try:
|
|
123
|
-
secrets = netrc.netrc(file=
|
|
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 {
|
|
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.
|
|
120
|
+
if self.active_config['spin'] is None and account is not None:
|
|
130
121
|
try:
|
|
131
|
-
self.
|
|
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',
|
|
136
|
-
raise AuthenticationError(f'Authentication using {
|
|
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
|
|
139
|
-
raise AuthenticationError(f'"skoda" entry was not found in {
|
|
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'{
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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,
|
|
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
|
|
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']
|
|
1004
|
-
|
|
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
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
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
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
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
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
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
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
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
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
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
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
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
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
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
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
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.
|
|
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.
|
|
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,,
|
|
File without changes
|
|
File without changes
|