pymammotion 0.5.69__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.
Files changed (154) hide show
  1. pymammotion/__init__.py +53 -0
  2. pymammotion/agora/__init__.py +0 -0
  3. pymammotion/agora/agora_api.py +755 -0
  4. pymammotion/agora/agora_rtc_capabilities.py +748 -0
  5. pymammotion/agora/agora_websockets.py +1175 -0
  6. pymammotion/aliyun/__init__.py +1 -0
  7. pymammotion/aliyun/client.py +235 -0
  8. pymammotion/aliyun/cloud_gateway.py +982 -0
  9. pymammotion/aliyun/model/aep_response.py +21 -0
  10. pymammotion/aliyun/model/connect_response.py +51 -0
  11. pymammotion/aliyun/model/dev_by_account_response.py +195 -0
  12. pymammotion/aliyun/model/login_by_oauth_response.py +64 -0
  13. pymammotion/aliyun/model/regions_response.py +29 -0
  14. pymammotion/aliyun/model/session_by_authcode_response.py +19 -0
  15. pymammotion/aliyun/model/thing_response.py +12 -0
  16. pymammotion/aliyun/regions.py +62 -0
  17. pymammotion/aliyun/tea/core.py +297 -0
  18. pymammotion/aliyun/tmp_constant.py +171 -0
  19. pymammotion/bluetooth/__init__.py +1 -0
  20. pymammotion/bluetooth/ble.py +62 -0
  21. pymammotion/bluetooth/ble_message.py +676 -0
  22. pymammotion/bluetooth/const.py +27 -0
  23. pymammotion/bluetooth/data/__init__.py +0 -0
  24. pymammotion/bluetooth/data/convert.py +25 -0
  25. pymammotion/bluetooth/data/framectrldata.py +40 -0
  26. pymammotion/bluetooth/data/notifydata.py +62 -0
  27. pymammotion/bluetooth/model/__init__.py +0 -0
  28. pymammotion/bluetooth/model/atomic_integer.py +54 -0
  29. pymammotion/const.py +13 -0
  30. pymammotion/data/__init__.py +0 -0
  31. pymammotion/data/model/__init__.py +8 -0
  32. pymammotion/data/model/account.py +8 -0
  33. pymammotion/data/model/device.py +192 -0
  34. pymammotion/data/model/device_config.py +72 -0
  35. pymammotion/data/model/device_info.py +60 -0
  36. pymammotion/data/model/device_limits.py +49 -0
  37. pymammotion/data/model/enums.py +77 -0
  38. pymammotion/data/model/errors.py +12 -0
  39. pymammotion/data/model/events.py +14 -0
  40. pymammotion/data/model/generate_geojson.py +565 -0
  41. pymammotion/data/model/generate_route_information.py +26 -0
  42. pymammotion/data/model/hash_list.py +475 -0
  43. pymammotion/data/model/location.py +36 -0
  44. pymammotion/data/model/mowing_modes.py +77 -0
  45. pymammotion/data/model/rapid_state.py +45 -0
  46. pymammotion/data/model/raw_data.py +215 -0
  47. pymammotion/data/model/region_data.py +102 -0
  48. pymammotion/data/model/report_info.py +182 -0
  49. pymammotion/data/model/work.py +27 -0
  50. pymammotion/data/mower_state_manager.py +369 -0
  51. pymammotion/data/mqtt/__init__.py +1 -0
  52. pymammotion/data/mqtt/event.py +227 -0
  53. pymammotion/data/mqtt/mammotion_properties.py +276 -0
  54. pymammotion/data/mqtt/properties.py +203 -0
  55. pymammotion/data/mqtt/status.py +57 -0
  56. pymammotion/event/__init__.py +6 -0
  57. pymammotion/event/event.py +96 -0
  58. pymammotion/homeassistant/__init__.py +3 -0
  59. pymammotion/homeassistant/mower_api.py +514 -0
  60. pymammotion/homeassistant/rtk_api.py +54 -0
  61. pymammotion/http/__init__.py +0 -0
  62. pymammotion/http/encryption.py +220 -0
  63. pymammotion/http/http.py +673 -0
  64. pymammotion/http/model/__init__.py +0 -0
  65. pymammotion/http/model/camera_stream.py +31 -0
  66. pymammotion/http/model/http.py +249 -0
  67. pymammotion/http/model/response_factory.py +61 -0
  68. pymammotion/http/model/rtk.py +16 -0
  69. pymammotion/mammotion/__init__.py +0 -0
  70. pymammotion/mammotion/commands/__init__.py +0 -0
  71. pymammotion/mammotion/commands/abstract_message.py +24 -0
  72. pymammotion/mammotion/commands/mammotion_command.py +81 -0
  73. pymammotion/mammotion/commands/messages/__init__.py +0 -0
  74. pymammotion/mammotion/commands/messages/basestation.py +43 -0
  75. pymammotion/mammotion/commands/messages/driver.py +122 -0
  76. pymammotion/mammotion/commands/messages/media.py +87 -0
  77. pymammotion/mammotion/commands/messages/navigation.py +564 -0
  78. pymammotion/mammotion/commands/messages/network.py +205 -0
  79. pymammotion/mammotion/commands/messages/ota.py +38 -0
  80. pymammotion/mammotion/commands/messages/system.py +330 -0
  81. pymammotion/mammotion/commands/messages/video.py +33 -0
  82. pymammotion/mammotion/control/__init__.py +0 -0
  83. pymammotion/mammotion/control/joystick.py +145 -0
  84. pymammotion/mammotion/devices/__init__.py +29 -0
  85. pymammotion/mammotion/devices/base.py +163 -0
  86. pymammotion/mammotion/devices/mammotion.py +571 -0
  87. pymammotion/mammotion/devices/mammotion_bluetooth.py +496 -0
  88. pymammotion/mammotion/devices/mammotion_cloud.py +355 -0
  89. pymammotion/mammotion/devices/mammotion_mower_ble.py +48 -0
  90. pymammotion/mammotion/devices/mammotion_mower_cloud.py +39 -0
  91. pymammotion/mammotion/devices/managers/managers.py +81 -0
  92. pymammotion/mammotion/devices/mower_device.py +120 -0
  93. pymammotion/mammotion/devices/mower_manager.py +107 -0
  94. pymammotion/mammotion/devices/rtk_ble.py +89 -0
  95. pymammotion/mammotion/devices/rtk_cloud.py +115 -0
  96. pymammotion/mammotion/devices/rtk_device.py +50 -0
  97. pymammotion/mammotion/devices/rtk_manager.py +125 -0
  98. pymammotion/mqtt/__init__.py +6 -0
  99. pymammotion/mqtt/aliyun_mqtt.py +237 -0
  100. pymammotion/mqtt/linkkit/__init__.py +5 -0
  101. pymammotion/mqtt/linkkit/h2client.py +585 -0
  102. pymammotion/mqtt/linkkit/linkkit.py +3025 -0
  103. pymammotion/mqtt/mammotion_future.py +26 -0
  104. pymammotion/mqtt/mammotion_mqtt.py +214 -0
  105. pymammotion/mqtt/mqtt_models.py +66 -0
  106. pymammotion/proto/__init__.py +4841 -0
  107. pymammotion/proto/basestation.proto +51 -0
  108. pymammotion/proto/basestation_pb2.py +35 -0
  109. pymammotion/proto/basestation_pb2.pyi +89 -0
  110. pymammotion/proto/common.proto +7 -0
  111. pymammotion/proto/common_pb2.py +25 -0
  112. pymammotion/proto/common_pb2.pyi +13 -0
  113. pymammotion/proto/dev_net.proto +321 -0
  114. pymammotion/proto/dev_net_pb2.py +111 -0
  115. pymammotion/proto/dev_net_pb2.pyi +515 -0
  116. pymammotion/proto/luba_msg.proto +76 -0
  117. pymammotion/proto/luba_msg_pb2.py +41 -0
  118. pymammotion/proto/luba_msg_pb2.pyi +97 -0
  119. pymammotion/proto/luba_mul.proto +129 -0
  120. pymammotion/proto/luba_mul_pb2.py +61 -0
  121. pymammotion/proto/luba_mul_pb2.pyi +178 -0
  122. pymammotion/proto/mctrl_driver.proto +107 -0
  123. pymammotion/proto/mctrl_driver_pb2.py +57 -0
  124. pymammotion/proto/mctrl_driver_pb2.pyi +167 -0
  125. pymammotion/proto/mctrl_nav.proto +591 -0
  126. pymammotion/proto/mctrl_nav_pb2.py +136 -0
  127. pymammotion/proto/mctrl_nav_pb2.pyi +1067 -0
  128. pymammotion/proto/mctrl_ota.proto +80 -0
  129. pymammotion/proto/mctrl_ota_pb2.py +45 -0
  130. pymammotion/proto/mctrl_ota_pb2.pyi +128 -0
  131. pymammotion/proto/mctrl_pept.proto +34 -0
  132. pymammotion/proto/mctrl_pept_pb2.py +33 -0
  133. pymammotion/proto/mctrl_pept_pb2.pyi +58 -0
  134. pymammotion/proto/mctrl_sys.proto +741 -0
  135. pymammotion/proto/mctrl_sys_pb2.py +206 -0
  136. pymammotion/proto/mctrl_sys_pb2.pyi +1213 -0
  137. pymammotion/proto/message_pool.py +3 -0
  138. pymammotion/proto/py.typed +0 -0
  139. pymammotion/py.typed +0 -0
  140. pymammotion/utility/constant/__init__.py +3 -0
  141. pymammotion/utility/constant/device_constant.py +315 -0
  142. pymammotion/utility/conversions.py +5 -0
  143. pymammotion/utility/datatype_converter.py +124 -0
  144. pymammotion/utility/device_config.py +755 -0
  145. pymammotion/utility/device_type.py +489 -0
  146. pymammotion/utility/map.py +259 -0
  147. pymammotion/utility/movement.py +18 -0
  148. pymammotion/utility/mur_mur_hash.py +159 -0
  149. pymammotion/utility/periodic.py +106 -0
  150. pymammotion/utility/rocker_util.py +194 -0
  151. pymammotion-0.5.69.dist-info/METADATA +93 -0
  152. pymammotion-0.5.69.dist-info/RECORD +154 -0
  153. pymammotion-0.5.69.dist-info/WHEEL +4 -0
  154. pymammotion-0.5.69.dist-info/licenses/LICENSE +674 -0
