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,6 +4,7 @@ from enum import IntEnum
|
|
|
4
4
|
from mashumaro.mixins.orjson import DataClassORJSONMixin
|
|
5
5
|
|
|
6
6
|
from pymammotion.proto import NavGetCommDataAck, NavGetHashListAck, SvgMessageAckT
|
|
7
|
+
from pymammotion.utility.mur_mur_hash import MurMurHashUtil
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
class PathType(IntEnum):
|
|
@@ -28,6 +29,13 @@ class AreaLabelName(DataClassORJSONMixin):
|
|
|
28
29
|
label: str = ""
|
|
29
30
|
|
|
30
31
|
|
|
32
|
+
@dataclass
|
|
33
|
+
class NavNameTime(DataClassORJSONMixin):
|
|
34
|
+
name: str = ""
|
|
35
|
+
create_time: int = 0
|
|
36
|
+
modify_time: int = 0
|
|
37
|
+
|
|
38
|
+
|
|
31
39
|
@dataclass
|
|
32
40
|
class NavGetCommData(DataClassORJSONMixin):
|
|
33
41
|
pver: int = 0
|
|
@@ -44,7 +52,35 @@ class NavGetCommData(DataClassORJSONMixin):
|
|
|
44
52
|
data_len: int = 0
|
|
45
53
|
data_couple: list["CommDataCouple"] = field(default_factory=list)
|
|
46
54
|
reserved: str = ""
|
|
47
|
-
|
|
55
|
+
name_time: NavNameTime = field(default_factory=NavNameTime)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class MowPathPacket(DataClassORJSONMixin):
|
|
60
|
+
path_hash: int = 0
|
|
61
|
+
path_type: int = 0
|
|
62
|
+
path_total: int = 0
|
|
63
|
+
path_cur: int = 0
|
|
64
|
+
zone_hash: int = 0
|
|
65
|
+
data_couple: list["CommDataCouple"] = field(default_factory=list)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class MowPath(DataClassORJSONMixin):
|
|
70
|
+
pver: int = 0
|
|
71
|
+
sub_cmd: int = 0
|
|
72
|
+
result: int = 0
|
|
73
|
+
area: int = 0
|
|
74
|
+
time: int = 0
|
|
75
|
+
total_frame: int = 0
|
|
76
|
+
current_frame: int = 0
|
|
77
|
+
total_path_num: int = 0
|
|
78
|
+
valid_path_num: int = 0
|
|
79
|
+
data_hash: int = 0
|
|
80
|
+
transaction_id: int = 0
|
|
81
|
+
reserved: list[int] = field(default_factory=list)
|
|
82
|
+
data_len: int = 0
|
|
83
|
+
path_packets: list[MowPathPacket] = field(default_factory=list)
|
|
48
84
|
|
|
49
85
|
|
|
50
86
|
@dataclass
|
|
@@ -172,13 +208,24 @@ class HashList(DataClassORJSONMixin):
|
|
|
172
208
|
line: dict[int, FrameList] = field(default_factory=dict) # type 10 possibly breakpoint? / sub cmd 3
|
|
173
209
|
plan: dict[str, Plan] = field(default_factory=dict)
|
|
174
210
|
area_name: list[AreaHashNameList] = field(default_factory=list)
|
|
211
|
+
current_mow_path: dict[int, MowPath] = field(default_factory=dict)
|
|
175
212
|
|
|
176
|
-
def update_hash_lists(self, hashlist: list[int]) -> None:
|
|
213
|
+
def update_hash_lists(self, hashlist: list[int], bol_hash: str | None = None) -> None:
|
|
214
|
+
if bol_hash:
|
|
215
|
+
self.invalidate_maps(int(bol_hash))
|
|
177
216
|
self.area = {hash_id: frames for hash_id, frames in self.area.items() if hash_id in hashlist}
|
|
178
217
|
self.path = {hash_id: frames for hash_id, frames in self.path.items() if hash_id in hashlist}
|
|
179
218
|
self.obstacle = {hash_id: frames for hash_id, frames in self.obstacle.items() if hash_id in hashlist}
|
|
180
219
|
self.dump = {hash_id: frames for hash_id, frames in self.dump.items() if hash_id in hashlist}
|
|
181
220
|
self.svg = {hash_id: frames for hash_id, frames in self.svg.items() if hash_id in hashlist}
|
|
221
|
+
|
|
222
|
+
area_hashes = list(self.area.keys())
|
|
223
|
+
for hash_id, plan_task in self.plan.copy().items():
|
|
224
|
+
for item in plan_task.zone_hashs:
|
|
225
|
+
if item not in area_hashes:
|
|
226
|
+
self.plan.pop(hash_id)
|
|
227
|
+
break
|
|
228
|
+
|
|
182
229
|
self.area_name = [
|
|
183
230
|
area_item
|
|
184
231
|
for area_item in self.area_name
|
|
@@ -259,61 +306,90 @@ class HashList(DataClassORJSONMixin):
|
|
|
259
306
|
return missing_frames
|
|
260
307
|
|
|
261
308
|
def missing_frame(self, hash_data: NavGetCommDataAck | SvgMessageAckT) -> list[int]:
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
if hash_data.type == PathType.OBSTACLE:
|
|
266
|
-
return self.find_missing_frames(self.obstacle.get(hash_data.hash))
|
|
309
|
+
frame_list = self._get_frame_list_by_type_and_hash(hash_data)
|
|
310
|
+
return self.find_missing_frames(frame_list)
|
|
267
311
|
|
|
268
|
-
|
|
269
|
-
|
|
312
|
+
def _get_frame_list_by_type_and_hash(self, hash_data: NavGetCommDataAck | SvgMessageAckT) -> FrameList | None:
|
|
313
|
+
"""Get the appropriate FrameList based on hash_data type and hash."""
|
|
314
|
+
path_type_mapping = self._get_path_type_mapping()
|
|
315
|
+
target_dict = path_type_mapping.get(hash_data.type)
|
|
270
316
|
|
|
271
|
-
if
|
|
272
|
-
return
|
|
317
|
+
if target_dict is None:
|
|
318
|
+
return None
|
|
273
319
|
|
|
274
|
-
|
|
275
|
-
|
|
320
|
+
# Handle SvgMessage with data_hash attribute
|
|
321
|
+
if isinstance(hash_data, SvgMessageAckT):
|
|
322
|
+
return target_dict.get(hash_data.data_hash)
|
|
276
323
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
return []
|
|
324
|
+
# Handle NavGetCommDataAck with hash attribute
|
|
325
|
+
return target_dict.get(hash_data.hash)
|
|
281
326
|
|
|
282
327
|
def update_plan(self, plan: Plan) -> None:
|
|
283
328
|
if plan.total_plan_num != 0:
|
|
284
329
|
self.plan[plan.plan_id] = plan
|
|
285
330
|
|
|
331
|
+
def _get_path_type_mapping(self) -> dict[int, dict[int, FrameList]]:
|
|
332
|
+
"""Return mapping of PathType to corresponding hash dictionary."""
|
|
333
|
+
return {
|
|
334
|
+
PathType.AREA: self.area,
|
|
335
|
+
PathType.OBSTACLE: self.obstacle,
|
|
336
|
+
PathType.PATH: self.path,
|
|
337
|
+
PathType.LINE: self.line,
|
|
338
|
+
PathType.DUMP: self.dump,
|
|
339
|
+
PathType.SVG: self.svg,
|
|
340
|
+
}
|
|
341
|
+
|
|
286
342
|
def update(self, hash_data: NavGetCommData | SvgMessage) -> bool:
|
|
287
343
|
"""Update the map data."""
|
|
288
344
|
|
|
289
|
-
if hash_data.type == PathType.AREA:
|
|
345
|
+
if hash_data.type == PathType.AREA and isinstance(hash_data, NavGetCommData):
|
|
290
346
|
existing_name = next((area for area in self.area_name if area.hash == hash_data.hash), None)
|
|
291
347
|
if not existing_name:
|
|
292
|
-
name = f"area {len(self.area_name)+1}"
|
|
348
|
+
name = f"area {len(self.area_name)+1}"
|
|
293
349
|
self.area_name.append(AreaHashNameList(name=name, hash=hash_data.hash))
|
|
294
350
|
result = self._add_hash_data(self.area, hash_data)
|
|
295
351
|
self.update_hash_lists(self.hashlist)
|
|
296
352
|
return result
|
|
297
353
|
|
|
298
|
-
|
|
299
|
-
|
|
354
|
+
path_type_mapping = self._get_path_type_mapping()
|
|
355
|
+
target_dict = path_type_mapping.get(hash_data.type)
|
|
300
356
|
|
|
301
|
-
if
|
|
302
|
-
return self._add_hash_data(
|
|
357
|
+
if target_dict is not None:
|
|
358
|
+
return self._add_hash_data(target_dict, hash_data)
|
|
303
359
|
|
|
304
|
-
|
|
305
|
-
return self._add_hash_data(self.line, hash_data)
|
|
360
|
+
return False
|
|
306
361
|
|
|
307
|
-
|
|
308
|
-
|
|
362
|
+
def find_missing_mow_path_frames(self) -> list[int]:
|
|
363
|
+
"""Find missing frames in current_mow_path based on total_frame."""
|
|
364
|
+
if not self.current_mow_path:
|
|
365
|
+
return []
|
|
309
366
|
|
|
310
|
-
|
|
311
|
-
|
|
367
|
+
# Get total_frame from any MowPath object (they should all have the same total_frame)
|
|
368
|
+
total_frame = next(iter(self.current_mow_path.values())).total_frame
|
|
312
369
|
|
|
313
|
-
|
|
370
|
+
if total_frame == 0:
|
|
371
|
+
return []
|
|
372
|
+
|
|
373
|
+
if total_frame == len(self.current_mow_path):
|
|
374
|
+
return []
|
|
375
|
+
|
|
376
|
+
# Generate list of expected frame numbers (1 to total_frame)
|
|
377
|
+
expected_frames = set(range(1, total_frame + 1))
|
|
378
|
+
|
|
379
|
+
# Get current frame numbers from dictionary keys
|
|
380
|
+
current_frames = set(self.current_mow_path.keys())
|
|
381
|
+
|
|
382
|
+
# Return sorted list of missing frames
|
|
383
|
+
missing_frames = sorted(expected_frames - current_frames)
|
|
384
|
+
return missing_frames
|
|
385
|
+
|
|
386
|
+
def update_mow_path(self, path: MowPath) -> None:
|
|
387
|
+
"""Update the current_mow_path with the latest MowPath data."""
|
|
388
|
+
# TODO check if we need to clear the current_mow_path first
|
|
389
|
+
self.current_mow_path[path.current_frame] = path
|
|
314
390
|
|
|
315
391
|
@staticmethod
|
|
316
|
-
def find_missing_frames(frame_list: FrameList | RootHashList) -> list[int]:
|
|
392
|
+
def find_missing_frames(frame_list: FrameList | RootHashList | None) -> list[int]:
|
|
317
393
|
if frame_list is None:
|
|
318
394
|
return []
|
|
319
395
|
|
|
@@ -328,7 +404,7 @@ class HashList(DataClassORJSONMixin):
|
|
|
328
404
|
@staticmethod
|
|
329
405
|
def _add_hash_data(hash_dict: dict[int, FrameList], hash_data: NavGetCommData | SvgMessage) -> bool:
|
|
330
406
|
if isinstance(hash_data, SvgMessage):
|
|
331
|
-
if hash_dict.get(hash_data.data_hash) is None:
|
|
407
|
+
if hash_dict.get(hash_data.data_hash, None) is None:
|
|
332
408
|
hash_dict[hash_data.data_hash] = FrameList(total_frame=hash_data.total_frame, data=[hash_data])
|
|
333
409
|
return True
|
|
334
410
|
|
|
@@ -347,7 +423,7 @@ class HashList(DataClassORJSONMixin):
|
|
|
347
423
|
return True
|
|
348
424
|
return False
|
|
349
425
|
|
|
350
|
-
if hash_dict.get(hash_data.hash) is None:
|
|
426
|
+
if hash_dict.get(hash_data.hash, None) is None:
|
|
351
427
|
hash_dict[hash_data.hash] = FrameList(total_frame=hash_data.total_frame, data=[hash_data])
|
|
352
428
|
return True
|
|
353
429
|
|
|
@@ -361,3 +437,7 @@ class HashList(DataClassORJSONMixin):
|
|
|
361
437
|
hash_dict[hash_data.hash].data.append(hash_data)
|
|
362
438
|
return True
|
|
363
439
|
return False
|
|
440
|
+
|
|
441
|
+
def invalidate_maps(self, bol_hash: int) -> None:
|
|
442
|
+
if MurMurHashUtil.hash_unsigned_list(self.hashlist) != bol_hash:
|
|
443
|
+
self.root_hash_lists = []
|
|
@@ -6,13 +6,13 @@ from mashumaro.mixins.orjson import DataClassORJSONMixin
|
|
|
6
6
|
@dataclass
|
|
7
7
|
class RegionData(DataClassORJSONMixin):
|
|
8
8
|
def __init__(self) -> None:
|
|
9
|
-
self.hash: int
|
|
9
|
+
self.hash: int = 0
|
|
10
10
|
self.action: int = 0
|
|
11
11
|
self.current_frame: int = 0
|
|
12
|
-
self.data_hash: int
|
|
12
|
+
self.data_hash: int = 0
|
|
13
13
|
self.data_len: int = 0
|
|
14
|
-
self.p_hash_a: int
|
|
15
|
-
self.p_hash_b: int
|
|
14
|
+
self.p_hash_a: int = 0
|
|
15
|
+
self.p_hash_b: int = 0
|
|
16
16
|
self.path: list[list[float]] | None = None
|
|
17
17
|
self.pver: int = 0
|
|
18
18
|
self.result: int = 0
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
"""Manage state from notifications into MowingDevice."""
|
|
2
2
|
|
|
3
|
+
from collections.abc import Awaitable, Callable
|
|
3
4
|
from datetime import UTC, datetime
|
|
4
5
|
import logging
|
|
5
|
-
from typing import Any
|
|
6
|
+
from typing import Any
|
|
6
7
|
|
|
7
8
|
import betterproto2
|
|
8
9
|
|
|
9
10
|
from pymammotion.data.model.device import MowingDevice
|
|
10
11
|
from pymammotion.data.model.device_info import SideLight
|
|
11
|
-
from pymammotion.data.model.
|
|
12
|
-
|
|
12
|
+
from pymammotion.data.model.hash_list import (
|
|
13
|
+
AreaHashNameList,
|
|
14
|
+
MowPath,
|
|
15
|
+
NavGetCommData,
|
|
16
|
+
NavGetHashListData,
|
|
17
|
+
Plan,
|
|
18
|
+
SvgMessage,
|
|
19
|
+
)
|
|
13
20
|
from pymammotion.data.model.work import CurrentTaskSettings
|
|
14
21
|
from pymammotion.data.mqtt.event import ThingEventMessage
|
|
15
22
|
from pymammotion.data.mqtt.properties import ThingPropertiesMessage
|
|
@@ -17,6 +24,9 @@ from pymammotion.data.mqtt.status import ThingStatusMessage
|
|
|
17
24
|
from pymammotion.event.event import DataEvent
|
|
18
25
|
from pymammotion.proto import (
|
|
19
26
|
AppGetAllAreaHashName,
|
|
27
|
+
AppGetCutterWorkMode,
|
|
28
|
+
AppSetCutterWorkMode,
|
|
29
|
+
CoverPathUploadT,
|
|
20
30
|
DeviceFwInfo,
|
|
21
31
|
DeviceProductTypeInfoT,
|
|
22
32
|
DrvDevInfoResp,
|
|
@@ -38,13 +48,13 @@ from pymammotion.proto import (
|
|
|
38
48
|
logger = logging.getLogger(__name__)
|
|
39
49
|
|
|
40
50
|
|
|
41
|
-
class
|
|
51
|
+
class MowerStateManager:
|
|
42
52
|
"""Manage state."""
|
|
43
53
|
|
|
44
54
|
def __init__(self, device: MowingDevice) -> None:
|
|
55
|
+
"""Initialize state manager with a device."""
|
|
45
56
|
self._device: MowingDevice = device
|
|
46
57
|
self.last_updated_at = datetime.now(UTC)
|
|
47
|
-
self.preference = ConnectionPreference.WIFI
|
|
48
58
|
self.cloud_gethash_ack_callback: Callable[[NavGetHashListAck], Awaitable[None]] | None = None
|
|
49
59
|
self.cloud_get_commondata_ack_callback: (
|
|
50
60
|
Callable[[NavGetCommDataAck | SvgMessageAckT], Awaitable[None]] | None
|
|
@@ -74,11 +84,13 @@ class StateManager:
|
|
|
74
84
|
self._device = device
|
|
75
85
|
|
|
76
86
|
async def properties(self, thing_properties: ThingPropertiesMessage) -> None:
|
|
87
|
+
"""Update device properties and invoke callback."""
|
|
77
88
|
# TODO update device based off thing properties
|
|
78
89
|
self._device.mqtt_properties = thing_properties
|
|
79
90
|
await self.on_properties_callback(thing_properties)
|
|
80
91
|
|
|
81
92
|
async def status(self, thing_status: ThingStatusMessage) -> None:
|
|
93
|
+
"""Update device status and invoke callback."""
|
|
82
94
|
if not self._device.online:
|
|
83
95
|
self._device.online = True
|
|
84
96
|
self._device.status_properties = thing_status
|
|
@@ -93,19 +105,23 @@ class StateManager:
|
|
|
93
105
|
|
|
94
106
|
@property
|
|
95
107
|
def online(self) -> bool:
|
|
108
|
+
"""Return online status."""
|
|
96
109
|
return self._device.online
|
|
97
110
|
|
|
98
111
|
@online.setter
|
|
99
112
|
def online(self, value: bool) -> None:
|
|
113
|
+
"""Set online status."""
|
|
100
114
|
self._device.online = value
|
|
101
115
|
|
|
102
116
|
async def gethash_ack_callback(self, msg: NavGetHashListAck) -> None:
|
|
117
|
+
"""Dispatch hash list acknowledgment to available callback."""
|
|
103
118
|
if self.cloud_gethash_ack_callback:
|
|
104
119
|
await self.cloud_gethash_ack_callback(msg)
|
|
105
120
|
elif self.ble_gethash_ack_callback:
|
|
106
121
|
await self.ble_gethash_ack_callback(msg)
|
|
107
122
|
|
|
108
123
|
async def on_notification_callback(self, res: tuple[str, Any | None]) -> None:
|
|
124
|
+
"""Dispatch notification to available callback."""
|
|
109
125
|
if self.cloud_on_notification_callback:
|
|
110
126
|
await self.cloud_on_notification_callback.data_event(res)
|
|
111
127
|
elif self.ble_on_notification_callback:
|
|
@@ -122,7 +138,7 @@ class StateManager:
|
|
|
122
138
|
await self.status_callback.data_event(thing_status)
|
|
123
139
|
|
|
124
140
|
async def on_device_event_callback(self, device_event: ThingEventMessage) -> None:
|
|
125
|
-
"""Executes the
|
|
141
|
+
"""Executes the event callback if it is set."""
|
|
126
142
|
if self.device_event_callback:
|
|
127
143
|
await self.device_event_callback.data_event(device_event)
|
|
128
144
|
|
|
@@ -134,6 +150,7 @@ class StateManager:
|
|
|
134
150
|
await self.ble_get_commondata_ack_callback(comm_data)
|
|
135
151
|
|
|
136
152
|
async def get_plan_callback(self, planjob: NavPlanJobSet) -> None:
|
|
153
|
+
"""Dispatch plan job to available callback."""
|
|
137
154
|
if self.cloud_get_plan_callback:
|
|
138
155
|
await self.cloud_get_plan_callback(planjob)
|
|
139
156
|
elif self.ble_get_plan_callback:
|
|
@@ -180,6 +197,10 @@ class StateManager:
|
|
|
180
197
|
)
|
|
181
198
|
if updated:
|
|
182
199
|
await self.get_commondata_ack_callback(common_data)
|
|
200
|
+
case "cover_path_upload":
|
|
201
|
+
mow_path: CoverPathUploadT = nav_msg[1]
|
|
202
|
+
self._device.map.update_mow_path(MowPath.from_dict(mow_path.to_dict(casing=betterproto2.Casing.SNAKE)))
|
|
203
|
+
|
|
183
204
|
case "todev_planjob_set":
|
|
184
205
|
planjob: NavPlanJobSet = nav_msg[1]
|
|
185
206
|
self._device.map.update_plan(Plan.from_dict(planjob.to_dict(casing=betterproto2.Casing.SNAKE)))
|
|
@@ -244,9 +265,19 @@ class StateManager:
|
|
|
244
265
|
self._device.mower_state.swversion = device_fw_info.version
|
|
245
266
|
|
|
246
267
|
def _update_driver_data(self, message) -> None:
|
|
247
|
-
|
|
268
|
+
"""Update driver data."""
|
|
269
|
+
driver_msg = betterproto2.which_one_of(message.driver, "SubDrvMsg")
|
|
270
|
+
match driver_msg[0]:
|
|
271
|
+
case "current_cutter_mode":
|
|
272
|
+
cutter_work_mode: AppGetCutterWorkMode = driver_msg[1]
|
|
273
|
+
self._device.mower_state.cutter_mode = cutter_work_mode.current_cutter_mode
|
|
274
|
+
self._device.mower_state.cutter_rpm = cutter_work_mode.current_cutter_rpm
|
|
275
|
+
case "cutter_mode_ctrl_by_hand":
|
|
276
|
+
cutter_work_mode_set: AppSetCutterWorkMode = driver_msg[1]
|
|
277
|
+
self._device.mower_state.cutter_mode = cutter_work_mode_set.cutter_mode
|
|
248
278
|
|
|
249
279
|
def _update_net_data(self, message) -> None:
|
|
280
|
+
"""Update network data."""
|
|
250
281
|
net_msg = betterproto2.which_one_of(message.net, "NetSubType")
|
|
251
282
|
match net_msg[0]:
|
|
252
283
|
case "toapp_wifi_iot_status":
|
|
@@ -264,15 +295,21 @@ class StateManager:
|
|
|
264
295
|
|
|
265
296
|
def _update_mul_data(self, message) -> None:
|
|
266
297
|
"""Media and video states."""
|
|
267
|
-
mul_msg = betterproto2.which_one_of(message.
|
|
298
|
+
mul_msg = betterproto2.which_one_of(message.mul, "SubMul")
|
|
268
299
|
match mul_msg[0]:
|
|
269
|
-
case "
|
|
300
|
+
case "get_lamp_rsp":
|
|
270
301
|
lamp_resp: Getlamprsp = mul_msg[1]
|
|
271
302
|
self._device.mower_state.lamp_info.lamp_bright = lamp_resp.lamp_bright
|
|
272
|
-
if lamp_resp.get_ids
|
|
273
|
-
self._device.mower_state.lamp_info.
|
|
303
|
+
if lamp_resp.get_ids in (1126, 1127):
|
|
304
|
+
self._device.mower_state.lamp_info.lamp_bright = lamp_resp.lamp_bright
|
|
305
|
+
self._device.mower_state.lamp_info.manual_light = bool(lamp_resp.lamp_manual_ctrl.value) or bool(
|
|
306
|
+
lamp_resp.lamp_bright
|
|
307
|
+
)
|
|
274
308
|
if lamp_resp.get_ids == 1123:
|
|
275
|
-
self._device.mower_state.lamp_info.
|
|
309
|
+
self._device.mower_state.lamp_info.lamp_bright = lamp_resp.lamp_bright
|
|
310
|
+
self._device.mower_state.lamp_info.night_light = bool(lamp_resp.lamp_ctrl.value) or bool(
|
|
311
|
+
lamp_resp.lamp_bright
|
|
312
|
+
)
|
|
276
313
|
|
|
277
314
|
def _update_ota_data(self, message) -> None:
|
|
278
|
-
|
|
315
|
+
"""Update OTA data."""
|
pymammotion/data/mqtt/event.py
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
from base64 import b64decode
|
|
2
2
|
from dataclasses import dataclass
|
|
3
|
-
from typing import Any, Literal
|
|
3
|
+
from typing import Annotated, Any, Literal
|
|
4
4
|
|
|
5
5
|
from google.protobuf import json_format
|
|
6
6
|
from mashumaro.mixins.orjson import DataClassORJSONMixin
|
|
7
|
-
from mashumaro.types import SerializableType
|
|
7
|
+
from mashumaro.types import Alias, SerializableType
|
|
8
8
|
|
|
9
9
|
from pymammotion.proto import luba_msg_pb2
|
|
10
10
|
|
|
@@ -73,34 +73,35 @@ class DeviceBizReqEventValue(DataClassORJSONMixin):
|
|
|
73
73
|
|
|
74
74
|
@dataclass
|
|
75
75
|
class GeneralParams(DataClassORJSONMixin):
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
76
|
+
group_id_list: Annotated[list[str], Alias("groupIdList")]
|
|
77
|
+
group_id: Annotated[str, Alias("groupId")]
|
|
78
|
+
category_key: Annotated[Literal["LawnMower", "Tracker"], Alias("categoryKey")]
|
|
79
|
+
batch_id: Annotated[str, Alias("batchId")]
|
|
80
|
+
gmt_create: Annotated[int, Alias("gmtCreate")]
|
|
81
|
+
product_key: Annotated[str, Alias("productKey")]
|
|
82
82
|
type: str
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
device_name: Annotated[str, Alias("deviceName")]
|
|
84
|
+
iot_id: Annotated[str, Alias("iotId")]
|
|
85
|
+
check_level: Annotated[int, Alias("checkLevel")]
|
|
86
86
|
namespace: str
|
|
87
|
-
|
|
87
|
+
tenant_id: Annotated[str, Alias("tenantId")]
|
|
88
88
|
name: str
|
|
89
|
-
|
|
89
|
+
thing_type: Annotated[Literal["DEVICE"], Alias("thingType")]
|
|
90
90
|
time: int
|
|
91
|
-
|
|
91
|
+
tenant_instance_id: Annotated[str, Alias("tenantInstanceId")]
|
|
92
92
|
value: Any
|
|
93
93
|
|
|
94
|
+
# Optional fields
|
|
94
95
|
identifier: str | None = None
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
check_failed_data: Annotated[dict | None, Alias("checkFailedData")] = None
|
|
97
|
+
_tenant_id: Annotated[str | None, Alias("_tenantId")] = None
|
|
98
|
+
generate_time: Annotated[int | None, Alias("generateTime")] = None
|
|
99
|
+
jmsx_delivery_count: Annotated[int | None, Alias("JMSXDeliveryCount")] = None
|
|
99
100
|
qos: int | None = None
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
request_id: Annotated[str | None, Alias("requestId")] = None
|
|
102
|
+
_category_key: Annotated[str | None, Alias("_categoryKey")] = None
|
|
103
|
+
device_type: Annotated[str | None, Alias("deviceType")] = None
|
|
104
|
+
_trace_id: Annotated[str | None, Alias("_traceId")] = None
|
|
104
105
|
|
|
105
106
|
|
|
106
107
|
@dataclass
|
|
@@ -196,3 +197,27 @@ class ThingEventMessage(DataClassORJSONMixin):
|
|
|
196
197
|
raise ValueError(f"Unknown identifier: {identifier} {params_dict}")
|
|
197
198
|
|
|
198
199
|
return cls(method=method, id=event_id, params=params_obj, version=version)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@dataclass
|
|
203
|
+
class MammotionProtoMsgParams(DataClassORJSONMixin, SerializableType):
|
|
204
|
+
value: DeviceProtobufMsgEventValue
|
|
205
|
+
iot_id: str = ""
|
|
206
|
+
product_key: str = ""
|
|
207
|
+
device_name: str = ""
|
|
208
|
+
|
|
209
|
+
@classmethod
|
|
210
|
+
def _deserialize(cls, d: dict[str, Any]) -> "MammotionProtoMsgParams":
|
|
211
|
+
"""Override from_dict to allow dict manipulation before conversion."""
|
|
212
|
+
proto: str = d["content"]
|
|
213
|
+
|
|
214
|
+
return cls(value=DeviceProtobufMsgEventValue(content=proto))
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@dataclass
|
|
218
|
+
class MammotionEventMessage(DataClassORJSONMixin):
|
|
219
|
+
id: str
|
|
220
|
+
version: str
|
|
221
|
+
sys: dict
|
|
222
|
+
params: MammotionProtoMsgParams
|
|
223
|
+
method: str
|