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.
- pymammotion/__init__.py +5 -4
- pymammotion/aliyun/client.py +235 -0
- pymammotion/aliyun/cloud_gateway.py +312 -64
- pymammotion/aliyun/model/aep_response.py +1 -2
- pymammotion/aliyun/model/dev_by_account_response.py +170 -23
- pymammotion/aliyun/model/login_by_oauth_response.py +2 -3
- pymammotion/aliyun/model/regions_response.py +3 -3
- pymammotion/aliyun/model/session_by_authcode_response.py +2 -2
- pymammotion/aliyun/model/thing_response.py +12 -0
- pymammotion/aliyun/regions.py +62 -0
- pymammotion/aliyun/tea/core.py +297 -0
- pymammotion/bluetooth/ble.py +7 -9
- pymammotion/bluetooth/ble_message.py +10 -14
- pymammotion/const.py +3 -0
- pymammotion/data/model/__init__.py +1 -2
- pymammotion/data/model/device.py +95 -27
- pymammotion/data/model/device_config.py +4 -4
- pymammotion/data/model/device_info.py +35 -0
- pymammotion/data/model/device_limits.py +10 -10
- pymammotion/data/model/enums.py +12 -2
- pymammotion/data/model/errors.py +12 -0
- pymammotion/data/model/events.py +14 -0
- pymammotion/data/model/generate_geojson.py +521 -0
- pymammotion/data/model/generate_route_information.py +2 -2
- pymammotion/data/model/hash_list.py +370 -57
- pymammotion/data/model/location.py +4 -4
- pymammotion/data/model/mowing_modes.py +17 -1
- pymammotion/data/model/raw_data.py +2 -10
- pymammotion/data/model/region_data.py +10 -11
- pymammotion/data/model/report_info.py +31 -5
- pymammotion/data/model/work.py +27 -0
- pymammotion/data/mower_state_manager.py +316 -0
- pymammotion/data/mqtt/event.py +73 -28
- pymammotion/data/mqtt/mammotion_properties.py +257 -0
- pymammotion/data/mqtt/properties.py +93 -78
- pymammotion/data/mqtt/status.py +18 -17
- pymammotion/event/event.py +27 -6
- pymammotion/homeassistant/__init__.py +3 -0
- pymammotion/homeassistant/mower_api.py +484 -0
- pymammotion/homeassistant/rtk_api.py +54 -0
- pymammotion/http/encryption.py +5 -6
- pymammotion/http/http.py +574 -28
- pymammotion/http/model/__init__.py +0 -0
- pymammotion/{aliyun/model/stream_subscription_response.py → http/model/camera_stream.py} +14 -2
- pymammotion/http/model/http.py +129 -4
- pymammotion/http/model/response_factory.py +61 -0
- pymammotion/http/model/rtk.py +16 -0
- pymammotion/mammotion/commands/abstract_message.py +7 -5
- pymammotion/mammotion/commands/mammotion_command.py +30 -1
- pymammotion/mammotion/commands/messages/basestation.py +43 -0
- pymammotion/mammotion/commands/messages/driver.py +61 -29
- pymammotion/mammotion/commands/messages/media.py +68 -15
- pymammotion/mammotion/commands/messages/navigation.py +61 -25
- pymammotion/mammotion/commands/messages/network.py +17 -23
- pymammotion/mammotion/commands/messages/ota.py +18 -18
- pymammotion/mammotion/commands/messages/system.py +32 -49
- pymammotion/mammotion/commands/messages/video.py +15 -16
- pymammotion/mammotion/devices/__init__.py +27 -3
- pymammotion/mammotion/devices/base.py +40 -131
- pymammotion/mammotion/devices/mammotion.py +436 -201
- pymammotion/mammotion/devices/mammotion_bluetooth.py +57 -47
- pymammotion/mammotion/devices/mammotion_cloud.py +134 -105
- pymammotion/mammotion/devices/mammotion_mower_ble.py +49 -0
- pymammotion/mammotion/devices/mammotion_mower_cloud.py +39 -0
- pymammotion/mammotion/devices/managers/managers.py +81 -0
- pymammotion/mammotion/devices/mower_device.py +124 -0
- pymammotion/mammotion/devices/mower_manager.py +107 -0
- pymammotion/mammotion/devices/rtk_ble.py +89 -0
- pymammotion/mammotion/devices/rtk_cloud.py +113 -0
- pymammotion/mammotion/devices/rtk_device.py +50 -0
- pymammotion/mammotion/devices/rtk_manager.py +122 -0
- pymammotion/mqtt/__init__.py +2 -1
- pymammotion/mqtt/aliyun_mqtt.py +232 -0
- pymammotion/mqtt/linkkit/__init__.py +5 -0
- pymammotion/mqtt/linkkit/h2client.py +585 -0
- pymammotion/mqtt/linkkit/linkkit.py +3023 -0
- pymammotion/mqtt/mammotion_mqtt.py +176 -169
- pymammotion/mqtt/mqtt_models.py +66 -0
- pymammotion/proto/__init__.py +4839 -4
- pymammotion/proto/basestation.proto +8 -0
- pymammotion/proto/basestation_pb2.py +11 -9
- pymammotion/proto/basestation_pb2.pyi +16 -2
- pymammotion/proto/dev_net.proto +79 -55
- pymammotion/proto/dev_net_pb2.py +60 -56
- pymammotion/proto/dev_net_pb2.pyi +49 -6
- pymammotion/proto/luba_msg.proto +2 -1
- pymammotion/proto/luba_msg_pb2.py +6 -6
- pymammotion/proto/luba_msg_pb2.pyi +1 -0
- pymammotion/proto/luba_mul.proto +62 -1
- pymammotion/proto/luba_mul_pb2.py +38 -22
- pymammotion/proto/luba_mul_pb2.pyi +94 -7
- pymammotion/proto/mctrl_driver.proto +44 -4
- pymammotion/proto/mctrl_driver_pb2.py +26 -14
- pymammotion/proto/mctrl_driver_pb2.pyi +66 -11
- pymammotion/proto/mctrl_nav.proto +93 -52
- pymammotion/proto/mctrl_nav_pb2.py +75 -67
- pymammotion/proto/mctrl_nav_pb2.pyi +142 -56
- pymammotion/proto/mctrl_ota.proto +40 -2
- pymammotion/proto/mctrl_ota_pb2.py +23 -13
- pymammotion/proto/mctrl_ota_pb2.pyi +67 -4
- pymammotion/proto/mctrl_pept.proto +8 -3
- pymammotion/proto/mctrl_pept_pb2.py +8 -6
- pymammotion/proto/mctrl_pept_pb2.pyi +14 -6
- pymammotion/proto/mctrl_sys.proto +325 -86
- pymammotion/proto/mctrl_sys_pb2.py +162 -98
- pymammotion/proto/mctrl_sys_pb2.pyi +451 -25
- pymammotion/proto/message_pool.py +3 -0
- pymammotion/proto/py.typed +0 -0
- pymammotion/utility/constant/device_constant.py +29 -5
- pymammotion/utility/datatype_converter.py +13 -12
- pymammotion/utility/device_config.py +522 -130
- pymammotion/utility/device_type.py +218 -21
- pymammotion/utility/map.py +238 -51
- pymammotion/utility/mur_mur_hash.py +159 -0
- {pymammotion-0.4.0a2.dist-info → pymammotion-0.5.51.dist-info}/METADATA +26 -31
- pymammotion-0.5.51.dist-info/RECORD +152 -0
- {pymammotion-0.4.0a2.dist-info → pymammotion-0.5.51.dist-info}/WHEEL +1 -1
- pymammotion/aliyun/cloud_service.py +0 -65
- pymammotion/data/model/plan.py +0 -58
- pymammotion/data/state_manager.py +0 -129
- pymammotion/proto/basestation.py +0 -59
- pymammotion/proto/common.py +0 -12
- pymammotion/proto/dev_net.py +0 -381
- pymammotion/proto/luba_msg.py +0 -81
- pymammotion/proto/luba_mul.py +0 -76
- pymammotion/proto/mctrl_driver.py +0 -100
- pymammotion/proto/mctrl_nav.py +0 -664
- pymammotion/proto/mctrl_ota.py +0 -48
- pymammotion/proto/mctrl_pept.py +0 -41
- pymammotion/proto/mctrl_sys.py +0 -574
- pymammotion-0.4.0a2.dist-info/RECORD +0 -131
- /pymammotion/http/{_init_.py → __init__.py} +0 -0
- {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
|
-
|
|
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:
|
|
126
|
+
dev_by_account: ListingDevAccountResponse | None = None,
|
|
86
127
|
) -> None:
|
|
87
128
|
"""Initialize the CloudIOTGateway."""
|
|
88
|
-
self.mammotion_http: MammotionHTTP
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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 =
|
|
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.
|
|
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 =
|
|
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
|
|
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
|
|
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.
|
|
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 =
|
|
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) ->
|
|
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.
|
|
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 =
|
|
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.
|
|
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 =
|
|
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
|
|
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) ->
|
|
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.
|
|
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 =
|
|
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["
|
|
665
|
+
raise Exception("Error in creating session: " + response_body_dict["message"])
|
|
582
666
|
|
|
583
|
-
self._devices_by_account_response =
|
|
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.
|
|
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 =
|
|
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 =
|
|
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
|
-
"""
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|