ESPHome Physical Button - Long Press action without release

Hi folks,

I’m building a simple device with an ESPHome ESP32 as the core. It has a single button and a single status LED phsyically wired to GPIOs - both are working.

Where I’m struggling is with managing the swtich press/clicks. What I want to acheive is a simple latch where the LED turns on when the button is short-pressed, and the LED turns off when the button is long-pressed. Sounds simple, and to some extent it is, I’ve acheived this very easily with:

    on_click:
    - min_length: 50ms
      max_length: 350ms
      then:
        - output.turn_on: led
    - min_length: 2000ms
      max_length: 5000ms
      then:
        - output.turn_off: led

However this isn’t quite what I want, as these on_click actions trigger on the trailing edge it requires the user to know how long to press the buttons for. What I would like is to make it more intuitve, so that as soon as the button is depressed for 50ms the LED lights up, and as soon as it depressed for more than 2000ms it turns off - i.e. the user gets instant feedback that the action has been completed without taking their finger off the button. I appreacite this comes with a complication, because technically depressing the button for 2000+ms is ALSO depressing it for more than 50ms but that is fine, I’m ok for both actions to be triggered by the long press.

Is there a way to do this using on_press? Or will it require a custom lambda? If so… any hints?

Thanks!

Maybe that helps?

I didn’t test it myself, but (assuming that you are using a Binary Sensor component) does this work for you:

    on_press:
      then:
        - output.turn_on: led
    on_click:
      min_length: 2000ms
      then:
        - output.turn_off: led

Like this you don’t have the 50 ms minimum, but is this really a problem?

Perhaps, but this turned out to be very easy to do with on_press I trigger the first action immediately then wait for the 2s long press timeout and test whether the button is still pressed, and reset conditionally on that. Possibly some race conditions might occur here because if you did mulitple short presses then you might have the switch ON during the test despite it not being a long press, but I don’t think that will be an issue with this use-case.

    on_press:
      then: 
        - output.turn_on: led
        - delay: 2s
        - if:
            condition:
              binary_sensor.is_on: button
            then: 
              - output.turn_off: led
              - logger.log: "LED is Reset"
            else:
              - logger.log: "LED is Set"

This doesn’t work because on_click requires a max_length and only operates on the trailing edge.

Really?
Like I wrote, I didn’t test it, but the max_length parameter is shown as Optional:

max_length

Edit:
Ah! I see what you mean:
afbeelding

Yep you got it, max_length is optional only because it defaults to 350ms. If your min_length is greater than 350ms then not providing max_length will fail to compile because min_length must be less than max_length.

Hmm - I have been thinking about something like this for a while. Can you combine on_press: with on_click:, so I can have some visible indications PLUS act on different length clicks?

You can, but you have to be aware the on_press action will trigger immediately and the full automation block for the on_press action will run regardless of whether the button is released before it completes.

Hmm ok - wil have a fiddle on the weekend. Thanks.

EDIT: As long as it is non-blocking then should be fine.

I realised you can avoid this race condition by just putting a ‘cooldown’ timer on the on_press automation that is longer than the long_press time. This only works if your real-world usecase can tolerate this and isn’t expecting multiple short presses in short space of time. My use case is indicating that someone has deposited a package in a location and I don’t need to know how many packages, and the chances of two packages being dropped by different people in the space of 2 seconds is virtually impossible.

One more (although wild and again not tested) idea:

Would this work:

    on_press:
      then:
        - output.turn_on: led
    filters:
      - invert:
      - delayed_on: 2000ms

Like this the led goes on immediately on pressing the button, and after the delay it is giving an OFF signal (inverted ON due to the invert filter)?
afbeelding

This is the full working code with the cool-down timer (set at 5 seconds):

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

binary_sensor:
  - platform: gpio
    pin:
      number: GPIO33
      mode:
        input: true
        pullup: true
      inverted: true
    id: button
    name: "Button"
    filters:
      - delayed_on: 50ms
    on_press:
      then: 
        - if:
            condition: 
              lambda: |-
                return (id(cool_down_timestamp) < (millis() - 5000));
            then:
              - lambda: |-
                  id(cool_down_timestamp) = millis();
              - output.turn_on: led
              - delay: 2s
              - if:
                  condition:
                    binary_sensor.is_on: button
                  then: 
                    - output.turn_off: led
                    - logger.log: "LED Reset"
                  else:
                    - logger.log: "LED Set"

I still didn’t test it, but this probably does not work, because the filter probably already applies to the initial on_press action so the led will not go on at all?

Just for fun one more idea: would it work to create two binary sensors listening to the same input button. The first sensor immediately switches the led on when the button is pressed, and the second sensor switches the led off again when the button is pressed for more than 2 seconds. Something like this:

binary_sensor:
  - platform: gpio
    pin: GPIO33
    name: led_switched_on
    on_press:
          then:
            - output.turn_on: led
    
binary_sensor:
  - platform: gpio
    pin: GPIO33
    name: led_switched_off
    filters:
      - delayed_on: 2000ms
    on_press:
          then:
            - output.turn_off: led

Or is this prone to that race condition effect?

Just for completeness: in the mean time I did a simple test, and indeed it does work like this.
This was my test set-up, with one momentary push button and one led connected to an ESP8266:

binary_sensor:
  - platform: gpio
    pin:
      number: GPIO4
      inverted: true
      mode: INPUT_PULLUP
    name: led_switched_on
    on_press:
      then:
        - light.turn_on: led
    
  - platform: gpio
    pin:
      number: GPIO4
      inverted: true
      mode: INPUT_PULLUP
    name: led_switched_off
    filters:
      - delayed_on: 2000ms
    on_press:
      then:
        - light.turn_off: led

output:
  - platform: gpio
    id: green_led
    pin:
      number: GPIO5

light:
 - platform: binary
   name: “Green LED”
   output: green_led
   id: led
2 Likes

how to convert this code to a toggle switch? it should just toggle the state on button press?

This is what on_multi_click is for:

binary_sensor:
  - platform: gpio
    id: g0
    pin:
      number: 0
      mode: input_pullup
      inverted: true
    on_press:
      - switch.turn_on: led
    on_multi_click:
      - timing:
          - on for at least 1s
        then:
          - switch.turn_off: led
3 Likes