pymammotion 0.5.27__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 (56) hide show
  1. pymammotion/__init__.py +3 -3
  2. pymammotion/aliyun/client.py +3 -0
  3. pymammotion/aliyun/cloud_gateway.py +117 -19
  4. pymammotion/aliyun/model/dev_by_account_response.py +198 -20
  5. pymammotion/const.py +3 -0
  6. pymammotion/data/model/device.py +1 -0
  7. pymammotion/data/model/device_config.py +1 -1
  8. pymammotion/data/model/enums.py +5 -3
  9. pymammotion/data/model/generate_route_information.py +2 -2
  10. pymammotion/data/model/hash_list.py +113 -33
  11. pymammotion/data/model/region_data.py +4 -4
  12. pymammotion/data/{state_manager.py → mower_state_manager.py} +17 -7
  13. pymammotion/data/mqtt/event.py +47 -22
  14. pymammotion/data/mqtt/mammotion_properties.py +257 -0
  15. pymammotion/data/mqtt/properties.py +32 -29
  16. pymammotion/data/mqtt/status.py +17 -16
  17. pymammotion/homeassistant/__init__.py +3 -0
  18. pymammotion/homeassistant/mower_api.py +446 -0
  19. pymammotion/homeassistant/rtk_api.py +54 -0
  20. pymammotion/http/http.py +431 -18
  21. pymammotion/http/model/http.py +82 -2
  22. pymammotion/http/model/response_factory.py +10 -4
  23. pymammotion/mammotion/commands/mammotion_command.py +20 -0
  24. pymammotion/mammotion/commands/messages/navigation.py +10 -6
  25. pymammotion/mammotion/commands/messages/system.py +0 -14
  26. pymammotion/mammotion/devices/__init__.py +27 -3
  27. pymammotion/mammotion/devices/base.py +22 -146
  28. pymammotion/mammotion/devices/mammotion.py +367 -206
  29. pymammotion/mammotion/devices/mammotion_bluetooth.py +8 -5
  30. pymammotion/mammotion/devices/mammotion_cloud.py +47 -83
  31. pymammotion/mammotion/devices/mammotion_mower_ble.py +49 -0
  32. pymammotion/mammotion/devices/mammotion_mower_cloud.py +39 -0
  33. pymammotion/mammotion/devices/managers/managers.py +81 -0
  34. pymammotion/mammotion/devices/mower_device.py +121 -0
  35. pymammotion/mammotion/devices/mower_manager.py +107 -0
  36. pymammotion/mammotion/devices/rtk_ble.py +89 -0
  37. pymammotion/mammotion/devices/rtk_cloud.py +113 -0
  38. pymammotion/mammotion/devices/rtk_device.py +50 -0
  39. pymammotion/mammotion/devices/rtk_manager.py +122 -0
  40. pymammotion/mqtt/__init__.py +2 -1
  41. pymammotion/mqtt/aliyun_mqtt.py +232 -0
  42. pymammotion/mqtt/mammotion_mqtt.py +174 -192
  43. pymammotion/mqtt/mqtt_models.py +66 -0
  44. pymammotion/proto/__init__.py +2 -2
  45. pymammotion/proto/mctrl_nav.proto +2 -2
  46. pymammotion/proto/mctrl_nav_pb2.py +1 -1
  47. pymammotion/proto/mctrl_nav_pb2.pyi +4 -4
  48. pymammotion/proto/mctrl_sys.proto +1 -1
  49. pymammotion/utility/datatype_converter.py +13 -12
  50. pymammotion/utility/device_type.py +88 -3
  51. pymammotion/utility/mur_mur_hash.py +132 -87
  52. {pymammotion-0.5.27.dist-info → pymammotion-0.5.44.dist-info}/METADATA +25 -30
  53. {pymammotion-0.5.27.dist-info → pymammotion-0.5.44.dist-info}/RECORD +61 -47
  54. {pymammotion-0.5.27.dist-info → pymammotion-0.5.44.dist-info}/WHEEL +1 -1
  55. pymammotion/http/_init_.py +0 -0
  56. {pymammotion-0.5.27.dist-info → pymammotion-0.5.44.dist-info/licenses}/LICENSE +0 -0
