Skip to content

Hype Button

One big button on a wall, and what it does depends on how you hit it. An ESP32 counts the presses — one, two, three, four, five, or a hold — and publishes each pattern to Home Assistant as a different string, so one button drives six automations. Eight pixels next to it show a level you set with the same button, so the same input is a dial as well as a trigger.

The button is a 22 mm industrial mushroom head in a two-hole control station box. That is the whole point: it is the size and the weight of a thing you hit, not a tactile switch you find with a fingernail.

What a press does

Every pattern publishes to one text sensor, ${device_name} Action. Two of them also move the level the LED bar shows.

PressAction becomesAlso
One short clicksingle short clickLevel +1
Hold for 1 s or moresingle long clickLevel −1
Two clicksdouble click
Three clickstriple click
Four clicksfour click
Five clicksfive click

The timings are strict, and they come straight from the config: each press must be down for 1 s or less, each gap between presses 1 s or less, and the sequence closes after 0.5 s with the button up. So a single click is not a single click until half a second after you let go — there is no way around that delay, because until the window closes the device cannot know a second click isn’t coming. The long press is the exception: it fires while your hand is still on it, the moment you cross a second.

One second after publishing, the sensor resets itself to standby.

Trigger your automations on the transition to a value, not on the value being present. The string is only there for a second, and any automation that polls for state == "triple click" will miss it most of the time.

Parts

Some links below are Amazon affiliate links. As an Amazon Associate I earn from qualifying purchases — at no extra cost to you. It helps keep these guides free. Outside the US, they should send you to your own Amazon store; if one doesn't, searching the part number there will find it.

Buy the momentary one. The 22 mm mushroom head also comes in a latching version — identical hole, identical look, and it stays down until you press it again. Multi-click detection needs a contact that closes and opens with your hand, so on a latching head every automation above becomes unreachable except the long press. Check the listing says momentary, not latching or maintained.

There is nothing to print. The control station box is the enclosure, and it arrives with both 22 mm holes already cut.

About the encoder

The comment at the top of the config says “rotary encoder with push button”, and that is what the pin is named after — encoder_sw. But there is no rotary_encoder: component anywhere in the file. Only the switch is read. Turning a knob does nothing; the level moves by clicking, up on a short press and down on a long one. Fit the mushroom head and ignore the name. If you want the knob, you are adding a rotary_encoder: component and two more pins yourself, and an EC11 needs a 7 mm hole drilled somewhere the box does not have one.

Wiring

Three wires to the LED board, two to the button.

PartESP32 pinNotes
Button, one terminalGPIO22The config sets pullup: true and inverted: yes, so the pin idles high and the press pulls it down. No external resistor
Button, other terminalGND
LED board DINGPIO13Keep this lead short
LED board 5V5V / VINNot 3V3 — the pixels are 5 V parts
LED board GNDGND

The button above has one normally-open contact on two screw terminals, so there is no polarity and nothing to choose: either terminal to GPIO22, the other to GND. Heads that ship with a screw-on 1NO1NC block give you four terminals instead, and only the NO pair works here.

Eight WS2812s at full white pull roughly 60 mA each, so about half an amp with everything on. That comes out of the ESP32’s 5 V pin, which is the USB rail, which is why the parts list says a 2.4 A charger and not the 500 mA one in the drawer.

Assembly

  1. Decide where the pixels go before anything else, because it decides what the second 22 mm hole is for. The LED board is a straight bar — eight 5050 packages in a row, over 40 mm end to end — so it does not sit behind a 22 mm round hole. Either pass a three-wire lead out through that hole and stick the bar where you can see it from across the room, or file the hole into a slot and mount the bar behind a strip of white plastic. Fit the key switch there instead and the bar goes outside.

  2. Fit the button. It drops into a 22 mm hole from the front and clamps with the ring nut from behind. Snug, not gorilla-tight — it is a plastic box.

  3. Wire the button to GPIO22 and GND with the silicone wire, under its two screw terminals. Bring the USB cable in at this point too — the switch body fills most of the box once it is in, and threading a plug past it afterwards is miserable.

  4. Wire the LED board to 5 V, GND and GPIO13. Short leads. A long data run to the first pixel is the usual cause of a first pixel that shows the wrong colour.

  5. Flash it over USB and test it with the box open. Press once, twice, three times, and watch the text sensor in Home Assistant change. Closing the box on a wire that has pulled out of a screw terminal is the one mistake that costs you the whole assembly again.

Firmware

The config is esphome/bruh-switch.yaml in the download section below. It is plain ESPHome — nothing to install beyond ESPHome itself, and the only C++ in it is the lambda that draws the level bar.

  1. Copy it into your ESPHome directory.

  2. Change the two substitutions at the top:

    substitutions:
    device_name: "Hype Button"
    device_id: "hype-button"
  3. Fill in secrets.yaml. This config uses four keys: wifi_ssid, wifi_password, ota_password and api_key. There is a secrets.yaml.example in the repo with every key these configs use.

  4. Flash over USB the first time. Every flash after that is over WiFi.

