roborock(石头扫地机)
石头扫地机走局域网直连(AES-256-GCM 加密的私有协议),运行期完全不接触石头云。
接一台要两样东西:IP 和 local_key。
[[device]]name = "robot"adapter = "roborock"ip = "192.168.52.66"local_key = "${ROBOROCK_LOCAL_KEY}"consumable_every = 40local_key 必须带外获取
Section titled “local_key 必须带外获取”rha 不做石头云登录,crate 里连 HTTP 客户端都没有。这不是偷懒:那条路会因为「用户
协议版本」这类服务端状态整个卡死(实测先撞 2031 需要二次验证,再撞 3006 用户协议),
把它焊进守护进程等于引入一个不可控的外部故障源。
取法任选其一,都只做一次:
pipx run --spec python-roborock roborock login --email <你的邮箱>pipx run --spec python-roborock roborock list_devices # 取目标设备的 localKey或者从已在用的 Home Assistant Roborock 集成的配置项里读同一个值。登录被 3006 拒是
石头服务端要求重新接受用户协议——先在石头 App 里点一次确认再重试。
设备不重新配网,这个 key 就不会变。
key 错了的症状是「握手成功但所有命令失败」:握手载荷是空的、不加密,所以错的 key
也能连上。日志里是 payload decryption failed (wrong local_key?)——别误诊成网络问题。
IP 必须写死
Section titled “IP 必须写死”这台机器不发 UDP 发现广播(Mac 与 PVE 上各监听约 5 分钟,一条未收到),所以没有 miot 那种「按 device_id 自动重定位」的兜底。建议在路由器上给它做 DHCP 保留,否则租约 一漂就得改配置。
耗材按 consumable_every 分频
Section titled “耗材按 consumable_every 分频”主刷、边刷、滤网寿命是小时级变化的量,没必要跟状态一起每 15 秒问一遍。所以耗材单独
分频:每 consumable_every 轮状态轮询才拉一次,缺省 40 轮 ≈ 10 分钟。
写 0 会在 check 阶段直接报 consumable_every must be >= 1——不拦的话它会变成「无声地
永不拉耗材」,那些实体永远没有值。
实体集是固定的
Section titled “实体集是固定的”describe() 必须离线(rha check 不允许探测设备),所以实体表写死在代码里,不像 miot
那样从 spec.d 读:
- 传感器:
state、battery、error、clean_area、clean_time、main_brush_life、side_brush_life、filter_life、drying、dock_error、water_shortage、dust_collecting - 可写:
action("clean"/"pause"/"stop"/"dock"/"dry"/"stop_dry"/"wash"/"stop_wash")、fan_power、water_box_mode、clean_room、mop_mode、dryer_auto、dry_time、wash_towel_mode、smart_wash、wash_interval、dust_collection_mode、auto_dust_collection
action 的值是命令词字符串,不是布尔:
[[rule]]name = "clean_at_9am"trigger = { cron = "0 9 * * *" }action = { entity = "robot.action", set = "clean" }未知的命令词一律报错,绝不静默忽略——静默忽略会让规则引擎收到「成功」却什么都没发生。
这台机器上仍然只有 battery 有跨生态的语义类。12 个可写实体分两类,全部没有 class:
action/fan_power/water_box_mode/clean_room/mop_mode/wash_towel_mode/dust_collection_mode的值是命令词或档位整数,dry_time/wash_interval是普通的秒数——这些都不是 class 词表能表达的语义(词表只覆盖 温度/湿度/电量/功率/体重,档位也不算转速Speed)。dryer_auto/smart_wash/auto_dust_collection恰好是布尔开关,DeviceClass::Switch这个兜底项本来标得上,但故意不标:它们不是「一台设备的主开关」,而是这台设备的 附属配置项,标了会在家庭 App 里凭空多出三个没有上下文的开关。真要导出,应当先在 class 词表里给「附属开关」补一个语义,而不是拿Switch兜底凑数。
因此这 12 个可写实体都导不进 HomeKit。
以下两张表覆盖的机型只有 P20 Pro(roborock.vacuum.a134),别的型号未必一样。
下表的档位标签(安静/标准/强力……)来自 python-roborock 的 v1_clean_modes.py
(第三方开源实现的说法),没有逐条在真机上验证过;rha 代码里也不解释这些标签,
只原样透传整数值。
| 实体 | 码值 |
|---|---|
fan_power |
101 安静 / 102 标准 / 103 强力 / 104 最大 / 105 纯拖 / 106 自定义 / 110 智能规划 |
water_box_mode |
200 关 / 201 低 / 202 中 / 203 高 / 204 自定义 / 209 智能规划 |
mop_mode |
300 标准 / 301 深度 / 303 深度+ / 304 快速 / 306 智能规划 |
wash_towel_mode |
0 快洗 / 1 日常 / 2 深度 / 8 超深度 / 10 智能 |
真机上实际碰到过的值(其余码值与全部语义标签都是上面第三方来源,未逐条验证):
| 实体 | 真机碰到过的值 | 怎么碰到的 |
|---|---|---|
fan_power |
104、105 | 104 读到;105 是 set_mop_mode 的隐式副作用 |
fan_power |
101 | 在设备自己的分区默认表(按房间存的清扫档位,出自 set_customize_clean_mode,rha 不接管这条命令)里读到的 |
water_box_mode |
203、201 | 203 读到;201 同上,也是分区默认表里读到的 |
mop_mode |
300、301 | 双向写入验证过(300↔301) |
wash_towel_mode |
10、1 | 双向写入验证过(10↔1) |
dust_collection_mode |
0、1 | 双向写入验证过(0↔1) |
dry_time |
10800、3600 | 双向写入验证过 |
wash_interval |
1200、900 | 双向写入验证过 |
dry_time 与 wash_interval 的单位都是秒。
mop_mode 的写入会连带写回当前的 fan_power 和 water_box_mode。 这不是多余
动作:真机实测直接发 set_mop_mode 会隐式把 fan_power 拽到 105(纯拖),而且把
拖地路径改回去也不会恢复——也就是「设个拖地路径,吸尘被悄悄关掉」。所以 rha 走
三合一命令把另外两项钉住。
烘干有三个实体,别混
Section titled “烘干有三个实体,别混”| 实体 | 是什么 |
|---|---|
robot.dryer_auto |
配置:洗完拖布要不要自动烘 |
robot.dry_time |
配置:烘多久(秒) |
robot.drying |
只读状态:现在正不正在烘,烘完会自己归零 |
action = "dry" |
动作:现在立刻烘一次,不改任何配置 |
设备把「配置开关」和「运行状态」放在两条不同的命令里,读写对不上,所以这里拆开而 不是合成一个开关。
action = "wash" / "stop_wash" 开始 / 停止洗拖布,真机实测:洗拖布结束后基站
会自动开始烘干(app_stop_wash 之后 dry_status 由 0 变 1),所以 "wash" 之后
不需要再手动发 "dry"。
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
consumable_every | integer | 40 | 每多少轮状态轮询才拉一次耗材。耗材是小时级变化的量。 |
dock_every | integer | 40 | 每多少轮状态轮询才拉一次基站设置(烘干时长、洗拖档位、集尘模式等)。 与 |
ip* | string | — | 扫地机在局域网里的地址, 必须是字面 IP: 主机名/mDNS 名过不了 与 miot 不同, 这里没有"配了 device_id 就自动重新定位"那一层(见
|
local_key* | string | — | 敏感值。建议写成 ${ENV} 由环境变量插值, 不要把明文提交进配置文件。该值绝不进日志与 trace。 |
max_timeouts | integer | 3 | 连续多少轮轮询超时后放弃(默认 3), 交给 supervise 标 unavailable + 指数退避重启。 只数超时(连着但不回话)。传输层断开不计入 —— 设备本来就会隔几分钟主动断一次, 那条路径是内部透明重连, 属常态而非故障。任何一轮成功都会把计数清零。 因此从设备真的失联到实体变 unavailable, 大约要 |
poll_ms | integer | 15000 | 状态轮询间隔, 默认 15 秒。一"轮"就是一次 它同时是 往大调是安全的: 连接保活是独立的 10 秒 PING(设备约 4.5 分钟收不到包就会主动 断连), 不靠状态轮询撑着。 |
poll_timeout_ms | integer | 5000 | 每一次 RPC 等响应的上限(毫秒), 默认 5000 —— 不只是状态轮询: 拉耗材、保活
PING、下发命令, 以及分区清扫前那次 调小了先坏的往往是命令: 设备执行慢一点就会以 |