pymammotion 0.3.8__py3-none-any.whl → 0.4.0__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 +2 -2
- pymammotion/aliyun/cloud_gateway.py +12 -9
- pymammotion/aliyun/model/aep_response.py +1 -2
- pymammotion/aliyun/model/dev_by_account_response.py +7 -8
- pymammotion/aliyun/model/login_by_oauth_response.py +2 -3
- pymammotion/aliyun/model/regions_response.py +3 -3
- pymammotion/aliyun/model/session_by_authcode_response.py +1 -2
- pymammotion/aliyun/model/stream_subscription_response.py +1 -2
- pymammotion/bluetooth/ble.py +5 -5
- pymammotion/bluetooth/ble_message.py +9 -13
- pymammotion/data/model/device.py +31 -228
- pymammotion/data/model/device_config.py +0 -10
- pymammotion/data/model/device_info.py +13 -0
- pymammotion/data/model/device_limits.py +49 -0
- pymammotion/data/model/generate_route_information.py +1 -1
- pymammotion/data/model/hash_list.py +6 -2
- pymammotion/data/model/plan.py +0 -3
- pymammotion/data/model/raw_data.py +215 -0
- pymammotion/data/model/region_data.py +10 -11
- pymammotion/data/model/report_info.py +1 -1
- pymammotion/data/mqtt/event.py +18 -14
- pymammotion/data/mqtt/properties.py +1 -1
- pymammotion/data/mqtt/status.py +1 -1
- pymammotion/data/state_manager.py +83 -23
- pymammotion/http/encryption.py +220 -0
- pymammotion/http/http.py +92 -39
- pymammotion/http/model/http.py +2 -2
- pymammotion/mammotion/commands/abstract_message.py +2 -2
- pymammotion/mammotion/commands/messages/driver.py +28 -21
- pymammotion/mammotion/commands/messages/media.py +10 -14
- pymammotion/mammotion/commands/messages/navigation.py +14 -11
- pymammotion/mammotion/commands/messages/network.py +15 -12
- pymammotion/mammotion/commands/messages/ota.py +9 -14
- pymammotion/mammotion/commands/messages/system.py +27 -24
- pymammotion/mammotion/commands/messages/video.py +9 -14
- pymammotion/mammotion/devices/base.py +7 -14
- pymammotion/mammotion/devices/mammotion.py +22 -13
- pymammotion/mammotion/devices/mammotion_bluetooth.py +15 -4
- pymammotion/mammotion/devices/mammotion_cloud.py +30 -12
- pymammotion/mqtt/linkkit/__init__.py +5 -0
- pymammotion/mqtt/linkkit/h2client.py +585 -0
- pymammotion/mqtt/linkkit/linkkit.py +3020 -0
- pymammotion/mqtt/mammotion_mqtt.py +13 -9
- pymammotion/proto/__init__.py +2176 -1
- pymammotion/proto/luba_mul.proto +1 -0
- pymammotion/proto/luba_mul_pb2.py +8 -8
- pymammotion/proto/luba_mul_pb2.pyi +1 -0
- pymammotion/proto/mctrl_nav_pb2.py +69 -67
- pymammotion/proto/mctrl_nav_pb2.pyi +13 -5
- pymammotion/proto/mctrl_sys_pb2.py +41 -37
- pymammotion/proto/mctrl_sys_pb2.pyi +34 -11
- pymammotion/utility/constant/device_constant.py +14 -5
- pymammotion/utility/device_config.py +754 -0
- pymammotion/utility/device_type.py +64 -16
- {pymammotion-0.3.8.dist-info → pymammotion-0.4.0.dist-info}/METADATA +9 -9
- {pymammotion-0.3.8.dist-info → pymammotion-0.4.0.dist-info}/RECORD +58 -62
- {pymammotion-0.3.8.dist-info → pymammotion-0.4.0.dist-info}/WHEEL +1 -1
- pymammotion/aliyun/cloud_service.py +0 -65
- pymammotion/proto/basestation.py +0 -59
- pymammotion/proto/common.py +0 -12
- pymammotion/proto/dev_net.py +0 -381
- pymammotion/proto/luba_msg.py +0 -81
- pymammotion/proto/luba_mul.py +0 -76
- pymammotion/proto/mctrl_driver.py +0 -100
- pymammotion/proto/mctrl_nav.py +0 -664
- pymammotion/proto/mctrl_ota.py +0 -48
- pymammotion/proto/mctrl_pept.py +0 -41
- pymammotion/proto/mctrl_sys.py +0 -574
- {pymammotion-0.3.8.dist-info → pymammotion-0.4.0.dist-info}/LICENSE +0 -0
@@ -0,0 +1,220 @@
|
|
1
|
+
import base64
|
2
|
+
import logging
|
3
|
+
import secrets
|
4
|
+
import string
|
5
|
+
|
6
|
+
from cryptography.hazmat.backends import default_backend
|
7
|
+
from cryptography.hazmat.primitives import padding, serialization
|
8
|
+
from cryptography.hazmat.primitives.asymmetric import padding as rsa_padding
|
9
|
+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
10
|
+
|
11
|
+
_LOGGER = logging.getLogger(__name__)
|
12
|
+
|
13
|
+
|
14
|
+
class EncryptionUtils:
|
15
|
+
PRIVATE_KEY = """MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAOFizbd1fC5XNKJ89u0XNvPZNR/L
|
16
|
+
0h547iSWjOCuvvMu76ZSaS3/Tu2C1C+XmlnmBWTyY4ON+xECiNUXm/aWQ3P0g+wf60zjPbNzgL2Q
|
17
|
+
7njXJG6wka4KkbdQxUdS0TTpL256LnV1LsG855bsbJIJiQPbfUq6HbB5xH7sXdrmFu1DAgMBAAEC
|
18
|
+
gYEAoT2TGE1ncquWjyxBZup1uMvKkp25C23OSMSfslmxZ75LWjyY3HxK1eYDsKyPkwLZFxfFE6du
|
19
|
+
VwPuKiyCuk1ToPfnb4niTGzXPyC2PbO4SFrWL8n1YZ80M0bfTGI9dMCZvpmZJ41WYUsBaf2374lt
|
20
|
+
oEiDEHJp7MeXk/970xiKP1ECQQD65rLHk840q+FZS6kZVexJucPZj/YAII6klU1E20ctioe8Pi5m
|
21
|
+
WSPqclH27/t4FqdvP7tFqaavyXg+CEQpxmxLAkEA5fddDuzcjWgF9pl9fP7/baFMYjUS9z1Vc3gx
|
22
|
+
CnvAgCnv71wjDQhvsUc6sAiidsBGFDyud06RyyLcOlQchMb36QJBAIui/Xjpn+fciQxjeXcqRNk7
|
23
|
+
U+6vml+zvu+GUHyz9Uc5RBXWHYjEr6J5gXiHU1MgeIsH0zgQFT7cR9luTFFbp0UCQFIntfogCocG
|
24
|
+
E6NOoHMoUi5jQnuPRHBJXB69YJ/DKDlhQhN8EhWU3voxXTkITKop9J9EMnvy+MjecljwNaQFxQkC
|
25
|
+
QB9lz67iDe9Gj8NxSElVZxUm9EfbL1RPqTZPx/lADR06CPB8pP3Bl5/5/5RGzc+UTZ+wX5GWKvC7
|
26
|
+
zUJaROxQB+E=""".replace(" ", "")
|
27
|
+
|
28
|
+
PUBLIC_KEY_PROD = """MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApLbeSgOvnwLTWbhaBQWNnnHMtSDAi
|
29
|
+
Gz0PEDbrtd1tLYoO0hukW5PSa6eHykch0Hc6etiqEx1xziS+vNf+iOXds70I4htaYit6yRToZlQ
|
30
|
+
Mim3DQxaZX68nIHIZogur0zGv9U8j01v5l/rHRxyDdlVx3+JkBg6Cqx4U1PXEnAJriqcyg0B8Gm
|
31
|
+
V8Lnmfng+aJLRyq5MkhstYCRv9AsmWu8NpZDJ1ffbkaS02Z9/wpubXTiFP6DG3V2mDw2VvzEcHi
|
32
|
+
cchw49oXmTi92yui+kBgSYlNygssOAyU6H071AfmRUeH3+TsV5u5rg+bCiKyHemVmcKdd3hhZB+
|
33
|
+
HjA8o3On6rg5wIDAQAB""".replace(" ", "")
|
34
|
+
|
35
|
+
PUBLIC_KEY_TEST = """MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC1nAzH31arNBmYKvTlvKgkxI1MIr4HpfLbmM
|
36
|
+
XPIhd8D/cXB0dYY1ppUq4a/ezq41YShN88e0elyZgqdnFrkhiLpnKWa7jXtVRgXi9eS18PLO8ns
|
37
|
+
eHude9URaj7relK1AZ0xovKsbLKHd01PpmngLXZfnKA06J2ru/zH+cnpXdy8QIDAQAB""".replace(" ", "")
|
38
|
+
|
39
|
+
def __init__(self) -> None:
|
40
|
+
self.AES_PASW = self.get_aes_key() # Get from previous implementation
|
41
|
+
self.IV = self.get_iv() # Get from previous implementation
|
42
|
+
self._public_key = self.load_public_key()
|
43
|
+
self._private_key = self.load_private_key()
|
44
|
+
|
45
|
+
@staticmethod
|
46
|
+
def load_private_key():
|
47
|
+
"""Load the private key from base64 encoded string"""
|
48
|
+
try:
|
49
|
+
private_key_bytes = base64.b64decode(EncryptionUtils.PRIVATE_KEY)
|
50
|
+
return serialization.load_der_private_key(private_key_bytes, password=None, backend=default_backend())
|
51
|
+
except Exception as e:
|
52
|
+
raise Exception(f"Failed to load private key: {e!s}")
|
53
|
+
|
54
|
+
@staticmethod
|
55
|
+
def load_public_key(is_production: bool = True):
|
56
|
+
"""Load the public key from base64 encoded string
|
57
|
+
|
58
|
+
Args:
|
59
|
+
is_production (bool): If True, uses production key, else uses test key
|
60
|
+
|
61
|
+
"""
|
62
|
+
try:
|
63
|
+
key_string = EncryptionUtils.PUBLIC_KEY_PROD if is_production else EncryptionUtils.PUBLIC_KEY_TEST
|
64
|
+
public_key_bytes = base64.b64decode(key_string)
|
65
|
+
return serialization.load_der_public_key(public_key_bytes, backend=default_backend())
|
66
|
+
except Exception as e:
|
67
|
+
raise Exception(f"Failed to load public key: {e!s}")
|
68
|
+
|
69
|
+
@staticmethod
|
70
|
+
def encrypt(plaintext: str, key: str, iv: str) -> str:
|
71
|
+
"""Encrypt text using AES/CBC/PKCS5Padding
|
72
|
+
|
73
|
+
Args:
|
74
|
+
plaintext (str): Text to encrypt
|
75
|
+
key (str): Encryption key
|
76
|
+
iv (str): Initialization vector
|
77
|
+
|
78
|
+
Returns:
|
79
|
+
str: Base64 encoded encrypted string
|
80
|
+
|
81
|
+
Raises:
|
82
|
+
Exception: If encryption fails
|
83
|
+
|
84
|
+
"""
|
85
|
+
try:
|
86
|
+
# Convert strings to bytes
|
87
|
+
plaintext_bytes = plaintext.encode("utf-8")
|
88
|
+
key_bytes = key.encode("utf-8")
|
89
|
+
iv_bytes = iv.encode("utf-8")
|
90
|
+
|
91
|
+
# Create padder
|
92
|
+
padder = padding.PKCS7(128).padder()
|
93
|
+
padded_data = padder.update(plaintext_bytes) + padder.finalize()
|
94
|
+
|
95
|
+
# Create cipher
|
96
|
+
cipher = Cipher(algorithms.AES(key_bytes), modes.CBC(iv_bytes), backend=default_backend())
|
97
|
+
|
98
|
+
# Encrypt
|
99
|
+
encryptor = cipher.encryptor()
|
100
|
+
encrypted_bytes = encryptor.update(padded_data) + encryptor.finalize()
|
101
|
+
|
102
|
+
# Encode to base64
|
103
|
+
return base64.b64encode(encrypted_bytes).decode("utf-8")
|
104
|
+
|
105
|
+
except Exception as e:
|
106
|
+
raise Exception(f"Encryption failed: {e!s}")
|
107
|
+
|
108
|
+
def encryption_by_aes(self, text: str) -> str:
|
109
|
+
"""Encrypt text using AES with class-level key and IV
|
110
|
+
|
111
|
+
Args:
|
112
|
+
text (str): Text to encrypt
|
113
|
+
|
114
|
+
Returns:
|
115
|
+
str: Encrypted text or None if encryption fails
|
116
|
+
|
117
|
+
"""
|
118
|
+
try:
|
119
|
+
# Perform encryption
|
120
|
+
encrypted = self.encrypt(text, self.AES_PASW, self.IV)
|
121
|
+
|
122
|
+
return encrypted
|
123
|
+
|
124
|
+
except Exception as e:
|
125
|
+
_LOGGER.error(f"Encryption failed: {e!s}")
|
126
|
+
return None
|
127
|
+
|
128
|
+
def encrypt_by_public_key(self) -> str | None:
|
129
|
+
"""Encrypt data using RSA public key.
|
130
|
+
|
131
|
+
Args:
|
132
|
+
|
133
|
+
Returns:
|
134
|
+
Optional[str]: Base64 encoded encrypted data or None if encryption fails
|
135
|
+
|
136
|
+
"""
|
137
|
+
|
138
|
+
data = f"{self.AES_PASW},{self.IV}"
|
139
|
+
|
140
|
+
if not self._public_key:
|
141
|
+
_LOGGER.error("Public key not initialized")
|
142
|
+
return None
|
143
|
+
|
144
|
+
try:
|
145
|
+
# Convert input string to bytes
|
146
|
+
data_bytes = data.encode("utf-8")
|
147
|
+
|
148
|
+
# Encrypt the data padding.PKCS7(128).padder()
|
149
|
+
encrypted_bytes = self._public_key.encrypt(data_bytes, rsa_padding.PKCS1v15())
|
150
|
+
|
151
|
+
# Convert to base64 string
|
152
|
+
encrypted_str = base64.b64encode(encrypted_bytes).decode("utf-8")
|
153
|
+
_LOGGER.debug("Data encrypted successfully")
|
154
|
+
|
155
|
+
return encrypted_str
|
156
|
+
|
157
|
+
except Exception as err:
|
158
|
+
_LOGGER.error("Encryption failed: %s", str(err))
|
159
|
+
return None
|
160
|
+
|
161
|
+
@staticmethod
|
162
|
+
def get_random_string(length: int) -> str:
|
163
|
+
"""Generate a random string of specified length using alphanumeric characters.
|
164
|
+
|
165
|
+
Args:
|
166
|
+
length (int): The desired length of the random string
|
167
|
+
|
168
|
+
Returns:
|
169
|
+
str: A random alphanumeric string of specified length
|
170
|
+
|
171
|
+
Raises:
|
172
|
+
ValueError: If length is less than 1
|
173
|
+
|
174
|
+
"""
|
175
|
+
if length < 1:
|
176
|
+
raise ValueError("Length must be positive")
|
177
|
+
|
178
|
+
charset = string.ascii_letters + string.digits
|
179
|
+
return "".join(secrets.choice(charset) for _ in range(length))
|
180
|
+
|
181
|
+
@staticmethod
|
182
|
+
def get_random_int(length: int) -> str:
|
183
|
+
"""Generate a random string of specified length containing only digits.
|
184
|
+
|
185
|
+
Args:
|
186
|
+
length (int): The desired length of the random number string
|
187
|
+
|
188
|
+
Returns:
|
189
|
+
str: A string of random digits of specified length
|
190
|
+
|
191
|
+
Raises:
|
192
|
+
ValueError: If length is less than 1
|
193
|
+
|
194
|
+
"""
|
195
|
+
if length < 1:
|
196
|
+
raise ValueError("Length must be positive")
|
197
|
+
|
198
|
+
return "".join(secrets.choice(string.digits) for _ in range(length))
|
199
|
+
|
200
|
+
@staticmethod
|
201
|
+
def get_aes_key() -> str:
|
202
|
+
"""Generate a random AES key of 16 characters using alphanumeric characters.
|
203
|
+
Matches Java implementation behavior.
|
204
|
+
|
205
|
+
Returns:
|
206
|
+
str: A 16-character random string for AES key
|
207
|
+
|
208
|
+
"""
|
209
|
+
return EncryptionUtils.get_random_string(16)
|
210
|
+
|
211
|
+
@staticmethod
|
212
|
+
def get_iv() -> str:
|
213
|
+
"""Generate a random initialization vector of 16 digits.
|
214
|
+
Matches Java implementation behavior.
|
215
|
+
|
216
|
+
Returns:
|
217
|
+
str: A 16-digit random string for initialization vector
|
218
|
+
|
219
|
+
"""
|
220
|
+
return EncryptionUtils.get_random_int(16)
|
pymammotion/http/http.py
CHANGED
@@ -4,23 +4,26 @@ from typing import cast
|
|
4
4
|
from aiohttp import ClientSession
|
5
5
|
|
6
6
|
from pymammotion.aliyun.model.stream_subscription_response import StreamSubscriptionResponse
|
7
|
-
from pymammotion.const import
|
8
|
-
|
9
|
-
MAMMOTION_CLIENT_ID,
|
10
|
-
MAMMOTION_CLIENT_SECRET,
|
11
|
-
MAMMOTION_DOMAIN,
|
12
|
-
)
|
7
|
+
from pymammotion.const import MAMMOTION_API_DOMAIN, MAMMOTION_CLIENT_ID, MAMMOTION_CLIENT_SECRET, MAMMOTION_DOMAIN
|
8
|
+
from pymammotion.http.encryption import EncryptionUtils
|
13
9
|
from pymammotion.http.model.http import ErrorInfo, LoginResponseData, Response
|
14
10
|
|
15
11
|
|
16
12
|
class MammotionHTTP:
|
17
|
-
def __init__(self
|
13
|
+
def __init__(self) -> None:
|
14
|
+
self.code = None
|
15
|
+
self.msg = None
|
16
|
+
self.response: Response | None = None
|
17
|
+
self.login_info: LoginResponseData | None = None
|
18
18
|
self._headers = {"User-Agent": "okhttp/3.14.9", "App-Version": "google Pixel 2 XL taimen-Android 11,1.11.332"}
|
19
|
-
self.
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
19
|
+
self.encryption_utils = EncryptionUtils()
|
20
|
+
|
21
|
+
@staticmethod
|
22
|
+
def generate_headers(token: str) -> dict:
|
23
|
+
return {"Authorization": f"Bearer {token}"}
|
24
|
+
|
25
|
+
async def login_by_email(self, email: str, password: str) -> Response[LoginResponseData]:
|
26
|
+
return await self.login(email, password)
|
24
27
|
|
25
28
|
async def get_all_error_codes(self) -> dict[str, ErrorInfo]:
|
26
29
|
async with ClientSession(MAMMOTION_API_DOMAIN) as session:
|
@@ -36,15 +39,55 @@ class MammotionHTTP:
|
|
36
39
|
codes[error_info.code] = error_info
|
37
40
|
return codes
|
38
41
|
|
39
|
-
async def oauth_check(self) ->
|
42
|
+
async def oauth_check(self) -> Response:
|
40
43
|
"""Check if token is valid.
|
41
44
|
|
42
45
|
Returns 401 if token is invalid. We then need to re-authenticate, can try to refresh token first
|
43
46
|
"""
|
44
47
|
async with ClientSession(MAMMOTION_API_DOMAIN) as session:
|
45
|
-
async with session.post("/user-server/v1/user/oauth/check") as resp:
|
48
|
+
async with session.post("/user-server/v1/user/oauth/check", headers=self._headers) as resp:
|
49
|
+
data = await resp.json()
|
50
|
+
return Response.from_dict(data)
|
51
|
+
|
52
|
+
async def pair_devices_mqtt(self, mower_name: str, rtk_name: str) -> Response:
|
53
|
+
async with ClientSession(MAMMOTION_API_DOMAIN) as session:
|
54
|
+
async with session.post(
|
55
|
+
"/device-server/v1/iot/device/pairing",
|
56
|
+
headers=self._headers,
|
57
|
+
json={"mowerName": mower_name, "rtkName": rtk_name},
|
58
|
+
) as resp:
|
59
|
+
data = await resp.json()
|
60
|
+
if data.get("status") == 200:
|
61
|
+
print(data)
|
62
|
+
return Response.from_dict(data)
|
63
|
+
else:
|
64
|
+
print(data)
|
65
|
+
|
66
|
+
async def unpair_devices_mqtt(self, mower_name: str, rtk_name: str) -> Response:
|
67
|
+
async with ClientSession(MAMMOTION_API_DOMAIN) as session:
|
68
|
+
async with session.post(
|
69
|
+
"/device-server/v1/iot/device/unpairing",
|
70
|
+
headers=self._headers,
|
71
|
+
json={"mowerName": mower_name, "rtkName": rtk_name},
|
72
|
+
) as resp:
|
73
|
+
data = await resp.json()
|
74
|
+
if data.get("status") == 200:
|
75
|
+
print(data)
|
76
|
+
return Response.from_dict(data)
|
77
|
+
else:
|
78
|
+
print(data)
|
79
|
+
|
80
|
+
async def net_rtk_enable(self, device_id: str) -> Response:
|
81
|
+
async with ClientSession(MAMMOTION_API_DOMAIN) as session:
|
82
|
+
async with session.post(
|
83
|
+
"/device-server/v1/iot/net-rtk/enable", headers=self._headers, json={"deviceId": device_id}
|
84
|
+
) as resp:
|
46
85
|
data = await resp.json()
|
47
|
-
|
86
|
+
if data.get("status") == 200:
|
87
|
+
print(data)
|
88
|
+
return Response.from_dict(data)
|
89
|
+
else:
|
90
|
+
print(data)
|
48
91
|
|
49
92
|
async def get_stream_subscription(self, iot_id: str) -> Response[StreamSubscriptionResponse]:
|
50
93
|
"""Get agora.io data for view camera stream"""
|
@@ -63,27 +106,37 @@ class MammotionHTTP:
|
|
63
106
|
# Assuming the data format matches the expected structure
|
64
107
|
return Response[StreamSubscriptionResponse].from_dict(data)
|
65
108
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
109
|
+
async def login(self, username: str, password: str) -> Response[LoginResponseData]:
|
110
|
+
async with ClientSession(MAMMOTION_DOMAIN) as session:
|
111
|
+
async with session.post(
|
112
|
+
"/oauth/token",
|
113
|
+
headers={
|
114
|
+
"User-Agent": "okhttp/3.14.9",
|
115
|
+
"App-Version": "google Pixel 2 XL taimen-Android 11,1.11.332",
|
116
|
+
"Encrypt-Key": self.encryption_utils.encrypt_by_public_key(),
|
117
|
+
"Decrypt-Type": "3",
|
118
|
+
"Ec-Version": "v1",
|
119
|
+
},
|
120
|
+
params=dict(
|
121
|
+
username=self.encryption_utils.encryption_by_aes(username),
|
122
|
+
password=self.encryption_utils.encryption_by_aes(password),
|
123
|
+
client_id=self.encryption_utils.encryption_by_aes(MAMMOTION_CLIENT_ID),
|
124
|
+
client_secret=self.encryption_utils.encryption_by_aes(MAMMOTION_CLIENT_SECRET),
|
125
|
+
grant_type=self.encryption_utils.encryption_by_aes("password"),
|
126
|
+
),
|
127
|
+
) as resp:
|
128
|
+
if resp.status != 200:
|
129
|
+
print(resp.json())
|
130
|
+
return Response.from_dict({"status": resp.status, "msg": "Login failed"})
|
131
|
+
data = await resp.json()
|
132
|
+
login_response = Response[LoginResponseData].from_dict(data)
|
133
|
+
self.login_info = LoginResponseData.from_dict(login_response.data)
|
134
|
+
self._headers["Authorization"] = (
|
135
|
+
f"Bearer {self.login_info.access_token}" if login_response.data else None
|
136
|
+
)
|
137
|
+
self.response = login_response
|
138
|
+
self.msg = login_response.msg
|
139
|
+
self.code = login_response.code
|
140
|
+
# TODO catch errors from mismatch user / password elsewhere
|
141
|
+
# Assuming the data format matches the expected structure
|
142
|
+
return login_response
|
pymammotion/http/model/http.py
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
from dataclasses import dataclass
|
2
|
-
from typing import Generic, Literal,
|
2
|
+
from typing import Generic, Literal, TypeVar
|
3
3
|
|
4
4
|
from mashumaro import DataClassDictMixin
|
5
5
|
from mashumaro.config import BaseConfig
|
@@ -83,7 +83,7 @@ class LoginResponseUserInformation(DataClassORJSONMixin):
|
|
83
83
|
userId: str
|
84
84
|
userAccount: str
|
85
85
|
authType: str
|
86
|
-
email:
|
86
|
+
email: str | None = None
|
87
87
|
|
88
88
|
class Config(BaseConfig):
|
89
89
|
omit_none = True
|
@@ -1,6 +1,6 @@
|
|
1
1
|
from abc import abstractmethod
|
2
2
|
|
3
|
-
from pymammotion.proto
|
3
|
+
from pymammotion.proto import MsgCmdType, MsgDevice
|
4
4
|
from pymammotion.utility.device_type import DeviceType
|
5
5
|
|
6
6
|
|
@@ -16,7 +16,7 @@ class AbstractMessage:
|
|
16
16
|
"""Changes the rcver name if it's not a luba1."""
|
17
17
|
if (
|
18
18
|
not DeviceType.is_luba1(self.get_device_name(), self.get_device_product_key())
|
19
|
-
and msg_type == MsgCmdType.
|
19
|
+
and msg_type == MsgCmdType.NAV
|
20
20
|
):
|
21
21
|
return MsgDevice.DEV_NAVIGATION
|
22
22
|
return msg_device
|
@@ -1,11 +1,22 @@
|
|
1
1
|
# === sendOrderMsg_Driver ===
|
2
|
-
import time
|
3
2
|
from abc import ABC
|
4
3
|
from logging import getLogger
|
4
|
+
import time
|
5
5
|
|
6
6
|
from pymammotion.mammotion.commands.abstract_message import AbstractMessage
|
7
|
-
from pymammotion.proto import
|
8
|
-
|
7
|
+
from pymammotion.proto import (
|
8
|
+
DrvKnifeHeight,
|
9
|
+
DrvMotionCtrl,
|
10
|
+
DrvMowCtrlByHand,
|
11
|
+
DrvSrSpeed,
|
12
|
+
LubaMsg,
|
13
|
+
MctlDriver,
|
14
|
+
MsgAttr,
|
15
|
+
MsgCmdType,
|
16
|
+
MsgDevice,
|
17
|
+
RtkCfgReqT,
|
18
|
+
RtkSysMaskQueryT,
|
19
|
+
)
|
9
20
|
|
10
21
|
logger = getLogger(__name__)
|
11
22
|
|
@@ -13,10 +24,10 @@ logger = getLogger(__name__)
|
|
13
24
|
class MessageDriver(AbstractMessage, ABC):
|
14
25
|
def send_order_msg_driver(self, driver) -> bytes:
|
15
26
|
return LubaMsg(
|
16
|
-
msgtype=MsgCmdType.
|
27
|
+
msgtype=MsgCmdType.EMBED_DRIVER,
|
17
28
|
sender=MsgDevice.DEV_MOBILEAPP,
|
18
|
-
rcver=self.get_msg_device(MsgCmdType.
|
19
|
-
msgattr=MsgAttr.
|
29
|
+
rcver=self.get_msg_device(MsgCmdType.EMBED_DRIVER, MsgDevice.DEV_MAINCTL),
|
30
|
+
msgattr=MsgAttr.REQ,
|
20
31
|
timestamp=round(time.time() * 1000),
|
21
32
|
seqs=1,
|
22
33
|
version=1,
|
@@ -26,25 +37,23 @@ class MessageDriver(AbstractMessage, ABC):
|
|
26
37
|
|
27
38
|
def set_blade_height(self, height: int):
|
28
39
|
logger.debug(f"Send knife height height={height}")
|
29
|
-
build =
|
40
|
+
build = MctlDriver(todev_knife_height_set=DrvKnifeHeight(knife_height=height))
|
30
41
|
logger.debug(f"Send command--Knife motor height setting height={height}")
|
31
42
|
return self.send_order_msg_driver(build)
|
32
43
|
|
33
44
|
def set_speed(self, speed: float):
|
34
45
|
logger.debug(f"{self.get_device_name()} set speed, {speed}")
|
35
|
-
build =
|
46
|
+
build = MctlDriver(bidire_speed_read_set=DrvSrSpeed(speed=speed, rw=1))
|
36
47
|
logger.debug(f"Send command--Speed setting speed={speed}")
|
37
48
|
return self.send_order_msg_driver(build)
|
38
49
|
|
39
50
|
def syn_nav_star_point_data(self, sat_system: int):
|
40
|
-
build =
|
51
|
+
build = MctlDriver(rtk_sys_mask_query=RtkSysMaskQueryT(sat_system=sat_system))
|
41
52
|
logger.debug(f"Send command--Navigation satellite frequency point synchronization={sat_system}")
|
42
53
|
return self.send_order_msg_driver(build)
|
43
54
|
|
44
55
|
def set_nav_star_point(self, cmd_req: str):
|
45
|
-
build =
|
46
|
-
rtk_cfg_req=mctrl_driver.RtkCfgReqT(cmd_req=cmd_req, cmd_length=len(cmd_req) - 1)
|
47
|
-
)
|
56
|
+
build = MctlDriver(rtk_cfg_req=RtkCfgReqT(cmd_req=cmd_req, cmd_length=len(cmd_req) - 1))
|
48
57
|
logger.debug(f"Send command--Navigation satellite frequency point setting={cmd_req}")
|
49
58
|
logger.debug(
|
50
59
|
f"Navigation satellite setting, Send command--Navigation satellite frequency point setting={cmd_req}"
|
@@ -52,7 +61,7 @@ class MessageDriver(AbstractMessage, ABC):
|
|
52
61
|
return self.send_order_msg_driver(build)
|
53
62
|
|
54
63
|
def get_speed(self):
|
55
|
-
build =
|
64
|
+
build = MctlDriver(bidire_speed_read_set=DrvSrSpeed(rw=0))
|
56
65
|
logger.debug("Send command--Get speed value")
|
57
66
|
return self.send_order_msg_driver(build)
|
58
67
|
|
@@ -63,12 +72,12 @@ class MessageDriver(AbstractMessage, ABC):
|
|
63
72
|
cut_knife_height: int,
|
64
73
|
max_run_speed: float,
|
65
74
|
):
|
66
|
-
build =
|
67
|
-
mow_ctrl_by_hand=
|
75
|
+
build = MctlDriver(
|
76
|
+
mow_ctrl_by_hand=DrvMowCtrlByHand(
|
68
77
|
main_ctrl=main_ctrl,
|
69
78
|
cut_knife_ctrl=cut_knife_ctrl,
|
70
79
|
cut_knife_height=cut_knife_height,
|
71
|
-
|
80
|
+
max_run_speed=max_run_speed,
|
72
81
|
)
|
73
82
|
)
|
74
83
|
logger.debug(
|
@@ -78,13 +87,11 @@ class MessageDriver(AbstractMessage, ABC):
|
|
78
87
|
|
79
88
|
return self.send_order_msg_driver(build)
|
80
89
|
|
81
|
-
def send_movement(self, linear_speed: int, angular_speed: int):
|
90
|
+
def send_movement(self, linear_speed: int, angular_speed: int) -> bytes:
|
82
91
|
logger.debug(f"Control command print, linearSpeed={
|
83
92
|
linear_speed} // angularSpeed={angular_speed}")
|
84
93
|
return self.send_order_msg_driver(
|
85
|
-
|
86
|
-
todev_devmotion_ctrl=
|
87
|
-
set_linear_speed=linear_speed, set_angular_speed=angular_speed
|
88
|
-
)
|
94
|
+
MctlDriver(
|
95
|
+
todev_devmotion_ctrl=DrvMotionCtrl(set_linear_speed=linear_speed, set_angular_speed=angular_speed)
|
89
96
|
)
|
90
97
|
)
|
@@ -2,18 +2,16 @@
|
|
2
2
|
from abc import ABC
|
3
3
|
|
4
4
|
from pymammotion.mammotion.commands.abstract_message import AbstractMessage
|
5
|
-
from pymammotion.proto import
|
6
|
-
from pymammotion.proto.luba_msg import MsgCmdType, MsgDevice
|
7
|
-
from pymammotion.proto.luba_mul import MUL_LANGUAGE
|
5
|
+
from pymammotion.proto import LubaMsg, MsgAttr, MsgCmdType, MsgDevice, MulLanguage
|
8
6
|
|
9
7
|
|
10
8
|
class MessageMedia(AbstractMessage, ABC):
|
11
9
|
def send_order_msg_media(self, mul):
|
12
|
-
luba_msg =
|
13
|
-
msgtype=
|
14
|
-
sender=
|
15
|
-
rcver=self.get_msg_device(MsgCmdType.
|
16
|
-
msgattr=
|
10
|
+
luba_msg = LubaMsg.LubaMsg(
|
11
|
+
msgtype=MsgCmdType.MUL,
|
12
|
+
sender=MsgDevice.DEV_MOBILEAPP,
|
13
|
+
rcver=self.get_msg_device(MsgCmdType.MUL, MsgDevice.SOC_MODULE_MULTIMEDIA),
|
14
|
+
msgattr=MsgAttr.REQ,
|
17
15
|
seqs=1,
|
18
16
|
version=1,
|
19
17
|
subtype=1,
|
@@ -23,12 +21,10 @@ class MessageMedia(AbstractMessage, ABC):
|
|
23
21
|
return luba_msg.SerializeToString()
|
24
22
|
|
25
23
|
def set_car_volume(self, volume: int):
|
26
|
-
return self.send_order_msg_media(
|
24
|
+
return self.send_order_msg_media(LubaMsg.SocMul(set_audio=LubaMsg.MulSetAudio(at_switch=volume)))
|
27
25
|
|
28
|
-
def set_car_voice_language(self, language_type:
|
29
|
-
return self.send_order_msg_media(
|
30
|
-
luba_mul_pb2.SocMul(set_audio=luba_mul_pb2.MulSetAudio(au_language=language_type))
|
31
|
-
)
|
26
|
+
def set_car_voice_language(self, language_type: MulLanguage | str | None):
|
27
|
+
return self.send_order_msg_media(LubaMsg.SocMul(set_audio=LubaMsg.MulSetAudio(au_language=language_type)))
|
32
28
|
|
33
29
|
def set_car_wiper(self, round_num: int):
|
34
|
-
return self.send_order_msg_media(
|
30
|
+
return self.send_order_msg_media(LubaMsg.SocMul(set_wiper=LubaMsg.MulSetWiper(round=round_num)))
|
@@ -1,16 +1,19 @@
|
|
1
1
|
# === sendOrderMsg_Nav ===
|
2
|
+
from abc import ABC
|
2
3
|
import logging
|
3
4
|
import time
|
4
|
-
from abc import ABC
|
5
5
|
|
6
6
|
from pymammotion.data.model import GenerateRouteInformation
|
7
7
|
from pymammotion.data.model.plan import Plan
|
8
8
|
from pymammotion.data.model.region_data import RegionData
|
9
9
|
from pymammotion.mammotion.commands.abstract_message import AbstractMessage
|
10
|
-
from pymammotion.proto
|
11
|
-
from pymammotion.proto.mctrl_nav import (
|
10
|
+
from pymammotion.proto import (
|
12
11
|
AppRequestCoverPathsT,
|
12
|
+
LubaMsg,
|
13
13
|
MctlNav,
|
14
|
+
MsgAttr,
|
15
|
+
MsgCmdType,
|
16
|
+
MsgDevice,
|
14
17
|
NavGetCommData,
|
15
18
|
NavGetHashList,
|
16
19
|
NavMapNameMsg,
|
@@ -32,10 +35,10 @@ logger = logging.getLogger(__name__)
|
|
32
35
|
class MessageNavigation(AbstractMessage, ABC):
|
33
36
|
def send_order_msg_nav(self, build) -> bytes:
|
34
37
|
luba_msg = LubaMsg(
|
35
|
-
msgtype=MsgCmdType.
|
38
|
+
msgtype=MsgCmdType.NAV,
|
36
39
|
sender=MsgDevice.DEV_MOBILEAPP,
|
37
|
-
rcver=self.get_msg_device(MsgCmdType.
|
38
|
-
msgattr=MsgAttr.
|
40
|
+
rcver=self.get_msg_device(MsgCmdType.NAV, MsgDevice.DEV_MAINCTL),
|
41
|
+
msgattr=MsgAttr.REQ,
|
39
42
|
seqs=1,
|
40
43
|
version=1,
|
41
44
|
subtype=1,
|
@@ -268,7 +271,7 @@ class MessageNavigation(AbstractMessage, ABC):
|
|
268
271
|
logger.debug("Send command--Get area name list")
|
269
272
|
return self.send_order_msg_nav(mctl_nav)
|
270
273
|
|
271
|
-
def set_area_name(self, device_id, hash_id: int, name: str) -> bytes:
|
274
|
+
def set_area_name(self, device_id: str, hash_id: int, name: str) -> bytes:
|
272
275
|
# Build the NavMapNameMsg with the specified parameters
|
273
276
|
mctl_nav = MctlNav(
|
274
277
|
toapp_map_name_msg=NavMapNameMsg(hash=hash_id, name=name, result=0, device_id=device_id, rw=1)
|
@@ -327,9 +330,9 @@ class MessageNavigation(AbstractMessage, ABC):
|
|
327
330
|
logger.debug(f"Send command--Send tool command id={param_id},values={values}")
|
328
331
|
return self.send_order_msg_nav(build)
|
329
332
|
|
330
|
-
def end_draw_border(self, type: int) -> bytes:
|
333
|
+
def end_draw_border(self, type: int) -> bytes | None:
|
331
334
|
if type == -1:
|
332
|
-
return
|
335
|
+
return None
|
333
336
|
build = MctlNav(todev_get_commondata=NavGetCommData(pver=1, action=1, type=type))
|
334
337
|
logger.debug(f"Send command--End drawing boundary, obstacle, channel command type={type}")
|
335
338
|
return self.send_order_msg_nav(build)
|
@@ -339,9 +342,9 @@ class MessageNavigation(AbstractMessage, ABC):
|
|
339
342
|
logger.debug("Send command--Cancel current recording (boundary, obstacle)")
|
340
343
|
return self.send_order_msg_nav(build)
|
341
344
|
|
342
|
-
def delete_map_elements(self, type: int, hash_num: int) -> bytes:
|
345
|
+
def delete_map_elements(self, type: int, hash_num: int) -> bytes | None:
|
343
346
|
if type == -1:
|
344
|
-
return
|
347
|
+
return None
|
345
348
|
build = MctlNav(todev_get_commondata=NavGetCommData(pver=1, action=6, type=type, hash=hash_num))
|
346
349
|
logger.debug(f"Send command--Delete boundary or obstacle or channel command type={type},hash={hash}")
|
347
350
|
return self.send_order_msg_nav(build)
|