Solution for Keeping Devices Awake During OTA Updates in ESPHome

Hello everyone!

I wanted to share with you a solution I found for a problem that many might be facing: how to keep devices awake during OTA updates in ESPHome.

The problem: I had devices that, for power-saving reasons, would enter deep sleep after sending data to Home Assistant. This meant they would only stay awake for a few seconds, making it impossible to update them via OTA.

The solution: After several attempts, I managed to find a solution that worked for me. I created a service in Home Assistant as an input_boolean. When this service is set to ON, it indicates that an update is desired. However, the issue was that devices couldn’t read this service upon boot as they were in deep sleep.

To address this, I included a while loop immediately after waking up. This loop persists while the service returns an empty value. Once it returns a value (which should be either OFF or ON), the device proceeds with the rest of the code. If it’s OFF, it continues with its scheduled operation; if it’s ON, it cancels deep sleep and remains awake to receive the update.

Why it works: This solution allows the device to be attentive to OTA updates only when needed, while still conserving power by remaining in deep sleep when an update isn’t required.

I hope this solution proves helpful to those facing the same issue. And I’d love to hear if anyone has an even more optimal solution than this!

The Home Assitant service:

input_boolean: 
  ota_update_available:
    name: OTA Update Available
    initial: off
    icon: mdi:nature-people

The ESPHome code (Lolin-S2 mini):

esp32:
  board: lolin_s2_mini
  framework:
    type: arduino

esphome:
  name: read-ha-on-awake
  friendly_name: read HA on awake
  
  on_boot:
    then:
      # I use the onboard led to visually check at which point of the process we are.
      - light.turn_on: light_esp
      # This is where the 'trick' is, I make my workaround with the while loop until I verify that we have a connection to HA.
      - while:
          condition:
            lambda: 'return id(ota_update_available).state == "";'
          then:
            - logger.log: "We're still not connecting to HA, retrying in 5s."
            - homeassistant.service:
                service: 'homeassistant.update_entity'
                data:
                  entity_id: 'input_boolean.ota_update_available'
            # I test using the onboard led.
            - light.turn_off: light_esp
            - delay: 50ms
            - light.turn_on: light_esp
            - delay: 5s
      - homeassistant.service:
          service: 'homeassistant.update_entity'
          data:
            entity_id: 'input_boolean.ota_update_available'
      - delay: 5s
      - if:
          condition:
            text_sensor.state:
              id: ota_update_available
              state: 'on'
          then:
            - logger.log: "OTA Update Available!"
            # I test using the onboard led.
            - light.turn_off: light_esp
            - delay: 500ms
            - light.turn_on: light_esp
            # Prevent the ESP32 from going to sleep
            - deep_sleep.prevent: deep_sleep_1 
          else:
            - logger.log: "no OTA Update Available!"
            # I test using the onboard led.
            - light.turn_off: light_esp
            - delay: 200ms
            - light.turn_on: light_esp
            - delay: 1000ms
            - light.turn_off: light_esp
            - delay: 200ms
            - light.turn_on: light_esp
            - delay: 1000ms
            - light.turn_off: light_esp
            - delay: 200ms
            - light.turn_on: light_esp
            - delay: 1000ms
            - light.turn_off: light_esp

logger:

api:
  encryption:
    key: "xxxxxxxxxxxxxx"

ota:
  password: "f41fefdee105a2e5c2d0263fe94810ca"

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

  ap:
    ssid: "Read-Ha-On-Awake"
    password: "xxxxxxxxx"

captive_portal:

web_server:

deep_sleep:
  id: deep_sleep_1
  run_duration: 60s
  sleep_duration: 20s

text_sensor:
  - platform: homeassistant
    name: "OTA Update Available"
    entity_id: "input_boolean.ota_update_available"
    id: ota_update_available

light:
  - platform: binary
    id: light_esp
    name: "ESP LIGHT"
    output: light_output

output:
  - id: light_output
    platform: gpio
    pin: GPIO15

Best regards to all!

1 Like

Nice.

If you use a wait_until then you have the option of having a timeout in case your HA server is down (to avoid battery being drained). I think…

Hi,

I was trying to use it, but I couldn’t find a way to wait_until it was connected to the api, how could I do it?

I haven’t used it, but there’s this…

and I thought you might be able to do a “not” type inversion of this?

I found this on the Internet a long time ago and it works flawlessly:

Starts from a similar boolean helper that I switch on/off in the UI.
Then:

esphome:
  name: temp-002
  on_boot:
    then:
      - script.execute: consider_deep_sleep_temp002

#...Set up Wi-Fi, sensors etc... Then:

deep_sleep:
  id: deep_sleep_control_temp002
  sleep_duration: 15min

# Will only pick up a value if this boolean helper device is configured in Home Assistant
# If the device isn't configured, or Home Assistant is offline, it'll default to false
binary_sensor:
  - platform: homeassistant
    id: prevent_deep_sleep_temp002
    entity_id: input_boolean.prevent_deep_sleep_temp002

script:
  - id: consider_deep_sleep_temp002
    mode: queued
    then:
      - delay: 20s
      - if:
          condition:
           binary_sensor.is_on: prevent_deep_sleep_temp002
          then:
           - logger.log: 'Skipping sleep, per prevent_deep_sleep_temp002'
          else:
           - deep_sleep.enter: deep_sleep_control_temp002
      - script.execute: consider_deep_sleep_temp002

Your nice solution of the blinking light has no use for me as my esp’s are housed in a non-transparent box.

I therefore have an automation that sends me a warning whenever one of the sensors changes status so that I know that the esp has become awake and ready to upload the firmware (and so that I do not forget to switch off the boolean switch to have the esp go back into deep sleep.) Smarter people must know a better solution than an automation to send such message?

How about disabling deep sleep mode from HA? Then update and automatically restore the deep sleep mode on the ESP upon reboot.

The solution you’re using is based on this 20s delay.

I can see that it’s probably fine for a lot of use cases, but it’s not that “optimal” or “smart”.

Other variants of this will check if they can go to sleep as soon as tasks are complete.

How would you do that?

Hey there again! I’ve tried this solution, replacing the previous code with something more direct:

yaml

- wait_until:
    condition:
      api.connected:
    timeout: 30s
- logger.log: "API connected!"

But it seems it still doesn’t work as expected. Although it waits for the API connection, it’s unable to correctly retrieve the value of the input_boolean at this point. It always behaves as if the state were OFF.

As for whether the lambda condition should be negated, the truth is no, because as long as there’s no value in the response, we remain in the checking loop.

Thanks for the ideas; I’m still working on finding a more optimal solution.

1 Like

Hi, thanks a lot for the code. Honestly, I’ve come across it before, but I ran into some issues trying to implement it in my project. Nevertheless, I’ll give it another go and test it out. I’m really keen on finding an optimal solution to this problem.

Regarding the LED usage, you’re absolutely right, it’s only useful for testing the system. In my case, I won’t have the opportunity to see the LED once my project is in production either. Although there’s always the option to include a visible LED, I’m not considering it due to the energy consumption it entails. For production, my plan is to send a notification through the Home Assistant app on my iPhone when the devices are ready for OTA update.

Thanks again for your help and for sharing your experience!

The ‘Delay’ simply replaces the ‘run_duration’ of the Deep Sleep component.
I will never pretend that the solution I found elsewhere is a smart or optimal one. Nice to point that out but it works for me and many other (more or less) newbies. Thanks to topic starter, such newbies have picked up a new approach.

2 Likes

If that’s the idea. Indicate from Home Assistant that there’s an OTA update available and that this can be applied to multiple devices at the same time. Having multiple devices of the same type, this solution is really practical.

In my code, the example I provided is simply a demonstration of how to control exactly that, the controls from Home Assistant. After the update, my plan is to change the state of the input_boolean in Home Assistant and then manage this change from the ESP32s so that they know they can enter deep_sleep mode.

It seems that after trying various suggestions, I haven’t yet found a more efficient method than the one I’ve proposed here. I’ve updated the code to include control of the deep sleep mode by changing the state of the Home Assistant service in real-time. Now, with the ESP32 powered on, the device detects and responds to changes in the service’s state, entering deep sleep mode or bypassing it as needed.

Additionally, I’m always open to suggestions for finding an even better method. Any ideas or advice would be greatly appreciated!

I hope this approach works as expected for you! If you need further assistance or have any other questions, I’m here to help, mate.

The Home Assitant service:

input_boolean: 
  ota_update_available:
    name: OTA Update Available
    initial: off
    icon: mdi:nature-people

The ESPHome code (Lolin-S2 mini):

esp32:
  board: lolin_s2_mini
  framework:
    type: arduino

esphome:
  name: read-ha-on-awake
  friendly_name: read HA on awake
  
  on_boot:
    then:
      # I use the onboard led to visually check at which point of the process we are.
      - light.turn_on: light_esp
      # This is where the 'trick' is, I make my workaround with the while loop until I verify that we have a connection to HA.
      - while:
          condition:
            lambda: 'return id(ota_update_available).state == "";'
          then:
            - logger.log: "We're still not connecting to HA, retrying in 5s."
            - homeassistant.service:
                service: 'homeassistant.update_entity'
                data:
                  entity_id: 'input_boolean.ota_update_available'
            # I test using the onboard led.
            - light.turn_off: light_esp
            - delay: 50ms
            - light.turn_on: light_esp
            - delay: 5s
      - homeassistant.service:
          service: 'homeassistant.update_entity'
          data:
            entity_id: 'input_boolean.ota_update_available'
      - delay: 5s
      - if:
          condition:
            text_sensor.state:
              id: ota_update_available
              state: 'on'
          then:
            - logger.log: "OTA Update Available!"
            # I test using the onboard led.
            - light.turn_off: light_esp
            - delay: 500ms
            - light.turn_on: light_esp
            # Prevent the ESP32 from going to sleep
            - deep_sleep.prevent: deep_sleep_1 
          else:
            - logger.log: "no OTA Update Available!"
            # I test using the onboard led.
            - light.turn_off: light_esp
            - delay: 200ms
            - light.turn_on: light_esp
            - delay: 1000ms
            - light.turn_off: light_esp
            - delay: 200ms
            - light.turn_on: light_esp
            - delay: 1000ms
            - light.turn_off: light_esp
            - delay: 200ms
            - light.turn_on: light_esp
            - delay: 1000ms
            - light.turn_off: light_esp

logger:

api:
  encryption:
    key: "xxxxxxxxxxxxxx"

ota:
  password: "f41fefdee105a2e5c2d0263fe94810ca"

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

  ap:
    ssid: "Read-Ha-On-Awake"
    password: "xxxxxxxxx"

captive_portal:

web_server:

deep_sleep:
  id: deep_sleep_1
  run_duration: 60s
  sleep_duration: 20s

light:
  - platform: binary
    id: light_esp
    name: "ESP LIGHT"
    output: light_output

output:
  - id: light_output
    platform: gpio
    pin: GPIO15


text_sensor:
  - platform: homeassistant
    name: "OTA Update Available"
    entity_id: "input_boolean.ota_update_available"
    id: ota_update_available
    on_value:
      then:
        - if:
            condition:
              text_sensor.state:
                id: ota_update_available
                state: 'off'
            then:
              - deep_sleep.enter: deep_sleep_1
            else:
              - deep_sleep.prevent: deep_sleep_1
              

1 Like

On mobile here, but look at the prevent call here: Deep Sleep Component — ESPHome.

It’s to solve exactly this issue.

Wrap that call in a template switch automation. You can then control the switch from HA.

@xmon-df ^^

If you’re running ESPHome add-on then you can add execution of a shell command to the service something like the below to automatically start the OTA. This needs the Advanced SSH & Web Terminal add-on with protected mode disabled and the SSH keys setup first.

shell_command:
  esphome_ota_device: ssh root@<HA IP> -i /config/.ssh/haid -o StrictHostKeyChecking=no docker exec addon_5c53de3b_esphome esphome run /config/esphome/<target>.yaml --device OTA --no-logs

Hmm right. Possibly you need to wait a little longer to then retrieve the actual value after connection. Not sure. Could try adding a 1sec delay etc.

What I meant was, you could “wait_until there is a value in the response”

So maybe wait until

lambda: 'return id(ota_update_available).state != "";'

But the esp is asleep and won’t receive the prevent deep sleep action?

This is why you need to wedge logic into on_boot actions.

The OP already makes use of deep_sleep.prevent.

Yes, I was just about to answer that you need to implement the on_boot action. It seems like you don’t like that.

I already have a similar solution to this which wakes up, updates all sensors, and goes to sleep if a deep sleep prevent toggle isn’t on.

Using on_boot and a prevent deep sleep switch/Boolean is a pretty standard approach around the forums.

The OP has a pretty good solution and is looking for suggestions of improvements.

The OP already uses both on boot and deep sleep prevent (as do I).

1 Like

Yeah, you’re right. I lost track of the fact that there was a working solution.

1 Like