pymammotion 0.2.62__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 (135) hide show
  1. pymammotion/__init__.py +9 -6
  2. pymammotion/aliyun/client.py +235 -0
  3. pymammotion/aliyun/cloud_gateway.py +320 -69
  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 +11 -15
  13. pymammotion/bluetooth/ble_message.py +389 -106
  14. pymammotion/bluetooth/model/atomic_integer.py +54 -0
  15. pymammotion/const.py +3 -0
  16. pymammotion/data/model/__init__.py +1 -2
  17. pymammotion/data/model/device.py +92 -240
  18. pymammotion/data/model/device_config.py +10 -24
  19. pymammotion/data/model/device_info.py +35 -0
  20. pymammotion/data/model/device_limits.py +49 -0
  21. pymammotion/data/model/enums.py +12 -2
  22. pymammotion/data/model/errors.py +12 -0
  23. pymammotion/data/model/events.py +14 -0
  24. pymammotion/data/model/generate_geojson.py +521 -0
  25. pymammotion/data/model/generate_route_information.py +3 -4
  26. pymammotion/data/model/hash_list.py +384 -48
  27. pymammotion/data/model/location.py +4 -4
  28. pymammotion/data/model/mowing_modes.py +24 -1
  29. pymammotion/data/model/raw_data.py +215 -0
  30. pymammotion/data/model/region_data.py +10 -11
  31. pymammotion/data/model/report_info.py +62 -6
  32. pymammotion/data/model/work.py +27 -0
  33. pymammotion/data/mower_state_manager.py +316 -0
  34. pymammotion/data/mqtt/event.py +73 -28
  35. pymammotion/data/mqtt/mammotion_properties.py +257 -0
  36. pymammotion/data/mqtt/properties.py +93 -78
  37. pymammotion/data/mqtt/status.py +18 -17
  38. pymammotion/event/event.py +32 -8
  39. pymammotion/homeassistant/__init__.py +3 -0
  40. pymammotion/homeassistant/mower_api.py +484 -0
  41. pymammotion/homeassistant/rtk_api.py +54 -0
  42. pymammotion/http/__init__.py +0 -0
  43. pymammotion/http/encryption.py +220 -0
  44. pymammotion/http/http.py +652 -44
  45. pymammotion/http/model/__init__.py +0 -0
  46. pymammotion/{aliyun/model/stream_subscription_response.py → http/model/camera_stream.py} +14 -2
  47. pymammotion/http/model/http.py +160 -9
  48. pymammotion/http/model/response_factory.py +61 -0
  49. pymammotion/http/model/rtk.py +16 -0
  50. pymammotion/mammotion/commands/abstract_message.py +7 -5
  51. pymammotion/mammotion/commands/mammotion_command.py +32 -3
  52. pymammotion/mammotion/commands/messages/basestation.py +43 -0
  53. pymammotion/mammotion/commands/messages/driver.py +61 -29
  54. pymammotion/mammotion/commands/messages/media.py +68 -15
  55. pymammotion/mammotion/commands/messages/navigation.py +61 -25
  56. pymammotion/mammotion/commands/messages/network.py +93 -100
  57. pymammotion/mammotion/commands/messages/ota.py +18 -18
  58. pymammotion/mammotion/commands/messages/system.py +97 -72
  59. pymammotion/mammotion/commands/messages/video.py +17 -12
  60. pymammotion/mammotion/devices/__init__.py +27 -3
  61. pymammotion/mammotion/devices/base.py +50 -127
  62. pymammotion/mammotion/devices/mammotion.py +447 -212
  63. pymammotion/mammotion/devices/mammotion_bluetooth.py +105 -60
  64. pymammotion/mammotion/devices/mammotion_cloud.py +157 -105
  65. pymammotion/mammotion/devices/mammotion_mower_ble.py +49 -0
  66. pymammotion/mammotion/devices/mammotion_mower_cloud.py +39 -0
  67. pymammotion/mammotion/devices/managers/managers.py +81 -0
  68. pymammotion/mammotion/devices/mower_device.py +124 -0
  69. pymammotion/mammotion/devices/mower_manager.py +107 -0
  70. pymammotion/mammotion/devices/rtk_ble.py +89 -0
  71. pymammotion/mammotion/devices/rtk_cloud.py +113 -0
  72. pymammotion/mammotion/devices/rtk_device.py +50 -0
  73. pymammotion/mammotion/devices/rtk_manager.py +122 -0
  74. pymammotion/mqtt/__init__.py +2 -1
  75. pymammotion/mqtt/aliyun_mqtt.py +232 -0
  76. pymammotion/mqtt/linkkit/__init__.py +5 -0
  77. pymammotion/mqtt/linkkit/h2client.py +585 -0
  78. pymammotion/mqtt/linkkit/linkkit.py +3023 -0
  79. pymammotion/mqtt/mammotion_mqtt.py +176 -169
  80. pymammotion/mqtt/mqtt_models.py +66 -0
  81. pymammotion/proto/__init__.py +4839 -4
  82. pymammotion/proto/basestation.proto +8 -0
  83. pymammotion/proto/basestation_pb2.py +11 -9
  84. pymammotion/proto/basestation_pb2.pyi +16 -2
  85. pymammotion/proto/dev_net.proto +79 -55
  86. pymammotion/proto/dev_net_pb2.py +60 -56
  87. pymammotion/proto/dev_net_pb2.pyi +49 -6
  88. pymammotion/proto/luba_msg.proto +2 -1
  89. pymammotion/proto/luba_msg_pb2.py +6 -6
  90. pymammotion/proto/luba_msg_pb2.pyi +1 -0
  91. pymammotion/proto/luba_mul.proto +62 -1
  92. pymammotion/proto/luba_mul_pb2.py +38 -22
  93. pymammotion/proto/luba_mul_pb2.pyi +94 -7
  94. pymammotion/proto/mctrl_driver.proto +44 -4
  95. pymammotion/proto/mctrl_driver_pb2.py +26 -14
  96. pymammotion/proto/mctrl_driver_pb2.pyi +66 -11
  97. pymammotion/proto/mctrl_nav.proto +97 -51
  98. pymammotion/proto/mctrl_nav_pb2.py +75 -67
  99. pymammotion/proto/mctrl_nav_pb2.pyi +142 -56
  100. pymammotion/proto/mctrl_ota.proto +40 -2
  101. pymammotion/proto/mctrl_ota_pb2.py +23 -13
  102. pymammotion/proto/mctrl_ota_pb2.pyi +67 -4
  103. pymammotion/proto/mctrl_pept.proto +8 -3
  104. pymammotion/proto/mctrl_pept_pb2.py +8 -6
  105. pymammotion/proto/mctrl_pept_pb2.pyi +14 -6
  106. pymammotion/proto/mctrl_sys.proto +325 -86
  107. pymammotion/proto/mctrl_sys_pb2.py +162 -98
  108. pymammotion/proto/mctrl_sys_pb2.pyi +451 -25
  109. pymammotion/proto/message_pool.py +3 -0
  110. pymammotion/proto/py.typed +0 -0
  111. pymammotion/utility/constant/device_constant.py +65 -21
  112. pymammotion/utility/datatype_converter.py +13 -12
  113. pymammotion/utility/device_config.py +755 -0
  114. pymammotion/utility/device_type.py +218 -21
  115. pymammotion/utility/map.py +238 -51
  116. pymammotion/utility/mur_mur_hash.py +159 -0
  117. {pymammotion-0.2.62.dist-info → pymammotion-0.5.51.dist-info}/METADATA +27 -31
  118. pymammotion-0.5.51.dist-info/RECORD +152 -0
  119. {pymammotion-0.2.62.dist-info → pymammotion-0.5.51.dist-info}/WHEEL +1 -1
  120. pymammotion/aliyun/cloud_service.py +0 -65
  121. pymammotion/data/model/plan.py +0 -58
  122. pymammotion/data/state_manager.py +0 -130
  123. pymammotion/proto/basestation.py +0 -59
  124. pymammotion/proto/common.py +0 -12
  125. pymammotion/proto/dev_net.py +0 -381
  126. pymammotion/proto/luba_msg.py +0 -81
  127. pymammotion/proto/luba_mul.py +0 -76
  128. pymammotion/proto/mctrl_driver.py +0 -100
  129. pymammotion/proto/mctrl_nav.py +0 -660
  130. pymammotion/proto/mctrl_ota.py +0 -48
  131. pymammotion/proto/mctrl_pept.py +0 -41
  132. pymammotion/proto/mctrl_sys.py +0 -574
  133. pymammotion-0.2.62.dist-info/RECORD +0 -125
  134. /pymammotion/{http/_init_.py → bluetooth/model/__init__.py} +0 -0
  135. {pymammotion-0.2.62.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,13 +550,12 @@ 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
- logger.debug("Try to refresh token")
558
+ logger.debug("Trying to refresh token")
482
559
  config = Config(
483
560
  app_key=self._app_key,
484
561
  app_secret=self._app_secret,
@@ -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,91 @@ 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())
694
+ logger.debug(response.status_message)
695
+ logger.debug(response.headers)
696
+ logger.debug(response.status_code)
697
+ logger.debug(response.body)
698
+
699
+ # Decode the response body
700
+ response_body_str = response.body.decode("utf-8")
701
+
702
+ # Load the JSON string into a dictionary
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
+ )
610
778
  logger.debug(response.status_message)
