[deep_sleep][logger][zephyr][zigbee] add deep sleep support with zigbee wakeup (#13950)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
tomaszduda23
2026-04-24 04:31:46 +02:00
committed by GitHub
parent 3ccaa771a7
commit 404620b99c
18 changed files with 196 additions and 20 deletions

View File

@@ -14,6 +14,7 @@ from esphome.components.esp32 import (
VARIANT_ESP32S3,
get_esp32_variant,
)
from esphome.components.zephyr import zephyr_add_prj_conf
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv
from esphome.const import (
@@ -33,6 +34,7 @@ from esphome.const import (
PLATFORM_BK72XX,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_NRF52,
PlatformFramework,
)
from esphome.core import CORE
@@ -304,7 +306,7 @@ CONFIG_SCHEMA = cv.All(
),
}
).extend(cv.COMPONENT_SCHEMA),
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX]),
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX, PLATFORM_NRF52]),
validate_config,
)
@@ -369,6 +371,8 @@ async def to_code(config):
if CONF_TOUCH_WAKEUP in config:
cg.add(var.set_touch_wakeup(config[CONF_TOUCH_WAKEUP]))
if CORE.using_zephyr and "zigbee" not in CORE.loaded_integrations:
zephyr_add_prj_conf("POWEROFF", True)
cg.add_define("USE_DEEP_SLEEP")

View File

@@ -59,6 +59,8 @@ void DeepSleepComponent::deep_sleep_() {
lt_deep_sleep_enter();
}
bool DeepSleepComponent::should_teardown_() { return true; }
} // namespace esphome::deep_sleep
#endif // USE_BK72XX

View File

@@ -9,11 +9,22 @@ static const char *const TAG = "deep_sleep";
// 5 seconds for deep sleep to ensure clean disconnect from Home Assistant
static const uint32_t TEARDOWN_TIMEOUT_DEEP_SLEEP_MS = 5000;
bool global_has_deep_sleep = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
bool global_has_deep_sleep = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
std::atomic<DeepSleepComponent *> global_deep_sleep; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void DeepSleepComponent::setup() {
#ifdef USE_ZEPHYR
k_sem_init(&this->wakeup_sem_, 0, 1);
#endif
global_has_deep_sleep = true;
this->schedule_sleep_();
// It can be used from another thread for waking up the device.
// It should be called as last item in setup.
global_deep_sleep.store(this);
}
void DeepSleepComponent::schedule_sleep_() {
this->next_enter_deep_sleep_ = false;
const optional<uint32_t> run_duration = get_run_duration_();
if (run_duration.has_value()) {
ESP_LOGI(TAG, "Scheduling in %" PRIu32 " ms", *run_duration);
@@ -58,13 +69,17 @@ void DeepSleepComponent::begin_sleep(bool manual) {
if (this->sleep_duration_.has_value()) {
ESP_LOGI(TAG, "Sleeping for %" PRId64 "us", *this->sleep_duration_);
}
App.run_safe_shutdown_hooks();
// It's critical to teardown components cleanly for deep sleep to ensure
// Home Assistant sees a clean disconnect instead of marking the device unavailable
App.teardown_components(TEARDOWN_TIMEOUT_DEEP_SLEEP_MS);
App.run_powerdown_hooks();
if (this->should_teardown_()) {
App.run_safe_shutdown_hooks();
// It's critical to teardown components cleanly for deep sleep to ensure
// Home Assistant sees a clean disconnect instead of marking the device unavailable
App.teardown_components(TEARDOWN_TIMEOUT_DEEP_SLEEP_MS);
App.run_powerdown_hooks();
}
this->deep_sleep_();
this->schedule_sleep_();
}
float DeepSleepComponent::get_setup_priority() const { return setup_priority::LATE; }

View File

@@ -4,6 +4,7 @@
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include <atomic>
#ifdef USE_ESP32
#include <esp_sleep.h>
@@ -14,6 +15,10 @@
#include "esphome/core/time.h"
#endif
#ifdef USE_ZEPHYR
#include <zephyr/kernel.h>
#endif
#include <cinttypes>
namespace esphome {
@@ -120,6 +125,9 @@ class DeepSleepComponent : public Component {
void prevent_deep_sleep();
void allow_deep_sleep();
#ifdef USE_ZEPHYR
void wakeup();
#endif
protected:
// Returns nullopt if no run duration is set. Otherwise, returns the run
@@ -129,6 +137,8 @@ class DeepSleepComponent : public Component {
void dump_config_platform_();
bool prepare_to_sleep_();
void deep_sleep_();
void schedule_sleep_();
bool should_teardown_();
#ifdef USE_BK72XX
bool pin_prevents_sleep_(WakeUpPinItem &pinItem) const;
@@ -157,6 +167,9 @@ class DeepSleepComponent : public Component {
optional<uint32_t> run_duration_;
bool next_enter_deep_sleep_{false};
bool prevent_{false};
#ifdef USE_ZEPHYR
k_sem wakeup_sem_;
#endif
};
extern bool global_has_deep_sleep; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@@ -243,5 +256,8 @@ template<typename... Ts> class AllowDeepSleepAction : public Action<Ts...>, publ
void play(const Ts &...x) override { this->parent_->allow_deep_sleep(); }
};
extern std::atomic<DeepSleepComponent *>
global_deep_sleep; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace deep_sleep
} // namespace esphome

View File

@@ -165,6 +165,8 @@ void DeepSleepComponent::deep_sleep_() {
esp_deep_sleep_start();
}
bool DeepSleepComponent::should_teardown_() { return true; }
} // namespace deep_sleep
} // namespace esphome
#endif // USE_ESP32

View File

@@ -18,6 +18,8 @@ void DeepSleepComponent::deep_sleep_() {
ESP.deepSleep(this->sleep_duration_.value_or(0)); // NOLINT(readability-static-accessed-through-instance)
}
bool DeepSleepComponent::should_teardown_() { return true; }
} // namespace deep_sleep
} // namespace esphome
#endif

View File

@@ -0,0 +1,60 @@
#include "deep_sleep_component.h"
#ifdef USE_ZEPHYR
#include "esphome/core/log.h"
#include <zephyr/sys/poweroff.h>
#include <zephyr/kernel.h>
#include <zephyr/stats/stats.h>
#include <zephyr/pm/pm.h>
namespace esphome::deep_sleep {
static const char *const TAG = "deep_sleep";
void DeepSleepComponent::wakeup() { k_sem_give(&this->wakeup_sem_); }
optional<uint32_t> DeepSleepComponent::get_run_duration_() const { return this->run_duration_; }
void DeepSleepComponent::dump_config_platform_() {}
bool DeepSleepComponent::prepare_to_sleep_() { return true; }
void DeepSleepComponent::deep_sleep_() {
k_timeout_t sleep_duration = K_FOREVER;
if (this->sleep_duration_.has_value()) {
sleep_duration = K_USEC(*this->sleep_duration_);
} else {
#ifndef USE_ZIGBEE
// the device can be woken up through one of the following signals:
// - The DETECT signal, optionally generated by the GPIO peripheral.
// - The ANADETECT signal, optionally generated by the LPCOMP module.
// - The SENSE signal, optionally generated by the NFC module to wake-on-field.
// - Detecting a valid USB voltage on the VBUS pin (VBUS,DETECT).
// - A reset.
//
// The system is reset when it wakes up from System OFF mode.
sys_poweroff();
#endif
}
// It might wake up immediately if k_sem_give was called again after wake up
int ret = k_sem_take(&this->wakeup_sem_, sleep_duration);
if (ret == 0) {
ESP_LOGD(TAG, "Woken up by another thread");
} else {
ESP_LOGD(TAG, "Timeout expired (normal sleep)");
}
}
bool DeepSleepComponent::should_teardown_() {
if (this->sleep_duration_.has_value()) {
return false;
}
#ifdef USE_ZIGBEE
return false;
#else
return true;
#endif
}
} // namespace esphome::deep_sleep
#endif

View File

@@ -472,14 +472,15 @@ async def _late_logger_init(config: ConfigType) -> None:
# esphome implement own fatal error handler which save PC/LR before reset
zephyr_add_prj_conf("RESET_ON_FATAL_ERROR", False)
zephyr_add_prj_conf("THREAD_LOCAL_STORAGE", True)
if config[CONF_HARDWARE_UART] == UART0:
zephyr_add_overlay("""&uart0 { status = "okay";};""")
if config[CONF_HARDWARE_UART] == UART1:
zephyr_add_overlay("""&uart1 { status = "okay";};""")
if config[CONF_HARDWARE_UART] == USB_CDC:
cg.add_define("USE_LOGGER_UART_SELECTION_USB_CDC")
zephyr_add_prj_conf("UART_LINE_CTRL", True)
zephyr_add_cdc_acm(config, 0)
if has_serial_logging:
if config[CONF_HARDWARE_UART] == UART0:
zephyr_add_overlay("""&uart0 { status = "okay";};""")
if config[CONF_HARDWARE_UART] == UART1:
zephyr_add_overlay("""&uart1 { status = "okay";};""")
if config[CONF_HARDWARE_UART] == USB_CDC:
cg.add_define("USE_LOGGER_UART_SELECTION_USB_CDC")
zephyr_add_prj_conf("UART_LINE_CTRL", True)
zephyr_add_cdc_acm(config, 0)
# Register at end for safe mode
await cg.register_component(log, config)

View File

@@ -65,10 +65,12 @@ void Logger::pre_setup() {
break;
#ifdef USE_LOGGER_USB_CDC
case UART_SELECTION_USB_CDC:
#ifdef CONFIG_USB_DEVICE_STACK
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(cdc_acm_uart0));
if (device_is_ready(uart_dev)) {
usb_enable(nullptr);
}
#endif
break;
#endif
}

View File

@@ -15,6 +15,7 @@ from .const import (
KEY_BOARD,
KEY_BOOTLOADER,
KEY_EXTRA_BUILD_FILES,
KEY_KCONFIG,
KEY_OVERLAY,
KEY_PM_STATIC,
KEY_PRJ_CONF,
@@ -54,6 +55,7 @@ class ZephyrData(TypedDict):
extra_build_files: dict[str, Path]
pm_static: list[Section]
user: dict[str, list[str]]
kconfig: str
def zephyr_set_core_data(config: ConfigType) -> None:
@@ -65,6 +67,7 @@ def zephyr_set_core_data(config: ConfigType) -> None:
extra_build_files={},
pm_static=[],
user={},
kconfig="",
)
@@ -185,8 +188,12 @@ def zephyr_add_cdc_acm(config: ConfigType, id: int) -> None:
)
def zephyr_add_pm_static(section: Section):
CORE.data[KEY_ZEPHYR][KEY_PM_STATIC].extend(section)
def zephyr_add_kconfig(kconfig: str) -> None:
zephyr_data()[KEY_KCONFIG] += textwrap.dedent(kconfig) + "\n"
def zephyr_add_pm_static(sections: list[Section]) -> None:
zephyr_data()[KEY_PM_STATIC].extend(sections)
def zephyr_add_user(key, value):
@@ -273,3 +280,18 @@ def copy_files():
write_file_if_changed(
CORE.relative_build_path("zephyr/pm_static.yml"), pm_static
)
kconfig = zephyr_data()[KEY_KCONFIG]
if kconfig:
kconfig = (
textwrap.dedent(
"""
menu "Zephyr"
source "Kconfig.zephyr"
endmenu
"""
)
+ "\n"
+ kconfig
)
write_file_if_changed(CORE.relative_build_path("zephyr/Kconfig"), kconfig)

View File

@@ -8,6 +8,7 @@ KEY_BOOTLOADER: Final = "bootloader"
KEY_EXTRA_BUILD_FILES: Final = "extra_build_files"
KEY_OVERLAY: Final = "overlay"
KEY_PM_STATIC: Final = "pm_static"
KEY_KCONFIG: Final = "kconfig"
KEY_PRJ_CONF: Final = "prj_conf"
KEY_ZEPHYR = "zephyr"
KEY_BOARD: Final = "board"

View File

@@ -32,6 +32,7 @@ from .const import (
from .const_zephyr import (
CONF_IEEE802154_VENDOR_OUI,
CONF_MAX_EP_NUMBER,
CONF_SLEEPY,
CONF_ZIGBEE_ID,
KEY_EP_NUMBER,
)
@@ -107,6 +108,9 @@ CONFIG_SCHEMA = cv.All(
),
cv.requires_component("nrf52"),
),
cv.OnlyWith(CONF_SLEEPY, "nrf52", default=False): cv.All(
cv.boolean,
),
}
).extend(cv.COMPONENT_SCHEMA),
zigbee_require_vfs_select,

View File

@@ -4,6 +4,7 @@ CONF_ZIGBEE_BINARY_SENSOR = "zigbee_binary_sensor"
CONF_ZIGBEE_SENSOR = "zigbee_sensor"
CONF_ZIGBEE_SWITCH = "zigbee_switch"
CONF_ZIGBEE_NUMBER = "zigbee_number"
CONF_SLEEPY = "sleepy"
CONF_IEEE802154_VENDOR_OUI = "ieee802154_vendor_oui"
# Keys for CORE.data storage

View File

@@ -4,6 +4,9 @@
#include <zephyr/settings/settings.h>
#include <zephyr/storage/flash_map.h>
#include "esphome/core/hal.h"
#ifdef USE_DEEP_SLEEP
#include "esphome/components/deep_sleep/deep_sleep_component.h"
#endif
extern "C" {
#include <zboss_api.h>
@@ -116,6 +119,12 @@ void ZigbeeComponent::zcl_device_cb(zb_bufid_t bufid) {
/* Set default response value. */
p_device_cb_param->status = RET_OK;
#ifdef USE_DEEP_SLEEP
if (auto *ds = deep_sleep::global_deep_sleep.load()) {
ds->wakeup();
}
#endif
// endpoints are enumerated from 1
if (global_zigbee->callbacks_.size() >= endpoint) {
const auto &cb = global_zigbee->callbacks_[endpoint - 1];
@@ -181,9 +190,11 @@ void ZigbeeComponent::setup() {
ESP_LOGE(TAG, "Cannot load settings, err: %d", err);
return;
}
zigbee_configure_sleepy_behavior(this->sleepy_);
zigbee_enable();
}
#ifdef ESPHOME_LOG_HAS_CONFIG
static const char *role() {
switch (zb_get_network_role()) {
case ZB_NWK_DEVICE_TYPE_COORDINATOR:
@@ -207,6 +218,7 @@ static const char *get_wipe_on_boot() {
return "NO";
#endif
}
#endif
void ZigbeeComponent::dump_config() {
char ieee_addr_buf[IEEE_ADDR_BUF_SIZE] = {0};
@@ -222,6 +234,7 @@ void ZigbeeComponent::dump_config() {
" Wipe on boot: %s\n"
" Device is joined to the network: %s\n"
" Sleep time: %us\n"
" RX ON when idle: %s\n"
" Current channel: %d\n"
" Current page: %d\n"
" Sleep threshold: %ums\n"
@@ -230,9 +243,9 @@ void ZigbeeComponent::dump_config() {
" Short addr: 0x%04X\n"
" Long pan id: 0x%s\n"
" Short pan id: 0x%04X",
get_wipe_on_boot(), YESNO(zb_zdo_joined()), this->sleep_time_, zb_get_current_channel(),
zb_get_current_page(), zb_get_sleep_threshold(), role(), ieee_addr_buf, zb_get_short_address(),
extended_pan_id_buf, zb_get_pan_id());
get_wipe_on_boot(), YESNO(zb_zdo_joined()), this->sleep_time_, YESNO(zb_get_rx_on_when_idle()),
zb_get_current_channel(), zb_get_current_page(), zb_get_sleep_threshold(), role(), ieee_addr_buf,
zb_get_short_address(), extended_pan_id_buf, zb_get_pan_id());
dump_reporting_();
}
@@ -302,6 +315,12 @@ void ZigbeeComponent::after_reporting_info(zb_zcl_configure_reporting_req_t *con
extern "C" {
void zboss_signal_handler(zb_uint8_t param) { esphome::zigbee::global_zigbee->zboss_signal_handler_esphome(param); }
void zb_osif_serial_put_bytes(const zb_uint8_t *buf, zb_short_t len) {
(void) buf;
(void) len;
}
void zb_osif_serial_flush() {}
void zb_osif_serial_init() {}
// NOLINTBEGIN(readability-identifier-naming,bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp)
extern zb_ret_t __real_zb_zcl_put_reporting_info_from_req(zb_zcl_configure_reporting_req_t *config_rep_req,

View File

@@ -81,6 +81,7 @@ class ZigbeeComponent : public Component {
Trigger<> *get_join_trigger() { return &this->join_trigger_; };
void force_report();
void loop() override;
void set_sleepy(bool sleepy) { this->sleepy_ = sleepy; }
protected:
static void zcl_device_cb(zb_bufid_t bufid);
@@ -95,6 +96,7 @@ class ZigbeeComponent : public Component {
bool force_report_{false};
uint32_t sleep_time_{};
uint32_t sleep_remainder_{};
bool sleepy_{};
};
class ZigbeeEntity {
@@ -107,5 +109,7 @@ class ZigbeeEntity {
ZigbeeComponent *parent_{nullptr};
};
extern ZigbeeComponent *global_zigbee; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace esphome::zigbee
#endif

View File

@@ -63,6 +63,7 @@ from .const import (
)
from .const_zephyr import (
CONF_IEEE802154_VENDOR_OUI,
CONF_SLEEPY,
CONF_ZIGBEE_BINARY_SENSOR,
CONF_ZIGBEE_ID,
CONF_ZIGBEE_NUMBER,
@@ -169,6 +170,11 @@ async def zephyr_to_code(config: ConfigType) -> None:
zephyr_add_prj_conf("NET_IP_ADDR_CHECK", False)
zephyr_add_prj_conf("NET_UDP", False)
# disable all extra to reduce power and save flash
zephyr_add_prj_conf("ZIGBEE_HAVE_SERIAL", False)
zephyr_add_prj_conf("ZBOSS_ERROR_PRINT_TO_LOG", False)
zephyr_add_prj_conf("DK_LIBRARY", False)
cg.add_build_flag("-Wl,--wrap=zb_zcl_put_reporting_info_from_req")
if CONF_IEEE802154_VENDOR_OUI in config:
@@ -200,6 +206,8 @@ async def zephyr_to_code(config: ConfigType) -> None:
CORE.add_job(_ctx_to_code, config)
cg.add(var.set_sleepy(config[CONF_SLEEPY]))
async def _attr_to_code(config: ConfigType) -> None:
# Create the basic attributes structure and attribute list

View File

@@ -0,0 +1,12 @@
deep_sleep:
run_duration: 10s
sleep_duration: 50s
<<: !include common.yaml
zigbee:
sensor:
- platform: template
name: "Temperature"
id: temperature_sensor

View File

@@ -4,3 +4,4 @@ zigbee:
wipe_on_boot: once
power_source: battery
ieee802154_vendor_oui: 0x231
sleepy: true