The ota: block in this config has no platform: key. Current ESPHome wants platform: esphome nested under ota: — add that line if the compile complains about it.

Two things in the file that are not this project

bruh-switch.yaml also carries a xiaomi_hhccjcy01 block and an esp32_ble_tracker:, reporting temperature, moisture, illuminance, conductivity and battery for a plant sensor called “Yard 1”. That has nothing to do with the button — it is there because an ESP32 will happily be a Bluetooth receiver and a button at the same time, and this one happened to be near a plant. Delete both blocks unless you have that sensor, and give the WiFi stack back the memory the Bluetooth radio was holding. If you are keeping it, put your own sensor’s MAC in mac_address:; the one in the file is Ben’s and will never advertise on your network.

There are also two internal template text sensors, last_message_line_1 and last_message_line_2, that nothing ever writes to. They are harmless and they are dead weight. Delete them.

The level bar

switch_1_value is a template number, 0 to 7, and the Status light effect draws it: pixels below the value light in whatever colour the light is currently set to, pixels above it stay dark. A short click adds one, a long press takes one away.

Two things about it are worth knowing before you wonder why nothing is happening. The light has to be on and Status has to be the selected effect — it is an effect, not a separate entity, so with the light off the bar is off too. And the number has neither restore_value nor initial_value, so after a reboot it comes up with no value at all and the first click has nothing to add to. One line fixes that:

number:
- platform: template
id: switch_1_value
restore_value: true

One more quirk you will see before you look for it: the effect lights the pixels below the value and max_value is 7, so the eighth pixel never comes on. A full level is seven lit and one dark. Set max_value: 8 if you want the whole bar.

The light carries fourteen more effects beyond Status — flicker, addressable flicker, strobe, random, rainbow, scan, colour wipe, twinkle, random twinkle, fireworks, a two-colour Police wipe and three BRUH-tuned flicker and twinkle variants. Those are what you fire from the celebration scene.

In Home Assistant

The device is discovered by ESPHome, so there is no YAML on the Home Assistant side. With the substitutions above you get:

  • sensor.hype_button_action — the string in the table at the top. This is the thing you automate on.
  • binary_sensor.hype_button_button — the raw contact, on while your hand is down. Check your wiring against this one before anything else; it should be off at rest and flick on under your palm.
  • light.hype_button_led — the eight pixels, with the whole effects list.
  • number.hype_button_switch_1 — the 0–7 level.
  • switch.hype_button_alert_mode — an optimistic template switch. It does nothing on the device; it is a flag you can set from the button’s automations and test from other ones. Delete it if you have no use for it.

A triple click firing a scene:

automation:
- alias: "Hype button — triple click"
triggers:
- trigger: state
entity_id: sensor.hype_button_action
to: "triple click"
actions:
- action: scene.turn_on
target:
entity_id: scene.hype

Repeat that with to: "double click", to: "five click" and so on. Six patterns, six automations, one button.

Troubleshooting

Every press comes back as a double click. Contact bounce. A big mechanical switch bounces far harder than a tactile button, and multi-click counts every edge it sees. Add a filter to the binary sensor:

binary_sensor:
- platform: gpio
id: encoder_sw
filters:
- delayed_on_off: 20ms

Two clicks report as one. The on_multi_click timings got reordered. They are listed longest-first in the config on purpose — ESPHome takes the first block that matches, and a single-click pattern sitting above a double-click pattern will match first and the longer one never fires. Put them back in the order the file ships in.

The button reads pressed all the time and clears while you hold it. You are on a normally-closed pair. Only a head with a 1NO1NC block can do this — move both wires to the NO terminals.

Nothing on the LEDs. In order: the light entity is off, or the effect isn’t Status; then 5 V wired to 3V3; then the data lead on the wrong pin. GPIO13.

The first pixel is the wrong colour and the rest are fine. That is the 3.3 V data line and a lead that is too long. Shorten it before reaching for a level shifter.

Files & downloads

ESPHome configuration

Copy this into your ESPHome directory and adjust the substitutions at the top. Secrets are referenced by name — see secrets.yaml.example .

bruh-switch.yaml 239 lines
bruh-switch.yaml
# BRUH Smart Button Switch
# ────────────────────────────────────────────────────────────
# Multi-click button controller with rotary encoder support and
# addressable LED feedback. Supports single click, double click,
# triple click, and long press actions with custom effects.
#
# Hardware:
# - Board: ESP32
# - Input: Rotary encoder with push button
# - Output: 8-LED NeoPixel strip for feedback
# ────────────────────────────────────────────────────────────
substitutions:
device_name: "BRUH Switch"
device_id: "bruh-switch-2"
esphome:
name: ${device_id}
esp32:
board: esp32dev
framework:
type: arduino
logger:
api:
encryption:
key: !secret api_key
ota:
password: !secret ota_password
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
captive_portal:
binary_sensor:
- platform: gpio
pin:
number: GPIO22
inverted: yes
mode:
input: true
pullup: true
id: encoder_sw
name: "${device_name} Button"
on_multi_click:
- timing:
- ON for at most 1s
- OFF for at most 1s
- ON for at most 1s
- OFF for at most 1s
- ON for at most 1s
- OFF for at most 1s
- ON for at most 1s
- OFF for at most 1s
- ON for at most 1s
- OFF for at least 0.5s
then:
- text_sensor.template.publish:
id: last_action
state: "five click"
- timing:
- ON for at most 1s
- OFF for at most 1s
- ON for at most 1s
- OFF for at most 1s
- ON for at most 1s
- OFF for at most 1s
- ON for at most 1s
- OFF for at least 0.5s
then:
- text_sensor.template.publish:
id: last_action
state: "four click"
- timing:
- ON for at most 1s
- OFF for at most 1s
- ON for at most 1s
- OFF for at most 1s
- ON for at most 1s
- OFF for at least 0.5s
then:
- text_sensor.template.publish:
id: last_action
state: "triple click"
- timing:
- ON for at most 1s
- OFF for at most 1s
- ON for at most 1s
- OFF for at least 0.5s
then:
- text_sensor.template.publish:
id: last_action
state: "double click"
- timing:
- ON for at least 1s
then:
- text_sensor.template.publish:
id: last_action
state: "single long click"
- number.set:
id: switch_1_value
value: !lambda 'return id(switch_1_value).state - 1;'
- timing:
- ON for at most 1s
- OFF for at least 0.5s
then:
- text_sensor.template.publish:
id: last_action
state: "single short click"
- number.set:
id: switch_1_value
value: !lambda 'return id(switch_1_value).state + 1;'
switch:
- platform: template
name: "${device_name} Alert Mode"
id: alert_mode
optimistic: true
text_sensor:
- platform: template
name: "${device_name} Action"
id: last_action
on_value:
then:
- delay: 1s
- if:
condition:
lambda: 'return id(last_action).state != "standby";'
then:
- text_sensor.template.publish:
id: last_action
state: "standby"
- platform: template
id: last_message_line_1
internal: true
- platform: template
id: last_message_line_2
internal: true
light:
- platform: neopixelbus
variant: 800KBPS
type: GRB
pin: GPIO13
id: led
num_leds: 8
name: "${device_name} LED"
default_transition_length: 500ms
effects:
- flicker:
- flicker:
name: "Flicker BRUH"
alpha: 92%
intensity: 4%
- strobe:
- random:
- addressable_rainbow:
- addressable_scan:
- addressable_color_wipe:
- addressable_color_wipe:
name: "Police"
colors:
- red: 100%
green: 0%
blue: 0%
num_leds: 6
- red: 0%
green: 0%
blue: 100%
num_leds: 6
add_led_interval: 30ms
reverse: false
- addressable_twinkle:
- addressable_twinkle:
name: "Twinkle BRUH"
twinkle_probability: 80%
progress_interval: 4ms
- addressable_random_twinkle:
- addressable_fireworks:
- addressable_flicker:
- addressable_flicker:
name: "Addressable Flicker BRUH"
update_interval: 25ms
intensity: 15%
- addressable_lambda:
name: "Status"
update_interval: 50ms
lambda: |-
int r = id(led).current_values.get_red() * 255;
int g = id(led).current_values.get_green() * 255;
int b = id(led).current_values.get_blue() * 255;
int brightness = id(led).current_values.get_brightness() * 255;
int ledArray[] { 0, 1, 2, 3, 4, 5, 6, 7};
int pos;
for (int i = 8 - 1; i >= 0; i--) {
pos = ledArray[i];
if (pos >= id(switch_1_value).state) {
it[pos] = Color(0, 0, 0);
} else {
it[pos] = Color(r, g, b);
}
}
number:
- platform: template
id: switch_1_value
name: "${device_name} Switch 1"
internal: false
optimistic: true
min_value: 0
max_value: 7
step: 1
esp32_ble_tracker:
sensor:
- platform: xiaomi_hhccjcy01
mac_address: "DC:23:4D:E5:62:8C"
temperature:
name: "Yard 1 Temperature"
moisture:
name: "Yard 1 Moisture"
illuminance:
name: "Yard 1 Illuminance"
conductivity:
name: "Yard 1 Soil Conductivity"
battery_level:
name: "Yard 1 Battery Level"

View on GitHub · Download raw

Every file for this project on GitHub →