Wiegand Keypad Garage Door — Thin-Edge ESPHome Reader

Wiegand Keypad Garage Door — Thin-Edge ESPHome Reader
Contents

Overview

An ESPHome config for a Wiegand keypad reader for a garage door. The design is built around a thin-edge philosophy — the device’s only job is to capture input and relay it upstream to Home Assistant, with no local secret storage or access-control logic on the device itself.

The reader originally ran on its own dedicated ESP32 mounted in the keypad enclosure. It’s since been folded into the ESP32 already running the garage’s RATGDO board — see Hardware Evolution below for why and how.

Wiegand Device Compatibility

This config works with any Wiegand-compatible reader — keypads, RFID readers, or combo units. However, many low-cost devices ship in a host mode where they store PINs internally and control a relay output directly. These devices need to be reconfigured to Wiegand output mode before they’ll work here. The reconfiguration process varies by device but typically involves a programming card sequence or a DIP switch. Once in Wiegand mode the device stops managing access locally and simply outputs credentials on the D0/D1 data lines — exactly what this config expects.

How It Works

The wiegand component reads raw input from a Wiegand-protocol keypad (or RFID reader) connected on two GPIO pins (D0/D1), handling raw key presses, tag reads, and raw bit streams. On top of that, the key_collector component assembles individual key presses into a complete 4-digit PIN:

  • # — enter / submit
  • *# — clear / reset
  • 4-second inactivity timeout

When a complete PIN is entered (or a tag is scanned), the value is published to a text_sensor called Keypad Code, which exposes it to Home Assistant. After a 500ms delay to ensure HA picks up the value, the sensor is cleared back to "0" to prevent duplicate processing.

That’s the full extent of what the device does — collect the credential and send it.

Design Intent

All actual logic — PIN validation, authorization decisions, door actuation — lives in Home Assistant automations, not on the ESP32. This approach:

  • Avoids storing PINs or secrets in ESPHome firmware (where they’d sit in plaintext flash memory)
  • Centralizes access control where it can be audited and changed without reflashing hardware
  • Keeps the edge device simple and easily replaceable

On the original standalone reader, the ESP32 CPU was clocked down to 160 MHz (from the default 240 MHz) to reduce power consumption, since the workload was minimal. Now that the reader shares an ESP32 with the RATGDO firmware, that workload assumption no longer holds — see below.

Hardware Evolution: From a Standalone Reader to a RATGDO Integration

The original build ran the Wiegand reader on its own ESP32, mounted inside the keypad’s outdoor enclosure alongside the RFID module and a 5V-to-12V boost converter that powered them. D0/D1 were wired straight to the ESP32’s GPIOs with no level shifting at all — since the keypad idles those lines at 5V against 3.3V-only-tolerant pins, the chip’s internal ESD clamp diodes were conducting continuously just to survive the idle state, a steady voltage stress on the pins. That ESP32 eventually failed and needed replacing; prolonged outdoor temperature extremes may have played a part too, though the enclosure was weatherproof and shielded, so that cause isn’t fully confirmed — the direct 5V-to-3.3V wiring is the more concrete suspect.

The replacement coincided with a second, larger change: the garage door itself moved from a Meross MSG100 smart door controller/monitor to a RATGDO board running the community ratgdo ESPHome component. Since the RATGDO already runs its own ESP32 inside the garage, that migration was the natural point to fold the keypad reader into it rather than replacing the failed standalone ESP32 in place.

RATGDO board wired with resistor-divider level shifting for the Wiegand keypad, mounted in the garage door opener housing

The Wiegand D0/D1 lines (idling at 5V) go into the RATGDO’s 3.3V-logic ESP32 through resistor voltage dividers — the small resistor cluster visible on the board above. The 5V-to-12V boost converter that powers the keypad’s RFID reader and backlight also moved out of the keypad enclosure, onto an inline pigtail near the RATGDO. That leaves the keypad end of the run with no active electronics at all — just a simple 4-wire cable (Wiegand D+/D-, 5V, and 12V) back to the pigtail.

This consolidation also fixed a problem the original layout had been quietly living with: with the ESP32 and the boost converter both crammed into the keypad enclosure right next to the RFID reader, the ESP32’s Wi-Fi saw occasional packet loss — most likely RF interference from one of its close neighbors. Moving the active electronics out to the RATGDO removed both the interference source and the weather exposure in one move.

