pymammotion 0.1.9__py3-none-any.whl → 0.2.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.
Potentially problematic release.
This version of pymammotion might be problematic. Click here for more details.
- pymammotion/__init__.py +2 -10
- pymammotion/aliyun/cloud_gateway.py +2 -3
- pymammotion/aliyun/dataclass/dev_by_account_response.py +1 -1
- pymammotion/data/model/account.py +8 -0
- pymammotion/data/model/device.py +5 -4
- pymammotion/data/model/device_config.py +2 -2
- pymammotion/data/model/location.py +1 -1
- pymammotion/data/model/mowing_modes.py +2 -1
- pymammotion/data/model/report_info.py +53 -48
- pymammotion/data/mqtt/event.py +32 -1
- pymammotion/http/http.py +12 -11
- pymammotion/mammotion/commands/mammotion_command.py +0 -4
- pymammotion/mammotion/commands/messages/navigation.py +4 -1
- pymammotion/mammotion/devices/mammotion.py +222 -44
- pymammotion/mqtt/mammotion_mqtt.py +13 -52
- pymammotion/utility/constant/device_constant.py +1 -1
- pymammotion/utility/map.py +9 -5
- {pymammotion-0.1.9.dist-info → pymammotion-0.2.0.dist-info}/METADATA +1 -1
- {pymammotion-0.1.9.dist-info → pymammotion-0.2.0.dist-info}/RECORD +21 -20
- {pymammotion-0.1.9.dist-info → pymammotion-0.2.0.dist-info}/LICENSE +0 -0
- {pymammotion-0.1.9.dist-info → pymammotion-0.2.0.dist-info}/WHEEL +0 -0
pymammotion/__init__.py
CHANGED
|
@@ -10,21 +10,13 @@ import os
|
|
|
10
10
|
|
|
11
11
|
# works outside HA on its own
|
|
12
12
|
from pymammotion.bluetooth.ble import LubaBLE
|
|
13
|
-
from pymammotion.http.http import
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
__all__ = [
|
|
22
|
-
'LubaBLE',
|
|
23
|
-
'LubaHTTP',
|
|
24
|
-
'connect_http',
|
|
25
|
-
'MammotionBaseBLEDevice',
|
|
26
|
-
'MammotionMQTT'
|
|
27
|
-
]
|
|
19
|
+
__all__ = ["LubaBLE", "MammotionHTTP", "connect_http", "MammotionBaseBLEDevice", "MammotionMQTT"]
|
|
28
20
|
|
|
29
21
|
logger = logging.getLogger(__name__)
|
|
30
22
|
|
|
@@ -391,8 +391,6 @@ class CloudIOTGateway:
|
|
|
391
391
|
|
|
392
392
|
def check_or_refresh_session(self):
|
|
393
393
|
"""Check or refresh the session."""
|
|
394
|
-
if self.load_saved_params() is False:
|
|
395
|
-
return False
|
|
396
394
|
config = Config(
|
|
397
395
|
app_key=self._app_key,
|
|
398
396
|
app_secret=self._app_secret,
|
|
@@ -436,7 +434,7 @@ class CloudIOTGateway:
|
|
|
436
434
|
# Carica la stringa JSON in un dizionario
|
|
437
435
|
json.loads(response_body_str)
|
|
438
436
|
|
|
439
|
-
def list_binding_by_account(self):
|
|
437
|
+
def list_binding_by_account(self) -> ListingDevByAccountResponse:
|
|
440
438
|
"""List bindings by account."""
|
|
441
439
|
config = Config(
|
|
442
440
|
app_key=self._app_key,
|
|
@@ -476,6 +474,7 @@ class CloudIOTGateway:
|
|
|
476
474
|
raise Exception("Error in creating session: " + response_body_dict["msg"])
|
|
477
475
|
|
|
478
476
|
self._listing_dev_by_account_response = ListingDevByAccountResponse.from_dict(response_body_dict)
|
|
477
|
+
return self._listing_dev_by_account_response
|
|
479
478
|
|
|
480
479
|
def send_cloud_command(self, iot_id: str, command: bytes) -> str:
|
|
481
480
|
"""Send a cloud command to the specified IoT device."""
|
|
@@ -6,7 +6,6 @@ from mashumaro.mixins.orjson import DataClassORJSONMixin
|
|
|
6
6
|
|
|
7
7
|
@dataclass
|
|
8
8
|
class Device(DataClassORJSONMixin):
|
|
9
|
-
productModel: str
|
|
10
9
|
gmtModified: int
|
|
11
10
|
netType: str
|
|
12
11
|
nickName: str
|
|
@@ -26,6 +25,7 @@ class Device(DataClassORJSONMixin):
|
|
|
26
25
|
status: int
|
|
27
26
|
productImage: Optional[str] = None
|
|
28
27
|
categoryImage: Optional[str] = None
|
|
28
|
+
productModel: Optional[str] = None
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
@dataclass
|
pymammotion/data/model/device.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""MowingDevice class to wrap around the betterproto dataclasses."""
|
|
2
|
+
|
|
2
3
|
import math
|
|
3
4
|
from dataclasses import dataclass
|
|
4
5
|
|
|
@@ -15,7 +16,7 @@ from pymammotion.proto.mctrl_driver import MctlDriver
|
|
|
15
16
|
from pymammotion.proto.mctrl_nav import MctlNav
|
|
16
17
|
from pymammotion.proto.mctrl_ota import MctlOta
|
|
17
18
|
from pymammotion.proto.mctrl_pept import MctlPept
|
|
18
|
-
from pymammotion.proto.mctrl_sys import MctlSys, MowToAppInfoT,
|
|
19
|
+
from pymammotion.proto.mctrl_sys import MctlSys, MowToAppInfoT, ReportInfoData, SystemUpdateBufMsg
|
|
19
20
|
from pymammotion.utility.map import CoordinateConverter
|
|
20
21
|
|
|
21
22
|
|
|
@@ -99,12 +100,12 @@ class MowingDevice:
|
|
|
99
100
|
if index == 0:
|
|
100
101
|
self.location.position_type = location.pos_type
|
|
101
102
|
self.location.orientation = location.real_toward / 10000
|
|
102
|
-
self.location.device = coordinate_converter.enu_to_lla(
|
|
103
|
+
self.location.device = coordinate_converter.enu_to_lla(
|
|
104
|
+
parse_double(location.real_pos_y, 4.0), parse_double(location.real_pos_x, 4.0)
|
|
105
|
+
)
|
|
103
106
|
|
|
104
107
|
self.report_data = self.report_data.from_dict(toapp_report_data.to_dict(casing=betterproto.Casing.SNAKE))
|
|
105
108
|
|
|
106
|
-
|
|
107
|
-
|
|
108
109
|
def mow_info(self, toapp_mow_info: MowToAppInfoT):
|
|
109
110
|
pass
|
|
110
111
|
|
|
@@ -39,7 +39,8 @@ class MowOrder(IntEnum):
|
|
|
39
39
|
|
|
40
40
|
class BypassStrategy(IntEnum):
|
|
41
41
|
"""Matches up with ultra_wave."""
|
|
42
|
+
|
|
42
43
|
direct_touch = 0
|
|
43
44
|
slow_touch = 1
|
|
44
45
|
less_touch = 2
|
|
45
|
-
no_touch = 3
|
|
46
|
+
no_touch = 3 # luba 2 yuka only or possibly value of 10
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
from dataclasses import dataclass, field
|
|
1
|
+
from dataclasses import asdict, dataclass, field
|
|
2
|
+
|
|
2
3
|
|
|
3
4
|
@dataclass
|
|
4
5
|
class ConnectData:
|
|
@@ -9,11 +10,12 @@ class ConnectData:
|
|
|
9
10
|
@classmethod
|
|
10
11
|
def from_dict(cls, data: dict):
|
|
11
12
|
return cls(
|
|
12
|
-
connect_type=data.get(
|
|
13
|
-
ble_rssi=data.get(
|
|
14
|
-
wifi_rssi=data.get(
|
|
13
|
+
connect_type=data.get("connect_type", 0),
|
|
14
|
+
ble_rssi=data.get("ble_rssi", 0),
|
|
15
|
+
wifi_rssi=data.get("wifi_rssi", 0),
|
|
15
16
|
)
|
|
16
17
|
|
|
18
|
+
|
|
17
19
|
@dataclass
|
|
18
20
|
class DeviceData:
|
|
19
21
|
sys_status: int = 0
|
|
@@ -26,14 +28,15 @@ class DeviceData:
|
|
|
26
28
|
@classmethod
|
|
27
29
|
def from_dict(cls, data: dict):
|
|
28
30
|
return cls(
|
|
29
|
-
sys_status=data.get(
|
|
30
|
-
charge_state=data.get(
|
|
31
|
-
battery_val=data.get(
|
|
32
|
-
sensor_status=data.get(
|
|
33
|
-
last_status=data.get(
|
|
34
|
-
sys_time_stamp=data.get(
|
|
31
|
+
sys_status=data.get("sys_status", 0),
|
|
32
|
+
charge_state=data.get("charge_state", 0),
|
|
33
|
+
battery_val=data.get("battery_val", 0),
|
|
34
|
+
sensor_status=data.get("sensor_status", 0),
|
|
35
|
+
last_status=data.get("last_status", 0),
|
|
36
|
+
sys_time_stamp=data.get("sys_time_stamp", ""),
|
|
35
37
|
)
|
|
36
38
|
|
|
39
|
+
|
|
37
40
|
@dataclass
|
|
38
41
|
class RTKData:
|
|
39
42
|
status: int = 0
|
|
@@ -45,13 +48,14 @@ class RTKData:
|
|
|
45
48
|
@classmethod
|
|
46
49
|
def from_dict(cls, data: dict):
|
|
47
50
|
return cls(
|
|
48
|
-
status=data.get(
|
|
49
|
-
pos_level=data.get(
|
|
50
|
-
gps_stars=data.get(
|
|
51
|
-
dis_status=data.get(
|
|
52
|
-
co_view_stars=data.get(
|
|
51
|
+
status=data.get("status", 0),
|
|
52
|
+
pos_level=data.get("pos_level", 0),
|
|
53
|
+
gps_stars=data.get("gps_stars", 0),
|
|
54
|
+
dis_status=data.get("dis_status", ""),
|
|
55
|
+
co_view_stars=data.get("co_view_stars", 0),
|
|
53
56
|
)
|
|
54
57
|
|
|
58
|
+
|
|
55
59
|
@dataclass
|
|
56
60
|
class LocationData:
|
|
57
61
|
real_pos_x: int = 0
|
|
@@ -63,13 +67,14 @@ class LocationData:
|
|
|
63
67
|
@classmethod
|
|
64
68
|
def from_dict(cls, data: dict):
|
|
65
69
|
return cls(
|
|
66
|
-
real_pos_x=data.get(
|
|
67
|
-
real_pos_y=data.get(
|
|
68
|
-
real_toward=data.get(
|
|
69
|
-
pos_type=data.get(
|
|
70
|
-
bol_hash=data.get(
|
|
70
|
+
real_pos_x=data.get("real_pos_x", 0),
|
|
71
|
+
real_pos_y=data.get("real_pos_y", 0),
|
|
72
|
+
real_toward=data.get("real_toward", 0),
|
|
73
|
+
pos_type=data.get("pos_type", 0),
|
|
74
|
+
bol_hash=data.get("bol_hash", ""),
|
|
71
75
|
)
|
|
72
76
|
|
|
77
|
+
|
|
73
78
|
@dataclass
|
|
74
79
|
class WorkData:
|
|
75
80
|
path: int = 0
|
|
@@ -96,28 +101,29 @@ class WorkData:
|
|
|
96
101
|
@classmethod
|
|
97
102
|
def from_dict(cls, data: dict):
|
|
98
103
|
return cls(
|
|
99
|
-
path=data.get(
|
|
100
|
-
path_hash=data.get(
|
|
101
|
-
progress=data.get(
|
|
102
|
-
area=data.get(
|
|
103
|
-
bp_info=data.get(
|
|
104
|
-
bp_hash=data.get(
|
|
105
|
-
bp_pos_x=data.get(
|
|
106
|
-
bp_pos_y=data.get(
|
|
107
|
-
real_path_num=data.get(
|
|
108
|
-
path_pos_x=data.get(
|
|
109
|
-
path_pos_y=data.get(
|
|
110
|
-
ub_zone_hash=data.get(
|
|
111
|
-
ub_path_hash=data.get(
|
|
112
|
-
init_cfg_hash=data.get(
|
|
113
|
-
ub_ecode_hash=data.get(
|
|
114
|
-
nav_run_mode=data.get(
|
|
115
|
-
test_mode_status=data.get(
|
|
116
|
-
man_run_speed=data.get(
|
|
117
|
-
nav_edit_status=data.get(
|
|
118
|
-
knife_height=data.get(
|
|
104
|
+
path=data.get("path", 0),
|
|
105
|
+
path_hash=data.get("path_hash", ""),
|
|
106
|
+
progress=data.get("progress", 0),
|
|
107
|
+
area=data.get("area", 0),
|
|
108
|
+
bp_info=data.get("bp_info", 0),
|
|
109
|
+
bp_hash=data.get("bp_hash", ""),
|
|
110
|
+
bp_pos_x=data.get("bp_pos_x", 0),
|
|
111
|
+
bp_pos_y=data.get("bp_pos_y", 0),
|
|
112
|
+
real_path_num=data.get("real_path_num", ""),
|
|
113
|
+
path_pos_x=data.get("path_pos_x", 0),
|
|
114
|
+
path_pos_y=data.get("path_pos_y", 0),
|
|
115
|
+
ub_zone_hash=data.get("ub_zone_hash", ""),
|
|
116
|
+
ub_path_hash=data.get("ub_path_hash", ""),
|
|
117
|
+
init_cfg_hash=data.get("init_cfg_hash", ""),
|
|
118
|
+
ub_ecode_hash=data.get("ub_ecode_hash", ""),
|
|
119
|
+
nav_run_mode=data.get("nav_run_mode", 0),
|
|
120
|
+
test_mode_status=data.get("test_mode_status", 0),
|
|
121
|
+
man_run_speed=data.get("man_run_speed", 0),
|
|
122
|
+
nav_edit_status=data.get("nav_edit_status", 0),
|
|
123
|
+
knife_height=data.get("knife_height", 0),
|
|
119
124
|
)
|
|
120
125
|
|
|
126
|
+
|
|
121
127
|
@dataclass
|
|
122
128
|
class ReportData:
|
|
123
129
|
connect: ConnectData = field(default_factory=ConnectData)
|
|
@@ -126,16 +132,15 @@ class ReportData:
|
|
|
126
132
|
locations: list[LocationData] = field(default_factory=list)
|
|
127
133
|
work: WorkData = field(default_factory=WorkData)
|
|
128
134
|
|
|
129
|
-
|
|
130
135
|
def from_dict(self, data: dict):
|
|
131
136
|
locations = self.locations
|
|
132
|
-
if data.get(
|
|
133
|
-
locations=[LocationData.from_dict(loc) for loc in data.get(
|
|
137
|
+
if data.get("locations") is not None:
|
|
138
|
+
locations = [LocationData.from_dict(loc) for loc in data.get("locations", [])]
|
|
134
139
|
|
|
135
140
|
return ReportData(
|
|
136
|
-
connect=ConnectData.from_dict(data.get(
|
|
137
|
-
dev=DeviceData.from_dict(data.get(
|
|
138
|
-
rtk=RTKData.from_dict(data.get(
|
|
141
|
+
connect=ConnectData.from_dict(data.get("connect", asdict(self.connect))),
|
|
142
|
+
dev=DeviceData.from_dict(data.get("dev", asdict(self.dev))),
|
|
143
|
+
rtk=RTKData.from_dict(data.get("rtk", asdict(self.rtk))),
|
|
139
144
|
locations=locations,
|
|
140
|
-
work=WorkData.from_dict(data.get(
|
|
141
|
-
)
|
|
145
|
+
work=WorkData.from_dict(data.get("work", asdict(self.work))),
|
|
146
|
+
)
|
pymammotion/data/mqtt/event.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from base64 import b64decode
|
|
2
2
|
from dataclasses import dataclass
|
|
3
|
-
from typing import Any, Literal, Union
|
|
3
|
+
from typing import Any, Literal, Optional, Union
|
|
4
4
|
|
|
5
5
|
from google.protobuf import json_format
|
|
6
6
|
from mashumaro.mixins.orjson import DataClassORJSONMixin
|
|
@@ -67,6 +67,17 @@ class GeneralParams(DataClassORJSONMixin):
|
|
|
67
67
|
tenantInstanceId: str
|
|
68
68
|
value: Any
|
|
69
69
|
|
|
70
|
+
# Campi opzionali
|
|
71
|
+
checkFailedData: Optional[dict] = None
|
|
72
|
+
_tenantId: Optional[str] = None
|
|
73
|
+
generateTime: Optional[int] = None
|
|
74
|
+
JMSXDeliveryCount: Optional[int] = None
|
|
75
|
+
qos: Optional[int] = None
|
|
76
|
+
requestId: Optional[str] = None
|
|
77
|
+
_categoryKey: Optional[str] = None
|
|
78
|
+
deviceType: Optional[str] = None
|
|
79
|
+
_traceId: Optional[str] = None
|
|
80
|
+
|
|
70
81
|
|
|
71
82
|
@dataclass
|
|
72
83
|
class DeviceProtobufMsgEventParams(GeneralParams):
|
|
@@ -88,3 +99,23 @@ class ThingEventMessage(DataClassORJSONMixin):
|
|
|
88
99
|
id: str
|
|
89
100
|
params: Union[DeviceProtobufMsgEventParams, DeviceWarningEventParams]
|
|
90
101
|
version: Literal["1.0"]
|
|
102
|
+
|
|
103
|
+
@classmethod
|
|
104
|
+
def from_dicts(cls, payload: dict) -> "ThingEventMessage":
|
|
105
|
+
"""Deserializza il payload JSON in un'istanza di ThingEventMessage."""
|
|
106
|
+
method = payload.get("method")
|
|
107
|
+
event_id = payload.get("id")
|
|
108
|
+
params_dict = payload.get("params", {})
|
|
109
|
+
version = payload.get("version")
|
|
110
|
+
|
|
111
|
+
# Determina quale classe usare per i parametri
|
|
112
|
+
identifier = params_dict.get("identifier")
|
|
113
|
+
if identifier == "device_protobuf_msg_event":
|
|
114
|
+
params_obj = DeviceProtobufMsgEventParams(**params_dict)
|
|
115
|
+
elif identifier == "device_warning_event":
|
|
116
|
+
params_obj = DeviceWarningEventParams(**params_dict)
|
|
117
|
+
else:
|
|
118
|
+
raise ValueError(f"Unknown identifier: {identifier}")
|
|
119
|
+
|
|
120
|
+
# Crea e restituisce l'istanza di ThingEventMessage
|
|
121
|
+
return cls(method=method, id=event_id, params=params_obj, version=version)
|
pymammotion/http/http.py
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
|
-
from typing import Generic, Literal, TypeVar
|
|
2
|
+
from typing import Generic, Literal, Optional, TypeVar
|
|
3
3
|
|
|
4
4
|
from aiohttp import ClientSession
|
|
5
|
+
from aiohttp.hdrs import AUTHORIZATION
|
|
5
6
|
from mashumaro import DataClassDictMixin
|
|
6
7
|
from mashumaro.mixins.orjson import DataClassORJSONMixin
|
|
7
8
|
|
|
9
|
+
from pymammotion.aliyun.dataclass.connect_response import Device
|
|
8
10
|
from pymammotion.const import (
|
|
9
11
|
MAMMOTION_CLIENT_ID,
|
|
10
12
|
MAMMOTION_CLIENT_SECRET,
|
|
@@ -25,7 +27,7 @@ class Response(DataClassDictMixin, Generic[DataT]):
|
|
|
25
27
|
class LoginResponseUserInformation(DataClassORJSONMixin):
|
|
26
28
|
areaCode: str
|
|
27
29
|
domainAbbreviation: str
|
|
28
|
-
email: str
|
|
30
|
+
email: Optional[str]
|
|
29
31
|
userId: str
|
|
30
32
|
userAccount: str
|
|
31
33
|
authType: str
|
|
@@ -44,11 +46,11 @@ class LoginResponseData(DataClassORJSONMixin):
|
|
|
44
46
|
jti: str
|
|
45
47
|
|
|
46
48
|
|
|
47
|
-
class
|
|
48
|
-
def __init__(self,
|
|
49
|
-
self.
|
|
50
|
-
self.
|
|
51
|
-
self.
|
|
49
|
+
class MammotionHTTP:
|
|
50
|
+
def __init__(self, login: LoginResponseData):
|
|
51
|
+
self._headers = dict()
|
|
52
|
+
self._headers["Authorization"] = f"Bearer {login.access_token}"
|
|
53
|
+
self.login = login
|
|
52
54
|
|
|
53
55
|
@classmethod
|
|
54
56
|
async def login(cls, session: ClientSession, username: str, password: str) -> Response[LoginResponseData]:
|
|
@@ -63,14 +65,13 @@ class LubaHTTP:
|
|
|
63
65
|
),
|
|
64
66
|
) as resp:
|
|
65
67
|
data = await resp.json()
|
|
66
|
-
print(data)
|
|
67
68
|
# TODO catch errors from mismatch user / password
|
|
68
69
|
# Assuming the data format matches the expected structure
|
|
69
70
|
login_response_data = LoginResponseData.from_dict(data["data"])
|
|
70
71
|
return Response(data=login_response_data, code=data["code"], msg=data["msg"])
|
|
71
72
|
|
|
72
73
|
|
|
73
|
-
async def connect_http(username: str, password: str) ->
|
|
74
|
+
async def connect_http(username: str, password: str) -> MammotionHTTP:
|
|
74
75
|
async with ClientSession(MAMMOTION_DOMAIN) as session:
|
|
75
|
-
login_response = await
|
|
76
|
-
return
|
|
76
|
+
login_response = await MammotionHTTP.login(session, username, password)
|
|
77
|
+
return MammotionHTTP(login_response.data)
|
|
@@ -4,14 +4,11 @@ from pymammotion.mammotion.commands.messages.ota import MessageOta
|
|
|
4
4
|
from pymammotion.mammotion.commands.messages.system import MessageSystem
|
|
5
5
|
from pymammotion.mammotion.commands.messages.video import MessageVideo
|
|
6
6
|
from pymammotion.proto import dev_net_pb2, luba_msg_pb2
|
|
7
|
-
from pymammotion.utility.device_type import DeviceType
|
|
8
7
|
|
|
9
8
|
|
|
10
9
|
class MammotionCommand(MessageSystem, MessageNavigation, MessageNetwork, MessageOta, MessageVideo):
|
|
11
10
|
"""MQTT commands for Luba."""
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
15
12
|
def __init__(self, device_name: str) -> None:
|
|
16
13
|
self._device_name = device_name
|
|
17
14
|
self._product_key = ""
|
|
@@ -26,7 +23,6 @@ class MammotionCommand(MessageSystem, MessageNavigation, MessageNetwork, Message
|
|
|
26
23
|
def set_device_product_key(self, product_key: str) -> None:
|
|
27
24
|
self._product_key = product_key
|
|
28
25
|
|
|
29
|
-
|
|
30
26
|
"""BLE commands for Luba."""
|
|
31
27
|
|
|
32
28
|
def send_todev_ble_sync(self, sync_type: int) -> bytes:
|
|
@@ -34,7 +34,10 @@ logger = logging.getLogger(__name__)
|
|
|
34
34
|
class MessageNavigation(AbstractMessage, ABC):
|
|
35
35
|
def get_msg_device(self, msg_type: MsgCmdType, msg_device: MsgDevice) -> MsgDevice:
|
|
36
36
|
"""Changes the rcver name if it's not a luba1."""
|
|
37
|
-
if
|
|
37
|
+
if (
|
|
38
|
+
not DeviceType.is_luba1(self.get_device_name(), self.get_device_product_key())
|
|
39
|
+
and msg_type == MsgCmdType.MSG_CMD_TYPE_NAV
|
|
40
|
+
):
|
|
38
41
|
return MsgDevice.DEV_NAVIGATION
|
|
39
42
|
return msg_device
|
|
40
43
|
|
|
@@ -3,15 +3,17 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
|
+
import base64
|
|
6
7
|
import codecs
|
|
7
8
|
import json
|
|
8
9
|
import logging
|
|
9
10
|
from abc import abstractmethod
|
|
10
11
|
from enum import Enum
|
|
11
|
-
from typing import Any, cast
|
|
12
|
+
from typing import Any, Callable, Optional, cast
|
|
12
13
|
from uuid import UUID
|
|
13
14
|
|
|
14
15
|
import betterproto
|
|
16
|
+
from aiohttp import ClientSession
|
|
15
17
|
from bleak import BleakClient
|
|
16
18
|
from bleak.backends.device import BLEDevice
|
|
17
19
|
from bleak.backends.service import BleakGATTCharacteristic, BleakGATTServiceCollection
|
|
@@ -23,11 +25,15 @@ from bleak_retry_connector import (
|
|
|
23
25
|
establish_connection,
|
|
24
26
|
)
|
|
25
27
|
|
|
28
|
+
from pymammotion.aliyun.cloud_gateway import CloudIOTGateway
|
|
26
29
|
from pymammotion.bluetooth import BleMessage
|
|
30
|
+
from pymammotion.const import MAMMOTION_DOMAIN
|
|
27
31
|
from pymammotion.data.model import RegionData
|
|
32
|
+
from pymammotion.data.model.account import Credentials
|
|
28
33
|
from pymammotion.data.model.device import MowingDevice
|
|
29
34
|
from pymammotion.data.mqtt.event import ThingEventMessage
|
|
30
35
|
from pymammotion.data.state_manager import StateManager
|
|
36
|
+
from pymammotion.http.http import MammotionHTTP, connect_http
|
|
31
37
|
from pymammotion.mammotion.commands.mammotion_command import MammotionCommand
|
|
32
38
|
from pymammotion.mqtt import MammotionMQTT
|
|
33
39
|
from pymammotion.proto.luba_msg import LubaMsg
|
|
@@ -71,7 +77,7 @@ _LOGGER = logging.getLogger(__name__)
|
|
|
71
77
|
def slashescape(err):
|
|
72
78
|
"""Escape a slash character."""
|
|
73
79
|
# print err, dir(err), err.start, err.end, err.object[:err.start]
|
|
74
|
-
thebyte = err.object[err.start: err.end]
|
|
80
|
+
thebyte = err.object[err.start : err.end]
|
|
75
81
|
repl = "\\x" + hex(ord(thebyte))[2:]
|
|
76
82
|
return (repl, err.end)
|
|
77
83
|
|
|
@@ -106,6 +112,12 @@ async def _handle_retry(fut: asyncio.Future[None], func, command: bytes) -> None
|
|
|
106
112
|
await func(command)
|
|
107
113
|
|
|
108
114
|
|
|
115
|
+
async def _handle_retry_cloud(fut: asyncio.Future[None], func, iotId: str, command: bytes) -> None:
|
|
116
|
+
"""Handle a retry."""
|
|
117
|
+
if not fut.done():
|
|
118
|
+
func(iotId, command)
|
|
119
|
+
|
|
120
|
+
|
|
109
121
|
class ConnectionPreference(Enum):
|
|
110
122
|
"""Enum for connection preference."""
|
|
111
123
|
|
|
@@ -118,20 +130,83 @@ class MammotionDevice:
|
|
|
118
130
|
"""Represents a Mammotion device."""
|
|
119
131
|
|
|
120
132
|
_ble_device: MammotionBaseBLEDevice | None = None
|
|
133
|
+
_cloud_device: MammotionBaseCloudDevice | None = None
|
|
134
|
+
_devices_list = []
|
|
121
135
|
|
|
122
136
|
def __init__(
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
137
|
+
self,
|
|
138
|
+
ble_device: BLEDevice,
|
|
139
|
+
cloud_credentials: Credentials | None = None,
|
|
140
|
+
preference: ConnectionPreference = ConnectionPreference.EITHER,
|
|
126
141
|
) -> None:
|
|
127
142
|
"""Initialize MammotionDevice."""
|
|
128
143
|
if ble_device:
|
|
129
144
|
self._ble_device = MammotionBaseBLEDevice(ble_device)
|
|
130
145
|
self._preference = preference
|
|
131
146
|
|
|
147
|
+
if cloud_credentials:
|
|
148
|
+
self.initiate_cloud_connection(cloud_credentials.account_id or cloud_credentials.email, cloud_credentials.password)
|
|
149
|
+
|
|
150
|
+
async def initiate_cloud_connection(self, account: str, password: str):
|
|
151
|
+
cloud_client = await self.login(account, password)
|
|
152
|
+
|
|
153
|
+
_mammotion_mqtt = MammotionMQTT(region_id=cloud_client._region.data.regionId,
|
|
154
|
+
product_key=cloud_client._aep_response.data.productKey,
|
|
155
|
+
device_name=cloud_client._aep_response.data.deviceName,
|
|
156
|
+
device_secret=cloud_client._aep_response.data.deviceSecret,
|
|
157
|
+
iot_token=cloud_client._session_by_authcode_response.data.iotToken,
|
|
158
|
+
client_id=cloud_client._client_id)
|
|
159
|
+
|
|
160
|
+
_mammotion_mqtt._cloud_client = cloud_client
|
|
161
|
+
_mammotion_mqtt.connect_async()
|
|
162
|
+
|
|
163
|
+
self._devices_list = []
|
|
164
|
+
for device in cloud_client._listing_dev_by_account_response.data.data:
|
|
165
|
+
if (device.deviceName.startswith(("Luba-", "Yuka-"))):
|
|
166
|
+
dev = MammotionBaseCloudDevice(
|
|
167
|
+
mqtt_client=_mammotion_mqtt,
|
|
168
|
+
iot_id=device.iotId,
|
|
169
|
+
device_name=device.deviceName,
|
|
170
|
+
nick_name=device.nickName
|
|
171
|
+
)
|
|
172
|
+
self._devices_list.append(dev)
|
|
173
|
+
|
|
174
|
+
@staticmethod
|
|
175
|
+
async def login(account: str, password: str) -> CloudIOTGateway:
|
|
176
|
+
"""Login to mammotion cloud."""
|
|
177
|
+
cloud_client = CloudIOTGateway()
|
|
178
|
+
async with ClientSession(MAMMOTION_DOMAIN) as session:
|
|
179
|
+
mammotion_http = await connect_http(account, password)
|
|
180
|
+
country_code = mammotion_http.login.userInformation.domainAbbreviation
|
|
181
|
+
_LOGGER.debug("CountryCode: " + country_code)
|
|
182
|
+
_LOGGER.debug("AuthCode: " + mammotion_http.login.authorization_code)
|
|
183
|
+
cloud_client.get_region(country_code, mammotion_http.login.authorization_code)
|
|
184
|
+
await cloud_client.connect()
|
|
185
|
+
await cloud_client.login_by_oauth(country_code, mammotion_http.login.authorization_code)
|
|
186
|
+
cloud_client.aep_handle()
|
|
187
|
+
cloud_client.session_by_auth_code()
|
|
188
|
+
|
|
189
|
+
cloud_client.list_binding_by_account()
|
|
190
|
+
return cloud_client
|
|
191
|
+
|
|
192
|
+
|
|
132
193
|
async def send_command(self, key: str):
|
|
133
194
|
"""Send a command to the device."""
|
|
134
|
-
|
|
195
|
+
if self._preference is ConnectionPreference.BLUETOOTH:
|
|
196
|
+
return await self._ble_device.command(key)
|
|
197
|
+
if self._preference is ConnectionPreference.WIFI:
|
|
198
|
+
return await self._cloud_device.command(key)
|
|
199
|
+
# TODO work with both with EITHER
|
|
200
|
+
|
|
201
|
+
async def send_command_with_args(self, key, kwargs):
|
|
202
|
+
"""Send a command with args to the device."""
|
|
203
|
+
if self._preference is ConnectionPreference.BLUETOOTH:
|
|
204
|
+
return await self._ble_device.command(key, **kwargs)
|
|
205
|
+
if self._preference is ConnectionPreference.WIFI:
|
|
206
|
+
return await self._cloud_device.command(key, **kwargs)
|
|
207
|
+
# TODO work with both with EITHER
|
|
208
|
+
|
|
209
|
+
|
|
135
210
|
|
|
136
211
|
|
|
137
212
|
def has_field(message: betterproto.Message) -> bool:
|
|
@@ -318,8 +393,10 @@ class MammotionBaseDevice:
|
|
|
318
393
|
|
|
319
394
|
await self._send_command_with_args("get_hash_response", total_frame=1, current_frame=1)
|
|
320
395
|
|
|
321
|
-
await self._send_command_with_args(
|
|
322
|
-
|
|
396
|
+
await self._send_command_with_args(
|
|
397
|
+
"get_area_name_list", device_id=self.luba_msg.device.net.toapp_wifi_iot_status.devicename
|
|
398
|
+
)
|
|
399
|
+
|
|
323
400
|
# sub_cmd 3 is job hashes??
|
|
324
401
|
# sub_cmd 4 is dump location (yuka)
|
|
325
402
|
# jobs list
|
|
@@ -545,7 +622,6 @@ class MammotionBaseBLEDevice(MammotionBaseDevice):
|
|
|
545
622
|
new_msg = LubaMsg().parse(data)
|
|
546
623
|
if betterproto.serialized_on_wire(new_msg.net):
|
|
547
624
|
if new_msg.net.todev_ble_sync != 0 or has_field(new_msg.net.toapp_wifi_iot_status):
|
|
548
|
-
|
|
549
625
|
if has_field(new_msg.net.toapp_wifi_iot_status) and self._commands.get_device_product_key() == "":
|
|
550
626
|
self._commands.set_device_product_key(new_msg.net.toapp_wifi_iot_status.productkey)
|
|
551
627
|
|
|
@@ -713,58 +789,149 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
|
|
|
713
789
|
"""Base class for Mammotion Cloud devices."""
|
|
714
790
|
|
|
715
791
|
def __init__(
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
792
|
+
self,
|
|
793
|
+
mqtt_client: MammotionMQTT,
|
|
794
|
+
iot_id: str,
|
|
795
|
+
device_name: str,
|
|
796
|
+
nick_name: str,
|
|
797
|
+
**kwargs: Any,
|
|
722
798
|
) -> None:
|
|
723
799
|
"""Initialize MammotionBaseCloudDevice."""
|
|
724
800
|
super().__init__()
|
|
801
|
+
self._ble_sync_task = None
|
|
802
|
+
self.is_ready = False
|
|
725
803
|
self._mqtt_client = mqtt_client
|
|
726
804
|
self.iot_id = iot_id
|
|
727
805
|
self.nick_name = nick_name
|
|
728
806
|
self._command_futures = {}
|
|
729
807
|
self._commands: MammotionCommand = MammotionCommand(device_name)
|
|
730
|
-
self.
|
|
808
|
+
self.currentID = ""
|
|
809
|
+
self.on_ready_callback: Optional[Callable[[], None]] = None
|
|
810
|
+
self._operation_lock = asyncio.Lock()
|
|
811
|
+
|
|
812
|
+
self._mqtt_client.on_connected = self.on_connected
|
|
813
|
+
self._mqtt_client.on_disconnected = self.on_disconnected
|
|
814
|
+
self._mqtt_client.on_message = self._on_mqtt_message
|
|
815
|
+
self._mqtt_client.on_ready = self.on_ready
|
|
816
|
+
if self._mqtt_client.is_connected:
|
|
817
|
+
self._ble_sync()
|
|
818
|
+
self.run_periodic_sync_task()
|
|
819
|
+
|
|
820
|
+
# temporary for testing only
|
|
821
|
+
# self._start_sync_task = self.loop.call_later(30, lambda: asyncio.ensure_future(self.start_sync(0)))
|
|
822
|
+
|
|
823
|
+
def on_ready(self):
|
|
824
|
+
"""Callback for when MQTT is subscribed to events."""
|
|
825
|
+
if self.on_ready_callback:
|
|
826
|
+
self.on_ready_callback()
|
|
827
|
+
|
|
828
|
+
def on_connected(self):
|
|
829
|
+
"""Callback for when MQTT connects."""
|
|
830
|
+
self._ble_sync()
|
|
831
|
+
self.run_periodic_sync_task()
|
|
832
|
+
|
|
833
|
+
def on_disconnected(self):
|
|
834
|
+
"""Callback for when MQTT disconnects."""
|
|
731
835
|
|
|
732
|
-
def
|
|
836
|
+
def _ble_sync(self):
|
|
837
|
+
command_bytes = self._commands.send_todev_ble_sync(3)
|
|
838
|
+
self._mqtt_client.get_cloud_client().send_cloud_command(self.iot_id, command_bytes)
|
|
839
|
+
|
|
840
|
+
async def run_periodic_sync_task(self) -> None:
|
|
841
|
+
"""Send ble sync to robot."""
|
|
842
|
+
try:
|
|
843
|
+
self._ble_sync()
|
|
844
|
+
finally:
|
|
845
|
+
self.schedule_ble_sync()
|
|
846
|
+
|
|
847
|
+
def schedule_ble_sync(self):
|
|
848
|
+
"""Periodically sync to keep connection alive."""
|
|
849
|
+
if self._mqtt_client is not None and self._mqtt_client.is_connected:
|
|
850
|
+
self._ble_sync_task = self.loop.call_later(
|
|
851
|
+
160, lambda: asyncio.ensure_future(self.run_periodic_sync_task())
|
|
852
|
+
)
|
|
853
|
+
|
|
854
|
+
def _on_mqtt_message(self, topic: str, payload: str, iot_id: str) -> None:
|
|
733
855
|
"""Handle incoming MQTT messages."""
|
|
734
856
|
_LOGGER.debug("MQTT message received on topic %s: %s", topic, payload)
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
if not future.done():
|
|
741
|
-
future.set_result(payload)
|
|
857
|
+
|
|
858
|
+
json_str = json.dumps(payload)
|
|
859
|
+
payload = json.loads(json_str)
|
|
860
|
+
|
|
861
|
+
self._handle_mqtt_message(topic, payload)
|
|
742
862
|
|
|
743
863
|
async def _send_command(self, key: str, retry: int | None = None) -> bytes | None:
|
|
744
864
|
"""Send command to device via MQTT and read response."""
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
self._command_futures[message_id] = future
|
|
865
|
+
if self._operation_lock.locked():
|
|
866
|
+
_LOGGER.debug("%s: Operation already in progress, waiting for it to complete;", self.nick_name)
|
|
867
|
+
|
|
868
|
+
async with self._operation_lock:
|
|
750
869
|
try:
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
870
|
+
command_bytes = getattr(self._commands, key)()
|
|
871
|
+
return await self._send_command_locked(key, command_bytes)
|
|
872
|
+
except Exception as ex:
|
|
873
|
+
_LOGGER.exception("%s: error in sending command - %s", self.nick_name, ex)
|
|
874
|
+
raise
|
|
875
|
+
|
|
876
|
+
async def _send_command_locked(self, key: str, command: bytes) -> bytes:
|
|
877
|
+
"""Send command to device and read response."""
|
|
878
|
+
try:
|
|
879
|
+
return await self._execute_command_locked(key, command)
|
|
880
|
+
except Exception as ex:
|
|
881
|
+
# Disconnect so we can reset state and try again
|
|
882
|
+
await asyncio.sleep(DBUS_ERROR_BACKOFF_TIME)
|
|
883
|
+
_LOGGER.debug(
|
|
884
|
+
"%s: error in _send_command_locked: %s",
|
|
885
|
+
self.nick_name,
|
|
886
|
+
ex,
|
|
887
|
+
)
|
|
888
|
+
raise
|
|
889
|
+
|
|
890
|
+
async def _execute_command_locked(self, key: str, command: bytes) -> bytes:
|
|
891
|
+
"""Execute command and read response."""
|
|
892
|
+
assert self._mqtt_client is not None
|
|
893
|
+
self._notify_future = self.loop.create_future()
|
|
894
|
+
self._key = key
|
|
895
|
+
_LOGGER.debug("%s: Sending command: %s", self.nick_name, key)
|
|
896
|
+
self._mqtt_client.get_cloud_client().send_cloud_command(self.iot_id, command)
|
|
897
|
+
|
|
898
|
+
retry_handle = self.loop.call_at(
|
|
899
|
+
self.loop.time() + 20,
|
|
900
|
+
lambda: asyncio.ensure_future(
|
|
901
|
+
_handle_retry_cloud(
|
|
902
|
+
self._notify_future, self._mqtt_client.get_cloud_client().send_cloud_command, self.iot_id, command
|
|
903
|
+
)
|
|
904
|
+
),
|
|
905
|
+
)
|
|
906
|
+
timeout = 20
|
|
907
|
+
timeout_handle = self.loop.call_at(self.loop.time() + timeout, _handle_timeout, self._notify_future)
|
|
908
|
+
timeout_expired = False
|
|
909
|
+
try:
|
|
910
|
+
notify_msg = await self._notify_future
|
|
911
|
+
except asyncio.TimeoutError:
|
|
912
|
+
timeout_expired = True
|
|
913
|
+
raise
|
|
914
|
+
finally:
|
|
915
|
+
if not timeout_expired:
|
|
916
|
+
timeout_handle.cancel()
|
|
917
|
+
retry_handle.cancel()
|
|
918
|
+
self._notify_future = None
|
|
919
|
+
|
|
920
|
+
_LOGGER.debug("%s: Message received", self.nick_name)
|
|
921
|
+
|
|
922
|
+
return notify_msg
|
|
755
923
|
|
|
756
924
|
async def _send_command_with_args(self, key: str, **kwargs: any) -> bytes | None:
|
|
757
925
|
"""Send command with arguments to device via MQTT and read response."""
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
if message_id != "":
|
|
762
|
-
self._command_futures[message_id] = future
|
|
926
|
+
if self._operation_lock.locked():
|
|
927
|
+
_LOGGER.debug("%s: Operation already in progress, waiting for it to complete;", self.nick_name)
|
|
928
|
+
async with self._operation_lock:
|
|
763
929
|
try:
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
930
|
+
command_bytes = getattr(self._commands, key)(**kwargs)
|
|
931
|
+
return await self._send_command_locked(key, command_bytes)
|
|
932
|
+
except Exception as ex:
|
|
933
|
+
_LOGGER.exception("%s: error in sending command - %s", self.nick_name, ex)
|
|
934
|
+
raise
|
|
768
935
|
|
|
769
936
|
def _extract_message_id(self, payload: dict) -> str:
|
|
770
937
|
"""Extract the message ID from the payload."""
|
|
@@ -782,10 +949,21 @@ class MammotionBaseCloudDevice(MammotionBaseDevice):
|
|
|
782
949
|
def _parse_mqtt_response(self, topic: str, payload: dict) -> None:
|
|
783
950
|
"""Parse the MQTT response."""
|
|
784
951
|
if topic.endswith("/app/down/thing/events"):
|
|
785
|
-
event
|
|
952
|
+
_LOGGER.debug("Thing event received")
|
|
953
|
+
event = ThingEventMessage.from_dicts(payload)
|
|
786
954
|
params = event.params
|
|
787
955
|
if params.identifier == "device_protobuf_msg_event":
|
|
788
|
-
|
|
956
|
+
_LOGGER.debug("Protobuf event")
|
|
957
|
+
binary_data = base64.b64decode(params.value.get("content", ""))
|
|
958
|
+
self._update_raw_data(cast(bytes, binary_data))
|
|
959
|
+
new_msg = LubaMsg().parse(cast(bytes, binary_data))
|
|
960
|
+
if self._notify_future and not self._notify_future.done():
|
|
961
|
+
self._notify_future.set_result(new_msg)
|
|
962
|
+
asyncio.run(self._state_manager.notification(new_msg))
|
|
963
|
+
|
|
964
|
+
def _handle_mqtt_message(self, topic: str, payload: dict) -> None:
|
|
965
|
+
"""Async handler for incoming MQTT messages."""
|
|
966
|
+
self._parse_mqtt_response(topic=topic, payload=payload)
|
|
789
967
|
|
|
790
968
|
async def _disconnect(self):
|
|
791
969
|
"""Disconnect the MQTT client."""
|
|
@@ -8,7 +8,7 @@ from logging import getLogger
|
|
|
8
8
|
from typing import Callable, Optional, cast
|
|
9
9
|
|
|
10
10
|
from linkkit.linkkit import LinkKit
|
|
11
|
-
from paho.mqtt.client import
|
|
11
|
+
from paho.mqtt.client import MQTTMessage
|
|
12
12
|
|
|
13
13
|
from pymammotion.aliyun.cloud_gateway import CloudIOTGateway
|
|
14
14
|
from pymammotion.data.mqtt.event import ThingEventMessage
|
|
@@ -22,8 +22,6 @@ logger = getLogger(__name__)
|
|
|
22
22
|
class MammotionMQTT:
|
|
23
23
|
"""MQTT client for pymammotion."""
|
|
24
24
|
|
|
25
|
-
_cloud_client = None
|
|
26
|
-
|
|
27
25
|
def __init__(
|
|
28
26
|
self,
|
|
29
27
|
region_id: str,
|
|
@@ -35,8 +33,11 @@ class MammotionMQTT:
|
|
|
35
33
|
):
|
|
36
34
|
"""Create instance of MammotionMQTT."""
|
|
37
35
|
super().__init__()
|
|
38
|
-
|
|
36
|
+
self._cloud_client = None
|
|
37
|
+
self.is_connected = False
|
|
38
|
+
self.is_ready = False
|
|
39
39
|
self.on_connected: Optional[Callable[[], None]] = None
|
|
40
|
+
self.on_ready: Optional[Callable[[], None]] = None
|
|
40
41
|
self.on_error: Optional[Callable[[str], None]] = None
|
|
41
42
|
self.on_disconnected: Optional[Callable[[], None]] = None
|
|
42
43
|
self.on_message: Optional[Callable[[str, str, str], None]] = None
|
|
@@ -73,41 +74,19 @@ class MammotionMQTT:
|
|
|
73
74
|
self._linkkit_client.on_disconnect = self._on_disconnect
|
|
74
75
|
self._linkkit_client.on_thing_enable = self._thing_on_thing_enable
|
|
75
76
|
self._linkkit_client.on_topic_message = self._thing_on_topic_message
|
|
76
|
-
# self._mqtt_host = "public.itls.eu-central-1.aliyuncs.com"
|
|
77
77
|
self._mqtt_host = f"{self._product_key}.iot-as-mqtt.{region_id}.aliyuncs.com"
|
|
78
78
|
|
|
79
|
-
self._client = Client(
|
|
80
|
-
client_id=self._mqtt_client_id,
|
|
81
|
-
protocol=MQTTv311,
|
|
82
|
-
)
|
|
83
|
-
self._client.on_message = self._on_message
|
|
84
|
-
self._client.on_connect = self._on_connect
|
|
85
|
-
self._client.on_disconnect = self._on_disconnect
|
|
86
|
-
self._client.username_pw_set(self._mqtt_username, self._mqtt_password)
|
|
87
|
-
self._client.enable_logger(logger.getChild("paho"))
|
|
88
|
-
|
|
89
|
-
# region Connection handling
|
|
90
|
-
def connect(self):
|
|
91
|
-
"""Connect to MQTT Server."""
|
|
92
|
-
logger.info("Connecting...")
|
|
93
|
-
self._client.connect(host=self._mqtt_host)
|
|
94
|
-
self._client.loop_forever()
|
|
95
|
-
|
|
96
79
|
def connect_async(self):
|
|
97
80
|
"""Connect async to MQTT Server."""
|
|
98
81
|
logger.info("Connecting...")
|
|
99
82
|
self._linkkit_client.thing_setup()
|
|
100
83
|
self._linkkit_client.connect_async()
|
|
101
|
-
# self._client.connect_async(host=self._mqtt_host)
|
|
102
|
-
# self._client.loop_start()
|
|
103
84
|
self._linkkit_client.start_worker_loop()
|
|
104
85
|
|
|
105
86
|
def disconnect(self):
|
|
106
87
|
"""Disconnect from MQTT Server."""
|
|
107
88
|
logger.info("Disconnecting...")
|
|
108
89
|
self._linkkit_client.disconnect()
|
|
109
|
-
self._client.disconnect()
|
|
110
|
-
self._client.loop_stop()
|
|
111
90
|
|
|
112
91
|
def _thing_on_thing_enable(self, user_data):
|
|
113
92
|
"""Is called when Thing is enabled."""
|
|
@@ -139,6 +118,9 @@ class MammotionMQTT:
|
|
|
139
118
|
),
|
|
140
119
|
)
|
|
141
120
|
|
|
121
|
+
if self.on_ready:
|
|
122
|
+
self.is_ready = True
|
|
123
|
+
self.on_ready()
|
|
142
124
|
# self._linkkit_client.query_ota_firmware()
|
|
143
125
|
# command = MammotionCommand(device_name="Luba")
|
|
144
126
|
# self._cloud_client.send_cloud_command(command.get_report_cfg())
|
|
@@ -158,39 +140,18 @@ class MammotionMQTT:
|
|
|
158
140
|
|
|
159
141
|
def _thing_on_connect(self, session_flag, rc, user_data):
|
|
160
142
|
"""Is called on thing connect."""
|
|
143
|
+
self.is_connected = True
|
|
144
|
+
if self.on_connected is not None:
|
|
145
|
+
self.on_connected()
|
|
161
146
|
logger.debug("on_connect, session_flag:%d, rc:%d", session_flag, rc)
|
|
162
147
|
|
|
163
148
|
# self._linkkit_client.subscribe_topic(f"/sys/{self._product_key}/{self._device_name}/#")
|
|
164
149
|
|
|
165
|
-
def _on_connect(self, _client, _userdata, _flags: dict, rc: int):
|
|
166
|
-
"""Is called when on connect."""
|
|
167
|
-
if rc == 0:
|
|
168
|
-
logger.debug("Connected")
|
|
169
|
-
self._client.subscribe(f"/sys/{self._product_key}/{self._device_name}/#")
|
|
170
|
-
self._client.subscribe(f"/sys/{self._product_key}/{self._device_name}/app/down/account/bind_reply")
|
|
171
|
-
|
|
172
|
-
self._client.publish(
|
|
173
|
-
f"/sys/{self._product_key}/{self._device_name}/app/up/account/bind",
|
|
174
|
-
json.dumps(
|
|
175
|
-
{
|
|
176
|
-
"id": "msgid1",
|
|
177
|
-
"version": "1.0",
|
|
178
|
-
"request": {"clientId": self._mqtt_username},
|
|
179
|
-
"params": {"iotToken": self._iot_token},
|
|
180
|
-
}
|
|
181
|
-
),
|
|
182
|
-
)
|
|
183
|
-
|
|
184
|
-
if self.on_connected:
|
|
185
|
-
self.on_connected()
|
|
186
|
-
else:
|
|
187
|
-
logger.error("Could not connect %s", connack_string(rc))
|
|
188
|
-
if self.on_error:
|
|
189
|
-
self.on_error(connack_string(rc))
|
|
190
|
-
|
|
191
150
|
def _on_disconnect(self, _client, _userdata, rc: int):
|
|
192
151
|
"""Is called on disconnect."""
|
|
193
152
|
logger.info("Disconnected")
|
|
153
|
+
self.is_connected = False
|
|
154
|
+
self.is_ready = False
|
|
194
155
|
logger.debug(rc)
|
|
195
156
|
if self.on_disconnected:
|
|
196
157
|
self.on_disconnected()
|
|
@@ -257,6 +257,7 @@ def device_mode(value) -> str:
|
|
|
257
257
|
|
|
258
258
|
class PosType(IntEnum):
|
|
259
259
|
"""Position of the robot."""
|
|
260
|
+
|
|
260
261
|
AREA_BORDER_ON = 7
|
|
261
262
|
AREA_INSIDE = 1
|
|
262
263
|
AREA_OUT = 0
|
|
@@ -268,4 +269,3 @@ class PosType(IntEnum):
|
|
|
268
269
|
OBS_ON = 2
|
|
269
270
|
TURN_AREA_INSIDE = 4
|
|
270
271
|
VIRTUAL_INSIDE = 6
|
|
271
|
-
|
pymammotion/utility/map.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import math
|
|
2
|
+
|
|
2
3
|
import numpy as np
|
|
3
4
|
|
|
4
5
|
from pymammotion.data.model.location import Point
|
|
@@ -8,11 +9,11 @@ class CoordinateConverter:
|
|
|
8
9
|
def __init__(self, latitude_rad: float, longitude_rad: float):
|
|
9
10
|
# Initialize constants
|
|
10
11
|
self.WGS84A = 6378137.0
|
|
11
|
-
self.f_ = 3.
|
|
12
|
+
self.f_ = 3.3528106647474805e-21
|
|
12
13
|
self.b_ = (1.0 - self.f_) * self.WGS84A
|
|
13
14
|
self.e2_ = (2.0 - self.f_) * self.f_
|
|
14
15
|
self.e2m_ = (1.0 - self.f_) * (1.0 - self.f_)
|
|
15
|
-
self.ep2_ = ((self.WGS84A
|
|
16
|
+
self.ep2_ = ((self.WGS84A**2) - (self.b_**2)) / (self.b_**2)
|
|
16
17
|
|
|
17
18
|
# Initialize rotation matrix
|
|
18
19
|
self.R_ = np.zeros((3, 3))
|
|
@@ -31,7 +32,7 @@ class CoordinateConverter:
|
|
|
31
32
|
sin_lon = math.sin(lon_rad)
|
|
32
33
|
cos_lon = math.cos(lon_rad)
|
|
33
34
|
|
|
34
|
-
sqrt = self.WGS84A / math.sqrt(1.0 - (self.e2_ * (sin_lat
|
|
35
|
+
sqrt = self.WGS84A / math.sqrt(1.0 - (self.e2_ * (sin_lat**2)))
|
|
35
36
|
d3 = sqrt * cos_lat
|
|
36
37
|
|
|
37
38
|
self.x0_ = d3 * cos_lon
|
|
@@ -62,7 +63,10 @@ class CoordinateConverter:
|
|
|
62
63
|
cos_lat = math.cos(atan2_lat)
|
|
63
64
|
|
|
64
65
|
lon = math.atan2(d4, d3) * 180.0 / math.pi
|
|
65
|
-
lat =
|
|
66
|
+
lat = (
|
|
67
|
+
math.atan2(d5 + self.ep2_ * self.b_ * (sin_lat**3), hypot - self.e2_ * self.WGS84A * (cos_lat**3))
|
|
68
|
+
* 180.0
|
|
69
|
+
/ math.pi
|
|
70
|
+
)
|
|
66
71
|
|
|
67
72
|
return Point(latitude=lat, longitude=lon)
|
|
68
|
-
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
pymammotion/__init__.py,sha256=
|
|
1
|
+
pymammotion/__init__.py,sha256=kmnjdt3AEMejIz5JK7h1tTJj5ZriAgKwZBa3ScA4-Ao,1516
|
|
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=7cU-40s70H8q5D53Qxnul9FenzIgzXZZXN9BEEDKMoo,18308
|
|
4
4
|
pymammotion/aliyun/cloud_service.py,sha256=YWcKuKK6iRWy5mTnBYgHxcCusiRGGzQt3spSf7dGDss,2183
|
|
5
5
|
pymammotion/aliyun/dataclass/aep_response.py,sha256=EPuTU8uN0vkbPY_8MdBKAxASSBI9r021kODeOqrcdtw,353
|
|
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=qU29Jdpjvxv6mfp4tC3V6UCCkjBwdwttuGfV2C6ELo4,878
|
|
8
8
|
pymammotion/aliyun/dataclass/login_by_oauth_response.py,sha256=6TQYAMyQ1jf_trsnTST007qlNXve3BqsvpV0Dwp7CFA,1245
|
|
9
9
|
pymammotion/aliyun/dataclass/regions_response.py,sha256=0Kcly3OPMIEGK36O0OGFvZrHl6nJfnKzNeC4RPXvFhU,591
|
|
10
10
|
pymammotion/aliyun/dataclass/session_by_authcode_response.py,sha256=wLGSX2nHkA7crmyYeE_dYly_lDtoYWAiIZjQ0C6m44o,358
|
|
@@ -20,36 +20,37 @@ pymammotion/bluetooth/data/notifydata.py,sha256=N1bphpueWUWbsWUcpZmMGt2CyCgLcKAF
|
|
|
20
20
|
pymammotion/const.py,sha256=3plR6t5sFVhx3LoUbW5PE2gqIANoD-fSm-T0q8uIwIU,336
|
|
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/
|
|
24
|
-
pymammotion/data/model/
|
|
23
|
+
pymammotion/data/model/account.py,sha256=rgYV61aTY2DlxLbY19Wa3Bp76KGos0_dnfRryvTXa80,118
|
|
24
|
+
pymammotion/data/model/device.py,sha256=jIroL0wEAbxnVQWzd4vKYTUvWwqmUDF4VPss6T7J1bs,10390
|
|
25
|
+
pymammotion/data/model/device_config.py,sha256=E3rhLvUH4BuWEpBfylBYBEwn4G8u7c0QbKxWRElw3Sg,177
|
|
25
26
|
pymammotion/data/model/enums.py,sha256=tD_vYsxstOV_lUkYF9uWkrjVOgAJPNnGevy_xmiu3WE,1558
|
|
26
27
|
pymammotion/data/model/excute_boarder_params.py,sha256=kadSth4y-VXlXIZ6R-Ng-kDvBbM-3YRr8bmR86qR0U0,1355
|
|
27
28
|
pymammotion/data/model/execute_boarder.py,sha256=oDb2h5tFtOQIa8OCNYaDugqCgCZBLjQRzQTNVcJVAGQ,1072
|
|
28
29
|
pymammotion/data/model/generate_route_information.py,sha256=5w1MM1-gXGXb_AoEap_I5xTxAFbNSzXuqQFEkw4NmDs,4301
|
|
29
30
|
pymammotion/data/model/hash_list.py,sha256=z4y0mzbW8LQ5nsaHbMlt1y6uS4suB4D_VljAyIEjFR0,1171
|
|
30
|
-
pymammotion/data/model/location.py,sha256=
|
|
31
|
-
pymammotion/data/model/mowing_modes.py,sha256=
|
|
31
|
+
pymammotion/data/model/location.py,sha256=Fw6r4PFCjBmbUPsI6b0Swke6RgUlzoW7NXjR8EGjgzk,784
|
|
32
|
+
pymammotion/data/model/mowing_modes.py,sha256=2GAF-xaHpv6CSGobYoNtfSi2if8_jW0nonCqN50SNx0,665
|
|
32
33
|
pymammotion/data/model/plan.py,sha256=7JvqAo0a9Yg1Vtifd4J3Dx3StEppxrMOfmq2-877kYg,2891
|
|
33
34
|
pymammotion/data/model/rapid_state.py,sha256=_e9M-65AbkvIqXyMYzLKBxbNvpso42qD8R-JSt66THY,986
|
|
34
35
|
pymammotion/data/model/region_data.py,sha256=75xOTM1qeRbSROp53eIczw3yCmYM9DgMjMh8qE9xkKo,2880
|
|
35
|
-
pymammotion/data/model/report_info.py,sha256=
|
|
36
|
+
pymammotion/data/model/report_info.py,sha256=1Rj_yRZ9apWX296QHae5prdObDbs32bsQf9rzMz2KEQ,4458
|
|
36
37
|
pymammotion/data/mqtt/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
37
|
-
pymammotion/data/mqtt/event.py,sha256=
|
|
38
|
+
pymammotion/data/mqtt/event.py,sha256=7hq-dl-HdKJB-TQ2kKfYfrebR_8qWE_lF2nQLlPyphU,3437
|
|
38
39
|
pymammotion/data/mqtt/properties.py,sha256=HkBPghr26L9_b4QaOi1DtPgb0UoPIOGSe9wb3kgnM6Y,2815
|
|
39
40
|
pymammotion/data/mqtt/status.py,sha256=zqnlo-MzejEQZszl0i0Wucoc3E76x6UtI9JLxoBnu54,1067
|
|
40
41
|
pymammotion/data/state_manager.py,sha256=ItihtkmJCeIdXOYD3YRBp241cP29oMABbSISyC5A7tw,2805
|
|
41
42
|
pymammotion/event/__init__.py,sha256=mgATR6vPHACNQ-0zH5fi7NdzeTCDV1CZyaWPmtUusi8,115
|
|
42
43
|
pymammotion/event/event.py,sha256=Fy5-I1p92AO_D67VW4eHQqA4pOt7MZsrP--tVfIVUz8,1820
|
|
43
44
|
pymammotion/http/_init_.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
44
|
-
pymammotion/http/http.py,sha256=
|
|
45
|
+
pymammotion/http/http.py,sha256=VoAuDTciiueo8JRjqoZHf8wa0cfvIsFizOp8ISVHRs4,2361
|
|
45
46
|
pymammotion/mammotion/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
46
47
|
pymammotion/mammotion/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
47
48
|
pymammotion/mammotion/commands/abstract_message.py,sha256=nw6r7694yzl7iJKqRqhLmAPRjd_TL_Xo_-JXq2_a_ug,222
|
|
48
|
-
pymammotion/mammotion/commands/mammotion_command.py,sha256=
|
|
49
|
+
pymammotion/mammotion/commands/mammotion_command.py,sha256=ppeWOBO6ifwyajpatIq4RLLpo82-XiU-mVtL-Q9W4Qc,1490
|
|
49
50
|
pymammotion/mammotion/commands/messages/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
50
51
|
pymammotion/mammotion/commands/messages/driver.py,sha256=IFtmp9gJ3p3U2xtTgWVpKBMbnVnUht6Ioih17po6Hr4,3783
|
|
51
52
|
pymammotion/mammotion/commands/messages/media.py,sha256=ps0l06CXy5Ej--gTNCsyKttwo7yHLVrJUpn-wNJYecs,1150
|
|
52
|
-
pymammotion/mammotion/commands/messages/navigation.py,sha256
|
|
53
|
+
pymammotion/mammotion/commands/messages/navigation.py,sha256=-qit4SrtMpnPr0qsub_HD0pzKMlYsyzspW5uf1Ym008,23217
|
|
53
54
|
pymammotion/mammotion/commands/messages/network.py,sha256=1a0BIhaBhBuhqYaK5EUqLbDgfzjzsIGrXS32wMKtxOU,8316
|
|
54
55
|
pymammotion/mammotion/commands/messages/ota.py,sha256=XkeuWBZtpYMMBze6r8UN7dJXbe2FxUNGNnjwBpXJKM0,1240
|
|
55
56
|
pymammotion/mammotion/commands/messages/system.py,sha256=nQ-g-0ESDQ2YBB6JW7IF4Q1bmTbX_baA4dOw2P3GixI,10957
|
|
@@ -57,9 +58,9 @@ pymammotion/mammotion/commands/messages/video.py,sha256=_8lJsU4sLm2CGnc7RDkueA0A
|
|
|
57
58
|
pymammotion/mammotion/control/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
58
59
|
pymammotion/mammotion/control/joystick.py,sha256=EWV20MMzQuhbLlNlXbsyZKSEpeM7x1CQL7saU4Pn0-g,6165
|
|
59
60
|
pymammotion/mammotion/devices/__init__.py,sha256=T72jt0ejtMjo1rPmn_FeMF3pmp0LLeRRpc9WcDKEYYY,126
|
|
60
|
-
pymammotion/mammotion/devices/mammotion.py,sha256=
|
|
61
|
+
pymammotion/mammotion/devices/mammotion.py,sha256=HooHk3GFCwIdZ79Fb4isMR2u_B1qCWpziSzC2nuTpFU,38641
|
|
61
62
|
pymammotion/mqtt/__init__.py,sha256=Ocs5e-HLJvTuDpVXyECEsWIvwsUaxzj7lZ9mSYutNDY,105
|
|
62
|
-
pymammotion/mqtt/mammotion_mqtt.py,sha256=
|
|
63
|
+
pymammotion/mqtt/mammotion_mqtt.py,sha256=VU0c8X1DrZSZDLCVAsCaEbLX2zcyrUbEAYaPzOXALNo,7523
|
|
63
64
|
pymammotion/proto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
64
65
|
pymammotion/proto/basestation.proto,sha256=_x5gAz3FkZXS1jtq4GgZgaDCuRU-UV-7HTFdsfQ3zbo,1034
|
|
65
66
|
pymammotion/proto/basestation.py,sha256=js64_N2xQYRxWPRdVNEapO0qe7vBlfYnjW5sE8hi7hw,2026
|
|
@@ -103,13 +104,13 @@ pymammotion/proto/mctrl_sys_pb2.py,sha256=DYemb514mlC7c27t-k1YqqBif0xxhLmnIWk8rX
|
|
|
103
104
|
pymammotion/proto/mctrl_sys_pb2.pyi,sha256=BrivyEP8s7nnuSQkezTbnse9A4J1ad5o6yFcufELLVU,38910
|
|
104
105
|
pymammotion/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
105
106
|
pymammotion/utility/constant/__init__.py,sha256=tZz7szqIhrzNjfcLU3ysfINfg5VEBUAisd9AhP4mvT0,38
|
|
106
|
-
pymammotion/utility/constant/device_constant.py,sha256=
|
|
107
|
+
pymammotion/utility/constant/device_constant.py,sha256=aM69DJJy-h-btLP9RJezld1zm1rGz4gZRdRSpArVtmc,7109
|
|
107
108
|
pymammotion/utility/datatype_converter.py,sha256=v6zym2Zu0upxQjR-xDqXwi3516zpntSlg7LP8tQF5K8,4216
|
|
108
109
|
pymammotion/utility/device_type.py,sha256=KYawu2glZMVlPmxRbA4kVFujXz3miHp3rJiOWRVj-GI,8285
|
|
109
|
-
pymammotion/utility/map.py,sha256=
|
|
110
|
+
pymammotion/utility/map.py,sha256=aoi-Luzuph02hKynTofMoq3mnPstanx75MDAVv49CuY,2211
|
|
110
111
|
pymammotion/utility/periodic.py,sha256=9wJMfwXPlx6Mbp3Fws7LLTI34ZDKphH1bva_Ggyk32g,3281
|
|
111
112
|
pymammotion/utility/rocker_util.py,sha256=syPL0QN4zMzHiTIkUKS7RXBBptjdbkfNlPddwUD5V3A,7171
|
|
112
|
-
pymammotion-0.
|
|
113
|
-
pymammotion-0.
|
|
114
|
-
pymammotion-0.
|
|
115
|
-
pymammotion-0.
|
|
113
|
+
pymammotion-0.2.0.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
114
|
+
pymammotion-0.2.0.dist-info/METADATA,sha256=Rhb3BsXkrf-fHqvTLQkqnRS1ldAsccbkc6ZhsuZpLR4,3922
|
|
115
|
+
pymammotion-0.2.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
116
|
+
pymammotion-0.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|