pymammotion 0.4.0a2__py3-none-any.whl → 0.5.51__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pymammotion might be problematic. Click here for more details.

Files changed (133) hide show
  1. pymammotion/__init__.py +5 -4
  2. pymammotion/aliyun/client.py +235 -0
  3. pymammotion/aliyun/cloud_gateway.py +312 -64
  4. pymammotion/aliyun/model/aep_response.py +1 -2
  5. pymammotion/aliyun/model/dev_by_account_response.py +170 -23
  6. pymammotion/aliyun/model/login_by_oauth_response.py +2 -3
  7. pymammotion/aliyun/model/regions_response.py +3 -3
  8. pymammotion/aliyun/model/session_by_authcode_response.py +2 -2
  9. pymammotion/aliyun/model/thing_response.py +12 -0
  10. pymammotion/aliyun/regions.py +62 -0
  11. pymammotion/aliyun/tea/core.py +297 -0
  12. pymammotion/bluetooth/ble.py +7 -9
  13. pymammotion/bluetooth/ble_message.py +10 -14
  14. pymammotion/const.py +3 -0
  15. pymammotion/data/model/__init__.py +1 -2
  16. pymammotion/data/model/device.py +95 -27
  17. pymammotion/data/model/device_config.py +4 -4
  18. pymammotion/data/model/device_info.py +35 -0
  19. pymammotion/data/model/device_limits.py +10 -10
  20. pymammotion/data/model/enums.py +12 -2
  21. pymammotion/data/model/errors.py +12 -0
  22. pymammotion/data/model/events.py +14 -0
  23. pymammotion/data/model/generate_geojson.py +521 -0
  24. pymammotion/data/model/generate_route_information.py +2 -2
  25. pymammotion/data/model/hash_list.py +370 -57
  26. pymammotion/data/model/location.py +4 -4
  27. pymammotion/data/model/mowing_modes.py +17 -1
  28. pymammotion/data/model/raw_data.py +2 -10
  29. pymammotion/data/model/region_data.py +10 -11
  30. pymammotion/data/model/report_info.py +31 -5
  31. pymammotion/data/model/work.py +27 -0
  32. pymammotion/data/mower_state_manager.py +316 -0
  33. pymammotion/data/mqtt/event.py +73 -28
  34. pymammotion/data/mqtt/mammotion_properties.py +257 -0
  35. pymammotion/data/mqtt/properties.py +93 -78
  36. pymammotion/data/mqtt/status.py +18 -17
  37. pymammotion/event/event.py +27 -6
  38. pymammotion/homeassistant/__init__.py +3 -0
  39. pymammotion/homeassistant/mower_api.py +484 -0
  40. pymammotion/homeassistant/rtk_api.py +54 -0
  41. pymammotion/http/encryption.py +5 -6
  42. pymammotion/http/http.py +574 -28
  43. pymammotion/http/model/__init__.py +0 -0
  44. pymammotion/{aliyun/model/stream_subscription_response.py → http/model/camera_stream.py} +14 -2
  45. pymammotion/http/model/http.py +129 -4
  46. pymammotion/http/model/response_factory.py +61 -0
  47. pymammotion/http/model/rtk.py +16 -0
  48. pymammotion/mammotion/commands/abstract_message.py +7 -5
  49. pymammotion/mammotion/commands/mammotion_command.py +30 -1
  50. pymammotion/mammotion/commands/messages/basestation.py +43 -0
  51. pymammotion/mammotion/commands/messages/driver.py +61 -29
  52. pymammotion/mammotion/commands/messages/media.py +68 -15
  53. pymammotion/mammotion/commands/messages/navigation.py +61 -25
  54. pymammotion/mammotion/commands/messages/network.py +17 -23
  55. pymammotion/mammotion/commands/messages/ota.py +18 -18
  56. pymammotion/mammotion/commands/messages/system.py +32 -49
  57. pymammotion/mammotion/commands/messages/video.py +15 -16
  58. pymammotion/mammotion/devices/__init__.py +27 -3
  59. pymammotion/mammotion/devices/base.py +40 -131
  60. pymammotion/mammotion/devices/mammotion.py +436 -201
  61. pymammotion/mammotion/devices/mammotion_bluetooth.py +57 -47
  62. pymammotion/mammotion/devices/mammotion_cloud.py +134 -105
  63. pymammotion/mammotion/devices/mammotion_mower_ble.py +49 -0
  64. pymammotion/mammotion/devices/mammotion_mower_cloud.py +39 -0
  65. pymammotion/mammotion/devices/managers/managers.py +81 -0
  66. pymammotion/mammotion/devices/mower_device.py +124 -0
  67. pymammotion/mammotion/devices/mower_manager.py +107 -0
  68. pymammotion/mammotion/devices/rtk_ble.py +89 -0
  69. pymammotion/mammotion/devices/rtk_cloud.py +113 -0
  70. pymammotion/mammotion/devices/rtk_device.py +50 -0
  71. pymammotion/mammotion/devices/rtk_manager.py +122 -0
  72. pymammotion/mqtt/__init__.py +2 -1
  73. pymammotion/mqtt/aliyun_mqtt.py +232 -0
  74. pymammotion/mqtt/linkkit/__init__.py +5 -0
  75. pymammotion/mqtt/linkkit/h2client.py +585 -0
  76. pymammotion/mqtt/linkkit/linkkit.py +3023 -0
  77. pymammotion/mqtt/mammotion_mqtt.py +176 -169
  78. pymammotion/mqtt/mqtt_models.py +66 -0
  79. pymammotion/proto/__init__.py +4839 -4
  80. pymammotion/proto/basestation.proto +8 -0
  81. pymammotion/proto/basestation_pb2.py +11 -9
  82. pymammotion/proto/basestation_pb2.pyi +16 -2
  83. pymammotion/proto/dev_net.proto +79 -55
  84. pymammotion/proto/dev_net_pb2.py +60 -56
  85. pymammotion/proto/dev_net_pb2.pyi +49 -6
  86. pymammotion/proto/luba_msg.proto +2 -1
  87. pymammotion/proto/luba_msg_pb2.py +6 -6
  88. pymammotion/proto/luba_msg_pb2.pyi +1 -0
  89. pymammotion/proto/luba_mul.proto +62 -1
  90. pymammotion/proto/luba_mul_pb2.py +38 -22
  91. pymammotion/proto/luba_mul_pb2.pyi +94 -7
  92. pymammotion/proto/mctrl_driver.proto +44 -4
  93. pymammotion/proto/mctrl_driver_pb2.py +26 -14
  94. pymammotion/proto/mctrl_driver_pb2.pyi +66 -11
  95. pymammotion/proto/mctrl_nav.proto +93 -52
  96. pymammotion/proto/mctrl_nav_pb2.py +75 -67
  97. pymammotion/proto/mctrl_nav_pb2.pyi +142 -56
  98. pymammotion/proto/mctrl_ota.proto +40 -2
  99. pymammotion/proto/mctrl_ota_pb2.py +23 -13
  100. pymammotion/proto/mctrl_ota_pb2.pyi +67 -4
  101. pymammotion/proto/mctrl_pept.proto +8 -3
  102. pymammotion/proto/mctrl_pept_pb2.py +8 -6
  103. pymammotion/proto/mctrl_pept_pb2.pyi +14 -6
  104. pymammotion/proto/mctrl_sys.proto +325 -86
  105. pymammotion/proto/mctrl_sys_pb2.py +162 -98
  106. pymammotion/proto/mctrl_sys_pb2.pyi +451 -25
  107. pymammotion/proto/message_pool.py +3 -0
  108. pymammotion/proto/py.typed +0 -0
  109. pymammotion/utility/constant/device_constant.py +29 -5
  110. pymammotion/utility/datatype_converter.py +13 -12
  111. pymammotion/utility/device_config.py +522 -130
  112. pymammotion/utility/device_type.py +218 -21
  113. pymammotion/utility/map.py +238 -51
  114. pymammotion/utility/mur_mur_hash.py +159 -0
  115. {pymammotion-0.4.0a2.dist-info → pymammotion-0.5.51.dist-info}/METADATA +26 -31
  116. pymammotion-0.5.51.dist-info/RECORD +152 -0
  117. {pymammotion-0.4.0a2.dist-info → pymammotion-0.5.51.dist-info}/WHEEL +1 -1
  118. pymammotion/aliyun/cloud_service.py +0 -65
  119. pymammotion/data/model/plan.py +0 -58
  120. pymammotion/data/state_manager.py +0 -129
  121. pymammotion/proto/basestation.py +0 -59
  122. pymammotion/proto/common.py +0 -12
  123. pymammotion/proto/dev_net.py +0 -381
  124. pymammotion/proto/luba_msg.py +0 -81
  125. pymammotion/proto/luba_mul.py +0 -76
  126. pymammotion/proto/mctrl_driver.py +0 -100
  127. pymammotion/proto/mctrl_nav.py +0 -664
  128. pymammotion/proto/mctrl_ota.py +0 -48
  129. pymammotion/proto/mctrl_pept.py +0 -41
  130. pymammotion/proto/mctrl_sys.py +0 -574
  131. pymammotion-0.4.0a2.dist-info/RECORD +0 -131
  132. /pymammotion/http/{_init_.py → __init__.py} +0 -0
  133. {pymammotion-0.4.0a2.dist-info → pymammotion-0.5.51.dist-info/licenses}/LICENSE +0 -0
