pymammotion 0.2.27__py3-none-any.whl → 0.2.29__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 +32 -16
- pymammotion/aliyun/dataclass/dev_by_account_response.py +0 -1
- pymammotion/aliyun/dataclass/session_by_authcode_response.py +1 -0
- pymammotion/bluetooth/ble.py +1 -1
- pymammotion/data/model/account.py +1 -1
- pymammotion/data/model/device.py +10 -5
- pymammotion/data/model/device_config.py +71 -0
- pymammotion/data/model/generate_route_information.py +18 -124
- pymammotion/data/model/hash_list.py +3 -6
- pymammotion/data/model/location.py +1 -1
- pymammotion/data/model/mowing_modes.py +8 -0
- pymammotion/data/mqtt/event.py +7 -2
- pymammotion/data/state_manager.py +4 -3
- pymammotion/http/http.py +10 -0
- pymammotion/mammotion/control/joystick.py +1 -0
- pymammotion/mammotion/devices/mammotion.py +63 -71
- pymammotion/mqtt/mammotion_future.py +1 -0
- pymammotion/mqtt/mammotion_mqtt.py +6 -9
- pymammotion/utility/conversions.py +1 -1
- pymammotion/utility/device_type.py +36 -7
- pymammotion/utility/movement.py +2 -1
- {pymammotion-0.2.27.dist-info → pymammotion-0.2.29.dist-info}/METADATA +1 -1
- {pymammotion-0.2.27.dist-info → pymammotion-0.2.29.dist-info}/RECORD +26 -26
- {pymammotion-0.2.27.dist-info → pymammotion-0.2.29.dist-info}/LICENSE +0 -0
- {pymammotion-0.2.27.dist-info → pymammotion-0.2.29.dist-info}/WHEEL +0 -0
pymammotion/__init__.py
CHANGED
@@ -9,14 +9,14 @@ import logging
|
|
9
9
|
import os
|
10
10
|
|
11
11
|
# works outside HA on its own
|
12
|
-
from pymammotion.bluetooth.ble import
|
12
|
+
from pymammotion.bluetooth.ble import MammotionBLE
|
13
13
|
from pymammotion.http.http import MammotionHTTP, connect_http
|
14
14
|
|
15
15
|
# TODO make a working device that will work outside HA too.
|
16
16
|
from pymammotion.mammotion.devices import MammotionBaseBLEDevice
|
17
17
|
from pymammotion.mqtt import MammotionMQTT
|
18
18
|
|
19
|
-
__all__ = ["
|
19
|
+
__all__ = ["MammotionBLE", "MammotionHTTP", "connect_http", "MammotionBaseBLEDevice", "MammotionMQTT"]
|
20
20
|
|
21
21
|
logger = logging.getLogger(__name__)
|
22
22
|
|
@@ -49,15 +49,19 @@ MOVE_HEADERS = (
|
|
49
49
|
class SetupException(Exception):
|
50
50
|
pass
|
51
51
|
|
52
|
+
|
52
53
|
class AuthRefreshException(Exception):
|
53
54
|
"""Raise exception when library cannot refresh token."""
|
54
55
|
|
56
|
+
|
55
57
|
class DeviceOfflineException(Exception):
|
56
58
|
"""Raise exception when device is offline."""
|
57
59
|
|
60
|
+
|
58
61
|
class LoginException(Exception):
|
59
62
|
"""Raise exception when library cannot log in."""
|
60
63
|
|
64
|
+
|
61
65
|
class CloudIOTGateway:
|
62
66
|
"""Class for interacting with Aliyun Cloud IoT Gateway."""
|
63
67
|
|
@@ -72,11 +76,19 @@ class CloudIOTGateway:
|
|
72
76
|
_devices_by_account_response: ListingDevByAccountResponse | None = None
|
73
77
|
_region_response = None
|
74
78
|
|
75
|
-
_iot_token_issued_at
|
79
|
+
_iot_token_issued_at: int = None
|
76
80
|
|
77
81
|
converter = DatatypeConverter()
|
78
82
|
|
79
|
-
def __init__(
|
83
|
+
def __init__(
|
84
|
+
self,
|
85
|
+
connect_response: ConnectResponse | None = None,
|
86
|
+
login_by_oauth_response: LoginByOAuthResponse | None = None,
|
87
|
+
aep_response: AepResponse | None = None,
|
88
|
+
session_by_authcode_response: SessionByAuthCodeResponse | None = None,
|
89
|
+
region_response: RegionResponse | None = None,
|
90
|
+
dev_by_account: ListingDevByAccountResponse | None = None,
|
91
|
+
):
|
80
92
|
"""Initialize the CloudIOTGateway."""
|
81
93
|
self.mammotion_http: MammotionHTTP | None = None
|
82
94
|
self._app_key = APP_KEY
|
@@ -376,11 +388,8 @@ class CloudIOTGateway:
|
|
376
388
|
async with session.post(
|
377
389
|
f"https://{region_url}/api/prd/loginbyoauth.json",
|
378
390
|
headers=headers,
|
379
|
-
data={
|
380
|
-
'loginByOauthRequest': json.dumps(_bodyParam, separators=(",", ":"))
|
381
|
-
}
|
391
|
+
data={"loginByOauthRequest": json.dumps(_bodyParam, separators=(",", ":"))},
|
382
392
|
) as resp:
|
383
|
-
|
384
393
|
data = await resp.json()
|
385
394
|
logger.debug(data)
|
386
395
|
if resp.status == 200:
|
@@ -441,7 +450,7 @@ class CloudIOTGateway:
|
|
441
450
|
raise Exception("Error in creating session: " + response_body_str)
|
442
451
|
|
443
452
|
self._session_by_authcode_response = session_by_auth
|
444
|
-
self._iot_token_issued_at
|
453
|
+
self._iot_token_issued_at = int(time.time())
|
445
454
|
|
446
455
|
return response.body
|
447
456
|
|
@@ -491,18 +500,23 @@ class CloudIOTGateway:
|
|
491
500
|
response_body_dict = json.loads(response_body_str)
|
492
501
|
|
493
502
|
if int(response_body_dict.get("code")) != 200:
|
494
|
-
|
503
|
+
logger.error(response_body_dict)
|
504
|
+
raise Exception("Error check or refresh token: " + response_body_dict.__str__())
|
495
505
|
|
496
506
|
session = SessionByAuthCodeResponse.from_dict(response_body_dict)
|
497
507
|
session_data = session.data
|
498
508
|
|
499
|
-
if
|
509
|
+
if (
|
510
|
+
session_data.identityId is None
|
511
|
+
or session_data.refreshTokenExpire is None
|
512
|
+
or session_data.iotToken is None
|
513
|
+
or session_data.iotTokenExpire is None
|
514
|
+
or session_data.refreshToken is None
|
515
|
+
):
|
500
516
|
raise Exception("Error check or refresh token: Parameters not correct")
|
501
517
|
|
502
518
|
self._session_by_authcode_response = session
|
503
|
-
self._iot_token_issued_at
|
504
|
-
|
505
|
-
|
519
|
+
self._iot_token_issued_at = int(time.time())
|
506
520
|
|
507
521
|
def list_binding_by_account(self) -> ListingDevByAccountResponse:
|
508
522
|
"""List bindings by account."""
|
@@ -550,14 +564,16 @@ class CloudIOTGateway:
|
|
550
564
|
"""Send a cloud command to the specified IoT device."""
|
551
565
|
|
552
566
|
"""Check if iotToken is expired"""
|
553
|
-
if self._iot_token_issued_at + self._session_by_authcode_response.data.iotTokenExpire <= (
|
567
|
+
if self._iot_token_issued_at + self._session_by_authcode_response.data.iotTokenExpire <= (
|
568
|
+
int(time.time()) + (5 * 3600)
|
569
|
+
):
|
554
570
|
"""Token expired - Try to refresh - Check if refreshToken is not expired"""
|
555
|
-
if self._iot_token_issued_at + self._session_by_authcode_response.data.refreshTokenExpire > (
|
571
|
+
if self._iot_token_issued_at + self._session_by_authcode_response.data.refreshTokenExpire > (
|
572
|
+
int(time.time())
|
573
|
+
):
|
556
574
|
self.check_or_refresh_session()
|
557
575
|
else:
|
558
576
|
raise AuthRefreshException("Refresh token expired. Please re-login")
|
559
|
-
|
560
|
-
|
561
577
|
|
562
578
|
config = Config(
|
563
579
|
app_key=self._app_key,
|
pymammotion/bluetooth/ble.py
CHANGED
pymammotion/data/model/device.py
CHANGED
@@ -16,8 +16,13 @@ from pymammotion.proto.mctrl_driver import MctlDriver
|
|
16
16
|
from pymammotion.proto.mctrl_nav import MctlNav
|
17
17
|
from pymammotion.proto.mctrl_ota import MctlOta
|
18
18
|
from pymammotion.proto.mctrl_pept import MctlPept
|
19
|
-
from pymammotion.proto.mctrl_sys import
|
20
|
-
|
19
|
+
from pymammotion.proto.mctrl_sys import (
|
20
|
+
MctlSys,
|
21
|
+
MowToAppInfoT,
|
22
|
+
ReportInfoData,
|
23
|
+
SystemUpdateBufMsg,
|
24
|
+
SystemRapidStateTunnelMsg,
|
25
|
+
)
|
21
26
|
from pymammotion.utility.constant import WorkMode
|
22
27
|
from pymammotion.utility.conversions import parse_double
|
23
28
|
from pymammotion.utility.map import CoordinateConverter
|
@@ -105,9 +110,9 @@ class MowingDevice:
|
|
105
110
|
parse_double(location.real_pos_y, 4.0), parse_double(location.real_pos_x, 4.0)
|
106
111
|
)
|
107
112
|
if location.zone_hash:
|
108
|
-
self.location.work_zone =
|
109
|
-
|
110
|
-
|
113
|
+
self.location.work_zone = (
|
114
|
+
location.zone_hash if self.report_data.dev.sys_status == WorkMode.MODE_WORKING else 0
|
115
|
+
)
|
111
116
|
|
112
117
|
self.report_data = self.report_data.from_dict(toapp_report_data.to_dict(casing=betterproto.Casing.SNAKE))
|
113
118
|
|
@@ -1,5 +1,7 @@
|
|
1
1
|
from dataclasses import dataclass
|
2
2
|
|
3
|
+
from pymammotion.utility.device_type import DeviceType
|
4
|
+
|
3
5
|
|
4
6
|
@dataclass
|
5
7
|
class DeviceLimits:
|
@@ -7,3 +9,72 @@ class DeviceLimits:
|
|
7
9
|
blade_height_max: int
|
8
10
|
working_speed_min: float
|
9
11
|
working_speed_max: float
|
12
|
+
|
13
|
+
|
14
|
+
@dataclass
|
15
|
+
class OperationSettings:
|
16
|
+
"""Operation settings for a device."""
|
17
|
+
|
18
|
+
is_mow: bool = True
|
19
|
+
is_dump: bool = True
|
20
|
+
is_edge: bool = False
|
21
|
+
collect_grass_frequency: int = 10
|
22
|
+
job_mode: int = 0 # taskMode
|
23
|
+
job_version: int = 0
|
24
|
+
job_id: int = 0
|
25
|
+
speed: float = 0.3
|
26
|
+
ultra_wave: int = 2 # touch no touch etc
|
27
|
+
channel_mode: int = 0 # line mode is grid single double or single2
|
28
|
+
channel_width: int = 25
|
29
|
+
rain_tactics: int = 0
|
30
|
+
blade_height: int = 0
|
31
|
+
path_order: str = ""
|
32
|
+
toward: int = 0 # is just angle
|
33
|
+
toward_included_angle: int = 0
|
34
|
+
toward_mode: int = 0 # angle type relative etc
|
35
|
+
border_mode: int = 1 # border laps
|
36
|
+
obstacle_laps: int = 1
|
37
|
+
start_progress: int = 0
|
38
|
+
|
39
|
+
|
40
|
+
def create_path_order(operation_mode: OperationSettings, device_name: str) -> str:
|
41
|
+
i = 8
|
42
|
+
bArr = bytearray(8)
|
43
|
+
bArr[0] = operation_mode.border_mode
|
44
|
+
bArr[1] = operation_mode.obstacle_laps
|
45
|
+
bArr[3] = operation_mode.start_progress
|
46
|
+
bArr[2] = 0
|
47
|
+
|
48
|
+
if not DeviceType.is_luba1(device_name):
|
49
|
+
bArr[4] = 0
|
50
|
+
if DeviceType.is_yuka(device_name):
|
51
|
+
i = calculate_yuka_mode(operation_mode)
|
52
|
+
elif not DeviceType.is_luba_2(device_name):
|
53
|
+
i = 0
|
54
|
+
bArr[5] = i
|
55
|
+
if operation_mode.is_dump:
|
56
|
+
b = operation_mode.collect_grass_frequency
|
57
|
+
else:
|
58
|
+
b = 10
|
59
|
+
bArr[6] = b
|
60
|
+
if DeviceType.is_luba1(device_name):
|
61
|
+
bArr[4] = operation_mode.toward_mode
|
62
|
+
return str(bArr, "UTF-8")
|
63
|
+
|
64
|
+
|
65
|
+
def calculate_yuka_mode(operation_mode: OperationSettings):
|
66
|
+
if operation_mode.is_mow and operation_mode.is_dump and operation_mode.is_edge:
|
67
|
+
return 14
|
68
|
+
if operation_mode.is_mow and operation_mode.is_dump and not operation_mode.is_edge:
|
69
|
+
return 12
|
70
|
+
if operation_mode.is_mow and not operation_mode.is_dump and operation_mode.is_edge:
|
71
|
+
return 10
|
72
|
+
if operation_mode.is_mow and not operation_mode.is_dump and not operation_mode.is_edge:
|
73
|
+
return 8
|
74
|
+
if not operation_mode.is_mow and operation_mode.is_dump and operation_mode.is_edge:
|
75
|
+
return 6
|
76
|
+
if not operation_mode.is_mow and not operation_mode.is_dump and operation_mode.is_edge:
|
77
|
+
return 2
|
78
|
+
if not operation_mode.is_mow and operation_mode.is_dump and not operation_mode.is_edge:
|
79
|
+
return 4
|
80
|
+
return 0
|
@@ -1,133 +1,27 @@
|
|
1
1
|
import logging
|
2
|
+
from dataclasses import dataclass
|
2
3
|
from typing import List
|
3
4
|
|
4
5
|
logger = logging.getLogger(__name__)
|
5
6
|
|
6
7
|
|
8
|
+
@dataclass
|
7
9
|
class GenerateRouteInformation:
|
8
10
|
"""Creates a model for generating route information and mowing plan before starting a job."""
|
9
11
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
self.one_hashs = one_hashs
|
27
|
-
self.rain_tactics = rain_tactics
|
28
|
-
self.job_mode = job_mode
|
29
|
-
self.knife_height = knife_height
|
30
|
-
self.speed = speed
|
31
|
-
self.ultra_wave = ultra_wave
|
32
|
-
self.channel_width = channel_width
|
33
|
-
self.channel_mode = channel_mode
|
34
|
-
self.toward = toward
|
35
|
-
self.edge_mode = rain_tactics
|
36
|
-
self.path_order = path_order
|
37
|
-
self.toward_included_angle = toward_included_angle
|
38
|
-
logger.debug(
|
39
|
-
f"Mode route command parameters jobMode={job_mode}//channelWidth={channel_width}//speed={speed}//UltraWave={ultra_wave}//channelMode={channel_mode}//edgeMode={rain_tactics}//knifeHeight={knife_height} pathOrder:{path_order.encode('utf-8')}"
|
40
|
-
)
|
41
|
-
|
42
|
-
def get_job_id(self) -> int:
|
43
|
-
return self.job_id
|
44
|
-
|
45
|
-
def set_job_id(self, job_id: int) -> None:
|
46
|
-
self.job_id = job_id
|
47
|
-
|
48
|
-
def get_job_ver(self) -> int:
|
49
|
-
return self.job_ver
|
50
|
-
|
51
|
-
def set_job_ver(self, job_ver: int) -> None:
|
52
|
-
self.job_ver = job_ver
|
53
|
-
|
54
|
-
def get_rain_tactics(self) -> int:
|
55
|
-
return self.rain_tactics
|
56
|
-
|
57
|
-
def set_rain_tactics(self, rain_tactics: int) -> None:
|
58
|
-
self.rain_tactics = rain_tactics
|
59
|
-
|
60
|
-
def get_job_mode(self) -> int:
|
61
|
-
return self.job_mode
|
62
|
-
|
63
|
-
def set_job_mode(self, job_mode: int) -> None:
|
64
|
-
self.job_mode = job_mode
|
65
|
-
|
66
|
-
def get_knife_height(self) -> int:
|
67
|
-
return self.knife_height
|
68
|
-
|
69
|
-
def set_knife_height(self, knife_height: int) -> None:
|
70
|
-
self.knife_height = knife_height
|
71
|
-
|
72
|
-
def get_speed(self) -> float:
|
73
|
-
return self.speed
|
74
|
-
|
75
|
-
def set_speed(self, speed: float) -> None:
|
76
|
-
self.speed = speed
|
77
|
-
|
78
|
-
def get_ultra_wave(self) -> int:
|
79
|
-
return self.ultra_wave
|
80
|
-
|
81
|
-
def set_ultra_wave(self, ultra_wave: int) -> None:
|
82
|
-
self.ultra_wave = ultra_wave
|
83
|
-
|
84
|
-
def get_channel_width(self) -> int:
|
85
|
-
return self.channel_width
|
86
|
-
|
87
|
-
def set_channel_width(self, channel_width: int) -> None:
|
88
|
-
self.channel_width = channel_width
|
89
|
-
|
90
|
-
def get_channel_mode(self) -> int:
|
91
|
-
return self.channel_mode
|
92
|
-
|
93
|
-
def set_channel_mode(self, channel_mode: int) -> None:
|
94
|
-
self.channel_mode = channel_mode
|
95
|
-
|
96
|
-
def get_toward(self) -> int:
|
97
|
-
return self.toward
|
98
|
-
|
99
|
-
def set_toward(self, toward: int) -> None:
|
100
|
-
self.toward = toward
|
101
|
-
|
102
|
-
def get_one_hashs(self) -> List[int]:
|
103
|
-
return self.one_hashs if self.one_hashs else []
|
104
|
-
|
105
|
-
def set_one_hashs(self, one_hashs: List[int]) -> None:
|
106
|
-
self.one_hashs = one_hashs
|
107
|
-
|
108
|
-
def get_path_order(self) -> str:
|
109
|
-
return self.path_order
|
110
|
-
|
111
|
-
def set_path_order(self, path_order: str) -> None:
|
112
|
-
self.path_order = path_order
|
113
|
-
|
114
|
-
def get_toward_included_angle(self) -> int:
|
115
|
-
return self.toward_included_angle
|
116
|
-
|
117
|
-
def set_toward_included_angle(self, toward_included_angle: int) -> None:
|
118
|
-
self.toward_included_angle = toward_included_angle
|
119
|
-
|
120
|
-
def get_toward_mode(self) -> int:
|
121
|
-
return self.toward_mode
|
122
|
-
|
123
|
-
def get_edge_mode(self) -> int:
|
124
|
-
return self.edge_mode
|
125
|
-
|
126
|
-
def set_edge_mode(self, edge_mode: int) -> None:
|
127
|
-
self.edge_mode = edge_mode
|
128
|
-
|
129
|
-
def __str__(self) -> str:
|
130
|
-
try:
|
131
|
-
return f"GenerateRouteInformation{{oneHashs={self.one_hashs}, jobId={self.job_id}, jobVer={self.job_ver}, rainTactics={self.rain_tactics}, jobMode={self.job_mode}, knifeHeight={self.knife_height}, speed={self.speed}, UltraWave={self.ultra_wave}, channelWidth={self.channel_width}, channelMode={self.channel_mode}, toward={self.toward}, pathOrder='{self.path_order.encode('utf-8')}', edgeMode={self.edge_mode}, towardIncludedAngle={self.toward_included_angle}}}"
|
132
|
-
except Exception as e:
|
133
|
-
return str(e)
|
12
|
+
one_hashs: list[int] = list
|
13
|
+
job_mode: int = 0 # taskMode
|
14
|
+
job_version: int = 0
|
15
|
+
job_id: int = 0
|
16
|
+
speed: float = 0.3
|
17
|
+
ultra_wave: int = 2 # touch no touch etc
|
18
|
+
channel_mode: int = 0 # line mode is grid single double or single2
|
19
|
+
channel_width: int = 25
|
20
|
+
rain_tactics: int = 0
|
21
|
+
blade_height: int = 0
|
22
|
+
path_order: str = ""
|
23
|
+
toward: int = 0 # is just angle
|
24
|
+
toward_included_angle: int = 0
|
25
|
+
toward_mode: int = 0 # angle type relative etc
|
26
|
+
edge_mode: int = 1 # border laps
|
27
|
+
obstacle_laps: int = 1
|
@@ -6,10 +6,12 @@ from pymammotion.proto.mctrl_nav import NavGetCommDataAck
|
|
6
6
|
|
7
7
|
class PathType(IntEnum):
|
8
8
|
"""Path types for common data."""
|
9
|
+
|
9
10
|
AREA = 0
|
10
11
|
OBSTACLE = 1
|
11
12
|
PATH = 2
|
12
13
|
|
14
|
+
|
13
15
|
@dataclass
|
14
16
|
class FrameList:
|
15
17
|
total_frame: int
|
@@ -34,7 +36,6 @@ class HashList:
|
|
34
36
|
self.path = {hash_id: frames for hash_id, frames in self.path.items() if hash_id in hashlist}
|
35
37
|
self.obstacle = {hash_id: frames for hash_id, frames in self.obstacle.items() if hash_id in hashlist}
|
36
38
|
|
37
|
-
|
38
39
|
def missing_frame(self, hash_data: NavGetCommDataAck) -> list[int]:
|
39
40
|
if hash_data.type == PathType.AREA:
|
40
41
|
return self._find_missing_frames(self.area.get(hash_data.hash))
|
@@ -45,8 +46,6 @@ class HashList:
|
|
45
46
|
if hash_data.type == PathType.PATH:
|
46
47
|
return self._find_missing_frames(self.path.get(hash_data.hash))
|
47
48
|
|
48
|
-
|
49
|
-
|
50
49
|
def update(self, hash_data: NavGetCommDataAck) -> bool:
|
51
50
|
"""Update the map data."""
|
52
51
|
if hash_data.type == PathType.AREA:
|
@@ -58,18 +57,16 @@ class HashList:
|
|
58
57
|
if hash_data.type == PathType.PATH:
|
59
58
|
return self._add_hash_data(self.path, hash_data)
|
60
59
|
|
61
|
-
|
62
60
|
@staticmethod
|
63
61
|
def _find_missing_frames(frame_list: FrameList) -> list[int]:
|
64
62
|
if frame_list.total_frame == len(frame_list.data):
|
65
63
|
return []
|
66
|
-
number_list = list(range(1, frame_list.total_frame+1))
|
64
|
+
number_list = list(range(1, frame_list.total_frame + 1))
|
67
65
|
|
68
66
|
current_frames = {frame.current_frame for frame in frame_list.data}
|
69
67
|
missing_numbers = [num for num in number_list if num not in current_frames]
|
70
68
|
return missing_numbers
|
71
69
|
|
72
|
-
|
73
70
|
@staticmethod
|
74
71
|
def _add_hash_data(hash_dict: dict, hash_data: NavGetCommDataAck) -> bool:
|
75
72
|
if hash_dict.get(hash_data.hash) is None:
|
@@ -44,3 +44,11 @@ class BypassStrategy(IntEnum):
|
|
44
44
|
slow_touch = 1
|
45
45
|
less_touch = 2
|
46
46
|
no_touch = 3 # luba 2 yuka only or possibly value of 10
|
47
|
+
|
48
|
+
|
49
|
+
class PathAngleSetting(IntEnum):
|
50
|
+
"""Path Angle type."""
|
51
|
+
|
52
|
+
relative_angle = 0
|
53
|
+
absolute_angle = 1
|
54
|
+
random_angle = 2 # Luba Pro / Luba 2 Yuka only
|
pymammotion/data/mqtt/event.py
CHANGED
@@ -45,23 +45,25 @@ class DeviceWarningEventValue(DataClassORJSONMixin):
|
|
45
45
|
# (see resources/res/values-en-rUS/strings.xml in APK)
|
46
46
|
code: int
|
47
47
|
|
48
|
+
|
48
49
|
@dataclass
|
49
50
|
class DeviceConfigurationRequestValue(DataClassORJSONMixin):
|
50
51
|
code: int
|
51
52
|
bizId: str
|
52
53
|
params: str
|
53
54
|
|
55
|
+
|
54
56
|
@dataclass
|
55
57
|
class DeviceNotificationEventCode(DataClassORJSONMixin):
|
56
58
|
localTime: int
|
57
59
|
code: str
|
58
60
|
|
61
|
+
|
59
62
|
@dataclass
|
60
63
|
class DeviceNotificationEventValue(DataClassORJSONMixin):
|
61
64
|
data: DeviceNotificationEventCode
|
62
65
|
|
63
66
|
|
64
|
-
|
65
67
|
@dataclass
|
66
68
|
class GeneralParams(DataClassORJSONMixin):
|
67
69
|
groupIdList: list[str]
|
@@ -100,6 +102,7 @@ class DeviceProtobufMsgEventParams(GeneralParams):
|
|
100
102
|
type: Literal["info"]
|
101
103
|
value: DeviceProtobufMsgEventValue
|
102
104
|
|
105
|
+
|
103
106
|
@dataclass
|
104
107
|
class DeviceNotificationEventParams(GeneralParams):
|
105
108
|
"""Device notification event.
|
@@ -111,12 +114,14 @@ class DeviceNotificationEventParams(GeneralParams):
|
|
111
114
|
type: Literal["info"]
|
112
115
|
value: DeviceNotificationEventValue
|
113
116
|
|
117
|
+
|
114
118
|
@dataclass
|
115
119
|
class DeviceWarningEventParams(GeneralParams):
|
116
120
|
identifier: Literal["device_warning_event"]
|
117
121
|
type: Literal["alert"]
|
118
122
|
value: DeviceWarningEventValue
|
119
123
|
|
124
|
+
|
120
125
|
@dataclass
|
121
126
|
class DeviceConfigurationRequestEvent(GeneralParams):
|
122
127
|
type: Literal["info"]
|
@@ -127,7 +132,7 @@ class DeviceConfigurationRequestEvent(GeneralParams):
|
|
127
132
|
class ThingEventMessage(DataClassORJSONMixin):
|
128
133
|
method: Literal["thing.events", "thing.properties"]
|
129
134
|
id: str
|
130
|
-
params: Union[DeviceProtobufMsgEventParams, DeviceWarningEventParams,
|
135
|
+
params: Union[DeviceProtobufMsgEventParams, DeviceWarningEventParams, dict]
|
131
136
|
version: Literal["1.0"]
|
132
137
|
|
133
138
|
@classmethod
|
@@ -1,4 +1,5 @@
|
|
1
1
|
"""Manage state from notifications into MowingDevice."""
|
2
|
+
|
2
3
|
from typing import Optional, Callable, Awaitable
|
3
4
|
|
4
5
|
import betterproto
|
@@ -15,9 +16,9 @@ class StateManager:
|
|
15
16
|
|
16
17
|
def __init__(self, device: MowingDevice):
|
17
18
|
self._device = device
|
18
|
-
self.gethash_ack_callback: Optional[Callable[[NavGetHashListAck],Awaitable[None]]] = None
|
19
|
-
self.get_commondata_ack_callback: Optional[Callable[[NavGetCommDataAck],Awaitable[None]]] = None
|
20
|
-
self.on_notification_callback: Optional[Callable[[],Awaitable[None]]] = None
|
19
|
+
self.gethash_ack_callback: Optional[Callable[[NavGetHashListAck], Awaitable[None]]] = None
|
20
|
+
self.get_commondata_ack_callback: Optional[Callable[[NavGetCommDataAck], Awaitable[None]]] = None
|
21
|
+
self.on_notification_callback: Optional[Callable[[], Awaitable[None]]] = None
|
21
22
|
|
22
23
|
def get_device(self) -> MowingDevice:
|
23
24
|
"""Get device."""
|
pymammotion/http/http.py
CHANGED
@@ -54,6 +54,16 @@ class MammotionHTTP:
|
|
54
54
|
self.msg = response.msg
|
55
55
|
self.code = response.code
|
56
56
|
|
57
|
+
async def get_all_error_codes(self):
|
58
|
+
async with ClientSession() as session:
|
59
|
+
async with session.post(
|
60
|
+
"code/record/export-data",
|
61
|
+
headers=self._headers,
|
62
|
+
) as resp:
|
63
|
+
if resp.status == 200:
|
64
|
+
data = await resp.json()
|
65
|
+
print(resp)
|
66
|
+
|
57
67
|
@classmethod
|
58
68
|
async def login(cls, session: ClientSession, username: str, password: str) -> Response[LoginResponseData]:
|
59
69
|
async with session.post(
|
@@ -133,13 +133,19 @@ class ConnectionPreference(Enum):
|
|
133
133
|
WIFI = 1
|
134
134
|
BLUETOOTH = 2
|
135
135
|
|
136
|
+
|
136
137
|
class MammotionMixedDeviceManager:
|
137
138
|
_ble_device: MammotionBaseBLEDevice | None = None
|
138
139
|
_cloud_device: MammotionBaseCloudDevice | None = None
|
139
140
|
_mowing_state: MowingDevice = MowingDevice()
|
140
141
|
|
141
|
-
def __init__(
|
142
|
-
|
142
|
+
def __init__(
|
143
|
+
self,
|
144
|
+
name: str,
|
145
|
+
cloud_device: Device | None = None,
|
146
|
+
ble_device: BLEDevice | None = None,
|
147
|
+
mqtt: MammotionMQTT | None = None,
|
148
|
+
) -> None:
|
143
149
|
self.name = name
|
144
150
|
self.add_ble(ble_device)
|
145
151
|
self.add_cloud(cloud_device, mqtt)
|
@@ -160,14 +166,13 @@ class MammotionMixedDeviceManager:
|
|
160
166
|
def add_cloud(self, cloud_device: Device | None = None, mqtt: MammotionMQTT | None = None) -> None:
|
161
167
|
if cloud_device is not None:
|
162
168
|
self._cloud_device = MammotionBaseCloudDevice(
|
163
|
-
mqtt_client=mqtt,
|
164
|
-
|
165
|
-
mowing_state=self._mowing_state)
|
169
|
+
mqtt_client=mqtt, cloud_device=cloud_device, mowing_state=self._mowing_state
|
170
|
+
)
|
166
171
|
|
167
|
-
def replace_cloud(self, cloud_device:MammotionBaseCloudDevice) -> None:
|
172
|
+
def replace_cloud(self, cloud_device: MammotionBaseCloudDevice) -> None:
|
168
173
|
self._cloud_device = cloud_device
|
169
174
|
|
170
|
-
def replace_ble(self, ble_device:MammotionBaseBLEDevice) -> None:
|
175
|
+
def replace_ble(self, ble_device: MammotionBaseBLEDevice) -> None:
|
171
176
|
self._ble_device = ble_device
|
172
177
|
|
173
178
|
def has_cloud(self) -> bool:
|
@@ -178,7 +183,6 @@ class MammotionMixedDeviceManager:
|
|
178
183
|
|
179
184
|
|
180
185
|
class MammotionDevices:
|
181
|
-
|
182
186
|
devices: dict[str, MammotionMixedDeviceManager] = {}
|
183
187
|
|
184
188
|
def add_device(self, mammotion_device: MammotionMixedDeviceManager) -> None:
|
@@ -194,14 +198,18 @@ class MammotionDevices:
|
|
194
198
|
def get_device(self, mammotion_device_name: str) -> MammotionMixedDeviceManager:
|
195
199
|
return self.devices.get(mammotion_device_name)
|
196
200
|
|
197
|
-
|
201
|
+
|
202
|
+
async def create_devices(
|
203
|
+
ble_device: BLEDevice,
|
198
204
|
cloud_credentials: Credentials | None = None,
|
199
|
-
preference: ConnectionPreference = ConnectionPreference.BLUETOOTH
|
205
|
+
preference: ConnectionPreference = ConnectionPreference.BLUETOOTH,
|
206
|
+
):
|
200
207
|
mammotion = Mammotion(ble_device, preference)
|
201
208
|
|
202
209
|
if cloud_credentials and preference == ConnectionPreference.EITHER or preference == ConnectionPreference.WIFI:
|
203
|
-
cloud_client = await Mammotion.login(
|
204
|
-
|
210
|
+
cloud_client = await Mammotion.login(
|
211
|
+
cloud_credentials.account_id or cloud_credentials.email, cloud_credentials.password
|
212
|
+
)
|
205
213
|
await mammotion.initiate_cloud_connection(cloud_client)
|
206
214
|
|
207
215
|
return mammotion
|
@@ -215,12 +223,8 @@ class Mammotion(object):
|
|
215
223
|
cloud_client: CloudIOTGateway | None = None
|
216
224
|
mqtt: MammotionMQTT | None = None
|
217
225
|
|
218
|
-
|
219
|
-
|
220
226
|
def __init__(
|
221
|
-
self,
|
222
|
-
ble_device: BLEDevice,
|
223
|
-
preference: ConnectionPreference = ConnectionPreference.BLUETOOTH
|
227
|
+
self, ble_device: BLEDevice, preference: ConnectionPreference = ConnectionPreference.BLUETOOTH
|
224
228
|
) -> None:
|
225
229
|
"""Initialize MammotionDevice."""
|
226
230
|
if ble_device:
|
@@ -235,20 +239,24 @@ class Mammotion(object):
|
|
235
239
|
return
|
236
240
|
|
237
241
|
self.cloud_client = cloud_client
|
238
|
-
self.mqtt = MammotionMQTT(
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
242
|
+
self.mqtt = MammotionMQTT(
|
243
|
+
region_id=cloud_client._region_response.data.regionId,
|
244
|
+
product_key=cloud_client._aep_response.data.productKey,
|
245
|
+
device_name=cloud_client._aep_response.data.deviceName,
|
246
|
+
device_secret=cloud_client._aep_response.data.deviceSecret,
|
247
|
+
iot_token=cloud_client._session_by_authcode_response.data.iotToken,
|
248
|
+
client_id=cloud_client._client_id,
|
249
|
+
)
|
244
250
|
|
245
251
|
self.mqtt._cloud_client = cloud_client
|
246
252
|
loop = asyncio.get_running_loop()
|
247
253
|
await loop.run_in_executor(None, self.mqtt.connect_async)
|
248
254
|
|
249
255
|
for device in cloud_client.listing_dev_by_account_response.data.data:
|
250
|
-
if device.deviceName.startswith(("Luba-", "Yuka-")):
|
251
|
-
self.devices.add_device(
|
256
|
+
if device.deviceName.startswith(("Luba-", "Yuka-")) and self.devices.get_device(device.deviceName) is None:
|
257
|
+
self.devices.add_device(
|
258
|
+
MammotionMixedDeviceManager(name=device.deviceName, cloud_device=device, mqtt=self.mqtt)
|
259
|
+
)
|
252
260
|
|
253
261
|
def set_disconnect_strategy(self, disconnect: bool):
|
254
262
|
for device_name, device in self.devices.devices:
|
@@ -267,7 +275,9 @@ class Mammotion(object):
|
|
267
275
|
_LOGGER.debug("AuthCode: " + mammotion_http.login_info.authorization_code)
|
268
276
|
loop = asyncio.get_running_loop()
|
269
277
|
cloud_client.set_http(mammotion_http)
|
270
|
-
await loop.run_in_executor(
|
278
|
+
await loop.run_in_executor(
|
279
|
+
None, cloud_client.get_region, country_code, mammotion_http.login_info.authorization_code
|
280
|
+
)
|
271
281
|
await cloud_client.connect()
|
272
282
|
await cloud_client.login_by_oauth(country_code, mammotion_http.login_info.authorization_code)
|
273
283
|
await loop.run_in_executor(None, cloud_client.aep_handle)
|
@@ -276,7 +286,6 @@ class Mammotion(object):
|
|
276
286
|
await loop.run_in_executor(None, cloud_client.list_binding_by_account)
|
277
287
|
return cloud_client
|
278
288
|
|
279
|
-
|
280
289
|
def get_device_by_name(self, name: str) -> MammotionMixedDeviceManager:
|
281
290
|
return self.devices.get_device(name)
|
282
291
|
|
@@ -290,7 +299,7 @@ class Mammotion(object):
|
|
290
299
|
return await device.cloud().command(key)
|
291
300
|
# TODO work with both with EITHER
|
292
301
|
|
293
|
-
async def send_command_with_args(self,name: str, key: str, **kwargs: any):
|
302
|
+
async def send_command_with_args(self, name: str, key: str, **kwargs: any):
|
294
303
|
"""Send a command with args to the device."""
|
295
304
|
device = self.get_device_by_name(name)
|
296
305
|
if device:
|
@@ -300,7 +309,7 @@ class Mammotion(object):
|
|
300
309
|
return await device.cloud().command(key, **kwargs)
|
301
310
|
# TODO work with both with EITHER
|
302
311
|
|
303
|
-
async def start_sync(self, name:str, retry: int):
|
312
|
+
async def start_sync(self, name: str, retry: int):
|
304
313
|
device = self.get_device_by_name(name)
|
305
314
|
if device:
|
306
315
|
if self._preference is ConnectionPreference.BLUETOOTH:
|
@@ -309,7 +318,7 @@ class Mammotion(object):
|
|
309
318
|
return await device.cloud().start_sync(retry)
|
310
319
|
# TODO work with both with EITHER
|
311
320
|
|
312
|
-
async def start_map_sync(self, name:str):
|
321
|
+
async def start_map_sync(self, name: str):
|
313
322
|
device = self.get_device_by_name(name)
|
314
323
|
if device:
|
315
324
|
if self._preference is ConnectionPreference.BLUETOOTH:
|
@@ -323,6 +332,7 @@ class Mammotion(object):
|
|
323
332
|
if device:
|
324
333
|
return device.mower_state()
|
325
334
|
|
335
|
+
|
326
336
|
def has_field(message: betterproto.Message) -> bool:
|
327
337
|
"""Check if the message has any fields serialized on wire."""
|
328
338
|
return betterproto.serialized_on_wire(message)
|
@@ -346,7 +356,7 @@ class MammotionBaseDevice:
|
|
346
356
|
self._notify_future: asyncio.Future[bytes] | None = None
|
347
357
|
self._cloud_device = cloud_device
|
348
358
|
|
349
|
-
def set_notification_callback(self, func: Callable[[],Awaitable[None]]):
|
359
|
+
def set_notification_callback(self, func: Callable[[], Awaitable[None]]):
|
350
360
|
self._state_manager.on_notification_callback = func
|
351
361
|
|
352
362
|
async def datahash_response(self, hash_ack: NavGetHashListAck):
|
@@ -368,8 +378,8 @@ class MammotionBaseDevice:
|
|
368
378
|
|
369
379
|
await self.queue_command("synchronize_hash_data", hash_num=data_hash)
|
370
380
|
else:
|
371
|
-
if current_frame != missing_frames[0]-1:
|
372
|
-
current_frame = missing_frames[0]-1
|
381
|
+
if current_frame != missing_frames[0] - 1:
|
382
|
+
current_frame = missing_frames[0] - 1
|
373
383
|
|
374
384
|
region_data = RegionData()
|
375
385
|
region_data.hash = common_data.hash
|
@@ -476,7 +486,7 @@ class MammotionBaseDevice:
|
|
476
486
|
return self._mower
|
477
487
|
|
478
488
|
@abstractmethod
|
479
|
-
async def queue_command(self, key: str,
|
489
|
+
async def queue_command(self, key: str, **kwargs: any) -> bytes:
|
480
490
|
"""Queue commands to mower."""
|
481
491
|
|
482
492
|
@abstractmethod
|
@@ -507,28 +517,22 @@ class MammotionBaseDevice:
|
|
507
517
|
|
508
518
|
await self.queue_command("get_hash_response", total_frame=1, current_frame=1)
|
509
519
|
|
510
|
-
|
511
520
|
# work out why this crashes sometimes for better proto
|
512
521
|
if self._cloud_device:
|
513
|
-
await self.queue_command(
|
514
|
-
"get_area_name_list", device_id=self._cloud_device.deviceName
|
515
|
-
)
|
522
|
+
await self.queue_command("get_area_name_list", device_id=self._cloud_device.deviceName)
|
516
523
|
if has_field(self._mower.net.toapp_wifi_iot_status):
|
517
|
-
await self.queue_command(
|
518
|
-
"get_area_name_list", device_id=self._mower.net.toapp_wifi_iot_status.devicename
|
519
|
-
)
|
520
|
-
|
524
|
+
await self.queue_command("get_area_name_list", device_id=self._mower.net.toapp_wifi_iot_status.devicename)
|
521
525
|
|
522
526
|
# sub_cmd 3 is job hashes??
|
523
527
|
# sub_cmd 4 is dump location (yuka)
|
524
528
|
# jobs list
|
525
529
|
# hash_list_result = await self._send_command_with_args("get_all_boundary_hash_list", sub_cmd=3)
|
530
|
+
|
526
531
|
async def async_get_errors(self):
|
527
532
|
"""Error codes."""
|
528
533
|
await self.queue_command("allpowerfull_rw", id=5, rw=1, context=2)
|
529
534
|
await self.queue_command("allpowerfull_rw", id=5, rw=1, context=3)
|
530
535
|
|
531
|
-
|
532
536
|
async def move_forward(self, linear: float):
|
533
537
|
"""Move forward. values 0.0 1.0."""
|
534
538
|
linear_percent = get_percent(abs(linear * 100))
|
@@ -553,7 +557,6 @@ class MammotionBaseDevice:
|
|
553
557
|
(linear_speed, angular_speed) = transform_both_speeds(0.0, 180.0, 0.0, angular_percent)
|
554
558
|
await self.queue_command("send_movement", linear_speed=linear_speed, angular_speed=angular_speed)
|
555
559
|
|
556
|
-
|
557
560
|
async def command(self, key: str, **kwargs):
|
558
561
|
"""Send a command to the device."""
|
559
562
|
return await self.queue_command(key, **kwargs)
|
@@ -815,7 +818,7 @@ class MammotionBaseBLEDevice(MammotionBaseDevice):
|
|
815
818
|
notify_msg = await self._notify_future
|
816
819
|
except asyncio.TimeoutError:
|
817
820
|
timeout_expired = True
|
818
|
-
notify_msg = b
|
821
|
+
notify_msg = b""
|
819
822
|
finally:
|
820
823
|
if not timeout_expired:
|
821
824
|
timeout_handle.cancel()
|
@@ -978,12 +981,7 @@ class MammotionBaseBLEDevice(MammotionBaseDevice):
|
|
978
981
|
class MammotionBaseCloudDevice(MammotionBaseDevice):
|
979
982
|
"""Base class for Mammotion Cloud devices."""
|
980
983
|
|
981
|
-
def __init__(
|
982
|
-
self,
|
983
|
-
mqtt_client: MammotionMQTT,
|
984
|
-
cloud_device: Device,
|
985
|
-
mowing_state: MowingDevice
|
986
|
-
) -> None:
|
984
|
+
def __init__(self, mqtt_client: MammotionMQTT, cloud_device: Device, mowing_state: MowingDevice) -> None:
|
987
985
|
"""Initialize MammotionBaseCloudDevice."""
|
988
986
|
super().__init__(mowing_state, cloud_device)
|
989
987
|
self._ble_sync_task = None
|
@@ -1015,7 +1013,6 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
|
|
1015
1013
|
"""Callback for when MQTT is subscribed to events."""
|
1016
1014
|
loop = asyncio.get_event_loop()
|
1017
1015
|
|
1018
|
-
|
1019
1016
|
await self._ble_sync()
|
1020
1017
|
await self.run_periodic_sync_task()
|
1021
1018
|
loop.create_task(self._process_queue())
|
@@ -1025,15 +1022,15 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
|
|
1025
1022
|
async def on_connected(self):
|
1026
1023
|
"""Callback for when MQTT connects."""
|
1027
1024
|
|
1028
|
-
|
1029
1025
|
async def on_disconnected(self):
|
1030
1026
|
"""Callback for when MQTT disconnects."""
|
1031
1027
|
|
1032
1028
|
async def _ble_sync(self):
|
1033
1029
|
command_bytes = self._commands.send_todev_ble_sync(3)
|
1034
1030
|
loop = asyncio.get_running_loop()
|
1035
|
-
await loop.run_in_executor(
|
1036
|
-
|
1031
|
+
await loop.run_in_executor(
|
1032
|
+
None, self._mqtt_client.get_cloud_client().send_cloud_command, self.iot_id, command_bytes
|
1033
|
+
)
|
1037
1034
|
|
1038
1035
|
async def run_periodic_sync_task(self) -> None:
|
1039
1036
|
"""Send ble sync to robot."""
|
@@ -1088,10 +1085,7 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
|
|
1088
1085
|
async def _send_command(self, key: str, retry: int | None = None) -> bytes | None:
|
1089
1086
|
"""Send command to device via MQTT and read response."""
|
1090
1087
|
if self._operation_lock.locked():
|
1091
|
-
_LOGGER.debug(
|
1092
|
-
"%s: Operation already in progress, waiting for it to complete;",
|
1093
|
-
self.device.nickName
|
1094
|
-
)
|
1088
|
+
_LOGGER.debug("%s: Operation already in progress, waiting for it to complete;", self.device.nickName)
|
1095
1089
|
with self._operation_lock:
|
1096
1090
|
try:
|
1097
1091
|
command_bytes = getattr(self._commands, key)()
|
@@ -1140,14 +1134,16 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
|
|
1140
1134
|
self._key = key
|
1141
1135
|
_LOGGER.debug("%s: Sending command: %s", self.device.nickName, key)
|
1142
1136
|
|
1143
|
-
await self.loop.run_in_executor(
|
1137
|
+
await self.loop.run_in_executor(
|
1138
|
+
None, self._mqtt_client.get_cloud_client().send_cloud_command, self.iot_id, command
|
1139
|
+
)
|
1144
1140
|
future = MammotionFuture()
|
1145
1141
|
self._waiting_queue.append(future)
|
1146
1142
|
timeout = 5
|
1147
1143
|
try:
|
1148
1144
|
notify_msg = await future.async_get(timeout)
|
1149
1145
|
except asyncio.TimeoutError:
|
1150
|
-
notify_msg = b
|
1146
|
+
notify_msg = b""
|
1151
1147
|
|
1152
1148
|
_LOGGER.debug("%s: Message received", self.device.nickName)
|
1153
1149
|
|
@@ -1156,10 +1152,7 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
|
|
1156
1152
|
async def _send_command_with_args(self, key: str, **kwargs: any) -> bytes | None:
|
1157
1153
|
"""Send command with arguments to device via MQTT and read response."""
|
1158
1154
|
if self._operation_lock.locked():
|
1159
|
-
_LOGGER.debug(
|
1160
|
-
"%s: Operation already in progress, waiting for it to complete;",
|
1161
|
-
self.device.nickName
|
1162
|
-
)
|
1155
|
+
_LOGGER.debug("%s: Operation already in progress, waiting for it to complete;", self.device.nickName)
|
1163
1156
|
with self._operation_lock:
|
1164
1157
|
try:
|
1165
1158
|
command_bytes = getattr(self._commands, key)(**kwargs)
|
@@ -1187,7 +1180,7 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
|
|
1187
1180
|
_LOGGER.debug("Thing event received")
|
1188
1181
|
event = ThingEventMessage.from_dicts(payload)
|
1189
1182
|
params = event.params
|
1190
|
-
if params.identifier is None:
|
1183
|
+
if params.get("identifier", None) is None:
|
1191
1184
|
return
|
1192
1185
|
if params.identifier == "device_protobuf_msg_event" and event.method == "thing.events":
|
1193
1186
|
_LOGGER.debug("Protobuf event")
|
@@ -1195,14 +1188,16 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
|
|
1195
1188
|
self._update_raw_data(cast(bytes, binary_data))
|
1196
1189
|
new_msg = LubaMsg().parse(cast(bytes, binary_data))
|
1197
1190
|
|
1198
|
-
if
|
1191
|
+
if (
|
1192
|
+
self._commands.get_device_product_key() == ""
|
1193
|
+
and self._commands.get_device_name() == event.params.deviceName
|
1194
|
+
):
|
1199
1195
|
self._commands.set_device_product_key(event.params.productKey)
|
1200
1196
|
|
1201
1197
|
if betterproto.serialized_on_wire(new_msg.net):
|
1202
1198
|
if new_msg.net.todev_ble_sync != 0 or has_field(new_msg.net.toapp_wifi_iot_status):
|
1203
1199
|
return
|
1204
1200
|
|
1205
|
-
|
1206
1201
|
if len(self._waiting_queue) > 0:
|
1207
1202
|
fut: MammotionFuture = self._waiting_queue.popleft()
|
1208
1203
|
while fut.fut.cancelled() and len(self._waiting_queue) > 0:
|
@@ -1220,6 +1215,3 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
|
|
1220
1215
|
def _disconnect(self):
|
1221
1216
|
"""Disconnect the MQTT client."""
|
1222
1217
|
self._mqtt_client.disconnect()
|
1223
|
-
|
1224
|
-
|
1225
|
-
|
@@ -1,4 +1,5 @@
|
|
1
1
|
"""MammotionMQTT."""
|
2
|
+
|
2
3
|
import asyncio
|
3
4
|
import hashlib
|
4
5
|
import hmac
|
@@ -36,11 +37,11 @@ class MammotionMQTT:
|
|
36
37
|
self._cloud_client = None
|
37
38
|
self.is_connected = False
|
38
39
|
self.is_ready = False
|
39
|
-
self.on_connected: Optional[Callable[[],Awaitable[None]]] = None
|
40
|
-
self.on_ready: Optional[Callable[[],Awaitable[None]]] = None
|
41
|
-
self.on_error: Optional[Callable[[str],Awaitable[None]]] = None
|
42
|
-
self.on_disconnected: Optional[Callable[[],Awaitable[None]]] = None
|
43
|
-
self.on_message: Optional[Callable[[str, str, str],Awaitable[None]]] = None
|
40
|
+
self.on_connected: Optional[Callable[[], Awaitable[None]]] = None
|
41
|
+
self.on_ready: Optional[Callable[[], Awaitable[None]]] = None
|
42
|
+
self.on_error: Optional[Callable[[str], Awaitable[None]]] = None
|
43
|
+
self.on_disconnected: Optional[Callable[[], Awaitable[None]]] = None
|
44
|
+
self.on_message: Optional[Callable[[str, str, str], Awaitable[None]]] = None
|
44
45
|
|
45
46
|
self._product_key = product_key
|
46
47
|
self._device_name = device_name
|
@@ -84,7 +85,6 @@ class MammotionMQTT:
|
|
84
85
|
self._linkkit_client.thing_setup()
|
85
86
|
self._linkkit_client.connect_async()
|
86
87
|
|
87
|
-
|
88
88
|
def disconnect(self):
|
89
89
|
"""Disconnect from MQTT Server."""
|
90
90
|
logger.info("Disconnecting...")
|
@@ -143,7 +143,6 @@ class MammotionMQTT:
|
|
143
143
|
future = asyncio.run_coroutine_threadsafe(self.on_message(topic, payload, iot_id), self.loop)
|
144
144
|
asyncio.wrap_future(future, loop=self.loop)
|
145
145
|
|
146
|
-
|
147
146
|
def _thing_on_connect(self, session_flag, rc, user_data):
|
148
147
|
"""Is called on thing connect."""
|
149
148
|
self.is_connected = True
|
@@ -164,7 +163,6 @@ class MammotionMQTT:
|
|
164
163
|
future = asyncio.run_coroutine_threadsafe(self.on_disconnected(), self.loop)
|
165
164
|
asyncio.wrap_future(future, loop=self.loop)
|
166
165
|
|
167
|
-
|
168
166
|
def _on_message(self, _client, _userdata, message: MQTTMessage):
|
169
167
|
"""Is called when message is received."""
|
170
168
|
logger.info("Message on topic %s", message.topic)
|
@@ -194,4 +192,3 @@ class MammotionMQTT:
|
|
194
192
|
def get_cloud_client(self) -> Optional[CloudIOTGateway]:
|
195
193
|
"""Return internal cloud client."""
|
196
194
|
return self._cloud_client
|
197
|
-
|
@@ -54,11 +54,15 @@ class DeviceType(Enum):
|
|
54
54
|
return DeviceType.LUBA_2
|
55
55
|
elif value == 3:
|
56
56
|
return DeviceType.LUBA_YUKA
|
57
|
+
elif value == 4:
|
58
|
+
return DeviceType.YUKA_MINI
|
59
|
+
elif value == 5:
|
60
|
+
return DeviceType.YUKA_MINI2
|
57
61
|
else:
|
58
62
|
return DeviceType.UNKNOWN
|
59
63
|
|
60
64
|
@staticmethod
|
61
|
-
def value_of_str(device_name, product_key=""):
|
65
|
+
def value_of_str(device_name: str, product_key=""):
|
62
66
|
"""Determine the type of device based on the provided device name and
|
63
67
|
product key.
|
64
68
|
|
@@ -78,15 +82,20 @@ class DeviceType(Enum):
|
|
78
82
|
substring = device_name[:3]
|
79
83
|
substring2 = device_name[:7]
|
80
84
|
|
81
|
-
if DeviceType.RTK.
|
85
|
+
if DeviceType.RTK.get_name() in substring or DeviceType.contain_rtk_product_key(product_key):
|
82
86
|
return DeviceType.RTK
|
83
|
-
elif DeviceType.LUBA_2.
|
87
|
+
elif DeviceType.LUBA_2.get_name() in substring2 or DeviceType.contain_luba_2_product_key(product_key):
|
84
88
|
return DeviceType.LUBA_2
|
85
|
-
elif DeviceType.LUBA_YUKA.
|
89
|
+
elif DeviceType.LUBA_YUKA.get_name() in substring2:
|
86
90
|
return DeviceType.LUBA_YUKA
|
87
|
-
elif DeviceType.
|
91
|
+
elif DeviceType.YUKA_MINI.get_name() in substring2:
|
92
|
+
return DeviceType.YUKA_MINI
|
93
|
+
elif DeviceType.YUKA_MINI2.get_name() in substring2:
|
94
|
+
return DeviceType.YUKA_MINI2
|
95
|
+
elif DeviceType.LUBA.get_name() in substring2 or DeviceType.contain_luba_product_key(product_key):
|
88
96
|
return DeviceType.LUBA
|
89
97
|
else:
|
98
|
+
print("unknown device type")
|
90
99
|
return DeviceType.UNKNOWN
|
91
100
|
except Exception:
|
92
101
|
return DeviceType.UNKNOWN
|
@@ -173,7 +182,11 @@ class DeviceType(Enum):
|
|
173
182
|
|
174
183
|
"""
|
175
184
|
|
176
|
-
return
|
185
|
+
return (
|
186
|
+
DeviceType.value_of_str(device_name).get_value() == DeviceType.LUBA_YUKA.get_value()
|
187
|
+
or DeviceType.value_of_str(device_name).get_value() == DeviceType.YUKA_MINI.get_value()
|
188
|
+
or DeviceType.value_of_str(device_name).get_value() == DeviceType.YUKA_MINI2.get_value()
|
189
|
+
)
|
177
190
|
|
178
191
|
@staticmethod
|
179
192
|
def is_rtk(device_name, product_key=""):
|
@@ -260,5 +273,21 @@ class DeviceType(Enum):
|
|
260
273
|
return False
|
261
274
|
return product_key in ["a1iMygIwxFC", "a1LLmy1zc0j", "a1LLmy1zc0j"]
|
262
275
|
|
276
|
+
@staticmethod
|
277
|
+
def contain_yuka_product_key(product_key):
|
278
|
+
"""Check if the given product key is present in a predefined list.
|
279
|
+
|
280
|
+
Args:
|
281
|
+
product_key (str): The product key to be checked.
|
282
|
+
|
283
|
+
Returns:
|
284
|
+
bool: True if the product key is in the predefined list, False otherwise.
|
285
|
+
|
286
|
+
"""
|
287
|
+
|
288
|
+
if not product_key:
|
289
|
+
return False
|
290
|
+
return product_key in ["a1IQV0BrnXb"]
|
291
|
+
|
263
292
|
def is_support_video(self):
|
264
|
-
return self
|
293
|
+
return self != DeviceType.LUBA
|
pymammotion/utility/movement.py
CHANGED
@@ -10,8 +10,9 @@ def transform_both_speeds(linear: float, angular: float, linear_percent: float,
|
|
10
10
|
angular_speed = int(transform4[1] * 4.5)
|
11
11
|
return linear_speed, angular_speed
|
12
12
|
|
13
|
+
|
13
14
|
def get_percent(percent: float):
|
14
15
|
if percent <= 15.0:
|
15
16
|
return 0.0
|
16
17
|
|
17
|
-
return percent - 15.0
|
18
|
+
return percent - 15.0
|
@@ -1,16 +1,16 @@
|
|
1
|
-
pymammotion/__init__.py,sha256=
|
1
|
+
pymammotion/__init__.py,sha256=IDqM_7hZPTOYRMqmNAHl0Z5qkdoByNR9gjbguJ4Hpgs,1526
|
2
2
|
pymammotion/aliyun/__init__.py,sha256=T1lkX7TRYiL4nqYanG4l4MImV-SlavSbuooC-W-uUGw,29
|
3
|
-
pymammotion/aliyun/cloud_gateway.py,sha256=
|
3
|
+
pymammotion/aliyun/cloud_gateway.py,sha256=NkxSeGXJQ16tWKAncsDfdwG1OJEOCdmSJ212VX2H0CE,22726
|
4
4
|
pymammotion/aliyun/cloud_service.py,sha256=YWcKuKK6iRWy5mTnBYgHxcCusiRGGzQt3spSf7dGDss,2183
|
5
5
|
pymammotion/aliyun/dataclass/aep_response.py,sha256=8f6GIP58ve8gd6AL3HBoXxsy0n2q4ygWvjELGnoOnVc,452
|
6
6
|
pymammotion/aliyun/dataclass/connect_response.py,sha256=Yz-fEbDzgGPTo5Of2oAjmFkSv08T7ze80pQU4k-gKIU,824
|
7
|
-
pymammotion/aliyun/dataclass/dev_by_account_response.py,sha256=
|
7
|
+
pymammotion/aliyun/dataclass/dev_by_account_response.py,sha256=o9X8hNc1y3XXhFa9Nbw7TLUG3F9rIpTj79tEg2uEmUI,1032
|
8
8
|
pymammotion/aliyun/dataclass/login_by_oauth_response.py,sha256=IXSLZ6XnOliOnyXo5Bh0ErqFjA11puACh_9NH0sSJGQ,1262
|
9
9
|
pymammotion/aliyun/dataclass/regions_response.py,sha256=CVPpdFhDD6_emWHyLRzOdp2j3HLPtP8tlNyzGnr8AcI,690
|
10
|
-
pymammotion/aliyun/dataclass/session_by_authcode_response.py,sha256=
|
10
|
+
pymammotion/aliyun/dataclass/session_by_authcode_response.py,sha256=1K8Uu_V6flSBU8kLCh1Qj59tD9aZQSn7X3hLKeouE_g,407
|
11
11
|
pymammotion/aliyun/tmp_constant.py,sha256=M4Hq_lrGB3LZdX6R2XohRPFoK1NDnNV-pTJwJcJ9838,6650
|
12
12
|
pymammotion/bluetooth/__init__.py,sha256=LAl8jqZ1fPh-3mLmViNQsP3s814C1vsocYUa6oSaXt0,36
|
13
|
-
pymammotion/bluetooth/ble.py,sha256=
|
13
|
+
pymammotion/bluetooth/ble.py,sha256=AIRxXw-ZTY3gTFLclYAasdJ8Djs1s4dRl6KY9hUDUcw,2409
|
14
14
|
pymammotion/bluetooth/ble_message.py,sha256=byibcxZ5b_hE3UzHAImyTcxdBOcya9rdswdtuLV8_hY,15663
|
15
15
|
pymammotion/bluetooth/const.py,sha256=CCqyHsYbB0BAYjwdhXt_n6eWWxmhlUrAFjvVv57mbvE,1749
|
16
16
|
pymammotion/bluetooth/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
@@ -20,29 +20,29 @@ pymammotion/bluetooth/data/notifydata.py,sha256=N1bphpueWUWbsWUcpZmMGt2CyCgLcKAF
|
|
20
20
|
pymammotion/const.py,sha256=EEmZ1v4MqN2nPiNpS_mJQqaCkSNEa1EXUmtwZBUhXIg,329
|
21
21
|
pymammotion/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
22
22
|
pymammotion/data/model/__init__.py,sha256=d8FlIgCcWqoH3jJSpnm-IY-25RM-l2nbRwLtWjSHo74,222
|
23
|
-
pymammotion/data/model/account.py,sha256=
|
24
|
-
pymammotion/data/model/device.py,sha256=
|
25
|
-
pymammotion/data/model/device_config.py,sha256=
|
23
|
+
pymammotion/data/model/account.py,sha256=vJM-KTf2q6eBfVC-UlNHBSmJvqHiCawZ40vnuhXhaz8,140
|
24
|
+
pymammotion/data/model/device.py,sha256=65wBLvnbsx4ngARDW6rI3wyL_G19xhgngtogvvGXolc,10968
|
25
|
+
pymammotion/data/model/device_config.py,sha256=KnrBytR7ADxcXXwPKMaH17-BVhJy6ph9PD_Hd_7jf-0,2567
|
26
26
|
pymammotion/data/model/enums.py,sha256=tD_vYsxstOV_lUkYF9uWkrjVOgAJPNnGevy_xmiu3WE,1558
|
27
27
|
pymammotion/data/model/excute_boarder_params.py,sha256=kadSth4y-VXlXIZ6R-Ng-kDvBbM-3YRr8bmR86qR0U0,1355
|
28
28
|
pymammotion/data/model/execute_boarder.py,sha256=oDb2h5tFtOQIa8OCNYaDugqCgCZBLjQRzQTNVcJVAGQ,1072
|
29
|
-
pymammotion/data/model/generate_route_information.py,sha256=
|
30
|
-
pymammotion/data/model/hash_list.py,sha256=
|
31
|
-
pymammotion/data/model/location.py,sha256=
|
32
|
-
pymammotion/data/model/mowing_modes.py,sha256=
|
29
|
+
pymammotion/data/model/generate_route_information.py,sha256=MkUBoqGtCAKmiVQ4Q1pEoDVHZs5uLIo7vhfWT4nGbtY,801
|
30
|
+
pymammotion/data/model/hash_list.py,sha256=LnCU6Bdr4uy4OF8VGCs5t-JM1Ah6fyaBC3Z_Qwlaa4Y,2682
|
31
|
+
pymammotion/data/model/location.py,sha256=sLWObWvfRoR_hmpX5r32iaPOEyk1gJ1vbTRKNVSrdLU,753
|
32
|
+
pymammotion/data/model/mowing_modes.py,sha256=UxQrRAHQNC_lJTOA0ms2wuLAAi5UEVTkvvSZjnFpUjE,826
|
33
33
|
pymammotion/data/model/plan.py,sha256=7JvqAo0a9Yg1Vtifd4J3Dx3StEppxrMOfmq2-877kYg,2891
|
34
34
|
pymammotion/data/model/rapid_state.py,sha256=BdJFD_DlhrVneg-PqEruqCoMty-CR7q_9Qna-hk1yd8,1171
|
35
35
|
pymammotion/data/model/region_data.py,sha256=75xOTM1qeRbSROp53eIczw3yCmYM9DgMjMh8qE9xkKo,2880
|
36
36
|
pymammotion/data/model/report_info.py,sha256=1Rj_yRZ9apWX296QHae5prdObDbs32bsQf9rzMz2KEQ,4458
|
37
37
|
pymammotion/data/mqtt/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
38
|
-
pymammotion/data/mqtt/event.py,sha256=
|
38
|
+
pymammotion/data/mqtt/event.py,sha256=ih6_Evd09GsdfeHZ_5a_L93dhFhFd0s_4zFC1EBck98,4528
|
39
39
|
pymammotion/data/mqtt/properties.py,sha256=HkBPghr26L9_b4QaOi1DtPgb0UoPIOGSe9wb3kgnM6Y,2815
|
40
40
|
pymammotion/data/mqtt/status.py,sha256=zqnlo-MzejEQZszl0i0Wucoc3E76x6UtI9JLxoBnu54,1067
|
41
|
-
pymammotion/data/state_manager.py,sha256=
|
41
|
+
pymammotion/data/state_manager.py,sha256=5zP7DdnYXWUhbFYBWiv25L64q8OOzQNzUXzbWEWzYVc,3099
|
42
42
|
pymammotion/event/__init__.py,sha256=mgATR6vPHACNQ-0zH5fi7NdzeTCDV1CZyaWPmtUusi8,115
|
43
43
|
pymammotion/event/event.py,sha256=Fy5-I1p92AO_D67VW4eHQqA4pOt7MZsrP--tVfIVUz8,1820
|
44
44
|
pymammotion/http/_init_.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
45
|
-
pymammotion/http/http.py,sha256=
|
45
|
+
pymammotion/http/http.py,sha256=wE05Yo8K592d1anNaZF-GJHr9D3kYWsd7Axpc1Yei_k,2834
|
46
46
|
pymammotion/mammotion/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
47
47
|
pymammotion/mammotion/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
48
48
|
pymammotion/mammotion/commands/abstract_message.py,sha256=nw6r7694yzl7iJKqRqhLmAPRjd_TL_Xo_-JXq2_a_ug,222
|
@@ -56,12 +56,12 @@ pymammotion/mammotion/commands/messages/ota.py,sha256=XkeuWBZtpYMMBze6r8UN7dJXbe
|
|
56
56
|
pymammotion/mammotion/commands/messages/system.py,sha256=m4Cc1zUcLgMFCu6zp5rMupwjDnMD-1PM6hqXpXuXRtw,10984
|
57
57
|
pymammotion/mammotion/commands/messages/video.py,sha256=_8lJsU4sLm2CGnc7RDkueA0A51Ysui6x7SqFnhX8O2g,1007
|
58
58
|
pymammotion/mammotion/control/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
59
|
-
pymammotion/mammotion/control/joystick.py,sha256=
|
59
|
+
pymammotion/mammotion/control/joystick.py,sha256=xHOp_YmEA-MLaZh3n9VhVOh7oHCti7aKARyfyvUJSjc,5558
|
60
60
|
pymammotion/mammotion/devices/__init__.py,sha256=T72jt0ejtMjo1rPmn_FeMF3pmp0LLeRRpc9WcDKEYYY,126
|
61
|
-
pymammotion/mammotion/devices/mammotion.py,sha256=
|
61
|
+
pymammotion/mammotion/devices/mammotion.py,sha256=nAPbc1cG5VoKAIf8g_sJMUx0gduoVKUOz8FxncwT-Kk,49380
|
62
62
|
pymammotion/mqtt/__init__.py,sha256=Ocs5e-HLJvTuDpVXyECEsWIvwsUaxzj7lZ9mSYutNDY,105
|
63
|
-
pymammotion/mqtt/mammotion_future.py,sha256=
|
64
|
-
pymammotion/mqtt/mammotion_mqtt.py,sha256=
|
63
|
+
pymammotion/mqtt/mammotion_future.py,sha256=sQsEpy957YsfejsiVul1vHE62yDk66KID0kRNjMPxH0,688
|
64
|
+
pymammotion/mqtt/mammotion_mqtt.py,sha256=VrvO7jfBeKCHwryIa5Elm6D_xrbLX1Hk-0M65h5-C2c,8129
|
65
65
|
pymammotion/proto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
66
66
|
pymammotion/proto/basestation.proto,sha256=_x5gAz3FkZXS1jtq4GgZgaDCuRU-UV-7HTFdsfQ3zbo,1034
|
67
67
|
pymammotion/proto/basestation.py,sha256=js64_N2xQYRxWPRdVNEapO0qe7vBlfYnjW5sE8hi7hw,2026
|
@@ -106,14 +106,14 @@ pymammotion/proto/mctrl_sys_pb2.pyi,sha256=Dj_1UM86kZ5MfcVyNC76Z0gKrfl5YFsVWP2b-
|
|
106
106
|
pymammotion/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
107
107
|
pymammotion/utility/constant/__init__.py,sha256=tZz7szqIhrzNjfcLU3ysfINfg5VEBUAisd9AhP4mvT0,38
|
108
108
|
pymammotion/utility/constant/device_constant.py,sha256=rAEK60F52VyJL31uLnq0Y60D-0VK5gVm59yi9kBfndM,7123
|
109
|
-
pymammotion/utility/conversions.py,sha256=
|
109
|
+
pymammotion/utility/conversions.py,sha256=v3YICy0zZwwBBzrUZgabI7GRfiDBnkiAX2qdtk3NxOY,89
|
110
110
|
pymammotion/utility/datatype_converter.py,sha256=v6zym2Zu0upxQjR-xDqXwi3516zpntSlg7LP8tQF5K8,4216
|
111
|
-
pymammotion/utility/device_type.py,sha256=
|
111
|
+
pymammotion/utility/device_type.py,sha256=2B0RPqBo1mveGfFInPQmiON3-TcFMQGZpt7oMSFevL4,9363
|
112
112
|
pymammotion/utility/map.py,sha256=aoi-Luzuph02hKynTofMoq3mnPstanx75MDAVv49CuY,2211
|
113
|
-
pymammotion/utility/movement.py,sha256=
|
113
|
+
pymammotion/utility/movement.py,sha256=N75oAoAgFydqoaOedYIxGUHmuTCtPzAOtb-d_29tpfI,615
|
114
114
|
pymammotion/utility/periodic.py,sha256=9wJMfwXPlx6Mbp3Fws7LLTI34ZDKphH1bva_Ggyk32g,3281
|
115
115
|
pymammotion/utility/rocker_util.py,sha256=syPL0QN4zMzHiTIkUKS7RXBBptjdbkfNlPddwUD5V3A,7171
|
116
|
-
pymammotion-0.2.
|
117
|
-
pymammotion-0.2.
|
118
|
-
pymammotion-0.2.
|
119
|
-
pymammotion-0.2.
|
116
|
+
pymammotion-0.2.29.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
117
|
+
pymammotion-0.2.29.dist-info/METADATA,sha256=kP1T_XwrwQcFmcNL5Vu0WhYWzQgtH92pv5H2JN86AvM,3969
|
118
|
+
pymammotion-0.2.29.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
119
|
+
pymammotion-0.2.29.dist-info/RECORD,,
|
File without changes
|
File without changes
|