Dimming lights with rotary encoder on ESP8266 and ESPHome

Hi, I’m trying to make a dimmer too. I just found out, that an automation with this data_template is not too optimal, because HA is handling every single pulse, the resolution just send multiple pulses at once. Any ideas, how to send only every 4th pulse as example? Or how to handle every 4th pulse in HA.

1 Like

OK, I found a solution:

sensor:
  - platform: rotary_encoder
    name: "Rotary Encoder"
    pin_a: PIN_A
    pin_b: PIN_B
    min_value: 0
    max_value: 100
    resolution: 2
    filters:
      - delta: 4.0

With delta, it will send data to HA if the difference is greater than delta’s value.

My automation:

- id: '1569433349039'
  alias: Bedroom Light Dimming
  trigger:
  - entity_id: sensor.rotary_encoder
    platform: state
  condition: []
  action:
  - data_template:
      brightness_pct: '{{states.sensor.rotary_encoder.state | int }}'
    service: light.turn_on
    entity_id: light.bedroom_light
3 Likes

Thanks guys! I really appreciate your help! Other on-going project kept me away from the dimmer, but I will resume it asap to integrate in my home setup.

Hi all

I have been messing about with this for a while. I have rotary encoders wired to wemos d1 minis where my dimmers would be on the walls and zigbee lights in the ceiling

Originally I created some custom MQTT code for the dimmers which was successful, but behaved a bit weirdly.

I now have what I believe is the best solution:

The rotarys just send status to home assistant through esphome

Then I use node red to handle the switch and the rotary movement.

The issue I was coming across was that every time the rotary is moved, the brightness is changed. If you rotate it fast, it generates a huge load/lag on the system while it catches up sending instructions to the lights.

So I installed ‘debounce’ in node red and set up a limit of 250ms with a counter. Basically, when you turn the rotary, node red counts how many times you’re rotating it in that 250ms and then turns that into a command for the lights.

This way, if you want a big adjustment, you rotate it fast and alot and if you want better control you rotate it slowly. There’s still a tiny lag (which is inevitable), but the whole system works brilliantly now.

I also went ahead and added a virtual switch to the esphome which is toggled by a long press on the dimmer. This is set to toggle the colour mode, so I can control brightness and colour temperature at the wall.

Happy to share any code, etc. if this is useful to anyone.

2 Likes

I’d like the code of your flow! I want to use òn_clockwise and òn_anticlockwiseto increase and decrease brightness. Seems as if Node-RED is the way to go.

Okey dokey.

So hopefully you can make sense of this.

I have two subflows. 1 is for rooms with colour temperature changing lights and adds a long press to the rotary encoder that switches to colour change mode instead of brightness. This is what my main flow looks like:

The brains for the rotary adjustment is in the dimmerControl subroutine. This has some hopefully self explanatory environmental variables:-

Then these are passed into the subflow:-

The magic happens at the top with the debounce node in combination with a counter. It’ll pass a number of how many times it’s been rotated in 250ms through the flow. The rest of the flow handles the environmental variables and then increments or decrements the brightness. It’ll stop at the max brightness set in the environmental variable and turn the light off if it hits minimum brightness.

Here’s the subflow:-

[{"id":"f4daa825.5dd3f8","type":"subflow","name":"dimmerControl (no colour)","info":"","category":"","in":[{"x":40,"y":80,"wires":[{"id":"2af0480a.6ebf38"}]}],"out":[],"env":[{"name":"light","type":"str","value":""},{"name":"brightstep","type":"num","value":"5"},{"name":"brightmin","type":"num","value":"5"},{"name":"brightmax","type":"str","value":"255"}],"color":"#DDAA99"},{"id":"2af0480a.6ebf38","type":"counter","z":"f4daa825.5dd3f8","name":"","init":"0","step":"1","lower":null,"upper":null,"mode":"increment","outputs":"1","x":180,"y":80,"wires":[["ce11729f.961e3"]]},{"id":"aa88fac1.a39c28","type":"debounce","z":"f4daa825.5dd3f8","time":"250","name":"debounce","x":520,"y":80,"wires":[["2b2bdf57.7b2f3","c536d7.0e53b928"]]},{"id":"2b2bdf57.7b2f3","type":"change","z":"f4daa825.5dd3f8","name":"reset count","rules":[{"t":"set","p":"reset","pt":"msg","to":"true","tot":"bool"}],"action":"","property":"","from":"","to":"","reg":false,"x":670,"y":80,"wires":[["2af0480a.6ebf38"]]},{"id":"ce11729f.961e3","type":"switch","z":"f4daa825.5dd3f8","name":"if non-zero","property":"count","propertyType":"msg","rules":[{"t":"neq","v":"0","vt":"num"}],"checkall":"true","repair":false,"outputs":1,"x":350,"y":80,"wires":[["aa88fac1.a39c28"]]},{"id":"b7c452a6.45573","type":"comment","z":"f4daa825.5dd3f8","name":"How many times have we moved in 250ms interval?","info":"","x":310,"y":40,"wires":[]},{"id":"992273c2.55bc","type":"api-current-state","z":"f4daa825.5dd3f8","name":"Current state - light","server":"e733376d.c37508","version":1,"outputs":1,"halt_if":"","halt_if_type":"str","halt_if_compare":"is","override_topic":false,"entity_id":"${light}","state_type":"str","state_location":"state","override_payload":"msg","entity_location":"lightdata","override_data":"msg","blockInputOverrides":false,"x":230,"y":280,"wires":[["5d3ed6e8.5482c8"]]},{"id":"5d3ed6e8.5482c8","type":"function","z":"f4daa825.5dd3f8","name":"Increment or Decrement?","func":"if (Number(msg.data.old_state.state) < Number(msg.data.new_state.state)) {\n    return [ msg, null ];\n} else {\n    return [ null, msg ];\n}","outputs":2,"noerr":0,"initialize":"","finalize":"","x":450,"y":360,"wires":[["92e9856d.0fee18"],["4fbf4574.3ea10c"]]},{"id":"2ec45dd6.827172","type":"change","z":"f4daa825.5dd3f8","name":"+ brightness","rules":[{"t":"set","p":"lightdata.attributes.brightness","pt":"msg","to":"$min([$number(lightdata.attributes.brightness + ($number(env.brightstep)*count)), $number(env.brightmax)])","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":550,"y":500,"wires":[["73047e8d.2f322"]]},{"id":"4fdc3278.fd253c","type":"change","z":"f4daa825.5dd3f8","name":"- brightness","rules":[{"t":"set","p":"lightdata.attributes.brightness","pt":"msg","to":"$max([$number(brightmin), $number(lightdata.attributes.brightness - ($number(env.brightstep)*count))])","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":570,"y":660,"wires":[["73047e8d.2f322"]]},{"id":"73047e8d.2f322","type":"api-call-service","z":"f4daa825.5dd3f8","name":"light_on","server":"e733376d.c37508","version":1,"debugenabled":false,"service_domain":"light","service":"turn_on","entityId":"${light}","data":"{\"brightness\": {{lightdata.attributes.brightness}}}","dataType":"json","mergecontext":"","output_location":"","output_location_type":"none","mustacheAltTags":false,"x":820,"y":500,"wires":[[]]},{"id":"4fbf4574.3ea10c","type":"switch","z":"f4daa825.5dd3f8","name":"brightness at bright min?","property":"lightdata.attributes.brightness","propertyType":"msg","rules":[{"t":"gt","v":"brightmin","vt":"env"},{"t":"lte","v":"brightmin","vt":"env"}],"checkall":"true","repair":false,"outputs":2,"x":370,"y":700,"wires":[["4fdc3278.fd253c"],["247dca32.696136"]]},{"id":"247dca32.696136","type":"api-call-service","z":"f4daa825.5dd3f8","name":"light_off","server":"e733376d.c37508","version":1,"debugenabled":false,"service_domain":"light","service":"turn_off","entityId":"${light}","data":"","dataType":"json","mergecontext":"","output_location":"","output_location_type":"none","mustacheAltTags":false,"x":800,"y":700,"wires":[[]]},{"id":"c536d7.0e53b928","type":"change","z":"f4daa825.5dd3f8","name":"copy env vars","rules":[{"t":"set","p":"env.brightmin","pt":"msg","to":"brightmin","tot":"env"},{"t":"set","p":"env.brightstep","pt":"msg","to":"brightstep","tot":"env"},{"t":"set","p":"env.brightmax","pt":"msg","to":"brightmax","tot":"env"}],"action":"","property":"","from":"","to":"","reg":false,"x":140,"y":200,"wires":[["992273c2.55bc"]]},{"id":"92e9856d.0fee18","type":"switch","z":"f4daa825.5dd3f8","name":"light off?","property":"lightdata.state","propertyType":"msg","rules":[{"t":"neq","v":"off","vt":"str"},{"t":"eq","v":"off","vt":"str"}],"checkall":"true","repair":false,"outputs":2,"x":360,"y":540,"wires":[["2ec45dd6.827172"],["581a8f95.14cfa"]]},{"id":"581a8f95.14cfa","type":"change","z":"f4daa825.5dd3f8","name":"brightness to min bright","rules":[{"t":"set","p":"lightdata.attributes.brightness","pt":"msg","to":"$number(env.brightmin)","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":570,"y":560,"wires":[["73047e8d.2f322"]]},{"id":"8cd6de54.b82f8","type":"comment","z":"f4daa825.5dd3f8","name":"Take the environmental vars into the msg","info":"","x":200,"y":160,"wires":[]},{"id":"6562bb6f.296884","type":"comment","z":"f4daa825.5dd3f8","name":"Get the current state of the light - msg. state msg.lightdata","info":"","x":250,"y":240,"wires":[]},{"id":"dbcc9e65.15529","type":"comment","z":"f4daa825.5dd3f8","name":"Rotary increased or decreased?","info":"","x":170,"y":320,"wires":[]},{"id":"e733376d.c37508","type":"server","name":"Home Assistant","legacy":false,"addon":true,"rejectUnauthorizedCerts":true,"ha_boolean":"y|yes|true|on|home|open","connectionDelay":true,"cacheJson":true}]

I hope that’s understandable!

1 Like

Thank you for the response!

Before you answered, I was playing around and came up with this:

The top one is for adjusting the brightness with rotation, and the bottom one turns the light on and off by pushing the knob.

I use this in conjunction with a template sensor that I feed into esphome, so that if I change the brightness in any other way than with the rotary encoder, the rotary encoder gets that value. I also set the min and max for the enoder to 0 and 100.

Apart from the color control, is there anything your flow does that mine doesn’t? I feel like I might be missing something.

So yours will slow the adjustment to a maximum of 1 step per 250ms.
Mine counts the steps made per 250ms and then fires off a brightness command accounting for the steps.
The result is that you can turn the rotary fast and get a larger brightness change, or turn it slow for a more delicate change.

Apart from that, it just deals with minimum and maximum conditions (turning off below minimum, turning on at minimum if rotated while off).

Actually, the flow above does that too. The debounce node passes on a maximum of 1 value per 250 ms, but that doesn’t mean that it only passes on 1 brightness step up or down. If I turn the rotary enconder fast so that it goes from 10 to 45 in 250 ms, then the debounce node will pass on the value 45 to the set-brightness-node, but omit all the steps between 10 and 45.

It also turns off when brightness is turned down to 0% and turns on if brightness is increased from 0%. I set the stepsize to 4 in the esphome rotary button config to avoid having to turn the know a 100 times for 100%.

If you want to simplify your flow, I think the above one would do the same as yours :slight_smile:

1 Like

Ah ok. Yeah I see. It’s because you’ve got the rotary value attached to the light.

I think at some point I abandoned that idea. I have 3 dimmers in the master bedroom and it was adding lag updating all the values.

This would be very useful to me. If you could share the code would be great.

to whoever bumps into this topic:
EspHome has a built in debounce filter

My code is this; I have a 20 step encoder. The first sensor is the rotary encoder itself, with debounce. The second sensor is receiving the new state from HA in case the state is changed from the frontend.

sensor:
  - platform: rotary_encoder
    name: "Aanrecht dimmer knop"
    id: my_rotary_encoder
    resolution: 1
    min_value: 0
    max_value: 20
    pin_a:
      number: GPIO26
      mode:
        input: true
        pullup: true
    pin_b:
      number: GPIO27
      mode:
        input: true
        pullup: true
    filters:
      debounce: 0.25s
  - platform: homeassistant
    name: Actual Light State
    id: actual_light_state
    entity_id: input_number.aanrecht_dimmer_knop_value
    on_value:
        then:
          - sensor.rotary_encoder.set_value:
              id: my_rotary_encoder
              value: !lambda 'return id(actual_light_state).state ;'

This automation translates the value from the rotary encoder (0-20) to a brightness value (0-255) following a square root function. I like this better than a linear profile. sqrt(encoder * 3250)

alias: 'Keuken aanrecht: rotary dim kitchen light'
description: ''
trigger:
  - platform: state
    entity_id: sensor.aanrecht_dimmer_knop
condition: []
action:
  - service: light.turn_on
    target:
      entity_id: light.keuken_aanrecht_groep
    data:
      brightness: '{{ (((trigger.to_state.state | float(0)) * 3250)**0.5) | round(0) }}'
mode: single

This function saves the actual brightness to an input_number following the inverse of the function above, to correlate brightness with the steps of the encoder. Trick here is the restart mode in combination with the 2 seconds delay; it functions as a debounce filter. (brightness^2)/3250

alias: 'Keuken aanrecht: Sync rotary kitchen light'
description: ''
trigger:
  - platform: state
    entity_id: light.keuken_aanrecht_groep
    attribute: brightness
condition: []
action:
  - delay:
      hours: 0
      minutes: 0
      seconds: 2
      milliseconds: 0
  - service: input_number.set_value
    target:
      entity_id: input_number.aanrecht_dimmer_knop_value
    data:
      value: >-
        {{ ((float(state_attr("light.keuken_aanrecht_groep", "brightness"),0)|
        int )**2/3250) | round(0) }}
mode: restart

This input_number is picked up by ESPHome and used as set_value for the rotary encoder.

6 Likes

I ran into an issue. Both the light (in HA) and the ESP were maintaining the absolute dimmer position. I had to keep them in sync which was a pain; updates were bouncing back and forth.

I have now changed the ESP to only issue an incremental value, generated within the debounce time frame. It creates a number in HA.

There is a global variable which stores the previous encoder position and it is subtracted from the current encoder position.

I hope this helps someone :slight_smile:

globals:
  - id: aanrechtoldvalue
    type: int
    restore_value: no
    initial_value: '0'
  

number:
  - platform: template
    name: Aanrecht Increment
    id: aanrechtincrement
    min_value: -20
    max_value: 20
    step: 1
    optimistic: true
    initial_value: 0
  
sensor:
  - platform: rotary_encoder
    name: "Aanrecht dimmer knop"
    id: aanrecht_rotary_encoder
    resolution: 1
    pin_a:
      number: GPIO26
      mode:
        input: true
        pullup: true
    pin_b:
      number: GPIO27
      mode:
        input: true
        pullup: true
    filters:
      debounce: 0.25s
    on_value:
      then:
        - number.set:
            id: aanrechtincrement
            value: !lambda 'return id(aanrecht_rotary_encoder).state - id(aanrechtoldvalue);'
        - globals.set:
            id: aanrechtoldvalue
            value: !lambda 'return id(aanrecht_rotary_encoder).state;'

The automation in HA picks up the new number value and issues a brightness_step command with that increment. I have 20 steps for a brightness resolution of 255, so multiplied by 13. There is also a pir sensor looking at the dimmer knob, so I have to disable that for a short while. The automation is queued so multiple number changes are being picked up correctly.

alias: 'Keuken aanrecht: rotary dim kitchen light (increment)'
description: ''
trigger:
  - platform: state
    entity_id: number.aanrecht_increment
condition: []
action:
  - service: light.turn_on
    target:
      entity_id: light.keuken_aanrecht_groep
    data:
      brightness_step: '{{ (((trigger.to_state.state | float(0)) * 13)) | round(0) }}'
  - service: input_boolean.turn_on
    target:
      entity_id: input_boolean.aanrecht_override_pir
  - delay:
      hours: 0
      minutes: 0
      seconds: 30
      milliseconds: 0
  - service: input_boolean.turn_off
    target:
      entity_id: input_boolean.aanrecht_override_pir
mode: queued
max: 10

and

alias: 'Keuken Aanrecht: Reset Override PIR'
description: ''
trigger:
  - platform: state
    entity_id: input_boolean.aanrecht_override_pir
    to: 'on'
condition: []
action:
  - delay:
      hours: 0
      minutes: 0
      seconds: 30
      milliseconds: 0
  - service: input_boolean.turn_off
    target:
      entity_id: input_boolean.aanrecht_override_pir
mode: restart

5 Likes

Thanks, I will try this. It seems better to do most the processing in ESP.

Hello

Thank you for The code, works pretty well. Smart solution,
Would it be possible to limit The dimmer value so it doesent turn of The light? Just stop on The minimum value.
Because i use The switch for on off toogle.

This was really useful. Especially since I’ll also be using this for my keuken! Many thanks.

I’m trying to configure a rotary encoder as described above, but it did not work for me. Then I simplified the code (see below), just to check if I would get a log message and not even that worked. I also tested with and without the debounce filter and using inverted and pullup as both true and false. None of the combinations worked for me. My encoder does not have a breakout board, as it would not fit the limited space I have, but I think this should not be a problem, as the ESP32 already has pullup in its breakout board. I did check the encoder for continuity with a meter and it seems ok., so it is not a faulty component.
Here is the code I used:

sensor:
  - platform: rotary_encoder
    name: "dim_Lucas" 
    pin_a:
      number: 18
      inverted: true
      mode:
        input: true
        pullup: true
    pin_b:
      number: 19
      inverted: true
      mode:
        input: true
        pullup: true
    id: dim_lucas        
    resolution: 1
    min_value: 0
    max_value: 25
    on_clockwise:
      - logger.log: "Turned Clockwise"
    on_anticlockwise:
      - logger.log: "Turned Anticlockwise"

Any ideas on how to solve this? :thinking:

How do you have things wired? Is the light controlled by the same esp board that the rotary encoder is on? You only show the rotary encoder part of your code so what type of light are you using? What hardware are you using? Is there a mosfet in here?. You’ve only given half the equation here. There are several eats to do this but if the light Is on the same board, is it even dimmable, do uou have the hardware to dim it, the type of light, etc all matter if you

Are you even seeing the rotary encoder values change in your logs? That’s how you know if it’s working or not. No need to speculate or wonder if it’s working, just look at the logs. The number should increase when spun one direction and decrease the other direction. Are you sure it’s a rotary encoder and not a potentiometer? They can look identical but function completely different. You’ve not provided much information, that’s why no one is answering youm

Thanks @Fallingaway24 for the reply.
The hardware is based on mosfet boards powering up COB LEDs, controlled by the same ESP32 connected to the rotary encoder. The lights work fine when controlled directly by HA but I’m struggling with the rotary encoder. I found the cause of the problem I mentioned earlier, it was related to the ESP pins I used. I was not even getting anything from it in the logs, but when I changed the pins to 32 and 33 it started working.
The problem now is how to get a stable signal from the rotary encoder. I created a new post, as this is a different problem. I will either have to replace the cable or add a new ESP32 closer to the encoder and use a different programming approach.