pymammotion 0.4.0a2__py3-none-any.whl → 0.5.51__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 (133) hide show
  1. pymammotion/__init__.py +5 -4
  2. pymammotion/aliyun/client.py +235 -0
  3. pymammotion/aliyun/cloud_gateway.py +312 -64
  4. pymammotion/aliyun/model/aep_response.py +1 -2
  5. pymammotion/aliyun/model/dev_by_account_response.py +170 -23
  6. pymammotion/aliyun/model/login_by_oauth_response.py +2 -3
  7. pymammotion/aliyun/model/regions_response.py +3 -3
  8. pymammotion/aliyun/model/session_by_authcode_response.py +2 -2
  9. pymammotion/aliyun/model/thing_response.py +12 -0
  10. pymammotion/aliyun/regions.py +62 -0
  11. pymammotion/aliyun/tea/core.py +297 -0
  12. pymammotion/bluetooth/ble.py +7 -9
  13. pymammotion/bluetooth/ble_message.py +10 -14
  14. pymammotion/const.py +3 -0
  15. pymammotion/data/model/__init__.py +1 -2
  16. pymammotion/data/model/device.py +95 -27
  17. pymammotion/data/model/device_config.py +4 -4
  18. pymammotion/data/model/device_info.py +35 -0
  19. pymammotion/data/model/device_limits.py +10 -10
  20. pymammotion/data/model/enums.py +12 -2
  21. pymammotion/data/model/errors.py +12 -0
  22. pymammotion/data/model/events.py +14 -0
  23. pymammotion/data/model/generate_geojson.py +521 -0
  24. pymammotion/data/model/generate_route_information.py +2 -2
  25. pymammotion/data/model/hash_list.py +370 -57
  26. pymammotion/data/model/location.py +4 -4
  27. pymammotion/data/model/mowing_modes.py +17 -1
  28. pymammotion/data/model/raw_data.py +2 -10
  29. pymammotion/data/model/region_data.py +10 -11
  30. pymammotion/data/model/report_info.py +31 -5
  31. pymammotion/data/model/work.py +27 -0
  32. pymammotion/data/mower_state_manager.py +316 -0
  33. pymammotion/data/mqtt/event.py +73 -28
  34. pymammotion/data/mqtt/mammotion_properties.py +257 -0
  35. pymammotion/data/mqtt/properties.py +93 -78
  36. pymammotion/data/mqtt/status.py +18 -17
  37. pymammotion/event/event.py +27 -6
  38. pymammotion/homeassistant/__init__.py +3 -0
  39. pymammotion/homeassistant/mower_api.py +484 -0
  40. pymammotion/homeassistant/rtk_api.py +54 -0
  41. pymammotion/http/encryption.py +5 -6
  42. pymammotion/http/http.py +574 -28
  43. pymammotion/http/model/__init__.py +0 -0
  44. pymammotion/{aliyun/model/stream_subscription_response.py → http/model/camera_stream.py} +14 -2
  45. pymammotion/http/model/http.py +129 -4
  46. pymammotion/http/model/response_factory.py +61 -0
  47. pymammotion/http/model/rtk.py +16 -0
  48. pymammotion/mammotion/commands/abstract_message.py +7 -5
  49. pymammotion/mammotion/commands/mammotion_command.py +30 -1
  50. pymammotion/mammotion/commands/messages/basestation.py +43 -0
  51. pymammotion/mammotion/commands/messages/driver.py +61 -29
  52. pymammotion/mammotion/commands/messages/media.py +68 -15
  53. pymammotion/mammotion/commands/messages/navigation.py +61 -25
  54. pymammotion/mammotion/commands/messages/network.py +17 -23
  55. pymammotion/mammotion/commands/messages/ota.py +18 -18
  56. pymammotion/mammotion/commands/messages/system.py +32 -49
  57. pymammotion/mammotion/commands/messages/video.py +15 -16
  58. pymammotion/mammotion/devices/__init__.py +27 -3
  59. pymammotion/mammotion/devices/base.py +40 -131
  60. pymammotion/mammotion/devices/mammotion.py +436 -201
  61. pymammotion/mammotion/devices/mammotion_bluetooth.py +57 -47
  62. pymammotion/mammotion/devices/mammotion_cloud.py +134 -105
  63. pymammotion/mammotion/devices/mammotion_mower_ble.py +49 -0
  64. pymammotion/mammotion/devices/mammotion_mower_cloud.py +39 -0
  65. pymammotion/mammotion/devices/managers/managers.py +81 -0
  66. pymammotion/mammotion/devices/mower_device.py +124 -0
  67. pymammotion/mammotion/devices/mower_manager.py +107 -0
  68. pymammotion/mammotion/devices/rtk_ble.py +89 -0
  69. pymammotion/mammotion/devices/rtk_cloud.py +113 -0
  70. pymammotion/mammotion/devices/rtk_device.py +50 -0
  71. pymammotion/mammotion/devices/rtk_manager.py +122 -0
  72. pymammotion/mqtt/__init__.py +2 -1
  73. pymammotion/mqtt/aliyun_mqtt.py +232 -0
  74. pymammotion/mqtt/linkkit/__init__.py +5 -0
  75. pymammotion/mqtt/linkkit/h2client.py +585 -0
  76. pymammotion/mqtt/linkkit/linkkit.py +3023 -0
  77. pymammotion/mqtt/mammotion_mqtt.py +176 -169
  78. pymammotion/mqtt/mqtt_models.py +66 -0
  79. pymammotion/proto/__init__.py +4839 -4
  80. pymammotion/proto/basestation.proto +8 -0
  81. pymammotion/proto/basestation_pb2.py +11 -9
  82. pymammotion/proto/basestation_pb2.pyi +16 -2
  83. pymammotion/proto/dev_net.proto +79 -55
  84. pymammotion/proto/dev_net_pb2.py +60 -56
  85. pymammotion/proto/dev_net_pb2.pyi +49 -6
  86. pymammotion/proto/luba_msg.proto +2 -1
  87. pymammotion/proto/luba_msg_pb2.py +6 -6
  88. pymammotion/proto/luba_msg_pb2.pyi +1 -0
  89. pymammotion/proto/luba_mul.proto +62 -1
  90. pymammotion/proto/luba_mul_pb2.py +38 -22
  91. pymammotion/proto/luba_mul_pb2.pyi +94 -7
  92. pymammotion/proto/mctrl_driver.proto +44 -4
  93. pymammotion/proto/mctrl_driver_pb2.py +26 -14
  94. pymammotion/proto/mctrl_driver_pb2.pyi +66 -11
  95. pymammotion/proto/mctrl_nav.proto +93 -52
  96. pymammotion/proto/mctrl_nav_pb2.py +75 -67
  97. pymammotion/proto/mctrl_nav_pb2.pyi +142 -56
  98. pymammotion/proto/mctrl_ota.proto +40 -2
  99. pymammotion/proto/mctrl_ota_pb2.py +23 -13
  100. pymammotion/proto/mctrl_ota_pb2.pyi +67 -4
  101. pymammotion/proto/mctrl_pept.proto +8 -3
  102. pymammotion/proto/mctrl_pept_pb2.py +8 -6
  103. pymammotion/proto/mctrl_pept_pb2.pyi +14 -6
  104. pymammotion/proto/mctrl_sys.proto +325 -86
  105. pymammotion/proto/mctrl_sys_pb2.py +162 -98
  106. pymammotion/proto/mctrl_sys_pb2.pyi +451 -25
  107. pymammotion/proto/message_pool.py +3 -0
  108. pymammotion/proto/py.typed +0 -0
  109. pymammotion/utility/constant/device_constant.py +29 -5
  110. pymammotion/utility/datatype_converter.py +13 -12
  111. pymammotion/utility/device_config.py +522 -130
  112. pymammotion/utility/device_type.py +218 -21
  113. pymammotion/utility/map.py +238 -51
  114. pymammotion/utility/mur_mur_hash.py +159 -0
  115. {pymammotion-0.4.0a2.dist-info → pymammotion-0.5.51.dist-info}/METADATA +26 -31
  116. pymammotion-0.5.51.dist-info/RECORD +152 -0
  117. {pymammotion-0.4.0a2.dist-info → pymammotion-0.5.51.dist-info}/WHEEL +1 -1
  118. pymammotion/aliyun/cloud_service.py +0 -65
  119. pymammotion/data/model/plan.py +0 -58
  120. pymammotion/data/state_manager.py +0 -129
  121. pymammotion/proto/basestation.py +0 -59
  122. pymammotion/proto/common.py +0 -12
  123. pymammotion/proto/dev_net.py +0 -381
  124. pymammotion/proto/luba_msg.py +0 -81
  125. pymammotion/proto/luba_mul.py +0 -76
  126. pymammotion/proto/mctrl_driver.py +0 -100
  127. pymammotion/proto/mctrl_nav.py +0 -664
  128. pymammotion/proto/mctrl_ota.py +0 -48
  129. pymammotion/proto/mctrl_pept.py +0 -41
  130. pymammotion/proto/mctrl_sys.py +0 -574
  131. pymammotion-0.4.0a2.dist-info/RECORD +0 -131
  132. /pymammotion/http/{_init_.py → __init__.py} +0 -0
  133. {pymammotion-0.4.0a2.dist-info → pymammotion-0.5.51.dist-info/licenses}/LICENSE +0 -0
