pymammotion 0.4.0a2__py3-none-any.whl → 0.5.51__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pymammotion might be problematic. Click here for more details.

Files changed (133) hide show
  1. pymammotion/__init__.py +5 -4
  2. pymammotion/aliyun/client.py +235 -0
  3. pymammotion/aliyun/cloud_gateway.py +312 -64
  4. pymammotion/aliyun/model/aep_response.py +1 -2
  5. pymammotion/aliyun/model/dev_by_account_response.py +170 -23
  6. pymammotion/aliyun/model/login_by_oauth_response.py +2 -3
  7. pymammotion/aliyun/model/regions_response.py +3 -3
  8. pymammotion/aliyun/model/session_by_authcode_response.py +2 -2
  9. pymammotion/aliyun/model/thing_response.py +12 -0
  10. pymammotion/aliyun/regions.py +62 -0
  11. pymammotion/aliyun/tea/core.py +297 -0
  12. pymammotion/bluetooth/ble.py +7 -9
  13. pymammotion/bluetooth/ble_message.py +10 -14
  14. pymammotion/const.py +3 -0
  15. pymammotion/data/model/__init__.py +1 -2
  16. pymammotion/data/model/device.py +95 -27
  17. pymammotion/data/model/device_config.py +4 -4
  18. pymammotion/data/model/device_info.py +35 -0
  19. pymammotion/data/model/device_limits.py +10 -10
  20. pymammotion/data/model/enums.py +12 -2
  21. pymammotion/data/model/errors.py +12 -0
  22. pymammotion/data/model/events.py +14 -0
  23. pymammotion/data/model/generate_geojson.py +521 -0
  24. pymammotion/data/model/generate_route_information.py +2 -2
  25. pymammotion/data/model/hash_list.py +370 -57
  26. pymammotion/data/model/location.py +4 -4
  27. pymammotion/data/model/mowing_modes.py +17 -1
  28. pymammotion/data/model/raw_data.py +2 -10
  29. pymammotion/data/model/region_data.py +10 -11
  30. pymammotion/data/model/report_info.py +31 -5
  31. pymammotion/data/model/work.py +27 -0
  32. pymammotion/data/mower_state_manager.py +316 -0
  33. pymammotion/data/mqtt/event.py +73 -28
  34. pymammotion/data/mqtt/mammotion_properties.py +257 -0
  35. pymammotion/data/mqtt/properties.py +93 -78
  36. pymammotion/data/mqtt/status.py +18 -17
  37. pymammotion/event/event.py +27 -6
  38. pymammotion/homeassistant/__init__.py +3 -0
  39. pymammotion/homeassistant/mower_api.py +484 -0
  40. pymammotion/homeassistant/rtk_api.py +54 -0
  41. pymammotion/http/encryption.py +5 -6
  42. pymammotion/http/http.py +574 -28
  43. pymammotion/http/model/__init__.py +0 -0
  44. pymammotion/{aliyun/model/stream_subscription_response.py → http/model/camera_stream.py} +14 -2
  45. pymammotion/http/model/http.py +129 -4
  46. pymammotion/http/model/response_factory.py +61 -0
  47. pymammotion/http/model/rtk.py +16 -0
  48. pymammotion/mammotion/commands/abstract_message.py +7 -5
  49. pymammotion/mammotion/commands/mammotion_command.py +30 -1
  50. pymammotion/mammotion/commands/messages/basestation.py +43 -0
  51. pymammotion/mammotion/commands/messages/driver.py +61 -29
  52. pymammotion/mammotion/commands/messages/media.py +68 -15
  53. pymammotion/mammotion/commands/messages/navigation.py +61 -25
  54. pymammotion/mammotion/commands/messages/network.py +17 -23
  55. pymammotion/mammotion/commands/messages/ota.py +18 -18
  56. pymammotion/mammotion/commands/messages/system.py +32 -49
  57. pymammotion/mammotion/commands/messages/video.py +15 -16
  58. pymammotion/mammotion/devices/__init__.py +27 -3
  59. pymammotion/mammotion/devices/base.py +40 -131
  60. pymammotion/mammotion/devices/mammotion.py +436 -201
  61. pymammotion/mammotion/devices/mammotion_bluetooth.py +57 -47
  62. pymammotion/mammotion/devices/mammotion_cloud.py +134 -105
  63. pymammotion/mammotion/devices/mammotion_mower_ble.py +49 -0
  64. pymammotion/mammotion/devices/mammotion_mower_cloud.py +39 -0
  65. pymammotion/mammotion/devices/managers/managers.py +81 -0
  66. pymammotion/mammotion/devices/mower_device.py +124 -0
  67. pymammotion/mammotion/devices/mower_manager.py +107 -0
  68. pymammotion/mammotion/devices/rtk_ble.py +89 -0
  69. pymammotion/mammotion/devices/rtk_cloud.py +113 -0
  70. pymammotion/mammotion/devices/rtk_device.py +50 -0
  71. pymammotion/mammotion/devices/rtk_manager.py +122 -0
  72. pymammotion/mqtt/__init__.py +2 -1
  73. pymammotion/mqtt/aliyun_mqtt.py +232 -0
  74. pymammotion/mqtt/linkkit/__init__.py +5 -0
  75. pymammotion/mqtt/linkkit/h2client.py +585 -0
  76. pymammotion/mqtt/linkkit/linkkit.py +3023 -0
  77. pymammotion/mqtt/mammotion_mqtt.py +176 -169
  78. pymammotion/mqtt/mqtt_models.py +66 -0
  79. pymammotion/proto/__init__.py +4839 -4
  80. pymammotion/proto/basestation.proto +8 -0
  81. pymammotion/proto/basestation_pb2.py +11 -9
  82. pymammotion/proto/basestation_pb2.pyi +16 -2
  83. pymammotion/proto/dev_net.proto +79 -55
  84. pymammotion/proto/dev_net_pb2.py +60 -56
  85. pymammotion/proto/dev_net_pb2.pyi +49 -6
  86. pymammotion/proto/luba_msg.proto +2 -1
  87. pymammotion/proto/luba_msg_pb2.py +6 -6
  88. pymammotion/proto/luba_msg_pb2.pyi +1 -0
  89. pymammotion/proto/luba_mul.proto +62 -1
  90. pymammotion/proto/luba_mul_pb2.py +38 -22
  91. pymammotion/proto/luba_mul_pb2.pyi +94 -7
  92. pymammotion/proto/mctrl_driver.proto +44 -4
  93. pymammotion/proto/mctrl_driver_pb2.py +26 -14
  94. pymammotion/proto/mctrl_driver_pb2.pyi +66 -11
  95. pymammotion/proto/mctrl_nav.proto +93 -52
  96. pymammotion/proto/mctrl_nav_pb2.py +75 -67
  97. pymammotion/proto/mctrl_nav_pb2.pyi +142 -56
  98. pymammotion/proto/mctrl_ota.proto +40 -2
  99. pymammotion/proto/mctrl_ota_pb2.py +23 -13
  100. pymammotion/proto/mctrl_ota_pb2.pyi +67 -4
  101. pymammotion/proto/mctrl_pept.proto +8 -3
  102. pymammotion/proto/mctrl_pept_pb2.py +8 -6
  103. pymammotion/proto/mctrl_pept_pb2.pyi +14 -6
  104. pymammotion/proto/mctrl_sys.proto +325 -86
  105. pymammotion/proto/mctrl_sys_pb2.py +162 -98
  106. pymammotion/proto/mctrl_sys_pb2.pyi +451 -25
  107. pymammotion/proto/message_pool.py +3 -0
  108. pymammotion/proto/py.typed +0 -0
  109. pymammotion/utility/constant/device_constant.py +29 -5
  110. pymammotion/utility/datatype_converter.py +13 -12
  111. pymammotion/utility/device_config.py +522 -130
  112. pymammotion/utility/device_type.py +218 -21
  113. pymammotion/utility/map.py +238 -51
  114. pymammotion/utility/mur_mur_hash.py +159 -0
  115. {pymammotion-0.4.0a2.dist-info → pymammotion-0.5.51.dist-info}/METADATA +26 -31
  116. pymammotion-0.5.51.dist-info/RECORD +152 -0
  117. {pymammotion-0.4.0a2.dist-info → pymammotion-0.5.51.dist-info}/WHEEL +1 -1
  118. pymammotion/aliyun/cloud_service.py +0 -65
  119. pymammotion/data/model/plan.py +0 -58
  120. pymammotion/data/state_manager.py +0 -129
  121. pymammotion/proto/basestation.py +0 -59
  122. pymammotion/proto/common.py +0 -12
  123. pymammotion/proto/dev_net.py +0 -381
  124. pymammotion/proto/luba_msg.py +0 -81
  125. pymammotion/proto/luba_mul.py +0 -76
  126. pymammotion/proto/mctrl_driver.py +0 -100
  127. pymammotion/proto/mctrl_nav.py +0 -664
  128. pymammotion/proto/mctrl_ota.py +0 -48
  129. pymammotion/proto/mctrl_pept.py +0 -41
  130. pymammotion/proto/mctrl_sys.py +0 -574
  131. pymammotion-0.4.0a2.dist-info/RECORD +0 -131
  132. /pymammotion/http/{_init_.py → __init__.py} +0 -0
  133. {pymammotion-0.4.0a2.dist-info → pymammotion-0.5.51.dist-info/licenses}/LICENSE +0 -0
