pymammotion 0.5.69__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.
- pymammotion/__init__.py +53 -0
- pymammotion/agora/__init__.py +0 -0
- pymammotion/agora/agora_api.py +755 -0
- pymammotion/agora/agora_rtc_capabilities.py +748 -0
- pymammotion/agora/agora_websockets.py +1175 -0
- pymammotion/aliyun/__init__.py +1 -0
- pymammotion/aliyun/client.py +235 -0
- pymammotion/aliyun/cloud_gateway.py +982 -0
- pymammotion/aliyun/model/aep_response.py +21 -0
- pymammotion/aliyun/model/connect_response.py +51 -0
- pymammotion/aliyun/model/dev_by_account_response.py +195 -0
- pymammotion/aliyun/model/login_by_oauth_response.py +64 -0
- pymammotion/aliyun/model/regions_response.py +29 -0
- pymammotion/aliyun/model/session_by_authcode_response.py +19 -0
- pymammotion/aliyun/model/thing_response.py +12 -0
- pymammotion/aliyun/regions.py +62 -0
- pymammotion/aliyun/tea/core.py +297 -0
- pymammotion/aliyun/tmp_constant.py +171 -0
- pymammotion/bluetooth/__init__.py +1 -0
- pymammotion/bluetooth/ble.py +62 -0
- pymammotion/bluetooth/ble_message.py +676 -0
- pymammotion/bluetooth/const.py +27 -0
- pymammotion/bluetooth/data/__init__.py +0 -0
- pymammotion/bluetooth/data/convert.py +25 -0
- pymammotion/bluetooth/data/framectrldata.py +40 -0
- pymammotion/bluetooth/data/notifydata.py +62 -0
- pymammotion/bluetooth/model/__init__.py +0 -0
- pymammotion/bluetooth/model/atomic_integer.py +54 -0
- pymammotion/const.py +13 -0
- pymammotion/data/__init__.py +0 -0
- pymammotion/data/model/__init__.py +8 -0
- pymammotion/data/model/account.py +8 -0
- pymammotion/data/model/device.py +192 -0
- pymammotion/data/model/device_config.py +72 -0
- pymammotion/data/model/device_info.py +60 -0
- pymammotion/data/model/device_limits.py +49 -0
- pymammotion/data/model/enums.py +77 -0
- pymammotion/data/model/errors.py +12 -0
- pymammotion/data/model/events.py +14 -0
- pymammotion/data/model/generate_geojson.py +565 -0
- pymammotion/data/model/generate_route_information.py +26 -0
- pymammotion/data/model/hash_list.py +475 -0
- pymammotion/data/model/location.py +36 -0
- pymammotion/data/model/mowing_modes.py +77 -0
- pymammotion/data/model/rapid_state.py +45 -0
- pymammotion/data/model/raw_data.py +215 -0
- pymammotion/data/model/region_data.py +102 -0
- pymammotion/data/model/report_info.py +182 -0
- pymammotion/data/model/work.py +27 -0
- pymammotion/data/mower_state_manager.py +369 -0
- pymammotion/data/mqtt/__init__.py +1 -0
- pymammotion/data/mqtt/event.py +227 -0
- pymammotion/data/mqtt/mammotion_properties.py +276 -0
- pymammotion/data/mqtt/properties.py +203 -0
- pymammotion/data/mqtt/status.py +57 -0
- pymammotion/event/__init__.py +6 -0
- pymammotion/event/event.py +96 -0
- pymammotion/homeassistant/__init__.py +3 -0
- pymammotion/homeassistant/mower_api.py +514 -0
- pymammotion/homeassistant/rtk_api.py +54 -0
- pymammotion/http/__init__.py +0 -0
- pymammotion/http/encryption.py +220 -0
- pymammotion/http/http.py +673 -0
- pymammotion/http/model/__init__.py +0 -0
- pymammotion/http/model/camera_stream.py +31 -0
- pymammotion/http/model/http.py +249 -0
- pymammotion/http/model/response_factory.py +61 -0
- pymammotion/http/model/rtk.py +16 -0
- pymammotion/mammotion/__init__.py +0 -0
- pymammotion/mammotion/commands/__init__.py +0 -0
- pymammotion/mammotion/commands/abstract_message.py +24 -0
- pymammotion/mammotion/commands/mammotion_command.py +81 -0
- pymammotion/mammotion/commands/messages/__init__.py +0 -0
- pymammotion/mammotion/commands/messages/basestation.py +43 -0
- pymammotion/mammotion/commands/messages/driver.py +122 -0
- pymammotion/mammotion/commands/messages/media.py +87 -0
- pymammotion/mammotion/commands/messages/navigation.py +564 -0
- pymammotion/mammotion/commands/messages/network.py +205 -0
- pymammotion/mammotion/commands/messages/ota.py +38 -0
- pymammotion/mammotion/commands/messages/system.py +330 -0
- pymammotion/mammotion/commands/messages/video.py +33 -0
- pymammotion/mammotion/control/__init__.py +0 -0
- pymammotion/mammotion/control/joystick.py +145 -0
- pymammotion/mammotion/devices/__init__.py +29 -0
- pymammotion/mammotion/devices/base.py +163 -0
- pymammotion/mammotion/devices/mammotion.py +571 -0
- pymammotion/mammotion/devices/mammotion_bluetooth.py +496 -0
- pymammotion/mammotion/devices/mammotion_cloud.py +355 -0
- pymammotion/mammotion/devices/mammotion_mower_ble.py +48 -0
- pymammotion/mammotion/devices/mammotion_mower_cloud.py +39 -0
- pymammotion/mammotion/devices/managers/managers.py +81 -0
- pymammotion/mammotion/devices/mower_device.py +120 -0
- pymammotion/mammotion/devices/mower_manager.py +107 -0
- pymammotion/mammotion/devices/rtk_ble.py +89 -0
- pymammotion/mammotion/devices/rtk_cloud.py +115 -0
- pymammotion/mammotion/devices/rtk_device.py +50 -0
- pymammotion/mammotion/devices/rtk_manager.py +125 -0
- pymammotion/mqtt/__init__.py +6 -0
- pymammotion/mqtt/aliyun_mqtt.py +237 -0
- pymammotion/mqtt/linkkit/__init__.py +5 -0
- pymammotion/mqtt/linkkit/h2client.py +585 -0
- pymammotion/mqtt/linkkit/linkkit.py +3025 -0
- pymammotion/mqtt/mammotion_future.py +26 -0
- pymammotion/mqtt/mammotion_mqtt.py +214 -0
- pymammotion/mqtt/mqtt_models.py +66 -0
- pymammotion/proto/__init__.py +4841 -0
- pymammotion/proto/basestation.proto +51 -0
- pymammotion/proto/basestation_pb2.py +35 -0
- pymammotion/proto/basestation_pb2.pyi +89 -0
- pymammotion/proto/common.proto +7 -0
- pymammotion/proto/common_pb2.py +25 -0
- pymammotion/proto/common_pb2.pyi +13 -0
- pymammotion/proto/dev_net.proto +321 -0
- pymammotion/proto/dev_net_pb2.py +111 -0
- pymammotion/proto/dev_net_pb2.pyi +515 -0
- pymammotion/proto/luba_msg.proto +76 -0
- pymammotion/proto/luba_msg_pb2.py +41 -0
- pymammotion/proto/luba_msg_pb2.pyi +97 -0
- pymammotion/proto/luba_mul.proto +129 -0
- pymammotion/proto/luba_mul_pb2.py +61 -0
- pymammotion/proto/luba_mul_pb2.pyi +178 -0
- pymammotion/proto/mctrl_driver.proto +107 -0
- pymammotion/proto/mctrl_driver_pb2.py +57 -0
- pymammotion/proto/mctrl_driver_pb2.pyi +167 -0
- pymammotion/proto/mctrl_nav.proto +591 -0
- pymammotion/proto/mctrl_nav_pb2.py +136 -0
- pymammotion/proto/mctrl_nav_pb2.pyi +1067 -0
- pymammotion/proto/mctrl_ota.proto +80 -0
- pymammotion/proto/mctrl_ota_pb2.py +45 -0
- pymammotion/proto/mctrl_ota_pb2.pyi +128 -0
- pymammotion/proto/mctrl_pept.proto +34 -0
- pymammotion/proto/mctrl_pept_pb2.py +33 -0
- pymammotion/proto/mctrl_pept_pb2.pyi +58 -0
- pymammotion/proto/mctrl_sys.proto +741 -0
- pymammotion/proto/mctrl_sys_pb2.py +206 -0
- pymammotion/proto/mctrl_sys_pb2.pyi +1213 -0
- pymammotion/proto/message_pool.py +3 -0
- pymammotion/proto/py.typed +0 -0
- pymammotion/py.typed +0 -0
- pymammotion/utility/constant/__init__.py +3 -0
- pymammotion/utility/constant/device_constant.py +315 -0
- pymammotion/utility/conversions.py +5 -0
- pymammotion/utility/datatype_converter.py +124 -0
- pymammotion/utility/device_config.py +755 -0
- pymammotion/utility/device_type.py +489 -0
- pymammotion/utility/map.py +259 -0
- pymammotion/utility/movement.py +18 -0
- pymammotion/utility/mur_mur_hash.py +159 -0
- pymammotion/utility/periodic.py +106 -0
- pymammotion/utility/rocker_util.py +194 -0
- pymammotion-0.5.69.dist-info/METADATA +93 -0
- pymammotion-0.5.69.dist-info/RECORD +154 -0
- pymammotion-0.5.69.dist-info/WHEEL +4 -0
- pymammotion-0.5.69.dist-info/licenses/LICENSE +674 -0
|
@@ -0,0 +1,982 @@
|
|
|
1
|
+
"""Module for interacting with Aliyun Cloud IoT Gateway."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import base64
|
|
5
|
+
import hashlib
|
|
6
|
+
import hmac
|
|
7
|
+
import itertools
|
|
8
|
+
import json
|
|
9
|
+
from json.decoder import JSONDecodeError
|
|
10
|
+
from logging import getLogger
|
|
11
|
+
import random
|
|
12
|
+
import string
|
|
13
|
+
import time
|
|
14
|
+
import uuid
|
|
15
|
+
|
|
16
|
+
from aiohttp import ClientSession, ConnectionTimeoutError
|
|
17
|
+
from alibabacloud_iot_api_gateway.models import CommonParams, Config, IoTApiRequest
|
|
18
|
+
from alibabacloud_tea_util.client import Client as UtilClient
|
|
19
|
+
from alibabacloud_tea_util.models import RuntimeOptions
|
|
20
|
+
from Tea.exceptions import UnretryableException
|
|
21
|
+
|
|
22
|
+
from pymammotion.aliyun.client import Client
|
|
23
|
+
from pymammotion.aliyun.model.aep_response import AepResponse
|
|
24
|
+
from pymammotion.aliyun.model.connect_response import ConnectResponse
|
|
25
|
+
from pymammotion.aliyun.model.dev_by_account_response import ListingDevAccountResponse
|
|
26
|
+
from pymammotion.aliyun.model.login_by_oauth_response import LoginByOAuthResponse
|
|
27
|
+
from pymammotion.aliyun.model.regions_response import RegionResponse
|
|
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
|
|
31
|
+
from pymammotion.const import ALIYUN_DOMAIN, APP_KEY, APP_SECRET, APP_VERSION
|
|
32
|
+
from pymammotion.http.http import MammotionHTTP
|
|
33
|
+
from pymammotion.utility.datatype_converter import DatatypeConverter
|
|
34
|
+
|
|
35
|
+
logger = getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
MOVE_HEADERS = (
|
|
38
|
+
"x-ca-signature",
|
|
39
|
+
"x-ca-signature-headers",
|
|
40
|
+
"accept",
|
|
41
|
+
"content-md5",
|
|
42
|
+
"content-type",
|
|
43
|
+
"date",
|
|
44
|
+
"host",
|
|
45
|
+
"token",
|
|
46
|
+
"user-agent",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class SetupException(Exception):
|
|
51
|
+
"""Raise when mqtt expires token or token is invalid."""
|
|
52
|
+
|
|
53
|
+
def __init__(self, *args: object) -> None:
|
|
54
|
+
super().__init__(args)
|
|
55
|
+
self.iot_id = args[1]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class AuthRefreshException(Exception):
|
|
59
|
+
"""Raise exception when library cannot refresh token."""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class DeviceOfflineException(Exception):
|
|
63
|
+
"""Raise exception when device is offline."""
|
|
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
|
+
|
|
97
|
+
|
|
98
|
+
class LoginException(Exception):
|
|
99
|
+
"""Raise exception when library cannot log in."""
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class CheckSessionException(Exception):
|
|
103
|
+
"""Raise exception when checking session results in a failure."""
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
EXPIRED_CREDENTIAL_EXCEPTIONS = (CheckSessionException, SetupException)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class CloudIOTGateway:
|
|
110
|
+
"""Class for interacting with Aliyun Cloud IoT Gateway."""
|
|
111
|
+
|
|
112
|
+
_client_id = ""
|
|
113
|
+
_device_sn = ""
|
|
114
|
+
_utdid = ""
|
|
115
|
+
|
|
116
|
+
converter = DatatypeConverter()
|
|
117
|
+
|
|
118
|
+
def __init__(
|
|
119
|
+
self,
|
|
120
|
+
mammotion_http: MammotionHTTP,
|
|
121
|
+
connect_response: ConnectResponse | None = None,
|
|
122
|
+
login_by_oauth_response: LoginByOAuthResponse | None = None,
|
|
123
|
+
aep_response: AepResponse | None = None,
|
|
124
|
+
session_by_authcode_response: SessionByAuthCodeResponse | None = None,
|
|
125
|
+
region_response: RegionResponse | None = None,
|
|
126
|
+
dev_by_account: ListingDevAccountResponse | None = None,
|
|
127
|
+
) -> None:
|
|
128
|
+
"""Initialize the CloudIOTGateway."""
|
|
129
|
+
self.mammotion_http: MammotionHTTP = mammotion_http
|
|
130
|
+
self._app_key = APP_KEY
|
|
131
|
+
self._app_secret = APP_SECRET
|
|
132
|
+
self.domain = ALIYUN_DOMAIN
|
|
133
|
+
self.message_delay = 1
|
|
134
|
+
self._client_id = self.generate_hardware_string(8) # 8 characters
|
|
135
|
+
self._device_sn = self.generate_hardware_string(32) # 32 characters
|
|
136
|
+
self._utdid = self.generate_hardware_string(32) # 32 characters
|
|
137
|
+
self._connect_response = connect_response
|
|
138
|
+
self._login_by_oauth_response = login_by_oauth_response
|
|
139
|
+
self._aep_response = aep_response
|
|
140
|
+
self._session_by_authcode_response = session_by_authcode_response
|
|
141
|
+
self._region_response = region_response
|
|
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
|
+
)
|
|
150
|
+
|
|
151
|
+
@staticmethod
|
|
152
|
+
def generate_random_string(length: int) -> str:
|
|
153
|
+
"""Generate a random string of specified length."""
|
|
154
|
+
characters = string.ascii_letters + string.digits
|
|
155
|
+
return "".join(random.choice(characters) for _ in range(length))
|
|
156
|
+
|
|
157
|
+
@staticmethod
|
|
158
|
+
def generate_hardware_string(length: int) -> str:
|
|
159
|
+
"""Generate hardware string that is consistent per device."""
|
|
160
|
+
hashed_uuid = hashlib.sha1(f"{uuid.getnode()}".encode()).hexdigest()
|
|
161
|
+
return "".join(itertools.islice(itertools.cycle(hashed_uuid), length))
|
|
162
|
+
|
|
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:
|
|
172
|
+
"""Generate signature for the given data."""
|
|
173
|
+
keys = ["appKey", "clientId", "deviceSn", "timestamp"]
|
|
174
|
+
concatenated_str = ""
|
|
175
|
+
for key in keys:
|
|
176
|
+
concatenated_str += f"{key}{data.get(key, '')}"
|
|
177
|
+
|
|
178
|
+
logger.debug("sign(), toSignStr = %s", concatenated_str)
|
|
179
|
+
|
|
180
|
+
return hmac.new(
|
|
181
|
+
self._app_secret.encode("utf-8"),
|
|
182
|
+
concatenated_str.encode("utf-8"),
|
|
183
|
+
hashlib.sha1,
|
|
184
|
+
).hexdigest()
|
|
185
|
+
|
|
186
|
+
async def get_region(self, country_code: str) -> RegionResponse:
|
|
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
|
+
|
|
193
|
+
config = Config(
|
|
194
|
+
app_key=self._app_key,
|
|
195
|
+
app_secret=self._app_secret,
|
|
196
|
+
domain=self.domain,
|
|
197
|
+
)
|
|
198
|
+
client = Client(config)
|
|
199
|
+
|
|
200
|
+
# build request
|
|
201
|
+
request = CommonParams(api_ver="1.0.2", language="en-US")
|
|
202
|
+
body = IoTApiRequest(
|
|
203
|
+
id=str(uuid.uuid4()),
|
|
204
|
+
params={
|
|
205
|
+
"authCode": auth_code,
|
|
206
|
+
"type": "THIRD_AUTHCODE",
|
|
207
|
+
"countryCode": country_code,
|
|
208
|
+
},
|
|
209
|
+
request=request,
|
|
210
|
+
version="1.0",
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
# send request
|
|
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
|
|
236
|
+
# Decode the response body
|
|
237
|
+
response_body_str = response.body.decode("utf-8")
|
|
238
|
+
|
|
239
|
+
# Load the JSON string into a dictionary
|
|
240
|
+
response_body_dict = self.parse_json_response(response_body_str)
|
|
241
|
+
|
|
242
|
+
if int(response_body_dict.get("code")) != 200:
|
|
243
|
+
raise Exception("Error in getting regions: " + response_body_dict["msg"])
|
|
244
|
+
|
|
245
|
+
self._region_response = RegionResponse.from_dict(response_body_dict)
|
|
246
|
+
logger.debug("Endpoint: %s", self._region_response.data.mqttEndpoint)
|
|
247
|
+
|
|
248
|
+
return response.body
|
|
249
|
+
|
|
250
|
+
async def aep_handle(self) -> AepResponse:
|
|
251
|
+
"""Handle AEP authentication."""
|
|
252
|
+
aep_domain = self.domain
|
|
253
|
+
|
|
254
|
+
if self._region_response.data.apiGatewayEndpoint is not None:
|
|
255
|
+
aep_domain = self._region_response.data.apiGatewayEndpoint
|
|
256
|
+
|
|
257
|
+
config = Config(
|
|
258
|
+
app_key=self._app_key,
|
|
259
|
+
app_secret=self._app_secret,
|
|
260
|
+
domain=aep_domain,
|
|
261
|
+
)
|
|
262
|
+
client = Client(config)
|
|
263
|
+
|
|
264
|
+
request = CommonParams(api_ver="1.0.0", language="en-US")
|
|
265
|
+
logger.debug("client id %s", self._client_id)
|
|
266
|
+
time_now = time.time()
|
|
267
|
+
data_to_sign = {
|
|
268
|
+
"appKey": self._app_key,
|
|
269
|
+
"clientId": self._client_id, # needs to be unique to device
|
|
270
|
+
"deviceSn": self._device_sn, # same here
|
|
271
|
+
"timestamp": str(time_now),
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
body = IoTApiRequest(
|
|
275
|
+
id=str(uuid.uuid4()),
|
|
276
|
+
params={
|
|
277
|
+
"authInfo": {
|
|
278
|
+
"clientId": self._client_id,
|
|
279
|
+
"sign": self.sign(data_to_sign),
|
|
280
|
+
"deviceSn": self._device_sn,
|
|
281
|
+
"timestamp": str(time_now),
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
request=request,
|
|
285
|
+
version="1.0",
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
# send request
|
|
289
|
+
response = await client.async_do_request("/app/aepauth/handle", "https", "POST", None, body, RuntimeOptions())
|
|
290
|
+
logger.debug(response.status_message)
|
|
291
|
+
logger.debug(response.headers)
|
|
292
|
+
logger.debug(response.status_code)
|
|
293
|
+
logger.debug(response.body)
|
|
294
|
+
|
|
295
|
+
response_body_str = response.body.decode("utf-8")
|
|
296
|
+
|
|
297
|
+
response_body_dict = self.parse_json_response(response_body_str)
|
|
298
|
+
|
|
299
|
+
if int(response_body_dict.get("code")) != 200:
|
|
300
|
+
raise Exception("Error in getting mqtt credentials: " + response_body_dict["msg"])
|
|
301
|
+
|
|
302
|
+
self._aep_response = AepResponse.from_dict(response_body_dict)
|
|
303
|
+
|
|
304
|
+
logger.debug(response_body_dict)
|
|
305
|
+
|
|
306
|
+
return self._aep_response
|
|
307
|
+
|
|
308
|
+
async def connect(self) -> ConnectResponse:
|
|
309
|
+
"""Connect to the Aliyun Cloud IoT Gateway."""
|
|
310
|
+
region_url = "sdk.openaccount.aliyun.com"
|
|
311
|
+
time_now = time.time()
|
|
312
|
+
async with ClientSession() as session:
|
|
313
|
+
headers = {
|
|
314
|
+
"host": region_url,
|
|
315
|
+
"date": UtilClient.get_date_utcstring(),
|
|
316
|
+
"x-ca-nonce": UtilClient.get_nonce(),
|
|
317
|
+
"x-ca-key": self._app_key,
|
|
318
|
+
"x-ca-signaturemethod": "HmacSHA256",
|
|
319
|
+
"accept": "application/json",
|
|
320
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
321
|
+
"user-agent": UtilClient.get_user_agent(None),
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
_bodyParam = {
|
|
325
|
+
"context": {
|
|
326
|
+
"sdkVersion": "3.4.2",
|
|
327
|
+
"platformName": "android",
|
|
328
|
+
"netType": "wifi",
|
|
329
|
+
"appKey": self._app_key,
|
|
330
|
+
"yunOSId": "",
|
|
331
|
+
"appVersion": APP_VERSION,
|
|
332
|
+
"utDid": self._utdid,
|
|
333
|
+
"appAuthToken": self._utdid, # ???
|
|
334
|
+
"securityToken": self._utdid, # ???
|
|
335
|
+
},
|
|
336
|
+
"config": {"version": 0, "lastModify": 0},
|
|
337
|
+
"device": {
|
|
338
|
+
"model": "sdk_gphone_x86_arm",
|
|
339
|
+
"brand": "goldfish_x86",
|
|
340
|
+
"platformVersion": "30",
|
|
341
|
+
},
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
# Get sign header
|
|
345
|
+
dic = headers.copy()
|
|
346
|
+
for key in MOVE_HEADERS:
|
|
347
|
+
dic.pop(key, None)
|
|
348
|
+
|
|
349
|
+
keys = sorted(dic.keys())
|
|
350
|
+
sign_headers = ",".join(keys)
|
|
351
|
+
header = "".join(f"{k}:{dic[k]}\n" for k in keys).strip()
|
|
352
|
+
|
|
353
|
+
headers["x-ca-signature-headers"] = sign_headers
|
|
354
|
+
string_to_sign = "POST\n{}\n\n{}\n{}\n{}\n/api/prd/connect.json?request={}".format(
|
|
355
|
+
headers["accept"],
|
|
356
|
+
headers["content-type"],
|
|
357
|
+
headers["date"],
|
|
358
|
+
header,
|
|
359
|
+
json.dumps(_bodyParam, separators=(",", ":")),
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
hash_val = hmac.new(
|
|
363
|
+
self._app_secret.encode("utf-8"),
|
|
364
|
+
string_to_sign.encode("utf-8"),
|
|
365
|
+
hashlib.sha256,
|
|
366
|
+
).digest()
|
|
367
|
+
signature = base64.b64encode(hash_val).decode("utf-8")
|
|
368
|
+
headers["x-ca-signature"] = signature
|
|
369
|
+
|
|
370
|
+
async with session.post(
|
|
371
|
+
f"https://{region_url}/api/prd/connect.json",
|
|
372
|
+
headers=headers,
|
|
373
|
+
params={"request": json.dumps(_bodyParam, separators=(",", ":"))},
|
|
374
|
+
) as resp:
|
|
375
|
+
data = await resp.json()
|
|
376
|
+
logger.debug(data)
|
|
377
|
+
if resp.status == 200:
|
|
378
|
+
self._connect_response = ConnectResponse.from_dict(data)
|
|
379
|
+
return self._connect_response
|
|
380
|
+
raise LoginException(data)
|
|
381
|
+
|
|
382
|
+
async def login_by_oauth(self, country_code: str):
|
|
383
|
+
"""Login by OAuth."""
|
|
384
|
+
auth_code = self.mammotion_http.login_info.authorization_code
|
|
385
|
+
region_url = self._region_response.data.oaApiGatewayEndpoint
|
|
386
|
+
|
|
387
|
+
async with ClientSession() as session:
|
|
388
|
+
headers = {
|
|
389
|
+
"host": region_url,
|
|
390
|
+
"date": UtilClient.get_date_utcstring(),
|
|
391
|
+
"x-ca-nonce": UtilClient.get_nonce(),
|
|
392
|
+
"x-ca-key": self._app_key,
|
|
393
|
+
"x-ca-signaturemethod": "HmacSHA256",
|
|
394
|
+
"accept": "application/json",
|
|
395
|
+
"content-type": "application/x-www-form-urlencoded; charset=utf-8",
|
|
396
|
+
"user-agent": UtilClient.get_user_agent(None),
|
|
397
|
+
"vid": self._connect_response.data.vid,
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
_bodyParam = {
|
|
401
|
+
"country": country_code,
|
|
402
|
+
"authCode": auth_code,
|
|
403
|
+
"oauthPlateform": 23,
|
|
404
|
+
"oauthAppKey": self._app_key,
|
|
405
|
+
"riskControlInfo": {
|
|
406
|
+
"appID": "com.agilexrobotics",
|
|
407
|
+
"appAuthToken": "",
|
|
408
|
+
"signType": "RSA",
|
|
409
|
+
"sdkVersion": "3.4.2",
|
|
410
|
+
"utdid": self._utdid,
|
|
411
|
+
"umidToken": self._utdid,
|
|
412
|
+
"deviceId": self._connect_response.data.data.device.data.deviceId,
|
|
413
|
+
"USE_OA_PWD_ENCRYPT": "true",
|
|
414
|
+
"USE_H5_NC": "true",
|
|
415
|
+
},
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
# Get sign header
|
|
419
|
+
dic = headers.copy()
|
|
420
|
+
for key in MOVE_HEADERS:
|
|
421
|
+
dic.pop(key, None)
|
|
422
|
+
|
|
423
|
+
keys = sorted(dic.keys())
|
|
424
|
+
sign_headers = ",".join(keys)
|
|
425
|
+
header = "".join(f"{k}:{dic[k]}\n" for k in keys).strip()
|
|
426
|
+
|
|
427
|
+
headers["x-ca-signature-headers"] = sign_headers
|
|
428
|
+
string_to_sign = "POST\n{}\n\n{}\n{}\n{}\n/api/prd/loginbyoauth.json?{}".format(
|
|
429
|
+
headers["accept"],
|
|
430
|
+
headers["content-type"],
|
|
431
|
+
headers["date"],
|
|
432
|
+
header,
|
|
433
|
+
f"loginByOauthRequest={json.dumps(_bodyParam, separators=(",", ":"))}",
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
hash_val = hmac.new(
|
|
437
|
+
self._app_secret.encode("utf-8"),
|
|
438
|
+
string_to_sign.encode("utf-8"),
|
|
439
|
+
hashlib.sha256,
|
|
440
|
+
).digest()
|
|
441
|
+
signature = base64.b64encode(hash_val).decode("utf-8")
|
|
442
|
+
headers["x-ca-signature"] = signature
|
|
443
|
+
async with session.post(
|
|
444
|
+
f"https://{region_url}/api/prd/loginbyoauth.json",
|
|
445
|
+
headers=headers,
|
|
446
|
+
data={"loginByOauthRequest": json.dumps(_bodyParam, separators=(",", ":"))},
|
|
447
|
+
) as resp:
|
|
448
|
+
data = await resp.json()
|
|
449
|
+
logger.debug(data)
|
|
450
|
+
if resp.status == 200:
|
|
451
|
+
self._login_by_oauth_response = LoginByOAuthResponse.from_dict(data)
|
|
452
|
+
return self._login_by_oauth_response
|
|
453
|
+
raise LoginException(data)
|
|
454
|
+
|
|
455
|
+
async def session_by_auth_code(self) -> SessionByAuthCodeResponse:
|
|
456
|
+
"""Create a session by auth code."""
|
|
457
|
+
config = Config(
|
|
458
|
+
app_key=self._app_key,
|
|
459
|
+
app_secret=self._app_secret,
|
|
460
|
+
domain=self._region_response.data.apiGatewayEndpoint,
|
|
461
|
+
)
|
|
462
|
+
client = Client(config)
|
|
463
|
+
|
|
464
|
+
# build request
|
|
465
|
+
request = CommonParams(api_ver="1.0.4", language="en-US")
|
|
466
|
+
body = IoTApiRequest(
|
|
467
|
+
id=str(uuid.uuid4()),
|
|
468
|
+
params={
|
|
469
|
+
"request": {
|
|
470
|
+
"authCode": self._login_by_oauth_response.data.data.loginSuccessResult.sid,
|
|
471
|
+
"accountType": "OA_SESSION",
|
|
472
|
+
"appKey": self._app_key,
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
request=request,
|
|
476
|
+
version="1.0",
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
# send request
|
|
480
|
+
response = await client.async_do_request(
|
|
481
|
+
"/account/createSessionByAuthCode",
|
|
482
|
+
"https",
|
|
483
|
+
"POST",
|
|
484
|
+
None,
|
|
485
|
+
body,
|
|
486
|
+
RuntimeOptions(),
|
|
487
|
+
)
|
|
488
|
+
logger.debug(response.status_message)
|
|
489
|
+
logger.debug(response.headers)
|
|
490
|
+
logger.debug(response.status_code)
|
|
491
|
+
logger.debug(response.body)
|
|
492
|
+
|
|
493
|
+
# Decode the response body
|
|
494
|
+
response_body_str = response.body.decode("utf-8")
|
|
495
|
+
|
|
496
|
+
# Load the JSON string into a dictionary
|
|
497
|
+
response_body_dict = self.parse_json_response(response_body_str)
|
|
498
|
+
|
|
499
|
+
session_by_auth = SessionByAuthCodeResponse.from_dict(response_body_dict)
|
|
500
|
+
|
|
501
|
+
if int(session_by_auth.code) != 200:
|
|
502
|
+
raise Exception("Error in creating session: " + response_body_str)
|
|
503
|
+
|
|
504
|
+
if session_by_auth.data.identityId is None:
|
|
505
|
+
raise Exception("Error in creating session: " + response_body_str)
|
|
506
|
+
|
|
507
|
+
self._session_by_authcode_response = session_by_auth
|
|
508
|
+
self._iot_token_issued_at = int(time.time())
|
|
509
|
+
|
|
510
|
+
return response.body
|
|
511
|
+
|
|
512
|
+
async def sign_out(self) -> dict:
|
|
513
|
+
config = Config(
|
|
514
|
+
app_key=self._app_key,
|
|
515
|
+
app_secret=self._app_secret,
|
|
516
|
+
domain=self._region_response.data.apiGatewayEndpoint,
|
|
517
|
+
)
|
|
518
|
+
client = Client(config)
|
|
519
|
+
|
|
520
|
+
# build request
|
|
521
|
+
request = CommonParams(api_ver="1.0.4", language="en-US")
|
|
522
|
+
body = IoTApiRequest(
|
|
523
|
+
id=str(uuid.uuid4()),
|
|
524
|
+
params={
|
|
525
|
+
"request": {
|
|
526
|
+
"refreshToken": self._session_by_authcode_response.data.refreshToken,
|
|
527
|
+
"identityId": self._session_by_authcode_response.data.identityId,
|
|
528
|
+
}
|
|
529
|
+
},
|
|
530
|
+
request=request,
|
|
531
|
+
version="1.0",
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
# send request
|
|
535
|
+
# possibly need to do this ourselves
|
|
536
|
+
response = await client.async_do_request(
|
|
537
|
+
"/iotx/account/invalidSession",
|
|
538
|
+
"https",
|
|
539
|
+
"POST",
|
|
540
|
+
None,
|
|
541
|
+
body,
|
|
542
|
+
RuntimeOptions(),
|
|
543
|
+
)
|
|
544
|
+
logger.debug(response.status_message)
|
|
545
|
+
logger.debug(response.headers)
|
|
546
|
+
logger.debug(response.status_code)
|
|
547
|
+
logger.debug(response.body)
|
|
548
|
+
|
|
549
|
+
# Decode the response body
|
|
550
|
+
response_body_str = response.body.decode("utf-8")
|
|
551
|
+
|
|
552
|
+
# Load the JSON string into a dictionary
|
|
553
|
+
response_body_dict = self.parse_json_response(response_body_str)
|
|
554
|
+
return response_body_dict
|
|
555
|
+
|
|
556
|
+
async def check_or_refresh_session(self) -> None:
|
|
557
|
+
"""Check or refresh the session."""
|
|
558
|
+
logger.debug("Trying to refresh token")
|
|
559
|
+
config = Config(
|
|
560
|
+
app_key=self._app_key,
|
|
561
|
+
app_secret=self._app_secret,
|
|
562
|
+
domain=self._region_response.data.apiGatewayEndpoint,
|
|
563
|
+
)
|
|
564
|
+
client = Client(config)
|
|
565
|
+
|
|
566
|
+
# build request
|
|
567
|
+
request = CommonParams(api_ver="1.0.4", language="en-US")
|
|
568
|
+
body = IoTApiRequest(
|
|
569
|
+
id=str(uuid.uuid4()),
|
|
570
|
+
params={
|
|
571
|
+
"request": {
|
|
572
|
+
"refreshToken": self._session_by_authcode_response.data.refreshToken,
|
|
573
|
+
"identityId": self._session_by_authcode_response.data.identityId,
|
|
574
|
+
}
|
|
575
|
+
},
|
|
576
|
+
request=request,
|
|
577
|
+
version="1.0",
|
|
578
|
+
)
|
|
579
|
+
|
|
580
|
+
# send request
|
|
581
|
+
# possibly need to do this ourselves
|
|
582
|
+
response = await client.async_do_request(
|
|
583
|
+
"/account/checkOrRefreshSession",
|
|
584
|
+
"https",
|
|
585
|
+
"POST",
|
|
586
|
+
None,
|
|
587
|
+
body,
|
|
588
|
+
RuntimeOptions(),
|
|
589
|
+
)
|
|
590
|
+
logger.debug(response.status_message)
|
|
591
|
+
logger.debug(response.headers)
|
|
592
|
+
logger.debug(response.status_code)
|
|
593
|
+
logger.debug(response.body)
|
|
594
|
+
|
|
595
|
+
# Decode the response body
|
|
596
|
+
response_body_str = response.body.decode("utf-8")
|
|
597
|
+
|
|
598
|
+
# Load the JSON string into a dictionary
|
|
599
|
+
response_body_dict = self.parse_json_response(response_body_str)
|
|
600
|
+
|
|
601
|
+
if int(response_body_dict.get("code")) != 200:
|
|
602
|
+
logger.error(response_body_dict)
|
|
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()
|
|
608
|
+
raise CheckSessionException("Error check or refresh token: " + response_body_dict.__str__())
|
|
609
|
+
|
|
610
|
+
session = SessionByAuthCodeResponse.from_dict(response_body_dict)
|
|
611
|
+
session_data = session.data
|
|
612
|
+
|
|
613
|
+
if (
|
|
614
|
+
session_data is None
|
|
615
|
+
or session_data.identityId is None
|
|
616
|
+
or session_data.refreshTokenExpire is None
|
|
617
|
+
or session_data.iotToken is None
|
|
618
|
+
or session_data.iotTokenExpire is None
|
|
619
|
+
or session_data.refreshToken is None
|
|
620
|
+
):
|
|
621
|
+
raise Exception("Error check or refresh token: Parameters not correct")
|
|
622
|
+
|
|
623
|
+
self._session_by_authcode_response = session
|
|
624
|
+
self._iot_token_issued_at = int(time.time())
|
|
625
|
+
|
|
626
|
+
async def list_binding_by_account(self) -> ListingDevAccountResponse:
|
|
627
|
+
"""List bindings by account."""
|
|
628
|
+
config = Config(
|
|
629
|
+
app_key=self._app_key,
|
|
630
|
+
app_secret=self._app_secret,
|
|
631
|
+
domain=self._region_response.data.apiGatewayEndpoint,
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
client = Client(config)
|
|
635
|
+
|
|
636
|
+
# build request
|
|
637
|
+
request = CommonParams(
|
|
638
|
+
api_ver="1.0.8",
|
|
639
|
+
language="en-US",
|
|
640
|
+
iot_token=self._session_by_authcode_response.data.iotToken,
|
|
641
|
+
)
|
|
642
|
+
body = IoTApiRequest(
|
|
643
|
+
id=str(uuid.uuid4()),
|
|
644
|
+
params={"pageSize": 100, "pageNo": 1},
|
|
645
|
+
request=request,
|
|
646
|
+
version="1.0",
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
# send request
|
|
650
|
+
response = await client.async_do_request(
|
|
651
|
+
"/uc/listBindingByAccount", "https", "POST", None, body, RuntimeOptions()
|
|
652
|
+
)
|
|
653
|
+
logger.debug(response.status_message)
|
|
654
|
+
logger.debug(response.headers)
|
|
655
|
+
logger.debug(response.status_code)
|
|
656
|
+
logger.debug(response.body)
|
|
657
|
+
|
|
658
|
+
# Decode the response body
|
|
659
|
+
response_body_str = response.body.decode("utf-8")
|
|
660
|
+
|
|
661
|
+
# Load the JSON string into a dictionary
|
|
662
|
+
response_body_dict = self.parse_json_response(response_body_str)
|
|
663
|
+
|
|
664
|
+
if int(response_body_dict.get("code")) != 200:
|
|
665
|
+
raise Exception("Error in creating session: " + response_body_dict["message"])
|
|
666
|
+
|
|
667
|
+
self._devices_by_account_response = ListingDevAccountResponse.from_dict(response_body_dict)
|
|
668
|
+
return self._devices_by_account_response
|
|
669
|
+
|
|
670
|
+
async def list_binding_by_dev(self, iot_id: str):
|
|
671
|
+
config = Config(
|
|
672
|
+
app_key=self._app_key,
|
|
673
|
+
app_secret=self._app_secret,
|
|
674
|
+
domain=self._region_response.data.apiGatewayEndpoint,
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
client = Client(config)
|
|
678
|
+
|
|
679
|
+
# build request
|
|
680
|
+
request = CommonParams(
|
|
681
|
+
api_ver="1.0.8",
|
|
682
|
+
language="en-US",
|
|
683
|
+
iot_token=self._session_by_authcode_response.data.iotToken,
|
|
684
|
+
)
|
|
685
|
+
body = IoTApiRequest(
|
|
686
|
+
id=str(uuid.uuid4()),
|
|
687
|
+
params={"pageSize": 100, "pageNo": 1, "iotId": iot_id},
|
|
688
|
+
request=request,
|
|
689
|
+
version="1.0",
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
# send request
|
|
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
|
+
)
|
|
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)
|
|
788
|
+
|
|
789
|
+
if int(response_body_dict.get("code")) != 200:
|
|
790
|
+
raise Exception("Error in creating session: " + response_body_dict["msg"])
|
|
791
|
+
|
|
792
|
+
self._devices_by_account_response = ListingDevAccountResponse.from_dict(response_body_dict)
|
|
793
|
+
return self._devices_by_account_response
|
|
794
|
+
|
|
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")
|
|
814
|
+
|
|
815
|
+
"""Check if iotToken is expired"""
|
|
816
|
+
if self._iot_token_issued_at + self._session_by_authcode_response.data.iotTokenExpire <= (
|
|
817
|
+
int(time.time()) + (5 * 3600)
|
|
818
|
+
):
|
|
819
|
+
"""Token expired - Try to refresh - Check if refreshToken is not expired"""
|
|
820
|
+
if self._iot_token_issued_at + self._session_by_authcode_response.data.refreshTokenExpire > (
|
|
821
|
+
int(time.time())
|
|
822
|
+
):
|
|
823
|
+
await self.check_or_refresh_session()
|
|
824
|
+
else:
|
|
825
|
+
raise AuthRefreshException("Refresh token expired. Please re-login")
|
|
826
|
+
|
|
827
|
+
config = Config(
|
|
828
|
+
app_key=self._app_key,
|
|
829
|
+
app_secret=self._app_secret,
|
|
830
|
+
domain=self._region_response.data.apiGatewayEndpoint,
|
|
831
|
+
)
|
|
832
|
+
|
|
833
|
+
client = Client(config)
|
|
834
|
+
# build request
|
|
835
|
+
request = CommonParams(
|
|
836
|
+
api_ver="1.0.5",
|
|
837
|
+
language="en-US",
|
|
838
|
+
iot_token=self._session_by_authcode_response.data.iotToken,
|
|
839
|
+
)
|
|
840
|
+
|
|
841
|
+
# TODO move to using InvokeThingServiceRequest()
|
|
842
|
+
|
|
843
|
+
message_id = str(uuid.uuid4())
|
|
844
|
+
|
|
845
|
+
body = IoTApiRequest(
|
|
846
|
+
id=message_id,
|
|
847
|
+
params={
|
|
848
|
+
"args": {"content": self.converter.printBase64Binary(command)},
|
|
849
|
+
"identifier": "device_protobuf_sync_service",
|
|
850
|
+
"iotId": f"{iot_id}",
|
|
851
|
+
},
|
|
852
|
+
request=request,
|
|
853
|
+
version="1.0",
|
|
854
|
+
)
|
|
855
|
+
logger.debug(self.converter.printBase64Binary(command))
|
|
856
|
+
# send request
|
|
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)
|
|
859
|
+
logger.debug(response.status_message)
|
|
860
|
+
logger.debug(response.headers)
|
|
861
|
+
logger.debug(response.status_code)
|
|
862
|
+
logger.debug(response.body)
|
|
863
|
+
logger.debug(iot_id)
|
|
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
|
+
|
|
875
|
+
response_body_str = response.body.decode("utf-8")
|
|
876
|
+
response_body_dict = self.parse_json_response(response_body_str)
|
|
877
|
+
|
|
878
|
+
if int(response_body_dict.get("code")) != 200:
|
|
879
|
+
logger.error(
|
|
880
|
+
"Error in sending cloud command: %s - %s",
|
|
881
|
+
str(response_body_dict.get("code")),
|
|
882
|
+
str(response_body_dict.get("message")),
|
|
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
|
+
|
|
891
|
+
if response_body_dict.get("code") == 29003:
|
|
892
|
+
logger.debug(self._session_by_authcode_response.data.identityId)
|
|
893
|
+
await self.sign_out()
|
|
894
|
+
raise SetupException(response_body_dict.get("code"), iot_id)
|
|
895
|
+
if response_body_dict.get("code") == 6205:
|
|
896
|
+
raise DeviceOfflineException(response_body_dict.get("code"), iot_id)
|
|
897
|
+
|
|
898
|
+
if response_body_dict.get("code") == 6205:
|
|
899
|
+
raise CheckSessionException(response_body_dict.get("message"))
|
|
900
|
+
|
|
901
|
+
if response_body_dict.get("code") == 460:
|
|
902
|
+
logger.debug("iotToken expired, must re-login.")
|
|
903
|
+
raise CheckSessionException(response_body_dict.get("message"))
|
|
904
|
+
|
|
905
|
+
if self.message_delay != 1:
|
|
906
|
+
self.message_delay = 1
|
|
907
|
+
|
|
908
|
+
return message_id
|
|
909
|
+
|
|
910
|
+
async def get_device_properties(self, iot_id: str) -> ThingPropertiesResponse:
|
|
911
|
+
"""List bindings by account."""
|
|
912
|
+
config = Config(
|
|
913
|
+
app_key=self._app_key,
|
|
914
|
+
app_secret=self._app_secret,
|
|
915
|
+
domain=self._region_response.data.apiGatewayEndpoint,
|
|
916
|
+
)
|
|
917
|
+
|
|
918
|
+
client = Client(config)
|
|
919
|
+
|
|
920
|
+
# build request
|
|
921
|
+
request = CommonParams(
|
|
922
|
+
api_ver="1.0.0",
|
|
923
|
+
language="en-US",
|
|
924
|
+
iot_token=self._session_by_authcode_response.data.iotToken,
|
|
925
|
+
)
|
|
926
|
+
body = IoTApiRequest(
|
|
927
|
+
id=str(uuid.uuid4()),
|
|
928
|
+
params={
|
|
929
|
+
"iotId": f"{iot_id}",
|
|
930
|
+
},
|
|
931
|
+
request=request,
|
|
932
|
+
version="1.0",
|
|
933
|
+
)
|
|
934
|
+
|
|
935
|
+
# send request
|
|
936
|
+
response = await client.async_do_request("/thing/properties/get", "https", "POST", None, body, RuntimeOptions())
|
|
937
|
+
logger.debug(response.status_message)
|
|
938
|
+
logger.debug(response.headers)
|
|
939
|
+
logger.debug(response.status_code)
|
|
940
|
+
logger.debug(response.body)
|
|
941
|
+
|
|
942
|
+
# Decode the response body
|
|
943
|
+
response_body_str = response.body.decode("utf-8")
|
|
944
|
+
|
|
945
|
+
# Load the JSON string into a dictionary
|
|
946
|
+
response_body_dict = self.parse_json_response(response_body_str)
|
|
947
|
+
|
|
948
|
+
if int(response_body_dict.get("code")) != 200:
|
|
949
|
+
raise Exception("Error in getting properties: " + response_body_dict["msg"])
|
|
950
|
+
|
|
951
|
+
return ThingPropertiesResponse.from_dict(response_body_dict)
|
|
952
|
+
|
|
953
|
+
@property
|
|
954
|
+
def devices_by_account_response(self):
|
|
955
|
+
return self._devices_by_account_response
|
|
956
|
+
|
|
957
|
+
def set_http(self, mammotion_http: MammotionHTTP) -> None:
|
|
958
|
+
self.mammotion_http = mammotion_http
|
|
959
|
+
|
|
960
|
+
@property
|
|
961
|
+
def region_response(self) -> RegionResponse | None:
|
|
962
|
+
return self._region_response
|
|
963
|
+
|
|
964
|
+
@property
|
|
965
|
+
def aep_response(self) -> AepResponse | None:
|
|
966
|
+
return self._aep_response
|
|
967
|
+
|
|
968
|
+
@property
|
|
969
|
+
def session_by_authcode_response(self) -> SessionByAuthCodeResponse:
|
|
970
|
+
return self._session_by_authcode_response
|
|
971
|
+
|
|
972
|
+
@property
|
|
973
|
+
def client_id(self) -> str:
|
|
974
|
+
return self._client_id
|
|
975
|
+
|
|
976
|
+
@property
|
|
977
|
+
def login_by_oauth_response(self) -> LoginByOAuthResponse | None:
|
|
978
|
+
return self._login_by_oauth_response
|
|
979
|
+
|
|
980
|
+
@property
|
|
981
|
+
def connect_response(self) -> ConnectResponse | None:
|
|
982
|
+
return self._connect_response
|