ESPHome 2025.11.0b4
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
6#ifdef USE_ESP32
7#if (ESP_IDF_VERSION_MAJOR >= 5 && ESP_IDF_VERSION_MINOR >= 1)
8#include <esp_eap_client.h>
9#else
10#include <esp_wpa2.h>
11#endif
12#endif
13
14#if defined(USE_ESP32)
15#include <esp_wifi.h>
16#endif
17#ifdef USE_ESP8266
18#include <user_interface.h>
19#endif
20
21#include <algorithm>
22#include <utility>
23#include "lwip/dns.h"
24#include "lwip/err.h"
25
27#include "esphome/core/hal.h"
29#include "esphome/core/log.h"
30#include "esphome/core/util.h"
31
32#ifdef USE_CAPTIVE_PORTAL
34#endif
35
36#ifdef USE_IMPROV
38#endif
39
40namespace esphome {
41namespace wifi {
42
43static const char *const TAG = "wifi";
44
146
147static const LogString *retry_phase_to_log_string(WiFiRetryPhase phase) {
148 switch (phase) {
150 return LOG_STR("INITIAL_CONNECT");
151#ifdef USE_WIFI_FAST_CONNECT
153 return LOG_STR("FAST_CONNECT_CYCLING");
154#endif
156 return LOG_STR("EXPLICIT_HIDDEN");
158 return LOG_STR("SCAN_CONNECTING");
160 return LOG_STR("RETRY_HIDDEN");
162 return LOG_STR("RESTARTING");
163 default:
164 return LOG_STR("UNKNOWN");
165 }
166}
167
169 // If first configured network is marked hidden, we went through EXPLICIT_HIDDEN phase
170 // This means those networks were already tried and should be skipped in RETRY_HIDDEN
171 return !this->sta_.empty() && this->sta_[0].get_hidden();
172}
173
175 // Find the first network that is NOT marked hidden:true
176 // This is where EXPLICIT_HIDDEN phase would have stopped
177 for (size_t i = 0; i < this->sta_.size(); i++) {
178 if (!this->sta_[i].get_hidden()) {
179 return static_cast<int8_t>(i);
180 }
181 }
182 return -1; // All networks are hidden
183}
184
185// 2 attempts per BSSID in SCAN_CONNECTING phase
186// Rationale: This is the ONLY phase where we decrease BSSID priority, so we must be very sure.
187// Auth failures are common immediately after scan due to WiFi stack state transitions.
188// Trying twice filters out false positives and prevents unnecessarily marking a good BSSID as bad.
189// After 2 genuine failures, priority degradation ensures we skip this BSSID on subsequent scans.
190static constexpr uint8_t WIFI_RETRY_COUNT_PER_BSSID = 2;
191
192// 1 attempt per SSID in RETRY_HIDDEN phase
193// Rationale: Try hidden mode once, then rescan to get next best BSSID via priority system
194static constexpr uint8_t WIFI_RETRY_COUNT_PER_SSID = 1;
195
196// 1 attempt per AP in fast_connect mode (INITIAL_CONNECT and FAST_CONNECT_CYCLING_APS)
197// Rationale: Fast connect prioritizes speed - try each AP once to find a working one quickly
198static constexpr uint8_t WIFI_RETRY_COUNT_PER_AP = 1;
199
202static constexpr uint32_t WIFI_COOLDOWN_DURATION_MS = 500;
203
207static constexpr uint32_t WIFI_COOLDOWN_WITH_AP_ACTIVE_MS = 30000;
208
209static constexpr uint8_t get_max_retries_for_phase(WiFiRetryPhase phase) {
210 switch (phase) {
212#ifdef USE_WIFI_FAST_CONNECT
214#endif
215 // INITIAL_CONNECT and FAST_CONNECT_CYCLING_APS both use 1 attempt per AP (fast_connect mode)
216 return WIFI_RETRY_COUNT_PER_AP;
218 // Explicitly hidden network: 1 attempt (user marked as hidden, try once then scan)
219 return WIFI_RETRY_COUNT_PER_SSID;
221 // Scan-based phase: 2 attempts per BSSID (handles transient auth failures after scan)
222 return WIFI_RETRY_COUNT_PER_BSSID;
224 // Hidden network mode: 1 attempt per SSID
225 return WIFI_RETRY_COUNT_PER_SSID;
226 default:
227 return WIFI_RETRY_COUNT_PER_BSSID;
228 }
229}
230
231static void apply_scan_result_to_params(WiFiAP &params, const WiFiScanResult &scan) {
232 params.set_hidden(false);
233 params.set_ssid(scan.get_ssid());
234 params.set_bssid(scan.get_bssid());
235 params.set_channel(scan.get_channel());
236}
237
239 // Only SCAN_CONNECTING phase needs scan results
241 return false;
242 }
243 // Need scan if we have no results or no matching networks
244 return this->scan_result_.empty() || !this->scan_result_[0].get_matches();
245}
246
247bool WiFiComponent::ssid_was_seen_in_scan_(const std::string &ssid) const {
248 // Check if this SSID is configured as hidden
249 // If explicitly marked hidden, we should always try hidden mode regardless of scan results
250 for (const auto &conf : this->sta_) {
251 if (conf.get_ssid() == ssid && conf.get_hidden()) {
252 return false; // Treat as not seen - force hidden mode attempt
253 }
254 }
255
256 // Otherwise, check if we saw it in scan results
257 for (const auto &scan : this->scan_result_) {
258 if (scan.get_ssid() == ssid) {
259 return true;
260 }
261 }
262 return false;
263}
264
265int8_t WiFiComponent::find_next_hidden_sta_(int8_t start_index) {
266 // Find next SSID that wasn't in scan results (might be hidden)
267 bool include_explicit_hidden = !this->went_through_explicit_hidden_phase_();
268 // Start searching from start_index + 1
269 for (size_t i = start_index + 1; i < this->sta_.size(); i++) {
270 const auto &sta = this->sta_[i];
271
272 // Skip networks that were already tried in EXPLICIT_HIDDEN phase
273 // Those are: networks marked hidden:true that appear before the first non-hidden network
274 // If all networks are hidden (first_non_hidden_idx == -1), skip all of them
275 if (!include_explicit_hidden && sta.get_hidden()) {
276 int8_t first_non_hidden_idx = this->find_first_non_hidden_index_();
277 if (first_non_hidden_idx < 0 || static_cast<int8_t>(i) < first_non_hidden_idx) {
278 ESP_LOGD(TAG, "Skipping " LOG_SECRET("'%s'") " (explicit hidden, already tried)", sta.get_ssid().c_str());
279 continue;
280 }
281 }
282
283 // If we didn't scan this cycle, treat all networks as potentially hidden
284 // Otherwise, only retry networks that weren't seen in the scan
285 if (!this->did_scan_this_cycle_ || !this->ssid_was_seen_in_scan_(sta.get_ssid())) {
286 ESP_LOGD(TAG, "Hidden candidate " LOG_SECRET("'%s'") " at index %d", sta.get_ssid().c_str(), static_cast<int>(i));
287 return static_cast<int8_t>(i);
288 }
289 ESP_LOGD(TAG, "Skipping hidden retry for visible network " LOG_SECRET("'%s'"), sta.get_ssid().c_str());
290 }
291 // No hidden SSIDs found
292 return -1;
293}
294
296 // If first network (highest priority) is explicitly marked hidden, try it first before scanning
297 // This respects user's priority order when they explicitly configure hidden networks
298 if (!this->sta_.empty() && this->sta_[0].get_hidden()) {
299 ESP_LOGI(TAG, "Starting with explicit hidden network (highest priority)");
300 this->selected_sta_index_ = 0;
303 this->start_connecting(params);
304 } else {
305 ESP_LOGI(TAG, "Starting scan");
306 this->start_scanning();
307 }
308}
309
310#if defined(USE_ESP32) && defined(USE_WIFI_WPA2_EAP) && ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
311static const char *eap_phase2_to_str(esp_eap_ttls_phase2_types type) {
312 switch (type) {
313 case ESP_EAP_TTLS_PHASE2_PAP:
314 return "pap";
315 case ESP_EAP_TTLS_PHASE2_CHAP:
316 return "chap";
317 case ESP_EAP_TTLS_PHASE2_MSCHAP:
318 return "mschap";
319 case ESP_EAP_TTLS_PHASE2_MSCHAPV2:
320 return "mschapv2";
321 case ESP_EAP_TTLS_PHASE2_EAP:
322 return "eap";
323 default:
324 return "unknown";
325 }
326}
327#endif
328
330
332 this->wifi_pre_setup_();
333 if (this->enable_on_boot_) {
334 this->start();
335 } else {
336#ifdef USE_ESP32
337 esp_netif_init();
338#endif
340 }
341}
342
344 ESP_LOGCONFIG(TAG,
345 "Starting\n"
346 " Local MAC: %s",
347 get_mac_address_pretty().c_str());
348 this->last_connected_ = millis();
349
350 uint32_t hash = this->has_sta() ? fnv1_hash(App.get_compilation_time()) : 88491487UL;
351
353#ifdef USE_WIFI_FAST_CONNECT
355#endif
356
357 SavedWifiSettings save{};
358 if (this->pref_.load(&save)) {
359 ESP_LOGD(TAG, "Loaded settings: %s", save.ssid);
360
361 WiFiAP sta{};
362 sta.set_ssid(save.ssid);
363 sta.set_password(save.password);
364 this->set_sta(sta);
365 }
366
367 if (this->has_sta()) {
368 this->wifi_sta_pre_setup_();
369 if (this->output_power_.has_value() && !this->wifi_apply_output_power_(*this->output_power_)) {
370 ESP_LOGV(TAG, "Setting Output Power Option failed");
371 }
372
373 if (!this->wifi_apply_power_save_()) {
374 ESP_LOGV(TAG, "Setting Power Save Option failed");
375 }
376
378#ifdef USE_WIFI_FAST_CONNECT
379 WiFiAP params;
380 bool loaded_fast_connect = this->load_fast_connect_settings_(params);
381 // Fast connect optimization: only use when we have saved BSSID+channel data
382 // Without saved data, try first configured network or use normal flow
383 if (loaded_fast_connect) {
384 ESP_LOGI(TAG, "Starting fast_connect (saved) " LOG_SECRET("'%s'"), params.get_ssid().c_str());
385 this->start_connecting(params);
386 } else if (!this->sta_.empty() && !this->sta_[0].get_hidden()) {
387 // No saved data, but have configured networks - try first non-hidden network
388 ESP_LOGI(TAG, "Starting fast_connect (config) " LOG_SECRET("'%s'"), this->sta_[0].get_ssid().c_str());
389 this->selected_sta_index_ = 0;
390 params = this->build_params_for_current_phase_();
391 this->start_connecting(params);
392 } else {
393 // No saved data and (no networks OR first is hidden) - use normal flow
395 }
396#else
397 // Without fast_connect: go straight to scanning (or hidden mode if all networks are hidden)
399#endif
400#ifdef USE_WIFI_AP
401 } else if (this->has_ap()) {
402 this->setup_ap_config_();
403 if (this->output_power_.has_value() && !this->wifi_apply_output_power_(*this->output_power_)) {
404 ESP_LOGV(TAG, "Setting Output Power Option failed");
405 }
406#ifdef USE_CAPTIVE_PORTAL
408 this->wifi_sta_pre_setup_();
409 this->start_scanning();
411 }
412#endif
413#endif // USE_WIFI_AP
414 }
415#ifdef USE_IMPROV
416 if (!this->has_sta() && esp32_improv::global_improv_component != nullptr) {
417 if (this->wifi_mode_(true, {}))
419 }
420#endif
421 this->wifi_apply_hostname_();
422}
423
425 ESP_LOGW(TAG, "Restarting adapter");
426 this->wifi_mode_(false, {});
427 this->error_from_callback_ = false;
428}
429
431 this->wifi_loop_();
432 const uint32_t now = App.get_loop_component_start_time();
433
434 if (this->has_sta()) {
435 if (this->is_connected() != this->handled_connected_state_) {
436 if (this->handled_connected_state_) {
438 } else {
439 this->connect_trigger_->trigger();
440 }
442 }
443
444 switch (this->state_) {
446 this->status_set_warning(LOG_STR("waiting to reconnect"));
447 // Skip cooldown if new credentials were provided while connecting
448 if (this->skip_cooldown_next_cycle_) {
449 this->skip_cooldown_next_cycle_ = false;
451 break;
452 }
453 // Use longer cooldown when captive portal/improv is active to avoid disrupting user config
454 bool portal_active = this->is_captive_portal_active_() || this->is_esp32_improv_active_();
455 uint32_t cooldown_duration = portal_active ? WIFI_COOLDOWN_WITH_AP_ACTIVE_MS : WIFI_COOLDOWN_DURATION_MS;
456 if (now - this->action_started_ > cooldown_duration) {
457 // After cooldown we either restarted the adapter because of
458 // a failure, or something tried to connect over and over
459 // so we entered cooldown. In both cases we call
460 // check_connecting_finished to continue the state machine.
462 }
463 break;
464 }
466 this->status_set_warning(LOG_STR("scanning for networks"));
468 break;
469 }
471 this->status_set_warning(LOG_STR("associating to network"));
473 break;
474 }
475
477 if (!this->is_connected()) {
478 ESP_LOGW(TAG, "Connection lost; reconnecting");
480 // Clear error flag before reconnecting so first attempt is not seen as immediate failure
481 this->error_from_callback_ = false;
482 this->retry_connect();
483 } else {
484 this->status_clear_warning();
485 this->last_connected_ = now;
486 }
487 break;
488 }
491 break;
493 return;
494 }
495
496#ifdef USE_WIFI_AP
497 if (this->has_ap() && !this->ap_setup_) {
498 if (this->ap_timeout_ != 0 && (now - this->last_connected_ > this->ap_timeout_)) {
499 ESP_LOGI(TAG, "Starting fallback AP");
500 this->setup_ap_config_();
501#ifdef USE_CAPTIVE_PORTAL
504#endif
505 }
506 }
507#endif // USE_WIFI_AP
508
509#ifdef USE_IMPROV
511 !esp32_improv::global_improv_component->should_start()) {
512 if (now - this->last_connected_ > esp32_improv::global_improv_component->get_wifi_timeout()) {
513 if (this->wifi_mode_(true, {}))
515 }
516 }
517
518#endif
519
520 if (!this->has_ap() && this->reboot_timeout_ != 0) {
521 if (now - this->last_connected_ > this->reboot_timeout_) {
522 ESP_LOGE(TAG, "Can't connect; rebooting");
523 App.reboot();
524 }
525 }
526 }
527}
528
530
531bool WiFiComponent::has_ap() const { return this->has_ap_; }
532bool WiFiComponent::has_sta() const { return !this->sta_.empty(); }
533#ifdef USE_WIFI_11KV_SUPPORT
534void WiFiComponent::set_btm(bool btm) { this->btm_ = btm; }
535void WiFiComponent::set_rrm(bool rrm) { this->rrm_ = rrm; }
536#endif
538 if (this->has_sta())
539 return this->wifi_sta_ip_addresses();
540
541#ifdef USE_WIFI_AP
542 if (this->has_ap())
543 return {this->wifi_soft_ap_ip()};
544#endif // USE_WIFI_AP
545
546 return {};
547}
549 if (this->has_sta())
550 return this->wifi_dns_ip_(num);
551 return {};
552}
553// set_use_address() is guaranteed to be called during component setup by Python code generation,
554// so use_address_ will always be valid when get_use_address() is called - no fallback needed.
555const char *WiFiComponent::get_use_address() const { return this->use_address_; }
556void WiFiComponent::set_use_address(const char *use_address) { this->use_address_ = use_address; }
557
558#ifdef USE_WIFI_AP
560 this->wifi_mode_({}, true);
561
562 if (this->ap_setup_)
563 return;
564
565 if (this->ap_.get_ssid().empty()) {
566 std::string name = App.get_name();
567 if (name.length() > 32) {
569 // Keep first 25 chars and last 7 chars (MAC suffix), remove middle
570 name.erase(25, name.length() - 32);
571 } else {
572 name.resize(32);
573 }
574 }
575 this->ap_.set_ssid(name);
576 }
577 this->ap_setup_ = this->wifi_start_ap_(this->ap_);
578
579 auto ip_address = this->wifi_soft_ap_ip().str();
580 ESP_LOGCONFIG(TAG,
581 "Setting up AP:\n"
582 " AP SSID: '%s'\n"
583 " AP Password: '%s'\n"
584 " IP Address: %s",
585 this->ap_.get_ssid().c_str(), this->ap_.get_password().c_str(), ip_address.c_str());
586
587#ifdef USE_WIFI_MANUAL_IP
588 auto manual_ip = this->ap_.get_manual_ip();
589 if (manual_ip.has_value()) {
590 ESP_LOGCONFIG(TAG,
591 " AP Static IP: '%s'\n"
592 " AP Gateway: '%s'\n"
593 " AP Subnet: '%s'",
594 manual_ip->static_ip.str().c_str(), manual_ip->gateway.str().c_str(),
595 manual_ip->subnet.str().c_str());
596 }
597#endif
598
599 if (!this->has_sta()) {
601 }
602}
603
605 this->ap_ = ap;
606 this->has_ap_ = true;
607}
608#endif // USE_WIFI_AP
609
611 return 10.0f; // before other loop components
612}
613
614void WiFiComponent::init_sta(size_t count) { this->sta_.init(count); }
615void WiFiComponent::add_sta(const WiFiAP &ap) { this->sta_.push_back(ap); }
617 this->clear_sta();
618 this->init_sta(1);
619 this->add_sta(ap);
620 this->selected_sta_index_ = 0;
621 // When new credentials are set (e.g., from improv), skip cooldown to retry immediately
622 this->skip_cooldown_next_cycle_ = true;
623}
624
626 const WiFiAP *config = this->get_selected_sta_();
627 if (config == nullptr) {
628 ESP_LOGE(TAG, "No valid network config (selected_sta_index_=%d, sta_.size()=%zu)",
629 static_cast<int>(this->selected_sta_index_), this->sta_.size());
630 // Return empty params - caller should handle this gracefully
631 return WiFiAP();
632 }
633
634 WiFiAP params = *config;
635
636 switch (this->retry_phase_) {
638#ifdef USE_WIFI_FAST_CONNECT
640#endif
641 // Fast connect phases: use config-only (no scan results)
642 // BSSID/channel from config if user specified them, otherwise empty
643 break;
644
647 // Hidden network mode: clear BSSID/channel to trigger probe request
648 // (both explicit hidden and retry hidden use same behavior)
651 break;
652
654 // Scan-based phase: always use best scan result (index 0 - highest priority after sorting)
655 if (!this->scan_result_.empty()) {
656 apply_scan_result_to_params(params, this->scan_result_[0]);
657 }
658 break;
659
661 // Should not be building params during restart
662 break;
663 }
664
665 return params;
666}
667
669 const WiFiAP *config = this->get_selected_sta_();
670 return config ? *config : WiFiAP{};
671}
672void WiFiComponent::save_wifi_sta(const std::string &ssid, const std::string &password) {
673 SavedWifiSettings save{}; // zero-initialized - all bytes set to \0, guaranteeing null termination
674 strncpy(save.ssid, ssid.c_str(), sizeof(save.ssid) - 1); // max 32 chars, byte 32 remains \0
675 strncpy(save.password, password.c_str(), sizeof(save.password) - 1); // max 64 chars, byte 64 remains \0
676 this->pref_.save(&save);
677 // ensure it's written immediately
679
680 WiFiAP sta{};
681 sta.set_ssid(ssid);
682 sta.set_password(password);
683 this->set_sta(sta);
684
685 // Trigger connection attempt (exits cooldown if needed, no-op if already connecting/connected)
686 this->connect_soon_();
687}
688
690 // Only trigger retry if we're in cooldown - if already connecting/connected, do nothing
692 ESP_LOGD(TAG, "Exiting cooldown early due to new WiFi credentials");
693 this->retry_connect();
694 }
695}
696
698 // Log connection attempt at INFO level with priority
699 std::string bssid_formatted;
700 int8_t priority = 0;
701
702 if (ap.get_bssid().has_value()) {
703 bssid_formatted = format_mac_address_pretty(ap.get_bssid().value().data());
705 }
706
707 ESP_LOGI(TAG,
708 "Connecting to " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") " (priority %d, attempt %u/%u in phase %s)...",
709 ap.get_ssid().c_str(), ap.get_bssid().has_value() ? bssid_formatted.c_str() : LOG_STR_LITERAL("any"),
710 priority, this->num_retried_ + 1, get_max_retries_for_phase(this->retry_phase_),
711 LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_)));
712
713#ifdef ESPHOME_LOG_HAS_VERBOSE
714 ESP_LOGV(TAG, "Connection Params:");
715 ESP_LOGV(TAG, " SSID: '%s'", ap.get_ssid().c_str());
716 if (ap.get_bssid().has_value()) {
717 ESP_LOGV(TAG, " BSSID: %s", format_mac_address_pretty(ap.get_bssid()->data()).c_str());
718 } else {
719 ESP_LOGV(TAG, " BSSID: Not Set");
720 }
721
722#ifdef USE_WIFI_WPA2_EAP
723 if (ap.get_eap().has_value()) {
724 ESP_LOGV(TAG, " WPA2 Enterprise authentication configured:");
725 EAPAuth eap_config = ap.get_eap().value();
726 ESP_LOGV(TAG, " Identity: " LOG_SECRET("'%s'"), eap_config.identity.c_str());
727 ESP_LOGV(TAG, " Username: " LOG_SECRET("'%s'"), eap_config.username.c_str());
728 ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), eap_config.password.c_str());
729#if defined(USE_ESP32) && defined(USE_WIFI_WPA2_EAP) && ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
730 ESP_LOGV(TAG, " TTLS Phase 2: " LOG_SECRET("'%s'"), eap_phase2_to_str(eap_config.ttls_phase_2));
731#endif
732 bool ca_cert_present = eap_config.ca_cert != nullptr && strlen(eap_config.ca_cert);
733 bool client_cert_present = eap_config.client_cert != nullptr && strlen(eap_config.client_cert);
734 bool client_key_present = eap_config.client_key != nullptr && strlen(eap_config.client_key);
735 ESP_LOGV(TAG, " CA Cert: %s", ca_cert_present ? "present" : "not present");
736 ESP_LOGV(TAG, " Client Cert: %s", client_cert_present ? "present" : "not present");
737 ESP_LOGV(TAG, " Client Key: %s", client_key_present ? "present" : "not present");
738 } else {
739#endif
740 ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), ap.get_password().c_str());
741#ifdef USE_WIFI_WPA2_EAP
742 }
743#endif
744 if (ap.get_channel().has_value()) {
745 ESP_LOGV(TAG, " Channel: %u", *ap.get_channel());
746 } else {
747 ESP_LOGV(TAG, " Channel not set");
748 }
749#ifdef USE_WIFI_MANUAL_IP
750 if (ap.get_manual_ip().has_value()) {
751 ManualIP m = *ap.get_manual_ip();
752 ESP_LOGV(TAG, " Manual IP: Static IP=%s Gateway=%s Subnet=%s DNS1=%s DNS2=%s", m.static_ip.str().c_str(),
753 m.gateway.str().c_str(), m.subnet.str().c_str(), m.dns1.str().c_str(), m.dns2.str().c_str());
754 } else
755#endif
756 {
757 ESP_LOGV(TAG, " Using DHCP IP");
758 }
759 ESP_LOGV(TAG, " Hidden: %s", YESNO(ap.get_hidden()));
760#endif
761
762 if (!this->wifi_sta_connect_(ap)) {
763 ESP_LOGE(TAG, "wifi_sta_connect_ failed");
764 // Enter cooldown to allow WiFi hardware to stabilize
765 // (immediate failure suggests hardware not ready, different from connection timeout)
767 } else {
769 }
770 this->action_started_ = millis();
771}
772
773const LogString *get_signal_bars(int8_t rssi) {
774 // Check for disconnected sentinel value first
775 if (rssi == WIFI_RSSI_DISCONNECTED) {
776 // MULTIPLICATION SIGN
777 // Unicode: U+00D7, UTF-8: C3 97
778 return LOG_STR("\033[0;31m" // red
779 "\xc3\x97\xc3\x97\xc3\x97\xc3\x97"
780 "\033[0m");
781 }
782 // LOWER ONE QUARTER BLOCK
783 // Unicode: U+2582, UTF-8: E2 96 82
784 // LOWER HALF BLOCK
785 // Unicode: U+2584, UTF-8: E2 96 84
786 // LOWER THREE QUARTERS BLOCK
787 // Unicode: U+2586, UTF-8: E2 96 86
788 // FULL BLOCK
789 // Unicode: U+2588, UTF-8: E2 96 88
790 if (rssi >= -50) {
791 return LOG_STR("\033[0;32m" // green
792 "\xe2\x96\x82"
793 "\xe2\x96\x84"
794 "\xe2\x96\x86"
795 "\xe2\x96\x88"
796 "\033[0m");
797 } else if (rssi >= -65) {
798 return LOG_STR("\033[0;33m" // yellow
799 "\xe2\x96\x82"
800 "\xe2\x96\x84"
801 "\xe2\x96\x86"
802 "\033[0;37m"
803 "\xe2\x96\x88"
804 "\033[0m");
805 } else if (rssi >= -85) {
806 return LOG_STR("\033[0;33m" // yellow
807 "\xe2\x96\x82"
808 "\xe2\x96\x84"
809 "\033[0;37m"
810 "\xe2\x96\x86"
811 "\xe2\x96\x88"
812 "\033[0m");
813 } else {
814 return LOG_STR("\033[0;31m" // red
815 "\xe2\x96\x82"
816 "\033[0;37m"
817 "\xe2\x96\x84"
818 "\xe2\x96\x86"
819 "\xe2\x96\x88"
820 "\033[0m");
821 }
822}
823
825 bssid_t bssid = wifi_bssid();
826
827 ESP_LOGCONFIG(TAG, " Local MAC: %s", get_mac_address_pretty().c_str());
828 if (this->is_disabled()) {
829 ESP_LOGCONFIG(TAG, " Disabled");
830 return;
831 }
832 for (auto &ip : wifi_sta_ip_addresses()) {
833 if (ip.is_set()) {
834 ESP_LOGCONFIG(TAG, " IP Address: %s", ip.str().c_str());
835 }
836 }
837 int8_t rssi = wifi_rssi();
838 ESP_LOGCONFIG(TAG,
839 " SSID: " LOG_SECRET("'%s'") "\n"
840 " BSSID: " LOG_SECRET("%s") "\n"
841 " Hostname: '%s'\n"
842 " Signal strength: %d dB %s\n"
843 " Channel: %" PRId32 "\n"
844 " Subnet: %s\n"
845 " Gateway: %s\n"
846 " DNS1: %s\n"
847 " DNS2: %s",
848 wifi_ssid().c_str(), format_mac_address_pretty(bssid.data()).c_str(), App.get_name().c_str(), rssi,
849 LOG_STR_ARG(get_signal_bars(rssi)), get_wifi_channel(), wifi_subnet_mask_().str().c_str(),
850 wifi_gateway_ip_().str().c_str(), wifi_dns_ip_(0).str().c_str(), wifi_dns_ip_(1).str().c_str());
851#ifdef ESPHOME_LOG_HAS_VERBOSE
852 if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_bssid().has_value()) {
853 ESP_LOGV(TAG, " Priority: %d", this->get_sta_priority(*config->get_bssid()));
854 }
855#endif
856#ifdef USE_WIFI_11KV_SUPPORT
857 ESP_LOGCONFIG(TAG,
858 " BTM: %s\n"
859 " RRM: %s",
860 this->btm_ ? "enabled" : "disabled", this->rrm_ ? "enabled" : "disabled");
861#endif
862}
863
866 return;
867
868 ESP_LOGD(TAG, "Enabling");
869 this->error_from_callback_ = false;
871 this->start();
872}
873
876 return;
877
878 ESP_LOGD(TAG, "Disabling");
880 this->wifi_disconnect_();
881 this->wifi_mode_(false, false);
882}
883
885
887 this->action_started_ = millis();
888 ESP_LOGD(TAG, "Starting scan");
889 this->wifi_scan_start_(this->passive_scan_);
891}
892
926[[nodiscard]] inline static bool wifi_scan_result_is_better(const WiFiScanResult &a, const WiFiScanResult &b) {
927 // Matching networks always come before non-matching
928 if (a.get_matches() && !b.get_matches())
929 return true;
930 if (!a.get_matches() && b.get_matches())
931 return false;
932
933 // Both matching: check priority first (tracks connection failures via priority degradation)
934 // Priority is decreased when a BSSID fails to connect, so lower priority = previously failed
935 if (a.get_matches() && b.get_matches() && a.get_priority() != b.get_priority()) {
936 return a.get_priority() > b.get_priority();
937 }
938
939 // Use RSSI as tiebreaker (for equal-priority matching networks or all non-matching networks)
940 return a.get_rssi() > b.get_rssi();
941}
942
943// Helper function for insertion sort of WiFi scan results
944// Using insertion sort instead of std::stable_sort saves flash memory
945// by avoiding template instantiations (std::rotate, std::stable_sort, lambdas)
946// IMPORTANT: This sort is stable (preserves relative order of equal elements)
947template<typename VectorType> static void insertion_sort_scan_results(VectorType &results) {
948 const size_t size = results.size();
949 for (size_t i = 1; i < size; i++) {
950 // Make a copy to avoid issues with move semantics during comparison
951 WiFiScanResult key = results[i];
952 int32_t j = i - 1;
953
954 // Move elements that are worse than key to the right
955 // For stability, we only move if key is strictly better than results[j]
956 while (j >= 0 && wifi_scan_result_is_better(key, results[j])) {
957 results[j + 1] = results[j];
958 j--;
959 }
960 results[j + 1] = key;
961 }
962}
963
964// Helper function to log scan results - marked noinline to prevent re-inlining into loop
965__attribute__((noinline)) static void log_scan_result(const WiFiScanResult &res) {
966 char bssid_s[18];
967 auto bssid = res.get_bssid();
968 format_mac_addr_upper(bssid.data(), bssid_s);
969
970 if (res.get_matches()) {
971 ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(),
972 res.get_is_hidden() ? LOG_STR_LITERAL("(HIDDEN) ") : LOG_STR_LITERAL(""), bssid_s,
973 LOG_STR_ARG(get_signal_bars(res.get_rssi())));
974 ESP_LOGD(TAG, " Channel: %2u, RSSI: %3d dB, Priority: %4d", res.get_channel(), res.get_rssi(), res.get_priority());
975 } else {
976 ESP_LOGD(TAG, "- " LOG_SECRET("'%s'") " " LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), bssid_s,
977 LOG_STR_ARG(get_signal_bars(res.get_rssi())));
978 }
979}
980
982 if (!this->scan_done_) {
983 if (millis() - this->action_started_ > 30000) {
984 ESP_LOGE(TAG, "Scan timeout");
985 this->retry_connect();
986 }
987 return;
988 }
989 this->scan_done_ = false;
990 this->did_scan_this_cycle_ = true;
991
992 if (this->scan_result_.empty()) {
993 ESP_LOGW(TAG, "No networks found");
994 this->retry_connect();
995 return;
996 }
997
998 ESP_LOGD(TAG, "Found networks:");
999 for (auto &res : this->scan_result_) {
1000 for (auto &ap : this->sta_) {
1001 if (res.matches(ap)) {
1002 res.set_matches(true);
1003 // Cache priority lookup - do single search instead of 2 separate searches
1004 const bssid_t &bssid = res.get_bssid();
1005 if (!this->has_sta_priority(bssid)) {
1006 this->set_sta_priority(bssid, ap.get_priority());
1007 }
1008 res.set_priority(this->get_sta_priority(bssid));
1009 break;
1010 }
1011 }
1012 }
1013
1014 // Sort scan results using insertion sort for better memory efficiency
1015 insertion_sort_scan_results(this->scan_result_);
1016
1017 for (auto &res : this->scan_result_) {
1018 log_scan_result(res);
1019 }
1020
1021 // SYNCHRONIZATION POINT: Establish link between scan_result_[0] and selected_sta_index_
1022 // After sorting, scan_result_[0] contains the best network. Now find which sta_[i] config
1023 // matches that network and record it in selected_sta_index_. This keeps the two indices
1024 // synchronized so build_params_for_current_phase_() can safely use both to build connection parameters.
1025 const WiFiScanResult &scan_res = this->scan_result_[0];
1026 bool found_match = false;
1027 if (scan_res.get_matches()) {
1028 for (size_t i = 0; i < this->sta_.size(); i++) {
1029 if (scan_res.matches(this->sta_[i])) {
1030 // Safe cast: sta_.size() limited to MAX_WIFI_NETWORKS (127) in __init__.py validation
1031 // No overflow check needed - YAML validation prevents >127 networks
1032 this->selected_sta_index_ = static_cast<int8_t>(i); // Links scan_result_[0] with sta_[i]
1033 found_match = true;
1034 break;
1035 }
1036 }
1037 }
1038
1039 if (!found_match) {
1040 ESP_LOGW(TAG, "No matching network found");
1041 // No scan results matched our configured networks - transition directly to hidden mode
1042 // Don't call retry_connect() since we never attempted a connection (no BSSID to penalize)
1044 // If no hidden networks to try, skip connection attempt (will be handled on next loop)
1045 if (this->selected_sta_index_ == -1) {
1046 return;
1047 }
1048 // Now start connection attempt in hidden mode
1050 return; // scan started, wait for next loop iteration
1051 }
1052
1053 yield();
1054
1055 WiFiAP params = this->build_params_for_current_phase_();
1056 // Ensure we're in SCAN_CONNECTING phase when connecting with scan results
1057 // (needed when scan was started directly without transition_to_phase_, e.g., initial scan)
1058 this->start_connecting(params);
1059}
1060
1062 ESP_LOGCONFIG(TAG,
1063 "WiFi:\n"
1064 " Connected: %s",
1065 YESNO(this->is_connected()));
1066 this->print_connect_params_();
1067}
1068
1070 auto status = this->wifi_sta_connect_status_();
1071
1073 if (wifi_ssid().empty()) {
1074 ESP_LOGW(TAG, "Connection incomplete");
1075 this->retry_connect();
1076 return;
1077 }
1078
1079 ESP_LOGI(TAG, "Connected");
1080 // Warn if we had to retry with hidden network mode for a network that's not marked hidden
1081 // Only warn if we actually connected without scan data (SSID only), not if scan succeeded on retry
1082 if (const WiFiAP *config = this->get_selected_sta_(); this->retry_phase_ == WiFiRetryPhase::RETRY_HIDDEN &&
1083 config && !config->get_hidden() &&
1084 this->scan_result_.empty()) {
1085 ESP_LOGW(TAG, LOG_SECRET("'%s'") " should be marked hidden", config->get_ssid().c_str());
1086 }
1087 // Reset to initial phase on successful connection (don't log transition, just reset state)
1089 this->num_retried_ = 0;
1090 // Ensure next connection attempt does not inherit error state
1091 // so when WiFi disconnects later we start fresh and don't see
1092 // the first connection as a failure.
1093 this->error_from_callback_ = false;
1094
1095 this->print_connect_params_();
1096
1097 if (this->has_ap()) {
1098#ifdef USE_CAPTIVE_PORTAL
1099 if (this->is_captive_portal_active_()) {
1101 }
1102#endif
1103 ESP_LOGD(TAG, "Disabling AP");
1104 this->wifi_mode_({}, false);
1105 }
1106#ifdef USE_IMPROV
1107 if (this->is_esp32_improv_active_()) {
1109 }
1110#endif
1111
1113 this->num_retried_ = 0;
1114
1115 // Clear priority tracking if all priorities are at minimum
1117
1118#ifdef USE_WIFI_FAST_CONNECT
1120#endif
1121
1122 // Free scan results memory unless a component needs them
1123 if (!this->keep_scan_results_) {
1124 this->scan_result_.clear();
1125 this->scan_result_.shrink_to_fit();
1126 }
1127
1128 return;
1129 }
1130
1131 uint32_t now = millis();
1132 if (now - this->action_started_ > 30000) {
1133 ESP_LOGW(TAG, "Connection timeout");
1134 this->retry_connect();
1135 return;
1136 }
1137
1138 if (this->error_from_callback_) {
1139 ESP_LOGW(TAG, "Connecting to network failed (callback)");
1140 this->retry_connect();
1141 return;
1142 }
1143
1145 return;
1146 }
1147
1149 ESP_LOGW(TAG, "Network no longer found");
1150 this->retry_connect();
1151 return;
1152 }
1153
1155 ESP_LOGW(TAG, "Connecting to network failed");
1156 this->retry_connect();
1157 return;
1158 }
1159
1160 ESP_LOGW(TAG, "Unknown connection status %d", (int) status);
1161 this->retry_connect();
1162}
1163
1171 switch (this->retry_phase_) {
1173#ifdef USE_WIFI_FAST_CONNECT
1175 // INITIAL_CONNECT and FAST_CONNECT_CYCLING_APS: no retries, try next AP or fall back to scan
1176 if (this->selected_sta_index_ < static_cast<int8_t>(this->sta_.size()) - 1) {
1177 return WiFiRetryPhase::FAST_CONNECT_CYCLING_APS; // Move to next AP
1178 }
1179#endif
1180 // Check if we should try explicit hidden networks before scanning
1181 // This handles reconnection after connection loss where first network is hidden
1182 if (!this->sta_.empty() && this->sta_[0].get_hidden()) {
1184 }
1185 // No more APs to try, fall back to scan
1187
1189 // Try all explicitly hidden networks before scanning
1190 if (this->num_retried_ + 1 < WIFI_RETRY_COUNT_PER_SSID) {
1191 return WiFiRetryPhase::EXPLICIT_HIDDEN; // Keep retrying same SSID
1192 }
1193
1194 // Exhausted retries on current SSID - check for more explicitly hidden networks
1195 // Stop when we reach a visible network (proceed to scanning)
1196 size_t next_index = this->selected_sta_index_ + 1;
1197 if (next_index < this->sta_.size() && this->sta_[next_index].get_hidden()) {
1198 // Found another explicitly hidden network
1200 }
1201
1202 // No more consecutive explicitly hidden networks
1203 // If ALL networks are hidden, skip scanning and go directly to restart
1204 if (this->find_first_non_hidden_index_() < 0) {
1206 }
1207 // Otherwise proceed to scanning for non-hidden networks
1209 }
1210
1212 // If scan found no matching networks, skip to hidden network mode
1213 if (!this->scan_result_.empty() && !this->scan_result_[0].get_matches()) {
1215 }
1216
1217 if (this->num_retried_ + 1 < WIFI_RETRY_COUNT_PER_BSSID) {
1218 return WiFiRetryPhase::SCAN_CONNECTING; // Keep retrying same BSSID
1219 }
1220
1221 // Exhausted retries on current BSSID (scan_result_[0])
1222 // Its priority has been decreased, so on next scan it will be sorted lower
1223 // and we'll try the next best BSSID.
1224 // Check if there are any potentially hidden networks to try
1225 if (this->find_next_hidden_sta_(-1) >= 0) {
1226 return WiFiRetryPhase::RETRY_HIDDEN; // Found hidden networks to try
1227 }
1228 // No hidden networks - always go through RESTARTING_ADAPTER phase
1229 // This ensures num_retried_ gets reset and a fresh scan is triggered
1230 // The actual adapter restart will be skipped if captive portal/improv is active
1232
1234 // If no hidden SSIDs to try (selected_sta_index_ == -1), skip directly to rescan
1235 if (this->selected_sta_index_ >= 0) {
1236 if (this->num_retried_ + 1 < WIFI_RETRY_COUNT_PER_SSID) {
1237 return WiFiRetryPhase::RETRY_HIDDEN; // Keep retrying same SSID
1238 }
1239
1240 // Exhausted retries on current SSID - check if there are more potentially hidden SSIDs to try
1241 if (this->selected_sta_index_ < static_cast<int8_t>(this->sta_.size()) - 1) {
1242 // Check if find_next_hidden_sta_() would actually find another hidden SSID
1243 // as it might have been seen in the scan results and we want to skip those
1244 // otherwise we will get stuck in RETRY_HIDDEN phase
1245 if (this->find_next_hidden_sta_(this->selected_sta_index_) != -1) {
1246 // More hidden SSIDs available - stay in RETRY_HIDDEN, advance will happen in retry_connect()
1248 }
1249 }
1250 }
1251 // Exhausted all potentially hidden SSIDs - always go through RESTARTING_ADAPTER
1252 // This ensures num_retried_ gets reset and a fresh scan is triggered
1253 // The actual adapter restart will be skipped if captive portal/improv is active
1255
1257 // After restart, go back to explicit hidden if we went through it initially
1260 }
1261 // Skip scanning when captive portal/improv is active to avoid disrupting AP
1262 // Even passive scans can cause brief AP disconnections on ESP32
1263 if (this->is_captive_portal_active_() || this->is_esp32_improv_active_()) {
1265 }
1267 }
1268
1269 // Should never reach here
1271}
1272
1283 WiFiRetryPhase old_phase = this->retry_phase_;
1284
1285 // No-op if staying in same phase
1286 if (old_phase == new_phase) {
1287 return false;
1288 }
1289
1290 ESP_LOGD(TAG, "Retry phase: %s → %s", LOG_STR_ARG(retry_phase_to_log_string(old_phase)),
1291 LOG_STR_ARG(retry_phase_to_log_string(new_phase)));
1292
1293 this->retry_phase_ = new_phase;
1294 this->num_retried_ = 0; // Reset retry counter on phase change
1295
1296 // Phase-specific setup
1297 switch (new_phase) {
1298#ifdef USE_WIFI_FAST_CONNECT
1300 // Move to next configured AP - clear old scan data so new AP is tried with config only
1301 this->selected_sta_index_++;
1302 this->scan_result_.clear();
1303 break;
1304#endif
1305
1307 // Starting explicit hidden phase - reset to first network
1308 this->selected_sta_index_ = 0;
1309 break;
1310
1312 // Transitioning to scan-based connection
1313#ifdef USE_WIFI_FAST_CONNECT
1315 ESP_LOGI(TAG, "Fast connect exhausted, falling back to scan");
1316 }
1317#endif
1318 // Trigger scan if we don't have scan results OR if transitioning from phases that need fresh scan
1319 if (this->scan_result_.empty() || old_phase == WiFiRetryPhase::EXPLICIT_HIDDEN ||
1321 this->selected_sta_index_ = -1; // Will be set after scan completes
1322 this->start_scanning();
1323 return true; // Started scan, wait for completion
1324 }
1325 // Already have scan results - selected_sta_index_ should already be synchronized
1326 // (set in check_scanning_finished() when scan completed)
1327 // No need to reset it here
1328 break;
1329
1331 // Starting hidden mode - find first SSID that wasn't in scan results
1332 if (old_phase == WiFiRetryPhase::SCAN_CONNECTING) {
1333 // Keep scan results so we can skip SSIDs that were visible in the scan
1334 // Don't clear scan_result_ - we need it to know which SSIDs are NOT hidden
1335
1336 // If first network is marked hidden, we went through EXPLICIT_HIDDEN phase
1337 // In that case, skip networks marked hidden:true (already tried)
1338 // Otherwise, include them (they haven't been tried yet)
1340
1341 if (this->selected_sta_index_ == -1) {
1342 ESP_LOGD(TAG, "All SSIDs visible or already tried, skipping hidden mode");
1343 }
1344 }
1345 break;
1346
1348 // Skip actual adapter restart if captive portal/improv is active
1349 // This allows state machine to reset num_retried_ and trigger fresh scan
1350 // without disrupting the captive portal/improv connection
1351 if (!this->is_captive_portal_active_() && !this->is_esp32_improv_active_()) {
1352 this->restart_adapter();
1353 }
1354 // Clear scan flag - we're starting a new retry cycle
1355 this->did_scan_this_cycle_ = false;
1356 // Always enter cooldown after restart (or skip-restart) to allow stabilization
1357 // Use extended cooldown when AP is active to avoid constant scanning that blocks DNS
1359 this->action_started_ = millis();
1360 // Return true to indicate we should wait (go to COOLDOWN) instead of immediately connecting
1361 return true;
1362
1363 default:
1364 break;
1365 }
1366
1367 return false; // Did not start scan, can proceed with connection
1368}
1369
1374 if (this->sta_priorities_.empty()) {
1375 return;
1376 }
1377
1378 int8_t first_priority = this->sta_priorities_[0].priority;
1379
1380 // Only clear if all priorities have been decremented to the minimum value
1381 // At this point, all BSSIDs have been equally penalized and priority info is useless
1382 if (first_priority != std::numeric_limits<int8_t>::min()) {
1383 return;
1384 }
1385
1386 for (const auto &pri : this->sta_priorities_) {
1387 if (pri.priority != first_priority) {
1388 return; // Not all same, nothing to do
1389 }
1390 }
1391
1392 // All priorities are at minimum - clear the vector to save memory and reset
1393 ESP_LOGD(TAG, "Clearing BSSID priorities (all at minimum)");
1394 this->sta_priorities_.clear();
1395 this->sta_priorities_.shrink_to_fit();
1396}
1397
1417 // Determine which BSSID we tried to connect to
1418 optional<bssid_t> failed_bssid;
1419
1420 if (this->retry_phase_ == WiFiRetryPhase::SCAN_CONNECTING && !this->scan_result_.empty()) {
1421 // Scan-based phase: always use best result (index 0)
1422 failed_bssid = this->scan_result_[0].get_bssid();
1423 } else if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_bssid()) {
1424 // Config has specific BSSID (fast_connect or user-specified)
1425 failed_bssid = *config->get_bssid();
1426 }
1427
1428 if (!failed_bssid.has_value()) {
1429 return; // No BSSID to penalize
1430 }
1431
1432 // Get SSID for logging
1433 std::string ssid;
1434 if (this->retry_phase_ == WiFiRetryPhase::SCAN_CONNECTING && !this->scan_result_.empty()) {
1435 ssid = this->scan_result_[0].get_ssid();
1436 } else if (const WiFiAP *config = this->get_selected_sta_()) {
1437 ssid = config->get_ssid();
1438 }
1439
1440 // Only decrease priority on the last attempt for this phase
1441 // This prevents false positives from transient WiFi stack issues
1442 uint8_t max_retries = get_max_retries_for_phase(this->retry_phase_);
1443 bool is_last_attempt = (this->num_retried_ + 1 >= max_retries);
1444
1445 // Decrease priority only on last attempt to avoid false positives from transient failures
1446 int8_t old_priority = this->get_sta_priority(failed_bssid.value());
1447 int8_t new_priority = old_priority;
1448
1449 if (is_last_attempt) {
1450 // Decrease priority, but clamp to int8_t::min to prevent overflow
1451 new_priority =
1452 (old_priority > std::numeric_limits<int8_t>::min()) ? (old_priority - 1) : std::numeric_limits<int8_t>::min();
1453 this->set_sta_priority(failed_bssid.value(), new_priority);
1454 }
1455 ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d", ssid.c_str(),
1456 format_mac_address_pretty(failed_bssid.value().data()).c_str(), old_priority, new_priority);
1457
1458 // After adjusting priority, check if all priorities are now at minimum
1459 // If so, clear the vector to save memory and reset for fresh start
1461}
1462
1474 WiFiRetryPhase current_phase = this->retry_phase_;
1475
1476 // Check if we need to advance to next AP/SSID within the same phase
1477#ifdef USE_WIFI_FAST_CONNECT
1478 if (current_phase == WiFiRetryPhase::FAST_CONNECT_CYCLING_APS) {
1479 // Fast connect: always advance to next AP (no retries per AP)
1480 this->selected_sta_index_++;
1481 this->num_retried_ = 0;
1482 ESP_LOGD(TAG, "Next AP in %s", LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_)));
1483 return;
1484 }
1485#endif
1486
1487 if (current_phase == WiFiRetryPhase::EXPLICIT_HIDDEN && this->num_retried_ + 1 >= WIFI_RETRY_COUNT_PER_SSID) {
1488 // Explicit hidden: exhausted retries on current SSID, find next explicitly hidden network
1489 // Stop when we reach a visible network (proceed to scanning)
1490 size_t next_index = this->selected_sta_index_ + 1;
1491 if (next_index < this->sta_.size() && this->sta_[next_index].get_hidden()) {
1492 this->selected_sta_index_ = static_cast<int8_t>(next_index);
1493 this->num_retried_ = 0;
1494 ESP_LOGD(TAG, "Next explicit hidden network at index %d", static_cast<int>(next_index));
1495 return;
1496 }
1497 // No more consecutive explicit hidden networks found - fall through to trigger phase change
1498 }
1499
1500 if (current_phase == WiFiRetryPhase::RETRY_HIDDEN && this->num_retried_ + 1 >= WIFI_RETRY_COUNT_PER_SSID) {
1501 // Hidden mode: exhausted retries on current SSID, find next potentially hidden SSID
1502 // If first network is marked hidden, we went through EXPLICIT_HIDDEN phase
1503 // In that case, skip networks marked hidden:true (already tried)
1504 // Otherwise, include them (they haven't been tried yet)
1505 int8_t next_index = this->find_next_hidden_sta_(this->selected_sta_index_);
1506 if (next_index != -1) {
1507 // Found another potentially hidden SSID
1508 this->selected_sta_index_ = next_index;
1509 this->num_retried_ = 0;
1510 return;
1511 }
1512 // No more potentially hidden SSIDs - set selected_sta_index_ to -1 to trigger phase change
1513 // This ensures determine_next_phase_() will skip the RETRY_HIDDEN logic and transition out
1514 this->selected_sta_index_ = -1;
1515 // Return early - phase change will happen on next wifi_loop() iteration
1516 return;
1517 }
1518
1519 // Don't increment retry counter if we're in a scan phase with no valid targets
1520 if (this->needs_scan_results_()) {
1521 return;
1522 }
1523
1524 // Increment retry counter to try the same target again
1525 this->num_retried_++;
1526 ESP_LOGD(TAG, "Retry attempt %u/%u in phase %s", this->num_retried_ + 1,
1527 get_max_retries_for_phase(this->retry_phase_), LOG_STR_ARG(retry_phase_to_log_string(this->retry_phase_)));
1528}
1529
1532
1533 // Determine next retry phase based on current state
1534 WiFiRetryPhase current_phase = this->retry_phase_;
1535 WiFiRetryPhase next_phase = this->determine_next_phase_();
1536
1537 // Handle phase transitions (transition_to_phase_ handles same-phase no-op internally)
1538 if (this->transition_to_phase_(next_phase)) {
1539 return; // Scan started or adapter restarted (which sets its own state)
1540 }
1541
1542 if (next_phase == current_phase) {
1544 }
1545
1546 this->error_from_callback_ = false;
1547
1548 yield();
1549 // Check if we have a valid target before building params
1550 // After exhausting all networks in a phase, selected_sta_index_ may be -1
1551 // In that case, skip connection and let next wifi_loop() handle phase transition
1552 if (this->selected_sta_index_ >= 0) {
1553 WiFiAP params = this->build_params_for_current_phase_();
1554 this->start_connecting(params);
1555 }
1556}
1557
1558void WiFiComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
1564
1565void WiFiComponent::set_passive_scan(bool passive) { this->passive_scan_ = passive; }
1566
1568#ifdef USE_CAPTIVE_PORTAL
1570#else
1571 return false;
1572#endif
1573}
1575#ifdef USE_IMPROV
1577#else
1578 return false;
1579#endif
1580}
1581
1582#ifdef USE_WIFI_FAST_CONNECT
1584 SavedWifiFastConnectSettings fast_connect_save{};
1585
1586 if (this->fast_connect_pref_.load(&fast_connect_save)) {
1587 // Validate saved AP index
1588 if (fast_connect_save.ap_index < 0 || static_cast<size_t>(fast_connect_save.ap_index) >= this->sta_.size()) {
1589 ESP_LOGW(TAG, "AP index out of bounds");
1590 return false;
1591 }
1592
1593 // Set selected index for future operations (save, retry, etc)
1594 this->selected_sta_index_ = fast_connect_save.ap_index;
1595
1596 // Copy entire config, then override with fast connect data
1597 params = this->sta_[fast_connect_save.ap_index];
1598
1599 // Override with saved BSSID/channel from fast connect (SSID/password/etc already copied from config)
1600 bssid_t bssid{};
1601 std::copy(fast_connect_save.bssid, fast_connect_save.bssid + 6, bssid.begin());
1602 params.set_bssid(bssid);
1603 params.set_channel(fast_connect_save.channel);
1604 // Fast connect uses specific BSSID+channel, not hidden network probe (even if config has hidden: true)
1605 params.set_hidden(false);
1606
1607 ESP_LOGD(TAG, "Loaded fast_connect settings");
1608 return true;
1609 }
1610
1611 return false;
1612}
1613
1615 bssid_t bssid = wifi_bssid();
1616 uint8_t channel = get_wifi_channel();
1617 // selected_sta_index_ is always valid here (called only after successful connection)
1618 // Fallback to 0 is defensive programming for robustness
1619 int8_t ap_index = this->selected_sta_index_ >= 0 ? this->selected_sta_index_ : 0;
1620
1621 // Skip save if settings haven't changed (compare with previously saved settings to reduce flash wear)
1622 SavedWifiFastConnectSettings previous_save{};
1623 if (this->fast_connect_pref_.load(&previous_save) && memcmp(previous_save.bssid, bssid.data(), 6) == 0 &&
1624 previous_save.channel == channel && previous_save.ap_index == ap_index) {
1625 return; // No change, nothing to save
1626 }
1627
1628 SavedWifiFastConnectSettings fast_connect_save{};
1629 memcpy(fast_connect_save.bssid, bssid.data(), 6);
1630 fast_connect_save.channel = channel;
1631 fast_connect_save.ap_index = ap_index;
1632
1633 this->fast_connect_pref_.save(&fast_connect_save);
1634
1635 ESP_LOGD(TAG, "Saved fast_connect settings");
1636}
1637#endif
1638
1639void WiFiAP::set_ssid(const std::string &ssid) { this->ssid_ = ssid; }
1640void WiFiAP::set_bssid(bssid_t bssid) { this->bssid_ = bssid; }
1641void WiFiAP::set_bssid(optional<bssid_t> bssid) { this->bssid_ = bssid; }
1642void WiFiAP::set_password(const std::string &password) { this->password_ = password; }
1643#ifdef USE_WIFI_WPA2_EAP
1644void WiFiAP::set_eap(optional<EAPAuth> eap_auth) { this->eap_ = std::move(eap_auth); }
1645#endif
1646void WiFiAP::set_channel(optional<uint8_t> channel) { this->channel_ = channel; }
1647#ifdef USE_WIFI_MANUAL_IP
1648void WiFiAP::set_manual_ip(optional<ManualIP> manual_ip) { this->manual_ip_ = manual_ip; }
1649#endif
1650void WiFiAP::set_hidden(bool hidden) { this->hidden_ = hidden; }
1651const std::string &WiFiAP::get_ssid() const { return this->ssid_; }
1652const optional<bssid_t> &WiFiAP::get_bssid() const { return this->bssid_; }
1653const std::string &WiFiAP::get_password() const { return this->password_; }
1654#ifdef USE_WIFI_WPA2_EAP
1655const optional<EAPAuth> &WiFiAP::get_eap() const { return this->eap_; }
1656#endif
1657const optional<uint8_t> &WiFiAP::get_channel() const { return this->channel_; }
1658#ifdef USE_WIFI_MANUAL_IP
1660#endif
1661bool WiFiAP::get_hidden() const { return this->hidden_; }
1662
1663WiFiScanResult::WiFiScanResult(const bssid_t &bssid, std::string ssid, uint8_t channel, int8_t rssi, bool with_auth,
1664 bool is_hidden)
1665 : bssid_(bssid),
1666 channel_(channel),
1667 rssi_(rssi),
1668 ssid_(std::move(ssid)),
1669 with_auth_(with_auth),
1670 is_hidden_(is_hidden) {}
1671bool WiFiScanResult::matches(const WiFiAP &config) const {
1672 if (config.get_hidden()) {
1673 // User configured a hidden network, only match actually hidden networks
1674 // don't match SSID
1675 if (!this->is_hidden_)
1676 return false;
1677 } else if (!config.get_ssid().empty()) {
1678 // check if SSID matches
1679 if (config.get_ssid() != this->ssid_)
1680 return false;
1681 } else {
1682 // network is configured without SSID - match other settings
1683 }
1684 // If BSSID configured, only match for correct BSSIDs
1685 if (config.get_bssid().has_value() && *config.get_bssid() != this->bssid_)
1686 return false;
1687
1688#ifdef USE_WIFI_WPA2_EAP
1689 // BSSID requires auth but no PSK or EAP credentials given
1690 if (this->with_auth_ && (config.get_password().empty() && !config.get_eap().has_value()))
1691 return false;
1692
1693 // BSSID does not require auth, but PSK or EAP credentials given
1694 if (!this->with_auth_ && (!config.get_password().empty() || config.get_eap().has_value()))
1695 return false;
1696#else
1697 // If PSK given, only match for networks with auth (and vice versa)
1698 if (config.get_password().empty() == this->with_auth_)
1699 return false;
1700#endif
1701
1702 // If channel configured, only match networks on that channel.
1703 if (config.get_channel().has_value() && *config.get_channel() != this->channel_) {
1704 return false;
1705 }
1706 return true;
1707}
1708bool WiFiScanResult::get_matches() const { return this->matches_; }
1709void WiFiScanResult::set_matches(bool matches) { this->matches_ = matches; }
1710const bssid_t &WiFiScanResult::get_bssid() const { return this->bssid_; }
1711const std::string &WiFiScanResult::get_ssid() const { return this->ssid_; }
1712uint8_t WiFiScanResult::get_channel() const { return this->channel_; }
1713int8_t WiFiScanResult::get_rssi() const { return this->rssi_; }
1714bool WiFiScanResult::get_with_auth() const { return this->with_auth_; }
1715bool WiFiScanResult::get_is_hidden() const { return this->is_hidden_; }
1716
1717bool WiFiScanResult::operator==(const WiFiScanResult &rhs) const { return this->bssid_ == rhs.bssid_; }
1718
1719WiFiComponent *global_wifi_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
1720
1721} // namespace wifi
1722} // namespace esphome
1723#endif
uint8_t m
Definition bl0906.h:1
uint8_t status
Definition bl0942.h:8
std::string get_compilation_time() const
bool is_name_add_mac_suffix_enabled() const
const std::string & get_name() const
Get the name of this Application set by pre_setup().
uint32_t IRAM_ATTR HOT get_loop_component_start_time() const
Get the cached time in milliseconds from when the current component started its loop execution.
void status_set_warning(const char *message=nullptr)
void status_clear_warning()
bool save(const T *src)
Definition preferences.h:21
virtual bool sync()=0
Commit pending writes to flash.
virtual ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash)=0
void trigger(const Ts &...x)
Inform the parent automation that the event has triggered.
Definition automation.h:169
bool has_value() const
Definition optional.h:92
value_type const & value() const
Definition optional.h:94
const optional< bssid_t > & get_bssid() const
const std::string & get_ssid() const
void set_ssid(const std::string &ssid)
const optional< uint8_t > & get_channel() const
const optional< EAPAuth > & get_eap() const
void set_channel(optional< uint8_t > channel)
const std::string & get_password() const
void set_bssid(bssid_t bssid)
optional< uint8_t > channel_
optional< EAPAuth > eap_
optional< bssid_t > bssid_
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)
This component is responsible for managing the ESP WiFi interface.
void add_sta(const WiFiAP &ap)
bool load_fast_connect_settings_(WiFiAP &params)
void set_ap(const WiFiAP &ap)
Setup an Access Point that should be created if no connection to a station can be made.
void set_sta(const WiFiAP &ap)
bool has_sta_priority(const bssid_t &bssid)
const WiFiAP * get_selected_sta_() const
int8_t get_sta_priority(const bssid_t bssid)
void log_and_adjust_priority_for_failed_connect_()
Log failed connection and decrease BSSID priority to avoid repeated attempts.
void save_wifi_sta(const std::string &ssid, const std::string &password)
wifi_scan_vector_t< WiFiScanResult > scan_result_
void set_sta_priority(const bssid_t bssid, int8_t priority)
void loop() override
Reconnect WiFi if required.
void start_connecting(const WiFiAP &ap)
void advance_to_next_target_or_increment_retry_()
Advance to next target (AP/SSID) within current phase, or increment retry counter Called when staying...
network::IPAddress get_dns_address(int num)
WiFiComponent()
Construct a WiFiComponent.
std::vector< WiFiSTAPriority > sta_priorities_
void set_passive_scan(bool passive)
void set_power_save_mode(WiFiPowerSaveMode power_save)
int8_t find_next_hidden_sta_(int8_t start_index)
Find next SSID that wasn't in scan results (might be hidden) Returns index of next potentially hidden...
ESPPreferenceObject fast_connect_pref_
void clear_priorities_if_all_min_()
Clear BSSID priority tracking if all priorities are at minimum (saves memory)
WiFiRetryPhase determine_next_phase_()
Determine next retry phase based on current state and failure conditions.
network::IPAddress wifi_dns_ip_(int num)
float get_loop_priority() const override
network::IPAddresses get_ip_addresses()
float get_setup_priority() const override
WIFI setup_priority.
FixedVector< WiFiAP > sta_
int8_t find_first_non_hidden_index_() const
Find the index of the first non-hidden network Returns where EXPLICIT_HIDDEN phase would have stopped...
bool ssid_was_seen_in_scan_(const std::string &ssid) const
Check if an SSID was seen in the most recent scan results Used to skip hidden mode for SSIDs we know ...
bool needs_scan_results_() const
Check if we need valid scan results for the current phase but don't have any Returns true if the phas...
bool transition_to_phase_(WiFiRetryPhase new_phase)
Transition to a new retry phase with logging Returns true if a scan was started (caller should wait),...
optional< float > output_power_
const char * get_use_address() const
WiFiSTAConnectStatus wifi_sta_connect_status_()
bool went_through_explicit_hidden_phase_() const
Check if we went through EXPLICIT_HIDDEN phase (first network is marked hidden) Used in RETRY_HIDDEN ...
bool wifi_mode_(optional< bool > sta, optional< bool > ap)
void set_reboot_timeout(uint32_t reboot_timeout)
network::IPAddresses wifi_sta_ip_addresses()
void start_initial_connection_()
Start initial connection - either scan or connect directly to hidden networks.
void setup() override
Setup WiFi interface.
void set_use_address(const char *use_address)
const std::string & get_ssid() const
const bssid_t & get_bssid() const
WiFiScanResult(const bssid_t &bssid, std::string ssid, uint8_t channel, int8_t rssi, bool with_auth, bool is_hidden)
bool matches(const WiFiAP &config) const
bool operator==(const WiFiScanResult &rhs) const
struct @65::@66 __attribute__
uint16_t type
uint8_t priority
CaptivePortal * global_captive_portal
ESP32ImprovComponent * global_improv_component
std::array< IPAddress, 5 > IPAddresses
Definition ip_address.h:144
std::array< uint8_t, 6 > bssid_t
const LogString * get_signal_bars(int8_t rssi)
WiFiRetryPhase
Tracks the current retry strategy/phase for WiFi connection attempts.
@ RETRY_HIDDEN
Retry networks not found in scan (might be hidden)
@ RESTARTING_ADAPTER
Restarting WiFi adapter to clear stuck state.
@ INITIAL_CONNECT
Initial connection attempt (varies based on fast_connect setting)
@ EXPLICIT_HIDDEN
Explicitly hidden networks (user marked as hidden, try before scanning)
@ FAST_CONNECT_CYCLING_APS
Fast connect mode: cycling through configured APs (config-only, no scan)
@ SCAN_CONNECTING
Scan-based: connecting to best AP from scan results.
WiFiComponent * global_wifi_component
@ WIFI_COMPONENT_STATE_DISABLED
WiFi is disabled.
@ WIFI_COMPONENT_STATE_AP
WiFi is in AP-only mode and internal AP is already enabled.
@ WIFI_COMPONENT_STATE_STA_CONNECTING
WiFi is in STA(+AP) mode and currently connecting to an AP.
@ WIFI_COMPONENT_STATE_OFF
Nothing has been initialized yet.
@ WIFI_COMPONENT_STATE_STA_SCANNING
WiFi is in STA-only mode and currently scanning for APs.
@ WIFI_COMPONENT_STATE_COOLDOWN
WiFi is in cooldown mode because something went wrong, scanning will begin after a short period of ti...
@ WIFI_COMPONENT_STATE_STA_CONNECTED
WiFi is in STA(+AP) mode and successfully connected.
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
void format_mac_addr_upper(const uint8_t *mac, char *output)
Format MAC address as XX:XX:XX:XX:XX:XX (uppercase)
Definition helpers.h:604
ESPPreferences * global_preferences
uint32_t fnv1_hash(const char *str)
Calculate a FNV-1 hash of str.
Definition helpers.cpp:146
std::string get_mac_address_pretty()
Get the device MAC address as a string, in colon-separated uppercase hex notation.
Definition helpers.cpp:640
void IRAM_ATTR HOT yield()
Definition core.cpp:29
std::string format_mac_address_pretty(const uint8_t *mac)
Definition helpers.cpp:282
uint32_t IRAM_ATTR HOT millis()
Definition core.cpp:30
Application App
Global storage of Application pointer - only one Application can exist.
std::string str() const
Definition ip_address.h:52
esp_eap_ttls_phase2_types ttls_phase_2
Struct for setting static IPs in WiFiComponent.