Most garage door and gate openers already have a wall button. Press it once and the door moves; press it again and it stops or reverses. An ESP32 with a relay can press that button for you, and two reed switches can tell Home Assistant whether the door is really open or closed. The parts cost little and the whole thing runs locally, with no cloud account from the opener's maker.

The wiring is the easy part. The hard part is that the opener has one button and no idea what you meant by pressing it. A command that opens a closed door can just as well close an open one, and a badly placed relay pulse after a reboot can start the door moving with nobody around. Most of this guide is about avoiding those two situations.

Check what your opener expects

The simple approach in this guide assumes an opener controlled by a single push button that just closes a contact. The behaviour described in the esphome-gdo project is typical: if the door is closed, one press opens it; if it is open or partly open, one press closes it; a press while opening stops it; a press while closing opens it. Your opener may differ, especially in what a press does in the middle of travel, so watch it for a few cycles with the wall button before you automate anything.

Not every opener accepts a plain contact closure. The same project notes that on Chamberlain Security+ 2.0 openers the relay has to press the physical button on the wall control panel, while on Security+ 1.0, Genie and similar openers you can short the control terminals on the opener itself. For Chamberlain and LiftMaster openers, the ratgdo board with its ESPHome firmware speaks the Security+ 1.0 and 2.0 protocols directly and is a better fit than a bare relay.

Look for the push-button input in the opener's manual. Many openers label a pair of terminals for an external button or key switch. That is where the relay contacts go, in parallel with the existing button, which keeps working as before.

Parts and wiring

You need:

  • an ESP32 board supported by ESPHome;
  • a relay module, or a board with a relay on it, that switches reliably from a 3.3 V GPIO signal;
  • two magnetic reed switches with magnets, one for the fully closed position and one for fully open;
  • a 5 V power supply near the opener and some low-voltage cable.

Connect the relay's normally open (NO) and common (COM) contacts to the opener's push-button terminals. The relay contacts are dry contacts: the ESP32 never shares a circuit with the opener's control input.

Each reed switch has two wires. Connect one to a GPIO pin and the other to GND. With the internal pull-up enabled, the pin reads high when the switch is open and low when the magnet closes it. Mount the switch on the frame and the magnet on the door so that they meet exactly at the end position you want to detect. If the door overshoots or bounces at the end, move the switch until the contact stays closed when the door has settled.

Pick the relay pin carefully

The ESPHome docs for the GPIO switch carry a warning worth reading twice: at reset, before any ESPHome code runs, relay GPIO pins may have pull-ups active, so the relay may switch on before ESPHome can turn it off. On a light that means a short flicker. On a garage door it can mean a press of the button every time the board reboots or receives an update.

So keep the relay off the strapping pins (Espressif lists GPIO0, 2, 5, 12 and 15 on the original ESP32) and off any pin that your board's documentation shows toggling at boot. Then test it: with the opener disconnected, power-cycle the board a dozen times and flash an update over the air while watching the relay LED. If it ever clicks during start-up, pick another pin before you connect the opener.

ESPHome configuration

Start from the configuration created by the ESPHome Device Builder wizard (Wi-Fi, logger, API with encryption, OTA) and add the parts below. The pins are examples for a classic ESP32 development board, and the travel times are placeholders: time your own door from fully closed to fully open with a stopwatch.

switch:
  - platform: gpio
    pin: GPIO25
    id: door_relay
    restore_mode: ALWAYS_OFF
    on_turn_on:
      - delay: 500ms
      - switch.turn_off: door_relay

binary_sensor:
  - platform: gpio
    id: door_closed
    name: "Garage door closed contact"
    pin:
      number: GPIO26
      mode:
        input: true
        pullup: true
      inverted: true
    filters:
      - delayed_on_off: 200ms
  - platform: gpio
    id: door_open
    name: "Garage door open contact"
    pin:
      number: GPIO27
      mode:
        input: true
        pullup: true
      inverted: true
    filters:
      - delayed_on_off: 200ms

cover:
  - platform: feedback
    name: "Garage door"
    device_class: garage
    has_built_in_endstop: true
    open_duration: 18s
    close_duration: 18s
    open_endstop: door_open
    close_endstop: door_closed
    open_action:
      - if:
          condition:
            binary_sensor.is_off: door_open
          then:
            - switch.turn_on: door_relay
    close_action:
      - if:
          condition:
            binary_sensor.is_off: door_closed
          then:
            - switch.turn_on: door_relay
    stop_action:
      - switch.turn_on: door_relay

