pymammotion 0.5.32__py3-none-any.whl → 0.5.45__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 +114 -18
  3. pymammotion/aliyun/model/dev_by_account_response.py +169 -21
  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 +392 -16
  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 -139
  26. pymammotion/mammotion/devices/mammotion.py +361 -203
  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.32.dist-info → pymammotion-0.5.45.dist-info}/METADATA +25 -31
  51. {pymammotion-0.5.32.dist-info → pymammotion-0.5.45.dist-info}/RECORD +59 -45
  52. {pymammotion-0.5.32.dist-info → pymammotion-0.5.45.dist-info}/WHEEL +1 -1
  53. pymammotion/http/_init_.py +0 -0
  54. {pymammotion-0.5.32.dist-info → pymammotion-0.5.45.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,7 +72,12 @@ 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)
@@ -95,9 +100,6 @@ class MammotionBaseBLEDevice(MammotionBaseDevice):
95
100
  self._key: str | None = None
96
101
  self._cloud_device = cloud_device
97
102
  self.set_queue_callback(self.queue_command)
98
- self._state_manager.ble_gethash_ack_callback = self.datahash_response
99
- self._state_manager.ble_get_commondata_ack_callback = self.commdata_response
100
- self._state_manager.ble_get_plan_callback = self.plan_callback
101
103
  loop = asyncio.get_event_loop()
102
104
  loop.create_task(self.process_queue())
103
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,7 +29,7 @@ _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
36
35
  self.command_sent_time = 0
@@ -70,7 +69,7 @@ class MammotionCloud:
70
69
  self._mqtt_client.connect_async()
71
70
 
72
71
  async def send_command(self, iot_id: str, command: bytes) -> None:
73
- await self._mqtt_client.get_cloud_client().send_cloud_command(iot_id, command)
72
+ await self._mqtt_client.send_cloud_command(iot_id, command)
74
73
 
75
74
  async def on_connected(self) -> None:
76
75
  """Callback for when MQTT connects."""
@@ -106,18 +105,16 @@ class MammotionCloud:
106
105
  self._key = key
107
106
  _LOGGER.debug("Sending command: %s", key)
108
107
  self.command_sent_time = time.time()
109
- await self._mqtt_client.get_cloud_client().send_cloud_command(iot_id, command)
108
+ await self._mqtt_client.send_cloud_command(iot_id, command)
110
109
 
111
- 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:
112
111
  """Handle incoming MQTT messages."""
113
- _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)
114
116
 