@@ -17,7 +17,7 @@ from bleak_retry_connector import (
17
17
 
18
18
  from pymammotion.aliyun.model.dev_by_account_response import Device
19
19
  from pymammotion.bluetooth import BleMessage
20
- from pymammotion.data.state_manager import StateManager
20
+ from pymammotion.data.mower_state_manager import MowerStateManager
21
21
  from pymammotion.mammotion.commands.mammotion_command import MammotionCommand
22
22
  from pymammotion.mammotion.devices.base import MammotionBaseDevice
23
23
  from pymammotion.proto import LubaMsg
@@ -72,10 +72,16 @@ class MammotionBaseBLEDevice(MammotionBaseDevice):
72
72
  """Base class for Mammotion BLE devices."""
73
73
 
74
74
  def __init__(
75
- self, state_manager: StateManager, cloud_device: Device, device: BLEDevice, interface: int = 0, **kwargs: Any
75
+ self,
76
+ state_manager: MowerStateManager,
77
+ cloud_device: Device,
78
+ device: BLEDevice,
79
+ interface: int = 0,
80
+ **kwargs: Any,
76
81
  ) -> None:
77
82
  """Initialize MammotionBaseBLEDevice."""
78
83
  super().__init__(state_manager, cloud_device)
84
+ self.command_sent_time = 0
79
85
  self._disconnect_strategy = True
80
86
  self._ble_sync_task = None
81
87
  self._prev_notification = None
@@ -94,9 +100,6 @@ class MammotionBaseBLEDevice(MammotionBaseDevice):
94
100
  self._key: str | None = None
95
101
  self._cloud_device = cloud_device
96
102
  self.set_queue_callback(self.queue_command)
97
- self._state_manager.ble_gethash_ack_callback = self.datahash_response
98
- self._state_manager.ble_get_commondata_ack_callback = self.commdata_response
99
- self._state_manager.ble_get_plan_callback = self.plan_callback
100
103
  loop = asyncio.get_event_loop()
101
104
  loop.create_task(self.process_queue())
102
105
 
@@ -1,5 +1,5 @@
1
1
  import asyncio
2
- from asyncio import InvalidStateError, TimerHandle
2
+ from asyncio import InvalidStateError
3
3
  import base64
4
4
  from collections import deque
5
5
  from collections.abc import Awaitable, Callable
@@ -11,18 +11,17 @@ from typing import Any
11
11
  import betterproto2
12
12
  from Tea.exceptions import UnretryableException
13
13
 
14
- from pymammotion import CloudIOTGateway, MammotionMQTT
15
- from pymammotion.aliyun.cloud_gateway import CheckSessionException, DeviceOfflineException, SetupException
14
+ from pymammotion import AliyunMQTT, CloudIOTGateway, MammotionMQTT
15
+ from pymammotion.aliyun.cloud_gateway import DeviceOfflineException
16
16
  from pymammotion.aliyun.model.dev_by_account_response import Device
17
- from pymammotion.data.mqtt.event import ThingEventMessage
18
- from pymammotion.data.mqtt.properties import ThingPropertiesMessage
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
19
20
  from pymammotion.data.mqtt.status import ThingStatusMessage
20
- from pymammotion.data.state_manager import StateManager
21
21
  from pymammotion.event.event import DataEvent
22
22
  from pymammotion.mammotion.commands.mammotion_command import MammotionCommand
23
23
  from pymammotion.mammotion.devices.base import MammotionBaseDevice
24
24
  from pymammotion.proto import LubaMsg
25
- from pymammotion.utility.constant.device_constant import NO_REQUEST_MODES
26
25
 
27
26
  _LOGGER = logging.getLogger(__name__)
28
27
 
@@ -30,9 +29,10 @@ _LOGGER = logging.getLogger(__name__)
30
29
  class MammotionCloud:
31
30
  """Per account MQTT cloud."""
32
31
 
33
- def __init__(self, mqtt_client: MammotionMQTT, cloud_client: CloudIOTGateway) -> None:
32
+ def __init__(self, mqtt_client: AliyunMQTT | MammotionMQTT, cloud_client: CloudIOTGateway) -> None:
34
33
  """Initialize MammotionCloud."""
35
34
  self.cloud_client = cloud_client
35
+ self.command_sent_time = 0
36
36
  self.loop = asyncio.get_event_loop()
37
37
  self.is_ready = False
38
38
  self.command_queue = asyncio.Queue()
@@ -69,7 +69,7 @@ class MammotionCloud:
69
69
  self._mqtt_client.connect_async()
70
70
 
71
71
  async def send_command(self, iot_id: str, command: bytes) -> None:
72
- await self._mqtt_client.get_cloud_client().send_cloud_command(iot_id, command)
72
+ await self._mqtt_client.send_cloud_command(iot_id, command)
73
73
 
74
74
  async def on_connected(self) -> None:
75
75
  """Callback for when MQTT connects."""
@@ -104,19 +104,17 @@ class MammotionCloud:
104
104
  assert self._mqtt_client is not None
105
105
  self._key = key
106
106
  _LOGGER.debug("Sending command: %s", key)
107
- await self._mqtt_client.get_cloud_client().send_cloud_command(iot_id, command)
108
107
  self.command_sent_time = time.time()
108
+ await self._mqtt_client.send_cloud_command(iot_id, command)
109
109
 
110
- async def _on_mqtt_message(self, topic: str, payload: str, iot_id: str) -> None:
110
+ async def _on_mqtt_message(self, topic: str, payload: bytes, iot_id: str) -> None:
111
111
  """Handle incoming MQTT messages."""
112
- _LOGGER.debug("MQTT message received on topic %s: %s, iot_id: %s", topic, payload, iot_id)
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)
113
116
 
114
- json_str = json.dumps(payload)
115
- payload = json.loads(json_str)
116
-
117
- await self._parse_mqtt_response(topic, payload)
118
-
119
- async def _parse_mqtt_response(self, topic: str, payload: dict) -> None:
117
+ async def _parse_mqtt_response(self, topic: str, payload: dict, iot_id: str) -> None:
120
118
  """Parse and handle MQTT responses based on the topic.
121
119
 
122
120
  This function processes different types of MQTT messages received from various
@@ -151,31 +149,41 @@ class MammotionCloud:
151
149
  property_event = ThingPropertiesMessage.from_dict(payload)
152
150
  await self.mqtt_properties_event.data_event(property_event)
153
151
 
152
+ if topic.endswith("/thing/event/device_protobuf_msg_event/post"):
153
+ _LOGGER.debug("Mammotion Thing event received")
154
+ mammotion_event = MammotionEventMessage.from_dict(payload)
155
+ mammotion_event.params.iot_id = iot_id
156
+ await self.mqtt_message_event.data_event(mammotion_event)
157
+ elif topic.endswith("/thing/event/property/post"):
158
+ _LOGGER.debug("Mammotion Property event received")
159
+ mammotion_property_event = MammotionPropertiesMessage.from_dict(payload)
160
+ mammotion_property_event.params.iot_id = iot_id
161
+ await self.mqtt_properties_event.data_event(mammotion_property_event)
162
+
154
163
  def _disconnect(self) -> None:
155
164
  """Disconnect the MQTT client."""
156
165
  self._mqtt_client.disconnect()
157
166
 
158
167
  @property
159
- def waiting_queue(self):
168
+ def waiting_queue(self) -> deque:
160
169
  return self._waiting_queue
161
170
 
162
171
 
163
172
  class MammotionBaseCloudDevice(MammotionBaseDevice):
164
173
  """Base class for Mammotion Cloud devices."""
165
174
 
166
- def __init__(self, mqtt: MammotionCloud, cloud_device: Device, state_manager: StateManager) -> None:
175
+ def __init__(self, mqtt: MammotionCloud, cloud_device: Device, state_manager: MowerStateManager) -> None:
167
176
  """Initialize MammotionBaseCloudDevice."""
168
177
  super().__init__(state_manager, cloud_device)
169
- self._ble_sync_task: TimerHandle | None = None
170
178
  self.stopped = False
171
179
  self.on_ready_callback: Callable[[], Awaitable[None]] | None = None
172
180
  self.loop = asyncio.get_event_loop()
173
181
  self._mqtt = mqtt
174
- self.iot_id = cloud_device.iotId
182
+ self.iot_id = cloud_device.iot_id
175
183
  self.device = cloud_device
176
184
  self._command_futures = {}
177
185
  self._commands: MammotionCommand = MammotionCommand(
178
- cloud_device.deviceName,
186
+ cloud_device.device_name,
179
187
  int(mqtt.cloud_client.mammotion_http.response.data.userInformation.userAccount),
180
188
  )
181
189
  self.currentID = ""
@@ -186,15 +194,10 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
186
194
  self._mqtt.on_ready_event.add_subscribers(self.on_ready)
187
195
  self._mqtt.on_disconnected_event.add_subscribers(self.on_disconnect)
188
196
  self._mqtt.on_connected_event.add_subscribers(self.on_connect)
189
- self._state_manager.cloud_gethash_ack_callback = self.datahash_response
190
- self._state_manager.cloud_get_commondata_ack_callback = self.commdata_response
191
- self._state_manager.cloud_get_plan_callback = self.plan_callback
192
197
  self.set_queue_callback(self.queue_command)
193
198
 
194
- if self._mqtt.is_ready:
195
- self.run_periodic_sync_task()
196
-
197
199
  def __del__(self) -> None:
200
+ """Cleanup subscriptions."""
198
201
  self._mqtt.on_ready_event.remove_subscribers(self.on_ready)
199
202
  self._mqtt.on_disconnected_event.remove_subscribers(self.on_disconnect)
200
203
  self._mqtt.on_connected_event.remove_subscribers(self.on_connect)
@@ -202,16 +205,12 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
202
205
  self._mqtt.mqtt_properties_event.remove_subscribers(self._parse_message_properties_for_device)
203
206
  self._mqtt.mqtt_status_event.remove_subscribers(self._parse_message_status_for_device)
204
207
  self._mqtt.mqtt_device_event.remove_subscribers(self._parse_device_event_for_device)
205
- self._state_manager.cloud_gethash_ack_callback = None
206
- self._state_manager.cloud_get_commondata_ack_callback = None
207
- self._state_manager.cloud_get_plan_callback = None
208
- if self._ble_sync_task:
209
- self._ble_sync_task.cancel()
210
-
211
- def __del__(self) -> None:
212
- """Cleanup."""
213
208
  self._state_manager.cloud_queue_command_callback.remove_subscribers(self.queue_command)
214
209
 
210
+ @property
211
+ def command_sent_time(self) -> float:
212
+ return self._mqtt.command_sent_time
213
+
215
214
  def set_notification_callback(self, func: Callable[[tuple[str, Any | None]], Awaitable[None]]) -> None:
216
215
  self._state_manager.cloud_on_notification_callback.add_subscribers(func)
217
216
 
@@ -229,26 +228,18 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
229
228
  _LOGGER.debug("Device is offline")
230
229
 
231
230
  async def on_disconnect(self) -> None:
232
- if self._ble_sync_task:
233
- self._ble_sync_task.cancel()
234
231
  self._mqtt.disconnect()
235
232
 
236
233
  async def on_connect(self) -> None:
237
- await self._ble_sync()
238
- if self._ble_sync_task is None or self._ble_sync_task.cancelled():
239
- await self.run_periodic_sync_task()
234
+ """On connect callback"""
240
235
 
241
236
  async def stop(self) -> None:
242
237
  """Stop all tasks and disconnect."""
243
- if self._ble_sync_task:
244
- self._ble_sync_task.cancel()
245
238
  # self._mqtt._mqtt_client.unsubscribe()
246
239
  self.stopped = True
247
240
 
248
241
  async def start(self) -> None:
249
- await self._ble_sync()
250
- if self._ble_sync_task is None or self._ble_sync_task.cancelled():
251
- await self.run_periodic_sync_task()
242
+ """Start the device connection."""
252
243
  self.stopped = False
253
244
  if not self.mqtt.is_connected():
254
245
  loop = asyncio.get_running_loop()
@@ -257,35 +248,7 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
257
248
  # self.mqtt._mqtt_client.thing_on_thing_enable(None)
258
249
 
259
250
  async def _ble_sync(self) -> None:
260
- if (
261
- self.stopped
262
- or self.mqtt.is_connected is False
263
- or self.state_manager.get_device().report_data.dev.sys_status in NO_REQUEST_MODES
264
- ):
265
- return
266
-
267
- command_bytes = self._commands.send_todev_ble_sync(3)
268
- try:
269
- await self._mqtt.send_command(self.iot_id, command_bytes)
270
- except (CheckSessionException, SetupException, DeviceOfflineException):
271
- if self._ble_sync_task:
272
- self._ble_sync_task.cancel()
273
-
274
- async def run_periodic_sync_task(self) -> None:
275
- """Send ble sync to robot."""
276
- try:
277
- if not self._mqtt._operation_lock.locked() or not self.stopped:
278
- await self._ble_sync()
279
- finally:
280
- if not self.stopped:
281
- self.schedule_ble_sync()
282
-
283
- def schedule_ble_sync(self) -> None:
284
- """Periodically sync to keep connection alive."""
285
- if self._mqtt is not None and self._mqtt.is_connected:
286
- self._ble_sync_task = self.loop.call_later(
287
- 160, lambda: asyncio.ensure_future(self.run_periodic_sync_task())
288
- )
251
+ pass
289
252
 
290
253
  async def queue_command(self, key: str, **kwargs: Any) -> None:
291
254
  # Create a future to hold the result
@@ -296,7 +259,8 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
296
259
  await self._mqtt.command_queue.put((self.iot_id, key, command_bytes, future))
297
260
  # Wait for the future to be resolved
298
261
  try:
299
- return await future
262
+ await future
263
+ return
300
264
  except asyncio.CancelledError:
301
265
  """Try again once."""
302
266
  future = asyncio.Future()
@@ -324,18 +288,18 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
324
288
  return None
325
289
 
326
290
  async def _parse_message_properties_for_device(self, event: ThingPropertiesMessage) -> None:
327
- if event.params.iotId != self.iot_id:
291
+ if event.params.iot_id != self.iot_id:
328
292
  return
329
293
  await self.state_manager.properties(event)
330
294
 
331
295
  async def _parse_message_status_for_device(self, status: ThingStatusMessage) -> None:
332
- if status.params.iotId != self.iot_id:
296
+ if status.params.iot_id != self.iot_id:
333
297
  return
334
298
  await self.state_manager.status(status)
335
299
 
336
300
  async def _parse_device_event_for_device(self, status: ThingStatusMessage) -> None:
337
301
  """Process device event if it matches the device's IoT ID."""
