🔗Goal
Build a smart door sensor that detects when a door is opened or closed and sends notifications over MQTT. The system uses a magnetic reed switch mounted on the door frame — when the door is closed, the magnet holds the switch shut. When the door opens, the magnet moves away and the switch opens, triggering the ESP32 to publish a message.
Here is how the system works at a high level:
graph LR
A[Reed Switch + Magnet] -->|GPIO 14| B[ESP32]
B -->|WiFi| C[MQTT Broker]
C --> D[Phone / MQTT Client]
C --> E[Home Automation]This is a practical home automation project. Once the ESP32 publishes door events to MQTT, you can use any MQTT-compatible system — MQTT Explorer, Node-RED, Home Assistant, or a custom app — to receive notifications and trigger automations.
🔗How a Reed Switch Works
A reed switch is a small glass tube containing two thin metal contacts (reeds). When a magnet is brought close to the switch, the reeds are pulled together and the circuit closes. When the magnet moves away, the reeds spring apart and the circuit opens.
For a door sensor:
- Door closed: Magnet is near the reed switch, circuit is closed (reads LOW with INPUT_PULLUP)
- Door open: Magnet is far from the reed switch, circuit is open (reads HIGH with INPUT_PULLUP)
graph TD
A[Magnet near switch] -->|Circuit closed| B[GPIO reads LOW]
B --> C[Door is CLOSED]
D[Magnet away from switch] -->|Circuit open| E[GPIO reads HIGH]
E --> F[Door is OPEN]We use the ESP32's built-in pull-up resistor (INPUT_PULLUP) so no external resistor is needed for the reed switch.
🔗Prerequisites
You will need the following components:
| Component | Qty | Notes | Buy |
|---|---|---|---|
| ESP32 dev board | 1 | AliExpress | Amazon.de .co.uk .com | |
| Magnetic reed switch | 1 | Normally open (NO) type, often sold with a magnet | AliExpress | Amazon.de .co.uk .com |
| Breadboard | 1 | For prototyping | AliExpress | Amazon.de .co.uk .com |
| Jumper wires | ~4 | AliExpress | Amazon.de .co.uk .com |
Links marked Amazon/AliExpress are affiliate links. We may earn a small commission at no extra cost to you.
You will also need the following Arduino libraries:
| Library | Author | Purpose |
|---|---|---|
| PubSubClient | Nick O'Leary | MQTT client for Arduino |
No external pull-up resistor is needed. We use the ESP32's internal pull-up via
INPUT_PULLUP.
🔗Tutorial
🔗Step 1: Wiring
The reed switch has two wires (no polarity — either wire can go to either pin).
| Component | Pin | ESP32 Pin |
|---|---|---|
| Reed switch | Wire 1 | GPIO 14 |
| Reed switch | Wire 2 | GND |
That is it — just two connections. With INPUT_PULLUP enabled on GPIO 14, the pin is held HIGH internally. When the reed switch closes (magnet nearby), it connects GPIO 14 to GND, pulling it LOW.
Pin labels and GPIO numbers vary between ESP32 boards. Always check your board's pinout diagram and datasheet.
🔗Step 2: Mount the sensor (or simulate it)
For testing on a breadboard, simply hold the magnet near the reed switch to simulate a closed door, and move it away to simulate an open door.
For permanent installation:
- Mount the reed switch on the door frame (the stationary part)
- Mount the magnet on the door itself (the moving part)
- When the door is closed, the magnet should be within $10$-$15\,\text{mm}$ of the reed switch
🔗Step 3: Upload the code
#include <WiFi.h>
#include <PubSubClient.h>
// ----- Configuration -----
const char* WIFI_SSID = "YOUR_WIFI_SSID";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";
const char* MQTT_BROKER = "test.mosquitto.org";
const int MQTT_PORT = 1883;
const char* MQTT_CLIENT_ID = "esp32_door_sensor";
const char* MQTT_TOPIC = "home/door/status";
#define REED_PIN 14
// ----- State -----
int lastDoorState = -1; // -1 means "unknown" on startup
unsigned long lastDebounceTime = 0;
const unsigned long DEBOUNCE_DELAY = 50; // 50ms debounce
int lastRawReading = -1;
// ----- Objects -----
WiFiClient wifiClient;
PubSubClient mqtt(wifiClient);
// ----- WiFi connection -----
void connectWiFi() {
if (WiFi.status() == WL_CONNECTED) return;
Serial.print("Connecting to WiFi");
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 40) {
delay(500);
Serial.print(".");
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println();
Serial.print("Connected! IP: ");
Serial.println(WiFi.localIP());
} else {
Serial.println();
Serial.println("WiFi connection failed. Will retry...");
}
}
// ----- MQTT connection -----
void connectMQTT() {
if (mqtt.connected()) return;
Serial.print("Connecting to MQTT broker...");
while (!mqtt.connected()) {
if (mqtt.connect(MQTT_CLIENT_ID)) {
Serial.println(" connected!");
} else {
Serial.print(" failed (rc=");
Serial.print(mqtt.state());
Serial.println("). Retrying in 5 seconds...");
delay(5000);
}
}
}
// ----- Publish door state -----
void publishDoorState(const char* state) {
// Build JSON payload with state and uptime
unsigned long uptimeSeconds = millis() / 1000;
String payload = "{";
payload += "\"status\":\"" + String(state) + "\",";
payload += "\"uptime_s\":" + String(uptimeSeconds);
payload += "}";
if (mqtt.publish(MQTT_TOPIC, payload.c_str(), true)) { // retained message
Serial.print("Published to ");
Serial.print(MQTT_TOPIC);
Serial.print(": ");
Serial.println(payload);
} else {
Serial.println("ERROR: MQTT publish failed.");
}
}
void setup() {
Serial.begin(115200);
// Reed switch: one wire to GPIO, other to GND
// INPUT_PULLUP means pin reads HIGH when switch is open (door open)
// and LOW when switch is closed (door closed, magnet nearby)
pinMode(REED_PIN, INPUT_PULLUP);
connectWiFi();
mqtt.setServer(MQTT_BROKER, MQTT_PORT);
connectMQTT();
// Publish initial state
int initialState = digitalRead(REED_PIN);
lastDoorState = initialState;
if (initialState == LOW) {
publishDoorState("CLOSED");
Serial.println("Initial state: CLOSED");
} else {
publishDoorState("OPEN");
Serial.println("Initial state: OPEN");
}
Serial.println("Smart Door Sensor ready.");
}
void loop() {
// Reconnect if needed
connectWiFi();
if (!mqtt.connected()) {
connectMQTT();
}
mqtt.loop();
// ----- Debounced reed switch reading -----
int currentReading = digitalRead(REED_PIN);
// If the raw reading changed, reset the debounce timer
if (currentReading != lastRawReading) {
lastDebounceTime = millis();
lastRawReading = currentReading;
}
// If the reading has been stable for longer than the debounce delay,
// accept it as the actual state
if ((millis() - lastDebounceTime) > DEBOUNCE_DELAY) {
if (currentReading != lastDoorState) {
lastDoorState = currentReading;
if (currentReading == LOW) {
Serial.println("Door CLOSED");
publishDoorState("CLOSED");
} else {
Serial.println("Door OPEN");
publishDoorState("OPEN");
}
}
}
delay(10); // Small delay to reduce CPU usage
}🔗Step 4: Test the sensor
- Upload the sketch and open the Serial Monitor at 115200 baud
- Hold the magnet near the reed switch — the Serial Monitor should show "Door CLOSED"
- Move the magnet away — it should show "Door OPEN"
- Each state change is published to MQTT
To verify MQTT messages from your computer:
mosquitto_sub -h test.mosquitto.org -t "home/door/status"You should see JSON messages like:
{"status":"OPEN","uptime_s":45}
{"status":"CLOSED","uptime_s":52}The message is published with the
retainedflag, so new subscribers immediately receive the last known state even if no state change has occurred recently.
🔗Step 5: Understanding debounce
Mechanical switches "bounce" — when the contacts open or close, they can rapidly toggle several times before settling. Without debouncing, a single door open might register as multiple events.
The code debounces by requiring the reading to remain stable for $50\,\text{ms}$ before accepting it as a valid state change. This timing diagram shows why:
sequenceDiagram
participant Door
participant Reed Switch
participant ESP32
participant MQTT
Door->>Reed Switch: Door opens (magnet moves away)
Reed Switch->>ESP32: Signal bounces HIGH/LOW/HIGH
Note over ESP32: Wait 50ms for stable reading
ESP32->>ESP32: Reading stable at HIGH
ESP32->>MQTT: Publish "OPEN"🔗Next Steps: Deep Sleep for Battery Operation
If you want to run this sensor on a battery, the ESP32's deep sleep mode can dramatically reduce power consumption. Instead of constantly polling the reed switch, the ESP32 sleeps and wakes up only when the pin state changes.
Deep sleep reduces current draw from approximately $40\,\text{mA}$ (active WiFi) to about $10\,\mu\text{A}$. This can extend battery life from days to months.
The trade-off: after waking from deep sleep, the ESP32 must reconnect to WiFi and MQTT, which takes 2-5 seconds. For a door sensor, this delay is usually acceptable.
Here is the concept — replace the loop() function with a wake-on-pin approach:
// At the end of setup(), after publishing the initial state:
// Configure GPIO 14 to wake from deep sleep on state change
esp_sleep_enable_ext0_wakeup(GPIO_NUM_14, lastDoorState == LOW ? 1 : 0);
// 1 = wake when pin goes HIGH (door opens)
// 0 = wake when pin goes LOW (door closes)
Serial.println("Entering deep sleep. Will wake on door state change.");
Serial.flush();
esp_deep_sleep_start();With this approach, the ESP32 wakes up, reads the door state, connects to WiFi, publishes the MQTT message, and goes back to sleep. The loop() function never runs because setup ends with deep sleep.
🔗Common Issues and Solutions
| Problem | Cause | Fix |
|---|---|---|
| Door state does not change | Magnet too far from reed switch | Bring the magnet within $10$-$15\,\text{mm}$ of the switch. Reed switches have a limited activation distance |
| Multiple MQTT messages on one open/close | Switch bounce | The code includes debouncing ($50\,\text{ms}$). If you still see duplicates, increase DEBOUNCE_DELAY to 100 |
| State is inverted (shows OPEN when closed) | Reed switch type is normally closed (NC) instead of normally open (NO) | Swap the logic: LOW means OPEN and HIGH means CLOSED, or use an NO reed switch |
| WiFi reconnection is slow | Normal behavior after deep sleep or WiFi dropout | WiFi reconnection takes 2-5 seconds. The code retries automatically |
| MQTT publishes fail | Broker unreachable or WiFi not connected | Check that WiFi is connected before publishing. The code handles reconnection in loop() |
| Sensor works on breadboard but not on door | Wires too long or poor connection | Use shielded wire for runs longer than $1\,\text{m}$. Solder connections instead of using breadboard jumpers |
| Battery drains in one day | ESP32 is not entering deep sleep | Implement the deep sleep approach from the "Next Steps" section. Active WiFi draws about $40\,\text{mA}$ |