pymammotion 0.5.34__py3-none-any.whl → 0.5.53__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 +169 -21
- pymammotion/const.py +3 -0
- pymammotion/data/model/device.py +22 -9
- pymammotion/data/model/device_config.py +1 -1
- pymammotion/data/model/device_info.py +1 -0
- pymammotion/data/model/enums.py +5 -3
- pymammotion/data/model/events.py +14 -0
- pymammotion/data/model/generate_geojson.py +551 -0
- pymammotion/data/model/generate_route_information.py +2 -2
- pymammotion/data/model/hash_list.py +129 -33
- pymammotion/data/model/location.py +4 -4
- pymammotion/data/model/region_data.py +4 -4
- pymammotion/data/model/report_info.py +7 -0
- pymammotion/data/{state_manager.py → mower_state_manager.py} +75 -11
- 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/event/event.py +5 -2
- pymammotion/homeassistant/__init__.py +3 -0
- pymammotion/homeassistant/mower_api.py +484 -0
- pymammotion/homeassistant/rtk_api.py +54 -0
- pymammotion/http/http.py +394 -14
- pymammotion/http/model/http.py +82 -2
- pymammotion/http/model/response_factory.py +10 -4
- pymammotion/mammotion/commands/mammotion_command.py +6 -0
- pymammotion/mammotion/commands/messages/navigation.py +39 -6
- pymammotion/mammotion/devices/__init__.py +27 -3
- pymammotion/mammotion/devices/base.py +16 -138
- pymammotion/mammotion/devices/mammotion.py +369 -200
- pymammotion/mammotion/devices/mammotion_bluetooth.py +7 -5
- pymammotion/mammotion/devices/mammotion_cloud.py +42 -83
- 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 +124 -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 +3 -3
- 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/proto/mctrl_sys_pb2.py +1 -1
- pymammotion/utility/constant/device_constant.py +1 -1
- pymammotion/utility/datatype_converter.py +13 -12
- pymammotion/utility/device_type.py +88 -3
- pymammotion/utility/map.py +238 -51
- pymammotion/utility/mur_mur_hash.py +132 -87
- {pymammotion-0.5.34.dist-info → pymammotion-0.5.53.dist-info}/METADATA +26 -31
- {pymammotion-0.5.34.dist-info → pymammotion-0.5.53.dist-info}/RECORD +67 -51
- {pymammotion-0.5.34.dist-info → pymammotion-0.5.53.dist-info}/WHEEL +1 -1
- pymammotion/http/_init_.py +0 -0
- {pymammotion-0.5.34.dist-info → pymammotion-0.5.53.dist-info/licenses}/LICENSE +0 -0
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
from dataclasses import dataclass, field
|
|
2
2
|
from enum import IntEnum
|
|
3
|
+
from typing import Any
|
|
3
4
|
|
|
4
5
|
from mashumaro.mixins.orjson import DataClassORJSONMixin
|
|
5
6
|
|
|
6
7
|
from pymammotion.proto import NavGetCommDataAck, NavGetHashListAck, SvgMessageAckT
|
|
8
|
+
from pymammotion.utility.mur_mur_hash import MurMurHashUtil
|
|
7
9
|
|
|
8
10
|
|
|
9
11
|
class PathType(IntEnum):
|
|
@@ -28,6 +30,13 @@ class AreaLabelName(DataClassORJSONMixin):
|
|
|
28
30
|
label: str = ""
|
|
29
31
|
|
|
30
32
|
|
|
33
|
+
@dataclass
|
|
34
|
+
class NavNameTime(DataClassORJSONMixin):
|
|
35
|
+
name: str = ""
|
|
36
|
+
create_time: int = 0
|
|
37
|
+
modify_time: int = 0
|
|
38
|
+
|
|
39
|
+
|
|
31
40
|
@dataclass
|
|
32
41
|
class NavGetCommData(DataClassORJSONMixin):
|
|
33
42
|
pver: int = 0
|
|
@@ -44,7 +53,35 @@ class NavGetCommData(DataClassORJSONMixin):
|
|
|
44
53
|
data_len: int = 0
|
|
45
54
|
data_couple: list["CommDataCouple"] = field(default_factory=list)
|
|
46
55
|
reserved: str = ""
|
|
47
|
-
|
|
56
|
+
name_time: NavNameTime = field(default_factory=NavNameTime)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class MowPathPacket(DataClassORJSONMixin):
|
|
61
|
+
path_hash: int = 0
|
|
62
|
+
path_type: int = 0
|
|
63
|
+
path_total: int = 0
|
|
64
|
+
path_cur: int = 0
|
|
65
|
+
zone_hash: int = 0
|
|
66
|
+
data_couple: list["CommDataCouple"] = field(default_factory=list)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class MowPath(DataClassORJSONMixin):
|
|
71
|
+
pver: int = 0
|
|
72
|
+
sub_cmd: int = 0
|
|
73
|
+
result: int = 0
|
|
74
|
+
area: int = 0
|
|
75
|
+
time: int = 0
|
|
76
|
+
total_frame: int = 0
|
|
77
|
+
current_frame: int = 0
|
|
78
|
+
total_path_num: int = 0
|
|
79
|
+
valid_path_num: int = 0
|
|
80
|
+
data_hash: int = 0
|
|
81
|
+
transaction_id: int = 0
|
|
82
|
+
reserved: list[int] = field(default_factory=list)
|
|
83
|
+
data_len: int = 0
|
|
84
|
+
path_packets: list[MowPathPacket] = field(default_factory=list)
|
|
48
85
|
|
|
49
86
|
|
|
50
87
|
@dataclass
|
|
@@ -172,13 +209,26 @@ class HashList(DataClassORJSONMixin):
|
|
|
172
209
|
line: dict[int, FrameList] = field(default_factory=dict) # type 10 possibly breakpoint? / sub cmd 3
|
|
173
210
|
plan: dict[str, Plan] = field(default_factory=dict)
|
|
174
211
|
area_name: list[AreaHashNameList] = field(default_factory=list)
|
|
212
|
+
current_mow_path: dict[int, MowPath] = field(default_factory=dict)
|
|
213
|
+
generated_geojson: dict[str, Any] = field(default_factory=dict)
|
|
214
|
+
generated_mow_path_geojson: dict[str, Any] = field(default_factory=dict)
|
|
175
215
|
|
|
176
|
-
def update_hash_lists(self, hashlist: list[int]) -> None:
|
|
216
|
+
def update_hash_lists(self, hashlist: list[int], bol_hash: str | None = None) -> None:
|
|
217
|
+
if bol_hash:
|
|
218
|
+
self.invalidate_maps(int(bol_hash))
|
|
177
219
|
self.area = {hash_id: frames for hash_id, frames in self.area.items() if hash_id in hashlist}
|
|
178
220
|
self.path = {hash_id: frames for hash_id, frames in self.path.items() if hash_id in hashlist}
|
|
179
221
|
self.obstacle = {hash_id: frames for hash_id, frames in self.obstacle.items() if hash_id in hashlist}
|
|
180
222
|
self.dump = {hash_id: frames for hash_id, frames in self.dump.items() if hash_id in hashlist}
|
|
181
223
|
self.svg = {hash_id: frames for hash_id, frames in self.svg.items() if hash_id in hashlist}
|
|
224
|
+
|
|
225
|
+
area_hashes = list(self.area.keys())
|
|
226
|
+
for hash_id, plan_task in self.plan.copy().items():
|
|
227
|
+
for item in plan_task.zone_hashs:
|
|
228
|
+
if item not in area_hashes:
|
|
229
|
+
self.plan.pop(hash_id)
|
|
230
|
+
break
|
|
231
|
+
|
|
182
232
|
self.area_name = [
|
|
183
233
|
area_item
|
|
184
234
|
for area_item in self.area_name
|
|
@@ -192,6 +242,19 @@ class HashList(DataClassORJSONMixin):
|
|
|
192
242
|
# Combine data_couple from all RootHashLists
|
|
193
243
|
return [i for root_list in self.root_hash_lists for obj in root_list.data for i in obj.data_couple]
|
|
194
244
|
|
|
245
|
+
@property
|
|
246
|
+
def area_root_hashlist(self) -> list[int]:
|
|
247
|
+
if not self.root_hash_lists:
|
|
248
|
+
return []
|
|
249
|
+
# Combine data_couple from all RootHashLists
|
|
250
|
+
return [
|
|
251
|
+
i
|
|
252
|
+
for root_list in self.root_hash_lists
|
|
253
|
+
for obj in root_list.data
|
|
254
|
+
for i in obj.data_couple
|
|
255
|
+
if root_list.sub_cmd == 0
|
|
256
|
+
]
|
|
257
|
+
|
|
195
258
|
def missing_hashlist(self, sub_cmd: int = 0) -> list[int]:
|
|
196
259
|
"""Return missing hashlist."""
|
|
197
260
|
all_hash_ids = set(self.area.keys()).union(
|
|
@@ -259,61 +322,90 @@ class HashList(DataClassORJSONMixin):
|
|
|
259
322
|
return missing_frames
|
|
260
323
|
|
|
261
324
|
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))
|
|
325
|
+
frame_list = self._get_frame_list_by_type_and_hash(hash_data)
|
|
326
|
+
return self.find_missing_frames(frame_list)
|
|
267
327
|
|
|
268
|
-
|
|
269
|
-
|
|
328
|
+
def _get_frame_list_by_type_and_hash(self, hash_data: NavGetCommDataAck | SvgMessageAckT) -> FrameList | None:
|
|
329
|
+
"""Get the appropriate FrameList based on hash_data type and hash."""
|
|
330
|
+
path_type_mapping = self._get_path_type_mapping()
|
|
331
|
+
target_dict = path_type_mapping.get(hash_data.type)
|
|
270
332
|
|
|
271
|
-
if
|
|
272
|
-
return
|
|
333
|
+
if target_dict is None:
|
|
334
|
+
return None
|
|
273
335
|
|
|
274
|
-
|
|
275
|
-
|
|
336
|
+
# Handle SvgMessage with data_hash attribute
|
|
337
|
+
if isinstance(hash_data, SvgMessageAckT):
|
|
338
|
+
return target_dict.get(hash_data.data_hash)
|
|
276
339
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
return []
|
|
340
|
+
# Handle NavGetCommDataAck with hash attribute
|
|
341
|
+
return target_dict.get(hash_data.hash)
|
|
281
342
|
|
|
282
343
|
def update_plan(self, plan: Plan) -> None:
|
|
283
344
|
if plan.total_plan_num != 0:
|
|
284
345
|
self.plan[plan.plan_id] = plan
|
|
285
346
|
|
|
347
|
+
def _get_path_type_mapping(self) -> dict[int, dict[int, FrameList]]:
|
|
348
|
+
"""Return mapping of PathType to corresponding hash dictionary."""
|
|
349
|
+
return {
|
|
350
|
+
PathType.AREA: self.area,
|
|
351
|
+
PathType.OBSTACLE: self.obstacle,
|
|
352
|
+
PathType.PATH: self.path,
|
|
353
|
+
PathType.LINE: self.line,
|
|
354
|
+
PathType.DUMP: self.dump,
|
|
355
|
+
PathType.SVG: self.svg,
|
|
356
|
+
}
|
|
357
|
+
|
|
286
358
|
def update(self, hash_data: NavGetCommData | SvgMessage) -> bool:
|
|
287
359
|
"""Update the map data."""
|
|
288
360
|
|
|
289
|
-
if hash_data.type == PathType.AREA:
|
|
361
|
+
if hash_data.type == PathType.AREA and isinstance(hash_data, NavGetCommData):
|
|
290
362
|
existing_name = next((area for area in self.area_name if area.hash == hash_data.hash), None)
|
|
291
363
|
if not existing_name:
|
|
292
|
-
name = f"area {len(self.area_name)+1}"
|
|
364
|
+
name = f"area {len(self.area_name)+1}"
|
|
293
365
|
self.area_name.append(AreaHashNameList(name=name, hash=hash_data.hash))
|
|
294
366
|
result = self._add_hash_data(self.area, hash_data)
|
|
295
367
|
self.update_hash_lists(self.hashlist)
|
|
296
368
|
return result
|
|
297
369
|
|
|
298
|
-
|
|
299
|
-
|
|
370
|
+
path_type_mapping = self._get_path_type_mapping()
|
|
371
|
+
target_dict = path_type_mapping.get(hash_data.type)
|
|
300
372
|
|
|
301
|
-
if
|
|
302
|
-
return self._add_hash_data(
|
|
373
|
+
if target_dict is not None:
|
|
374
|
+
return self._add_hash_data(target_dict, hash_data)
|
|
303
375
|
|
|
304
|
-
|
|
305
|
-
return self._add_hash_data(self.line, hash_data)
|
|
376
|
+
return False
|
|
306
377
|
|
|
307
|
-
|
|
308
|
-
|
|
378
|
+
def find_missing_mow_path_frames(self) -> list[int]:
|
|
379
|
+
"""Find missing frames in current_mow_path based on total_frame."""
|
|
380
|
+
if not self.current_mow_path:
|
|
381
|
+
return []
|
|
309
382
|
|
|
310
|
-
|
|
311
|
-
|
|
383
|
+
# Get total_frame from any MowPath object (they should all have the same total_frame)
|
|
384
|
+
total_frame = next(iter(self.current_mow_path.values())).total_frame
|
|
312
385
|
|
|
313
|
-
|
|
386
|
+
if total_frame == 0:
|
|
387
|
+
return []
|
|
388
|
+
|
|
389
|
+
if total_frame == len(self.current_mow_path):
|
|
390
|
+
return []
|
|
391
|
+
|
|
392
|
+
# Generate list of expected frame numbers (1 to total_frame)
|
|
393
|
+
expected_frames = set(range(1, total_frame + 1))
|
|
394
|
+
|
|
395
|
+
# Get current frame numbers from dictionary keys
|
|
396
|
+
current_frames = set(self.current_mow_path.keys())
|
|
397
|
+
|
|
398
|
+
# Return sorted list of missing frames
|
|
399
|
+
missing_frames = sorted(expected_frames - current_frames)
|
|
400
|
+
return missing_frames
|
|
401
|
+
|
|
402
|
+
def update_mow_path(self, path: MowPath) -> None:
|
|
403
|
+
"""Update the current_mow_path with the latest MowPath data."""
|
|
404
|
+
# TODO check if we need to clear the current_mow_path first
|
|
405
|
+
self.current_mow_path[path.current_frame] = path
|
|
314
406
|
|
|
315
407
|
@staticmethod
|
|
316
|
-
def find_missing_frames(frame_list: FrameList | RootHashList) -> list[int]:
|
|
408
|
+
def find_missing_frames(frame_list: FrameList | RootHashList | None) -> list[int]:
|
|
317
409
|
if frame_list is None:
|
|
318
410
|
return []
|
|
319
411
|
|
|
@@ -328,7 +420,7 @@ class HashList(DataClassORJSONMixin):
|
|
|
328
420
|
@staticmethod
|
|
329
421
|
def _add_hash_data(hash_dict: dict[int, FrameList], hash_data: NavGetCommData | SvgMessage) -> bool:
|
|
330
422
|
if isinstance(hash_data, SvgMessage):
|
|
331
|
-
if hash_dict.get(hash_data.data_hash) is None:
|
|
423
|
+
if hash_dict.get(hash_data.data_hash, None) is None:
|
|
332
424
|
hash_dict[hash_data.data_hash] = FrameList(total_frame=hash_data.total_frame, data=[hash_data])
|
|
333
425
|
return True
|
|
334
426
|
|
|
@@ -347,7 +439,7 @@ class HashList(DataClassORJSONMixin):
|
|
|
347
439
|
return True
|
|
348
440
|
return False
|
|
349
441
|
|
|
350
|
-
if hash_dict.get(hash_data.hash) is None:
|
|
442
|
+
if hash_dict.get(hash_data.hash, None) is None:
|
|
351
443
|
hash_dict[hash_data.hash] = FrameList(total_frame=hash_data.total_frame, data=[hash_data])
|
|
352
444
|
return True
|
|
353
445
|
|
|
@@ -361,3 +453,7 @@ class HashList(DataClassORJSONMixin):
|
|
|
361
453
|
hash_dict[hash_data.hash].data.append(hash_data)
|
|
362
454
|
return True
|
|
363
455
|
return False
|
|
456
|
+
|
|
457
|
+
def invalidate_maps(self, bol_hash: int) -> None:
|
|
458
|
+
if MurMurHashUtil.hash_unsigned_list(self.area_root_hashlist) != bol_hash:
|
|
459
|
+
self.root_hash_lists = []
|
|
@@ -6,7 +6,7 @@ from mashumaro.mixins.orjson import DataClassORJSONMixin
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
@dataclass
|
|
9
|
-
class
|
|
9
|
+
class LocationPoint(DataClassORJSONMixin):
|
|
10
10
|
"""Returns a lat long."""
|
|
11
11
|
|
|
12
12
|
latitude: float = 0.0
|
|
@@ -18,7 +18,7 @@ class Point(DataClassORJSONMixin):
|
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
@dataclass
|
|
21
|
-
class Dock(
|
|
21
|
+
class Dock(LocationPoint):
|
|
22
22
|
"""Stores robot dock position."""
|
|
23
23
|
|
|
24
24
|
rotation: int = 0
|
|
@@ -28,8 +28,8 @@ class Dock(Point):
|
|
|
28
28
|
class Location(DataClassORJSONMixin):
|
|
29
29
|
"""Stores/retrieves RTK GPS data."""
|
|
30
30
|
|
|
31
|
-
device:
|
|
32
|
-
RTK:
|
|
31
|
+
device: LocationPoint = field(default_factory=LocationPoint)
|
|
32
|
+
RTK: LocationPoint = field(default_factory=LocationPoint)
|
|
33
33
|
dock: Dock = field(default_factory=Dock)
|
|
34
34
|
position_type: int = 0
|
|
35
35
|
orientation: int = 0 # 360 degree rotation +-
|
|
@@ -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
|
|
@@ -79,8 +79,15 @@ class LocationData(DataClassORJSONMixin):
|
|
|
79
79
|
bol_hash: str = ""
|
|
80
80
|
|
|
81
81
|
|
|
82
|
+
@dataclass
|
|
83
|
+
class BladeUsed(DataClassORJSONMixin):
|
|
84
|
+
blade_used_time: int = 0
|
|
85
|
+
blade_used_warn_time: int = 0
|
|
86
|
+
|
|
87
|
+
|
|
82
88
|
@dataclass
|
|
83
89
|
class Maintain(DataClassORJSONMixin):
|
|
90
|
+
blade_used_time: BladeUsed = field(default_factory=BladeUsed)
|
|
84
91
|
mileage: int = 0
|
|
85
92
|
work_time: int = 0
|
|
86
93
|
bat_cycles: int = 0
|
|
@@ -6,11 +6,20 @@ import logging
|
|
|
6
6
|
from typing import Any
|
|
7
7
|
|
|
8
8
|
import betterproto2
|
|
9
|
+
from shapely import Point
|
|
9
10
|
|
|
10
11
|
from pymammotion.data.model.device import MowingDevice
|
|
11
12
|
from pymammotion.data.model.device_info import SideLight
|
|
12
|
-
from pymammotion.data.model.
|
|
13
|
-
from pymammotion.data.model.hash_list import
|
|
13
|
+
from pymammotion.data.model.generate_geojson import GeojsonGenerator
|
|
14
|
+
from pymammotion.data.model.hash_list import (
|
|
15
|
+
AreaHashNameList,
|
|
16
|
+
MowPath,
|
|
17
|
+
NavGetCommData,
|
|
18
|
+
NavGetHashListData,
|
|
19
|
+
Plan,
|
|
20
|
+
SvgMessage,
|
|
21
|
+
)
|
|
22
|
+
from pymammotion.data.model.location import Dock, LocationPoint
|
|
14
23
|
from pymammotion.data.model.work import CurrentTaskSettings
|
|
15
24
|
from pymammotion.data.mqtt.event import ThingEventMessage
|
|
16
25
|
from pymammotion.data.mqtt.properties import ThingPropertiesMessage
|
|
@@ -20,6 +29,7 @@ from pymammotion.proto import (
|
|
|
20
29
|
AppGetAllAreaHashName,
|
|
21
30
|
AppGetCutterWorkMode,
|
|
22
31
|
AppSetCutterWorkMode,
|
|
32
|
+
CoverPathUploadT,
|
|
23
33
|
DeviceFwInfo,
|
|
24
34
|
DeviceProductTypeInfoT,
|
|
25
35
|
DrvDevInfoResp,
|
|
@@ -37,18 +47,18 @@ from pymammotion.proto import (
|
|
|
37
47
|
TimeCtrlLight,
|
|
38
48
|
WifiIotStatusReport,
|
|
39
49
|
)
|
|
50
|
+
from pymammotion.utility.map import CoordinateConverter
|
|
40
51
|
|
|
41
52
|
logger = logging.getLogger(__name__)
|
|
42
53
|
|
|
43
54
|
|
|
44
|
-
class
|
|
55
|
+
class MowerStateManager:
|
|
45
56
|
"""Manage state."""
|
|
46
57
|
|
|
47
58
|
def __init__(self, device: MowingDevice) -> None:
|
|
48
59
|
"""Initialize state manager with a device."""
|
|
49
60
|
self._device: MowingDevice = device
|
|
50
61
|
self.last_updated_at = datetime.now(UTC)
|
|
51
|
-
self.preference = ConnectionPreference.WIFI
|
|
52
62
|
self.cloud_gethash_ack_callback: Callable[[NavGetHashListAck], Awaitable[None]] | None = None
|
|
53
63
|
self.cloud_get_commondata_ack_callback: (
|
|
54
64
|
Callable[[NavGetCommDataAck | SvgMessageAckT], Awaitable[None]] | None
|
|
@@ -132,7 +142,7 @@ class StateManager:
|
|
|
132
142
|
await self.status_callback.data_event(thing_status)
|
|
133
143
|
|
|
134
144
|
async def on_device_event_callback(self, device_event: ThingEventMessage) -> None:
|
|
135
|
-
"""Executes the
|
|
145
|
+
"""Executes the event callback if it is set."""
|
|
136
146
|
if self.device_event_callback:
|
|
137
147
|
await self.device_event_callback.data_event(device_event)
|
|
138
148
|
|
|
@@ -150,6 +160,13 @@ class StateManager:
|
|
|
150
160
|
elif self.ble_get_plan_callback:
|
|
151
161
|
await self.ble_get_plan_callback(planjob)
|
|
152
162
|
|
|
163
|
+
async def queue_command_callback(self, **kwargs: Any) -> None:
|
|
164
|
+
"""Queue command to available callback."""
|
|
165
|
+
if self.ble_queue_command_callback:
|
|
166
|
+
await self.ble_queue_command_callback.data_event(**kwargs)
|
|
167
|
+
elif self.cloud_queue_command_callback:
|
|
168
|
+
await self.cloud_queue_command_callback.data_event(**kwargs)
|
|
169
|
+
|
|
153
170
|
async def notification(self, message: LubaMsg) -> None:
|
|
154
171
|
"""Handle protobuf notifications."""
|
|
155
172
|
res = betterproto2.which_one_of(message, "LubaSubMsg")
|
|
@@ -174,7 +191,7 @@ class StateManager:
|
|
|
174
191
|
|
|
175
192
|
await self.on_notification_callback(res)
|
|
176
193
|
|
|
177
|
-
async def _update_nav_data(self, message) -> None:
|
|
194
|
+
async def _update_nav_data(self, message: LubaMsg) -> None:
|
|
178
195
|
"""Update nav data."""
|
|
179
196
|
nav_msg = betterproto2.which_one_of(message.nav, "SubNavMsg")
|
|
180
197
|
match nav_msg[0]:
|
|
@@ -190,7 +207,16 @@ class StateManager:
|
|
|
190
207
|
NavGetCommData.from_dict(common_data.to_dict(casing=betterproto2.Casing.SNAKE))
|
|
191
208
|
)
|
|
192
209
|
if updated:
|
|
210
|
+
if len(self._device.map.missing_hashlist(0)) == 0:
|
|
211
|
+
self.generate_geojson(self._device.location.RTK, self._device.location.dock)
|
|
212
|
+
|
|
193
213
|
await self.get_commondata_ack_callback(common_data)
|
|
214
|
+
case "cover_path_upload":
|
|
215
|
+
mow_path: CoverPathUploadT = nav_msg[1]
|
|
216
|
+
self._device.map.update_mow_path(MowPath.from_dict(mow_path.to_dict(casing=betterproto2.Casing.SNAKE)))
|
|
217
|
+
if len(self._device.map.find_missing_mow_path_frames()) == 0:
|
|
218
|
+
self.generate_mowing_geojson(self._device.location.RTK)
|
|
219
|
+
|
|
194
220
|
case "todev_planjob_set":
|
|
195
221
|
planjob: NavPlanJobSet = nav_msg[1]
|
|
196
222
|
self._device.map.update_plan(Plan.from_dict(planjob.to_dict(casing=betterproto2.Casing.SNAKE)))
|
|
@@ -211,9 +237,17 @@ class StateManager:
|
|
|
211
237
|
|
|
212
238
|
case "bidire_reqconver_path":
|
|
213
239
|
work_settings: NavReqCoverPath = nav_msg[1]
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
240
|
+
|
|
241
|
+
current_task = CurrentTaskSettings.from_dict(work_settings.to_dict(casing=betterproto2.Casing.SNAKE))
|
|
242
|
+
|
|
243
|
+
if current_task.path_hash == 0:
|
|
244
|
+
self._device.map.current_mow_path = {}
|
|
245
|
+
|
|
246
|
+
if current_task.path_hash != self._device.work.path_hash:
|
|
247
|
+
await self.queue_command_callback(command="get_all_boundary_hash_list", sub_cmd=3)
|
|
248
|
+
|
|
249
|
+
self._device.work = current_task
|
|
250
|
+
|
|
217
251
|
case "nav_sys_param_cmd":
|
|
218
252
|
settings: NavSysParamMsg = nav_msg[1]
|
|
219
253
|
match settings.id:
|
|
@@ -225,6 +259,7 @@ class StateManager:
|
|
|
225
259
|
self._device.mower_state.traversal_mode = settings.context
|
|
226
260
|
case "todev_unable_time_set":
|
|
227
261
|
nav_non_work_time: NavUnableTimeSet = nav_msg[1]
|
|
262
|
+
self._device.non_work_hours.non_work_sub_cmd = nav_non_work_time.sub_cmd
|
|
228
263
|
self._device.non_work_hours.start_time = nav_non_work_time.unable_start_time
|
|
229
264
|
self._device.non_work_hours.end_time = nav_non_work_time.unable_end_time
|
|
230
265
|
|
|
@@ -263,8 +298,8 @@ class StateManager:
|
|
|
263
298
|
self._device.mower_state.cutter_mode = cutter_work_mode.current_cutter_mode
|
|
264
299
|
self._device.mower_state.cutter_rpm = cutter_work_mode.current_cutter_rpm
|
|
265
300
|
case "cutter_mode_ctrl_by_hand":
|
|
266
|
-
|
|
267
|
-
self._device.mower_state.cutter_mode =
|
|
301
|
+
cutter_work_mode_set: AppSetCutterWorkMode = driver_msg[1]
|
|
302
|
+
self._device.mower_state.cutter_mode = cutter_work_mode_set.cutter_mode
|
|
268
303
|
|
|
269
304
|
def _update_net_data(self, message) -> None:
|
|
270
305
|
"""Update network data."""
|
|
@@ -303,3 +338,32 @@ class StateManager:
|
|
|
303
338
|
|
|
304
339
|
def _update_ota_data(self, message) -> None:
|
|
305
340
|
"""Update OTA data."""
|
|
341
|
+
|
|
342
|
+
def generate_geojson(self, rtk: LocationPoint, dock: Dock) -> Any:
|
|
343
|
+
"""Generate geojson from frames."""
|
|
344
|
+
coordinator_converter = CoordinateConverter(rtk.latitude, rtk.longitude)
|
|
345
|
+
RTK_real_loc = coordinator_converter.enu_to_lla(0, 0)
|
|
346
|
+
|
|
347
|
+
dock_location = coordinator_converter.enu_to_lla(dock.latitude, dock.longitude)
|
|
348
|
+
dock_rotation = coordinator_converter.get_transform_yaw_with_yaw(dock.rotation) + 180
|
|
349
|
+
|
|
350
|
+
self._device.map.generated_geojson = GeojsonGenerator.generate_geojson(
|
|
351
|
+
self._device.map,
|
|
352
|
+
Point(RTK_real_loc.latitude, RTK_real_loc.longitude),
|
|
353
|
+
Point(dock_location.latitude, dock_location.longitude),
|
|
354
|
+
int(dock_rotation),
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
return self._device.map.generated_geojson
|
|
358
|
+
|
|
359
|
+
def generate_mowing_geojson(self, rtk: LocationPoint) -> Any:
|
|
360
|
+
"""Generate geojson from frames."""
|
|
361
|
+
coordinator_converter = CoordinateConverter(rtk.latitude, rtk.longitude)
|
|
362
|
+
RTK_real_loc = coordinator_converter.enu_to_lla(0, 0)
|
|
363
|
+
|
|
364
|
+
self._device.map.generated_mow_path_geojson = GeojsonGenerator.generate_mow_path_geojson(
|
|
365
|
+
self._device.map,
|
|
366
|
+
Point(RTK_real_loc.latitude, RTK_real_loc.longitude),
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
return self._device.map.generated_mow_path_geojson
|
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
|