找回密码
 立即注册

微信扫码登录

搜索
查看: 766|回复: 6

[基础教程] 美的风扇完美接入巴法和HA

[复制链接]

1

主题

21

回帖

224

积分

中级会员

积分
224
金钱
202
HASS币
0
发表于 2026-8-2 10:59:29 | 显示全部楼层 |阅读模式
【开源分享】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,欢迎在评论区交流,我会尽力解答!

如果觉得有用,请点个赞支持一下开源分享!👍






回复

使用道具 举报

1

主题

21

回帖

224

积分

中级会员

积分
224
金钱
202
HASS币
0
 楼主| 发表于 2026-8-2 11:05:48 | 显示全部楼层
目前实现的功能
1 巴法直连  无HA 也能用 状态反馈自动更新

                               
登录/注册后可看大图

2.ha 直连 方便自动化

                               
登录/注册后可看大图


                               
登录/注册后可看大图


3.可定制化 可以自定义发送指定代码
回复

使用道具 举报

1

主题

21

回帖

224

积分

中级会员

积分
224
金钱
202
HASS币
0
 楼主| 发表于 2026-8-2 11:09:52 | 显示全部楼层
直接git上编译即可
需要自己抓包  
下载固件

                               
登录/注册后可看大图

esp32-wroom-32e-2-main.zip

8.94 KB, 下载次数: 2

回复

使用道具 举报

9

主题

101

回帖

715

积分

高级会员

积分
715
金钱
605
HASS币
0
发表于 2026-8-3 08:00:05 来自手机 | 显示全部楼层
感谢大佬,我是昨晚才搞定的,正准备分享出来了,跟你同款风扇灯,之前搞了很久的checksum,一直没搞定,昨天让google genmi搞了下,最终给我解密出来了,搞定了亮度、色温,正反转,转速档位,与你不同的是你用的是固定的广播码
回复

使用道具 举报

1

主题

21

回帖

224

积分

中级会员

积分
224
金钱
202
HASS币
0
 楼主| 发表于 2026-8-3 23:44:06 | 显示全部楼层
xhack123 发表于 2026-8-3 08:00
感谢大佬,我是昨晚才搞定的,正准备分享出来了,跟你同款风扇灯,之前搞了很久的checksum,一直没搞定,昨 ...

是的  你解密了么 我搞了好久都没有解密
能解密分享下么
回复

使用道具 举报

9

主题

101

回帖

715

积分

高级会员

积分
715
金钱
605
HASS币
0
发表于 2026-8-5 21:55:52 来自手机 | 显示全部楼层
wgchen 发表于 2026-8-3 23:44
是的  你解密了么 我搞了好久都没有解密
能解密分享下么

https://bbs.hassbian.com/thread-32881-1-1.html 我分享出来了,广播码你可以参考我另外的一个帖子
回复

使用道具 举报

1

主题

21

回帖

224

积分

中级会员

积分
224
金钱
202
HASS币
0
 楼主| 发表于 2026-8-7 20:02:05 | 显示全部楼层
xhack123 发表于 2026-8-5 21:55
https://bbs.hassbian.com/thread-32881-1-1.html 我分享出来了,广播码你可以参考我另外的一个帖子 ...

感谢 我也让GPT 解析出来   感谢您做出的贡献
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

Archiver|手机版|小黑屋|Hassbian ( 晋ICP备17001384号-1 )|网站地图

GMT+8, 2026-8-20 05:33 , Processed in 0.014744 second(s), 4 queries , Redis On.

Powered by Discuz! X3.5

© 2001-2026 Discuz! Team.

快速回复 返回顶部 返回列表