pymammotion 0.2.7__py3-none-any.whl → 0.2.9__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.

@@ -10,6 +10,7 @@ import string
10
10
  import time
11
11
  import uuid
12
12
  from logging import getLogger, exception
13
+ from datetime import datetime
13
14
 
14
15
  from aiohttp import ClientSession
15
16
  from alibabacloud_iot_api_gateway.client import Client
@@ -48,6 +49,9 @@ MOVE_HEADERS = (
48
49
  class SetupException(Exception):
49
50
  pass
50
51
 
52
+ class AuthRefreshException(Exception):
53
+ """Raise exception when library cannot refresh token."""
54
+
51
55
 
52
56
  class CloudIOTGateway:
53
57
  """Class for interacting with Aliyun Cloud IoT Gateway."""
@@ -56,16 +60,18 @@ class CloudIOTGateway:
56
60
  _device_sn = ""
57
61
  _utdid = ""
58
62
 
59
- _connect_response = None
60
- _login_by_oauth_response = None
61
- _aep_response = None
62
- _session_by_authcode_response = None
63
- _listing_dev_by_account_response = None
64
- _region = None
63
+ _connect_response: ConnectResponse | None = None
64
+ _login_by_oauth_response: LoginByOAuthResponse | None = None
65
+ _aep_response: AepResponse | None = None
66
+ _session_by_authcode_response: SessionByAuthCodeResponse | None = None
67
+ _devices_by_account_response: ListingDevByAccountResponse | None = None
68
+ _region_response = None
69
+
70
+ _iot_token_issued_at : int = None
65
71
 
66
72
  converter = DatatypeConverter()
67
73
 
68
- def __init__(self):
74
+ def __init__(self, connect_response: ConnectResponse | None = None, login_by_oauth_response: LoginByOAuthResponse | None = None, aep_response: AepResponse | None = None, session_by_authcode_response: SessionByAuthCodeResponse | None = None, region_response: RegionResponse | None = None, dev_by_account: ListingDevByAccountResponse | None = None):
69
75
  """Initialize the CloudIOTGateway."""
70
76
  self._app_key = APP_KEY
71
77
  self._app_secret = APP_SECRET
@@ -74,6 +80,12 @@ class CloudIOTGateway:
74
80
  self._client_id = self.generate_hardware_string(8) # 8 characters
75
81
  self._device_sn = self.generate_hardware_string(32) # 32 characters
76
82
  self._utdid = self.generate_hardware_string(32) # 32 characters
83
+ self._connect_response = connect_response
84
+ self._login_by_oauth_response = login_by_oauth_response
85
+ self._aep_response = aep_response
86
+ self._session_by_authcode_response = session_by_authcode_response
87
+ self._region_response = region_response
88
+ self._devices_by_account_response = dev_by_account
77
89
 
78
90
  @staticmethod
79
91
  def generate_random_string(length):
@@ -102,6 +114,24 @@ class CloudIOTGateway:
102
114
  hashlib.sha1,
103
115
  ).hexdigest()
104
116
 
117
+ def get_connect_response(self):
118
+ return self._connect_response
119
+
120
+ def get_login_by_oauth_response(self):
121
+ return self._login_by_oauth_response
122
+
123
+ def get_aep_response(self):
124
+ return self._aep_response
125
+
126
+ def get_session_by_authcode_response(self):
127
+ return self._session_by_authcode_response
128
+
129
+ def get_devices_by_account_response(self):
130
+ return self._devices_by_account_response
131
+
132
+ def get_region_response(self):
133
+ return self._region_response
134
+
105
135
  def get_region(self, country_code: str, auth_code: str):
106
136
  """Get the region based on country code and auth code."""
