mirror of
https://github.com/esphome/esphome.git
synced 2026-11-09 14:45:57 +00:00
Co-authored-by: J. Nick Koston <nick@home-assistant.io> Co-authored-by: J. Nick Koston <nick@koston.org>
217 lines
8.0 KiB
Python
217 lines
8.0 KiB
Python
"""Tests for switch `initial_state` / `restore_state` (including `storage`)."""
|
|
|
|
from collections.abc import Callable
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from esphome.components import switch
|
|
from esphome.components.libretiny.const import (
|
|
FAMILY_BK7231T,
|
|
FAMILY_BK7251,
|
|
FAMILY_LN882H,
|
|
FAMILY_RTL8720C,
|
|
KEY_FAMILY,
|
|
KEY_LIBRETINY,
|
|
)
|
|
import esphome.config_validation as cv
|
|
from esphome.const import PlatformFramework
|
|
from esphome.core import CORE
|
|
from tests.component_tests.types import SetCoreConfigCallable
|
|
|
|
GATE = "USE_SWITCH_STORAGE"
|
|
ESP32_RTC = "USE_ESP32_RTC_PREFERENCES"
|
|
|
|
|
|
def _defines() -> set[str]:
|
|
return {define.name for define in CORE.defines}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("switch_id", "mode"),
|
|
[
|
|
("keep_short", "SWITCH_RESTORE_DEFAULT_OFF"),
|
|
("all_short", "SWITCH_RESTORE_DEFAULT_OFF"),
|
|
("invert_on", "SWITCH_RESTORE_INVERTED_DEFAULT_ON"),
|
|
("initial_on", "SWITCH_ALWAYS_ON"),
|
|
("none_on", "SWITCH_ALWAYS_ON"),
|
|
("initial_only", "SWITCH_ALWAYS_ON"),
|
|
("legacy", "SWITCH_RESTORE_DEFAULT_OFF"),
|
|
("plain", "SWITCH_ALWAYS_OFF"),
|
|
],
|
|
)
|
|
def test_restore_state_maps_to_a_restore_mode(
|
|
generate_main: Callable[[str | Path], str],
|
|
component_config_path: Callable[[str], Path],
|
|
switch_id: str,
|
|
mode: str,
|
|
) -> None:
|
|
"""Every initial_state / restore_state combination becomes one existing restore mode."""
|
|
main_cpp = generate_main(component_config_path("restore_state_esp32.yaml"))
|
|
|
|
assert f"{switch_id}->set_restore_mode(switch_::{mode});" in main_cpp
|
|
|
|
|
|
def test_storage_is_passed_only_when_set(
|
|
generate_main: Callable[[str | Path], str],
|
|
component_config_path: Callable[[str], Path],
|
|
) -> None:
|
|
main_cpp = generate_main(component_config_path("restore_state_esp32.yaml"))
|
|
|
|
assert (
|
|
"keep_rtc->set_restore_mode(static_cast<switch_::SwitchRestoreMode>("
|
|
"switch_::SWITCH_RESTORE_DEFAULT_ON | switch_::RESTORE_MODE_STORAGE_RTC_MASK));"
|
|
) in main_cpp
|
|
assert (
|
|
"keep_flash->set_restore_mode(static_cast<switch_::SwitchRestoreMode>("
|
|
"switch_::SWITCH_RESTORE_DEFAULT_OFF | switch_::RESTORE_MODE_STORAGE_FLASH_MASK));"
|
|
) in main_cpp
|
|
assert (
|
|
"keep_short->set_restore_mode(switch_::SWITCH_RESTORE_DEFAULT_OFF);" in main_cpp
|
|
)
|
|
assert GATE in _defines()
|
|
assert ESP32_RTC in _defines()
|
|
|
|
|
|
def test_nothing_new_emitted_without_restore_state(
|
|
generate_main: Callable[[str | Path], str],
|
|
component_config_path: Callable[[str], Path],
|
|
) -> None:
|
|
"""Configs using only restore_mode generate exactly what they did before."""
|
|
main_cpp = generate_main(component_config_path("restore_state_absent_esp32.yaml"))
|
|
|
|
assert "RESTORE_MODE_STORAGE_" not in main_cpp
|
|
assert GATE not in _defines()
|
|
assert ESP32_RTC not in _defines()
|
|
|
|
|
|
def test_rtc_applies_per_switch_on_esp8266_with_restore_from_flash(
|
|
generate_main: Callable[[str | Path], str],
|
|
component_config_path: Callable[[str], Path],
|
|
) -> None:
|
|
main_cpp = generate_main(component_config_path("restore_state_esp8266.yaml"))
|
|
|
|
assert (
|
|
"keep_rtc->set_restore_mode(static_cast<switch_::SwitchRestoreMode>("
|
|
"switch_::SWITCH_RESTORE_DEFAULT_ON | switch_::RESTORE_MODE_STORAGE_RTC_MASK));"
|
|
) in main_cpp
|
|
assert ESP32_RTC not in _defines()
|
|
|
|
|
|
def _validate(config: dict, *, default_restore_mode: str = cv.UNDEFINED) -> dict:
|
|
schema = switch.switch_schema(
|
|
switch.Switch, default_restore_mode=default_restore_mode
|
|
)
|
|
return schema({"id": "test_switch", **config})
|
|
|
|
|
|
@pytest.fixture
|
|
def esp8266(set_core_config: SetCoreConfigCallable) -> None:
|
|
set_core_config(PlatformFramework.ESP8266_ARDUINO)
|
|
|
|
|
|
@pytest.mark.usefixtures("esp8266")
|
|
def test_restore_mode_and_restore_state_are_exclusive() -> None:
|
|
with pytest.raises(cv.Invalid, match="restore"):
|
|
_validate({"restore_mode": "RESTORE_DEFAULT_OFF", "restore_state": "all"})
|
|
|
|
|
|
@pytest.mark.usefixtures("esp8266")
|
|
def test_initial_state_rejected_with_restore_mode() -> None:
|
|
"""restore_mode already decides the boot state, so initial_state would be ignored."""
|
|
with pytest.raises(cv.Invalid, match="initial_state"):
|
|
_validate({"restore_mode": "ALWAYS_OFF", "initial_state": True})
|
|
|
|
|
|
@pytest.mark.usefixtures("esp8266")
|
|
def test_storage_rejected_when_nothing_is_restored() -> None:
|
|
with pytest.raises(cv.Invalid, match="only applies"):
|
|
_validate({"restore_state": {"state": "INITIAL", "storage": "rtc"}})
|
|
|
|
|
|
@pytest.mark.usefixtures("esp8266")
|
|
@pytest.mark.parametrize("value", ["RESTORE", "ON", "OFF", "KEEP", "INVERT", "INITIAL"])
|
|
def test_unsupported_restore_state_values_rejected(value: object) -> None:
|
|
# Only light's shorthands (all, none) are accepted as a string
|
|
with pytest.raises(cv.Invalid, match="expected 'all', 'none' or a mapping"):
|
|
_validate({"restore_state": value})
|
|
|
|
|
|
@pytest.mark.usefixtures("esp8266")
|
|
@pytest.mark.parametrize("state", [True, False, "ON", "off"])
|
|
def test_restore_state_on_off_explains_what_to_use(state: object) -> None:
|
|
# Valid on light, so people will try it; YAML turns ON into True
|
|
with pytest.raises(
|
|
cv.Invalid, match="initial_state: ON' \\(or OFF\\) with 'restore_state: none'"
|
|
):
|
|
_validate({"restore_state": {"state": state}})
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("platform", "family"),
|
|
[
|
|
(PlatformFramework.BK72XX_ARDUINO, FAMILY_BK7251),
|
|
(PlatformFramework.RTL87XX_ARDUINO, FAMILY_RTL8720C),
|
|
(PlatformFramework.LN882X_ARDUINO, FAMILY_LN882H),
|
|
(PlatformFramework.RP2040_ARDUINO, None),
|
|
(PlatformFramework.NRF52_ZEPHYR, None),
|
|
(PlatformFramework.HOST_NATIVE, None),
|
|
],
|
|
)
|
|
def test_rtc_rejected_without_an_rtc_backend(
|
|
set_core_config: SetCoreConfigCallable,
|
|
platform: PlatformFramework,
|
|
family: str | None,
|
|
) -> None:
|
|
"""These preferences backends ignore in_flash, so the state would silently stay in flash."""
|
|
set_core_config(platform)
|
|
if family is not None:
|
|
CORE.data[KEY_LIBRETINY] = {KEY_FAMILY: family}
|
|
with pytest.raises(cv.Invalid, match="'rtc' storage is not supported"):
|
|
_validate({"restore_state": {"state": "KEEP", "storage": "rtc"}})
|
|
|
|
|
|
def test_rtc_accepted_on_a_beken_chip_with_the_emulation(
|
|
set_core_config: SetCoreConfigCallable,
|
|
) -> None:
|
|
set_core_config(PlatformFramework.BK72XX_ARDUINO)
|
|
CORE.data[KEY_LIBRETINY] = {KEY_FAMILY: FAMILY_BK7231T}
|
|
_validate({"restore_state": {"state": "KEEP", "storage": "rtc"}})
|
|
|
|
|
|
@pytest.mark.usefixtures("esp8266")
|
|
def test_rtc_accepted_on_esp8266() -> None:
|
|
_validate({"restore_state": {"state": "KEEP", "storage": "rtc"}})
|
|
|
|
|
|
@pytest.mark.usefixtures("esp8266")
|
|
def test_platform_default_restore_mode_applies_only_when_nothing_is_set() -> None:
|
|
"""Platforms such as haier default to DISABLED; restore_state must still be usable there."""
|
|
assert _validate({}, default_restore_mode="DISABLED")["restore_mode"] == "DISABLED"
|
|
configured = _validate({"restore_state": "all"}, default_restore_mode="DISABLED")
|
|
assert "restore_mode" not in configured
|
|
|
|
|
|
@pytest.mark.usefixtures("esp8266")
|
|
@pytest.mark.parametrize(
|
|
("default_restore_mode", "initial_state", "expected"),
|
|
[
|
|
# A restoring default keeps restoring; initial_state only replaces its fallback
|
|
("RESTORE_DEFAULT_ON", False, "RESTORE_DEFAULT_OFF"),
|
|
("RESTORE_DEFAULT_OFF", True, "RESTORE_DEFAULT_ON"),
|
|
("RESTORE_INVERTED_DEFAULT_OFF", True, "RESTORE_INVERTED_DEFAULT_ON"),
|
|
# A default that restores nothing starts in initial_state
|
|
("ALWAYS_OFF", True, "ALWAYS_ON"),
|
|
("DISABLED", True, "ALWAYS_ON"),
|
|
],
|
|
)
|
|
def test_initial_state_alone_keeps_a_restoring_platform_default(
|
|
default_restore_mode: str, initial_state: bool, expected: str
|
|
) -> None:
|
|
config = _validate(
|
|
{"initial_state": initial_state}, default_restore_mode=default_restore_mode
|
|
)
|
|
assert "restore_mode" not in config
|
|
assert "storage" not in config.get("restore_state", {})
|
|
assert switch._resolve_restore_mode(config) is switch.RESTORE_MODES[expected]
|