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.

Deep_sleep allows ESPHome to power off the main CPU when it is not needed, and reboot it after a timer or an external signal. However 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-so-technical language. I welcome additions, comments and suggestions.

Firstly, not all ESP32 models support deep_sleep, and some models (including the ESP32-S3 which i am using) have a secondary low power low speed core which is used by deep_sleep to perform the timings. Without the second core you will require additional circuitry to do the timing and wake the main core.

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.

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. This works well … assuming that is all you want.

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 … like receiving messages over wi-fi.
  • 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. If your ESP32 is awake 5 minutes then asleep for 1 hour, 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 at random through the common update_interval.

In my testing, setting an update_interval longer than 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. So, not a good option :frowning:

Setting update_interval equal to the run_duration gave 1 result per sensor for each awake period. 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.

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. 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

ESP32’s deep_sleep is actually a shutdown and a boot operation – but maybe we 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 - or not updated during the run_interval. 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 a different matter 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 could actually be a pointer to the location in memory of the wakeup cause function, rather than its value. Easily solved by referring the .state property like ‘id(wakeup_cause).state‘

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

esphome:
  on_boot: 
    then:
    - lambda: |-
        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) );
        };

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.

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.

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.

UPDATE: NOT TRUE ! For an explanation see post #11

2 Likes

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 ?

Yes, my own use case is basically taking environmental sensor readings in a greenhouse (and so not mobile) - you are correct so far - but there is no mains power to the back of the property. I am renting in a small block of apartments, and cannot justify the cost of laying power cables to the back of the property, or paying to undo the damage when my lease ends.

A solar panel and LiPo battery are working well when combined with deep_sleep to reduce power consumption.

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
2 Likes

Sending ESPHome updates or updating our yaml

To conserve battery power, we want to be awake only a short time, and sleep a long time. But sometimes we need to update the ESPHome firmware OTA or to update our program code … and it is hard to catch the ESP32 when it’s awake only a short time.

The technique here is basically to set a switch (or button or whatever) in the Home Assistant interface, which is checked every time the ESP32 wakes up; triggering the automation on the ESP32 to execute a deep_sleep.prevent action.

When the user is ready, resetting the switch in Home Automation tells the ESP32 that it can deep_sleep.allow to continue the regular deep_sleep operation.

Of course, Murphy’s Law says that your ESP32 will have just entered deep_sleep when you want it to stay awake, and so you may have to wait a long time for the next awake period. Remember also that the ESP32 is powered off when asleep, so there is no way to wake it up quicker - short of physically pressing the reset button.

Communicate with Home Assistant

ESPHome provides the homeassistant platform, properly known as Native API, which at first seems the obvious method to communicate with Home Assistant - however it is well known that the homeassistant API does not play nicely with deep_sleep. This was raised back in 2019 (see this github discussion for details), and there is no ‘solution’ yet.

UPDATE: Post #9 below shows how you can use the Native API and helpers at the HA end.

The work-around is to use MQTT (a 3rd-party message transport standard) instead; and there are several threads on this forum which provide code to accomplish this. MQTT is not built into Home Assistant - but there are a few supported Add-Ons which are easily installed and set up through Home Assistant’s Settings > Add-Ons menu. I found the “Mosquitto Broker” does the basic job without requiring much configuration, and is what I use here.

What is MQTT ?

Rather than explain the basics of MQTT here, there are plenty of useful resources, including Steve’s Guide to Networking, IoT and the MQTT Protocol and HiveMQ’s MQTT Essentials

You will need a username and password to connect to your MQTT broker. If using HA’s Mosquitto Add-on, you can use an existing HA user, or create a generic user in HA Setting > People > Users.

Then in your ESPHome yaml code add the following - remembering to change my 192.168.1.98 to the name of your MQTT Broker (if using HA’s Mosquitto Broker it will be the same as your HomeAssistant server), and enter your mqtt username and password.
Note that I made up “greenhouse/deep_sleep_mode” and you can use whatever Topic and Payload is sensible to you.

