pymammotion 0.5.32__py3-none-any.whl → 0.5.45__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 +114 -18
  3. pymammotion/aliyun/model/dev_by_account_response.py +169 -21
  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 +392 -16
  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 -139
  26. pymammotion/mammotion/devices/mammotion.py +361 -203
  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.32.dist-info → pymammotion-0.5.45.dist-info}/METADATA +25 -31
  51. {pymammotion-0.5.32.dist-info → pymammotion-0.5.45.dist-info}/RECORD +59 -45
  52. {pymammotion-0.5.32.dist-info → pymammotion-0.5.45.dist-info}/WHEEL +1 -1
  53. pymammotion/http/_init_.py +0 -0
  54. {pymammotion-0.5.32.dist-info → pymammotion-0.5.45.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,263 @@ 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
+ await self.connect_iot(exists_aliyun.cloud_client)
190
+ if len(mammotion_http.device_records.records) != 0:
191
+ await mammotion_http.get_mqtt_credentials()
192
+
193
+ if exists_aliyun and not exists_aliyun.is_connected():
206
194
  loop = asyncio.get_running_loop()
207
- await loop.run_in_executor(None, exists.connect_async)
195
+ await loop.run_in_executor(None, exists_aliyun.connect_async)
196
+ if exists_mammotion and not exists_mammotion.is_connected():
197
+ loop = asyncio.get_running_loop()
198
+ await loop.run_in_executor(None, exists_mammotion.connect_async)
199
+
200
+ @staticmethod
201
+ def shim_cloud_devices(devices: list[DeviceRecord]) -> list[Device]:
202
+ device_list: list[Device] = []
203
+ for device in devices:
204
+ device_list.append(
205
+ Device(
206
+ gmt_modified=0,
207
+ product_name="",
208
+ status=0,
209
+ net_type="NET_WIFI",
210
+ is_edge_gateway=False,
211
+ category_name="",
212
+ owned=1,
213
+ identity_alias="UNKNOW",
214
+ thing_type="DEVICE",
215
+ identity_id=device.identity_id,
216
+ device_name=device.device_name,
217
+ product_key=device.product_key,
218
+ iot_id=device.iot_id,
219
+ bind_time=device.bind_time,
220
+ node_type="DEVICE",
221
+ category_key="LawnMower",
222
+ )
223
+ )
224
+
225
+ return device_list
226
+
227
+ async def initiate_ble_connection(self, devices: dict[str, BLEDevice], cloud_devices: list[Device]) -> None:
228
+ """Initiate BLE connection."""
229
+ for device in cloud_devices:
230
+ if ble_device := devices.get(device.device_name):
231
+ if device.device_name.startswith(("Luba-", "Yuka-")):
232
+ if not self.device_manager.has_device(device.device_name):
233
+ self.device_manager.add_device(
234
+ MammotionMowerDeviceManager(
235
+ name=device.device_name,
236
+ iot_id=device.iot_id,
237
+ cloud_device=device,
238
+ ble_device=ble_device,
239
+ preference=ConnectionPreference.BLUETOOTH,
240
+ cloud_client=CloudIOTGateway(MammotionHTTP()),
241
+ )
242
+ )
243
+ else:
244
+ self.device_manager.get_device(device.device_name).add_ble(ble_device)
245
+ if device.device_name.startswith(("RTK", "RBS")):
246
+ if not self.device_manager.has_rtk_device(device.device_name):
247
+ self.device_manager.add_rtk_device(
248
+ MammotionRTKDeviceManager(
249
+ name=device.device_name,
250
+ iot_id=device.iot_id,
251
+ cloud_device=device,
252
+ ble_device=ble_device,
253
+ preference=ConnectionPreference.BLUETOOTH,
254
+ cloud_client=CloudIOTGateway(MammotionHTTP()),
255
+ )
256
+ )
257
+ else:
258
+ self.device_manager.get_rtk_device(device.device_name).add_ble(ble_device)
208
259
 
209
260
  async def initiate_cloud_connection(self, account: str, cloud_client: CloudIOTGateway) -> None:
210
261
  """Initiate cloud connection."""
211
262
  loop = asyncio.get_running_loop()
212
- if mqtt := self.mqtt_list.get(account):
263
+
264
+ mammotion_http = cloud_client.mammotion_http
265
+
266
+ if mqtt := self.mqtt_list.get(f"{account}_aliyun"):
213
267
  if mqtt.is_connected():
214
268
  await loop.run_in_executor(None, mqtt.disconnect)
215
269
 
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)
270
+ if mqtt := self.mqtt_list.get(f"{account}_mammotion"):
271
+ if mqtt.is_connected():
272
+ await loop.run_in_executor(None, mqtt.disconnect)
273
+
274
+ if len(cloud_client.devices_by_account_response.data.data) != 0:
275
+ mammotion_cloud = MammotionCloud(
276
+ AliyunMQTT(
277
+ region_id=cloud_client.region_response.data.regionId,
278
+ product_key=cloud_client.aep_response.data.productKey,
279
+ device_name=cloud_client.aep_response.data.deviceName,
280
+ device_secret=cloud_client.aep_response.data.deviceSecret,
281
+ iot_token=cloud_client.session_by_authcode_response.data.iotToken,
282
+ client_id=cloud_client.client_id,
283
+ cloud_client=cloud_client,
284
+ ),
285
+ cloud_client,
286
+ )
287
+ self.mqtt_list[f"{account}_aliyun"] = mammotion_cloud
288
+ self.add_cloud_devices(mammotion_cloud)
289
+
290
+ await loop.run_in_executor(None, self.mqtt_list[f"{account}_aliyun"].connect_async)
291
+ if len(mammotion_http.device_records.records) != 0:
292
+ mammotion_cloud = MammotionCloud(
293
+ MammotionMQTT(
294
+ records=mammotion_http.device_records.records,
295
+ mammotion_http=mammotion_http,
296
+ mqtt_connection=mammotion_http.mqtt_credentials,
297
+ ),
298
+ cloud_client,
299
+ )
300
+ self.mqtt_list[f"{account}_mammotion"] = mammotion_cloud
301
+ self.add_mammotion_devices(mammotion_cloud, mammotion_http.device_records.records)
302
+
303
+ await loop.run_in_executor(None, self.mqtt_list[f"{account}_mammotion"].connect_async)
304
+
305
+ def add_mammotion_devices(self, mqtt_client: MammotionCloud, devices: list[DeviceRecord]) -> None:
306
+ """Add devices from mammotion cloud."""
307
+ for device in devices:
308
+ if device.device_name.startswith(("Luba-", "Yuka-")):
309
+ has_device = self.device_manager.has_device(device.device_name)
310
+ if has_device:
311
+ mower_device = self.device_manager.get_device(device.device_name)
312
+ if mower_device.cloud is None:
313
+ mower_device.add_cloud(mqtt=mqtt_client)
314
+ else:
315
+ mower_device.replace_mqtt(mqtt_client)
230
316
 
