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.

Files changed (135) hide show
  1. pymammotion/__init__.py +9 -6
  2. pymammotion/aliyun/client.py +235 -0
  3. pymammotion/aliyun/cloud_gateway.py +320 -69
  4. pymammotion/aliyun/model/aep_response.py +1 -2
  5. pymammotion/aliyun/model/dev_by_account_response.py +170 -23
  6. pymammotion/aliyun/model/login_by_oauth_response.py +2 -3
  7. pymammotion/aliyun/model/regions_response.py +3 -3
  8. pymammotion/aliyun/model/session_by_authcode_response.py +2 -2
  9. pymammotion/aliyun/model/thing_response.py +12 -0
  10. pymammotion/aliyun/regions.py +62 -0
  11. pymammotion/aliyun/tea/core.py +297 -0
  12. pymammotion/bluetooth/ble.py +11 -15
  13. pymammotion/bluetooth/ble_message.py +389 -106
  14. pymammotion/bluetooth/model/atomic_integer.py +54 -0
  15. pymammotion/const.py +3 -0
  16. pymammotion/data/model/__init__.py +1 -2
  17. pymammotion/data/model/device.py +92 -240
  18. pymammotion/data/model/device_config.py +10 -24
  19. pymammotion/data/model/device_info.py +35 -0
  20. pymammotion/data/model/device_limits.py +49 -0
  21. pymammotion/data/model/enums.py +12 -2
  22. pymammotion/data/model/errors.py +12 -0
  23. pymammotion/data/model/events.py +14 -0
  24. pymammotion/data/model/generate_geojson.py +521 -0
  25. pymammotion/data/model/generate_route_information.py +3 -4
  26. pymammotion/data/model/hash_list.py +384 -48
  27. pymammotion/data/model/location.py +4 -4
  28. pymammotion/data/model/mowing_modes.py +24 -1
  29. pymammotion/data/model/raw_data.py +215 -0
  30. pymammotion/data/model/region_data.py +10 -11
  31. pymammotion/data/model/report_info.py +62 -6
  32. pymammotion/data/model/work.py +27 -0
  33. pymammotion/data/mower_state_manager.py +316 -0
  34. pymammotion/data/mqtt/event.py +73 -28
  35. pymammotion/data/mqtt/mammotion_properties.py +257 -0
  36. pymammotion/data/mqtt/properties.py +93 -78
  37. pymammotion/data/mqtt/status.py +18 -17
  38. pymammotion/event/event.py +32 -8
  39. pymammotion/homeassistant/__init__.py +3 -0
  40. pymammotion/homeassistant/mower_api.py +484 -0
  41. pymammotion/homeassistant/rtk_api.py +54 -0
  42. pymammotion/http/__init__.py +0 -0
  43. pymammotion/http/encryption.py +220 -0
  44. pymammotion/http/http.py +652 -44
  45. pymammotion/http/model/__init__.py +0 -0
  46. pymammotion/{aliyun/model/stream_subscription_response.py → http/model/camera_stream.py} +14 -2
  47. pymammotion/http/model/http.py +160 -9
  48. pymammotion/http/model/response_factory.py +61 -0
  49. pymammotion/http/model/rtk.py +16 -0
  50. pymammotion/mammotion/commands/abstract_message.py +7 -5
  51. pymammotion/mammotion/commands/mammotion_command.py +32 -3
  52. pymammotion/mammotion/commands/messages/basestation.py +43 -0
  53. pymammotion/mammotion/commands/messages/driver.py +61 -29
  54. pymammotion/mammotion/commands/messages/media.py +68 -15
  55. pymammotion/mammotion/commands/messages/navigation.py +61 -25
  56. pymammotion/mammotion/commands/messages/network.py +93 -100
  57. pymammotion/mammotion/commands/messages/ota.py +18 -18
  58. pymammotion/mammotion/commands/messages/system.py +97 -72
  59. pymammotion/mammotion/commands/messages/video.py +17 -12
  60. pymammotion/mammotion/devices/__init__.py +27 -3
  61. pymammotion/mammotion/devices/base.py +50 -127
  62. pymammotion/mammotion/devices/mammotion.py +447 -212
  63. pymammotion/mammotion/devices/mammotion_bluetooth.py +105 -60
  64. pymammotion/mammotion/devices/mammotion_cloud.py +157 -105
  65. pymammotion/mammotion/devices/mammotion_mower_ble.py +49 -0
  66. pymammotion/mammotion/devices/mammotion_mower_cloud.py +39 -0
  67. pymammotion/mammotion/devices/managers/managers.py +81 -0
  68. pymammotion/mammotion/devices/mower_device.py +124 -0
  69. pymammotion/mammotion/devices/mower_manager.py +107 -0
  70. pymammotion/mammotion/devices/rtk_ble.py +89 -0
  71. pymammotion/mammotion/devices/rtk_cloud.py +113 -0
  72. pymammotion/mammotion/devices/rtk_device.py +50 -0
  73. pymammotion/mammotion/devices/rtk_manager.py +122 -0
  74. pymammotion/mqtt/__init__.py +2 -1
  75. pymammotion/mqtt/aliyun_mqtt.py +232 -0
  76. pymammotion/mqtt/linkkit/__init__.py +5 -0
  77. pymammotion/mqtt/linkkit/h2client.py +585 -0
  78. pymammotion/mqtt/linkkit/linkkit.py +3023 -0
  79. pymammotion/mqtt/mammotion_mqtt.py +176 -169
  80. pymammotion/mqtt/mqtt_models.py +66 -0
  81. pymammotion/proto/__init__.py +4839 -4
  82. pymammotion/proto/basestation.proto +8 -0
  83. pymammotion/proto/basestation_pb2.py +11 -9
  84. pymammotion/proto/basestation_pb2.pyi +16 -2
  85. pymammotion/proto/dev_net.proto +79 -55
  86. pymammotion/proto/dev_net_pb2.py +60 -56
  87. pymammotion/proto/dev_net_pb2.pyi +49 -6
  88. pymammotion/proto/luba_msg.proto +2 -1
  89. pymammotion/proto/luba_msg_pb2.py +6 -6
  90. pymammotion/proto/luba_msg_pb2.pyi +1 -0
  91. pymammotion/proto/luba_mul.proto +62 -1
  92. pymammotion/proto/luba_mul_pb2.py +38 -22
  93. pymammotion/proto/luba_mul_pb2.pyi +94 -7
  94. pymammotion/proto/mctrl_driver.proto +44 -4
  95. pymammotion/proto/mctrl_driver_pb2.py +26 -14
  96. pymammotion/proto/mctrl_driver_pb2.pyi +66 -11
  97. pymammotion/proto/mctrl_nav.proto +97 -51
  98. pymammotion/proto/mctrl_nav_pb2.py +75 -67
  99. pymammotion/proto/mctrl_nav_pb2.pyi +142 -56
  100. pymammotion/proto/mctrl_ota.proto +40 -2
  101. pymammotion/proto/mctrl_ota_pb2.py +23 -13
  102. pymammotion/proto/mctrl_ota_pb2.pyi +67 -4
  103. pymammotion/proto/mctrl_pept.proto +8 -3
  104. pymammotion/proto/mctrl_pept_pb2.py +8 -6
  105. pymammotion/proto/mctrl_pept_pb2.pyi +14 -6
  106. pymammotion/proto/mctrl_sys.proto +325 -86
  107. pymammotion/proto/mctrl_sys_pb2.py +162 -98
  108. pymammotion/proto/mctrl_sys_pb2.pyi +451 -25
  109. pymammotion/proto/message_pool.py +3 -0
  110. pymammotion/proto/py.typed +0 -0
  111. pymammotion/utility/constant/device_constant.py +65 -21
  112. pymammotion/utility/datatype_converter.py +13 -12
  113. pymammotion/utility/device_config.py +755 -0
  114. pymammotion/utility/device_type.py +218 -21
  115. pymammotion/utility/map.py +238 -51
  116. pymammotion/utility/mur_mur_hash.py +159 -0
  117. {pymammotion-0.2.62.dist-info → pymammotion-0.5.51.dist-info}/METADATA +27 -31
  118. pymammotion-0.5.51.dist-info/RECORD +152 -0
  119. {pymammotion-0.2.62.dist-info → pymammotion-0.5.51.dist-info}/WHEEL +1 -1
  120. pymammotion/aliyun/cloud_service.py +0 -65
  121. pymammotion/data/model/plan.py +0 -58
  122. pymammotion/data/state_manager.py +0 -130
  123. pymammotion/proto/basestation.py +0 -59
  124. pymammotion/proto/common.py +0 -12
  125. pymammotion/proto/dev_net.py +0 -381
  126. pymammotion/proto/luba_msg.py +0 -81
  127. pymammotion/proto/luba_mul.py +0 -76
  128. pymammotion/proto/mctrl_driver.py +0 -100
  129. pymammotion/proto/mctrl_nav.py +0 -660
  130. pymammotion/proto/mctrl_ota.py +0 -48
  131. pymammotion/proto/mctrl_pept.py +0 -41
  132. pymammotion/proto/mctrl_sys.py +0 -574
  133. pymammotion-0.2.62.dist-info/RECORD +0 -125
  134. /pymammotion/{http/_init_.py → bluetooth/model/__init__.py} +0 -0
  135. {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
- from asyncio import TimerHandle
6
- from collections import deque
7
- from typing import Any, Awaitable, Callable, Optional, cast
8
+ import time
9
+ from typing import Any
8
10
 
9
- import betterproto
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, SetupException
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.model.device import MowingDevice
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.mqtt.mammotion_future import MammotionFuture
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
- self._mqtt_client.disconnect()
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.get_cloud_client().send_cloud_command(iot_id, command)
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
- future.set_exception(ex)
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) -> 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
- await self.loop.run_in_executor(None, self._mqtt_client.get_cloud_client().send_cloud_command, iot_id, command)
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
- json_str = json.dumps(payload)
114
- payload = json.loads(json_str)
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
- await self._handle_mqtt_message(topic, payload)
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
- async def _parse_mqtt_response(self, topic: str, payload: dict) -> None:
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
- async def _handle_mqtt_message(self, topic: str, payload: dict) -> None:
136
- """Async handler for incoming MQTT messages."""
137
- await self._parse_mqtt_response(topic=topic, payload=payload)
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, mowing_state: MowingDevice) -> None:
175
+ def __init__(self, mqtt: MammotionCloud, cloud_device: Device, state_manager: MowerStateManager) -> None:
152
176
  """Initialize MammotionBaseCloudDevice."""
153
- super().__init__(mowing_state, cloud_device)
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: Optional[Callable[[], Awaitable[None]]] = None
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.iotId
182
+ self.iot_id = cloud_device.iot_id
160
183
  self.device = cloud_device
161
184
  self._command_futures = {}
162
- self._commands: MammotionCommand = MammotionCommand(cloud_device.deviceName)
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
- if self._mqtt.is_ready:
171
- self.run_periodic_sync_task()
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
- await self.stop()
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
- await loop.run_in_executor(None, self._mqtt.cloud_client.sign_out)
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
- if self._ble_sync_task:
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
- command_bytes = self._commands.send_todev_ble_sync(3)
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 run_periodic_sync_task(self) -> None:
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
- return await future
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.iotId != self.iot_id:
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.iotId != self.iot_id:
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.deviceName
332
+ and self._commands.get_device_name() == event.params.device_name
274
333
  ):
275
- self._commands.set_device_product_key(event.params.productKey)
334
+ self._commands.set_device_product_key(event.params.product_key)
276
335
 
277
- if betterproto.serialized_on_wire(new_msg.net):
278
- if new_msg.net.todev_ble_sync != 0 or has_field(new_msg.net.toapp_wifi_iot_status):
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."""