ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

ESP32-C3蓝牙网关实战:小米温湿度计接入Home Assistant

ESP32-C3蓝牙网关实战:小米温湿度计接入Home Assistant 用一块ESP32-C3做蓝牙网关把小米温湿度计的数字通过MQTT自动送进Home Assistant再把完整代码贴出来大概是这个项目最吸引人的地方。我折腾了两周现在它在家里7x24小时跑着稳定得很。整个过程不依赖米家云手机断网也不影响本地自动化适合手里正好有ESP32-C3开发板、又想把米家传感器拉进Home Assistant生态的玩家参考。如果你只是想偶尔看一眼温度直接用米家App就够了但想要温度数据参与联动报警、历史图表、自动化规则就必须让HA能拿到数据。小米温湿度计本身只有蓝牙广播没有Wi-Fi所以需要一个“翻译官”ESP32-C3监听传感器广播包解析出温度、湿度和电池电量再通过MQTT协议送给HA。这个项目的核心就是用几百行代码实现一个轻量级蓝牙网关。1. 项目逻辑梳理先搞清楚数据怎么走1.1 为什么需要一块独立的BLE网关小米温湿度计常见型号是LYWSD03MMC它工作在低功耗蓝牙广播模式下不会主动连接任何设备只是每隔几秒向外喊一句话内容包含温度、湿度、电池状态。这种设计省电一颗纽扣电池能用大半年但缺点也很明显普通电脑的蓝牙往往接收距离短、稳定性差而且需要额外软件才能解析广播长期挂机不太现实。HA主机如果是树莓派或者NUC内置蓝牙也能直连传感器但蓝牙天线位置受限距离一远就丢包家里多几堵墙基本就断断续续。用ESP32-C3做独立网关可以把这个“收音机”放到离传感器最近的地方再通过Wi-Fi把数据传出去信号覆盖更灵活。与此同时C3开发板价格只有十几到二十几块功耗也比一台树莓派低得多适合做这种常驻型小网关。1.2 数据链路全流程整个链路可以用一句话概括小米温湿度计发出BLE广播ESP32-C3扫描到广播后解析出数值再通过MQTT发布到TopicHome Assistant订阅这些Topic并生成实体最终在面板上显示。拆成节点来看是这样传感器节点LYWSD03MMC按照自己的广播周期通常2~10秒发出蓝牙广播包。网关节点ESP32-C3开启BLE扫描捕获空气中的广播包过滤出小米设备再解析出温度、湿度、电量。消息节点网关把数据打包成JSON通过MQTT发布到home/传感器名/state这类Topic。平台节点Home Assistant通过MQTT集成订阅Topic自动创建出三个实体温度、湿度、电量。展示节点把实体添加到仪表盘就能看到实时数据和历史曲线。这个链路里最容易踩坑的是第一步和第二步也就是“传感器广播是否明文”以及“网关能否正确解析”。后面会单独展开。1.3 为什么选择ESP32-C3而不是其他方案有人会问用ESP8266能不能做ESP8266没有BLE硬件只能靠外挂模块通信链路复杂不太推荐。用树莓派做又显得大材小用。ESP32-C3是乐鑫推出的高性价比芯片单核RISC-V、支持Wi-Fi和BLE 5.0最重要是Arduino生态成熟开发门槛低国内各种开发板选择很多社区资料丰富。而且C3的BLE接收灵敏度不错在室内隔一堵墙的距离实测也能稳定捕获传感器广播。再加上它自带Wi-Fi不需要额外配网模块一个芯片就能同时搞定蓝牙和网络两个任务。虽然同系列ESP32经典款也能做但C3功耗更低、价格便宜、体积更小做网关更合适。2. 硬件选型与准备2.1 开发板怎么选市面上常见的ESP32-C3开发板有两类一类是合宙ESP32-C3板子很小、引脚引出较少适合模块化嵌入另一类是带排针的全功能开发板比如ESP32-C3-DevKitM-1方便直接插面包板调试。我的建议如果只是做网关放在插座旁边选哪种都可以关键是看供电方式和串口芯片。开发板一般带USB口插上充电头或者路由器USB口就能长期供电。需要注意有些板子的USB转串口芯片是CH340有些是CP2102两者都需要安装驱动。买板子的时候先问清型号避免到手后电脑识别不了。温度计本身不需要改硬件LYWSD03MMC是纽扣电池供电的蓝牙设备买来就能用。不过要注意的是我下面这套方案默认传感器已经刷成第三方固件也就是ATC或PVVX固件具体原因见第3章。2.2 开发环境搭建我用的是Arduino IDE开发环境简单直接。安装和配置分三步第一步安装Arduino IDE建议2.x版本界面更干净库管理也方便。第二步安装ESP32开发板支持。在“文件 - 首选项 - 附加开发板管理器地址”里填入乐鑫官方JSON地址https://espressif.github.io/arduino-esp32/package_esp32_index.json。然后在“开发板管理器”中搜索ESP32安装2.0.x版本。我这里实测用的是2.0.17稳定没有遇到编译问题。第三步在库管理器里安装PubSubClient和ArduinoJson。前者负责MQTT通信后者负责生成和解析JSON配置。这两个库都很常用直接搜索安装即可。如果不想用Arduino IDE也可以换成PlatformIO在platformio.ini里写[env:esp32-c3-devkitm-1] platform espressif32 board esp32-c3-devkitm-1 framework arduino lib_deps knolleary/PubSubClient^2.8 bblanchon/ArduinoJson^6.21.5两种方式编译出来的固件效果一样看个人使用习惯。2.3 硬件接线与供电这个项目几乎不需要接线USB插上就能跑。唯一要注意供电稳定性ESP32-C3在Wi-Fi和BLE同时启动时瞬时电流可能到200mA以上建议用5V/1A以上的充电头不要用电脑的USB口长期供电插拔容易断。如果要把温度计单独放远处网关放在中间位置即可。C3的蓝牙接收半径实测大概在10~15米环境越空旷越好。传感器和网关之间尽量避免厚重墙体遮挡。3. 关键一步让小米温湿度计输出明文广播3.1 原厂广播是加密的直接解析行不通小米温湿度计出厂时运行的是米家固件广播内容是经过加密的第三方设备包括ESP32只能看到一堆密文解不出温度湿度。米家App能显示数据是因为它在配对时和传感器协商了密钥并且使用随机MAC地址这给本地解析带来了不小麻烦。网上有一些方案可以在ESP32里实现MiBeacon解密但需要提前从米家App里提取绑定密钥流程繁琐而且传感器会定期更换随机地址维护成本很高。所以我的建议是别在原厂固件上硬刚直接给传感器刷第三方固件。3.2 ATC固件与PVVX固件的区别第三方固件里最常用的是ATC和PVVX两个版本。它们共同点是关闭广播加密把温度、湿度、电量明文打进广播包里且支持更高的广播频率默认大约2秒一次非常适合网关解析。两种固件的差别主要在于广播包结构和一些附加功能。我使用的是ATC固件它的广播包以43 4D 54开头也就是ASCII字符“CMT”解析起来非常方便。PVVX固件也有类似设计但偏移量略有差异。刷机方法很简单在手机上用Chrome或Edge浏览器打开ATC固件在线刷写页面靠近传感器浏览器会通过Web Bluetooth读取到设备并烧录。整个过程不需要拆机大概几十秒完成。刷完后传感器在米家App里会失效也就是说它再也回不了米家智能联动这是需要提前接受的一点。3.3 刷机后的确认刷机后可以先用“BLE调试助手”之类的手机App扫描一下如果看到设备名称像是LYWSD03MMC或者广播数据里有一段43 4D 54开头的内容就说明刷机成功了。后面ESP32-C3解析的正是这串数据。这里特别提醒一句如果你手头的传感器已经接入米家并绑定了账号需要先彻底解绑再刷第三方固件否则浏览器可能连接不上设备。我一开始没解绑反复扫描都找不到亏得重启了好几次才反应过来。4. ESP32-C3完整代码与逐段解析4.1 代码运行的总体思路代码主要分为四块Wi-Fi连接、MQTT连接、BLE扫描解析、数据发布。启动后先连Wi-Fi和MQTT然后开启BLE扫描后台不断监听传感器广播。每次解析出新数据就存到全局变量主循环定时把数据推送到HA。我采用的是Arduino框架自带的BLE库接口在ESP32-C3上兼容性很好。下面给出的代码是完整版可以直接复制到Arduino IDE里使用。#include WiFi.h #include PubSubClient.h #include ArduinoJson.h #include BLEDevice.h #include BLEScan.h #include BLEAdvertisedDevice.h // 需要修改的配置 const char* WIFI_SSID your_wifi_ssid; const char* WIFI_PASS your_wifi_password; const char* MQTT_HOST 192.168.1.100; // HA的IP const uint16_t MQTT_PORT 1883; const char* MQTT_USER ; // 如果MQTT没开认证就留空 const char* MQTT_PASS ; // 传感器MAC地址留空则自动抓取第一个名称匹配的设备 const char* SENSOR_MAC a4:c1:38:xx:xx:xx; const char* SENSOR_NAME livingroom; // 用于HA实体命名建议用英文 // 全局状态 float temperature 0; float humidity 0; int batteryPercent 0; bool dataUpdated false; unsigned long lastPublish 0; const unsigned long PUBLISH_INTERVAL 30000; // 30秒发布一次可按需调整 WiFiClient wifiClient; PubSubClient mqttClient(wifiClient); class ScanCallbacks : public BLEAdvertisedDeviceCallbacks { void onResult(BLEAdvertisedDevice advertisedDevice) override { // 用MAC过滤时只解析指定设备 if (strlen(SENSOR_MAC) 0) { String addr advertisedDevice.getAddress().toString().c_str(); addr.toLowerCase(); if (addr ! SENSOR_MAC) return; } else { // 没有MAC时根据设备名过滤兼容随机地址设备 std::string devName advertisedDevice.getName(); if (devName.find(LYWSD03) std::string::npos devName.find(ATC) std::string::npos) return; } // 取广播中的厂家数据ATC固件以 CMT 开头 std::string mfg advertisedDevice.getManufacturerData(); if (mfg.length() 14) return; const uint8_t* d (const uint8_t*)mfg.data(); if (d[0] ! 0x43 || d[1] ! 0x4D || d[2] ! 0x54) return; // CMT // ATC固件数据格式温度小端有符号除以10湿度小端除以10 int16_t tempRaw (int16_t)((d[7] 8) | d[6]); uint16_t humRaw (d[9] 8) | d[8]; uint16_t battMv (d[11] 8) | d[10]; uint8_t battPct d[12]; temperature tempRaw / 10.0f; humidity humRaw / 10.0f; batteryPercent battPct; dataUpdated true; Serial.printf(Temp: %.1f C, Hum: %.1f %%, Batt: %d%% (%dmV)\n, temperature, humidity, batteryPercent, battMv); } }; ScanCallbacks scanCallbacks; void connectWiFi() { WiFi.mode(WIFI_STA); WiFi.begin(WIFI_SSID, WIFI_PASS); Serial.print(Connecting WiFi); while (WiFi.status() ! WL_CONNECTED) { delay(500); Serial.print(.); } Serial.println( OK); } void connectMQTT() { int retryCount 0; while (!mqttClient.connected() retryCount 20) { Serial.print(Connecting MQTT...); if (mqttClient.connect(ESP32C3_GW, MQTT_USER, MQTT_PASS)) { Serial.println( OK); return; } else { Serial.print( failed, rc); Serial.println(mqttClient.state()); retryCount; delay(2000); } } } void publishDiscovery() { String baseTopic String(homeassistant/sensor/) SENSOR_NAME _; String tempConfig String({\name\:\) SENSOR_NAME _temperature\, \device_class\:\temperature\, \state_topic\:\home/ SENSOR_NAME /state\, \value_template\:\{{ value_json.temperature }}\, \unit_of_measurement\:\°C\, \unique_id\:\ SENSOR_NAME _temperature\}; mqttClient.publish((baseTopic temperature/config).c_str(), tempConfig.c_str()); String humConfig String({\name\:\) SENSOR_NAME _humidity\, \device_class\:\humidity\, \state_topic\:\home/ SENSOR_NAME /state\, \value_template\:\{{ value_json.humidity }}\, \unit_of_measurement\:\%\, \unique_id\:\ SENSOR_NAME _humidity\}; mqttClient.publish((baseTopic humidity/config).c_str(), humConfig.c_str()); String battConfig String({\name\:\) SENSOR_NAME _battery\, \device_class\:\battery\, \state_topic\:\home/ SENSOR_NAME /state\, \value_template\:\{{ value_json.battery }}\, \unit_of_measurement\:\%\, \unique_id\:\ SENSOR_NAME _battery\}; mqttClient.publish((baseTopic battery/config).c_str(), battConfig.c_str()); } void publishState() { DynamicJsonDocument doc(128); doc[temperature] temperature; doc[humidity] humidity; doc[battery] batteryPercent; char buffer[128]; serializeJson(doc, buffer); String topic String(home/) SENSOR_NAME /state; mqttClient.publish(topic.c_str(), buffer); Serial.println(State published); } void setup() { Serial.begin(115200); connectWiFi(); mqttClient.setServer(MQTT_HOST, MQTT_PORT); connectMQTT(); publishDiscovery(); BLEDevice::init(ESP32C3_GW); BLEScan* pBLEScan BLEDevice::getScan(); pBLEScan-setAdvertisedDeviceCallbacks(scanCallbacks, false); pBLEScan-setActiveScan(true); pBLEScan-setInterval(100); pBLEScan-setWindow(99); pBLEScan-start(0); // 持续扫描不阻塞主循环 } void loop() { if (!mqttClient.connected()) { connectMQTT(); } mqttClient.loop(); unsigned long now millis(); if (dataUpdated (now - lastPublish PUBLISH_INTERVAL)) { publishState(); dataUpdated false; lastPublish now; } }4.2 代码里的关键细节pBLEScan-start(0)这一行是整个程序的核心它让BLE扫描始终运行。不同版本的ESP32 Arduino库对start(0)的行为略有差异如果你发现主循环被卡住或者MQTT数据发不出去可以把这种持续扫描改成周期扫描也就是每10秒调用一次stop()然后start(5, false)让扫描让出CPU时间。数据解析部分最关键的是温度的正负号处理。ATC固件里温度是带符号的16位整数低位在前所以代码中用int16_t接收(d[7] 8) | d[6]再除以10。如果直接声明成uint16_t冬天零下温度就会显示成65535之类的异常值。湿度没有负数直接用无符号处理。发布频率我设的是30秒一次。传感器本身广播间隔大概2秒数据其实变化不快30秒足够。如果你要做实时性要求高的联动可以改成5秒甚至10秒但要注意MQTT消息量和HA端数据库写入速度。4.3 为什么代码里包含自动发现Home Assistant的MQTT集成默认开启自动发现只要设备向homeassistant/sensor/xxx/config这类主题发布一段标准JSON配置HA就会自动创建实体不需要手动写YAML、重启HA。代码里的publishDiscovery()就是干这个事。这种方式的优势非常明显网关代码自描述烧录后不用改HA配置自动就能出现传感器卡片。如果自动发现偶尔失败比如HA晚于网关启动最简单的方法是重启一次网关它会重新发布配置。5. Home Assistant接入MQTT与自动发现5.1 在HA里部署MQTT BrokerHA侧首先要有一个MQTT服务器。如果你用的是Home Assistant OS直接在加载项商店里搜索Mosquitto broker并安装然后创建一个用户。记住用户名密码填入ESP32代码的MQTT_USER和MQTT_PASS。如果和我一样在Docker里跑HA也可以用官方的Mosquitto容器配置一次性后面基本不用管。MQTT端口默认1883HA所在的IP地址就是代码里的MQTT_HOST。5.2 自动发现的工作流程代码烧录后ESP32-C3一开机就会发布三条自动发现配置温度、湿度、电池。HA收到配置后自动生成三个实体实体名会带上SENSOR_NAME作为前缀方便区分不同房间。如果你想手动配置也可以不依赖自动发现直接修改HA的configuration.yamlmqtt: sensor: - name: 客厅温度 state_topic: home/livingroom/state value_template: {{ value_json.temperature }} unit_of_measurement: °C device_class: temperature - name: 客厅湿度 state_topic: home/livingroom/state value_template: {{ value_json.humidity }} unit_of_measurement: % device_class: humidity - name: 客厅温湿度计电量 state_topic: home/livingroom/state value_template: {{ value_json.battery }} unit_of_measurement: % device_class: battery手动方式最大的好处是调试时能直观看见解析过程不容易被自动发现的报错绕晕。5.3 用仪表盘展示数据实体创建后去HA“仪表盘”里添加“传感器卡片”选出刚才生成的三个实体。一个卡片可以同时显示温度和湿度电池电量可以放在旁边小卡片上。HA会自动记录历史数据温度曲线、温湿度对比图这些功能都能直接用。6. 常见问题烧录、扫描、数据解析避坑实录6.1 烧录失败Failed to connect to ESP32-C3这是最常见的拦路虎我第一次拿到C3开发板时就遇到了。报错信息大致是A fatal error occurred: Failed to connect to ESP32-C3: No serial data received。排查看三处驱动是否安装。很多C3开发板的USB转串口芯片是CH340Windows系统往往不会自动安装驱动需要去芯片官网下载。识别不到COM口先装驱动。开发板型号是否选对。Arduino IDE里要选“ESP32C3 Dev Module”不要选成ESP32。是否进入下载模式。连接好USB后按住开发板上的BOOT键不放点击烧录等串口出现“Connecting”再松开BOOT键。另外烧录时不要开其他占用串口的软件比如串口监视器会抢占COM口导致烧录中断。6.2 扫描不到传感器如果C3能正常跑但串口一直没打印传感器数据优先检查三件事。第一传感器是否真的刷好了ATC固件。如果还是原厂固件广播内容是加密的C3扫到也过滤不掉因为设备名可能不包含“LYWSD03”厂家数据也不是“CMT”开头。第二距离是否太远。LYWSD03MMC的广播功率很低实测超过15米或者隔两堵墙就会明显丢包。把传感器拿到C3旁边测试一下能收到再布点。第三广播间隔。ATC固件默认广播间隔大约2秒但有些版本可能支持配置为更长时间耐心等待10秒以上再说。6.3 温度数值明显不对数值不对优先怀疑解析偏移量。ATC固件数据格式在历代版本中基本一致但如果你用的是PVVX固件或冷门改版偏移会不同。排查方法是在回调函数里把原始的ManufacturerData按照十六进制打印出来手工对照改偏移。还有一个容易被忽略的点数据单位。ATC固件里温度原始值要除以10也就是25.6度会存成256。如果忘记除以10HA上就会显示256度。湿度同理。6.4 MQTT连接成功但HA不显示实体先检查Topic。自动发现主题必须严格以homeassistant/开头而且必须发布到/config结尾。我一开始把主题写成了homeassistant/sensor/xxx/stateHA完全没反应。再用MQTT客户端订阅homeassistant/sensor/#和home/#看看消息是否正常。如果自动发现始终不生效直接改用5.2节的手动YAML方式排错更快。6.5 关于功耗的实测ESP32-C3跑这个固件Wi-Fi保持连接、BLE持续扫描实测静态电流大约100mA瞬时峰值能到200mA以上。用USB供电完全没压力但如果想用电池供电做便携网关就必须改成低功耗方案用深度睡眠每隔10分钟唤醒一次扫描30秒发完数据重新睡平均电流能降到10mA以下。这种模式适合阳台、车库等不方便插电的位置。7. 后续扩展多传感器、低功耗与更多玩法这个网关架构可以轻松扩展到多个传感器。把代码里的单MAC过滤改成数组为每个传感器分配独立名称就能同时监控客厅、卧室、厨房等多个点位。我家现在是三个温度计在跑代码核心逻辑没变只是在回调里增加了一个设备名到Topic的映射表。低功耗方向也值得研究。C3的RTC唤醒机制很成熟定时唤醒后扫描、连Wi-Fi、发MQTT、再睡单次工作流可以控制在几秒内。我在调试这类方案时发现一个经验Wi-Fi连接时功耗最高所以唤醒后尽量先扫描传感器再连Wi-Fi发送而不是先连Wi-Fi再扫描。这样扫描阶段的低功耗状态就能充分利用起来。另外这套代码稍加改造可以支持更多米家BLE设备比如门窗传感器、人体传感器。只要了解它们的广播协议在回调里增加解析分支即可。整个项目最关键的价值在于打通了“米家蓝牙设备 - 本地网关 - Home Assistant”这条离线链路后续无论接Node-RED还是做语音联动都只是在消费这条链路上的数据罢了。
返回列表