pymammotion 0.5.34__py3-none-any.whl → 0.5.41__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 +113 -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 +115 -4
  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.34.dist-info → pymammotion-0.5.41.dist-info}/METADATA +25 -31
  45. {pymammotion-0.5.34.dist-info → pymammotion-0.5.41.dist-info}/RECORD +54 -40
  46. {pymammotion-0.5.34.dist-info → pymammotion-0.5.41.dist-info}/WHEEL +1 -1
  47. {pymammotion-0.5.34.dist-info → pymammotion-0.5.41.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,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.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
- 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))
309
+ frame_list = self._get_frame_list_by_type_and_hash(hash_data)
310
+ return self.find_missing_frames(frame_list)
267
311
 
268
- if hash_data.type == PathType.PATH:
269
- return self.find_missing_frames(self.path.get(hash_data.hash))
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 hash_data.type == PathType.LINE:
272
- return self.find_missing_frames(self.line.get(hash_data.hash))
317
+ if target_dict is None:
318
+ return None
273
319
 
274
- if hash_data.type == PathType.DUMP:
275
- return self.find_missing_frames(self.dump.get(hash_data.hash))
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
- if hash_data.type == PathType.SVG:
278
- return self.find_missing_frames(self.svg.get(hash_data.data_hash))
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}" if hash_data.area_label is None else hash_data.area_label.label
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
- if hash_data.type == PathType.OBSTACLE:
299
- return self._add_hash_data(self.obstacle, hash_data)
354
+ path_type_mapping = self._get_path_type_mapping()
355
+ target_dict = path_type_mapping.get(hash_data.type)
300
356
 
301
- if hash_data.type == PathType.PATH:
302
- return self._add_hash_data(self.path, hash_data)
357
+ if target_dict is not None:
358
+ return self._add_hash_data(target_dict, hash_data)
303
359
 
304
- if hash_data.type == PathType.LINE:
305
- return self._add_hash_data(self.line, hash_data)
360
+ return False
306
361
 
307
- if hash_data.type == PathType.DUMP:
308
- return self._add_hash_data(self.dump, hash_data)
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
- if hash_data.type == PathType.SVG:
311
- return self._add_hash_data(self.svg, hash_data)
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
- return False
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 | 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"]