diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index d57cedd144..f7518771d7 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -572,9 +572,13 @@ async def register_mqtt_component(var, config): if not config.get(CONF_DISCOVERY, True): cg.add(var.disable_discovery()) if CONF_STATE_TOPIC in config: - cg.add(var.set_custom_state_topic(config[CONF_STATE_TOPIC])) + state_topic = await cg.templatable(config[CONF_STATE_TOPIC], [], cg.std_string) + cg.add(var.set_custom_state_topic(state_topic)) if CONF_COMMAND_TOPIC in config: - cg.add(var.set_custom_command_topic(config[CONF_COMMAND_TOPIC])) + command_topic = await cg.templatable( + config[CONF_COMMAND_TOPIC], [], cg.std_string + ) + cg.add(var.set_custom_command_topic(command_topic)) if CONF_COMMAND_RETAIN in config: cg.add(var.set_command_retain(config[CONF_COMMAND_RETAIN])) if CONF_AVAILABILITY in config: diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index 40eb15acdd..13f496c6a7 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -94,14 +94,14 @@ std::string MQTTComponent::get_default_topic_for_(const std::string &suffix) con } std::string MQTTComponent::get_state_topic_() const { - if (this->has_custom_state_topic_) - return this->custom_state_topic_.str(); + if (this->custom_state_topic_.has_value()) + return this->custom_state_topic_.value(); return this->get_default_topic_for_("state"); } std::string MQTTComponent::get_command_topic_() const { - if (this->has_custom_command_topic_) - return this->custom_command_topic_.str(); + if (this->custom_command_topic_.has_value()) + return this->custom_command_topic_.value(); return this->get_default_topic_for_("command"); } @@ -273,14 +273,6 @@ MQTTComponent::MQTTComponent() = default; float MQTTComponent::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; } void MQTTComponent::disable_discovery() { this->discovery_enabled_ = false; } -void MQTTComponent::set_custom_state_topic(const char *custom_state_topic) { - this->custom_state_topic_ = StringRef(custom_state_topic); - this->has_custom_state_topic_ = true; -} -void MQTTComponent::set_custom_command_topic(const char *custom_command_topic) { - this->custom_command_topic_ = StringRef(custom_command_topic); - this->has_custom_command_topic_ = true; -} void MQTTComponent::set_command_retain(bool command_retain) { this->command_retain_ = command_retain; } void MQTTComponent::set_availability(std::string topic, std::string payload_available, @@ -349,13 +341,13 @@ StringRef MQTTComponent::get_default_object_id_to_(std::spanget_entity()->get_icon_ref(); } bool MQTTComponent::is_disabled_by_default_() const { return this->get_entity()->is_disabled_by_default(); } bool MQTTComponent::is_internal() { - if (this->has_custom_state_topic_) { + if (this->custom_state_topic_.has_value()) { // If the custom state_topic is null, return true as it is internal and should not publish // else, return false, as it is explicitly set to a topic, so it is not internal and should publish return this->get_state_topic_().empty(); } - if (this->has_custom_command_topic_) { + if (this->custom_command_topic_.has_value()) { // If the custom command_topic is null, return true as it is internal and should not publish // else, return false, as it is explicitly set to a topic, so it is not internal and should publish return this->get_command_topic_().empty(); diff --git a/esphome/components/mqtt/mqtt_component.h b/esphome/components/mqtt/mqtt_component.h index e0b751f05f..11dfd093f3 100644 --- a/esphome/components/mqtt/mqtt_component.h +++ b/esphome/components/mqtt/mqtt_component.h @@ -6,6 +6,7 @@ #include +#include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/entity_base.h" #include "esphome/core/string_ref.h" @@ -109,10 +110,13 @@ class MQTTComponent : public Component { /// Override this method to return the component type (e.g. "light", "sensor", ...) virtual const char *component_type() const = 0; - /// Set a custom state topic. Set to "" for default behavior. - void set_custom_state_topic(const char *custom_state_topic); - /// Set a custom command topic. Set to "" for default behavior. - void set_custom_command_topic(const char *custom_command_topic); + /// Set a custom state topic. Do not set for default behavior. + template void set_custom_state_topic(T &&custom_state_topic) { + this->custom_state_topic_ = std::forward(custom_state_topic); + } + template void set_custom_command_topic(T &&custom_command_topic) { + this->custom_command_topic_ = std::forward(custom_command_topic); + } /// Set whether command message should be retained. void set_command_retain(bool command_retain); @@ -203,14 +207,11 @@ class MQTTComponent : public Component { /// Get the object ID for this MQTT component, writing to the provided buffer. StringRef get_default_object_id_to_(std::span buf) const; - StringRef custom_state_topic_{}; - StringRef custom_command_topic_{}; + TemplatableValue custom_state_topic_{}; + TemplatableValue custom_command_topic_{}; std::unique_ptr availability_; - bool has_custom_state_topic_{false}; - bool has_custom_command_topic_{false}; - bool command_retain_{false}; bool retain_{true}; uint8_t qos_{0}; diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 81a30cb0b7..8e2fadbea8 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1966,7 +1966,9 @@ MQTT_COMPONENT_SCHEMA = Schema( Optional(CONF_RETAIN): All(requires_component("mqtt"), boolean), Optional(CONF_DISCOVERY): All(requires_component("mqtt"), boolean), Optional(CONF_SUBSCRIBE_QOS): All(requires_component("mqtt"), mqtt_qos), - Optional(CONF_STATE_TOPIC): All(requires_component("mqtt"), publish_topic), + Optional(CONF_STATE_TOPIC): All( + requires_component("mqtt"), templatable(publish_topic) + ), Optional(CONF_AVAILABILITY): All( requires_component("mqtt"), Any(None, MQTT_COMPONENT_AVAILABILITY_SCHEMA) ), @@ -1975,7 +1977,9 @@ MQTT_COMPONENT_SCHEMA = Schema( MQTT_COMMAND_COMPONENT_SCHEMA = MQTT_COMPONENT_SCHEMA.extend( { - Optional(CONF_COMMAND_TOPIC): All(requires_component("mqtt"), subscribe_topic), + Optional(CONF_COMMAND_TOPIC): All( + requires_component("mqtt"), templatable(subscribe_topic) + ), Optional(CONF_COMMAND_RETAIN): All(requires_component("mqtt"), boolean), } ) diff --git a/esphome/core/automation.h b/esphome/core/automation.h index 585b434bb2..eac469d0fc 100644 --- a/esphome/core/automation.h +++ b/esphome/core/automation.h @@ -42,6 +42,10 @@ template struct gens<0, S...> { using type = seq; }; #define TEMPLATABLE_VALUE(type, name) TEMPLATABLE_VALUE_(type, name) template class TemplatableValue { + // For std::string, store pointer to heap-allocated string to keep union pointer-sized. + // For other types, store value inline. + static constexpr bool USE_HEAP_STORAGE = std::same_as; + public: TemplatableValue() : type_(NONE) {} @@ -52,7 +56,11 @@ template class TemplatableValue { } template TemplatableValue(F value) requires(!std::invocable) : type_(VALUE) { - new (&this->value_) T(std::move(value)); + if constexpr (USE_HEAP_STORAGE) { + this->value_ = new T(std::move(value)); + } else { + new (&this->value_) T(std::move(value)); + } } // For stateless lambdas (convertible to function pointer): use function pointer @@ -71,7 +79,11 @@ template class TemplatableValue { // Copy constructor TemplatableValue(const TemplatableValue &other) : type_(other.type_) { if (this->type_ == VALUE) { - new (&this->value_) T(other.value_); + if constexpr (USE_HEAP_STORAGE) { + this->value_ = new T(*other.value_); + } else { + new (&this->value_) T(other.value_); + } } else if (this->type_ == LAMBDA) { this->f_ = new std::function(*other.f_); } else if (this->type_ == STATELESS_LAMBDA) { @@ -84,7 +96,12 @@ template class TemplatableValue { // Move constructor TemplatableValue(TemplatableValue &&other) noexcept : type_(other.type_) { if (this->type_ == VALUE) { - new (&this->value_) T(std::move(other.value_)); + if constexpr (USE_HEAP_STORAGE) { + this->value_ = other.value_; + other.value_ = nullptr; + } else { + new (&this->value_) T(std::move(other.value_)); + } } else if (this->type_ == LAMBDA) { this->f_ = other.f_; other.f_ = nullptr; @@ -115,23 +132,31 @@ template class TemplatableValue { ~TemplatableValue() { if (this->type_ == VALUE) { - this->value_.~T(); + if constexpr (USE_HEAP_STORAGE) { + delete this->value_; + } else { + this->value_.~T(); + } } else if (this->type_ == LAMBDA) { delete this->f_; } // STATELESS_LAMBDA/STATIC_STRING/NONE: no cleanup needed (pointers, not heap-allocated) } - bool has_value() { return this->type_ != NONE; } + bool has_value() const { return this->type_ != NONE; } - T value(X... x) { + T value(X... x) const { switch (this->type_) { case STATELESS_LAMBDA: return this->stateless_f_(x...); // Direct function pointer call case LAMBDA: return (*this->f_)(x...); // std::function call case VALUE: - return this->value_; + if constexpr (USE_HEAP_STORAGE) { + return *this->value_; + } else { + return this->value_; + } case STATIC_STRING: // if constexpr required: code must compile for all T, but STATIC_STRING // can only be set when T is std::string (enforced by constructor constraint) @@ -174,8 +199,11 @@ template class TemplatableValue { STATIC_STRING, // For const char* when T is std::string - avoids heap allocation } type_; + // For std::string, use heap pointer to minimize union size (4 bytes vs 12+). + // For other types, store value inline as before. + using ValueStorage = std::conditional_t; union { - T value_; + ValueStorage value_; // T for inline storage, T* for heap storage std::function *f_; T (*stateless_f_)(X...); const char *static_str_; // For STATIC_STRING type diff --git a/tests/components/mqtt/common.yaml b/tests/components/mqtt/common.yaml index 33988cebb4..4cf2692593 100644 --- a/tests/components/mqtt/common.yaml +++ b/tests/components/mqtt/common.yaml @@ -91,6 +91,7 @@ button: - platform: template name: "Template Button" state_topic: some/topic/button + command_topic: !lambda return "some/topic/button/command"; qos: 2 on_press: - mqtt.disable @@ -295,7 +296,7 @@ event: fan: - platform: template name: Template Fan - state_topic: some/topic/fan + state_topic: !lambda return "some/topic/fan"; direction_state_topic: some/topic/direction/state direction_command_topic: some/topic/direction/command qos: 2