Detect when a Rotary Encoder is stationary (Cover Opearation)

Hi All,

I’m trying to work out how to detect when a rotary encoder is stationary, so i can set the cover operation to idle. I’ve tried a few things but i just can’t seem to get it to work in a stable way.

Project: Super Smart Garage Door with Dumb Controller

Summary: I have an old merlin garage door opener that simply has some contact to trigger. Depending on the weather i like to leave the door open 25% to allow air flow.

  1. Ultrasonic Sensor is used to measure the distance and effectively simulate limit switches
  2. Rotary Encoder is resting against the drive chain to get the actual door position.

My current problem is how do i set the “Current Operation” of the Cover. I can easily get the Opening, Closing, but how do i set Idle? When the rotary encoder is not moving. Here is some of my code:

The Cover:

cover:
  - platform: template
    device_class: garage
    name: "${friendly_name} Control"
    has_position: true
    optimistic: false
    assumed_state: false
    ### This Lambda is to ensure that the door has an open and closed value based on the Ultrasonic even if the door hasnt been calibrated. If the Globals have been set to True then it will calculate the Exact position.
    lambda: !lambda |- 
      if (id(closed_value_set) && id(open_value_set) ) {
         return ( (id(encoder_last_value))  /  (id(encoder_open_steps) ) );
      }  else  {
         if (id(door_state_closed_by_ultrasonic).state) {
           return 0.0;
         }  else  {
           return 1.0;
         }
      }
    
    id: ${devicename}_control
    open_action:
      - script.stop: door_position_command
      - lambda: id(position_request) = 1;
      - script.execute: door_position_command
    stop_action:
      - script.stop: door_position_command
      - script.execute: stop_door
    close_action:
      - script.stop: door_position_command
      - lambda: id(position_request) = 0;
      - text_sensor.template.publish:
          id: door_messages
          state: "Requesting Door Fully Close" 
      - script.execute: door_position_command
      - wait_until:
          binary_sensor.is_on: door_state_closed_by_ultrasonic
      - text_sensor.template.publish:
          id: door_messages
          state: "Door is now fully closed - Resetting Encoder to Zero" 
      - script.execute: reset_encoder_on_close
#      - logger.log: "Running Encoder Reset on Close"
    position_action:
      - script.stop: door_position_command
#      - lambda: id(${devicename}_position).state;
      - lambda: id(position_request) = pos;
      - lambda: ESP_LOGD("main", "Roller 1 setting position to %f", pos);
      - script.execute: door_position_command

The Sensors:

sensor:

  - platform: template
    name: "direction"
    id: direction
    update_interval: 0.5s
    internal: true
    lambda: !lambda |-
      return id(direction_encoder);
    filters:
      - lambda: |-
          if (id(movement).state) {
            return (x);
          } else {
            return 0.0;
          }
      - throttle: 1s

    on_value_range:
      - above: 0.9
        then:
          - cover.template.publish:
              id: ${devicename}_control
              current_operation: OPENING     
      - below: -0.9
        then:
          - cover.template.publish:
              id: ${devicename}_control
              current_operation: CLOSING     
    on_value:
      then:
        - if:
            condition:
              lambda: 'return id(direction).state == 0.0;'
            then:
              - cover.template.publish:
                  id: ${devicename}_control
                  current_operation: IDLE             

  - platform: rotary_encoder #this is the rotary encoder connected to the drive chain of the door
    name: "Rotary Encoder"
    id: door_rotary_encoder
    min_value: 0
    pin_a:
      number: D1
      mode: INPUT
    pin_b:
      number: D7
      mode: INPUT
    on_clockwise:
     - delay: 0.25s
     - globals.set:
         id: direction_encoder
         value: '1.0'

    on_anticlockwise:
     - delay: 0.25s
     - globals.set:
         id: direction_encoder
         value: '-1.0'

    on_value:
      then:
#        - globals.set:
#            id: direction_encoder
#            value: '0.0'
        - globals.set:
            id: rotation_detect
            value: 'true'
        - globals.set:
            id: encoder_last_value
            value: !lambda "return (x);"
        - delay: 0.75s
        - globals.set:
            id: rotation_detect
            value: 'false'

            
  - platform: ultrasonic  ### This is the US-100 Ultrasonic Sensor that measures the distance from the motor to the door attachment on the slider that the chain moves.
    name: "${friendly_name} Distance"
    id: ${devicename}_distance
    trigger_pin: GPIO12 #D6
    echo_pin: GPIO14 #D5
    update_interval: 1s
    unit_of_measurement: "cm"
    accuracy_decimals: 0
    pulse_time: 10us
    timeout: 20m
    internal: false
    filters:
      - filter_out: nan  # filter timeouts
      - multiply: 100
      - median:
          window_size: 15
          send_every: 5
          send_first_at: 3
      - lambda: |-
          if ((x) >= (${distance_closed}) ) {
            return (${distance_closed});
          } else if ((x) <= (${distance_open}) ) {
            return (${distance_open});
          } else {
            return x;
          }
    
      
    
          
  - platform: template # This displays the value of the variable Open Value when it is set
    name: "Global Steps Open as set"
    lambda: 'return (id(encoder_open_steps));'
    accuracy_decimals: 0
    update_interval: 5s
    
  - platform: template #calculate the actual position of the door in % using the last known step count for door open
    name: "Door Postion"
    id: ${devicename}_position
    unit_of_measurement: "pos"
    icon: "mdi:door"
    update_interval: 1s
    accuracy_decimals: 2
    lambda: return id(${devicename}_control).position;

  - platform: template
    name: "Position Request Value"
    lambda: 'return id(position_request);'
    update_interval: 2s
  
  - platform: template
    name: "Last known Value of Encoder"
    lambda: 'return id(encoder_last_value);'
    update_interval: 2s

Any ideas or suggestions?

Have you found solution already? I had the same problem and came across your old question

I haven’t found a better solution than preserving the previous count by timer and comparing it against latest count.

Essentially I’m using two global variables to track movement and one variable to track direction (-1, 0, 1) and only publish state change when direction changes.

Not the most elegant solution and I’d love to get an advice on how to improve it.

text_sensor:
  - platform: template
    name: "Garage Door Movement"
    id: garage_door_movement

globals:
  - id: movement_direction
    type: int
    restore_value: no
    initial_value: '0'
  - id: previous_encoder_position
    type: int
    restore_value: no
    initial_value: '0'
  - id: current_encoder_position
    type: int
    restore_value: no
    initial_value: '0'

interval:
  - interval: 1000ms
    then:
      if:
        condition:
          - lambda: return id(movement_direction) != 0;
        then:
          - if:
              condition:
                - lambda: return id(current_encoder_position) == id(previous_encoder_position);
              then:
                - logger.log: "Not turning"
                - lambda: id(movement_direction) = 0; id(current_encoder_position) = 0; id(previous_encoder_position) = 0; id(garage_door_movement).publish_state("");
                - output.turn_off: led
              else:
                - lambda: id(previous_encoder_position) = id(current_encoder_position);

sensor:
  - platform: rotary_encoder
    name: "Rotary Encoder"
    id: rotation_detector
    pin_a:
      number: 9
      mode:
        input: true
        pullup: true
      inverted: true
    pin_b:
      number: 8
      mode:
        input: true
        pullup: true
      inverted: true
    on_clockwise:
      - logger.log: "Turned Clockwise"
      - lambda: id(current_encoder_position) += 1;
      - if:
          condition:
            - lambda: return id(movement_direction) <= 0;
          then:
            - lambda: id(movement_direction) = 1; id(garage_door_movement).publish_state("Opening");
    on_anticlockwise:
      - logger.log: "Turned Anticlockwise"
      - lambda: id(current_encoder_position) -= 1;
      - if:
          condition:
            - lambda: return id(movement_direction) >= 0;
          then:
            - lambda: id(movement_direction) = -1; id(garage_door_movement).publish_state("Closing");
1 Like

how’s the project going? I finished installing a rotary encoder on my garage door and I thought this had been done in the past but i’m not having much luck finding information available. I havn’t tried your code but it looks ok.

I think i went away from using the direction sensors because they were overloading the messages. In the end I also went to a industrial rotary encoder which had a higher resolution.

I used the encoder to publish to a template sensor which updates every 0.5s.

sensor:
  - platform: rotary_encoder #this is the rotary encoder connected to the geared wheel inside the merlin box. I couldn't find a reliable way of doing it externally
    name: "Rotary Encoder"
    id: door_rotary_encoder
    publish_initial_value: true
    entity_category: diagnostic
    min_value: 0
    accuracy_decimals: 0
    pin_a:
      number: GPIO17 #the encoder i used has pullup resistors on each pulse pin
      mode: INPUT
    pin_b:
      number: GPIO18
      mode: INPUT
    filters:
      - multiply: 0.1
      - delta: 5
#      - throttle_average: 0.5s
    on_value:
      then: 
        - sensor.template.publish:
            id: door_position
            state: !lambda 'return ( (id(door_rotary_encoder).state)  /  (id(door_open_steps).state ) );'
#        - globals.set:
#            id: last_value_encoder
#            value: !lambda 'return (x);'
  - platform: template
    name: "Door Numerical Direction"
    id: door_direction_numerical
    entity_category: diagnostic
    lambda: |-
      return (id(door_rotary_encoder).state);
    filters:
      - lambda: |-
          static int last_value = 0;
          static int distance_change = 0;
          distance_change = x - last_value;
          last_value = x;
          return (distance_change);   
    update_interval: 0.5s

Then a template text sensor updates the direction. Its probably not the best way of doing it, but it has been working reliably for over a year.

text_sensor:
  - platform: template
    name: "Door Messages"
    id: door_messages
    entity_category: diagnostic
  - platform: template
    name: "Encoder Direction"
    id: encoder_direction
    entity_category: diagnostic
    lambda: |-
      if (id(door_direction_numerical).state > 0.0) {
        return {"Up"};
      } else if (id(door_direction_numerical).state < 0.0) {
        return {"Down"};
      } else {
        return {"Idle"};
      }
    on_value:
      - if:
          condition:
            text_sensor.state:
              id: encoder_direction
              state: 'Idle'
          then:
            - cover.template.publish:
                id: ${devicename}_control
                current_operation: IDLE
      - if:
          condition:
            text_sensor.state:
              id: encoder_direction
              state: 'Up'
          then:
            - cover.template.publish:
                id: ${devicename}_control
                current_operation: OPENING
      - if:
          condition:
            text_sensor.state:
              id: encoder_direction
              state: 'Down'
          then:
            - cover.template.publish:
                id: ${devicename}_control
                current_operation: CLOSING
    update_interval: 0.5s

Hope this helps.