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

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

Potentially problematic release.


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

Files changed (133) hide show
  1. pymammotion/__init__.py +5 -4
  2. pymammotion/aliyun/client.py +235 -0
  3. pymammotion/aliyun/cloud_gateway.py +312 -64
  4. pymammotion/aliyun/model/aep_response.py +1 -2
  5. pymammotion/aliyun/model/dev_by_account_response.py +170 -23
  6. pymammotion/aliyun/model/login_by_oauth_response.py +2 -3
  7. pymammotion/aliyun/model/regions_response.py +3 -3
  8. pymammotion/aliyun/model/session_by_authcode_response.py +2 -2
  9. pymammotion/aliyun/model/thing_response.py +12 -0
  10. pymammotion/aliyun/regions.py +62 -0
  11. pymammotion/aliyun/tea/core.py +297 -0
  12. pymammotion/bluetooth/ble.py +7 -9
  13. pymammotion/bluetooth/ble_message.py +10 -14
  14. pymammotion/const.py +3 -0
  15. pymammotion/data/model/__init__.py +1 -2
  16. pymammotion/data/model/device.py +95 -27
  17. pymammotion/data/model/device_config.py +4 -4
  18. pymammotion/data/model/device_info.py +35 -0
  19. pymammotion/data/model/device_limits.py +10 -10
  20. pymammotion/data/model/enums.py +12 -2
  21. pymammotion/data/model/errors.py +12 -0
  22. pymammotion/data/model/events.py +14 -0
  23. pymammotion/data/model/generate_geojson.py +521 -0
  24. pymammotion/data/model/generate_route_information.py +2 -2
  25. pymammotion/data/model/hash_list.py +370 -57
  26. pymammotion/data/model/location.py +4 -4
  27. pymammotion/data/model/mowing_modes.py +17 -1
  28. pymammotion/data/model/raw_data.py +2 -10
  29. pymammotion/data/model/region_data.py +10 -11
  30. pymammotion/data/model/report_info.py +31 -5
  31. pymammotion/data/model/work.py +27 -0
  32. pymammotion/data/mower_state_manager.py +316 -0
  33. pymammotion/data/mqtt/event.py +73 -28
  34. pymammotion/data/mqtt/mammotion_properties.py +257 -0
  35. pymammotion/data/mqtt/properties.py +93 -78
  36. pymammotion/data/mqtt/status.py +18 -17
  37. pymammotion/event/event.py +27 -6
  38. pymammotion/homeassistant/__init__.py +3 -0
  39. pymammotion/homeassistant/mower_api.py +484 -0
  40. pymammotion/homeassistant/rtk_api.py +54 -0
  41. pymammotion/http/encryption.py +5 -6
  42. pymammotion/http/http.py +574 -28
  43. pymammotion/http/model/__init__.py +0 -0
  44. pymammotion/{aliyun/model/stream_subscription_response.py → http/model/camera_stream.py} +14 -2
  45. pymammotion/http/model/http.py +129 -4
  46. pymammotion/http/model/response_factory.py +61 -0
  47. pymammotion/http/model/rtk.py +16 -0
  48. pymammotion/mammotion/commands/abstract_message.py +7 -5
  49. pymammotion/mammotion/commands/mammotion_command.py +30 -1
  50. pymammotion/mammotion/commands/messages/basestation.py +43 -0
  51. pymammotion/mammotion/commands/messages/driver.py +61 -29
  52. pymammotion/mammotion/commands/messages/media.py +68 -15
  53. pymammotion/mammotion/commands/messages/navigation.py +61 -25
  54. pymammotion/mammotion/commands/messages/network.py +17 -23
  55. pymammotion/mammotion/commands/messages/ota.py +18 -18
  56. pymammotion/mammotion/commands/messages/system.py +32 -49
  57. pymammotion/mammotion/commands/messages/video.py +15 -16
  58. pymammotion/mammotion/devices/__init__.py +27 -3
  59. pymammotion/mammotion/devices/base.py +40 -131
  60. pymammotion/mammotion/devices/mammotion.py +436 -201
  61. pymammotion/mammotion/devices/mammotion_bluetooth.py +57 -47
  62. pymammotion/mammotion/devices/mammotion_cloud.py +134 -105
  63. pymammotion/mammotion/devices/mammotion_mower_ble.py +49 -0
  64. pymammotion/mammotion/devices/mammotion_mower_cloud.py +39 -0
  65. pymammotion/mammotion/devices/managers/managers.py +81 -0
  66. pymammotion/mammotion/devices/mower_device.py +124 -0
  67. pymammotion/mammotion/devices/mower_manager.py +107 -0
  68. pymammotion/mammotion/devices/rtk_ble.py +89 -0
  69. pymammotion/mammotion/devices/rtk_cloud.py +113 -0
  70. pymammotion/mammotion/devices/rtk_device.py +50 -0
  71. pymammotion/mammotion/devices/rtk_manager.py +122 -0
  72. pymammotion/mqtt/__init__.py +2 -1
  73. pymammotion/mqtt/aliyun_mqtt.py +232 -0
  74. pymammotion/mqtt/linkkit/__init__.py +5 -0
  75. pymammotion/mqtt/linkkit/h2client.py +585 -0
  76. pymammotion/mqtt/linkkit/linkkit.py +3023 -0
  77. pymammotion/mqtt/mammotion_mqtt.py +176 -169
  78. pymammotion/mqtt/mqtt_models.py +66 -0
  79. pymammotion/proto/__init__.py +4839 -4
  80. pymammotion/proto/basestation.proto +8 -0
  81. pymammotion/proto/basestation_pb2.py +11 -9
  82. pymammotion/proto/basestation_pb2.pyi +16 -2
  83. pymammotion/proto/dev_net.proto +79 -55
  84. pymammotion/proto/dev_net_pb2.py +60 -56
  85. pymammotion/proto/dev_net_pb2.pyi +49 -6
  86. pymammotion/proto/luba_msg.proto +2 -1
  87. pymammotion/proto/luba_msg_pb2.py +6 -6
  88. pymammotion/proto/luba_msg_pb2.pyi +1 -0
  89. pymammotion/proto/luba_mul.proto +62 -1
  90. pymammotion/proto/luba_mul_pb2.py +38 -22
  91. pymammotion/proto/luba_mul_pb2.pyi +94 -7
  92. pymammotion/proto/mctrl_driver.proto +44 -4
  93. pymammotion/proto/mctrl_driver_pb2.py +26 -14
  94. pymammotion/proto/mctrl_driver_pb2.pyi +66 -11
  95. pymammotion/proto/mctrl_nav.proto +93 -52
  96. pymammotion/proto/mctrl_nav_pb2.py +75 -67
  97. pymammotion/proto/mctrl_nav_pb2.pyi +142 -56
  98. pymammotion/proto/mctrl_ota.proto +40 -2
  99. pymammotion/proto/mctrl_ota_pb2.py +23 -13
  100. pymammotion/proto/mctrl_ota_pb2.pyi +67 -4
  101. pymammotion/proto/mctrl_pept.proto +8 -3
  102. pymammotion/proto/mctrl_pept_pb2.py +8 -6
  103. pymammotion/proto/mctrl_pept_pb2.pyi +14 -6
  104. pymammotion/proto/mctrl_sys.proto +325 -86
  105. pymammotion/proto/mctrl_sys_pb2.py +162 -98
  106. pymammotion/proto/mctrl_sys_pb2.pyi +451 -25
  107. pymammotion/proto/message_pool.py +3 -0
  108. pymammotion/proto/py.typed +0 -0
  109. pymammotion/utility/constant/device_constant.py +29 -5
  110. pymammotion/utility/datatype_converter.py +13 -12
  111. pymammotion/utility/device_config.py +522 -130
  112. pymammotion/utility/device_type.py +218 -21
  113. pymammotion/utility/map.py +238 -51
  114. pymammotion/utility/mur_mur_hash.py +159 -0
  115. {pymammotion-0.4.0a2.dist-info → pymammotion-0.5.51.dist-info}/METADATA +26 -31
  116. pymammotion-0.5.51.dist-info/RECORD +152 -0
  117. {pymammotion-0.4.0a2.dist-info → pymammotion-0.5.51.dist-info}/WHEEL +1 -1
  118. pymammotion/aliyun/cloud_service.py +0 -65
  119. pymammotion/data/model/plan.py +0 -58
  120. pymammotion/data/state_manager.py +0 -129
  121. pymammotion/proto/basestation.py +0 -59
  122. pymammotion/proto/common.py +0 -12
  123. pymammotion/proto/dev_net.py +0 -381
  124. pymammotion/proto/luba_msg.py +0 -81
  125. pymammotion/proto/luba_mul.py +0 -76
  126. pymammotion/proto/mctrl_driver.py +0 -100
  127. pymammotion/proto/mctrl_nav.py +0 -664
  128. pymammotion/proto/mctrl_ota.py +0 -48
  129. pymammotion/proto/mctrl_pept.py +0 -41
  130. pymammotion/proto/mctrl_sys.py +0 -574
  131. pymammotion-0.4.0a2.dist-info/RECORD +0 -131
  132. /pymammotion/http/{_init_.py → __init__.py} +0 -0
  133. {pymammotion-0.4.0a2.dist-info → pymammotion-0.5.51.dist-info/licenses}/LICENSE +0 -0
@@ -1,32 +1,33 @@
1
1
  """Module for interacting with Aliyun Cloud IoT Gateway."""
2
2
 
3
+ import asyncio
3
4
  import base64
4
5
  import hashlib
5
6
  import hmac
6
7
  import itertools
7
8
  import json
9
+ from json.decoder import JSONDecodeError
10
+ from logging import getLogger
8
11
  import random
9
12
  import string
10
13
  import time
11
14
  import uuid
12
- from logging import getLogger
13
15
 
14
- from aiohttp import ClientSession
15
- from alibabacloud_iot_api_gateway.client import Client
16
+ from aiohttp import ClientSession, ConnectionTimeoutError
16
17
  from alibabacloud_iot_api_gateway.models import CommonParams, Config, IoTApiRequest
17
18
  from alibabacloud_tea_util.client import Client as UtilClient
18
19
  from alibabacloud_tea_util.models import RuntimeOptions
20
+ from Tea.exceptions import UnretryableException
19
21
 
22
+ from pymammotion.aliyun.client import Client
20
23
  from pymammotion.aliyun.model.aep_response import AepResponse
21
24
  from pymammotion.aliyun.model.connect_response import ConnectResponse
22
- from pymammotion.aliyun.model.dev_by_account_response import (
23
- ListingDevByAccountResponse,
24
- )
25
+ from pymammotion.aliyun.model.dev_by_account_response import ListingDevAccountResponse
25
26
  from pymammotion.aliyun.model.login_by_oauth_response import LoginByOAuthResponse
