GPIO high for 1500ms before turning relay on, but GPIO status immediate in UX - best way?

I installed 2 infrared barriers (1m long) on my overhead garage door opening that will detect cars, people, or an open SUV’s liftgate that would get damaged if the garage door were to close. A typical overhead garage door has a sensor low to the ground that would not detect a person standing in the opening if the legs were not in the way of the sensor.

For the project I used this Lilygo T-Relay board I just discovered. Both the barrier contacts (ir barriers have relays) and the magnetic floor mounted contact provide 12V to an optocoupler (based on PC817) board that is connected to the GPIO on the header.

The overhead garage door opener is disabled from closing the overhead garage door simply by opening the circuit to the included sensors. Relay 1 turns on breaking the circuit (this way if this system is powered down, the overhead garage door opener will keep working) between the sensor and the garage door opener.

Question: Everything works beautifully… BUT, I’d like to add a 1 or 2 second delay between the barrier being interrupted and Relay 1 being activated to break the sensor circuit. I want to see the barrier being blocked immediately, but if it doesn’t stay blocked for the threshold I pick, I do not want the relay to get triggered. I did see the delayed_on option but that would also delay me seeing the barrier being triggered… I’ve used on_click that allows timing of a click but it forces you to select a max too so it won’t work.

Suggestions?

The part I likely need to edit to add this is:

binary_sensor:
  - platform: gpio
    pin:
      number: 23
      inverted: true
      mode:
        input: true
        pullup: true
    name: "${friendly_devicename}: High Barrier"
    id: high_barrier
    on_press:
      then:
        - lambda: |-
            if (!id(disable_barrier)) {
              id(on_barrier_on).execute();
            }
    on_release:
      then:
        - lambda: |-
            if (!id(disable_barrier)) {
              id(on_barrier_off).execute();
            }

  - platform: gpio
    pin:
      number: 27
      inverted: true
      mode:
        input: true
        pullup: true
    name: "${friendly_devicename}: Low Barrier"
    id: low_barrier
    on_press:
      then:
        - lambda: |-
            if (!id(disable_barrier)) {
              id(on_barrier_on).execute();
            }
    on_release:
      then:
        - lambda: |-
            if (!id(disable_barrier)) {
              id(on_barrier_off).execute();
            }

All the code:

substitutions:
  devicename: overhead-garage-door-barrier
  devicename_no_dashes: overhead_garage_door_barrier
  friendly_devicename: "Overhead Garage Door Barrier"
  device_description: "Overhead Garage Door Barrier"
  update_interval_s: "60s"
  #Only reason not to set it much longer is for wifi troubleshooting ease
  update_interval_wifi: "120s"

esphome:
  name: ${devicename}
  comment: ${device_description}

esp32:
  board: esp32dev
  framework:
    type: arduino

wifi:
  ssid: !secret iot_wifi_ssid
  password: !secret iot_wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "${devicename}"
    password: !secret iot_wifi_password

#Faster than DHCP. Also use if can't reach because of name change
  manual_ip:
    static_ip: 192.168.3.207
    gateway: 192.168.3.1
    subnet: 255.255.255.0
    dns1: 192.168.1.25
    dns2: 192.168.1.26

#Manually override what address to use to connect to the ESP.
#Defaults to auto-generated value. Example, if you have changed your
#static IP and want to flash OTA to the previously configured IP address.
  use_address: 192.168.3.207
  
logger:
    baud_rate: 0 #disabled

api:
  
ota:
  
web_server:
  port: 80
  include_internal: true

# Sync time with Home Assistant
time:
  - platform: homeassistant
    id: ha_time

globals:
  - id: active_barrier
    type: int
    restore_value: no
    initial_value: '3' # 0 Disabled; 1 Low Barrier; 2 High Barrier; 3 Both Barriers
  - id: disable_barrier
    type: bool
    restore_value: yes
    initial_value: 'true' # When true, the system has no effect on the door operation

text_sensor:
  - platform: wifi_info
    ip_address:
      name: "${friendly_devicename}: IP"
      icon: "mdi:ip-outline"
      update_interval: ${update_interval_wifi}
    ssid:
      name: "${friendly_devicename}: SSID"
      icon: "mdi:wifi-settings"
      update_interval: ${update_interval_wifi}
    bssid:
      name: "${friendly_devicename}: BSSID"
      icon: "mdi:wifi-settings"
      update_interval: ${update_interval_wifi}
    mac_address:
      name: "${friendly_devicename}: MAC"
      icon: "mdi:network-outline"
    scan_results:
      name: "${friendly_devicename}: Wifi Scan"
      icon: "mdi:wifi-refresh"
      disabled_by_default: true
  
