
很多第一次把设备接入 Home Assistant后面统称 HA的人都会在设备页里遭遇一种说不清道不明的混乱明明一个传感器上报了温度、湿度、电量三个数值结果 HA 里冒出来三四个互相没有归属的实体有些挂在“无设备”下面有些干脆看起来像是从地底下冒出来的孤魂野鬼。因为 MQTT 本身只是一个消息总线它只负责把主题和 payload 传来传去并不知道这些数据来自哪台物理设备而 HA 又要努力保存“一台设备”这个心智模型所以需要我们额外做点事把多个实体聚合到同一个设备名下。这篇文章就专门讲怎么处理“一个 MQTT 设备对应多个实体”的三种典型做法MQTT Discovery 自动发现、configuration.yaml 手写配置、以及用 json_attributes_topic 配合 Template 传感器做状态包拆分。对应到实际场景就是“固件主动向 HA 报名”“人在 HA 侧手动声明”“把单个 JSON 主题灵活再加工”三条路。只要手里有 MQTT 设备并且希望 HA 的设备页干净得像一本有序的相册这篇文章都值得看完。1. HA 里的“多实体设备”先得搞懂实体与设备的绑定关系1.1 MQTT 设备不会自己“组装”设备很多刚接触 MQTT 的人会以为只要设备往一个 topic 上发数据HA 就会自动知道这是哪台设备。其实完全不是这样。MQTT 主题天生是平铺的设备只是发布者消息里没有任何“我是一台设备”的元信息所以 HA 必须依靠额外的描述性配置才能决定一个实体归属于哪台设备。举个例子我用一块 ESP32 开发板采集书房温度和湿度分别发到home/study/temp和home/study/humidity两个主题上。如果我只在 HA 里配了两个 MQTT 传感器而没给任何设备信息那么这两个实体会各自独立地出现在 HA 里不会自动组成“书房传感器”这个设备。哪怕你给他们取名字叫“书房温度”和“书房湿度”HA 也只会认为这是两个没有物理归属的散装实体。这就是理解“多实体设备”的第一把钥匙实体是实体设备是设备两者通过配置中的 device 字段建立关系。没有这个关系数据再多也是一盘散沙。1.2 device 字段就是聚合钥匙在 MQTT 实体的配置中有一个 device 字段专门用来描述硬件本身。常见的子字段包括identifiers设备唯一标识符字符串或字符串数组name显示在 HA 设备页里的设备名称manufacturer / model厂商与型号信息sw_version固件版本via_device如果设备是通过网关接入用来标识上层网关真正起“聚合”作用的是 identifiers。只要两个实体的 device.identifiers 有任意一个字符串相同HA 就会把它们归到同一台设备下。这个合并规则是跨实体类型的也就是说一个 sensor 实体和一个 switch 实体只要 identifiers 里都写着kitchen_plug_01它们就会整整齐齐地出现在同一个设备卡片下。我见过不少翻车案例问题就出在 identifiers 太“大众”了。比如两个不同房间的 ESP01 模块都用了esp01作为标识符HA 就会把两个完全无关的设备合并成一个然后温度、开关、灯全混在一起删也删不干净。所以我的习惯是 identifiers 里带上场景前缀例如study_esp01_01、living_esp01_01宁可丑一点也不要撞车。1.3 三种方式的适用场景速查接入方式配置发生在哪一端适合场景主要代价MQTT Discovery 自动发现固件或网关主动发布 config 主题自研固件、支持自动发现的量产设备、希望“插电即用”固件侧要按约定格式构造 JSONYAML 手写配置HA 的 configuration.yaml固件不支持自动发现、临时联调、只想快速接入每次改动需要重载或重启 HAJSON 状态包拆分HA 侧的 json_attributes Template设备只发一个综合 JSON 包、想在 HA 侧派生更多实体需要模板知识链路稍长这三种方式不是互斥关系后面我会专门讲组合使用。先分别把每一种的原理和实操细节讲透。2. 方式一用 MQTT Discovery 让设备自动“报名”多实体2.1 Discovery 的工作过程开启 MQTT 集成时有一个开关叫“启用发现”默认是打开的。开启后HA 的 MQTT 集成会订阅一类特殊主题格式是homeassistant/组件类型/node_id/object_id/config其中组件类型可以是 sensor、binary_sensor、switch、climate、light 等等最后必须以/config结尾。设备把一段 JSON 发布到这种主题上HA 收到后就会自动创建实体并根据 JSON 里的 device 字段把实体归类到设备下。关键点在于每个实体都需要单独一条 config 主题但所有 config 里的 device.identifiers 可以指向同一个设备。这就是一个多实体设备在自动发现机制下的呈现方式。比如下面这段模拟操作我手动向 MQTT 服务器发布两个实体配置就足以让 HA 里出现一台设备下挂两个传感器的效果。2.2 用 mosquitto_pub 手动模拟一个双实体设备我假设你本地已经有一个 MQTT 服务器并能从 HA 所在主机访问它。用命令行的 mosquitto_pub 来模拟一台“书房多功能传感器”的上报过程先发温度实体的配置mosquitto_pub -h 127.0.0.1 -t homeassistant/sensor/study_multi/temperature/config -m { name: 书房温度, unique_id: study_multi_temp, state_topic: home/study/multi_sensor, value_template: {{ value_json.temp }}, unit_of_measurement: °C, device_class: temperature, device: { identifiers: [study_multi], name: 书房多功能传感器, manufacturer: 自制, model: ESP32-DHT22, sw_version: 1.0 } }再发湿度实体的配置注意 device 部分完全一致mosquitto_pub -h 127.0.0.1 -t homeassistant/sensor/study_multi/humidity/config -m { name: 书房湿度, unique_id: study_multi_humidity, state_topic: home/study/multi_sensor, value_template: {{ value_json.hum }}, unit_of_measurement: %, device_class: humidity, device: { identifiers: [study_multi], name: 书房多功能传感器, manufacturer: 自制, model: ESP32-DHT22, sw_version: 1.0 } }然后模拟设备上报一次数据mosquitto_pub -h 127.0.0.1 -t home/study/multi_sensor -m {temp: 25.3, hum: 52}此时你到 HA 的设备页搜索“书房多功能传感器”会看到一台设备下面挂着“书房温度”和“书房湿度”两个实体。这两个实体都由同一个 state_topic 驱动只是通过 value_template 各自解析出 JSON 中不同的字段。这就是 Discovery 方式最核心的姿势实体靠 config 主题去创建设备靠 identifiers 去归组数据靠 state_topic 去更新。2.3 固件侧发布 config 的参考写法可能有人觉得这只是命令行模拟真到了写固件的时候不知道从哪下手。其实原理一模一样只是把发布 config 的动作放到 MCU 代码里。下面是一段基于 Arduino PubSubClient 的简化片段演示设备上电后发布两个实体的配置然后在循环里上报一次数据#include PubSubClient.h void publishDeviceConfigs(PubSubClient client) { client.publish( homeassistant/sensor/study_multi/temperature/config, {\name\:\书房温度\,\unique_id\:\study_multi_temp\, \state_topic\:\home/study/multi_sensor\, \value_template\:\{{ value_json.temp }}\, \unit_of_measurement\:\°C\,\device_class\:\temperature\, \device\:{\identifiers\:[\study_multi\], \name\:\书房多功能传感器\}} ); client.publish( homeassistant/sensor/study_multi/humidity/config, {\name\:\书房湿度\,\unique_id\:\study_multi_humidity\, \state_topic\:\home/study/multi_sensor\, \value_template\:\{{ value_json.hum }}\, \unit_of_measurement\:\%\,\device_class\:\humidity\, \device\:{\identifiers\:[\study_multi\], \name\:\书房多功能传感器\}} ); } void loop() { char payload[64]; snprintf(payload, sizeof(payload), {\temp\:%.1f,\hum\:%.1f}, temp, hum); client.publish(home/study/multi_sensor, payload); delay(10000); }要注意config 主题必须在实体真正上报数据之前发布一次不然 HA 还没见过这个实体后面的 state 消息只会被当作陌生主题忽略掉。设备重启时重新发布 config 也是安全的HA 会用相同 unique_id 的配置刷新实体不会产生重复实体。2.4 Discovery 方式的坑我替你踩过了第一unique_id 一定要全局唯一。unique_id 是 HA 识别一个实体身份的“身份证”。如果两个不同设备发出了一模一样的 unique_id它们就会在 HA 里互相覆盖今天显示的是 A 设备的数据明天变成 B 设备的数据排查起来非常痛苦。我在第 1 节说的 identifiers 要加前缀对 unique_id 同样成立。第二空 payload 会删除实体。向 config 主题发送空消息表示“撤销这个实体”HA 会把它从设备页移除。移除后历史数据还在但实体要重新发布 config 才能恢复。所以调试的时候别手滑按了空发布。第三value_template 里访问的是 JSON 包全部内容必须用value_json.字段名。如果 JSON 包里有嵌套结构就要写多层路径比如{{ value_json.sensor.avg_temp }}。模板出错时实体通常显示为不可用而不是报一个明明白白的错这一点对新手不那么友好。第四建议同一台设备的所有实体统一配置一个 availability_topic。比如设备在线时发online离线时发offline这样只要主设备掉线所有实体都会一起变灰而不是保留最后一个假数据。多实体设备最怕的就是“温度已 35 度设备其实早断电了”。3. 方式二手写 YAML 把多实体“钉”进同一个设备3.1 什么时候该放弃固件层面Discovery 方式很优雅但有个前提固件或者网关愿意在启动时发那几条 config 主题。很多成品设备、老旧传感器并没有这个能力它们只会闷头往一个固定 topic 上发数据。这种时候最直接的办法就是在 HA 的 configuration.yaml 里手写实体定义。对临时联调来说这尤其快。比如我拿到一个二手空气质量检测仪它是基于 MQTT 的但协议文档里完全没提自动发现只知道 it 上报到一个 JSON 主题。我完全没必要去拆固件直接在 HA 里写几个 sensor 指向同一个主题、同一个 device 信息就完事了。改动虽然要重载 HA但胜在快速和可控。3.2 用 YAML 锚点复用 device 信息既然要把多个实体挂到同一个设备每个实体里都得写一遍 device 字段重复信息特别多。这里我推荐用 YAML 锚点来复用写法如下mqtt: sensor: - name: 客厅空调温度 unique_id: living_ac_temperature_mqtt state_topic: home/living_ac/status value_template: {{ value_json.temperature }} device_class: temperature unit_of_measurement: °C device: ac_device identifiers: - living_ac name: 客厅空调 manufacturer: 某品牌 model: 空调网关 sw_version: 1.2.0 - name: 客厅空调设定温度 unique_id: living_ac_target_temp_mqtt state_topic: home/living_ac/status value_template: {{ value_json.target_temperature }} device_class: temperature unit_of_measurement: °C device: *ac_device - name: 客厅空调风速 unique_id: living_ac_fan_speed_mqtt state_topic: home/living_ac/status value_template: {{ value_json.fan_speed }} device: *ac_deviceac_device是锚点定义表示“把这个 device 内容记下来起个名字叫 ac_device”后面的*ac_device是引用锚点表示“把前面记下来的内容原样展开在这里”。加了锚点之后就算一个空调设备挂七八个实体device 字段也只写一遍后续维护时改设备名、改型号只改一处即可。如果个别实体需要和默认 device 信息略有不同比如版本号不一样YAML 锚点并不擅长局部覆盖。这种情况我一般直接复制粘贴 device 块或者在一个实体上用: *ac_device再覆盖字段但后者要判断旧版本 HA 的兼容性实战里反而没有复制清晰。所以我建议大多数实体完全相同就直接用锚点只有一两个特殊实体就复制。3.3 把开关、按钮等控制实体也挂到同一设备YAML 方式不只是能把多个 sensor 挂到一台设备它也能把不同类型的实体混合挂载。比如空调这个设备除了温度传感器通常还得有一个电源开关。在同一个mqtt:配置块里继续加 switch 就行mqtt: sensor: # 上面那些传感器继续写省略 switch: - name: 客厅空调电源 unique_id: living_ac_power_mqtt state_topic: home/living_ac/status value_template: {{ ON if value_json.power else OFF }} command_topic: home/living_ac/set payload_on: power_on payload_off: power_off device: *ac_device这里有个非常容易踩的点MQTT switch 默认认为状态主题里的ON/OFF两个大写字符串代表开关状态。如果设备返回的数据是true/false或者1/0你必须用 value_template 先把数据转换成 ON/OFF变成上面那种写法或者你把 payload_on、payload_off 显式设置成设备实际返回的字符串值。我见过很多人在这一步卡住开关状态一直显示“不可用”其实不是数据没到而是 HA 看不懂 payload。3.4 手写 YAML 最常翻车的四个细节第一缩进。YAML 对缩进极其敏感mqtt:一定要顶格下面的sensor:、switch:缩进两个空格或四个空格实体列表在它们之下继续缩进。任何一级错位HA 重载时都会直接报错甚至在集成页显示“无法加载配置”。第二unique_id 不能和 UI 里已有的实体冲突。从新版 HA 开始MQTT 的 YAML 配置和 UI 添加的实体可以共存但如果两个地方定义了同一个 unique_id就会有一个实体注册失败。我的经验是同一个设备尽量只从一个渠道管理要么全 YAML要么全 UI避免精神分裂。第三改动后必须重载或重启。YAML 不是热加载的改完 configuration.yaml 需要到开发者工具里重载配置或者直接重启 HA新实体才会出现。很多人写完发现没变化以为配错了其实只是没重载。第四value_template 返回的是字符串。如果温度模板里直接{{ value_json.temperature }}HA 会把它当成字符串而不是数字单位显示和某些仪表盘组件就会表现得很怪。建议在模板里做类型转换例如{{ value_json.temperature | float }}简单有效。4. 方式三单个 JSON 主题拆出整套实体的属性箱方案4.1 场景与思路有一类设备特别让 YAML 党头疼它不按主题拆数据而是把所有运行参数打包成一个 JSON统一发到一个 topic 上。比如一个家庭电表一次报文里可能有几十个字段瞬时功率、电压、电流、频率、总电量、开关状态有时候还会带上温度和错误码。如果按方式二的做法为每一个字段都写一个 MQTT sensorconfiguration.yaml 会被撑得很长而且每当设备新增一个字段都得改一遍 HA 配置。更麻烦的是很多字段你可能只是偶尔看一眼并不需要它们都成为 HA 里的“实体”。这时候就该用第三套方案让一个 MQTT sensor 把整个原始 JSON 装进 attributes然后用 Template 传感器按需派生你真正关心的实体。4.2 MQTT 侧先落一个“状态包”实体状态包实体的功能很简单接收原始 JSON 报文把整包数据原样保存到属性的位置同时从里面挑一个字段当“主状态值”。以电表为例mqtt: sensor: - name: 家庭电表状态包 unique_id: home_power_meter_raw state_topic: home/power_meter/data json_attributes_topic: home/power_meter/data value_template: {{ value_json.state }} device: identifiers: - home_power_meter name: 家庭电表 manufacturer: 某品牌 model: PZEM-004T 改造版关键在json_attributes_topic。它会把这个 topic 上收到的 JSON 整体存入实体的 attributes 中之后任何其他配置都能通过state_attr(sensor.home_power_meter_raw, 字段名)读到对应属性。json_attributes_topic和state_topic相同时每次报文到达都会同时更新主状态值和 attributes这是最常见的用法。为什么不用一堆 value_template 直接拆出所有实体原因很简单拆得太碎会污染实体列表而且很多字段你根本用不上。状态包方案的最大价值是原始数据完整保留派生工作交给 HA 侧处理的 Template 实体。4.3 用 Template 传感器生成派生实体有了状态包实体接下来就可以用 HA 的 Template 传感器按需制造实体了。比如我想单独看功率、电压、总电量就写template: - sensor: - name: 家庭电表瞬时功率 unique_id: home_power_meter_power state: {{ state_attr(sensor.home_power_meter_raw, power) | float }} unit_of_measurement: W device_class: power - name: 家庭电表电压 unique_id: home_power_meter_voltage state: {{ state_attr(sensor.home_power_meter_raw, voltage) | float }} unit_of_measurement: V device_class: voltage - name: 家庭电表累计电量 unique_id: home_power_meter_energy state: {{ state_attr(sensor.home_power_meter_raw, energy_kwh) | float }} unit_of_measurement: kWh device_class: energy状态包实体收到的 JSON 里如果有{voltage: 220.5, power: 320.8, energy_kwh: 123.45}这三个 Template 实体就会各取所需分别显示对应数值。派生实体本身的设备归属在新的 HA 版本里也可以通过 device 字段指定到“家庭电表”这台设备上如果你的版本不支持也不影响功能只用留意一下设备页里这个派生实体是独立显示的即可。这种做法的另一个好处是派生实体可以做得非常“业务化”。比如你想在功率超过某阈值时生成一个标记实体就不需要去读原始传感器再写一堆复杂自动化直接写一个二值 Template 传感器即可。派生逻辑都集中在模板配置里一目了然。4.4 方式三的限制不是所有高频数据都适合无脑映射需要泼一盆冷水json_attributes_topic 虽然方便但并不意味着可以把高频主题直接映射成 HA 实体。MQTT 报文每一次到达HA 都会更新状态及 attributes同时写入历史数据库。如果一个设备每秒发 10 条报文第二天你的 HA 数据库就会膨胀得非常快查询历史也变得拖沓。所以我的建议是这种方式适合发布频率不高于每 10 秒一次的数据量测类设备比较合适。如果是高频波形、瞬时采样之类的数据应该让设备侧先做聚合在 MQTT 端发分钟均值或者只发“变化超过阈值”的报文再由 HA 接收。核心原则是HA 是自动化中枢不是时序数据库别把原始流往里硬灌。5. 三种方式怎么选、怎么混用、怎么平滑迁移5.1 实际项目里混用才是常态很多教程喜欢把三种方式讲成非此即彼但真实项目里我几乎都是混着用。举一个我实际搭过的小项目一个自制网关下挂了一堆传感器有门磁、温度、湿度固件本身支持 MQTT Discovery所以这部分我用的是方式一网关还同时把一块电表的数据整理成 JSON 转发到home/gateway/energymeter电表部分我用的是方式三做状态包拆分后来临时想给网关加一个手动重启开关固件又不支持我就直接在 YAML 里加了一个 MQTT switch也就是方式二。有意思的是只要我在三种配置里都让 device.identifiers 保持一致HA 最终会把它们全部聚合到同一台网关设备下门磁、温度和开关出现在同一张设备卡片里。这说明三种方式之间的关系不是“选哪个更好”而是“哪个环节最省事就用哪个”HA 只关心最终的 device 归属。5.2 迁移时保住历史记录的关键操作还有一种非常常见的场景设备固件本来不支持 Discovery我用 YAML 接好了也积攒了一个月的历史数据。后来固件升级了支持自动发现了我想平滑切换到方式一。这时候最忌讳直接删 YAML、让 Discovery 从头创建实体因为一旦 unique_id 变了HA 会把它当成一个全新实体旧 entity_id 的历史就全断了。正确的顺序是先让固件发布新的 config 主题并且 config 里的 unique_id 必须和原来 YAML 里保持一致。等 HA 里新实体出现并开始正常更新之后再删除 YAML 里的旧配置并重启 HA。这样 HA 会把同一个 unique_id 对应的新配置当作“对旧实体的更新”entity_id 基本保持不变历史记录也能连上。所以我给所有设备的唯一建议就是从第一天起就认真设计 unique_id 的命名规则把它当作跟设备 MAC 地址一样不可变的东西。实体可以改名主题可以重配但 unique_id 变了历史就真的回不来了。5.3 多实体设备的统一离线开关最后补一个多实体设备绕不开的点availability。一台设备挂了三五个实体之后如果每个实体单独判断在线离线很容易出现“温度显示 25.3 度、湿度已经不可用”这种奇怪的混合状态。合理做法是让所有实体共用同一个 availability_topic设备离线时发布offline在线时发布online。mqtt: sensor: - name: 书房温度 unique_id: study_multi_temp state_topic: home/study/multi_sensor value_template: {{ value_json.temp }} availability_topic: home/study/status payload_available: online payload_not_available: offline device: identifiers: [study_multi] name: 书房多功能传感器如果设备端的在线状态不是单独的online/offline文本而是放在某个 JSON 包里面可以用availability_topic加availability_template来解析。只要主设备掉电或断网这台设备名下的所有实体就会整体变灰HA 的前端和自动化也都能立刻知道这台设备不可信而不是对着一个过期的 25.3 度继续做决策。个人分享一句体会我后来凡是做带多传感器的设备都会先在草稿纸上把 unique_id 和 identifiers 的命名表列出来再决定用哪种方式接入。毕竟 HA 的实体可以随便增删但历史数据不是你想留就能留得住的。唯一一次从 YAML 平滑迁到 Discovery就是因为命名一路没变整个过程顺得让我差点忘了这回事。建议你也试试这个迁移动作等新配置发出去、旧配置删干净、重启一次之后看到设备卡片和所有历史都安安稳稳躺在原处那种舒爽感值得折腾一次。