pymammotion 0.5.34__py3-none-any.whl → 0.5.44__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 (54) hide show
  1. pymammotion/__init__.py +3 -3
  2. pymammotion/aliyun/cloud_gateway.py +106 -18
  3. pymammotion/aliyun/model/dev_by_account_response.py +198 -20
  4. pymammotion/const.py +3 -0
  5. pymammotion/data/model/device.py +1 -0
  6. pymammotion/data/model/device_config.py +1 -1
  7. pymammotion/data/model/enums.py +5 -3
  8. pymammotion/data/model/generate_route_information.py +2 -2
  9. pymammotion/data/model/hash_list.py +113 -33
  10. pymammotion/data/model/region_data.py +4 -4
  11. pymammotion/data/{state_manager.py → mower_state_manager.py} +17 -7
  12. pymammotion/data/mqtt/event.py +47 -22
  13. pymammotion/data/mqtt/mammotion_properties.py +257 -0
  14. pymammotion/data/mqtt/properties.py +32 -29
  15. pymammotion/data/mqtt/status.py +17 -16
  16. pymammotion/homeassistant/__init__.py +3 -0
  17. pymammotion/homeassistant/mower_api.py +446 -0
  18. pymammotion/homeassistant/rtk_api.py +54 -0
  19. pymammotion/http/http.py +387 -13
  20. pymammotion/http/model/http.py +82 -2
  21. pymammotion/http/model/response_factory.py +10 -4
  22. pymammotion/mammotion/commands/mammotion_command.py +6 -0
  23. pymammotion/mammotion/commands/messages/navigation.py +10 -6
  24. pymammotion/mammotion/devices/__init__.py +27 -3
  25. pymammotion/mammotion/devices/base.py +16 -138
  26. pymammotion/mammotion/devices/mammotion.py +364 -204
  27. pymammotion/mammotion/devices/mammotion_bluetooth.py +7 -5
  28. pymammotion/mammotion/devices/mammotion_cloud.py +42 -83
  29. pymammotion/mammotion/devices/mammotion_mower_ble.py +49 -0
  30. pymammotion/mammotion/devices/mammotion_mower_cloud.py +39 -0
  31. pymammotion/mammotion/devices/managers/managers.py +81 -0
  32. pymammotion/mammotion/devices/mower_device.py +121 -0
  33. pymammotion/mammotion/devices/mower_manager.py +107 -0
  34. pymammotion/mammotion/devices/rtk_ble.py +89 -0
  35. pymammotion/mammotion/devices/rtk_cloud.py +113 -0
  36. pymammotion/mammotion/devices/rtk_device.py +50 -0
  37. pymammotion/mammotion/devices/rtk_manager.py +122 -0
  38. pymammotion/mqtt/__init__.py +2 -1
  39. pymammotion/mqtt/aliyun_mqtt.py +232 -0
  40. pymammotion/mqtt/mammotion_mqtt.py +174 -192
  41. pymammotion/mqtt/mqtt_models.py +66 -0
  42. pymammotion/proto/__init__.py +1 -1
  43. pymammotion/proto/mctrl_nav.proto +1 -1
  44. pymammotion/proto/mctrl_nav_pb2.py +1 -1
  45. pymammotion/proto/mctrl_nav_pb2.pyi +4 -4
  46. pymammotion/proto/mctrl_sys.proto +1 -1
  47. pymammotion/utility/datatype_converter.py +13 -12
  48. pymammotion/utility/device_type.py +88 -3
  49. pymammotion/utility/mur_mur_hash.py +132 -87
  50. {pymammotion-0.5.34.dist-info → pymammotion-0.5.44.dist-info}/METADATA +25 -31
  51. {pymammotion-0.5.34.dist-info → pymammotion-0.5.44.dist-info}/RECORD +59 -45
  52. {pymammotion-0.5.34.dist-info → pymammotion-0.5.44.dist-info}/WHEEL +1 -1
  53. pymammotion/http/_init_.py +0 -0
  54. {pymammotion-0.5.34.dist-info → pymammotion-0.5.44.dist-info/licenses}/LICENSE +0 -0
@@ -4,165 +4,142 @@ from __future__ import annotations
4
4
 
5
5
  import asyncio
6
6
  import logging
7
- from typing import Any
7
+ from typing import TYPE_CHECKING, Any
8
8
 
9
- from bleak.backends.device import BLEDevice
9
+ from bleak import BLEDevice
10
10
 
11
+ from pymammotion import MammotionMQTT
11
12
  from pymammotion.aliyun.cloud_gateway import CloudIOTGateway
12
13
  from pymammotion.aliyun.model.dev_by_account_response import Device
13
14
  from pymammotion.data.model.device import MowingDevice
14
15
  from pymammotion.data.model.enums import ConnectionPreference
