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
@@ -2,130 +2,30 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import queue
6
- import threading
7
5
  import asyncio
8
- import base64
9
- import codecs
10
- import json
11
6
  import logging
12
- from abc import abstractmethod
13
- from collections import deque
14
7
  from enum import Enum
15
8
  from functools import cache
16
- from typing import Any, Callable, Optional, cast, Awaitable
17
- from uuid import UUID
9
+ from typing import Any
18
10
 
19
- import betterproto
20
11
  from aiohttp import ClientSession
21
12
  from bleak.backends.device import BLEDevice
22
- from bleak.backends.service import BleakGATTCharacteristic, BleakGATTServiceCollection
23
- from bleak.exc import BleakDBusError
24
- from bleak_retry_connector import (
25
- BLEAK_RETRY_EXCEPTIONS,
26
- BleakClientWithServiceCache,
27
- BleakNotFoundError,
28
- establish_connection,
29
- )
30
13
 
31
- from pymammotion.aliyun.cloud_gateway import CloudIOTGateway, DeviceOfflineException, SetupException
14
+ from pymammotion.aliyun.cloud_gateway import CloudIOTGateway
32
15
  from pymammotion.aliyun.dataclass.dev_by_account_response import Device
33
- from pymammotion.bluetooth import BleMessage
34
16
  from pymammotion.const import MAMMOTION_DOMAIN
35
- from pymammotion.data.model import RegionData
36
17
  from pymammotion.data.model.account import Credentials
37
18
  from pymammotion.data.model.device import MowingDevice
38
- from pymammotion.data.mqtt.event import ThingEventMessage
39
- from pymammotion.data.state_manager import StateManager
40
19
  from pymammotion.http.http import connect_http
41
- from pymammotion.mammotion.commands.mammotion_command import MammotionCommand
20
+ from pymammotion.mammotion.devices.mammotion_bluetooth import MammotionBaseBLEDevice
21
+ from pymammotion.mammotion.devices.mammotion_cloud import MammotionBaseCloudDevice, MammotionCloud
42
22
  from pymammotion.mqtt import MammotionMQTT
43
- from pymammotion.mqtt.mammotion_future import MammotionFuture
44
- from pymammotion.proto.luba_msg import LubaMsg
45
- from pymammotion.proto.mctrl_nav import NavGetCommDataAck, NavGetHashListAck
46
- from pymammotion.utility.movement import get_percent, transform_both_speeds
47
-
48
-
49
- class CharacteristicMissingError(Exception):
50
- """Raised when a characteristic is missing."""
51
-
52
-
53
- def _sb_uuid(comms_type: str = "service") -> UUID | str:
54
- """Return Mammotion UUID.
55
-
56
- Args:
57
- comms_type (str): The type of communication (tx, rx, or service).
58
-
59
- Returns:
60
- UUID | str: The UUID for the specified communication type or an error message.
61
-
62
- """
63
- _uuid = {"tx": "ff01", "rx": "ff02", "service": "2A05"}
64
-
65
- if comms_type in _uuid:
66
- return UUID(f"0000{_uuid[comms_type]}-0000-1000-8000-00805f9b34fb")
67
-
68
- return "Incorrect type, choose between: tx, rx or service"
69
-
70
-
71
- READ_CHAR_UUID = _sb_uuid(comms_type="rx")
72
- WRITE_CHAR_UUID = _sb_uuid(comms_type="tx")
73
-
74
- DBUS_ERROR_BACKOFF_TIME = 0.25
75
-
76
- DISCONNECT_DELAY = 10
77
23
 
78
24
  TIMEOUT_CLOUD_RESPONSE = 10
79
25
 
80
26
  _LOGGER = logging.getLogger(__name__)
81
27
 
82
28
 
83
- def slashescape(err):
84
- """Escape a slash character."""
85
- # print err, dir(err), err.start, err.end, err.object[:err.start]
86
- thebyte = err.object[err.start : err.end]
87
- repl = "\\x" + hex(ord(thebyte))[2:]
88
- return (repl, err.end)
89
-
90
-
91
- codecs.register_error("slashescape", slashescape)
92
-
93
-
94
- def find_next_integer(lst: list[int], current_hash: float) -> int | None:
95
- try:
96
- # Find the index of the current integer
97
- current_index = lst.index(current_hash)
98
-
99
- # Check if there is a next integer in the list
100
- if current_index + 1 < len(lst):
101
- return lst[current_index + 1]
102
- else:
103
- return None # Or raise an exception or handle it in some other way
104
- except ValueError:
105
- # Handle the case where current_int is not in the list
106
- return None # Or raise an exception or handle it in some other way
107
-
108
-
109
- def _handle_timeout(fut: asyncio.Future[None]) -> None:
110
- """Handle a timeout."""
111
- if not fut.done():
112
- fut.set_exception(asyncio.TimeoutError)
113
-
114
-
115
- async def _handle_retry(fut: asyncio.Future[None], func, command: bytes) -> None:
116
- """Handle a retry."""
117
- if not fut.done():
118
- await func(command)
119
-
120
-
121
- async def _handle_retry_cloud(self, fut: asyncio.Future[None], func, iot_id: str, command: bytes) -> None:
122
- """Handle a retry."""
123
-
124
- if not fut.done():
125
- self._operation_lock.release()
126
- await self.loop.run_in_executor(None, func, iot_id, command)
127
-
128
-
129
29
  class ConnectionPreference(Enum):
130
30
  """Enum for connection preference."""
131
31
 
@@ -144,7 +44,7 @@ class MammotionMixedDeviceManager:
144
44
  name: str,
145
45
  cloud_device: Device | None = None,
146
46
  ble_device: BLEDevice | None = None,
147
- mqtt: MammotionMQTT | None = None,
47
+ mqtt: MammotionCloud | None = None,
148
48
  ) -> None:
149
49
  self.name = name
150
50
  self.add_ble(ble_device)
@@ -163,10 +63,10 @@ class MammotionMixedDeviceManager:
163
63
  if ble_device is not None:
164
64
  self._ble_device = MammotionBaseBLEDevice(self._mowing_state, ble_device)
165
65
 
166
- def add_cloud(self, cloud_device: Device | None = None, mqtt: MammotionMQTT | None = None) -> None:
66
+ def add_cloud(self, cloud_device: Device | None = None, mqtt: MammotionCloud | None = None) -> None:
167
67
  if cloud_device is not None:
168
68
  self._cloud_device = MammotionBaseCloudDevice(
169
- mqtt_client=mqtt, cloud_device=cloud_device, mowing_state=self._mowing_state
69
+ mqtt, cloud_device=cloud_device, mowing_state=self._mowing_state
170
70
  )
171
71
 
172
72
  def replace_cloud(self, cloud_device: MammotionBaseCloudDevice) -> None:
@@ -216,12 +116,12 @@ async def create_devices(
216
116
 
217
117
 
218
118
  @cache
219
- class Mammotion(object):
220
- """Represents a Mammotion device."""
119
+ class Mammotion:
120
+ """Represents a Mammotion account and its devices."""
221
121
 
222
122
  devices = MammotionDevices()
223
123
  cloud_client: CloudIOTGateway | None = None
224
- mqtt: MammotionMQTT | None = None
124
+ mqtt: MammotionCloud | None = None
225
125
 
226
126
  def __init__(
227
127
  self, ble_device: BLEDevice, preference: ConnectionPreference = ConnectionPreference.BLUETOOTH
@@ -239,26 +139,28 @@ class Mammotion(object):
239
139
  return
240
140
 
241
141
  self.cloud_client = cloud_client
242
- self.mqtt = MammotionMQTT(
243
- region_id=cloud_client._region_response.data.regionId,
244
- product_key=cloud_client._aep_response.data.productKey,
245
- device_name=cloud_client._aep_response.data.deviceName,
246
- device_secret=cloud_client._aep_response.data.deviceSecret,
247
- iot_token=cloud_client._session_by_authcode_response.data.iotToken,
248
- client_id=cloud_client._client_id,
142
+ self.mqtt = MammotionCloud(
143
+ MammotionMQTT(
144
+ region_id=cloud_client.region_response.data.regionId,
145
+ product_key=cloud_client.aep_response.data.productKey,
146
+ device_name=cloud_client.aep_response.data.deviceName,
147
+ device_secret=cloud_client.aep_response.data.deviceSecret,
148
+ iot_token=cloud_client.session_by_authcode_response.data.iotToken,
149
+ client_id=cloud_client.client_id,
150
+ cloud_client=cloud_client,
151
+ )
249
152
  )
250
153
 
251
- self.mqtt._cloud_client = cloud_client
252
154
  loop = asyncio.get_running_loop()
253
155
  await loop.run_in_executor(None, self.mqtt.connect_async)
254
156
 
255
- for device in cloud_client.listing_dev_by_account_response.data.data:
157
+ for device in cloud_client.devices_by_account_response.data.data:
256
158
  if device.deviceName.startswith(("Luba-", "Yuka-")) and self.devices.get_device(device.deviceName) is None:
257
159
  self.devices.add_device(
258
160
  MammotionMixedDeviceManager(name=device.deviceName, cloud_device=device, mqtt=self.mqtt)
259
161
  )
260
162
 
261
- def set_disconnect_strategy(self, disconnect: bool):
163
+ def set_disconnect_strategy(self, disconnect: bool) -> None:
262
164
  for device_name, device in self.devices.devices:
263
165
  if device.ble() is not None:
264
166
  ble_device: MammotionBaseBLEDevice = device.ble()
@@ -299,7 +201,7 @@ class Mammotion(object):
299
201
  return await device.cloud().command(key)
300
202
  # TODO work with both with EITHER
301
203
 
302
- async def send_command_with_args(self, name: str, key: str, **kwargs: any):
204
+ async def send_command_with_args(self, name: str, key: str, **kwargs: Any):
303
205
  """Send a command with args to the device."""
304
206
  device = self.get_device_by_name(name)
305
207
  if device:
@@ -331,887 +233,3 @@ class Mammotion(object):
331
233
  device = self.get_device_by_name(name)
332
234
  if device:
333
235
  return device.mower_state()
334
-
335
-
336
- def has_field(message: betterproto.Message) -> bool:
337
- """Check if the message has any fields serialized on wire."""
338
- return betterproto.serialized_on_wire(message)
339
-
340
-
341
- class MammotionBaseDevice:
342
- """Base class for Mammotion devices."""
343
-
344
- _mower: MowingDevice
345
- _state_manager: StateManager
346
- _cloud_device: Device | None = None
347
-
348
- def __init__(self, device: MowingDevice, cloud_device: Device | None = None) -> None:
349
- """Initialize MammotionBaseDevice."""
350
- self.loop = asyncio.get_event_loop()
351
- self._raw_data = LubaMsg().to_dict(casing=betterproto.Casing.SNAKE)
352
- self._mower = device
353
- self._state_manager = StateManager(self._mower)
354
- self._state_manager.gethash_ack_callback = self.datahash_response
355
- self._state_manager.get_commondata_ack_callback = self.commdata_response
356
- self._notify_future: asyncio.Future[bytes] | None = None
357
- self._cloud_device = cloud_device
358
-
359
- def set_notification_callback(self, func: Callable[[], Awaitable[None]]):
360
- self._state_manager.on_notification_callback = func
361
-
362
- async def datahash_response(self, hash_ack: NavGetHashListAck):
363
- """Handle datahash responses."""
364
- await self.queue_command("synchronize_hash_data", hash_num=hash_ack.data_couple[0])
365
-
366
- async def commdata_response(self, common_data: NavGetCommDataAck):
367
- """Handle common data responses."""
368
- total_frame = common_data.total_frame
369
- current_frame = common_data.current_frame
370
-
371
- missing_frames = self._mower.map.missing_frame(common_data)
372
- if len(missing_frames) == 0:
373
- # get next in hash ack list
374
-
375
- data_hash = find_next_integer(self._mower.nav.toapp_gethash_ack.data_couple, common_data.hash)
376
- if data_hash is None:
377
- return
378
-
379
- await self.queue_command("synchronize_hash_data", hash_num=data_hash)
380
- else:
381
- if current_frame != missing_frames[0] - 1:
382
- current_frame = missing_frames[0] - 1
383
-
384
- region_data = RegionData()
385
- region_data.hash = common_data.hash
386
- region_data.action = common_data.action
387
- region_data.type = common_data.type
388
- region_data.total_frame = total_frame
389
- region_data.current_frame = current_frame
390
- await self.queue_command("get_regional_data", regional_data=region_data)
391
-
392
- def _update_raw_data(self, data: bytes) -> None:
393
- """Update raw and model data from notifications."""
394
- tmp_msg = LubaMsg().parse(data)
395
- res = betterproto.which_one_of(tmp_msg, "LubaSubMsg")
396
- match res[0]:
397
- case "nav":
398
- self._update_nav_data(tmp_msg)
399
- case "sys":
400
- self._update_sys_data(tmp_msg)
401
- case "driver":
402
- self._update_driver_data(tmp_msg)
403
- case "net":
404
- self._update_net_data(tmp_msg)
405
- case "mul":
406
- self._update_mul_data(tmp_msg)
407
- case "ota":
408
- self._update_ota_data(tmp_msg)
409
-
410
- self._mower.update_raw(self._raw_data)
411
-
412
- def _update_nav_data(self, tmp_msg):
413
- """Update navigation data."""
414
- nav_sub_msg = betterproto.which_one_of(tmp_msg.nav, "SubNavMsg")
415
- if nav_sub_msg[1] is None:
416
- _LOGGER.debug("Sub message was NoneType %s", nav_sub_msg[0])
417
- return
418
- nav = self._raw_data.get("nav", {})
419
- if isinstance(nav_sub_msg[1], int):
420
- nav[nav_sub_msg[0]] = nav_sub_msg[1]
421
- else:
422
- nav[nav_sub_msg[0]] = nav_sub_msg[1].to_dict(casing=betterproto.Casing.SNAKE)
423
- self._raw_data["nav"] = nav
424
-
425
- def _update_sys_data(self, tmp_msg):
426
- """Update system data."""
427
- sys_sub_msg = betterproto.which_one_of(tmp_msg.sys, "SubSysMsg")
428
- if sys_sub_msg[1] is None:
429
- _LOGGER.debug("Sub message was NoneType %s", sys_sub_msg[0])
430
- return
431
- sys = self._raw_data.get("sys", {})
432
- sys[sys_sub_msg[0]] = sys_sub_msg[1].to_dict(casing=betterproto.Casing.SNAKE)
433
- self._raw_data["sys"] = sys
434
-
435
- def _update_driver_data(self, tmp_msg):
436
- """Update driver data."""
437
- drv_sub_msg = betterproto.which_one_of(tmp_msg.driver, "SubDrvMsg")
438
- if drv_sub_msg[1] is None:
439
- _LOGGER.debug("Sub message was NoneType %s", drv_sub_msg[0])
440
- return
441
- drv = self._raw_data.get("driver", {})
442
- drv[drv_sub_msg[0]] = drv_sub_msg[1].to_dict(casing=betterproto.Casing.SNAKE)
443
- self._raw_data["driver"] = drv
444
-
445
- def _update_net_data(self, tmp_msg):
446
- """Update network data."""
447
- net_sub_msg = betterproto.which_one_of(tmp_msg.net, "NetSubType")
448
- if net_sub_msg[1] is None:
449
- _LOGGER.debug("Sub message was NoneType %s", net_sub_msg[0])
450
- return
451
- net = self._raw_data.get("net", {})
452
- if isinstance(net_sub_msg[1], int):
453
- net[net_sub_msg[0]] = net_sub_msg[1]
454
- else:
455
- net[net_sub_msg[0]] = net_sub_msg[1].to_dict(casing=betterproto.Casing.SNAKE)
456
- self._raw_data["net"] = net
457
-
458
- def _update_mul_data(self, tmp_msg):
459
- """Update mul data."""
460
- mul_sub_msg = betterproto.which_one_of(tmp_msg.mul, "SubMul")
461
- if mul_sub_msg[1] is None:
462
- _LOGGER.debug("Sub message was NoneType %s", mul_sub_msg[0])
463
- return
464
- mul = self._raw_data.get("mul", {})
465
- mul[mul_sub_msg[0]] = mul_sub_msg[1].to_dict(casing=betterproto.Casing.SNAKE)
466
- self._raw_data["mul"] = mul
467
-
468
- def _update_ota_data(self, tmp_msg):
469
- """Update OTA data."""
470
- ota_sub_msg = betterproto.which_one_of(tmp_msg.ota, "SubOtaMsg")
471
- if ota_sub_msg[1] is None:
472
- _LOGGER.debug("Sub message was NoneType %s", ota_sub_msg[0])
473
- return
474
- ota = self._raw_data.get("ota", {})
475
- ota[ota_sub_msg[0]] = ota_sub_msg[1].to_dict(casing=betterproto.Casing.SNAKE)
476
- self._raw_data["ota"] = ota
477
-
478
- @property
479
- def raw_data(self) -> dict[str, Any]:
480
- """Get the raw data of the device."""
481
- return self._raw_data
482
-
483
- @property
484
- def mower(self) -> MowingDevice:
485
- """Get the LubaMsg of the device."""
486
- return self._mower
487
-
488
- @abstractmethod
489
- async def queue_command(self, key: str, **kwargs: any) -> bytes:
490
- """Queue commands to mower."""
491
-
492
- @abstractmethod
493
- async def _send_command(self, key: str, retry: int | None = None) -> bytes | None:
494
- """Send command to device and read response."""
495
-
496
- @abstractmethod
497
- async def _send_command_with_args(self, key: str, **kwargs: any) -> bytes | None:
498
- """Send command to device and read response."""
499
-
500
- @abstractmethod
501
- async def _ble_sync(self):
502
- """Send ble sync command every 3 seconds or sooner."""
503
-
504
- async def start_sync(self, retry: int):
505
- """Start synchronization with the device."""
506
- await self.queue_command("get_device_base_info")
507
- await self.queue_command("get_device_product_model")
508
- await self.queue_command("get_report_cfg")
509
- """RTK and dock location."""
510
- await self.queue_command("allpowerfull_rw", id=5, rw=1, context=1)
511
-
512
- async def start_map_sync(self):
513
- """Start sync of map data."""
514
- await self.queue_command("read_plan", sub_cmd=2, plan_index=0)
515
-
516
- await self.queue_command("get_all_boundary_hash_list", sub_cmd=0)
517
-
518
- await self.queue_command("get_hash_response", total_frame=1, current_frame=1)
519
-
520
- # work out why this crashes sometimes for better proto
521
- if self._cloud_device:
522
- await self.queue_command("get_area_name_list", device_id=self._cloud_device.deviceName)
523
- if has_field(self._mower.net.toapp_wifi_iot_status):
524
- await self.queue_command("get_area_name_list", device_id=self._mower.net.toapp_wifi_iot_status.devicename)
525
-
526
- # sub_cmd 3 is job hashes??
527
- # sub_cmd 4 is dump location (yuka)
528
- # jobs list
529
- # hash_list_result = await self._send_command_with_args("get_all_boundary_hash_list", sub_cmd=3)
530
-
531
- async def async_get_errors(self):
532
- """Error codes."""
533
- await self.queue_command("allpowerfull_rw", id=5, rw=1, context=2)
534
- await self.queue_command("allpowerfull_rw", id=5, rw=1, context=3)
535
-
536
- async def move_forward(self, linear: float):
537
- """Move forward. values 0.0 1.0."""
538
- linear_percent = get_percent(abs(linear * 100))
539
- (linear_speed, angular_speed) = transform_both_speeds(90.0, 0.0, linear_percent, 0.0)
540
- await self.queue_command("send_movement", linear_speed=linear_speed, angular_speed=angular_speed)
541
-
542
- async def move_back(self, linear: float):
543
- """Move back. values 0.0 1.0."""
544
- linear_percent = get_percent(abs(linear * 100))
545
- (linear_speed, angular_speed) = transform_both_speeds(270.0, 0.0, linear_percent, 0.0)
546
- await self.queue_command("send_movement", linear_speed=linear_speed, angular_speed=angular_speed)
547
-
548
- async def move_left(self, angulur: float):
549
- """Move forward. values 0.0 1.0."""
550
- angular_percent = get_percent(abs(angulur * 100))
551
- (linear_speed, angular_speed) = transform_both_speeds(0.0, 0.0, 0.0, angular_percent)
552
- await self.queue_command("send_movement", linear_speed=linear_speed, angular_speed=angular_speed)
553
-
554
- async def move_right(self, angulur: float):
555
- """Move back. values 0.0 1.0."""
556
- angular_percent = get_percent(abs(angulur * 100))
557
- (linear_speed, angular_speed) = transform_both_speeds(0.0, 180.0, 0.0, angular_percent)
558
- await self.queue_command("send_movement", linear_speed=linear_speed, angular_speed=angular_speed)
559
-
560
- async def command(self, key: str, **kwargs):
561
- """Send a command to the device."""
562
- return await self.queue_command(key, **kwargs)
563
-
564
-
565
- class MammotionBaseBLEDevice(MammotionBaseDevice):
566
- """Base class for Mammotion BLE devices."""
567
-
568
- def __init__(self, mowing_state: MowingDevice, device: BLEDevice, interface: int = 0, **kwargs: Any) -> None:
569
- """Initialize MammotionBaseBLEDevice."""
570
- super().__init__(mowing_state)
571
- self._disconnect_strategy = True
572
- self._ble_sync_task = None
573
- self._prev_notification = None
574
- self._interface = f"hci{interface}"
575
- self._device = device
576
- self._mower = mowing_state
577
- self._client: BleakClientWithServiceCache | None = None
578
- self._read_char: BleakGATTCharacteristic | None = None
579
- self._write_char: BleakGATTCharacteristic | None = None
580
- self._disconnect_timer: asyncio.TimerHandle | None = None
581
- self._message: BleMessage | None = None
582
- self._commands: MammotionCommand = MammotionCommand(device.name)
583
- self._expected_disconnect = False
584
- self._connect_lock = asyncio.Lock()
585
- self._operation_lock = asyncio.Lock()
586
- self._key: str | None = None
587
-
588
- def update_device(self, device: BLEDevice) -> None:
589
- """Update the BLE device."""
590
- self._device = device
591
-
592
- async def _ble_sync(self):
593
- command_bytes = self._commands.send_todev_ble_sync(2)
594
- await self._message.post_custom_data_bytes(command_bytes)
595
-
596
- async def run_periodic_sync_task(self) -> None:
597
- """Send ble sync to robot."""
598
- try:
599
- await self._ble_sync()
600
- finally:
601
- self.schedule_ble_sync()
602
-
603
- def schedule_ble_sync(self):
604
- """Periodically sync to keep connection alive."""
605
- if self._client is not None and self._client.is_connected:
606
- self._ble_sync_task = self.loop.call_later(
607
- 130, lambda: asyncio.ensure_future(self.run_periodic_sync_task())
608
- )
609
-
610
- async def queue_command(self, key: str, **kwargs: any) -> bytes | None:
611
- return await self._send_command_with_args(key, **kwargs)
612
-
613
- async def _send_command_with_args(self, key: str, **kwargs) -> bytes | None:
614
- """Send command to device and read response."""
615
- if self._operation_lock.locked():
616
- _LOGGER.debug(
617
- "%s: Operation already in progress, waiting for it to complete; RSSI: %s",
618
- self.name,
619
- self.rssi,
620
- )
621
- async with self._operation_lock:
622
- try:
623
- command_bytes = getattr(self._commands, key)(**kwargs)
624
- return await self._send_command_locked(key, command_bytes)
625
- except BleakNotFoundError:
626
- _LOGGER.exception(
627
- "%s: device not found, no longer in range, or poor RSSI: %s",
628
- self.name,
629
- self.rssi,
630
- )
631
- raise
632
- except CharacteristicMissingError as ex:
633
- _LOGGER.debug(
634
- "%s: characteristic missing: %s; RSSI: %s",
635
- self.name,
636
- ex,
637
- self.rssi,
638
- exc_info=True,
639
- )
640
- except BLEAK_RETRY_EXCEPTIONS:
641
- _LOGGER.debug("%s: communication failed with:", self.name, exc_info=True)
642
-
643
- async def _send_command(self, key: str, retry: int | None = None) -> bytes | None:
644
- """Send command to device and read response."""
645
- if self._operation_lock.locked():
646
- _LOGGER.debug(
647
- "%s: Operation already in progress, waiting for it to complete; RSSI: %s",
648
- self.name,
649
- self.rssi,
650
- )
651
- async with self._operation_lock:
652
- try:
653
- command_bytes = getattr(self._commands, key)()
654
- return await self._send_command_locked(key, command_bytes)
655
- except BleakNotFoundError:
656
- _LOGGER.exception(
657
- "%s: device not found, no longer in range, or poor RSSI: %s",
658
- self.name,
659
- self.rssi,
660
- )
661
- raise
662
- except CharacteristicMissingError as ex:
663
- _LOGGER.debug(
664
- "%s: characteristic missing: %s; RSSI: %s",
665
- self.name,
666
- ex,
667
- self.rssi,
668
- exc_info=True,
669
- )
670
- except BLEAK_RETRY_EXCEPTIONS:
671
- _LOGGER.debug("%s: communication failed with:", self.name, exc_info=True)
672
-
673
- @property
674
- def name(self) -> str:
675
- """Return device name."""
676
- return f"{self._device.name} ({self._device.address})"
677
-
678
- @property
679
- def rssi(self) -> int:
680
- """Return RSSI of device."""
681
- try:
682
- return self._mower.sys.toapp_report_data.connect.ble_rssi
683
- finally:
684
- return 0
685
-
686
- async def _ensure_connected(self):
687
- """Ensure connection to device is established."""
688
- if self._connect_lock.locked():
689
- _LOGGER.debug(
690
- "%s: Connection already in progress, waiting for it to complete; RSSI: %s",
691
- self.name,
692
- self.rssi,
693
- )
694
- if self._client and self._client.is_connected:
695
- _LOGGER.debug(
696
- "%s: Already connected before obtaining lock, resetting timer; RSSI: %s",
697
- self.name,
698
- self.rssi,
699
- )
700
- self._reset_disconnect_timer()
701
- return
702
- async with self._connect_lock:
703
- # Check again while holding the lock
704
- if self._client and self._client.is_connected:
705
- _LOGGER.debug(
706
- "%s: Already connected after obtaining lock, resetting timer; RSSI: %s",
707
- self.name,
708
- self.rssi,
709
- )
710
- self._reset_disconnect_timer()
711
- return
712
- _LOGGER.debug("%s: Connecting; RSSI: %s", self.name, self.rssi)
713
- client: BleakClientWithServiceCache = await establish_connection(
714
- BleakClientWithServiceCache,
715
- self._device,
716
- self.name,
717
- self._disconnected,
718
- max_attempts=10,
719
- ble_device_callback=lambda: self._device,
720
- )
721
- _LOGGER.debug("%s: Connected; RSSI: %s", self.name, self.rssi)
722
- self._client = client
723
- self._message = BleMessage(client)
724
-
725
- try:
726
- self._resolve_characteristics(client.services)
727
- except CharacteristicMissingError as ex:
728
- _LOGGER.debug(
729
- "%s: characteristic missing, clearing cache: %s; RSSI: %s",
730
- self.name,
731
- ex,
732
- self.rssi,
733
- exc_info=True,
734
- )
735
- await client.clear_cache()
736
- self._cancel_disconnect_timer()
737
- await self._execute_disconnect_with_lock()
738
- raise
739
-
740
- _LOGGER.debug(
741
- "%s: Starting notify and disconnect timer; RSSI: %s",
742
- self.name,
743
- self.rssi,
744
- )
745
- self._reset_disconnect_timer()
746
- await self._start_notify()
747
- command_bytes = self._commands.send_todev_ble_sync(2)
748
- await self._message.post_custom_data_bytes(command_bytes)
749
- self.schedule_ble_sync()
750
-
751
- async def _send_command_locked(self, key: str, command: bytes) -> bytes:
752
- """Send command to device and read response."""
753
- await self._ensure_connected()
754
- try:
755
- return await self._execute_command_locked(key, command)
756
- except BleakDBusError as ex:
757
- # Disconnect so we can reset state and try again
758
- await asyncio.sleep(DBUS_ERROR_BACKOFF_TIME)
759
- _LOGGER.debug(
760
- "%s: RSSI: %s; Backing off %ss; Disconnecting due to error: %s",
761
- self.name,
762
- self.rssi,
763
- DBUS_ERROR_BACKOFF_TIME,
764
- ex,
765
- )
766
- await self._execute_forced_disconnect()
767
- raise
768
- except BLEAK_RETRY_EXCEPTIONS as ex:
769
- # Disconnect so we can reset state and try again
770
- _LOGGER.debug("%s: RSSI: %s; Disconnecting due to error: %s", self.name, self.rssi, ex)
771
- await self._execute_forced_disconnect()
772
- raise
773
-
774
- async def _notification_handler(self, _sender: BleakGATTCharacteristic, data: bytearray) -> None:
775
- """Handle notification responses."""
776
- result = self._message.parseNotification(data)
777
- if result == 0:
778
- data = await self._message.parseBlufiNotifyData(True)
779
- self._update_raw_data(data)
780
- self._message.clearNotification()
781
- _LOGGER.debug("%s: Received notification: %s", self.name, data)
782
- else:
783
- return
784
- new_msg = LubaMsg().parse(data)
785
- if betterproto.serialized_on_wire(new_msg.net):
786
- if new_msg.net.todev_ble_sync != 0 or has_field(new_msg.net.toapp_wifi_iot_status):
787
- if has_field(new_msg.net.toapp_wifi_iot_status) and self._commands.get_device_product_key() == "":
788
- self._commands.set_device_product_key(new_msg.net.toapp_wifi_iot_status.productkey)
789
-
790
- return
791
-
792
- # may or may not be correct, some work could be done here to correctly match responses
793
- if self._notify_future and not self._notify_future.done():
794
- self._notify_future.set_result(data)
795
-
796
- self._reset_disconnect_timer()
797
- await self._state_manager.notification(new_msg)
798
-
799
- async def _start_notify(self) -> None:
800
- """Start notification."""
801
- _LOGGER.debug("%s: Subscribe to notifications; RSSI: %s", self.name, self.rssi)
802
- await self._client.start_notify(self._read_char, self._notification_handler)
803
-
804
- async def _execute_command_locked(self, key: str, command: bytes) -> bytes:
805
- """Execute command and read response."""
806
- assert self._client is not None
807
- assert self._read_char is not None
808
- assert self._write_char is not None
809
- self._notify_future = self.loop.create_future()
810
- self._key = key
811
- _LOGGER.debug("%s: Sending command: %s", self.name, key)
812
- await self._message.post_custom_data_bytes(command)
813
-
814
- timeout = 2
815
- timeout_handle = self.loop.call_at(self.loop.time() + timeout, _handle_timeout, self._notify_future)
816
- timeout_expired = False
817
- try:
818
- notify_msg = await self._notify_future
819
- except asyncio.TimeoutError:
820
- timeout_expired = True
821
- notify_msg = b""
822
- finally:
823
- if not timeout_expired:
824
- timeout_handle.cancel()
825
- self._notify_future = None
826
-
827
- _LOGGER.debug("%s: Notification received: %s", self.name, notify_msg.hex())
828
- return notify_msg
829
-
830
- async def _execute_command_locked_old(self, key: str, command: bytes) -> bytes:
831
- """Execute command and read response."""
832
- assert self._client is not None
833
- assert self._read_char is not None
834
- assert self._write_char is not None
835
- self._notify_future = self.loop.create_future()
836
- self._key = key
837
- _LOGGER.debug("%s: Sending command: %s", self.name, key)
838
- await self._message.post_custom_data_bytes(command)
839
-
840
- retry_handle = self.loop.call_at(
841
- self.loop.time() + 2,
842
- lambda: asyncio.ensure_future(
843
- _handle_retry(self._notify_future, self._message.post_custom_data_bytes, command)
844
- ),
845
- )
846
- timeout = 5
847
- timeout_handle = self.loop.call_at(self.loop.time() + timeout, _handle_timeout, self._notify_future)
848
- timeout_expired = False
849
- try:
850
- notify_msg = await self._notify_future
851
- except asyncio.TimeoutError:
852
- timeout_expired = True
853
- raise
854
- finally:
855
- if not timeout_expired:
856
- timeout_handle.cancel()
857
- retry_handle.cancel()
858
- self._notify_future = None
859
-
860
- _LOGGER.debug("%s: Notification received: %s", self.name, notify_msg.hex())
861
- return notify_msg
862
-
863
- def get_address(self) -> str:
864
- """Return address of device."""
865
- return self._device.address
866
-
867
- def _resolve_characteristics(self, services: BleakGATTServiceCollection) -> None:
868
- """Resolve characteristics."""
869
- self._read_char = services.get_characteristic(READ_CHAR_UUID)
870
- if not self._read_char:
871
- self._read_char = READ_CHAR_UUID
872
- _LOGGER.error(CharacteristicMissingError(READ_CHAR_UUID))
873
- self._write_char = services.get_characteristic(WRITE_CHAR_UUID)
874
- if not self._write_char:
875
- self._write_char = WRITE_CHAR_UUID
876
- _LOGGER.error(CharacteristicMissingError(WRITE_CHAR_UUID))
877
-
878
- def _reset_disconnect_timer(self):
879
- """Reset disconnect timer."""
880
- self._cancel_disconnect_timer()
881
- self._expected_disconnect = False
882
- self._disconnect_timer = self.loop.call_later(DISCONNECT_DELAY, self._disconnect_from_timer)
883
-
884
- def _disconnected(self, client: BleakClientWithServiceCache) -> None:
885
- """Disconnected callback."""
886
- if self._expected_disconnect:
887
- _LOGGER.debug("%s: Disconnected from device; RSSI: %s", self.name, self.rssi)
888
- return
889
- _LOGGER.warning(
890
- "%s: Device unexpectedly disconnected; RSSI: %s",
891
- self.name,
892
- self.rssi,
893
- )
894
- self._cancel_disconnect_timer()
895
-
896
- def _disconnect_from_timer(self):
897
- """Disconnect from device."""
898
- if self._operation_lock.locked() and self._client.is_connected:
899
- _LOGGER.debug(
900
- "%s: Operation in progress, resetting disconnect timer; RSSI: %s",
901
- self.name,
902
- self.rssi,
903
- )
904
- self._reset_disconnect_timer()
905
- return
906
- self._cancel_disconnect_timer()
907
- self._timed_disconnect_task = asyncio.create_task(self._execute_timed_disconnect())
908
-
909
- def _cancel_disconnect_timer(self):
910
- """Cancel disconnect timer."""
911
- if self._disconnect_timer:
912
- self._disconnect_timer.cancel()
913
- self._disconnect_timer = None
914
-
915
- async def _execute_forced_disconnect(self) -> None:
916
- """Execute forced disconnection."""
917
- self._cancel_disconnect_timer()
918
- _LOGGER.debug(
919
- "%s: Executing forced disconnect",
920
- self.name,
921
- )
922
- await self._execute_disconnect()
923
-
924
- async def _execute_timed_disconnect(self) -> None:
925
- """Execute timed disconnection."""
926
- if not self._disconnect_strategy:
927
- return
928
- _LOGGER.debug(
929
- "%s: Executing timed disconnect after timeout of %s",
930
- self.name,
931
- DISCONNECT_DELAY,
932
- )
933
- await self._execute_disconnect()
934
-
935
- async def _execute_disconnect(self) -> None:
936
- """Execute disconnection."""
937
- _LOGGER.debug("%s: Executing disconnect", self.name)
938
- async with self._connect_lock:
939
- await self._execute_disconnect_with_lock()
940
-
941
- async def _execute_disconnect_with_lock(self) -> None:
942
- """Execute disconnection while holding the lock."""
943
- assert self._connect_lock.locked(), "Lock not held"
944
- _LOGGER.debug("%s: Executing disconnect with lock", self.name)
945
- if self._disconnect_timer: # If the timer was reset, don't disconnect
946
- _LOGGER.debug("%s: Skipping disconnect as timer reset", self.name)
947
- return
948
- client = self._client
949
- self._expected_disconnect = True
950
-
951
- if not client:
952
- _LOGGER.debug("%s: Already disconnected", self.name)
953
- return
954
- _LOGGER.debug("%s: Disconnecting", self.name)
955
- try:
956
- """We reset what command the robot last heard before disconnecting."""
957
- if client is not None and client.is_connected:
958
- command_bytes = self._commands.send_todev_ble_sync(2)
959
- await self._message.post_custom_data_bytes(command_bytes)
960
- await client.stop_notify(self._read_char)
961
- await client.disconnect()
962
- except BLEAK_RETRY_EXCEPTIONS as ex:
963
- _LOGGER.warning(
964
- "%s: Error disconnecting: %s; RSSI: %s",
965
- self.name,
966
- ex,
967
- self.rssi,
968
- )
969
- else:
970
- _LOGGER.debug("%s: Disconnect completed successfully", self.name)
971
- self._client = None
972
-
973
- async def _disconnect(self) -> bool:
974
- if self._client is not None:
975
- return await self._client.disconnect()
976
-
977
- def set_disconnect_strategy(self, disconnect):
978
- self._disconnect_strategy = disconnect
979
-
980
-
981
- class MammotionBaseCloudDevice(MammotionBaseDevice):
982
- """Base class for Mammotion Cloud devices."""
983
-
984
- def __init__(self, mqtt_client: MammotionMQTT, cloud_device: Device, mowing_state: MowingDevice) -> None:
985
- """Initialize MammotionBaseCloudDevice."""
986
- super().__init__(mowing_state, cloud_device)
987
- self._ble_sync_task = None
988
- self.is_ready = False
989
- self.command_queue = asyncio.Queue()
990
- self._mqtt_client = mqtt_client
991
- self.iot_id = cloud_device.iotId
992
- self.device = cloud_device
993
- self._mower = mowing_state
994
- self._command_futures = {}
995
- self._commands: MammotionCommand = MammotionCommand(cloud_device.deviceName)
996
- self.currentID = ""
997
- self.on_ready_callback: Optional[Callable[[], Awaitable[None]]] = None
998
- self._waiting_queue = deque()
999
- self._operation_lock = asyncio.Lock()
1000
-
1001
- self._mqtt_client.on_connected = self.on_connected
1002
- self._mqtt_client.on_disconnected = self.on_disconnected
1003
- self._mqtt_client.on_message = self._on_mqtt_message
1004
- self._mqtt_client.on_ready = self.on_ready
1005
- if self._mqtt_client.is_connected:
1006
- self._ble_sync()
1007
- self.run_periodic_sync_task()
1008
-
1009
- # temporary for testing only
1010
- # self._start_sync_task = self.loop.call_later(30, lambda: asyncio.ensure_future(self.start_sync(0)))
1011
-
1012
- async def on_ready(self):
1013
- """Callback for when MQTT is subscribed to events."""
1014
- loop = asyncio.get_event_loop()
1015
-
1016
- await self._ble_sync()
1017
- await self.run_periodic_sync_task()
1018
- loop.create_task(self._process_queue())
1019
- if self.on_ready_callback:
1020
- await self.on_ready_callback()
1021
-
1022
- async def on_connected(self):
1023
- """Callback for when MQTT connects."""
1024
-
1025
- async def on_disconnected(self):
1026
- """Callback for when MQTT disconnects."""
1027
-
1028
- async def _ble_sync(self):
1029
- command_bytes = self._commands.send_todev_ble_sync(3)
1030
- loop = asyncio.get_running_loop()
1031
- await loop.run_in_executor(
1032
- None, self._mqtt_client.get_cloud_client().send_cloud_command, self.iot_id, command_bytes
1033
- )
1034
-
1035
- async def run_periodic_sync_task(self) -> None:
1036
- """Send ble sync to robot."""
1037
- try:
1038
- if not self._operation_lock.locked():
1039
- await self._ble_sync()
1040
- finally:
1041
- self.schedule_ble_sync()
1042
-
1043
- def schedule_ble_sync(self):
1044
- """Periodically sync to keep connection alive."""
1045
- if self._mqtt_client is not None and self._mqtt_client.is_connected:
1046
- self._ble_sync_task = self.loop.call_later(
1047
- 160, lambda: asyncio.ensure_future(self.run_periodic_sync_task())
1048
- )
1049
-
1050
- async def queue_command(self, key: str, **kwargs: any) -> bytes:
1051
- # Create a future to hold the result
1052
- _LOGGER.debug("Queueing command: %s", key)
1053
- future = asyncio.Future()
1054
- # Put the command in the queue as a tuple (key, command, future)
1055
- command_bytes = getattr(self._commands, key)(**kwargs)
1056
- await self.command_queue.put((key, command_bytes, future))
1057
- # Wait for the future to be resolved
1058
- return await future
1059
-
1060
- async def _process_queue(self):
1061
- while True:
1062
- # Get the next item from the queue
1063
- key, command, future = await self.command_queue.get()
1064
- try:
1065
- # Process the command using _execute_command_locked
1066
- result = await self._execute_command_locked(key, command)
1067
- # Set the result on the future
1068
- future.set_result(result)
1069
- except Exception as ex:
1070
- # Set the exception on the future if something goes wrong
1071
- future.set_exception(ex)
1072
- finally:
1073
- # Mark the task as done
1074
- self.command_queue.task_done()
1075
-
1076
- async def _on_mqtt_message(self, topic: str, payload: str, iot_id: str) -> None:
1077
- """Handle incoming MQTT messages."""
1078
- _LOGGER.debug("MQTT message received on topic %s: %s, iot_id: %s", topic, payload, iot_id)
1079
-
1080
- json_str = json.dumps(payload)
1081
- payload = json.loads(json_str)
1082
-
1083
- await self._handle_mqtt_message(topic, payload)
1084
-
1085
- async def _send_command(self, key: str, retry: int | None = None) -> bytes | None:
1086
- """Send command to device via MQTT and read response."""
1087
- if self._operation_lock.locked():
1088
- _LOGGER.debug("%s: Operation already in progress, waiting for it to complete;", self.device.nickName)
1089
- with self._operation_lock:
1090
- try:
1091
- command_bytes = getattr(self._commands, key)()
1092
- return await self._send_command_locked(key, command_bytes)
1093
- except Exception as ex:
1094
- _LOGGER.exception("%s: error in sending command - %s", self.device.nickName, ex)
1095
- raise
1096
-
1097
- async def _send_command_locked(self, key: str, command: bytes) -> bytes:
1098
- """Send command to device and read response."""
1099
- if not self._mqtt_client.is_connected:
1100
- loop = asyncio.get_running_loop()
1101
- await loop.run_in_executor(None, self._mqtt_client.connect_async)
1102
-
1103
- if not self._mqtt_client.is_connected:
1104
- raise Exception("MQTT not connected, couldn't recover")
1105
- try:
1106
- return await self._execute_command_locked(key, command)
1107
- except DeviceOfflineException as ex:
1108
- _LOGGER.debug(
1109
- "%s: device offline in _send_command_locked: %s",
1110
- self.device.nickName,
1111
- ex,
1112
- )
1113
- except SetupException as ex:
1114
- session = self._mqtt_client.get_cloud_client().get_session_by_authcode_response()
1115
- _LOGGER.debug(
1116
- "%s: session identityId mssing in _send_command_locked: %s",
1117
- self.device.nickName,
1118
- session,
1119
- )
1120
- if session.data.identityId is None:
1121
- await self._mqtt_client.get_cloud_client().session_by_auth_code()
1122
-
1123
- except Exception as ex:
1124
- _LOGGER.debug(
1125
- "%s: error in _send_command_locked: %s",
1126
- self.device.nickName,
1127
- ex,
1128
- )
1129
- raise
1130
-
1131
- async def _execute_command_locked(self, key: str, command: bytes) -> bytes:
1132
- """Execute command and read response."""
1133
- assert self._mqtt_client is not None
1134
- self._key = key
1135
- _LOGGER.debug("%s: Sending command: %s", self.device.nickName, key)
1136
-
1137
- await self.loop.run_in_executor(
1138
- None, self._mqtt_client.get_cloud_client().send_cloud_command, self.iot_id, command
1139
- )
1140
- future = MammotionFuture()
1141
- self._waiting_queue.append(future)
1142
- timeout = 5
1143
- try:
1144
- notify_msg = await future.async_get(timeout)
1145
- except asyncio.TimeoutError:
1146
- notify_msg = b""
1147
-
1148
- _LOGGER.debug("%s: Message received", self.device.nickName)
1149
-
1150
- return notify_msg
1151
-
1152
- async def _send_command_with_args(self, key: str, **kwargs: any) -> bytes | None:
1153
- """Send command with arguments to device via MQTT and read response."""
1154
- if self._operation_lock.locked():
1155
- _LOGGER.debug("%s: Operation already in progress, waiting for it to complete;", self.device.nickName)
1156
- with self._operation_lock:
1157
- try:
1158
- command_bytes = getattr(self._commands, key)(**kwargs)
1159
- return await self._send_command_locked(key, command_bytes)
1160
- except Exception as ex:
1161
- _LOGGER.exception("%s: error in sending command - %s", self.device.nickName, ex)
1162
- raise
1163
-
1164
- def _extract_message_id(self, payload: dict) -> str:
1165
- """Extract the message ID from the payload."""
1166
- return payload.get("id", "")
1167
-
1168
- def _extract_encoded_message(self, payload: dict) -> str:
1169
- """Extract the encoded message from the payload."""
1170
- try:
1171
- content = payload.get("data", {}).get("data", {}).get("params", {}).get("content", "")
1172
- return str(content)
1173
- except AttributeError:
1174
- _LOGGER.error("Error extracting encoded message. Payload: %s", payload)
1175
- return ""
1176
-
1177
- async def _parse_mqtt_response(self, topic: str, payload: dict) -> None:
1178
- """Parse the MQTT response."""
1179
- if topic.endswith("/app/down/thing/events"):
1180
- _LOGGER.debug("Thing event received")
1181
- event = ThingEventMessage.from_dicts(payload)
1182
- params = event.params
1183
- if params.get("identifier", None) is None:
1184
- return
1185
- if params.identifier == "device_protobuf_msg_event" and event.method == "thing.events":
1186
- _LOGGER.debug("Protobuf event")
1187
- binary_data = base64.b64decode(params.value.get("content", ""))
1188
- self._update_raw_data(cast(bytes, binary_data))
1189
- new_msg = LubaMsg().parse(cast(bytes, binary_data))
1190
-
1191
- if (
1192
- self._commands.get_device_product_key() == ""
1193
- and self._commands.get_device_name() == event.params.deviceName
1194
- ):
1195
- self._commands.set_device_product_key(event.params.productKey)
1196
-
1197
- if betterproto.serialized_on_wire(new_msg.net):
1198
- if new_msg.net.todev_ble_sync != 0 or has_field(new_msg.net.toapp_wifi_iot_status):
1199
- return
1200
-
1201
- if len(self._waiting_queue) > 0:
1202
- fut: MammotionFuture = self._waiting_queue.popleft()
1203
- while fut.fut.cancelled() and len(self._waiting_queue) > 0:
1204
- fut: MammotionFuture = self._waiting_queue.popleft()
1205
- if not fut.fut.cancelled():
1206
- fut.resolve(cast(bytes, binary_data))
1207
- await self._state_manager.notification(new_msg)
1208
- if event.method == "thing.properties":
1209
- _LOGGER.debug(event)
1210
-
1211
- async def _handle_mqtt_message(self, topic: str, payload: dict) -> None:
1212
- """Async handler for incoming MQTT messages."""
1213
- await self._parse_mqtt_response(topic=topic, payload=payload)
1214
-
1215
- def _disconnect(self):
1216
- """Disconnect the MQTT client."""
1217
- self._mqtt_client.disconnect()