☰
MQTT Discovery 实战:设备零配置接入 Home Assistant
2026/10/1 20:54:23 网站建设 项目流程

1. 为什么我们要折腾 MQTT Discovery

前几年我给朋友家里做了一套环境监测,ESP8266 加上 DHT22,几块钱的成本,能看温湿度。设备端跑得挺稳,问题出在接入环节:每加一个传感器,我都要在 Home Assistant 的配置文件里手写一段 YAML,写上state_topic、unit_of_measurement、device_class,改完还得重启服务。加到第五个节点的时候,我开始怀疑人生——明明硬件已经足够傻瓜,为什么接入还得靠人肉填表。

后来我把这套逻辑换成了 MQTT Discovery,也就是 MQTT 设备自发现。设备一上电,自己在 MQTT 上发一条约定格式的配置消息,Home Assistant 监听这个主题,自动就把实体建好了,页面上直接就出现温湿度卡片。整个过程我没有登录服务器,没有改一行配置文件,没有重启。这就是我想在这篇里完整拆开讲的东西:它是什么、凭什么能做到、怎么落地、坑在哪。

一句话概括,MQTT Discovery 是 Home Assistant 定义的一套约定:设备把“我是谁、我能干什么、数据从哪个主题来”写成 JSON,发布到一个固定前缀的主题上,HA 侧自动解析并生成对应实体。它解决的核心问题是设备接入的解耦——设备端不需要知道 HA 的存在和地址,HA 侧也不需要为每个新设备写配置。适合谁看?手上有一堆自制或第三方 MQTT 设备、想让它们零配置进 HA 的人;做智能硬件想兼容主流家居平台的人;以及被 YAML 手工配置折磨过的运维向玩家。接下来我会从原理到落地,一层层把这件事讲透,包括主题怎么拼、负载字段怎么填、多实体怎么聚合,以及我自己踩过的那几个至今记忆犹新的坑。

2. 自发现机制的底层原理拆解

2.1 发现主题的命名规则与层级

要理解自发现,先得理解“发现”这两个字在 MQTT 里的物理含义。它没有魔法,本质就是一个订阅关系:Home Assistant 在启动后,会向 MQTT Broker 订阅一个通配主题,比如默认前缀下的<discovery_prefix>/+/+/config,或者开启设备名后变成<discovery_prefix>/+/+/+/config。设备只要往符合这个通配规则的任意主题上发消息,HA 就会收到。

主题的完整格式官方定义是这样的:

<discovery_prefix>/<component>/[<node_id>/]<object_id>/config

拆开来看,discovery_prefix默认是homeassistant,可以在 HA 集成里改,我一般不动它。component是要生成的实体类型,比如sensor、binary_sensor、switch、light、cover、climate等等,这个字段直接决定 HA 拿这条配置去实例化哪一类实体。node_id是可选的,用来做分组,我通常填设备型号或者设备 ID。object_id是这条实体的唯一标识,同一类 component 下不能重复。

举个具体的例子,我那个温湿度节点发的主题长这样:

homeassistant/sensor/dht22_livingroom/temperature/config homeassistant/sensor/dht22_livingroom/humidity/config

HA 收到第一条,就知道“客厅这个 DHT22 节点要注册一个 sensor 实体,标识是 temperature”。这个命名规则不是随便定的,它特意把 component 和 object_id 放在主题里而不是全塞进 payload,是为了让 HA 能用通配符订阅后就近筛掉无关消息——Broker 层面的主题过滤比在应用层解析 JSON 效率高得多,节点多的时候这个设计差别很明显。

注意:object_id里不要用斜杠,斜杠会被当成层级分隔符,导致 HA 解析出的主题结构和你预期不一致,实体要么建不出来,要么建到一个很奇怪的名字下。

2.2 配置消息必须保留,这是自发现的关键

这一条是新手最容易忽略、也最容易导致“明明发了消息实体却不见了”的根因:配置消息必须以 retained 标志发布。

