[wifi] Add DHCP lease cache (#17767)

Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Chris Boot
2026-10-11 18:48:13 -10:00
committed by GitHub
co-authored by J. Nick Koston J. Nick Koston
parent 3f21ddf6ff
commit 19eeab0385
8 changed files with 375 additions and 20 deletions
+42
View File
@@ -62,6 +62,7 @@ from esphome.const import (
CONF_USE_ADDRESS,
CONF_USERNAME,
PLACEHOLDER_WIFI_SSID,
Framework,
Platform,
PlatformFramework,
)
@@ -490,6 +491,38 @@ def _fast_connect_schema(value: Any) -> ConfigType:
return FAST_CONNECT_SCHEMA(value)
CONF_LEASE_CACHE = "lease_cache"
CONF_MAX_AGE = "max_age"
LEASE_CACHE_SCHEMA = cv.Schema(
{
cv.Optional(CONF_ENABLED, default=True): cv.boolean,
# Defaults to RTC in _lease_cache_schema; flash wears and must be opted into.
cv.Optional(CONF_STORAGE): preferences.validate_storage,
cv.Optional(CONF_MAX_AGE): cv.positive_time_period_seconds,
}
)
def _lease_cache_schema(value: Any) -> ConfigType:
"""Accept a plain boolean shorthand or a dict with enabled/storage/max_age keys."""
if not isinstance(value, dict):
value = {CONF_ENABLED: value}
value = LEASE_CACHE_SCHEMA(value)
if value[CONF_ENABLED]:
cv.only_on_esp32(value)
cv.only_with_framework(Framework.ESP_IDF)(value)
if CONF_STORAGE not in value:
if not preferences.rtc_supported():
raise cv.Invalid(
"This ESP32 variant has no RTC memory; set 'storage: flash' to "
"cache the lease in flash",
path=[CONF_STORAGE],
)
value[CONF_STORAGE] = preferences.STORAGE_RTC
return value
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
@@ -518,6 +551,7 @@ CONFIG_SCHEMA = cv.All(
ln882x="none",
): cv.enum(WIFI_POWER_SAVE_MODES, upper=True),
cv.Optional(CONF_FAST_CONNECT, default=False): _fast_connect_schema,
cv.Optional(CONF_LEASE_CACHE, default=False): _lease_cache_schema,
cv.Optional(CONF_USE_ADDRESS): cv.string_strict,
cv.Optional(CONF_MIN_AUTH_MODE): cv.All(
VALIDATE_WIFI_MIN_AUTH_MODE,
@@ -723,6 +757,14 @@ async def to_code(config):
# 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")
lease_cache = config[CONF_LEASE_CACHE]
if lease_cache[CONF_ENABLED]:
cg.add_define("USE_WIFI_LEASE_CACHE")
# is_in_flash() also emits USE_ESP32_RTC_PREFERENCES when rtc is chosen.
if preferences.is_in_flash(lease_cache[CONF_STORAGE]):
cg.add_define("USE_WIFI_LEASE_CACHE_IN_FLASH")
if (max_age := lease_cache.get(CONF_MAX_AGE)) is not None:
cg.add(var.set_lease_cache_max_age(int(max_age.total_seconds)))
# passive_scan defaults to false in C++ - only set if true
if config[CONF_PASSIVE_SCAN]:
cg.add(var.set_passive_scan(True))
+40 -1
View File
@@ -398,7 +398,6 @@ static constexpr uint32_t WIFI_AP_EXCLUSIVE_DWELL_MS = 300000;
/// disconnect event cannot keep the networks from being tried for good.
static constexpr uint32_t WIFI_AP_EXCLUSIVE_MAX_DWELL_MS = 3 * WIFI_AP_EXCLUSIVE_DWELL_MS;
static constexpr uint32_t WIFI_AP_EXCLUSIVE_HANDOVER_MS = 1000;
static constexpr uint32_t AP_HANDOVER_TIMEOUT_ID = 0;
#endif
/// Cooldown duration when fallback AP is active and captive portal may be running
@@ -702,6 +701,15 @@ void WiFiComponent::start() {
this->fast_connect_pref_ =
global_preferences->make_preference<wifi::SavedWifiFastConnectSettings>(hash + 1, fast_connect_in_flash);
#endif
#ifdef USE_WIFI_LEASE_CACHE
#ifdef USE_WIFI_LEASE_CACHE_IN_FLASH
const bool lease_cache_in_flash = true;
#else
const bool lease_cache_in_flash = false;
#endif
this->lease_cache_pref_ =
global_preferences->make_preference<wifi::SavedWifiLeaseSettings>(hash + 2, lease_cache_in_flash);
#endif
SavedWifiSettings save{};
if (this->pref_.load(&save)) {
@@ -712,6 +720,10 @@ void WiFiComponent::start() {
sta.set_password(save.password);
this->set_sta(sta);
}
#ifdef USE_WIFI_LEASE_CACHE
// After the saved credentials: set_sta() clears the cache, and the lease belongs to that same network
this->load_lease_settings_();
#endif
if (this->has_sta()) {
this->wifi_sta_pre_setup_();
@@ -1149,6 +1161,10 @@ void WiFiComponent::set_sta(const WiFiAP &ap) {
this->selected_sta_index_ = 0;
// When new credentials are set (e.g., from improv), skip cooldown to retry immediately
this->skip_cooldown_next_cycle_ = true;
#ifdef USE_WIFI_LEASE_CACHE
// Index 0 is a different network now, so its cached lease must not be re-applied
this->clear_lease_settings_();
#endif
}
WiFiAP WiFiComponent::build_params_for_current_phase_() {
@@ -1448,6 +1464,9 @@ void WiFiComponent::disable() {
ESP_LOGD(TAG, "Disabling");
this->state_ = WIFI_COMPONENT_STATE_DISABLED;
#ifdef USE_WIFI_LEASE_CACHE
this->cancel_timeout(LEASE_RENEW_TIMEOUT_ID);
#endif
this->wifi_disconnect_();
this->wifi_mode_(false, false);
}
@@ -2440,6 +2459,26 @@ void WiFiComponent::save_fast_connect_settings_(const bssid_t &bssid, uint8_t ch
}
#endif
#ifdef USE_WIFI_LEASE_CACHE
void WiFiComponent::load_lease_settings_() {
if (!this->lease_cache_pref_.load(&this->cached_lease_) || this->cached_lease_.version != SAVED_WIFI_LEASE_VERSION) {
this->cached_lease_ = {};
return;
}
const uint8_t *ip = reinterpret_cast<const uint8_t *>(&this->cached_lease_.ip); // network byte order
ESP_LOGD(TAG, "Loaded cached lease %u.%u.%u.%u (network %d, lease %us)", ip[0], ip[1], ip[2], ip[3],
this->cached_lease_.ap_index, this->cached_lease_.lease_secs);
}
void WiFiComponent::clear_lease_settings_() {
if (this->cached_lease_.version == 0)
return;
this->cached_lease_ = {};
this->lease_cache_pref_.save(&this->cached_lease_);
ESP_LOGD(TAG, "Lease cache: cleared");
}
#endif
WiFiScanResult::WiFiScanResult(const bssid_t &bssid, const char *ssid, size_t ssid_len, uint8_t channel, int8_t rssi,
bool with_auth, bool is_hidden)
: bssid_(bssid),
+42
View File
@@ -86,6 +86,25 @@ struct SavedWifiFastConnectSettings {
int8_t ap_index;
} PACKED; // NOLINT
#ifdef USE_WIFI_LEASE_CACHE
// Checked on load; a zeroed record (version 0) means no cached lease.
static constexpr uint8_t SAVED_WIFI_LEASE_VERSION = 1;
// Cached DHCP lease. IPv4 fields in network byte order, times in seconds.
struct SavedWifiLeaseSettings {
uint8_t version;
int8_t ap_index;
uint32_t ip;
uint32_t netmask;
uint32_t gateway;
uint32_t dns1;
uint32_t dns2;
uint32_t lease_secs;
uint32_t t1_renew;
uint32_t saved_at; // RTC-domain clock at capture
} PACKED; // NOLINT
#endif
enum WiFiComponentState : uint8_t {
/** Nothing has been initialized yet. Internal AP, if configured, is disabled at this point. */
WIFI_COMPONENT_STATE_OFF = 0,
@@ -429,6 +448,10 @@ class WiFiPowerSaveListener {
virtual void on_wifi_power_save(WiFiPowerSaveMode mode) = 0;
};
// Scheduler ids
static constexpr uint32_t AP_HANDOVER_TIMEOUT_ID = 0;
static constexpr uint32_t LEASE_RENEW_TIMEOUT_ID = 1;
/// This component is responsible for managing the ESP WiFi interface.
class WiFiComponent final : public Component {
public:
@@ -565,6 +588,9 @@ class WiFiComponent final : public Component {
int8_t wifi_rssi();
void set_enable_on_boot(bool enable_on_boot) { this->enable_on_boot_ = enable_on_boot; }
#ifdef USE_WIFI_LEASE_CACHE
void set_lease_cache_max_age(uint32_t seconds) { this->lease_cache_max_age_ = seconds; }
#endif
void set_keep_scan_results(bool keep_scan_results) { this->keep_scan_results_ = keep_scan_results; }
void set_post_connect_roaming(bool enabled) { this->post_connect_roaming_ = enabled; }
@@ -813,6 +839,15 @@ class WiFiComponent final : public Component {
void save_fast_connect_settings_(const bssid_t &bssid, uint8_t channel);
#endif
#ifdef USE_WIFI_LEASE_CACHE
void load_lease_settings_();
void save_lease_settings_();
void clear_lease_settings_();
// Cached lease belongs to the selected network and is still provably valid.
bool lease_valid_for_apply_() const;
void renew_cached_lease_();
#endif
// Post-connect roaming methods
void check_roaming_(uint32_t now);
void process_roaming_scan_();
@@ -899,6 +934,13 @@ class WiFiComponent final : public Component {
#ifdef USE_WIFI_FAST_CONNECT
ESPPreferenceObject fast_connect_pref_;
#endif
#ifdef USE_WIFI_LEASE_CACHE
ESPPreferenceObject lease_cache_pref_;
SavedWifiLeaseSettings cached_lease_{}; // version 0 = none
// Current IP came from the cache (DHCP not run); suppresses saving until a real lease arrives.
bool applied_cached_lease_{false};
uint32_t lease_cache_max_age_{0}; // 0 = no cap
#endif
#ifdef USE_WIFI_CONNECT_TRIGGER
Trigger<> connect_trigger_;
#endif
@@ -35,6 +35,12 @@
#include "lwip/apps/sntp.h"
#include "lwip/dns.h"
#include "lwip/err.h"
#ifdef USE_WIFI_LEASE_CACHE
#include <esp_netif_net_stack.h> // esp_netif_get_netif_impl()
#include <esp_rtc_time.h> // esp_rtc_get_time_us() -- RTC-domain clock for lease age
#include "lwip/dhcp.h"
#include "lwip/netif.h"
#endif
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
@@ -553,6 +559,68 @@ bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
return true;
}
// sntp_servermode_dhcp() is an empty macro unless lwIP takes NTP servers from DHCP; that build needs the core lock
static void disable_dhcp_sntp() {
#if SNTP_GET_SERVERS_FROM_DHCP || SNTP_GET_SERVERS_FROM_DHCPV6
LwIPLock lock;
#endif
sntp_servermode_dhcp(false);
}
#ifdef USE_WIFI_LEASE_CACHE
// Minimum lease time that must remain before a cached lease is applied.
static constexpr uint32_t LEASE_APPLY_MARGIN_SECS = 60;
// Seconds since capture on the RTC-domain clock; UINT32_MAX if the clock was reset (age unknown).
static uint32_t lease_cache_age_secs(const SavedWifiLeaseSettings &lease) {
uint32_t now = static_cast<uint32_t>(esp_rtc_get_time_us() / 1000000ULL);
return now >= lease.saved_at ? now - lease.saved_at : UINT32_MAX;
}
// Renewal deadline (T1) from capture; half the lease when T1 is missing or invalid (RFC 2131).
static uint32_t lease_renew_at_secs(const SavedWifiLeaseSettings &lease) {
uint32_t renew_at = lease.t1_renew;
if (renew_at == 0 || renew_at >= lease.lease_secs)
renew_at = lease.lease_secs / 2;
return renew_at;
}
// Starting the DHCP client drops the static address and DNS servers, so open connections reset
void WiFiComponent::renew_cached_lease_() {
ESP_LOGI(TAG, "Cached lease renew deadline reached; re-acquiring the address over DHCP, connections may drop");
this->applied_cached_lease_ = false;
// Same SNTP leak guard as the DHCP path: https://github.com/esphome/issues/issues/2299
disable_dhcp_sntp();
esp_err_t err = esp_netif_dhcpc_start(s_sta_netif);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Lease renew: starting DHCP client failed: %s, retrying in 60 s", esp_err_to_name(err));
this->set_timeout(LEASE_RENEW_TIMEOUT_ID, 60000, [this]() { this->renew_cached_lease_(); });
}
}
bool WiFiComponent::lease_valid_for_apply_() const {
const SavedWifiLeaseSettings &lease = this->cached_lease_;
if (lease.version != SAVED_WIFI_LEASE_VERSION || this->selected_sta_index_ < 0 ||
lease.ap_index != this->selected_sta_index_)
return false;
// The age is only known when the RTC clock survived the reset (deep sleep or software reboot).
esp_reset_reason_t reason = esp_reset_reason();
if (reason != ESP_RST_DEEPSLEEP && reason != ESP_RST_SW)
return false;
uint32_t age = lease_cache_age_secs(lease);
if (age == UINT32_MAX)
return false;
if (this->lease_cache_max_age_ != 0 && age > this->lease_cache_max_age_)
return false;
// Past T1 the lease needs DHCP renewal anyway.
if (age >= lease_renew_at_secs(lease))
return false;
// Scale the margin with the lease: the RTC RC oscillator drifts a few percent.
uint32_t margin = std::max<uint32_t>(LEASE_APPLY_MARGIN_SECS, lease.lease_secs / 16);
return lease.lease_secs > age + margin;
}
#endif
bool WiFiComponent::wifi_sta_ip_config_(const optional<ManualIP> &manual_ip) {
// enable STA
if (!this->wifi_mode_(true, {}))
@@ -571,18 +639,43 @@ bool WiFiComponent::wifi_sta_ip_config_(const optional<ManualIP> &manual_ip) {
return false;
}
if (!manual_ip.has_value()) {
#ifdef USE_WIFI_LEASE_CACHE
// Without a manual IP, a still-valid cached lease is applied as a static IP to skip DHCP.
optional<ManualIP> effective_ip = manual_ip;
this->applied_cached_lease_ = false;
this->cancel_timeout(LEASE_RENEW_TIMEOUT_ID);
if (!effective_ip.has_value() && this->lease_valid_for_apply_()) {
const SavedWifiLeaseSettings &lease = this->cached_lease_;
esp_ip4_addr_t ip4{lease.ip}, mask4{lease.netmask}, gw4{lease.gateway}, dns1_4{lease.dns1}, dns2_4{lease.dns2};
ManualIP cached;
cached.static_ip = network::IPAddress(&ip4);
cached.gateway = network::IPAddress(&gw4);
cached.subnet = network::IPAddress(&mask4);
cached.dns1 = network::IPAddress(&dns1_4);
cached.dns2 = network::IPAddress(&dns2_4);
effective_ip = cached;
this->applied_cached_lease_ = true;
uint32_t age = lease_cache_age_secs(lease);
uint32_t remaining = lease.lease_secs > age ? lease.lease_secs - age : 0;
ESP_LOGI(TAG, "Applying cached lease " IPSTR ", ~%us left, skipping DHCP", IP2STR(&ip4), remaining);
// Still awake at T1: switch to real DHCP. lease_valid_for_apply_() guarantees age < T1.
uint32_t renew_in = lease_renew_at_secs(lease) - age;
// Clamp below UINT32_MAX, which the scheduler treats as "don't run".
uint32_t renew_ms =
static_cast<uint32_t>(std::min<uint64_t>(static_cast<uint64_t>(renew_in) * 1000ULL, UINT32_MAX - 1));
this->set_timeout(LEASE_RENEW_TIMEOUT_ID, renew_ms, [this]() { this->renew_cached_lease_(); });
}
#else
const optional<ManualIP> &effective_ip = manual_ip;
#endif
if (!effective_ip.has_value()) {
// lwIP starts the SNTP client if it gets an SNTP server from DHCP. We don't need the time, and more importantly,
// the built-in SNTP client has a memory leak in certain situations. Disable this feature.
// https://github.com/esphome/issues/issues/2299
{
#if SNTP_GET_SERVERS_FROM_DHCP || SNTP_GET_SERVERS_FROM_DHCPV6
// sntp_servermode_dhcp() is an empty macro unless lwIP is built with
// DHCP-supplied NTP servers, so only that build needs the core lock.
LwIPLock lock;
#endif
sntp_servermode_dhcp(false);
}
disable_dhcp_sntp();
// No manual IP is set; use DHCP client
if (dhcp_status != ESP_NETIF_DHCP_STARTED) {
@@ -596,9 +689,9 @@ bool WiFiComponent::wifi_sta_ip_config_(const optional<ManualIP> &manual_ip) {
}
esp_netif_ip_info_t info; // struct of ip4_addr_t with ip, netmask, gw
info.ip = manual_ip->static_ip;
info.gw = manual_ip->gateway;
info.netmask = manual_ip->subnet;
info.ip = effective_ip->static_ip;
info.gw = effective_ip->gateway;
info.netmask = effective_ip->subnet;
err = esp_netif_dhcpc_stop(s_sta_netif);
if (err != ESP_OK && err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED) {
ESP_LOGV(TAG, "Stopping DHCP client failed: %s", esp_err_to_name(err));
@@ -610,18 +703,82 @@ bool WiFiComponent::wifi_sta_ip_config_(const optional<ManualIP> &manual_ip) {
}
esp_netif_dns_info_t dns;
if (manual_ip->dns1.is_set()) {
dns.ip = manual_ip->dns1;
if (effective_ip->dns1.is_set()) {
dns.ip = effective_ip->dns1;
esp_netif_set_dns_info(s_sta_netif, ESP_NETIF_DNS_MAIN, &dns);
}
if (manual_ip->dns2.is_set()) {
dns.ip = manual_ip->dns2;
if (effective_ip->dns2.is_set()) {
dns.ip = effective_ip->dns2;
esp_netif_set_dns_info(s_sta_netif, ESP_NETIF_DNS_BACKUP, &dns);
}
return true;
}
#ifdef USE_WIFI_LEASE_CACHE
void WiFiComponent::save_lease_settings_() {
// A cached lease applied as a static IP has no DHCP timing to capture.
if (s_sta_netif == nullptr || this->applied_cached_lease_)
return;
#ifdef USE_WIFI_MANUAL_IP
if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value())
return;
#endif
esp_netif_ip_info_t info;
if (esp_netif_get_ip_info(s_sta_netif, &info) != ESP_OK || info.ip.addr == 0)
return;
SavedWifiLeaseSettings lease{};
lease.version = SAVED_WIFI_LEASE_VERSION;
lease.ap_index = this->selected_sta_index_;
lease.ip = info.ip.addr;
lease.netmask = info.netmask.addr;
lease.gateway = info.gw.addr;
esp_netif_dns_info_t dns;
if (esp_netif_get_dns_info(s_sta_netif, ESP_NETIF_DNS_MAIN, &dns) == ESP_OK && dns.ip.type == ESP_IPADDR_TYPE_V4) {
lease.dns1 = dns.ip.u_addr.ip4.addr;
}
if (esp_netif_get_dns_info(s_sta_netif, ESP_NETIF_DNS_BACKUP, &dns) == ESP_OK && dns.ip.type == ESP_IPADDR_TYPE_V4) {
lease.dns2 = dns.ip.u_addr.ip4.addr;
}
// Lease timing is only in lwIP's internal DHCP state; without it the lease can't be aged.
bool have_timing = false;
if (auto *impl = static_cast<struct netif *>(esp_netif_get_netif_impl(s_sta_netif))) {
LwIPLock lock;
if (struct dhcp *d = netif_dhcp_data(impl)) {
lease.lease_secs = d->offered_t0_lease;
lease.t1_renew = d->offered_t1_renew;
have_timing = true;
}
}
if (!have_timing)
return;
lease.saved_at = static_cast<uint32_t>(esp_rtc_get_time_us() / 1000000ULL);
// On flash, skip a write that would only bump saved_at (an older one only overstates the age),
// unless it is from a previous RTC clock period (power cycle).
#ifdef USE_WIFI_LEASE_CACHE_IN_FLASH
constexpr size_t compare_len = offsetof(SavedWifiLeaseSettings, saved_at);
#else
constexpr size_t compare_len = sizeof(SavedWifiLeaseSettings);
#endif
const SavedWifiLeaseSettings &previous = this->cached_lease_;
if (previous.saved_at <= lease.saved_at && memcmp(&previous, &lease, compare_len) == 0)
return;
this->lease_cache_pref_.save(&lease);
this->cached_lease_ = lease;
ESP_LOGD(TAG, "Lease cache: saved " IPSTR " (network %d, lease %us)", IP2STR(&info.ip), lease.ap_index,
lease.lease_secs);
}
#endif
esp_netif_t *WiFiComponent::get_esp_netif_sta() { return s_sta_netif; }
network::IPAddresses WiFiComponent::wifi_sta_ip_addresses() {
@@ -853,9 +1010,17 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
// This ensures wifi.connected condition returns true in listener automations
this->pending_.connect_state = true;
#endif
// For static IP configurations, GOT_IP event may not fire, so notify IP listeners here
#if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP)
if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) {
// For static IP configurations (manual or cached lease), GOT_IP event may not fire, so notify IP listeners here
#if defined(USE_WIFI_IP_STATE_LISTENERS) && (defined(USE_WIFI_MANUAL_IP) || defined(USE_WIFI_LEASE_CACHE))
bool static_ip = false;
#ifdef USE_WIFI_MANUAL_IP
const WiFiAP *config = this->get_selected_sta_();
static_ip = config && config->get_manual_ip().has_value();
#endif
#ifdef USE_WIFI_LEASE_CACHE
static_ip = static_ip || this->applied_cached_lease_;
#endif
if (static_ip) {
this->notify_ip_state_listeners_();
}
#endif
@@ -889,6 +1054,9 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
const auto &it = data->data.ip_got_ip;
ESP_LOGV(TAG, "static_ip=" IPSTR " gateway=" IPSTR, IP2STR(&it.ip_info.ip), IP2STR(&it.ip_info.gw));
this->got_ipv4_address_ = true;
#ifdef USE_WIFI_LEASE_CACHE
this->save_lease_settings_(); // events are drained on the main loop
#endif
#ifdef USE_WIFI_IP_STATE_LISTENERS
this->notify_ip_state_listeners_();
#endif
+2
View File
@@ -464,6 +464,8 @@
#define USE_WIFI_11KV_SUPPORT
#define USE_WIFI_FAST_CONNECT
#define USE_WIFI_FAST_CONNECT_IN_FLASH
#define USE_WIFI_LEASE_CACHE
#define USE_WIFI_LEASE_CACHE_IN_FLASH
#define USE_WIFI_PHY_MODE
#define USE_WIFI_IP_STATE_LISTENERS
#define USE_WIFI_SCAN_RESULTS_LISTENERS
@@ -0,0 +1,41 @@
"""Tests for the lease_cache storage validation."""
import pytest
from esphome import config_validation as cv
from esphome.components.esp32 import KEY_VARIANT
from esphome.components.esp32.const import VARIANT_ESP32, VARIANT_ESP32C2
from esphome.components.wifi import CONFIG_SCHEMA
from esphome.const import PlatformFramework
from tests.component_tests.types import SetCoreConfigCallable
def _validate(lease_cache: dict) -> dict:
return CONFIG_SCHEMA(
{
"ssid": "net",
"password": "secret12",
"use_address": "test.local",
"lease_cache": lease_cache,
}
)
def test_storage_defaults_to_rtc(set_core_config: SetCoreConfigCallable) -> None:
set_core_config(
PlatformFramework.ESP32_IDF, platform_data={KEY_VARIANT: VARIANT_ESP32}
)
config = _validate({"max_age": "12h"})
assert config["lease_cache"]["storage"] == "rtc"
def test_no_rtc_variant_requires_flash(set_core_config: SetCoreConfigCallable) -> None:
"""A variant without RTC memory must say storage: flash; the error points at that key."""
set_core_config(
PlatformFramework.ESP32_IDF, platform_data={KEY_VARIANT: VARIANT_ESP32C2}
)
with pytest.raises(cv.Invalid, match="no RTC memory") as exc:
_validate({"max_age": "12h"})
assert exc.value.path == ["lease_cache", "storage"]
config = _validate({"storage": "flash"})
assert config["lease_cache"]["storage"] == "flash"
@@ -0,0 +1,8 @@
# DHCP lease cache with the default RTC storage and max_age.
wifi:
lease_cache:
enabled: true
max_age: 12h
packages:
wifi: !include common.yaml
@@ -0,0 +1,13 @@
# lease_cache with storage: flash, alongside manual_ip.
wifi:
lease_cache:
enabled: true
storage: flash
manual_ip:
static_ip: 192.168.1.100
gateway: 192.168.1.1
subnet: 255.255.255.0
dns1: 1.1.1.1
packages:
wifi: !include common.yaml