Some hardware links on this page are affiliate links. As an Amazon Associate I earn from qualifying purchases. We may earn a commission if you buy through them, at no extra cost to you. See the affiliate disclosure.
A phone timer is invisible from across the kitchen, and it dies with the screen. This pack puts the countdown on a NeoPixel ring: a Home Assistant timer helper is the clock, an ESP32 drives the ring over the local API, and the lit arc shrinks as the remaining time drops. When the timer finishes, the ring flashes and then goes dark. Pause holds the arc. Cancel turns it off without the flash.
The ring is a display for a timer you already start from a dashboard button, a wall button, or a voice satellite phrase. The pixels are not a cloud round trip. Wi-Fi credentials stay in secrets.yaml. Never paste SSIDs, PSKs, API keys, or OTA passwords into a published YAML.
The LiDAR garage parking light uses a ring as a distance gauge while you pull in. This one is a countdown. Different sensors, different effect, same idea that a ring across the room is easier to read than a phone.
What problem it solves
You want a kitchen timer you can start without finding the phone, see while your hands are busy, and trust to flash once when it hits zero — running between Home Assistant and an ESP32 on the LAN.
How it works
- Timer helper — one
timer(pack example duration 10 minutes, restore on). Dashboard buttons, a wall button, and a voice satellite all call the same script:timer.startwith a duration. They do not talk to the ESP32 directly. - Numbers the ring can read — a running timer’s
remainingattribute is only refreshed when the timer starts or pauses, so it cannot drive a smooth countdown. The ESP32 reads the timer’s state, itsduration, and itsfinishes_attime with thehomeassistantplatform, takes the clock from Home Assistant, and computes the seconds left on the device ten times a second. - Fill — an addressable effect lights
ceil(remaining / duration × num_leds)LEDs in amber and leaves the rest off. At the start the ring is full. At the end it is empty.num_ledsis a substitution: 12, 16, or 24. - Finish flash — Home Assistant fires
timer.finishedonly when the timer expires, not when you cancel it. That automation turns a boolean on for 20 seconds. The ring sees the boolean and blinks. Cancel (timer.cancel) goes idle with the boolean off, so the ring just goes dark. - Pause —
timer.pausestores the time left in theremainingattribute; while the state ispausedthe ring reads that value and the arc holds. Resume continues the empty-down. - Brightness — the effect is capped (pack default 0.35) so a USB supply can feed the board and the ring together. Full-white on 24 LEDs is unnecessary on a counter and will brown out a weak port.
If the ESP32 reboots mid-timer, it picks up the sensors again as soon as the API connects. The timer itself lives in Home Assistant, with restore, so a board reboot does not reset the countdown.
Hardware notes
- Ring. Any WS2812 or SK6812 NeoPixel ring. Set
num_ledsto the count you bought. A 16-LED ring is 44.5 mm across and readable from a counter. The Adafruit ring has solder pads: three short silicone wires to 5V, GND, and Data In. - Board. ESP32-C6 Super Mini. Leave GPIO8 (onboard LED and boot strap), GPIO9 (BOOT and strap), GPIO12 and GPIO13 (USB), and GPIO15 (strap) alone. Data is GPIO2.
- Power. One 5 V USB-C supply into the board. The ring takes 5 V from the board’s 5V pin. Do not also feed 5 V into the 5V pin while USB is plugged in. The YAML’s brightness cap keeps the ring well under an amp; a data-only laptop port is still a bad supply.
- Data line. The ring expects 5 V logic. A short 3.3 V wire works on most rings. If colors are random, add a 330 Ω resistor in series with the data wire.
Wiring
| NeoPixel ring | ESP32-C6 Super Mini |
|---|---|
| 5V (PWR) | 5V |
| GND | GND |
| Data In (DIN) | GPIO2 |
DIN is the input pad, not DOUT. If the first LED lights and the rest stay dead, you are on the wrong pad.
Snippet
Start script and finish flash (in the pack’s Home Assistant package; timer.kitchen is a stand-in you can rename):
script:
start_kitchen_timer:
fields:
minutes:
default: 10
sequence:
- action: timer.start
target:
entity_id: timer.kitchen
data:
duration: "{{ (minutes | int) * 60 }}"
automation:
- triggers:
- trigger: event
event_type: timer.finished
event_data:
entity_id: timer.kitchen
actions:
- action: input_boolean.turn_on
target:
entity_id: input_boolean.kitchen_timer_flash
- delay:
seconds: 20
- action: input_boolean.turn_off
target:
entity_id: input_boolean.kitchen_timer_flash
Ring, trimmed. Wi-Fi, API, and OTA use !secret in the full YAML, which also turns finishes_at into a timestamp:
substitutions:
device_name: kitchen-timer-ring
friendly_name: Kitchen timer ring
led_pin: GPIO2
num_leds: "16"
max_brightness: "0.35"
timer_entity: timer.kitchen
flash_entity: input_boolean.kitchen_timer_flash
time:
- platform: homeassistant
id: ha_time
text_sensor:
- platform: homeassistant
id: timer_state
entity_id: ${timer_entity}
- platform: homeassistant
id: timer_finishes
entity_id: ${timer_entity}
attribute: finishes_at
- platform: homeassistant
id: timer_duration
entity_id: ${timer_entity}
attribute: duration
- platform: homeassistant
id: timer_remaining
entity_id: ${timer_entity}
attribute: remaining
light:
- platform: esp32_rmt_led_strip
id: ring
pin: ${led_pin}
num_leds: ${num_leds}
chipset: ws2812
rgb_order: GRB
effects:
- addressable_lambda:
name: Timer
update_interval: 100ms
lambda: |-
// flash while the finished flag is on; otherwise
// lit = ceil(seconds_left / duration * num_leds) in amber
While any time is left, at least one LED stays on, so the ring does not look “already done” before the flash takes over.
Setup outline
- Add the pack’s Home Assistant package (timer with Restore on, flash boolean, start script, finish automation). Start the timer from Developer Tools and confirm the timer shows
activewith afinishes_attime. - Solder the ring. Flash the ESPHome YAML over USB with
num_ledsset to your ring. Adopt it in Home Assistant (Settings → Devices & services) so thehomeassistantsensors can connect. - With the timer idle the ring is dark. Start a one-minute timer: the arc should shrink. Let it finish: red flashes for about 20 seconds, then dark. Cancel a second run: dark, no flash.
- Put the start script on a dashboard button. Point a wall button or an existing voice-satellite phrase at the same script if you want those too.
- Move updates to Wi-Fi after the first USB flash. Mount the ring where you can see it from the counter, not where the USB cable strains the pads.
Troubleshooting
- Ring stays dark while the timer runs.
timer_entitydoes not match your timer, or the node is not adopted, so it cannot read Home Assistant states or the clock. Check the device log for the timer state andfinishes_atvalues before you touch the LEDs. - Ring dark or stuck after a reboot. The board has no time yet. It takes the time from Home Assistant once the API connects; give it a few seconds.
- Cancel flashes too. The boolean is tied to
timer.cancelledor to “state became idle.” Usetimer.finishedonly. - Random colors or a dead ring. Shorten the data wire, add the 330 Ω series resistor, and confirm DIN rather than DOUT. If red and green are swapped, change
rgb_orderfromGRBtoRGB. - Only some LEDs.
num_ledsdoes not match the ring. - Flicker or reboot when it goes white. Brightness is uncapped and the supply sags. Keep
max_brightnessat or below the pack default and use a real 5 V supply. - Voice phrase does nothing to the ring. The phrase never called
timer.start. The ring only mirrors the helper. Fix the script call; the ESP32 does not parse speech.
Credits
Inspired by the community project Echo Wall Clock: show timers on its LED ring, locally. This write-up is an original ESP32 and NeoPixel recipe for a Home Assistant timer helper. It does not use that clock or its protocol.