pymammotion 0.2.29__py3-none-any.whl → 0.2.30__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 (49) hide show
  1. pymammotion/__init__.py +11 -8
  2. pymammotion/aliyun/cloud_gateway.py +26 -24
  3. pymammotion/aliyun/cloud_service.py +3 -3
  4. pymammotion/aliyun/dataclass/dev_by_account_response.py +1 -1
  5. pymammotion/bluetooth/ble.py +5 -5
  6. pymammotion/bluetooth/ble_message.py +30 -16
  7. pymammotion/bluetooth/data/convert.py +1 -1
  8. pymammotion/bluetooth/data/framectrldata.py +1 -1
  9. pymammotion/bluetooth/data/notifydata.py +6 -6
  10. pymammotion/const.py +1 -0
  11. pymammotion/data/model/__init__.py +2 -0
  12. pymammotion/data/model/device.py +22 -20
  13. pymammotion/data/model/device_config.py +1 -1
  14. pymammotion/data/model/enums.py +4 -4
  15. pymammotion/data/model/excute_boarder_params.py +5 -5
  16. pymammotion/data/model/execute_boarder.py +4 -4
  17. pymammotion/data/model/hash_list.py +1 -1
  18. pymammotion/data/model/location.py +2 -2
  19. pymammotion/data/model/plan.py +4 -4
  20. pymammotion/data/model/region_data.py +4 -4
  21. pymammotion/data/model/report_info.py +1 -1
  22. pymammotion/data/mqtt/event.py +1 -1
  23. pymammotion/data/state_manager.py +9 -9
  24. pymammotion/event/event.py +14 -14
  25. pymammotion/http/http.py +29 -51
  26. pymammotion/http/model/http.py +75 -0
  27. pymammotion/mammotion/commands/messages/driver.py +20 -23
  28. pymammotion/mammotion/commands/messages/navigation.py +47 -48
  29. pymammotion/mammotion/commands/messages/network.py +17 -35
  30. pymammotion/mammotion/commands/messages/system.py +6 -7
  31. pymammotion/mammotion/control/joystick.py +10 -10
  32. pymammotion/mammotion/devices/__init__.py +2 -2
  33. pymammotion/mammotion/devices/base.py +248 -0
  34. pymammotion/mammotion/devices/mammotion.py +23 -1005
  35. pymammotion/mammotion/devices/mammotion_bluetooth.py +447 -0
  36. pymammotion/mammotion/devices/mammotion_cloud.py +244 -0
  37. pymammotion/mqtt/mammotion_future.py +2 -2
  38. pymammotion/mqtt/mammotion_mqtt.py +17 -14
  39. pymammotion/proto/__init__.py +6 -0
  40. pymammotion/utility/constant/__init__.py +3 -1
  41. pymammotion/utility/datatype_converter.py +9 -9
  42. pymammotion/utility/device_type.py +13 -13
  43. pymammotion/utility/map.py +2 -2
  44. pymammotion/utility/periodic.py +5 -5
  45. pymammotion/utility/rocker_util.py +1 -1
  46. {pymammotion-0.2.29.dist-info → pymammotion-0.2.30.dist-info}/METADATA +3 -1
  47. {pymammotion-0.2.29.dist-info → pymammotion-0.2.30.dist-info}/RECORD +49 -45
  48. {pymammotion-0.2.29.dist-info → pymammotion-0.2.30.dist-info}/LICENSE +0 -0
  49. {pymammotion-0.2.29.dist-info → pymammotion-0.2.30.dist-info}/WHEEL +0 -0