@@ -4,159 +4,155 @@ from __future__ import annotations
4
4
 
5
5
  import asyncio
6
6
  import logging
7
- from enum import Enum
8
- from typing import Any
7
+ from typing import TYPE_CHECKING, Any
9
8
 
10
- from bleak.backends.device import BLEDevice
9
+ from bleak import BLEDevice
11
10
 
11
+ from pymammotion import MammotionMQTT
12
12
  from pymammotion.aliyun.cloud_gateway import CloudIOTGateway
13
13
  from pymammotion.aliyun.model.dev_by_account_response import Device
14
- from pymammotion.data.model.account import Credentials
15
14
  from pymammotion.data.model.device import MowingDevice
16
- from pymammotion.data.state_manager import StateManager
15
+ from pymammotion.data.model.enums import ConnectionPreference
17
16
  from pymammotion.http.http import MammotionHTTP
18
- from pymammotion.mammotion.devices.mammotion_bluetooth import MammotionBaseBLEDevice
19
- from pymammotion.mammotion.devices.mammotion_cloud import MammotionBaseCloudDevice, MammotionCloud
20
- from pymammotion.mqtt import MammotionMQTT
17
+ from pymammotion.http.model.camera_stream import StreamSubscriptionResponse, VideoResourceResponse
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
25
+ from pymammotion.utility.device_type import DeviceType
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
21
30
 
22
31
  TIMEOUT_CLOUD_RESPONSE = 10
23
32
 
24
33
  _LOGGER = logging.getLogger(__name__)
25
34
 
26
35
 
27
- class ConnectionPreference(Enum):
28
- """Enum for connection preference."""
29
-
30
- EITHER = 0
31
- WIFI = 1
32
- BLUETOOTH = 2
33
-
34
-
35
- class MammotionMixedDeviceManager:
36
- preference: ConnectionPreference
37
-
38
- def __init__(
39
- self,
40
- name: str,
41
- cloud_device: Device | None = None,
42
- ble_device: BLEDevice | None = None,
43
- mqtt: MammotionCloud | None = None,
44
- preference: ConnectionPreference = ConnectionPreference.BLUETOOTH,
45
- ) -> None:
46
- self.name = name
47
- self._state_manager = StateManager(MowingDevice())
48
- self._ble_device: MammotionBaseBLEDevice | None = None
49
- self._cloud_device: MammotionBaseCloudDevice | None = None
50
- self.add_ble(ble_device)
51
- self.add_cloud(cloud_device, mqtt)
52
- self.preference = preference
36
+ class MammotionDeviceManager:
37
+ """Manage devices - both mowers and RTK."""
53
38
 
54
- @property
55
- def mower_state(self):
56
- return self._state_manager.get_device()
39
+ def __init__(self) -> None:
40
+ self.devices: dict[str, MammotionMowerDeviceManager] = {}
41
+ self.rtk_devices: dict[str, MammotionRTKDeviceManager] = {}
57
42
 
58
- @mower_state.setter
59
- def mower_state(self, value: MowingDevice) -> None:
60
- self._state_manager.set_device(value)
43
+ def _should_disconnect_mqtt(self, device_for_removal: AbstractDeviceManager) -> bool:
44
+ """Check if MQTT connection should be disconnected.
61
45
 
62
- def ble(self) -> MammotionBaseBLEDevice | None:
63
- return self._ble_device
46
+ Returns True if no other devices share the same MQTT connection.
47
+ """
48
+ if not device_for_removal.cloud:
49
+ return False
64
50
 
65
- def cloud(self) -> MammotionBaseCloudDevice | None:
66
- return self._cloud_device
51
+ mqtt_to_check = device_for_removal.cloud.mqtt
67
52
 
