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.

Files changed (63) 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 +169 -21
  4. pymammotion/const.py +3 -0
  5. pymammotion/data/model/device.py +22 -9
  6. pymammotion/data/model/device_config.py +1 -1
  7. pymammotion/data/model/device_info.py +1 -0
  8. pymammotion/data/model/enums.py +5 -3
  9. pymammotion/data/model/events.py +14 -0
  10. pymammotion/data/model/generate_geojson.py +551 -0
  11. pymammotion/data/model/generate_route_information.py +2 -2
  12. pymammotion/data/model/hash_list.py +129 -33
  13. pymammotion/data/model/location.py +4 -4
  14. pymammotion/data/model/region_data.py +4 -4
  15. pymammotion/data/model/report_info.py +7 -0
  16. pymammotion/data/{state_manager.py → mower_state_manager.py} +75 -11
  17. pymammotion/data/mqtt/event.py +47 -22
  18. pymammotion/data/mqtt/mammotion_properties.py +257 -0
  19. pymammotion/data/mqtt/properties.py +32 -29
  20. pymammotion/data/mqtt/status.py +17 -16
  21. pymammotion/event/event.py +5 -2
  22. pymammotion/homeassistant/__init__.py +3 -0
  23. pymammotion/homeassistant/mower_api.py +484 -0
  24. pymammotion/homeassistant/rtk_api.py +54 -0
  25. pymammotion/http/http.py +394 -14
  26. pymammotion/http/model/http.py +82 -2
  27. pymammotion/http/model/response_factory.py +10 -4
  28. pymammotion/mammotion/commands/mammotion_command.py +6 -0
  29. pymammotion/mammotion/commands/messages/navigation.py +39 -6
  30. pymammotion/mammotion/devices/__init__.py +27 -3
  31. pymammotion/mammotion/devices/base.py +16 -138
  32. pymammotion/mammotion/devices/mammotion.py +369 -200
  33. pymammotion/mammotion/devices/mammotion_bluetooth.py +7 -5
  34. pymammotion/mammotion/devices/mammotion_cloud.py +42 -83
  35. pymammotion/mammotion/devices/mammotion_mower_ble.py +49 -0
  36. pymammotion/mammotion/devices/mammotion_mower_cloud.py +39 -0
  37. pymammotion/mammotion/devices/managers/managers.py +81 -0
  38. pymammotion/mammotion/devices/mower_device.py +124 -0
  39. pymammotion/mammotion/devices/mower_manager.py +107 -0
  40. pymammotion/mammotion/devices/rtk_ble.py +89 -0
  41. pymammotion/mammotion/devices/rtk_cloud.py +113 -0
  42. pymammotion/mammotion/devices/rtk_device.py +50 -0
  43. pymammotion/mammotion/devices/rtk_manager.py +122 -0
  44. pymammotion/mqtt/__init__.py +2 -1
  45. pymammotion/mqtt/aliyun_mqtt.py +232 -0
  46. pymammotion/mqtt/mammotion_mqtt.py +174 -192
  47. pymammotion/mqtt/mqtt_models.py +66 -0
  48. pymammotion/proto/__init__.py +3 -3
  49. pymammotion/proto/mctrl_nav.proto +1 -1
  50. pymammotion/proto/mctrl_nav_pb2.py +1 -1
  51. pymammotion/proto/mctrl_nav_pb2.pyi +4 -4
  52. pymammotion/proto/mctrl_sys.proto +1 -1
  53. pymammotion/proto/mctrl_sys_pb2.py +1 -1
  54. pymammotion/utility/constant/device_constant.py +1 -1
  55. pymammotion/utility/datatype_converter.py +13 -12
  56. pymammotion/utility/device_type.py +88 -3
  57. pymammotion/utility/map.py +238 -51
  58. pymammotion/utility/mur_mur_hash.py +132 -87
  59. {pymammotion-0.5.34.dist-info → pymammotion-0.5.53.dist-info}/METADATA +26 -31
  60. {pymammotion-0.5.34.dist-info → pymammotion-0.5.53.dist-info}/RECORD +67 -51
  61. {pymammotion-0.5.34.dist-info → pymammotion-0.5.53.dist-info}/WHEEL +1 -1
  62. pymammotion/http/_init_.py +0 -0
  63. {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
- area_label: "AreaLabelName" = field(default_factory=AreaLabelName)
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
- 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))
325
+ frame_list = self._get_frame_list_by_type_and_hash(hash_data)
326
+ return self.find_missing_frames(frame_list)
267
327
 
