pymammotion 0.5.33__py3-none-any.whl → 0.5.40__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/cloud_gateway.py +106 -18
- pymammotion/aliyun/model/dev_by_account_response.py +198 -20
- pymammotion/data/model/device.py +1 -0
- pymammotion/data/model/device_config.py +1 -1
- pymammotion/data/model/enums.py +3 -1
- pymammotion/data/model/generate_route_information.py +2 -2
- pymammotion/data/model/hash_list.py +105 -33
- pymammotion/data/model/region_data.py +4 -4
- pymammotion/data/{state_manager.py → mower_state_manager.py} +17 -7
- pymammotion/homeassistant/__init__.py +3 -0
- pymammotion/homeassistant/mower_api.py +446 -0
- pymammotion/homeassistant/rtk_api.py +54 -0
- pymammotion/http/http.py +118 -7
- pymammotion/http/model/http.py +77 -2
- pymammotion/http/model/response_factory.py +10 -4
- pymammotion/mammotion/commands/mammotion_command.py +6 -0
- pymammotion/mammotion/commands/messages/navigation.py +10 -6
- pymammotion/mammotion/devices/__init__.py +27 -3
- pymammotion/mammotion/devices/base.py +16 -138
- pymammotion/mammotion/devices/mammotion.py +361 -204
- pymammotion/mammotion/devices/mammotion_bluetooth.py +7 -5
- pymammotion/mammotion/devices/mammotion_cloud.py +22 -74
- 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 +132 -194
- pymammotion/mqtt/mqtt_models.py +66 -0
- pymammotion/proto/__init__.py +1 -1
- pymammotion/proto/mctrl_nav.proto +1 -1
- 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/device_type.py +88 -3
- pymammotion/utility/mur_mur_hash.py +132 -87
- {pymammotion-0.5.33.dist-info → pymammotion-0.5.40.dist-info}/METADATA +25 -31
- {pymammotion-0.5.33.dist-info → pymammotion-0.5.40.dist-info}/RECORD +54 -40
- {pymammotion-0.5.33.dist-info → pymammotion-0.5.40.dist-info}/WHEEL +1 -1
- {pymammotion-0.5.33.dist-info → pymammotion-0.5.40.dist-info/licenses}/LICENSE +0 -0
pymammotion/http/http.py
CHANGED
|
@@ -1,35 +1,58 @@
|
|
|
1
1
|
import csv
|
|
2
|
+
import random
|
|
2
3
|
import time
|
|
3
4
|
from typing import cast
|
|
4
5
|
|
|
5
6
|
from aiohttp import ClientSession
|
|
7
|
+
import jwt
|
|
6
8
|
|
|
7
9
|
from pymammotion.const import MAMMOTION_API_DOMAIN, MAMMOTION_CLIENT_ID, MAMMOTION_CLIENT_SECRET, MAMMOTION_DOMAIN
|
|
8
10
|
from pymammotion.http.encryption import EncryptionUtils
|
|
9
11
|
from pymammotion.http.model.camera_stream import StreamSubscriptionResponse, VideoResourceResponse
|
|
10
|
-
from pymammotion.http.model.http import
|
|
12
|
+
from pymammotion.http.model.http import (
|
|
13
|
+
CheckDeviceVersion,
|
|
14
|
+
DeviceInfo,
|
|
15
|
+
DeviceRecords,
|
|
16
|
+
ErrorInfo,
|
|
17
|
+
JWTTokenInfo,
|
|
18
|
+
LoginResponseData,
|
|
19
|
+
MQTTConnection,
|
|
20
|
+
Response,
|
|
21
|
+
)
|
|
11
22
|
from pymammotion.http.model.response_factory import response_factory
|
|
12
23
|
from pymammotion.http.model.rtk import RTK
|
|
13
24
|
|
|
14
25
|
|
|
15
26
|
class MammotionHTTP:
|
|
16
|
-
def __init__(self) -> None:
|
|
17
|
-
self.
|
|
27
|
+
def __init__(self, account: str | None = None, password: str | None = None) -> None:
|
|
28
|
+
self.device_info: list[DeviceInfo] = []
|
|
29
|
+
self.mqtt_credentials: MQTTConnection | None = None
|
|
30
|
+
self.device_records: DeviceRecords = DeviceRecords(records=[], current=0, total=0, size=0, pages=0)
|
|
31
|
+
self.expires_in = 0.0
|
|
18
32
|
self.code = 0
|
|
19
33
|
self.msg = None
|
|
20
|
-
self.account =
|
|
21
|
-
self._password =
|
|
34
|
+
self.account = account
|
|
35
|
+
self._password = password
|
|
22
36
|
self.response: Response | None = None
|
|
23
37
|
self.login_info: LoginResponseData | None = None
|
|
38
|
+
self.jwt_info: JWTTokenInfo = JWTTokenInfo("", "")
|
|
24
39
|
self._headers = {"User-Agent": "okhttp/4.9.3", "App-Version": "Home Assistant,1.14.2.29"}
|
|
25
40
|
self.encryption_utils = EncryptionUtils()
|
|
26
41
|
|
|
42
|
+
# Add this method to generate a 10-digit random number
|
|
43
|
+
def get_10_random() -> str:
|
|
44
|
+
"""Generate a 10-digit random number as a string."""
|
|
45
|
+
return "".join([str(random.randint(0, 9)) for _ in range(7)])
|
|
46
|
+
|
|
47
|
+
# Replace the line in the __init__ method with:
|
|
48
|
+
self.client_id = f"{int(time.time() * 1000)}_{get_10_random()}_1"
|
|
49
|
+
|
|
27
50
|
@staticmethod
|
|
28
51
|
def generate_headers(token: str) -> dict:
|
|
29
52
|
return {"Authorization": f"Bearer {token}"}
|
|
30
53
|
|
|
31
54
|
async def handle_expiry(self, resp: Response) -> Response:
|
|
32
|
-
if resp.code == 401:
|
|
55
|
+
if resp.code == 401 and self.account and self._password:
|
|
33
56
|
return await self.login(self.account, self._password)
|
|
34
57
|
return resp
|
|
35
58
|
|
|
@@ -89,7 +112,9 @@ class MammotionHTTP:
|
|
|
89
112
|
) as resp:
|
|
90
113
|
data = await resp.json()
|
|
91
114
|
|
|
115
|
+
self.login_info.access_token = data["data"].get("accessToken", self.login_info.access_token)
|
|
92
116
|
self.login_info.authorization_code = data["data"].get("code", self.login_info.authorization_code)
|
|
117
|
+
await self.get_mqtt_credentials()
|
|
93
118
|
return Response.from_dict(data)
|
|
94
119
|
|
|
95
120
|
async def pair_devices_mqtt(self, mower_name: str, rtk_name: str) -> Response:
|
|
@@ -163,6 +188,7 @@ class MammotionHTTP:
|
|
|
163
188
|
) -> Response[StreamSubscriptionResponse]:
|
|
164
189
|
# Prepare the payload with cameraStates based on is_yuka flag
|
|
165
190
|
"""Fetches stream subscription data for a given IoT device."""
|
|
191
|
+
|
|
166
192
|
payload = {"deviceId": iot_id, "mode": 0, "cameraStates": []}
|
|
167
193
|
|
|
168
194
|
# Add appropriate cameraStates based on the is_yuka flag
|
|
@@ -264,6 +290,88 @@ class MammotionHTTP:
|
|
|
264
290
|
|
|
265
291
|
return response_factory(Response[list[RTK]], data)
|
|
266
292
|
|
|
293
|
+
async def get_user_device_list(self) -> Response[list[DeviceInfo]]:
|
|
294
|
+
"""Fetches device list for a user, older devices / aliyun."""
|
|
295
|
+
async with ClientSession(MAMMOTION_API_DOMAIN) as session:
|
|
296
|
+
async with session.get(
|
|
297
|
+
"/device-server/v1/device/list",
|
|
298
|
+
headers={
|
|
299
|
+
**self._headers,
|
|
300
|
+
"Authorization": f"Bearer {self.login_info.access_token}",
|
|
301
|
+
"Content-Type": "application/json",
|
|
302
|
+
"User-Agent": "okhttp/4.9.3",
|
|
303
|
+
},
|
|
304
|
+
) as resp:
|
|
305
|
+
resp_dict = await resp.json()
|
|
306
|
+
response = response_factory(Response[list[DeviceInfo]], resp_dict)
|
|
307
|
+
self.device_info = response.data if response.data else self.device_info
|
|
308
|
+
return response
|
|
309
|
+
|
|
310
|
+
async def get_user_device_page(self) -> Response[DeviceRecords]:
|
|
311
|
+
"""Fetches device list for a user, is either new API or for newer devices."""
|
|
312
|
+
async with ClientSession(self.jwt_info.iot) as session:
|
|
313
|
+
async with session.post(
|
|
314
|
+
"/v1/user/device/page",
|
|
315
|
+
json={
|
|
316
|
+
"iotId": "",
|
|
317
|
+
"pageNumber": 1,
|
|
318
|
+
"pageSize": 100,
|
|
319
|
+
},
|
|
320
|
+
headers={
|
|
321
|
+
**self._headers,
|
|
322
|
+
"Authorization": f"Bearer {self.login_info.access_token}",
|
|
323
|
+
"Content-Type": "application/json",
|
|
324
|
+
"User-Agent": "okhttp/4.9.3",
|
|
325
|
+
"Client-Id": self.client_id,
|
|
326
|
+
"Client-Type": "1",
|
|
327
|
+
},
|
|
328
|
+
) as resp:
|
|
329
|
+
if resp.status != 200:
|
|
330
|
+
return Response.from_dict({"code": resp.status, "msg": "get device list failed"})
|
|
331
|
+
resp_dict = await resp.json()
|
|
332
|
+
response = response_factory(Response[DeviceRecords], resp_dict)
|
|
333
|
+
self.device_records = response.data if response.data else self.device_records
|
|
334
|
+
return response
|
|
335
|
+
|
|
336
|
+
async def get_mqtt_credentials(self) -> Response[MQTTConnection]:
|
|
337
|
+
"""Get mammotion mqtt credentials"""
|
|
338
|
+
async with ClientSession(self.jwt_info.iot) as session:
|
|
339
|
+
async with session.post(
|
|
340
|
+
"/v1/mqtt/auth/jwt",
|
|
341
|
+
headers={
|
|
342
|
+
**self._headers,
|
|
343
|
+
"Authorization": f"Bearer {self.login_info.access_token}",
|
|
344
|
+
"Content-Type": "application/json",
|
|
345
|
+
"User-Agent": "okhttp/4.9.3",
|
|
346
|
+
},
|
|
347
|
+
) as resp:
|
|
348
|
+
if resp.status != 200:
|
|
349
|
+
return Response.from_dict({"code": resp.status, "msg": "get mqtt failed"})
|
|
350
|
+
resp_dict = await resp.json()
|
|
351
|
+
response = response_factory(Response[MQTTConnection], resp_dict)
|
|
352
|
+
self.mqtt_credentials = response.data
|
|
353
|
+
return response
|
|
354
|
+
|
|
355
|
+
async def mqtt_invoke(self, content: str, device_name: str, iot_id: str) -> Response[dict]:
|
|
356
|
+
"""Send mqtt commands to devices."""
|
|
357
|
+
async with ClientSession(self.jwt_info.iot) as session:
|
|
358
|
+
async with session.post(
|
|
359
|
+
"/v1/mqtt/rpc/thing/service/invoke",
|
|
360
|
+
json={"args": {"content": content, "deviceName": device_name, "iotId": iot_id, "productKey": ""}},
|
|
361
|
+
headers={
|
|
362
|
+
**self._headers,
|
|
363
|
+
"Authorization": f"Bearer {self.login_info.access_token}",
|
|
364
|
+
"Content-Type": "application/json",
|
|
365
|
+
"User-Agent": "okhttp/4.9.3",
|
|
366
|
+
"Client-Id": self.client_id,
|
|
367
|
+
"Client-Type": "1",
|
|
368
|
+
},
|
|
369
|
+
) as resp:
|
|
370
|
+
if resp.status != 200:
|
|
371
|
+
return Response.from_dict({"code": resp.status, "msg": "get mqtt failed"})
|
|
372
|
+
resp_dict = await resp.json()
|
|
373
|
+
return response_factory(Response[dict], resp_dict)
|
|
374
|
+
|
|
267
375
|
async def refresh_login(self) -> Response[LoginResponseData]:
|
|
268
376
|
if self.expires_in > time.time():
|
|
269
377
|
res = await self.refresh_token()
|
|
@@ -297,7 +405,7 @@ class MammotionHTTP:
|
|
|
297
405
|
return Response.from_dict({"code": resp.status, "msg": "Login failed"})
|
|
298
406
|
data = await resp.json()
|
|
299
407
|
login_response = response_factory(Response[LoginResponseData], data)
|
|
300
|
-
if login_response.data is None:
|
|
408
|
+
if login_response is None or login_response.data is None:
|
|
301
409
|
print(login_response)
|
|
302
410
|
return Response.from_dict({"code": resp.status, "msg": "Login failed"})
|
|
303
411
|
self.login_info = login_response.data
|
|
@@ -308,6 +416,9 @@ class MammotionHTTP:
|
|
|
308
416
|
self.response = login_response
|
|
309
417
|
self.msg = login_response.msg
|
|
310
418
|
self.code = login_response.code
|
|
419
|
+
decoded_token = jwt.decode(self.response.data.access_token, options={"verify_signature": False})
|
|
420
|
+
if isinstance(decoded_token, dict):
|
|
421
|
+
self.jwt_info = JWTTokenInfo(iot=decoded_token.get("iot", ""), robot=decoded_token.get("robot", ""))
|
|
311
422
|
# TODO catch errors from mismatch user / password elsewhere
|
|
312
423
|
# Assuming the data format matches the expected structure
|
|
313
424
|
return login_response
|
pymammotion/http/model/http.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from dataclasses import dataclass
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
2
|
from typing import Annotated, Generic, Literal, TypeVar
|
|
3
3
|
|
|
4
4
|
from mashumaro import DataClassDictMixin
|
|
@@ -68,9 +68,76 @@ class ErrorInfo(DataClassDictMixin):
|
|
|
68
68
|
|
|
69
69
|
|
|
70
70
|
@dataclass
|
|
71
|
-
class
|
|
71
|
+
class SettingVo(DataClassORJSONMixin):
|
|
72
|
+
"""Device setting configuration."""
|
|
73
|
+
|
|
74
|
+
type: int = 0
|
|
75
|
+
is_switch: Annotated[int, Alias("isSwitch")] = 0
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class LocationVo(DataClassORJSONMixin):
|
|
80
|
+
"""Device location information."""
|
|
81
|
+
|
|
82
|
+
date_time: Annotated[str, Alias("dateTime")] = ""
|
|
83
|
+
date_timestamp: Annotated[int, Alias("dateTimestamp")] = 0
|
|
84
|
+
location: list[float] = field(default_factory=lambda: [0.0, 0.0])
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass
|
|
88
|
+
class DeviceInfo:
|
|
89
|
+
"""Complete device information."""
|
|
90
|
+
|
|
91
|
+
iot_id: Annotated[str, Alias("iotId")] = ""
|
|
92
|
+
device_id: Annotated[str, Alias("deviceId")] = ""
|
|
93
|
+
device_name: Annotated[str, Alias("deviceName")] = ""
|
|
94
|
+
device_type: Annotated[str, Alias("deviceType")] = ""
|
|
95
|
+
series: str = ""
|
|
96
|
+
product_series: Annotated[str, Alias("productSeries")] = ""
|
|
97
|
+
icon_code: Annotated[str, Alias("iconCode")] = ""
|
|
98
|
+
generation: int = 0
|
|
99
|
+
status: int = 0
|
|
100
|
+
is_subscribe: Annotated[int, Alias("isSubscribe")] = 0
|
|
101
|
+
setting_vos: Annotated[list[SettingVo], Alias("settingVos")] = field(default_factory=list)
|
|
102
|
+
active_time: Annotated[str, Alias("activeTime")] = ""
|
|
103
|
+
active_timestamp: Annotated[int, Alias("activeTimestamp")] = 0
|
|
104
|
+
location_vo: Annotated[LocationVo | None, Alias("locationVo")] = None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass
|
|
108
|
+
class DeviceRecord(DataClassORJSONMixin):
|
|
109
|
+
identity_id: Annotated[str, Alias("identityId")]
|
|
110
|
+
iot_id: Annotated[str, Alias("iotId")]
|
|
111
|
+
product_key: Annotated[str, Alias("productKey")]
|
|
112
|
+
device_name: Annotated[str, Alias("deviceName")]
|
|
113
|
+
owned: int
|
|
114
|
+
status: int
|
|
115
|
+
bind_time: Annotated[int, Alias("bindTime")]
|
|
116
|
+
create_time: Annotated[str, Alias("createTime")]
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass
|
|
120
|
+
class DeviceRecords(DataClassORJSONMixin):
|
|
121
|
+
records: list[DeviceRecord]
|
|
122
|
+
total: int
|
|
123
|
+
size: int
|
|
124
|
+
current: int
|
|
125
|
+
pages: int
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass
|
|
129
|
+
class MQTTConnection(DataClassORJSONMixin):
|
|
130
|
+
host: str
|
|
131
|
+
jwt: str
|
|
132
|
+
client_id: Annotated[str, Alias("clientId")]
|
|
133
|
+
username: str
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@dataclass
|
|
137
|
+
class Response(DataClassORJSONMixin, Generic[DataT]):
|
|
72
138
|
code: int
|
|
73
139
|
msg: str
|
|
140
|
+
request_id: Annotated[str, Alias("requestId")] | None = None
|
|
74
141
|
data: DataT | None = None
|
|
75
142
|
|
|
76
143
|
class Config(BaseConfig):
|
|
@@ -90,6 +157,14 @@ class LoginResponseUserInformation(DataClassORJSONMixin):
|
|
|
90
157
|
omit_none = True
|
|
91
158
|
|
|
92
159
|
|
|
160
|
+
@dataclass
|
|
161
|
+
class JWTTokenInfo(DataClassORJSONMixin):
|
|
162
|
+
"""specifically for newer devices and mqtt"""
|
|
163
|
+
|
|
164
|
+
iot: str # iot domain e.g api-iot-business-eu-dcdn.mammotion.com
|
|
165
|
+
robot: str # e.g api-robot-eu.mammotion.com
|
|
166
|
+
|
|
167
|
+
|
|
93
168
|
@dataclass
|
|
94
169
|
class LoginResponseData(DataClassORJSONMixin):
|
|
95
170
|
access_token: str
|
|
@@ -7,19 +7,20 @@ T = TypeVar("T")
|
|
|
7
7
|
|
|
8
8
|
def deserialize_data(value, target_type):
|
|
9
9
|
"""Deserialize data into a specified target type.
|
|
10
|
-
|
|
10
|
+
|
|
11
11
|
The function handles deserialization of basic types, lists, and unions. It
|
|
12
12
|
recursively processes list elements and supports optional types by handling
|
|
13
13
|
Union[T, None]. For custom types with a `from_dict` method, it calls this
|
|
14
14
|
method for deserialization. If the target type is unknown or unsupported, it
|
|
15
15
|
returns the value unchanged.
|
|
16
|
-
|
|
16
|
+
|
|
17
17
|
Args:
|
|
18
18
|
value: The data to be deserialized.
|
|
19
19
|
target_type (type): The desired type into which the data should be deserialized.
|
|
20
|
-
|
|
20
|
+
|
|
21
21
|
Returns:
|
|
22
22
|
The deserialized data in the specified target type.
|
|
23
|
+
|
|
23
24
|
"""
|
|
24
25
|
if value is None:
|
|
25
26
|
return None
|
|
@@ -35,7 +36,12 @@ def deserialize_data(value, target_type):
|
|
|
35
36
|
# Support Optional[T] = Union[T, None]
|
|
36
37
|
non_none_types = [t for t in args if t is not type(None)]
|
|
37
38
|
if len(non_none_types) == 1:
|
|
38
|
-
|
|
39
|
+
target = non_none_types[0]
|
|
40
|
+
# Handle Response[list[type]] case
|
|
41
|
+
if get_origin(target) is list and get_args(target):
|
|
42
|
+
item_type = get_args(target)[0]
|
|
43
|
+
return [deserialize_data(v, item_type) for v in value]
|
|
44
|
+
return deserialize_data(value, target)
|
|
39
45
|
|
|
40
46
|
if hasattr(target_type, "from_dict"):
|
|
41
47
|
return target_type.from_dict(value)
|
|
@@ -44,6 +44,12 @@ class MammotionCommand(
|
|
|
44
44
|
# 1 multipoint turn
|
|
45
45
|
return self.read_write_device(6, context, 1)
|
|
46
46
|
|
|
47
|
+
def get_error_code(self) -> bytes:
|
|
48
|
+
return self.read_write_device(5, 2, 1)
|
|
49
|
+
|
|
50
|
+
def get_error_timestamp(self) -> bytes:
|
|
51
|
+
return self.read_write_device(5, 3, 1)
|
|
52
|
+
|
|
47
53
|
def get_device_product_key(self) -> str:
|
|
48
54
|
return self._product_key
|
|
49
55
|
|
|
@@ -33,7 +33,7 @@ logger = logging.getLogger(__name__)
|
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
class MessageNavigation(AbstractMessage, ABC):
|
|
36
|
-
def send_order_msg_nav(self, build) -> bytes:
|
|
36
|
+
def send_order_msg_nav(self, build: MctlNav) -> bytes:
|
|
37
37
|
luba_msg = LubaMsg(
|
|
38
38
|
msgtype=MsgCmdType.NAV,
|
|
39
39
|
sender=MsgDevice.DEV_MOBILEAPP,
|
|
@@ -206,9 +206,9 @@ class MessageNavigation(AbstractMessage, ABC):
|
|
|
206
206
|
reserved=plan_bean.reserved,
|
|
207
207
|
weeks=plan_bean.weeks,
|
|
208
208
|
start_date=plan_bean.start_date,
|
|
209
|
-
trigger_type=plan_bean.
|
|
210
|
-
day=plan_bean.
|
|
211
|
-
toward_included_angle=plan_bean.
|
|
209
|
+
trigger_type=plan_bean.trigger_type,
|
|
210
|
+
day=plan_bean.day,
|
|
211
|
+
toward_included_angle=plan_bean.toward_included_angle,
|
|
212
212
|
toward_mode=0,
|
|
213
213
|
)
|
|
214
214
|
logger.debug(f"Send read job plan command planBean={plan_bean}")
|
|
@@ -262,7 +262,7 @@ class MessageNavigation(AbstractMessage, ABC):
|
|
|
262
262
|
toapp_map_name_msg=NavMapNameMsg(
|
|
263
263
|
hash=0,
|
|
264
264
|
result=0,
|
|
265
|
-
device_id=device_id, #
|
|
265
|
+
device_id=device_id, # iot_id
|
|
266
266
|
rw=0,
|
|
267
267
|
)
|
|
268
268
|
)
|
|
@@ -390,7 +390,7 @@ class MessageNavigation(AbstractMessage, ABC):
|
|
|
390
390
|
generate_route_information}")
|
|
391
391
|
return self.send_order_msg_nav(MctlNav(bidire_reqconver_path=build))
|
|
392
392
|
|
|
393
|
-
def
|
|
393
|
+
def modify_route_information(self, generate_route_information: GenerateRouteInformation) -> bytes:
|
|
394
394
|
logger.debug(f"Generate route data source: {generate_route_information}")
|
|
395
395
|
build = NavReqCoverPath(
|
|
396
396
|
pver=1,
|
|
@@ -432,7 +432,11 @@ class MessageNavigation(AbstractMessage, ABC):
|
|
|
432
432
|
return self.send_order_msg_nav(build)
|
|
433
433
|
|
|
434
434
|
def get_line_info_list(self, hash_list: list[int], transaction_id: int) -> bytes:
|
|
435
|
+
"""Get route information (mow path) corresponding to the specified hash list based on time.
|
|
436
|
+
e.g transaction_id = int(time.time() * 1000)
|
|
437
|
+
"""
|
|
435
438
|
logger.debug(f"Sending==========Get route command: {hash_list}")
|
|
439
|
+
|
|
436
440
|
build = MctlNav(
|
|
437
441
|
app_request_cover_paths=AppRequestCoverPathsT(
|
|
438
442
|
pver=1, hash_list=hash_list, transaction_id=transaction_id, sub_cmd=0
|
|
@@ -1,5 +1,29 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Mammotion devices module."""
|
|
2
2
|
|
|
3
|
-
from .mammotion import
|
|
3
|
+
from .mammotion import Mammotion, MammotionDeviceManager
|
|
4
|
+
from .mammotion_bluetooth import MammotionBaseBLEDevice
|
|
5
|
+
from .mammotion_cloud import MammotionBaseCloudDevice, MammotionCloud
|
|
6
|
+
from .mammotion_mower_ble import MammotionMowerBLEDevice
|
|
7
|
+
from .mammotion_mower_cloud import MammotionMowerCloudDevice
|
|
8
|
+
from .mower_device import MammotionMowerDevice
|
|
9
|
+
from .mower_manager import MammotionMowerDeviceManager
|
|
10
|
+
from .rtk_ble import MammotionRTKBLEDevice
|
|
11
|
+
from .rtk_cloud import MammotionRTKCloudDevice
|
|
12
|
+
from .rtk_device import MammotionRTKDevice
|
|
13
|
+
from .rtk_manager import MammotionRTKDeviceManager
|
|
4
14
|
|
|
5
|
-
__all__ = [
|
|
15
|
+
__all__ = [
|
|
16
|
+
"Mammotion",
|
|
17
|
+
"MammotionDeviceManager",
|
|
18
|
+
"MammotionMowerDeviceManager",
|
|
19
|
+
"MammotionBaseBLEDevice",
|
|
20
|
+
"MammotionBaseCloudDevice",
|
|
21
|
+
"MammotionCloud",
|
|
22
|
+
"MammotionMowerBLEDevice",
|
|
23
|
+
"MammotionMowerCloudDevice",
|
|
24
|
+
"MammotionMowerDevice",
|
|
25
|
+
"MammotionRTKBLEDevice",
|
|
26
|
+
"MammotionRTKCloudDevice",
|
|
27
|
+
"MammotionRTKDevice",
|
|
28
|
+
"MammotionRTKDeviceManager",
|
|
29
|
+
]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from abc import abstractmethod
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
2
|
import asyncio
|
|
3
3
|
import logging
|
|
4
4
|
from typing import Any
|
|
@@ -6,35 +6,18 @@ from typing import Any
|
|
|
6
6
|
import betterproto2
|
|
7
7
|
|
|
8
8
|
from pymammotion.aliyun.model.dev_by_account_response import Device
|
|
9
|
-
from pymammotion.data.model import RegionData
|
|
10
9
|
from pymammotion.data.model.device import MowingDevice
|
|
11
10
|
from pymammotion.data.model.raw_data import RawMowerData
|
|
12
|
-
from pymammotion.data.
|
|
13
|
-
from pymammotion.proto import LubaMsg
|
|
14
|
-
from pymammotion.utility.device_type import DeviceType
|
|
11
|
+
from pymammotion.data.mower_state_manager import MowerStateManager
|
|
12
|
+
from pymammotion.proto import LubaMsg
|
|
15
13
|
|
|
16
14
|
_LOGGER = logging.getLogger(__name__)
|
|
17
15
|
|
|
18
16
|
|
|
19
|
-
|
|
20
|
-
try:
|
|
21
|
-
# Find the index of the current integer
|
|
22
|
-
current_index = lst.index(current_hash)
|
|
23
|
-
|
|
24
|
-
# Check if there is a next integer in the list
|
|
25
|
-
if current_index + 1 < len(lst):
|
|
26
|
-
return lst[current_index + 1]
|
|
27
|
-
else:
|
|
28
|
-
return None # Or raise an exception or handle it in some other way
|
|
29
|
-
except ValueError:
|
|
30
|
-
# Handle the case where current_int is not in the list
|
|
31
|
-
return None # Or raise an exception or handle it in some other way
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
class MammotionBaseDevice:
|
|
17
|
+
class MammotionBaseDevice(ABC):
|
|
35
18
|
"""Base class for Mammotion devices."""
|
|
36
19
|
|
|
37
|
-
def __init__(self, state_manager:
|
|
20
|
+
def __init__(self, state_manager: MowerStateManager, cloud_device: Device) -> None:
|
|
38
21
|
"""Initialize MammotionBaseDevice."""
|
|
39
22
|
self.loop = asyncio.get_event_loop()
|
|
40
23
|
self._state_manager = state_manager
|
|
@@ -43,57 +26,6 @@ class MammotionBaseDevice:
|
|
|
43
26
|
self._notify_future: asyncio.Future[bytes] | None = None
|
|
44
27
|
self._cloud_device = cloud_device
|
|
45
28
|
|
|
46
|
-
async def datahash_response(self, hash_ack: NavGetHashListAck) -> None:
|
|
47
|
-
"""Handle datahash responses for root level hashs."""
|
|
48
|
-
current_frame = hash_ack.current_frame
|
|
49
|
-
|
|
50
|
-
missing_frames = self.mower.map.missing_root_hash_frame(hash_ack)
|
|
51
|
-
if len(missing_frames) == 0:
|
|
52
|
-
if len(self.mower.map.missing_hashlist(hash_ack.sub_cmd)) > 0:
|
|
53
|
-
data_hash = self.mower.map.missing_hashlist(hash_ack.sub_cmd).pop(0)
|
|
54
|
-
await self.queue_command("synchronize_hash_data", hash_num=data_hash)
|
|
55
|
-
return
|
|
56
|
-
|
|
57
|
-
if current_frame != missing_frames[0] - 1:
|
|
58
|
-
current_frame = missing_frames[0] - 1
|
|
59
|
-
await self.queue_command("get_hash_response", total_frame=hash_ack.total_frame, current_frame=current_frame)
|
|
60
|
-
|
|
61
|
-
async def commdata_response(self, common_data: NavGetCommDataAck | SvgMessageAckT) -> None:
|
|
62
|
-
"""Handle common data responses."""
|
|
63
|
-
total_frame = common_data.total_frame
|
|
64
|
-
current_frame = common_data.current_frame
|
|
65
|
-
|
|
66
|
-
missing_frames = self.mower.map.missing_frame(common_data)
|
|
67
|
-
if len(missing_frames) == 0:
|
|
68
|
-
# get next in hash ack list
|
|
69
|
-
|
|
70
|
-
data_hash = (
|
|
71
|
-
self.mower.map.missing_hashlist(common_data.sub_cmd).pop(0)
|
|
72
|
-
if len(self.mower.map.missing_hashlist(common_data.sub_cmd)) > 0
|
|
73
|
-
else None
|
|
74
|
-
)
|
|
75
|
-
if data_hash is None:
|
|
76
|
-
return
|
|
77
|
-
|
|
78
|
-
await self.queue_command("synchronize_hash_data", hash_num=data_hash)
|
|
79
|
-
else:
|
|
80
|
-
if current_frame != missing_frames[0] - 1:
|
|
81
|
-
current_frame = missing_frames[0] - 1
|
|
82
|
-
|
|
83
|
-
region_data = RegionData()
|
|
84
|
-
region_data.hash = common_data.data_hash if isinstance(common_data, SvgMessageAckT) else common_data.hash
|
|
85
|
-
region_data.action = common_data.action if isinstance(common_data, NavGetCommDataAck) else None
|
|
86
|
-
region_data.type = common_data.type
|
|
87
|
-
region_data.sub_cmd = common_data.sub_cmd
|
|
88
|
-
region_data.total_frame = total_frame
|
|
89
|
-
region_data.current_frame = current_frame
|
|
90
|
-
await self.queue_command("get_regional_data", regional_data=region_data)
|
|
91
|
-
|
|
92
|
-
async def plan_callback(self, plan: NavPlanJobSet) -> None:
|
|
93
|
-
if plan.plan_index < plan.total_plan_num - 1:
|
|
94
|
-
index = plan.plan_index + 1
|
|
95
|
-
await self.queue_command("read_plan", sub_cmd=2, plan_index=index)
|
|
96
|
-
|
|
97
29
|
def _update_raw_data(self, data: bytes) -> None:
|
|
98
30
|
"""Update raw and model data from notifications."""
|
|
99
31
|
tmp_msg = LubaMsg().parse(data)
|
|
@@ -127,7 +59,7 @@ class MammotionBaseDevice:
|
|
|
127
59
|
nav[nav_sub_msg[0]] = nav_sub_msg[1].to_dict(casing=betterproto2.Casing.SNAKE)
|
|
128
60
|
self._raw_data["nav"] = nav
|
|
129
61
|
|
|
130
|
-
def _update_sys_data(self, tmp_msg) -> None:
|
|
62
|
+
def _update_sys_data(self, tmp_msg: LubaMsg) -> None:
|
|
131
63
|
"""Update system data."""
|
|
132
64
|
sys_sub_msg = betterproto2.which_one_of(tmp_msg.sys, "SubSysMsg")
|
|
133
65
|
if sys_sub_msg[1] is None:
|
|
@@ -137,7 +69,7 @@ class MammotionBaseDevice:
|
|
|
137
69
|
sys[sys_sub_msg[0]] = sys_sub_msg[1].to_dict(casing=betterproto2.Casing.SNAKE)
|
|
138
70
|
self._raw_data["sys"] = sys
|
|
139
71
|
|
|
140
|
-
def _update_driver_data(self, tmp_msg) -> None:
|
|
72
|
+
def _update_driver_data(self, tmp_msg: LubaMsg) -> None:
|
|
141
73
|
"""Update driver data."""
|
|
142
74
|
drv_sub_msg = betterproto2.which_one_of(tmp_msg.driver, "SubDrvMsg")
|
|
143
75
|
if drv_sub_msg[1] is None:
|
|
@@ -147,7 +79,7 @@ class MammotionBaseDevice:
|
|
|
147
79
|
drv[drv_sub_msg[0]] = drv_sub_msg[1].to_dict(casing=betterproto2.Casing.SNAKE)
|
|
148
80
|
self._raw_data["driver"] = drv
|
|
149
81
|
|
|
150
|
-
def _update_net_data(self, tmp_msg) -> None:
|
|
82
|
+
def _update_net_data(self, tmp_msg: LubaMsg) -> None:
|
|
151
83
|
"""Update network data."""
|
|
152
84
|
net_sub_msg = betterproto2.which_one_of(tmp_msg.net, "NetSubType")
|
|
153
85
|
if net_sub_msg[1] is None:
|
|
@@ -160,7 +92,7 @@ class MammotionBaseDevice:
|
|
|
160
92
|
net[net_sub_msg[0]] = net_sub_msg[1].to_dict(casing=betterproto2.Casing.SNAKE)
|
|
161
93
|
self._raw_data["net"] = net
|
|
162
94
|
|
|
163
|
-
def _update_mul_data(self, tmp_msg) -> None:
|
|
95
|
+
def _update_mul_data(self, tmp_msg: LubaMsg) -> None:
|
|
164
96
|
"""Update mul data."""
|
|
165
97
|
mul_sub_msg = betterproto2.which_one_of(tmp_msg.mul, "SubMul")
|
|
166
98
|
if mul_sub_msg[1] is None:
|
|
@@ -170,7 +102,7 @@ class MammotionBaseDevice:
|
|
|
170
102
|
mul[mul_sub_msg[0]] = mul_sub_msg[1].to_dict(casing=betterproto2.Casing.SNAKE)
|
|
171
103
|
self._raw_data["mul"] = mul
|
|
172
104
|
|
|
173
|
-
def _update_ota_data(self, tmp_msg) -> None:
|
|
105
|
+
def _update_ota_data(self, tmp_msg: LubaMsg) -> None:
|
|
174
106
|
"""Update OTA data."""
|
|
175
107
|
ota_sub_msg = betterproto2.which_one_of(tmp_msg.ota, "SubOtaMsg")
|
|
176
108
|
if ota_sub_msg[1] is None:
|
|
@@ -191,71 +123,17 @@ class MammotionBaseDevice:
|
|
|
191
123
|
return self._state_manager.get_device()
|
|
192
124
|
|
|
193
125
|
@abstractmethod
|
|
194
|
-
async def queue_command(self, key: str, **kwargs:
|
|
126
|
+
async def queue_command(self, key: str, **kwargs: Any) -> None:
|
|
195
127
|
"""Queue commands to mower."""
|
|
196
128
|
|
|
197
129
|
@abstractmethod
|
|
198
|
-
async def _ble_sync(self):
|
|
130
|
+
async def _ble_sync(self) -> None:
|
|
199
131
|
"""Send ble sync command every 3 seconds or sooner."""
|
|
200
132
|
|
|
201
133
|
@abstractmethod
|
|
202
|
-
def stop(self):
|
|
134
|
+
def stop(self) -> None:
|
|
203
135
|
"""Stop everything ready for destroying."""
|
|
204
136
|
|
|
205
|
-
async def start_sync(self, retry: int) -> None:
|
|
206
|
-
"""Start synchronization with the device."""
|
|
207
|
-
await self.queue_command("get_device_base_info")
|
|
208
|
-
await self.queue_command("get_device_product_model")
|
|
209
|
-
await self.queue_command("get_report_cfg")
|
|
210
|
-
"""RTK and dock location."""
|
|
211
|
-
await self.queue_command("read_write_device", rw_id=5, context=1, rw=1)
|
|
212
|
-
await self.async_read_settings()
|
|
213
|
-
|
|
214
|
-
async def start_map_sync(self) -> None:
|
|
215
|
-
"""Start sync of map data."""
|
|
216
|
-
|
|
217
|
-
self.mower.map.update_hash_lists(self.mower.map.hashlist)
|
|
218
|
-
|
|
219
|
-
await self.queue_command("send_todev_ble_sync", sync_type=3)
|
|
220
|
-
|
|
221
|
-
if self._cloud_device and len(self.mower.map.area_name) == 0 and not DeviceType.is_luba1(self.mower.name):
|
|
222
|
-
await self.queue_command("get_area_name_list", device_id=self._cloud_device.iotId)
|
|
223
|
-
|
|
224
|
-
if len(self.mower.map.plan) == 0 or list(self.mower.map.plan.values())[0].total_plan_num != len(
|
|
225
|
-
self.mower.map.plan
|
|
226
|
-
):
|
|
227
|
-
await self.queue_command("read_plan", sub_cmd=2, plan_index=0)
|
|
228
|
-
|
|
229
|
-
for hash_id, frame in list(self.mower.map.area.items()):
|
|
230
|
-
missing_frames = self.mower.map.find_missing_frames(frame)
|
|
231
|
-
if len(missing_frames) > 0:
|
|
232
|
-
del self.mower.map.area[hash_id]
|
|
233
|
-
|
|
234
|
-
for hash_id, frame in list(self.mower.map.path.items()):
|
|
235
|
-
missing_frames = self.mower.map.find_missing_frames(frame)
|
|
236
|
-
if len(missing_frames) > 0:
|
|
237
|
-
del self.mower.map.path[hash_id]
|
|
238
|
-
|
|
239
|
-
for hash_id, frame in list(self.mower.map.obstacle.items()):
|
|
240
|
-
missing_frames = self.mower.map.find_missing_frames(frame)
|
|
241
|
-
if len(missing_frames) > 0:
|
|
242
|
-
del self.mower.map.obstacle[hash_id]
|
|
243
|
-
|
|
244
|
-
# don't know why but total frame on svg is wrong
|
|
245
|
-
# for hash, frame in self.mower.map.svg.items():
|
|
246
|
-
# missing_frames = self.mower.map.find_missing_frames(frame)
|
|
247
|
-
# if len(missing_frames) > 0:
|
|
248
|
-
# del self.mower.map.svg[hash]
|
|
249
|
-
|
|
250
|
-
if len(self.mower.map.root_hash_lists) == 0 or len(self.mower.map.missing_hashlist()) > 0:
|
|
251
|
-
await self.queue_command("get_all_boundary_hash_list", sub_cmd=0)
|
|
252
|
-
|
|
253
|
-
# sub_cmd 3 is job hashes??
|
|
254
|
-
# sub_cmd 4 is dump location (yuka)
|
|
255
|
-
# jobs list
|
|
256
|
-
#
|
|
257
|
-
# await self.queue_command("get_all_boundary_hash_list", sub_cmd=3)
|
|
258
|
-
|
|
259
137
|
async def async_read_settings(self) -> None:
|
|
260
138
|
"""Read settings from device."""
|
|
261
139
|
# no cutting in rain nav_sys_param_cmd (id 3 context 1/0)
|
|
@@ -276,10 +154,10 @@ class MammotionBaseDevice:
|
|
|
276
154
|
await self.queue_command("read_write_device", rw_id=5, rw=1, context=2)
|
|
277
155
|
await self.queue_command("read_write_device", rw_id=5, rw=1, context=3)
|
|
278
156
|
|
|
279
|
-
async def command(self, key: str, **kwargs):
|
|
157
|
+
async def command(self, key: str, **kwargs: Any) -> None:
|
|
280
158
|
"""Send a command to the device."""
|
|
281
|
-
|
|
159
|
+
await self.queue_command(key, **kwargs)
|
|
282
160
|
|
|
283
161
|
@property
|
|
284
|
-
def state_manager(self):
|
|
162
|
+
def state_manager(self) -> MowerStateManager:
|
|
285
163
|
return self._state_manager
|