pymammotion 0.5.27__py3-none-any.whl → 0.5.44__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 +3 -3
- pymammotion/aliyun/client.py +3 -0
- pymammotion/aliyun/cloud_gateway.py +117 -19
- pymammotion/aliyun/model/dev_by_account_response.py +198 -20
- pymammotion/const.py +3 -0
- pymammotion/data/model/device.py +1 -0
- pymammotion/data/model/device_config.py +1 -1
- pymammotion/data/model/enums.py +5 -3
- pymammotion/data/model/generate_route_information.py +2 -2
- pymammotion/data/model/hash_list.py +113 -33
- pymammotion/data/model/region_data.py +4 -4
- pymammotion/data/{state_manager.py → mower_state_manager.py} +17 -7
- pymammotion/data/mqtt/event.py +47 -22
- pymammotion/data/mqtt/mammotion_properties.py +257 -0
- pymammotion/data/mqtt/properties.py +32 -29
- pymammotion/data/mqtt/status.py +17 -16
- pymammotion/homeassistant/__init__.py +3 -0
- pymammotion/homeassistant/mower_api.py +446 -0
- pymammotion/homeassistant/rtk_api.py +54 -0
- pymammotion/http/http.py +431 -18
- pymammotion/http/model/http.py +82 -2
- pymammotion/http/model/response_factory.py +10 -4
- pymammotion/mammotion/commands/mammotion_command.py +20 -0
- pymammotion/mammotion/commands/messages/navigation.py +10 -6
- pymammotion/mammotion/commands/messages/system.py +0 -14
- pymammotion/mammotion/devices/__init__.py +27 -3
- pymammotion/mammotion/devices/base.py +22 -146
- pymammotion/mammotion/devices/mammotion.py +367 -206
- pymammotion/mammotion/devices/mammotion_bluetooth.py +8 -5
- pymammotion/mammotion/devices/mammotion_cloud.py +47 -83
- pymammotion/mammotion/devices/mammotion_mower_ble.py +49 -0
- pymammotion/mammotion/devices/mammotion_mower_cloud.py +39 -0
- pymammotion/mammotion/devices/managers/managers.py +81 -0
- pymammotion/mammotion/devices/mower_device.py +121 -0
- pymammotion/mammotion/devices/mower_manager.py +107 -0
- pymammotion/mammotion/devices/rtk_ble.py +89 -0
- pymammotion/mammotion/devices/rtk_cloud.py +113 -0
- pymammotion/mammotion/devices/rtk_device.py +50 -0
- pymammotion/mammotion/devices/rtk_manager.py +122 -0
- pymammotion/mqtt/__init__.py +2 -1
- pymammotion/mqtt/aliyun_mqtt.py +232 -0
- pymammotion/mqtt/mammotion_mqtt.py +174 -192
- pymammotion/mqtt/mqtt_models.py +66 -0
- pymammotion/proto/__init__.py +2 -2
- pymammotion/proto/mctrl_nav.proto +2 -2
- pymammotion/proto/mctrl_nav_pb2.py +1 -1
- pymammotion/proto/mctrl_nav_pb2.pyi +4 -4
- pymammotion/proto/mctrl_sys.proto +1 -1
- pymammotion/utility/datatype_converter.py +13 -12
- pymammotion/utility/device_type.py +88 -3
- pymammotion/utility/mur_mur_hash.py +132 -87
- {pymammotion-0.5.27.dist-info → pymammotion-0.5.44.dist-info}/METADATA +25 -30
- {pymammotion-0.5.27.dist-info → pymammotion-0.5.44.dist-info}/RECORD +61 -47
- {pymammotion-0.5.27.dist-info → pymammotion-0.5.44.dist-info}/WHEEL +1 -1
- pymammotion/http/_init_.py +0 -0
- {pymammotion-0.5.27.dist-info → pymammotion-0.5.44.dist-info/licenses}/LICENSE +0 -0
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import Annotated, Any, Literal, Union
|
|
3
3
|
|
|
4
4
|
from mashumaro import DataClassDictMixin
|
|
5
5
|
from mashumaro.mixins.orjson import DataClassORJSONMixin
|
|
6
|
+
from mashumaro.types import Alias
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
@dataclass
|
|
11
|
-
class Item(DataClassDictMixin, Generic[DataT]):
|
|
12
|
-
time: int
|
|
13
|
-
value: DataT
|
|
8
|
+
from pymammotion.data.mqtt.mammotion_properties import DeviceProperties
|
|
14
9
|
|
|
15
10
|
|
|
16
11
|
@dataclass
|
|
@@ -163,29 +158,29 @@ class Items(DataClassDictMixin):
|
|
|
163
158
|
|
|
164
159
|
@dataclass
|
|
165
160
|
class Params(DataClassORJSONMixin):
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
161
|
+
device_type: Annotated[Literal["LawnMower", "Tracker"], Alias("deviceType")]
|
|
162
|
+
check_failed_data: Annotated[dict[str, Any], Alias("checkFailedData")]
|
|
163
|
+
group_id_list: Annotated[list[str], Alias("groupIdList")]
|
|
164
|
+
_tenant_id: Annotated[str, Alias("_tenantId")]
|
|
165
|
+
group_id: Annotated[str, Alias("groupId")]
|
|
166
|
+
category_key: Annotated[Literal["LawnMower", "Tracker"], Alias("categoryKey")]
|
|
167
|
+
batch_id: Annotated[str, Alias("batchId")]
|
|
168
|
+
gmt_create: Annotated[int, Alias("gmtCreate")]
|
|
169
|
+
product_key: Annotated[str, Alias("productKey")]
|
|
170
|
+
generate_time: Annotated[int, Alias("generateTime")]
|
|
171
|
+
device_name: Annotated[str, Alias("deviceName")]
|
|
172
|
+
_trace_id: Annotated[str, Alias("_traceId")]
|
|
173
|
+
iot_id: Annotated[str, Alias("iotId")]
|
|
174
|
+
jmsx_delivery_count: Annotated[int, Alias("JMSXDeliveryCount")]
|
|
175
|
+
check_level: Annotated[int, Alias("checkLevel")]
|
|
181
176
|
qos: int
|
|
182
|
-
|
|
183
|
-
|
|
177
|
+
request_id: Annotated[str, Alias("requestId")]
|
|
178
|
+
_category_key: Annotated[str, Alias("_categoryKey")]
|
|
184
179
|
namespace: str
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
items: Items
|
|
188
|
-
|
|
180
|
+
tenant_id: Annotated[str, Alias("tenantId")]
|
|
181
|
+
thing_type: Annotated[Literal["DEVICE"], Alias("thingType")]
|
|
182
|
+
items: Annotated["Items", Alias("items")]
|
|
183
|
+
tenant_instance_id: Annotated[str, Alias("tenantInstanceId")]
|
|
189
184
|
|
|
190
185
|
|
|
191
186
|
@dataclass
|
|
@@ -194,3 +189,11 @@ class ThingPropertiesMessage(DataClassORJSONMixin):
|
|
|
194
189
|
id: str
|
|
195
190
|
params: Params
|
|
196
191
|
version: Literal["1.0"]
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
@dataclass
|
|
195
|
+
class MammotionPropertiesMessage(DataClassORJSONMixin):
|
|
196
|
+
id: str
|
|
197
|
+
version: str
|
|
198
|
+
sys: dict
|
|
199
|
+
params: DeviceProperties
|
pymammotion/data/mqtt/status.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
2
|
from enum import Enum
|
|
3
|
-
from typing import Literal
|
|
3
|
+
from typing import Annotated, Literal
|
|
4
4
|
|
|
5
5
|
from mashumaro.mixins.orjson import DataClassORJSONMixin
|
|
6
|
+
from mashumaro.types import Alias
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
@dataclass
|
|
@@ -25,23 +26,23 @@ class Status(DataClassORJSONMixin):
|
|
|
25
26
|
|
|
26
27
|
@dataclass
|
|
27
28
|
class Params(DataClassORJSONMixin):
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
group_id_list: Annotated[list[GroupIdListItem], Alias("groupIdList")]
|
|
30
|
+
net_type: Annotated[Literal["NET_WIFI", "NET_MNET"], Alias("netType")]
|
|
31
|
+
active_time: Annotated[int, Alias("activeTime")]
|
|
31
32
|
ip: str
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
33
|
+
aliyun_commodity_code: Annotated[Literal["iothub_senior"], Alias("aliyunCommodityCode")]
|
|
34
|
+
category_key: Annotated[Literal["LawnMower", "Tracker"], Alias("categoryKey")]
|
|
35
|
+
node_type: Annotated[Literal["DEVICE"], Alias("nodeType")]
|
|
36
|
+
product_key: Annotated[str, Alias("productKey")]
|
|
37
|
+
status_last: Annotated[int, Alias("statusLast")]
|
|
38
|
+
device_name: Annotated[str, Alias("deviceName")]
|
|
39
|
+
iot_id: Annotated[str, Alias("iotId")]
|
|
39
40
|
namespace: str
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
status: Status
|
|
41
|
+
tenant_id: Annotated[str, Alias("tenantId")]
|
|
42
|
+
thing_type: Annotated[Literal["DEVICE"], Alias("thingType")]
|
|
43
|
+
tenant_instance_id: Annotated[str, Alias("tenantInstanceId")]
|
|
44
|
+
category_id: Annotated[int, Alias("categoryId")]
|
|
45
|
+
status: Annotated[Status, Alias("status")]
|
|
45
46
|
|
|
46
47
|
|
|
47
48
|
@dataclass
|
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
"""Thin api layer between home assistant and pymammotion."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from datetime import datetime, timedelta
|
|
5
|
+
from logging import getLogger
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from pymammotion.aliyun.cloud_gateway import (
|
|
9
|
+
EXPIRED_CREDENTIAL_EXCEPTIONS,
|
|
10
|
+
DeviceOfflineException,
|
|
11
|
+
FailedRequestException,
|
|
12
|
+
GatewayTimeoutException,
|
|
13
|
+
NoConnectionException,
|
|
14
|
+
)
|
|
15
|
+
from pymammotion.data.model import GenerateRouteInformation
|
|
16
|
+
from pymammotion.data.model.device import MowingDevice
|
|
17
|
+
from pymammotion.data.model.device_config import OperationSettings, create_path_order
|
|
18
|
+
from pymammotion.mammotion.devices import MammotionMowerDeviceManager
|
|
19
|
+
from pymammotion.mammotion.devices.mammotion import Mammotion
|
|
20
|
+
from pymammotion.proto import RptAct, RptInfoType
|
|
21
|
+
from pymammotion.utility.device_type import DeviceType
|
|
22
|
+
|
|
23
|
+
logger = getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class HomeAssistantMowerApi:
|
|
27
|
+
"""API for interacting with Mammotion Mowers for Home Assistant."""
|
|
28
|
+
|
|
29
|
+
def __init__(self) -> None:
|
|
30
|
+
self.update_failures = 0
|
|
31
|
+
self._mammotion = Mammotion()
|
|
32
|
+
self._map_lock = asyncio.Lock()
|
|
33
|
+
self._last_call_times: dict[str, datetime] = {}
|
|
34
|
+
self._call_intervals = {
|
|
35
|
+
"check_maps": timedelta(minutes=1),
|
|
36
|
+
"read_settings": timedelta(minutes=5),
|
|
37
|
+
"get_errors": timedelta(minutes=1),
|
|
38
|
+
"get_report_cfg": timedelta(seconds=5),
|
|
39
|
+
"get_maintenance": timedelta(minutes=30),
|
|
40
|
+
"device_version_upgrade": timedelta(hours=5),
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def mammotion(self) -> Mammotion:
|
|
45
|
+
return self._mammotion
|
|
46
|
+
|
|
47
|
+
def _should_call_api(self, api_name: str, device: MowingDevice | None = None) -> bool:
|
|
48
|
+
"""Check if API should be called based on time or criteria."""
|
|
49
|
+
# Time-based check
|
|
50
|
+
if api_name not in self._last_call_times:
|
|
51
|
+
return True
|
|
52
|
+
|
|
53
|
+
last_call = self._last_call_times[api_name]
|
|
54
|
+
interval = self._call_intervals.get(api_name, timedelta(seconds=10))
|
|
55
|
+
|
|
56
|
+
# Criteria-based checks
|
|
57
|
+
if api_name == "check_maps" and device:
|
|
58
|
+
# Call immediately if map data is incomplete
|
|
59
|
+
if len(device.map.area) == 0 or device.map.missing_hashlist():
|
|
60
|
+
return True
|
|
61
|
+
|
|
62
|
+
return datetime.now() - last_call >= interval
|
|
63
|
+
|
|
64
|
+
def _mark_api_called(self, api_name: str) -> None:
|
|
65
|
+
"""Mark an API as called with the current timestamp."""
|
|
66
|
+
self._last_call_times[api_name] = datetime.now()
|
|
67
|
+
|
|
68
|
+
async def update(self, device_name: str) -> MowingDevice:
|
|
69
|
+
device = self._mammotion.get_device_by_name(device_name)
|
|
70
|
+
|
|
71
|
+
if device.has_queued_commands():
|
|
72
|
+
return device.state
|
|
73
|
+
|
|
74
|
+
if self._map_lock.locked():
|
|
75
|
+
# if maps is not complete kick off the map sync process again
|
|
76
|
+
if len(device.state.map.missing_hashlist()) > 0:
|
|
77
|
+
await self._mammotion.start_map_sync(device_name)
|
|
78
|
+
return device.state
|
|
79
|
+
# if maps complete
|
|
80
|
+
else:
|
|
81
|
+
self._map_lock.release()
|
|
82
|
+
|
|
83
|
+
# Check maps periodically
|
|
84
|
+
if self._should_call_api("check_maps"):
|
|
85
|
+
await self._map_lock.acquire()
|
|
86
|
+
await self.mammotion.start_map_sync(device_name)
|
|
87
|
+
self._mark_api_called("check_maps")
|
|
88
|
+
|
|
89
|
+
# Read settings less frequently
|
|
90
|
+
if self._should_call_api("read_settings"):
|
|
91
|
+
# await device.async_read_settings()
|
|
92
|
+
self._mark_api_called("read_settings")
|
|
93
|
+
|
|
94
|
+
# Check for errors periodically
|
|
95
|
+
if self._should_call_api("get_errors"):
|
|
96
|
+
await self.async_send_command(device_name, "get_error_code")
|
|
97
|
+
await self.async_send_command(device_name, "get_error_timestamp")
|
|
98
|
+
self._mark_api_called("get_errors")
|
|
99
|
+
|
|
100
|
+
if self._should_call_api("get_report_cfg"):
|
|
101
|
+
await self.async_send_command(device_name, "get_report_cfg")
|
|
102
|
+
self._mark_api_called("get_report_cfg")
|
|
103
|
+
|
|
104
|
+
if self._should_call_api("get_maintenance"):
|
|
105
|
+
await self.async_send_command(device_name, "get_maintenance")
|
|
106
|
+
self._mark_api_called("get_maintenance")
|
|
107
|
+
|
|
108
|
+
if self._should_call_api("device_version_upgrade"):
|
|
109
|
+
self._mark_api_called("device_version_upgrade")
|
|
110
|
+
|
|
111
|
+
return device.state
|
|
112
|
+
|
|
113
|
+
async def async_send_command(self, device_name: str, command: str, **kwargs: Any) -> bool | None:
|
|
114
|
+
"""Send command."""
|
|
115
|
+
device = self._mammotion.get_device_by_name(device_name)
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
# TODO check preference
|
|
119
|
+
if device.cloud:
|
|
120
|
+
return await self.async_send_cloud_command(device, command, **kwargs)
|
|
121
|
+
elif device.ble:
|
|
122
|
+
return await self.async_send_bluetooth_command(device, command, **kwargs)
|
|
123
|
+
except (DeviceOfflineException, NoConnectionException) as ex:
|
|
124
|
+
"""Device is offline try bluetooth if we have it."""
|
|
125
|
+
logger.error(f"Device offline: {ex.iot_id}")
|
|
126
|
+
if ble := device.ble:
|
|
127
|
+
# if we don't do this, it will stay connected and no longer update over Wi-Fi
|
|
128
|
+
ble.set_disconnect_strategy(disconnect=True)
|
|
129
|
+
await ble.queue_command(command, **kwargs)
|
|
130
|
+
|
|
131
|
+
return True
|
|
132
|
+
raise DeviceOfflineException(ex.args[0], device.iot_id)
|
|
133
|
+
return False
|
|
134
|
+
|
|
135
|
+
async def async_send_cloud_command(
|
|
136
|
+
self, device: MammotionMowerDeviceManager, key: str, **kwargs: Any
|
|
137
|
+
) -> bool | None:
|
|
138
|
+
"""Send command."""
|
|
139
|
+
if cloud := device.cloud:
|
|
140
|
+
if not device.state.online:
|
|
141
|
+
return False
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
await cloud.command(key, **kwargs)
|
|
145
|
+
self.update_failures = 0
|
|
146
|
+
return True
|
|
147
|
+
except FailedRequestException:
|
|
148
|
+
self.update_failures += 1
|
|
149
|
+
if self.update_failures < 5:
|
|
150
|
+
await cloud.command(key, **kwargs)
|
|
151
|
+
return True
|
|
152
|
+
return False
|
|
153
|
+
except EXPIRED_CREDENTIAL_EXCEPTIONS:
|
|
154
|
+
self.update_failures += 1
|
|
155
|
+
await self._mammotion.refresh_login(device.mammotion_http.account)
|
|
156
|
+
# TODO tell home assistant the credentials have changed
|
|
157
|
+
if self.update_failures < 5:
|
|
158
|
+
await cloud.command(key, **kwargs)
|
|
159
|
+
return True
|
|
160
|
+
return False
|
|
161
|
+
except GatewayTimeoutException as ex:
|
|
162
|
+
logger.error(f"Gateway timeout exception: {ex.iot_id}")
|
|
163
|
+
self.update_failures = 0
|
|
164
|
+
return False
|
|
165
|
+
except (DeviceOfflineException, NoConnectionException) as ex:
|
|
166
|
+
"""Device is offline try bluetooth if we have it."""
|
|
167
|
+
logger.error(f"Device offline: {ex.iot_id}")
|
|
168
|
+
return False
|
|
169
|
+
|
|
170
|
+
@staticmethod
|
|
171
|
+
async def async_send_bluetooth_command(device: MammotionMowerDeviceManager, key: str, **kwargs: Any) -> bool | None:
|
|
172
|
+
"""Send command."""
|
|
173
|
+
if ble := device.ble:
|
|
174
|
+
await ble.command(key, **kwargs)
|
|
175
|
+
|
|
176
|
+
return True
|
|
177
|
+
raise DeviceOfflineException("bluetooth command failed", device.iot_id)
|
|
178
|
+
|
|
179
|
+
async def set_scheduled_updates(self, device_name: str, enabled: bool) -> None:
|
|
180
|
+
device = self.mammotion.get_device_by_name(device_name)
|
|
181
|
+
device.state.enabled = enabled
|
|
182
|
+
if device.state.enabled:
|
|
183
|
+
self.update_failures = 0
|
|
184
|
+
if not device.state.online:
|
|
185
|
+
device.state.online = True
|
|
186
|
+
if device.cloud and device.cloud.stopped:
|
|
187
|
+
await device.cloud.start()
|
|
188
|
+
else:
|
|
189
|
+
if device.cloud:
|
|
190
|
+
await device.cloud.stop()
|
|
191
|
+
if device.cloud.mqtt.is_connected():
|
|
192
|
+
device.cloud.mqtt.disconnect()
|
|
193
|
+
if device.ble:
|
|
194
|
+
await device.ble.stop()
|
|
195
|
+
|
|
196
|
+
def is_online(self, device_name: str) -> bool:
|
|
197
|
+
if device := self.mammotion.get_device_by_name(device_name):
|
|
198
|
+
ble = device.ble
|
|
199
|
+
return device.state.online or ble is not None and ble.client.is_connected
|
|
200
|
+
return False
|
|
201
|
+
|
|
202
|
+
async def update_firmware(self, device_name: str, version: str) -> None:
|
|
203
|
+
"""Update firmware."""
|
|
204
|
+
device = self.mammotion.get_device_by_name(device_name)
|
|
205
|
+
await device.mammotion_http.start_ota_upgrade(device.iot_id, version)
|
|
206
|
+
|
|
207
|
+
async def async_start_stop_blades(self, device_name: str, start_stop: bool, blade_height: int = 60) -> None:
|
|
208
|
+
"""Start stop blades."""
|
|
209
|
+
if DeviceType.is_luba1(device_name):
|
|
210
|
+
if start_stop:
|
|
211
|
+
await self.async_send_command(device_name, "set_blade_control", on_off=1)
|
|
212
|
+
else:
|
|
213
|
+
await self.async_send_command(device_name, "set_blade_control", on_off=0)
|
|
214
|
+
elif start_stop:
|
|
215
|
+
if DeviceType.is_yuka(device_name) or DeviceType.is_yuka_mini(device_name):
|
|
216
|
+
blade_height = 0
|
|
217
|
+
|
|
218
|
+
await self.async_send_command(
|
|
219
|
+
"operate_on_device",
|
|
220
|
+
main_ctrl=1,
|
|
221
|
+
cut_knife_ctrl=1,
|
|
222
|
+
cut_knife_height=blade_height,
|
|
223
|
+
max_run_speed=1.2,
|
|
224
|
+
)
|
|
225
|
+
else:
|
|
226
|
+
await self.async_send_command(
|
|
227
|
+
"operate_on_device",
|
|
228
|
+
main_ctrl=0,
|
|
229
|
+
cut_knife_ctrl=0,
|
|
230
|
+
cut_knife_height=blade_height,
|
|
231
|
+
max_run_speed=1.2,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
async def async_set_rain_detection(self, device_name: str, on_off: bool) -> None:
|
|
235
|
+
"""Set rain detection."""
|
|
236
|
+
await self.async_send_command(device_name, "read_write_device", rw_id=3, context=int(on_off), rw=1)
|
|
237
|
+
|
|
238
|
+
async def async_read_rain_detection(self, device_name: str) -> None:
|
|
239
|
+
"""Set rain detection."""
|
|
240
|
+
await self.async_send_command(device_name, "read_write_device", rw_id=3, context=1, rw=0)
|
|
241
|
+
|
|
242
|
+
async def async_set_sidelight(self, device_name: str, on_off: int) -> None:
|
|
243
|
+
"""Set Sidelight."""
|
|
244
|
+
await self.async_send_command(device_name, "read_and_set_sidelight", is_sidelight=bool(on_off), operate=0)
|
|
245
|
+
await self.async_read_sidelight()
|
|
246
|
+
|
|
247
|
+
async def async_read_sidelight(self, device_name: str) -> None:
|
|
248
|
+
"""Set Sidelight."""
|
|
249
|
+
await self.async_send_command(device_name, "read_and_set_sidelight", is_sidelight=False, operate=1)
|
|
250
|
+
|
|
251
|
+
async def async_set_manual_light(self, device_name: str, manual_ctrl: bool) -> None:
|
|
252
|
+
"""Set manual night light."""
|
|
253
|
+
await self.async_send_command(device_name, "set_car_manual_light", manual_ctrl=manual_ctrl)
|
|
254
|
+
await self.async_send_command(device_name, "get_car_light", ids=1126)
|
|
255
|
+
|
|
256
|
+
async def async_set_night_light(self, device_name: str, night_light: bool) -> None:
|
|
257
|
+
"""Set night light."""
|
|
258
|
+
await self.async_send_command(device_name, "set_car_light", on_off=night_light)
|
|
259
|
+
await self.async_send_command(device_name, "get_car_light", ids=1123)
|
|
260
|
+
|
|
261
|
+
async def async_set_traversal_mode(self, device_name: str, context: int) -> None:
|
|
262
|
+
"""Set traversal mode."""
|
|
263
|
+
await self.async_send_command(device_name, "traverse_mode", context=context)
|
|
264
|
+
|
|
265
|
+
async def async_set_turning_mode(self, device_name: str, context: int) -> None:
|
|
266
|
+
"""Set turning mode."""
|
|
267
|
+
await self.async_send_command(device_name, "turning_mode", context=context)
|
|
268
|
+
|
|
269
|
+
async def async_blade_height(self, device_name: str, height: int) -> int:
|
|
270
|
+
"""Set blade height."""
|
|
271
|
+
await self.async_send_command(device_name, "set_blade_height", height=height)
|
|
272
|
+
return height
|
|
273
|
+
|
|
274
|
+
async def async_set_cutter_speed(self, device_name: str, mode: int) -> None:
|
|
275
|
+
"""Set cutter speed."""
|
|
276
|
+
await self.async_send_command(device_name, "set_cutter_mode", cutter_mode=mode)
|
|
277
|
+
|
|
278
|
+
async def async_set_speed(self, device_name: str, speed: float) -> None:
|
|
279
|
+
"""Set working speed."""
|
|
280
|
+
await self.async_send_command(device_name, "set_speed", speed=speed)
|
|
281
|
+
|
|
282
|
+
async def async_leave_dock(self, device_name: str) -> None:
|
|
283
|
+
"""Leave dock."""
|
|
284
|
+
await self.send_command_and_update(device_name, "leave_dock")
|
|
285
|
+
|
|
286
|
+
async def async_cancel_task(self, device_name: str) -> None:
|
|
287
|
+
"""Cancel task."""
|
|
288
|
+
await self.send_command_and_update(device_name, "cancel_job")
|
|
289
|
+
|
|
290
|
+
async def async_move_forward(self, device_name: str, speed: float) -> None:
|
|
291
|
+
"""Move forward."""
|
|
292
|
+
device = self.mammotion.get_device_by_name(device_name)
|
|
293
|
+
await self.async_send_bluetooth_command(device, "move_forward", linear=speed)
|
|
294
|
+
|
|
295
|
+
async def async_move_left(self, device_name: str, speed: float) -> None:
|
|
296
|
+
"""Move left."""
|
|
297
|
+
device = self.mammotion.get_device_by_name(device_name)
|
|
298
|
+
await self.async_send_bluetooth_command(device, "move_left", angular=speed)
|
|
299
|
+
|
|
300
|
+
async def async_move_right(self, device_name: str, speed: float) -> None:
|
|
301
|
+
"""Move right."""
|
|
302
|
+
device = self.mammotion.get_device_by_name(device_name)
|
|
303
|
+
await self.async_send_bluetooth_command(device, "move_right", angular=speed)
|
|
304
|
+
|
|
305
|
+
async def async_move_back(self, device_name: str, speed: float) -> None:
|
|
306
|
+
"""Move back."""
|
|
307
|
+
device = self.mammotion.get_device_by_name(device_name)
|
|
308
|
+
await self.async_send_bluetooth_command(device, "move_back", linear=speed)
|
|
309
|
+
|
|
310
|
+
async def async_rtk_dock_location(self, device_name: str) -> None:
|
|
311
|
+
"""RTK and dock location."""
|
|
312
|
+
await self.async_send_command(device_name, "read_write_device", rw_id=5, rw=1, context=1)
|
|
313
|
+
|
|
314
|
+
async def async_get_area_list(self, device_name: str, iot_id: str) -> None:
|
|
315
|
+
"""Mowing area List."""
|
|
316
|
+
await self.async_send_command(device_name, "get_area_name_list", device_id=iot_id)
|
|
317
|
+
|
|
318
|
+
async def async_relocate_charging_station(self, device_name: str) -> None:
|
|
319
|
+
"""Reset charging station."""
|
|
320
|
+
await self.async_send_command(device_name, "delete_charge_point")
|
|
321
|
+
# fetch charging location?
|
|
322
|
+
"""
|
|
323
|
+
nav {
|
|
324
|
+
todev_get_commondata {
|
|
325
|
+
pver: 1
|
|
326
|
+
subCmd: 2
|
|
327
|
+
action: 6
|
|
328
|
+
type: 5
|
|
329
|
+
totalFrame: 1
|
|
330
|
+
currentFrame: 1
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
"""
|
|
334
|
+
|
|
335
|
+
async def send_command_and_update(self, device_name: str, command_str: str, **kwargs: Any) -> None:
|
|
336
|
+
"""Send command and update."""
|
|
337
|
+
await self.async_send_command(device_name, command_str, **kwargs)
|
|
338
|
+
await self.async_request_iot_sync(device_name)
|
|
339
|
+
|
|
340
|
+
async def async_request_iot_sync(self, device_name: str, stop: bool = False) -> None:
|
|
341
|
+
"""Sync specific info from device."""
|
|
342
|
+
await self.async_send_command(
|
|
343
|
+
device_name,
|
|
344
|
+
"request_iot_sys",
|
|
345
|
+
rpt_act=RptAct.RPT_STOP if stop else RptAct.RPT_START,
|
|
346
|
+
rpt_info_type=[
|
|
347
|
+
RptInfoType.RIT_DEV_STA,
|
|
348
|
+
RptInfoType.RIT_DEV_LOCAL,
|
|
349
|
+
RptInfoType.RIT_WORK,
|
|
350
|
+
RptInfoType.RIT_MAINTAIN,
|
|
351
|
+
RptInfoType.RIT_BASESTATION_INFO,
|
|
352
|
+
RptInfoType.RIT_VIO,
|
|
353
|
+
],
|
|
354
|
+
timeout=10000,
|
|
355
|
+
period=3000,
|
|
356
|
+
no_change_period=4000,
|
|
357
|
+
count=0,
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
def generate_route_information(
|
|
361
|
+
self, device_name: str, operation_settings: OperationSettings
|
|
362
|
+
) -> GenerateRouteInformation:
|
|
363
|
+
"""Generate route information."""
|
|
364
|
+
device = self.mammotion.get_device_by_name(device_name)
|
|
365
|
+
if device.state.report_data.dev:
|
|
366
|
+
dev = device.state.report_data.dev
|
|
367
|
+
if dev.collector_status.collector_installation_status == 0:
|
|
368
|
+
operation_settings.is_dump = False
|
|
369
|
+
|
|
370
|
+
if DeviceType.is_yuka(device_name):
|
|
371
|
+
operation_settings.blade_height = -10
|
|
372
|
+
|
|
373
|
+
route_information = GenerateRouteInformation(
|
|
374
|
+
one_hashs=list(operation_settings.areas),
|
|
375
|
+
rain_tactics=operation_settings.rain_tactics,
|
|
376
|
+
speed=operation_settings.speed,
|
|
377
|
+
ultra_wave=operation_settings.ultra_wave, # touch no touch etc
|
|
378
|
+
toward=operation_settings.toward, # is just angle (route angle)
|
|
379
|
+
toward_included_angle=operation_settings.toward_included_angle # demond_angle
|
|
380
|
+
if operation_settings.channel_mode == 1
|
|
381
|
+
else 0, # crossing angle relative to grid
|
|
382
|
+
toward_mode=operation_settings.toward_mode,
|
|
383
|
+
blade_height=operation_settings.blade_height,
|
|
384
|
+
channel_mode=operation_settings.channel_mode, # single, double, segment or none (route mode)
|
|
385
|
+
channel_width=operation_settings.channel_width, # path space
|
|
386
|
+
job_mode=operation_settings.job_mode, # taskMode grid or border first
|
|
387
|
+
edge_mode=operation_settings.mowing_laps, # perimeter/mowing laps
|
|
388
|
+
path_order=create_path_order(operation_settings, device_name),
|
|
389
|
+
obstacle_laps=operation_settings.obstacle_laps,
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
if DeviceType.is_luba1(device_name):
|
|
393
|
+
route_information.toward_mode = 0
|
|
394
|
+
route_information.toward_included_angle = 0
|
|
395
|
+
return route_information
|
|
396
|
+
|
|
397
|
+
async def async_plan_route(self, device_name: str, operation_settings: OperationSettings) -> bool | None:
|
|
398
|
+
"""Plan mow."""
|
|
399
|
+
route_information = self.generate_route_information(device_name, operation_settings)
|
|
400
|
+
|
|
401
|
+
# not sure if this is artificial limit
|
|
402
|
+
# if (
|
|
403
|
+
# DeviceType.is_mini_or_x_series(device_name)
|
|
404
|
+
# and route_information.toward_mode == 0
|
|
405
|
+
# ):
|
|
406
|
+
# route_information.toward = 0
|
|
407
|
+
|
|
408
|
+
return await self.async_send_command(
|
|
409
|
+
device_name, "generate_route_information", generate_route_information=route_information
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
async def async_modify_plan_route(self, device_name: str, operation_settings: OperationSettings) -> bool | None:
|
|
413
|
+
"""Modify plan mow."""
|
|
414
|
+
device = self.mammotion.get_device_by_name(device_name)
|
|
415
|
+
|
|
416
|
+
if work := device.state.work:
|
|
417
|
+
operation_settings.areas = work.zone_hashs
|
|
418
|
+
operation_settings.toward = work.toward
|
|
419
|
+
operation_settings.toward_mode = work.toward_mode
|
|
420
|
+
operation_settings.toward_included_angle = work.toward_included_angle
|
|
421
|
+
operation_settings.mowing_laps = work.edge_mode
|
|
422
|
+
operation_settings.job_mode = work.job_mode
|
|
423
|
+
operation_settings.job_id = work.job_id
|
|
424
|
+
operation_settings.job_version = work.job_ver
|
|
425
|
+
|
|
426
|
+
route_information = self.generate_route_information(device_name, operation_settings)
|
|
427
|
+
if route_information.toward_mode == 0:
|
|
428
|
+
route_information.toward = 0
|
|
429
|
+
|
|
430
|
+
return await self.async_send_command(
|
|
431
|
+
device_name, "modify_route_information", generate_route_information=route_information
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
async def start_task(self, device_name: str, plan_id: str) -> None:
|
|
435
|
+
"""Start task."""
|
|
436
|
+
await self.async_send_command(device_name, "single_schedule", plan_id=plan_id)
|
|
437
|
+
|
|
438
|
+
async def clear_update_failures(self, device_name: str) -> None:
|
|
439
|
+
"""Clear update failures."""
|
|
440
|
+
self.update_failures = 0
|
|
441
|
+
device = self.mammotion.get_device_by_name(device_name)
|
|
442
|
+
if not device.state.online:
|
|
443
|
+
device.state.online = True
|
|
444
|
+
if cloud := device.cloud:
|
|
445
|
+
if cloud.stopped:
|
|
446
|
+
await cloud.start()
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from pymammotion.aliyun.cloud_gateway import DeviceOfflineException, GatewayTimeoutException, SetupException
|
|
4
|
+
from pymammotion.data.model.device import RTKDevice
|
|
5
|
+
from pymammotion.http.model.http import CheckDeviceVersion
|
|
6
|
+
from pymammotion.mammotion.devices.mammotion import Mammotion
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class HomeAssistantRTKApi:
|
|
10
|
+
def __init__(self) -> None:
|
|
11
|
+
self._mammotion = Mammotion()
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def mammotion(self) -> Mammotion:
|
|
15
|
+
return self._mammotion
|
|
16
|
+
|
|
17
|
+
async def update(self, device_name: str) -> RTKDevice:
|
|
18
|
+
"""Update RTK data."""
|
|
19
|
+
device = self.mammotion.get_rtk_device_by_name(device_name)
|
|
20
|
+
try:
|
|
21
|
+
response = await device.cloud_client.get_device_properties(device.iot_id)
|
|
22
|
+
if response.code == 200:
|
|
23
|
+
data = response.data
|
|
24
|
+
if ota_progress := data.otaProgress:
|
|
25
|
+
device.update_check = CheckDeviceVersion.from_dict(ota_progress.value)
|
|
26
|
+
if network_info := data.networkInfo:
|
|
27
|
+
network = json.loads(network_info.value)
|
|
28
|
+
device.state.wifi_rssi = network["wifi_rssi"]
|
|
29
|
+
device.state.wifi_sta_mac = network["wifi_sta_mac"]
|
|
30
|
+
device.state.bt_mac = network["bt_mac"]
|
|
31
|
+
if coordinate := data.coordinate:
|
|
32
|
+
coord_val = json.loads(coordinate.value)
|
|
33
|
+
if device.state.lat == 0:
|
|
34
|
+
device.state.lat = coord_val["lat"]
|
|
35
|
+
if device.state.lon == 0:
|
|
36
|
+
device.state.lon = coord_val["lon"]
|
|
37
|
+
if device_version := data.deviceVersion:
|
|
38
|
+
device.state.device_version = device_version.value
|
|
39
|
+
device.state.online = True
|
|
40
|
+
|
|
41
|
+
ota_info = await device.cloud_client.mammotion_http.get_device_ota_firmware([device.state.iot_id])
|
|
42
|
+
if check_versions := ota_info.data:
|
|
43
|
+
for check_version in check_versions:
|
|
44
|
+
if check_version.device_id == device.state.iot_id:
|
|
45
|
+
device.state.update_check = check_version
|
|
46
|
+
return device.state
|
|
47
|
+
except SetupException:
|
|
48
|
+
"""Cloud IOT Gateway is not setup."""
|
|
49
|
+
return device.state
|
|
50
|
+
except DeviceOfflineException:
|
|
51
|
+
device.state.online = False
|
|
52
|
+
except GatewayTimeoutException:
|
|
53
|
+
"""Gateway is timing out again."""
|
|
54
|
+
return device.state
|