ESPHome 2026.8.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#include <type_traits>
7
8#ifdef USE_ESP32
9#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
10#include <esp_eap_client.h>
11#else
12#include <esp_wpa2.h>
13#endif
14#endif
15
16#if defined(USE_ESP32)
17#include <esp_wifi.h>
18#endif
19#ifdef USE_ESP8266
20#include <user_interface.h>
21#endif
22
23#include <algorithm>
24#include <new>
25#include <utility>
26#include "lwip/dns.h"
27#include "lwip/err.h"
28
30#include "esphome/core/hal.h"
32#include "esphome/core/log.h"
34#include "esphome/core/util.h"
35
36#ifdef USE_CAPTIVE_PORTAL
38#endif
39
40#ifdef USE_IMPROV
42#endif
43
44#ifdef USE_IMPROV_SERIAL
46#endif
47
48#ifdef USE_PROVISIONING
50#endif
51
52namespace esphome::wifi {
53
54static const char *const TAG = "wifi";
55
56// CompactString implementation
57CompactString::CompactString(const char *str, size_t len) {
58 if (len > MAX_LENGTH) {
59 len = MAX_LENGTH; // Clamp to max valid length
60 }
61
62 this->length_ = len;
63 if (len <= INLINE_CAPACITY) {
64 // Store inline with null terminator
65 this->is_heap_ = 0;
66 if (len > 0) {
67 std::memcpy(this->storage_, str, len);
68 }
69 this->storage_[len] = '\0';
70 } else {
71 // Heap allocate with null terminator
72 this->is_heap_ = 1;
73 char *heap_data = new char[len + 1]; // NOLINT(cppcoreguidelines-owning-memory)
74 std::memcpy(heap_data, str, len);
75 heap_data[len] = '\0';
76 this->set_heap_ptr_(heap_data);
77 }
78}
79
80CompactString::CompactString(const CompactString &other) : CompactString(other.data(), other.size()) {}
81
83 if (this != &other) {
84 this->~CompactString();
85 new (this) CompactString(other);
86 }
87 return *this;
88}
89
90CompactString::CompactString(CompactString &&other) noexcept : length_(other.length_), is_heap_(other.is_heap_) {
91 // Copy full storage (includes null terminator for inline, or pointer for heap)
92 std::memcpy(this->storage_, other.storage_, INLINE_CAPACITY + 1);
93 other.length_ = 0;
94 other.is_heap_ = 0;
95 other.storage_[0] = '\0';
96}
97
99 if (this != &other) {
100 this->~CompactString();
101 new (this) CompactString(std::move(other));
102 }
103 return *this;
104}
105
107 if (this->is_heap_) {
108 delete[] this->get_heap_ptr_(); // NOLINT(cppcoreguidelines-owning-memory)
109 }
110}
111
112bool CompactString::operator==(const CompactString &other) const {
113 return this->size() == other.size() && std::memcmp(this->data(), other.data(), this->size()) == 0;
114}
115bool CompactString::operator==(const StringRef &other) const {
116 return this->size() == other.size() && std::memcmp(this->data(), other.c_str(), this->size()) == 0;
117}
118
314
315#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_INFO
316#ifdef USE_WIFI_PHY_MODE
317// Use if-chain instead of switch to avoid jump table in RODATA (wastes RAM on ESP8266)
318static const LogString *phy_mode_to_log_string(WiFi8266PhyMode mode) {
320 return LOG_STR("11B");
322 return LOG_STR("11G");
324 return LOG_STR("11N");
325 return LOG_STR("Auto");
326}
327#endif
328// Use if-chain instead of switch to avoid jump table in RODATA (wastes RAM on ESP8266)
329static const LogString *retry_phase_to_log_string(WiFiRetryPhase phase) {
331 return LOG_STR("INITIAL_CONNECT");
332#ifdef USE_WIFI_FAST_CONNECT
334 return LOG_STR("FAST_CONNECT_CYCLING");
335#endif
337 return LOG_STR("EXPLICIT_HIDDEN");
339 return LOG_STR("SCAN_CONNECTING");
340 if (phase == WiFiRetryPhase::RETRY_HIDDEN)
341 return LOG_STR("RETRY_HIDDEN");
343 return LOG_STR("RESTARTING");
344 return LOG_STR("UNKNOWN");
345}
346#endif // ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_INFO
347
349 // If first configured network is marked hidden, we went through EXPLICIT_HIDDEN phase
350 // This means those networks were already tried and should be skipped in RETRY_HIDDEN
351 return !this->sta_.empty() && this->sta_[0].get_hidden();
352}
353
355 // Find the first network that is NOT marked hidden:true
356 // This is where EXPLICIT_HIDDEN phase would have stopped
357 for (size_t i = 0; i < this->sta_.size(); i++) {
358 if (!this->sta_[i].get_hidden()) {
359 return static_cast<int8_t>(i);
360 }
361 }
362 return -1; // All networks are hidden
363}
364
365// 2 attempts per BSSID in SCAN_CONNECTING phase
366// Rationale: This is the ONLY phase where we decrease BSSID priority, so we must be very sure.
367// Auth failures are common immediately after scan due to WiFi stack state transitions.
368// Trying twice filters out false positives and prevents unnecessarily marking a good BSSID as bad.
369// After 2 genuine failures, priority degradation ensures we skip this BSSID on subsequent scans.
370static constexpr uint8_t WIFI_RETRY_COUNT_PER_BSSID = 2;
371
372// 1 attempt per SSID in RETRY_HIDDEN phase
373// Rationale: Try hidden mode once, then rescan to get next best BSSID via priority system
374static constexpr uint8_t WIFI_RETRY_COUNT_PER_SSID = 1;
375
376// 1 attempt per AP in fast_connect mode (INITIAL_CONNECT and FAST_CONNECT_CYCLING_APS)
377// Rationale: Fast connect prioritizes speed - try each AP once to find a working one quickly
378static constexpr uint8_t WIFI_RETRY_COUNT_PER_AP = 1;
379
382static constexpr uint32_t WIFI_COOLDOWN_DURATION_MS = 500;
383
387static constexpr uint32_t WIFI_COOLDOWN_WITH_AP_ACTIVE_MS = 30000;
388
392static constexpr uint32_t WIFI_SCAN_TIMEOUT_MS = 31000;
393
402static constexpr uint32_t WIFI_CONNECT_TIMEOUT_MS = 46000;
403
404static constexpr uint8_t get_max_retries_for_phase(WiFiRetryPhase phase) {
405 switch (phase) {
407#ifdef USE_WIFI_FAST_CONNECT
409#endif
410 // INITIAL_CONNECT and FAST_CONNECT_CYCLING_APS both use 1 attempt per AP (fast_connect mode)
411 return WIFI_RETRY_COUNT_PER_AP;
413 // Explicitly hidden network: 1 attempt (user marked as hidden, try once then scan)
414 return WIFI_RETRY_COUNT_PER_SSID;
416 // Scan-based phase: 2 attempts per BSSID (handles transient auth failures after scan)
417 return WIFI_RETRY_COUNT_PER_BSSID;
419 // Hidden network mode: 1 attempt per SSID
420 return WIFI_RETRY_COUNT_PER_SSID;
421 default:
422 return WIFI_RETRY_COUNT_PER_BSSID;
423 }
424}
425
426static void apply_scan_result_to_params(WiFiAP &params, const WiFiScanResult &scan) {
427 params.set_hidden(false);
428 params.set_ssid(scan.get_ssid());
429 params.set_bssid(scan.get_bssid());
430 params.set_channel(scan.get_channel());
431}
432
434 // Only SCAN_CONNECTING phase needs scan results
436 return false;
437 }
438 // Need scan if we have no results or no matching networks
439 return this->scan_result_.empty() || !this->scan_result_[0].get_matches();
440}
441
443 // Check if this SSID is configured as hidden
444 // If explicitly marked hidden, we should always try hidden mode regardless of scan results
445 for (const auto &conf : this->sta_) {
446 if (conf.ssid_ == ssid && conf.get_hidden()) {
447 return false; // Treat as not seen - force hidden mode attempt
448 }
449 }
450
451 // Otherwise, check if we saw it in scan results
452 for (const auto &scan : this->scan_result_) {
453 if (scan.ssid_ == ssid) {
454 return true;
455 }
456 }
457 return false;
458}
459
461 // Components that require full scan results (for example, scan result listeners)
462 // are expected to call request_wifi_scan_results(), which sets keep_scan_results_.
463 if (this->keep_scan_results_) {
464 return true;
465 }
466
467#ifdef USE_CAPTIVE_PORTAL
468 // Captive portal needs full results when active (showing network list to user)
470 return true;
471 }
472#endif
473
474#ifdef USE_IMPROV_SERIAL
475 // Improv serial needs results during provisioning (before connected)
477 return true;
478 }
479#endif
480
481#ifdef USE_IMPROV
482 // BLE improv also needs results during provisioning
484 return true;
485 }
486#endif
487
488 return false;
489}
490
491bool WiFiComponent::matches_configured_network_(const char *ssid, const uint8_t *bssid) const {
492 // Hidden networks in scan results have empty SSIDs - skip them
493 if (ssid[0] == '\0') {
494 return false;
495 }
496 for (const auto &sta : this->sta_) {
497 // Skip hidden network configs (they don't appear in normal scans)
498 if (sta.get_hidden()) {
499 continue;
500 }
501 // For BSSID-only configs (empty SSID), match by BSSID
502 if (sta.ssid_.empty()) {
503 if (sta.has_bssid() && std::memcmp(sta.get_bssid().data(), bssid, 6) == 0) {
504 return true;
505 }
506 continue;
507 }
508 // Match by SSID
509 if (sta.ssid_ == ssid) {
510 return true;
511 }
512 }
513 return false;
514}
515
517 for (auto &it : this->sta_priorities_) {
518 if (it.bssid == bssid) {
519 it.priority = priority;
520 return;
521 }
522 }
523 this->sta_priorities_.push_back(WiFiSTAPriority{
524 .bssid = bssid,
525 .priority = priority,
526 });
527}
528
529void WiFiComponent::log_discarded_scan_result_(const char *ssid, const uint8_t *bssid, int8_t rssi, uint8_t channel) {
530#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
531 // Skip logging during roaming scans to avoid log buffer overflow
532 // (roaming scans typically find many networks but only care about same-SSID APs)
534 return;
535 }
536 char bssid_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
537 format_mac_addr_upper(bssid, bssid_s);
538 ESP_LOGV(TAG, "- " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") " %ddB Ch:%u", ssid, bssid_s, rssi, channel);
539#endif
540}
541
542int8_t WiFiComponent::find_next_hidden_sta_(int8_t start_index) {
543 // Find next SSID to try in RETRY_HIDDEN phase.
544 //
545 // This function operates in two modes based on retry_hidden_mode_:
546 //
547 // 1. SCAN_BASED mode:
548 // After SCAN_CONNECTING phase, only returns networks that were NOT visible
549 // in the scan (truly hidden networks that need probe requests).
550 //
551 // 2. BLIND_RETRY mode:
552 // When captive portal/improv is active, scanning is skipped to avoid
553 // disrupting the AP. In this mode, ALL configured networks are returned
554 // as candidates, cycling through them sequentially. This allows the device
555 // to keep trying all networks while users configure WiFi via captive portal.
556 //
557 // Additionally, if EXPLICIT_HIDDEN phase was executed (first network marked hidden:true),
558 // those networks are skipped here since they were already tried.
559 //
560 bool include_explicit_hidden = !this->went_through_explicit_hidden_phase_();
561 // Start searching from start_index + 1
562 for (size_t i = start_index + 1; i < this->sta_.size(); i++) {
563 const auto &sta = this->sta_[i];
564
565 // Skip networks that were already tried in EXPLICIT_HIDDEN phase
566 // Those are: networks marked hidden:true that appear before the first non-hidden network
567 // If all networks are hidden (first_non_hidden_idx == -1), skip all of them
568 if (!include_explicit_hidden && sta.get_hidden()) {
569 int8_t first_non_hidden_idx = this->find_first_non_hidden_index_();
570 if (first_non_hidden_idx < 0 || static_cast<int8_t>(i) < first_non_hidden_idx) {
571 ESP_LOGD(TAG, "Skipping " LOG_SECRET("'%s'") " (explicit hidden, already tried)", sta.ssid_.c_str());
572 continue;
573 }
574 }
575
576 // In BLIND_RETRY mode, treat all networks as candidates
577 // In SCAN_BASED mode, only retry networks that weren't seen in the scan
579 ESP_LOGD(TAG, "Hidden candidate " LOG_SECRET("'%s'") " at index %d", sta.ssid_.c_str(), static_cast<int>(i));
580 return static_cast<int8_t>(i);
581 }
582 ESP_LOGD(TAG, "Skipping hidden retry for visible network " LOG_SECRET("'%s'"), sta.ssid_.c_str());
583 }
584 // No hidden SSIDs found
585 return -1;
586}
587
589 // If first network (highest priority) is explicitly marked hidden, try it first before scanning
590 // This respects user's priority order when they explicitly configure hidden networks
591 if (!this->sta_.empty() && this->sta_[0].get_hidden()) {
592 ESP_LOGI(TAG, "Starting with explicit hidden network (highest priority)");
593 this->selected_sta_index_ = 0;
596 this->start_connecting(params);
597 } else {
598 this->start_scanning();
599 }
600}
601
602#if defined(USE_ESP32) && defined(USE_WIFI_WPA2_EAP) && ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
603static const char *eap_phase2_to_str(esp_eap_ttls_phase2_types type) {
604 switch (type) {
605 case ESP_EAP_TTLS_PHASE2_PAP:
606 return "pap";
607 case ESP_EAP_TTLS_PHASE2_CHAP:
608 return "chap";
609 case ESP_EAP_TTLS_PHASE2_MSCHAP:
610 return "mschap";
611 case ESP_EAP_TTLS_PHASE2_MSCHAPV2:
612 return "mschapv2";
613 case ESP_EAP_TTLS_PHASE2_EAP:
614 return "eap";
615 default:
616 return "unknown";
617 }
618}
619#endif
620
622
624 this->wifi_pre_setup_();
625
626#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
627 // Create semaphore for high-performance mode requests
628 // Start at 0, increment on request, decrement on release
629 this->high_performance_semaphore_ = xSemaphoreCreateCounting(UINT32_MAX, 0);
630 if (this->high_performance_semaphore_ == nullptr) {
631 ESP_LOGE(TAG, "Failed semaphore");
632 }
633
634 // Store the configured power save mode as baseline
636#endif
637
638 if (this->enable_on_boot_) {
639#ifdef USE_ESP32
640 this->wifi_lazy_init_();
641#endif
642 this->start();
643 } else {
645 }
646}
647
649 ESP_LOGCONFIG(TAG, "Starting");
650 this->last_connected_ = millis();
651
652 uint32_t hash = this->has_sta() ? App.get_config_version_hash() : 88491487UL;
653
655#ifdef USE_WIFI_FAST_CONNECT
656#ifdef USE_WIFI_FAST_CONNECT_IN_FLASH
657 const bool fast_connect_in_flash = true;
658#else
659 const bool fast_connect_in_flash = false;
660#endif
661 this->fast_connect_pref_ =
663#endif
664
665 SavedWifiSettings save{};
666 if (this->pref_.load(&save)) {
667 ESP_LOGD(TAG, "Loaded settings: %s", save.ssid);
668
669 WiFiAP sta{};
670 sta.set_ssid(save.ssid);
671 sta.set_password(save.password);
672 this->set_sta(sta);
673 }
674
675 if (this->has_sta()) {
676 this->wifi_sta_pre_setup_();
677 if (!std::isnan(this->output_power_) && !this->wifi_apply_output_power_(this->output_power_)) {
678 ESP_LOGV(TAG, "Setting Output Power Option failed");
679 }
680
681#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
682 // Synchronize power_save_ with semaphore state before applying
683 if (this->high_performance_semaphore_ != nullptr) {
684 UBaseType_t semaphore_count = uxSemaphoreGetCount(this->high_performance_semaphore_);
685 if (semaphore_count > 0) {
687 this->is_high_performance_mode_ = true;
688 } else {
690 this->is_high_performance_mode_ = false;
691 }
692 }
693#endif
694 if (!this->wifi_apply_power_save_()) {
695 ESP_LOGV(TAG, "Setting Power Save Option failed");
696 }
697
699#ifdef USE_WIFI_FAST_CONNECT
700 WiFiAP params;
701 bool loaded_fast_connect = this->load_fast_connect_settings_(params);
702 // Fast connect optimization: only use when we have saved BSSID+channel data
703 // Without saved data, try first configured network or use normal flow
704 if (loaded_fast_connect) {
705 ESP_LOGI(TAG, "Starting fast_connect (saved) " LOG_SECRET("'%s'"), params.ssid_.c_str());
706 this->start_connecting(params);
707 } else if (!this->sta_.empty() && !this->sta_[0].get_hidden()) {
708 // No saved data, but have configured networks - try first non-hidden network
709 ESP_LOGI(TAG, "Starting fast_connect (config) " LOG_SECRET("'%s'"), this->sta_[0].ssid_.c_str());
710 this->selected_sta_index_ = 0;
711 params = this->build_params_for_current_phase_();
712 this->start_connecting(params);
713 } else {
714 // No saved data and (no networks OR first is hidden) - use normal flow
716 }
717#else
718 // Without fast_connect: go straight to scanning (or hidden mode if all networks are hidden)
720#endif
721#ifdef USE_WIFI_AP
722 } else if (this->has_ap()) {
723 this->setup_ap_config_();
724 if (!std::isnan(this->output_power_) && !this->wifi_apply_output_power_(this->output_power_)) {
725 ESP_LOGV(TAG, "Setting Output Power Option failed");
726 }
727#ifdef USE_CAPTIVE_PORTAL
729 this->wifi_sta_pre_setup_();
730 this->start_scanning();
732 }
733#endif
734#endif // USE_WIFI_AP
735 }
736#ifdef USE_IMPROV
737 if (!this->has_sta() && esp32_improv::global_improv_component != nullptr) {
738 if (this->wifi_mode_(true, {}))
740 }
741#endif
742 this->wifi_apply_hostname_();
743}
744
746 ESP_LOGW(TAG, "Restarting adapter");
747 this->wifi_mode_(false, {});
748 // Clear error flag here because restart_adapter() enters COOLDOWN state,
749 // and check_connecting_finished() is called after cooldown without going
750 // through start_connecting() first. Without this clear, stale errors would
751 // trigger spurious "failed (callback)" logs. The canonical clear location
752 // is in start_connecting(); this is the only exception to that pattern.
753 this->error_from_callback_ = false;
754}
755
757 bool events_processed = this->wifi_loop_();
759 // Connection state can only change when events are processed (ESP-IDF/LibreTiny)
760 // or polled (ESP8266/Pico W). Skip the expensive wifi_sta_connect_status_() call
761 // when no events arrived and we're already in steady state.
762 // Must also run when connected_ is false — after state transitions to STA_CONNECTED,
763 // connected_ won't be set until update_connected_state_() runs.
764 if (events_processed || !this->connected_) {
766 }
767
768 if (this->has_sta()) {
769#if defined(USE_WIFI_CONNECT_TRIGGER) || defined(USE_WIFI_DISCONNECT_TRIGGER)
770 if (this->is_connected() != this->handled_connected_state_) {
771#ifdef USE_WIFI_DISCONNECT_TRIGGER
772 if (this->handled_connected_state_) {
774 }
775#endif
776#ifdef USE_WIFI_CONNECT_TRIGGER
777 if (!this->handled_connected_state_) {
779 }
780#endif
782 }
783#endif // USE_WIFI_CONNECT_TRIGGER || USE_WIFI_DISCONNECT_TRIGGER
784
785 switch (this->state_) {
787 this->status_set_warning(LOG_STR("waiting to reconnect"));
788 // Skip cooldown if new credentials were provided while connecting
789 if (this->skip_cooldown_next_cycle_) {
790 this->skip_cooldown_next_cycle_ = false;
791 this->check_connecting_finished(now);
792 break;
793 }
794 // Use longer cooldown when captive portal/improv is active to avoid disrupting user config
795 bool portal_active = this->is_captive_portal_active_() || this->is_esp32_improv_active_();
796 uint32_t cooldown_duration = portal_active ? WIFI_COOLDOWN_WITH_AP_ACTIVE_MS : WIFI_COOLDOWN_DURATION_MS;
797 if (now - this->action_started_ > cooldown_duration) {
798 // After cooldown we either restarted the adapter because of
799 // a failure, or something tried to connect over and over
800 // so we entered cooldown. In both cases we call
801 // check_connecting_finished to continue the state machine.
802 this->check_connecting_finished(now);
803 }
804 break;
805 }
807 this->status_set_warning(LOG_STR("scanning for networks"));
809 break;
810 }
812 this->status_set_warning(LOG_STR("associating to network"));
813 this->check_connecting_finished(now);
814 break;
815 }
816
818 // Use cached connected_ set unconditionally at the top of loop()
819 if (!this->connected_) {
820 ESP_LOGW(TAG, "Connection lost; reconnecting");
822 this->retry_connect();
823 } else {
824 this->status_clear_warning();
825 this->last_connected_ = now;
826
827 // Post-connect roaming: check for better AP
828 if (this->post_connect_roaming_) {
830 if (this->scan_done_) {
831 this->process_roaming_scan_();
832 }
833 // else: scan in progress, wait
836 this->check_roaming_(now);
837 }
838 }
839 }
840 break;
841 }
844 break;
846 return;
847 }
848
849#ifdef USE_WIFI_AP
850 if (this->has_ap() && !this->ap_setup_) {
851 if (this->ap_timeout_ != 0 && (now - this->last_connected_ > this->ap_timeout_)) {
852 ESP_LOGI(TAG, "Starting fallback AP");
853 this->setup_ap_config_();
854#ifdef USE_CAPTIVE_PORTAL
856 // Reset so we force one full scan after captive portal starts
857 // (previous scans were filtered because captive portal wasn't active yet)
860 }
861#endif
862 }
863 }
864#endif // USE_WIFI_AP
865
866#ifdef USE_IMPROV
868 !esp32_improv::global_improv_component->should_start()) {
869 if (now - this->last_connected_ > esp32_improv::global_improv_component->get_wifi_timeout()) {
870 if (this->wifi_mode_(true, {}))
872 }
873 }
874
875#endif
876
877 if (!this->has_ap() && this->reboot_timeout_ != 0) {
878 if (now - this->last_connected_ > this->reboot_timeout_) {
879 bool suppress = false;
880#ifdef USE_PROVISIONING
881 // Don't reboot while a provisioning window is pending (device unprovisioned).
882 // The device is legitimately waiting to be onboarded (Wi-Fi must come up
883 // before the controller can set credentials), and an auto-reboot would reopen
884 // the window without the deliberate power cycle / reset that is meant to be
885 // required. Resumes normal reboot behavior once provisioned.
886 suppress = provisioning::global_provisioning_manager != nullptr &&
888#endif
889 if (!suppress) {
890 ESP_LOGE(TAG, "Can't connect; rebooting");
891 App.reboot();
892 }
893 }
894 }
895 }
896
897#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
898 // Check if power save mode needs to be updated based on high-performance requests
899 if (this->high_performance_semaphore_ != nullptr) {
900 // Semaphore count directly represents active requests (starts at 0, increments on request)
901 UBaseType_t semaphore_count = uxSemaphoreGetCount(this->high_performance_semaphore_);
902
903 if (semaphore_count > 0 && !this->is_high_performance_mode_) {
904 // Transition to high-performance mode (no power save)
905 ESP_LOGV(TAG, "Switching to high-performance mode (%" PRIu32 " active %s)", (uint32_t) semaphore_count,
906 semaphore_count == 1 ? "request" : "requests");
908 if (this->wifi_apply_power_save_()) {
909 this->is_high_performance_mode_ = true;
910 }
911 } else if (semaphore_count == 0 && this->is_high_performance_mode_) {
912 // Restore to configured power save mode
913 ESP_LOGV(TAG, "Restoring power save mode to configured setting");
915 if (this->wifi_apply_power_save_()) {
916 this->is_high_performance_mode_ = false;
917 }
918 }
919 }
920#endif
921}
922
924
925#ifdef USE_WIFI_11KV_SUPPORT
926void WiFiComponent::set_btm(bool btm) { this->btm_ = btm; }
927void WiFiComponent::set_rrm(bool rrm) { this->rrm_ = rrm; }
928#endif
930 if (this->has_sta())
931 return this->wifi_sta_ip_addresses();
932
933#ifdef USE_WIFI_AP
934 if (this->has_ap())
935 return {this->wifi_soft_ap_ip()};
936#endif // USE_WIFI_AP
937
938 return {};
939}
941 if (this->has_sta())
942 return this->wifi_dns_ip_(num);
943 return {};
944}
945
946#ifdef USE_WIFI_AP
948 this->wifi_mode_({}, true);
949
950 if (this->ap_setup_)
951 return;
952
953 if (this->ap_.ssid_.empty()) {
954 // Build AP SSID from app name without heap allocation
955 // WiFi SSID max is 32 bytes, with MAC suffix we keep first 25 + last 7
956 static constexpr size_t AP_SSID_MAX_LEN = 32;
957 static constexpr size_t AP_SSID_PREFIX_LEN = 25;
958 static constexpr size_t AP_SSID_SUFFIX_LEN = 7;
959
960 const auto &app_name = App.get_name();
961 const char *name_ptr = app_name.c_str();
962 size_t name_len = app_name.length();
963
964 if (name_len <= AP_SSID_MAX_LEN) {
965 // Name fits, use directly
966 this->ap_.set_ssid(name_ptr);
967 } else {
968 // Name too long, need to truncate into stack buffer
969 char ssid_buf[AP_SSID_MAX_LEN + 1];
971 // Keep first 25 chars and last 7 chars (MAC suffix), remove middle
972 memcpy(ssid_buf, name_ptr, AP_SSID_PREFIX_LEN);
973 memcpy(ssid_buf + AP_SSID_PREFIX_LEN, name_ptr + name_len - AP_SSID_SUFFIX_LEN, AP_SSID_SUFFIX_LEN);
974 } else {
975 memcpy(ssid_buf, name_ptr, AP_SSID_MAX_LEN);
976 }
977 ssid_buf[AP_SSID_MAX_LEN] = '\0';
978 this->ap_.set_ssid(ssid_buf);
979 }
980 }
981 this->ap_setup_ = this->wifi_start_ap_(this->ap_);
982
983 char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
984 ESP_LOGCONFIG(TAG,
985 "Setting up AP:\n"
986 " AP SSID: '%s'\n"
987 " AP Password: '%s'\n"
988 " IP Address: %s",
989 this->ap_.ssid_.c_str(), this->ap_.password_.c_str(), this->wifi_soft_ap_ip().str_to(ip_buf));
990
991#ifdef USE_WIFI_MANUAL_IP
992 auto manual_ip = this->ap_.get_manual_ip();
993 if (manual_ip.has_value()) {
994 char static_ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
995 char gateway_buf[network::IP_ADDRESS_BUFFER_SIZE];
996 char subnet_buf[network::IP_ADDRESS_BUFFER_SIZE];
997 ESP_LOGCONFIG(TAG,
998 " AP Static IP: '%s'\n"
999 " AP Gateway: '%s'\n"
1000 " AP Subnet: '%s'",
1001 manual_ip->static_ip.str_to(static_ip_buf), manual_ip->gateway.str_to(gateway_buf),
1002 manual_ip->subnet.str_to(subnet_buf));
1003 }
1004#endif
1005
1006 if (!this->has_sta()) {
1008 }
1009}
1010
1012 this->ap_ = ap;
1013 this->has_ap_ = true;
1014}
1015#endif // USE_WIFI_AP
1016
1017void WiFiComponent::init_sta(size_t count) { this->sta_.init(count); }
1018void WiFiComponent::add_sta(const WiFiAP &ap) { this->sta_.push_back(ap); }
1020 // Clear roaming state - no more configured networks
1021 this->clear_roaming_state_();
1022 this->sta_.clear();
1023 this->selected_sta_index_ = -1;
1024}
1026 this->clear_sta(); // Also clears roaming state
1027 this->init_sta(1);
1028 this->add_sta(ap);
1029 this->selected_sta_index_ = 0;
1030 // When new credentials are set (e.g., from improv), skip cooldown to retry immediately
1031 this->skip_cooldown_next_cycle_ = true;
1032}
1033
1035 const WiFiAP *config = this->get_selected_sta_();
1036 if (config == nullptr) {
1037 ESP_LOGE(TAG, "No valid network config (selected_sta_index_=%d, sta_.size()=%zu)",
1038 static_cast<int>(this->selected_sta_index_), this->sta_.size());
1039 // Return empty params - caller should handle this gracefully
1040 return WiFiAP();
1041 }
1042
1043 WiFiAP params = *config;
1044
1045 switch (this->retry_phase_) {
1047#ifdef USE_WIFI_FAST_CONNECT
1049#endif
1050 // Fast connect phases: use config-only (no scan results)
1051 // BSSID/channel from config if user specified them, otherwise empty
1052 break;
1053
1056 // Hidden network mode: clear BSSID/channel to trigger probe request
1057 // (both explicit hidden and retry hidden use same behavior)
1058 params.clear_bssid();
1059 params.clear_channel();
1060 break;
1061
1063 // Scan-based phase: always use best scan result (index 0 - highest priority after sorting)
1064 if (!this->scan_result_.empty()) {
1065 apply_scan_result_to_params(params, this->scan_result_[0]);
1066 }
1067 break;
1068
1070 // Should not be building params during restart
1071 break;
1072 }
1073
1074 return params;
1075}
1076
1078 const WiFiAP *config = this->get_selected_sta_();
1079 return config ? *config : WiFiAP{};
1080}
1081void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &password) {
1082 this->save_wifi_sta(ssid.c_str(), password.c_str());
1083}
1084void WiFiComponent::save_wifi_sta(const char *ssid, const char *password) {
1085 SavedWifiSettings save{}; // zero-initialized - all bytes set to \0, guaranteeing null termination
1086 strncpy(save.ssid, ssid, sizeof(save.ssid) - 1); // max 32 chars, byte 32 remains \0
1087 strncpy(save.password, password, sizeof(save.password) - 1); // max 64 chars, byte 64 remains \0
1088 this->pref_.save(&save);
1089 // ensure it's written immediately
1091
1092 WiFiAP sta{};
1093 sta.set_ssid(ssid);
1094 sta.set_password(password);
1095 this->set_sta(sta);
1096
1097 // Trigger connection attempt (exits cooldown if needed, no-op if already connecting/connected)
1098 this->connect_soon_();
1099}
1100
1102 // Only trigger retry if we're in cooldown - if already connecting/connected, do nothing
1104 ESP_LOGD(TAG, "Exiting cooldown early due to new WiFi credentials");
1105 this->retry_connect();
1106 }
1107}
1108
1110 // Log connection attempt at INFO level with priority
1111 char bssid_s[18];
1112 int8_t priority = 0;
1113
1114 if (ap.has_bssid()) {
1115 format_mac_addr_upper(ap.get_bssid().data(), bssid_s);
1116 priority = this->get_sta_priority(ap.get_bssid());
1117 }
1118
1119 ESP_LOGI(TAG,
1120 "Connecting to " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") " (priority %d, attempt %u/%u in phase %s)...",
1121 ap.ssid_.c_str(), ap.has_bssid() ? bssid_s : LOG_STR_LITERAL("any"), priority, this->num_retried_ + 1,
1122 get_max_retries_for_phase(this->retry_phase_), LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_)));
1123
1124#ifdef ESPHOME_LOG_HAS_VERBOSE
1125 ESP_LOGV(TAG,
1126 "Connection Params:\n"
1127 " SSID: '%s'",
1128 ap.ssid_.c_str());
1129 if (ap.has_bssid()) {
1130 ESP_LOGV(TAG, " BSSID: %s", bssid_s);
1131 } else {
1132 ESP_LOGV(TAG, " BSSID: Not Set");
1133 }
1134
1135#ifdef USE_WIFI_WPA2_EAP
1136 const auto &eap_opt = ap.get_eap();
1137 if (eap_opt.has_value()) {
1138 const EAPAuth &eap_config = *eap_opt;
1139 // clang-format off
1140 ESP_LOGV(
1141 TAG,
1142 " WPA2 Enterprise authentication configured:\n"
1143 " Identity: " LOG_SECRET("'%s'") "\n"
1144 " Username: " LOG_SECRET("'%s'") "\n"
1145 " Password: " LOG_SECRET("'%s'"),
1146 eap_config.identity.c_str(), eap_config.username.c_str(), eap_config.password.c_str());
1147 // clang-format on
1148#if defined(USE_ESP32) && defined(USE_WIFI_WPA2_EAP) && ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
1149 ESP_LOGV(TAG, " TTLS Phase 2: " LOG_SECRET("'%s'"), eap_phase2_to_str(eap_config.ttls_phase_2));
1150#endif
1151 bool ca_cert_present = eap_config.ca_cert != nullptr && strlen(eap_config.ca_cert);
1152 bool client_cert_present = eap_config.client_cert != nullptr && strlen(eap_config.client_cert);
1153 bool client_key_present = eap_config.client_key != nullptr && strlen(eap_config.client_key);
1154 ESP_LOGV(TAG,
1155 " CA Cert: %s\n"
1156 " Client Cert: %s\n"
1157 " Client Key: %s",
1158 ca_cert_present ? "present" : "not present", client_cert_present ? "present" : "not present",
1159 client_key_present ? "present" : "not present");
1160 } else {
1161#endif
1162 ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), ap.password_.c_str());
1163#ifdef USE_WIFI_WPA2_EAP
1164 }
1165#endif
1166 if (ap.has_channel()) {
1167 ESP_LOGV(TAG, " Channel: %u", ap.get_channel());
1168 } else {
1169 ESP_LOGV(TAG, " Channel not set");
1170 }
1171#ifdef USE_WIFI_MANUAL_IP
1172 auto manual_ip = ap.get_manual_ip();
1173 if (manual_ip.has_value()) {
1174 ManualIP m = *manual_ip;
1175 char static_ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
1176 char gateway_buf[network::IP_ADDRESS_BUFFER_SIZE];
1177 char subnet_buf[network::IP_ADDRESS_BUFFER_SIZE];
1178 char dns1_buf[network::IP_ADDRESS_BUFFER_SIZE];
1179 char dns2_buf[network::IP_ADDRESS_BUFFER_SIZE];
1180 ESP_LOGV(TAG, " Manual IP: Static IP=%s Gateway=%s Subnet=%s DNS1=%s DNS2=%s", m.static_ip.str_to(static_ip_buf),
1181 m.gateway.str_to(gateway_buf), m.subnet.str_to(subnet_buf), m.dns1.str_to(dns1_buf),
1182 m.dns2.str_to(dns2_buf));
1183 } else
1184#endif
1185 {
1186 ESP_LOGV(TAG, " Using DHCP IP");
1187 }
1188 ESP_LOGV(TAG, " Hidden: %s", YESNO(ap.get_hidden()));
1189#endif
1190
1191 // Clear any stale error from previous connection attempt.
1192 // This is the canonical location for clearing the flag since all connection
1193 // attempts go through start_connecting(). The only other clear is in
1194 // restart_adapter() which enters COOLDOWN without calling start_connecting().
1195 this->error_from_callback_ = false;
1196
1197 if (!this->wifi_sta_connect_(ap)) {
1198 ESP_LOGE(TAG, "wifi_sta_connect_ failed");
1199 // Enter cooldown to allow WiFi hardware to stabilize
1200 // (immediate failure suggests hardware not ready, different from connection timeout)
1202 } else {
1204 }
1205 this->action_started_ = millis();
1206}
1207
1208const LogString *get_signal_bars(int8_t rssi) {
1209 // LOWER ONE QUARTER BLOCK
1210 // Unicode: U+2582, UTF-8: E2 96 82
1211 // LOWER HALF BLOCK
1212 // Unicode: U+2584, UTF-8: E2 96 84
1213 // LOWER THREE QUARTERS BLOCK
1214 // Unicode: U+2586, UTF-8: E2 96 86
1215 // FULL BLOCK
1216 // Unicode: U+2588, UTF-8: E2 96 88
1217 if (rssi >= -50) {
1218 return LOG_STR("\033[0;32m" // green
1219 "\xe2\x96\x82"
1220 "\xe2\x96\x84"
1221 "\xe2\x96\x86"
1222 "\xe2\x96\x88"
1223 "\033[0m");
1224 } else if (rssi >= -65) {
1225 return LOG_STR("\033[0;33m" // yellow
1226 "\xe2\x96\x82"
1227 "\xe2\x96\x84"
1228 "\xe2\x96\x86"
1229 "\033[0;37m"
1230 "\xe2\x96\x88"
1231 "\033[0m");
1232 } else if (rssi >= -85) {
1233 return LOG_STR("\033[0;33m" // yellow
1234 "\xe2\x96\x82"
1235 "\xe2\x96\x84"
1236 "\033[0;37m"
1237 "\xe2\x96\x86"
1238 "\xe2\x96\x88"
1239 "\033[0m");
1240 } else {
1241 return LOG_STR("\033[0;31m" // red
1242 "\xe2\x96\x82"
1243 "\033[0;37m"
1244 "\xe2\x96\x84"
1245 "\xe2\x96\x86"
1246 "\xe2\x96\x88"
1247 "\033[0m");
1248 }
1249}
1250
1252 bssid_t bssid = wifi_bssid();
1253 char bssid_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
1254 format_mac_addr_upper(bssid.data(), bssid_s);
1255 // Use stack buffers for IP address formatting to avoid heap allocations
1256 char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
1257 for (auto &ip : wifi_sta_ip_addresses()) {
1258 if (ip.is_set()) {
1259 ESP_LOGCONFIG(TAG, " IP Address: %s", ip.str_to(ip_buf));
1260 }
1261 }
1262 int8_t rssi = wifi_rssi();
1263 // Use stack buffers for SSID and all IP addresses to avoid heap allocations
1264 char ssid_buf[SSID_BUFFER_SIZE];
1265 char subnet_buf[network::IP_ADDRESS_BUFFER_SIZE];
1266 char gateway_buf[network::IP_ADDRESS_BUFFER_SIZE];
1267 char dns1_buf[network::IP_ADDRESS_BUFFER_SIZE];
1268 char dns2_buf[network::IP_ADDRESS_BUFFER_SIZE];
1269 // clang-format off
1270 ESP_LOGCONFIG(TAG,
1271 " SSID: " LOG_SECRET("'%s'") "\n"
1272 " BSSID: " LOG_SECRET("%s") "\n"
1273 " Hostname: '%s'\n"
1274 " Signal strength: %d dB %s\n"
1275 " Channel: %" PRId32 "\n"
1276 " Subnet: %s\n"
1277 " Gateway: %s\n"
1278 " DNS1: %s\n"
1279 " DNS2: %s",
1280 wifi_ssid_to(ssid_buf), bssid_s, App.get_name().c_str(), rssi, LOG_STR_ARG(get_signal_bars(rssi)),
1281 get_wifi_channel(), wifi_subnet_mask_().str_to(subnet_buf), wifi_gateway_ip_().str_to(gateway_buf),
1282 wifi_dns_ip_(0).str_to(dns1_buf), wifi_dns_ip_(1).str_to(dns2_buf));
1283 // clang-format on
1284#ifdef ESPHOME_LOG_HAS_VERBOSE
1285 if (const WiFiAP *config = this->get_selected_sta_(); config && config->has_bssid()) {
1286 ESP_LOGV(TAG, " Priority: %d", this->get_sta_priority(config->get_bssid()));
1287 }
1288#endif
1289#ifdef USE_WIFI_11KV_SUPPORT
1290 ESP_LOGCONFIG(TAG,
1291 " BTM: %s\n"
1292 " RRM: %s",
1293 this->btm_ ? "enabled" : "disabled", this->rrm_ ? "enabled" : "disabled");
1294#endif
1295}
1296
1299 return;
1300
1301 ESP_LOGD(TAG, "Enabling");
1303#ifdef USE_ESP32
1304 // Idempotent — only allocates DMA buffers + netifs on the first call. After this,
1305 // start() can safely run.
1306 this->wifi_lazy_init_();
1307#endif
1308 this->start();
1309}
1310
1313 return;
1314
1315 ESP_LOGD(TAG, "Disabling");
1317 this->wifi_disconnect_();
1318 this->wifi_mode_(false, false);
1319}
1320
1322
1324 this->action_started_ = millis();
1325 ESP_LOGD(TAG, "Starting scan");
1326 this->wifi_scan_start_(this->passive_scan_);
1328}
1329
1363[[nodiscard]] inline static bool wifi_scan_result_is_better(const WiFiScanResult &a, const WiFiScanResult &b) {
1364 // Matching networks always come before non-matching
1365 if (a.get_matches() && !b.get_matches())
1366 return true;
1367 if (!a.get_matches() && b.get_matches())
1368 return false;
1369
1370 // Both matching: check priority first (tracks connection failures via priority degradation)
1371 // Priority is decreased when a BSSID fails to connect, so lower priority = previously failed
1372 if (a.get_matches() && b.get_matches() && a.get_priority() != b.get_priority()) {
1373 return a.get_priority() > b.get_priority();
1374 }
1375
1376 // Use RSSI as tiebreaker (for equal-priority matching networks or all non-matching networks)
1377 return a.get_rssi() > b.get_rssi();
1378}
1379
1380// Helper function for insertion sort of WiFi scan results
1381// Using insertion sort instead of std::stable_sort saves flash memory
1382// by avoiding template instantiations (std::rotate, std::stable_sort, lambdas)
1383// IMPORTANT: This sort is stable (preserves relative order of equal elements)
1384//
1385// Uses raw memcpy instead of copy assignment to avoid CompactString's
1386// destructor/constructor overhead (heap delete[]/new[] for long SSIDs).
1387// Copy assignment calls ~CompactString() then placement-new for every shift,
1388// which means delete[]/new[] per shift for heap-allocated SSIDs. With 70+
1389// networks (e.g., captive portal showing full scan results), this caused
1390// event loop blocking from hundreds of heap operations in a tight loop.
1391//
1392// This is safe because we're permuting elements within the same array —
1393// each slot is overwritten exactly once, so no ownership duplication occurs.
1394// All members of WiFiScanResult are either trivially copyable (bssid, channel,
1395// rssi, priority, flags) or CompactString, which stores either inline data or
1396// a heap pointer — never a self-referential pointer (unlike std::string's SSO
1397// on some implementations). This was not possible before PR#13472 replaced
1398// std::string with CompactString, since std::string's internal layout is
1399// implementation-defined and may use self-referential pointers.
1400//
1401// TODO: If C++ standardizes std::trivially_relocatable, add the assertion for
1402// WiFiScanResult/CompactString here to formally express the memcpy safety guarantee.
1403template<typename VectorType> static void insertion_sort_scan_results(VectorType &results) {
1404 // memcpy-based sort requires no self-referential pointers or virtual dispatch.
1405 // These static_asserts guard the assumptions. If any fire, the memcpy sort
1406 // must be reviewed for safety before updating the expected values.
1407 //
1408 // No vtable pointers (memcpy would corrupt vptr)
1409 static_assert(!std::is_polymorphic<WiFiScanResult>::value, "WiFiScanResult must not have vtable");
1410 static_assert(!std::is_polymorphic<CompactString>::value, "CompactString must not have vtable");
1411 // Standard layout ensures predictable memory layout with no virtual bases
1412 // and no mixed-access-specifier reordering
1413 static_assert(std::is_standard_layout<WiFiScanResult>::value, "WiFiScanResult must be standard layout");
1414 static_assert(std::is_standard_layout<CompactString>::value, "CompactString must be standard layout");
1415 // Size checks catch added/removed fields that may need safety review
1416 static_assert(sizeof(WiFiScanResult) == 32, "WiFiScanResult size changed - verify memcpy sort is still safe");
1417 static_assert(sizeof(CompactString) == 20, "CompactString size changed - verify memcpy sort is still safe");
1418 // Alignment must match for reinterpret_cast of key_buf to be valid
1419 static_assert(alignof(WiFiScanResult) <= alignof(std::max_align_t), "WiFiScanResult alignment exceeds max_align_t");
1420 const size_t size = results.size();
1421 constexpr size_t elem_size = sizeof(WiFiScanResult);
1422 // Suppress warnings for intentional memcpy on non-trivially-copyable type.
1423 // Safety is guaranteed by the static_asserts above and the permutation invariant.
1424 // NOLINTNEXTLINE(bugprone-undefined-memory-manipulation)
1425 auto *memcpy_fn = &memcpy;
1426 for (size_t i = 1; i < size; i++) {
1427 alignas(WiFiScanResult) uint8_t key_buf[elem_size];
1428 memcpy_fn(key_buf, &results[i], elem_size);
1429 const auto &key = *reinterpret_cast<const WiFiScanResult *>(key_buf);
1430 int32_t j = i - 1;
1431
1432 // Move elements that are worse than key to the right
1433 // For stability, we only move if key is strictly better than results[j]
1434 while (j >= 0 && wifi_scan_result_is_better(key, results[j])) {
1435 memcpy_fn(&results[j + 1], &results[j], elem_size);
1436 j--;
1437 }
1438 memcpy_fn(&results[j + 1], key_buf, elem_size);
1439 }
1440}
1441
1442// Helper function to log matching scan results - marked noinline to prevent re-inlining into loop
1443//
1444// IMPORTANT: This function deliberately uses a SINGLE log call to minimize blocking.
1445// In environments with many matching networks (e.g., 18+ mesh APs), multiple log calls
1446// per network would block the main loop for an unacceptable duration. Each log call
1447// has overhead from UART transmission, so combining INFO+DEBUG into one line halves
1448// the blocking time. Do NOT split this into separate ESP_LOGI/ESP_LOGD calls.
1449__attribute__((noinline)) static void log_scan_result(const WiFiScanResult &res) {
1450 char bssid_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
1451 auto bssid = res.get_bssid();
1452 format_mac_addr_upper(bssid.data(), bssid_s);
1453
1454#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
1455 // Single combined log line with all details when DEBUG enabled
1456 ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s Ch:%2u %3ddB P:%d", res.get_ssid().c_str(),
1457 res.get_is_hidden() ? LOG_STR_LITERAL("(HIDDEN) ") : LOG_STR_LITERAL(""), bssid_s,
1458 LOG_STR_ARG(get_signal_bars(res.get_rssi())), res.get_channel(), res.get_rssi(), res.get_priority());
1459#else
1460 ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(),
1461 res.get_is_hidden() ? LOG_STR_LITERAL("(HIDDEN) ") : LOG_STR_LITERAL(""), bssid_s,
1462 LOG_STR_ARG(get_signal_bars(res.get_rssi())));
1463#endif
1464}
1465
1467 if (!this->scan_done_) {
1468 if (millis() - this->action_started_ > WIFI_SCAN_TIMEOUT_MS) {
1469 ESP_LOGE(TAG, "Scan timeout");
1470 this->retry_connect();
1471 }
1472 return;
1473 }
1474 this->scan_done_ = false;
1476 true; // Track that we've done a scan since captive portal started
1478
1479 if (this->scan_result_.empty()) {
1480 ESP_LOGW(TAG, "No networks found");
1481 this->retry_connect();
1482 return;
1483 }
1484
1485 ESP_LOGD(TAG, "Found networks:");
1486 for (auto &res : this->scan_result_) {
1487 for (auto &ap : this->sta_) {
1488 if (res.matches(ap)) {
1489 res.set_matches(true);
1490 // Cache priority lookup - do single search instead of 2 separate searches
1491 const bssid_t &bssid = res.get_bssid();
1492 if (!this->has_sta_priority(bssid)) {
1493 this->set_sta_priority(bssid, ap.get_priority());
1494 }
1495 res.set_priority(this->get_sta_priority(bssid));
1496 break;
1497 }
1498 }
1499 }
1500
1501 // Sort scan results using insertion sort for better memory efficiency
1502 insertion_sort_scan_results(this->scan_result_);
1503
1504 // Log matching networks (non-matching already logged at VERBOSE in scan callback)
1505 for (auto &res : this->scan_result_) {
1506 if (res.get_matches()) {
1507 log_scan_result(res);
1508 }
1509 }
1510
1511 // SYNCHRONIZATION POINT: Establish link between scan_result_[0] and selected_sta_index_
1512 // After sorting, scan_result_[0] contains the best network. Now find which sta_[i] config
1513 // matches that network and record it in selected_sta_index_. This keeps the two indices
1514 // synchronized so build_params_for_current_phase_() can safely use both to build connection parameters.
1515 const WiFiScanResult &scan_res = this->scan_result_[0];
1516 bool found_match = false;
1517 if (scan_res.get_matches()) {
1518 for (size_t i = 0; i < this->sta_.size(); i++) {
1519 if (scan_res.matches(this->sta_[i])) {
1520 // Safe cast: sta_.size() limited to MAX_WIFI_NETWORKS (127) in __init__.py validation
1521 // No overflow check needed - YAML validation prevents >127 networks
1522 this->selected_sta_index_ = static_cast<int8_t>(i); // Links scan_result_[0] with sta_[i]
1523 found_match = true;
1524 break;
1525 }
1526 }
1527 }
1528
1529 if (!found_match) {
1530 ESP_LOGW(TAG, "No matching network found");
1531 // No scan results matched our configured networks - transition directly to hidden mode
1532 // Don't call retry_connect() since we never attempted a connection (no BSSID to penalize)
1534 // If no hidden networks to try, skip connection attempt (will be handled on next loop)
1535 if (this->selected_sta_index_ == -1) {
1536 return;
1537 }
1538 // Now start connection attempt in hidden mode
1540 return; // scan started, wait for next loop iteration
1541 }
1542
1543 yield();
1544
1545 WiFiAP params = this->build_params_for_current_phase_();
1546 // Ensure we're in SCAN_CONNECTING phase when connecting with scan results
1547 // (needed when scan was started directly without transition_to_phase_, e.g., initial scan)
1548 this->start_connecting(params);
1549}
1550
1552 char mac_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
1553 ESP_LOGCONFIG(TAG,
1554 "WiFi:\n"
1555 " Local MAC: %s\n"
1556 " Connected: %s",
1557 get_mac_address_pretty_into_buffer(mac_s), YESNO(this->is_connected()));
1558 if (this->is_disabled()) {
1559 ESP_LOGCONFIG(TAG, " Disabled");
1560 return;
1561 }
1562#if defined(USE_ESP32) && defined(SOC_WIFI_SUPPORT_5G)
1563 const char *band_mode_s;
1564 switch (this->band_mode_) {
1565 case WIFI_BAND_MODE_2G_ONLY:
1566 band_mode_s = "2.4GHz";
1567 break;
1568 case WIFI_BAND_MODE_5G_ONLY:
1569 band_mode_s = "5GHz";
1570 break;
1571 case WIFI_BAND_MODE_AUTO:
1572 default:
1573 band_mode_s = "Auto";
1574 break;
1575 }
1576 ESP_LOGCONFIG(TAG, " Band Mode: %s", band_mode_s);
1577#endif
1578#ifdef USE_WIFI_PHY_MODE
1579 ESP_LOGCONFIG(TAG, " PHY Mode: %s", LOG_STR_ARG(phy_mode_to_log_string(this->phy_mode_)));
1580#endif
1581 if (this->is_connected()) {
1582 this->print_connect_params_();
1583 }
1584}
1585
1587 auto status = this->wifi_sta_connect_status_();
1588
1590 char ssid_buf[SSID_BUFFER_SIZE];
1591 if (wifi_ssid_to(ssid_buf)[0] == '\0') {
1592 ESP_LOGW(TAG, "Connection incomplete");
1593 this->retry_connect();
1594 return;
1595 }
1596
1597 ESP_LOGI(TAG, "Connected");
1598 // Warn if we had to retry with hidden network mode for a network that's not marked hidden
1599 // Only warn if we actually connected without scan data (SSID only), not if scan succeeded on retry
1600 if (const WiFiAP *config = this->get_selected_sta_(); this->retry_phase_ == WiFiRetryPhase::RETRY_HIDDEN &&
1601 config && !config->get_hidden() &&
1602 this->scan_result_.empty()) {
1603 ESP_LOGW(TAG, LOG_SECRET("'%s'") " should be marked hidden", config->ssid_.c_str());
1604 }
1605 // Reset to initial phase on successful connection (don't log transition, just reset state)
1607 this->num_retried_ = 0;
1608 if (this->has_ap()) {
1609#ifdef USE_CAPTIVE_PORTAL
1610 if (this->is_captive_portal_active_()) {
1612 }
1613#endif
1614 ESP_LOGD(TAG, "Disabling AP");
1615 this->wifi_mode_({}, false);
1616 }
1617#ifdef USE_IMPROV
1618 if (this->is_esp32_improv_active_()) {
1620 }
1621#endif
1622
1624 // Refresh is_connected() cache; loop()'s refresh ran before this transition.
1626 this->num_retried_ = 0;
1627 this->print_connect_params_();
1628
1629 // Reset roaming state on successful connection
1630 this->roaming_last_check_ = now;
1631 // Only preserve attempts if reconnecting after a failed roam attempt
1632 // This prevents ping-pong between APs when a roam target is unreachable
1634 // Successful roam to better AP on first try - reset attempts so we can roam again later
1635 ESP_LOGD(TAG, "Roam successful");
1636 this->roaming_attempts_ = 0;
1637 } else if (this->roaming_state_ == RoamingState::RECONNECTING) {
1638 // Check if we ended up on the roam target despite needing a retry
1639 // (e.g., first connect failed but scan-based retry found and connected to the same better AP)
1640 bssid_t current_bssid = this->wifi_bssid();
1641 if (this->roaming_target_bssid_ != bssid_t{} && current_bssid == this->roaming_target_bssid_) {
1642 char bssid_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
1643 format_mac_addr_upper(current_bssid.data(), bssid_buf);
1644 ESP_LOGD(TAG, "Roam successful (via retry, attempt %u/%u) to %s", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS,
1645 bssid_buf);
1646 this->roaming_attempts_ = 0;
1647 } else if (this->roaming_target_bssid_ != bssid_t{}) {
1648 // Failed roam to specific target, reconnected to different AP - keep attempts to prevent ping-pong
1649 ESP_LOGD(TAG, "Reconnected after failed roam (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS);
1650 } else {
1651 // Reconnected after scan-induced disconnect (no roam target) - keep attempts
1652 ESP_LOGD(TAG, "Reconnected after roam scan (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS);
1653 }
1654 } else {
1655 // Normal connection (boot, credentials changed, etc.)
1656 this->roaming_attempts_ = 0;
1657 }
1659 this->roaming_target_bssid_ = {};
1660 this->roaming_scan_end_ = 0;
1661
1662 // Clear all priority penalties - the next reconnect will happen when an AP disconnects,
1663 // which means the landscape has likely changed and previous tracked failures are stale
1665
1666#ifdef USE_WIFI_FAST_CONNECT
1668#endif
1669
1670 this->release_scan_results_();
1671
1672#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
1673 // Notify listeners now that state machine has reached STA_CONNECTED
1674 // This ensures wifi.connected condition returns true in listener automations
1676#endif
1677
1678#if defined(USE_ESP8266) && defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP)
1679 // On ESP8266, GOT_IP event may not fire for static IP configurations,
1680 // so notify IP state listeners here as a fallback.
1681 if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) {
1683 }
1684#endif
1685
1686 return;
1687 }
1688
1689 if (now - this->action_started_ > WIFI_CONNECT_TIMEOUT_MS) {
1690 ESP_LOGW(TAG, "Connection timeout, aborting connection attempt");
1691 this->wifi_disconnect_();
1692 this->retry_connect();
1693 return;
1694 }
1695
1696 if (this->error_from_callback_) {
1697 // ESP8266: logging done in callback, listeners deferred via pending_.disconnect
1698 // Other platforms: just log generic failure message
1699#ifndef USE_ESP8266
1700 ESP_LOGW(TAG, "Connecting to network failed (callback)");
1701#endif
1702 this->retry_connect();
1703 return;
1704 }
1705
1707 return;
1708 }
1709
1711 ESP_LOGW(TAG, "Network no longer found");
1712 this->retry_connect();
1713 return;
1714 }
1715
1717 ESP_LOGW(TAG, "Connecting to network failed");
1718 this->retry_connect();
1719 return;
1720 }
1721
1722 ESP_LOGW(TAG, "Unknown connection status %d", (int) status);
1723 this->retry_connect();
1724}
1725
1733 switch (this->retry_phase_) {
1735#ifdef USE_WIFI_FAST_CONNECT
1737 // INITIAL_CONNECT and FAST_CONNECT_CYCLING_APS: no retries, try next AP or fall back to scan
1738 if (this->selected_sta_index_ < static_cast<int8_t>(this->sta_.size()) - 1) {
1739 return WiFiRetryPhase::FAST_CONNECT_CYCLING_APS; // Move to next AP
1740 }
1741#endif
1742 // Check if we should try explicit hidden networks before scanning
1743 // This handles reconnection after connection loss where first network is hidden
1744 if (!this->sta_.empty() && this->sta_[0].get_hidden()) {
1746 }
1747 // No more APs to try, fall back to scan
1749
1751 // Try all explicitly hidden networks before scanning
1752 if (this->num_retried_ + 1 < WIFI_RETRY_COUNT_PER_SSID) {
1753 return WiFiRetryPhase::EXPLICIT_HIDDEN; // Keep retrying same SSID
1754 }
1755
1756 // Exhausted retries on current SSID - check for more explicitly hidden networks
1757 // Stop when we reach a visible network (proceed to scanning)
1758 size_t next_index = this->selected_sta_index_ + 1;
1759 if (next_index < this->sta_.size() && this->sta_[next_index].get_hidden()) {
1760 // Found another explicitly hidden network
1762 }
1763
1764 // No more consecutive explicitly hidden networks
1765 // If ALL networks are hidden, skip scanning and go directly to restart
1766 if (this->find_first_non_hidden_index_() < 0) {
1768 }
1769 // Otherwise proceed to scanning for non-hidden networks
1771 }
1772
1774 // If scan found no networks or no matching networks, skip to hidden network mode
1775 if (this->scan_result_.empty() || !this->scan_result_[0].get_matches()) {
1777 }
1778
1779 if (this->num_retried_ + 1 < WIFI_RETRY_COUNT_PER_BSSID) {
1780 return WiFiRetryPhase::SCAN_CONNECTING; // Keep retrying same BSSID
1781 }
1782
1783 // Exhausted retries on current BSSID (scan_result_[0])
1784 // Its priority has been decreased, so on next scan it will be sorted lower
1785 // and we'll try the next best BSSID.
1786 // Check if there are any potentially hidden networks to try
1787 if (this->find_next_hidden_sta_(-1) >= 0) {
1788 return WiFiRetryPhase::RETRY_HIDDEN; // Found hidden networks to try
1789 }
1790 // No hidden networks - always go through RESTARTING_ADAPTER phase
1791 // This ensures num_retried_ gets reset and a fresh scan is triggered
1792 // The actual adapter restart will be skipped if captive portal/improv is active
1794
1796 // If no hidden SSIDs to try (selected_sta_index_ == -1), skip directly to rescan
1797 if (this->selected_sta_index_ >= 0) {
1798 if (this->num_retried_ + 1 < WIFI_RETRY_COUNT_PER_SSID) {
1799 return WiFiRetryPhase::RETRY_HIDDEN; // Keep retrying same SSID
1800 }
1801
1802 // Exhausted retries on current SSID - check if there are more potentially hidden SSIDs to try
1803 if (this->selected_sta_index_ < static_cast<int8_t>(this->sta_.size()) - 1) {
1804 // Check if find_next_hidden_sta_() would actually find another hidden SSID
1805 // as it might have been seen in the scan results and we want to skip those
1806 // otherwise we will get stuck in RETRY_HIDDEN phase
1807 if (this->find_next_hidden_sta_(this->selected_sta_index_) != -1) {
1808 // More hidden SSIDs available - stay in RETRY_HIDDEN, advance will happen in retry_connect()
1810 }
1811 }
1812 }
1813 // Exhausted all potentially hidden SSIDs - always go through RESTARTING_ADAPTER
1814 // This ensures num_retried_ gets reset and a fresh scan is triggered
1815 // The actual adapter restart will be skipped if captive portal/improv is active
1817
1819 // After restart, go back to explicit hidden if we went through it initially
1822 }
1823 // Skip scanning when captive portal/improv is active to avoid disrupting AP,
1824 // BUT only if we've already completed at least one scan AFTER the portal started.
1825 // When captive portal first starts, scan results may be filtered/stale, so we need
1826 // to do one full scan to populate available networks for the captive portal UI.
1827 //
1828 // WHY SCANNING DISRUPTS AP MODE:
1829 // WiFi scanning requires the radio to leave the AP's channel and hop through
1830 // other channels to listen for beacons. During this time (even for passive scans),
1831 // the AP cannot service connected clients - they experience disconnections or
1832 // timeouts. On ESP32, even passive scans cause brief but noticeable disruptions
1833 // that break captive portal HTTP requests and DNS lookups.
1834 //
1835 // BLIND RETRY MODE:
1836 // When captive portal/improv is active, we use RETRY_HIDDEN as a "try all networks
1837 // blindly" mode. Since retry_hidden_mode_ is set to BLIND_RETRY (in RESTARTING_ADAPTER
1838 // transition), find_next_hidden_sta_() will treat ALL configured networks as
1839 // candidates, cycling through them without requiring scan results.
1840 //
1841 // This allows users to configure WiFi via captive portal while the device keeps
1842 // attempting to connect to all configured networks in sequence.
1843 // Captive portal needs scan results to show available networks.
1844 // If captive portal is active, only skip scanning if we've done a scan after it started.
1845 // If only improv is active (no captive portal), skip scanning since improv doesn't need results.
1846 if (this->is_captive_portal_active_()) {
1849 }
1850 // Need to scan for captive portal
1851 } else if (this->is_esp32_improv_active_()) {
1852 // Improv doesn't need scan results
1854 }
1856 }
1857
1858 // Should never reach here
1860}
1861
1872 WiFiRetryPhase old_phase = this->retry_phase_;
1873
1874 // No-op if staying in same phase
1875 if (old_phase == new_phase) {
1876 return false;
1877 }
1878
1879 ESP_LOGD(TAG, "Retry phase: %s → %s", LOG_STR_ARG(retry_phase_to_log_string(old_phase)),
1880 LOG_STR_ARG(retry_phase_to_log_string(new_phase)));
1881
1882 this->retry_phase_ = new_phase;
1883 this->num_retried_ = 0; // Reset retry counter on phase change
1884
1885 // Phase-specific setup
1886 switch (new_phase) {
1887#ifdef USE_WIFI_FAST_CONNECT
1889 // Move to next configured AP - clear old scan data so new AP is tried with config only
1890 this->selected_sta_index_++;
1891 this->scan_result_.clear();
1892 break;
1893#endif
1894
1896 // Starting explicit hidden phase - reset to first network
1897 this->selected_sta_index_ = 0;
1898 break;
1899
1901 // Transitioning to scan-based connection
1902#ifdef USE_WIFI_FAST_CONNECT
1904 ESP_LOGI(TAG, "Fast connect exhausted, falling back to scan");
1905 }
1906#endif
1907 // Trigger scan if we don't have scan results OR if transitioning from phases that need fresh scan
1908 if (this->scan_result_.empty() || old_phase == WiFiRetryPhase::EXPLICIT_HIDDEN ||
1910 this->selected_sta_index_ = -1; // Will be set after scan completes
1911 this->start_scanning();
1912 return true; // Started scan, wait for completion
1913 }
1914 // Already have scan results - selected_sta_index_ should already be synchronized
1915 // (set in check_scanning_finished() when scan completed)
1916 // No need to reset it here
1917 break;
1918
1920 // Always reset to first candidate when entering this phase.
1921 // This phase can be entered from:
1922 // - SCAN_CONNECTING: normal flow, find_next_hidden_sta_() skips networks visible in scan
1923 // - RESTARTING_ADAPTER: captive portal active, find_next_hidden_sta_() tries ALL networks
1924 //
1925 // The retry_hidden_mode_ controls the behavior:
1926 // - SCAN_BASED: scan_result_ is checked, visible networks are skipped
1927 // - BLIND_RETRY: scan_result_ is ignored, all networks become candidates
1928 // We don't clear scan_result_ here - the mode controls whether it's consulted.
1930
1931 if (this->selected_sta_index_ == -1) {
1932 ESP_LOGD(TAG, "All SSIDs visible or already tried, skipping hidden mode");
1933 }
1934 break;
1935
1937 // Skip actual adapter restart if captive portal/improv is active
1938 // This allows state machine to reset num_retried_ and trigger fresh scan
1939 // without disrupting the captive portal/improv connection
1940 if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_()) {
1941 this->restart_adapter();
1942 } else {
1943 // Even when skipping full restart, disconnect to clear driver state
1944 // Without this, platforms like LibreTiny may think we're still connecting
1945 this->wifi_disconnect_();
1946 }
1947 // Clear scan flag - we're starting a new retry cycle
1948 // This is critical for captive portal/improv flow: when determine_next_phase_()
1949 // returns RETRY_HIDDEN (because scanning is skipped), find_next_hidden_sta_()
1950 // will see BLIND_RETRY mode and treat ALL networks as candidates,
1951 // effectively cycling through all configured networks without scan results.
1953 // Always enter cooldown after restart (or skip-restart) to allow stabilization
1954 // Use extended cooldown when AP is active to avoid constant scanning that blocks DNS
1956 this->action_started_ = millis();
1957 // Return true to indicate we should wait (go to COOLDOWN) instead of immediately connecting
1958 return true;
1959
1960 default:
1961 break;
1962 }
1963
1964 return false; // Did not start scan, can proceed with connection
1965}
1966
1968 if (!this->sta_priorities_.empty()) {
1969 decltype(this->sta_priorities_)().swap(this->sta_priorities_);
1970 }
1971}
1972
1977 if (this->sta_priorities_.empty()) {
1978 return;
1979 }
1980
1981 int8_t first_priority = this->sta_priorities_[0].priority;
1982
1983 // Only clear if all priorities have been decremented to the minimum value
1984 // At this point, all BSSIDs have been equally penalized and priority info is useless
1985 if (first_priority != std::numeric_limits<int8_t>::min()) {
1986 return;
1987 }
1988
1989 for (const auto &pri : this->sta_priorities_) {
1990 if (pri.priority != first_priority) {
1991 return; // Not all same, nothing to do
1992 }
1993 }
1994
1995 // All priorities are at minimum - clear the vector to save memory and reset
1996 ESP_LOGD(TAG, "Clearing BSSID priorities (all at minimum)");
1998}
1999
2019 // Determine which BSSID we tried to connect to
2020 optional<bssid_t> failed_bssid;
2021
2022 if (this->retry_phase_ == WiFiRetryPhase::SCAN_CONNECTING && !this->scan_result_.empty()) {
2023 // Scan-based phase: always use best result (index 0)
2024 failed_bssid = this->scan_result_[0].get_bssid();
2025 } else if (const WiFiAP *config = this->get_selected_sta_(); config && config->has_bssid()) {
2026 // Config has specific BSSID (fast_connect or user-specified)
2027 failed_bssid = config->get_bssid();
2028 }
2029
2030 if (!failed_bssid.has_value()) {
2031 return; // No BSSID to penalize
2032 }
2033
2034 // Get SSID for logging (use pointer to avoid copy)
2035 const char *ssid = nullptr;
2036 if (this->retry_phase_ == WiFiRetryPhase::SCAN_CONNECTING && !this->scan_result_.empty()) {
2037 ssid = this->scan_result_[0].ssid_.c_str();
2038 } else if (const WiFiAP *config = this->get_selected_sta_()) {
2039 ssid = config->ssid_.c_str();
2040 }
2041
2042 // Only decrease priority on the last attempt for this phase
2043 // This prevents false positives from transient WiFi stack issues
2044 uint8_t max_retries = get_max_retries_for_phase(this->retry_phase_);
2045 bool is_last_attempt = (this->num_retried_ + 1 >= max_retries);
2046
2047 // Decrease priority only on last attempt to avoid false positives from transient failures
2048 int8_t old_priority = this->get_sta_priority(failed_bssid.value());
2049 int8_t new_priority = old_priority;
2050
2051 if (is_last_attempt) {
2052 // Decrease priority, but clamp to int8_t::min to prevent overflow
2053 new_priority =
2054 (old_priority > std::numeric_limits<int8_t>::min()) ? (old_priority - 1) : std::numeric_limits<int8_t>::min();
2055 this->set_sta_priority(failed_bssid.value(), new_priority);
2056 }
2057 char bssid_s[18];
2058 format_mac_addr_upper(failed_bssid.value().data(), bssid_s);
2059 ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d", ssid != nullptr ? ssid : "",
2060 bssid_s, old_priority, new_priority);
2061
2062 // After adjusting priority, check if all priorities are now at minimum
2063 // If so, clear the vector to save memory and reset for fresh start
2065}
2066
2078 WiFiRetryPhase current_phase = this->retry_phase_;
2079
2080 // Check if we need to advance to next AP/SSID within the same phase
2081#ifdef USE_WIFI_FAST_CONNECT
2082 if (current_phase == WiFiRetryPhase::FAST_CONNECT_CYCLING_APS) {
2083 // Fast connect: always advance to next AP (no retries per AP)
2084 this->selected_sta_index_++;
2085 this->num_retried_ = 0;
2086 ESP_LOGD(TAG, "Next AP in %s", LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_)));
2087 return;
2088 }
2089#endif
2090
2091 if (current_phase == WiFiRetryPhase::EXPLICIT_HIDDEN && this->num_retried_ + 1 >= WIFI_RETRY_COUNT_PER_SSID) {
2092 // Explicit hidden: exhausted retries on current SSID, find next explicitly hidden network
2093 // Stop when we reach a visible network (proceed to scanning)
2094 size_t next_index = this->selected_sta_index_ + 1;
2095 if (next_index < this->sta_.size() && this->sta_[next_index].get_hidden()) {
2096 this->selected_sta_index_ = static_cast<int8_t>(next_index);
2097 this->num_retried_ = 0;
2098 ESP_LOGD(TAG, "Next explicit hidden network at index %d", static_cast<int>(next_index));
2099 return;
2100 }
2101 // No more consecutive explicit hidden networks found - fall through to trigger phase change
2102 }
2103
2104 if (current_phase == WiFiRetryPhase::RETRY_HIDDEN && this->num_retried_ + 1 >= WIFI_RETRY_COUNT_PER_SSID) {
2105 // Hidden mode: exhausted retries on current SSID, find next potentially hidden SSID
2106 // If first network is marked hidden, we went through EXPLICIT_HIDDEN phase
2107 // In that case, skip networks marked hidden:true (already tried)
2108 // Otherwise, include them (they haven't been tried yet)
2109 int8_t next_index = this->find_next_hidden_sta_(this->selected_sta_index_);
2110 if (next_index != -1) {
2111 // Found another potentially hidden SSID
2112 this->selected_sta_index_ = next_index;
2113 this->num_retried_ = 0;
2114 return;
2115 }
2116 // No more potentially hidden SSIDs - set selected_sta_index_ to -1 to trigger phase change
2117 // This ensures determine_next_phase_() will skip the RETRY_HIDDEN logic and transition out
2118 this->selected_sta_index_ = -1;
2119 // Return early - phase change will happen on next wifi_loop() iteration
2120 return;
2121 }
2122
2123 // Don't increment retry counter if we're in a scan phase with no valid targets
2124 if (this->needs_scan_results_()) {
2125 return;
2126 }
2127
2128 // Increment retry counter to try the same target again
2129 this->num_retried_++;
2130 ESP_LOGD(TAG, "Retry attempt %u/%u in phase %s", this->num_retried_ + 1,
2131 get_max_retries_for_phase(this->retry_phase_), LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_)));
2132}
2133
2135 // Handle roaming state transitions - preserve attempts counter to prevent ping-pong
2136 // to unreachable APs after ROAMING_MAX_ATTEMPTS failures
2138 // Roam connection failed - transition to reconnecting
2139 ESP_LOGD(TAG, "Roam failed, reconnecting (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS);
2141 } else if (this->roaming_state_ == RoamingState::SCANNING) {
2142 // Disconnected during roam scan - transition to RECONNECTING so the attempts
2143 // counter is preserved when reconnection succeeds (IDLE would reset it)
2144 ESP_LOGD(TAG, "Disconnected during roam scan (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS);
2146 } else if (this->roaming_state_ == RoamingState::IDLE) {
2147 // Check if a roaming scan recently completed - on ESP8266, going off-channel
2148 // during scan can cause a delayed Beacon Timeout 8-20 seconds after scan finishes.
2149 // Transition to RECONNECTING so the attempts counter is preserved on reconnect.
2151 ESP_LOGD(TAG, "Disconnect after roam scan (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS);
2153 } else {
2154 // Not a roaming-triggered reconnect, reset state
2155 this->clear_roaming_state_();
2156 }
2157 }
2158 // RECONNECTING: keep state and counter, still trying to reconnect
2159
2161
2162 // Determine next retry phase based on current state
2163 WiFiRetryPhase current_phase = this->retry_phase_;
2164 WiFiRetryPhase next_phase = this->determine_next_phase_();
2165
2166 // Handle phase transitions (transition_to_phase_ handles same-phase no-op internally)
2167 if (this->transition_to_phase_(next_phase)) {
2168 return; // Scan started or adapter restarted (which sets its own state)
2169 }
2170
2171 if (next_phase == current_phase) {
2173 }
2174
2175 yield();
2176 // Check if we have a valid target before building params
2177 // After exhausting all networks in a phase, selected_sta_index_ may be -1
2178 // In that case, skip connection and let next wifi_loop() handle phase transition
2179 if (this->selected_sta_index_ >= 0) {
2180 WiFiAP params = this->build_params_for_current_phase_();
2181 this->start_connecting(params);
2182 }
2183}
2184
2185void WiFiComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
2187 this->power_save_ = power_save;
2188#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
2189 this->configured_power_save_ = power_save;
2190#endif
2191}
2192
2193void WiFiComponent::set_passive_scan(bool passive) { this->passive_scan_ = passive; }
2194
2196#ifdef USE_CAPTIVE_PORTAL
2198#else
2199 return false;
2200#endif
2201}
2203#ifdef USE_IMPROV
2205#else
2206 return false;
2207#endif
2208}
2209
2210#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
2212 // Already configured for high performance - request satisfied
2214 return true;
2215 }
2216
2217 // Semaphore initialization failed
2218 if (this->high_performance_semaphore_ == nullptr) {
2219 return false;
2220 }
2221
2222 // Give the semaphore (non-blocking). This increments the count.
2223 bool success = xSemaphoreGive(this->high_performance_semaphore_) == pdTRUE;
2224
2225 // Wake the main loop so the switch to high-performance mode is applied on the
2226 // next tick instead of waiting up to loop_interval.
2227 if (success) {
2229 }
2230
2231 return success;
2232}
2233
2235 // Already configured for high performance - nothing to release
2237 return true;
2238 }
2239
2240 // Semaphore initialization failed
2241 if (this->high_performance_semaphore_ == nullptr) {
2242 return false;
2243 }
2244
2245 // Take the semaphore (non-blocking). This decrements the count.
2246 return xSemaphoreTake(this->high_performance_semaphore_, 0) == pdTRUE;
2247}
2248#endif // USE_ESP32 && USE_WIFI_RUNTIME_POWER_SAVE
2249
2250#ifdef USE_WIFI_FAST_CONNECT
2252 SavedWifiFastConnectSettings fast_connect_save{};
2253
2254 if (this->fast_connect_pref_.load(&fast_connect_save)) {
2255 // Validate saved AP index
2256 if (fast_connect_save.ap_index < 0 || static_cast<size_t>(fast_connect_save.ap_index) >= this->sta_.size()) {
2257 ESP_LOGW(TAG, "AP index out of bounds");
2258 return false;
2259 }
2260
2261 // Set selected index for future operations (save, retry, etc)
2262 this->selected_sta_index_ = fast_connect_save.ap_index;
2263
2264 // Copy entire config, then override with fast connect data
2265 params = this->sta_[fast_connect_save.ap_index];
2266
2267 // Override with saved BSSID/channel from fast connect (SSID/password/etc already copied from config)
2268 bssid_t bssid{};
2269 std::copy(fast_connect_save.bssid, fast_connect_save.bssid + 6, bssid.begin());
2270 params.set_bssid(bssid);
2271 params.set_channel(fast_connect_save.channel);
2272 // Fast connect uses specific BSSID+channel, not hidden network probe (even if config has hidden: true)
2273 params.set_hidden(false);
2274
2275 ESP_LOGD(TAG, "Loaded fast_connect settings");
2276#if defined(USE_ESP32) && defined(SOC_WIFI_SUPPORT_5G)
2277 if ((this->band_mode_ == WIFI_BAND_MODE_5G_ONLY && fast_connect_save.channel < FIRST_5GHZ_CHANNEL) ||
2278 (this->band_mode_ == WIFI_BAND_MODE_2G_ONLY && fast_connect_save.channel >= FIRST_5GHZ_CHANNEL)) {
2279 ESP_LOGW(TAG, "Saved channel %u not allowed by band mode, ignoring fast_connect", fast_connect_save.channel);
2280 this->selected_sta_index_ = -1;
2281 return false;
2282 }
2283#endif
2284 return true;
2285 }
2286
2287 return false;
2288}
2289
2291 bssid_t bssid = wifi_bssid();
2292 uint8_t channel = get_wifi_channel();
2293 // selected_sta_index_ is always valid here (called only after successful connection)
2294 // Fallback to 0 is defensive programming for robustness
2295 int8_t ap_index = this->selected_sta_index_ >= 0 ? this->selected_sta_index_ : 0;
2296
2297 // Skip save if settings haven't changed (compare with previously saved settings to reduce flash wear)
2298 SavedWifiFastConnectSettings previous_save{};
2299 if (this->fast_connect_pref_.load(&previous_save) && memcmp(previous_save.bssid, bssid.data(), 6) == 0 &&
2300 previous_save.channel == channel && previous_save.ap_index == ap_index) {
2301 return; // No change, nothing to save
2302 }
2303
2304 SavedWifiFastConnectSettings fast_connect_save{};
2305 memcpy(fast_connect_save.bssid, bssid.data(), 6);
2306 fast_connect_save.channel = channel;
2307 fast_connect_save.ap_index = ap_index;
2308
2309 this->fast_connect_pref_.save(&fast_connect_save);
2310
2311 ESP_LOGD(TAG, "Saved fast_connect settings");
2312}
2313#endif
2314
2315void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = CompactString(ssid.c_str(), ssid.size()); }
2316void WiFiAP::set_ssid(const char *ssid) { this->ssid_ = CompactString(ssid, strlen(ssid)); }
2317void WiFiAP::set_bssid(const bssid_t &bssid) { this->bssid_ = bssid; }
2318void WiFiAP::clear_bssid() { this->bssid_ = {}; }
2319void WiFiAP::set_password(const std::string &password) {
2320 this->password_ = CompactString(password.c_str(), password.size());
2321}
2322void WiFiAP::set_password(const char *password) { this->password_ = CompactString(password, strlen(password)); }
2323#ifdef USE_WIFI_WPA2_EAP
2324void WiFiAP::set_eap(optional<EAPAuth> eap_auth) { this->eap_ = std::move(eap_auth); }
2325#endif
2326void WiFiAP::set_channel(uint8_t channel) { this->channel_ = channel; }
2327void WiFiAP::clear_channel() { this->channel_ = 0; }
2328#ifdef USE_WIFI_MANUAL_IP
2329void WiFiAP::set_manual_ip(optional<ManualIP> manual_ip) { this->manual_ip_ = manual_ip; }
2330#endif
2331void WiFiAP::set_hidden(bool hidden) { this->hidden_ = hidden; }
2332const bssid_t &WiFiAP::get_bssid() const { return this->bssid_; }
2333bool WiFiAP::has_bssid() const { return this->bssid_ != bssid_t{}; }
2334#ifdef USE_WIFI_WPA2_EAP
2335const optional<EAPAuth> &WiFiAP::get_eap() const { return this->eap_; }
2336#endif
2337#ifdef USE_WIFI_MANUAL_IP
2338const optional<ManualIP> &WiFiAP::get_manual_ip() const { return this->manual_ip_; }
2339#endif
2340bool WiFiAP::get_hidden() const { return this->hidden_; }
2341
2342WiFiScanResult::WiFiScanResult(const bssid_t &bssid, const char *ssid, size_t ssid_len, uint8_t channel, int8_t rssi,
2343 bool with_auth, bool is_hidden)
2344 : bssid_(bssid),
2345 channel_(channel),
2346 rssi_(rssi),
2347 ssid_(ssid, ssid_len),
2348 with_auth_(with_auth),
2349 is_hidden_(is_hidden) {}
2350bool WiFiScanResult::matches(const WiFiAP &config) const {
2351 if (config.get_hidden()) {
2352 // User configured a hidden network, only match actually hidden networks
2353 // don't match SSID
2354 if (!this->is_hidden_)
2355 return false;
2356 } else if (!config.ssid_.empty()) {
2357 // check if SSID matches
2358 if (this->ssid_ != config.ssid_)
2359 return false;
2360 } else {
2361 // network is configured without SSID - match other settings
2362 }
2363 // If BSSID configured, only match for correct BSSIDs
2364 if (config.has_bssid() && config.get_bssid() != this->bssid_)
2365 return false;
2366
2367#ifdef USE_WIFI_WPA2_EAP
2368 // BSSID requires auth but no PSK or EAP credentials given
2369 if (this->with_auth_ && (config.password_.empty() && !config.get_eap().has_value()))
2370 return false;
2371
2372 // BSSID does not require auth, but PSK or EAP credentials given
2373 if (!this->with_auth_ && (!config.password_.empty() || config.get_eap().has_value()))
2374 return false;
2375#else
2376 // If PSK given, only match for networks with auth (and vice versa)
2377 if (config.password_.empty() == this->with_auth_)
2378 return false;
2379#endif
2380
2381 // If channel configured, only match networks on that channel.
2382 if (config.has_channel() && config.get_channel() != this->channel_) {
2383 return false;
2384 }
2385 return true;
2386}
2387bool WiFiScanResult::get_matches() const { return this->matches_; }
2388void WiFiScanResult::set_matches(bool matches) { this->matches_ = matches; }
2389const bssid_t &WiFiScanResult::get_bssid() const { return this->bssid_; }
2390uint8_t WiFiScanResult::get_channel() const { return this->channel_; }
2391int8_t WiFiScanResult::get_rssi() const { return this->rssi_; }
2392bool WiFiScanResult::get_with_auth() const { return this->with_auth_; }
2393bool WiFiScanResult::get_is_hidden() const { return this->is_hidden_; }
2394
2395bool WiFiScanResult::operator==(const WiFiScanResult &rhs) const { return this->bssid_ == rhs.bssid_; }
2396
2398 this->roaming_attempts_ = 0;
2399 this->roaming_last_check_ = 0;
2400 this->roaming_scan_end_ = 0;
2401 this->roaming_target_bssid_ = {};
2403}
2404
2406 if (!this->keep_scan_results_) {
2407#if defined(USE_RP2) || defined(USE_ESP32)
2408 // std::vector - use swap trick since shrink_to_fit is non-binding
2409 decltype(this->scan_result_)().swap(this->scan_result_);
2410#else
2411 // FixedVector::release() frees all memory
2412 this->scan_result_.release();
2413#endif
2414 }
2415}
2416
2417#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
2419 if (!this->pending_.connect_state)
2420 return;
2421 this->pending_.connect_state = false;
2422 // Get current SSID and BSSID from the WiFi driver
2423 char ssid_buf[SSID_BUFFER_SIZE];
2424 const char *ssid = this->wifi_ssid_to(ssid_buf);
2425 bssid_t bssid = this->wifi_bssid();
2426 for (auto *listener : this->connect_state_listeners_) {
2427 listener->on_wifi_connect_state(StringRef(ssid, strlen(ssid)), bssid);
2428 }
2429}
2430
2432 constexpr uint8_t empty_bssid[6] = {};
2433 for (auto *listener : this->connect_state_listeners_) {
2434 listener->on_wifi_connect_state(StringRef(), empty_bssid);
2435 }
2436}
2437#endif // USE_WIFI_CONNECT_STATE_LISTENERS
2438
2439#ifdef USE_WIFI_IP_STATE_LISTENERS
2441 for (auto *listener : this->ip_state_listeners_) {
2442 listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1));
2443 }
2444}
2445#endif // USE_WIFI_IP_STATE_LISTENERS
2446
2447#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS
2449 for (auto *listener : this->scan_results_listeners_) {
2450 listener->on_wifi_scan_results(this->scan_result_);
2451 }
2452}
2453#endif // USE_WIFI_SCAN_RESULTS_LISTENERS
2454
2456 // Guard: not for hidden networks (may not appear in scan)
2457 const WiFiAP *selected = this->get_selected_sta_();
2458 if (selected == nullptr || selected->get_hidden()) {
2459 this->roaming_attempts_ = ROAMING_MAX_ATTEMPTS; // Stop checking forever
2460 return;
2461 }
2462
2463 this->roaming_last_check_ = now;
2464 this->roaming_attempts_++;
2465
2466 // Guard: skip scan if signal is already good (no meaningful improvement possible)
2467 int8_t rssi = this->wifi_rssi();
2468 if (rssi > ROAMING_GOOD_RSSI) {
2469 ESP_LOGD(TAG, "Roam check skipped, signal good (%d dBm, attempt %u/%u)", rssi, this->roaming_attempts_,
2471 return;
2472 }
2473
2474 ESP_LOGD(TAG, "Roam scan (%d dBm, attempt %u/%u)", rssi, this->roaming_attempts_, ROAMING_MAX_ATTEMPTS);
2476 this->wifi_scan_start_(this->passive_scan_);
2477}
2478
2480 this->scan_done_ = false;
2481 // Default to IDLE - will be set to CONNECTING if we find a better AP
2483 // Record when scan completed so delayed disconnects (e.g., ESP8266 Beacon Timeout)
2484 // can be attributed to the scan and avoid resetting the attempts counter
2485 this->roaming_scan_end_ = millis();
2486
2487 // Get current connection info
2488 int8_t current_rssi = this->wifi_rssi();
2489 // Guard: must still be connected (RSSI may have become invalid during scan)
2490 if (current_rssi == WIFI_RSSI_DISCONNECTED) {
2491 this->release_scan_results_();
2492 return;
2493 }
2494
2495 char ssid_buf[SSID_BUFFER_SIZE];
2496 StringRef current_ssid(this->wifi_ssid_to(ssid_buf));
2497 bssid_t current_bssid = this->wifi_bssid();
2498
2499 // Find best candidate: same SSID, different BSSID
2500 const WiFiScanResult *best = nullptr;
2501 char bssid_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
2502
2503 for (const auto &result : this->scan_result_) {
2504 // Must be same SSID, different BSSID
2505 if (result.ssid_ != current_ssid || result.get_bssid() == current_bssid)
2506 continue;
2507
2508#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
2509 format_mac_addr_upper(result.get_bssid().data(), bssid_buf);
2510 ESP_LOGV(TAG, "Roam candidate %s %d dBm", bssid_buf, result.get_rssi());
2511#endif
2512
2513 // Track the best candidate
2514 if (best == nullptr || result.get_rssi() > best->get_rssi()) {
2515 best = &result;
2516 }
2517 }
2518
2519 // Check if best candidate meets minimum improvement threshold
2520 const WiFiAP *selected = this->get_selected_sta_();
2521 int8_t improvement = (best == nullptr) ? 0 : best->get_rssi() - current_rssi;
2522 if (selected == nullptr || improvement < ROAMING_MIN_IMPROVEMENT) {
2523 ESP_LOGV(TAG, "Roam best %+d dB (need +%d), attempt %u/%u", improvement, ROAMING_MIN_IMPROVEMENT,
2525 this->release_scan_results_();
2526 return;
2527 }
2528
2529 format_mac_addr_upper(best->get_bssid().data(), bssid_buf);
2530 ESP_LOGI(TAG, "Roaming to %s (%+d dB)", bssid_buf, improvement);
2531
2532 WiFiAP roam_params = *selected;
2533 apply_scan_result_to_params(roam_params, *best);
2534
2535 // Mark as roaming attempt - affects retry behavior if connection fails
2537 this->roaming_target_bssid_ = best->get_bssid(); // Must read before releasing scan results
2538
2539 this->release_scan_results_();
2540
2541 // Connect directly - wifi_sta_connect_ handles disconnect internally
2542 this->start_connecting(roam_params);
2543}
2544
2545WiFiComponent *global_wifi_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
2546
2547} // namespace esphome::wifi
2548#endif
BedjetMode mode
BedJet operating mode.
uint8_t m
Definition bl0906.h:1
uint8_t status
Definition bl0942.h:8
const StringRef & get_name() const
Get the name of this Application set by pre_setup().
void wake_loop_threadsafe()
Wake the main event loop from another thread or callback.
bool is_name_add_mac_suffix_enabled() const
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_clear_warning()
Definition component.h:289
StringRef is a reference to a string owned by something else.
Definition string_ref.h:26
constexpr const char * c_str() const
Definition string_ref.h:73
constexpr size_type size() const
Definition string_ref.h:74
void trigger(const Ts &...x) ESPHOME_ALWAYS_INLINE
Inform the parent automation that the event has triggered.
Definition automation.h:461
20-byte string: 18 chars inline + null, heap for longer.
const char * data() const
CompactString & operator=(const CompactString &other)
bool operator==(const CompactString &other) const
static constexpr uint8_t INLINE_CAPACITY
const char * c_str() const
char storage_[INLINE_CAPACITY+1]
static constexpr uint8_t MAX_LENGTH
uint8_t get_channel() const
void set_ssid(const std::string &ssid)
const optional< EAPAuth > & get_eap() 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 notify_scan_results_listeners_()
Notify scan results listeners with current scan results.
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 roaming_suppressed_() const
Returns true if a component has requested that roaming scans be suppressed (e.g. during audio playbac...
bool has_sta_priority(const bssid_t &bssid)
const WiFiAP * get_selected_sta_() const
WiFiSTAConnectStatus wifi_sta_connect_status_() 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)
void notify_connect_state_listeners_()
Notify connect state listeners (called after state machine reaches STA_CONNECTED)
wifi_scan_vector_t< WiFiScanResult > scan_result_
WiFiPowerSaveMode configured_power_save_
void set_sta_priority(bssid_t bssid, int8_t priority)
StaticVector< WiFiScanResultsListener *, ESPHOME_WIFI_SCAN_RESULTS_LISTENERS > scan_results_listeners_
void loop() override
Reconnect WiFi if required.
void notify_ip_state_listeners_()
Notify IP state listeners with current addresses.
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...
static constexpr uint32_t ROAMING_CHECK_INTERVAL
SemaphoreHandle_t high_performance_semaphore_
network::IPAddress get_dns_address(int num)
WiFiComponent()
Construct a WiFiComponent.
std::vector< WiFiSTAPriority > sta_priorities_
static constexpr int8_t ROAMING_GOOD_RSSI
void notify_disconnect_state_listeners_()
Notify connect state listeners of disconnection.
StaticVector< WiFiConnectStateListener *, ESPHOME_WIFI_CONNECT_STATE_LISTENERS > connect_state_listeners_
void log_discarded_scan_result_(const char *ssid, const uint8_t *bssid, int8_t rssi, uint8_t channel)
Log a discarded scan result at VERBOSE level (skipped during roaming scans to avoid log overflow)
ESPDEPRECATED("Use wifi_ssid_to() instead. Removed in 2026.9.0", "2026.3.0") std const char * wifi_ssid_to(std::span< char, SSID_BUFFER_SIZE > buffer)
Write SSID to buffer without heap allocation.
static constexpr uint8_t ROAMING_MAX_ATTEMPTS
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)
static constexpr uint32_t ROAMING_SCAN_GRACE_PERIOD
WiFiRetryPhase determine_next_phase_()
Determine next retry phase based on current state and failure conditions.
network::IPAddress wifi_dns_ip_(int num)
network::IPAddresses get_ip_addresses()
static constexpr int8_t ROAMING_MIN_IMPROVEMENT
bool matches_configured_network_(const char *ssid, const uint8_t *bssid) const
Check if network matches any configured network (for scan result filtering) Matches by SSID when conf...
float get_setup_priority() const override
WIFI setup_priority.
StaticVector< WiFiIPStateListener *, ESPHOME_WIFI_IP_STATE_LISTENERS > ip_state_listeners_
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...
void release_scan_results_()
Free scan results memory unless a component needs them.
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 needs_full_scan_results_() const
Check if full scan results are needed (captive portal active, improv, listeners)
static constexpr uint8_t FIRST_5GHZ_CHANNEL
bool release_high_performance()
Release a high-performance mode request.
bool wifi_apply_output_power_(float output_power)
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 check_connecting_finished(uint32_t now)
void start_initial_connection_()
Start initial connection - either scan or connect directly to hidden networks.
bool ssid_was_seen_in_scan_(const CompactString &ssid) const
Check if an SSID was seen in the most recent scan results Used to skip hidden mode for SSIDs we know ...
void setup() override
Setup WiFi interface.
struct esphome::wifi::WiFiComponent::@193 pending_
void clear_all_bssid_priorities_()
Clear all BSSID priority penalties after successful connection (stale after disconnect)
WiFiScanResult(const bssid_t &bssid, const char *ssid, size_t ssid_len, uint8_t channel, int8_t rssi, bool with_auth, bool is_hidden)
const bssid_t & get_bssid() const
bool matches(const WiFiAP &config) const
bool operator==(const WiFiScanResult &rhs) const
struct @66::@67 __attribute__
Wake the main loop task from an ISR. ISR-safe.
Definition main_task.h:32
void yield(void)
uint16_t type
uint8_t priority
CaptivePortal * global_captive_portal
ESP32ImprovComponent * global_improv_component
ImprovSerialComponent * global_improv_serial_component
std::array< IPAddress, 5 > IPAddresses
Definition ip_address.h:299
ProvisioningManager * global_provisioning_manager
constexpr float WIFI
Definition component.h:50
const char *const TAG
Definition spi.cpp:7
std::array< uint8_t, 6 > bssid_t
const LogString * get_signal_bars(int8_t rssi)
@ BLIND_RETRY
Blind retry mode: scanning disabled (captive portal/improv active), try ALL configured networks seque...
@ SCAN_BASED
Normal mode: scan completed, only try networks NOT visible in scan results (truly hidden networks tha...
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
@ SCANNING
Scanning for better AP.
@ CONNECTING
Attempting to connect to better AP found in scan.
@ IDLE
Not roaming, waiting for next check interval.
@ RECONNECTING
Roam connection failed, reconnecting to any available AP.
@ 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.
uint16_t uint16_t size_t elem_size
Definition helpers.cpp:26
const void size_t len
Definition hal.h:64
uint16_t size
Definition helpers.cpp:25
ESPPreferences * global_preferences
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:750
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:28
Application App
Global storage of Application pointer - only one Application can exist.
char * 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:1467
static void uint32_t
ESPPreferenceObject make_preference(size_t, uint32_t, bool)
Definition preferences.h:24
bool sync()
Commit pending writes to flash.
Definition preferences.h:32
esp_eap_ttls_phase2_types ttls_phase_2
Struct for setting static IPs in WiFiComponent.