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:
OPENonly pulses the relay when the door readsclosed, andCLOSEonly when it readsopen. Because one press toggles the motor, sendingOPENto 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
- Keep the motor's own safety features (photo-eyes, auto-reverse) connected and working.
- Never retain command messages.
- Only act on
OPEN/CLOSEwhen the sensor confirms the opposite state. - Use TLS and private credentials before connecting the motor.
- Add a notification when the door has been
openfor longer than you expect. - Mount the relay inside an enclosure away from moving parts.
- 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.



