From 20f2d409f77f2bd71fda1b701ddffcdb590ef443 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 11:06:57 -0600 Subject: [PATCH 01/65] wip --- esphome/components/wifi/wifi_component.cpp | 165 ++++++++++++++------- esphome/components/wifi/wifi_component.h | 18 ++- 2 files changed, 125 insertions(+), 58 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 51b5756f29..1c7b810772 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -111,10 +111,12 @@ void WiFiComponent::start() { #ifdef USE_WIFI_FAST_CONNECT this->trying_loaded_ap_ = this->load_fast_connect_settings_(); if (!this->trying_loaded_ap_) { + // Fast connect failed - start from first configured AP without scan result this->ap_index_ = 0; - this->selected_ap_ = this->sta_[this->ap_index_]; + this->selected_ap_index_ = 0; + this->selected_scan_index_ = -1; } - this->start_connecting(this->selected_ap_, false); + this->start_connecting_to_selected_(false); #else this->start_scanning(); #endif @@ -170,14 +172,12 @@ void WiFiComponent::loop() { if (millis() - this->action_started_ > 5000) { #ifdef USE_WIFI_FAST_CONNECT // NOTE: This check may not make sense here as it could interfere with AP cycling - if (!this->selected_ap_.get_bssid().has_value()) - this->selected_ap_ = this->sta_[0]; - this->start_connecting(this->selected_ap_, false); + this->reset_selected_ap_to_first_if_invalid_(); + this->start_connecting_to_selected_(false); #else if (this->retry_hidden_) { - if (!this->selected_ap_.get_bssid().has_value()) - this->selected_ap_ = this->sta_[0]; - this->start_connecting(this->selected_ap_, false); + this->reset_selected_ap_to_first_if_invalid_(); + this->start_connecting_to_selected_(false); } else { this->start_scanning(); } @@ -336,8 +336,61 @@ void WiFiComponent::set_sta(const WiFiAP &ap) { this->clear_sta(); this->init_sta(1); this->add_sta(ap); + this->selected_ap_index_ = 0; + this->selected_scan_index_ = -1; +} +void WiFiComponent::clear_sta() { + this->sta_.clear(); + this->selected_ap_index_ = -1; + this->selected_scan_index_ = -1; +} + +WiFiAP WiFiComponent::build_selected_ap_() const { + WiFiAP params; + + if (this->selected_ap_index_ >= 0 && this->selected_ap_index_ < this->sta_.size()) { + const WiFiAP &config = this->sta_[this->selected_ap_index_]; + + // Copy config data + params.set_password(config.get_password()); + params.set_manual_ip(config.get_manual_ip()); + params.set_priority(config.get_priority()); + +#ifdef USE_WIFI_WPA2_EAP + params.set_eap(config.get_eap()); +#endif + + // Use config SSID for hidden networks + if (config.get_hidden()) { + params.set_hidden(true); + params.set_ssid(config.get_ssid()); + } + } + + // Overlay scan result data (if available) + if (this->selected_scan_index_ >= 0 && this->selected_scan_index_ < this->scan_result_.size()) { + const WiFiScanResult &scan = this->scan_result_[this->selected_scan_index_]; + + if (!params.get_hidden()) { + // For visible networks, use scan data to limit connection to exactly this network + // (network selection is done during scan phase). + params.set_ssid(scan.get_ssid()); + params.set_bssid(scan.get_bssid()); + params.set_channel(scan.get_channel()); + } + // For hidden networks, don't use scan BSSID/channel - there might be multiple hidden networks + // and we can't know which one is correct. Rely on probe-req with just SSID. + } + + return params; +} + +WiFiAP WiFiComponent::get_sta() { + if (this->selected_ap_index_ >= 0 && this->selected_ap_index_ < this->sta_.size()) { + return this->sta_[this->selected_ap_index_]; + } + return WiFiAP{}; } -void WiFiComponent::clear_sta() { this->sta_.clear(); } void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &password) { SavedWifiSettings save{}; // zero-initialized - all bytes set to \0, guaranteeing null termination strncpy(save.ssid, ssid.c_str(), sizeof(save.ssid) - 1); // max 32 chars, byte 32 remains \0 @@ -485,8 +538,11 @@ void WiFiComponent::print_connect_params_() { LOG_STR_ARG(get_signal_bars(rssi)), get_wifi_channel(), wifi_subnet_mask_().str().c_str(), wifi_gateway_ip_().str().c_str(), wifi_dns_ip_(0).str().c_str(), wifi_dns_ip_(1).str().c_str()); #ifdef ESPHOME_LOG_HAS_VERBOSE - if (this->selected_ap_.get_bssid().has_value()) { - ESP_LOGV(TAG, " Priority: %.1f", this->get_sta_priority(*this->selected_ap_.get_bssid())); + if (this->selected_ap_index_ >= 0 && this->selected_ap_index_ < this->sta_.size()) { + const WiFiAP &config = this->sta_[this->selected_ap_index_]; + if (config.get_bssid().has_value()) { + ESP_LOGV(TAG, " Priority: %.1f", this->get_sta_priority(*config.get_bssid())); + } } #endif #ifdef USE_WIFI_11KV_SUPPORT @@ -639,49 +695,23 @@ void WiFiComponent::check_scanning_finished() { return; } - // Build connection params directly into selected_ap_ to avoid extra copy + // Find matching config and set indices for on-demand connection params building const WiFiScanResult &scan_res = this->scan_result_[0]; - WiFiAP &selected = this->selected_ap_; - for (auto &config : this->sta_) { + this->selected_scan_index_ = 0; + + for (size_t i = 0; i < this->sta_.size(); i++) { // search for matching STA config, at least one will match (from checks before) - if (!scan_res.matches(config)) { + if (!scan_res.matches(this->sta_[i])) { continue; } - if (config.get_hidden()) { - // selected network is hidden, we use the data from the config - selected.set_hidden(true); - selected.set_ssid(config.get_ssid()); - // Clear channel and BSSID for hidden networks - there might be multiple hidden networks - // but we can't know which one is the correct one. Rely on probe-req with just SSID. - selected.set_channel(0); - selected.set_bssid(optional{}); - } else { - // selected network is visible, we use the data from the scan - // limit the connect params to only connect to exactly this network - // (network selection is done during scan phase). - selected.set_hidden(false); - selected.set_ssid(scan_res.get_ssid()); - selected.set_channel(scan_res.get_channel()); - selected.set_bssid(scan_res.get_bssid()); - } - // copy manual IP (if set) - selected.set_manual_ip(config.get_manual_ip()); - -#ifdef USE_WIFI_WPA2_EAP - // copy EAP parameters (if set) - selected.set_eap(config.get_eap()); -#endif - - // copy password (if set) - selected.set_password(config.get_password()); - + this->selected_ap_index_ = i; break; } yield(); - this->start_connecting(this->selected_ap_, false); + this->start_connecting_to_selected_(false); } void WiFiComponent::dump_config() { @@ -701,8 +731,11 @@ void WiFiComponent::check_connecting_finished() { ESP_LOGI(TAG, "Connected"); // We won't retry hidden networks unless a reconnect fails more than three times again - if (this->retry_hidden_ && !this->selected_ap_.get_hidden()) - ESP_LOGW(TAG, "Network '%s' should be marked as hidden", this->selected_ap_.get_ssid().c_str()); + if (this->retry_hidden_ && this->selected_ap_index_ >= 0 && this->selected_ap_index_ < this->sta_.size()) { + const WiFiAP &config = this->sta_[this->selected_ap_index_]; + if (!config.get_hidden()) + ESP_LOGW(TAG, "Network '%s' should be marked as hidden", config.get_ssid().c_str()); + } this->retry_hidden_ = false; this->print_connect_params_(); @@ -772,10 +805,13 @@ void WiFiComponent::check_connecting_finished() { } void WiFiComponent::retry_connect() { - if (this->selected_ap_.get_bssid()) { - auto bssid = *this->selected_ap_.get_bssid(); - float priority = this->get_sta_priority(bssid); - this->set_sta_priority(bssid, priority - 1.0f); + if (this->selected_ap_index_ >= 0 && this->selected_ap_index_ < this->sta_.size()) { + const WiFiAP &config = this->sta_[this->selected_ap_index_]; + if (config.get_bssid()) { + auto bssid = *config.get_bssid(); + float priority = this->get_sta_priority(bssid); + this->set_sta_priority(bssid, priority - 1.0f); + } } delay(10); @@ -794,7 +830,8 @@ void WiFiComponent::retry_connect() { this->ap_index_++; } this->num_retried_ = 0; - this->selected_ap_ = this->sta_[this->ap_index_]; + this->selected_ap_index_ = this->ap_index_; + this->selected_scan_index_ = -1; #else if (this->num_retried_ > 5) { // If retry failed for more than 5 times, let's restart STA @@ -813,7 +850,7 @@ void WiFiComponent::retry_connect() { if (this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTING) { yield(); this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING_2; - this->start_connecting(this->selected_ap_, true); + this->start_connecting_to_selected_(true); return; } @@ -856,12 +893,19 @@ bool WiFiComponent::load_fast_connect_settings_() { SavedWifiFastConnectSettings fast_connect_save{}; if (this->fast_connect_pref_.load(&fast_connect_save)) { + // Load BSSID from saved settings bssid_t bssid{}; std::copy(fast_connect_save.bssid, fast_connect_save.bssid + 6, bssid.begin()); + + // Create a temporary scan result with the fast connect BSSID and channel + this->scan_result_.init(1); + WiFiScanResult fast_connect_scan(bssid, "", fast_connect_save.channel, 0, false, false); + this->scan_result_.push_back(fast_connect_scan); + + // Set indices to use the loaded AP config and temporary scan result this->ap_index_ = fast_connect_save.ap_index; - this->selected_ap_ = this->sta_[this->ap_index_]; - this->selected_ap_.set_bssid(bssid); - this->selected_ap_.set_channel(fast_connect_save.channel); + this->selected_ap_index_ = this->ap_index_; + this->selected_scan_index_ = 0; ESP_LOGD(TAG, "Loaded fast_connect settings"); return true; @@ -874,7 +918,16 @@ void WiFiComponent::save_fast_connect_settings_() { bssid_t bssid = wifi_bssid(); uint8_t channel = get_wifi_channel(); - if (bssid != this->selected_ap_.get_bssid() || channel != this->selected_ap_.get_channel()) { + // Check if we need to save (compare with current scan result if available) + bool should_save = true; + if (this->selected_scan_index_ >= 0 && this->selected_scan_index_ < this->scan_result_.size()) { + const WiFiScanResult &scan = this->scan_result_[this->selected_scan_index_]; + if (bssid == scan.get_bssid() && channel == scan.get_channel()) { + should_save = false; + } + } + + if (should_save) { SavedWifiFastConnectSettings fast_connect_save{}; memcpy(fast_connect_save.bssid, bssid.data(), 6); diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index ac63e0eb0c..93b5a2d0a2 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -218,7 +218,7 @@ class WiFiComponent : public Component { WiFiComponent(); void set_sta(const WiFiAP &ap); - WiFiAP get_sta() { return this->selected_ap_; } + WiFiAP get_sta(); void init_sta(size_t count); void add_sta(const WiFiAP &ap); void clear_sta(); @@ -337,6 +337,19 @@ class WiFiComponent : public Component { #endif // USE_WIFI_AP void print_connect_params_(); + WiFiAP build_selected_ap_() const; + + void reset_selected_ap_to_first_if_invalid_() { + if (this->selected_ap_index_ < 0 || this->selected_ap_index_ >= this->sta_.size()) { + this->selected_ap_index_ = this->sta_.empty() ? -1 : 0; + this->selected_scan_index_ = -1; + } + } + + void start_connecting_to_selected_(bool two) { + WiFiAP connection_params = this->build_selected_ap_(); + this->start_connecting(connection_params, two); + } void wifi_loop_(); bool wifi_mode_(optional sta, optional ap); @@ -396,7 +409,6 @@ class WiFiComponent : public Component { FixedVector sta_; std::vector sta_priorities_; wifi_scan_vector_t scan_result_; - WiFiAP selected_ap_; WiFiAP ap_; optional output_power_; ESPPreferenceObject pref_; @@ -417,6 +429,8 @@ class WiFiComponent : public Component { #ifdef USE_WIFI_FAST_CONNECT uint8_t ap_index_{0}; #endif + int8_t selected_ap_index_{-1}; + int8_t selected_scan_index_{-1}; #if USE_NETWORK_IPV6 uint8_t num_ipv6_addresses_{0}; #endif /* USE_NETWORK_IPV6 */ From 378e591e70d5a19ebf0d415d32c5650b426b205b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 11:11:58 -0600 Subject: [PATCH 02/65] preen --- esphome/components/wifi/wifi_component.cpp | 15 ++++++--------- esphome/components/wifi/wifi_component.h | 3 --- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 1c7b810772..2ede813289 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -112,7 +112,6 @@ void WiFiComponent::start() { this->trying_loaded_ap_ = this->load_fast_connect_settings_(); if (!this->trying_loaded_ap_) { // Fast connect failed - start from first configured AP without scan result - this->ap_index_ = 0; this->selected_ap_index_ = 0; this->selected_scan_index_ = -1; } @@ -820,17 +819,16 @@ void WiFiComponent::retry_connect() { #ifdef USE_WIFI_FAST_CONNECT if (this->trying_loaded_ap_) { this->trying_loaded_ap_ = false; - this->ap_index_ = 0; // Retry from the first configured AP - } else if (this->ap_index_ >= this->sta_.size() - 1) { + this->selected_ap_index_ = 0; // Retry from the first configured AP + } else if (this->selected_ap_index_ >= static_cast(this->sta_.size()) - 1) { ESP_LOGW(TAG, "No more APs to try"); - this->ap_index_ = 0; + this->selected_ap_index_ = 0; this->restart_adapter(); } else { // Try next AP - this->ap_index_++; + this->selected_ap_index_++; } this->num_retried_ = 0; - this->selected_ap_index_ = this->ap_index_; this->selected_scan_index_ = -1; #else if (this->num_retried_ > 5) { @@ -903,8 +901,7 @@ bool WiFiComponent::load_fast_connect_settings_() { this->scan_result_.push_back(fast_connect_scan); // Set indices to use the loaded AP config and temporary scan result - this->ap_index_ = fast_connect_save.ap_index; - this->selected_ap_index_ = this->ap_index_; + this->selected_ap_index_ = fast_connect_save.ap_index; this->selected_scan_index_ = 0; ESP_LOGD(TAG, "Loaded fast_connect settings"); @@ -932,7 +929,7 @@ void WiFiComponent::save_fast_connect_settings_() { memcpy(fast_connect_save.bssid, bssid.data(), 6); fast_connect_save.channel = channel; - fast_connect_save.ap_index = this->ap_index_; + fast_connect_save.ap_index = this->selected_ap_index_ >= 0 ? this->selected_ap_index_ : 0; this->fast_connect_pref_.save(&fast_connect_save); diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 93b5a2d0a2..ca500a5246 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -426,9 +426,6 @@ class WiFiComponent : public Component { WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF}; WiFiPowerSaveMode power_save_{WIFI_POWER_SAVE_NONE}; uint8_t num_retried_{0}; -#ifdef USE_WIFI_FAST_CONNECT - uint8_t ap_index_{0}; -#endif int8_t selected_ap_index_{-1}; int8_t selected_scan_index_{-1}; #if USE_NETWORK_IPV6 From 13ee597ce04daf215f72a1687cdfc13cea21f4f4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 11:17:17 -0600 Subject: [PATCH 03/65] preen --- esphome/components/wifi/wifi_component.cpp | 20 ++++++++------------ esphome/components/wifi/wifi_component.h | 4 ++++ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 2ede813289..96dc421e7f 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -915,26 +915,22 @@ void WiFiComponent::save_fast_connect_settings_() { bssid_t bssid = wifi_bssid(); uint8_t channel = get_wifi_channel(); - // Check if we need to save (compare with current scan result if available) - bool should_save = true; + // Skip save if settings haven't changed (compare with current scan result if available) if (this->selected_scan_index_ >= 0 && this->selected_scan_index_ < this->scan_result_.size()) { const WiFiScanResult &scan = this->scan_result_[this->selected_scan_index_]; if (bssid == scan.get_bssid() && channel == scan.get_channel()) { - should_save = false; + return; // No change, nothing to save } } - if (should_save) { - SavedWifiFastConnectSettings fast_connect_save{}; + SavedWifiFastConnectSettings fast_connect_save{}; + memcpy(fast_connect_save.bssid, bssid.data(), 6); + fast_connect_save.channel = channel; + fast_connect_save.ap_index = this->selected_ap_index_ >= 0 ? this->selected_ap_index_ : 0; - memcpy(fast_connect_save.bssid, bssid.data(), 6); - fast_connect_save.channel = channel; - fast_connect_save.ap_index = this->selected_ap_index_ >= 0 ? this->selected_ap_index_ : 0; + this->fast_connect_pref_.save(&fast_connect_save); - this->fast_connect_pref_.save(&fast_connect_save); - - ESP_LOGD(TAG, "Saved fast_connect settings"); - } + ESP_LOGD(TAG, "Saved fast_connect settings"); } #endif diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index ca500a5246..155c1f419e 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -426,7 +426,11 @@ class WiFiComponent : public Component { WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF}; WiFiPowerSaveMode power_save_{WIFI_POWER_SAVE_NONE}; uint8_t num_retried_{0}; + // Index into sta_ array for the currently selected AP configuration (-1 = none selected) + // Used to access password, manual_ip, priority, EAP settings, and hidden flag int8_t selected_ap_index_{-1}; + // Index into scan_result_ array for the currently selected scan result (-1 = no scan data) + // Used to access scanned SSID, BSSID, and channel. Also used for fast connect (synthetic scan result) int8_t selected_scan_index_{-1}; #if USE_NETWORK_IPV6 uint8_t num_ipv6_addresses_{0}; From 34317ab343afbf7cea1bf173dbde9ca06943aba1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 11:21:44 -0600 Subject: [PATCH 04/65] preen --- esphome/components/wifi/wifi_component.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 96dc421e7f..4f0f37fed0 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -363,6 +363,11 @@ WiFiAP WiFiComponent::build_selected_ap_() const { if (config.get_hidden()) { params.set_hidden(true); params.set_ssid(config.get_ssid()); + // Clear BSSID and channel for hidden networks - there might be multiple hidden networks + // and we can't know which one is correct. Rely on probe-req with just SSID. + // Leaving channel empty triggers ALL_CHANNEL_SCAN instead of FAST_SCAN. + params.set_bssid(optional{}); + params.set_channel(optional{}); } } @@ -377,8 +382,6 @@ WiFiAP WiFiComponent::build_selected_ap_() const { params.set_bssid(scan.get_bssid()); params.set_channel(scan.get_channel()); } - // For hidden networks, don't use scan BSSID/channel - there might be multiple hidden networks - // and we can't know which one is correct. Rely on probe-req with just SSID. } return params; From 4500006aab9a97d15621b9f80236dbbd12c937b3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 11:32:06 -0600 Subject: [PATCH 05/65] preen --- esphome/components/wifi/wifi_component.cpp | 40 +++++++++++----------- esphome/components/wifi/wifi_component.h | 6 ++-- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 4f0f37fed0..976b76b195 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -112,7 +112,7 @@ void WiFiComponent::start() { this->trying_loaded_ap_ = this->load_fast_connect_settings_(); if (!this->trying_loaded_ap_) { // Fast connect failed - start from first configured AP without scan result - this->selected_ap_index_ = 0; + this->selected_sta_index_ = 0; this->selected_scan_index_ = -1; } this->start_connecting_to_selected_(false); @@ -335,20 +335,20 @@ void WiFiComponent::set_sta(const WiFiAP &ap) { this->clear_sta(); this->init_sta(1); this->add_sta(ap); - this->selected_ap_index_ = 0; + this->selected_sta_index_ = 0; this->selected_scan_index_ = -1; } void WiFiComponent::clear_sta() { this->sta_.clear(); - this->selected_ap_index_ = -1; + this->selected_sta_index_ = -1; this->selected_scan_index_ = -1; } WiFiAP WiFiComponent::build_selected_ap_() const { WiFiAP params; - if (this->selected_ap_index_ >= 0 && this->selected_ap_index_ < this->sta_.size()) { - const WiFiAP &config = this->sta_[this->selected_ap_index_]; + if (this->selected_sta_index_ >= 0 && this->selected_sta_index_ < this->sta_.size()) { + const WiFiAP &config = this->sta_[this->selected_sta_index_]; // Copy config data params.set_password(config.get_password()); @@ -388,8 +388,8 @@ WiFiAP WiFiComponent::build_selected_ap_() const { } WiFiAP WiFiComponent::get_sta() { - if (this->selected_ap_index_ >= 0 && this->selected_ap_index_ < this->sta_.size()) { - return this->sta_[this->selected_ap_index_]; + if (this->selected_sta_index_ >= 0 && this->selected_sta_index_ < this->sta_.size()) { + return this->sta_[this->selected_sta_index_]; } return WiFiAP{}; } @@ -540,8 +540,8 @@ void WiFiComponent::print_connect_params_() { LOG_STR_ARG(get_signal_bars(rssi)), get_wifi_channel(), wifi_subnet_mask_().str().c_str(), wifi_gateway_ip_().str().c_str(), wifi_dns_ip_(0).str().c_str(), wifi_dns_ip_(1).str().c_str()); #ifdef ESPHOME_LOG_HAS_VERBOSE - if (this->selected_ap_index_ >= 0 && this->selected_ap_index_ < this->sta_.size()) { - const WiFiAP &config = this->sta_[this->selected_ap_index_]; + if (this->selected_sta_index_ >= 0 && this->selected_sta_index_ < this->sta_.size()) { + const WiFiAP &config = this->sta_[this->selected_sta_index_]; if (config.get_bssid().has_value()) { ESP_LOGV(TAG, " Priority: %.1f", this->get_sta_priority(*config.get_bssid())); } @@ -707,7 +707,7 @@ void WiFiComponent::check_scanning_finished() { continue; } - this->selected_ap_index_ = i; + this->selected_sta_index_ = i; break; } @@ -733,8 +733,8 @@ void WiFiComponent::check_connecting_finished() { ESP_LOGI(TAG, "Connected"); // We won't retry hidden networks unless a reconnect fails more than three times again - if (this->retry_hidden_ && this->selected_ap_index_ >= 0 && this->selected_ap_index_ < this->sta_.size()) { - const WiFiAP &config = this->sta_[this->selected_ap_index_]; + if (this->retry_hidden_ && this->selected_sta_index_ >= 0 && this->selected_sta_index_ < this->sta_.size()) { + const WiFiAP &config = this->sta_[this->selected_sta_index_]; if (!config.get_hidden()) ESP_LOGW(TAG, "Network '%s' should be marked as hidden", config.get_ssid().c_str()); } @@ -807,8 +807,8 @@ void WiFiComponent::check_connecting_finished() { } void WiFiComponent::retry_connect() { - if (this->selected_ap_index_ >= 0 && this->selected_ap_index_ < this->sta_.size()) { - const WiFiAP &config = this->sta_[this->selected_ap_index_]; + if (this->selected_sta_index_ >= 0 && this->selected_sta_index_ < this->sta_.size()) { + const WiFiAP &config = this->sta_[this->selected_sta_index_]; if (config.get_bssid()) { auto bssid = *config.get_bssid(); float priority = this->get_sta_priority(bssid); @@ -822,14 +822,14 @@ void WiFiComponent::retry_connect() { #ifdef USE_WIFI_FAST_CONNECT if (this->trying_loaded_ap_) { this->trying_loaded_ap_ = false; - this->selected_ap_index_ = 0; // Retry from the first configured AP - } else if (this->selected_ap_index_ >= static_cast(this->sta_.size()) - 1) { + this->selected_sta_index_ = 0; // Retry from the first configured AP + } else if (this->selected_sta_index_ >= static_cast(this->sta_.size()) - 1) { ESP_LOGW(TAG, "No more APs to try"); - this->selected_ap_index_ = 0; + this->selected_sta_index_ = 0; this->restart_adapter(); } else { // Try next AP - this->selected_ap_index_++; + this->selected_sta_index_++; } this->num_retried_ = 0; this->selected_scan_index_ = -1; @@ -904,7 +904,7 @@ bool WiFiComponent::load_fast_connect_settings_() { this->scan_result_.push_back(fast_connect_scan); // Set indices to use the loaded AP config and temporary scan result - this->selected_ap_index_ = fast_connect_save.ap_index; + this->selected_sta_index_ = fast_connect_save.ap_index; this->selected_scan_index_ = 0; ESP_LOGD(TAG, "Loaded fast_connect settings"); @@ -929,7 +929,7 @@ void WiFiComponent::save_fast_connect_settings_() { SavedWifiFastConnectSettings fast_connect_save{}; memcpy(fast_connect_save.bssid, bssid.data(), 6); fast_connect_save.channel = channel; - fast_connect_save.ap_index = this->selected_ap_index_ >= 0 ? this->selected_ap_index_ : 0; + fast_connect_save.ap_index = this->selected_sta_index_ >= 0 ? this->selected_sta_index_ : 0; this->fast_connect_pref_.save(&fast_connect_save); diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 155c1f419e..d787e35a63 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -340,8 +340,8 @@ class WiFiComponent : public Component { WiFiAP build_selected_ap_() const; void reset_selected_ap_to_first_if_invalid_() { - if (this->selected_ap_index_ < 0 || this->selected_ap_index_ >= this->sta_.size()) { - this->selected_ap_index_ = this->sta_.empty() ? -1 : 0; + if (this->selected_sta_index_ < 0 || this->selected_sta_index_ >= this->sta_.size()) { + this->selected_sta_index_ = this->sta_.empty() ? -1 : 0; this->selected_scan_index_ = -1; } } @@ -428,7 +428,7 @@ class WiFiComponent : public Component { uint8_t num_retried_{0}; // Index into sta_ array for the currently selected AP configuration (-1 = none selected) // Used to access password, manual_ip, priority, EAP settings, and hidden flag - int8_t selected_ap_index_{-1}; + int8_t selected_sta_index_{-1}; // Index into scan_result_ array for the currently selected scan result (-1 = no scan data) // Used to access scanned SSID, BSSID, and channel. Also used for fast connect (synthetic scan result) int8_t selected_scan_index_{-1}; From 670d85090c6113e94aca14994d969cfe18bcdae3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 11:34:15 -0600 Subject: [PATCH 06/65] preen --- esphome/components/wifi/wifi_component.cpp | 43 ++++++++++------------ esphome/components/wifi/wifi_component.h | 7 ++++ 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 976b76b195..d5489b0641 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -347,22 +347,20 @@ void WiFiComponent::clear_sta() { WiFiAP WiFiComponent::build_selected_ap_() const { WiFiAP params; - if (this->selected_sta_index_ >= 0 && this->selected_sta_index_ < this->sta_.size()) { - const WiFiAP &config = this->sta_[this->selected_sta_index_]; - + if (const WiFiAP *config = this->get_selected_sta_()) { // Copy config data - params.set_password(config.get_password()); - params.set_manual_ip(config.get_manual_ip()); - params.set_priority(config.get_priority()); + params.set_password(config->get_password()); + params.set_manual_ip(config->get_manual_ip()); + params.set_priority(config->get_priority()); #ifdef USE_WIFI_WPA2_EAP - params.set_eap(config.get_eap()); + params.set_eap(config->get_eap()); #endif // Use config SSID for hidden networks - if (config.get_hidden()) { + if (config->get_hidden()) { params.set_hidden(true); - params.set_ssid(config.get_ssid()); + params.set_ssid(config->get_ssid()); // Clear BSSID and channel for hidden networks - there might be multiple hidden networks // and we can't know which one is correct. Rely on probe-req with just SSID. // Leaving channel empty triggers ALL_CHANNEL_SCAN instead of FAST_SCAN. @@ -388,8 +386,8 @@ WiFiAP WiFiComponent::build_selected_ap_() const { } WiFiAP WiFiComponent::get_sta() { - if (this->selected_sta_index_ >= 0 && this->selected_sta_index_ < this->sta_.size()) { - return this->sta_[this->selected_sta_index_]; + if (const WiFiAP *config = this->get_selected_sta_()) { + return *config; } return WiFiAP{}; } @@ -540,10 +538,9 @@ void WiFiComponent::print_connect_params_() { LOG_STR_ARG(get_signal_bars(rssi)), get_wifi_channel(), wifi_subnet_mask_().str().c_str(), wifi_gateway_ip_().str().c_str(), wifi_dns_ip_(0).str().c_str(), wifi_dns_ip_(1).str().c_str()); #ifdef ESPHOME_LOG_HAS_VERBOSE - if (this->selected_sta_index_ >= 0 && this->selected_sta_index_ < this->sta_.size()) { - const WiFiAP &config = this->sta_[this->selected_sta_index_]; - if (config.get_bssid().has_value()) { - ESP_LOGV(TAG, " Priority: %.1f", this->get_sta_priority(*config.get_bssid())); + if (const WiFiAP *config = this->get_selected_sta_()) { + if (config->get_bssid().has_value()) { + ESP_LOGV(TAG, " Priority: %.1f", this->get_sta_priority(*config->get_bssid())); } } #endif @@ -733,10 +730,11 @@ void WiFiComponent::check_connecting_finished() { ESP_LOGI(TAG, "Connected"); // We won't retry hidden networks unless a reconnect fails more than three times again - if (this->retry_hidden_ && this->selected_sta_index_ >= 0 && this->selected_sta_index_ < this->sta_.size()) { - const WiFiAP &config = this->sta_[this->selected_sta_index_]; - if (!config.get_hidden()) - ESP_LOGW(TAG, "Network '%s' should be marked as hidden", config.get_ssid().c_str()); + if (this->retry_hidden_) { + if (const WiFiAP *config = this->get_selected_sta_()) { + if (!config->get_hidden()) + ESP_LOGW(TAG, "Network '%s' should be marked as hidden", config->get_ssid().c_str()); + } } this->retry_hidden_ = false; @@ -807,10 +805,9 @@ void WiFiComponent::check_connecting_finished() { } void WiFiComponent::retry_connect() { - if (this->selected_sta_index_ >= 0 && this->selected_sta_index_ < this->sta_.size()) { - const WiFiAP &config = this->sta_[this->selected_sta_index_]; - if (config.get_bssid()) { - auto bssid = *config.get_bssid(); + if (const WiFiAP *config = this->get_selected_sta_()) { + if (config->get_bssid()) { + auto bssid = *config->get_bssid(); float priority = this->get_sta_priority(bssid); this->set_sta_priority(bssid, priority - 1.0f); } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index d787e35a63..6ccb4fbff2 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -339,6 +339,13 @@ class WiFiComponent : public Component { void print_connect_params_(); WiFiAP build_selected_ap_() const; + const WiFiAP *get_selected_sta_() const { + if (this->selected_sta_index_ >= 0 && this->selected_sta_index_ < this->sta_.size()) { + return &this->sta_[this->selected_sta_index_]; + } + return nullptr; + } + void reset_selected_ap_to_first_if_invalid_() { if (this->selected_sta_index_ < 0 || this->selected_sta_index_ >= this->sta_.size()) { this->selected_sta_index_ = this->sta_.empty() ? -1 : 0; From e7e2df5c6d9b1c05fc611318dc4b0900051882c0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 11:35:57 -0600 Subject: [PATCH 07/65] preen --- esphome/components/wifi/wifi_component.cpp | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index d5489b0641..7fe683ef0e 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -538,10 +538,8 @@ void WiFiComponent::print_connect_params_() { LOG_STR_ARG(get_signal_bars(rssi)), get_wifi_channel(), wifi_subnet_mask_().str().c_str(), wifi_gateway_ip_().str().c_str(), wifi_dns_ip_(0).str().c_str(), wifi_dns_ip_(1).str().c_str()); #ifdef ESPHOME_LOG_HAS_VERBOSE - if (const WiFiAP *config = this->get_selected_sta_()) { - if (config->get_bssid().has_value()) { - ESP_LOGV(TAG, " Priority: %.1f", this->get_sta_priority(*config->get_bssid())); - } + if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_bssid().has_value()) { + ESP_LOGV(TAG, " Priority: %.1f", this->get_sta_priority(*config->get_bssid())); } #endif #ifdef USE_WIFI_11KV_SUPPORT @@ -731,9 +729,8 @@ void WiFiComponent::check_connecting_finished() { ESP_LOGI(TAG, "Connected"); // We won't retry hidden networks unless a reconnect fails more than three times again if (this->retry_hidden_) { - if (const WiFiAP *config = this->get_selected_sta_()) { - if (!config->get_hidden()) - ESP_LOGW(TAG, "Network '%s' should be marked as hidden", config->get_ssid().c_str()); + if (const WiFiAP *config = this->get_selected_sta_(); config && !config->get_hidden()) { + ESP_LOGW(TAG, "Network '%s' should be marked as hidden", config->get_ssid().c_str()); } } this->retry_hidden_ = false; @@ -805,12 +802,10 @@ void WiFiComponent::check_connecting_finished() { } void WiFiComponent::retry_connect() { - if (const WiFiAP *config = this->get_selected_sta_()) { - if (config->get_bssid()) { - auto bssid = *config->get_bssid(); - float priority = this->get_sta_priority(bssid); - this->set_sta_priority(bssid, priority - 1.0f); - } + if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_bssid()) { + auto bssid = *config->get_bssid(); + float priority = this->get_sta_priority(bssid); + this->set_sta_priority(bssid, priority - 1.0f); } delay(10); From 7d4b3ff3a6ad5c731d9978b9fbffb12d83c53eef Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 11:36:52 -0600 Subject: [PATCH 08/65] preen --- esphome/components/wifi/wifi_component.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 7fe683ef0e..d2a44ce8da 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -728,10 +728,8 @@ void WiFiComponent::check_connecting_finished() { ESP_LOGI(TAG, "Connected"); // We won't retry hidden networks unless a reconnect fails more than three times again - if (this->retry_hidden_) { - if (const WiFiAP *config = this->get_selected_sta_(); config && !config->get_hidden()) { - ESP_LOGW(TAG, "Network '%s' should be marked as hidden", config->get_ssid().c_str()); - } + if (const WiFiAP *config = this->get_selected_sta_(); this->retry_hidden_ && config && !config->get_hidden()) { + ESP_LOGW(TAG, "Network '%s' should be marked as hidden", config->get_ssid().c_str()); } this->retry_hidden_ = false; From bfca9cb6c245102dfb24bd35ec6d16e332ffa318 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 11:37:08 -0600 Subject: [PATCH 09/65] preen --- esphome/components/wifi/wifi_component.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index d2a44ce8da..db99c78329 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -386,10 +386,8 @@ WiFiAP WiFiComponent::build_selected_ap_() const { } WiFiAP WiFiComponent::get_sta() { - if (const WiFiAP *config = this->get_selected_sta_()) { - return *config; - } - return WiFiAP{}; + const WiFiAP *config = this->get_selected_sta_(); + return config ? *config : WiFiAP{}; } void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &password) { SavedWifiSettings save{}; // zero-initialized - all bytes set to \0, guaranteeing null termination From 03fd2eef2f5466f5c1de665a8f940439b2ad2abb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 11:46:59 -0600 Subject: [PATCH 10/65] preen --- esphome/components/wifi/wifi_component.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index db99c78329..109af317ec 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -751,16 +751,17 @@ void WiFiComponent::check_connecting_finished() { this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTED; this->num_retried_ = 0; +#ifdef USE_WIFI_FAST_CONNECT + this->save_fast_connect_settings_(); +#endif + // Free scan results memory unless a component needs them if (!this->keep_scan_results_) { this->scan_result_.clear(); this->scan_result_.shrink_to_fit(); + this->selected_scan_index_ = -1; // Invalidate index since scan results are gone } -#ifdef USE_WIFI_FAST_CONNECT - this->save_fast_connect_settings_(); -#endif - return; } From 27fb72a1d3be3974d1dcdaa708b825e4cc661235 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 11:47:58 -0600 Subject: [PATCH 11/65] preen --- esphome/components/wifi/wifi_component.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 109af317ec..660be761a0 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -348,7 +348,7 @@ WiFiAP WiFiComponent::build_selected_ap_() const { WiFiAP params; if (const WiFiAP *config = this->get_selected_sta_()) { - // Copy config data + // Copy config data (password, manual IP, priority, EAP) params.set_password(config->get_password()); params.set_manual_ip(config->get_manual_ip()); params.set_priority(config->get_priority()); @@ -357,12 +357,12 @@ WiFiAP WiFiComponent::build_selected_ap_() const { params.set_eap(config->get_eap()); #endif - // Use config SSID for hidden networks + // Selected network is hidden, we use the data from the config if (config->get_hidden()) { params.set_hidden(true); params.set_ssid(config->get_ssid()); // Clear BSSID and channel for hidden networks - there might be multiple hidden networks - // and we can't know which one is correct. Rely on probe-req with just SSID. + // but we can't know which one is the correct one. Rely on probe-req with just SSID. // Leaving channel empty triggers ALL_CHANNEL_SCAN instead of FAST_SCAN. params.set_bssid(optional{}); params.set_channel(optional{}); @@ -374,7 +374,8 @@ WiFiAP WiFiComponent::build_selected_ap_() const { const WiFiScanResult &scan = this->scan_result_[this->selected_scan_index_]; if (!params.get_hidden()) { - // For visible networks, use scan data to limit connection to exactly this network + // Selected network is visible, we use the data from the scan. + // Limit the connect params to only connect to exactly this network // (network selection is done during scan phase). params.set_ssid(scan.get_ssid()); params.set_bssid(scan.get_bssid()); From 083f41c43f8d7438f421dc3fd3987823ad8be589 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 11:54:09 -0600 Subject: [PATCH 12/65] preen --- esphome/components/wifi/wifi_component.cpp | 20 +++++++------------- esphome/components/wifi/wifi_component.h | 4 ---- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 660be761a0..a7a3fb8949 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -113,7 +113,6 @@ void WiFiComponent::start() { if (!this->trying_loaded_ap_) { // Fast connect failed - start from first configured AP without scan result this->selected_sta_index_ = 0; - this->selected_scan_index_ = -1; } this->start_connecting_to_selected_(false); #else @@ -336,12 +335,10 @@ void WiFiComponent::set_sta(const WiFiAP &ap) { this->init_sta(1); this->add_sta(ap); this->selected_sta_index_ = 0; - this->selected_scan_index_ = -1; } void WiFiComponent::clear_sta() { this->sta_.clear(); this->selected_sta_index_ = -1; - this->selected_scan_index_ = -1; } WiFiAP WiFiComponent::build_selected_ap_() const { @@ -370,8 +367,9 @@ WiFiAP WiFiComponent::build_selected_ap_() const { } // Overlay scan result data (if available) - if (this->selected_scan_index_ >= 0 && this->selected_scan_index_ < this->scan_result_.size()) { - const WiFiScanResult &scan = this->scan_result_[this->selected_scan_index_]; + // Scan results are sorted, so index 0 is always the best network + if (!this->scan_result_.empty()) { + const WiFiScanResult &scan = this->scan_result_[0]; if (!params.get_hidden()) { // Selected network is visible, we use the data from the scan. @@ -691,9 +689,8 @@ void WiFiComponent::check_scanning_finished() { return; } - // Find matching config and set indices for on-demand connection params building + // Find matching config for on-demand connection params building const WiFiScanResult &scan_res = this->scan_result_[0]; - this->selected_scan_index_ = 0; for (size_t i = 0; i < this->sta_.size(); i++) { // search for matching STA config, at least one will match (from checks before) @@ -760,7 +757,6 @@ void WiFiComponent::check_connecting_finished() { if (!this->keep_scan_results_) { this->scan_result_.clear(); this->scan_result_.shrink_to_fit(); - this->selected_scan_index_ = -1; // Invalidate index since scan results are gone } return; @@ -822,7 +818,6 @@ void WiFiComponent::retry_connect() { this->selected_sta_index_++; } this->num_retried_ = 0; - this->selected_scan_index_ = -1; #else if (this->num_retried_ > 5) { // If retry failed for more than 5 times, let's restart STA @@ -893,9 +888,8 @@ bool WiFiComponent::load_fast_connect_settings_() { WiFiScanResult fast_connect_scan(bssid, "", fast_connect_save.channel, 0, false, false); this->scan_result_.push_back(fast_connect_scan); - // Set indices to use the loaded AP config and temporary scan result + // Set index to use the loaded AP config with temporary scan result this->selected_sta_index_ = fast_connect_save.ap_index; - this->selected_scan_index_ = 0; ESP_LOGD(TAG, "Loaded fast_connect settings"); return true; @@ -909,8 +903,8 @@ void WiFiComponent::save_fast_connect_settings_() { uint8_t channel = get_wifi_channel(); // Skip save if settings haven't changed (compare with current scan result if available) - if (this->selected_scan_index_ >= 0 && this->selected_scan_index_ < this->scan_result_.size()) { - const WiFiScanResult &scan = this->scan_result_[this->selected_scan_index_]; + if (!this->scan_result_.empty()) { + const WiFiScanResult &scan = this->scan_result_[0]; if (bssid == scan.get_bssid() && channel == scan.get_channel()) { return; // No change, nothing to save } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 6ccb4fbff2..ae8dafe14b 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -349,7 +349,6 @@ class WiFiComponent : public Component { void reset_selected_ap_to_first_if_invalid_() { if (this->selected_sta_index_ < 0 || this->selected_sta_index_ >= this->sta_.size()) { this->selected_sta_index_ = this->sta_.empty() ? -1 : 0; - this->selected_scan_index_ = -1; } } @@ -436,9 +435,6 @@ class WiFiComponent : public Component { // Index into sta_ array for the currently selected AP configuration (-1 = none selected) // Used to access password, manual_ip, priority, EAP settings, and hidden flag int8_t selected_sta_index_{-1}; - // Index into scan_result_ array for the currently selected scan result (-1 = no scan data) - // Used to access scanned SSID, BSSID, and channel. Also used for fast connect (synthetic scan result) - int8_t selected_scan_index_{-1}; #if USE_NETWORK_IPV6 uint8_t num_ipv6_addresses_{0}; #endif /* USE_NETWORK_IPV6 */ From 2c110a9e7e259f9d75625a1c7a6ce661d9c0e390 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 11:57:23 -0600 Subject: [PATCH 13/65] preen --- esphome/components/wifi/wifi_component.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index a7a3fb8949..76797f4a7d 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -683,15 +683,15 @@ void WiFiComponent::check_scanning_finished() { log_scan_result(res); } - if (!this->scan_result_[0].get_matches()) { + // Find matching config for on-demand connection params building + const WiFiScanResult &scan_res = this->scan_result_[0]; + + if (!scan_res.get_matches()) { ESP_LOGW(TAG, "No matching network found"); this->retry_connect(); return; } - // Find matching config for on-demand connection params building - const WiFiScanResult &scan_res = this->scan_result_[0]; - for (size_t i = 0; i < this->sta_.size(); i++) { // search for matching STA config, at least one will match (from checks before) if (!scan_res.matches(this->sta_[i])) { From 60d6144574308b578394d0786de81286d6f11af1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 12:02:18 -0600 Subject: [PATCH 14/65] preen --- esphome/components/wifi/wifi_component.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 76797f4a7d..ee4a27e4d4 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -367,7 +367,11 @@ WiFiAP WiFiComponent::build_selected_ap_() const { } // Overlay scan result data (if available) - // Scan results are sorted, so index 0 is always the best network + // SYNCHRONIZATION: selected_sta_index_ and scan_result_[0] are kept in sync: + // - wifi_scan_done() sorts all scan results by priority/RSSI (best first) + // - It then finds which sta_[i] config matches scan_result_[0] + // - Sets selected_sta_index_ = i to record that matching config + // Therefore scan_result_[0] is guaranteed to match sta_[selected_sta_index_] if (!this->scan_result_.empty()) { const WiFiScanResult &scan = this->scan_result_[0]; From 25ef0043d24906d84a952fa10433adfd2fc0bda9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 12:07:42 -0600 Subject: [PATCH 15/65] preen --- esphome/components/wifi/wifi_component.cpp | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index ee4a27e4d4..d2db78ae00 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -111,7 +111,9 @@ void WiFiComponent::start() { #ifdef USE_WIFI_FAST_CONNECT this->trying_loaded_ap_ = this->load_fast_connect_settings_(); if (!this->trying_loaded_ap_) { - // Fast connect failed - start from first configured AP without scan result + // FAST CONNECT FALLBACK: No saved settings available + // Set selected_sta_index_ to first config without any scan result + // build_selected_ap_() will use config data only (no SSID/BSSID/channel from scan) this->selected_sta_index_ = 0; } this->start_connecting_to_selected_(false); @@ -688,6 +690,10 @@ void WiFiComponent::check_scanning_finished() { } // Find matching config for on-demand connection params building + // SYNCHRONIZATION POINT: Establish link between scan_result_[0] and selected_sta_index_ + // After sorting, scan_result_[0] contains the best network. Now find which sta_[i] config + // matches that network and record it in selected_sta_index_. This keeps the two indices + // synchronized so build_selected_ap_() can safely use both to build connection parameters. const WiFiScanResult &scan_res = this->scan_result_[0]; if (!scan_res.get_matches()) { @@ -702,7 +708,7 @@ void WiFiComponent::check_scanning_finished() { continue; } - this->selected_sta_index_ = i; + this->selected_sta_index_ = i; // Links scan_result_[0] with sta_[i] break; } @@ -887,12 +893,15 @@ bool WiFiComponent::load_fast_connect_settings_() { bssid_t bssid{}; std::copy(fast_connect_save.bssid, fast_connect_save.bssid + 6, bssid.begin()); + // FAST CONNECT SUCCESS: Restore saved settings without scanning // Create a temporary scan result with the fast connect BSSID and channel this->scan_result_.init(1); WiFiScanResult fast_connect_scan(bssid, "", fast_connect_save.channel, 0, false, false); this->scan_result_.push_back(fast_connect_scan); - // Set index to use the loaded AP config with temporary scan result + // SYNCHRONIZATION: Link scan_result_[0] (temporary) with sta_[saved_index] + // Unlike wifi_scan_done() which sorts then finds the match, here we know exactly + // which config was used before and create a matching temporary scan result this->selected_sta_index_ = fast_connect_save.ap_index; ESP_LOGD(TAG, "Loaded fast_connect settings"); From 5543acf3ab7b1a7dfe553466c18624726ce56a72 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 12:10:18 -0600 Subject: [PATCH 16/65] preen --- esphome/components/wifi/wifi_component.cpp | 28 ++++------------------ esphome/components/wifi/wifi_component.h | 28 ++++++++++++++++++++++ 2 files changed, 33 insertions(+), 23 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index d2db78ae00..e3f2e50d20 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -112,8 +112,7 @@ void WiFiComponent::start() { this->trying_loaded_ap_ = this->load_fast_connect_settings_(); if (!this->trying_loaded_ap_) { // FAST CONNECT FALLBACK: No saved settings available - // Set selected_sta_index_ to first config without any scan result - // build_selected_ap_() will use config data only (no SSID/BSSID/channel from scan) + // Use first config without any scan result (config data only, no SSID/BSSID/channel) this->selected_sta_index_ = 0; } this->start_connecting_to_selected_(false); @@ -689,29 +688,16 @@ void WiFiComponent::check_scanning_finished() { log_scan_result(res); } - // Find matching config for on-demand connection params building // SYNCHRONIZATION POINT: Establish link between scan_result_[0] and selected_sta_index_ // After sorting, scan_result_[0] contains the best network. Now find which sta_[i] config // matches that network and record it in selected_sta_index_. This keeps the two indices // synchronized so build_selected_ap_() can safely use both to build connection parameters. - const WiFiScanResult &scan_res = this->scan_result_[0]; - - if (!scan_res.get_matches()) { + if (!this->sync_selected_sta_to_best_scan_result_()) { ESP_LOGW(TAG, "No matching network found"); this->retry_connect(); return; } - for (size_t i = 0; i < this->sta_.size(); i++) { - // search for matching STA config, at least one will match (from checks before) - if (!scan_res.matches(this->sta_[i])) { - continue; - } - - this->selected_sta_index_ = i; // Links scan_result_[0] with sta_[i] - break; - } - yield(); this->start_connecting_to_selected_(false); @@ -894,15 +880,11 @@ bool WiFiComponent::load_fast_connect_settings_() { std::copy(fast_connect_save.bssid, fast_connect_save.bssid + 6, bssid.begin()); // FAST CONNECT SUCCESS: Restore saved settings without scanning - // Create a temporary scan result with the fast connect BSSID and channel - this->scan_result_.init(1); - WiFiScanResult fast_connect_scan(bssid, "", fast_connect_save.channel, 0, false, false); - this->scan_result_.push_back(fast_connect_scan); - - // SYNCHRONIZATION: Link scan_result_[0] (temporary) with sta_[saved_index] + // SYNCHRONIZATION: Link temporary scan result with sta_[saved_index] // Unlike wifi_scan_done() which sorts then finds the match, here we know exactly // which config was used before and create a matching temporary scan result - this->selected_sta_index_ = fast_connect_save.ap_index; + WiFiScanResult fast_connect_scan(bssid, "", fast_connect_save.channel, 0, false, false); + this->set_selected_sta_with_scan_(fast_connect_save.ap_index, fast_connect_scan); ESP_LOGD(TAG, "Loaded fast_connect settings"); return true; diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index ae8dafe14b..a89c1fb430 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -352,6 +352,34 @@ class WiFiComponent : public Component { } } + // SYNCHRONIZATION HELPERS: Encapsulate the relationship between selected_sta_index_ and scan_result_ + + // Set selected sta with a temporary scan result (fast connect path) + void set_selected_sta_with_scan_(int8_t sta_index, const WiFiScanResult &scan) { + this->scan_result_.init(1); + this->scan_result_.push_back(scan); + this->selected_sta_index_ = sta_index; + } + + // Find which sta_[i] matches scan_result_[0] and set selected_sta_index_ (scan done path) + // Returns true if match found, false otherwise + bool sync_selected_sta_to_best_scan_result_() { + if (this->scan_result_.empty()) + return false; + + const WiFiScanResult &scan_res = this->scan_result_[0]; + if (!scan_res.get_matches()) + return false; + + for (size_t i = 0; i < this->sta_.size(); i++) { + if (scan_res.matches(this->sta_[i])) { + this->selected_sta_index_ = i; // Links scan_result_[0] with sta_[i] + return true; + } + } + return false; + } + void start_connecting_to_selected_(bool two) { WiFiAP connection_params = this->build_selected_ap_(); this->start_connecting(connection_params, two); From d38703c18ac6e3eeb94999bd0b62d15a931cfd2d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 12:31:14 -0600 Subject: [PATCH 17/65] [wifi] Refactor AP selection with synchronization helpers --- esphome/components/wifi/wifi_component.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index a89c1fb430..b230ef38bb 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -340,14 +340,14 @@ class WiFiComponent : public Component { WiFiAP build_selected_ap_() const; const WiFiAP *get_selected_sta_() const { - if (this->selected_sta_index_ >= 0 && this->selected_sta_index_ < this->sta_.size()) { + if (this->selected_sta_index_ >= 0 && static_cast(this->selected_sta_index_) < this->sta_.size()) { return &this->sta_[this->selected_sta_index_]; } return nullptr; } void reset_selected_ap_to_first_if_invalid_() { - if (this->selected_sta_index_ < 0 || this->selected_sta_index_ >= this->sta_.size()) { + if (this->selected_sta_index_ < 0 || static_cast(this->selected_sta_index_) >= this->sta_.size()) { this->selected_sta_index_ = this->sta_.empty() ? -1 : 0; } } @@ -373,7 +373,7 @@ class WiFiComponent : public Component { for (size_t i = 0; i < this->sta_.size(); i++) { if (scan_res.matches(this->sta_[i])) { - this->selected_sta_index_ = i; // Links scan_result_[0] with sta_[i] + this->selected_sta_index_ = static_cast(i); // Links scan_result_[0] with sta_[i] return true; } } From 37620e61f948ea6b406a4e5c78885f0f4e7b0145 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 12:36:35 -0600 Subject: [PATCH 18/65] fast connect fixes --- esphome/components/wifi/wifi_component.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index e3f2e50d20..d6715dbf40 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -875,6 +875,12 @@ bool WiFiComponent::load_fast_connect_settings_() { SavedWifiFastConnectSettings fast_connect_save{}; if (this->fast_connect_pref_.load(&fast_connect_save)) { + // Validate saved AP index + if (fast_connect_save.ap_index >= this->sta_.size()) { + ESP_LOGW(TAG, "Saved AP index out of bounds"); + return false; + } + // Load BSSID from saved settings bssid_t bssid{}; std::copy(fast_connect_save.bssid, fast_connect_save.bssid + 6, bssid.begin()); @@ -883,7 +889,9 @@ bool WiFiComponent::load_fast_connect_settings_() { // SYNCHRONIZATION: Link temporary scan result with sta_[saved_index] // Unlike wifi_scan_done() which sorts then finds the match, here we know exactly // which config was used before and create a matching temporary scan result - WiFiScanResult fast_connect_scan(bssid, "", fast_connect_save.channel, 0, false, false); + // Use SSID from config for the temporary scan result + const std::string &ssid = this->sta_[fast_connect_save.ap_index].get_ssid(); + WiFiScanResult fast_connect_scan(bssid, ssid, fast_connect_save.channel, 0, false, false); this->set_selected_sta_with_scan_(fast_connect_save.ap_index, fast_connect_scan); ESP_LOGD(TAG, "Loaded fast_connect settings"); From 6d958a6640e755db8cdf8c15aba5072ab6170eab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 12:58:03 -0600 Subject: [PATCH 19/65] fixes for no fast connect yet --- esphome/components/wifi/wifi_component.cpp | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index d6715dbf40..105f2e4cb9 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -112,7 +112,7 @@ void WiFiComponent::start() { this->trying_loaded_ap_ = this->load_fast_connect_settings_(); if (!this->trying_loaded_ap_) { // FAST CONNECT FALLBACK: No saved settings available - // Use first config without any scan result (config data only, no SSID/BSSID/channel) + // Use first config (will use SSID from config since scan_result_ is empty) this->selected_sta_index_ = 0; } this->start_connecting_to_selected_(false); @@ -355,15 +355,20 @@ WiFiAP WiFiComponent::build_selected_ap_() const { params.set_eap(config->get_eap()); #endif - // Selected network is hidden, we use the data from the config + // Set network parameters from config + // These will be used as-is for hidden networks, or overridden by scan for visible networks + params.set_ssid(config->get_ssid()); + if (config->get_hidden()) { params.set_hidden(true); - params.set_ssid(config->get_ssid()); - // Clear BSSID and channel for hidden networks - there might be multiple hidden networks - // but we can't know which one is the correct one. Rely on probe-req with just SSID. - // Leaving channel empty triggers ALL_CHANNEL_SCAN instead of FAST_SCAN. + // For hidden networks, clear BSSID and channel even if set in config + // There might be multiple hidden networks with same SSID but we can't know which is correct + // Rely on probe-req with just SSID. Leaving channel empty triggers ALL_CHANNEL_SCAN. params.set_bssid(optional{}); params.set_channel(optional{}); + } else { + params.set_bssid(config->get_bssid()); + params.set_channel(config->get_channel()); } } @@ -377,7 +382,7 @@ WiFiAP WiFiComponent::build_selected_ap_() const { const WiFiScanResult &scan = this->scan_result_[0]; if (!params.get_hidden()) { - // Selected network is visible, we use the data from the scan. + // Selected network is visible, override with data from the scan. // Limit the connect params to only connect to exactly this network // (network selection is done during scan phase). params.set_ssid(scan.get_ssid()); From 57a88e82118f9d22a3bee55bbc1e839d2a0c7222 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 13:11:06 -0600 Subject: [PATCH 20/65] fixes for no fast connect yet --- esphome/components/wifi/wifi_component.cpp | 26 +++++++++++++++++----- esphome/components/wifi/wifi_component.h | 1 + 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 105f2e4cb9..1c043e221e 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -170,9 +170,14 @@ void WiFiComponent::loop() { this->status_set_warning(LOG_STR("waiting to reconnect")); if (millis() - this->action_started_ > 5000) { #ifdef USE_WIFI_FAST_CONNECT - // NOTE: This check may not make sense here as it could interfere with AP cycling - this->reset_selected_ap_to_first_if_invalid_(); - this->start_connecting_to_selected_(false); + if (this->fast_connect_exhausted_) { + // All APs tried, fall back to scanning + this->start_scanning(); + } else { + // NOTE: This check may not make sense here as it could interfere with AP cycling + this->reset_selected_ap_to_first_if_invalid_(); + this->start_connecting_to_selected_(false); + } #else if (this->retry_hidden_) { this->reset_selected_ap_to_first_if_invalid_(); @@ -703,6 +708,11 @@ void WiFiComponent::check_scanning_finished() { return; } +#ifdef USE_WIFI_FAST_CONNECT + // Scan found a network, reset exhausted flag to allow fast connect to work next time + this->fast_connect_exhausted_ = false; +#endif + yield(); this->start_connecting_to_selected_(false); @@ -751,6 +761,7 @@ void WiFiComponent::check_connecting_finished() { this->num_retried_ = 0; #ifdef USE_WIFI_FAST_CONNECT + this->fast_connect_exhausted_ = false; // Reset on successful connection this->save_fast_connect_settings_(); #endif @@ -810,15 +821,18 @@ void WiFiComponent::retry_connect() { if (this->trying_loaded_ap_) { this->trying_loaded_ap_ = false; this->selected_sta_index_ = 0; // Retry from the first configured AP + this->num_retried_ = 0; } else if (this->selected_sta_index_ >= static_cast(this->sta_.size()) - 1) { - ESP_LOGW(TAG, "No more APs to try"); - this->selected_sta_index_ = 0; + // Exhausted all configured APs, fall back to full scan + ESP_LOGW(TAG, "No more APs to try, starting scan"); + this->fast_connect_exhausted_ = true; this->restart_adapter(); + return; } else { // Try next AP this->selected_sta_index_++; + this->num_retried_ = 0; } - this->num_retried_ = 0; #else if (this->num_retried_ > 5) { // If retry failed for more than 5 times, let's restart STA diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index b230ef38bb..2e7a0728b4 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -470,6 +470,7 @@ class WiFiComponent : public Component { // Group all boolean values together #ifdef USE_WIFI_FAST_CONNECT bool trying_loaded_ap_{false}; + bool fast_connect_exhausted_{false}; // All APs tried, fall back to scan #endif bool retry_hidden_{false}; bool has_ap_{false}; From 047773e62f748f228cef6b8463f309f4089a5f56 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 13:32:51 -0600 Subject: [PATCH 21/65] fixes for no fast connect yet --- esphome/components/wifi/wifi_component.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 1c043e221e..f774f6b64a 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -895,7 +895,7 @@ bool WiFiComponent::load_fast_connect_settings_() { if (this->fast_connect_pref_.load(&fast_connect_save)) { // Validate saved AP index - if (fast_connect_save.ap_index >= this->sta_.size()) { + if (static_cast(fast_connect_save.ap_index) >= this->sta_.size()) { ESP_LOGW(TAG, "Saved AP index out of bounds"); return false; } From 703b1cf314c18dada13f349983d4613db588deb8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 13:51:47 -0600 Subject: [PATCH 22/65] cleanup --- esphome/components/wifi/wifi_component.cpp | 4 ++-- esphome/components/wifi/wifi_component.h | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index f774f6b64a..da07f0eab7 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -821,7 +821,7 @@ void WiFiComponent::retry_connect() { if (this->trying_loaded_ap_) { this->trying_loaded_ap_ = false; this->selected_sta_index_ = 0; // Retry from the first configured AP - this->num_retried_ = 0; + this->reset_for_next_ap_attempt_(); } else if (this->selected_sta_index_ >= static_cast(this->sta_.size()) - 1) { // Exhausted all configured APs, fall back to full scan ESP_LOGW(TAG, "No more APs to try, starting scan"); @@ -831,7 +831,7 @@ void WiFiComponent::retry_connect() { } else { // Try next AP this->selected_sta_index_++; - this->num_retried_ = 0; + this->reset_for_next_ap_attempt_(); } #else if (this->num_retried_ > 5) { diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 2e7a0728b4..739585c6fc 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -385,6 +385,13 @@ class WiFiComponent : public Component { this->start_connecting(connection_params, two); } + // Reset state for next fast connect AP attempt + // Clears old scan data so the new AP is tried with config only (SSID without specific BSSID/channel) + void reset_for_next_ap_attempt_() { + this->num_retried_ = 0; + this->scan_result_.clear(); + } + void wifi_loop_(); bool wifi_mode_(optional sta, optional ap); bool wifi_sta_pre_setup_(); From 4439b45fba239f37aa99035e3fa7e72239fe78e8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 13:52:32 -0600 Subject: [PATCH 23/65] cleanup --- esphome/components/wifi/wifi_component.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 739585c6fc..284c2eb12f 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -385,12 +385,14 @@ class WiFiComponent : public Component { this->start_connecting(connection_params, two); } +#ifdef USE_WIFI_FAST_CONNECT // Reset state for next fast connect AP attempt // Clears old scan data so the new AP is tried with config only (SSID without specific BSSID/channel) void reset_for_next_ap_attempt_() { this->num_retried_ = 0; this->scan_result_.clear(); } +#endif void wifi_loop_(); bool wifi_mode_(optional sta, optional ap); From ef680933dceb66625de3f58a90b2aedf25c9aa34 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 13:55:08 -0600 Subject: [PATCH 24/65] cleanup --- esphome/components/wifi/wifi_component.cpp | 20 +++++++++----------- esphome/components/wifi/wifi_component.h | 3 +++ 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index da07f0eab7..1373a53079 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -174,7 +174,8 @@ void WiFiComponent::loop() { // All APs tried, fall back to scanning this->start_scanning(); } else { - // NOTE: This check may not make sense here as it could interfere with AP cycling + // Safety check: Ensure selected_sta_index_ is valid before retrying + // (should already be set by retry_connect(), but check for robustness) this->reset_selected_ap_to_first_if_invalid_(); this->start_connecting_to_selected_(false); } @@ -708,11 +709,6 @@ void WiFiComponent::check_scanning_finished() { return; } -#ifdef USE_WIFI_FAST_CONNECT - // Scan found a network, reset exhausted flag to allow fast connect to work next time - this->fast_connect_exhausted_ = false; -#endif - yield(); this->start_connecting_to_selected_(false); @@ -923,11 +919,13 @@ bool WiFiComponent::load_fast_connect_settings_() { void WiFiComponent::save_fast_connect_settings_() { bssid_t bssid = wifi_bssid(); uint8_t channel = get_wifi_channel(); + int8_t ap_index = this->selected_sta_index_ >= 0 ? this->selected_sta_index_ : 0; - // Skip save if settings haven't changed (compare with current scan result if available) - if (!this->scan_result_.empty()) { - const WiFiScanResult &scan = this->scan_result_[0]; - if (bssid == scan.get_bssid() && channel == scan.get_channel()) { + // Skip save if settings haven't changed (compare with previously saved settings to reduce flash wear) + SavedWifiFastConnectSettings previous_save{}; + if (this->fast_connect_pref_.load(&previous_save)) { + if (memcmp(previous_save.bssid, bssid.data(), 6) == 0 && previous_save.channel == channel && + previous_save.ap_index == ap_index) { return; // No change, nothing to save } } @@ -935,7 +933,7 @@ void WiFiComponent::save_fast_connect_settings_() { SavedWifiFastConnectSettings fast_connect_save{}; memcpy(fast_connect_save.bssid, bssid.data(), 6); fast_connect_save.channel = channel; - fast_connect_save.ap_index = this->selected_sta_index_ >= 0 ? this->selected_sta_index_ : 0; + fast_connect_save.ap_index = ap_index; this->fast_connect_pref_.save(&fast_connect_save); diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 284c2eb12f..f81597fba5 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -218,6 +218,8 @@ class WiFiComponent : public Component { WiFiComponent(); void set_sta(const WiFiAP &ap); + // Returns a copy of the currently selected AP configuration + // Note: This copies the 88-byte WiFiAP. Only used by WiFiConfigureAction for state save/restore. WiFiAP get_sta(); void init_sta(size_t count); void add_sta(const WiFiAP &ap); @@ -471,6 +473,7 @@ class WiFiComponent : public Component { uint8_t num_retried_{0}; // Index into sta_ array for the currently selected AP configuration (-1 = none selected) // Used to access password, manual_ip, priority, EAP settings, and hidden flag + // int8_t limits to 127 APs which should be sufficient for all practical use cases int8_t selected_sta_index_{-1}; #if USE_NETWORK_IPV6 uint8_t num_ipv6_addresses_{0}; From 190668c25f6391cb2b06b23510b6e4cb70df3604 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 13:58:39 -0600 Subject: [PATCH 25/65] fix false positive logging --- esphome/components/wifi/wifi_component.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 1373a53079..65a9491897 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -730,8 +730,10 @@ void WiFiComponent::check_connecting_finished() { } ESP_LOGI(TAG, "Connected"); - // We won't retry hidden networks unless a reconnect fails more than three times again - if (const WiFiAP *config = this->get_selected_sta_(); this->retry_hidden_ && config && !config->get_hidden()) { + // Warn if we had to retry with hidden network mode for a network that's not marked hidden + // Only warn if we actually connected without scan data (SSID only), not if scan succeeded on retry + if (const WiFiAP *config = this->get_selected_sta_(); + this->retry_hidden_ && config && !config->get_hidden() && this->scan_result_.empty()) { ESP_LOGW(TAG, "Network '%s' should be marked as hidden", config->get_ssid().c_str()); } this->retry_hidden_ = false; From d8b419b60cbf3dcf24a20d4671f21aa3d6f5f809 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 14:42:42 -0600 Subject: [PATCH 26/65] not needed --- esphome/components/wifi/wifi_component.cpp | 23 ++++++++-------------- esphome/components/wifi/wifi_component.h | 1 - 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 65a9491897..880b6960ea 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -170,15 +170,10 @@ void WiFiComponent::loop() { this->status_set_warning(LOG_STR("waiting to reconnect")); if (millis() - this->action_started_ > 5000) { #ifdef USE_WIFI_FAST_CONNECT - if (this->fast_connect_exhausted_) { - // All APs tried, fall back to scanning - this->start_scanning(); - } else { - // Safety check: Ensure selected_sta_index_ is valid before retrying - // (should already be set by retry_connect(), but check for robustness) - this->reset_selected_ap_to_first_if_invalid_(); - this->start_connecting_to_selected_(false); - } + // Safety check: Ensure selected_sta_index_ is valid before retrying + // (should already be set by retry_connect(), but check for robustness) + this->reset_selected_ap_to_first_if_invalid_(); + this->start_connecting_to_selected_(false); #else if (this->retry_hidden_) { this->reset_selected_ap_to_first_if_invalid_(); @@ -759,7 +754,6 @@ void WiFiComponent::check_connecting_finished() { this->num_retried_ = 0; #ifdef USE_WIFI_FAST_CONNECT - this->fast_connect_exhausted_ = false; // Reset on successful connection this->save_fast_connect_settings_(); #endif @@ -821,11 +815,10 @@ void WiFiComponent::retry_connect() { this->selected_sta_index_ = 0; // Retry from the first configured AP this->reset_for_next_ap_attempt_(); } else if (this->selected_sta_index_ >= static_cast(this->sta_.size()) - 1) { - // Exhausted all configured APs, fall back to full scan - ESP_LOGW(TAG, "No more APs to try, starting scan"); - this->fast_connect_exhausted_ = true; - this->restart_adapter(); - return; + // Exhausted all configured APs, cycle back to first + // Each AP is tried with SSID only (no BSSID/channel) which triggers ESP-IDF internal scanning + this->selected_sta_index_ = 0; + this->reset_for_next_ap_attempt_(); } else { // Try next AP this->selected_sta_index_++; diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index f81597fba5..9c7f9a4e7a 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -482,7 +482,6 @@ class WiFiComponent : public Component { // Group all boolean values together #ifdef USE_WIFI_FAST_CONNECT bool trying_loaded_ap_{false}; - bool fast_connect_exhausted_{false}; // All APs tried, fall back to scan #endif bool retry_hidden_{false}; bool has_ap_{false}; From ebda7dace0852b56b0aa02e6a909437c5bc352be Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 14:46:11 -0600 Subject: [PATCH 27/65] not needed --- esphome/components/wifi/wifi_component.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 880b6960ea..12c2dda128 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -815,10 +815,13 @@ void WiFiComponent::retry_connect() { this->selected_sta_index_ = 0; // Retry from the first configured AP this->reset_for_next_ap_attempt_(); } else if (this->selected_sta_index_ >= static_cast(this->sta_.size()) - 1) { - // Exhausted all configured APs, cycle back to first + // Exhausted all configured APs, restart adapter and cycle back to first + // Restart clears any stuck WiFi driver state // Each AP is tried with SSID only (no BSSID/channel) which triggers ESP-IDF internal scanning + ESP_LOGW(TAG, "No more APs to try"); this->selected_sta_index_ = 0; this->reset_for_next_ap_attempt_(); + this->restart_adapter(); } else { // Try next AP this->selected_sta_index_++; From e4a56c6bc9b498fe7830705b16c2619cc2ebf86b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 14:49:52 -0600 Subject: [PATCH 28/65] not needed --- esphome/components/wifi/wifi_component.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 12c2dda128..59e6faa17c 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -817,7 +817,8 @@ void WiFiComponent::retry_connect() { } else if (this->selected_sta_index_ >= static_cast(this->sta_.size()) - 1) { // Exhausted all configured APs, restart adapter and cycle back to first // Restart clears any stuck WiFi driver state - // Each AP is tried with SSID only (no BSSID/channel) which triggers ESP-IDF internal scanning + // Each AP is tried with config data only (SSID + optional BSSID/channel if user configured them) + // Typically SSID only, which triggers ESP-IDF internal scanning ESP_LOGW(TAG, "No more APs to try"); this->selected_sta_index_ = 0; this->reset_for_next_ap_attempt_(); From 1fb233e22fe890cc2132d5d9df3bd51090462836 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 15:07:47 -0600 Subject: [PATCH 29/65] fix false positive logging --- esphome/components/wifi/wifi_component.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 59e6faa17c..c9b72a0c72 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -922,11 +922,9 @@ void WiFiComponent::save_fast_connect_settings_() { // Skip save if settings haven't changed (compare with previously saved settings to reduce flash wear) SavedWifiFastConnectSettings previous_save{}; - if (this->fast_connect_pref_.load(&previous_save)) { - if (memcmp(previous_save.bssid, bssid.data(), 6) == 0 && previous_save.channel == channel && - previous_save.ap_index == ap_index) { - return; // No change, nothing to save - } + if (this->fast_connect_pref_.load(&previous_save) && memcmp(previous_save.bssid, bssid.data(), 6) == 0 && + previous_save.channel == channel && previous_save.ap_index == ap_index) { + return; // No change, nothing to save } SavedWifiFastConnectSettings fast_connect_save{}; From 0044c51474cf6a07bb8db9874ef9dc8fc404cb4a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 15:37:01 -0600 Subject: [PATCH 30/65] defensive to make bot happy --- esphome/components/wifi/wifi_component.cpp | 11 ++++++++--- esphome/components/wifi/wifi_component.h | 4 ++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index c9b72a0c72..2bd86949e3 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -364,7 +364,8 @@ WiFiAP WiFiComponent::build_selected_ap_() const { params.set_hidden(true); // For hidden networks, clear BSSID and channel even if set in config // There might be multiple hidden networks with same SSID but we can't know which is correct - // Rely on probe-req with just SSID. Leaving channel empty triggers ALL_CHANNEL_SCAN. + // Rely on probe-req with just SSID. Empty channel triggers ALL_CHANNEL_SCAN. + // Note: Scan data is never used for hidden networks (see check below at line ~390) params.set_bssid(optional{}); params.set_channel(optional{}); } else { @@ -810,7 +811,11 @@ void WiFiComponent::retry_connect() { if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_() && (this->num_retried_ > 3 || this->error_from_callback_)) { #ifdef USE_WIFI_FAST_CONNECT - if (this->trying_loaded_ap_) { + if (this->sta_.empty()) { + // No configured networks - shouldn't happen in fast_connect mode, but handle defensively + ESP_LOGW(TAG, "No configured networks available"); + this->restart_adapter(); + } else if (this->trying_loaded_ap_) { this->trying_loaded_ap_ = false; this->selected_sta_index_ = 0; // Retry from the first configured AP this->reset_for_next_ap_attempt_(); @@ -890,7 +895,7 @@ bool WiFiComponent::load_fast_connect_settings_() { if (this->fast_connect_pref_.load(&fast_connect_save)) { // Validate saved AP index - if (static_cast(fast_connect_save.ap_index) >= this->sta_.size()) { + if (fast_connect_save.ap_index < 0 || static_cast(fast_connect_save.ap_index) >= this->sta_.size()) { ESP_LOGW(TAG, "Saved AP index out of bounds"); return false; } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 9c7f9a4e7a..defce1d0ac 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -375,6 +375,10 @@ class WiFiComponent : public Component { for (size_t i = 0; i < this->sta_.size(); i++) { if (scan_res.matches(this->sta_[i])) { + if (i > std::numeric_limits::max()) { + ESP_LOGE(TAG, "Matched AP index %zu exceeds int8_t range", i); + continue; + } this->selected_sta_index_ = static_cast(i); // Links scan_result_[0] with sta_[i] return true; } From b74f415509f7e50a792f603538eba1969e29cc2a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 15:37:58 -0600 Subject: [PATCH 31/65] defensive to make bot happy --- esphome/components/wifi/wifi_component.cpp | 4 ++-- esphome/components/wifi/wifi_component.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 2bd86949e3..a6f44ac758 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -813,7 +813,7 @@ void WiFiComponent::retry_connect() { #ifdef USE_WIFI_FAST_CONNECT if (this->sta_.empty()) { // No configured networks - shouldn't happen in fast_connect mode, but handle defensively - ESP_LOGW(TAG, "No configured networks available"); + ESP_LOGW(TAG, "No configured networks"); this->restart_adapter(); } else if (this->trying_loaded_ap_) { this->trying_loaded_ap_ = false; @@ -896,7 +896,7 @@ bool WiFiComponent::load_fast_connect_settings_() { if (this->fast_connect_pref_.load(&fast_connect_save)) { // Validate saved AP index if (fast_connect_save.ap_index < 0 || static_cast(fast_connect_save.ap_index) >= this->sta_.size()) { - ESP_LOGW(TAG, "Saved AP index out of bounds"); + ESP_LOGW(TAG, "AP index out of bounds"); return false; } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index defce1d0ac..3dc31880a8 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -376,7 +376,7 @@ class WiFiComponent : public Component { for (size_t i = 0; i < this->sta_.size(); i++) { if (scan_res.matches(this->sta_[i])) { if (i > std::numeric_limits::max()) { - ESP_LOGE(TAG, "Matched AP index %zu exceeds int8_t range", i); + ESP_LOGE(TAG, "AP index %zu too large", i); continue; } this->selected_sta_index_ = static_cast(i); // Links scan_result_[0] with sta_[i] From b366bc8dbad8ece73fd6681b57793b50a8c952f1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 15:40:32 -0600 Subject: [PATCH 32/65] defensive to make bot happy --- esphome/components/wifi/wifi_component.cpp | 21 +++++++++++++++++++++ esphome/components/wifi/wifi_component.h | 21 +-------------------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index a6f44ac758..6e4611d765 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -396,6 +396,27 @@ WiFiAP WiFiComponent::build_selected_ap_() const { return params; } +bool WiFiComponent::sync_selected_sta_to_best_scan_result_() { + if (this->scan_result_.empty()) + return false; + + const WiFiScanResult &scan_res = this->scan_result_[0]; + if (!scan_res.get_matches()) + return false; + + for (size_t i = 0; i < this->sta_.size(); i++) { + if (scan_res.matches(this->sta_[i])) { + if (i > std::numeric_limits::max()) { + ESP_LOGE(TAG, "AP index %zu too large", i); + return false; + } + this->selected_sta_index_ = static_cast(i); // Links scan_result_[0] with sta_[i] + return true; + } + } + return false; +} + WiFiAP WiFiComponent::get_sta() { const WiFiAP *config = this->get_selected_sta_(); return config ? *config : WiFiAP{}; diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 3dc31880a8..6cdab1660f 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -365,26 +365,7 @@ class WiFiComponent : public Component { // Find which sta_[i] matches scan_result_[0] and set selected_sta_index_ (scan done path) // Returns true if match found, false otherwise - bool sync_selected_sta_to_best_scan_result_() { - if (this->scan_result_.empty()) - return false; - - const WiFiScanResult &scan_res = this->scan_result_[0]; - if (!scan_res.get_matches()) - return false; - - for (size_t i = 0; i < this->sta_.size(); i++) { - if (scan_res.matches(this->sta_[i])) { - if (i > std::numeric_limits::max()) { - ESP_LOGE(TAG, "AP index %zu too large", i); - continue; - } - this->selected_sta_index_ = static_cast(i); // Links scan_result_[0] with sta_[i] - return true; - } - } - return false; - } + bool sync_selected_sta_to_best_scan_result_(); void start_connecting_to_selected_(bool two) { WiFiAP connection_params = this->build_selected_ap_(); From 0eafe5259f00be1860943ed8234810f94b4872e3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 15:44:48 -0600 Subject: [PATCH 33/65] defensive to make bot happy --- esphome/components/wifi/wifi_component.cpp | 2 +- esphome/components/wifi/wifi_component.h | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 6e4611d765..131fd122d7 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -365,7 +365,7 @@ WiFiAP WiFiComponent::build_selected_ap_() const { // For hidden networks, clear BSSID and channel even if set in config // There might be multiple hidden networks with same SSID but we can't know which is correct // Rely on probe-req with just SSID. Empty channel triggers ALL_CHANNEL_SCAN. - // Note: Scan data is never used for hidden networks (see check below at line ~390) + // Note: Scan data is never used for hidden networks (see !params.get_hidden() check below) params.set_bssid(optional{}); params.set_channel(optional{}); } else { diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 6cdab1660f..04adb57b44 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -358,7 +358,12 @@ class WiFiComponent : public Component { // Set selected sta with a temporary scan result (fast connect path) void set_selected_sta_with_scan_(int8_t sta_index, const WiFiScanResult &scan) { +#ifdef USE_RP2040 + this->scan_result_.clear(); + this->scan_result_.reserve(1); +#else this->scan_result_.init(1); +#endif this->scan_result_.push_back(scan); this->selected_sta_index_ = sta_index; } From db0b1e0b5c50ceb27b1bc104fb507b87de0a69e4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 15:45:41 -0600 Subject: [PATCH 34/65] defensive to make bot happy --- esphome/components/wifi/wifi_component.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 131fd122d7..d70903b0d5 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -840,7 +840,7 @@ void WiFiComponent::retry_connect() { this->trying_loaded_ap_ = false; this->selected_sta_index_ = 0; // Retry from the first configured AP this->reset_for_next_ap_attempt_(); - } else if (this->selected_sta_index_ >= static_cast(this->sta_.size()) - 1) { + } else if (static_cast(this->selected_sta_index_) >= this->sta_.size() - 1) { // Exhausted all configured APs, restart adapter and cycle back to first // Restart clears any stuck WiFi driver state // Each AP is tried with config data only (SSID + optional BSSID/channel if user configured them) From 8eb509f8f0845e620aa44cd46011b87159d418cb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 15:52:11 -0600 Subject: [PATCH 35/65] revert copilot suggestion .. we will never have more then 5 anyways --- esphome/components/wifi/wifi_component.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index d70903b0d5..c4d106c0fe 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -840,7 +840,7 @@ void WiFiComponent::retry_connect() { this->trying_loaded_ap_ = false; this->selected_sta_index_ = 0; // Retry from the first configured AP this->reset_for_next_ap_attempt_(); - } else if (static_cast(this->selected_sta_index_) >= this->sta_.size() - 1) { + } else if (this->selected_sta_index_ >= static_cast(this->sta_.size()) - 1) { // Exhausted all configured APs, restart adapter and cycle back to first // Restart clears any stuck WiFi driver state // Each AP is tried with config data only (SSID + optional BSSID/channel if user configured them) From 47874ef516d4d3014ae60ea2ad929f9552b39e6a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 15:53:48 -0600 Subject: [PATCH 36/65] revert copilot suggestion .. we will never have more then 5 anyways --- esphome/components/wifi/wifi_component.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index c4d106c0fe..0d44f2f47f 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -841,6 +841,7 @@ void WiFiComponent::retry_connect() { this->selected_sta_index_ = 0; // Retry from the first configured AP this->reset_for_next_ap_attempt_(); } else if (this->selected_sta_index_ >= static_cast(this->sta_.size()) - 1) { + // Cast size to int (not int8_t which overflows at 127, not size_t which wraps negative indices) // Exhausted all configured APs, restart adapter and cycle back to first // Restart clears any stuck WiFi driver state // Each AP is tried with config data only (SSID + optional BSSID/channel if user configured them) @@ -944,6 +945,8 @@ bool WiFiComponent::load_fast_connect_settings_() { void WiFiComponent::save_fast_connect_settings_() { bssid_t bssid = wifi_bssid(); uint8_t channel = get_wifi_channel(); + // selected_sta_index_ is always valid here (called only after successful connection) + // Fallback to 0 is defensive programming for robustness int8_t ap_index = this->selected_sta_index_ >= 0 ? this->selected_sta_index_ : 0; // Skip save if settings haven't changed (compare with previously saved settings to reduce flash wear) From 38cf003bf3758a0344115dff0ef7b6f0cd9de8c3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 15:58:05 -0600 Subject: [PATCH 37/65] preen --- esphome/components/wifi/wifi_component.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 0d44f2f47f..a2de77db95 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -366,8 +366,8 @@ WiFiAP WiFiComponent::build_selected_ap_() const { // There might be multiple hidden networks with same SSID but we can't know which is correct // Rely on probe-req with just SSID. Empty channel triggers ALL_CHANNEL_SCAN. // Note: Scan data is never used for hidden networks (see !params.get_hidden() check below) - params.set_bssid(optional{}); - params.set_channel(optional{}); + params.set_bssid({}); + params.set_channel({}); } else { params.set_bssid(config->get_bssid()); params.set_channel(config->get_channel()); From 541e0cfde8db500db3b9739a0ffbcf270766ac66 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 16:00:37 -0600 Subject: [PATCH 38/65] preen --- esphome/components/wifi/wifi_component.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index a2de77db95..a7bc809312 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -840,8 +840,7 @@ void WiFiComponent::retry_connect() { this->trying_loaded_ap_ = false; this->selected_sta_index_ = 0; // Retry from the first configured AP this->reset_for_next_ap_attempt_(); - } else if (this->selected_sta_index_ >= static_cast(this->sta_.size()) - 1) { - // Cast size to int (not int8_t which overflows at 127, not size_t which wraps negative indices) + } else if (this->selected_sta_index_ >= static_cast(this->sta_.size()) - 1) { // Exhausted all configured APs, restart adapter and cycle back to first // Restart clears any stuck WiFi driver state // Each AP is tried with config data only (SSID + optional BSSID/channel if user configured them) From 7d48df9fe152105e1d1c69901cfc795ea29b1f83 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 16:04:00 -0600 Subject: [PATCH 39/65] remove overly defensive suggestions from copilot --- esphome/components/wifi/__init__.py | 8 +++++++- esphome/components/wifi/wifi_component.cpp | 14 +++++--------- esphome/components/wifi/wifi_component.h | 2 +- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index b980bab4aa..5f4190a933 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -54,6 +54,10 @@ AUTO_LOAD = ["network"] NO_WIFI_VARIANTS = [const.VARIANT_ESP32H2, const.VARIANT_ESP32P4] CONF_SAVE = "save" +# Maximum number of WiFi networks that can be configured +# Limited to 127 because selected_sta_index_ is int8_t in C++ +MAX_WIFI_NETWORKS = 127 + wifi_ns = cg.esphome_ns.namespace("wifi") EAPAuth = wifi_ns.struct("EAPAuth") ManualIP = wifi_ns.struct("ManualIP") @@ -260,7 +264,9 @@ CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(WiFiComponent), - cv.Optional(CONF_NETWORKS): cv.ensure_list(WIFI_NETWORK_STA), + cv.Optional(CONF_NETWORKS): cv.All( + cv.ensure_list(WIFI_NETWORK_STA), cv.Length(max=MAX_WIFI_NETWORKS) + ), cv.Optional(CONF_SSID): cv.ssid, cv.Optional(CONF_PASSWORD): validate_password, cv.Optional(CONF_MANUAL_IP): STA_MANUAL_IP_SCHEMA, diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index a7bc809312..9e85b194c7 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -406,10 +406,8 @@ bool WiFiComponent::sync_selected_sta_to_best_scan_result_() { for (size_t i = 0; i < this->sta_.size(); i++) { if (scan_res.matches(this->sta_[i])) { - if (i > std::numeric_limits::max()) { - ESP_LOGE(TAG, "AP index %zu too large", i); - return false; - } + // Safe cast: sta_.size() limited to MAX_WIFI_NETWORKS (127) in __init__.py validation + // No overflow check needed - YAML validation prevents >127 networks this->selected_sta_index_ = static_cast(i); // Links scan_result_[0] with sta_[i] return true; } @@ -832,15 +830,13 @@ void WiFiComponent::retry_connect() { if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_() && (this->num_retried_ > 3 || this->error_from_callback_)) { #ifdef USE_WIFI_FAST_CONNECT - if (this->sta_.empty()) { - // No configured networks - shouldn't happen in fast_connect mode, but handle defensively - ESP_LOGW(TAG, "No configured networks"); - this->restart_adapter(); - } else if (this->trying_loaded_ap_) { + // No empty check needed - YAML validation requires at least one network for fast_connect + if (this->trying_loaded_ap_) { this->trying_loaded_ap_ = false; this->selected_sta_index_ = 0; // Retry from the first configured AP this->reset_for_next_ap_attempt_(); } else if (this->selected_sta_index_ >= static_cast(this->sta_.size()) - 1) { + // Safe cast: sta_.size() limited to MAX_WIFI_NETWORKS (127) in __init__.py validation // Exhausted all configured APs, restart adapter and cycle back to first // Restart clears any stuck WiFi driver state // Each AP is tried with config data only (SSID + optional BSSID/channel if user configured them) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 04adb57b44..772d63b701 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -463,7 +463,7 @@ class WiFiComponent : public Component { uint8_t num_retried_{0}; // Index into sta_ array for the currently selected AP configuration (-1 = none selected) // Used to access password, manual_ip, priority, EAP settings, and hidden flag - // int8_t limits to 127 APs which should be sufficient for all practical use cases + // int8_t limits to 127 APs (enforced in __init__.py via MAX_WIFI_NETWORKS) int8_t selected_sta_index_{-1}; #if USE_NETWORK_IPV6 uint8_t num_ipv6_addresses_{0}; From 7041c3324bb3b413c2e2e46f44731beaea3d4c1a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 16:05:15 -0600 Subject: [PATCH 40/65] revert yet another bad copilot suggesiton --- esphome/components/wifi/wifi_component.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 9e85b194c7..38d4fc44b5 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -366,8 +366,8 @@ WiFiAP WiFiComponent::build_selected_ap_() const { // There might be multiple hidden networks with same SSID but we can't know which is correct // Rely on probe-req with just SSID. Empty channel triggers ALL_CHANNEL_SCAN. // Note: Scan data is never used for hidden networks (see !params.get_hidden() check below) - params.set_bssid({}); - params.set_channel({}); + params.set_bssid(optional{}); + params.set_channel(optional{}); } else { params.set_bssid(config->get_bssid()); params.set_channel(config->get_channel()); From 8a927918872bc328ae2d7823e0cc81203a27628a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 16:08:14 -0600 Subject: [PATCH 41/65] remove non-logical check --- esphome/components/wifi/wifi_component.cpp | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 38d4fc44b5..8812f1becf 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -382,15 +382,11 @@ WiFiAP WiFiComponent::build_selected_ap_() const { // Therefore scan_result_[0] is guaranteed to match sta_[selected_sta_index_] if (!this->scan_result_.empty()) { const WiFiScanResult &scan = this->scan_result_[0]; - - if (!params.get_hidden()) { - // Selected network is visible, override with data from the scan. - // Limit the connect params to only connect to exactly this network - // (network selection is done during scan phase). - params.set_ssid(scan.get_ssid()); - params.set_bssid(scan.get_bssid()); - params.set_channel(scan.get_channel()); - } + // If we have scan data, the network is visible (not hidden) - use it regardless of config + // Hidden networks don't appear in scan results, so presence of scan data is ground truth + params.set_ssid(scan.get_ssid()); + params.set_bssid(scan.get_bssid()); + params.set_channel(scan.get_channel()); } return params; From cde767d83da400a90c1c3bf39379745b18723a68 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 16:12:19 -0600 Subject: [PATCH 42/65] improve comment --- esphome/components/wifi/wifi_component.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 772d63b701..ad879ef7d5 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -356,7 +356,7 @@ class WiFiComponent : public Component { // SYNCHRONIZATION HELPERS: Encapsulate the relationship between selected_sta_index_ and scan_result_ - // Set selected sta with a temporary scan result (fast connect path) + // Add temporary scan result and set selected sta index (fast connect path) void set_selected_sta_with_scan_(int8_t sta_index, const WiFiScanResult &scan) { #ifdef USE_RP2040 this->scan_result_.clear(); From a0b273c6f32dcbb2f9a1d270671c2cb163f0c9cc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 16:23:56 -0600 Subject: [PATCH 43/65] not hidden if found --- esphome/components/wifi/wifi_component.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 8812f1becf..2f3f78de08 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -384,6 +384,7 @@ WiFiAP WiFiComponent::build_selected_ap_() const { const WiFiScanResult &scan = this->scan_result_[0]; // If we have scan data, the network is visible (not hidden) - use it regardless of config // Hidden networks don't appear in scan results, so presence of scan data is ground truth + params.set_hidden(false); params.set_ssid(scan.get_ssid()); params.set_bssid(scan.get_bssid()); params.set_channel(scan.get_channel()); From f4d2b000dae6f359e2f06a3202a4e93e3f33c057 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 16:25:23 -0600 Subject: [PATCH 44/65] reduce --- esphome/components/wifi/wifi_component.cpp | 58 ++++++++++------------ 1 file changed, 26 insertions(+), 32 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 2f3f78de08..a2d43b7d2e 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -347,49 +347,43 @@ WiFiAP WiFiComponent::build_selected_ap_() const { WiFiAP params; if (const WiFiAP *config = this->get_selected_sta_()) { - // Copy config data (password, manual IP, priority, EAP) + // Copy config data that's never overridden (password, manual IP, priority, EAP) params.set_password(config->get_password()); params.set_manual_ip(config->get_manual_ip()); params.set_priority(config->get_priority()); - #ifdef USE_WIFI_WPA2_EAP params.set_eap(config->get_eap()); #endif - // Set network parameters from config - // These will be used as-is for hidden networks, or overridden by scan for visible networks - params.set_ssid(config->get_ssid()); - - if (config->get_hidden()) { - params.set_hidden(true); - // For hidden networks, clear BSSID and channel even if set in config - // There might be multiple hidden networks with same SSID but we can't know which is correct - // Rely on probe-req with just SSID. Empty channel triggers ALL_CHANNEL_SCAN. - // Note: Scan data is never used for hidden networks (see !params.get_hidden() check below) - params.set_bssid(optional{}); - params.set_channel(optional{}); + // SYNCHRONIZATION: selected_sta_index_ and scan_result_[0] are kept in sync: + // - wifi_scan_done() sorts all scan results by priority/RSSI (best first) + // - It then finds which sta_[i] config matches scan_result_[0] + // - Sets selected_sta_index_ = i to record that matching config + // Therefore scan_result_[0] is guaranteed to match sta_[selected_sta_index_] + if (!this->scan_result_.empty()) { + // Use scan data - proves network is visible (not hidden) + const WiFiScanResult &scan = this->scan_result_[0]; + params.set_hidden(false); + params.set_ssid(scan.get_ssid()); + params.set_bssid(scan.get_bssid()); + params.set_channel(scan.get_channel()); } else { - params.set_bssid(config->get_bssid()); - params.set_channel(config->get_channel()); + // Use config settings + params.set_ssid(config->get_ssid()); + if (config->get_hidden()) { + params.set_hidden(true); + // For hidden networks, clear BSSID and channel even if set in config + // There might be multiple hidden networks with same SSID but we can't know which is correct + // Rely on probe-req with just SSID. Empty channel triggers ALL_CHANNEL_SCAN. + params.set_bssid(optional{}); + params.set_channel(optional{}); + } else { + params.set_bssid(config->get_bssid()); + params.set_channel(config->get_channel()); + } } } - // Overlay scan result data (if available) - // SYNCHRONIZATION: selected_sta_index_ and scan_result_[0] are kept in sync: - // - wifi_scan_done() sorts all scan results by priority/RSSI (best first) - // - It then finds which sta_[i] config matches scan_result_[0] - // - Sets selected_sta_index_ = i to record that matching config - // Therefore scan_result_[0] is guaranteed to match sta_[selected_sta_index_] - if (!this->scan_result_.empty()) { - const WiFiScanResult &scan = this->scan_result_[0]; - // If we have scan data, the network is visible (not hidden) - use it regardless of config - // Hidden networks don't appear in scan results, so presence of scan data is ground truth - params.set_hidden(false); - params.set_ssid(scan.get_ssid()); - params.set_bssid(scan.get_bssid()); - params.set_channel(scan.get_channel()); - } - return params; } From 645820304fa4cbf5f2ce85de35d5f10edfef8a55 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 16:28:54 -0600 Subject: [PATCH 45/65] reduce complexity --- esphome/components/wifi/wifi_component.cpp | 6 +++--- esphome/components/wifi/wifi_component.h | 7 +------ 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index a2d43b7d2e..fb7ca15a7a 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -343,7 +343,7 @@ void WiFiComponent::clear_sta() { this->selected_sta_index_ = -1; } -WiFiAP WiFiComponent::build_selected_ap_() const { +void WiFiComponent::start_connecting_to_selected_(bool two) { WiFiAP params; if (const WiFiAP *config = this->get_selected_sta_()) { @@ -384,7 +384,7 @@ WiFiAP WiFiComponent::build_selected_ap_() const { } } - return params; + this->start_connecting(params, two); } bool WiFiComponent::sync_selected_sta_to_best_scan_result_() { @@ -708,7 +708,7 @@ void WiFiComponent::check_scanning_finished() { // SYNCHRONIZATION POINT: Establish link between scan_result_[0] and selected_sta_index_ // After sorting, scan_result_[0] contains the best network. Now find which sta_[i] config // matches that network and record it in selected_sta_index_. This keeps the two indices - // synchronized so build_selected_ap_() can safely use both to build connection parameters. + // synchronized so start_connecting_to_selected_() can safely use both to build connection parameters. if (!this->sync_selected_sta_to_best_scan_result_()) { ESP_LOGW(TAG, "No matching network found"); this->retry_connect(); diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index ad879ef7d5..aba087c07b 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -339,7 +339,7 @@ class WiFiComponent : public Component { #endif // USE_WIFI_AP void print_connect_params_(); - WiFiAP build_selected_ap_() const; + void start_connecting_to_selected_(bool two); const WiFiAP *get_selected_sta_() const { if (this->selected_sta_index_ >= 0 && static_cast(this->selected_sta_index_) < this->sta_.size()) { @@ -372,11 +372,6 @@ class WiFiComponent : public Component { // Returns true if match found, false otherwise bool sync_selected_sta_to_best_scan_result_(); - void start_connecting_to_selected_(bool two) { - WiFiAP connection_params = this->build_selected_ap_(); - this->start_connecting(connection_params, two); - } - #ifdef USE_WIFI_FAST_CONNECT // Reset state for next fast connect AP attempt // Clears old scan data so the new AP is tried with config only (SSID without specific BSSID/channel) From df1ffbaf5d70e9685e32e94579733de7fdf5d2d4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 16:30:12 -0600 Subject: [PATCH 46/65] reduce complexity --- esphome/components/wifi/wifi_component.cpp | 67 +++++++++++----------- 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index fb7ca15a7a..b30b477e5e 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -344,43 +344,46 @@ void WiFiComponent::clear_sta() { } void WiFiComponent::start_connecting_to_selected_(bool two) { - WiFiAP params; + const WiFiAP *config = this->get_selected_sta_(); + if (!config) { + ESP_LOGE(TAG, "No config selected"); + return; + } - if (const WiFiAP *config = this->get_selected_sta_()) { - // Copy config data that's never overridden (password, manual IP, priority, EAP) - params.set_password(config->get_password()); - params.set_manual_ip(config->get_manual_ip()); - params.set_priority(config->get_priority()); + WiFiAP params; + // Copy config data that's never overridden (password, manual IP, priority, EAP) + params.set_password(config->get_password()); + params.set_manual_ip(config->get_manual_ip()); + params.set_priority(config->get_priority()); #ifdef USE_WIFI_WPA2_EAP - params.set_eap(config->get_eap()); + params.set_eap(config->get_eap()); #endif - // SYNCHRONIZATION: selected_sta_index_ and scan_result_[0] are kept in sync: - // - wifi_scan_done() sorts all scan results by priority/RSSI (best first) - // - It then finds which sta_[i] config matches scan_result_[0] - // - Sets selected_sta_index_ = i to record that matching config - // Therefore scan_result_[0] is guaranteed to match sta_[selected_sta_index_] - if (!this->scan_result_.empty()) { - // Use scan data - proves network is visible (not hidden) - const WiFiScanResult &scan = this->scan_result_[0]; - params.set_hidden(false); - params.set_ssid(scan.get_ssid()); - params.set_bssid(scan.get_bssid()); - params.set_channel(scan.get_channel()); + // SYNCHRONIZATION: selected_sta_index_ and scan_result_[0] are kept in sync: + // - wifi_scan_done() sorts all scan results by priority/RSSI (best first) + // - It then finds which sta_[i] config matches scan_result_[0] + // - Sets selected_sta_index_ = i to record that matching config + // Therefore scan_result_[0] is guaranteed to match sta_[selected_sta_index_] + if (!this->scan_result_.empty()) { + // Use scan data - proves network is visible (not hidden) + const WiFiScanResult &scan = this->scan_result_[0]; + params.set_hidden(false); + params.set_ssid(scan.get_ssid()); + params.set_bssid(scan.get_bssid()); + params.set_channel(scan.get_channel()); + } else { + // Use config settings + params.set_ssid(config->get_ssid()); + if (config->get_hidden()) { + params.set_hidden(true); + // For hidden networks, clear BSSID and channel even if set in config + // There might be multiple hidden networks with same SSID but we can't know which is correct + // Rely on probe-req with just SSID. Empty channel triggers ALL_CHANNEL_SCAN. + params.set_bssid(optional{}); + params.set_channel(optional{}); } else { - // Use config settings - params.set_ssid(config->get_ssid()); - if (config->get_hidden()) { - params.set_hidden(true); - // For hidden networks, clear BSSID and channel even if set in config - // There might be multiple hidden networks with same SSID but we can't know which is correct - // Rely on probe-req with just SSID. Empty channel triggers ALL_CHANNEL_SCAN. - params.set_bssid(optional{}); - params.set_channel(optional{}); - } else { - params.set_bssid(config->get_bssid()); - params.set_channel(config->get_channel()); - } + params.set_bssid(config->get_bssid()); + params.set_channel(config->get_channel()); } } From 936a6cb71e501acafd392fbd0d531a16ff08c2f0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 16:31:00 -0600 Subject: [PATCH 47/65] reduce complexity --- esphome/components/wifi/wifi_component.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index b30b477e5e..a0293cdaa3 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -365,9 +365,8 @@ void WiFiComponent::start_connecting_to_selected_(bool two) { // - Sets selected_sta_index_ = i to record that matching config // Therefore scan_result_[0] is guaranteed to match sta_[selected_sta_index_] if (!this->scan_result_.empty()) { - // Use scan data - proves network is visible (not hidden) + // Use scan data - network is visible (hidden defaults to false) const WiFiScanResult &scan = this->scan_result_[0]; - params.set_hidden(false); params.set_ssid(scan.get_ssid()); params.set_bssid(scan.get_bssid()); params.set_channel(scan.get_channel()); From df35036c8da50cdf32f522adc1046c19e362035d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 16:52:30 -0600 Subject: [PATCH 48/65] refator --- esphome/components/wifi/wifi_component.cpp | 57 +++++++++++++--------- esphome/components/wifi/wifi_component.h | 22 ++------- 2 files changed, 39 insertions(+), 40 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index a0293cdaa3..8bcd5ec35c 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -109,13 +109,15 @@ void WiFiComponent::start() { } #ifdef USE_WIFI_FAST_CONNECT - this->trying_loaded_ap_ = this->load_fast_connect_settings_(); + WiFiAP params; + this->trying_loaded_ap_ = this->load_fast_connect_settings_(params); if (!this->trying_loaded_ap_) { // FAST CONNECT FALLBACK: No saved settings available - // Use first config (will use SSID from config since scan_result_ is empty) + // Use first config (will use SSID from config) this->selected_sta_index_ = 0; + params = this->build_wifi_ap_from_selected_(); } - this->start_connecting_to_selected_(false); + this->start_connecting(params, false); #else this->start_scanning(); #endif @@ -173,11 +175,13 @@ void WiFiComponent::loop() { // Safety check: Ensure selected_sta_index_ is valid before retrying // (should already be set by retry_connect(), but check for robustness) this->reset_selected_ap_to_first_if_invalid_(); - this->start_connecting_to_selected_(false); + WiFiAP params = this->build_wifi_ap_from_selected_(); + this->start_connecting(params, false); #else if (this->retry_hidden_) { this->reset_selected_ap_to_first_if_invalid_(); - this->start_connecting_to_selected_(false); + WiFiAP params = this->build_wifi_ap_from_selected_(); + this->start_connecting(params, false); } else { this->start_scanning(); } @@ -343,11 +347,11 @@ void WiFiComponent::clear_sta() { this->selected_sta_index_ = -1; } -void WiFiComponent::start_connecting_to_selected_(bool two) { +WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { const WiFiAP *config = this->get_selected_sta_(); if (!config) { ESP_LOGE(TAG, "No config selected"); - return; + return {}; } WiFiAP params; @@ -386,7 +390,7 @@ void WiFiComponent::start_connecting_to_selected_(bool two) { } } - this->start_connecting(params, two); + return params; } bool WiFiComponent::sync_selected_sta_to_best_scan_result_() { @@ -710,7 +714,7 @@ void WiFiComponent::check_scanning_finished() { // SYNCHRONIZATION POINT: Establish link between scan_result_[0] and selected_sta_index_ // After sorting, scan_result_[0] contains the best network. Now find which sta_[i] config // matches that network and record it in selected_sta_index_. This keeps the two indices - // synchronized so start_connecting_to_selected_() can safely use both to build connection parameters. + // synchronized so build_wifi_ap_from_selected_() can safely use both to build connection parameters. if (!this->sync_selected_sta_to_best_scan_result_()) { ESP_LOGW(TAG, "No matching network found"); this->retry_connect(); @@ -719,7 +723,8 @@ void WiFiComponent::check_scanning_finished() { yield(); - this->start_connecting_to_selected_(false); + WiFiAP params = this->build_wifi_ap_from_selected_(); + this->start_connecting(params, false); } void WiFiComponent::dump_config() { @@ -861,7 +866,8 @@ void WiFiComponent::retry_connect() { if (this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTING) { yield(); this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING_2; - this->start_connecting_to_selected_(true); + WiFiAP params = this->build_wifi_ap_from_selected_(); + this->start_connecting(params, true); return; } @@ -900,7 +906,7 @@ bool WiFiComponent::is_esp32_improv_active_() { } #ifdef USE_WIFI_FAST_CONNECT -bool WiFiComponent::load_fast_connect_settings_() { +bool WiFiComponent::load_fast_connect_settings_(WiFiAP ¶ms) { SavedWifiFastConnectSettings fast_connect_save{}; if (this->fast_connect_pref_.load(&fast_connect_save)) { @@ -910,18 +916,25 @@ bool WiFiComponent::load_fast_connect_settings_() { return false; } - // Load BSSID from saved settings + // Set selected index for future operations (save, retry, etc) + this->selected_sta_index_ = fast_connect_save.ap_index; + + // Build WiFiAP directly from saved settings + config + const WiFiAP &config = this->sta_[fast_connect_save.ap_index]; + params.set_password(config.get_password()); + params.set_manual_ip(config.get_manual_ip()); + params.set_priority(config.get_priority()); +#ifdef USE_WIFI_WPA2_EAP + params.set_eap(config.get_eap()); +#endif + + // Use saved BSSID/channel from fast connect, SSID from config + params.set_ssid(config.get_ssid()); bssid_t bssid{}; std::copy(fast_connect_save.bssid, fast_connect_save.bssid + 6, bssid.begin()); - - // FAST CONNECT SUCCESS: Restore saved settings without scanning - // SYNCHRONIZATION: Link temporary scan result with sta_[saved_index] - // Unlike wifi_scan_done() which sorts then finds the match, here we know exactly - // which config was used before and create a matching temporary scan result - // Use SSID from config for the temporary scan result - const std::string &ssid = this->sta_[fast_connect_save.ap_index].get_ssid(); - WiFiScanResult fast_connect_scan(bssid, ssid, fast_connect_save.channel, 0, false, false); - this->set_selected_sta_with_scan_(fast_connect_save.ap_index, fast_connect_scan); + params.set_bssid(bssid); + params.set_channel(fast_connect_save.channel); + // hidden defaults to false (network was found before, so not hidden) ESP_LOGD(TAG, "Loaded fast_connect settings"); return true; diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index aba087c07b..48ecb3fe8d 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -339,7 +339,7 @@ class WiFiComponent : public Component { #endif // USE_WIFI_AP void print_connect_params_(); - void start_connecting_to_selected_(bool two); + WiFiAP build_wifi_ap_from_selected_() const; const WiFiAP *get_selected_sta_() const { if (this->selected_sta_index_ >= 0 && static_cast(this->selected_sta_index_) < this->sta_.size()) { @@ -354,22 +354,8 @@ class WiFiComponent : public Component { } } - // SYNCHRONIZATION HELPERS: Encapsulate the relationship between selected_sta_index_ and scan_result_ - - // Add temporary scan result and set selected sta index (fast connect path) - void set_selected_sta_with_scan_(int8_t sta_index, const WiFiScanResult &scan) { -#ifdef USE_RP2040 - this->scan_result_.clear(); - this->scan_result_.reserve(1); -#else - this->scan_result_.init(1); -#endif - this->scan_result_.push_back(scan); - this->selected_sta_index_ = sta_index; - } - - // Find which sta_[i] matches scan_result_[0] and set selected_sta_index_ (scan done path) - // Returns true if match found, false otherwise + // SYNCHRONIZATION HELPER: Find which sta_[i] matches scan_result_[0] and set selected_sta_index_ + // Returns true if match found, false otherwise (scan done path) bool sync_selected_sta_to_best_scan_result_(); #ifdef USE_WIFI_FAST_CONNECT @@ -408,7 +394,7 @@ class WiFiComponent : public Component { bool is_esp32_improv_active_(); #ifdef USE_WIFI_FAST_CONNECT - bool load_fast_connect_settings_(); + bool load_fast_connect_settings_(WiFiAP ¶ms); void save_fast_connect_settings_(); #endif From 03c5655201071491bc0bf454c13c2780b762dd32 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 16:58:40 -0600 Subject: [PATCH 49/65] dry --- esphome/components/wifi/wifi_component.cpp | 25 +++++++++++----------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 8bcd5ec35c..a15c8d826f 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -347,6 +347,16 @@ void WiFiComponent::clear_sta() { this->selected_sta_index_ = -1; } +// Helper to copy base config settings (password, manual_ip, priority, EAP) from source to dest +static void copy_wifi_ap_base_config(WiFiAP &dest, const WiFiAP &source) { + dest.set_password(source.get_password()); + dest.set_manual_ip(source.get_manual_ip()); + dest.set_priority(source.get_priority()); +#ifdef USE_WIFI_WPA2_EAP + dest.set_eap(source.get_eap()); +#endif +} + WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { const WiFiAP *config = this->get_selected_sta_(); if (!config) { @@ -356,12 +366,7 @@ WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { WiFiAP params; // Copy config data that's never overridden (password, manual IP, priority, EAP) - params.set_password(config->get_password()); - params.set_manual_ip(config->get_manual_ip()); - params.set_priority(config->get_priority()); -#ifdef USE_WIFI_WPA2_EAP - params.set_eap(config->get_eap()); -#endif + copy_wifi_ap_base_config(params, *config); // SYNCHRONIZATION: selected_sta_index_ and scan_result_[0] are kept in sync: // - wifi_scan_done() sorts all scan results by priority/RSSI (best first) @@ -921,12 +926,8 @@ bool WiFiComponent::load_fast_connect_settings_(WiFiAP ¶ms) { // Build WiFiAP directly from saved settings + config const WiFiAP &config = this->sta_[fast_connect_save.ap_index]; - params.set_password(config.get_password()); - params.set_manual_ip(config.get_manual_ip()); - params.set_priority(config.get_priority()); -#ifdef USE_WIFI_WPA2_EAP - params.set_eap(config.get_eap()); -#endif + // Copy config data that's never overridden (password, manual IP, priority, EAP) + copy_wifi_ap_base_config(params, config); // Use saved BSSID/channel from fast connect, SSID from config params.set_ssid(config.get_ssid()); From 2e1fd30ea0e21b3e31dd8ecf19c0a4df1e1d2305 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 17:00:42 -0600 Subject: [PATCH 50/65] dry --- esphome/components/wifi/wifi_component.cpp | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index a15c8d826f..c42a69f5e8 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -382,19 +382,14 @@ WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { } else { // Use config settings params.set_ssid(config->get_ssid()); - if (config->get_hidden()) { - params.set_hidden(true); - // For hidden networks, clear BSSID and channel even if set in config - // There might be multiple hidden networks with same SSID but we can't know which is correct - // Rely on probe-req with just SSID. Empty channel triggers ALL_CHANNEL_SCAN. - params.set_bssid(optional{}); - params.set_channel(optional{}); - } else { - params.set_bssid(config->get_bssid()); - params.set_channel(config->get_channel()); - } + const auto hidden = config->get_hidden(); + params.set_hidden(hidden); + // For hidden networks, clear BSSID and channel even if set in config + // There might be multiple hidden networks with same SSID but we can't know which is correct + // Rely on probe-req with just SSID. Empty channel triggers ALL_CHANNEL_SCAN. + params.set_bssid(hidden ? optional{} : config->get_bssid()); + params.set_channel(hidden ? optional{} : config->get_channel()); } - return params; } From 90feecb7bf383e55527f7e8195e78a0ae386ca87 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 17:06:04 -0600 Subject: [PATCH 51/65] dry --- esphome/components/wifi/wifi_component.cpp | 40 +++++++--------------- 1 file changed, 12 insertions(+), 28 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index c42a69f5e8..00e47b6559 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -347,16 +347,6 @@ void WiFiComponent::clear_sta() { this->selected_sta_index_ = -1; } -// Helper to copy base config settings (password, manual_ip, priority, EAP) from source to dest -static void copy_wifi_ap_base_config(WiFiAP &dest, const WiFiAP &source) { - dest.set_password(source.get_password()); - dest.set_manual_ip(source.get_manual_ip()); - dest.set_priority(source.get_priority()); -#ifdef USE_WIFI_WPA2_EAP - dest.set_eap(source.get_eap()); -#endif -} - WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { const WiFiAP *config = this->get_selected_sta_(); if (!config) { @@ -364,9 +354,8 @@ WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { return {}; } - WiFiAP params; - // Copy config data that's never overridden (password, manual IP, priority, EAP) - copy_wifi_ap_base_config(params, *config); + // Start with a copy of the entire config + WiFiAP params = *config; // SYNCHRONIZATION: selected_sta_index_ and scan_result_[0] are kept in sync: // - wifi_scan_done() sorts all scan results by priority/RSSI (best first) @@ -374,22 +363,20 @@ WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { // - Sets selected_sta_index_ = i to record that matching config // Therefore scan_result_[0] is guaranteed to match sta_[selected_sta_index_] if (!this->scan_result_.empty()) { - // Use scan data - network is visible (hidden defaults to false) + // Override with scan data - network is visible (hidden defaults to false) const WiFiScanResult &scan = this->scan_result_[0]; + params.set_hidden(false); params.set_ssid(scan.get_ssid()); params.set_bssid(scan.get_bssid()); params.set_channel(scan.get_channel()); - } else { - // Use config settings - params.set_ssid(config->get_ssid()); - const auto hidden = config->get_hidden(); - params.set_hidden(hidden); + } else if (config->get_hidden()) { // For hidden networks, clear BSSID and channel even if set in config // There might be multiple hidden networks with same SSID but we can't know which is correct // Rely on probe-req with just SSID. Empty channel triggers ALL_CHANNEL_SCAN. - params.set_bssid(hidden ? optional{} : config->get_bssid()); - params.set_channel(hidden ? optional{} : config->get_channel()); + params.set_bssid(optional{}); + params.set_channel(optional{}); } + return params; } @@ -919,18 +906,15 @@ bool WiFiComponent::load_fast_connect_settings_(WiFiAP ¶ms) { // Set selected index for future operations (save, retry, etc) this->selected_sta_index_ = fast_connect_save.ap_index; - // Build WiFiAP directly from saved settings + config - const WiFiAP &config = this->sta_[fast_connect_save.ap_index]; - // Copy config data that's never overridden (password, manual IP, priority, EAP) - copy_wifi_ap_base_config(params, config); + // Copy entire config, then override with fast connect data + params = this->sta_[fast_connect_save.ap_index]; - // Use saved BSSID/channel from fast connect, SSID from config - params.set_ssid(config.get_ssid()); + // Override with saved BSSID/channel from fast connect (SSID/password/hidden/etc already copied) bssid_t bssid{}; std::copy(fast_connect_save.bssid, fast_connect_save.bssid + 6, bssid.begin()); params.set_bssid(bssid); params.set_channel(fast_connect_save.channel); - // hidden defaults to false (network was found before, so not hidden) + // Network was found before, so not hidden (already false in default-constructed WiFiAP) ESP_LOGD(TAG, "Loaded fast_connect settings"); return true; From 400a18fddc5c1b04c0a0747d5e3933f861ac8013 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 17:12:05 -0600 Subject: [PATCH 52/65] dry --- esphome/components/wifi/wifi_component.cpp | 38 +++++++++++++++++----- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 00e47b6559..5c9b23d025 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -347,6 +347,17 @@ void WiFiComponent::clear_sta() { this->selected_sta_index_ = -1; } +// Helper to copy base config settings (password, manual_ip, priority, EAP) from source to dest +// These fields are never overridden by scan data, so we copy them once to avoid full WiFiAP copy +static void copy_wifi_ap_base_config(WiFiAP &dest, const WiFiAP &source) { + dest.set_password(source.get_password()); + dest.set_manual_ip(source.get_manual_ip()); + dest.set_priority(source.get_priority()); +#ifdef USE_WIFI_WPA2_EAP + dest.set_eap(source.get_eap()); +#endif +} + WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { const WiFiAP *config = this->get_selected_sta_(); if (!config) { @@ -354,8 +365,9 @@ WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { return {}; } - // Start with a copy of the entire config - WiFiAP params = *config; + WiFiAP params; + // Copy only base fields (password, manual_ip, priority, EAP) to avoid copying strings we'll override + copy_wifi_ap_base_config(params, *config); // SYNCHRONIZATION: selected_sta_index_ and scan_result_[0] are kept in sync: // - wifi_scan_done() sorts all scan results by priority/RSSI (best first) @@ -365,16 +377,23 @@ WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { if (!this->scan_result_.empty()) { // Override with scan data - network is visible (hidden defaults to false) const WiFiScanResult &scan = this->scan_result_[0]; - params.set_hidden(false); params.set_ssid(scan.get_ssid()); params.set_bssid(scan.get_bssid()); params.set_channel(scan.get_channel()); + // hidden defaults to false, no need to set explicitly } else if (config->get_hidden()) { - // For hidden networks, clear BSSID and channel even if set in config + // For hidden networks, use config SSID but clear BSSID and channel // There might be multiple hidden networks with same SSID but we can't know which is correct // Rely on probe-req with just SSID. Empty channel triggers ALL_CHANNEL_SCAN. + params.set_ssid(config->get_ssid()); + params.set_hidden(true); params.set_bssid(optional{}); params.set_channel(optional{}); + } else { + // No scan data, visible network - use all config values + params.set_ssid(config->get_ssid()); + params.set_bssid(config->get_bssid()); + params.set_channel(config->get_channel()); } return params; @@ -906,15 +925,18 @@ bool WiFiComponent::load_fast_connect_settings_(WiFiAP ¶ms) { // Set selected index for future operations (save, retry, etc) this->selected_sta_index_ = fast_connect_save.ap_index; - // Copy entire config, then override with fast connect data - params = this->sta_[fast_connect_save.ap_index]; + // Build WiFiAP from config + saved fast connect data + const WiFiAP &config = this->sta_[fast_connect_save.ap_index]; + // Copy only base fields (password, manual_ip, priority, EAP) to avoid unnecessary string copies + copy_wifi_ap_base_config(params, config); - // Override with saved BSSID/channel from fast connect (SSID/password/hidden/etc already copied) + // Use SSID from config, BSSID/channel from saved fast connect data + params.set_ssid(config.get_ssid()); bssid_t bssid{}; std::copy(fast_connect_save.bssid, fast_connect_save.bssid + 6, bssid.begin()); params.set_bssid(bssid); params.set_channel(fast_connect_save.channel); - // Network was found before, so not hidden (already false in default-constructed WiFiAP) + // Network was found before, so not hidden (hidden defaults to false) ESP_LOGD(TAG, "Loaded fast_connect settings"); return true; From 6e685f1b2d84d787edac3fefcde0a9aa1a77abf5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 17:15:57 -0600 Subject: [PATCH 53/65] dry --- esphome/components/wifi/wifi_component.cpp | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 5c9b23d025..bd18cc6f1b 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -381,19 +381,16 @@ WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { params.set_bssid(scan.get_bssid()); params.set_channel(scan.get_channel()); // hidden defaults to false, no need to set explicitly - } else if (config->get_hidden()) { - // For hidden networks, use config SSID but clear BSSID and channel + } else { + // No scan data - use config SSID + params.set_ssid(config->get_ssid()); + const bool hidden = config->get_hidden(); + params.set_hidden(hidden); + // Hidden network - clear BSSID and channel even if set in config // There might be multiple hidden networks with same SSID but we can't know which is correct // Rely on probe-req with just SSID. Empty channel triggers ALL_CHANNEL_SCAN. - params.set_ssid(config->get_ssid()); - params.set_hidden(true); - params.set_bssid(optional{}); - params.set_channel(optional{}); - } else { - // No scan data, visible network - use all config values - params.set_ssid(config->get_ssid()); - params.set_bssid(config->get_bssid()); - params.set_channel(config->get_channel()); + params.set_bssid(hidden ? optional{} : config->get_bssid()); + params.set_channel(hidden ? optional{} : config->get_channel()); } return params; From bf52b9fe06cceb5ecf8249cf112725c122216ad2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 17:20:01 -0600 Subject: [PATCH 54/65] dry --- esphome/components/wifi/wifi_component.cpp | 34 ++++++---------------- 1 file changed, 9 insertions(+), 25 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index bd18cc6f1b..6c92d1f955 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -347,17 +347,6 @@ void WiFiComponent::clear_sta() { this->selected_sta_index_ = -1; } -// Helper to copy base config settings (password, manual_ip, priority, EAP) from source to dest -// These fields are never overridden by scan data, so we copy them once to avoid full WiFiAP copy -static void copy_wifi_ap_base_config(WiFiAP &dest, const WiFiAP &source) { - dest.set_password(source.get_password()); - dest.set_manual_ip(source.get_manual_ip()); - dest.set_priority(source.get_priority()); -#ifdef USE_WIFI_WPA2_EAP - dest.set_eap(source.get_eap()); -#endif -} - WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { const WiFiAP *config = this->get_selected_sta_(); if (!config) { @@ -365,9 +354,8 @@ WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { return {}; } - WiFiAP params; - // Copy only base fields (password, manual_ip, priority, EAP) to avoid copying strings we'll override - copy_wifi_ap_base_config(params, *config); + // Start with a copy of the entire config + WiFiAP params = *config; // SYNCHRONIZATION: selected_sta_index_ and scan_result_[0] are kept in sync: // - wifi_scan_done() sorts all scan results by priority/RSSI (best first) @@ -375,15 +363,14 @@ WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { // - Sets selected_sta_index_ = i to record that matching config // Therefore scan_result_[0] is guaranteed to match sta_[selected_sta_index_] if (!this->scan_result_.empty()) { - // Override with scan data - network is visible (hidden defaults to false) + // Override with scan data - network is visible const WiFiScanResult &scan = this->scan_result_[0]; + params.set_hidden(false); params.set_ssid(scan.get_ssid()); params.set_bssid(scan.get_bssid()); params.set_channel(scan.get_channel()); - // hidden defaults to false, no need to set explicitly } else { - // No scan data - use config SSID - params.set_ssid(config->get_ssid()); + // No scan data - use config values const bool hidden = config->get_hidden(); params.set_hidden(hidden); // Hidden network - clear BSSID and channel even if set in config @@ -922,18 +909,15 @@ bool WiFiComponent::load_fast_connect_settings_(WiFiAP ¶ms) { // Set selected index for future operations (save, retry, etc) this->selected_sta_index_ = fast_connect_save.ap_index; - // Build WiFiAP from config + saved fast connect data - const WiFiAP &config = this->sta_[fast_connect_save.ap_index]; - // Copy only base fields (password, manual_ip, priority, EAP) to avoid unnecessary string copies - copy_wifi_ap_base_config(params, config); + // Copy entire config, then override with fast connect data + params = this->sta_[fast_connect_save.ap_index]; - // Use SSID from config, BSSID/channel from saved fast connect data - params.set_ssid(config.get_ssid()); + // Override with saved BSSID/channel from fast connect (SSID/password/hidden/etc already copied) bssid_t bssid{}; std::copy(fast_connect_save.bssid, fast_connect_save.bssid + 6, bssid.begin()); params.set_bssid(bssid); params.set_channel(fast_connect_save.channel); - // Network was found before, so not hidden (hidden defaults to false) + // Network was found before, so not hidden (already false in default-constructed WiFiAP) ESP_LOGD(TAG, "Loaded fast_connect settings"); return true; From 0a741007bf5850112ee081acfffe2315f5a74e72 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 17:21:14 -0600 Subject: [PATCH 55/65] dry --- esphome/components/wifi/wifi_component.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 6c92d1f955..4dd344e4d8 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -369,15 +369,12 @@ WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { params.set_ssid(scan.get_ssid()); params.set_bssid(scan.get_bssid()); params.set_channel(scan.get_channel()); - } else { - // No scan data - use config values - const bool hidden = config->get_hidden(); - params.set_hidden(hidden); + } else if (config->get_hidden()) { // Hidden network - clear BSSID and channel even if set in config // There might be multiple hidden networks with same SSID but we can't know which is correct // Rely on probe-req with just SSID. Empty channel triggers ALL_CHANNEL_SCAN. - params.set_bssid(hidden ? optional{} : config->get_bssid()); - params.set_channel(hidden ? optional{} : config->get_channel()); + params.set_bssid(optional{}); + params.set_channel(optional{}); } return params; From b4b24c500cd5fa39f73fef51ae5daa221db7bd66 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 17:24:07 -0600 Subject: [PATCH 56/65] dry --- esphome/components/wifi/wifi_component.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 4dd344e4d8..8e3fae99cd 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -348,15 +348,12 @@ void WiFiComponent::clear_sta() { } WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { - const WiFiAP *config = this->get_selected_sta_(); - if (!config) { + WiFiAP params = this->get_sta(); + if (params.get_ssid().empty()) { ESP_LOGE(TAG, "No config selected"); return {}; } - // Start with a copy of the entire config - WiFiAP params = *config; - // SYNCHRONIZATION: selected_sta_index_ and scan_result_[0] are kept in sync: // - wifi_scan_done() sorts all scan results by priority/RSSI (best first) // - It then finds which sta_[i] config matches scan_result_[0] @@ -369,7 +366,7 @@ WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { params.set_ssid(scan.get_ssid()); params.set_bssid(scan.get_bssid()); params.set_channel(scan.get_channel()); - } else if (config->get_hidden()) { + } else if (params.get_hidden()) { // Hidden network - clear BSSID and channel even if set in config // There might be multiple hidden networks with same SSID but we can't know which is correct // Rely on probe-req with just SSID. Empty channel triggers ALL_CHANNEL_SCAN. From 3b9570d916bb4451f2ad7020b81bdb4ae2483255 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 17:25:52 -0600 Subject: [PATCH 57/65] dry --- esphome/components/wifi/wifi_component.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 48ecb3fe8d..040dad3253 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -220,7 +220,7 @@ class WiFiComponent : public Component { void set_sta(const WiFiAP &ap); // Returns a copy of the currently selected AP configuration // Note: This copies the 88-byte WiFiAP. Only used by WiFiConfigureAction for state save/restore. - WiFiAP get_sta(); + WiFiAP get_sta() const; void init_sta(size_t count); void add_sta(const WiFiAP &ap); void clear_sta(); From 0893de4f297887a7e4196aac364dc5937ca5ed9e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 17:26:51 -0600 Subject: [PATCH 58/65] dry --- esphome/components/wifi/wifi_component.cpp | 2 +- esphome/components/wifi/wifi_component.h | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 8e3fae99cd..9531c6dfc8 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -396,7 +396,7 @@ bool WiFiComponent::sync_selected_sta_to_best_scan_result_() { return false; } -WiFiAP WiFiComponent::get_sta() { +WiFiAP WiFiComponent::get_sta() const { const WiFiAP *config = this->get_selected_sta_(); return config ? *config : WiFiAP{}; } diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 040dad3253..f5b50f29e9 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -218,8 +218,7 @@ class WiFiComponent : public Component { WiFiComponent(); void set_sta(const WiFiAP &ap); - // Returns a copy of the currently selected AP configuration - // Note: This copies the 88-byte WiFiAP. Only used by WiFiConfigureAction for state save/restore. + // Returns a copy of the currently selected AP configuration (88 bytes) WiFiAP get_sta() const; void init_sta(size_t count); void add_sta(const WiFiAP &ap); From 81bc2d82d691bf64eb53eee0aa6e125531d9d5f3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 17:30:16 -0600 Subject: [PATCH 59/65] dry --- esphome/components/wifi/wifi_component.cpp | 39 +++++++++++----------- esphome/components/wifi/wifi_component.h | 4 --- 2 files changed, 19 insertions(+), 24 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 9531c6dfc8..910edb01ac 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -377,25 +377,6 @@ WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { return params; } -bool WiFiComponent::sync_selected_sta_to_best_scan_result_() { - if (this->scan_result_.empty()) - return false; - - const WiFiScanResult &scan_res = this->scan_result_[0]; - if (!scan_res.get_matches()) - return false; - - for (size_t i = 0; i < this->sta_.size(); i++) { - if (scan_res.matches(this->sta_[i])) { - // Safe cast: sta_.size() limited to MAX_WIFI_NETWORKS (127) in __init__.py validation - // No overflow check needed - YAML validation prevents >127 networks - this->selected_sta_index_ = static_cast(i); // Links scan_result_[0] with sta_[i] - return true; - } - } - return false; -} - WiFiAP WiFiComponent::get_sta() const { const WiFiAP *config = this->get_selected_sta_(); return config ? *config : WiFiAP{}; @@ -699,7 +680,25 @@ void WiFiComponent::check_scanning_finished() { // After sorting, scan_result_[0] contains the best network. Now find which sta_[i] config // matches that network and record it in selected_sta_index_. This keeps the two indices // synchronized so build_wifi_ap_from_selected_() can safely use both to build connection parameters. - if (!this->sync_selected_sta_to_best_scan_result_()) { + const WiFiScanResult &scan_res = this->scan_result_[0]; + if (!scan_res.get_matches()) { + ESP_LOGW(TAG, "No matching network found"); + this->retry_connect(); + return; + } + + bool found_match = false; + for (size_t i = 0; i < this->sta_.size(); i++) { + if (scan_res.matches(this->sta_[i])) { + // Safe cast: sta_.size() limited to MAX_WIFI_NETWORKS (127) in __init__.py validation + // No overflow check needed - YAML validation prevents >127 networks + this->selected_sta_index_ = static_cast(i); // Links scan_result_[0] with sta_[i] + found_match = true; + break; + } + } + + if (!found_match) { ESP_LOGW(TAG, "No matching network found"); this->retry_connect(); return; diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index f5b50f29e9..a457ae6bd9 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -353,10 +353,6 @@ class WiFiComponent : public Component { } } - // SYNCHRONIZATION HELPER: Find which sta_[i] matches scan_result_[0] and set selected_sta_index_ - // Returns true if match found, false otherwise (scan done path) - bool sync_selected_sta_to_best_scan_result_(); - #ifdef USE_WIFI_FAST_CONNECT // Reset state for next fast connect AP attempt // Clears old scan data so the new AP is tried with config only (SSID without specific BSSID/channel) From c6da4e4777625174f2bf16c4ebaeb03d88c3782f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 17:38:26 -0600 Subject: [PATCH 60/65] dry --- esphome/components/wifi/wifi_component.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 910edb01ac..1bf07bbde6 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -349,10 +349,9 @@ void WiFiComponent::clear_sta() { WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { WiFiAP params = this->get_sta(); - if (params.get_ssid().empty()) { - ESP_LOGE(TAG, "No config selected"); - return {}; - } + // PRECONDITION: selected_sta_index_ must be valid (ensured by all callers) + // If SSID is empty, it means selected_sta_index_ was invalid - this is a bug + assert(!params.get_ssid().empty() && "build_wifi_ap_from_selected_() called with invalid selected_sta_index_"); // SYNCHRONIZATION: selected_sta_index_ and scan_result_[0] are kept in sync: // - wifi_scan_done() sorts all scan results by priority/RSSI (best first) From 148cbc03db69fbf60b67058a852d6aa5bb9b3bae Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 17:39:23 -0600 Subject: [PATCH 61/65] dry --- esphome/components/wifi/wifi_component.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 1bf07bbde6..2b085f66f7 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1,5 +1,6 @@ #include "wifi_component.h" #ifdef USE_WIFI +#include #include #ifdef USE_ESP32 From 282f6e04b37a7159bd1d8a7cf4e43d6b9020eb4a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 17:40:53 -0600 Subject: [PATCH 62/65] dry --- esphome/components/wifi/wifi_component.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 2b085f66f7..0ac675c0a3 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -352,7 +352,7 @@ WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { WiFiAP params = this->get_sta(); // PRECONDITION: selected_sta_index_ must be valid (ensured by all callers) // If SSID is empty, it means selected_sta_index_ was invalid - this is a bug - assert(!params.get_ssid().empty() && "build_wifi_ap_from_selected_() called with invalid selected_sta_index_"); + assert(!params.get_ssid().empty()); // SYNCHRONIZATION: selected_sta_index_ and scan_result_[0] are kept in sync: // - wifi_scan_done() sorts all scan results by priority/RSSI (best first) From d96e8a9c4b47469688085016c99fc24b3dfc4fd8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 17:43:52 -0600 Subject: [PATCH 63/65] dry --- esphome/components/wifi/wifi_component.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 0ac675c0a3..4a76603691 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -349,10 +349,10 @@ void WiFiComponent::clear_sta() { } WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { - WiFiAP params = this->get_sta(); // PRECONDITION: selected_sta_index_ must be valid (ensured by all callers) - // If SSID is empty, it means selected_sta_index_ was invalid - this is a bug - assert(!params.get_ssid().empty()); + const WiFiAP *config = this->get_selected_sta_(); + assert(config != nullptr); + WiFiAP params = *config; // SYNCHRONIZATION: selected_sta_index_ and scan_result_[0] are kept in sync: // - wifi_scan_done() sorts all scan results by priority/RSSI (best first) From d6528f906ebd73db248845a86e3cad2d1a1340f9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 17:49:45 -0600 Subject: [PATCH 64/65] dry --- esphome/components/wifi/wifi_component.cpp | 4 ---- esphome/components/wifi/wifi_component.h | 5 ++++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 4a76603691..a47cba169a 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -343,10 +343,6 @@ void WiFiComponent::set_sta(const WiFiAP &ap) { this->add_sta(ap); this->selected_sta_index_ = 0; } -void WiFiComponent::clear_sta() { - this->sta_.clear(); - this->selected_sta_index_ = -1; -} WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { // PRECONDITION: selected_sta_index_ must be valid (ensured by all callers) diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index a457ae6bd9..394f00fd57 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -222,7 +222,10 @@ class WiFiComponent : public Component { WiFiAP get_sta() const; void init_sta(size_t count); void add_sta(const WiFiAP &ap); - void clear_sta(); + void clear_sta() { + this->sta_.clear(); + this->selected_sta_index_ = -1; + } #ifdef USE_WIFI_AP /** Setup an Access Point that should be created if no connection to a station can be made. From f3c9ab7cb463b66c4b7fd567f224b3f39315f8e1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 6 Nov 2025 18:06:10 -0600 Subject: [PATCH 65/65] address final bot comments --- esphome/components/wifi/wifi_component.cpp | 9 +++++---- esphome/components/wifi/wifi_component.h | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index a47cba169a..789c22bae1 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -350,11 +350,11 @@ WiFiAP WiFiComponent::build_wifi_ap_from_selected_() const { assert(config != nullptr); WiFiAP params = *config; - // SYNCHRONIZATION: selected_sta_index_ and scan_result_[0] are kept in sync: + // SYNCHRONIZATION: selected_sta_index_ and scan_result_[0] are kept in sync after wifi_scan_done(): // - wifi_scan_done() sorts all scan results by priority/RSSI (best first) // - It then finds which sta_[i] config matches scan_result_[0] // - Sets selected_sta_index_ = i to record that matching config - // Therefore scan_result_[0] is guaranteed to match sta_[selected_sta_index_] + // This sync holds until scan_result_ is cleared (e.g., after connection or in reset_for_next_ap_attempt_()) if (!this->scan_result_.empty()) { // Override with scan data - network is visible const WiFiScanResult &scan = this->scan_result_[0]; @@ -901,12 +901,13 @@ bool WiFiComponent::load_fast_connect_settings_(WiFiAP ¶ms) { // Copy entire config, then override with fast connect data params = this->sta_[fast_connect_save.ap_index]; - // Override with saved BSSID/channel from fast connect (SSID/password/hidden/etc already copied) + // Override with saved BSSID/channel from fast connect (SSID/password/etc already copied from config) bssid_t bssid{}; std::copy(fast_connect_save.bssid, fast_connect_save.bssid + 6, bssid.begin()); params.set_bssid(bssid); params.set_channel(fast_connect_save.channel); - // Network was found before, so not hidden (already false in default-constructed WiFiAP) + // Fast connect uses specific BSSID+channel, not hidden network probe (even if config has hidden: true) + params.set_hidden(false); ESP_LOGD(TAG, "Loaded fast_connect settings"); return true; diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 394f00fd57..228894076e 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -218,7 +218,7 @@ class WiFiComponent : public Component { WiFiComponent(); void set_sta(const WiFiAP &ap); - // Returns a copy of the currently selected AP configuration (88 bytes) + // Returns a copy of the currently selected AP configuration WiFiAP get_sta() const; void init_sta(size_t count); void add_sta(const WiFiAP &ap);