26
27
  from pymammotion.aliyun.model.regions_response import RegionResponse
27
- from pymammotion.aliyun.model.session_by_authcode_response import (
28
- SessionByAuthCodeResponse,
29
- )
28
+ from pymammotion.aliyun.model.session_by_authcode_response import SessionByAuthCodeResponse
29
+ from pymammotion.aliyun.model.thing_response import ThingPropertiesResponse
30
+ from pymammotion.aliyun.regions import region_mappings
30
31
  from pymammotion.const import ALIYUN_DOMAIN, APP_KEY, APP_SECRET, APP_VERSION
31
32
  from pymammotion.http.http import MammotionHTTP
32
33
  from pymammotion.utility.datatype_converter import DatatypeConverter
@@ -49,6 +50,10 @@ MOVE_HEADERS = (
49
50
  class SetupException(Exception):
50
51
  """Raise when mqtt expires token or token is invalid."""
51
52
 
53
+ def __init__(self, *args: object) -> None:
54
+ super().__init__(args)
55
+ self.iot_id = args[1]
56
+
52
57
 
53
58
  class AuthRefreshException(Exception):
54
59
  """Raise exception when library cannot refresh token."""
@@ -57,6 +62,38 @@ class AuthRefreshException(Exception):
57
62
  class DeviceOfflineException(Exception):
58
63
  """Raise exception when device is offline."""
59
64
 
65
+ def __init__(self, *args: object) -> None:
66
+ super().__init__(args)
67
+ self.iot_id = args[1]
68
+
69
+
70
+ class FailedRequestException(Exception):
71
+ """Raise exception when request response is bad."""
72
+
73
+ def __init__(self, *args: object) -> None:
74
+ super().__init__(args)
75
+ self.iot_id = args[0]
76
+
77
+
78
+ class NoConnectionException(UnretryableException):
79
+ """Raise exception when device is unreachable."""
80
+
81
+
82
+ class GatewayTimeoutException(Exception):
83
+ """Raise exception when the gateway times out."""
84
+
85
+ def __init__(self, *args: object) -> None:
86
+ super().__init__(args)
87
+ self.iot_id = args[1]
88
+
89
+
90
+ class TooManyRequestsException(Exception):
91
+ """Raise exception when the gateway times out."""
92
+
93
+ def __init__(self, *args: object) -> None:
94
+ super().__init__(args)
95
+ self.iot_id = args[1]
96
+
60
97
 
61
98
  class LoginException(Exception):
62
99
  """Raise exception when library cannot log in."""
@@ -66,6 +103,9 @@ class CheckSessionException(Exception):
66
103
  """Raise exception when checking session results in a failure."""
67
104
 
68
105
 
106
+ EXPIRED_CREDENTIAL_EXCEPTIONS = (CheckSessionException, SetupException)
107
+
108
+
69
109
  class CloudIOTGateway:
70
110
  """Class for interacting with Aliyun Cloud IoT Gateway."""
71
111
 
@@ -77,19 +117,20 @@ class CloudIOTGateway:
77
117
 
78
118
  def __init__(
79
119
  self,
120
+ mammotion_http: MammotionHTTP,
80
121
  connect_response: ConnectResponse | None = None,
81
122
  login_by_oauth_response: LoginByOAuthResponse | None = None,
82
123
  aep_response: AepResponse | None = None,
83
124
  session_by_authcode_response: SessionByAuthCodeResponse | None = None,
84
125
  region_response: RegionResponse | None = None,
85
- dev_by_account: ListingDevByAccountResponse | None = None,
126
+ dev_by_account: ListingDevAccountResponse | None = None,
86
127
  ) -> None:
87
128
  """Initialize the CloudIOTGateway."""
88
- self.mammotion_http: MammotionHTTP | None = None
129
+ self.mammotion_http: MammotionHTTP = mammotion_http
89
130
  self._app_key = APP_KEY
90
131
  self._app_secret = APP_SECRET
91
132
  self.domain = ALIYUN_DOMAIN
92
-
133
+ self.message_delay = 1
93
134
  self._client_id = self.generate_hardware_string(8) # 8 characters
94
135
  self._device_sn = self.generate_hardware_string(32) # 32 characters
95
136
  self._utdid = self.generate_hardware_string(32) # 32 characters
@@ -99,9 +140,16 @@ class CloudIOTGateway:
99
140
  self._session_by_authcode_response = session_by_authcode_response
100
141
  self._region_response = region_response
101
142
  self._devices_by_account_response = dev_by_account
143
+ self._iot_token_issued_at = int(time.time())
144
+ if self._session_by_authcode_response:
145
+ self._iot_token_issued_at = (
146
+ self._session_by_authcode_response.token_issued_at
147
+ if self._session_by_authcode_response.token_issued_at is not None
148
+ else int(time.time())
149
+ )
102
150
 
103
151
  @staticmethod
104
- def generate_random_string(length: int):
152
+ def generate_random_string(length: int) -> str:
105
153
  """Generate a random string of specified length."""
106
154
  characters = string.ascii_letters + string.digits
107
155
  return "".join(random.choice(characters) for _ in range(length))
@@ -112,7 +160,15 @@ class CloudIOTGateway:
112
160
  hashed_uuid = hashlib.sha1(f"{uuid.getnode()}".encode()).hexdigest()
113
161
  return "".join(itertools.islice(itertools.cycle(hashed_uuid), length))
114
162
 
115
- def sign(self, data):
163
+ @staticmethod
164
+ def parse_json_response(response_body_str: str) -> dict:
165
+ try:
166
+ return json.loads(response_body_str) if response_body_str is not None else {}
167
+ except JSONDecodeError:
168
+ logger.error("Couldn't decode message %s", response_body_str)
169
+ return {"code": 22000}
170
+
171
+ def sign(self, data: dict) -> str:
116
172
  """Generate signature for the given data."""
117
173
  keys = ["appKey", "clientId", "deviceSn", "timestamp"]
118
174
  concatenated_str = ""
@@ -127,8 +183,13 @@ class CloudIOTGateway:
127
183
  hashlib.sha1,
128
184
  ).hexdigest()
129
185
 
130
- def get_region(self, country_code: str, auth_code: str):
186
+ async def get_region(self, country_code: str) -> RegionResponse:
131
187
  """Get the region based on country code and auth code."""
188
+ auth_code = self.mammotion_http.login_info.authorization_code
189
+
190
+ if self._region_response is not None:
191
+ return self._region_response
192
+
132
193
  config = Config(
133
194
  app_key=self._app_key,
134
195
  app_secret=self._app_secret,
@@ -150,17 +211,33 @@ class CloudIOTGateway:
150
211
  )
151
212
 
152
213
  # send request
153
- response = client.do_request("/living/account/region/get", "https", "POST", None, body, RuntimeOptions())
154
- logger.debug(response.status_message)
155
- logger.debug(response.headers)
156
- logger.debug(response.status_code)
157
- logger.debug(response.body)
158
-
214
+ try:
215
+ response = await client.async_do_request(
216
+ "/living/account/region/get", "https", "POST", None, body, RuntimeOptions()
217
+ )
218
+ logger.debug(response.status_message)
219
+ logger.debug(response.headers)
220
+ logger.debug(response.status_code)
221
+ logger.debug(response.body)
222
+ except ConnectionTimeoutError:
223
+ body = {"data": {}, "code": 200}
224
+
225
+ region = region_mappings.get(country_code, "US")
226
+ body["data"]["shortRegionId"] = region
227
+ body["data"]["regionEnglishName"] = ""
228
+ body["data"]["oaApiGatewayEndpoint"] = f"living-account.{region}.aliyuncs.com"
229
+ body["data"]["regionId"] = region
230
+ body["data"]["mqttEndpoint"] = f"public.itls.{region}.aliyuncs.com:1883"
231
+ body["data"]["pushChannelEndpoint"] = f"living-accs.{region}.aliyuncs.com"
232
+ body["data"]["apiGatewayEndpoint"] = f"{region}.api-iot.aliyuncs.com"
233
+
234
+ RegionResponse.from_dict(body)
235
+ return body
159
236
  # Decode the response body
160
237
  response_body_str = response.body.decode("utf-8")
161
238
 
162
239
  # Load the JSON string into a dictionary
163
- response_body_dict = json.loads(response_body_str)
240
+ response_body_dict = self.parse_json_response(response_body_str)
164
241
 
165
242
  if int(response_body_dict.get("code")) != 200:
166
243
  raise Exception("Error in getting regions: " + response_body_dict["msg"])
@@ -170,7 +247,7 @@ class CloudIOTGateway:
170
247
 
171
248
  return response.body
172
249
 
173
- def aep_handle(self):
250
+ async def aep_handle(self) -> AepResponse:
174
251
  """Handle AEP authentication."""
175
252
  aep_domain = self.domain
176
253
 
