pymammotion 0.5.69__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 +53 -0
- pymammotion/agora/__init__.py +0 -0
- pymammotion/agora/agora_api.py +755 -0
- pymammotion/agora/agora_rtc_capabilities.py +748 -0
- pymammotion/agora/agora_websockets.py +1175 -0
- pymammotion/aliyun/__init__.py +1 -0
- pymammotion/aliyun/client.py +235 -0
- pymammotion/aliyun/cloud_gateway.py +982 -0
- pymammotion/aliyun/model/aep_response.py +21 -0
- pymammotion/aliyun/model/connect_response.py +51 -0
- pymammotion/aliyun/model/dev_by_account_response.py +195 -0
- pymammotion/aliyun/model/login_by_oauth_response.py +64 -0
- pymammotion/aliyun/model/regions_response.py +29 -0
- pymammotion/aliyun/model/session_by_authcode_response.py +19 -0
- pymammotion/aliyun/model/thing_response.py +12 -0
- pymammotion/aliyun/regions.py +62 -0
- pymammotion/aliyun/tea/core.py +297 -0
- pymammotion/aliyun/tmp_constant.py +171 -0
- pymammotion/bluetooth/__init__.py +1 -0
- pymammotion/bluetooth/ble.py +62 -0
- pymammotion/bluetooth/ble_message.py +676 -0
- pymammotion/bluetooth/const.py +27 -0
- pymammotion/bluetooth/data/__init__.py +0 -0
- pymammotion/bluetooth/data/convert.py +25 -0
- pymammotion/bluetooth/data/framectrldata.py +40 -0
- pymammotion/bluetooth/data/notifydata.py +62 -0
- pymammotion/bluetooth/model/__init__.py +0 -0
- pymammotion/bluetooth/model/atomic_integer.py +54 -0
- pymammotion/const.py +13 -0
- pymammotion/data/__init__.py +0 -0
- pymammotion/data/model/__init__.py +8 -0
- pymammotion/data/model/account.py +8 -0
- pymammotion/data/model/device.py +192 -0
- pymammotion/data/model/device_config.py +72 -0
- pymammotion/data/model/device_info.py +60 -0
- pymammotion/data/model/device_limits.py +49 -0
- pymammotion/data/model/enums.py +77 -0
- pymammotion/data/model/errors.py +12 -0
- pymammotion/data/model/events.py +14 -0
- pymammotion/data/model/generate_geojson.py +565 -0
- pymammotion/data/model/generate_route_information.py +26 -0
- pymammotion/data/model/hash_list.py +475 -0
- pymammotion/data/model/location.py +36 -0
- pymammotion/data/model/mowing_modes.py +77 -0
- pymammotion/data/model/rapid_state.py +45 -0
- pymammotion/data/model/raw_data.py +215 -0
- pymammotion/data/model/region_data.py +102 -0
- pymammotion/data/model/report_info.py +182 -0
- pymammotion/data/model/work.py +27 -0
- pymammotion/data/mower_state_manager.py +369 -0
- pymammotion/data/mqtt/__init__.py +1 -0
- pymammotion/data/mqtt/event.py +227 -0
- pymammotion/data/mqtt/mammotion_properties.py +276 -0
- pymammotion/data/mqtt/properties.py +203 -0
- pymammotion/data/mqtt/status.py +57 -0
- pymammotion/event/__init__.py +6 -0
- pymammotion/event/event.py +96 -0
- pymammotion/homeassistant/__init__.py +3 -0
- pymammotion/homeassistant/mower_api.py +514 -0
- pymammotion/homeassistant/rtk_api.py +54 -0
- pymammotion/http/__init__.py +0 -0
- pymammotion/http/encryption.py +220 -0
- pymammotion/http/http.py +673 -0
- pymammotion/http/model/__init__.py +0 -0
- pymammotion/http/model/camera_stream.py +31 -0
- pymammotion/http/model/http.py +249 -0
- pymammotion/http/model/response_factory.py +61 -0
- pymammotion/http/model/rtk.py +16 -0
- pymammotion/mammotion/__init__.py +0 -0
- pymammotion/mammotion/commands/__init__.py +0 -0
- pymammotion/mammotion/commands/abstract_message.py +24 -0
- pymammotion/mammotion/commands/mammotion_command.py +81 -0
- pymammotion/mammotion/commands/messages/__init__.py +0 -0
- pymammotion/mammotion/commands/messages/basestation.py +43 -0
- pymammotion/mammotion/commands/messages/driver.py +122 -0
- pymammotion/mammotion/commands/messages/media.py +87 -0
- pymammotion/mammotion/commands/messages/navigation.py +564 -0
- pymammotion/mammotion/commands/messages/network.py +205 -0
- pymammotion/mammotion/commands/messages/ota.py +38 -0
- pymammotion/mammotion/commands/messages/system.py +330 -0
- pymammotion/mammotion/commands/messages/video.py +33 -0
- pymammotion/mammotion/control/__init__.py +0 -0
- pymammotion/mammotion/control/joystick.py +145 -0
- pymammotion/mammotion/devices/__init__.py +29 -0
- pymammotion/mammotion/devices/base.py +163 -0
- pymammotion/mammotion/devices/mammotion.py +571 -0
- pymammotion/mammotion/devices/mammotion_bluetooth.py +496 -0
- pymammotion/mammotion/devices/mammotion_cloud.py +355 -0
- pymammotion/mammotion/devices/mammotion_mower_ble.py +48 -0
- pymammotion/mammotion/devices/mammotion_mower_cloud.py +39 -0
- pymammotion/mammotion/devices/managers/managers.py +81 -0
- pymammotion/mammotion/devices/mower_device.py +120 -0
- pymammotion/mammotion/devices/mower_manager.py +107 -0
- pymammotion/mammotion/devices/rtk_ble.py +89 -0
- pymammotion/mammotion/devices/rtk_cloud.py +115 -0
- pymammotion/mammotion/devices/rtk_device.py +50 -0
- pymammotion/mammotion/devices/rtk_manager.py +125 -0
- pymammotion/mqtt/__init__.py +6 -0
- pymammotion/mqtt/aliyun_mqtt.py +237 -0
- pymammotion/mqtt/linkkit/__init__.py +5 -0
- pymammotion/mqtt/linkkit/h2client.py +585 -0
- pymammotion/mqtt/linkkit/linkkit.py +3025 -0
- pymammotion/mqtt/mammotion_future.py +26 -0
- pymammotion/mqtt/mammotion_mqtt.py +214 -0
- pymammotion/mqtt/mqtt_models.py +66 -0
- pymammotion/proto/__init__.py +4841 -0
- pymammotion/proto/basestation.proto +51 -0
- pymammotion/proto/basestation_pb2.py +35 -0
- pymammotion/proto/basestation_pb2.pyi +89 -0
- pymammotion/proto/common.proto +7 -0
- pymammotion/proto/common_pb2.py +25 -0
- pymammotion/proto/common_pb2.pyi +13 -0
- pymammotion/proto/dev_net.proto +321 -0
- pymammotion/proto/dev_net_pb2.py +111 -0
- pymammotion/proto/dev_net_pb2.pyi +515 -0
- pymammotion/proto/luba_msg.proto +76 -0
- pymammotion/proto/luba_msg_pb2.py +41 -0
- pymammotion/proto/luba_msg_pb2.pyi +97 -0
- pymammotion/proto/luba_mul.proto +129 -0
- pymammotion/proto/luba_mul_pb2.py +61 -0
- pymammotion/proto/luba_mul_pb2.pyi +178 -0
- pymammotion/proto/mctrl_driver.proto +107 -0
- pymammotion/proto/mctrl_driver_pb2.py +57 -0
- pymammotion/proto/mctrl_driver_pb2.pyi +167 -0
- pymammotion/proto/mctrl_nav.proto +591 -0
- pymammotion/proto/mctrl_nav_pb2.py +136 -0
- pymammotion/proto/mctrl_nav_pb2.pyi +1067 -0
- pymammotion/proto/mctrl_ota.proto +80 -0
- pymammotion/proto/mctrl_ota_pb2.py +45 -0
- pymammotion/proto/mctrl_ota_pb2.pyi +128 -0
- pymammotion/proto/mctrl_pept.proto +34 -0
- pymammotion/proto/mctrl_pept_pb2.py +33 -0
- pymammotion/proto/mctrl_pept_pb2.pyi +58 -0
- pymammotion/proto/mctrl_sys.proto +741 -0
- pymammotion/proto/mctrl_sys_pb2.py +206 -0
- pymammotion/proto/mctrl_sys_pb2.pyi +1213 -0
- pymammotion/proto/message_pool.py +3 -0
- pymammotion/proto/py.typed +0 -0
- pymammotion/py.typed +0 -0
- pymammotion/utility/constant/__init__.py +3 -0
- pymammotion/utility/constant/device_constant.py +315 -0
- pymammotion/utility/conversions.py +5 -0
- pymammotion/utility/datatype_converter.py +124 -0
- pymammotion/utility/device_config.py +755 -0
- pymammotion/utility/device_type.py +489 -0
- pymammotion/utility/map.py +259 -0
- pymammotion/utility/movement.py +18 -0
- pymammotion/utility/mur_mur_hash.py +159 -0
- pymammotion/utility/periodic.py +106 -0
- pymammotion/utility/rocker_util.py +194 -0
- pymammotion-0.5.69.dist-info/METADATA +93 -0
- pymammotion-0.5.69.dist-info/RECORD +154 -0
- pymammotion-0.5.69.dist-info/WHEEL +4 -0
- pymammotion-0.5.69.dist-info/licenses/LICENSE +674 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"""Manage state from notifications into MowingDevice."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Awaitable, Callable
|
|
4
|
+
from datetime import UTC, datetime
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import betterproto2
|
|
9
|
+
from shapely import Point
|
|
10
|
+
|
|
11
|
+
from pymammotion.data.model.device import MowingDevice
|
|
12
|
+
from pymammotion.data.model.device_info import SideLight
|
|
13
|
+
from pymammotion.data.model.generate_geojson import GeojsonGenerator
|
|
14
|
+
from pymammotion.data.model.hash_list import (
|
|
15
|
+
AreaHashNameList,
|
|
16
|
+
MowPath,
|
|
17
|
+
NavGetCommData,
|
|
18
|
+
NavGetHashListData,
|
|
19
|
+
Plan,
|
|
20
|
+
SvgMessage,
|
|
21
|
+
)
|
|
22
|
+
from pymammotion.data.model.location import Dock, LocationPoint
|
|
23
|
+
from pymammotion.data.model.work import CurrentTaskSettings
|
|
24
|
+
from pymammotion.data.mqtt.event import ThingEventMessage
|
|
25
|
+
from pymammotion.data.mqtt.properties import ThingPropertiesMessage
|
|
26
|
+
from pymammotion.data.mqtt.status import ThingStatusMessage
|
|
27
|
+
from pymammotion.event.event import DataEvent
|
|
28
|
+
from pymammotion.proto import (
|
|
29
|
+
AppGetAllAreaHashName,
|
|
30
|
+
AppGetCutterWorkMode,
|
|
31
|
+
AppSetCutterWorkMode,
|
|
32
|
+
CoverPathUploadT,
|
|
33
|
+
DeviceFwInfo,
|
|
34
|
+
DeviceProductTypeInfoT,
|
|
35
|
+
DrvDevInfoResp,
|
|
36
|
+
DrvDevInfoResult,
|
|
37
|
+
Getlamprsp,
|
|
38
|
+
GetNetworkInfoRsp,
|
|
39
|
+
LubaMsg,
|
|
40
|
+
NavGetCommDataAck,
|
|
41
|
+
NavGetHashListAck,
|
|
42
|
+
NavPlanJobSet,
|
|
43
|
+
NavReqCoverPath,
|
|
44
|
+
NavSysParamMsg,
|
|
45
|
+
NavUnableTimeSet,
|
|
46
|
+
SvgMessageAckT,
|
|
47
|
+
TimeCtrlLight,
|
|
48
|
+
WifiIotStatusReport,
|
|
49
|
+
)
|
|
50
|
+
from pymammotion.utility.map import CoordinateConverter
|
|
51
|
+
|
|
52
|
+
logger = logging.getLogger(__name__)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class MowerStateManager:
|
|
56
|
+
"""Manage state."""
|
|
57
|
+
|
|
58
|
+
def __init__(self, device: MowingDevice) -> None:
|
|
59
|
+
"""Initialize state manager with a device."""
|
|
60
|
+
self._device: MowingDevice = device
|
|
61
|
+
self.last_updated_at = datetime.now(UTC)
|
|
62
|
+
self.cloud_gethash_ack_callback: Callable[[NavGetHashListAck], Awaitable[None]] | None = None
|
|
63
|
+
self.cloud_get_commondata_ack_callback: (
|
|
64
|
+
Callable[[NavGetCommDataAck | SvgMessageAckT], Awaitable[None]] | None
|
|
65
|
+
) = None
|
|
66
|
+
self.cloud_on_notification_callback = DataEvent()
|
|
67
|
+
self.cloud_queue_command_callback = DataEvent()
|
|
68
|
+
|
|
69
|
+
self.cloud_get_plan_callback: Callable[[NavPlanJobSet], Awaitable[None]] | None = None
|
|
70
|
+
self.ble_gethash_ack_callback: Callable[[NavGetHashListAck], Awaitable[None]] | None = None
|
|
71
|
+
self.ble_get_commondata_ack_callback: Callable[[NavGetCommDataAck | SvgMessageAckT], Awaitable[None]] | None = (
|
|
72
|
+
None
|
|
73
|
+
)
|
|
74
|
+
self.ble_get_plan_callback: Callable[[NavPlanJobSet], Awaitable[None]] | None = None
|
|
75
|
+
self.ble_on_notification_callback = DataEvent()
|
|
76
|
+
self.ble_queue_command_callback = DataEvent()
|
|
77
|
+
|
|
78
|
+
self.properties_callback = DataEvent()
|
|
79
|
+
self.status_callback = DataEvent()
|
|
80
|
+
self.device_event_callback = DataEvent()
|
|
81
|
+
|
|
82
|
+
def get_device(self) -> MowingDevice:
|
|
83
|
+
"""Get device."""
|
|
84
|
+
return self._device
|
|
85
|
+
|
|
86
|
+
def set_device(self, device: MowingDevice) -> None:
|
|
87
|
+
"""Set device."""
|
|
88
|
+
self._device = device
|
|
89
|
+
|
|
90
|
+
async def properties(self, thing_properties: ThingPropertiesMessage) -> None:
|
|
91
|
+
"""Update device properties and invoke callback."""
|
|
92
|
+
# TODO update device based off thing properties
|
|
93
|
+
self._device.mqtt_properties = thing_properties
|
|
94
|
+
await self.on_properties_callback(thing_properties)
|
|
95
|
+
|
|
96
|
+
async def status(self, thing_status: ThingStatusMessage) -> None:
|
|
97
|
+
"""Update device status and invoke callback."""
|
|
98
|
+
if not self._device.online:
|
|
99
|
+
self._device.online = True
|
|
100
|
+
self._device.status_properties = thing_status
|
|
101
|
+
if self._device.mower_state.product_key == "":
|
|
102
|
+
self._device.mower_state.product_key = thing_status.params.product_key
|
|
103
|
+
await self.on_status_callback(thing_status)
|
|
104
|
+
|
|
105
|
+
async def device_event(self, device_event: ThingEventMessage) -> None:
|
|
106
|
+
"""Sets MQTT event and calls callback."""
|
|
107
|
+
self._device.mqtt_device_event = device_event
|
|
108
|
+
await self.on_device_event_callback(device_event)
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def online(self) -> bool:
|
|
112
|
+
"""Return online status."""
|
|
113
|
+
return self._device.online
|
|
114
|
+
|
|
115
|
+
@online.setter
|
|
116
|
+
def online(self, value: bool) -> None:
|
|
117
|
+
"""Set online status."""
|
|
118
|
+
self._device.online = value
|
|
119
|
+
|
|
120
|
+
async def gethash_ack_callback(self, msg: NavGetHashListAck) -> None:
|
|
121
|
+
"""Dispatch hash list acknowledgment to available callback."""
|
|
122
|
+
if self.cloud_gethash_ack_callback:
|
|
123
|
+
await self.cloud_gethash_ack_callback(msg)
|
|
124
|
+
elif self.ble_gethash_ack_callback:
|
|
125
|
+
await self.ble_gethash_ack_callback(msg)
|
|
126
|
+
|
|
127
|
+
async def on_notification_callback(self, res: tuple[str, Any | None]) -> None:
|
|
128
|
+
"""Dispatch notification to available callback."""
|
|
129
|
+
if self.cloud_on_notification_callback:
|
|
130
|
+
await self.cloud_on_notification_callback.data_event(res)
|
|
131
|
+
elif self.ble_on_notification_callback:
|
|
132
|
+
await self.ble_on_notification_callback.data_event(res)
|
|
133
|
+
|
|
134
|
+
async def on_properties_callback(self, thing_properties: ThingPropertiesMessage) -> None:
|
|
135
|
+
"""Call properties callback if it exists."""
|
|
136
|
+
if self.properties_callback:
|
|
137
|
+
await self.properties_callback.data_event(thing_properties)
|
|
138
|
+
|
|
139
|
+
async def on_status_callback(self, thing_status: ThingStatusMessage) -> None:
|
|
140
|
+
"""Execute the status callback if it is set."""
|
|
141
|
+
if self.status_callback:
|
|
142
|
+
await self.status_callback.data_event(thing_status)
|
|
143
|
+
|
|
144
|
+
async def on_device_event_callback(self, device_event: ThingEventMessage) -> None:
|
|
145
|
+
"""Executes the event callback if it is set."""
|
|
146
|
+
if self.device_event_callback:
|
|
147
|
+
await self.device_event_callback.data_event(device_event)
|
|
148
|
+
|
|
149
|
+
async def get_commondata_ack_callback(self, comm_data: NavGetCommDataAck | SvgMessageAckT) -> None:
|
|
150
|
+
"""Asynchronously calls the appropriate callback based on available handlers."""
|
|
151
|
+
if self.cloud_get_commondata_ack_callback:
|
|
152
|
+
await self.cloud_get_commondata_ack_callback(comm_data)
|
|
153
|
+
elif self.ble_get_commondata_ack_callback:
|
|
154
|
+
await self.ble_get_commondata_ack_callback(comm_data)
|
|
155
|
+
|
|
156
|
+
async def get_plan_callback(self, planjob: NavPlanJobSet) -> None:
|
|
157
|
+
"""Dispatch plan job to available callback."""
|
|
158
|
+
if self.cloud_get_plan_callback:
|
|
159
|
+
await self.cloud_get_plan_callback(planjob)
|
|
160
|
+
elif self.ble_get_plan_callback:
|
|
161
|
+
await self.ble_get_plan_callback(planjob)
|
|
162
|
+
|
|
163
|
+
async def queue_command_callback(self, **kwargs: Any) -> None:
|
|
164
|
+
"""Queue command to available callback."""
|
|
165
|
+
if self.cloud_queue_command_callback:
|
|
166
|
+
await self.cloud_queue_command_callback.data_event(**kwargs)
|
|
167
|
+
elif self.ble_queue_command_callback:
|
|
168
|
+
await self.ble_queue_command_callback.data_event(**kwargs)
|
|
169
|
+
|
|
170
|
+
async def notification(self, message: LubaMsg) -> None:
|
|
171
|
+
"""Handle protobuf notifications."""
|
|
172
|
+
res = betterproto2.which_one_of(message, "LubaSubMsg")
|
|
173
|
+
self.last_updated_at = datetime.now(UTC)
|
|
174
|
+
# additional catch all if we don't get a status update
|
|
175
|
+
if not self._device.online:
|
|
176
|
+
self._device.online = True
|
|
177
|
+
|
|
178
|
+
match res[0]:
|
|
179
|
+
case "nav":
|
|
180
|
+
await self._update_nav_data(message)
|
|
181
|
+
case "sys":
|
|
182
|
+
self._update_sys_data(message)
|
|
183
|
+
case "driver":
|
|
184
|
+
self._update_driver_data(message)
|
|
185
|
+
case "net":
|
|
186
|
+
self._update_net_data(message)
|
|
187
|
+
case "mul":
|
|
188
|
+
self._update_mul_data(message)
|
|
189
|
+
case "ota":
|
|
190
|
+
self._update_ota_data(message)
|
|
191
|
+
|
|
192
|
+
await self.on_notification_callback(res)
|
|
193
|
+
|
|
194
|
+
async def _update_nav_data(self, message: LubaMsg) -> None:
|
|
195
|
+
"""Update nav data."""
|
|
196
|
+
nav_msg = betterproto2.which_one_of(message.nav, "SubNavMsg")
|
|
197
|
+
match nav_msg[0]:
|
|
198
|
+
case "toapp_gethash_ack":
|
|
199
|
+
hashlist_ack: NavGetHashListAck = nav_msg[1]
|
|
200
|
+
self._device.map.update_root_hash_list(
|
|
201
|
+
NavGetHashListData.from_dict(hashlist_ack.to_dict(casing=betterproto2.Casing.SNAKE))
|
|
202
|
+
)
|
|
203
|
+
await self.gethash_ack_callback(nav_msg[1])
|
|
204
|
+
case "toapp_get_commondata_ack":
|
|
205
|
+
common_data: NavGetCommDataAck = nav_msg[1]
|
|
206
|
+
updated = self._device.map.update(
|
|
207
|
+
NavGetCommData.from_dict(common_data.to_dict(casing=betterproto2.Casing.SNAKE))
|
|
208
|
+
)
|
|
209
|
+
if updated:
|
|
210
|
+
if len(self._device.map.missing_hashlist(0)) == 0:
|
|
211
|
+
self.generate_geojson(self._device.location.RTK, self._device.location.dock)
|
|
212
|
+
|
|
213
|
+
await self.get_commondata_ack_callback(common_data)
|
|
214
|
+
case "cover_path_upload":
|
|
215
|
+
mow_path: CoverPathUploadT = nav_msg[1]
|
|
216
|
+
self._device.map.update_mow_path(MowPath.from_dict(mow_path.to_dict(casing=betterproto2.Casing.SNAKE)))
|
|
217
|
+
if len(self._device.map.find_missing_mow_path_frames()) == 0:
|
|
218
|
+
self.generate_mowing_geojson(self._device.location.RTK)
|
|
219
|
+
|
|
220
|
+
case "todev_planjob_set":
|
|
221
|
+
planjob: NavPlanJobSet = nav_msg[1]
|
|
222
|
+
self._device.map.update_plan(Plan.from_dict(planjob.to_dict(casing=betterproto2.Casing.SNAKE)))
|
|
223
|
+
await self.get_plan_callback(planjob)
|
|
224
|
+
|
|
225
|
+
case "toapp_svg_msg":
|
|
226
|
+
common_svg_data: SvgMessageAckT = nav_msg[1]
|
|
227
|
+
updated = self._device.map.update(
|
|
228
|
+
SvgMessage.from_dict(common_svg_data.to_dict(casing=betterproto2.Casing.SNAKE))
|
|
229
|
+
)
|
|
230
|
+
if updated:
|
|
231
|
+
await self.get_commondata_ack_callback(common_svg_data)
|
|
232
|
+
|
|
233
|
+
case "toapp_all_hash_name":
|
|
234
|
+
hash_names: AppGetAllAreaHashName = nav_msg[1]
|
|
235
|
+
converted_list = [AreaHashNameList(name=item.name, hash=item.hash) for item in hash_names.hashnames]
|
|
236
|
+
self._device.map.area_name = converted_list
|
|
237
|
+
|
|
238
|
+
case "bidire_reqconver_path":
|
|
239
|
+
work_settings: NavReqCoverPath = nav_msg[1]
|
|
240
|
+
|
|
241
|
+
current_task = CurrentTaskSettings.from_dict(work_settings.to_dict(casing=betterproto2.Casing.SNAKE))
|
|
242
|
+
|
|
243
|
+
if current_task.path_hash == 0:
|
|
244
|
+
self._device.map.current_mow_path = {}
|
|
245
|
+
|
|
246
|
+
if work_settings.sub_cmd == 0:
|
|
247
|
+
await self.queue_command_callback(key="get_all_boundary_hash_list", sub_cmd=3)
|
|
248
|
+
|
|
249
|
+
self._device.work = current_task
|
|
250
|
+
|
|
251
|
+
case "nav_sys_param_cmd":
|
|
252
|
+
settings: NavSysParamMsg = nav_msg[1]
|
|
253
|
+
match settings.id:
|
|
254
|
+
case 3:
|
|
255
|
+
self._device.mower_state.rain_detection = bool(settings.context)
|
|
256
|
+
case 6:
|
|
257
|
+
self._device.mower_state.turning_mode = settings.context
|
|
258
|
+
case 7:
|
|
259
|
+
self._device.mower_state.traversal_mode = settings.context
|
|
260
|
+
case "todev_unable_time_set":
|
|
261
|
+
nav_non_work_time: NavUnableTimeSet = nav_msg[1]
|
|
262
|
+
self._device.non_work_hours.non_work_sub_cmd = nav_non_work_time.sub_cmd
|
|
263
|
+
self._device.non_work_hours.start_time = nav_non_work_time.unable_start_time
|
|
264
|
+
self._device.non_work_hours.end_time = nav_non_work_time.unable_end_time
|
|
265
|
+
|
|
266
|
+
def _update_sys_data(self, message) -> None:
|
|
267
|
+
"""Update system."""
|
|
268
|
+
sys_msg = betterproto2.which_one_of(message.sys, "SubSysMsg")
|
|
269
|
+
match sys_msg[0]:
|
|
270
|
+
case "system_update_buf":
|
|
271
|
+
self._device.buffer(sys_msg[1])
|
|
272
|
+
case "toapp_report_data":
|
|
273
|
+
self._device.update_report_data(sys_msg[1])
|
|
274
|
+
case "mow_to_app_info":
|
|
275
|
+
self._device.mow_info(sys_msg[1])
|
|
276
|
+
case "system_tard_state_tunnel":
|
|
277
|
+
self._device.run_state_update(sys_msg[1])
|
|
278
|
+
case "todev_time_ctrl_light":
|
|
279
|
+
ctrl_light: TimeCtrlLight = sys_msg[1]
|
|
280
|
+
side_led: SideLight = SideLight.from_dict(ctrl_light.to_dict(casing=betterproto2.Casing.SNAKE))
|
|
281
|
+
self._device.mower_state.side_led = side_led
|
|
282
|
+
case "device_product_type_info":
|
|
283
|
+
device_product_type: DeviceProductTypeInfoT = sys_msg[1]
|
|
284
|
+
if device_product_type.main_product_type != "" or device_product_type.sub_product_type != "":
|
|
285
|
+
self._device.mower_state.model_id = device_product_type.main_product_type
|
|
286
|
+
self._device.mower_state.sub_model_id = device_product_type.sub_product_type
|
|
287
|
+
case "toapp_dev_fw_info":
|
|
288
|
+
device_fw_info: DeviceFwInfo = sys_msg[1]
|
|
289
|
+
self._device.device_firmwares.device_version = device_fw_info.version
|
|
290
|
+
self._device.mower_state.swversion = device_fw_info.version
|
|
291
|
+
|
|
292
|
+
def _update_driver_data(self, message) -> None:
|
|
293
|
+
"""Update driver data."""
|
|
294
|
+
driver_msg = betterproto2.which_one_of(message.driver, "SubDrvMsg")
|
|
295
|
+
match driver_msg[0]:
|
|
296
|
+
case "current_cutter_mode":
|
|
297
|
+
cutter_work_mode: AppGetCutterWorkMode = driver_msg[1]
|
|
298
|
+
self._device.mower_state.cutter_mode = cutter_work_mode.current_cutter_mode
|
|
299
|
+
self._device.mower_state.cutter_rpm = cutter_work_mode.current_cutter_rpm
|
|
300
|
+
case "cutter_mode_ctrl_by_hand":
|
|
301
|
+
cutter_work_mode_set: AppSetCutterWorkMode = driver_msg[1]
|
|
302
|
+
self._device.mower_state.cutter_mode = cutter_work_mode_set.cutter_mode
|
|
303
|
+
|
|
304
|
+
def _update_net_data(self, message) -> None:
|
|
305
|
+
"""Update network data."""
|
|
306
|
+
net_msg = betterproto2.which_one_of(message.net, "NetSubType")
|
|
307
|
+
match net_msg[0]:
|
|
308
|
+
case "toapp_wifi_iot_status":
|
|
309
|
+
wifi_iot_status: WifiIotStatusReport = net_msg[1]
|
|
310
|
+
self._device.mower_state.product_key = wifi_iot_status.product_key
|
|
311
|
+
case "toapp_devinfo_resp":
|
|
312
|
+
toapp_devinfo_resp: DrvDevInfoResp = net_msg[1]
|
|
313
|
+
for resp in toapp_devinfo_resp.resp_ids:
|
|
314
|
+
if resp.res == DrvDevInfoResult.DRV_RESULT_SUC and resp.id == 1 and resp.type == 6:
|
|
315
|
+
self._device.mower_state.swversion = resp.info
|
|
316
|
+
self._device.device_firmwares.device_version = resp.info
|
|
317
|
+
case "toapp_networkinfo_rsp":
|
|
318
|
+
get_network_info_resp: GetNetworkInfoRsp = net_msg[1]
|
|
319
|
+
self._device.mower_state.wifi_mac = get_network_info_resp.wifi_mac
|
|
320
|
+
|
|
321
|
+
def _update_mul_data(self, message) -> None:
|
|
322
|
+
"""Media and video states."""
|
|
323
|
+
mul_msg = betterproto2.which_one_of(message.mul, "SubMul")
|
|
324
|
+
match mul_msg[0]:
|
|
325
|
+
case "get_lamp_rsp":
|
|
326
|
+
lamp_resp: Getlamprsp = mul_msg[1]
|
|
327
|
+
self._device.mower_state.lamp_info.lamp_bright = lamp_resp.lamp_bright
|
|
328
|
+
if lamp_resp.get_ids in (1126, 1127):
|
|
329
|
+
self._device.mower_state.lamp_info.lamp_bright = lamp_resp.lamp_bright
|
|
330
|
+
self._device.mower_state.lamp_info.manual_light = bool(lamp_resp.lamp_manual_ctrl.value) or bool(
|
|
331
|
+
lamp_resp.lamp_bright
|
|
332
|
+
)
|
|
333
|
+
if lamp_resp.get_ids == 1123:
|
|
334
|
+
self._device.mower_state.lamp_info.lamp_bright = lamp_resp.lamp_bright
|
|
335
|
+
self._device.mower_state.lamp_info.night_light = bool(lamp_resp.lamp_ctrl.value) or bool(
|
|
336
|
+
lamp_resp.lamp_bright
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
def _update_ota_data(self, message) -> None:
|
|
340
|
+
"""Update OTA data."""
|
|
341
|
+
|
|
342
|
+
def generate_geojson(self, rtk: LocationPoint, dock: Dock) -> Any:
|
|
343
|
+
"""Generate geojson from frames."""
|
|
344
|
+
coordinator_converter = CoordinateConverter(rtk.latitude, rtk.longitude)
|
|
345
|
+
RTK_real_loc = coordinator_converter.enu_to_lla(0, 0)
|
|
346
|
+
|
|
347
|
+
dock_location = coordinator_converter.enu_to_lla(dock.latitude, dock.longitude)
|
|
348
|
+
dock_rotation = coordinator_converter.get_transform_yaw_with_yaw(dock.rotation) + 180
|
|
349
|
+
|
|
350
|
+
self._device.map.generated_geojson = GeojsonGenerator.generate_geojson(
|
|
351
|
+
self._device.map,
|
|
352
|
+
Point(RTK_real_loc.latitude, RTK_real_loc.longitude),
|
|
353
|
+
Point(dock_location.latitude, dock_location.longitude),
|
|
354
|
+
int(dock_rotation),
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
return self._device.map.generated_geojson
|
|
358
|
+
|
|
359
|
+
def generate_mowing_geojson(self, rtk: LocationPoint) -> Any:
|
|
360
|
+
"""Generate geojson from frames."""
|
|
361
|
+
coordinator_converter = CoordinateConverter(rtk.latitude, rtk.longitude)
|
|
362
|
+
RTK_real_loc = coordinator_converter.enu_to_lla(0, 0)
|
|
363
|
+
|
|
364
|
+
self._device.map.generated_mow_path_geojson = GeojsonGenerator.generate_mow_path_geojson(
|
|
365
|
+
self._device.map,
|
|
366
|
+
Point(RTK_real_loc.latitude, RTK_real_loc.longitude),
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
return self._device.map.generated_mow_path_geojson
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
from base64 import b64decode
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Annotated, Any, Literal
|
|
4
|
+
|
|
5
|
+
from google.protobuf import json_format
|
|
6
|
+
from mashumaro.config import BaseConfig
|
|
7
|
+
from mashumaro.mixins.orjson import DataClassORJSONMixin
|
|
8
|
+
from mashumaro.types import Alias, SerializableType
|
|
9
|
+
|
|
10
|
+
from pymammotion.proto import luba_msg_pb2
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Base64EncodedProtobuf(SerializableType):
|
|
14
|
+
def __init__(self, proto: str) -> None:
|
|
15
|
+
self.proto = proto
|
|
16
|
+
|
|
17
|
+
def _serialize(self):
|
|
18
|
+
return self.proto
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def _deserialize(cls, value):
|
|
22
|
+
return cls(*value)
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def __get_validators__(cls):
|
|
26
|
+
yield cls.validate
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def validate(cls, v: str):
|
|
30
|
+
if not isinstance(v, str):
|
|
31
|
+
raise TypeError("string required")
|
|
32
|
+
binary = b64decode(v, validate=True)
|
|
33
|
+
data = luba_msg_pb2.LubaMsg()
|
|
34
|
+
data.ParseFromString(binary)
|
|
35
|
+
return json_format.MessageToDict(data)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class DeviceProtobufMsgEventValue(DataClassORJSONMixin):
|
|
40
|
+
content: str
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class DeviceWarningEventValue(DataClassORJSONMixin):
|
|
45
|
+
# TODO: enum for error codes
|
|
46
|
+
# (see resources/res/values-en-rUS/strings.xml in APK)
|
|
47
|
+
code: int
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class DeviceConfigurationRequestValue(DataClassORJSONMixin):
|
|
52
|
+
code: int
|
|
53
|
+
bizId: str
|
|
54
|
+
params: str
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class DeviceNotificationEventCode(DataClassORJSONMixin):
|
|
59
|
+
localTime: int
|
|
60
|
+
code: str
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class DeviceNotificationEventValue(DataClassORJSONMixin):
|
|
65
|
+
data: str # parsed to DeviceNotificationEventCode
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class DeviceBizReqEventValue(DataClassORJSONMixin):
|
|
70
|
+
bizType: str
|
|
71
|
+
bizId: str
|
|
72
|
+
params: str
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class GeneralParams(DataClassORJSONMixin):
|
|
77
|
+
group_id_list: Annotated[list[str], Alias("groupIdList")]
|
|
78
|
+
group_id: Annotated[str, Alias("groupId")]
|
|
79
|
+
category_key: Annotated[Literal["LawnMower", "Tracker"], Alias("categoryKey")]
|
|
80
|
+
batch_id: Annotated[str, Alias("batchId")]
|
|
81
|
+
gmt_create: Annotated[int, Alias("gmtCreate")]
|
|
82
|
+
product_key: Annotated[str, Alias("productKey")]
|
|
83
|
+
type: str
|
|
84
|
+
device_name: Annotated[str, Alias("deviceName")]
|
|
85
|
+
iot_id: Annotated[str, Alias("iotId")]
|
|
86
|
+
check_level: Annotated[int, Alias("checkLevel")]
|
|
87
|
+
namespace: str
|
|
88
|
+
tenant_id: Annotated[str, Alias("tenantId")]
|
|
89
|
+
name: str
|
|
90
|
+
thing_type: Annotated[Literal["DEVICE"], Alias("thingType")]
|
|
91
|
+
time: int
|
|
92
|
+
tenant_instance_id: Annotated[str, Alias("tenantInstanceId")]
|
|
93
|
+
value: Any
|
|
94
|
+
|
|
95
|
+
# Optional fields
|
|
96
|
+
identifier: str | None = None
|
|
97
|
+
check_failed_data: Annotated[dict | None, Alias("checkFailedData")] = None
|
|
98
|
+
_tenant_id: Annotated[str | None, Alias("_tenantId")] = None
|
|
99
|
+
generate_time: Annotated[int | None, Alias("generateTime")] = None
|
|
100
|
+
jmsx_delivery_count: Annotated[int | None, Alias("JMSXDeliveryCount")] = None
|
|
101
|
+
qos: int | None = None
|
|
102
|
+
request_id: Annotated[str | None, Alias("requestId")] = None
|
|
103
|
+
_category_key: Annotated[str | None, Alias("_categoryKey")] = None
|
|
104
|
+
device_type: Annotated[str | None, Alias("deviceType")] = None
|
|
105
|
+
_trace_id: Annotated[str | None, Alias("_traceId")] = None
|
|
106
|
+
|
|
107
|
+
class Config(BaseConfig):
|
|
108
|
+
allow_deserialization_not_by_alias = True
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@dataclass
|
|
112
|
+
class DeviceProtobufMsgEventParams(GeneralParams):
|
|
113
|
+
identifier: Literal["device_protobuf_msg_event"]
|
|
114
|
+
type: Literal["info"]
|
|
115
|
+
value: DeviceProtobufMsgEventValue
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@dataclass
|
|
119
|
+
class DeviceNotificationEventParams(GeneralParams):
|
|
120
|
+
"""Device notification event.
|
|
121
|
+
|
|
122
|
+
{'data': '{"localTime":1725159492000,"code":"1002"}'},
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
identifier: Literal["device_notification_event", "device_information_event", "device_warning_code_event"]
|
|
126
|
+
type: Literal["info"]
|
|
127
|
+
value: DeviceNotificationEventValue
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@dataclass
|
|
131
|
+
class DeviceBizReqEventParams(GeneralParams):
|
|
132
|
+
identifier: Literal["device_biz_req_event"]
|
|
133
|
+
type: Literal["info"]
|
|
134
|
+
value: DeviceBizReqEventValue
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@dataclass
|
|
138
|
+
class DeviceWarningEventParams(GeneralParams):
|
|
139
|
+
identifier: Literal["device_warning_event"]
|
|
140
|
+
type: Literal["alert"]
|
|
141
|
+
value: DeviceWarningEventValue
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@dataclass
|
|
145
|
+
class DeviceConfigurationRequestEvent(GeneralParams):
|
|
146
|
+
type: Literal["info"]
|
|
147
|
+
value: DeviceConfigurationRequestValue
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@dataclass
|
|
151
|
+
class DeviceLogProgressEventParams(GeneralParams):
|
|
152
|
+
identifier: Literal["device_log_progress_event"]
|
|
153
|
+
type: Literal["info"]
|
|
154
|
+
value: DeviceNotificationEventValue
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@dataclass
|
|
158
|
+
class ThingEventMessage(DataClassORJSONMixin):
|
|
159
|
+
method: Literal["thing.events", "thing.properties"]
|
|
160
|
+
id: str
|
|
161
|
+
params: (
|
|
162
|
+
DeviceProtobufMsgEventParams
|
|
163
|
+
| DeviceWarningEventParams
|
|
164
|
+
| DeviceNotificationEventParams
|
|
165
|
+
| DeviceLogProgressEventParams
|
|
166
|
+
| DeviceBizReqEventParams
|
|
167
|
+
| DeviceConfigurationRequestEvent
|
|
168
|
+
| dict
|
|
169
|
+
)
|
|
170
|
+
version: Literal["1.0"]
|
|
171
|
+
|
|
172
|
+
@classmethod
|
|
173
|
+
def from_dicts(cls, payload: dict) -> "ThingEventMessage":
|
|
174
|
+
"""Deserialize payload JSON ThingEventMessage."""
|
|
175
|
+
method = payload.get("method")
|
|
176
|
+
event_id = payload.get("id")
|
|
177
|
+
params_dict = payload.get("params", {})
|
|
178
|
+
version = payload.get("version")
|
|
179
|
+
|
|
180
|
+
identifier = params_dict.get("identifier")
|
|
181
|
+
if identifier is None:
|
|
182
|
+
"""Request configuration event."""
|
|
183
|
+
params_obj = DeviceConfigurationRequestEvent.from_dict(params_dict)
|
|
184
|
+
elif identifier == "device_protobuf_msg_event":
|
|
185
|
+
params_obj = DeviceProtobufMsgEventParams.from_dict(params_dict)
|
|
186
|
+
elif identifier == "device_warning_event":
|
|
187
|
+
params_obj = DeviceWarningEventParams.from_dict(params_dict)
|
|
188
|
+
elif identifier == "device_biz_req_event":
|
|
189
|
+
params_obj = DeviceBizReqEventParams.from_dict(params_dict)
|
|
190
|
+
elif identifier == "device_log_progress_event":
|
|
191
|
+
params_obj = DeviceLogProgressEventParams.from_dict(params_dict)
|
|
192
|
+
elif identifier == "device_config_req_event":
|
|
193
|
+
params_obj = payload.get("params", {})
|
|
194
|
+
elif (
|
|
195
|
+
identifier == "device_notification_event"
|
|
196
|
+
or identifier == "device_warning_code_event"
|
|
197
|
+
or identifier == "device_information_event"
|
|
198
|
+
):
|
|
199
|
+
params_obj = DeviceNotificationEventParams.from_dict(params_dict)
|
|
200
|
+
else:
|
|
201
|
+
raise ValueError(f"Unknown identifier: {identifier} {params_dict}")
|
|
202
|
+
|
|
203
|
+
return cls(method=method, id=event_id, params=params_obj, version=version)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@dataclass
|
|
207
|
+
class MammotionProtoMsgParams(DataClassORJSONMixin, SerializableType):
|
|
208
|
+
value: DeviceProtobufMsgEventValue
|
|
209
|
+
iot_id: str = ""
|
|
210
|
+
product_key: str = ""
|
|
211
|
+
device_name: str = ""
|
|
212
|
+
|
|
213
|
+
@classmethod
|
|
214
|
+
def _deserialize(cls, d: dict[str, Any]) -> "MammotionProtoMsgParams":
|
|
215
|
+
"""Override from_dict to allow dict manipulation before conversion."""
|
|
216
|
+
proto: str = d["content"]
|
|
217
|
+
|
|
218
|
+
return cls(value=DeviceProtobufMsgEventValue(content=proto))
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@dataclass
|
|
222
|
+
class MammotionEventMessage(DataClassORJSONMixin):
|
|
223
|
+
id: str
|
|
224
|
+
version: str
|
|
225
|
+
sys: dict
|
|
226
|
+
params: MammotionProtoMsgParams
|
|
227
|
+
method: str
|