跳转到内容

roborock(石头扫地机)

石头扫地机走局域网直连(AES-256-GCM 加密的私有协议),运行期完全不接触石头云。 接一台要两样东西:IP 和 local_key。

[[device]]
name = "robot"
adapter = "roborock"
ip = "192.168.52.66"
local_key = "${ROBOROCK_LOCAL_KEY}"
consumable_every = 40

rha 不做石头云登录,crate 里连 HTTP 客户端都没有。这不是偷懒:那条路会因为「用户 协议版本」这类服务端状态整个卡死(实测先撞 2031 需要二次验证,再撞 3006 用户协议), 把它焊进守护进程等于引入一个不可控的外部故障源。

取法任选其一,都只做一次:

Terminal window
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?)——别误诊成网络问题。

这台机器不发 UDP 发现广播(Mac 与 PVE 上各监听约 5 分钟,一条未收到),所以没有 miot 那种「按 device_id 自动重定位」的兜底。建议在路由器上给它做 DHCP 保留,否则租约 一漂就得改配置。

主刷、边刷、滤网寿命是小时级变化的量,没必要跟状态一起每 15 秒问一遍。所以耗材单独 分频:每 consumable_every 轮状态轮询才拉一次,缺省 40 轮 ≈ 10 分钟。

写 0 会在 check 阶段直接报 consumable_every must be >= 1——不拦的话它会变成「无声地 永不拉耗材」,那些实体永远没有值。

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 走 三合一命令把另外两项钉住。

实体 是什么
robot.dryer_auto 配置:洗完拖布要不要自动烘
robot.dry_time 配置:烘多久(秒)
robot.drying 只读状态:现在正不正在烘,烘完会自己归零
action = "dry" 动作:现在立刻烘一次,不改任何配置

设备把「配置开关」和「运行状态」放在两条不同的命令里,读写对不上,所以这里拆开而 不是合成一个开关。

action = "wash" / "stop_wash" 开始 / 停止洗拖布,真机实测:洗拖布结束后基站 会自动开始烘干(app_stop_wash 之后 dry_status 由 0 变 1),所以 "wash" 之后 不需要再手动发 "dry"。

字段类型默认说明
consumable_everyinteger40

每多少轮状态轮询才拉一次耗材。耗材是小时级变化的量。

dock_everyinteger40

每多少轮状态轮询才拉一次基站设置(烘干时长、洗拖档位、集尘模式等)。

与 consumable_every 分开是刻意的:耗材是磨损量, 基站设置是用户在 App 里会改 的配置, 两者该有各自的节奏。缺省同为 40 轮 ≈ 10 分钟。

ip*string—

扫地机在局域网里的地址, 必须是字面 IP: 主机名/mDNS 名过不了 rha check (报 bad ip)。整条链路是直连设备的 58867 端口, 不经过任何云服务。

与 miot 不同, 这里没有"配了 device_id 就自动重新定位"那一层(见 rha_adapter_miot 的 device_id): DHCP 把地址换掉之后, 表现就是一直连不上、 supervise 退避重试到你改配置为止。所以在路由器上给它留个固定地址。

local_key*string
Secret
—

敏感值。建议写成 ${ENV} 由环境变量插值, 不要把明文提交进配置文件。该值绝不进日志与 trace。

max_timeoutsinteger3

连续多少轮轮询超时后放弃(默认 3), 交给 supervise 标 unavailable + 指数退避重启。

只数超时(连着但不回话)。传输层断开不计入 —— 设备本来就会隔几分钟主动断一次, 那条路径是内部透明重连, 属常态而非故障。任何一轮成功都会把计数清零。

因此从设备真的失联到实体变 unavailable, 大约要 max_timeouts 轮(默认约半分钟); 想更快发现就一起调小 poll_ms, 而不是只把这里改成 1 —— 那样一次偶发丢包就会 触发一次重启。

poll_msinteger15000

状态轮询间隔, 默认 15 秒。一"轮"就是一次 get_status: 除三个耗材实体(它们走 consumable_every 分频)和只用于下发的 action/clean_room 外, 其余实体的值 都由它刷新。

它同时是 consumable_every 的计时单位: 耗材的真实间隔是 poll_ms × consumable_every(默认 15 秒 × 40 = 10 分钟), 改这里会连带把耗材 节奏一起改掉。

往大调是安全的: 连接保活是独立的 10 秒 PING(设备约 4.5 分钟收不到包就会主动 断连), 不靠状态轮询撑着。

poll_timeout_msinteger5000

每一次 RPC 等响应的上限(毫秒), 默认 5000 —— 不只是状态轮询: 拉耗材、保活 PING、下发命令, 以及分区清扫前那次 get_room_mapping 校验都按它计时。

调小了先坏的往往是命令: 设备执行慢一点就会以 Timeout 回执告诉规则引擎, 而机器 其实动了。它与 max_timeouts 一起决定多久判定设备失联。