68
- def has_queued_commands(self) -> bool:
69
- if self.has_cloud() and self.preference == ConnectionPreference.WIFI:
70
- return not self.cloud().mqtt.command_queue.empty()
71
- else:
72
- return not self.ble().command_queue.empty()
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
+ }
73
59
 
74
- def add_ble(self, ble_device: BLEDevice) -> None:
75
- if ble_device is not None:
76
- self._ble_device = MammotionBaseBLEDevice(state_manager=self._state_manager, device=ble_device)
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
+ }
67
+ )
77
68
 
78
- def add_cloud(self, cloud_device: Device | None = None, mqtt: MammotionCloud | None = None) -> None:
79
- if cloud_device is not None:
80
- self._cloud_device = MammotionBaseCloudDevice(
81
- mqtt, cloud_device=cloud_device, state_manager=self._state_manager
82
- )
69
+ return len(shared_devices) == 0
83
70
 
84
- def replace_cloud(self, cloud_device: MammotionBaseCloudDevice) -> None:
85
- self._cloud_device = cloud_device
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)
86
81
 
87
- def replace_ble(self, ble_device: MammotionBaseBLEDevice) -> None:
88
- self._ble_device = ble_device
82
+ def add_rtk_device(self, rtk_device: MammotionRTKDeviceManager) -> None:
83
+ """Add an RTK device."""
89
84
 