@@ -0,0 +1,355 @@
1
+ import asyncio
2
+ from asyncio import InvalidStateError
3
+ import base64
4
+ from collections import deque
5
+ from collections.abc import Awaitable, Callable
6
+ import json
7
+ import logging
8
+ import time
9
+ from typing import Any
10
+
11
+ import betterproto2
12
+ from Tea.exceptions import UnretryableException
13
+
14
+ from pymammotion import AliyunMQTT, CloudIOTGateway, MammotionMQTT
15
+ from pymammotion.aliyun.cloud_gateway import DeviceOfflineException
16
+ from pymammotion.aliyun.model.dev_by_account_response import Device
17
+ from pymammotion.data.mower_state_manager import MowerStateManager
18
+ from pymammotion.data.mqtt.event import MammotionEventMessage, ThingEventMessage
19
+ from pymammotion.data.mqtt.properties import MammotionPropertiesMessage, ThingPropertiesMessage
20
+ from pymammotion.data.mqtt.status import ThingStatusMessage
21
+ from pymammotion.event.event import DataEvent
22
+ from pymammotion.mammotion.commands.mammotion_command import MammotionCommand
23
+ from pymammotion.mammotion.devices.base import MammotionBaseDevice
24
+ from pymammotion.proto import LubaMsg
25
+
26
+ _LOGGER = logging.getLogger(__name__)
27
+
28
+
29
+ class MammotionCloud:
30
+ """Per account MQTT cloud."""
31
+
32
+ def __init__(self, mqtt_client: AliyunMQTT | MammotionMQTT, cloud_client: CloudIOTGateway) -> None:
33
+ """Initialize MammotionCloud."""
34
+ self.cloud_client = cloud_client
35
+ self.command_sent_time = 0
36
+ self.loop = asyncio.get_event_loop()
37
+ self.is_ready = False
38
+ self.command_queue = asyncio.Queue()
39
+ self._waiting_queue = deque()
40
+ self.mqtt_message_event = DataEvent()
41
+ self.mqtt_properties_event = DataEvent()
42
+ self.mqtt_status_event = DataEvent()
43
+ self.mqtt_device_event = DataEvent()
44
+ self.on_ready_event = DataEvent()
45
+ self.on_disconnected_event = DataEvent()
46
+ self.on_connected_event = DataEvent()
47
+ self._operation_lock = asyncio.Lock()
48
+ self._mqtt_client = mqtt_client
49
+ self._mqtt_client.on_connected = self.on_connected
50
+ self._mqtt_client.on_disconnected = self.on_disconnected
51
+ self._mqtt_client.on_message = self._on_mqtt_message
52
+ self._mqtt_client.on_ready = self.on_ready
53
+
54
+ async def on_ready(self) -> None:
55
+ """Starts processing the queue and emits the ready event."""
56
+ loop = asyncio.get_event_loop()
57
+ loop.create_task(self.process_queue())
58
+ await self.on_ready_event.data_event(None)
59
+
60
+ def is_connected(self) -> bool:
61
+ return self._mqtt_client.is_connected
62
+
63
+ def disconnect(self) -> None:
64
+ """Disconnect the MQTT client."""
65
+ if self.is_connected:
66
+ self._mqtt_client.disconnect()
67
+
68
+ def connect_async(self) -> None:
69
+ self._mqtt_client.connect_async()
70
+
71
+ async def send_command(self, iot_id: str, command: bytes) -> None:
72
+ await self._mqtt_client.send_cloud_command(iot_id, command)
73
+
74
+ async def on_connected(self) -> None:
75
+ """Callback for when MQTT connects."""
76
+ await self.on_connected_event.data_event(None)
77
+
78
+ async def on_disconnected(self) -> None:
79
+ """Callback for when MQTT disconnects."""
80
+ await self.on_disconnected_event.data_event(None)
81
+
82
+ async def process_queue(self) -> None:
83
+ while True:
84
+ # Get the next item from the queue
85
+ iot_id, key, command, future = await self.command_queue.get()
86
+ try:
87
+ # Process the command using _execute_command_locked
88
+ result = await self._execute_command_locked(iot_id, key, command)
89
+ # Set the result on the future
90
+ future.set_result(result)
91
+ except Exception as ex:
92
+ # Set the exception on the future if something goes wrong
93
+ try:
94
+ future.set_exception(ex)
95
+ except InvalidStateError:
96
+ """Dead end, log an error."""
97
+ _LOGGER.exception("InvalidStateError while trying to bubble up exception")
98
+ finally:
99
+ # Mark the task as done
100
+ self.command_queue.task_done()
101
+
102
+ async def _execute_command_locked(self, iot_id: str, key: str, command: bytes) -> None:
103
+ """Execute command and read response."""
104
+ assert self._mqtt_client is not None
105
+ self._key = key
106
+ _LOGGER.debug("Sending command: %s", key)
107
+ self.command_sent_time = time.time()
108
+ await self._mqtt_client.send_cloud_command(iot_id, command)
109
+
110
+ async def _on_mqtt_message(self, topic: str, payload: bytes, iot_id: str) -> None:
111
+ """Handle incoming MQTT messages."""
112
+ # _LOGGER.debug("MQTT message received on topic %s: %s, iot_id: %s", topic, payload, iot_id)
113
+ json_str = payload.decode("utf-8")
114
+ dict_payload = json.loads(json_str)
115
+ await self._parse_mqtt_response(topic, dict_payload, iot_id)
116
+
117
+ async def _parse_mqtt_response(self, topic: str, payload: dict, iot_id: str) -> None:
118
+ """Parse and handle MQTT responses based on the topic.
119
+
120
+ This function processes different types of MQTT messages received from various
121
+ topics. It logs debug information and calls appropriate callback methods for
122
+ each event type.
123
+
124
+ Args:
125
+ topic (str): The MQTT topic from which the message was received.
126
+ payload (dict): The payload data of the MQTT message.
127
+
128
+ """
129
+ if topic.endswith("/app/down/account/bind_reply"):
130
+ code = payload.get("code", 0)
131
+ if code != 200:
132
+ _LOGGER.error("Failed to bind account: %s", payload)
133
+ # TODO send message to re login to aliyun mqtt
134
+ self._disconnect_error = payload
135
+ return
136
+
137
+ if topic.endswith("/app/down/thing/events"):
138
+ _LOGGER.debug("Thing event received")
139
+ event = ThingEventMessage.from_dicts(payload)
140
+ params = event.params
141
+ if isinstance(params, dict) or params.identifier is None:
142
+ _LOGGER.debug("Received dict params: %s", params)
143
+ return
144
+ if params.identifier == "device_protobuf_msg_event" and event.method == "thing.events":
145
+ _LOGGER.debug("Protobuf event")
146
+ # Call the callbacks for each cloudDevice
147
+ await self.mqtt_message_event.data_event(event)
148
+ if event.method == "thing.events":
149
+ await self.mqtt_device_event.data_event(event)
150
+ if event.method == "thing.properties":
151
+ await self.mqtt_properties_event.data_event(event)
152
+ _LOGGER.debug(event)
153
+ elif topic.endswith("/app/down/thing/status"):
154
+ status = ThingStatusMessage.from_dict(payload)
155
+ await self.mqtt_status_event.data_event(status)
156
+ elif topic.endswith("app/down/thing/properties"):
157
+ property_event = ThingPropertiesMessage.from_dict(payload)
158
+ await self.mqtt_properties_event.data_event(property_event)
159
+
160
+ if topic.endswith("/thing/event/device_protobuf_msg_event/post"):
161
+ _LOGGER.debug("Mammotion Thing event received")
162
+ mammotion_event = MammotionEventMessage.from_dict(payload)
163
+ mammotion_event.params.iot_id = iot_id
164
+ await self.mqtt_message_event.data_event(mammotion_event)
165
+ elif topic.endswith("/thing/event/property/post"):
166
+ _LOGGER.debug("Mammotion Property event received")
167
+ mammotion_property_event = MammotionPropertiesMessage.from_dict(payload)
168
+ mammotion_property_event.params.iot_id = iot_id
169
+ await self.mqtt_properties_event.data_event(mammotion_property_event)
170
+
171
+ def _disconnect(self) -> None:
172
+ """Disconnect the MQTT client."""
173
+ self._mqtt_client.disconnect()
174
+
175
+ @property
176
+ def waiting_queue(self) -> deque:
177
+ return self._waiting_queue
178
+
179
+
180
+ class MammotionBaseCloudDevice(MammotionBaseDevice):
181
+ """Base class for Mammotion Cloud devices."""
182
+
183
+ def __init__(self, mqtt: MammotionCloud, cloud_device: Device, state_manager: MowerStateManager) -> None:
184
+ """Initialize MammotionBaseCloudDevice."""
185
+ super().__init__(state_manager, cloud_device)
186
+ self.stopped = False
187
+ self.on_ready_callback: Callable[[], Awaitable[None]] | None = None
188
+ self.loop = asyncio.get_event_loop()
189
+ self._mqtt = mqtt
190
+ self.iot_id = cloud_device.iot_id
191
+ self.device = cloud_device
192
+ self._command_futures = {}
193
+ self._commands: MammotionCommand = MammotionCommand(
194
+ cloud_device.device_name,
195
+ int(mqtt.cloud_client.mammotion_http.response.data.userInformation.userAccount),
196
+ )
197
+ self.currentID = ""
198
+ self._mqtt.mqtt_message_event.add_subscribers(self._parse_message_for_device)
199
+ self._mqtt.mqtt_properties_event.add_subscribers(self._parse_message_properties_for_device)
200
+ self._mqtt.mqtt_status_event.add_subscribers(self._parse_message_status_for_device)
201
+ self._mqtt.mqtt_device_event.add_subscribers(self._parse_device_event_for_device)
202
+ self._mqtt.on_ready_event.add_subscribers(self.on_ready)
203
+ self._mqtt.on_disconnected_event.add_subscribers(self.on_disconnect)
204
+ self._mqtt.on_connected_event.add_subscribers(self.on_connect)
205
+ self.set_queue_callback(self.queue_command)
206
+
207
+ def __del__(self) -> None:
208
+ """Cleanup subscriptions."""
209
+ self._mqtt.on_ready_event.remove_subscribers(self.on_ready)
210
+ self._mqtt.on_disconnected_event.remove_subscribers(self.on_disconnect)
211
+ self._mqtt.on_connected_event.remove_subscribers(self.on_connect)
212
+ self._mqtt.mqtt_message_event.remove_subscribers(self._parse_message_for_device)
213
+ self._mqtt.mqtt_properties_event.remove_subscribers(self._parse_message_properties_for_device)
214
+ self._mqtt.mqtt_status_event.remove_subscribers(self._parse_message_status_for_device)
215
+ self._mqtt.mqtt_device_event.remove_subscribers(self._parse_device_event_for_device)
216
+ self._state_manager.cloud_queue_command_callback.remove_subscribers(self.queue_command)
217
+
218
+ @property
219
+ def command_sent_time(self) -> float:
220
+ return self._mqtt.command_sent_time
221
+
222
+ def set_notification_callback(self, func: Callable[[tuple[str, Any | None]], Awaitable[None]]) -> None:
223
+ self._state_manager.cloud_on_notification_callback.add_subscribers(func)
224
+
225
+ def set_queue_callback(self, func: Callable[[str, dict[str, Any]], Awaitable[None]]) -> None:
226
+ self._state_manager.cloud_queue_command_callback.add_subscribers(func)
227
+
228
+ async def on_ready(self) -> None:
229
+ """Callback for when MQTT is subscribed to events."""
230
+ if self.stopped:
231
+ return
232
+ try:
233
+ if self.on_ready_callback:
234
+ await self.on_ready_callback()
235
+ except (DeviceOfflineException, UnretryableException):
236
+ _LOGGER.debug("Device is offline")
237
+
238
+ async def on_disconnect(self) -> None:
239
+ self._mqtt.disconnect()
240
+
241
+ async def on_connect(self) -> None:
242
+ """On connect callback"""
243
+
244
+ def stop(self) -> None:
245
+ """Stop all tasks and disconnect."""
246
+ # self._mqtt._mqtt_client.unsubscribe()
247
+ if self.mqtt.is_connected():
248
+ self._mqtt.disconnect()
249
+ self.stopped = True
250
+
251
+ async def start(self) -> None:
252
+ """Start the device connection."""
253
+ self.stopped = False
254
+ if not self.mqtt.is_connected():
255
+ loop = asyncio.get_running_loop()
256
+ await loop.run_in_executor(None, self.mqtt.connect_async)
257
+ # else:
258
+ # self.mqtt._mqtt_client.thing_on_thing_enable(None)
259
+
260
+ async def _ble_sync(self) -> None:
261
+ pass
262
+
263
+ async def queue_command(self, key: str, **kwargs: Any) -> None:
264
+ # Create a future to hold the result
265
+ _LOGGER.debug("Queueing command: %s", key)
266
+ future = asyncio.Future()
267
+ # Put the command in the queue as a tuple (key, command, future)
268
+ command_bytes = getattr(self._commands, key)(**kwargs)
269
+ await self._mqtt.command_queue.put((self.iot_id, key, command_bytes, future))
270
+ # Wait for the future to be resolved
271
+ try:
272
+ await future
273
+ return
274
+ except asyncio.CancelledError:
275
+ """Try again once."""
276
+ future = asyncio.Future()
277
+ await self._mqtt.command_queue.put((self.iot_id, key, command_bytes, future))
278
+
279
+ def _extract_message_id(self, payload: dict) -> str:
280
+ """Extract the message ID from the payload."""
281
+ return payload.get("id", "")
282
+
283
+ def _extract_encoded_message(self, payload: dict) -> str:
284
+ """Extract the encoded message from the payload."""
285
+ try:
286
+ content = payload.get("data", {}).get("data", {}).get("params", {}).get("content", "")
287
+ return str(content)
288
+ except AttributeError:
289
+ _LOGGER.error("Error extracting encoded message. Payload: %s", payload)
290
+ return ""
291
+
292
+ @staticmethod
293
+ def dequeue_by_iot_id(queue, iot_id):
294
+ for item in queue:
295
+ if item.iot_id == iot_id:
296
+ queue.remove(item)
297
+ return item
298
+ return None
299
+
300
+ async def _parse_message_properties_for_device(self, event: ThingPropertiesMessage) -> None:
301
+ if event.params.iot_id != self.iot_id:
302
+ return
303
+ await self.state_manager.properties(event)
304
+
305
+ async def _parse_message_status_for_device(self, status: ThingStatusMessage) -> None:
306
+ if status.params.iot_id != self.iot_id:
307
+ return
308
+ await self.state_manager.status(status)
309
+
310
+ async def _parse_device_event_for_device(self, status: ThingStatusMessage) -> None:
311
+ """Process device event if it matches the device's IoT ID."""
312
+ if status.params.iot_id != self.iot_id:
313
+ return
314
+ await self.state_manager.device_event(status)
315
+
316
+ async def _parse_message_for_device(self, event: ThingEventMessage) -> None:
317
+ """Parses a message received from a device and updates internal state.
318
+
319
+ This function processes an incoming `ThingEventMessage`, checks if the message
320
+ is intended for this device, decodes the binary data, and updates raw data. It
321
+ then attempts to parse the binary data into a `LubaMsg`. If parsing fails, it
322
+ logs the exception. The function also handles setting the device product key if
323
+ not already set and processes specific sub-messages based on their types.
324
+
325
+ Args:
326
+ event (ThingEventMessage): The event message received from the device.
327
+
328
+ """
329
+ params = event.params
330
+ new_msg = LubaMsg()
331
+ if event.params.iot_id != self.iot_id:
332
+ return
333
+ binary_data = base64.b64decode(params.value.content)
334
+ try:
335
+ self._update_raw_data(binary_data)
336
+ new_msg = LubaMsg().parse(binary_data)
337
+ except (KeyError, ValueError, IndexError, UnicodeDecodeError):
338
+ _LOGGER.exception("Error parsing message %s", binary_data)
339
+
340
+ if (
341
+ self._commands.get_device_product_key() == ""
342
+ and self._commands.get_device_name() == event.params.device_name
343
+ ):
344
+ self._commands.set_device_product_key(event.params.product_key)
345
+
346
+ res = betterproto2.which_one_of(new_msg, "LubaSubMsg")
347
+ if res[0] == "net":
348
+ if new_msg.net.todev_ble_sync != 0 or new_msg.net.toapp_wifi_iot_status is not None:
349
+ return
350
+
351
+ await self._state_manager.notification(new_msg)
352
+
353
+ @property
354
+ def mqtt(self):
355
+ return self._mqtt
@@ -0,0 +1,48 @@
1
+ """Mower device with Bluetooth LE connectivity."""
2
+
3
+ import logging
4
+ from typing import Any
5
+
6
+ from bleak import BLEDevice
7
+
8
+ from pymammotion.aliyun.model.dev_by_account_response import Device
9
+ from pymammotion.data.mower_state_manager import MowerStateManager
10
+ from pymammotion.mammotion.devices.mammotion_bluetooth import MammotionBaseBLEDevice
11
+ from pymammotion.mammotion.devices.mower_device import MammotionMowerDevice
12
+
13
+ _LOGGER = logging.getLogger(__name__)
14
+
15
+
16
+ class MammotionMowerBLEDevice(MammotionBaseBLEDevice, MammotionMowerDevice):
17
+ """Mower device with BLE connectivity and map synchronization."""
18
+
19
+ def __init__(
20
+ self,
21
+ state_manager: MowerStateManager,
22
+ cloud_device: Device,
23
+ device: BLEDevice,
24
+ interface: int = 0,
25
+ **kwargs: Any,
26
+ ) -> None:
27
+ """Initialize MammotionMowerBLEDevice.
28
+
29
+ Uses multiple inheritance to combine:
30
+ - MammotionBaseBLEDevice: BLE communication
31
+ - MammotionMowerDevice: Map sync callbacks
32
+ """
33
+ # Initialize base BLE device (which also initializes MammotionBaseDevice)
34
+ MammotionBaseBLEDevice.__init__(self, state_manager, cloud_device, device, interface, **kwargs)
35
+ # Set up mower-specific BLE callbacks
36
+ self._state_manager.ble_gethash_ack_callback = self.datahash_response
37
+ self._state_manager.ble_get_commondata_ack_callback = self.commdata_response
38
+ self._state_manager.ble_get_plan_callback = self.plan_callback
39
+
40
+ def __del__(self) -> None:
41
+ """Cleanup subscriptions and callbacks."""
42
+ # Clean up mower-specific callbacks
43
+ if hasattr(self, "_state_manager"):
44
+ self._state_manager.ble_gethash_ack_callback = None
45
+ self._state_manager.ble_get_commondata_ack_callback = None
46
+ self._state_manager.ble_get_plan_callback = None
47
+ # Call parent cleanup
48
+ super().__del__()
@@ -0,0 +1,39 @@
1
+ """Mower device with cloud MQTT connectivity."""
2
+
3
+ import logging
4
+
5
+ from pymammotion.aliyun.model.dev_by_account_response import Device
6
+ from pymammotion.data.mower_state_manager import MowerStateManager
7
+ from pymammotion.mammotion.devices.mammotion_cloud import MammotionBaseCloudDevice, MammotionCloud
8
+ from pymammotion.mammotion.devices.mower_device import MammotionMowerDevice
9
+
10
+ _LOGGER = logging.getLogger(__name__)
11
+
12
+
13
+ class MammotionMowerCloudDevice(MammotionBaseCloudDevice, MammotionMowerDevice):
14
+ """Mower device with cloud connectivity and map synchronization."""
15
+
16
+ def __init__(self, mqtt: MammotionCloud, cloud_device: Device, state_manager: MowerStateManager) -> None:
17
+ """Initialize MammotionMowerCloudDevice.
18
+
19
+ Uses multiple inheritance to combine:
20
+ - MammotionBaseCloudDevice: MQTT communication
21
+ - MammotionMowerDevice: Map sync callbacks
22
+ """
23
+ # Initialize base cloud device (which also initializes MammotionBaseDevice)
24
+ super().__init__(mqtt, cloud_device, state_manager)
25
+ # Initialize mower device callbacks (but skip base device init as it's already done)
26
+ # We manually set the callbacks that MammotionMowerDevice would set
27
+ self._state_manager.cloud_gethash_ack_callback = self.datahash_response
28
+ self._state_manager.cloud_get_commondata_ack_callback = self.commdata_response
29
+ self._state_manager.cloud_get_plan_callback = self.plan_callback
30
+
31
+ def __del__(self) -> None:
32
+ """Cleanup subscriptions and callbacks."""
33
+ # Clean up mower-specific callbacks
34
+ if hasattr(self, "_state_manager"):
35
+ self._state_manager.cloud_gethash_ack_callback = None
36
+ self._state_manager.cloud_get_commondata_ack_callback = None
37
+ self._state_manager.cloud_get_plan_callback = None
38
+ # Call parent cleanup
39
+ super().__del__()
@@ -0,0 +1,81 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any
3
+
4
+ from bleak import BLEDevice
5
+
6
+ from pymammotion import CloudIOTGateway
7
+ from pymammotion.aliyun.model.dev_by_account_response import Device
8
+ from pymammotion.data.model.device import MowingDevice, RTKDevice
9
+ from pymammotion.data.model.enums import ConnectionPreference
10
+ from pymammotion.mammotion.devices.mammotion_cloud import MammotionCloud
11
+
12
+
13
+ class AbstractDeviceManager(ABC):
14
+ """Abstract base class for device managers."""
15
+
16
+ def __init__(
17
+ self,
18
+ name: str,
19
+ iot_id: str,
20
+ cloud_client: CloudIOTGateway,
21
+ cloud_device: Device,
22
+ preference: ConnectionPreference = ConnectionPreference.BLUETOOTH,
23
+ ) -> None:
24
+ self.name = name
25
+ self.iot_id = iot_id
26
+ self.cloud_client = cloud_client
27
+ self._device: Device = cloud_device
28
+ self.mammotion_http = cloud_client.mammotion_http
29
+ self.preference = preference
30
+
31
+ @property
32
+ @abstractmethod
33
+ def state(self) -> MowingDevice | RTKDevice:
34
+ """Return the state of the device."""
35
+
36
+ @state.setter
37
+ @abstractmethod
38
+ def state(self, value: MowingDevice | RTKDevice) -> None:
39
+ """Set the device state."""
40
+
41
+ @property
42
+ @abstractmethod
43
+ def ble(self) -> Any | None:
44
+ """Return BLE device interface."""
45
+
46
+ @property
47
+ @abstractmethod
48
+ def cloud(self) -> Any | None:
49
+ """Return cloud device interface."""
50
+
51
+ @abstractmethod
52
+ def has_queued_commands(self) -> bool:
53
+ """Check if there are queued commands."""
54
+
55
+ @abstractmethod
56
+ def add_ble(self, ble_device: BLEDevice) -> Any:
57
+ """Add BLE device."""
58
+
59
+ @abstractmethod
60
+ def add_cloud(self, mqtt: MammotionCloud) -> Any:
61
+ """Add cloud device."""
62
+
63
+ @abstractmethod
64
+ def replace_cloud(self, cloud_device: Any) -> None:
65
+ """Replace cloud device."""
66
+
67
+ @abstractmethod
68
+ def remove_cloud(self) -> None:
69
+ """Remove cloud device."""
70
+
71
+ @abstractmethod
72
+ def replace_ble(self, ble_device: Any) -> None:
73
+ """Replace BLE device."""
74
+
75
+ @abstractmethod
76
+ def remove_ble(self) -> None:
77
+ """Remove BLE device."""
78
+
79
+ @abstractmethod
80
+ def replace_mqtt(self, mqtt: MammotionCloud) -> None:
81
+ """Replace MQTT connection."""
@@ -0,0 +1,120 @@
1
+ """Mower-specific device class with map synchronization callbacks."""
2
+
3
+ from abc import ABC
4
+ import logging
5
+
6
+ from pymammotion.aliyun.model.dev_by_account_response import Device
7
+ from pymammotion.data.model import RegionData
8
+ from pymammotion.data.mower_state_manager import MowerStateManager
9
+ from pymammotion.mammotion.devices.base import MammotionBaseDevice
10
+ from pymammotion.proto import NavGetCommDataAck, NavGetHashListAck, NavPlanJobSet, SvgMessageAckT
11
+ from pymammotion.utility.device_type import DeviceType
12
+
13
+ _LOGGER = logging.getLogger(__name__)
14
+
15
+
16
+ def find_next_integer(lst: list[int], current_hash: int) -> int | None:
17
+ """Find the next integer in a list after the current hash."""
18
+ try:
19
+ current_index = lst.index(current_hash)
20
+ if current_index + 1 < len(lst):
21
+ return lst[current_index + 1]
22
+ else:
23
+ return None
24
+ except ValueError:
25
+ return None
26
+
27
+
28
+ class MammotionMowerDevice(MammotionBaseDevice, ABC):
29
+ """Mower device with map synchronization support."""
30
+
31
+ def __init__(self, state_manager: MowerStateManager, cloud_device: Device) -> None:
32
+ """Initialize MammotionMowerDevice."""
33
+ super().__init__(state_manager, cloud_device)
34
+
35
+ async def datahash_response(self, hash_ack: NavGetHashListAck) -> None:
36
+ """Handle datahash responses for root level hashs."""
37
+ current_frame = hash_ack.current_frame
38
+
39
+ missing_frames = self.mower.map.missing_root_hash_frame(hash_ack)
40
+ if len(missing_frames) == 0:
41
+ if len(self.mower.map.missing_hashlist(hash_ack.sub_cmd)) > 0:
42
+ data_hash = self.mower.map.missing_hashlist(hash_ack.sub_cmd).pop(0)
43
+ await self.queue_command("synchronize_hash_data", hash_num=data_hash)
44
+ return
45
+
46
+ if current_frame != missing_frames[0] - 1:
47
+ current_frame = missing_frames[0] - 1
48
+ await self.queue_command("get_hash_response", total_frame=hash_ack.total_frame, current_frame=current_frame)
49
+
50
+ async def commdata_response(self, common_data: NavGetCommDataAck | SvgMessageAckT) -> None:
51
+ """Handle common data responses."""
52
+ total_frame = common_data.total_frame
53
+ current_frame = common_data.current_frame
54
+
55
+ missing_frames = self.mower.map.missing_frame(common_data)
56
+ if len(missing_frames) == 0:
57
+ # get next in hash ack list
58
+ data_hash = (
59
+ self.mower.map.missing_hashlist(common_data.sub_cmd).pop(0)
60
+ if len(self.mower.map.missing_hashlist(common_data.sub_cmd)) > 0
61
+ else None
62
+ )
63
+ if data_hash is None:
64
+ return
65
+
66
+ await self.queue_command("synchronize_hash_data", hash_num=data_hash)
67
+ else:
68
+ if current_frame != missing_frames[0] - 1:
69
+ current_frame = missing_frames[0] - 1
70
+
71
+ region_data = RegionData()
72
+ region_data.hash = common_data.data_hash if isinstance(common_data, SvgMessageAckT) else common_data.hash
73
+ region_data.action = common_data.action if isinstance(common_data, NavGetCommDataAck) else 0
74
+ region_data.type = common_data.type
75
+ region_data.sub_cmd = common_data.sub_cmd
76
+ region_data.total_frame = total_frame
77
+ region_data.current_frame = current_frame
78
+ await self.queue_command("get_regional_data", regional_data=region_data)
79
+
80
+ async def plan_callback(self, plan: NavPlanJobSet) -> None:
81
+ """Handle plan job responses."""
82
+ if plan.plan_index < plan.total_plan_num - 1:
83
+ index = plan.plan_index + 1
84
+ await self.queue_command("read_plan", sub_cmd=2, plan_index=index)
85
+
86
+ async def start_schedule_sync(self) -> None:
87
+ """Start sync of schedule data."""
88
+ if len(self.mower.map.plan) == 0 or list(self.mower.map.plan.values())[0].total_plan_num != len(
89
+ self.mower.map.plan
90
+ ):
91
+ await self.queue_command("read_plan", sub_cmd=2, plan_index=0)
92
+
93
+ async def start_map_sync(self) -> None:
94
+ """Start sync of map data."""
95
+ if location := next((loc for loc in self.mower.report_data.locations if loc.pos_type == 5), None):
96
+ self.mower.map.update_hash_lists(self.mower.map.hashlist, location.bol_hash)
97
+
98
+ await self.queue_command("send_todev_ble_sync", sync_type=3)
99
+
100
+ # TODO correctly check if area names exist for a zone.
101
+ if self._cloud_device and len(self.mower.map.area_name) == 0 and not DeviceType.is_luba1(self.mower.name):
102
+ await self.queue_command("get_area_name_list", device_id=self._cloud_device.iot_id)
103
+
104
+ if len(self.mower.map.root_hash_lists) == 0 or len(self.mower.map.missing_hashlist()) > 0:
105
+ await self.queue_command("get_all_boundary_hash_list", sub_cmd=0)
106
+
107
+ for hash_id, frame in list(self.mower.map.area.items()):
108
+ missing_frames = self.mower.map.find_missing_frames(frame)
109
+ if len(missing_frames) > 0:
110
+ del self.mower.map.area[hash_id]
111
+
112
+ for hash_id, frame in list(self.mower.map.path.items()):
113
+ missing_frames = self.mower.map.find_missing_frames(frame)
114
+ if len(missing_frames) > 0:
115
+ del self.mower.map.path[hash_id]
116
+
117
+ for hash_id, frame in list(self.mower.map.obstacle.items()):
118
+ missing_frames = self.mower.map.find_missing_frames(frame)
119
+ if len(missing_frames) > 0:
120
+ del self.mower.map.obstacle[hash_id]