pymammotion 0.3.8__py3-none-any.whl → 0.4.0__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.
Files changed (69) hide show
  1. pymammotion/__init__.py +2 -2
  2. pymammotion/aliyun/cloud_gateway.py +12 -9
  3. pymammotion/aliyun/model/aep_response.py +1 -2
  4. pymammotion/aliyun/model/dev_by_account_response.py +7 -8
  5. pymammotion/aliyun/model/login_by_oauth_response.py +2 -3
  6. pymammotion/aliyun/model/regions_response.py +3 -3
  7. pymammotion/aliyun/model/session_by_authcode_response.py +1 -2
  8. pymammotion/aliyun/model/stream_subscription_response.py +1 -2
  9. pymammotion/bluetooth/ble.py +5 -5
  10. pymammotion/bluetooth/ble_message.py +9 -13
  11. pymammotion/data/model/device.py +31 -228
  12. pymammotion/data/model/device_config.py +0 -10
  13. pymammotion/data/model/device_info.py +13 -0
  14. pymammotion/data/model/device_limits.py +49 -0
  15. pymammotion/data/model/generate_route_information.py +1 -1
  16. pymammotion/data/model/hash_list.py +6 -2
  17. pymammotion/data/model/plan.py +0 -3
  18. pymammotion/data/model/raw_data.py +215 -0
  19. pymammotion/data/model/region_data.py +10 -11
  20. pymammotion/data/model/report_info.py +1 -1
  21. pymammotion/data/mqtt/event.py +18 -14
  22. pymammotion/data/mqtt/properties.py +1 -1
  23. pymammotion/data/mqtt/status.py +1 -1
  24. pymammotion/data/state_manager.py +83 -23
  25. pymammotion/http/encryption.py +220 -0
  26. pymammotion/http/http.py +92 -39
  27. pymammotion/http/model/http.py +2 -2
  28. pymammotion/mammotion/commands/abstract_message.py +2 -2
  29. pymammotion/mammotion/commands/messages/driver.py +28 -21
  30. pymammotion/mammotion/commands/messages/media.py +10 -14
  31. pymammotion/mammotion/commands/messages/navigation.py +14 -11
  32. pymammotion/mammotion/commands/messages/network.py +15 -12
  33. pymammotion/mammotion/commands/messages/ota.py +9 -14
  34. pymammotion/mammotion/commands/messages/system.py +27 -24
  35. pymammotion/mammotion/commands/messages/video.py +9 -14
  36. pymammotion/mammotion/devices/base.py +7 -14
  37. pymammotion/mammotion/devices/mammotion.py +22 -13
  38. pymammotion/mammotion/devices/mammotion_bluetooth.py +15 -4
  39. pymammotion/mammotion/devices/mammotion_cloud.py +30 -12
  40. pymammotion/mqtt/linkkit/__init__.py +5 -0
  41. pymammotion/mqtt/linkkit/h2client.py +585 -0
  42. pymammotion/mqtt/linkkit/linkkit.py +3020 -0
  43. pymammotion/mqtt/mammotion_mqtt.py +13 -9
  44. pymammotion/proto/__init__.py +2176 -1
  45. pymammotion/proto/luba_mul.proto +1 -0
  46. pymammotion/proto/luba_mul_pb2.py +8 -8
  47. pymammotion/proto/luba_mul_pb2.pyi +1 -0
  48. pymammotion/proto/mctrl_nav_pb2.py +69 -67
  49. pymammotion/proto/mctrl_nav_pb2.pyi +13 -5
  50. pymammotion/proto/mctrl_sys_pb2.py +41 -37
  51. pymammotion/proto/mctrl_sys_pb2.pyi +34 -11
  52. pymammotion/utility/constant/device_constant.py +14 -5
  53. pymammotion/utility/device_config.py +754 -0
  54. pymammotion/utility/device_type.py +64 -16
  55. {pymammotion-0.3.8.dist-info → pymammotion-0.4.0.dist-info}/METADATA +9 -9
  56. {pymammotion-0.3.8.dist-info → pymammotion-0.4.0.dist-info}/RECORD +58 -62
  57. {pymammotion-0.3.8.dist-info → pymammotion-0.4.0.dist-info}/WHEEL +1 -1
  58. pymammotion/aliyun/cloud_service.py +0 -65
  59. pymammotion/proto/basestation.py +0 -59
  60. pymammotion/proto/common.py +0 -12
  61. pymammotion/proto/dev_net.py +0 -381
  62. pymammotion/proto/luba_msg.py +0 -81
  63. pymammotion/proto/luba_mul.py +0 -76
  64. pymammotion/proto/mctrl_driver.py +0 -100
  65. pymammotion/proto/mctrl_nav.py +0 -664
  66. pymammotion/proto/mctrl_ota.py +0 -48
  67. pymammotion/proto/mctrl_pept.py +0 -41
  68. pymammotion/proto/mctrl_sys.py +0 -574
  69. {pymammotion-0.3.8.dist-info → pymammotion-0.4.0.dist-info}/LICENSE +0 -0
@@ -0,0 +1,49 @@
1
+ from dataclasses import dataclass, field
2
+
3
+
4
+ @dataclass
5
+ class RangeLimit:
6
+ min: float
7
+ max: float
8
+
9
+
10
+ @dataclass
11
+ class DeviceLimits:
12
+ cutter_height: RangeLimit = field(default_factory=RangeLimit)
13
+ working_speed: RangeLimit = field(default_factory=RangeLimit)
14
+ working_path: RangeLimit = field(default_factory=RangeLimit)
15
+ work_area_num_max: int = 60
16
+ display_image_type: int = 0
17
+
18
+ def to_dict(self) -> dict:
19
+ """Convert the device limits to a dictionary format."""
20
+ return {
21
+ "cutter_height": {"min": self.cutter_height.min, "max": self.cutter_height.max},
22
+ "working_speed": {"min": self.working_speed.min, "max": self.working_speed.max},
23
+ "working_path": {"min": self.working_path.min, "max": self.working_path.max},
24
+ "work_area_num_max": self.work_area_num_max,
25
+ "display_image_type": self.display_image_type,
26
+ }
27
+
28
+ @classmethod
29
+ def from_dict(cls, data: dict) -> "DeviceLimits":
30
+ """Create a DeviceLimits instance from a dictionary."""
31
+ return cls(
32
+ cutter_height=RangeLimit(min=data["cutter_height"]["min"], max=data["cutter_height"]["max"]),
33
+ working_speed=RangeLimit(min=data["working_speed"]["min"], max=data["working_speed"]["max"]),
34
+ working_path=RangeLimit(min=data["working_path"]["min"], max=data["working_path"]["max"]),
35
+ work_area_num_max=data["work_area_num_max"],
36
+ display_image_type=data["display_image_type"],
37
+ )
38
+
39
+ def validate(self) -> bool:
40
+ """Validate that all ranges are logical (min <= max)."""
41
+ return all(
42
+ [
43
+ self.cutter_height.min <= self.cutter_height.max,
44
+ self.working_speed.min <= self.working_speed.max,
45
+ self.working_path.min <= self.working_path.max,
46
+ self.work_area_num_max > 0,
47
+ self.display_image_type in (0, 1),
48
+ ]
49
+ )
@@ -1,5 +1,5 @@
1
- import logging
2
1
  from dataclasses import dataclass
2
+ import logging
3
3
 
4
4
  logger = logging.getLogger(__name__)
5
5
 
@@ -3,7 +3,7 @@ from enum import IntEnum
3
3
 
4
4
  from mashumaro.mixins.orjson import DataClassORJSONMixin
5
5
 
6
- from pymammotion.proto.mctrl_nav import NavGetCommDataAck, NavGetHashListAck, SvgMessageAckT
6
+ from pymammotion.proto import NavGetCommDataAck, NavGetHashListAck, SvgMessageAckT
7
7
 
8
8
 
9
9
  class PathType(IntEnum):
@@ -93,7 +93,7 @@ class HashList(DataClassORJSONMixin):
93
93
  # If no match was found, append the new item
94
94
  self.root_hash_list.data.append(hash_list)
95
95
 
96
- def missing_hash_frame(self):
96
+ def missing_hash_frame(self) -> list[int]:
97
97
  return self._find_missing_frames(self.root_hash_list)
98
98
 
99
99
  def missing_frame(self, hash_data: NavGetCommDataAck | SvgMessageAckT) -> list[int]:
@@ -112,6 +112,8 @@ class HashList(DataClassORJSONMixin):
112
112
  if hash_data.type == PathType.SVG:
