1
0
mirror of https://github.com/esphome/esphome.git synced 2025-10-13 15:23:49 +01:00

Merge branch 'mdns_value_flash' into integration

This commit is contained in:
J. Nick Koston
2025-10-07 21:51:56 -10:00
12 changed files with 96 additions and 71 deletions

View File

@@ -5,7 +5,7 @@ namespace dashboard_import {
static std::string g_package_import_url; // NOLINT static std::string g_package_import_url; // NOLINT
std::string get_package_import_url() { return g_package_import_url; } const std::string &get_package_import_url() { return g_package_import_url; }
void set_package_import_url(std::string url) { g_package_import_url = std::move(url); } void set_package_import_url(std::string url) { g_package_import_url = std::move(url); }
} // namespace dashboard_import } // namespace dashboard_import

View File

@@ -5,7 +5,7 @@
namespace esphome { namespace esphome {
namespace dashboard_import { namespace dashboard_import {
std::string get_package_import_url(); const std::string &get_package_import_url();
void set_package_import_url(std::string url); void set_package_import_url(std::string url);
} // namespace dashboard_import } // namespace dashboard_import

View File

@@ -58,14 +58,6 @@ CONFIG_SCHEMA = cv.All(
) )
def mdns_txt_record(key: str, value: str):
return cg.StructInitializer(
MDNSTXTRecord,
("key", cg.RawExpression(f"MDNS_STR({cg.safe_exp(key)})")),
("value", value),
)
def mdns_service( def mdns_service(
service: str, proto: str, port: int, txt_records: list[dict[str, str]] service: str, proto: str, port: int, txt_records: list[dict[str, str]]
): ):
@@ -107,23 +99,51 @@ async def to_code(config):
# Ensure at least 1 service (fallback service) # Ensure at least 1 service (fallback service)
cg.add_define("MDNS_SERVICE_COUNT", max(1, service_count)) cg.add_define("MDNS_SERVICE_COUNT", max(1, service_count))
# Calculate compile-time dynamic TXT value count
# Dynamic values are those that cannot be stored in flash at compile time
dynamic_txt_count = 0
if "api" in CORE.config:
# Always: get_mac_address()
dynamic_txt_count += 1
# User-provided templatable TXT values (only lambdas, not static strings)
dynamic_txt_count += sum(
1
for service in config[CONF_SERVICES]
for txt_value in service[CONF_TXT].values()
if cg.is_template(txt_value)
)
# Ensure at least 1 to avoid zero-size array
cg.add_define("MDNS_DYNAMIC_TXT_COUNT", max(1, dynamic_txt_count))
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
for service in config[CONF_SERVICES]: for service in config[CONF_SERVICES]:
txt = [ # Build the txt records list for the service
cg.StructInitializer( txt_records = []
MDNSTXTRecord, for txt_key, txt_value in service[CONF_TXT].items():
("key", cg.RawExpression(f"MDNS_STR({cg.safe_exp(txt_key)})")), if cg.is_template(txt_value):
("value", await cg.templatable(txt_value, [], cg.std_string)), # It's a lambda - evaluate and store using helper
templated_value = await cg.templatable(txt_value, [], cg.std_string)
txt_records.append(
cg.RawExpression(
f"{{MDNS_STR({cg.safe_exp(txt_key)}), MDNS_STR({var}->add_dynamic_txt_value(({templated_value})()))}}"
) )
for txt_key, txt_value in service[CONF_TXT].items() )
] else:
# It's a static string - use directly in flash, no need to store in vector
txt_records.append(
cg.RawExpression(
f"{{MDNS_STR({cg.safe_exp(txt_key)}), MDNS_STR({cg.safe_exp(txt_value)})}}"
)
)
exp = mdns_service( exp = mdns_service(
service[CONF_SERVICE], service[CONF_SERVICE],
service[CONF_PROTOCOL], service[CONF_PROTOCOL],
await cg.templatable(service[CONF_PORT], [], cg.uint16), await cg.templatable(service[CONF_PORT], [], cg.uint16),
txt, txt_records,
) )
cg.add(var.add_extra_service(exp)) cg.add(var.add_extra_service(exp))

View File

@@ -9,21 +9,9 @@
#include <pgmspace.h> #include <pgmspace.h>
// Macro to define strings in PROGMEM on ESP8266, regular memory on other platforms // Macro to define strings in PROGMEM on ESP8266, regular memory on other platforms
#define MDNS_STATIC_CONST_CHAR(name, value) static const char name[] PROGMEM = value #define MDNS_STATIC_CONST_CHAR(name, value) static const char name[] PROGMEM = value
// Helper to convert PROGMEM string to std::string for TemplatableValue
// Only define this function if we have services that will use it
#if defined(USE_API) || defined(USE_PROMETHEUS) || defined(USE_WEBSERVER) || defined(USE_MDNS_EXTRA_SERVICES)
static std::string mdns_str_value(PGM_P str) {
char buf[64];
strncpy_P(buf, str, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
return std::string(buf);
}
#define MDNS_STR_VALUE(name) mdns_str_value(name)
#endif
#else #else
// On non-ESP8266 platforms, use regular const char* // On non-ESP8266 platforms, use regular const char*
#define MDNS_STATIC_CONST_CHAR(name, value) static constexpr const char name[] = value #define MDNS_STATIC_CONST_CHAR(name, value) static constexpr const char name[] = value
#define MDNS_STR_VALUE(name) std::string(name)
#endif #endif
#ifdef USE_API #ifdef USE_API
@@ -68,6 +56,14 @@ MDNS_STATIC_CONST_CHAR(NETWORK_WIFI, "wifi");
MDNS_STATIC_CONST_CHAR(NETWORK_ETHERNET, "ethernet"); MDNS_STATIC_CONST_CHAR(NETWORK_ETHERNET, "ethernet");
MDNS_STATIC_CONST_CHAR(NETWORK_THREAD, "thread"); MDNS_STATIC_CONST_CHAR(NETWORK_THREAD, "thread");
// Wrap build-time defines into flash storage
MDNS_STATIC_CONST_CHAR(VALUE_VERSION, ESPHOME_VERSION);
MDNS_STATIC_CONST_CHAR(VALUE_BOARD, ESPHOME_BOARD);
#ifdef ESPHOME_PROJECT_NAME
MDNS_STATIC_CONST_CHAR(VALUE_PROJECT_NAME, ESPHOME_PROJECT_NAME);
MDNS_STATIC_CONST_CHAR(VALUE_PROJECT_VERSION, ESPHOME_PROJECT_VERSION);
#endif
void MDNSComponent::compile_records_() { void MDNSComponent::compile_records_() {
this->hostname_ = App.get_name(); this->hostname_ = App.get_name();
@@ -109,47 +105,46 @@ void MDNSComponent::compile_records_() {
txt_records.reserve(txt_count); txt_records.reserve(txt_count);
if (!friendly_name_empty) { if (!friendly_name_empty) {
txt_records.push_back({MDNS_STR(TXT_FRIENDLY_NAME), friendly_name}); txt_records.push_back({MDNS_STR(TXT_FRIENDLY_NAME), MDNS_STR(friendly_name.c_str())});
} }
txt_records.push_back({MDNS_STR(TXT_VERSION), ESPHOME_VERSION}); txt_records.push_back({MDNS_STR(TXT_VERSION), MDNS_STR(VALUE_VERSION)});
txt_records.push_back({MDNS_STR(TXT_MAC), get_mac_address()}); txt_records.push_back({MDNS_STR(TXT_MAC), MDNS_STR(this->add_dynamic_txt_value(get_mac_address()))});
#ifdef USE_ESP8266 #ifdef USE_ESP8266
txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR_VALUE(PLATFORM_ESP8266)}); txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(PLATFORM_ESP8266)});
#elif defined(USE_ESP32) #elif defined(USE_ESP32)
txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR_VALUE(PLATFORM_ESP32)}); txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(PLATFORM_ESP32)});
#elif defined(USE_RP2040) #elif defined(USE_RP2040)
txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR_VALUE(PLATFORM_RP2040)}); txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(PLATFORM_RP2040)});
#elif defined(USE_LIBRETINY) #elif defined(USE_LIBRETINY)
txt_records.push_back({MDNS_STR(TXT_PLATFORM), lt_cpu_get_model_name()}); txt_records.push_back({MDNS_STR(TXT_PLATFORM), MDNS_STR(lt_cpu_get_model_name())});
#endif #endif
txt_records.push_back({MDNS_STR(TXT_BOARD), ESPHOME_BOARD}); txt_records.push_back({MDNS_STR(TXT_BOARD), MDNS_STR(VALUE_BOARD)});
#if defined(USE_WIFI) #if defined(USE_WIFI)
txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR_VALUE(NETWORK_WIFI)}); txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_WIFI)});
#elif defined(USE_ETHERNET) #elif defined(USE_ETHERNET)
txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR_VALUE(NETWORK_ETHERNET)}); txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_ETHERNET)});
#elif defined(USE_OPENTHREAD) #elif defined(USE_OPENTHREAD)
txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR_VALUE(NETWORK_THREAD)}); txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_THREAD)});
#endif #endif
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
MDNS_STATIC_CONST_CHAR(NOISE_ENCRYPTION, "Noise_NNpsk0_25519_ChaChaPoly_SHA256"); MDNS_STATIC_CONST_CHAR(NOISE_ENCRYPTION, "Noise_NNpsk0_25519_ChaChaPoly_SHA256");
if (api::global_api_server->get_noise_ctx()->has_psk()) { txt_records.push_back({MDNS_STR(api::global_api_server->get_noise_ctx()->has_psk() ? TXT_API_ENCRYPTION
txt_records.push_back({MDNS_STR(TXT_API_ENCRYPTION), MDNS_STR_VALUE(NOISE_ENCRYPTION)}); : TXT_API_ENCRYPTION_SUPPORTED),
} else { MDNS_STR(NOISE_ENCRYPTION)});
txt_records.push_back({MDNS_STR(TXT_API_ENCRYPTION_SUPPORTED), MDNS_STR_VALUE(NOISE_ENCRYPTION)});
}
#endif #endif
#ifdef ESPHOME_PROJECT_NAME #ifdef ESPHOME_PROJECT_NAME
txt_records.push_back({MDNS_STR(TXT_PROJECT_NAME), ESPHOME_PROJECT_NAME}); txt_records.push_back({MDNS_STR(TXT_PROJECT_NAME), MDNS_STR(VALUE_PROJECT_NAME)});
txt_records.push_back({MDNS_STR(TXT_PROJECT_VERSION), ESPHOME_PROJECT_VERSION}); txt_records.push_back({MDNS_STR(TXT_PROJECT_VERSION), MDNS_STR(VALUE_PROJECT_VERSION)});
#endif // ESPHOME_PROJECT_NAME #endif // ESPHOME_PROJECT_NAME
#ifdef USE_DASHBOARD_IMPORT #ifdef USE_DASHBOARD_IMPORT
txt_records.push_back({MDNS_STR(TXT_PACKAGE_IMPORT_URL), dashboard_import::get_package_import_url()}); txt_records.push_back(
{MDNS_STR(TXT_PACKAGE_IMPORT_URL), MDNS_STR(dashboard_import::get_package_import_url().c_str())});
#endif #endif
} }
#endif // USE_API #endif // USE_API
@@ -175,7 +170,7 @@ void MDNSComponent::compile_records_() {
fallback_service.service_type = MDNS_STR(SERVICE_HTTP); fallback_service.service_type = MDNS_STR(SERVICE_HTTP);
fallback_service.proto = MDNS_STR(SERVICE_TCP); fallback_service.proto = MDNS_STR(SERVICE_TCP);
fallback_service.port = USE_WEBSERVER_PORT; fallback_service.port = USE_WEBSERVER_PORT;
fallback_service.txt_records.push_back({MDNS_STR(TXT_VERSION), ESPHOME_VERSION}); fallback_service.txt_records.push_back({MDNS_STR(TXT_VERSION), MDNS_STR(VALUE_VERSION)});
#endif #endif
} }
@@ -190,8 +185,7 @@ void MDNSComponent::dump_config() {
ESP_LOGV(TAG, " - %s, %s, %d", MDNS_STR_ARG(service.service_type), MDNS_STR_ARG(service.proto), ESP_LOGV(TAG, " - %s, %s, %d", MDNS_STR_ARG(service.service_type), MDNS_STR_ARG(service.proto),
const_cast<TemplatableValue<uint16_t> &>(service.port).value()); const_cast<TemplatableValue<uint16_t> &>(service.port).value());
for (const auto &record : service.txt_records) { for (const auto &record : service.txt_records) {
ESP_LOGV(TAG, " TXT: %s = %s", MDNS_STR_ARG(record.key), ESP_LOGV(TAG, " TXT: %s = %s", MDNS_STR_ARG(record.key), MDNS_STR_ARG(record.value));
const_cast<TemplatableValue<std::string> &>(record.value).value().c_str());
} }
} }
#endif #endif