231
- await loop.run_in_executor(None, self.mqtt_list[account].connect_async)
317
+ else:
318
+ cloud_device_shim = Device(
319
+ gmt_modified=0,
320
+ product_name="",
321
+ status=0,
322
+ net_type="NET_WIFI",
323
+ is_edge_gateway=False,
324
+ category_name="",
325
+ owned=1,
326
+ identity_alias="UNKNOW",
327
+ thing_type="DEVICE",
328
+ identity_id=device.identity_id,
329
+ device_name=device.device_name,
330
+ product_key=device.product_key,
331
+ iot_id=device.iot_id,
332
+ bind_time=device.bind_time,
333
+ node_type="DEVICE",
334
+ category_key="LawnMower",
335
+ )
336
+
337
+ mixed_device = MammotionMowerDeviceManager(
338
+ name=device.device_name,
339
+ iot_id=device.iot_id,
340
+ cloud_client=mqtt_client.cloud_client,
341
+ cloud_device=cloud_device_shim,
342
+ mqtt=mqtt_client,
343
+ preference=ConnectionPreference.WIFI,
344
+ )
345
+ mixed_device.state.mower_state.product_key = device.product_key
346
+ self.device_manager.add_device(mixed_device)
232
347
 
233
348
  def add_cloud_devices(self, mqtt_client: MammotionCloud) -> None:
234
- """Add devices from cloud."""
349
+ """Add devices from cloud - both mowers and RTK."""
350
+ from pymammotion.mammotion.devices.rtk_manager import MammotionRTKDeviceManager
351
+
235
352
  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)