113
113
  return self._find_missing_frames(self.svg.get(hash_data.data_hash))
114
114
 
115
+ return []
116
+
115
117
  def update(self, hash_data: NavGetCommDataAck | SvgMessageAckT) -> bool:
116
118
  """Update the map data."""
117
119
  if hash_data.type == PathType.AREA:
@@ -133,6 +135,8 @@ class HashList(DataClassORJSONMixin):
133
135
  if hash_data.type == PathType.SVG:
134
136
  return self._add_hash_data(self.svg, hash_data)
135
137
 
138
+ return False
139
+
136
140
  @staticmethod
137
141
  def _find_missing_frames(frame_list: FrameList | RootHashList) -> list[int]:
138
142
  if frame_list.total_frame == len(frame_list.data):
@@ -1,6 +1,3 @@
1
- from typing import List
2
-
3
-
4
1
  class Plan:
5
2
  def __init__(self) -> None:
6
3
  self.pver: int = 0
@@ -0,0 +1,215 @@
1
+ from dataclasses import dataclass, field
2
+
3
+ from mashumaro.mixins.orjson import DataClassORJSONMixin
4
+
5
+ from pymammotion.proto import DevNet, LubaMsg, MctlDriver, MctlNav, MctlOta, MctlPept, MctlSys, SocMul
6
+
7
+
8
+ @dataclass
9
+ class RawMowerData:
10
+ raw: LubaMsg | None = field(default_factory=LubaMsg)
11
+
12
+ @classmethod
13
+ def from_raw(cls, raw: dict) -> "RawMowerData":
14
+ """Take in raw data to hold in the betterproto dataclass."""
15
+ return RawMowerData(raw=LubaMsg(**raw))
16
+
17
+ def update_raw(self, raw: dict) -> None:
18
+ """Update the raw LubaMsg data."""
19
+ self.raw = LubaMsg(**raw)
20
+
21
+ @property
22
+ def net(self):
23
+ """Will return a wrapped betterproto of net."""
24
+ return DevNetData(net=self.raw.net)
25
+
26
+ @property
27
+ def sys(self):
28
+ """Will return a wrapped betterproto of sys."""
29
+ return SysData(sys=self.raw.sys)
30
+
31
+ @property
32
+ def nav(self):
33
+ """Will return a wrapped betterproto of nav."""
34
+ return NavData(nav=self.raw.nav)
35
+
36
+ @property
37
+ def driver(self):
38
+ """Will return a wrapped betterproto of driver."""
39
+ return DriverData(driver=self.raw.driver)
40
+
41
+ @property
42
+ def mul(self):
43
+ """Will return a wrapped betterproto of mul."""
44
+ return MulData(mul=self.raw.mul)
45
+
46
+ @property
47
+ def ota(self):
48
+ """Will return a wrapped betterproto of ota."""
49
+ return OtaData(ota=self.raw.ota)
50
+
51
+ @property
52
+ def pept(self):
53
+ """Will return a wrapped betterproto of pept."""
54
+ return PeptData(pept=self.raw.pept)
55
+
56
+
57
+ @dataclass
58
+ class DevNetData(DataClassORJSONMixin):
59
+ """Wrapping class around LubaMsg to return a dataclass from the raw dict."""
60
+
61
+ net: dict
62
+
63
+ def __init__(self, net: DevNet) -> None:
64
+ if isinstance(net, dict):
65
+ self.net = net
66
+ else:
67
+ self.net = net.to_dict()
68
+
69
+ def __getattr__(self, item):
70
+ """Intercept call to get net in dict and return a betterproto dataclass."""
71
+ if self.net.get(item) is None:
72
+ return DevNet().__getattribute__(item)
73
+
74
+ if not isinstance(self.net.get(item), dict):
75
+ return self.net.get(item)
76
+
77
+ return DevNet().__getattribute__(item).from_dict(value=self.net.get(item))
78
+
79
+
80
+ @dataclass
81
+ class SysData(DataClassORJSONMixin):
82
+ """Wrapping class around LubaMsg to return a dataclass from the raw dict."""
83
+
84
+ sys: dict
85
+
86
+ def __init__(self, sys: MctlSys) -> None:
87
+ if isinstance(sys, dict):
88
+ self.sys = sys
89
+ else:
90
+ self.sys = sys.to_dict()
91
+
92
+ def __getattr__(self, item: str):
93
+ """Intercept call to get sys in dict and return a betterproto dataclass."""
94
+ if self.sys.get(item) is None:
95
+ return MctlSys().__getattribute__(item)
96
+
97
+ if not isinstance(self.sys.get(item), dict):
98
+ return self.sys.get(item)
99
+
100
+ return MctlSys().__getattribute__(item).from_dict(value=self.sys.get(item))
101
+
102
+
103
+ @dataclass
104
+ class NavData(DataClassORJSONMixin):
105
+ """Wrapping class around LubaMsg to return a dataclass from the raw dict."""
106
+
107
+ nav: dict
108
+
109
+ def __init__(self, nav: MctlNav) -> None:
110
+ if isinstance(nav, dict):
111
+ self.nav = nav
112
+ else:
113
+ self.nav = nav.to_dict()
114
+
115
+ def __getattr__(self, item: str):
116
+ """Intercept call to get nav in dict and return a betterproto dataclass."""
117
+ if self.nav.get(item) is None:
118
+ return MctlNav().__getattribute__(item)
119
+
120
+ if not isinstance(self.nav.get(item), dict):
121
+ return self.nav.get(item)
122
+
123
+ return MctlNav().__getattribute__(item).from_dict(value=self.nav.get(item))
124
+
125
+
126
+ @dataclass
127
+ class DriverData(DataClassORJSONMixin):
128
+ """Wrapping class around LubaMsg to return a dataclass from the raw dict."""
129
+
130
+ driver: dict
131
+
132
+ def __init__(self, driver: MctlDriver) -> None:
133
+ if isinstance(driver, dict):
134
+ self.driver = driver
135
+ else:
136
+ self.driver = driver.to_dict()
137
+
138
+ def __getattr__(self, item: str):
139
+ """Intercept call to get driver in dict and return a betterproto dataclass."""
140
+ if self.driver.get(item) is None:
141
+ return MctlDriver().__getattribute__(item)
142
+
143
+ if not isinstance(self.driver.get(item), dict):
144
+ return self.driver.get(item)
145
+
146
+ return MctlDriver().__getattribute__(item).from_dict(value=self.driver.get(item))
147
+
148
+
149
+ @dataclass
150
+ class MulData(DataClassORJSONMixin):
151
+ """Wrapping class around LubaMsg to return a dataclass from the raw dict."""
152
+
153
+ mul: dict
154
+
155
+ def __init__(self, mul: SocMul) -> None:
156
+ if isinstance(mul, dict):
157
+ self.mul = mul
158
+ else:
159
+ self.mul = mul.to_dict()
160
+
161
+ def __getattr__(self, item: str):
162
+ """Intercept call to get mul in dict and return a betterproto dataclass."""
163
+ if self.mul.get(item) is None:
164
+ return SocMul().__getattribute__(item)
165
+
166
+ if not isinstance(self.mul.get(item), dict):
167
+ return self.mul.get(item)
168
+
169
+ return SocMul().__getattribute__(item).from_dict(value=self.mul.get(item))
170
+
171
+
172
+ @dataclass
173
+ class OtaData(DataClassORJSONMixin):
174
+ """Wrapping class around LubaMsg to return a dataclass from the raw dict."""
175
+
176
+ ota: dict
177
+
178
+ def __init__(self, ota: MctlOta) -> None:
179
+ if isinstance(ota, dict):
180
+ self.ota = ota
181
+ else:
182
+ self.ota = ota.to_dict()
183
+
184
+ def __getattr__(self, item: str):
185
+ """Intercept call to get ota in dict and return a betterproto dataclass."""
186
+ if self.ota.get(item) is None:
187
+ return MctlOta().__getattribute__(item)
188
+
189
+ if not isinstance(self.ota.get(item), dict):
190
+ return self.ota.get(item)
191
+
192
+ return MctlOta().__getattribute__(item).from_dict(value=self.ota.get(item))
193
+
194
+
195
+ @dataclass
196
+ class PeptData(DataClassORJSONMixin):
197
+ """Wrapping class around LubaMsg to return a dataclass from the raw dict."""
198
+
199
+ pept: dict
200
+
201
+ def __init__(self, pept: MctlPept) -> None:
202
+ if isinstance(pept, dict):
203
+ self.pept = pept
204
+ else:
205
+ self.pept = pept.to_dict()
206
+
207
+ def __getattr__(self, item: str):
208
+ """Intercept call to get pept in dict and return a betterproto dataclass."""
209
+ if self.pept.get(item) is None:
210
+ return MctlPept().__getattribute__(item)
211
+
212
+ if not isinstance(self.pept.get(item), dict):
213
+ return self.pept.get(item)
214
+
215
+ return MctlPept().__getattribute__(item).from_dict(value=self.pept.get(item))
@@ -1,5 +1,4 @@
1
1
  from dataclasses import dataclass
2
- from typing import Optional
3
2
 
4
3
  from mashumaro.mixins.orjson import DataClassORJSONMixin
5
4
 
@@ -7,14 +6,14 @@ from mashumaro.mixins.orjson import DataClassORJSONMixin
7
6
  @dataclass
8
7
  class RegionData(DataClassORJSONMixin):
9
8
  def __init__(self) -> None:
10
- self.hash: Optional[int] = None
9
+ self.hash: int | None = None
11
10
  self.action: int = 0
12
11
  self.current_frame: int = 0
13
- self.data_hash: Optional[int] = None
12
+ self.data_hash: int | None = None
14
13
  self.data_len: int = 0
15
- self.p_hash_a: Optional[int] = None
16
- self.p_hash_b: Optional[int] = None
17
- self.path: Optional[list[list[float]]] = None
14
+ self.p_hash_a: int | None = None
15
+ self.p_hash_b: int | None = None
16
+ self.path: list[list[float]] | None = None
18
17
  self.pver: int = 0
19
18
  self.result: int = 0
20
19
  self.sub_cmd: int = 0
@@ -72,31 +71,31 @@ class RegionData(DataClassORJSONMixin):
72
71
  def set_current_frame(self, current_frame: int) -> None:
73
72
  self.current_frame = current_frame
74
73
 
75
- def get_path(self) -> Optional[list[list[float]]]:
74
+ def get_path(self) -> list[list[float]] | None:
76
75
  return self.path
77
76
 
78
77
  def set_path(self, path: list[list[float]]) -> None:
79
78
  self.path = path
80
79
 
81
- def get_hash(self) -> Optional[int]:
80
+ def get_hash(self) -> int | None:
82
81
  return self.hash
83
82
 
84
83
  def set_data_hash(self, data_hash: int) -> None:
85
84
  self.data_hash = data_hash
86
85
 
87
- def get_data_hash(self) -> Optional[int]:
86
+ def get_data_hash(self) -> int | None:
88
87
  return self.data_hash
89
88
 
90
89
  def set_p_hash_a(self, p_hash_a: int) -> None:
91
90
  self.p_hash_a = p_hash_a
92
91
 
93
- def get_p_hash_a(self) -> Optional[int]:
92
+ def get_p_hash_a(self) -> int | None:
94
93
  return self.p_hash_a
95
94
 
96
95
  def set_p_hash_b(self, p_hash_b: int) -> None:
97
96
  self.p_hash_b = p_hash_b
98
97
 
99
- def get_p_hash_b(self) -> Optional[int]:
98
+ def get_p_hash_b(self) -> int | None:
100
99
  return self.p_hash_b
101
100
 
102
101
  def __str__(self) -> str:
@@ -8,7 +8,7 @@ class ConnectData(DataClassORJSONMixin):
8
8
  connect_type: int = 0
9
9
  ble_rssi: int = 0
10
10
  wifi_rssi: int = 0
11
- used_net: str = ""
11
+ used_net: str = "None"
12
12
 
13
13
 
14
14
  @dataclass
@@ -1,6 +1,6 @@
1
1
  from base64 import b64decode
2
2
  from dataclasses import dataclass
3
- from typing import Any, Literal, Optional, Union
3
+ from typing import Any, Literal
4
4
 
5
5
  from google.protobuf import json_format
6
6
  from mashumaro.mixins.orjson import DataClassORJSONMixin
@@ -91,16 +91,16 @@ class GeneralParams(DataClassORJSONMixin):
91
91
  tenantInstanceId: str
92
92
  value: Any
93
93
 
