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.

Files changed (47) hide show
  1. pymammotion/__init__.py +3 -3
  2. pymammotion/aliyun/cloud_gateway.py +106 -18
  3. pymammotion/aliyun/model/dev_by_account_response.py +198 -20
  4. pymammotion/data/model/device.py +1 -0
  5. pymammotion/data/model/device_config.py +1 -1
  6. pymammotion/data/model/enums.py +3 -1
  7. pymammotion/data/model/generate_route_information.py +2 -2
  8. pymammotion/data/model/hash_list.py +105 -33
  9. pymammotion/data/model/region_data.py +4 -4
  10. pymammotion/data/{state_manager.py → mower_state_manager.py} +17 -7
  11. pymammotion/homeassistant/__init__.py +3 -0
  12. pymammotion/homeassistant/mower_api.py +446 -0
  13. pymammotion/homeassistant/rtk_api.py +54 -0
  14. pymammotion/http/http.py +118 -7
  15. pymammotion/http/model/http.py +77 -2
  16. pymammotion/http/model/response_factory.py +10 -4
  17. pymammotion/mammotion/commands/mammotion_command.py +6 -0
  18. pymammotion/mammotion/commands/messages/navigation.py +10 -6
  19. pymammotion/mammotion/devices/__init__.py +27 -3
  20. pymammotion/mammotion/devices/base.py +16 -138
  21. pymammotion/mammotion/devices/mammotion.py +361 -204
  22. pymammotion/mammotion/devices/mammotion_bluetooth.py +7 -5
  23. pymammotion/mammotion/devices/mammotion_cloud.py +22 -74
  24. pymammotion/mammotion/devices/mammotion_mower_ble.py +49 -0
  25. pymammotion/mammotion/devices/mammotion_mower_cloud.py +39 -0
  26. pymammotion/mammotion/devices/managers/managers.py +81 -0
  27. pymammotion/mammotion/devices/mower_device.py +121 -0
  28. pymammotion/mammotion/devices/mower_manager.py +107 -0
  29. pymammotion/mammotion/devices/rtk_ble.py +89 -0
  30. pymammotion/mammotion/devices/rtk_cloud.py +113 -0
  31. pymammotion/mammotion/devices/rtk_device.py +50 -0
  32. pymammotion/mammotion/devices/rtk_manager.py +122 -0
  33. pymammotion/mqtt/__init__.py +2 -1
  34. pymammotion/mqtt/aliyun_mqtt.py +232 -0
  35. pymammotion/mqtt/mammotion_mqtt.py +132 -194
  36. pymammotion/mqtt/mqtt_models.py +66 -0
  37. pymammotion/proto/__init__.py +1 -1
  38. pymammotion/proto/mctrl_nav.proto +1 -1
  39. pymammotion/proto/mctrl_nav_pb2.py +1 -1
  40. pymammotion/proto/mctrl_nav_pb2.pyi +4 -4
  41. pymammotion/proto/mctrl_sys.proto +1 -1
  42. pymammotion/utility/device_type.py +88 -3
  43. pymammotion/utility/mur_mur_hash.py +132 -87
  44. {pymammotion-0.5.33.dist-info → pymammotion-0.5.40.dist-info}/METADATA +25 -31
  45. {pymammotion-0.5.33.dist-info → pymammotion-0.5.40.dist-info}/RECORD +54 -40
  46. {pymammotion-0.5.33.dist-info → pymammotion-0.5.40.dist-info}/WHEEL +1 -1
  47. {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
- area_label: "AreaLabelName" = field(default_factory=AreaLabelName)
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
- if hash_data.type == PathType.AREA:
263
- return self.find_missing_frames(self.area.get(hash_data.hash))
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
- if hash_data.type == PathType.PATH:
269
- return self.find_missing_frames(self.path.get(hash_data.hash))
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 hash_data.type == PathType.LINE:
272
- return self.find_missing_frames(self.line.get(hash_data.hash))
309
+ if target_dict is None:
310
+ return None
273
311
 
274
- if hash_data.type == PathType.DUMP:
275
- return self.find_missing_frames(self.dump.get(hash_data.hash))
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
- if hash_data.type == PathType.SVG:
278
- return self.find_missing_frames(self.svg.get(hash_data.data_hash))
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}" if hash_data.area_label is None else hash_data.area_label.label
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
- if hash_data.type == PathType.OBSTACLE:
299
- return self._add_hash_data(self.obstacle, hash_data)
346
+ path_type_mapping = self._get_path_type_mapping()
347
+ target_dict = path_type_mapping.get(hash_data.type)
300
348
 
301
- if hash_data.type == PathType.PATH:
302
- return self._add_hash_data(self.path, hash_data)
349
+ if target_dict is not None:
350
+ return self._add_hash_data(target_dict, hash_data)
303
351
 
304
- if hash_data.type == PathType.LINE:
305
- return self._add_hash_data(self.line, hash_data)
352
+ return False
306
353
 
307
- if hash_data.type == PathType.DUMP:
308
- return self._add_hash_data(self.dump, hash_data)
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
- if hash_data.type == PathType.SVG:
311
- return self._add_hash_data(self.svg, hash_data)
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
- return False
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 | None = None
9
+ self.hash: int = 0
10
10
  self.action: int = 0
11
11
  self.current_frame: int = 0
12
- self.data_hash: int | None = None
12
+ self.data_hash: int = 0
13
13
  self.data_len: int = 0
14
- self.p_hash_a: int | None = None
15
- self.p_hash_b: int | None = None
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.enums import ConnectionPreference
13
- from pymammotion.data.model.hash_list import AreaHashNameList, NavGetCommData, NavGetHashListData, Plan, SvgMessage
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 StateManager:
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 status callback if it is set."""
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
- cutter_work_mode: AppSetCutterWorkMode = driver_msg[1]
267
- self._device.mower_state.cutter_mode = cutter_work_mode.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."""
@@ -0,0 +1,3 @@
1
+ from pymammotion.homeassistant.mower_api import HomeAssistantMowerApi
2
+
3
+ __all__ = ["HomeAssistantMowerApi"]