Using a custom smart dial to control my lights

Using Home Assistant and the ESP-Home firmware, I was able to build a bedside table smart dial that controls the lights in my room in ways not possible with a normal light switch.

The base electronics are relatively simple: A rotary encoder wired up to an ESP32 microcontroller. The rotary encoder has a built-in push button and turns in discrete steps. As for the controller, I wanted a smaller module and the XIAO ESP32-C3 was great for this. It is small, has USB-C and optionally supports a rechargeable single-cell lithium ion battery.

Unfortunately, I thought about the battery part a bit too late and didn’t feel like removing solder to get at the under side of the board to attach the battery pads. The next version I make for the living room might have a battery and make use of the ESP-32’s deep sleep mode, but for now it will just be powered over USB.

The physical construction of the final devices is a 3D-printed base plate and cover which makes everything look tidy. It took me a few tries to get this right as seen in the video:

This is what 3D printing is good at though, making small iterations takes basically no time to implement. The final product has the right dimensions of 70x70x45mm including the height of the knob. This was my first time creating an enclosure for an electronics project so I would say three tries is not bad!

To make this project work on the software side, I already had my Home Assistant server running and all of my lights connected to it through local control over Wi-Fi. The ESP32 inside the dial itself, connected to the same network, is running the ESP Home firmware. This makes it easy to configure the messages the controller sends to the Home Assistant API without writing custom code. Just a YAML config file needs to be set up to create custom behaviors and values coming from the dial.


This is the YAML config file I created for my setup:

number:
  - platform: template
    name: Dial Increment
    id: dialincrement
    min_value: -10
    max_value: 10
    step: 1
    optimistic: true
    initial_value: 0

sensor:
  - platform: rotary_encoder
    name: "Dial Rotary Encoder"
    id: dial_encoder
    pin_a:
      number: GPIO20
      inverted: true
      mode:
        input: true
        pullup: true
    pin_b:
      number: GPIO7
      inverted: true
      mode:
        input: true
        pullup: true
    filters:
      - debounce: 80ms    
    on_clockwise:
      then:
        - number.set:
            id: dialincrement
            value: 1

    on_anticlockwise:
          then:
            - number.set:
                id: dialincrement
                value: -1

binary_sensor:
  - platform: gpio
    pin:
      number: GPIO21
      inverted: true
      mode: INPUT_PULLUP
    name: "Dial Button"
    on_click:
      then:
        - select.set:
            id: dial_mode
            option: "Brightness"
    on_multi_click:
        - timing:
            - ON for at least 500ms
          then:
            - logger.log: "Button long-pressed"
            - switch.toggle: dial_toggle_switch 
        - timing:
            - ON for at least 150ms
            - OFF for at most 250ms
            - ON for at least 150ms
            - OFF for at least 250ms
          then:
            - logger.log: "Button double long-pressed"
            - select.set:
                id: dial_mode
                option: "Normal"
        - timing:
            - ON for at least 50ms
            - OFF for at most 200ms
            - ON for at least 50ms
            - OFF for at least 200ms
          then:
            - logger.log: "Double-Clicked"
            - select.set:
                id: dial_mode
                option: "ColorTemp"
        - timing:
            - ON for at least 50ms
            - OFF for at most 200ms
            - ON for at least 50ms
            - OFF for at most 200ms
            - ON for at least 50ms
            - OFF for at least 200ms
          then:
            - logger.log: "Triple-Clicked"
            - select.set:
                id: dial_mode
                option: "SceneCycle"

switch:
  - platform: template
    name: "DialToggleSwitch"
    id: dial_toggle_switch
    optimistic: true

select:
  - platform: template
    name: "Dial Mode"
    id: dial_mode
    options:
      - "Brightness"
      - "ColorTemp"
      - "SceneCycle"
      - "Normal"
    optimistic: true

This config attaches the physical button and both pins of the rotary encoder to the system. When the knob is rotated it updates the value of ‘dialincrement’ number to a 1 or -1 depending on the direction, clockwise or counter-clockwise.

The button is set up with the multi-click feature of ESP-Home. The time-intervals for each button click are defined as a sequence in milliseconds in the ‘timing’ sections. Actions are then performed per each definition of a timing. Each successive button press, (1, 2, and 3 clicks) set the mode to “Brightness”, “ColorTemp”, and “SceneCycle” respectively. “Brightness” is the default mode which, you guessed it, controls the global brightness of the room lights. I have set up a 30-second ‘timer helper’ on the Home Assistant side which is started every time any action is made on the dial. After 30 seconds since the last action, the timer expires and triggers another automation that sets the mode back to brightness control. When I turn the knob again after any amount of time later it will automatically be in brightness mode by default. I can return to the “Brightness” mode by single cling the knob at any time.

The “ColorTemp” mode which can be accessed after two clicks of the button allows for control of the global color temperature of the lights. Turning it clockwise is to make the lights warmer in color and counter-clockwise makes them cooler.

“SceneCycle” mode uses a ‘select helper’ in Home Assistant to cycle between a list of scenes I have set up as presets. This way I can do more fun things like change the colors and effects of the lights to ones I regularly enjoy.

The last mode is “Normal” which resets the lights back to a normal lighting scenario. Clicking the button twice slowly just triggers a scene which is a neutral light to reset the state. Pressing and holding the button toggles all of the lights on and off.

All in total the smart dial uses 9 automations, 1 script to reset the timer for brightness mode, and the YAML config above to create an intuitive system that was easy for me to remember. Of course the beauty of ESP-Home is that all of these custom behaviors can be simplified or modified for any other behavior when I want to change it. You could, for instance, have a a guest mode toggle that only allows for diming and toggling to avoid confusion. All of this can be updated over-the-air using a Wi-Fi connection and the Home Assistant frontend.

This was a nice weekend project (that I finished many weekends later) which creates an easy way for me to not need my phone to control my lights but still have full control over my Home Assistant setup.


Newsletter

A letter from my computer to yours focused on helping other makers. Included in each monthly issue are the resources and info I’m using to build my current projects as well as a summary of what went on at Hackbatch this month.