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