原因很好理解。HA 可能比你的设备晚启动,比如路由器重启后 HA 服务拉起来要二三十秒,而你的传感器五秒就上线了。如果配置消息不是 retained 的,Broker 转手就丢了,HA 启动后订阅那个主题,什么都收不到,实体自然不存在。把配置消息设成 retained,Broker 会把它留住,HA 无论什么时候订阅,都会立刻收到最后一条保留的配置,马上建出实体。

这一点和状态消息不一样。状态消息(比如温度读数)可以不是 retained 的,因为它是持续刷新的,丢一条无所谓;但配置消息是一次性声明,必须留底。我在设备端固件里做重连逻辑时,专门把配置发布单独拎出来,每次 MQTT 连接成功后都重发一遍 retained 的配置,这样即便 Broker 清了保留消息,设备重连时也能补回去。

这里有个很实用的经验:设备启动流程设计成“连上 Broker → 发一遍保留配置 → 再开始周期上报状态”。顺序不能反,否则 HA 可能先收到状态、后收到配置,实体建出来时状态是空的,要等下一个上报周期才填充,观感上就像卡了一下。

2.3 负载字段:哪些必填,哪些能省

光有主题还不够,主题只告诉 HA“我要注册哪类实体、叫什么名字”,真正的能力描述在 payload 里。payload 是一个 JSON,字段可以分为三类。

第一类是定位数据源的字段,最核心的是state_topic,告诉 HA 这个实体的状态从哪里读。开关类实体还需要command_topic,告诉 HA 用户点了开关后指令发到哪里。这两个字段没有的话,实体建出来也是个摆设,点不动、读不到。

第二类是描述性字段,包括name(显示名)、unit_of_measurement(单位)、device_class(设备类别)、value_template(取值模板)、icon(图标)等。这些不影响功能,但决定用户体验。

第三类是聚合字段,也就是device块,它的作用是把多个实体绑定成同一个物理设备,后面我会单独讲。

不同 component 的必填字段差异很大。sensor最宽松,只要有state_topic基本就能用;switch必须有command_topic;light如果要做调光,就得加brightness_state_topic和brightness_command_topic。下面这张表是我整理的高频组件必填项速查:

组件类型必填字段常用选填字段
sensorstate_topicunit_of_measurement, device_class
binary_sensorstate_topicpayload_on, payload_off, device_class
switchcommand_topicstate_topic, payload_on, payload_off
lightcommand_topicbrightness_state_topic, brightness_command_topic
covercommand_topicposition_topic, set_position_topic

我个人的习惯是,device_class只要能用就一定填。因为它不只是显示个图标那么简单,它会改变 HA 对这个值的解释方式。比如温度填了device_class: temperature,HA 就允许你在前端把单位从摄氏度切到华氏度,历史记录里也会正确识别这类数据。不填的话,HA 只当它是一串数字,很多联动能力用不上。

3. 从零落地一个温湿度节点接入

3.1 Broker 与 HA 侧的准备工作

动手之前,先把地基检查一遍。你需要一个跑起来的 MQTT Broker,Mosquitto 是最常见的选择,本地装在树莓派或者 NAS 上都行。Broker 侧要确认两件事:监听端口正常(默认 1883),以及如果你开了认证,HA 和设备用的账号密码都配好。

HA 侧要做的是添加 MQTT 集成。进入集成页面加上 MQTT,填 Broker 地址、端口、用户名密码,注意把发现(Discovery)开关打开,这是关键,默认前缀留homeassistant。保存后 HA 就会开始订阅发现主题。

验证订阅是否生效,我最喜欢的手段是在 Broker 那台机器上用命令行订阅一把:

mosquitto_sub -h 127.0.0.1 -p 1883 -u ha -P yourpass \ -t 'homeassistant/#' -v

这条命令会打印所有发现前缀下的消息,包括配置。你发一条测试消息,这里能立刻看到,就说明 HA 的订阅链路通了。如果这里收不到,那问题在 Broker 或订阅侧,不用往下查设备了。

注意:如果你用 Docker 跑 Mosquitto,留意mosquitto.conf里有没有挂persistence。开启持久化后保留消息会落盘,重启 Broker 不会丢,这对自发现的稳定性帮助很大。但反过来,调试时残留的测试保留消息也会一直跟着你,后面排查幽灵实体会用到这个知识点。

3.2 设备端发布配置主题的完整报文

环境通了,重头戏来了。我以那个 DHT22 节点为例,把它发的配置消息完整写出来。温湿度用同一个主题结构,分两条配置:

{ "name": "客厅温度", "state_topic": "dht22/livingroom/state", "unit_of_measurement": "°C", "device_class": "temperature", "value_template": "{{ value_json.temperature }}", "availability_topic": "dht22/livingroom/status", "payload_available": "online", "payload_not_available": "offline", "unique_id": "dht22_livingroom_temp", "device": { "identifiers": ["dht22_livingroom"], "name": "客厅环境节点", "model": "DHT22 Node", "manufacturer": "DIY" } }

湿度那条除了name、device_class、value_template换成对应的,其他都一致。发到主题:

homeassistant/sensor/dht22_livingroom/temperature/config

这里有几个我强烈建议采用的做法。第一,用value_template从一条 JSON 状态里取值。设备端上报状态时发一个完整对象到state_topic,比如{"temperature": 24.6, "humidity": 58.2},HA 侧用模板各取所需。这样设备只需要发一条状态消息,两个实体都能读到,MQTT 消息量直接砍半。第二,一定要填unique_id,它让实体在 HA 里有一个稳定身份,重启、改名都不会导致历史数据断档。第三,availability_topic加进去,这样设备掉线时 HA 能把实体标成不可用,而不是傻傻显示最后一次读数。

配置发布这一动作在代码里通常就是一行client.publish(topic, payload, retain=True)。注意最后那个retain=True,前面讲了,这是命门。我踩过的坑就是早期用 Node-RED 的 MQTT out 节点发配置,忘了勾 retained,结果每次重启 HA 实体就集体消失,折腾了我两个晚上才定位到。

3.3 状态上报与指令回写的实操细节

配置发完,HA 页面几秒内就会出现实体卡片,但初始状态是空的。这时候设备要开始往state_topic发状态。我的上报节奏是这样设计的:上电后立即发一次,之后每 30 秒一次,变化超过 0.5 度或 3% 湿度时额外补发一次。这种“定时 + 变化触发”的组合,比纯定时省流量,又比纯变化触发可靠,页面上的数值不会长时间不动。

状态消息就是一个 JSON 字符串:

{"temperature": 24.6, "humidity": 58.2}

发布到dht22/livingroom/state,可以不 retained,也可以 retained。我倾向状态也 retained,这样 HA 重启后能立刻恢复最后读数,不用等下一个上报周期。代价是历史记录里可能会出现一条重复值,但影响不大。

指令回写是方向反过来。像开关、灯这类可控实体,HA 会把用户操作发到command_topic。设备端订阅这个主题,收到指令后执行动作,然后主动把新状态发回state_topic,而不是假设动作成功了就完事。这个“状态回读”的做法很重要,它保证了 HA 显示的状态永远反映设备的真实情况,而不是一个乐观的猜测。我曾见过一台自制的继电器,HA 点了关,页面立刻显示关,但其实继电器卡住了没动作,页面和现实脱节了半小时都没人发现。加上状态回读,这种问题一目了然。

3.4 可用性监控让实体不再骗人

availability_topic值得单独说,因为它直接决定你的自动化会不会发疯。设想一个场景:你写了个“温度超过 30 度开空调”的自动化,某天传感器没电了,最后一条保留的读数是 31 度,HA 一直拿这个陈旧值判断,空调要么一直开,要么反复触发。

正确的做法是设备端用 MQTT 的遗嘱消息(Will)机制。连接 Broker 时声明:如果我异常断开,请代我发一条offline到dht22/livingroom/status。设备正常运行时也可以主动保持online。HA 侧通过配置里的availability_topic和payload_available/payload_not_available来解读这些消息,设备一离线,相关实体立刻变灰不可用,依赖它的自动化就不会拿假数据做判断。

client.will_set("dht22/livingroom/status", "offline", retain=True) client.connect(broker, 1883, 60) client.publish("dht22/livingroom/status", "online", retain=True)

这段顺序也有讲究:遗嘱先声明、连接、再上线,缺一不可。如果先发 online 再声明 will,中间那一小段窗口里断线,Broker 不知道该怎么处理,实体状态就可能卡在 online。

4. 一个设备多个实体的聚合玩法

4.1 用 device 块把散装实体绑成一个设备

前面配置里那个device块,单独拎出来讲是因为它在多实体的设备上价值极大。没有它的时候,温度、湿度、信号强度这三个实体在 HA 里是三个孤立的卡片,各挂各家。有了device块,只要多条配置里的identifiers相同,HA 就把它们归到同一个物理设备下,界面上显示为一个设备,点进去能看到全部实体,还能一键查看这台设备的全部历史。

"device": { "identifiers": ["dht22_livingroom"], "name": "客厅环境节点", "model": "DHT22 Node", "sw_version": "1.2.0", "via_device": "gateway_01" }

identifiers是设备的稳定唯一标识,必须有;via_device用来表达拓扑关系,比如这个节点是通过某个网关接入的,填了之后 HA 的设备页面会画出层级。sw_version、model这些纯信息字段,方便你日后排查“到底是哪一批硬件出的问题”。我维护过十几个节点,没有sw_version的时候,一旦蓝牙或固件出问题,根本分不清手上这台跑的是哪个版本,补上之后省事太多。

4.2 复杂设备的主题结构设计建议

当设备功能变多,主题结构如果没有提前规划,后面会乱成一锅粥。我总结了一套自己一直在用的命名约定:

<设备ID>/<实体名>/state 状态读取 <设备ID>/<实体名>/set 指令写入 <设备ID>/status 在线状态

对应的发现主题统一为homeassistant/<component>/<设备ID>_<实体名>/config。这样一眼就能看出主题归属。之前有个朋友把他的主题命名成temp1、temp2、data,过了半年自己都忘了哪个是哪个,最后全拆了重做。

还有一个容易忽略的点:MQTT 主题是区分大小写的。LivingRoom和livingroom是两个不同的主题,设备端写一个、配置里写另一个,实体就会永远读不到数据。统一用小写加下划线,是最省心的选择。

4.3 动态修改与干净删除实体

自发现的一个好处是设备能力可以动态变化。比如你的节点固件升级后多出了 PM2.5 检测,只需要在启动时多发一条对应的配置即可,老实体不受影响。反过来,要删除一个实体,标准做法是往它的配置主题发一条空的保留消息,HA 收到后会移除该实体。命令长这样:

mosquitto_pub -h 127.0.0.1 -u ha -P yourpass \ -t 'homeassistant/sensor/dht22_livingroom/old_metric/config' \ -r -n

-r是保留,-n是发送空消息。这两者结合,等于用一条空保留消息覆盖掉原来的配置,同时告诉 Broker“这个保留消息可以清了”。删除实体后,别忘了设备端固件里的发布逻辑也要同步去掉,否则下次设备重连又把实体建回来,就会出现“删了又回来”的灵异现象。我遇到过有人反复删同一个实体,查了半天,最后发现是设备端每次重连都在补发配置。

5. 常见问题与排查实录

5.1 实体不出现的排查顺序

这是最高频的问题,我自己也数不清帮人看了多少次。别乱猜,按这个顺序查,基本十有八九能定位:

  1. 先看 Broker 有没有收到配置消息。用前面那条mosquitto_sub命令盯着发现前缀,让设备重启一次,看有没有消息出来。没有,问题在设备端发布环节。
  2. 再看消息是不是 retained。用mosquitto_sub加-v参数订阅,收到的消息前面会带保留标记;或者干脆断开设备,再订阅一次,如果还能收到配置消息,说明是保留的,否则不是。这一步是排查重灾区。
  3. 看主题格式对不对。数一下层级,homeassistant/sensor/node_id/object_id/config是五层,少一层、多一层,HA 的通配订阅都匹配不上。
  4. 看 payload 是不是合法 JSON。一个中文逗号、一个多余的尾逗号,都会让 HA 解析失败,而且很多时候日志报得很隐晦。把 payload 复制到任意 JSON 校验工具里过一遍。
  5. 看 HA 日志。设置里搜 MQTT 相关日志,解析失败的报文一般会有提示,比如缺字段、类型不对。
  6. 看组件类型是否支持。极少数情况下你用的 component 名拼错了,HA 不认识,静默忽略。

5.2 幽灵实体与保留消息残留

幽灵实体指的是那些你早就不用、固件也删了,但 HA 里还在、点开还没数据的实体。根因几乎都是保留消息没清。Broker 里还留着一条老配置,HA 每次启动都读一遍,实体自然常在。

清理办法就是前面说的,往那个主题发一条空保留消息。但麻烦在于,你可能已经忘了旧主题叫什么。这时候用通配订阅把所有发现配置拉一遍,直接看列表:

mosquitto_sub -h 127.0.0.1 -u ha -P yourpass \ -t 'homeassistant/#' -v --retained-only

--retained-only这个参数特别好用,它只显示保留消息,跳过实时流转的。拉出来的清单就是当前 Broker 里所有活着的自发现配置,挨个核对,不需要的发空消息清掉。我建议每隔几个月做一次这种清理,尤其是折腾期,能省掉很多“为什么这个实体删不掉”的困惑。

5.3 高频问题速查表

现象可能原因处理动作
实体完全不出现配置消息未保留发布时加 retain 标志
实体出现但无数据state_topic 拼写或大小写不符核对设备发布主题与配置一致
重启 HA 后实体消失配置消息非保留或 Broker 未持久化开保留 + 开 Broker 持久化
数值一直是旧值没有 availability 配置加遗嘱消息和可用性主题
删了实体又回来设备端重连补发配置固件同步移除发布逻辑
页面图标不对未填 device_class按测量类型补上对应类别

6. 我在长期维护中攒下的几点体会

折腾自发现这几年,最大的感受是:把配置当作一种状态来管理,比把它当成一次性动作要靠谱得多。我现在的固件里,配置发布和状态上报走的是同一条重连恢复路径,任何一次断线重连,配置都重发一遍。看起来很笨,多发了消息,但它带来的是极强的自愈能力——Broker 清了保留、HA 重装、网络抖动,都不需要你手动干预,设备自己就能把整个接入关系重新建立起来。

另一个体会是关于命名。前期图省事用sensor1、sensor2,后来节点一多,完全分不清谁是谁。现在我的命名规则是“位置 + 功能 + 类型”,比如livingroom_temperature,配上统一的sw_version和model,日后定位问题能省下大量时间。至于上报频率,别贪快也别贪慢,30 秒到 1 分钟是个甜点区间,再快对家居场景意义不大,反而让 Broker 和数据库压力陡增。

最后分享一个我最近才发现的小技巧:如果你的设备支持,可以在配置里加"enabled_by_default": false,实体建出来默认是禁用状态,等你在 HA 里手动启用才生效。这个特别适合那些调试用的诊断实体,比如信号强度、运行时长,平时不占地方,需要时再开。后续这套机制还能往上报诊断、下发 OTA 指令的目录扩展,本质上,只要能把消息发到 MQTT 上,自发现就能帮你把它变成 HA 里的一个可交互实体。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询