Door Position Sensor

Going nuts here… cannot figure out what i’m missing and have been searching everything i can find.

I’m trying to have my door report its ‘Actual’ position. I am using and HC-SR04 for the position measurement and have a sensor template setup to provide the position between 0.0 or 1.0, which is working.

I can’t for the life of me figure out how to get the position reported back. Ultimately i want to be able to set the door to go to a specific position.

Here’s what i have:

esphome:
  name: garage_door_v1
  platform: ESP8266
  board: d1_mini

wifi:
  ssid: "******"
  password: "*******"

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Garage Door V1 Fallback Hotspot"
    password: "******"

substitutions:
  devicename: garagedoor
  description: OpenGarage.io Device
  friendly_name: Garage Overhead Door
  auto_close_delay: "300s" # 5 minutes
  buzzer_delay: "0.5s"
  open_distance: "99" # fully open less than or equal in cm
  closed_distance: "0" #closed is greater than 230 
  open_close_time: "20s" # allow 20s for motion to complete
  reporting_delta: "3" # minimum 25cm change
  distance_closed: "254.0"
  distance_open: "38.0"

captive_portal:

# Enable logging
logger:

# Enable Home Assistant API
api:

ota:

binary_sensor:
  - platform: gpio
    pin: GPIO2
    name: "Door PIR North"
    device_class: motion
  - platform: gpio
    pin: GPIO4
    name: "Door PIR South"
    device_class: motion
  

  
switch:
    # The door opener contact (internal)
  - platform: gpio
    id: garage_button
    pin: 13
    restore_mode: ALWAYS_OFF
    internal: true
    on_turn_on:
    - delay: 500ms
    - switch.turn_off: garage_button

output:
  - platform: esp8266_pwm
    id: buzzer
    pin: D1
    frequency: 1000 Hz

sensor:
  - platform: ultrasonic
    name: "${friendly_name} Distance"
    id: ${devicename}_distance
    trigger_pin: GPIO12
    echo_pin: GPIO14
    update_interval: 0.25s
    unit_of_measurement: "cm"
    accuracy_decimals: 2
    pulse_time: 10us
    timeout: 20m
    internal: false
    filters:
     # - lambda: return ((( x - 254.0 ) / ( 35.0 - 254.0 ) ) / 100.0 );
     # - lambda: return ((( x - ${distance_open} ) / ( ${distance_closed} - ${distance_open} ) ) / 100 );
      - filter_out: nan  # filter timeouts
      - multiply: 100
      - median:
          window_size: 5
          send_every: 4
          send_first_at: 3
      #- sliding_window_moving_average:
       #   window_size: 5
        #  send_every: 3
         # send_first_at: 3
      #- delta: ${reporting_delta}

  - platform: template
    name: "Door Position"
    id: ${devicename}_position
    accuracy_decimals: 2
    unit_of_measurement: "pos"
    icon: "mdi:door"
    lambda: return (( id(${devicename}_distance).state - ${distance_open} ) / ( ${distance_closed} - ${distance_open} ));
    #lambda: |-
    #  if (id(${devicename}_distance).state > 254.0 ) {
     #   return 999;
    #  } else if (id(${devicename}_distance).state > 35.0) {
    #    return 0;
    #  } else {
    #    return (35.0 - (int) id(${devicename}_distance).state);
    #  }
    update_interval: 0.5s


globals:
  - id: last_door_reading
    type: float
    restore_value: no

      
cover:
  - platform: template
    device_class: garage
    name: "${friendly_name} Control"
    has_position: true
    id: ${devicename}_control
    # set the distance so it is equal or less than $open_distance when open
    lambda: !lambda |-
      if (id(${devicename}_distance).state >= ${closed_distance}) {
        return COVER_CLOSED;
      } else {
        return COVER_OPEN;
      }
    open_action:
       - if:
          condition:
            lambda: "return id(${devicename}_control).position == cover::COVER_CLOSED;"
          then:
            - output.turn_on: buzzer
            - delay: 2s
            - output.turn_off: buzzer
            - switch.turn_on: garage_button
            - delay: ${open_close_time}
            - script.execute: auto_close
    stop_action:
      - switch.turn_on: garage_button
    close_action:
      - if:
         condition:
           lambda: "return id(${devicename}_control).position == cover::COVER_OPEN;"
         then:
           - script.execute: buzzer_close
           - delay: 3s
           - switch.turn_on: garage_button
           # Disable any auto close pending action
           - script.stop: auto_close
           - delay: ${open_close_time}
            
script:
  - id: auto_close
    then:
      - logger.log: "Automtically closing in ${auto_close_delay}"
      - delay: ${auto_close_delay}
      - logger.log: "Initiating automatic garage closing"
      - cover.close: ${devicename}_control
  - id: buzzer_close
    then:
      - output.turn_on: buzzer
      - delay: 1s
      - output.turn_off: buzzer
      - output.turn_on: buzzer
      - delay: 1s
      - output.turn_off: buzzer
      - output.turn_on: buzzer
      - delay: 1s
      - output.turn_off: buzzer   

Interesting idea you have there.

There is something called position_action in the cover template.

position_action (Optional, Action): The action that should be performed when the remote (like Home Assistant’s frontend) requests the cover be set to a specific position. The desired position is available in the lambda in the pos variable.

What happens in home assistant when you define one of those?
Sounds like the thing you are looking for.

/Mattias

Thanks, its a work in progress. I’ll share more when I’m done.

My main question is: How do I tell ESPHome how to get the exact position. I know it needs to be between 0.0 and 1.0 and achieved this via a Sensor Template.

Surely I need to tell ESPhome which sensor to look at for the position?

Hi.

Maybe I understood you wrong. I thought you wanted the position from Home assistant to esphome.
So in Home assistant you set a value between 0-1 and then your esphome should act on this.
I have not tried this myself. So if you define the ''position_action" I thought maybe there going to appear a control in Home Assistant make you control for that device. The position_action need not be doing anything just to try. Maybe logging something. Based on this:

If that doesn’t work you could listen to a home assistant sensor. There are these helpers in HA. On of them are a slider. You may be listen to the from esphome.

I connected a display to my esphome that showed a tempeture and a text from home assistant.
Looked like this:

text_sensor:
  - platform: homeassistant
    name: "EspText"
    id: esp_text
    entity_id: input_text.esptext

sensor:
  - platform: homeassistant
    name: "Ute temp"
    id: ute_temp
    entity_id: sensor.ute_temp_combined

display:
  - platform: ssd1306_i2c
    model: "SSD1306 64x48"
    reset_pin: D0
    address: 0x3C
    lambda: |-
      it.image(0, 0, id(my_image));
      it.printf(55, 30,id(my_font), ".%1d", (int)round(10*(id(ute_temp).state-floor(id(ute_temp).state))));
      it.printf(0, 30,id(large_font), "% 3.0f", floor(id(ute_temp).state));```

That transport states from home assistant to esphome.

/Mattias

I got it! Finally… a bit more to do. But thought i’d drop the code quickly to help anyone else out.

Key was to put the publish the position in the template sensor. I’ve also had to use an absolute value calculation to reverse the Open and Close states.

esphome:
  name: garage_distance_sensor
  platform: ESP8266
  board: d1_mini

wifi:
  ssid: "*************"
  password: "**********"

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Garage Distance Sensor"
    password: "**********"

captive_portal:

# Enable logging
logger:

# Enable Home Assistant API
api:

ota:

substitutions:
  devicename: garagedoor
  description: OpenGarage.io Device
  friendly_name: Garage Overhead Door
  auto_close_delay: "300s" # 5 minutes
  buzzer_delay: "0.5s"
  open_distance: "99" # fully open less than or equal in cm
  closed_distance: "0" #closed is greater than 230 
  open_close_time: "20s" # allow 20s for motion to complete
  reporting_delta: "3" # minimum 25cm change
  distance_closed: "254.0"
  distance_open: "36.0"


binary_sensor:
  - platform: gpio
    pin: GPIO2
    name: "Door PIR North"
    device_class: motion
  - platform: gpio
    pin: GPIO4
    name: "Door PIR South"
    device_class: motion
  
switch:
    # The door opener contact (internal)
  - platform: gpio
    id: garage_button
    pin: 13
    restore_mode: ALWAYS_OFF
    internal: true
    on_turn_on:
    - delay: 500ms
    - switch.turn_off: garage_button

output:
  - platform: esp8266_pwm
    id: buzzer
    pin: D1
    frequency: 1000 Hz

sensor:
  - platform: ultrasonic
    name: "${friendly_name} Distance"
    id: ${devicename}_distance
    trigger_pin: GPIO12
    echo_pin: GPIO14
    update_interval: 0.25s
    unit_of_measurement: "cm"
    accuracy_decimals: 2
    pulse_time: 10us
    timeout: 20m
    internal: false
    filters:
      - filter_out: nan  # filter timeouts
      - multiply: 100
      - median:
          window_size: 5
          send_every: 4
          send_first_at: 3


  - platform: template
    name: "Door Position"
    id: ${devicename}_position
    accuracy_decimals: 2
    unit_of_measurement: "pos"
    icon: "mdi:door"
    lambda: |-
      return (( abs(id(${devicename}_distance).state - ${distance_open} - ${distance_closed}) ) / ( ${distance_closed} - ${distance_open} ));
    on_value:
      then:
        - cover.template.publish:
            id:  ${devicename}_control
            position: !lambda "return id(${devicename}_position).state;"
      
    update_interval: 0.5s


globals:
  - id: last_door_reading
    type: float
    restore_value: no
  - id: position
    type: float


      
cover:
  - platform: template
    device_class: garage
    name: "${friendly_name} Control"
    has_position: true
    optimistic: true
    id: ${devicename}_control
   # lambda: return pos = id(${devicename}_position);
    #lambda: return id(${devicename}_position).position;
    open_action:
       - if:
          condition:
            lambda: "return id(${devicename}_control).position == cover::COVER_CLOSED;"
          then:
            - output.turn_on: buzzer
            - delay: 2s
            - output.turn_off: buzzer
            - switch.turn_on: garage_button
            - delay: ${open_close_time}
            - script.execute: auto_close
    stop_action:
      - switch.turn_on: garage_button
    close_action:
      - if:
          condition:
            lambda: "return id(${devicename}_control).position == cover::COVER_OPEN;"
          then:
           - script.execute: buzzer_close
           - delay: 3s
           - switch.turn_on: garage_button
           # Disable any auto close pending action
           - script.stop: auto_close
           - delay: ${open_close_time}
    position_action:
      - lambda: |-
           id(${devicename}_position);       
script:
  - id: auto_close
    then:
      - logger.log: "Automtically closing in ${auto_close_delay}"
      - delay: ${auto_close_delay}
      - logger.log: "Initiating automatic garage closing"
      - cover.close: ${devicename}_control
  - id: buzzer_close
    then:
      - output.turn_on: buzzer
      - delay: 1s
      - output.turn_off: buzzer
      - output.turn_on: buzzer
      - delay: 1s
      - output.turn_off: buzzer
      - output.turn_on: buzzer
      - delay: 1s
      - output.turn_off: buzzer     

Ok, now i need some ideas on how to set the direction of the door. Is it going up or down.

To do this i have a variable called last_door_reading . I was then going to use a template sensor to return door raising or lowering or idle depending on whats happening.

Any ideas?

Position Template Sensor:

  - platform: template
    name: "Door Position"
    id: ${devicename}_position
    accuracy_decimals: 2
    unit_of_measurement: "pos"
    icon: "mdi:door"
    lambda: |-
      return (( id(${devicename}_distance).state - ${distance_open} ) / ( ${distance_closed} - ${distance_open} ));
    on_value:
      then:
        - cover.template.publish:
            id:  ${devicename}_control
            position: !lambda "return id(${devicename}_position).state;"
        - lambda: !lambda "(id(last_door_reading) = x);"

Current Operation Sensor:

  - platform: template
    name: ${devicename}_operation
    id: ${devicename}_operation
    lambda: |-
      if ( 5 > id(${devicename}_position).state) {
        return id(${devicename}_control).current_operation == cover::COVER_OPERATION_OPENING;
      } else {
        return 0.0;
      }

Cant you just calculate the difference between the last value and the current value.

diff = lastvalue-now

If the values are the same = not moving (diff== 0)
if the difference its negative then going up.
if the difference is positive the going down.
(if 1 is fully open and 0 closed )

Then set the current_state based on that.

(Comparing two floats can causes problem due to the nature of how float are stored on a computer/cpu registers)

is this what you looking for.
/Mattias

Yup! Late last night i found somebody using this… i’m curious if theres a simpler way using diff =1 like your suggestion. I’ve tidied my yaml up a bit and have got it reporting the direction.

I think i’m getting there slowly. One thing im really struggling with is Lambdas and the syntax. I can’t find what the limitations are and what i can and can’t do in a lambda. Is it just straight C++? or is it like Arduino.

sensor:
  - 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
    echo_pin: GPIO14
    update_interval: 0.25s
    unit_of_measurement: "cm"
    accuracy_decimals: 2
    pulse_time: 10us
    timeout: 20m
    internal: false
    filters:
      - filter_out: nan  # filter timeouts
      - offset: -0.36
      - multiply: 100
      - median:
          window_size: 5
          send_every: 2
          send_first_at: 2

  - platform: template  #This sensor takes the reading from the Ultrasonic and converts into a value between 0 and 1 to define the door position.
    name: "Door Position"
    id: ${devicename}_position
    accuracy_decimals: 2
    unit_of_measurement: "pos"
    icon: "mdi:door"
    lambda: |-
      return (( id(${devicename}_distance).state ) / ( ${distance_closed} - ${distance_open} ));
    on_value:
      then:
        - cover.template.publish:
            id:  ${devicename}_control
            position: !lambda "return id(${devicename}_position).state;"


  - platform: template #This sensor calculates the difference between the last reading. A negative will indicate closing and positive opening.
    name: "Door Movement"
    id: door_movement
    lambda: |-
      return (id(${devicename}_distance).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
  

text_sensor:
  - platform: template
    name: "Door Direction"
    id: door_direction
    lambda: |-
      if (id(door_movement).state > 0.0) {
        return {"Up"};
      } else if (id(door_movement).state < 0.0) {
        return {"Down"};
      } else {
        return {"None"};
      }
    update_interval: 2s
1 Like

Hi.

It’s c++. But everything is generated. So the lambda expression are what I understand just pasted in. Where all the id(…) is transferred to generated instances of the for example a Sensor. So you could be rather creative. I think global variables becomes small classes or structs. You can probably find the generated code in the folder with the same folder with the same name as the yaml file in the file system.
I converted an arduino sketch loop part to a big lambda where used global variables for the states. So the lambdas can be rather large.
There is also the custom components that are more c++ like. But I have not used these.

This is how I have understood it.

There are probably better solutions to solve directions. But if’s working that’s great.
When you have everything working as it should you refactor and makes it nicer.

/Mattias

Thank you for posting this thread. I have modified the config and plan to try it out in my garage with a VL53L1x sensor sometime. I don’t have a coding background so being able to work off of your config is much appreciated.

I’ve done a heap more with it since then. I could never get the ultrasonic sensor working accurately and responsive enough. I ended up using a rotary encoder. I messed around with a LiDAR distance sensor but couldn’t seem to get the custom uart component working.

It’s now working fine and the intention is to use the ultrasonic sensor as a way of self-calibrating the encoder to account for any slippage and drift.

I’ll post the code a bit later.

1 Like

I was thinking the same thing; a rotary encoder on the chain/belt would be an ideal way to track door distance without introducing noise or errors. I’ll take a look at my system and see if I can envision one I can implement myself.

I’m sure many of us will look forward to your new config.

I’m interested also.

Re: rotary encoder, there was a thread about this, with photos to document his journey along the way.

I’ve put the code up on Git. I’ll add some more details and photos to it shortly.

Here is a link to the encoder i used:
https://core-electronics.com.au/incremental-photoelectric-rotary-encoder-400p-r.html

1 Like

Regarding optimal hardware for movement/distance tracking:

I’m too much of a novice to try to devise a solution based off of this optical mouse concept, but perhaps someone with more coding knowledge would be interested. I think it would be a neat way to repurpose an old mouse …and I’m not sure how I’d connect a rotary encoder to my drive belt so an optical solution is attractive to me. @scoobee81 if/when you post a photo I might get some better ideas as to how.