pymammotion 0.5.21__py3-none-any.whl → 0.5.45__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 (59) hide show
  1. pymammotion/__init__.py +3 -3
  2. pymammotion/aliyun/client.py +5 -2
  3. pymammotion/aliyun/cloud_gateway.py +137 -20
  4. pymammotion/aliyun/model/dev_by_account_response.py +169 -21
  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/device_info.py +4 -0
  9. pymammotion/data/model/enums.py +5 -3
  10. pymammotion/data/model/generate_route_information.py +2 -2
  11. pymammotion/data/model/hash_list.py +113 -33
  12. pymammotion/data/model/mowing_modes.py +8 -0
  13. pymammotion/data/model/region_data.py +4 -4
  14. pymammotion/data/{state_manager.py → mower_state_manager.py} +50 -13
  15. pymammotion/data/mqtt/event.py +47 -22
  16. pymammotion/data/mqtt/mammotion_properties.py +257 -0
  17. pymammotion/data/mqtt/properties.py +32 -29
  18. pymammotion/data/mqtt/status.py +17 -16
  19. pymammotion/homeassistant/__init__.py +3 -0
  20. pymammotion/homeassistant/mower_api.py +446 -0
  21. pymammotion/homeassistant/rtk_api.py +54 -0
  22. pymammotion/http/http.py +433 -18
  23. pymammotion/http/model/http.py +82 -2
  24. pymammotion/http/model/response_factory.py +10 -4
  25. pymammotion/mammotion/commands/mammotion_command.py +20 -0
  26. pymammotion/mammotion/commands/messages/driver.py +25 -0
  27. pymammotion/mammotion/commands/messages/navigation.py +10 -6
  28. pymammotion/mammotion/commands/messages/system.py +0 -14
  29. pymammotion/mammotion/devices/__init__.py +27 -3
  30. pymammotion/mammotion/devices/base.py +22 -146
  31. pymammotion/mammotion/devices/mammotion.py +364 -205
  32. pymammotion/mammotion/devices/mammotion_bluetooth.py +11 -8
  33. pymammotion/mammotion/devices/mammotion_cloud.py +49 -85
  34. pymammotion/mammotion/devices/mammotion_mower_ble.py +49 -0
  35. pymammotion/mammotion/devices/mammotion_mower_cloud.py +39 -0
  36. pymammotion/mammotion/devices/managers/managers.py +81 -0
  37. pymammotion/mammotion/devices/mower_device.py +121 -0
  38. pymammotion/mammotion/devices/mower_manager.py +107 -0
  39. pymammotion/mammotion/devices/rtk_ble.py +89 -0
  40. pymammotion/mammotion/devices/rtk_cloud.py +113 -0
  41. pymammotion/mammotion/devices/rtk_device.py +50 -0
  42. pymammotion/mammotion/devices/rtk_manager.py +122 -0
  43. pymammotion/mqtt/__init__.py +2 -1
  44. pymammotion/mqtt/aliyun_mqtt.py +232 -0
  45. pymammotion/mqtt/mammotion_mqtt.py +174 -192
  46. pymammotion/mqtt/mqtt_models.py +66 -0
  47. pymammotion/proto/__init__.py +2 -2
  48. pymammotion/proto/mctrl_nav.proto +2 -2
  49. pymammotion/proto/mctrl_nav_pb2.py +1 -1
  50. pymammotion/proto/mctrl_nav_pb2.pyi +4 -4
  51. pymammotion/proto/mctrl_sys.proto +1 -1
  52. pymammotion/utility/datatype_converter.py +13 -12
  53. pymammotion/utility/device_type.py +88 -3
  54. pymammotion/utility/mur_mur_hash.py +132 -87
  55. {pymammotion-0.5.21.dist-info → pymammotion-0.5.45.dist-info}/METADATA +25 -30
  56. {pymammotion-0.5.21.dist-info → pymammotion-0.5.45.dist-info}/RECORD +64 -50
  57. {pymammotion-0.5.21.dist-info → pymammotion-0.5.45.dist-info}/WHEEL +1 -1
  58. pymammotion/http/_init_.py +0 -0
  59. {pymammotion-0.5.21.dist-info → pymammotion-0.5.45.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 = []
@@ -10,6 +10,14 @@ class CuttingMode(IntEnum):
10
10
  no_grid = 3
11
11
 
12
12
 
13
+ class CuttingSpeedMode(IntEnum):
14
+ """speed"""
15
+
16
+ normal = 0
17
+ slow = 1
18
+ fast = 2
19
+
20
+
13
21
  class BorderPatrolMode(IntEnum):
14
22
  """"""
15
23
 
@@ -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
@@ -1,15 +1,22 @@
1
1
  """Manage state from notifications into MowingDevice."""
2
2
 
3
+ from collections.abc import Awaitable, Callable
3
4
  from datetime import UTC, datetime
4
5
  import logging
5
- from typing import Any, Awaitable, Callable
6
+ from typing import Any
6
7
 
7
8
  import betterproto2
8
9
 
9
10
  from pymammotion.data.model.device import MowingDevice
10
11
  from pymammotion.data.model.device_info import SideLight
11
- from pymammotion.data.model.enums import ConnectionPreference
12
- 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
+ )
13
20
  from pymammotion.data.model.work import CurrentTaskSettings