115
- json_str = json.dumps(payload)
116
- payload = json.loads(json_str)
117
-
118
- await self._parse_mqtt_response(topic, payload)
119
-
120
- 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:
121
118
  """Parse and handle MQTT responses based on the topic.
122
119
 
123
120
  This function processes different types of MQTT messages received from various
@@ -152,31 +149,41 @@ class MammotionCloud:
152
149
  property_event = ThingPropertiesMessage.from_dict(payload)
153
150
  await self.mqtt_properties_event.data_event(property_event)
154
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
+
155
163
  def _disconnect(self) -> None:
156
164
  """Disconnect the MQTT client."""
157
165
  self._mqtt_client.disconnect()
158
166
 
159
167
  @property
160
- def waiting_queue(self):
168
+ def waiting_queue(self) -> deque:
161
169
  return self._waiting_queue
162
170
 
163
171
 
164
172
  class MammotionBaseCloudDevice(MammotionBaseDevice):
165
173
  """Base class for Mammotion Cloud devices."""
166
174
 
167
- 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:
168
176
  """Initialize MammotionBaseCloudDevice."""
169
177
  super().__init__(state_manager, cloud_device)
170
- self._ble_sync_task: TimerHandle | None = None
171
178
  self.stopped = False
172
179
  self.on_ready_callback: Callable[[], Awaitable[None]] | None = None
173
180
  self.loop = asyncio.get_event_loop()
174
181
  self._mqtt = mqtt
175
- self.iot_id = cloud_device.iotId
182
+ self.iot_id = cloud_device.iot_id
176
183
  self.device = cloud_device
177
184
  self._command_futures = {}
178
185
  self._commands: MammotionCommand = MammotionCommand(
179
- cloud_device.deviceName,
186
+ cloud_device.device_name,
180
187
  int(mqtt.cloud_client.mammotion_http.response.data.userInformation.userAccount),
181
188
  )
182
189
  self.currentID = ""
@@ -187,15 +194,10 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
187
194
  self._mqtt.on_ready_event.add_subscribers(self.on_ready)
188
195
  self._mqtt.on_disconnected_event.add_subscribers(self.on_disconnect)
189
196
  self._mqtt.on_connected_event.add_subscribers(self.on_connect)
190
- self._state_manager.cloud_gethash_ack_callback = self.datahash_response
191
- self._state_manager.cloud_get_commondata_ack_callback = self.commdata_response
192
- self._state_manager.cloud_get_plan_callback = self.plan_callback
193
197
  self.set_queue_callback(self.queue_command)
194
198
 
195
- if self._mqtt.is_ready:
196
- self.run_periodic_sync_task()
197
-
198
199
  def __del__(self) -> None:
200
+ """Cleanup subscriptions."""
199
201
  self._mqtt.on_ready_event.remove_subscribers(self.on_ready)
200
202
  self._mqtt.on_disconnected_event.remove_subscribers(self.on_disconnect)
201
203
  self._mqtt.on_connected_event.remove_subscribers(self.on_connect)
@@ -203,14 +205,6 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
203
205
  self._mqtt.mqtt_properties_event.remove_subscribers(self._parse_message_properties_for_device)
204
206
  self._mqtt.mqtt_status_event.remove_subscribers(self._parse_message_status_for_device)
205
207
  self._mqtt.mqtt_device_event.remove_subscribers(self._parse_device_event_for_device)
206
- self._state_manager.cloud_gethash_ack_callback = None
207
- self._state_manager.cloud_get_commondata_ack_callback = None
208
- self._state_manager.cloud_get_plan_callback = None
209
- if self._ble_sync_task:
210
- self._ble_sync_task.cancel()
211
-
212
- def __del__(self) -> None:
213
- """Cleanup."""
214
208
  self._state_manager.cloud_queue_command_callback.remove_subscribers(self.queue_command)
215
209
 
216
210
  @property
@@ -234,26 +228,18 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
234
228
  _LOGGER.debug("Device is offline")
235
229
 
236
230
  async def on_disconnect(self) -> None:
237
- if self._ble_sync_task:
238
- self._ble_sync_task.cancel()
239
231
  self._mqtt.disconnect()
240
232
 
241
233
  async def on_connect(self) -> None:
242
- await self._ble_sync()
243
- if self._ble_sync_task is None or self._ble_sync_task.cancelled():
244
- await self.run_periodic_sync_task()
234
+ """On connect callback"""
245
235
 
246
236
  async def stop(self) -> None:
247
237
  """Stop all tasks and disconnect."""
248
- if self._ble_sync_task:
249
- self._ble_sync_task.cancel()
250
238
  # self._mqtt._mqtt_client.unsubscribe()
251
239
  self.stopped = True
252
240
 
253
241
  async def start(self) -> None:
254
- await self._ble_sync()
255
- if self._ble_sync_task is None or self._ble_sync_task.cancelled():
256
- await self.run_periodic_sync_task()
242
+ """Start the device connection."""
257
243
  self.stopped = False
258
244
  if not self.mqtt.is_connected():
259
245
  loop = asyncio.get_running_loop()
@@ -262,35 +248,7 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
262
248
  # self.mqtt._mqtt_client.thing_on_thing_enable(None)
263
249
 
264
250
  async def _ble_sync(self) -> None:
265
- if (
266
- self.stopped
267
- or self.mqtt.is_connected is False
268
- or self.state_manager.get_device().report_data.dev.sys_status in NO_REQUEST_MODES
269
- ):
270
- return
271
-
272
- command_bytes = self._commands.send_todev_ble_sync(3)
273
- try:
274
- await self._mqtt.send_command(self.iot_id, command_bytes)
275
- except (CheckSessionException, SetupException, DeviceOfflineException):
276
- if self._ble_sync_task:
277
- self._ble_sync_task.cancel()
278
-
279
- async def run_periodic_sync_task(self) -> None:
280
- """Send ble sync to robot."""
281
- try:
282
- if not self._mqtt._operation_lock.locked() or not self.stopped:
283
- await self._ble_sync()
284
- finally:
285
- if not self.stopped:
286
- self.schedule_ble_sync()
287
-
288
- def schedule_ble_sync(self) -> None:
289
- """Periodically sync to keep connection alive."""
290
- if self._mqtt is not None and self._mqtt.is_connected:
291
- self._ble_sync_task = self.loop.call_later(
292
- 160, lambda: asyncio.ensure_future(self.run_periodic_sync_task())
293
- )
251
+ pass
294
252
 
295
253
  async def queue_command(self, key: str, **kwargs: Any) -> None:
296
254
  # Create a future to hold the result
@@ -301,7 +259,8 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
301
259
  await self._mqtt.command_queue.put((self.iot_id, key, command_bytes, future))
302
260
  # Wait for the future to be resolved
303
261
  try:
304
- return await future
262
+ await future
263
+ return
305
264
  except asyncio.CancelledError:
306
265
  """Try again once."""
307
266
  future = asyncio.Future()
@@ -329,18 +288,18 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
329
288
  return None
330
289
 
331
290
  async def _parse_message_properties_for_device(self, event: ThingPropertiesMessage) -> None:
332
- if event.params.iotId != self.iot_id:
291
+ if event.params.iot_id != self.iot_id:
333
292
  return
334
293
  await self.state_manager.properties(event)
335
294
 
336
295
  async def _parse_message_status_for_device(self, status: ThingStatusMessage) -> None:
337
- if status.params.iotId != self.iot_id:
296
+ if status.params.iot_id != self.iot_id:
338
297
  return
339
298
  await self.state_manager.status(status)
340
299
 
341
300
  async def _parse_device_event_for_device(self, status: ThingStatusMessage) -> None:
342
301
  """Process device event if it matches the device's IoT ID."""
343
- if status.params.iotId != self.iot_id:
302
+ if status.params.iot_id != self.iot_id:
344
303
  return
345
304
  await self.state_manager.device_event(status)
346
305
 
@@ -359,7 +318,7 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
359
318
  """
360
319
  params = event.params
361
320
  new_msg = LubaMsg()
362
- if event.params.iotId != self.iot_id:
321
+ if event.params.iot_id != self.iot_id:
363
322
  return
364
323
  binary_data = base64.b64decode(params.value.content)
365
324
  try:
@@ -370,9 +329,9 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
370
329
 
371
330
  if (
372
331
  self._commands.get_device_product_key() == ""
373
- and self._commands.get_device_name() == event.params.deviceName
332
+ and self._commands.get_device_name() == event.params.device_name
374
333
  ):
375
- self._commands.set_device_product_key(event.params.productKey)
334
+ self._commands.set_device_product_key(event.params.product_key)
376
335
 
377
336
  res = betterproto2.which_one_of(new_msg, "LubaSubMsg")
378
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]