@@ -1,9 +1,11 @@
1
1
  import asyncio
2
+ from collections.abc import Awaitable, Callable
2
3
  import logging
3
- from typing import Any, cast
4
+ import time
5
+ from typing import Any
4
6
  from uuid import UUID
5
7
 
6
- import betterproto
8
+ import betterproto2
7
9
  from bleak import BleakGATTCharacteristic, BleakGATTServiceCollection, BLEDevice
8
10
  from bleak.exc import BleakDBusError
9
11
  from bleak_retry_connector import (
@@ -13,12 +15,12 @@ from bleak_retry_connector import (
13
15
  establish_connection,
14
16
  )
15
17
 
18
+ from pymammotion.aliyun.model.dev_by_account_response import Device
16
19
  from pymammotion.bluetooth import BleMessage
17
- from pymammotion.data.state_manager import StateManager
20
+ from pymammotion.data.mower_state_manager import MowerStateManager
18
21
  from pymammotion.mammotion.commands.mammotion_command import MammotionCommand
19
22
  from pymammotion.mammotion.devices.base import MammotionBaseDevice
20
- from pymammotion.proto import has_field
21
- from pymammotion.proto.luba_msg import LubaMsg
23
+ from pymammotion.proto import LubaMsg
22
24
 
23
25
  DBUS_ERROR_BACKOFF_TIME = 0.25
24
26
 
@@ -69,9 +71,17 @@ async def _handle_retry(fut: asyncio.Future[None], func, command: bytes) -> None
69
71
  class MammotionBaseBLEDevice(MammotionBaseDevice):
70
72
  """Base class for Mammotion BLE devices."""
71
73
 
72
- def __init__(self, state_manager: StateManager, device: BLEDevice, interface: int = 0, **kwargs: Any) -> None:
74
+ def __init__(
75
+ self,
76
+ state_manager: MowerStateManager,
77
+ cloud_device: Device,
78
+ device: BLEDevice,
79
+ interface: int = 0,
80
+ **kwargs: Any,
81
+ ) -> None:
73
82
  """Initialize MammotionBaseBLEDevice."""
74
- super().__init__(state_manager)
83
+ super().__init__(state_manager, cloud_device)
84
+ self.command_sent_time = 0
75
85
  self._disconnect_strategy = True
76
86
  self._ble_sync_task = None
77
87
  self._prev_notification = None
@@ -82,23 +92,38 @@ class MammotionBaseBLEDevice(MammotionBaseDevice):
82
92
  self._write_char: BleakGATTCharacteristic | int | str | UUID = 0
83
93
  self._disconnect_timer: asyncio.TimerHandle | None = None
84
94
  self._message: BleMessage | None = None
85
- self._commands: MammotionCommand = MammotionCommand(device.name)
95
+ self._commands: MammotionCommand = MammotionCommand(device.name, 1)
86
96
  self.command_queue = asyncio.Queue()
87
97
  self._expected_disconnect = False
88
98
  self._connect_lock = asyncio.Lock()
89
99
  self._operation_lock = asyncio.Lock()
90
100
  self._key: str | None = None
101
+ self._cloud_device = cloud_device
91
102
  self.set_queue_callback(self.queue_command)
92
103
  loop = asyncio.get_event_loop()
93
104
  loop.create_task(self.process_queue())
94
105
 
106
+ def __del__(self) -> None:
107
+ """Cleanup."""
108
+ if self._disconnect_timer:
109
+ self._disconnect_timer.cancel()
110
+ if self._ble_sync_task:
111
+ self._ble_sync_task.cancel()
112
+
113
+ self._state_manager.ble_queue_command_callback.remove_subscribers(self.queue_command)
114
+
115
+ def set_notification_callback(self, func: Callable[[tuple[str, Any | None]], Awaitable[None]]) -> None:
116
+ self._state_manager.ble_on_notification_callback.add_subscribers(func)
117
+
118
+ def set_queue_callback(self, func: Callable[[str, dict[str, Any]], Awaitable[None]]) -> None:
119
+ self._state_manager.ble_queue_command_callback.add_subscribers(func)
120
+
95
121
  def update_device(self, device: BLEDevice) -> None:
96
122
  """Update the BLE device."""
97
123
  self.ble_device = device
98
124
 
99
125
  async def _ble_sync(self) -> None:
100
126
  if self._client is not None and self._client.is_connected:
101
- _LOGGER.debug("BLE SYNC")
102
127
  command_bytes = self._commands.send_todev_ble_sync(2)
103
128
  await self._message.post_custom_data_bytes(command_bytes)
104
129
 
@@ -120,10 +145,10 @@ class MammotionBaseBLEDevice(MammotionBaseDevice):
120
145
  """Stop all tasks and disconnect."""
121
146
  if self._ble_sync_task:
122
147
  self._ble_sync_task.cancel()
123
- if self._client is not None:
148
+ if self._client is not None and self._client.is_connected:
124
149
  await self._client.disconnect()
125
150
 
126
- async def queue_command(self, key: str, **kwargs: Any) -> bytes | None:
151
+ async def queue_command(self, key: str, **kwargs: Any) -> None:
127
152
  # Create a future to hold the result
128
153
  _LOGGER.debug("Queueing command: %s", key)
129
154
  future = asyncio.Future()
@@ -131,7 +156,7 @@ class MammotionBaseBLEDevice(MammotionBaseDevice):
131
156
  command_bytes = getattr(self._commands, key)(**kwargs)
132
157
  await self.command_queue.put((key, command_bytes, future))
133
158
  # Wait for the future to be resolved
134
- return await future
159
+ await future
135
160
  # return await self._send_command_with_args(key, **kwargs)
136
161
 
137
162
  async def process_queue(self) -> None:
@@ -220,10 +245,12 @@ class MammotionBaseBLEDevice(MammotionBaseDevice):
220
245
  @property
221
246
  def rssi(self) -> int:
222
247
  """Return RSSI of device."""
223
- try:
224
- return cast(self.mower.sys.toapp_report_data.connect.ble_rssi, int)
225
- finally:
226
- return 0
248
+ return self.mower.report_data.connect.ble_rssi
249
+
250
+ @property
251
+ def client(self) -> BleakClientWithServiceCache | None:
252
+ """Return client."""
253
+ return self._client
227
254
 
228
255
  async def _ensure_connected(self) -> None:
229
256
  """Ensure connection to device is established."""
@@ -289,7 +316,7 @@ class MammotionBaseBLEDevice(MammotionBaseDevice):
289
316
  await self._ble_sync()
290
317
  self.schedule_ble_sync()
291
318
 
292
- async def _send_command_locked(self, key: str, command: bytes) -> bytes:
319
+ async def _send_command_locked(self, key: str, command: bytes) -> None:
293
320
  """Send command to device and read response."""
294
321
  await self._ensure_connected()
295
322
  try:
@@ -314,65 +341,48 @@ class MammotionBaseBLEDevice(MammotionBaseDevice):
314
341
 
315
342
  async def _notification_handler(self, _sender: BleakGATTCharacteristic, data: bytearray) -> None:
316
343
  """Handle notification responses."""
344
+
317
345
  if self._message is None:
318
346
  return
319
347
  result = self._message.parseNotification(data)
320
348
  if result == 0:
321
349
  data = await self._message.parseBlufiNotifyData(True)
350
+ self._message.clear_notification()
322
351
  try:
323
352
  self._update_raw_data(data)
324
353
  except (KeyError, ValueError, IndexError, UnicodeDecodeError):
325
354
  _LOGGER.exception("Error parsing message %s", data)
326
355
  data = b""
327
- finally:
328
- self._message.clearNotification()
329
356
 
330
357
  _LOGGER.debug("%s: Received notification: %s", self.name, data)
331
358
  else:
332
359
  return
360
+
333
361
  new_msg = LubaMsg().parse(data)
334
- if betterproto.serialized_on_wire(new_msg.net):
335
- if new_msg.net.todev_ble_sync != 0 or has_field(new_msg.net.toapp_wifi_iot_status):
336
- if has_field(new_msg.net.toapp_wifi_iot_status) and self._commands.get_device_product_key() == "":
362
+ res = betterproto2.which_one_of(new_msg, "LubaSubMsg")
363
+ if res[0] == "net":
364
+ if new_msg.net.todev_ble_sync != 0 or new_msg.net.toapp_wifi_iot_status is not None:
365
+ if new_msg.net.toapp_wifi_iot_status is not None and self._commands.get_device_product_key() == "":
337
366
  self._commands.set_device_product_key(new_msg.net.toapp_wifi_iot_status.productkey)
338
367
 
339
- return
368
+ await self._state_manager.notification(new_msg)
340
369
 
341
- # may or may not be correct, some work could be done here to correctly match responses
342
- if self._notify_future and not self._notify_future.done():
343
- self._notify_future.set_result(data)
370
+ if self._execute_timed_disconnect is None:
371
+ await self._execute_forced_disconnect()
344
372
 
345
373
  self._reset_disconnect_timer()
346
- await self._state_manager.notification(new_msg)
347
374
 
348
375
  async def _start_notify(self) -> None:
349
376
  """Start notification."""
350
377
  _LOGGER.debug("%s: Subscribe to notifications; RSSI: %s", self.name, self.rssi)
351
378
  await self._client.start_notify(self._read_char, self._notification_handler)
352
379
 
353
- async def _execute_command_locked(self, key: str, command: bytes) -> bytes:
380
+ async def _execute_command_locked(self, key: str, command: bytes) -> None:
354
381
  """Execute command and read response."""
355
382
  assert self._client is not None
356
- self._notify_future = self.loop.create_future()
357
- self._key = key
358
383
  _LOGGER.debug("%s: Sending command: %s", self.name, key)
359
384
  await self._message.post_custom_data_bytes(command)
360
-
361
- timeout = 2
362
- timeout_handle = self.loop.call_at(self.loop.time() + timeout, _handle_timeout, self._notify_future)
363
- timeout_expired = False
364
- try:
365
- notify_msg = await self._notify_future
366
- except asyncio.TimeoutError:
367
- timeout_expired = True
368
- notify_msg = b""
369
- finally:
370
- if not timeout_expired:
371
- timeout_handle.cancel()
372
- self._notify_future = None
373
-
374
- _LOGGER.debug("%s: Notification received: %s", self.name, notify_msg.hex())
375
- return notify_msg
385
+ self.command_sent_time = time.time()
376
386
 
377
387
  def get_address(self) -> str:
378
388
  """Return address of device."""
@@ -483,5 +493,5 @@ class MammotionBaseBLEDevice(MammotionBaseDevice):
483
493
  _LOGGER.debug("%s: Disconnect completed successfully", self.name)
484
494
  self._client = None
485
495
 
486
- def set_disconnect_strategy(self, disconnect: bool) -> None:
496
+ def set_disconnect_strategy(self, *, disconnect: bool) -> None:
487
497
  self._disconnect_strategy = disconnect
@@ -1,25 +1,27 @@
1
1
  import asyncio
2
+ from asyncio import InvalidStateError
2
3
  import base64
4
+ from collections import deque
5
+ from collections.abc import Awaitable, Callable
3
6
  import json
4
7
  import logging
5
- from asyncio import TimerHandle
6
- from collections import deque
7
- from typing import Any, Awaitable, Callable, Optional, cast
8
+ import time
9
+ from typing import Any
8
10
 
9
- import betterproto
11
+ import betterproto2
12
+ from Tea.exceptions import UnretryableException
10
13
 
11
- from pymammotion import CloudIOTGateway, MammotionMQTT
14
+ from pymammotion import AliyunMQTT, CloudIOTGateway, MammotionMQTT
12
15
  from pymammotion.aliyun.cloud_gateway import DeviceOfflineException
13
16
  from pymammotion.aliyun.model.dev_by_account_response import Device
14
- from pymammotion.data.mqtt.event import ThingEventMessage
15
- from pymammotion.data.mqtt.properties import ThingPropertiesMessage
16
- from pymammotion.data.state_manager import StateManager
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
17
21
  from pymammotion.event.event import DataEvent
18
22
  from pymammotion.mammotion.commands.mammotion_command import MammotionCommand
19
23
  from pymammotion.mammotion.devices.base import MammotionBaseDevice
20
- from pymammotion.mqtt.mammotion_future import MammotionFuture
21
- from pymammotion.proto import has_field
22
- from pymammotion.proto.luba_msg import LubaMsg
24
+ from pymammotion.proto import LubaMsg
23
25
 
24
26
  _LOGGER = logging.getLogger(__name__)
25
27
 
@@ -27,14 +29,18 @@ _LOGGER = logging.getLogger(__name__)
27
29
  class MammotionCloud:
28
30
  """Per account MQTT cloud."""
29
31
 
30
- def __init__(self, mqtt_client: MammotionMQTT, cloud_client: CloudIOTGateway) -> None:
32
+ def __init__(self, mqtt_client: AliyunMQTT | MammotionMQTT, cloud_client: CloudIOTGateway) -> None:
33
+ """Initialize MammotionCloud."""
31
34
  self.cloud_client = cloud_client
35
+ self.command_sent_time = 0
32
36
  self.loop = asyncio.get_event_loop()
33
37
  self.is_ready = False
34
38
  self.command_queue = asyncio.Queue()
35
39
  self._waiting_queue = deque()
36
40
  self.mqtt_message_event = DataEvent()
37
41
  self.mqtt_properties_event = DataEvent()
42
+ self.mqtt_status_event = DataEvent()
43
+ self.mqtt_device_event = DataEvent()
38
44
  self.on_ready_event = DataEvent()
39
45
  self.on_disconnected_event = DataEvent()
40
46
  self.on_connected_event = DataEvent()
@@ -45,10 +51,8 @@ class MammotionCloud:
45
51
  self._mqtt_client.on_message = self._on_mqtt_message
46
52
  self._mqtt_client.on_ready = self.on_ready
47
53
 
48
- # temporary for testing only
49
- # self._start_sync_task = self.loop.call_later(30, lambda: asyncio.ensure_future(self.start_sync(0)))
50
-
51
54
  async def on_ready(self) -> None:
55
+ """Starts processing the queue and emits the ready event."""
52
56
  loop = asyncio.get_event_loop()
53
57
  loop.create_task(self.process_queue())
54
58
  await self.on_ready_event.data_event(None)
@@ -57,13 +61,15 @@ class MammotionCloud:
57
61
  return self._mqtt_client.is_connected
58
62
 
59
63
  def disconnect(self) -> None:
60
- self._mqtt_client.disconnect()
64
+ """Disconnect the MQTT client."""
65
+ if self.is_connected:
66
+ self._mqtt_client.disconnect()
61
67
 
62
68
  def connect_async(self) -> None:
63
69
  self._mqtt_client.connect_async()
64
70
 
65
- def send_command(self, iot_id: str, command: bytes) -> None:
66
- self._mqtt_client.get_cloud_client().send_cloud_command(iot_id, command)
71
+ async def send_command(self, iot_id: str, command: bytes) -> None:
72
+ await self._mqtt_client.send_cloud_command(iot_id, command)
67
73
 
68
74
  async def on_connected(self) -> None:
69
75
  """Callback for when MQTT connects."""
@@ -84,42 +90,42 @@ class MammotionCloud:
84
90
  future.set_result(result)
85
91
  except Exception as ex:
86
92
  # Set the exception on the future if something goes wrong
87
- future.set_exception(ex)
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")
88
98
  finally:
89
99
  # Mark the task as done
90
100
  self.command_queue.task_done()
91
101
 
92
- async def _execute_command_locked(self, iot_id: str, key: str, command: bytes) -> bytes:
102
+ async def _execute_command_locked(self, iot_id: str, key: str, command: bytes) -> None:
93
103
  """Execute command and read response."""
94
104
  assert self._mqtt_client is not None
95
105
  self._key = key
96
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)
97
109
 
