pymammotion 0.5.69__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.
- pymammotion/__init__.py +53 -0
- pymammotion/agora/__init__.py +0 -0
- pymammotion/agora/agora_api.py +755 -0
- pymammotion/agora/agora_rtc_capabilities.py +748 -0
- pymammotion/agora/agora_websockets.py +1175 -0
- pymammotion/aliyun/__init__.py +1 -0
- pymammotion/aliyun/client.py +235 -0
- pymammotion/aliyun/cloud_gateway.py +982 -0
- pymammotion/aliyun/model/aep_response.py +21 -0
- pymammotion/aliyun/model/connect_response.py +51 -0
- pymammotion/aliyun/model/dev_by_account_response.py +195 -0
- pymammotion/aliyun/model/login_by_oauth_response.py +64 -0
- pymammotion/aliyun/model/regions_response.py +29 -0
- pymammotion/aliyun/model/session_by_authcode_response.py +19 -0
- pymammotion/aliyun/model/thing_response.py +12 -0
- pymammotion/aliyun/regions.py +62 -0
- pymammotion/aliyun/tea/core.py +297 -0
- pymammotion/aliyun/tmp_constant.py +171 -0
- pymammotion/bluetooth/__init__.py +1 -0
- pymammotion/bluetooth/ble.py +62 -0
- pymammotion/bluetooth/ble_message.py +676 -0
- pymammotion/bluetooth/const.py +27 -0
- pymammotion/bluetooth/data/__init__.py +0 -0
- pymammotion/bluetooth/data/convert.py +25 -0
- pymammotion/bluetooth/data/framectrldata.py +40 -0
- pymammotion/bluetooth/data/notifydata.py +62 -0
- pymammotion/bluetooth/model/__init__.py +0 -0
- pymammotion/bluetooth/model/atomic_integer.py +54 -0
- pymammotion/const.py +13 -0
- pymammotion/data/__init__.py +0 -0
- pymammotion/data/model/__init__.py +8 -0
- pymammotion/data/model/account.py +8 -0
- pymammotion/data/model/device.py +192 -0
- pymammotion/data/model/device_config.py +72 -0
- pymammotion/data/model/device_info.py +60 -0
- pymammotion/data/model/device_limits.py +49 -0
- pymammotion/data/model/enums.py +77 -0
- pymammotion/data/model/errors.py +12 -0
- pymammotion/data/model/events.py +14 -0
- pymammotion/data/model/generate_geojson.py +565 -0
- pymammotion/data/model/generate_route_information.py +26 -0
- pymammotion/data/model/hash_list.py +475 -0
- pymammotion/data/model/location.py +36 -0
- pymammotion/data/model/mowing_modes.py +77 -0
- pymammotion/data/model/rapid_state.py +45 -0
- pymammotion/data/model/raw_data.py +215 -0
- pymammotion/data/model/region_data.py +102 -0
- pymammotion/data/model/report_info.py +182 -0
- pymammotion/data/model/work.py +27 -0
- pymammotion/data/mower_state_manager.py +369 -0
- pymammotion/data/mqtt/__init__.py +1 -0
- pymammotion/data/mqtt/event.py +227 -0
- pymammotion/data/mqtt/mammotion_properties.py +276 -0
- pymammotion/data/mqtt/properties.py +203 -0
- pymammotion/data/mqtt/status.py +57 -0
- pymammotion/event/__init__.py +6 -0
- pymammotion/event/event.py +96 -0
- pymammotion/homeassistant/__init__.py +3 -0
- pymammotion/homeassistant/mower_api.py +514 -0
- pymammotion/homeassistant/rtk_api.py +54 -0
- pymammotion/http/__init__.py +0 -0
- pymammotion/http/encryption.py +220 -0
- pymammotion/http/http.py +673 -0
- pymammotion/http/model/__init__.py +0 -0
- pymammotion/http/model/camera_stream.py +31 -0
- pymammotion/http/model/http.py +249 -0
- pymammotion/http/model/response_factory.py +61 -0
- pymammotion/http/model/rtk.py +16 -0
- pymammotion/mammotion/__init__.py +0 -0
- pymammotion/mammotion/commands/__init__.py +0 -0
- pymammotion/mammotion/commands/abstract_message.py +24 -0
- pymammotion/mammotion/commands/mammotion_command.py +81 -0
- pymammotion/mammotion/commands/messages/__init__.py +0 -0
- pymammotion/mammotion/commands/messages/basestation.py +43 -0
- pymammotion/mammotion/commands/messages/driver.py +122 -0
- pymammotion/mammotion/commands/messages/media.py +87 -0
- pymammotion/mammotion/commands/messages/navigation.py +564 -0
- pymammotion/mammotion/commands/messages/network.py +205 -0
- pymammotion/mammotion/commands/messages/ota.py +38 -0
- pymammotion/mammotion/commands/messages/system.py +330 -0
- pymammotion/mammotion/commands/messages/video.py +33 -0
- pymammotion/mammotion/control/__init__.py +0 -0
- pymammotion/mammotion/control/joystick.py +145 -0
- pymammotion/mammotion/devices/__init__.py +29 -0
- pymammotion/mammotion/devices/base.py +163 -0
- pymammotion/mammotion/devices/mammotion.py +571 -0
- pymammotion/mammotion/devices/mammotion_bluetooth.py +496 -0
- pymammotion/mammotion/devices/mammotion_cloud.py +355 -0
- pymammotion/mammotion/devices/mammotion_mower_ble.py +48 -0
- pymammotion/mammotion/devices/mammotion_mower_cloud.py +39 -0
- pymammotion/mammotion/devices/managers/managers.py +81 -0
- pymammotion/mammotion/devices/mower_device.py +120 -0
- pymammotion/mammotion/devices/mower_manager.py +107 -0
- pymammotion/mammotion/devices/rtk_ble.py +89 -0
- pymammotion/mammotion/devices/rtk_cloud.py +115 -0
- pymammotion/mammotion/devices/rtk_device.py +50 -0
- pymammotion/mammotion/devices/rtk_manager.py +125 -0
- pymammotion/mqtt/__init__.py +6 -0
- pymammotion/mqtt/aliyun_mqtt.py +237 -0
- pymammotion/mqtt/linkkit/__init__.py +5 -0
- pymammotion/mqtt/linkkit/h2client.py +585 -0
- pymammotion/mqtt/linkkit/linkkit.py +3025 -0
- pymammotion/mqtt/mammotion_future.py +26 -0
- pymammotion/mqtt/mammotion_mqtt.py +214 -0
- pymammotion/mqtt/mqtt_models.py +66 -0
- pymammotion/proto/__init__.py +4841 -0
- pymammotion/proto/basestation.proto +51 -0
- pymammotion/proto/basestation_pb2.py +35 -0
- pymammotion/proto/basestation_pb2.pyi +89 -0
- pymammotion/proto/common.proto +7 -0
- pymammotion/proto/common_pb2.py +25 -0
- pymammotion/proto/common_pb2.pyi +13 -0
- pymammotion/proto/dev_net.proto +321 -0
- pymammotion/proto/dev_net_pb2.py +111 -0
- pymammotion/proto/dev_net_pb2.pyi +515 -0
- pymammotion/proto/luba_msg.proto +76 -0
- pymammotion/proto/luba_msg_pb2.py +41 -0
- pymammotion/proto/luba_msg_pb2.pyi +97 -0
- pymammotion/proto/luba_mul.proto +129 -0
- pymammotion/proto/luba_mul_pb2.py +61 -0
- pymammotion/proto/luba_mul_pb2.pyi +178 -0
- pymammotion/proto/mctrl_driver.proto +107 -0
- pymammotion/proto/mctrl_driver_pb2.py +57 -0
- pymammotion/proto/mctrl_driver_pb2.pyi +167 -0
- pymammotion/proto/mctrl_nav.proto +591 -0
- pymammotion/proto/mctrl_nav_pb2.py +136 -0
- pymammotion/proto/mctrl_nav_pb2.pyi +1067 -0
- pymammotion/proto/mctrl_ota.proto +80 -0
- pymammotion/proto/mctrl_ota_pb2.py +45 -0
- pymammotion/proto/mctrl_ota_pb2.pyi +128 -0
- pymammotion/proto/mctrl_pept.proto +34 -0
- pymammotion/proto/mctrl_pept_pb2.py +33 -0
- pymammotion/proto/mctrl_pept_pb2.pyi +58 -0
- pymammotion/proto/mctrl_sys.proto +741 -0
- pymammotion/proto/mctrl_sys_pb2.py +206 -0
- pymammotion/proto/mctrl_sys_pb2.pyi +1213 -0
- pymammotion/proto/message_pool.py +3 -0
- pymammotion/proto/py.typed +0 -0
- pymammotion/py.typed +0 -0
- pymammotion/utility/constant/__init__.py +3 -0
- pymammotion/utility/constant/device_constant.py +315 -0
- pymammotion/utility/conversions.py +5 -0
- pymammotion/utility/datatype_converter.py +124 -0
- pymammotion/utility/device_config.py +755 -0
- pymammotion/utility/device_type.py +489 -0
- pymammotion/utility/map.py +259 -0
- pymammotion/utility/movement.py +18 -0
- pymammotion/utility/mur_mur_hash.py +159 -0
- pymammotion/utility/periodic.py +106 -0
- pymammotion/utility/rocker_util.py +194 -0
- pymammotion-0.5.69.dist-info/METADATA +93 -0
- pymammotion-0.5.69.dist-info/RECORD +154 -0
- pymammotion-0.5.69.dist-info/WHEEL +4 -0
- pymammotion-0.5.69.dist-info/licenses/LICENSE +674 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from asyncio import InvalidStateError
|
|
3
|
+
import base64
|
|
4
|
+
from collections import deque
|
|
5
|
+
from collections.abc import Awaitable, Callable
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import time
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import betterproto2
|
|
12
|
+
from Tea.exceptions import UnretryableException
|
|
13
|
+
|
|
14
|
+
from pymammotion import AliyunMQTT, CloudIOTGateway, MammotionMQTT
|
|
15
|
+
from pymammotion.aliyun.cloud_gateway import DeviceOfflineException
|
|
16
|
+
from pymammotion.aliyun.model.dev_by_account_response import Device
|
|
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
|
|
21
|
+
from pymammotion.event.event import DataEvent
|
|
22
|
+
from pymammotion.mammotion.commands.mammotion_command import MammotionCommand
|
|
23
|
+
from pymammotion.mammotion.devices.base import MammotionBaseDevice
|
|
24
|
+
from pymammotion.proto import LubaMsg
|
|
25
|
+
|
|
26
|
+
_LOGGER = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class MammotionCloud:
|
|
30
|
+
"""Per account MQTT cloud."""
|
|
31
|
+
|
|
32
|
+
def __init__(self, mqtt_client: AliyunMQTT | MammotionMQTT, cloud_client: CloudIOTGateway) -> None:
|
|
33
|
+
"""Initialize MammotionCloud."""
|
|
34
|
+
self.cloud_client = cloud_client
|
|
35
|
+
self.command_sent_time = 0
|
|
36
|
+
self.loop = asyncio.get_event_loop()
|
|
37
|
+
self.is_ready = False
|
|
38
|
+
self.command_queue = asyncio.Queue()
|
|
39
|
+
self._waiting_queue = deque()
|
|
40
|
+
self.mqtt_message_event = DataEvent()
|
|
41
|
+
self.mqtt_properties_event = DataEvent()
|
|
42
|
+
self.mqtt_status_event = DataEvent()
|
|
43
|
+
self.mqtt_device_event = DataEvent()
|
|
44
|
+
self.on_ready_event = DataEvent()
|
|
45
|
+
self.on_disconnected_event = DataEvent()
|
|
46
|
+
self.on_connected_event = DataEvent()
|
|
47
|
+
self._operation_lock = asyncio.Lock()
|
|
48
|
+
self._mqtt_client = mqtt_client
|
|
49
|
+
self._mqtt_client.on_connected = self.on_connected
|
|
50
|
+
self._mqtt_client.on_disconnected = self.on_disconnected
|
|
51
|
+
self._mqtt_client.on_message = self._on_mqtt_message
|
|
52
|
+
self._mqtt_client.on_ready = self.on_ready
|
|
53
|
+
|
|
54
|
+
async def on_ready(self) -> None:
|
|
55
|
+
"""Starts processing the queue and emits the ready event."""
|
|
56
|
+
loop = asyncio.get_event_loop()
|
|
57
|
+
loop.create_task(self.process_queue())
|
|
58
|
+
await self.on_ready_event.data_event(None)
|
|
59
|
+
|
|
60
|
+
def is_connected(self) -> bool:
|
|
61
|
+
return self._mqtt_client.is_connected
|
|
62
|
+
|
|
63
|
+
def disconnect(self) -> None:
|
|
64
|
+
"""Disconnect the MQTT client."""
|
|
65
|
+
if self.is_connected:
|
|
66
|
+
self._mqtt_client.disconnect()
|
|
67
|
+
|
|
68
|
+
def connect_async(self) -> None:
|
|
69
|
+
self._mqtt_client.connect_async()
|
|
70
|
+
|
|
71
|
+
async def send_command(self, iot_id: str, command: bytes) -> None:
|
|
72
|
+
await self._mqtt_client.send_cloud_command(iot_id, command)
|
|
73
|
+
|
|
74
|
+
async def on_connected(self) -> None:
|
|
75
|
+
"""Callback for when MQTT connects."""
|
|
76
|
+
await self.on_connected_event.data_event(None)
|
|
77
|
+
|
|
78
|
+
async def on_disconnected(self) -> None:
|
|
79
|
+
"""Callback for when MQTT disconnects."""
|
|
80
|
+
await self.on_disconnected_event.data_event(None)
|
|
81
|
+
|
|
82
|
+
async def process_queue(self) -> None:
|
|
83
|
+
while True:
|
|
84
|
+
# Get the next item from the queue
|
|
85
|
+
iot_id, key, command, future = await self.command_queue.get()
|
|
86
|
+
try:
|
|
87
|
+
# Process the command using _execute_command_locked
|
|
88
|
+
result = await self._execute_command_locked(iot_id, key, command)
|
|
89
|
+
# Set the result on the future
|
|
90
|
+
future.set_result(result)
|
|
91
|
+
except Exception as ex:
|
|
92
|
+
# Set the exception on the future if something goes wrong
|
|
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")
|
|
98
|
+
finally:
|
|
99
|
+
# Mark the task as done
|
|
100
|
+
self.command_queue.task_done()
|
|
101
|
+
|
|
102
|
+
async def _execute_command_locked(self, iot_id: str, key: str, command: bytes) -> None:
|
|
103
|
+
"""Execute command and read response."""
|
|
104
|
+
assert self._mqtt_client is not None
|
|
105
|
+
self._key = key
|
|
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)
|
|
109
|
+
|
|
110
|
+
async def _on_mqtt_message(self, topic: str, payload: bytes, iot_id: str) -> None:
|
|
111
|
+
"""Handle incoming MQTT messages."""
|
|
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.
|
|
119
|
+
|
|
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.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
topic (str): The MQTT topic from which the message was received.
|
|
126
|
+
payload (dict): The payload data of the MQTT message.
|
|
127
|
+
|
|
128
|
+
"""
|
|
129
|
+
if topic.endswith("/app/down/account/bind_reply"):
|
|
130
|
+
code = payload.get("code", 0)
|
|
131
|
+
if code != 200:
|
|
132
|
+
_LOGGER.error("Failed to bind account: %s", payload)
|
|
133
|
+
# TODO send message to re login to aliyun mqtt
|
|
134
|
+
self._disconnect_error = payload
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
if topic.endswith("/app/down/thing/events"):
|
|
138
|
+
_LOGGER.debug("Thing event received")
|
|
139
|
+
event = ThingEventMessage.from_dicts(payload)
|
|
140
|
+
params = event.params
|
|
141
|
+
if isinstance(params, dict) or params.identifier is None:
|
|
142
|
+
_LOGGER.debug("Received dict params: %s", params)
|
|
143
|
+
return
|
|
144
|
+
if params.identifier == "device_protobuf_msg_event" and event.method == "thing.events":
|
|
145
|
+
_LOGGER.debug("Protobuf event")
|
|
146
|
+
# Call the callbacks for each cloudDevice
|
|
147
|
+
await self.mqtt_message_event.data_event(event)
|
|
148
|
+
if event.method == "thing.events":
|
|
149
|
+
await self.mqtt_device_event.data_event(event)
|
|
150
|
+
if event.method == "thing.properties":
|
|
151
|
+
await self.mqtt_properties_event.data_event(event)
|
|
152
|
+
_LOGGER.debug(event)
|
|
153
|
+
elif topic.endswith("/app/down/thing/status"):
|
|
154
|
+
status = ThingStatusMessage.from_dict(payload)
|
|
155
|
+
await self.mqtt_status_event.data_event(status)
|
|
156
|
+
elif topic.endswith("app/down/thing/properties"):
|
|
157
|
+
property_event = ThingPropertiesMessage.from_dict(payload)
|
|
158
|
+
await self.mqtt_properties_event.data_event(property_event)
|
|
159
|
+
|
|
160
|
+
if topic.endswith("/thing/event/device_protobuf_msg_event/post"):
|
|
161
|
+
_LOGGER.debug("Mammotion Thing event received")
|
|
162
|
+
mammotion_event = MammotionEventMessage.from_dict(payload)
|
|
163
|
+
mammotion_event.params.iot_id = iot_id
|
|
164
|
+
await self.mqtt_message_event.data_event(mammotion_event)
|
|
165
|
+
elif topic.endswith("/thing/event/property/post"):
|
|
166
|
+
_LOGGER.debug("Mammotion Property event received")
|
|
167
|
+
mammotion_property_event = MammotionPropertiesMessage.from_dict(payload)
|
|
168
|
+
mammotion_property_event.params.iot_id = iot_id
|
|
169
|
+
await self.mqtt_properties_event.data_event(mammotion_property_event)
|
|
170
|
+
|
|
171
|
+
def _disconnect(self) -> None:
|
|
172
|
+
"""Disconnect the MQTT client."""
|
|
173
|
+
self._mqtt_client.disconnect()
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def waiting_queue(self) -> deque:
|
|
177
|
+
return self._waiting_queue
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class MammotionBaseCloudDevice(MammotionBaseDevice):
|
|
181
|
+
"""Base class for Mammotion Cloud devices."""
|
|
182
|
+
|
|
183
|
+
def __init__(self, mqtt: MammotionCloud, cloud_device: Device, state_manager: MowerStateManager) -> None:
|
|
184
|
+
"""Initialize MammotionBaseCloudDevice."""
|
|
185
|
+
super().__init__(state_manager, cloud_device)
|
|
186
|
+
self.stopped = False
|
|
187
|
+
self.on_ready_callback: Callable[[], Awaitable[None]] | None = None
|
|
188
|
+
self.loop = asyncio.get_event_loop()
|
|
189
|
+
self._mqtt = mqtt
|
|
190
|
+
self.iot_id = cloud_device.iot_id
|
|
191
|
+
self.device = cloud_device
|
|
192
|
+
self._command_futures = {}
|
|
193
|
+
self._commands: MammotionCommand = MammotionCommand(
|
|
194
|
+
cloud_device.device_name,
|
|
195
|
+
int(mqtt.cloud_client.mammotion_http.response.data.userInformation.userAccount),
|
|
196
|
+
)
|
|
197
|
+
self.currentID = ""
|
|
198
|
+
self._mqtt.mqtt_message_event.add_subscribers(self._parse_message_for_device)
|
|
199
|
+
self._mqtt.mqtt_properties_event.add_subscribers(self._parse_message_properties_for_device)
|
|
200
|
+
self._mqtt.mqtt_status_event.add_subscribers(self._parse_message_status_for_device)
|
|
201
|
+
self._mqtt.mqtt_device_event.add_subscribers(self._parse_device_event_for_device)
|
|
202
|
+
self._mqtt.on_ready_event.add_subscribers(self.on_ready)
|
|
203
|
+
self._mqtt.on_disconnected_event.add_subscribers(self.on_disconnect)
|
|
204
|
+
self._mqtt.on_connected_event.add_subscribers(self.on_connect)
|
|
205
|
+
self.set_queue_callback(self.queue_command)
|
|
206
|
+
|
|
207
|
+
def __del__(self) -> None:
|
|
208
|
+
"""Cleanup subscriptions."""
|
|
209
|
+
self._mqtt.on_ready_event.remove_subscribers(self.on_ready)
|
|
210
|
+
self._mqtt.on_disconnected_event.remove_subscribers(self.on_disconnect)
|
|
211
|
+
self._mqtt.on_connected_event.remove_subscribers(self.on_connect)
|
|
212
|
+
self._mqtt.mqtt_message_event.remove_subscribers(self._parse_message_for_device)
|
|
213
|
+
self._mqtt.mqtt_properties_event.remove_subscribers(self._parse_message_properties_for_device)
|
|
214
|
+
self._mqtt.mqtt_status_event.remove_subscribers(self._parse_message_status_for_device)
|
|
215
|
+
self._mqtt.mqtt_device_event.remove_subscribers(self._parse_device_event_for_device)
|
|
216
|
+
self._state_manager.cloud_queue_command_callback.remove_subscribers(self.queue_command)
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def command_sent_time(self) -> float:
|
|
220
|
+
return self._mqtt.command_sent_time
|
|
221
|
+
|
|
222
|
+
def set_notification_callback(self, func: Callable[[tuple[str, Any | None]], Awaitable[None]]) -> None:
|
|
223
|
+
self._state_manager.cloud_on_notification_callback.add_subscribers(func)
|
|
224
|
+
|
|
225
|
+
def set_queue_callback(self, func: Callable[[str, dict[str, Any]], Awaitable[None]]) -> None:
|
|
226
|
+
self._state_manager.cloud_queue_command_callback.add_subscribers(func)
|
|
227
|
+
|
|
228
|
+
async def on_ready(self) -> None:
|
|
229
|
+
"""Callback for when MQTT is subscribed to events."""
|
|
230
|
+
if self.stopped:
|
|
231
|
+
return
|
|
232
|
+
try:
|
|
233
|
+
if self.on_ready_callback:
|
|
234
|
+
await self.on_ready_callback()
|
|
235
|
+
except (DeviceOfflineException, UnretryableException):
|
|
236
|
+
_LOGGER.debug("Device is offline")
|
|
237
|
+
|
|
238
|
+
async def on_disconnect(self) -> None:
|
|
239
|
+
self._mqtt.disconnect()
|
|
240
|
+
|
|
241
|
+
async def on_connect(self) -> None:
|
|
242
|
+
"""On connect callback"""
|
|
243
|
+
|
|
244
|
+
def stop(self) -> None:
|
|
245
|
+
"""Stop all tasks and disconnect."""
|
|
246
|
+
# self._mqtt._mqtt_client.unsubscribe()
|
|
247
|
+
if self.mqtt.is_connected():
|
|
248
|
+
self._mqtt.disconnect()
|
|
249
|
+
self.stopped = True
|
|
250
|
+
|
|
251
|
+
async def start(self) -> None:
|
|
252
|
+
"""Start the device connection."""
|
|
253
|
+
self.stopped = False
|
|
254
|
+
if not self.mqtt.is_connected():
|
|
255
|
+
loop = asyncio.get_running_loop()
|
|
256
|
+
await loop.run_in_executor(None, self.mqtt.connect_async)
|
|
257
|
+
# else:
|
|
258
|
+
# self.mqtt._mqtt_client.thing_on_thing_enable(None)
|
|
259
|
+
|
|
260
|
+
async def _ble_sync(self) -> None:
|
|
261
|
+
pass
|
|
262
|
+
|
|
263
|
+
async def queue_command(self, key: str, **kwargs: Any) -> None:
|
|
264
|
+
# Create a future to hold the result
|
|
265
|
+
_LOGGER.debug("Queueing command: %s", key)
|
|
266
|
+
future = asyncio.Future()
|
|
267
|
+
# Put the command in the queue as a tuple (key, command, future)
|
|
268
|
+
command_bytes = getattr(self._commands, key)(**kwargs)
|
|
269
|
+
await self._mqtt.command_queue.put((self.iot_id, key, command_bytes, future))
|
|
270
|
+
# Wait for the future to be resolved
|
|
271
|
+
try:
|
|
272
|
+
await future
|
|
273
|
+
return
|
|
274
|
+
except asyncio.CancelledError:
|
|
275
|
+
"""Try again once."""
|
|
276
|
+
future = asyncio.Future()
|
|
277
|
+
await self._mqtt.command_queue.put((self.iot_id, key, command_bytes, future))
|
|
278
|
+
|
|
279
|
+
def _extract_message_id(self, payload: dict) -> str:
|
|
280
|
+
"""Extract the message ID from the payload."""
|
|
281
|
+
return payload.get("id", "")
|
|
282
|
+
|
|
283
|
+
def _extract_encoded_message(self, payload: dict) -> str:
|
|
284
|
+
"""Extract the encoded message from the payload."""
|
|
285
|
+
try:
|
|
286
|
+
content = payload.get("data", {}).get("data", {}).get("params", {}).get("content", "")
|
|
287
|
+
return str(content)
|
|
288
|
+
except AttributeError:
|
|
289
|
+
_LOGGER.error("Error extracting encoded message. Payload: %s", payload)
|
|
290
|
+
return ""
|
|
291
|
+
|
|
292
|
+
@staticmethod
|
|
293
|
+
def dequeue_by_iot_id(queue, iot_id):
|
|
294
|
+
for item in queue:
|
|
295
|
+
if item.iot_id == iot_id:
|
|
296
|
+
queue.remove(item)
|
|
297
|
+
return item
|
|
298
|
+
return None
|
|
299
|
+
|
|
300
|
+
async def _parse_message_properties_for_device(self, event: ThingPropertiesMessage) -> None:
|
|
301
|
+
if event.params.iot_id != self.iot_id:
|
|
302
|
+
return
|
|
303
|
+
await self.state_manager.properties(event)
|
|
304
|
+
|
|
305
|
+
async def _parse_message_status_for_device(self, status: ThingStatusMessage) -> None:
|
|
306
|
+
if status.params.iot_id != self.iot_id:
|
|
307
|
+
return
|
|
308
|
+
await self.state_manager.status(status)
|
|
309
|
+
|
|
310
|
+
async def _parse_device_event_for_device(self, status: ThingStatusMessage) -> None:
|
|
311
|
+
"""Process device event if it matches the device's IoT ID."""
|
|
312
|
+
if status.params.iot_id != self.iot_id:
|
|
313
|
+
return
|
|
314
|
+
await self.state_manager.device_event(status)
|
|
315
|
+
|
|
316
|
+
async def _parse_message_for_device(self, event: ThingEventMessage) -> None:
|
|
317
|
+
"""Parses a message received from a device and updates internal state.
|
|
318
|
+
|
|
319
|
+
This function processes an incoming `ThingEventMessage`, checks if the message
|
|
320
|
+
is intended for this device, decodes the binary data, and updates raw data. It
|
|
321
|
+
then attempts to parse the binary data into a `LubaMsg`. If parsing fails, it
|
|
322
|
+
logs the exception. The function also handles setting the device product key if
|
|
323
|
+
not already set and processes specific sub-messages based on their types.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
event (ThingEventMessage): The event message received from the device.
|
|
327
|
+
|
|
328
|
+
"""
|
|
329
|
+
params = event.params
|
|
330
|
+
new_msg = LubaMsg()
|
|
331
|
+
if event.params.iot_id != self.iot_id:
|
|
332
|
+
return
|
|
333
|
+
binary_data = base64.b64decode(params.value.content)
|
|
334
|
+
try:
|
|
335
|
+
self._update_raw_data(binary_data)
|
|
336
|
+
new_msg = LubaMsg().parse(binary_data)
|
|
337
|
+
except (KeyError, ValueError, IndexError, UnicodeDecodeError):
|
|
338
|
+
_LOGGER.exception("Error parsing message %s", binary_data)
|
|
339
|
+
|
|
340
|
+
if (
|
|
341
|
+
self._commands.get_device_product_key() == ""
|
|
342
|
+
and self._commands.get_device_name() == event.params.device_name
|
|
343
|
+
):
|
|
344
|
+
self._commands.set_device_product_key(event.params.product_key)
|
|
345
|
+
|
|
346
|
+
res = betterproto2.which_one_of(new_msg, "LubaSubMsg")
|
|
347
|
+
if res[0] == "net":
|
|
348
|
+
if new_msg.net.todev_ble_sync != 0 or new_msg.net.toapp_wifi_iot_status is not None:
|
|
349
|
+
return
|
|
350
|
+
|
|
351
|
+
await self._state_manager.notification(new_msg)
|
|
352
|
+
|
|
353
|
+
@property
|
|
354
|
+
def mqtt(self):
|
|
355
|
+
return self._mqtt
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
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
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from bleak import BLEDevice
|
|
5
|
+
|
|
6
|
+
from pymammotion import CloudIOTGateway
|
|
7
|
+
from pymammotion.aliyun.model.dev_by_account_response import Device
|
|
8
|
+
from pymammotion.data.model.device import MowingDevice, RTKDevice
|
|
9
|
+
from pymammotion.data.model.enums import ConnectionPreference
|
|
10
|
+
from pymammotion.mammotion.devices.mammotion_cloud import 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) -> Any | None:
|
|
44
|
+
"""Return BLE device interface."""
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
@abstractmethod
|
|
48
|
+
def cloud(self) -> Any | 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) -> Any:
|
|
57
|
+
"""Add BLE device."""
|
|
58
|
+
|
|
59
|
+
@abstractmethod
|
|
60
|
+
def add_cloud(self, mqtt: MammotionCloud) -> Any:
|
|
61
|
+
"""Add cloud device."""
|
|
62
|
+
|
|
63
|
+
@abstractmethod
|
|
64
|
+
def replace_cloud(self, cloud_device: Any) -> 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: Any) -> 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,120 @@
|
|
|
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
|
+
|
|
35
|
+
async def datahash_response(self, hash_ack: NavGetHashListAck) -> None:
|
|
36
|
+
"""Handle datahash responses for root level hashs."""
|
|
37
|
+
current_frame = hash_ack.current_frame
|
|
38
|
+
|
|
39
|
+
missing_frames = self.mower.map.missing_root_hash_frame(hash_ack)
|
|
40
|
+
if len(missing_frames) == 0:
|
|
41
|
+
if len(self.mower.map.missing_hashlist(hash_ack.sub_cmd)) > 0:
|
|
42
|
+
data_hash = self.mower.map.missing_hashlist(hash_ack.sub_cmd).pop(0)
|
|
43
|
+
await self.queue_command("synchronize_hash_data", hash_num=data_hash)
|
|
44
|
+
return
|
|
45
|
+
|
|
46
|
+
if current_frame != missing_frames[0] - 1:
|
|
47
|
+
current_frame = missing_frames[0] - 1
|
|
48
|
+
await self.queue_command("get_hash_response", total_frame=hash_ack.total_frame, current_frame=current_frame)
|
|
49
|
+
|
|
50
|
+
async def commdata_response(self, common_data: NavGetCommDataAck | SvgMessageAckT) -> None:
|
|
51
|
+
"""Handle common data responses."""
|
|
52
|
+
total_frame = common_data.total_frame
|
|
53
|
+
current_frame = common_data.current_frame
|
|
54
|
+
|
|
55
|
+
missing_frames = self.mower.map.missing_frame(common_data)
|
|
56
|
+
if len(missing_frames) == 0:
|
|
57
|
+
# get next in hash ack list
|
|
58
|
+
data_hash = (
|
|
59
|
+
self.mower.map.missing_hashlist(common_data.sub_cmd).pop(0)
|
|
60
|
+
if len(self.mower.map.missing_hashlist(common_data.sub_cmd)) > 0
|
|
61
|
+
else None
|
|
62
|
+
)
|
|
63
|
+
if data_hash is None:
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
await self.queue_command("synchronize_hash_data", hash_num=data_hash)
|
|
67
|
+
else:
|
|
68
|
+
if current_frame != missing_frames[0] - 1:
|
|
69
|
+
current_frame = missing_frames[0] - 1
|
|
70
|
+
|
|
71
|
+
region_data = RegionData()
|
|
72
|
+
region_data.hash = common_data.data_hash if isinstance(common_data, SvgMessageAckT) else common_data.hash
|
|
73
|
+
region_data.action = common_data.action if isinstance(common_data, NavGetCommDataAck) else 0
|
|
74
|
+
region_data.type = common_data.type
|
|
75
|
+
region_data.sub_cmd = common_data.sub_cmd
|
|
76
|
+
region_data.total_frame = total_frame
|
|
77
|
+
region_data.current_frame = current_frame
|
|
78
|
+
await self.queue_command("get_regional_data", regional_data=region_data)
|
|
79
|
+
|
|
80
|
+
async def plan_callback(self, plan: NavPlanJobSet) -> None:
|
|
81
|
+
"""Handle plan job responses."""
|
|
82
|
+
if plan.plan_index < plan.total_plan_num - 1:
|
|
83
|
+
index = plan.plan_index + 1
|
|
84
|
+
await self.queue_command("read_plan", sub_cmd=2, plan_index=index)
|
|
85
|
+
|
|
86
|
+
async def start_schedule_sync(self) -> None:
|
|
87
|
+
"""Start sync of schedule data."""
|
|
88
|
+
if len(self.mower.map.plan) == 0 or list(self.mower.map.plan.values())[0].total_plan_num != len(
|
|
89
|
+
self.mower.map.plan
|
|
90
|
+
):
|
|
91
|
+
await self.queue_command("read_plan", sub_cmd=2, plan_index=0)
|
|
92
|
+
|
|
93
|
+
async def start_map_sync(self) -> None:
|
|
94
|
+
"""Start sync of map data."""
|
|
95
|
+
if location := next((loc for loc in self.mower.report_data.locations if loc.pos_type == 5), None):
|
|
96
|
+
self.mower.map.update_hash_lists(self.mower.map.hashlist, location.bol_hash)
|
|
97
|
+
|
|
98
|
+
await self.queue_command("send_todev_ble_sync", sync_type=3)
|
|
99
|
+
|
|
100
|
+
# TODO correctly check if area names exist for a zone.
|
|
101
|
+
if self._cloud_device and len(self.mower.map.area_name) == 0 and not DeviceType.is_luba1(self.mower.name):
|
|
102
|
+
await self.queue_command("get_area_name_list", device_id=self._cloud_device.iot_id)
|
|
103
|
+
|
|
104
|
+
if len(self.mower.map.root_hash_lists) == 0 or len(self.mower.map.missing_hashlist()) > 0:
|
|
105
|
+
await self.queue_command("get_all_boundary_hash_list", sub_cmd=0)
|
|
106
|
+
|
|
107
|
+
for hash_id, frame in list(self.mower.map.area.items()):
|
|
108
|
+
missing_frames = self.mower.map.find_missing_frames(frame)
|
|
109
|
+
if len(missing_frames) > 0:
|
|
110
|
+
del self.mower.map.area[hash_id]
|
|
111
|
+
|
|
112
|
+
for hash_id, frame in list(self.mower.map.path.items()):
|
|
113
|
+
missing_frames = self.mower.map.find_missing_frames(frame)
|
|
114
|
+
if len(missing_frames) > 0:
|
|
115
|
+
del self.mower.map.path[hash_id]
|
|
116
|
+
|
|
117
|
+
for hash_id, frame in list(self.mower.map.obstacle.items()):
|
|
118
|
+
missing_frames = self.mower.map.find_missing_frames(frame)
|
|
119
|
+
if len(missing_frames) > 0:
|
|
120
|
+
del self.mower.map.obstacle[hash_id]
|