Merge branch 'wifi-fast-connect-substitution-string' into integration

This commit is contained in:
J. Nick Koston
2026-07-05 23:00:39 -05:00
40 changed files with 693 additions and 48 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ RUN \
-r /requirements.txt
# Install the ESPHome Device Builder dashboard.
RUN uv pip install --no-cache-dir esphome-device-builder==1.0.29
RUN uv pip install --no-cache-dir esphome-device-builder==1.1.0
RUN \
platformio settings set enable_telemetry No \
+15 -1
View File
@@ -6,7 +6,11 @@ from pathlib import Path
from esphome.components.esp32 import get_esp32_variant, idf_version
import esphome.config_validation as cv
from esphome.core import CORE
from esphome.framework_helpers import get_project_compile_flags, get_project_link_flags
from esphome.framework_helpers import (
get_project_compile_flags,
get_project_cxx_compile_flags,
get_project_link_flags,
)
from esphome.helpers import mkdir_p, write_file_if_changed
# Replaces the IDF default C++ standard (-std=gnu++2b appended to
@@ -91,6 +95,14 @@ def get_project_cmakelists(minimal: bool = False) -> str:
for flag in project_compile_opts
)
# Flags registered via cg.add_cxx_build_flag() go on CXX_COMPILE_OPTIONS
# (not COMPILE_OPTIONS) because GCC warns when a C++-only flag such as
# -Wno-volatile is passed on a C compile.
cxx_compile_options = "\n".join(
f'idf_build_set_property(CXX_COMPILE_OPTIONS "{flag}" APPEND)'
for flag in get_project_cxx_compile_flags()
)
cpp_standard_options = (
CPP_STANDARD_TEMPLATE.format(standard=CORE.cpp_standard)
if CORE.cpp_standard
@@ -155,6 +167,8 @@ include($ENV{{IDF_PATH}}/tools/cmake/project.cmake)
{cpp_standard_options}
{cxx_compile_options}
{extra_compile_options}
{managed_components_property}
+2 -3
View File
@@ -108,7 +108,6 @@ Import("env")
def write_cxx_flags_script() -> None:
path = CORE.relative_build_path(CXX_FLAGS_FILE_NAME)
contents = CXX_FLAGS_FILE_CONTENTS
if not CORE.is_host:
contents += 'env.Append(CXXFLAGS=["-Wno-volatile"])'
contents += "\n"
for flag in sorted(CORE.cxx_build_flags):
contents += f'env.Append(CXXFLAGS=["{flag}"])\n'
write_file_if_changed(path, contents)
+1
View File
@@ -26,6 +26,7 @@ from esphome.cpp_generator import ( # noqa: F401
add,
add_build_flag,
add_build_unflag,
add_cxx_build_flag,
add_define,
add_global,
add_library,
@@ -11,8 +11,11 @@ class ESP32PreferenceBackend final {
bool save(const uint8_t *data, size_t len);
bool load(uint8_t *data, size_t len);
uint32_t key;
uint32_t nvs_handle;
uint32_t key{0};
uint32_t nvs_handle{0}; // NVS (flash) path
uint16_t rtc_offset{0}; // RTC path: word offset into the RTC storage region
uint8_t length_words{0}; // RTC path: data length in 32-bit words
bool in_flash{true}; // true: store in NVS (flash); false: store in RTC memory
};
class ESP32Preferences;
+103
View File
@@ -3,7 +3,10 @@
#include "preferences.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/preferences_rtc.h"
#include <esp_attr.h>
#include <nvs_flash.h>
#include <soc/soc_caps.h>
#include <cstring>
#include <vector>
@@ -18,6 +21,48 @@ struct NVSData {
static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
// RTC memory backend for preferences requested with in_flash=false. Survives deep sleep and
// software/CPU resets, but not power loss; integrity is guarded by a per-record checksum so
// power-on garbage is detected on load. Keep this small: RTC memory is scarce and shared.
//
// Only compiled in when USE_ESP32_RTC_PREFERENCES_STORAGE is set (see preferences.h): the storage
// buffer reserves RTC memory, so it exists only when some config option actually selected RTC
// storage AND the variant has RTC memory (the ESP32-C2 and -C61 have none, so RTC_NOINIT_ATTR would
// have no section to land in and fail to link). Otherwise in_flash=false transparently falls back
// to NVS (see make_preference below).
//
// On variants with only RTC fast memory (C3/C6/H2/P4/C5/...) RTC_NOINIT_ATTR lands in RTC fast memory.
// This is still safe: the linker reserves .rtc_noinit ahead of any RTC-fast-as-heap pool
// (CONFIG_ESP_SYSTEM_ALLOW_RTC_FAST_MEM_AS_HEAP), and IDF keeps the RTC fast power domain on in deep
// sleep (forced on whether or not it is used as heap), so the data is retained across both resets and
// deep sleep -- only power loss clears it.
#ifdef USE_ESP32_RTC_PREFERENCES_STORAGE
static constexpr size_t RTC_PREF_SIZE_WORDS = 64; // 256 bytes
static constexpr size_t RTC_PREF_MAX_WORDS = 255; // length_words field is a uint8_t
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
static RTC_NOINIT_ATTR uint32_t s_rtc_storage[RTC_PREF_SIZE_WORDS];
static bool save_to_rtc(uint16_t offset, uint32_t key, uint8_t length_words, const uint8_t *data, size_t len) {
if (rtc_pref_bytes_to_words(len) != length_words)
return false;
const size_t buffer_size = static_cast<size_t>(length_words) + 1;
if (static_cast<size_t>(offset) + buffer_size > RTC_PREF_SIZE_WORDS)
return false;
rtc_pref_encode(&s_rtc_storage[offset], key, length_words, data, len);
return true;
}
static bool load_from_rtc(uint16_t offset, uint32_t key, uint8_t length_words, uint8_t *data, size_t len) {
if (rtc_pref_bytes_to_words(len) != length_words)
return false;
const size_t buffer_size = static_cast<size_t>(length_words) + 1;
if (static_cast<size_t>(offset) + buffer_size > RTC_PREF_SIZE_WORDS)
return false;
return rtc_pref_decode(&s_rtc_storage[offset], key, length_words, data, len);
}
#endif // USE_ESP32_RTC_PREFERENCES_STORAGE
// open() runs from app_main() before the logger is initialized, so any failure
// must be deferred until after global_logger is set. This is emitted from the
// first make_preference() call, which runs from the generated setup() after
@@ -25,6 +70,10 @@ static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-n
static esp_err_t s_open_err = ESP_OK; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
bool ESP32PreferenceBackend::save(const uint8_t *data, size_t len) {
#ifdef USE_ESP32_RTC_PREFERENCES_STORAGE
if (!this->in_flash)
return save_to_rtc(this->rtc_offset, this->key, this->length_words, data, len);
#endif
// try find in pending saves and update that
for (auto &obj : s_pending_save) {
if (obj.key == this->key) {
@@ -41,6 +90,10 @@ bool ESP32PreferenceBackend::save(const uint8_t *data, size_t len) {
}
bool ESP32PreferenceBackend::load(uint8_t *data, size_t len) {
#ifdef USE_ESP32_RTC_PREFERENCES_STORAGE
if (!this->in_flash)
return load_from_rtc(this->rtc_offset, this->key, this->length_words, data, len);
#endif
// try find in pending saves and load from that
for (auto &obj : s_pending_save) {
if (obj.key == this->key) {
@@ -94,6 +147,26 @@ void ESP32Preferences::open() {
}
}
ESPPreferenceObject ESP32Preferences::make_preference(size_t length, uint32_t type, bool in_flash) {
#ifdef USE_ESP32_RTC_PREFERENCES_STORAGE
if (!in_flash)
return this->make_rtc_preference_(length, type);
#else
if (!in_flash) {
// RTC storage is not compiled in (no config option selected it), so this request
// falls back to NVS -- the historic ESP32 behavior. Warn once so callers explicitly
// asking for RTC storage can discover the fallback.
static bool warned = false;
if (!warned) {
ESP_LOGW(TAG, "RTC preference storage not compiled in; using NVS (enable with 'preferences: rtc_storage: true')");
warned = true;
}
}
#endif
// in_flash, or RTC storage not compiled in: fall back to NVS.
return this->make_preference(length, type);
}
ESPPreferenceObject ESP32Preferences::make_preference(size_t length, uint32_t type) {
if (s_open_err != ESP_OK) {
if (this->nvs_handle == 0) {
@@ -106,10 +179,34 @@ ESPPreferenceObject ESP32Preferences::make_preference(size_t length, uint32_t ty
auto *pref = new ESP32PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory)
pref->nvs_handle = this->nvs_handle;
pref->key = type;
pref->in_flash = true;
return ESPPreferenceObject(pref);
}
#ifdef USE_ESP32_RTC_PREFERENCES_STORAGE
ESPPreferenceObject ESP32Preferences::make_rtc_preference_(size_t length, uint32_t type) {
const uint32_t length_words = rtc_pref_bytes_to_words(length);
if (length_words > RTC_PREF_MAX_WORDS) {
ESP_LOGE(TAG, "RTC preference too large: %" PRIu32 " words", length_words);
return {};
}
const uint32_t total_words = length_words + 1; // +1 for checksum
if (static_cast<size_t>(this->current_rtc_offset_) + total_words > RTC_PREF_SIZE_WORDS) {
ESP_LOGE(TAG, "RTC preference storage full, cannot allocate %" PRIu32 " words", total_words);
return {};
}
auto *pref = new ESP32PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory)
pref->key = type;
pref->in_flash = false;
pref->rtc_offset = this->current_rtc_offset_;
pref->length_words = static_cast<uint8_t>(length_words);
this->current_rtc_offset_ += static_cast<uint16_t>(total_words);
return ESPPreferenceObject(pref);
}
#endif // USE_ESP32_RTC_PREFERENCES_STORAGE
bool ESP32Preferences::sync() {
if (s_pending_save.empty())
return true;
@@ -186,6 +283,12 @@ bool ESP32Preferences::is_changed_(uint32_t nvs_handle, const NVSData &to_save,
bool ESP32Preferences::reset() {
ESP_LOGD(TAG, "Erasing storage");
s_pending_save.clear();
#ifdef USE_ESP32_RTC_PREFERENCES_STORAGE
// Invalidate RTC-backed preferences too (checksum will no longer match). current_rtc_offset_ is
// deliberately left alone: existing backends keep pointing at their allocated slots, and reset()
// is always followed by a restart (same reason nvs_handle is zeroed below).
memset(s_rtc_storage, 0, sizeof(s_rtc_storage));
#endif
nvs_flash_deinit();
nvs_flash_erase();
+18 -3
View File
@@ -2,6 +2,15 @@
#ifdef USE_ESP32
#include "esphome/core/preference_backend.h"
#include <soc/soc_caps.h>
// RTC-backed preference storage is compiled in only when a config option actually selects it
// (USE_ESP32_RTC_PREFERENCES, emitted during code generation) and the variant has RTC memory
// (SOC_RTC_MEM_SUPPORTED; the ESP32-C2 and -C61 have none). Otherwise in_flash=false falls
// back to NVS and no RTC memory is reserved.
#if defined(USE_ESP32_RTC_PREFERENCES) && SOC_RTC_MEM_SUPPORTED
#define USE_ESP32_RTC_PREFERENCES_STORAGE
#endif
namespace esphome::esp32 {
@@ -11,9 +20,8 @@ class ESP32Preferences final : public PreferencesMixin<ESP32Preferences> {
public:
using PreferencesMixin<ESP32Preferences>::make_preference;
void open();
ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) {
return this->make_preference(length, type);
}
ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash);
// Two-argument form defaults to NVS (flash) storage, preserving historic ESP32 behavior.
ESPPreferenceObject make_preference(size_t length, uint32_t type);
bool sync();
bool reset();
@@ -22,6 +30,13 @@ class ESP32Preferences final : public PreferencesMixin<ESP32Preferences> {
protected:
bool is_changed_(uint32_t nvs_handle, const NVSData &to_save, const char *key_str);
#ifdef USE_ESP32_RTC_PREFERENCES_STORAGE
// RTC-backed storage (in_flash=false).
ESPPreferenceObject make_rtc_preference_(size_t length, uint32_t type);
// Next free word offset in the RTC storage region (bump allocated in make_preference order).
uint16_t current_rtc_offset_{0};
#endif
};
void setup_preferences();
+7 -21
View File
@@ -8,6 +8,7 @@ extern "C" {
#include "preferences.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/preferences_rtc.h"
#include <cstring>
@@ -80,16 +81,6 @@ static uint32_t get_esp8266_flash_sector() {
}
static uint32_t get_esp8266_flash_address() { return get_esp8266_flash_sector() * SPI_FLASH_SEC_SIZE; }
static inline size_t bytes_to_words(size_t bytes) { return (bytes + 3) / 4; }
template<class It> uint32_t calculate_crc(It first, It last, uint32_t type) {
uint32_t crc = type;
while (first != last) {
crc ^= (*first++ * 2654435769UL) >> 1;
}
return crc;
}
static bool save_to_flash(size_t offset, const uint32_t *data, size_t len) {
for (uint32_t i = 0; i < len; i++) {
uint32_t j = offset + i;
@@ -137,21 +128,19 @@ static constexpr size_t PREF_MAX_BUFFER_WORDS =
ESP8266_FLASH_STORAGE_SIZE > RTC_NORMAL_REGION_WORDS ? ESP8266_FLASH_STORAGE_SIZE : RTC_NORMAL_REGION_WORDS;
bool ESP8266PreferenceBackend::save(const uint8_t *data, size_t len) {
if (bytes_to_words(len) != this->length_words)
if (rtc_pref_bytes_to_words(len) != this->length_words)
return false;
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
if (buffer_size > PREF_MAX_BUFFER_WORDS)
return false;
uint32_t buffer[PREF_MAX_BUFFER_WORDS];
memset(buffer, 0, buffer_size * sizeof(uint32_t));
memcpy(buffer, data, len);
buffer[this->length_words] = calculate_crc(buffer, buffer + this->length_words, this->type);
rtc_pref_encode(buffer, this->type, this->length_words, data, len);
return this->in_flash ? save_to_flash(this->offset, buffer, buffer_size)
: save_to_rtc(this->offset, buffer, buffer_size);
}
bool ESP8266PreferenceBackend::load(uint8_t *data, size_t len) {
if (bytes_to_words(len) != this->length_words)
if (rtc_pref_bytes_to_words(len) != this->length_words)
return false;
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
if (buffer_size > PREF_MAX_BUFFER_WORDS)
@@ -161,10 +150,7 @@ bool ESP8266PreferenceBackend::load(uint8_t *data, size_t len) {
: load_from_rtc(this->offset, buffer, buffer_size);
if (!ret)
return false;
if (buffer[this->length_words] != calculate_crc(buffer, buffer + this->length_words, this->type))
return false;
memcpy(data, buffer, len);
return true;
return rtc_pref_decode(buffer, this->type, this->length_words, data, len);
}
void ESP8266Preferences::setup() {
@@ -177,13 +163,13 @@ void ESP8266Preferences::setup() {
}
ESPPreferenceObject ESP8266Preferences::make_preference(size_t length, uint32_t type, bool in_flash) {
const uint32_t length_words = bytes_to_words(length);
const uint32_t length_words = rtc_pref_bytes_to_words(length);
if (length_words > MAX_PREFERENCE_WORDS) {
ESP_LOGE(TAG, "Preference too large: %u words", static_cast<unsigned int>(length_words));
return {};
}
const uint32_t total_words = length_words + 1; // +1 for CRC
const uint32_t total_words = length_words + 1; // +1 for checksum
uint16_t offset;
if (in_flash) {
@@ -16,6 +16,7 @@ from esphome.components.mipi import (
delay,
)
from esphome.components.spi import TYPE_QUAD
from esphome.config_validation import UNDEFINED
DriverChip(
"T-DISPLAY-S3-AMOLED",
@@ -97,6 +98,9 @@ CO5300 = DriverChip(
color_order=MODE_RGB,
bus_mode=TYPE_QUAD,
no_slpout=True,
swap_xy=UNDEFINED,
width=480,
height=480,
initsequence=(
(SLPOUT,), # Requires early SLPOUT
(PAGESEL, 0x00),
@@ -282,3 +282,13 @@ ST7789V.extend(
invert_colors=True,
data_rate="40MHz",
)
CO5300.extend(
"WAVESHARE-ESP32-S3-TOUCH-AMOLED-1.64",
width=280,
height=456,
offset_width=20,
cs_pin=9,
reset_pin=21,
enable_pin=1,
)
+13
View File
@@ -200,6 +200,7 @@ DeviceFirmwareUpdate = nrf52_ns.class_("DeviceFirmwareUpdate", cg.Component)
CONF_DFU = "dfu"
CONF_DCDC = "dcdc"
CONF_LIBC_NANO = "libc_nano"
CONF_REG0 = "reg0"
CONF_UICR_ERASE = "uicr_erase"
@@ -248,6 +249,7 @@ CONFIG_SCHEMA = cv.All(
): cv.Schema(
{
cv.Optional(CONF_VERSION): cv.string_strict,
cv.Optional(CONF_LIBC_NANO, default=True): cv.boolean,
cv.Optional(CONF_ADVANCED, default={}): cv.Schema(
{
cv.Optional(
@@ -273,6 +275,7 @@ def _validate_mcumgr(config):
def _final_validate(config):
if CONF_DFU in config:
_validate_mcumgr(config)
if config[KEY_BOOTLOADER] == BOOTLOADER_ADAFRUIT:
@@ -283,6 +286,13 @@ def _final_validate(config):
conf = config[CONF_FRAMEWORK]
advanced = conf[CONF_ADVANCED]
if conf[CONF_LIBC_NANO] and "logger" in CORE.loaded_integrations:
_LOGGER.warning(
"Logger is enabled with newlib-nano (libc_nano: true). Some format specifiers "
"such as %%zu are not supported and will print incorrectly. "
"Set 'libc_nano: false' under 'framework:' to use the full newlib."
)
if advanced[CONF_ENABLE_OTA_ROLLBACK]:
# "disabled: false" means safe mode *is* enabled.
safe_mode_config = full_config.get(CONF_SAFE_MODE, {CONF_DISABLED: True})
@@ -379,6 +389,9 @@ async def to_code(config: ConfigType) -> None:
# Enable OTA rollback support
if advanced[CONF_ENABLE_OTA_ROLLBACK]:
cg.add_define("USE_OTA_ROLLBACK")
zephyr_add_prj_conf("NEWLIB_LIBC", True)
zephyr_add_prj_conf("NEWLIB_LIBC_FLOAT_PRINTF", True)
zephyr_add_prj_conf("NEWLIB_LIBC_NANO", conf[CONF_LIBC_NANO])
# c++ support
if framework_ver < cv.Version(2, 9, 2):
zephyr_add_prj_conf("CPLUSPLUS", True)
@@ -1,3 +1,4 @@
from esphome import preferences
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ID
@@ -10,10 +11,17 @@ preferences_ns = cg.esphome_ns.namespace("preferences")
IntervalSyncer = preferences_ns.class_("IntervalSyncer", cg.Component)
CONF_FLASH_WRITE_INTERVAL = "flash_write_interval"
CONF_RTC_STORAGE = "rtc_storage"
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(IntervalSyncer),
cv.Optional(CONF_FLASH_WRITE_INTERVAL, default="60s"): cv.update_interval,
# Compile the RTC-backed storage into the ESP32 preferences backend even
# when no other option selects it, so components (including external
# ones) requesting in_flash=false are honoured instead of falling back
# to NVS. No default: absence means "no request" (see
# preferences.validate_rtc_storage for the per-platform rules).
cv.Optional(CONF_RTC_STORAGE): preferences.validate_rtc_storage,
}
).extend(cv.COMPONENT_SCHEMA)
@@ -26,4 +34,6 @@ async def to_code(config):
cg.add_define("USE_PREFERENCES_SYNC_EVERY_LOOP")
else:
cg.add(var.set_write_interval(write_interval))
if config.get(CONF_RTC_STORAGE):
preferences.request_rtc_storage()
await cg.register_component(var, config)
+4 -1
View File
@@ -1,4 +1,4 @@
from esphome import automation
from esphome import automation, preferences
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import (
@@ -7,6 +7,7 @@ from esphome.const import (
CONF_NUM_ATTEMPTS,
CONF_REBOOT_TIMEOUT,
CONF_SAFE_MODE,
CONF_STORAGE,
KEY_PAST_SAFE_MODE,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
@@ -42,6 +43,7 @@ CONFIG_SCHEMA = cv.All(
CONF_REBOOT_TIMEOUT, default="5min"
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_ON_SAFE_MODE): automation.validate_automation({}),
**preferences.storage_schema(),
}
).extend(cv.COMPONENT_SCHEMA),
_remove_id_if_disabled,
@@ -87,6 +89,7 @@ async def to_code(config):
config[CONF_NUM_ATTEMPTS],
config[CONF_REBOOT_TIMEOUT],
config[CONF_BOOT_IS_GOOD_AFTER],
preferences.is_in_flash(config[CONF_STORAGE]),
)
cg.add(RawExpression(f"if ({condition}) return"))
+3 -3
View File
@@ -162,13 +162,13 @@ bool SafeModeComponent::get_safe_mode_pending() {
return this->read_rtc_() == SafeModeComponent::ENTER_SAFE_MODE_MAGIC;
}
bool SafeModeComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time,
uint32_t boot_is_good_after) {
bool SafeModeComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time, uint32_t boot_is_good_after,
bool in_flash) {
this->safe_mode_start_time_ = millis();
this->safe_mode_enable_time_ = enable_time;
this->safe_mode_boot_is_good_after_ = boot_is_good_after;
this->safe_mode_num_attempts_ = num_attempts;
this->rtc_ = global_preferences->make_preference<uint32_t>(RTC_KEY, false);
this->rtc_ = global_preferences->make_preference<uint32_t>(RTC_KEY, in_flash);
#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK)
// Check partition state to detect if bootloader supports rollback
+1 -1
View File
@@ -17,7 +17,7 @@ constexpr uint32_t RTC_KEY = 233825507UL;
/// SafeModeComponent provides a safe way to recover from repeated boot failures
class SafeModeComponent final : public Component {
public:
bool should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time, uint32_t boot_is_good_after);
bool should_enter_safe_mode(uint8_t num_attempts, uint32_t enable_time, uint32_t boot_is_good_after, bool in_flash);
/// Set to true if the next startup will enter safe mode
void set_safe_mode_pending(const bool &pending);
+29 -4
View File
@@ -1,10 +1,11 @@
import logging
import math
from typing import Any
from esphome import automation
from esphome import automation, preferences
from esphome.automation import Condition
import esphome.codegen as cg
from esphome.components.const import CONF_USE_PSRAM
from esphome.components.const import CONF_ENABLED, CONF_USE_PSRAM
from esphome.components.esp32 import (
add_idf_sdkconfig_option,
const,
@@ -50,6 +51,7 @@ from esphome.const import (
CONF_REBOOT_TIMEOUT,
CONF_SSID,
CONF_STATIC_IP,
CONF_STORAGE,
CONF_SUBNET,
CONF_TIMEOUT,
CONF_TTLS_PHASE_2,
@@ -434,6 +436,23 @@ def _validate(config):
CONF_PASSIVE_SCAN = "passive_scan"
FAST_CONNECT_SCHEMA = cv.Schema(
{
cv.Optional(CONF_ENABLED, default=True): cv.boolean,
**preferences.storage_schema(),
}
)
def _fast_connect_schema(value: Any) -> ConfigType:
"""Accept the historic plain boolean (including boolean-like strings from
substitutions) or a dict with enabled/storage keys."""
if not isinstance(value, dict):
value = {CONF_ENABLED: value}
return FAST_CONNECT_SCHEMA(value)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
@@ -459,7 +478,7 @@ CONFIG_SCHEMA = cv.All(
rtl87xx="none",
ln882x="light",
): cv.enum(WIFI_POWER_SAVE_MODES, upper=True),
cv.Optional(CONF_FAST_CONNECT, default=False): cv.boolean,
cv.Optional(CONF_FAST_CONNECT, default=False): _fast_connect_schema,
cv.Optional(CONF_USE_ADDRESS): cv.string_strict,
cv.Optional(CONF_MIN_AUTH_MODE): cv.All(
VALIDATE_WIFI_MIN_AUTH_MODE,
@@ -619,8 +638,14 @@ async def to_code(config):
cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE]))
if CONF_MIN_AUTH_MODE in config:
cg.add(var.set_min_auth_mode(config[CONF_MIN_AUTH_MODE]))
if config[CONF_FAST_CONNECT]:
fast_connect = config[CONF_FAST_CONNECT]
if fast_connect[CONF_ENABLED]:
cg.add_define("USE_WIFI_FAST_CONNECT")
# The storage default preserves this preference's historic location:
# ESP8266 has always used RTC memory; every other platform effectively
# used flash (the in_flash flag was previously ignored outside ESP8266).
if preferences.is_in_flash(fast_connect[CONF_STORAGE]):
cg.add_define("USE_WIFI_FAST_CONNECT_IN_FLASH")
# passive_scan defaults to false in C++ - only set if true
if config[CONF_PASSIVE_SCAN]:
cg.add(var.set_passive_scan(True))
+7 -1
View File
@@ -649,7 +649,13 @@ void WiFiComponent::start() {
this->pref_ = global_preferences->make_preference<wifi::SavedWifiSettings>(hash, true);
#ifdef USE_WIFI_FAST_CONNECT
this->fast_connect_pref_ = global_preferences->make_preference<wifi::SavedWifiFastConnectSettings>(hash + 1, false);
#ifdef USE_WIFI_FAST_CONNECT_IN_FLASH
const bool fast_connect_in_flash = true;
#else
const bool fast_connect_in_flash = false;
#endif
this->fast_connect_pref_ =
global_preferences->make_preference<wifi::SavedWifiFastConnectSettings>(hash + 1, fast_connect_in_flash);
#endif
SavedWifiSettings save{};
-2
View File
@@ -134,9 +134,7 @@ def zephyr_to_code(config: ConfigType) -> None:
cg.add_define("USE_NATIVE_64BIT_TIME")
cg.set_cpp_standard("gnu++20")
# c++ support
zephyr_add_prj_conf("NEWLIB_LIBC", True)
zephyr_add_prj_conf("FPU", True)
zephyr_add_prj_conf("NEWLIB_LIBC_FLOAT_PRINTF", True)
zephyr_add_prj_conf("STD_CPP20", True)
# random_bytes() uses sys_rand_get() which requires the entropy subsystem
zephyr_add_prj_conf("ENTROPY_GENERATOR", True)
+1
View File
@@ -976,6 +976,7 @@ CONF_STEP_PIN = "step_pin"
CONF_STILL_THRESHOLD = "still_threshold"
CONF_STOP = "stop"
CONF_STOP_ACTION = "stop_action"
CONF_STORAGE = "storage"
CONF_STORE_BASELINE = "store_baseline"
CONF_SUBNET = "subnet"
CONF_SUBSCRIBE_QOS = "subscribe_qos"
+9
View File
@@ -627,6 +627,9 @@ class EsphomeCore:
self.platformio_libraries: dict[str, Library] = {}
# A set of build flags to set in the platformio project
self.build_flags: set[str] = set()
# A set of build flags that apply to C++ compiles only (CXXFLAGS /
# CXX_COMPILE_OPTIONS), for flags GCC rejects or warns about on C
self.cxx_build_flags: set[str] = set()
# A set of build unflags to set in the platformio project
self.build_unflags: set[str] = set()
# The C++ language standard for the build (e.g. "gnu++20"), set via cg.set_cpp_standard()
@@ -686,6 +689,7 @@ class EsphomeCore:
self.global_statements = []
self.platformio_libraries = {}
self.build_flags = set()
self.cxx_build_flags = set()
self.build_unflags = set()
self.cpp_standard = None
self.defines = set()
@@ -993,6 +997,11 @@ class EsphomeCore:
_LOGGER.debug("Adding build flag: %s", build_flag)
return build_flag
def add_cxx_build_flag(self, build_flag: str) -> str:
self.cxx_build_flags.add(build_flag)
_LOGGER.debug("Adding C++ build flag: %s", build_flag)
return build_flag
def add_build_unflag(self, build_unflag: str) -> None:
if self.using_toolchain_esp_idf:
# The native ESP-IDF build generator does not consume build_unflags
+9
View File
@@ -723,6 +723,15 @@ async def to_code(config: ConfigType) -> None:
cg.add_build_flag("-Wno-unused-variable")
cg.add_build_flag("-Wno-unused-but-set-variable")
cg.add_build_flag("-Wno-sign-compare")
# C++20 deprecated ++/--, compound assignment, and chained assignment on
# volatile lvalues; GCC warns via -Wvolatile, on by default at gnu++20.
# C++23 (P2327R1) removed the deprecation for compound assignment, so the
# warning flags patterns that are valid again under newer standards.
# C++-only flag: GCC warns when it is passed on a C compile, hence
# add_cxx_build_flag. Skipped for host builds, where the compiler may be
# clang, which does not know this GCC option.
if not CORE.is_host:
cg.add_cxx_build_flag("-Wno-volatile")
if config[CONF_DEBUG_SCHEDULER]:
cg.add_define("ESPHOME_DEBUG_SCHEDULER")
+2
View File
@@ -240,6 +240,7 @@
#define USE_OTA_ROLLBACK
#define USE_OTA_SIGNED_VERIFICATION
#define USE_ESP32_MIN_CHIP_REVISION_SET
#define USE_ESP32_RTC_PREFERENCES
#define USE_ESP32_SRAM1_AS_IRAM
#define USE_BLUETOOTH_PROXY
@@ -300,6 +301,7 @@
#define USE_CAPTIVE_PORTAL_GZIP
#define USE_WIFI_11KV_SUPPORT
#define USE_WIFI_FAST_CONNECT
#define USE_WIFI_FAST_CONNECT_IN_FLASH
#define USE_WIFI_PHY_MODE
#define USE_WIFI_IP_STATE_LISTENERS
#define USE_WIFI_SCAN_RESULTS_LISTENERS
+54
View File
@@ -0,0 +1,54 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <cstring>
namespace esphome {
// Shared storage format for word-addressable preference backends.
//
// Several platforms persist preferences as a buffer of 32-bit words followed by a
// single checksum word, seeded with the preference's `type` (its hashed key). This
// format is used for RTC user memory (ESP8266, ESP32) and for the ESP8266
// flash-emulation buffer. The helpers here are platform independent; each backend
// supplies its own word read/write primitives and offset allocation.
/// Round a byte count up to whole 32-bit words.
inline size_t rtc_pref_bytes_to_words(size_t bytes) { return (bytes + 3) / 4; }
/// Compute the integrity checksum over [first, last), seeded with `type`.
/// Iterates over 32-bit words; the result is stored as the trailing word of a record.
/// (Not a true CRC -- it XORs each word after a Fibonacci-hash multiply -- but the
/// algorithm is kept as-is for compatibility with records written by old firmware.)
template<class It> uint32_t rtc_pref_calculate_checksum(It first, It last, uint32_t type) {
uint32_t checksum = type;
while (first != last) {
// UINT32_C keeps the multiply wrapping at 32 bits regardless of the width of
// unsigned long, so 64-bit host builds compute the same value as the devices.
checksum ^= (*first++ * UINT32_C(2654435769)) >> 1;
}
return checksum;
}
/// Encode `len` data bytes into `buffer` (length_words data words + 1 trailing checksum word).
/// `buffer` must have capacity for at least `length_words + 1` words. Trailing padding in
/// the final data word is zeroed so the checksum is deterministic.
inline void rtc_pref_encode(uint32_t *buffer, uint32_t type, uint8_t length_words, const uint8_t *data, size_t len) {
memset(buffer, 0, (static_cast<size_t>(length_words) + 1) * sizeof(uint32_t));
memcpy(buffer, data, len);
buffer[length_words] = rtc_pref_calculate_checksum(buffer, buffer + length_words, type);
}
/// Verify the checksum of a record held in `buffer` (length_words data words + 1 checksum
/// word) and, on success, copy `len` bytes out to `data`. Returns false on checksum mismatch
/// (e.g. the record was never written or RTC memory holds power-on garbage).
inline bool rtc_pref_decode(const uint32_t *buffer, uint32_t type, uint8_t length_words, uint8_t *data, size_t len) {
if (buffer[length_words] != rtc_pref_calculate_checksum(buffer, buffer + length_words, type)) {
return false;
}
memcpy(data, buffer, len);
return true;
}
} // namespace esphome
+9
View File
@@ -724,6 +724,15 @@ def add_build_flag(build_flag: str):
CORE.add_build_flag(build_flag)
def add_cxx_build_flag(build_flag: str) -> None:
"""Add a global build flag that applies to C++ compiles only.
Use for flags GCC rejects or warns about when passed on C compiles
(e.g. ``-Wno-volatile``).
"""
CORE.add_cxx_build_flag(build_flag)
def add_build_unflag(build_unflag: str) -> None:
"""Add a global build unflag to the compiler flags."""
CORE.add_build_unflag(build_unflag)
+7
View File
@@ -37,6 +37,13 @@ def get_project_compile_flags() -> list[str]:
]
def get_project_cxx_compile_flags() -> list[str]:
"""Return the sorted flags that apply to C++ compiles only."""
from esphome.core import CORE # local import to avoid circular dependency
return sorted(CORE.cxx_build_flags)
def str_to_lst_of_str(a: str | list[str]) -> list[str]:
"""
Convert a string to a list of string
+106
View File
@@ -0,0 +1,106 @@
"""Helpers for letting a component choose where a preference is persisted.
Preferences can be stored either in flash (durable across power loss) or in RTC
memory (fast, survives deep sleep and soft resets but not power loss). The
flash-vs-RTC choice is only meaningful on platforms whose preferences backend
honors the ``in_flash`` flag currently ESP32 and ESP8266. On other platforms
the value is accepted only as ``flash`` (the sole supported backend).
Components include :func:`storage_schema` in their config and convert the chosen
value with :func:`is_in_flash` when calling ``make_preference``.
"""
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_STORAGE
from esphome.core import CORE
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.
"""
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
return get_esp32_variant() not in (VARIANT_ESP32C2, VARIANT_ESP32C61)
return False
def _default_storage() -> str:
"""Default that preserves each platform's historic behavior.
ESP8266 has always stored these preferences in RTC memory; every other
platform effectively used flash. Evaluated at validation time.
"""
return STORAGE_RTC if CORE.is_esp8266 else STORAGE_FLASH
def _validate_storage(value):
value = cv.one_of(STORAGE_FLASH, STORAGE_RTC, lower=True)(value)
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"
)
return 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}
def request_rtc_storage() -> None:
"""Compile the RTC-backed storage into the ESP32 preferences backend.
The RTC storage region is left out of ESP32 builds unless something asks for
it, so unused builds don't reserve RTC memory. Call this from ``to_code``
when a config option selects RTC storage. No-op on other platforms (ESP8266
always has its RTC backend).
"""
if CORE.is_esp32:
cg.add_define("USE_ESP32_RTC_PREFERENCES")
def validate_rtc_storage(value):
"""Validate a boolean option that requests RTC-backed preference storage.
``false`` means "no request", not "disable": it never turns RTC storage off
(another option selecting ``storage: rtc`` still compiles it in). On ESP8266
the backend is integral and always enabled, so ``false`` is rejected rather
than silently ignored; ``true`` is a tolerated no-op there so shared config
packages work across mixed fleets.
"""
value = cv.boolean(value)
if not value:
if CORE.is_esp8266:
raise cv.Invalid(
"RTC preference storage is always enabled on ESP8266 and cannot "
"be disabled"
)
return value
if not _rtc_supported():
raise cv.Invalid("RTC preference storage is not supported on this platform")
return value
def is_in_flash(value: str) -> bool:
"""Map a CONF_STORAGE value to the ``in_flash`` argument of make_preference.
Call this from ``to_code``: when RTC storage is selected on ESP32 it also emits
the define that compiles the RTC storage buffer into the ESP32 backend (see
:func:`request_rtc_storage`).
"""
in_flash = value == STORAGE_FLASH
if not in_flash:
request_rtc_storage()
return in_flash
+1 -1
View File
@@ -20,7 +20,7 @@ resvg-py==0.3.3
freetype-py==2.5.1
jinja2==3.1.6
bleak==2.1.1
smpclient==6.0.0
smpclient==7.2.0
requests==2.34.2
py7zr==1.1.3
platformdirs==4.10.0 # native esp-idf toolchain global cache dir
+1 -1
View File
@@ -555,7 +555,7 @@ def lint_constants_usage():
# Maximum allowed CONF_ constants in esphome/const.py.
# This file is frozen — new constants go in esphome/components/const/__init__.py.
# Decrease this number when constants are moved out of const.py.
CONST_PY_MAX_CONF = 1014
CONST_PY_MAX_CONF = 1015
@lint_content_check(include=["esphome/const.py"])
+1 -1
View File
@@ -1,5 +1,5 @@
{
"target_module": "esphome.__main__",
"margin_pct": 20,
"cumulative_us": 91000
"cumulative_us": 95000
}
+3 -2
View File
@@ -4,5 +4,6 @@ substitutions:
reset_pin: "21"
packages:
- !include ../../test_build_components/common/i2c/esp32-idf.yaml
- !include common.yaml
i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml
<<: !include common.yaml
@@ -2,3 +2,5 @@ nrf52:
dfu: true
reg0:
voltage: 1.8V
framework:
libc_nano: false
@@ -0,0 +1,4 @@
# Exercises the opt-in that compiles the RTC-backed preference storage into
# the ESP32 backend without any other option selecting it.
preferences:
rtc_storage: true
@@ -0,0 +1,4 @@
# Exercises the ESP32 RTC-backed preferences path (storage: rtc) for safe_mode.
safe_mode:
num_attempts: 3
storage: rtc
@@ -0,0 +1,7 @@
# Exercises the dict form of fast_connect with RTC-backed preference storage.
wifi:
ssid: MySSID
password: password1
fast_connect:
enabled: true
storage: rtc
@@ -0,0 +1,7 @@
# Exercises the dict form of fast_connect overriding the ESP8266 default (rtc)
# back to flash storage.
wifi:
ssid: MySSID
password: password1
fast_connect:
storage: flash
@@ -0,0 +1,9 @@
# fast_connect passed through a substitution arrives as a string ("false"),
# which must be accepted like the historic plain boolean form.
substitutions:
fast_connect_value: "false"
wifi:
ssid: MySSID
password: password1
fast_connect: ${fast_connect_value}
+23
View File
@@ -243,6 +243,7 @@ def test_get_project_cmakelists_no_cpp_standard(tmp_path: Path) -> None:
patch("esphome.build_gen.espidf.get_esp32_variant", return_value="ESP32"),
patch.object(CORE, "name", "test"),
patch.object(CORE, "cpp_standard", None),
patch.object(CORE, "cxx_build_flags", set()),
):
from esphome.build_gen.espidf import get_project_cmakelists
@@ -251,6 +252,28 @@ def test_get_project_cmakelists_no_cpp_standard(tmp_path: Path) -> None:
assert "CXX_COMPILE_OPTIONS" not in content
def test_get_project_cmakelists_cxx_build_flags(tmp_path: Path) -> None:
"""Flags registered via cg.add_cxx_build_flag() are appended to
CXX_COMPILE_OPTIONS (C++-only, GCC warns if they reach C compiles)
between include(project.cmake) and project()."""
with (
patch("esphome.build_gen.espidf.get_esp32_variant", return_value="ESP32"),
patch.object(CORE, "name", "test"),
patch.object(CORE, "cpp_standard", None),
patch.object(CORE, "cxx_build_flags", {"-Wno-volatile"}),
):
from esphome.build_gen.espidf import get_project_cmakelists
content = get_project_cmakelists(minimal=True)
flag_line = 'idf_build_set_property(CXX_COMPILE_OPTIONS "-Wno-volatile" APPEND)'
assert flag_line in content
include_pos = content.index("tools/cmake/project.cmake")
flag_pos = content.index(flag_line)
project_pos = content.index("project(test)")
assert include_pos < flag_pos < project_pos
def test_get_component_cmakelists_no_compile_features() -> None:
"""The C++ standard is pinned project-wide via CXX_COMPILE_OPTIONS in the
top-level CMakeLists; the src component must not set its own."""
@@ -200,3 +200,32 @@ def test_get_ini_content_no_cpp_standard(
content = platformio.get_ini_content()
assert "-std=" not in content
def test_write_cxx_flags_script_emits_registered_flags(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Flags registered via cg.add_cxx_build_flag() are emitted as CXXFLAGS,
sorted, so they apply to C++ compiles only."""
CORE.build_path = str(tmp_path)
monkeypatch.setattr(CORE, "cxx_build_flags", {"-Wno-volatile", "-Wno-deprecated"})
platformio.write_cxx_flags_script()
content = (tmp_path / platformio.CXX_FLAGS_FILE_NAME).read_text()
assert (
'env.Append(CXXFLAGS=["-Wno-deprecated"])\n'
'env.Append(CXXFLAGS=["-Wno-volatile"])\n'
) in content
def test_write_cxx_flags_script_no_flags(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
CORE.build_path = str(tmp_path)
monkeypatch.setattr(CORE, "cxx_build_flags", set())
platformio.write_cxx_flags_script()
content = (tmp_path / platformio.CXX_FLAGS_FILE_NAME).read_text()
assert "CXXFLAGS" not in content
@@ -26,6 +26,7 @@ from esphome.framework_helpers import (
create_venv,
download_from_mirrors,
get_project_compile_flags,
get_project_cxx_compile_flags,
get_project_link_flags,
get_python_env_executable_path,
get_system_python_path,
@@ -1048,3 +1049,25 @@ class TestGetProjectLinkFlags:
):
result = get_project_link_flags()
assert result == sorted(result)
def _make_core_cxx(flags: set[str]) -> MagicMock:
core = MagicMock()
core.cxx_build_flags = flags
return core
class TestGetProjectCxxCompileFlags:
def test_returns_sorted_flags(self) -> None:
with patch(
"esphome.core.CORE",
_make_core_cxx({"-Wno-volatile", "-Wno-deprecated"}),
):
assert get_project_cxx_compile_flags() == [
"-Wno-deprecated",
"-Wno-volatile",
]
def test_empty_flags(self) -> None:
with patch("esphome.core.CORE", _make_core_cxx(set())):
assert get_project_cxx_compile_flags() == []
+149
View File
@@ -0,0 +1,149 @@
"""Tests for esphome.preferences storage backend selection."""
import pytest
from esphome import preferences
from esphome.components.esp32 import KEY_ESP32
from esphome.components.esp32.const import (
VARIANT_ESP32,
VARIANT_ESP32C2,
VARIANT_ESP32C3,
VARIANT_ESP32C61,
)
import esphome.config_validation as cv
from esphome.const import (
CONF_STORAGE,
KEY_CORE,
KEY_TARGET_PLATFORM,
KEY_VARIANT,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_RP2040,
)
from esphome.core import CORE
def _set_platform(platform: str) -> None:
CORE.data[KEY_CORE] = {KEY_TARGET_PLATFORM: platform}
def _set_esp32(variant: str) -> None:
_set_platform(PLATFORM_ESP32)
CORE.data[KEY_ESP32] = {KEY_VARIANT: variant}
def _validate(value: dict):
return cv.Schema(preferences.storage_schema())(value)
def _define_names() -> set[str]:
return {define.name for define in CORE.defines}
def test_is_in_flash() -> None:
_set_platform(PLATFORM_ESP8266)
assert preferences.is_in_flash(preferences.STORAGE_FLASH) is True
assert preferences.is_in_flash(preferences.STORAGE_RTC) is False
# The RTC storage define is ESP32-specific.
assert "USE_ESP32_RTC_PREFERENCES" not in _define_names()
def test_is_in_flash_esp32_rtc_emits_define() -> None:
_set_esp32(VARIANT_ESP32)
assert preferences.is_in_flash(preferences.STORAGE_FLASH) is True
assert "USE_ESP32_RTC_PREFERENCES" not in _define_names()
assert preferences.is_in_flash(preferences.STORAGE_RTC) is False
assert "USE_ESP32_RTC_PREFERENCES" in _define_names()
def test_request_rtc_storage_esp32_only() -> None:
_set_platform(PLATFORM_ESP8266)
preferences.request_rtc_storage()
# ESP8266 always has its RTC backend; no define is needed or emitted.
assert "USE_ESP32_RTC_PREFERENCES" not in _define_names()
def test_request_rtc_storage_esp32_emits_define() -> None:
_set_esp32(VARIANT_ESP32)
preferences.request_rtc_storage()
assert "USE_ESP32_RTC_PREFERENCES" in _define_names()
@pytest.mark.parametrize("variant", [VARIANT_ESP32, VARIANT_ESP32C3])
def test_validate_rtc_storage_accepted(variant: str) -> None:
_set_esp32(variant)
assert preferences.validate_rtc_storage(True) is True
assert preferences.validate_rtc_storage(False) is False
def test_validate_rtc_storage_esp8266() -> None:
_set_platform(PLATFORM_ESP8266)
# Tolerated no-op: the ESP8266 backend always has RTC storage.
assert preferences.validate_rtc_storage(True) is True
# But it cannot be disabled, so an explicit false is an error.
with pytest.raises(cv.Invalid, match="always enabled on ESP8266"):
preferences.validate_rtc_storage(False)
@pytest.mark.parametrize("variant", [VARIANT_ESP32C2, VARIANT_ESP32C61])
def test_validate_rtc_storage_rejected_without_rtc_memory(variant: str) -> None:
_set_esp32(variant)
with pytest.raises(cv.Invalid, match="not supported on this platform"):
preferences.validate_rtc_storage(True)
# Disabling it is always fine.
assert preferences.validate_rtc_storage(False) is False
def test_validate_rtc_storage_rejected_on_unsupported_platform() -> None:
_set_platform(PLATFORM_RP2040)
with pytest.raises(cv.Invalid, match="not supported on this platform"):
preferences.validate_rtc_storage(True)
@pytest.mark.parametrize(
("platform", "expected"),
[
# Defaults preserve each platform's historic behavior.
(PLATFORM_ESP8266, preferences.STORAGE_RTC),
(PLATFORM_RP2040, preferences.STORAGE_FLASH),
],
)
def test_default_storage_per_platform(platform: str, expected: str) -> None:
_set_platform(platform)
assert _validate({})[CONF_STORAGE] == expected
@pytest.mark.parametrize("variant", [VARIANT_ESP32, VARIANT_ESP32C2])
def test_default_storage_esp32_is_flash(variant: str) -> None:
# ESP32 defaults to flash on every variant, including those without RTC memory.
_set_esp32(variant)
assert _validate({})[CONF_STORAGE] == preferences.STORAGE_FLASH
def test_rtc_allowed_on_esp8266() -> None:
_set_platform(PLATFORM_ESP8266)
assert _validate({CONF_STORAGE: "rtc"})[CONF_STORAGE] == preferences.STORAGE_RTC
@pytest.mark.parametrize("variant", [VARIANT_ESP32, VARIANT_ESP32C3])
def test_rtc_allowed_on_esp32_with_rtc_memory(variant: str) -> None:
_set_esp32(variant)
assert _validate({CONF_STORAGE: "rtc"})[CONF_STORAGE] == preferences.STORAGE_RTC
@pytest.mark.parametrize("variant", [VARIANT_ESP32C2, VARIANT_ESP32C61])
def test_rtc_rejected_on_esp32_without_rtc_memory(variant: str) -> None:
_set_esp32(variant)
with pytest.raises(cv.Invalid, match="not supported on this platform"):
_validate({CONF_STORAGE: "rtc"})
def test_rtc_rejected_on_unsupported_platform() -> None:
_set_platform(PLATFORM_RP2040)
with pytest.raises(cv.Invalid, match="not supported on this platform"):
_validate({CONF_STORAGE: "rtc"})
def test_flash_allowed_on_unsupported_platform() -> None:
_set_platform(PLATFORM_RP2040)
assert _validate({CONF_STORAGE: "flash"})[CONF_STORAGE] == preferences.STORAGE_FLASH