IoT Automation

DIY MQTT Garage Door Opener with ESP32, Relay and Reed Switch

Turn your existing garage door motor into a smart one with an ESP32, a relay that presses the wall button for you, and a reed switch that reports the real door state as a retained MQTT message.

MQTT garage door opener built with an ESP32, a relay module and a magnetic reed switch on the door frame
Run this on your own MQTT brokerGet a private, managed MQTT broker on the Free plan and start from ready-made code samples.
Try it free Code samples No card required

An MQTT garage door opener lets you open, close and, most importantly, check your garage door from anywhere, using the motor you already have. In this tutorial you will wire an ESP32 to a relay that "presses" the wall button and a reed switch that reports whether the door is really closed, then publish that state as a retained MQTT message so every app sees the truth the moment it connects.

How the Opener Works

Almost every residential garage door motor has terminals for a wall push button. Shorting those two terminals briefly is the same as pressing the button: the door starts, stops or reverses depending on where it is. That makes the hardware side simple and non-invasive:

  • A relay wired in parallel with the wall button gives a 500 ms "press".
  • A reed switch on the frame, with a magnet on the door, closes when the door is fully shut.
  • The ESP32 listens for commands, pulses the relay, and publishes the reed switch state.

The key design choice is that the opener never assumes what the door did. It publishes what the sensor sees. A command is a request; the reed switch is the fact.

Parts List

Part Purpose
ESP32 DevKit Wi-Fi and MQTT client
1-channel 3.3 V-compatible relay module (or optocoupled 5 V module) Shorts the wall button terminals
Magnetic reed switch, normally open, surface-mount Detects fully closed door
Optional second reed switch Detects fully open door
5 V USB supply and enclosure Power and protection from dust
Two-core bell wire Relay to motor terminals, reed to ESP32

Before wiring anything, check your opener's manual. Some newer motors use a digital wall panel rather than a dry contact; those need a compatible interface or a spare remote with its button wired to the relay.

Wiring

From To Notes
Relay IN GPIO 26 Active level depends on your module
Relay VCC / GND 5V / GND Optocoupled modules may need a separate JD-VCC
Relay COM and NO Wall button terminals on the motor In parallel with the existing button
Reed switch (closed position) GPIO 27 and GND Uses the internal pull-up
Reed switch (open position, optional) GPIO 25 and GND Uses the internal pull-up

With INPUT_PULLUP, a closed reed switch reads LOW. Mount the closed-position reed so the magnet sits within about a centimetre only when the door is fully down.

Topic Design for the MQTT Garage Door Opener

Following the conventions from our smart home MQTT topic structure guide:

<base>/garage/door/state          retained: "closed", "open", "opening/closing", "unknown"
<base>/garage/door/state/set      command: "OPEN", "CLOSE", "PRESS" (never retained)
<base>/garage/door/availability   retained: "online" / "offline" via last will

Retaining the state topic means your phone dashboard shows "closed" immediately on connect instead of waiting for the next change. Never retaining the command topic means a power cut cannot replay an old OPEN and open the door by itself.

The ESP32 Sketch

The sketch uses PubSubClient. Set BENCH_TEST to true while testing on the public broker with an LED instead of the motor; set it to false for your private broker with TLS.

#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <PubSubClient.h>

#define BENCH_TEST true

const char* WIFI_SSID = "YOUR_WIFI";
const char* WIFI_PASS = "YOUR_WIFI_PASSWORD";
const char* BROKER    = "broker.connectmq.com";

#if BENCH_TEST
  const char* BASE = "connectmq/demo/g4k9t2";   // random prefix, public area
  const char* CLIENT_ID = "garage-g4k9t2";
  const char* MQTT_USER = nullptr;
  const char* MQTT_PASS = nullptr;
  WiFiClient net;
  const uint16_t PORT = 1883;
#else
  const char* BASE = "home1";
  const char* CLIENT_ID = "YOUR_CLIENT_ID";
  const char* MQTT_USER = "YOUR_USERNAME";
  const char* MQTT_PASS = "YOUR_PASSWORD";
  const char* ROOT_CA = R"EOF(
