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
|
@@ -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,8 +208,11 @@ 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}
|
|
@@ -259,61 +298,90 @@ class HashList(DataClassORJSONMixin):
|
|
|
259
298
|
return missing_frames
|
|
260
299
|
|
|
261
300
|
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))
|
|
301
|
+
frame_list = self._get_frame_list_by_type_and_hash(hash_data)
|
|
302
|
+
return self.find_missing_frames(frame_list)
|
|
267
303
|
|
|
268
|
-
|
|
269
|
-
|
|
304
|
+
def _get_frame_list_by_type_and_hash(self, hash_data: NavGetCommDataAck | SvgMessageAckT) -> FrameList | None:
|
|
305
|
+
"""Get the appropriate FrameList based on hash_data type and hash."""
|
|
306
|
+
path_type_mapping = self._get_path_type_mapping()
|
|
307
|
+
target_dict = path_type_mapping.get(hash_data.type)
|
|
270
308
|
|
|
271
|
-
if
|
|
272
|
-
return
|
|
309
|
+
if target_dict is None:
|
|
310
|
+
return None
|
|
273
311
|
|
|
274
|
-
|
|
275
|
-
|
|
312
|
+
# Handle SvgMessage with data_hash attribute
|
|
313
|
+
if isinstance(hash_data, SvgMessageAckT):
|
|
314
|
+
return target_dict.get(hash_data.data_hash)
|
|
276
315
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
return []
|
|
316
|
+
# Handle NavGetCommDataAck with hash attribute
|
|
317
|
+
return target_dict.get(hash_data.hash)
|
|
281
318
|
|
|
282
319
|
def update_plan(self, plan: Plan) -> None:
|
|
283
320
|
if plan.total_plan_num != 0:
|
|
284
321
|
self.plan[plan.plan_id] = plan
|
|
285
322
|
|
|
323
|
+
def _get_path_type_mapping(self) -> dict[int, dict[int, FrameList]]:
|
|
324
|
+
"""Return mapping of PathType to corresponding hash dictionary."""
|
|
325
|
+
return {
|
|
326
|
+
PathType.AREA: self.area,
|
|
327
|
+
PathType.OBSTACLE: self.obstacle,
|
|
328
|
+
PathType.PATH: self.path,
|
|
329
|
+
PathType.LINE: self.line,
|
|
330
|
+
PathType.DUMP: self.dump,
|
|
331
|
+
PathType.SVG: self.svg,
|
|
332
|
+
}
|
|
333
|
+
|
|
286
334
|
def update(self, hash_data: NavGetCommData | SvgMessage) -> bool:
|
|
287
335
|
"""Update the map data."""
|
|
288
336
|
|
|
289
|
-
if hash_data.type == PathType.AREA:
|
|
337
|
+
if hash_data.type == PathType.AREA and isinstance(hash_data, NavGetCommData):
|
|
290
338
|
existing_name = next((area for area in self.area_name if area.hash == hash_data.hash), None)
|
|
291
339
|
if not existing_name:
|
|
292
|
-
name = f"area {len(self.area_name)+1}"
|
|
340
|
+
name = f"area {len(self.area_name)+1}"
|
|
293
341
|
self.area_name.append(AreaHashNameList(name=name, hash=hash_data.hash))
|
|
294
342
|
result = self._add_hash_data(self.area, hash_data)
|
|
295
343
|
self.update_hash_lists(self.hashlist)
|
|
296
344
|
return result
|
|
297
345
|
|
|
298
|
-
|
|
299
|
-
|
|
346
|
+
path_type_mapping = self._get_path_type_mapping()
|
|
347
|
+
target_dict = path_type_mapping.get(hash_data.type)
|
|
300
348
|
|
|
301
|
-
if
|
|
302
|
-
return self._add_hash_data(
|
|
349
|
+
if target_dict is not None:
|
|
350
|
+
return self._add_hash_data(target_dict, hash_data)
|
|
303
351
|
|
|
304
|
-
|
|
305
|
-
return self._add_hash_data(self.line, hash_data)
|
|
352
|
+
return False
|
|
306
353
|
|
|
307
|
-
|
|
308
|
-
|
|
354
|
+
def find_missing_mow_path_frames(self) -> list[int]:
|
|
355
|
+
"""Find missing frames in current_mow_path based on total_frame."""
|
|
356
|
+
if not self.current_mow_path:
|
|
357
|
+
return []
|
|
309
358
|
|
|
310
|
-
|
|
311
|
-
|
|
359
|
+
# Get total_frame from any MowPath object (they should all have the same total_frame)
|
|
360
|
+
total_frame = next(iter(self.current_mow_path.values())).total_frame
|
|
312
361
|
|
|
313
|
-
|
|
362
|
+
if total_frame == 0:
|
|
363
|
+
return []
|
|
364
|
+
|
|
365
|
+
if total_frame == len(self.current_mow_path):
|
|
366
|
+
return []
|
|
367
|
+
|
|
368
|
+
# Generate list of expected frame numbers (1 to total_frame)
|
|
369
|
+
expected_frames = set(range(1, total_frame + 1))
|
|
370
|
+
|
|
371
|
+
# Get current frame numbers from dictionary keys
|
|
372
|
+
current_frames = set(self.current_mow_path.keys())
|
|
373
|
+
|
|
374
|
+
# Return sorted list of missing frames
|
|
375
|
+
missing_frames = sorted(expected_frames - current_frames)
|
|
376
|
+
return missing_frames
|
|
377
|
+
|
|
378
|
+
def update_mow_path(self, path: MowPath) -> None:
|
|
379
|
+
"""Update the current_mow_path with the latest MowPath data."""
|
|
380
|
+
# TODO check if we need to clear the current_mow_path first
|
|
381
|
+
self.current_mow_path[path.current_frame] = path
|
|
314
382
|
|
|
315
383
|
@staticmethod
|
|
316
|
-
def find_missing_frames(frame_list: FrameList | RootHashList) -> list[int]:
|
|
384
|
+
def find_missing_frames(frame_list: FrameList | RootHashList | None) -> list[int]:
|
|
317
385
|
if frame_list is None:
|
|
318
386
|
return []
|
|
319
387
|
|
|
@@ -328,7 +396,7 @@ class HashList(DataClassORJSONMixin):
|
|
|
328
396
|
@staticmethod
|
|
329
397
|
def _add_hash_data(hash_dict: dict[int, FrameList], hash_data: NavGetCommData | SvgMessage) -> bool:
|
|
330
398
|
if isinstance(hash_data, SvgMessage):
|
|
331
|
-
if hash_dict.get(hash_data.data_hash) is None:
|
|
399
|
+
if hash_dict.get(hash_data.data_hash, None) is None:
|
|
332
400
|
hash_dict[hash_data.data_hash] = FrameList(total_frame=hash_data.total_frame, data=[hash_data])
|
|
333
401
|
return True
|
|
334
402
|
|
|
@@ -347,7 +415,7 @@ class HashList(DataClassORJSONMixin):
|
|
|
347
415
|
return True
|
|
348
416
|
return False
|
|
349
417
|
|
|
350
|
-
if hash_dict.get(hash_data.hash) is None:
|
|
418
|
+
if hash_dict.get(hash_data.hash, None) is None:
|
|
351
419
|
hash_dict[hash_data.hash] = FrameList(total_frame=hash_data.total_frame, data=[hash_data])
|
|
352
420
|
return True
|
|
353
421
|
|
|
@@ -361,3 +429,7 @@ class HashList(DataClassORJSONMixin):
|
|
|
361
429
|
hash_dict[hash_data.hash].data.append(hash_data)
|
|
362
430
|
return True
|
|
363
431
|
return False
|
|
432
|
+
|
|
433
|
+
def invalidate_maps(self, bol_hash: int) -> None:
|
|
434
|
+
if MurMurHashUtil.hash_unsigned_list(self.hashlist) != bol_hash:
|
|
435
|
+
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
|
|
@@ -9,8 +9,14 @@ import betterproto2
|
|
|
9
9
|
|
|
10
10
|
from pymammotion.data.model.device import MowingDevice
|
|
11
11
|
from pymammotion.data.model.device_info import SideLight
|
|
12
|
-
from pymammotion.data.model.
|
|
13
|
-
|
|
12
|
+
from pymammotion.data.model.hash_list import (
|
|
13
|
+
AreaHashNameList,
|
|
14
|
+
MowPath,
|
|
15
|
+
NavGetCommData,
|
|
16
|
+
NavGetHashListData,
|
|
17
|
+
Plan,
|
|
18
|
+
SvgMessage,
|
|
19
|
+
)
|
|
14
20
|
from pymammotion.data.model.work import CurrentTaskSettings
|
|
15
21
|
from pymammotion.data.mqtt.event import ThingEventMessage
|
|
16
22
|
from pymammotion.data.mqtt.properties import ThingPropertiesMessage
|
|
@@ -20,6 +26,7 @@ from pymammotion.proto import (
|
|
|
20
26
|
AppGetAllAreaHashName,
|
|
21
27
|
AppGetCutterWorkMode,
|
|
22
28
|
AppSetCutterWorkMode,
|
|
29
|
+
CoverPathUploadT,
|
|
23
30
|
DeviceFwInfo,
|
|
24
31
|
DeviceProductTypeInfoT,
|
|
25
32
|
DrvDevInfoResp,
|
|
@@ -41,14 +48,13 @@ from pymammotion.proto import (
|
|
|
41
48
|
logger = logging.getLogger(__name__)
|
|
42
49
|
|
|
43
50
|
|
|
44
|
-
class
|
|
51
|
+
class MowerStateManager:
|
|
45
52
|
"""Manage state."""
|
|
46
53
|
|
|
47
54
|
def __init__(self, device: MowingDevice) -> None:
|
|
48
55
|
"""Initialize state manager with a device."""
|
|
49
56
|
self._device: MowingDevice = device
|
|
50
57
|
self.last_updated_at = datetime.now(UTC)
|
|
51
|
-
self.preference = ConnectionPreference.WIFI
|
|
52
58
|
self.cloud_gethash_ack_callback: Callable[[NavGetHashListAck], Awaitable[None]] | None = None
|
|
53
59
|
self.cloud_get_commondata_ack_callback: (
|
|
54
60
|
Callable[[NavGetCommDataAck | SvgMessageAckT], Awaitable[None]] | None
|
|
@@ -132,7 +138,7 @@ class StateManager:
|
|
|
132
138
|
await self.status_callback.data_event(thing_status)
|
|
133
139
|
|
|
134
140
|
async def on_device_event_callback(self, device_event: ThingEventMessage) -> None:
|
|
135
|
-
"""Executes the
|
|
141
|
+
"""Executes the event callback if it is set."""
|
|
136
142
|
if self.device_event_callback:
|
|
137
143
|
await self.device_event_callback.data_event(device_event)
|
|
138
144
|
|
|
@@ -191,6 +197,10 @@ class StateManager:
|
|
|
191
197
|
)
|
|
192
198
|
if updated:
|
|
193
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
|
+
|
|
194
204
|
case "todev_planjob_set":
|
|
195
205
|
planjob: NavPlanJobSet = nav_msg[1]
|
|
196
206
|
self._device.map.update_plan(Plan.from_dict(planjob.to_dict(casing=betterproto2.Casing.SNAKE)))
|
|
@@ -263,8 +273,8 @@ class StateManager:
|
|
|
263
273
|
self._device.mower_state.cutter_mode = cutter_work_mode.current_cutter_mode
|
|
264
274
|
self._device.mower_state.cutter_rpm = cutter_work_mode.current_cutter_rpm
|
|
265
275
|
case "cutter_mode_ctrl_by_hand":
|
|
266
|
-
|
|
267
|
-
self._device.mower_state.cutter_mode =
|
|
276
|
+
cutter_work_mode_set: AppSetCutterWorkMode = driver_msg[1]
|
|
277
|
+
self._device.mower_state.cutter_mode = cutter_work_mode_set.cutter_mode
|
|
268
278
|
|
|
269
279
|
def _update_net_data(self, message) -> None:
|
|
270
280
|
"""Update network data."""
|