[power_management] Add the power_management domain with an esp32_power_management platform (#12325)

Co-authored-by: Kevin Ahrendt <kahrendt@gmail.com>
Co-authored-by: Kevin Ahrendt <kevin.ahrendt@openhomefoundation.org>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
rwrozelle
2026-10-12 11:23:55 -10:00
committed by GitHub
co-authored by Kevin Ahrendt Kevin Ahrendt Claude Sonnet 4.6 J. Nick Koston J. Nick Koston
parent 6bf091104f
commit 1c8808339a
14 changed files with 783 additions and 0 deletions
+2
View File
@@ -184,6 +184,7 @@ esphome/components/esp32_camera_web_server/* @ayufan
esphome/components/esp32_can/* @Sympatron
esphome/components/esp32_hosted/* @swoboda1337
esphome/components/esp32_hosted/update/* @swoboda1337
esphome/components/esp32_power_management/* @rwrozelle
esphome/components/esp32_rmt/* @jesserockz
esphome/components/esp32_rmt_led_strip/* @jesserockz
esphome/components/esp8266/* @esphome/core
@@ -436,6 +437,7 @@ esphome/components/pn7160/* @jesserockz @kbx81
esphome/components/pn7160_i2c/* @jesserockz @kbx81
esphome/components/pn7160_spi/* @jesserockz @kbx81
esphome/components/pn71xx/* @jesserockz @kbx81
esphome/components/power_management/* @rwrozelle
esphome/components/power_supply/* @esphome/core
esphome/components/preferences/* @esphome/core
esphome/components/provisioning/* @esphome/core
+4
View File
@@ -164,6 +164,10 @@ void ESP32InternalGPIOPin::setup() {
conf.pull_down_en = this->flags_ & gpio::FLAG_PULLDOWN ? GPIO_PULLDOWN_ENABLE : GPIO_PULLDOWN_DISABLE;
conf.intr_type = GPIO_INTR_DISABLE;
gpio_config(&conf);
#ifdef CONFIG_FREERTOS_USE_TICKLESS_IDLE
// If auto light sleep is used gpio sleep mode needs to be disabled for the pin
gpio_sleep_sel_dis(this->get_pin_num());
#endif
if (this->flags_ & gpio::FLAG_OUTPUT) {
gpio_set_drive_capability(this->get_pin_num(), this->get_drive_strength());
}
@@ -0,0 +1 @@
DOMAIN = "esp32_power_management"
@@ -0,0 +1,75 @@
#include "esp32_power_management.h"
#ifdef USE_ESP32
#include "esphome/core/log.h"
#include "esp_private/esp_clk.h"
#include "soc/rtc.h"
namespace esphome::esp32_power_management {
ESPHOME_LOG_TAG(TAG, "esp32_power_management");
void ESP32PowerManagement::setup() {
if (this->max_freq_mhz_ == 0)
this->max_freq_mhz_ = CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ;
if (this->min_freq_mhz_ == 0)
this->min_freq_mhz_ = esp_clk_xtal_freq() / MHZ;
// A low boot frequency (ESP32-H2 at 16 MHz) can sit below the crystal frequency.
if (this->min_freq_mhz_ > this->max_freq_mhz_)
this->min_freq_mhz_ = this->max_freq_mhz_;
esp_pm_config_t pm_config = {
.max_freq_mhz = this->max_freq_mhz_,
.min_freq_mhz = this->min_freq_mhz_,
#if CONFIG_FREERTOS_USE_TICKLESS_IDLE
.light_sleep_enable = true,
#endif
};
esp_err_t rc = esp_pm_configure(&pm_config);
if (rc != ESP_OK) {
// The usual cause is a minimum frequency that does not divide this board's crystal
ESP_LOGE(TAG, "esp_pm_configure failed: %s (crystal %u MHz)", esp_err_to_name(rc),
static_cast<unsigned>(esp_clk_xtal_freq() / MHZ));
this->mark_failed();
}
}
void ESP32PowerManagement::dump_config() {
ESP_LOGCONFIG(TAG,
"Power Management:\n"
" Max Frequency: %uMHZ\n"
" Min Frequency: %uMHZ",
this->max_freq_mhz_, this->min_freq_mhz_);
#if SOC_CLK_XTAL32K_SUPPORTED
ESP_LOGCONFIG(TAG, " 32k XTAL: %s",
rtc_clk_slow_src_get() == SOC_RTC_SLOW_CLK_SRC_XTAL32K ? LOG_STR_LITERAL("in use")
: LOG_STR_LITERAL("not in use"));
#endif
#if CONFIG_FREERTOS_USE_TICKLESS_IDLE
ESP_LOGCONFIG(TAG, " Light Sleep Enabled");
#if CONFIG_ESP_SLEEP_POWER_DOWN_FLASH
ESP_LOGCONFIG(TAG, " PM Flash Power Down in Light Sleep Enabled");
#endif
#if CONFIG_PM_POWER_DOWN_PERIPHERAL_IN_LIGHT_SLEEP
ESP_LOGCONFIG(TAG, " PM Peripheral Power Down in Light Sleep Enabled");
#endif
#if CONFIG_IEEE802154_SLEEP_ENABLE
ESP_LOGCONFIG(TAG, " ieee802154 Sleep Enabled");
#endif
#if CONFIG_PM_PROFILING
ESP_LOGCONFIG(TAG, " PM Profiling Enabled");
#endif
#if CONFIG_PM_TRACE
ESP_LOGCONFIG(TAG, " PM Trace Enabled");
#endif
#if defined(CONFIG_LWIP_IPV6) && !defined(CONFIG_LWIP_ND6)
ESP_LOGCONFIG(TAG, " IPv6 Neighbor Discovery Disabled (CONFIG_LWIP_ND6)");
#endif
#if CONFIG_USJ_NO_AUTO_LS_ON_CONNECTION
ESP_LOGCONFIG(TAG, " USB Serial/JTAG Light Sleep Prevention Enabled");
#endif
#endif
}
} // namespace esphome::esp32_power_management
#endif
@@ -0,0 +1,28 @@
#pragma once
#ifdef USE_ESP32
#include "esphome/core/component.h"
#include "esphome/components/power_management/power_management.h"
// Lets a YAML lambda call esp_pm_dump_locks().
#include "esp_pm.h"
namespace esphome::esp32_power_management {
class ESP32PowerManagement : public power_management::PowerManagementComponent {
public:
float get_setup_priority() const override { return setup_priority::POWER; }
void setup() override;
void dump_config() override;
void set_max_freq_mhz(uint16_t max_freq_mhz) { this->max_freq_mhz_ = max_freq_mhz; }
void set_min_freq_mhz(uint16_t min_freq_mhz) { this->min_freq_mhz_ = min_freq_mhz; }
protected:
// Zero means the build default; setup() replaces it with the value applied.
uint16_t max_freq_mhz_{0};
uint16_t min_freq_mhz_{0};
};
} // namespace esphome::esp32_power_management
#endif
@@ -0,0 +1,254 @@
import logging
import esphome.codegen as cg
from esphome.components import esp32, power_management
from esphome.components.esp32 import (
VARIANT_ESP32,
VARIANT_ESP32C2,
VARIANT_ESP32C3,
VARIANT_ESP32C5,
VARIANT_ESP32C6,
VARIANT_ESP32C61,
VARIANT_ESP32H2,
VARIANT_ESP32H4,
VARIANT_ESP32H21,
VARIANT_ESP32P4,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
VARIANT_ESP32S31,
add_idf_sdkconfig_option,
get_esp32_variant,
)
from esphome.components.openthread.const import CONF_POLL_PERIOD
from esphome.components.psram import DOMAIN as PSRAM_DOMAIN
from esphome.components.zigbee import DOMAIN as ZIGBEE_DOMAIN
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_OPENTHREAD, CONF_PLATFORM
from esphome.core import CORE
import esphome.final_validate as fv
from esphome.types import ConfigType
from . import DOMAIN
_LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@rwrozelle"]
DEPENDENCIES = ["esp32"]
CONF_ENABLE_LIGHT_SLEEP = "enable_light_sleep"
CONF_IDLE_TIME_BEFORE_SLEEP = "idle_time_before_sleep"
CONF_MAX_FREQUENCY = "max_frequency"
CONF_MIN_FREQUENCY = "min_frequency"
CONF_POWER_DOWN_FLASH = "power_down_flash"
CONF_POWER_DOWN_PERIPHERALS = "power_down_peripherals"
CONF_PROFILING = "profiling"
CONF_TRACE = "trace"
# Variants with a TOP power domain, which can power down peripherals in light sleep.
_TOP_PD_VARIANTS = [
VARIANT_ESP32C5,
VARIANT_ESP32C6,
VARIANT_ESP32C61,
VARIANT_ESP32H2,
VARIANT_ESP32H21,
VARIANT_ESP32H4,
VARIANT_ESP32P4,
VARIANT_ESP32S31,
]
# Crystal frequencies each variant can be built for (ESP-IDF's SOC_XTAL_FREQ_* values).
# Below the PLL frequencies the CPU runs from the crystal through an integer divider.
_XTAL_MHZ = {
VARIANT_ESP32: (24, 26, 40),
VARIANT_ESP32C2: (26, 32, 40),
VARIANT_ESP32C3: (32, 40),
VARIANT_ESP32C5: (40, 48),
VARIANT_ESP32C6: (40,),
VARIANT_ESP32C61: (40,),
VARIANT_ESP32H2: (32,),
VARIANT_ESP32H21: (32,),
VARIANT_ESP32H4: (32,),
VARIANT_ESP32P4: (40,),
VARIANT_ESP32S2: (40,),
VARIANT_ESP32S3: (32, 40),
VARIANT_ESP32S31: (40,),
}
# esp_pm_configure() keeps the 1 MHz reference clock reachable: its divider must be
# at least 10 on the ESP32 and 2 on every other variant.
_MIN_FREQUENCY_FLOOR_MHZ = {VARIANT_ESP32: 10}
esp32_power_management_ns = cg.esphome_ns.namespace("esp32_power_management")
PowerManagement = esp32_power_management_ns.class_(
"ESP32PowerManagement", power_management.PowerManagementComponent
)
def _mhz(value: object) -> int:
"""Validate a frequency and keep it as whole megahertz."""
return int(cv.frequency(value)) // 1_000_000
def valid_min_frequencies(variant: str) -> list[int]:
"""The minimum CPU frequencies esp_pm_configure() accepts on a variant, in MHz.
A value must divide one of the variant's crystal frequencies, stay above the
reference clock floor, or be one of the PLL frequencies.
"""
floor = _MIN_FREQUENCY_FLOOR_MHZ.get(variant, 2)
mhz = {
xtal // divider
for xtal in _XTAL_MHZ[variant]
for divider in range(1, xtal // floor + 1)
if xtal % divider == 0
}
mhz.update(int(freq[:-3]) for freq in esp32.CPU_FREQUENCIES[variant])
return sorted(mhz)
def _validate(config: ConfigType) -> ConfigType:
variant = get_esp32_variant()
if (max_mhz := config.get(CONF_MAX_FREQUENCY)) is not None:
valid = esp32.CPU_FREQUENCIES[variant]
if f"{max_mhz}MHZ" not in valid:
raise cv.Invalid(
f"{max_mhz}MHZ is not a valid CPU frequency for {variant}. "
f"Valid options are: {', '.join(valid)}",
path=[CONF_MAX_FREQUENCY],
)
if (min_mhz := config.get(CONF_MIN_FREQUENCY)) is not None and (
min_mhz not in (valid_min := valid_min_frequencies(variant))
):
raise cv.Invalid(
f"{min_mhz}MHZ is not a valid minimum frequency for {variant}; it must "
f"divide the crystal frequency. Valid options are: "
f"{', '.join(f'{mhz}MHZ' for mhz in valid_min)}",
path=[CONF_MIN_FREQUENCY],
)
if not config[CONF_ENABLE_LIGHT_SLEEP]:
for key in (CONF_POWER_DOWN_PERIPHERALS, CONF_POWER_DOWN_FLASH):
if config.get(key):
raise cv.Invalid(
f"{key} requires {CONF_ENABLE_LIGHT_SLEEP}: true", path=[key]
)
config[CONF_POWER_DOWN_PERIPHERALS] = False
elif CONF_POWER_DOWN_PERIPHERALS not in config:
config[CONF_POWER_DOWN_PERIPHERALS] = variant in _TOP_PD_VARIANTS
elif config[CONF_POWER_DOWN_PERIPHERALS] and variant not in _TOP_PD_VARIANTS:
raise cv.Invalid(
f"{CONF_POWER_DOWN_PERIPHERALS} is only available on "
f"{', '.join(_TOP_PD_VARIANTS)}",
path=[CONF_POWER_DOWN_PERIPHERALS],
)
return config
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(PowerManagement),
cv.Optional(CONF_MAX_FREQUENCY): _mhz,
cv.Optional(CONF_MIN_FREQUENCY): _mhz,
cv.Optional(CONF_ENABLE_LIGHT_SLEEP, default=False): cv.boolean,
cv.Optional(CONF_IDLE_TIME_BEFORE_SLEEP, default=3): cv.int_range(
min=2, max=4294967295
),
cv.Optional(CONF_POWER_DOWN_PERIPHERALS): cv.boolean,
cv.Optional(CONF_POWER_DOWN_FLASH, default=False): cv.boolean,
cv.Optional(CONF_PROFILING, default=False): cv.boolean,
# Repurposes GPIOs as trace outputs, so keep it out of the visual editor.
cv.Optional(
CONF_TRACE, default=False, visibility=cv.Visibility.YAML_ONLY
): cv.boolean,
}
).extend(cv.COMPONENT_SCHEMA),
_validate,
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await power_management.register_power_management(var, config)
add_idf_sdkconfig_option("CONFIG_PM_ENABLE", True)
if (max_mhz := config.get(CONF_MAX_FREQUENCY)) is not None:
cg.add(var.set_max_freq_mhz(max_mhz))
if (min_mhz := config.get(CONF_MIN_FREQUENCY)) is not None:
cg.add(var.set_min_freq_mhz(min_mhz))
if config[CONF_PROFILING]:
add_idf_sdkconfig_option("CONFIG_PM_PROFILING", True)
if config[CONF_TRACE]:
add_idf_sdkconfig_option("CONFIG_PM_TRACE", True)
if not config[CONF_ENABLE_LIGHT_SLEEP]:
return
# Automatic light sleep whenever no task is runnable.
add_idf_sdkconfig_option("CONFIG_FREERTOS_USE_TICKLESS_IDLE", True)
add_idf_sdkconfig_option("CONFIG_ESP_PHY_MAC_BB_PD", True)
# Kconfig's own "depends on" no-ops this where it doesn't apply.
add_idf_sdkconfig_option("CONFIG_USJ_NO_AUTO_LS_ON_CONNECTION", True)
add_idf_sdkconfig_option(
"CONFIG_FREERTOS_IDLE_TIME_BEFORE_SLEEP", config[CONF_IDLE_TIME_BEFORE_SLEEP]
)
if config[CONF_POWER_DOWN_PERIPHERALS]:
add_idf_sdkconfig_option("CONFIG_PM_POWER_DOWN_PERIPHERAL_IN_LIGHT_SLEEP", True)
if config[CONF_POWER_DOWN_FLASH]:
add_idf_sdkconfig_option("CONFIG_ESP_SLEEP_POWER_DOWN_FLASH", True)
ot_conf = CORE.config.get(CONF_OPENTHREAD)
# A polling (sleepy) Thread end device cannot answer neighbour solicitations.
if (
ot_conf is not None
and (poll_period := ot_conf.get(CONF_POLL_PERIOD)) is not None
and poll_period.total_milliseconds > 0
):
add_idf_sdkconfig_option("CONFIG_LWIP_ND6", False)
if ot_conf is not None or ZIGBEE_DOMAIN in CORE.config:
add_idf_sdkconfig_option("CONFIG_IEEE802154_SLEEP_ENABLE", True)
def _final_validate(config: ConfigType) -> ConfigType:
full_config = fv.full_config.get()
entries = full_config.get(power_management.DOMAIN, [])
if sum(entry.get(CONF_PLATFORM) == DOMAIN for entry in entries) > 1:
raise cv.Invalid(f"Only one {DOMAIN} instance is allowed")
boot_freq = full_config[esp32.DOMAIN][esp32.CONF_CPU_FREQUENCY]
boot_mhz = int(boot_freq[:-3])
max_mhz = config.get(CONF_MAX_FREQUENCY)
# Without max_frequency the boot frequency is the ceiling esp_pm_configure() gets.
if (min_mhz := config.get(CONF_MIN_FREQUENCY)) is not None and min_mhz > (
max_mhz if max_mhz is not None else boot_mhz
):
raise cv.Invalid(
f"{CONF_MIN_FREQUENCY} must not be greater than the maximum frequency",
path=[CONF_MIN_FREQUENCY],
)
if max_mhz is not None:
if boot_mhz > max_mhz:
_LOGGER.info(
"esp32.%s (%s) is higher than %s (%dMHZ); the CPU will be "
"downclocked to %dMHZ as soon as power management is set up",
esp32.CONF_CPU_FREQUENCY,
boot_freq,
CONF_MAX_FREQUENCY,
max_mhz,
max_mhz,
)
elif boot_mhz < max_mhz:
_LOGGER.warning(
"esp32.%s (%s) is lower than %s (%dMHZ); the CPU may run as "
"high as %dMHZ under load once power management is set up",
esp32.CONF_CPU_FREQUENCY,
boot_freq,
CONF_MAX_FREQUENCY,
max_mhz,
max_mhz,
)
if config[CONF_POWER_DOWN_FLASH] and PSRAM_DOMAIN in full_config:
raise cv.Invalid(
f"{CONF_POWER_DOWN_FLASH} is not allowed when the device has PSRAM",
path=[CONF_POWER_DOWN_FLASH],
)
return config
FINAL_VALIDATE_SCHEMA = _final_validate
@@ -0,0 +1,16 @@
import esphome.codegen as cg
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType
IS_PLATFORM_COMPONENT = True
CODEOWNERS = ["@rwrozelle"]
DOMAIN = "power_management"
power_management_ns = cg.esphome_ns.namespace("power_management")
PowerManagementComponent = power_management_ns.class_(
"PowerManagementComponent", cg.Component
)
async def register_power_management(var: MockObj, config: ConfigType) -> None:
await cg.register_component(var, config)
@@ -0,0 +1,9 @@
#pragma once
#include "esphome/core/component.h"
namespace esphome::power_management {
class PowerManagementComponent : public Component {};
} // namespace esphome::power_management
@@ -0,0 +1 @@
"""Tests for the esp32_power_management component."""
@@ -0,0 +1,361 @@
"""Tests for esp32_power_management configuration validation and sdkconfig wiring."""
from contextlib import nullcontext
import logging
from unittest import mock
import pytest
from esphome import config_validation as cv
from esphome.components.esp32 import (
KEY_VARIANT,
VARIANT_ESP32,
VARIANT_ESP32C6,
VARIANT_ESP32H2,
)
from esphome.components.esp32_power_management.power_management import (
CONF_ENABLE_LIGHT_SLEEP,
CONF_IDLE_TIME_BEFORE_SLEEP,
CONF_MAX_FREQUENCY,
CONF_MIN_FREQUENCY,
CONF_POWER_DOWN_FLASH,
CONF_POWER_DOWN_PERIPHERALS,
CONFIG_SCHEMA,
_final_validate,
to_code,
valid_min_frequencies,
)
from esphome.components.openthread.const import CONF_DEVICE_TYPE, CONF_POLL_PERIOD
from esphome.const import CONF_OPENTHREAD, KEY_FRAMEWORK_VERSION, PlatformFramework
from esphome.core import CORE, TimePeriodMilliseconds
from esphome.types import ConfigType
from tests.component_tests.types import SetCoreConfigCallable
LIGHT_SLEEP = {CONF_ENABLE_LIGHT_SLEEP: True}
def _set_variant(
set_core_config: SetCoreConfigCallable,
variant: str,
/,
*,
full_config: ConfigType | None = None,
) -> None:
set_core_config(
PlatformFramework.ESP32_IDF,
core_data={KEY_FRAMEWORK_VERSION: cv.Version(5, 5, 5)},
platform_data={KEY_VARIANT: variant},
full_config=full_config,
)
@pytest.mark.parametrize(
("variant", "config", "expectation"),
[
pytest.param(
VARIANT_ESP32,
{CONF_MAX_FREQUENCY: "96MHZ"},
pytest.raises(cv.Invalid, match="not a valid CPU frequency"),
id="max_invalid_for_variant",
),
pytest.param(
VARIANT_ESP32,
{CONF_POWER_DOWN_PERIPHERALS: True},
pytest.raises(cv.Invalid, match="requires enable_light_sleep"),
id="power_down_peripherals_without_light_sleep",
),
pytest.param(
VARIANT_ESP32,
{CONF_POWER_DOWN_FLASH: True},
pytest.raises(cv.Invalid, match="requires enable_light_sleep"),
id="power_down_flash_without_light_sleep",
),
pytest.param(
VARIANT_ESP32,
LIGHT_SLEEP | {CONF_POWER_DOWN_PERIPHERALS: True},
pytest.raises(cv.Invalid, match="only available on"),
id="power_down_peripherals_unsupported_variant",
),
pytest.param(
VARIANT_ESP32H2,
LIGHT_SLEEP | {CONF_POWER_DOWN_PERIPHERALS: True},
nullcontext(),
id="power_down_peripherals_top_pd_variant",
),
# The ESP32 keeps its reference clock divider at 10 or more
pytest.param(
VARIANT_ESP32,
{CONF_MIN_FREQUENCY: "8MHZ"},
pytest.raises(cv.Invalid, match="not a valid minimum frequency"),
id="min_frequency_below_esp32_floor",
),
# 26 MHz crystal modules exist, so 13 MHz is reachable on an ESP32
pytest.param(
VARIANT_ESP32,
{CONF_MIN_FREQUENCY: "13MHZ"},
nullcontext(),
id="min_frequency_esp32_26mhz_crystal",
),
pytest.param(
VARIANT_ESP32,
{CONF_MIN_FREQUENCY: "80MHZ"},
nullcontext(),
id="min_frequency_pll",
),
# The ESP32-H2 runs from a 32 MHz crystal, so 8 MHz divides it and 10 MHz does not
pytest.param(
VARIANT_ESP32H2,
{CONF_MIN_FREQUENCY: "8MHZ"},
nullcontext(),
id="min_frequency_h2_crystal_divider",
),
pytest.param(
VARIANT_ESP32H2,
{CONF_MIN_FREQUENCY: "10MHZ"},
pytest.raises(cv.Invalid, match="must divide the crystal"),
id="min_frequency_h2_not_a_divider",
),
pytest.param(
VARIANT_ESP32C6,
{CONF_MIN_FREQUENCY: "1MHZ"},
pytest.raises(cv.Invalid, match="not a valid minimum frequency"),
id="min_frequency_below_reference_clock_floor",
),
],
)
def test_schema_validation(
set_core_config: SetCoreConfigCallable,
variant: str,
config: ConfigType,
expectation,
) -> None:
_set_variant(set_core_config, variant)
with expectation:
CONFIG_SCHEMA(config)
@pytest.mark.parametrize(
("variant", "expected"),
[
pytest.param(
VARIANT_ESP32, [10, 12, 13, 20, 24, 26, 40, 80, 160, 240], id="esp32"
),
pytest.param(VARIANT_ESP32H2, [2, 4, 8, 16, 32, 48, 64, 96], id="esp32h2"),
pytest.param(
VARIANT_ESP32C6, [2, 4, 5, 8, 10, 20, 40, 80, 120, 160], id="esp32c6"
),
],
)
def test_valid_min_frequencies(variant: str, expected: list[int]) -> None:
assert valid_min_frequencies(variant) == expected
def test_frequencies_are_kept_in_mhz(set_core_config: SetCoreConfigCallable) -> None:
_set_variant(set_core_config, VARIANT_ESP32)
config = CONFIG_SCHEMA({CONF_MAX_FREQUENCY: "160MHZ", CONF_MIN_FREQUENCY: 40e6})
assert config[CONF_MAX_FREQUENCY] == 160
assert config[CONF_MIN_FREQUENCY] == 40
assert config[CONF_IDLE_TIME_BEFORE_SLEEP] == 3
@pytest.mark.parametrize(
("variant", "config", "expected"),
[
pytest.param(VARIANT_ESP32H2, LIGHT_SLEEP, True, id="top_pd_light_sleep"),
pytest.param(VARIANT_ESP32, LIGHT_SLEEP, False, id="no_top_pd_light_sleep"),
pytest.param(VARIANT_ESP32H2, {}, False, id="no_light_sleep"),
pytest.param(
VARIANT_ESP32H2,
LIGHT_SLEEP | {CONF_POWER_DOWN_PERIPHERALS: False},
False,
id="explicit_value_kept",
),
],
)
def test_power_down_peripherals_default(
set_core_config: SetCoreConfigCallable,
variant: str,
config: ConfigType,
expected: bool,
) -> None:
_set_variant(set_core_config, variant)
assert CONFIG_SCHEMA(config)[CONF_POWER_DOWN_PERIPHERALS] is expected
def _final_validate_config(
set_core_config: SetCoreConfigCallable,
config: ConfigType,
full_config: ConfigType | None = None,
boot_frequency: str = "160MHZ",
) -> None:
"""Validate config, then run _final_validate against the given full config."""
full_config = {"esp32": {"cpu_frequency": boot_frequency}} | (full_config or {})
_set_variant(set_core_config, VARIANT_ESP32, full_config=full_config)
_final_validate(CONFIG_SCHEMA(config))
def test_rejects_multiple_esp32_power_management_instances(
set_core_config: SetCoreConfigCallable,
) -> None:
with pytest.raises(cv.Invalid, match="Only one esp32_power_management instance"):
_final_validate_config(
set_core_config,
{},
{"power_management": [{"platform": "esp32_power_management"}] * 2},
)
@pytest.mark.parametrize(
("max_frequency", "boot_frequency", "expected"),
[
pytest.param("160MHZ", "240MHZ", "downclocked to 160MHZ", id="boot_higher"),
pytest.param("240MHZ", "80MHZ", "may run as high as 240MHZ", id="boot_lower"),
pytest.param("160MHZ", "160MHZ", "", id="equal"),
],
)
def test_boot_frequency_log(
set_core_config: SetCoreConfigCallable,
caplog: pytest.LogCaptureFixture,
max_frequency: str,
boot_frequency: str,
expected: str,
) -> None:
caplog.set_level(logging.INFO)
_final_validate_config(
set_core_config,
{CONF_MAX_FREQUENCY: max_frequency},
boot_frequency=boot_frequency,
)
assert expected in caplog.text
assert (caplog.text == "") is (expected == "")
@pytest.mark.parametrize(
("config", "boot_frequency", "expectation"),
[
pytest.param(
{CONF_MAX_FREQUENCY: "80MHZ", CONF_MIN_FREQUENCY: "160MHZ"},
"240MHZ",
pytest.raises(cv.Invalid, match="must not be greater than"),
id="min_above_max",
),
pytest.param(
{CONF_MIN_FREQUENCY: "160MHZ"},
"80MHZ",
pytest.raises(cv.Invalid, match="must not be greater than"),
id="min_above_boot_frequency",
),
pytest.param(
{CONF_MIN_FREQUENCY: "80MHZ"},
"80MHZ",
nullcontext(),
id="min_equal_to_boot_frequency",
),
],
)
def test_min_frequency_against_effective_max(
set_core_config: SetCoreConfigCallable,
config: ConfigType,
boot_frequency: str,
expectation,
) -> None:
with expectation:
_final_validate_config(set_core_config, config, boot_frequency=boot_frequency)
def test_rejects_power_down_flash_with_psram(
set_core_config: SetCoreConfigCallable,
) -> None:
with pytest.raises(cv.Invalid, match="not allowed when the device has PSRAM"):
_final_validate_config(
set_core_config,
LIGHT_SLEEP | {CONF_POWER_DOWN_FLASH: True},
{"psram": {"mode": "quad"}},
)
@pytest.fixture
def sdkconfig_calls() -> dict[str, object]:
"""Run to_code() without codegen and collect the sdkconfig options it sets."""
calls: dict[str, object] = {}
with (
mock.patch(
"esphome.components.esp32_power_management.power_management.cg.new_Pvariable",
return_value=mock.MagicMock(),
),
mock.patch(
"esphome.components.esp32_power_management.power_management.power_management."
"register_power_management",
new_callable=mock.AsyncMock,
),
mock.patch(
"esphome.components.esp32_power_management.power_management.add_idf_sdkconfig_option",
side_effect=calls.__setitem__,
),
):
yield calls
SLEEPY_MTD = {
CONF_OPENTHREAD: {
CONF_DEVICE_TYPE: "MTD",
CONF_POLL_PERIOD: TimePeriodMilliseconds(milliseconds=1000),
}
}
NOT_SET = object()
@pytest.mark.asyncio
@pytest.mark.parametrize(
("core_config", "light_sleep", "key", "expected"),
[
pytest.param(SLEEPY_MTD, True, "CONFIG_LWIP_ND6", False, id="nd6_sleepy_mtd"),
pytest.param(
{CONF_OPENTHREAD: {CONF_DEVICE_TYPE: "FTD"}},
True,
"CONFIG_LWIP_ND6",
NOT_SET,
id="nd6_ftd",
),
pytest.param({}, True, "CONFIG_LWIP_ND6", NOT_SET, id="nd6_no_openthread"),
pytest.param(
{CONF_OPENTHREAD: {CONF_DEVICE_TYPE: "FTD"}},
True,
"CONFIG_IEEE802154_SLEEP_ENABLE",
True,
id="radio_sleep_openthread",
),
pytest.param(
{"zigbee": {"id": "zigbee_id"}},
True,
"CONFIG_IEEE802154_SLEEP_ENABLE",
True,
id="radio_sleep_zigbee",
),
pytest.param(
{}, True, "CONFIG_IEEE802154_SLEEP_ENABLE", NOT_SET, id="radio_sleep_none"
),
pytest.param(
SLEEPY_MTD, False, "CONFIG_LWIP_ND6", NOT_SET, id="nd6_light_sleep_off"
),
pytest.param(
SLEEPY_MTD,
False,
"CONFIG_IEEE802154_SLEEP_ENABLE",
NOT_SET,
id="radio_sleep_light_sleep_off",
),
],
)
async def test_to_code_sdkconfig(
set_core_config: SetCoreConfigCallable,
sdkconfig_calls: dict[str, object],
core_config: ConfigType,
light_sleep: bool,
key: str,
expected: object,
) -> None:
_set_variant(set_core_config, VARIANT_ESP32H2)
CORE.config = core_config
await to_code(CONFIG_SCHEMA({CONF_ENABLE_LIGHT_SLEEP: light_sleep}))
assert sdkconfig_calls.get(key, NOT_SET) is expected
@@ -0,0 +1,11 @@
power_management:
- platform: esp32_power_management
id: pm_id
max_frequency: ${esp32_power_management_max_frequency}
min_frequency: ${esp32_power_management_min_frequency}
enable_light_sleep: true
idle_time_before_sleep: 3
power_down_flash: true
power_down_peripherals: ${esp32_power_management_power_down_peripherals}
profiling: true
trace: true
@@ -0,0 +1,7 @@
substitutions:
esp32_power_management_max_frequency: 160MHZ
esp32_power_management_min_frequency: 40MHZ
esp32_power_management_power_down_peripherals: "false"
packages:
esp32_power_management: !include common.yaml
@@ -0,0 +1,7 @@
substitutions:
esp32_power_management_max_frequency: 96MHZ
esp32_power_management_min_frequency: 32MHZ
esp32_power_management_power_down_peripherals: "true"
packages:
esp32_power_management: !include common.yaml
@@ -0,0 +1,7 @@
substitutions:
esp32_power_management_max_frequency: 160MHZ
esp32_power_management_min_frequency: 40MHZ
esp32_power_management_power_down_peripherals: "false"
packages:
esp32_power_management: !include common.yaml