Auto-update ESPHome Devices via Automation

I learned something new in a Saturday morning… :wink:
Thanks for sharing!!

1 Like

Updating the esphome devices one-by-one sequentially is considered, as this script runs every minute from 3.00-3.59am, on the condition that none of the esphome nodes in the list has in_progress=true, and passes only the first node that has update.state=on to the update.install service .

  - service: update.install
    target:
      entity_id: "{{ (states.update | selectattr('entity_id', 'match', 'update\.node\d+_firmware')
        | selectattr('state', 'eq', 'on') | first).entity_id }}"

Unless you meant calling up the entire states.update object and them filtering them poses a huge cpu drain, then yea that I’m unaware of… Is processing a list of entities really that resource intensive?

1 Like

Thanks for the script, @loongyh. I am getting an error when trying to edit it in the File Editor - but can’t figure out what is wrong.

image

The code is copy / pasted from yours so I’m pretty sure it’s not a typing error and as I am a novice at this I don’t know how to fix it.

Does anyone have an idea, please?

I just stumbled upon this myself today and decided to go a slightly different way:

automation:
  - alias: Auto-update ESP devices
    trigger:
      - platform: time_pattern
        hours: "3"
        minutes: "*"
    action:
      - variables:
          outdated_esp_items: >-
            {{ states.update
                | selectattr('state', 'eq', 'on')
                | selectattr('attributes.title', 'match', 'ESPHome')
                | selectattr('attributes.auto_update', 'eq', false)
                | selectattr('attributes.in_progress', 'eq', false)
                | sort(attribute='attributes.installed_version')
                | map(attribute='entity_id')
                | list }}
          next_to_update: "{{ outdated_esp_items | first }}"
      - condition:
          - "{{ outdated_esp_items | count > 0 }}"
      - service: update.install
        target:
          entity_id: "{{ next_to_update }}"
    mode: single
6 Likes

This works great! Thank you @bjeanes !

The naming convention I have for my ESPHome nodes uses prefixes (e.g. light_, switch_, sensor_) - and I’d like to be able to run this for the various groups separately (for example, run lights_ during the day when the flash as they restart won’t matter).

Have you any thoughts for how that functionality to this, please?

There are a few ways one might achieve this. If I were doing it, I’d probably do something like this, using trigger variables:

automation:
  - alias: Auto-update ESP devices
    trigger:
      - platform: time_pattern
        hours: 3
        minutes: "*"
        variables:
          prefix: sensor_

      - platform: time_pattern
        hours: 12
        minutes: "*"
        variables:
          prefix: light_
      # etc ...
    action:
      - variables:
          outdated_esp_items: >-
            {{ states.update
                | selectattr('state', 'eq', 'on')
                | selectattr('attributes.title', 'match', 'ESPHome')
                | selectattr('attributes.auto_update', 'eq', false)
                | selectattr('attributes.in_progress', 'eq', false)
                | selectattr('entity_id', 'match', prefix)
                | sort(attribute='attributes.installed_version')
                | map(attribute='entity_id')
                | list }}
          next_to_update: "{{ outdated_esp_items | first }}"
      - condition:
          - "{{ outdated_esp_items | count > 0 }}"
      - service: update.install
        target:
          entity_id: "{{ next_to_update }}"
    mode: single

In other words, each trigger can introduce its own values for variables, so we create a trigger for each entity_id prefix, and add a filter to the outdated_esp_items template to use the prefix that the trigger sets.

Note: I haven’t tested this; I wrote it directly into here, so it might have some typos or might be slightly off. But I suggest trying to read through Understanding Automations - Home Assistant, including every other nav item under Automations in the Topics sidebar.

Thank you so much - I will try that, and go and do some reading!

I double checked with the last update:
The esphome-addon will compile and update the devices one-by-one after the update-service ist called for the complete list of outdated esphomes.

So it should be fine to call the update once a night:

automation:
  - alias: Auto-update ESP devices
    trigger:
      - platform: time_pattern
        hours: "3"
        minutes: "15"
    action:
      - variables:
          outdated_esp_items: |-
            {{ states.update
                | selectattr('state', 'eq', 'on')
                | selectattr('attributes.title', 'match', 'ESPHome')
                | selectattr('attributes.device_class', 'eq', 'firmware')
                | selectattr('attributes.auto_update', 'eq', false)
                | selectattr('attributes.in_progress', 'eq', false)
                | sort(attribute='attributes.installed_version')
                | map(attribute='entity_id')
                | list }}
      - condition: template
        value_template: "{{ outdated_esp_items | count > 0 }}"
      - service: update.install
        target:
          entity_id: "{{ outdated_esp_items }}"
    mode: single

Edit: Better condition that shows up in the gui.
Edit 2: Select only “firmware” entities and leave the addon update to the addon manager
Edit 3: Delete duplicate condition

7 Likes

Thanks for sharing, this was driving me crazy and saved me so much time!

… although the compile itself seems to run at the same time, I have 6 cc1 processes and 6 ESPHome nodes?

Running with Frigate, which is pretty hungry anyway, doesn’t leave me much/any headroom in terms of processing.

Will have to look into something that does sequential compile, I think, for me personally.

EDIT: Untested, but perhaps something like this

Create a script:

update_esphome_sequentially:
  for_each:  >-
    {{ states.update
        | selectattr('state', 'eq', 'on')
        | selectattr('attributes.title', 'match', 'ESPHome')
        | selectattr('attributes.device_class', 'eq', 'firmware')
        | selectattr('attributes.auto_update', 'eq', false)
        | selectattr('attributes.in_progress', 'eq', false)
        | sort(attribute='attributes.installed_version')
        | map(attribute='entity_id')
        | list }}
  sequence:
    - service: update.install
      target:
        entity_id: "{{ repeat.item }}"

Call the script in the automation at the end, instead of the update.install?

EDIT 2: Oh maybe not, needs some tweaking:

Script with object id 'update_esphome_sequentially' could not be validated and has been disabled: extra keys not allowed

EDIT 3: Had a formatting error, the below validates and loads:

update_esphome_sequentially:
  sequence:
  - repeat:
      for_each:  >-
        {{ states.update
            | selectattr('state', 'eq', 'on')
            | selectattr('attributes.title', 'match', 'ESPHome')
            | selectattr('attributes.device_class', 'eq', 'firmware')
            | selectattr('attributes.auto_update', 'eq', false)
            | selectattr('attributes.in_progress', 'eq', false)
            | sort(attribute='attributes.installed_version')
            | map(attribute='entity_id')
            | list }}
      sequence:
        - service: update.install
          target:
            entity_id: "{{ repeat.item }}"

Whether it will do what I want, remains to be seen. And then the automation:

- id: 'Auto-update ESP devices'
  alias: Auto-update ESP devices
  trigger:
    - platform: time_pattern
      hours: "3"
      minutes: "15"
  action:
    - variables:
        outdated_esp_items: |-
          {{ states.update
              | selectattr('state', 'eq', 'on')
              | selectattr('attributes.title', 'match', 'ESPHome')
              | selectattr('attributes.device_class', 'eq', 'firmware')
              | selectattr('attributes.auto_update', 'eq', false)
              | selectattr('attributes.in_progress', 'eq', false)
              | sort(attribute='attributes.installed_version')
              | map(attribute='entity_id')
              | list }}
    - condition: template
      value_template: "{{ outdated_esp_items | count > 0 }}"
    - service: script.update_esphome_sequentially

EDIT 4: Probably not needed after all, see below…

3 Likes

interesting… my observation comes from the esphome-addon’s logfile where every update processes seemed to be started sequentially node by node.
Could you cross-check (i) how your addon log behaves and (ii) how many cc1 processes will be created with a single update?

1 Like

… Looks like you’re right! When doing a clean build re-install as a test, anyway. Should have checked this to start with! Got carried away.

I assume it’s spawning 1 per core (4 core box) that do the heavy lifting.

Oh well, learnt something about for_each and repeat in scripts :wink:

Can you share your script.notify please? Thanks!

I guess this script is sending data to script.notify

That’s a very convoluted script that handles all notification channels, etc. I’m not proud of it.
What are you looking for?

For your take on a general notification script .) I understand its purpose from the code you use but never thought to write my own as I am not a coder, so what I can with code mostly is to alter examples for my own purposes. So if you could share yours, I would be grateful

Thank you! This is wonderful!

1 Like

I’m using the Home Assistant Yellow and found that the updates would time out due to lack of resources so I implemented a stop add-ons | Update esphome | restart add-ons workflow:

Automation:

alias: Update- ESPHome Update All Devices
description: ""
trigger:
  - platform: template
    value_template: >-
      {{ integration_entities('esphome') | select('match', '^update.') |
      select('is_state', 'on') | list | count > 0 }}
condition: []
action:
  - service: script.esphome_update_all_esphome_devices
    metadata: {}
    data: {}
mode: single

Script to update:

alias: ESPHome- Update All ESPHome Devices
sequence:
  - service: script.add_ons_stop
    data: {}
  - repeat:
      sequence:
        - service: update.install
          data: {}
          target:
            entity_id: "{{ repeat.item }}"
        - wait_template: "{{ is_state(repeat.item, 'off') }}"
          continue_on_timeout: true
      for_each: >-
        {{ states.update | selectattr('state', 'eq', 'on') | 
        map(attribute='entity_id') |
        select('in',integration_entities('esphome')) | list }}
  - service: script.add_ons_start
    data: {}
mode: single

Script to stop:

alias: Add-Ons_Stop
sequence:
  - service: hassio.addon_stop
    data:
      addon: core_whisper
  - service: hassio.addon_stop
    data:
      addon: d63406df_acurite2mqtt
  - service: hassio.addon_stop
    data:
      addon: a0d7b954_ssh
  - service: hassio.addon_stop
    data:
      addon: a0d7b954_airsonos
  - service: hassio.addon_stop
    data:
      addon: a0d7b954_appdaemon
  - service: hassio.addon_stop
    data:
      addon: a0d7b954_chrony
  - service: hassio.addon_stop
    data:
      addon: core_configurator
  - service: hassio.addon_stop
    data:
      addon: cebe7a76_hassio_google_drive_backup
  - service: hassio.addon_stop
    data:
      addon: core_mosquitto
  - service: hassio.addon_stop
    data:
      addon: 2ad4c73a_mqtt-explorer
  - service: hassio.addon_stop
    data:
      addon: a0d7b954_nut
  - service: hassio.addon_stop
    data:
      addon: core_openwakeword
  - service: hassio.addon_stop
    data:
      addon: core_piper
  - service: hassio.addon_stop
    data:
      addon: 03cabcc9_ring_mqtt
  - service: hassio.addon_stop
    data:
      addon: 15d21743_samba_backup
  - service: hassio.addon_stop
    data:
      addon: core_samba
  - service: hassio.addon_stop
    data:
      addon: a0d7b954_vscode
  - service: hassio.addon_stop
    data:
      addon: core_whisper
mode: single

Script to start:

alias: Add_Ons_Start
sequence:
  - service: hassio.addon_start
    data:
      addon: core_whisper
  - service: hassio.addon_start
    data:
      addon: d63406df_acurite2mqtt
  - service: hassio.addon_start
    data:
      addon: a0d7b954_ssh
  - service: hassio.addon_start
    data:
      addon: a0d7b954_airsonos
  - service: hassio.addon_start
    data:
      addon: a0d7b954_appdaemon
  - service: hassio.addon_start
    data:
      addon: a0d7b954_chrony
  - service: hassio.addon_start
    data:
      addon: core_configurator
  - service: hassio.addon_start
    data:
      addon: cebe7a76_hassio_google_drive_backup
  - service: hassio.addon_start
    data:
      addon: core_mosquitto
  - service: hassio.addon_start
    data:
      addon: 2ad4c73a_mqtt-explorer
  - service: hassio.addon_start
    data:
      addon: a0d7b954_nut
  - service: hassio.addon_start
    data:
      addon: core_openwakeword
  - service: hassio.addon_start
    data:
      addon: core_piper
  - service: hassio.addon_start
    data:
      addon: 03cabcc9_ring_mqtt
  - service: hassio.addon_start
    data:
      addon: 15d21743_samba_backup
  - service: hassio.addon_start
    data:
      addon: core_samba
  - service: hassio.addon_start
    data:
      addon: a0d7b954_vscode
mode: single

Would love to clean up the list of add-ons to stop/start if possible but so far, this has gotten me past the timeouts while attempting to update. I’ve also added the stop/start scripts to an update-dashboard so I can call them manually in the event I want to apply a custom firmware to a device (such as having the AtomEcho output its audio to the nearest Sonos speaker)