mirror of
https://github.com/esphome/esphome.git
synced 2026-02-08 00:31:58 +00:00
[mqtt] templatable state and command topics (#12441)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
@@ -572,9 +572,13 @@ async def register_mqtt_component(var, config):
|
|||||||
if not config.get(CONF_DISCOVERY, True):
|
if not config.get(CONF_DISCOVERY, True):
|
||||||
cg.add(var.disable_discovery())
|
cg.add(var.disable_discovery())
|
||||||
if CONF_STATE_TOPIC in config:
|
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:
|
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:
|
if CONF_COMMAND_RETAIN in config:
|
||||||
cg.add(var.set_command_retain(config[CONF_COMMAND_RETAIN]))
|
cg.add(var.set_command_retain(config[CONF_COMMAND_RETAIN]))
|
||||||
if CONF_AVAILABILITY in config:
|
if CONF_AVAILABILITY in config:
|
||||||
|
|||||||
@@ -94,14 +94,14 @@ std::string MQTTComponent::get_default_topic_for_(const std::string &suffix) con
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::string MQTTComponent::get_state_topic_() const {
|
std::string MQTTComponent::get_state_topic_() const {
|
||||||
if (this->has_custom_state_topic_)
|
if (this->custom_state_topic_.has_value())
|
||||||
return this->custom_state_topic_.str();
|
return this->custom_state_topic_.value();
|
||||||
return this->get_default_topic_for_("state");
|
return this->get_default_topic_for_("state");
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string MQTTComponent::get_command_topic_() const {
|
std::string MQTTComponent::get_command_topic_() const {
|
||||||
if (this->has_custom_command_topic_)
|
if (this->custom_command_topic_.has_value())
|
||||||
return this->custom_command_topic_.str();
|
return this->custom_command_topic_.value();
|
||||||
return this->get_default_topic_for_("command");
|
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; }
|
float MQTTComponent::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; }
|
||||||
void MQTTComponent::disable_discovery() { this->discovery_enabled_ = false; }
|
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_command_retain(bool command_retain) { this->command_retain_ = command_retain; }
|
||||||
|
|
||||||
void MQTTComponent::set_availability(std::string topic, std::string payload_available,
|
void MQTTComponent::set_availability(std::string topic, std::string payload_available,
|
||||||
@@ -349,13 +341,13 @@ StringRef MQTTComponent::get_default_object_id_to_(std::span<char, OBJECT_ID_MAX
|
|||||||
StringRef MQTTComponent::get_icon_ref_() const { return this->get_entity()->get_icon_ref(); }
|
StringRef MQTTComponent::get_icon_ref_() const { return this->get_entity()->get_icon_ref(); }
|
||||||
bool MQTTComponent::is_disabled_by_default_() const { return this->get_entity()->is_disabled_by_default(); }
|
bool MQTTComponent::is_disabled_by_default_() const { return this->get_entity()->is_disabled_by_default(); }
|
||||||
bool MQTTComponent::is_internal() {
|
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
|
// 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
|
// 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();
|
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
|
// 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
|
// 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();
|
return this->get_command_topic_().empty();
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
|
||||||
|
#include "esphome/core/automation.h"
|
||||||
#include "esphome/core/component.h"
|
#include "esphome/core/component.h"
|
||||||
#include "esphome/core/entity_base.h"
|
#include "esphome/core/entity_base.h"
|
||||||
#include "esphome/core/string_ref.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", ...)
|
/// Override this method to return the component type (e.g. "light", "sensor", ...)
|
||||||
virtual const char *component_type() const = 0;
|
virtual const char *component_type() const = 0;
|
||||||
|
|
||||||
/// Set a custom state topic. Set to "" for default behavior.
|
/// Set a custom state topic. Do not set for default behavior.
|
||||||
void set_custom_state_topic(const char *custom_state_topic);
|
template<typename T> void set_custom_state_topic(T &&custom_state_topic) {
|
||||||
/// Set a custom command topic. Set to "" for default behavior.
|
this->custom_state_topic_ = std::forward<T>(custom_state_topic);
|
||||||
void set_custom_command_topic(const char *custom_command_topic);
|
}
|
||||||
|
template<typename T> void set_custom_command_topic(T &&custom_command_topic) {
|
||||||
|
this->custom_command_topic_ = std::forward<T>(custom_command_topic);
|
||||||
|
}
|
||||||
/// Set whether command message should be retained.
|
/// Set whether command message should be retained.
|
||||||
void set_command_retain(bool command_retain);
|
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.
|
/// Get the object ID for this MQTT component, writing to the provided buffer.
|
||||||
StringRef get_default_object_id_to_(std::span<char, OBJECT_ID_MAX_LEN> buf) const;
|
StringRef get_default_object_id_to_(std::span<char, OBJECT_ID_MAX_LEN> buf) const;
|
||||||
|
|
||||||
StringRef custom_state_topic_{};
|
TemplatableValue<std::string> custom_state_topic_{};
|
||||||
StringRef custom_command_topic_{};
|
TemplatableValue<std::string> custom_command_topic_{};
|
||||||
|
|
||||||
std::unique_ptr<Availability> availability_;
|
std::unique_ptr<Availability> availability_;
|
||||||
|
|
||||||
bool has_custom_state_topic_{false};
|
|
||||||
bool has_custom_command_topic_{false};
|
|
||||||
|
|
||||||
bool command_retain_{false};
|
bool command_retain_{false};
|
||||||
bool retain_{true};
|
bool retain_{true};
|
||||||
uint8_t qos_{0};
|
uint8_t qos_{0};
|
||||||
|
|||||||
@@ -1966,7 +1966,9 @@ MQTT_COMPONENT_SCHEMA = Schema(
|
|||||||
Optional(CONF_RETAIN): All(requires_component("mqtt"), boolean),
|
Optional(CONF_RETAIN): All(requires_component("mqtt"), boolean),
|
||||||
Optional(CONF_DISCOVERY): 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_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(
|
Optional(CONF_AVAILABILITY): All(
|
||||||
requires_component("mqtt"), Any(None, MQTT_COMPONENT_AVAILABILITY_SCHEMA)
|
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(
|
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),
|
Optional(CONF_COMMAND_RETAIN): All(requires_component("mqtt"), boolean),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ template<int... S> struct gens<0, S...> { using type = seq<S...>; };
|
|||||||
#define TEMPLATABLE_VALUE(type, name) TEMPLATABLE_VALUE_(type, name)
|
#define TEMPLATABLE_VALUE(type, name) TEMPLATABLE_VALUE_(type, name)
|
||||||
|
|
||||||
template<typename T, typename... X> class TemplatableValue {
|
template<typename T, typename... X> 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<T, std::string>;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
TemplatableValue() : type_(NONE) {}
|
TemplatableValue() : type_(NONE) {}
|
||||||
|
|
||||||
@@ -52,7 +56,11 @@ template<typename T, typename... X> class TemplatableValue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
template<typename F> TemplatableValue(F value) requires(!std::invocable<F, X...>) : type_(VALUE) {
|
template<typename F> TemplatableValue(F value) requires(!std::invocable<F, X...>) : 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
|
// For stateless lambdas (convertible to function pointer): use function pointer
|
||||||
@@ -71,7 +79,11 @@ template<typename T, typename... X> class TemplatableValue {
|
|||||||
// Copy constructor
|
// Copy constructor
|
||||||
TemplatableValue(const TemplatableValue &other) : type_(other.type_) {
|
TemplatableValue(const TemplatableValue &other) : type_(other.type_) {
|
||||||
if (this->type_ == VALUE) {
|
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) {
|
} else if (this->type_ == LAMBDA) {
|
||||||
this->f_ = new std::function<T(X...)>(*other.f_);
|
this->f_ = new std::function<T(X...)>(*other.f_);
|
||||||
} else if (this->type_ == STATELESS_LAMBDA) {
|
} else if (this->type_ == STATELESS_LAMBDA) {
|
||||||
@@ -84,7 +96,12 @@ template<typename T, typename... X> class TemplatableValue {
|
|||||||
// Move constructor
|
// Move constructor
|
||||||
TemplatableValue(TemplatableValue &&other) noexcept : type_(other.type_) {
|
TemplatableValue(TemplatableValue &&other) noexcept : type_(other.type_) {
|
||||||
if (this->type_ == VALUE) {
|
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) {
|
} else if (this->type_ == LAMBDA) {
|
||||||
this->f_ = other.f_;
|
this->f_ = other.f_;
|
||||||
other.f_ = nullptr;
|
other.f_ = nullptr;
|
||||||
@@ -115,23 +132,31 @@ template<typename T, typename... X> class TemplatableValue {
|
|||||||
|
|
||||||
~TemplatableValue() {
|
~TemplatableValue() {
|
||||||
if (this->type_ == VALUE) {
|
if (this->type_ == VALUE) {
|
||||||
this->value_.~T();
|
if constexpr (USE_HEAP_STORAGE) {
|
||||||
|
delete this->value_;
|
||||||
|
} else {
|
||||||
|
this->value_.~T();
|
||||||
|
}
|
||||||
} else if (this->type_ == LAMBDA) {
|
} else if (this->type_ == LAMBDA) {
|
||||||
delete this->f_;
|
delete this->f_;
|
||||||
}
|
}
|
||||||
// STATELESS_LAMBDA/STATIC_STRING/NONE: no cleanup needed (pointers, not heap-allocated)
|
// 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_) {
|
switch (this->type_) {
|
||||||
case STATELESS_LAMBDA:
|
case STATELESS_LAMBDA:
|
||||||
return this->stateless_f_(x...); // Direct function pointer call
|
return this->stateless_f_(x...); // Direct function pointer call
|
||||||
case LAMBDA:
|
case LAMBDA:
|
||||||
return (*this->f_)(x...); // std::function call
|
return (*this->f_)(x...); // std::function call
|
||||||
case VALUE:
|
case VALUE:
|
||||||
return this->value_;
|
if constexpr (USE_HEAP_STORAGE) {
|
||||||
|
return *this->value_;
|
||||||
|
} else {
|
||||||
|
return this->value_;
|
||||||
|
}
|
||||||
case STATIC_STRING:
|
case STATIC_STRING:
|
||||||
// if constexpr required: code must compile for all T, but 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)
|
// can only be set when T is std::string (enforced by constructor constraint)
|
||||||
@@ -174,8 +199,11 @@ template<typename T, typename... X> class TemplatableValue {
|
|||||||
STATIC_STRING, // For const char* when T is std::string - avoids heap allocation
|
STATIC_STRING, // For const char* when T is std::string - avoids heap allocation
|
||||||
} type_;
|
} 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<USE_HEAP_STORAGE, T *, T>;
|
||||||
union {
|
union {
|
||||||
T value_;
|
ValueStorage value_; // T for inline storage, T* for heap storage
|
||||||
std::function<T(X...)> *f_;
|
std::function<T(X...)> *f_;
|
||||||
T (*stateless_f_)(X...);
|
T (*stateless_f_)(X...);
|
||||||
const char *static_str_; // For STATIC_STRING type
|
const char *static_str_; // For STATIC_STRING type
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ button:
|
|||||||
- platform: template
|
- platform: template
|
||||||
name: "Template Button"
|
name: "Template Button"
|
||||||
state_topic: some/topic/button
|
state_topic: some/topic/button
|
||||||
|
command_topic: !lambda return "some/topic/button/command";
|
||||||
qos: 2
|
qos: 2
|
||||||
on_press:
|
on_press:
|
||||||
- mqtt.disable
|
- mqtt.disable
|
||||||
@@ -295,7 +296,7 @@ event:
|
|||||||
fan:
|
fan:
|
||||||
- platform: template
|
- platform: template
|
||||||
name: Template Fan
|
name: Template Fan
|
||||||
state_topic: some/topic/fan
|
state_topic: !lambda return "some/topic/fan";
|
||||||
direction_state_topic: some/topic/direction/state
|
direction_state_topic: some/topic/direction/state
|
||||||
direction_command_topic: some/topic/direction/command
|
direction_command_topic: some/topic/direction/command
|
||||||
qos: 2
|
qos: 2
|
||||||
|
|||||||
Reference in New Issue
Block a user