积分 224
金钱 202
威望 0
贡献 0
HASS币 0
中级会员
积分 224
金钱 202
HASS币 0
【开源分享】ESP32 完美接入非标准蓝牙风扇灯(支持 HA + 小爱同学 + 物理遥控器双向同步) 💡 项目背景 家里买了支持蓝牙 Mesh 的风扇灯(协议特征码 134D),自带遥控器和小爱同学控制。但痛点很明显:
无法直接接入 Home Assistant。 网上现有的蓝牙网关方案,要么只能单向控制,要么状态延迟严重,要么用物理遥控器关灯后,HA 里的状态依然显示“开”。
经过几天的死磕和抓包分析,我终于用 ESP32 + ESPHome 打造了一个工业级稳定的蓝牙网关,完美解决了所有痛点!
✨ 最终实现的功能 ✅ 全平台控制:支持 Home Assistant、米家 APP、小爱同学语音控制。 ✅ 完美双向同步:在 HA 或小爱中操作,状态秒级同步;使用物理遥控器操作,HA 和小爱也能在 1~2 秒内自动更新真实状态。 ✅ 精细控制:支持灯的开关、亮度 (0-100%)、色温 (2700K-6500K),以及风扇的 4 个档位。 ✅ 断网可用:即使外网断开,只要局域网还在,HA 依然可以通过 ESPHome API 完美控制设备。 ✅ 极低资源占用:智能节流机制,静止状态下每 60 秒才上报一次保活,极大减轻 MQTT 负担。
🕳️ 核心避坑指南(干货满满,建议收藏) 在开发过程中,我踩了无数个坑,如果你也在做类似项目,请务必注意以下几点:
1. 致命陷阱:ESPHome 中文实体名导致 ID 冲突 现象:编译报错 Duplicate sensor entity... Both convert to ASCII ID: '_____'。原因:ESPHome 在底层生成 ID 时,会把中文字符全部替换为下划线 _。导致“餐厅灯亮度”和“餐厅灯色温”都被转成了 _____dining,引发冲突。解决:name 字段必须使用纯英文(如 Dining Light Brightness)。刷入固件后,可以在 Home Assistant 的实体设置中,手动将其“显示名称”改回中文,这样既保证了底层 ID 唯一,又兼顾了界面美观。
2. 版本变迁:ESPHome 2026.x 已废弃 light.template 现象:报错 Platform not found: 'light.template'。原因:新版 ESPHome 移除了该组件。解决:放弃使用虚拟 Light 实体,改用 switch.template + binary_sensor.template + sensor.template 的组合。通过 switch 发送控制指令,通过 sensor 接收 esp32_ble_tracker 解析出的真实状态,逻辑更清晰,且 100% 兼容新版。
3. 逻辑灾难:MQTT 控制与上报回环(点一次发 4 次指令) 现象:在 HA 点击一次开灯,日志显示连续发送了 4 次 BLE 指令,设备状态在开/关之间反复横跳。原因:在 switch 的 turn_on_action 中,不仅发送了 BLE 指令,还执行了 id(mqtt).publish("topic", "on")。由于 ESP32 自己也订阅了这个 topic,它收到了自己发出的消息,再次触发了 on_message,形成了死循环。解决:控制端(switch/button)只负责发送 BLE 指令,绝不主动 publish 控制 topic! 状态的上报全部交由 esp32_ble_tracker 监听到物理设备广播后,统一向 /up 主题发布。
4. 体验优化:解决 BLE 硬件广播延迟带来的“假死”错觉 现象:点击开关后,HA 界面要等 1~2 秒才变化,感觉非常卡顿。原因:ESP32 是被动监听者,必须等待灯具处理完指令并发出下一个 BLE 广播(通常周期为 1-2 秒)才能捕获到状态变化。解决:
方案 A(推荐):在 switch 中设置 optimistic: false,并配合 lambda: return id(sensor).state;。让 UI 状态严格跟随物理设备的真实广播,虽然慢 1 秒,但绝对准确,永远不会出现状态撕裂。 方案 B:在 esp32_ble_tracker 中加入智能重试机制。如果状态变化后首次上报失败,会在 3 秒后自动重试最多 3 次,确保状态最终一致性。
🏗️ 系统架构与数据流 为了保证绝对的稳定性,我将“控制流”和“状态流”彻底解耦:
控制流(下发):HA / 小爱同学 ➔ MQTT (ctlight002) ➔ ESP32 on_message ➔ 发送 BLE HEX 指令 ➔ 物理灯具执行。 状态流(上报):物理灯具执行完毕 ➔ 发出 BLE 广播 ➔ ESP32 esp32_ble_tracker 捕获并解析 ➔ 更新内部 sensor (供 HA 实时显示) ➔ 同步更新 switch 状态 ➔ 发布到 MQTT (ctlight002/up,供小爱同学同步)。
📦 核心代码片段分享
yaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# ===================== 项目配置 =====================substitutions: project_name: "byy1" build_time: "Unknown"esphome: name: ble-2 friendly_name: "${project_name}" comment: "${project_name} - Build: ${build_time}"esp32: board: esp32dev flash_size: 4MB framework: type: esp-idf sdkconfig_options: CONFIG_FREERTOS_UNICORE: y CONFIG_BT_ENABLED: y CONFIG_BT_BLE_ENABLED: y# ===================== 系统基础 =====================logger: baud_rate: 0 level: INFOwifi: ssid: "***********" password: "...************" fast_connect: true power_save_mode: none ap: ssid: "BLE-GW Fallback" password: "12345678"captive_portal:web_server: port: 80ota: - platform: esphomeapi: reboot_timeout: 0s# ===================== BLE 组件 =====================external_components: - source: type: local path: componentsble_tx: id: txesp32_ble: io_capability: none enable_on_boot: true bluetooth_proxy: active: true cache_services: true# ===================== 巴法云 MQTT =====================mqtt: id: bemfa broker: bemfa.com port: 9501 username: "**********" password: "**********" client_id: "*********" keepalive: 30s discovery: false birth_message: will_message: on_message: # ========================================================== # 餐厅灯 (ctlight002) # ========================================================== - topic: "ctlight002" then: - lambda: |- if (x == "on") { id(tx).send_hex("0201021BFF114D191848802770000001819721277070005FC86FB84848A7F8|0201021BFF114D191848802770000001819727277070005FC86FB84848A7F2"); } else if (x == "off") { id(tx).send_hex("0201021BFF114D191A4880277000000126277670005FC86FB84848A7F08088|0201021BFF114D19174880277000000181809727277070005FC86FB84848A5"); } else if (x.find("on#") == 0) { std::string rest = x.substr(3); int v = atoi(rest.c_str()); size_t p2 = rest.find('#'); if (p2 != std::string::npos) { int k = atoi(rest.substr(p2 + 1).c_str()); if (k <= 3100) { id(tx).send_hex("0201021BFF114D1917488027700000018181C227277070005FC86FB84848FF|0201021BFF114D191C488027700000017170005FC86FB84848A7F080809725"); } else if (k <= 5000) { id(tx).send_hex("0201021BFF114D191248802770000001B9491DDDF080809727277070005F1A|0201021BFF114D191A4880277000000126277070005FC86FB84848A7F08082"); } else { id(tx).send_hex("0201021BFF114D1917488027700000018181C2D8277070005FC86FB84848F0|0201021BFF114D191A4880277000000126277070005FC86FB84848A7F08082"); } } if (v <= 10) { id(tx).send_hex("0201021BFF114D191848802770000001819676257070005FC86FB84848A7A6|0201021BFF114D1911488027700000016EB84848A7F080809727277070005D"); } else if (v <= 30) { id(tx).send_hex("0201021BFF114D19144880277000000149A6A1B3809727277070005FC86F3F|0201021BFF114D191F488027700000015EC86FB84848A7F080809727277072"); } else if (v <= 45) { id(tx).send_hex("0201021BFF114D191B48802770000001267121665FC86FB84848A7F080802D|0201021BFF114D19144880277000000149A7F080809727277070005FC86FBA"); } else if (v <= 55) { id(tx).send_hex("0201021BFF114D191248802770000001B94919D8F080809727277070005F1B|0201021BFF114D19144880277000000149A7F080809727277070005FC86FBA"); } else if (v <= 70) { id(tx).send_hex("0201021BFF114D191948802770000001962676E970005FC86FB84848A7F06D|0201021BFF114D1913488027700000014948A7F080809727277070005FC86D"); } else if (v <= 90) { id(tx).send_hex("0201021BFF114D1913488027700000014949F63C80809727277070005FC84F|0201021BFF114D1911488027700000016EB84848A7F080809727277070005D"); } else { id(tx).send_hex("0201021BFF114D191B48802770000001267121FF5FC86FB84848A7F08080C4|0201021BFF114D191E48802770000001015FC86FB84848A7F0808097272772"); } } # ========================================================== # 餐厅风扇 (ctfan003) # ========================================================== - topic: "ctfan003" then: - lambda: |- if (x == "on") { id(tx).send_hex("0201021BFF114D19184880277000000181972E277070005FC86FB84848A7FB|0201021BFF114D191F488027700000015EC86FB84848A7F080809727277072"); } else if (x == "off") { id(tx).send_hex("0201021BFF114D191648802770000001F180899727277070005FC86FB84843|0201021BFF114D1919488027700000019627277070005FC86FB84848A7F082"); } else if (x.find("on#") == 0) { int s = atoi(x.substr(3).c_str()); ESP_LOGW("CMD", "ctfan speed=%d", s); if (s == 1) { id(tx).send_hex("0201021BFF114D191048802770000001C96FA24848A7F0808097272770701C|0201021BFF114D191548802770000001A6F080809727277070005FC86FB84A"); } else if (s == 2) { id(tx).send_hex("0201021BFF114D1918488027700000018197AF277070005FC86FB84848A77A|0201021BFF114D191C488027700000017170005FC86FB84848A7F080809725"); } else if (s == 3) { id(tx).send_hex("0201021BFF114D191A488027700000012627F570005FC86FB84848A7F08007|0201021BFF114D191C488027700000017170005FC86FB84848A7F080809725"); } else if (s == 4) { id(tx).send_hex("0201021BFF114D191548802770000001A6F080809727277070005FC86FB84A|0201021BFF114D1911488027700000016EB8CE48A7F08080972727707000D7"); } } # ========================================================== # 次卧灯 (cwlight002) # ========================================================== - topic: "cwlight002" then: - lambda: |- if (x == "on") { id(tx).send_hex("0201021BFF114D1917F0CF2D70000001CECF9B2D2D7070005CBF1D60F0F0F4|0201021BFF114D191AF0CF2D700000012C2D7070005CBF1D60F0F0FC3FCFCD"); } else if (x == "off") { id(tx).send_hex("0201021BFF114D191AF0CF2D700000012C2D7670005CBF1D60F0F0FC3FCFC7|0201021BFF114D191AF0CF2D700000012C2D7070005CBF1D60F0F0FC3FCFCD"); } else if (x.find("on#") == 0) { std::string rest = x.substr(3); int v = atoi(rest.c_str()); size_t p2 = rest.find('#'); if (p2 != std::string::npos) { int k = atoi(rest.substr(p2 + 1).c_str()); if (k <= 3100) { id(tx).send_hex("0201021BFF114D191AF0CF2D700000012C2C2570005CBF1D60F0F0FC3FCF97|0201021BFF114D1918F0CF2D70000001CE9D2D2D7070005CBF1D60F0F0FC3D"); } else if (k <= 5000) { id(tx).send_hex("0201021BFF114D1912F0CF2D7000000161F1A5893FCFCF9D2D2D7070005C72|0201021BFF114D1914F0CF2D70000001F1FC3FCFCF9D2D2D7070005CBF1D62"); } else { id(tx).send_hex("0201021BFF114D1912F0CF2D7000000161F1A5033FCFCF9D2D2D7070005CE8|0201021BFF114D191AF0CF2D700000012C2D7070005CBF1D60F0F0FC3FCFCD"); } } if (v <= 10) { id(tx).send_hex("0201021BFF114D191DF0CF2D7000000171010DBD1D60F0F0FC3FCFCF9D2D7B|0201021BFF114D191EF0CF2D70000001015CBF1D60F0F0FC3FCFCF9D2D2D72"); } else if (v <= 30) { id(tx).send_hex("0201021BFF114D191CF0CF2D7000000171715169BF1D60F0F0FC3FCFCF9DA4|0201021BFF114D191CF0CF2D700000017170005CBF1D60F0F0FC3FCFCF9D2F"); } else if (v <= 45) { id(tx).send_hex("0201021BFF114D191BF0CF2D700000012C7121685CBF1D60F0F0FC3FCFCF21|0201021BFF114D191CF0CF2D700000017170005CBF1D60F0F0FC3FCFCF9D2F"); } else if (v <= 55) { id(tx).send_hex("0201021BFF114D1910F0CF2D70000001BE1C318FF0FC3FCFCF9D2D2D7070D3|0201021BFF114D1914F0CF2D70000001F1FC3FCFCF9D2D2D7070005CBF1D62"); } else if (v <= 70) { id(tx).send_hex("0201021BFF114D191EF0CF2D70000001015DEE8B60F0F0FC3FCFCF9D2D2D9A|0201021BFF114D1910F0CF2D70000001BE1D60F0F0FC3FCFCF9D2D2D707002"); } else if (v <= 90) { id(tx).send_hex("0201021BFF114D1913F0CF2D70000001F1F1ADF6CFCF9D2D2D7070005CBF00|0201021BFF114D191DF0CF2D7000000171005CBF1D60F0F0FC3FCFCF9D2D2F"); } else { id(tx).send_hex("0201021BFF114D191FF0CF2D700000015DBE4C9FF0F0FC3FCFCF9D2D2D7023|0201021BFF114D1915F0CF2D70000001FD3FCFCF9D2D2D7070005CBF1D60F2"); } } # ========================================================== # 次卧风扇 (cwfan003) # ========================================================== - topic: "cwfan003" then: - lambda: |- if (x == "on") { id(tx).send_hex("0201021BFF114D1911F0CF2D700000011C60F9F0FC3FCFCF9D2D2D70700057|0201021BFF114D191DF0CF2D7000000171005CBF1D60F0F0FC3FCFCF9D2D2F"); } else if (x == "off") { id(tx).send_hex("0201021BFF114D191BF0CF2D700000012C7079005CBF1D60F0F0FC3FCFCF96|0201021BFF114D1918F0CF2D70000001CE9D2D2D7070005CBF1D60F0F0FC3D"); } else if (x.find("on#") == 0) { int s = atoi(x.substr(3).c_str()); ESP_LOGW("CMD", "cwfan speed=%d", s); if (s == 1) { id(tx).send_hex("0201021BFF114D1915F0CF2D70000001FD3FD5CF9D2D2D7070005CBF1D60EC|0201021BFF114D1915F0CF2D70000001FD3FCFCF9D2D2D7070005CBF1D60F2"); } else if (s == 2) { id(tx).send_hex("0201021BFF114D191BF0CF2D700000012C70F8005CBF1D60F0F0FC3FCFCF17|0201021BFF114D1913F0CF2D70000001F1F0FC3FCFCF9D2D2D7070005CBF1F"); } else if (s == 3) { id(tx).send_hex("0201021BFF114D1917F0CF2D70000001CECF182D2D7070005CBF1D60F0F07B|0201021BFF114D1919F0CF2D700000019C2D2D7070005CBF1D60F0F0FC3FCD"); } else if (s == 4) { id(tx).send_hex("0201021BFF114D191BF0CF2D700000012C70F6005CBF1D60F0F0FC3FCFCF15|0201021BFF114D1917F0CF2D70000001CECF9D2D2D7070005CBF1D60F0F0FE"); } }# ===================== BLE 扫描与状态上报 =====================esp32_ble_tracker: scan_parameters: interval: 640ms window: 30ms active: false on_ble_advertise: - then: - lambda: |- auto datas = x.get_manufacturer_datas(); for (auto &data : datas) { auto &raw = data.data; if (raw.size() < 20) continue; // ===== Dining 餐厅设备 (48:80:27:70:00:00) ===== uint8_t ct_mac[6] = {0x48, 0x80, 0x27, 0x70, 0x00, 0x00}; for (int i = 2; i <= (int)raw.size() - 6; i++) { if (memcmp(&raw, ct_mac, 6) == 0) { bool light_on = (raw[i + 7] == 0x01); int brt = (int)(raw[i + 12] / 255.0f * 100.0f); int ct_k = 2700 + (6500 - 2700) * (int)(raw[i + 13] / 255.0f * 100.0f) / 100; uint8_t fan_state = raw[i + 16]; bool fan_run = (fan_state & 0x01) != 0; int fan_spd = fan_run ? (raw[i + 17] + 1) : 0; static bool last_light = false; static int last_brt = -1; static int last_ct = -1; static bool last_fan_run = false; static int last_fan_spd = -1; static uint32_t last_report = 0; static uint32_t last_change = 0; static uint8_t retry_count = 0; bool changed = (light_on != last_light) || (brt != last_brt) || (ct_k != last_ct) || (fan_run != last_fan_run) || (fan_spd != last_fan_spd); if (changed) { last_change = millis(); retry_count = 0; last_light = light_on; last_brt = brt; last_ct = ct_k; last_fan_run = fan_run; last_fan_spd = fan_spd; } bool heartbeat = (millis() - last_report > 60000); bool retry = (millis() - last_change < 10000) && (retry_count < 3) && (millis() - last_report > 3000); if (changed || heartbeat || retry) { if (retry) retry_count++; else retry_count = 0; last_report = millis(); const char* reason = changed ? "CHANGE" : (retry ? "RETRY" : "HEARTBEAT"); ESP_LOGI("DINING", "[%s] Light:%d Brt:%d CT:%dK Fan:%d Spd:%d", reason, light_on, brt, ct_k, fan_run, fan_spd); if (light_on) { id(bemfa).publish("ctlight002/up", "on#" + to_string(brt) + "#" + to_string(ct_k)); } else { id(bemfa).publish("ctlight002/up", "off"); } if (fan_run) { id(bemfa).publish("ctfan003/up", "on#" + to_string(fan_spd)); } else { id(bemfa).publish("ctfan003/up", "off"); } id(ct_light_state).publish_state(light_on); id(ct_light_brt).publish_state(brt); id(ct_light_ct).publish_state(ct_k); id(ct_fan_state).publish_state(fan_run); id(ct_fan_spd).publish_state(fan_spd); // 🔥 同步开关状态 id(ct_light_sw).publish_state(light_on); id(ct_fan_sw).publish_state(fan_run); } break; } } // ===== Bedroom 次卧设备 (F0:CF:2D:70:00:00) ===== uint8_t cw_mac[6] = {0xF0, 0xCF, 0x2D, 0x70, 0x00, 0x00}; for (int i = 2; i <= (int)raw.size() - 6; i++) { if (memcmp(&raw, cw_mac, 6) == 0) { bool light_on = (raw[i + 7] == 0x01); int brt = (int)(raw[i + 12] / 255.0f * 100.0f); int ct_k = 2700 + (6500 - 2700) * (int)(raw[i + 13] / 255.0f * 100.0f) / 100; uint8_t fan_state = raw[i + 16]; bool fan_run = (fan_state & 0x01) != 0; int fan_spd = fan_run ? (raw[i + 17] + 1) : 0; static bool last_light = false; static int last_brt = -1; static int last_ct = -1; static bool last_fan_run = false; static int last_fan_spd = -1; static uint32_t last_report = 0; static uint32_t last_change = 0; static uint8_t retry_count = 0; bool changed = (light_on != last_light) || (brt != last_brt) || (ct_k != last_ct) || (fan_run != last_fan_run) || (fan_spd != last_fan_spd); if (changed) { last_change = millis(); retry_count = 0; last_light = light_on; last_brt = brt; last_ct = ct_k; last_fan_run = fan_run; last_fan_spd = fan_spd; } bool heartbeat = (millis() - last_report > 60000); bool retry = (millis() - last_change < 10000) && (retry_count < 3) && (millis() - last_report > 3000); if (changed || heartbeat || retry) { if (retry) retry_count++; else retry_count = 0; last_report = millis(); const char* reason = changed ? "CHANGE" : (retry ? "RETRY" : "HEARTBEAT"); ESP_LOGI("BEDROOM", "[%s] Light:%d Brt:%d CT:%dK Fan:%d Spd:%d", reason, light_on, brt, ct_k, fan_run, fan_spd); if (light_on) { id(bemfa).publish("cwlight002/up", "on#" + to_string(brt) + "#" + to_string(ct_k)); } else { id(bemfa).publish("cwlight002/up", "off"); } if (fan_run) { id(bemfa).publish("cwfan003/up", "on#" + to_string(fan_spd)); } else { id(bemfa).publish("cwfan003/up", "off"); } id(cw_light_state).publish_state(light_on); id(cw_light_brt).publish_state(brt); id(cw_light_ct).publish_state(ct_k); id(cw_fan_state).publish_state(fan_run); id(cw_fan_spd).publish_state(fan_spd); // 🔥 同步开关状态 id(cw_light_sw).publish_state(light_on); id(cw_fan_sw).publish_state(fan_run); } break; } } }# ===================== 开关(支持状态反馈) =====================switch: # === 餐厅灯 === - platform: template name: "Dining Light 餐厅灯" id: ct_light_sw icon: mdi:ceiling-light optimistic: false restore_mode: RESTORE_DEFAULT_OFF turn_on_action: - lambda: 'id(tx).send_hex("0201021BFF114D191848802770000001819721277070005FC86FB84848A7F8|0201021BFF114D191848802770000001819727277070005FC86FB84848A7F2");' turn_off_action: - lambda: 'id(tx).send_hex("0201021BFF114D191A4880277000000126277670005FC86FB84848A7F08088|0201021BFF114D19174880277000000181809727277070005FC86FB84848A5");' lambda: |- return id(ct_light_state).state; # === 餐厅风扇 === - platform: template name: "Dining Fan 餐厅风扇" id: ct_fan_sw icon: mdi:fan optimistic: false restore_mode: RESTORE_DEFAULT_OFF turn_on_action: - lambda: 'id(tx).send_hex("0201021BFF114D19184880277000000181972E277070005FC86FB84848A7FB|0201021BFF114D191F488027700000015EC86FB84848A7F080809727277072");' turn_off_action: - lambda: 'id(tx).send_hex("0201021BFF114D191648802770000001F180899727277070005FC86FB84843|0201021BFF114D1919488027700000019627277070005FC86FB84848A7F082");' lambda: |- return id(ct_fan_state).state; # === 次卧灯 === - platform: template name: "Bedroom Light 次卧灯" id: cw_light_sw icon: mdi:ceiling-light optimistic: false restore_mode: RESTORE_DEFAULT_OFF turn_on_action: - lambda: 'id(tx).send_hex("0201021BFF114D1917F0CF2D70000001CECF9B2D2D7070005CBF1D60F0F0F4|0201021BFF114D191AF0CF2D700000012C2D7070005CBF1D60F0F0FC3FCFCD");' turn_off_action: - lambda: 'id(tx).send_hex("0201021BFF114D191AF0CF2D700000012C2D7670005CBF1D60F0F0FC3FCFC7|0201021BFF114D191AF0CF2D700000012C2D7070005CBF1D60F0F0FC3FCFCD");' lambda: |- return id(cw_light_state).state; # === 次卧风扇 === - platform: template name: "Bedroom Fan 次卧风扇" id: cw_fan_sw icon: mdi:fan optimistic: false restore_mode: RESTORE_DEFAULT_OFF turn_on_action: - lambda: 'id(tx).send_hex("0201021BFF114D1911F0CF2D700000011C60F9F0FC3FCFCF9D2D2D70700057|0201021BFF114D191DF0CF2D7000000171005CBF1D60F0F0FC3FCFCF9D2D2F");' turn_off_action: - lambda: 'id(tx).send_hex("0201021BFF114D191BF0CF2D700000012C7079005CBF1D60F0F0FC3FCFCF96|0201021BFF114D1918F0CF2D70000001CE9D2D2D7070005CBF1D60F0F0FC3D");' lambda: |- return id(cw_fan_state).state;# ===================== 按钮(风扇档位) =====================button: - platform: restart name: "Restart 重启网关" icon: mdi:restart entity_category: diagnostic # === 餐厅风扇档位 === - platform: template name: "Dining Fan 1 餐厅风扇1档" icon: mdi:numeric-1-circle entity_category: config on_press: - lambda: 'id(tx).send_hex("0201021BFF114D191048802770000001C96FA24848A7F0808097272770701C|0201021BFF114D191548802770000001A6F080809727277070005FC86FB84A");' - platform: template name: "Dining Fan 2 餐厅风扇2档" icon: mdi:numeric-2-circle entity_category: config on_press: - lambda: 'id(tx).send_hex("0201021BFF114D1918488027700000018197AF277070005FC86FB84848A77A|0201021BFF114D191C488027700000017170005FC86FB84848A7F080809725");' - platform: template name: "Dining Fan 3 餐厅风扇3档" icon: mdi:numeric-3-circle entity_category: config on_press: - lambda: 'id(tx).send_hex("0201021BFF114D191A488027700000012627F570005FC86FB84848A7F08007|0201021BFF114D191C488027700000017170005FC86FB84848A7F080809725");' - platform: template name: "Dining Fan 4 餐厅风扇4档" icon: mdi:numeric-4-circle entity_category: config on_press: - lambda: 'id(tx).send_hex("0201021BFF114D191548802770000001A6F080809727277070005FC86FB84A|0201021BFF114D1911488027700000016EB8CE48A7F08080972727707000D7");' # === 次卧风扇档位 === - platform: template name: "Bedroom Fan 1 次卧风扇1档" icon: mdi:numeric-1-circle entity_category: config on_press: - lambda: 'id(tx).send_hex("0201021BFF114D1915F0CF2D70000001FD3FD5CF9D2D2D7070005CBF1D60EC|0201021BFF114D1915F0CF2D70000001FD3FCFCF9D2D2D7070005CBF1D60F2");' - platform: template name: "Bedroom Fan 2 次卧风扇2档" icon: mdi:numeric-2-circle entity_category: config on_press: - lambda: 'id(tx).send_hex("0201021BFF114D191BF0CF2D700000012C70F8005CBF1D60F0F0FC3FCFCF17|0201021BFF114D1913F0CF2D70000001F1F0FC3FCFCF9D2D2D7070005CBF1F");' - platform: template name: "Bedroom Fan 3 次卧风扇3档" icon: mdi:numeric-3-circle entity_category: config on_press: - lambda: 'id(tx).send_hex("0201021BFF114D1917F0CF2D70000001CECF182D2D7070005CBF1D60F0F07B|0201021BFF114D1919F0CF2D700000019C2D2D7070005CBF1D60F0F0FC3FCD");' - platform: template name: "Bedroom Fan 4 次卧风扇4档" icon: mdi:numeric-4-circle entity_category: config on_press: - lambda: 'id(tx).send_hex("0201021BFF114D191BF0CF2D700000012C70F6005CBF1D60F0F0FC3FCFCF15|0201021BFF114D1917F0CF2D70000001CECF9D2D2D7070005CBF1D60F0F0FE");'# ===================== 传感器 =====================sensor: # === 餐厅灯 === - platform: template name: "Dining Brt 餐厅灯亮度" id: ct_light_brt unit_of_measurement: "%" icon: mdi:brightness-percent accuracy_decimals: 0 - platform: template name: "Dining CT 餐厅灯色温" id: ct_light_ct unit_of_measurement: "K" icon: mdi:thermometer # === 餐厅风扇 === - platform: template name: "Dining Spd 餐厅风扇档位" id: ct_fan_spd icon: mdi:fan-speed-1 # === 次卧灯 === - platform: template name: "Bedroom Brt 次卧灯亮度" id: cw_light_brt unit_of_measurement: "%" icon: mdi:brightness-percent accuracy_decimals: 0 - platform: template name: "Bedroom CT 次卧灯色温" id: cw_light_ct unit_of_measurement: "K" icon: mdi:thermometer # === 次卧风扇 === - platform: template name: "Bedroom Spd 次卧风扇档位" id: cw_fan_spd icon: mdi:fan-speed-1 # === 系统状态 === - platform: uptime name: "Gateway Uptime 运行时间" id: gateway_uptime update_interval: 60s icon: mdi:timer-outline - platform: internal_temperature name: "ESP32 Temp 芯片温度" unit_of_measurement: "°C" accuracy_decimals: 1 update_interval: 60s icon: mdi:thermometer - platform: wifi_signal name: "WiFi dBm 信号强度" id: wifi_signal_db update_interval: 60s icon: mdi:wifi - platform: copy source_id: wifi_signal_db name: "WiFi Pct 信号百分比" filters: - lambda: return min(max(2 * (x + 100.0), 0.0), 100.0); unit_of_measurement: "%" accuracy_decimals: 0 icon: mdi:wifi-strength-4# ===================== 二进制传感器 =====================binary_sensor: - platform: status name: "Gateway Status 网关状态" device_class: connectivity - platform: template name: "Dining Light State 餐厅灯状态" id: ct_light_state device_class: light - platform: template name: "Bedroom Light State 次卧灯状态" id: cw_light_state device_class: light - platform: template name: "Dining Fan State 餐厅风扇状态" id: ct_fan_state device_class: running - platform: template name: "Bedroom Fan State 次卧风扇状态" id: cw_fan_state device_class: running# ===================== 文本传感器 =====================text_sensor: - platform: template name: "Build Time 编译时间" lambda: 'return {"${build_time}"};' icon: mdi:calendar-clock update_interval: 3600s - platform: version name: "ESPHome Ver 版本" hide_timestamp: true - platform: wifi_info ip_address: name: "ESP IP 地址" icon: mdi:ip ssid: name: "ESP SSID WiFi名" icon: mdi:wifi mac_address: name: "ESP MAC 物理地址"
🙏 结语 这个项目让我深刻体会到了 ESPHome 的强大,也明白了在物联网开发中,“以物理设备的真实状态为唯一真理”是多么重要。
代码已经过长时间的压力测试,非常稳定。如果你也有类似的蓝牙风扇灯需要接入 HA,欢迎在评论区交流,我会尽力解答!
如果觉得有用,请点个赞支持一下开源分享!👍