94
- identifier: Optional[str] = None
95
- checkFailedData: Optional[dict] = None
96
- _tenantId: Optional[str] = None
97
- generateTime: Optional[int] = None
98
- JMSXDeliveryCount: Optional[int] = None
99
- qos: Optional[int] = None
100
- requestId: Optional[str] = None
101
- _categoryKey: Optional[str] = None
102
- deviceType: Optional[str] = None
103
- _traceId: Optional[str] = None
94
+ identifier: str | None = None
95
+ checkFailedData: dict | None = None
96
+ _tenantId: str | None = None
97
+ generateTime: int | None = None
98
+ JMSXDeliveryCount: int | None = None
99
+ qos: int | None = None
100
+ requestId: str | None = None
101
+ _categoryKey: str | None = None
102
+ deviceType: str | None = None
103
+ _traceId: str | None = None
104
104
 
105
105
 
106
106
  @dataclass
@@ -117,7 +117,7 @@ class DeviceNotificationEventParams(GeneralParams):
117
117
  {'data': '{"localTime":1725159492000,"code":"1002"}'},
118
118
  """
119
119
 
120
- identifier: Literal["device_notification_event", "device_warning_code_event"]
120
+ identifier: Literal["device_notification_event", "device_information_event", "device_warning_code_event"]
121
121
  type: Literal["info"]
122
122
  value: DeviceNotificationEventValue
123
123
 
@@ -146,7 +146,7 @@ class DeviceConfigurationRequestEvent(GeneralParams):
146
146
  class ThingEventMessage(DataClassORJSONMixin):
147
147
  method: Literal["thing.events", "thing.properties"]
148
148
  id: str
149
- params: Union[DeviceProtobufMsgEventParams, DeviceWarningEventParams, dict]
149
+ params: DeviceProtobufMsgEventParams | DeviceWarningEventParams | dict
150
150
  version: Literal["1.0"]
151
151
 
152
152
  @classmethod
@@ -170,7 +170,11 @@ class ThingEventMessage(DataClassORJSONMixin):
170
170
  params_obj = DeviceBizReqEventParams.from_dict(params_dict)
171
171
  elif identifier == "device_config_req_event":
172
172
  params_obj = payload.get("params", {})
173
- elif identifier == "device_notification_event" or identifier == "device_warning_code_event":
173
+ elif (
174
+ identifier == "device_notification_event"
175
+ or identifier == "device_warning_code_event"
176
+ or identifier == "device_information_event"
177
+ ):
174
178
  params_obj = DeviceNotificationEventParams.from_dict(params_dict)
175
179
  else:
176
180
  raise ValueError(f"Unknown identifier: {identifier} {params_dict}")
@@ -116,7 +116,7 @@ Items = Union[
116
116
  @dataclass
117
117
  class Item:
118
118
  time: int
119
- value: Union[int, float, str, dict[str, Any]] # Depending on the type of value
119
+ value: int | float | str | dict[str, Any] # Depending on the type of value
120
120
 
121
121
 
122
122
  @dataclass
@@ -26,7 +26,7 @@ class Status(DataClassORJSONMixin):
26
26
  @dataclass
27
27
  class Params(DataClassORJSONMixin):
28
28
  groupIdList: list[GroupIdListItem]
29
- netType: Literal["NET_WIFI"]
29
+ netType: Literal["NET_WIFI", "NET_MNET"]
30
30
  activeTime: int
31
31
  ip: str
32
32
  aliyunCommodityCode: Literal["iothub_senior"]
@@ -1,8 +1,9 @@
1
1
  """Manage state from notifications into MowingDevice."""
2
2
 
3
- import logging
3
+ from collections.abc import Awaitable, Callable
4
4
  from datetime import datetime
5
- from typing import Any, Awaitable, Callable, Optional
5
+ import logging
6
+ from typing import Any
6
7
 
7
8
  import betterproto
8
9
 
@@ -10,10 +11,20 @@ from pymammotion.data.model.device import MowingDevice
10
11
  from pymammotion.data.model.device_info import SideLight
11
12
  from pymammotion.data.model.hash_list import AreaHashNameList
12
13
  from pymammotion.data.mqtt.properties import ThingPropertiesMessage
13
- from pymammotion.proto.dev_net import WifiIotStatusReport
14
- from pymammotion.proto.luba_msg import LubaMsg
15
- from pymammotion.proto.mctrl_nav import AppGetAllAreaHashName, NavGetCommDataAck, NavGetHashListAck, SvgMessageAckT
16
- from pymammotion.proto.mctrl_sys import DeviceProductTypeInfoT, TimeCtrlLight
14
+ from pymammotion.data.mqtt.status import ThingStatusMessage
15
+ from pymammotion.proto import (
16
+ AppGetAllAreaHashName,
17
+ DeviceFwInfo,
18
+ DeviceProductTypeInfoT,
19
+ DrvDevInfoResp,
20
+ DrvDevInfoResult,
21
+ LubaMsg,
22
+ NavGetCommDataAck,
23
+ NavGetHashListAck,
24
+ SvgMessageAckT,
25
+ TimeCtrlLight,
26
+ WifiIotStatusReport,
27
+ )
17
28
 
18
29
  logger = logging.getLogger(__name__)
19
30
 
@@ -23,15 +34,22 @@ class StateManager:
23
34
 
24
35
  _device: MowingDevice
25
36
  last_updated_at: datetime = datetime.now()
37
+ cloud_gethash_ack_callback: Callable[[NavGetHashListAck], Awaitable[None]] | None = None
38
+ cloud_get_commondata_ack_callback: Callable[[NavGetCommDataAck | SvgMessageAckT], Awaitable[None]] | None = None
39
+ cloud_on_notification_callback: Callable[[tuple[str, Any | None]], Awaitable[None]] | None = None
40
+
41
+ # possibly don't need anymore
42
+ cloud_queue_command_callback: Callable[[str, dict[str, Any]], Awaitable[bytes]] | None = None
43
+
44
+ ble_gethash_ack_callback: Callable[[NavGetHashListAck], Awaitable[None]] | None = None
45
+ ble_get_commondata_ack_callback: Callable[[NavGetCommDataAck | SvgMessageAckT], Awaitable[None]] | None = None
46
+ ble_on_notification_callback: Callable[[tuple[str, Any | None]], Awaitable[None]] | None = None
47
+
48
+ # possibly don't need anymore
49
+ ble_queue_command_callback: Callable[[str, dict[str, Any]], Awaitable[bytes]] | None = None
26
50
 
27
51
  def __init__(self, device: MowingDevice) -> None:
28
52
  self._device = device
29
- self.gethash_ack_callback: Optional[Callable[[NavGetHashListAck], Awaitable[None]]] = None
30
- self.get_commondata_ack_callback: Optional[Callable[[NavGetCommDataAck | SvgMessageAckT], Awaitable[None]]] = (
31
- None
32
- )
33
- self.on_notification_callback: Optional[Callable[[tuple[str, Any | None]], Awaitable[None]]] = None
34
- self.queue_command_callback: Optional[Callable[[str, dict[str, Any]], Awaitable[bytes]]] = None
35
53
  self.last_updated_at = datetime.now()
36
54
 
37
55
  def get_device(self) -> MowingDevice:
@@ -42,9 +60,40 @@ class StateManager:
42
60
  """Set device."""
43
61
  self._device = device
44
62
 
45
- async def properties(self, properties: ThingPropertiesMessage) -> None:
46
- params = properties.params
47
- self._device.mqtt_properties = params
63
+ def properties(self, thing_properties: ThingPropertiesMessage) -> None:
64
+ # TODO update device based off thing properties
65
+ self._device.mqtt_properties = thing_properties
66
+
67
+ def status(self, thing_status: ThingStatusMessage) -> None:
68
+ if not self._device.online:
69
+ self._device.online = True
70
+ self._device.status_properties = thing_status
71
+
72
+ @property
73
+ def online(self) -> bool:
74
+ return self._device.online
75
+
76
+ @online.setter
77
+ def online(self, value: bool) -> None:
78
+ self._device.online = value
79
+
80
+ async def gethash_ack_callback(self, msg: NavGetHashListAck) -> None:
81
+ if self.cloud_gethash_ack_callback:
82
+ await self.cloud_gethash_ack_callback(msg)
83
+ elif self.ble_gethash_ack_callback:
84
+ await self.ble_gethash_ack_callback(msg)
85
+
86
+ async def on_notification_callback(self, res: tuple[str, Any | None]) -> None:
87
+ if self.cloud_on_notification_callback:
88
+ await self.cloud_on_notification_callback(res)
89
+ elif self.ble_on_notification_callback:
90
+ await self.ble_on_notification_callback(res)
91
+
92
+ async def get_commondata_ack_callback(self, comm_data: NavGetCommDataAck | SvgMessageAckT) -> None:
93
+ if self.cloud_get_commondata_ack_callback:
94
+ await self.cloud_get_commondata_ack_callback(comm_data)
95
+ elif self.ble_get_commondata_ack_callback:
96
+ await self.ble_get_commondata_ack_callback(comm_data)
48
97
 
49
98
  async def notification(self, message: LubaMsg) -> None:
50
99
  """Handle protobuf notifications."""
@@ -55,7 +104,7 @@ class StateManager:
55
104
  case "nav":
56
105
  await self._update_nav_data(message)
57
106
  case "sys":
58
- await self._update_sys_data(message)
107
+ self._update_sys_data(message)
59
108
  case "driver":
60
109
  self._update_driver_data(message)
61
110
  case "net":
@@ -65,8 +114,7 @@ class StateManager:
65
114
  case "ota":
66
115
  self._update_ota_data(message)
67
116
 
68
- if self.on_notification_callback:
69
- await self.on_notification_callback(res)
117
+ await self.on_notification_callback(res)
70
118
 
71
119
  async def _update_nav_data(self, message) -> None:
72
120
  """Update nav data."""
@@ -82,17 +130,17 @@ class StateManager:
82
130
  if updated:
83
131
  await self.get_commondata_ack_callback(common_data)
84
132
  case "toapp_svg_msg":
85
- common_data: SvgMessageAckT = nav_msg[1]
86
- updated = self._device.map.update(common_data)
133
+ common_svg_data: SvgMessageAckT = nav_msg[1]
134
+ updated = self._device.map.update(common_svg_data)
87
135
  if updated:
88
- await self.get_commondata_ack_callback(common_data)
136
+ await self.get_commondata_ack_callback(common_svg_data)
89
137
 
90
138
  case "toapp_all_hash_name":
91
139
  hash_names: AppGetAllAreaHashName = nav_msg[1]
92
140
  converted_list = [AreaHashNameList(name=item.name, hash=item.hash) for item in hash_names.hashnames]
93
141
  self._device.map.area_name = converted_list
94
142
 
95
- async def _update_sys_data(self, message) -> None:
143
+ def _update_sys_data(self, message) -> None:
96
144
  """Update system."""
97
145
  sys_msg = betterproto.which_one_of(message.sys, "SubSysMsg")
98
146
  match sys_msg[0]:
@@ -110,7 +158,13 @@ class StateManager:
110
158
  self._device.mower_state.side_led = side_led
111
159
  case "device_product_type_info":
112
160
  device_product_type: DeviceProductTypeInfoT = sys_msg[1]
113
- self._device.mower_state.model_id = device_product_type.main_product_type
161
+ if device_product_type.main_product_type != "" or device_product_type.sub_product_type != "":
162
+ self._device.mower_state.model_id = device_product_type.main_product_type
163
+ self._device.mower_state.sub_model_id = device_product_type.sub_product_type
164
+ case "toapp_dev_fw_info":
165
+ device_fw_info: DeviceFwInfo = sys_msg[1]
166
+ self._device.device_firmwares.device_version = device_fw_info.version
167
+ self._device.mower_state.swversion = device_fw_info.version
114
168
 
115
169
  def _update_driver_data(self, message) -> None:
116
170
  pass
@@ -121,6 +175,12 @@ class StateManager:
121
175
  case "toapp_wifi_iot_status":
122
176
  wifi_iot_status: WifiIotStatusReport = net_msg[1]
123
177
  self._device.mower_state.product_key = wifi_iot_status.productkey
178
+ case "toapp_devinfo_resp":
179
+ toapp_devinfo_resp: DrvDevInfoResp = net_msg[1]
180
+ for resp in toapp_devinfo_resp.resp_ids:
181
+ if resp.res == DrvDevInfoResult.DRV_RESULT_SUC and resp.id == 1 and resp.type == 6:
182
+ self._device.mower_state.swversion = resp.info
183
+ self._device.device_firmwares.device_version = resp.info
124
184
 
125
185
  def _update_mul_data(self, message) -> None:
126
186
  pass