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