@@ -0,0 +1,484 @@
1
+ """Thin api layer between home assistant and pymammotion."""
2
+
3
+ import asyncio
4
+ from datetime import datetime, timedelta
5
+ from logging import getLogger
6
+ from typing import Any
7
+
8
+ from pymammotion.aliyun.cloud_gateway import (
9
+ EXPIRED_CREDENTIAL_EXCEPTIONS,
10
+ DeviceOfflineException,
11
+ FailedRequestException,
12
+ GatewayTimeoutException,
13
+ NoConnectionException,
14
+ )
15
+ from pymammotion.data.model import GenerateRouteInformation
16
+ from pymammotion.data.model.device import MowingDevice
17
+ from pymammotion.data.model.device_config import OperationSettings, create_path_order
18
+ from pymammotion.mammotion.devices import MammotionMowerDeviceManager
19
+ from pymammotion.mammotion.devices.mammotion import Mammotion
20
+ from pymammotion.proto import RptAct, RptInfoType
21
+ from pymammotion.utility.device_type import DeviceType
22
+
23
+ logger = getLogger(__name__)
24
+
25
+
26
+ class HomeAssistantMowerApi:
27
+ """API for interacting with Mammotion Mowers for Home Assistant."""
28
+
29
+ def __init__(self) -> None:
30
+ self._plan_lock = asyncio.Lock()
31
+ self.update_failures = 0
32
+ self._mammotion = Mammotion()
33
+ self._map_lock = asyncio.Lock()
34
+ self._last_call_times: dict[str, datetime] = {}
35
+ self._call_intervals = {
36
+ "check_maps": timedelta(minutes=5),
37
+ "read_plan": timedelta(minutes=30),
38
+ "read_settings": timedelta(minutes=5),
39
+ "get_errors": timedelta(minutes=1),
40
+ "get_report_cfg": timedelta(seconds=5),
41
+ "get_maintenance": timedelta(minutes=30),
42
+ "device_version_upgrade": timedelta(hours=5),
43
+ }
44
+
45
+ @property
46
+ def mammotion(self) -> Mammotion:
47
+ return self._mammotion
48
+
49
+ def _should_call_api(self, api_name: str, device: MowingDevice | None = None) -> bool:
50
+ """Check if API should be called based on time or criteria."""
51
+ # Time-based check
52
+ if api_name not in self._last_call_times:
53
+ return True
54
+
55
+ last_call = self._last_call_times[api_name]
56
+ interval = self._call_intervals.get(api_name, timedelta(seconds=10))
57
+
58
+ # Criteria-based checks
59
+ if api_name == "check_maps" and device:
60
+ # Call immediately if map data is incomplete
61
+ if len(device.map.area) == 0 or device.map.missing_hashlist():
62
+ return True
63
+
64
+ return datetime.now() - last_call >= interval
65
+
66
+ def _mark_api_called(self, api_name: str) -> None:
67
+ """Mark an API as called with the current timestamp."""
68
+ self._last_call_times[api_name] = datetime.now()
69
+
70
+ async def update(self, device_name: str) -> MowingDevice:
71
+ device = self._mammotion.get_device_by_name(device_name)
72
+
73
+ if device.has_queued_commands():
74
+ return device.state
75
+
76
+ if self._map_lock.locked():
77
+ # if maps is not complete kick off the map sync process again
78
+ if len(device.state.map.missing_hashlist()) > 0:
79
+ await self._mammotion.start_map_sync(device_name)
80
+ return device.state
81
+ # if maps complete
82
+ else:
83
+ self._map_lock.release()
84
+
85
+ # Check maps periodically
86
+ if self._should_call_api("check_maps") and not self._map_lock.locked():
87
+ await self._map_lock.acquire()
88
+ await self.mammotion.start_map_sync(device_name)
89
+ self._mark_api_called("check_maps")
90
+ return device.state
91
+
92
+ if self._should_call_api("read_plan"):
93
+ if len(device.state.map.plan) == 0 or list(device.state.map.plan.values())[0].total_plan_num != len(
94
+ device.state.map.plan
95
+ ):
96
+ await self.async_send_command(device_name, "read_plan", sub_cmd=2, plan_index=0)
97
+ self._mark_api_called("read_plan")
98
+ return device.state
99
+
100
+ # Read settings less frequently
101
+ if self._should_call_api("read_settings"):
102
+ # await device.async_read_settings()
103
+ self._mark_api_called("read_settings")
104
+
105
+ # Check for errors periodically
106
+ if self._should_call_api("get_errors"):
107
+ await self.async_send_command(device_name, "get_error_code")
108
+ await self.async_send_command(device_name, "get_error_timestamp")
109
+ self._mark_api_called("get_errors")
110
+
111
+ if self._should_call_api("get_report_cfg"):
112
+ await self.async_send_command(device_name, "get_report_cfg")
113
+ self._mark_api_called("get_report_cfg")
114
+
115
+ if self._should_call_api("get_maintenance"):
116
+ await self.async_send_command(device_name, "get_maintenance")
117
+ self._mark_api_called("get_maintenance")
118
+
119
+ if self._should_call_api("device_version_upgrade"):
120
+ self._mark_api_called("device_version_upgrade")
121
+
122
+ return device.state
123
+
124
+ async def async_send_command(self, device_name: str, command: str, **kwargs: Any) -> bool | None:
125
+ """Send command."""
126
+ device = self._mammotion.get_device_by_name(device_name)
127
+
128
+ try:
129
+ # TODO check preference
130
+ if device.cloud:
131
+ return await self.async_send_cloud_command(device, command, **kwargs)
132
+ elif device.ble:
133
+ return await self.async_send_bluetooth_command(device, command, **kwargs)
134
+ except (DeviceOfflineException, NoConnectionException) as ex:
135
+ """Device is offline try bluetooth if we have it."""
136
+ logger.error(f"Device offline: {ex.iot_id}")
137
+ if ble := device.ble:
138
+ # if we don't do this, it will stay connected and no longer update over Wi-Fi
139
+ ble.set_disconnect_strategy(disconnect=True)
140
+ await ble.queue_command(command, **kwargs)
141
+
142
+ return True
143
+ raise DeviceOfflineException(ex.args[0], device.iot_id)
144
+ return False
145
+
146
+ async def async_send_cloud_command(
147
+ self, device: MammotionMowerDeviceManager, key: str, **kwargs: Any
148
+ ) -> bool | None:
149
+ """Send command."""
150
+ if cloud := device.cloud:
151
+ if not device.state.online:
152
+ return False
153
+
154
+ try:
155
+ await cloud.command(key, **kwargs)
156
+ self.update_failures = 0
157
+ return True
158
+ except FailedRequestException:
159
+ self.update_failures += 1
160
+ if self.update_failures < 5:
161
+ await cloud.command(key, **kwargs)
162
+ return True
163
+ return False
164
+ except EXPIRED_CREDENTIAL_EXCEPTIONS:
165
+ self.update_failures += 1
166
+ await self._mammotion.refresh_login(device.mammotion_http.account)
167
+ # TODO tell home assistant the credentials have changed
168
+ if self.update_failures < 5:
169
+ await cloud.command(key, **kwargs)
170
+ return True
171
+ return False
172
+ except GatewayTimeoutException as ex:
173
+ logger.error(f"Gateway timeout exception: {ex.iot_id}")
174
+ self.update_failures = 0
175
+ return False
176
+ except (DeviceOfflineException, NoConnectionException) as ex:
177
+ """Device is offline try bluetooth if we have it."""
178
+ logger.error(f"Device offline: {ex.iot_id}")
179
+ return False
180
+
181
+ @staticmethod
182
+ async def async_send_bluetooth_command(device: MammotionMowerDeviceManager, key: str, **kwargs: Any) -> bool | None:
183
+ """Send command."""
184
+ if ble := device.ble:
185
+ await ble.command(key, **kwargs)
186
+
187
+ return True
188
+ raise DeviceOfflineException("bluetooth command failed", device.iot_id)
189
+
190
+ async def set_scheduled_updates(self, device_name: str, enabled: bool) -> None:
191
+ device = self.mammotion.get_device_by_name(device_name)
192
+ device.state.enabled = enabled
193
+ if device.state.enabled:
194
+ self.update_failures = 0
195
+ if not device.state.online:
196
+ device.state.online = True
197
+ if device.cloud and device.cloud.stopped:
198
+ await device.cloud.start()
199
+ else:
200
+ if device.cloud:
201
+ await device.cloud.stop()
202
+ if device.cloud.mqtt.is_connected():
203
+ device.cloud.mqtt.disconnect()
204
+ if device.ble:
205
+ await device.ble.stop()
206
+
207
+ def is_online(self, device_name: str) -> bool:
208
+ if device := self.mammotion.get_device_by_name(device_name):
209
+ if ble := device.ble:
210
+ return device.state.online or ble is not None and ble.client.is_connected
211
+ return False
212
+
213
+ async def update_firmware(self, device_name: str, version: str) -> None:
214
+ """Update firmware."""
215
+ device = self.mammotion.get_device_by_name(device_name)
216
+ await device.mammotion_http.start_ota_upgrade(device.iot_id, version)
217
+
218
+ async def async_start_stop_blades(self, device_name: str, start_stop: bool, blade_height: int = 60) -> None:
219
+ """Start stop blades."""
220
+ if DeviceType.is_luba1(device_name):
221
+ if start_stop:
222
+ await self.async_send_command(device_name, "set_blade_control", on_off=1)
223
+ else:
224
+ await self.async_send_command(device_name, "set_blade_control", on_off=0)
225
+ elif start_stop:
226
+ if DeviceType.is_yuka(device_name) or DeviceType.is_yuka_mini(device_name):
227
+ blade_height = 0
228
+
229
+ await self.async_send_command(
230
+ "operate_on_device",
231
+ main_ctrl=1,
232
+ cut_knife_ctrl=1,
233
+ cut_knife_height=blade_height,
234
+ max_run_speed=1.2,
235
+ )
236
+ else:
237
+ await self.async_send_command(
238
+ "operate_on_device",
239
+ main_ctrl=0,
240
+ cut_knife_ctrl=0,
241
+ cut_knife_height=blade_height,
242
+ max_run_speed=1.2,
243
+ )
244
+
245
+ async def async_set_rain_detection(self, device_name: str, on_off: bool) -> None:
246
+ """Set rain detection."""
247
+ await self.async_send_command(device_name, "read_write_device", rw_id=3, context=int(on_off), rw=1)
248
+
249
+ async def async_read_rain_detection(self, device_name: str) -> None:
250
+ """Set rain detection."""
251
+ await self.async_send_command(device_name, "read_write_device", rw_id=3, context=1, rw=0)
252
+
253
+ async def async_set_sidelight(self, device_name: str, on_off: int) -> None:
254
+ """Set Sidelight."""
255
+ await self.async_send_command(device_name, "read_and_set_sidelight", is_sidelight=bool(on_off), operate=0)
256
+ await self.async_read_sidelight(device_name)
257
+
258
+ async def async_read_sidelight(self, device_name: str) -> None:
259
+ """Set Sidelight."""
260
+ await self.async_send_command(device_name, "read_and_set_sidelight", is_sidelight=False, operate=1)
261
+
262
+ async def async_set_manual_light(self, device_name: str, manual_ctrl: bool) -> None:
263
+ """Set manual night light."""
264
+ await self.async_send_command(device_name, "set_car_manual_light", manual_ctrl=manual_ctrl)
265
+ await self.async_send_command(device_name, "get_car_light", ids=1126)
266
+
267
+ async def async_set_night_light(self, device_name: str, night_light: bool) -> None:
268
+ """Set night light."""
269
+ await self.async_send_command(device_name, "set_car_light", on_off=night_light)
270
+ await self.async_send_command(device_name, "get_car_light", ids=1123)
271
+
272
+ async def async_set_traversal_mode(self, device_name: str, context: int) -> None:
273
+ """Set traversal mode."""
274
+ await self.async_send_command(device_name, "traverse_mode", context=context)
275
+
276
+ async def async_set_turning_mode(self, device_name: str, context: int) -> None:
277
+ """Set turning mode."""
278
+ await self.async_send_command(device_name, "turning_mode", context=context)
279
+
280
+ async def async_blade_height(self, device_name: str, height: int) -> int:
281
+ """Set blade height."""
282
+ await self.async_send_command(device_name, "set_blade_height", height=height)
283
+ return height
284
+
285
+ async def async_set_cutter_speed(self, device_name: str, mode: int) -> None:
286
+ """Set cutter speed."""
287
+ await self.async_send_command(device_name, "set_cutter_mode", cutter_mode=mode)
288
+
289
+ async def async_set_speed(self, device_name: str, speed: float) -> None:
290
+ """Set working speed."""
291
+ await self.async_send_command(device_name, "set_speed", speed=speed)
292
+
293
+ async def async_leave_dock(self, device_name: str) -> None:
294
+ """Leave dock."""
295
+ await self.send_command_and_update(device_name, "leave_dock")
296
+
297
+ async def async_cancel_task(self, device_name: str) -> None:
298
+ """Cancel task."""
299
+ await self.send_command_and_update(device_name, "cancel_job")
300
+
301
+ async def async_move_forward(self, device_name: str, speed: float) -> None:
302
+ """Move forward."""
303
+ device = self.mammotion.get_device_by_name(device_name)
304
+ await self.async_send_bluetooth_command(device, "move_forward", linear=speed)
305
+
306
+ async def async_move_left(self, device_name: str, speed: float) -> None:
307
+ """Move left."""
308
+ device = self.mammotion.get_device_by_name(device_name)
309
+ await self.async_send_bluetooth_command(device, "move_left", angular=speed)
310
+
311
+ async def async_move_right(self, device_name: str, speed: float) -> None:
312
+ """Move right."""
313
+ device = self.mammotion.get_device_by_name(device_name)
314
+ await self.async_send_bluetooth_command(device, "move_right", angular=speed)
315
+
316
+ async def async_move_back(self, device_name: str, speed: float) -> None:
317
+ """Move back."""
318
+ device = self.mammotion.get_device_by_name(device_name)
319
+ await self.async_send_bluetooth_command(device, "move_back", linear=speed)
320
+
321
+ async def async_rtk_dock_location(self, device_name: str) -> None:
322
+ """RTK and dock location."""
323
+ await self.async_send_command(device_name, "read_write_device", rw_id=5, rw=1, context=1)
324
+
325
+ async def async_get_area_list(self, device_name: str, iot_id: str) -> None:
326
+ """Mowing area List."""
327
+ await self.async_send_command(device_name, "get_area_name_list", device_id=iot_id)
328
+
329
+ async def async_relocate_charging_station(self, device_name: str) -> None:
330
+ """Reset charging station."""
331
+ await self.async_send_command(device_name, "delete_charge_point")
332
+ # fetch charging location?
333
+ """
334
+ nav {
335
+ todev_get_commondata {
336
+ pver: 1
337
+ subCmd: 2
338
+ action: 6
339
+ type: 5
340
+ totalFrame: 1
341
+ currentFrame: 1
342
+ }
343
+ }
344
+ """
345
+
346
+ async def async_set_non_work_hours(self, device_name: str, start_time: str, end_time: str) -> None:
347
+ """Set non work hours l1?."""
348
+ device = self._mammotion.get_device_by_name(device_name)
349
+ await self.async_send_command(
350
+ device_name,
351
+ "set_plan_unable_time",
352
+ sub_cmd=device.state.non_work_hours.sub_cmd,
353
+ device_id=device.iot_id,
354
+ unable_end_time=end_time,
355
+ unable_start_time=start_time,
356
+ )
357
+
358
+ async def async_set_job_dnd(self, device_name: str, start_time: str, end_time: str) -> None:
359
+ """Set non work hours."""
360
+ await self.async_send_command(
361
+ device_name,
362
+ "job_do_not_disturb",
363
+ sub_cmd=1,
364
+ trigger=1,
365
+ unable_end_time=end_time,
366
+ unable_start_time=start_time,
367
+ )
368
+
369
+ async def async_del_job_dnd(self, device_name: str) -> None:
370
+ """Del non work hours."""
371
+ await self.async_send_command(device_name, "job_do_not_disturb", sub_cmd=1, trigger=0)
372
+
373
+ async def send_command_and_update(self, device_name: str, command_str: str, **kwargs: Any) -> None:
374
+ """Send command and update."""
375
+ await self.async_send_command(device_name, command_str, **kwargs)
376
+ await self.async_request_iot_sync(device_name)
377
+
378
+ async def async_request_iot_sync(self, device_name: str, stop: bool = False) -> None:
379
+ """Sync specific info from device."""
380
+ await self.async_send_command(
381
+ device_name,
382
+ "request_iot_sys",
383
+ rpt_act=RptAct.RPT_STOP if stop else RptAct.RPT_START,
384
+ rpt_info_type=[
385
+ RptInfoType.RIT_DEV_STA,
386
+ RptInfoType.RIT_DEV_LOCAL,
387
+ RptInfoType.RIT_WORK,
388
+ RptInfoType.RIT_MAINTAIN,
389
+ RptInfoType.RIT_BASESTATION_INFO,
390
+ RptInfoType.RIT_VIO,
391
+ ],
392
+ timeout=10000,
393
+ period=3000,
394
+ no_change_period=4000,
395
+ count=0,
396
+ )
397
+
398
+ def generate_route_information(
399
+ self, device_name: str, operation_settings: OperationSettings
400
+ ) -> GenerateRouteInformation:
401
+ """Generate route information."""
402
+ device = self.mammotion.get_device_by_name(device_name)
403
+ if device.state.report_data.dev:
404
+ dev = device.state.report_data.dev
405
+ if dev.collector_status.collector_installation_status == 0:
406
+ operation_settings.is_dump = False
407
+
408
+ if DeviceType.is_yuka(device_name):
409
+ operation_settings.blade_height = -10
410
+
411
+ route_information = GenerateRouteInformation(
412
+ one_hashs=list(operation_settings.areas),
413
+ rain_tactics=operation_settings.rain_tactics,
414
+ speed=operation_settings.speed,
415
+ ultra_wave=operation_settings.ultra_wave, # touch no touch etc
416
+ toward=operation_settings.toward, # is just angle (route angle)
417
+ toward_included_angle=operation_settings.toward_included_angle # demond_angle
418
+ if operation_settings.channel_mode == 1
419
+ else 0, # crossing angle relative to grid
420
+ toward_mode=operation_settings.toward_mode,
421
+ blade_height=operation_settings.blade_height,
422
+ channel_mode=operation_settings.channel_mode, # single, double, segment or none (route mode)
423
+ channel_width=operation_settings.channel_width, # path space
424
+ job_mode=operation_settings.job_mode, # taskMode grid or border first
425
+ edge_mode=operation_settings.mowing_laps, # perimeter/mowing laps
426
+ path_order=create_path_order(operation_settings, device_name),
427
+ obstacle_laps=operation_settings.obstacle_laps,
428
+ )
429
+
430
+ if DeviceType.is_luba1(device_name):
431
+ route_information.toward_mode = 0
432
+ route_information.toward_included_angle = 0
433
+ return route_information
434
+
435
+ async def async_plan_route(self, device_name: str, operation_settings: OperationSettings) -> bool | None:
436
+ """Plan mow."""
437
+ route_information = self.generate_route_information(device_name, operation_settings)
438
+
439
+ # not sure if this is artificial limit
440
+ # if (
441
+ # DeviceType.is_mini_or_x_series(device_name)
442
+ # and route_information.toward_mode == 0
443
+ # ):
444
+ # route_information.toward = 0
445
+
446
+ return await self.async_send_command(
447
+ device_name, "generate_route_information", generate_route_information=route_information
448
+ )
449
+
450
+ async def async_modify_plan_route(self, device_name: str, operation_settings: OperationSettings) -> bool | None:
451
+ """Modify plan mow."""
452
+ device = self.mammotion.get_device_by_name(device_name)
453
+
454
+ if work := device.state.work:
455
+ operation_settings.areas = set(work.zone_hashs)
456
+ operation_settings.toward = work.toward
457
+ operation_settings.toward_mode = work.toward_mode
458
+ operation_settings.toward_included_angle = work.toward_included_angle
459
+ operation_settings.mowing_laps = work.edge_mode
460
+ operation_settings.job_mode = work.job_mode
461
+ operation_settings.job_id = work.job_id
462
+ operation_settings.job_version = work.job_ver
463
+
464
+ route_information = self.generate_route_information(device_name, operation_settings)
465
+ if route_information.toward_mode == 0:
466
+ route_information.toward = 0
467
+
468
+ return await self.async_send_command(
469
+ device_name, "modify_route_information", generate_route_information=route_information
470
+ )
471
+
472
+ async def start_task(self, device_name: str, plan_id: str) -> None:
473
+ """Start task."""
474
+ await self.async_send_command(device_name, "single_schedule", plan_id=plan_id)
475
+
476
+ async def clear_update_failures(self, device_name: str) -> None:
477
+ """Clear update failures."""
478
+ self.update_failures = 0
479
+ device = self.mammotion.get_device_by_name(device_name)
480
+ if not device.state.online:
481
+ device.state.online = True
482
+ if cloud := device.cloud:
483
+ if cloud.stopped:
484
+ await cloud.start()
@@ -0,0 +1,54 @@
1
+ import json
2
+
3
+ from pymammotion.aliyun.cloud_gateway import DeviceOfflineException, GatewayTimeoutException, SetupException
4
+ from pymammotion.data.model.device import RTKDevice
5
+ from pymammotion.http.model.http import CheckDeviceVersion
6
+ from pymammotion.mammotion.devices.mammotion import Mammotion
7
+
8
+
9
+ class HomeAssistantRTKApi:
10
+ def __init__(self) -> None:
11
+ self._mammotion = Mammotion()
12
+
13
+ @property
14
+ def mammotion(self) -> Mammotion:
15
+ return self._mammotion
16
+
17
+ async def update(self, device_name: str) -> RTKDevice:
18
+ """Update RTK data."""
19
+ device = self.mammotion.get_rtk_device_by_name(device_name)
20
+ try:
21
+ response = await device.cloud_client.get_device_properties(device.iot_id)
22
+ if response.code == 200:
23
+ if data := response.data:
24
+ if ota_progress := data.otaProgress:
25
+ device.state.update_check = CheckDeviceVersion.from_dict(ota_progress.value)
26
+ if network_info := data.networkInfo:
27
+ network = json.loads(network_info.value)
28
+ device.state.wifi_rssi = network["wifi_rssi"]
29
+ device.state.wifi_sta_mac = network["wifi_sta_mac"]
30
+ device.state.bt_mac = network["bt_mac"]
31
+ if coordinate := data.coordinate:
32
+ coord_val = json.loads(coordinate.value)
33
+ if device.state.lat == 0:
34
+ device.state.lat = coord_val["lat"]
35
+ if device.state.lon == 0:
36
+ device.state.lon = coord_val["lon"]
37
+ if device_version := data.deviceVersion:
38
+ device.state.device_version = device_version.value
39
+ device.state.online = True
40
+
41
+ ota_info = await device.cloud_client.mammotion_http.get_device_ota_firmware([device.state.iot_id])
42
+ if check_versions := ota_info.data:
43
+ for check_version in check_versions:
44
+ if check_version.device_id == device.state.iot_id:
45
+ device.state.update_check = check_version
46
+ return device.state
47
+ except SetupException:
48
+ """Cloud IOT Gateway is not setup."""
49
+ return device.state
50
+ except DeviceOfflineException:
51
+ device.state.online = False
52
+ except GatewayTimeoutException:
53
+ """Gateway is timing out again."""
54
+ return device.state
@@ -2,7 +2,6 @@ import base64
2
2
  import logging
3
3
  import secrets
4
4
  import string
5
- from typing import Optional
6
5
 
7
6
  from cryptography.hazmat.backends import default_backend
8
7
  from cryptography.hazmat.primitives import padding, serialization
@@ -50,7 +49,7 @@ class EncryptionUtils:
50
49
  private_key_bytes = base64.b64decode(EncryptionUtils.PRIVATE_KEY)
51
50
  return serialization.load_der_private_key(private_key_bytes, password=None, backend=default_backend())
52
51
  except Exception as e:
53
- raise Exception(f"Failed to load private key: {str(e)}")
52
+ raise Exception(f"Failed to load private key: {e!s}")
54
53
 
55
54
  @staticmethod
56
55
  def load_public_key(is_production: bool = True):
@@ -65,7 +64,7 @@ class EncryptionUtils:
65
64
  public_key_bytes = base64.b64decode(key_string)
66
65
  return serialization.load_der_public_key(public_key_bytes, backend=default_backend())
67
66
  except Exception as e:
68
- raise Exception(f"Failed to load public key: {str(e)}")
67
+ raise Exception(f"Failed to load public key: {e!s}")
69
68
 
70
69
  @staticmethod
71
70
  def encrypt(plaintext: str, key: str, iv: str) -> str:
@@ -104,7 +103,7 @@ class EncryptionUtils:
104
103
  return base64.b64encode(encrypted_bytes).decode("utf-8")
105
104
 
106
105
  except Exception as e:
107
- raise Exception(f"Encryption failed: {str(e)}")
106
+ raise Exception(f"Encryption failed: {e!s}")
108
107
 
109
108
  def encryption_by_aes(self, text: str) -> str:
110
109
  """Encrypt text using AES with class-level key and IV
@@ -123,10 +122,10 @@ class EncryptionUtils:
123
122
  return encrypted
124
123
 
125
124
  except Exception as e:
126
- _LOGGER.error(f"Encryption failed: {str(e)}")
125
+ _LOGGER.error(f"Encryption failed: {e!s}")
127
126
  return None
128
127
 
129
- def encrypt_by_public_key(self) -> Optional[str]:
128
+ def encrypt_by_public_key(self) -> str | None:
130
129
  """Encrypt data using RSA public key.
131
130
 
132
131
  Args: