跳转到内容

rha 命令行

rha 没有配置界面。改配置靠编辑 TOML,看状态和排障靠这套命令行——面板是 agent 画给 人看的展示层,不是控制台。所以下面这些命令就是运维这套系统的全部入口。

先分清两类,很多困惑出在这里:

  • serve / check / doctor / miot-token / spec-gen / config-schema 直接读 配置目录干活,不需要 daemon 在跑。

  • 其余全部(status / get / set / history / trace / reload / mcp / dashboard)是 HTTP API 的客户端。它们不读设备也不解析规则,只是把请求发给正在 跑的 daemon。daemon 没起来就是这一条:

    error: request failed: error sending request for url (http://127.0.0.1:8420/api/entities)

--config / --url / --token 是全局参数,放在子命令前后都认:

Terminal window
rha --config /etc/rha status
rha status --config /etc/rha # 完全一样
rha status --url http://192.168.52.40:8420 --token "$RHA_TOKEN"

--config 默认 /etc/rha。--url / --token 不写时从配置目录取——地址是 [api].listen 拼成的 http://<listen>,token 是 [api].token。

环境变量 RHA_URL / RHA_TOKEN 与对应参数等价,命令行参数优先。

status 列全部实体,get <设备名> 只列这一台的:

$ rha status
fan_demo.power true
thermo_demo.temperature 21.5°C
broken_plug.power null

null 是「这个实体还没有过值」——推送型设备(MQTT、BLE 广播)冷启动时正常会这样,等 下一次上报。实体不可用时行尾多一个 [unavailable]。

Terminal window
rha set fan_demo.power on
rha set light.brightness 80

值的解析规则很短:on / true → 布尔真,off / false → 布尔假,能解析成整数就是 整数,能解析成小数就是小数,其余当字符串。之后再按实体声明的类型做一次转换,对不上就 拒绝:

$ rha set fan_demo.power hello
error: 400 Bad Request: value type mismatch for fan_demo.power (Bool)

只有 kind = "switch" 的实体能写。 传感器读数、瞬时信号一律拒掉,而且明说是因为 kind:

$ rha set thermo_demo.temperature 25
error: 400 Bad Request: entity thermo_demo.temperature is not writable (kind: Sensor)

需要 storage 打开,关着的话:

$ rha history fan_demo.power
error: 503 Service Unavailable: storage disabled

时间窗默认是最近 24 小时(--to 缺省为现在,--from 缺省为 --to 减 24 小时), 两个都收 RFC 3339 时间串:

Terminal window
rha history thermo.temperature --from 2026-08-01T00:00:00Z --to 2026-08-02T00:00:00Z
rha history thermo.temperature --hourly

--limit 默认 500,服务端上限 5000(要更多就缩小时间窗分段拉)。

$ rha trace fan_on_hot --limit 5
2026-08-03T01:15:13.936567Z throttled fan_demo.power: Some(Bool(false)) -> Bool(true)
2026-08-03T01:15:13.936538Z done 1 action(s)
2026-08-03T01:15:13.936512Z fired fan_demo.power: Some(Bool(true)) -> Bool(false)

新→旧排列,--limit 默认 20。每一档 outcome 的含义见 automations 的四道闸。

两条限制要知道:

  • 规则名必须在当前生效的规则集里,否则是 404 而不是空列表。刚 rha reload 过、规则 被改名或删掉,这里就查不到了。
  • trace 是内存里的环形缓冲,全部规则共用 1000 条的总容量。 一条每秒都在触发的规则会 把安静规则的历史挤出去;daemon 一重启,全部清空(storage 打开时 trace 另有一份落盘, 但 rha trace 读的是内存那份)。

check —— 离线校验,能进 pre-commit

Section titled “check —— 离线校验,能进 pre-commit”
$ rha --config /etc/rha check
config ok: 1 entities, 0 rules

失败时逐条列到 stderr,最后一行给总数,退出 1:

$ rha --config /etc/rha check
error: automations (x): trigger entity "nope.thing" does not exist
1 error(s)

它完全不碰网络:不连设备、不连 broker、不开蓝牙扫描、不查 DNS。这不是碰巧成立的, 是 adapter 的 describe() 契约撑起来的——每个 adapter 必须能在 离线状态下说清「我这台设备有哪些实体」。

代价和收益都在这条上:收益是 rha check 可以挂 git pre-commit、可以在 CI 里跑,也可以 在完全没有设备的开发机上验整份配置;代价是 rha 不做设备自动发现(那需要联网才知道有 什么实体),MQTT 设备得手写。

rha serve 启动时跑的是同一段校验,所以配置坏了 serve 也起不来(退出 1)。

$ rha reload
reloaded: 1 rules, 0 actions

这三种写法走的是同一条代码路径,效果完全相同:

Terminal window
rha reload # 打 POST /api/reload
kill -HUP <pid> # 直接发信号
systemctl reload rha # ExecReload=/bin/kill -HUP $MAINPID

先校验、全过了才切换。任何一条不过,现役配置一动不动——不存在一次 reload 把系统搞挂 的路径。

现在能热加载的是两样:

  • 规则与命名动作(automations/*.toml)
  • 设备的增、删、改(devices.toml)。加一台设备不会重启 daemon,既有设备的连接、 会话、轮询节奏都不受影响;改参数(比如 IP 变了)等价于只重启那一台。判断「变没变」 用的是 devices.toml 里那一条的原文,所以只改 label 也会让这台设备重连一次——换来 的是不会出现「改了显示名 reload 说成功、实际还是旧名字」这种静默失效。

仍然要 systemctl restart 的:[api] / [storage] / [llm] / [notify] 四段, 以及 bridges.toml 和 spec.d/(型号表在启动时读进内存,跑着的 adapter 手里就是那一份)。

还有一个可见的副作用:设备集一变,全部 bridge 会重起一遍。 桥的导出集跟着设备走, 不重起就还导着旧的那批。表现是 HomeKit 断开重连大约一秒。

Terminal window
rha --config /etc/rha serve

前台运行,不会自己变成守护进程;rha listening on 127.0.0.1:8420 这一行走 stdout, 日志全部走 stderr(这是全局固定的,为的是 rha mcp 的 stdout 只有 JSON-RPC 帧)。 日志级别用 RUST_LOG 调,默认 info。

信号处置:

  • SIGTERM(systemctl stop|restart 发的)与 SIGINT(Ctrl-C)走同一条优雅关停路径。
  • SIGHUP 触发一次 reload,结果写日志,reload 失败不会让进程退出。

systemd 单元用 Type=notify:就绪后发 READY=1,关停时发 STOPPING=1,并按 WatchdogSec 喂狗。所以 systemctl start rha 返回时服务是真的在听了,不是「进程起来了 但还在初始化」。

$ rha --config /etc/rha doctor
[OK] config: 1 entities, 0 rules
[WARN] miot-spec: /etc/rha/spec.d 里没有任何型号 —— 跑 `rha spec-gen` 生成
[OK] adapter-registration: 1 device(s), all adapters registered
[OK] storage: storage disabled
[WARN] bluetooth: bluetooth check unavailable (needs a linux build with the mibeacon feature)
[WARN] device-reachability: lamp (192.168.52.31:54321) unreachable (device may be off)
[OK] token-format: all tokens/bindkeys are valid 32-hex

逐项查什么:

检查项 查的是 失败等级
config 与 rha check 完全同一条路径:加载四份配置 + 语义校验 FAIL
miot-spec spec.d/ 能不能读、里面有几个型号 空目录 WARN,读取出错 FAIL
adapter-registration 每台设备的 adapter 名在不在本二进制注册的列表里 FAIL
storage 在 db 文件的父目录里建一个探测文件再删掉,验可写 FAIL
bluetooth 能不能开 bluez 会话、有没有默认适配器 一律 WARN
device-reachability 只探 miot(UDP hello 包)和 reolink(TCP 80/443),各 2 秒超时 一律 WARN
token-format miot 的 token 与 mibeacon 的 bindkey 是不是 32 位十六进制。纯格式,不联网 FAIL

miot-spec 单独列一项是有原因的:这个目录缺了的话,每台 miot 设备都会报「型号不在 spec.d 里」,真正的原因(目录压根不在)会被埋在一堆派生错误里。

doctor 是 check 的超集,但它会碰网络(UDP/TCP 探测、BLE 会话)。所以能进 pre-commit 的是 check,不是 doctor。

还有一个读日志的技巧:配置压根加载不出来时,miot-spec 那一行会整个消失,其余各项 变成 skipped: config failed to load。看到一份只有 [FAIL] config 加一串 skipped 的 输出,先修 TOML 语法,别看后面。

这三个都是运维时命令:联网(或读代码里的静态信息)产出配置材料,跑完就完事,不参与 serve。

miot-token —— 从米家云取设备凭据

Section titled “miot-token —— 从米家云取设备凭据”
Terminal window
export MI_PASSWORD='...'
rha miot-token --username 13800138000 --region cn

输出一台设备一行,token 填进 miot 设备的 token 字段, bindkey 填进 mibeacon 设备的 bindkey 字段(同行给出 mac,用来跟配置对上号)。

--password 能直接写在命令行上,但建议走 MI_PASSWORD 环境变量:argv 对同一台机器 上的其他用户 ps 可见,也会进 shell history。--region 默认 cn,其他区域(us / eu / ru / in)会换成对应的子域。

两个会让人以为出了故障、其实是正常提示的地方:

  • 云端没给某台设备 token(常见于网关子设备)不会静默跳过,会往 stderr 写一条 warning: <名字> (<did>) 云端未返回 token。少一行会被当成「这台设备不存在」。
  • BLE 设备拿到的 bindkey 不是 32 位十六进制时,行尾会跟一句 <- 不是 32 hex, rha 的 MiBeacon v5 解密用不了。那是旧版设备,当场说清好过等你在 rha check 撞墙。
Terminal window
rha --config /etc/rha spec-gen # 联网生成 spec.d/10-generated.toml
rha --config /etc/rha spec-gen --check # 只比对不写盘, 有差异非零退出

它只解析 devices.toml 里真出现过的型号,--check 适合挂定时任务发现「上游把 spec 改了」。细节(文件归属、加载顺序、能力名格式)见 spec.d。

config-schema —— 输出配置项的 JSON Schema

Section titled “config-schema —— 输出配置项的 JSON Schema”

本站所有字段表的数据来源。不需要配置目录也能跑(schema 描述的是配置的形状,不是某 一份具体配置)。

Terminal window
rha --config /etc/rha mcp

挂到 Claude Code(~/.claude.json 的 mcpServers):

{ "rha": { "command": "rha", "args": ["--config", "/etc/rha", "mcp"] } }

daemon 自己也内置一个 POST /mcp(与 API 同一个 bearer token)。rha mcp 是给只会说 stdio 的客户端准备的另一个入口,不是另一套实现。

工具共 10 个:list_entities / list_devices / get_state / history / control / list_rules / upsert_rule / delete_rule / get_trace / upload_dashboard。

rha mcp 的 stdout 是 JSON-RPC 帧通道,所以全部日志固定写 stderr——这是全局设置, 不是只对 mcp 生效。

agent 画好一个静态目录,用这组命令托管到 daemon 上:

Terminal window
rha dashboard push bedroom ./dist # 目录里必须有 index.html
rha dashboard list
rha dashboard rollback bedroom
rha dashboard rm bedroom

面板名只能是 [a-z0-9_-],最长 64 字符(大写字母会被拒:bad dashboard name "Demo")。

push 会在本机先做四件事再上传,为的是让错误当场出现而不是等服务端:

  • 目录里没有 index.html 就直接拒绝——面板入口必须叫这个名字。
  • 跳过 node_modules/ 和 .git/。一次误传就能撞上大小上限。
  • 拒绝符号链接。 一个指向 /etc/rha/rha.toml 的链接会把 API token 一起打包发走。
  • 打包后超过 8 MiB 本地就拦下。服务端超限回的是 413,响应体是纯文本而不是 {ok,data,error} 信封,本机先拦能给出看得懂的错误(最常见的原因是把整个项目目录而不是构建产物目录传了 进去)。

list 在没有面板时不打印一个 [],而是给一句能照着做的话:

$ rha dashboard list
还没有面板。用 `rha dashboard push <名字> <目录>` 传一个

只有 0 和 1 两个值。 没有 2、3 这类细分码,所以脚本里区分失败原因要看 stderr,不能 只看退出码。

命令 0 表示 1 表示
check 配置全过 加载失败或校验失败;错误逐条在 stderr,末行是 N error(s)
doctor 没有 FAIL 项(WARN 不算) 至少一项 FAIL
serve 收到 SIGTERM / SIGINT 后优雅退出 启动期校验失败、端口占用、绑定失败
reload 校验通过并已切换 校验失败(此时旧配置继续跑)或连不上 daemon
status / get 请求成功——哪怕一行都没打印 连不上、认证失败、配置读不出来
set 命令结果是 ok failed / timeout / 实体不存在 / 类型不匹配 / 不可写
history / trace 请求成功 实体或规则不存在、storage 关着、连不上
spec-gen 写盘成功;--check 时表示与官方 spec 一致 拉取失败、型号不在官方目录、--check 发现差异
miot-token 设备表打印完毕 登录被拒、二次验证失败、拉设备表失败
dashboard * 操作成功 本地校验失败或服务端拒绝
mcp 客户端断开,会话正常结束 URL 不合法、传输错误
config-schema 总是 0 ——

要进 CI 和监控的是 check 与 doctor 这两条,它们的语义刻意分开了:

  • check 管「配置对不对」,离线、可复现、与环境无关。放 pre-commit 和 CI。
  • doctor 管「这台机器现在能不能跑」,会碰网络。放开机自检和监控告警,而且因为 「设备关机」只是 WARN,它不会因为你出门时拔了一个插座就把你叫醒。