Skip to main content

Ditching the Garage Door Subscription

The Project #

My garage door opener is made by Chamberlain, and Chamberlain would very much like me to pay them a monthly fee for the privilege of opening my own garage door from my phone. Their myQ platform routes every command through their cloud, and they’ve spent the last few years actively locking out third-party integrations that used to work for free (including Amazon Alexa and my beloved Home Assistant).

I declined. Instead, I built my own system: a keypad and RFID reader at the garage entrance, backed by a real credential database with per-user codes, time-based access rules, and a complete audit log. It runs entirely on hardware I own, integrates with the Home Assistant setup I already have, and costs me nothing per month (well, not nothing, but I don’t give any money to Chamberlain and I think what I do spend is worth it).

Here’s how it works.

The keypad and RFID reader mounted at the garage entrance.


Taking back the opener with RatGDO #

The foundation of the whole thing is RatGDO, a small ESP-based board that wires directly into the garage door opener’s control terminals. It speaks the opener’s own wire protocol, which means it gets real state information (door position, obstruction detection, motion, lock status). It is not just a dumb relay closure that hopes for the best.

I run the ESPHome firmware on mine, so it shows up in Home Assistant as a native device: a door entity with actual open/closed/opening/closing states, plus sensors for everything else the opener knows. From the moment it was installed, I had full local control of the door from Home Assistant and the phone app. No cloud, no fee.

The opener itself has built-in Wi-Fi. That’s how it wants to reach myQ. I’ve deliberately never connected it. It sits there with no network, no account, and no way to phone home. Whatever telemetry it was designed to send about when my door opens and who’s coming and going, it can’t. RatGDO gets everything it needs over the wire, so the opener’s radio is just dead weight that I’m happy to leave dead.

That alone kills the subscription. But a phone is a lousy way to get into the garage when your hands are full of groceries, and I wanted to give family members and the occasional contractor/guest/house sitter access without handing out a shared secret that I’d have to change every time someone moved on. (We all know how those “temporary” codes become permanent.) That meant building an entry system.


The entry point: Wiegand keypad and an ESP32 #

At the garage entrance there’s a weatherproof keypad with an integrated RFID reader. These readers are commodity access-control parts, and most of them speak Wiegand (a time-tested two-wire protocol from the 1980s that clocks out key presses and card reads as pulses on a pair of data lines). It’s simple, it’s easy to source, and ESPHome supports it natively.

The reader’s Wiegand lines connect to an ESP32 (mounted inside the garage) running ESPHome. The ESP32 does two jobs:

  • The Wiegand component decodes card scans and individual key presses.
  • The key collector component assembles key presses into a complete PIN: # submits the code, * clears it, and a timeout wipes any half-entered code automatically.

Here’s the shape of it (pins and details omitted):

wiegand:
  - id: reader
    d0: GPIOxx
    d1: GPIOxx
    on_tag:
      - mqtt.publish:
          topic: garage-entry/keypad/tag
          payload: !lambda "return x;"

key_collector:
  - id: pin_input
    source_id: reader
    min_length: 4
    max_length: 6
    end_keys: "#"
    back_keys: "*"
    on_result:
      - mqtt.publish:
          topic: garage-entry/keypad/code
          payload: !lambda "return result;"

It is important to note what the ESP32 doesn’t do. It doesn’t know any codes, and it doesn’t decide anything. It’s a dumb terminal. A PIN entry or a tag scan just gets published to MQTT, and the decision happens elsewhere. If someone pried the keypad off the wall, they’d just have a keypad and a mess of wire. Even the ESP32 “brain” is just a microcontroller with no secrets on it.


The messaging layer: MQTT #

Everything the keypad says and hears travels over MQTT (Message Queue Telemetry Transport), the same lightweight pub/sub protocol that glues together other parts of my home automation. A Mosquitto broker runs on my home server, and it’s locked down the way a broker handling door-access events should be: anonymous connections are disabled outright, every client authenticates with its own credentials, and the broker is reachable only from inside my network. Nothing about it is exposed to the internet.

