diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index dc86a314da..db915e5f89 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -169,16 +169,24 @@ async def to_code(config): elif CORE.is_rp2040: cg.add_library("LEAmDNS", None) - # 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. 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 + # ESP8266 and RP2040 use a network 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. + # + # We subscribe to any listener interface that's configured (WiFi and/or + # Ethernet); the same on_ip_state() override satisfies both because the + # listener signatures match. If neither is available the component falls + # back to the legacy polling loop at MDNS_UPDATE_INTERVAL_MS. + if CORE.is_esp8266 or CORE.is_rp2040: + if "wifi" in CORE.config: + from esphome.components import wifi - wifi.request_wifi_ip_state_listener() + wifi.request_wifi_ip_state_listener() + if CORE.is_rp2040 and "ethernet" in CORE.config: + from esphome.components import ethernet + + ethernet.request_ethernet_ip_state_listener() if CORE.is_esp32: add_idf_component(name="espressif/mdns", ref="1.11.0") diff --git a/esphome/components/mdns/mdns_component.h b/esphome/components/mdns/mdns_component.h index 32a365bb62..ea74e22818 100644 --- a/esphome/components/mdns/mdns_component.h +++ b/esphome/components/mdns/mdns_component.h @@ -6,14 +6,24 @@ #include "esphome/core/component.h" #include "esphome/core/helpers.h" // 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) +// scheduler-backed MDNS.update() (ESP8266, RP2040). It's enabled when at least one +// compatible network IP state listener slot is available — the mdns Python to_code() +// requests a WiFi slot when WiFi is in the config and an Ethernet slot when Ethernet +// is in the config. If neither is available (e.g. clang-tidy running without full +// codegen), 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)) || \ + (defined(USE_ETHERNET) && defined(USE_ETHERNET_IP_STATE_LISTENERS))) #include "esphome/components/network/ip_address.h" -#include "esphome/components/wifi/wifi_component.h" #define USE_MDNS_EVENT_DRIVEN_POLLING +#if defined(USE_WIFI) && defined(USE_WIFI_IP_STATE_LISTENERS) +#include "esphome/components/wifi/wifi_component.h" +#define USE_MDNS_WIFI_LISTENER +#endif +#if defined(USE_ETHERNET) && defined(USE_ETHERNET_IP_STATE_LISTENERS) +#include "esphome/components/ethernet/ethernet_component.h" +#define USE_MDNS_ETHERNET_LISTENER +#endif #endif namespace esphome::mdns { @@ -58,10 +68,14 @@ struct MDNSService { }; class MDNSComponent final : public Component -#ifdef USE_MDNS_EVENT_DRIVEN_POLLING +#ifdef USE_MDNS_WIFI_LISTENER , public wifi::WiFiIPStateListener #endif +#ifdef USE_MDNS_ETHERNET_LISTENER + , + public ethernet::EthernetIPStateListener +#endif { public: void setup() override; diff --git a/esphome/components/mdns/mdns_esp8266.cpp b/esphome/components/mdns/mdns_esp8266.cpp index 072be497db..c7737bcb14 100644 --- a/esphome/components/mdns/mdns_esp8266.cpp +++ b/esphome/components/mdns/mdns_esp8266.cpp @@ -7,10 +7,10 @@ #include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" -#ifdef USE_MDNS_EVENT_DRIVEN_POLLING +#include "mdns_component.h" +#ifdef USE_MDNS_WIFI_LISTENER #include "esphome/components/wifi/wifi_component.h" #endif -#include "mdns_component.h" namespace esphome::mdns { @@ -50,6 +50,8 @@ void MDNSComponent::setup() { // 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. + // ESP8266 has no ethernet driver in the Arduino build so only the WiFi listener + // branch is reachable here. wifi::global_wifi_component->add_ip_state_listener(this); this->start_polling_window_(); #else diff --git a/esphome/components/mdns/mdns_rp2040.cpp b/esphome/components/mdns/mdns_rp2040.cpp index cc2d81fe24..8a6a36096f 100644 --- a/esphome/components/mdns/mdns_rp2040.cpp +++ b/esphome/components/mdns/mdns_rp2040.cpp @@ -6,9 +6,12 @@ #include "esphome/core/application.h" #include "esphome/core/log.h" #include "mdns_component.h" -#ifdef USE_MDNS_EVENT_DRIVEN_POLLING +#ifdef USE_MDNS_WIFI_LISTENER #include "esphome/components/wifi/wifi_component.h" #endif +#ifdef USE_MDNS_ETHERNET_LISTENER +#include "esphome/components/ethernet/ethernet_component.h" +#endif // Arduino-Pico's PolledTimeout.h (pulled in by ESP8266mDNS.h) redefines IRAM_ATTR to empty. // Save and restore our definition around the include to avoid a redefinition warning. @@ -57,19 +60,36 @@ void MDNSComponent::setup() { // // Workaround: defer MDNS.begin() and service registration until the network has an IP, // then call notifyAPChange() on subsequent reconnects to restart mDNS probing and - // announcing — all from main loop context via the WiFiIPStateListener callback so it's - // thread-safe. + // announcing — all from main loop context via the IP state listener callback(s) so + // it's thread-safe. We subscribe to any listener interface that's active (WiFi and/or + // Ethernet); the same on_ip_state() override serves both because the signatures match. #ifdef USE_MDNS_EVENT_DRIVEN_POLLING +#ifdef USE_MDNS_WIFI_LISTENER wifi::global_wifi_component->add_ip_state_listener(this); // AFTER_CONNECTION priority means the network may already be up when setup() runs; // the listener only fires on subsequent state changes, so seed the current state. - const auto ips = wifi::global_wifi_component->wifi_sta_ip_addresses(); - if (ips[0].is_set()) { - this->on_ip_state(ips, wifi::global_wifi_component->get_dns_address(0), - wifi::global_wifi_component->get_dns_address(1)); + { + const auto ips = wifi::global_wifi_component->wifi_sta_ip_addresses(); + if (ips[0].is_set()) { + this->on_ip_state(ips, wifi::global_wifi_component->get_dns_address(0), + wifi::global_wifi_component->get_dns_address(1)); + } } +#endif +#ifdef USE_MDNS_ETHERNET_LISTENER + ethernet::global_eth_component->add_ip_state_listener(this); + // Seed current Ethernet state for the same reason — if the interface is already up + // when mdns setup() runs, we need to kick off begin() + polling here. + if (ethernet::global_eth_component->is_connected()) { + const auto ips = ethernet::global_eth_component->get_ip_addresses(); + if (ips[0].is_set()) { + this->on_ip_state(ips, network::IPAddress{}, network::IPAddress{}); + } + } +#endif #else - // Fallback (non-WiFi build): poll forever, checking connection state each tick. + // Fallback (no IP state listener available): poll forever, checking connection state + // each tick. this->set_interval(MDNS_UPDATE_INTERVAL_MS, [this]() { bool connected = network::is_connected(); if (connected && !this->was_connected_) { @@ -91,11 +111,11 @@ void MDNSComponent::setup() { #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 (see - // wifi_component_pico_w.cpp), not on IP loss, so every notification represents a - // fresh IP that needs a probe/announce cycle. The library's internal - // LwipIntf::stateUpCB is stubbed out on arduino-pico (see setup()), so we drive - // begin/restart ourselves from this callback. + // Both WiFi and Ethernet IP state listeners only notify on IP acquisition (see + // wifi_component_pico_w.cpp and ethernet_component.cpp), not on IP loss, so every + // notification represents a fresh IP that needs a probe/announce cycle. The library's + // internal LwipIntf::stateUpCB is stubbed out on arduino-pico (see setup()), so we + // drive begin/restart ourselves from this callback. if (!ips[0].is_set()) { return; } diff --git a/tests/components/mdns/common-enabled-ethernet.yaml b/tests/components/mdns/common-enabled-ethernet.yaml new file mode 100644 index 0000000000..bfa9321d43 --- /dev/null +++ b/tests/components/mdns/common-enabled-ethernet.yaml @@ -0,0 +1,23 @@ +ethernet: + type: W5500 + clk_pin: 18 + mosi_pin: 19 + miso_pin: 16 + cs_pin: 17 + interrupt_pin: 21 + reset_pin: 20 + manual_ip: + static_ip: 192.168.178.56 + gateway: 192.168.178.1 + subnet: 255.255.255.0 + domain: .local + mac_address: "02:AA:BB:CC:DD:01" + +mdns: + disabled: false + services: + - service: _test_service + protocol: _tcp + port: 8888 + txt: + static_string: Anything diff --git a/tests/components/mdns/test-enabled-ethernet.rp2040-ard.yaml b/tests/components/mdns/test-enabled-ethernet.rp2040-ard.yaml new file mode 100644 index 0000000000..f84a0bc276 --- /dev/null +++ b/tests/components/mdns/test-enabled-ethernet.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common-enabled-ethernet.yaml