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.
- pymammotion/__init__.py +2 -2
- pymammotion/aliyun/cloud_gateway.py +12 -9
- pymammotion/aliyun/model/aep_response.py +1 -2
- pymammotion/aliyun/model/dev_by_account_response.py +7 -8
- pymammotion/aliyun/model/login_by_oauth_response.py +2 -3
- pymammotion/aliyun/model/regions_response.py +3 -3
- pymammotion/aliyun/model/session_by_authcode_response.py +1 -2
- pymammotion/aliyun/model/stream_subscription_response.py +1 -2
- pymammotion/bluetooth/ble.py +5 -5
- pymammotion/bluetooth/ble_message.py +9 -13
- pymammotion/data/model/device.py +31 -228
- pymammotion/data/model/device_config.py +0 -10
- pymammotion/data/model/device_info.py +13 -0
- pymammotion/data/model/device_limits.py +49 -0
- pymammotion/data/model/generate_route_information.py +1 -1
- pymammotion/data/model/hash_list.py +6 -2
- pymammotion/data/model/plan.py +0 -3
- pymammotion/data/model/raw_data.py +215 -0
- pymammotion/data/model/region_data.py +10 -11
- pymammotion/data/model/report_info.py +1 -1
- pymammotion/data/mqtt/event.py +18 -14
- pymammotion/data/mqtt/properties.py +1 -1
- pymammotion/data/mqtt/status.py +1 -1
- pymammotion/data/state_manager.py +83 -23
- pymammotion/http/encryption.py +220 -0
- pymammotion/http/http.py +92 -39
- pymammotion/http/model/http.py +2 -2
- pymammotion/mammotion/commands/abstract_message.py +2 -2
- pymammotion/mammotion/commands/messages/driver.py +28 -21
- pymammotion/mammotion/commands/messages/media.py +10 -14
- pymammotion/mammotion/commands/messages/navigation.py +14 -11
- pymammotion/mammotion/commands/messages/network.py +15 -12
- pymammotion/mammotion/commands/messages/ota.py +9 -14
- pymammotion/mammotion/commands/messages/system.py +27 -24
- pymammotion/mammotion/commands/messages/video.py +9 -14
- pymammotion/mammotion/devices/base.py +7 -14
- pymammotion/mammotion/devices/mammotion.py +22 -13
- pymammotion/mammotion/devices/mammotion_bluetooth.py +15 -4
- pymammotion/mammotion/devices/mammotion_cloud.py +30 -12
- pymammotion/mqtt/linkkit/__init__.py +5 -0
- pymammotion/mqtt/linkkit/h2client.py +585 -0
- pymammotion/mqtt/linkkit/linkkit.py +3020 -0
- pymammotion/mqtt/mammotion_mqtt.py +13 -9
- pymammotion/proto/__init__.py +2176 -1
- pymammotion/proto/luba_mul.proto +1 -0
- pymammotion/proto/luba_mul_pb2.py +8 -8
- pymammotion/proto/luba_mul_pb2.pyi +1 -0
- pymammotion/proto/mctrl_nav_pb2.py +69 -67
- pymammotion/proto/mctrl_nav_pb2.pyi +13 -5
- pymammotion/proto/mctrl_sys_pb2.py +41 -37
- pymammotion/proto/mctrl_sys_pb2.pyi +34 -11
- pymammotion/utility/constant/device_constant.py +14 -5
- pymammotion/utility/device_config.py +754 -0
- pymammotion/utility/device_type.py +64 -16
- {pymammotion-0.3.8.dist-info → pymammotion-0.4.0.dist-info}/METADATA +9 -9
- {pymammotion-0.3.8.dist-info → pymammotion-0.4.0.dist-info}/RECORD +58 -62
- {pymammotion-0.3.8.dist-info → pymammotion-0.4.0.dist-info}/WHEEL +1 -1
- pymammotion/aliyun/cloud_service.py +0 -65
- pymammotion/proto/basestation.py +0 -59
- pymammotion/proto/common.py +0 -12
- pymammotion/proto/dev_net.py +0 -381
- pymammotion/proto/luba_msg.py +0 -81
- pymammotion/proto/luba_mul.py +0 -76
- pymammotion/proto/mctrl_driver.py +0 -100
- pymammotion/proto/mctrl_nav.py +0 -664
- pymammotion/proto/mctrl_ota.py +0 -48
- pymammotion/proto/mctrl_pept.py +0 -41
- pymammotion/proto/mctrl_sys.py +0 -574
- {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
|
+
)
|
@@ -3,7 +3,7 @@ from enum import IntEnum
|
|
3
3
|
|
4
4
|
from mashumaro.mixins.orjson import DataClassORJSONMixin
|
5
5
|
|
6
|
-
from pymammotion.proto
|
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):
|
pymammotion/data/model/plan.py
CHANGED
@@ -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:
|
9
|
+
self.hash: int | None = None
|
11
10
|
self.action: int = 0
|
12
11
|
self.current_frame: int = 0
|
13
|
-
self.data_hash:
|
12
|
+
self.data_hash: int | None = None
|
14
13
|
self.data_len: int = 0
|
15
|
-
self.p_hash_a:
|
16
|
-
self.p_hash_b:
|
17
|
-
self.path:
|
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) ->
|
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) ->
|
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) ->
|
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) ->
|
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) ->
|
98
|
+
def get_p_hash_b(self) -> int | None:
|
100
99
|
return self.p_hash_b
|
101
100
|
|
102
101
|
def __str__(self) -> str:
|
pymammotion/data/mqtt/event.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
from base64 import b64decode
|
2
2
|
from dataclasses import dataclass
|
3
|
-
from typing import Any, Literal
|
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:
|
95
|
-
checkFailedData:
|
96
|
-
_tenantId:
|
97
|
-
generateTime:
|
98
|
-
JMSXDeliveryCount:
|
99
|
-
qos:
|
100
|
-
requestId:
|
101
|
-
_categoryKey:
|
102
|
-
deviceType:
|
103
|
-
_traceId:
|
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:
|
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
|
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}")
|
pymammotion/data/mqtt/status.py
CHANGED
@@ -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
|
3
|
+
from collections.abc import Awaitable, Callable
|
4
4
|
from datetime import datetime
|
5
|
-
|
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.
|
14
|
-
from pymammotion.proto
|
15
|
-
|
16
|
-
|
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
|
-
|
46
|
-
|
47
|
-
self._device.mqtt_properties =
|
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
|
-
|
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
|
-
|
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
|
-
|
86
|
-
updated = self._device.map.update(
|
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(
|
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
|
-
|
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
|
-
|
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
|