From 5cb258034bc8cc9b683954e1cbb4fe67435272d5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 23 Apr 2026 19:36:11 -0500 Subject: [PATCH] [mdns] Fall back to legacy polling when WiFi IP state listener isn't available MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit clang-tidy CI compiles the source with the esp8266-arduino-tidy env's raw build flags (-DUSE_ESP8266 only) without running the Python codegen that adds USE_WIFI_IP_STATE_LISTENERS. The previous guard assumed USE_WIFI_IP_STATE_LISTENERS would always be defined on ESP8266, so clang-tidy failed with 'no member named add_ip_state_listener in wifi::WiFiComponent'. Gate USE_MDNS_EVENT_DRIVEN_POLLING on USE_WIFI + USE_WIFI_IP_STATE_LISTENERS for both ESP8266 and RP2040. When either is absent, fall back to the pre-PR behaviour: set_interval(MDNS_UPDATE_INTERVAL_MS, MDNS.update) running forever. Python side already only requests the listener slot when WiFi is in the config, so real production builds on ESP8266 (which always have WiFi) continue to use the event-driven path — only the clang-tidy static-analysis build takes the fallback. --- esphome/components/mdns/__init__.py | 11 ++++------- esphome/components/mdns/mdns_component.h | 22 +++++++++++----------- esphome/components/mdns/mdns_esp8266.cpp | 12 ++++++++++++ 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index 3e07c91d53..dc86a314da 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -172,13 +172,10 @@ async def to_code(config): # ESP8266 and RP2040 use a WiFi IP state listener to arm a bounded MDNS.update() # polling window only while the library is in its probe+announce phase. This # eliminates the steady-state 50ms interval that ran forever (1200+ dispatches - # per minute) and its scheduler overhead. - # - # ESP8266 has no ethernet driver in the Arduino build, so it's always a WiFi - # device — the listener is unconditional. RP2040 supports the W5500 ethernet - # shield without WiFi, so the listener is requested only when WiFi is present; - # ethernet-only RP2040 builds fall back to the legacy polling loop. - if CORE.is_esp8266 or (CORE.is_rp2040 and "wifi" in CORE.config): + # per minute) and its scheduler overhead. When WiFi is absent (e.g. an + # ethernet-only RP2040 build) the component falls back to the legacy polling + # loop at MDNS_UPDATE_INTERVAL_MS. + if (CORE.is_esp8266 or CORE.is_rp2040) and "wifi" in CORE.config: from esphome.components import wifi wifi.request_wifi_ip_state_listener() diff --git a/esphome/components/mdns/mdns_component.h b/esphome/components/mdns/mdns_component.h index 7be2f4665e..32a365bb62 100644 --- a/esphome/components/mdns/mdns_component.h +++ b/esphome/components/mdns/mdns_component.h @@ -5,13 +5,12 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/helpers.h" -// Event-driven polling is used whenever the scheduler-backed MDNS.update() interval -// needs to be gated on network state (ESP8266, or RP2040 with WiFi). ESP8266 mDNS -// always runs over WiFi — there is no ethernet driver for ESP8266 in the Arduino -// build — so this path is unconditional on ESP8266. RP2040 can run mDNS over the -// W5500 ethernet shield without WiFi, so it falls back to the legacy polling loop -// when WiFi is absent. -#if defined(USE_ESP8266) || (defined(USE_RP2040) && defined(USE_WIFI) && defined(USE_WIFI_IP_STATE_LISTENERS)) +// Event-driven polling replaces the legacy set_interval() loop on platforms that need +// scheduler-backed MDNS.update() (ESP8266, RP2040). It's enabled when a WiFi IP state +// listener slot is available — the mdns Python to_code() requests one when WiFi is in +// the config. If it's not (e.g. clang-tidy running without full codegen, or an +// ethernet-only RP2040 build), the component falls back to the legacy polling loop. +#if (defined(USE_ESP8266) || defined(USE_RP2040)) && defined(USE_WIFI) && defined(USE_WIFI_IP_STATE_LISTENERS) #include "esphome/components/network/ip_address.h" #include "esphome/components/wifi/wifi_component.h" #define USE_MDNS_EVENT_DRIVEN_POLLING @@ -68,7 +67,6 @@ class MDNSComponent final : public Component void setup() override; void dump_config() override; -#ifdef USE_MDNS_EVENT_DRIVEN_POLLING // On ESP8266 and RP2040, MDNS.update() calls _process(true) which only manages // timer-driven state machines (probe/announce timeouts and service query cache TTLs). // Incoming mDNS packets are handled independently via the lwIP onRx UDP callback and @@ -80,10 +78,12 @@ class MDNSComponent final : public Component // resetToNeverExpires(). ESPHome does not issue mDNS service queries, so the service // query cache is always empty. Every subsequent update() call is pure overhead. // - // Instead of polling forever, we arm a bounded polling window driven by - // WiFiIPStateListener events. A fresh window covers each probe/announce cycle that - // follows initial connect or reconnect; outside the window no update() calls occur. + // When USE_MDNS_EVENT_DRIVEN_POLLING is defined we arm a bounded polling window from + // WiFiIPStateListener events so update() only runs during the probe+announce phase; + // outside that window no update() calls occur. Otherwise (fallback), we poll at + // MDNS_UPDATE_INTERVAL_MS forever. static constexpr uint32_t MDNS_UPDATE_INTERVAL_MS = 50; +#ifdef USE_MDNS_EVENT_DRIVEN_POLLING // Boot probe+announce phase is ~9.0s (3*250ms probes + 8*1000ms announces). Window // includes margin for the initial `rand() % MDNS_PROBE_DELAY` jitter and for the // debounced internal restart triggered by netif status changes on ESP8266. diff --git a/esphome/components/mdns/mdns_esp8266.cpp b/esphome/components/mdns/mdns_esp8266.cpp index d6254de568..072be497db 100644 --- a/esphome/components/mdns/mdns_esp8266.cpp +++ b/esphome/components/mdns/mdns_esp8266.cpp @@ -7,7 +7,9 @@ #include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" +#ifdef USE_MDNS_EVENT_DRIVEN_POLLING #include "esphome/components/wifi/wifi_component.h" +#endif #include "mdns_component.h" namespace esphome::mdns { @@ -37,18 +39,27 @@ static void register_esp8266(MDNSComponent *, StaticVectorsetup_buffers_and_register_(register_esp8266); +#ifdef USE_MDNS_EVENT_DRIVEN_POLLING // Arduino LEAmDNS registers its own LwipIntf::statusChangeCB that calls _restart() // on every netif status change (link up, IP up, etc.), so we don't trigger begin() // or restart here — we just cover the probe+announce window with a bounded polling // schedule. The listener catches subsequent reconnects and re-arms the window. wifi::global_wifi_component->add_ip_state_listener(this); this->start_polling_window_(); +#else + // Fallback for builds without a WiFi IP state listener (e.g. clang-tidy without + // codegen defines). Matches the pre-PR behaviour: poll forever at 50ms. + this->set_interval(MDNS_UPDATE_INTERVAL_MS, []() { MDNS.update(); }); +#endif } +#ifdef USE_MDNS_EVENT_DRIVEN_POLLING void MDNSComponent::on_ip_state(const network::IPAddresses &ips, const network::IPAddress &, const network::IPAddress &) { // ESPHome's WiFiIPStateListener only notifies on IP acquisition (GOT_IP events on @@ -60,6 +71,7 @@ void MDNSComponent::on_ip_state(const network::IPAddresses &ips, const network:: this->start_polling_window_(); } } +#endif void MDNSComponent::on_shutdown() { MDNS.close();