ESPHomeHome Assistant

Kitchen timer LED ring

A Home Assistant timer — started from a dashboard, a button, or voice — drives an ESP32 NeoPixel ring that empties as time runs out and flashes when the timer finishes. ESPHome, on the LAN.

Difficulty · Beginner–IntermediateSetup · 1–2 hoursHA · current stable (native API)ESPHome · 2026.9
Get the pack · from $19Have us set it up

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

  1. 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.start with a duration. They do not talk to the ESP32 directly.
  2. Numbers the ring can read — a running timer’s remaining attribute is only refreshed when the timer starts or pauses, so it cannot drive a smooth countdown. The ESP32 reads the timer’s state, its duration, and its finishes_at time with the homeassistant platform, takes the clock from Home Assistant, and computes the seconds left on the device ten times a second.
  3. 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_leds is a substitution: 12, 16, or 24.
  4. Finish flash — Home Assistant fires timer.finished only 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.
  5. Pause — timer.pause stores the time left in the remaining attribute; while the state is paused the ring reads that value and the arc holds. Resume continues the empty-down.
  6. 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_leds to 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

  1. 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 active with a finishes_at time.
  2. Solder the ring. Flash the ESPHome YAML over USB with num_leds set to your ring. Adopt it in Home Assistant (Settings → Devices & services) so the homeassistant sensors can connect.
  3. 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.
  4. 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.
  5. 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_entity does 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 and finishes_at values 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.cancelled or to “state became idle.” Use timer.finished only.
  • 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_order from GRB to RGB.
  • Only some LEDs. num_leds does not match the ring.
  • Flicker or reboot when it goes white. Brightness is uncapped and the supply sags. Keep max_brightness at 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.

Changelog

v1.0 · Oct 19, 2026: First publish.