pymammotion 0.2.62__py3-none-any.whl → 0.5.51__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (135) hide show
  1. pymammotion/__init__.py +9 -6
  2. pymammotion/aliyun/client.py +235 -0
  3. pymammotion/aliyun/cloud_gateway.py +320 -69
  4. pymammotion/aliyun/model/aep_response.py +1 -2
  5. pymammotion/aliyun/model/dev_by_account_response.py +170 -23
  6. pymammotion/aliyun/model/login_by_oauth_response.py +2 -3
  7. pymammotion/aliyun/model/regions_response.py +3 -3
  8. pymammotion/aliyun/model/session_by_authcode_response.py +2 -2
  9. pymammotion/aliyun/model/thing_response.py +12 -0
  10. pymammotion/aliyun/regions.py +62 -0
  11. pymammotion/aliyun/tea/core.py +297 -0
  12. pymammotion/bluetooth/ble.py +11 -15
  13. pymammotion/bluetooth/ble_message.py +389 -106
  14. pymammotion/bluetooth/model/atomic_integer.py +54 -0
  15. pymammotion/const.py +3 -0
  16. pymammotion/data/model/__init__.py +1 -2
  17. pymammotion/data/model/device.py +92 -240
  18. pymammotion/data/model/device_config.py +10 -24
  19. pymammotion/data/model/device_info.py +35 -0
  20. pymammotion/data/model/device_limits.py +49 -0
  21. pymammotion/data/model/enums.py +12 -2
  22. pymammotion/data/model/errors.py +12 -0
  23. pymammotion/data/model/events.py +14 -0
  24. pymammotion/data/model/generate_geojson.py +521 -0
  25. pymammotion/data/model/generate_route_information.py +3 -4
  26. pymammotion/data/model/hash_list.py +384 -48
  27. pymammotion/data/model/location.py +4 -4
  28. pymammotion/data/model/mowing_modes.py +24 -1
  29. pymammotion/data/model/raw_data.py +215 -0
  30. pymammotion/data/model/region_data.py +10 -11
  31. pymammotion/data/model/report_info.py +62 -6
  32. pymammotion/data/model/work.py +27 -0
  33. pymammotion/data/mower_state_manager.py +316 -0
  34. pymammotion/data/mqtt/event.py +73 -28
  35. pymammotion/data/mqtt/mammotion_properties.py +257 -0
  36. pymammotion/data/mqtt/properties.py +93 -78
  37. pymammotion/data/mqtt/status.py +18 -17
  38. pymammotion/event/event.py +32 -8
  39. pymammotion/homeassistant/__init__.py +3 -0
  40. pymammotion/homeassistant/mower_api.py +484 -0
  41. pymammotion/homeassistant/rtk_api.py +54 -0
  42. pymammotion/http/__init__.py +0 -0
  43. pymammotion/http/encryption.py +220 -0
  44. pymammotion/http/http.py +652 -44
  45. pymammotion/http/model/__init__.py +0 -0
  46. pymammotion/{aliyun/model/stream_subscription_response.py → http/model/camera_stream.py} +14 -2
  47. pymammotion/http/model/http.py +160 -9
  48. pymammotion/http/model/response_factory.py +61 -0
  49. pymammotion/http/model/rtk.py +16 -0
  50. pymammotion/mammotion/commands/abstract_message.py +7 -5
  51. pymammotion/mammotion/commands/mammotion_command.py +32 -3
  52. pymammotion/mammotion/commands/messages/basestation.py +43 -0
  53. pymammotion/mammotion/commands/messages/driver.py +61 -29
  54. pymammotion/mammotion/commands/messages/media.py +68 -15
  55. pymammotion/mammotion/commands/messages/navigation.py +61 -25
  56. pymammotion/mammotion/commands/messages/network.py +93 -100
  57. pymammotion/mammotion/commands/messages/ota.py +18 -18
  58. pymammotion/mammotion/commands/messages/system.py +97 -72
  59. pymammotion/mammotion/commands/messages/video.py +17 -12
  60. pymammotion/mammotion/devices/__init__.py +27 -3
  61. pymammotion/mammotion/devices/base.py +50 -127
  62. pymammotion/mammotion/devices/mammotion.py +447 -212
  63. pymammotion/mammotion/devices/mammotion_bluetooth.py +105 -60
  64. pymammotion/mammotion/devices/mammotion_cloud.py +157 -105
  65. pymammotion/mammotion/devices/mammotion_mower_ble.py +49 -0
  66. pymammotion/mammotion/devices/mammotion_mower_cloud.py +39 -0
  67. pymammotion/mammotion/devices/managers/managers.py +81 -0
  68. pymammotion/mammotion/devices/mower_device.py +124 -0
  69. pymammotion/mammotion/devices/mower_manager.py +107 -0
  70. pymammotion/mammotion/devices/rtk_ble.py +89 -0
  71. pymammotion/mammotion/devices/rtk_cloud.py +113 -0
  72. pymammotion/mammotion/devices/rtk_device.py +50 -0
  73. pymammotion/mammotion/devices/rtk_manager.py +122 -0
  74. pymammotion/mqtt/__init__.py +2 -1
  75. pymammotion/mqtt/aliyun_mqtt.py +232 -0
  76. pymammotion/mqtt/linkkit/__init__.py +5 -0
  77. pymammotion/mqtt/linkkit/h2client.py +585 -0
  78. pymammotion/mqtt/linkkit/linkkit.py +3023 -0
  79. pymammotion/mqtt/mammotion_mqtt.py +176 -169
  80. pymammotion/mqtt/mqtt_models.py +66 -0
  81. pymammotion/proto/__init__.py +4839 -4
  82. pymammotion/proto/basestation.proto +8 -0
  83. pymammotion/proto/basestation_pb2.py +11 -9
  84. pymammotion/proto/basestation_pb2.pyi +16 -2
  85. pymammotion/proto/dev_net.proto +79 -55
  86. pymammotion/proto/dev_net_pb2.py +60 -56
  87. pymammotion/proto/dev_net_pb2.pyi +49 -6
  88. pymammotion/proto/luba_msg.proto +2 -1
  89. pymammotion/proto/luba_msg_pb2.py +6 -6
  90. pymammotion/proto/luba_msg_pb2.pyi +1 -0
  91. pymammotion/proto/luba_mul.proto +62 -1
  92. pymammotion/proto/luba_mul_pb2.py +38 -22
  93. pymammotion/proto/luba_mul_pb2.pyi +94 -7
  94. pymammotion/proto/mctrl_driver.proto +44 -4
  95. pymammotion/proto/mctrl_driver_pb2.py +26 -14
  96. pymammotion/proto/mctrl_driver_pb2.pyi +66 -11
  97. pymammotion/proto/mctrl_nav.proto +97 -51
  98. pymammotion/proto/mctrl_nav_pb2.py +75 -67
  99. pymammotion/proto/mctrl_nav_pb2.pyi +142 -56
  100. pymammotion/proto/mctrl_ota.proto +40 -2
  101. pymammotion/proto/mctrl_ota_pb2.py +23 -13
  102. pymammotion/proto/mctrl_ota_pb2.pyi +67 -4
  103. pymammotion/proto/mctrl_pept.proto +8 -3
  104. pymammotion/proto/mctrl_pept_pb2.py +8 -6
  105. pymammotion/proto/mctrl_pept_pb2.pyi +14 -6
  106. pymammotion/proto/mctrl_sys.proto +325 -86
  107. pymammotion/proto/mctrl_sys_pb2.py +162 -98
  108. pymammotion/proto/mctrl_sys_pb2.pyi +451 -25
  109. pymammotion/proto/message_pool.py +3 -0
  110. pymammotion/proto/py.typed +0 -0
  111. pymammotion/utility/constant/device_constant.py +65 -21
  112. pymammotion/utility/datatype_converter.py +13 -12
  113. pymammotion/utility/device_config.py +755 -0
  114. pymammotion/utility/device_type.py +218 -21
  115. pymammotion/utility/map.py +238 -51
  116. pymammotion/utility/mur_mur_hash.py +159 -0
  117. {pymammotion-0.2.62.dist-info → pymammotion-0.5.51.dist-info}/METADATA +27 -31
  118. pymammotion-0.5.51.dist-info/RECORD +152 -0
  119. {pymammotion-0.2.62.dist-info → pymammotion-0.5.51.dist-info}/WHEEL +1 -1
  120. pymammotion/aliyun/cloud_service.py +0 -65
  121. pymammotion/data/model/plan.py +0 -58
  122. pymammotion/data/state_manager.py +0 -130
  123. pymammotion/proto/basestation.py +0 -59
  124. pymammotion/proto/common.py +0 -12
  125. pymammotion/proto/dev_net.py +0 -381
  126. pymammotion/proto/luba_msg.py +0 -81
  127. pymammotion/proto/luba_mul.py +0 -76
  128. pymammotion/proto/mctrl_driver.py +0 -100
  129. pymammotion/proto/mctrl_nav.py +0 -660
  130. pymammotion/proto/mctrl_ota.py +0 -48
  131. pymammotion/proto/mctrl_pept.py +0 -41
  132. pymammotion/proto/mctrl_sys.py +0 -574
  133. pymammotion-0.2.62.dist-info/RECORD +0 -125
  134. /pymammotion/{http/_init_.py → bluetooth/model/__init__.py} +0 -0
  135. {pymammotion-0.2.62.dist-info → pymammotion-0.5.51.dist-info/licenses}/LICENSE +0 -0