mqtt:
  discovery: true
  broker: 192.168.1.98
  port: 1883
  username: !secret mqtt_user
  password: !secret mqtt_password
  birth_message:
    topic: greenhouse/deep_sleep_mode
    payload: ''
  will_message:
    topic: greenhouse/deep_sleep_mode
    payload: ''
    
  on_message:
    - topic: greenhouse/deep_sleep_mode
      payload: 'OFF'
      then:
        - logger.log: ">>>>>> MQTT received   topic=greenhouse/deep_sleep_mode, payload='OFF'"
        - deep_sleep.prevent: deep_sleep_1
    - topic: greenhouse/deep_sleep_mode
      payload: 'ON'
      then:
        - logger.log: ">>>>>> MQTT received    topic=greenhouse/deep_sleep_mode, payload='ON'" 
        - deep_sleep.enter: deep_sleep_1

If you look through the purple text when the device starts up you wall probably notice a lot more MQTT topics have automatically been setup for your other sensors - but we don’t have to use them.

How to publish the MQTT packet to turn off deep_sleep ?

Under Settings > Devices & services click on the “MQTT integration” then “Configure” to bring you to a page which lets you Publish a packet or Listen to a topic.


Enter the Topic which you have chosen, and the Payload for the action you want, then click “Publish”.

MQTT QoS

MQTT runs over TCP/IP which reliably delivers each message in the correct order to the recipient device … but is not so great if the device isn’t awake when the message gets delivered. MQTT provides three levels of QoS:

  • QOS 0 – commonly called “fire and forget” functions akin to the underlying TCP protocol, where the message is sent without further follow-up or confirmation. It is possible that the receiver never gets the packet so should be used where some packet loss is acceptable.
  • QOS 1 – the sender keeps a copy of the message until it receives a packet from the receiver to confirm the successful receipt. If the sender doesn’t receive the confirmation packet within a reasonable time frame, it re-transmits the message to ensure its delivery. It is therefore possible that the receiver receives the same packet more than once.
  • QOS 2 – by adding extra overhead (confirming the confirmation packets) it is possible to guarantee that the payload is delivered Only Once.

I thought I was finished with deep_sleep, but now I have found latching relays like DFRobot’s DFR0996 Magnetic Latching Relay - and I love them ! Forget DFRobot’s bullshit about them being a new invention; but they can be particularly useful in battery-powered IoT devices.

Use case for latching relay vs regular relay

In my case I’m developing a system for my partners orchid greenhouse, and I want to do the watering at a set time every day - and also as required on particularly hot days. Simple answer is to use a standard relay switch to drive a 12v or 24V water solenoid valve to let the water flow.

The relay uses a small amount of current to hold the relay open, but when the device is battery-powered we start to look at things differently:
(a) if we put the ESP32 into deep_sleep … the ESP32 powers off, so the relay stops being held open, and the water stops.
(a) if the relay is held open for say 1 hour per day, the ESP32 + relay current drain adds up quickly.

Another use case is in the power module for the same project. When the Li_Po battery is charged to 4.2V I want to disconnect the solar panel from the Solar Power manager board to prevent over-charging. It is over charging because there is an excess of solar power so using a little to hold the relay open isn’t an issue.

But when the battery voltage is getting low I want to take action before the battery goes flat … At 3.4V I send notifications to Home Assistant, and at 3.2V I will put the ESP32 into extra long deep_sleep; but at 3.3V is there some other action I can take ? I was thinking to run out to the greenhouse (probably in the rain) and connect a powerbank to to the Power Manager’s USB-In connector to charge the main battery … but what if I already had a powerbank (or some other secondary battery) in place, and just needed to switch it on ? As it happens, my bench testing shows this will work with my older powerbank.
But once again, using power to hold the relay closed is power I would rather not be spending, especially if it is stopping my ESP32 from going back to deep_sleep.

Enter Latching Relay

A Latching Relay takes only a 10ms pulse to flip the switch, and the switch stays set until another pulse unsets it. There is no default closed position - COM is connected to A; or it is connected to B.

The disadvantage is that it requires 2 GPIO pins - one sends the pulse to set COM to A; and another to set COM to B.

I couldn’t find any ESPHome integration for a Latching Relay, but sending a pulse down a GPIO pin seems easy enough using switch: platform: GPIO to start the pulse and turning the pulse off after 10ms in the on_turn_on: section.

