Making my keyboard the brains to control my room.
A Razer Huntsman keyboard mounted on the wall, connected to a Raspberry Pi 3B+, functioning as a physical macro panel. No display. No terminal. Keys map to actions. RGB provides feedback. The whole thing boots headlessly and runs as a systemd user service.
- Intercepts raw keyboard input via evdev (keys don't reach the OS — the daemon owns them)
- Maps keys to actions (HTTP webhooks, shell commands, whatever you want)
- Drives keyboard RGB via OpenRazer D-Bus as a feedback and ambient display
- Runs at boot with no login required via
loginctl enable-linger
| Key | Action |
|---|---|
F1 |
Toggle lights via Home Assistant webhook |
ESC |
Kill switch — toggle all LEDs off/on |
| State | Behavior |
|---|---|
| Active | Static white, full brightness |
| Idle (1min no activity) | Breathing white, ~15% brightness |
| Macro success | Three white >>> chevrons sweep right |
| Macro failure | Red flash × 3 |
| HA unreachable | Static amber (auto-restores when HA returns) |
| Kill switch ON | All LEDs off |
- Raspberry Pi 3B+ (arm64, running Raspberry Pi OS Trixie Lite)
- Razer Huntsman (USB)
| Layer | Tool |
|---|---|
| Language | Go — single binary, zero runtime deps on the Pi |
| Key capture | github.com/holoplot/go-evdev |
| RGB control | github.com/godbus/dbus/v5 → OpenRazer daemon |
| Light toggle | net/http GET to Home Assistant webhook |
| Service | systemd user unit + loginctl enable-linger |
| Cross-compile | GOOS=linux GOARCH=arm64 go build |
echo 'deb http://download.opensuse.org/repositories/hardware:/razer/Debian_13/ /' \
| sudo tee /etc/apt/sources.list.d/hardware:razer.list
curl -fsSL https://download.opensuse.org/repositories/hardware:razer/Debian_13/Release.key \
| gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/hardware_razer.gpg > /dev/null
sudo apt update
sudo apt install -y openrazer-daemon openrazer-driver-dkms linux-headers-$(uname -r)sudo gpasswd -a pi plugdev
sudo gpasswd -a pi input
loginctl enable-linger pi # user units start at boot without login
sudo mkdir -p /opt/huntsman-panel
sudo chown pi:pi /opt/huntsman-panelecho 'razerkbd' | sudo tee /etc/modules-load.d/razerkbd.confsudo udevadm control --reload-rules && sudo udevadm trigger
# unplug and replug the Huntsman — udev needs to apply plugdev ownership to sysfs nodesmkdir -p ~/.config/systemd/user
cp huntsman-panel.service ~/.config/systemd/user/
systemctl --user daemon-reload
systemctl --user enable --now huntsman-panelecho 'HA_LIGHT_TOGGLE_URL=http://your-ha-ip:8123/api/webhook/yourwebhook' \
> /opt/huntsman-panel/.envCode lives on your main machine. The Pi just runs the binary.
# Build for Pi
make build
# Build + push + restart + tail logs (inner loop)
make ship
# Just tail logs
make logs
# SSH into the Pi
make sshThree things to change:
1. Find your event node — plug in the keyboard, then:
sudo python3 /tmp/findkeys.py # see bringup notes belowRazer keyboards expose multiple event nodes. The one that produces key events is often if01, not the default event-kbd. Update KeyboardDevice in internal/config/config.go.
2. Get your matrix dimensions — after openrazer-daemon detects your device:
python3 -c "
import dbus
bus = dbus.SessionBus()
obj = bus.get_object('org.razer', '/org/razer')
serial = str(obj.getDevices(dbus_interface='razer.devices')[0])
dev = bus.get_object('org.razer', '/org/razer/device/' + serial)
print(dev.getMatrixDimensions(dbus_interface='razer.device.misc'))
"Update MatrixRows and MatrixCols in internal/razer/razer.go.
3. Add your macros — edit the macros map in cmd/daemon/main.go.
Things that weren't obvious and took debugging to discover:
-
The event node is not what you think. Razer keyboards expose 3–5
/dev/input/eventXnodes. On the Huntsman, key events come throughif01-event-kbd(event1), notevent-kbd(event0). Use evtest or the Python script above to find yours. -
The D-Bus serial is case-sensitive.
openrazer-daemonreturns the serial in uppercase (e.g.PM1839F24710643) and the object path must match exactly — do not lowercase it. -
openrazer-daemon finds no devices until udev rules apply. After installing, you must run
udevadm control --reload-rules && udevadm triggerand then physically replug the keyboard. Theplugdevgroup must own the sysfs device nodes. -
The razerkbd kernel module doesn't load automatically until you add it to
/etc/modules-load.d/. Without it, openrazer-daemon starts but detects nothing. -
User units need
loginctl enable-linger. Without it, the systemd user session (and your service) only starts on interactive login, not at boot. -
Don't use a system unit. openrazer-daemon runs on the session D-Bus, which a system unit cannot reach. Use a user unit.
TheLair/
├── cmd/daemon/main.go # entry point, evdev loop, macro dispatch
├── internal/
│ ├── config/config.go # constants, colors, timing
│ ├── razer/razer.go # D-Bus client for OpenRazer
│ ├── animations/animations.go # chevron, error flash, idle
│ └── actions/actions.go # HTTP webhook calls
├── huntsman-panel.service # systemd user unit
├── Makefile
└── .env # on Pi only, not committed — HA_LIGHT_TOGGLE_URL