90
- def replace_mqtt(self, mqtt: MammotionCloud) -> None:
91
- device = self._cloud_device.device
92
- self._cloud_device = MammotionBaseCloudDevice(mqtt, cloud_device=device, state_manager=self._state_manager)
85
+ exists: MammotionRTKDeviceManager | None = self.rtk_devices.get(rtk_device.name)
86
+ if exists is None:
87
+ self.rtk_devices[rtk_device.name] = rtk_device
88
+ return
89
+ if rtk_device.cloud:
90
+ exists.replace_cloud(rtk_device.cloud)
91
+ if rtk_device.ble:
92
+ exists.replace_ble(rtk_device.ble)
93
+
94
+ def has_device(self, mammotion_device_name: str) -> bool:
95
+ """Check if a mower device exists."""
96
+ if self.devices.get(mammotion_device_name, None) is not None:
97
+ return True
98
+ return False
99
+
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."""
108
+ return self.devices[mammotion_device_name]
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]
93
113
 
94
- def has_cloud(self) -> bool:
95
- return self._cloud_device is not None
114
+ async def remove_device(self, name: str) -> None:
115
+ """Remove a mower device."""
116
+ if self.devices.get(name):
117
+ device_for_removal = self.devices.pop(name)
118
+ loop = asyncio.get_running_loop()
96
119
 
97
- def has_ble(self) -> bool:
98
- return self._ble_device is not None
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()
99
124
 
125
+ if device_for_removal.ble:
126
+ await device_for_removal.ble.stop()
100
127
 
101
- class MammotionDeviceManager:
102
- devices: dict[str, MammotionMixedDeviceManager] = {}
128
+ del device_for_removal
103
129
 
104
- def add_device(self, mammotion_device: MammotionMixedDeviceManager) -> None:
105
- exists: MammotionMixedDeviceManager | None = self.devices.get(mammotion_device.name)
106
- if exists is None:
107
- self.devices[mammotion_device.name] = mammotion_device
108
- return
109
- if mammotion_device.has_cloud():
110
- exists.replace_cloud(mammotion_device.cloud())
111
- if mammotion_device.has_ble():
112
- exists.replace_ble(mammotion_device.ble())
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()
113
135
 
114
- def get_device(self, mammotion_device_name: str) -> MammotionMixedDeviceManager:
115
- return self.devices.get(mammotion_device_name)
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()
116
140
 
117
- async def remove_device(self, name: str) -> None:
118
- device_for_removal = self.devices.pop(name)
119
- loop = asyncio.get_running_loop()
120
- if device_for_removal.has_cloud():
121
- should_disconnect = {
122
- device
123
- for key, device in self.devices.items()
124
- if device.cloud() is not None and device.cloud().mqtt == device_for_removal.cloud().mqtt
125
- }
126
- if len(should_disconnect) == 0:
127
- await loop.run_in_executor(None, device_for_removal.cloud().mqtt.disconnect)
128
- await device_for_removal.cloud().stop()
129
- if device_for_removal.has_ble():
130
- await device_for_removal.ble().stop()
131
-
132
- del device_for_removal
133
-
134
-
135
- async def create_devices(
136
- ble_device: BLEDevice,
137
- cloud_credentials: Credentials | None = None,
138
- preference: ConnectionPreference = ConnectionPreference.BLUETOOTH,
139
- ):
140
- mammotion = Mammotion()
141
- mammotion.add_ble_device(ble_device, preference)
142
-
143
- if cloud_credentials and preference == ConnectionPreference.EITHER or preference == ConnectionPreference.WIFI:
144
- await mammotion.login_and_initiate_cloud(
145
- cloud_credentials.account_id or cloud_credentials.email, cloud_credentials.password
146
- )
141
+ if device_for_removal.ble:
142
+ await device_for_removal.ble.stop()
147
143
 
148
- return mammotion
144
+ del device_for_removal
149
145
 
150
146
 
151
147
  class Mammotion:
152
148
  """Represents a Mammotion account and its devices."""
153
149
 
154
150
  device_manager = MammotionDeviceManager()
155
- mqtt_list: dict[str, MammotionCloud] = dict()
156
151
 
157
- _instance: Mammotion = None
152
+ _instance: Mammotion | None = None
158
153
 
159
- def __new__(cls, *args: Any, **kwargs: Any):
154
+ def __new__(cls) -> Mammotion:
155
+ """Create a singleton."""
160
156
  if not cls._instance:
161
157
  cls._instance = super().__new__(cls)
162
158
  return cls._instance
@@ -164,143 +160,382 @@ class Mammotion:
164
160
  def __init__(self) -> None:
165
161
  """Initialize MammotionDevice."""
166
162
  self._login_lock = asyncio.Lock()
167
-
168
- def add_ble_device(
169
- self, ble_device: BLEDevice, preference: ConnectionPreference = ConnectionPreference.BLUETOOTH
170
- ) -> None:
171
- if ble_device:
172
- self.devices.add_device(
173
- MammotionMixedDeviceManager(name=ble_device.name, ble_device=ble_device, preference=preference)
174
- )
163
+ self.mqtt_list: dict[str, MammotionCloud] = {}
175
164
 
176
165
  async def login_and_initiate_cloud(self, account, password, force: bool = False) -> None:
177
166
  async with self._login_lock:
178
- exists: MammotionCloud | None = self.mqtt_list.get(account)
179
- 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:
180
170
  cloud_client = await self.login(account, password)
181
171
  await self.initiate_cloud_connection(account, cloud_client)
182
172
 
173
+ async def refresh_login(self, account: str) -> None:
174
+ """Refresh login."""
175
+ async with self._login_lock:
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:
180
+ return
181
+ mammotion_http = (
182
+ exists_aliyun.cloud_client.mammotion_http
183
+ if exists_aliyun
184
+ else exists_mammotion.cloud_client.mammotion_http
185
+ )
186
+
187
+ await mammotion_http.refresh_login()
188
+
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():
194
+ loop = asyncio.get_running_loop()
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)
259
+
183
260
  async def initiate_cloud_connection(self, account: str, cloud_client: CloudIOTGateway) -> None:
261
+ """Initiate cloud connection."""
184
262
  loop = asyncio.get_running_loop()
185
- 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"):
186
267
  if mqtt.is_connected():
187
268
  await loop.run_in_executor(None, mqtt.disconnect)
188
269
 
189
- mammotion_cloud = MammotionCloud(
190
- MammotionMQTT(
191
- region_id=cloud_client.region_response.data.regionId,
192
- product_key=cloud_client.aep_response.data.productKey,
193
- device_name=cloud_client.aep_response.data.deviceName,
194
- device_secret=cloud_client.aep_response.data.deviceSecret,
195
- iot_token=cloud_client.session_by_authcode_response.data.iotToken,
196
- client_id=cloud_client.client_id,
197
- cloud_client=cloud_client,
198
- ),
199
- cloud_client,
200
- )
201
- self.mqtt_list[account] = mammotion_cloud
202
- 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)
316
+
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
+ )
203
336
 
204
- await loop.run_in_executor(None, self.mqtt_list[account].connect_async)
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)
205
347
 
206
348
  def add_cloud_devices(self, mqtt_client: MammotionCloud) -> None:
349
+ """Add devices from cloud - both mowers and RTK."""
350
+ from pymammotion.mammotion.devices.rtk_manager import MammotionRTKDeviceManager
351
+
207
352
  for device in mqtt_client.cloud_client.devices_by_account_response.data.data:
208
- mower_device = self.devices.get_device(device.deviceName)
209
- if device.deviceName.startswith(("Luba-", "Yuka-")) and mower_device is None:
210
- self.devices.add_device(
211
- MammotionMixedDeviceManager(
212
- name=device.deviceName,
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,
213
361
  cloud_device=device,
214
362
  mqtt=mqtt_client,
215
363
  preference=ConnectionPreference.WIFI,
216
364
  )
217
- )
218
- elif device.deviceName.startswith(("Luba-", "Yuka-")) and mower_device:
219
- if mower_device.cloud() is None:
220
- mower_device.add_cloud(cloud_device=device, mqtt=mqtt_client)
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)
221
370
  else:
222
- mower_device.replace_mqtt(mqtt_client)
223
-
224
- def set_disconnect_strategy(self, disconnect: bool) -> None:
225
- for device_name, device in self.devices.devices.items():
226
- if device.ble() is not None:
227
- ble_device: MammotionBaseBLEDevice = device.ble()
228
- ble_device.set_disconnect_strategy(disconnect)
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)
390
+ else:
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)
396
+
397
+ def set_disconnect_strategy(self, *, disconnect: bool) -> None:
398
+ """Set disconnect strategy for all BLE devices (mowers and RTK)."""
399
+ for device in self.device_manager.devices.values():
400
+ if device.ble is not None:
401
+ ble_device: MammotionMowerBLEDevice = device.ble
402
+ ble_device.set_disconnect_strategy(disconnect=disconnect)
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)
229
408
 
230
409
  async def login(self, account: str, password: str) -> CloudIOTGateway:
231
410
  """Login to mammotion cloud."""
232
- cloud_client = CloudIOTGateway()
233
411
  mammotion_http = MammotionHTTP()
234
- await mammotion_http.login(account, password)
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()
417
+ cloud_client = CloudIOTGateway(mammotion_http)
418
+ await self.connect_iot(cloud_client)
419
+ return cloud_client
420
+
421
+ @staticmethod
422
+ async def connect_iot(cloud_client: CloudIOTGateway) -> None:
423
+ """Connect to aliyun cloud and fetch device info."""
424
+ mammotion_http = cloud_client.mammotion_http
235
425
  country_code = mammotion_http.login_info.userInformation.domainAbbreviation
236
- _LOGGER.debug("CountryCode: " + country_code)
237
- _LOGGER.debug("AuthCode: " + mammotion_http.login_info.authorization_code)
238
- cloud_client.set_http(mammotion_http)
239
- loop = asyncio.get_running_loop()
240
- await loop.run_in_executor(
241
- None, cloud_client.get_region, country_code, mammotion_http.login_info.authorization_code
242
- )
426
+ if cloud_client.region_response is None:
427
+ await cloud_client.get_region(country_code)
243
428
  await cloud_client.connect()
244
- await cloud_client.login_by_oauth(country_code, mammotion_http.login_info.authorization_code)
245
- await loop.run_in_executor(None, cloud_client.aep_handle)
246
- await loop.run_in_executor(None, cloud_client.session_by_auth_code)
247
-
248
- await loop.run_in_executor(None, cloud_client.list_binding_by_account)
249
- return cloud_client
429
+ await cloud_client.login_by_oauth(country_code)
430
+ await cloud_client.aep_handle()
431
+ await cloud_client.session_by_auth_code()
432
+ await cloud_client.list_binding_by_account()
250
433
 
251
434
  async def remove_device(self, name: str) -> None:
252
- await self.devices.remove_device(name)
253
-
254
- def get_device_by_name(self, name: str) -> MammotionMixedDeviceManager:
255
- return self.devices.get_device(name)
435
+ """Remove a mower device."""
436
+ await self.device_manager.remove_device(name)
437
+
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."""
444
+ return self.device_manager.get_device(name)
445
+
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()),
460
+ mqtt=mqtt_client,
461
+ cloud_device=device,
462
+ ble_device=ble_device,
463
+ preference=ConnectionPreference.WIFI if mqtt_client else ConnectionPreference.BLUETOOTH,
464
+ )
465
+ self.device_manager.add_device(mow_device)
466
+ return mow_device
256
467
 