I have added a switch template to translate the latchswitch_a and latchswitch_b to an On/Off switch (assuming I have A connected to my load and B with no connection), and a global variable relayState to remember the current state of the switch through deep_sleep.

switch:

  # The latching relay uses two GPIO pins (GPIOs 48 & 21)
  # - turning on latchswitch_a sends the pulse to latch COM-A, and 
  # - turning on latchswitch_b sends the pulse to latch COM-B
  # but the latching Switch template below makes them look 
  # like an on/off switch, so the GPIO pins can be hidden (internal)
  - platform: gpio
    pin: 21
    id: latchswitch_a
    internal: True
    restore_mode: ALWAYS_OFF
    on_turn_on:
    - delay: 10ms
    - switch.turn_off: latchswitch_a
    - globals.set:
        id: relayState 
        value: 'true'
    interlock: [latchswitch_b]

  - platform: gpio
    pin: 47
    id: latchswitch_b
    internal: True
    restore_mode: ALWAYS_OFF
    on_turn_on:
    - delay: 10ms
    - switch.turn_off: latchswitch_b
    - lambda: id(relayState) = false;
    interlock: [latchswitch_a]

  # create a template to remember the state of the latching switch
  - platform: template
    name: $devicename Latching Switch
    restore_mode: DISABLED
    lambda: |-
      if ( id(relayState) ) {
        return true;
      } else {
        return false;
      }
    # COM-A is turned ON, and COM-B is OFF
    turn_on_action:
      - switch.turn_on: latchswitch_a
    turn_off_action:
      - switch.turn_on: latchswitch_b

globals:
  - id: relayState
    type: bool
    restore_value: yes

There is probably more elegant way to do this - please let me know.

Excellent post. Thank you Don!
It is also possible to programmatically set the run_duration (it was a guess!)

          - lambda: |-
              id(deep_sleep_1).set_run_duration(300000);
1 Like

Above I mentioned MQTT as a way for us to change ESPHome settings from a Home Assistant dashboard - most specifically to tell ESPHome to stop going into deep_sleep so we can send a firmware update.

MQTT definitely has an advantage when QoS level 2 is invoked because it keeps trying to send the message until it is confirmed as having been received … but MQTT also applies extra overhead to all the other sensor value updates. And MQTT and the native API don’t play so nicely together so you have to choose one or the other.

So … I have had a play with HA input helpers and ESPHome’s homeassistant platform to achieve the same effect (ie being able to change a setting on HA dashboard while ESP is asleep, and for the ESP to respond to it when it wakes up).

The key is to create input helpers in HA (so they are not unavailable while ESP is alseep). In HA use Setting > Devices & Services > Helpers tab and press the [+ Create helper] button. It seems to me that input_boolean, input_number, input_select and input_text are the most useful to pass data from HA to ESPHome.

For a simple switch to disable deep_sleep I used a HA input_boolean which displays on HA as a switch, and matches up perfectly with an ESPHome switch.

switch:
  - platform: homeassistant
    id: stay_awake 
    name: Stay awake toggle
    entity_id: input_boolean.greenhouse_stay_awake
    on_turn_on:
      - logger.log:
          format: "**********  stay_awake  switch turned ON  ! *****  stay_awake=%s, prevent deep_sleep "
          args: [ 'id(stay_awake).state ? "true" : "false"' ]
      - deep_sleep.prevent: deep_sleep_1
    on_turn_off:
      - logger.log:
          format: "**********  stay_awake  switch turned OFF ! *****  stay_awake=%s, allow deep_sleep "
          args: [ 'id(stay_awake).state ? "true" : "false"' ]
      - deep_sleep.allow: deep_sleep_1

Input_number displays as a sliding scale which is intuitive … but a little tricky to select when you have a large range, such as selecting deep_sleep period between 5 minutes and 720 minutes (12 hours). I changed to using an input_select which displays a dropdown box of selected values in HA panel, and passes the selected value to ESPHome as a text value.

text_sensor:
  # use a select list with 10 entries instead of 720 numbers
  - platform: homeassistant
    id: minutes_asleep
    entity_id: input_select.greenhouse_time_asleep
    on_raw_value:
      then:
        #  the HA input.select passes us the chosen value, as a string
        - lambda: |-
            auto minutes = atoi(x.c_str());
            id(global_sleep_duration) = minutes * 60000;        // minutes to milliseconds
            id(deep_sleep_1).set_sleep_duration( id(global_sleep_duration) );
        - logger.log: 
            format: "**********  greenhouse_minutes_asleep  on_raw_value  ****  was old minutes_asleep=%s,  new value x=%s, global_sleep_duration=%d"
            args: [ 'id(minutes_asleep).state.c_str()', 'x.c_str()', 'id(global_sleep_duration)' ]

When the value is received I convert it from a string to a temporary integer called minutes, then convert that to milliseconds which I save in a global variable and use to set_sleep_duration. I used the global variable because the wi-fi at my test bench had a lot of interference and so ESPHome native API was often unable to check the current value of the input_helper, and every on_boot would reset the deep_sleep to the default value. you can probably simplify the lambda to

            id(deep_sleep_1).set_sleep_duration( atoi(x.c_str()) * 60000 );

How it works

ESPHome components can be displayed on HA panels - but when the ESP32 is unavailable (read asleep) the value is unavailable, and the value cannot be changed because the ESP32 is not there to process the change.

But input_helpers are created and belong in Home Assistant, and hence they cannot become unavailable, and can be changed at any time.

When ESPHome boots up we have many setup operations performing, many in parallel.

  • ESPHome’s on_boot procedure starts the process of establishing the wi-fi connection to the router - but on_boot does not wait for it to complete.
  • ESPHome completes establishing the connection to the router. For debugging there are on_connect and on_disconnect actions - though you will need a serial connection to see them.
  • After wi-fi is connected, ESPHome will setup the native API and/or MQTT components. Again there are on_connect and on_disconnect if you need to debug these.
  • After the native API is connected, ESPHome requests from HA the current values of those components using the homeassistant platform, and processes them.

Note that it takes time to establish wi-fi and native API connections, and so processing of other local components may have taken place before the homeassistant components have received any value - which I found confusing.

1 Like

Great post. I’ve started playing around with ESPHome and temperature sensors. I’ve found that BMP280 temp sensors when encased with at WEMOS D1 Mini are affected by the CPU temp. A case redesign is probably in order, but considered deep sleep as one another option. Thanks for the write-up!

Not true !
A large part of my problems were due to unreliable wi-fi at my desk/workbench. Sometimes my ESP32 under test would not get to connect to HA during the entire awake period … resulting in some very inconsistent results.

Booting and Wi-Fi connection

The overview is that when the ESP32 starts up, ESPHome Core’s procedure starts initialising components for your selected network hardware (eg wi-fi, Ethernet, open thread) and network protocols (including Native API, MQTT, ESP-Now) - dependent on what components you have used in your YAML, and in your on_boot: YAML automation.

I’ll talk about Wi-Fi and native API but the same process applies to whichever you have selected.

Looking at the documentation for on_boot we see several priority levels allowing us to add custom initialisation code at different stages of ESPHome startup.

  • priority (Optional, float): The priority to execute your custom initialization code. A higher value means a high priority and thus also your code being executed earlier. Please note this is an ESPHome-internal value and any change will not be marked as a breaking change. Defaults to 600. Priorities (you can use any value between them too):
    • 800.0 : This is where all hardware initialization of vital components is executed. For example setting switches to their initial state.
    • 600.0 : This is where most sensors are set up.
    • 250.0 : At this priority, Wi-Fi is initialized.
    • 200.0 : Network connections like MQTT/native API are set up at this priority.
    • -100.0 : At this priority, pretty much everything should already be initialized.

The wording is rather ambiguous and I assumed that priority 200 happens after the Wi-Fi is running. Not true - at priority 250 the Wi-fi component was started in a separate thread but on_boot does not wait for it to finish before continuing to priority 200. What actually happens is that the Native API and/or MQTT have to wait until wi-fi is working, even if that is after the rest of on_boot has finished. In my case, sometimes it was still waiting for wi-fi when the ESP32 shutdown for deep_sleep.

