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.
- pymammotion/__init__.py +9 -6
- pymammotion/aliyun/client.py +235 -0
- pymammotion/aliyun/cloud_gateway.py +320 -69
- 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 +11 -15
- pymammotion/bluetooth/ble_message.py +389 -106
- pymammotion/bluetooth/model/atomic_integer.py +54 -0
- pymammotion/const.py +3 -0
- pymammotion/data/model/__init__.py +1 -2
- pymammotion/data/model/device.py +92 -240
- pymammotion/data/model/device_config.py +10 -24
- pymammotion/data/model/device_info.py +35 -0
- pymammotion/data/model/device_limits.py +49 -0
- 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 +3 -4
- pymammotion/data/model/hash_list.py +384 -48
- pymammotion/data/model/location.py +4 -4
- pymammotion/data/model/mowing_modes.py +24 -1
- pymammotion/data/model/raw_data.py +215 -0
- pymammotion/data/model/region_data.py +10 -11
- pymammotion/data/model/report_info.py +62 -6
- 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 +32 -8
- pymammotion/homeassistant/__init__.py +3 -0
- pymammotion/homeassistant/mower_api.py +484 -0
- pymammotion/homeassistant/rtk_api.py +54 -0
- pymammotion/http/__init__.py +0 -0
- pymammotion/http/encryption.py +220 -0
- pymammotion/http/http.py +652 -44
- 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 +160 -9
- 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 +32 -3
- 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 +93 -100
- pymammotion/mammotion/commands/messages/ota.py +18 -18
- pymammotion/mammotion/commands/messages/system.py +97 -72
- pymammotion/mammotion/commands/messages/video.py +17 -12
- pymammotion/mammotion/devices/__init__.py +27 -3
- pymammotion/mammotion/devices/base.py +50 -127
- pymammotion/mammotion/devices/mammotion.py +447 -212
- pymammotion/mammotion/devices/mammotion_bluetooth.py +105 -60
- pymammotion/mammotion/devices/mammotion_cloud.py +157 -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 +97 -51
- 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 +65 -21
- pymammotion/utility/datatype_converter.py +13 -12
- pymammotion/utility/device_config.py +755 -0
- pymammotion/utility/device_type.py +218 -21
- pymammotion/utility/map.py +238 -51
- pymammotion/utility/mur_mur_hash.py +159 -0
- {pymammotion-0.2.62.dist-info → pymammotion-0.5.51.dist-info}/METADATA +27 -31
- pymammotion-0.5.51.dist-info/RECORD +152 -0
- {pymammotion-0.2.62.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 -130
- 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 -660
- pymammotion/proto/mctrl_ota.py +0 -48
- pymammotion/proto/mctrl_pept.py +0 -41
- pymammotion/proto/mctrl_sys.py +0 -574
- pymammotion-0.2.62.dist-info/RECORD +0 -125
- /pymammotion/{http/_init_.py → bluetooth/model/__init__.py} +0 -0
- {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
|
-
|
|
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,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 =
|
|
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("
|
|
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.
|
|
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,91 @@ 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())
|
|
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 =
|
|
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.
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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("
|
|
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
|
-
|
|
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
|