I wanted a door keypad that actually looked good on the wall — slim, wireless, no visible screws, no cloud dependency. Something I could mount next to the front door, punch in a code, and have Home Assistant react instantly. Off-the-shelf Zigbee keypads exist, but they're usually expensive, mains-powered, or locked to one ecosystem. So I built my own.
The result is a battery-powered touch keypad about the size of a pez dispenser. It runs for roughly six months on a single charge, talks Zigbee to whatever coordinator you already run, and wakes from deep sleep the moment you touch a key. This repo has everything you need to build one yourself: Gerber files, a 3D-printable case, and working firmware.
The firmware ships as a Zigbee end-device example. The same ESP32-C6 board is equally happy running Matter or Thread — the touch handling and power logic stay the same; only the radio stack changes.
The design goal was something that disappears into a modern entryway. A white slab on textured walls, twelve touch buttons, no mechanics to wear out.
Early renders explored proportions and mounting position before the first PCB arrived. Getting the aspect ratio right mattered — too tall and it looks just weird, too wide and it reads as a light switch.
The front panel is a 2×6 grid: digits 1–9, 0, a clear key, and enter. Labels are printed directly on the PCB solder mask — no separate overlay, no glue, nothing to peel off after two winters.
Pop the case apart and the internals are deliberately simple. A custom backplane PCB, a 1300 mAh LiPo filling most of the volume, and an ESP32-C6 dev board at the bottom with a USB-C port for charging and flashing.
The whole assembly is rather light hence can be mounted to the wall with a few bites of white tack. For scale:
Three layers, top to bottom:
- Keypad face — the touch matrix PCB you see and press
- Electronics bay — battery, ESP32-C6, MPR121 touch controller
- Back cover — 3D-printed shell (
case_mid_final.stl) that clips on without screws
The onboard LED sits inside the white case that is somewhat translucent. When the device wakes from deep sleep it glows briefly; each key press flashes it again so you always know your touch registered.
You don't need to lay out a board from scratch. The Gerber files are in this repo and ready to upload to any fab. I've been using JLCPCB — pick a colour scheme, drop in the matching zip, and order. No CAM fixes, no layer renaming, no back-and-forth with support.
| File | Colour scheme |
|---|---|
touch_matrix_Y24.zip |
Black on white — black key labels on a white PCB (latest revision) |
touch_matrix_Y23.zip |
White on black — white key labels on a black PCB |
Same layout, same touch pads — only the silkscreen colours differ. Choose whichever fits your wall and enclosure.
The back of the PCB carries the JLC order markings and a five-pin header for the MPR121:
| Pin | Function |
|---|---|
| GND | Ground |
| INT | Interrupt (wake from sleep) |
| SCL | I²C clock |
| SDA | I²C data |
| 3.3 V | Power |
The firmware expects this pin mapping on the ESP32-C6 DevKitC-1:
| Signal | GPIO | Notes |
|---|---|---|
| I²C SDA | 18 | MPR121 data |
| I²C SCL | 4 | MPR121 clock |
| IRQ | 6 | MPR121 interrupt — also the deep-sleep wake source |
| Battery ADC | 2 | Voltage divider input |
| BOOT button | 9 | Hold 3 s for Zigbee factory reset |
The wiring is very straigtforward - simply connect all the pins as described, connect the battery to ESP32-C6 bat pins, route 3.3V and ground to the PCB and done. If you would like the battery SOC reported, you will need to add an additional voltage device to the battery pins.
The twelve MPR121 channels map to the front-panel keys like this:
| Channel | Key |
|---|---|
| 0–8 | 1 – 9 |
| 9 | 0 |
| 10 | Clear |
| 11 | Enter |
For now the software allows coes up to eight digits.
The project builds with PlatformIO. Clone the repo, connect the ESP32-C6 over USB-C, and:
pio run --target uploadTo watch the serial log while pairing:
pio device monitorThe build targets Zigbee end-device mode (ZIGBEE_MODE_ED) with a custom partition table (partitions_zigbee.csv) that reserves flash for Zigbee network storage. Dependencies are pulled automatically — the main one is the Adafruit MPR121 library.
On first boot the keypad scans for a coordinator. Dots print on the serial console at 115200 baud until it joins. In Home Assistant that means adding it through ZHA or Zigbee2MQTT — the device shows up as manufacturer DrDoms, model KeyPad2.
Two Zigbee endpoints are exposed:
| Endpoint | Cluster | What it reports |
|---|---|---|
| 1 | Analog Input + Battery | Entered passcode, battery voltage |
| 2 | Analog Input | Battery state of charge (%) |
The passcode flow works like this:
- Touch digits on the pad — the LED confirms each press.
- Press Enter. The code is reported on endpoint 1 as an analog value.
- Battery level is reported at the same time.
- Two seconds later the reported value resets to 0, ready for the next entry.
Leading 1: The firmware always prepends a 1 to every code before sending it. The input buffer starts as "1" and resets to "1" after clear or enter. This is because ensure that leading zeros in your entered code are not stripped as the value is purely numeric, not a string.
Press Clear at any time to wipe the buffer and start over.
Once the keypad is flashed, getting it into Home Assistant through ZHA takes about two minutes.
- In Home Assistant go to Settings → Devices & Services → Zigbee Home Automation and open your coordinator.
- Click Add device and confirm — this opens the network for joining.
- Wake the keypad by touching any key. Within a few seconds it should appear as DrDoms KeyPad2.
- Finish the interview if ZHA asks — no special configuration is needed.
After pairing, open the device page and note the entity names ZHA created. The important one is the Passcode analog-input sensor — in my setup it shows up as sensor.drdoms_keypad2_passcode. You will also see battery voltage and percentage sensors from the second endpoint. Entity IDs can differ slightly depending on your ZHA version and how many Espressif devices you already have, so always copy the names from your own device page rather than assuming mine.
When you enter a code and press Enter, that sensor updates to the numeric value of your input — including the leading 1 described above — holds it for two seconds, then drops back to 0. That brief window is what your automations listen for.
The file keypad_automation.yaml is a worked example that maps passcodes to actions — opening a door, raising a cover, closing a cover. Import it as a starting point and adapt it to your home:
- Go to Settings → Automations & Scenes → Create Automation → Edit in YAML (top-right menu).
- Paste the contents of
keypad_automation.yaml, or save the file to your Home Assistantpackages/folder if you use packages. - Replace every
entity_idwith the ones from your installation — the passcode sensor, the door button, the cover, and so on. - Change the template values to match the codes you actually want. Each
ifblock compares the passcode sensor against a number; when they match, the action runs. Remember to account for the leading 1 — the sample value12345means you typed 2-3-4-5 on the pad, not 1-2-3-4-5. A code you enter as 0-1 will appear as 101 in Home Assistant.
# Example: open a cover when passcode 12345 is entered
- if:
- condition: template
value_template: "{{ states('sensor.drdoms_keypad2_passcode') | int(0) == 12345 }}"
then:
- action: cover.open_cover
target:
entity_id: cover.basement_control_coverThe sample automation listens on two sensor entities because entity naming changed between ZHA versions — sensor.espressif_zigbeeanalogdevice_power is the older name, sensor.drdoms_keypad2_passcode is the current one. Keep whichever matches your device and delete the other trigger block.
To find the exact integer your keypad sends for a given code, enter the code, press Enter, and watch the sensor in Developer Tools → States. Use that number in your template condition.
This repo demonstrates Zigbee because it's the fastest path to a working smart-home integration. But the ESP32-C6 has a native 802.15.4 radio, so porting to Thread and Matter is straightforward — the touch scanning, debouncing, battery ADC, and deep-sleep wake logic in main.cpp don't care which protocol sends the numbers upstream. Espressif's ESP-Matter SDK is the place to start if you want to go that route.
Battery life was the hardest constraint. A keypad that's dead every two weeks is worse than no keypad at all.
The trick is aggressive deep sleep. After ten seconds without a touch, the firmware shuts everything down and puts the MPR121 into its slowest scan rate. The ESP32-C6 draws almost nothing. The next finger on the pad pulls the MPR121 interrupt line low, the MCU wakes, re-initialises I²C, and is ready to scan within milliseconds.
With a 1300 mAh cell and typical use — a few code entries per day — that works out to roughly half a year between USB-C charges. Battery voltage and percentage are reported over Zigbee so you can set a low-battery alert in Home Assistant before it goes flat.
- Walk up and touch a key. The LED glows through the case as the device wakes.
- Enter your code. Each digit flashes the LED.
- Press Enter to send it to your automation.
- Walk away. Ten seconds later it sleeps again.
If you need to re-pair — new coordinator, corrupted network, that sort of thing — hold the BOOT button for more than three seconds. The device factory-resets its Zigbee stack and reboots.
keypad/
├── main.cpp # Touch handling, Zigbee reporting, power management
├── platformio.ini # Board target, libraries, build flags
├── partitions_zigbee.csv # Flash layout with Zigbee storage partitions
├── case_mid_final.stl # 3D-printable enclosure mid-section
├── touch_matrix_Y24.zip # PCB Gerbers — black on white
├── touch_matrix_Y23.zip # PCB Gerbers — white on black
├── keypad_automation.yaml # Home Assistant example — passcode → actions
└── touch_matrix_*.jpg # Photos from the build
| Part | License | File |
|---|---|---|
Firmware (main.cpp, build config) |
MIT License | LICENSE |
| Hardware (PCB Gerbers, STL, design files) | CERN Open Hardware Licence v2 — Strongly Reciprocal | LICENSE.hardware |
It really works well - has been my method of entry for the past year. If you make a variant — Matter port, different key layout, waterproof enclosure — I'd love to see it.




