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