From 7aef173e650da3bce3975be0f88dcfefb427f8ff Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 20 Jan 2026 20:19:35 -1000 Subject: [PATCH] [wifi] Filter scan results to only store matching networks --- esphome/components/wifi/wifi_component.cpp | 49 ++++++++++++++++++- esphome/components/wifi/wifi_component.h | 10 ++-- .../wifi/wifi_component_esp8266.cpp | 23 ++++++--- .../wifi/wifi_component_esp_idf.cpp | 26 +++++++--- .../wifi/wifi_component_libretiny.cpp | 35 +++++++++---- .../components/wifi/wifi_component_pico_w.cpp | 12 ++++- 6 files changed, 126 insertions(+), 29 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index ff6284c073..64abf03c51 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -39,6 +39,10 @@ #include "esphome/components/esp32_improv/esp32_improv_component.h" #endif +#ifdef USE_IMPROV_SERIAL +#include "esphome/components/improv_serial/improv_serial_component.h" +#endif + namespace esphome::wifi { static const char *const TAG = "wifi"; @@ -365,6 +369,49 @@ bool WiFiComponent::ssid_was_seen_in_scan_(const std::string &ssid) const { return false; } +bool WiFiComponent::needs_full_scan_results_() const { + // Listeners always need full results + if (this->keep_scan_results_) { + return true; + } + +#ifdef USE_CAPTIVE_PORTAL + // Captive portal needs full results when active (showing network list to user) + if (captive_portal::global_captive_portal != nullptr && captive_portal::global_captive_portal->is_active()) { + return true; + } +#endif + +#ifdef USE_IMPROV_SERIAL + // Improv serial needs results during provisioning (before connected) + if (improv_serial::global_improv_serial_component != nullptr && !this->is_connected()) { + return true; + } +#endif + +#ifdef USE_IMPROV + // BLE improv also needs results during provisioning + if (esp32_improv::global_improv_component != nullptr && esp32_improv::global_improv_component->is_active()) { + return true; + } +#endif + + return false; +} + +bool WiFiComponent::matches_configured_ssid_(const char *ssid) const { + // Hidden networks in scan results have empty SSIDs - skip them + if (ssid[0] == '\0') { + return false; + } + for (const auto &sta : this->sta_) { + if (!sta.get_hidden() && (sta.get_ssid().empty() || sta.get_ssid() == ssid)) { + return true; + } + } + return false; +} + int8_t WiFiComponent::find_next_hidden_sta_(int8_t start_index) { // Find next SSID to try in RETRY_HIDDEN phase. // @@ -2079,7 +2126,7 @@ void WiFiComponent::clear_roaming_state_() { void WiFiComponent::release_scan_results_() { if (!this->keep_scan_results_) { -#ifdef USE_RP2040 +#if defined(USE_RP2040) || defined(USE_ESP32) // std::vector - use swap trick since shrink_to_fit is non-binding decltype(this->scan_result_)().swap(this->scan_result_); #else diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index dfc91fb5da..3a6bf2994d 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -161,9 +161,9 @@ struct EAPAuth { using bssid_t = std::array; -// Use std::vector for RP2040 since scan count is unknown (callback-based) -// Use FixedVector for other platforms where count is queried first -#ifdef USE_RP2040 +// Use std::vector for RP2040 (callback-based) and ESP32 (destructive scan API) +// Use FixedVector for ESP8266 and LibreTiny where two-pass exact allocation is possible +#if defined(USE_RP2040) || defined(USE_ESP32) template using wifi_scan_vector_t = std::vector; #else template using wifi_scan_vector_t = FixedVector; @@ -539,6 +539,10 @@ class WiFiComponent : public Component { /// Check if an SSID was seen in the most recent scan results /// Used to skip hidden mode for SSIDs we know are visible bool ssid_was_seen_in_scan_(const std::string &ssid) const; + /// Check if full scan results are needed (captive portal active, improv, listeners) + bool needs_full_scan_results_() const; + /// Check if SSID matches any configured network (for scan result filtering) + bool matches_configured_ssid_(const char *ssid) const; /// Find next SSID that wasn't in scan results (might be hidden) /// Returns index of next potentially hidden SSID, or -1 if none found /// @param start_index Start searching from index after this (-1 to start from beginning) diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index de0600cf5b..dad899ff0b 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -756,19 +756,28 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) { return; } - // Count the number of results first auto *head = reinterpret_cast(arg); + bool needs_full = this->needs_full_scan_results_(); + + // First pass: count matching networks (linked list is non-destructive) size_t count = 0; for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) { - count++; + const char *ssid_cstr = reinterpret_cast(it->ssid); + if (needs_full || this->matches_configured_ssid_(ssid_cstr)) { + count++; + } } - this->scan_result_.init(count); + this->scan_result_.init(count); // Exact allocation + + // Second pass: store matching networks for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) { - this->scan_result_.emplace_back( - bssid_t{it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]}, - std::string(reinterpret_cast(it->ssid), it->ssid_len), it->channel, it->rssi, it->authmode != AUTH_OPEN, - it->is_hidden != 0); + const char *ssid_cstr = reinterpret_cast(it->ssid); + if (needs_full || this->matches_configured_ssid_(ssid_cstr)) { + this->scan_result_.emplace_back( + bssid_t{it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]}, + std::string(ssid_cstr, it->ssid_len), it->channel, it->rssi, it->authmode != AUTH_OPEN, it->is_hidden != 0); + } } this->scan_done_ = true; #ifdef USE_WIFI_SCAN_RESULTS_LISTENERS diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 99474ac2f8..a8eef686d0 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -827,7 +827,14 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { } uint16_t number = it.number; - scan_result_.init(number); + bool needs_full = this->needs_full_scan_results_(); + + // Smart reserve: full capacity if needed, small reserve otherwise + if (needs_full) { + this->scan_result_.reserve(number); + } else { + this->scan_result_.reserve(8); // Typical: 1-3 matching networks + } // Process one record at a time to avoid large buffer allocation wifi_ap_record_t record; @@ -838,11 +845,18 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { esp_wifi_clear_ap_list(); // Free remaining records not yet retrieved break; } - bssid_t bssid; - std::copy(record.bssid, record.bssid + 6, bssid.begin()); - std::string ssid(reinterpret_cast(record.ssid)); - scan_result_.emplace_back(bssid, ssid, record.primary, record.rssi, record.authmode != WIFI_AUTH_OPEN, - ssid.empty()); + + // Check C string first - avoid std::string construction for non-matching networks + const char *ssid_cstr = reinterpret_cast(record.ssid); + + // Only construct std::string and store if needed + if (needs_full || this->matches_configured_ssid_(ssid_cstr)) { + bssid_t bssid; + std::copy(record.bssid, record.bssid + 6, bssid.begin()); + std::string ssid(ssid_cstr); + this->scan_result_.emplace_back(bssid, std::move(ssid), record.primary, record.rssi, + record.authmode != WIFI_AUTH_OPEN, ssid.empty()); + } } #ifdef USE_WIFI_SCAN_RESULTS_LISTENERS for (auto *listener : this->scan_results_listeners_) { diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index 162ed4e835..f0928603e1 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -664,17 +664,32 @@ void WiFiComponent::wifi_scan_done_callback_() { if (num < 0) return; - this->scan_result_.init(static_cast(num)); - for (int i = 0; i < num; i++) { - String ssid = WiFi.SSID(i); - wifi_auth_mode_t authmode = WiFi.encryptionType(i); - int32_t rssi = WiFi.RSSI(i); - uint8_t *bssid = WiFi.BSSID(i); - int32_t channel = WiFi.channel(i); + bool needs_full = this->needs_full_scan_results_(); - this->scan_result_.emplace_back(bssid_t{bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]}, - std::string(ssid.c_str()), channel, rssi, authmode != WIFI_AUTH_OPEN, - ssid.length() == 0); + // Access scan results directly via WiFi.scan struct to avoid Arduino String allocations + // WiFi.scan is public in LibreTiny for WiFiEvents & WiFiScan static handlers + auto *scan = WiFi.scan; + + // First pass: count matching networks + size_t count = 0; + for (int i = 0; i < num; i++) { + const char *ssid_cstr = scan->ap[i].ssid; + if (needs_full || this->matches_configured_ssid_(ssid_cstr)) { + count++; + } + } + + this->scan_result_.init(count); // Exact allocation + + // Second pass: store matching networks + for (int i = 0; i < num; i++) { + const char *ssid_cstr = scan->ap[i].ssid; + if (needs_full || this->matches_configured_ssid_(ssid_cstr)) { + auto &ap = scan->ap[i]; + this->scan_result_.emplace_back( + bssid_t{ap.bssid[0], ap.bssid[1], ap.bssid[2], ap.bssid[3], ap.bssid[4], ap.bssid[5]}, std::string(ssid_cstr), + ap.channel, ap.rssi, ap.auth != WIFI_AUTH_OPEN, ssid_cstr[0] == '\0'); + } } WiFi.scanDelete(); #ifdef USE_WIFI_SCAN_RESULTS_LISTENERS diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 29ac096d94..0db2300b8f 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -137,10 +137,18 @@ int WiFiComponent::s_wifi_scan_result(void *env, const cyw43_ev_scan_result_t *r } void WiFiComponent::wifi_scan_result(void *env, const cyw43_ev_scan_result_t *result) { + const char *ssid_cstr = reinterpret_cast(result->ssid); + + // Skip networks that don't match any configured SSID (unless full results needed) + if (!this->needs_full_scan_results_() && !this->matches_configured_ssid_(ssid_cstr)) { + return; + } + bssid_t bssid; std::copy(result->bssid, result->bssid + 6, bssid.begin()); - std::string ssid(reinterpret_cast(result->ssid)); - WiFiScanResult res(bssid, ssid, result->channel, result->rssi, result->auth_mode != CYW43_AUTH_OPEN, ssid.empty()); + std::string ssid(ssid_cstr); + WiFiScanResult res(bssid, std::move(ssid), result->channel, result->rssi, result->auth_mode != CYW43_AUTH_OPEN, + ssid.empty()); if (std::find(this->scan_result_.begin(), this->scan_result_.end(), res) == this->scan_result_.end()) { this->scan_result_.push_back(res); }