How to keep a rotary encoder (or other input-like sensor) synced with HA without looping

I’ve thrown together a esphome device with rotary encoder and oled screen to show/control the volume on my surround receiver.

The meat of it is:

sensor:
  - platform: rotary_encoder
    name: "volume"
    id: rcvolume
    pin_a: D3
    pin_b: D2
    min_value: 0
    max_value: 200
    on_value:
      then:
        - script.execute: pagetimeout

  # Sensor from home assistant showing onkyo volume.
  - platform: homeassistant
    name: "Onkyo Volume Acording to home assistant"
    id: "vknob_onkyo_volume"
    entity_id: media_player.onkyoreceiver
    attribute: volume_level
    accuracy_decimals: 0
    filters:
      - calibrate_linear:
          # Map values from 0-1 to 0-200
          - 0.0 -> 0.0
          - 1.0 -> 200
      - debounce: 1s #attempt but not solving issue
      - throttle: 3s      #attempt but not solving issue
    on_value:
      - sensor.rotary_encoder.set_value:
          id: rcvolume
          value: !lambda |-
            return id(vknob_onkyo_volume).state;
      - script.execute: pagetimeout

Changing the value from the device works great, but when I change the volume from the receiver the value propagates to the device and updates the view, it updates the rotary encoder steps so that turning the knob will always be in reference to the current volume so the volume doesn’t jump around.

Is there some way to update the rotary encoder steps without sending the value back out to HA? The main issue is if I slowly hunt the volume on the receiver, it tells home assistant, which tells my esphome device, that then tells HA which sets the volume in a constant loop until the value settles. Which is not desirable. I just want want them to stay in sync based on the last one to cause the change.

I considered having the device just send up/down commands and using the media_player sensor for the feedback, but its too slow to feel good.

debounce sounded like all I need but I tried debouncing for 1, 3, 5, 15 seconds and it didn’t seem to keep the issue of the volume sometimes jumping back to an old value sometimes.

The only think I can think of is fork the project and modify the rotary_encoder lib to have a setValueLocally which isn’t the answer I’d really like to choose.

any suggestions?

I’ve tried hiding the rotary encoder sensor and using a template sensor and manually sending the data when it changes for any reason other than data coming back from home assistant using a global variable. It does seem to work most of the time, though occasionally it does seem to still push an old value from HA through causing non-smooth volume changing using the the receiver it’s self.

globals:
  - id: should_send
    type: bool
    initial_value: 'true'

sensor:
  - platform: template
    name: "VKnob1 Value Template"
    id: exposed_volume
    update_interval: never
    accuracy_decimals: 3

  - platform: rotary_encoder
    name: "volume"
    internal: True # don't share this remotely
    id: rcvolume
    pin_a: D3
    pin_b: D2
    min_value: 0
    max_value: 200
    on_value:
      then:
        - script.execute: debounce_sendvolume
        - script.execute: pagetimeout

  # Sensor from home assistant showing onkyo volume.
  - platform: homeassistant
    name: "Onkyo Volume Acording to home assistant"
    internal: True # don't share this remotely
    id: "vknob_onkyo_volume"
    entity_id: media_player.onkyoreceiver
    attribute: volume_level
    accuracy_decimals: 0
    filters:
      - calibrate_linear:
          # Map values from 0-1 to 0-200
          - 0.0 -> 0.0
          - 1.0 -> 200
      - debounce: 1s
    on_value:
      - globals.set:
          id: should_send
          value: 'false'
      - sensor.rotary_encoder.set_value:
          id: rcvolume
          value: !lambda |-
            return id(vknob_onkyo_volume).state;
      - globals.set:
          id: should_send
          value: 'true'

script:
  - id: debounce_sendvolume
    mode: restart
    then:
      - delay: 0.1s
      - lambda: |-
          if(id(should_send)){
            id(exposed_volume).publish_state(id(rcvolume).state / 200.f);
          }

I completely decoupled the encoder value from the stored value and now manually send the template sensor on encoder events but not when it changes. This seems to work without any weird jank.

  globals:
    - id: rval
      type: int
      initial_value: '50'
    - id: last_knob_millis
      type: int
      initial_value: '0'
      
  sensor:
    - platform: template
      name: "VKnob1 Value Template"
      id: exposed_volume
      update_interval: never
      accuracy_decimals: 3
  
    - platform: rotary_encoder
      name: "volume"
      internal: True # don't share this remotely
      id: rcvolume
      pin_a: D3
      pin_b: D2
      on_clockwise: 
        then:
          - lambda: |-
              id(rval) = (id(rval) + 1) >= 200 ? 200 : id(rval) + 1;
              id(last_knob_millis) = millis();
          - script.execute: debounce_sendvolume
          - script.execute: pagetimeout
      on_anticlockwise:
        then:
          - lambda: |-
              id(rval) = (id(rval)-1) <= 0 ? 0 : id(rval) - 1;
              id(last_knob_millis) = millis();
          - script.execute: debounce_sendvolume
          - script.execute: pagetimeout
  
    # Sensor from home assistant showing onkyo volume.
    - platform: homeassistant
      name: "Onkyo Volume Acording to home assistant"
      internal: True # don't share this remotely
      id: "vknob_onkyo_volume"
      entity_id: media_player.onkyoreceiver
      attribute: volume_level
      accuracy_decimals: 0
      filters:
        - calibrate_linear:
            # Map values from 0-1 to 0-200
            - 0.0 -> 0.0
            - 1.0 -> 200
      on_value:
        - lambda: |-
            if(millis() - id(last_knob_millis) > 3000){
              id(rval) = id(vknob_onkyo_volume).state;
            }
        - script.execute: pagetimeout
 
    - id: debounce_sendvolume
      mode: restart
      then:
        - delay: 0.1s
        - lambda: |-
            id(exposed_volume).publish_state(id(rval) / 200.f);

I’m fine with this solution, still happy to hear other solutions

I think that rather than using the absolute value of the rotary encoder, just use on_clockwise and on_anticlockwise.

Then on_clockwise can trigger a media_player.volume_up service in HA.

what isn’t exactly clear form the code snippets is that I’m using an OLED to show the volume as the goal is to be able to control the volume when I can’t see/reach the avreceiver. The on_clockwise, call media_player.volume_up in HA, HA->avreciever, media_player.attributes.volume_level triggering the espdevice to draw the OLED screen feedback loop is a terrible user experience. I need all the updates to be optimistic for the display.

How so, describe why, take a video if necessary.

Not sure what this means.

Waiting for the value to be sent to the server validated and come back to update the number (that’s how the esphome sensor values operate) prevents it from smoothly changing the value.

Optimistic updates means the display changes the value to what I want it to be, not what it currently is, provided I am still interacting with the knob.

This solution worked acceptably but putting home assistant in between the the receiver and the esp was unnecessary lag and traffic, I ended up replacing it with custom sketch that connects directly to the receiver.

Thanks for checking in!