What is worse is that on_boot defaults (if you don’t specify a priority) to priority 600, so your on_boot routine is running at the same time while ESPHome is starting to establish Wi-Fi and Native API. No wonder it looked as though things had been updated already before the OTA log started … because they had !

Can we know whether Wi-Fi and Native APIs have been established ?

Yes, within the Wi-Fi component configuration is an on_connect trigger, and elsewhere you can use Wi-Fi.connected condition. Similarly the Native API component has an on_client_connected trigger and an api.connected condition.

1 Like

Don, thank you for an excellent article. I am familiar with ESP32 deep sleep from an earlier project, prior to buying a Home Assistant. My question is about light sleep, but first some context…
I’m now recycling that old project with its super low power board (an EZSBC; not all boards are made equal) and the 2Ah Li battery which could go 3 months between charges when sampling frequently and posting via WiFi once every 6 hours.
I’m now coupling it to my solar diverter, well the low energy power sampling unit, and that then controls the immersion heater. My set up is a very variable energy tariff called Octopus Agile, which occasionally plummets or goes negative, so I’ll control the immersion in an intelligent ‘upcycling’ way, instead of ripping out the diverter and buying a 16A smart power switch.
So I want the ESP device to sleep, wake on the half hour, check if the diverter should be on and if so switch a gpio on or off, then go back to sleep if its off. The gpio is simply wired to enable the 3.3v regulated power via a transistor to power on the diverter power detection unit.
Really it should be a latching switch but I doubt it will be on too often, so…
I’m looking to use light sleep since I recall this was a way to reduce consumption whilst keeping the gpio pins set. Have you tried this and do you have any advice with this and ESPHome?

Wow Austin you’re assuming way more expertise than I have ! I haven’t been using ESPHome for long, and found ESPHome documentation to be very “by developers, for developers” - where the brief and cryptic explanation often only made sense after I have spent quite a bit of time and effort experimenting to work it out for myself.

I also found that there were many other areas which impact on deep_sleep, and which deep-sleep impacts on; and that new users could do with a more tutorial style. And as I have learnt more I have added to these notes.

As for your project … most of it is over my head :frowning_face: I haven’t even looked at light sleep … but I did swap to a latching relay in my greenhouse project when I realised that deep_sleep turns off a regular relay. The main difference is that the latching relay uses 2 GPIO pins - a pulse on one to turn the switch to A, and a pulse on the other to switch it to B - and other than those pulses it doesn’t require any power.

From other comments I have read (and hopefully remember correctly) wi-fi is a significant power use, but the wi-fi component allows you to enable wi-fi when you want to use it, do your communications in the on_connect trigger, and disable wi-fi when done.
I guess it comes down to a trade-off between power usage for light sleep and holding a standard relay active vs the cost of a latching relay and using deep sleep.

Beginning deep_sleep just after the pulse_counter is published will mean not using the deep-sleep run_duration.
I have been testing this, and present my example code.

I have created a number template awake_timer to count down the time before beginning deep_sleep. In on_boot it is set to 2 minutes, so that I default to take 2 samples (with update_interval 60 seconds) before going back to deep_sleep.

My rain_gauge uses a pulse_counter so that I don’t have to count each individual pulse. Every 60 seconds it publishes the number of pulses that it has counted. To minimise the chance of deep_sleep starting while there are pulses which have been counted but not published, I begin deep_sleep from the on_value: of the rain_gauge.

If there are pulses, it is raining, and so I reset the awake_time … actually I choose to leave the ESP32 awake for 5 minutes to make sure the rain really has stopped.

The GPIO pin which the pulse_counter is connected to can also be used to wake the ESP32. Make sure you have the same pin: parameters and allow_other_uses: true on both the rain_gauge and deep_sleep components. Note that the pulse which wakes the ESP32 - and any other pulses which happen before the ESP is sufficiently awake - will not be counted, and so I have moved it earlier in the on_boot priority. If you need better accuracy the suggestion is to use a hardware counter.

#####################################################################
#
# The ESP32-S3-mini board is CURRENTLY used for debugging parts of the greenhouse code
#
#####################################################################

