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.
- pymammotion/__init__.py +5 -4
- pymammotion/aliyun/client.py +235 -0
- pymammotion/aliyun/cloud_gateway.py +312 -64
- pymammotion/aliyun/model/aep_response.py +1 -2
- pymammotion/aliyun/model/dev_by_account_response.py +170 -23
- pymammotion/aliyun/model/login_by_oauth_response.py +2 -3
- pymammotion/aliyun/model/regions_response.py +3 -3
- pymammotion/aliyun/model/session_by_authcode_response.py +2 -2
- pymammotion/aliyun/model/thing_response.py +12 -0
- pymammotion/aliyun/regions.py +62 -0
- pymammotion/aliyun/tea/core.py +297 -0
- pymammotion/bluetooth/ble.py +7 -9
- pymammotion/bluetooth/ble_message.py +10 -14
- pymammotion/const.py +3 -0
- pymammotion/data/model/__init__.py +1 -2
- pymammotion/data/model/device.py +95 -27
- pymammotion/data/model/device_config.py +4 -4
- pymammotion/data/model/device_info.py +35 -0
- pymammotion/data/model/device_limits.py +10 -10
- pymammotion/data/model/enums.py +12 -2
- pymammotion/data/model/errors.py +12 -0
- pymammotion/data/model/events.py +14 -0
- pymammotion/data/model/generate_geojson.py +521 -0
- pymammotion/data/model/generate_route_information.py +2 -2
- pymammotion/data/model/hash_list.py +370 -57
- pymammotion/data/model/location.py +4 -4
- pymammotion/data/model/mowing_modes.py +17 -1
- pymammotion/data/model/raw_data.py +2 -10
- pymammotion/data/model/region_data.py +10 -11
- pymammotion/data/model/report_info.py +31 -5
- pymammotion/data/model/work.py +27 -0
- pymammotion/data/mower_state_manager.py +316 -0
- pymammotion/data/mqtt/event.py +73 -28
- pymammotion/data/mqtt/mammotion_properties.py +257 -0
- pymammotion/data/mqtt/properties.py +93 -78
- pymammotion/data/mqtt/status.py +18 -17
- pymammotion/event/event.py +27 -6
- pymammotion/homeassistant/__init__.py +3 -0
- pymammotion/homeassistant/mower_api.py +484 -0
- pymammotion/homeassistant/rtk_api.py +54 -0
- pymammotion/http/encryption.py +5 -6
- pymammotion/http/http.py +574 -28
- pymammotion/http/model/__init__.py +0 -0
- pymammotion/{aliyun/model/stream_subscription_response.py → http/model/camera_stream.py} +14 -2
- pymammotion/http/model/http.py +129 -4
- pymammotion/http/model/response_factory.py +61 -0
- pymammotion/http/model/rtk.py +16 -0
- pymammotion/mammotion/commands/abstract_message.py +7 -5
- pymammotion/mammotion/commands/mammotion_command.py +30 -1
- pymammotion/mammotion/commands/messages/basestation.py +43 -0
- pymammotion/mammotion/commands/messages/driver.py +61 -29
- pymammotion/mammotion/commands/messages/media.py +68 -15
- pymammotion/mammotion/commands/messages/navigation.py +61 -25
- pymammotion/mammotion/commands/messages/network.py +17 -23
- pymammotion/mammotion/commands/messages/ota.py +18 -18
- pymammotion/mammotion/commands/messages/system.py +32 -49
- pymammotion/mammotion/commands/messages/video.py +15 -16
- pymammotion/mammotion/devices/__init__.py +27 -3
- pymammotion/mammotion/devices/base.py +40 -131
- pymammotion/mammotion/devices/mammotion.py +436 -201
- pymammotion/mammotion/devices/mammotion_bluetooth.py +57 -47
- pymammotion/mammotion/devices/mammotion_cloud.py +134 -105
- pymammotion/mammotion/devices/mammotion_mower_ble.py +49 -0
- pymammotion/mammotion/devices/mammotion_mower_cloud.py +39 -0
- pymammotion/mammotion/devices/managers/managers.py +81 -0
- pymammotion/mammotion/devices/mower_device.py +124 -0
- pymammotion/mammotion/devices/mower_manager.py +107 -0
- pymammotion/mammotion/devices/rtk_ble.py +89 -0
- pymammotion/mammotion/devices/rtk_cloud.py +113 -0
- pymammotion/mammotion/devices/rtk_device.py +50 -0
- pymammotion/mammotion/devices/rtk_manager.py +122 -0
- pymammotion/mqtt/__init__.py +2 -1
- pymammotion/mqtt/aliyun_mqtt.py +232 -0
- pymammotion/mqtt/linkkit/__init__.py +5 -0
- pymammotion/mqtt/linkkit/h2client.py +585 -0
- pymammotion/mqtt/linkkit/linkkit.py +3023 -0
- pymammotion/mqtt/mammotion_mqtt.py +176 -169
- pymammotion/mqtt/mqtt_models.py +66 -0
- pymammotion/proto/__init__.py +4839 -4
- pymammotion/proto/basestation.proto +8 -0
- pymammotion/proto/basestation_pb2.py +11 -9
- pymammotion/proto/basestation_pb2.pyi +16 -2
- pymammotion/proto/dev_net.proto +79 -55
- pymammotion/proto/dev_net_pb2.py +60 -56
- pymammotion/proto/dev_net_pb2.pyi +49 -6
- pymammotion/proto/luba_msg.proto +2 -1
- pymammotion/proto/luba_msg_pb2.py +6 -6
- pymammotion/proto/luba_msg_pb2.pyi +1 -0
- pymammotion/proto/luba_mul.proto +62 -1
- pymammotion/proto/luba_mul_pb2.py +38 -22
- pymammotion/proto/luba_mul_pb2.pyi +94 -7
- pymammotion/proto/mctrl_driver.proto +44 -4
- pymammotion/proto/mctrl_driver_pb2.py +26 -14
- pymammotion/proto/mctrl_driver_pb2.pyi +66 -11
- pymammotion/proto/mctrl_nav.proto +93 -52
- pymammotion/proto/mctrl_nav_pb2.py +75 -67
- pymammotion/proto/mctrl_nav_pb2.pyi +142 -56
- pymammotion/proto/mctrl_ota.proto +40 -2
- pymammotion/proto/mctrl_ota_pb2.py +23 -13
- pymammotion/proto/mctrl_ota_pb2.pyi +67 -4
- pymammotion/proto/mctrl_pept.proto +8 -3
- pymammotion/proto/mctrl_pept_pb2.py +8 -6
- pymammotion/proto/mctrl_pept_pb2.pyi +14 -6
- pymammotion/proto/mctrl_sys.proto +325 -86
- pymammotion/proto/mctrl_sys_pb2.py +162 -98
- pymammotion/proto/mctrl_sys_pb2.pyi +451 -25
- pymammotion/proto/message_pool.py +3 -0
- pymammotion/proto/py.typed +0 -0
- pymammotion/utility/constant/device_constant.py +29 -5
- pymammotion/utility/datatype_converter.py +13 -12
- pymammotion/utility/device_config.py +522 -130
- pymammotion/utility/device_type.py +218 -21
- pymammotion/utility/map.py +238 -51
- pymammotion/utility/mur_mur_hash.py +159 -0
- {pymammotion-0.4.0a2.dist-info → pymammotion-0.5.51.dist-info}/METADATA +26 -31
- pymammotion-0.5.51.dist-info/RECORD +152 -0
- {pymammotion-0.4.0a2.dist-info → pymammotion-0.5.51.dist-info}/WHEEL +1 -1
- pymammotion/aliyun/cloud_service.py +0 -65
- pymammotion/data/model/plan.py +0 -58
- pymammotion/data/state_manager.py +0 -129
- pymammotion/proto/basestation.py +0 -59
- pymammotion/proto/common.py +0 -12
- pymammotion/proto/dev_net.py +0 -381
- pymammotion/proto/luba_msg.py +0 -81
- pymammotion/proto/luba_mul.py +0 -76
- pymammotion/proto/mctrl_driver.py +0 -100
- pymammotion/proto/mctrl_nav.py +0 -664
- pymammotion/proto/mctrl_ota.py +0 -48
- pymammotion/proto/mctrl_pept.py +0 -41
- pymammotion/proto/mctrl_sys.py +0 -574
- pymammotion-0.4.0a2.dist-info/RECORD +0 -131
- /pymammotion/http/{_init_.py → __init__.py} +0 -0
- {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
|
-
|
|
4
|
+
import time
|
|
5
|
+
from typing import Any
|
|
4
6
|
from uuid import UUID
|
|
5
7
|
|
|
6
|
-
import
|
|
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.
|
|
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
|
|
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__(
|
|
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) ->
|
|
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
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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) ->
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
368
|
+
await self._state_manager.notification(new_msg)
|
|
340
369
|
|
|
341
|
-
|
|
342
|
-
|
|
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) ->
|
|
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
|
-
|
|
6
|
-
from
|
|
7
|
-
from typing import Any, Awaitable, Callable, Optional, cast
|
|
8
|
+
import time
|
|
9
|
+
from typing import Any
|
|
8
10
|
|
|
9
|
-
import
|
|
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.
|
|
15
|
-
from pymammotion.data.mqtt.
|
|
16
|
-
from pymammotion.data.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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) ->
|
|
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
|
-
|
|
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
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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:
|
|
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:
|
|
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.
|
|
182
|
+
self.iot_id = cloud_device.iot_id
|
|
163
183
|
self.device = cloud_device
|
|
164
184
|
self._command_futures = {}
|
|
165
|
-
self._commands: MammotionCommand = MammotionCommand(
|
|
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
|
-
|
|
183
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
332
|
+
and self._commands.get_device_name() == event.params.device_name
|
|
297
333
|
):
|
|
298
|
-
self._commands.set_device_product_key(event.params.
|
|
334
|
+
self._commands.set_device_product_key(event.params.product_key)
|
|
299
335
|
|
|
300
|
-
|
|
301
|
-
|
|
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__()
|