mirror of
https://github.com/esphome/esphome.git
synced 2025-11-18 15:55:46 +00:00
Merge branch 'integration' into memory_api
This commit is contained in:
@@ -72,6 +72,16 @@ def _final_validate(config: ConfigType) -> ConfigType:
|
|||||||
"Add 'ap:' to your WiFi configuration to enable the captive portal."
|
"Add 'ap:' to your WiFi configuration to enable the captive portal."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Register socket needs for DNS server and additional HTTP connections
|
||||||
|
# - 1 UDP socket for DNS server
|
||||||
|
# - 3 additional TCP sockets for captive portal detection probes + configuration requests
|
||||||
|
# OS captive portal detection makes multiple probe requests that stay in TIME_WAIT.
|
||||||
|
# Need headroom for actual user configuration requests.
|
||||||
|
# LRU purging will reclaim idle sockets to prevent exhaustion from repeated attempts.
|
||||||
|
from esphome.components import socket
|
||||||
|
|
||||||
|
socket.consume_sockets(4, "captive_portal")(config)
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -52,8 +52,8 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) {
|
|||||||
ESP_LOGI(TAG, "Requested WiFi Settings Change:");
|
ESP_LOGI(TAG, "Requested WiFi Settings Change:");
|
||||||
ESP_LOGI(TAG, " SSID='%s'", ssid.c_str());
|
ESP_LOGI(TAG, " SSID='%s'", ssid.c_str());
|
||||||
ESP_LOGI(TAG, " Password=" LOG_SECRET("'%s'"), psk.c_str());
|
ESP_LOGI(TAG, " Password=" LOG_SECRET("'%s'"), psk.c_str());
|
||||||
wifi::global_wifi_component->save_wifi_sta(ssid, psk);
|
// Defer save to main loop thread to avoid NVS operations from HTTP thread
|
||||||
wifi::global_wifi_component->start_scanning();
|
this->defer([ssid, psk]() { wifi::global_wifi_component->save_wifi_sta(ssid, psk); });
|
||||||
request->redirect(ESPHOME_F("/?save"));
|
request->redirect(ESPHOME_F("/?save"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,6 +65,12 @@ void CaptivePortal::start() {
|
|||||||
this->base_->init();
|
this->base_->init();
|
||||||
if (!this->initialized_) {
|
if (!this->initialized_) {
|
||||||
this->base_->add_handler(this);
|
this->base_->add_handler(this);
|
||||||
|
#ifdef USE_ESP32
|
||||||
|
// Enable LRU socket purging to handle captive portal detection probe bursts
|
||||||
|
// OS captive portal detection makes many simultaneous HTTP requests which can
|
||||||
|
// exhaust sockets. LRU purging automatically closes oldest idle connections.
|
||||||
|
this->base_->get_server()->set_lru_purge_enable(true);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip();
|
network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip();
|
||||||
|
|||||||
@@ -40,6 +40,10 @@ class CaptivePortal : public AsyncWebHandler, public Component {
|
|||||||
void end() {
|
void end() {
|
||||||
this->active_ = false;
|
this->active_ = false;
|
||||||
this->disable_loop(); // Stop processing DNS requests
|
this->disable_loop(); // Stop processing DNS requests
|
||||||
|
#ifdef USE_ESP32
|
||||||
|
// Disable LRU socket purging now that captive portal is done
|
||||||
|
this->base_->get_server()->set_lru_purge_enable(false);
|
||||||
|
#endif
|
||||||
this->base_->deinit();
|
this->base_->deinit();
|
||||||
if (this->dns_server_ != nullptr) {
|
if (this->dns_server_ != nullptr) {
|
||||||
this->dns_server_->stop();
|
this->dns_server_->stop();
|
||||||
|
|||||||
@@ -94,6 +94,18 @@ void AsyncWebServer::end() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AsyncWebServer::set_lru_purge_enable(bool enable) {
|
||||||
|
if (this->lru_purge_enable_ == enable) {
|
||||||
|
return; // No change needed
|
||||||
|
}
|
||||||
|
this->lru_purge_enable_ = enable;
|
||||||
|
// If server is already running, restart it with new config
|
||||||
|
if (this->server_) {
|
||||||
|
this->end();
|
||||||
|
this->begin();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void AsyncWebServer::begin() {
|
void AsyncWebServer::begin() {
|
||||||
if (this->server_) {
|
if (this->server_) {
|
||||||
this->end();
|
this->end();
|
||||||
@@ -101,6 +113,8 @@ void AsyncWebServer::begin() {
|
|||||||
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||||
config.server_port = this->port_;
|
config.server_port = this->port_;
|
||||||
config.uri_match_fn = [](const char * /*unused*/, const char * /*unused*/, size_t /*unused*/) { return true; };
|
config.uri_match_fn = [](const char * /*unused*/, const char * /*unused*/, size_t /*unused*/) { return true; };
|
||||||
|
// Enable LRU purging if requested (e.g., by captive portal to handle probe bursts)
|
||||||
|
config.lru_purge_enable = this->lru_purge_enable_;
|
||||||
if (httpd_start(&this->server_, &config) == ESP_OK) {
|
if (httpd_start(&this->server_, &config) == ESP_OK) {
|
||||||
const httpd_uri_t handler_get = {
|
const httpd_uri_t handler_get = {
|
||||||
.uri = "",
|
.uri = "",
|
||||||
@@ -242,6 +256,7 @@ void AsyncWebServerRequest::send(int code, const char *content_type, const char
|
|||||||
void AsyncWebServerRequest::redirect(const std::string &url) {
|
void AsyncWebServerRequest::redirect(const std::string &url) {
|
||||||
httpd_resp_set_status(*this, "302 Found");
|
httpd_resp_set_status(*this, "302 Found");
|
||||||
httpd_resp_set_hdr(*this, "Location", url.c_str());
|
httpd_resp_set_hdr(*this, "Location", url.c_str());
|
||||||
|
httpd_resp_set_hdr(*this, "Connection", "close");
|
||||||
httpd_resp_send(*this, nullptr, 0);
|
httpd_resp_send(*this, nullptr, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -199,9 +199,13 @@ class AsyncWebServer {
|
|||||||
return *handler;
|
return *handler;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void set_lru_purge_enable(bool enable);
|
||||||
|
httpd_handle_t get_server() { return this->server_; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
uint16_t port_{};
|
uint16_t port_{};
|
||||||
httpd_handle_t server_{};
|
httpd_handle_t server_{};
|
||||||
|
bool lru_purge_enable_{false};
|
||||||
static esp_err_t request_handler(httpd_req_t *r);
|
static esp_err_t request_handler(httpd_req_t *r);
|
||||||
static esp_err_t request_post_handler(httpd_req_t *r);
|
static esp_err_t request_post_handler(httpd_req_t *r);
|
||||||
esp_err_t request_handler_(AsyncWebServerRequest *request) const;
|
esp_err_t request_handler_(AsyncWebServerRequest *request) const;
|
||||||
|
|||||||
@@ -69,6 +69,12 @@ CONF_MIN_AUTH_MODE = "min_auth_mode"
|
|||||||
# Limited to 127 because selected_sta_index_ is int8_t in C++
|
# Limited to 127 because selected_sta_index_ is int8_t in C++
|
||||||
MAX_WIFI_NETWORKS = 127
|
MAX_WIFI_NETWORKS = 127
|
||||||
|
|
||||||
|
# Default AP timeout - allows sufficient time to try all BSSIDs during initial connection
|
||||||
|
# After AP starts, WiFi scanning is skipped to avoid disrupting the AP, so we only
|
||||||
|
# get best-effort connection attempts. Longer timeout ensures we exhaust all options
|
||||||
|
# before falling back to AP mode.
|
||||||
|
DEFAULT_AP_TIMEOUT = "2min"
|
||||||
|
|
||||||
wifi_ns = cg.esphome_ns.namespace("wifi")
|
wifi_ns = cg.esphome_ns.namespace("wifi")
|
||||||
EAPAuth = wifi_ns.struct("EAPAuth")
|
EAPAuth = wifi_ns.struct("EAPAuth")
|
||||||
ManualIP = wifi_ns.struct("ManualIP")
|
ManualIP = wifi_ns.struct("ManualIP")
|
||||||
@@ -177,7 +183,7 @@ CONF_AP_TIMEOUT = "ap_timeout"
|
|||||||
WIFI_NETWORK_AP = WIFI_NETWORK_BASE.extend(
|
WIFI_NETWORK_AP = WIFI_NETWORK_BASE.extend(
|
||||||
{
|
{
|
||||||
cv.Optional(
|
cv.Optional(
|
||||||
CONF_AP_TIMEOUT, default="1min"
|
CONF_AP_TIMEOUT, default=DEFAULT_AP_TIMEOUT
|
||||||
): cv.positive_time_period_milliseconds,
|
): cv.positive_time_period_milliseconds,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -199,7 +199,12 @@ static constexpr uint8_t WIFI_RETRY_COUNT_PER_AP = 1;
|
|||||||
|
|
||||||
/// Cooldown duration in milliseconds after adapter restart or repeated failures
|
/// Cooldown duration in milliseconds after adapter restart or repeated failures
|
||||||
/// Allows WiFi hardware to stabilize before next connection attempt
|
/// Allows WiFi hardware to stabilize before next connection attempt
|
||||||
static constexpr uint32_t WIFI_COOLDOWN_DURATION_MS = 1000;
|
static constexpr uint32_t WIFI_COOLDOWN_DURATION_MS = 500;
|
||||||
|
|
||||||
|
/// Cooldown duration when fallback AP is active and captive portal may be running
|
||||||
|
/// Longer interval gives users time to configure WiFi without constant connection attempts
|
||||||
|
/// While connecting, WiFi can't beacon the AP properly, so needs longer cooldown
|
||||||
|
static constexpr uint32_t WIFI_COOLDOWN_WITH_AP_ACTIVE_MS = 30000;
|
||||||
|
|
||||||
static constexpr uint8_t get_max_retries_for_phase(WiFiRetryPhase phase) {
|
static constexpr uint8_t get_max_retries_for_phase(WiFiRetryPhase phase) {
|
||||||
switch (phase) {
|
switch (phase) {
|
||||||
@@ -418,10 +423,6 @@ void WiFiComponent::start() {
|
|||||||
void WiFiComponent::restart_adapter() {
|
void WiFiComponent::restart_adapter() {
|
||||||
ESP_LOGW(TAG, "Restarting adapter");
|
ESP_LOGW(TAG, "Restarting adapter");
|
||||||
this->wifi_mode_(false, {});
|
this->wifi_mode_(false, {});
|
||||||
// Enter cooldown state to allow WiFi hardware to stabilize after restart
|
|
||||||
// Don't set retry_phase_ or num_retried_ here - state machine handles transitions
|
|
||||||
this->state_ = WIFI_COMPONENT_STATE_COOLDOWN;
|
|
||||||
this->action_started_ = millis();
|
|
||||||
this->error_from_callback_ = false;
|
this->error_from_callback_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -442,7 +443,10 @@ void WiFiComponent::loop() {
|
|||||||
switch (this->state_) {
|
switch (this->state_) {
|
||||||
case WIFI_COMPONENT_STATE_COOLDOWN: {
|
case WIFI_COMPONENT_STATE_COOLDOWN: {
|
||||||
this->status_set_warning(LOG_STR("waiting to reconnect"));
|
this->status_set_warning(LOG_STR("waiting to reconnect"));
|
||||||
if (now - this->action_started_ > WIFI_COOLDOWN_DURATION_MS) {
|
// Use longer cooldown when captive portal/improv is active to avoid disrupting user config
|
||||||
|
bool portal_active = this->is_captive_portal_active_() || this->is_esp32_improv_active_();
|
||||||
|
uint32_t cooldown_duration = portal_active ? WIFI_COOLDOWN_WITH_AP_ACTIVE_MS : WIFI_COOLDOWN_DURATION_MS;
|
||||||
|
if (now - this->action_started_ > cooldown_duration) {
|
||||||
// After cooldown we either restarted the adapter because of
|
// After cooldown we either restarted the adapter because of
|
||||||
// a failure, or something tried to connect over and over
|
// a failure, or something tried to connect over and over
|
||||||
// so we entered cooldown. In both cases we call
|
// so we entered cooldown. In both cases we call
|
||||||
@@ -667,6 +671,21 @@ void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &pa
|
|||||||
sta.set_ssid(ssid);
|
sta.set_ssid(ssid);
|
||||||
sta.set_password(password);
|
sta.set_password(password);
|
||||||
this->set_sta(sta);
|
this->set_sta(sta);
|
||||||
|
|
||||||
|
// Force scan on next attempt even if captive portal is still active
|
||||||
|
// This ensures new credentials are tried with proper BSSID selection after provisioning
|
||||||
|
this->force_scan_after_provision_ = true;
|
||||||
|
|
||||||
|
// Trigger connection attempt (exits cooldown if needed, no-op if already connecting/connected)
|
||||||
|
this->connect_soon_();
|
||||||
|
}
|
||||||
|
|
||||||
|
void WiFiComponent::connect_soon_() {
|
||||||
|
// Only trigger retry if we're in cooldown - if already connecting/connected, do nothing
|
||||||
|
if (this->state_ == WIFI_COMPONENT_STATE_COOLDOWN) {
|
||||||
|
ESP_LOGD(TAG, "Exiting cooldown early due to new WiFi credentials");
|
||||||
|
this->retry_connect();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void WiFiComponent::start_connecting(const WiFiAP &ap) {
|
void WiFiComponent::start_connecting(const WiFiAP &ap) {
|
||||||
@@ -866,6 +885,8 @@ void WiFiComponent::start_scanning() {
|
|||||||
ESP_LOGD(TAG, "Starting scan");
|
ESP_LOGD(TAG, "Starting scan");
|
||||||
this->wifi_scan_start_(this->passive_scan_);
|
this->wifi_scan_start_(this->passive_scan_);
|
||||||
this->state_ = WIFI_COMPONENT_STATE_STA_SCANNING;
|
this->state_ = WIFI_COMPONENT_STATE_STA_SCANNING;
|
||||||
|
// Clear the force scan flag after starting the scan
|
||||||
|
this->force_scan_after_provision_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Comparator for WiFi scan result sorting - determines which network should be tried first
|
/// Comparator for WiFi scan result sorting - determines which network should be tried first
|
||||||
@@ -1231,9 +1252,18 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() {
|
|||||||
return WiFiRetryPhase::RESTARTING_ADAPTER;
|
return WiFiRetryPhase::RESTARTING_ADAPTER;
|
||||||
|
|
||||||
case WiFiRetryPhase::RESTARTING_ADAPTER:
|
case WiFiRetryPhase::RESTARTING_ADAPTER:
|
||||||
// After restart, go back to explicit hidden if we went through it initially, otherwise scan
|
// After restart, go back to explicit hidden if we went through it initially
|
||||||
return this->went_through_explicit_hidden_phase_() ? WiFiRetryPhase::EXPLICIT_HIDDEN
|
if (this->went_through_explicit_hidden_phase_()) {
|
||||||
: WiFiRetryPhase::SCAN_CONNECTING;
|
return WiFiRetryPhase::EXPLICIT_HIDDEN;
|
||||||
|
}
|
||||||
|
// Skip scanning when captive portal/improv is active to avoid disrupting AP
|
||||||
|
// Even passive scans can cause brief AP disconnections on ESP32
|
||||||
|
// UNLESS new credentials were just provisioned - then we need to scan
|
||||||
|
if ((this->is_captive_portal_active_() || this->is_esp32_improv_active_()) &&
|
||||||
|
!this->force_scan_after_provision_) {
|
||||||
|
return WiFiRetryPhase::RETRY_HIDDEN;
|
||||||
|
}
|
||||||
|
return WiFiRetryPhase::SCAN_CONNECTING;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should never reach here
|
// Should never reach here
|
||||||
@@ -1321,6 +1351,10 @@ bool WiFiComponent::transition_to_phase_(WiFiRetryPhase new_phase) {
|
|||||||
if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_()) {
|
if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_()) {
|
||||||
this->restart_adapter();
|
this->restart_adapter();
|
||||||
}
|
}
|
||||||
|
// Always enter cooldown after restart (or skip-restart) to allow stabilization
|
||||||
|
// Use extended cooldown when AP is active to avoid constant scanning that blocks DNS
|
||||||
|
this->state_ = WIFI_COMPONENT_STATE_COOLDOWN;
|
||||||
|
this->action_started_ = millis();
|
||||||
// Return true to indicate we should wait (go to COOLDOWN) instead of immediately connecting
|
// Return true to indicate we should wait (go to COOLDOWN) instead of immediately connecting
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
|||||||
@@ -291,6 +291,7 @@ class WiFiComponent : public Component {
|
|||||||
void set_passive_scan(bool passive);
|
void set_passive_scan(bool passive);
|
||||||
|
|
||||||
void save_wifi_sta(const std::string &ssid, const std::string &password);
|
void save_wifi_sta(const std::string &ssid, const std::string &password);
|
||||||
|
|
||||||
// ========== INTERNAL METHODS ==========
|
// ========== INTERNAL METHODS ==========
|
||||||
// (In most use cases you won't need these)
|
// (In most use cases you won't need these)
|
||||||
/// Setup WiFi interface.
|
/// Setup WiFi interface.
|
||||||
@@ -424,6 +425,8 @@ class WiFiComponent : public Component {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void connect_soon_();
|
||||||
|
|
||||||
void wifi_loop_();
|
void wifi_loop_();
|
||||||
bool wifi_mode_(optional<bool> sta, optional<bool> ap);
|
bool wifi_mode_(optional<bool> sta, optional<bool> ap);
|
||||||
bool wifi_sta_pre_setup_();
|
bool wifi_sta_pre_setup_();
|
||||||
@@ -529,6 +532,7 @@ class WiFiComponent : public Component {
|
|||||||
bool enable_on_boot_;
|
bool enable_on_boot_;
|
||||||
bool got_ipv4_address_{false};
|
bool got_ipv4_address_{false};
|
||||||
bool keep_scan_results_{false};
|
bool keep_scan_results_{false};
|
||||||
|
bool force_scan_after_provision_{false};
|
||||||
|
|
||||||
// Pointers at the end (naturally aligned)
|
// Pointers at the end (naturally aligned)
|
||||||
Trigger<> *connect_trigger_{new Trigger<>()};
|
Trigger<> *connect_trigger_{new Trigger<>()};
|
||||||
|
|||||||
Reference in New Issue
Block a user