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.
Files changed (154) hide show
  1. pymammotion/__init__.py +53 -0
  2. pymammotion/agora/__init__.py +0 -0
  3. pymammotion/agora/agora_api.py +755 -0
  4. pymammotion/agora/agora_rtc_capabilities.py +748 -0
  5. pymammotion/agora/agora_websockets.py +1175 -0
  6. pymammotion/aliyun/__init__.py +1 -0
  7. pymammotion/aliyun/client.py +235 -0
  8. pymammotion/aliyun/cloud_gateway.py +982 -0
  9. pymammotion/aliyun/model/aep_response.py +21 -0
  10. pymammotion/aliyun/model/connect_response.py +51 -0
  11. pymammotion/aliyun/model/dev_by_account_response.py +195 -0
  12. pymammotion/aliyun/model/login_by_oauth_response.py +64 -0
  13. pymammotion/aliyun/model/regions_response.py +29 -0
  14. pymammotion/aliyun/model/session_by_authcode_response.py +19 -0
  15. pymammotion/aliyun/model/thing_response.py +12 -0
  16. pymammotion/aliyun/regions.py +62 -0
  17. pymammotion/aliyun/tea/core.py +297 -0
  18. pymammotion/aliyun/tmp_constant.py +171 -0
  19. pymammotion/bluetooth/__init__.py +1 -0
  20. pymammotion/bluetooth/ble.py +62 -0
  21. pymammotion/bluetooth/ble_message.py +676 -0
  22. pymammotion/bluetooth/const.py +27 -0
  23. pymammotion/bluetooth/data/__init__.py +0 -0
  24. pymammotion/bluetooth/data/convert.py +25 -0
  25. pymammotion/bluetooth/data/framectrldata.py +40 -0
  26. pymammotion/bluetooth/data/notifydata.py +62 -0
  27. pymammotion/bluetooth/model/__init__.py +0 -0
  28. pymammotion/bluetooth/model/atomic_integer.py +54 -0
  29. pymammotion/const.py +13 -0
  30. pymammotion/data/__init__.py +0 -0
  31. pymammotion/data/model/__init__.py +8 -0
  32. pymammotion/data/model/account.py +8 -0
  33. pymammotion/data/model/device.py +192 -0
  34. pymammotion/data/model/device_config.py +72 -0
  35. pymammotion/data/model/device_info.py +60 -0
  36. pymammotion/data/model/device_limits.py +49 -0
  37. pymammotion/data/model/enums.py +77 -0
  38. pymammotion/data/model/errors.py +12 -0
  39. pymammotion/data/model/events.py +14 -0
  40. pymammotion/data/model/generate_geojson.py +565 -0
  41. pymammotion/data/model/generate_route_information.py +26 -0
  42. pymammotion/data/model/hash_list.py +475 -0
  43. pymammotion/data/model/location.py +36 -0
  44. pymammotion/data/model/mowing_modes.py +77 -0
  45. pymammotion/data/model/rapid_state.py +45 -0
  46. pymammotion/data/model/raw_data.py +215 -0
  47. pymammotion/data/model/region_data.py +102 -0
  48. pymammotion/data/model/report_info.py +182 -0
  49. pymammotion/data/model/work.py +27 -0
  50. pymammotion/data/mower_state_manager.py +369 -0
  51. pymammotion/data/mqtt/__init__.py +1 -0
  52. pymammotion/data/mqtt/event.py +227 -0
  53. pymammotion/data/mqtt/mammotion_properties.py +276 -0
  54. pymammotion/data/mqtt/properties.py +203 -0
  55. pymammotion/data/mqtt/status.py +57 -0
  56. pymammotion/event/__init__.py +6 -0
  57. pymammotion/event/event.py +96 -0
  58. pymammotion/homeassistant/__init__.py +3 -0
  59. pymammotion/homeassistant/mower_api.py +514 -0
  60. pymammotion/homeassistant/rtk_api.py +54 -0
  61. pymammotion/http/__init__.py +0 -0
  62. pymammotion/http/encryption.py +220 -0
  63. pymammotion/http/http.py +673 -0
  64. pymammotion/http/model/__init__.py +0 -0
  65. pymammotion/http/model/camera_stream.py +31 -0
  66. pymammotion/http/model/http.py +249 -0
  67. pymammotion/http/model/response_factory.py +61 -0
  68. pymammotion/http/model/rtk.py +16 -0
  69. pymammotion/mammotion/__init__.py +0 -0
  70. pymammotion/mammotion/commands/__init__.py +0 -0
  71. pymammotion/mammotion/commands/abstract_message.py +24 -0
  72. pymammotion/mammotion/commands/mammotion_command.py +81 -0
  73. pymammotion/mammotion/commands/messages/__init__.py +0 -0
  74. pymammotion/mammotion/commands/messages/basestation.py +43 -0
  75. pymammotion/mammotion/commands/messages/driver.py +122 -0
  76. pymammotion/mammotion/commands/messages/media.py +87 -0
  77. pymammotion/mammotion/commands/messages/navigation.py +564 -0
  78. pymammotion/mammotion/commands/messages/network.py +205 -0
  79. pymammotion/mammotion/commands/messages/ota.py +38 -0
  80. pymammotion/mammotion/commands/messages/system.py +330 -0
  81. pymammotion/mammotion/commands/messages/video.py +33 -0
  82. pymammotion/mammotion/control/__init__.py +0 -0
  83. pymammotion/mammotion/control/joystick.py +145 -0
  84. pymammotion/mammotion/devices/__init__.py +29 -0
  85. pymammotion/mammotion/devices/base.py +163 -0
  86. pymammotion/mammotion/devices/mammotion.py +571 -0
  87. pymammotion/mammotion/devices/mammotion_bluetooth.py +496 -0
  88. pymammotion/mammotion/devices/mammotion_cloud.py +355 -0
  89. pymammotion/mammotion/devices/mammotion_mower_ble.py +48 -0
  90. pymammotion/mammotion/devices/mammotion_mower_cloud.py +39 -0
  91. pymammotion/mammotion/devices/managers/managers.py +81 -0
  92. pymammotion/mammotion/devices/mower_device.py +120 -0
  93. pymammotion/mammotion/devices/mower_manager.py +107 -0
  94. pymammotion/mammotion/devices/rtk_ble.py +89 -0
  95. pymammotion/mammotion/devices/rtk_cloud.py +115 -0
  96. pymammotion/mammotion/devices/rtk_device.py +50 -0
  97. pymammotion/mammotion/devices/rtk_manager.py +125 -0
  98. pymammotion/mqtt/__init__.py +6 -0
  99. pymammotion/mqtt/aliyun_mqtt.py +237 -0
  100. pymammotion/mqtt/linkkit/__init__.py +5 -0
  101. pymammotion/mqtt/linkkit/h2client.py +585 -0
  102. pymammotion/mqtt/linkkit/linkkit.py +3025 -0
  103. pymammotion/mqtt/mammotion_future.py +26 -0
  104. pymammotion/mqtt/mammotion_mqtt.py +214 -0
  105. pymammotion/mqtt/mqtt_models.py +66 -0
  106. pymammotion/proto/__init__.py +4841 -0
  107. pymammotion/proto/basestation.proto +51 -0
  108. pymammotion/proto/basestation_pb2.py +35 -0
  109. pymammotion/proto/basestation_pb2.pyi +89 -0
  110. pymammotion/proto/common.proto +7 -0
  111. pymammotion/proto/common_pb2.py +25 -0
  112. pymammotion/proto/common_pb2.pyi +13 -0
  113. pymammotion/proto/dev_net.proto +321 -0
  114. pymammotion/proto/dev_net_pb2.py +111 -0
  115. pymammotion/proto/dev_net_pb2.pyi +515 -0
  116. pymammotion/proto/luba_msg.proto +76 -0
  117. pymammotion/proto/luba_msg_pb2.py +41 -0
  118. pymammotion/proto/luba_msg_pb2.pyi +97 -0
  119. pymammotion/proto/luba_mul.proto +129 -0
  120. pymammotion/proto/luba_mul_pb2.py +61 -0
  121. pymammotion/proto/luba_mul_pb2.pyi +178 -0
  122. pymammotion/proto/mctrl_driver.proto +107 -0
  123. pymammotion/proto/mctrl_driver_pb2.py +57 -0
  124. pymammotion/proto/mctrl_driver_pb2.pyi +167 -0
  125. pymammotion/proto/mctrl_nav.proto +591 -0
  126. pymammotion/proto/mctrl_nav_pb2.py +136 -0
  127. pymammotion/proto/mctrl_nav_pb2.pyi +1067 -0
  128. pymammotion/proto/mctrl_ota.proto +80 -0
  129. pymammotion/proto/mctrl_ota_pb2.py +45 -0
  130. pymammotion/proto/mctrl_ota_pb2.pyi +128 -0
  131. pymammotion/proto/mctrl_pept.proto +34 -0
  132. pymammotion/proto/mctrl_pept_pb2.py +33 -0
  133. pymammotion/proto/mctrl_pept_pb2.pyi +58 -0
  134. pymammotion/proto/mctrl_sys.proto +741 -0
  135. pymammotion/proto/mctrl_sys_pb2.py +206 -0
  136. pymammotion/proto/mctrl_sys_pb2.pyi +1213 -0
  137. pymammotion/proto/message_pool.py +3 -0
  138. pymammotion/proto/py.typed +0 -0
  139. pymammotion/py.typed +0 -0
  140. pymammotion/utility/constant/__init__.py +3 -0
  141. pymammotion/utility/constant/device_constant.py +315 -0
  142. pymammotion/utility/conversions.py +5 -0
  143. pymammotion/utility/datatype_converter.py +124 -0
  144. pymammotion/utility/device_config.py +755 -0
  145. pymammotion/utility/device_type.py +489 -0
  146. pymammotion/utility/map.py +259 -0
  147. pymammotion/utility/movement.py +18 -0
  148. pymammotion/utility/mur_mur_hash.py +159 -0
  149. pymammotion/utility/periodic.py +106 -0
  150. pymammotion/utility/rocker_util.py +194 -0
  151. pymammotion-0.5.69.dist-info/METADATA +93 -0
  152. pymammotion-0.5.69.dist-info/RECORD +154 -0
  153. pymammotion-0.5.69.dist-info/WHEEL +4 -0
  154. pymammotion-0.5.69.dist-info/licenses/LICENSE +674 -0
@@ -0,0 +1,571 @@
1
+ """Device control of mammotion robots over bluetooth or MQTT."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ from aiohttp import ClientSession
10
+ from bleak import BLEDevice
11
+
12
+ from pymammotion import MammotionMQTT
13
+ from pymammotion.aliyun.cloud_gateway import CloudIOTGateway
14
+ from pymammotion.aliyun.model.dev_by_account_response import Device
15
+ from pymammotion.data.model.device import MowingDevice
16
+ from pymammotion.data.model.enums import ConnectionPreference
17
+ from pymammotion.http.http import MammotionHTTP
18
+ from pymammotion.http.model.camera_stream import StreamSubscriptionResponse, VideoResourceResponse
19
+ from pymammotion.http.model.http import DeviceRecord, Response
20
+ from pymammotion.mammotion.devices.mammotion_cloud import MammotionCloud
21
+ from pymammotion.mammotion.devices.mammotion_mower_ble import MammotionMowerBLEDevice
22
+ from pymammotion.mammotion.devices.managers.managers import AbstractDeviceManager
23
+ from pymammotion.mammotion.devices.mower_manager import MammotionMowerDeviceManager
24
+ from pymammotion.mammotion.devices.rtk_manager import MammotionRTKDeviceManager
25
+ from pymammotion.mqtt import AliyunMQTT
26
+ from pymammotion.utility.device_type import DeviceType
27
+
28
+ # RTK imports - imported here for type hints, full import in add_cloud_devices
29
+ if TYPE_CHECKING:
30
+ from pymammotion.mammotion.devices.rtk_ble import MammotionRTKBLEDevice
31
+
32
+ TIMEOUT_CLOUD_RESPONSE = 10
33
+
34
+ _LOGGER = logging.getLogger(__name__)
35
+
36
+
37
+ class MammotionDeviceManager:
38
+ """Manage devices - both mowers and RTK."""
39
+
40
+ def __init__(self) -> None:
41
+ self.devices: dict[str, MammotionMowerDeviceManager] = {}
42
+ self.rtk_devices: dict[str, MammotionRTKDeviceManager] = {}
43
+
44
+ async def stop(self) -> None:
45
+ for mower in self.devices.values():
46
+ if cloud := mower.cloud:
47
+ cloud.stop()
48
+ if ble := mower.ble:
49
+ await ble.stop()
50
+
51
+ def _should_disconnect_mqtt(self, device_for_removal: AbstractDeviceManager) -> bool:
52
+ """Check if MQTT connection should be disconnected.
53
+
54
+ Returns True if no other devices share the same MQTT connection.
55
+ """
56
+ if not device_for_removal.cloud:
57
+ return False
58
+
59
+ mqtt_to_check = device_for_removal.cloud.mqtt
60
+
61
+ # Check if any mower device shares this MQTT connection
62
+ shared_devices: set[AbstractDeviceManager] = {
63
+ device
64
+ for device in self.devices.values()
65
+ if device.cloud is not None and device.cloud.mqtt == mqtt_to_check
66
+ }
67
+
68
+ # Also check RTK devices for shared MQTT
69
+ shared_devices.update(
70
+ {
71
+ device
72
+ for device in self.rtk_devices.values()
73
+ if device.cloud is not None and device.cloud.mqtt == mqtt_to_check
74
+ }
75
+ )
76
+
77
+ return len(shared_devices) == 0
78
+
79
+ def add_device(self, mammotion_device: MammotionMowerDeviceManager) -> None:
80
+ """Add a mower device."""
81
+ exists: MammotionMowerDeviceManager | None = self.devices.get(mammotion_device.name)
82
+ if exists is None:
83
+ self.devices[mammotion_device.name] = mammotion_device
84
+ return
85
+ if mammotion_device.cloud is not None:
86
+ exists.replace_cloud(mammotion_device.cloud)
87
+ if mammotion_device.ble:
88
+ exists.replace_ble(mammotion_device.ble)
89
+
90
+ def add_rtk_device(self, rtk_device: MammotionRTKDeviceManager) -> None:
91
+ """Add an RTK device."""
92
+
93
+ exists: MammotionRTKDeviceManager | None = self.rtk_devices.get(rtk_device.name)
94
+ if exists is None:
95
+ self.rtk_devices[rtk_device.name] = rtk_device
96
+ return
97
+ if rtk_device.cloud:
98
+ exists.replace_cloud(rtk_device.cloud)
99
+ if rtk_device.ble:
100
+ exists.replace_ble(rtk_device.ble)
101
+
102
+ def has_device(self, mammotion_device_name: str) -> bool:
103
+ """Check if a mower device exists."""
104
+ if self.devices.get(mammotion_device_name, None) is not None:
105
+ return True
106
+ return False
107
+
108
+ def has_rtk_device(self, rtk_device_name: str) -> bool:
109
+ """Check if an RTK device exists."""
110
+ if self.rtk_devices.get(rtk_device_name, None) is not None:
111
+ return True
112
+ return False
113
+
114
+ def get_device(self, mammotion_device_name: str) -> MammotionMowerDeviceManager:
115
+ """Get a mower device."""
116
+ return self.devices[mammotion_device_name]
117
+
118
+ def get_device_by_iot_id(self, iot_id: str) -> MammotionMowerDeviceManager | None:
119
+ """Get a mower device by IoT ID."""
120
+ for device in self.devices.values():
121
+ if device.iot_id == iot_id:
122
+ return device
123
+ return None
124
+
125
+ def get_rtk_device(self, rtk_device_name: str) -> MammotionRTKDeviceManager:
126
+ """Get an RTK device."""
127
+ return self.rtk_devices[rtk_device_name]
128
+
129
+ async def remove_device(self, name: str) -> None:
130
+ """Remove a mower device."""
131
+ if self.devices.get(name):
132
+ device_for_removal = self.devices.pop(name)
133
+ loop = asyncio.get_running_loop()
134
+
135
+ if device_for_removal.cloud:
136
+ if self._should_disconnect_mqtt(device_for_removal):
137
+ await loop.run_in_executor(None, device_for_removal.cloud.mqtt.disconnect)
138
+ device_for_removal.cloud.stop()
139
+
140
+ if device_for_removal.ble:
141
+ await device_for_removal.ble.stop()
142
+
143
+ del device_for_removal
144
+
145
+ async def remove_rtk_device(self, name: str) -> None:
146
+ """Remove an RTK device."""
147
+ if self.rtk_devices.get(name):
148
+ device_for_removal = self.rtk_devices.pop(name)
149
+ loop = asyncio.get_running_loop()
150
+
151
+ if device_for_removal.cloud:
152
+ if self._should_disconnect_mqtt(device_for_removal):
153
+ await loop.run_in_executor(None, device_for_removal.cloud.mqtt.disconnect)
154
+ device_for_removal.cloud.stop()
155
+
156
+ if device_for_removal.ble:
157
+ await device_for_removal.ble.stop()
158
+
159
+ del device_for_removal
160
+
161
+
162
+ class Mammotion:
163
+ """Represents a Mammotion account and its devices."""
164
+
165
+ device_manager = MammotionDeviceManager()
166
+
167
+ _instance: Mammotion | None = None
168
+
169
+ def __new__(cls) -> Mammotion:
170
+ """Create a singleton."""
171
+ if not cls._instance:
172
+ cls._instance = super().__new__(cls)
173
+ return cls._instance
174
+
175
+ def __init__(self, session: ClientSession | None = None) -> None:
176
+ """Initialize MammotionDevice."""
177
+ self._login_lock = asyncio.Lock()
178
+ self._session: ClientSession | None = session
179
+ self.mqtt_list: dict[str, MammotionCloud] = {}
180
+
181
+ async def login_and_initiate_cloud(self, account: str, password: str, force: bool = False) -> None:
182
+ async with self._login_lock:
183
+ exists_aliyun: MammotionCloud | None = self.mqtt_list.get(f"{account}_aliyun")
184
+ exists_mammotion: MammotionCloud | None = self.mqtt_list.get(f"{account}_mammotion")
185
+ if (not exists_aliyun and not exists_mammotion) or force:
186
+ cloud_client = await self.login(account, password)
187
+ await self.initiate_cloud_connection(account, cloud_client)
188
+
189
+ async def refresh_login(self, account: str) -> None:
190
+ """Refresh login."""
191
+ async with self._login_lock:
192
+ exists_aliyun: MammotionCloud | None = self.mqtt_list.get(f"{account}_aliyun")
193
+ exists_mammotion: MammotionCloud | None = self.mqtt_list.get(f"{account}_mammotion")
194
+
195
+ if not exists_aliyun and not exists_mammotion:
196
+ return
197
+ mammotion_http = (
198
+ exists_aliyun.cloud_client.mammotion_http
199
+ if exists_aliyun
200
+ else exists_mammotion.cloud_client.mammotion_http
201
+ )
202
+
203
+ await mammotion_http.refresh_login()
204
+
205
+ if len(mammotion_http.device_records.records) != 0:
206
+ await mammotion_http.get_mqtt_credentials()
207
+
208
+ if exists_aliyun and exists_aliyun.is_connected():
209
+ exists_aliyun.disconnect()
210
+ await self.connect_iot(exists_aliyun.cloud_client)
211
+
212
+ if exists_mammotion and exists_mammotion.is_connected():
213
+ exists_mammotion.disconnect()
214
+
215
+ if exists_aliyun and not exists_aliyun.is_connected():
216
+ loop = asyncio.get_running_loop()
217
+ await loop.run_in_executor(None, exists_aliyun.connect_async)
218
+ if exists_mammotion and not exists_mammotion.is_connected():
219
+ loop = asyncio.get_running_loop()
220
+ await loop.run_in_executor(None, exists_mammotion.connect_async)
221
+
222
+ @staticmethod
223
+ def shim_cloud_devices(devices: list[DeviceRecord]) -> list[Device]:
224
+ device_list: list[Device] = []
225
+ for device in devices:
226
+ device_list.append(
227
+ Device(
228
+ gmt_modified=0,
229
+ product_name="",
230
+ status=0,
231
+ net_type="NET_WIFI",
232
+ is_edge_gateway=False,
233
+ category_name="",
234
+ owned=1,
235
+ identity_alias="UNKNOW",
236
+ thing_type="DEVICE",
237
+ identity_id=device.identity_id,
238
+ device_name=device.device_name,
239
+ product_key=device.product_key,
240
+ iot_id=device.iot_id,
241
+ bind_time=device.bind_time,
242
+ node_type="DEVICE",
243
+ category_key="LawnMower",
244
+ )
245
+ )
246
+
247
+ return device_list
248
+
249
+ async def initiate_ble_connection(self, devices: dict[str, BLEDevice], cloud_devices: list[Device]) -> None:
250
+ """Initiate BLE connection."""
251
+ for device in cloud_devices:
252
+ if ble_device := devices.get(device.device_name):
253
+ if device.device_name.startswith(("Luba-", "Yuka-")):
254
+ if not self.device_manager.has_device(device.device_name):
255
+ self.device_manager.add_device(
256
+ MammotionMowerDeviceManager(
257
+ name=device.device_name,
258
+ iot_id=device.iot_id,
259
+ cloud_device=device,
260
+ ble_device=ble_device,
261
+ preference=ConnectionPreference.BLUETOOTH,
262
+ cloud_client=CloudIOTGateway(MammotionHTTP(session=self._session)),
263
+ )
264
+ )
265
+ else:
266
+ self.device_manager.get_device(device.device_name).add_ble(ble_device)
267
+ if device.device_name.startswith(("RTK", "RBS")):
268
+ if not self.device_manager.has_rtk_device(device.device_name):
269
+ self.device_manager.add_rtk_device(
270
+ MammotionRTKDeviceManager(
271
+ name=device.device_name,
272
+ iot_id=device.iot_id,
273
+ cloud_device=device,
274
+ ble_device=ble_device,
275
+ preference=ConnectionPreference.BLUETOOTH,
276
+ cloud_client=CloudIOTGateway(MammotionHTTP(session=self._session)),
277
+ )
278
+ )
279
+ else:
280
+ self.device_manager.get_rtk_device(device.device_name).add_ble(ble_device)
281
+
282
+ async def initiate_cloud_connection(self, account: str, cloud_client: CloudIOTGateway) -> None:
283
+ """Initiate cloud connection."""
284
+ loop = asyncio.get_running_loop()
285
+
286
+ mammotion_http = cloud_client.mammotion_http
287
+
288
+ if mqtt := self.mqtt_list.get(f"{account}_aliyun"):
289
+ if mqtt.is_connected():
290
+ await loop.run_in_executor(None, mqtt.disconnect)
291
+
292
+ if mqtt := self.mqtt_list.get(f"{account}_mammotion"):
293
+ if mqtt.is_connected():
294
+ await loop.run_in_executor(None, mqtt.disconnect)
295
+
296
+ if len(cloud_client.devices_by_account_response.data.data) != 0:
297
+ mammotion_cloud = MammotionCloud(
298
+ AliyunMQTT(
299
+ region_id=cloud_client.region_response.data.regionId,
300
+ product_key=cloud_client.aep_response.data.productKey,
301
+ device_name=cloud_client.aep_response.data.deviceName,
302
+ device_secret=cloud_client.aep_response.data.deviceSecret,
303
+ iot_token=cloud_client.session_by_authcode_response.data.iotToken,
304
+ client_id=cloud_client.client_id,
305
+ cloud_client=cloud_client,
306
+ ),
307
+ cloud_client,
308
+ )
309
+ self.mqtt_list[f"{account}_aliyun"] = mammotion_cloud
310
+ self.add_cloud_devices(mammotion_cloud)
311
+
312
+ await loop.run_in_executor(None, self.mqtt_list[f"{account}_aliyun"].connect_async)
313
+ if len(mammotion_http.device_records.records) != 0:
314
+ mammotion_cloud = MammotionCloud(
315
+ MammotionMQTT(
316
+ records=mammotion_http.device_records.records,
317
+ mammotion_http=mammotion_http,
318
+ mqtt_connection=mammotion_http.mqtt_credentials,
319
+ ),
320
+ cloud_client,
321
+ )
322
+ self.mqtt_list[f"{account}_mammotion"] = mammotion_cloud
323
+ self.add_mammotion_devices(mammotion_cloud, mammotion_http.device_records.records)
324
+
325
+ await loop.run_in_executor(None, self.mqtt_list[f"{account}_mammotion"].connect_async)
326
+
327
+ def add_mammotion_devices(self, mqtt_client: MammotionCloud, devices: list[DeviceRecord]) -> None:
328
+ """Add devices from mammotion cloud."""
329
+ for device in devices:
330
+ if device.device_name.startswith(("Luba-", "Yuka-")):
331
+ has_device = self.device_manager.has_device(device.device_name)
332
+ if has_device:
333
+ mower_device = self.device_manager.get_device(device.device_name)
334
+ if mower_device.cloud is None:
335
+ mower_device.add_cloud(mqtt=mqtt_client)
336
+ else:
337
+ mower_device.replace_mqtt(mqtt_client)
338
+
339
+ else:
340
+ cloud_device_shim = Device(
341
+ gmt_modified=0,
342
+ product_name="",
343
+ status=0,
344
+ net_type="NET_WIFI",
345
+ is_edge_gateway=False,
346
+ category_name="",
347
+ owned=1,
348
+ identity_alias="UNKNOW",
349
+ thing_type="DEVICE",
350
+ identity_id=device.identity_id,
351
+ device_name=device.device_name,
352
+ product_key=device.product_key,
353
+ iot_id=device.iot_id,
354
+ bind_time=device.bind_time,
355
+ node_type="DEVICE",
356
+ category_key="LawnMower",
357
+ )
358
+
359
+ mixed_device = MammotionMowerDeviceManager(
360
+ name=device.device_name,
361
+ iot_id=device.iot_id,
362
+ cloud_client=mqtt_client.cloud_client,
363
+ cloud_device=cloud_device_shim,
364
+ mqtt=mqtt_client,
365
+ preference=ConnectionPreference.WIFI,
366
+ )
367
+ mixed_device.state.mower_state.product_key = device.product_key
368
+ self.device_manager.add_device(mixed_device)
369
+
370
+ def add_cloud_devices(self, mqtt_client: MammotionCloud) -> None:
371
+ """Add devices from cloud - both mowers and RTK."""
372
+ from pymammotion.mammotion.devices.rtk_manager import MammotionRTKDeviceManager
373
+
374
+ for device in mqtt_client.cloud_client.devices_by_account_response.data.data:
375
+ # Handle mower devices (Luba, Yuka)
376
+ if device.device_name.startswith(("Luba-", "Yuka-")):
377
+ has_device = self.device_manager.has_device(device.device_name)
378
+ if not has_device:
379
+ mixed_device = MammotionMowerDeviceManager(
380
+ name=device.device_name,
381
+ iot_id=device.iot_id,
382
+ cloud_client=mqtt_client.cloud_client,
383
+ cloud_device=device,
384
+ mqtt=mqtt_client,
385
+ preference=ConnectionPreference.WIFI,
386
+ )
387
+ mixed_device.state.mower_state.product_key = device.product_key
388
+ mixed_device.state.mower_state.model = (
389
+ device.product_name if device.product_model is None else device.product_model
390
+ )
391
+ self.device_manager.add_device(mixed_device)
392
+ else:
393
+ mower_device = self.device_manager.get_device(device.device_name)
394
+ if mower_device.cloud is None:
395
+ mower_device.add_cloud(mqtt=mqtt_client)
396
+ else:
397
+ mower_device.replace_mqtt(mqtt_client)
398
+
399
+ # Handle RTK devices
400
+ elif device.device_name.startswith(("RTK", "RBS")):
401
+ has_rtk_device = self.device_manager.has_rtk_device(device.device_name)
402
+ if not has_rtk_device:
403
+ rtk_device = MammotionRTKDeviceManager(
404
+ name=device.device_name,
405
+ iot_id=device.iot_id,
406
+ cloud_client=mqtt_client.cloud_client,
407
+ cloud_device=device,
408
+ mqtt=mqtt_client,
409
+ preference=ConnectionPreference.WIFI,
410
+ )
411
+ self.device_manager.add_rtk_device(rtk_device)
412
+ else:
413
+ rtk_device = self.device_manager.get_rtk_device(device.device_name)
414
+ if rtk_device.cloud is None:
415
+ rtk_device.add_cloud(mqtt=mqtt_client)
416
+ else:
417
+ rtk_device.replace_mqtt(mqtt_client)
418
+
419
+ def set_disconnect_strategy(self, *, disconnect: bool) -> None:
420
+ """Set disconnect strategy for all BLE devices (mowers and RTK)."""
421
+ for device in self.device_manager.devices.values():
422
+ if device.ble is not None:
423
+ ble_device: MammotionMowerBLEDevice = device.ble
424
+ ble_device.set_disconnect_strategy(disconnect=disconnect)
425
+
426
+ for rtk_device in self.device_manager.rtk_devices.values():
427
+ if rtk_device.ble is not None:
428
+ ble_rtk_device: MammotionRTKBLEDevice = rtk_device.ble
429
+ ble_rtk_device.set_disconnect_strategy(disconnect=disconnect)
430
+
431
+ async def login(self, account: str, password: str) -> CloudIOTGateway:
432
+ """Login to mammotion cloud."""
433
+ mammotion_http = MammotionHTTP(session=self._session)
434
+ await mammotion_http.login_v2(account, password)
435
+ await mammotion_http.get_user_device_page()
436
+ device_list = await mammotion_http.get_user_device_list()
437
+ _LOGGER.debug("device_list: %s", device_list)
438
+ await mammotion_http.get_mqtt_credentials()
439
+ cloud_client = CloudIOTGateway(mammotion_http)
440
+ await self.connect_iot(cloud_client)
441
+ return cloud_client
442
+
443
+ async def stop(self) -> None:
444
+ await self.device_manager.stop()
445
+ for mqtt in self.mqtt_list.values():
446
+ if mqtt.is_connected():
447
+ mqtt.disconnect()
448
+
449
+ @staticmethod
450
+ async def connect_iot(cloud_client: CloudIOTGateway) -> None:
451
+ """Connect to aliyun cloud and fetch device info."""
452
+ mammotion_http = cloud_client.mammotion_http
453
+ country_code = mammotion_http.login_info.userInformation.domainAbbreviation
454
+ if cloud_client.region_response is None:
455
+ await cloud_client.get_region(country_code)
456
+ await cloud_client.connect()
457
+ await cloud_client.login_by_oauth(country_code)
458
+ await cloud_client.aep_handle()
459
+ await cloud_client.session_by_auth_code()
460
+ await cloud_client.list_binding_by_account()
461
+
462
+ async def remove_device(self, name: str) -> None:
463
+ """Remove a mower device."""
464
+ await self.device_manager.remove_device(name)
465
+
466
+ async def remove_rtk_device(self, name: str) -> None:
467
+ """Remove an RTK device."""
468
+ await self.device_manager.remove_rtk_device(name)
469
+
470
+ def get_device_by_name(self, name: str) -> MammotionMowerDeviceManager:
471
+ """Get a mower device by name."""
472
+ return self.device_manager.get_device(name)
473
+
474
+ def get_rtk_device_by_name(self, name: str) -> MammotionRTKDeviceManager:
475
+ """Get an RTK device by name."""
476
+ return self.device_manager.get_rtk_device(name)
477
+
478
+ def get_or_create_device_by_name(
479
+ self, device: Device, mqtt_client: MammotionCloud | None, ble_device: BLEDevice | None
480
+ ) -> MammotionMowerDeviceManager:
481
+ """Get or create a mower device by name."""
482
+ if self.device_manager.has_device(device.device_name):
483
+ return self.device_manager.get_device(device.device_name)
484
+ mow_device = MammotionMowerDeviceManager(
485
+ name=device.device_name,
486
+ iot_id=device.iot_id,
487
+ cloud_client=mqtt_client.cloud_client
488
+ if mqtt_client
489
+ else CloudIOTGateway(MammotionHTTP(session=self._session)),
490
+ mqtt=mqtt_client,
491
+ cloud_device=device,
492
+ ble_device=ble_device,
493
+ preference=ConnectionPreference.WIFI if mqtt_client else ConnectionPreference.BLUETOOTH,
494
+ )
495
+ self.device_manager.add_device(mow_device)
496
+ return mow_device
497
+
498
+ async def send_command(self, name: str, key: str):
499
+ """Send a command to the device."""
500
+ device = self.get_device_by_name(name)
501
+ if device:
502
+ if device.preference is ConnectionPreference.BLUETOOTH and device.ble:
503
+ return await device.ble.command(key)
504
+ if device.preference is ConnectionPreference.WIFI and device.cloud:
505
+ return await device.cloud.command(key)
506
+ # TODO work with both with EITHER
507
+ return None
508
+
509
+ async def send_command_with_args(self, name: str, key: str, **kwargs: Any):
510
+ """Send a command with args to the device."""
511
+ device = self.get_device_by_name(name)
512
+ if device:
513
+ if device.preference is ConnectionPreference.BLUETOOTH and device.ble:
514
+ return await device.ble.command(key, **kwargs)
515
+ if device.preference is ConnectionPreference.WIFI and device.cloud:
516
+ return await device.cloud.command(key, **kwargs)
517
+ # TODO work with both with EITHER
518
+ return None
519
+
520
+ async def start_map_sync(self, name: str) -> None:
521
+ """Start map sync."""
522
+ device = self.get_device_by_name(name)
523
+ if device:
524
+ if device.preference is ConnectionPreference.BLUETOOTH and device.ble:
525
+ return await device.ble.start_map_sync()
526
+ if device.preference is ConnectionPreference.WIFI and device.cloud:
527
+ return await device.cloud.start_map_sync()
528
+ # TODO work with both with EITHER
529
+ return None
530
+
531
+ async def start_schedule_sync(self, name: str) -> None:
532
+ """Start map sync."""
533
+ device = self.get_device_by_name(name)
534
+ if device:
535
+ if device.preference is ConnectionPreference.BLUETOOTH and device.ble:
536
+ return await device.ble.start_schedule_sync()
537
+ if device.preference is ConnectionPreference.WIFI and device.cloud:
538
+ return await device.cloud.start_schedule_sync()
539
+ # TODO work with both with EITHER
540
+ return None
541
+
542
+ async def get_stream_subscription(self, name: str, iot_id: str) -> Response[StreamSubscriptionResponse] | Any:
543
+ """Get stream subscription."""
544
+ device = self.get_device_by_name(name)
545
+ if DeviceType.is_mini_or_x_series(name):
546
+ _stream_response = await device.mammotion_http.get_stream_subscription_mini_or_x_series(
547
+ iot_id, DeviceType.is_yuka(name) and not DeviceType.is_yuka_mini(name)
548
+ )
549
+ _LOGGER.debug(_stream_response)
550
+ return _stream_response
551
+ else:
552
+ _stream_response = await device.mammotion_http.get_stream_subscription(iot_id)
553
+ _LOGGER.debug(_stream_response)
554
+ return _stream_response
555
+
556
+ async def get_video_resource(self, name: str, iot_id: str) -> Response[VideoResourceResponse] | None:
557
+ """Get video resource."""
558
+ device = self.get_device_by_name(name)
559
+
560
+ if DeviceType.is_mini_or_x_series(name):
561
+ _video_resource_response = await device.mammotion_http.get_video_resource(iot_id)
562
+ _LOGGER.debug(_video_resource_response)
563
+ return _video_resource_response
564
+ return None
565
+
566
+ def mower(self, name: str) -> MowingDevice | None:
567
+ """Get a mower device by name."""
568
+ device = self.get_device_by_name(name)
569
+ if device:
570
+ return device.state
571
+ return None