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