Asynchronous scripting

After reading the documentation, I wasn’t completely sure about the asynchronous nature of scripts in ESPhome. And, I was too busy/lazy/stupid to track it down through the native code. Instead, I did a little experiment. Here it is. It’s nothing earth-shaking, but maybe it will save someone else a few minutes of pondering or experimenting.

The bottom line is that the scripts do run independently (or at least appear to do so to the extent that it probably matters for whatever you need to do).

At boot time, I launch 3 scripts. Each script is an infinite loop with a delay in the body of the loop. Each script uses a different delay value to make it clearer that they are not in lock-step with each other. (In actual usage, delay values are templatable, so they needn’t be constants.)

Here’s the configuration:

wifi:
  ssid:      !secret wifi_ssid
  id:        !secret wifi_ssid
  password:  !secret wifi_password
ota:
  password: !secret ota_password
api:
  password: !secret api_password
logger:
#  level: 'DEBUG'

esphome:
  name: async
  platform: ESP32
  board: esp32thing
  on_boot:
    then:
    - delay: 15s
    - script.execute: every_13
    - script.execute: every_23
    - script.execute: every_51
    - logger.log: "scripts have been launched ------------------------------"

script:
  - id: every_13
    then:
    - while:
        condition:
        - lambda: "return true;"
        then:
        - delay: 13s
        - logger.log: "EVERY ---------------- 13"
    - logger.log: "13 loop dropped"

  - id: every_23
    then:
    - while:
        condition:
        - lambda: "return true;"
        then:
        - delay: 23s
        - logger.log: "EVERY ----------------------- 23"
    - logger.log: "23 loop dropped"

  - id: every_51
    then:
    - while:
        condition:
        - lambda: "return true;"
        then:
        - delay: 51s
        - logger.log: "EVERY ------------------------------------- 51"
    - logger.log: "51 loop dropped"

and here is some sample logging output:

[11:51:33][D][main:236]: EVERY ---------------- 13
[11:51:46][D][main:236]: EVERY ---------------- 13
[11:51:53][D][main:257]: EVERY ----------------------- 23
[11:51:59][D][main:236]: EVERY ---------------- 13
[11:51:59][D][main:274]: EVERY ------------------------------------- 51
[11:52:12][D][main:236]: EVERY ---------------- 13
[11:52:16][D][main:257]: EVERY ----------------------- 23
[11:52:25][D][main:236]: EVERY ---------------- 13
[11:52:38][D][main:236]: EVERY ---------------- 13
[11:52:39][D][main:257]: EVERY ----------------------- 23
[11:52:50][D][main:274]: EVERY ------------------------------------- 51
[11:52:51][D][main:236]: EVERY ---------------- 13
[11:53:03][D][main:257]: EVERY ----------------------- 23
[11:53:04][D][main:236]: EVERY ---------------- 13
[11:53:17][D][main:236]: EVERY ---------------- 13
[11:53:26][D][main:257]: EVERY ----------------------- 23
[11:53:30][D][main:236]: EVERY ---------------- 13
[11:53:41][D][main:274]: EVERY ------------------------------------- 51
[11:53:43][D][main:236]: EVERY ---------------- 13
[11:53:49][D][main:257]: EVERY ----------------------- 23
[11:53:56][D][main:236]: EVERY ---------------- 13
[11:54:09][D][main:236]: EVERY ---------------- 13
[11:54:12][D][main:257]: EVERY ----------------------- 23
[11:54:22][D][main:236]: EVERY ---------------- 13
[11:54:32][D][main:274]: EVERY ------------------------------------- 51
[11:54:35][D][main:257]: EVERY ----------------------- 23
[11:54:35][D][main:236]: EVERY ---------------- 13
[11:54:48][D][main:236]: EVERY ---------------- 13
1 Like

Here’s a variation of the above. In this case, it wasn’t really to see exactly how something works, since the documentation is pretty clear. It was more to work out the syntax (YAML can make me a little crazy). I’m planning to use something like this in a project I’m working on.

In this config, I’m launching the script blink from any one of the three original scripts, but I only want it to be running at most once at any given time. The original scripts check to see if it’s already running and only launch it if it isn’t. blink logs what it’s doing instead of blinking an actual LED. It turns out it’s tricky to copy and paste an LED blink into a forum posting. :slight_smile:. As a bonus, the delay time in blink is templated and depends on which script launched it (every_13 causes a delay of 130ms, every_23 causes a delay of 230ms, and every_51 causes a delay of 510ms).

The config:

wifi:
  ssid:      !secret wifi_ssid
  id:        !secret wifi_ssid
  password:  !secret wifi_password
ota:
  password: !secret ota_password
api:
  password: !secret api_password
logger:

globals:
  - id: who_blinked
    type: int
    initial_value: '0'

esphome:
  name: async
  platform: ESP32
  board: esp32thing
  on_boot:
    then:
    - delay: 15s
    - script.execute: every_13
    - script.execute: every_23
    - script.execute: every_51
    - logger.log: "scripts have been launched ------------------------------"

script:
  - id: every_13
    then:
    - while:
        condition:
        - lambda: "return true;"
        then:
        - delay: 13s
        - logger.log: "EVERY ---------------- 13"
        - if:
            condition:
              not:
                script.is_running: blink
            then:
              - globals.set:
                  id: who_blinked
                  value: '13'
              - script.execute: blink
    - logger.log: "13 loop dropped"

  - id: every_23
    then:
    - while:
        condition:
        - lambda: "return true;"
        then:
        - delay: 23s
        - logger.log: "EVERY ----------------------- 23"
        - if:
            condition:
              not:
                script.is_running: blink
            then:
              - globals.set:
                  id: who_blinked
                  value: '23'
              - script.execute: blink
    - logger.log: "23 loop dropped"

  - id: every_51
    then:
    - while:
        condition:
        - lambda: "return true;"
        then:
        - delay: 51s
        - logger.log: "EVERY ------------------------------------- 51"
        - if:
            condition:
              not:
                script.is_running: blink
            then:
              - globals.set:
                  id: who_blinked
                  value: '51'
              - script.execute: blink
    - logger.log: "51 loop dropped"

  - id: blink
    then:
    - logger.log:
        format: ".........BLINK ON........... %d"
        args: [ 'id(who_blinked)' ]
    - delay: !lambda "return 100 * id(who_blinked);" # ms
    - logger.log:
        format: ".........blink off.......... %d"
        args: [ 'id(who_blinked)' ]
    - globals.set:
        id: who_blinked
        value: '0'

Sample output:

[13:45:57][D][main:393]: EVERY ------------------------------------- 51
[13:45:57][D][main:409]: .........BLINK ON........... 51
[13:46:01][D][main:334]: EVERY ---------------- 13
[13:46:03][D][main:417]: .........blink off.......... 51
[13:46:06][D][main:369]: EVERY ----------------------- 23
[13:46:06][D][main:409]: .........BLINK ON........... 23
[13:46:08][D][main:417]: .........blink off.......... 23
[13:46:14][D][main:334]: EVERY ---------------- 13
[13:46:14][D][main:409]: .........BLINK ON........... 13
[13:46:15][D][main:417]: .........blink off.......... 13
[13:46:27][D][main:334]: EVERY ---------------- 13
[13:46:27][D][main:409]: .........BLINK ON........... 13
[13:46:28][D][main:417]: .........blink off.......... 13
[13:46:29][D][main:369]: EVERY ----------------------- 23
[13:46:29][D][main:409]: .........BLINK ON........... 23
[13:46:31][D][main:417]: .........blink off.......... 23
[13:46:40][D][main:334]: EVERY ---------------- 13
[13:46:40][D][main:409]: .........BLINK ON........... 13
[13:46:41][D][main:417]: .........blink off.......... 13
[13:46:48][D][main:393]: EVERY ------------------------------------- 51
[13:46:49][D][main:409]: .........BLINK ON........... 51
[13:46:52][D][main:369]: EVERY ----------------------- 23
[13:46:53][D][main:334]: EVERY ---------------- 13
[13:46:54][D][main:417]: .........blink off.......... 51
[13:47:06][D][main:334]: EVERY ---------------- 13
[13:47:06][D][main:409]: .........BLINK ON........... 13
[13:47:07][D][main:417]: .........blink off.......... 13
[13:47:15][D][main:369]: EVERY ----------------------- 23
[13:47:15][D][main:409]: .........BLINK ON........... 23
[13:47:17][D][main:417]: .........blink off.......... 23

A similar test, but I took out the guard checks that prevented multiple runnings of the blink script. The outcome was pretty much as I expected (confusion about globals due to overlapped invocations), but I ran the test for completeness.

The configuration:

wifi:
  ssid:      !secret wifi_ssid
  id:        !secret wifi_ssid
  password:  !secret wifi_password
ota:
  password: !secret ota_password
api:
  password: !secret api_password
logger:

globals:
  - id: who_blinked
    type: int
    initial_value: '0'

esphome:
  name: async
  platform: ESP32
  board: esp32thing
  on_boot:
    then:
    - delay: 15s
    - script.execute: every_13
    - script.execute: every_23
    - script.execute: every_51
    - logger.log: "scripts have been launched ------------------------------"

script:
  - id: every_13
    then:
    - while:
        condition:
        - lambda: "return true;"
        then:
        - delay: 13s
        - logger.log: "EVERY ---------------- 13"
        - globals.set:
            id: who_blinked
            value: '13'
        - script.execute: blink
    - logger.log: "13 loop dropped"

  - id: every_23
    then:
    - while:
        condition:
        - lambda: "return true;"
        then:
        - delay: 23s
        - logger.log: "EVERY ----------------------- 23"
        - globals.set:
            id: who_blinked
            value: '23'
        - script.execute: blink
    - logger.log: "23 loop dropped"

  - id: every_51
    then:
    - while:
        condition:
        - lambda: "return true;"
        then:
        - delay: 51s
        - logger.log: "EVERY ------------------------------------- 51"
        - globals.set:
            id: who_blinked
            value: '51'
        - script.execute: blink
    - logger.log: "51 loop dropped"

  - id: blink
    then:
    - logger.log:
        format: ".........BLINK ON........... %d"
        args: [ 'id(who_blinked)' ]
    - delay: !lambda "return 100 * id(who_blinked);" # ms
    - logger.log:
        format: ".........blink off.......... %d"
        args: [ 'id(who_blinked)' ]
    - globals.set:
        id: who_blinked
        value: '0'

Sample output:

[17:08:17][D][main:301]: scripts have been launched ------------------------------
[17:08:31][D][main:298]: EVERY ---------------- 13
[17:08:31][D][main:361]: .........BLINK ON........... 13
[17:08:32][D][main:369]: .........blink off.......... 13
[17:08:40][D][main:329]: EVERY ----------------------- 23
[17:08:41][D][main:361]: .........BLINK ON........... 23
[17:08:43][D][main:369]: .........blink off.......... 23
[17:08:44][D][main:298]: EVERY ---------------- 13
[17:08:44][D][main:361]: .........BLINK ON........... 13
[17:08:45][D][main:369]: .........blink off.......... 13
[17:08:56][D][main:298]: EVERY ---------------- 13
[17:08:57][D][main:361]: .........BLINK ON........... 13
[17:08:58][D][main:369]: .........blink off.......... 13
[17:09:03][D][main:329]: EVERY ----------------------- 23
[17:09:03][D][main:361]: .........BLINK ON........... 23
[17:09:06][D][main:369]: .........blink off.......... 23
[17:09:08][D][main:349]: EVERY ------------------------------------- 51
[17:09:08][D][main:361]: .........BLINK ON........... 51
[17:09:09][D][main:298]: EVERY ---------------- 13
[17:09:09][D][main:361]: .........BLINK ON........... 13
[17:09:11][D][main:369]: .........blink off.......... 13
[17:09:14][D][main:369]: .........blink off.......... 0
[17:09:22][D][main:298]: EVERY ---------------- 13
[17:09:22][D][main:361]: .........BLINK ON........... 13
[17:09:24][D][main:369]: .........blink off.......... 13
[17:09:26][D][main:329]: EVERY ----------------------- 23
[17:09:26][D][main:361]: .........BLINK ON........... 23
[17:09:29][D][main:369]: .........blink off.......... 23
[17:09:35][D][main:298]: EVERY ---------------- 13
[17:09:35][D][main:361]: .........BLINK ON........... 13
[17:09:37][D][main:369]: .........blink off.......... 13
[17:09:48][D][main:298]: EVERY ---------------- 13
[17:09:48][D][main:361]: .........BLINK ON........... 13
[17:09:49][D][main:329]: EVERY ----------------------- 23
[17:09:49][D][main:361]: .........BLINK ON........... 23
[17:09:50][D][main:369]: .........blink off.......... 23
[17:09:52][D][main:369]: .........blink off.......... 0

Ah, well, I see that esphome 1.15.x adds script modes. In addition to having built-in finer-grained control over how multiple script invocations interact, the mere presence of the feature provides clarification on what those interactions can be. Yay. https://github.com/esphome/esphome/pull/1168