substitutions:
  devicename:     "s3-test"                     # last octet of the IP Address
  deviceIP:       "123"                     # last octet of the IP Address
  wifi_ssid:      !secret upstairs_ssid     ### which wi-fi to connect to
  wifi_password:  !secret upstairs_password
  update_interval_sensor:  "1 min"        # How often to measure and report sensor values
  update_interval_network: "5 min"        # How often to measure and report network values
  rain_after_timeout:      5              # how many minutes to wait after rain stops
  awake_time_limit:        2              # how many minutes (rain_gauge cycles) before going back to sleep

esphome:
  name: $devicename
  platformio_options:
    board_build.flash_mode: dio
  on_boot:
    - priority: 600            # most sensors setup, but before wi-fi connected
      then:
        #  2 - ESP_SLEEP_WAKEUP_EXT0: Wakeup caused by external signal using RTC_IO
        - lambda: |-
            id(awake_timer).state = $awake_time_limit;        // set default time to be awake (in minutes)
            if ( int(esp_sleep_get_wakeup_cause()) == 2 ) {
                      // process the rain gauge pulse which woke us up asap so we don't miss more tips
              ESP_LOGD("", "   >>>> RAIN detected while asleep. <<<<" );
              id(rain_gauge).publish_state(1.0);
                      // hopefully the next tip will be recorded by the pulse_counter
              ESP_LOGD(" ", " ");
            };

  on_shutdown:
    - logger.log: "##### on_shutdown -100 finished. "
    - logger.log: "---------------------------------------------------------------------"
    - logger.log: "                                                                     "

sensor:
    # rain gauge hardware sends pulses as the bucket tips.
    # this rain_guage component returns the number of pulses in the preceeding update_interval (1 minute)
  - platform: pulse_counter
    name: Rain Gauge
    id:   rain_gauge
    pin:
      number: GPIO7
      allow_other_uses: true
      inverted: true
      mode:          # prevent lots of ON/OFF events
        input: true
        pullup: true
    update_interval: 60s      # check the rain_gauge every minute 
    on_value:
      then:
        - lambda: |-
            if ( x > 0.0 ) {
                      // YES, rain was detected !
              ESP_LOGD(" ", ">>>>>> NEW RAIN DETECTED counter started, x=%f pulses", x );
              id(deep_sleep_1).prevent_deep_sleep();            // don't go into deep_sleep, because that will stop the rain counting
              id(awake_timer).state = $rain_after_timeout;      // how many minutes to wait after rain stops
            } else if ( id(awake_timer).state < 0.5 ) {
                      // we have timed out, so go to sleep
              ESP_LOGD(" ", ">>>>>> RAIN TIMED OUT <<<<<<   Back to normal.  " );
                      // there was no rain this iteration, so doesn't matter if we sleep before the publish 
                      //    is sent to HA
              id(check_deep_sleep).execute();
            } else {
                      // one more iteration through the loop
              id(awake_timer).state-- ;
            };
        - logger.log: " "

number:
  - platform: template
      # This is actually a counter of minutes to stay awake - counting DOWN.
      #   By default it is set in on_boot:; but is reset when rain is detected:
      #   0 means it is time to go back to deep_sleep
    id: awake_timer
    optimistic: true
    min_value: 0
    max_value: 10
    step: 1

script:
  - id: check_deep_sleep
    then:
      # no outstanding reason to prevent deep_sleep, so... start deep_sleep 
      - logger.log: "<<<<< check-deep_sleep >>>>>  No active reasons to postpone deep_sleep, so activated"
      - deep_sleep.enter:
          id: deep_sleep_1
          sleep_duration: 5min

deep_sleep:
  id: deep_sleep_1
  # timings for deep_sleep moved to be driven by rain_gauge
  wakeup_pin:                # wake up if a rain gauge pulse is detected
    number: GPIO7
    allow_other_uses: true
    inverted: true
    mode:                    # prevent lots of ON/OFF events
      input: true
      pullup: false

esp32:
  board: esp32-s3-devkitm-1  # ESP32-S3-zero from Core Electronics
  variant: esp32s3
  framework:
    type: esp-idf
    version: recommended

logger:
  level: DEBUG      # VERBOSE

packages:
  common_wifi: !include _common_wifi.yaml      # uses the  deviceIP:  substitution
1 Like