From e50a0d902e2c1e5d4b72a2f52dfacecc7ffb51a1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 11 Oct 2026 14:42:21 -1000 Subject: [PATCH] [core] Expose the preferences storage helpers to components (#20535) --- esphome/preferences.py | 18 +++++------- tests/unit_tests/test_preferences.py | 44 ++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 10 deletions(-) diff --git a/esphome/preferences.py b/esphome/preferences.py index fce85191302..6e7e4eda33d 100644 --- a/esphome/preferences.py +++ b/esphome/preferences.py @@ -19,18 +19,15 @@ STORAGE_FLASH = "flash" STORAGE_RTC = "rtc" -def _rtc_supported() -> bool: - """Whether the active platform has an RTC-backed preferences backend. - - Mirrors the C++ ``SOC_RTC_MEM_SUPPORTED`` guard in the ESP32 backend: the ESP32-C2 - and -C61 have no RTC memory at all, so RTC storage is unavailable there. - """ +def rtc_supported() -> bool: + """Whether the target has RTC-backed preferences (not ESP32-C2/C61); call at validation time.""" if CORE.is_esp8266: return True if CORE.is_esp32: from esphome.components.esp32 import get_esp32_variant from esphome.components.esp32.const import VARIANT_ESP32C2, VARIANT_ESP32C61 + # Mirrors the C++ SOC_RTC_MEM_SUPPORTED guard in the ESP32 backend return get_esp32_variant() not in (VARIANT_ESP32C2, VARIANT_ESP32C61) return False @@ -44,9 +41,10 @@ def _default_storage() -> str: return STORAGE_RTC if CORE.is_esp8266 else STORAGE_FLASH -def _validate_storage(value): +def validate_storage(value: object) -> str: + """Validate a ``storage:`` value, ``flash`` or ``rtc`` where supported.""" value = cv.one_of(STORAGE_FLASH, STORAGE_RTC, lower=True)(value) - if value == STORAGE_RTC and not _rtc_supported(): + if value == STORAGE_RTC and not rtc_supported(): raise cv.Invalid( f"'{STORAGE_RTC}' storage is not supported on this platform; only " f"'{STORAGE_FLASH}' is available" @@ -56,7 +54,7 @@ def _validate_storage(value): def storage_schema(): """Return an Optional(CONF_STORAGE) entry for merging into a component schema.""" - return {cv.Optional(CONF_STORAGE, default=_default_storage): _validate_storage} + return {cv.Optional(CONF_STORAGE, default=_default_storage): validate_storage} def request_rtc_storage() -> None: @@ -88,7 +86,7 @@ def validate_rtc_storage(value): "be disabled" ) return value - if not _rtc_supported(): + if not rtc_supported(): raise cv.Invalid("RTC preference storage is not supported on this platform") return value diff --git a/tests/unit_tests/test_preferences.py b/tests/unit_tests/test_preferences.py index 677eeee7f26..a1ab78bfbff 100644 --- a/tests/unit_tests/test_preferences.py +++ b/tests/unit_tests/test_preferences.py @@ -147,3 +147,47 @@ def test_rtc_rejected_on_unsupported_platform() -> None: def test_flash_allowed_on_unsupported_platform() -> None: _set_platform(PLATFORM_RP2040) assert _validate({CONF_STORAGE: "flash"})[CONF_STORAGE] == preferences.STORAGE_FLASH + + +@pytest.mark.parametrize("variant", [VARIANT_ESP32, VARIANT_ESP32C3]) +def test_rtc_supported_on_esp32_with_rtc_memory(variant: str) -> None: + _set_esp32(variant) + assert preferences.rtc_supported() is True + + +@pytest.mark.parametrize("variant", [VARIANT_ESP32C2, VARIANT_ESP32C61]) +def test_rtc_supported_false_without_rtc_memory(variant: str) -> None: + _set_esp32(variant) + assert preferences.rtc_supported() is False + + +@pytest.mark.parametrize( + ("platform", "expected"), + [(PLATFORM_ESP8266, True), (PLATFORM_RP2040, False)], +) +def test_rtc_supported_per_platform(platform: str, expected: bool) -> None: + _set_platform(platform) + assert preferences.rtc_supported() is expected + + +def test_validate_storage_accepts_flash_everywhere() -> None: + _set_platform(PLATFORM_RP2040) + assert preferences.validate_storage("flash") == preferences.STORAGE_FLASH + assert preferences.validate_storage("FLASH") == preferences.STORAGE_FLASH + + +def test_validate_storage_accepts_rtc_where_supported() -> None: + _set_esp32(VARIANT_ESP32) + assert preferences.validate_storage("rtc") == preferences.STORAGE_RTC + + +def test_validate_storage_rejects_rtc_without_rtc_memory() -> None: + _set_esp32(VARIANT_ESP32C2) + with pytest.raises(cv.Invalid, match="not supported on this platform"): + preferences.validate_storage("rtc") + + +def test_validate_storage_rejects_unknown_value() -> None: + _set_esp32(VARIANT_ESP32) + with pytest.raises(cv.Invalid): + preferences.validate_storage("nvs")