ESPHome 2026.5.3
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 this->start();
636 } else {
637#ifdef USE_ESP32
638 esp_netif_init();
639#endif
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 this->start();
1282}
1283
1286 return;
1287
1288 ESP_LOGD(TAG, "Disabling");
1290 this->wifi_disconnect_();
1291 this->wifi_mode_(false, false);
1292}
1293
1295
1297 this->action_started_ = millis();
1298 ESP_LOGD(TAG, "Starting scan");
1299 this->wifi_scan_start_(this->passive_scan_);
1301}
1302
1336[[nodiscard]] inline static bool wifi_scan_result_is_better(const WiFiScanResult &a, const WiFiScanResult &b) {
1337 // Matching networks always come before non-matching
1338 if (a.get_matches() && !b.get_matches())
1339 return true;
1340 if (!a.get_matches() && b.get_matches())
1341 return false;
1342
1343 // Both matching: check priority first (tracks connection failures via priority degradation)
1344 // Priority is decreased when a BSSID fails to connect, so lower priority = previously failed
1345 if (a.get_matches() && b.get_matches() && a.get_priority() != b.get_priority()) {
1346 return a.get_priority() > b.get_priority();
1347 }
1348
1349 // Use RSSI as tiebreaker (for equal-priority matching networks or all non-matching networks)
1350 return a.get_rssi() > b.get_rssi();
1351}
1352
1353// Helper function for insertion sort of WiFi scan results
1354// Using insertion sort instead of std::stable_sort saves flash memory
1355// by avoiding template instantiations (std::rotate, std::stable_sort, lambdas)
1356// IMPORTANT: This sort is stable (preserves relative order of equal elements)
1357//
1358// Uses raw memcpy instead of copy assignment to avoid CompactString's
1359// destructor/constructor overhead (heap delete[]/new[] for long SSIDs).
1360// Copy assignment calls ~CompactString() then placement-new for every shift,
1361// which means delete[]/new[] per shift for heap-allocated SSIDs. With 70+
1362// networks (e.g., captive portal showing full scan results), this caused
1363// event loop blocking from hundreds of heap operations in a tight loop.
1364//
1365// This is safe because we're permuting elements within the same array —
1366// each slot is overwritten exactly once, so no ownership duplication occurs.
1367// All members of WiFiScanResult are either trivially copyable (bssid, channel,
1368// rssi, priority, flags) or CompactString, which stores either inline data or
1369// a heap pointer — never a self-referential pointer (unlike std::string's SSO
1370// on some implementations). This was not possible before PR#13472 replaced
1371// std::string with CompactString, since std::string's internal layout is
1372// implementation-defined and may use self-referential pointers.
1373//
1374// TODO: If C++ standardizes std::trivially_relocatable, add the assertion for
1375// WiFiScanResult/CompactString here to formally express the memcpy safety guarantee.
1376template<typename VectorType> static void insertion_sort_scan_results(VectorType &results) {
1377 // memcpy-based sort requires no self-referential pointers or virtual dispatch.
1378 // These static_asserts guard the assumptions. If any fire, the memcpy sort
1379 // must be reviewed for safety before updating the expected values.
1380 //
1381 // No vtable pointers (memcpy would corrupt vptr)
1382 static_assert(!std::is_polymorphic<WiFiScanResult>::value, "WiFiScanResult must not have vtable");
1383 static_assert(!std::is_polymorphic<CompactString>::value, "CompactString must not have vtable");
1384 // Standard layout ensures predictable memory layout with no virtual bases
1385 // and no mixed-access-specifier reordering
1386 static_assert(std::is_standard_layout<WiFiScanResult>::value, "WiFiScanResult must be standard layout");
1387 static_assert(std::is_standard_layout<CompactString>::value, "CompactString must be standard layout");
1388 // Size checks catch added/removed fields that may need safety review
1389 static_assert(sizeof(WiFiScanResult) == 32, "WiFiScanResult size changed - verify memcpy sort is still safe");
1390 static_assert(sizeof(CompactString) == 20, "CompactString size changed - verify memcpy sort is still safe");
1391 // Alignment must match for reinterpret_cast of key_buf to be valid
1392 static_assert(alignof(WiFiScanResult) <= alignof(std::max_align_t), "WiFiScanResult alignment exceeds max_align_t");
1393 const size_t size = results.size();
1394 constexpr size_t elem_size = sizeof(WiFiScanResult);
1395 // Suppress warnings for intentional memcpy on non-trivially-copyable type.
1396 // Safety is guaranteed by the static_asserts above and the permutation invariant.
1397 // NOLINTNEXTLINE(bugprone-undefined-memory-manipulation)
1398 auto *memcpy_fn = &memcpy;
1399 for (size_t i = 1; i < size; i++) {
1400 alignas(WiFiScanResult) uint8_t key_buf[elem_size];
1401 memcpy_fn(key_buf, &results[i], elem_size);
1402 const auto &key = *reinterpret_cast<const WiFiScanResult *>(key_buf);
1403 int32_t j = i - 1;
1404
1405 // Move elements that are worse than key to the right
1406 // For stability, we only move if key is strictly better than results[j]
1407 while (j >= 0 && wifi_scan_result_is_better(key, results[j])) {
1408 memcpy_fn(&results[j + 1], &results[j], elem_size);
1409 j--;
1410 }
1411 memcpy_fn(&results[j + 1], key_buf, elem_size);
1412 }
1413}
1414
1415// Helper function to log matching scan results - marked noinline to prevent re-inlining into loop
1416//
1417// IMPORTANT: This function deliberately uses a SINGLE log call to minimize blocking.
1418// In environments with many matching networks (e.g., 18+ mesh APs), multiple log calls
1419// per network would block the main loop for an unacceptable duration. Each log call
1420// has overhead from UART transmission, so combining INFO+DEBUG into one line halves
1421// the blocking time. Do NOT split this into separate ESP_LOGI/ESP_LOGD calls.
1422__attribute__((noinline)) static void log_scan_result(const WiFiScanResult &res) {
1423 char bssid_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
1424 auto bssid = res.get_bssid();
1425 format_mac_addr_upper(bssid.data(), bssid_s);
1426
1427#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
1428 // Single combined log line with all details when DEBUG enabled
1429 ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s Ch:%2u %3ddB P:%d", res.get_ssid().c_str(),
1430 res.get_is_hidden() ? LOG_STR_LITERAL("(HIDDEN) ") : LOG_STR_LITERAL(""), bssid_s,
1431 LOG_STR_ARG(get_signal_bars(res.get_rssi())), res.get_channel(), res.get_rssi(), res.get_priority());
1432#else
1433 ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(),
1434 res.get_is_hidden() ? LOG_STR_LITERAL("(HIDDEN) ") : LOG_STR_LITERAL(""), bssid_s,
1435 LOG_STR_ARG(get_signal_bars(res.get_rssi())));
1436#endif
1437}
1438
1440 if (!this->scan_done_) {
1441 if (millis() - this->action_started_ > WIFI_SCAN_TIMEOUT_MS) {
1442 ESP_LOGE(TAG, "Scan timeout");
1443 this->retry_connect();
1444 }
1445 return;
1446 }
1447 this->scan_done_ = false;
1449 true; // Track that we've done a scan since captive portal started
1451
1452 if (this->scan_result_.empty()) {
1453 ESP_LOGW(TAG, "No networks found");
1454 this->retry_connect();
1455 return;
1456 }
1457
1458 ESP_LOGD(TAG, "Found networks:");
1459 for (auto &res : this->scan_result_) {
1460 for (auto &ap : this->sta_) {
1461 if (res.matches(ap)) {
1462 res.set_matches(true);
1463 // Cache priority lookup - do single search instead of 2 separate searches
1464 const bssid_t &bssid = res.get_bssid();
1465 if (!this->has_sta_priority(bssid)) {
1466 this->set_sta_priority(bssid, ap.get_priority());
1467 }
1468 res.set_priority(this->get_sta_priority(bssid));
1469 break;
1470 }
1471 }
1472 }
1473
1474 // Sort scan results using insertion sort for better memory efficiency
1475 insertion_sort_scan_results(this->scan_result_);
1476
1477 // Log matching networks (non-matching already logged at VERBOSE in scan callback)
1478 for (auto &res : this->scan_result_) {
1479 if (res.get_matches()) {
1480 log_scan_result(res);
1481 }
1482 }
1483
1484 // SYNCHRONIZATION POINT: Establish link between scan_result_[0] and selected_sta_index_
1485 // After sorting, scan_result_[0] contains the best network. Now find which sta_[i] config
1486 // matches that network and record it in selected_sta_index_. This keeps the two indices
1487 // synchronized so build_params_for_current_phase_() can safely use both to build connection parameters.
1488 const WiFiScanResult &scan_res = this->scan_result_[0];
1489 bool found_match = false;
1490 if (scan_res.get_matches()) {
1491 for (size_t i = 0; i < this->sta_.size(); i++) {
1492 if (scan_res.matches(this->sta_[i])) {
1493 // Safe cast: sta_.size() limited to MAX_WIFI_NETWORKS (127) in __init__.py validation
1494 // No overflow check needed - YAML validation prevents >127 networks
1495 this->selected_sta_index_ = static_cast<int8_t>(i); // Links scan_result_[0] with sta_[i]
1496 found_match = true;
1497 break;
1498 }
1499 }
1500 }
1501
1502 if (!found_match) {
1503 ESP_LOGW(TAG, "No matching network found");
1504 // No scan results matched our configured networks - transition directly to hidden mode
1505 // Don't call retry_connect() since we never attempted a connection (no BSSID to penalize)
1507 // If no hidden networks to try, skip connection attempt (will be handled on next loop)
1508 if (this->selected_sta_index_ == -1) {
1509 return;
1510 }
1511 // Now start connection attempt in hidden mode
1513 return; // scan started, wait for next loop iteration
1514 }
1515
1516 yield();
1517
1518 WiFiAP params = this->build_params_for_current_phase_();
1519 // Ensure we're in SCAN_CONNECTING phase when connecting with scan results
1520 // (needed when scan was started directly without transition_to_phase_, e.g., initial scan)
1521 this->start_connecting(params);
1522}
1523
1525 char mac_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
1526 ESP_LOGCONFIG(TAG,
1527 "WiFi:\n"
1528 " Local MAC: %s\n"
1529 " Connected: %s",
1530 get_mac_address_pretty_into_buffer(mac_s), YESNO(this->is_connected()));
1531 if (this->is_disabled()) {
1532 ESP_LOGCONFIG(TAG, " Disabled");
1533 return;
1534 }
1535#if defined(USE_ESP32) && defined(SOC_WIFI_SUPPORT_5G)
1536 const char *band_mode_s;
1537 switch (this->band_mode_) {
1538 case WIFI_BAND_MODE_2G_ONLY:
1539 band_mode_s = "2.4GHz";
1540 break;
1541 case WIFI_BAND_MODE_5G_ONLY:
1542 band_mode_s = "5GHz";
1543 break;
1544 case WIFI_BAND_MODE_AUTO:
1545 default:
1546 band_mode_s = "Auto";
1547 break;
1548 }
1549 ESP_LOGCONFIG(TAG, " Band Mode: %s", band_mode_s);
1550#endif
1551#ifdef USE_WIFI_PHY_MODE
1552 ESP_LOGCONFIG(TAG, " PHY Mode: %s", LOG_STR_ARG(phy_mode_to_log_string(this->phy_mode_)));
1553#endif
1554 if (this->is_connected()) {
1555 this->print_connect_params_();
1556 }
1557}
1558
1560 auto status = this->wifi_sta_connect_status_();
1561
1563 char ssid_buf[SSID_BUFFER_SIZE];
1564 if (wifi_ssid_to(ssid_buf)[0] == '\0') {
1565 ESP_LOGW(TAG, "Connection incomplete");
1566 this->retry_connect();
1567 return;
1568 }
1569
1570 ESP_LOGI(TAG, "Connected");
1571 // Warn if we had to retry with hidden network mode for a network that's not marked hidden
1572 // Only warn if we actually connected without scan data (SSID only), not if scan succeeded on retry
1573 if (const WiFiAP *config = this->get_selected_sta_(); this->retry_phase_ == WiFiRetryPhase::RETRY_HIDDEN &&
1574 config && !config->get_hidden() &&
1575 this->scan_result_.empty()) {
1576 ESP_LOGW(TAG, LOG_SECRET("'%s'") " should be marked hidden", config->ssid_.c_str());
1577 }
1578 // Reset to initial phase on successful connection (don't log transition, just reset state)
1580 this->num_retried_ = 0;
1581 if (this->has_ap()) {
1582#ifdef USE_CAPTIVE_PORTAL
1583 if (this->is_captive_portal_active_()) {
1585 }
1586#endif
1587 ESP_LOGD(TAG, "Disabling AP");
1588 this->wifi_mode_({}, false);
1589 }
1590#ifdef USE_IMPROV
1591 if (this->is_esp32_improv_active_()) {
1593 }
1594#endif
1595
1597 // Refresh is_connected() cache; loop()'s refresh ran before this transition.
1599 this->num_retried_ = 0;
1600 this->print_connect_params_();
1601
1602 // Reset roaming state on successful connection
1603 this->roaming_last_check_ = now;
1604 // Only preserve attempts if reconnecting after a failed roam attempt
1605 // This prevents ping-pong between APs when a roam target is unreachable
1607 // Successful roam to better AP on first try - reset attempts so we can roam again later
1608 ESP_LOGD(TAG, "Roam successful");
1609 this->roaming_attempts_ = 0;
1610 } else if (this->roaming_state_ == RoamingState::RECONNECTING) {
1611 // Check if we ended up on the roam target despite needing a retry
1612 // (e.g., first connect failed but scan-based retry found and connected to the same better AP)
1613 bssid_t current_bssid = this->wifi_bssid();
1614 if (this->roaming_target_bssid_ != bssid_t{} && current_bssid == this->roaming_target_bssid_) {
1615 char bssid_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
1616 format_mac_addr_upper(current_bssid.data(), bssid_buf);
1617 ESP_LOGD(TAG, "Roam successful (via retry, attempt %u/%u) to %s", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS,
1618 bssid_buf);
1619 this->roaming_attempts_ = 0;
1620 } else if (this->roaming_target_bssid_ != bssid_t{}) {
1621 // Failed roam to specific target, reconnected to different AP - keep attempts to prevent ping-pong
1622 ESP_LOGD(TAG, "Reconnected after failed roam (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS);
1623 } else {
1624 // Reconnected after scan-induced disconnect (no roam target) - keep attempts
1625 ESP_LOGD(TAG, "Reconnected after roam scan (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS);
1626 }
1627 } else {
1628 // Normal connection (boot, credentials changed, etc.)
1629 this->roaming_attempts_ = 0;
1630 }
1632 this->roaming_target_bssid_ = {};
1633 this->roaming_scan_end_ = 0;
1634
1635 // Clear all priority penalties - the next reconnect will happen when an AP disconnects,
1636 // which means the landscape has likely changed and previous tracked failures are stale
1638
1639#ifdef USE_WIFI_FAST_CONNECT
1641#endif
1642
1643 this->release_scan_results_();
1644
1645#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
1646 // Notify listeners now that state machine has reached STA_CONNECTED
1647 // This ensures wifi.connected condition returns true in listener automations
1649#endif
1650
1651#if defined(USE_ESP8266) && defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP)
1652 // On ESP8266, GOT_IP event may not fire for static IP configurations,
1653 // so notify IP state listeners here as a fallback.
1654 if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) {
1656 }
1657#endif
1658
1659 return;
1660 }
1661
1662 if (now - this->action_started_ > WIFI_CONNECT_TIMEOUT_MS) {
1663 ESP_LOGW(TAG, "Connection timeout, aborting connection attempt");
1664 this->wifi_disconnect_();
1665 this->retry_connect();
1666 return;
1667 }
1668
1669 if (this->error_from_callback_) {
1670 // ESP8266: logging done in callback, listeners deferred via pending_.disconnect
1671 // Other platforms: just log generic failure message
1672#ifndef USE_ESP8266
1673 ESP_LOGW(TAG, "Connecting to network failed (callback)");
1674#endif
1675 this->retry_connect();
1676 return;
1677 }
1678
1680 return;
1681 }
1682
1684 ESP_LOGW(TAG, "Network no longer found");
1685 this->retry_connect();
1686 return;
1687 }
1688
1690 ESP_LOGW(TAG, "Connecting to network failed");
1691 this->retry_connect();
1692 return;
1693 }
1694
1695 ESP_LOGW(TAG, "Unknown connection status %d", (int) status);
1696 this->retry_connect();
1697}
1698
1706 switch (this->retry_phase_) {
1708#ifdef USE_WIFI_FAST_CONNECT
1710 // INITIAL_CONNECT and FAST_CONNECT_CYCLING_APS: no retries, try next AP or fall back to scan
1711 if (this->selected_sta_index_ < static_cast<int8_t>(this->sta_.size()) - 1) {
1712 return WiFiRetryPhase::FAST_CONNECT_CYCLING_APS; // Move to next AP
1713 }
1714#endif
1715 // Check if we should try explicit hidden networks before scanning
1716 // This handles reconnection after connection loss where first network is hidden
1717 if (!this->sta_.empty() && this->sta_[0].get_hidden()) {
1719 }
1720 // No more APs to try, fall back to scan
1722
1724 // Try all explicitly hidden networks before scanning
1725 if (this->num_retried_ + 1 < WIFI_RETRY_COUNT_PER_SSID) {
1726 return WiFiRetryPhase::EXPLICIT_HIDDEN; // Keep retrying same SSID
1727 }
1728
1729 // Exhausted retries on current SSID - check for more explicitly hidden networks
1730 // Stop when we reach a visible network (proceed to scanning)
1731 size_t next_index = this->selected_sta_index_ + 1;
1732 if (next_index < this->sta_.size() && this->sta_[next_index].get_hidden()) {
1733 // Found another explicitly hidden network
1735 }
1736
1737 // No more consecutive explicitly hidden networks
1738 // If ALL networks are hidden, skip scanning and go directly to restart
1739 if (this->find_first_non_hidden_index_() < 0) {
1741 }
1742 // Otherwise proceed to scanning for non-hidden networks
1744 }
1745
1747 // If scan found no networks or no matching networks, skip to hidden network mode
1748 if (this->scan_result_.empty() || !this->scan_result_[0].get_matches()) {
1750 }
1751
1752 if (this->num_retried_ + 1 < WIFI_RETRY_COUNT_PER_BSSID) {
1753 return WiFiRetryPhase::SCAN_CONNECTING; // Keep retrying same BSSID
1754 }
1755
1756 // Exhausted retries on current BSSID (scan_result_[0])
1757 // Its priority has been decreased, so on next scan it will be sorted lower
1758 // and we'll try the next best BSSID.
1759 // Check if there are any potentially hidden networks to try
1760 if (this->find_next_hidden_sta_(-1) >= 0) {
1761 return WiFiRetryPhase::RETRY_HIDDEN; // Found hidden networks to try
1762 }
1763 // No hidden networks - always go through RESTARTING_ADAPTER phase
1764 // This ensures num_retried_ gets reset and a fresh scan is triggered
1765 // The actual adapter restart will be skipped if captive portal/improv is active
1767
1769 // If no hidden SSIDs to try (selected_sta_index_ == -1), skip directly to rescan
1770 if (this->selected_sta_index_ >= 0) {
1771 if (this->num_retried_ + 1 < WIFI_RETRY_COUNT_PER_SSID) {
1772 return WiFiRetryPhase::RETRY_HIDDEN; // Keep retrying same SSID
1773 }
1774
1775 // Exhausted retries on current SSID - check if there are more potentially hidden SSIDs to try
1776 if (this->selected_sta_index_ < static_cast<int8_t>(this->sta_.size()) - 1) {
1777 // Check if find_next_hidden_sta_() would actually find another hidden SSID
1778 // as it might have been seen in the scan results and we want to skip those
1779 // otherwise we will get stuck in RETRY_HIDDEN phase
1780 if (this->find_next_hidden_sta_(this->selected_sta_index_) != -1) {
1781 // More hidden SSIDs available - stay in RETRY_HIDDEN, advance will happen in retry_connect()
1783 }
1784 }
1785 }
1786 // Exhausted all potentially hidden SSIDs - always go through RESTARTING_ADAPTER
1787 // This ensures num_retried_ gets reset and a fresh scan is triggered
1788 // The actual adapter restart will be skipped if captive portal/improv is active
1790
1792 // After restart, go back to explicit hidden if we went through it initially
1795 }
1796 // Skip scanning when captive portal/improv is active to avoid disrupting AP,
1797 // BUT only if we've already completed at least one scan AFTER the portal started.
1798 // When captive portal first starts, scan results may be filtered/stale, so we need
1799 // to do one full scan to populate available networks for the captive portal UI.
1800 //
1801 // WHY SCANNING DISRUPTS AP MODE:
1802 // WiFi scanning requires the radio to leave the AP's channel and hop through
1803 // other channels to listen for beacons. During this time (even for passive scans),
1804 // the AP cannot service connected clients - they experience disconnections or
1805 // timeouts. On ESP32, even passive scans cause brief but noticeable disruptions
1806 // that break captive portal HTTP requests and DNS lookups.
1807 //
1808 // BLIND RETRY MODE:
1809 // When captive portal/improv is active, we use RETRY_HIDDEN as a "try all networks
1810 // blindly" mode. Since retry_hidden_mode_ is set to BLIND_RETRY (in RESTARTING_ADAPTER
1811 // transition), find_next_hidden_sta_() will treat ALL configured networks as
1812 // candidates, cycling through them without requiring scan results.
1813 //
1814 // This allows users to configure WiFi via captive portal while the device keeps
1815 // attempting to connect to all configured networks in sequence.
1816 // Captive portal needs scan results to show available networks.
1817 // If captive portal is active, only skip scanning if we've done a scan after it started.
1818 // If only improv is active (no captive portal), skip scanning since improv doesn't need results.
1819 if (this->is_captive_portal_active_()) {
1822 }
1823 // Need to scan for captive portal
1824 } else if (this->is_esp32_improv_active_()) {
1825 // Improv doesn't need scan results
1827 }
1829 }
1830
1831 // Should never reach here
1833}
1834
1845 WiFiRetryPhase old_phase = this->retry_phase_;
1846
1847 // No-op if staying in same phase
1848 if (old_phase == new_phase) {
1849 return false;
1850 }
1851
1852 ESP_LOGD(TAG, "Retry phase: %s → %s", LOG_STR_ARG(retry_phase_to_log_string(old_phase)),
1853 LOG_STR_ARG(retry_phase_to_log_string(new_phase)));
1854
1855 this->retry_phase_ = new_phase;
1856 this->num_retried_ = 0; // Reset retry counter on phase change
1857
1858 // Phase-specific setup
1859 switch (new_phase) {
1860#ifdef USE_WIFI_FAST_CONNECT
1862 // Move to next configured AP - clear old scan data so new AP is tried with config only
1863 this->selected_sta_index_++;
1864 this->scan_result_.clear();
1865 break;
1866#endif
1867
1869 // Starting explicit hidden phase - reset to first network
1870 this->selected_sta_index_ = 0;
1871 break;
1872
1874 // Transitioning to scan-based connection
1875#ifdef USE_WIFI_FAST_CONNECT
1877 ESP_LOGI(TAG, "Fast connect exhausted, falling back to scan");
1878 }
1879#endif
1880 // Trigger scan if we don't have scan results OR if transitioning from phases that need fresh scan
1881 if (this->scan_result_.empty() || old_phase == WiFiRetryPhase::EXPLICIT_HIDDEN ||
1883 this->selected_sta_index_ = -1; // Will be set after scan completes
1884 this->start_scanning();
1885 return true; // Started scan, wait for completion
1886 }
1887 // Already have scan results - selected_sta_index_ should already be synchronized
1888 // (set in check_scanning_finished() when scan completed)
1889 // No need to reset it here
1890 break;
1891
1893 // Always reset to first candidate when entering this phase.
1894 // This phase can be entered from:
1895 // - SCAN_CONNECTING: normal flow, find_next_hidden_sta_() skips networks visible in scan
1896 // - RESTARTING_ADAPTER: captive portal active, find_next_hidden_sta_() tries ALL networks
1897 //
1898 // The retry_hidden_mode_ controls the behavior:
1899 // - SCAN_BASED: scan_result_ is checked, visible networks are skipped
1900 // - BLIND_RETRY: scan_result_ is ignored, all networks become candidates
1901 // We don't clear scan_result_ here - the mode controls whether it's consulted.
1903
1904 if (this->selected_sta_index_ == -1) {
1905 ESP_LOGD(TAG, "All SSIDs visible or already tried, skipping hidden mode");
1906 }
1907 break;
1908
1910 // Skip actual adapter restart if captive portal/improv is active
1911 // This allows state machine to reset num_retried_ and trigger fresh scan
1912 // without disrupting the captive portal/improv connection
1913 if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_()) {
1914 this->restart_adapter();
1915 } else {
1916 // Even when skipping full restart, disconnect to clear driver state
1917 // Without this, platforms like LibreTiny may think we're still connecting
1918 this->wifi_disconnect_();
1919 }
1920 // Clear scan flag - we're starting a new retry cycle
1921 // This is critical for captive portal/improv flow: when determine_next_phase_()
1922 // returns RETRY_HIDDEN (because scanning is skipped), find_next_hidden_sta_()
1923 // will see BLIND_RETRY mode and treat ALL networks as candidates,
1924 // effectively cycling through all configured networks without scan results.
1926 // Always enter cooldown after restart (or skip-restart) to allow stabilization
1927 // Use extended cooldown when AP is active to avoid constant scanning that blocks DNS
1929 this->action_started_ = millis();
1930 // Return true to indicate we should wait (go to COOLDOWN) instead of immediately connecting
1931 return true;
1932
1933 default:
1934 break;
1935 }
1936
1937 return false; // Did not start scan, can proceed with connection
1938}
1939
1941 if (!this->sta_priorities_.empty()) {
1942 decltype(this->sta_priorities_)().swap(this->sta_priorities_);
1943 }
1944}
1945
1950 if (this->sta_priorities_.empty()) {
1951 return;
1952 }
1953
1954 int8_t first_priority = this->sta_priorities_[0].priority;
1955
1956 // Only clear if all priorities have been decremented to the minimum value
1957 // At this point, all BSSIDs have been equally penalized and priority info is useless
1958 if (first_priority != std::numeric_limits<int8_t>::min()) {
1959 return;
1960 }
1961
1962 for (const auto &pri : this->sta_priorities_) {
1963 if (pri.priority != first_priority) {
1964 return; // Not all same, nothing to do
1965 }
1966 }
1967
1968 // All priorities are at minimum - clear the vector to save memory and reset
1969 ESP_LOGD(TAG, "Clearing BSSID priorities (all at minimum)");
1971}
1972
1992 // Determine which BSSID we tried to connect to
1993 optional<bssid_t> failed_bssid;
1994
1995 if (this->retry_phase_ == WiFiRetryPhase::SCAN_CONNECTING && !this->scan_result_.empty()) {
1996 // Scan-based phase: always use best result (index 0)
1997 failed_bssid = this->scan_result_[0].get_bssid();
1998 } else if (const WiFiAP *config = this->get_selected_sta_(); config && config->has_bssid()) {
1999 // Config has specific BSSID (fast_connect or user-specified)
2000 failed_bssid = config->get_bssid();
2001 }
2002
2003 if (!failed_bssid.has_value()) {
2004 return; // No BSSID to penalize
2005 }
2006
2007 // Get SSID for logging (use pointer to avoid copy)
2008 const char *ssid = nullptr;
2009 if (this->retry_phase_ == WiFiRetryPhase::SCAN_CONNECTING && !this->scan_result_.empty()) {
2010 ssid = this->scan_result_[0].ssid_.c_str();
2011 } else if (const WiFiAP *config = this->get_selected_sta_()) {
2012 ssid = config->ssid_.c_str();
2013 }
2014
2015 // Only decrease priority on the last attempt for this phase
2016 // This prevents false positives from transient WiFi stack issues
2017 uint8_t max_retries = get_max_retries_for_phase(this->retry_phase_);
2018 bool is_last_attempt = (this->num_retried_ + 1 >= max_retries);
2019
2020 // Decrease priority only on last attempt to avoid false positives from transient failures
2021 int8_t old_priority = this->get_sta_priority(failed_bssid.value());
2022 int8_t new_priority = old_priority;
2023
2024 if (is_last_attempt) {
2025 // Decrease priority, but clamp to int8_t::min to prevent overflow
2026 new_priority =
2027 (old_priority > std::numeric_limits<int8_t>::min()) ? (old_priority - 1) : std::numeric_limits<int8_t>::min();
2028 this->set_sta_priority(failed_bssid.value(), new_priority);
2029 }
2030 char bssid_s[18];
2031 format_mac_addr_upper(failed_bssid.value().data(), bssid_s);
2032 ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d", ssid != nullptr ? ssid : "",
2033 bssid_s, old_priority, new_priority);
2034
2035 // After adjusting priority, check if all priorities are now at minimum
2036 // If so, clear the vector to save memory and reset for fresh start
2038}
2039
2051 WiFiRetryPhase current_phase = this->retry_phase_;
2052
2053 // Check if we need to advance to next AP/SSID within the same phase
2054#ifdef USE_WIFI_FAST_CONNECT
2055 if (current_phase == WiFiRetryPhase::FAST_CONNECT_CYCLING_APS) {
2056 // Fast connect: always advance to next AP (no retries per AP)
2057 this->selected_sta_index_++;
2058 this->num_retried_ = 0;
2059 ESP_LOGD(TAG, "Next AP in %s", LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_)));
2060 return;
2061 }
2062#endif
2063
2064 if (current_phase == WiFiRetryPhase::EXPLICIT_HIDDEN && this->num_retried_ + 1 >= WIFI_RETRY_COUNT_PER_SSID) {
2065 // Explicit hidden: exhausted retries on current SSID, find next explicitly hidden network
2066 // Stop when we reach a visible network (proceed to scanning)
2067 size_t next_index = this->selected_sta_index_ + 1;
2068 if (next_index < this->sta_.size() && this->sta_[next_index].get_hidden()) {
2069 this->selected_sta_index_ = static_cast<int8_t>(next_index);
2070 this->num_retried_ = 0;
2071 ESP_LOGD(TAG, "Next explicit hidden network at index %d", static_cast<int>(next_index));
2072 return;
2073 }
2074 // No more consecutive explicit hidden networks found - fall through to trigger phase change
2075 }
2076
2077 if (current_phase == WiFiRetryPhase::RETRY_HIDDEN && this->num_retried_ + 1 >= WIFI_RETRY_COUNT_PER_SSID) {
2078 // Hidden mode: exhausted retries on current SSID, find next potentially hidden SSID
2079 // If first network is marked hidden, we went through EXPLICIT_HIDDEN phase
2080 // In that case, skip networks marked hidden:true (already tried)
2081 // Otherwise, include them (they haven't been tried yet)
2082 int8_t next_index = this->find_next_hidden_sta_(this->selected_sta_index_);
2083 if (next_index != -1) {
2084 // Found another potentially hidden SSID
2085 this->selected_sta_index_ = next_index;
2086 this->num_retried_ = 0;
2087 return;
2088 }
2089 // No more potentially hidden SSIDs - set selected_sta_index_ to -1 to trigger phase change
2090 // This ensures determine_next_phase_() will skip the RETRY_HIDDEN logic and transition out
2091 this->selected_sta_index_ = -1;
2092 // Return early - phase change will happen on next wifi_loop() iteration
2093 return;
2094 }
2095
2096 // Don't increment retry counter if we're in a scan phase with no valid targets
2097 if (this->needs_scan_results_()) {
2098 return;
2099 }
2100
2101 // Increment retry counter to try the same target again
2102 this->num_retried_++;
2103 ESP_LOGD(TAG, "Retry attempt %u/%u in phase %s", this->num_retried_ + 1,
2104 get_max_retries_for_phase(this->retry_phase_), LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_)));
2105}
2106
2108 // Handle roaming state transitions - preserve attempts counter to prevent ping-pong
2109 // to unreachable APs after ROAMING_MAX_ATTEMPTS failures
2111 // Roam connection failed - transition to reconnecting
2112 ESP_LOGD(TAG, "Roam failed, reconnecting (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS);
2114 } else if (this->roaming_state_ == RoamingState::SCANNING) {
2115 // Disconnected during roam scan - transition to RECONNECTING so the attempts
2116 // counter is preserved when reconnection succeeds (IDLE would reset it)
2117 ESP_LOGD(TAG, "Disconnected during roam scan (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS);
2119 } else if (this->roaming_state_ == RoamingState::IDLE) {
2120 // Check if a roaming scan recently completed - on ESP8266, going off-channel
2121 // during scan can cause a delayed Beacon Timeout 8-20 seconds after scan finishes.
2122 // Transition to RECONNECTING so the attempts counter is preserved on reconnect.
2124 ESP_LOGD(TAG, "Disconnect after roam scan (attempt %u/%u)", this->roaming_attempts_, ROAMING_MAX_ATTEMPTS);
2126 } else {
2127 // Not a roaming-triggered reconnect, reset state
2128 this->clear_roaming_state_();
2129 }
2130 }
2131 // RECONNECTING: keep state and counter, still trying to reconnect
2132
2134
2135 // Determine next retry phase based on current state
2136 WiFiRetryPhase current_phase = this->retry_phase_;
2137 WiFiRetryPhase next_phase = this->determine_next_phase_();
2138
2139 // Handle phase transitions (transition_to_phase_ handles same-phase no-op internally)
2140 if (this->transition_to_phase_(next_phase)) {
2141 return; // Scan started or adapter restarted (which sets its own state)
2142 }
2143
2144 if (next_phase == current_phase) {
2146 }
2147
2148 yield();
2149 // Check if we have a valid target before building params
2150 // After exhausting all networks in a phase, selected_sta_index_ may be -1
2151 // In that case, skip connection and let next wifi_loop() handle phase transition
2152 if (this->selected_sta_index_ >= 0) {
2153 WiFiAP params = this->build_params_for_current_phase_();
2154 this->start_connecting(params);
2155 }
2156}
2157
2158void WiFiComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
2160 this->power_save_ = power_save;
2161#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
2162 this->configured_power_save_ = power_save;
2163#endif
2164}
2165
2166void WiFiComponent::set_passive_scan(bool passive) { this->passive_scan_ = passive; }
2167
2169#ifdef USE_CAPTIVE_PORTAL
2171#else
2172 return false;
2173#endif
2174}
2176#ifdef USE_IMPROV
2178#else
2179 return false;
2180#endif
2181}
2182
2183#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE)
2185 // Already configured for high performance - request satisfied
2187 return true;
2188 }
2189
2190 // Semaphore initialization failed
2191 if (this->high_performance_semaphore_ == nullptr) {
2192 return false;
2193 }
2194
2195 // Give the semaphore (non-blocking). This increments the count.
2196 return xSemaphoreGive(this->high_performance_semaphore_) == pdTRUE;
2197}
2198
2200 // Already configured for high performance - nothing to release
2202 return true;
2203 }
2204
2205 // Semaphore initialization failed
2206 if (this->high_performance_semaphore_ == nullptr) {
2207 return false;
2208 }
2209
2210 // Take the semaphore (non-blocking). This decrements the count.
2211 return xSemaphoreTake(this->high_performance_semaphore_, 0) == pdTRUE;
2212}
2213#endif // USE_ESP32 && USE_WIFI_RUNTIME_POWER_SAVE
2214
2215#ifdef USE_WIFI_FAST_CONNECT
2217 SavedWifiFastConnectSettings fast_connect_save{};
2218
2219 if (this->fast_connect_pref_.load(&fast_connect_save)) {
2220 // Validate saved AP index
2221 if (fast_connect_save.ap_index < 0 || static_cast<size_t>(fast_connect_save.ap_index) >= this->sta_.size()) {
2222 ESP_LOGW(TAG, "AP index out of bounds");
2223 return false;
2224 }
2225
2226 // Set selected index for future operations (save, retry, etc)
2227 this->selected_sta_index_ = fast_connect_save.ap_index;
2228
2229 // Copy entire config, then override with fast connect data
2230 params = this->sta_[fast_connect_save.ap_index];
2231
2232 // Override with saved BSSID/channel from fast connect (SSID/password/etc already copied from config)
2233 bssid_t bssid{};
2234 std::copy(fast_connect_save.bssid, fast_connect_save.bssid + 6, bssid.begin());
2235 params.set_bssid(bssid);
2236 params.set_channel(fast_connect_save.channel);
2237 // Fast connect uses specific BSSID+channel, not hidden network probe (even if config has hidden: true)
2238 params.set_hidden(false);
2239
2240 ESP_LOGD(TAG, "Loaded fast_connect settings");
2241#if defined(USE_ESP32) && defined(SOC_WIFI_SUPPORT_5G)
2242 if ((this->band_mode_ == WIFI_BAND_MODE_5G_ONLY && fast_connect_save.channel < FIRST_5GHZ_CHANNEL) ||
2243 (this->band_mode_ == WIFI_BAND_MODE_2G_ONLY && fast_connect_save.channel >= FIRST_5GHZ_CHANNEL)) {
2244 ESP_LOGW(TAG, "Saved channel %u not allowed by band mode, ignoring fast_connect", fast_connect_save.channel);
2245 this->selected_sta_index_ = -1;
2246 return false;
2247 }
2248#endif
2249 return true;
2250 }
2251
2252 return false;
2253}
2254
2256 bssid_t bssid = wifi_bssid();
2257 uint8_t channel = get_wifi_channel();
2258 // selected_sta_index_ is always valid here (called only after successful connection)
2259 // Fallback to 0 is defensive programming for robustness
2260 int8_t ap_index = this->selected_sta_index_ >= 0 ? this->selected_sta_index_ : 0;
2261
2262 // Skip save if settings haven't changed (compare with previously saved settings to reduce flash wear)
2263 SavedWifiFastConnectSettings previous_save{};
2264 if (this->fast_connect_pref_.load(&previous_save) && memcmp(previous_save.bssid, bssid.data(), 6) == 0 &&
2265 previous_save.channel == channel && previous_save.ap_index == ap_index) {
2266 return; // No change, nothing to save
2267 }
2268
2269 SavedWifiFastConnectSettings fast_connect_save{};
2270 memcpy(fast_connect_save.bssid, bssid.data(), 6);
2271 fast_connect_save.channel = channel;
2272 fast_connect_save.ap_index = ap_index;
2273
2274 this->fast_connect_pref_.save(&fast_connect_save);
2275
2276 ESP_LOGD(TAG, "Saved fast_connect settings");
2277}
2278#endif
2279
2280void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = CompactString(ssid.c_str(), ssid.size()); }
2281void WiFiAP::set_ssid(const char *ssid) { this->ssid_ = CompactString(ssid, strlen(ssid)); }
2282void WiFiAP::set_bssid(const bssid_t &bssid) { this->bssid_ = bssid; }
2283void WiFiAP::clear_bssid() { this->bssid_ = {}; }
2284void WiFiAP::set_password(const std::string &password) {
2285 this->password_ = CompactString(password.c_str(), password.size());
2286}
2287void WiFiAP::set_password(const char *password) { this->password_ = CompactString(password, strlen(password)); }
2288#ifdef USE_WIFI_WPA2_EAP
2289void WiFiAP::set_eap(optional<EAPAuth> eap_auth) { this->eap_ = std::move(eap_auth); }
2290#endif
2291void WiFiAP::set_channel(uint8_t channel) { this->channel_ = channel; }
2292void WiFiAP::clear_channel() { this->channel_ = 0; }
2293#ifdef USE_WIFI_MANUAL_IP
2294void WiFiAP::set_manual_ip(optional<ManualIP> manual_ip) { this->manual_ip_ = manual_ip; }
2295#endif
2296void WiFiAP::set_hidden(bool hidden) { this->hidden_ = hidden; }
2297const bssid_t &WiFiAP::get_bssid() const { return this->bssid_; }
2298bool WiFiAP::has_bssid() const { return this->bssid_ != bssid_t{}; }
2299#ifdef USE_WIFI_WPA2_EAP
2300const optional<EAPAuth> &WiFiAP::get_eap() const { return this->eap_; }
2301#endif
2302#ifdef USE_WIFI_MANUAL_IP
2303const optional<ManualIP> &WiFiAP::get_manual_ip() const { return this->manual_ip_; }
2304#endif
2305bool WiFiAP::get_hidden() const { return this->hidden_; }
2306
2307WiFiScanResult::WiFiScanResult(const bssid_t &bssid, const char *ssid, size_t ssid_len, uint8_t channel, int8_t rssi,
2308 bool with_auth, bool is_hidden)
2309 : bssid_(bssid),
2310 channel_(channel),
2311 rssi_(rssi),
2312 ssid_(ssid, ssid_len),
2313 with_auth_(with_auth),
2314 is_hidden_(is_hidden) {}
2315bool WiFiScanResult::matches(const WiFiAP &config) const {
2316 if (config.get_hidden()) {
2317 // User configured a hidden network, only match actually hidden networks
2318 // don't match SSID
2319 if (!this->is_hidden_)
2320 return false;
2321 } else if (!config.ssid_.empty()) {
2322 // check if SSID matches
2323 if (this->ssid_ != config.ssid_)
2324 return false;
2325 } else {
2326 // network is configured without SSID - match other settings
2327 }
2328 // If BSSID configured, only match for correct BSSIDs
2329 if (config.has_bssid() && config.get_bssid() != this->bssid_)
2330 return false;
2331
2332#ifdef USE_WIFI_WPA2_EAP
2333 // BSSID requires auth but no PSK or EAP credentials given
2334 if (this->with_auth_ && (config.password_.empty() && !config.get_eap().has_value()))
2335 return false;
2336
2337 // BSSID does not require auth, but PSK or EAP credentials given
2338 if (!this->with_auth_ && (!config.password_.empty() || config.get_eap().has_value()))
2339 return false;
2340#else
2341 // If PSK given, only match for networks with auth (and vice versa)
2342 if (config.password_.empty() == this->with_auth_)
2343 return false;
2344#endif
2345
2346 // If channel configured, only match networks on that channel.
2347 if (config.has_channel() && config.get_channel() != this->channel_) {
2348 return false;
2349 }
2350 return true;
2351}
2352bool WiFiScanResult::get_matches() const { return this->matches_; }
2353void WiFiScanResult::set_matches(bool matches) { this->matches_ = matches; }
2354const bssid_t &WiFiScanResult::get_bssid() const { return this->bssid_; }
2355uint8_t WiFiScanResult::get_channel() const { return this->channel_; }
2356int8_t WiFiScanResult::get_rssi() const { return this->rssi_; }
2357bool WiFiScanResult::get_with_auth() const { return this->with_auth_; }
2358bool WiFiScanResult::get_is_hidden() const { return this->is_hidden_; }
2359
2360bool WiFiScanResult::operator==(const WiFiScanResult &rhs) const { return this->bssid_ == rhs.bssid_; }
2361
2363 this->roaming_attempts_ = 0;
2364 this->roaming_last_check_ = 0;
2365 this->roaming_scan_end_ = 0;
2366 this->roaming_target_bssid_ = {};
2368}
2369
2371 if (!this->keep_scan_results_) {
2372#if defined(USE_RP2040) || defined(USE_ESP32)
2373 // std::vector - use swap trick since shrink_to_fit is non-binding
2374 decltype(this->scan_result_)().swap(this->scan_result_);
2375#else
2376 // FixedVector::release() frees all memory
2377 this->scan_result_.release();
2378#endif
2379 }
2380}
2381
2382#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
2384 if (!this->pending_.connect_state)
2385 return;
2386 this->pending_.connect_state = false;
2387 // Get current SSID and BSSID from the WiFi driver
2388 char ssid_buf[SSID_BUFFER_SIZE];
2389 const char *ssid = this->wifi_ssid_to(ssid_buf);
2390 bssid_t bssid = this->wifi_bssid();
2391 for (auto *listener : this->connect_state_listeners_) {
2392 listener->on_wifi_connect_state(StringRef(ssid, strlen(ssid)), bssid);
2393 }
2394}
2395
2397 constexpr uint8_t empty_bssid[6] = {};
2398 for (auto *listener : this->connect_state_listeners_) {
2399 listener->on_wifi_connect_state(StringRef(), empty_bssid);
2400 }
2401}
2402#endif // USE_WIFI_CONNECT_STATE_LISTENERS
2403
2404#ifdef USE_WIFI_IP_STATE_LISTENERS
2406 for (auto *listener : this->ip_state_listeners_) {
2407 listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1));
2408 }
2409}
2410#endif // USE_WIFI_IP_STATE_LISTENERS
2411
2412#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS
2414 for (auto *listener : this->scan_results_listeners_) {
2415 listener->on_wifi_scan_results(this->scan_result_);
2416 }
2417}
2418#endif // USE_WIFI_SCAN_RESULTS_LISTENERS
2419
2421 // Guard: not for hidden networks (may not appear in scan)
2422 const WiFiAP *selected = this->get_selected_sta_();
2423 if (selected == nullptr || selected->get_hidden()) {
2424 this->roaming_attempts_ = ROAMING_MAX_ATTEMPTS; // Stop checking forever
2425 return;
2426 }
2427
2428 this->roaming_last_check_ = now;
2429 this->roaming_attempts_++;
2430
2431 // Guard: skip scan if signal is already good (no meaningful improvement possible)
2432 int8_t rssi = this->wifi_rssi();
2433 if (rssi > ROAMING_GOOD_RSSI) {
2434 ESP_LOGD(TAG, "Roam check skipped, signal good (%d dBm, attempt %u/%u)", rssi, this->roaming_attempts_,
2436 return;
2437 }
2438
2439 ESP_LOGD(TAG, "Roam scan (%d dBm, attempt %u/%u)", rssi, this->roaming_attempts_, ROAMING_MAX_ATTEMPTS);
2441 this->wifi_scan_start_(this->passive_scan_);
2442}
2443
2445 this->scan_done_ = false;
2446 // Default to IDLE - will be set to CONNECTING if we find a better AP
2448 // Record when scan completed so delayed disconnects (e.g., ESP8266 Beacon Timeout)
2449 // can be attributed to the scan and avoid resetting the attempts counter
2450 this->roaming_scan_end_ = millis();
2451
2452 // Get current connection info
2453 int8_t current_rssi = this->wifi_rssi();
2454 // Guard: must still be connected (RSSI may have become invalid during scan)
2455 if (current_rssi == WIFI_RSSI_DISCONNECTED) {
2456 this->release_scan_results_();
2457 return;
2458 }
2459
2460 char ssid_buf[SSID_BUFFER_SIZE];
2461 StringRef current_ssid(this->wifi_ssid_to(ssid_buf));
2462 bssid_t current_bssid = this->wifi_bssid();
2463
2464 // Find best candidate: same SSID, different BSSID
2465 const WiFiScanResult *best = nullptr;
2466 char bssid_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
2467
2468 for (const auto &result : this->scan_result_) {
2469 // Must be same SSID, different BSSID
2470 if (result.ssid_ != current_ssid || result.get_bssid() == current_bssid)
2471 continue;
2472
2473#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
2474 format_mac_addr_upper(result.get_bssid().data(), bssid_buf);
2475 ESP_LOGV(TAG, "Roam candidate %s %d dBm", bssid_buf, result.get_rssi());
2476#endif
2477
2478 // Track the best candidate
2479 if (best == nullptr || result.get_rssi() > best->get_rssi()) {
2480 best = &result;
2481 }
2482 }
2483
2484 // Check if best candidate meets minimum improvement threshold
2485 const WiFiAP *selected = this->get_selected_sta_();
2486 int8_t improvement = (best == nullptr) ? 0 : best->get_rssi() - current_rssi;
2487 if (selected == nullptr || improvement < ROAMING_MIN_IMPROVEMENT) {
2488 ESP_LOGV(TAG, "Roam best %+d dB (need +%d), attempt %u/%u", improvement, ROAMING_MIN_IMPROVEMENT,
2490 this->release_scan_results_();
2491 return;
2492 }
2493
2494 format_mac_addr_upper(best->get_bssid().data(), bssid_buf);
2495 ESP_LOGI(TAG, "Roaming to %s (%+d dB)", bssid_buf, improvement);
2496
2497 WiFiAP roam_params = *selected;
2498 apply_scan_result_to_params(roam_params, *best);
2499
2500 // Mark as roaming attempt - affects retry behavior if connection fails
2502 this->roaming_target_bssid_ = best->get_bssid(); // Must read before releasing scan results
2503
2504 this->release_scan_results_();
2505
2506 // Connect directly - wifi_sta_connect_ handles disconnect internally
2507 this->start_connecting(roam_params);
2508}
2509
2510WiFiComponent *global_wifi_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
2511
2512} // namespace esphome::wifi
2513#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().
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:306
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:482
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:187
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
std::string size_t len
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:751
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.