pymammotion 0.5.34__py3-none-any.whl → 0.5.44__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 pymammotion might be problematic. Click here for more details.
- pymammotion/__init__.py +3 -3
- pymammotion/aliyun/cloud_gateway.py +106 -18
- pymammotion/aliyun/model/dev_by_account_response.py +198 -20
- pymammotion/const.py +3 -0
- pymammotion/data/model/device.py +1 -0
- pymammotion/data/model/device_config.py +1 -1
- pymammotion/data/model/enums.py +5 -3
- pymammotion/data/model/generate_route_information.py +2 -2
- pymammotion/data/model/hash_list.py +113 -33
- pymammotion/data/model/region_data.py +4 -4
- pymammotion/data/{state_manager.py → mower_state_manager.py} +17 -7
- pymammotion/data/mqtt/event.py +47 -22
- pymammotion/data/mqtt/mammotion_properties.py +257 -0
- pymammotion/data/mqtt/properties.py +32 -29
- pymammotion/data/mqtt/status.py +17 -16
- pymammotion/homeassistant/__init__.py +3 -0
- pymammotion/homeassistant/mower_api.py +446 -0
- pymammotion/homeassistant/rtk_api.py +54 -0
- pymammotion/http/http.py +387 -13
- pymammotion/http/model/http.py +82 -2
- pymammotion/http/model/response_factory.py +10 -4
- pymammotion/mammotion/commands/mammotion_command.py +6 -0
- pymammotion/mammotion/commands/messages/navigation.py +10 -6
- pymammotion/mammotion/devices/__init__.py +27 -3
- pymammotion/mammotion/devices/base.py +16 -138
- pymammotion/mammotion/devices/mammotion.py +364 -204
- pymammotion/mammotion/devices/mammotion_bluetooth.py +7 -5
- pymammotion/mammotion/devices/mammotion_cloud.py +42 -83
- pymammotion/mammotion/devices/mammotion_mower_ble.py +49 -0
- pymammotion/mammotion/devices/mammotion_mower_cloud.py +39 -0
- pymammotion/mammotion/devices/managers/managers.py +81 -0
- pymammotion/mammotion/devices/mower_device.py +121 -0
- pymammotion/mammotion/devices/mower_manager.py +107 -0
- pymammotion/mammotion/devices/rtk_ble.py +89 -0
- pymammotion/mammotion/devices/rtk_cloud.py +113 -0
- pymammotion/mammotion/devices/rtk_device.py +50 -0
- pymammotion/mammotion/devices/rtk_manager.py +122 -0
- pymammotion/mqtt/__init__.py +2 -1
- pymammotion/mqtt/aliyun_mqtt.py +232 -0
- pymammotion/mqtt/mammotion_mqtt.py +174 -192
- pymammotion/mqtt/mqtt_models.py +66 -0
- pymammotion/proto/__init__.py +1 -1
- pymammotion/proto/mctrl_nav.proto +1 -1
- pymammotion/proto/mctrl_nav_pb2.py +1 -1
- pymammotion/proto/mctrl_nav_pb2.pyi +4 -4
- pymammotion/proto/mctrl_sys.proto +1 -1
- pymammotion/utility/datatype_converter.py +13 -12
- pymammotion/utility/device_type.py +88 -3
- pymammotion/utility/mur_mur_hash.py +132 -87
- {pymammotion-0.5.34.dist-info → pymammotion-0.5.44.dist-info}/METADATA +25 -31
- {pymammotion-0.5.34.dist-info → pymammotion-0.5.44.dist-info}/RECORD +59 -45
- {pymammotion-0.5.34.dist-info → pymammotion-0.5.44.dist-info}/WHEEL +1 -1
- pymammotion/http/_init_.py +0 -0
- {pymammotion-0.5.34.dist-info → pymammotion-0.5.44.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"""MammotionMQTT."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import base64
|
|
5
|
+
from collections.abc import Awaitable, Callable
|
|
6
|
+
import hashlib
|
|
7
|
+
import hmac
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
from logging import getLogger
|
|
11
|
+
|
|
12
|
+
import betterproto2
|
|
13
|
+
from paho.mqtt.client import MQTTMessage
|
|
14
|
+
|
|
15
|
+
from pymammotion.aliyun.cloud_gateway import CloudIOTGateway
|
|
16
|
+
from pymammotion.data.mqtt.event import ThingEventMessage
|
|
17
|
+
from pymammotion.data.mqtt.properties import ThingPropertiesMessage
|
|
18
|
+
from pymammotion.data.mqtt.status import ThingStatusMessage
|
|
19
|
+
from pymammotion.mqtt.linkkit.linkkit import LinkKit
|
|
20
|
+
from pymammotion.proto import LubaMsg
|
|
21
|
+
|
|
22
|
+
logger = getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class AliyunMQTT:
|
|
26
|
+
"""MQTT client for pymammotion."""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
region_id: str,
|
|
31
|
+
product_key: str,
|
|
32
|
+
device_name: str,
|
|
33
|
+
device_secret: str,
|
|
34
|
+
iot_token: str,
|
|
35
|
+
cloud_client: CloudIOTGateway,
|
|
36
|
+
client_id: str | None = None,
|
|
37
|
+
) -> None:
|
|
38
|
+
"""Create instance of MammotionMQTT."""
|
|
39
|
+
super().__init__()
|
|
40
|
+
self._cloud_client = cloud_client
|
|
41
|
+
self.is_connected = False
|
|
42
|
+
self.is_ready = False
|
|
43
|
+
self.on_connected: Callable[[], Awaitable[None]] | None = None
|
|
44
|
+
self.on_ready: Callable[[], Awaitable[None]] | None = None
|
|
45
|
+
self.on_error: Callable[[str], Awaitable[None]] | None = None
|
|
46
|
+
self.on_disconnected: Callable[[], Awaitable[None]] | None = None
|
|
47
|
+
self.on_message: Callable[[str, str, str], Awaitable[None]] | None = None
|
|
48
|
+
|
|
49
|
+
self._product_key = product_key
|
|
50
|
+
self._device_name = device_name
|
|
51
|
+
self._device_secret = device_secret
|
|
52
|
+
self._iot_token = iot_token
|
|
53
|
+
self._mqtt_username = f"{device_name}&{product_key}"
|
|
54
|
+
# linkkit provides the correct MQTT service for all of this and uses paho under the hood
|
|
55
|
+
if client_id is None:
|
|
56
|
+
client_id = f"python-{device_name}"
|
|
57
|
+
self._mqtt_client_id = f"{client_id}|securemode=2,signmethod=hmacsha1|"
|
|
58
|
+
sign_content = f"clientId{client_id}deviceName{device_name}productKey{product_key}"
|
|
59
|
+
self._mqtt_password = hmac.new(
|
|
60
|
+
device_secret.encode("utf-8"), sign_content.encode("utf-8"), hashlib.sha1
|
|
61
|
+
).hexdigest()
|
|
62
|
+
|
|
63
|
+
self._client_id = client_id
|
|
64
|
+
self.loop = asyncio.get_running_loop()
|
|
65
|
+
|
|
66
|
+
self._linkkit_client = LinkKit(
|
|
67
|
+
region_id,
|
|
68
|
+
product_key,
|
|
69
|
+
device_name,
|
|
70
|
+
device_secret,
|
|
71
|
+
auth_type="",
|
|
72
|
+
client_id=client_id,
|
|
73
|
+
password=self._mqtt_password,
|
|
74
|
+
username=self._mqtt_username,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
self._linkkit_client.enable_logger(level=logging.ERROR)
|
|
78
|
+
self._linkkit_client.on_connect = self._thing_on_connect
|
|
79
|
+
self._linkkit_client.on_disconnect = self._on_disconnect
|
|
80
|
+
self._linkkit_client.on_thing_enable = self._thing_on_thing_enable
|
|
81
|
+
self._linkkit_client.on_topic_message = self._thing_on_topic_message
|
|
82
|
+
self._mqtt_host = f"{self._product_key}.iot-as-mqtt.{region_id}.aliyuncs.com"
|
|
83
|
+
|
|
84
|
+
def connect_async(self) -> None:
|
|
85
|
+
"""Connect async to MQTT Server."""
|
|
86
|
+
logger.info("Connecting...")
|
|
87
|
+
if self._linkkit_client.check_state() is LinkKit.LinkKitState.INITIALIZED:
|
|
88
|
+
self._linkkit_client.thing_setup()
|
|
89
|
+
self._linkkit_client.connect_async()
|
|
90
|
+
|
|
91
|
+
def disconnect(self) -> None:
|
|
92
|
+
"""Disconnect from MQTT Server."""
|
|
93
|
+
logger.info("Disconnecting...")
|
|
94
|
+
|
|
95
|
+
self._linkkit_client.disconnect()
|
|
96
|
+
|
|
97
|
+
def _thing_on_thing_enable(self, user_data) -> None:
|
|
98
|
+
"""Is called when Thing is enabled."""
|
|
99
|
+
logger.debug("on_thing_enable")
|
|
100
|
+
self.is_connected = True
|
|
101
|
+
# logger.debug('subscribe_topic, topic:%s' % echo_topic)
|
|
102
|
+
# self._linkkit_client.subscribe_topic(echo_topic, 0)
|
|
103
|
+
self._linkkit_client.subscribe_topic(
|
|
104
|
+
f"/sys/{self._product_key}/{self._device_name}/app/down/account/bind_reply"
|
|
105
|
+
)
|
|
106
|
+
self._linkkit_client.subscribe_topic(
|
|
107
|
+
f"/sys/{self._product_key}/{self._device_name}/app/down/thing/event/property/post_reply"
|
|
108
|
+
)
|
|
109
|
+
self._linkkit_client.subscribe_topic(
|
|
110
|
+
f"/sys/{self._product_key}/{self._device_name}/app/down/thing/wifi/status/notify"
|
|
111
|
+
)
|
|
112
|
+
self._linkkit_client.subscribe_topic(
|
|
113
|
+
f"/sys/{self._product_key}/{self._device_name}/app/down/thing/wifi/connect/event/notify"
|
|
114
|
+
)
|
|
115
|
+
self._linkkit_client.subscribe_topic(
|
|
116
|
+
f"/sys/{self._product_key}/{self._device_name}/app/down/_thing/event/notify"
|
|
117
|
+
)
|
|
118
|
+
self._linkkit_client.subscribe_topic(f"/sys/{self._product_key}/{self._device_name}/app/down/thing/events")
|
|
119
|
+
self._linkkit_client.subscribe_topic(f"/sys/{self._product_key}/{self._device_name}/app/down/thing/status")
|
|
120
|
+
self._linkkit_client.subscribe_topic(f"/sys/{self._product_key}/{self._device_name}/app/down/thing/properties")
|
|
121
|
+
self._linkkit_client.subscribe_topic(
|
|
122
|
+
f"/sys/{self._product_key}/{self._device_name}/app/down/thing/model/down_raw"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
self._linkkit_client.publish_topic(
|
|
126
|
+
f"/sys/{self._product_key}/{self._device_name}/app/up/account/bind",
|
|
127
|
+
json.dumps(
|
|
128
|
+
{
|
|
129
|
+
"id": "msgid1",
|
|
130
|
+
"version": "1.0",
|
|
131
|
+
"request": {"clientId": self._mqtt_username},
|
|
132
|
+
"params": {"iotToken": self._iot_token},
|
|
133
|
+
}
|
|
134
|
+
),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
if self.on_ready:
|
|
138
|
+
self.is_ready = True
|
|
139
|
+
future = asyncio.run_coroutine_threadsafe(self.on_ready(), self.loop)
|
|
140
|
+
asyncio.wrap_future(future, loop=self.loop)
|
|
141
|
+
|
|
142
|
+
def unsubscribe(self) -> None:
|
|
143
|
+
self._linkkit_client.unsubscribe_topic(
|
|
144
|
+
f"/sys/{self._product_key}/{self._device_name}/app/down/account/bind_reply"
|
|
145
|
+
)
|
|
146
|
+
self._linkkit_client.unsubscribe_topic(
|
|
147
|
+
f"/sys/{self._product_key}/{self._device_name}/app/down/thing/event/property/post_reply"
|
|
148
|
+
)
|
|
149
|
+
self._linkkit_client.unsubscribe_topic(
|
|
150
|
+
f"/sys/{self._product_key}/{self._device_name}/app/down/thing/wifi/status/notify"
|
|
151
|
+
)
|
|
152
|
+
self._linkkit_client.unsubscribe_topic(
|
|
153
|
+
f"/sys/{self._product_key}/{self._device_name}/app/down/thing/wifi/connect/event/notify"
|
|
154
|
+
)
|
|
155
|
+
self._linkkit_client.unsubscribe_topic(
|
|
156
|
+
f"/sys/{self._product_key}/{self._device_name}/app/down/_thing/event/notify"
|
|
157
|
+
)
|
|
158
|
+
self._linkkit_client.unsubscribe_topic(f"/sys/{self._product_key}/{self._device_name}/app/down/thing/events")
|
|
159
|
+
self._linkkit_client.unsubscribe_topic(f"/sys/{self._product_key}/{self._device_name}/app/down/thing/status")
|
|
160
|
+
self._linkkit_client.unsubscribe_topic(
|
|
161
|
+
f"/sys/{self._product_key}/{self._device_name}/app/down/thing/properties"
|
|
162
|
+
)
|
|
163
|
+
self._linkkit_client.unsubscribe_topic(
|
|
164
|
+
f"/sys/{self._product_key}/{self._device_name}/app/down/thing/model/down_raw"
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
def _thing_on_topic_message(self, topic: str, payload: str, qos, user_data) -> None:
|
|
168
|
+
"""Is called when thing topic comes in."""
|
|
169
|
+
logger.debug(
|
|
170
|
+
"on_topic_message, receive message, topic:%s, payload:%s, qos:%d",
|
|
171
|
+
topic,
|
|
172
|
+
payload,
|
|
173
|
+
qos,
|
|
174
|
+
)
|
|
175
|
+
json_payload = json.loads(payload)
|
|
176
|
+
iot_id = json_payload.get("params", {}).get("iotId", "")
|
|
177
|
+
if iot_id != "" and self.on_message is not None:
|
|
178
|
+
future = asyncio.run_coroutine_threadsafe(self.on_message(topic, payload, iot_id), self.loop)
|
|
179
|
+
asyncio.wrap_future(future, loop=self.loop)
|
|
180
|
+
|
|
181
|
+
def _thing_on_connect(self, session_flag, rc, user_data) -> None:
|
|
182
|
+
"""Handle connection event and execute callback if set."""
|
|
183
|
+
self.is_connected = True
|
|
184
|
+
if self.on_connected is not None:
|
|
185
|
+
future = asyncio.run_coroutine_threadsafe(self.on_connected(), self.loop)
|
|
186
|
+
asyncio.wrap_future(future, loop=self.loop)
|
|
187
|
+
|
|
188
|
+
logger.debug("on_connect, session_flag:%d, rc:%d", session_flag, rc)
|
|
189
|
+
|
|
190
|
+
def _on_disconnect(self, _client, _userdata) -> None:
|
|
191
|
+
"""Is called on disconnect."""
|
|
192
|
+
if self._linkkit_client.check_state() is LinkKit.LinkKitState.DISCONNECTED:
|
|
193
|
+
logger.info("Disconnected")
|
|
194
|
+
self.is_connected = False
|
|
195
|
+
self.is_ready = False
|
|
196
|
+
if self.on_disconnected:
|
|
197
|
+
future = asyncio.run_coroutine_threadsafe(self.on_disconnected(), self.loop)
|
|
198
|
+
asyncio.wrap_future(future, loop=self.loop)
|
|
199
|
+
|
|
200
|
+
def _on_message(self, _client, _userdata, message: MQTTMessage) -> None:
|
|
201
|
+
"""Is called when message is received."""
|
|
202
|
+
logger.debug("Message on topic %s", message.topic)
|
|
203
|
+
|
|
204
|
+
payload = json.loads(message.payload)
|
|
205
|
+
if message.topic.endswith("/app/down/thing/events"):
|
|
206
|
+
event = ThingEventMessage(**payload)
|
|
207
|
+
params = event.params
|
|
208
|
+
if params.identifier == "device_protobuf_msg_event":
|
|
209
|
+
content = LubaMsg().parse(base64.b64decode(params.value.content))
|
|
210
|
+
|
|
211
|
+
logger.info("Unhandled protobuf event: %s", betterproto2.which_one_of(content, "LubaSubMsg"))
|
|
212
|
+
elif params.identifier == "device_warning_event":
|
|
213
|
+
logger.debug("identifier event: %s", params.identifier)
|
|
214
|
+
else:
|
|
215
|
+
logger.info("Unhandled event: %s", params.identifier)
|
|
216
|
+
elif message.topic.endswith("/app/down/thing/status"):
|
|
217
|
+
# the tell if a device has come back online
|
|
218
|
+
# lastStatus
|
|
219
|
+
# 1 online?
|
|
220
|
+
# 3 offline?
|
|
221
|
+
status = ThingStatusMessage(**payload)
|
|
222
|
+
logger.debug(status.params.status.value)
|
|
223
|
+
elif message.topic.endswith("/app/down/thing/properties"):
|
|
224
|
+
properties = ThingPropertiesMessage(**payload)
|
|
225
|
+
logger.debug("properties: %s", properties)
|
|
226
|
+
else:
|
|
227
|
+
logger.debug("Unhandled topic: %s", message.topic)
|
|
228
|
+
logger.debug(payload)
|
|
229
|
+
|
|
230
|
+
async def send_cloud_command(self, iot_id: str, command: bytes) -> str:
|
|
231
|
+
"""Return internal cloud client."""
|
|
232
|
+
return await self._cloud_client.send_cloud_command(iot_id, command)
|
|
@@ -1,232 +1,214 @@
|
|
|
1
|
-
"""MammotionMQTT."""
|
|
2
|
-
|
|
3
1
|
import asyncio
|
|
4
|
-
import
|
|
5
|
-
from typing import Awaitable, Callable
|
|
6
|
-
import hashlib
|
|
7
|
-
import hmac
|
|
2
|
+
from collections.abc import Awaitable, Callable
|
|
8
3
|
import json
|
|
9
4
|
import logging
|
|
10
|
-
|
|
5
|
+
import ssl
|
|
6
|
+
from typing import Any
|
|
7
|
+
from urllib.parse import urlparse
|
|
11
8
|
|
|
12
|
-
import
|
|
13
|
-
from paho.mqtt.
|
|
9
|
+
import paho.mqtt.client as mqtt
|
|
10
|
+
from paho.mqtt.properties import Properties
|
|
11
|
+
from paho.mqtt.reasoncodes import ReasonCode
|
|
14
12
|
|
|
15
|
-
from pymammotion
|
|
16
|
-
from pymammotion.
|
|
17
|
-
from pymammotion.
|
|
18
|
-
from pymammotion.data.mqtt.status import ThingStatusMessage
|
|
19
|
-
from pymammotion.mqtt.linkkit.linkkit import LinkKit
|
|
20
|
-
from pymammotion.proto import LubaMsg
|
|
13
|
+
from pymammotion import MammotionHTTP
|
|
14
|
+
from pymammotion.http.model.http import DeviceRecord, MQTTConnection, Response, UnauthorizedException
|
|
15
|
+
from pymammotion.utility.datatype_converter import DatatypeConverter
|
|
21
16
|
|
|
22
|
-
logger = getLogger(__name__)
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
23
18
|
|
|
24
19
|
|
|
25
20
|
class MammotionMQTT:
|
|
26
|
-
"""MQTT
|
|
21
|
+
"""Mammotion MQTT Client."""
|
|
22
|
+
|
|
23
|
+
converter = DatatypeConverter()
|
|
27
24
|
|
|
28
25
|
def __init__(
|
|
29
|
-
self,
|
|
30
|
-
region_id: str,
|
|
31
|
-
product_key: str,
|
|
32
|
-
device_name: str,
|
|
33
|
-
device_secret: str,
|
|
34
|
-
iot_token: str,
|
|
35
|
-
cloud_client: CloudIOTGateway,
|
|
36
|
-
client_id: str | None = None,
|
|
26
|
+
self, mqtt_connection: MQTTConnection, mammotion_http: MammotionHTTP, records: list[DeviceRecord]
|
|
37
27
|
) -> None:
|
|
38
|
-
"""Create instance of MammotionMQTT."""
|
|
39
|
-
super().__init__()
|
|
40
|
-
self._cloud_client = cloud_client
|
|
41
|
-
self.is_connected = False
|
|
42
|
-
self.is_ready = False
|
|
43
28
|
self.on_connected: Callable[[], Awaitable[None]] | None = None
|
|
44
29
|
self.on_ready: Callable[[], Awaitable[None]] | None = None
|
|
45
30
|
self.on_error: Callable[[str], Awaitable[None]] | None = None
|
|
46
31
|
self.on_disconnected: Callable[[], Awaitable[None]] | None = None
|
|
47
|
-
self.on_message: Callable[[str,
|
|
48
|
-
|
|
49
|
-
self._product_key = product_key
|
|
50
|
-
self._device_name = device_name
|
|
51
|
-
self._device_secret = device_secret
|
|
52
|
-
self._iot_token = iot_token
|
|
53
|
-
self._mqtt_username = f"{device_name}&{product_key}"
|
|
54
|
-
# linkkit provides the correct MQTT service for all of this and uses paho under the hood
|
|
55
|
-
if client_id is None:
|
|
56
|
-
client_id = f"python-{device_name}"
|
|
57
|
-
self._mqtt_client_id = f"{client_id}|securemode=2,signmethod=hmacsha1|"
|
|
58
|
-
sign_content = f"clientId{client_id}deviceName{device_name}productKey{product_key}"
|
|
59
|
-
self._mqtt_password = hmac.new(
|
|
60
|
-
device_secret.encode("utf-8"), sign_content.encode("utf-8"), hashlib.sha1
|
|
61
|
-
).hexdigest()
|
|
62
|
-
|
|
63
|
-
self._client_id = client_id
|
|
32
|
+
self.on_message: Callable[[str, bytes, str], Awaitable[None]] | None = None
|
|
64
33
|
self.loop = asyncio.get_running_loop()
|
|
34
|
+
self.mammotion_http = mammotion_http
|
|
35
|
+
self.mqtt_connection = mqtt_connection
|
|
36
|
+
self.client = self.build(mqtt_connection)
|
|
65
37
|
|
|
66
|
-
self.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
username=self._mqtt_username,
|
|
75
|
-
)
|
|
38
|
+
self.records = records
|
|
39
|
+
|
|
40
|
+
# wire callbacks from the service object if present
|
|
41
|
+
self.client.on_connect = self._on_connect
|
|
42
|
+
self.client.on_message = self._on_message
|
|
43
|
+
self.client.on_disconnect = self._on_disconnect
|
|
44
|
+
# client.on_subscribe = getattr(mqtt_service_obj, "on_subscribe", None)
|
|
45
|
+
# client.on_publish = getattr(mqtt_service_obj, "on_publish", None)
|
|
76
46
|
|
|
77
|
-
|
|
78
|
-
self.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
self._mqtt_host = f"{self._product_key}.iot-as-mqtt.{region_id}.aliyuncs.com"
|
|
47
|
+
def __del__(self) -> None:
|
|
48
|
+
if self.client.is_connected():
|
|
49
|
+
for record in self.records:
|
|
50
|
+
self.unsubscribe_all(record.product_key, record.device_name)
|
|
51
|
+
self.client.disconnect()
|
|
83
52
|
|
|
84
53
|
def connect_async(self) -> None:
|
|
85
54
|
"""Connect async to MQTT Server."""
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
self.
|
|
89
|
-
|
|
55
|
+
if not self.client.is_connected():
|
|
56
|
+
logger.info("Connecting...")
|
|
57
|
+
self.client.connect_async(host=self.client.host, port=self.client.port, keepalive=self.client.keepalive)
|
|
58
|
+
self.client.loop_start()
|
|
90
59
|
|
|
91
60
|
def disconnect(self) -> None:
|
|
92
61
|
"""Disconnect from MQTT Server."""
|
|
93
62
|
logger.info("Disconnecting...")
|
|
63
|
+
self.client.disconnect()
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def build(mqtt_connection: MQTTConnection, keepalive: int = 60, timeout: int = 30) -> mqtt.Client:
|
|
67
|
+
"""get_jwt_response: object with attributes .client_id, .username, .jwt (password), .host (e.g. 'mqtts://broker:8883' or 'broker:1883' or 'broker').
|
|
68
|
+
mqtt_service_obj: object that exposes callback methods (on_connect, on_message, on_disconnect, etc.)
|
|
69
|
+
Returns: (client, connected_bool, rc)
|
|
70
|
+
"""
|
|
71
|
+
host = mqtt_connection.host
|
|
72
|
+
# Ensure urlparse can parse plain hosts
|
|
73
|
+
parsed = urlparse(host if "://" in host else "tcp://" + host)
|
|
74
|
+
scheme = parsed.scheme
|
|
75
|
+
hostname = parsed.hostname
|
|
76
|
+
port = parsed.port
|
|
77
|
+
|
|
78
|
+
# decide TLS/ssl and default port
|
|
79
|
+
use_ssl = scheme in ("mqtts", "ssl")
|
|
80
|
+
if port is None:
|
|
81
|
+
port = 8883 if use_ssl else 1883
|
|
82
|
+
|
|
83
|
+
client = mqtt.Client(
|
|
84
|
+
callback_api_version=mqtt.CallbackAPIVersion.VERSION2,
|
|
85
|
+
client_id=mqtt_connection.client_id,
|
|
86
|
+
clean_session=True,
|
|
87
|
+
protocol=mqtt.MQTTv311,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
client.username_pw_set(mqtt_connection.username, mqtt_connection.jwt)
|
|
91
|
+
|
|
92
|
+
if use_ssl:
|
|
93
|
+
# use system default CA certs; adjust tls_set() params if custom CA/client certs required
|
|
94
|
+
client.tls_set(cert_reqs=ssl.CERT_REQUIRED)
|
|
95
|
+
client.tls_insecure_set(False)
|
|
96
|
+
|
|
97
|
+
# automatic reconnect backoff
|
|
98
|
+
client.reconnect_delay_set(min_delay=1, max_delay=120)
|
|
99
|
+
|
|
100
|
+
# connect (synchronous connect attempt) and start background loop
|
|
101
|
+
if hostname:
|
|
102
|
+
client.host = hostname
|
|
103
|
+
client.port = port
|
|
104
|
+
client.keepalive = keepalive
|
|
105
|
+
|
|
106
|
+
return client
|
|
107
|
+
|
|
108
|
+
def _on_message(self, _client: mqtt.Client, _userdata: Any, message: mqtt.MQTTMessage) -> None:
|
|
109
|
+
"""Is called when message is received."""
|
|
110
|
+
logger.debug("Message on topic %s", message.topic)
|
|
111
|
+
logger.debug(message)
|
|
112
|
+
|
|
113
|
+
if self.on_message is not None:
|
|
114
|
+
iot_id = None
|
|
115
|
+
# Parse the topic path to get product_key and device_name
|
|
116
|
+
topic_parts = message.topic.split("/")
|
|
117
|
+
if len(topic_parts) >= 4:
|
|
118
|
+
product_key = topic_parts[2]
|
|
119
|
+
device_name = topic_parts[3]
|
|
120
|
+
|
|
121
|
+
# Filter records to find matching device
|
|
122
|
+
filtered_records = [
|
|
123
|
+
record
|
|
124
|
+
for record in self.records
|
|
125
|
+
if record.product_key == product_key and record.device_name == device_name
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
if filtered_records:
|
|
129
|
+
iot_id = filtered_records[0].iot_id
|
|
130
|
+
payload = json.loads(message.payload.decode("utf-8"))
|
|
131
|
+
payload["iot_id"] = iot_id
|
|
132
|
+
payload["product_key"] = product_key
|
|
133
|
+
payload["device_name"] = device_name
|
|
134
|
+
message.payload = json.dumps(payload).encode("utf-8")
|
|
135
|
+
|
|
136
|
+
if iot_id:
|
|
137
|
+
future = asyncio.run_coroutine_threadsafe(
|
|
138
|
+
self.on_message(message.topic, message.payload, iot_id), self.loop
|
|
139
|
+
)
|
|
140
|
+
asyncio.wrap_future(future, loop=self.loop)
|
|
94
141
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
142
|
+
def _on_connect(
|
|
143
|
+
self,
|
|
144
|
+
_client: mqtt.Client,
|
|
145
|
+
user_data: Any,
|
|
146
|
+
session_flag: mqtt.ConnectFlags,
|
|
147
|
+
rc: ReasonCode,
|
|
148
|
+
properties: Properties | None,
|
|
149
|
+
) -> None:
|
|
150
|
+
"""Handle connection event and execute callback if set."""
|
|
100
151
|
self.is_connected = True
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
self.
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
self._linkkit_client.subscribe_topic(
|
|
107
|
-
f"/sys/{self._product_key}/{self._device_name}/app/down/thing/event/property/post_reply"
|
|
108
|
-
)
|
|
109
|
-
self._linkkit_client.subscribe_topic(
|
|
110
|
-
f"/sys/{self._product_key}/{self._device_name}/app/down/thing/wifi/status/notify"
|
|
111
|
-
)
|
|
112
|
-
self._linkkit_client.subscribe_topic(
|
|
113
|
-
f"/sys/{self._product_key}/{self._device_name}/app/down/thing/wifi/connect/event/notify"
|
|
114
|
-
)
|
|
115
|
-
self._linkkit_client.subscribe_topic(
|
|
116
|
-
f"/sys/{self._product_key}/{self._device_name}/app/down/_thing/event/notify"
|
|
117
|
-
)
|
|
118
|
-
self._linkkit_client.subscribe_topic(f"/sys/{self._product_key}/{self._device_name}/app/down/thing/events")
|
|
119
|
-
self._linkkit_client.subscribe_topic(f"/sys/{self._product_key}/{self._device_name}/app/down/thing/status")
|
|
120
|
-
self._linkkit_client.subscribe_topic(f"/sys/{self._product_key}/{self._device_name}/app/down/thing/properties")
|
|
121
|
-
self._linkkit_client.subscribe_topic(
|
|
122
|
-
f"/sys/{self._product_key}/{self._device_name}/app/down/thing/model/down_raw"
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
self._linkkit_client.publish_topic(
|
|
126
|
-
f"/sys/{self._product_key}/{self._device_name}/app/up/account/bind",
|
|
127
|
-
json.dumps(
|
|
128
|
-
{
|
|
129
|
-
"id": "msgid1",
|
|
130
|
-
"version": "1.0",
|
|
131
|
-
"request": {"clientId": self._mqtt_username},
|
|
132
|
-
"params": {"iotToken": self._iot_token},
|
|
133
|
-
}
|
|
134
|
-
),
|
|
135
|
-
)
|
|
152
|
+
for record in self.records:
|
|
153
|
+
self.subscribe_all(record.product_key, record.device_name)
|
|
154
|
+
if self.on_connected is not None:
|
|
155
|
+
future = asyncio.run_coroutine_threadsafe(self.on_connected(), self.loop)
|
|
156
|
+
asyncio.wrap_future(future, loop=self.loop)
|
|
136
157
|
|
|
137
158
|
if self.on_ready:
|
|
138
159
|
self.is_ready = True
|
|
139
160
|
future = asyncio.run_coroutine_threadsafe(self.on_ready(), self.loop)
|
|
140
161
|
asyncio.wrap_future(future, loop=self.loop)
|
|
141
162
|
|
|
142
|
-
|
|
143
|
-
self._linkkit_client.unsubscribe_topic(
|
|
144
|
-
f"/sys/{self._product_key}/{self._device_name}/app/down/account/bind_reply"
|
|
145
|
-
)
|
|
146
|
-
self._linkkit_client.unsubscribe_topic(
|
|
147
|
-
f"/sys/{self._product_key}/{self._device_name}/app/down/thing/event/property/post_reply"
|
|
148
|
-
)
|
|
149
|
-
self._linkkit_client.unsubscribe_topic(
|
|
150
|
-
f"/sys/{self._product_key}/{self._device_name}/app/down/thing/wifi/status/notify"
|
|
151
|
-
)
|
|
152
|
-
self._linkkit_client.unsubscribe_topic(
|
|
153
|
-
f"/sys/{self._product_key}/{self._device_name}/app/down/thing/wifi/connect/event/notify"
|
|
154
|
-
)
|
|
155
|
-
self._linkkit_client.unsubscribe_topic(
|
|
156
|
-
f"/sys/{self._product_key}/{self._device_name}/app/down/_thing/event/notify"
|
|
157
|
-
)
|
|
158
|
-
self._linkkit_client.unsubscribe_topic(f"/sys/{self._product_key}/{self._device_name}/app/down/thing/events")
|
|
159
|
-
self._linkkit_client.unsubscribe_topic(f"/sys/{self._product_key}/{self._device_name}/app/down/thing/status")
|
|
160
|
-
self._linkkit_client.unsubscribe_topic(
|
|
161
|
-
f"/sys/{self._product_key}/{self._device_name}/app/down/thing/properties"
|
|
162
|
-
)
|
|
163
|
-
self._linkkit_client.unsubscribe_topic(
|
|
164
|
-
f"/sys/{self._product_key}/{self._device_name}/app/down/thing/model/down_raw"
|
|
165
|
-
)
|
|
163
|
+
logger.debug("on_connect, session_flag:%s, rc:%s", session_flag, rc)
|
|
166
164
|
|
|
167
|
-
def
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
165
|
+
def _on_disconnect(
|
|
166
|
+
self,
|
|
167
|
+
_client: mqtt.Client,
|
|
168
|
+
user_data: Any | None,
|
|
169
|
+
disconnect_flags: mqtt.DisconnectFlags,
|
|
170
|
+
rc: ReasonCode,
|
|
171
|
+
properties: Properties | None,
|
|
172
|
+
**kwargs: Any,
|
|
173
|
+
) -> None:
|
|
174
|
+
"""Handle disconnection event and execute callback if set."""
|
|
175
|
+
self.is_connected = False
|
|
176
|
+
if self.on_disconnected is not None:
|
|
177
|
+
for record in self.records:
|
|
178
|
+
self.unsubscribe_all(record.product_key, record.device_name)
|
|
179
|
+
future = asyncio.run_coroutine_threadsafe(self.on_disconnected(), self.loop)
|
|
179
180
|
asyncio.wrap_future(future, loop=self.loop)
|
|
180
181
|
|
|
181
|
-
|
|
182
|
-
"""Handle connection event and execute callback if set."""
|
|
183
|
-
self.is_connected = True
|
|
184
|
-
if self.on_connected is not None:
|
|
185
|
-
future = asyncio.run_coroutine_threadsafe(self.on_connected(), self.loop)
|
|
186
|
-
asyncio.wrap_future(future, loop=self.loop)
|
|
182
|
+
logger.debug("on_disconnect, rc:%s", rc)
|
|
187
183
|
|
|
188
|
-
|
|
184
|
+
def subscribe_all(self, product_key: str, device_name: str) -> None:
|
|
185
|
+
"""Subscribe to all topics for the given device."""
|
|
189
186
|
|
|
190
|
-
|
|
191
|
-
"""
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if self.on_disconnected:
|
|
197
|
-
future = asyncio.run_coroutine_threadsafe(self.on_disconnected(), self.loop)
|
|
198
|
-
asyncio.wrap_future(future, loop=self.loop)
|
|
187
|
+
# "/sys/" + this.$productKey + "/" + this.$deviceName + "/thing/event/+/post"
|
|
188
|
+
# "/sys/proto/" + this.$productKey + "/" + this.$deviceName + "/thing/event/+/post"
|
|
189
|
+
# "/sys/" + this.$productKey + "/" + this.$deviceName + "/app/down/thing/status"
|
|
190
|
+
self.client.subscribe(f"/sys/{product_key}/{device_name}/app/down/thing/status")
|
|
191
|
+
self.client.subscribe(f"/sys/{product_key}/{device_name}/thing/event/+/post")
|
|
192
|
+
self.client.subscribe(f"/sys/proto/{product_key}/{device_name}/thing/event/+/post")
|
|
199
193
|
|
|
200
|
-
def
|
|
201
|
-
"""
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
status = ThingStatusMessage(**payload)
|
|
222
|
-
logger.debug(status.params.status.value)
|
|
223
|
-
elif message.topic.endswith("/app/down/thing/properties"):
|
|
224
|
-
properties = ThingPropertiesMessage(**payload)
|
|
225
|
-
logger.debug("properties: %s", properties)
|
|
226
|
-
else:
|
|
227
|
-
logger.debug("Unhandled topic: %s", message.topic)
|
|
228
|
-
logger.debug(payload)
|
|
229
|
-
|
|
230
|
-
def get_cloud_client(self) -> CloudIOTGateway:
|
|
231
|
-
"""Return internal cloud client."""
|
|
232
|
-
return self._cloud_client
|
|
194
|
+
def unsubscribe_all(self, product_key: str, device_name: str) -> None:
|
|
195
|
+
"""Unsubscribe from all topics for the given device."""
|
|
196
|
+
self.client.unsubscribe(f"/sys/{product_key}/{device_name}/app/down/thing/status")
|
|
197
|
+
self.client.unsubscribe(f"/sys/{product_key}/{device_name}/thing/event/+/post")
|
|
198
|
+
self.client.unsubscribe(f"/sys/proto/{product_key}/{device_name}/thing/event/+/post")
|
|
199
|
+
|
|
200
|
+
async def send_cloud_command(self, iot_id: str, command: bytes) -> str:
|
|
201
|
+
"""Send command to cloud."""
|
|
202
|
+
res: Response[dict] = await self.mammotion_http.mqtt_invoke(
|
|
203
|
+
self.converter.printBase64Binary(command), "", iot_id
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
logger.debug("send_cloud_command: %s", res)
|
|
207
|
+
|
|
208
|
+
if res.code == 500:
|
|
209
|
+
return res.msg
|
|
210
|
+
|
|
211
|
+
if res.code == 401:
|
|
212
|
+
raise UnauthorizedException(res.msg)
|
|
213
|
+
|
|
214
|
+
return str(res.data["result"])
|