返回博客

HA实体没有唯一标识符?ESP32传感器接入方法

ESP32 自制传感器推进 Home Assistant 后提示“没有唯一标识符”?用输入数值和模板传感器两层实体解决,不写 YAML,附界面步骤和自动创建的 API。

2026年11月2日lvruan
HA实体没有唯一标识符?ESP32传感器接入方法

自己做的 ESP32 温湿度、人体感应,数据推进 Home Assistant 之后,点开实体只看到一行灰字:

此实体(xiaozhi.xiaozhi_a1b2c3_humidity)没有唯一标识符,因此无法在用户界面管理其设置。

改不了名字,分不了区域,HA 一重启实体还会消失,要等设备下次上报才回来。网上的答案大多是"去写 YAML"。其实不用碰配置文件:靠 HA 自带的两种辅助元素,再加几个 API 调用,就能把 ESP32 的数据变成有唯一 ID 的正式实体。下面是我给书房那块 ESP32-S3(接了 DHT11 和 HC-SR501)实际跑通的做法。

改造后:6 个实体都能在“辅助元素”里管理

为什么会提示“没有唯一标识符”

最省事的推数据方法,是直接调用 REST 接口:

POST /api/states/sensor.study_temperature
{"state": 23.1, "attributes": {"unit_of_measurement": "°C"}}

这样写进去的只是一条临时状态。它不进实体注册表(entity registry),没有 unique_id,所以:

  • 界面里不能改名、改图标、分配区域;
  • 每次打开都弹出上面那句提示;
  • HA 重启后状态清空,要等下一次上报才出现。

想在界面里管理,实体就得由某个集成正式登记。不装 MQTT、不写 YAML 的话,最顺手的是 HA 自带的两种辅助元素:输入数值 / 输入布尔值,和模板传感器。

思路:两层实体,各管一件事

ESP32 ──定时上报──▶ 你的中转服务(或脚本)
                        │ 调用服务 input_number.set_value / input_boolean.turn_on|off
                        ▼
   第一层:原始值(隐藏)  input_number.xxx_temperature_raw
                          input_number.xxx_humidity_raw
                          input_boolean.xxx_motion_raw
                        │ 模板读取
                        ▼
   第二层:对外实体        sensor.xxx_temperature      书房 温度
                          sensor.xxx_humidity         书房 湿度
                          binary_sensor.xxx_motion    书房 有人
  • 第一层只负责存数。输入数值、输入布尔值本身就有唯一 ID,可以通过服务调用写入,写完把它们隐藏起来。
  • 第二层才是给仪表盘和自动化用的。模板传感器带上设备类别(温度、湿度、运动)和单位,HA 的历史曲线、统计、语音助手都会把它当成真传感器。
  • 传感器拔掉时,往第一层写一个哨兵值(温度写 -100、湿度写 -1),第二层的可用性模板据此显示“不可用”,不会留着一个过期的旧数。

做完之后,打开对外实体的设置,名称、图标、区域都能改,实体标识符那一栏也能编辑:

模板实体有唯一 ID,设置页可以正常编辑

在界面里手动配置(以温度为例)

湿度、人体感应同理,换一下名称和类型即可。

第 1 步:建原始值辅助元素

设置 → 设备与服务 → 辅助元素 → 创建辅助元素 → 数字:

  • 名称:xxx_temperature_raw(实体 ID 会按名称生成,先用英文,之后再改中文显示名)
  • 最小值 -100,最大值 100,步长 0.1,显示模式选“输入框”,单位 °C

人体感应选 开关(input_boolean)。

第 2 步:建对外的模板传感器

创建辅助元素 → 模板 → 模板传感器:

  • 名称:xxx_temperature
  • 状态模板:{{ states('input_number.xxx_temperature_raw') }}
  • 度量单位 °C,设备类别“温度”,状态类“测量值”
  • 高级选项 → 可用性模板:{{ states('input_number.xxx_temperature_raw') | float(-100) > -50 }}

模板传感器的配置:状态模板读取原始值

人体感应用模板二元传感器,状态模板写 {{ is_state('input_boolean.xxx_motion_raw', 'on') }},设备类别选“运动”。

第 3 步:整理

把三个原始值实体设成隐藏,把三个对外实体改成中文名(比如“书房 温度”),再分配到“书房”区域。

写入数据

ESP32 本身,或者它上报到的中转服务,按下面的方式调用服务就行。令牌在 HA 个人资料页底部的“长期访问令牌”里创建:

# 温度
curl -X POST "$HA_URL/api/services/input_number/set_value" \
  -H "Authorization: Bearer $HA_TOKEN" -H "Content-Type: application/json" \
  -d '{"entity_id": "input_number.xxx_temperature_raw", "value": 23.1}'

# 有人 / 没人
curl -X POST "$HA_URL/api/services/input_boolean/turn_on" \
  -H "Authorization: Bearer $HA_TOKEN" -H "Content-Type: application/json" \
  -d '{"entity_id": "input_boolean.xxx_motion_raw"}'

传感器读不到时,温度写 -100、湿度写 -1,对外实体会自动变成“不可用”。

设备多了,用 API 自动建实体

一台设备手动点一遍还行,设备多了就该交给程序。上面两层实体都能通过接口创建,我的中转服务在每台设备第一次上报时自动检查,缺什么补什么:

  1. 建原始值:走 WebSocket(/api/websocket)认证后发 {"type": "input_number/create", "name": "xxx_temperature_raw", "min": -100, "max": 100, "step": 0.1, "mode": "box", "unit_of_measurement": "°C"},输入布尔值用 input_boolean/create。
  2. 建模板实体:走配置流程,依次 POST 三次:
    • /api/config/config_entries/flow,body {"handler": "template"}
    • /api/config/config_entries/flow/<flow_id>,body {"next_step_id": "sensor"}(二元传感器填 binary_sensor)
    • 同一地址,body {"name": "xxx_temperature", "state": "{{ ... }}", "unit_of_measurement": "°C", "device_class": "temperature", "state_class": "measurement", "additional_options": {"availability": "{{ ... }}"}}
  3. 改显示名、隐藏原始值:WebSocket 发 config/entity_registry/update,带上 name 和 "hidden_by": "user"。
  4. 已经存在就跳过:先用 config/entity_registry/get 查一下实体是否已登记。模板公式要升级时,走选项流程 /api/config/config_entries/options/flow 更新,不用删了重建。

令牌需要管理员账号的长期访问令牌,普通用户调不了这些创建接口。

效果

对外实体就是普通传感器,历史曲线照常记录。下图是书房一个下午的温度,中间那段降温和回升都完整记了下来,右侧的小缺口是从临时状态迁移到正式实体时留下的。

书房温度的历史曲线

踩过的坑

  • DHT11 读数会跳:标称误差 ±2°C,每分钟读一次,会在 ±0.5°C 范围里来回变,屏幕显示和 HA 记录差 1 度很正常。想要更准,换 SHT30 或 AHT20。
  • HA 和设备屏幕对不上:设备屏幕是实时值,HA 里的值最多晚一个上报周期(我设的 60 秒)。
  • 旧的临时状态要清掉:从“直接写状态”迁移过来时,先 DELETE /api/states/<entity_id> 删掉同名临时状态,再建模板实体,不然新实体会拿到带 _2 后缀的 ID。
  • 还是看到“没有唯一标识符”:说明还有程序在用 POST /api/states 写状态,把它改成调用 input_number.set_value 就行。