diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index c4bfdf3c42..e9b78c9225 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -1464,6 +1464,12 @@ void WiFiComponent::check_connecting_finished(uint32_t now) { this->release_scan_results_(); +#ifdef USE_WIFI_CONNECT_STATE_LISTENERS + // Notify listeners now that state machine has reached STA_CONNECTED + // This ensures wifi.connected condition returns true in listener automations + this->notify_connect_state_listeners_(); +#endif + return; } @@ -2183,6 +2189,21 @@ void WiFiComponent::release_scan_results_() { } } +#ifdef USE_WIFI_CONNECT_STATE_LISTENERS +void WiFiComponent::notify_connect_state_listeners_() { + if (!this->pending_.connect_state) + return; + this->pending_.connect_state = false; + // Get current SSID and BSSID from the WiFi driver + char ssid_buf[SSID_BUFFER_SIZE]; + const char *ssid = this->wifi_ssid_to(ssid_buf); + bssid_t bssid = this->wifi_bssid(); + for (auto *listener : this->connect_state_listeners_) { + listener->on_wifi_connect_state(StringRef(ssid, strlen(ssid)), bssid); + } +} +#endif // USE_WIFI_CONNECT_STATE_LISTENERS + void WiFiComponent::check_roaming_(uint32_t now) { // Guard: not for hidden networks (may not appear in scan) const WiFiAP *selected = this->get_selected_sta_(); diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index 4bdc253f66..98f339809a 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -632,6 +632,11 @@ class WiFiComponent : public Component { /// 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_(); +#endif + #ifdef USE_ESP8266 static void wifi_event_callback(System_Event_t *event); void wifi_scan_done_callback_(void *arg, STATUS status); @@ -739,6 +744,16 @@ class WiFiComponent : public Component { SemaphoreHandle_t high_performance_semaphore_{nullptr}; #endif +#ifdef USE_WIFI_CONNECT_STATE_LISTENERS + // Pending listener notifications deferred until state machine reaches appropriate state. + // Listeners are notified after state transitions complete so conditions like + // wifi.connected return correct values in automations. + // Uses bitfields to minimize memory; more flags may be added as needed. + struct { + bool connect_state : 1; // Notify connect state listeners after STA_CONNECTED + } pending_{}; +#endif + #ifdef USE_WIFI_CONNECT_TRIGGER Trigger<> connect_trigger_; #endif diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index c714afaad3..c6bd40037d 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -500,6 +500,10 @@ const LogString *get_disconnect_reason_str(uint8_t reason) { } } +// TODO: This callback runs in ESP8266 system context with limited stack (~2KB). +// All listener notifications should be deferred to wifi_loop_() via pending_ flags +// to avoid stack overflow. Currently only connect_state is deferred; disconnect, +// IP, and scan listeners still run in this context and should be migrated. void WiFiComponent::wifi_event_callback(System_Event_t *event) { switch (event->event) { case EVENT_STAMODE_CONNECTED: { @@ -512,9 +516,9 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { #endif s_sta_connected = true; #ifdef USE_WIFI_CONNECT_STATE_LISTENERS - for (auto *listener : global_wifi_component->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(it.ssid, it.ssid_len), it.bssid); - } + // Defer listener notification until state machine reaches STA_CONNECTED + // This ensures wifi.connected condition returns true in listener automations + global_wifi_component->pending_.connect_state = true; #endif // For static IP configurations, GOT_IP event may not fire, so notify IP listeners here #if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP) diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index a32232a758..22bf4be483 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -710,6 +710,9 @@ void WiFiComponent::wifi_loop_() { delete data; // NOLINT(cppcoreguidelines-owning-memory) } } +// Events are processed from queue in main loop context, but listener notifications +// must be deferred until after the state machine transitions (in check_connecting_finished) +// so that conditions like wifi.connected return correct values in automations. void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { esp_err_t err; if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_START) { @@ -743,9 +746,9 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { #endif s_sta_connected = true; #ifdef USE_WIFI_CONNECT_STATE_LISTENERS - for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(it.ssid, it.ssid_len), it.bssid); - } + // Defer listener notification until state machine reaches STA_CONNECTED + // This ensures wifi.connected condition returns true in listener automations + this->pending_.connect_state = true; #endif // For static IP configurations, GOT_IP event may not fire, so notify IP listeners here #if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP) diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index af2b82c3c6..285a520ef5 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -423,7 +423,10 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ } } -// Process a single event from the queue - runs in main loop context +// Process a single event from the queue - runs in main loop context. +// Listener notifications must be deferred until after the state machine transitions +// (in check_connecting_finished) so that conditions like wifi.connected return +// correct values in automations. void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { switch (event->event_id) { case ESPHOME_EVENT_ID_WIFI_READY: { @@ -456,9 +459,9 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { // This matches ESP32 IDF behavior where s_sta_connected is set but // wifi_sta_connect_status_() also checks got_ipv4_address_ #ifdef USE_WIFI_CONNECT_STATE_LISTENERS - for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(it.ssid, it.ssid_len), it.bssid); - } + // Defer listener notification until state machine reaches STA_CONNECTED + // This ensures wifi.connected condition returns true in listener automations + this->pending_.connect_state = true; #endif // For static IP configurations, GOT_IP event may not fire, so set connected state here #ifdef USE_WIFI_MANUAL_IP diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 84c10d5d43..1ce36c2d93 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -252,6 +252,10 @@ network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return network::IPAddress(dns_ip); } +// Pico W uses polling for connection state detection. +// Connect state listener notifications are deferred until after the state machine +// transitions (in check_connecting_finished) so that conditions like wifi.connected +// return correct values in automations. void WiFiComponent::wifi_loop_() { // Handle scan completion if (this->state_ == WIFI_COMPONENT_STATE_STA_SCANNING && !cyw43_wifi_scan_active(&cyw43_state)) { @@ -278,11 +282,9 @@ void WiFiComponent::wifi_loop_() { s_sta_was_connected = true; ESP_LOGV(TAG, "Connected"); #ifdef USE_WIFI_CONNECT_STATE_LISTENERS - String ssid = WiFi.SSID(); - bssid_t bssid = this->wifi_bssid(); - for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(ssid.c_str(), ssid.length()), bssid); - } + // Defer listener notification until state machine reaches STA_CONNECTED + // This ensures wifi.connected condition returns true in listener automations + this->pending_.connect_state = true; #endif // For static IP configurations, notify IP listeners immediately as the IP is already configured #if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP)