Working dimmable LED light with button control

Hey folks, I am new to HA and ESPHome and wanted to share my experience with others to help them. I wanted an easy first project, and the naive-me figured that a dimmable LED with a button control would be as easy as it can get and would be a great candidate to start with. Boy, was I surprised!

My first attempt was to look for working samples. I figured everyone would do one of these projects while they are learning and there should be ample examples around. I see that there is a good library of examples for ESPHome, but curiously, there were none for this scenario. I also did not find much on the forum with only other people struggling and no solutions posted. Then I delved into AI and had Microsoft CoPilot help me. That was eventually what helped me the most, especially in working out the quirks of the behaviors of the framework, but it was a long and entertaining journey. I don’t think I could have been successful without the AI assistance, even though a lot of things it said did not initially work properly.

For the benefit of the community, I am sharing my code and project details, then following that I will share some of the oddities in ESPHome that I found that tripped me up along the journey.

Project goal
I am outfitting my Ford Transit to be a campervan and installing my own electrical. I want this to be a smart van, but also have analog controls. One thing I need is smart dimmable LED lights. My van’s electrical system is 12v (actually 13.5v LiFePO4 batteries, but nominally a 12v system). I want multiple buttons to control the lights - one near the sliding door (entrance) and one near the rear of the van. I also likely will design a remote that I can use in the cabin while driving, but that is not part of this project.

Button Design
I want to use a momentary button (one that you just press and release - it does not latch on or off, just closes the circuit when pressed). The user interactions I want to support are the following:

  1. The system should boot with the lights off
  2. A short press of the button should toggle the light on/off
  3. A long press of the button should brighten or dim the light gradually while the button is being held, switching between the brightening and dimming modes on each press.

LED light
My van has 12v puck lights that take just a couple watts each. I will have a string of them that are controlled as a group with a single switch. Since they are 12v, I am using a PWM 5-36v/15A(400w) Mosfet control board. This takes in a PWM signal from my microcontroller and uses a MOSFET to drive that same PWM signal on a 12V output that goes to my LED lights. For the purposes of reproducing this project, this control board is optional - you can just skip this and wire up an LED bulb and appropriate resistor directly to your microcontroller and avoid the external 12v power supply.

Microcontroller
I am waiting for a batch of ESP32-C Mini development boards, but I had a spare ESP32 DevKit v1 lurking around, so I decided to do my initial development with that. The board is supported by ESPHome and uses the esp-idf framework. I am powering my board with the USB cable for now, but will have a step down converter for 12v->3.3v for my real project as I will not have USB power where I will be installing this. I am using Wifi to connect to Home Assistant.

The pinout plan that I chose to use is as follows, but please adjust to suitable pins for your own microcontroller. Please select those carefully as you need a stable pin for PWM output and a pin with internal pull up resistor for the button. This is one area that the AI agents excel at advising you.

GPIO5 - PWM signal output for LED light
GPIO13 - monentary button input (button is connected this pin and ground)

Working Code

esphome:
  name: esphome-web-3c3bc0
  friendly_name: DimWatt
  min_version: 2025.11.0
  name_add_mac_suffix: false

esp32:
  variant: esp32
  framework:
    type: esp-idf

# Enable logging
logger: 

# Enable Home Assistant API
api:

# Allow Over-The-Air updates
ota:
- platform: esphome

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

globals:
  - id: dim_direction
    type: bool
    restore_value: no
    initial_value: "true"

  - id: dimming_active
    type: bool
    restore_value: no
    initial_value: "false"

# -------------------------------------------------------
#  PWM OUTPUT
# -------------------------------------------------------
output:
  - platform: ledc
    pin: GPIO5
    id: led_pwm
    frequency: 1000 Hz

# -------------------------------------------------------
#  LIGHT
# -------------------------------------------------------
light:
  - platform: monochromatic
    name: "Dimmable LED"
    output: led_pwm
    id: dimmable_light
    restore_mode: ALWAYS_OFF
    default_transition_length: 0s

# -------------------------------------------------------
#  BUTTON
# -------------------------------------------------------
binary_sensor:
  - platform: gpio
    pin:
      number: GPIO13
      mode:
        input: true
        pullup: true
      inverted: true
    id: button

    # ---------------------------------------------------
    #  SHORT PRESS — INSTANT TOGGLE
    #  (Cancel dimming only if dimming was active)
    # ---------------------------------------------------
    on_multi_click:
      - timing:
          - ON for at most 350ms
          - OFF for at least 50ms
        then:
          # Cancel dimming only if it was active
          - if:
              condition:
                lambda: 'return id(dimming_active);'
              then:
                - light.turn_on:
                    id: dimmable_light
                    brightness: !lambda return id(dimmable_light).current_values.get_brightness();
                    transition_length: 0s
                - lambda: |-
                    id(dimming_active) = false;

          # Now toggle
          - if:
              condition:
                light.is_on: dimmable_light
              then:
                - light.turn_off:
                    id: dimmable_light
                    transition_length: 0s
              else:
                - light.turn_on:
                    id: dimmable_light
                    transition_length: 0s

    # ---------------------------------------------------
    #  LONG PRESS — DIM UP OR DOWN
    # ---------------------------------------------------
    on_press:
      - delay: 400ms

      # Only dim if still held
      - if:
          condition:
            binary_sensor.is_on: button
          then:
            - lambda: |-
                id(dimming_active) = true;

            - if:
                condition:
                  lambda: 'return id(dim_direction);'
                then:
                  - light.dim_relative:
                      id: dimmable_light
                      relative_brightness: -100%
                      transition_length: 3s
                else:
                  - light.dim_relative:
                      id: dimmable_light
                      relative_brightness: 100%
                      transition_length: 3s

    # ---------------------------------------------------
    #  RELEASE — STOP DIMMING (ONLY if dimming was active)
    # ---------------------------------------------------
    on_release:
      - if:
          condition:
            lambda: 'return id(dimming_active);'
          then:
            # Cancel dimming
            - light.turn_on:
                id: dimmable_light
                brightness: !lambda return id(dimmable_light).current_values.get_brightness();
                transition_length: 0s

            # Flip direction
            - lambda: |-
                id(dim_direction) = !id(dim_direction);

            # Reset flag
            - lambda: |-
                id(dimming_active) = false;

My learnings:
This project had a couple of important challenges. At first glance, a simple button and dimmable PWM light seemed trivial. ESPHome has support for both, so this should be easy, right? I found out that it was not easy at all…

After experimenting all day to get to a full solution, what I learned is that there are a lot of opaque behaviors that are possibly chip and framework specific in ESPHome that make things very difficult. One of those things is in how it implements light behaviors, specifically dimming and brightening. These are accomplished with some asynchronous animations in the platform and there are some very strange behaviors that are not apparent when you are using the light object. Specifically, there is a transition length that has a default of 3 seconds, and this applies to some transitions but not turning on or off. What took most of my day was figuring out when I turned the light off, why it initially went off but then started to turn back on. I discovered that there was some other animation still in play and if I turned the light off, that animation would still be working and start brightening it again. The light object does not have any direct API to call to cancel all animations (this IMO, would be an essential thing to add). Stopping these animations is done by changing the brightness again with an explicit duration of 0s, AND, here comes the kicker - you have to change it while in the on state AND actually change the value. If all of these conditions are met, ESPHome framework will cancel those animations and you can control the light without them affecting it further, but if you don’t have all of these met, expect some oddities in the light behavior. From one of the things I learned in my AI copilot session, this seems to be related to the implementation of the esp-idf framework, so you may or may not encounter it.

I also learned that the button event model is not very straight forward. I used at least three different mechanisms before I discovered what works. It is important to note that it is easy to get into a situation where multiple events are occurring at the same time. Please turn on your trace and examine the events carefully so you know what code is being run and why your light is behaving the way it is. I had to resort in using a couple of global variables to track state to harden my code against multiple events occurring. I would not consider my code perfect in that regard as I am not testing at the millisecond scale, but it seems to be behaving as I expect with my fingers manually pressing the button. There are some limits on the global variables, so be careful in using them for more complex projects, especially ones that have multiple buttons. I think that the ESPHome framework could be improved to project better programming control over multiple events occurring, perhaps with waiting for events to complete, or cancelling them.

Another thing I found frustrating, and it is not about the framework, but the tools: there is no clear button while viewing the log. When you have verbose debugging on and you are dealing with a lot of events, it is very difficult to stop and restart the log session to get a clean screen to examine the next series of events. A simple Clear button on that screen would be incredibly helpful!

So that is my first day experience of using ESPHome. I hope this post is helpful to the community and helps you with your own learning. If you have suggestions on improving the code further, I am welcoming suggestions. There are lots of things I have not yet implemented - a minimum threshold for dimming, a double click for full brightness, a triple click for instant off.

My next project is likely to extend this to accommodate for signals from a second (or third) button. I likely will not hard wire the other buttons to my microcontroller and implement the other buttons on their own microcontroller - primarily because I want one to be a remote I can keep in the cabin area of the van. I have to think through the messaging model that is involved in having two different microcontrollers acting on the same LED string. Please let me know if you have interest in tagging along on that project too.