pymammotion 0.5.34__py3-none-any.whl → 0.5.40__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 +106 -18
- pymammotion/aliyun/model/dev_by_account_response.py +198 -20
- pymammotion/data/model/device.py +1 -0
- pymammotion/data/model/device_config.py +1 -1
- pymammotion/data/model/enums.py +3 -1
- pymammotion/data/model/generate_route_information.py +2 -2
- pymammotion/data/model/hash_list.py +105 -33
- pymammotion/data/model/region_data.py +4 -4
- pymammotion/data/{state_manager.py → mower_state_manager.py} +17 -7
- pymammotion/homeassistant/__init__.py +3 -0
- pymammotion/homeassistant/mower_api.py +446 -0
- pymammotion/homeassistant/rtk_api.py +54 -0
- pymammotion/http/http.py +115 -4
- pymammotion/http/model/http.py +77 -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 -138
- pymammotion/mammotion/devices/mammotion.py +361 -204
- pymammotion/mammotion/devices/mammotion_bluetooth.py +7 -5
- pymammotion/mammotion/devices/mammotion_cloud.py +22 -74
- 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 +132 -194
- 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/device_type.py +88 -3
- pymammotion/utility/mur_mur_hash.py +132 -87
- {pymammotion-0.5.34.dist-info → pymammotion-0.5.40.dist-info}/METADATA +25 -31
- {pymammotion-0.5.34.dist-info → pymammotion-0.5.40.dist-info}/RECORD +54 -40
- {pymammotion-0.5.34.dist-info → pymammotion-0.5.40.dist-info}/WHEEL +1 -1
- {pymammotion-0.5.34.dist-info → pymammotion-0.5.40.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.mower_state_manager import MowerStateManager
|
|
17
18
|
from pymammotion.data.mqtt.event import ThingEventMessage
|
|
18
19
|
from pymammotion.data.mqtt.properties import 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,16 +105,15 @@ 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)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
await self._parse_mqtt_response(topic, payload)
|
|
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
|
+
print(dict_payload)
|
|
116
|
+
await self._parse_mqtt_response(topic, dict_payload)
|
|
119
117
|
|
|
120
118
|
async def _parse_mqtt_response(self, topic: str, payload: dict) -> None:
|
|
121
119
|
"""Parse and handle MQTT responses based on the topic.
|
|
@@ -157,26 +155,25 @@ class MammotionCloud:
|
|
|
157
155
|
self._mqtt_client.disconnect()
|
|
158
156
|
|
|
159
157
|
@property
|
|
160
|
-
def waiting_queue(self):
|
|
158
|
+
def waiting_queue(self) -> deque:
|
|
161
159
|
return self._waiting_queue
|
|
162
160
|
|
|
163
161
|
|
|
164
162
|
class MammotionBaseCloudDevice(MammotionBaseDevice):
|
|
165
163
|
"""Base class for Mammotion Cloud devices."""
|
|
166
164
|
|
|
167
|
-
def __init__(self, mqtt: MammotionCloud, cloud_device: Device, state_manager:
|
|
165
|
+
def __init__(self, mqtt: MammotionCloud, cloud_device: Device, state_manager: MowerStateManager) -> None:
|
|
168
166
|
"""Initialize MammotionBaseCloudDevice."""
|
|
169
167
|
super().__init__(state_manager, cloud_device)
|
|
170
|
-
self._ble_sync_task: TimerHandle | None = None
|
|
171
168
|
self.stopped = False
|
|
172
169
|
self.on_ready_callback: Callable[[], Awaitable[None]] | None = None
|
|
173
170
|
self.loop = asyncio.get_event_loop()
|
|
174
171
|
self._mqtt = mqtt
|
|
175
|
-
self.iot_id = cloud_device.
|
|
172
|
+
self.iot_id = cloud_device.iot_id
|
|
176
173
|
self.device = cloud_device
|
|
177
174
|
self._command_futures = {}
|
|
178
175
|
self._commands: MammotionCommand = MammotionCommand(
|
|
179
|
-
cloud_device.
|
|
176
|
+
cloud_device.device_name,
|
|
180
177
|
int(mqtt.cloud_client.mammotion_http.response.data.userInformation.userAccount),
|
|
181
178
|
)
|
|
182
179
|
self.currentID = ""
|
|
@@ -187,15 +184,10 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
|
|
|
187
184
|
self._mqtt.on_ready_event.add_subscribers(self.on_ready)
|
|
188
185
|
self._mqtt.on_disconnected_event.add_subscribers(self.on_disconnect)
|
|
189
186
|
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
187
|
self.set_queue_callback(self.queue_command)
|
|
194
188
|
|
|
195
|
-
if self._mqtt.is_ready:
|
|
196
|
-
self.run_periodic_sync_task()
|
|
197
|
-
|
|
198
189
|
def __del__(self) -> None:
|
|
190
|
+
"""Cleanup subscriptions."""
|
|
199
191
|
self._mqtt.on_ready_event.remove_subscribers(self.on_ready)
|
|
200
192
|
self._mqtt.on_disconnected_event.remove_subscribers(self.on_disconnect)
|
|
201
193
|
self._mqtt.on_connected_event.remove_subscribers(self.on_connect)
|
|
@@ -203,14 +195,6 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
|
|
|
203
195
|
self._mqtt.mqtt_properties_event.remove_subscribers(self._parse_message_properties_for_device)
|
|
204
196
|
self._mqtt.mqtt_status_event.remove_subscribers(self._parse_message_status_for_device)
|
|
205
197
|
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
198
|
self._state_manager.cloud_queue_command_callback.remove_subscribers(self.queue_command)
|
|
215
199
|
|
|
216
200
|
@property
|
|
@@ -234,26 +218,17 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
|
|
|
234
218
|
_LOGGER.debug("Device is offline")
|
|
235
219
|
|
|
236
220
|
async def on_disconnect(self) -> None:
|
|
237
|
-
if self._ble_sync_task:
|
|
238
|
-
self._ble_sync_task.cancel()
|
|
239
221
|
self._mqtt.disconnect()
|
|
240
222
|
|
|
241
223
|
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()
|
|
224
|
+
"""On connect callback"""
|
|
245
225
|
|
|
246
226
|
async def stop(self) -> None:
|
|
247
227
|
"""Stop all tasks and disconnect."""
|
|
248
|
-
if self._ble_sync_task:
|
|
249
|
-
self._ble_sync_task.cancel()
|
|
250
228
|
# self._mqtt._mqtt_client.unsubscribe()
|
|
251
229
|
self.stopped = True
|
|
252
230
|
|
|
253
231
|
async def start(self) -> None:
|
|
254
|
-
await self._ble_sync()
|
|
255
|
-
if self._ble_sync_task is None or self._ble_sync_task.cancelled():
|
|
256
|
-
await self.run_periodic_sync_task()
|
|
257
232
|
self.stopped = False
|
|
258
233
|
if not self.mqtt.is_connected():
|
|
259
234
|
loop = asyncio.get_running_loop()
|
|
@@ -262,35 +237,7 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
|
|
|
262
237
|
# self.mqtt._mqtt_client.thing_on_thing_enable(None)
|
|
263
238
|
|
|
264
239
|
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
|
-
)
|
|
240
|
+
pass
|
|
294
241
|
|
|
295
242
|
async def queue_command(self, key: str, **kwargs: Any) -> None:
|
|
296
243
|
# Create a future to hold the result
|
|
@@ -301,7 +248,8 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
|
|
|
301
248
|
await self._mqtt.command_queue.put((self.iot_id, key, command_bytes, future))
|
|
302
249
|
# Wait for the future to be resolved
|
|
303
250
|
try:
|
|
304
|
-
|
|
251
|
+
await future
|
|
252
|
+
return
|
|
305
253
|
except asyncio.CancelledError:
|
|
306
254
|
"""Try again once."""
|
|
307
255
|
future = asyncio.Future()
|
|
@@ -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]
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
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
|
|
8
|
+
from pymammotion.data.model.enums import ConnectionPreference
|
|
9
|
+
from pymammotion.data.mower_state_manager import MowerStateManager
|
|
10
|
+
from pymammotion.mammotion.devices.mammotion_cloud import MammotionCloud
|
|
11
|
+
from pymammotion.mammotion.devices.mammotion_mower_ble import MammotionMowerBLEDevice
|
|
12
|
+
from pymammotion.mammotion.devices.mammotion_mower_cloud import MammotionMowerCloudDevice
|
|
13
|
+
from pymammotion.mammotion.devices.managers.managers import AbstractDeviceManager
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MammotionMowerDeviceManager(AbstractDeviceManager):
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
name: str,
|
|
20
|
+
iot_id: str,
|
|
21
|
+
cloud_client: CloudIOTGateway,
|
|
22
|
+
cloud_device: Device,
|
|
23
|
+
ble_device: BLEDevice | None = None,
|
|
24
|
+
mqtt: MammotionCloud | None = None,
|
|
25
|
+
preference: ConnectionPreference = ConnectionPreference.BLUETOOTH,
|
|
26
|
+
) -> None:
|
|
27
|
+
super().__init__(name, iot_id, cloud_client, cloud_device, preference)
|
|
28
|
+
self._ble_device: MammotionMowerBLEDevice | None = None
|
|
29
|
+
self._cloud_device: MammotionMowerCloudDevice | None = None
|
|
30
|
+
|
|
31
|
+
self._state_manager = MowerStateManager(MowingDevice())
|
|
32
|
+
self._state_manager.get_device().name = name
|
|
33
|
+
self.add_ble(ble_device) if ble_device else None
|
|
34
|
+
self.add_cloud(mqtt) if mqtt else None
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def state_manager(self) -> MowerStateManager:
|
|
38
|
+
"""Return the state manager."""
|
|
39
|
+
return self._state_manager
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def state(self) -> MowingDevice:
|
|
43
|
+
"""Return the state of the device."""
|
|
44
|
+
return self._state_manager.get_device()
|
|
45
|
+
|
|
46
|
+
@state.setter
|
|
47
|
+
def state(self, value: MowingDevice) -> None:
|
|
48
|
+
self._state_manager.set_device(value)
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def ble(self) -> MammotionMowerBLEDevice | None:
|
|
52
|
+
return self._ble_device
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def cloud(self) -> MammotionMowerCloudDevice | None:
|
|
56
|
+
return self._cloud_device
|
|
57
|
+
|
|
58
|
+
def has_queued_commands(self) -> bool:
|
|
59
|
+
if self.cloud and self.preference == ConnectionPreference.WIFI:
|
|
60
|
+
return not self.cloud.mqtt.command_queue.empty()
|
|
61
|
+
elif self.ble:
|
|
62
|
+
return not self.ble.command_queue.empty()
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
def add_ble(self, ble_device: BLEDevice) -> MammotionMowerBLEDevice:
|
|
66
|
+
self._ble_device = MammotionMowerBLEDevice(
|
|
67
|
+
state_manager=self._state_manager, cloud_device=self._device, device=ble_device
|
|
68
|
+
)
|
|
69
|
+
return self._ble_device
|
|
70
|
+
|
|
71
|
+
def add_cloud(self, mqtt: MammotionCloud) -> MammotionMowerCloudDevice:
|
|
72
|
+
self._cloud_device = MammotionMowerCloudDevice(
|
|
73
|
+
mqtt, cloud_device=self._device, state_manager=self._state_manager
|
|
74
|
+
)
|
|
75
|
+
return self._cloud_device
|
|
76
|
+
|
|
77
|
+
def replace_cloud(self, cloud_device: MammotionMowerCloudDevice) -> None:
|
|
78
|
+
self._cloud_device = cloud_device
|
|
79
|
+
|
|
80
|
+
def remove_cloud(self) -> None:
|
|
81
|
+
self._state_manager.cloud_get_commondata_ack_callback = None
|
|
82
|
+
self._state_manager.cloud_get_hashlist_ack_callback = None
|
|
83
|
+
self._state_manager.cloud_get_plan_callback = None
|
|
84
|
+
self._state_manager.cloud_on_notification_callback = None
|
|
85
|
+
self._state_manager.cloud_gethash_ack_callback = None
|
|
86
|
+
self._cloud_device = None
|
|
87
|
+
|
|
88
|
+
def replace_ble(self, ble_device: MammotionMowerBLEDevice) -> None:
|
|
89
|
+
self._ble_device = ble_device
|
|
90
|
+
|
|
91
|
+
def remove_ble(self) -> None:
|
|
92
|
+
self._state_manager.ble_get_commondata_ack_callback = None
|
|
93
|
+
self._state_manager.ble_get_hashlist_ack_callback = None
|
|
94
|
+
self._state_manager.ble_get_plan_callback = None
|
|
95
|
+
self._state_manager.ble_on_notification_callback = None
|
|
96
|
+
self._state_manager.ble_gethash_ack_callback = None
|
|
97
|
+
self._ble_device = None
|
|
98
|
+
|
|
99
|
+
def replace_mqtt(self, mqtt: MammotionCloud) -> None:
|
|
100
|
+
device = self._cloud_device.device
|
|
101
|
+
self._cloud_device = MammotionMowerCloudDevice(mqtt, cloud_device=device, state_manager=self._state_manager)
|
|
102
|
+
|
|
103
|
+
def has_cloud(self) -> bool:
|
|
104
|
+
return self._cloud_device is not None
|
|
105
|
+
|
|
106
|
+
def has_ble(self) -> bool:
|
|
107
|
+
return self._ble_device is not None
|