ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
wifi_component_esp_idf.cpp
Go to the documentation of this file.
1#include "wifi_component.h"
3
4#ifdef USE_WIFI
5#ifdef USE_ESP32
6
7#include <esp_event.h>
8#include <esp_netif.h>
9#include <esp_system.h>
10#include <esp_wifi.h>
11#include <esp_wifi_types.h>
12#include <freertos/FreeRTOS.h>
13#include <freertos/event_groups.h>
14#include <freertos/task.h>
15
16#include <algorithm>
17#include <cinttypes>
18#include <memory>
19#include <utility>
20#ifdef USE_WIFI_WPA2_EAP
21#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
22#include <esp_eap_client.h>
23#else
24#include <esp_wpa2.h>
25#endif
26#endif
27
28#ifdef USE_WIFI_DPP
29#include <esp_dpp.h>
30#endif
31
32#ifdef USE_WIFI_AP
33#include "dhcpserver/dhcpserver.h"
34#endif // USE_WIFI_AP
35
36#ifdef USE_CAPTIVE_PORTAL
38#endif
39
40#include "lwip/apps/sntp.h"
41#include "lwip/dns.h"
42#include "lwip/err.h"
43#ifdef USE_WIFI_LEASE_CACHE
44#include <esp_netif_net_stack.h> // esp_netif_get_netif_impl()
45#include <esp_rtc_time.h> // esp_rtc_get_time_us() -- RTC-domain clock for lease age
46#include "lwip/dhcp.h"
47#include "lwip/netif.h"
48#endif
49
51#include "esphome/core/hal.h"
53#include "esphome/core/log.h"
54#include "esphome/core/util.h"
55
56namespace esphome::wifi {
57
58ESPHOME_LOG_TAG(TAG, "wifi_esp32");
59
60static EventGroupHandle_t s_wifi_event_group; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
61static esp_netif_t *s_sta_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
62#ifdef USE_WIFI_AP
63static esp_netif_t *s_ap_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
64#endif // USE_WIFI_AP
65static bool s_sta_connected = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
66static bool s_sta_connect_not_found = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
67static bool s_sta_connect_error = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
68static bool s_sta_connecting = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
69static bool s_wifi_started = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
70
71#ifdef USE_WIFI_DPP
73
74static void copy_terminated(char *dst, const uint8_t *src, size_t len) {
75 memcpy(dst, src, len);
76 dst[len] = '\0';
77}
78#endif
79
80struct IDFWiFiEvent {
81 esp_event_base_t event_base;
82 int32_t event_id;
83 union {
84 wifi_event_sta_scan_done_t sta_scan_done;
85 wifi_event_sta_connected_t sta_connected;
86 wifi_event_sta_disconnected_t sta_disconnected;
87 wifi_event_sta_authmode_change_t sta_authmode_change;
88 wifi_event_ap_staconnected_t ap_staconnected;
89 wifi_event_ap_stadisconnected_t ap_stadisconnected;
90 wifi_event_ap_probe_req_rx_t ap_probe_req_rx;
91 wifi_event_bss_rssi_low_t bss_rssi_low;
92#ifdef USE_WIFI_DPP
93 SavedWifiSettings *dpp_credentials; // owned by the event; the rare 98 byte record does not widen the union
94 wifi_event_dpp_failed_t dpp_failed;
95#endif
96 ip_event_got_ip_t ip_got_ip;
97#if USE_NETWORK_IPV6
98 ip_event_got_ip6_t ip_got_ip6;
99#endif /* USE_NETWORK_IPV6 */
100#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
101 ip_event_assigned_ip_to_client_t ip_assigned_ip_to_client;
102#else
103 ip_event_ap_staipassigned_t ip_ap_staipassigned;
104#endif
105 } data;
106};
107
108// general design: event handler translates events and pushes them to a queue,
109// events get processed in the main loop
110void event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data) {
111 IDFWiFiEvent event;
112 memset(&event, 0, sizeof(IDFWiFiEvent));
113 event.event_base = event_base;
114 event.event_id = event_id;
115 if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) { // NOLINT(bugprone-branch-clone)
116 // no data
117 } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_STOP) { // NOLINT(bugprone-branch-clone)
118 // no data
119 } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_AUTHMODE_CHANGE) {
120 memcpy(&event.data.sta_authmode_change, event_data, sizeof(wifi_event_sta_authmode_change_t));
121 } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_CONNECTED) {
122 memcpy(&event.data.sta_connected, event_data, sizeof(wifi_event_sta_connected_t));
123 } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
124 memcpy(&event.data.sta_disconnected, event_data, sizeof(wifi_event_sta_disconnected_t));
125 } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
126 memcpy(&event.data.ip_got_ip, event_data, sizeof(ip_event_got_ip_t));
127#if USE_NETWORK_IPV6
128 } else if (event_base == IP_EVENT && event_id == IP_EVENT_GOT_IP6) {
129 memcpy(&event.data.ip_got_ip6, event_data, sizeof(ip_event_got_ip6_t));
130#endif
131#if defined(USE_WIFI_AP_WISP) && !defined(USE_WIFI_AP_WISP_STATIC_DNS)
132 } else if (event_base == IP_EVENT && event_id == IP_EVENT_ETH_GOT_IP) { // NOLINT(bugprone-branch-clone)
133 // no data: the bridge rereads the resolvers an ethernet upstream just received
134#endif
135 } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_LOST_IP) { // NOLINT(bugprone-branch-clone)
136 // no data
137 } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_SCAN_DONE) {
138 memcpy(&event.data.sta_scan_done, event_data, sizeof(wifi_event_sta_scan_done_t));
139 } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_START) { // NOLINT(bugprone-branch-clone)
140 // no data
141 } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_STOP) { // NOLINT(bugprone-branch-clone)
142 // no data
143 } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_PROBEREQRECVED) {
144 memcpy(&event.data.ap_probe_req_rx, event_data, sizeof(wifi_event_ap_probe_req_rx_t));
145 } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_STACONNECTED) {
146 memcpy(&event.data.ap_staconnected, event_data, sizeof(wifi_event_ap_staconnected_t));
147 } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_STADISCONNECTED) {
148 memcpy(&event.data.ap_stadisconnected, event_data, sizeof(wifi_event_ap_stadisconnected_t));
149#ifdef USE_WIFI_DPP
150 } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_DPP_URI_READY) {
151 // Kept on the component for the whole boot; the queued event only announces it
152 const auto *uri = static_cast<const wifi_event_dpp_uri_ready_t *>(event_data);
153 const size_t len = strnlen(uri->uri, uri->uri_data_len);
154 auto copy = std::make_unique<char[]>(len + 1);
155 copy_terminated(copy.get(), reinterpret_cast<const uint8_t *>(uri->uri), len);
156 global_wifi_component->dpp_uri_ = std::move(copy);
157 } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_DPP_CFG_RECVD) {
158 const auto &sta = static_cast<const wifi_event_dpp_config_received_t *>(event_data)->wifi_cfg.sta;
159 auto *credentials = new SavedWifiSettings; // NOLINT(cppcoreguidelines-owning-memory)
160 copy_terminated(credentials->ssid, sta.ssid, sizeof(sta.ssid));
161 copy_terminated(credentials->password, sta.password, sizeof(sta.password));
162 event.data.dpp_credentials = credentials;
163 } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_DPP_FAILED) {
164 memcpy(&event.data.dpp_failed, event_data, sizeof(wifi_event_dpp_failed_t));
165#endif
166#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
167 } else if (event_base == IP_EVENT && event_id == IP_EVENT_ASSIGNED_IP_TO_CLIENT) {
168 memcpy(&event.data.ip_assigned_ip_to_client, event_data, sizeof(ip_event_assigned_ip_to_client_t));
169#else
170 } else if (event_base == IP_EVENT && event_id == IP_EVENT_AP_STAIPASSIGNED) {
171 memcpy(&event.data.ip_ap_staipassigned, event_data, sizeof(ip_event_ap_staipassigned_t));
172#endif
173 } else {
174 // did not match any event, don't send anything
175 return;
176 }
177
178 // copy to heap — WiFi events are rare so heap alloc is fine
179 auto *to_send = new IDFWiFiEvent; // NOLINT(cppcoreguidelines-owning-memory)
180 memcpy(to_send, &event, sizeof(IDFWiFiEvent));
181 if (!global_wifi_component->event_queue_.push(to_send)) {
182#ifdef USE_WIFI_DPP
183 if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_DPP_CFG_RECVD)
184 delete to_send->data.dpp_credentials; // NOLINT(cppcoreguidelines-owning-memory)
185#endif
186 delete to_send; // NOLINT(cppcoreguidelines-owning-memory)
187 }
188}
189
191 // Network interface setup handled by network component
192 s_wifi_event_group = xEventGroupCreate();
193 if (s_wifi_event_group == nullptr) {
194 ESP_LOGE(TAG, "xEventGroupCreate failed");
195 return;
196 }
197 esp_event_handler_instance_t instance_wifi_id, instance_ip_id;
198 esp_err_t err =
199 esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, nullptr, &instance_wifi_id);
200 if (err != ERR_OK) {
201 ESP_LOGE(TAG, "esp_event_handler_instance_register failed: %s", esp_err_to_name(err));
202 return;
203 }
204 err = esp_event_handler_instance_register(IP_EVENT, ESP_EVENT_ANY_ID, &event_handler, nullptr, &instance_ip_id);
205 if (err != ERR_OK) {
206 ESP_LOGE(TAG, "esp_event_handler_instance_register failed: %s", esp_err_to_name(err));
207 return;
208 }
209 // NOTE: netif creation + esp_wifi_init() used to live here. They allocate ~15-30KB of
210 // DMA-capable internal SRAM, which competes with W5500 SPI DMA and I2S DMA on
211 // memory-tight devices. They are now deferred to wifi_lazy_init_(), called from
212 // setup() when enable_on_boot_ is true, or from enable() on first runtime enable.
213 // This makes enable_on_boot:false genuinely skip the wifi DMA allocation.
214}
215
217 if (this->wifi_initialized_)
218 return;
219
220 // Guard each creation so partial init (e.g. a failed esp_wifi_init() below)
221 // followed by a retry via enable() does not leak the existing netif handle
222 // nor re-register the default WiFi handlers.
223 if (s_sta_netif == nullptr)
224 s_sta_netif = esp_netif_create_default_wifi_sta();
225 if (s_sta_netif == nullptr) {
226 // Allocation failed; leave wifi_initialized_ false so a later enable() retries.
227 ESP_LOGE(TAG, "esp_netif_create_default_wifi_sta failed");
228 return;
229 }
230
231#ifdef USE_WIFI_AP
232 if (s_ap_netif == nullptr)
233 s_ap_netif = esp_netif_create_default_wifi_ap();
234#endif // USE_WIFI_AP
235
236 // The WiFi driver was started (e.g. by ESP-NOW with the wifi component disabled at
237 // boot) before our STA netif existed. The default WIFI_EVENT_STA_START handler
238 // therefore ran with no netif and never called esp_wifi_register_if_rxcb() -- the
239 // only thing that points the driver's RX path at a netif (it sets
240 // s_wifi_netifs[WIFI_IF_STA]). A bare esp_netif_action_start() would stop the
241 // immediate crash (#17232) but leaves RX unbound, so the first association
242 // associates at L2 yet never receives DHCP replies and times out (#17239). Restart
243 // the driver now that the netif exists so STA_START re-runs the default handler and
244 // wires RX correctly. ESP-NOW survives the stop/start (its peer state persists).
245 // This also matches a self-retry: if esp_wifi_set_storage() below failed on a
246 // previous wifi_lazy_init_() it returned without setting wifi_initialized_, and
247 // esp_wifi_init() has since run, so esp_wifi_get_mode() now succeeds here too.
248 wifi_mode_t mode;
249 if (esp_wifi_get_mode(&mode) == ESP_OK) {
250 ESP_LOGD(TAG, "WiFi driver already started without STA netif; restarting to bind it");
251 esp_err_t err = esp_wifi_stop();
252 if (err != ESP_OK) {
253 ESP_LOGW(TAG, "esp_wifi_stop failed: %s", esp_err_to_name(err));
254 }
255 // Re-apply RAM storage; the normal init path does this, but it is skipped on
256 // the self-retry case above, which would otherwise let the driver persist
257 // credentials to NVS for the rest of the boot.
258 err = esp_wifi_set_storage(WIFI_STORAGE_RAM);
259 if (err != ESP_OK) {
260 ESP_LOGW(TAG, "esp_wifi_set_storage failed: %s", esp_err_to_name(err));
261 }
262 err = esp_wifi_start();
263 if (err != ESP_OK) {
264 ESP_LOGE(TAG, "esp_wifi_start failed: %s", esp_err_to_name(err));
265 return;
266 }
267 s_wifi_started = true;
268 this->wifi_initialized_ = true;
269#ifdef USE_WIFI_DPP
270 this->wifi_dpp_init_();
271#endif
272 return;
273 }
274
275 wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
276 if (global_preferences->nvs_handle == 0) {
277 ESP_LOGW(TAG, "starting wifi without nvs");
278 cfg.nvs_enable = false;
279 }
280 esp_err_t err = esp_wifi_init(&cfg);
281 if (err != ERR_OK) {
282 ESP_LOGE(TAG, "esp_wifi_init failed: %s", esp_err_to_name(err));
283 return;
284 }
285 err = esp_wifi_set_storage(WIFI_STORAGE_RAM);
286 if (err != ERR_OK) {
287 ESP_LOGE(TAG, "esp_wifi_set_storage failed: %s", esp_err_to_name(err));
288 return;
289 }
290
291 this->wifi_initialized_ = true;
292#ifdef USE_WIFI_DPP
293 this->wifi_dpp_init_();
294#endif
295}
296
297#ifdef USE_WIFI_DPP
299 // With no callback the supplicant posts WIFI_EVENT_DPP_* events, handled with the other events
300#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
301 esp_err_t err = esp_supp_dpp_init();
302#else
303 esp_err_t err = esp_supp_dpp_init(nullptr);
304#endif
305 if (err != ESP_OK) {
306 ESP_LOGW(TAG, "%s failed: %s", LOG_STR_LITERAL("esp_supp_dpp_init"), esp_err_to_name(err));
307 return;
308 }
309 err = esp_supp_dpp_bootstrap_gen(this->dpp_channels_, DPP_BOOTSTRAP_QR_CODE, this->dpp_key_, this->dpp_device_info_);
310 if (err != ESP_OK) {
311 ESP_LOGW(TAG, "%s failed: %s", LOG_STR_LITERAL("esp_supp_dpp_bootstrap_gen"), esp_err_to_name(err));
312 }
313}
314#endif
315
316bool WiFiComponent::wifi_mode_(optional<bool> sta, optional<bool> ap) {
317 esp_err_t err;
318 wifi_mode_t current_mode = WIFI_MODE_NULL;
319 if (s_wifi_started) {
320 err = esp_wifi_get_mode(&current_mode);
321 if (err != ERR_OK) {
322 ESP_LOGW(TAG, "esp_wifi_get_mode failed: %s", esp_err_to_name(err));
323 return false;
324 }
325 }
326 bool current_sta = current_mode == WIFI_MODE_STA || current_mode == WIFI_MODE_APSTA;
327 bool current_ap = current_mode == WIFI_MODE_AP || current_mode == WIFI_MODE_APSTA;
328
329 bool set_sta = sta.value_or(current_sta);
330 bool set_ap = ap.value_or(current_ap);
331
332 wifi_mode_t set_mode;
333 if (set_sta && set_ap) {
334 set_mode = WIFI_MODE_APSTA;
335 } else if (set_sta && !set_ap) {
336 set_mode = WIFI_MODE_STA;
337 } else if (!set_sta && set_ap) {
338 set_mode = WIFI_MODE_AP;
339 } else {
340 set_mode = WIFI_MODE_NULL;
341 }
342
343 if (current_mode == set_mode)
344 return true;
345
346 if (set_sta && !current_sta) {
347 ESP_LOGV(TAG, "Enabling STA");
348 } else if (!set_sta && current_sta) {
349 ESP_LOGV(TAG, "Disabling STA");
350 }
351 if (set_ap && !current_ap) {
352 ESP_LOGV(TAG, "Enabling AP");
353 } else if (!set_ap && current_ap) {
354 ESP_LOGV(TAG, "Disabling AP");
355 }
356
357 if (set_mode == WIFI_MODE_NULL && s_wifi_started) {
358 err = esp_wifi_stop();
359 if (err != ESP_OK) {
360 ESP_LOGV(TAG, "esp_wifi_stop failed: %s", esp_err_to_name(err));
361 return false;
362 }
363 s_wifi_started = false;
364 return true;
365 }
366
367 err = esp_wifi_set_mode(set_mode);
368 if (err != ERR_OK) {
369 ESP_LOGW(TAG, "esp_wifi_set_mode failed: %s", esp_err_to_name(err));
370 return false;
371 }
372
373 if (set_mode != WIFI_MODE_NULL && !s_wifi_started) {
374 err = esp_wifi_start();
375 if (err != ESP_OK) {
376 ESP_LOGV(TAG, "esp_wifi_start failed: %s", esp_err_to_name(err));
377 return false;
378 }
379 s_wifi_started = true;
380 }
381
382 return true;
383}
384
385bool WiFiComponent::wifi_sta_pre_setup_() { return this->wifi_mode_(true, {}); }
386
387bool WiFiComponent::wifi_apply_output_power_(float output_power) {
388 int8_t val = static_cast<int8_t>(output_power * 4);
389 return esp_wifi_set_max_tx_power(val) == ESP_OK;
390}
391
393 wifi_ps_type_t power_save;
394 switch (this->power_save_) {
396 power_save = WIFI_PS_MIN_MODEM;
397 break;
399 power_save = WIFI_PS_MAX_MODEM;
400 break;
402 default:
403 power_save = WIFI_PS_NONE;
404 break;
405 }
406 bool success = esp_wifi_set_ps(power_save) == ESP_OK;
407#ifdef USE_WIFI_POWER_SAVE_LISTENERS
408 if (success) {
409 for (auto *listener : this->power_save_listeners_) {
410 listener->on_wifi_power_save(this->power_save_);
411 }
412 }
413#endif
414 return success;
415}
416
417#ifdef SOC_WIFI_SUPPORT_5G
418bool WiFiComponent::wifi_apply_band_mode_() { return esp_wifi_set_band_mode(this->band_mode_) == ESP_OK; }
419#endif
420
421bool WiFiComponent::wifi_sta_connect_(const WiFiAP &ap) {
422 // enable STA
423 if (!this->wifi_mode_(true, {}))
424 return false;
425#ifdef USE_WIFI_DPP
426 // A connect attempt ends the supplicant's listen; the next cooldown starts it again
427 this->stop_dpp_listen_();
428#endif
429
430 // https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/network/esp_wifi.html#_CPPv417wifi_sta_config_t
431 wifi_config_t conf;
432 memset(&conf, 0, sizeof(conf));
433 if (ap.ssid_.size() > sizeof(conf.sta.ssid)) {
434 ESP_LOGE(TAG, "SSID too long");
435 return false;
436 }
437 if (ap.password_.size() > sizeof(conf.sta.password)) {
438 ESP_LOGE(TAG, "Password too long");
439 return false;
440 }
441 memcpy(reinterpret_cast<char *>(conf.sta.ssid), ap.ssid_.c_str(), ap.ssid_.size());
442 memcpy(reinterpret_cast<char *>(conf.sta.password), ap.password_.c_str(), ap.password_.size());
443
444 // The weakest authmode to accept in the fast scan mode
445 if (ap.password_.empty()) {
446 conf.sta.threshold.authmode = WIFI_AUTH_OPEN;
447 } else {
448 // Set threshold based on configured minimum auth mode
449 switch (this->min_auth_mode_) {
451 conf.sta.threshold.authmode = WIFI_AUTH_WPA_PSK;
452 break;
454 conf.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK;
455 break;
457 conf.sta.threshold.authmode = WIFI_AUTH_WPA3_PSK;
458 break;
459 }
460 }
461
462#ifdef USE_WIFI_WPA2_EAP
463 if (ap.get_eap().has_value()) {
464 conf.sta.threshold.authmode = WIFI_AUTH_WPA2_ENTERPRISE;
465 }
466#endif
467
468#ifdef USE_WIFI_11KV_SUPPORT
469 conf.sta.btm_enabled = this->btm_;
470 conf.sta.rm_enabled = this->rrm_;
471#endif
472
473 if (ap.has_bssid()) {
474 conf.sta.bssid_set = true;
475 memcpy(conf.sta.bssid, ap.get_bssid().data(), 6);
476 } else {
477 conf.sta.bssid_set = false;
478 }
479 if (ap.has_channel()) {
480 conf.sta.channel = ap.get_channel();
481 conf.sta.scan_method = WIFI_FAST_SCAN;
482 } else {
483 conf.sta.scan_method = WIFI_ALL_CHANNEL_SCAN;
484 }
485 // Listen interval for ESP32 station to receive beacon when WIFI_PS_MAX_MODEM is set.
486 // Units: AP beacon intervals. Defaults to 3 if set to 0.
487 conf.sta.listen_interval = 0;
488
489 // Protected Management Frame
490 // Device will prefer to connect in PMF mode if other device also advertises PMF capability.
491 conf.sta.pmf_cfg.capable = true;
492 conf.sta.pmf_cfg.required = false;
493
494 // note, we do our own filtering
495 // The minimum rssi to accept in the fast scan mode
496 conf.sta.threshold.rssi = -127;
497
498 wifi_config_t current_conf;
499 esp_err_t err;
500 err = esp_wifi_get_config(WIFI_IF_STA, &current_conf);
501 if (err != ERR_OK) {
502 ESP_LOGW(TAG, "esp_wifi_get_config failed: %s", esp_err_to_name(err));
503 // can continue
504 }
505
506 if (memcmp(&current_conf, &conf, sizeof(wifi_config_t)) != 0) { // NOLINT
507 err = esp_wifi_disconnect();
508 if (err != ESP_OK) {
509 ESP_LOGV(TAG, "esp_wifi_disconnect failed: %s", esp_err_to_name(err));
510 return false;
511 }
512 }
513
514 err = esp_wifi_set_config(WIFI_IF_STA, &conf);
515 if (err != ESP_OK) {
516 ESP_LOGV(TAG, "esp_wifi_set_config failed: %s", esp_err_to_name(err));
517 return false;
518 }
519
520#ifdef USE_WIFI_MANUAL_IP
521 if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) {
522 return false;
523 }
524#else
525 if (!this->wifi_sta_ip_config_({})) {
526 return false;
527 }
528#endif
529
530 // setup enterprise authentication if required
531#ifdef USE_WIFI_WPA2_EAP
532 const auto &eap_opt = ap.get_eap();
533 if (eap_opt.has_value()) {
534 // note: all certificates and keys have to be null terminated. Lengths are appended by +1 to include \0.
535 const EAPAuth &eap = *eap_opt;
536#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
537 err = esp_eap_client_set_identity((uint8_t *) eap.identity.c_str(), eap.identity.length());
538#else
539 err = esp_wifi_sta_wpa2_ent_set_identity((uint8_t *) eap.identity.c_str(), eap.identity.length());
540#endif
541 if (err != ESP_OK) {
542 ESP_LOGV(TAG, "set_identity failed %d", err);
543 }
544 int ca_cert_len = strlen(eap.ca_cert);
545 int client_cert_len = strlen(eap.client_cert);
546 int client_key_len = strlen(eap.client_key);
547 if (ca_cert_len) {
548#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
549 err = esp_eap_client_set_ca_cert((uint8_t *) eap.ca_cert, ca_cert_len + 1);
550#else
551 err = esp_wifi_sta_wpa2_ent_set_ca_cert((uint8_t *) eap.ca_cert, ca_cert_len + 1);
552#endif
553 if (err != ESP_OK) {
554 ESP_LOGV(TAG, "set_ca_cert failed %d", err);
555 }
556 }
557 // workout what type of EAP this is
558 // validation is not required as the config tool has already validated it
559 if (client_cert_len && client_key_len) {
560 // if we have certs, this must be EAP-TLS
561#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
562 err = esp_eap_client_set_certificate_and_key((uint8_t *) eap.client_cert, client_cert_len + 1,
563 (uint8_t *) eap.client_key, client_key_len + 1,
564 (uint8_t *) eap.password.c_str(), eap.password.length());
565#else
566 err = esp_wifi_sta_wpa2_ent_set_cert_key((uint8_t *) eap.client_cert, client_cert_len + 1,
567 (uint8_t *) eap.client_key, client_key_len + 1,
568 (uint8_t *) eap.password.c_str(), eap.password.length());
569#endif
570 if (err != ESP_OK) {
571 ESP_LOGV(TAG, "set_cert_key failed %d", err);
572 }
573 } else {
574 // in the absence of certs, assume this is username/password based
575#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
576 err = esp_eap_client_set_username((uint8_t *) eap.username.c_str(), eap.username.length());
577#else
578 err = esp_wifi_sta_wpa2_ent_set_username((uint8_t *) eap.username.c_str(), eap.username.length());
579#endif
580 if (err != ESP_OK) {
581 ESP_LOGV(TAG, "set_username failed %d", err);
582 }
583#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
584 err = esp_eap_client_set_password((uint8_t *) eap.password.c_str(), eap.password.length());
585#else
586 err = esp_wifi_sta_wpa2_ent_set_password((uint8_t *) eap.password.c_str(), eap.password.length());
587#endif
588 if (err != ESP_OK) {
589 ESP_LOGV(TAG, "set_password failed %d", err);
590 }
591 // set TTLS Phase 2, defaults to MSCHAPV2
592#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
593 err = esp_eap_client_set_ttls_phase2_method(eap.ttls_phase_2);
594#else
595 err = esp_wifi_sta_wpa2_ent_set_ttls_phase2_method(eap.ttls_phase_2);
596#endif
597 if (err != ESP_OK) {
598 ESP_LOGV(TAG, "set_ttls_phase2_method failed %d", err);
599 }
600 }
601#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
602 err = esp_wifi_sta_enterprise_enable();
603#else
604 err = esp_wifi_sta_wpa2_ent_enable();
605#endif
606 if (err != ESP_OK) {
607 ESP_LOGV(TAG, "enterprise_enable failed %d", err);
608 }
609 }
610#endif // USE_WIFI_WPA2_EAP
611
612 // Reset flags, do this _before_ wifi_station_connect as the callback method
613 // may be called from wifi_station_connect
614 s_sta_connecting = true;
615 s_sta_connected = false;
616 s_sta_connect_error = false;
617 s_sta_connect_not_found = false;
618 // Reset IP address flags - ensures we don't report connected before DHCP completes
619 // (IP_EVENT_STA_LOST_IP doesn't always fire on disconnect)
620 this->got_ipv4_address_ = false;
621#if USE_NETWORK_IPV6
622 this->num_ipv6_addresses_ = 0;
623#endif
624
625 err = esp_wifi_connect();
626 if (err != ESP_OK) {
627 ESP_LOGW(TAG, "esp_wifi_connect failed: %s", esp_err_to_name(err));
628 return false;
629 }
630
631 return true;
632}
633
634// sntp_servermode_dhcp() is an empty macro unless lwIP takes NTP servers from DHCP; that build needs the core lock
635static void disable_dhcp_sntp() {
636#if SNTP_GET_SERVERS_FROM_DHCP || SNTP_GET_SERVERS_FROM_DHCPV6
637 LwIPLock lock;
638#endif
639 sntp_servermode_dhcp(false);
640}
641
642#ifdef USE_WIFI_LEASE_CACHE
643// Minimum lease time that must remain before a cached lease is applied.
644static constexpr uint32_t LEASE_APPLY_MARGIN_SECS = 60;
645
646// Seconds since capture on the RTC-domain clock; UINT32_MAX if the clock was reset (age unknown).
647static uint32_t lease_cache_age_secs(const SavedWifiLeaseSettings &lease) {
648 uint32_t now = static_cast<uint32_t>(esp_rtc_get_time_us() / 1000000ULL);
649 return now >= lease.saved_at ? now - lease.saved_at : UINT32_MAX;
650}
651
652// Renewal deadline (T1) from capture; half the lease when T1 is missing or invalid (RFC 2131).
653static uint32_t lease_renew_at_secs(const SavedWifiLeaseSettings &lease) {
654 uint32_t renew_at = lease.t1_renew;
655 if (renew_at == 0 || renew_at >= lease.lease_secs)
656 renew_at = lease.lease_secs / 2;
657 return renew_at;
658}
659
660// Starting the DHCP client drops the static address and DNS servers, so open connections reset
662 ESP_LOGI(TAG, "Cached lease renew deadline reached; re-acquiring the address over DHCP, connections may drop");
663 this->applied_cached_lease_ = false;
664 // Same SNTP leak guard as the DHCP path: https://github.com/esphome/issues/issues/2299
665 disable_dhcp_sntp();
666 esp_err_t err = esp_netif_dhcpc_start(s_sta_netif);
667 if (err != ESP_OK) {
668 ESP_LOGW(TAG, "Lease renew: starting DHCP client failed: %s, retrying in 60 s", esp_err_to_name(err));
669 this->set_timeout(LEASE_RENEW_TIMEOUT_ID, 60000, [this]() { this->renew_cached_lease_(); });
670 }
671}
672
674 const SavedWifiLeaseSettings &lease = this->cached_lease_;
675 if (lease.version != SAVED_WIFI_LEASE_VERSION || this->selected_sta_index_ < 0 ||
676 lease.ap_index != this->selected_sta_index_)
677 return false;
678 // The age is only known when the RTC clock survived the reset (deep sleep or software reboot).
679 esp_reset_reason_t reason = esp_reset_reason();
680 if (reason != ESP_RST_DEEPSLEEP && reason != ESP_RST_SW)
681 return false;
682 uint32_t age = lease_cache_age_secs(lease);
683 if (age == UINT32_MAX)
684 return false;
685 if (this->lease_cache_max_age_ != 0 && age > this->lease_cache_max_age_)
686 return false;
687 // Past T1 the lease needs DHCP renewal anyway.
688 if (age >= lease_renew_at_secs(lease))
689 return false;
690 // Scale the margin with the lease: the RTC RC oscillator drifts a few percent.
691 uint32_t margin = std::max<uint32_t>(LEASE_APPLY_MARGIN_SECS, lease.lease_secs / 16);
692 return lease.lease_secs > age + margin;
693}
694#endif
695
696bool WiFiComponent::wifi_sta_ip_config_(const optional<ManualIP> &manual_ip) {
697 // enable STA
698 if (!this->wifi_mode_(true, {}))
699 return false;
700
701 // Check if the STA interface is initialized before using it
702 if (s_sta_netif == nullptr) {
703 ESP_LOGW(TAG, "STA interface not initialized");
704 return false;
705 }
706
707 esp_netif_dhcp_status_t dhcp_status;
708 esp_err_t err = esp_netif_dhcpc_get_status(s_sta_netif, &dhcp_status);
709 if (err != ESP_OK) {
710 ESP_LOGV(TAG, "esp_netif_dhcpc_get_status failed: %s", esp_err_to_name(err));
711 return false;
712 }
713
714#ifdef USE_WIFI_LEASE_CACHE
715 // Without a manual IP, a still-valid cached lease is applied as a static IP to skip DHCP.
716 optional<ManualIP> effective_ip = manual_ip;
717 this->applied_cached_lease_ = false;
718 this->cancel_timeout(LEASE_RENEW_TIMEOUT_ID);
719 if (!effective_ip.has_value() && this->lease_valid_for_apply_()) {
720 const SavedWifiLeaseSettings &lease = this->cached_lease_;
721 esp_ip4_addr_t ip4{lease.ip}, mask4{lease.netmask}, gw4{lease.gateway}, dns1_4{lease.dns1}, dns2_4{lease.dns2};
722 ManualIP cached;
723 cached.static_ip = network::IPAddress(&ip4);
724 cached.gateway = network::IPAddress(&gw4);
725 cached.subnet = network::IPAddress(&mask4);
726 cached.dns1 = network::IPAddress(&dns1_4);
727 cached.dns2 = network::IPAddress(&dns2_4);
728 effective_ip = cached;
729 this->applied_cached_lease_ = true;
730
731 uint32_t age = lease_cache_age_secs(lease);
732 uint32_t remaining = lease.lease_secs > age ? lease.lease_secs - age : 0;
733 ESP_LOGI(TAG, "Applying cached lease " IPSTR ", ~%" PRIu32 "s left, skipping DHCP", IP2STR(&ip4), remaining);
734
735 // Still awake at T1: switch to real DHCP. lease_valid_for_apply_() guarantees age < T1.
736 uint32_t renew_in = lease_renew_at_secs(lease) - age;
737 // Clamp below UINT32_MAX, which the scheduler treats as "don't run".
738 uint32_t renew_ms =
739 static_cast<uint32_t>(std::min<uint64_t>(static_cast<uint64_t>(renew_in) * 1000ULL, UINT32_MAX - 1));
740 this->set_timeout(LEASE_RENEW_TIMEOUT_ID, renew_ms, [this]() { this->renew_cached_lease_(); });
741 }
742#else
743 const optional<ManualIP> &effective_ip = manual_ip;
744#endif
745
746 if (!effective_ip.has_value()) {
747 // lwIP starts the SNTP client if it gets an SNTP server from DHCP. We don't need the time, and more importantly,
748 // the built-in SNTP client has a memory leak in certain situations. Disable this feature.
749 // https://github.com/esphome/issues/issues/2299
750 disable_dhcp_sntp();
751
752 // No manual IP is set; use DHCP client
753 if (dhcp_status != ESP_NETIF_DHCP_STARTED) {
754 err = esp_netif_dhcpc_start(s_sta_netif);
755 if (err != ESP_OK) {
756 ESP_LOGV(TAG, "Starting DHCP client failed: %d", err);
757 }
758 return err == ESP_OK;
759 }
760 return true;
761 }
762
763 esp_netif_ip_info_t info; // struct of ip4_addr_t with ip, netmask, gw
764 info.ip = effective_ip->static_ip;
765 info.gw = effective_ip->gateway;
766 info.netmask = effective_ip->subnet;
767 err = esp_netif_dhcpc_stop(s_sta_netif);
768 if (err != ESP_OK && err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED) {
769 ESP_LOGV(TAG, "Stopping DHCP client failed: %s", esp_err_to_name(err));
770 }
771
772 err = esp_netif_set_ip_info(s_sta_netif, &info);
773 if (err != ESP_OK) {
774 ESP_LOGV(TAG, "Setting manual IP info failed: %s", esp_err_to_name(err));
775 }
776
777 esp_netif_dns_info_t dns;
778 if (effective_ip->dns1.is_set()) {
779 dns.ip = effective_ip->dns1;
780 esp_netif_set_dns_info(s_sta_netif, ESP_NETIF_DNS_MAIN, &dns);
781 }
782 if (effective_ip->dns2.is_set()) {
783 dns.ip = effective_ip->dns2;
784 esp_netif_set_dns_info(s_sta_netif, ESP_NETIF_DNS_BACKUP, &dns);
785 }
786
787 return true;
788}
789
790#ifdef USE_WIFI_LEASE_CACHE
792 // A cached lease applied as a static IP has no DHCP timing to capture.
793 if (s_sta_netif == nullptr || this->applied_cached_lease_)
794 return;
795
796#ifdef USE_WIFI_MANUAL_IP
797 if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value())
798 return;
799#endif
800
801 esp_netif_ip_info_t info;
802 if (esp_netif_get_ip_info(s_sta_netif, &info) != ESP_OK || info.ip.addr == 0)
803 return;
804
805 SavedWifiLeaseSettings lease{};
806 lease.version = SAVED_WIFI_LEASE_VERSION;
807 lease.ap_index = this->selected_sta_index_;
808 lease.ip = info.ip.addr;
809 lease.netmask = info.netmask.addr;
810 lease.gateway = info.gw.addr;
811
812 esp_netif_dns_info_t dns;
813 if (esp_netif_get_dns_info(s_sta_netif, ESP_NETIF_DNS_MAIN, &dns) == ESP_OK && dns.ip.type == ESP_IPADDR_TYPE_V4) {
814 lease.dns1 = dns.ip.u_addr.ip4.addr;
815 }
816 if (esp_netif_get_dns_info(s_sta_netif, ESP_NETIF_DNS_BACKUP, &dns) == ESP_OK && dns.ip.type == ESP_IPADDR_TYPE_V4) {
817 lease.dns2 = dns.ip.u_addr.ip4.addr;
818 }
819
820 // Lease timing is only in lwIP's internal DHCP state; without it the lease can't be aged.
821 bool have_timing = false;
822 if (auto *impl = static_cast<struct netif *>(esp_netif_get_netif_impl(s_sta_netif))) {
823 LwIPLock lock;
824 if (struct dhcp *d = netif_dhcp_data(impl)) {
825 lease.lease_secs = d->offered_t0_lease;
826 lease.t1_renew = d->offered_t1_renew;
827 have_timing = true;
828 }
829 }
830 if (!have_timing)
831 return;
832
833 lease.saved_at = static_cast<uint32_t>(esp_rtc_get_time_us() / 1000000ULL);
834
835 // On flash, skip a write that would only bump saved_at (an older one only overstates the age),
836 // unless it is from a previous RTC clock period (power cycle).
837#ifdef USE_WIFI_LEASE_CACHE_IN_FLASH
838 constexpr size_t compare_len = offsetof(SavedWifiLeaseSettings, saved_at);
839#else
840 constexpr size_t compare_len = sizeof(SavedWifiLeaseSettings);
841#endif
842 const SavedWifiLeaseSettings &previous = this->cached_lease_;
843 if (previous.saved_at <= lease.saved_at && memcmp(&previous, &lease, compare_len) == 0)
844 return;
845
846 this->lease_cache_pref_.save(&lease);
847 this->cached_lease_ = lease;
848
849 ESP_LOGD(TAG, "Lease cache: saved " IPSTR " (network %d, lease %" PRIu32 "s)", IP2STR(&info.ip), lease.ap_index,
850 lease.lease_secs);
851}
852#endif
853
854esp_netif_t *WiFiComponent::get_esp_netif_sta() { return s_sta_netif; }
855
857 if (!this->has_sta())
858 return {};
859 network::IPAddresses addresses;
860 esp_netif_ip_info_t ip;
861 esp_err_t err = esp_netif_get_ip_info(s_sta_netif, &ip);
862 if (err != ESP_OK) {
863 ESP_LOGV(TAG, "esp_netif_get_ip_info failed: %s", esp_err_to_name(err));
864 // TODO: do something smarter
865 // return false;
866 } else {
867 addresses[0] = network::IPAddress(&ip.ip);
868 }
869#if USE_NETWORK_IPV6
870 network::fill_ip6_addresses(s_sta_netif, addresses);
871#endif /* USE_NETWORK_IPV6 */
872 return addresses;
873}
874
876 // setting is done in SYSTEM_EVENT_STA_START callback
877 return true;
878}
879const char *get_auth_mode_str(uint8_t mode) {
880 switch (mode) {
881 case WIFI_AUTH_OPEN:
882 return "OPEN";
883 case WIFI_AUTH_WEP:
884 return "WEP";
885 case WIFI_AUTH_WPA_PSK:
886 return "WPA PSK";
887 case WIFI_AUTH_WPA2_PSK:
888 return "WPA2 PSK";
889 case WIFI_AUTH_WPA_WPA2_PSK:
890 return "WPA/WPA2 PSK";
891 case WIFI_AUTH_WPA2_ENTERPRISE:
892 return "WPA2 Enterprise";
893 case WIFI_AUTH_WPA3_PSK:
894 return "WPA3 PSK";
895 case WIFI_AUTH_WPA2_WPA3_PSK:
896 return "WPA2/WPA3 PSK";
897 case WIFI_AUTH_WAPI_PSK:
898 return "WAPI PSK";
899 default:
900 return "UNKNOWN";
901 }
902}
903
904const char *get_disconnect_reason_str(uint8_t reason) {
905 switch (reason) {
906 case WIFI_REASON_AUTH_EXPIRE:
907 return "Auth Expired";
908 case WIFI_REASON_AUTH_LEAVE:
909 return "Auth Leave";
910#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
911 case WIFI_REASON_DISASSOC_DUE_TO_INACTIVITY:
912 return "Disassociated Due to Inactivity";
913#else
914 case WIFI_REASON_ASSOC_EXPIRE:
915 return "Association Expired";
916#endif
917 case WIFI_REASON_ASSOC_TOOMANY:
918 return "Too Many Associations";
919#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
920 case WIFI_REASON_CLASS2_FRAME_FROM_NONAUTH_STA:
921 return "Class 2 Frame from Non-Authenticated STA";
922 case WIFI_REASON_CLASS3_FRAME_FROM_NONASSOC_STA:
923 return "Class 3 Frame from Non-Associated STA";
924#else
925 case WIFI_REASON_NOT_AUTHED:
926 return "Not Authenticated";
927 case WIFI_REASON_NOT_ASSOCED:
928 return "Not Associated";
929#endif
930 case WIFI_REASON_ASSOC_LEAVE:
931 return "Association Leave";
932 case WIFI_REASON_ASSOC_NOT_AUTHED:
933 return "Association not Authenticated";
934 case WIFI_REASON_DISASSOC_PWRCAP_BAD:
935 return "Disassociate Power Cap Bad";
936 case WIFI_REASON_DISASSOC_SUPCHAN_BAD:
937 return "Disassociate Supported Channel Bad";
938 case WIFI_REASON_IE_INVALID:
939 return "IE Invalid";
940 case WIFI_REASON_MIC_FAILURE:
941 return "Mic Failure";
942 case WIFI_REASON_4WAY_HANDSHAKE_TIMEOUT:
943 return "4-Way Handshake Timeout";
944 case WIFI_REASON_GROUP_KEY_UPDATE_TIMEOUT:
945 return "Group Key Update Timeout";
946 case WIFI_REASON_IE_IN_4WAY_DIFFERS:
947 return "IE In 4-Way Handshake Differs";
948 case WIFI_REASON_GROUP_CIPHER_INVALID:
949 return "Group Cipher Invalid";
950 case WIFI_REASON_PAIRWISE_CIPHER_INVALID:
951 return "Pairwise Cipher Invalid";
952 case WIFI_REASON_AKMP_INVALID:
953 return "AKMP Invalid";
954 case WIFI_REASON_UNSUPP_RSN_IE_VERSION:
955 return "Unsupported RSN IE version";
956 case WIFI_REASON_INVALID_RSN_IE_CAP:
957 return "Invalid RSN IE Cap";
958 case WIFI_REASON_802_1X_AUTH_FAILED:
959 return "802.1x Authentication Failed";
960 case WIFI_REASON_CIPHER_SUITE_REJECTED:
961 return "Cipher Suite Rejected";
962 case WIFI_REASON_BEACON_TIMEOUT:
963 return "Beacon Timeout";
964 case WIFI_REASON_NO_AP_FOUND:
965 return "AP Not Found";
966 case WIFI_REASON_AUTH_FAIL:
967 return "Authentication Failed";
968 case WIFI_REASON_ASSOC_FAIL:
969 return "Association Failed";
970 case WIFI_REASON_HANDSHAKE_TIMEOUT:
971 return "Handshake Failed";
972 case WIFI_REASON_CONNECTION_FAIL:
973 return "Connection Failed";
974 case WIFI_REASON_AP_TSF_RESET:
975 return "AP TSF reset";
976 case WIFI_REASON_ROAMING:
977 return "Station Roaming";
978 case WIFI_REASON_ASSOC_COMEBACK_TIME_TOO_LONG:
979 return "Association comeback time too long";
980 case WIFI_REASON_SA_QUERY_TIMEOUT:
981 return "SA query timeout";
982#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 2, 0)
983 case WIFI_REASON_NO_AP_FOUND_W_COMPATIBLE_SECURITY:
984 return "No AP found with compatible security";
985 case WIFI_REASON_NO_AP_FOUND_IN_AUTHMODE_THRESHOLD:
986 return "No AP found in auth mode threshold";
987 case WIFI_REASON_NO_AP_FOUND_IN_RSSI_THRESHOLD:
988 return "No AP found in RSSI threshold";
989#endif
990 case WIFI_REASON_UNSPECIFIED:
991 default:
992 return "Unspecified";
993 }
994}
995
996#ifdef USE_WIFI_AP
997// The driver's own station list cannot drift, so the count is read from it rather than kept by arithmetic
998static void read_ap_clients(uint8_t &count) {
999 wifi_sta_list_t sta_list;
1000 if (esp_wifi_ap_get_sta_list(&sta_list) == ESP_OK)
1001 count = sta_list.num;
1002}
1003#endif
1004
1006 // Use pop() directly instead of empty() — pop() costs 1 memw (acquire on tail_),
1007 // while empty() costs 2 memw (acquire on both head_ and tail_) on Xtensa.
1008 IDFWiFiEvent *data = this->event_queue_.pop();
1009 if (data == nullptr)
1010 return false;
1011
1012 do {
1013 wifi_process_event_(data);
1014 delete data; // NOLINT(cppcoreguidelines-owning-memory)
1015 } while ((data = this->event_queue_.pop()) != nullptr);
1016
1017 // Drops only occur when the queue is full, and only this loop drains it,
1018 // so if pop() returned nullptr above we can skip this check.
1019 uint16_t dropped = this->event_queue_.get_and_reset_dropped_count();
1020 if (dropped > 0) {
1021 ESP_LOGW(TAG, "Dropped %u WiFi events due to buffer overflow", dropped);
1022#ifdef USE_WIFI_AP
1023 // A dropped join or leave is made up for by reading the list again
1024 if (this->ap_setup_)
1025 read_ap_clients(this->ap_clients_);
1026#endif
1027 }
1028 return true;
1029}
1030// Events are processed from queue in main loop context, but listener notifications
1031// must be deferred until after the state machine transitions (in check_connecting_finished)
1032// so that conditions like wifi.connected return correct values in automations.
1033void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
1034 esp_err_t err;
1035 if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_START) {
1036 ESP_LOGV(TAG, "STA start");
1037 // apply hostname
1038 err = esp_netif_set_hostname(s_sta_netif, App.get_name().c_str());
1039 if (err != ERR_OK) {
1040 ESP_LOGW(TAG, "esp_netif_set_hostname failed: %s", esp_err_to_name(err));
1041 }
1042
1043 // re-apply power save mode
1045#ifdef SOC_WIFI_SUPPORT_5G
1047#endif
1048
1049 } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_STOP) {
1050 ESP_LOGV(TAG, "STA stop");
1051 s_sta_connecting = false;
1052
1053 } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_AUTHMODE_CHANGE) {
1054 const auto &it = data->data.sta_authmode_change;
1055 ESP_LOGV(TAG, "Authmode Change old=%s new=%s", get_auth_mode_str(it.old_mode), get_auth_mode_str(it.new_mode));
1056
1057 } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_CONNECTED) {
1058 const auto &it = data->data.sta_connected;
1059#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
1060 char bssid_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
1061 format_mac_addr_upper(it.bssid, bssid_buf);
1062 ESP_LOGV(TAG, "Connected ssid='%.*s' bssid=" LOG_SECRET("%s") " channel=%u, authmode=%s", it.ssid_len,
1063 (const char *) it.ssid, bssid_buf, it.channel, get_auth_mode_str(it.authmode));
1064#endif
1065 s_sta_connected = true;
1066#if USE_NETWORK_IPV6
1067 // Start SLAAC on association, not after a DHCPv4 lease that may never arrive.
1068 if (esp_err_t ll_err = esp_netif_create_ip6_linklocal(s_sta_netif); ll_err != ESP_OK) {
1069 ESP_LOGW(TAG, "esp_netif_create_ip6_linklocal failed: %s", esp_err_to_name(ll_err));
1070 }
1071#endif /* USE_NETWORK_IPV6 */
1073 // Driver-initiated roam: the WIFI_REASON_ROAMING disconnect was ignored,
1074 // so the state machine never left STA_CONNECTED.
1075#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_INFO
1076 char roam_bssid_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
1077 format_mac_addr_upper(it.bssid, roam_bssid_s);
1078 ESP_LOGI(TAG, "Roamed ssid='%.*s' bssid=" LOG_SECRET("%s") " channel=%u", it.ssid_len, (const char *) it.ssid,
1079 roam_bssid_s, it.channel);
1080#endif
1081 bssid_t roam_bssid;
1082 std::copy(it.bssid, it.bssid + 6, roam_bssid.begin());
1083 this->handle_driver_roam_(roam_bssid, it.channel);
1084 }
1085#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
1086 // Defer listener notification until state machine reaches STA_CONNECTED
1087 // This ensures wifi.connected condition returns true in listener automations
1088 this->pending_.connect_state = true;
1089#endif
1090 // For static IP configurations (manual or cached lease), GOT_IP event may not fire, so notify IP listeners here
1091#if defined(USE_WIFI_IP_STATE_LISTENERS) && (defined(USE_WIFI_MANUAL_IP) || defined(USE_WIFI_LEASE_CACHE))
1092 bool static_ip = false;
1093#ifdef USE_WIFI_MANUAL_IP
1094 const WiFiAP *config = this->get_selected_sta_();
1095 static_ip = config && config->get_manual_ip().has_value();
1096#endif
1097#ifdef USE_WIFI_LEASE_CACHE
1098 static_ip = static_ip || this->applied_cached_lease_;
1099#endif
1100 if (static_ip) {
1102 }
1103#endif
1104
1105 } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_DISCONNECTED) {
1106 const auto &it = data->data.sta_disconnected;
1107 if (it.reason == WIFI_REASON_NO_AP_FOUND) {
1108 ESP_LOGW(TAG, "Disconnected ssid='%.*s' reason='Probe Request Unsuccessful'", it.ssid_len,
1109 (const char *) it.ssid);
1110 s_sta_connect_not_found = true;
1111 } else if (it.reason == WIFI_REASON_ROAMING) {
1112 ESP_LOGI(TAG, "Disconnected ssid='%.*s' reason='Station Roaming'", it.ssid_len, (const char *) it.ssid);
1113 return;
1114 } else {
1115 char bssid_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
1116 format_mac_addr_upper(it.bssid, bssid_s);
1117 ESP_LOGW(TAG, "Disconnected ssid='%.*s' bssid=" LOG_SECRET("%s") " reason='%s'", it.ssid_len,
1118 (const char *) it.ssid, bssid_s, get_disconnect_reason_str(it.reason));
1119 s_sta_connect_error = true;
1120 }
1121 s_sta_connected = false;
1122 s_sta_connecting = false;
1123 error_from_callback_ = true;
1124 // Refresh is_connected() cache; error_from_callback_ makes it false.
1126#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
1128#endif
1129
1130#if defined(USE_WIFI_AP_WISP) && !defined(USE_WIFI_AP_WISP_STATIC_DNS)
1131 } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_ETH_GOT_IP) {
1132 this->apply_ap_dns_();
1133#endif
1134 } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_STA_GOT_IP) {
1135 const auto &it = data->data.ip_got_ip;
1136 ESP_LOGV(TAG, "static_ip=" IPSTR " gateway=" IPSTR, IP2STR(&it.ip_info.ip), IP2STR(&it.ip_info.gw));
1137 this->got_ipv4_address_ = true;
1138#if defined(USE_WIFI_AP_WISP) && !defined(USE_WIFI_AP_WISP_STATIC_DNS)
1139 this->apply_ap_dns_();
1140#endif
1141#ifdef USE_WIFI_LEASE_CACHE
1142 this->save_lease_settings_(); // events are drained on the main loop
1143#endif
1144#ifdef USE_WIFI_IP_STATE_LISTENERS
1146#endif
1147
1148#if USE_NETWORK_IPV6
1149 } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_GOT_IP6) {
1150 const auto &it = data->data.ip_got_ip6;
1151 ESP_LOGV(TAG, "IPv6 address=" IPV6STR, IPV62STR(it.ip6_info.ip));
1152 this->num_ipv6_addresses_++;
1153#ifdef USE_WIFI_IP_STATE_LISTENERS
1155#endif
1156#endif /* USE_NETWORK_IPV6 */
1157
1158 } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_STA_LOST_IP) {
1159 ESP_LOGV(TAG, "Lost IP");
1160 this->got_ipv4_address_ = false;
1161
1162 } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_SCAN_DONE) {
1163 const auto &it = data->data.sta_scan_done;
1164 ESP_LOGV(TAG, "Scan done: status=%" PRIu32 " number=%u scan_id=%u", it.status, it.number, it.scan_id);
1165
1166 uint16_t number = it.number;
1167 const bool filtered = this->is_scan_driver_filtered_();
1168 const bool needs_full = this->needs_full_scan_results_();
1169 {
1170 // Mutate in place under the lock; blocking a portal request is fine and
1171 // avoids scratch buffers
1172 ScanResultsLock lock(this);
1173 this->scan_result_.clear();
1174 this->scan_done_ = true;
1175 if (it.status != 0) {
1176 // scan error
1177 return;
1178 }
1179
1180 if (number == 0) {
1181 // no results
1182 return;
1183 }
1184
1185 const size_t wanted = filtered ? std::min<size_t>(number, WIFI_SCAN_RESULT_BOUND) : number;
1186 // Storage is reused across the scans of one retry cycle and freed on connect; an exhausted
1187 // heap drops this scan and the retry logic scans again
1188 if (this->scan_result_.capacity() < wanted && !this->scan_result_.try_init(wanted)) {
1189 esp_wifi_clear_ap_list();
1190 ESP_LOGW(TAG, "No memory for %zu scan results", wanted);
1191 return;
1192 }
1193
1194#ifdef USE_ESP32_HOSTED
1195 // getting records one at a time fails on P4 with hosted esp32 WiFi coprocessor
1196 // Presumably an upstream bug, work-around by getting all records at once
1197 // Use stack buffer (3904 bytes / ~80 bytes per record = ~48 records) with heap fallback
1198 static constexpr size_t SCAN_RECORD_STACK_COUNT = 3904 / sizeof(wifi_ap_record_t);
1199 SmallBufferWithHeapFallback<SCAN_RECORD_STACK_COUNT, wifi_ap_record_t> records(number);
1200 err = esp_wifi_scan_get_ap_records(&number, records.get());
1201 if (err != ESP_OK) {
1202 esp_wifi_clear_ap_list();
1203 ESP_LOGW(TAG, "esp_wifi_scan_get_ap_records failed: %s", esp_err_to_name(err));
1204 return;
1205 }
1206 for (uint16_t i = 0; i < number; i++) {
1207 wifi_ap_record_t &record = records.get()[i];
1208#else
1209 // Process one record at a time to avoid large buffer allocation
1210 for (uint16_t i = 0; i < number; i++) {
1211 wifi_ap_record_t record;
1212 err = esp_wifi_scan_get_ap_record(&record);
1213 if (err != ESP_OK) {
1214 ESP_LOGW(TAG, "esp_wifi_scan_get_ap_record failed: %s", esp_err_to_name(err));
1215 esp_wifi_clear_ap_list(); // Free remaining records not yet retrieved
1216 break;
1217 }
1218#endif // USE_ESP32_HOSTED
1219
1220 const char *ssid_cstr = reinterpret_cast<const char *>(record.ssid);
1221 if (!needs_full && !this->matches_configured_network_(ssid_cstr, record.bssid)) {
1222 this->log_discarded_scan_result_(ssid_cstr, record.bssid, record.rssi, record.primary);
1223 continue;
1224 }
1225 bssid_t bssid;
1226 std::copy(record.bssid, record.bssid + 6, bssid.begin());
1227 if (this->scan_result_.size() < wanted) {
1228 this->scan_result_.emplace_back(bssid, ssid_cstr, strlen(ssid_cstr), record.primary, record.rssi,
1229 record.authmode != WIFI_AUTH_OPEN, ssid_cstr[0] == '\0');
1230 continue;
1231 }
1232 // Records arrive in scan order, not by signal, so a bounded store keeps the strongest by
1233 // replacing its weakest entry. Only SSID and signal decide here; a channel or auth constrained
1234 // network hidden behind 12 stronger APs of its own SSID is not a real deployment
1235 WiFiScanResult *weakest = &this->scan_result_[0];
1236 for (auto &res : this->scan_result_) {
1237 if (res.get_rssi() < weakest->get_rssi())
1238 weakest = &res;
1239 }
1240 if (record.rssi <= weakest->get_rssi()) {
1241 this->log_discarded_scan_result_(ssid_cstr, record.bssid, record.rssi, record.primary);
1242 continue;
1243 }
1244 // Rebuilt in place rather than assigned; assignment pulls in CompactString's operators, 104 B of flash
1245 weakest->~WiFiScanResult();
1246 new (weakest) WiFiScanResult(bssid, ssid_cstr, strlen(ssid_cstr), record.primary, record.rssi,
1247 record.authmode != WIFI_AUTH_OPEN, ssid_cstr[0] == '\0');
1248 }
1249 }
1250 ESP_LOGV(TAG, "Scan complete: %u found, %zu stored%s", number, this->scan_result_.size(),
1251 filtered ? LOG_STR_LITERAL(" (driver filtered)") : LOG_STR_LITERAL(""));
1252#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS
1254#endif
1255
1256 } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_START) {
1257 ESP_LOGV(TAG, "AP start");
1258 this->ap_started_ = true;
1259#ifdef USE_WIFI_AP_WISP
1260 // NAPT needs the interface up, so it cannot be part of the setup. Without it the clients have no way out, so
1261 // the AP goes down and its setup runs again, which brings it back up through this event.
1262 if (esp_err_t err = esp_netif_napt_enable(s_ap_netif); err != ESP_OK) {
1263 ESP_LOGE(TAG, "Failed to enable NAT: %s", esp_err_to_name(err));
1264 this->fail_bridge_();
1265 }
1266#endif
1267
1268 } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_STOP) {
1269 ESP_LOGV(TAG, "AP stop");
1270 this->ap_started_ = false;
1271
1272 } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_PROBEREQRECVED) {
1273#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
1274 const auto &it = data->data.ap_probe_req_rx;
1275 char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
1276 format_mac_addr_upper(it.mac, mac_buf);
1277 ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", mac_buf, it.rssi);
1278#endif
1279
1280 } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_STACONNECTED) {
1281#ifdef USE_WIFI_AP
1282 read_ap_clients(this->ap_clients_);
1283#endif
1284#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
1285 const auto &it = data->data.ap_staconnected;
1286 char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
1287 format_mac_addr_upper(it.mac, mac_buf);
1288 ESP_LOGV(TAG, "AP client connected MAC=%s", mac_buf);
1289#endif
1290
1291 } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_STADISCONNECTED) {
1292#ifdef USE_WIFI_AP
1293 read_ap_clients(this->ap_clients_);
1294#endif
1295#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
1296 const auto &it = data->data.ap_stadisconnected;
1297 char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
1298 format_mac_addr_upper(it.mac, mac_buf);
1299 ESP_LOGV(TAG, "AP client disconnected MAC=%s", mac_buf);
1300#endif
1301
1302#ifdef USE_WIFI_DPP
1303 } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_DPP_URI_READY) {
1304 ESP_LOGD(TAG, "DPP URI: %s", this->dpp_uri_.get());
1305#ifdef USE_WIFI_DPP_URI_TRIGGER
1306 this->dpp_uri_callback_.call(this->dpp_uri_.get());
1307#endif
1308
1309 } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_DPP_CFG_RECVD) {
1310 const std::unique_ptr<SavedWifiSettings> credentials(data->data.dpp_credentials);
1311 this->dpp_listening_ = false;
1312 ESP_LOGI(TAG, "DPP configured network " LOG_SECRET("'%s'"), credentials->ssid);
1313 // The reboot timer starts over for the connect attempt, as after a boot with saved credentials
1315 // Saved like portal credentials; from a cooldown this already restarts the retries
1316 this->save_wifi_sta(credentials->ssid, credentials->password);
1318 // Without networks the station machine was never started; an AP without a portal sits in AP
1319 this->start_connecting(*this->get_selected_sta_());
1320 }
1321
1322 } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_DPP_FAILED) {
1323 this->dpp_listening_ = false;
1324 ESP_LOGW(TAG, "DPP failed: %s", esp_err_to_name(data->data.dpp_failed.failure_reason));
1325#endif // USE_WIFI_DPP
1326
1327#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
1328 } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_ASSIGNED_IP_TO_CLIENT) {
1329 const auto &it = data->data.ip_assigned_ip_to_client;
1330#else
1331 } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_AP_STAIPASSIGNED) {
1332 const auto &it = data->data.ip_ap_staipassigned;
1333#endif
1334 ESP_LOGV(TAG, "AP client assigned IP " IPSTR, IP2STR(&it.ip));
1335 }
1336}
1337
1339 if (s_sta_connected && this->got_ipv4_address_) {
1340#if USE_NETWORK_IPV6 && (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0)
1341 if (this->num_ipv6_addresses_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT) {
1343 }
1344#else
1346#endif /* USE_NETWORK_IPV6 */
1347 }
1348 if (s_sta_connect_error) {
1350 }
1351 if (s_sta_connect_not_found) {
1353 }
1354 if (s_sta_connecting) {
1356 }
1358}
1359bool WiFiComponent::wifi_scan_start_(bool passive) {
1360 // enable STA
1361 if (!this->wifi_mode_(true, {}))
1362 return false;
1363
1364 wifi_scan_config_t config{};
1365 config.ssid = nullptr;
1366 config.bssid = nullptr;
1367#ifndef USE_WIFI_MULTI_SSID
1368 // One configured network with an SSID: let the driver keep only its APs, so the WiFi library
1369 // holds fewer records during the scan. Full results (portal, provisioning, listeners) and a
1370 // network configured by BSSID alone still scan everything
1371 this->scan_driver_filtered_ =
1372 !this->needs_full_scan_results_() && this->sta_.size() == 1 && !this->sta_[0].get_ssid().empty();
1373 if (this->scan_driver_filtered_) {
1374 config.ssid = const_cast<uint8_t *>(reinterpret_cast<const uint8_t *>(this->sta_[0].get_ssid().c_str()));
1375 }
1376#endif
1377 config.channel = 0;
1378 config.show_hidden = true;
1379 config.scan_type = passive ? WIFI_SCAN_TYPE_PASSIVE : WIFI_SCAN_TYPE_ACTIVE;
1380 if (passive) {
1381 config.scan_time.passive = 300;
1382 } else {
1383 config.scan_time.active.min = 100;
1384 config.scan_time.active.max = 300;
1385 }
1386 // When scanning while connected (roaming), return to home channel between
1387 // each scanned channel to maintain the connection (helps with BLE/WiFi coexistence)
1388#ifdef CONFIG_SOC_WIFI_SUPPORTED
1389 if (this->is_roaming_scan_active()) {
1390 config.coex_background_scan = true;
1391 }
1392#endif
1393
1394 esp_err_t err = esp_wifi_scan_start(&config, false);
1395 if (err != ESP_OK) {
1396 ESP_LOGV(TAG, "esp_wifi_scan_start failed: %s", esp_err_to_name(err));
1397 return false;
1398 }
1399
1400 this->scan_done_ = false;
1401 return true;
1402}
1403
1404#ifdef USE_WIFI_AP
1405bool WiFiComponent::wifi_ap_ip_config_(const optional<ManualIP> &manual_ip) {
1406 esp_err_t err;
1407
1408 // Check if the AP interface is initialized before using it
1409 if (s_ap_netif == nullptr) {
1410 ESP_LOGW(TAG, "AP interface not initialized");
1411 return false;
1412 }
1413
1414 esp_netif_ip_info_t info;
1415 if (manual_ip.has_value()) {
1416 info.ip = manual_ip->static_ip;
1417 info.gw = manual_ip->gateway;
1418 info.netmask = manual_ip->subnet;
1419 } else {
1420 info.ip = network::IPAddress(192, 168, 4, 1);
1421 info.gw = network::IPAddress(192, 168, 4, 1);
1422 info.netmask = network::IPAddress(255, 255, 255, 0);
1423 }
1424
1425 err = esp_netif_dhcps_stop(s_ap_netif);
1426 if (err != ESP_OK && err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED) {
1427 ESP_LOGE(TAG, "esp_netif_dhcps_stop failed: %s", esp_err_to_name(err));
1428 return false;
1429 }
1430
1431 err = esp_netif_set_ip_info(s_ap_netif, &info);
1432 if (err != ESP_OK) {
1433 ESP_LOGE(TAG, "esp_netif_set_ip_info failed: %d", err);
1434 return false;
1435 }
1436
1437 dhcps_lease_t lease;
1438 lease.enable = true;
1439 network::IPAddress start_address = network::IPAddress(&info.ip);
1440 start_address += 99;
1441 lease.start_ip = start_address;
1442#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
1443 char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
1444#endif
1445 ESP_LOGV(TAG, "DHCP server IP lease start: %s", start_address.str_to(ip_buf));
1446 start_address += 10;
1447 lease.end_ip = start_address;
1448 ESP_LOGV(TAG, "DHCP server IP lease end: %s", start_address.str_to(ip_buf));
1449 err = esp_netif_dhcps_option(s_ap_netif, ESP_NETIF_OP_SET, ESP_NETIF_REQUESTED_IP_ADDRESS, &lease, sizeof(lease));
1450
1451 if (err != ESP_OK) {
1452 ESP_LOGE(TAG, "esp_netif_dhcps_option failed: %d", err);
1453 return false;
1454 }
1455
1456#if (defined(USE_CAPTIVE_PORTAL) || defined(USE_WEBSERVER_CAPTIVE)) && ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 4, 0)
1457 // DHCP option 114 (captive portal URI) lets clients find the portal; either define means one exists
1458 // Buffer must be static - dhcps_set_option_info stores pointer, doesn't copy
1459 static char captive_portal_uri[24]; // "http://" (7) + IPv4 max (15) + null
1460 memcpy(captive_portal_uri, "http://", 7); // NOLINT(bugprone-not-null-terminated-result) - str_to null-terminates
1461 network::IPAddress(&info.ip).str_to(captive_portal_uri + 7);
1462 err = esp_netif_dhcps_option(s_ap_netif, ESP_NETIF_OP_SET, ESP_NETIF_CAPTIVEPORTAL_URI, captive_portal_uri,
1463 strlen(captive_portal_uri));
1464 if (err != ESP_OK) {
1465 ESP_LOGV(TAG, "Failed to set DHCP captive portal URI: %s", esp_err_to_name(err));
1466 } else {
1467 ESP_LOGV(TAG, "DHCP Captive Portal URI set to: %s", captive_portal_uri);
1468 }
1469#endif
1470
1471#ifdef USE_WIFI_AP_WISP
1472 // Offer resolvers through DHCP: the configured ones, otherwise the station's. Without them the server would
1473 // offer the AP's own address, so a failure fails the setup and it is tried again.
1474 uint8_t dns_offer = 1;
1475 err =
1476 esp_netif_dhcps_option(s_ap_netif, ESP_NETIF_OP_SET, ESP_NETIF_DOMAIN_NAME_SERVER, &dns_offer, sizeof(dns_offer));
1477 if (err != ESP_OK) {
1478 ESP_LOGE(TAG, "Failed to enable DHCP DNS option: %s", esp_err_to_name(err));
1479 }
1480 if (err != ESP_OK || !this->write_ap_dns_())
1481 return false;
1482#endif
1483
1484 err = esp_netif_dhcps_start(s_ap_netif);
1485 // Before the access point is up, its start event brings the server up with these settings
1486 if (err != ESP_OK && err != ESP_ERR_ESP_NETIF_IF_NOT_READY) {
1487 ESP_LOGE(TAG, "esp_netif_dhcps_start failed: %d", err);
1488 return false;
1489 }
1490
1491 return true;
1492}
1493
1494bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
1495 // The address and DHCP settings go in before the access point starts: its start event starts the DHCP
1496 // server on the event task, and settings written after that race it.
1497#ifdef USE_WIFI_MANUAL_IP
1498 if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) {
1499#else
1500 if (!this->wifi_ap_ip_config_({})) {
1501#endif
1502 ESP_LOGE(TAG, "wifi_ap_ip_config_ failed:");
1503 return false;
1504 }
1505
1506 // enable AP
1507 if (!this->wifi_mode_({}, true))
1508 return false;
1509
1510 wifi_config_t conf;
1511 memset(&conf, 0, sizeof(conf));
1512 if (ap.ssid_.size() > sizeof(conf.ap.ssid)) {
1513 ESP_LOGE(TAG, "AP SSID too long");
1514 return false;
1515 }
1516 memcpy(reinterpret_cast<char *>(conf.ap.ssid), ap.ssid_.c_str(), ap.ssid_.size());
1517 conf.ap.channel = ap.has_channel() ? ap.get_channel() : 1;
1518 conf.ap.ssid_hidden = ap.get_hidden();
1519 conf.ap.max_connection = 5;
1520 conf.ap.beacon_interval = 100;
1521
1522 if (ap.password_.empty()) {
1523 conf.ap.authmode = WIFI_AUTH_OPEN;
1524 *conf.ap.password = 0;
1525 } else {
1526 conf.ap.authmode = WIFI_AUTH_WPA2_PSK;
1527 if (ap.password_.size() > sizeof(conf.ap.password)) {
1528 ESP_LOGE(TAG, "AP password too long");
1529 return false;
1530 }
1531 memcpy(reinterpret_cast<char *>(conf.ap.password), ap.password_.c_str(), ap.password_.size());
1532 }
1533
1534 // pairwise cipher of SoftAP, group cipher will be derived using this.
1535 conf.ap.pairwise_cipher = WIFI_CIPHER_TYPE_CCMP;
1536
1537 esp_err_t err = esp_wifi_set_config(WIFI_IF_AP, &conf);
1538 if (err != ESP_OK) {
1539 ESP_LOGE(TAG, "esp_wifi_set_config failed: %d", err);
1540 return false;
1541 }
1542 return true;
1543}
1544
1545network::IPAddress WiFiComponent::wifi_soft_ap_ip() {
1546 esp_netif_ip_info_t ip;
1547 esp_netif_get_ip_info(s_ap_netif, &ip);
1548 return network::IPAddress(&ip.ip);
1549}
1550#endif // USE_WIFI_AP
1551
1552#ifdef USE_WIFI_DPP
1554 esp_err_t err = esp_supp_dpp_start_listen();
1555 if (err != ESP_OK) {
1556 ESP_LOGW(TAG, "%s failed: %s", LOG_STR_LITERAL("esp_supp_dpp_start_listen"), esp_err_to_name(err));
1557 return false;
1558 }
1559 ESP_LOGD(TAG, "DPP listening for a configurator");
1560 return true;
1561}
1562
1563void WiFiComponent::wifi_dpp_stop_listen_() { esp_supp_dpp_stop_listen(); }
1564#endif
1565
1566bool WiFiComponent::wifi_disconnect_() { return esp_wifi_disconnect(); }
1567
1569 bssid_t bssid{};
1570 wifi_ap_record_t info;
1571 esp_err_t err = esp_wifi_sta_get_ap_info(&info);
1572 if (err != ESP_OK) {
1573 // Very verbose only: this is expected during dump_config() before connection is established (PR #9823)
1574 ESP_LOGVV(TAG, "esp_wifi_sta_get_ap_info failed: %s", esp_err_to_name(err));
1575 return bssid;
1576 }
1577 std::copy(info.bssid, info.bssid + 6, bssid.begin());
1578 return bssid;
1579}
1580const char *WiFiComponent::wifi_ssid_to(std::span<char, SSID_BUFFER_SIZE> buffer) {
1581 wifi_ap_record_t info{};
1582 esp_err_t err = esp_wifi_sta_get_ap_info(&info);
1583 if (err != ESP_OK) {
1584 buffer[0] = '\0';
1585 return buffer.data();
1586 }
1587 // info.ssid is uint8[33], but only 32 bytes are SSID data
1588 size_t len = strnlen(reinterpret_cast<const char *>(info.ssid), 32);
1589 memcpy(buffer.data(), info.ssid, len);
1590 buffer[len] = '\0';
1591 return buffer.data();
1592}
1593int8_t WiFiComponent::wifi_rssi() {
1594 wifi_ap_record_t info;
1595 esp_err_t err = esp_wifi_sta_get_ap_info(&info);
1596 if (err != ESP_OK) {
1597 // Very verbose only: this is expected during dump_config() before connection is established (PR #9823)
1598 ESP_LOGVV(TAG, "esp_wifi_sta_get_ap_info failed: %s", esp_err_to_name(err));
1599 return WIFI_RSSI_DISCONNECTED;
1600 }
1601 return info.rssi;
1602}
1604 uint8_t primary;
1605 wifi_second_chan_t second;
1606 esp_err_t err = esp_wifi_get_channel(&primary, &second);
1607 if (err != ESP_OK) {
1608 ESP_LOGW(TAG, "esp_wifi_get_channel failed: %s", esp_err_to_name(err));
1609 return 0;
1610 }
1611 return primary;
1612}
1613network::IPAddress WiFiComponent::wifi_subnet_mask_() {
1614 esp_netif_ip_info_t ip;
1615 esp_err_t err = esp_netif_get_ip_info(s_sta_netif, &ip);
1616 if (err != ESP_OK) {
1617 ESP_LOGW(TAG, "esp_netif_get_ip_info failed: %s", esp_err_to_name(err));
1618 return {};
1619 }
1620 return network::IPAddress(&ip.netmask);
1621}
1622network::IPAddress WiFiComponent::wifi_gateway_ip_() {
1623 esp_netif_ip_info_t ip;
1624 esp_err_t err = esp_netif_get_ip_info(s_sta_netif, &ip);
1625 if (err != ESP_OK) {
1626 ESP_LOGW(TAG, "esp_netif_get_ip_info failed: %s", esp_err_to_name(err));
1627 return {};
1628 }
1629 return network::IPAddress(&ip.gw);
1630}
1631network::IPAddress WiFiComponent::wifi_dns_ip_(int num) {
1632 const ip_addr_t *dns_ip = dns_getserver(num);
1633 return network::IPAddress(dns_ip);
1634}
1635
1636#ifdef USE_WIFI_AP_WISP
1637// A DHCP server interface takes a backup resolver from ESP-IDF 6.0
1638#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
1639static constexpr size_t AP_DNS_COUNT = 2;
1640#else
1641static constexpr size_t AP_DNS_COUNT = 1;
1642#endif
1643#ifdef USE_WIFI_AP_WISP_STATIC_DNS
1644// The configured resolvers as lwIP stores them, from codegen
1645static constexpr uint32_t AP_DNS[] = {
1646 WIFI_AP_WISP_DNS_0,
1647#ifdef WIFI_AP_WISP_DNS_1
1648 WIFI_AP_WISP_DNS_1,
1649#endif
1650};
1651#endif
1652
1653static uint32_t ap_dns_to_offer(size_t i) {
1654#ifdef USE_WIFI_AP_WISP_STATIC_DNS
1655 return i < std::size(AP_DNS) ? AP_DNS[i] : 0;
1656#else
1657 return ip_addr_get_ip4_u32(dns_getserver(i));
1658#endif
1659}
1660
1661bool WiFiComponent::ap_dns_known_() { return ap_dns_to_offer(0) != 0; }
1662
1664 bool ok = true;
1665 for (size_t i = 0; i < AP_DNS_COUNT; i++) {
1666 const uint32_t addr = ap_dns_to_offer(i);
1667 if (addr == 0)
1668 continue;
1669 esp_netif_dns_info_t info{};
1670 info.ip.type = ESP_IPADDR_TYPE_V4;
1671 info.ip.u_addr.ip4.addr = addr;
1672 esp_err_t err = esp_netif_set_dns_info(s_ap_netif, static_cast<esp_netif_dns_type_t>(i), &info);
1673 if (err != ESP_OK) {
1674 ESP_LOGW(TAG, "Failed to set DHCP DNS server %u: %s", (unsigned) i, esp_err_to_name(err));
1675 ok = false;
1676 } else {
1677 ESP_LOGV(TAG, "DHCP DNS server: " IPSTR, IP2STR(&info.ip.u_addr.ip4));
1678 }
1679 }
1680 return ok;
1681}
1682
1683#ifndef USE_WIFI_AP_WISP_STATIC_DNS
1685 if (!this->ap_setup_)
1686 return; // loop() brings the bridge up once the upstream has a resolver, and its setup writes them
1687 // The DHCP server holds what it offers, so there is nothing to cache; a failed update is noticed again with the
1688 // next lease. A resolver the station lost stays offered: set_dns_info rejects an empty address.
1689 bool changed = false;
1690 for (size_t i = 0; i < AP_DNS_COUNT && !changed; i++) {
1691 const uint32_t addr = ap_dns_to_offer(i);
1692 esp_netif_dns_info_t current{};
1693 changed =
1694 addr != 0 && (esp_netif_get_dns_info(s_ap_netif, static_cast<esp_netif_dns_type_t>(i), &current) != ESP_OK ||
1695 current.ip.u_addr.ip4.addr != addr);
1696 }
1697 if (!changed)
1698 return;
1699 // The IP setup programs the DHCP server from scratch without touching the soft AP, so clients stay joined
1700#ifdef USE_WIFI_MANUAL_IP
1701 const bool ok = this->wifi_ap_ip_config_(this->ap_.get_manual_ip());
1702#else
1703 const bool ok = this->wifi_ap_ip_config_({});
1704#endif
1705 if (!ok)
1706 this->fail_bridge_();
1707}
1708#endif // USE_WIFI_AP_WISP_STATIC_DNS
1709#endif // USE_WIFI_AP_WISP
1710
1711} // namespace esphome::wifi
1712#endif // USE_ESP32
1713#endif
BedjetMode mode
BedJet operating mode.
const StringRef & 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.
bool cancel_timeout(const char *name)
Cancel a timeout function.
void set_timeout(const char *name, uint32_t timeout, std::function< void()> &&f)
Set a timeout function with a const char* name.
Definition component.cpp:96
uint16_t get_and_reset_dropped_count()
bool push(T *element)
constexpr const char * c_str() const
Definition string_ref.h:78
const optional< ManualIP > & get_manual_ip() const
void notify_scan_results_listeners_()
Notify scan results listeners with current scan results.
void fail_bridge_()
Take the bridge down so loop() runs its setup again after ap_timeout.
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)
const WiFiAP * get_selected_sta_() const
WiFiSTAConnectStatus wifi_sta_connect_status_() const
void set_last_connected_(uint32_t now)
void save_wifi_sta(const std::string &ssid, const std::string &password)
SavedWifiLeaseSettings cached_lease_
wifi_scan_vector_t< WiFiScanResult > scan_result_
void notify_ip_state_listeners_()
Notify IP state listeners with current addresses.
void start_connecting(const WiFiAP &ap)
ESPPreferenceObject lease_cache_pref_
bool wifi_sta_ip_config_(const optional< ManualIP > &manual_ip)
esp_netif_t * get_esp_netif_sta()
esp_netif handle of the station interface, used by network for default-route arbitration.
bool ap_dns_known_()
Whether there is an IPv4 resolver to offer AP clients: a configured one, or the station's.
void apply_ap_dns_()
Pass the station's resolvers on to AP clients when they changed.
void wifi_process_event_(IDFWiFiEvent *data)
bool lease_valid_for_apply_() const
void notify_disconnect_state_listeners_()
Notify connect state listeners of disconnection.
friend void event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data)
void log_discarded_scan_result_(const char *ssid, const uint8_t *bssid, int8_t rssi, uint8_t channel)
Log a discarded scan result at VERBOSE level (skipped during roaming scans to avoid log overflow)
const char * wifi_ssid_to(std::span< char, SSID_BUFFER_SIZE > buffer)
Write SSID to buffer without heap allocation.
CallbackManager< void(const char *)> dpp_uri_callback_
bool is_roaming_scan_active() const
True while a post-connect roaming scan holds the radio off-channel.
network::IPAddress wifi_dns_ip_(int num)
bool matches_configured_network_(const char *ssid, const uint8_t *bssid) const
Check if network matches any configured network (for scan result filtering) Matches by SSID when conf...
struct esphome::wifi::WiFiComponent::@194 pending_
FixedVector< WiFiAP > sta_
bool wifi_ap_ip_config_(const optional< ManualIP > &manual_ip)
bool needs_full_scan_results_() const
Check if full scan results are needed (captive portal active, improv, listeners)
LockFreeQueue< IDFWiFiEvent, 17 > event_queue_
StaticVector< WiFiPowerSaveListener *, ESPHOME_WIFI_POWER_SAVE_LISTENERS > power_save_listeners_
bool wifi_apply_output_power_(float output_power)
bool wifi_mode_(optional< bool > sta, optional< bool > ap)
network::IPAddresses wifi_sta_ip_addresses()
std::unique_ptr< char[]> dpp_uri_
void handle_driver_roam_(const bssid_t &bssid, uint8_t channel)
Redo post-connect bookkeeping after a driver-initiated roam (e.g.
bool write_ap_dns_()
Hand the AP DHCP server its resolvers; false when one could not be set.
uint8_t second
unsigned count
Definition gif.cpp:51
uint32_t buffer
Definition gif.cpp:50
mopeka_std_values val[3]
const char * esp_err_to_name(esp_err_t err)
std::span< const uint8_t > data
std::array< IPAddress, 5 > IPAddresses
Definition ip_address.h:369
void fill_ip6_addresses(esp_netif_t *netif, IPAddresses &addresses)
Copies the IPv6 addresses of an esp_netif into addresses[1..], warning once when some do not fit.
Definition util.cpp:13
std::array< uint8_t, 6 > bssid_t
const LogString * get_auth_mode_str(uint8_t mode)
ESPHOME_LOG_TAG(TAG, "wifi")
const LogString * get_disconnect_reason_str(uint8_t reason)
WiFiComponent * global_wifi_component
@ WIFI_COMPONENT_STATE_AP
WiFi is in AP-only mode and internal AP is already enabled.
@ WIFI_COMPONENT_STATE_OFF
Nothing has been initialized yet.
@ WIFI_COMPONENT_STATE_STA_CONNECTED
WiFi is in STA(+AP) mode and successfully connected.
const void size_t len
Definition hal.h:68
ESPPreferences * global_preferences
const void * src
Definition hal.h:68
Application App
Global storage of Application pointer - only one Application can exist.
char * format_mac_addr_upper(const uint8_t *mac, char *output)
Format MAC address as XX:XX:XX:XX:XX:XX (uppercase, colon separators)
Definition helpers.h:1676
struct esp_netif_obj esp_netif_t
static void uint32_t
uint8_t event_id
Definition tt21100.cpp:3
SemaphoreHandle_t lock