跳转到内容

mqtt(Zigbee2MQTT / Tasmota / ESPHome)

其它 adapter 一个只覆盖一个厂商,这个不一样:只要设备的数据能进 MQTT,rha 就能读它、 控它。Zigbee2MQTT(几百款 Zigbee 设备)、Tasmota、ESPHome、以及任何自己往 broker 发 数据的东西,走的都是这一条。

代价是它不认识你的设备——协议本身只是「主题 + 字节」,哪条主题是温度、payload 怎么解, 必须你在 devices.toml 里说清楚。

[[device]]
name = "thermo_desk"
label = "书桌温湿度计"
adapter = "mqtt"
broker = "192.168.52.10:1883"
password = "${MQTT_PASSWORD}"
state_topic = "zigbee2mqtt/thermo_desk"
availability_topic = "zigbee2mqtt/thermo_desk/availability"
availability_json_field = "state"
manufacturer = "Aqara"
model = "WSDCGQ11LM"
[[device.entity]]
name = "temperature"
json_field = "temperature"
type = "float"
unit = "°C"
class = "temperature"
[[device.entity]]
name = "humidity"
json_field = "humidity"
type = "float"
unit = "%"
class = "humidity"
[[device.entity]]
name = "battery"
json_field = "battery"
type = "int"
unit = "%"
class = "battery"

这一台产出三个实体:thermo_desk.temperature、thermo_desk.humidity、 thermo_desk.battery。

一条主题一个 JSON,拆成几个实体

Section titled “一条主题一个 JSON,拆成几个实体”

Zigbee2MQTT 的典型形态是一台设备一条主题,payload 是一个 JSON,温度湿度电量全在 里面:

zigbee2mqtt/thermo_desk {"temperature":21.5,"humidity":48.0,"battery":100,"linkquality":72}

所以上面那份配置里三个实体共用同一个 state_topic(写在设备级),各自用 json_field 从里面挑一个字段。用不上的字段(linkquality)不声明就是了,不会报错。

不写 json_field 表示整条 payload 就是值——Tasmota 那种 stat/plug/POWER 直接发 ON 的形态就是这样。两种可以在同一台设备上混用,逐实体决定。

rha 不会连上 broker 听 homeassistant/+/+/config 自己长出实体来。

原因是 rha check 的契约:它必须能在完全不碰网络的情况下校验整份配置,包括「规则 引用的实体存不存在」。实体清单如果是运行期从 broker 长出来的,rha check 就只能说 「不知道」——而那正是这个项目最不想要的那种答案。

不等于要你手抄。rha spec-gen 已经是同款先例(联网从米家云拉规格、生成 spec.d/ 让你 review 后入库),MQTT 的发现器按同一条路走,配置 结构就是照「能被自动生成」设计的。发现器本身还没做,现在得手写。

只读是默认。要能写,实体上标 settable = true,并且有命令主题可用(设备级 command_topic,或实体自己覆盖):

[[device]]
name = "plug_desk"
adapter = "mqtt"
broker = "192.168.52.10:1883"
state_topic = "zigbee2mqtt/plug_desk"
command_topic = "zigbee2mqtt/plug_desk/set"
[[device.entity]]
name = "state"
json_field = "state"
type = "bool"
settable = true

rha set plug_desk.state on 会往 zigbee2mqtt/plug_desk/set 发 {"state":"ON"}—— 带 json_field 就包成 JSON,不带就发裸的 ON。

ON / OFF 这两个串可以用实体上的 on / off 改(有些固件用 1 / 0、 true / false)。

可用性:设备自己说的,不是猜的

Section titled “可用性:设备自己说的,不是猜的”

availability_topic 是设备(或网关)汇报在线状态的主题。收到 payload_not_available (默认 offline)时,这台设备名下的实体全部标不可用——同一个 broker 上的其它设备 不受影响。

新版 Zigbee2MQTT 发的是 {"state":"online"},所以要配 availability_json_field = "state"; 旧版发裸的 online,去掉那行即可。

不配 availability_topic 也能跑,只是失去了「设备离线」这个信号——broker 断了仍然会让 实体不可用,那一层是连接状态给的。

broker 写在每台设备上,但相同的 broker + 账号 + client id 只会开一条连接。三十台 Zigbee 设备共用一个网关,broker 上看到的是一个 client,不是三十个。

broker 要写 host 或 host:port,不要带 mqtt:// 前缀(当前只支持明文 TCP, 带 scheme 会在 rha check 直接报错)。主题里不能有通配符——rha 只按精确主题收发, 配了 + 或 # 会当场报错而不是留一个永远收不到消息的实体。

MQTT 里没有「问一遍」这个动作,值是设备自己发布上来的。所以它名下的实体 poll_tier 恒为空,rha status 不会把它们标成「每 300 秒轮询 一次」——那对一台从不被轮询的设备是错的说法。

代价是冷启动时可能一段时间没值(要等设备下一次发布)。缓解手段是让设备/网关把状态发成 retained,那样 rha 一订阅就立刻拿到当前值。Zigbee2MQTT 的 retain: true 就是干这个的。

设备级:

字段类型默认说明
availability_json_fieldstring可省略

可用性 payload 是 JSON 时取哪个字段。

Zigbee2MQTT 1.30 起默认发 {"state":"online"}(旧版发裸的 online), 所以新版要配 availability_json_field = "state"。

availability_topicstring可省略

设备在线状态的主题(Zigbee2MQTT 是 zigbee2mqtt/<设备名>/availability)。

不配也能跑: 那样实体的可用性只跟着 broker 连接走 —— broker 断了标不可用, broker 在就一直可用, 哪怕那台 Zigbee 设备已经没电了。