98
- await self.loop.run_in_executor(None, self._mqtt_client.get_cloud_client().send_cloud_command, iot_id, command)
99
- future = MammotionFuture(iot_id)
100
- self._waiting_queue.append(future)
101
- timeout = 5
102
- try:
103
- notify_msg = await future.async_get(timeout)
104
- except asyncio.TimeoutError:
105
- _LOGGER.debug("command_locked TimeoutError")
106
- notify_msg = b""
107
-
108
- _LOGGER.debug("%s: Message received", iot_id)
109
-
110
- return notify_msg
111
-
112
- 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:
113
111
  """Handle incoming MQTT messages."""
114
- _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)
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.
115
119
 
116
- json_str = json.dumps(payload)
117
- payload = json.loads(json_str)
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.
118
123
 
119
- await self._handle_mqtt_message(topic, payload)
124
+ Args:
125
+ topic (str): The MQTT topic from which the message was received.
126
+ payload (dict): The payload data of the MQTT message.
120
127
 
121
- async def _parse_mqtt_response(self, topic: str, payload: dict) -> None:
122
- """Parse the MQTT response."""
128
+ """
123
129
  if topic.endswith("/app/down/thing/events"):
124
130
  _LOGGER.debug("Thing event received")
125
131
  event = ThingEventMessage.from_dicts(payload)
@@ -131,56 +137,85 @@ class MammotionCloud:
131
137
  _LOGGER.debug("Protobuf event")
132
138
  # Call the callbacks for each cloudDevice
133
139
  await self.mqtt_message_event.data_event(event)
140
+ if event.method == "thing.events":
141
+ await self.mqtt_device_event.data_event(event)
134
142
  if event.method == "thing.properties":
135
143
  await self.mqtt_properties_event.data_event(event)
136
144
  _LOGGER.debug(event)
137
-
138
- async def _handle_mqtt_message(self, topic: str, payload: dict) -> None:
139
- """Async handler for incoming MQTT messages."""
140
- await self._parse_mqtt_response(topic=topic, payload=payload)
145
+ elif topic.endswith("/app/down/thing/status"):
146
+ status = ThingStatusMessage.from_dict(payload)
147
+ await self.mqtt_status_event.data_event(status)
148
+ elif topic.endswith("app/down/thing/properties"):
149
+ property_event = ThingPropertiesMessage.from_dict(payload)
150
+ await self.mqtt_properties_event.data_event(property_event)
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)
141
162
 
142
163
  def _disconnect(self) -> None:
143
164
  """Disconnect the MQTT client."""
144
165
  self._mqtt_client.disconnect()
145
166
 
146
167
  @property
147
- def waiting_queue(self):
168
+ def waiting_queue(self) -> deque:
148
169
  return self._waiting_queue
149
170
 
150
171
 
151
172
  class MammotionBaseCloudDevice(MammotionBaseDevice):
152
173
  """Base class for Mammotion Cloud devices."""
153
174
 
154
- 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:
155
176
  """Initialize MammotionBaseCloudDevice."""
156
177
  super().__init__(state_manager, cloud_device)
157
- self._ble_sync_task: TimerHandle | None = None
158
178
  self.stopped = False
159
- self.on_ready_callback: Optional[Callable[[], Awaitable[None]]] = None
179
+ self.on_ready_callback: Callable[[], Awaitable[None]] | None = None
160
180
  self.loop = asyncio.get_event_loop()
161
181
  self._mqtt = mqtt
162
- self.iot_id = cloud_device.iotId
182
+ self.iot_id = cloud_device.iot_id
163
183
  self.device = cloud_device
164
184
  self._command_futures = {}
165
- self._commands: MammotionCommand = MammotionCommand(cloud_device.deviceName)
185
+ self._commands: MammotionCommand = MammotionCommand(
186
+ cloud_device.device_name,
187
+ int(mqtt.cloud_client.mammotion_http.response.data.userInformation.userAccount),
188
+ )
166
189
  self.currentID = ""
167
190
  self._mqtt.mqtt_message_event.add_subscribers(self._parse_message_for_device)
168
191
  self._mqtt.mqtt_properties_event.add_subscribers(self._parse_message_properties_for_device)
192
+ self._mqtt.mqtt_status_event.add_subscribers(self._parse_message_status_for_device)
193
+ self._mqtt.mqtt_device_event.add_subscribers(self._parse_device_event_for_device)
169
194
  self._mqtt.on_ready_event.add_subscribers(self.on_ready)
170
195
  self._mqtt.on_disconnected_event.add_subscribers(self.on_disconnect)
171
196
  self._mqtt.on_connected_event.add_subscribers(self.on_connect)
172
197
  self.set_queue_callback(self.queue_command)
173
198
 
174
- if self._mqtt.is_ready:
175
- self.run_periodic_sync_task()
176
-
177
199
  def __del__(self) -> None:
200
+ """Cleanup subscriptions."""
178
201
  self._mqtt.on_ready_event.remove_subscribers(self.on_ready)
179
202
  self._mqtt.on_disconnected_event.remove_subscribers(self.on_disconnect)
180
203
  self._mqtt.on_connected_event.remove_subscribers(self.on_connect)
181
204
  self._mqtt.mqtt_message_event.remove_subscribers(self._parse_message_for_device)
182
- if self._ble_sync_task:
183
- self._ble_sync_task.cancel()
205
+ self._mqtt.mqtt_properties_event.remove_subscribers(self._parse_message_properties_for_device)
206
+ self._mqtt.mqtt_status_event.remove_subscribers(self._parse_message_status_for_device)
207
+ self._mqtt.mqtt_device_event.remove_subscribers(self._parse_device_event_for_device)
208
+ self._state_manager.cloud_queue_command_callback.remove_subscribers(self.queue_command)
209
+
210
+ @property
211
+ def command_sent_time(self) -> float:
212
+ return self._mqtt.command_sent_time
213
+
214
+ def set_notification_callback(self, func: Callable[[tuple[str, Any | None]], Awaitable[None]]) -> None:
215
+ self._state_manager.cloud_on_notification_callback.add_subscribers(func)
216
+
217
+ def set_queue_callback(self, func: Callable[[str, dict[str, Any]], Awaitable[None]]) -> None:
218
+ self._state_manager.cloud_queue_command_callback.add_subscribers(func)
184
219
 
185
220
  async def on_ready(self) -> None:
186
221
  """Callback for when MQTT is subscribed to events."""
@@ -189,55 +224,33 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
189
224
  try:
190
225
  if self.on_ready_callback:
191
226
  await self.on_ready_callback()
192
- except DeviceOfflineException:
227
+ except (DeviceOfflineException, UnretryableException):
193
228
  _LOGGER.debug("Device is offline")
194
229
 
195
230
  async def on_disconnect(self) -> None:
196
- if self._ble_sync_task:
197
- self._ble_sync_task.cancel()
198
231
  self._mqtt.disconnect()
199
232
 
200
233
  async def on_connect(self) -> None:
201
- await self._ble_sync()
202
- if self._ble_sync_task is None or self._ble_sync_task.cancelled():
203
- await self.run_periodic_sync_task()
234
+ """On connect callback"""
204
235
 
205
236
  async def stop(self) -> None:
206
237
  """Stop all tasks and disconnect."""
207
- if self._ble_sync_task:
208
- self._ble_sync_task.cancel()
238
+ # self._mqtt._mqtt_client.unsubscribe()
209
239
  self.stopped = True
210
240
 
211
241
  async def start(self) -> None:
212
- await self._ble_sync()
213
- if self._ble_sync_task is None or self._ble_sync_task.cancelled():
214
- await self.run_periodic_sync_task()
242
+ """Start the device connection."""
215
243
  self.stopped = False
216
244
  if not self.mqtt.is_connected():
217
- self.mqtt.connect_async()
245
+ loop = asyncio.get_running_loop()
246
+ await loop.run_in_executor(None, self.mqtt.connect_async)
247
+ # else:
248
+ # self.mqtt._mqtt_client.thing_on_thing_enable(None)
218
249
 
219
250
  async def _ble_sync(self) -> None:
220
- command_bytes = self._commands.send_todev_ble_sync(3)
221
- loop = asyncio.get_running_loop()
222
- await loop.run_in_executor(None, self._mqtt.send_command, self.iot_id, command_bytes)
251
+ pass
223
252
 
224
- async def run_periodic_sync_task(self) -> None:
225
- """Send ble sync to robot."""
226
- try:
227
- if not self._mqtt._operation_lock.locked() or not self.stopped:
228
- await self._ble_sync()
229
- finally:
230
- if not self.stopped:
231
- self.schedule_ble_sync()
232
-
233
- def schedule_ble_sync(self) -> None:
234
- """Periodically sync to keep connection alive."""
235
- if self._mqtt is not None and self._mqtt.is_connected:
236
- self._ble_sync_task = self.loop.call_later(
237
- 160, lambda: asyncio.ensure_future(self.run_periodic_sync_task())
238
- )
239
-
240
- async def queue_command(self, key: str, **kwargs: Any) -> bytes:
253
+ async def queue_command(self, key: str, **kwargs: Any) -> None:
241
254
  # Create a future to hold the result
242
255
  _LOGGER.debug("Queueing command: %s", key)
243
256
  future = asyncio.Future()
@@ -246,7 +259,8 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
246
259
  await self._mqtt.command_queue.put((self.iot_id, key, command_bytes, future))
247
260
  # Wait for the future to be resolved
248
261
  try:
249
- return await future
262
+ await future
263
+ return
250
264
  except asyncio.CancelledError:
251
265
  """Try again once."""
252
266
  future = asyncio.Future()
@@ -274,15 +288,37 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
274
288
  return None
275
289
 
276
290
  async def _parse_message_properties_for_device(self, event: ThingPropertiesMessage) -> None:
277
- if event.params.iotId != self.iot_id:
291
+ if event.params.iot_id != self.iot_id:
292
+ return
293
+ await self.state_manager.properties(event)
294
+
295
+ async def _parse_message_status_for_device(self, status: ThingStatusMessage) -> None:
296
+ if status.params.iot_id != self.iot_id:
278
297
  return
279
- self.state_manager.properties(event)
298
+ await self.state_manager.status(status)
299
+
300
+ async def _parse_device_event_for_device(self, status: ThingStatusMessage) -> None:
301
+ """Process device event if it matches the device's IoT ID."""
302
+ if status.params.iot_id != self.iot_id:
303
+ return
304
+ await self.state_manager.device_event(status)
280
305
 
