[mqtt] Reduce heap allocations in hot paths (#13362)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
This commit is contained in:
J. Nick Koston
2026-01-21 18:35:59 -10:00
committed by GitHub
parent aa5092bdc2
commit 99aa83564e
15 changed files with 179 additions and 72 deletions
@@ -43,7 +43,7 @@ void MQTTAlarmControlPanelComponent::setup() {
void MQTTAlarmControlPanelComponent::dump_config() { void MQTTAlarmControlPanelComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT alarm_control_panel '%s':", this->alarm_control_panel_->get_name().c_str()); ESP_LOGCONFIG(TAG, "MQTT alarm_control_panel '%s':", this->alarm_control_panel_->get_name().c_str());
LOG_MQTT_COMPONENT(true, true) LOG_MQTT_COMPONENT(true, true);
ESP_LOGCONFIG(TAG, ESP_LOGCONFIG(TAG,
" Supported Features: %" PRIu32 "\n" " Supported Features: %" PRIu32 "\n"
" Requires Code to Disarm: %s\n" " Requires Code to Disarm: %s\n"
@@ -19,7 +19,7 @@ void MQTTBinarySensorComponent::setup() {
void MQTTBinarySensorComponent::dump_config() { void MQTTBinarySensorComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT Binary Sensor '%s':", this->binary_sensor_->get_name().c_str()); ESP_LOGCONFIG(TAG, "MQTT Binary Sensor '%s':", this->binary_sensor_->get_name().c_str());
LOG_MQTT_COMPONENT(true, false) LOG_MQTT_COMPONENT(true, false);
} }
MQTTBinarySensorComponent::MQTTBinarySensorComponent(binary_sensor::BinarySensor *binary_sensor) MQTTBinarySensorComponent::MQTTBinarySensorComponent(binary_sensor::BinarySensor *binary_sensor)
: binary_sensor_(binary_sensor) { : binary_sensor_(binary_sensor) {
+67 -34
View File
@@ -27,20 +27,23 @@ inline char *append_char(char *p, char c) {
// Max lengths for stack-based topic building. // Max lengths for stack-based topic building.
// These limits are enforced at Python config validation time in mqtt/__init__.py // These limits are enforced at Python config validation time in mqtt/__init__.py
// using cv.Length() validators for topic_prefix and discovery_prefix. // using cv.Length() validators for topic_prefix and discovery_prefix.
// MQTT_COMPONENT_TYPE_MAX_LEN and MQTT_SUFFIX_MAX_LEN are defined in mqtt_component.h. // MQTT_COMPONENT_TYPE_MAX_LEN, MQTT_SUFFIX_MAX_LEN, and MQTT_DEFAULT_TOPIC_MAX_LEN are in mqtt_component.h.
// ESPHOME_DEVICE_NAME_MAX_LEN and OBJECT_ID_MAX_LEN are defined in entity_base.h. // ESPHOME_DEVICE_NAME_MAX_LEN and OBJECT_ID_MAX_LEN are defined in entity_base.h.
// This ensures the stack buffers below are always large enough. // This ensures the stack buffers below are always large enough.
static constexpr size_t TOPIC_PREFIX_MAX_LEN = 64; // Validated in Python: cv.Length(max=64)
static constexpr size_t DISCOVERY_PREFIX_MAX_LEN = 64; // Validated in Python: cv.Length(max=64) static constexpr size_t DISCOVERY_PREFIX_MAX_LEN = 64; // Validated in Python: cv.Length(max=64)
// Stack buffer sizes - safe because all inputs are length-validated at config time
// Format: prefix + "/" + type + "/" + object_id + "/" + suffix + null
static constexpr size_t DEFAULT_TOPIC_MAX_LEN =
TOPIC_PREFIX_MAX_LEN + 1 + MQTT_COMPONENT_TYPE_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 1 + MQTT_SUFFIX_MAX_LEN + 1;
// Format: prefix + "/" + type + "/" + name + "/" + object_id + "/config" + null // Format: prefix + "/" + type + "/" + name + "/" + object_id + "/config" + null
static constexpr size_t DISCOVERY_TOPIC_MAX_LEN = DISCOVERY_PREFIX_MAX_LEN + 1 + MQTT_COMPONENT_TYPE_MAX_LEN + 1 + static constexpr size_t DISCOVERY_TOPIC_MAX_LEN = DISCOVERY_PREFIX_MAX_LEN + 1 + MQTT_COMPONENT_TYPE_MAX_LEN + 1 +
ESPHOME_DEVICE_NAME_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 7 + 1; ESPHOME_DEVICE_NAME_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 7 + 1;
// Function implementation of LOG_MQTT_COMPONENT macro to reduce code size
void log_mqtt_component(const char *tag, MQTTComponent *obj, bool state_topic, bool command_topic) {
char buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
if (state_topic)
ESP_LOGCONFIG(tag, " State Topic: '%s'", obj->get_state_topic_to_(buf).c_str());
if (command_topic)
ESP_LOGCONFIG(tag, " Command Topic: '%s'", obj->get_command_topic_to_(buf).c_str());
}
void MQTTComponent::set_qos(uint8_t qos) { this->qos_ = qos; } void MQTTComponent::set_qos(uint8_t qos) { this->qos_ = qos; }
void MQTTComponent::set_subscribe_qos(uint8_t qos) { this->subscribe_qos_ = qos; } void MQTTComponent::set_subscribe_qos(uint8_t qos) { this->subscribe_qos_ = qos; }
@@ -69,19 +72,18 @@ std::string MQTTComponent::get_discovery_topic_(const MQTTDiscoveryInfo &discove
return std::string(buf, p - buf); return std::string(buf, p - buf);
} }
std::string MQTTComponent::get_default_topic_for_(const std::string &suffix) const { StringRef MQTTComponent::get_default_topic_for_to_(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf, const char *suffix,
size_t suffix_len) const {
const std::string &topic_prefix = global_mqtt_client->get_topic_prefix(); const std::string &topic_prefix = global_mqtt_client->get_topic_prefix();
if (topic_prefix.empty()) { if (topic_prefix.empty()) {
// If the topic_prefix is null, the default topic should be null return StringRef(); // Empty topic_prefix means no default topic
return "";
} }
const char *comp_type = this->component_type(); const char *comp_type = this->component_type();
char object_id_buf[OBJECT_ID_MAX_LEN]; char object_id_buf[OBJECT_ID_MAX_LEN];
StringRef object_id = this->get_default_object_id_to_(object_id_buf); StringRef object_id = this->get_default_object_id_to_(object_id_buf);
char buf[DEFAULT_TOPIC_MAX_LEN]; char *p = buf.data();
char *p = buf;
p = append_str(p, topic_prefix.data(), topic_prefix.size()); p = append_str(p, topic_prefix.data(), topic_prefix.size());
p = append_char(p, '/'); p = append_char(p, '/');
@@ -89,21 +91,44 @@ std::string MQTTComponent::get_default_topic_for_(const std::string &suffix) con
p = append_char(p, '/'); p = append_char(p, '/');
p = append_str(p, object_id.c_str(), object_id.size()); p = append_str(p, object_id.c_str(), object_id.size());
p = append_char(p, '/'); p = append_char(p, '/');
p = append_str(p, suffix.data(), suffix.size()); p = append_str(p, suffix, suffix_len);
*p = '\0';
return std::string(buf, p - buf); return StringRef(buf.data(), p - buf.data());
}
std::string MQTTComponent::get_default_topic_for_(const std::string &suffix) const {
char buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
StringRef ref = this->get_default_topic_for_to_(buf, suffix.data(), suffix.size());
return std::string(ref.c_str(), ref.size());
}
StringRef MQTTComponent::get_state_topic_to_(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf) const {
if (this->custom_state_topic_.has_value()) {
// Returns ref to existing data for static/value, uses buf only for lambda case
return this->custom_state_topic_.ref_or_copy_to(buf.data(), buf.size());
}
return this->get_default_topic_for_to_(buf, "state", 5);
}
StringRef MQTTComponent::get_command_topic_to_(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf) const {
if (this->custom_command_topic_.has_value()) {
// Returns ref to existing data for static/value, uses buf only for lambda case
return this->custom_command_topic_.ref_or_copy_to(buf.data(), buf.size());
}
return this->get_default_topic_for_to_(buf, "command", 7);
} }
std::string MQTTComponent::get_state_topic_() const { std::string MQTTComponent::get_state_topic_() const {
if (this->custom_state_topic_.has_value()) char buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
return this->custom_state_topic_.value(); StringRef ref = this->get_state_topic_to_(buf);
return this->get_default_topic_for_("state"); return std::string(ref.c_str(), ref.size());
} }
std::string MQTTComponent::get_command_topic_() const { std::string MQTTComponent::get_command_topic_() const {
if (this->custom_command_topic_.has_value()) char buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
return this->custom_command_topic_.value(); StringRef ref = this->get_command_topic_to_(buf);
return this->get_default_topic_for_("command"); return std::string(ref.c_str(), ref.size());
} }
bool MQTTComponent::publish(const std::string &topic, const std::string &payload) { bool MQTTComponent::publish(const std::string &topic, const std::string &payload) {
@@ -168,10 +193,14 @@ bool MQTTComponent::send_discovery_() {
break; break;
} }
if (config.state_topic) if (config.state_topic) {
root[MQTT_STATE_TOPIC] = this->get_state_topic_(); char state_topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
if (config.command_topic) root[MQTT_STATE_TOPIC] = this->get_state_topic_to_(state_topic_buf);
root[MQTT_COMMAND_TOPIC] = this->get_command_topic_(); }
if (config.command_topic) {
char command_topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
root[MQTT_COMMAND_TOPIC] = this->get_command_topic_to_(command_topic_buf);
}
if (this->command_retain_) if (this->command_retain_)
root[MQTT_COMMAND_RETAIN] = true; root[MQTT_COMMAND_RETAIN] = true;
@@ -309,7 +338,9 @@ void MQTTComponent::set_availability(std::string topic, std::string payload_avai
} }
void MQTTComponent::disable_availability() { this->set_availability("", "", ""); } void MQTTComponent::disable_availability() { this->set_availability("", "", ""); }
void MQTTComponent::call_setup() { void MQTTComponent::call_setup() {
if (this->is_internal()) // Cache is_internal result once during setup - topics don't change after this
this->is_internal_ = this->compute_is_internal_();
if (this->is_internal_)
return; return;
this->setup(); this->setup();
@@ -361,26 +392,28 @@ 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::compute_is_internal_() {
if (this->custom_state_topic_.has_value()) { 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 empty, 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(); // Using is_empty() avoids heap allocation for non-lambda cases
return this->custom_state_topic_.is_empty();
} }
if (this->custom_command_topic_.has_value()) { 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 empty, 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(); // Using is_empty() avoids heap allocation for non-lambda cases
return this->custom_command_topic_.is_empty();
} }
// No custom topics have been set // No custom topics have been set - check topic_prefix directly to avoid allocation
if (this->get_default_topic_for_("").empty()) { if (global_mqtt_client->get_topic_prefix().empty()) {
// If the default topic prefix is null, then the component, by default, is internal and should not publish // If the default topic prefix is empty, then the component, by default, is internal and should not publish
return true; return true;
} }
// Use ESPHome's component internal state if topic_prefix is not null with no custom state_topic or command_topic // Use ESPHome's component internal state if topic_prefix is not empty with no custom state_topic or command_topic
return this->get_entity()->is_internal(); return this->get_entity()->is_internal();
} }
+51 -18
View File
@@ -20,17 +20,22 @@ struct SendDiscoveryConfig {
bool command_topic{true}; ///< If the command topic should be included. Default to true. bool command_topic{true}; ///< If the command topic should be included. Default to true.
}; };
// Max lengths for stack-based topic building (must match mqtt_component.cpp) // Max lengths for stack-based topic building.
// These limits are enforced at Python config validation time in mqtt/__init__.py
// using cv.Length() validators for topic_prefix and discovery_prefix.
// This ensures the stack buffers are always large enough.
static constexpr size_t MQTT_COMPONENT_TYPE_MAX_LEN = 20; static constexpr size_t MQTT_COMPONENT_TYPE_MAX_LEN = 20;
static constexpr size_t MQTT_SUFFIX_MAX_LEN = 32; static constexpr size_t MQTT_SUFFIX_MAX_LEN = 32;
static constexpr size_t MQTT_TOPIC_PREFIX_MAX_LEN = 64; // Validated in Python: cv.Length(max=64)
// Stack buffer size - safe because all inputs are length-validated at config time
// Format: prefix + "/" + type + "/" + object_id + "/" + suffix + null
static constexpr size_t MQTT_DEFAULT_TOPIC_MAX_LEN =
MQTT_TOPIC_PREFIX_MAX_LEN + 1 + MQTT_COMPONENT_TYPE_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 1 + MQTT_SUFFIX_MAX_LEN + 1;
#define LOG_MQTT_COMPONENT(state_topic, command_topic) \ class MQTTComponent; // Forward declaration
if (state_topic) { \ void log_mqtt_component(const char *tag, MQTTComponent *obj, bool state_topic, bool command_topic);
ESP_LOGCONFIG(TAG, " State Topic: '%s'", this->get_state_topic_().c_str()); \
} \ #define LOG_MQTT_COMPONENT(state_topic, command_topic) log_mqtt_component(TAG, this, state_topic, command_topic)
if (command_topic) { \
ESP_LOGCONFIG(TAG, " Command Topic: '%s'", this->get_command_topic_().c_str()); \
}
// Macro to define component_type() with compile-time length verification // Macro to define component_type() with compile-time length verification
// Usage: MQTT_COMPONENT_TYPE(MQTTSensorComponent, "sensor") // Usage: MQTT_COMPONENT_TYPE(MQTTSensorComponent, "sensor")
@@ -74,6 +79,8 @@ static constexpr size_t MQTT_SUFFIX_MAX_LEN = 32;
* a clean separation. * a clean separation.
*/ */
class MQTTComponent : public Component { class MQTTComponent : public Component {
friend void log_mqtt_component(const char *tag, MQTTComponent *obj, bool state_topic, bool command_topic);
public: public:
/// Constructs a MQTTComponent. /// Constructs a MQTTComponent.
explicit MQTTComponent(); explicit MQTTComponent();
@@ -88,7 +95,8 @@ class MQTTComponent : public Component {
virtual bool send_initial_state() = 0; virtual bool send_initial_state() = 0;
virtual bool is_internal(); /// Returns cached is_internal result (computed once during setup).
bool is_internal() const { return this->is_internal_; }
/// Set QOS for state messages. /// Set QOS for state messages.
void set_qos(uint8_t qos); void set_qos(uint8_t qos);
@@ -179,7 +187,16 @@ class MQTTComponent : public Component {
/// Helper method to get the discovery topic for this component. /// Helper method to get the discovery topic for this component.
std::string get_discovery_topic_(const MQTTDiscoveryInfo &discovery_info) const; std::string get_discovery_topic_(const MQTTDiscoveryInfo &discovery_info) const;
/** Get this components state/command/... topic. /** Get this components state/command/... topic into a buffer.
*
* @param buf The buffer to write to (must be exactly MQTT_DEFAULT_TOPIC_MAX_LEN).
* @param suffix The suffix/key such as "state" or "command".
* @return StringRef pointing to the buffer with the topic.
*/
StringRef get_default_topic_for_to_(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf, const char *suffix,
size_t suffix_len) const;
/** Get this components state/command/... topic (allocates std::string).
* *
* @param suffix The suffix/key such as "state" or "command". * @param suffix The suffix/key such as "state" or "command".
* @return The full topic. * @return The full topic.
@@ -200,10 +217,20 @@ class MQTTComponent : public Component {
/// Get whether the underlying Entity is disabled by default /// Get whether the underlying Entity is disabled by default
bool is_disabled_by_default_() const; bool is_disabled_by_default_() const;
/// Get the MQTT topic that new states will be shared to. /// Get the MQTT state topic into a buffer (no heap allocation for non-lambda custom topics).
/// @param buf Buffer of exactly MQTT_DEFAULT_TOPIC_MAX_LEN bytes.
/// @return StringRef pointing to the topic in the buffer.
StringRef get_state_topic_to_(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf) const;
/// Get the MQTT command topic into a buffer (no heap allocation for non-lambda custom topics).
/// @param buf Buffer of exactly MQTT_DEFAULT_TOPIC_MAX_LEN bytes.
/// @return StringRef pointing to the topic in the buffer.
StringRef get_command_topic_to_(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf) const;
/// Get the MQTT topic that new states will be shared to (allocates std::string).
std::string get_state_topic_() const; std::string get_state_topic_() const;
/// Get the MQTT topic for listening to commands. /// Get the MQTT topic for listening to commands (allocates std::string).
std::string get_command_topic_() const; std::string get_command_topic_() const;
bool is_connected_() const; bool is_connected_() const;
@@ -221,12 +248,18 @@ class MQTTComponent : public Component {
std::unique_ptr<Availability> availability_; std::unique_ptr<Availability> availability_;
bool command_retain_{false}; // Packed bitfields - QoS values are 0-2, bools are flags
bool retain_{true}; uint8_t qos_ : 2 {0};
uint8_t qos_{0}; uint8_t subscribe_qos_ : 2 {0};
uint8_t subscribe_qos_{0}; bool command_retain_ : 1 {false};
bool discovery_enabled_{true}; bool retain_ : 1 {true};
bool resend_state_{false}; bool discovery_enabled_ : 1 {true};
bool resend_state_ : 1 {false};
bool is_internal_ : 1 {false}; ///< Cached result of compute_is_internal_(), set during setup
/// Compute is_internal status based on topics and entity state.
/// Called once during setup to cache the result.
bool compute_is_internal_();
}; };
} // namespace esphome::mqtt } // namespace esphome::mqtt
+1 -1
View File
@@ -51,7 +51,7 @@ void MQTTCoverComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT cover '%s':", this->cover_->get_name().c_str()); ESP_LOGCONFIG(TAG, "MQTT cover '%s':", this->cover_->get_name().c_str());
auto traits = this->cover_->get_traits(); auto traits = this->cover_->get_traits();
bool has_command_topic = traits.get_supports_position() || !traits.get_supports_tilt(); bool has_command_topic = traits.get_supports_position() || !traits.get_supports_tilt();
LOG_MQTT_COMPONENT(true, has_command_topic) LOG_MQTT_COMPONENT(true, has_command_topic);
if (traits.get_supports_position()) { if (traits.get_supports_position()) {
ESP_LOGCONFIG(TAG, ESP_LOGCONFIG(TAG,
" Position State Topic: '%s'\n" " Position State Topic: '%s'\n"
+1 -1
View File
@@ -36,7 +36,7 @@ void MQTTDateComponent::setup() {
void MQTTDateComponent::dump_config() { void MQTTDateComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT Date '%s':", this->date_->get_name().c_str()); ESP_LOGCONFIG(TAG, "MQTT Date '%s':", this->date_->get_name().c_str());
LOG_MQTT_COMPONENT(true, true) LOG_MQTT_COMPONENT(true, true);
} }
MQTT_COMPONENT_TYPE(MQTTDateComponent, "date") MQTT_COMPONENT_TYPE(MQTTDateComponent, "date")
+1 -1
View File
@@ -47,7 +47,7 @@ void MQTTDateTimeComponent::setup() {
void MQTTDateTimeComponent::dump_config() { void MQTTDateTimeComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT DateTime '%s':", this->datetime_->get_name().c_str()); ESP_LOGCONFIG(TAG, "MQTT DateTime '%s':", this->datetime_->get_name().c_str());
LOG_MQTT_COMPONENT(true, true) LOG_MQTT_COMPONENT(true, true);
} }
MQTT_COMPONENT_TYPE(MQTTDateTimeComponent, "datetime") MQTT_COMPONENT_TYPE(MQTTDateTimeComponent, "datetime")
+1 -1
View File
@@ -90,7 +90,7 @@ void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscovery
bool MQTTJSONLightComponent::send_initial_state() { return this->publish_state_(); } bool MQTTJSONLightComponent::send_initial_state() { return this->publish_state_(); }
void MQTTJSONLightComponent::dump_config() { void MQTTJSONLightComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT Light '%s':", this->state_->get_name().c_str()); ESP_LOGCONFIG(TAG, "MQTT Light '%s':", this->state_->get_name().c_str());
LOG_MQTT_COMPONENT(true, true) LOG_MQTT_COMPONENT(true, true);
} }
} // namespace esphome::mqtt } // namespace esphome::mqtt
+1 -1
View File
@@ -30,7 +30,7 @@ void MQTTNumberComponent::setup() {
void MQTTNumberComponent::dump_config() { void MQTTNumberComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT Number '%s':", this->number_->get_name().c_str()); ESP_LOGCONFIG(TAG, "MQTT Number '%s':", this->number_->get_name().c_str());
LOG_MQTT_COMPONENT(true, false) LOG_MQTT_COMPONENT(true, false);
} }
MQTT_COMPONENT_TYPE(MQTTNumberComponent, "number") MQTT_COMPONENT_TYPE(MQTTNumberComponent, "number")
+1 -1
View File
@@ -25,7 +25,7 @@ void MQTTSelectComponent::setup() {
void MQTTSelectComponent::dump_config() { void MQTTSelectComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT Select '%s':", this->select_->get_name().c_str()); ESP_LOGCONFIG(TAG, "MQTT Select '%s':", this->select_->get_name().c_str());
LOG_MQTT_COMPONENT(true, false) LOG_MQTT_COMPONENT(true, false);
} }
MQTT_COMPONENT_TYPE(MQTTSelectComponent, "select") MQTT_COMPONENT_TYPE(MQTTSelectComponent, "select")
+1 -1
View File
@@ -28,7 +28,7 @@ void MQTTSensorComponent::dump_config() {
if (this->get_expire_after() > 0) { if (this->get_expire_after() > 0) {
ESP_LOGCONFIG(TAG, " Expire After: %" PRIu32 "s", this->get_expire_after() / 1000); ESP_LOGCONFIG(TAG, " Expire After: %" PRIu32 "s", this->get_expire_after() / 1000);
} }
LOG_MQTT_COMPONENT(true, false) LOG_MQTT_COMPONENT(true, false);
} }
MQTT_COMPONENT_TYPE(MQTTSensorComponent, "sensor") MQTT_COMPONENT_TYPE(MQTTSensorComponent, "sensor")
+1 -1
View File
@@ -26,7 +26,7 @@ void MQTTTextComponent::setup() {
void MQTTTextComponent::dump_config() { void MQTTTextComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT text '%s':", this->text_->get_name().c_str()); ESP_LOGCONFIG(TAG, "MQTT text '%s':", this->text_->get_name().c_str());
LOG_MQTT_COMPONENT(true, true) LOG_MQTT_COMPONENT(true, true);
} }
MQTT_COMPONENT_TYPE(MQTTTextComponent, "text") MQTT_COMPONENT_TYPE(MQTTTextComponent, "text")
+1 -1
View File
@@ -36,7 +36,7 @@ void MQTTTimeComponent::setup() {
void MQTTTimeComponent::dump_config() { void MQTTTimeComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT Time '%s':", this->time_->get_name().c_str()); ESP_LOGCONFIG(TAG, "MQTT Time '%s':", this->time_->get_name().c_str());
LOG_MQTT_COMPONENT(true, true) LOG_MQTT_COMPONENT(true, true);
} }
MQTT_COMPONENT_TYPE(MQTTTimeComponent, "time") MQTT_COMPONENT_TYPE(MQTTTimeComponent, "time")
+1 -1
View File
@@ -39,7 +39,7 @@ void MQTTValveComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT valve '%s':", this->valve_->get_name().c_str()); ESP_LOGCONFIG(TAG, "MQTT valve '%s':", this->valve_->get_name().c_str());
auto traits = this->valve_->get_traits(); auto traits = this->valve_->get_traits();
bool has_command_topic = traits.get_supports_position(); bool has_command_topic = traits.get_supports_position();
LOG_MQTT_COMPONENT(true, has_command_topic) LOG_MQTT_COMPONENT(true, has_command_topic);
if (traits.get_supports_position()) { if (traits.get_supports_position()) {
ESP_LOGCONFIG(TAG, ESP_LOGCONFIG(TAG,
" Position State Topic: '%s'\n" " Position State Topic: '%s'\n"
+44 -3
View File
@@ -4,6 +4,7 @@
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/preferences.h" #include "esphome/core/preferences.h"
#include "esphome/core/string_ref.h"
#include <concepts> #include <concepts>
#include <functional> #include <functional>
#include <utility> #include <utility>
@@ -190,15 +191,55 @@ template<typename T, typename... X> class TemplatableValue {
/// Get the static string pointer (only valid if is_static_string() returns true) /// Get the static string pointer (only valid if is_static_string() returns true)
const char *get_static_string() const { return this->static_str_; } const char *get_static_string() const { return this->static_str_; }
protected: /// Check if the string value is empty without allocating (for std::string specialization).
enum : uint8_t { /// For NONE, returns true. For STATIC_STRING/VALUE, checks without allocation.
/// For LAMBDA/STATELESS_LAMBDA, must call value() which may allocate.
bool is_empty() const requires std::same_as<T, std::string> {
switch (this->type_) {
case NONE:
return true;
case STATIC_STRING:
return this->static_str_ == nullptr || this->static_str_[0] == '\0';
case VALUE:
return this->value_->empty();
default: // LAMBDA/STATELESS_LAMBDA - must call value()
return this->value().empty();
}
}
/// Get a StringRef to the string value without heap allocation when possible.
/// For STATIC_STRING/VALUE, returns reference to existing data (no allocation).
/// For LAMBDA/STATELESS_LAMBDA, calls value(), copies to provided buffer, returns ref to buffer.
/// @param lambda_buf Buffer used only for lambda case (must remain valid while StringRef is used).
/// @param lambda_buf_size Size of the buffer.
/// @return StringRef pointing to the string data.
StringRef ref_or_copy_to(char *lambda_buf, size_t lambda_buf_size) const requires std::same_as<T, std::string> {
switch (this->type_) {
case NONE:
return StringRef();
case STATIC_STRING:
if (this->static_str_ == nullptr)
return StringRef();
return StringRef(this->static_str_, strlen(this->static_str_));
case VALUE:
return StringRef(this->value_->data(), this->value_->size());
default: { // LAMBDA/STATELESS_LAMBDA - must call value() and copy
std::string result = this->value();
size_t copy_len = std::min(result.size(), lambda_buf_size - 1);
memcpy(lambda_buf, result.data(), copy_len);
lambda_buf[copy_len] = '\0';
return StringRef(lambda_buf, copy_len);
}
}
}
protected : enum : uint8_t {
NONE, NONE,
VALUE, VALUE,
LAMBDA, LAMBDA,
STATELESS_LAMBDA, STATELESS_LAMBDA,
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 std::string, use heap pointer to minimize union size (4 bytes vs 12+).
// For other types, store value inline as before. // For other types, store value inline as before.
using ValueStorage = std::conditional_t<USE_HEAP_STORAGE, T *, T>; using ValueStorage = std::conditional_t<USE_HEAP_STORAGE, T *, T>;