In hindsight: resistor dividers work, but a single series diode per line (cathode toward the keypad, anode toward the GPIO) is simpler and doesn’t load the open-collector line. See One Diode to Read a 5V Wiegand Keypad on a 3.3V ESP32 for the technique — some RATGDO GPIO inputs may already carry their own clamping-diode protection, which would make even that external diode unnecessary.

Current Firmware

The RATGDO now runs both the door controller and the keypad reader from one ESPHome config, composed from packages:

substitutions:
  esphome_name: garage-door
  friendly_name: Garage Door

packages:
  network: !include .network.yaml
  web_server: !include .web-server.yaml
  wiegand_keypad: !include .wiegand-keypad.yaml

esphome:
  name: ${esphome_name}
  friendly_name: ${friendly_name}
esp32:
  board: esp32dev
  framework:
    type: esp-idf

external_components:
  - source: github://ratgdo/esphome-ratgdo@main
    components: [ratgdo]
    refresh: 1d

safe_mode:

logger:
  level: INFO

preferences:
  flash_write_interval: 1min

ratgdo:
  id: my_garage_door_opener
  input_gdo_pin: GPIO21   # RX from opener data bus
  output_gdo_pin: GPIO17  # TX to opener data bus
  input_obst_pin: GPIO4   # obstruction sensor (safety beam)
  protocol: secplusv1     # LiftMaster 41AC050-2, purple learn button = Security+ 1.0

The rest of the config exposes the door itself to Home Assistant: a cover for the door, light and lock (remotes) entities, binary_sensors for obstruction/motion/button plus a physically-wired reed switch for door-closed state, numbers for the rolling code counter and open/close durations, and template buttons for sync and manual toggle.

The wiegand_keypad Package

This is the reader logic described above, now just one package among several instead of the whole device. D0/D1 land on GPIO33/GPIO35 — through the resistor dividers pictured above — and feed the wiegand: and key_collector: components that were previously the entire standalone config:

substitutions:
  wiegand_d0: GPIO33
  wiegand_d1: GPIO35

wiegand:
  - id: mykeypad
    d0: ${wiegand_d0}
    d1: ${wiegand_d1}
    on_key:
      - lambda: ESP_LOGI("KEY", "received key %d", x);
    on_tag:
      - lambda: ESP_LOGI("TAG", "received tag %s", x.c_str());
      - text_sensor.template.publish:
          id: keypad
          state: !lambda "return x.c_str();"
    on_raw:
      - lambda: ESP_LOGI("RAW", "received raw %d bits, value %llx", bits, value);

key_collector:
  - id: pincode_reader
    source_id: mykeypad
    min_length: 4
    max_length: 4
    end_keys: "#"
    clear_keys: "*#"
    allowed_keys: "0123456789"
    timeout: 4s
    on_progress:
      - logger.log:
          format: "input progress: '%s', started by '%c'"
          args: ['x.c_str()', "(start == 0 ? '~' : start)"]
    on_result:
      - logger.log:
          format: "input result: '%s', started by '%c', ended by '%c'"
          args: ['x.c_str()', "(start == 0 ? '~' : start)", "(end == 0 ? '~' : end)"]
      - text_sensor.template.publish:
          id: keypad
          state: !lambda "return x.c_str();"
      - delay: 500ms
      - text_sensor.template.publish:
          id: keypad
          state: "0"
    on_timeout:
      - logger.log:
          format: "input timeout: '%s', started by '%c'"
          args: ['x.c_str()', "(start == 0 ? '~' : start)"]

text_sensor:
  - platform: template
    name: "Keypad Code"
    id: keypad

Tag reads publish straight to the Keypad Code text sensor via on_tag; PIN entry goes through key_collector, which only publishes on_result — a completed 4-digit code terminated by #, or a reset on *#/timeout. Either path lands on the same sensor, so Home Assistant only ever has one place to watch. The on_key/on_raw handlers just log at INFO for visibility — no logic runs on the device itself, matching the thin-edge design described above.

Configuration Practices

Network config is pulled from an external .network.yaml include, keeping WiFi credentials separated from the main config — a clean practice for version control. Production logging is set to INFO, with DEBUG-level messages available on the key-press and raw-data handlers for troubleshooting.