268
- if hash_data.type == PathType.PATH:
269
- return self.find_missing_frames(self.path.get(hash_data.hash))
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 hash_data.type == PathType.LINE:
272
- return self.find_missing_frames(self.line.get(hash_data.hash))
333
+ if target_dict is None:
334
+ return None
273
335
 
274
- if hash_data.type == PathType.DUMP:
275
- return self.find_missing_frames(self.dump.get(hash_data.hash))
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
- if hash_data.type == PathType.SVG:
278
- return self.find_missing_frames(self.svg.get(hash_data.data_hash))
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}" if hash_data.area_label is None else hash_data.area_label.label
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
- if hash_data.type == PathType.OBSTACLE:
299
- return self._add_hash_data(self.obstacle, hash_data)
370
+ path_type_mapping = self._get_path_type_mapping()
371
+ target_dict = path_type_mapping.get(hash_data.type)
300
372
 
301
- if hash_data.type == PathType.PATH:
302
- return self._add_hash_data(self.path, hash_data)
373
+ if target_dict is not None:
374
+ return self._add_hash_data(target_dict, hash_data)
303
375
 
304
- if hash_data.type == PathType.LINE:
305
- return self._add_hash_data(self.line, hash_data)
376
+ return False
306
377
 
307
- if hash_data.type == PathType.DUMP:
308
- return self._add_hash_data(self.dump, hash_data)
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
- if hash_data.type == PathType.SVG:
311
- return self._add_hash_data(self.svg, hash_data)
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
- return False
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 Point(DataClassORJSONMixin):
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(Point):
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: Point = field(default_factory=Point)
32
- RTK: Point = field(default_factory=Point)
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 | 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
@@ -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.enums import ConnectionPreference
13
- from pymammotion.data.model.hash_list import AreaHashNameList, NavGetCommData, NavGetHashListData, Plan, SvgMessage
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 StateManager:
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 status callback if it is set."""
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
- self._device.work = CurrentTaskSettings.from_dict(
215
- work_settings.to_dict(casing=betterproto2.Casing.SNAKE)
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
- cutter_work_mode: AppSetCutterWorkMode = driver_msg[1]
267
- self._device.mower_state.cutter_mode = cutter_work_mode.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
@@ -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
- groupIdList: list[str]
77
- groupId: str
78
- categoryKey: Literal["LawnMower", "Tracker"]
79
- batchId: str
80
- gmtCreate: int
81
- productKey: str
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
- deviceName: str
84
- iotId: str
85
- checkLevel: int
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
- tenantId: str
87
+ tenant_id: Annotated[str, Alias("tenantId")]
88
88
  name: str
89
- thingType: Literal["DEVICE"]
89
+ thing_type: Annotated[Literal["DEVICE"], Alias("thingType")]
90
90
  time: int
91
- tenantInstanceId: str
91
+ tenant_instance_id: Annotated[str, Alias("tenantInstanceId")]
92
92
  value: Any
93
93
 
94
+ # Optional fields
94
95
  identifier: str | None = None
95
- checkFailedData: dict | None = None
96
- _tenantId: str | None = None
97
- generateTime: int | None = None
98
- JMSXDeliveryCount: int | None = None
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
- requestId: str | None = None
101
- _categoryKey: str | None = None
102
- deviceType: str | None = None
103
- _traceId: str | None = None
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