[deep_sleep] Reject wakeup_pin configs that fail after validation (#18616)

Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Harvey
2026-10-13 15:29:42 +11:00
committed by GitHub
co-authored by clydebarrow Claude Opus 5.5
parent e289a94451
commit 7881dfc5d8
2 changed files with 101 additions and 29 deletions
+38 -29
View File
@@ -143,6 +143,8 @@ def validate_wakeup_pin(
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
@@ -154,29 +156,34 @@ def validate_wakeup_pin(
def validate_config(config: ConfigType) -> ConfigType:
# right now only BK72XX supports the list format for wakeup pins
if CORE.is_bk72xx:
if CONF_WAKEUP_PIN_MODE in config:
wakeup_pins = config.get(CONF_WAKEUP_PIN, [])
if len(wakeup_pins) > 1:
raise cv.Invalid(
"You need to remove the global wakeup_pin_mode and define it per pin"
)
if wakeup_pins:
if CONF_WAKEUP_PIN_MODE in wakeup_pins[0]:
raise cv.Invalid(
"Specify wakeup_pin_mode either at the top level under deep_sleep "
"or under the pin entry, not both"
)
wakeup_pins[0][CONF_WAKEUP_PIN_MODE] = config.pop(CONF_WAKEUP_PIN_MODE)
elif (
isinstance(config.get(CONF_WAKEUP_PIN), list)
and len(config[CONF_WAKEUP_PIN]) > 1
):
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(
"Your platform does not support providing multiple entries in wakeup_pin"
"'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],
)
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(
"Specify 'wakeup_pin_mode' either at the top level under 'deep_sleep' "
"or under the pin entry, not both",
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
@@ -203,11 +210,11 @@ 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")
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")
raise cv.Invalid("'sleep_duration' cannot be more than 49 days on Zephyr")
return value
@@ -290,7 +297,10 @@ CONFIG_SCHEMA = cv.All(
cv.positive_time_period_milliseconds,
_validate_sleep_duration,
),
cv.Optional(CONF_WAKEUP_PIN): validate_wakeup_pin,
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),
@@ -347,11 +357,10 @@ async def to_code(config: ConfigType) -> None:
if CONF_SLEEP_DURATION in config:
cg.add(var.set_sleep_duration(config[CONF_SLEEP_DURATION]))
if CONF_WAKEUP_PIN in config:
pins_as_list = config.get(CONF_WAKEUP_PIN, [])
if (wakeup_pins := config.get(CONF_WAKEUP_PIN)) is not None:
if CORE.is_bk72xx:
cg.add(var.init_wakeup_pins(len(pins_as_list)))
for item in pins_as_list:
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]),
@@ -361,7 +370,7 @@ async def to_code(config: ConfigType) -> None:
)
)
else:
pin = await cg.gpio_pin_expression(pins_as_list[0][CONF_PIN])
pin = await cg.gpio_pin_expression(wakeup_pins[0][CONF_PIN])
cg.add(var.set_wakeup_pin(pin))
if CONF_WAKEUP_PIN_MODE in config:
cg.add(var.set_wakeup_pin_mode(config[CONF_WAKEUP_PIN_MODE]))
@@ -123,3 +123,66 @@ def test_deep_sleep_bk72xx_top_level_wakeup_pin_mode_moved_onto_single_pin(
assert (
result[CONF_WAKEUP_PIN][0][deep_sleep.CONF_WAKEUP_PIN_MODE] == "INVERT_WAKEUP"
)
def test_deep_sleep_empty_wakeup_pin_list_rejected() -> None:
"""An empty wakeup_pin list is rejected at validation instead of crashing codegen."""
with pytest.raises(cv.Invalid, match="at least one entry"):
deep_sleep.validate_wakeup_pin([])
def test_deep_sleep_bk72xx_wakeup_pin_mode_without_wakeup_pin_rejected(
set_core_config: SetCoreConfigCallable,
) -> None:
"""On BK72xx, a top-level wakeup_pin_mode with no wakeup_pin is rejected at validation."""
set_core_config(PlatformFramework.BK72XX_ARDUINO)
config = {deep_sleep.CONF_WAKEUP_PIN_MODE: "KEEP_AWAKE"}
with pytest.raises(cv.Invalid, match="requires 'wakeup_pin'"):
deep_sleep.validate_config(config)
def test_deep_sleep_esp32_wakeup_pin_mode_without_wakeup_pin_rejected(
set_core_config: SetCoreConfigCallable,
) -> None:
"""On ESP32 the same mode-only config is rejected too, instead of being silently dead."""
set_core_config(PlatformFramework.ESP32_IDF)
config = {deep_sleep.CONF_WAKEUP_PIN_MODE: "KEEP_AWAKE"}
with pytest.raises(cv.Invalid, match="requires 'wakeup_pin'"):
deep_sleep.validate_config(config)
@pytest.mark.parametrize(
"platform", [PlatformFramework.ESP8266_ARDUINO, PlatformFramework.NRF52_ZEPHYR]
)
def test_deep_sleep_wakeup_pin_rejected_on_unsupported_platform(
set_core_config: SetCoreConfigCallable, platform: PlatformFramework
) -> None:
"""wakeup_pin is rejected where the C++ component has no wakeup pin support."""
set_core_config(platform)
with pytest.raises(cv.Invalid, match="only available on"):
deep_sleep.CONFIG_SCHEMA({CONF_WAKEUP_PIN: "GPIO4"})
def test_deep_sleep_esp32_multiple_wakeup_pins_rejected(
set_core_config: SetCoreConfigCallable,
) -> None:
"""ESP32 accepts only one wakeup_pin entry."""
set_core_config(PlatformFramework.ESP32_IDF)
config = {CONF_WAKEUP_PIN: [{"pin": "GPIO4"}, {"pin": "GPIO5"}]}
with pytest.raises(cv.Invalid, match="ESP32 does not support multiple") as exc:
deep_sleep.validate_config(config)
assert exc.value.path == [CONF_WAKEUP_PIN]
def test_deep_sleep_bk72xx_global_wakeup_pin_mode_with_multiple_pins_rejected(
set_core_config: SetCoreConfigCallable,
) -> None:
"""On BK72xx, a top-level wakeup_pin_mode cannot be applied to several pins."""
set_core_config(PlatformFramework.BK72XX_ARDUINO)
config = {
CONF_WAKEUP_PIN: [{"pin": "GPIO12"}, {"pin": "GPIO13"}],
deep_sleep.CONF_WAKEUP_PIN_MODE: "KEEP_AWAKE",
}
with pytest.raises(cv.Invalid, match="define it per pin") as exc:
deep_sleep.validate_config(config)
assert exc.value.path == [deep_sleep.CONF_WAKEUP_PIN_MODE]