@@ -4,160 +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.http.http import connect_http
17
- from pymammotion.mammotion.devices.mammotion_bluetooth import MammotionBaseBLEDevice
18
- from pymammotion.mammotion.devices.mammotion_cloud import MammotionBaseCloudDevice, MammotionCloud
19
- from pymammotion.mqtt import MammotionMQTT
15
+ from pymammotion.data.model.enums import ConnectionPreference
16
+ from pymammotion.http.http import MammotionHTTP
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
20
30
 
21
31
  TIMEOUT_CLOUD_RESPONSE = 10
22
32
 
23
33
  _LOGGER = logging.getLogger(__name__)
24
34
 
25
35
 
26
- class ConnectionPreference(Enum):
27
- """Enum for connection preference."""
28
-
29
- EITHER = 0
30
- WIFI = 1
31
- BLUETOOTH = 2
32
-
33
-
34
- class MammotionMixedDeviceManager:
35
- _ble_device: MammotionBaseBLEDevice | None = None
36
- _cloud_device: MammotionBaseCloudDevice | None = None
37
- preference: ConnectionPreference
38
-
39
- def __init__(
40
- self,
41
- name: str,
42
- cloud_device: Device | None = None,
43
- ble_device: BLEDevice | None = None,
44
- mqtt: MammotionCloud | None = None,
45
- preference: ConnectionPreference = ConnectionPreference.BLUETOOTH,
46
- ) -> None:
47
- self.name = name
48
- self._mower_state = MowingDevice()
49
- self.add_ble(ble_device)
50
- self.add_cloud(cloud_device, mqtt)
51
- self.preference = preference
52
-
53
- @property
54
- def mower_state(self):
55
- return self._mower_state
56
-
57
- @mower_state.setter
58
- def mower_state(self, value: MowingDevice) -> None:
59
- if self._cloud_device:
60
- self._cloud_device.state_manager.set_device(value)
61
- if self._ble_device:
62
- self._ble_device.state_manager.set_device(value)
63
- self._mower_state = value
64
-
65
- def ble(self) -> MammotionBaseBLEDevice | None:
66
- return self._ble_device
67
-
68
- def cloud(self) -> MammotionBaseCloudDevice | None:
69
- return self._cloud_device
70
-
71
- def has_queued_commands(self) -> bool:
72
- if self.has_cloud() and self.preference == ConnectionPreference.WIFI:
73
- return not self.cloud()._mqtt.command_queue.empty()
74
- else:
75
- return False
36
+ class MammotionDeviceManager:
37
+ """Manage devices - both mowers and RTK."""
76
38
 
77
- def add_ble(self, ble_device: BLEDevice) -> None:
78
- if ble_device is not None:
79
- self._ble_device = MammotionBaseBLEDevice(self.mower_state, ble_device)
39
+ def __init__(self) -> None:
40
+ self.devices: dict[str, MammotionMowerDeviceManager] = {}
41
+ self.rtk_devices: dict[str, MammotionRTKDeviceManager] = {}
80
42
 
81
- def add_cloud(self, cloud_device: Device | None = None, mqtt: MammotionCloud | None = None) -> None:
82
- if cloud_device is not None:
83
- self._cloud_device = MammotionBaseCloudDevice(
84
- mqtt, cloud_device=cloud_device, mowing_state=self.mower_state
85
- )
43
+ def _should_disconnect_mqtt(self, device_for_removal: AbstractDeviceManager) -> bool:
44
+ """Check if MQTT connection should be disconnected.
86
45
 
87
- def replace_cloud(self, cloud_device: MammotionBaseCloudDevice) -> None:
88
- self._cloud_device = cloud_device
46
+ Returns True if no other devices share the same MQTT connection.
47
+ """
48
+ if not device_for_removal.cloud:
49
+ return False
89
50
 
90
- def replace_ble(self, ble_device: MammotionBaseBLEDevice) -> None:
91
- self._ble_device = ble_device
51
+ mqtt_to_check = device_for_removal.cloud.mqtt
92
52
 