338
- if status.params.iotId != self.iot_id:
302
+ if status.params.iot_id != self.iot_id:
339
303
  return
340
304
  await self.state_manager.device_event(status)
341
305
 
@@ -354,7 +318,7 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
354
318
  """
355
319
  params = event.params
356
320
  new_msg = LubaMsg()
357
- if event.params.iotId != self.iot_id:
321
+ if event.params.iot_id != self.iot_id:
358
322
  return
359
323
  binary_data = base64.b64decode(params.value.content)
360
324
  try:
@@ -365,9 +329,9 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
365
329
 
366
330
  if (
367
331
  self._commands.get_device_product_key() == ""
368
- and self._commands.get_device_name() == event.params.deviceName
332
+ and self._commands.get_device_name() == event.params.device_name
369
333
  ):
370
- self._commands.set_device_product_key(event.params.productKey)
334
+ self._commands.set_device_product_key(event.params.product_key)
371
335
 
372
336
  res = betterproto2.which_one_of(new_msg, "LubaSubMsg")
373
337
  if res[0] == "net":
@@ -0,0 +1,49 @@
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
+ if MammotionBaseBLEDevice.__del__:
49
+ 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
+
3
+ from bleak import BLEDevice
4
+
5
+ from pymammotion import CloudIOTGateway
6
+ from pymammotion.aliyun.model.dev_by_account_response import Device
7
+ from pymammotion.data.model.device import MowingDevice, RTKDevice
8
+ from pymammotion.data.model.enums import ConnectionPreference
9
+ from pymammotion.mammotion.devices.mammotion_bluetooth import MammotionBaseBLEDevice
10
+ from pymammotion.mammotion.devices.mammotion_cloud import MammotionBaseCloudDevice, 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) -> MammotionBaseBLEDevice | None:
44
+ """Return BLE device interface."""
45
+
46
+ @property
47
+ @abstractmethod
48
+ def cloud(self) -> MammotionBaseCloudDevice | 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) -> MammotionBaseBLEDevice:
57
+ """Add BLE device."""
58
+
59
+ @abstractmethod
60
+ def add_cloud(self, mqtt: MammotionCloud) -> MammotionBaseCloudDevice:
61
+ """Add cloud device."""
62
+
63
+ @abstractmethod
64
+ def replace_cloud(self, cloud_device: MammotionBaseCloudDevice) -> 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: MammotionBaseBLEDevice) -> 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,121 @@
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
+ # Register mower-specific callbacks
35
+ self._state_manager.cloud_gethash_ack_callback = self.datahash_response
36
+ self._state_manager.cloud_get_commondata_ack_callback = self.commdata_response
37
+ self._state_manager.cloud_get_plan_callback = self.plan_callback
38
+
39
+ async def datahash_response(self, hash_ack: NavGetHashListAck) -> None:
40
+ """Handle datahash responses for root level hashs."""
41
+ current_frame = hash_ack.current_frame
42
+
43
+ missing_frames = self.mower.map.missing_root_hash_frame(hash_ack)
44
+ if len(missing_frames) == 0:
45
+ if len(self.mower.map.missing_hashlist(hash_ack.sub_cmd)) > 0:
46
+ data_hash = self.mower.map.missing_hashlist(hash_ack.sub_cmd).pop(0)
47
+ await self.queue_command("synchronize_hash_data", hash_num=data_hash)
48
+ return
49
+
50
+ if current_frame != missing_frames[0] - 1:
51
+ current_frame = missing_frames[0] - 1
52
+ await self.queue_command("get_hash_response", total_frame=hash_ack.total_frame, current_frame=current_frame)
53
+
54
+ async def commdata_response(self, common_data: NavGetCommDataAck | SvgMessageAckT) -> None:
55
+ """Handle common data responses."""
56
+ total_frame = common_data.total_frame
57
+ current_frame = common_data.current_frame
58
+
59
+ missing_frames = self.mower.map.missing_frame(common_data)
60
+ if len(missing_frames) == 0:
61
+ # get next in hash ack list
62
+ data_hash = (
63
+ self.mower.map.missing_hashlist(common_data.sub_cmd).pop(0)
64
+ if len(self.mower.map.missing_hashlist(common_data.sub_cmd)) > 0
65
+ else None
66
+ )
67
+ if data_hash is None:
68
+ return
69
+
70
+ await self.queue_command("synchronize_hash_data", hash_num=data_hash)
71
+ else:
72
+ if current_frame != missing_frames[0] - 1:
73
+ current_frame = missing_frames[0] - 1
74
+
75
+ region_data = RegionData()
76
+ region_data.hash = common_data.data_hash if isinstance(common_data, SvgMessageAckT) else common_data.hash
77
+ region_data.action = common_data.action if isinstance(common_data, NavGetCommDataAck) else 0
78
+ region_data.type = common_data.type
79
+ region_data.sub_cmd = common_data.sub_cmd
80
+ region_data.total_frame = total_frame
81
+ region_data.current_frame = current_frame
82
+ await self.queue_command("get_regional_data", regional_data=region_data)
83
+
84
+ async def plan_callback(self, plan: NavPlanJobSet) -> None:
85
+ """Handle plan job responses."""
86
+ if plan.plan_index < plan.total_plan_num - 1:
87
+ index = plan.plan_index + 1
88
+ await self.queue_command("read_plan", sub_cmd=2, plan_index=index)
89
+
90
+ async def start_map_sync(self) -> None:
91
+ """Start sync of map data."""
92
+ if location := next((loc for loc in self.mower.report_data.locations if loc.pos_type == 5), None):
93
+ self.mower.map.update_hash_lists(self.mower.map.hashlist, location.bol_hash)
94
+
95
+ await self.queue_command("send_todev_ble_sync", sync_type=3)
96
+
97
+ if self._cloud_device and len(self.mower.map.area_name) == 0 and not DeviceType.is_luba1(self.mower.name):
98
+ await self.queue_command("get_area_name_list", device_id=self._cloud_device.iot_id)
99
+
100
+ if len(self.mower.map.root_hash_lists) == 0 or len(self.mower.map.missing_hashlist()) > 0:
101
+ await self.queue_command("get_all_boundary_hash_list", sub_cmd=0)
102
+
103
+ if len(self.mower.map.plan) == 0 or list(self.mower.map.plan.values())[0].total_plan_num != len(
104
+ self.mower.map.plan
105
+ ):
106
+ await self.queue_command("read_plan", sub_cmd=2, plan_index=0)
107
+
108
+ for hash_id, frame in list(self.mower.map.area.items()):
109
+ missing_frames = self.mower.map.find_missing_frames(frame)
110
+ if len(missing_frames) > 0:
111
+ del self.mower.map.area[hash_id]
112
+
113
+ for hash_id, frame in list(self.mower.map.path.items()):
114
+ missing_frames = self.mower.map.find_missing_frames(frame)
115
+ if len(missing_frames) > 0:
116
+ del self.mower.map.path[hash_id]
117
+
118
+ for hash_id, frame in list(self.mower.map.obstacle.items()):
119
+ missing_frames = self.mower.map.find_missing_frames(frame)
120
+ if len(missing_frames) > 0:
121
+ del self.mower.map.obstacle[hash_id]