Write-up

Garage Door Left-Open Alert

A $10 ESP32 that pushes a phone alert when the garage door has been left open — and the engineering case for deliberately scoping it down to monitoring only.

July 2026 ESP32ESPHomeIoTEmbedded

An ESP32 that watches whether my garage door is open and pushes a notification to my phone if I’ve left it that way. No hub, no cloud platform, no always-on server — a $10 microcontroller, a magnetic reed switch, and about forty lines of YAML.


The problem

I have a remote-controlled overhead garage door (ATA GDO-9v2 Enduro Gen 2). The failure mode is mundane and entirely human: I open it, unload the car, walk inside, and forget. Sometimes it stays open overnight.

The door itself works perfectly. The gap is feedback — once I’m inside the house, the system gives me no indication of its own state. There’s no annunciator, no status anywhere I’d naturally look.

So the requirement was never “automate the door.” It was narrower than that:

Tell me when the door has been open longer than it should be, wherever I am.

Worth stating plainly, because it determined most of what follows. An earlier version of this design included a relay wired into the opener’s O/S/C terminal so I could close the door remotely from my phone. It worked on paper and I’ve documented it below as a deliberate non-goal. I dropped it, for two reasons:

  1. It solved a problem I didn’t have. The issue was never that closing the door was hard — it was that I didn’t know it was open. Adding an actuator doubled the complexity to address the easy half of the problem.
  2. Anything that can move the door carries real safety weight. The manufacturer makes photo-electric safety beams compulsory for auto-close operation, and rightly so — an unattended closing door is a hazard to people, pets, and vehicles. Taking the actuator out means this system is, by construction, incapable of causing harm. It can only observe.

Constraining scope to monitoring made the whole thing simpler, cheaper, and safer. That trade is the most interesting engineering decision in the project.


Design

Architecture

Reed switch on door + magnet closed = door down ESP32 · ESPHome ▹ read reed (GPIO13) ▹ timer: open > 15 min ▹ night check after 22:00 ▹ HTTP POST → Pushover Pushover cloud push APNs / FCM delivery + retry alert wire Wi-Fi push
Three hops — the ESP32 holds all the logic; Pushover handles delivery to a phone that may be asleep or away from home.

Three hops. The ESP32 holds all the logic; Pushover handles the push-delivery plumbing (APNs/FCM registration, retry, delivery to a device that may be asleep or off-network) that a microcontroller has no business trying to do itself.

Why this shape

Why not Home Assistant? The obvious route is ESP32 → Home Assistant → Companion app. It’s a good architecture and I’d use it if I were building out a wider smart-home setup. But it needs an always-on machine — a Pi, a NUC, a NAS container — running a fairly large piece of software, purely to relay one boolean to my phone. For a single-signal appliance that’s a lot of standing infrastructure and a lot of surface area to maintain.

Why not a bare ESP32 with no service at all? Because push notification is genuinely hard. Reaching a phone that’s asleep, on cellular, behind CGNAT requires a registered app and a persistent connection to Apple’s or Google’s push infrastructure. Pushover is a thin, cheap, one-time-purchase shim over exactly that. It’s the smallest possible dependency that solves the actual hard part.

Why a reed switch rather than sensing the opener? The reed reports the physical position of the door, which is the thing I care about. Inferring state from the opener’s electronics means inferring — and it means wiring into the opener. A magnet on the door panel is unambiguous and completely passive.

State model

The reed switch is a plain mechanical contact: closed when the magnet is adjacent (door shut), open otherwise. ESPHome reads it with an internal pull-up on an interrupt-driven GPIO.

PhysicalContactReported state
Door shutClosed (magnet near)OFF
Door openOpen (magnet away)ON

A delayed_on_off: 500ms filter debounces it, so door rattle and vibration don’t produce spurious transitions.

Alert logic

Three independent behaviours:

That last one matters more than it looks. A monitoring device that fails silently is worse than no device, because you keep trusting it. The heartbeat converts silent failure into something observable.


Sequence — from open to alert

The logic the ESP32 runs continuously. Two independent triggers fire an alert — the open-too-long timer and the nightly check — and closing the door cancels a pending timer outright.

Door Reed switch ESP32 Pushover → Phone you open it magnet moves away contact opens → state = OPEN start 15-min timer (restart) closed before 15 min → cancel, no alert still OPEN at 15 min POST “Door open 15 min” — repeats every 15 min 📲 phone buzzes nightly 22:00 check if still OPEN → second alert
Two independent triggers, one cancel path. The teal flow is sensing/logic, amber is network/push, and the red state is the one that actually fires.

Bill of materials

ItemCost (AUD)Notes & source
ESP32-WROOM-32 dev board$0–13I used a Freenove board I already had; any WROOM-32 works. Core Electronics
Magnetic reed switch (door contact)$4–8Surface-mount body + separate magnet. A wide-gap “overhead door” variant tolerates panel movement better. Core: door sensor · wide-gap on eBay
5 V USB supply + cable$0–12Any phone charger, 5 V ≥ 1 A. Core: USB supplies
Hook-up wire (~3 m)$5Two-core, to reach from the board to the door. Core: hook-up wire
Enclosure$0Temporary cardboard box for now; a sealed plastic enclosure is the proper call — garages are dusty and damp. Core: enclosures
Pushover licence~$8 onceOne-time per platform, no subscription. pushover.net

Total: roughly $20–40, less if the board and charger come out of a drawer.

On the connections: right now this runs on jumper wires into a cardboard box — fine to prove the idea out, but bare stranded wire in a Dupont jumper is unreliable over months of vibration and temperature swing, and it’s the joint most likely to fail silently. For a permanent install, move to a screw terminal or solder the joints, in a sealed enclosure.


Wiring

The entire circuit is two wires. The reed switch has no polarity — either leg to either connection.

5 V USB charger any phone plug ≥ 1 A ESP32-WROOM dev board USB-C — power + first flash internal pull-up enabled in firmware 5V / VIN GND GPIO13 GND Reed switch 2 legs · no polarity leg A leg B
The whole circuit is two signal wires plus USB power — signal (GPIO13), ground, 5 V in.

The ESP32’s internal pull-up holds GPIO13 high; closing the reed pulls it to ground. No external resistor, no breadboard, no soldering on the board side — dev board headers come pre-soldered.

Mounting: switch body on the fixed frame or track, magnet on the moving door panel, aligned so they sit within the switch’s gap spec when the door is fully closed.


Build

1. Pushover

Install the app, create an account, and collect two values from the dashboard:

2. ESPHome

ESPHome runs standalone; it does not require Home Assistant. On macOS:

brew install esphome
esphome version

ESPHome is a code generator, not a runtime. It compiles the YAML into C++, builds it with the ESP-IDF toolchain, and flashes a native binary. Nothing of ESPHome remains on the chip, and nothing needs to keep running on your laptop afterwards.

3. Flash

Save the configuration below as garage-door.yaml, substituting your Wi-Fi credentials and the two Pushover values (the token/key pair appears in three places).

esphome run garage-door.yaml

Choose the USB serial port for the first flash. On macOS it appears as /dev/cu.usbserial-XXXX or /dev/cu.wchusbserial-XXXX. If the port doesn’t appear, install the CH340 driver. If it stalls at “Connecting…”, hold the board’s BOOT button.

Every subsequent update goes over the air — pick the OTA option instead and the firmware travels over Wi-Fi. The cable is needed exactly once.

4. Verify polarity

This is the step worth being careful about, because getting it backwards produces a system that alerts when the door is closed and stays silent when it’s open — a failure that looks like it’s working.

With the reed mounted and the door shut, stream the logs:

esphome logs garage-door.yaml

The state must read OFF. If it reads ON, flip inverted: in the pin config and re-flash over the air.

Reed switches come in normally-open and normally-closed varieties, and there’s no reliable way to tell by looking. The mounted-door observation settles it regardless of which type you have.

5. Bench-test the timers before trusting them

Temporarily set both delay: 15min values to 30s, push over the air, and confirm two things:

A repeat loop that won’t terminate is the failure mode to rule out before this goes on a wall. Then set both back and push again.


Configuration

Save this as garage-door.yaml. Before flashing, replace the four placeholders with your own values — everything in UPPER_CASE below:

