Template switch component and optimitic mode doesn't update state until after actions

I have a use case where I am using a template switch component essentially as a virtual switch so I can turn things on/off from the HA UI, but also track that state inside the ESPHome firmware using the state of the template swtich. Since there is no hardware backing the switch, I used the optimistic setting. All seems well, except when I try to use the state of the switch in the on_turn_on and on_turn_off actions, the state of the template switch isn’t the new value, it’s the old value. Only after the actions have run is the state of the switch updated.

My expected behavior would be that the state of the optimistic switch immediately reflects the new state before the actions are run. Maybe I’m misusing the template switch and this is expected behavior. Let me know. I know I could just assume the state by putting my automation directly in the action, but I’m attempting to reduce duplicated code by running a script that checks the state of the switch to decide what to do (and that script is used elsewhere besides the switch actions).

minimal example:

esphome:
  name: test-device
  platform: ESP8266
  board: nodemcuv2

logger:

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

api:

ota:

time:
  platform: homeassistant
  on_time:
    - seconds: /10
      then:
        - lambda: ESP_LOGD("timer", "State of test_switch is %d", id(test_switch).state);

switch:
  - platform: template
    id: test_switch
    name: test_switch
    optimistic: true
    turn_on_action:
      then:
        - script.execute: test_script

    turn_off_action:
      then:
        - script.execute: test_script

script:
  - id: test_script
    then:
      - lambda: ESP_LOGD("script", "State of test_switch is %d", id(test_switch).state);

Log output:

[11:38:50][D][timer:022]: State of test_switch is 0
[11:39:00][D][timer:022]: State of test_switch is 0
[11:39:06][D][switch:013]: 'test_switch' Turning ON. # turn on switch with HA UI
[11:39:06][D][script:044]: State of test_switch is 0 # Expected 1 here
[11:39:06][D][switch:037]: 'test_switch': Sending state ON
[11:39:10][D][timer:022]: State of test_switch is 1 # switch is now on after automation action ran
[11:39:20][D][timer:022]: State of test_switch is 1
[11:39:28][D][switch:017]: 'test_switch' Turning OFF. # turn off switch with HA UI
[11:39:28][D][script:044]: State of test_switch is 1 # Expected 0 here
[11:39:28][D][switch:037]: 'test_switch': Sending state OFF
[11:39:30][D][timer:022]: State of test_switch is 0 # switch is now off after automation action ran
[11:39:40][D][timer:022]: State of test_switch is 0
[11:39:50][D][timer:022]: State of test_switch is 0

A work around I found is to ditch optimistic all together (although it would still work with this), and just publish the state of the switch at the start of the action. This works, but feels a bit cumbersome to remember to do. I would possibly expect a template switch to automatically do this is neither a lambda nor optimistic are set, but that might have other side effects that break other use cases.

I guess I have a few questions:

  • Should template switches with optimistic set update their internal state before running actions?
  • Should template switches with neither a lambda nor optimistic set implicitly update their internal state?
  • Is there a less verbose way of updating a template switch’s internal state without explicitly publishing updates on actions?

Modified Yaml:

esphome:
  name: test-device
  platform: ESP8266
  board: nodemcuv2

logger:

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

api:

ota:

time:
  platform: homeassistant
  on_time:
    - seconds: /10
      then:
        - lambda: ESP_LOGD("timer", "State of test_switch is %d", id(test_switch).state);

switch:
  - platform: template
    id: test_switch
    name: test_switch
    optimistic: true
    turn_on_action:
      then:
        - lambda: id(test_switch).publish_state(true);
        - script.execute: test_script

    turn_off_action:
      then:
        - lambda: id(test_switch).publish_state(false);
        - script.execute: test_script

script:
  - id: test_script
    then:
      - lambda: ESP_LOGD("script", "State of test_switch is %d", id(test_switch).state);

Modified output:

[12:34:30][D][timer:022]: State of test_switch is 0
[12:34:40][D][timer:022]: State of test_switch is 0
[12:34:43][D][switch:013]: 'test_switch' Turning ON.
[12:34:43][D][switch:037]: 'test_switch': Sending state ON
[12:34:43][D][script:042]: State of test_switch is 1
[12:34:50][D][timer:022]: State of test_switch is 1
[12:35:00][D][timer:022]: State of test_switch is 1
[12:35:01][D][switch:017]: 'test_switch' Turning OFF.
[12:35:01][D][switch:037]: 'test_switch': Sending state OFF
[12:35:01][D][script:042]: State of test_switch is 0
[12:35:10][D][timer:022]: State of test_switch is 0

Actually looking again I don’t see that you need then after turn_on_action

    turn_on_action:
      - lambda: id(test_switch).publish_state(true);
      - script.execute: test_script

Thanks, you nailed it. turn_on_action and turn_off_action are specific to template switches, where as on_turn_on and friends are for the generic switch class. The turn_on_action runs before the state has chanced which makes some kind of sense. When playing around I realized it’s an error if you don’t have at least one of turn_[on|off]_action, optimistic, or lambda. It also seems to me that you would really only ever want just one of those, which is what I was doing with optimistic and turn_[on|off]_action.

Looks like the options are:

  • Use turn_[on|off]_action and publish_state in a lambda. Then I can either run the script like you pointed out there, or I can use on_turn_[on|off]
  • Use optimistic and use on_turn_[on|off]. This is less verbose, but I’m not a fan of having the UI show changes like that optimistically, so I might use option on.

I wonder if enough people have the use case of setting turn_[on|off]_action to just publish the state if it’s worth adding an option in YAML as short hand for this. Likely not, but just throwing it out there.

Thanks @Mikefila !