257
468
  async def send_command(self, name: str, key: str):
258
469
  """Send a command to the device."""
259
470
  device = self.get_device_by_name(name)
260
471
  if device:
261
- if device.preference is ConnectionPreference.BLUETOOTH:
262
- return await device.ble().command(key)
263
- if device.preference is ConnectionPreference.WIFI:
264
- 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)
265
476
  # TODO work with both with EITHER
477
+ return None
266
478
 
267
479
  async def send_command_with_args(self, name: str, key: str, **kwargs: Any):
268
480
  """Send a command with args to the device."""
269
481
  device = self.get_device_by_name(name)
270
482
  if device:
271
- if device.preference is ConnectionPreference.BLUETOOTH:
272
- return await device.ble().command(key, **kwargs)
273
- if device.preference is ConnectionPreference.WIFI:
274
- return await device.cloud().command(key, **kwargs)
483
+ if device.preference is ConnectionPreference.BLUETOOTH and device.ble:
484
+ return await device.ble.command(key, **kwargs)
485
+ if device.preference is ConnectionPreference.WIFI and device.cloud:
486
+ return await device.cloud.command(key, **kwargs)
275
487
  # TODO work with both with EITHER
488
+ return None
276
489
 
277
- async def start_sync(self, name: str, retry: int):
490
+ async def start_map_sync(self, name: str) -> None:
491
+ """Start map sync."""
278
492
  device = self.get_device_by_name(name)
279
493
  if device:
280
- if device.preference is ConnectionPreference.BLUETOOTH:
281
- return await device.ble().start_sync(retry)
282
- if device.preference is ConnectionPreference.WIFI:
283
- return await device.cloud().start_sync(retry)
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()
284
498
  # TODO work with both with EITHER
499
+ return None
285
500
 
286
- async def start_map_sync(self, name: str):
501
+ async def start_schedule_sync(self, name: str) -> None:
502
+ """Start map sync."""
287
503
  device = self.get_device_by_name(name)
288
504
  if device:
289
- if device.preference is ConnectionPreference.BLUETOOTH:
290
- return await device.ble().start_map_sync()
291
- if device.preference is ConnectionPreference.WIFI:
292
- return await device.cloud().start_map_sync()
505
+ if device.preference is ConnectionPreference.BLUETOOTH and device.ble:
506
+ return await device.ble.start_schedule_sync()
507
+ if device.preference is ConnectionPreference.WIFI and device.cloud:
508
+ return await device.cloud.start_schedule_sync()
293
509
  # TODO work with both with EITHER
510
+ return None
511
+
512
+ async def get_stream_subscription(self, name: str, iot_id: str) -> Response[StreamSubscriptionResponse] | Any:
513
+ """Get stream subscription."""
514
+ device = self.get_device_by_name(name)
515
+ if DeviceType.is_mini_or_x_series(name):
516
+ _stream_response = await device.mammotion_http.get_stream_subscription_mini_or_x_series(
517
+ iot_id, DeviceType.is_yuka(name) and not DeviceType.is_yuka_mini(name)
518
+ )
519
+ _LOGGER.debug(_stream_response)
520
+ return _stream_response
521
+ else:
522
+ _stream_response = await device.mammotion_http.get_stream_subscription(iot_id)
523
+ _LOGGER.debug(_stream_response)
524
+ return _stream_response
294
525
 
295
- async def get_stream_subscription(self, name: str):
526
+ async def get_video_resource(self, name: str, iot_id: str) -> Response[VideoResourceResponse] | None:
527
+ """Get video resource."""
296
528
  device = self.get_device_by_name(name)
297
- if device.preference is ConnectionPreference.WIFI:
298
- if device.has_cloud():
299
- _stream_response = await device.cloud().mqtt.cloud_client.get_stream_subscription(device.cloud().iot_id)
300
- _LOGGER.debug(_stream_response)
301
- return _stream_response
529
+
530
+ if DeviceType.is_mini_or_x_series(name):
531
+ _video_resource_response = await device.mammotion_http.get_video_resource(iot_id)
532
+ _LOGGER.debug(_video_resource_response)
533
+ return _video_resource_response
534
+ return None
302
535
 
303
536
  def mower(self, name: str) -> MowingDevice | None:
537
+ """Get a mower device by name."""
304
538
  device = self.get_device_by_name(name)
305
539
  if device:
306
- return device.mower_state
540
+ return device.state
541
+ return None