The topic layout keeps the roles distinct. The keypad publishes attempts to its own topics (garage-entry/keypad/code for PINs, garage-entry/keypad/tag for RFID scans) and subscribes to a result topic so it can give feedback at the door. The authentication service listens on the attempt topics and publishes on the result topic. Neither side can do the other’s job, because neither side has access to the other’s topics. MQTT’s small, dumb, fire-and-forget messages are exactly the right shape for this. An access attempt is a small, dumb, fire-and-forget message.

It’s also fast. The hop from keypress to broker to authentication decision and back happens in well under a second, all on the LAN. There’s no round trip to anyone’s cloud. This matters at a door, where “the vendor’s API is having a bad day” is not an acceptable reason to be standing in the rain. I tap my keytag, the keypad beeps, and the door opens. The delay is no more than the delay when clicking the wireless remote from the car. That is to say it is imperceptible.


The brain: Node-RED and a real database #

Every access attempt lands on the broker and gets picked up by Node-RED, which runs the authentication flow against a MariaDB database (it could be any SQL database).

I could have hardcoded a list of valid codes in the ESPHome YAML and been done in an afternoon. But hardcoded codes are exactly the shared-secret problem I was trying to avoid. I wanted the things a commercial access-control system has:

  • Users own credentials. Each person has their own profile, and each profile can have a PIN, an RFID tag, or both. Nobody shares a code.
  • Time-based rules. A credential can be limited to certain days of the week and hours of the day. For example, the cleaning service’s tag could be set to work during their window and be inactive the rest of the time.
  • Validity dates. A contractor or house sitter gets a code that starts and expires when I specify, automatically. No remembering to revoke anything.
  • Kill switches at two levels. I can disable a single credential (lost tag) or an entire user (everything they have) with one toggle.

The Node-RED flow normalizes the input, stamps it with a correlation ID, and runs a single parameterized SQL query that checks all of the above in one shot: credential enabled, user active, inside the date window, right day, right hours. If a row comes back, access is allowed. If not, it is denied. Either way, the attempt is logged.

graph TD kp["Keypad + RFID reader
(Wiegand → ESP32, ESPHome)"] mqtt["MQTT broker
(Mosquitto)"] nr["Node-RED
(auth flow)"] db[("MariaDB
users / credentials / access log")] ha["Home Assistant"] gdo["RatGDO
(wired to opener)"] door["Garage door"] kp -->|"PIN / tag attempt"| mqtt mqtt --> nr nr <-->|"parameterized query"| db nr -->|"log every attempt"| db nr -->|"result (allow/deny)"| mqtt nr -->|"on allow: press toggle button"| ha ha --> gdo gdo --> door

One-way trust: the keypad reports attempts, the server decides, and only an allow ever reaches the door.

On an allow, the result goes back out over MQTT so the keypad can beep and blink appropriately and, more importantly, Node-RED tells Home Assistant to open the door. Which brings us to who actually holds the keys.


Home Assistant opens the door #

It would be easy to read the diagram above and think Node-RED runs the show, but Node-RED only decides. Home Assistant controls the door. RatGDO belongs to Home Assistant. It’s a native ESPHome device there, and HA owns its entities: the door itself, the toggle button, the obstruction and motion sensors, the lock. Every way of operating this door, whether it’s the keypad, the HA phone app, a dashboard button, or an automation, ultimately goes through Home Assistant. There is exactly one component in the house with the authority to move the door, and it’s the one I already trust to run everything else.

So when authentication succeeds, Node-RED doesn’t control the opener. It makes an authenticated service call to Home Assistant over HA’s WebSocket API. This call is server to server, entirely on the local network, using an access token that lives on the Node-RED side and never goes anywhere near the keypad or the ESP32. Home Assistant receives the call, presses the RatGDO’s door-toggle button entity, and RatGDO pulses the opener over its hardwired connection. That’s the only path from a keypad press to the door moving, and it only fires after authentication succeeds.

Keeping HA as the single point of control has a nice side effect: the keypad is just another door-opening client, no more privileged than my phone. All the door logic I’d want anyway (e.g., “alert me if the door has been open for twenty minutes,” “close it automatically at 10pm,” “don’t close on an obstruction”) lives in one place and applies no matter how the door was opened. I can (and do) have a local automation that reminds me if the garage door was left open for too long and Chamberlain knows nothing about it. (Well, I guess they do if they read this, but they cannot collect data on the usage of my door.)


Taking the cloud out of the loop #

Trace the whole path end to end: Wiegand wires to the ESP32, ESP32 to Mosquitto over the LAN, Mosquitto to Node-RED, Node-RED to MariaDB, Node-RED to Home Assistant over the LAN, Home Assistant to RatGDO over the LAN, RatGDO hardwired to the opener. Every hop is inside my house.

If my internet connection dies, the keypad works. If Chamberlain’s cloud has an outage (as it does more than it should, judging by the myQ subreddit), the keypad works. If Chamberlain decides to change their API, deprecate their app, or double their subscription price, nothing in this system notices, because nothing in this system has ever spoken to them. I don’t even have the GDO’s Wi-Fi connected to anything. The only infrastructure this depends on is my local infrastructure. As long as the LAN is up, the door opens. And unlike some recent incidents of IoT devices refusing to work at all during an internet outage, I can always just press the button to open the garage door.

I’m not just preventing another insidious monthly “micro” transaction; I am declaring my digital sovereignty. myQ doesn’t just cost a monthly fee. It puts a vendor’s business decisions between me and my garage.


The audit log #

Every attempt (allowed or denied) is written to an access log in the database with a timestamp, the credential type, the result, a reason code, and the correlation ID from the original event. The reason codes help me understand what’s happening with the system:

  • ALLOW — matched a credential and passed every check.
  • NO_MATCH — no valid credential. Could be a typo, could be someone guessing.
  • MISMATCH — the checks disagreed with each other, which should never happen and would indicate a bug rather than a bad code.

That last one is a built-in assertion. It has stayed at zero, which is exactly what I hoped. Even though I view the results in a table, because everything is in SQL, questions like “show me every denied attempt this week” or “when did the contractor’s code last get used” are one custom query away.


Managing it from my phone #

In my first iteration, adding a user meant SSHing into the server and manually typing INSERT statements. That’s fine for an initial setup, but does not cut it long term. So I built a small Flask web app (creatively named the GDO Credential Manager) that fronts the database. I can add users, attach credentials, set day/time windows and validity dates, toggle things on and off, and browse the access log from my phone.

Like the phone remote from my office sign project, it’s reachable only over my Tailscale tailnet. It has no public exposure at all. It is not on the internet, not on the regular home LAN, and certainly not accessible by Chamberlain. If you’re not on the tailnet, the service doesn’t exist.


Security #

I’m deliberately not publishing a wiring diagram of my own attack surface, but here are the highlights.

  • The device at the door is trustless. The keypad and ESP32 hold no credentials and make no decisions. All secrets and all logic live on a server inside the house.
  • Authentication is centralized. One SQL query, checked at the moment of the attempt. There are no cached credentials.
  • Least privilege everywhere. The keypad can publish attempts and receive results; it cannot press the door button. Only the server-side flow can, and only on a verified allow.
  • Everything is logged. Denials are more interesting than allows, and I can see all of them.
  • Management is unreachable. The admin UI lives on an overlay network that outsiders can’t route to, let alone log into.

Compare that to the alternative I declined: a cloud service where the vendor controls access to my house, sees every open and close, and can (and did, for many users) change the rules whenever their business model shifts.


Was it worth it? #

The monthly fee I’m avoiding is a few dollars. I could tell myself that the parts (RatGDO board, ESP32, reader, and the server capacity I already had) paid for themselves within the first year, but that’s not the point (and might not be entirely accurate…). The point is that my garage door now has better access control than the subscription would have given me: individual credentials, scheduled access, instant revocation, and a complete audit trail, all running on hardware I own and can fix.

Also, tapping an RFID fob on my own homebuilt access control system and hearing the door start moving never stops being satisfying.


Built on: RatGDO, ESP32, ESPHome, a Wiegand keypad/RFID reader, Mosquitto, Node-RED, MariaDB, Flask, Home Assistant, Tailscale.