Notes on ESPHome deep_sleep

When running from a battery it is often desirable to minimise the power consumption; particularly where the ESP32 is taking sensor reading on a regular basis but doing nothing between them.
Some (but not all) ESP32 models support deep_sleep, which allows ESPHome to power off the main CPU when it is not needed, and reboot it after a timer or other input.

I found a number of issues with deep_sleep – many are not problems, but just things which aren’t covered in much detail in the docs. This is my attempt to collect the various pieces I have learnt about deep_sleep in one place, and trying to explain in not-very-technical language. I welcome additions, comments and suggestions.

Intended deep-sleep operation

There is not much information about deep_sleep in the documentation, which gives the impression that its operation is very simple, and it can be:

  1. setup the sensors, etc as normal
  2. add the deep_sleep component, specifying how long it should sleep for, and how long it should be awake for.

This works well, assuming it is all you want. After the run_period, ESPHome turns the main CPU off. After a period of time or some other signal, the CPU is booted up, and starts again.

But if you want to further reduce battery consumption, or wake for other reasons, it gets more complicated …

deep_sleep is actually a shutdown, power-off and boot

The first thing to know is that ESP32’s deep_sleep are actually a shutdown and a boot operation.

  • Calling deep_sleep_enter action runs esphome: on_shutdown;
  • While “asleep”, the ESP32 is powered off and consequently cannot do anything.
  • Esphome: on_boot: is the first thing run when the ESP32 wakes up.

Relationship between update-interval and run_duration

Each sensor has its own update_interval where you specify how often it should be updated – some things (like room temperature) a reading once an hour may be sufficient, but other things (like rainfall) you might like to know minute-by-minute

It gets a little confusing if you are wanting your ESP32 to deep_sleep, then take readings all at the same time when awake. Do you set update_interval for 1 hour or for 5 minutes ?

I created a substitution for update_interval and applied it to all my sensors. I had expected that setting all sensors to the same 5 minute interval they would all be read at the start, then there would be a pause until the start of the next 5 minute interval, telling me how long I need my ESP32 to be awake.

But in practice ESPHome spreads the sensor reading through the common update_interval. If you specify a 5 minute interval, ESPHome will take 5 minutes to gather all the sensor readings; even if they could be reported on in 30 seconds.

In my testing, setting an update_interval longer that the run_duration only reported some of the sensors before going back to sleep – and then next awake period didn’t continue with the sensors missed the previous time; but started again, usually reporting the same sensor as in the previous awake period.

Setting update_interval equal to the run_duration gave 1 result per sensor for each awake period. To determine the minimum time to report all your sensors will take some trial and error, and maybe add a little programming magic to ensure that deep_sleep doesn’t happen until all sensors are reported.

Setting update_interval less than the run_duration gave multiple readings per awake period, though sometimes the order that the sensors were read can change between update intervals.

Sending yaml or ESPHome updates

Consider also that the shorter the run_duration, the less time in which to send yaml configuration or ESPHome updates. For testing I have been using 2 mins run_duration and only 2 mins sleep_duration. There are a few threads on this forum which provide methods to pause deep_sleep to accomplish this … though most use MQTT messages as the trigger.

Wakeup Cause

We probably want to know whether the ESP32 is booting because power is first turned on, because the sleep_duration time is up, because an external signal has caused a wakeup, or some other reason.

In ESPHome’s documentation for deep_sleep, the “ESP32 Wakeup Cause” section gives a yaml example for a template sensor to store the reason for the wakeup, which can then be used to determine the reason for the wakeup. Again, this code works and is useful to report the reason back to Home Assistant, but …

  • Depending on your run_interval and this sensors update_interval it could be updated multiple times while awake. If it is only returning esp_sleep_get_wakeup_cause() (as in the example code) not a problem since the same wakeup code will be returned each time. But not if you want to use this sensor to trigger other actions once per run_duration, or want the actions performed once only, or guaranteed at the beginning of the run_duration.
  • Similarly if you have other code which uses the wakeup cause sensor, remember that it may not be updated until after your other code uses it.
  • If you find you are getting a ridiculously large number for wakeup cause code (like me), it is actually a pointer to the wakeup cause function, rather than its value. Easily solved by referring the .state property like ‘id(wakeup_cause).state‘

Using on_boot:
I recommend setting the wakeup cause variable in your on_boot:, such as

esphome:
  on_boot: 
    then:
    - lambda: |-
        ESP_LOGD("testing", "vvvvvvvvvv ON_BOOT vvvvvvvvvv");
        ESP_LOGD("testing", ">>>>>> on_boot: esp_sleep_get_wakeup_cause=%num_wake_cycles=%d", esp_sleep_get_wakeup_cause(), id(num_wake_cycles) );
        id(wakeup_cause).state = esp_sleep_get_wakeup_cause();
        id(num_wake_cycles) += 1;
        if (esp_sleep_get_wakeup_cause == 0) {
          // reason 0 - ESP_SLEEP_WAKEUP_UNDEFINED: probably power-on
          ESP_LOGD("testing", ">>>>>> on_boot: power-on esp_sleep_get_wakeup_cause=%d, num_wake_cycles=%d", esp_sleep_get_wakeup_cause(), id(num_wake_cycles) );
        } else {
          // 4 - ESP_SLEEP_WAKEUP_TIMER: Wakeup caused by timer
        ESP_LOGD("testing", ">>>>>> on_boot: wakeup   esp_sleep_get_wakeup_cause=%d, num_wake_cycles=%d", esp_sleep_get_wakeup_cause(), id(num_wake_cycles) );
        };
    - logger.log: 
        format: " >>>>>> on_boot: esp_sleep_get_wakeup_cause=%d, num_wake_cycles=%d"
        args: [ esp_sleep_get_wakeup_cause(), id(num_wake_cycles) ]