sensor:
  - platform: wifi_signal
    name: "${friendly_devicename}: WiFi Signal"
    update_interval: ${update_interval_wifi}
    device_class: signal_strength


binary_sensor:
  - platform: gpio
    pin:
      number: 23
      inverted: true
      mode:
        input: true
        pullup: true
    name: "${friendly_devicename}: High Barrier"
    id: high_barrier
    on_press:
      then:
        - lambda: |-
            if (!id(disable_barrier)) {
              id(on_barrier_on).execute();
            }
    on_release:
      then:
        - lambda: |-
            if (!id(disable_barrier)) {
              id(on_barrier_off).execute();
            }

  - platform: gpio
    pin:
      number: 27
      inverted: true
      mode:
        input: true
        pullup: true
    name: "${friendly_devicename}: Low Barrier"
    id: low_barrier
    on_press:
      then:
        - lambda: |-
            if (!id(disable_barrier)) {
              id(on_barrier_on).execute();
            }
    on_release:
      then:
        - lambda: |-
            if (!id(disable_barrier)) {
              id(on_barrier_off).execute();
            }

  - platform: gpio
    pin:
      number: 22
      mode:
        input: true
        pullup: true
    name: "${friendly_devicename}: Door Contact"
    id: door_contact
    device_class: garage_door
    on_press:
      then:
        - output.turn_on: onboard_led
    on_release:
      then:
        - output.turn_off: onboard_led 

  - platform: gpio
    pin:
      number: 26
      inverted: true
      mode:
        input: true
        pullup: true
    name: "${friendly_devicename}: Spare Contact"
    id: spare_contact
    on_press:
      then:
        - output.turn_on: onboard_led
    on_release:
      then:
        - output.turn_off: onboard_led 
switch:
  - platform: restart
    name: "${friendly_devicename}: Restart"

  # COM-NC closes safety sensor circuit so GD Opener works normally. To disable door closing, relay is turned on thus breaking circuit.
  # GD Opener will just see it as a bad sensor.
  - platform: gpio
    pin:
      number: 21
    name: "${friendly_devicename}: Disable Door"
    id: relay1

  # Sensors are wired through COM-NC of relay. To disable door, turn relay on so door sensors are disconnected disabling the door.
  - platform: gpio
    pin:
      number: 19
      inverted: true # To save relay and energy, turning off barrier will turn on relay. Inverting to make it more intuitive.
    name: "${friendly_devicename}: High Barrier Power"
    id: relay2
    restore_mode: RESTORE_DEFAULT_ON
    on_turn_on:
      then:
        - script.execute: set_active_barrier
    on_turn_off:
      then:
        - script.execute: set_active_barrier
 
  # Sensors are wired through COM-NC of relay. To disable door, turn relay on so door sensors are disconnected disabling the door.
  - platform: gpio
    pin:
      number: 18
      inverted: true # To save relay and energy, turning off barrier will turn on relay. Inverting to make it more intuitive.
    name: "${friendly_devicename}: Low Barrier Power"
    id: relay3
    restore_mode: RESTORE_DEFAULT_ON
    on_turn_on:
      then:
        - script.execute: set_active_barrier
    on_turn_off:
      then:
        - script.execute: set_active_barrier

  - platform: gpio
    pin:
      number: 5
    name: "${friendly_devicename}: Spare Relay"
    id: relay4

  - platform: template
    id: disable_barrier_ha
    name: "${friendly_devicename}: Disable Barrier"
    optimistic: true
    turn_on_action:
      - globals.set:
          id: disable_barrier
          value: 'true'
    turn_off_action:
      - globals.set:
          id: disable_barrier
          value: 'false'

button:
  - platform: safe_mode
    name: "${friendly_devicename}: Restart (Safe Mode)"

#deep_sleep:
#  id: ${devicename_no_dashes}_deep_sleep
#  sleep_duration: 30min
  
#status_led:
#  pin:
#    number: 2
#    inverted: false

output:
  - id: onboard_led
    platform: gpio
    pin: 25
    inverted: true