broker*string—

broker 地址, host 或 host:port(端口默认 1883)。

不要带 mqtt:// / tcp:// 前缀 —— 带了是 rha check 期硬错误, 不会被 当成主机名拿去解析。当前只支持明文 TCP, 没有 TLS(理由见 rha-mqtt 的说明: TLS 会把 musl 全静态变体搞坏), 所以也没有 mqtts:// 可写。

同一个 broker + username + client_id 的设备共用一条连接, 写多少遍 都只有一条。

client_idstring可省略

MQTT client id。不写就用 rha 启动时生成的 rha-<8位随机>。

只有想让某几台设备走一条独立连接时才需要写(写了就是另一条连接)。 注意两个 client 用同一个 id 连同一个 broker 会互相踢下线, 所以别在两台 机器上写同一个固定值。

command_topicstring可省略

这台设备的默认命令主题, 各实体可以自己覆盖。只有 settable = true 的 实体用得到。

entityarray可省略

这台设备暴露哪些实体。至少要有一个。

keep_alive_secsinteger可省略

心跳间隔秒数, 默认 60。

manufacturerstring可省略

厂商, 只用于北向协议的「配件信息」显示。MQTT 协议本身不带这个信息, 不写就留空(HomeKit 那边会显示成 rha)。

modelstring可省略

型号, 用途同 manufacturer。

passwordstring
Secret
可省略

broker 密码。建议写成 ${MQTT_PASSWORD} 由环境变量插值。

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

payload_availablestring可省略

可用性主题里表示"在线"的值, 默认 online。

payload_not_availablestring可省略

可用性主题里表示"离线"的值, 默认 offline。

state_topicstring可省略

这台设备的默认状态主题, 各实体可以自己覆盖。

Zigbee2MQTT 的形态是一条主题一个 JSON(zigbee2mqtt/<设备名>), 温度/湿度/ 电量全在里面, 所以写在设备级、各实体只写 json_field 最省。

不能含 + / #: rha 只按精确主题分发, 通配主题订了也永远收不到消息, 所以这是加载期硬错误而不是"配了不生效"。

usernamestring可省略

broker 账号。broker 开了匿名就不用写。

每个 [[device.entity]]:

字段类型默认说明
classstring
DeviceClass
可省略

语义类, 决定这个实体能不能导出到 HomeKit 这类北向协议。

adapter 给的是默认值, devices.toml 里的 [device.class] 可以逐个覆盖。

一个实体承载的语义。

刻意窄 —— 封闭枚举加一项就要动 core 和所有 bridge, 宁可少。刻意不照抄 HomeKit 的 Service 列表: DeviceClass::Power 与 DeviceClass::Weight 在 HAP 里没有 标准对应, 照抄的话这两个语义根本进不来, 接 MQTT/HA 时又得重新发明。内核词汇表与 任何单一协议的对象模型必须分开。

command_topicstring可省略

覆盖设备级的 command_topic。

json_fieldstring可省略

从 payload 的 JSON 里取哪个字段; 不写表示整条 payload 就是值。

支持 . 分隔的多段路径, 因为 Tasmota 的传感器 payload 是嵌套的 ({"SI7021":{"Temperature":24.6}} → json_field = "SI7021.Temperature")。

字段不在这条消息里不算错(Zigbee2MQTT 的增量上报只带变了的字段); 但 payload 压根不是 JSON 对象会报错 —— 那是 json_field 配多了。

maxinteger可省略

数值实体的取值上界, 见 min。

mininteger可省略

数值实体的取值下界, 与 max 成对出现。

北向 bridge 靠它把设备刻度换算成协议刻度 —— 一根 HomeKit 转速滑条固定是 0-100 的百分比, 不知道设备侧是 1-4 档还是 1-100 无级就没法换算。 想把某个实体标成 class = "speed" 就得给这一对。

name*string—

能力名, 也就是实体 id 点号后面那半(<设备名>.<能力名>)。snake_case。

offstring可省略

bool 类型的实体里, 表示"假"的 payload。默认 OFF。

onstring可省略

bool 类型的实体里, 表示"真"的 payload。默认 ON。

精确匹配, 不折叠大小写。门磁这类设备发的是 open/closed, 配上即可。

settablebooleanfalse

这个实体能不能写(rha set / 规则的 set 动作 / 北向协议的控制)。

默认 false。设成 true 就必须能定出命令主题(实体级或设备级的 command_topic)。

state_topicstring可省略

覆盖设备级的 state_topic。设备级也没写就是加载期错误。

type*string
PayloadType
—

这个值读成什么: bool / int / float / string。

payload 里那个值该被读成什么。

unitstring可省略

单位, 只是给人看的标签(北向 bridge 看的是 class, 不是它)。

type 的取值:

取值含义
bool

布尔。payload 里几乎不会是 JSON 的 true/false, 而是 "ON"/"OFF" 这类词 —— 所以有 on / off 两个字段可配, 见 Codec::on。

int

整数。21 与 21.0 都收(不少设备就这么发), 但有小数部分的会当场判失败 而不是静默截断 —— 否则"温度 21.5 显示成 21"这种事一点痕迹都不会留。 要小数就写 float。

float

小数。整数形态的 payload(21)照收。

与 int 一样, 引号包起来的数字串("21.5")也认 —— 有些固件发的 JSON 里数值 是带引号的。

string

配置里写 "string" 而不是 "str" —— 与 int / float 那种通用缩写不同, str 是 Rust 的词, 配置文件里没有理由让用户跟着 Rust 走。