14
21
  from pymammotion.data.mqtt.event import ThingEventMessage
15
22
  from pymammotion.data.mqtt.properties import ThingPropertiesMessage
@@ -17,6 +24,9 @@ from pymammotion.data.mqtt.status import ThingStatusMessage
17
24
  from pymammotion.event.event import DataEvent
18
25
  from pymammotion.proto import (
19
26
  AppGetAllAreaHashName,
27
+ AppGetCutterWorkMode,
28
+ AppSetCutterWorkMode,
29
+ CoverPathUploadT,
20
30
  DeviceFwInfo,
21
31
  DeviceProductTypeInfoT,
22
32
  DrvDevInfoResp,
@@ -38,13 +48,13 @@ from pymammotion.proto import (
38
48
  logger = logging.getLogger(__name__)
39
49
 
40
50
 
41
- class StateManager:
51
+ class MowerStateManager:
42
52
  """Manage state."""
43
53
 
44
54
  def __init__(self, device: MowingDevice) -> None:
55
+ """Initialize state manager with a device."""
45
56
  self._device: MowingDevice = device
46
57
  self.last_updated_at = datetime.now(UTC)
47
- self.preference = ConnectionPreference.WIFI
48
58
  self.cloud_gethash_ack_callback: Callable[[NavGetHashListAck], Awaitable[None]] | None = None
49
59
  self.cloud_get_commondata_ack_callback: (
50
60
  Callable[[NavGetCommDataAck | SvgMessageAckT], Awaitable[None]] | None
@@ -74,11 +84,13 @@ class StateManager:
74
84
  self._device = device
75
85
 
76
86
  async def properties(self, thing_properties: ThingPropertiesMessage) -> None:
87
+ """Update device properties and invoke callback."""
77
88
  # TODO update device based off thing properties
78
89
  self._device.mqtt_properties = thing_properties
79
90
  await self.on_properties_callback(thing_properties)
80
91
 
81
92
  async def status(self, thing_status: ThingStatusMessage) -> None:
93
+ """Update device status and invoke callback."""
82
94
  if not self._device.online:
83
95
  self._device.online = True
84
96
  self._device.status_properties = thing_status
@@ -93,19 +105,23 @@ class StateManager:
93
105
 
94
106
  @property
95
107
  def online(self) -> bool:
108
+ """Return online status."""
96
109
  return self._device.online
97
110
 
98
111
  @online.setter
99
112
  def online(self, value: bool) -> None:
113
+ """Set online status."""
100
114
  self._device.online = value
101
115
 
102
116
  async def gethash_ack_callback(self, msg: NavGetHashListAck) -> None:
117
+ """Dispatch hash list acknowledgment to available callback."""
103
118
  if self.cloud_gethash_ack_callback:
104
119
  await self.cloud_gethash_ack_callback(msg)
105
120
  elif self.ble_gethash_ack_callback:
106
121
  await self.ble_gethash_ack_callback(msg)
107
122
 
108
123
  async def on_notification_callback(self, res: tuple[str, Any | None]) -> None:
124
+ """Dispatch notification to available callback."""
109
125
  if self.cloud_on_notification_callback:
110
126
  await self.cloud_on_notification_callback.data_event(res)
111
127
  elif self.ble_on_notification_callback:
@@ -122,7 +138,7 @@ class StateManager:
122
138
  await self.status_callback.data_event(thing_status)
123
139
 
124
140
  async def on_device_event_callback(self, device_event: ThingEventMessage) -> None:
125
- """Executes the status callback if it is set."""
141
+ """Executes the event callback if it is set."""
126
142
  if self.device_event_callback:
127
143
  await self.device_event_callback.data_event(device_event)
128
144
 
@@ -134,6 +150,7 @@ class StateManager:
134
150
  await self.ble_get_commondata_ack_callback(comm_data)
135
151
 
136
152
  async def get_plan_callback(self, planjob: NavPlanJobSet) -> None:
153
+ """Dispatch plan job to available callback."""
137
154
  if self.cloud_get_plan_callback:
138
155
  await self.cloud_get_plan_callback(planjob)
139
156
  elif self.ble_get_plan_callback:
@@ -180,6 +197,10 @@ class StateManager:
180
197
  )
181
198
  if updated:
182
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
+
183
204
  case "todev_planjob_set":
184
205
  planjob: NavPlanJobSet = nav_msg[1]
185
206
  self._device.map.update_plan(Plan.from_dict(planjob.to_dict(casing=betterproto2.Casing.SNAKE)))
@@ -244,9 +265,19 @@ class StateManager:
244
265
  self._device.mower_state.swversion = device_fw_info.version
245
266
 
246
267
  def _update_driver_data(self, message) -> None:
247
- pass
268
+ """Update driver data."""
269
+ driver_msg = betterproto2.which_one_of(message.driver, "SubDrvMsg")
270
+ match driver_msg[0]:
271
+ case "current_cutter_mode":
272
+ cutter_work_mode: AppGetCutterWorkMode = driver_msg[1]
273
+ self._device.mower_state.cutter_mode = cutter_work_mode.current_cutter_mode
274
+ self._device.mower_state.cutter_rpm = cutter_work_mode.current_cutter_rpm
275
+ case "cutter_mode_ctrl_by_hand":
276
+ cutter_work_mode_set: AppSetCutterWorkMode = driver_msg[1]
277
+ self._device.mower_state.cutter_mode = cutter_work_mode_set.cutter_mode
248
278
 
249
279
  def _update_net_data(self, message) -> None:
280
+ """Update network data."""
250
281
  net_msg = betterproto2.which_one_of(message.net, "NetSubType")
251
282
  match net_msg[0]:
252
283
  case "toapp_wifi_iot_status":
@@ -264,15 +295,21 @@ class StateManager:
264
295
 
265
296
  def _update_mul_data(self, message) -> None:
266
297
  """Media and video states."""
267
- mul_msg = betterproto2.which_one_of(message.net, "SubMul")
298
+ mul_msg = betterproto2.which_one_of(message.mul, "SubMul")
268
299
  match mul_msg[0]:
269
- case "Getlamprsp":
300
+ case "get_lamp_rsp":
270
301
  lamp_resp: Getlamprsp = mul_msg[1]
271
302
  self._device.mower_state.lamp_info.lamp_bright = lamp_resp.lamp_bright
272
- if lamp_resp.get_ids == 1126:
273
- self._device.mower_state.lamp_info.manual_light = bool(lamp_resp.lamp_manual_ctrl.value)
303
+ if lamp_resp.get_ids in (1126, 1127):
304
+ self._device.mower_state.lamp_info.lamp_bright = lamp_resp.lamp_bright
305
+ self._device.mower_state.lamp_info.manual_light = bool(lamp_resp.lamp_manual_ctrl.value) or bool(
306
+ lamp_resp.lamp_bright
307
+ )
274
308
  if lamp_resp.get_ids == 1123:
275
- self._device.mower_state.lamp_info.night_light = bool(lamp_resp.lamp_ctrl.value)
309
+ self._device.mower_state.lamp_info.lamp_bright = lamp_resp.lamp_bright
310
+ self._device.mower_state.lamp_info.night_light = bool(lamp_resp.lamp_ctrl.value) or bool(
311
+ lamp_resp.lamp_bright
312
+ )
276
313
 
277
314
  def _update_ota_data(self, message) -> None:
278
- pass
315
+ """Update OTA 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