93
- def replace_mqtt(self, mqtt: MammotionCloud) -> None:
94
- self._cloud_device._mqtt = mqtt
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
+ }
95
59
 
96
- def has_cloud(self) -> bool:
97
- return self._cloud_device is not None
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
+ )
98
68
 
99
- def has_ble(self) -> bool:
100
- return self._ble_device is not None
69
+ return len(shared_devices) == 0
101
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)
102
81
 
103
- class MammotionDevices:
104
- devices: dict[str, MammotionMixedDeviceManager] = {}
82
+ def add_rtk_device(self, rtk_device: MammotionRTKDeviceManager) -> None:
83
+ """Add an RTK device."""
105
84
 
106
- def add_device(self, mammotion_device: MammotionMixedDeviceManager) -> None:
107
- exists: MammotionMixedDeviceManager | None = self.devices.get(mammotion_device.name)
85
+ exists: MammotionRTKDeviceManager | None = self.rtk_devices.get(rtk_device.name)
108
86
  if exists is None:
109
- self.devices[mammotion_device.name] = mammotion_device
87
+ self.rtk_devices[rtk_device.name] = rtk_device
110
88
  return
111
- if mammotion_device.has_cloud():
112
- exists.replace_cloud(mammotion_device.cloud())
113
- if mammotion_device.has_ble():
114
- exists.replace_ble(mammotion_device.ble())
115
-
116
- def get_device(self, mammotion_device_name: str) -> MammotionMixedDeviceManager:
117
- return self.devices.get(mammotion_device_name)
118
-
119
- async def remove_device(self, name) -> None:
120
- device_for_removal = self.devices.pop(name)
121
- if device_for_removal.has_cloud():
122
- should_disconnect = {
123
- device
124
- for key, device in self.devices.items()
125
- if device.cloud() is not None and device.cloud()._mqtt == device_for_removal.cloud()._mqtt
126
- }
127
- if len(should_disconnect) == 0:
128
- device_for_removal.cloud()._mqtt.disconnect()
129
- await device_for_removal.cloud().stop()
130
- if device_for_removal.has_ble():
131
- await device_for_removal.ble().stop()
132
-
133
- del device_for_removal
134
-
135
-
136
- async def create_devices(
137
- ble_device: BLEDevice,
138
- cloud_credentials: Credentials | None = None,
139
- preference: ConnectionPreference = ConnectionPreference.BLUETOOTH,
140
- ):
141
- mammotion = Mammotion()
142
- mammotion.add_ble_device(ble_device, preference)
143
-
144
- if cloud_credentials and preference == ConnectionPreference.EITHER or preference == ConnectionPreference.WIFI:
145
- await mammotion.login_and_initiate_cloud(
146
- cloud_credentials.account_id or cloud_credentials.email, cloud_credentials.password
147
- )
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]
113
+
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()
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
148
129
 
149
- return mammotion
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()
143
+
144
+ del device_for_removal
150
145
 
151
146
 
152
147
  class Mammotion:
153
148
  """Represents a Mammotion account and its devices."""
154
149
 
155
- devices = MammotionDevices()
156
- mqtt_list: dict[str, MammotionCloud] = dict()
150
+ device_manager = MammotionDeviceManager()
157
151
 