PlaceholderReplace withWhere to find it
YOUR_WIFI_NAMEYour Wi-Fi network name (SSID)Your router / the network you connect to
YOUR_WIFI_PASSWORDYour Wi-Fi passwordSame
YOUR_API_TOKENThe Pushover application API tokenpushover.net → create an application
YOUR_USER_KEYYour Pushover user keypushover.net dashboard (shown on login)

The Wi-Fi pair appears once; the token/user-key pair appears three times (the two-alert and heartbeat POST bodies) — replace it in all three, or the alerts won’t send.

esphome:
  name: garage-door

esp32:
  board: esp32dev

wifi:
  ssid: "YOUR_WIFI_NAME"
  password: "YOUR_WIFI_PASSWORD"

logger:
api:
ota:
  - platform: esphome

http_request:
  useragent: esphome/garage
  timeout: 10s

# ---------- door sensor ----------
binary_sensor:
  - platform: gpio
    id: reed
    name: "Garage Door"
    device_class: garage_door
    pin:
      number: GPIO13
      mode:
        input: true
        pullup: true
      inverted: false
    filters:
      - delayed_on_off: 500ms
    on_press:
      - script.execute: open_timer
    on_release:
      - script.stop: open_timer

# ---------- open too long, repeats until closed ----------
script:
  - id: open_timer
    mode: restart
    then:
      - delay: 15min
      - while:
          condition:
            binary_sensor.is_on: reed
          then:
            - http_request.post:
                url: "https://api.pushover.net/1/messages.json"
                request_headers:
                  Content-Type: application/x-www-form-urlencoded
                body: 'token=YOUR_API_TOKEN&user=YOUR_USER_KEY&title=Garage&message=Door still open'
            - delay: 15min

# ---------- night check + daily silent heartbeat ----------
time:
  - platform: sntp
    on_time:
      # 22:00 — alert if still open
      - seconds: 0
        minutes: 0
        hours: 22
        then:
          - if:
              condition:
                binary_sensor.is_on: reed
              then:
                - http_request.post:
                    url: "https://api.pushover.net/1/messages.json"
                    request_headers:
                      Content-Type: application/x-www-form-urlencoded
                    body: 'token=YOUR_API_TOKEN&user=YOUR_USER_KEY&title=Garage&message=Door still open after 10pm'

      # 09:00 — silent liveness ping
      - seconds: 0
        minutes: 0
        hours: 9
        then:
          - http_request.post:
              url: "https://api.pushover.net/1/messages.json"
              request_headers:
                Content-Type: application/x-www-form-urlencoded
              body: 'token=YOUR_API_TOKEN&user=YOUR_USER_KEY&title=Garage&message=Sensor alive&priority=-2'

Notes and possible extensions

Explicitly not done: remote close. The opener’s terminal block exposes an O/S/C input — a momentary dry contact that a relay could drive, giving remote open/stop/close. I designed it, then removed it. Beyond the safety argument above, there’s a subtlety worth recording: the O/S/C input is not a “close” command but a state-machine toggle. One pulse from closed opens; a pulse mid-travel stops; the next reverses. Any implementation therefore has to read the door’s actual state before acting and confirm the result afterwards, or it can leave the door stopped halfway. That interlock is straightforward to write and easy to get subtly wrong, which is a reasonable argument for not writing it at all when the feature wasn’t needed.

Better liveness monitoring. The daily heartbeat is self-reported, which means a device that’s completely dead can’t tell you it’s dead — you have to notice the absence yourself. The stronger pattern inverts it: ping an external watchdog (Healthchecks.io or similar) on a schedule and let that service alert on a missed check-in. Pushover has this as “Inactivity Monitors” but only on its Teams tier.

Wi-Fi coverage. Check signal strength at the actual mounting position, not on the bench. Anything better than about −70 dB is comfortable.

Power. No battery backup. A power cut means no alerts until it returns; the board rejoins Wi-Fi automatically when it does. The heartbeat is what surfaces this.


Stack

ESP32-WROOM-32 · ESPHome 2026.7 · ESP-IDF 5.5 · Pushover · magnetic reed switch