-----BEGIN CERTIFICATE-----
...paste the ISRG Root X1 certificate here...
-----END CERTIFICATE-----
)EOF";
  WiFiClientSecure net;
  const uint16_t PORT = 8883;
#endif

const int RELAY_PIN = 26;
const int REED_CLOSED_PIN = 27;
const int REED_OPEN_PIN = 25;
const bool RELAY_ACTIVE = HIGH;            // change to LOW for active-low modules
const unsigned long PRESS_MS = 500;
const unsigned long TRAVEL_TIMEOUT_MS = 30000;

PubSubClient mqtt(net);
String tState, tSet, tAvail;
String lastState = "";
unsigned long lastPressAt = 0;

String readDoor() {
  bool closed = digitalRead(REED_CLOSED_PIN) == LOW;
  bool open = digitalRead(REED_OPEN_PIN) == LOW;
  if (closed) return "closed";
  if (open) return "open";
  if (millis() - lastPressAt < TRAVEL_TIMEOUT_MS) return "opening/closing";
  return "unknown";   // stopped halfway or the open sensor is missing
}

void pressButton() {
  if (millis() - lastPressAt < 2000) return;   // ignore rapid repeats
  digitalWrite(RELAY_PIN, RELAY_ACTIVE);
  delay(PRESS_MS);
  digitalWrite(RELAY_PIN, !RELAY_ACTIVE);
  lastPressAt = millis();
}

void onMessage(char* topic, byte* payload, unsigned int len) {
  String cmd;
  for (unsigned int i = 0; i < len; i++) cmd += (char)payload[i];
  cmd.trim();
  cmd.toUpperCase();
  String door = readDoor();
  if (cmd == "OPEN" && door == "closed") pressButton();
  else if (cmd == "CLOSE" && door == "open") pressButton();
  else if (cmd == "PRESS") pressButton();
  // OPEN while open or CLOSE while closed is ignored on purpose
}

void connectMqtt() {
  while (!mqtt.connected()) {
    if (mqtt.connect(CLIENT_ID, MQTT_USER, MQTT_PASS, tAvail.c_str(), 1, true, "offline")) {
      mqtt.publish(tAvail.c_str(), "online", true);
      mqtt.subscribe(tSet.c_str(), 1);
      lastState = "";   // force a fresh retained state
    } else {
      delay(5000);
    }
  }
}

void setup() {
  pinMode(RELAY_PIN, OUTPUT);
  digitalWrite(RELAY_PIN, !RELAY_ACTIVE);
  pinMode(REED_CLOSED_PIN, INPUT_PULLUP);
  pinMode(REED_OPEN_PIN, INPUT_PULLUP);

  tState = String(BASE) + "/garage/door/state";
  tSet   = tState + "/set";
  tAvail = String(BASE) + "/garage/door/availability";

  WiFi.begin(WIFI_SSID, WIFI_PASS);
  while (WiFi.status() != WL_CONNECTED) delay(500);
#if !BENCH_TEST
  net.setCACert(ROOT_CA);
#endif
  mqtt.setServer(BROKER, PORT);
  mqtt.setCallback(onMessage);
}

void loop() {
  if (!mqtt.connected()) connectMqtt();
  mqtt.loop();

  static unsigned long lastCheck = 0;
  if (millis() - lastCheck > 200) {
    lastCheck = millis();
    String s = readDoor();
    if (s != lastState) {
      mqtt.publish(tState.c_str(), s.c_str(), true);   // retained state
      lastState = s;
    }
  }
}

A few details worth noticing:

  • OPEN only pulses the relay when the door reads closed, and CLOSE only when it reads open. Because one press toggles the motor, sending OPEN to an open door would otherwise close it.
  • Presses closer than two seconds apart are ignored, which stops a double-tapped button or a retried QoS 1 message from reversing the door.
  • The 200 ms polling acts as simple debouncing for the reed switch.

Testing on the Bench

Wire an LED with a resistor to the relay output (or just listen for the relay click), keep BENCH_TEST true and flash the board. Then, from a terminal:

B=connectmq/demo/g4k9t2
mosquitto_sub -h broker.connectmq.com -p 1883 -i mon-g4k9t2 -t "$B/garage/#" -v
mosquitto_pub -h broker.connectmq.com -p 1883 -i cmd-g4k9t2 -q 1 -t $B/garage/door/state/set -m PRESS

