mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 10:12:52 +00:00
[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:
co-authored by
Kevin Ahrendt
Kevin Ahrendt
Claude Sonnet 4.6
J. Nick Koston
J. Nick Koston
parent
6bf091104f
commit
1c8808339a
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user