ESPHome 2026.1.0-dev
Loading...
Searching...
No Matches
wifi_component.cpp
Go to the documentation of this file.
1#include "wifi_component.h"
2#ifdef USE_WIFI
3#include <cassert>
4#include <cinttypes>
5#include <cmath>
6
7#ifdef USE_ESP32
8#if (ESP_IDF_VERSION_MAJOR >= 5 && ESP_IDF_VERSION_MINOR >= 1)
9#include <esp_eap_client.h>
10#else
11#include <esp_wpa2.h>
12#endif
13#endif
14
15#if defined(USE_ESP32)
16#include <esp_wifi.h>
17#endif
18#ifdef USE_ESP8266
19#include <user_interface.h>
20#endif
21
22#include <algorithm>
23#include <utility>
24#include "lwip/dns.h"
25#include "lwip/err.h"
26
28#include "esphome/core/hal.h"
30#include "esphome/core/log.h"
31#include "esphome/core/util.h"
32
33#ifdef USE_CAPTIVE_PORTAL
35#endif
36
37#ifdef USE_IMPROV
39#endif
40
41namespace esphome::wifi {
42
43static const char *const TAG = "wifi";
44
146
147static const LogString *retry_phase_to_log_string(WiFiRetryPhase phase) {
148 switch (phase) {
150 return LOG_STR("INITIAL_CONNECT");
151#ifdef USE_WIFI_FAST_CONNECT
153 return LOG_STR("FAST_CONNECT_CYCLING");
154#endif
156 return LOG_STR("EXPLICIT_HIDDEN");
158 return LOG_STR("SCAN_CONNECTING");
160 return LOG_STR("RETRY_HIDDEN");
162 return LOG_STR("RESTARTING");
163 default:
164 return LOG_STR("UNKNOWN");
165 }
166}
167
169 // If first configured network is marked hidden, we went through EXPLICIT_HIDDEN phase
170 // This means those networks were already tried and should be skipped in RETRY_HIDDEN
171 return !this->sta_.empty() && this->sta_[0].get_hidden();
172}
173
175 // Find the first network that is NOT marked hidden:true
176 // This is where EXPLICIT_HIDDEN phase would have stopped
177 for (size_t i = 0; i < this->sta_.size(); i++) {
178 if (!this->sta_[i].get_hidden()) {
179 return static_cast<int8_t>(i);
180 }
181 }
182 return -1; // All networks are hidden
183}
184
185// 2 attempts per BSSID in SCAN_CONNECTING phase
186// Rationale: This is the ONLY phase where we decrease BSSID priority, so we must be very sure.
187// Auth failures are common immediately after scan due to WiFi stack state transitions.
188// Trying twice filters out false positives and prevents unnecessarily marking a good BSSID as bad.
189// After 2 genuine failures, priority degradation ensures we skip this BSSID on subsequent scans.
190static constexpr uint8_t WIFI_RETRY_COUNT_PER_BSSID = 2;
191
192// 1 attempt per SSID in RETRY_HIDDEN phase
193// Rationale: Try hidden mode once, then rescan to get next best BSSID via priority system
194static constexpr uint8_t WIFI_RETRY_COUNT_PER_SSID = 1;
195
196// 1 attempt per AP in fast_connect mode (INITIAL_CONNECT and FAST_CONNECT_CYCLING_APS)
197// Rationale: Fast connect prioritizes speed - try each AP once to find a working one quickly
198static constexpr uint8_t WIFI_RETRY_COUNT_PER_AP = 1;
199
202static constexpr uint32_t WIFI_COOLDOWN_DURATION_MS = 500;
203
207static constexpr uint32_t WIFI_COOLDOWN_WITH_AP_ACTIVE_MS = 30000;
208
212static constexpr uint32_t WIFI_SCAN_TIMEOUT_MS = 31000;
213
222static constexpr uint32_t WIFI_CONNECT_TIMEOUT_MS = 46000;
223
224static constexpr uint8_t get_max_retries_for_phase(WiFiRetryPhase phase) {
225 switch (phase) {
227#ifdef USE_WIFI_FAST_CONNECT
229#endif
230 // INITIAL_CONNECT and FAST_CONNECT_CYCLING_APS both use 1 attempt per AP (fast_connect mode)
231 return WIFI_RETRY_COUNT_PER_AP;
233 // Explicitly hidden network: 1 attempt (user marked as hidden, try once then scan)
234 return WIFI_RETRY_COUNT_PER_SSID;
236 // Scan-based phase: 2 attempts per BSSID (handles transient auth failures after scan)
237 return WIFI_RETRY_COUNT_PER_BSSID;
239 // Hidden network mode: 1 attempt per SSID
240 return WIFI_RETRY_COUNT_PER_SSID;
241 default:
242 return WIFI_RETRY_COUNT_PER_BSSID;
243 }
244}
245
246static void apply_scan_result_to_params(WiFiAP &params, const WiFiScanResult &scan) {
247 params.set_hidden(false);
248 params.set_ssid(scan.get_ssid());
249 params.set_bssid(scan.get_bssid());
250 params.set_channel(scan.get_channel());
251}
252
254 // Only SCAN_CONNECTING phase needs scan results
256 return false;
257 }
258 // Need scan if we have no results or no matching networks
259 return this->scan_result_.empty() || !this->scan_result_[0].get_matches();
260}
261
262bool WiFiComponent::ssid_was_seen_in_scan_(const std::string &ssid) const {
263 // Check if this SSID is configured as hidden
264 // If explicitly marked hidden, we should always try hidden mode regardless of scan results
265 for (const auto &conf : this->sta_) {
266 if (conf.get_ssid() == ssid && conf.get_hidden()) {
267 return false; // Treat as not seen - force hidden mode attempt
268 }
269 }
270
271 // Otherwise, check if we saw it in scan results
272 for (const auto &scan : this->scan_result_) {
273 if (scan.get_ssid() == ssid) {
274 return true;
275 }
276 }
277 return false;
278}
279
280int8_t WiFiComponent::find_next_hidden_sta_(int8_t start_index) {
281 // Find next SSID that wasn't in scan results (might be hidden)
282 bool include_explicit_hidden = !this->went_through_explicit_hidden_phase_();
283 // Start searching from start_index + 1
284 for (size_t i = start_index + 1; i < this->sta_.size(); i++) {
285 const auto &sta = this->sta_[i];
286
287 // Skip networks that were already tried in EXPLICIT_HIDDEN phase
288 // Those are: networks marked hidden:true that appear before the first non-hidden network
289 // If all networks are hidden (first_non_hidden_idx == -1), skip all of them
290 if (!include_explicit_hidden && sta.get_hidden()) {
291 int8_t first_non_hidden_idx = this->find_first_non_hidden_index_();
292 if (first_non_hidden_idx < 0 || static_cast<int8_t>(i) < first_non_hidden_idx) {
293 ESP_LOGD(TAG, "Skipping " LOG_SECRET("'%s'") " (explicit hidden, already tried)", sta.get_ssid().c_str());
294 continue;
295 }
296 }
297
298 // If we didn't scan this cycle, treat all networks as potentially hidden
299 // Otherwise, only retry networks that weren't seen in the scan
300 if (!this->did_scan_this_cycle_ || !this->ssid_was_seen_in_scan_(sta.get_ssid())) {
301 ESP_LOGD(TAG, "Hidden candidate " LOG_SECRET("'%s'") " at index %d", sta.get_ssid().c_str(), static_cast<int>(i));
302 return static_cast<int8_t>(i);
303 }
304 ESP_LOGD(TAG, "Skipping hidden retry for visible network " LOG_SECRET("'%s'"), sta.get_ssid().c_str());
305 }
306 // No hidden SSIDs found
307 return -1;
308}
309
311 // If first network (highest priority) is explicitly marked hidden, try it first before scanning
312 // This respects user's priority order when they explicitly configure hidden networks
313 if (!this->sta_.empty() && this->sta_[0].get_hidden()) {
314 ESP_LOGI(TAG, "Starting with explicit hidden network (highest priority)");
315 this->selected_sta_index_ = 0;
318 this->start_connecting(params);
319 } else {
320 ESP_LOGI(TAG, "Starting scan");
321 this->start_scanning();
322 }
323}
324
325#if defined(USE_ESP32) && defined(USE_WIFI_WPA2_EAP) && ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
326static const char *eap_phase2_to_str(esp_eap_ttls_phase2_types type) {
327 switch (type) {
328 case ESP_EAP_TTLS_PHASE2_PAP:
329 return "pap";
330 case ESP_EAP_TTLS_PHASE2_CHAP:
331 return "chap";
332 case ESP_EAP_TTLS_PHASE2_MSCHAP:
333 return "mschap";
334 case ESP_EAP_TTLS_PHASE2_MSCHAPV2:
335 return "mschapv2";
336 case ESP_EAP_TTLS_PHASE2_EAP:
337 return "eap";
338 default:
339 return "unknown";
340 }
341}
342#endif
343
345
347 this->wifi_pre_setup_();
348
349#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
350 // Create semaphore for high-performance mode requests
351 // Start at 0, increment on request, decrement on release
352 this->high_performance_semaphore_ = xSemaphoreCreateCounting(UINT32_MAX, 0);
353 if (this->high_performance_semaphore_ == nullptr) {
354 ESP_LOGE(TAG, "Failed semaphore");
355 }
356
357 // Store the configured power save mode as baseline
359#endif
360
361 if (this->enable_on_boot_) {
362 this->start();
363 } else {
364#ifdef USE_ESP32
365 esp_netif_init();
366#endif
368 }
369}
370
372 char mac_s[18];
373 ESP_LOGCONFIG(TAG,
374 "Starting\n"
375 " Local MAC: %s",
377 this->last_connected_ = millis();
378
379 uint32_t hash = this->has_sta() ? App.get_config_version_hash() : 88491487UL;
380
382#ifdef USE_WIFI_FAST_CONNECT
384#endif
385
386 SavedWifiSettings save{};
387 if (this->pref_.load(&save)) {
388 ESP_LOGD(TAG, "Loaded settings: %s", save.ssid);
389
390 WiFiAP sta{};
391 sta.set_ssid(save.ssid);
392 sta.set_password(save.password);
393 this->set_sta(sta);
394 }
395
396 if (this->has_sta()) {
397 this->wifi_sta_pre_setup_();
398 if (!std::isnan(this->output_power_) && !this->wifi_apply_output_power_(this->output_power_)) {
399 ESP_LOGV(TAG, "Setting Output Power Option failed");
400 }
401
402#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
403 // Synchronize power_save_ with semaphore state before applying
404 if (this->high_performance_semaphore_ != nullptr) {
405 UBaseType_t semaphore_count = uxSemaphoreGetCount(this->high_performance_semaphore_);
406 if (semaphore_count > 0) {
408 this->is_high_performance_mode_ = true;
409 } else {
411 this->is_high_performance_mode_ = false;
412 }
413 }
414#endif
415 if (!this->wifi_apply_power_save_()) {
416 ESP_LOGV(TAG, "Setting Power Save Option failed");
417 }
418
420#ifdef USE_WIFI_FAST_CONNECT
421 WiFiAP params;
422 bool loaded_fast_connect = this->load_fast_connect_settings_(params);
423 // Fast connect optimization: only use when we have saved BSSID+channel data
424 // Without saved data, try first configured network or use normal flow
425 if (loaded_fast_connect) {
426 ESP_LOGI(TAG, "Starting fast_connect (saved) " LOG_SECRET("'%s'"), params.get_ssid().c_str());
427 this->start_connecting(params);
428 } else if (!this->sta_.empty() && !this->sta_[0].get_hidden()) {
429 // No saved data, but have configured networks - try first non-hidden network
430 ESP_LOGI(TAG, "Starting fast_connect (config) " LOG_SECRET("'%s'"), this->sta_[0].get_ssid().c_str());
431 this->selected_sta_index_ = 0;
432 params = this->build_params_for_current_phase_();
433 this->start_connecting(params);
434 } else {
435 // No saved data and (no networks OR first is hidden) - use normal flow
437 }
438#else
439 // Without fast_connect: go straight to scanning (or hidden mode if all networks are hidden)
441#endif
442#ifdef USE_WIFI_AP
443 } else if (this->has_ap()) {
444 this->setup_ap_config_();
445 if (!std::isnan(this->output_power_) && !this->wifi_apply_output_power_(this->output_power_)) {
446 ESP_LOGV(TAG, "Setting Output Power Option failed");
447 }
448#ifdef USE_CAPTIVE_PORTAL
450 this->wifi_sta_pre_setup_();
451 this->start_scanning();
453 }
454#endif
455#endif // USE_WIFI_AP
456 }
457#ifdef USE_IMPROV
458 if (!this->has_sta() && esp32_improv::global_improv_component != nullptr) {
459 if (this->wifi_mode_(true, {}))
461 }
462#endif
463 this->wifi_apply_hostname_();
464}
465
467 ESP_LOGW(TAG, "Restarting adapter");
468 this->wifi_mode_(false, {});
469 this->error_from_callback_ = false;
470}
471
473 this->wifi_loop_();
474 const uint32_t now = App.get_loop_component_start_time();
475
476 if (this->has_sta()) {
477 if (this->is_connected() != this->handled_connected_state_) {
478 if (this->handled_connected_state_) {
480 } else {
481 this->connect_trigger_->trigger();
482 }
484 }
485
486 switch (this->state_) {
488 this->status_set_warning(LOG_STR("waiting to reconnect"));
489 // Skip cooldown if new credentials were provided while connecting
490 if (this->skip_cooldown_next_cycle_) {
491 this->skip_cooldown_next_cycle_ = false;
493 break;
494 }
495 // Use longer cooldown when captive portal/improv is active to avoid disrupting user config
496 bool portal_active = this->is_captive_portal_active_() || this->is_esp32_improv_active_();
497 uint32_t cooldown_duration = portal_active ? WIFI_COOLDOWN_WITH_AP_ACTIVE_MS : WIFI_COOLDOWN_DURATION_MS;
498 if (now - this->action_started_ > cooldown_duration) {
499 // After cooldown we either restarted the adapter because of
500 // a failure, or something tried to connect over and over
501 // so we entered cooldown. In both cases we call
502 // check_connecting_finished to continue the state machine.
504 }
505 break;
506 }
508 this->status_set_warning(LOG_STR("scanning for networks"));
510 break;
511 }
513 this->status_set_warning(LOG_STR("associating to network"));
515 break;
516 }
517
519 if (!this->is_connected()) {
520 ESP_LOGW(TAG, "Connection lost; reconnecting");
522 // Clear error flag before reconnecting so first attempt is not seen as immediate failure
523 this->error_from_callback_ = false;
524 this->retry_connect();
525 } else {
526 this->status_clear_warning();
527 this->last_connected_ = now;
528 }
529 break;
530 }
533 break;
535 return;
536 }
537
538#ifdef USE_WIFI_AP
539 if (this->has_ap() && !this->ap_setup_) {
540 if (this->ap_timeout_ != 0 && (now - this->last_connected_ > this->ap_timeout_)) {
541 ESP_LOGI(TAG, "Starting fallback AP");
542 this->setup_ap_config_();
543#ifdef USE_CAPTIVE_PORTAL
546#endif
547 }
548 }
549#endif // USE_WIFI_AP
550
551#ifdef USE_IMPROV
553 !esp32_improv::global_improv_component->should_start()) {
554 if (now - this->last_connected_ > esp32_improv::global_improv_component->get_wifi_timeout()) {
555 if (this->wifi_mode_(true, {}))
557 }
558 }
559
560#endif
561
562 if (!this->has_ap() && this->reboot_timeout_ != 0) {
563 if (now - this->last_connected_ > this->reboot_timeout_) {
564 ESP_LOGE(TAG, "Can't connect; rebooting");
565 App.reboot();
566 }
567 }
568 }
569
570#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
571 // Check if power save mode needs to be updated based on high-performance requests
572 if (this->high_performance_semaphore_ != nullptr) {
573 // Semaphore count directly represents active requests (starts at 0, increments on request)
574 UBaseType_t semaphore_count = uxSemaphoreGetCount(this->high_performance_semaphore_);
575
576 if (semaphore_count > 0 && !this->is_high_performance_mode_) {
577 // Transition to high-performance mode (no power save)
578 ESP_LOGV(TAG, "Switching to high-performance mode (%" PRIu32 " active %s)", (uint32_t) semaphore_count,
579 semaphore_count == 1 ? "request" : "requests");
581 if (this->wifi_apply_power_save_()) {
582 this->is_high_performance_mode_ = true;
583 }
584 } else if (semaphore_count == 0 && this->is_high_performance_mode_) {
585 // Restore to configured power save mode
586 ESP_LOGV(TAG, "Restoring power save mode to configured setting");
588 if (this->wifi_apply_power_save_()) {
589 this->is_high_performance_mode_ = false;
590 }
591 }
592 }
593#endif
594}
595
597
598bool WiFiComponent::has_ap() const { return this->has_ap_; }
599bool WiFiComponent::is_ap_active() const { return this->ap_started_; }
600bool WiFiComponent::has_sta() const { return !this->sta_.empty(); }
601#ifdef USE_WIFI_11KV_SUPPORT
602void WiFiComponent::set_btm(bool btm) { this->btm_ = btm; }
603void WiFiComponent::set_rrm(bool rrm) { this->rrm_ = rrm; }
604#endif
606 if (this->has_sta())
607 return this->wifi_sta_ip_addresses();
608
609#ifdef USE_WIFI_AP
610 if (this->has_ap())
611 return {this->wifi_soft_ap_ip()};
612#endif // USE_WIFI_AP
613
614 return {};
615}
617 if (this->has_sta())
618 return this->wifi_dns_ip_(num);
619 return {};
620}
621// set_use_address() is guaranteed to be called during component setup by Python code generation,
622// so use_address_ will always be valid when get_use_address() is called - no fallback needed.
623const char *WiFiComponent::get_use_address() const { return this->use_address_; }
624void WiFiComponent::set_use_address(const char *use_address) { this->use_address_ = use_address; }
625
626#ifdef USE_WIFI_AP
628 this->wifi_mode_({}, true);
629
630 if (this->ap_setup_)
631 return;
632
633 if (this->ap_.get_ssid().empty()) {
634 std::string name = App.get_name();
635 if (name.length() > 32) {
637 // Keep first 25 chars and last 7 chars (MAC suffix), remove middle
638 name.erase(25, name.length() - 32);
639 } else {
640 name.resize(32);
641 }
642 }
643 this->ap_.set_ssid(name);
644 }
645 this->ap_setup_ = this->wifi_start_ap_(this->ap_);
646
647 auto ip_address = this->wifi_soft_ap_ip().str();
648 ESP_LOGCONFIG(TAG,
649 "Setting up AP:\n"
650 " AP SSID: '%s'\n"
651 " AP Password: '%s'\n"
652 " IP Address: %s",
653 this->ap_.get_ssid().c_str(), this->ap_.get_password().c_str(), ip_address.c_str());
654
655#ifdef USE_WIFI_MANUAL_IP
656 auto manual_ip = this->ap_.get_manual_ip();
657 if (manual_ip.has_value()) {
658 ESP_LOGCONFIG(TAG,
659 " AP Static IP: '%s'\n"
660 " AP Gateway: '%s'\n"
661 " AP Subnet: '%s'",
662 manual_ip->static_ip.str().c_str(), manual_ip->gateway.str().c_str(),
663 manual_ip->subnet.str().c_str());
664 }
665#endif
666
667 if (!this->has_sta()) {
669 }
670}
671
673 this->ap_ = ap;
674 this->has_ap_ = true;
675}
676#endif // USE_WIFI_AP
677
679 return 10.0f; // before other loop components
680}
681
682void WiFiComponent::init_sta(size_t count) { this->sta_.init(count); }
683void WiFiComponent::add_sta(const WiFiAP &ap) { this->sta_.push_back(ap); }
685 this->clear_sta();
686 this->init_sta(1);
687 this->add_sta(ap);
688 this->selected_sta_index_ = 0;
689 // When new credentials are set (e.g., from improv), skip cooldown to retry immediately
690 this->skip_cooldown_next_cycle_ = true;
691}
692
694 const WiFiAP *config = this->get_selected_sta_();
695 if (config == nullptr) {
696 ESP_LOGE(TAG, "No valid network config (selected_sta_index_=%d, sta_.size()=%zu)",
697 static_cast<int>(this->selected_sta_index_), this->sta_.size());
698 // Return empty params - caller should handle this gracefully
699 return WiFiAP();
700 }
701
702 WiFiAP params = *config;
703
704 switch (this->retry_phase_) {
706#ifdef USE_WIFI_FAST_CONNECT
708#endif
709 // Fast connect phases: use config-only (no scan results)
710 // BSSID/channel from config if user specified them, otherwise empty
711 break;
712
715 // Hidden network mode: clear BSSID/channel to trigger probe request
716 // (both explicit hidden and retry hidden use same behavior)
717 params.clear_bssid();
718 params.clear_channel();
719 break;
720
722 // Scan-based phase: always use best scan result (index 0 - highest priority after sorting)
723 if (!this->scan_result_.empty()) {
724 apply_scan_result_to_params(params, this->scan_result_[0]);
725 }
726 break;
727
729 // Should not be building params during restart
730 break;
731 }
732
733 return params;
734}
735
737 const WiFiAP *config = this->get_selected_sta_();
738 return config ? *config : WiFiAP{};
739}
740void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &password) {
741 SavedWifiSettings save{}; // zero-initialized - all bytes set to \0, guaranteeing null termination
742 strncpy(save.ssid, ssid.c_str(), sizeof(save.ssid) - 1); // max 32 chars, byte 32 remains \0
743 strncpy(save.password, password.c_str(), sizeof(save.password) - 1); // max 64 chars, byte 64 remains \0
744 this->pref_.save(&save);
745 // ensure it's written immediately
747
748 WiFiAP sta{};
749 sta.set_ssid(ssid);
750 sta.set_password(password);
751 this->set_sta(sta);
752
753 // Trigger connection attempt (exits cooldown if needed, no-op if already connecting/connected)
754 this->connect_soon_();
755}
756
758 // Only trigger retry if we're in cooldown - if already connecting/connected, do nothing
760 ESP_LOGD(TAG, "Exiting cooldown early due to new WiFi credentials");
761 this->retry_connect();
762 }
763}
764
766 // Log connection attempt at INFO level with priority
767 char bssid_s[18];
768 int8_t priority = 0;
769
770 if (ap.has_bssid()) {
771 format_mac_addr_upper(ap.get_bssid().data(), bssid_s);
772 priority = this->get_sta_priority(ap.get_bssid());
773 }
774
775 ESP_LOGI(TAG,
776 "Connecting to " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") " (priority %d, attempt %u/%u in phase %s)...",
777 ap.get_ssid().c_str(), ap.has_bssid() ? bssid_s : LOG_STR_LITERAL("any"), priority, this->num_retried_ + 1,
778 get_max_retries_for_phase(this->retry_phase_), LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_)));
779
780#ifdef ESPHOME_LOG_HAS_VERBOSE
781 ESP_LOGV(TAG, "Connection Params:");
782 ESP_LOGV(TAG, " SSID: '%s'", ap.get_ssid().c_str());
783 if (ap.has_bssid()) {
784 ESP_LOGV(TAG, " BSSID: %s", bssid_s);
785 } else {
786 ESP_LOGV(TAG, " BSSID: Not Set");
787 }
788
789#ifdef USE_WIFI_WPA2_EAP
790 if (ap.get_eap().has_value()) {
791 ESP_LOGV(TAG, " WPA2 Enterprise authentication configured:");
792 EAPAuth eap_config = ap.get_eap().value();
793 ESP_LOGV(TAG, " Identity: " LOG_SECRET("'%s'"), eap_config.identity.c_str());
794 ESP_LOGV(TAG, " Username: " LOG_SECRET("'%s'"), eap_config.username.c_str());
795 ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), eap_config.password.c_str());
796#if defined(USE_ESP32) && defined(USE_WIFI_WPA2_EAP) && ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
797 ESP_LOGV(TAG, " TTLS Phase 2: " LOG_SECRET("'%s'"), eap_phase2_to_str(eap_config.ttls_phase_2));
798#endif
799 bool ca_cert_present = eap_config.ca_cert != nullptr && strlen(eap_config.ca_cert);
800 bool client_cert_present = eap_config.client_cert != nullptr && strlen(eap_config.client_cert);
801 bool client_key_present = eap_config.client_key != nullptr && strlen(eap_config.client_key);
802 ESP_LOGV(TAG, " CA Cert: %s", ca_cert_present ? "present" : "not present");
803 ESP_LOGV(TAG, " Client Cert: %s", client_cert_present ? "present" : "not present");
804 ESP_LOGV(TAG, " Client Key: %s", client_key_present ? "present" : "not present");
805 } else {
806#endif
807 ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), ap.get_password().c_str());
808#ifdef USE_WIFI_WPA2_EAP
809 }
810#endif
811 if (ap.has_channel()) {
812 ESP_LOGV(TAG, " Channel: %u", ap.get_channel());
813 } else {
814 ESP_LOGV(TAG, " Channel not set");
815 }
816#ifdef USE_WIFI_MANUAL_IP
817 if (ap.get_manual_ip().has_value()) {
818 ManualIP m = *ap.get_manual_ip();
819 ESP_LOGV(TAG, " Manual IP: Static IP=%s Gateway=%s Subnet=%s DNS1=%s DNS2=%s", m.static_ip.str().c_str(),
820 m.gateway.str().c_str(), m.subnet.str().c_str(), m.dns1.str().c_str(), m.dns2.str().c_str());
821 } else
822#endif
823 {
824 ESP_LOGV(TAG, " Using DHCP IP");
825 }
826 ESP_LOGV(TAG, " Hidden: %s", YESNO(ap.get_hidden()));
827#endif
828
829 if (!this->wifi_sta_connect_(ap)) {
830 ESP_LOGE(TAG, "wifi_sta_connect_ failed");
831 // Enter cooldown to allow WiFi hardware to stabilize
832 // (immediate failure suggests hardware not ready, different from connection timeout)
834 } else {
836 }
837 this->action_started_ = millis();
838}
839
840const LogString *get_signal_bars(int8_t rssi) {
841 // Check for disconnected sentinel value first
842 if (rssi == WIFI_RSSI_DISCONNECTED) {
843 // MULTIPLICATION SIGN
844 // Unicode: U+00D7, UTF-8: C3 97
845 return LOG_STR("\033[0;31m" // red
846 "\xc3\x97\xc3\x97\xc3\x97\xc3\x97"
847 "\033[0m");
848 }
849 // LOWER ONE QUARTER BLOCK
850 // Unicode: U+2582, UTF-8: E2 96 82
851 // LOWER HALF BLOCK
852 // Unicode: U+2584, UTF-8: E2 96 84
853 // LOWER THREE QUARTERS BLOCK
854 // Unicode: U+2586, UTF-8: E2 96 86
855 // FULL BLOCK
856 // Unicode: U+2588, UTF-8: E2 96 88
857 if (rssi >= -50) {
858 return LOG_STR("\033[0;32m" // green
859 "\xe2\x96\x82"
860 "\xe2\x96\x84"
861 "\xe2\x96\x86"
862 "\xe2\x96\x88"
863 "\033[0m");
864 } else if (rssi >= -65) {
865 return LOG_STR("\033[0;33m" // yellow
866 "\xe2\x96\x82"
867 "\xe2\x96\x84"
868 "\xe2\x96\x86"
869 "\033[0;37m"
870 "\xe2\x96\x88"
871 "\033[0m");
872 } else if (rssi >= -85) {
873 return LOG_STR("\033[0;33m" // yellow
874 "\xe2\x96\x82"
875 "\xe2\x96\x84"
876 "\033[0;37m"
877 "\xe2\x96\x86"
878 "\xe2\x96\x88"
879 "\033[0m");
880 } else {
881 return LOG_STR("\033[0;31m" // red
882 "\xe2\x96\x82"
883 "\033[0;37m"
884 "\xe2\x96\x84"
885 "\xe2\x96\x86"
886 "\xe2\x96\x88"
887 "\033[0m");
888 }
889}
890
892 bssid_t bssid = wifi_bssid();
893 char bssid_s[18];
894 format_mac_addr_upper(bssid.data(), bssid_s);
895
896 char mac_s[18];
897 ESP_LOGCONFIG(TAG, " Local MAC: %s", get_mac_address_pretty_into_buffer(mac_s));
898 if (this->is_disabled()) {
899 ESP_LOGCONFIG(TAG, " Disabled");
900 return;
901 }
902 // Use stack buffers for IP address formatting to avoid heap allocations
903 char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
904 for (auto &ip : wifi_sta_ip_addresses()) {
905 if (ip.is_set()) {
906 ESP_LOGCONFIG(TAG, " IP Address: %s", ip.str_to(ip_buf));
907 }
908 }
909 int8_t rssi = wifi_rssi();
910 // Use stack buffers for SSID and all IP addresses to avoid heap allocations
911 char ssid_buf[SSID_BUFFER_SIZE];
912 char subnet_buf[network::IP_ADDRESS_BUFFER_SIZE];
913 char gateway_buf[network::IP_ADDRESS_BUFFER_SIZE];
914 char dns1_buf[network::IP_ADDRESS_BUFFER_SIZE];
915 char dns2_buf[network::IP_ADDRESS_BUFFER_SIZE];
916 ESP_LOGCONFIG(TAG,
917 " SSID: " LOG_SECRET("'%s'") "\n"
918 " BSSID: " LOG_SECRET("%s") "\n"
919 " Hostname: '%s'\n"
920 " Signal strength: %d dB %s\n"
921 " Channel: %" PRId32 "\n"
922 " Subnet: %s\n"
923 " Gateway: %s\n"
924 " DNS1: %s\n"
925 " DNS2: %s",
926 wifi_ssid_to(ssid_buf), bssid_s, App.get_name().c_str(), rssi, LOG_STR_ARG(get_signal_bars(rssi)),
927 get_wifi_channel(), wifi_subnet_mask_().str_to(subnet_buf), wifi_gateway_ip_().str_to(gateway_buf),
928 wifi_dns_ip_(0).str_to(dns1_buf), wifi_dns_ip_(1).str_to(dns2_buf));
929#ifdef ESPHOME_LOG_HAS_VERBOSE
930 if (const WiFiAP *config = this->get_selected_sta_(); config && config->has_bssid()) {
931 ESP_LOGV(TAG, " Priority: %d", this->get_sta_priority(config->get_bssid()));
932 }
933#endif
934#ifdef USE_WIFI_11KV_SUPPORT
935 ESP_LOGCONFIG(TAG,
936 " BTM: %s\n"
937 " RRM: %s",
938 this->btm_ ? "enabled" : "disabled", this->rrm_ ? "enabled" : "disabled");
939#endif
940}
941
944 return;
945
946 ESP_LOGD(TAG, "Enabling");
947 this->error_from_callback_ = false;
949 this->start();
950}
951
954 return;
955
956 ESP_LOGD(TAG, "Disabling");
958 this->wifi_disconnect_();
959 this->wifi_mode_(false, false);
960}
961
963
965 this->action_started_ = millis();
966 ESP_LOGD(TAG, "Starting scan");
967 this->wifi_scan_start_(this->passive_scan_);
969}
970
1004[[nodiscard]] inline static bool wifi_scan_result_is_better(const WiFiScanResult &a, const WiFiScanResult &b) {
1005 // Matching networks always come before non-matching
1006 if (a.get_matches() && !b.get_matches())
1007 return true;
1008 if (!a.get_matches() && b.get_matches())
1009 return false;
1010
1011 // Both matching: check priority first (tracks connection failures via priority degradation)
1012 // Priority is decreased when a BSSID fails to connect, so lower priority = previously failed
1013 if (a.get_matches() && b.get_matches() && a.get_priority() != b.get_priority()) {
1014 return a.get_priority() > b.get_priority();
1015 }
1016
1017 // Use RSSI as tiebreaker (for equal-priority matching networks or all non-matching networks)
1018 return a.get_rssi() > b.get_rssi();
1019}
1020
1021// Helper function for insertion sort of WiFi scan results
1022// Using insertion sort instead of std::stable_sort saves flash memory
1023// by avoiding template instantiations (std::rotate, std::stable_sort, lambdas)
1024// IMPORTANT: This sort is stable (preserves relative order of equal elements)
1025template<typename VectorType> static void insertion_sort_scan_results(VectorType &results) {
1026 const size_t size = results.size();
1027 for (size_t i = 1; i < size; i++) {
1028 // Make a copy to avoid issues with move semantics during comparison
1029 WiFiScanResult key = results[i];
1030 int32_t j = i - 1;
1031
1032 // Move elements that are worse than key to the right
1033 // For stability, we only move if key is strictly better than results[j]
1034 while (j >= 0 && wifi_scan_result_is_better(key, results[j])) {
1035 results[j + 1] = results[j];
1036 j--;
1037 }
1038 results[j + 1] = key;
1039 }
1040}
1041
1042// Helper function to log matching scan results - marked noinline to prevent re-inlining into loop
1043__attribute__((noinline)) static void log_scan_result(const WiFiScanResult &res) {
1044 char bssid_s[18];
1045 auto bssid = res.get_bssid();
1046 format_mac_addr_upper(bssid.data(), bssid_s);
1047
1048 ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(),
1049 res.get_is_hidden() ? LOG_STR_LITERAL("(HIDDEN) ") : LOG_STR_LITERAL(""), bssid_s,
1050 LOG_STR_ARG(get_signal_bars(res.get_rssi())));
1051 ESP_LOGD(TAG, " Channel: %2u, RSSI: %3d dB, Priority: %4d", res.get_channel(), res.get_rssi(), res.get_priority());
1052}
1053
1054#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
1055// Helper function to log non-matching scan results at verbose level
1056__attribute__((noinline)) static void log_scan_result_non_matching(const WiFiScanResult &res) {
1057 char bssid_s[18];
1058 auto bssid = res.get_bssid();
1059 format_mac_addr_upper(bssid.data(), bssid_s);
1060
1061 ESP_LOGV(TAG, "- " LOG_SECRET("'%s'") " " LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), bssid_s,
1062 LOG_STR_ARG(get_signal_bars(res.get_rssi())));
1063}
1064#endif
1065
1067 if (!this->scan_done_) {
1068 if (millis() - this->action_started_ > WIFI_SCAN_TIMEOUT_MS) {
1069 ESP_LOGE(TAG, "Scan timeout");
1070 this->retry_connect();
1071 }
1072 return;
1073 }
1074 this->scan_done_ = false;
1075 this->did_scan_this_cycle_ = true;
1076
1077 if (this->scan_result_.empty()) {
1078 ESP_LOGW(TAG, "No networks found");
1079 this->retry_connect();
1080 return;
1081 }
1082
1083 ESP_LOGD(TAG, "Found networks:");
1084 for (auto &res : this->scan_result_) {
1085 for (auto &ap : this->sta_) {
1086 if (res.matches(ap)) {
1087 res.set_matches(true);
1088 // Cache priority lookup - do single search instead of 2 separate searches
1089 const bssid_t &bssid = res.get_bssid();
1090 if (!this->has_sta_priority(bssid)) {
1091 this->set_sta_priority(bssid, ap.get_priority());
1092 }
1093 res.set_priority(this->get_sta_priority(bssid));
1094 break;
1095 }
1096 }
1097 }
1098
1099 // Sort scan results using insertion sort for better memory efficiency
1100 insertion_sort_scan_results(this->scan_result_);
1101
1102 size_t non_matching_count = 0;
1103 for (auto &res : this->scan_result_) {
1104 if (res.get_matches()) {
1105 log_scan_result(res);
1106 } else {
1107#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
1108 log_scan_result_non_matching(res);
1109#else
1110 non_matching_count++;
1111#endif
1112 }
1113 }
1114 if (non_matching_count > 0) {
1115 ESP_LOGD(TAG, "- %zu non-matching (VERBOSE to show)", non_matching_count);
1116 }
1117
1118 // SYNCHRONIZATION POINT: Establish link between scan_result_[0] and selected_sta_index_
1119 // After sorting, scan_result_[0] contains the best network. Now find which sta_[i] config
1120 // matches that network and record it in selected_sta_index_. This keeps the two indices
1121 // synchronized so build_params_for_current_phase_() can safely use both to build connection parameters.
1122 const WiFiScanResult &scan_res = this->scan_result_[0];
1123 bool found_match = false;
1124 if (scan_res.get_matches()) {
1125 for (size_t i = 0; i < this->sta_.size(); i++) {
1126 if (scan_res.matches(this->sta_[i])) {
1127 // Safe cast: sta_.size() limited to MAX_WIFI_NETWORKS (127) in __init__.py validation
1128 // No overflow check needed - YAML validation prevents >127 networks
1129 this->selected_sta_index_ = static_cast<int8_t>(i); // Links scan_result_[0] with sta_[i]
1130 found_match = true;
1131 break;
1132 }
1133 }
1134 }
1135
1136 if (!found_match) {
1137 ESP_LOGW(TAG, "No matching network found");
1138 // No scan results matched our configured networks - transition directly to hidden mode
1139 // Don't call retry_connect() since we never attempted a connection (no BSSID to penalize)
1141 // If no hidden networks to try, skip connection attempt (will be handled on next loop)
1142 if (this->selected_sta_index_ == -1) {
1143 return;
1144 }
1145 // Now start connection attempt in hidden mode
1147 return; // scan started, wait for next loop iteration
1148 }
1149
1150 yield();
1151
1152 WiFiAP params = this->build_params_for_current_phase_();
1153 // Ensure we're in SCAN_CONNECTING phase when connecting with scan results
1154 // (needed when scan was started directly without transition_to_phase_, e.g., initial scan)
1155 this->start_connecting(params);
1156}
1157
1159 ESP_LOGCONFIG(TAG,
1160 "WiFi:\n"
1161 " Connected: %s",
1162 YESNO(this->is_connected()));
1163 this->print_connect_params_();
1164}
1165
1167 auto status = this->wifi_sta_connect_status_();
1168
1170 if (wifi_ssid().empty()) {
1171 ESP_LOGW(TAG, "Connection incomplete");
1172 this->retry_connect();
1173 return;
1174 }
1175
1176 ESP_LOGI(TAG, "Connected");
1177 // Warn if we had to retry with hidden network mode for a network that's not marked hidden
1178 // Only warn if we actually connected without scan data (SSID only), not if scan succeeded on retry
1179 if (const WiFiAP *config = this->get_selected_sta_(); this->retry_phase_ == WiFiRetryPhase::RETRY_HIDDEN &&
1180 config && !config->get_hidden() &&
1181 this->scan_result_.empty()) {
1182 ESP_LOGW(TAG, LOG_SECRET("'%s'") " should be marked hidden", config->get_ssid().c_str());
1183 }
1184 // Reset to initial phase on successful connection (don't log transition, just reset state)
1186 this->num_retried_ = 0;
1187 // Ensure next connection attempt does not inherit error state
1188 // so when WiFi disconnects later we start fresh and don't see
1189 // the first connection as a failure.
1190 this->error_from_callback_ = false;
1191
1192 this->print_connect_params_();
1193
1194 if (this->has_ap()) {
1195#ifdef USE_CAPTIVE_PORTAL
1196 if (this->is_captive_portal_active_()) {
1198 }
1199#endif
1200 ESP_LOGD(TAG, "Disabling AP");
1201 this->wifi_mode_({}, false);
1202 }
1203#ifdef USE_IMPROV
1204 if (this->is_esp32_improv_active_()) {
1206 }
1207#endif
1208
1210 this->num_retried_ = 0;
1211
1212 // Clear priority tracking if all priorities are at minimum
1214
1215#ifdef USE_WIFI_FAST_CONNECT
1217#endif
1218
1219 // Free scan results memory unless a component needs them
1220 if (!this->keep_scan_results_) {
1221 this->scan_result_.clear();
1222 this->scan_result_.shrink_to_fit();
1223 }
1224
1225 return;
1226 }
1227
1228 uint32_t now = millis();
1229 if (now - this->action_started_ > WIFI_CONNECT_TIMEOUT_MS) {
1230 ESP_LOGW(TAG, "Connection timeout, aborting connection attempt");
1231 this->wifi_disconnect_();
1232 this->retry_connect();
1233 return;
1234 }
1235
1236 if (this->error_from_callback_) {
1237 ESP_LOGW(TAG, "Connecting to network failed (callback)");
1238 this->retry_connect();
1239 return;
1240 }
1241
1243 return;
1244 }
1245
1247 ESP_LOGW(TAG, "Network no longer found");
1248 this->retry_connect();
1249 return;
1250 }
1251
1253 ESP_LOGW(TAG, "Connecting to network failed");
1254 this->retry_connect();
1255 return;
1256 }
1257
1258 ESP_LOGW(TAG, "Unknown connection status %d", (int) status);
1259 this->retry_connect();
1260}
1261
1269 switch (this->retry_phase_) {
1271#ifdef USE_WIFI_FAST_CONNECT
1273 // INITIAL_CONNECT and FAST_CONNECT_CYCLING_APS: no retries, try next AP or fall back to scan
1274 if (this->selected_sta_index_ < static_cast<int8_t>(this->sta_.size()) - 1) {
1275 return WiFiRetryPhase::FAST_CONNECT_CYCLING_APS; // Move to next AP
1276 }
1277#endif
1278 // Check if we should try explicit hidden networks before scanning
1279 // This handles reconnection after connection loss where first network is hidden
1280 if (!this->sta_.empty() && this->sta_[0].get_hidden()) {
1282 }
1283 // No more APs to try, fall back to scan
1285
1287 // Try all explicitly hidden networks before scanning
1288 if (this->num_retried_ + 1 < WIFI_RETRY_COUNT_PER_SSID) {
1289 return WiFiRetryPhase::EXPLICIT_HIDDEN; // Keep retrying same SSID
1290 }
1291
1292 // Exhausted retries on current SSID - check for more explicitly hidden networks
1293 // Stop when we reach a visible network (proceed to scanning)
1294 size_t next_index = this->selected_sta_index_ + 1;
1295 if (next_index < this->sta_.size() && this->sta_[next_index].get_hidden()) {
1296 // Found another explicitly hidden network
1298 }
1299
1300 // No more consecutive explicitly hidden networks
1301 // If ALL networks are hidden, skip scanning and go directly to restart
1302 if (this->find_first_non_hidden_index_() < 0) {
1304 }
1305 // Otherwise proceed to scanning for non-hidden networks
1307 }
1308
1310 // If scan found no networks or no matching networks, skip to hidden network mode
1311 if (this->scan_result_.empty() || !this->scan_result_[0].get_matches()) {
1313 }
1314
1315 if (this->num_retried_ + 1 < WIFI_RETRY_COUNT_PER_BSSID) {
1316 return WiFiRetryPhase::SCAN_CONNECTING; // Keep retrying same BSSID
1317 }
1318
1319 // Exhausted retries on current BSSID (scan_result_[0])
1320 // Its priority has been decreased, so on next scan it will be sorted lower
1321 // and we'll try the next best BSSID.
1322 // Check if there are any potentially hidden networks to try
1323 if (this->find_next_hidden_sta_(-1) >= 0) {
1324 return WiFiRetryPhase::RETRY_HIDDEN; // Found hidden networks to try
1325 }
1326 // No hidden networks - always go through RESTARTING_ADAPTER phase
1327 // This ensures num_retried_ gets reset and a fresh scan is triggered
1328 // The actual adapter restart will be skipped if captive portal/improv is active
1330
1332 // If no hidden SSIDs to try (selected_sta_index_ == -1), skip directly to rescan
1333 if (this->selected_sta_index_ >= 0) {
1334 if (this->num_retried_ + 1 < WIFI_RETRY_COUNT_PER_SSID) {
1335 return WiFiRetryPhase::RETRY_HIDDEN; // Keep retrying same SSID
1336 }
1337
1338 // Exhausted retries on current SSID - check if there are more potentially hidden SSIDs to try
1339 if (this->selected_sta_index_ < static_cast<int8_t>(this->sta_.size()) - 1) {
1340 // Check if find_next_hidden_sta_() would actually find another hidden SSID
1341 // as it might have been seen in the scan results and we want to skip those
1342 // otherwise we will get stuck in RETRY_HIDDEN phase
1343 if (this->find_next_hidden_sta_(this->selected_sta_index_) != -1) {
1344 // More hidden SSIDs available - stay in RETRY_HIDDEN, advance will happen in retry_connect()
1346 }
1347 }
1348 }
1349 // Exhausted all potentially hidden SSIDs - always go through RESTARTING_ADAPTER
1350 // This ensures num_retried_ gets reset and a fresh scan is triggered
1351 // The actual adapter restart will be skipped if captive portal/improv is active
1353
1355 // After restart, go back to explicit hidden if we went through it initially
1358 }
1359 // Skip scanning when captive portal/improv is active to avoid disrupting AP
1360 // Even passive scans can cause brief AP disconnections on ESP32
1361 if (this->is_captive_portal_active_() || this->is_esp32_improv_active_()) {
1363 }
1365 }
1366
1367 // Should never reach here
1369}
1370
1381 WiFiRetryPhase old_phase = this->retry_phase_;
1382
1383 // No-op if staying in same phase
1384 if (old_phase == new_phase) {
1385 return false;
1386 }
1387
1388 ESP_LOGD(TAG, "Retry phase: %s → %s", LOG_STR_ARG(retry_phase_to_log_string(old_phase)),
1389 LOG_STR_ARG(retry_phase_to_log_string(new_phase)));
1390
1391 this->retry_phase_ = new_phase;
1392 this->num_retried_ = 0; // Reset retry counter on phase change
1393
1394 // Phase-specific setup
1395 switch (new_phase) {
1396#ifdef USE_WIFI_FAST_CONNECT
1398 // Move to next configured AP - clear old scan data so new AP is tried with config only
1399 this->selected_sta_index_++;
1400 this->scan_result_.clear();
1401 break;
1402#endif
1403
1405 // Starting explicit hidden phase - reset to first network
1406 this->selected_sta_index_ = 0;
1407 break;
1408
1410 // Transitioning to scan-based connection
1411#ifdef USE_WIFI_FAST_CONNECT
1413 ESP_LOGI(TAG, "Fast connect exhausted, falling back to scan");
1414 }
1415#endif
1416 // Trigger scan if we don't have scan results OR if transitioning from phases that need fresh scan
1417 if (this->scan_result_.empty() || old_phase == WiFiRetryPhase::EXPLICIT_HIDDEN ||
1419 this->selected_sta_index_ = -1; // Will be set after scan completes
1420 this->start_scanning();
1421 return true; // Started scan, wait for completion
1422 }
1423 // Already have scan results - selected_sta_index_ should already be synchronized
1424 // (set in check_scanning_finished() when scan completed)
1425 // No need to reset it here
1426 break;
1427
1429 // Starting hidden mode - find first SSID that wasn't in scan results
1430 if (old_phase == WiFiRetryPhase::SCAN_CONNECTING) {
1431 // Keep scan results so we can skip SSIDs that were visible in the scan
1432 // Don't clear scan_result_ - we need it to know which SSIDs are NOT hidden
1433
1434 // If first network is marked hidden, we went through EXPLICIT_HIDDEN phase
1435 // In that case, skip networks marked hidden:true (already tried)
1436 // Otherwise, include them (they haven't been tried yet)
1438
1439 if (this->selected_sta_index_ == -1) {
1440 ESP_LOGD(TAG, "All SSIDs visible or already tried, skipping hidden mode");
1441 }
1442 }
1443 break;
1444
1446 // Skip actual adapter restart if captive portal/improv is active
1447 // This allows state machine to reset num_retried_ and trigger fresh scan
1448 // without disrupting the captive portal/improv connection
1449 if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_()) {
1450 this->restart_adapter();
1451 } else {
1452 // Even when skipping full restart, disconnect to clear driver state
1453 // Without this, platforms like LibreTiny may think we're still connecting
1454 this->wifi_disconnect_();
1455 }
1456 // Clear scan flag - we're starting a new retry cycle
1457 this->did_scan_this_cycle_ = false;
1458 // Always enter cooldown after restart (or skip-restart) to allow stabilization
1459 // Use extended cooldown when AP is active to avoid constant scanning that blocks DNS
1461 this->action_started_ = millis();
1462 // Return true to indicate we should wait (go to COOLDOWN) instead of immediately connecting
1463 return true;
1464
1465 default:
1466 break;
1467 }
1468
1469 return false; // Did not start scan, can proceed with connection
1470}
1471
1476 if (this->sta_priorities_.empty()) {
1477 return;
1478 }
1479
1480 int8_t first_priority = this->sta_priorities_[0].priority;
1481
1482 // Only clear if all priorities have been decremented to the minimum value
1483 // At this point, all BSSIDs have been equally penalized and priority info is useless
1484 if (first_priority != std::numeric_limits<int8_t>::min()) {
1485 return;
1486 }
1487
1488 for (const auto &pri : this->sta_priorities_) {
1489 if (pri.priority != first_priority) {
1490 return; // Not all same, nothing to do
1491 }
1492 }
1493
1494 // All priorities are at minimum - clear the vector to save memory and reset
1495 ESP_LOGD(TAG, "Clearing BSSID priorities (all at minimum)");
1496 this->sta_priorities_.clear();
1497 this->sta_priorities_.shrink_to_fit();
1498}
1499
1519 // Determine which BSSID we tried to connect to
1520 optional<bssid_t> failed_bssid;
1521
1522 if (this->retry_phase_ == WiFiRetryPhase::SCAN_CONNECTING && !this->scan_result_.empty()) {
1523 // Scan-based phase: always use best result (index 0)
1524 failed_bssid = this->scan_result_[0].get_bssid();
1525 } else if (const WiFiAP *config = this->get_selected_sta_(); config && config->has_bssid()) {
1526 // Config has specific BSSID (fast_connect or user-specified)
1527 failed_bssid = config->get_bssid();
1528 }
1529
1530 if (!failed_bssid.has_value()) {
1531 return; // No BSSID to penalize
1532 }
1533
1534 // Get SSID for logging (use pointer to avoid copy)
1535 const std::string *ssid = nullptr;
1536 if (this->retry_phase_ == WiFiRetryPhase::SCAN_CONNECTING && !this->scan_result_.empty()) {
1537 ssid = &this->scan_result_[0].get_ssid();
1538 } else if (const WiFiAP *config = this->get_selected_sta_()) {
1539 ssid = &config->get_ssid();
1540 }
1541
1542 // Only decrease priority on the last attempt for this phase
1543 // This prevents false positives from transient WiFi stack issues
1544 uint8_t max_retries = get_max_retries_for_phase(this->retry_phase_);
1545 bool is_last_attempt = (this->num_retried_ + 1 >= max_retries);
1546
1547 // Decrease priority only on last attempt to avoid false positives from transient failures
1548 int8_t old_priority = this->get_sta_priority(failed_bssid.value());
1549 int8_t new_priority = old_priority;
1550
1551 if (is_last_attempt) {
1552 // Decrease priority, but clamp to int8_t::min to prevent overflow
1553 new_priority =
1554 (old_priority > std::numeric_limits<int8_t>::min()) ? (old_priority - 1) : std::numeric_limits<int8_t>::min();
1555 this->set_sta_priority(failed_bssid.value(), new_priority);
1556 }
1557 char bssid_s[18];
1558 format_mac_addr_upper(failed_bssid.value().data(), bssid_s);
1559 ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d",
1560 ssid != nullptr ? ssid->c_str() : "", bssid_s, old_priority, new_priority);
1561
1562 // After adjusting priority, check if all priorities are now at minimum
1563 // If so, clear the vector to save memory and reset for fresh start
1565}
1566
1578 WiFiRetryPhase current_phase = this->retry_phase_;
1579
1580 // Check if we need to advance to next AP/SSID within the same phase
1581#ifdef USE_WIFI_FAST_CONNECT
1582 if (current_phase == WiFiRetryPhase::FAST_CONNECT_CYCLING_APS) {
1583 // Fast connect: always advance to next AP (no retries per AP)
1584 this->selected_sta_index_++;
1585 this->num_retried_ = 0;
1586 ESP_LOGD(TAG, "Next AP in %s", LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_)));
1587 return;
1588 }
1589#endif
1590
1591 if (current_phase == WiFiRetryPhase::EXPLICIT_HIDDEN && this->num_retried_ + 1 >= WIFI_RETRY_COUNT_PER_SSID) {
1592 // Explicit hidden: exhausted retries on current SSID, find next explicitly hidden network
1593 // Stop when we reach a visible network (proceed to scanning)
1594 size_t next_index = this->selected_sta_index_ + 1;
1595 if (next_index < this->sta_.size() && this->sta_[next_index].get_hidden()) {
1596 this->selected_sta_index_ = static_cast<int8_t>(next_index);
1597 this->num_retried_ = 0;
1598 ESP_LOGD(TAG, "Next explicit hidden network at index %d", static_cast<int>(next_index));
1599 return;
1600 }
1601 // No more consecutive explicit hidden networks found - fall through to trigger phase change
1602 }
1603
1604 if (current_phase == WiFiRetryPhase::RETRY_HIDDEN && this->num_retried_ + 1 >= WIFI_RETRY_COUNT_PER_SSID) {
1605 // Hidden mode: exhausted retries on current SSID, find next potentially hidden SSID
1606 // If first network is marked hidden, we went through EXPLICIT_HIDDEN phase
1607 // In that case, skip networks marked hidden:true (already tried)
1608 // Otherwise, include them (they haven't been tried yet)
1609 int8_t next_index = this->find_next_hidden_sta_(this->selected_sta_index_);
1610 if (next_index != -1) {
1611 // Found another potentially hidden SSID
1612 this->selected_sta_index_ = next_index;
1613 this->num_retried_ = 0;
1614 return;
1615 }
1616 // No more potentially hidden SSIDs - set selected_sta_index_ to -1 to trigger phase change
1617 // This ensures determine_next_phase_() will skip the RETRY_HIDDEN logic and transition out
1618 this->selected_sta_index_ = -1;
1619 // Return early - phase change will happen on next wifi_loop() iteration
1620 return;
1621 }
1622
1623 // Don't increment retry counter if we're in a scan phase with no valid targets
1624 if (this->needs_scan_results_()) {
1625 return;
1626 }
1627
1628 // Increment retry counter to try the same target again
1629 this->num_retried_++;
1630 ESP_LOGD(TAG, "Retry attempt %u/%u in phase %s", this->num_retried_ + 1,
1631 get_max_retries_for_phase(this->retry_phase_), LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_)));
1632}
1633
1636
1637 // Determine next retry phase based on current state
1638 WiFiRetryPhase current_phase = this->retry_phase_;
1639 WiFiRetryPhase next_phase = this->determine_next_phase_();
1640
1641 // Handle phase transitions (transition_to_phase_ handles same-phase no-op internally)
1642 if (this->transition_to_phase_(next_phase)) {
1643 return; // Scan started or adapter restarted (which sets its own state)
1644 }
1645
1646 if (next_phase == current_phase) {
1648 }
1649
1650 this->error_from_callback_ = false;
1651
1652 yield();
1653 // Check if we have a valid target before building params
1654 // After exhausting all networks in a phase, selected_sta_index_ may be -1
1655 // In that case, skip connection and let next wifi_loop() handle phase transition
1656 if (this->selected_sta_index_ >= 0) {
1657 WiFiAP params = this->build_params_for_current_phase_();
1658 this->start_connecting(params);
1659 }
1660}
1661
1662#ifdef USE_RP2040
1663// RP2040's mDNS library (LEAmDNS) relies on LwipIntf::stateUpCB() to restart
1664// mDNS when the network interface reconnects. However, this callback is disabled
1665// in the arduino-pico framework. As a workaround, we block component setup until
1666// WiFi is connected, ensuring mDNS.begin() is called with an active connection.
1667
1669 if (!this->has_sta() || this->state_ == WIFI_COMPONENT_STATE_DISABLED || this->ap_setup_) {
1670 return true;
1671 }
1672 return this->is_connected();
1673}
1674#endif
1675
1676void WiFiComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
1682 this->power_save_ = power_save;
1683#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
1684 this->configured_power_save_ = power_save;
1685#endif
1686}
1687
1688void WiFiComponent::set_passive_scan(bool passive) { this->passive_scan_ = passive; }
1689
1691#ifdef USE_CAPTIVE_PORTAL
1693#else
1694 return false;
1695#endif
1696}
1698#ifdef USE_IMPROV
1700#else
1701 return false;
1702#endif
1703}
1704
1705#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
1707 // Already configured for high performance - request satisfied
1709 return true;
1710 }
1711
1712 // Semaphore initialization failed
1713 if (this->high_performance_semaphore_ == nullptr) {
1714 return false;
1715 }
1716
1717 // Give the semaphore (non-blocking). This increments the count.
1718 return xSemaphoreGive(this->high_performance_semaphore_) == pdTRUE;
1719}
1720
1722 // Already configured for high performance - nothing to release
1724 return true;
1725 }
1726
1727 // Semaphore initialization failed
1728 if (this->high_performance_semaphore_ == nullptr) {
1729 return false;
1730 }
1731
1732 // Take the semaphore (non-blocking). This decrements the count.
1733 return xSemaphoreTake(this->high_performance_semaphore_, 0) == pdTRUE;
1734}
1735#endif // USE_ESP32 && USE_WIFI_RUNTIME_POWER_SAVE
1736
1737#ifdef USE_WIFI_FAST_CONNECT
1739 SavedWifiFastConnectSettings fast_connect_save{};
1740
1741 if (this->fast_connect_pref_.load(&fast_connect_save)) {
1742 // Validate saved AP index
1743 if (fast_connect_save.ap_index < 0 || static_cast<size_t>(fast_connect_save.ap_index) >= this->sta_.size()) {
1744 ESP_LOGW(TAG, "AP index out of bounds");
1745 return false;
1746 }
1747
1748 // Set selected index for future operations (save, retry, etc)
1749 this->selected_sta_index_ = fast_connect_save.ap_index;
1750
1751 // Copy entire config, then override with fast connect data
1752 params = this->sta_[fast_connect_save.ap_index];
1753
1754 // Override with saved BSSID/channel from fast connect (SSID/password/etc already copied from config)
1755 bssid_t bssid{};
1756 std::copy(fast_connect_save.bssid, fast_connect_save.bssid + 6, bssid.begin());
1757 params.set_bssid(bssid);
1758 params.set_channel(fast_connect_save.channel);
1759 // Fast connect uses specific BSSID+channel, not hidden network probe (even if config has hidden: true)
1760 params.set_hidden(false);
1761
1762 ESP_LOGD(TAG, "Loaded fast_connect settings");
1763 return true;
1764 }
1765
1766 return false;
1767}
1768
1770 bssid_t bssid = wifi_bssid();
1771 uint8_t channel = get_wifi_channel();
1772 // selected_sta_index_ is always valid here (called only after successful connection)
1773 // Fallback to 0 is defensive programming for robustness
1774 int8_t ap_index = this->selected_sta_index_ >= 0 ? this->selected_sta_index_ : 0;
1775
1776 // Skip save if settings haven't changed (compare with previously saved settings to reduce flash wear)
1777 SavedWifiFastConnectSettings previous_save{};
1778 if (this->fast_connect_pref_.load(&previous_save) && memcmp(previous_save.bssid, bssid.data(), 6) == 0 &&
1779 previous_save.channel == channel && previous_save.ap_index == ap_index) {
1780 return; // No change, nothing to save
1781 }
1782
1783 SavedWifiFastConnectSettings fast_connect_save{};
1784 memcpy(fast_connect_save.bssid, bssid.data(), 6);
1785 fast_connect_save.channel = channel;
1786 fast_connect_save.ap_index = ap_index;
1787
1788 this->fast_connect_pref_.save(&fast_connect_save);
1789
1790 ESP_LOGD(TAG, "Saved fast_connect settings");
1791}
1792#endif
1793
1794void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = ssid; }
1795void WiFiAP::set_bssid(const bssid_t &bssid) { this->bssid_ = bssid; }
1796void WiFiAP::clear_bssid() { this->bssid_ = {}; }
1797void WiFiAP::set_password(const std::string &password) { this->password_ = password; }
1798#ifdef USE_WIFI_WPA2_EAP
1799void WiFiAP::set_eap(optional<EAPAuth> eap_auth) { this->eap_ = std::move(eap_auth); }
1800#endif
1801void WiFiAP::set_channel(uint8_t channel) { this->channel_ = channel; }
1802void WiFiAP::clear_channel() { this->channel_ = 0; }
1803#ifdef USE_WIFI_MANUAL_IP
1804void WiFiAP::set_manual_ip(optional<ManualIP> manual_ip) { this->manual_ip_ = manual_ip; }
1805#endif
1806void WiFiAP::set_hidden(bool hidden) { this->hidden_ = hidden; }
1807const std::string &WiFiAP::get_ssid() const { return this->ssid_; }
1808const bssid_t &WiFiAP::get_bssid() const { return this->bssid_; }
1809bool WiFiAP::has_bssid() const { return this->bssid_ != bssid_t{}; }
1810const std::string &WiFiAP::get_password() const { return this->password_; }
1811#ifdef USE_WIFI_WPA2_EAP
1812const optional<EAPAuth> &WiFiAP::get_eap() const { return this->eap_; }
1813#endif
1814uint8_t WiFiAP::get_channel() const { return this->channel_; }
1815bool WiFiAP::has_channel() const { return this->channel_ != 0; }
1816#ifdef USE_WIFI_MANUAL_IP
1818#endif
1819bool WiFiAP::get_hidden() const { return this->hidden_; }
1820
1821WiFiScanResult::WiFiScanResult(const bssid_t &bssid, std::string ssid, uint8_t channel, int8_t rssi, bool with_auth,
1822 bool is_hidden)
1823 : bssid_(bssid),
1824 channel_(channel),
1825 rssi_(rssi),
1826 ssid_(std::move(ssid)),
1827 with_auth_(with_auth),
1828 is_hidden_(is_hidden) {}
1829bool WiFiScanResult::matches(const WiFiAP &config) const {
1830 if (config.get_hidden()) {
1831 // User configured a hidden network, only match actually hidden networks
1832 // don't match SSID
1833 if (!this->is_hidden_)
1834 return false;
1835 } else if (!config.get_ssid().empty()) {
1836 // check if SSID matches
1837 if (config.get_ssid() != this->ssid_)
1838 return false;
1839 } else {
1840 // network is configured without SSID - match other settings
1841 }
1842 // If BSSID configured, only match for correct BSSIDs
1843 if (config.has_bssid() && config.get_bssid() != this->bssid_)
1844 return false;
1845
1846#ifdef USE_WIFI_WPA2_EAP
1847 // BSSID requires auth but no PSK or EAP credentials given
1848 if (this->with_auth_ && (config.get_password().empty() && !config.get_eap().has_value()))
1849 return false;
1850
1851 // BSSID does not require auth, but PSK or EAP credentials given
1852 if (!this->with_auth_ && (!config.get_password().empty() || config.get_eap().has_value()))
1853 return false;
1854#else
1855 // If PSK given, only match for networks with auth (and vice versa)
1856 if (config.get_password().empty() == this->with_auth_)
1857 return false;
1858#endif
1859
1860 // If channel configured, only match networks on that channel.
1861 if (config.has_channel() && config.get_channel() != this->channel_) {
1862 return false;
1863 }
1864 return true;
1865}
1866bool WiFiScanResult::get_matches() const { return this->matches_; }
1867void WiFiScanResult::set_matches(bool matches) { this->matches_ = matches; }
1868const bssid_t &WiFiScanResult::get_bssid() const { return this->bssid_; }
1869const std::string &WiFiScanResult::get_ssid() const { return this->ssid_; }
1870uint8_t WiFiScanResult::get_channel() const { return this->channel_; }
1871int8_t WiFiScanResult::get_rssi() const { return this->rssi_; }
1872bool WiFiScanResult::get_with_auth() const { return this->with_auth_; }
1873bool WiFiScanResult::get_is_hidden() const { return this->is_hidden_; }
1874
1875bool WiFiScanResult::operator==(const WiFiScanResult &rhs) const { return this->bssid_ == rhs.bssid_; }
1876
1877WiFiComponent *global_wifi_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
1878
1879} // namespace esphome::wifi
1880#endif
uint8_t m
Definition bl0906.h:1
uint8_t status
Definition bl0942.h:8
bool is_name_add_mac_suffix_enabled() const
const std::string & get_name() const
Get the name of this Application set by pre_setup().
constexpr uint32_t get_config_version_hash()
Get the config hash extended with ESPHome version.
uint32_t IRAM_ATTR HOT get_loop_component_start_time() const
Get the cached time in milliseconds from when the current component started its loop execution.
void status_set_warning(const char *message=nullptr)
void status_clear_warning()
bool save(const T *src)
Definition preferences.h:21
virtual bool sync()=0
Commit pending writes to flash.
virtual ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash)=0
void trigger(const Ts &...x)
Inform the parent automation that the event has triggered.
Definition automation.h:204
bool has_value() const
Definition optional.h:92
value_type const & value() const
Definition optional.h:94
uint8_t get_channel() const
const std::string & get_ssid() const
void set_ssid(const std::string &ssid)
const optional< EAPAuth > & get_eap() const
const std::string & get_password() const
void set_bssid(const bssid_t &bssid)
void set_channel(uint8_t channel)
optional< EAPAuth > eap_
optional< ManualIP > manual_ip_
void set_eap(optional< EAPAuth > eap_auth)
void set_password(const std::string &password)
void set_manual_ip(optional< ManualIP > manual_ip)
const optional< ManualIP > & get_manual_ip() const
void set_hidden(bool hidden)
const bssid_t & get_bssid() const
This component is responsible for managing the ESP WiFi interface.
void add_sta(const WiFiAP &ap)
bool load_fast_connect_settings_(WiFiAP &params)
void set_ap(const WiFiAP &ap)
Setup an Access Point that should be created if no connection to a station can be made.
bool request_high_performance()
Request high-performance mode (no power saving) for improved WiFi latency.
void set_sta(const WiFiAP &ap)
bool has_sta_priority(const bssid_t &bssid)
const WiFiAP * get_selected_sta_() const
int8_t get_sta_priority(const bssid_t bssid)
void log_and_adjust_priority_for_failed_connect_()
Log failed connection and decrease BSSID priority to avoid repeated attempts.
void save_wifi_sta(const std::string &ssid, const std::string &password)
wifi_scan_vector_t< WiFiScanResult > scan_result_
WiFiPowerSaveMode configured_power_save_
void set_sta_priority(const bssid_t bssid, int8_t priority)
void loop() override
Reconnect WiFi if required.
void start_connecting(const WiFiAP &ap)
void advance_to_next_target_or_increment_retry_()
Advance to next target (AP/SSID) within current phase, or increment retry counter Called when staying...
SemaphoreHandle_t high_performance_semaphore_
network::IPAddress get_dns_address(int num)
WiFiComponent()
Construct a WiFiComponent.
std::vector< WiFiSTAPriority > sta_priorities_
const char * wifi_ssid_to(std::span< char, SSID_BUFFER_SIZE > buffer)
Write SSID to buffer without heap allocation.
void set_passive_scan(bool passive)
void set_power_save_mode(WiFiPowerSaveMode power_save)
int8_t find_next_hidden_sta_(int8_t start_index)
Find next SSID that wasn't in scan results (might be hidden) Returns index of next potentially hidden...
ESPPreferenceObject fast_connect_pref_
void clear_priorities_if_all_min_()
Clear BSSID priority tracking if all priorities are at minimum (saves memory)
WiFiRetryPhase determine_next_phase_()
Determine next retry phase based on current state and failure conditions.
network::IPAddress wifi_dns_ip_(int num)
float get_loop_priority() const override
network::IPAddresses get_ip_addresses()
float get_setup_priority() const override
WIFI setup_priority.
FixedVector< WiFiAP > sta_
int8_t find_first_non_hidden_index_() const
Find the index of the first non-hidden network Returns where EXPLICIT_HIDDEN phase would have stopped...
bool ssid_was_seen_in_scan_(const std::string &ssid) const
Check if an SSID was seen in the most recent scan results Used to skip hidden mode for SSIDs we know ...
bool needs_scan_results_() const
Check if we need valid scan results for the current phase but don't have any Returns true if the phas...
bool transition_to_phase_(WiFiRetryPhase new_phase)
Transition to a new retry phase with logging Returns true if a scan was started (caller should wait),...
bool release_high_performance()
Release a high-performance mode request.
bool wifi_apply_output_power_(float output_power)
const char * get_use_address() const
WiFiSTAConnectStatus wifi_sta_connect_status_()
bool went_through_explicit_hidden_phase_() const
Check if we went through EXPLICIT_HIDDEN phase (first network is marked hidden) Used in RETRY_HIDDEN ...
bool wifi_mode_(optional< bool > sta, optional< bool > ap)
void set_reboot_timeout(uint32_t reboot_timeout)
network::IPAddresses wifi_sta_ip_addresses()
void start_initial_connection_()
Start initial connection - either scan or connect directly to hidden networks.
void setup() override
Setup WiFi interface.
void set_use_address(const char *use_address)
const std::string & get_ssid() const
const bssid_t & get_bssid() const
WiFiScanResult(const bssid_t &bssid, std::string ssid, uint8_t channel, int8_t rssi, bool with_auth, bool is_hidden)
bool matches(const WiFiAP &config) const
bool operator==(const WiFiScanResult &rhs) const
struct @65::@66 __attribute__
uint16_t type
uint8_t priority
CaptivePortal * global_captive_portal
ESP32ImprovComponent * global_improv_component
std::array< IPAddress, 5 > IPAddresses
Definition ip_address.h:158
const char *const TAG
Definition spi.cpp:7
std::array< uint8_t, 6 > bssid_t
const LogString * get_signal_bars(int8_t rssi)
WiFiRetryPhase
Tracks the current retry strategy/phase for WiFi connection attempts.
@ RETRY_HIDDEN
Retry networks not found in scan (might be hidden)
@ RESTARTING_ADAPTER
Restarting WiFi adapter to clear stuck state.
@ INITIAL_CONNECT
Initial connection attempt (varies based on fast_connect setting)
@ EXPLICIT_HIDDEN
Explicitly hidden networks (user marked as hidden, try before scanning)
@ FAST_CONNECT_CYCLING_APS
Fast connect mode: cycling through configured APs (config-only, no scan)
@ SCAN_CONNECTING
Scan-based: connecting to best AP from scan results.
WiFiComponent * global_wifi_component
@ WIFI_COMPONENT_STATE_DISABLED
WiFi is disabled.
@ WIFI_COMPONENT_STATE_AP
WiFi is in AP-only mode and internal AP is already enabled.
@ WIFI_COMPONENT_STATE_STA_CONNECTING
WiFi is in STA(+AP) mode and currently connecting to an AP.
@ WIFI_COMPONENT_STATE_OFF
Nothing has been initialized yet.
@ WIFI_COMPONENT_STATE_STA_SCANNING
WiFi is in STA-only mode and currently scanning for APs.
@ WIFI_COMPONENT_STATE_COOLDOWN
WiFi is in cooldown mode because something went wrong, scanning will begin after a short period of ti...
@ WIFI_COMPONENT_STATE_STA_CONNECTED
WiFi is in STA(+AP) mode and successfully connected.
void format_mac_addr_upper(const uint8_t *mac, char *output)
Format MAC address as XX:XX:XX:XX:XX:XX (uppercase, colon separators)
Definition helpers.h:765
ESPPreferences * global_preferences
void IRAM_ATTR HOT yield()
Definition core.cpp:24
const char * get_mac_address_pretty_into_buffer(std::span< char, MAC_ADDRESS_PRETTY_BUFFER_SIZE > buf)
Get the device MAC address into the given buffer, in colon-separated uppercase hex notation.
Definition helpers.cpp:710
uint32_t IRAM_ATTR HOT millis()
Definition core.cpp:25
Application App
Global storage of Application pointer - only one Application can exist.
std::string str() const
Definition ip_address.h:55
esp_eap_ttls_phase2_types ttls_phase_2
Struct for setting static IPs in WiFiComponent.