(Battery voltage, AC power status, and 3× DHT22 temps/humidity) — with Home Assistant auto-discovery + email alerts + a tiny web UI
This project turns an ESP32 into a small “facility watchdog” that publishes:
- Battery voltage (ADC on
GPIO36
) - AC power status (sense on
GPIO39
, lights the onboard LED on loss) - Three DHT22 sensors for ambient/top rack/bottom rack (°C, °F, %RH)
- A simple web dashboard at
/
and a/data
JSON endpoint - Home Assistant MQTT Auto-Discovery so all sensors appear automatically
- Email alerts via Gmail when AC power drops or voltage falls below a threshold
⚠️ Replace every credential in the code below with your own placeholders before flashing (Wi-Fi SSID/password, Gmail app password, MQTT user/pass). Never publish real secrets online.
What you’ll need
- ESP32 DevKit (any common module)
- 3× DHT22 sensors (or fewer if you like) and 10 kΩ pull-ups if needed
- Voltage sense network (proper resistor divider into
GPIO36
—don’t exceed 3.3 V at the pin) - AC-present detector feeding a safe DC signal into
GPIO39
(again ≤ 3.3 V) - An MQTT broker (e.g., Mosquitto) reachable by the ESP32
- Home Assistant with the MQTT integration enabled
- A Gmail app password (2-step verification required)
⚠️ Boot-strap pins:
GPIO0
is a strapping pin. If your DHT22 drags it low at reset, the ESP32 may fail to boot. If you run into that, move the “Ambient” sensor to a safer pin likeGPIO4
and update the code.
How it works
- Voltage scaling:
constant
approximates your ADC scale × divider ratio. Calibrate by reading a known voltage and nudging the constant until Home Assistant matches your meter. - AC power: Any sensed value > ~5 V (after your divider/scaler math) maps to
"ON"
(mains present). When it drops, we:- flip the LED (on
GPIO2
) - fire a one-shot email (“AC Power Lost”)
- flip the LED (on
- DHTs: We publish °C, °F (derived), and %RH for each sensor.
- Home Assistant auto-discovery: We publish
config
topics underhomeassistant/.../config
once on MQTT connect; then we streamstate
topics every second.
Wiring (textual)
- Voltage sense: Battery + → resistor divider →
GPIO36
(ADC1), Battery − → GND - AC sense: AC-OK signal (through opto or rectified/scaled DC) →
GPIO39
(ADC1), GND common - DHT22:
- Ambient → data pin
GPIO0
(or better:GPIO4
), 3.3 V, GND - Top Rack → data pin
GPIO22
, 3.3 V, GND - Bottom Rack → data pin
GPIO13
, 3.3 V, GND - Add 10 kΩ pull-up from each DHT data pin to 3.3 V if the breakout doesn’t include one
- Ambient → data pin
Home Assistant: what appears
- Battery Voltage (V)
- AC Power (binary sensor)
- Ambient / Top Rack / Bottom Rack: temp (°C & °F) and humidity (%)
No YAML — the devices show up automatically once the ESP32 connects and publishes discovery messages.
Security & reliability notes
- Use a Gmail app password (not your account password).
- Prefer a local MQTT broker on a trusted LAN/VPN.
- If your ADC readings wander, average a few samples or add a small RC filter.
- ESP32 ADC2 conflicts with Wi-Fi; this sketch uses ADC1 pins (36/39), which are safe.
The code (sanitized & fixed)
Copy into an Arduino IDE / PlatformIO project. Install libraries: 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 and Email Configuration --------------------------
#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>" // use an app password
// -------------------------- Voltage and Switch Detection --------------------------
#define VOLTAGE_PIN_1 36 // ADC1 - safe with Wi-Fi
#define SCALE_CONSTANT 38.1 // calibrate this constant to your divider
#define LOW_VOLTAGE_THRESHOLD 20.0 // V
#define VOLTAGE_PIN_2 39 // AC-present sense (scaled to <=3.3V)
#define LED_PIN 2
#define SWITCH_ON LOW
#define SWITCH_OFF HIGH
// -------------------------- DHT22 Sensor Configuration --------------------------
#define DHTPIN0 0 // Consider using 4 if boot issues arise
#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 Configuration --------------------------
#define MQTT_SERVER "<YOUR_MQTT_HOST>"
#define MQTT_PORT 1883
#define MQTT_USER "<YOUR_MQTT_USER>"
#define MQTT_PASSWORD "<YOUR_MQTT_PASSWORD>"
// Discovery/state topics (one root per device keeps things tidy)
const char* deviceIdentifier = "hackmech_door_01";
WiFiClient espClient;
PubSubClient mqttClient(espClient);
// -------------------------- Globals --------------------------
SMTPSession smtp;
int lostPowerLatched = 0;
int lowVoltLatched = 0;
WebServer server(80);
// -------------------------- Function Prototypes --------------------------
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();
// -------------------------- Wi-Fi Connection --------------------------
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 Connection --------------------------
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);
}
}
}
// -------------------------- MQTT Auto-Discovery Config --------------------------
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);
};
// Voltage
{
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);
}
// AC Power (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);
}
// DHTs
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);
}
}
}
// Helper to capitalize "dht1" -> "DHT1"
String capitalize(const String& s){
if (s.length() == 0) return s;
String t = s;
t.setCharAt(0, t.charAt(0) - 32); // assumes lowercase ascii letter at [0]
return t;
}
// -------------------------- Voltage Measurement --------------------------
float readVoltage() {
// Ensure your divider never exceeds 3.3V at the pin.
return (float)analogRead(VOLTAGE_PIN_1) / 4095.0f * SCALE_CONSTANT;
}
// -------------------------- Switch State --------------------------
String readSwitchState() {
// Map your AC-present sensing to a threshold; adjust 64.5 & 5.0 as you calibrate
float v = (float)analogRead(VOLTAGE_PIN_2) / 4095.0f * 64.5f;
return (v > 5.0f) ? "ON" : "OFF";
}
// -------------------------- Email Sending --------------------------
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::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 Server --------------------------
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");
}
// -------------------------- Periodic MQTT Publishing --------------------------
void publishSensorData() {
// Voltage & AC power
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);
// DHTs
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();
}
// -------------------------- Main Loop --------------------------
void loop() {
if (!mqttClient.connected()) connectMQTT();
mqttClient.loop();
server.handleClient();
// Read sensors
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);
// Alerts
float v = readVoltage();
String sw = readSwitchState();
// Low voltage one-shot
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;
}
// AC lost one-shot + 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);
}
Setup steps (quick hits)
- Install libraries in Arduino IDE:
- ESP Mail Client (Mobizt)
- PubSubClient (Nick O’Leary)
- DHT sensor library (Adafruit)
- Create a Gmail app password, paste into
sender_password
. - Point MQTT to your broker (
MQTT_SERVER
, port, user/pass). - Flash to ESP32.
- In Home Assistant, make sure the MQTT integration is active. Within ~10 seconds of ESP32 connecting, the entities should appear under HACKMECH Door Monitor.
Troubleshooting
- Entities not showing in HA → Open MQTT Explorer and verify you see
homeassistant/.../config
and.../state
topics. - ESP32 won’t boot → Move DHT from
GPIO0
toGPIO4
(strap pin issue). - Gmail send fails → Confirm port 587, app password, and that your network allows outbound SMTP.
- ADC reads wrong → Calibrate
SCALE_CONSTANT
, check divider, ensure max 3.3 V at the pin. - AC status flaps → Add hysteresis or average a few samples before deciding “ON/OFF”.
Comments NOTHING