Extends the WiFi-only listener pattern from the previous commit to also subscribe
to EthernetIPStateListener when Ethernet is configured. RP2040 can run mDNS over a
W5500 ethernet shield without WiFi, and mDNS and WiFi are mutually exclusive on
RP2040 (the framework doesn't support both simultaneously on the CYW43/PIO paths),
so this adds the ethernet-only path without touching the WiFi path.
ESPHome's wifi and ethernet components already publish compatible IP state listener
APIs (`WiFiIPStateListener::on_ip_state` and `EthernetIPStateListener::on_ip_state`
with identical signatures). MDNSComponent multiply-inherits both when available; a
single on_ip_state() override satisfies both vtable entries.
- New `USE_MDNS_WIFI_LISTENER` / `USE_MDNS_ETHERNET_LISTENER` gates control per-
interface subscription. `USE_MDNS_EVENT_DRIVEN_POLLING` fires if either is
available.
- Python side now calls `ethernet.request_ethernet_ip_state_listener()` when
ethernet is in the config (RP2040 only — ESP8266 has no ethernet driver).
- setup() seeds current state for each registered listener so an already-up
interface still triggers MDNS.begin() + polling window under AFTER_CONNECTION
priority.
Tests: adds `test-enabled-ethernet.rp2040-ard.yaml` covering the ethernet-only
path. Existing `test-enabled.rp2040-ard.yaml` (WiFi-only) and ESP8266 tests
continue to pass.
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's WiFiIPStateListener only notifies on IP acquisition (GOT_IP events), not
on IP loss — on disconnect, only the WiFiConnectStateListener's disconnect path
fires (see wifi_component_esp8266.cpp:952-962 and wifi_component_pico_w.cpp:340).
The previous commit's `ip_was_up_` transition tracking was broken: after the first
IP-up event, `ip_was_up_` latched to true and never reset, so subsequent
disconnect+reconnect cycles would see has_ip=true && ip_was_up_=true and skip
re-arming the polling window.
Fix: always re-arm on any IP notification. The scheduler's set_interval/set_timeout
with a uint32_t ID already performs atomic cancel-and-add for matching IDs
(Scheduler::set_timer_common_ line 232-234), so start_polling_window_ is idempotent
and needs no explicit cancel. Drop the ip_was_up_ field and cancel_polling_window_
helper entirely.
The !has_ip branch (cancel on disconnect) was dead code: it would never fire because
the listener doesn't receive disconnect events. Removing it; the polling window will
naturally expire on its own (at most 12s of harmless MDNS.update() calls during a
disconnect that isn't followed by reconnect within the window).
The Arduino LEAmDNS library only has meaningful timer-driven work during the
~9 s probe+announce phase following MDNS.begin() or _restart(): 3 probes at
250 ms + 8 announcements at 1000 ms, then all internal timeouts are set to
resetToNeverExpires(). Incoming packets are handled via the lwIP UDP RX
callback independently of update(). ESPHome does not issue service queries,
so the query cache path is always a no-op.
The previous implementation ran set_interval(50) forever — ~20 dispatches/sec,
1200+ scheduler calls per minute of pure overhead once probing completed.
This PR arms a bounded MDNS_POLL_WINDOW_MS (12 s) polling window driven by
WiFiIPStateListener events. A fresh window covers each probe/announce cycle
(boot, wifi reconnect, or internal _restart() triggered by netif changes);
outside the window there are zero scheduler dispatches and the scheduler
heap contains no mDNS items.
ESP8266 is WiFi-only in the Arduino build so the path is unconditional.
RP2040 supports W5500 ethernet without WiFi, so the listener is requested
only when WiFi is in the config; ethernet-only RP2040 builds keep the
legacy polling loop.
Scheduler IDs use uint32_t (MDNS_POLL_ID / MDNS_POLL_STOP_ID) to avoid the
name-hash/strcmp cost of string-named timers on the cancel + re-arm paths.