281
306
  async def _parse_message_for_device(self, event: ThingEventMessage) -> None:
282
- _LOGGER.debug("_parse_message_for_device")
307
+ """Parses a message received from a device and updates internal state.
308
+
309
+ This function processes an incoming `ThingEventMessage`, checks if the message
310
+ is intended for this device, decodes the binary data, and updates raw data. It
311
+ then attempts to parse the binary data into a `LubaMsg`. If parsing fails, it
312
+ logs the exception. The function also handles setting the device product key if
313
+ not already set and processes specific sub-messages based on their types.
314
+
315
+ Args:
316
+ event (ThingEventMessage): The event message received from the device.
317
+
318
+ """
283
319
  params = event.params
284
320
  new_msg = LubaMsg()
285
- if event.params.iotId != self.iot_id:
321
+ if event.params.iot_id != self.iot_id:
286
322
  return
287
323
  binary_data = base64.b64decode(params.value.content)
288
324
  try:
@@ -293,22 +329,15 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
293
329
 
294
330
  if (
295
331
  self._commands.get_device_product_key() == ""
296
- and self._commands.get_device_name() == event.params.deviceName
332
+ and self._commands.get_device_name() == event.params.device_name
297
333
  ):
298
- self._commands.set_device_product_key(event.params.productKey)
334
+ self._commands.set_device_product_key(event.params.product_key)
299
335
 
300
- if betterproto.serialized_on_wire(new_msg.net):
301
- if new_msg.net.todev_ble_sync != 0 or has_field(new_msg.net.toapp_wifi_iot_status):
336
+ res = betterproto2.which_one_of(new_msg, "LubaSubMsg")
337
+ if res[0] == "net":
338
+ if new_msg.net.todev_ble_sync != 0 or new_msg.net.toapp_wifi_iot_status is not None:
302
339
  return
303
340
 
304
- if len(self._mqtt.waiting_queue) > 0:
305
- fut: MammotionFuture = self.dequeue_by_iot_id(self._mqtt.waiting_queue, self.iot_id)
306
- if fut is None:
307
- return
308
- while fut.fut.cancelled() and len(self._mqtt.waiting_queue) > 0:
309
- fut = self.dequeue_by_iot_id(self._mqtt.waiting_queue, self.iot_id)
310
- if not fut.fut.cancelled():
311
- fut.resolve(cast(bytes, binary_data))
312
341
  await self._state_manager.notification(new_msg)
313
342
 
314
343
  @property
@@ -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__()