diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index 3fa4d2ebef..61fc1e196a 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -58,11 +58,21 @@ CONFIG_SCHEMA = cv.All( ) -def mdns_txt_record(key: str, value: str): +def mdns_txt_record_static(key: str, value: str): + """Create a TXT record with a static (compile-time) value stored in flash.""" return cg.StructInitializer( MDNSTXTRecord, ("key", cg.RawExpression(f"MDNS_STR({cg.safe_exp(key)})")), - ("value", value), + ("value", cg.RawExpression(f"MDNS_STR({cg.safe_exp(value)})")), + ) + + +def mdns_txt_record_dynamic(key: str, value_expr: str): + """Create a TXT record with a dynamic value (will be evaluated and stored in vector).""" + return cg.StructInitializer( + MDNSTXTRecord, + ("key", cg.RawExpression(f"MDNS_STR({cg.safe_exp(key)})")), + ("value", cg.RawExpression(f"MDNS_STR({value_expr})")), ) @@ -107,23 +117,56 @@ async def to_code(config): # Ensure at least 1 service (fallback service) 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 + # Conditional: friendly_name (if not empty, but we conservatively count it) + dynamic_txt_count += 1 + # Conditional: dashboard_import_url + if "dashboard_import" in CORE.config: + 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]) await cg.register_component(var, config) for service in config[CONF_SERVICES]: - txt = [ - cg.StructInitializer( - MDNSTXTRecord, - ("key", cg.RawExpression(f"MDNS_STR({cg.safe_exp(txt_key)})")), - ("value", await cg.templatable(txt_value, [], cg.std_string)), - ) - for txt_key, txt_value in service[CONF_TXT].items() - ] + # Build the txt records list for the service + txt_records = [] + for txt_key, txt_value in service[CONF_TXT].items(): + if cg.is_template(txt_value): + # 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})()))}}" + ) + ) + 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( service[CONF_SERVICE], service[CONF_PROTOCOL], await cg.templatable(service[CONF_PORT], [], cg.uint16), - txt, + txt_records, ) cg.add(var.add_extra_service(exp)) diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index 8945053b7d..15310815fb 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -9,21 +9,9 @@ #include // 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 -// 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 // On non-ESP8266 platforms, use regular const char* #define MDNS_STATIC_CONST_CHAR(name, value) static constexpr const char name[] = value -#define MDNS_STR_VALUE(name) std::string(name) #endif #ifdef USE_API @@ -109,47 +97,48 @@ void MDNSComponent::compile_records_() { txt_records.reserve(txt_count); 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(this->add_dynamic_txt_value(friendly_name))}); } - txt_records.push_back({MDNS_STR(TXT_VERSION), ESPHOME_VERSION}); - txt_records.push_back({MDNS_STR(TXT_MAC), get_mac_address()}); + txt_records.push_back({MDNS_STR(TXT_VERSION), MDNS_STR(ESPHOME_VERSION)}); + txt_records.push_back({MDNS_STR(TXT_MAC), MDNS_STR(this->add_dynamic_txt_value(get_mac_address()))}); #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) - 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) - 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) - 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 - txt_records.push_back({MDNS_STR(TXT_BOARD), ESPHOME_BOARD}); + txt_records.push_back({MDNS_STR(TXT_BOARD), MDNS_STR(ESPHOME_BOARD)}); #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) - 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) - 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 #ifdef USE_API_NOISE 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(TXT_API_ENCRYPTION), MDNS_STR_VALUE(NOISE_ENCRYPTION)}); + txt_records.push_back({MDNS_STR(TXT_API_ENCRYPTION), MDNS_STR(NOISE_ENCRYPTION)}); } else { - txt_records.push_back({MDNS_STR(TXT_API_ENCRYPTION_SUPPORTED), MDNS_STR_VALUE(NOISE_ENCRYPTION)}); + txt_records.push_back({MDNS_STR(TXT_API_ENCRYPTION_SUPPORTED), MDNS_STR(NOISE_ENCRYPTION)}); } #endif #ifdef ESPHOME_PROJECT_NAME - txt_records.push_back({MDNS_STR(TXT_PROJECT_NAME), ESPHOME_PROJECT_NAME}); - txt_records.push_back({MDNS_STR(TXT_PROJECT_VERSION), ESPHOME_PROJECT_VERSION}); + txt_records.push_back({MDNS_STR(TXT_PROJECT_NAME), MDNS_STR(ESPHOME_PROJECT_NAME)}); + txt_records.push_back({MDNS_STR(TXT_PROJECT_VERSION), MDNS_STR(ESPHOME_PROJECT_VERSION)}); #endif // ESPHOME_PROJECT_NAME #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(this->add_dynamic_txt_value(dashboard_import::get_package_import_url()))}); #endif } #endif // USE_API @@ -175,7 +164,7 @@ void MDNSComponent::compile_records_() { fallback_service.service_type = MDNS_STR(SERVICE_HTTP); fallback_service.proto = MDNS_STR(SERVICE_TCP); 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(ESPHOME_VERSION)}); #endif } @@ -190,8 +179,7 @@ void MDNSComponent::dump_config() { ESP_LOGV(TAG, " - %s, %s, %d", MDNS_STR_ARG(service.service_type), MDNS_STR_ARG(service.proto), const_cast &>(service.port).value()); for (const auto &record : service.txt_records) { - ESP_LOGV(TAG, " TXT: %s = %s", MDNS_STR_ARG(record.key), - const_cast &>(record.value).value().c_str()); + ESP_LOGV(TAG, " TXT: %s = %s", MDNS_STR_ARG(record.key), MDNS_STR_ARG(record.value)); } } #endif diff --git a/esphome/components/mdns/mdns_component.h b/esphome/components/mdns/mdns_component.h index b1f73fbb32..a3684f6f5e 100644 --- a/esphome/components/mdns/mdns_component.h +++ b/esphome/components/mdns/mdns_component.h @@ -27,7 +27,7 @@ struct MDNSString; struct MDNSTXTRecord { const MDNSString *key; - TemplatableValue value; + const MDNSString *value; }; struct MDNSService { @@ -59,6 +59,14 @@ class MDNSComponent : public Component { 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(); + } + + StaticVector dynamic_txt_values_; + protected: StaticVector services_{}; std::string hostname_; diff --git a/esphome/components/mdns/mdns_esp32.cpp b/esphome/components/mdns/mdns_esp32.cpp index 40d305a1e6..223eeb8e8c 100644 --- a/esphome/components/mdns/mdns_esp32.cpp +++ b/esphome/components/mdns/mdns_esp32.cpp @@ -29,10 +29,10 @@ void MDNSComponent::setup() { std::vector txt_records; for (const auto &record : service.txt_records) { 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_ + // ESP-IDF requires strdup for both to keep them alive during mdns operation it.key = MDNS_STR_ARG(record.key); - // value is a temporary from TemplatableValue, must strdup to keep it alive - it.value = strdup(const_cast &>(record.value).value().c_str()); + it.value = strdup(MDNS_STR_ARG(record.value)); txt_records.push_back(it); } uint16_t port = const_cast &>(service.port).value(); diff --git a/esphome/components/mdns/mdns_esp8266.cpp b/esphome/components/mdns/mdns_esp8266.cpp index f1c8909807..f3779042ed 100644 --- a/esphome/components/mdns/mdns_esp8266.cpp +++ b/esphome/components/mdns/mdns_esp8266.cpp @@ -33,7 +33,7 @@ void MDNSComponent::setup() { MDNS.addService(FPSTR(service_type), FPSTR(proto), port); for (const auto &record : service.txt_records) { MDNS.addServiceTxt(FPSTR(service_type), FPSTR(proto), FPSTR(MDNS_STR_ARG(record.key)), - const_cast &>(record.value).value().c_str()); + FPSTR(MDNS_STR_ARG(record.value))); } } } diff --git a/esphome/components/mdns/mdns_libretiny.cpp b/esphome/components/mdns/mdns_libretiny.cpp index 9010ca2bc6..5540bf361a 100644 --- a/esphome/components/mdns/mdns_libretiny.cpp +++ b/esphome/components/mdns/mdns_libretiny.cpp @@ -32,8 +32,7 @@ void MDNSComponent::setup() { uint16_t port_ = const_cast &>(service.port).value(); MDNS.addService(service_type, proto, port_); for (const auto &record : service.txt_records) { - MDNS.addServiceTxt(service_type, proto, MDNS_STR_ARG(record.key), - const_cast &>(record.value).value().c_str()); + MDNS.addServiceTxt(service_type, proto, MDNS_STR_ARG(record.key), MDNS_STR_ARG(record.value)); } } } diff --git a/esphome/components/mdns/mdns_rp2040.cpp b/esphome/components/mdns/mdns_rp2040.cpp index 039453f501..5ad006f5d4 100644 --- a/esphome/components/mdns/mdns_rp2040.cpp +++ b/esphome/components/mdns/mdns_rp2040.cpp @@ -32,8 +32,7 @@ void MDNSComponent::setup() { uint16_t port = const_cast &>(service.port).value(); MDNS.addService(service_type, proto, port); for (const auto &record : service.txt_records) { - MDNS.addServiceTxt(service_type, proto, MDNS_STR_ARG(record.key), - const_cast &>(record.value).value().c_str()); + MDNS.addServiceTxt(service_type, proto, MDNS_STR_ARG(record.key), MDNS_STR_ARG(record.value)); } } } diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index e06f2d15ef..fe89f0b24f 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -146,6 +146,9 @@ template class StaticVector { T &operator[](size_t i) { 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 iterator begin() { return data_.begin(); } iterator end() { return data_.begin() + count_; } diff --git a/tests/components/mdns/test-comprehensive.esp8266-ard.yaml b/tests/components/mdns/test-comprehensive.esp8266-ard.yaml index 02767833a3..3129ca3143 100644 --- a/tests/components/mdns/test-comprehensive.esp8266-ard.yaml +++ b/tests/components/mdns/test-comprehensive.esp8266-ard.yaml @@ -25,6 +25,9 @@ mdns: - service: _http protocol: _tcp port: 80 + txt: + version: "1.0" + path: "/" # OTA should run at priority 54 (after mdns) ota: