Files
esphome/esphome/components/deep_sleep/__init__.py
T

518 lines
16 KiB
Python

from esphome import automation, core, pins
import esphome.codegen as cg
from esphome.components import esp32, time
from esphome.components.esp32 import (
VARIANT_ESP32,
VARIANT_ESP32C2,
VARIANT_ESP32C3,
VARIANT_ESP32C5,
VARIANT_ESP32C6,
VARIANT_ESP32C61,
VARIANT_ESP32H2,
VARIANT_ESP32P4,
VARIANT_ESP32S2,
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 (
CONF_DEFAULT,
CONF_HOUR,
CONF_ID,
CONF_MINUTE,
CONF_MODE,
CONF_NUMBER,
CONF_PIN,
CONF_PINS,
CONF_RUN_DURATION,
CONF_SECOND,
CONF_SLEEP_DURATION,
CONF_TIME_ID,
CONF_TRIGGER_ID,
CONF_WAKEUP_PIN,
PLATFORM_BK72XX,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_NRF52,
PlatformFramework,
)
from esphome.core import CORE, ID
from esphome.cpp_generator import MockObj, TemplateArgsType
from esphome.types import ConfigType
WAKEUP_PINS = {
VARIANT_ESP32: [
0,
2,
4,
12,
13,
14,
15,
25,
26,
27,
32,
33,
34,
35,
36,
37,
38,
39,
],
VARIANT_ESP32C2: [0, 1, 2, 3, 4, 5],
VARIANT_ESP32C3: [0, 1, 2, 3, 4, 5],
VARIANT_ESP32C5: [0, 1, 2, 3, 4, 5, 6, 7],
VARIANT_ESP32C6: [0, 1, 2, 3, 4, 5, 6, 7],
VARIANT_ESP32C61: [0, 1, 2, 3, 4, 5, 6],
VARIANT_ESP32H2: [7, 8, 9, 10, 11, 12, 13, 14],
VARIANT_ESP32P4: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
VARIANT_ESP32S2: [
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
],
VARIANT_ESP32S3: [
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
13,
14,
15,
16,
17,
18,
19,
20,
21,
],
}
def validate_pin_number_esp32(value: ConfigType) -> ConfigType:
valid_pins = WAKEUP_PINS.get(get_esp32_variant(), WAKEUP_PINS[VARIANT_ESP32])
if value[CONF_NUMBER] not in valid_pins:
raise cv.Invalid(
f"Only pins {', '.join(str(x) for x in valid_pins)} support wakeup"
)
return value
def validate_pin_number(value: ConfigType) -> ConfigType:
if not CORE.is_esp32:
return value
return validate_pin_number_esp32(value)
def validate_wakeup_pin(
value: ConfigType | list[ConfigType],
) -> list[ConfigType]:
if not isinstance(value, list):
processed_pins: list[ConfigType] = [{CONF_PIN: value}]
else:
processed_pins = list(value)
if not processed_pins:
raise cv.Invalid("'wakeup_pin' needs at least one entry")
for i, pin_config in enumerate(processed_pins):
# now validate each item
validated_pin = WAKEUP_PIN_SCHEMA(pin_config)
validate_pin_number(validated_pin[CONF_PIN])
processed_pins[i] = validated_pin
return processed_pins
_WAKEUP_PIN_MODE_BOTH_LEVELS = (
"Specify 'wakeup_pin_mode' either at the top level under 'deep_sleep' "
"or under the pin entry, not both"
)
def validate_config(config: ConfigType) -> ConfigType:
wakeup_pins = config.get(CONF_WAKEUP_PIN)
has_pin_mode = CONF_WAKEUP_PIN_MODE in config
if has_pin_mode and wakeup_pins is None:
raise cv.Invalid(
"'wakeup_pin_mode' requires 'wakeup_pin'", path=[CONF_WAKEUP_PIN_MODE]
)
multiple_pins = wakeup_pins is not None and len(wakeup_pins) > 1
# wakeup_pin is limited to ESP32 and BK72XX, and only BK72XX accepts a list
if not CORE.is_bk72xx:
if multiple_pins:
raise cv.Invalid(
"ESP32 does not support multiple entries for 'wakeup_pin'",
path=[CONF_WAKEUP_PIN],
)
if wakeup_pins and CONF_WAKEUP_PIN_MODE in (pin := wakeup_pins[0]):
if has_pin_mode:
raise cv.Invalid(
_WAKEUP_PIN_MODE_BOTH_LEVELS,
path=[CONF_WAKEUP_PIN, 0, CONF_WAKEUP_PIN_MODE],
)
# ESP32 has one global mode, which to_code() reads from the top level
config[CONF_WAKEUP_PIN_MODE] = pin.pop(CONF_WAKEUP_PIN_MODE)
elif has_pin_mode:
if multiple_pins:
raise cv.Invalid(
"Remove the global 'wakeup_pin_mode' and define it per pin",
path=[CONF_WAKEUP_PIN_MODE],
)
if CONF_WAKEUP_PIN_MODE in (pin := wakeup_pins[0]):
raise cv.Invalid(
_WAKEUP_PIN_MODE_BOTH_LEVELS,
path=[CONF_WAKEUP_PIN, 0, CONF_WAKEUP_PIN_MODE],
)
# Move the global mode into the single pin entry, which to_code() reads
pin[CONF_WAKEUP_PIN_MODE] = config.pop(CONF_WAKEUP_PIN_MODE)
return config
def _validate_ex1_wakeup_mode(value: str) -> str:
if value == "ALL_LOW":
esp32.only_on_variant(supported=[VARIANT_ESP32], msg_prefix="ALL_LOW")(value)
if value == "ANY_LOW":
esp32.only_on_variant(
supported=[
VARIANT_ESP32C5,
VARIANT_ESP32C6,
VARIANT_ESP32C61,
VARIANT_ESP32H2,
VARIANT_ESP32P4,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
],
msg_prefix="ANY_LOW",
)(value)
return value
def _validate_sleep_duration(value: core.TimePeriod) -> core.TimePeriod:
if CORE.is_bk72xx:
max_duration = core.TimePeriod(hours=36)
if value > max_duration:
raise cv.Invalid("'sleep_duration' cannot be more than 36 hours on BK72XX")
elif CORE.using_zephyr:
max_duration = core.TimePeriod(days=49)
if value > max_duration:
raise cv.Invalid("'sleep_duration' cannot be more than 49 days on Zephyr")
return value
deep_sleep_ns = cg.esphome_ns.namespace("deep_sleep")
DeepSleepComponent = deep_sleep_ns.class_("DeepSleepComponent", cg.Component)
EnterDeepSleepAction = deep_sleep_ns.class_("EnterDeepSleepAction", automation.Action)
WakeupPinMode = deep_sleep_ns.enum("WakeupPinMode")
WAKEUP_PIN_MODES = WakeupPinMode.options(
"IGNORE", "KEEP_AWAKE", "INVERT_WAKEUP", prefix="WAKEUP_PIN_MODE_"
)
esp_sleep_ext1_wakeup_mode_t = cg.global_ns.enum("esp_sleep_ext1_wakeup_mode_t")
Ext1Wakeup = deep_sleep_ns.struct("Ext1Wakeup")
EXT1_WAKEUP_MODES = esp_sleep_ext1_wakeup_mode_t.options(
"ANY_LOW", "ALL_LOW", "ANY_HIGH", prefix="ESP_EXT1_WAKEUP_"
)
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_ESP32_EXT1_WAKEUP = "esp32_ext1_wakeup"
CONF_TOUCH_WAKEUP = "touch_wakeup"
CONF_GPIO_WAKEUP_REASON = "gpio_wakeup_reason"
CONF_TOUCH_WAKEUP_REASON = "touch_wakeup_reason"
CONF_UNTIL = "until"
WAKEUP_CAUSES_SCHEMA = cv.Schema(
{
cv.Required(CONF_DEFAULT): cv.positive_time_period_milliseconds,
cv.Optional(CONF_TOUCH_WAKEUP_REASON): cv.positive_time_period_milliseconds,
cv.Optional(CONF_GPIO_WAKEUP_REASON): cv.positive_time_period_milliseconds,
}
)
WAKEUP_PIN_SCHEMA = cv.Schema(
{
cv.Required(CONF_PIN): pins.internal_gpio_input_pin_schema,
cv.Optional(CONF_WAKEUP_PIN_MODE): cv.enum(WAKEUP_PIN_MODES, upper=True),
}
)
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(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(DeepSleepComponent),
cv.Optional(CONF_RUN_DURATION): cv.Any(
cv.All(cv.only_on_esp32, WAKEUP_CAUSES_SCHEMA),
cv.positive_time_period_milliseconds,
),
cv.Optional(CONF_SLEEP_DURATION): cv.All(
cv.positive_time_period_milliseconds,
_validate_sleep_duration,
),
cv.Optional(CONF_WAKEUP_PIN): cv.All(
cv.only_on([PLATFORM_ESP32, PLATFORM_BK72XX]),
validate_wakeup_pin,
),
cv.Optional(CONF_WAKEUP_PIN_MODE): cv.All(
cv.only_on([PLATFORM_ESP32, PLATFORM_BK72XX]),
cv.enum(WAKEUP_PIN_MODES, upper=True),
),
cv.Optional(CONF_ESP32_EXT1_WAKEUP): cv.All(
cv.only_on_esp32,
esp32.only_on_variant(
unsupported=[VARIANT_ESP32C2, VARIANT_ESP32C3],
msg_prefix="Wakeup from ext1",
),
cv.Schema(
{
cv.Required(CONF_PINS): cv.ensure_list(
validate_ext1_wakeup_pin,
),
cv.Required(CONF_MODE): cv.All(
cv.enum(EXT1_WAKEUP_MODES, upper=True),
_validate_ex1_wakeup_mode,
),
}
),
),
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.only_on_esp32,
esp32.only_on_variant(
unsupported=[
VARIANT_ESP32C2,
VARIANT_ESP32C3,
VARIANT_ESP32C5,
VARIANT_ESP32C6,
VARIANT_ESP32C61,
VARIANT_ESP32H2,
],
msg_prefix="Wakeup from touch",
),
cv.boolean,
),
}
).extend(cv.COMPONENT_SCHEMA),
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX, PLATFORM_NRF52]),
validate_config,
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
cg.add_optional(var.set_sleep_duration, config.get(CONF_SLEEP_DURATION))
if (wakeup_pins := config.get(CONF_WAKEUP_PIN)) is not None:
if CORE.is_bk72xx:
cg.add(var.init_wakeup_pins(len(wakeup_pins)))
for item in wakeup_pins:
cg.add(
var.add_wakeup_pin(
await cg.gpio_pin_expression(item[CONF_PIN]),
item.get(
CONF_WAKEUP_PIN_MODE, WakeupPinMode.WAKEUP_PIN_MODE_IGNORE
),
)
)
else:
await cg.add_pin(var.set_wakeup_pin, wakeup_pins[0][CONF_PIN])
cg.add_optional(var.set_wakeup_pin_mode, config.get(CONF_WAKEUP_PIN_MODE))
if CONF_RUN_DURATION in config:
run_duration_config = config[CONF_RUN_DURATION]
if not isinstance(run_duration_config, dict):
cg.add(var.set_run_duration(config[CONF_RUN_DURATION]))
else:
default_run_duration = run_duration_config[CONF_DEFAULT]
wakeup_cause_to_run_duration = cg.StructInitializer(
WakeupCauseToRunDuration,
("default_cause", default_run_duration),
(
"touch_cause",
run_duration_config.get(
CONF_TOUCH_WAKEUP_REASON, default_run_duration
),
),
(
"gpio_cause",
run_duration_config.get(
CONF_GPIO_WAKEUP_REASON, default_run_duration
),
),
)
cg.add(var.set_run_duration(wakeup_cause_to_run_duration))
if (ext1_conf := config.get(CONF_ESP32_EXT1_WAKEUP)) is not None:
mask = 0
for pin_conf in ext1_conf[CONF_PINS]:
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(
Ext1Wakeup, ("mask", mask), ("wakeup_mode", ext1_conf[CONF_MODE])
)
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")
cg.add_optional(var.set_touch_wakeup, config.get(CONF_TOUCH_WAKEUP))
if CORE.using_zephyr:
# Devices are suspended when CPU is entering a low power state
# https://github.com/nrfconnect/sdk-zephyr/blob/v3.7.99-ncs2-2/doc/services/pm/device.rst#system-managed-device-power-management
zephyr_add_prj_conf("PM_DEVICE", True)
if "zigbee" not in CORE.loaded_integrations:
zephyr_add_prj_conf("POWEROFF", True)
cg.add_define("USE_DEEP_SLEEP")
DEEP_SLEEP_ACTION_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.use_id(DeepSleepComponent),
}
)
DEEP_SLEEP_ENTER_SCHEMA = cv.All(
automation.maybe_simple_id(
DEEP_SLEEP_ACTION_SCHEMA.extend(
cv.Schema(
{
cv.Exclusive(CONF_SLEEP_DURATION, "time"): cv.templatable(
cv.All(
cv.positive_time_period_milliseconds,
_validate_sleep_duration,
)
),
# Only on ESP32 due to how long the RTC on ESP8266 can stay asleep
cv.Exclusive(CONF_UNTIL, "time"): cv.All(
cv.only_on_esp32, cv.time_of_day
),
cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
}
)
)
),
cv.has_none_or_all_keys(CONF_UNTIL, CONF_TIME_ID),
)
@automation.register_action(
"deep_sleep.enter",
EnterDeepSleepAction,
DEEP_SLEEP_ENTER_SCHEMA,
synchronous=True,
)
async def deep_sleep_enter_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
if CONF_SLEEP_DURATION in config:
template_ = await cg.templatable(config[CONF_SLEEP_DURATION], args, cg.uint32)
cg.add(var.set_sleep_duration(template_))
if CONF_UNTIL in config:
until = config[CONF_UNTIL]
cg.add(var.set_until(until[CONF_HOUR], until[CONF_MINUTE], until[CONF_SECOND]))
await cg.add_variable(var.set_time, config[CONF_TIME_ID])
return var
for _name, _call in (
("deep_sleep.prevent", "prevent_deep_sleep()"),
("deep_sleep.allow", "allow_deep_sleep()"),
):
automation.register_apply_action(
_name,
automation.maybe_simple_id(DEEP_SLEEP_ACTION_SCHEMA),
automation.ApplyCall(_call),
)
DOMAIN = "deep_sleep"
FILTER_SOURCE_FILES = filter_source_files_from_platform(
{
"deep_sleep_esp32.cpp": {
PlatformFramework.ESP32_ARDUINO,
PlatformFramework.ESP32_IDF,
},
"deep_sleep_esp8266.cpp": {PlatformFramework.ESP8266_ARDUINO},
"deep_sleep_bk72xx.cpp": {PlatformFramework.BK72XX_ARDUINO},
}
)