(电池电压、交流电状态、3×DHT22 温湿度)——支持 Home Assistant MQTT Auto-Discovery、邮件告警与轻量 Web 面板
这个项目把一块 ESP32 变成小型“值班员”,它会持续上报与展示:
- 电池电压(
GPIO36
模拟量) - 交流电是否存在(
GPIO39
检测,掉电时点亮板载 LED) - 三路 DHT22:环境 / 顶层机架 / 底层机架(°C、°F、%RH)
- 一个简洁的 网页仪表盘(
/
)与 JSON 数据接口(/data
) - Home Assistant MQTT 自动发现,实体开箱即用,无需 YAML
- Gmail 邮件告警:市电掉电或电压低于阈值时单次提醒
⚠️ 在烧录前,请把下面代码中的 Wi-Fi、Gmail、MQTT 等凭据替换为你自己的占位符。不要在公网帖中泄露真实密码或服务器地址。
准备材料
- ESP32 开发板(常见 DevKit 均可)
- 3× DHT22(也可用 1~2 个),若无板载上拉需自备 10 kΩ 上拉电阻
- 电压采样分压 到
GPIO36
(ADC1);引脚最大 3.3 V,请正确选择分压 - 市电存在检测:经光耦/整流稳压后形成 ≤3.3 V 的直流信号输入
GPIO39
- 一台可达的 MQTT Broker(如 Mosquitto)
- 已启用 MQTT 集成 的 Home Assistant
- 开启两步验证后申请的 Gmail 应用专用密码
⚠️ 启动脚位注意:
GPIO0
属于上电拉脚。如果 DHT22 在上电时把它拉低,ESP32 可能无法正常启动。若遇到此问题,把“环境”DHT 改接GPIO4
(并同步修改代码)。
工作原理(简述)
- 电压标定:
SCALE_CONSTANT
结合分压比与 ADC 标定用于换算电压值。用万用表对比并微调该常数,让 Home Assistant 显示与实际一致。 - 市电状态:将感应到的电压换算后与阈值比较(示例中约 5 V)。当判断为 OFF:
- 切换 LED 指示;
- 发送一次 “市电丢失” 邮件(恢复后复位锁存)。
- DHT:分别发布 °C、由 °C 计算的 °F、以及湿度 %。
- HA 自动发现:首次连接 MQTT 时发布
homeassistant/.../config
主题;随后每秒发布.../state
主题。
接线(文字说明)
- 电池电压:电池 + → 电阻分压 →
GPIO36
(ADC1);电池 − → GND - 市电存在:通过光耦/整流稳压等把“市电 OK”转换为安全直流 →
GPIO39
(ADC1),公共地 → GND - DHT22:
- 环境:数据脚 →
GPIO0
(若有启动问题改为GPIO4
),供电 3.3 V、GND - 顶层机架:数据脚 →
GPIO22
- 底层机架:数据脚 →
GPIO13
- 若模块无上拉,请在数据脚与 3.3 V 之间接 10 kΩ 上拉
- 环境:数据脚 →
Home Assistant 中会出现的实体
- Battery Voltage(电池电压,V)
- AC Power(二进制传感器)
- Ambient / Top Rack / Bottom Rack:温度(°C 与 °F)与湿度(%)
无需 YAML,设备上电联网并成功连接 MQTT 后即可自动出现。
安全与稳定性
- Gmail 必须用“应用专用密码”,不要用账号真实密码。
- MQTT 建议在 可信局域网/VPN 内使用,避免暴露到公网。
- ADC 抖动可通过 多次采样平均 或在模拟端加入 RC 滤波 改善。
- 切记 ESP32 ADC2 与 Wi-Fi 冲突,本项目选用 ADC1(36/39) 是安全的。
代码(已清理与修正)
需要的库(Arduino IDE“库管理器”可搜):ESP Mail Client、PubSubClient、DHT sensor library
#include <WiFi.h>
#include <ESP_Mail_Client.h>
#include <WebServer.h>
#include <PubSubClient.h>
#include "DHT.h"
// -------------------------- Wi-Fi 与邮件配置 --------------------------
#define WIFI_SSID "<YOUR_WIFI_SSID>"
#define WIFI_PASSWORD "<YOUR_WIFI_PASSWORD>"
#define SMTP_server "smtp.gmail.com"
#define SMTP_Port 587 // STARTTLS
#define sender_email "<YOUR_GMAIL_ADDRESS>"
#define sender_password "<YOUR_GMAIL_APP_PASSWORD>" // 用应用专用密码
// -------------------------- 电压与市电检测 --------------------------
#define VOLTAGE_PIN_1 36 // ADC1 - 与 Wi-Fi 不冲突
#define SCALE_CONSTANT 38.1 // 按分压与标定调整
#define LOW_VOLTAGE_THRESHOLD 20.0 // 低压阈值(V)
#define VOLTAGE_PIN_2 39 // 市电存在检测(≤3.3V)
#define LED_PIN 2
#define SWITCH_ON LOW
#define SWITCH_OFF HIGH
// -------------------------- DHT22 传感器 --------------------------
#define DHTPIN0 0 // 若影响启动,改用 4 并同步修改
#define DHTPIN1 22
#define DHTPIN2 13
#define DHTTYPE DHT22
DHT dht1(DHTPIN0, DHTTYPE);
DHT dht2(DHTPIN1, DHTTYPE);
DHT dht3(DHTPIN2, DHTTYPE);
float dht1_temp = NAN, dht1_hum = NAN;
float dht2_temp = NAN, dht2_hum = NAN;
float dht3_temp = NAN, dht3_hum = NAN;
float dht1_temp_f = NAN;
float dht2_temp_f = NAN;
float dht3_temp_f = NAN;
// -------------------------- MQTT 配置 --------------------------
#define MQTT_SERVER "<YOUR_MQTT_HOST>"
#define MQTT_PORT 1883
#define MQTT_USER "<YOUR_MQTT_USER>"
#define MQTT_PASSWORD "<YOUR_MQTT_PASSWORD>"
// 设备标识
const char* deviceIdentifier = "hackmech_door_01";
WiFiClient espClient;
PubSubClient mqttClient(espClient);
// -------------------------- 全局变量 --------------------------
SMTPSession smtp;
int lostPowerLatched = 0; // 掉电一次性告警锁存
int lowVoltLatched = 0; // 低压一次性告警锁存
WebServer server(80);
// -------------------------- 函数原型 --------------------------
void connectWiFi();
void connectMQTT();
void publishAutoDiscoveryConfigs();
void publishSensorData();
void sendEmail(const String& subject, const String& body);
float readVoltage();
String readSwitchState();
void handleRoot();
void handleData();
void handleNotFound();
String capitalize(const String& s); // 提前声明,避免原型问题
// -------------------------- Wi-Fi 连接 --------------------------
void connectWiFi() {
Serial.println("\nConnecting to WiFi...");
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
for (int i = 0; i < 40 && WiFi.status() != WL_CONNECTED; ++i) {
delay(250);
Serial.print(".");
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\nWiFi connected!");
Serial.print("IP Address: ");
Serial.println(WiFi.localIP());
} else {
Serial.println("\nFailed to connect to WiFi. Rebooting...");
delay(2000);
ESP.restart();
}
}
// -------------------------- MQTT 连接 --------------------------
void connectMQTT() {
while (!mqttClient.connected()) {
Serial.print("Connecting to MQTT...");
if (mqttClient.connect("HACKMECHDoorMonitor", MQTT_USER, MQTT_PASSWORD)) {
Serial.println("connected");
publishAutoDiscoveryConfigs();
} else {
Serial.print("failed, rc=");
Serial.print(mqttClient.state());
Serial.println(" retrying in 5 seconds");
delay(5000);
}
}
}
// -------------------------- HA 自动发现配置 --------------------------
void publishAutoDiscoveryConfigs() {
String dev = String("\"device\":{\"identifiers\":[\"") + deviceIdentifier +
"\"],\"name\":\"HACKMECH Door Monitor\",\"manufacturer\":\"HACKMECH IoT\",\"model\":\"ESP32\"}";
auto pub_cfg = [&](const String& path, const String& payload){
mqttClient.publish(path.c_str(), payload.c_str(), true);
};
// 电压
{
String obj = String(deviceIdentifier) + "_voltage";
String cfgPath = "homeassistant/sensor/" + String(deviceIdentifier) + "/voltage/config";
String stPath = "homeassistant/sensor/" + String(deviceIdentifier) + "/voltage/state";
String cfg = String("{\"name\":\"Battery Voltage\",\"device_class\":\"voltage\",\"unit_of_measurement\":\"V\",")
+ "\"state_topic\":\"" + stPath + "\",\"unique_id\":\"" + obj + "\"," + dev + "}";
pub_cfg(cfgPath, cfg);
}
// 市电(binary_sensor)
{
String obj = String(deviceIdentifier) + "_ac_power";
String cfgPath = "homeassistant/binary_sensor/" + String(deviceIdentifier) + "/ac_power/config";
String stPath = "homeassistant/binary_sensor/" + String(deviceIdentifier) + "/ac_power/state";
String cfg = String("{\"name\":\"AC Power\",\"device_class\":\"power\",\"payload_on\":\"ON\",\"payload_off\":\"OFF\",")
+ "\"state_topic\":\"" + stPath + "\",\"unique_id\":\"" + obj + "\"," + dev + "}";
pub_cfg(cfgPath, cfg);
}
// 三路 DHT
for (int i = 1; i <= 3; i++) {
String sensor = "dht" + String(i);
struct Item { const char* key; const char* name; const char* unit; const char* devclass; } items[] = {
{"temp_c", "Temperature C", "°C", "temperature"},
{"temp_f", "Temperature F", "°F", "temperature"},
{"humidity","Humidity", "%", "humidity"}
};
for (auto &it : items) {
String obj = String(deviceIdentifier) + "_" + sensor + "_" + it.key;
String cfgPath= "homeassistant/sensor/" + String(deviceIdentifier) + "/" + sensor + "_" + it.key + "/config";
String stPath = "homeassistant/sensor/" + String(deviceIdentifier) + "/" + sensor + "/" + it.key + "/state";
String cfg = String("{\"name\":\"") + capitalize(sensor) + " " + it.name + "\","
+ "\"device_class\":\"" + it.devclass + "\","
+ "\"unit_of_measurement\":\"" + it.unit + "\","
+ "\"state_topic\":\"" + stPath + "\","
+ "\"unique_id\":\"" + obj + "\"," + dev + "}";
pub_cfg(cfgPath, cfg);
}
}
}
// 把 "dht1" 的首字母变大写 -> "DHT1"(简易实现)
String capitalize(const String& s){
if (s.length() == 0) return s;
String t = s;
char c = t.charAt(0);
if (c >= 'a' && c <= 'z') t.setCharAt(0, c - 32);
return t;
}
// -------------------------- 读取电压 --------------------------
float readVoltage() {
// 注意分压与 3.3V 上限
return (float)analogRead(VOLTAGE_PIN_1) / 4095.0f * SCALE_CONSTANT;
}
// -------------------------- 市电状态 --------------------------
String readSwitchState() {
// 阈值请按实际标定(示例把 ADC 映射到约 0~64.5V 的量程,然后以 5V 为阈)
float v = (float)analogRead(VOLTAGE_PIN_2) / 4095.0f * 64.5f;
return (v > 5.0f) ? "ON" : "OFF";
}
// -------------------------- 发送邮件 --------------------------
void sendEmail(const String& subject, const String& body) {
ESP_Mail_Session session;
session.server.host_name = SMTP_server;
session.server.port = SMTP_Port;
session.login.email = sender_email;
session.login.password = sender_password;
session.login.user_domain= "";
SMTP_Message message;
message.sender.name = "Door Alarm System";
message.sender.email = sender_email;
message.subject = subject;
message.priority = esp_mail_smtp_priority_high;
struct Recip { const char* name; const char* email; } recipients[] = {
{"Ops1", "<[email protected]>"},
{"Ops2", "<[email protected]>"},
{"Ops3", "<[email protected]>"}
};
for (auto &r : recipients) message.addRecipient(r.name, r.email);
message.html.content = body.c_str();
message.html.charSet = "utf-8";
message.html.transfer_encoding = Content_Transfer_Encoding::enc_7bit;
if (!smtp.connect(&session)) {
Serial.println("SMTP connect failed: " + smtp.errorReason());
return;
}
if (!MailClient.sendMail(&smtp, &message)) {
Serial.println("sendMail failed: " + smtp.errorReason());
} else {
Serial.println("Email sent successfully");
}
smtp.closeSession();
}
// -------------------------- Web 服务 --------------------------
void handleRoot() {
server.send(200, "text/html", R"rawliteral(
<!DOCTYPE html><html><head><meta charset="utf-8"><title>HACKMECH Door</title>
<style>body{font-family:system-ui;margin:24px} h1{margin:0 0 12px}</style>
<script>
setInterval(()=>fetch('/data').then(r=>r.json()).then(d=>{
const ids=['voltage','switchState',
'dht1_temp','dht1_temp_f','dht1_hum',
'dht2_temp','dht2_temp_f','dht2_hum',
'dht3_temp','dht3_temp_f','dht3_hum'];
ids.forEach(id=>{document.getElementById(id).innerText = d[id];});
}),1000);
</script></head><body>
<h1>HACKMECH Door Monitoring</h1>
<p>Battery Voltage: <b><span id="voltage">...</span> V</b></p>
<p>AC Power: <b><span id="switchState">...</span></b></p>
<h2>Ambient</h2>
<p>Temperature (C): <span id="dht1_temp">...</span></p>
<p>Temperature (F): <span id="dht1_temp_f">...</span></p>
<p>Humidity: <span id="dht1_hum">...</span> %</p>
<h2>Top Rack</h2>
<p>Temperature (C): <span id="dht2_temp">...</span></p>
<p>Temperature (F): <span id="dht2_temp_f">...</span></p>
<p>Humidity: <span id="dht2_hum">...</span> %</p>
<h2>Bottom Rack</h2>
<p>Temperature (C): <span id="dht3_temp">...</span></p>
<p>Temperature (F): <span id="dht3_temp_f">...</span></p>
<p>Humidity: <span id="dht3_hum">...</span> %</p>
</body></html>
)rawliteral");
}
void handleData() {
String json = "{";
json += "\"voltage\":" + String(readVoltage(), 2) + ",";
json += "\"switch\":\"" + readSwitchState() + "\",";
json += "\"dht1_temp\":" + (isnan(dht1_temp) ? String("\"N/A\"") : String(dht1_temp, 2)) + ",";
json += "\"dht1_temp_f\":" + (isnan(dht1_temp_f) ? String("\"N/A\"") : String(dht1_temp_f, 2)) + ",";
json += "\"dht1_hum\":" + (isnan(dht1_hum) ? String("\"N/A\"") : String(dht1_hum, 2)) + ",";
json += "\"dht2_temp\":" + (isnan(dht2_temp) ? String("\"N/A\"") : String(dht2_temp, 2)) + ",";
json += "\"dht2_temp_f\":" + (isnan(dht2_temp_f) ? String("\"N/A\"") : String(dht2_temp_f, 2)) + ",";
json += "\"dht2_hum\":" + (isnan(dht2_hum) ? String("\"N/A\"") : String(dht2_hum, 2)) + ",";
json += "\"dht3_temp\":" + (isnan(dht3_temp) ? String("\"N/A\"") : String(dht3_temp, 2)) + ",";
json += "\"dht3_temp_f\":" + (isnan(dht3_temp_f) ? String("\"N/A\"") : String(dht3_temp_f, 2)) + ",";
json += "\"dht3_hum\":" + (isnan(dht3_hum) ? String("\"N/A\"") : String(dht3_hum, 2));
json += "}";
server.send(200, "application/json", json);
}
void handleNotFound() {
server.send(404, "text/plain", "404: Not Found");
}
// -------------------------- 定时发布 MQTT --------------------------
void publishSensorData() {
// 电压与市电
mqttClient.publish(
("homeassistant/sensor/" + String(deviceIdentifier) + "/voltage/state").c_str(),
String(readVoltage(), 2).c_str(), true);
mqttClient.publish(
("homeassistant/binary_sensor/" + String(deviceIdentifier) + "/ac_power/state").c_str(),
readSwitchState().c_str(), true);
// 三路 DHT
struct Pack { const char* name; float* tc; float* tf; float* rh; } packs[] = {
{"dht1", &dht1_temp, &dht1_temp_f, &dht1_hum},
{"dht2", &dht2_temp, &dht2_temp_f, &dht2_hum},
{"dht3", &dht3_temp, &dht3_temp_f, &dht3_hum}
};
for (auto &p : packs) {
if (!isnan(*p.tc)) mqttClient.publish(("homeassistant/sensor/" + String(deviceIdentifier) + "/" + p.name + "/temp_c/state").c_str(), String(*p.tc, 2).c_str(), true);
if (!isnan(*p.tf)) mqttClient.publish(("homeassistant/sensor/" + String(deviceIdentifier) + "/" + p.name + "/temp_f/state").c_str(), String(*p.tf, 2).c_str(), true);
if (!isnan(*p.rh)) mqttClient.publish(("homeassistant/sensor/" + String(deviceIdentifier) + "/" + p.name + "/humidity/state").c_str(), String(*p.rh, 2).c_str(), true);
}
}
// -------------------------- Setup --------------------------
void setup() {
Serial.begin(921600);
delay(200);
connectWiFi();
pinMode(VOLTAGE_PIN_1, INPUT);
pinMode(VOLTAGE_PIN_2, INPUT);
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LOW);
dht1.begin();
dht2.begin();
dht3.begin();
server.on("/", handleRoot);
server.on("/data", handleData);
server.onNotFound(handleNotFound);
server.begin();
mqttClient.setServer(MQTT_SERVER, MQTT_PORT);
connectMQTT();
}
// -------------------------- Loop --------------------------
void loop() {
if (!mqttClient.connected()) connectMQTT();
mqttClient.loop();
server.handleClient();
// 读 DHT
dht1_temp = dht1.readTemperature(); dht1_hum = dht1.readHumidity();
dht2_temp = dht2.readTemperature(); dht2_hum = dht2.readHumidity();
dht3_temp = dht3.readTemperature(); dht3_hum = dht3.readHumidity();
dht1_temp_f = isnan(dht1_temp) ? NAN : (dht1_temp * 9.0f/5.0f + 32.0f);
dht2_temp_f = isnan(dht2_temp) ? NAN : (dht2_temp * 9.0f/5.0f + 32.0f);
dht3_temp_f = isnan(dht3_temp) ? NAN : (dht3_temp * 9.0f/5.0f + 32.0f);
// 告警逻辑
float v = readVoltage();
String sw = readSwitchState();
// 低压一次性告警
if (v < LOW_VOLTAGE_THRESHOLD && lowVoltLatched == 0) {
sendEmail("Low Voltage Alert", "Voltage dropped to " + String(v, 1) + " V");
lowVoltLatched = 1;
} else if (v >= LOW_VOLTAGE_THRESHOLD) {
lowVoltLatched = 0;
}
// 市电丢失一次性告警 + LED
digitalWrite(LED_PIN, (sw == "ON") ? HIGH : LOW);
if (sw == "OFF" && lostPowerLatched == 0) {
sendEmail("AC Power Lost", "Mains lost — running on battery.");
lostPowerLatched = 1;
} else if (sw == "ON") {
lostPowerLatched = 0;
}
publishSensorData();
delay(1000);
}
快速上手
- 安装库:ESP Mail Client、PubSubClient、DHT sensor library。
- 申请 Gmail 应用专用密码 并填入
sender_password
。 - 配置 MQTT:填好
MQTT_SERVER/PORT/USER/PASSWORD
。 - 烧录 到 ESP32,串口观察联网/连接日志。
- Home Assistant 中启用 MQTT Integration,设备应在连接后 10 秒内自动出现。
故障排查
- HA 无实体 → 用 MQTT Explorer 检查是否有
homeassistant/.../config
与.../state
主题。 - ESP32 不启动 → 把 DHT 从
GPIO0
移到GPIO4
。 - Gmail 发送失败 → 确认端口 587、应用专用密码、网络允许外发 SMTP。
- ADC 数值偏差 → 标定
SCALE_CONSTANT
,核对分压与参考电压,确保引脚不超 3.3 V。 - 市电状态抖动 → 增加 滞回/平均滤波,或提高检测阈值的鲁棒性。
Comments NOTHING