☰
Home Assistant MQTT Discovery 自动发现与实体配置
2026/10/1 1:15:26 网站建设 项目流程

1. 为什么我最后放弃了在HA里手写实体配置

先说个真实经历。早期做智能家居接入,我的习惯是每个设备都在configuration.yaml里老老实实写一遍实体定义。一个带温度、湿度、电量、开关状态的四合一传感器,就要写四段配置;十个设备写下来,配置文件三百多行,改一个设备名得全局搜替换,漏一处就报错重启。最要命的是,设备一多,我根本记不清哪个unique_id对应哪个物理设备,排查起来全靠翻日志。

后来接触了MQTT Discovery(设备自发现),整个思路变了。简单说,它是 Home Assistant 和 MQTT 之间的一套约定:设备或网关不需要你手动在配置里声明实体,只要往约定的 MQTT 主题上发一条符合规范的 JSON 消息,HA 就会自动把实体创建出来,并绑定好状态主题、命令主题、单位、设备分类等所有元信息。设备离线后,你还可以再发一条空消息把这个实体删掉。

这套机制解决的核心问题是规模化接入。一台自制 ESP 传感器、一块 STM32 加 4G 模块的远程采集板、一个用脚本跑起来的虚拟设备,只要它能发 MQTT,就能被 HA 自动识别。对于做批量设备接入、做二次开发、做网关中间件的人来说,这几乎是绕不开的一环。

这篇文章适合三类人看:一是刚上手 Home Assistant、还在被 yaml 配置折磨的人;二是想给自己做的硬件设备加上"能被 HA 自动发现"能力的嵌入式开发者;三是做物联网平台或网关、需要把第三方设备统一映射到 HA 的工程师。我会把 Discovery 的主题结构、发现消息的字段逻辑、设备分组的坑、上线和下线流程、以及实际调试中怎么定位"消息发了但实体不出来"这类问题,一条条讲清楚。

需要提前说明的是,HA 的 Discovery 规范细节较多,不同版本间偶有字段调整,我下面给的都是长期稳定、实际在用的写法,涉及版本差异的地方会单独标注。凡是我补全的、原始规范里没写得太细的部分,都是基于我自己的实践总结,你按自己环境微调即可。

2. Discovery 的通信骨架:主题命名与消息流向

2.1 发现主题的层级到底怎么拼

HA 的 MQTT Discovery 默认使用homeassistant作为发现前缀。一条完整的发现主题长这样:

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

拆开看每一段:

  • discovery_prefix:默认homeassistant,可以在configuration.yaml的mqtt段里改。除非有特殊隔离需求,否则建议保持默认,因为很多现成的固件和中间件都硬编码了这个前缀。
  • component:实体类型,比如sensor、binary_sensor、switch、light、climate、cover、number、select等。它决定了 HA 用哪个平台去解析这条消息。
  • node_id:可选,用来做设备分组。同一个物理设备下的多个实体放同一个node_id,HA 会把它们聚合到一张设备卡片下。
  • object_id:这个实体在 HA 内部的唯一标识,也是实体的entity_id基础部分。
  • config:固定后缀,标记这是一条"配置/发现"消息。

举个例子,一个节点叫living_room_sensor,里面有个温度实体,那么发现主题就是:

homeassistant/sensor/living_room_sensor/temperature/config

对应的实体 ID 大致会生成sensor.living_room_sensor_temperature。这里有个细节很多人踩过:object_id里不要用大写、空格和特殊符号,统一用小写字母加下划线,否则生成的 entity_id 会很难看,甚至出现转义问题。

提示:node_id和object_id的拼接顺序不是随意的。如果你把两者写反,HA 会把它当成不同设备,聚合效果就没了。判断方法很简单——看 HA 里生成的实体前缀是不是符合你的预期。

2.2 发现消息发出去之后,HA 做了什么

理解消息流向,调试时能省一半时间。整个过程其实是这样:

  1. 设备或网关连上 MQTT Broker,往<prefix>/<component>/.../config发布一条 JSON,通常设置retain = true。
  2. HA 的 MQTT 集成在订阅了homeassistant/#,收到这条 retained 消息后,解析 JSON,注册实体。
  3. HA 根据消息里的state_topic去订阅状态;如果配了command_topic,则向该主题发布控制指令。
  4. Broker 重启或 HA 重启后,因为消息是 retained 的,HA 重新订阅时立刻又能拿到,实体自动恢复,不需要设备重新上线。

为什么一定要用 retain?这是新手最容易忽略的点。如果不用 retained 消息,设备发完发现消息就完事了,HA 那一刻如果没在线(比如正在重启),这条消息就丢了,实体永远不会出现。而 retain 让 Broker 替你保存最后一条消息,任何新订阅者(包括重启后的 HA)都能立刻收到。代价是:消息会一直存在 Broker 上,设备彻底不用了记得发空消息清理,否则会留下一堆"僵尸实体"。

清理方法:往同一个 config 主题发一个空 payload(空字符串),并保持 retain,HA 收到后会删除该实体。

# 用 mosquitto_pub 发一条空消息,retain 保持 mosquitto_pub -h 192.168.1.10 -t "homeassistant/sensor/living_room_sensor/temperature/config" -r -n

-n表示发送空 payload,-r表示 retained。这条命令我经常用来手动清理测试产生的垃圾实体。

2.3 发现消息里那几个必填字段

一条能成功注册实体的消息,最少要包含这些字段:

{ "name": "客厅温度", "state_topic": "home/sensor/living_room/state", "unit_of_measurement": "°C", "device_class": "temperature", "value_template": "{{ value_json.temperature }}", "unique_id": "living_room_sensor_temperature", "device": { "identifiers": ["living_room_sensor_01"], "name": "客厅环境传感器", "model": "DIY-ESP32-Sensor", "manufacturer": "selfmade" } }

逐个解释为什么这么写:

  • name:显示名。如果不写device,每个实体就是孤立的一张卡片;写了device并带上identifiers,HA 会把多个实体聚合成一个设备。
  • state_topic:状态来源。HA 只是订阅者,不会主动去问设备。
  • unit_of_measurement和device_class:决定 HA 界面的图标、单位显示,以及能否参与统计图表。device_class填对了,历史数据可以自动换算和长期统计。
  • value_template:从原始 payload 里提取字段。如果设备直接发纯数字,这一项可以省略;如果发的是 JSON,就必须用模板取值。
  • unique_id:实体在 HA 注册表里的唯一键。强烈建议每个实体都写,不写的话,你无法在界面上重命名实体,实体 ID 也不稳定。
  • device:设备分组信息。identifiers是设备唯一标识,必须全局唯一,通常用设备序列号或 MAC。

这里说个我踩过的坑:unique_id一旦用了就不能随便改。改了之后 HA 会认为是一个全新实体,旧的实体变成"不可用",历史数据断掉。所以命名要提前规划好,最好和设备物理编号绑定。

3. 不同实体类型的 Discovery 配置差异

3.1 传感器与二进制传感器

component为sensor时,HA 期望收到可读的数值或文本;为binary_sensor时,只认ON/OFF两种状态。

传感器的典型配置重点是device_class和state_class:

{ "name": "电量", "state_topic": "home/sensor/living_room/state", "value_template": "{{ value_json.battery }}", "unit_of_measurement": "%", "device_class": "battery", "state_class": "measurement", "entity_category": "diagnostic" }

state_class有三个常用值:measurement(瞬时值,如温度、湿度)、total(累计值,如总电量)、total_increasing(只增不减的累计值,如用电量)。填错了,HA 的统计功能会报错或算错。entity_category设为diagnostic后,这个实体不会出现在主控面板,而是折叠到设备详情的诊断区,像电量、信号强度这类就该这么处理。

二进制传感器我常用在门窗磁、人体感应上:

{ "name": "门窗状态", "state_topic": "home/sensor/door/state", "payload_on": "OPEN", "payload_off": "CLOSED", "device_class": "door", "value_template": "{{ value_json.contact }}" }

payload_on/payload_off是重点。很多硬件上报的是1/0或true/false,和 HA 默认期待的ON/OFF不一致,必须在发现消息里显式声明,否则状态显示是反的或者一直是"未知"。

注意:device_class为door、window、motion等安全类时,HA 界面会用不同的图标和颜色标识,选对了体验好很多。别偷懒一律不写。

3.2 开关、灯与可控实体

可控实体和只读传感器最大的区别,是多了一个command_topic。HA 向这个主题发指令,设备订阅后执行。

{ "name": "补光灯", "state_topic": "home/light/grow/state", "command_topic": "home/light/grow/set", "payload_on": "ON", "payload_off": "OFF", "state_on": "ON", "state_off": "OFF", "brightness_state_topic": "home/light/grow/brightness", "brightness_command_topic": "home/light/grow/brightness/set", "brightness_scale": 255, "unique_id": "grow_light_01" }

几个容易混的字段:

字段作用常见错误
payload_on/offHA 发出的指令内容与设备约定的值不匹配
state_on/off判断设备状态用的值和 payload 混用导致状态不刷新
brightness_scale亮度上限,默认 255设备用 0-100 时没改,导致亮度算错

我最早接一个支持 0-100 亮度调节的灯带,忘了设brightness_scale: 100,结果滑条拉到最大,HA 发的是 255,灯带直接按最大值处理,中间全乱。这类字段一定要和设备的实际协议对齐。

3.3 设备分组与命名冲突处理

设备分组靠device.identifiers。同一个identifiers下的所有实体,HA 会合并到一张卡片。这里有两个坑:

一是identifiers重复。如果你有两个不同设备用了同一个 identifier,HA 会把它们当成同一台设备,实体混在一起,日志里会提示冲突。我的做法是直接用设备的 MAC 或芯片唯一 ID 拼在 identifier 里,例如dev_a1b2c3d4。

二是unique_id重复。写入 HA 注册表的unique_id全局唯一,重名会导致后来者注册失败。建议格式统一为<设备ID>_<功能>,生成时就带上,别靠肉眼保证不重复。

当设备固件升级、发现消息内容变化时,只要unique_id不变,HA 会更新实体属性而不是新建实体。所以unique_id是整个 Discovery 体系里最需要稳定的字段。

4. 从零跑通一次完整的自发现流程

4.1 环境准备与最小验证

在动设备之前,先用命令行把 Broker 和 HA 的通路验证一遍。准备:

  • 一个 MQTT Broker(Mosquitto 就行)。
  • Home Assistant,已配置 MQTT 集成并连上 Broker。
  • 一个 MQTT 客户端工具,命令行用mosquitto_pub/mosquitto_sub,图形化用 MQTTX。

先在 HA 的 MQTT 集成里确认连接正常。然后手动发一条发现消息:

mosquitto_pub -h 192.168.1.10 \ -t "homeassistant/sensor/test_node/test_temp/config" \ -r \ -m '{ "name": "测试温度", "state_topic": "home/test_node/state", "unit_of_measurement": "°C", "device_class": "temperature", "value_template": "{{ value_json.temp }}", "unique_id": "test_node_temp", "device": { "identifiers": ["test_node_01"], "name": "测试节点" } }'

发完之后,去 HA 的实体列表里搜"测试温度"。能出现,说明链路通了。这一步我用得很多,属于排查问题的第一道验证:先排除设备和固件的问题,确认 HA 端一切正常,再往设备侧查。

接着发一条状态数据:

mosquitto_pub -h 192.168.1.10 \ -t "home/test_node/state" \ -m '{"temp": 24.5}'

HA 界面上这个实体应该显示 24.5。

4.2 嵌入式设备侧的实现思路

如果你是用 ESP32、ESP8266 这类设备,思路是:上电连上 WiFi 和 MQTT 后,先把发现消息以 retained 方式发出去,然后周期性发状态。以 Arduino 框架的 PubSubClient 为例,核心逻辑是这样:

// 连接成功后发布发现消息 void publishDiscovery() { const char* topic = "homeassistant/sensor/esp_node/esp_temp/config"; const char* payload = R"({ "name": "节点温度", "state_topic": "home/esp_node/state", "unit_of_measurement": "°C", "device_class": "temperature", "value_template": "{{ value_json.temp }}", "unique_id": "esp_node_temp", "device": { "identifiers": ["esp_node_01"], "name": "ESP测试节点", "model": "ESP32", "manufacturer": "selfmade" } })"; mqttClient.publish(topic, payload, true); // true = retain }

几个实践要点:

  • 发现消息只在连接成功后发一次即可,不需要每次上报状态都发。但如果 HA 重启,retained 消息会帮它恢复,设备无需干预。
  • 判断是否需要重发:可以监听 HA 的状态主题,或者在设备上做一次"重连后重发"。我的经验是 retained 足够,重发多了反而产生大量重复消息。
  • 如果是 STM32 加 4G 模块这类资源受限设备,发现消息 JSON 可以预先拼成字符串常量,减少内存拼接开销。TLS 加密连接时,注意发现消息体积别太大,分段发送在某些模块上会出问题。

4.3 上线、下线与失效处理

设备正常上线就是发 retained 发现消息。设备要下线或彻底停用时,有两种做法:

一是发空配置清实体:

mosquitto_pub -h 192.168.1.10 -t "homeassistant/sensor/esp_node/esp_temp/config" -r -n

二是配合 HA 的availability(可用性)机制。在发现消息里加:

{ "availability_topic": "home/esp_node/status", "payload_available": "online", "payload_not_available": "offline" }

设备连上后往availability_topic发online,断开前发offline(最好用 MQTT 遗嘱消息 Last Will 实现,设备异常掉线时 Broker 自动代发offline)。这样 HA 界面上的实体状态会正确显示为"不可用",而不是一直卡在最后上报的数值上。

提示:遗嘱消息要在 MQTT 连接时就指定。ESP 上用mqttClient.connect(clientId, user, pass, willTopic, willQos, willRetain, willMessage)的重载版本,把离线主题和消息带上。这是让设备状态"诚实反映现实"的关键,很多人忘了配,结果设备拔电了 HA 还显示在线。

5. 排查"消息发了,实体却没出来"

5.1 分层定位法

这个问题我遇到过无数次,总结了一套自下而上的排查顺序:

  1. Broker 层:用mosquitto_sub -t "homeassistant/#" -v订阅,确认消息真的发出去了,主题和 payload 都对。这一步能过滤掉一大半"设备根本没发"的情况。
  2. HA 订阅层:确认 HA 的 MQTT 集成连的是同一个 Broker、同一个端口、同一套账号密码。我见过有人设备连的是 1883,HA 配的是别的端口,一直不通。
  3. 消息格式层:JSON 必须是合法 JSON,不能有注释、不能有尾逗号。用在线工具格式化一下,或者用jq校验:echo '<payload>' | jq .。格式错 HA 会静默丢弃,日志里可能只有一行不易察觉的警告。
  4. 字段语义层:检查component和字段是否匹配。比如把带command_topic的配置发到了sensor类型下,HA 会报字段不识别。

5.2 常见错误对照表

现象最可能原因处理方式
实体完全不出现发现消息没 retain,HA 未收到加-r重发
实体出现但一直"未知"state_topic没收到数据,或模板取值错误用mosquitto_sub核对状态主题和字段
状态显示反了payload_on/off与设备值不符显式声明 payload 或改 value_template
实体重复出现多个unique_id重复改为全局唯一,清理旧实体
删除后发现消息还在空消息没 retain发空 payload 时带上-r
重启后实体丢失消息未 retain全程 retained 发布

5.3 用 value_template 处理复杂 payload

设备上报的数据常常是一个大 JSON,包含多个字段。这时每个实体的发现消息都订阅同一状态主题,各自用模板取字段,是最高效的做法:

{ "state_topic": "home/node1/state", "value_template": "{{ value_json.data.temperature }}", "unit_of_measurement": "°C", "device_class": "temperature" }

如果字段是嵌套的,模板里用点号逐层取。若值是字符串要先转数字,用{{ value_json.temp | float }}。我遇到过一个设备把温度上报成"24.5"字符串,HA 图形化统计里没数据,加上| float过滤器就好了。这类小过滤器在实际项目里能救不少急。

另外要留意模板的容错。设备刚上线还没发状态时,模板对空字符串求值会报错。可以在模板里加默认值:{{ value_json.temp | default(0) }},避免日志刷满警告。

6. 规模化接入时的工程化建议

6.1 主题规划要提前做

设备少时怎么写都行,一旦上百个设备、上千个实体,主题规划就是生死线。我现在的习惯是:

homeassistant/<component>/<device_id>/<entity>/config 发现主题 home/<device_id>/state 设备状态 home/<device_id>/<entity>/set 控制命令 home/<device_id>/status 在线状态

device_id用物理编号,和device.identifiers保持一致。这样一眼就能看出消息属于哪个设备,日志排查、批量脚本操作都方便。切忌用"设备名字+随机数"这种不可追溯的命名。

6.2 网关中间件统一翻译

如果设备本身不会发 Discovery 消息(很多厂家设备只发自己的私有协议),常见做法是做一个网关中间件:订阅设备原始数据,转换成 HA 的发现消息和状态消息再转发。用 Python 的paho-mqtt写一个这样的网关其实不复杂:

import paho.mqtt.client as mqtt import json DISCOVERY_PREFIX = "homeassistant" def publish_discovery(client, device_id, device_name, entity_key, entity_name, unit, dev_class): topic = f"{DISCOVERY_PREFIX}/sensor/{device_id}/{entity_key}/config" payload = { "name": entity_name, "state_topic": f"home/{device_id}/state", "unit_of_measurement": unit, "device_class": dev_class, "value_template": "{{ value_json.%s }}" % entity_key, "unique_id": f"{device_id}_{entity_key}", "device": { "identifiers": [device_id], "name": device_name } } client.publish(topic, json.dumps(payload), retain=True) client = mqtt.Client() client.connect("192.168.1.10", 1883, 60) publish_discovery(client, "sensor01", "网关温度计", "temperature", "温度", "°C", "temperature") client.loop_forever()

这个模式的好处是:设备端零改动,所有转换逻辑集中在中间件,改起来只动一处。生产环境里我会把设备清单做成配置文件,启动时批量注册发现消息,新增设备只加一行配置。

6.3 版本兼容与字段演进

HA 对 Discovery 的字段做过若干次调整,比如状态类、设备分组的相关字段。我的应对策略:

  • 尽量只用长期稳定的核心字段:name、state_topic、command_topic、unique_id、device、value_template。这些几乎不会变。
  • 对新增的可选字段(如entity_category、suggested_display_precision),按需使用,但要留意 HA 版本说明。
  • 升级 HA 前,先在测试实例上导入一份线上发现消息,验证实体能否正常注册和历史数据是否保留。

字段演进不是大问题,真正麻烦的是升级后某些实体变成"恢复状态"。这通常是因为unique_id或device.identifiers变了导致的重新注册。只要这两项不动,升级一般无感。

7. 我在实际项目里踩过的几个具体坑

第一个坑:发现消息的device_class拼错。当时把temperature写成了temp,HA 没有报错,但实体被当作普通文本传感器,单位也能显示,可就是进不了统计和历史图表。找了两小时才定位到字符串拼错。现在的习惯是发现消息发给 HA 后,先在开发者工具里看一眼实体的属性,确认device_class被正确识别。

第二个坑:多个实体共用状态主题时,模板取值失败。有次一个设备上报的是数组而非对象,所有value_template全都取不到值。解决方法是先把原始 payload 打印出来看一眼,再决定模板怎么写。别凭想象写模板,这是铁律。

第三个坑:retain 和遗嘱消息一起用时的顺序问题。设备异常重启后,Broker 代发的遗嘱offline是 retained 的,如果设备上线后没及时更新为online,HA 会一直显示离线。我现在都是设备一连接上就立即发online,并且把发现消息和状态主题都设为 retained,确保状态一致。

第四个坑:清理不彻底留下僵尸实体。测试阶段发过大量发现消息,后来设备删了但 retained 消息还在,HA 每次启动都恢复一堆无效实体。解决办法就是前面说的,删除时发空 retained 消息,或者干脆把测试用的前缀和其他设备隔离。

这些坑的共同点是:它们在日志里几乎都是静默的。HA 不会主动告诉你"你的 JSON 少了个字段"或者"这个 device_class 不认识",绝大多数问题得靠你自己去对比规范、去订阅主题看原始消息。所以养成"发消息前先在订阅端看一眼"的习惯,能省掉大量返工。

最后分享一个我常用的调试动作:把发现消息、状态主题、命令主题都订阅在一个终端里,用mosquitto_sub -t "homeassistant/#" -v -t "home/#" -v,操作设备时全程盯着消息流。消息怎么流的、字段长什么样、retain 标志有没有,一眼全清楚,比翻日志快得多。

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

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

立即咨询