#pragma once #include "esphome/core/defines.h" #ifdef USE_WIFI #include "esphome/components/network/ip_address.h" #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" #ifdef USE_ESP32 #include "esphome/core/lock_free_queue.h" #endif #include "esphome/core/string_ref.h" #include #include #include #include #ifdef USE_LIBRETINY #include #endif #if defined(USE_ESP32) && defined(USE_WIFI_WPA2_EAP) #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0) #include #else #include #endif #endif #ifdef USE_ESP8266 #include #include #if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE < VERSION_CODE(2, 4, 0) extern "C" { #include }; #endif #endif #ifdef USE_RP2040 extern "C" { #include "cyw43.h" #include "cyw43_country.h" #include "pico/cyw43_arch.h" } #include #endif #if defined(USE_ESP32) && defined(SOC_WIFI_SUPPORT_5G) #include #endif #if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) #include #include #endif namespace esphome::wifi { /// Sentinel value for RSSI when WiFi is not connected static constexpr int8_t WIFI_RSSI_DISCONNECTED = -127; /// Buffer size for SSID (IEEE 802.11 max 32 bytes + null terminator) static constexpr size_t SSID_BUFFER_SIZE = 33; struct SavedWifiSettings { char ssid[33]; char password[65]; } PACKED; // NOLINT struct SavedWifiFastConnectSettings { uint8_t bssid[6]; uint8_t channel; int8_t ap_index; } PACKED; // NOLINT enum WiFiComponentState : uint8_t { /** Nothing has been initialized yet. Internal AP, if configured, is disabled at this point. */ WIFI_COMPONENT_STATE_OFF = 0, /** WiFi is disabled. */ WIFI_COMPONENT_STATE_DISABLED, /** WiFi is in cooldown mode because something went wrong, scanning will begin after a short period of time. */ WIFI_COMPONENT_STATE_COOLDOWN, /** WiFi is in STA-only mode and currently scanning for APs. */ WIFI_COMPONENT_STATE_STA_SCANNING, /** WiFi is in STA(+AP) mode and currently connecting to an AP. */ WIFI_COMPONENT_STATE_STA_CONNECTING, /** WiFi is in STA(+AP) mode and successfully connected. */ WIFI_COMPONENT_STATE_STA_CONNECTED, /** WiFi is in AP-only mode and internal AP is already enabled. */ WIFI_COMPONENT_STATE_AP, }; enum class WiFiSTAConnectStatus : int { IDLE, CONNECTING, CONNECTED, ERROR_NETWORK_NOT_FOUND, ERROR_CONNECT_FAILED, }; /// Tracks the current retry strategy/phase for WiFi connection attempts enum class WiFiRetryPhase : uint8_t { /// Initial connection attempt (varies based on fast_connect setting) INITIAL_CONNECT, #ifdef USE_WIFI_FAST_CONNECT /// Fast connect mode: cycling through configured APs (config-only, no scan) FAST_CONNECT_CYCLING_APS, #endif /// Explicitly hidden networks (user marked as hidden, try before scanning) EXPLICIT_HIDDEN, /// Scan-based: connecting to best AP from scan results SCAN_CONNECTING, /// Retry networks not found in scan (might be hidden) RETRY_HIDDEN, /// Restarting WiFi adapter to clear stuck state RESTARTING_ADAPTER, }; /// Tracks post-connect roaming state machine enum class RoamingState : uint8_t { /// Not roaming, waiting for next check interval IDLE, /// Scanning for better AP SCANNING, /// Attempting to connect to better AP found in scan CONNECTING, /// Roam connection failed, reconnecting to any available AP RECONNECTING, }; /// Controls how RETRY_HIDDEN phase selects networks to try enum class RetryHiddenMode : uint8_t { /// Normal mode: scan completed, only try networks NOT visible in scan results /// (truly hidden networks that need probe requests) SCAN_BASED, /// Blind retry mode: scanning disabled (captive portal/improv active), /// try ALL configured networks sequentially without consulting scan results BLIND_RETRY, }; /// Struct for setting static IPs in WiFiComponent. struct ManualIP { network::IPAddress static_ip; network::IPAddress gateway; network::IPAddress subnet; network::IPAddress dns1; ///< The first DNS server. 0.0.0.0 for default. network::IPAddress dns2; ///< The second DNS server. 0.0.0.0 for default. }; #ifdef USE_WIFI_WPA2_EAP struct EAPAuth { std::string identity; // required for all auth types std::string username; std::string password; const char *ca_cert; // optionally verify authentication server // used for EAP-TLS const char *client_cert; const char *client_key; // used for EAP-TTLS #ifdef USE_ESP32 esp_eap_ttls_phase2_types ttls_phase_2; #endif }; #endif // USE_WIFI_WPA2_EAP using bssid_t = std::array; /// Initial reserve size for filtered scan results (typical: 1-3 matching networks per SSID) static constexpr size_t WIFI_SCAN_RESULT_FILTERED_RESERVE = 8; // Use std::vector for RP2040 (callback-based) and ESP32 (destructive scan API) // Use FixedVector for ESP8266 and LibreTiny where two-pass exact allocation is possible #if defined(USE_RP2040) || defined(USE_ESP32) template using wifi_scan_vector_t = std::vector; #else template using wifi_scan_vector_t = FixedVector; #endif /// 20-byte string: 18 chars inline + null, heap for longer. Always null-terminated. /// Used internally for WiFi SSID/password storage to reduce heap fragmentation. class CompactString { public: static constexpr uint8_t MAX_LENGTH = 127; static constexpr uint8_t INLINE_CAPACITY = 18; // 18 chars + null terminator fits in 19 bytes CompactString() : length_(0), is_heap_(0) { this->storage_[0] = '\0'; } CompactString(const char *str, size_t len); CompactString(const CompactString &other); CompactString(CompactString &&other) noexcept; CompactString &operator=(const CompactString &other); CompactString &operator=(CompactString &&other) noexcept; ~CompactString(); const char *data() const { return this->is_heap_ ? this->get_heap_ptr_() : this->storage_; } const char *c_str() const { return this->data(); } // Always null-terminated size_t size() const { return this->length_; } bool empty() const { return this->length_ == 0; } /// Return a StringRef view of this string (zero-copy) StringRef ref() const { return StringRef(this->data(), this->size()); } bool operator==(const CompactString &other) const; bool operator!=(const CompactString &other) const { return !(*this == other); } bool operator==(const StringRef &other) const; bool operator!=(const StringRef &other) const { return !(*this == other); } bool operator==(const char *other) const { return *this == StringRef(other); } bool operator!=(const char *other) const { return !(*this == other); } protected: char *get_heap_ptr_() const { char *ptr; std::memcpy(&ptr, this->storage_, sizeof(ptr)); return ptr; } void set_heap_ptr_(char *ptr) { std::memcpy(this->storage_, &ptr, sizeof(ptr)); } // Storage for string data. When is_heap_=0, contains the string directly (null-terminated). // When is_heap_=1, first sizeof(char*) bytes contain pointer to heap allocation. char storage_[INLINE_CAPACITY + 1]; // 19 bytes: 18 chars + null terminator uint8_t length_ : 7; // String length (0-127) uint8_t is_heap_ : 1; // 1 if using heap pointer, 0 if using inline storage // Total size: 20 bytes (19 bytes storage + 1 byte bitfields) }; static_assert(sizeof(CompactString) == 20, "CompactString must be exactly 20 bytes"); // CompactString is not trivially copyable (non-trivial destructor/copy for heap case). // However, its layout has no self-referential pointers: storage_[] contains either inline // data or an external heap pointer — never a pointer to itself. This is unlike libstdc++ // std::string SSO where _M_p points to _M_local_buf within the same object. // This property allows memcpy-based permutation sorting where each element ends up in // exactly one slot (no ownership duplication). These asserts document that layout property. static_assert(std::is_standard_layout::value, "CompactString must be standard layout"); static_assert(!std::is_polymorphic::value, "CompactString must not have vtable"); class WiFiAP { friend class WiFiComponent; friend class WiFiScanResult; public: void set_ssid(const std::string &ssid); void set_ssid(const char *ssid); void set_ssid(StringRef ssid) { this->ssid_ = CompactString(ssid.c_str(), ssid.size()); } void set_bssid(const bssid_t &bssid); void clear_bssid(); void set_password(const std::string &password); void set_password(const char *password); void set_password(StringRef password) { this->password_ = CompactString(password.c_str(), password.size()); } #ifdef USE_WIFI_WPA2_EAP void set_eap(optional eap_auth); #endif // USE_WIFI_WPA2_EAP void set_channel(uint8_t channel); void clear_channel(); void set_priority(int8_t priority) { priority_ = priority; } #ifdef USE_WIFI_MANUAL_IP void set_manual_ip(optional manual_ip); #endif void set_hidden(bool hidden); StringRef get_ssid() const { return this->ssid_.ref(); } StringRef get_password() const { return this->password_.ref(); } const bssid_t &get_bssid() const; bool has_bssid() const; #ifdef USE_WIFI_WPA2_EAP const optional &get_eap() const; #endif // USE_WIFI_WPA2_EAP uint8_t get_channel() const { return this->channel_; } bool has_channel() const { return this->channel_ != 0; } int8_t get_priority() const { return priority_; } #ifdef USE_WIFI_MANUAL_IP const optional &get_manual_ip() const; #endif bool get_hidden() const; protected: CompactString ssid_; CompactString password_; #ifdef USE_WIFI_WPA2_EAP optional eap_; #endif // USE_WIFI_WPA2_EAP #ifdef USE_WIFI_MANUAL_IP optional manual_ip_; #endif // Group small types together to minimize padding bssid_t bssid_{}; // 6 bytes, all zeros = any/not set uint8_t channel_{0}; // 1 byte, 0 = auto/not set int8_t priority_{0}; // 1 byte bool hidden_{false}; // 1 byte (+ 3 bytes end padding to 4-byte align) }; class WiFiScanResult { friend class WiFiComponent; public: WiFiScanResult(const bssid_t &bssid, const char *ssid, size_t ssid_len, uint8_t channel, int8_t rssi, bool with_auth, bool is_hidden); bool matches(const WiFiAP &config) const; bool get_matches() const; void set_matches(bool matches); const bssid_t &get_bssid() const; StringRef get_ssid() const { return this->ssid_.ref(); } uint8_t get_channel() const; int8_t get_rssi() const; bool get_with_auth() const; bool get_is_hidden() const; int8_t get_priority() const { return priority_; } void set_priority(int8_t priority) { priority_ = priority; } bool operator==(const WiFiScanResult &rhs) const; protected: bssid_t bssid_; uint8_t channel_; int8_t rssi_; CompactString ssid_; int8_t priority_{0}; bool matches_{false}; bool with_auth_; bool is_hidden_; }; struct WiFiSTAPriority { bssid_t bssid; int8_t priority; }; enum WiFiPowerSaveMode : uint8_t { WIFI_POWER_SAVE_NONE = 0, WIFI_POWER_SAVE_LIGHT, WIFI_POWER_SAVE_HIGH, }; enum WifiMinAuthMode : uint8_t { WIFI_MIN_AUTH_MODE_WPA = 0, WIFI_MIN_AUTH_MODE_WPA2, WIFI_MIN_AUTH_MODE_WPA3, }; #ifdef USE_ESP32 struct IDFWiFiEvent; #endif #ifdef USE_LIBRETINY struct LTWiFiEvent; #endif /** Listener interface for WiFi IP state changes. * * Components can implement this interface to receive IP address updates * without the overhead of std::function callbacks. * * @note Components must call wifi.request_wifi_ip_state_listener() in their * Python to_code() to register for this listener type. */ class WiFiIPStateListener { public: virtual void on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1, const network::IPAddress &dns2) = 0; }; /** Listener interface for WiFi scan results. * * Components can implement this interface to receive scan results * without the overhead of std::function callbacks. * * @note Components must call wifi.request_wifi_scan_results_listener() in their * Python to_code() to register for this listener type. */ class WiFiScanResultsListener { public: virtual void on_wifi_scan_results(const wifi_scan_vector_t &results) = 0; }; /** Listener interface for WiFi connection state changes. * * Components can implement this interface to receive connection updates * without the overhead of std::function callbacks. * * @note Components must call wifi.request_wifi_connect_state_listener() in their * Python to_code() to register for this listener type. */ class WiFiConnectStateListener { public: virtual void on_wifi_connect_state(StringRef ssid, std::span bssid) = 0; }; /** Listener interface for WiFi power save mode changes. * * Components can implement this interface to receive power save mode updates * without the overhead of std::function callbacks. * * @note Components must call wifi.request_wifi_power_save_listener() in their * Python to_code() to register for this listener type. */ class WiFiPowerSaveListener { public: virtual void on_wifi_power_save(WiFiPowerSaveMode mode) = 0; }; /// This component is responsible for managing the ESP WiFi interface. class WiFiComponent final : public Component { public: /// Construct a WiFiComponent. WiFiComponent(); void set_sta(const WiFiAP &ap); // Returns a copy of the currently selected AP configuration WiFiAP get_sta() const; void init_sta(size_t count); void add_sta(const WiFiAP &ap); void clear_sta(); #ifdef USE_WIFI_AP /** Setup an Access Point that should be created if no connection to a station can be made. * * This can also be used without set_sta(). Then the AP will always be active. * * If both STA and AP are defined, then both will be enabled at startup, but if a connection to a station * can be made, the AP will be turned off again. */ void set_ap(const WiFiAP &ap); WiFiAP get_ap() { return this->ap_; } void set_ap_timeout(uint32_t ap_timeout) { ap_timeout_ = ap_timeout; } #endif // USE_WIFI_AP void enable(); void disable(); bool is_disabled(); void start_scanning(); void check_scanning_finished(); void start_connecting(const WiFiAP &ap); // Backward compatibility overload - ignores 'two' parameter void start_connecting(const WiFiAP &ap, bool /* two */) { this->start_connecting(ap); } void check_connecting_finished(uint32_t now); void retry_connect(); void set_reboot_timeout(uint32_t reboot_timeout); bool is_connected() const { return this->connected_; } void set_power_save_mode(WiFiPowerSaveMode power_save); void set_min_auth_mode(WifiMinAuthMode min_auth_mode) { min_auth_mode_ = min_auth_mode; } void set_output_power(float output_power) { output_power_ = output_power; } #if defined(USE_ESP32) && defined(SOC_WIFI_SUPPORT_5G) void set_band_mode(wifi_band_mode_t band_mode) { this->band_mode_ = band_mode; } #endif void set_passive_scan(bool passive); void save_wifi_sta(const std::string &ssid, const std::string &password); void save_wifi_sta(const char *ssid, const char *password); void save_wifi_sta(StringRef ssid, StringRef password) { this->save_wifi_sta(ssid.c_str(), password.c_str()); } // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) /// Setup WiFi interface. void setup() override; void start(); void dump_config() override; void restart_adapter(); /// WIFI setup_priority. float get_setup_priority() const override; /// Reconnect WiFi if required. void loop() override; bool has_sta() const { return !this->sta_.empty(); } bool has_ap() const { return this->has_ap_; } bool is_ap_active() const { return this->ap_started_; } #ifdef USE_WIFI_11KV_SUPPORT void set_btm(bool btm); void set_rrm(bool rrm); #endif network::IPAddress get_dns_address(int num); network::IPAddresses get_ip_addresses(); const char *get_use_address() const { return this->use_address_; } void set_use_address(const char *use_address) { this->use_address_ = use_address; } const wifi_scan_vector_t &get_scan_result() const { return scan_result_; } network::IPAddress wifi_soft_ap_ip(); bool has_sta_priority(const bssid_t &bssid) { for (auto &it : this->sta_priorities_) { if (it.bssid == bssid) return true; } return false; } int8_t get_sta_priority(const bssid_t bssid) { for (auto &it : this->sta_priorities_) { if (it.bssid == bssid) return it.priority; } return 0; } void set_sta_priority(bssid_t bssid, int8_t priority); network::IPAddresses wifi_sta_ip_addresses(); // Remove before 2026.9.0 ESPDEPRECATED("Use wifi_ssid_to() instead. Removed in 2026.9.0", "2026.3.0") std::string wifi_ssid(); /// Write SSID to buffer without heap allocation. /// Returns pointer to buffer, or empty string if not connected. const char *wifi_ssid_to(std::span buffer); bssid_t wifi_bssid(); int8_t wifi_rssi(); void set_enable_on_boot(bool enable_on_boot) { this->enable_on_boot_ = enable_on_boot; } void set_keep_scan_results(bool keep_scan_results) { this->keep_scan_results_ = keep_scan_results; } void set_post_connect_roaming(bool enabled) { this->post_connect_roaming_ = enabled; } #ifdef USE_WIFI_CONNECT_TRIGGER Trigger<> *get_connect_trigger() { return &this->connect_trigger_; } #endif #ifdef USE_WIFI_DISCONNECT_TRIGGER Trigger<> *get_disconnect_trigger() { return &this->disconnect_trigger_; } #endif int32_t get_wifi_channel(); #ifdef USE_WIFI_IP_STATE_LISTENERS /** Add a listener for IP state changes. * Listener receives: IP addresses, DNS address 1, DNS address 2 */ void add_ip_state_listener(WiFiIPStateListener *listener) { this->ip_state_listeners_.push_back(listener); } #endif // USE_WIFI_IP_STATE_LISTENERS #ifdef USE_WIFI_SCAN_RESULTS_LISTENERS /// Add a listener for WiFi scan results void add_scan_results_listener(WiFiScanResultsListener *listener) { this->scan_results_listeners_.push_back(listener); } #endif // USE_WIFI_SCAN_RESULTS_LISTENERS #ifdef USE_WIFI_CONNECT_STATE_LISTENERS /** Add a listener for WiFi connection state changes. * Listener receives: SSID, BSSID */ void add_connect_state_listener(WiFiConnectStateListener *listener) { this->connect_state_listeners_.push_back(listener); } #endif // USE_WIFI_CONNECT_STATE_LISTENERS #ifdef USE_WIFI_POWER_SAVE_LISTENERS /** Add a listener for WiFi power save mode changes. * Listener receives: WiFiPowerSaveMode */ void add_power_save_listener(WiFiPowerSaveListener *listener) { this->power_save_listeners_.push_back(listener); } #endif // USE_WIFI_POWER_SAVE_LISTENERS #ifdef USE_WIFI_RUNTIME_POWER_SAVE /** Request high-performance mode (no power saving) for improved WiFi latency. * * Components that need maximum WiFi performance (e.g., audio streaming, large data transfers) * can call this method to temporarily disable WiFi power saving. Multiple components can * request high performance simultaneously using a counting semaphore. * * Power saving will be restored to the YAML-configured mode when all components have * called release_high_performance(). * * Note: Only supported on ESP32. * * @return true if request was satisfied (high-performance mode active or already configured), * false if operation failed (semaphore error) */ bool request_high_performance(); /** Release a high-performance mode request. * * Should be called when a component no longer needs maximum WiFi latency. * When all requests are released (semaphore count reaches zero), WiFi power saving * is restored to the YAML-configured mode. * * Note: Only supported on ESP32. * * @return true if release was successful (or already in high-performance config), * false if operation failed (semaphore error) */ bool release_high_performance(); #endif // USE_WIFI_RUNTIME_POWER_SAVE protected: #ifdef USE_WIFI_AP void setup_ap_config_(); #endif // USE_WIFI_AP void print_connect_params_(); WiFiAP build_params_for_current_phase_(); /// Determine next retry phase based on current state and failure conditions WiFiRetryPhase determine_next_phase_(); /// Transition to a new retry phase with logging /// Returns true if a scan was started (caller should wait), false otherwise bool transition_to_phase_(WiFiRetryPhase new_phase); /// Check if we need valid scan results for the current phase but don't have any /// Returns true if the phase requires scan results but they're missing or don't match bool needs_scan_results_() const; /// Check if we went through EXPLICIT_HIDDEN phase (first network is marked hidden) /// Used in RETRY_HIDDEN to determine whether to skip explicitly hidden networks bool went_through_explicit_hidden_phase_() const; /// Find the index of the first non-hidden network /// Returns where EXPLICIT_HIDDEN phase would have stopped, or -1 if all networks are hidden int8_t find_first_non_hidden_index_() const; /// Check if an SSID was seen in the most recent scan results /// Used to skip hidden mode for SSIDs we know are visible bool ssid_was_seen_in_scan_(const CompactString &ssid) const; /// Check if full scan results are needed (captive portal active, improv, listeners) bool needs_full_scan_results_() const; /// Check if network matches any configured network (for scan result filtering) /// Matches by SSID when configured, or by BSSID for BSSID-only configs bool matches_configured_network_(const char *ssid, const uint8_t *bssid) const; /// Log a discarded scan result at VERBOSE level (skipped during roaming scans to avoid log overflow) void log_discarded_scan_result_(const char *ssid, const uint8_t *bssid, int8_t rssi, uint8_t channel); /// Find next SSID that wasn't in scan results (might be hidden) /// Returns index of next potentially hidden SSID, or -1 if none found /// @param start_index Start searching from index after this (-1 to start from beginning) int8_t find_next_hidden_sta_(int8_t start_index); /// Log failed connection and decrease BSSID priority to avoid repeated attempts void log_and_adjust_priority_for_failed_connect_(); /// Clear all BSSID priority penalties after successful connection (stale after disconnect) void clear_all_bssid_priorities_(); /// Clear BSSID priority tracking if all priorities are at minimum (saves memory) void clear_priorities_if_all_min_(); /// Advance to next target (AP/SSID) within current phase, or increment retry counter /// Called when staying in the same phase after a failed connection attempt void advance_to_next_target_or_increment_retry_(); /// Start initial connection - either scan or connect directly to hidden networks void start_initial_connection_(); const WiFiAP *get_selected_sta_() const { if (this->selected_sta_index_ >= 0 && static_cast(this->selected_sta_index_) < this->sta_.size()) { return &this->sta_[this->selected_sta_index_]; } return nullptr; } void reset_selected_ap_to_first_if_invalid_() { if (this->selected_sta_index_ < 0 || static_cast(this->selected_sta_index_) >= this->sta_.size()) { this->selected_sta_index_ = this->sta_.empty() ? -1 : 0; } } bool all_networks_hidden_() const { if (this->sta_.empty()) return false; for (const auto &ap : this->sta_) { if (!ap.get_hidden()) return false; } return true; } void connect_soon_(); void wifi_loop_(); #ifdef USE_ESP8266 void process_pending_callbacks_(); #endif bool wifi_mode_(optional sta, optional ap); bool wifi_sta_pre_setup_(); bool wifi_apply_output_power_(float output_power); bool wifi_apply_power_save_(); #if defined(USE_ESP32) && defined(SOC_WIFI_SUPPORT_5G) bool wifi_apply_band_mode_(); #endif bool wifi_sta_ip_config_(const optional &manual_ip); bool wifi_apply_hostname_(); bool wifi_sta_connect_(const WiFiAP &ap); void wifi_pre_setup_(); WiFiSTAConnectStatus wifi_sta_connect_status_() const; bool is_connected_() const { return this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED && this->wifi_sta_connect_status_() == WiFiSTAConnectStatus::CONNECTED && !this->error_from_callback_; } void update_connected_state_() { this->connected_ = this->is_connected_(); } bool wifi_scan_start_(bool passive); #ifdef USE_WIFI_AP bool wifi_ap_ip_config_(const optional &manual_ip); bool wifi_start_ap_(const WiFiAP &ap); #endif // USE_WIFI_AP bool wifi_disconnect_(); network::IPAddress wifi_subnet_mask_(); network::IPAddress wifi_gateway_ip_(); network::IPAddress wifi_dns_ip_(int num); bool is_captive_portal_active_(); bool is_esp32_improv_active_(); #ifdef USE_WIFI_FAST_CONNECT bool load_fast_connect_settings_(WiFiAP ¶ms); void save_fast_connect_settings_(); #endif // Post-connect roaming methods void check_roaming_(uint32_t now); void process_roaming_scan_(); void clear_roaming_state_(); /// Free scan results memory unless a component needs them void release_scan_results_(); #ifdef USE_WIFI_CONNECT_STATE_LISTENERS /// Notify connect state listeners (called after state machine reaches STA_CONNECTED) void notify_connect_state_listeners_(); /// Notify connect state listeners of disconnection void notify_disconnect_state_listeners_(); #endif #ifdef USE_WIFI_IP_STATE_LISTENERS /// Notify IP state listeners with current addresses void notify_ip_state_listeners_(); #endif #ifdef USE_WIFI_SCAN_RESULTS_LISTENERS /// Notify scan results listeners with current scan results void notify_scan_results_listeners_(); #endif #ifdef USE_ESP8266 static void wifi_event_callback(System_Event_t *event); void wifi_scan_done_callback_(void *arg, STATUS status); static void s_wifi_scan_done_callback(void *arg, STATUS status); #endif #ifdef USE_ESP32 void wifi_process_event_(IDFWiFiEvent *data); friend void event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data); #endif #ifdef USE_RP2040 static int s_wifi_scan_result(void *env, const cyw43_ev_scan_result_t *result); void wifi_scan_result(void *env, const cyw43_ev_scan_result_t *result); #endif #ifdef USE_LIBRETINY void wifi_event_callback_(arduino_event_id_t event, arduino_event_info_t info); void wifi_process_event_(LTWiFiEvent *event); void wifi_scan_done_callback_(); #endif // Large/pointer-aligned members first FixedVector sta_; std::vector sta_priorities_; wifi_scan_vector_t scan_result_; #ifdef USE_WIFI_AP WiFiAP ap_; #endif #ifdef USE_WIFI_IP_STATE_LISTENERS StaticVector ip_state_listeners_; #endif #ifdef USE_WIFI_SCAN_RESULTS_LISTENERS StaticVector scan_results_listeners_; #endif #ifdef USE_WIFI_CONNECT_STATE_LISTENERS StaticVector connect_state_listeners_; #endif #ifdef USE_WIFI_POWER_SAVE_LISTENERS StaticVector power_save_listeners_; #endif ESPPreferenceObject pref_; #ifdef USE_WIFI_FAST_CONNECT ESPPreferenceObject fast_connect_pref_; #endif #ifdef USE_WIFI_CONNECT_TRIGGER Trigger<> connect_trigger_; #endif #ifdef USE_WIFI_DISCONNECT_TRIGGER Trigger<> disconnect_trigger_; #endif #if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) SemaphoreHandle_t high_performance_semaphore_{nullptr}; #endif static constexpr uint8_t FIRST_5GHZ_CHANNEL = 36; // Post-connect roaming constants static constexpr uint32_t ROAMING_CHECK_INTERVAL = 5 * 60 * 1000; // 5 minutes static constexpr int8_t ROAMING_MIN_IMPROVEMENT = 10; // dB static constexpr int8_t ROAMING_GOOD_RSSI = -49; // Skip scan if signal is excellent static constexpr uint8_t ROAMING_MAX_ATTEMPTS = 3; // Grace period after roaming scan completes. If WiFi disconnects within this // window (e.g., ESP8266 Beacon Timeout caused by going off-channel during scan), // the disconnect is treated as roaming-related and the attempts counter is preserved. static constexpr uint32_t ROAMING_SCAN_GRACE_PERIOD = 30 * 1000; // 30 seconds // 4-byte members float output_power_{NAN}; uint32_t action_started_; uint32_t last_connected_{0}; uint32_t reboot_timeout_{}; uint32_t roaming_last_check_{0}; uint32_t roaming_scan_end_{0}; // Timestamp when last roaming scan completed #ifdef USE_WIFI_AP uint32_t ap_timeout_{}; #endif // 1-byte enums and integers WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF}; WiFiPowerSaveMode power_save_{WIFI_POWER_SAVE_NONE}; #if defined(USE_ESP32) && defined(SOC_WIFI_SUPPORT_5G) wifi_band_mode_t band_mode_{WIFI_BAND_MODE_AUTO}; #endif WifiMinAuthMode min_auth_mode_{WIFI_MIN_AUTH_MODE_WPA2}; WiFiRetryPhase retry_phase_{WiFiRetryPhase::INITIAL_CONNECT}; uint8_t num_retried_{0}; // Index into sta_ array for the currently selected AP configuration (-1 = none selected) // Used to access password, manual_ip, priority, EAP settings, and hidden flag // int8_t limits to 127 APs (enforced in __init__.py via MAX_WIFI_NETWORKS) int8_t selected_sta_index_{-1}; uint8_t roaming_attempts_{0}; #if USE_NETWORK_IPV6 uint8_t num_ipv6_addresses_{0}; #endif /* USE_NETWORK_IPV6 */ bool error_from_callback_{false}; #if defined(USE_ESP8266) || defined(USE_LIBRETINY) // Platform-specific STA state enum, defined in platform cpp file. // On ESP8266, written from SDK system context (wifi_event_callback) — // uint8_t writes are atomic on Xtensa LX106 so no synchronization is needed. uint8_t sta_state_{0}; #endif RetryHiddenMode retry_hidden_mode_{RetryHiddenMode::BLIND_RETRY}; RoamingState roaming_state_{RoamingState::IDLE}; bssid_t roaming_target_bssid_{}; // BSSID of the AP we're trying to roam to #if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) WiFiPowerSaveMode configured_power_save_{WIFI_POWER_SAVE_NONE}; #endif // Bools and bitfields // Pending listener callbacks deferred from platform callbacks to main loop. struct { #ifdef USE_WIFI_CONNECT_STATE_LISTENERS // Deferred until state machine reaches STA_CONNECTED so wifi.connected // condition returns true in listener automations. bool connect_state : 1; #ifdef USE_ESP8266 // ESP8266: also defer disconnect notification to main loop bool disconnect : 1; #endif #endif #if defined(USE_ESP8266) && defined(USE_WIFI_IP_STATE_LISTENERS) bool got_ip : 1; #endif #if defined(USE_ESP8266) && defined(USE_WIFI_SCAN_RESULTS_LISTENERS) bool scan_complete : 1; #endif } pending_{}; bool has_ap_{false}; #if defined(USE_WIFI_CONNECT_TRIGGER) || defined(USE_WIFI_DISCONNECT_TRIGGER) bool handled_connected_state_{false}; #endif bool scan_done_{false}; bool ap_setup_{false}; bool ap_started_{false}; bool passive_scan_{false}; bool has_saved_wifi_settings_{false}; #ifdef USE_WIFI_11KV_SUPPORT bool btm_{false}; bool rrm_{false}; #endif bool enable_on_boot_{true}; bool got_ipv4_address_{false}; bool keep_scan_results_{false}; bool has_completed_scan_after_captive_portal_start_{ false}; // Tracks if we've completed a scan after captive portal started bool skip_cooldown_next_cycle_{false}; bool connected_{false}; bool post_connect_roaming_{true}; // Enabled by default #if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) bool is_high_performance_mode_{false}; #endif #ifdef USE_ESP32 // Lock-free SPSC queue for WiFi events from ESP-IDF event handler. // 17 slots = 16 usable (ring buffer reserves one slot). WiFi events are rare. // Placed at end of class to avoid padding between smaller fields. LockFreeQueue event_queue_; #endif private: // Stores a pointer to a string literal (static storage duration). // ONLY set from Python-generated code with string literals - never dynamic strings. const char *use_address_{""}; }; extern WiFiComponent *global_wifi_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) } // namespace esphome::wifi #endif