Move a magnet past the reed switch and watch the state change. Unplug the ESP32 and, after the keep-alive timeout, the broker publishes offline on the availability topic.

Do not connect the real door motor while using the public broker. Anyone who guesses your topic can read and publish there. Switch to private credentials first.

Going Live on a Private Broker

Set BENCH_TEST to false, paste the ISRG Root X1 certificate (the Let's Encrypt root), and fill in the username, password and client ID generated in the ConnectMQ portal. Now the opener connects over TLS on port 8883 and only clients with your credentials can command it. Our MQTT security best practices article explains why credentials and TLS are non-negotiable for anything that opens a door.

To control it from Home Assistant, add an MQTT cover entity pointing at the same topics, or follow the Home Assistant MQTT guide:

mqtt:
  cover:
    - name: "Garage Door"
      command_topic: "home1/garage/door/state/set"
      state_topic: "home1/garage/door/state"
      availability_topic: "home1/garage/door/availability"
      payload_open: "OPEN"
      payload_close: "CLOSE"
      payload_stop: "PRESS"
      state_open: "open"
      state_closed: "closed"
      state_opening: "opening/closing"
      qos: 1

Safety Checklist

  1. Keep the motor's own safety features (photo-eyes, auto-reverse) connected and working.
  2. Never retain command messages.
  3. Only act on OPEN/CLOSE when the sensor confirms the opposite state.
  4. Use TLS and private credentials before connecting the motor.
  5. Add a notification when the door has been open for longer than you expect.
  6. Mount the relay inside an enclosure away from moving parts.
  7. Test what happens when Wi-Fi drops mid-travel: the door must simply finish its movement.

For the "door left open" notification, the threshold alert rules in the ConnectMQ portal can flag values on a topic, or an automation in Home Assistant or Node-RED can watch for open lasting more than ten minutes.

Troubleshooting

The relay clicks but the door does not move. Your motor probably uses a digital wall panel. Wire the relay across the button of a spare remote instead.

State flickers between closed and unknown. The magnet is too far from the reed switch or the door rattles. Move them closer or add a longer debounce.

The door reverses when you send OPEN. You are sending PRESS, or the closed sensor is not reading LOW. Check the reed wiring with the serial monitor.

The device never comes back online. Check that the client ID is unique; two devices with the same ID kick each other off. On a private broker, also confirm you are using the exact client ID issued with your credentials, because client IDs are bound to them.

Commands arrive late or twice. QoS 1 guarantees at-least-once delivery, so a retried message can arrive after a reconnect. The two-second press lockout and the state checks in onMessage make duplicates harmless.

Control Your Garage Securely with ConnectMQ

A door opener deserves a private, encrypted broker rather than a public one. ConnectMQ is a fully managed MQTT service with TLS, generated credentials and a live topic explorer for watching your door state. Create a free Serverless broker at app.connectmq.com, no credit card required, flip BENCH_TEST to false and you are live. For hosting background, read about the MQTT broker in the cloud, and browse more projects such as water leak detection with IoT in our IoT automation guide.

Frequently Asked Questions

Will this work with any garage door motor?

It works with motors that accept a simple dry-contact wall button, which covers most residential openers. Motors with digital wall panels need a spare remote wired to the relay instead.

Why use a reed switch instead of tracking commands?

Commands can fail, the door can be stopped by an obstacle, or someone can use the regular remote. Only a sensor tells you where the door actually is.

Is it safe to control a garage door over MQTT?

It is reasonable when you use TLS, private credentials, non-retained commands and keep the motor's built-in safety reverse. Never connect a real door to a public test broker.

Can I add a second sensor for the fully open position?

Yes. The sketch already reads an optional open-position reed on GPIO 25, which turns "unknown" into a clear "open" state.

Related articles

Zigbee2MQTT Setup Guide: From Coordinator to Dashboard

Install Zigbee2MQTT with Docker, configure the coordinator and MQTT connection, pair and rename devices, control them over MQTT and view their states on a dashboard, testing first on a free public broker.

7 min read

All articles →

Your private MQTT broker is a few clicks away

Create an account, get your broker credentials and connect your first device — no servers to install, patch or scale.