Three and a half new ways to deep sleep in ESPHome

I’ve been wanting to do some stuff with solar power and low energy and ESPHome. Thing is, either of those devices eats up quite a bit of current. Less than a Pi, but still enough power to matter if you are using solar power.

Furthermore, I think that ESPHome’s over-the-air software update system is amazing. It’s great to be sitting at my desktop pushing updates to the actual device without juggling cables and stuff.

If you get your ESP into deep sleep, pretty much everything is shut down and stopped and so the power usage is dramatically reduced.

This creates a problem. You can’t trigger an OTA update while the device is in deep sleep.

The only documentation is for how to use MQTT to disable deep sleep, so I decided to explore how to make this work. I ended up grabbing a bunch of help from oskar & ssieb from the ESPHome discord to figure this out.

A quick note about deep sleep on ESP8266 and ESP32 devices

If you want deep sleep on the ESP8266, you need to connect a cable from GPIO16 to the RST pin. On the ESP32, you don’t need to do this.

Deep sleep method 0: MQTT as per the docs

The manual describes how to use MQTT to accomplish this goal

Deep sleep method 1: Controlling deep-sleep by a switch

My first thought was that I’d like to just have a switch. If the switch is on, deep sleep is pervented.

Thus, I took a SPDT switch, connected one side to ground and the other side to 3.3v, and then connected that to pin #5:

schematic

The big thing that I discovered, other than butting my head against how to write actions, is that it’s a little tricky to have the state of your ESPHome device change while the device is sleeping. Remember, it costs resources to store the actual state of anything between resources.

Therefore, the on_state action doesn’t fire while the device is asleep and you need to use an on_boot event to check the button at startup.

esphome:
  name: solar
  platform: ESP32
  board: featheresp32
  on_boot:
    priority: -100
    then:
      - logger.log: "Checking sleep"
      - lambda: |-
          if (id(defeat).state) {
            ESP_LOGD("main", "Prevent sleep");
            id(deep_sleep_1).prevent_deep_sleep();
          } else {
            ESP_LOGD("main", "Allow sleep");
          }

wifi:
# Wifi details go here

deep_sleep:
  run_duration: 30s
  sleep_duration: 5s
  id: deep_sleep_1

binary_sensor:
  - platform: gpio
    name: "Defeat"
    id: "defeat"
    pin:
      number: 5
      mode: INPUT
    on_press:
      then:
        - logger.log: "Prevent deep sleep"
        - deep_sleep.prevent: deep_sleep_1

captive_portal:

# Enable logging
logger:

# Enable Home Assistant API
api:

ota:

This has a few quirks, which may or may not bother you:

  • It won’t prevent sleep until the device wakes up to process the message.
  • Once you disable sleep, it’s disabled until the device reboots, which means that the physical switch lies to you.

Deep sleep method 2: Controlling deep-sleep by a button

How about instead of a switch, since it’s going to always be a little bit weird, we use a button? While we’re at it, let’s use a feature of the ESP32 that lets you trigger the device to wake up.

This requires you to use one of the pins that is wakeup-enabled, so I’m using pin 4 instead of pin 5.

Instead of using a switch, let’s use a button connected between ground and pin #4. This way, you can configure the device to wake up the second you hit the button:

schematic

It’s easy to make the device wake up, but it still requires a trick from the previous method to work, for roughly the same reason. First it triggers the device to wake up, then when the device wakes up, it reads the state of the button and stays awake.

This means that you want to hold the button down for a few seconds when you wake it up.

esphome:
  name: solar
  platform: ESP32
  board: featheresp32
  on_boot:
    priority: -100
    then:
      - logger.log: "Checking sleep"
      - lambda: |-
          if (id(defeat).state) {
            ESP_LOGD("main", "Prevent sleep");
            id(deep_sleep_1).prevent_deep_sleep();
          } else {
            ESP_LOGD("main", "Allow sleep");
          }

wifi:
# Wifi details go here

deep_sleep:
  run_duration: 20s
  sleep_duration: 10s
  wakeup_pin:
    number: 4
    inverted: True
    mode: INPUT_PULLUP
  id: deep_sleep_1

binary_sensor:
  - platform: gpio
    name: "Defeat"
    id: "defeat"
    pin:
      number: 4
      mode: INPUT_PULLUP
      inverted: True
    on_press:
      then:
        - logger.log: "Prevent deep sleep"
        - deep_sleep.prevent: deep_sleep_1

captive_portal:

# Enable logging
logger:

# Enable Home Assistant API
api:

ota:

This is still pretty nice. It’s got one problem, which is that I can’t make it sticky for rapid iteration.

Deep sleep method 3: Adding remote sleep control via a switch control

I thought this was the best I could do, but you want to skip to the fourth method.

The problem here is that when you are sending commands to a device via Home Assistant, it mostly expects the device to be up. So, in this method, if you try to disable sleep while the device is sleeping, when it wakes up, the state won’t be changed.

What I’m going to do is take the previous method and then add a switch to the Home Assistant side of things so that, once you’ve woken it up, you can slide the switch over and keep it from deep sleeping.

This means that either you need to wait till the device appears and use the control in your Home Assistant… or just hit the button on the device and then use Home Assistant to make the change stick until you are done hacking.

