pymammotion 0.5.21__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.
- pymammotion/__init__.py +3 -3
- pymammotion/aliyun/client.py +5 -2
- pymammotion/aliyun/cloud_gateway.py +137 -20
- pymammotion/aliyun/model/dev_by_account_response.py +169 -21
- pymammotion/const.py +3 -0
- pymammotion/data/model/device.py +1 -0
- pymammotion/data/model/device_config.py +1 -1
- pymammotion/data/model/device_info.py +4 -0
- pymammotion/data/model/enums.py +5 -3
- pymammotion/data/model/generate_route_information.py +2 -2
- pymammotion/data/model/hash_list.py +113 -33
- pymammotion/data/model/mowing_modes.py +8 -0
- pymammotion/data/model/region_data.py +4 -4
- pymammotion/data/{state_manager.py → mower_state_manager.py} +50 -13
- pymammotion/data/mqtt/event.py +47 -22
- pymammotion/data/mqtt/mammotion_properties.py +257 -0
- pymammotion/data/mqtt/properties.py +32 -29
- pymammotion/data/mqtt/status.py +17 -16
- pymammotion/homeassistant/__init__.py +3 -0
- pymammotion/homeassistant/mower_api.py +446 -0
- pymammotion/homeassistant/rtk_api.py +54 -0
- pymammotion/http/http.py +433 -18
- pymammotion/http/model/http.py +82 -2
- pymammotion/http/model/response_factory.py +10 -4
- pymammotion/mammotion/commands/mammotion_command.py +20 -0
- pymammotion/mammotion/commands/messages/driver.py +25 -0
- pymammotion/mammotion/commands/messages/navigation.py +10 -6
- pymammotion/mammotion/commands/messages/system.py +0 -14
- pymammotion/mammotion/devices/__init__.py +27 -3
- pymammotion/mammotion/devices/base.py +22 -146
- pymammotion/mammotion/devices/mammotion.py +364 -205
- pymammotion/mammotion/devices/mammotion_bluetooth.py +11 -8
- pymammotion/mammotion/devices/mammotion_cloud.py +49 -85
- pymammotion/mammotion/devices/mammotion_mower_ble.py +49 -0
- pymammotion/mammotion/devices/mammotion_mower_cloud.py +39 -0
- pymammotion/mammotion/devices/managers/managers.py +81 -0
- pymammotion/mammotion/devices/mower_device.py +121 -0
- pymammotion/mammotion/devices/mower_manager.py +107 -0
- pymammotion/mammotion/devices/rtk_ble.py +89 -0
- pymammotion/mammotion/devices/rtk_cloud.py +113 -0
- pymammotion/mammotion/devices/rtk_device.py +50 -0
- pymammotion/mammotion/devices/rtk_manager.py +122 -0
- pymammotion/mqtt/__init__.py +2 -1
- pymammotion/mqtt/aliyun_mqtt.py +232 -0
- pymammotion/mqtt/mammotion_mqtt.py +174 -192
- pymammotion/mqtt/mqtt_models.py +66 -0
- pymammotion/proto/__init__.py +2 -2
- pymammotion/proto/mctrl_nav.proto +2 -2
- pymammotion/proto/mctrl_nav_pb2.py +1 -1
- pymammotion/proto/mctrl_nav_pb2.pyi +4 -4
- pymammotion/proto/mctrl_sys.proto +1 -1
- pymammotion/utility/datatype_converter.py +13 -12
- pymammotion/utility/device_type.py +88 -3
- pymammotion/utility/mur_mur_hash.py +132 -87
- {pymammotion-0.5.21.dist-info → pymammotion-0.5.45.dist-info}/METADATA +25 -30
- {pymammotion-0.5.21.dist-info → pymammotion-0.5.45.dist-info}/RECORD +64 -50
- {pymammotion-0.5.21.dist-info → pymammotion-0.5.45.dist-info}/WHEEL +1 -1
- pymammotion/http/_init_.py +0 -0
- {pymammotion-0.5.21.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
|
|
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.
|
|
20
|
-
from pymammotion.mammotion.devices.
|
|
21
|
-
from pymammotion.
|
|
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
|
|
30
|
-
|
|
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
|
|
87
|
-
self.
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
|
|
126
|
-
|
|
82
|
+
def add_rtk_device(self, rtk_device: MammotionRTKDeviceManager) -> None:
|
|
83
|
+
"""Add an RTK device."""
|
|
127
84
|
|
|
128
|
-
|
|
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.
|
|
87
|
+
self.rtk_devices[rtk_device.name] = rtk_device
|
|
136
88
|
return
|
|
137
|
-
if
|
|
138
|
-
exists.replace_cloud(
|
|
139
|
-
if
|
|
140
|
-
exists.replace_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
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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,93 +164,267 @@ 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
|
-
|
|
191
|
-
|
|
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
|
|
173
|
+
async def refresh_login(self, account: str) -> None:
|
|
196
174
|
"""Refresh login."""
|
|
197
175
|
async with self._login_lock:
|
|
198
|
-
|
|
199
|
-
|
|
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 =
|
|
202
|
-
|
|
203
|
-
|
|
181
|
+
mammotion_http = (
|
|
182
|
+
exists_aliyun.cloud_client.mammotion_http
|
|
183
|
+
if exists_aliyun
|
|
184
|
+
else exists_mammotion.cloud_client.mammotion_http
|
|
185
|
+
)
|
|
204
186
|
|
|
205
|
-
|
|
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():
|
|
206
194
|
loop = asyncio.get_running_loop()
|
|
207
|
-
await loop.run_in_executor(None,
|
|
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
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
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
|
-
|
|
237
|
-
if device.
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
mower_device.
|
|
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
|
-
|
|
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
|
|
261
|
-
ble_device:
|
|
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:
|
|
427
|
+
await cloud_client.get_region(country_code)
|
|
277
428
|
await cloud_client.connect()
|
|
278
429
|
await cloud_client.login_by_oauth(country_code)
|
|
279
430
|
await cloud_client.aep_handle()
|
|
@@ -281,21 +432,35 @@ class Mammotion:
|
|
|
281
432
|
await cloud_client.list_binding_by_account()
|
|
282
433
|
|
|
283
434
|
async def remove_device(self, name: str) -> None:
|
|
435
|
+
"""Remove a mower device."""
|
|
284
436
|
await self.device_manager.remove_device(name)
|
|
285
437
|
|
|
286
|
-
def
|
|
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."""
|
|
287
444
|
return self.device_manager.get_device(name)
|
|
288
445
|
|
|
289
|
-
def
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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()),
|
|
296
460
|
mqtt=mqtt_client,
|
|
297
461
|
cloud_device=device,
|
|
298
|
-
ble_device=
|
|
462
|
+
ble_device=ble_device,
|
|
463
|
+
preference=ConnectionPreference.WIFI if mqtt_client else ConnectionPreference.BLUETOOTH,
|
|
299
464
|
)
|
|
300
465
|
self.device_manager.add_device(mow_device)
|
|
301
466
|
return mow_device
|
|
@@ -304,10 +469,10 @@ class Mammotion:
|
|
|
304
469
|
"""Send a command to the device."""
|
|
305
470
|
device = self.get_device_by_name(name)
|
|
306
471
|
if device:
|
|
307
|
-
if device.preference is ConnectionPreference.BLUETOOTH and device.
|
|
308
|
-
return await device.ble
|
|
309
|
-
if device.preference is ConnectionPreference.WIFI:
|
|
310
|
-
return await device.cloud
|
|
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)
|
|
311
476
|
# TODO work with both with EITHER
|
|
312
477
|
return None
|
|
313
478
|
|
|
@@ -315,34 +480,26 @@ class Mammotion:
|
|
|
315
480
|
"""Send a command with args to the device."""
|
|
316
481
|
device = self.get_device_by_name(name)
|
|
317
482
|
if device:
|
|
318
|
-
if device.preference is ConnectionPreference.BLUETOOTH and device.
|
|
319
|
-
return await device.ble
|
|
320
|
-
if device.preference is ConnectionPreference.WIFI:
|
|
321
|
-
return await device.cloud().command(key, **kwargs)
|
|
322
|
-
# TODO work with both with EITHER
|
|
323
|
-
return None
|
|
324
|
-
|
|
325
|
-
async def start_sync(self, name: str, retry: int):
|
|
326
|
-
device = self.get_device_by_name(name)
|
|
327
|
-
if device:
|
|
328
|
-
if device.preference is ConnectionPreference.BLUETOOTH and device.has_ble():
|
|
329
|
-
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)
|
|
330
485
|
if device.preference is ConnectionPreference.WIFI:
|
|
331
|
-
return await device.cloud
|
|
486
|
+
return await device.cloud.command(key, **kwargs)
|
|
332
487
|
# TODO work with both with EITHER
|
|
333
488
|
return None
|
|
334
489
|
|
|
335
490
|
async def start_map_sync(self, name: str):
|
|
491
|
+
"""Start map sync."""
|
|
336
492
|
device = self.get_device_by_name(name)
|
|
337
493
|
if device:
|
|
338
|
-
if device.preference is ConnectionPreference.BLUETOOTH and device.
|
|
339
|
-
return await device.ble
|
|
340
|
-
if device.preference is ConnectionPreference.WIFI:
|
|
341
|
-
return await device.cloud
|
|
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()
|
|
342
498
|
# TODO work with both with EITHER
|
|
343
499
|
return None
|
|
344
500
|
|
|
345
501
|
async def get_stream_subscription(self, name: str, iot_id: str) -> Response[StreamSubscriptionResponse] | Any:
|
|
502
|
+
"""Get stream subscription."""
|
|
346
503
|
device = self.get_device_by_name(name)
|
|
347
504
|
if DeviceType.is_mini_or_x_series(name):
|
|
348
505
|
_stream_response = await device.mammotion_http.get_stream_subscription_mini_or_x_series(
|
|
@@ -356,6 +513,7 @@ class Mammotion:
|
|
|
356
513
|
return _stream_response
|
|
357
514
|
|
|
358
515
|
async def get_video_resource(self, name: str, iot_id: str) -> Response[VideoResourceResponse] | None:
|
|
516
|
+
"""Get video resource."""
|
|
359
517
|
device = self.get_device_by_name(name)
|
|
360
518
|
|
|
361
519
|
if DeviceType.is_mini_or_x_series(name):
|
|
@@ -365,6 +523,7 @@ class Mammotion:
|
|
|
365
523
|
return None
|
|
366
524
|
|
|
367
525
|
def mower(self, name: str) -> MowingDevice | None:
|
|
526
|
+
"""Get a mower device by name."""
|
|
368
527
|
device = self.get_device_by_name(name)
|
|
369
528
|
if device:
|
|
370
529
|
return device.state
|