107
137
  config = Config(
@@ -140,8 +170,8 @@ class CloudIOTGateway:
140
170
  if int(response_body_dict.get("code")) != 200:
141
171
  raise Exception("Error in getting regions: " + response_body_dict["msg"])
142
172
 
143
- self._region = RegionResponse.from_dict(response_body_dict)
144
- logger.debug("Endpoint: %s", self._region.data.mqttEndpoint)
173
+ self._region_response = RegionResponse.from_dict(response_body_dict)
174
+ logger.debug("Endpoint: %s", self._region_response.data.mqttEndpoint)
145
175
 
146
176
  return response.body
147
177
 
@@ -149,8 +179,8 @@ class CloudIOTGateway:
149
179
  """Handle AEP authentication."""
150
180
  aep_domain = self.domain
151
181
 
152
- if self._region.data.apiGatewayEndpoint is not None:
153
- aep_domain = self._region.data.apiGatewayEndpoint
182
+ if self._region_response.data.apiGatewayEndpoint is not None:
183
+ aep_domain = self._region_response.data.apiGatewayEndpoint
154
184
 
155
185
  config = Config(
156
186
  app_key=self._app_key,
@@ -275,7 +305,7 @@ class CloudIOTGateway:
275
305
 
276
306
  async def login_by_oauth(self, country_code: str, auth_code: str):
277
307
  """Login by OAuth."""
278
- region_url = self._region.data.oaApiGatewayEndpoint
308
+ region_url = self._region_response.data.oaApiGatewayEndpoint
279
309
 
280
310
  async with ClientSession() as session:
281
311
  headers = {
@@ -347,7 +377,7 @@ class CloudIOTGateway:
347
377
  config = Config(
348
378
  app_key=self._app_key,
349
379
  app_secret=self._app_secret,
350
- domain=self._region.data.apiGatewayEndpoint,
380
+ domain=self._region_response.data.apiGatewayEndpoint,
351
381
  )
352
382
  client = Client(config)
353
383
 
@@ -390,15 +420,17 @@ class CloudIOTGateway:
390
420
  raise Exception("Error in creating session: " + response_body_dict["msg"])
391
421
 
392
422
  self._session_by_authcode_response = SessionByAuthCodeResponse.from_dict(response_body_dict)
423
+ self._iot_token_issued_at = int(time.time())
393
424
 
394
425
  return response.body
395
426
 
396
427
  def check_or_refresh_session(self):
397
428
  """Check or refresh the session."""
429
+ logger.debug("Try to refresh token")
398
430
  config = Config(
399
431
  app_key=self._app_key,
400
432
  app_secret=self._app_secret,
401
- domain=self._region.data.apiGatewayEndpoint,
433
+ domain=self._region_response.data.apiGatewayEndpoint,
402
434
  )
403
435
  client = Client(config)
404
436
 
@@ -431,19 +463,36 @@ class CloudIOTGateway:
431
463
  logger.debug(response.status_code)
432
464
  logger.debug(response.body)
433
465
 
434
- # self._region = response.body.data
435
- # Decodifica il corpo della risposta
466
+ # Decode the response body
436
467
  response_body_str = response.body.decode("utf-8")
437
468
 
438
- # Carica la stringa JSON in un dizionario
439
- json.loads(response_body_str)
469
+ # Load the JSON string into a dictionary
470
+ response_body_dict = json.loads(response_body_str)
471
+
472
+ if int(response_body_dict.get("code")) != 200:
473
+ raise Exception("Error check or refresh token: " + response_body_dict.get('msg', ''))
474
+
475
+ identityId = response_body_dict.get('data', {}).get('identityId', None)
476
+ refreshTokenExpire = response_body_dict.get('data', {}).get('refreshTokenExpire', None)
477
+ iotToken = response_body_dict.get('data', {}).get('iotToken', None)
478
+ iotTokenExpire = response_body_dict.get('data', {}).get('iotTokenExpire', None)
479
+ refreshToken = response_body_dict.get('data', {}).get('refreshToken', None)
480
+
481
+
482
+ if (identityId is None or refreshTokenExpire is None or iotToken is None or iotTokenExpire is None or refreshToken is None):
483
+ raise Exception("Error check or refresh token: Parameters not correct")
484
+
485
+ self._session_by_authcode_response = SessionByAuthCodeResponse.from_dict(response_body_dict)
486
+ self._iot_token_issued_at = int(time.time())
487
+
488
+
440
489
 
441
490
  def list_binding_by_account(self) -> ListingDevByAccountResponse:
442
491
  """List bindings by account."""
443
492
  config = Config(
444
493
  app_key=self._app_key,
445
494
  app_secret=self._app_secret,
446
- domain=self._region.data.apiGatewayEndpoint,
495
+ domain=self._region_response.data.apiGatewayEndpoint,
447
496
  )
448
497
 
449
498
  client = Client(config)
@@ -477,15 +526,26 @@ class CloudIOTGateway:
477
526
  if int(response_body_dict.get("code")) != 200:
478
527
  raise Exception("Error in creating session: " + response_body_dict["msg"])
479
528
 
480
- self._listing_dev_by_account_response = ListingDevByAccountResponse.from_dict(response_body_dict)
481
- return self._listing_dev_by_account_response
529
+ self._devices_by_account_response = ListingDevByAccountResponse.from_dict(response_body_dict)
530
+ return self._devices_by_account_response
482
531
 
483
532
  def send_cloud_command(self, iot_id: str, command: bytes) -> str:
484
533
  """Send a cloud command to the specified IoT device."""
534
+
535
+ """Check if iotToken is expired"""
536
+ if self._iot_token_issued_at + self._session_by_authcode_response.data.iotTokenExpire <= (int(time.time()) + (5 * 3600)):
537
+ """Token expired - Try to refresh - Check if refreshToken is not expired"""
538
+ if self._iot_token_issued_at + self._session_by_authcode_response.data.refreshTokenExpire > (int(time.time())):
539
+ self.check_or_refresh_session()
540
+ else:
541
+ raise AuthRefreshException("Refresh token expired. Please re-login")
542
+
543
+
544
+
485
545
  config = Config(
486
546
  app_key=self._app_key,
487
547
  app_secret=self._app_secret,
488
- domain=self._region.data.apiGatewayEndpoint,
548
+ domain=self._region_response.data.apiGatewayEndpoint,
489
549
  )
490
550
 
491
551
  client = Client(config)
@@ -537,4 +597,4 @@ class CloudIOTGateway:
537
597
 
538
598
  @property
539
599
  def listing_dev_by_account_response(self):
540
- return self._listing_dev_by_account_response
600
+ return self._devices_by_account_response
@@ -1,18 +1,18 @@
1
- from dataclasses import dataclass
1
+ from dataclasses import dataclass, field
2
+ from datetime import time
2
3
 
3
4
  from mashumaro.mixins.orjson import DataClassORJSONMixin
4
5
 
5
6
 
6
7
  @dataclass
7
- class TokenData(DataClassORJSONMixin):
8
+ class SessionOauthToken(DataClassORJSONMixin):
8
9
  identityId: str
9
10
  refreshTokenExpire: int
10
11
  iotToken: str
11
12
  iotTokenExpire: int
12
13
  refreshToken: str
13
14
 
14
-
15
15
  @dataclass
16
16
  class SessionByAuthCodeResponse(DataClassORJSONMixin):
17
17
  code: int
18
- data: TokenData
18
+ data: SessionOauthToken
@@ -21,10 +21,6 @@ class Dock(Point):
21
21
 
22
22
  rotation: int = 0
23
23
 
24
- def __init__(self):
25
- super().__init__()
26
- self.rotation = 0
27
-
28
24
 
29
25
  @dataclass
30
26
  class Location:
@@ -1,24 +1,22 @@
1
1
  """Manage state from notifications into MowingDevice."""
2
+ from typing import Optional, Callable, Awaitable
2
3
 
3
4
  import betterproto
4
5
 
5
6
  from pymammotion.data.model.device import MowingDevice
6
- from pymammotion.event.event import DataEvent
7
7
  from pymammotion.proto.luba_msg import LubaMsg
8
- from pymammotion.proto.mctrl_nav import NavGetCommDataAck
8
+ from pymammotion.proto.mctrl_nav import NavGetCommDataAck, NavGetHashListAck
9
9
 
10
10
 
11
11
  class StateManager:
12
12
  """Manage state."""
13
13
 
14
14
  _device: MowingDevice
15
- gethash_ack_callback: DataEvent
16
- get_commondata_ack_callback: DataEvent
17
15
 
18
16
  def __init__(self, device: MowingDevice):
19
17
  self._device = device
20
- self.gethash_ack_callback = DataEvent()
21
- self.get_commondata_ack_callback = DataEvent()
18
+ self.gethash_ack_callback: Optional[Callable[[NavGetHashListAck],Awaitable[None]]] = None
19
+ self.get_commondata_ack_callback: Optional[Callable[[NavGetCommDataAck],Awaitable[None]]] = None
22
20
 
23
21
  def get_device(self) -> MowingDevice:
24
22
  """Get device."""
@@ -54,12 +52,12 @@ class StateManager:
54
52
  self._device.map.obstacle = dict()
55
53
  self._device.map.area = dict()
56
54
  self._device.map.path = dict()
57
- await self.gethash_ack_callback.data_event(nav_msg[1])
55
+ await self.gethash_ack_callback(nav_msg[1])
58
56
  case "toapp_get_commondata_ack":
59
57
  common_data: NavGetCommDataAck = nav_msg[1]
60
58
  updated = self._device.map.update(common_data)
61
59
  if updated:
62
- await self.get_commondata_ack_callback.data_event(common_data)
60
+ await self.get_commondata_ack_callback(common_data)
63
61
 
64
62
  def _update_sys_data(self, message):
65
63
  """Update system."""
@@ -13,7 +13,7 @@ from abc import abstractmethod
13
13
  from collections import deque
14
14
  from enum import Enum
15
15
  from functools import cache
16
- from typing import Any, Callable, Optional, cast
16
+ from typing import Any, Callable, Optional, cast, Awaitable
17
17
  from uuid import UUID
18
18
 
19
19
  import betterproto
@@ -212,7 +212,8 @@ class Mammotion(object):
212
212
  """Represents a Mammotion device."""
213
213
 
214
214
  devices = MammotionDevices()
215
- _mammotion_mqtt: MammotionMQTT | None = None
215
+ cloud_client: CloudIOTGateway | None = None
216
+ mqtt: MammotionMQTT | None = None
216
217
 
217
218
 
218
219
 
@@ -229,25 +230,25 @@ class Mammotion(object):
229
230
  self._preference = preference
230
231
 
231
232
  async def initiate_cloud_connection(self, cloud_client: CloudIOTGateway) -> None:
232
- if self._mammotion_mqtt is not None:
233
- if self._mammotion_mqtt.is_connected:
233
+ if self.mqtt is not None:
234
+ if self.mqtt.is_connected:
234
235
  return
235
236
 
236
-
237
- self._mammotion_mqtt = MammotionMQTT(region_id=cloud_client._region.data.regionId,
237
+ self.cloud_client = cloud_client
238
+ self.mqtt = MammotionMQTT(region_id=cloud_client._region_response.data.regionId,
238
239
  product_key=cloud_client._aep_response.data.productKey,
239
240
  device_name=cloud_client._aep_response.data.deviceName,
240
241
  device_secret=cloud_client._aep_response.data.deviceSecret,
241
242
  iot_token=cloud_client._session_by_authcode_response.data.iotToken,
242
243
  client_id=cloud_client._client_id)
243
244
 
244
- self._mammotion_mqtt._cloud_client = cloud_client
245
+ self.mqtt._cloud_client = cloud_client
245
246
  loop = asyncio.get_running_loop()
246
- await loop.run_in_executor(None, self._mammotion_mqtt.connect_async)
247
+ await loop.run_in_executor(None, self.mqtt.connect_async)
247
248
 
248
249
  for device in cloud_client.listing_dev_by_account_response.data.data:
249
250
  if device.deviceName.startswith(("Luba-", "Yuka-")):
250
- self.devices.add_device(MammotionMixedDeviceManager(name=device.deviceName, cloud_device=device, mqtt=self._mammotion_mqtt))
251
+ self.devices.add_device(MammotionMixedDeviceManager(name=device.deviceName, cloud_device=device, mqtt=self.mqtt))
251
252
 
252
253
  def set_disconnect_strategy(self, disconnect: bool):
253
254
  for device_name, device in self.devices.devices:
@@ -331,22 +332,24 @@ class MammotionBaseDevice:
331
332
 
332
333
  _mower: MowingDevice
333
334
  _state_manager: StateManager
335
+ _cloud_device: Device | None = None
334
336
 
335
- def __init__(self, device: MowingDevice) -> None:
337
+ def __init__(self, device: MowingDevice, cloud_device: Device | None = None) -> None:
336
338
  """Initialize MammotionBaseDevice."""
337
339
  self.loop = asyncio.get_event_loop()
338
340
  self._raw_data = LubaMsg().to_dict(casing=betterproto.Casing.SNAKE)
339
341
  self._mower = device
340
342
  self._state_manager = StateManager(self._mower)
341
- self._state_manager.gethash_ack_callback.add_subscribers(self.datahash_response)
342
- self._state_manager.get_commondata_ack_callback.add_subscribers(self.commdata_response)
343
+ self._state_manager.gethash_ack_callback = self.datahash_response
344
+ self._state_manager.get_commondata_ack_callback = self.commdata_response
343
345
  self._notify_future: asyncio.Future[bytes] | None = None
346
+ self._cloud_device = cloud_device
344
347
 
345
348
  async def datahash_response(self, hash_ack: NavGetHashListAck):
346
349
  """Handle datahash responses."""
347
350
  result_hash = 0
348
351
  while hash_ack.data_couple[0] != result_hash:
349
- data = await self._send_command_with_args("synchronize_hash_data", hash_num=hash_ack.data_couple[0])
352
+ data = await self.queue_command("synchronize_hash_data", hash_num=hash_ack.data_couple[0])
350
353
  msg = LubaMsg().parse(data)
351
354
  if betterproto.serialized_on_wire(msg.nav.toapp_get_commondata_ack):
352
355
  result_hash = msg.nav.toapp_get_commondata_ack.hash
@@ -364,7 +367,7 @@ class MammotionBaseDevice:
364
367
  return
365
368
  result_hash = 0
366
369
  while data_hash != result_hash:
367
- data = await self._send_command_with_args("synchronize_hash_data", hash_num=data_hash)
370
+ data = await self.queue_command("synchronize_hash_data", hash_num=data_hash)
368
371
  msg = LubaMsg().parse(data)
369
372
  if betterproto.serialized_on_wire(msg.nav.toapp_get_commondata_ack):
370
373
  result_hash = msg.nav.toapp_get_commondata_ack.hash
@@ -376,7 +379,7 @@ class MammotionBaseDevice:
376
379
  region_data.type = common_data.type
377
380
  region_data.total_frame = total_frame
378
381
  region_data.current_frame = current_frame
379
- await self._send_command_with_args("get_regional_data", regional_data=region_data)
382
+ await self.queue_command("get_regional_data", regional_data=region_data)
380
383
 
381
384
  def _update_raw_data(self, data: bytes) -> None:
382
385
  """Update raw and model data from notifications."""
@@ -474,6 +477,10 @@ class MammotionBaseDevice:
474
477
  """Get the LubaMsg of the device."""
475
478
  return self._mower
476
479
 
480
+ @abstractmethod
481
+ async def queue_command(self, key: str, **kwargs: any) -> bytes:
482
+ """Queue commands to mower."""
483
+
477
484
  @abstractmethod
478
485
  async def _send_command(self, key: str, retry: int | None = None) -> bytes | None:
479
486
  """Send command to device and read response."""
@@ -504,9 +511,15 @@ class MammotionBaseDevice:
504
511
 
505
512
  await self._send_command_with_args("get_hash_response", total_frame=1, current_frame=1)
506
513
 
507
- await self._send_command_with_args(
508
- "get_area_name_list", device_id=self._mower.device.net.toapp_wifi_iot_status.devicename
509
- )
514
+ if self._cloud_device:
515
+ await self._send_command_with_args(
516
+ "get_area_name_list", device_id=self._cloud_device.deviceName
517
+ )
518
+ if has_field(self._mower.net.toapp_wifi_iot_status):
519
+ await self._send_command_with_args(
520
+ "get_area_name_list", device_id=self._mower.net.toapp_wifi_iot_status.devicename
521
+ )
522
+
510
523
 
511
524
  # sub_cmd 3 is job hashes??
512
525
  # sub_cmd 4 is dump location (yuka)
@@ -585,6 +598,9 @@ class MammotionBaseBLEDevice(MammotionBaseDevice):
585
598
  130, lambda: asyncio.ensure_future(self.run_periodic_sync_task())
586
599
  )
587
600
 
601
+ async def queue_command(self, key: str, **kwargs: any) -> bytes | None:
602
+ return await self._send_command_with_args(key, **kwargs)
603
+
588
604
  async def _send_command_with_args(self, key: str, **kwargs) -> bytes | None:
589
605
  """Send command to device and read response."""
590
606
  if self._operation_lock.locked():
@@ -654,7 +670,7 @@ class MammotionBaseBLEDevice(MammotionBaseDevice):
654
670
  def rssi(self) -> int:
655
671
  """Return RSSI of device."""
656
672
  try:
657
- return self._mower.device.sys.toapp_report_data.connect.ble_rssi
673
+ return self._mower.sys.toapp_report_data.connect.ble_rssi
658
674
  finally:
659
675
  return 0
660
676
 
@@ -937,9 +953,11 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
937
953
  mowing_state: MowingDevice
938
954
  ) -> None:
939
955
  """Initialize MammotionBaseCloudDevice."""
940
- super().__init__(mowing_state)
956
+ super().__init__(mowing_state, cloud_device)
941
957
  self._ble_sync_task = None
942
958
  self.is_ready = False
959
+ self.command_queue = asyncio.Queue()
960
+ self.processing_task = asyncio.create_task(self._process_queue())
943
961
  self._mqtt_client = mqtt_client
944
962
  self.iot_id = cloud_device.iotId
945
963
  self.device = cloud_device
@@ -947,8 +965,9 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
947
965
  self._command_futures = {}
948
966
  self._commands: MammotionCommand = MammotionCommand(cloud_device.deviceName)
949
967
  self.currentID = ""
950
- self.on_ready_callback: Optional[Callable[[], None]] = None
968
+ self.on_ready_callback: Optional[Callable[[], Awaitable[None]]] = None
951
969
  self._waiting_queue = deque()
970
+ self._operation_lock = threading.Lock()
952
971
 
953
972
  self._mqtt_client.on_connected = self.on_connected
954
973
  self._mqtt_client.on_disconnected = self.on_disconnected
@@ -964,7 +983,7 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
964
983
  async def on_ready(self):
965
984
  """Callback for when MQTT is subscribed to events."""
966
985
  if self.on_ready_callback:
967
- self.on_ready_callback()
986
+ await self.on_ready_callback()
968
987
 
969
988
  await self._ble_sync()
970
989
  await self.run_periodic_sync_task()
@@ -996,9 +1015,35 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
996
1015
  160, lambda: asyncio.ensure_future(self.run_periodic_sync_task())
997
1016
  )
998
1017
 
1018
+ async def queue_command(self, key: str, **kwargs: any) -> bytes:
1019
+ # Create a future to hold the result
1020
+ _LOGGER.debug("Queueing command: %s", key)
1021
+ future = asyncio.Future()
1022
+ # Put the command in the queue as a tuple (key, command, future)
1023
+ command_bytes = getattr(self._commands, key)(**kwargs)
1024
+ await self.command_queue.put((key, command_bytes, future))
1025
+ # Wait for the future to be resolved
1026
+ return await future
1027
+
1028
+ async def _process_queue(self):
1029
+ while True:
1030
+ # Get the next item from the queue
1031
+ key, command, future = await self.command_queue.get()
1032
+ try:
1033
+ # Process the command using _execute_command_locked
1034
+ result = await self._execute_command_locked(key, command)
1035
+ # Set the result on the future
1036
+ future.set_result(result)
1037
+ except Exception as ex:
1038
+ # Set the exception on the future if something goes wrong
1039
+ future.set_exception(ex)
1040
+ finally:
1041
+ # Mark the task as done
1042
+ self.command_queue.task_done()
1043
+
999
1044
  async def _on_mqtt_message(self, topic: str, payload: str, iot_id: str) -> None:
1000
1045
  """Handle incoming MQTT messages."""
1001
- _LOGGER.debug("MQTT message received on topic %s: %s", topic, payload, iot_id)
1046
+ _LOGGER.debug("MQTT message received on topic %s: %s, iot_id: %s", topic, payload, iot_id)
1002
1047
 
1003
1048
  json_str = json.dumps(payload)
1004
1049
  payload = json.loads(json_str)
@@ -1007,13 +1052,18 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
1007
1052
 
1008
1053
  async def _send_command(self, key: str, retry: int | None = None) -> bytes | None:
1009
1054
  """Send command to device via MQTT and read response."""
1010
-
1011
- try:
1012
- command_bytes = getattr(self._commands, key)()
1013
- return await self._send_command_locked(key, command_bytes)
1014
- except Exception as ex:
1015
- _LOGGER.exception("%s: error in sending command - %s", self.device.nickName, ex)
1016
- raise
1055
+ if self._operation_lock.locked():
1056
+ _LOGGER.debug(
1057
+ "%s: Operation already in progress, waiting for it to complete;",
1058
+ self.device.nickName
1059
+ )
1060
+ with self._operation_lock:
1061
+ try:
1062
+ command_bytes = getattr(self._commands, key)()
1063
+ return await self._send_command_locked(key, command_bytes)
1064
+ except Exception as ex:
1065
+ _LOGGER.exception("%s: error in sending command - %s", self.device.nickName, ex)
1066
+ raise
1017
1067
 
1018
1068
  async def _send_command_locked(self, key: str, command: bytes) -> bytes:
1019
1069
  """Send command to device and read response."""
@@ -1049,12 +1099,18 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
1049
1099
 
1050
1100
  async def _send_command_with_args(self, key: str, **kwargs: any) -> bytes | None:
1051
1101
  """Send command with arguments to device via MQTT and read response."""
1052
- try:
1053
- command_bytes = getattr(self._commands, key)(**kwargs)
1054
- return await self._send_command_locked(key, command_bytes)
1055
- except Exception as ex:
1056
- _LOGGER.exception("%s: error in sending command - %s", self.device.nickName, ex)
1057
- raise
1102
+ if self._operation_lock.locked():
1103
+ _LOGGER.debug(
1104
+ "%s: Operation already in progress, waiting for it to complete;",
1105
+ self.device.nickName
1106
+ )
1107
+ with self._operation_lock:
1108
+ try:
1109
+ command_bytes = getattr(self._commands, key)(**kwargs)
1110
+ return await self._send_command_locked(key, command_bytes)
1111
+ except Exception as ex:
1112
+ _LOGGER.exception("%s: error in sending command - %s", self.device.nickName, ex)
1113
+ raise
1058
1114
 
1059
1115
  def _extract_message_id(self, payload: dict) -> str:
1060
1116
  """Extract the message ID from the payload."""
@@ -1091,6 +1147,8 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
1091
1147
 
1092
1148
  if len(self._waiting_queue) > 0:
1093
1149
  fut: MammotionFuture = self._waiting_queue.popleft()
1150
+ while fut.fut.cancelled():
1151
+ fut: MammotionFuture = self._waiting_queue.popleft()
1094
1152
  fut.resolve(cast(bytes, binary_data))
1095
1153
  await self._state_manager.notification(new_msg)
1096
1154
 
@@ -1102,3 +1160,4 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
1102
1160
  """Disconnect the MQTT client."""
1103
1161
  self._mqtt_client.disconnect()
1104
1162
 
1163
+
@@ -57,7 +57,7 @@ class MammotionMQTT:
57
57
  ).hexdigest()
