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