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.

Files changed (54) hide show
  1. pymammotion/__init__.py +3 -3
  2. pymammotion/aliyun/cloud_gateway.py +106 -18
  3. pymammotion/aliyun/model/dev_by_account_response.py +198 -20
  4. pymammotion/const.py +3 -0
  5. pymammotion/data/model/device.py +1 -0
  6. pymammotion/data/model/device_config.py +1 -1
  7. pymammotion/data/model/enums.py +5 -3
  8. pymammotion/data/model/generate_route_information.py +2 -2
  9. pymammotion/data/model/hash_list.py +113 -33
  10. pymammotion/data/model/region_data.py +4 -4
  11. pymammotion/data/{state_manager.py → mower_state_manager.py} +17 -7
  12. pymammotion/data/mqtt/event.py +47 -22
  13. pymammotion/data/mqtt/mammotion_properties.py +257 -0
  14. pymammotion/data/mqtt/properties.py +32 -29
  15. pymammotion/data/mqtt/status.py +17 -16
  16. pymammotion/homeassistant/__init__.py +3 -0
  17. pymammotion/homeassistant/mower_api.py +446 -0
  18. pymammotion/homeassistant/rtk_api.py +54 -0
  19. pymammotion/http/http.py +387 -13
  20. pymammotion/http/model/http.py +82 -2
  21. pymammotion/http/model/response_factory.py +10 -4
  22. pymammotion/mammotion/commands/mammotion_command.py +6 -0
  23. pymammotion/mammotion/commands/messages/navigation.py +10 -6
  24. pymammotion/mammotion/devices/__init__.py +27 -3
  25. pymammotion/mammotion/devices/base.py +16 -138
  26. pymammotion/mammotion/devices/mammotion.py +364 -204
  27. pymammotion/mammotion/devices/mammotion_bluetooth.py +7 -5
  28. pymammotion/mammotion/devices/mammotion_cloud.py +42 -83
  29. pymammotion/mammotion/devices/mammotion_mower_ble.py +49 -0
  30. pymammotion/mammotion/devices/mammotion_mower_cloud.py +39 -0
  31. pymammotion/mammotion/devices/managers/managers.py +81 -0
  32. pymammotion/mammotion/devices/mower_device.py +121 -0
  33. pymammotion/mammotion/devices/mower_manager.py +107 -0
  34. pymammotion/mammotion/devices/rtk_ble.py +89 -0
  35. pymammotion/mammotion/devices/rtk_cloud.py +113 -0
  36. pymammotion/mammotion/devices/rtk_device.py +50 -0
  37. pymammotion/mammotion/devices/rtk_manager.py +122 -0
  38. pymammotion/mqtt/__init__.py +2 -1
  39. pymammotion/mqtt/aliyun_mqtt.py +232 -0
  40. pymammotion/mqtt/mammotion_mqtt.py +174 -192
  41. pymammotion/mqtt/mqtt_models.py +66 -0
  42. pymammotion/proto/__init__.py +1 -1
  43. pymammotion/proto/mctrl_nav.proto +1 -1
  44. pymammotion/proto/mctrl_nav_pb2.py +1 -1
  45. pymammotion/proto/mctrl_nav_pb2.pyi +4 -4
  46. pymammotion/proto/mctrl_sys.proto +1 -1
  47. pymammotion/utility/datatype_converter.py +13 -12
  48. pymammotion/utility/device_type.py +88 -3
  49. pymammotion/utility/mur_mur_hash.py +132 -87
  50. {pymammotion-0.5.34.dist-info → pymammotion-0.5.44.dist-info}/METADATA +25 -31
  51. {pymammotion-0.5.34.dist-info → pymammotion-0.5.44.dist-info}/RECORD +59 -45
  52. {pymammotion-0.5.34.dist-info → pymammotion-0.5.44.dist-info}/WHEEL +1 -1
  53. pymammotion/http/_init_.py +0 -0
  54. {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 base64
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
- from logging import getLogger
5
+ import ssl
6
+ from typing import Any
7
+ from urllib.parse import urlparse
11
8
 
12
- import betterproto2
13
- from paho.mqtt.client import MQTTMessage
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.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
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 client for pymammotion."""
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, 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
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._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
- )
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
- 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"
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
- 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()
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
- 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")
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
- # 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
- )
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
- 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
- )
163
+ logger.debug("on_connect, session_flag:%s, rc:%s", session_flag, rc)
166
164
 
167
- def _thing_on_topic_message(self, topic, payload, 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
- payload = json.loads(payload)
176
- iot_id = 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)
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
- 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)
182
+ logger.debug("on_disconnect, rc:%s", rc)
187
183
 
188
- logger.debug("on_connect, session_flag:%d, rc:%d", session_flag, rc)
184
+ def subscribe_all(self, product_key: str, device_name: str) -> None:
185
+ """Subscribe to all topics for the given device."""
189
186
 
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)
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 _on_message(self, _client, _userdata, message: MQTTMessage) -> None:
201
- """Is called when message is received."""
202
- logger.info("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
- 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"])