611
779
  logger.debug(response.headers)
612
780
  logger.debug(response.status_code)
@@ -616,16 +784,33 @@ class CloudIOTGateway:
616
784
  response_body_str = response.body.decode("utf-8")
617
785
 
618
786
  # Load the JSON string into a dictionary
619
- response_body_dict = json.loads(response_body_str)
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.
807
+
808
+ Returns:
809
+ str: A unique message ID for the sent command.
810
+
811
+ """
812
+ if command is None:
813
+ raise Exception("Command is missing / None")
629
814
 
630
815
  """Check if iotToken is expired"""
631
816
  if self._iot_token_issued_at + self._session_by_authcode_response.data.iotTokenExpire <= (
@@ -635,7 +820,7 @@ class CloudIOTGateway:
635
820
  if self._iot_token_issued_at + self._session_by_authcode_response.data.refreshTokenExpire > (
636
821
  int(time.time())
637
822
  ):
638
- self.check_or_refresh_session()
823
+ await self.check_or_refresh_session()
639
824
  else:
640
825
  raise AuthRefreshException("Refresh token expired. Please re-login")
641
826
 
@@ -646,7 +831,6 @@ class CloudIOTGateway:
646
831
  )
647
832
 
648
833
  client = Client(config)
649
-
650
834
  # build request
651
835
  request = CommonParams(
652
836
  api_ver="1.0.5",
@@ -654,7 +838,7 @@ class CloudIOTGateway:
654
838
  iot_token=self._session_by_authcode_response.data.iotToken,
655
839
  )
656
840
 
657
- # TODO move to using InvokeThingServiceRequest()
841
+ # TODO move to using InvokeThingServiceRequest()
658
842
 
659
843
  message_id = str(uuid.uuid4())
660
844
 
@@ -670,59 +854,126 @@ class CloudIOTGateway:
670
854
  )
671
855
  logger.debug(self.converter.printBase64Binary(command))
672
856
  # send request
673
- 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)
674
859
  logger.debug(response.status_message)
675
860
  logger.debug(response.headers)
676
861
  logger.debug(response.status_code)
677
862
  logger.debug(response.body)
678
863
  logger.debug(iot_id)
679
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
+
680
875
  response_body_str = response.body.decode("utf-8")
681
- response_body_dict = json.loads(response_body_str)
876
+ response_body_dict = self.parse_json_response(response_body_str)
682
877
 
683
878
  if int(response_body_dict.get("code")) != 200:
684
879
  logger.error(
685
880
  "Error in sending cloud command: %s - %s",
686
881
  str(response_body_dict.get("code")),
687
- str(response_body_dict.get("msg")),
882
+ str(response_body_dict.get("message")),
688
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
+
689
891
  if response_body_dict.get("code") == 29003:
690
892
  logger.debug(self._session_by_authcode_response.data.identityId)
691
- self.sign_out()
692
- raise SetupException(response_body_dict.get("code"))
893
+ await self.sign_out()
894
+ raise SetupException(response_body_dict.get("code"), iot_id)
693
895
  if response_body_dict.get("code") == 6205:
694
- raise DeviceOfflineException(response_body_dict.get("code"))
695
- """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
696
904
 