58
58
 
59
59
  self._client_id = client_id
60
- self.loop = asyncio.get_event_loop()
60
+ self.loop = asyncio.get_running_loop()
61
61
 
62
62
  self._linkkit_client = LinkKit(
63
63
  region_id,
@@ -80,6 +80,8 @@ class MammotionMQTT:
80
80
  def connect_async(self):
81
81
  """Connect async to MQTT Server."""
82
82
  logger.info("Connecting...")
83
+ if self._linkkit_client.check_state() is LinkKit.LinkKitState.INITIALIZED:
84
+ self._linkkit_client.thing_setup()
83
85
  self._linkkit_client.connect_async()
84
86
 
85
87
 
@@ -121,8 +123,7 @@ class MammotionMQTT:
121
123
 
122
124
  if self.on_ready:
123
125
  self.is_ready = True
124
- future = asyncio.run_coroutine_threadsafe(self.on_ready(), self.loop)
125
- asyncio.wrap_future(future, loop=self.loop)
126
+ asyncio.run_coroutine_threadsafe(self.on_ready(), self.loop).result()
126
127
  # self._linkkit_client.query_ota_firmware()
127
128
  # command = MammotionCommand(device_name="Luba")
128
129
  # self._cloud_client.send_cloud_command(command.get_report_cfg())
@@ -139,7 +140,7 @@ class MammotionMQTT:
139
140
  iot_id = payload.get("params", {}).get("iotId", "")
140
141
  if iot_id != "" and self.on_message:
141
142
  future = asyncio.run_coroutine_threadsafe(self.on_message(topic, payload, iot_id), self.loop)
142
- return asyncio.wrap_future(future, loop=self.loop)
143
+ asyncio.wrap_future(future, loop=self.loop)
143
144
 
144
145
 
145
146
  def _thing_on_connect(self, session_flag, rc, user_data):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pymammotion
3
- Version: 0.2.7
3
+ Version: 0.2.9
4
4
  Summary:
5
5
  License: GNU-3.0
6
6
  Author: Michael Arthur
@@ -1,13 +1,13 @@
1
1
  pymammotion/__init__.py,sha256=kmnjdt3AEMejIz5JK7h1tTJj5ZriAgKwZBa3ScA4-Ao,1516
2
2
  pymammotion/aliyun/__init__.py,sha256=T1lkX7TRYiL4nqYanG4l4MImV-SlavSbuooC-W-uUGw,29
3
- pymammotion/aliyun/cloud_gateway.py,sha256=ZjGSRdfxujOEHgL2gr8rZsGsYlJtGdS2YWGEWLXaEhg,18681
3
+ pymammotion/aliyun/cloud_gateway.py,sha256=IqmxgjDIgRoZvJvQnwYGOSl4XaCV4MoMKTlfhC9-6Ys,21830
4
4
  pymammotion/aliyun/cloud_service.py,sha256=YWcKuKK6iRWy5mTnBYgHxcCusiRGGzQt3spSf7dGDss,2183
5
5
  pymammotion/aliyun/dataclass/aep_response.py,sha256=8f6GIP58ve8gd6AL3HBoXxsy0n2q4ygWvjELGnoOnVc,452
6
6
  pymammotion/aliyun/dataclass/connect_response.py,sha256=Yz-fEbDzgGPTo5Of2oAjmFkSv08T7ze80pQU4k-gKIU,824
7
7
  pymammotion/aliyun/dataclass/dev_by_account_response.py,sha256=DtGeMCzoiFrfSniHoL6db9xcheQgmcTvGySkGBjKdKE,977
8
8
  pymammotion/aliyun/dataclass/login_by_oauth_response.py,sha256=6TQYAMyQ1jf_trsnTST007qlNXve3BqsvpV0Dwp7CFA,1245
9
9
  pymammotion/aliyun/dataclass/regions_response.py,sha256=CVPpdFhDD6_emWHyLRzOdp2j3HLPtP8tlNyzGnr8AcI,690
10
- pymammotion/aliyun/dataclass/session_by_authcode_response.py,sha256=wLGSX2nHkA7crmyYeE_dYly_lDtoYWAiIZjQ0C6m44o,358
10
+ pymammotion/aliyun/dataclass/session_by_authcode_response.py,sha256=_5vsyIxocoecEqygPTmhOQnzjwaK845ssIqbTeaLcmk,406
11
11
  pymammotion/aliyun/tmp_constant.py,sha256=M4Hq_lrGB3LZdX6R2XohRPFoK1NDnNV-pTJwJcJ9838,6650
12
12
  pymammotion/bluetooth/__init__.py,sha256=LAl8jqZ1fPh-3mLmViNQsP3s814C1vsocYUa6oSaXt0,36
13
13
  pymammotion/bluetooth/ble.py,sha256=9fA8yw5xXNJRSbC68SdOMf2QJnHoD7RhhCjLFoF0c0I,2404
@@ -28,7 +28,7 @@ pymammotion/data/model/excute_boarder_params.py,sha256=kadSth4y-VXlXIZ6R-Ng-kDvB
28
28
  pymammotion/data/model/execute_boarder.py,sha256=oDb2h5tFtOQIa8OCNYaDugqCgCZBLjQRzQTNVcJVAGQ,1072
29
29
  pymammotion/data/model/generate_route_information.py,sha256=5w1MM1-gXGXb_AoEap_I5xTxAFbNSzXuqQFEkw4NmDs,4301
30
30
  pymammotion/data/model/hash_list.py,sha256=z4y0mzbW8LQ5nsaHbMlt1y6uS4suB4D_VljAyIEjFR0,1171
31
- pymammotion/data/model/location.py,sha256=mXiJUnD2AIpcN7HgV8OLVL3QFLNAiVWqaVDFGmDjFXU,807
31
+ pymammotion/data/model/location.py,sha256=qO3G0U_eWP9alswbZXTpmYImIcXJeteBVHX1cGZGbHg,729
32
32
  pymammotion/data/model/mowing_modes.py,sha256=2GAF-xaHpv6CSGobYoNtfSi2if8_jW0nonCqN50SNx0,665
33
33
  pymammotion/data/model/plan.py,sha256=7JvqAo0a9Yg1Vtifd4J3Dx3StEppxrMOfmq2-877kYg,2891
34
34
  pymammotion/data/model/rapid_state.py,sha256=_e9M-65AbkvIqXyMYzLKBxbNvpso42qD8R-JSt66THY,986
@@ -38,7 +38,7 @@ pymammotion/data/mqtt/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdr
38
38
  pymammotion/data/mqtt/event.py,sha256=7hq-dl-HdKJB-TQ2kKfYfrebR_8qWE_lF2nQLlPyphU,3437
39
39
  pymammotion/data/mqtt/properties.py,sha256=HkBPghr26L9_b4QaOi1DtPgb0UoPIOGSe9wb3kgnM6Y,2815
40
40
  pymammotion/data/mqtt/status.py,sha256=zqnlo-MzejEQZszl0i0Wucoc3E76x6UtI9JLxoBnu54,1067
41
- pymammotion/data/state_manager.py,sha256=ItihtkmJCeIdXOYD3YRBp241cP29oMABbSISyC5A7tw,2805
41
+ pymammotion/data/state_manager.py,sha256=mNIAhP8f1Cb6-wFm2R5e-NlZauj-71iDGz8oZvdTq3Y,2826
42
42
  pymammotion/event/__init__.py,sha256=mgATR6vPHACNQ-0zH5fi7NdzeTCDV1CZyaWPmtUusi8,115
43
43
  pymammotion/event/event.py,sha256=Fy5-I1p92AO_D67VW4eHQqA4pOt7MZsrP--tVfIVUz8,1820
44
44
  pymammotion/http/_init_.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -58,10 +58,10 @@ pymammotion/mammotion/commands/messages/video.py,sha256=_8lJsU4sLm2CGnc7RDkueA0A
58
58
  pymammotion/mammotion/control/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
59
59
  pymammotion/mammotion/control/joystick.py,sha256=EWV20MMzQuhbLlNlXbsyZKSEpeM7x1CQL7saU4Pn0-g,6165
60
60
  pymammotion/mammotion/devices/__init__.py,sha256=T72jt0ejtMjo1rPmn_FeMF3pmp0LLeRRpc9WcDKEYYY,126
61
- pymammotion/mammotion/devices/mammotion.py,sha256=zoFNPcZGgucFUCpPKE4m_5U7TW6kMGsgyYmXHibNT40,44308
61
+ pymammotion/mammotion/devices/mammotion.py,sha256=IqOPXLVDcq8VQ2mm33qHrauzkS9XAKOVFE0-TOk-q4k,46909
62
62
  pymammotion/mqtt/__init__.py,sha256=Ocs5e-HLJvTuDpVXyECEsWIvwsUaxzj7lZ9mSYutNDY,105
63
63
  pymammotion/mqtt/mammotion_future.py,sha256=WKnHqeHiS2Ut-SaDBNOxqh1jDLeTiyLTsJ7PNUexrjk,687
64
- pymammotion/mqtt/mammotion_mqtt.py,sha256=eCAdOx7ikDiaetXyy9wusisds3klCx6n3DNH-8ZQaHA,8002
64
+ pymammotion/mqtt/mammotion_mqtt.py,sha256=K9TokiQWYJloWu5Hom00g7cfGhWHDSWqbcU9AqW4C9Q,8071
65
65
  pymammotion/proto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
66
66
  pymammotion/proto/basestation.proto,sha256=_x5gAz3FkZXS1jtq4GgZgaDCuRU-UV-7HTFdsfQ3zbo,1034
67
67
  pymammotion/proto/basestation.py,sha256=js64_N2xQYRxWPRdVNEapO0qe7vBlfYnjW5sE8hi7hw,2026
@@ -111,7 +111,7 @@ pymammotion/utility/device_type.py,sha256=KYawu2glZMVlPmxRbA4kVFujXz3miHp3rJiOWR
111
111
  pymammotion/utility/map.py,sha256=aoi-Luzuph02hKynTofMoq3mnPstanx75MDAVv49CuY,2211
112
112
  pymammotion/utility/periodic.py,sha256=9wJMfwXPlx6Mbp3Fws7LLTI34ZDKphH1bva_Ggyk32g,3281
113
113
  pymammotion/utility/rocker_util.py,sha256=syPL0QN4zMzHiTIkUKS7RXBBptjdbkfNlPddwUD5V3A,7171
114
- pymammotion-0.2.7.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
115
- pymammotion-0.2.7.dist-info/METADATA,sha256=WPR_bP14ZVgPXEUbRRSNOLP_cI75tPF0rVZBle-6pwU,3968
116
- pymammotion-0.2.7.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
117
- pymammotion-0.2.7.dist-info/RECORD,,
114
+ pymammotion-0.2.9.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
115
+ pymammotion-0.2.9.dist-info/METADATA,sha256=sUQS-JHi__zyOgzpZzSQDKZpkCJ4yzl4UjNQdGE73M0,3968
116
+ pymammotion-0.2.9.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
117
+ pymammotion-0.2.9.dist-info/RECORD,,