From c53d50b6cf2424f160bf5d7a6fdad9cb3b76bdf7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 13 Feb 2026 14:36:51 -0600 Subject: [PATCH 1/4] [wifi] Use DHCP_STATE_BOUND check instead of netif_is_link_up MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit netif_is_link_up() is insufficient — if wifi_station_connect() completes quickly (e.g. fast_connect), the setup() call at line 710 could reach dhcp_renew() with link up but DHCP still in SELECTING or REQUESTING state, causing the same state corruption. Check dhcp->state == DHCP_STATE_BOUND directly to ensure dhcp_renew() is only called when there is an actual lease to renew. Co-Authored-By: Claude Opus 4.6 --- .../components/wifi/wifi_component_esp8266.cpp | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index b913ca5f70..caf362756d 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -16,7 +16,8 @@ extern "C" { #include "lwip/err.h" #include "lwip/dns.h" #include "lwip/dhcp.h" -#include "lwip/init.h" // LWIP_VERSION_ +#include "lwip/prot/dhcp.h" // DHCP_STATE_BOUND +#include "lwip/init.h" // LWIP_VERSION_ #include "lwip/apps/sntp.h" #include "lwip/netif.h" // struct netif #include @@ -224,13 +225,14 @@ bool WiFiComponent::wifi_apply_hostname_() { #else intf->hostname = wifi_station_get_hostname(); #endif - if (netif_dhcp_data(intf) != nullptr && netif_is_link_up(intf)) { - // Renew already started DHCP leases to inform server of hostname change. - // Only attempt when the interface has link — calling dhcp_renew() without - // an active connection corrupts lwIP's DHCP state machine (it unconditionally - // sets state to RENEWING before attempting to send, and never rolls back on - // failure). This causes dhcp_network_changed() to call dhcp_reboot() instead - // of dhcp_discover() when WiFi later connects, sending a bogus DHCP REQUEST + struct dhcp *dhcp_data = netif_dhcp_data(intf); + if (dhcp_data != nullptr && dhcp_data->state == DHCP_STATE_BOUND) { + // Renew already-bound DHCP leases to inform server of hostname change. + // Only attempt when DHCP is BOUND — calling dhcp_renew() in any other + // state corrupts lwIP's DHCP state machine (it unconditionally sets state + // to RENEWING before attempting to send, and never rolls back on failure). + // This causes dhcp_network_changed() to call dhcp_reboot() instead of + // dhcp_discover() when WiFi later connects, sending a bogus DHCP REQUEST // for IP 0.0.0.0 that can put some routers into a persistent bad state. err_t lwipret = dhcp_renew(intf); if (lwipret != ERR_OK) { From 8abb472c3acd5a1b36188709656bdd9d4592cda1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 13 Feb 2026 20:04:51 -0700 Subject: [PATCH 2/4] [wifi] Also require netif_is_link_up for dhcp_renew guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DHCP_STATE_BOUND alone is insufficient — during reconnection, DHCP can remain BOUND from a previous connection while the link is down (wifi_disconnect_() doesn't stop DHCP). Both conditions are needed: DHCP must be BOUND and the interface must have link. Co-Authored-By: Claude Opus 4.6 --- esphome/components/wifi/wifi_component_esp8266.cpp | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index caf362756d..4e94f86ef5 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -226,12 +226,16 @@ bool WiFiComponent::wifi_apply_hostname_() { intf->hostname = wifi_station_get_hostname(); #endif struct dhcp *dhcp_data = netif_dhcp_data(intf); - if (dhcp_data != nullptr && dhcp_data->state == DHCP_STATE_BOUND) { + if (dhcp_data != nullptr && dhcp_data->state == DHCP_STATE_BOUND && netif_is_link_up(intf)) { // Renew already-bound DHCP leases to inform server of hostname change. - // Only attempt when DHCP is BOUND — calling dhcp_renew() in any other - // state corrupts lwIP's DHCP state machine (it unconditionally sets state - // to RENEWING before attempting to send, and never rolls back on failure). - // This causes dhcp_network_changed() to call dhcp_reboot() instead of + // Both conditions are required: + // - DHCP must be BOUND (have an active lease to renew) + // - Interface must have link (able to send packets) + // During reconnection, DHCP can remain BOUND from a previous connection + // while the link is down. Calling dhcp_renew() without link corrupts + // lwIP's DHCP state machine (it unconditionally sets state to RENEWING + // before attempting to send, and never rolls back on failure). This + // causes dhcp_network_changed() to call dhcp_reboot() instead of // dhcp_discover() when WiFi later connects, sending a bogus DHCP REQUEST // for IP 0.0.0.0 that can put some routers into a persistent bad state. err_t lwipret = dhcp_renew(intf); From f7705c85892f32366eb1906969119a2464c9a943 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 14 Feb 2026 05:22:39 -0700 Subject: [PATCH 3/4] =?UTF-8?q?[wifi]=20Remove=20dhcp=5Frenew()=20entirely?= =?UTF-8?q?=20=E2=80=94=20it=20can=20never=20run=20usefully?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dhcp_renew() loop was cargo-culted from Arduino ESP8266's WiFi.hostname() which was designed for changing the hostname on an already-connected system with multiple interfaces (WiFi+Ethernet). In ESPHome, wifi_apply_hostname_() is only called from: - setup_() — before WiFi connects (link never up) - wifi_sta_connect_() — after wifi_disconnect_() (link always down) The hostname is fixed at compile time and never changes at runtime. Setting intf->hostname is sufficient — lwIP automatically includes it in DHCP DISCOVER/REQUEST packets via LWIP_NETIF_HOSTNAME. Co-Authored-By: Claude Opus 4.6 --- .../wifi/wifi_component_esp8266.cpp | 30 ++++--------------- 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 4e94f86ef5..6f8decc0ae 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -16,8 +16,7 @@ extern "C" { #include "lwip/err.h" #include "lwip/dns.h" #include "lwip/dhcp.h" -#include "lwip/prot/dhcp.h" // DHCP_STATE_BOUND -#include "lwip/init.h" // LWIP_VERSION_ +#include "lwip/init.h" // LWIP_VERSION_ #include "lwip/apps/sntp.h" #include "lwip/netif.h" // struct netif #include @@ -217,34 +216,17 @@ bool WiFiComponent::wifi_apply_hostname_() { ESP_LOGV(TAG, "Set hostname failed"); } - // inform dhcp server of hostname change using dhcp_renew() + // Update hostname on all lwIP interfaces so DHCP packets include it. + // No dhcp_renew() call here — the hostname is fixed at compile time and + // this function is only called during setup() or wifi_sta_connect_() when + // the interface is always disconnected. lwIP includes the hostname in + // DHCP DISCOVER/REQUEST automatically via LWIP_NETIF_HOSTNAME. for (netif *intf = netif_list; intf; intf = intf->next) { - // unconditionally update all known interfaces #if LWIP_VERSION_MAJOR == 1 intf->hostname = (char *) wifi_station_get_hostname(); #else intf->hostname = wifi_station_get_hostname(); #endif - struct dhcp *dhcp_data = netif_dhcp_data(intf); - if (dhcp_data != nullptr && dhcp_data->state == DHCP_STATE_BOUND && netif_is_link_up(intf)) { - // Renew already-bound DHCP leases to inform server of hostname change. - // Both conditions are required: - // - DHCP must be BOUND (have an active lease to renew) - // - Interface must have link (able to send packets) - // During reconnection, DHCP can remain BOUND from a previous connection - // while the link is down. Calling dhcp_renew() without link corrupts - // lwIP's DHCP state machine (it unconditionally sets state to RENEWING - // before attempting to send, and never rolls back on failure). This - // causes dhcp_network_changed() to call dhcp_reboot() instead of - // dhcp_discover() when WiFi later connects, sending a bogus DHCP REQUEST - // for IP 0.0.0.0 that can put some routers into a persistent bad state. - err_t lwipret = dhcp_renew(intf); - if (lwipret != ERR_OK) { - ESP_LOGW(TAG, "wifi_apply_hostname_(%s): lwIP error %d on interface %c%c (index %d)", intf->hostname, - (int) lwipret, intf->name[0], intf->name[1], intf->num); - ret = false; - } - } } return ret; From 46cc9c0eefd4e6ff49066e37bdacf87442c180c4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 14 Feb 2026 05:24:52 -0700 Subject: [PATCH 4/4] =?UTF-8?q?[wifi]=20Simplify=20comment=20=E2=80=94=20r?= =?UTF-8?q?emove=20unverified=20link=20state=20claim?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cannot confirm wifi_station_disconnect() synchronously clears the lwIP netif LINK_UP flag on ESP8266 NONOS SDK. The comment doesn't need to make claims about link state since the fix is simply that the hostname never changes at runtime, making dhcp_renew() pointless. Co-Authored-By: Claude Opus 4.6 --- esphome/components/wifi/wifi_component_esp8266.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index 6f8decc0ae..cbf7d7d80f 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -217,10 +217,9 @@ bool WiFiComponent::wifi_apply_hostname_() { } // Update hostname on all lwIP interfaces so DHCP packets include it. - // No dhcp_renew() call here — the hostname is fixed at compile time and - // this function is only called during setup() or wifi_sta_connect_() when - // the interface is always disconnected. lwIP includes the hostname in - // DHCP DISCOVER/REQUEST automatically via LWIP_NETIF_HOSTNAME. + // lwIP includes the hostname in DHCP DISCOVER/REQUEST automatically + // via LWIP_NETIF_HOSTNAME — no dhcp_renew() needed. The hostname is + // fixed at compile time and never changes at runtime. for (netif *intf = netif_list; intf; intf = intf->next) { #if LWIP_VERSION_MAJOR == 1 intf->hostname = (char *) wifi_station_get_hostname();