697
905
  return message_id
698
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
+
699
950
  @property
700
951
  def devices_by_account_response(self):
701
952
  return self._devices_by_account_response
702
953
 
703
- def set_http(self, mammotion_http) -> None:
954
+ def set_http(self, mammotion_http: MammotionHTTP) -> None:
704
955
  self.mammotion_http = mammotion_http
705
956
 
706
957
  @property
707
- def region_response(self):
958
+ def region_response(self) -> RegionResponse | None:
708
959
  return self._region_response
709
960
 
710
961
  @property
711
- def aep_response(self):
962
+ def aep_response(self) -> AepResponse | None:
712
963
  return self._aep_response
713
964
 
714
965
  @property
715
- def session_by_authcode_response(self):
966
+ def session_by_authcode_response(self) -> SessionByAuthCodeResponse:
716
967
  return self._session_by_authcode_response
717
968
 
718
969
  @property
719
- def client_id(self):
970
+ def client_id(self) -> str:
720
971
  return self._client_id
721
972
 
722
973
  @property
723
- def login_by_oauth_response(self):
974
+ def login_by_oauth_response(self) -> LoginByOAuthResponse | None:
724
975
  return self._login_by_oauth_response
725
976
 
726
977
  @property
727
- def connect_response(self):
978
+ def connect_response(self) -> ConnectResponse | None:
728
979
  return self._connect_response