mirror of
https://github.com/esphome/esphome.git
synced 2026-09-17 18:18:43 +00:00
[mdns] Add runtime service enable/disable API (ESP32 only) (#19325)
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user