What each part does:

  • The relay is a momentary switch. The on_turn_on block turns it off again after 500 ms, the same pattern the ESPHome docs use for a single-button gate remote. Because the switch has an id but no name, it stays internal and does not appear in Home Assistant, so nobody can leave it switched on from a dashboard. ALWAYS_OFF is the default restore mode; writing it out makes the intent clear.
  • The reed switches are inverted, so a closed contact (magnet present) reads as ON. The delayed_on_off filter only passes a state that has held for 200 ms, which hides contact bounce while the door shakes into position.
  • The feedback cover combines the end-position sensors with timing. Between the end positions it estimates the door's position from how long it has been moving.

Why has_built_in_endstop matters here

Without this option, the feedback cover runs stop_action when the door reaches an end position or when the travel time runs out. With a separate stop motor that is correct. With a single button, stop_action is another press, so the door would arrive at the end position and immediately be sent back the other way. With has_built_in_endstop: true, ESPHome lets the opener stop by itself and does not press anything at the end.

The same option has a side effect: the docs say that open and close commands are then performed without checking the current state. A close command sent to a door that is already closed would press the button and open it. Home Assistant's own controls hide that button when the state is known, but an automation or a voice command does not care. The if blocks in open_action and close_action add that check on the ESP32 itself: if the matching contact already reports the end position, nothing happens.

Limits of a single-button setup

Know what this configuration can and cannot tell. At the end positions it is reliable, because a reed switch confirms the state. In between it is guessing.

  • Stop presses the button. If the door is already standing still halfway, a stop command starts it moving again.
  • Commands in mid-travel depend on the opener. When you press Close while the door is opening, the opener may stop, reverse or ignore it, and the cover's idea of the direction can drift from reality. The feedback cover has a direction_change_wait_time option that inserts a stop action before reversing, which matches openers that stop on the next press, but test it against your own door.
  • Someone may use the wall button or a remote. The end-position contacts catch that when the door reaches an end. If it stops in the middle, the state is unknown until it reaches an end again.

If you want reliable partial positions with one button, look at the esphome-gdo package, which models the press sequence and can calculate the number of presses needed, or juaigl's external component, which does something similar. For most garages, open and closed are all that matters, and two reed switches handle that well.

In Home Assistant

The ESP32 shows up under Settings > Devices & services as an ESPHome device. With device_class: garage, Home Assistant uses garage door icons and offers garage door triggers and conditions, such as Garage door opened, in the automation editor. The two contact sensors are useful on their own, for example in a history graph or as a backup signal in an automation.

A useful first automation tells you when the door has been left open:

alias: "Garage door left open"
triggers:
  - trigger: state
    entity_id: cover.garage_door
    to: "open"
    for: "00:15:00"
actions:
  - action: notify.mobile_app_your_phone
    data:
      message: "The garage door has been open for 15 minutes."

Replace cover.garage_door and the notify action with your own entity and phone. Closing the door automatically at a certain hour is easy to add, but think about who or what might be under it at that moment.

Security and safety

A device that opens the house is not the place for shortcuts.

  • Keep the API encryption key and OTA password that the wizard generates. Do not add the ESPHome web_server component without authentication, because anyone on the same network could press the button from a browser.
  • Be careful with voice assistants. Home Assistant requires you to expose entities to Assist, Google Assistant or Alexa explicitly, and its documentation names garage doors as one of the reasons. Leave the cover unexposed unless you really need it.
  • Remote closing without seeing the door is a risk. The relay does the same thing as the wall button, so the opener's own safety features should still behave as they do when you press the button, but check that they work before you rely on them. A camera pointed at the door helps when you close it from away.
  • Mains wiring stays with the opener. Everything in this project runs on low voltage. If the only place to tap the button input is next to mains terminals, cut the power to the opener first or get someone qualified to do it.

Testing before you trust it

  1. With the relay disconnected from the opener, reboot the ESP32 several times and update it over the air. The relay must not click.
  2. Move the door by hand or with the wall button and check in the ESPHome logs that both contact sensors change at the right moment.
  3. Connect the relay and run open and close from Home Assistant from both end positions.
  4. Send a close command to a door that is already closed (for example in Settings > Tools > Actions with cover.close_cover). Nothing should happen.
  5. Interrupt a cycle with the wall button and watch what state Home Assistant shows, so you know how your opener and the cover behave in that case.