Simulate ESPHome physical button press

I’ve looked but I can’t find this answer anywhere. It’s probably a quite niche issue but I can’t figure it out.

I’ve got an RGB light that I stuck an ESP32 in to be able to control it. Originally this light also has a button for ordinary physical control, switching effects, turning it on and off etc. This is an IKEA AFTONSPARV Planet lamp by the way.

So I removed the original MCU, connected the PWM signals and the button input, made the yaml and everything is working as it should. But now what I want, is to be able to “simulate” a button press via Home Assistant/ESPHome web interface, in the same way as it’s received by ESPHome from the real button. Meaning the same reaction to multiple presses, double clicking etc. The only things I can find online is activating a switch from home assistant but this is an RGB light with a bunch of special configuration for the particular button presses.

Any hints are appreciated.

See here my current yaml:

substitutions:
  name: saturn-lamp
  friendly_name: Saturn Lamp

esp32:
  board: esp32-c3-devkitm-1
  framework:
    type: arduino

esphome:
  name: ${name}
  friendly_name: ${friendly_name}

web_server:
  port: 80

<<: !include includes/common.yaml

globals:
   - id: active_effect
     type: int
     restore_value: yes
     initial_value: '0' 
   - id: active_color
     type: int
     restore_value: yes
     initial_value: '0'

switch:
  - platform: gpio
    id: onboard_led
    pin:
      number: GPIO8
      mode: OUTPUT
    restore_mode: ALWAYS_OFF

binary_sensor:
  - platform: gpio
    name: "Saturn Button"
    id: saturn_button
    pin:
      number: GPIO3
      mode: INPUT_PULLUP
      inverted: True
    on_multi_click:
    - timing:
          - ON for at most 1s
          - OFF for at most 0.3s
          - ON for at most 1s
          - OFF for at least 0.2s
      then:
          - lambda: !lambda |-
                    id(active_effect) += 1;
                    auto call = id(saturn_ring).turn_on();
                    switch (id(active_effect)) {
                      case 1:
                        call.set_effect("Fast Random Effect");
                        break;
                      case 2:
                        call.set_effect("Strobe");
                        break;
                      default:
                        id(active_effect) = 0;
                        call.set_effect("None");
                        break;
                    }
                    call.perform();
    - timing:
          - ON for at most 1s
          - OFF for at least 0.5s
      then:
          - lambda: !lambda |-
                    id(active_color) += 1;
                    auto call = id(saturn_ring).turn_on();
                    switch (id(active_color)) {
                      case 1: 
                        call.set_effect("None");
                        call.set_rgb(0.20,0.20,0.20);
                        break;
                      case 2: 
                        call.set_effect("None");
                        call.set_rgb(1.0,0.0,0.0);
                        break;
                      case 3: 
                        call.set_effect("None");
                        call.set_rgb(0.00,1.00,0.00);
                        break;
                      case 4: 
                        call.set_effect("None");
                        call.set_rgb(0.0,0.0,1.0);
                        break;
                      default: 
                        call.set_effect("None");
                        id(active_color) = 0;
                        call.set_rgb(0.00,0.00,0.00);
                        break;
                    }
                    call.perform();
                    
output:
  - platform: ledc
    pin: GPIO10
    id: pwm_2
    frequency: 100 Hz
    max_power: 100%
  - platform: ledc
    pin: GPIO20
    id: pwm_1
    frequency: 100 Hz
    max_power: 100%
  - platform: ledc
    pin: GPIO21
    id: pwm_3
    frequency: 100 Hz
    max_power: 100%

light:
  - platform: rgb
    red: pwm_1
    green: pwm_2
    blue: pwm_3
    name: "Saturn Ring"
    id: saturn_ring
    effects:
      - random:
          name: "Slow Random Effect"
          transition_length: 10s
          update_interval: 10s
      - random:
          name: "Fast Random Effect"
          transition_length: 4s
          update_interval: 5s
      - strobe:
          name: "Police Strobe"
          colors:
            - state: True
              brightness: 100%
              red: 100%
              green: 0%
              blue: 0%
              duration: 500ms
            - state: False
              duration: 250ms
            - state: True
              brightness: 100%
              red: 0%
              green: 0%
              blue: 100%
              duration: 500ms

If I understand correctly, you want a script, or a UI button in HA to be able to “press” the button on your ESP.

Does HA’s “button: Press” service not work for you?

Unfortunately not, since that only works on things that are configured as button: in ESPHome, while the physical button is a binary_sensor: as you can see:

But it does make me think, can I make a template button that somehow gets activated by the binary sensor? I could make all of this work if I made Home Assistant do all the work but I want it to be as self-contained as possible, so that all functionality still works even without HA, or WiFi.

Ah, yes, that might complicate it a bit, but not make it impossible.
You can configure a ‘template button’ in ESPHome, which it will expose as a Button (to HA). In its on_press… triggers, you’ll need to make the same calls to the other logic as does your binary_sensor. That would mean replicating all your event logic - not a good approach.
Alternatively, you can ‘push’ the binary sensor as though its GPIO were triggering it.
I believe here is the kind of code that will help you do this.

1 Like

Since writing the above, it occurs to me that a cleaner approach might be to:

  1. Redefine your existing binary_sensor as a button.
  2. Define a new binary_sensor that listens to the physical button’s GPIO.
  3. in the binary_sensor’s on_… automations, send the press event to the ‘button’ defined in step #1

This is predicated on the idea that the ‘press’ action might be neater to code when it targets a software ‘button’ than a binary_sensor.

1 Like

I would say move your main actions to an esphome script then you can have whatever you want calling that centralised script easily whether it be a virtual or physical button.

1 Like

I liked this idea (since it was the easiest to modify to) but unfortunately that seems to be not compatible with my other requirement, the on_multi_click: functionality:

[on_multi_click] is an invalid option for [button.template]. Please check the indentation.

Looks like the Button Component — ESPHome only supports on_press: and nothing further (since normally it would be clicked from the web interface/HA so multi-click and hold isn’t even looked for probably).

It’s looking like I’m going to have to rewrite the whole thing from scratch to be able to do this, (doubling up the actions), which will have (mostly) the same end result but it’s not very elegant. So I will make a software button for single press, and another software button for double press. (I don’t need any other multi-click possibilities). Then copy-paste the actions (or put them in a script as @Mahko_Mahko suggested). Let me try this out and I’ll post my results.

Got it working by combining both of your solutions, unfortunately I can only mark one as a solution so first come first serve :slight_smile:

My complete yaml is now as follows:

substitutions:
  name: saturn-lamp
  friendly_name: Saturn Lamp

esp32:
  board: esp32-c3-devkitm-1
  framework:
    type: arduino

esphome:
  name: ${name}
  friendly_name: ${friendly_name}

web_server:
  port: 80

<<: !include includes/common.yaml

globals:
   - id: active_effect
     type: int
     initial_value: '0' 
   - id: active_color
     type: int
     initial_value: '0'

switch:
  - platform: gpio
    id: onboard_led
    pin:
      number: GPIO8
      mode: OUTPUT
      ignore_strapping_warning: true
    restore_mode: ALWAYS_OFF

script:
  - id: single_button
    mode: restart
    then:
      - lambda: !lambda |-
                    id(active_color) += 1;
                    auto call = id(saturn_ring).turn_on();
                    switch (id(active_color)) {
                      case 1: 
                        call.set_effect("None");
                        call.set_rgb(1.0,1.0,1.0);
                        break;
                      case 2: 
                        call.set_effect("None");
                        call.set_rgb(1.0,0.0,0.0);
                        break;
                      case 3: 
                        call.set_effect("None");
                        call.set_rgb(1.0,1.0,0.0);
                        break;
                      case 4: 
                        call.set_effect("None");
                        call.set_rgb(0.0,1.0,0.0);
                        break;
                      case 5: 
                        call.set_effect("None");
                        call.set_rgb(0.0,1.0,1.0);
                        break;
                      case 6: 
                        call.set_effect("None");
                        call.set_rgb(0.0,0.0,1.0);
                        break;
                      case 7: 
                        call.set_effect("None");
                        call.set_rgb(1.0,0.0,1.0);
                        id(active_color) = 0;
                        break;
                    }
                    call.perform();
  - id: double_button
    mode: restart
    then:
      - lambda: !lambda |-
                    id(active_effect) += 1;
                    auto call = id(saturn_ring).turn_on();
                    switch (id(active_effect)) {
                      case 1:
                        call.set_effect("Slow Random Effect");
                        break;
                      case 2:
                        call.set_effect("Fast Random Effect");
                        break;
                      case 3:
                        call.set_effect("Police Strobe");
                        break;
                      case 4:
                        call.set_effect("Police Strobe Fast");
                        break;
                      default:
                        id(active_effect) = 0;
                        call.set_effect("None");
                        break;
                    }
                    call.perform();

button:
  - platform: restart
    name: "Restart ${friendly_name}"
  - platform: safe_mode
    name: "${friendly_name} Restart (Safe Mode)"
  - platform: template
    name: "Single Press"
    id: single_press
    on_press:
      then:
        - script.execute: single_button
  - platform: template
    name: "Double Press"
    id: double_press
    on_press:
      then:
        - script.execute: double_button

binary_sensor:
  - platform: gpio
    name: "Saturn Button"
    id: saturn_button
    pin:
      number: GPIO3
      mode: INPUT_PULLUP
      inverted: True
    on_multi_click:
    - timing:
          - ON for at most 1s
          - OFF for at most 0.3s
          - ON for at most 1s
          - OFF for at least 0.2s
      then:
        - script.execute: double_button
    - timing:
          - ON for at most 1s
          - OFF for at least 0.5s
      then:
         - script.execute: single_button
                    
output:
  - platform: ledc
    pin: GPIO10
    id: pwm_2
    frequency: 100 Hz
    max_power: 100%
  - platform: ledc
    pin: GPIO20
    id: pwm_1
    frequency: 100 Hz
    max_power: 100%
  - platform: ledc
    pin: GPIO21
    id: pwm_3
    frequency: 100 Hz
    max_power: 100%

light:
  - platform: rgb
    red: pwm_1
    green: pwm_2
    blue: pwm_3
    name: "Saturn Ring"
    id: saturn_ring
    effects:
      - random:
          name: "Slow Random Effect"
          transition_length: 30s
          update_interval: 30s
      - random:
          name: "Fast Random Effect"
          transition_length: 5s
          update_interval: 5s
      - strobe:
          name: "Police Strobe"
          colors:
            - state: True
              brightness: 100%
              red: 100%
              green: 0%
              blue: 0%
              duration: 500ms
            - state: True
              brightness: 100%
              red: 0%
              green: 0%
              blue: 100%
              duration: 500ms
      - strobe:
          name: "Police Strobe Fast"
          colors:
            - state: True
              brightness: 100%
              red: 100%
              green: 0%
              blue: 0%
              duration: 250ms
            - state: True
              brightness: 100%
              red: 0%
              green: 0%
              blue: 100%
              duration: 250ms

So all the actions are now in a script, and I’ve just got a physical button and 2 separate software buttons that both call on to these scripts. This way if I change something I only have to change it once and everything I needed is in there.
And I can change the button functions and test them without physically being there.
(I have a camera in the room where the light is)

Thanks for the assist everyone!

1 Like

If you are really keen to mimic the button presses on the virtual button, you can have just the one virtual button which momentarily turns on and off a template binary sensor.

Then on that binary sensor you have the same press pattern logic as your physical button.

You could even move your button pattern press logic to this master binary sensor and just have both buttons pass on thier presses to this main one.

1 Like

That would make the code more elegant, I like it. I hadn’t seen the template binary sensor before, I think that should do exactly what I want. I’ll try it out tomorrow and post the result once more. This whole solution will also help me with another ESP’d device I’ve got (a white noise speaker) that became unable to be controlled locally due to my previous solution. Thank you both for the help!

2 Likes

Appreciate that you’ve probably got your solution already, but wanted to share a similar problem I had (though it isn’t exactly the same I think).

My problem was that I wanted to define an action that could be fired by a button press (templated), but also in other actions. I don’t see a button.press action in esphome, so I ended up firing the button in a lambda…

button:
  - platform: template
    id: "spinning_lights"
    name: "Spinning lights"
    on_press:
      - repeat:
          count: 8
          then:
            - output.turn_on: pwm_output_d1
            - delay: "200ms"
            - output.turn_off: pwm_output_d1
            - output.turn_on: pwm_output_tx
            - delay: "200ms"
            - output.turn_off: pwm_output_tx
            - output.turn_on: pwm_output_rx
            - delay: "200ms"
            - output.turn_off: pwm_output_rx

Then I can fire that button press from another action (in my case, when an rfid tag is read):

  on_tag:
    then:
      - lambda: |-
          id(spinning_lights).press();

It is simple, but it took me a good while to figure it out!