pymammotion 0.5.27__py3-none-any.whl → 0.5.44__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 (56) hide show
  1. pymammotion/__init__.py +3 -3
  2. pymammotion/aliyun/client.py +3 -0
  3. pymammotion/aliyun/cloud_gateway.py +117 -19
  4. pymammotion/aliyun/model/dev_by_account_response.py +198 -20
  5. pymammotion/const.py +3 -0
  6. pymammotion/data/model/device.py +1 -0
  7. pymammotion/data/model/device_config.py +1 -1
  8. pymammotion/data/model/enums.py +5 -3
  9. pymammotion/data/model/generate_route_information.py +2 -2
  10. pymammotion/data/model/hash_list.py +113 -33
  11. pymammotion/data/model/region_data.py +4 -4
  12. pymammotion/data/{state_manager.py → mower_state_manager.py} +17 -7
  13. pymammotion/data/mqtt/event.py +47 -22
  14. pymammotion/data/mqtt/mammotion_properties.py +257 -0
  15. pymammotion/data/mqtt/properties.py +32 -29
  16. pymammotion/data/mqtt/status.py +17 -16
  17. pymammotion/homeassistant/__init__.py +3 -0
  18. pymammotion/homeassistant/mower_api.py +446 -0
  19. pymammotion/homeassistant/rtk_api.py +54 -0
  20. pymammotion/http/http.py +431 -18
  21. pymammotion/http/model/http.py +82 -2
  22. pymammotion/http/model/response_factory.py +10 -4
  23. pymammotion/mammotion/commands/mammotion_command.py +20 -0
  24. pymammotion/mammotion/commands/messages/navigation.py +10 -6
  25. pymammotion/mammotion/commands/messages/system.py +0 -14
  26. pymammotion/mammotion/devices/__init__.py +27 -3
  27. pymammotion/mammotion/devices/base.py +22 -146
  28. pymammotion/mammotion/devices/mammotion.py +367 -206
  29. pymammotion/mammotion/devices/mammotion_bluetooth.py +8 -5
  30. pymammotion/mammotion/devices/mammotion_cloud.py +47 -83
  31. pymammotion/mammotion/devices/mammotion_mower_ble.py +49 -0
  32. pymammotion/mammotion/devices/mammotion_mower_cloud.py +39 -0
  33. pymammotion/mammotion/devices/managers/managers.py +81 -0
  34. pymammotion/mammotion/devices/mower_device.py +121 -0
  35. pymammotion/mammotion/devices/mower_manager.py +107 -0
  36. pymammotion/mammotion/devices/rtk_ble.py +89 -0
  37. pymammotion/mammotion/devices/rtk_cloud.py +113 -0
  38. pymammotion/mammotion/devices/rtk_device.py +50 -0
  39. pymammotion/mammotion/devices/rtk_manager.py +122 -0
  40. pymammotion/mqtt/__init__.py +2 -1
  41. pymammotion/mqtt/aliyun_mqtt.py +232 -0
  42. pymammotion/mqtt/mammotion_mqtt.py +174 -192
  43. pymammotion/mqtt/mqtt_models.py +66 -0
  44. pymammotion/proto/__init__.py +2 -2
  45. pymammotion/proto/mctrl_nav.proto +2 -2
  46. pymammotion/proto/mctrl_nav_pb2.py +1 -1
  47. pymammotion/proto/mctrl_nav_pb2.pyi +4 -4
  48. pymammotion/proto/mctrl_sys.proto +1 -1
  49. pymammotion/utility/datatype_converter.py +13 -12
  50. pymammotion/utility/device_type.py +88 -3
  51. pymammotion/utility/mur_mur_hash.py +132 -87
  52. {pymammotion-0.5.27.dist-info → pymammotion-0.5.44.dist-info}/METADATA +25 -30
  53. {pymammotion-0.5.27.dist-info → pymammotion-0.5.44.dist-info}/RECORD +61 -47
  54. {pymammotion-0.5.27.dist-info → pymammotion-0.5.44.dist-info}/WHEEL +1 -1
  55. pymammotion/http/_init_.py +0 -0
  56. {pymammotion-0.5.27.dist-info → pymammotion-0.5.44.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.copy().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."""
@@ -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
@@ -0,0 +1,257 @@
1
+ from dataclasses import dataclass
2
+ from typing import Annotated
3
+
4
+ from mashumaro.config import BaseConfig
5
+ from mashumaro.mixins.orjson import DataClassORJSONMixin
6
+ from mashumaro.types import Alias
7
+
8
+
9
+ @dataclass
10
+ class FirmwareInfo(DataClassORJSONMixin):
11
+ t: str
12
+ c: str
13
+ v: str
14
+
15
+
16
+ @dataclass
17
+ class DeviceVersionInfo(DataClassORJSONMixin):
18
+ dev_ver: Annotated[str, Alias("devVer")]
19
+ whole: int
20
+ fw_info: Annotated[list[FirmwareInfo], Alias("fwInfo")]
21
+
22
+
23
+ @dataclass
24
+ class Coordinate(DataClassORJSONMixin):
25
+ lon: float
26
+ lat: float
27
+
28
+
29
+ @dataclass
30
+ class InternalNavigation(DataClassORJSONMixin):
31
+ nav: Annotated[str, Alias("NAV")]
32
+ pau: Annotated[str, Alias("Pau")]
33
+ r_pau: Annotated[str, Alias("rPau")]
34
+ mcu: Annotated[str, Alias("MCU")]
35
+ app: Annotated[str, Alias("APP")]
36
+ w_slp: Annotated[str, Alias("wSlp")]
37
+ i_slp: Annotated[str, Alias("iSlp")]
38
+
39
+
40
+ @dataclass
41
+ class BandwidthTraffic(DataClassORJSONMixin):
42
+ iot: Annotated[str, Alias("IoT")]
43
+ roi: Annotated[str, Alias("RoI")]
44
+ fpv: Annotated[str, Alias("FPV")]
45
+ inav: InternalNavigation
46
+
47
+
48
+ @dataclass
49
+ class TrafficPeriod(DataClassORJSONMixin):
50
+ r: str
51
+ t: str
52
+ s: str
53
+
54
+
55
+ @dataclass
56
+ class TrafficData(DataClassORJSONMixin):
57
+ upt: str
58
+ hour: Annotated[dict[str, TrafficPeriod], Alias("Hour")]
59
+ day: Annotated[dict[str, TrafficPeriod], Alias("Day")]
60
+ mon: Annotated[dict[str, TrafficPeriod], Alias("Mon")]
61
+
62
+
63
+ @dataclass
64
+ class NetworkInfo(DataClassORJSONMixin):
65
+ ssid: str
66
+ ip: str
67
+ wifi_sta_mac: str
68
+ wifi_rssi: int
69
+ wifi_available: int
70
+ bt_mac: str
71
+ mnet_model: str
72
+ imei: str
73
+ fw_ver: str
74
+ sim: str
75
+ imsi: str
76
+ iccid: str
77
+ sim_source: str
78
+ mnet_rssi: int
79
+ signal: int
80
+ mnet_link: int
81
+ mnet_option: str
82
+ mnet_ip: str
83
+ mnet_reg: str
84
+ mnet_rsrp: str
85
+ mnet_snr: str
86
+ mnet_enable: int
87
+ apn_num: int
88
+ apn_info: str
89
+ apn_cid: int
90
+ used_net: int
91
+ hub_reset: int
92
+ mnet_dis: int
93
+ airplane_times: int
94
+ lsusb_num: int
95
+ b_tra: Annotated[BandwidthTraffic, Alias("bTra")]
96
+ bw_tra: Annotated[BandwidthTraffic, Alias("bwTra")]
97
+ mnet_rx: str
98
+ mnet_tx: str
99
+ m_tra: Annotated[TrafficData, Alias("mTra")]
100
+ mnet_uniot: int
101
+ mnet_un_getiot: int
102
+ ssh_flag: str
103
+ mileage: str
104
+ work_time: str
105
+ wt_sec: int
106
+ bat_cycles: str
107
+
108
+
109
+ @dataclass
110
+ class DeviceOtherInfo(DataClassORJSONMixin):
111
+ soc_up_time: Annotated[int, Alias("socUpTime")]
112
+ mcu_up_time: Annotated[int, Alias("mcuUpTime")]
113
+ soc_loads: Annotated[str, Alias("socLoads")]
114
+ soc_mem_free: Annotated[int, Alias("socMemFree")]
115
+ soc_mem_total: Annotated[int, Alias("socMemTotal")]
116
+ soc_mmc_life_time: Annotated[int, Alias("socMmcLifeTime")]
117
+ usb_dis_cnt: Annotated[int, Alias("usbDisCnt")]
118
+ soc_pstore: Annotated[int, Alias("socPstore")]
119
+ soc_coredump: Annotated[int, Alias("socCoredump")]
120
+ soc_tmp: Annotated[int, Alias("socTmp")]
121
+ mc_mcu: Annotated[str, Alias("mcMcu")]
122
+ tilt_degree: str
123
+ i_msg_free: Annotated[int, Alias("iMsgFree")]
124
+ i_msg_limit: Annotated[int, Alias("iMsgLimit")]
125
+ i_msg_raw: Annotated[int, Alias("iMsgRaw")]
126
+ i_msg_prop: Annotated[int, Alias("iMsgprop")]
127
+ i_msg_serv: Annotated[int, Alias("iMsgServ")]
128
+ i_msg_info: Annotated[int, Alias("iMsgInfo")]
129
+ i_msg_warn: Annotated[int, Alias("iMsgWarn")]
130
+ i_msg_fault: Annotated[int, Alias("iMsgFault")]
131
+ i_msg_ota_stage: Annotated[int, Alias("iMsgOtaStage")]
132
+ i_msg_protobuf: Annotated[int, Alias("iMsgProtobuf")]
133
+ i_msg_notify: Annotated[int, Alias("iMsgNotify")]
134
+ i_msg_log_prog: Annotated[int, Alias("iMsgLogProg")]
135
+ i_msg_biz_req: Annotated[int, Alias("iMsgBizReq")]
136
+ i_msg_cfg_req: Annotated[int, Alias("iMsgCfgReq")]
137
+ i_msg_voice: Annotated[int, Alias("iMsgVoice")]
138
+ i_msg_warn_code: Annotated[int, Alias("iMsgWarnCode")]
139
+ pb_net: Annotated[int, Alias("pbNet")]
140
+ pb_sys: Annotated[int, Alias("pbSys")]
141
+ pb_nav: Annotated[int, Alias("pbNav")]
142
+ pb_local: Annotated[int, Alias("pbLocal")]
143
+ pb_plan: Annotated[int, Alias("pbPlan")]
144
+ pb_e_drv: Annotated[int, Alias("pbEDrv")]
145
+ pb_e_sys: Annotated[int, Alias("pbESys")]
146
+ pb_midware: Annotated[int, Alias("pbMidware")]
147
+ pb_ota: Annotated[int, Alias("pbOta")]
148
+ pb_appl: Annotated[int, Alias("pbAppl")]
149
+ pb_mul: Annotated[int, Alias("pbMul")]
150
+ pb_other: Annotated[int, Alias("pbOther")]
151
+ lora_connect: Annotated[int, Alias("loraConnect")]
152
+ base_status: Annotated[int, Alias("Basestatus")]
153
+ mqtt_rtk_switch: int
154
+ mqtt_rtk_channel: int
155
+ mqtt_rtk_status: int
156
+ mqtt_rtcm_cnt: int
157
+ mqtt_conn_cnt: int
158
+ mqtt_disconn_cnt: int
159
+ mqtt_rtk_hb_flag: int
160
+ mqtt_rtk_hb_count: int
161
+ mqtt_start_cnt: int
162
+ mqtt_close_cnt: int
163
+ mqtt_rtk_ssl_fail: int
164
+ mqtt_rtk_wifi_config: int
165
+ nrtk_svc_prov: int
166
+ nrtk_svc_err: int
167
+ base_stn_id: int
168
+ rtk_status: int
169
+ charge_status: int
170
+ chassis_state: int
171
+ nav: str
172
+ ins_fusion: str
173
+ perception: str
174
+ vision_proxy: str
175
+ vslam_vio: str
176
+ iot_con_timeout: int
177
+ iot_con: int
178
+ iot_con_fail_max: str
179
+ iot_con_fail_min: Annotated[str, Alias("iot_con__fail_min")]
180
+ iot_url_count: int
181
+ iot_url_max: str
182
+ iot_url_min: str
183
+ iot_cn: int
184
+ iot_ap: int
185
+ iot_us: int
186
+ iot_eu: int
187
+ task_area: float
188
+ task_count: int
189
+ task_hash: str
190
+ systemio_boot_time: Annotated[str, Alias("systemioBootTime")]
191
+ dds_no_gdc: int
192
+
193
+
194
+ @dataclass
195
+ class CheckData(DataClassORJSONMixin):
196
+ result: str
197
+ error: Annotated[list[int], Alias("Error")]
198
+ warn: Annotated[list[int], Alias("Warn")]
199
+ ok: Annotated[list[int], Alias("OK")]
200
+
201
+
202
+ @dataclass
203
+ class DeviceProperties(DataClassORJSONMixin):
204
+ device_state: Annotated[int, Alias("deviceState")]
205
+ battery_percentage: Annotated[int, Alias("batteryPercentage")]
206
+ device_version: Annotated[str, Alias("deviceVersion")]
207
+ knife_height: Annotated[int, Alias("knifeHeight")]
208
+ lora_general_config: Annotated[str, Alias("loraGeneralConfig")]
209
+ ext_mod: Annotated[str, Alias("extMod")]
210
+ int_mod: Annotated[str, Alias("intMod")]
211
+ iot_state: Annotated[int, Alias("iotState")]
212
+ iot_msg_total: Annotated[int, Alias("iotMsgTotal")]
213
+ iot_msg_hz: Annotated[int, Alias("iotMsgHz")]
214
+ lt_mr_mod: Annotated[str, Alias("ltMrMod")]
215
+ rt_mr_mod: Annotated[str, Alias("rtMrMod")]
216
+ bms_hardware_version: Annotated[str, Alias("bmsHardwareVersion")]
217
+ stm32_h7_version: Annotated[str, Alias("stm32H7Version")]
218
+ left_motor_version: Annotated[str, Alias("leftMotorVersion")]
219
+ right_motor_version: Annotated[str, Alias("rightMotorVersion")]
220
+ rtk_version: Annotated[str, Alias("rtkVersion")]
221
+ bms_version: Annotated[str, Alias("bmsVersion")]
222
+ mc_boot_version: Annotated[str, Alias("mcBootVersion")]
223
+ left_motor_boot_version: Annotated[str, Alias("leftMotorBootVersion")]
224
+ right_motor_boot_version: Annotated[str, Alias("rightMotorBootVersion")]
225
+
226
+ # Nested JSON objects
227
+ device_version_info: Annotated[DeviceVersionInfo, Alias("deviceVersionInfo")]
228
+ coordinate: Coordinate
229
+ device_other_info: Annotated[DeviceOtherInfo, Alias("deviceOtherInfo")]
230
+ network_info: Annotated[NetworkInfo, Alias("networkInfo")]
231
+ check_data: Annotated[CheckData, Alias("checkData")]
232
+ iot_id: str = ""
233
+
234
+ class Config(BaseConfig):
235
+ # Custom deserializer for nested JSON strings
236
+ serialization_strategy = {
237
+ DeviceVersionInfo: {
238
+ "deserialize": lambda x: DeviceVersionInfo.from_json(x) if isinstance(x, str) else x,
239
+ "serialize": lambda x: x.to_json() if hasattr(x, "to_json") else x,
240
+ },
241
+ Coordinate: {
242
+ "deserialize": lambda x: Coordinate.from_json(x) if isinstance(x, str) else x,
243
+ "serialize": lambda x: x.to_json() if hasattr(x, "to_json") else x,
244
+ },
245
+ DeviceOtherInfo: {
246
+ "deserialize": lambda x: DeviceOtherInfo.from_json(x) if isinstance(x, str) else x,
247
+ "serialize": lambda x: x.to_json() if hasattr(x, "to_json") else x,
248
+ },
249
+ NetworkInfo: {
250
+ "deserialize": lambda x: NetworkInfo.from_json(x) if isinstance(x, str) else x,
251
+ "serialize": lambda x: x.to_json() if hasattr(x, "to_json") else x,
252
+ },
253
+ CheckData: {
254
+ "deserialize": lambda x: CheckData.from_json(x) if isinstance(x, str) else x,
255
+ "serialize": lambda x: x.to_json() if hasattr(x, "to_json") else x,
256
+ },
257
+ }