Skip to content

2dom/keypad

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

32 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Zigbee Touch Keypad

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.

Watch the keypad demo on YouTube

What it looks like on the wall

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.

Finished keypad mounted on a wall beside a door

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.

Early prototype mounted on a concrete wall

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.


Tear it down

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.

Exploded view showing PCB back, battery and ESP32 assembly, and keypad face

The whole assembly is rather light hence can be mounted to the wall with a few bites of white tack. For scale:

Exploded assembly on a wooden workbench All parts laid out next to a banana for size comparison

Three layers, top to bottom:

  1. Keypad face — the touch matrix PCB you see and press
  2. Electronics bay — battery, ESP32-C6, MPR121 touch controller
  3. 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.


Order the PCB

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

Wire it up

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.


Flash the firmware

The project builds with PlatformIO. Clone the repo, connect the ESP32-C6 over USB-C, and:

pio run --target upload

To watch the serial log while pairing:

pio device monitor

The 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.


Join your Zigbee network

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:

  1. Touch digits on the pad — the LED confirms each press.
  2. Press Enter. The code is reported on endpoint 1 as an analog value.
  3. Battery level is reported at the same time.
  4. 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.

Home Assistant with ZHA

Once the keypad is flashed, getting it into Home Assistant through ZHA takes about two minutes.

  1. In Home Assistant go to Settings → Devices & Services → Zigbee Home Automation and open your coordinator.
  2. Click Add device and confirm — this opens the network for joining.
  3. Wake the keypad by touching any key. Within a few seconds it should appear as DrDoms KeyPad2.
  4. 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:

  1. Go to Settings → Automations & Scenes → Create Automation → Edit in YAML (top-right menu).
  2. Paste the contents of keypad_automation.yaml, or save the file to your Home Assistant packages/ folder if you use packages.
  3. Replace every entity_id with the ones from your installation — the passcode sensor, the door button, the cover, and so on.
  4. Change the template values to match the codes you actually want. Each if block compares the passcode sensor against a number; when they match, the action runs. Remember to account for the leading 1 — the sample value 12345 means 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_cover

The 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.

Matter and Thread

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.


Power: six months on a charge

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.


Day-to-day use

  1. Walk up and touch a key. The LED glows through the case as the device wakes.
  2. Enter your code. Each digit flashes the LED.
  3. Press Enter to send it to your automation.
  4. 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.


What's in this repo

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

License

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.

About

Open Source minimalist Keypad for Zigbee / Thread / Matter

Resources

License

MIT, Unknown licenses found

Licenses found

MIT
LICENSE
Unknown
LICENSE.hardware

Stars

99 stars

Watchers

1 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages