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/configHA 收到第一条就知道“客厅这个 DHT22 节点要注册一个 sensor 实体标识是 temperature”。这个命名规则不是随便定的它特意把 component 和 object_id 放在主题里而不是全塞进 payload是为了让 HA 能用通配符订阅后就近筛掉无关消息——Broker 层面的主题过滤比在应用层解析 JSON 效率高得多节点多的时候这个设计差别很明显。注意object_id里不要用斜杠斜杠会被当成层级分隔符导致 HA 解析出的主题结构和你预期不一致实体要么建不出来要么建到一个很奇怪的名字下。2.2 配置消息必须保留这是自发现的关键这一条是新手最容易忽略、也最容易导致“明明发了消息实体却不见了”的根因配置消息必须以 retained 标志发布。原因很好理解。HA 可能比你的设备晚启动比如路由器重启后 HA 服务拉起来要二三十秒而你的传感器五秒就上线了。如果配置消息不是 retained 的Broker 转手就丢了HA 启动后订阅那个主题什么都收不到实体自然不存在。把配置消息设成 retainedBroker 会把它留住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_topiclight如果要做调光就得加brightness_state_topic和brightness_command_topic。下面这张表是我整理的高频组件必填项速查组件类型必填字段常用选填字段sensorstate_topicunit_of_measurement, device_classbinary_sensorstate_topicpayload_on, payload_off, device_classswitchcommand_topicstate_topic, payload_on, payload_offlightcommand_topicbrightness_state_topic, brightness_command_topiccovercommand_topicposition_topic, set_position_topic我个人的习惯是device_class只要能用就一定填。因为它不只是显示个图标那么简单它会改变 HA 对这个值的解释方式。比如温度填了device_class: temperatureHA 就允许你在前端把单位从摄氏度切到华氏度历史记录里也会正确识别这类数据。不填的话HA 只当它是一串数字很多联动能力用不上。3. 从零落地一个温湿度节点接入3.1 Broker 与 HA 侧的准备工作动手之前先把地基检查一遍。你需要一个跑起来的 MQTT BrokerMosquitto 是最常见的选择本地装在树莓派或者 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, retainTrue)。注意最后那个retainTrue前面讲了这是命门。我踩过的坑就是早期用 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, retainTrue) client.connect(broker, 1883, 60) client.publish(dht22/livingroom/status, online, retainTrue)这段顺序也有讲究遗嘱先声明、连接、再上线缺一不可。如果先发 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 实体不出现的排查顺序这是最高频的问题我自己也数不清帮人看了多少次。别乱猜按这个顺序查基本十有八九能定位先看 Broker 有没有收到配置消息。用前面那条mosquitto_sub命令盯着发现前缀让设备重启一次看有没有消息出来。没有问题在设备端发布环节。再看消息是不是 retained。用mosquitto_sub加-v参数订阅收到的消息前面会带保留标记或者干脆断开设备再订阅一次如果还能收到配置消息说明是保留的否则不是。这一步是排查重灾区。看主题格式对不对。数一下层级homeassistant/sensor/node_id/object_id/config是五层少一层、多一层HA 的通配订阅都匹配不上。看 payload 是不是合法 JSON。一个中文逗号、一个多余的尾逗号都会让 HA 解析失败而且很多时候日志报得很隐晦。把 payload 复制到任意 JSON 校验工具里过一遍。看 HA 日志。设置里搜 MQTT 相关日志解析失败的报文一般会有提示比如缺字段、类型不对。看组件类型是否支持。极少数情况下你用的 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 里的一个可交互实体。