@@ -0,0 +1,248 @@
1
+ import asyncio
2
+ import logging
3
+ from abc import abstractmethod
4
+ from typing import Any, Awaitable, Callable
5
+
6
+ import betterproto
7
+
8
+ from pymammotion.aliyun.dataclass.connect_response import Device
9
+ from pymammotion.data.model import RegionData
10
+ from pymammotion.data.model.device import MowingDevice
11
+ from pymammotion.data.state_manager import StateManager
12
+ from pymammotion.proto import has_field
13
+ from pymammotion.proto.luba_msg import LubaMsg
14
+ from pymammotion.proto.mctrl_nav import NavGetCommDataAck, NavGetHashListAck
15
+ from pymammotion.utility.movement import get_percent, transform_both_speeds
16
+
17
+ _LOGGER = logging.getLogger(__name__)
18
+
19
+
20
+ def find_next_integer(lst: list[int], current_hash: float) -> int | None:
21
+ try:
22
+ # Find the index of the current integer
23
+ current_index = lst.index(current_hash)
24
+
25
+ # Check if there is a next integer in the list
26
+ if current_index + 1 < len(lst):
27
+ return lst[current_index + 1]
28
+ else:
29
+ return None # Or raise an exception or handle it in some other way
30
+ except ValueError:
31
+ # Handle the case where current_int is not in the list
32
+ return None # Or raise an exception or handle it in some other way
33
+
34
+
35
+ class MammotionBaseDevice:
36
+ """Base class for Mammotion devices."""
37
+
38
+ _mower: MowingDevice
39
+ _state_manager: StateManager
40
+ _cloud_device: Device | None = None
41
+
42
+ def __init__(self, device: MowingDevice, cloud_device: Device | None = None) -> None:
43
+ """Initialize MammotionBaseDevice."""
44
+ self.loop = asyncio.get_event_loop()
45
+ self._raw_data = LubaMsg().to_dict(casing=betterproto.Casing.SNAKE)
46
+ self._mower = device
47
+ self._state_manager = StateManager(self._mower)
48
+ self._state_manager.gethash_ack_callback = self.datahash_response
49
+ self._state_manager.get_commondata_ack_callback = self.commdata_response
50
+ self._notify_future: asyncio.Future[bytes] | None = None
51
+ self._cloud_device = cloud_device
52
+
53
+ def set_notification_callback(self, func: Callable[[], Awaitable[None]]) -> None:
54
+ self._state_manager.on_notification_callback = func
55
+
56
+ async def datahash_response(self, hash_ack: NavGetHashListAck) -> None:
57
+ """Handle datahash responses."""
58
+ await self.queue_command("synchronize_hash_data", hash_num=hash_ack.data_couple[0])
59
+
60
+ async def commdata_response(self, common_data: NavGetCommDataAck) -> None:
61
+ """Handle common data responses."""
62
+ total_frame = common_data.total_frame
63
+ current_frame = common_data.current_frame
64
+
65
+ missing_frames = self._mower.map.missing_frame(common_data)
66
+ if len(missing_frames) == 0:
67
+ # get next in hash ack list
68
+
69
+ data_hash = find_next_integer(self._mower.nav.toapp_gethash_ack.data_couple, common_data.hash)
70
+ if data_hash is None:
71
+ return
72
+
73
+ await self.queue_command("synchronize_hash_data", hash_num=data_hash)
74
+ else:
75
+ if current_frame != missing_frames[0] - 1:
76
+ current_frame = missing_frames[0] - 1
77
+
78
+ region_data = RegionData()
79
+ region_data.hash = common_data.hash
80
+ region_data.action = common_data.action
81
+ region_data.type = common_data.type
82
+ region_data.total_frame = total_frame
83
+ region_data.current_frame = current_frame
84
+ await self.queue_command("get_regional_data", regional_data=region_data)
85
+
86
+ def _update_raw_data(self, data: bytes) -> None:
87
+ """Update raw and model data from notifications."""
88
+ tmp_msg = LubaMsg().parse(data)
89
+ res = betterproto.which_one_of(tmp_msg, "LubaSubMsg")
90
+ match res[0]:
91
+ case "nav":
92
+ self._update_nav_data(tmp_msg)
93
+ case "sys":
94
+ self._update_sys_data(tmp_msg)
95
+ case "driver":
96
+ self._update_driver_data(tmp_msg)
97
+ case "net":
98
+ self._update_net_data(tmp_msg)
99
+ case "mul":
100
+ self._update_mul_data(tmp_msg)
101
+ case "ota":
102
+ self._update_ota_data(tmp_msg)
103
+
104
+ self._mower.update_raw(self._raw_data)
105
+
106
+ def _update_nav_data(self, tmp_msg) -> None:
107
+ """Update navigation data."""
108
+ nav_sub_msg = betterproto.which_one_of(tmp_msg.nav, "SubNavMsg")
109
+ if nav_sub_msg[1] is None:
110
+ _LOGGER.debug("Sub message was NoneType %s", nav_sub_msg[0])
111
+ return
112
+ nav = self._raw_data.get("nav", {})
113
+ if isinstance(nav_sub_msg[1], int):
114
+ nav[nav_sub_msg[0]] = nav_sub_msg[1]
115
+ else:
116
+ nav[nav_sub_msg[0]] = nav_sub_msg[1].to_dict(casing=betterproto.Casing.SNAKE)
117
+ self._raw_data["nav"] = nav
118
+
119
+ def _update_sys_data(self, tmp_msg) -> None:
120
+ """Update system data."""
121
+ sys_sub_msg = betterproto.which_one_of(tmp_msg.sys, "SubSysMsg")
122
+ if sys_sub_msg[1] is None:
123
+ _LOGGER.debug("Sub message was NoneType %s", sys_sub_msg[0])
124
+ return
125
+ sys = self._raw_data.get("sys", {})
126
+ sys[sys_sub_msg[0]] = sys_sub_msg[1].to_dict(casing=betterproto.Casing.SNAKE)
127
+ self._raw_data["sys"] = sys
128
+
129
+ def _update_driver_data(self, tmp_msg) -> None:
130
+ """Update driver data."""
131
+ drv_sub_msg = betterproto.which_one_of(tmp_msg.driver, "SubDrvMsg")
132
+ if drv_sub_msg[1] is None:
133
+ _LOGGER.debug("Sub message was NoneType %s", drv_sub_msg[0])
134
+ return
135
+ drv = self._raw_data.get("driver", {})
136
+ drv[drv_sub_msg[0]] = drv_sub_msg[1].to_dict(casing=betterproto.Casing.SNAKE)
137
+ self._raw_data["driver"] = drv
138
+
139
+ def _update_net_data(self, tmp_msg) -> None:
140
+ """Update network data."""
141
+ net_sub_msg = betterproto.which_one_of(tmp_msg.net, "NetSubType")
142
+ if net_sub_msg[1] is None:
143
+ _LOGGER.debug("Sub message was NoneType %s", net_sub_msg[0])
144
+ return
145
+ net = self._raw_data.get("net", {})
146
+ if isinstance(net_sub_msg[1], int):
147
+ net[net_sub_msg[0]] = net_sub_msg[1]
148
+ else:
149
+ net[net_sub_msg[0]] = net_sub_msg[1].to_dict(casing=betterproto.Casing.SNAKE)
150
+ self._raw_data["net"] = net
151
+
152
+ def _update_mul_data(self, tmp_msg) -> None:
153
+ """Update mul data."""
154
+ mul_sub_msg = betterproto.which_one_of(tmp_msg.mul, "SubMul")
155
+ if mul_sub_msg[1] is None:
156
+ _LOGGER.debug("Sub message was NoneType %s", mul_sub_msg[0])
157
+ return
158
+ mul = self._raw_data.get("mul", {})
159
+ mul[mul_sub_msg[0]] = mul_sub_msg[1].to_dict(casing=betterproto.Casing.SNAKE)
160
+ self._raw_data["mul"] = mul
161
+
162
+ def _update_ota_data(self, tmp_msg) -> None:
163
+ """Update OTA data."""
164
+ ota_sub_msg = betterproto.which_one_of(tmp_msg.ota, "SubOtaMsg")
165
+ if ota_sub_msg[1] is None:
166
+ _LOGGER.debug("Sub message was NoneType %s", ota_sub_msg[0])
167
+ return
168
+ ota = self._raw_data.get("ota", {})
169
+ ota[ota_sub_msg[0]] = ota_sub_msg[1].to_dict(casing=betterproto.Casing.SNAKE)
170
+ self._raw_data["ota"] = ota
171
+
172
+ @property
173
+ def raw_data(self) -> dict[str, Any]:
174
+ """Get the raw data of the device."""
175
+ return self._raw_data
176
+
177
+ @property
178
+ def mower(self) -> MowingDevice:
179
+ """Get the LubaMsg of the device."""
180
+ return self._mower
181
+
182
+ @abstractmethod
183
+ async def queue_command(self, key: str, **kwargs: any) -> bytes | None:
184
+ """Queue commands to mower."""
185
+
186
+ @abstractmethod
187
+ async def _ble_sync(self):
188
+ """Send ble sync command every 3 seconds or sooner."""
189
+
190
+ async def start_sync(self, retry: int) -> None:
191
+ """Start synchronization with the device."""
192
+ await self.queue_command("get_device_base_info")
193
+ await self.queue_command("get_device_product_model")
194
+ await self.queue_command("get_report_cfg")
195
+ """RTK and dock location."""
196
+ await self.queue_command("allpowerfull_rw", id=5, rw=1, context=1)
197
+
198
+ async def start_map_sync(self) -> None:
199
+ """Start sync of map data."""
200
+ await self.queue_command("read_plan", sub_cmd=2, plan_index=0)
201
+
202
+ await self.queue_command("get_all_boundary_hash_list", sub_cmd=0)
203
+
204
+ await self.queue_command("get_hash_response", total_frame=1, current_frame=1)
205
+
206
+ # work out why this crashes sometimes for better proto
207
+ if self._cloud_device:
208
+ await self.queue_command("get_area_name_list", device_id=self._cloud_device.deviceName)
209
+ if has_field(self._mower.net.toapp_wifi_iot_status):
210
+ await self.queue_command("get_area_name_list", device_id=self._mower.net.toapp_wifi_iot_status.devicename)
211
+
212
+ # sub_cmd 3 is job hashes??
213
+ # sub_cmd 4 is dump location (yuka)
214
+ # jobs list
215
+ # hash_list_result = await self._send_command_with_args("get_all_boundary_hash_list", sub_cmd=3)
216
+
217
+ async def async_get_errors(self) -> None:
218
+ """Error codes."""
219
+ await self.queue_command("allpowerfull_rw", id=5, rw=1, context=2)
220
+ await self.queue_command("allpowerfull_rw", id=5, rw=1, context=3)
221
+
222
+ async def move_forward(self, linear: float) -> None:
223
+ """Move forward. values 0.0 1.0."""
224
+ linear_percent = get_percent(abs(linear * 100))
225
+ (linear_speed, angular_speed) = transform_both_speeds(90.0, 0.0, linear_percent, 0.0)
226
+ await self.queue_command("send_movement", linear_speed=linear_speed, angular_speed=angular_speed)
227
+
228
+ async def move_back(self, linear: float) -> None:
229
+ """Move back. values 0.0 1.0."""
230
+ linear_percent = get_percent(abs(linear * 100))
231
+ (linear_speed, angular_speed) = transform_both_speeds(270.0, 0.0, linear_percent, 0.0)
232
+ await self.queue_command("send_movement", linear_speed=linear_speed, angular_speed=angular_speed)
233
+
234
+ async def move_left(self, angulur: float) -> None:
235
+ """Move forward. values 0.0 1.0."""
236
+ angular_percent = get_percent(abs(angulur * 100))
237
+ (linear_speed, angular_speed) = transform_both_speeds(0.0, 0.0, 0.0, angular_percent)
238
+ await self.queue_command("send_movement", linear_speed=linear_speed, angular_speed=angular_speed)
239
+
240
+ async def move_right(self, angulur: float) -> None:
241
+ """Move back. values 0.0 1.0."""
242
+ angular_percent = get_percent(abs(angulur * 100))
243
+ (linear_speed, angular_speed) = transform_both_speeds(0.0, 180.0, 0.0, angular_percent)
244
+ await self.queue_command("send_movement", linear_speed=linear_speed, angular_speed=angular_speed)
245
+
246
+ async def command(self, key: str, **kwargs):
247
+ """Send a command to the device."""
248
+ return await self.queue_command(key, **kwargs)