Notes on ESPHome deep_sleep

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 works, and has advantages, particularly 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). I now recommend the Native API over MQTT for people not already using MQTT.

The key is to create Input Helpers in HA - they are still available to be changed while ESP32 is alseep, and the related ESP32 controls will automatically be updated when the ESP32 next reboots. 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, in Home Assistant I created a helper input_boolean


This creates an HA switch entity which is easy to add to a Lovelace Entities card

and in ESPHome it matches up with an ESPHome switch with homeassistant platform.

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 in HA as a sliding scale which is intuitive ... but a little tricky to select when you have a large range.

In my case I want to select deep_sleep periods between 5 minutes (for testing) and 720 minutes (12 hours if the battery gets especially low). I created a "Dropdown" helper


This creates an HA entity input_select.greenhouse_time_asleep, which is easily added to the Lovelace Entities card. The Home Assistant Dropdown contains a list of text options, and passes the selected text value to ESPHome.

The minor complication here is that the value is received into ESPHome as a Homeassistant text_sensor, but I want to use it as a number, so I add some processing in the on_raw_value: automation.

text_sensor:
  # use a select list with 10 entries instead of 720 numbers
  - platform: homeassistant
    id: minutes_asleep
    entity_id: input_select.greenhouse_minutes_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)' ]

In the lambda, the C++ atoi() function converts the raw text value (in variable "x") to an integer, and saves it in variable "minutes"; then goes on to convert the minutes 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

Regular ESPHome components can be displayed on HA panels - but when the ESP32 is unavailable (ie 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.