roborock-cli 0.1.1__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.
- roborock_cli/__init__.py +3 -0
- roborock_cli/__main__.py +76 -0
- roborock_cli/_vendor/VERSION +6 -0
- roborock_cli/_vendor/__init__.py +0 -0
- roborock_cli/_vendor/roborock/__init__.py +27 -0
- roborock_cli/_vendor/roborock/broadcast_protocol.py +114 -0
- roborock_cli/_vendor/roborock/callbacks.py +130 -0
- roborock_cli/_vendor/roborock/cli.py +1338 -0
- roborock_cli/_vendor/roborock/const.py +84 -0
- roborock_cli/_vendor/roborock/data/__init__.py +9 -0
- roborock_cli/_vendor/roborock/data/b01_q10/__init__.py +2 -0
- roborock_cli/_vendor/roborock/data/b01_q10/b01_q10_code_mappings.py +213 -0
- roborock_cli/_vendor/roborock/data/b01_q10/b01_q10_containers.py +102 -0
- roborock_cli/_vendor/roborock/data/b01_q7/__init__.py +2 -0
- roborock_cli/_vendor/roborock/data/b01_q7/b01_q7_code_mappings.py +303 -0
- roborock_cli/_vendor/roborock/data/b01_q7/b01_q7_containers.py +302 -0
- roborock_cli/_vendor/roborock/data/code_mappings.py +198 -0
- roborock_cli/_vendor/roborock/data/containers.py +530 -0
- roborock_cli/_vendor/roborock/data/dyad/__init__.py +2 -0
- roborock_cli/_vendor/roborock/data/dyad/dyad_code_mappings.py +102 -0
- roborock_cli/_vendor/roborock/data/dyad/dyad_containers.py +28 -0
- roborock_cli/_vendor/roborock/data/v1/__init__.py +3 -0
- roborock_cli/_vendor/roborock/data/v1/v1_clean_modes.py +192 -0
- roborock_cli/_vendor/roborock/data/v1/v1_code_mappings.py +644 -0
- roborock_cli/_vendor/roborock/data/v1/v1_containers.py +800 -0
- roborock_cli/_vendor/roborock/data/zeo/__init__.py +2 -0
- roborock_cli/_vendor/roborock/data/zeo/zeo_code_mappings.py +138 -0
- roborock_cli/_vendor/roborock/data/zeo/zeo_containers.py +0 -0
- roborock_cli/_vendor/roborock/device_features.py +668 -0
- roborock_cli/_vendor/roborock/devices/README.md +41 -0
- roborock_cli/_vendor/roborock/devices/__init__.py +11 -0
- roborock_cli/_vendor/roborock/devices/cache.py +143 -0
- roborock_cli/_vendor/roborock/devices/device.py +240 -0
- roborock_cli/_vendor/roborock/devices/device_manager.py +269 -0
- roborock_cli/_vendor/roborock/devices/file_cache.py +79 -0
- roborock_cli/_vendor/roborock/devices/rpc/__init__.py +14 -0
- roborock_cli/_vendor/roborock/devices/rpc/a01_channel.py +94 -0
- roborock_cli/_vendor/roborock/devices/rpc/b01_q10_channel.py +57 -0
- roborock_cli/_vendor/roborock/devices/rpc/b01_q7_channel.py +101 -0
- roborock_cli/_vendor/roborock/devices/rpc/v1_channel.py +457 -0
- roborock_cli/_vendor/roborock/devices/traits/__init__.py +28 -0
- roborock_cli/_vendor/roborock/devices/traits/a01/__init__.py +191 -0
- roborock_cli/_vendor/roborock/devices/traits/b01/__init__.py +12 -0
- roborock_cli/_vendor/roborock/devices/traits/b01/q10/__init__.py +76 -0
- roborock_cli/_vendor/roborock/devices/traits/b01/q10/command.py +32 -0
- roborock_cli/_vendor/roborock/devices/traits/b01/q10/common.py +115 -0
- roborock_cli/_vendor/roborock/devices/traits/b01/q10/status.py +32 -0
- roborock_cli/_vendor/roborock/devices/traits/b01/q10/vacuum.py +81 -0
- roborock_cli/_vendor/roborock/devices/traits/b01/q7/__init__.py +136 -0
- roborock_cli/_vendor/roborock/devices/traits/b01/q7/clean_summary.py +75 -0
- roborock_cli/_vendor/roborock/devices/traits/traits_mixin.py +64 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/__init__.py +344 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/child_lock.py +29 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/clean_summary.py +83 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/command.py +38 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/common.py +172 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/consumeable.py +48 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/device_features.py +74 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/do_not_disturb.py +41 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/dust_collection_mode.py +13 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/flow_led_status.py +29 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/home.py +285 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/led_status.py +43 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/map_content.py +83 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/maps.py +80 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/network_info.py +55 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/rooms.py +105 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/routines.py +26 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/smart_wash_params.py +13 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/status.py +101 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/valley_electricity_timer.py +44 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/volume.py +27 -0
- roborock_cli/_vendor/roborock/devices/traits/v1/wash_towel_mode.py +13 -0
- roborock_cli/_vendor/roborock/devices/transport/__init__.py +8 -0
- roborock_cli/_vendor/roborock/devices/transport/channel.py +32 -0
- roborock_cli/_vendor/roborock/devices/transport/local_channel.py +295 -0
- roborock_cli/_vendor/roborock/devices/transport/mqtt_channel.py +118 -0
- roborock_cli/_vendor/roborock/diagnostics.py +166 -0
- roborock_cli/_vendor/roborock/exceptions.py +95 -0
- roborock_cli/_vendor/roborock/map/__init__.py +7 -0
- roborock_cli/_vendor/roborock/map/map_parser.py +123 -0
- roborock_cli/_vendor/roborock/mqtt/__init__.py +10 -0
- roborock_cli/_vendor/roborock/mqtt/health_manager.py +60 -0
- roborock_cli/_vendor/roborock/mqtt/roborock_session.py +463 -0
- roborock_cli/_vendor/roborock/mqtt/session.py +108 -0
- roborock_cli/_vendor/roborock/protocol.py +558 -0
- roborock_cli/_vendor/roborock/protocols/__init__.py +3 -0
- roborock_cli/_vendor/roborock/protocols/a01_protocol.py +74 -0
- roborock_cli/_vendor/roborock/protocols/b01_q10_protocol.py +87 -0
- roborock_cli/_vendor/roborock/protocols/b01_q7_protocol.py +81 -0
- roborock_cli/_vendor/roborock/protocols/v1_protocol.py +271 -0
- roborock_cli/_vendor/roborock/py.typed +0 -0
- roborock_cli/_vendor/roborock/roborock_message.py +246 -0
- roborock_cli/_vendor/roborock/roborock_typing.py +382 -0
- roborock_cli/_vendor/roborock/util.py +54 -0
- roborock_cli/_vendor/roborock/web_api.py +761 -0
- roborock_cli/cli.py +715 -0
- roborock_cli/connection.py +202 -0
- roborock_cli/helpers.py +71 -0
- roborock_cli/server.py +759 -0
- roborock_cli/setup_auth.py +92 -0
- roborock_cli-0.1.1.dist-info/METADATA +172 -0
- roborock_cli-0.1.1.dist-info/RECORD +106 -0
- roborock_cli-0.1.1.dist-info/WHEEL +4 -0
- roborock_cli-0.1.1.dist-info/entry_points.txt +2 -0
- roborock_cli-0.1.1.dist-info/licenses/LICENSE +674 -0
roborock_cli/server.py
ADDED
|
@@ -0,0 +1,759 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP 服务器
|
|
3
|
+
|
|
4
|
+
查询工具(4 个):
|
|
5
|
+
0. get_devices - 列出所有设备
|
|
6
|
+
1. get_status - 设备全量状态(合并 8 个 trait 查询)
|
|
7
|
+
2. get_map_content - 地图内容(图片 + 元数据)
|
|
8
|
+
3. get_routines - 列出智能场景
|
|
9
|
+
|
|
10
|
+
控制工具(10 个):
|
|
11
|
+
4. start_clean / 5. stop_clean / 6. pause_clean / 7. resume_clean
|
|
12
|
+
8. go_home / 9. find_robot / 10. dock_action / 11. set_volume
|
|
13
|
+
12. set_dnd / 13. execute_routine
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
import functools
|
|
18
|
+
import json
|
|
19
|
+
import logging
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from mcp.server.fastmcp import FastMCP
|
|
23
|
+
from mcp.server.fastmcp.utilities.types import Image
|
|
24
|
+
from mcp.types import TextContent
|
|
25
|
+
from roborock_cli._vendor.roborock import RoborockCommand
|
|
26
|
+
from roborock_cli._vendor.roborock.data import DnDTimer
|
|
27
|
+
from roborock_cli._vendor.roborock.exceptions import RoborockException
|
|
28
|
+
|
|
29
|
+
from .connection import (
|
|
30
|
+
AmbiguousDeviceError,
|
|
31
|
+
AuthExpiredError,
|
|
32
|
+
ConnectionManager,
|
|
33
|
+
DeviceNotFoundError,
|
|
34
|
+
NoDeviceError,
|
|
35
|
+
lifespan,
|
|
36
|
+
)
|
|
37
|
+
from .helpers import STATUS_FIELDS, field, safe_refresh, serialize_value
|
|
38
|
+
|
|
39
|
+
logger = logging.getLogger(__name__)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _handle_errors(func):
|
|
43
|
+
"""包装工具函数,将已知异常转为结构化错误响应"""
|
|
44
|
+
@functools.wraps(func)
|
|
45
|
+
async def wrapper(*args, **kwargs):
|
|
46
|
+
try:
|
|
47
|
+
return await func(*args, **kwargs)
|
|
48
|
+
except (AuthExpiredError, NoDeviceError, AmbiguousDeviceError,
|
|
49
|
+
DeviceNotFoundError, ValueError) as e:
|
|
50
|
+
return {"error": str(e)}
|
|
51
|
+
except RoborockException as e:
|
|
52
|
+
logger.warning("工具 %s 设备通信失败: %s", func.__name__, e)
|
|
53
|
+
return {"error": f"设备通信失败: {type(e).__name__}: {e}"}
|
|
54
|
+
except Exception as e:
|
|
55
|
+
logger.exception("工具 %s 执行失败", func.__name__)
|
|
56
|
+
return {"error": f"内部错误: {type(e).__name__}: {e}"}
|
|
57
|
+
return wrapper
|
|
58
|
+
|
|
59
|
+
# 创建 FastMCP 实例
|
|
60
|
+
mcp = FastMCP(
|
|
61
|
+
"roborock-cli",
|
|
62
|
+
lifespan=lifespan,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_conn() -> ConnectionManager:
|
|
67
|
+
"""从 MCP context 获取 ConnectionManager"""
|
|
68
|
+
return mcp.get_context().request_context.lifespan_context["conn"]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ============================================================================
|
|
72
|
+
# 工具定义
|
|
73
|
+
# ============================================================================
|
|
74
|
+
|
|
75
|
+
@mcp.tool(
|
|
76
|
+
annotations={
|
|
77
|
+
"title": "List Roborock Devices",
|
|
78
|
+
"readOnlyHint": True,
|
|
79
|
+
"destructiveHint": False,
|
|
80
|
+
"idempotentHint": True,
|
|
81
|
+
"openWorldHint": True,
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
@_handle_errors
|
|
85
|
+
async def get_devices() -> dict[str, Any]:
|
|
86
|
+
"""列出账号下所有 Roborock 设备。
|
|
87
|
+
|
|
88
|
+
通过 Roborock 云端 API 获取设备列表(不走 MQTT),返回每台设备的基本信息。
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
dict: {
|
|
92
|
+
"count": int, # 设备数量
|
|
93
|
+
"devices": [
|
|
94
|
+
{
|
|
95
|
+
"name": str, # 设备名称(用户在 App 中设置的名称)
|
|
96
|
+
"duid": str, # 设备唯一标识符
|
|
97
|
+
"model": str, # 设备型号(如 "roborock.vacuum.a75")
|
|
98
|
+
"firmware": str # 固件版本号
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
}
|
|
102
|
+
"""
|
|
103
|
+
conn = get_conn()
|
|
104
|
+
devices = await conn.list_devices()
|
|
105
|
+
|
|
106
|
+
result = {
|
|
107
|
+
"count": len(devices),
|
|
108
|
+
"devices": []
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for device in devices:
|
|
112
|
+
info = {
|
|
113
|
+
"name": device.name,
|
|
114
|
+
"duid": device.duid,
|
|
115
|
+
"model": device.product.model if device.product else None,
|
|
116
|
+
"firmware": device.device_info.fv if device.device_info else None,
|
|
117
|
+
}
|
|
118
|
+
result["devices"].append(info)
|
|
119
|
+
|
|
120
|
+
return result
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@mcp.tool(
|
|
124
|
+
annotations={
|
|
125
|
+
"title": "Get Device Full Status",
|
|
126
|
+
"readOnlyHint": True,
|
|
127
|
+
"destructiveHint": False,
|
|
128
|
+
"idempotentHint": True,
|
|
129
|
+
"openWorldHint": True,
|
|
130
|
+
}
|
|
131
|
+
)
|
|
132
|
+
@_handle_errors
|
|
133
|
+
async def get_status(device_name: str | None = None) -> dict[str, Any]:
|
|
134
|
+
"""获取 Roborock 设备精简状态(通过 MQTT 实时查询 8 个 trait)。
|
|
135
|
+
|
|
136
|
+
每个字段返回 {"value": x, "description": "..."} 结构,方便理解字段含义。
|
|
137
|
+
并行查询设备信息,包含:
|
|
138
|
+
- 实时状态:电量、吸力、清洁进度、基座状态、错误码等(已精简,去除内部字段和重复字段)
|
|
139
|
+
- 勿扰模式:开关状态及时间段
|
|
140
|
+
- 清洁历史:累计清洁时间/面积/次数,最近一次清洁详情
|
|
141
|
+
- 音量、房间列表(含 segment_id)、地图列表、耗材使用时间
|
|
142
|
+
|
|
143
|
+
单个 trait 查询失败不影响其他 trait 返回,失败的 trait 对应字段会缺失。
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
device_name: 设备名称,支持模糊匹配。单设备时可省略,自动选择唯一设备。
|
|
147
|
+
"""
|
|
148
|
+
conn = get_conn()
|
|
149
|
+
device = await conn.get_device(device_name)
|
|
150
|
+
|
|
151
|
+
if not device.v1_properties:
|
|
152
|
+
return {"error": "设备不支持 V1 协议"}
|
|
153
|
+
|
|
154
|
+
props = device.v1_properties
|
|
155
|
+
|
|
156
|
+
# 并行刷新所有 trait(device_features 保留 refresh 但不输出,fan_speed_name 等依赖它)
|
|
157
|
+
trait_names = [
|
|
158
|
+
"status", "dnd", "clean_summary", "sound_volume",
|
|
159
|
+
"rooms", "maps", "consumables",
|
|
160
|
+
"device_features",
|
|
161
|
+
]
|
|
162
|
+
refresh_results = await asyncio.gather(
|
|
163
|
+
*[safe_refresh(getattr(props, name), name) for name in trait_names]
|
|
164
|
+
)
|
|
165
|
+
ok = dict(zip(trait_names, refresh_results))
|
|
166
|
+
|
|
167
|
+
result: dict[str, Any] = {"device_name": device.name}
|
|
168
|
+
|
|
169
|
+
# --- status trait ---
|
|
170
|
+
if ok["status"]:
|
|
171
|
+
status = props.status
|
|
172
|
+
|
|
173
|
+
# 白名单字段
|
|
174
|
+
for fname, desc in STATUS_FIELDS.items():
|
|
175
|
+
raw = getattr(status, fname, None)
|
|
176
|
+
result[fname] = field(serialize_value(raw), desc)
|
|
177
|
+
|
|
178
|
+
# computed properties — 用户友好值
|
|
179
|
+
result["state"] = field(status.state_name, "设备状态 (charging/cleaning/paused/returning_home/idle 等)")
|
|
180
|
+
result["error_code"] = field(status.error_code_name, "错误状态 (none=正常, lidar_blocked/bumper_stuck 等)")
|
|
181
|
+
result["clean_area_m2"] = field(status.square_meter_clean_area, "当前/最近清洁面积 (m²)")
|
|
182
|
+
result["clean_time_minutes"] = field(
|
|
183
|
+
round(status.clean_time / 60, 1) if status.clean_time else 0,
|
|
184
|
+
"当前/最近清洁时长 (分钟)",
|
|
185
|
+
)
|
|
186
|
+
result["fan_speed_name"] = field(status.fan_speed_name, "吸力档位名称")
|
|
187
|
+
result["water_mode_name"] = field(status.water_mode_name, "拖布湿度名称")
|
|
188
|
+
result["mop_route_name"] = field(status.mop_route_name, "拖地路线名称")
|
|
189
|
+
|
|
190
|
+
# dss 拆解(不输出原始 dss)
|
|
191
|
+
result["clear_water_box_status"] = field(
|
|
192
|
+
serialize_value(status.clear_water_box_status), "清水箱状态 (okay/out_of_water)")
|
|
193
|
+
result["dirty_water_box_status"] = field(
|
|
194
|
+
serialize_value(status.dirty_water_box_status), "污水箱状态 (okay/full_not_installed)")
|
|
195
|
+
result["dust_bag_status"] = field(
|
|
196
|
+
serialize_value(status.dust_bag_status), "尘袋状态 (okay/not_installed/full)")
|
|
197
|
+
result["water_box_filter_status"] = field(
|
|
198
|
+
status.water_box_filter_status, "水滤网状态")
|
|
199
|
+
result["clean_fluid_status"] = field(
|
|
200
|
+
serialize_value(status.clean_fluid_status), "清洁液状态 (okay/empty_not_installed)")
|
|
201
|
+
result["hatch_door_status"] = field(
|
|
202
|
+
status.hatch_door_status, "舱门状态")
|
|
203
|
+
result["dock_cool_fan_status"] = field(
|
|
204
|
+
status.dock_cool_fan_status, "基座散热风扇状态")
|
|
205
|
+
|
|
206
|
+
# --- dnd ---
|
|
207
|
+
if ok["dnd"]:
|
|
208
|
+
dnd = props.dnd
|
|
209
|
+
result["dnd_enabled"] = field(dnd.is_on, "勿扰模式是否启用")
|
|
210
|
+
result["dnd_start_time"] = field(
|
|
211
|
+
f"{dnd.start_hour:02d}:{dnd.start_minute:02d}" if dnd.is_on else None,
|
|
212
|
+
"勿扰开始时间 (HH:MM)",
|
|
213
|
+
)
|
|
214
|
+
result["dnd_end_time"] = field(
|
|
215
|
+
f"{dnd.end_hour:02d}:{dnd.end_minute:02d}" if dnd.is_on else None,
|
|
216
|
+
"勿扰结束时间 (HH:MM)",
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
# --- clean_summary ---
|
|
220
|
+
if ok["clean_summary"]:
|
|
221
|
+
summary = props.clean_summary
|
|
222
|
+
result["total_clean_time_hours"] = field(
|
|
223
|
+
round(summary.clean_time / 3600, 1) if summary.clean_time else 0,
|
|
224
|
+
"累计清洁时长 (小时)",
|
|
225
|
+
)
|
|
226
|
+
result["total_clean_area_m2"] = field(
|
|
227
|
+
round(summary.clean_area / 1_000_000, 1) if summary.clean_area else 0,
|
|
228
|
+
"累计清洁面积 (m²)",
|
|
229
|
+
)
|
|
230
|
+
result["total_clean_count"] = field(summary.clean_count, "累计清洁次数")
|
|
231
|
+
result["clean_record_count"] = field(
|
|
232
|
+
len(summary.records) if summary.records else 0, "清洁记录条数")
|
|
233
|
+
|
|
234
|
+
if summary.last_clean_record:
|
|
235
|
+
record = summary.last_clean_record
|
|
236
|
+
result["last_clean_begin"] = field(
|
|
237
|
+
record.begin_datetime.isoformat() if record.begin_datetime else None,
|
|
238
|
+
"最近清洁开始时间 (ISO8601)",
|
|
239
|
+
)
|
|
240
|
+
result["last_clean_end"] = field(
|
|
241
|
+
record.end_datetime.isoformat() if record.end_datetime else None,
|
|
242
|
+
"最近清洁结束时间 (ISO8601)",
|
|
243
|
+
)
|
|
244
|
+
result["last_clean_duration_seconds"] = field(
|
|
245
|
+
record.duration, "最近清洁时长 (秒)")
|
|
246
|
+
result["last_clean_area_m2"] = field(
|
|
247
|
+
record.area / 1_000_000 if record.area else 0,
|
|
248
|
+
"最近清洁面积 (m²)",
|
|
249
|
+
)
|
|
250
|
+
result["last_clean_start_type"] = field(
|
|
251
|
+
record.start_type.name if record.start_type else None,
|
|
252
|
+
"启动方式 (app/schedule/button/routines 等)",
|
|
253
|
+
)
|
|
254
|
+
result["last_clean_avoid_count"] = field(
|
|
255
|
+
record.avoid_count, "最近清洁避障次数")
|
|
256
|
+
result["last_clean_wash_count"] = field(
|
|
257
|
+
record.wash_count, "最近清洁洗拖布次数")
|
|
258
|
+
|
|
259
|
+
# --- sound_volume ---
|
|
260
|
+
if ok["sound_volume"]:
|
|
261
|
+
result["volume"] = field(props.sound_volume.volume, "语音音量 (0-100)")
|
|
262
|
+
|
|
263
|
+
# --- rooms ---
|
|
264
|
+
if ok["rooms"]:
|
|
265
|
+
rooms = props.rooms
|
|
266
|
+
room_list = []
|
|
267
|
+
for room in rooms.rooms or []:
|
|
268
|
+
room_list.append({
|
|
269
|
+
"segment_id": room.segment_id,
|
|
270
|
+
"iot_id": room.iot_id,
|
|
271
|
+
"name": room.raw_name or "<未命名>",
|
|
272
|
+
})
|
|
273
|
+
result["room_count"] = field(len(room_list), "房间数量")
|
|
274
|
+
result["rooms"] = field(room_list, "房间列表 (含 segment_id 用于指定房间清洁)")
|
|
275
|
+
|
|
276
|
+
# --- maps ---
|
|
277
|
+
if ok["maps"]:
|
|
278
|
+
maps = props.maps
|
|
279
|
+
map_list = []
|
|
280
|
+
for map_info in maps.map_info or []:
|
|
281
|
+
map_list.append({
|
|
282
|
+
"map_flag": map_info.map_flag,
|
|
283
|
+
"name": map_info.name or "<未命名>",
|
|
284
|
+
"room_count": len(map_info.rooms) if map_info.rooms else 0,
|
|
285
|
+
"is_current": map_info.map_flag == maps.current_map,
|
|
286
|
+
})
|
|
287
|
+
result["current_map_flag"] = field(maps.current_map, "当前地图 ID")
|
|
288
|
+
result["map_count"] = field(len(map_list), "地图数量")
|
|
289
|
+
result["maps"] = field(map_list, "地图列表")
|
|
290
|
+
|
|
291
|
+
# --- consumables ---
|
|
292
|
+
if ok["consumables"]:
|
|
293
|
+
consumables = props.consumables
|
|
294
|
+
SECS_PER_HOUR = 3600
|
|
295
|
+
result["main_brush_hours"] = field(
|
|
296
|
+
round(consumables.main_brush_work_time / SECS_PER_HOUR, 1)
|
|
297
|
+
if consumables.main_brush_work_time else 0,
|
|
298
|
+
"主刷已使用时长 (小时,建议 300 小时更换)",
|
|
299
|
+
)
|
|
300
|
+
result["side_brush_hours"] = field(
|
|
301
|
+
round(consumables.side_brush_work_time / SECS_PER_HOUR, 1)
|
|
302
|
+
if consumables.side_brush_work_time else 0,
|
|
303
|
+
"边刷已使用时长 (小时,建议 200 小时更换)",
|
|
304
|
+
)
|
|
305
|
+
result["filter_hours"] = field(
|
|
306
|
+
round(consumables.filter_work_time / SECS_PER_HOUR, 1)
|
|
307
|
+
if consumables.filter_work_time else 0,
|
|
308
|
+
"滤网已使用时长 (小时,建议 150 小时更换)",
|
|
309
|
+
)
|
|
310
|
+
result["sensor_hours"] = field(
|
|
311
|
+
round(consumables.sensor_dirty_time / SECS_PER_HOUR, 1)
|
|
312
|
+
if consumables.sensor_dirty_time else 0,
|
|
313
|
+
"传感器清洁后使用时长 (小时,建议 30 小时清洁)",
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
return result
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
@mcp.tool(
|
|
320
|
+
annotations={
|
|
321
|
+
"title": "Get Map Content",
|
|
322
|
+
"readOnlyHint": True,
|
|
323
|
+
"destructiveHint": False,
|
|
324
|
+
"idempotentHint": False,
|
|
325
|
+
"openWorldHint": True,
|
|
326
|
+
}
|
|
327
|
+
)
|
|
328
|
+
@_handle_errors
|
|
329
|
+
async def get_map_content(device_name: str | None = None):
|
|
330
|
+
"""获取当前地图的图片和元数据(通过 MQTT 拉取,数据量较大)。
|
|
331
|
+
|
|
332
|
+
返回地图 PNG 图片(通过 MCP Image 协议直接传输)和元数据(JSON)。
|
|
333
|
+
元数据包含地图图片大小、机器人实时坐标、充电座坐标。
|
|
334
|
+
此工具走 MQTT 拉取地图数据(几百KB~几MB),比 get_status 更重,
|
|
335
|
+
仅在需要地图或位置信息时调用。
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
device_name: 设备名称,支持模糊匹配。单设备时可省略。
|
|
339
|
+
"""
|
|
340
|
+
conn = get_conn()
|
|
341
|
+
device = await conn.get_device(device_name)
|
|
342
|
+
|
|
343
|
+
if not device.v1_properties:
|
|
344
|
+
return {"error": "设备不支持 V1 协议"}
|
|
345
|
+
|
|
346
|
+
props = device.v1_properties
|
|
347
|
+
await props.map_content.refresh()
|
|
348
|
+
content = props.map_content
|
|
349
|
+
|
|
350
|
+
metadata = {
|
|
351
|
+
"device_name": device.name,
|
|
352
|
+
"image_size_bytes": len(content.image_content) if content.image_content else 0,
|
|
353
|
+
"has_map_data": content.map_data is not None,
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if content.map_data:
|
|
357
|
+
vacuum_pos = content.map_data.vacuum_position
|
|
358
|
+
charger_pos = content.map_data.charger
|
|
359
|
+
metadata["vacuum_position"] = {
|
|
360
|
+
"x": vacuum_pos.x if vacuum_pos else None,
|
|
361
|
+
"y": vacuum_pos.y if vacuum_pos else None,
|
|
362
|
+
}
|
|
363
|
+
metadata["charger_position"] = {
|
|
364
|
+
"x": charger_pos.x if charger_pos else None,
|
|
365
|
+
"y": charger_pos.y if charger_pos else None,
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
result = [TextContent(type="text", text=json.dumps(metadata, ensure_ascii=False))]
|
|
369
|
+
|
|
370
|
+
if content.image_content:
|
|
371
|
+
result.append(Image(data=content.image_content, format="png"))
|
|
372
|
+
|
|
373
|
+
return result
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
@mcp.tool(
|
|
377
|
+
annotations={
|
|
378
|
+
"title": "List Routines",
|
|
379
|
+
"readOnlyHint": True,
|
|
380
|
+
"destructiveHint": False,
|
|
381
|
+
"idempotentHint": True,
|
|
382
|
+
"openWorldHint": True,
|
|
383
|
+
}
|
|
384
|
+
)
|
|
385
|
+
@_handle_errors
|
|
386
|
+
async def get_routines(device_name: str | None = None) -> dict[str, Any]:
|
|
387
|
+
"""列出设备的智能场景(通过云端 API 获取)。
|
|
388
|
+
|
|
389
|
+
返回可执行的预设场景列表,每个场景有 id 和名称。
|
|
390
|
+
使用 execute_routine 执行具体场景。
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
device_name: 设备名称,支持模糊匹配。单设备时可省略。
|
|
394
|
+
"""
|
|
395
|
+
conn = get_conn()
|
|
396
|
+
device = await conn.get_device(device_name)
|
|
397
|
+
if not device.v1_properties:
|
|
398
|
+
return {"error": "设备不支持 V1 协议"}
|
|
399
|
+
|
|
400
|
+
routines = await device.v1_properties.routines.get_routines()
|
|
401
|
+
return {
|
|
402
|
+
"device_name": device.name,
|
|
403
|
+
"count": len(routines),
|
|
404
|
+
"routines": [{"id": r.id, "name": r.name} for r in routines],
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
# ============================================================================
|
|
409
|
+
# 控制类工具
|
|
410
|
+
# ============================================================================
|
|
411
|
+
|
|
412
|
+
@mcp.tool(
|
|
413
|
+
annotations={
|
|
414
|
+
"title": "Start Cleaning",
|
|
415
|
+
"readOnlyHint": False,
|
|
416
|
+
"destructiveHint": False,
|
|
417
|
+
"idempotentHint": False,
|
|
418
|
+
"openWorldHint": True,
|
|
419
|
+
}
|
|
420
|
+
)
|
|
421
|
+
@_handle_errors
|
|
422
|
+
async def start_clean(
|
|
423
|
+
device_name: str | None = None,
|
|
424
|
+
segments: list[int] | None = None,
|
|
425
|
+
repeat: int = 1,
|
|
426
|
+
) -> dict[str, Any]:
|
|
427
|
+
"""开始清洁。
|
|
428
|
+
|
|
429
|
+
不传 segments 则全屋清洁,传 segments 则清洁指定房间。
|
|
430
|
+
房间 segment_id 可通过 get_status 的 rooms 字段获取。
|
|
431
|
+
命令已发送并确认,但设备执行需要几秒,建议稍后调用 get_status 查看最新状态。
|
|
432
|
+
|
|
433
|
+
Args:
|
|
434
|
+
device_name: 设备名称,支持模糊匹配。单设备时可省略。
|
|
435
|
+
segments: 房间 segment_id 列表。不传则全屋清洁。
|
|
436
|
+
repeat: 清洁遍数,默认 1。
|
|
437
|
+
"""
|
|
438
|
+
conn = get_conn()
|
|
439
|
+
device = await conn.get_device(device_name)
|
|
440
|
+
if not device.v1_properties:
|
|
441
|
+
return {"error": "设备不支持 V1 协议"}
|
|
442
|
+
|
|
443
|
+
cmd = device.v1_properties.command
|
|
444
|
+
if segments:
|
|
445
|
+
params = [{"segments": segments, "repeat": repeat}]
|
|
446
|
+
await cmd.send(RoborockCommand.APP_SEGMENT_CLEAN, params=params)
|
|
447
|
+
return {"status": "ok", "action": "segment_clean", "segments": segments, "repeat": repeat}
|
|
448
|
+
else:
|
|
449
|
+
await cmd.send(RoborockCommand.APP_START)
|
|
450
|
+
return {"status": "ok", "action": "full_clean"}
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
@mcp.tool(
|
|
454
|
+
annotations={
|
|
455
|
+
"title": "Stop Cleaning",
|
|
456
|
+
"readOnlyHint": False,
|
|
457
|
+
"destructiveHint": False,
|
|
458
|
+
"idempotentHint": True,
|
|
459
|
+
"openWorldHint": True,
|
|
460
|
+
}
|
|
461
|
+
)
|
|
462
|
+
@_handle_errors
|
|
463
|
+
async def stop_clean(device_name: str | None = None) -> dict[str, Any]:
|
|
464
|
+
"""停止清洁。机器人将原地停止。
|
|
465
|
+
|
|
466
|
+
命令已发送并确认,但设备执行需要几秒,建议稍后调用 get_status 查看最新状态。
|
|
467
|
+
|
|
468
|
+
Args:
|
|
469
|
+
device_name: 设备名称,支持模糊匹配。单设备时可省略。
|
|
470
|
+
"""
|
|
471
|
+
conn = get_conn()
|
|
472
|
+
device = await conn.get_device(device_name)
|
|
473
|
+
if not device.v1_properties:
|
|
474
|
+
return {"error": "设备不支持 V1 协议"}
|
|
475
|
+
|
|
476
|
+
await device.v1_properties.command.send(RoborockCommand.APP_STOP)
|
|
477
|
+
return {"status": "ok", "action": "stop"}
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
@mcp.tool(
|
|
481
|
+
annotations={
|
|
482
|
+
"title": "Pause Cleaning",
|
|
483
|
+
"readOnlyHint": False,
|
|
484
|
+
"destructiveHint": False,
|
|
485
|
+
"idempotentHint": True,
|
|
486
|
+
"openWorldHint": True,
|
|
487
|
+
}
|
|
488
|
+
)
|
|
489
|
+
@_handle_errors
|
|
490
|
+
async def pause_clean(device_name: str | None = None) -> dict[str, Any]:
|
|
491
|
+
"""暂停清洁。可通过 resume_clean 恢复。
|
|
492
|
+
|
|
493
|
+
命令已发送并确认,但设备执行需要几秒,建议稍后调用 get_status 查看最新状态。
|
|
494
|
+
|
|
495
|
+
Args:
|
|
496
|
+
device_name: 设备名称,支持模糊匹配。单设备时可省略。
|
|
497
|
+
"""
|
|
498
|
+
conn = get_conn()
|
|
499
|
+
device = await conn.get_device(device_name)
|
|
500
|
+
if not device.v1_properties:
|
|
501
|
+
return {"error": "设备不支持 V1 协议"}
|
|
502
|
+
|
|
503
|
+
await device.v1_properties.command.send(RoborockCommand.APP_PAUSE)
|
|
504
|
+
return {"status": "ok", "action": "pause"}
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
@mcp.tool(
|
|
508
|
+
annotations={
|
|
509
|
+
"title": "Resume Cleaning",
|
|
510
|
+
"readOnlyHint": False,
|
|
511
|
+
"destructiveHint": False,
|
|
512
|
+
"idempotentHint": True,
|
|
513
|
+
"openWorldHint": True,
|
|
514
|
+
}
|
|
515
|
+
)
|
|
516
|
+
@_handle_errors
|
|
517
|
+
async def resume_clean(device_name: str | None = None) -> dict[str, Any]:
|
|
518
|
+
"""恢复之前暂停的清洁任务。
|
|
519
|
+
|
|
520
|
+
使用 APP_START 恢复,无论全屋还是分区清洁均可恢复。
|
|
521
|
+
命令已发送并确认,但设备执行需要几秒,建议稍后调用 get_status 查看最新状态。
|
|
522
|
+
|
|
523
|
+
Args:
|
|
524
|
+
device_name: 设备名称,支持模糊匹配。单设备时可省略。
|
|
525
|
+
"""
|
|
526
|
+
conn = get_conn()
|
|
527
|
+
device = await conn.get_device(device_name)
|
|
528
|
+
if not device.v1_properties:
|
|
529
|
+
return {"error": "设备不支持 V1 协议"}
|
|
530
|
+
|
|
531
|
+
await device.v1_properties.command.send(RoborockCommand.APP_START)
|
|
532
|
+
return {"status": "ok", "action": "resume"}
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
@mcp.tool(
|
|
536
|
+
annotations={
|
|
537
|
+
"title": "Go Home",
|
|
538
|
+
"readOnlyHint": False,
|
|
539
|
+
"destructiveHint": False,
|
|
540
|
+
"idempotentHint": True,
|
|
541
|
+
"openWorldHint": True,
|
|
542
|
+
}
|
|
543
|
+
)
|
|
544
|
+
@_handle_errors
|
|
545
|
+
async def go_home(device_name: str | None = None) -> dict[str, Any]:
|
|
546
|
+
"""让机器人回充电座。
|
|
547
|
+
|
|
548
|
+
命令已发送并确认,但设备执行需要几秒,建议稍后调用 get_status 查看最新状态。
|
|
549
|
+
|
|
550
|
+
Args:
|
|
551
|
+
device_name: 设备名称,支持模糊匹配。单设备时可省略。
|
|
552
|
+
"""
|
|
553
|
+
conn = get_conn()
|
|
554
|
+
device = await conn.get_device(device_name)
|
|
555
|
+
if not device.v1_properties:
|
|
556
|
+
return {"error": "设备不支持 V1 协议"}
|
|
557
|
+
|
|
558
|
+
await device.v1_properties.command.send(RoborockCommand.APP_CHARGE)
|
|
559
|
+
return {"status": "ok", "action": "go_home"}
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
@mcp.tool(
|
|
563
|
+
annotations={
|
|
564
|
+
"title": "Find Robot",
|
|
565
|
+
"readOnlyHint": False,
|
|
566
|
+
"destructiveHint": False,
|
|
567
|
+
"idempotentHint": True,
|
|
568
|
+
"openWorldHint": True,
|
|
569
|
+
}
|
|
570
|
+
)
|
|
571
|
+
@_handle_errors
|
|
572
|
+
async def find_robot(device_name: str | None = None) -> dict[str, Any]:
|
|
573
|
+
"""让机器人发出声音,帮助定位。
|
|
574
|
+
|
|
575
|
+
命令已发送并确认,但设备执行需要几秒。
|
|
576
|
+
|
|
577
|
+
Args:
|
|
578
|
+
device_name: 设备名称,支持模糊匹配。单设备时可省略。
|
|
579
|
+
"""
|
|
580
|
+
conn = get_conn()
|
|
581
|
+
device = await conn.get_device(device_name)
|
|
582
|
+
if not device.v1_properties:
|
|
583
|
+
return {"error": "设备不支持 V1 协议"}
|
|
584
|
+
|
|
585
|
+
await device.v1_properties.command.send(RoborockCommand.FIND_ME)
|
|
586
|
+
return {"status": "ok", "action": "find_robot"}
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
@mcp.tool(
|
|
590
|
+
annotations={
|
|
591
|
+
"title": "Dock Action",
|
|
592
|
+
"readOnlyHint": False,
|
|
593
|
+
"destructiveHint": False,
|
|
594
|
+
"idempotentHint": False,
|
|
595
|
+
"openWorldHint": True,
|
|
596
|
+
}
|
|
597
|
+
)
|
|
598
|
+
@_handle_errors
|
|
599
|
+
async def dock_action(
|
|
600
|
+
action: str,
|
|
601
|
+
device_name: str | None = None,
|
|
602
|
+
) -> dict[str, Any]:
|
|
603
|
+
"""控制基座操作。
|
|
604
|
+
|
|
605
|
+
命令已发送并确认,但设备执行需要几秒,建议稍后调用 get_status 查看最新状态。
|
|
606
|
+
|
|
607
|
+
Args:
|
|
608
|
+
action: 操作类型。可选值:
|
|
609
|
+
- "start_collect_dust": 开始集尘
|
|
610
|
+
- "stop_collect_dust": 停止集尘
|
|
611
|
+
- "start_wash": 开始洗拖布
|
|
612
|
+
- "stop_wash": 停止洗拖布
|
|
613
|
+
device_name: 设备名称,支持模糊匹配。单设备时可省略。
|
|
614
|
+
"""
|
|
615
|
+
ACTION_MAP = {
|
|
616
|
+
"start_collect_dust": RoborockCommand.APP_START_COLLECT_DUST,
|
|
617
|
+
"stop_collect_dust": RoborockCommand.APP_STOP_COLLECT_DUST,
|
|
618
|
+
"start_wash": RoborockCommand.APP_START_WASH,
|
|
619
|
+
"stop_wash": RoborockCommand.APP_STOP_WASH,
|
|
620
|
+
}
|
|
621
|
+
if action not in ACTION_MAP:
|
|
622
|
+
return {"error": f"未知操作: {action},可选: {', '.join(ACTION_MAP)}"}
|
|
623
|
+
|
|
624
|
+
conn = get_conn()
|
|
625
|
+
device = await conn.get_device(device_name)
|
|
626
|
+
if not device.v1_properties:
|
|
627
|
+
return {"error": "设备不支持 V1 协议"}
|
|
628
|
+
|
|
629
|
+
await device.v1_properties.command.send(ACTION_MAP[action])
|
|
630
|
+
return {"status": "ok", "action": action}
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
@mcp.tool(
|
|
634
|
+
annotations={
|
|
635
|
+
"title": "Set Volume",
|
|
636
|
+
"readOnlyHint": False,
|
|
637
|
+
"destructiveHint": False,
|
|
638
|
+
"idempotentHint": True,
|
|
639
|
+
"openWorldHint": True,
|
|
640
|
+
}
|
|
641
|
+
)
|
|
642
|
+
@_handle_errors
|
|
643
|
+
async def set_volume(
|
|
644
|
+
device_name: str | None = None,
|
|
645
|
+
volume: int = 50,
|
|
646
|
+
) -> dict[str, Any]:
|
|
647
|
+
"""设置机器人语音音量。
|
|
648
|
+
|
|
649
|
+
命令已发送并确认,但设备执行需要几秒,建议稍后调用 get_status 查看最新状态。
|
|
650
|
+
|
|
651
|
+
Args:
|
|
652
|
+
device_name: 设备名称,支持模糊匹配。单设备时可省略。
|
|
653
|
+
volume: 音量值,0-100。
|
|
654
|
+
"""
|
|
655
|
+
if not 0 <= volume <= 100:
|
|
656
|
+
return {"error": "音量值需在 0-100 之间"}
|
|
657
|
+
|
|
658
|
+
conn = get_conn()
|
|
659
|
+
device = await conn.get_device(device_name)
|
|
660
|
+
if not device.v1_properties:
|
|
661
|
+
return {"error": "设备不支持 V1 协议"}
|
|
662
|
+
|
|
663
|
+
await device.v1_properties.sound_volume.set_volume(volume)
|
|
664
|
+
return {"status": "ok", "volume": volume}
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
@mcp.tool(
|
|
668
|
+
annotations={
|
|
669
|
+
"title": "Set Do Not Disturb",
|
|
670
|
+
"readOnlyHint": False,
|
|
671
|
+
"destructiveHint": False,
|
|
672
|
+
"idempotentHint": True,
|
|
673
|
+
"openWorldHint": True,
|
|
674
|
+
}
|
|
675
|
+
)
|
|
676
|
+
@_handle_errors
|
|
677
|
+
async def set_dnd(
|
|
678
|
+
action: str,
|
|
679
|
+
start_hour: int = 22,
|
|
680
|
+
start_minute: int = 0,
|
|
681
|
+
end_hour: int = 8,
|
|
682
|
+
end_minute: int = 0,
|
|
683
|
+
device_name: str | None = None,
|
|
684
|
+
) -> dict[str, Any]:
|
|
685
|
+
"""设置勿扰模式。
|
|
686
|
+
|
|
687
|
+
enable 同时设置时间段并启用勿扰,disable 关闭勿扰。
|
|
688
|
+
默认勿扰时间段 22:00-08:00,支持跨天(如 23:00-07:00)。
|
|
689
|
+
命令已发送并确认,建议稍后调用 get_status 查看最新状态。
|
|
690
|
+
|
|
691
|
+
Args:
|
|
692
|
+
action: 操作类型。可选值:
|
|
693
|
+
- "enable": 启用勿扰并设置时间段
|
|
694
|
+
- "disable": 关闭勿扰
|
|
695
|
+
start_hour: 勿扰开始小时 (0-23),默认 22。
|
|
696
|
+
start_minute: 勿扰开始分钟 (0-59),默认 0。
|
|
697
|
+
end_hour: 勿扰结束小时 (0-23),默认 8。
|
|
698
|
+
end_minute: 勿扰结束分钟 (0-59),默认 0。
|
|
699
|
+
device_name: 设备名称,支持模糊匹配。单设备时可省略。
|
|
700
|
+
"""
|
|
701
|
+
if action not in ("enable", "disable"):
|
|
702
|
+
return {"error": f"未知操作: {action},可选: enable, disable"}
|
|
703
|
+
|
|
704
|
+
conn = get_conn()
|
|
705
|
+
device = await conn.get_device(device_name)
|
|
706
|
+
if not device.v1_properties:
|
|
707
|
+
return {"error": "设备不支持 V1 协议"}
|
|
708
|
+
|
|
709
|
+
dnd = device.v1_properties.dnd
|
|
710
|
+
|
|
711
|
+
if action == "enable":
|
|
712
|
+
# 校验时间范围
|
|
713
|
+
if not (0 <= start_hour <= 23 and 0 <= end_hour <= 23):
|
|
714
|
+
return {"error": "小时值需在 0-23 之间"}
|
|
715
|
+
if not (0 <= start_minute <= 59 and 0 <= end_minute <= 59):
|
|
716
|
+
return {"error": "分钟值需在 0-59 之间"}
|
|
717
|
+
|
|
718
|
+
timer = DnDTimer(start_hour, start_minute, end_hour, end_minute)
|
|
719
|
+
await dnd.set_dnd_timer(timer)
|
|
720
|
+
return {
|
|
721
|
+
"status": "ok",
|
|
722
|
+
"action": "enable",
|
|
723
|
+
"time": f"{start_hour:02d}:{start_minute:02d}-{end_hour:02d}:{end_minute:02d}",
|
|
724
|
+
}
|
|
725
|
+
else:
|
|
726
|
+
await dnd.disable()
|
|
727
|
+
return {"status": "ok", "action": "disable"}
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
@mcp.tool(
|
|
731
|
+
annotations={
|
|
732
|
+
"title": "Execute Routine",
|
|
733
|
+
"readOnlyHint": False,
|
|
734
|
+
"destructiveHint": False,
|
|
735
|
+
"idempotentHint": False,
|
|
736
|
+
"openWorldHint": True,
|
|
737
|
+
}
|
|
738
|
+
)
|
|
739
|
+
@_handle_errors
|
|
740
|
+
async def execute_routine(
|
|
741
|
+
routine_id: int,
|
|
742
|
+
device_name: str | None = None,
|
|
743
|
+
) -> dict[str, Any]:
|
|
744
|
+
"""执行智能场景。
|
|
745
|
+
|
|
746
|
+
通过 get_routines 获取可用场景及其 ID,然后传入 routine_id 执行。
|
|
747
|
+
命令已发送并确认,但设备执行需要几秒,建议稍后调用 get_status 查看最新状态。
|
|
748
|
+
|
|
749
|
+
Args:
|
|
750
|
+
routine_id: 场景 ID,通过 get_routines 获取。必填参数。
|
|
751
|
+
device_name: 设备名称,支持模糊匹配。单设备时可省略。
|
|
752
|
+
"""
|
|
753
|
+
conn = get_conn()
|
|
754
|
+
device = await conn.get_device(device_name)
|
|
755
|
+
if not device.v1_properties:
|
|
756
|
+
return {"error": "设备不支持 V1 协议"}
|
|
757
|
+
|
|
758
|
+
await device.v1_properties.routines.execute_routine(routine_id)
|
|
759
|
+
return {"status": "ok", "routine_id": routine_id}
|