pymammotion 0.5.21__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.
- pymammotion/__init__.py +3 -3
- pymammotion/aliyun/client.py +5 -2
- pymammotion/aliyun/cloud_gateway.py +137 -20
- pymammotion/aliyun/model/dev_by_account_response.py +169 -21
- pymammotion/const.py +3 -0
- pymammotion/data/model/device.py +1 -0
- pymammotion/data/model/device_config.py +1 -1
- pymammotion/data/model/device_info.py +4 -0
- pymammotion/data/model/enums.py +5 -3
- pymammotion/data/model/generate_route_information.py +2 -2
- pymammotion/data/model/hash_list.py +113 -33
- pymammotion/data/model/mowing_modes.py +8 -0
- pymammotion/data/model/region_data.py +4 -4
- pymammotion/data/{state_manager.py → mower_state_manager.py} +50 -13
- pymammotion/data/mqtt/event.py +47 -22
- pymammotion/data/mqtt/mammotion_properties.py +257 -0
- pymammotion/data/mqtt/properties.py +32 -29
- pymammotion/data/mqtt/status.py +17 -16
- pymammotion/homeassistant/__init__.py +3 -0
- pymammotion/homeassistant/mower_api.py +446 -0
- pymammotion/homeassistant/rtk_api.py +54 -0
- pymammotion/http/http.py +433 -18
- pymammotion/http/model/http.py +82 -2
- pymammotion/http/model/response_factory.py +10 -4
- pymammotion/mammotion/commands/mammotion_command.py +20 -0
- pymammotion/mammotion/commands/messages/driver.py +25 -0
- pymammotion/mammotion/commands/messages/navigation.py +10 -6
- pymammotion/mammotion/commands/messages/system.py +0 -14
- pymammotion/mammotion/devices/__init__.py +27 -3
- pymammotion/mammotion/devices/base.py +22 -146
- pymammotion/mammotion/devices/mammotion.py +364 -205
- pymammotion/mammotion/devices/mammotion_bluetooth.py +11 -8
- pymammotion/mammotion/devices/mammotion_cloud.py +49 -85
- 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 +121 -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/mammotion_mqtt.py +174 -192
- pymammotion/mqtt/mqtt_models.py +66 -0
- pymammotion/proto/__init__.py +2 -2
- pymammotion/proto/mctrl_nav.proto +2 -2
- pymammotion/proto/mctrl_nav_pb2.py +1 -1
- pymammotion/proto/mctrl_nav_pb2.pyi +4 -4
- pymammotion/proto/mctrl_sys.proto +1 -1
- pymammotion/utility/datatype_converter.py +13 -12
- pymammotion/utility/device_type.py +88 -3
- pymammotion/utility/mur_mur_hash.py +132 -87
- {pymammotion-0.5.21.dist-info → pymammotion-0.5.45.dist-info}/METADATA +25 -30
- {pymammotion-0.5.21.dist-info → pymammotion-0.5.45.dist-info}/RECORD +64 -50
- {pymammotion-0.5.21.dist-info → pymammotion-0.5.45.dist-info}/WHEEL +1 -1
- pymammotion/http/_init_.py +0 -0
- {pymammotion-0.5.21.dist-info → pymammotion-0.5.45.dist-info/licenses}/LICENSE +0 -0
|
@@ -17,10 +17,10 @@ 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.
|
|
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
|
-
from pymammotion.proto import LubaMsg
|
|
23
|
+
from pymammotion.proto import LubaMsg
|
|
24
24
|
|
|
25
25
|
DBUS_ERROR_BACKOFF_TIME = 0.25
|
|
26
26
|
|
|
@@ -72,10 +72,16 @@ class MammotionBaseBLEDevice(MammotionBaseDevice):
|
|
|
72
72
|
"""Base class for Mammotion BLE devices."""
|
|
73
73
|
|
|
74
74
|
def __init__(
|
|
75
|
-
self,
|
|
75
|
+
self,
|
|
76
|
+
state_manager: MowerStateManager,
|
|
77
|
+
cloud_device: Device,
|
|
78
|
+
device: BLEDevice,
|
|
79
|
+
interface: int = 0,
|
|
80
|
+
**kwargs: Any,
|
|
76
81
|
) -> None:
|
|
77
82
|
"""Initialize MammotionBaseBLEDevice."""
|
|
78
83
|
super().__init__(state_manager, cloud_device)
|
|
84
|
+
self.command_sent_time = 0
|
|
79
85
|
self._disconnect_strategy = True
|
|
80
86
|
self._ble_sync_task = None
|
|
81
87
|
self._prev_notification = None
|
|
@@ -94,9 +100,6 @@ class MammotionBaseBLEDevice(MammotionBaseDevice):
|
|
|
94
100
|
self._key: str | None = None
|
|
95
101
|
self._cloud_device = cloud_device
|
|
96
102
|
self.set_queue_callback(self.queue_command)
|
|
97
|
-
self._state_manager.ble_gethash_ack_callback = self.datahash_response
|
|
98
|
-
self._state_manager.ble_get_commondata_ack_callback = self.commdata_response
|
|
99
|
-
self._state_manager.ble_get_plan_callback = self.plan_callback
|
|
100
103
|
loop = asyncio.get_event_loop()
|
|
101
104
|
loop.create_task(self.process_queue())
|
|
102
105
|
|
|
@@ -358,8 +361,8 @@ class MammotionBaseBLEDevice(MammotionBaseDevice):
|
|
|
358
361
|
new_msg = LubaMsg().parse(data)
|
|
359
362
|
res = betterproto2.which_one_of(new_msg, "LubaSubMsg")
|
|
360
363
|
if res[0] == "net":
|
|
361
|
-
if new_msg.net.todev_ble_sync != 0 or
|
|
362
|
-
if
|
|
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() == "":
|
|
363
366
|
self._commands.set_device_product_key(new_msg.net.toapp_wifi_iot_status.productkey)
|
|
364
367
|
|
|
365
368
|
await self._state_manager.notification(new_msg)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
from asyncio import InvalidStateError
|
|
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
|
|
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.
|
|
18
|
-
from pymammotion.data.mqtt.
|
|
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
|
-
from pymammotion.proto import LubaMsg
|
|
25
|
-
from pymammotion.utility.constant.device_constant import NO_REQUEST_MODES
|
|
24
|
+
from pymammotion.proto import LubaMsg
|
|
26
25
|
|
|
27
26
|
_LOGGER = logging.getLogger(__name__)
|
|
28
27
|
|
|
@@ -30,9 +29,10 @@ _LOGGER = logging.getLogger(__name__)
|
|
|
30
29
|
class MammotionCloud:
|
|
31
30
|
"""Per account MQTT cloud."""
|
|
32
31
|
|
|
33
|
-
def __init__(self, mqtt_client: MammotionMQTT, cloud_client: CloudIOTGateway) -> None:
|
|
32
|
+
def __init__(self, mqtt_client: AliyunMQTT | MammotionMQTT, cloud_client: CloudIOTGateway) -> None:
|
|
34
33
|
"""Initialize MammotionCloud."""
|
|
35
34
|
self.cloud_client = cloud_client
|
|
35
|
+
self.command_sent_time = 0
|
|
36
36
|
self.loop = asyncio.get_event_loop()
|
|
37
37
|
self.is_ready = False
|
|
38
38
|
self.command_queue = asyncio.Queue()
|
|
@@ -69,7 +69,7 @@ class MammotionCloud:
|
|
|
69
69
|
self._mqtt_client.connect_async()
|
|
70
70
|
|
|
71
71
|
async def send_command(self, iot_id: str, command: bytes) -> None:
|
|
72
|
-
await self._mqtt_client.
|
|
72
|
+
await self._mqtt_client.send_cloud_command(iot_id, command)
|
|
73
73
|
|
|
74
74
|
async def on_connected(self) -> None:
|
|
75
75
|
"""Callback for when MQTT connects."""
|
|
@@ -104,19 +104,17 @@ class MammotionCloud:
|
|
|
104
104
|
assert self._mqtt_client is not None
|
|
105
105
|
self._key = key
|
|
106
106
|
_LOGGER.debug("Sending command: %s", key)
|
|
107
|
-
await self._mqtt_client.get_cloud_client().send_cloud_command(iot_id, command)
|
|
108
107
|
self.command_sent_time = time.time()
|
|
108
|
+
await self._mqtt_client.send_cloud_command(iot_id, command)
|
|
109
109
|
|
|
110
|
-
async def _on_mqtt_message(self, topic: str, payload:
|
|
110
|
+
async def _on_mqtt_message(self, topic: str, payload: bytes, iot_id: str) -> None:
|
|
111
111
|
"""Handle incoming MQTT messages."""
|
|
112
|
-
_LOGGER.debug("MQTT message received on topic %s: %s, iot_id: %s", topic, payload, iot_id)
|
|
112
|
+
# _LOGGER.debug("MQTT message received on topic %s: %s, iot_id: %s", topic, payload, iot_id)
|
|
113
|
+
json_str = payload.decode("utf-8")
|
|
114
|
+
dict_payload = json.loads(json_str)
|
|
115
|
+
await self._parse_mqtt_response(topic, dict_payload, iot_id)
|
|
113
116
|
|
|
114
|
-
|
|
115
|
-
payload = json.loads(json_str)
|
|
116
|
-
|
|
117
|
-
await self._parse_mqtt_response(topic, payload)
|
|
118
|
-
|
|
119
|
-
async def _parse_mqtt_response(self, topic: str, payload: dict) -> None:
|
|
117
|
+
async def _parse_mqtt_response(self, topic: str, payload: dict, iot_id: str) -> None:
|
|
120
118
|
"""Parse and handle MQTT responses based on the topic.
|
|
121
119
|
|
|
122
120
|
This function processes different types of MQTT messages received from various
|
|
@@ -151,31 +149,41 @@ class MammotionCloud:
|
|
|
151
149
|
property_event = ThingPropertiesMessage.from_dict(payload)
|
|
152
150
|
await self.mqtt_properties_event.data_event(property_event)
|
|
153
151
|
|
|
152
|
+
if topic.endswith("/thing/event/device_protobuf_msg_event/post"):
|
|
153
|
+
_LOGGER.debug("Mammotion Thing event received")
|
|
154
|
+
mammotion_event = MammotionEventMessage.from_dict(payload)
|
|
155
|
+
mammotion_event.params.iot_id = iot_id
|
|
156
|
+
await self.mqtt_message_event.data_event(mammotion_event)
|
|
157
|
+
elif topic.endswith("/thing/event/property/post"):
|
|
158
|
+
_LOGGER.debug("Mammotion Property event received")
|
|
159
|
+
mammotion_property_event = MammotionPropertiesMessage.from_dict(payload)
|
|
160
|
+
mammotion_property_event.params.iot_id = iot_id
|
|
161
|
+
await self.mqtt_properties_event.data_event(mammotion_property_event)
|
|
162
|
+
|
|
154
163
|
def _disconnect(self) -> None:
|
|
155
164
|
"""Disconnect the MQTT client."""
|
|
156
165
|
self._mqtt_client.disconnect()
|
|
157
166
|
|
|
158
167
|
@property
|
|
159
|
-
def waiting_queue(self):
|
|
168
|
+
def waiting_queue(self) -> deque:
|
|
160
169
|
return self._waiting_queue
|
|
161
170
|
|
|
162
171
|
|
|
163
172
|
class MammotionBaseCloudDevice(MammotionBaseDevice):
|
|
164
173
|
"""Base class for Mammotion Cloud devices."""
|
|
165
174
|
|
|
166
|
-
def __init__(self, mqtt: MammotionCloud, cloud_device: Device, state_manager:
|
|
175
|
+
def __init__(self, mqtt: MammotionCloud, cloud_device: Device, state_manager: MowerStateManager) -> None:
|
|
167
176
|
"""Initialize MammotionBaseCloudDevice."""
|
|
168
177
|
super().__init__(state_manager, cloud_device)
|
|
169
|
-
self._ble_sync_task: TimerHandle | None = None
|
|
170
178
|
self.stopped = False
|
|
171
179
|
self.on_ready_callback: Callable[[], Awaitable[None]] | None = None
|
|
172
180
|
self.loop = asyncio.get_event_loop()
|
|
173
181
|
self._mqtt = mqtt
|
|
174
|
-
self.iot_id = cloud_device.
|
|
182
|
+
self.iot_id = cloud_device.iot_id
|
|
175
183
|
self.device = cloud_device
|
|
176
184
|
self._command_futures = {}
|
|
177
185
|
self._commands: MammotionCommand = MammotionCommand(
|
|
178
|
-
cloud_device.
|
|
186
|
+
cloud_device.device_name,
|
|
179
187
|
int(mqtt.cloud_client.mammotion_http.response.data.userInformation.userAccount),
|
|
180
188
|
)
|
|
181
189
|
self.currentID = ""
|
|
@@ -186,15 +194,10 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
|
|
|
186
194
|
self._mqtt.on_ready_event.add_subscribers(self.on_ready)
|
|
187
195
|
self._mqtt.on_disconnected_event.add_subscribers(self.on_disconnect)
|
|
188
196
|
self._mqtt.on_connected_event.add_subscribers(self.on_connect)
|
|
189
|
-
self._state_manager.cloud_gethash_ack_callback = self.datahash_response
|
|
190
|
-
self._state_manager.cloud_get_commondata_ack_callback = self.commdata_response
|
|
191
|
-
self._state_manager.cloud_get_plan_callback = self.plan_callback
|
|
192
197
|
self.set_queue_callback(self.queue_command)
|
|
193
198
|
|
|
194
|
-
if self._mqtt.is_ready:
|
|
195
|
-
self.run_periodic_sync_task()
|
|
196
|
-
|
|
197
199
|
def __del__(self) -> None:
|
|
200
|
+
"""Cleanup subscriptions."""
|
|
198
201
|
self._mqtt.on_ready_event.remove_subscribers(self.on_ready)
|
|
199
202
|
self._mqtt.on_disconnected_event.remove_subscribers(self.on_disconnect)
|
|
200
203
|
self._mqtt.on_connected_event.remove_subscribers(self.on_connect)
|
|
@@ -202,16 +205,12 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
|
|
|
202
205
|
self._mqtt.mqtt_properties_event.remove_subscribers(self._parse_message_properties_for_device)
|
|
203
206
|
self._mqtt.mqtt_status_event.remove_subscribers(self._parse_message_status_for_device)
|
|
204
207
|
self._mqtt.mqtt_device_event.remove_subscribers(self._parse_device_event_for_device)
|
|
205
|
-
self._state_manager.cloud_gethash_ack_callback = None
|
|
206
|
-
self._state_manager.cloud_get_commondata_ack_callback = None
|
|
207
|
-
self._state_manager.cloud_get_plan_callback = None
|
|
208
|
-
if self._ble_sync_task:
|
|
209
|
-
self._ble_sync_task.cancel()
|
|
210
|
-
|
|
211
|
-
def __del__(self) -> None:
|
|
212
|
-
"""Cleanup."""
|
|
213
208
|
self._state_manager.cloud_queue_command_callback.remove_subscribers(self.queue_command)
|
|
214
209
|
|
|
210
|
+
@property
|
|
211
|
+
def command_sent_time(self) -> float:
|
|
212
|
+
return self._mqtt.command_sent_time
|
|
213
|
+
|
|
215
214
|
def set_notification_callback(self, func: Callable[[tuple[str, Any | None]], Awaitable[None]]) -> None:
|
|
216
215
|
self._state_manager.cloud_on_notification_callback.add_subscribers(func)
|
|
217
216
|
|
|
@@ -229,26 +228,18 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
|
|
|
229
228
|
_LOGGER.debug("Device is offline")
|
|
230
229
|
|
|
231
230
|
async def on_disconnect(self) -> None:
|
|
232
|
-
if self._ble_sync_task:
|
|
233
|
-
self._ble_sync_task.cancel()
|
|
234
231
|
self._mqtt.disconnect()
|
|
235
232
|
|
|
236
233
|
async def on_connect(self) -> None:
|
|
237
|
-
|
|
238
|
-
if self._ble_sync_task is None or self._ble_sync_task.cancelled():
|
|
239
|
-
await self.run_periodic_sync_task()
|
|
234
|
+
"""On connect callback"""
|
|
240
235
|
|
|
241
236
|
async def stop(self) -> None:
|
|
242
237
|
"""Stop all tasks and disconnect."""
|
|
243
|
-
if self._ble_sync_task:
|
|
244
|
-
self._ble_sync_task.cancel()
|
|
245
238
|
# self._mqtt._mqtt_client.unsubscribe()
|
|
246
239
|
self.stopped = True
|
|
247
240
|
|
|
248
241
|
async def start(self) -> None:
|
|
249
|
-
|
|
250
|
-
if self._ble_sync_task is None or self._ble_sync_task.cancelled():
|
|
251
|
-
await self.run_periodic_sync_task()
|
|
242
|
+
"""Start the device connection."""
|
|
252
243
|
self.stopped = False
|
|
253
244
|
if not self.mqtt.is_connected():
|
|
254
245
|
loop = asyncio.get_running_loop()
|
|
@@ -257,35 +248,7 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
|
|
|
257
248
|
# self.mqtt._mqtt_client.thing_on_thing_enable(None)
|
|
258
249
|
|
|
259
250
|
async def _ble_sync(self) -> None:
|
|
260
|
-
|
|
261
|
-
self.stopped
|
|
262
|
-
or self.mqtt.is_connected is False
|
|
263
|
-
or self.state_manager.get_device().report_data.dev.sys_status in NO_REQUEST_MODES
|
|
264
|
-
):
|
|
265
|
-
return
|
|
266
|
-
|
|
267
|
-
command_bytes = self._commands.send_todev_ble_sync(3)
|
|
268
|
-
try:
|
|
269
|
-
await self._mqtt.send_command(self.iot_id, command_bytes)
|
|
270
|
-
except (CheckSessionException, SetupException, DeviceOfflineException):
|
|
271
|
-
if self._ble_sync_task:
|
|
272
|
-
self._ble_sync_task.cancel()
|
|
273
|
-
|
|
274
|
-
async def run_periodic_sync_task(self) -> None:
|
|
275
|
-
"""Send ble sync to robot."""
|
|
276
|
-
try:
|
|
277
|
-
if not self._mqtt._operation_lock.locked() or not self.stopped:
|
|
278
|
-
await self._ble_sync()
|
|
279
|
-
finally:
|
|
280
|
-
if not self.stopped:
|
|
281
|
-
self.schedule_ble_sync()
|
|
282
|
-
|
|
283
|
-
def schedule_ble_sync(self) -> None:
|
|
284
|
-
"""Periodically sync to keep connection alive."""
|
|
285
|
-
if self._mqtt is not None and self._mqtt.is_connected:
|
|
286
|
-
self._ble_sync_task = self.loop.call_later(
|
|
287
|
-
160, lambda: asyncio.ensure_future(self.run_periodic_sync_task())
|
|
288
|
-
)
|
|
251
|
+
pass
|
|
289
252
|
|
|
290
253
|
async def queue_command(self, key: str, **kwargs: Any) -> None:
|
|
291
254
|
# Create a future to hold the result
|
|
@@ -296,7 +259,8 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
|
|
|
296
259
|
await self._mqtt.command_queue.put((self.iot_id, key, command_bytes, future))
|
|
297
260
|
# Wait for the future to be resolved
|
|
298
261
|
try:
|
|
299
|
-
|
|
262
|
+
await future
|
|
263
|
+
return
|
|
300
264
|
except asyncio.CancelledError:
|
|
301
265
|
"""Try again once."""
|
|
302
266
|
future = asyncio.Future()
|
|
@@ -324,18 +288,18 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
|
|
|
324
288
|
return None
|
|
325
289
|
|
|
326
290
|
async def _parse_message_properties_for_device(self, event: ThingPropertiesMessage) -> None:
|
|
327
|
-
if event.params.
|
|
291
|
+
if event.params.iot_id != self.iot_id:
|
|
328
292
|
return
|
|
329
293
|
await self.state_manager.properties(event)
|
|
330
294
|
|
|
331
295
|
async def _parse_message_status_for_device(self, status: ThingStatusMessage) -> None:
|
|
332
|
-
if status.params.
|
|
296
|
+
if status.params.iot_id != self.iot_id:
|
|
333
297
|
return
|
|
334
298
|
await self.state_manager.status(status)
|
|
335
299
|
|
|
336
300
|
async def _parse_device_event_for_device(self, status: ThingStatusMessage) -> None:
|
|
337
301
|
"""Process device event if it matches the device's IoT ID."""
|
|
338
|
-
if status.params.
|
|
302
|
+
if status.params.iot_id != self.iot_id:
|
|
339
303
|
return
|
|
340
304
|
await self.state_manager.device_event(status)
|
|
341
305
|
|
|
@@ -354,7 +318,7 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
|
|
|
354
318
|
"""
|
|
355
319
|
params = event.params
|
|
356
320
|
new_msg = LubaMsg()
|
|
357
|
-
if event.params.
|
|
321
|
+
if event.params.iot_id != self.iot_id:
|
|
358
322
|
return
|
|
359
323
|
binary_data = base64.b64decode(params.value.content)
|
|
360
324
|
try:
|
|
@@ -365,13 +329,13 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
|
|
|
365
329
|
|
|
366
330
|
if (
|
|
367
331
|
self._commands.get_device_product_key() == ""
|
|
368
|
-
and self._commands.get_device_name() == event.params.
|
|
332
|
+
and self._commands.get_device_name() == event.params.device_name
|
|
369
333
|
):
|
|
370
|
-
self._commands.set_device_product_key(event.params.
|
|
334
|
+
self._commands.set_device_product_key(event.params.product_key)
|
|
371
335
|
|
|
372
336
|
res = betterproto2.which_one_of(new_msg, "LubaSubMsg")
|
|
373
337
|
if res[0] == "net":
|
|
374
|
-
if new_msg.net.todev_ble_sync != 0 or
|
|
338
|
+
if new_msg.net.todev_ble_sync != 0 or new_msg.net.toapp_wifi_iot_status is not None:
|
|
375
339
|
return
|
|
376
340
|
|
|
377
341
|
await self._state_manager.notification(new_msg)
|
|
@@ -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]
|