[deep_sleep] Add on_wake automation triggers (#17569)

This commit is contained in:
Jesse Hills
2026-07-15 21:19:41 -04:00
committed by GitHub
parent dedca344f9
commit 3439a08b68
13 changed files with 247 additions and 9 deletions
+49 -7
View File
@@ -30,6 +30,7 @@ from esphome.const import (
CONF_SECOND, CONF_SECOND,
CONF_SLEEP_DURATION, CONF_SLEEP_DURATION,
CONF_TIME_ID, CONF_TIME_ID,
CONF_TRIGGER_ID,
CONF_WAKEUP_PIN, CONF_WAKEUP_PIN,
PLATFORM_BK72XX, PLATFORM_BK72XX,
PLATFORM_ESP32, PLATFORM_ESP32,
@@ -234,6 +235,15 @@ EXT1_WAKEUP_MODES = {
} }
WakeupCauseToRunDuration = deep_sleep_ns.struct("WakeupCauseToRunDuration") WakeupCauseToRunDuration = deep_sleep_ns.struct("WakeupCauseToRunDuration")
WakeupCause = deep_sleep_ns.enum("WakeupCause")
WakeTrigger = deep_sleep_ns.class_(
"WakeTrigger", automation.Trigger.template(WakeupCause), cg.Component
)
Ext1WakeTrigger = deep_sleep_ns.class_(
"Ext1WakeTrigger", automation.Trigger.template(), cg.Component
)
CONF_ON_WAKE = "on_wake"
CONF_WAKEUP_PIN_MODE = "wakeup_pin_mode" CONF_WAKEUP_PIN_MODE = "wakeup_pin_mode"
CONF_ESP32_EXT1_WAKEUP = "esp32_ext1_wakeup" CONF_ESP32_EXT1_WAKEUP = "esp32_ext1_wakeup"
CONF_TOUCH_WAKEUP = "touch_wakeup" CONF_TOUCH_WAKEUP = "touch_wakeup"
@@ -256,6 +266,22 @@ WAKEUP_PIN_SCHEMA = cv.Schema(
} }
) )
EXT1_WAKEUP_PIN_SCHEMA = cv.Schema(
{
cv.Required(CONF_PIN): cv.All(
pins.internal_gpio_input_pin_schema, validate_pin_number_esp32
),
cv.Optional(CONF_ON_WAKE): automation.validate_automation(
{cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(Ext1WakeTrigger)}
),
}
)
# Entries that are not in the {pin: ..., on_wake: ...} form are treated as a
# bare pin config (the original syntax, e.g. a plain "GPIO5" or {number: 5}).
validate_ext1_wakeup_pin = cv.maybe_simple_value(EXT1_WAKEUP_PIN_SCHEMA, key=CONF_PIN)
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
cv.Schema( cv.Schema(
{ {
@@ -282,8 +308,7 @@ CONFIG_SCHEMA = cv.All(
cv.Schema( cv.Schema(
{ {
cv.Required(CONF_PINS): cv.ensure_list( cv.Required(CONF_PINS): cv.ensure_list(
pins.internal_gpio_input_pin_schema, validate_ext1_wakeup_pin,
validate_pin_number_esp32,
), ),
cv.Required(CONF_MODE): cv.All( cv.Required(CONF_MODE): cv.All(
cv.enum(EXT1_WAKEUP_MODES, upper=True), cv.enum(EXT1_WAKEUP_MODES, upper=True),
@@ -292,6 +317,12 @@ CONFIG_SCHEMA = cv.All(
} }
), ),
), ),
cv.Optional(CONF_ON_WAKE): cv.All(
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX]),
automation.validate_automation(
{cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(WakeTrigger)}
),
),
cv.Optional(CONF_TOUCH_WAKEUP): cv.All( cv.Optional(CONF_TOUCH_WAKEUP): cv.All(
cv.only_on_esp32, cv.only_on_esp32,
esp32.only_on_variant( esp32.only_on_variant(
@@ -362,16 +393,27 @@ async def to_code(config):
) )
cg.add(var.set_run_duration(wakeup_cause_to_run_duration)) cg.add(var.set_run_duration(wakeup_cause_to_run_duration))
if CONF_ESP32_EXT1_WAKEUP in config: if (ext1_conf := config.get(CONF_ESP32_EXT1_WAKEUP)) is not None:
conf = config[CONF_ESP32_EXT1_WAKEUP]
mask = 0 mask = 0
for pin in conf[CONF_PINS]: for pin_conf in ext1_conf[CONF_PINS]:
mask |= 1 << pin[CONF_NUMBER] number = pin_conf[CONF_PIN][CONF_NUMBER]
mask |= 1 << number
for wake_conf in pin_conf.get(CONF_ON_WAKE, []):
trigger = cg.new_Pvariable(wake_conf[CONF_TRIGGER_ID], number)
await cg.register_component(trigger, wake_conf)
await automation.build_automation(trigger, [], wake_conf)
cg.add_define("USE_DEEP_SLEEP_ON_WAKE")
struct = cg.StructInitializer( struct = cg.StructInitializer(
Ext1Wakeup, ("mask", mask), ("wakeup_mode", conf[CONF_MODE]) Ext1Wakeup, ("mask", mask), ("wakeup_mode", ext1_conf[CONF_MODE])
) )
cg.add(var.set_ext1_wakeup(struct)) cg.add(var.set_ext1_wakeup(struct))
for wake_conf in config.get(CONF_ON_WAKE, []):
trigger = cg.new_Pvariable(wake_conf[CONF_TRIGGER_ID])
await cg.register_component(trigger, wake_conf)
await automation.build_automation(trigger, [(WakeupCause, "cause")], wake_conf)
cg.add_define("USE_DEEP_SLEEP_ON_WAKE")
if CONF_TOUCH_WAKEUP in config: if CONF_TOUCH_WAKEUP in config:
cg.add(var.set_touch_wakeup(config[CONF_TOUCH_WAKEUP])) cg.add(var.set_touch_wakeup(config[CONF_TOUCH_WAKEUP]))
if CORE.using_zephyr and "zigbee" not in CORE.loaded_integrations: if CORE.using_zephyr and "zigbee" not in CORE.loaded_integrations:
@@ -7,6 +7,21 @@ namespace esphome::deep_sleep {
static const char *const TAG = "deep_sleep.bk72xx"; static const char *const TAG = "deep_sleep.bk72xx";
#ifdef USE_DEEP_SLEEP_ON_WAKE
WakeupCause get_wakeup_cause() {
switch (lt_get_reboot_reason()) {
case REBOOT_REASON_SLEEP_GPIO:
return WAKEUP_CAUSE_GPIO;
case REBOOT_REASON_SLEEP_RTC:
return WAKEUP_CAUSE_TIMER;
case REBOOT_REASON_SLEEP_USB:
return WAKEUP_CAUSE_UNKNOWN;
default:
return WAKEUP_CAUSE_NONE;
}
}
#endif // USE_DEEP_SLEEP_ON_WAKE
optional<uint32_t> DeepSleepComponent::get_run_duration_() const { return this->run_duration_; } optional<uint32_t> DeepSleepComponent::get_run_duration_() const { return this->run_duration_; }
void DeepSleepComponent::dump_config_platform_() { void DeepSleepComponent::dump_config_platform_() {
@@ -60,6 +60,65 @@ struct WakeupCauseToRunDuration {
#endif // USE_ESP32 #endif // USE_ESP32
#ifdef USE_DEEP_SLEEP_ON_WAKE
/// Why the device woke from deep sleep. Passed to on_wake automations.
enum WakeupCause : uint8_t {
/// The device did not wake from deep sleep (for example a cold boot, reset or OTA restart).
WAKEUP_CAUSE_NONE = 0,
/// The device woke from deep sleep, but the source could not be identified.
WAKEUP_CAUSE_UNKNOWN,
/// The device was woken by the sleep timer.
WAKEUP_CAUSE_TIMER,
/// The device was woken by a GPIO pin (wakeup_pin or esp32_ext1_wakeup).
WAKEUP_CAUSE_GPIO,
/// The device was woken by a touch pad.
WAKEUP_CAUSE_TOUCH,
};
/// Return why the device woke from deep sleep. Implemented per platform.
WakeupCause get_wakeup_cause();
/** Setup priority of on_wake triggers.
*
* Between restoring global variables (setup_priority::HARDWARE, 800) and on_boot automations at
* their default priority (600), so on_wake automations can update state (e.g. globals) that
* on_boot automations then use.
*/
inline constexpr float ON_WAKE_TRIGGER_SETUP_PRIORITY = 700.0f;
/// Fires once on boot when the device woke from deep sleep, with the wakeup cause.
class WakeTrigger : public Trigger<WakeupCause>, public Component {
public:
void setup() override {
const WakeupCause cause = get_wakeup_cause();
if (cause != WAKEUP_CAUSE_NONE) {
this->trigger(cause);
}
}
float get_setup_priority() const override { return ON_WAKE_TRIGGER_SETUP_PRIORITY; }
};
#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3)
/// Fires once on boot when the device was woken from deep sleep by the given ext1 pin.
class Ext1WakeTrigger : public Trigger<>, public Component {
public:
explicit Ext1WakeTrigger(uint8_t pin) : pin_(pin) {}
void setup() override {
if (esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_EXT1 &&
(esp_sleep_get_ext1_wakeup_status() & (1ULL << this->pin_))) {
this->trigger();
}
}
float get_setup_priority() const override { return ON_WAKE_TRIGGER_SETUP_PRIORITY; }
protected:
uint8_t pin_;
};
#endif
#endif // USE_DEEP_SLEEP_ON_WAKE
template<typename... Ts> class EnterDeepSleepAction; template<typename... Ts> class EnterDeepSleepAction;
template<typename... Ts> class PreventDeepSleepAction; template<typename... Ts> class PreventDeepSleepAction;
@@ -30,6 +30,25 @@ namespace esphome::deep_sleep {
static const char *const TAG = "deep_sleep"; static const char *const TAG = "deep_sleep";
#ifdef USE_DEEP_SLEEP_ON_WAKE
WakeupCause get_wakeup_cause() {
switch (esp_sleep_get_wakeup_cause()) {
case ESP_SLEEP_WAKEUP_EXT0:
case ESP_SLEEP_WAKEUP_EXT1:
case ESP_SLEEP_WAKEUP_GPIO:
return WAKEUP_CAUSE_GPIO;
case ESP_SLEEP_WAKEUP_TIMER:
return WAKEUP_CAUSE_TIMER;
case ESP_SLEEP_WAKEUP_TOUCHPAD:
return WAKEUP_CAUSE_TOUCH;
case ESP_SLEEP_WAKEUP_UNDEFINED:
return WAKEUP_CAUSE_NONE;
default:
return WAKEUP_CAUSE_UNKNOWN;
}
}
#endif // USE_DEEP_SLEEP_ON_WAKE
optional<uint32_t> DeepSleepComponent::get_run_duration_() const { optional<uint32_t> DeepSleepComponent::get_run_duration_() const {
if (this->wakeup_cause_to_run_duration_.has_value()) { if (this->wakeup_cause_to_run_duration_.has_value()) {
esp_sleep_wakeup_cause_t wakeup_cause = esp_sleep_get_wakeup_cause(); esp_sleep_wakeup_cause_t wakeup_cause = esp_sleep_get_wakeup_cause();
@@ -3,10 +3,27 @@
#include <Esp.h> #include <Esp.h>
#ifdef USE_DEEP_SLEEP_ON_WAKE
extern "C" {
#include <user_interface.h>
}
#endif
namespace esphome::deep_sleep { namespace esphome::deep_sleep {
static const char *const TAG = "deep_sleep"; static const char *const TAG = "deep_sleep";
#ifdef USE_DEEP_SLEEP_ON_WAKE
WakeupCause get_wakeup_cause() {
// The ESP8266 can only wake from deep sleep through the RTC timer (via GPIO16 -> RST).
// NOLINTNEXTLINE(readability-static-accessed-through-instance)
if (ESP.getResetInfoPtr()->reason == REASON_DEEP_SLEEP_AWAKE) {
return WAKEUP_CAUSE_TIMER;
}
return WAKEUP_CAUSE_NONE;
}
#endif // USE_DEEP_SLEEP_ON_WAKE
optional<uint32_t> DeepSleepComponent::get_run_duration_() const { return this->run_duration_; } optional<uint32_t> DeepSleepComponent::get_run_duration_() const { return this->run_duration_; }
void DeepSleepComponent::dump_config_platform_() {} void DeepSleepComponent::dump_config_platform_() {}
+1
View File
@@ -56,6 +56,7 @@
#define USE_DATETIME_TIME #define USE_DATETIME_TIME
#define USE_DEBUG #define USE_DEBUG
#define USE_DEEP_SLEEP #define USE_DEEP_SLEEP
#define USE_DEEP_SLEEP_ON_WAKE
#define USE_DEVICES #define USE_DEVICES
#define USE_DISPLAY #define USE_DISPLAY
#define USE_ENTITY_DEVICE_CLASS #define USE_ENTITY_DEVICE_CLASS
@@ -33,6 +33,43 @@ def test_deep_sleep_run_duration_simple(generate_main):
assert "deepsleep->set_run_duration(10000);" in main_cpp assert "deepsleep->set_run_duration(10000);" in main_cpp
def test_deep_sleep_on_wake_trigger(generate_main):
"""
When deep sleep is configured with a component-level on_wake automation,
a WakeTrigger component should be registered with the wakeup cause as
the automation argument.
"""
main_cpp = generate_main("tests/component_tests/deep_sleep/test_deep_sleep3.yaml")
assert "deep_sleep::WakeTrigger();" in main_cpp
assert "Automation<deep_sleep::WakeupCause>" in main_cpp
def test_deep_sleep_ext1_on_wake_triggers(generate_main):
"""
Each esp32_ext1_wakeup pin with an on_wake automation should get its own
Ext1WakeTrigger with the pin number, and all pins (including the legacy
bare-pin shorthand) should contribute to the ext1 wakeup mask.
"""
main_cpp = generate_main("tests/component_tests/deep_sleep/test_deep_sleep3.yaml")
assert "deep_sleep::Ext1WakeTrigger(2);" in main_cpp
assert "deep_sleep::Ext1WakeTrigger(4);" in main_cpp
# GPIO13 has no on_wake, so no trigger is created for it
assert "deep_sleep::Ext1WakeTrigger(13)" not in main_cpp
# mask covers GPIO2, GPIO4 and GPIO13
assert ".mask = 8212," in main_cpp
def test_deep_sleep_no_on_wake_no_triggers(generate_main):
"""
Without any on_wake automations, no wake trigger code should be generated.
"""
main_cpp = generate_main("tests/component_tests/deep_sleep/test_deep_sleep1.yaml")
assert "WakeTrigger" not in main_cpp
def test_deep_sleep_run_duration_dictionary(generate_main): def test_deep_sleep_run_duration_dictionary(generate_main):
""" """
When deep sleep is configured with dictionary run duration, it should be set. When deep sleep is configured with dictionary run duration, it should be set.
@@ -0,0 +1,23 @@
esphome:
name: test
esp32:
board: nodemcu-32s
deep_sleep:
id: deepsleep
sleep_duration: 1min
run_duration: 10s
on_wake:
- lambda: 'ESP_LOGD("test", "cause %d", static_cast<int>(cause));'
esp32_ext1_wakeup:
mode: ANY_HIGH
pins:
- pin: GPIO2
on_wake:
- lambda: 'ESP_LOGD("test", "left");'
- pin:
number: GPIO4
on_wake:
- lambda: 'ESP_LOGD("test", "right");'
- number: GPIO13
@@ -6,9 +6,15 @@ deep_sleep:
sleep_duration: 50s sleep_duration: 50s
wakeup_pin: ${wakeup_pin} wakeup_pin: ${wakeup_pin}
wakeup_pin_mode: INVERT_WAKEUP wakeup_pin_mode: INVERT_WAKEUP
on_wake:
- logger.log:
format: "Woke up, cause %d"
args: ["static_cast<int>(cause)"]
esp32_ext1_wakeup: esp32_ext1_wakeup:
pins: pins:
- number: GPIO2 - pin: GPIO2
on_wake:
- logger.log: Woken by ext1 pin GPIO2
- number: GPIO13 - number: GPIO13
mode: ANY_HIGH mode: ANY_HIGH
touch_wakeup: true touch_wakeup: true
@@ -5,8 +5,15 @@ deep_sleep:
sleep_duration: 50s sleep_duration: 50s
wakeup_pin: ${wakeup_pin} wakeup_pin: ${wakeup_pin}
wakeup_pin_mode: INVERT_WAKEUP wakeup_pin_mode: INVERT_WAKEUP
on_wake:
- logger.log:
format: "Woke up, cause %d"
args: ["static_cast<int>(cause)"]
esp32_ext1_wakeup: esp32_ext1_wakeup:
pins: pins:
- number: GPIO2 - pin:
number: GPIO2
on_wake:
- logger.log: Woken by ext1 pin GPIO2
- number: GPIO5 - number: GPIO5
mode: ANY_HIGH mode: ANY_HIGH
@@ -5,3 +5,7 @@ deep_sleep:
sleep_duration: 50s sleep_duration: 50s
wakeup_pin: ${wakeup_pin} wakeup_pin: ${wakeup_pin}
wakeup_pin_mode: INVERT_WAKEUP wakeup_pin_mode: INVERT_WAKEUP
on_wake:
- logger.log:
format: "Woke up, cause %d"
args: ["static_cast<int>(cause)"]
@@ -1,6 +1,10 @@
deep_sleep: deep_sleep:
run_duration: 30s run_duration: 30s
sleep_duration: 12h sleep_duration: 12h
on_wake:
- logger.log:
format: "Woke up, cause %d"
args: ["static_cast<int>(cause)"]
wakeup_pin: wakeup_pin:
- pin: - pin:
number: P6 number: P6
@@ -1,5 +1,9 @@
deep_sleep: deep_sleep:
run_duration: 10s run_duration: 10s
sleep_duration: 50s sleep_duration: 50s
on_wake:
- logger.log:
format: "Woke up, cause %d"
args: ["static_cast<int>(cause)"]
<<: !include common.yaml <<: !include common.yaml