pymammotion 0.2.62__py3-none-any.whl → 0.5.51__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pymammotion might be problematic. Click here for more details.
- pymammotion/__init__.py +9 -6
- pymammotion/aliyun/client.py +235 -0
- pymammotion/aliyun/cloud_gateway.py +320 -69
- pymammotion/aliyun/model/aep_response.py +1 -2
- pymammotion/aliyun/model/dev_by_account_response.py +170 -23
- pymammotion/aliyun/model/login_by_oauth_response.py +2 -3
- pymammotion/aliyun/model/regions_response.py +3 -3
- pymammotion/aliyun/model/session_by_authcode_response.py +2 -2
- pymammotion/aliyun/model/thing_response.py +12 -0
- pymammotion/aliyun/regions.py +62 -0
- pymammotion/aliyun/tea/core.py +297 -0
- pymammotion/bluetooth/ble.py +11 -15
- pymammotion/bluetooth/ble_message.py +389 -106
- pymammotion/bluetooth/model/atomic_integer.py +54 -0
- pymammotion/const.py +3 -0
- pymammotion/data/model/__init__.py +1 -2
- pymammotion/data/model/device.py +92 -240
- pymammotion/data/model/device_config.py +10 -24
- pymammotion/data/model/device_info.py +35 -0
- pymammotion/data/model/device_limits.py +49 -0
- pymammotion/data/model/enums.py +12 -2
- pymammotion/data/model/errors.py +12 -0
- pymammotion/data/model/events.py +14 -0
- pymammotion/data/model/generate_geojson.py +521 -0
- pymammotion/data/model/generate_route_information.py +3 -4
- pymammotion/data/model/hash_list.py +384 -48
- pymammotion/data/model/location.py +4 -4
- pymammotion/data/model/mowing_modes.py +24 -1
- pymammotion/data/model/raw_data.py +215 -0
- pymammotion/data/model/region_data.py +10 -11
- pymammotion/data/model/report_info.py +62 -6
- pymammotion/data/model/work.py +27 -0
- pymammotion/data/mower_state_manager.py +316 -0
- pymammotion/data/mqtt/event.py +73 -28
- pymammotion/data/mqtt/mammotion_properties.py +257 -0
- pymammotion/data/mqtt/properties.py +93 -78
- pymammotion/data/mqtt/status.py +18 -17
- pymammotion/event/event.py +32 -8
- pymammotion/homeassistant/__init__.py +3 -0
- pymammotion/homeassistant/mower_api.py +484 -0
- pymammotion/homeassistant/rtk_api.py +54 -0
- pymammotion/http/__init__.py +0 -0
- pymammotion/http/encryption.py +220 -0
- pymammotion/http/http.py +652 -44
- pymammotion/http/model/__init__.py +0 -0
- pymammotion/{aliyun/model/stream_subscription_response.py → http/model/camera_stream.py} +14 -2
- pymammotion/http/model/http.py +160 -9
- pymammotion/http/model/response_factory.py +61 -0
- pymammotion/http/model/rtk.py +16 -0
- pymammotion/mammotion/commands/abstract_message.py +7 -5
- pymammotion/mammotion/commands/mammotion_command.py +32 -3
- pymammotion/mammotion/commands/messages/basestation.py +43 -0
- pymammotion/mammotion/commands/messages/driver.py +61 -29
- pymammotion/mammotion/commands/messages/media.py +68 -15
- pymammotion/mammotion/commands/messages/navigation.py +61 -25
- pymammotion/mammotion/commands/messages/network.py +93 -100
- pymammotion/mammotion/commands/messages/ota.py +18 -18
- pymammotion/mammotion/commands/messages/system.py +97 -72
- pymammotion/mammotion/commands/messages/video.py +17 -12
- pymammotion/mammotion/devices/__init__.py +27 -3
- pymammotion/mammotion/devices/base.py +50 -127
- pymammotion/mammotion/devices/mammotion.py +447 -212
- pymammotion/mammotion/devices/mammotion_bluetooth.py +105 -60
- pymammotion/mammotion/devices/mammotion_cloud.py +157 -105
- pymammotion/mammotion/devices/mammotion_mower_ble.py +49 -0
- pymammotion/mammotion/devices/mammotion_mower_cloud.py +39 -0
- pymammotion/mammotion/devices/managers/managers.py +81 -0
- pymammotion/mammotion/devices/mower_device.py +124 -0
- pymammotion/mammotion/devices/mower_manager.py +107 -0
- pymammotion/mammotion/devices/rtk_ble.py +89 -0
- pymammotion/mammotion/devices/rtk_cloud.py +113 -0
- pymammotion/mammotion/devices/rtk_device.py +50 -0
- pymammotion/mammotion/devices/rtk_manager.py +122 -0
- pymammotion/mqtt/__init__.py +2 -1
- pymammotion/mqtt/aliyun_mqtt.py +232 -0
- pymammotion/mqtt/linkkit/__init__.py +5 -0
- pymammotion/mqtt/linkkit/h2client.py +585 -0
- pymammotion/mqtt/linkkit/linkkit.py +3023 -0
- pymammotion/mqtt/mammotion_mqtt.py +176 -169
- pymammotion/mqtt/mqtt_models.py +66 -0
- pymammotion/proto/__init__.py +4839 -4
- pymammotion/proto/basestation.proto +8 -0
- pymammotion/proto/basestation_pb2.py +11 -9
- pymammotion/proto/basestation_pb2.pyi +16 -2
- pymammotion/proto/dev_net.proto +79 -55
- pymammotion/proto/dev_net_pb2.py +60 -56
- pymammotion/proto/dev_net_pb2.pyi +49 -6
- pymammotion/proto/luba_msg.proto +2 -1
- pymammotion/proto/luba_msg_pb2.py +6 -6
- pymammotion/proto/luba_msg_pb2.pyi +1 -0
- pymammotion/proto/luba_mul.proto +62 -1
- pymammotion/proto/luba_mul_pb2.py +38 -22
- pymammotion/proto/luba_mul_pb2.pyi +94 -7
- pymammotion/proto/mctrl_driver.proto +44 -4
- pymammotion/proto/mctrl_driver_pb2.py +26 -14
- pymammotion/proto/mctrl_driver_pb2.pyi +66 -11
- pymammotion/proto/mctrl_nav.proto +97 -51
- pymammotion/proto/mctrl_nav_pb2.py +75 -67
- pymammotion/proto/mctrl_nav_pb2.pyi +142 -56
- pymammotion/proto/mctrl_ota.proto +40 -2
- pymammotion/proto/mctrl_ota_pb2.py +23 -13
- pymammotion/proto/mctrl_ota_pb2.pyi +67 -4
- pymammotion/proto/mctrl_pept.proto +8 -3
- pymammotion/proto/mctrl_pept_pb2.py +8 -6
- pymammotion/proto/mctrl_pept_pb2.pyi +14 -6
- pymammotion/proto/mctrl_sys.proto +325 -86
- pymammotion/proto/mctrl_sys_pb2.py +162 -98
- pymammotion/proto/mctrl_sys_pb2.pyi +451 -25
- pymammotion/proto/message_pool.py +3 -0
- pymammotion/proto/py.typed +0 -0
- pymammotion/utility/constant/device_constant.py +65 -21
- pymammotion/utility/datatype_converter.py +13 -12
- pymammotion/utility/device_config.py +755 -0
- pymammotion/utility/device_type.py +218 -21
- pymammotion/utility/map.py +238 -51
- pymammotion/utility/mur_mur_hash.py +159 -0
- {pymammotion-0.2.62.dist-info → pymammotion-0.5.51.dist-info}/METADATA +27 -31
- pymammotion-0.5.51.dist-info/RECORD +152 -0
- {pymammotion-0.2.62.dist-info → pymammotion-0.5.51.dist-info}/WHEEL +1 -1
- pymammotion/aliyun/cloud_service.py +0 -65
- pymammotion/data/model/plan.py +0 -58
- pymammotion/data/state_manager.py +0 -130
- pymammotion/proto/basestation.py +0 -59
- pymammotion/proto/common.py +0 -12
- pymammotion/proto/dev_net.py +0 -381
- pymammotion/proto/luba_msg.py +0 -81
- pymammotion/proto/luba_mul.py +0 -76
- pymammotion/proto/mctrl_driver.py +0 -100
- pymammotion/proto/mctrl_nav.py +0 -660
- pymammotion/proto/mctrl_ota.py +0 -48
- pymammotion/proto/mctrl_pept.py +0 -41
- pymammotion/proto/mctrl_sys.py +0 -574
- pymammotion-0.2.62.dist-info/RECORD +0 -125
- /pymammotion/{http/_init_.py → bluetooth/model/__init__.py} +0 -0
- {pymammotion-0.2.62.dist-info → pymammotion-0.5.51.dist-info/licenses}/LICENSE +0 -0
|
@@ -1,25 +1,27 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
from asyncio import InvalidStateError
|
|
2
3
|
import base64
|
|
4
|
+
from collections import deque
|
|
5
|
+
from collections.abc import Awaitable, Callable
|
|
3
6
|
import json
|
|
4
7
|
import logging
|
|
5
|
-
|
|
6
|
-
from
|
|
7
|
-
from typing import Any, Awaitable, Callable, Optional, cast
|
|
8
|
+
import time
|
|
9
|
+
from typing import Any
|
|
8
10
|
|
|
9
|
-
import
|
|
11
|
+
import betterproto2
|
|
12
|
+
from Tea.exceptions import UnretryableException
|
|
10
13
|
|
|
11
|
-
from pymammotion import CloudIOTGateway, MammotionMQTT
|
|
12
|
-
from pymammotion.aliyun.cloud_gateway import DeviceOfflineException
|
|
14
|
+
from pymammotion import AliyunMQTT, CloudIOTGateway, MammotionMQTT
|
|
15
|
+
from pymammotion.aliyun.cloud_gateway import DeviceOfflineException
|
|
13
16
|
from pymammotion.aliyun.model.dev_by_account_response import Device
|
|
14
|
-
from pymammotion.data.
|
|
15
|
-
from pymammotion.data.mqtt.event import ThingEventMessage
|
|
16
|
-
from pymammotion.data.mqtt.properties import ThingPropertiesMessage
|
|
17
|
+
from pymammotion.data.mower_state_manager import MowerStateManager
|
|
18
|
+
from pymammotion.data.mqtt.event import MammotionEventMessage, ThingEventMessage
|
|
19
|
+
from pymammotion.data.mqtt.properties import MammotionPropertiesMessage, ThingPropertiesMessage
|
|
20
|
+
from pymammotion.data.mqtt.status import ThingStatusMessage
|
|
17
21
|
from pymammotion.event.event import DataEvent
|
|
18
22
|
from pymammotion.mammotion.commands.mammotion_command import MammotionCommand
|
|
19
23
|
from pymammotion.mammotion.devices.base import MammotionBaseDevice
|
|
20
|
-
from pymammotion.
|
|
21
|
-
from pymammotion.proto import has_field
|
|
22
|
-
from pymammotion.proto.luba_msg import LubaMsg
|
|
24
|
+
from pymammotion.proto import LubaMsg
|
|
23
25
|
|
|
24
26
|
_LOGGER = logging.getLogger(__name__)
|
|
25
27
|
|
|
@@ -27,16 +29,21 @@ _LOGGER = logging.getLogger(__name__)
|
|
|
27
29
|
class MammotionCloud:
|
|
28
30
|
"""Per account MQTT cloud."""
|
|
29
31
|
|
|
30
|
-
def __init__(self, mqtt_client: MammotionMQTT, cloud_client: CloudIOTGateway) -> None:
|
|
32
|
+
def __init__(self, mqtt_client: AliyunMQTT | MammotionMQTT, cloud_client: CloudIOTGateway) -> None:
|
|
33
|
+
"""Initialize MammotionCloud."""
|
|
31
34
|
self.cloud_client = cloud_client
|
|
35
|
+
self.command_sent_time = 0
|
|
32
36
|
self.loop = asyncio.get_event_loop()
|
|
33
37
|
self.is_ready = False
|
|
34
38
|
self.command_queue = asyncio.Queue()
|
|
35
39
|
self._waiting_queue = deque()
|
|
36
40
|
self.mqtt_message_event = DataEvent()
|
|
37
41
|
self.mqtt_properties_event = DataEvent()
|
|
42
|
+
self.mqtt_status_event = DataEvent()
|
|
43
|
+
self.mqtt_device_event = DataEvent()
|
|
38
44
|
self.on_ready_event = DataEvent()
|
|
39
45
|
self.on_disconnected_event = DataEvent()
|
|
46
|
+
self.on_connected_event = DataEvent()
|
|
40
47
|
self._operation_lock = asyncio.Lock()
|
|
41
48
|
self._mqtt_client = mqtt_client
|
|
42
49
|
self._mqtt_client.on_connected = self.on_connected
|
|
@@ -44,10 +51,8 @@ class MammotionCloud:
|
|
|
44
51
|
self._mqtt_client.on_message = self._on_mqtt_message
|
|
45
52
|
self._mqtt_client.on_ready = self.on_ready
|
|
46
53
|
|
|
47
|
-
# temporary for testing only
|
|
48
|
-
# self._start_sync_task = self.loop.call_later(30, lambda: asyncio.ensure_future(self.start_sync(0)))
|
|
49
|
-
|
|
50
54
|
async def on_ready(self) -> None:
|
|
55
|
+
"""Starts processing the queue and emits the ready event."""
|
|
51
56
|
loop = asyncio.get_event_loop()
|
|
52
57
|
loop.create_task(self.process_queue())
|
|
53
58
|
await self.on_ready_event.data_event(None)
|
|
@@ -56,16 +61,19 @@ class MammotionCloud:
|
|
|
56
61
|
return self._mqtt_client.is_connected
|
|
57
62
|
|
|
58
63
|
def disconnect(self) -> None:
|
|
59
|
-
|
|
64
|
+
"""Disconnect the MQTT client."""
|
|
65
|
+
if self.is_connected:
|
|
66
|
+
self._mqtt_client.disconnect()
|
|
60
67
|
|
|
61
68
|
def connect_async(self) -> None:
|
|
62
69
|
self._mqtt_client.connect_async()
|
|
63
70
|
|
|
64
|
-
def send_command(self, iot_id: str, command: bytes) -> None:
|
|
65
|
-
self._mqtt_client.
|
|
71
|
+
async def send_command(self, iot_id: str, command: bytes) -> None:
|
|
72
|
+
await self._mqtt_client.send_cloud_command(iot_id, command)
|
|
66
73
|
|
|
67
74
|
async def on_connected(self) -> None:
|
|
68
75
|
"""Callback for when MQTT connects."""
|
|
76
|
+
await self.on_connected_event.data_event(None)
|
|
69
77
|
|
|
70
78
|
async def on_disconnected(self) -> None:
|
|
71
79
|
"""Callback for when MQTT disconnects."""
|
|
@@ -82,41 +90,42 @@ class MammotionCloud:
|
|
|
82
90
|
future.set_result(result)
|
|
83
91
|
except Exception as ex:
|
|
84
92
|
# Set the exception on the future if something goes wrong
|
|
85
|
-
|
|
93
|
+
try:
|
|
94
|
+
future.set_exception(ex)
|
|
95
|
+
except InvalidStateError:
|
|
96
|
+
"""Dead end, log an error."""
|
|
97
|
+
_LOGGER.exception("InvalidStateError while trying to bubble up exception")
|
|
86
98
|
finally:
|
|
87
99
|
# Mark the task as done
|
|
88
100
|
self.command_queue.task_done()
|
|
89
101
|
|
|
90
|
-
async def _execute_command_locked(self, iot_id: str, key: str, command: bytes) ->
|
|
102
|
+
async def _execute_command_locked(self, iot_id: str, key: str, command: bytes) -> None:
|
|
91
103
|
"""Execute command and read response."""
|
|
92
104
|
assert self._mqtt_client is not None
|
|
93
105
|
self._key = key
|
|
94
106
|
_LOGGER.debug("Sending command: %s", key)
|
|
107
|
+
self.command_sent_time = time.time()
|
|
108
|
+
await self._mqtt_client.send_cloud_command(iot_id, command)
|
|
95
109
|
|
|
96
|
-
|
|
97
|
-
future = MammotionFuture(iot_id)
|
|
98
|
-
self._waiting_queue.append(future)
|
|
99
|
-
timeout = 5
|
|
100
|
-
try:
|
|
101
|
-
notify_msg = await future.async_get(timeout)
|
|
102
|
-
except asyncio.TimeoutError:
|
|
103
|
-
notify_msg = b""
|
|
104
|
-
|
|
105
|
-
_LOGGER.debug("%s: Message received", iot_id)
|
|
106
|
-
|
|
107
|
-
return notify_msg
|
|
108
|
-
|
|
109
|
-
async def _on_mqtt_message(self, topic: str, payload: str, iot_id: str) -> None:
|
|
110
|
+
async def _on_mqtt_message(self, topic: str, payload: bytes, iot_id: str) -> None:
|
|
110
111
|
"""Handle incoming MQTT messages."""
|
|
111
|
-
_LOGGER.debug("MQTT message received on topic %s: %s, iot_id: %s", topic, payload, iot_id)
|
|
112
|
+
# _LOGGER.debug("MQTT message received on topic %s: %s, iot_id: %s", topic, payload, iot_id)
|
|
113
|
+
json_str = payload.decode("utf-8")
|
|
114
|
+
dict_payload = json.loads(json_str)
|
|
115
|
+
await self._parse_mqtt_response(topic, dict_payload, iot_id)
|
|
116
|
+
|
|
117
|
+
async def _parse_mqtt_response(self, topic: str, payload: dict, iot_id: str) -> None:
|
|
118
|
+
"""Parse and handle MQTT responses based on the topic.
|
|
112
119
|
|
|
113
|
-
|
|
114
|
-
|
|
120
|
+
This function processes different types of MQTT messages received from various
|
|
121
|
+
topics. It logs debug information and calls appropriate callback methods for
|
|
122
|
+
each event type.
|
|
115
123
|
|
|
116
|
-
|
|
124
|
+
Args:
|
|
125
|
+
topic (str): The MQTT topic from which the message was received.
|
|
126
|
+
payload (dict): The payload data of the MQTT message.
|
|
117
127
|
|
|
118
|
-
|
|
119
|
-
"""Parse the MQTT response."""
|
|
128
|
+
"""
|
|
120
129
|
if topic.endswith("/app/down/thing/events"):
|
|
121
130
|
_LOGGER.debug("Thing event received")
|
|
122
131
|
event = ThingEventMessage.from_dicts(payload)
|
|
@@ -128,99 +137,120 @@ class MammotionCloud:
|
|
|
128
137
|
_LOGGER.debug("Protobuf event")
|
|
129
138
|
# Call the callbacks for each cloudDevice
|
|
130
139
|
await self.mqtt_message_event.data_event(event)
|
|
140
|
+
if event.method == "thing.events":
|
|
141
|
+
await self.mqtt_device_event.data_event(event)
|
|
131
142
|
if event.method == "thing.properties":
|
|
132
143
|
await self.mqtt_properties_event.data_event(event)
|
|
133
144
|
_LOGGER.debug(event)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
145
|
+
elif topic.endswith("/app/down/thing/status"):
|
|
146
|
+
status = ThingStatusMessage.from_dict(payload)
|
|
147
|
+
await self.mqtt_status_event.data_event(status)
|
|
148
|
+
elif topic.endswith("app/down/thing/properties"):
|
|
149
|
+
property_event = ThingPropertiesMessage.from_dict(payload)
|
|
150
|
+
await self.mqtt_properties_event.data_event(property_event)
|
|
151
|
+
|
|
152
|
+
if topic.endswith("/thing/event/device_protobuf_msg_event/post"):
|
|
153
|
+
_LOGGER.debug("Mammotion Thing event received")
|
|
154
|
+
mammotion_event = MammotionEventMessage.from_dict(payload)
|
|
155
|
+
mammotion_event.params.iot_id = iot_id
|
|
156
|
+
await self.mqtt_message_event.data_event(mammotion_event)
|
|
157
|
+
elif topic.endswith("/thing/event/property/post"):
|
|
158
|
+
_LOGGER.debug("Mammotion Property event received")
|
|
159
|
+
mammotion_property_event = MammotionPropertiesMessage.from_dict(payload)
|
|
160
|
+
mammotion_property_event.params.iot_id = iot_id
|
|
161
|
+
await self.mqtt_properties_event.data_event(mammotion_property_event)
|
|
138
162
|
|
|
139
163
|
def _disconnect(self) -> None:
|
|
140
164
|
"""Disconnect the MQTT client."""
|
|
141
165
|
self._mqtt_client.disconnect()
|
|
142
166
|
|
|
143
167
|
@property
|
|
144
|
-
def waiting_queue(self):
|
|
168
|
+
def waiting_queue(self) -> deque:
|
|
145
169
|
return self._waiting_queue
|
|
146
170
|
|
|
147
171
|
|
|
148
172
|
class MammotionBaseCloudDevice(MammotionBaseDevice):
|
|
149
173
|
"""Base class for Mammotion Cloud devices."""
|
|
150
174
|
|
|
151
|
-
def __init__(self, mqtt: MammotionCloud, cloud_device: Device,
|
|
175
|
+
def __init__(self, mqtt: MammotionCloud, cloud_device: Device, state_manager: MowerStateManager) -> None:
|
|
152
176
|
"""Initialize MammotionBaseCloudDevice."""
|
|
153
|
-
super().__init__(
|
|
154
|
-
self._ble_sync_task: TimerHandle | None = None
|
|
177
|
+
super().__init__(state_manager, cloud_device)
|
|
155
178
|
self.stopped = False
|
|
156
|
-
self.on_ready_callback:
|
|
179
|
+
self.on_ready_callback: Callable[[], Awaitable[None]] | None = None
|
|
157
180
|
self.loop = asyncio.get_event_loop()
|
|
158
181
|
self._mqtt = mqtt
|
|
159
|
-
self.iot_id = cloud_device.
|
|
182
|
+
self.iot_id = cloud_device.iot_id
|
|
160
183
|
self.device = cloud_device
|
|
161
184
|
self._command_futures = {}
|
|
162
|
-
self._commands: MammotionCommand = MammotionCommand(
|
|
185
|
+
self._commands: MammotionCommand = MammotionCommand(
|
|
186
|
+
cloud_device.device_name,
|
|
187
|
+
int(mqtt.cloud_client.mammotion_http.response.data.userInformation.userAccount),
|
|
188
|
+
)
|
|
163
189
|
self.currentID = ""
|
|
164
190
|
self._mqtt.mqtt_message_event.add_subscribers(self._parse_message_for_device)
|
|
165
191
|
self._mqtt.mqtt_properties_event.add_subscribers(self._parse_message_properties_for_device)
|
|
192
|
+
self._mqtt.mqtt_status_event.add_subscribers(self._parse_message_status_for_device)
|
|
193
|
+
self._mqtt.mqtt_device_event.add_subscribers(self._parse_device_event_for_device)
|
|
166
194
|
self._mqtt.on_ready_event.add_subscribers(self.on_ready)
|
|
167
195
|
self._mqtt.on_disconnected_event.add_subscribers(self.on_disconnect)
|
|
196
|
+
self._mqtt.on_connected_event.add_subscribers(self.on_connect)
|
|
168
197
|
self.set_queue_callback(self.queue_command)
|
|
169
198
|
|
|
170
|
-
|
|
171
|
-
|
|
199
|
+
def __del__(self) -> None:
|
|
200
|
+
"""Cleanup subscriptions."""
|
|
201
|
+
self._mqtt.on_ready_event.remove_subscribers(self.on_ready)
|
|
202
|
+
self._mqtt.on_disconnected_event.remove_subscribers(self.on_disconnect)
|
|
203
|
+
self._mqtt.on_connected_event.remove_subscribers(self.on_connect)
|
|
204
|
+
self._mqtt.mqtt_message_event.remove_subscribers(self._parse_message_for_device)
|
|
205
|
+
self._mqtt.mqtt_properties_event.remove_subscribers(self._parse_message_properties_for_device)
|
|
206
|
+
self._mqtt.mqtt_status_event.remove_subscribers(self._parse_message_status_for_device)
|
|
207
|
+
self._mqtt.mqtt_device_event.remove_subscribers(self._parse_device_event_for_device)
|
|
208
|
+
self._state_manager.cloud_queue_command_callback.remove_subscribers(self.queue_command)
|
|
209
|
+
|
|
210
|
+
@property
|
|
211
|
+
def command_sent_time(self) -> float:
|
|
212
|
+
return self._mqtt.command_sent_time
|
|
213
|
+
|
|
214
|
+
def set_notification_callback(self, func: Callable[[tuple[str, Any | None]], Awaitable[None]]) -> None:
|
|
215
|
+
self._state_manager.cloud_on_notification_callback.add_subscribers(func)
|
|
216
|
+
|
|
217
|
+
def set_queue_callback(self, func: Callable[[str, dict[str, Any]], Awaitable[None]]) -> None:
|
|
218
|
+
self._state_manager.cloud_queue_command_callback.add_subscribers(func)
|
|
172
219
|
|
|
173
220
|
async def on_ready(self) -> None:
|
|
174
221
|
"""Callback for when MQTT is subscribed to events."""
|
|
175
222
|
if self.stopped:
|
|
176
223
|
return
|
|
177
224
|
try:
|
|
178
|
-
await self._ble_sync()
|
|
179
|
-
if self._ble_sync_task is None or self._ble_sync_task.cancelled():
|
|
180
|
-
await self.run_periodic_sync_task()
|
|
181
225
|
if self.on_ready_callback:
|
|
182
226
|
await self.on_ready_callback()
|
|
183
|
-
except DeviceOfflineException:
|
|
184
|
-
|
|
185
|
-
except SetupException:
|
|
186
|
-
await self.stop()
|
|
227
|
+
except (DeviceOfflineException, UnretryableException):
|
|
228
|
+
_LOGGER.debug("Device is offline")
|
|
187
229
|
|
|
188
230
|
async def on_disconnect(self) -> None:
|
|
189
|
-
if self._ble_sync_task:
|
|
190
|
-
self._ble_sync_task.cancel()
|
|
191
|
-
loop = asyncio.get_event_loop()
|
|
192
231
|
self._mqtt.disconnect()
|
|
193
|
-
|
|
232
|
+
|
|
233
|
+
async def on_connect(self) -> None:
|
|
234
|
+
"""On connect callback"""
|
|
194
235
|
|
|
195
236
|
async def stop(self) -> None:
|
|
196
237
|
"""Stop all tasks and disconnect."""
|
|
197
|
-
|
|
198
|
-
self._ble_sync_task.cancel()
|
|
199
|
-
self._mqtt.on_ready_event.remove_subscribers(self.on_ready)
|
|
238
|
+
# self._mqtt._mqtt_client.unsubscribe()
|
|
200
239
|
self.stopped = True
|
|
201
240
|
|
|
241
|
+
async def start(self) -> None:
|
|
242
|
+
"""Start the device connection."""
|
|
243
|
+
self.stopped = False
|
|
244
|
+
if not self.mqtt.is_connected():
|
|
245
|
+
loop = asyncio.get_running_loop()
|
|
246
|
+
await loop.run_in_executor(None, self.mqtt.connect_async)
|
|
247
|
+
# else:
|
|
248
|
+
# self.mqtt._mqtt_client.thing_on_thing_enable(None)
|
|
249
|
+
|
|
202
250
|
async def _ble_sync(self) -> None:
|
|
203
|
-
|
|
204
|
-
loop = asyncio.get_running_loop()
|
|
205
|
-
await loop.run_in_executor(None, self._mqtt.send_command, self.iot_id, command_bytes)
|
|
251
|
+
pass
|
|
206
252
|
|
|
207
|
-
async def
|
|
208
|
-
"""Send ble sync to robot."""
|
|
209
|
-
try:
|
|
210
|
-
if not self._mqtt._operation_lock.locked() or not self.stopped:
|
|
211
|
-
await self._ble_sync()
|
|
212
|
-
finally:
|
|
213
|
-
if not self.stopped:
|
|
214
|
-
self.schedule_ble_sync()
|
|
215
|
-
|
|
216
|
-
def schedule_ble_sync(self) -> None:
|
|
217
|
-
"""Periodically sync to keep connection alive."""
|
|
218
|
-
if self._mqtt is not None and self._mqtt.is_connected:
|
|
219
|
-
self._ble_sync_task = self.loop.call_later(
|
|
220
|
-
160, lambda: asyncio.ensure_future(self.run_periodic_sync_task())
|
|
221
|
-
)
|
|
222
|
-
|
|
223
|
-
async def queue_command(self, key: str, **kwargs: Any) -> bytes:
|
|
253
|
+
async def queue_command(self, key: str, **kwargs: Any) -> None:
|
|
224
254
|
# Create a future to hold the result
|
|
225
255
|
_LOGGER.debug("Queueing command: %s", key)
|
|
226
256
|
future = asyncio.Future()
|
|
@@ -228,7 +258,13 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
|
|
|
228
258
|
command_bytes = getattr(self._commands, key)(**kwargs)
|
|
229
259
|
await self._mqtt.command_queue.put((self.iot_id, key, command_bytes, future))
|
|
230
260
|
# Wait for the future to be resolved
|
|
231
|
-
|
|
261
|
+
try:
|
|
262
|
+
await future
|
|
263
|
+
return
|
|
264
|
+
except asyncio.CancelledError:
|
|
265
|
+
"""Try again once."""
|
|
266
|
+
future = asyncio.Future()
|
|
267
|
+
await self._mqtt.command_queue.put((self.iot_id, key, command_bytes, future))
|
|
232
268
|
|
|
233
269
|
def _extract_message_id(self, payload: dict) -> str:
|
|
234
270
|
"""Extract the message ID from the payload."""
|
|
@@ -252,40 +288,56 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
|
|
|
252
288
|
return None
|
|
253
289
|
|
|
254
290
|
async def _parse_message_properties_for_device(self, event: ThingPropertiesMessage) -> None:
|
|
255
|
-
if event.params.
|
|
291
|
+
if event.params.iot_id != self.iot_id:
|
|
256
292
|
return
|
|
257
|
-
self.state_manager.properties(event)
|
|
293
|
+
await self.state_manager.properties(event)
|
|
294
|
+
|
|
295
|
+
async def _parse_message_status_for_device(self, status: ThingStatusMessage) -> None:
|
|
296
|
+
if status.params.iot_id != self.iot_id:
|
|
297
|
+
return
|
|
298
|
+
await self.state_manager.status(status)
|
|
299
|
+
|
|
300
|
+
async def _parse_device_event_for_device(self, status: ThingStatusMessage) -> None:
|
|
301
|
+
"""Process device event if it matches the device's IoT ID."""
|
|
302
|
+
if status.params.iot_id != self.iot_id:
|
|
303
|
+
return
|
|
304
|
+
await self.state_manager.device_event(status)
|
|
258
305
|
|
|
259
306
|
async def _parse_message_for_device(self, event: ThingEventMessage) -> None:
|
|
307
|
+
"""Parses a message received from a device and updates internal state.
|
|
308
|
+
|
|
309
|
+
This function processes an incoming `ThingEventMessage`, checks if the message
|
|
310
|
+
is intended for this device, decodes the binary data, and updates raw data. It
|
|
311
|
+
then attempts to parse the binary data into a `LubaMsg`. If parsing fails, it
|
|
312
|
+
logs the exception. The function also handles setting the device product key if
|
|
313
|
+
not already set and processes specific sub-messages based on their types.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
event (ThingEventMessage): The event message received from the device.
|
|
317
|
+
|
|
318
|
+
"""
|
|
260
319
|
params = event.params
|
|
261
320
|
new_msg = LubaMsg()
|
|
262
|
-
if event.params.
|
|
321
|
+
if event.params.iot_id != self.iot_id:
|
|
263
322
|
return
|
|
264
323
|
binary_data = base64.b64decode(params.value.content)
|
|
265
324
|
try:
|
|
266
325
|
self._update_raw_data(binary_data)
|
|
267
326
|
new_msg = LubaMsg().parse(binary_data)
|
|
268
|
-
except (KeyError, ValueError, IndexError):
|
|
327
|
+
except (KeyError, ValueError, IndexError, UnicodeDecodeError):
|
|
269
328
|
_LOGGER.exception("Error parsing message %s", binary_data)
|
|
270
329
|
|
|
271
330
|
if (
|
|
272
331
|
self._commands.get_device_product_key() == ""
|
|
273
|
-
and self._commands.get_device_name() == event.params.
|
|
332
|
+
and self._commands.get_device_name() == event.params.device_name
|
|
274
333
|
):
|
|
275
|
-
self._commands.set_device_product_key(event.params.
|
|
334
|
+
self._commands.set_device_product_key(event.params.product_key)
|
|
276
335
|
|
|
277
|
-
|
|
278
|
-
|
|
336
|
+
res = betterproto2.which_one_of(new_msg, "LubaSubMsg")
|
|
337
|
+
if res[0] == "net":
|
|
338
|
+
if new_msg.net.todev_ble_sync != 0 or new_msg.net.toapp_wifi_iot_status is not None:
|
|
279
339
|
return
|
|
280
340
|
|
|
281
|
-
if len(self._mqtt.waiting_queue) > 0:
|
|
282
|
-
fut: MammotionFuture = self.dequeue_by_iot_id(self._mqtt.waiting_queue, self.iot_id)
|
|
283
|
-
if fut is None:
|
|
284
|
-
return
|
|
285
|
-
while fut.fut.cancelled() and len(self._mqtt.waiting_queue) > 0:
|
|
286
|
-
fut: MammotionFuture = self.dequeue_by_iot_id(self._mqtt.waiting_queue, self.iot_id)
|
|
287
|
-
if not fut.fut.cancelled():
|
|
288
|
-
fut.resolve(cast(bytes, binary_data))
|
|
289
341
|
await self._state_manager.notification(new_msg)
|
|
290
342
|
|
|
291
343
|
@property
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Mower device with Bluetooth LE connectivity."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from bleak import BLEDevice
|
|
7
|
+
|
|
8
|
+
from pymammotion.aliyun.model.dev_by_account_response import Device
|
|
9
|
+
from pymammotion.data.mower_state_manager import MowerStateManager
|
|
10
|
+
from pymammotion.mammotion.devices.mammotion_bluetooth import MammotionBaseBLEDevice
|
|
11
|
+
from pymammotion.mammotion.devices.mower_device import MammotionMowerDevice
|
|
12
|
+
|
|
13
|
+
_LOGGER = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MammotionMowerBLEDevice(MammotionBaseBLEDevice, MammotionMowerDevice):
|
|
17
|
+
"""Mower device with BLE connectivity and map synchronization."""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
state_manager: MowerStateManager,
|
|
22
|
+
cloud_device: Device,
|
|
23
|
+
device: BLEDevice,
|
|
24
|
+
interface: int = 0,
|
|
25
|
+
**kwargs: Any,
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Initialize MammotionMowerBLEDevice.
|
|
28
|
+
|
|
29
|
+
Uses multiple inheritance to combine:
|
|
30
|
+
- MammotionBaseBLEDevice: BLE communication
|
|
31
|
+
- MammotionMowerDevice: Map sync callbacks
|
|
32
|
+
"""
|
|
33
|
+
# Initialize base BLE device (which also initializes MammotionBaseDevice)
|
|
34
|
+
MammotionBaseBLEDevice.__init__(self, state_manager, cloud_device, device, interface, **kwargs)
|
|
35
|
+
# Set up mower-specific BLE callbacks
|
|
36
|
+
self._state_manager.ble_gethash_ack_callback = self.datahash_response
|
|
37
|
+
self._state_manager.ble_get_commondata_ack_callback = self.commdata_response
|
|
38
|
+
self._state_manager.ble_get_plan_callback = self.plan_callback
|
|
39
|
+
|
|
40
|
+
def __del__(self) -> None:
|
|
41
|
+
"""Cleanup subscriptions and callbacks."""
|
|
42
|
+
# Clean up mower-specific callbacks
|
|
43
|
+
if hasattr(self, "_state_manager"):
|
|
44
|
+
self._state_manager.ble_gethash_ack_callback = None
|
|
45
|
+
self._state_manager.ble_get_commondata_ack_callback = None
|
|
46
|
+
self._state_manager.ble_get_plan_callback = None
|
|
47
|
+
# Call parent cleanup
|
|
48
|
+
if MammotionBaseBLEDevice.__del__:
|
|
49
|
+
super().__del__()
|
|
@@ -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."""
|