script:
  # 0 Disabled; 1 Low Barrier (relay3); 2 High Barrier (relay2); 3 Both Barriers
  - id: set_active_barrier
    then:
      - lambda: |-
          if(id(relay2).state && id(relay3).state) {
            id(active_barrier) = 3;
          }
          if(id(relay2).state && !id(relay3).state) {
            id(active_barrier) = 2;
          }
          if(!id(relay2).state && id(relay3).state) {
            id(active_barrier) = 1;
          }
          if(!id(relay2).state && !id(relay3).state) {
            id(active_barrier) = 0;
          }
          ESP_LOGD("DEBUG", "Active Barrier: %d", int(id(active_barrier)));
  
  # Disabled barriers can't be triggered as they have no power so state can be ignored
  - id: on_barrier_on
    then:
      # 0 Disabled; 1 Low Barrier (relay3); 2 High Barrier (relay2); 3 Both Barriers
      - lambda: |-
          if(id(active_barrier) == 3) {
            if(id(low_barrier).state || id(high_barrier).state) {
              id(relay1).turn_on();
              id(onboard_led).turn_on();
            }
          };
          if(id(active_barrier) == 2) {
            if(id(high_barrier).state) {
              id(relay1).turn_on();
              id(onboard_led).turn_on();
            }
          };
          if(id(active_barrier) == 1) {
            if(id(low_barrier).state) {
              id(relay1).turn_on();
              id(onboard_led).turn_on();
            }
          };
          if(id(active_barrier) == 0) {
            ESP_LOGD("DEBUG", "Impossible condition as both barriers are disabled.");
          };

  - id: on_barrier_off
    then:
      # 0 Disabled; 1 Low Barrier (relay3); 2 High Barrier (relay2); 3 Both Barriers
      - lambda: |-
          if(id(active_barrier) == 3) {
            if(!id(low_barrier).state && !id(high_barrier).state) {
              id(relay1).turn_off();
              id(onboard_led).turn_off();
            }
          };
          if(id(active_barrier) == 2) {
            if(!id(high_barrier).state) {
              id(relay1).turn_off();
              id(onboard_led).turn_off();
            }
          };
          if(id(active_barrier) == 1) {
            if(!id(low_barrier).state) {
              id(relay1).turn_off();
              id(onboard_led).turn_off();
            }
          };
          if(id(active_barrier) == 0) {
            ESP_LOGD("DEBUG", "Impossible condition as both barriers are disabled.");
          };
          

How about a copy binary sensor?

The original is used for “instant notification”.

You add a delay_on to the second copy sensor and trigger actions from that one?

2 Likes

Before venturing down the Copy Integration path I had tried an alternative which worked. I needed to be able to cancel the delayed relay trigger in the even the barrier was no longer blocked before the relay got triggered. Was that possible with the Copy Integration?

I used a script that starts with a delay, and I cancel it if the barrier is restored. I had to lengthen the delay from 1.5s to 2.0s as the barrier itself also has a delay and simply walking between the barriers often triggered the relay on and then immediately off. With 2s I am avoiding a lot of pointless disconnections of the overhead garage door sensor which is likely not a scenario they tested for and might cause issues in the long run…

Updated code below:

The script:

  - id: delayed_barrier_blocked
    then:
      - delay: 2000ms
      - script.execute: barrier_blocked

Where I call and cancel the script:

  - platform: gpio
    pin:
      number: 27
      inverted: true
      mode:
        input: true
        pullup: true
    name: "${friendly_devicename}: Low Barrier"
    id: low_barrier
    device_class: motion
    on_press:
      then:
        - lambda: |-
            if (id(infrared_barrier)) {
              id(delayed_barrier_blocked).execute();
            }
    on_release:
      then:
        - lambda: |-
            if (id(infrared_barrier)) {
                if (id(delayed_barrier_blocked).is_running()) {
                    ESP_LOGI("main", "Stopping disable door relay trigger timer!");
                    id(delayed_barrier_blocked).stop();
                }
                id(barrier_clear).execute();
            }

I know I could do without using the lambda but I find it a cleaner (compact) solution in this case.

All the code:

substitutions:
  devicename: overhead-garage-door-barrier
  devicename_no_dashes: overhead_garage_door_barrier
  friendly_devicename: "Overhead Garage Door Barrier"
  device_description: "Overhead Garage Door Barrier"
  update_interval_s: "60s"
  #Only reason not to set it much longer is for wifi troubleshooting ease
  update_interval_wifi: "120s"

esphome:
  name: ${devicename}
  comment: ${device_description}

esp32:
  board: esp32dev
  framework:
    type: arduino