353
+ # Handle mower devices (Luba, Yuka)
354
+ if device.device_name.startswith(("Luba-", "Yuka-")):
355
+ has_device = self.device_manager.has_device(device.device_name)
356
+ if not has_device:
357
+ mixed_device = MammotionMowerDeviceManager(
358
+ name=device.device_name,
359
+ iot_id=device.iot_id,
360
+ cloud_client=mqtt_client.cloud_client,
361
+ cloud_device=device,
362
+ mqtt=mqtt_client,
363
+ preference=ConnectionPreference.WIFI,
364
+ )
365
+ mixed_device.state.mower_state.product_key = device.product_key
366
+ mixed_device.state.mower_state.model = (
367
+ device.product_name if device.product_model is None else device.product_model
368
+ )
369
+ self.device_manager.add_device(mixed_device)
370
+ else:
371
+ mower_device = self.device_manager.get_device(device.device_name)
372
+ if mower_device.cloud is None:
373
+ mower_device.add_cloud(mqtt=mqtt_client)
374
+ else:
375
+ mower_device.replace_mqtt(mqtt_client)
376
+
377
+ # Handle RTK devices
378
+ elif device.device_name.startswith(("RTK", "RBS")):
379
+ has_rtk_device = self.device_manager.has_rtk_device(device.device_name)
380
+ if not has_rtk_device:
381
+ rtk_device = MammotionRTKDeviceManager(
382
+ name=device.device_name,
383
+ iot_id=device.iot_id,
384
+ cloud_client=mqtt_client.cloud_client,
385
+ cloud_device=device,
386
+ mqtt=mqtt_client,
387
+ preference=ConnectionPreference.WIFI,
388
+ )
389
+ self.device_manager.add_rtk_device(rtk_device)
255
390
  else:
256
- mower_device.replace_mqtt(mqtt_client)
391
+ rtk_device = self.device_manager.get_rtk_device(device.device_name)
392
+ if rtk_device.cloud is None:
393
+ rtk_device.add_cloud(mqtt=mqtt_client)
394
+ else:
395
+ rtk_device.replace_mqtt(mqtt_client)
257
396
 
258
397
  def set_disconnect_strategy(self, *, disconnect: bool) -> None:
398
+ """Set disconnect strategy for all BLE devices (mowers and RTK)."""
259
399
  for device in self.device_manager.devices.values():
260
- if device.ble() is not None:
261
- ble_device: MammotionBaseBLEDevice = device.ble()
400
+ if device.ble is not None:
401
+ ble_device: MammotionMowerBLEDevice = device.ble
262
402
  ble_device.set_disconnect_strategy(disconnect=disconnect)
263
403
 
404
+ for rtk_device in self.device_manager.rtk_devices.values():
405
+ if rtk_device.ble is not None:
406
+ ble_rtk_device: MammotionRTKBLEDevice = rtk_device.ble
407
+ ble_rtk_device.set_disconnect_strategy(disconnect=disconnect)
408
+
264
409
  async def login(self, account: str, password: str) -> CloudIOTGateway:
265
410
  """Login to mammotion cloud."""
266
411
  mammotion_http = MammotionHTTP()
412
+ await mammotion_http.login_v2(account, password)
413
+ await mammotion_http.get_user_device_page()
414
+ device_list = await mammotion_http.get_user_device_list()
415
+ _LOGGER.debug("device_list: %s", device_list)
416
+ await mammotion_http.get_mqtt_credentials()
267
417
  cloud_client = CloudIOTGateway(mammotion_http)
268
- await mammotion_http.login(account, password)
269
418
  await self.connect_iot(cloud_client)
270
419
  return cloud_client
271
420
 
272
421
  @staticmethod
273
422
  async def connect_iot(cloud_client: CloudIOTGateway) -> None:
423
+ """Connect to aliyun cloud and fetch device info."""
274
424
  mammotion_http = cloud_client.mammotion_http
275
425
  country_code = mammotion_http.login_info.userInformation.domainAbbreviation
276
426
  if cloud_client.region_response is None:
@@ -282,21 +432,35 @@ class Mammotion:
282
432
  await cloud_client.list_binding_by_account()
283
433
 
284
434
  async def remove_device(self, name: str) -> None:
435
+ """Remove a mower device."""
285
436
  await self.device_manager.remove_device(name)
286
437
 
