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,673 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ from collections.abc import Awaitable, Callable
5
+ import csv
6
+ from functools import wraps
7
+ import hashlib
8
+ import hmac
9
+ import json
10
+ import random
11
+ import time
12
+ from typing import Any, TypeVar, cast
13
+
14
+ from aiohttp import ClientSession
15
+ import jwt
16
+
17
+ from pymammotion.const import (
18
+ MAMMOTION_API_DOMAIN,
19
+ MAMMOTION_CLIENT_ID,
20
+ MAMMOTION_CLIENT_SECRET,
21
+ MAMMOTION_DOMAIN,
22
+ MAMMOTION_OUATH2_CLIENT_ID,
23
+ MAMMOTION_OUATH2_CLIENT_SECRET,
24
+ )
25
+ from pymammotion.http.encryption import EncryptionUtils
26
+ from pymammotion.http.model.camera_stream import StreamSubscriptionResponse, VideoResourceResponse
27
+ from pymammotion.http.model.http import (
28
+ CheckDeviceVersion,
29
+ DeviceInfo,
30
+ DeviceRecords,
31
+ ErrorInfo,
32
+ JWTTokenInfo,
33
+ LoginResponseData,
34
+ MQTTConnection,
35
+ Response,
36
+ UnauthorizedException,
37
+ )
38
+ from pymammotion.http.model.response_factory import response_factory
39
+ from pymammotion.http.model.rtk import RTK
40
+
41
+ T = TypeVar("T")
42
+
43
+
44
+ def sign_with_hmac_sha256(data: str, app_secret: str) -> str:
45
+ """Sign data with HMAC-SHA256 algorithm.
46
+
47
+ Args:
48
+ data: The data to sign
49
+ app_secret: The secret key for signing
50
+
51
+ Returns:
52
+ Hex string of the signature
53
+
54
+ Raises:
55
+ RuntimeError: If signing fails
56
+
57
+ """
58
+ if data is None:
59
+ raise ValueError("data cannot be None")
60
+ if app_secret is None:
61
+ raise ValueError("app_secret cannot be None")
62
+
63
+ try:
64
+ # Convert strings to bytes using UTF-8 encoding
65
+ data_bytes = data.encode("utf-8")
66
+ secret_bytes = app_secret.encode("utf-8")
67
+
68
+ # Create HMAC-SHA256 hash
69
+ hmac_obj = hmac.new(secret_bytes, data_bytes, hashlib.sha256)
70
+
71
+ # Get the digest
72
+ digest = hmac_obj.digest()
73
+
74
+ # Convert to hex string
75
+ hex_string = digest.hex()
76
+
77
+ return hex_string
78
+
79
+ except Exception as e:
80
+ raise RuntimeError(f"toSignWithHmacSha256 error: {e}") from e
81
+
82
+
83
+ def create_oauth_signature(login_req: dict, client_id: str, client_secret: str, token_endpoint: str) -> str:
84
+ """Create OAuth signature for login request.
85
+
86
+ Args:
87
+ login_req: Login request data as dictionary
88
+ client_id: OAuth client ID
89
+ client_secret: OAuth client secret
90
+ token_endpoint: Token endpoint path
91
+
92
+ Returns:
93
+ HMAC-SHA256 signature
94
+
95
+ """
96
+ # Convert dict to JSON without HTML escaping (ensure_ascii=False handles this)
97
+ json_data = json.dumps(login_req, ensure_ascii=False, separators=(",", ":"))
98
+
99
+ # Get current timestamp in milliseconds
100
+ timestamp = str(int(time.time() * 1000))
101
+
102
+ # Construct the string to sign
103
+ str_to_sign = f"{client_id}{timestamp}{token_endpoint}{json_data}"
104
+
105
+ # Create MD5 hash of client secret
106
+ try:
107
+ md5_hash = hashlib.md5(client_secret.encode("utf-8")).digest()
108
+ # Convert to hex string
109
+ hashed_secret = md5_hash.hex()
110
+ except Exception:
111
+ hashed_secret = ""
112
+
113
+ # Sign with HMAC-SHA256
114
+ signature = sign_with_hmac_sha256(str_to_sign, hashed_secret)
115
+
116
+ return signature
117
+
118
+
119
+ class MammotionHTTP:
120
+ def __init__(
121
+ self, account: str | None = None, password: str | None = None, session: ClientSession | None = None
122
+ ) -> None:
123
+ self.device_info: list[DeviceInfo] = []
124
+ self.mqtt_credentials: MQTTConnection | None = None
125
+ self.device_records: DeviceRecords = DeviceRecords(records=[], current=0, total=0, size=0, pages=0)
126
+ self.expires_in = 0.0
127
+ self.code = 0
128
+ self.msg = None
129
+ self._session = session if session else ClientSession()
130
+ self.account = account
131
+ self._password = password
132
+ self._response: Response | None = None
133
+ self.login_info: LoginResponseData | None = None
134
+ self.jwt_info: JWTTokenInfo = JWTTokenInfo("", "")
135
+ self._headers = {"User-Agent": "okhttp/4.9.3", "App-Version": "Home Assistant,1.15.6.14"}
136
+ self.encryption_utils = EncryptionUtils()
137
+
138
+ # Add this method to generate a 10-digit random number
139
+ def get_10_random() -> str:
140
+ """Generate a 10-digit random number as a string."""
141
+ return "".join([str(random.randint(0, 9)) for _ in range(7)])
142
+
143
+ # Replace the line in the __init__ method with:
144
+ self.client_id = f"{int(time.time() * 1000)}_{get_10_random()}_1"
145
+
146
+ @property
147
+ def response(self) -> Response | None:
148
+ return self._response
149
+
150
+ @response.setter
151
+ def response(self, response: Response) -> None:
152
+ self._response = response
153
+ decoded_token = jwt.decode(response.data.access_token, options={"verify_signature": False})
154
+ if isinstance(decoded_token, dict):
155
+ self.jwt_info = JWTTokenInfo(iot=decoded_token.get("iot", ""), robot=decoded_token.get("robot", ""))
156
+
157
+ @staticmethod
158
+ def generate_headers(token: str) -> dict:
159
+ return {"Authorization": f"Bearer {token}"}
160
+
161
+ @staticmethod
162
+ def refresh_token_decorator(func: Callable[..., Awaitable[T]]) -> Callable[..., Awaitable[T]]:
163
+ """Decorator to handle token refresh before executing a function.
164
+
165
+ Args:
166
+ func: The async function to be decorated
167
+
168
+ Returns:
169
+ The wrapped async function that handles token refresh
170
+
171
+ """
172
+
173
+ @wraps(func)
174
+ async def wrapper(self: MammotionHTTP, *args: Any, **kwargs: Any) -> T:
175
+ # Check if token will expire in the next 5 minutes
176
+ if self.expires_in < time.time() + 300: # 300 seconds = 5 minutes
177
+ await self.refresh_login()
178
+ return await func(self, *args, **kwargs)
179
+
180
+ return wrapper
181
+
182
+ async def handle_expiry(self, resp: Response) -> Response:
183
+ if resp.code == 401 and self.account and self._password:
184
+ return await self.login_v2(self.account, self._password)
185
+ return resp
186
+
187
+ async def login_by_email(self, email: str, password: str) -> Response[LoginResponseData]:
188
+ return await self.login_v2(email, password)
189
+
190
+ @refresh_token_decorator
191
+ async def get_all_error_codes(self) -> dict[str, ErrorInfo]:
192
+ """Retrieves and parses all error codes from the MAMMOTION API."""
193
+ resp = await self._session.post(
194
+ f"{MAMMOTION_API_DOMAIN}/user-server/v1/code/record/export-data",
195
+ headers={
196
+ **self._headers,
197
+ "Authorization": f"Bearer {self.login_info.access_token}",
198
+ "Content-Type": "application/json",
199
+ "User-Agent": "okhttp/4.9.3",
200
+ },
201
+ )
202
+ data = await resp.json()
203
+ reader = csv.DictReader(data.get("data", "").split("\n"), delimiter=",")
204
+ codes = dict()
205
+ for row in reader:
206
+ error_info = ErrorInfo(**cast(dict[str, Any], row))
207
+ codes[error_info.code] = error_info
208
+ return codes
209
+
210
+ async def oauth_check(self) -> Response:
211
+ """Check if token is valid.
212
+
213
+ Returns 401 if token is invalid. We then need to re-authenticate, can try to refresh token first
214
+ """
215
+ resp = await self._session.post(
216
+ f"{MAMMOTION_DOMAIN}/user-server/v1/user/oauth/check",
217
+ headers={
218
+ **self._headers,
219
+ "Authorization": f"Bearer {self.login_info.access_token}",
220
+ "Content-Type": "application/json",
221
+ "User-Agent": "okhttp/4.9.3",
222
+ },
223
+ )
224
+ data = await resp.json()
225
+ return Response.from_dict(data)
226
+
227
+ @refresh_token_decorator
228
+ async def refresh_authorization_code(self) -> Response:
229
+ """Refresh token."""
230
+ resp = await self._session.post(
231
+ f"{MAMMOTION_DOMAIN}/authorization/code",
232
+ headers={
233
+ **self._headers,
234
+ "Authorization": f"Bearer {self.login_info.access_token}",
235
+ "Content-Type": "application/json",
236
+ "User-Agent": "okhttp/4.9.3",
237
+ },
238
+ json={"clientId": MAMMOTION_CLIENT_ID},
239
+ )
240
+ data = await resp.json()
241
+ print(data)
242
+ self.login_info.access_token = data["data"].get("accessToken", self.login_info.access_token)
243
+ self.login_info.authorization_code = data["data"].get("code", self.login_info.authorization_code)
244
+ await self.get_mqtt_credentials()
245
+ return Response.from_dict(data)
246
+
247
+ @refresh_token_decorator
248
+ async def pair_devices_mqtt(self, mower_name: str, rtk_name: str) -> Response:
249
+ resp = await self._session.post(
250
+ f"{MAMMOTION_API_DOMAIN}/device-server/v1/iot/device/pairing",
251
+ headers=self._headers,
252
+ json={"mowerName": mower_name, "rtkName": rtk_name},
253
+ )
254
+ data = await resp.json()
255
+ if data.get("status") == 200:
256
+ print(data)
257
+ return Response.from_dict(data)
258
+ else:
259
+ print(data)
260
+ return Response.from_dict(data)
261
+
262
+ @refresh_token_decorator
263
+ async def unpair_devices_mqtt(self, mower_name: str, rtk_name: str) -> Response:
264
+ resp = await self._session.post(
265
+ f"{MAMMOTION_API_DOMAIN}/device-server/v1/iot/device/unpairing",
266
+ headers=self._headers,
267
+ json={"mowerName": mower_name, "rtkName": rtk_name},
268
+ )
269
+ data = await resp.json()
270
+ if data.get("status") == 200:
271
+ print(data)
272
+ return Response.from_dict(data)
273
+ else:
274
+ print(data)
275
+ return Response.from_dict(data)
276
+
277
+ @refresh_token_decorator
278
+ async def net_rtk_enable(self, device_id: str) -> Response:
279
+ resp = await self._session.post(
280
+ f"{MAMMOTION_API_DOMAIN}/device-server/v1/iot/net-rtk/enable",
281
+ headers=self._headers,
282
+ json={"deviceId": device_id},
283
+ )
284
+ data = await resp.json()
285
+ if data.get("status") == 200:
286
+ print(data)
287
+ return Response.from_dict(data)
288
+ else:
289
+ print(data)
290
+ return Response.from_dict(data)
291
+
292
+ @refresh_token_decorator
293
+ async def get_stream_subscription(self, iot_id: str) -> Response[StreamSubscriptionResponse]:
294
+ """Fetches stream subscription data from agora.io for a given IoT device."""
295
+ resp = await self._session.post(
296
+ f"{MAMMOTION_API_DOMAIN}/device-server/v1/stream/subscription",
297
+ json={"deviceId": iot_id},
298
+ headers={
299
+ **self._headers,
300
+ "Authorization": f"Bearer {self.login_info.access_token}",
301
+ "Content-Type": "application/json",
302
+ "User-Agent": "okhttp/4.9.3",
303
+ },
304
+ )
305
+ data = await resp.json()
306
+ # TODO catch errors from mismatch like token expire etc
307
+ # Assuming the data format matches the expected structure
308
+ response = Response[StreamSubscriptionResponse].from_dict(data)
309
+ await self.handle_expiry(response)
310
+ if response.code != 0:
311
+ return response
312
+ response.data = StreamSubscriptionResponse.from_dict(data.get("data", {}))
313
+ return response
314
+
315
+ @refresh_token_decorator
316
+ async def get_stream_subscription_mini_or_x_series(
317
+ self, iot_id: str, is_yuka: bool
318
+ ) -> Response[StreamSubscriptionResponse]:
319
+ # Prepare the payload with cameraStates based on is_yuka flag
320
+ """Fetches stream subscription data for a given IoT device."""
321
+
322
+ payload = {"deviceId": iot_id, "mode": 0, "cameraStates": []}
323
+
324
+ # Add appropriate cameraStates based on the is_yuka flag
325
+ if is_yuka:
326
+ payload["cameraStates"] = [{"cameraState": 1}, {"cameraState": 0}, {"cameraState": 1}]
327
+ else:
328
+ payload["cameraStates"] = [{"cameraState": 1}, {"cameraState": 0}, {"cameraState": 0}]
329
+
330
+ resp = await self._session.post(
331
+ f"{MAMMOTION_API_DOMAIN}/device-server/v1/stream/token",
332
+ json=payload,
333
+ headers={
334
+ **self._headers,
335
+ "Authorization": f"Bearer {self.login_info.access_token}",
336
+ "Content-Type": "application/json",
337
+ "User-Agent": "okhttp/4.9.3",
338
+ },
339
+ )
340
+ data = await resp.json()
341
+ # TODO catch errors from mismatch like token expire etc
342
+ # Assuming the data format matches the expected structure
343
+ response = Response[StreamSubscriptionResponse].from_dict(data)
344
+ await self.handle_expiry(response)
345
+ if response.code != 0:
346
+ return response
347
+ response.data = StreamSubscriptionResponse.from_dict(data.get("data", {}))
348
+ return response
349
+
350
+ @refresh_token_decorator
351
+ async def get_video_resource(self, iot_id: str) -> Response[VideoResourceResponse]:
352
+ """Fetch video resource for a given IoT ID."""
353
+ resp = await self._session.get(
354
+ f"{MAMMOTION_API_DOMAIN}/device-server/v1/video-resource/{iot_id}",
355
+ headers={
356
+ "Authorization": f"Bearer {self.login_info.access_token}",
357
+ "Content-Type": "application/json",
358
+ "User-Agent": "okhttp/4.9.3",
359
+ },
360
+ )
361
+ data = await resp.json()
362
+ # TODO catch errors from mismatch like token expire etc
363
+ # Assuming the data format matches the expected structure
364
+ response = Response[VideoResourceResponse].from_dict(data)
365
+ if response.code != 0:
366
+ return response
367
+ response.data = VideoResourceResponse.from_dict(data.get("data", {}))
368
+ return response
369
+
370
+ @refresh_token_decorator
371
+ async def get_device_ota_firmware(self, iot_ids: list[str]) -> Response[list[CheckDeviceVersion]]:
372
+ """Checks device firmware versions for a list of IoT IDs."""
373
+ resp = await self._session.post(
374
+ f"{MAMMOTION_API_DOMAIN}/device-server/v1/devices/version/check",
375
+ json={"deviceIds": iot_ids},
376
+ headers={
377
+ **self._headers,
378
+ "Authorization": f"Bearer {self.login_info.access_token}",
379
+ "Content-Type": "application/json",
380
+ "User-Agent": "okhttp/4.9.3",
381
+ "Client-Id": self.client_id,
382
+ "Client-Type": "1",
383
+ },
384
+ )
385
+ data = await resp.json()
386
+ # TODO catch errors from mismatch like token expire etc
387
+ # Assuming the data format matches the expected structure
388
+ return response_factory(Response[list[CheckDeviceVersion]], data)
389
+
390
+ @refresh_token_decorator
391
+ async def start_ota_upgrade(self, iot_id: str, version: str) -> Response[str]:
392
+ """Initiates an OTA upgrade for a device."""
393
+ resp = await self._session.post(
394
+ f"{MAMMOTION_API_DOMAIN}/device-server/v1/ota/device/upgrade",
395
+ json={"deviceId": iot_id, "version": version},
396
+ headers={
397
+ **self._headers,
398
+ "Authorization": f"Bearer {self.login_info.access_token}",
399
+ "Content-Type": "application/json",
400
+ "User-Agent": "okhttp/4.9.3",
401
+ "Client-Id": self.client_id,
402
+ "Client-Type": "1",
403
+ },
404
+ )
405
+ data = await resp.json()
406
+ # TODO catch errors from mismatch like token expire etc
407
+ # Assuming the data format matches the expected structure
408
+ return response_factory(Response[str], data)
409
+
410
+ @refresh_token_decorator
411
+ async def get_rtk_devices(self) -> Response[list[RTK]]:
412
+ """Fetches stream subscription data from agora.io for a given IoT device."""
413
+ resp = await self._session.get(
414
+ f"{MAMMOTION_API_DOMAIN}/device-server/v1/rtk/devices",
415
+ headers={
416
+ **self._headers,
417
+ "Authorization": f"Bearer {self.login_info.access_token}",
418
+ "Content-Type": "application/json",
419
+ "User-Agent": "okhttp/4.9.3",
420
+ },
421
+ )
422
+ data = await resp.json()
423
+
424
+ return response_factory(Response[list[RTK]], data)
425
+
426
+ @refresh_token_decorator
427
+ async def get_user_device_list(self) -> Response[list[DeviceInfo]]:
428
+ """Fetches device list for a user (owned not shared, shared returns nothing)."""
429
+ resp = await self._session.get(
430
+ f"{MAMMOTION_API_DOMAIN}/device-server/v1/device/list",
431
+ headers={
432
+ **self._headers,
433
+ "Authorization": f"Bearer {self.login_info.access_token}",
434
+ "Content-Type": "application/json",
435
+ "User-Agent": "okhttp/4.9.3",
436
+ "Client-Id": self.client_id,
437
+ "Client-Type": "1",
438
+ },
439
+ )
440
+ resp_dict = await resp.json()
441
+ response = response_factory(Response[list[DeviceInfo]], resp_dict)
442
+ self.device_info = response.data if response.data else self.device_info
443
+ return response
444
+
445
+ @refresh_token_decorator
446
+ async def get_user_shared_device_page(self) -> Response[DeviceRecords]:
447
+ """Fetches device list for a user (shared) but not accepted."""
448
+ """Can set owned to zero or one to possibly check for not accepted mowers?"""
449
+ resp = await self._session.post(
450
+ f"{MAMMOTION_API_DOMAIN}/user-server/v1/share/device/page",
451
+ json={"iotId": "", "owned": 0, "pageNumber": 1, "pageSize": 200, "statusList": [-1]},
452
+ headers={
453
+ **self._headers,
454
+ "Authorization": f"Bearer {self.login_info.access_token}",
455
+ "Content-Type": "application/json",
456
+ "User-Agent": "okhttp/4.9.3",
457
+ },
458
+ )
459
+ resp_dict = await resp.json()
460
+ response = response_factory(Response[DeviceRecords], resp_dict)
461
+ self.devices_shared_info = response.data if response.data else self.devices_shared_info
462
+ return response
463
+
464
+ @refresh_token_decorator
465
+ async def get_user_device_page(self) -> Response[DeviceRecords]:
466
+ """Fetches device list for a user, is either new API or for newer devices."""
467
+ resp = await self._session.post(
468
+ f"{self.jwt_info.iot}/v1/user/device/page",
469
+ json={
470
+ "iotId": "",
471
+ "pageNumber": 1,
472
+ "pageSize": 100,
473
+ },
474
+ headers={
475
+ **self._headers,
476
+ "Authorization": f"Bearer {self.login_info.access_token}",
477
+ "Content-Type": "application/json",
478
+ "User-Agent": "okhttp/4.9.3",
479
+ "Client-Id": self.client_id,
480
+ "Client-Type": "1",
481
+ },
482
+ )
483
+ if resp.status != 200:
484
+ return Response.from_dict({"code": resp.status, "msg": "get device list failed"})
485
+ resp_dict = await resp.json()
486
+ response = response_factory(Response[DeviceRecords], resp_dict)
487
+ self.device_records = response.data if response.data else self.device_records
488
+ return response
489
+
490
+ @refresh_token_decorator
491
+ async def get_mqtt_credentials(self) -> Response[MQTTConnection]:
492
+ """Get mammotion mqtt credentials"""
493
+ resp = await self._session.post(
494
+ f"{self.jwt_info.iot}/v1/mqtt/auth/jwt",
495
+ headers={
496
+ **self._headers,
497
+ "Authorization": f"Bearer {self.login_info.access_token}",
498
+ "Content-Type": "application/json",
499
+ "User-Agent": "okhttp/4.9.3",
500
+ },
501
+ )
502
+ if resp.status != 200:
503
+ return Response.from_dict({"code": resp.status, "msg": "get mqtt failed"})
504
+ resp_dict = await resp.json()
505
+ response = response_factory(Response[MQTTConnection], resp_dict)
506
+ self.mqtt_credentials = response.data
507
+ return response
508
+
509
+ @refresh_token_decorator
510
+ async def mqtt_invoke(self, content: str, device_name: str, iot_id: str) -> Response[dict]:
511
+ """Send mqtt commands to devices."""
512
+ resp = await self._session.post(
513
+ f"{self.jwt_info.iot}/v1/mqtt/rpc/thing/service/invoke",
514
+ json={
515
+ "args": {"content": content},
516
+ "deviceName": device_name,
517
+ "identifier": "device_protobuf_sync_service",
518
+ "iotId": iot_id,
519
+ "productKey": "",
520
+ },
521
+ headers={
522
+ **self._headers,
523
+ "Authorization": f"Bearer {self.login_info.access_token}",
524
+ "Content-Type": "application/json",
525
+ "User-Agent": "okhttp/4.9.3",
526
+ "Client-Id": self.client_id,
527
+ "Client-Type": "1",
528
+ },
529
+ )
530
+ if resp.status != 200:
531
+ return Response.from_dict({"code": resp.status, "msg": "invoke mqtt failed"})
532
+ if resp.status == 401:
533
+ raise UnauthorizedException("Access Token expired")
534
+ resp_dict = await resp.json()
535
+ return response_factory(Response[dict], resp_dict)
536
+
537
+ async def refresh_login(self) -> Response[LoginResponseData]:
538
+ if self.expires_in > time.time():
539
+ res = await self.refresh_token_v2()
540
+ if res.code == 0:
541
+ return res
542
+ return await self.login_v2(self.account, self._password)
543
+
544
+ async def login(self, account: str, password: str) -> Response[LoginResponseData]:
545
+ """Logs in to the service using provided account and password."""
546
+ self.account = account
547
+ self._password = password
548
+ resp = await self._session.post(
549
+ f"{MAMMOTION_DOMAIN}/oauth/token",
550
+ headers={
551
+ **self._headers,
552
+ "Encrypt-Key": self.encryption_utils.encrypt_by_public_key(),
553
+ "Decrypt-Type": "3",
554
+ "Ec-Version": "v1",
555
+ },
556
+ params={
557
+ "username": self.encryption_utils.encryption_by_aes(account),
558
+ "password": self.encryption_utils.encryption_by_aes(password),
559
+ "client_id": self.encryption_utils.encryption_by_aes(MAMMOTION_CLIENT_ID),
560
+ "client_secret": self.encryption_utils.encryption_by_aes(MAMMOTION_CLIENT_SECRET),
561
+ "grant_type": self.encryption_utils.encryption_by_aes("password"),
562
+ },
563
+ )
564
+ if resp.status != 200:
565
+ print(resp.json())
566
+ return Response.from_dict({"code": resp.status, "msg": "Login failed"})
567
+ data = await resp.json()
568
+ login_response = response_factory(Response[LoginResponseData], data)
569
+ if login_response is None or login_response.data is None:
570
+ print(login_response)
571
+ return Response.from_dict({"code": resp.status, "msg": "Login failed"})
572
+ self.login_info = login_response.data
573
+ self.expires_in = login_response.data.expires_in + time.time()
574
+ self._headers["Authorization"] = f"Bearer {self.login_info.access_token}" if login_response.data else None
575
+ self.response = login_response
576
+ self.msg = login_response.msg
577
+ self.code = login_response.code
578
+ # TODO catch errors from mismatch user / password elsewhere
579
+ # Assuming the data format matches the expected structure
580
+ return login_response
581
+
582
+ async def refresh_token_v2(self) -> Response[LoginResponseData]:
583
+ """Refresh token v2."""
584
+
585
+ refresh_request = {
586
+ "client_id": MAMMOTION_OUATH2_CLIENT_ID,
587
+ "refresh_token": self.login_info.refresh_token,
588
+ "grant_type": "refresh_token",
589
+ }
590
+
591
+ oauth_signature = create_oauth_signature(
592
+ login_req=refresh_request,
593
+ client_id=MAMMOTION_OUATH2_CLIENT_ID,
594
+ client_secret=MAMMOTION_OUATH2_CLIENT_SECRET,
595
+ token_endpoint="/oauth2/token",
596
+ )
597
+
598
+ resp = await self._session.post(
599
+ f"{MAMMOTION_DOMAIN}/oauth2/token",
600
+ headers={
601
+ **self._headers,
602
+ "Ma-Iot-Signature": oauth_signature,
603
+ "Ma-Timestamp": str(int(time.time())),
604
+ "Client-Id": self.client_id,
605
+ "Client-Type": "1",
606
+ },
607
+ params={
608
+ **refresh_request,
609
+ },
610
+ )
611
+ data = await resp.json()
612
+ refresh_response = response_factory(Response[LoginResponseData], data)
613
+ if refresh_response is None or refresh_response.data is None:
614
+ return Response.from_dict({"code": resp.status, "msg": "Login failed"})
615
+ self.login_info = refresh_response.data
616
+ self.expires_in = refresh_response.data.expires_in + time.time()
617
+ self._headers["Authorization"] = f"Bearer {self.login_info.access_token}" if refresh_response.data else None
618
+ self.response = refresh_response
619
+ self.msg = refresh_response.msg
620
+ self.code = refresh_response.code
621
+ return refresh_response
622
+
623
+ async def login_v2(self, account: str, password: str) -> Response[LoginResponseData]:
624
+ """Logs in to the service using provided account and password."""
625
+ self.account = account
626
+ self._password = password
627
+
628
+ login_request = {
629
+ "username": account,
630
+ "password": base64.b64encode(password.encode("utf-8")).decode("utf-8"),
631
+ "client_id": MAMMOTION_OUATH2_CLIENT_ID,
632
+ "grant_type": "password",
633
+ "authType": "0",
634
+ }
635
+
636
+ oauth_signature = create_oauth_signature(
637
+ login_req=login_request,
638
+ client_id=MAMMOTION_OUATH2_CLIENT_ID,
639
+ client_secret=MAMMOTION_OUATH2_CLIENT_SECRET,
640
+ token_endpoint="/oauth2/token",
641
+ )
642
+
643
+ resp = await self._session.post(
644
+ f"{MAMMOTION_DOMAIN}/oauth2/token",
645
+ headers={
646
+ **self._headers,
647
+ "Ma-App-Key": MAMMOTION_OUATH2_CLIENT_ID,
648
+ "Ma-Signature": oauth_signature,
649
+ "Ma-Timestamp": str(int(time.time())),
650
+ "Client-Id": self.client_id,
651
+ "Client-Type": "1",
652
+ },
653
+ params={
654
+ **login_request,
655
+ },
656
+ )
657
+ if resp.status != 200:
658
+ return Response.from_dict({"code": resp.status, "msg": "Login failed"})
659
+ data = await resp.json()
660
+ if data.get("code") != 0:
661
+ return Response.from_dict({"code": resp.status, "msg": data.get("msg") or "Login failed"})
662
+ login_response = response_factory(Response[LoginResponseData], data)
663
+ if login_response is None or login_response.data is None:
664
+ return Response.from_dict({"code": resp.status, "msg": "Login failed"})
665
+ self.login_info = login_response.data
666
+ self.expires_in = login_response.data.expires_in + time.time()
667
+ self._headers["Authorization"] = f"Bearer {self.login_info.access_token}" if login_response.data else None
668
+ self.response = login_response
669
+ self.msg = login_response.msg
670
+ self.code = login_response.code
671
+ # TODO catch errors from mismatch user / password elsewhere
672
+ # Assuming the data format matches the expected structure
673
+ return login_response
File without changes