158
- _instance = None
152
+ _instance: Mammotion | None = None
159
153
 
160
- def __new__(cls, *args, **kwargs):
154
+ def __new__(cls) -> Mammotion:
155
+ """Create a singleton."""
161
156
  if not cls._instance:
162
157
  cls._instance = super().__new__(cls)
163
158
  return cls._instance
@@ -165,142 +160,382 @@ class Mammotion:
165
160
  def __init__(self) -> None:
166
161
  """Initialize MammotionDevice."""
167
162
  self._login_lock = asyncio.Lock()
168
-
169
- def add_ble_device(
170
- self, ble_device: BLEDevice, preference: ConnectionPreference = ConnectionPreference.BLUETOOTH
171
- ) -> None:
172
- if ble_device:
173
- self.devices.add_device(
174
- MammotionMixedDeviceManager(name=ble_device.name, ble_device=ble_device, preference=preference)
175
- )
163
+ self.mqtt_list: dict[str, MammotionCloud] = {}
176
164
 
177
165
  async def login_and_initiate_cloud(self, account, password, force: bool = False) -> None:
178
166
  async with self._login_lock:
179
- exists: MammotionCloud | None = self.mqtt_list.get(account)
180
- 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:
181
170
  cloud_client = await self.login(account, password)
182
171
  await self.initiate_cloud_connection(account, cloud_client)
183
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
+
184
260
  async def initiate_cloud_connection(self, account: str, cloud_client: CloudIOTGateway) -> None:
261
+ """Initiate cloud connection."""
185
262
  loop = asyncio.get_running_loop()
186
- if self.mqtt_list.get(account) is not None:
187
- if self.mqtt_list.get(account).is_connected():
188
- await loop.run_in_executor(None, self.mqtt_list.get(account).disconnect)
189
-
190
- mammotion_cloud = MammotionCloud(
191
- MammotionMQTT(
192
- region_id=cloud_client.region_response.data.regionId,
193
- product_key=cloud_client.aep_response.data.productKey,
194
- device_name=cloud_client.aep_response.data.deviceName,
195
- device_secret=cloud_client.aep_response.data.deviceSecret,
196
- iot_token=cloud_client.session_by_authcode_response.data.iotToken,
197
- client_id=cloud_client.client_id,
198
- cloud_client=cloud_client,
199
- ),
200
- cloud_client,
201
- )
202
- self.mqtt_list[account] = mammotion_cloud
203
- self.add_cloud_devices(mammotion_cloud)
204
263
 
205
- await loop.run_in_executor(None, self.mqtt_list[account].connect_async)
264
+ mammotion_http = cloud_client.mammotion_http
265
+
266
+ if mqtt := self.mqtt_list.get(f"{account}_aliyun"):
267
+ if mqtt.is_connected():
268
+ await loop.run_in_executor(None, mqtt.disconnect)
269
+
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
+ )
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)
206
347
 
207
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
+
208
352
  for device in mqtt_client.cloud_client.devices_by_account_response.data.data:
209
- mower_device = self.devices.get_device(device.deviceName)
210
- if device.deviceName.startswith(("Luba-", "Yuka-")) and mower_device is None:
211
- self.devices.add_device(
212
- MammotionMixedDeviceManager(
213
- 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,
214
361
  cloud_device=device,
215
362
  mqtt=mqtt_client,
216
363
  preference=ConnectionPreference.WIFI,
217
364
  )
218
- )
219
- elif device.deviceName.startswith(("Luba-", "Yuka-")) and mower_device:
220
- if mower_device.cloud() is None:
221
- 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)
222
370
  else:
223
- mower_device.replace_mqtt(mqtt_client)
224
-
225
- def set_disconnect_strategy(self, disconnect: bool) -> None:
226
- for device_name, device in self.devices.devices:
227
- if device.ble() is not None:
228
- ble_device: MammotionBaseBLEDevice = device.ble()
229
- 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)
230
408
 
231
409
  async def login(self, account: str, password: str) -> CloudIOTGateway:
232
410
  """Login to mammotion cloud."""
233
- cloud_client = CloudIOTGateway()
234
- mammotion_http = await connect_http(account, password)
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()
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