287
- def get_device_by_name(self, name: str) -> MammotionMixedDeviceManager:
438
+ async def remove_rtk_device(self, name: str) -> None:
439
+ """Remove an RTK device."""
440
+ await self.device_manager.remove_rtk_device(name)
441
+
442
+ def get_device_by_name(self, name: str) -> MammotionMowerDeviceManager:
443
+ """Get a mower device by name."""
288
444
  return self.device_manager.get_device(name)
289
445
 
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,
446
+ def get_rtk_device_by_name(self, name: str) -> MammotionRTKDeviceManager:
447
+ """Get an RTK device by name."""
448
+ return self.device_manager.get_rtk_device(name)
449
+
450
+ def get_or_create_device_by_name(
451
+ self, device: Device, mqtt_client: MammotionCloud | None, ble_device: BLEDevice | None
452
+ ) -> MammotionMowerDeviceManager:
453
+ """Get or create a mower device by name."""
454
+ if self.device_manager.has_device(device.device_name):
455
+ return self.device_manager.get_device(device.device_name)
456
+ mow_device = MammotionMowerDeviceManager(
457
+ name=device.device_name,
458
+ iot_id=device.iot_id,
459
+ cloud_client=mqtt_client.cloud_client if mqtt_client else CloudIOTGateway(MammotionHTTP()),
297
460
  mqtt=mqtt_client,
298
461
  cloud_device=device,
299
- ble_device=None,
462
+ ble_device=ble_device,
463
+ preference=ConnectionPreference.WIFI if mqtt_client else ConnectionPreference.BLUETOOTH,
300
464
  )
301
465
  self.device_manager.add_device(mow_device)
302
466
  return mow_device
@@ -305,10 +469,10 @@ class Mammotion:
305
469
  """Send a command to the device."""
306
470
  device = self.get_device_by_name(name)
307
471
  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)
472
+ if device.preference is ConnectionPreference.BLUETOOTH and device.ble:
473
+ return await device.ble.command(key)
474
+ if device.preference is ConnectionPreference.WIFI and device.cloud:
475
+ return await device.cloud.command(key)
312
476
  # TODO work with both with EITHER
313
477
  return None
314
478
 
@@ -316,34 +480,26 @@ class Mammotion:
316
480
  """Send a command with args to the device."""
317
481
  device = self.get_device_by_name(name)
318
482
  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)
483
+ if device.preference is ConnectionPreference.BLUETOOTH and device.ble:
484
+ return await device.ble.command(key, **kwargs)
331
485
  if device.preference is ConnectionPreference.WIFI:
332
- return await device.cloud().start_sync(retry)
486
+ return await device.cloud.command(key, **kwargs)
333
487
  # TODO work with both with EITHER
334
488
  return None
335
489
 
336
490
  async def start_map_sync(self, name: str):
491
+ """Start map sync."""
337
492
  device = self.get_device_by_name(name)
338
493
  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()
494
+ if device.preference is ConnectionPreference.BLUETOOTH and device.ble:
495
+ return await device.ble.start_map_sync()
496
+ if device.preference is ConnectionPreference.WIFI and device.cloud:
497
+ return await device.cloud.start_map_sync()
343
498
  # TODO work with both with EITHER
344
499
  return None
345
500
 
346
501
  async def get_stream_subscription(self, name: str, iot_id: str) -> Response[StreamSubscriptionResponse] | Any:
502
+ """Get stream subscription."""
347
503
  device = self.get_device_by_name(name)
348
504
  if DeviceType.is_mini_or_x_series(name):
349
505
  _stream_response = await device.mammotion_http.get_stream_subscription_mini_or_x_series(
@@ -357,6 +513,7 @@ class Mammotion:
357
513
  return _stream_response
358
514
 
359
515
  async def get_video_resource(self, name: str, iot_id: str) -> Response[VideoResourceResponse] | None:
516
+ """Get video resource."""
360
517
  device = self.get_device_by_name(name)
361
518
 
362
519
  if DeviceType.is_mini_or_x_series(name):
@@ -366,6 +523,7 @@ class Mammotion:
366
523
  return None
367
524
 
368
525
  def mower(self, name: str) -> MowingDevice | None:
526
+ """Get a mower device by name."""
369
527
  device = self.get_device_by_name(name)
370
528
  if device:
371
529
  return device.state