carconnectivity-connector-skoda 0.1a2__py3-none-any.whl → 0.1a3__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.1a2.dist-info → carconnectivity_connector_skoda-0.1a3.dist-info}/METADATA +2 -1
- {carconnectivity_connector_skoda-0.1a2.dist-info → carconnectivity_connector_skoda-0.1a3.dist-info}/RECORD +11 -10
- carconnectivity_connectors/skoda/_version.py +1 -1
- carconnectivity_connectors/skoda/auth/my_skoda_session.py +35 -31
- carconnectivity_connectors/skoda/auth/openid_session.py +14 -2
- carconnectivity_connectors/skoda/auth/session_manager.py +7 -6
- carconnectivity_connectors/skoda/connector.py +53 -6
- carconnectivity_connectors/skoda/mqtt_client.py +405 -0
- {carconnectivity_connector_skoda-0.1a2.dist-info → carconnectivity_connector_skoda-0.1a3.dist-info}/LICENSE +0 -0
- {carconnectivity_connector_skoda-0.1a2.dist-info → carconnectivity_connector_skoda-0.1a3.dist-info}/WHEEL +0 -0
- {carconnectivity_connector_skoda-0.1a2.dist-info → carconnectivity_connector_skoda-0.1a3.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: carconnectivity-connector-skoda
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.1a3
|
|
4
4
|
Summary: CarConnectivity connector for Skoda services
|
|
5
5
|
Author: Till Steinbach
|
|
6
6
|
License: MIT License
|
|
@@ -40,6 +40,7 @@ License-File: LICENSE
|
|
|
40
40
|
Requires-Dist: carconnectivity
|
|
41
41
|
Requires-Dist: oauthlib~=3.2.2
|
|
42
42
|
Requires-Dist: requests~=2.32.3
|
|
43
|
+
Requires-Dist: jwt~=1.3.1
|
|
43
44
|
|
|
44
45
|
|
|
45
46
|
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
carconnectivity_connectors/skoda/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
carconnectivity_connectors/skoda/_version.py,sha256=
|
|
2
|
+
carconnectivity_connectors/skoda/_version.py,sha256=MxrCYTTCC48jBbLpi48mgHtAaKA57W1ry3ksdEQnH64,408
|
|
3
3
|
carconnectivity_connectors/skoda/capability.py,sha256=JlNEaisVYF8qWv0wNDHTaas36uIpTIQ3NVR69wesiYQ,4513
|
|
4
|
-
carconnectivity_connectors/skoda/connector.py,sha256=
|
|
4
|
+
carconnectivity_connectors/skoda/connector.py,sha256=UT4C3xlLMs5m245_LrMKAcnyKLmYKoIPvWj9Ln_HUzY,42728
|
|
5
|
+
carconnectivity_connectors/skoda/mqtt_client.py,sha256=zpfE5MGC6iLm1uLiLyVKo50Dw39-LVW5b7pdc8tavhk,18276
|
|
5
6
|
carconnectivity_connectors/skoda/vehicle.py,sha256=H3GRDNimMghFwFi--y9BsgoSK3pMibNf_l6SsDN6gvQ,2759
|
|
6
7
|
carconnectivity_connectors/skoda/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
8
|
carconnectivity_connectors/skoda/auth/auth_util.py,sha256=dGLUbUre0HBsTg_Ii5vW34f8DLrCykYJYCyzEvUBBEE,4434
|
|
8
|
-
carconnectivity_connectors/skoda/auth/my_skoda_session.py,sha256=
|
|
9
|
-
carconnectivity_connectors/skoda/auth/openid_session.py,sha256=
|
|
10
|
-
carconnectivity_connectors/skoda/auth/session_manager.py,sha256=
|
|
9
|
+
carconnectivity_connectors/skoda/auth/my_skoda_session.py,sha256=sKO73OjQzvZ70n0VLBj2vxkuLYiXabS11fh2XvvboGw,10306
|
|
10
|
+
carconnectivity_connectors/skoda/auth/openid_session.py,sha256=PLWSSKw9Dg7hBbhzJ_nEycNrqiG6GiEM15h2wduL8jI,16592
|
|
11
|
+
carconnectivity_connectors/skoda/auth/session_manager.py,sha256=Uf1vujuDBYUCAXhYToOsZkgbTtfmY3Qe0ICTfwomBpI,2899
|
|
11
12
|
carconnectivity_connectors/skoda/auth/skoda_web_session.py,sha256=cjzMkzx473Sh-4RgZAQULeRRcxB1MboddldCVM_y5LE,10640
|
|
12
13
|
carconnectivity_connectors/skoda/auth/helpers/blacklist_retry.py,sha256=f3wsiY5bpHDBxp7Va1Mv9nKJ4u3qnCHZZmDu78_AhMk,1251
|
|
13
|
-
carconnectivity_connector_skoda-0.
|
|
14
|
-
carconnectivity_connector_skoda-0.
|
|
15
|
-
carconnectivity_connector_skoda-0.
|
|
16
|
-
carconnectivity_connector_skoda-0.
|
|
17
|
-
carconnectivity_connector_skoda-0.
|
|
14
|
+
carconnectivity_connector_skoda-0.1a3.dist-info/LICENSE,sha256=PIwI1alwDyOfvEQHdGCm2u9uf_mGE8030xZDfun0xTo,1071
|
|
15
|
+
carconnectivity_connector_skoda-0.1a3.dist-info/METADATA,sha256=MN054LLlHhhTqKeBoJATkbvGIaRtpr6JRyY7RuZDV3c,5326
|
|
16
|
+
carconnectivity_connector_skoda-0.1a3.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
|
17
|
+
carconnectivity_connector_skoda-0.1a3.dist-info/top_level.txt,sha256=KqA8GviZsDH4PtmnwSQsz0HB_w-TWkeEHLIRNo5dTaI,27
|
|
18
|
+
carconnectivity_connector_skoda-0.1a3.dist-info/RECORD,,
|
|
@@ -26,10 +26,10 @@ from carconnectivity_connectors.skoda.auth.openid_session import AccessType
|
|
|
26
26
|
from carconnectivity_connectors.skoda.auth.skoda_web_session import SkodaWebSession
|
|
27
27
|
|
|
28
28
|
if TYPE_CHECKING:
|
|
29
|
-
from typing import
|
|
29
|
+
from typing import Set
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
LOG: logging.Logger = logging.getLogger("carconnectivity
|
|
32
|
+
LOG: logging.Logger = logging.getLogger("carconnectivity.connectors.skoda.auth")
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
class MySkodaSession(SkodaWebSession):
|
|
@@ -38,7 +38,7 @@ class MySkodaSession(SkodaWebSession):
|
|
|
38
38
|
"""
|
|
39
39
|
def __init__(self, session_user, **kwargs) -> None:
|
|
40
40
|
super(MySkodaSession, self).__init__(client_id='7f045eee-7003-4379-9968-9355ed2adb06@apps_vw-dilab_com',
|
|
41
|
-
refresh_url='https://
|
|
41
|
+
refresh_url='https://mysmob.api.connect.skoda-auto.cz/api/v1/authentication/refresh-token?tokenType=CONNECT',
|
|
42
42
|
scope='address badge birthdate cars driversLicense dealers email mileage mbb nationalIdentifier openid phone profession profile vin',
|
|
43
43
|
redirect_uri='myskoda://redirect/login/',
|
|
44
44
|
session_user=session_user,
|
|
@@ -73,7 +73,7 @@ class MySkodaSession(SkodaWebSession):
|
|
|
73
73
|
def refresh(self) -> None:
|
|
74
74
|
# refresh tokens from refresh endpoint
|
|
75
75
|
self.refresh_tokens(
|
|
76
|
-
'https://
|
|
76
|
+
'https://mysmob.api.connect.skoda-auto.cz/api/v1/authentication/refresh-token?tokenType=CONNECT',
|
|
77
77
|
)
|
|
78
78
|
|
|
79
79
|
def fetch_tokens(
|
|
@@ -133,15 +133,20 @@ class MySkodaSession(SkodaWebSession):
|
|
|
133
133
|
token = json.loads(token_response)
|
|
134
134
|
except json.decoder.JSONDecodeError as err:
|
|
135
135
|
raise TemporaryAuthenticationError('Token could not be refreshed due to temporary WeConnect failure: json could not be decoded') from err
|
|
136
|
+
found_tokens: Set[str] = set()
|
|
136
137
|
# Fix token keys, we want access_token instead of accessToken
|
|
137
138
|
if 'accessToken' in token:
|
|
139
|
+
found_tokens.add('accessToken')
|
|
138
140
|
token['access_token'] = token.pop('accessToken')
|
|
139
141
|
# Fix token keys, we want id_token instead of idToken
|
|
140
142
|
if 'idToken' in token:
|
|
143
|
+
found_tokens.add('idToken')
|
|
141
144
|
token['id_token'] = token.pop('idToken')
|
|
142
145
|
# Fix token keys, we want refresh_token instead of refreshToken
|
|
143
146
|
if 'refreshToken' in token:
|
|
147
|
+
found_tokens.add('refreshToken')
|
|
144
148
|
token['refresh_token'] = token.pop('refreshToken')
|
|
149
|
+
LOG.info(f'Found tokens in answer: {found_tokens}')
|
|
145
150
|
# generate json from fixed dict
|
|
146
151
|
fixed_token_response = to_unicode(json.dumps(token)).encode("utf-8")
|
|
147
152
|
# Let OAuthlib parse the token
|
|
@@ -185,33 +190,32 @@ class MySkodaSession(SkodaWebSession):
|
|
|
185
190
|
if not is_secure_transport(token_url):
|
|
186
191
|
raise InsecureTransportError()
|
|
187
192
|
|
|
188
|
-
# Store old refresh token in case no new one is given
|
|
189
193
|
refresh_token = refresh_token or self.refresh_token
|
|
190
194
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
195
|
+
# Generate json body for token request
|
|
196
|
+
body: str = json.dumps(
|
|
197
|
+
{
|
|
198
|
+
'token': refresh_token,
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
request_headers: CaseInsensitiveDict = self.headers # pyright: ignore reportAssignmentType
|
|
202
|
+
request_headers['accept'] = 'application/json'
|
|
203
|
+
request_headers['content-type'] = 'application/json'
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
# request tokens from token_url
|
|
207
|
+
token_response = self.post(token_url, headers=request_headers, data=body, allow_redirects=False,
|
|
208
|
+
access_type=AccessType.NONE) # pyright: ignore reportCallIssue
|
|
209
|
+
if token_response.status_code == requests.codes['ok']:
|
|
210
|
+
# parse token from response body
|
|
211
|
+
token = self.parse_from_body(token_response.text)
|
|
212
|
+
return token
|
|
213
|
+
elif token_response.status_code == requests.codes['unauthorized']:
|
|
214
|
+
LOG.info('Refreshing tokens failed: Server requests new authorization, will login now')
|
|
215
|
+
self.login()
|
|
216
|
+
return self.token
|
|
217
|
+
else:
|
|
218
|
+
raise TemporaryAuthenticationError(f'Token could not be fetched due to temporary MySkoda failure: {token_response.status_code}')
|
|
219
|
+
except ConnectionError:
|
|
220
|
+
self.login()
|
|
215
221
|
return self.token
|
|
216
|
-
else:
|
|
217
|
-
raise RetrievalError(f'Status Code from WeConnect while refreshing tokens was: {token_response.status_code}')
|
|
@@ -4,8 +4,10 @@ from typing import TYPE_CHECKING
|
|
|
4
4
|
|
|
5
5
|
from enum import Enum, auto
|
|
6
6
|
import time
|
|
7
|
+
from datetime import datetime, timezone
|
|
7
8
|
import logging
|
|
8
9
|
import requests
|
|
10
|
+
from jwt import JWT
|
|
9
11
|
|
|
10
12
|
from oauthlib.common import UNICODE_ASCII_CHARACTER_SET, generate_nonce, generate_token
|
|
11
13
|
from oauthlib.oauth2.rfc6749.parameters import parse_authorization_code_response, parse_token_response, prepare_grant_uri
|
|
@@ -22,7 +24,7 @@ from carconnectivity_connectors.volkswagen.auth.helpers.blacklist_retry import B
|
|
|
22
24
|
if TYPE_CHECKING:
|
|
23
25
|
from typing import Dict
|
|
24
26
|
|
|
25
|
-
LOG = logging.getLogger("carconnectivity
|
|
27
|
+
LOG = logging.getLogger("carconnectivity.connectors.skoda.auth")
|
|
26
28
|
|
|
27
29
|
|
|
28
30
|
class AccessType(Enum):
|
|
@@ -146,7 +148,17 @@ class OpenIDSession(requests.Session):
|
|
|
146
148
|
if self._token is not None and 'expires_in' in self._token:
|
|
147
149
|
new_token['expires_in'] = self._token['expires_in']
|
|
148
150
|
else:
|
|
149
|
-
|
|
151
|
+
if 'id_token' in new_token:
|
|
152
|
+
jwt_instance = JWT()
|
|
153
|
+
meta_data = jwt_instance.decode(new_token['id_token'], do_verify=False)
|
|
154
|
+
if 'exp' in meta_data:
|
|
155
|
+
new_token['expires_at'] = meta_data['exp']
|
|
156
|
+
expires_at = datetime.fromtimestamp(meta_data['exp'], tz=timezone.utc)
|
|
157
|
+
new_token['expires_in'] = (expires_at - datetime.now(tz=timezone.utc)).total_seconds()
|
|
158
|
+
else:
|
|
159
|
+
new_token['expires_in'] = 3600
|
|
160
|
+
else:
|
|
161
|
+
new_token['expires_in'] = 3600
|
|
150
162
|
# If expires_in is set and expires_at is not set we calculate expires_at from expires_in using the current time
|
|
151
163
|
if 'expires_in' in new_token and 'expires_at' not in new_token:
|
|
152
164
|
new_token['expires_at'] = time.time() + int(new_token.get('expires_in'))
|
|
@@ -14,7 +14,7 @@ from requests import Session
|
|
|
14
14
|
|
|
15
15
|
from carconnectivity_connectors.skoda.auth.my_skoda_session import MySkodaSession
|
|
16
16
|
|
|
17
|
-
LOG = logging.getLogger("
|
|
17
|
+
LOG = logging.getLogger("carconnectivity.connectors.skoda.auth")
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
class SessionUser():
|
|
@@ -74,8 +74,9 @@ class SessionManager():
|
|
|
74
74
|
|
|
75
75
|
def persist(self) -> None:
|
|
76
76
|
for (service, user), session in self.sessions.items():
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
if session.token is not None:
|
|
78
|
+
identifier: str = SessionManager.generate_identifier(service, user)
|
|
79
|
+
self.tokenstore[identifier] = {}
|
|
80
|
+
self.tokenstore[identifier]['token'] = session.token
|
|
81
|
+
self.tokenstore[identifier]['metadata'] = session.metadata
|
|
82
|
+
self.cache[identifier] = session.cache
|
|
@@ -23,15 +23,15 @@ from carconnectivity.attributes import BooleanAttribute, DurationAttribute
|
|
|
23
23
|
|
|
24
24
|
from carconnectivity_connectors.base.connector import BaseConnector
|
|
25
25
|
from carconnectivity_connectors.skoda.auth.session_manager import SessionManager, SessionUser, Service
|
|
26
|
+
from carconnectivity_connectors.skoda.auth.my_skoda_session import MySkodaSession
|
|
26
27
|
from carconnectivity_connectors.skoda.vehicle import SkodaVehicle, SkodaElectricVehicle, SkodaCombustionVehicle, SkodaHybridVehicle
|
|
27
28
|
from carconnectivity_connectors.skoda.capability import Capability
|
|
28
29
|
from carconnectivity_connectors.skoda._version import __version__
|
|
30
|
+
from carconnectivity_connectors.skoda.mqtt_client import SkodaMQTTClient
|
|
29
31
|
|
|
30
32
|
if TYPE_CHECKING:
|
|
31
33
|
from typing import Dict, List, Optional, Any
|
|
32
34
|
|
|
33
|
-
from requests import Session
|
|
34
|
-
|
|
35
35
|
from carconnectivity.carconnectivity import CarConnectivity
|
|
36
36
|
|
|
37
37
|
LOG: logging.Logger = logging.getLogger("carconnectivity.connectors.skoda")
|
|
@@ -50,12 +50,17 @@ class Connector(BaseConnector):
|
|
|
50
50
|
def __init__(self, connector_id: str, car_connectivity: CarConnectivity, config: Dict) -> None:
|
|
51
51
|
BaseConnector.__init__(self, connector_id=connector_id, car_connectivity=car_connectivity, config=config)
|
|
52
52
|
|
|
53
|
+
self._mqtt_client: SkodaMQTTClient = SkodaMQTTClient(skoda_connector=self)
|
|
54
|
+
|
|
53
55
|
self._background_thread: Optional[threading.Thread] = None
|
|
56
|
+
self._background_connect_thread: Optional[threading.Thread] = None
|
|
54
57
|
self._stop_event = threading.Event()
|
|
55
58
|
|
|
56
59
|
self.connected: BooleanAttribute = BooleanAttribute(name="connected", parent=self)
|
|
57
60
|
self.interval: DurationAttribute = DurationAttribute(name="interval", parent=self)
|
|
58
61
|
|
|
62
|
+
self.user_id: Optional[str] = None
|
|
63
|
+
|
|
59
64
|
# Configure logging
|
|
60
65
|
if 'log_level' in config and config['log_level'] is not None:
|
|
61
66
|
config['log_level'] = config['log_level'].upper()
|
|
@@ -115,13 +120,36 @@ class Connector(BaseConnector):
|
|
|
115
120
|
raise AuthenticationError('Username or password not provided')
|
|
116
121
|
|
|
117
122
|
self._manager: SessionManager = SessionManager(tokenstore=car_connectivity.get_tokenstore(), cache=car_connectivity.get_cache())
|
|
118
|
-
|
|
123
|
+
session: requests.Session = self._manager.get_session(Service.MY_SKODA, SessionUser(username=username, password=password))
|
|
124
|
+
if not isinstance(session, MySkodaSession):
|
|
125
|
+
raise AuthenticationError('Could not create session')
|
|
126
|
+
self._session: MySkodaSession = session
|
|
127
|
+
self._session.refresh()
|
|
119
128
|
|
|
120
129
|
self._elapsed: List[timedelta] = []
|
|
121
130
|
|
|
122
131
|
def startup(self) -> None:
|
|
132
|
+
self._stop_event.clear()
|
|
133
|
+
# Start background thread for Rest API polling
|
|
123
134
|
self._background_thread = threading.Thread(target=self._background_loop, daemon=False)
|
|
124
135
|
self._background_thread.start()
|
|
136
|
+
# Start background thread for MQTT connection
|
|
137
|
+
self._background_connect_thread = threading.Thread(target=self._background_connect_loop, daemon=False)
|
|
138
|
+
self._background_connect_thread.start()
|
|
139
|
+
# Start MQTT thread
|
|
140
|
+
self._mqtt_client.loop_start()
|
|
141
|
+
|
|
142
|
+
def _background_connect_loop(self) -> None:
|
|
143
|
+
while not self._stop_event.is_set():
|
|
144
|
+
try:
|
|
145
|
+
if not self._session.expired and self._session.access_token is not None:
|
|
146
|
+
access_token: str = self._session.access_token
|
|
147
|
+
LOG.info('Connecting to Skoda MQTT-Server')
|
|
148
|
+
self._mqtt_client.connect_client(access_token)
|
|
149
|
+
break
|
|
150
|
+
except ConnectionRefusedError as e:
|
|
151
|
+
LOG.error('Could not connect to MQTT-Server: %s, will retry in 10 seconds', e)
|
|
152
|
+
self._stop_event.wait(10)
|
|
125
153
|
|
|
126
154
|
def _background_loop(self) -> None:
|
|
127
155
|
self._stop_event.clear()
|
|
@@ -134,7 +162,6 @@ class Connector(BaseConnector):
|
|
|
134
162
|
if self.interval.value is not None:
|
|
135
163
|
interval: int = self.interval.value.total_seconds()
|
|
136
164
|
except Exception:
|
|
137
|
-
self.connected._set_value(value=False) # pylint: disable=protected-access
|
|
138
165
|
if self.interval.value is not None:
|
|
139
166
|
interval: int = self.interval.value.total_seconds()
|
|
140
167
|
raise
|
|
@@ -151,7 +178,6 @@ class Connector(BaseConnector):
|
|
|
151
178
|
LOG.error('Temporary authentification error during update (%s). Will try again after configured interval of %ss', str(err), interval)
|
|
152
179
|
self._stop_event.wait(interval)
|
|
153
180
|
else:
|
|
154
|
-
self.connected._set_value(value=True) # pylint: disable=protected-access
|
|
155
181
|
self._stop_event.wait(interval)
|
|
156
182
|
|
|
157
183
|
def persist(self) -> None:
|
|
@@ -174,6 +200,9 @@ class Connector(BaseConnector):
|
|
|
174
200
|
3. Sets the session and manager to None.
|
|
175
201
|
4. Calls the shutdown method of the base connector.
|
|
176
202
|
"""
|
|
203
|
+
self._mqtt_client.disconnect()
|
|
204
|
+
# Stop MQTT thread
|
|
205
|
+
self._mqtt_client.loop_stop()
|
|
177
206
|
# Disable and remove all vehicles managed soley by this connector
|
|
178
207
|
for vehicle in self.car_connectivity.garage.list_vehicles():
|
|
179
208
|
if len(vehicle.managing_connectors) == 1 and self in vehicle.managing_connectors:
|
|
@@ -182,6 +211,8 @@ class Connector(BaseConnector):
|
|
|
182
211
|
self._stop_event.set()
|
|
183
212
|
if self._background_thread is not None:
|
|
184
213
|
self._background_thread.join()
|
|
214
|
+
if self._background_connect_thread is not None:
|
|
215
|
+
self._background_connect_thread.join()
|
|
185
216
|
self.persist()
|
|
186
217
|
self._session.close()
|
|
187
218
|
return super().shutdown()
|
|
@@ -195,6 +226,21 @@ class Connector(BaseConnector):
|
|
|
195
226
|
self.fetch_vehicles()
|
|
196
227
|
self.car_connectivity.transaction_end()
|
|
197
228
|
|
|
229
|
+
def fetch_user(self) -> None:
|
|
230
|
+
"""
|
|
231
|
+
Fetches the user data from the Skoda Connect API.
|
|
232
|
+
|
|
233
|
+
This method sends a request to the Skoda Connect API to retrieve the user data associated with the user's account.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
None
|
|
237
|
+
"""
|
|
238
|
+
url = 'https://mysmob.api.connect.skoda-auto.cz/api/v1/users'
|
|
239
|
+
data: Dict[str, Any] | None = self._fetch_data(url, session=self._session)
|
|
240
|
+
if data:
|
|
241
|
+
if 'id' in data and data['id'] is not None:
|
|
242
|
+
self.user_id = data['id']
|
|
243
|
+
|
|
198
244
|
def fetch_vehicles(self) -> None:
|
|
199
245
|
"""
|
|
200
246
|
Fetches the list of vehicles from the Skoda Connect API and updates the garage with new vehicles.
|
|
@@ -215,7 +261,7 @@ class Connector(BaseConnector):
|
|
|
215
261
|
seen_vehicle_vins.add(vehicle_dict['vin'])
|
|
216
262
|
vehicle: Optional[SkodaVehicle] = garage.get_vehicle(vehicle_dict['vin']) # pyright: ignore[reportAssignmentType]
|
|
217
263
|
if not vehicle:
|
|
218
|
-
vehicle = SkodaVehicle(vin=vehicle_dict['vin'], garage=garage)
|
|
264
|
+
vehicle = SkodaVehicle(vin=vehicle_dict['vin'], garage=garage, managing_connector=self)
|
|
219
265
|
garage.add_vehicle(vehicle_dict['vin'], vehicle)
|
|
220
266
|
|
|
221
267
|
if 'licensePlate' in vehicle_dict and vehicle_dict['licensePlate'] is not None:
|
|
@@ -363,6 +409,7 @@ class Connector(BaseConnector):
|
|
|
363
409
|
|
|
364
410
|
log_extra_keys(LOG_API, f'{drive_id}EngineRange', range_data[f'{drive_id}EngineRange'], {'engineType',
|
|
365
411
|
'currentSoCInPercent',
|
|
412
|
+
'currentFuelLevelInPercent',
|
|
366
413
|
'remainingRangeInKm'})
|
|
367
414
|
log_extra_keys(LOG_API, '/api/v2/vehicle-status/{vin}/driving-range', range_data, {'carCapturedTimestamp',
|
|
368
415
|
'carType',
|
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
"""Module implements the MQTT client."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import uuid
|
|
7
|
+
import ssl
|
|
8
|
+
|
|
9
|
+
from paho.mqtt.client import Client
|
|
10
|
+
from paho.mqtt.enums import MQTTProtocolVersion, CallbackAPIVersion, MQTTErrorCode
|
|
11
|
+
|
|
12
|
+
from carconnectivity.observable import Observable
|
|
13
|
+
from carconnectivity.vehicle import GenericVehicle
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from typing import Set
|
|
18
|
+
|
|
19
|
+
from carconnectivity_connectors.skoda.connector import Connector
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
LOG: logging.Logger = logging.getLogger("carconnectivity.connectors.skoda.mqtt")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SkodaMQTTClient(Client): # pylint: disable=too-many-instance-attributes
|
|
26
|
+
"""
|
|
27
|
+
MQTT client for the myskoda event push service.
|
|
28
|
+
"""
|
|
29
|
+
def __init__(self, skoda_connector: Connector) -> None:
|
|
30
|
+
super().__init__(callback_api_version=CallbackAPIVersion.VERSION2,
|
|
31
|
+
client_id="Id" + str(uuid.uuid4()) + "#" + str(uuid.uuid4()),
|
|
32
|
+
transport="tcp",
|
|
33
|
+
protocol=MQTTProtocolVersion.MQTTv311,
|
|
34
|
+
reconnect_on_failure=True,
|
|
35
|
+
clean_session=True)
|
|
36
|
+
self._skoda_connector: Connector = skoda_connector
|
|
37
|
+
|
|
38
|
+
self.username = 'android-app'
|
|
39
|
+
|
|
40
|
+
self.on_connect = self._on_connect_callback
|
|
41
|
+
self.on_message = self._on_message_callback
|
|
42
|
+
self.on_disconnect = self._on_disconnect_callback
|
|
43
|
+
self.on_subscribe = self._on_subscribe_callback
|
|
44
|
+
self.subscribed_topics: Set[str] = set()
|
|
45
|
+
|
|
46
|
+
def connect_client(self, access_token: str) -> MQTTErrorCode:
|
|
47
|
+
"""
|
|
48
|
+
Connects the MQTT client using the provided access token.
|
|
49
|
+
|
|
50
|
+
This method sets the client's password to the provided access token,
|
|
51
|
+
configures TLS settings, and attempts to connect to the MQTT broker.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
access_token (str): The access token used as the password for the MQTT client.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
MQTTErrorCode: The result of the connection attempt.
|
|
58
|
+
"""
|
|
59
|
+
self.password = access_token # pylint: disable=attribute-defined-outside-init # this is a false positive, password has a setter in super class
|
|
60
|
+
self.tls_set(cert_reqs=ssl.CERT_NONE)
|
|
61
|
+
return super().connect(host='mqtt.messagehub.de', port=8883, keepalive=60)
|
|
62
|
+
|
|
63
|
+
def _on_carconnectivity_vehicle_enabled(self, element, flags):
|
|
64
|
+
"""
|
|
65
|
+
Handles the event when a vehicle is enabled or disabled in the car connectivity system.
|
|
66
|
+
|
|
67
|
+
This method is triggered when the state of a vehicle changes. It subscribes to the vehicle
|
|
68
|
+
if it is enabled and unsubscribes if it is disabled.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
element: The element whose state has changed.
|
|
72
|
+
flags (Observable.ObserverEvent): The event flags indicating the state change.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
None
|
|
76
|
+
"""
|
|
77
|
+
if (flags & Observable.ObserverEvent.ENABLED) and isinstance(element, GenericVehicle):
|
|
78
|
+
self._subscribe_vehicle(element)
|
|
79
|
+
elif (flags & Observable.ObserverEvent.DISABLED) and isinstance(element, GenericVehicle):
|
|
80
|
+
self._subscribe_vehicle(element)
|
|
81
|
+
|
|
82
|
+
def _subscribe_vehicles(self) -> None:
|
|
83
|
+
"""
|
|
84
|
+
Subscribes to all vehicles the connector is responsible for.
|
|
85
|
+
|
|
86
|
+
This method iterates through the list of vehicles in the carconnectivity
|
|
87
|
+
garage and subscribes to eliable vehicles by calling the _subscribe_vehicle method.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
None
|
|
91
|
+
"""
|
|
92
|
+
for vehicle in self._skoda_connector.car_connectivity.garage.list_vehicles():
|
|
93
|
+
self._subscribe_vehicle(vehicle)
|
|
94
|
+
|
|
95
|
+
def _unsubscribe_vehicles(self) -> None:
|
|
96
|
+
"""
|
|
97
|
+
Unsubscribes from all vehicles the client is subscribed for.
|
|
98
|
+
|
|
99
|
+
This method iterates through the list of vehicles in the garage and
|
|
100
|
+
unsubscribes from each one by calling the _unsubscribe_vehicle method.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
None
|
|
104
|
+
"""
|
|
105
|
+
for vehicle in self._skoda_connector.car_connectivity.garage.list_vehicles():
|
|
106
|
+
self._unsubscribe_vehicle(vehicle)
|
|
107
|
+
|
|
108
|
+
def _subscribe_vehicle(self, vehicle: GenericVehicle) -> None:
|
|
109
|
+
"""
|
|
110
|
+
Subscribes to MQTT topics for a given vehicle.
|
|
111
|
+
|
|
112
|
+
This method subscribes to various MQTT topics related to the vehicle's
|
|
113
|
+
account events, operation requests, and service events. It ensures that
|
|
114
|
+
the user ID is fetched if not already available and checks if the vehicle
|
|
115
|
+
has a valid VIN before subscribing.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
vehicle (GenericVehicle): The vehicle object containing VIN and other
|
|
119
|
+
relevant information.
|
|
120
|
+
|
|
121
|
+
Raises:
|
|
122
|
+
None
|
|
123
|
+
|
|
124
|
+
Logs:
|
|
125
|
+
- Warnings if the vehicle does not have a VIN.
|
|
126
|
+
- Info messages upon successful subscription to a topic.
|
|
127
|
+
- Error messages if subscription to a topic fails.
|
|
128
|
+
"""
|
|
129
|
+
# to subscribe the user_id must be known
|
|
130
|
+
if self._skoda_connector.user_id is None:
|
|
131
|
+
self._skoda_connector.fetch_user()
|
|
132
|
+
# Can only subscribe with user_id
|
|
133
|
+
if self._skoda_connector.user_id is not None:
|
|
134
|
+
user_id: str = self._skoda_connector.user_id
|
|
135
|
+
if not vehicle.vin.enabled or vehicle.vin.value is None:
|
|
136
|
+
LOG.warning('Could not subscribe to vehicle without vin')
|
|
137
|
+
else:
|
|
138
|
+
vin: str = vehicle.vin.value
|
|
139
|
+
# If the skoda connector is managing this vehicle
|
|
140
|
+
if self._skoda_connector in vehicle.managing_connectors:
|
|
141
|
+
account_events: Set[str] = {'privacy'}
|
|
142
|
+
operation_requests: Set[str] = {
|
|
143
|
+
'air-conditioning/set-air-conditioning-at-unlock',
|
|
144
|
+
'air-conditioning/set-air-conditioning-seats-heating',
|
|
145
|
+
'air-conditioning/set-air-conditioning-timers',
|
|
146
|
+
'air-conditioning/set-air-conditioning-without-external-power',
|
|
147
|
+
'air-conditioning/set-target-temperature',
|
|
148
|
+
'air-conditioning/start-stop-air-conditioning',
|
|
149
|
+
'auxiliary-heating/start-stop-auxiliary-heating',
|
|
150
|
+
'air-conditioning/start-stop-window-heating',
|
|
151
|
+
'air-conditioning/windows-heating',
|
|
152
|
+
'charging/start-stop-charging',
|
|
153
|
+
'charging/update-battery-support',
|
|
154
|
+
'charging/update-auto-unlock-plug',
|
|
155
|
+
'charging/update-care-mode',
|
|
156
|
+
'charging/update-charge-limit',
|
|
157
|
+
'charging/update-charge-mode',
|
|
158
|
+
'charging/update-charging-profiles',
|
|
159
|
+
'charging/update-charging-current',
|
|
160
|
+
'departure/update-departure-timers',
|
|
161
|
+
'departure/update-minimal-soc',
|
|
162
|
+
'vehicle-access/honk-and-flash',
|
|
163
|
+
'vehicle-access/lock-vehicle',
|
|
164
|
+
'vehicle-services-backup/apply-backup',
|
|
165
|
+
'vehicle-wakeup/wakeup'
|
|
166
|
+
}
|
|
167
|
+
service_events: Set[str] = {
|
|
168
|
+
'air-conditioning',
|
|
169
|
+
'charging',
|
|
170
|
+
'departure',
|
|
171
|
+
'vehicle-status/access',
|
|
172
|
+
'vehicle-status/lights'
|
|
173
|
+
}
|
|
174
|
+
possible_topics: Set[str] = set()
|
|
175
|
+
# Compile all possible topics
|
|
176
|
+
for event in account_events:
|
|
177
|
+
possible_topics.add(f'{user_id}/{vin}/account-event/{event}')
|
|
178
|
+
for event in operation_requests:
|
|
179
|
+
possible_topics.add(f'{user_id}/{vin}/operation-request/{event}')
|
|
180
|
+
for event in service_events:
|
|
181
|
+
possible_topics.add(f'{user_id}/{vin}/service-event/{event}')
|
|
182
|
+
|
|
183
|
+
# Subscribe to all topics
|
|
184
|
+
for topic in possible_topics:
|
|
185
|
+
if topic not in self.subscribed_topics:
|
|
186
|
+
mqtt_err, mid = self.subscribe(topic)
|
|
187
|
+
if mqtt_err == MQTTErrorCode.MQTT_ERR_SUCCESS:
|
|
188
|
+
self.subscribed_topics.add(topic)
|
|
189
|
+
LOG.debug('Subscribe to topic %s with %d', topic, mid)
|
|
190
|
+
else:
|
|
191
|
+
LOG.error('Could not subscribe to topic %s (%s)', topic, mqtt_err)
|
|
192
|
+
else:
|
|
193
|
+
LOG.warning('Could not subscribe to vehicle without user_id')
|
|
194
|
+
|
|
195
|
+
def _unsubscribe_vehicle(self, vehicle: GenericVehicle) -> None:
|
|
196
|
+
"""
|
|
197
|
+
Unsubscribe from all MQTT topics related to a specific vehicle.
|
|
198
|
+
|
|
199
|
+
This method checks if the vehicle's VIN (Vehicle Identification Number) is enabled and not None.
|
|
200
|
+
If the VIN is valid, it iterates through the list of subscribed topics and unsubscribes from
|
|
201
|
+
any topic that contains the VIN. It also removes the topic from the list of subscribed topics
|
|
202
|
+
and logs the unsubscription.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
vehicle (GenericVehicle): The vehicle object containing the VIN information.
|
|
206
|
+
|
|
207
|
+
Raises:
|
|
208
|
+
None
|
|
209
|
+
|
|
210
|
+
Logs:
|
|
211
|
+
- Warning if the vehicle's VIN is not enabled or is None.
|
|
212
|
+
- Info for each topic successfully unsubscribed.
|
|
213
|
+
"""
|
|
214
|
+
if not vehicle.vin.enabled or vehicle.vin.value is None:
|
|
215
|
+
LOG.warning('Could not unsubscribe to vehicle without vin')
|
|
216
|
+
else:
|
|
217
|
+
vin: str = vehicle.vin.value
|
|
218
|
+
for topic in self.subscribed_topics:
|
|
219
|
+
if vin in topic:
|
|
220
|
+
self.unsubscribe(topic)
|
|
221
|
+
self.subscribed_topics.remove(topic)
|
|
222
|
+
LOG.debug('Unsubscribed from topic %s', topic)
|
|
223
|
+
|
|
224
|
+
def _on_connect_callback(self, mqttc, obj, flags, reason_code, properties) -> None:
|
|
225
|
+
"""
|
|
226
|
+
Callback function that is called when the MQTT client connects to the broker.
|
|
227
|
+
|
|
228
|
+
It registers a callback to observe new vehicles being added and subscribes MQTT topics for all vehicles
|
|
229
|
+
handled by this connector.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
mqttc: The MQTT client instance (unused).
|
|
233
|
+
obj: User-defined object passed to the callback (unused).
|
|
234
|
+
flags: Response flags sent by the broker (unused).
|
|
235
|
+
reason_code: The connection result code.
|
|
236
|
+
properties: MQTT v5 properties (unused).
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
None
|
|
240
|
+
|
|
241
|
+
The function logs the connection status and handles different reason codes:
|
|
242
|
+
- 0: Connection successful.
|
|
243
|
+
- 128: Unspecified error.
|
|
244
|
+
- 129: Malformed packet.
|
|
245
|
+
- 130: Protocol error.
|
|
246
|
+
- 131: Implementation specific error.
|
|
247
|
+
- 132: Unsupported protocol version.
|
|
248
|
+
- 133: Client identifier not valid.
|
|
249
|
+
- 134: Bad user name or password.
|
|
250
|
+
- 135: Not authorized.
|
|
251
|
+
- 136: Server unavailable.
|
|
252
|
+
- 137: Server busy. Retrying.
|
|
253
|
+
- 138: Banned.
|
|
254
|
+
- 140: Bad authentication method.
|
|
255
|
+
- 144: Topic name invalid.
|
|
256
|
+
- 149: Packet too large.
|
|
257
|
+
- 151: Quota exceeded.
|
|
258
|
+
- 154: Retain not supported.
|
|
259
|
+
- 155: QoS not supported.
|
|
260
|
+
- 156: Use another server.
|
|
261
|
+
- 157: Server move.
|
|
262
|
+
- 159: Connection rate exceeded.
|
|
263
|
+
- Other: Generic connection error.
|
|
264
|
+
"""
|
|
265
|
+
del mqttc # unused
|
|
266
|
+
del obj # unused
|
|
267
|
+
del flags # unused
|
|
268
|
+
del properties
|
|
269
|
+
# reason_code 0 means success
|
|
270
|
+
if reason_code == 0:
|
|
271
|
+
LOG.info('Connected to MQTT broker')
|
|
272
|
+
self._skoda_connector.connected._set_value(value=True) # pylint: disable=protected-access
|
|
273
|
+
observer_flags: Observable.ObserverEvent = Observable.ObserverEvent.ENABLED | Observable.ObserverEvent.DISABLED
|
|
274
|
+
self._skoda_connector.car_connectivity.garage.add_observer(observer=self._on_carconnectivity_vehicle_enabled,
|
|
275
|
+
flag=observer_flags,
|
|
276
|
+
priority=Observable.ObserverPriority.USER_MID)
|
|
277
|
+
self._subscribe_vehicles()
|
|
278
|
+
|
|
279
|
+
# Handle different reason codes
|
|
280
|
+
elif reason_code == 128:
|
|
281
|
+
LOG.error('Could not connect (%s): Unspecified error', reason_code)
|
|
282
|
+
elif reason_code == 129:
|
|
283
|
+
LOG.error('Could not connect (%s): Malformed packet', reason_code)
|
|
284
|
+
elif reason_code == 130:
|
|
285
|
+
LOG.error('Could not connect (%s): Protocol error', reason_code)
|
|
286
|
+
elif reason_code == 131:
|
|
287
|
+
LOG.error('Could not connect (%s): Implementation specific error', reason_code)
|
|
288
|
+
elif reason_code == 132:
|
|
289
|
+
LOG.error('Could not connect (%s): Unsupported protocol version', reason_code)
|
|
290
|
+
elif reason_code == 133:
|
|
291
|
+
LOG.error('Could not connect (%s): Client identifier not valid', reason_code)
|
|
292
|
+
elif reason_code == 134:
|
|
293
|
+
LOG.error('Could not connect (%s): Bad user name or password', reason_code)
|
|
294
|
+
elif reason_code == 135:
|
|
295
|
+
LOG.error('Could not connect (%s): Not authorized', reason_code)
|
|
296
|
+
elif reason_code == 136:
|
|
297
|
+
LOG.error('Could not connect (%s): Server unavailable', reason_code)
|
|
298
|
+
elif reason_code == 137:
|
|
299
|
+
LOG.error('Could not connect (%s): Server busy. Retrying', reason_code)
|
|
300
|
+
elif reason_code == 138:
|
|
301
|
+
LOG.error('Could not connect (%s): Banned', reason_code)
|
|
302
|
+
elif reason_code == 140:
|
|
303
|
+
LOG.error('Could not connect (%s): Bad authentication method', reason_code)
|
|
304
|
+
elif reason_code == 144:
|
|
305
|
+
LOG.error('Could not connect (%s): Topic name invalid', reason_code)
|
|
306
|
+
elif reason_code == 149:
|
|
307
|
+
LOG.error('Could not connect (%s): Packet too large', reason_code)
|
|
308
|
+
elif reason_code == 151:
|
|
309
|
+
LOG.error('Could not connect (%s): Quota exceeded', reason_code)
|
|
310
|
+
elif reason_code == 154:
|
|
311
|
+
LOG.error('Could not connect (%s): Retain not supported', reason_code)
|
|
312
|
+
elif reason_code == 155:
|
|
313
|
+
LOG.error('Could not connect (%s): QoS not supported', reason_code)
|
|
314
|
+
elif reason_code == 156:
|
|
315
|
+
LOG.error('Could not connect (%s): Use another server', reason_code)
|
|
316
|
+
elif reason_code == 157:
|
|
317
|
+
LOG.error('Could not connect (%s): Server move', reason_code)
|
|
318
|
+
elif reason_code == 159:
|
|
319
|
+
LOG.error('Could not connect (%s): Connection rate exceeded', reason_code)
|
|
320
|
+
else:
|
|
321
|
+
LOG.error('Could not connect (%s)', reason_code)
|
|
322
|
+
|
|
323
|
+
def _on_disconnect_callback(self, client, userdata, flags, reason_code, properties) -> None:
|
|
324
|
+
"""
|
|
325
|
+
Callback function that is called when the MQTT client disconnects.
|
|
326
|
+
|
|
327
|
+
This function handles the disconnection of the MQTT client and logs the appropriate
|
|
328
|
+
messages based on the reason code for the disconnection. It also removes the observer
|
|
329
|
+
from the garage to not get any notifications for vehicles being added or removed.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
client: The MQTT client instance that disconnected.
|
|
333
|
+
userdata: The private user data as set in Client() or userdata_set().
|
|
334
|
+
flags: Response flags sent by the broker.
|
|
335
|
+
reason_code: The reason code for the disconnection.
|
|
336
|
+
properties: The properties associated with the disconnection.
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
None
|
|
340
|
+
"""
|
|
341
|
+
del client
|
|
342
|
+
del properties
|
|
343
|
+
del flags
|
|
344
|
+
|
|
345
|
+
self._skoda_connector.connected._set_value(value=False) # pylint: disable=protected-access
|
|
346
|
+
self._skoda_connector.car_connectivity.garage.remove_observer(observer=self._on_carconnectivity_vehicle_enabled)
|
|
347
|
+
|
|
348
|
+
if reason_code == 0:
|
|
349
|
+
LOG.info('Client successfully disconnected')
|
|
350
|
+
elif reason_code == 4:
|
|
351
|
+
LOG.info('Client successfully disconnected: %s', userdata)
|
|
352
|
+
elif reason_code == 137:
|
|
353
|
+
LOG.error('Client disconnected: Server busy')
|
|
354
|
+
elif reason_code == 139:
|
|
355
|
+
LOG.error('Client disconnected: Server shutting down')
|
|
356
|
+
elif reason_code == 160:
|
|
357
|
+
LOG.error('Client disconnected: Maximum connect time')
|
|
358
|
+
else:
|
|
359
|
+
LOG.error('Client unexpectedly disconnected (%s), trying to reconnect', reason_code)
|
|
360
|
+
|
|
361
|
+
def _on_subscribe_callback(self, mqttc, obj, mid, reason_codes, properties) -> None:
|
|
362
|
+
"""
|
|
363
|
+
Callback function for MQTT subscription.
|
|
364
|
+
|
|
365
|
+
This method is called when the client receives a SUBACK response from the server.
|
|
366
|
+
It checks the reason codes to determine if the subscription was successful.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
mqttc: The MQTT client instance (unused).
|
|
370
|
+
obj: User-defined data of any type (unused).
|
|
371
|
+
mid: The message ID of the subscribe request.
|
|
372
|
+
reason_codes: A list of reason codes indicating the result of the subscription.
|
|
373
|
+
properties: MQTT v5.0 properties (unused).
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
None
|
|
377
|
+
"""
|
|
378
|
+
del mqttc # unused
|
|
379
|
+
del obj # unused
|
|
380
|
+
del properties # unused
|
|
381
|
+
if any(x in [0, 1, 2] for x in reason_codes):
|
|
382
|
+
LOG.debug('sucessfully subscribed to topic of mid %d', mid)
|
|
383
|
+
else:
|
|
384
|
+
LOG.error('Subscribe was not successfull (%s)', ', '.join(reason_codes))
|
|
385
|
+
|
|
386
|
+
def _on_message_callback(self, mqttc, obj, msg) -> None: # noqa: C901
|
|
387
|
+
"""
|
|
388
|
+
Callback function for handling incoming MQTT messages.
|
|
389
|
+
|
|
390
|
+
This function is called when a message is received on a subscribed topic.
|
|
391
|
+
It logs an error message indicating that the message is not understood.
|
|
392
|
+
In the next step this needs to be implemented with real behaviour.
|
|
393
|
+
|
|
394
|
+
Args:
|
|
395
|
+
mqttc: The MQTT client instance (unused).
|
|
396
|
+
obj: The user data (unused).
|
|
397
|
+
msg: The MQTT message instance containing topic and payload.
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
None
|
|
401
|
+
"""
|
|
402
|
+
del mqttc # unused
|
|
403
|
+
del obj # unused
|
|
404
|
+
error_message = f'I don\'t understand message {msg.topic}: {msg.payload}'
|
|
405
|
+
LOG.info(error_message)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|