[mdns] Add runtime service enable/disable API (ESP32 only) (#19325)

This commit is contained in:
Kevin Ahrendt
2026-09-15 20:06:40 -05:00
committed by GitHub
parent 57860dc06c
commit ec1d8fa953
6 changed files with 168 additions and 14 deletions
+24
View File
@@ -5,6 +5,8 @@ import esphome.config_validation as cv
from esphome.const import (
CONF_DISABLED,
CONF_ID,
CONF_MDNS,
CONF_OPENTHREAD,
CONF_PORT,
CONF_PROTOCOL,
CONF_SERVICE,
@@ -184,6 +186,28 @@ def enable_mdns_storage() -> None:
cg.add_define("USE_MDNS_STORE_SERVICES")
def request_service_enable_disable() -> bool:
"""Request MDNSComponent::set_service_enabled() support.
ESP32 only, not with OpenThread. Returns True when the
USE_MDNS_SUPPORTS_ENABLE_DISABLE define was added; guard C++ usage with it.
Public API for external components. Do not remove.
"""
mdns_config = CORE.config.get(CONF_MDNS)
if (
mdns_config is None
or mdns_config[CONF_DISABLED]
or not CORE.is_esp32
or CONF_OPENTHREAD in CORE.config
):
return False
cg.add_define("USE_MDNS_SUPPORTS_ENABLE_DISABLE")
# Services must stay stored so a disabled service can be re-registered
enable_mdns_storage()
return True
@coroutine_with_priority(CoroPriority.NETWORK_SERVICES)
async def to_code(config: ConfigType) -> None:
if config[CONF_DISABLED] is True:
+16
View File
@@ -63,6 +63,9 @@ struct MDNSService {
const MDNSString *proto;
TemplatableFn<uint16_t> port;
FixedVector<MDNSTXTRecord> txt_records;
#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE
bool enabled{true};
#endif
};
class MDNSComponent final : public Component
@@ -112,6 +115,19 @@ class MDNSComponent final : public Component
const StaticVector<MDNSService, MDNS_SERVICE_COUNT> &get_services() const { return this->services_; }
#endif
#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE
#ifndef USE_MDNS_STORE_SERVICES
#error "USE_MDNS_SUPPORTS_ENABLE_DISABLE requires USE_MDNS_STORE_SERVICES"
#endif
#ifdef USE_OPENTHREAD
#error "USE_MDNS_SUPPORTS_ENABLE_DISABLE is not supported with OpenThread"
#endif
/// Enable or disable a compiled-in service, matched by type and proto (e.g. "_sendspin", "_tcp").
/// Only valid once this component is ready. Re-enabling re-reads the port but keeps the boot-time TXT values.
/// Returns true if the service is in the requested state afterwards. Blocks briefly on the mDNS task.
bool set_service_enabled(const char *service_type, const char *proto, bool enabled);
#endif
void on_shutdown() override;
#ifdef USE_MDNS_DYNAMIC_TXT
+55 -14
View File
@@ -2,6 +2,7 @@
#if defined(USE_ESP32) && defined(USE_MDNS)
#include <mdns.h>
#include <cstring>
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
@@ -11,6 +12,23 @@ namespace esphome::mdns {
static const char *const TAG = "mdns";
#ifndef USE_OPENTHREAD
static esp_err_t add_service(const MDNSService &service) {
// Stack buffer for up to 16 txt records, heap fallback for more
SmallBufferWithHeapFallback<16, mdns_txt_item_t> txt_records(service.txt_records.size());
for (size_t i = 0; i < service.txt_records.size(); i++) {
const auto &record = service.txt_records[i];
// key and value are either compile-time string literals in flash or pointers to dynamic_txt_values_
// Both remain valid for the lifetime of this function, and ESP-IDF makes internal copies
txt_records.get()[i].key = MDNS_STR_ARG(record.key);
txt_records.get()[i].value = MDNS_STR_ARG(record.value);
}
uint16_t port = service.port.value();
return mdns_service_add(nullptr, MDNS_STR_ARG(service.service_type), MDNS_STR_ARG(service.proto), port,
txt_records.get(), service.txt_records.size());
}
#endif
static void register_esp32(MDNSComponent *comp, StaticVector<MDNSService, MDNS_SERVICE_COUNT> &services) {
#ifdef USE_OPENTHREAD
// OpenThread handles service registration via SRP client
@@ -27,27 +45,50 @@ static void register_esp32(MDNSComponent *comp, StaticVector<MDNSService, MDNS_S
mdns_hostname_set(hostname);
mdns_instance_name_set(hostname);
for (const auto &service : services) {
// Stack buffer for up to 16 txt records, heap fallback for more
SmallBufferWithHeapFallback<16, mdns_txt_item_t> txt_records(service.txt_records.size());
for (size_t i = 0; i < service.txt_records.size(); i++) {
const auto &record = service.txt_records[i];
// key and value are either compile-time string literals in flash or pointers to dynamic_txt_values_
// Both remain valid for the lifetime of this function, and ESP-IDF makes internal copies
txt_records.get()[i].key = MDNS_STR_ARG(record.key);
txt_records.get()[i].value = MDNS_STR_ARG(record.value);
}
uint16_t port = service.port.value();
err = mdns_service_add(nullptr, MDNS_STR_ARG(service.service_type), MDNS_STR_ARG(service.proto), port,
txt_records.get(), service.txt_records.size());
for (auto &service : services) {
#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE
if (!service.enabled)
continue;
#endif
err = add_service(service);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Failed to register service %s: %s", MDNS_STR_ARG(service.service_type), esp_err_to_name(err));
#ifdef USE_MDNS_SUPPORTS_ENABLE_DISABLE
// Let a later enable call retry
service.enabled = false;
#endif
}
}
#endif
}
#if defined(USE_MDNS_SUPPORTS_ENABLE_DISABLE) && !defined(USE_OPENTHREAD)
bool MDNSComponent::set_service_enabled(const char *service_type, const char *proto, bool enabled) {
// services_ is compiled in setup()
if (!this->is_ready()) {
ESP_LOGW(TAG, "Cannot %s service %s before setup", enabled ? "enable" : "disable", service_type);
return false;
}
for (auto &service : this->services_) {
if (strcmp(MDNS_STR_ARG(service.service_type), service_type) != 0 ||
strcmp(MDNS_STR_ARG(service.proto), proto) != 0) {
continue;
}
if (service.enabled == enabled)
return true;
esp_err_t err = enabled ? add_service(service) : mdns_service_remove(service_type, proto);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Failed to %s service %s: %s", enabled ? "enable" : "disable", service_type, esp_err_to_name(err));
return false;
}
service.enabled = enabled;
return true;
}
ESP_LOGW(TAG, "Service %s not found", service_type);
return false;
}
#endif // USE_MDNS_SUPPORTS_ENABLE_DISABLE && !USE_OPENTHREAD
void MDNSComponent::setup() { this->setup_buffers_and_register_(register_esp32); }
void MDNSComponent::on_shutdown() {
+3
View File
@@ -479,6 +479,9 @@
#define USE_OPENTHREAD
#define USE_ZIGBEE
#endif
#ifndef USE_OPENTHREAD
#define USE_MDNS_SUPPORTS_ENABLE_DISABLE
#endif
#endif
#if defined(USE_ESP32_VARIANT_ESP32S2)
@@ -0,0 +1,70 @@
"""request_service_enable_disable() only opts in on platforms whose mDNS stack
can add and remove services after setup, and tells the caller so."""
import pytest
from esphome.components import mdns
from esphome.const import CONF_DISABLED, PlatformFramework
from esphome.core import CORE
from tests.component_tests.types import SetCoreConfigCallable
DEFINE = "USE_MDNS_SUPPORTS_ENABLE_DISABLE"
def _defines() -> set[str]:
return {define.name for define in CORE.defines}
def _set_config(
set_core_config: SetCoreConfigCallable,
platform_framework: PlatformFramework,
config: dict,
) -> None:
set_core_config(platform_framework)
CORE.config = config
@pytest.mark.parametrize(
"platform_framework",
[PlatformFramework.ESP32_IDF, PlatformFramework.ESP32_ARDUINO],
)
def test_esp32_adds_define_and_keeps_services_stored(
set_core_config: SetCoreConfigCallable, platform_framework: PlatformFramework
) -> None:
_set_config(set_core_config, platform_framework, {"mdns": {CONF_DISABLED: False}})
assert mdns.request_service_enable_disable() is True
# Disabled services must stay stored so they can be re-registered later.
assert {DEFINE, "USE_MDNS_STORE_SERVICES"} <= _defines()
@pytest.mark.parametrize(
"platform_framework",
[PlatformFramework.ESP8266_ARDUINO, PlatformFramework.RP2_ARDUINO],
)
def test_other_platforms_return_false(
set_core_config: SetCoreConfigCallable, platform_framework: PlatformFramework
) -> None:
_set_config(set_core_config, platform_framework, {"mdns": {CONF_DISABLED: False}})
assert mdns.request_service_enable_disable() is False
assert DEFINE not in _defines()
@pytest.mark.parametrize(
"config",
[
pytest.param({}, id="no_mdns"),
pytest.param({"mdns": {CONF_DISABLED: True}}, id="mdns_disabled"),
pytest.param(
{"mdns": {CONF_DISABLED: False}, "openthread": {}}, id="openthread"
),
],
)
def test_esp32_returns_false_when_services_cannot_be_toggled(
set_core_config: SetCoreConfigCallable, config: dict
) -> None:
_set_config(set_core_config, PlatformFramework.ESP32_IDF, config)
assert mdns.request_service_enable_disable() is False
assert DEFINE not in _defines()