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.
Files changed (154) hide show
  1. pymammotion/__init__.py +53 -0
  2. pymammotion/agora/__init__.py +0 -0
  3. pymammotion/agora/agora_api.py +755 -0
  4. pymammotion/agora/agora_rtc_capabilities.py +748 -0
  5. pymammotion/agora/agora_websockets.py +1175 -0
  6. pymammotion/aliyun/__init__.py +1 -0
  7. pymammotion/aliyun/client.py +235 -0
  8. pymammotion/aliyun/cloud_gateway.py +982 -0
  9. pymammotion/aliyun/model/aep_response.py +21 -0
  10. pymammotion/aliyun/model/connect_response.py +51 -0
  11. pymammotion/aliyun/model/dev_by_account_response.py +195 -0
  12. pymammotion/aliyun/model/login_by_oauth_response.py +64 -0
  13. pymammotion/aliyun/model/regions_response.py +29 -0
  14. pymammotion/aliyun/model/session_by_authcode_response.py +19 -0
  15. pymammotion/aliyun/model/thing_response.py +12 -0
  16. pymammotion/aliyun/regions.py +62 -0
  17. pymammotion/aliyun/tea/core.py +297 -0
  18. pymammotion/aliyun/tmp_constant.py +171 -0
  19. pymammotion/bluetooth/__init__.py +1 -0
  20. pymammotion/bluetooth/ble.py +62 -0
  21. pymammotion/bluetooth/ble_message.py +676 -0
  22. pymammotion/bluetooth/const.py +27 -0
  23. pymammotion/bluetooth/data/__init__.py +0 -0
  24. pymammotion/bluetooth/data/convert.py +25 -0
  25. pymammotion/bluetooth/data/framectrldata.py +40 -0
  26. pymammotion/bluetooth/data/notifydata.py +62 -0
  27. pymammotion/bluetooth/model/__init__.py +0 -0
  28. pymammotion/bluetooth/model/atomic_integer.py +54 -0
  29. pymammotion/const.py +13 -0
  30. pymammotion/data/__init__.py +0 -0
  31. pymammotion/data/model/__init__.py +8 -0
  32. pymammotion/data/model/account.py +8 -0
  33. pymammotion/data/model/device.py +192 -0
  34. pymammotion/data/model/device_config.py +72 -0
  35. pymammotion/data/model/device_info.py +60 -0
  36. pymammotion/data/model/device_limits.py +49 -0
  37. pymammotion/data/model/enums.py +77 -0
  38. pymammotion/data/model/errors.py +12 -0
  39. pymammotion/data/model/events.py +14 -0
  40. pymammotion/data/model/generate_geojson.py +565 -0
  41. pymammotion/data/model/generate_route_information.py +26 -0
  42. pymammotion/data/model/hash_list.py +475 -0
  43. pymammotion/data/model/location.py +36 -0
  44. pymammotion/data/model/mowing_modes.py +77 -0
  45. pymammotion/data/model/rapid_state.py +45 -0
  46. pymammotion/data/model/raw_data.py +215 -0
  47. pymammotion/data/model/region_data.py +102 -0
  48. pymammotion/data/model/report_info.py +182 -0
  49. pymammotion/data/model/work.py +27 -0
  50. pymammotion/data/mower_state_manager.py +369 -0
  51. pymammotion/data/mqtt/__init__.py +1 -0
  52. pymammotion/data/mqtt/event.py +227 -0
  53. pymammotion/data/mqtt/mammotion_properties.py +276 -0
  54. pymammotion/data/mqtt/properties.py +203 -0
  55. pymammotion/data/mqtt/status.py +57 -0
  56. pymammotion/event/__init__.py +6 -0
  57. pymammotion/event/event.py +96 -0
  58. pymammotion/homeassistant/__init__.py +3 -0
  59. pymammotion/homeassistant/mower_api.py +514 -0
  60. pymammotion/homeassistant/rtk_api.py +54 -0
  61. pymammotion/http/__init__.py +0 -0
  62. pymammotion/http/encryption.py +220 -0
  63. pymammotion/http/http.py +673 -0
  64. pymammotion/http/model/__init__.py +0 -0
  65. pymammotion/http/model/camera_stream.py +31 -0
  66. pymammotion/http/model/http.py +249 -0
  67. pymammotion/http/model/response_factory.py +61 -0
  68. pymammotion/http/model/rtk.py +16 -0
  69. pymammotion/mammotion/__init__.py +0 -0
  70. pymammotion/mammotion/commands/__init__.py +0 -0
  71. pymammotion/mammotion/commands/abstract_message.py +24 -0
  72. pymammotion/mammotion/commands/mammotion_command.py +81 -0
  73. pymammotion/mammotion/commands/messages/__init__.py +0 -0
  74. pymammotion/mammotion/commands/messages/basestation.py +43 -0
  75. pymammotion/mammotion/commands/messages/driver.py +122 -0
  76. pymammotion/mammotion/commands/messages/media.py +87 -0
  77. pymammotion/mammotion/commands/messages/navigation.py +564 -0
  78. pymammotion/mammotion/commands/messages/network.py +205 -0
  79. pymammotion/mammotion/commands/messages/ota.py +38 -0
  80. pymammotion/mammotion/commands/messages/system.py +330 -0
  81. pymammotion/mammotion/commands/messages/video.py +33 -0
  82. pymammotion/mammotion/control/__init__.py +0 -0
  83. pymammotion/mammotion/control/joystick.py +145 -0
  84. pymammotion/mammotion/devices/__init__.py +29 -0
  85. pymammotion/mammotion/devices/base.py +163 -0
  86. pymammotion/mammotion/devices/mammotion.py +571 -0
  87. pymammotion/mammotion/devices/mammotion_bluetooth.py +496 -0
  88. pymammotion/mammotion/devices/mammotion_cloud.py +355 -0
  89. pymammotion/mammotion/devices/mammotion_mower_ble.py +48 -0
  90. pymammotion/mammotion/devices/mammotion_mower_cloud.py +39 -0
  91. pymammotion/mammotion/devices/managers/managers.py +81 -0
  92. pymammotion/mammotion/devices/mower_device.py +120 -0
  93. pymammotion/mammotion/devices/mower_manager.py +107 -0
  94. pymammotion/mammotion/devices/rtk_ble.py +89 -0
  95. pymammotion/mammotion/devices/rtk_cloud.py +115 -0
  96. pymammotion/mammotion/devices/rtk_device.py +50 -0
  97. pymammotion/mammotion/devices/rtk_manager.py +125 -0
  98. pymammotion/mqtt/__init__.py +6 -0
  99. pymammotion/mqtt/aliyun_mqtt.py +237 -0
  100. pymammotion/mqtt/linkkit/__init__.py +5 -0
  101. pymammotion/mqtt/linkkit/h2client.py +585 -0
  102. pymammotion/mqtt/linkkit/linkkit.py +3025 -0
  103. pymammotion/mqtt/mammotion_future.py +26 -0
  104. pymammotion/mqtt/mammotion_mqtt.py +214 -0
  105. pymammotion/mqtt/mqtt_models.py +66 -0
  106. pymammotion/proto/__init__.py +4841 -0
  107. pymammotion/proto/basestation.proto +51 -0
  108. pymammotion/proto/basestation_pb2.py +35 -0
  109. pymammotion/proto/basestation_pb2.pyi +89 -0
  110. pymammotion/proto/common.proto +7 -0
  111. pymammotion/proto/common_pb2.py +25 -0
  112. pymammotion/proto/common_pb2.pyi +13 -0
  113. pymammotion/proto/dev_net.proto +321 -0
  114. pymammotion/proto/dev_net_pb2.py +111 -0
  115. pymammotion/proto/dev_net_pb2.pyi +515 -0
  116. pymammotion/proto/luba_msg.proto +76 -0
  117. pymammotion/proto/luba_msg_pb2.py +41 -0
  118. pymammotion/proto/luba_msg_pb2.pyi +97 -0
  119. pymammotion/proto/luba_mul.proto +129 -0
  120. pymammotion/proto/luba_mul_pb2.py +61 -0
  121. pymammotion/proto/luba_mul_pb2.pyi +178 -0
  122. pymammotion/proto/mctrl_driver.proto +107 -0
  123. pymammotion/proto/mctrl_driver_pb2.py +57 -0
  124. pymammotion/proto/mctrl_driver_pb2.pyi +167 -0
  125. pymammotion/proto/mctrl_nav.proto +591 -0
  126. pymammotion/proto/mctrl_nav_pb2.py +136 -0
  127. pymammotion/proto/mctrl_nav_pb2.pyi +1067 -0
  128. pymammotion/proto/mctrl_ota.proto +80 -0
  129. pymammotion/proto/mctrl_ota_pb2.py +45 -0
  130. pymammotion/proto/mctrl_ota_pb2.pyi +128 -0
  131. pymammotion/proto/mctrl_pept.proto +34 -0
  132. pymammotion/proto/mctrl_pept_pb2.py +33 -0
  133. pymammotion/proto/mctrl_pept_pb2.pyi +58 -0
  134. pymammotion/proto/mctrl_sys.proto +741 -0
  135. pymammotion/proto/mctrl_sys_pb2.py +206 -0
  136. pymammotion/proto/mctrl_sys_pb2.pyi +1213 -0
  137. pymammotion/proto/message_pool.py +3 -0
  138. pymammotion/proto/py.typed +0 -0
  139. pymammotion/py.typed +0 -0
  140. pymammotion/utility/constant/__init__.py +3 -0
  141. pymammotion/utility/constant/device_constant.py +315 -0
  142. pymammotion/utility/conversions.py +5 -0
  143. pymammotion/utility/datatype_converter.py +124 -0
  144. pymammotion/utility/device_config.py +755 -0
  145. pymammotion/utility/device_type.py +489 -0
  146. pymammotion/utility/map.py +259 -0
  147. pymammotion/utility/movement.py +18 -0
  148. pymammotion/utility/mur_mur_hash.py +159 -0
  149. pymammotion/utility/periodic.py +106 -0
  150. pymammotion/utility/rocker_util.py +194 -0
  151. pymammotion-0.5.69.dist-info/METADATA +93 -0
  152. pymammotion-0.5.69.dist-info/RECORD +154 -0
  153. pymammotion-0.5.69.dist-info/WHEEL +4 -0
  154. 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