automations/*.toml
配置目录下 automations/ 里的所有 .toml 按文件名字典序加载,[[rule]] 与
[actions.<名字>] 汇总成两张全局表。拆成几个文件纯粹是给人看的,引擎不区分——所以
规则名和命名动作名都是跨文件唯一。
[[rule]]name = "fan_on_hot"trigger = { entity = "thermo.temperature", above = 28.0, for = "5m" }condition = { time = "07:00..23:00" }action = { entity = "fan.power", set = true }mode = "restart"这是配置目录里唯一参与热重载的一份:改完跑 rha reload(或发 SIGHUP)即可生效,
不用重启。设备、bridge、[api] 的变更都要重启。
一次触发要过四道闸
Section titled “一次触发要过四道闸”trigger 命中 → throttle 窗口 → condition 求值 → mode 并发判定 → 执行动作序列顺序是固定的,每一道都会在 trace 里留下痕迹(throttled / blocked / skipped /
restarted / done)。「规则为什么没跑」先看 trace,别猜。
两个反直觉的细节:节流闸排在条件之前,但计时起点只在真正执行时才刷新——被条件挡下
的那一次不占用窗口;cron 触发豁免 throttle,但条件与并发模式照过。
触发器 trigger
Section titled “触发器 trigger”above / below / fired / changed / cron 必须恰好写一个。引擎里没有「区间
触发」,既要 above 又要 below 得拆成两条规则。
本页有两个 above,语义不一样,这是最容易混的一点:触发器的是边沿(跨越那一刻
触发一次),下一节条件的是电平(触发时刻当前值在不在阈值外)。所以
trigger.above = 28 配 condition.above = 28 并不互相抵消,两张表要分开看。
fired 要配的瞬时信号实体由 adapter 产出,当前只有
reolink 有。
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
above | number | 可省略 | 数值实体向上穿越阈值时触发, 判定是严格大于( 这是边沿语义, 不是「每轮采样只要超标就触发」 —— 只在「上一次的值不越界、这一次 越界」的那一刻触发一次; 之后值继续待在阈值之上怎么变都不会再触发, 要等它跌回阈值 以下再重新升上来才算下一次。阈值 28 时, 29°C 升到 30°C 不触发。 几个边角:
|
below | number | 可省略 | 数值实体向下穿越阈值时触发, 判定是严格小于( 不能和 |
changed | boolean | 可省略 | 实体状态每次真的发生变化就触发, 不看变成了什么值。适合「只要开关动了就记一笔 / 通知一次」这类规则。 与 「没变的上报」不算变化 —— 设备每 30s 报同一个温度只会触发 0 次。反过来, 数值实体上
配 不能配 |
cron | string | 可省略 | 按时间表定时触发, 与任何实体无关。表达式用 croner 方言: 标准的 5 段 (分 时 日 月 周), 也可以在最前面多写一段秒变成 6 段。按本机本地时区计算下一次 触发时刻。 秒是可选的第一段, 这是最容易写错的地方: 时区取的是容器 / 主机的本地时区 —— 容器时区没跟宿主对齐会整体偏几个小时。 cron 触发豁免 |
entity | string | 可省略 | 这条规则盯哪个实体, 写实体 id( 实体必须真实存在(由设备段声明): 拼错的 id 在 check 阶段就被拒
( cron 触发必须不写它, 写了报 |
fired | boolean | 可省略 | 监听「瞬时信号」实体的一次动作 —— 门铃按下、按键点击这类。这类实体没有持续状态, 只在被按下的那一刻由 adapter 上报一次, 平时读不到任何值。 它不是在 只看这个键写没写, 不看写的值:
|
for | string | 可省略 | 给阈值触发加「持续多久」的要求: 值穿越阈值后并不立刻执行动作, 而是先等这段时间,
期间没有跌回阈值内侧、且到期时当前值仍在阈值外侧, 才真正触发。写法是
只能配 等待期间只要收到一次「回落」事件, 计时就被取消, 得重新穿越才会重新开始计时, 不累计。到期时会再查一次当前值兜底, 仍越界才执行; 如果计时期间实体不再上报, 到期读到的就是最后已知值。 热加载规则( |
条件 condition
Section titled “条件 condition”一个 condition 块只能是一种条件:实体谓词(entity + above/below/equals)、
time、all、any、not 五组恰好命中一组。所以
condition = { entity = "a.b", above = 1, time = "07:00..23:00" }不是「而且」,是配置错误。要「而且」只有 all 一条路:
[[rule]]name = "fan_on_hot_daytime"trigger = { entity = "thermo.temperature", above = 28.0 }action = { entity = "fan.power", set = true }condition.all = [ { time = "07:00..23:00" }, { entity = "fan.power", equals = false },]区间也一样要拆成 all 下的两个谓词。
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
above | number | 可省略 | 实体当前值严格大于给定数值时条件成立, 要与 与触发器的 正好等于阈值不算成立。实体的值类型必须是 int / float, 对 bool / 字符串实体写
|
all | array | 可省略 | 与组合: 列表里的每个子条件都成立, 整体才成立。子条件是完整的 condition 块, 可以
继续嵌套
不能与 |
any | array | 可省略 | 或组合: 列表里任意一个子条件成立, 整体就成立。子条件同样可以继续嵌套。
|
below | number | 可省略 | 实体当前值严格小于给定数值时条件成立, 要与
|
entity | string | 可省略 | 这条实体谓词看哪个实体, 写完整实体 id( 实体必须已经在设备配置里存在, 否则 三个运行期的坑:
|
equals | any | 可省略 | 实体当前值与给定值完全相等时条件成立。接受 TOML 的布尔、整数、浮点、字符串四种
标量; 数组、内联表、TOML 日期时间会在 check 阶段报
值的类型要与实体声明的类型对得上, 只做 int → float 的单向放宽: float 实体写
浮点实体上是精确相等比较, 传感器读数几乎不可能命中 —— 用 |
not | object | 可省略 | 取反: 内层条件成立时整体不成立, 反之亦然。内层是单个 condition 块, 不是列表。 因为「无值 / 未知实体」一律算 false, 规则的条件闸门。一个 condition 块只能是一种条件。 引擎把字段分成五组 —— ① 实体谓词( 组内还有第二道闸: 实体谓词里 总基调是 fail-closed: 实体不存在、或者从未上报过值, 谓词一律算 false。
唯一会把它翻成 true 的是
|
time | string | 可省略 | 时间窗条件, 格式固定为 按 daemon 所在机器的本地时区判定 —— 容器时区没跟宿主对齐会整体偏几个小时。 写法很挑: 只认 两个易错点: 终点不含, |
动作 action / actions
Section titled “动作 action / actions”一个 action 表只能干一件事:set(即 entity + set)/ delay / notify / run /
script / llm 六选一。entity 与 set 算同一组,所以
{ entity = "...", notify = { ... } } 会被算成两个动作而报错。
单个动作写 action,多个写 actions 数组,二者二选一。
[[rule]]name = "notify_when_very_hot"trigger = { entity = "thermo.temperature", above = 30.0 }actions = [ { entity = "fan.power", set = true }, { delay = "10s" }, { run = "push_hot" },]
[actions.push_hot.notify]url = "https://ntfy.sh/rha-demo"message = "客厅超过 30 度, 已开风扇"title = "rha"| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
delay | string | 可省略 | 让当前动作序列在这里停住指定时长, 再继续执行后面的动作。只阻塞本条规则的这一次 执行, 不影响其他规则。 必须是带单位的字符串: 更容易踩的是并发语义: sleep 期间这条规则算「运行中」, 缺省 |
entity | string | 可省略 | 与 它只为 |
llm | object | 可省略 | 把一次分支决策交给 LLM: 按 只能写在规则的 一次 LLM 分支决策。 「一定会有结果」是硬契约: 没配
用 |
notify | object | 可省略 | 向一个 webhook 发 HTTP POST(ntfy / Bark 这类)。请求 10 秒超时; 发送失败只记 trace 和日志, 动作序列继续往下跑 —— 所以「规则跑了」不等于「通知到了」。 目标 URL 受 两道闸的严格程度不一样, 别拿 check 当保证: 所以内网自建的 ntfy 要真发得出去, 得开 一次 webhook 通知: 以 |
run | string | 可省略 | 执行一个命名动作 内置名字 命名动作体里不能再写 |
script | string | 可省略 | 一段内联写在配置里的 rhai 脚本源码(不是脚本文件路径)。 脚本里只有两个与 rha 相关的函数:
语法错误在 TOML 里写多行脚本建议用 |
set | any | 可省略 | 往 值的类型必须与实体的类型精确匹配, 唯一允许的宽化是整数 → 浮点。所以布尔开关
必须写 只支持 bool / 整数 / 浮点 / 字符串四种标量, 数组和表会被拒。 |
notify:发一个 webhook
Section titled “notify:发一个 webhook”向 ntfy / Bark 这类 webhook 发一个 HTTP POST。能不能发得出去不由这张表决定——目标
URL 要过 rha.toml 的 [notify] 段那道 SSRF 闸,内网自建的接收端
默认就是被拒的。
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
message* | string | — | 通知正文, 整串原样作为 HTTP 请求体 POST 出去。 不支持实体插值 —— 想让正文带上触发实体的读数, 当前版本做不到。 但 插值没有转义写法, 正文里带不出字面的 |
priority | string | 可省略 | 通知优先级, 作为 HTTP 请求头 rha 完全不校验取值域 —— 它不是枚举而是自由字符串, 合法值由你的通知服务定义
(ntfy 是 |
title | string | 可省略 | 通知标题, 作为 HTTP 请求头 它走的是 HTTP 头而不是正文: 含换行或其它控制字符会让整个通知请求构建失败,
这条通知发不出去, 只在 trace 里留一条错误。与 |
url* | string | — | POST 的目标 URL。只允许 http / https, 且要过 把整条规则文件当密码文件对待: ntfy 常把 token 放 query、Bark 直接把 device key
放 path。因此两条路径都只暴露
|
llm:把分支交给模型
Section titled “llm:把分支交给模型”键多,写成分表({ } 不能跨行,见页首那条提示):
[[rule]]name = "ask_before_fan"trigger = { entity = "thermo.temperature", above = 28.0 }
[rule.action.llm]prompt = "现在该开风扇吗? 只在确实需要降温时选 turn_on_fan。"branches = ["turn_on_fan", "ignore"]fallback = "ignore"timeout = "8s"entities = ["thermo.temperature"]
[actions.turn_on_fan]entity = "fan.power"set = true这个动作要配合 rha.toml 的 [llm] 段——那一段不配也不报错,
只是每次都走 fallback。
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
branches | array | 可省略 | 模型可选的分支名清单, 至少 2 个。每个名字必须是同一配置里已声明的命名动作
类型上是可选, 但不写或不足 2 个, check 报
分支名虽然以 enum 写进给模型的工具 schema, 仍会二次校验: 模型返回集合外的名字一律
降级 |
entities | array | 可省略 | 注入上下文的实体; 触发实体的状态总会自动带入(spec §5), 这里再声明的是 额外要注入上下文的实体。 每个 id 必须真实存在, 否则 check 失败。cron 触发的规则没有触发实体, 此时不声明
这里列出的实体 + 触发实体 + 副作用: 列进来的实体会被算进「被规则引用」集合, 因而升级为 hot 轮询(每轮轮询并落
历史库), 不只是「读一下」。注入格式是 Rust 的 Debug 形式, 例如
|
fallback | string | 可省略 | 模型不可用时执行的分支, 必须是 「失败」的覆盖面比直觉宽得多, 以下全部静默走 fallback、规则照常算成功: 没配
也就是说一个从没接通过模型的配置看起来完全正常, 只有 trace 里的
|
prompt* | string | — | 给模型的判断指令。引擎会自动在它后面拼上「当前状态」(实体快照)和「从这些分支里选 一个: a, b」, 所以只写判断要求即可, 不用自己列分支名或实体值。 模型不是自由回话, 而是被强制调用一个 |
snapshot | string | 可省略 | spec §5 快照槽位: 快照 URL(如 reolink 快照端点), 附作 LLM 请求的图片输入。 决策前运行时 GET 拉取, 把字节 base64 后作为图片块随 prompt 一起发给模型。拉取失败
或被
它复用 notify 的同一条 SSRF 闸: 摄像头在内网时必须先开 rha 不替你做任何认证: reolink 的快照端点带的 token 需要登录才能拿到、而且会过期, 直接把它写进配置不能长期工作。 |
timeout | string | 可省略 | 这次 LLM 调用的墙钟上限, humantime 格式( 类型上是可选, 但不写 超时期间整条动作序列是阻塞的(动作顺序执行)。它同时覆盖共享 HTTP client 的 10s 缺省超时 —— 否则配 30s 会在 10s 被悄悄掐断。 但它不等于这条规则的最坏延迟: 配了 |
命名动作 [actions.<名字>]
Section titled “命名动作 [actions.<名字>]”一段可复用的动作序列,靠 run = "<名字>" 或 llm 的 branches 引用。两种写法等价:
[actions.night_mode] # 单个动作entity = "fan.power"set = false
[[actions.multi_step]] # 序列: 重复若干段, 按书写顺序执行delay = "1s"[[actions.multi_step]]entity = "fan.power"set = true体内不能再写 run,也不能写 llm:只允许一层展开,让「llm 选分支 → 分支是命名
动作」这条链在结构上不可能自引用。体内每一段仍然遵守上面那条「六选一」。
名字要 snake_case,跨所有 automations/*.toml 不能重名;"ignore" 是内置的空动作,
是保留名,不能自己定义同名的。
一条 [[rule]] 的必填组合是 name + trigger + (action 或非空 actions,恰好一个)。
condition / mode / throttle 三个可省,省了分别等于「无条件」「ignore」「1s」——注意
这三个默认值里没有一个是「关掉」,throttle 尤其不是 0。
| 字段 | 类型 | 默认 | 说明 |
|---|---|---|---|
action | object | 可省略 | 这条规则要执行的单个动作。与
一个动作。一个 action 表只能干一件事。
|
actions | array | 可省略 | 动作序列, 按数组顺序串行执行; 失败语义是尽力而为, 不是原子: 某一步失败(例如 notify 请求出错)不会中断后面的
动作, 只往 trace 的 detail 追加一条 note, 整条规则最终仍记
|
condition | object | 可省略 | 触发之后、执行动作之前的一道额外闸门: 不成立就不执行, 并在 trace 里记一条
只在触发那一刻求值一次: 动作序列跑起来之后不再复查。序列里带 被条件挡下的这一次不刷新节流计时( 规则的条件闸门。一个 condition 块只能是一种条件。 引擎把字段分成五组 —— ① 实体谓词( 组内还有第二道闸: 实体谓词里 总基调是 fail-closed: 实体不存在、或者从未上报过值, 谓词一律算 false。
唯一会把它翻成 true 的是
|
mode | string | 可省略 | 同一条规则的上一轮动作序列还没跑完时, 新触发怎么办。只有两个合法取值:
只有动作序列会持续一段时间(含
|
name* | string | — | 规则的唯一标识 —— 节流记录、并发去重、trace 都以它为键, HTTP API 的
跨 手写文件时字符集不受限(中文、空格都能过 check), 但通过 HTTP API / MCP 写规则时
name 同时是文件名 |
throttle | string | 可省略 | 这条规则两次实际执行之间的最小间隔, 带单位的时长串( 缺省不是 0, 因为规则之间会互相触发(A 开风扇 → 风扇状态变化 → 又触发 B → ……),
一个默认下限能把这种回路限成每秒一次而不是打满。确实需要更快就显式写
三个容易踩的点:
热重载后仍然存在的规则会保留它的节流计时, |
trigger* | object | — | 什么时候考虑执行这条规则。每条规则有且只有一个 trigger。 触发只是第一道闸: 判定通过之后还要依次过 规则的触发条件, 写在
另外三条互斥:
|