@@ -209,7 +286,7 @@ class CloudIOTGateway:
209
286
  )
210
287
 
211
288
  # send request
212
- response = client.do_request("/app/aepauth/handle", "https", "POST", None, body, RuntimeOptions())
289
+ response = await client.async_do_request("/app/aepauth/handle", "https", "POST", None, body, RuntimeOptions())
213
290
  logger.debug(response.status_message)
214
291
  logger.debug(response.headers)
215
292
  logger.debug(response.status_code)
@@ -217,7 +294,7 @@ class CloudIOTGateway:
217
294
 
218
295
  response_body_str = response.body.decode("utf-8")
219
296
 
220
- response_body_dict = json.loads(response_body_str)
297
+ response_body_dict = self.parse_json_response(response_body_str)
221
298
 
222
299
  if int(response_body_dict.get("code")) != 200:
223
300
  raise Exception("Error in getting mqtt credentials: " + response_body_dict["msg"])
@@ -226,9 +303,9 @@ class CloudIOTGateway:
226
303
 
227
304
  logger.debug(response_body_dict)
228
305
 
229
- return response.body
306
+ return self._aep_response
230
307
 
231
- async def connect(self):
308
+ async def connect(self) -> ConnectResponse:
232
309
  """Connect to the Aliyun Cloud IoT Gateway."""
233
310
  region_url = "sdk.openaccount.aliyun.com"
234
311
  time_now = time.time()
@@ -302,8 +379,9 @@ class CloudIOTGateway:
302
379
  return self._connect_response
303
380
  raise LoginException(data)
304
381
 
305
- async def login_by_oauth(self, country_code: str, auth_code: str):
382
+ async def login_by_oauth(self, country_code: str):
306
383
  """Login by OAuth."""
384
+ auth_code = self.mammotion_http.login_info.authorization_code
307
385
  region_url = self._region_response.data.oaApiGatewayEndpoint
308
386
 
309
387
  async with ClientSession() as session:
@@ -374,7 +452,7 @@ class CloudIOTGateway:
374
452
  return self._login_by_oauth_response
375
453
  raise LoginException(data)
376
454
 
377
- def session_by_auth_code(self):
455
+ async def session_by_auth_code(self) -> SessionByAuthCodeResponse:
378
456
  """Create a session by auth code."""
379
457
  config = Config(
380
458
  app_key=self._app_key,
@@ -399,7 +477,7 @@ class CloudIOTGateway:
399
477
  )
400
478
 
401
479
  # send request
