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:
- setup the sensors, etc as normal
- 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.