pymammotion 0.4.17__py3-none-any.whl → 0.4.19__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/aliyun/client.py +231 -0
- pymammotion/aliyun/cloud_gateway.py +32 -20
- pymammotion/aliyun/model/session_by_authcode_response.py +1 -0
- pymammotion/aliyun/tea/core.py +295 -0
- pymammotion/data/model/hash_list.py +46 -9
- pymammotion/http/http.py +9 -2
- pymammotion/mammotion/commands/messages/navigation.py +1 -1
- pymammotion/mammotion/devices/base.py +37 -13
- pymammotion/mammotion/devices/mammotion.py +21 -11
- pymammotion/mammotion/devices/mammotion_bluetooth.py +1 -2
- pymammotion/mammotion/devices/mammotion_cloud.py +9 -33
- pymammotion/mqtt/mammotion_mqtt.py +7 -6
- pymammotion/proto/mctrl_nav_pb2.py +1 -1
- {pymammotion-0.4.17.dist-info → pymammotion-0.4.19.dist-info}/METADATA +1 -1
- {pymammotion-0.4.17.dist-info → pymammotion-0.4.19.dist-info}/RECORD +17 -15
- {pymammotion-0.4.17.dist-info → pymammotion-0.4.19.dist-info}/LICENSE +0 -0
- {pymammotion-0.4.17.dist-info → pymammotion-0.4.19.dist-info}/WHEEL +0 -0
@@ -0,0 +1,295 @@
|
|
1
|
+
import asyncio
|
2
|
+
import logging
|
3
|
+
import os
|
4
|
+
import ssl
|
5
|
+
import time
|
6
|
+
from typing import Any
|
7
|
+
from urllib.parse import urlencode, urlparse
|
8
|
+
|
9
|
+
import aiohttp
|
10
|
+
import certifi
|
11
|
+
from requests import PreparedRequest, adapters, status_codes
|
12
|
+
from Tea.exceptions import RequiredArgumentException, RetryError
|
13
|
+
from Tea.model import TeaModel
|
14
|
+
from Tea.request import TeaRequest
|
15
|
+
from Tea.response import TeaResponse
|
16
|
+
from Tea.stream import BaseStream
|
17
|
+
|
18
|
+
DEFAULT_CONNECT_TIMEOUT = 5000
|
19
|
+
DEFAULT_READ_TIMEOUT = 10000
|
20
|
+
DEFAULT_POOL_SIZE = 10
|
21
|
+
|
22
|
+
logger = logging.getLogger("alibabacloud-tea")
|
23
|
+
logger.setLevel(logging.DEBUG)
|
24
|
+
ch = logging.StreamHandler()
|
25
|
+
logger.addHandler(ch)
|
26
|
+
|
27
|
+
|
28
|
+
class TeaCore:
|
29
|
+
http_adapter = adapters.HTTPAdapter(pool_connections=DEFAULT_POOL_SIZE, pool_maxsize=DEFAULT_POOL_SIZE * 4)
|
30
|
+
https_adapter = adapters.HTTPAdapter(pool_connections=DEFAULT_POOL_SIZE, pool_maxsize=DEFAULT_POOL_SIZE * 4)
|
31
|
+
|
32
|
+
@staticmethod
|
33
|
+
def get_adapter(prefix):
|
34
|
+
if prefix.upper() == "HTTP":
|
35
|
+
return TeaCore.http_adapter
|
36
|
+
else:
|
37
|
+
return TeaCore.https_adapter
|
38
|
+
|
39
|
+
@staticmethod
|
40
|
+
def _prepare_http_debug(request, symbol):
|
41
|
+
base = ""
|
42
|
+
for key, value in request.headers.items():
|
43
|
+
base += f"\n{symbol} {key} : {value}"
|
44
|
+
return base
|
45
|
+
|
46
|
+
@staticmethod
|
47
|
+
def _do_http_debug(request, response) -> None:
|
48
|
+
# logger the request
|
49
|
+
url = urlparse(request.url)
|
50
|
+
request_base = f"\n> {request.method.upper()} {url.path + url.query} HTTP/1.1"
|
51
|
+
logger.debug(request_base + TeaCore._prepare_http_debug(request, ">"))
|
52
|
+
|
53
|
+
# logger the response
|
54
|
+
response_base = (
|
55
|
+
f"\n< HTTP/1.1 {response.status_code}" f" {status_codes._codes.get(response.status_code)[0].upper()}"
|
56
|
+
)
|
57
|
+
logger.debug(response_base + TeaCore._prepare_http_debug(response, "<"))
|
58
|
+
|
59
|
+
@staticmethod
|
60
|
+
def compose_url(request):
|
61
|
+
host = request.headers.get("host")
|
62
|
+
if not host:
|
63
|
+
raise RequiredArgumentException("endpoint")
|
64
|
+
else:
|
65
|
+
host = host.rstrip("/")
|
66
|
+
protocol = f"{request.protocol.lower()}://"
|
67
|
+
pathname = request.pathname
|
68
|
+
|
69
|
+
if host.startswith(("http://", "https://")):
|
70
|
+
protocol = ""
|
71
|
+
|
72
|
+
if request.port == 80:
|
73
|
+
port = ""
|
74
|
+
else:
|
75
|
+
port = f":{request.port}"
|
76
|
+
|
77
|
+
url = protocol + host + port + pathname
|
78
|
+
|
79
|
+
if request.query:
|
80
|
+
if "?" in url:
|
81
|
+
if not url.endswith("&"):
|
82
|
+
url += "&"
|
83
|
+
else:
|
84
|
+
url += "?"
|
85
|
+
|
86
|
+
encode_query = {}
|
87
|
+
for key in request.query:
|
88
|
+
value = request.query[key]
|
89
|
+
if value is not None:
|
90
|
+
encode_query[key] = str(value)
|
91
|
+
url += urlencode(encode_query)
|
92
|
+
return url.rstrip("?&")
|
93
|
+
|
94
|
+
@staticmethod
|
95
|
+
async def async_do_action(request: TeaRequest, runtime_option=None) -> TeaResponse:
|
96
|
+
runtime_option = runtime_option or {}
|
97
|
+
|
98
|
+
url = TeaCore.compose_url(request)
|
99
|
+
verify = not runtime_option.get("ignoreSSL", False)
|
100
|
+
|
101
|
+
timeout = runtime_option.get("timeout")
|
102
|
+
connect_timeout = runtime_option.get("connectTimeout") or timeout or DEFAULT_CONNECT_TIMEOUT
|
103
|
+
read_timeout = runtime_option.get("readTimeout") or timeout or DEFAULT_READ_TIMEOUT
|
104
|
+
|
105
|
+
connect_timeout, read_timeout = (int(connect_timeout) / 1000, int(read_timeout) / 1000)
|
106
|
+
|
107
|
+
proxy = None
|
108
|
+
if request.protocol.upper() == "HTTP":
|
109
|
+
proxy = runtime_option.get("httpProxy")
|
110
|
+
if not proxy:
|
111
|
+
proxy = os.environ.get("HTTP_PROXY") or os.environ.get("http_proxy")
|
112
|
+
elif request.protocol.upper() == "HTTPS":
|
113
|
+
proxy = runtime_option.get("httpsProxy")
|
114
|
+
if not proxy:
|
115
|
+
proxy = os.environ.get("HTTPS_PROXY") or os.environ.get("https_proxy")
|
116
|
+
|
117
|
+
connector = None
|
118
|
+
ca_cert = certifi.where()
|
119
|
+
if ca_cert and request.protocol.upper() == "HTTPS":
|
120
|
+
loop = asyncio.get_event_loop()
|
121
|
+
|
122
|
+
ssl_context = await loop.run_in_executor(None, ssl.create_default_context, ssl.Purpose.SERVER_AUTH)
|
123
|
+
await loop.run_in_executor(None, ssl_context.load_verify_locations, ca_cert)
|
124
|
+
connector = aiohttp.TCPConnector(
|
125
|
+
ssl=ssl_context,
|
126
|
+
)
|
127
|
+
else:
|
128
|
+
verify = False
|
129
|
+
|
130
|
+
timeout = aiohttp.ClientTimeout(sock_read=read_timeout, sock_connect=connect_timeout)
|
131
|
+
async with aiohttp.ClientSession(connector=connector) as s:
|
132
|
+
body = b""
|
133
|
+
if isinstance(request.body, BaseStream):
|
134
|
+
for content in request.body:
|
135
|
+
body += content
|
136
|
+
elif isinstance(request.body, str):
|
137
|
+
body = request.body.encode("utf-8")
|
138
|
+
else:
|
139
|
+
body = request.body
|
140
|
+
try:
|
141
|
+
async with s.request(
|
142
|
+
request.method, url, data=body, headers=request.headers, ssl=verify, proxy=proxy, timeout=timeout
|
143
|
+
) as response:
|
144
|
+
tea_resp = TeaResponse()
|
145
|
+
tea_resp.body = await response.read()
|
146
|
+
tea_resp.headers = {k.lower(): v for k, v in response.headers.items()}
|
147
|
+
tea_resp.status_code = response.status
|
148
|
+
tea_resp.status_message = response.reason
|
149
|
+
tea_resp.response = response
|
150
|
+
except OSError as e:
|
151
|
+
raise RetryError(str(e))
|
152
|
+
return tea_resp
|
153
|
+
|
154
|
+
@staticmethod
|
155
|
+
def do_action(request: TeaRequest, runtime_option=None) -> TeaResponse:
|
156
|
+
url = TeaCore.compose_url(request)
|
157
|
+
|
158
|
+
runtime_option = runtime_option or {}
|
159
|
+
|
160
|
+
verify = not runtime_option.get("ignoreSSL", False)
|
161
|
+
if verify:
|
162
|
+
verify = runtime_option.get("ca", True) if runtime_option.get("ca", True) is not None else True
|
163
|
+
cert = runtime_option.get("cert", None)
|
164
|
+
|
165
|
+
timeout = runtime_option.get("timeout")
|
166
|
+
connect_timeout = runtime_option.get("connectTimeout") or timeout or DEFAULT_CONNECT_TIMEOUT
|
167
|
+
read_timeout = runtime_option.get("readTimeout") or timeout or DEFAULT_READ_TIMEOUT
|
168
|
+
|
169
|
+
timeout = (int(connect_timeout) / 1000, int(read_timeout) / 1000)
|
170
|
+
|
171
|
+
if isinstance(request.body, str):
|
172
|
+
request.body = request.body.encode("utf-8")
|
173
|
+
|
174
|
+
p = PreparedRequest()
|
175
|
+
p.prepare(
|
176
|
+
method=request.method.upper(),
|
177
|
+
url=url,
|
178
|
+
data=request.body,
|
179
|
+
headers=request.headers,
|
180
|
+
)
|
181
|
+
|
182
|
+
proxies = {}
|
183
|
+
http_proxy = runtime_option.get("httpProxy")
|
184
|
+
https_proxy = runtime_option.get("httpsProxy")
|
185
|
+
no_proxy = runtime_option.get("noProxy")
|
186
|
+
|
187
|
+
if not http_proxy:
|
188
|
+
http_proxy = os.environ.get("HTTP_PROXY") or os.environ.get("http_proxy")
|
189
|
+
if not https_proxy:
|
190
|
+
https_proxy = os.environ.get("HTTPS_PROXY") or os.environ.get("https_proxy")
|
191
|
+
|
192
|
+
if http_proxy:
|
193
|
+
proxies["http"] = http_proxy
|
194
|
+
if https_proxy:
|
195
|
+
proxies["https"] = https_proxy
|
196
|
+
if no_proxy:
|
197
|
+
proxies["no_proxy"] = no_proxy
|
198
|
+
|
199
|
+
adapter = TeaCore.get_adapter(request.protocol)
|
200
|
+
try:
|
201
|
+
resp = adapter.send(
|
202
|
+
p,
|
203
|
+
proxies=proxies,
|
204
|
+
timeout=timeout,
|
205
|
+
verify=verify,
|
206
|
+
cert=cert,
|
207
|
+
)
|
208
|
+
except OSError as e:
|
209
|
+
raise RetryError(str(e))
|
210
|
+
|
211
|
+
debug = runtime_option.get("debug") or os.getenv("DEBUG")
|
212
|
+
if debug and debug.lower() == "sdk":
|
213
|
+
TeaCore._do_http_debug(p, resp)
|
214
|
+
|
215
|
+
response = TeaResponse()
|
216
|
+
response.status_message = resp.reason
|
217
|
+
response.status_code = resp.status_code
|
218
|
+
response.headers = {k.lower(): v for k, v in resp.headers.items()}
|
219
|
+
response.body = resp.content
|
220
|
+
response.response = resp
|
221
|
+
return response
|
222
|
+
|
223
|
+
@staticmethod
|
224
|
+
def get_response_body(resp) -> str:
|
225
|
+
return resp.content.decode("utf-8")
|
226
|
+
|
227
|
+
@staticmethod
|
228
|
+
def allow_retry(dic, retry_times, now=None) -> bool:
|
229
|
+
if retry_times == 0:
|
230
|
+
return True
|
231
|
+
if dic is None or not dic.__contains__("maxAttempts") or dic.get("retryable") is not True and retry_times >= 1:
|
232
|
+
return False
|
233
|
+
else:
|
234
|
+
retry = 0 if dic.get("maxAttempts") is None else int(dic.get("maxAttempts"))
|
235
|
+
return retry >= retry_times
|
236
|
+
|
237
|
+
@staticmethod
|
238
|
+
def get_backoff_time(dic, retry_times) -> int:
|
239
|
+
default_back_off_time = 0
|
240
|
+
if dic is None or not dic.get("policy") or dic.get("policy") == "no":
|
241
|
+
return default_back_off_time
|
242
|
+
|
243
|
+
back_off_time = dic.get("period", default_back_off_time)
|
244
|
+
if not isinstance(back_off_time, int) and not (isinstance(back_off_time, str) and back_off_time.isdigit()):
|
245
|
+
return default_back_off_time
|
246
|
+
|
247
|
+
back_off_time = int(back_off_time)
|
248
|
+
if back_off_time < 0:
|
249
|
+
return retry_times
|
250
|
+
|
251
|
+
return back_off_time
|
252
|
+
|
253
|
+
@staticmethod
|
254
|
+
async def sleep_async(t) -> None:
|
255
|
+
await asyncio.sleep(t)
|
256
|
+
|
257
|
+
@staticmethod
|
258
|
+
def sleep(t) -> None:
|
259
|
+
time.sleep(t)
|
260
|
+
|
261
|
+
@staticmethod
|
262
|
+
def is_retryable(ex) -> bool:
|
263
|
+
return isinstance(ex, RetryError)
|
264
|
+
|
265
|
+
@staticmethod
|
266
|
+
def bytes_readable(body):
|
267
|
+
return body
|
268
|
+
|
269
|
+
@staticmethod
|
270
|
+
def merge(*dic_list) -> dict:
|
271
|
+
dic_result = {}
|
272
|
+
for item in dic_list:
|
273
|
+
if isinstance(item, dict):
|
274
|
+
dic_result.update(item)
|
275
|
+
elif isinstance(item, TeaModel):
|
276
|
+
dic_result.update(item.to_map())
|
277
|
+
return dic_result
|
278
|
+
|
279
|
+
@staticmethod
|
280
|
+
def to_map(model: TeaModel | None) -> dict[str, Any]:
|
281
|
+
if isinstance(model, TeaModel):
|
282
|
+
return model.to_map()
|
283
|
+
else:
|
284
|
+
return dict()
|
285
|
+
|
286
|
+
@staticmethod
|
287
|
+
def from_map(model: TeaModel, dic: dict[str, Any]) -> TeaModel:
|
288
|
+
if isinstance(model, TeaModel):
|
289
|
+
try:
|
290
|
+
return model.from_map(dic)
|
291
|
+
except Exception:
|
292
|
+
model._map = dic
|
293
|
+
return model
|
294
|
+
else:
|
295
|
+
return model
|
@@ -12,6 +12,7 @@ class PathType(IntEnum):
|
|
12
12
|
AREA = 0
|
13
13
|
OBSTACLE = 1
|
14
14
|
PATH = 2
|
15
|
+
LINE = 10
|
15
16
|
DUMP = 12
|
16
17
|
SVG = 13
|
17
18
|
|
@@ -83,6 +84,32 @@ class FrameList(DataClassORJSONMixin):
|
|
83
84
|
data: list[NavGetCommData | SvgMessage] = field(default_factory=list)
|
84
85
|
|
85
86
|
|
87
|
+
@dataclass
|
88
|
+
class Plan(DataClassORJSONMixin):
|
89
|
+
sub_cmd: int = 2
|
90
|
+
version: str = ""
|
91
|
+
user_id: str = ""
|
92
|
+
device_id: str = ""
|
93
|
+
plan_id: str = ""
|
94
|
+
task_id: str = ""
|
95
|
+
start_time: str = "00:00"
|
96
|
+
knife_height: int = 0
|
97
|
+
model: int = 0
|
98
|
+
edge_mode: int = 0
|
99
|
+
route_model: int = 0
|
100
|
+
route_spacing: int = 0
|
101
|
+
ultrasonic_barrier: int = 0
|
102
|
+
total_plan_num: int = 0
|
103
|
+
speed: float = 0.0
|
104
|
+
task_name: str = ""
|
105
|
+
zone_hashs: list[str] = field(default_factory=list)
|
106
|
+
reserved: str = ""
|
107
|
+
start_date: str = ""
|
108
|
+
trigger_type: int = 0
|
109
|
+
remained_seconds: str = "-1"
|
110
|
+
toward_included_angle: int = 0
|
111
|
+
|
112
|
+
|
86
113
|
@dataclass(eq=False, repr=False)
|
87
114
|
class NavGetHashListData(DataClassORJSONMixin):
|
88
115
|
"""Dataclass for NavGetHashListData."""
|
@@ -124,8 +151,10 @@ class HashList(DataClassORJSONMixin):
|
|
124
151
|
area: dict[int, FrameList] = field(default_factory=dict) # type 0
|
125
152
|
path: dict[int, FrameList] = field(default_factory=dict) # type 2
|
126
153
|
obstacle: dict[int, FrameList] = field(default_factory=dict) # type 1
|
127
|
-
dump: dict[int, FrameList] = field(default_factory=dict) # type 12?
|
154
|
+
dump: dict[int, FrameList] = field(default_factory=dict) # type 12? / sub cmd 4
|
128
155
|
svg: dict[int, FrameList] = field(default_factory=dict) # type 13
|
156
|
+
line: dict[int, FrameList] = field(default_factory=dict) # type 10 possibly breakpoint? / sub cmd 3
|
157
|
+
plan: dict[int, Plan] = field(default_factory=dict)
|
129
158
|
area_name: list[AreaHashNameList] = field(default_factory=list)
|
130
159
|
|
131
160
|
def update_hash_lists(self, hashlist: list[int]) -> None:
|
@@ -152,6 +181,8 @@ class HashList(DataClassORJSONMixin):
|
|
152
181
|
all_hash_ids = set(self.area.keys()).union(
|
153
182
|
self.path.keys(), self.obstacle.keys(), self.dump.keys(), self.svg.keys()
|
154
183
|
)
|
184
|
+
if sub_cmd == 3:
|
185
|
+
all_hash_ids = set(self.line.keys())
|
155
186
|
return [
|
156
187
|
i
|
157
188
|
for root_list in self.root_hash_lists
|
@@ -174,7 +205,7 @@ class HashList(DataClassORJSONMixin):
|
|
174
205
|
if target_root_list is None:
|
175
206
|
return []
|
176
207
|
|
177
|
-
return self.
|
208
|
+
return self.find_missing_frames(target_root_list)
|
178
209
|
|
179
210
|
def update_root_hash_list(self, hash_list: NavGetHashListData) -> None:
|
180
211
|
target_root_list = next(
|
@@ -206,26 +237,29 @@ class HashList(DataClassORJSONMixin):
|
|
206
237
|
missing_frames = []
|
207
238
|
filtered_lists = [rl for rl in self.root_hash_lists if rl.sub_cmd == hash_ack.sub_cmd]
|
208
239
|
for root_list in filtered_lists:
|
209
|
-
missing = self.
|
240
|
+
missing = self.find_missing_frames(root_list)
|
210
241
|
if missing:
|
211
242
|
missing_frames.extend(missing)
|
212
243
|
return missing_frames
|
213
244
|
|
214
245
|
def missing_frame(self, hash_data: NavGetCommDataAck | SvgMessageAckT) -> list[int]:
|
215
246
|
if hash_data.type == PathType.AREA:
|
216
|
-
return self.
|
247
|
+
return self.find_missing_frames(self.area.get(hash_data.hash))
|
217
248
|
|
218
249
|
if hash_data.type == PathType.OBSTACLE:
|
219
|
-
return self.
|
250
|
+
return self.find_missing_frames(self.obstacle.get(hash_data.hash))
|
220
251
|
|
221
252
|
if hash_data.type == PathType.PATH:
|
222
|
-
return self.
|
253
|
+
return self.find_missing_frames(self.path.get(hash_data.hash))
|
254
|
+
|
255
|
+
if hash_data.type == PathType.LINE:
|
256
|
+
return self.find_missing_frames(self.line.get(hash_data.hash))
|
223
257
|
|
224
258
|
if hash_data.type == PathType.DUMP:
|
225
|
-
return self.
|
259
|
+
return self.find_missing_frames(self.dump.get(hash_data.hash))
|
226
260
|
|
227
261
|
if hash_data.type == PathType.SVG:
|
228
|
-
return self.
|
262
|
+
return self.find_missing_frames(self.svg.get(hash_data.data_hash))
|
229
263
|
|
230
264
|
return []
|
231
265
|
|
@@ -247,6 +281,9 @@ class HashList(DataClassORJSONMixin):
|
|
247
281
|
if hash_data.type == PathType.PATH:
|
248
282
|
return self._add_hash_data(self.path, hash_data)
|
249
283
|
|
284
|
+
if hash_data.type == PathType.LINE:
|
285
|
+
return self._add_hash_data(self.line, hash_data)
|
286
|
+
|
250
287
|
if hash_data.type == PathType.DUMP:
|
251
288
|
return self._add_hash_data(self.dump, hash_data)
|
252
289
|
|
@@ -256,7 +293,7 @@ class HashList(DataClassORJSONMixin):
|
|
256
293
|
return False
|
257
294
|
|
258
295
|
@staticmethod
|
259
|
-
def
|
296
|
+
def find_missing_frames(frame_list: FrameList | RootHashList) -> list[int]:
|
260
297
|
if frame_list is None:
|
261
298
|
return []
|
262
299
|
|
pymammotion/http/http.py
CHANGED
@@ -13,6 +13,8 @@ class MammotionHTTP:
|
|
13
13
|
def __init__(self) -> None:
|
14
14
|
self.code = None
|
15
15
|
self.msg = None
|
16
|
+
self.account = None
|
17
|
+
self._password = None
|
16
18
|
self.response: Response | None = None
|
17
19
|
self.login_info: LoginResponseData | None = None
|
18
20
|
self._headers = {"User-Agent": "okhttp/3.14.9", "App-Version": "google Pixel 2 XL taimen-Android 11,1.11.332"}
|
@@ -106,7 +108,12 @@ class MammotionHTTP:
|
|
106
108
|
# Assuming the data format matches the expected structure
|
107
109
|
return Response[StreamSubscriptionResponse].from_dict(data)
|
108
110
|
|
109
|
-
async def
|
111
|
+
async def refresh_login(self) -> Response[LoginResponseData]:
|
112
|
+
return await self.login(self.account, self._password)
|
113
|
+
|
114
|
+
async def login(self, account: str, password: str) -> Response[LoginResponseData]:
|
115
|
+
self.account = account
|
116
|
+
self._password = password
|
110
117
|
async with ClientSession(MAMMOTION_DOMAIN) as session:
|
111
118
|
async with session.post(
|
112
119
|
"/oauth/token",
|
@@ -118,7 +125,7 @@ class MammotionHTTP:
|
|
118
125
|
"Ec-Version": "v1",
|
119
126
|
},
|
120
127
|
params=dict(
|
121
|
-
username=self.encryption_utils.encryption_by_aes(
|
128
|
+
username=self.encryption_utils.encryption_by_aes(account),
|
122
129
|
password=self.encryption_utils.encryption_by_aes(password),
|
123
130
|
client_id=self.encryption_utils.encryption_by_aes(MAMMOTION_CLIENT_ID),
|
124
131
|
client_secret=self.encryption_utils.encryption_by_aes(MAMMOTION_CLIENT_SECRET),
|
@@ -297,7 +297,7 @@ class MessageNavigation(AbstractMessage, ABC):
|
|
297
297
|
|
298
298
|
def synchronize_hash_data(self, hash_num: int) -> bytes:
|
299
299
|
build = MctlNav(todev_get_commondata=NavGetCommData(pver=1, action=8, hash=hash_num, sub_cmd=1))
|
300
|
-
logger.debug(f"Send command--209,hash synchronize area data hash:{
|
300
|
+
logger.debug(f"Send command--209,hash synchronize area data hash:{hash_num}")
|
301
301
|
return self.send_order_msg_nav(build)
|
302
302
|
|
303
303
|
def get_area_to_be_transferred(self) -> bytes:
|
@@ -44,19 +44,19 @@ class MammotionBaseDevice:
|
|
44
44
|
self._cloud_device = cloud_device
|
45
45
|
|
46
46
|
async def datahash_response(self, hash_ack: NavGetHashListAck) -> None:
|
47
|
-
"""Handle datahash responses."""
|
47
|
+
"""Handle datahash responses for root level hashs."""
|
48
|
+
current_frame = hash_ack.current_frame
|
48
49
|
|
49
50
|
missing_frames = self.mower.map.missing_root_hash_frame(hash_ack)
|
50
51
|
if len(missing_frames) == 0:
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
for data_hash in self.mower.map.missing_hashlist(hash_ack.sub_cmd):
|
55
|
-
await self.queue_command("synchronize_hash_data", hash_num=data_hash)
|
52
|
+
if len(self.mower.map.missing_hashlist(0)) > 0:
|
53
|
+
data_hash = self.mower.map.missing_hashlist(hash_ack.sub_cmd).pop()
|
54
|
+
await self.queue_command("synchronize_hash_data", hash_num=data_hash)
|
56
55
|
return
|
57
56
|
|
58
|
-
|
59
|
-
|
57
|
+
if current_frame != missing_frames[0] - 1:
|
58
|
+
current_frame = missing_frames[0] - 1
|
59
|
+
await self.queue_command("get_hash_response", total_frame=hash_ack.total_frame, current_frame=current_frame)
|
60
60
|
|
61
61
|
async def commdata_response(self, common_data: NavGetCommDataAck | SvgMessageAckT) -> None:
|
62
62
|
"""Handle common data responses."""
|
@@ -211,25 +211,49 @@ class MammotionBaseDevice:
|
|
211
211
|
|
212
212
|
self.mower.map.update_hash_lists(self.mower.map.hashlist)
|
213
213
|
|
214
|
+
await self.queue_command("send_todev_ble_sync", sync_type=3)
|
215
|
+
|
214
216
|
if self._cloud_device and len(self.mower.map.area_name) == 0 and not DeviceType.is_luba1(self.mower.name):
|
215
217
|
await self.queue_command("get_area_name_list", device_id=self._cloud_device.iotId)
|
216
218
|
|
217
219
|
await self.queue_command("read_plan", sub_cmd=2, plan_index=0)
|
218
220
|
|
219
|
-
|
221
|
+
if len(self.mower.map.root_hash_lists) == 0:
|
222
|
+
await self.queue_command("get_all_boundary_hash_list", sub_cmd=0)
|
223
|
+
|
224
|
+
for hash, frame in self.mower.map.area.items():
|
225
|
+
missing_frames = self.mower.map.find_missing_frames(frame)
|
226
|
+
if len(missing_frames) > 0:
|
227
|
+
del self.mower.map.area[hash]
|
228
|
+
|
229
|
+
for hash, frame in self.mower.map.path.items():
|
230
|
+
missing_frames = self.mower.map.find_missing_frames(frame)
|
231
|
+
if len(missing_frames) > 0:
|
232
|
+
del self.mower.map.path[hash]
|
233
|
+
|
234
|
+
for hash, frame in self.mower.map.obstacle.items():
|
235
|
+
missing_frames = self.mower.map.find_missing_frames(frame)
|
236
|
+
if len(missing_frames) > 0:
|
237
|
+
del self.mower.map.obstacle[hash]
|
238
|
+
|
239
|
+
# for hash, frame in self.mower.map.svg.items():
|
240
|
+
# missing_frames = self.mower.map.find_missing_frames(frame)
|
241
|
+
# if len(missing_frames) > 0:
|
242
|
+
# del self.mower.map.svg[hash]
|
243
|
+
|
220
244
|
if len(self.mower.map.missing_hashlist()) > 0:
|
221
245
|
data_hash = self.mower.map.missing_hashlist().pop()
|
222
246
|
await self.queue_command("synchronize_hash_data", hash_num=data_hash)
|
223
247
|
|
224
|
-
if len(self.mower.map.missing_hashlist(3)) > 0:
|
225
|
-
|
226
|
-
|
248
|
+
# if len(self.mower.map.missing_hashlist(3)) > 0:
|
249
|
+
# data_hash = self.mower.map.missing_hashlist(3).pop()
|
250
|
+
# await self.queue_command("synchronize_hash_data", hash_num=data_hash)
|
227
251
|
|
228
252
|
# sub_cmd 3 is job hashes??
|
229
253
|
# sub_cmd 4 is dump location (yuka)
|
230
254
|
# jobs list
|
231
255
|
#
|
232
|
-
#
|
256
|
+
# await self.queue_command("get_all_boundary_hash_list", sub_cmd=3)
|
233
257
|
|
234
258
|
async def async_read_settings(self) -> None:
|
235
259
|
"""Read settings from device."""
|
@@ -187,6 +187,18 @@ class Mammotion:
|
|
187
187
|
cloud_client = await self.login(account, password)
|
188
188
|
await self.initiate_cloud_connection(account, cloud_client)
|
189
189
|
|
190
|
+
async def refresh_login(self, account: str) -> None:
|
191
|
+
async with self._login_lock:
|
192
|
+
exists: MammotionCloud | None = self.mqtt_list.get(account)
|
193
|
+
if not exists:
|
194
|
+
return
|
195
|
+
mammotion_http = exists.cloud_client.mammotion_http
|
196
|
+
await mammotion_http.refresh_login()
|
197
|
+
await self.connect_iot(mammotion_http, exists.cloud_client)
|
198
|
+
if not exists.is_connected():
|
199
|
+
loop = asyncio.get_running_loop()
|
200
|
+
await loop.run_in_executor(None, exists.connect_async)
|
201
|
+
|
190
202
|
async def initiate_cloud_connection(self, account: str, cloud_client: CloudIOTGateway) -> None:
|
191
203
|
loop = asyncio.get_running_loop()
|
192
204
|
if mqtt := self.mqtt_list.get(account):
|
@@ -242,21 +254,19 @@ class Mammotion:
|
|
242
254
|
cloud_client = CloudIOTGateway()
|
243
255
|
mammotion_http = MammotionHTTP()
|
244
256
|
await mammotion_http.login(account, password)
|
257
|
+
await self.connect_iot(mammotion_http, cloud_client)
|
258
|
+
await cloud_client.list_binding_by_account()
|
259
|
+
return cloud_client
|
260
|
+
|
261
|
+
@staticmethod
|
262
|
+
async def connect_iot(mammotion_http: MammotionHTTP, cloud_client: CloudIOTGateway) -> None:
|
245
263
|
country_code = mammotion_http.login_info.userInformation.domainAbbreviation
|
246
|
-
_LOGGER.debug("CountryCode: " + country_code)
|
247
|
-
_LOGGER.debug("AuthCode: " + mammotion_http.login_info.authorization_code)
|
248
264
|
cloud_client.set_http(mammotion_http)
|
249
|
-
|
250
|
-
await loop.run_in_executor(
|
251
|
-
None, cloud_client.get_region, country_code, mammotion_http.login_info.authorization_code
|
252
|
-
)
|
265
|
+
await cloud_client.get_region(country_code, mammotion_http.login_info.authorization_code)
|
253
266
|
await cloud_client.connect()
|
254
267
|
await cloud_client.login_by_oauth(country_code, mammotion_http.login_info.authorization_code)
|
255
|
-
await
|
256
|
-
await
|
257
|
-
|
258
|
-
await loop.run_in_executor(None, cloud_client.list_binding_by_account)
|
259
|
-
return cloud_client
|
268
|
+
await cloud_client.aep_handle()
|
269
|
+
await cloud_client.session_by_auth_code()
|
260
270
|
|
261
271
|
async def remove_device(self, name: str) -> None:
|
262
272
|
await self.device_manager.remove_device(name)
|
@@ -106,7 +106,6 @@ class MammotionBaseBLEDevice(MammotionBaseDevice):
|
|
106
106
|
|
107
107
|
async def _ble_sync(self) -> None:
|
108
108
|
if self._client is not None and self._client.is_connected:
|
109
|
-
_LOGGER.debug("BLE SYNC")
|
110
109
|
command_bytes = self._commands.send_todev_ble_sync(2)
|
111
110
|
await self._message.post_custom_data_bytes(command_bytes)
|
112
111
|
|
@@ -369,7 +368,7 @@ class MammotionBaseBLEDevice(MammotionBaseDevice):
|
|
369
368
|
_LOGGER.debug("%s: Sending command: %s", self.name, key)
|
370
369
|
await self._message.post_custom_data_bytes(command)
|
371
370
|
|
372
|
-
timeout =
|
371
|
+
timeout = 1
|
373
372
|
timeout_handle = self.loop.call_at(self.loop.time() + timeout, _handle_timeout, self._notify_future)
|
374
373
|
timeout_expired = False
|
375
374
|
try:
|