15
- from pymammotion.data.state_manager import StateManager
16
16
  from pymammotion.http.http import MammotionHTTP
17
17
  from pymammotion.http.model.camera_stream import StreamSubscriptionResponse, VideoResourceResponse
18
- from pymammotion.http.model.http import Response
19
- from pymammotion.mammotion.devices.mammotion_bluetooth import MammotionBaseBLEDevice
20
- from pymammotion.mammotion.devices.mammotion_cloud import MammotionBaseCloudDevice, MammotionCloud
21
- from pymammotion.mqtt import MammotionMQTT
18
+ from pymammotion.http.model.http import DeviceRecord, Response
19
+ from pymammotion.mammotion.devices.mammotion_cloud import MammotionCloud
20
+ from pymammotion.mammotion.devices.mammotion_mower_ble import MammotionMowerBLEDevice
21
+ from pymammotion.mammotion.devices.managers.managers import AbstractDeviceManager
22
+ from pymammotion.mammotion.devices.mower_manager import MammotionMowerDeviceManager
23
+ from pymammotion.mammotion.devices.rtk_manager import MammotionRTKDeviceManager
24
+ from pymammotion.mqtt import AliyunMQTT
22
25
  from pymammotion.utility.device_type import DeviceType
23
26
 
27
+ # RTK imports - imported here for type hints, full import in add_cloud_devices
28
+ if TYPE_CHECKING:
29
+ from pymammotion.mammotion.devices.rtk_ble import MammotionRTKBLEDevice
30
+
24
31
  TIMEOUT_CLOUD_RESPONSE = 10
25
32
 
26
33
  _LOGGER = logging.getLogger(__name__)
27
34
 
28
35
 
29
- class MammotionMixedDeviceManager:
30
- def __init__(
31
- self,
32
- name: str,
33
- iot_id: str,
34
- cloud_client: CloudIOTGateway,
35
- cloud_device: Device,
36
- ble_device: BLEDevice | None = None,
37
- mqtt: MammotionCloud | None = None,
38
- preference: ConnectionPreference = ConnectionPreference.BLUETOOTH,
39
- ) -> None:
40
- self._ble_device: MammotionBaseBLEDevice | None = None
41
- self._cloud_device: MammotionBaseCloudDevice | None = None
42
- self.name = name
43
- self.iot_id = iot_id
44
- self.cloud_client = cloud_client
45
- self._state_manager = StateManager(MowingDevice())
46
- self._state_manager.get_device().name = name
47
- self._device: Device = cloud_device
48
- self.add_ble(ble_device) if ble_device else None
49
- self.add_cloud(mqtt) if mqtt else None
50
- self.mammotion_http = cloud_client.mammotion_http
51
- self.preference = preference
52
- self._state_manager.preference = preference
53
-
54
- @property
55
- def state_manager(self) -> StateManager:
56
- """Return the state manager."""
57
- return self._state_manager
58
-
59
- @property
60
- def state(self):
61
- """Return the state of the device."""
62
- return self._state_manager.get_device()
63
-
64
- @state.setter
65
- def state(self, value: MowingDevice) -> None:
66
- self._state_manager.set_device(value)
67
-
68
- def ble(self) -> MammotionBaseBLEDevice | None:
69
- return self._ble_device
70
-
71
- def cloud(self) -> MammotionBaseCloudDevice | None:
72
- return self._cloud_device
73
-
74
- def has_queued_commands(self) -> bool:
75
- if self.has_cloud() and self.preference == ConnectionPreference.WIFI:
76
- return not self.cloud().mqtt.command_queue.empty()
77
- else:
78
- return not self.ble().command_queue.empty()
79
-
80
- def add_ble(self, ble_device: BLEDevice) -> MammotionBaseBLEDevice:
81
- self._ble_device = MammotionBaseBLEDevice(
82
- state_manager=self._state_manager, cloud_device=self._device, device=ble_device
83
- )
84
- return self._ble_device
36
+ class MammotionDeviceManager:
37
+ """Manage devices - both mowers and RTK."""
85
38
 
