ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
wifi_component_esp8266.cpp
Go to the documentation of this file.
1#include "wifi_component.h"
3
4#ifdef USE_WIFI
5#ifdef USE_ESP8266
6
7#include <user_interface.h>
8
9#include <cassert>
10#include <utility>
11#include <algorithm>
12#ifdef USE_WIFI_WPA2_EAP
13#include <wpa2_enterprise.h>
14#endif
15
16extern "C" {
17#include "lwip/err.h"
18#include "lwip/dns.h"
19#include "lwip/dhcp.h"
20#include "lwip/init.h" // LWIP_VERSION_
21#include "lwip/apps/sntp.h"
22#include "lwip/netif.h" // struct netif
23#include <AddrList.h>
24#include "LwipDhcpServer.h"
25#if USE_ARDUINO_VERSION_CODE < VERSION_CODE(3, 1, 0)
26#include <ESP8266WiFi.h>
27#include "ESP8266WiFiAP.h"
28#define wifi_softap_set_dhcps_lease(lease) dhcpSoftAP.set_dhcps_lease(lease)
29#define wifi_softap_set_dhcps_lease_time(time) dhcpSoftAP.set_dhcps_lease_time(time)
30#define wifi_softap_set_dhcps_offer_option(offer, mode) dhcpSoftAP.set_dhcps_offer_option(offer, mode)
31#endif
32}
33
35#include "esphome/core/hal.h"
37#include "esphome/core/log.h"
39#include "esphome/core/util.h"
40
41namespace esphome::wifi {
42
43ESPHOME_LOG_TAG(TAG, "wifi_esp8266");
44
45enum class ESP8266WiFiSTAState : uint8_t {
46 IDLE, // Not connecting
47 CONNECTING, // Connection in progress
48 ASSOCIATED, // Associated to AP, waiting for IP
49 CONNECTED, // Successfully connected with IP
50 ERROR_NOT_FOUND, // AP not found (probe failed)
51 ERROR_FAILED, // Connection failed (auth, timeout, etc.)
52};
53
54bool WiFiComponent::wifi_mode_(optional<bool> sta, optional<bool> ap) {
55 uint8_t current_mode = wifi_get_opmode();
56 bool current_sta = current_mode & 0b01;
57 bool current_ap = current_mode & 0b10;
58 bool target_sta = sta.value_or(current_sta);
59 bool target_ap = ap.value_or(current_ap);
60 if (current_sta == target_sta && current_ap == target_ap)
61 return true;
62
63 if (target_sta && !current_sta) {
64 ESP_LOGV(TAG, "Enabling STA");
65 } else if (!target_sta && current_sta) {
66 ESP_LOGV(TAG, "Disabling STA");
67 // Stop DHCP client when disabling STA
68 // See https://github.com/esp8266/Arduino/pull/5703
69 wifi_station_dhcpc_stop();
70 }
71 if (target_ap && !current_ap) {
72 ESP_LOGV(TAG, "Enabling AP");
73 } else if (!target_ap && current_ap) {
74 ESP_LOGV(TAG, "Disabling AP");
75 }
76
77 ETS_UART_INTR_DISABLE();
78 uint8_t mode = 0;
79 if (target_sta)
80 mode |= 0b01;
81 if (target_ap)
82 mode |= 0b10;
83 bool ret = wifi_set_opmode_current(mode);
84 ETS_UART_INTR_ENABLE();
85
86 if (!ret) {
87 ESP_LOGW(TAG, "Set mode failed");
88 return false;
89 }
90
91 this->ap_started_ = target_ap;
92
93 return ret;
94}
96 // ESP8266 sleep types have confusing names — LIGHT_SLEEP_T is the MORE aggressive mode.
97 // SDK enum: NONE_SLEEP_T=0, LIGHT_SLEEP_T=1, MODEM_SLEEP_T=2
98 // https://github.com/esp8266/Arduino/blob/3.1.2/tools/sdk/include/user_interface.h#L447-L451
99 // Arduino ESP32 compat confirms: WIFI_PS_MIN_MODEM=MODEM_SLEEP, WIFI_PS_MAX_MODEM=LIGHT_SLEEP
100 // https://github.com/esp8266/Arduino/blob/3.1.2/libraries/ESP8266WiFi/src/ESP8266WiFiType.h#L53-L55
101 sleep_type_t power_save;
102 switch (this->power_save_) {
104 // MODEM_SLEEP_T: only the WiFi modem sleeps between DTIM beacons, CPU stays active.
105 // Matches ESP32's WIFI_PS_MIN_MODEM.
106 power_save = MODEM_SLEEP_T;
107 break;
109 // LIGHT_SLEEP_T: both WiFi modem AND CPU suspend between DTIM beacons.
110 // Most aggressive — prevents TCP processing during sleep. Matches ESP32's WIFI_PS_MAX_MODEM.
111 // See https://github.com/esphome/esphome/issues/14999
112 power_save = LIGHT_SLEEP_T;
113 break;
115 default:
116 power_save = NONE_SLEEP_T;
117 break;
118 }
119 wifi_fpm_auto_sleep_set_in_null_mode(1);
120 bool success = wifi_set_sleep_type(power_save);
121#ifdef USE_WIFI_POWER_SAVE_LISTENERS
122 if (success) {
123 for (auto *listener : this->power_save_listeners_) {
124 listener->on_wifi_power_save(this->power_save_);
125 }
126 }
127#endif
128 return success;
129}
130
131#if LWIP_VERSION_MAJOR != 1
132/*
133 lwip v2 needs to be notified of IP changes, see also
134 https://github.com/d-a-v/Arduino/blob/0e7d21e17144cfc5f53c016191daca8723e89ee8/libraries/ESP8266WiFi/src/ESP8266WiFiSTA.cpp#L251
135 */
136#undef netif_set_addr // need to call lwIP-v1.4 netif_set_addr()
137#undef netif_set_down // need to call lwIP-v1.4 netif_set_down()
138extern "C" {
139struct netif *eagle_lwip_getif(int netif_index);
140void netif_set_addr(struct netif *netif, const ip4_addr_t *ip, const ip4_addr_t *netmask, const ip4_addr_t *gw);
141void netif_set_down(struct netif *netif);
142};
143
144// The SDK can free its WiFi connection node before taking the STA netif down, letting lwIP
145// timers (e.g. IGMP reports armed by mDNS) transmit into the dead driver and crash in
146// cnx_node_search; taking the netif down first makes the glue drop such frames (#18308).
147static void sta_netif_down() {
148 struct netif *iface = eagle_lwip_getif(STATION_IF);
149 if (iface != nullptr)
150 netif_set_down(iface);
151}
152#endif
153
154bool WiFiComponent::wifi_sta_ip_config_(const optional<ManualIP> &manual_ip) {
155 // enable STA
156 if (!this->wifi_mode_(true, {}))
157 return false;
158
159 enum dhcp_status dhcp_status = wifi_station_dhcpc_status();
160 if (!manual_ip.has_value()) {
161 // lwIP starts the SNTP client if it gets an SNTP server from DHCP. We don't need the time, and more importantly,
162 // the built-in SNTP client has a memory leak in certain situations. Disable this feature.
163 // https://github.com/esphome/issues/issues/2299
164 sntp_servermode_dhcp(false);
165
166 // Use DHCP client
167 if (dhcp_status != DHCP_STARTED) {
168 bool ret = wifi_station_dhcpc_start();
169 if (!ret) {
170 ESP_LOGV(TAG, "Starting DHCP client failed");
171 }
172 return ret;
173 }
174 return true;
175 }
176
177 bool ret = true;
178
179#if LWIP_VERSION_MAJOR != 1
180 // get current->previous IP address
181 // (check below)
182 ip_info previp{};
183 wifi_get_ip_info(STATION_IF, &previp);
184#endif
185
186 struct ip_info info{};
187 info.ip = manual_ip->static_ip;
188 info.gw = manual_ip->gateway;
189 info.netmask = manual_ip->subnet;
190
191 if (dhcp_status == DHCP_STARTED) {
192 bool dhcp_stop_ret = wifi_station_dhcpc_stop();
193 if (!dhcp_stop_ret) {
194 ESP_LOGV(TAG, "Stopping DHCP client failed");
195 ret = false;
196 }
197 }
198 bool wifi_set_info_ret = wifi_set_ip_info(STATION_IF, &info);
199 if (!wifi_set_info_ret) {
200 ESP_LOGV(TAG, "Set manual IP info failed");
201 ret = false;
202 }
203
204 ip_addr_t dns;
205 if (manual_ip->dns1.is_set()) {
206 dns = manual_ip->dns1;
207 dns_setserver(0, &dns);
208 }
209 if (manual_ip->dns2.is_set()) {
210 dns = manual_ip->dns2;
211 dns_setserver(1, &dns);
212 }
213
214#if LWIP_VERSION_MAJOR != 1
215 // trigger address change by calling lwIP-v1.4 api
216 // only when ip is already set by other mean (generally dhcp)
217 if (previp.ip.addr != 0 && previp.ip.addr != info.ip.addr) {
218 netif_set_addr(eagle_lwip_getif(STATION_IF), reinterpret_cast<const ip4_addr_t *>(&info.ip),
219 reinterpret_cast<const ip4_addr_t *>(&info.netmask), reinterpret_cast<const ip4_addr_t *>(&info.gw));
220 }
221#endif
222 return ret;
223}
224
226 if (!this->has_sta())
227 return {};
228 network::IPAddresses addresses;
229 uint8_t index = 0;
230 // addrList enumerates all lwIP netifs, including the SoftAP / fallback hotspot. Filter out
231 // the AP address so the STA address is reported as the device IP (see issue #17181).
232 struct ip_info ap_ip{};
233 wifi_get_ip_info(SOFTAP_IF, &ap_ip);
234 network::IPAddress ap_address(&ap_ip.ip);
235 bool filter_ap = ap_address.is_set();
236 for (auto &addr : addrList) {
237 network::IPAddress ip(addr.ipFromNetifNum());
238 if (filter_ap && ip == ap_address)
239 continue;
240 assert(index < addresses.size());
241 addresses[index++] = ip;
242 }
243 return addresses;
244}
246 const auto &hostname = App.get_name();
247 bool ret = wifi_station_set_hostname(const_cast<char *>(hostname.c_str()));
248 if (!ret) {
249 ESP_LOGV(TAG, "Set hostname failed");
250 }
251
252 // Update hostname on all lwIP interfaces so DHCP packets include it.
253 // lwIP includes the hostname in DHCP DISCOVER/REQUEST automatically
254 // via LWIP_NETIF_HOSTNAME — no dhcp_renew() needed. The hostname is
255 // fixed at compile time and never changes at runtime.
256 for (netif *intf = netif_list; intf; intf = intf->next) {
257#if LWIP_VERSION_MAJOR == 1
258 intf->hostname = (char *) wifi_station_get_hostname();
259#else
260 intf->hostname = wifi_station_get_hostname();
261#endif
262 }
263
264 return ret;
265}
266
268 // enable STA
269 if (!this->wifi_mode_(true, {}))
270 return false;
271
272 this->wifi_disconnect_();
273
274 struct station_config conf{};
275 memset(&conf, 0, sizeof(conf));
276 if (ap.ssid_.size() > sizeof(conf.ssid)) {
277 ESP_LOGE(TAG, "SSID too long");
278 return false;
279 }
280 if (ap.password_.size() > sizeof(conf.password)) {
281 ESP_LOGE(TAG, "Password too long");
282 return false;
283 }
284 memcpy(reinterpret_cast<char *>(conf.ssid), ap.ssid_.c_str(), ap.ssid_.size());
285 memcpy(reinterpret_cast<char *>(conf.password), ap.password_.c_str(), ap.password_.size());
286
287 if (ap.has_bssid()) {
288 conf.bssid_set = 1;
289 memcpy(conf.bssid, ap.get_bssid().data(), 6);
290 } else {
291 conf.bssid_set = 0;
292 }
293
294 if (ap.password_.empty()) {
295 conf.threshold.authmode = AUTH_OPEN;
296 } else {
297 // Set threshold based on configured minimum auth mode
298 // Note: ESP8266 doesn't support WPA3
299 switch (this->min_auth_mode_) {
301 conf.threshold.authmode = AUTH_WPA_PSK;
302 break;
304 case WIFI_MIN_AUTH_MODE_WPA3: // Fall back to WPA2 for ESP8266
305 conf.threshold.authmode = AUTH_WPA2_PSK;
306 break;
307 }
308 }
309 conf.threshold.rssi = -127;
310
311 ETS_UART_INTR_DISABLE();
312 bool ret = wifi_station_set_config_current(&conf);
313 ETS_UART_INTR_ENABLE();
314
315 if (!ret) {
316 ESP_LOGV(TAG, "Set Station config failed");
317 return false;
318 }
319
320#ifdef USE_WIFI_MANUAL_IP
321 if (!this->wifi_sta_ip_config_(ap.get_manual_ip())) {
322 return false;
323 }
324#else
325 if (!this->wifi_sta_ip_config_({})) {
326 return false;
327 }
328#endif
329
330 // setup enterprise authentication if required
331#ifdef USE_WIFI_WPA2_EAP
332 const auto &eap_opt = ap.get_eap();
333 if (eap_opt.has_value()) {
334 // note: all certificates and keys have to be null terminated. Lengths are appended by +1 to include \0.
335 const EAPAuth &eap = *eap_opt;
336 ret = wifi_station_set_enterprise_identity((uint8_t *) eap.identity.c_str(), eap.identity.length());
337 if (ret) {
338 ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_identity failed: %d", ret);
339 }
340 int ca_cert_len = strlen(eap.ca_cert);
341 int client_cert_len = strlen(eap.client_cert);
342 int client_key_len = strlen(eap.client_key);
343 if (ca_cert_len) {
344 ret = wifi_station_set_enterprise_ca_cert((uint8_t *) eap.ca_cert, ca_cert_len + 1);
345 if (ret) {
346 ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_ca_cert failed: %d", ret);
347 }
348 }
349 // workout what type of EAP this is
350 // validation is not required as the config tool has already validated it
351 if (client_cert_len && client_key_len) {
352 // if we have certs, this must be EAP-TLS
353 ret = wifi_station_set_enterprise_cert_key((uint8_t *) eap.client_cert, client_cert_len + 1,
354 (uint8_t *) eap.client_key, client_key_len + 1,
355 (uint8_t *) eap.password.c_str(), eap.password.length());
356 if (ret) {
357 ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_cert_key failed: %d", ret);
358 }
359 } else {
360 // in the absence of certs, assume this is username/password based
361 ret = wifi_station_set_enterprise_username((uint8_t *) eap.username.c_str(), eap.username.length());
362 if (ret) {
363 ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_username failed: %d", ret);
364 }
365 ret = wifi_station_set_enterprise_password((uint8_t *) eap.password.c_str(), eap.password.length());
366 if (ret) {
367 ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_set_password failed: %d", ret);
368 }
369 }
370 ret = wifi_station_set_wpa2_enterprise_auth(true);
371 if (ret) {
372 ESP_LOGV(TAG, "esp_wifi_sta_wpa2_ent_enable failed: %d", ret);
373 }
374 }
375#endif // USE_WIFI_WPA2_EAP
376
377 this->wifi_apply_hostname_();
378
379 ETS_UART_INTR_DISABLE();
380 ret = wifi_station_connect();
381 ETS_UART_INTR_ENABLE();
382 if (!ret) {
383 ESP_LOGV(TAG, "wifi_station_connect failed");
384 this->sta_state_ = static_cast<uint8_t>(ESP8266WiFiSTAState::ERROR_FAILED);
385 return false;
386 }
387 // Reset after, not before: wifi_station_connect() synchronously reports the attempt it replaces as failed
388 this->sta_state_ = static_cast<uint8_t>(ESP8266WiFiSTAState::CONNECTING);
389 this->error_from_callback_ = false;
390
391#if USE_NETWORK_IPV6
392 bool connected = false;
393 while (!connected) {
394 uint8_t ipv6_addr_count = 0;
395 for (auto addr : addrList) {
396 char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
397 ESP_LOGV(TAG, "Address %s", network::IPAddress(addr.ipFromNetifNum()).str_to(ip_buf));
398 if (addr.isV6()) {
399 ipv6_addr_count++;
400 }
401 }
402 delay(500); // NOLINT
403 connected = (ipv6_addr_count >= USE_NETWORK_MIN_IPV6_ADDR_COUNT);
404 }
405#endif /* USE_NETWORK_IPV6 */
406
407 if (ap.has_channel()) {
408 ret = wifi_set_channel(ap.get_channel());
409 if (!ret) {
410 ESP_LOGV(TAG, "wifi_set_channel failed");
411 return false;
412 }
413 }
414
415 return true;
416}
417
418class WiFiMockClass : public ESP8266WiFiGenericClass {
419 public:
420 static void _event_callback(void *event) { ESP8266WiFiGenericClass::_eventCallback(event); } // NOLINT
421};
422
423// Auth mode strings indexed by AUTH_* constants (0-4), with UNKNOWN at last index
424// Static asserts verify the SDK constants are contiguous as expected
425static_assert(AUTH_OPEN == 0 && AUTH_WEP == 1 && AUTH_WPA_PSK == 2 && AUTH_WPA2_PSK == 3 && AUTH_WPA_WPA2_PSK == 4,
426 "AUTH_* constants are not contiguous");
427PROGMEM_STRING_TABLE(AuthModeStrings, "OPEN", "WEP", "WPA PSK", "WPA2 PSK", "WPA/WPA2 PSK", "UNKNOWN");
428
429const LogString *get_auth_mode_str(uint8_t mode) {
430 return AuthModeStrings::get_log_str(mode, AuthModeStrings::LAST_INDEX);
431}
432
433// WiFi op mode strings indexed by WIFI_* constants (0-3), with UNKNOWN at last index
434static_assert(WIFI_OFF == 0 && WIFI_STA == 1 && WIFI_AP == 2 && WIFI_AP_STA == 3,
435 "WIFI_* op mode constants are not contiguous");
436PROGMEM_STRING_TABLE(OpModeStrings, "OFF", "STA", "AP", "AP+STA", "UNKNOWN");
437
438const LogString *get_op_mode_str(uint8_t mode) { return OpModeStrings::get_log_str(mode, OpModeStrings::LAST_INDEX); }
439
440// Use if-chain instead of switch to avoid jump tables in RODATA (wastes RAM on ESP8266).
441// A single switch would generate a sparse lookup table with ~175 default entries, wasting 700 bytes of RAM.
442// Even split switches still generate smaller jump tables in RODATA.
443const LogString *get_disconnect_reason_str(uint8_t reason) {
444 if (reason == REASON_AUTH_EXPIRE)
445 return LOG_STR("Auth Expired");
446 if (reason == REASON_AUTH_LEAVE)
447 return LOG_STR("Auth Leave");
448 if (reason == REASON_ASSOC_EXPIRE)
449 return LOG_STR("Association Expired");
450 if (reason == REASON_ASSOC_TOOMANY)
451 return LOG_STR("Too Many Associations");
452 if (reason == REASON_NOT_AUTHED)
453 return LOG_STR("Not Authenticated");
454 if (reason == REASON_NOT_ASSOCED)
455 return LOG_STR("Not Associated");
456 if (reason == REASON_ASSOC_LEAVE)
457 return LOG_STR("Association Leave");
458 if (reason == REASON_ASSOC_NOT_AUTHED)
459 return LOG_STR("Association not Authenticated");
460 if (reason == REASON_DISASSOC_PWRCAP_BAD)
461 return LOG_STR("Disassociate Power Cap Bad");
462 if (reason == REASON_DISASSOC_SUPCHAN_BAD)
463 return LOG_STR("Disassociate Supported Channel Bad");
464 if (reason == REASON_IE_INVALID)
465 return LOG_STR("IE Invalid");
466 if (reason == REASON_MIC_FAILURE)
467 return LOG_STR("Mic Failure");
468 if (reason == REASON_4WAY_HANDSHAKE_TIMEOUT)
469 return LOG_STR("4-Way Handshake Timeout");
470 if (reason == REASON_GROUP_KEY_UPDATE_TIMEOUT)
471 return LOG_STR("Group Key Update Timeout");
472 if (reason == REASON_IE_IN_4WAY_DIFFERS)
473 return LOG_STR("IE In 4-Way Handshake Differs");
474 if (reason == REASON_GROUP_CIPHER_INVALID)
475 return LOG_STR("Group Cipher Invalid");
476 if (reason == REASON_PAIRWISE_CIPHER_INVALID)
477 return LOG_STR("Pairwise Cipher Invalid");
478 if (reason == REASON_AKMP_INVALID)
479 return LOG_STR("AKMP Invalid");
480 if (reason == REASON_UNSUPP_RSN_IE_VERSION)
481 return LOG_STR("Unsupported RSN IE version");
482 if (reason == REASON_INVALID_RSN_IE_CAP)
483 return LOG_STR("Invalid RSN IE Cap");
484 if (reason == REASON_802_1X_AUTH_FAILED)
485 return LOG_STR("802.1x Authentication Failed");
486 if (reason == REASON_CIPHER_SUITE_REJECTED)
487 return LOG_STR("Cipher Suite Rejected");
488 if (reason == REASON_BEACON_TIMEOUT)
489 return LOG_STR("Beacon Timeout");
490 if (reason == REASON_NO_AP_FOUND)
491 return LOG_STR("AP Not Found");
492 if (reason == REASON_AUTH_FAIL)
493 return LOG_STR("Authentication Failed");
494 if (reason == REASON_ASSOC_FAIL)
495 return LOG_STR("Association Failed");
496 if (reason == REASON_HANDSHAKE_TIMEOUT)
497 return LOG_STR("Handshake Failed");
498 return LOG_STR("Unspecified");
499}
500
501void WiFiComponent::wifi_event_callback(System_Event_t *event) {
502 switch (event->event) {
503 case EVENT_STAMODE_CONNECTED: {
504 auto it = event->event_info.connected;
505#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
506 char bssid_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
507 format_mac_addr_upper(it.bssid, bssid_buf);
508 ESP_LOGV(TAG, "Connected ssid='%.*s' bssid=%s channel=%u", it.ssid_len, (const char *) it.ssid, bssid_buf,
509 it.channel);
510#endif
512#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
513 // Defer listener notification until state machine reaches STA_CONNECTED
514 // This ensures wifi.connected condition returns true in listener automations
516#endif
517 break;
518 }
519 case EVENT_STAMODE_DISCONNECTED: {
520 auto it = event->event_info.disconnected;
521 if (it.reason == REASON_NO_AP_FOUND) {
522 ESP_LOGW(TAG, "Disconnected ssid='%.*s' reason='Probe Request Unsuccessful'", it.ssid_len,
523 (const char *) it.ssid);
525 } else {
526 char bssid_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
527 format_mac_addr_upper(it.bssid, bssid_s);
528 ESP_LOGW(TAG, "Disconnected ssid='%.*s' bssid=" LOG_SECRET("%s") " reason='%s'", it.ssid_len,
529 (const char *) it.ssid, bssid_s, LOG_STR_ARG(get_disconnect_reason_str(it.reason)));
531 }
533#if LWIP_VERSION_MAJOR != 1
534 sta_netif_down();
535#endif
536#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
538#endif
539 break;
540 }
541 case EVENT_STAMODE_AUTHMODE_CHANGE: {
542 auto it = event->event_info.auth_change;
543 ESP_LOGV(TAG, "Changed Authmode old=%s new=%s", LOG_STR_ARG(get_auth_mode_str(it.old_mode)),
544 LOG_STR_ARG(get_auth_mode_str(it.new_mode)));
545 // Mitigate CVE-2020-12638
546 // https://lbsfilm.at/blog/wpa2-authenticationmode-downgrade-in-espressif-microprocessors
547 if (it.old_mode != AUTH_OPEN && it.new_mode == AUTH_OPEN) {
548 ESP_LOGW(TAG, "Potential Authmode downgrade detected, disconnecting");
549#if LWIP_VERSION_MAJOR != 1
550 sta_netif_down();
551#endif
552 wifi_station_disconnect();
554 }
555 break;
556 }
557 case EVENT_STAMODE_GOT_IP: {
558 auto it = event->event_info.got_ip;
559 char ip_buf[network::IP_ADDRESS_BUFFER_SIZE], gw_buf[network::IP_ADDRESS_BUFFER_SIZE],
560 mask_buf[network::IP_ADDRESS_BUFFER_SIZE];
561 ESP_LOGV(TAG, "static_ip=%s gateway=%s netmask=%s", network::IPAddress(&it.ip).str_to(ip_buf),
562 network::IPAddress(&it.gw).str_to(gw_buf), network::IPAddress(&it.mask).str_to(mask_buf));
564#ifdef USE_WIFI_IP_STATE_LISTENERS
565 // Defer listener callbacks to main loop - system context has limited stack
567#endif
568 break;
569 }
570 case EVENT_STAMODE_DHCP_TIMEOUT: {
571 ESP_LOGW(TAG, "DHCP request timeout");
572 break;
573 }
574 case EVENT_SOFTAPMODE_STACONNECTED: {
575#ifdef USE_WIFI_AP
577#endif
578#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
579 auto it = event->event_info.sta_connected;
580 char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
581 format_mac_addr_upper(it.mac, mac_buf);
582 ESP_LOGV(TAG, "AP client connected MAC=%s aid=%u", mac_buf, it.aid);
583#endif
584 break;
585 }
586 case EVENT_SOFTAPMODE_STADISCONNECTED: {
587#ifdef USE_WIFI_AP
590#endif
591#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
592 auto it = event->event_info.sta_disconnected;
593 char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
594 format_mac_addr_upper(it.mac, mac_buf);
595 ESP_LOGV(TAG, "AP client disconnected MAC=%s aid=%u", mac_buf, it.aid);
596#endif
597 break;
598 }
599 case EVENT_SOFTAPMODE_PROBEREQRECVED: {
600#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
601 auto it = event->event_info.ap_probereqrecved;
602 char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
603 format_mac_addr_upper(it.mac, mac_buf);
604 ESP_LOGVV(TAG, "AP receive Probe Request MAC=%s RSSI=%d", mac_buf, it.rssi);
605#endif
606 break;
607 }
608 case EVENT_OPMODE_CHANGED: {
609 auto it = event->event_info.opmode_changed;
610 ESP_LOGV(TAG, "Changed Mode old=%s new=%s", LOG_STR_ARG(get_op_mode_str(it.old_opmode)),
611 LOG_STR_ARG(get_op_mode_str(it.new_opmode)));
612 break;
613 }
614 case EVENT_SOFTAPMODE_DISTRIBUTE_STA_IP: {
615#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
616 auto it = event->event_info.distribute_sta_ip;
617 char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
618 char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
619 format_mac_addr_upper(it.mac, mac_buf);
620 ESP_LOGV(TAG, "AP Distribute Station IP MAC=%s IP=%s aid=%u", mac_buf, network::IPAddress(&it.ip).str_to(ip_buf),
621 it.aid);
622#endif
623 break;
624 }
625 default:
626 break;
627 }
628
629 WiFiMockClass::_event_callback(event);
630}
631
633 uint8_t val = static_cast<uint8_t>(output_power * 4);
634 system_phy_set_max_tpw(val);
635 return true;
636}
638 if (!this->wifi_mode_(true, {}))
639 return false;
640
641 bool ret1, ret2;
642 ETS_UART_INTR_DISABLE();
643 ret1 = wifi_station_set_auto_connect(0);
644 ret2 = wifi_station_set_reconnect_policy(false);
645 ETS_UART_INTR_ENABLE();
646
647 if (!ret1 || !ret2) {
648 ESP_LOGV(TAG, "Disabling Auto-Connect failed");
649 }
650
651#ifdef USE_WIFI_PHY_MODE
652 if (!this->wifi_apply_phy_mode_()) {
653 ESP_LOGV(TAG, "Setting PHY Mode failed");
654 }
655#endif
656
657 delay(10);
658 return true;
659}
660
661#ifdef USE_WIFI_PHY_MODE
664 return true;
665 // Values of WiFi8266PhyMode are aligned with the SDK's phy_mode_t enum.
666 return wifi_set_phy_mode(static_cast<phy_mode_t>(this->phy_mode_));
667}
668#endif
669
671 wifi_set_event_handler_cb(&WiFiComponent::wifi_event_callback);
672
673 // Make sure WiFi is in clean state before anything starts
674 this->wifi_mode_(false, false);
675}
676
678 // Use cached state from wifi_event_callback() instead of calling
679 // wifi_station_get_connect_status() which queries the SDK every time.
680 // Use if statements with early returns instead of switch to avoid GCC
681 // generating a CSWTCH lookup table in .rodata (flash) on ESP8266.
682 auto state = static_cast<ESP8266WiFiSTAState>(this->sta_state_);
692}
693
695 // enable STA
696 if (!this->wifi_mode_(true, {}))
697 return false;
698
699 // Reset scan_done_ before starting new scan to prevent stale flag from previous scan
700 // (e.g., roaming scan completed just before unexpected disconnect)
701 this->scan_done_ = false;
702
703 struct scan_config config{};
704 memset(&config, 0, sizeof(config));
705 config.ssid = nullptr;
706 config.bssid = nullptr;
707 config.channel = 0;
708 config.show_hidden = 1;
709 config.scan_type = passive ? WIFI_SCAN_TYPE_PASSIVE : WIFI_SCAN_TYPE_ACTIVE;
710 // Use shorter dwell times for roaming scans - we only need to detect strong
711 // nearby APs, not do a thorough survey. This also reduces off-channel time
712 // which can cause Beacon Timeout disconnects on some APs.
713 // Roaming times match the ESP32 IDF scan defaults.
714 static constexpr uint32_t SCAN_PASSIVE_DEFAULT_MS = 500;
715 static constexpr uint32_t SCAN_PASSIVE_ROAMING_MS = 300;
716 static constexpr uint32_t SCAN_ACTIVE_MIN_DEFAULT_MS = 400;
717 static constexpr uint32_t SCAN_ACTIVE_MAX_DEFAULT_MS = 500;
718 static constexpr uint32_t SCAN_ACTIVE_MIN_ROAMING_MS = 100;
719 static constexpr uint32_t SCAN_ACTIVE_MAX_ROAMING_MS = 300;
720 bool roaming = this->is_roaming_scan_active();
721 if (passive) {
722 config.scan_time.passive = roaming ? SCAN_PASSIVE_ROAMING_MS : SCAN_PASSIVE_DEFAULT_MS;
723 } else {
724 config.scan_time.active.min = roaming ? SCAN_ACTIVE_MIN_ROAMING_MS : SCAN_ACTIVE_MIN_DEFAULT_MS;
725 config.scan_time.active.max = roaming ? SCAN_ACTIVE_MAX_ROAMING_MS : SCAN_ACTIVE_MAX_DEFAULT_MS;
726 }
727 bool ret = wifi_station_scan(&config, &WiFiComponent::s_wifi_scan_done_callback);
728 if (!ret) {
729 ESP_LOGV(TAG, "wifi_station_scan failed");
730 return false;
731 }
732
733 return ret;
734}
736 bool ret = true;
737 // Only call disconnect if interface is up
738 if (wifi_get_opmode() & WIFI_STA) {
739#if LWIP_VERSION_MAJOR != 1
740 sta_netif_down();
741#endif
742 ret = wifi_station_disconnect();
743 }
744 station_config conf{};
745 memset(&conf, 0, sizeof(conf));
746 ETS_UART_INTR_DISABLE();
747 wifi_station_set_config_current(&conf);
748 ETS_UART_INTR_ENABLE();
749 return ret;
750}
754
755void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) {
756 // Compiles to nothing here; kept so every scan_result_ mutation holds the lock
757 ScanResultsLock lock(this);
758 this->scan_result_.clear();
759
760 if (status != OK) {
761 ESP_LOGV(TAG, "Scan failed: %d", status);
762 // Don't call retry_connect() here - this callback runs in SDK system context
763 // where yield() cannot be called. Instead, just set scan_done_ and let
764 // check_scanning_finished() handle the empty scan_result_ from loop context.
765 this->scan_done_ = true;
766 return;
767 }
768
769 auto *head = reinterpret_cast<bss_info *>(arg);
770 bool needs_full = this->needs_full_scan_results_();
771
772 // First pass: count matching networks (linked list is non-destructive)
773 size_t total = 0;
774 size_t count = 0;
775 for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) {
776 total++;
777 const char *ssid_cstr = reinterpret_cast<const char *>(it->ssid);
778 if (needs_full || this->matches_configured_network_(ssid_cstr, it->bssid)) {
779 count++;
780 }
781 }
782
783 if (!this->scan_result_.try_init(count)) {
784 ESP_LOGW(TAG, "No memory for %zu scan results", count);
785 this->scan_done_ = true;
786 return;
787 }
788
789 // Second pass: store matching networks
790 for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) {
791 const char *ssid_cstr = reinterpret_cast<const char *>(it->ssid);
792 if (needs_full || this->matches_configured_network_(ssid_cstr, it->bssid)) {
793 this->scan_result_.emplace_back(
794 bssid_t{it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]}, ssid_cstr,
795 it->ssid_len, it->channel, it->rssi, it->authmode != AUTH_OPEN, it->is_hidden != 0);
796 } else {
797 this->log_discarded_scan_result_(ssid_cstr, it->bssid, it->rssi, it->channel);
798 }
799 }
800 ESP_LOGV(TAG, "Scan complete: %zu found, %zu stored%s", total, this->scan_result_.size(),
801 needs_full ? LOG_STR_LITERAL("") : LOG_STR_LITERAL(" (filtered)"));
802 this->scan_done_ = true;
803#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS
804 this->pending_.scan_complete = true; // Defer listener callbacks to main loop
805#endif
806}
807
808#ifdef USE_WIFI_AP
809bool WiFiComponent::wifi_ap_ip_config_(const optional<ManualIP> &manual_ip) {
810 // enable AP
811 if (!this->wifi_mode_({}, true))
812 return false;
813
814 struct ip_info info{};
815 if (manual_ip.has_value()) {
816 info.ip = manual_ip->static_ip;
817 info.gw = manual_ip->gateway;
818 info.netmask = manual_ip->subnet;
819 } else {
820 info.ip = network::IPAddress(192, 168, 4, 1);
821 info.gw = network::IPAddress(192, 168, 4, 1);
822 info.netmask = network::IPAddress(255, 255, 255, 0);
823 }
824
825 if (wifi_softap_dhcps_status() == DHCP_STARTED) {
826 if (!wifi_softap_dhcps_stop()) {
827 ESP_LOGW(TAG, "Stopping DHCP server failed");
828 }
829 }
830
831 if (!wifi_set_ip_info(SOFTAP_IF, &info)) {
832 ESP_LOGE(TAG, "Set SoftAP info failed");
833 return false;
834 }
835
836#if USE_ARDUINO_VERSION_CODE < VERSION_CODE(3, 1, 0)
837 dhcpSoftAP.begin(&info);
838#endif
839
840 struct dhcps_lease lease{};
841 lease.enable = true;
842 network::IPAddress start_address = network::IPAddress(&info.ip);
843 start_address += 99;
844 lease.start_ip = start_address;
845#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
846 char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
847#endif
848 ESP_LOGV(TAG, "DHCP server IP lease start: %s", start_address.str_to(ip_buf));
849 start_address += 10;
850 lease.end_ip = start_address;
851 ESP_LOGV(TAG, "DHCP server IP lease end: %s", start_address.str_to(ip_buf));
852 if (!wifi_softap_set_dhcps_lease(&lease)) {
853 ESP_LOGE(TAG, "Set SoftAP DHCP lease failed");
854 return false;
855 }
856
857 // lease time 1440 minutes (=24 hours)
858 if (!wifi_softap_set_dhcps_lease_time(1440)) {
859 ESP_LOGE(TAG, "Set SoftAP DHCP lease time failed");
860 return false;
861 }
862
863#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 1, 0)
864 ESP8266WiFiClass::softAPDhcpServer().setRouter(true); // send ROUTER option with netif's gateway IP
865#else
866 uint8_t mode = 1;
867 // bit0, 1 enables router information from ESP8266 SoftAP DHCP server.
868 if (!wifi_softap_set_dhcps_offer_option(OFFER_ROUTER, &mode)) {
869 ESP_LOGE(TAG, "wifi_softap_set_dhcps_offer_option failed");
870 return false;
871 }
872#endif
873
874 if (!wifi_softap_dhcps_start()) {
875 ESP_LOGE(TAG, "Starting SoftAP DHCPS failed");
876 return false;
877 }
878
879 return true;
880}
881
883 // enable AP
884 if (!this->wifi_mode_({}, true))
885 return false;
886
887 struct softap_config conf{};
888 if (ap.ssid_.size() > sizeof(conf.ssid)) {
889 ESP_LOGE(TAG, "AP SSID too long");
890 return false;
891 }
892 memcpy(reinterpret_cast<char *>(conf.ssid), ap.ssid_.c_str(), ap.ssid_.size());
893 conf.ssid_len = static_cast<uint8>(ap.ssid_.size());
894 conf.channel = ap.has_channel() ? ap.get_channel() : 1;
895 conf.ssid_hidden = ap.get_hidden();
896 conf.max_connection = 5;
897 conf.beacon_interval = 100;
898
899 if (ap.password_.empty()) {
900 conf.authmode = AUTH_OPEN;
901 *conf.password = 0;
902 } else {
903 conf.authmode = AUTH_WPA2_PSK;
904 if (ap.password_.size() > sizeof(conf.password)) {
905 ESP_LOGE(TAG, "AP password too long");
906 return false;
907 }
908 memcpy(reinterpret_cast<char *>(conf.password), ap.password_.c_str(), ap.password_.size());
909 }
910
911 ETS_UART_INTR_DISABLE();
912 bool ret = wifi_softap_set_config_current(&conf);
913 ETS_UART_INTR_ENABLE();
914
915 if (!ret) {
916 ESP_LOGV(TAG, "wifi_softap_set_config_current failed");
917 return false;
918 }
919
920#ifdef USE_WIFI_MANUAL_IP
921 if (!this->wifi_ap_ip_config_(ap.get_manual_ip())) {
922 ESP_LOGV(TAG, "wifi_ap_ip_config_ failed");
923 return false;
924 }
925#else
926 if (!this->wifi_ap_ip_config_({})) {
927 ESP_LOGV(TAG, "wifi_ap_ip_config_ failed");
928 return false;
929 }
930#endif
931
932 return true;
933}
934
936 struct ip_info ip{};
937 wifi_get_ip_info(SOFTAP_IF, &ip);
938 return network::IPAddress(&ip.ip);
939}
940#endif // USE_WIFI_AP
941
943 bssid_t bssid{};
944 struct station_config conf{};
945 if (wifi_station_get_config(&conf)) {
946 std::copy_n(conf.bssid, bssid.size(), bssid.begin());
947 }
948 return bssid;
949}
950const char *WiFiComponent::wifi_ssid_to(std::span<char, SSID_BUFFER_SIZE> buffer) {
951 struct station_config conf{};
952 if (!wifi_station_get_config(&conf)) {
953 buffer[0] = '\0';
954 return buffer.data();
955 }
956 // conf.ssid is uint8[32], not null-terminated if full
957 size_t len = strnlen(reinterpret_cast<const char *>(conf.ssid), sizeof(conf.ssid));
958 memcpy(buffer.data(), conf.ssid, len);
959 buffer[len] = '\0';
960 return buffer.data();
961}
963 if (wifi_station_get_connect_status() != STATION_GOT_IP)
964 return WIFI_RSSI_DISCONNECTED;
965 sint8 rssi = wifi_station_get_rssi();
966 // Values >= 31 are error codes per NONOS SDK API, not valid RSSI readings
967 return rssi >= 31 ? WIFI_RSSI_DISCONNECTED : rssi;
968}
969int32_t WiFiComponent::get_wifi_channel() { return wifi_get_channel(); }
971 struct ip_info ip{};
972 wifi_get_ip_info(STATION_IF, &ip);
973 return network::IPAddress(&ip.netmask);
974}
976 struct ip_info ip{};
977 wifi_get_ip_info(STATION_IF, &ip);
978 return network::IPAddress(&ip.gw);
979}
983 return true;
984}
985
987 // Process callbacks deferred from ESP8266 SDK system context (~2KB stack)
988 // to main loop context (full stack). Connect state listeners are handled
989 // by notify_connect_state_listeners_() in the shared state machine code.
990
991#ifdef USE_WIFI_CONNECT_STATE_LISTENERS
992 if (this->pending_.disconnect) {
993 this->pending_.disconnect = false;
994 // Refresh is_connected() cache here, not in the SDK callback (sys context).
997 }
998#endif
999
1000#ifdef USE_WIFI_IP_STATE_LISTENERS
1001 if (this->pending_.got_ip) {
1002 this->pending_.got_ip = false;
1004 }
1005#endif
1006
1007#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS
1008 if (this->pending_.scan_complete) {
1009 this->pending_.scan_complete = false;
1011 }
1012#endif
1013}
1014
1015} // namespace esphome::wifi
1016#endif
1017#endif
BedjetMode mode
BedJet operating mode.
uint8_t status
Definition bl0942.h:8
const StringRef & get_name() const
Get the name of this Application set by pre_setup().
const char * c_str() const
Guards WiFiComponent::scan_result_.
uint8_t get_channel() const
const optional< EAPAuth > & get_eap() const
const optional< ManualIP > & get_manual_ip() const
const bssid_t & get_bssid() const
void wifi_scan_done_callback_(void *arg, STATUS status)
void notify_scan_results_listeners_()
Notify scan results listeners with current scan results.
WiFiSTAConnectStatus wifi_sta_connect_status_() const
wifi_scan_vector_t< WiFiScanResult > scan_result_
void notify_ip_state_listeners_()
Notify IP state listeners with current addresses.
bool wifi_sta_ip_config_(const optional< ManualIP > &manual_ip)
static void wifi_event_callback(System_Event_t *event)
void notify_disconnect_state_listeners_()
Notify connect state listeners of disconnection.
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.
static void s_wifi_scan_done_callback(void *arg, STATUS status)
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_
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)
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()
NameCopy * next
bool state
Definition fan.h:2
unsigned count
Definition gif.cpp:51
uint32_t buffer
Definition gif.cpp:50
struct in_addr ip4_addr_t
Definition ip_address.h:23
int ret
mopeka_std_values val[3]
std::array< IPAddress, 5 > IPAddresses
Definition ip_address.h:369
std::array< uint8_t, 6 > bssid_t
const LogString * get_auth_mode_str(uint8_t mode)
void netif_set_down(struct netif *netif)
ESPHOME_LOG_TAG(TAG, "wifi")
const LogString * get_disconnect_reason_str(uint8_t reason)
struct netif * eagle_lwip_getif(int netif_index)
void netif_set_addr(struct netif *netif, const ip4_addr_t *ip, const ip4_addr_t *netmask, const ip4_addr_t *gw)
WiFiComponent * global_wifi_component
PROGMEM_STRING_TABLE(AuthModeStrings, "OPEN", "WEP", "WPA PSK", "WPA2 PSK", "WPA/WPA2 PSK", "UNKNOWN")
const LogString * get_op_mode_str(uint8_t mode)
const void size_t len
Definition hal.h:68
void HOT delay(uint32_t ms)
Definition hal.cpp:85
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
static void uint32_t
char * str_to(char *buf) const
Definition ip_address.h:153
SemaphoreHandle_t lock