402
- response = client.do_request(
480
+ response = await client.async_do_request(
403
481
  "/account/createSessionByAuthCode",
404
482
  "https",
405
483
  "POST",
@@ -416,7 +494,7 @@ class CloudIOTGateway:
416
494
  response_body_str = response.body.decode("utf-8")
417
495
 
418
496
  # Load the JSON string into a dictionary
419
- response_body_dict = json.loads(response_body_str)
497
+ response_body_dict = self.parse_json_response(response_body_str)
420
498
 
421
499
  session_by_auth = SessionByAuthCodeResponse.from_dict(response_body_dict)
422
500
 
@@ -431,7 +509,7 @@ class CloudIOTGateway:
431
509
 
432
510
  return response.body
433
511
 
434
- def sign_out(self) -> None:
512
+ async def sign_out(self) -> dict:
435
513
  config = Config(
436
514
  app_key=self._app_key,
437
515
  app_secret=self._app_secret,
@@ -455,7 +533,7 @@ class CloudIOTGateway:
455
533
 
456
534
  # send request
457
535
  # possibly need to do this ourselves
458
- response = client.do_request(
536
+ response = await client.async_do_request(
459
537
  "/iotx/account/invalidSession",
460
538
  "https",
461
539
  "POST",
@@ -472,11 +550,10 @@ class CloudIOTGateway:
472
550
  response_body_str = response.body.decode("utf-8")
473
551
 
474
552
  # Load the JSON string into a dictionary
475
- response_body_dict = json.loads(response_body_str)
476
- logger.debug(response_body_dict)
553
+ response_body_dict = self.parse_json_response(response_body_str)
477
554
  return response_body_dict
478
555
 
479
- def check_or_refresh_session(self):
556
+ async def check_or_refresh_session(self):
480
557
  """Check or refresh the session."""
481
558
  logger.debug("Trying to refresh token")
482
559
  config = Config(
@@ -502,7 +579,7 @@ class CloudIOTGateway:
502
579
 
503
580
  # send request
504
581
  # possibly need to do this ourselves
505
- response = client.do_request(
582
+ response = await client.async_do_request(
506
583
  "/account/checkOrRefreshSession",
507
584
  "https",
508
585
  "POST",
@@ -519,18 +596,23 @@ class CloudIOTGateway:
519
596
  response_body_str = response.body.decode("utf-8")
520
597
 
521
598
  # Load the JSON string into a dictionary
522
- response_body_dict = json.loads(response_body_str)
599
+ response_body_dict = self.parse_json_response(response_body_str)
523
600
 
524
601
  if int(response_body_dict.get("code")) != 200:
525
602
  logger.error(response_body_dict)
526
- self.sign_out()
603
+ await self.sign_out()
604
+ raise CheckSessionException("Error check or refresh token: " + response_body_dict.__str__())
605
+
606
+ if response_body_dict.get("code") == 2401:
607
+ await self.sign_out()
527
608
  raise CheckSessionException("Error check or refresh token: " + response_body_dict.__str__())
528
609
 
529
610
  session = SessionByAuthCodeResponse.from_dict(response_body_dict)
530
611
  session_data = session.data
531
612
 
532
613
  if (
533
- session_data.identityId is None
614
+ session_data is None
615
+ or session_data.identityId is None
534
616
  or session_data.refreshTokenExpire is None
535
617
  or session_data.iotToken is None
536
618
  or session_data.iotTokenExpire is None
@@ -541,7 +623,7 @@ class CloudIOTGateway:
541
623
  self._session_by_authcode_response = session
542
624
  self._iot_token_issued_at = int(time.time())
543
625
 
544
- def list_binding_by_account(self) -> ListingDevByAccountResponse:
626
+ async def list_binding_by_account(self) -> ListingDevAccountResponse:
545
627
  """List bindings by account."""
546
628
  config = Config(
547
629
  app_key=self._app_key,
@@ -565,7 +647,9 @@ class CloudIOTGateway:
565
647
  )
566
648
 
567
649
  # send request
568
- response = client.do_request("/uc/listBindingByAccount", "https", "POST", None, body, RuntimeOptions())
650
+ response = await client.async_do_request(
651
+ "/uc/listBindingByAccount", "https", "POST", None, body, RuntimeOptions()
652
+ )
569
653
  logger.debug(response.status_message)
570
654
  logger.debug(response.headers)
571
655
  logger.debug(response.status_code)
@@ -575,15 +659,15 @@ class CloudIOTGateway:
575
659
  response_body_str = response.body.decode("utf-8")
576
660
 
577
661
  # Load the JSON string into a dictionary
578
- response_body_dict = json.loads(response_body_str)
662
+ response_body_dict = self.parse_json_response(response_body_str)
579
663
 
580
664
  if int(response_body_dict.get("code")) != 200:
581
- raise Exception("Error in creating session: " + response_body_dict["msg"])
665
+ raise Exception("Error in creating session: " + response_body_dict["message"])
582
666
 
583
- self._devices_by_account_response = ListingDevByAccountResponse.from_dict(response_body_dict)
667
+ self._devices_by_account_response = ListingDevAccountResponse.from_dict(response_body_dict)
584
668
  return self._devices_by_account_response
585
669
 
586
- def list_binding_by_dev(self, iot_id: str):
670
+ async def list_binding_by_dev(self, iot_id: str):
587
671
  config = Config(
588
672
  app_key=self._app_key,
589
673
  app_secret=self._app_secret,
@@ -606,7 +690,7 @@ class CloudIOTGateway:
606
690
  )
607
691
 
608
692
  # send request
609
- response = client.do_request("/uc/listBindingByDev", "https", "POST", None, body, RuntimeOptions())
693
+ response = await client.async_do_request("/uc/listBindingByDev", "https", "POST", None, body, RuntimeOptions())
610
694
  logger.debug(response.status_message)
611
695
  logger.debug(response.headers)
612
696
  logger.debug(response.status_code)
@@ -616,17 +700,115 @@ class CloudIOTGateway:
616
700
  response_body_str = response.body.decode("utf-8")
617
701
 
618
702
  # Load the JSON string into a dictionary
619
- response_body_dict = json.loads(response_body_str)
703
+ response_body_dict = self.parse_json_response(response_body_str)
704
+
705
+ if int(response_body_dict.get("code")) != 200:
706
+ raise Exception("Error in getting shared device list: " + response_body_dict["msg"])
707
+
708
+ self._devices_by_account_response = ListingDevAccountResponse.from_dict(response_body_dict)
709
+ return self._devices_by_account_response
710
+
711
+ async def confirm_share(self, record_list: list[str]) -> bool:
712
+ config = Config(
713
+ app_key=self._app_key,
714
+ app_secret=self._app_secret,
715
+ domain=self._region_response.data.apiGatewayEndpoint,
716
+ )
717
+
718
+ client = Client(config)
719
+
720
+ # build request
721
+ request = CommonParams(
722
+ api_ver="1.0.7",
723
+ language="en-US",
724
+ iot_token=self._session_by_authcode_response.data.iotToken,
725
+ )
726
+ body = IoTApiRequest(
727
+ id=str(uuid.uuid4()),
728
+ params={"agree": 1, "recordIdList": record_list},
729
+ request=request,
730
+ version="1.0",
731
+ )
732
+
733
+ # send request
734
+ response = await client.async_do_request("/uc/confirmShare", "https", "POST", None, body, RuntimeOptions())
735
+ logger.debug(response.status_message)
736
+ logger.debug(response.headers)
737
+ logger.debug(response.status_code)
738
+ logger.debug(response.body)
739
+
740
+ # Decode the response body
741
+ response_body_str = response.body.decode("utf-8")
742
+
743
+ # Load the JSON string into a dictionary
744
+ response_body_dict = self.parse_json_response(response_body_str)
745
+
746
+ if int(response_body_dict.get("code")) != 200:
747
+ raise Exception("Error in accepting share: " + response_body_dict["msg"])
748
+
749
+ return True
750
+
751
+ async def get_shared_notice_list(self):
752
+ ### status 0 accepted status -1 ready to be accepted 3 expired
753
+ config = Config(
754
+ app_key=self._app_key,
755
+ app_secret=self._app_secret,
756
+ domain=self._region_response.data.apiGatewayEndpoint,
757
+ )
758
+
759
+ client = Client(config)
760
+
761
+ # build request
762
+ request = CommonParams(
763
+ api_ver="1.0.9",
764
+ language="en-US",
765
+ iot_token=self._session_by_authcode_response.data.iotToken,
766
+ )
767
+ body = IoTApiRequest(
768
+ id=str(uuid.uuid4()),
769
+ params={"pageSize": 100, "pageNo": 1},
770
+ request=request,
771
+ version="1.0",
772
+ )
773
+
774
+ # send request
775
+ response = await client.async_do_request(
776
+ "/uc/getShareNoticeList", "https", "POST", None, body, RuntimeOptions()
777
+ )
778
+ logger.debug(response.status_message)
779
+ logger.debug(response.headers)
780
+ logger.debug(response.status_code)
781
+ logger.debug(response.body)
782
+
783
+ # Decode the response body
784
+ response_body_str = response.body.decode("utf-8")
785
+
786
+ # Load the JSON string into a dictionary
787
+ response_body_dict = self.parse_json_response(response_body_str)
620
788
 
621
789
  if int(response_body_dict.get("code")) != 200:
622
790
  raise Exception("Error in creating session: " + response_body_dict["msg"])
623
791
 
624
- self._devices_by_account_response = ListingDevByAccountResponse.from_dict(response_body_dict)
792
+ self._devices_by_account_response = ListingDevAccountResponse.from_dict(response_body_dict)
625
793
  return self._devices_by_account_response
626
794
 
627
- def send_cloud_command(self, iot_id: str, command: bytes) -> str:
628
- """Send a cloud command to the specified IoT device."""
795
+ async def send_cloud_command(self, iot_id: str, command: bytes) -> str:
796
+ """Sends a cloud command to a specified IoT device.
797
+
798
+ This function checks if the IoT token is expired and attempts to refresh it if
799
+ possible. It then constructs a request using the provided command and sends it
800
+ to the IoT device via an asynchronous HTTP POST request. The function handles
801
+ various error codes and exceptions based on the response from the cloud
802
+ service.
803
+
804
+ Args:
805
+ iot_id (str): The unique identifier of the IoT device.
806
+ command (bytes): The command to be sent to the IoT device in binary format.
629
807
 
808
+ Returns:
809
+ str: A unique message ID for the sent command.
810
+
811
+ """
630
812
  if command is None:
631
813
  raise Exception("Command is missing / None")
632
814
 
@@ -638,7 +820,7 @@ class CloudIOTGateway:
638
820
  if self._iot_token_issued_at + self._session_by_authcode_response.data.refreshTokenExpire > (
639
821
  int(time.time())
640
822
  ):
641
- self.check_or_refresh_session()
823
+ await self.check_or_refresh_session()
642
824
  else:
643
825
  raise AuthRefreshException("Refresh token expired. Please re-login")
644
826
 
@@ -649,7 +831,6 @@ class CloudIOTGateway:
649
831
  )
650
832
 
651
833
  client = Client(config)
652
-
653
834
  # build request
654
835
  request = CommonParams(
655
836
  api_ver="1.0.5",
@@ -657,7 +838,7 @@ class CloudIOTGateway:
657
838
  iot_token=self._session_by_authcode_response.data.iotToken,
658
839
  )
659
840
 
660
- # TODO move to using InvokeThingServiceRequest()
841
+ # TODO move to using InvokeThingServiceRequest()
661
842
 
662
843
  message_id = str(uuid.uuid4())
663
844
 
@@ -673,15 +854,26 @@ class CloudIOTGateway:
673
854
  )
674
855
  logger.debug(self.converter.printBase64Binary(command))
675
856
  # send request
676
- response = client.do_request("/thing/service/invoke", "https", "POST", None, body, RuntimeOptions())
857
+ runtime_options = RuntimeOptions(autoretry=True, backoff_policy="yes")
858
+ response = await client.async_do_request("/thing/service/invoke", "https", "POST", None, body, runtime_options)
677
859
  logger.debug(response.status_message)
678
860
  logger.debug(response.headers)
679
861
  logger.debug(response.status_code)
680
862
  logger.debug(response.body)
681
863
  logger.debug(iot_id)
682
864
 
865
+ if response.status_code == 429:
866
+ logger.debug("too many requests.")
867
+ if self.message_delay > 8:
868
+ raise TooManyRequestsException(response.status_message, iot_id)
869
+ asyncio.get_event_loop().call_later(
870
+ self.message_delay, lambda: asyncio.ensure_future(self.send_cloud_command(iot_id, command))
871
+ )
872
+ self.message_delay = self.message_delay * 2
873
+ return message_id
874
+
683
875
  response_body_str = response.body.decode("utf-8")
684
- response_body_dict = json.loads(response_body_str)
876
+ response_body_dict = self.parse_json_response(response_body_str)
685
877
 
686
878
  if int(response_body_dict.get("code")) != 200:
687
879
  logger.error(
@@ -689,16 +881,72 @@ class CloudIOTGateway:
689
881
  str(response_body_dict.get("code")),
690
882
  str(response_body_dict.get("message")),
691
883
  )
884
+ if response_body_dict.get("code") == 22000:
885
+ logger.error(response.body)
886
+ raise FailedRequestException(iot_id)
887
+ if response_body_dict.get("code") == 20056:
888
+ logger.debug("Gateway timeout.")
889
+ raise GatewayTimeoutException(response_body_dict.get("code"), iot_id)
890
+
692
891
  if response_body_dict.get("code") == 29003:
693
892
  logger.debug(self._session_by_authcode_response.data.identityId)
694
- self.sign_out()
695
- raise SetupException(response_body_dict.get("code"))
893
+ await self.sign_out()
894
+ raise SetupException(response_body_dict.get("code"), iot_id)
696
895
  if response_body_dict.get("code") == 6205:
697
- raise DeviceOfflineException(response_body_dict.get("code"))
698
- """Device is offline."""
896
+ raise DeviceOfflineException(response_body_dict.get("code"), iot_id)
897
+
898
+ if response_body_dict.get("code") == 460:
899
+ logger.debug("iotToken expired, must re-login.")
900
+ raise CheckSessionException(response_body_dict.get("message"))
901
+
902
+ if self.message_delay != 1:
903
+ self.message_delay = 1
699
904
 
700
905
  return message_id
701
906
 
907
+ async def get_device_properties(self, iot_id: str) -> ThingPropertiesResponse:
908
+ """List bindings by account."""
909
+ config = Config(
910
+ app_key=self._app_key,
911
+ app_secret=self._app_secret,
912
+ domain=self._region_response.data.apiGatewayEndpoint,
913
+ )
914
+
915
+ client = Client(config)
916
+
917
+ # build request
918
+ request = CommonParams(
919
+ api_ver="1.0.0",
920
+ language="en-US",
921
+ iot_token=self._session_by_authcode_response.data.iotToken,
922
+ )
923
+ body = IoTApiRequest(
924
+ id=str(uuid.uuid4()),
925
+ params={
926
+ "iotId": f"{iot_id}",
927
+ },
928
+ request=request,
929
+ version="1.0",
930
+ )
931
+
932
+ # send request
933
+ response = await client.async_do_request("/thing/properties/get", "https", "POST", None, body, RuntimeOptions())
934
+ logger.debug(response.status_message)
935
+ logger.debug(response.headers)
936
+ logger.debug(response.status_code)
937
+ logger.debug(response.body)
938
+
939
+ # Decode the response body
940
+ response_body_str = response.body.decode("utf-8")
941
+
942
+ # Load the JSON string into a dictionary
943
+ response_body_dict = self.parse_json_response(response_body_str)
944
+
945
+ if int(response_body_dict.get("code")) != 200:
946
+ raise Exception("Error in getting properties: " + response_body_dict["msg"])
947
+
948
+ return ThingPropertiesResponse.from_dict(response_body_dict)
949
+
702
950
  @property
703
951
  def devices_by_account_response(self):
704
952
  return self._devices_by_account_response
@@ -707,11 +955,11 @@ class CloudIOTGateway:
707
955
  self.mammotion_http = mammotion_http
708
956
 
709
957
  @property
710
- def region_response(self) -> RegionResponse:
958
+ def region_response(self) -> RegionResponse | None:
711
959
  return self._region_response
712
960
 
713
961
  @property
714
- def aep_response(self) -> AepResponse:
962
+ def aep_response(self) -> AepResponse | None:
715
963
  return self._aep_response
716
964
 
717
965
  @property
@@ -723,9 +971,9 @@ class CloudIOTGateway:
723
971
  return self._client_id
724
972
 
725
973
  @property
726
- def login_by_oauth_response(self) -> LoginByOAuthResponse:
974
+ def login_by_oauth_response(self) -> LoginByOAuthResponse | None:
727
975
  return self._login_by_oauth_response
728
976
 
729
977
  @property
730
- def connect_response(self) -> ConnectResponse:
978
+ def connect_response(self) -> ConnectResponse | None:
731
979
  return self._connect_response