esphome:
  name: solar
  platform: ESP32
  board: featheresp32
  on_boot:
    priority: -100
    then:
      - logger.log: "Checking sleep"
      - lambda: |-
          if (id(defeat).state) {
            ESP_LOGD("main", "Prevent sleep");
            id(deep_sleep_1).prevent_deep_sleep();
            id(defeat_sleep) = true;
          } else {
            ESP_LOGD("main", "Allow sleep");
          }
      - logger.log: "Sleep checked"

wifi:
# Wifi details go here

globals:
  - id: defeat_sleep
    type: bool
    initial_value: "false"

switch:
  - platform: restart
    name: "Remote restart"
  - platform: template
    restore_state: True
    name: "Remote sleep defeat"
    lambda: |-
        return id(defeat_sleep);
    turn_on_action:
      - logger.log: "Press defeat"
      - deep_sleep.prevent: deep_sleep_1
      - globals.set:
          id: defeat_sleep
          value: "true"
    turn_off_action:
      - logger.log: "Will be OK at next restart"
      - globals.set:
          id: defeat_sleep
          value: "false"

deep_sleep:
  run_duration: 20s
  sleep_duration: 10s
  wakeup_pin:
    number: 4
    inverted: True
    mode: INPUT_PULLUP
  id: deep_sleep_1

binary_sensor:
  - platform: gpio
    name: "Defeat"
    id: "defeat"
    internal: True
    pin:
      number: 4
      mode: INPUT_PULLUP
      inverted: True
    on_press:
      then:
        - logger.log: "press defeat"
        - deep_sleep.prevent: deep_sleep_1
        - globals.set:
            id: defeat_sleep
            value: "true"

captive_portal:

# Enable logging
logger:

# Enable Home Assistant API
api:

ota:

Deep sleep method 4: Adding remote sleep control via a input boolean in Home Assistant, and a bug.

This is the ultimate case. I’m preserving the button, for instant wake-up. But now I can set the variable as well.

What we’re doing here is inverting the direction of the variables. Before, we were using a switch, which requires Home Assistant to directly poke at the device. Now, we’re using a boolean variable, which the ESPHome device reads when it starts up.

Thus, first you want to create an input boolean inside of your Home Assistant configuration:

input_boolean:
  defeat_sleep:
    name: "Disable sleep on ESPHome"
    initial: false

Then you want to create binary sensor that fetches the value. Because of the eventing, you still need to do a remote poll, and it’s a little harder here because Home Assistant is connecting to the ESPHome device as a server.

This case works:

esphome:
  name: solar
  platform: ESP32
  board: featheresp32
  on_boot:
    priority: -200
    then:
      - delay: 10s
      - logger.log: "Checking sleep"
      - lambda: |-
          if (id(defeat).state) {
            ESP_LOGD("main", "Prevent sleep");
            id(deep_sleep_1).prevent_deep_sleep();
          } else {
            ESP_LOGD("main", "Allow sleep");
          }
          if(id(remote_defeat).state) {
            ESP_LOGD("main", "Remote prevent sleep");
            id(deep_sleep_1).prevent_deep_sleep();
          } else {
            ESP_LOGD("main", "Remote allow sleep");
          }
      - logger.log: "Sleep checked"

wifi:
# Wifi details go here

deep_sleep:
  run_duration: 20s
  sleep_duration: 10s
  wakeup_pin:
    number: 4
    inverted: True
    mode: INPUT_PULLUP
  id: deep_sleep_1

binary_sensor:
  - platform: homeassistant
    name: "Remote Defeat Sleep"
    internal: True
    id: "remote_defeat"
    entity_id: input_boolean.defeat_sleep
    on_press:
      then:
        - logger.log: "remote press defeat"
        - deep_sleep.prevent: deep_sleep_1
    on_state:
      then:
        - delay: 4s
        - logger.log: "Remote state state"

  - platform: gpio
    name: "Defeat"
    id: "defeat"
    internal: True
    pin:
      number: 4
      mode: INPUT_PULLUP
      inverted: True
    on_press:
      then:
        - logger.log: "press defeat"
        - deep_sleep.prevent: deep_sleep_1

captive_portal:

# Enable logging
logger:

# Enable Home Assistant API
api:

I don’t like the delay timeout here. It’s never a good thing to see.

Unfortunately, when I try to use a wait_until instead of a long delay, something like this:

esphome:
  name: solar
  platform: ESP32
  board: featheresp32
  on_boot:
    priority: -200
    then:
      - wait_until:
          api.connected:
      - logger.log: "Checking sleep"
      - lambda: |-
          if (id(defeat).state) {
            ESP_LOGD("main", "Prevent sleep");
            id(deep_sleep_1).prevent_deep_sleep();
          } else {
            ESP_LOGD("main", "Allow sleep");
          }
          if(id(remote_defeat).state) {
            ESP_LOGD("main", "Remote prevent sleep");
            id(deep_sleep_1).prevent_deep_sleep();
          } else {
            ESP_LOGD("main", "Remote allow sleep");
          }
      - logger.log: "Sleep checked"

It doesn’t trigger properly.

That’s probably a bug.

But at least I’ve given you three and a half new ways


Posted:

Updated: