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
pymammotion/http/http.py
ADDED
|
@@ -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
|