View File

@@ -27,7 +27,7 @@ struct MDNSString;
struct MDNSTXTRecord { struct MDNSTXTRecord {
const MDNSString *key; const MDNSString *key;
TemplatableValue<std::string> value; const MDNSString *value;
}; };
struct MDNSService { struct MDNSService {
@@ -59,6 +59,17 @@ class MDNSComponent : public Component {
void on_shutdown() override; void on_shutdown() override;
/// Add a dynamic TXT value and return pointer to it for use in MDNSTXTRecord
const char *add_dynamic_txt_value(const std::string &value) {
this->dynamic_txt_values_.push_back(value);
return this->dynamic_txt_values_.back().c_str();
}
/// Storage for runtime-generated TXT values (MAC address, user lambdas)
/// Pre-sized at compile time via MDNS_DYNAMIC_TXT_COUNT to avoid heap allocations.
/// Static/compile-time values (version, board, etc.) are stored directly in flash and don't use this.
StaticVector<std::string, MDNS_DYNAMIC_TXT_COUNT> dynamic_txt_values_;
protected: protected:
StaticVector<MDNSService, MDNS_SERVICE_COUNT> services_{}; StaticVector<MDNSService, MDNS_SERVICE_COUNT> services_{};
std::string hostname_; std::string hostname_;

View File

@@ -2,7 +2,6 @@
#if defined(USE_ESP32) && defined(USE_MDNS) #if defined(USE_ESP32) && defined(USE_MDNS)
#include <mdns.h> #include <mdns.h>
#include <cstring>
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "mdns_component.h" #include "mdns_component.h"
@@ -29,21 +28,16 @@ void MDNSComponent::setup() {
std::vector<mdns_txt_item_t> txt_records; std::vector<mdns_txt_item_t> txt_records;
for (const auto &record : service.txt_records) { for (const auto &record : service.txt_records) {
mdns_txt_item_t it{}; mdns_txt_item_t it{};
// key is a compile-time string literal in flash, no need to strdup // 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
it.key = MDNS_STR_ARG(record.key); it.key = MDNS_STR_ARG(record.key);
// value is a temporary from TemplatableValue, must strdup to keep it alive it.value = MDNS_STR_ARG(record.value);
it.value = strdup(const_cast<TemplatableValue<std::string> &>(record.value).value().c_str());
txt_records.push_back(it); txt_records.push_back(it);
} }
uint16_t port = const_cast<TemplatableValue<uint16_t> &>(service.port).value(); uint16_t port = const_cast<TemplatableValue<uint16_t> &>(service.port).value();
err = mdns_service_add(nullptr, MDNS_STR_ARG(service.service_type), MDNS_STR_ARG(service.proto), port, err = mdns_service_add(nullptr, MDNS_STR_ARG(service.service_type), MDNS_STR_ARG(service.proto), port,
txt_records.data(), txt_records.size()); txt_records.data(), txt_records.size());
// free records
for (const auto &it : txt_records) {
free((void *) it.value); // NOLINT(cppcoreguidelines-no-malloc)
}
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGW(TAG, "Failed to register service %s: %s", MDNS_STR_ARG(service.service_type), esp_err_to_name(err)); ESP_LOGW(TAG, "Failed to register service %s: %s", MDNS_STR_ARG(service.service_type), esp_err_to_name(err));
} }

View File

@@ -33,7 +33,7 @@ void MDNSComponent::setup() {
MDNS.addService(FPSTR(service_type), FPSTR(proto), port); MDNS.addService(FPSTR(service_type), FPSTR(proto), port);
for (const auto &record : service.txt_records) { for (const auto &record : service.txt_records) {
MDNS.addServiceTxt(FPSTR(service_type), FPSTR(proto), FPSTR(MDNS_STR_ARG(record.key)), MDNS.addServiceTxt(FPSTR(service_type), FPSTR(proto), FPSTR(MDNS_STR_ARG(record.key)),
const_cast<TemplatableValue<std::string> &>(record.value).value().c_str()); FPSTR(MDNS_STR_ARG(record.value)));
} }
} }
} }