The problem with doing much in the on_boot: and on_shutdown: sections is that you don’t get to see any of the debugging logger.log or ESP_LOGD() statements unless you have your terminal program connected to the ESP32’s UART … a topic for a separate rant. Thus, while the above debugging code is executed, you won’t normally see any of the results.

Note that there are several forum posts where the user has taken the approach of forcing all the sensor updates and processing in the on_boot: routine, ending by directly calling deep_sleep.enter. This does seem a particularly effective way to minimise the time that the ESP32 is awake … at the cost of being difficult to debug.

Note also that some actions have the effect of exiting the list of actions in on_boot: and on_shutdown: without any notification – eg trying to turn off the RGB LED on the ESP32 board.

Extra increments of a counter

I also noticed that my counter of the number of wake cycles was being incremented twice. Turns out that sensors get updated silently in the startup (in that delay in the HA ESPHome Builder LOG before you start to see any log entries), so be aware of this.

1 Like

It kind of sounds like your talking about some esp projects that dont actually need batteries for their power source to begin with and these certainly dont sound like they need to be portable so, i question is why mess with deep_sleep and lifetime battery maintenance for some esp projects that are made to be stationary and only take environmental sensor readings… Those aren’t good candidates for battery powered projects.

I am curious as to which “esp projects that dont actually need batteries” you are referring to ?

Programmatically changing deep-sleep duration

This is actually fairly easy using the set_sleep_duration() function in a lambda call (which I do not think is mentioned anywhere in the documentation).

    on_value:
      then:
      - if:
          condition:
            lambda: 'return id(some_sensor_id).state > 30.0 ;'
          then:
          - lambda: |-
              id(deep_sleep_1).set_sleep_duration(300000);
          else:
          - lambda: |-
              id(deep_sleep_1).set_sleep_duration(900000);

Time units

While we can specify time in our yaml code in convenient units (hours, minutes, seconds), the lambda set_deep_sleep_duration() function requires a number of milliseconds.
To make it easier for me to use substitution to change my timings during testing I have ended up with:

substitutions:
  # changing the sleep_duration in an automation requires the time 
  #   as a number of milli-seconds, so probably easier to use the
  #   same us unit everywhere, starting with defining durations here.
  #   There are 60 seconds in a minute, 1000 milliseconds (ms) in a second ... so 
  #   1 min = 60 seconds x 1000ms = 60,000ms ;  10 min=600,000ms, 15min=900,000ms, 30min=1800,000ms
  #   1 hour=3600,000ms, 2 hours=7200,000ms,  3 hours= 10800,000ms; 4 hours=14400,000ms; 6 hours=21600,000ms
  sleep_awake_duration:      '600000'   # =10 min  # how long to run (be awake)
  sleep_asleep_duration:     '300000'   # =5 min   # how long to sleep for
  sleep_low_batt_duration: '10800000'   # =3 hours

...

deep_sleep: 
  id: deep_sleep_1
  run_duration:   ${sleep_awake_duration} ms   # how long to run (be awake)
  sleep_duration: ${sleep_asleep_duration} ms  # how long to sleep for

...

script:
      # if low battery, take long sleeps until battery is recharged
      - if:
          condition: 
            lambda: return id(batt_voltage).state < 3.2;
          then:
            # raise an alarm / notification
            - logger.log: 
                format: "##### battery is <3.2v (%.2f) LOW BATTERY - SHUTTING DOWN #####"
                args: [ 'id(batt_voltage).state' ]
            - globals.set:
                id: is_low_battery
                value: 'true'
            - lambda: |-
                id(deep_sleep_1).set_sleep_duration( $sleep_low_batt_duration );  

...
 
      # if battery has charged, go back to normal deep sleeps 
      - if:
          condition:
            lambda: return id(is_low_battery) == true;
          then:
            - logger.log: "##### return from low battery state"
            - globals.set:
                id: is_low_battery
                value: "false"
            - lambda: |-
                id(deep_sleep_1).set_sleep_duration( $sleep_asleep_duration );  
1 Like

Deep_sleep at set times

Sometimes we want to sleep at set times; for example Mahko_Mahko’s ESPlanty solar powered plant watering wants to check his plant through the day - but to sleep all night.

He uses the time: component’s on_time trigger at 8pm (20:00:00 in 24-hour time) to enter deep_sleep and wake at 5am (05:00:00). When the ESP32 wakes (boots) in the morning it uses the run_duration and sleep_duration defined in the deep_sleep: section

deep_sleep: 
  id: deep_sleep_1
  run_duration: ${auto_wake_time}
  sleep_duration: ${sleep_time}
  
time:
  - platform: homeassistant
    id: esptime
    on_time:
      #Deep sleep at 8pm and wake up at 5am.
      - hours: 20
        then:
          #Publish battery level to the end of day sensor since it will be our last update.
          - lambda: return id(end_of_day_battery_percent).publish_state(id(batt_level).state);
          #Reset culmulative light sesnor reset
          - sensor.integration.reset: irrigation_cul_lux_hours
          - delay: 1s
          #Rest up
          - deep_sleep.enter:
              id: deep_sleep_1
              until: "05:00:00"
              time_id: esptime
1 Like