86
- def add_cloud(self, mqtt: MammotionCloud) -> MammotionBaseCloudDevice:
87
- self._cloud_device = MammotionBaseCloudDevice(
88
- mqtt, cloud_device=self._device, state_manager=self._state_manager
39
+ def __init__(self) -> None:
40
+ self.devices: dict[str, MammotionMowerDeviceManager] = {}
41
+ self.rtk_devices: dict[str, MammotionRTKDeviceManager] = {}
42
+
43
+ def _should_disconnect_mqtt(self, device_for_removal: AbstractDeviceManager) -> bool:
44
+ """Check if MQTT connection should be disconnected.
45
+
46
+ Returns True if no other devices share the same MQTT connection.
47
+ """
48
+ if not device_for_removal.cloud:
49
+ return False
50
+
51
+ mqtt_to_check = device_for_removal.cloud.mqtt
52
+
53
+ # Check if any mower device shares this MQTT connection
54
+ shared_devices: set[AbstractDeviceManager] = {
55
+ device
56
+ for device in self.devices.values()
57
+ if device.cloud is not None and device.cloud.mqtt == mqtt_to_check
58
+ }
59
+
60
+ # Also check RTK devices for shared MQTT
61
+ shared_devices.update(
62
+ {
63
+ device
64
+ for device in self.rtk_devices.values()
65
+ if device.cloud is not None and device.cloud.mqtt == mqtt_to_check
66
+ }
89
67
  )
90
- return self._cloud_device
91
-
92
- def replace_cloud(self, cloud_device: MammotionBaseCloudDevice) -> None:
93
- self._cloud_device = cloud_device
94
-
95
- def remove_cloud(self) -> None:
96
- self._state_manager.cloud_get_commondata_ack_callback = None
97
- self._state_manager.cloud_get_hashlist_ack_callback = None
98
- self._state_manager.cloud_get_plan_callback = None
99
- self._state_manager.cloud_on_notification_callback = None
100
- self._state_manager.cloud_gethash_ack_callback = None
101
- self._cloud_device = None
102
-
103
- def replace_ble(self, ble_device: MammotionBaseBLEDevice) -> None:
104
- self._ble_device = ble_device
105
68
 
106
- def remove_ble(self) -> None:
107
- self._state_manager.ble_get_commondata_ack_callback = None
108
- self._state_manager.ble_get_hashlist_ack_callback = None
109
- self._state_manager.ble_get_plan_callback = None
110
- self._state_manager.ble_on_notification_callback = None
111
- self._state_manager.ble_gethash_ack_callback = None
112
- self._ble_device = None
113
-
114
- def replace_mqtt(self, mqtt: MammotionCloud) -> None:
115
- device = self._cloud_device.device
116
- self._cloud_device = MammotionBaseCloudDevice(mqtt, cloud_device=device, state_manager=self._state_manager)
117
-
118
- def has_cloud(self) -> bool:
119
- return self._cloud_device is not None
120
-
121
- def has_ble(self) -> bool:
122
- return self._ble_device is not None
69
+ return len(shared_devices) == 0
123
70
 
71
+ def add_device(self, mammotion_device: MammotionMowerDeviceManager) -> None:
72
+ """Add a mower device."""
73
+ exists: MammotionMowerDeviceManager | None = self.devices.get(mammotion_device.name)
74
+ if exists is None:
75
+ self.devices[mammotion_device.name] = mammotion_device
76
+ return
77
+ if mammotion_device.cloud is not None:
78
+ exists.replace_cloud(mammotion_device.cloud)
79
+ if mammotion_device.ble:
80
+ exists.replace_ble(mammotion_device.ble)
124
81
 
125
- class MammotionDeviceManager:
126
- """Manage devices."""
82
+ def add_rtk_device(self, rtk_device: MammotionRTKDeviceManager) -> None:
83
+ """Add an RTK device."""
127
84
 
128
- def __init__(self) -> None:
129
- self.devices: dict[str, MammotionMixedDeviceManager] = {}
130
-
131
- def add_device(self, mammotion_device: MammotionMixedDeviceManager) -> None:
132
- """Add a device."""
133
- exists: MammotionMixedDeviceManager | None = self.devices.get(mammotion_device.name)
85
+ exists: MammotionRTKDeviceManager | None = self.rtk_devices.get(rtk_device.name)
134
86
  if exists is None:
135
- self.devices[mammotion_device.name] = mammotion_device
87
+ self.rtk_devices[rtk_device.name] = rtk_device
136
88
  return
137
- if mammotion_device.has_cloud():
138
- exists.replace_cloud(mammotion_device.cloud())
139
- if mammotion_device.has_ble():
140
- exists.replace_ble(mammotion_device.ble())
89
+ if rtk_device.cloud:
90
+ exists.replace_cloud(rtk_device.cloud)
91
+ if rtk_device.ble:
92
+ exists.replace_ble(rtk_device.ble)
141
93
 
142
94
  def has_device(self, mammotion_device_name: str) -> bool:
95
+ """Check if a mower device exists."""
143
96
  if self.devices.get(mammotion_device_name, None) is not None:
144
97
  return True
145
98
  return False
146
99
 
147
- def get_device(self, mammotion_device_name: str) -> MammotionMixedDeviceManager:
100
+ def has_rtk_device(self, rtk_device_name: str) -> bool:
101
+ """Check if an RTK device exists."""
102
+ if self.rtk_devices.get(rtk_device_name, None) is not None:
103
+ return True
104
+ return False
105
+
106
+ def get_device(self, mammotion_device_name: str) -> MammotionMowerDeviceManager:
107
+ """Get a mower device."""
148
108
  return self.devices[mammotion_device_name]
149
109
 
110
+ def get_rtk_device(self, rtk_device_name: str) -> MammotionRTKDeviceManager:
111
+ """Get an RTK device."""
112
+ return self.rtk_devices[rtk_device_name]
113
+
150
114
  async def remove_device(self, name: str) -> None:
151
- """Remove a device."""
115
+ """Remove a mower device."""
152
116
  if self.devices.get(name):
153
117
  device_for_removal = self.devices.pop(name)
154
118
  loop = asyncio.get_running_loop()
155
- if device_for_removal.has_cloud():
156
- should_disconnect = {
157
- device
158
- for key, device in self.devices.items()
159
- if device.cloud() is not None and device.cloud().mqtt == device_for_removal.cloud().mqtt
160
- }
161
- if len(should_disconnect) == 0:
162
- await loop.run_in_executor(None, device_for_removal.cloud().mqtt.disconnect)
163
- await device_for_removal.cloud().stop()
164
- if device_for_removal.has_ble():
165
- await device_for_removal.ble().stop()
119
+
120
+ if device_for_removal.cloud:
121
+ if self._should_disconnect_mqtt(device_for_removal):
122
+ await loop.run_in_executor(None, device_for_removal.cloud.mqtt.disconnect)
123
+ await device_for_removal.cloud.stop()
124
+
125
+ if device_for_removal.ble:
126
+ await device_for_removal.ble.stop()
127
+
128
+ del device_for_removal
129
+
130
+ async def remove_rtk_device(self, name: str) -> None:
131
+ """Remove an RTK device."""
132
+ if self.rtk_devices.get(name):
133
+ device_for_removal = self.rtk_devices.pop(name)
134
+ loop = asyncio.get_running_loop()
135
+
136
+ if device_for_removal.cloud:
137
+ if self._should_disconnect_mqtt(device_for_removal):
138
+ await loop.run_in_executor(None, device_for_removal.cloud.mqtt.disconnect)
139
+ await device_for_removal.cloud.stop()
140
+
141
+ if device_for_removal.ble:
142
+ await device_for_removal.ble.stop()
166
143
 
167
144
  del device_for_removal
168
145
 
@@ -187,90 +164,265 @@ class Mammotion:
187
164
 
188
165
  async def login_and_initiate_cloud(self, account, password, force: bool = False) -> None:
189
166
  async with self._login_lock:
190
- exists: MammotionCloud | None = self.mqtt_list.get(account)
191
- if not exists or force:
167
+ exists_aliyun: MammotionCloud | None = self.mqtt_list.get(f"{account}_aliyun")
168
+ exists_mammotion: MammotionCloud | None = self.mqtt_list.get(f"{account}_mammotion")
169
+ if (not exists_aliyun and not exists_mammotion) or force:
192
170
  cloud_client = await self.login(account, password)
193
171
  await self.initiate_cloud_connection(account, cloud_client)
194
172
 
195
- async def refresh_login(self, account: str, password: str | None = None) -> None:
173
+ async def refresh_login(self, account: str) -> None:
196
174
  """Refresh login."""
197
175
  async with self._login_lock:
198
- exists: MammotionCloud | None = self.mqtt_list.get(account)
199
- if not exists:
176
+ exists_aliyun: MammotionCloud | None = self.mqtt_list.get(f"{account}_aliyun")
177
+ exists_mammotion: MammotionCloud | None = self.mqtt_list.get(f"{account}_mammotion")
178
+
179
+ if not exists_aliyun and not exists_mammotion:
200
180
  return
201
- mammotion_http = exists.cloud_client.mammotion_http
181
+ mammotion_http = (
182
+ exists_aliyun.cloud_client.mammotion_http
183
+ if exists_aliyun
184
+ else exists_mammotion.cloud_client.mammotion_http
185
+ )
186
+
202
187
  await mammotion_http.refresh_login()
203
- await self.connect_iot(exists.cloud_client)
204
188
 
205
- if not exists.is_connected():
189
+ if len(mammotion_http.device_info) != 0:
190
+ await self.connect_iot(exists_aliyun.cloud_client)
191
+ if len(mammotion_http.device_records.records) != 0:
192
+ await mammotion_http.get_mqtt_credentials()
193
+
194
+ if exists_aliyun and not exists_aliyun.is_connected():
206
195
  loop = asyncio.get_running_loop()
207
- await loop.run_in_executor(None, exists.connect_async)
196
+ await loop.run_in_executor(None, exists_aliyun.connect_async)
197
+ if exists_mammotion and not exists_mammotion.is_connected():
198
+ loop = asyncio.get_running_loop()
199
+ await loop.run_in_executor(None, exists_mammotion.connect_async)
200
+
201
+ @staticmethod
202
+ def shim_cloud_devices(devices: list[DeviceRecord]) -> list[Device]:
203
+ device_list: list[Device] = []
204
+ for device in devices:
205
+ device_list.append(
206
+ Device(
207
+ gmt_modified=0,
208
+ product_name="",
209
+ status=0,
210
+ net_type="NET_WIFI",
211
+ is_edge_gateway=False,
212
+ category_name="",
213
+ owned=1,
214
+ identity_alias="UNKNOW",
215
+ thing_type="DEVICE",
216
+ identity_id=device.identity_id,
217
+ device_name=device.device_name,
218
+ product_key=device.product_key,
219
+ iot_id=device.iot_id,
220
+ bind_time=device.bind_time,
221
+ node_type="DEVICE",
222
+ category_key="LawnMower",
223
+ )
224
+ )
225
+
226
+ return device_list
227
+
228
+ async def initiate_ble_connection(self, devices: dict[str, BLEDevice], cloud_devices: list[Device]) -> None:
229
+ """Initiate BLE connection."""
230
+ for device in cloud_devices:
231
+ if ble_device := devices.get(device.device_name):
232
+ if device.device_name.startswith(("Luba-", "Yuka-")):
233
+ if not self.device_manager.has_device(device.device_name):
234
+ self.device_manager.add_device(
235
+ MammotionMowerDeviceManager(
236
+ name=device.device_name,
237
+ iot_id=device.iot_id,
238
+ cloud_device=device,
239
+ ble_device=ble_device,
240
+ preference=ConnectionPreference.BLUETOOTH,
241
+ cloud_client=CloudIOTGateway(MammotionHTTP()),
242
+ )
243
+ )
244
+ else:
245
+ self.device_manager.get_device(device.device_name).add_ble(ble_device)
246
+ if device.device_name.startswith(("RTK", "RBS")):
247
+ if not self.device_manager.has_rtk_device(device.device_name):
248
+ self.device_manager.add_rtk_device(
249
+ MammotionRTKDeviceManager(
250
+ name=device.device_name,
251
+ iot_id=device.iot_id,
252
+ cloud_device=device,
253
+ ble_device=ble_device,
254
+ preference=ConnectionPreference.BLUETOOTH,
255
+ cloud_client=CloudIOTGateway(MammotionHTTP()),
256
+ )
257
+ )
258
+ else:
259
+ self.device_manager.get_rtk_device(device.device_name).add_ble(ble_device)
208
260
 
209
261
  async def initiate_cloud_connection(self, account: str, cloud_client: CloudIOTGateway) -> None:
210
262
  """Initiate cloud connection."""
211
263
  loop = asyncio.get_running_loop()
212
- if mqtt := self.mqtt_list.get(account):
264
+
265
+ mammotion_http = cloud_client.mammotion_http
266
+
267
+ if mqtt := self.mqtt_list.get(f"{account}_aliyun"):
213
268
  if mqtt.is_connected():
214
269
  await loop.run_in_executor(None, mqtt.disconnect)
215
270
 
216
- mammotion_cloud = MammotionCloud(
217
- MammotionMQTT(
218
- region_id=cloud_client.region_response.data.regionId,
219
- product_key=cloud_client.aep_response.data.productKey,
220
- device_name=cloud_client.aep_response.data.deviceName,
221
- device_secret=cloud_client.aep_response.data.deviceSecret,
222
- iot_token=cloud_client.session_by_authcode_response.data.iotToken,
223
- client_id=cloud_client.client_id,
224
- cloud_client=cloud_client,
225
- ),
226
- cloud_client,
227
- )
228
- self.mqtt_list[account] = mammotion_cloud
229
- self.add_cloud_devices(mammotion_cloud)
271
+ if mqtt := self.mqtt_list.get(f"{account}_mammotion"):
272
+ if mqtt.is_connected():
273
+ await loop.run_in_executor(None, mqtt.disconnect)
274
+
275
+ if len(mammotion_http.device_info) != 0:
276
+ mammotion_cloud = MammotionCloud(
277
+ AliyunMQTT(
278
+ region_id=cloud_client.region_response.data.regionId,
279
+ product_key=cloud_client.aep_response.data.productKey,
280
+ device_name=cloud_client.aep_response.data.deviceName,
281
+ device_secret=cloud_client.aep_response.data.deviceSecret,
282
+ iot_token=cloud_client.session_by_authcode_response.data.iotToken,
283
+ client_id=cloud_client.client_id,
284
+ cloud_client=cloud_client,
285
+ ),
286
+ cloud_client,
287
+ )
288
+ self.mqtt_list[f"{account}_aliyun"] = mammotion_cloud
289
+ self.add_cloud_devices(mammotion_cloud)
290
+
291
+ await loop.run_in_executor(None, self.mqtt_list[f"{account}_aliyun"].connect_async)
292
+ if len(mammotion_http.device_records.records) != 0:
293
+ mammotion_cloud = MammotionCloud(
294
+ MammotionMQTT(
295
+ records=mammotion_http.device_records.records,
296
+ mammotion_http=mammotion_http,
297
+ mqtt_connection=mammotion_http.mqtt_credentials,
298
+ ),
299
+ cloud_client,
300
+ )
301
+ self.mqtt_list[f"{account}_mammotion"] = mammotion_cloud
302
+ self.add_mammotion_devices(mammotion_cloud, mammotion_http.device_records.records)
303
+
304
+ await loop.run_in_executor(None, self.mqtt_list[f"{account}_mammotion"].connect_async)
305
+
306
+ def add_mammotion_devices(self, mqtt_client: MammotionCloud, devices: list[DeviceRecord]) -> None:
307
+ """Add devices from mammotion cloud."""
308
+ for device in devices:
309
+ if device.device_name.startswith(("Luba-", "Yuka-")):
310
+ has_device = self.device_manager.has_device(device.device_name)
311
+ if has_device:
312
+ mower_device = self.device_manager.get_device(device.device_name)
313
+ if mower_device.cloud is None:
314
+ mower_device.add_cloud(mqtt=mqtt_client)
315
+ else:
316
+ mower_device.replace_mqtt(mqtt_client)
230
317
 
231
- await loop.run_in_executor(None, self.mqtt_list[account].connect_async)
318
+ else:
319
+ cloud_device_shim = Device(
320
+ gmt_modified=0,
321
+ product_name="",
322
+ status=0,
323
+ net_type="NET_WIFI",
324
+ is_edge_gateway=False,
325
+ category_name="",
326
+ owned=1,
327
+ identity_alias="UNKNOW",
328
+ thing_type="DEVICE",
329
+ identity_id=device.identity_id,
330
+ device_name=device.device_name,
331
+ product_key=device.product_key,
332
+ iot_id=device.iot_id,
333
+ bind_time=device.bind_time,
334
+ node_type="DEVICE",
335
+ category_key="LawnMower",
336
+ )
337
+
338
+ mixed_device = MammotionMowerDeviceManager(
339
+ name=device.device_name,
340
+ iot_id=device.iot_id,
341
+ cloud_client=mqtt_client.cloud_client,
342
+ cloud_device=cloud_device_shim,
343
+ mqtt=mqtt_client,
344
+ preference=ConnectionPreference.WIFI,
345
+ )
346
+ mixed_device.state.mower_state.product_key = device.product_key
347
+ self.device_manager.add_device(mixed_device)
232
348
 
233
349
  def add_cloud_devices(self, mqtt_client: MammotionCloud) -> None:
234
- """Add devices from cloud."""
350
+ """Add devices from cloud - both mowers and RTK."""
351
+ from pymammotion.mammotion.devices.rtk_manager import MammotionRTKDeviceManager
352
+
235
353
  for device in mqtt_client.cloud_client.devices_by_account_response.data.data:
236
- has_device = self.device_manager.has_device(device.deviceName)
237
- if device.deviceName.startswith(("Luba-", "Yuka-")) and not has_device:
238
- mixed_device = MammotionMixedDeviceManager(
239
- name=device.deviceName,
240
- iot_id=device.iotId,
241
- cloud_client=mqtt_client.cloud_client,
242
- cloud_device=device,
243
- mqtt=mqtt_client,
244
- preference=ConnectionPreference.WIFI,
245
- )
246
- mixed_device.state.mower_state.product_key = device.productKey
247
- mixed_device.state.mower_state.model = (
248
- device.productName if device.productModel is None else device.productModel
249
- )
250
- self.device_manager.add_device(mixed_device)
251
- elif device.deviceName.startswith(("Luba-", "Yuka-")) and has_device:
252
- mower_device = self.device_manager.get_device(device.deviceName)
253
- if mower_device.cloud() is None:
254
- mower_device.add_cloud(mqtt=mqtt_client)
354
+ # Handle mower devices (Luba, Yuka)
355
+ if device.device_name.startswith(("Luba-", "Yuka-")):
356
+ has_device = self.device_manager.has_device(device.device_name)
357
+ if not has_device:
358
+ mixed_device = MammotionMowerDeviceManager(
359
+ name=device.device_name,
360
+ iot_id=device.iot_id,
361
+ cloud_client=mqtt_client.cloud_client,
362
+ cloud_device=device,
363
+ mqtt=mqtt_client,
364
+ preference=ConnectionPreference.WIFI,
365
+ )
366
+ mixed_device.state.mower_state.product_key = device.product_key
367
+ mixed_device.state.mower_state.model = (
368
+ device.product_name if device.product_model is None else device.product_model
369
+ )
370
+ self.device_manager.add_device(mixed_device)
371
+ else:
372
+ mower_device = self.device_manager.get_device(device.device_name)
373
+ if mower_device.cloud is None:
374
+ mower_device.add_cloud(mqtt=mqtt_client)
375
+ else:
376
+ mower_device.replace_mqtt(mqtt_client)
377
+
378
+ # Handle RTK devices
379
+ elif device.device_name.startswith(("RTK", "RBS")):
380
+ has_rtk_device = self.device_manager.has_rtk_device(device.device_name)
381
+ if not has_rtk_device:
382
+ rtk_device = MammotionRTKDeviceManager(
383
+ name=device.device_name,
384
+ iot_id=device.iot_id,
385
+ cloud_client=mqtt_client.cloud_client,
386
+ cloud_device=device,
387
+ mqtt=mqtt_client,
388
+ preference=ConnectionPreference.WIFI,
389
+ )
390
+ self.device_manager.add_rtk_device(rtk_device)
255
391
  else:
256
- mower_device.replace_mqtt(mqtt_client)
392
+ rtk_device = self.device_manager.get_rtk_device(device.device_name)
393
+ if rtk_device.cloud is None:
394
+ rtk_device.add_cloud(mqtt=mqtt_client)
395
+ else:
396
+ rtk_device.replace_mqtt(mqtt_client)
257
397
 
258
398
  def set_disconnect_strategy(self, *, disconnect: bool) -> None:
399
+ """Set disconnect strategy for all BLE devices (mowers and RTK)."""
259
400
  for device in self.device_manager.devices.values():
260
- if device.ble() is not None:
261
- ble_device: MammotionBaseBLEDevice = device.ble()
401
+ if device.ble is not None:
402
+ ble_device: MammotionMowerBLEDevice = device.ble
262
403
  ble_device.set_disconnect_strategy(disconnect=disconnect)
263
404
 
405
+ for rtk_device in self.device_manager.rtk_devices.values():
406
+ if rtk_device.ble is not None:
407
+ ble_rtk_device: MammotionRTKBLEDevice = rtk_device.ble
408
+ ble_rtk_device.set_disconnect_strategy(disconnect=disconnect)
409
+
264
410
  async def login(self, account: str, password: str) -> CloudIOTGateway:
265
411
  """Login to mammotion cloud."""
266
412
  mammotion_http = MammotionHTTP()
413
+ await mammotion_http.login_v2(account, password)
414
+ await mammotion_http.get_user_device_page()
415
+ device_list = await mammotion_http.get_user_device_list()
416
+ _LOGGER.debug("device_list: %s", device_list)
417
+ await mammotion_http.get_mqtt_credentials()
267
418
  cloud_client = CloudIOTGateway(mammotion_http)
268
- await mammotion_http.login(account, password)
269
- await self.connect_iot(cloud_client)
419
+ if len(device_list.data or []) != 0:
420
+ await self.connect_iot(cloud_client)
270
421
  return cloud_client
271
422
 
272
423
  @staticmethod
273
424
  async def connect_iot(cloud_client: CloudIOTGateway) -> None:
425
+ """Connect to aliyun cloud and fetch device info."""
274
426
  mammotion_http = cloud_client.mammotion_http
275
427
  country_code = mammotion_http.login_info.userInformation.domainAbbreviation
276
428
  if cloud_client.region_response is None:
@@ -282,21 +434,35 @@ class Mammotion:
282
434
  await cloud_client.list_binding_by_account()
283
435
 
284
436
  async def remove_device(self, name: str) -> None:
437
+ """Remove a mower device."""
285
438
  await self.device_manager.remove_device(name)
286
439
 
287
- def get_device_by_name(self, name: str) -> MammotionMixedDeviceManager:
440
+ async def remove_rtk_device(self, name: str) -> None:
441
+ """Remove an RTK device."""
442
+ await self.device_manager.remove_rtk_device(name)
443
+
444
+ def get_device_by_name(self, name: str) -> MammotionMowerDeviceManager:
445
+ """Get a mower device by name."""
288
446
  return self.device_manager.get_device(name)
289
447
 
290
- def get_or_create_device_by_name(self, device: Device, mqtt_client: MammotionCloud) -> MammotionMixedDeviceManager:
291
- if self.device_manager.has_device(device.deviceName):
292
- return self.device_manager.get_device(device.deviceName)
293
- mow_device = MammotionMixedDeviceManager(
294
- name=device.deviceName,
295
- iot_id=device.iotId,
296
- cloud_client=mqtt_client.cloud_client,
448
+ def get_rtk_device_by_name(self, name: str) -> MammotionRTKDeviceManager:
449
+ """Get an RTK device by name."""
450
+ return self.device_manager.get_rtk_device(name)
451
+
452
+ def get_or_create_device_by_name(
453
+ self, device: Device, mqtt_client: MammotionCloud | None, ble_device: BLEDevice | None
454
+ ) -> MammotionMowerDeviceManager:
455
+ """Get or create a mower device by name."""
456
+ if self.device_manager.has_device(device.device_name):
457
+ return self.device_manager.get_device(device.device_name)
458
+ mow_device = MammotionMowerDeviceManager(
459
+ name=device.device_name,
460
+ iot_id=device.iot_id,
461
+ cloud_client=mqtt_client.cloud_client if mqtt_client else CloudIOTGateway(MammotionHTTP()),
297
462
  mqtt=mqtt_client,
298
463
  cloud_device=device,
299
- ble_device=None,
464
+ ble_device=ble_device,
465
+ preference=ConnectionPreference.WIFI if mqtt_client else ConnectionPreference.BLUETOOTH,
300
466
  )
301
467
  self.device_manager.add_device(mow_device)
302
468
  return mow_device
@@ -305,10 +471,10 @@ class Mammotion:
305
471
  """Send a command to the device."""
306
472
  device = self.get_device_by_name(name)
307
473
  if device:
308
- if device.preference is ConnectionPreference.BLUETOOTH and device.has_ble():
309
- return await device.ble().command(key)
310
- if device.preference is ConnectionPreference.WIFI:
311
- return await device.cloud().command(key)
474
+ if device.preference is ConnectionPreference.BLUETOOTH and device.ble:
475
+ return await device.ble.command(key)
476
+ if device.preference is ConnectionPreference.WIFI and device.cloud:
477
+ return await device.cloud.command(key)
312
478
  # TODO work with both with EITHER
313
479
  return None
314
480
 
@@ -316,34 +482,26 @@ class Mammotion:
316
482
  """Send a command with args to the device."""
317
483
  device = self.get_device_by_name(name)
318
484
  if device:
319
- if device.preference is ConnectionPreference.BLUETOOTH and device.has_ble():
320
- return await device.ble().command(key, **kwargs)
321
- if device.preference is ConnectionPreference.WIFI:
322
- return await device.cloud().command(key, **kwargs)
323
- # TODO work with both with EITHER
324
- return None
325
-
326
- async def start_sync(self, name: str, retry: int):
327
- device = self.get_device_by_name(name)
328
- if device:
329
- if device.preference is ConnectionPreference.BLUETOOTH and device.has_ble():
330
- return await device.ble().start_sync(retry)
485
+ if device.preference is ConnectionPreference.BLUETOOTH and device.ble:
486
+ return await device.ble.command(key, **kwargs)
331
487
  if device.preference is ConnectionPreference.WIFI:
332
- return await device.cloud().start_sync(retry)
488
+ return await device.cloud.command(key, **kwargs)
333
489
  # TODO work with both with EITHER
334
490
  return None
335
491
 
336
492
  async def start_map_sync(self, name: str):
493
+ """Start map sync."""
337
494
  device = self.get_device_by_name(name)
338
495
  if device:
339
- if device.preference is ConnectionPreference.BLUETOOTH and device.has_ble():
340
- return await device.ble().start_map_sync()
341
- if device.preference is ConnectionPreference.WIFI:
342
- return await device.cloud().start_map_sync()
496
+ if device.preference is ConnectionPreference.BLUETOOTH and device.ble:
497
+ return await device.ble.start_map_sync()
498
+ if device.preference is ConnectionPreference.WIFI and device.cloud:
499
+ return await device.cloud.start_map_sync()
343
500
  # TODO work with both with EITHER
344
501
  return None
345
502
 
346
503
  async def get_stream_subscription(self, name: str, iot_id: str) -> Response[StreamSubscriptionResponse] | Any:
504
+ """Get stream subscription."""
347
505
  device = self.get_device_by_name(name)
348
506
  if DeviceType.is_mini_or_x_series(name):
349
507
  _stream_response = await device.mammotion_http.get_stream_subscription_mini_or_x_series(
@@ -357,6 +515,7 @@ class Mammotion:
357
515
  return _stream_response
358
516
 
359
517
  async def get_video_resource(self, name: str, iot_id: str) -> Response[VideoResourceResponse] | None:
518
+ """Get video resource."""
360
519
  device = self.get_device_by_name(name)
361
520
 
362
521
  if DeviceType.is_mini_or_x_series(name):
@@ -366,6 +525,7 @@ class Mammotion:
366
525
  return None
367
526
 
368
527
  def mower(self, name: str) -> MowingDevice | None:
528
+ """Get a mower device by name."""
369
529
  device = self.get_device_by_name(name)
370
530
  if device:
371
531
  return device.state