wifi:
  ssid: !secret iot_wifi_ssid
  password: !secret iot_wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "${devicename}"
    password: !secret iot_wifi_password

#Faster than DHCP. Also use if can't reach because of name change
  manual_ip:
    static_ip: 192.168.3.207
    gateway: 192.168.3.1
    subnet: 255.255.255.0
    dns1: 192.168.1.25
    dns2: 192.168.1.26

#Manually override what address to use to connect to the ESP.
#Defaults to auto-generated value. Example, if you have changed your
#static IP and want to flash OTA to the previously configured IP address.
  use_address: 192.168.3.207
  
logger:
    baud_rate: 0 #disabled

api:
  
ota:
  
web_server:
  port: 80
  include_internal: true

# Sync time with Home Assistant
time:
  - platform: homeassistant
    id: ha_time

globals:
  - id: active_barrier
    type: int
    restore_value: no
    initial_value: '3' # 0 Disabled; 1 Low Barrier; 2 High Barrier; 3 Both Barriers
  - id: infrared_barrier
    type: bool
    restore_value: yes
    initial_value: 'false' # When false, the system has no effect on the door operation

text_sensor:
  - platform: wifi_info
    ip_address:
      name: "${friendly_devicename}: IP"
      icon: "mdi:ip-outline"
      update_interval: ${update_interval_wifi}
    ssid:
      name: "${friendly_devicename}: SSID"
      icon: "mdi:wifi-settings"
      update_interval: ${update_interval_wifi}
    bssid:
      name: "${friendly_devicename}: BSSID"
      icon: "mdi:wifi-settings"
      update_interval: ${update_interval_wifi}
    mac_address:
      name: "${friendly_devicename}: MAC"
      icon: "mdi:network-outline"
    scan_results:
      name: "${friendly_devicename}: Wifi Scan"
      icon: "mdi:wifi-refresh"
      disabled_by_default: true
  
sensor:
  - platform: wifi_signal
    name: "${friendly_devicename}: WiFi Signal"
    update_interval: ${update_interval_wifi}
    device_class: signal_strength


binary_sensor:
  - platform: gpio
    pin:
      number: 23
      inverted: true
      mode:
        input: true
        pullup: true
    name: "${friendly_devicename}: High Barrier"
    id: high_barrier
    device_class: motion
    on_press:
      then:
        - lambda: |-
            if (id(infrared_barrier)) {
              id(delayed_barrier_blocked).execute();
            }
    on_release:
      then:
        - lambda: |-
            if (id(infrared_barrier)) {
                if (id(delayed_barrier_blocked).is_running()) {
                    ESP_LOGI("main", "Stopping disable door relay trigger timer!");
                    id(delayed_barrier_blocked).stop();
                }
                id(barrier_clear).execute();
            }

  - platform: gpio
    pin:
      number: 27
      inverted: true
      mode:
        input: true
        pullup: true
    name: "${friendly_devicename}: Low Barrier"
    id: low_barrier
    device_class: motion
    on_press:
      then:
        - lambda: |-
            if (id(infrared_barrier)) {
              id(delayed_barrier_blocked).execute();
            }
    on_release:
      then:
        - lambda: |-
            if (id(infrared_barrier)) {
                if (id(delayed_barrier_blocked).is_running()) {
                    ESP_LOGI("main", "Stopping disable door relay trigger timer!");
                    id(delayed_barrier_blocked).stop();
                }
                id(barrier_clear).execute();
            }

  - platform: gpio
    pin:
      number: 22
      mode:
        input: true
        pullup: true
    name: "${friendly_devicename}: Door Contact"
    id: door_contact
    device_class: garage_door
    on_press:
      then:
        - output.turn_on: onboard_led
    on_release:
      then:
        - output.turn_off: onboard_led 

  - platform: gpio
    pin:
      number: 26
      inverted: true
      mode:
        input: true
        pullup: true
    name: "${friendly_devicename}: Spare Contact"
    id: spare_contact
    on_press:
      then:
        - output.turn_on: onboard_led
    on_release:
      then:
        - output.turn_off: onboard_led 
switch:
  - platform: restart
    name: "${friendly_devicename}: Restart"

  # COM-NC closes safety sensor circuit so GD Opener works normally. To disable door closing, relay is turned on thus breaking circuit.
  # GD Opener will just see it as a bad sensor.
  - platform: gpio
    pin:
      number: 21
    name: "${friendly_devicename}: Disable Door Closure"
    id: relay1

  # Sensors are wired through COM-NC of relay. To disable door, turn relay on so door sensors are disconnected disabling the door.
  - platform: gpio
    pin:
      number: 19
      inverted: true # To save relay and energy, turning off barrier will turn on relay. Inverting to make it more intuitive.
    name: "${friendly_devicename}: High Barrier Power"
    id: relay2
    restore_mode: RESTORE_DEFAULT_ON
    on_turn_on:
      then:
        - script.execute: set_active_barrier
    on_turn_off:
      then:
        - script.execute: set_active_barrier
 
  # Sensors are wired through COM-NC of relay. To disable door, turn relay on so door sensors are disconnected disabling the door.
  - platform: gpio
    pin:
      number: 18
      inverted: true # To save relay and energy, turning off barrier will turn on relay. Inverting to make it more intuitive.
    name: "${friendly_devicename}: Low Barrier Power"
    id: relay3
    restore_mode: RESTORE_DEFAULT_ON
    on_turn_on:
      then:
        - script.execute: set_active_barrier
    on_turn_off:
      then:
        - script.execute: set_active_barrier

  - platform: gpio
    pin:
      number: 5
    name: "${friendly_devicename}: Spare Relay"
    id: relay4

  - platform: template
    id: infrared_barrier_ha
    name: "${friendly_devicename}"
    optimistic: true
    turn_on_action:
      - globals.set:
          id: infrared_barrier
          value: 'true'
    turn_off_action:
      - globals.set:
          id: infrared_barrier
          value: 'false'

button:
  - platform: safe_mode
    name: "${friendly_devicename}: Restart (Safe Mode)"

#deep_sleep:
#  id: ${devicename_no_dashes}_deep_sleep
#  sleep_duration: 30min
  
#status_led:
#  pin:
#    number: 25
#    inverted: false

output:
  - id: onboard_led
    platform: gpio
    pin: 25
    inverted: true

script:
  # 0 Disabled; 1 Low Barrier (relay3); 2 High Barrier (relay2); 3 Both Barriers
  - id: set_active_barrier
    then:
      - lambda: |-
          if(id(relay2).state && id(relay3).state) {
            id(active_barrier) = 3;
          }
          if(id(relay2).state && !id(relay3).state) {
            id(active_barrier) = 2;
          }
          if(!id(relay2).state && id(relay3).state) {
            id(active_barrier) = 1;
          }
          if(!id(relay2).state && !id(relay3).state) {
            id(active_barrier) = 0;
          }
          ESP_LOGD("DEBUG", "Active Barrier: %d", int(id(active_barrier)));
  
  # Disabled barriers can't be triggered as they have no power so state can be ignored
  - id: barrier_blocked
    then:
      # 0 Disabled; 1 Low Barrier (relay3); 2 High Barrier (relay2); 3 Both Barriers
      - lambda: |-
          if(id(active_barrier) == 3) {
            if(id(low_barrier).state || id(high_barrier).state) {
              id(relay1).turn_on();
              id(onboard_led).turn_on();
            }
          };
          if(id(active_barrier) == 2) {
            if(id(high_barrier).state) {
              id(relay1).turn_on();
              id(onboard_led).turn_on();
            }
          };
          if(id(active_barrier) == 1) {
            if(id(low_barrier).state) {
              id(relay1).turn_on();
              id(onboard_led).turn_on();
            }
          };
          if(id(active_barrier) == 0) {
            ESP_LOGD("DEBUG", "Impossible condition as both barriers are disabled.");
          };

  - id: barrier_clear
    then:
      # 0 Disabled; 1 Low Barrier (relay3); 2 High Barrier (relay2); 3 Both Barriers
      - lambda: |-
          if(id(active_barrier) == 3) {
            if(!id(low_barrier).state && !id(high_barrier).state) {
              id(relay1).turn_off();
              id(onboard_led).turn_off();
            }
          };
          if(id(active_barrier) == 2) {
            if(!id(high_barrier).state) {
              id(relay1).turn_off();
              id(onboard_led).turn_off();
            }
          };
          if(id(active_barrier) == 1) {
            if(!id(low_barrier).state) {
              id(relay1).turn_off();
              id(onboard_led).turn_off();
            }
          };
          if(id(active_barrier) == 0) {
            ESP_LOGD("DEBUG", "Impossible condition as both barriers are disabled.");
          };

  - id: delayed_barrier_blocked
    then:
      - delay: 2000ms
      - script.execute: barrier_blocked
1 Like