pymammotion 0.5.32__py3-none-any.whl → 0.5.45__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pymammotion might be problematic. Click here for more details.
- pymammotion/__init__.py +3 -3
- pymammotion/aliyun/cloud_gateway.py +114 -18
- 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/enums.py +5 -3
- pymammotion/data/model/generate_route_information.py +2 -2
- pymammotion/data/model/hash_list.py +113 -33
- pymammotion/data/model/region_data.py +4 -4
- pymammotion/data/{state_manager.py → mower_state_manager.py} +17 -7
- 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 +392 -16
- pymammotion/http/model/http.py +82 -2
- pymammotion/http/model/response_factory.py +10 -4
- pymammotion/mammotion/commands/mammotion_command.py +6 -0
- pymammotion/mammotion/commands/messages/navigation.py +10 -6
- pymammotion/mammotion/devices/__init__.py +27 -3
- pymammotion/mammotion/devices/base.py +16 -139
- pymammotion/mammotion/devices/mammotion.py +361 -203
- pymammotion/mammotion/devices/mammotion_bluetooth.py +7 -5
- pymammotion/mammotion/devices/mammotion_cloud.py +42 -83
- 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 +1 -1
- pymammotion/proto/mctrl_nav.proto +1 -1
- 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.32.dist-info → pymammotion-0.5.45.dist-info}/METADATA +25 -31
- {pymammotion-0.5.32.dist-info → pymammotion-0.5.45.dist-info}/RECORD +59 -45
- {pymammotion-0.5.32.dist-info → pymammotion-0.5.45.dist-info}/WHEEL +1 -1
- pymammotion/http/_init_.py +0 -0
- {pymammotion-0.5.32.dist-info → pymammotion-0.5.45.dist-info/licenses}/LICENSE +0 -0
|
@@ -17,7 +17,7 @@ from bleak_retry_connector import (
|
|
|
17
17
|
|
|
18
18
|
from pymammotion.aliyun.model.dev_by_account_response import Device
|
|
19
19
|
from pymammotion.bluetooth import BleMessage
|
|
20
|
-
from pymammotion.data.
|
|
20
|
+
from pymammotion.data.mower_state_manager import MowerStateManager
|
|
21
21
|
from pymammotion.mammotion.commands.mammotion_command import MammotionCommand
|
|
22
22
|
from pymammotion.mammotion.devices.base import MammotionBaseDevice
|
|
23
23
|
from pymammotion.proto import LubaMsg
|
|
@@ -72,7 +72,12 @@ class MammotionBaseBLEDevice(MammotionBaseDevice):
|
|
|
72
72
|
"""Base class for Mammotion BLE devices."""
|
|
73
73
|
|
|
74
74
|
def __init__(
|
|
75
|
-
self,
|
|
75
|
+
self,
|
|
76
|
+
state_manager: MowerStateManager,
|
|
77
|
+
cloud_device: Device,
|
|
78
|
+
device: BLEDevice,
|
|
79
|
+
interface: int = 0,
|
|
80
|
+
**kwargs: Any,
|
|
76
81
|
) -> None:
|
|
77
82
|
"""Initialize MammotionBaseBLEDevice."""
|
|
78
83
|
super().__init__(state_manager, cloud_device)
|
|
@@ -95,9 +100,6 @@ class MammotionBaseBLEDevice(MammotionBaseDevice):
|
|
|
95
100
|
self._key: str | None = None
|
|
96
101
|
self._cloud_device = cloud_device
|
|
97
102
|
self.set_queue_callback(self.queue_command)
|
|
98
|
-
self._state_manager.ble_gethash_ack_callback = self.datahash_response
|
|
99
|
-
self._state_manager.ble_get_commondata_ack_callback = self.commdata_response
|
|
100
|
-
self._state_manager.ble_get_plan_callback = self.plan_callback
|
|
101
103
|
loop = asyncio.get_event_loop()
|
|
102
104
|
loop.create_task(self.process_queue())
|
|
103
105
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
from asyncio import InvalidStateError
|
|
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
24
|
from pymammotion.proto import LubaMsg
|
|
25
|
-
from pymammotion.utility.constant.device_constant import NO_REQUEST_MODES
|
|
26
25
|
|
|
27
26
|
_LOGGER = logging.getLogger(__name__)
|
|
28
27
|
|
|
@@ -30,7 +29,7 @@ _LOGGER = logging.getLogger(__name__)
|
|
|
30
29
|
class MammotionCloud:
|
|
31
30
|
"""Per account MQTT cloud."""
|
|
32
31
|
|
|
33
|
-
def __init__(self, mqtt_client: MammotionMQTT, cloud_client: CloudIOTGateway) -> None:
|
|
32
|
+
def __init__(self, mqtt_client: AliyunMQTT | MammotionMQTT, cloud_client: CloudIOTGateway) -> None:
|
|
34
33
|
"""Initialize MammotionCloud."""
|
|
35
34
|
self.cloud_client = cloud_client
|
|
36
35
|
self.command_sent_time = 0
|
|
@@ -70,7 +69,7 @@ class MammotionCloud:
|
|
|
70
69
|
self._mqtt_client.connect_async()
|
|
71
70
|
|
|
72
71
|
async def send_command(self, iot_id: str, command: bytes) -> None:
|
|
73
|
-
await self._mqtt_client.
|
|
72
|
+
await self._mqtt_client.send_cloud_command(iot_id, command)
|
|
74
73
|
|
|
75
74
|
async def on_connected(self) -> None:
|
|
76
75
|
"""Callback for when MQTT connects."""
|
|
@@ -106,18 +105,16 @@ class MammotionCloud:
|
|
|
106
105
|
self._key = key
|
|
107
106
|
_LOGGER.debug("Sending command: %s", key)
|
|
108
107
|
self.command_sent_time = time.time()
|
|
109
|
-
await self._mqtt_client.
|
|
108
|
+
await self._mqtt_client.send_cloud_command(iot_id, command)
|
|
110
109
|
|
|
111
|
-
async def _on_mqtt_message(self, topic: str, payload:
|
|
110
|
+
async def _on_mqtt_message(self, topic: str, payload: bytes, iot_id: str) -> None:
|
|
112
111
|
"""Handle incoming MQTT messages."""
|
|
113
|
-
_LOGGER.debug("MQTT message received on topic %s: %s, iot_id: %s", topic, payload, iot_id)
|
|
112
|
+
# _LOGGER.debug("MQTT message received on topic %s: %s, iot_id: %s", topic, payload, iot_id)
|
|
113
|
+
json_str = payload.decode("utf-8")
|
|
114
|
+
dict_payload = json.loads(json_str)
|
|
115
|
+
await self._parse_mqtt_response(topic, dict_payload, iot_id)
|
|
114
116
|
|
|
115
|
-
|
|
116
|
-
payload = json.loads(json_str)
|
|
117
|
-
|
|
118
|
-
await self._parse_mqtt_response(topic, payload)
|
|
119
|
-
|
|
120
|
-
async def _parse_mqtt_response(self, topic: str, payload: dict) -> None:
|
|
117
|
+
async def _parse_mqtt_response(self, topic: str, payload: dict, iot_id: str) -> None:
|
|
121
118
|
"""Parse and handle MQTT responses based on the topic.
|
|
122
119
|
|
|
123
120
|
This function processes different types of MQTT messages received from various
|
|
@@ -152,31 +149,41 @@ class MammotionCloud:
|
|
|
152
149
|
property_event = ThingPropertiesMessage.from_dict(payload)
|
|
153
150
|
await self.mqtt_properties_event.data_event(property_event)
|
|
154
151
|
|
|
152
|
+
if topic.endswith("/thing/event/device_protobuf_msg_event/post"):
|
|
153
|
+
_LOGGER.debug("Mammotion Thing event received")
|
|
154
|
+
mammotion_event = MammotionEventMessage.from_dict(payload)
|
|
155
|
+
mammotion_event.params.iot_id = iot_id
|
|
156
|
+
await self.mqtt_message_event.data_event(mammotion_event)
|
|
157
|
+
elif topic.endswith("/thing/event/property/post"):
|
|
158
|
+
_LOGGER.debug("Mammotion Property event received")
|
|
159
|
+
mammotion_property_event = MammotionPropertiesMessage.from_dict(payload)
|
|
160
|
+
mammotion_property_event.params.iot_id = iot_id
|
|
161
|
+
await self.mqtt_properties_event.data_event(mammotion_property_event)
|
|
162
|
+
|
|
155
163
|
def _disconnect(self) -> None:
|
|
156
164
|
"""Disconnect the MQTT client."""
|
|
157
165
|
self._mqtt_client.disconnect()
|
|
158
166
|
|
|
159
167
|
@property
|
|
160
|
-
def waiting_queue(self):
|
|
168
|
+
def waiting_queue(self) -> deque:
|
|
161
169
|
return self._waiting_queue
|
|
162
170
|
|
|
163
171
|
|
|
164
172
|
class MammotionBaseCloudDevice(MammotionBaseDevice):
|
|
165
173
|
"""Base class for Mammotion Cloud devices."""
|
|
166
174
|
|
|
167
|
-
def __init__(self, mqtt: MammotionCloud, cloud_device: Device, state_manager:
|
|
175
|
+
def __init__(self, mqtt: MammotionCloud, cloud_device: Device, state_manager: MowerStateManager) -> None:
|
|
168
176
|
"""Initialize MammotionBaseCloudDevice."""
|
|
169
177
|
super().__init__(state_manager, cloud_device)
|
|
170
|
-
self._ble_sync_task: TimerHandle | None = None
|
|
171
178
|
self.stopped = False
|
|
172
179
|
self.on_ready_callback: Callable[[], Awaitable[None]] | None = None
|
|
173
180
|
self.loop = asyncio.get_event_loop()
|
|
174
181
|
self._mqtt = mqtt
|
|
175
|
-
self.iot_id = cloud_device.
|
|
182
|
+
self.iot_id = cloud_device.iot_id
|
|
176
183
|
self.device = cloud_device
|
|
177
184
|
self._command_futures = {}
|
|
178
185
|
self._commands: MammotionCommand = MammotionCommand(
|
|
179
|
-
cloud_device.
|
|
186
|
+
cloud_device.device_name,
|
|
180
187
|
int(mqtt.cloud_client.mammotion_http.response.data.userInformation.userAccount),
|
|
181
188
|
)
|
|
182
189
|
self.currentID = ""
|
|
@@ -187,15 +194,10 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
|
|
|
187
194
|
self._mqtt.on_ready_event.add_subscribers(self.on_ready)
|
|
188
195
|
self._mqtt.on_disconnected_event.add_subscribers(self.on_disconnect)
|
|
189
196
|
self._mqtt.on_connected_event.add_subscribers(self.on_connect)
|
|
190
|
-
self._state_manager.cloud_gethash_ack_callback = self.datahash_response
|
|
191
|
-
self._state_manager.cloud_get_commondata_ack_callback = self.commdata_response
|
|
192
|
-
self._state_manager.cloud_get_plan_callback = self.plan_callback
|
|
193
197
|
self.set_queue_callback(self.queue_command)
|
|
194
198
|
|
|
195
|
-
if self._mqtt.is_ready:
|
|
196
|
-
self.run_periodic_sync_task()
|
|
197
|
-
|
|
198
199
|
def __del__(self) -> None:
|
|
200
|
+
"""Cleanup subscriptions."""
|
|
199
201
|
self._mqtt.on_ready_event.remove_subscribers(self.on_ready)
|
|
200
202
|
self._mqtt.on_disconnected_event.remove_subscribers(self.on_disconnect)
|
|
201
203
|
self._mqtt.on_connected_event.remove_subscribers(self.on_connect)
|
|
@@ -203,14 +205,6 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
|
|
|
203
205
|
self._mqtt.mqtt_properties_event.remove_subscribers(self._parse_message_properties_for_device)
|
|
204
206
|
self._mqtt.mqtt_status_event.remove_subscribers(self._parse_message_status_for_device)
|
|
205
207
|
self._mqtt.mqtt_device_event.remove_subscribers(self._parse_device_event_for_device)
|
|
206
|
-
self._state_manager.cloud_gethash_ack_callback = None
|
|
207
|
-
self._state_manager.cloud_get_commondata_ack_callback = None
|
|
208
|
-
self._state_manager.cloud_get_plan_callback = None
|
|
209
|
-
if self._ble_sync_task:
|
|
210
|
-
self._ble_sync_task.cancel()
|
|
211
|
-
|
|
212
|
-
def __del__(self) -> None:
|
|
213
|
-
"""Cleanup."""
|
|
214
208
|
self._state_manager.cloud_queue_command_callback.remove_subscribers(self.queue_command)
|
|
215
209
|
|
|
216
210
|
@property
|
|
@@ -234,26 +228,18 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
|
|
|
234
228
|
_LOGGER.debug("Device is offline")
|
|
235
229
|
|
|
236
230
|
async def on_disconnect(self) -> None:
|
|
237
|
-
if self._ble_sync_task:
|
|
238
|
-
self._ble_sync_task.cancel()
|
|
239
231
|
self._mqtt.disconnect()
|
|
240
232
|
|
|
241
233
|
async def on_connect(self) -> None:
|
|
242
|
-
|
|
243
|
-
if self._ble_sync_task is None or self._ble_sync_task.cancelled():
|
|
244
|
-
await self.run_periodic_sync_task()
|
|
234
|
+
"""On connect callback"""
|
|
245
235
|
|
|
246
236
|
async def stop(self) -> None:
|
|
247
237
|
"""Stop all tasks and disconnect."""
|
|
248
|
-
if self._ble_sync_task:
|
|
249
|
-
self._ble_sync_task.cancel()
|
|
250
238
|
# self._mqtt._mqtt_client.unsubscribe()
|
|
251
239
|
self.stopped = True
|
|
252
240
|
|
|
253
241
|
async def start(self) -> None:
|
|
254
|
-
|
|
255
|
-
if self._ble_sync_task is None or self._ble_sync_task.cancelled():
|
|
256
|
-
await self.run_periodic_sync_task()
|
|
242
|
+
"""Start the device connection."""
|
|
257
243
|
self.stopped = False
|
|
258
244
|
if not self.mqtt.is_connected():
|
|
259
245
|
loop = asyncio.get_running_loop()
|
|
@@ -262,35 +248,7 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
|
|
|
262
248
|
# self.mqtt._mqtt_client.thing_on_thing_enable(None)
|
|
263
249
|
|
|
264
250
|
async def _ble_sync(self) -> None:
|
|
265
|
-
|
|
266
|
-
self.stopped
|
|
267
|
-
or self.mqtt.is_connected is False
|
|
268
|
-
or self.state_manager.get_device().report_data.dev.sys_status in NO_REQUEST_MODES
|
|
269
|
-
):
|
|
270
|
-
return
|
|
271
|
-
|
|
272
|
-
command_bytes = self._commands.send_todev_ble_sync(3)
|
|
273
|
-
try:
|
|
274
|
-
await self._mqtt.send_command(self.iot_id, command_bytes)
|
|
275
|
-
except (CheckSessionException, SetupException, DeviceOfflineException):
|
|
276
|
-
if self._ble_sync_task:
|
|
277
|
-
self._ble_sync_task.cancel()
|
|
278
|
-
|
|
279
|
-
async def run_periodic_sync_task(self) -> None:
|
|
280
|
-
"""Send ble sync to robot."""
|
|
281
|
-
try:
|
|
282
|
-
if not self._mqtt._operation_lock.locked() or not self.stopped:
|
|
283
|
-
await self._ble_sync()
|
|
284
|
-
finally:
|
|
285
|
-
if not self.stopped:
|
|
286
|
-
self.schedule_ble_sync()
|
|
287
|
-
|
|
288
|
-
def schedule_ble_sync(self) -> None:
|
|
289
|
-
"""Periodically sync to keep connection alive."""
|
|
290
|
-
if self._mqtt is not None and self._mqtt.is_connected:
|
|
291
|
-
self._ble_sync_task = self.loop.call_later(
|
|
292
|
-
160, lambda: asyncio.ensure_future(self.run_periodic_sync_task())
|
|
293
|
-
)
|
|
251
|
+
pass
|
|
294
252
|
|
|
295
253
|
async def queue_command(self, key: str, **kwargs: Any) -> None:
|
|
296
254
|
# Create a future to hold the result
|
|
@@ -301,7 +259,8 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
|
|
|
301
259
|
await self._mqtt.command_queue.put((self.iot_id, key, command_bytes, future))
|
|
302
260
|
# Wait for the future to be resolved
|
|
303
261
|
try:
|
|
304
|
-
|
|
262
|
+
await future
|
|
263
|
+
return
|
|
305
264
|
except asyncio.CancelledError:
|
|
306
265
|
"""Try again once."""
|
|
307
266
|
future = asyncio.Future()
|
|
@@ -329,18 +288,18 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
|
|
|
329
288
|
return None
|
|
330
289
|
|
|
331
290
|
async def _parse_message_properties_for_device(self, event: ThingPropertiesMessage) -> None:
|
|
332
|
-
if event.params.
|
|
291
|
+
if event.params.iot_id != self.iot_id:
|
|
333
292
|
return
|
|
334
293
|
await self.state_manager.properties(event)
|
|
335
294
|
|
|
336
295
|
async def _parse_message_status_for_device(self, status: ThingStatusMessage) -> None:
|
|
337
|
-
if status.params.
|
|
296
|
+
if status.params.iot_id != self.iot_id:
|
|
338
297
|
return
|
|
339
298
|
await self.state_manager.status(status)
|
|
340
299
|
|
|
341
300
|
async def _parse_device_event_for_device(self, status: ThingStatusMessage) -> None:
|
|
342
301
|
"""Process device event if it matches the device's IoT ID."""
|
|
343
|
-
if status.params.
|
|
302
|
+
if status.params.iot_id != self.iot_id:
|
|
344
303
|
return
|
|
345
304
|
await self.state_manager.device_event(status)
|
|
346
305
|
|
|
@@ -359,7 +318,7 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
|
|
|
359
318
|
"""
|
|
360
319
|
params = event.params
|
|
361
320
|
new_msg = LubaMsg()
|
|
362
|
-
if event.params.
|
|
321
|
+
if event.params.iot_id != self.iot_id:
|
|
363
322
|
return
|
|
364
323
|
binary_data = base64.b64decode(params.value.content)
|
|
365
324
|
try:
|
|
@@ -370,9 +329,9 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
|
|
|
370
329
|
|
|
371
330
|
if (
|
|
372
331
|
self._commands.get_device_product_key() == ""
|
|
373
|
-
and self._commands.get_device_name() == event.params.
|
|
332
|
+
and self._commands.get_device_name() == event.params.device_name
|
|
374
333
|
):
|
|
375
|
-
self._commands.set_device_product_key(event.params.
|
|
334
|
+
self._commands.set_device_product_key(event.params.product_key)
|
|
376
335
|
|
|
377
336
|
res = betterproto2.which_one_of(new_msg, "LubaSubMsg")
|
|
378
337
|
if res[0] == "net":
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Mower device with Bluetooth LE connectivity."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from bleak import BLEDevice
|
|
7
|
+
|
|
8
|
+
from pymammotion.aliyun.model.dev_by_account_response import Device
|
|
9
|
+
from pymammotion.data.mower_state_manager import MowerStateManager
|
|
10
|
+
from pymammotion.mammotion.devices.mammotion_bluetooth import MammotionBaseBLEDevice
|
|
11
|
+
from pymammotion.mammotion.devices.mower_device import MammotionMowerDevice
|
|
12
|
+
|
|
13
|
+
_LOGGER = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MammotionMowerBLEDevice(MammotionBaseBLEDevice, MammotionMowerDevice):
|
|
17
|
+
"""Mower device with BLE connectivity and map synchronization."""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
state_manager: MowerStateManager,
|
|
22
|
+
cloud_device: Device,
|
|
23
|
+
device: BLEDevice,
|
|
24
|
+
interface: int = 0,
|
|
25
|
+
**kwargs: Any,
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Initialize MammotionMowerBLEDevice.
|
|
28
|
+
|
|
29
|
+
Uses multiple inheritance to combine:
|
|
30
|
+
- MammotionBaseBLEDevice: BLE communication
|
|
31
|
+
- MammotionMowerDevice: Map sync callbacks
|
|
32
|
+
"""
|
|
33
|
+
# Initialize base BLE device (which also initializes MammotionBaseDevice)
|
|
34
|
+
MammotionBaseBLEDevice.__init__(self, state_manager, cloud_device, device, interface, **kwargs)
|
|
35
|
+
# Set up mower-specific BLE callbacks
|
|
36
|
+
self._state_manager.ble_gethash_ack_callback = self.datahash_response
|
|
37
|
+
self._state_manager.ble_get_commondata_ack_callback = self.commdata_response
|
|
38
|
+
self._state_manager.ble_get_plan_callback = self.plan_callback
|
|
39
|
+
|
|
40
|
+
def __del__(self) -> None:
|
|
41
|
+
"""Cleanup subscriptions and callbacks."""
|
|
42
|
+
# Clean up mower-specific callbacks
|
|
43
|
+
if hasattr(self, "_state_manager"):
|
|
44
|
+
self._state_manager.ble_gethash_ack_callback = None
|
|
45
|
+
self._state_manager.ble_get_commondata_ack_callback = None
|
|
46
|
+
self._state_manager.ble_get_plan_callback = None
|
|
47
|
+
# Call parent cleanup
|
|
48
|
+
if MammotionBaseBLEDevice.__del__:
|
|
49
|
+
super().__del__()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Mower device with cloud MQTT connectivity."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
from pymammotion.aliyun.model.dev_by_account_response import Device
|
|
6
|
+
from pymammotion.data.mower_state_manager import MowerStateManager
|
|
7
|
+
from pymammotion.mammotion.devices.mammotion_cloud import MammotionBaseCloudDevice, MammotionCloud
|
|
8
|
+
from pymammotion.mammotion.devices.mower_device import MammotionMowerDevice
|
|
9
|
+
|
|
10
|
+
_LOGGER = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MammotionMowerCloudDevice(MammotionBaseCloudDevice, MammotionMowerDevice):
|
|
14
|
+
"""Mower device with cloud connectivity and map synchronization."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, mqtt: MammotionCloud, cloud_device: Device, state_manager: MowerStateManager) -> None:
|
|
17
|
+
"""Initialize MammotionMowerCloudDevice.
|
|
18
|
+
|
|
19
|
+
Uses multiple inheritance to combine:
|
|
20
|
+
- MammotionBaseCloudDevice: MQTT communication
|
|
21
|
+
- MammotionMowerDevice: Map sync callbacks
|
|
22
|
+
"""
|
|
23
|
+
# Initialize base cloud device (which also initializes MammotionBaseDevice)
|
|
24
|
+
super().__init__(mqtt, cloud_device, state_manager)
|
|
25
|
+
# Initialize mower device callbacks (but skip base device init as it's already done)
|
|
26
|
+
# We manually set the callbacks that MammotionMowerDevice would set
|
|
27
|
+
self._state_manager.cloud_gethash_ack_callback = self.datahash_response
|
|
28
|
+
self._state_manager.cloud_get_commondata_ack_callback = self.commdata_response
|
|
29
|
+
self._state_manager.cloud_get_plan_callback = self.plan_callback
|
|
30
|
+
|
|
31
|
+
def __del__(self) -> None:
|
|
32
|
+
"""Cleanup subscriptions and callbacks."""
|
|
33
|
+
# Clean up mower-specific callbacks
|
|
34
|
+
if hasattr(self, "_state_manager"):
|
|
35
|
+
self._state_manager.cloud_gethash_ack_callback = None
|
|
36
|
+
self._state_manager.cloud_get_commondata_ack_callback = None
|
|
37
|
+
self._state_manager.cloud_get_plan_callback = None
|
|
38
|
+
# Call parent cleanup
|
|
39
|
+
super().__del__()
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
from bleak import BLEDevice
|
|
4
|
+
|
|
5
|
+
from pymammotion import CloudIOTGateway
|
|
6
|
+
from pymammotion.aliyun.model.dev_by_account_response import Device
|
|
7
|
+
from pymammotion.data.model.device import MowingDevice, RTKDevice
|
|
8
|
+
from pymammotion.data.model.enums import ConnectionPreference
|
|
9
|
+
from pymammotion.mammotion.devices.mammotion_bluetooth import MammotionBaseBLEDevice
|
|
10
|
+
from pymammotion.mammotion.devices.mammotion_cloud import MammotionBaseCloudDevice, MammotionCloud
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AbstractDeviceManager(ABC):
|
|
14
|
+
"""Abstract base class for device managers."""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
name: str,
|
|
19
|
+
iot_id: str,
|
|
20
|
+
cloud_client: CloudIOTGateway,
|
|
21
|
+
cloud_device: Device,
|
|
22
|
+
preference: ConnectionPreference = ConnectionPreference.BLUETOOTH,
|
|
23
|
+
) -> None:
|
|
24
|
+
self.name = name
|
|
25
|
+
self.iot_id = iot_id
|
|
26
|
+
self.cloud_client = cloud_client
|
|
27
|
+
self._device: Device = cloud_device
|
|
28
|
+
self.mammotion_http = cloud_client.mammotion_http
|
|
29
|
+
self.preference = preference
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
@abstractmethod
|
|
33
|
+
def state(self) -> MowingDevice | RTKDevice:
|
|
34
|
+
"""Return the state of the device."""
|
|
35
|
+
|
|
36
|
+
@state.setter
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def state(self, value: MowingDevice | RTKDevice) -> None:
|
|
39
|
+
"""Set the device state."""
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
@abstractmethod
|
|
43
|
+
def ble(self) -> MammotionBaseBLEDevice | None:
|
|
44
|
+
"""Return BLE device interface."""
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
@abstractmethod
|
|
48
|
+
def cloud(self) -> MammotionBaseCloudDevice | None:
|
|
49
|
+
"""Return cloud device interface."""
|
|
50
|
+
|
|
51
|
+
@abstractmethod
|
|
52
|
+
def has_queued_commands(self) -> bool:
|
|
53
|
+
"""Check if there are queued commands."""
|
|
54
|
+
|
|
55
|
+
@abstractmethod
|
|
56
|
+
def add_ble(self, ble_device: BLEDevice) -> MammotionBaseBLEDevice:
|
|
57
|
+
"""Add BLE device."""
|
|
58
|
+
|
|
59
|
+
@abstractmethod
|
|
60
|
+
def add_cloud(self, mqtt: MammotionCloud) -> MammotionBaseCloudDevice:
|
|
61
|
+
"""Add cloud device."""
|
|
62
|
+
|
|
63
|
+
@abstractmethod
|
|
64
|
+
def replace_cloud(self, cloud_device: MammotionBaseCloudDevice) -> None:
|
|
65
|
+
"""Replace cloud device."""
|
|
66
|
+
|
|
67
|
+
@abstractmethod
|
|
68
|
+
def remove_cloud(self) -> None:
|
|
69
|
+
"""Remove cloud device."""
|
|
70
|
+
|
|
71
|
+
@abstractmethod
|
|
72
|
+
def replace_ble(self, ble_device: MammotionBaseBLEDevice) -> None:
|
|
73
|
+
"""Replace BLE device."""
|
|
74
|
+
|
|
75
|
+
@abstractmethod
|
|
76
|
+
def remove_ble(self) -> None:
|
|
77
|
+
"""Remove BLE device."""
|
|
78
|
+
|
|
79
|
+
@abstractmethod
|
|
80
|
+
def replace_mqtt(self, mqtt: MammotionCloud) -> None:
|
|
81
|
+
"""Replace MQTT connection."""
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Mower-specific device class with map synchronization callbacks."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
from pymammotion.aliyun.model.dev_by_account_response import Device
|
|
7
|
+
from pymammotion.data.model import RegionData
|
|
8
|
+
from pymammotion.data.mower_state_manager import MowerStateManager
|
|
9
|
+
from pymammotion.mammotion.devices.base import MammotionBaseDevice
|
|
10
|
+
from pymammotion.proto import NavGetCommDataAck, NavGetHashListAck, NavPlanJobSet, SvgMessageAckT
|
|
11
|
+
from pymammotion.utility.device_type import DeviceType
|
|
12
|
+
|
|
13
|
+
_LOGGER = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def find_next_integer(lst: list[int], current_hash: int) -> int | None:
|
|
17
|
+
"""Find the next integer in a list after the current hash."""
|
|
18
|
+
try:
|
|
19
|
+
current_index = lst.index(current_hash)
|
|
20
|
+
if current_index + 1 < len(lst):
|
|
21
|
+
return lst[current_index + 1]
|
|
22
|
+
else:
|
|
23
|
+
return None
|
|
24
|
+
except ValueError:
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class MammotionMowerDevice(MammotionBaseDevice, ABC):
|
|
29
|
+
"""Mower device with map synchronization support."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, state_manager: MowerStateManager, cloud_device: Device) -> None:
|
|
32
|
+
"""Initialize MammotionMowerDevice."""
|
|
33
|
+
super().__init__(state_manager, cloud_device)
|
|
34
|
+
# Register mower-specific callbacks
|
|
35
|
+
self._state_manager.cloud_gethash_ack_callback = self.datahash_response
|
|
36
|
+
self._state_manager.cloud_get_commondata_ack_callback = self.commdata_response
|
|
37
|
+
self._state_manager.cloud_get_plan_callback = self.plan_callback
|
|
38
|
+
|
|
39
|
+
async def datahash_response(self, hash_ack: NavGetHashListAck) -> None:
|
|
40
|
+
"""Handle datahash responses for root level hashs."""
|
|
41
|
+
current_frame = hash_ack.current_frame
|
|
42
|
+
|
|
43
|
+
missing_frames = self.mower.map.missing_root_hash_frame(hash_ack)
|
|
44
|
+
if len(missing_frames) == 0:
|
|
45
|
+
if len(self.mower.map.missing_hashlist(hash_ack.sub_cmd)) > 0:
|
|
46
|
+
data_hash = self.mower.map.missing_hashlist(hash_ack.sub_cmd).pop(0)
|
|
47
|
+
await self.queue_command("synchronize_hash_data", hash_num=data_hash)
|
|
48
|
+
return
|
|
49
|
+
|
|
50
|
+
if current_frame != missing_frames[0] - 1:
|
|
51
|
+
current_frame = missing_frames[0] - 1
|
|
52
|
+
await self.queue_command("get_hash_response", total_frame=hash_ack.total_frame, current_frame=current_frame)
|
|
53
|
+
|
|
54
|
+
async def commdata_response(self, common_data: NavGetCommDataAck | SvgMessageAckT) -> None:
|
|
55
|
+
"""Handle common data responses."""
|
|
56
|
+
total_frame = common_data.total_frame
|
|
57
|
+
current_frame = common_data.current_frame
|
|
58
|
+
|
|
59
|
+
missing_frames = self.mower.map.missing_frame(common_data)
|
|
60
|
+
if len(missing_frames) == 0:
|
|
61
|
+
# get next in hash ack list
|
|
62
|
+
data_hash = (
|
|
63
|
+
self.mower.map.missing_hashlist(common_data.sub_cmd).pop(0)
|
|
64
|
+
if len(self.mower.map.missing_hashlist(common_data.sub_cmd)) > 0
|
|
65
|
+
else None
|
|
66
|
+
)
|
|
67
|
+
if data_hash is None:
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
await self.queue_command("synchronize_hash_data", hash_num=data_hash)
|
|
71
|
+
else:
|
|
72
|
+
if current_frame != missing_frames[0] - 1:
|
|
73
|
+
current_frame = missing_frames[0] - 1
|
|
74
|
+
|
|
75
|
+
region_data = RegionData()
|
|
76
|
+
region_data.hash = common_data.data_hash if isinstance(common_data, SvgMessageAckT) else common_data.hash
|
|
77
|
+
region_data.action = common_data.action if isinstance(common_data, NavGetCommDataAck) else 0
|
|
78
|
+
region_data.type = common_data.type
|
|
79
|
+
region_data.sub_cmd = common_data.sub_cmd
|
|
80
|
+
region_data.total_frame = total_frame
|
|
81
|
+
region_data.current_frame = current_frame
|
|
82
|
+
await self.queue_command("get_regional_data", regional_data=region_data)
|
|
83
|
+
|
|
84
|
+
async def plan_callback(self, plan: NavPlanJobSet) -> None:
|
|
85
|
+
"""Handle plan job responses."""
|
|
86
|
+
if plan.plan_index < plan.total_plan_num - 1:
|
|
87
|
+
index = plan.plan_index + 1
|
|
88
|
+
await self.queue_command("read_plan", sub_cmd=2, plan_index=index)
|
|
89
|
+
|
|
90
|
+
async def start_map_sync(self) -> None:
|
|
91
|
+
"""Start sync of map data."""
|
|
92
|
+
if location := next((loc for loc in self.mower.report_data.locations if loc.pos_type == 5), None):
|
|
93
|
+
self.mower.map.update_hash_lists(self.mower.map.hashlist, location.bol_hash)
|
|
94
|
+
|
|
95
|
+
await self.queue_command("send_todev_ble_sync", sync_type=3)
|
|
96
|
+
|
|
97
|
+
if self._cloud_device and len(self.mower.map.area_name) == 0 and not DeviceType.is_luba1(self.mower.name):
|
|
98
|
+
await self.queue_command("get_area_name_list", device_id=self._cloud_device.iot_id)
|
|
99
|
+
|
|
100
|
+
if len(self.mower.map.root_hash_lists) == 0 or len(self.mower.map.missing_hashlist()) > 0:
|
|
101
|
+
await self.queue_command("get_all_boundary_hash_list", sub_cmd=0)
|
|
102
|
+
|
|
103
|
+
if len(self.mower.map.plan) == 0 or list(self.mower.map.plan.values())[0].total_plan_num != len(
|
|
104
|
+
self.mower.map.plan
|
|
105
|
+
):
|
|
106
|
+
await self.queue_command("read_plan", sub_cmd=2, plan_index=0)
|
|
107
|
+
|
|
108
|
+
for hash_id, frame in list(self.mower.map.area.items()):
|
|
109
|
+
missing_frames = self.mower.map.find_missing_frames(frame)
|
|
110
|
+
if len(missing_frames) > 0:
|
|
111
|
+
del self.mower.map.area[hash_id]
|
|
112
|
+
|
|
113
|
+
for hash_id, frame in list(self.mower.map.path.items()):
|
|
114
|
+
missing_frames = self.mower.map.find_missing_frames(frame)
|
|
115
|
+
if len(missing_frames) > 0:
|
|
116
|
+
del self.mower.map.path[hash_id]
|
|
117
|
+
|
|
118
|
+
for hash_id, frame in list(self.mower.map.obstacle.items()):
|
|
119
|
+
missing_frames = self.mower.map.find_missing_frames(frame)
|
|
120
|
+
if len(missing_frames) > 0:
|
|
121
|
+
del self.mower.map.obstacle[hash_id]
|