Garage door controller.

ESP32 + ESPHome controlling two garage doors with relay pulses, reed switches reporting true state, and a Bluetooth proxy thrown in for free. Talks directly to Home Assistant.

A small board in the garage that opens, closes, and reports the state of two garage doors. About the size of a deck of cards, runs ESPHome, talks to Home Assistant over its native API, costs about $15.

LilyGO TTGO T-Relay — ESP32, four onboard relays, 4 MB flash, WiFi and Bluetooth, $15 from AliExpress.

Architecture

home assistantcover entities · automationslilygo TTGO T-RelayESP32 · ESPHome · BLE proxy2× pulse_relay scripts · 20ms reed debounceleft doorrelay (out) + reed (in)right doorrelay (out) + reed (in)native APISolid arrows out: relay pulses (open/close commands).Dashed arrows in: reed switch state (ground truth — the door's actual position).Same board doubles as a Bluetooth proxy for BLE temp sensors the rest of the network couldn't reach.
Two relays out, two reed switches in. Home Assistant sees one cover entity per door whose state reflects the actual door, not what was last commanded.

Why reed switches

A garage door controller without state detection is just a remote button — it doesn't know whether the door is open, only what command it last sent. Reed switches screwed to the door and into the garage floor give the ESP32 a direct read on physical state, which means Home Assistant can show you whether each door is actually open right now, not what someone asked it to do five minutes ago.

The ESPHome bit

Each door pairs a relay (output, fires the door opener button) with a reed switch (input, reads the actual position). A cover: template entity glues them together so Home Assistant sees one entity per door:

cover:
  - platform: template
    name: "Left Garage Door"
    device_class: garage
    lambda: 'return id(garage_door_sensor_left).state ? COVER_OPEN : COVER_CLOSED;'
    open_action:  { script.execute: pulse_relay_left }
    close_action: { script.execute: pulse_relay_left }
    stop_action:  { script.execute: pulse_relay_left }

The pulse_relay_* scripts are tiny — turn the relay on, sleep 500ms, turn it off — DRY-ed out so each door's open/close/stop actions reuse the same one. There's no real "open vs close" command at the relay level: a real garage door opener has a single button that toggles.

script:
  - id: pulse_relay_left
    then:
      - switch.turn_on: garage_door_relay_left
      - delay: 500ms
      - switch.turn_off: garage_door_relay_left

A 20ms delayed_on_off filter on each reed switch debounces flutter as the door starts moving — the magnet doesn't pass cleanly past the sensor at the edges, so without the filter Home Assistant would see the door rapidly opening and closing for a fraction of a second.

Bonus: Bluetooth proxy

The board sits in a little cabinet in the garage, and is connected to everything by some re-used CAT5 scraps. It also happens to be in range of two BLE temperature sensors. ESPHome has a built-in Bluetooth proxy that lets any ESP32 forward BLE advertisements up to Home Assistant — so the same board doing the doors also extends BLE coverage into a part of the house that was previously dead to it.

esp32_ble_tracker:
  scan_parameters:
    active: true

bluetooth_proxy:
  active: true

It's pretty cool how cheap and easy it is to hook a microcontroller up to things these days.