View File

@@ -32,8 +32,7 @@ void MDNSComponent::setup() {
uint16_t port_ = const_cast<TemplatableValue<uint16_t> &>(service.port).value(); uint16_t port_ = const_cast<TemplatableValue<uint16_t> &>(service.port).value();
MDNS.addService(service_type, proto, port_); MDNS.addService(service_type, proto, port_);
for (const auto &record : service.txt_records) { for (const auto &record : service.txt_records) {
MDNS.addServiceTxt(service_type, proto, MDNS_STR_ARG(record.key), MDNS.addServiceTxt(service_type, proto, MDNS_STR_ARG(record.key), MDNS_STR_ARG(record.value));
const_cast<TemplatableValue<std::string> &>(record.value).value().c_str());
} }
} }
} }

View File

@@ -32,8 +32,7 @@ void MDNSComponent::setup() {
uint16_t port = const_cast<TemplatableValue<uint16_t> &>(service.port).value(); uint16_t port = const_cast<TemplatableValue<uint16_t> &>(service.port).value();
MDNS.addService(service_type, proto, port); MDNS.addService(service_type, proto, port);
for (const auto &record : service.txt_records) { for (const auto &record : service.txt_records) {
MDNS.addServiceTxt(service_type, proto, MDNS_STR_ARG(record.key), MDNS.addServiceTxt(service_type, proto, MDNS_STR_ARG(record.key), MDNS_STR_ARG(record.value));
const_cast<TemplatableValue<std::string> &>(record.value).value().c_str());
} }
} }
} }

View File

@@ -180,10 +180,12 @@ void OpenThreadSrpComponent::setup() {
entry->mService.mNumTxtEntries = service.txt_records.size(); entry->mService.mNumTxtEntries = service.txt_records.size();
for (size_t i = 0; i < service.txt_records.size(); i++) { for (size_t i = 0; i < service.txt_records.size(); i++) {
const auto &txt = service.txt_records[i]; const auto &txt = service.txt_records[i];
auto value = const_cast<TemplatableValue<std::string> &>(txt.value).value(); // Value is either a compile-time string literal in flash or a pointer to dynamic_txt_values_
// OpenThread SRP client expects the data to persist, so we strdup it
const char *value_str = MDNS_STR_ARG(txt.value);
txt_entries[i].mKey = MDNS_STR_ARG(txt.key); txt_entries[i].mKey = MDNS_STR_ARG(txt.key);
txt_entries[i].mValue = reinterpret_cast<const uint8_t *>(strdup(value.c_str())); txt_entries[i].mValue = reinterpret_cast<const uint8_t *>(strdup(value_str));
txt_entries[i].mValueLength = value.size(); txt_entries[i].mValueLength = strlen(value_str);
} }
entry->mService.mTxtEntries = txt_entries; entry->mService.mTxtEntries = txt_entries;
entry->mService.mNumTxtEntries = service.txt_records.size(); entry->mService.mNumTxtEntries = service.txt_records.size();

View File

@@ -149,6 +149,9 @@ template<typename T, size_t N> class StaticVector {
T &operator[](size_t i) { return data_[i]; } T &operator[](size_t i) { return data_[i]; }
const T &operator[](size_t i) const { return data_[i]; } const T &operator[](size_t i) const { return data_[i]; }
T &back() { return data_[count_ - 1]; }
const T &back() const { return data_[count_ - 1]; }
// For range-based for loops // For range-based for loops
iterator begin() { return data_.begin(); } iterator begin() { return data_.begin(); }
iterator end() { return data_.begin() + count_; } iterator end() { return data_.begin() + count_; }

View File

@@ -25,6 +25,9 @@ mdns:
- service: _http - service: _http
protocol: _tcp protocol: _tcp
port: 80 port: 80
txt:
version: "1.0"
path: "/"
# OTA should run at priority 54 (after mdns) # OTA should run at priority 54 (after mdns)
ota: ota: