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.
- pymammotion/aliyun/cloud_gateway.py +83 -23
- pymammotion/aliyun/dataclass/session_by_authcode_response.py +4 -4
- pymammotion/data/model/location.py +0 -4
- pymammotion/data/state_manager.py +6 -8
- pymammotion/mammotion/devices/mammotion.py +95 -36
- pymammotion/mqtt/mammotion_mqtt.py +5 -4
- {pymammotion-0.2.7.dist-info → pymammotion-0.2.9.dist-info}/METADATA +1 -1
- {pymammotion-0.2.7.dist-info → pymammotion-0.2.9.dist-info}/RECORD +10 -10
- {pymammotion-0.2.7.dist-info → pymammotion-0.2.9.dist-info}/LICENSE +0 -0
- {pymammotion-0.2.7.dist-info → pymammotion-0.2.9.dist-info}/WHEEL +0 -0
|
@@ -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
|
-
|
|
64
|
-
|
|
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.
|
|
144
|
-
logger.debug("Endpoint: %s", self.
|
|
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.
|
|
153
|
-
aep_domain = self.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
#
|
|
435
|
-
# Decodifica il corpo della risposta
|
|
466
|
+
# Decode the response body
|
|
436
467
|
response_body_str = response.body.decode("utf-8")
|
|
437
468
|
|
|
438
|
-
#
|
|
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.
|
|
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.
|
|
481
|
-
return self.
|
|
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.
|
|
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.
|
|
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
|
|
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:
|
|
18
|
+
data: SessionOauthToken
|
|
@@ -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 =
|
|
21
|
-
self.get_commondata_ack_callback =
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
233
|
-
if self.
|
|
233
|
+
if self.mqtt is not None:
|
|
234
|
+
if self.mqtt.is_connected:
|
|
234
235
|
return
|
|
235
236
|
|
|
236
|
-
|
|
237
|
-
self.
|
|
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.
|
|
245
|
+
self.mqtt._cloud_client = cloud_client
|
|
245
246
|
loop = asyncio.get_running_loop()
|
|
246
|
-
await loop.run_in_executor(None, self.
|
|
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.
|
|
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
|
|
342
|
-
self._state_manager.get_commondata_ack_callback
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
508
|
-
|
|
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.
|
|
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
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
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
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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,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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
115
|
-
pymammotion-0.2.
|
|
116
|
-
pymammotion-0.2.
|
|
117
|
-
pymammotion-0.2.
|
|
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,,
|
|
File without changes
|
|
File without changes
|