Build a Wi-Fi + MQTT Door & Rack Monitor on ESP32

zach Posted on 25 days ago 66 Views HackMech


(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 like GPIO4 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”)
  • DHTs: We publish °C, °F (derived), and %RH for each sensor.
  • Home Assistant auto-discovery: We publish config topics under homeassistant/.../config once on MQTT connect; then we stream state 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

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)

  1. Install libraries in Arduino IDE:
    • ESP Mail Client (Mobizt)
    • PubSubClient (Nick O’Leary)
    • DHT sensor library (Adafruit)
  2. Create a Gmail app password, paste into sender_password.
  3. Point MQTT to your broker (MQTT_SERVER, port, user/pass).
  4. Flash to ESP32.
  5. 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 to GPIO4 (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”.