From e301b8d0e0af68502adb73952c79abae36b20522 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Wed, 7 Jan 2026 21:44:10 -0600 Subject: [PATCH 01/11] [thermostat] Allow `heat_cool_mode` without an automation (#13069) Co-authored-by: J. Nick Koston --- esphome/components/thermostat/climate.py | 25 ++++++++++++++++++------ tests/components/thermostat/common.yaml | 1 + 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/esphome/components/thermostat/climate.py b/esphome/components/thermostat/climate.py index a3c155aac0..f7c1298d68 100644 --- a/esphome/components/thermostat/climate.py +++ b/esphome/components/thermostat/climate.py @@ -153,6 +153,19 @@ def generate_comparable_preset(config, name): return comparable_preset +def validate_heat_cool_mode(value) -> list: + """Validate heat_cool_mode - accepts either True or an automation.""" + if value is True: + # Convert True to empty automation list + return [] + if value is False: + raise cv.Invalid( + "heat_cool_mode cannot be 'false'. Specify 'true' to enable the mode or provide an automation" + ) + # Otherwise validate as automation + return automation.validate_automation(single=True)(value) + + def validate_thermostat(config): # verify corresponding action(s) exist(s) for any defined climate mode or action requirements = { @@ -554,9 +567,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_FAN_ONLY_MODE): automation.validate_automation( single=True ), - cv.Optional(CONF_HEAT_COOL_MODE): automation.validate_automation( - single=True - ), + cv.Optional(CONF_HEAT_COOL_MODE): validate_heat_cool_mode, cv.Optional(CONF_HEAT_MODE): automation.validate_automation(single=True), cv.Optional(CONF_OFF_MODE): automation.validate_automation(single=True), cv.Optional(CONF_FAN_MODE_ON_ACTION): automation.validate_automation( @@ -828,9 +839,11 @@ async def to_code(config): ) cg.add(var.set_supports_heat(True)) if CONF_HEAT_COOL_MODE in config: - await automation.build_automation( - var.get_heat_cool_mode_trigger(), [], config[CONF_HEAT_COOL_MODE] - ) + # Build automation only if user provided actions (not just `true`) + if config[CONF_HEAT_COOL_MODE]: + await automation.build_automation( + var.get_heat_cool_mode_trigger(), [], config[CONF_HEAT_COOL_MODE] + ) cg.add(var.set_supports_heat_cool(True)) if CONF_OFF_MODE in config: await automation.build_automation( diff --git a/tests/components/thermostat/common.yaml b/tests/components/thermostat/common.yaml index 4aa87c0ac3..63bd174e14 100644 --- a/tests/components/thermostat/common.yaml +++ b/tests/components/thermostat/common.yaml @@ -41,6 +41,7 @@ climate: - logger.log: dry_mode fan_only_mode: - logger.log: fan_only_mode + heat_cool_mode: true fan_mode_auto_action: - logger.log: fan_mode_auto_action fan_mode_on_action: From b5b78a674e38935525bbe061396f25f0995be753 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 7 Jan 2026 18:42:58 -1000 Subject: [PATCH 02/11] [mqtt] Reduce heap allocations in topic string building --- esphome/components/mqtt/__init__.py | 17 +++- .../mqtt/mqtt_alarm_control_panel.cpp | 2 +- .../mqtt/mqtt_alarm_control_panel.h | 2 +- .../components/mqtt/mqtt_binary_sensor.cpp | 2 +- esphome/components/mqtt/mqtt_binary_sensor.h | 2 +- esphome/components/mqtt/mqtt_button.cpp | 2 +- esphome/components/mqtt/mqtt_button.h | 2 +- esphome/components/mqtt/mqtt_climate.cpp | 2 +- esphome/components/mqtt/mqtt_climate.h | 2 +- esphome/components/mqtt/mqtt_component.cpp | 79 ++++++++++++++++--- esphome/components/mqtt/mqtt_component.h | 21 ++++- esphome/components/mqtt/mqtt_cover.cpp | 2 +- esphome/components/mqtt/mqtt_cover.h | 2 +- esphome/components/mqtt/mqtt_date.cpp | 2 +- esphome/components/mqtt/mqtt_date.h | 2 +- esphome/components/mqtt/mqtt_datetime.cpp | 2 +- esphome/components/mqtt/mqtt_datetime.h | 2 +- esphome/components/mqtt/mqtt_event.cpp | 2 +- esphome/components/mqtt/mqtt_event.h | 2 +- esphome/components/mqtt/mqtt_fan.cpp | 2 +- esphome/components/mqtt/mqtt_fan.h | 2 +- esphome/components/mqtt/mqtt_light.cpp | 2 +- esphome/components/mqtt/mqtt_light.h | 2 +- esphome/components/mqtt/mqtt_lock.cpp | 2 +- esphome/components/mqtt/mqtt_lock.h | 2 +- esphome/components/mqtt/mqtt_number.cpp | 2 +- esphome/components/mqtt/mqtt_number.h | 2 +- esphome/components/mqtt/mqtt_select.cpp | 2 +- esphome/components/mqtt/mqtt_select.h | 2 +- esphome/components/mqtt/mqtt_sensor.cpp | 2 +- esphome/components/mqtt/mqtt_sensor.h | 2 +- esphome/components/mqtt/mqtt_switch.cpp | 2 +- esphome/components/mqtt/mqtt_switch.h | 2 +- esphome/components/mqtt/mqtt_text.cpp | 2 +- esphome/components/mqtt/mqtt_text.h | 2 +- esphome/components/mqtt/mqtt_text_sensor.cpp | 2 +- esphome/components/mqtt/mqtt_text_sensor.h | 2 +- esphome/components/mqtt/mqtt_time.cpp | 2 +- esphome/components/mqtt/mqtt_time.h | 2 +- esphome/components/mqtt/mqtt_update.cpp | 2 +- esphome/components/mqtt/mqtt_update.h | 2 +- esphome/components/mqtt/mqtt_valve.cpp | 2 +- esphome/components/mqtt/mqtt_valve.h | 2 +- 43 files changed, 141 insertions(+), 56 deletions(-) diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index e73de49fef..f01c928b30 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -77,6 +77,13 @@ CONF_DISCOVER_IP = "discover_ip" CONF_IDF_SEND_ASYNC = "idf_send_async" CONF_WAIT_FOR_CONNECTION = "wait_for_connection" +# Max lengths for stack-based topic building. +# These values are used in cv.Length() validators below to ensure the C++ code +# in mqtt_component.cpp can safely use fixed-size stack buffers without overflow. +# If you change these, update the corresponding constants in mqtt_component.cpp. +TOPIC_PREFIX_MAX_LEN = 64 # Default is device name, typically short +DISCOVERY_PREFIX_MAX_LEN = 64 # Default is "homeassistant" (13 chars) + def validate_message_just_topic(value): value = cv.publish_topic(value) @@ -253,9 +260,9 @@ CONFIG_SCHEMA = cv.All( ), cv.Optional(CONF_DISCOVERY_RETAIN, default=True): cv.boolean, cv.Optional(CONF_DISCOVER_IP, default=True): cv.boolean, - cv.Optional( - CONF_DISCOVERY_PREFIX, default="homeassistant" - ): cv.publish_topic, + cv.Optional(CONF_DISCOVERY_PREFIX, default="homeassistant"): cv.All( + cv.publish_topic, cv.Length(max=DISCOVERY_PREFIX_MAX_LEN) + ), cv.Optional(CONF_DISCOVERY_UNIQUE_ID_GENERATOR, default="legacy"): cv.enum( MQTT_DISCOVERY_UNIQUE_ID_GENERATOR_OPTIONS ), @@ -266,7 +273,9 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_BIRTH_MESSAGE): MQTT_MESSAGE_SCHEMA, cv.Optional(CONF_WILL_MESSAGE): MQTT_MESSAGE_SCHEMA, cv.Optional(CONF_SHUTDOWN_MESSAGE): MQTT_MESSAGE_SCHEMA, - cv.Optional(CONF_TOPIC_PREFIX, default=lambda: CORE.name): cv.publish_topic, + cv.Optional(CONF_TOPIC_PREFIX, default=lambda: CORE.name): cv.All( + cv.publish_topic, cv.Length(max=TOPIC_PREFIX_MAX_LEN) + ), cv.Optional(CONF_LOG_TOPIC): cv.Any( None, MQTT_MESSAGE_BASE.extend( diff --git a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp index eb46c3b10c..6245d10882 100644 --- a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp +++ b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp @@ -79,7 +79,7 @@ void MQTTAlarmControlPanelComponent::send_discovery(JsonObject root, mqtt::SendD root[MQTT_CODE_ARM_REQUIRED] = this->alarm_control_panel_->get_requires_code_to_arm(); } -std::string MQTTAlarmControlPanelComponent::component_type() const { return "alarm_control_panel"; } +MQTT_COMPONENT_TYPE(MQTTAlarmControlPanelComponent, "alarm_control_panel") const EntityBase *MQTTAlarmControlPanelComponent::get_entity() const { return this->alarm_control_panel_; } bool MQTTAlarmControlPanelComponent::send_initial_state() { return this->publish_state(); } diff --git a/esphome/components/mqtt/mqtt_alarm_control_panel.h b/esphome/components/mqtt/mqtt_alarm_control_panel.h index cf4fac1511..89a0ff1be8 100644 --- a/esphome/components/mqtt/mqtt_alarm_control_panel.h +++ b/esphome/components/mqtt/mqtt_alarm_control_panel.h @@ -25,7 +25,7 @@ class MQTTAlarmControlPanelComponent : public mqtt::MQTTComponent { void dump_config() override; protected: - std::string component_type() const override; + const char *component_type() const override; const EntityBase *get_entity() const override; alarm_control_panel::AlarmControlPanel *alarm_control_panel_; diff --git a/esphome/components/mqtt/mqtt_binary_sensor.cpp b/esphome/components/mqtt/mqtt_binary_sensor.cpp index 146ca46f68..a37043406b 100644 --- a/esphome/components/mqtt/mqtt_binary_sensor.cpp +++ b/esphome/components/mqtt/mqtt_binary_sensor.cpp @@ -10,7 +10,7 @@ namespace esphome::mqtt { static const char *const TAG = "mqtt.binary_sensor"; -std::string MQTTBinarySensorComponent::component_type() const { return "binary_sensor"; } +MQTT_COMPONENT_TYPE(MQTTBinarySensorComponent, "binary_sensor") const EntityBase *MQTTBinarySensorComponent::get_entity() const { return this->binary_sensor_; } void MQTTBinarySensorComponent::setup() { diff --git a/esphome/components/mqtt/mqtt_binary_sensor.h b/esphome/components/mqtt/mqtt_binary_sensor.h index 82176ec97b..5917a9966c 100644 --- a/esphome/components/mqtt/mqtt_binary_sensor.h +++ b/esphome/components/mqtt/mqtt_binary_sensor.h @@ -29,7 +29,7 @@ class MQTTBinarySensorComponent : public mqtt::MQTTComponent { bool publish_state(bool state); protected: - std::string component_type() const override; + const char *component_type() const override; const EntityBase *get_entity() const override; binary_sensor::BinarySensor *binary_sensor_; diff --git a/esphome/components/mqtt/mqtt_button.cpp b/esphome/components/mqtt/mqtt_button.cpp index 2b700a4962..718fe93016 100644 --- a/esphome/components/mqtt/mqtt_button.cpp +++ b/esphome/components/mqtt/mqtt_button.cpp @@ -39,7 +39,7 @@ void MQTTButtonComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) } -std::string MQTTButtonComponent::component_type() const { return "button"; } +MQTT_COMPONENT_TYPE(MQTTButtonComponent, "button") const EntityBase *MQTTButtonComponent::get_entity() const { return this->button_; } } // namespace esphome::mqtt diff --git a/esphome/components/mqtt/mqtt_button.h b/esphome/components/mqtt/mqtt_button.h index ec802664df..a2db64d39d 100644 --- a/esphome/components/mqtt/mqtt_button.h +++ b/esphome/components/mqtt/mqtt_button.h @@ -26,7 +26,7 @@ class MQTTButtonComponent : public mqtt::MQTTComponent { protected: /// "button" component type. - std::string component_type() const override; + const char *component_type() const override; const EntityBase *get_entity() const override; button::Button *button_; diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index d402fff6e6..77aabb2461 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -254,7 +254,7 @@ void MQTTClimateComponent::setup() { } MQTTClimateComponent::MQTTClimateComponent(Climate *device) : device_(device) {} bool MQTTClimateComponent::send_initial_state() { return this->publish_state_(); } -std::string MQTTClimateComponent::component_type() const { return "climate"; } +MQTT_COMPONENT_TYPE(MQTTClimateComponent, "climate") const EntityBase *MQTTClimateComponent::get_entity() const { return this->device_; } bool MQTTClimateComponent::publish_state_() { diff --git a/esphome/components/mqtt/mqtt_climate.h b/esphome/components/mqtt/mqtt_climate.h index f561627ac9..f0715929d4 100644 --- a/esphome/components/mqtt/mqtt_climate.h +++ b/esphome/components/mqtt/mqtt_climate.h @@ -15,7 +15,7 @@ class MQTTClimateComponent : public mqtt::MQTTComponent { MQTTClimateComponent(climate::Climate *device); void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override; bool send_initial_state() override; - std::string component_type() const override; + const char *component_type() const override; void setup() override; MQTT_COMPONENT_CUSTOM_TOPIC(current_temperature, state) diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index ccbdb2ea91..bc209a7d11 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -13,6 +13,36 @@ namespace esphome::mqtt { static const char *const TAG = "mqtt.component"; +// Helper functions for building topic strings on stack +inline char *append_str(char *p, const char *s, size_t len) { + memcpy(p, s, len); + return p + len; +} + +inline char *append_char(char *p, char c) { + *p = c; + return p + 1; +} + +// 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. +// OBJECT_ID_MAX_LEN is defined in entity_base.h and derived from friendly_name limits. +// 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 COMPONENT_TYPE_MAX_LEN = 20; // Longest: "alarm_control_panel" = 19 +static constexpr size_t SUFFIX_MAX_LEN = 32; // Longest: "target_temperature_high/command" = 28 +static constexpr size_t SANITIZED_NAME_MAX_LEN = 64; // Same as topic_prefix (device name) +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 + COMPONENT_TYPE_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 1 + SUFFIX_MAX_LEN + 1; +// Format: prefix + "/" + type + "/" + name + "/" + object_id + "/config" + null +static constexpr size_t DISCOVERY_TOPIC_MAX_LEN = + DISCOVERY_PREFIX_MAX_LEN + 1 + COMPONENT_TYPE_MAX_LEN + 1 + SANITIZED_NAME_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 7 + 1; + void MQTTComponent::set_qos(uint8_t qos) { this->qos_ = qos; } void MQTTComponent::set_subscribe_qos(uint8_t qos) { this->subscribe_qos_ = qos; } @@ -21,8 +51,23 @@ void MQTTComponent::set_retain(bool retain) { this->retain_ = retain; } std::string MQTTComponent::get_discovery_topic_(const MQTTDiscoveryInfo &discovery_info) const { std::string sanitized_name = str_sanitize(App.get_name()); - return discovery_info.prefix + "/" + this->component_type() + "/" + sanitized_name + "/" + - this->get_default_object_id_() + "/config"; + const char *comp_type = this->component_type(); + char object_id_buf[OBJECT_ID_MAX_LEN]; + StringRef object_id = this->get_default_object_id_to_(object_id_buf); + + char buf[DISCOVERY_TOPIC_MAX_LEN]; + char *p = buf; + + p = append_str(p, discovery_info.prefix.data(), discovery_info.prefix.size()); + p = append_char(p, '/'); + p = append_str(p, comp_type, strlen(comp_type)); + p = append_char(p, '/'); + p = append_str(p, sanitized_name.data(), sanitized_name.size()); + p = append_char(p, '/'); + p = append_str(p, object_id.c_str(), object_id.size()); + p = append_str(p, "/config", 7); + + return std::string(buf, p - buf); } std::string MQTTComponent::get_default_topic_for_(const std::string &suffix) const { @@ -32,7 +77,22 @@ std::string MQTTComponent::get_default_topic_for_(const std::string &suffix) con return ""; } - return topic_prefix + "/" + this->component_type() + "/" + this->get_default_object_id_() + "/" + suffix; + const char *comp_type = this->component_type(); + char object_id_buf[OBJECT_ID_MAX_LEN]; + StringRef object_id = this->get_default_object_id_to_(object_id_buf); + + char buf[DEFAULT_TOPIC_MAX_LEN]; + char *p = buf; + + p = append_str(p, topic_prefix.data(), topic_prefix.size()); + p = append_char(p, '/'); + p = append_str(p, comp_type, strlen(comp_type)); + p = append_char(p, '/'); + p = append_str(p, object_id.c_str(), object_id.size()); + p = append_char(p, '/'); + p = append_str(p, suffix.data(), suffix.size()); + + return std::string(buf, p - buf); } std::string MQTTComponent::get_state_topic_() const { @@ -123,6 +183,8 @@ bool MQTTComponent::send_discovery_() { } const MQTTDiscoveryInfo &discovery_info = global_mqtt_client->get_discovery_info(); + char object_id_buf[OBJECT_ID_MAX_LEN]; + StringRef object_id = this->get_default_object_id_to_(object_id_buf); if (discovery_info.unique_id_generator == MQTT_MAC_ADDRESS_UNIQUE_ID_GENERATOR) { char friendly_name_hash[9]; sprintf(friendly_name_hash, "%08" PRIx32, fnv1_hash(this->friendly_name_())); @@ -131,12 +193,12 @@ bool MQTTComponent::send_discovery_() { } else { // default to almost-unique ID. It's a hack but the only way to get that // gorgeous device registry view. - root[MQTT_UNIQUE_ID] = "ESP" + this->component_type() + this->get_default_object_id_(); + root[MQTT_UNIQUE_ID] = "ESP" + std::string(this->component_type()) + object_id.c_str(); } const std::string &node_name = App.get_name(); if (discovery_info.object_id_generator == MQTT_DEVICE_NAME_OBJECT_ID_GENERATOR) - root[MQTT_OBJECT_ID] = node_name + "_" + this->get_default_object_id_(); + root[MQTT_OBJECT_ID] = node_name + "_" + object_id.c_str(); const std::string &friendly_name_ref = App.get_friendly_name(); const std::string &node_friendly_name = friendly_name_ref.empty() ? node_name : friendly_name_ref; @@ -194,10 +256,6 @@ bool MQTTComponent::is_discovery_enabled() const { return this->discovery_enabled_ && global_mqtt_client->is_discovery_enabled(); } -std::string MQTTComponent::get_default_object_id_() const { - return str_sanitize(str_snake_case(this->friendly_name_())); -} - void MQTTComponent::subscribe(const std::string &topic, mqtt_callback_t callback, uint8_t qos) { global_mqtt_client->subscribe(topic, std::move(callback), qos); } @@ -280,6 +338,9 @@ bool MQTTComponent::is_connected_() const { return global_mqtt_client->is_connec // Pull these properties from EntityBase if not overridden std::string MQTTComponent::friendly_name_() const { return this->get_entity()->get_name(); } +StringRef MQTTComponent::get_default_object_id_to_(std::span buf) const { + return this->get_entity()->get_object_id_to(buf); +} 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_internal() { diff --git a/esphome/components/mqtt/mqtt_component.h b/esphome/components/mqtt/mqtt_component.h index e5f9664f77..e0b751f05f 100644 --- a/esphome/components/mqtt/mqtt_component.h +++ b/esphome/components/mqtt/mqtt_component.h @@ -19,6 +19,10 @@ struct SendDiscoveryConfig { 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) +static constexpr size_t MQTT_COMPONENT_TYPE_MAX_LEN = 20; +static constexpr size_t MQTT_SUFFIX_MAX_LEN = 32; + #define LOG_MQTT_COMPONENT(state_topic, command_topic) \ if (state_topic) { \ ESP_LOGCONFIG(TAG, " State Topic: '%s'", this->get_state_topic_().c_str()); \ @@ -27,7 +31,18 @@ struct SendDiscoveryConfig { ESP_LOGCONFIG(TAG, " Command Topic: '%s'", this->get_command_topic_().c_str()); \ } +// Macro to define component_type() with compile-time length verification +// Usage: MQTT_COMPONENT_TYPE(MQTTSensorComponent, "sensor") +#define MQTT_COMPONENT_TYPE(class_name, type_str) \ + const char *class_name::component_type() const { return type_str; } \ + static_assert(sizeof(type_str) - 1 <= MQTT_COMPONENT_TYPE_MAX_LEN, \ + #class_name "::component_type() exceeds MQTT_COMPONENT_TYPE_MAX_LEN"); + +// Macro to define custom topic getter/setter with compile-time suffix length verification #define MQTT_COMPONENT_CUSTOM_TOPIC_(name, type) \ + static_assert(sizeof(#name "/" #type) - 1 <= MQTT_SUFFIX_MAX_LEN, \ + "topic suffix " #name "/" #type " exceeds MQTT_SUFFIX_MAX_LEN"); \ +\ protected: \ std::string custom_##name##_##type##_topic_{}; \ \ @@ -92,7 +107,7 @@ class MQTTComponent : public Component { void set_subscribe_qos(uint8_t qos); /// Override this method to return the component type (e.g. "light", "sensor", ...) - virtual std::string component_type() const = 0; + 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); @@ -185,8 +200,8 @@ class MQTTComponent : public Component { // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) - /// Generate the Home Assistant MQTT discovery object id by automatically transforming the friendly name. - std::string get_default_object_id_() const; + /// 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_{}; diff --git a/esphome/components/mqtt/mqtt_cover.cpp b/esphome/components/mqtt/mqtt_cover.cpp index e628ac37a9..4505027485 100644 --- a/esphome/components/mqtt/mqtt_cover.cpp +++ b/esphome/components/mqtt/mqtt_cover.cpp @@ -90,7 +90,7 @@ void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConf } } -std::string MQTTCoverComponent::component_type() const { return "cover"; } +MQTT_COMPONENT_TYPE(MQTTCoverComponent, "cover") const EntityBase *MQTTCoverComponent::get_entity() const { return this->cover_; } bool MQTTCoverComponent::send_initial_state() { return this->publish_state(); } diff --git a/esphome/components/mqtt/mqtt_cover.h b/esphome/components/mqtt/mqtt_cover.h index 6b874af16a..13582d14d1 100644 --- a/esphome/components/mqtt/mqtt_cover.h +++ b/esphome/components/mqtt/mqtt_cover.h @@ -29,7 +29,7 @@ class MQTTCoverComponent : public mqtt::MQTTComponent { void dump_config() override; protected: - std::string component_type() const override; + const char *component_type() const override; const EntityBase *get_entity() const override; cover::Cover *cover_; diff --git a/esphome/components/mqtt/mqtt_date.cpp b/esphome/components/mqtt/mqtt_date.cpp index 1715384c5f..dba7c1a671 100644 --- a/esphome/components/mqtt/mqtt_date.cpp +++ b/esphome/components/mqtt/mqtt_date.cpp @@ -39,7 +39,7 @@ void MQTTDateComponent::dump_config() { LOG_MQTT_COMPONENT(true, true) } -std::string MQTTDateComponent::component_type() const { return "date"; } +MQTT_COMPONENT_TYPE(MQTTDateComponent, "date") const EntityBase *MQTTDateComponent::get_entity() const { return this->date_; } void MQTTDateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { diff --git a/esphome/components/mqtt/mqtt_date.h b/esphome/components/mqtt/mqtt_date.h index 380bb69e0e..4a626becb2 100644 --- a/esphome/components/mqtt/mqtt_date.h +++ b/esphome/components/mqtt/mqtt_date.h @@ -31,7 +31,7 @@ class MQTTDateComponent : public mqtt::MQTTComponent { bool publish_state(uint16_t year, uint8_t month, uint8_t day); protected: - std::string component_type() const override; + const char *component_type() const override; const EntityBase *get_entity() const override; datetime::DateEntity *date_; diff --git a/esphome/components/mqtt/mqtt_datetime.cpp b/esphome/components/mqtt/mqtt_datetime.cpp index 79a2c82180..5f1cf19b97 100644 --- a/esphome/components/mqtt/mqtt_datetime.cpp +++ b/esphome/components/mqtt/mqtt_datetime.cpp @@ -50,7 +50,7 @@ void MQTTDateTimeComponent::dump_config() { LOG_MQTT_COMPONENT(true, true) } -std::string MQTTDateTimeComponent::component_type() const { return "datetime"; } +MQTT_COMPONENT_TYPE(MQTTDateTimeComponent, "datetime") const EntityBase *MQTTDateTimeComponent::get_entity() const { return this->datetime_; } void MQTTDateTimeComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { diff --git a/esphome/components/mqtt/mqtt_datetime.h b/esphome/components/mqtt/mqtt_datetime.h index 8706bfcf75..d02d6f579c 100644 --- a/esphome/components/mqtt/mqtt_datetime.h +++ b/esphome/components/mqtt/mqtt_datetime.h @@ -31,7 +31,7 @@ class MQTTDateTimeComponent : public mqtt::MQTTComponent { bool publish_state(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, uint8_t second); protected: - std::string component_type() const override; + const char *component_type() const override; const EntityBase *get_entity() const override; datetime::DateTimeEntity *datetime_; diff --git a/esphome/components/mqtt/mqtt_event.cpp b/esphome/components/mqtt/mqtt_event.cpp index 67a7aab5bd..42fbc1eabd 100644 --- a/esphome/components/mqtt/mqtt_event.cpp +++ b/esphome/components/mqtt/mqtt_event.cpp @@ -50,7 +50,7 @@ bool MQTTEventComponent::publish_event_(const std::string &event_type) { }); } -std::string MQTTEventComponent::component_type() const { return "event"; } +MQTT_COMPONENT_TYPE(MQTTEventComponent, "event") const EntityBase *MQTTEventComponent::get_entity() const { return this->event_; } } // namespace esphome::mqtt diff --git a/esphome/components/mqtt/mqtt_event.h b/esphome/components/mqtt/mqtt_event.h index fc6e778d44..e6d5b6f278 100644 --- a/esphome/components/mqtt/mqtt_event.h +++ b/esphome/components/mqtt/mqtt_event.h @@ -25,7 +25,7 @@ class MQTTEventComponent : public mqtt::MQTTComponent { protected: bool publish_event_(const std::string &event_type); - std::string component_type() const override; + const char *component_type() const override; const EntityBase *get_entity() const override; event::Event *event_; diff --git a/esphome/components/mqtt/mqtt_fan.cpp b/esphome/components/mqtt/mqtt_fan.cpp index ffecd9c663..bd6c98b679 100644 --- a/esphome/components/mqtt/mqtt_fan.cpp +++ b/esphome/components/mqtt/mqtt_fan.cpp @@ -15,7 +15,7 @@ using namespace esphome::fan; MQTTFanComponent::MQTTFanComponent(Fan *state) : state_(state) {} Fan *MQTTFanComponent::get_state() const { return this->state_; } -std::string MQTTFanComponent::component_type() const { return "fan"; } +MQTT_COMPONENT_TYPE(MQTTFanComponent, "fan") const EntityBase *MQTTFanComponent::get_entity() const { return this->state_; } void MQTTFanComponent::setup() { diff --git a/esphome/components/mqtt/mqtt_fan.h b/esphome/components/mqtt/mqtt_fan.h index 16ce246853..43ef67e733 100644 --- a/esphome/components/mqtt/mqtt_fan.h +++ b/esphome/components/mqtt/mqtt_fan.h @@ -36,7 +36,7 @@ class MQTTFanComponent : public mqtt::MQTTComponent { bool send_initial_state() override; bool publish_state(); /// 'fan' component type for discovery. - std::string component_type() const override; + const char *component_type() const override; fan::Fan *get_state() const; diff --git a/esphome/components/mqtt/mqtt_light.cpp b/esphome/components/mqtt/mqtt_light.cpp index 0dafe487ff..2d588ed10b 100644 --- a/esphome/components/mqtt/mqtt_light.cpp +++ b/esphome/components/mqtt/mqtt_light.cpp @@ -14,7 +14,7 @@ static const char *const TAG = "mqtt.light"; using namespace esphome::light; -std::string MQTTJSONLightComponent::component_type() const { return "light"; } +MQTT_COMPONENT_TYPE(MQTTJSONLightComponent, "light") const EntityBase *MQTTJSONLightComponent::get_entity() const { return this->state_; } void MQTTJSONLightComponent::setup() { diff --git a/esphome/components/mqtt/mqtt_light.h b/esphome/components/mqtt/mqtt_light.h index 2cc631c901..41981655ef 100644 --- a/esphome/components/mqtt/mqtt_light.h +++ b/esphome/components/mqtt/mqtt_light.h @@ -28,7 +28,7 @@ class MQTTJSONLightComponent : public mqtt::MQTTComponent, public light::LightRe void on_light_remote_values_update() override; protected: - std::string component_type() const override; + const char *component_type() const override; const EntityBase *get_entity() const override; bool publish_state_(); diff --git a/esphome/components/mqtt/mqtt_lock.cpp b/esphome/components/mqtt/mqtt_lock.cpp index 58fa675eb7..43ef60bdf4 100644 --- a/esphome/components/mqtt/mqtt_lock.cpp +++ b/esphome/components/mqtt/mqtt_lock.cpp @@ -34,7 +34,7 @@ void MQTTLockComponent::dump_config() { LOG_MQTT_COMPONENT(true, true); } -std::string MQTTLockComponent::component_type() const { return "lock"; } +MQTT_COMPONENT_TYPE(MQTTLockComponent, "lock") const EntityBase *MQTTLockComponent::get_entity() const { return this->lock_; } void MQTTLockComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson diff --git a/esphome/components/mqtt/mqtt_lock.h b/esphome/components/mqtt/mqtt_lock.h index 6fb4998b25..666882c73d 100644 --- a/esphome/components/mqtt/mqtt_lock.h +++ b/esphome/components/mqtt/mqtt_lock.h @@ -27,7 +27,7 @@ class MQTTLockComponent : public mqtt::MQTTComponent { protected: /// "lock" component type. - std::string component_type() const override; + const char *component_type() const override; const EntityBase *get_entity() const override; lock::Lock *lock_; diff --git a/esphome/components/mqtt/mqtt_number.cpp b/esphome/components/mqtt/mqtt_number.cpp index 381574ae56..8342210ee4 100644 --- a/esphome/components/mqtt/mqtt_number.cpp +++ b/esphome/components/mqtt/mqtt_number.cpp @@ -33,7 +33,7 @@ void MQTTNumberComponent::dump_config() { LOG_MQTT_COMPONENT(true, false) } -std::string MQTTNumberComponent::component_type() const { return "number"; } +MQTT_COMPONENT_TYPE(MQTTNumberComponent, "number") const EntityBase *MQTTNumberComponent::get_entity() const { return this->number_; } void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { diff --git a/esphome/components/mqtt/mqtt_number.h b/esphome/components/mqtt/mqtt_number.h index b89e78a454..021a539988 100644 --- a/esphome/components/mqtt/mqtt_number.h +++ b/esphome/components/mqtt/mqtt_number.h @@ -32,7 +32,7 @@ class MQTTNumberComponent : public mqtt::MQTTComponent { protected: /// Override for MQTTComponent, returns "number". - std::string component_type() const override; + const char *component_type() const override; const EntityBase *get_entity() const override; number::Number *number_; diff --git a/esphome/components/mqtt/mqtt_select.cpp b/esphome/components/mqtt/mqtt_select.cpp index 5edc5c50dc..09d90ed46e 100644 --- a/esphome/components/mqtt/mqtt_select.cpp +++ b/esphome/components/mqtt/mqtt_select.cpp @@ -28,7 +28,7 @@ void MQTTSelectComponent::dump_config() { LOG_MQTT_COMPONENT(true, false) } -std::string MQTTSelectComponent::component_type() const { return "select"; } +MQTT_COMPONENT_TYPE(MQTTSelectComponent, "select") const EntityBase *MQTTSelectComponent::get_entity() const { return this->select_; } void MQTTSelectComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { diff --git a/esphome/components/mqtt/mqtt_select.h b/esphome/components/mqtt/mqtt_select.h index 19aad662e5..aaf174ff72 100644 --- a/esphome/components/mqtt/mqtt_select.h +++ b/esphome/components/mqtt/mqtt_select.h @@ -32,7 +32,7 @@ class MQTTSelectComponent : public mqtt::MQTTComponent { protected: /// Override for MQTTComponent, returns "select". - std::string component_type() const override; + const char *component_type() const override; const EntityBase *get_entity() const override; select::Select *select_; diff --git a/esphome/components/mqtt/mqtt_sensor.cpp b/esphome/components/mqtt/mqtt_sensor.cpp index bd79ae40fe..14eb160e72 100644 --- a/esphome/components/mqtt/mqtt_sensor.cpp +++ b/esphome/components/mqtt/mqtt_sensor.cpp @@ -31,7 +31,7 @@ void MQTTSensorComponent::dump_config() { LOG_MQTT_COMPONENT(true, false) } -std::string MQTTSensorComponent::component_type() const { return "sensor"; } +MQTT_COMPONENT_TYPE(MQTTSensorComponent, "sensor") const EntityBase *MQTTSensorComponent::get_entity() const { return this->sensor_; } uint32_t MQTTSensorComponent::get_expire_after() const { diff --git a/esphome/components/mqtt/mqtt_sensor.h b/esphome/components/mqtt/mqtt_sensor.h index 8c60199e1b..e8202aa8e2 100644 --- a/esphome/components/mqtt/mqtt_sensor.h +++ b/esphome/components/mqtt/mqtt_sensor.h @@ -43,7 +43,7 @@ class MQTTSensorComponent : public mqtt::MQTTComponent { protected: /// Override for MQTTComponent, returns "sensor". - std::string component_type() const override; + const char *component_type() const override; const EntityBase *get_entity() const override; sensor::Sensor *sensor_; diff --git a/esphome/components/mqtt/mqtt_switch.cpp b/esphome/components/mqtt/mqtt_switch.cpp index a35ae8f9b6..a985ec66be 100644 --- a/esphome/components/mqtt/mqtt_switch.cpp +++ b/esphome/components/mqtt/mqtt_switch.cpp @@ -41,7 +41,7 @@ void MQTTSwitchComponent::dump_config() { LOG_MQTT_COMPONENT(true, true); } -std::string MQTTSwitchComponent::component_type() const { return "switch"; } +MQTT_COMPONENT_TYPE(MQTTSwitchComponent, "switch") const EntityBase *MQTTSwitchComponent::get_entity() const { return this->switch_; } void MQTTSwitchComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson diff --git a/esphome/components/mqtt/mqtt_switch.h b/esphome/components/mqtt/mqtt_switch.h index fb6a13f172..5f6cb841fd 100644 --- a/esphome/components/mqtt/mqtt_switch.h +++ b/esphome/components/mqtt/mqtt_switch.h @@ -27,7 +27,7 @@ class MQTTSwitchComponent : public mqtt::MQTTComponent { protected: /// "switch" component type. - std::string component_type() const override; + const char *component_type() const override; const EntityBase *get_entity() const override; switch_::Switch *switch_; diff --git a/esphome/components/mqtt/mqtt_text.cpp b/esphome/components/mqtt/mqtt_text.cpp index 3cb851fd38..cee94965c6 100644 --- a/esphome/components/mqtt/mqtt_text.cpp +++ b/esphome/components/mqtt/mqtt_text.cpp @@ -29,7 +29,7 @@ void MQTTTextComponent::dump_config() { LOG_MQTT_COMPONENT(true, true) } -std::string MQTTTextComponent::component_type() const { return "text"; } +MQTT_COMPONENT_TYPE(MQTTTextComponent, "text") const EntityBase *MQTTTextComponent::get_entity() const { return this->text_; } void MQTTTextComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { diff --git a/esphome/components/mqtt/mqtt_text.h b/esphome/components/mqtt/mqtt_text.h index 0480b89395..8ae0b9e29a 100644 --- a/esphome/components/mqtt/mqtt_text.h +++ b/esphome/components/mqtt/mqtt_text.h @@ -32,7 +32,7 @@ class MQTTTextComponent : public mqtt::MQTTComponent { protected: /// Override for MQTTComponent, returns "text". - std::string component_type() const override; + const char *component_type() const override; const EntityBase *get_entity() const override; text::Text *text_; diff --git a/esphome/components/mqtt/mqtt_text_sensor.cpp b/esphome/components/mqtt/mqtt_text_sensor.cpp index c87f22fb8e..5346923b41 100644 --- a/esphome/components/mqtt/mqtt_text_sensor.cpp +++ b/esphome/components/mqtt/mqtt_text_sensor.cpp @@ -39,7 +39,7 @@ bool MQTTTextSensor::send_initial_state() { return true; } } -std::string MQTTTextSensor::component_type() const { return "sensor"; } +MQTT_COMPONENT_TYPE(MQTTTextSensor, "sensor") const EntityBase *MQTTTextSensor::get_entity() const { return this->sensor_; } } // namespace esphome::mqtt diff --git a/esphome/components/mqtt/mqtt_text_sensor.h b/esphome/components/mqtt/mqtt_text_sensor.h index d4d38d7eb2..d8f9315c1e 100644 --- a/esphome/components/mqtt/mqtt_text_sensor.h +++ b/esphome/components/mqtt/mqtt_text_sensor.h @@ -25,7 +25,7 @@ class MQTTTextSensor : public mqtt::MQTTComponent { bool send_initial_state() override; protected: - std::string component_type() const override; + const char *component_type() const override; const EntityBase *get_entity() const override; text_sensor::TextSensor *sensor_; diff --git a/esphome/components/mqtt/mqtt_time.cpp b/esphome/components/mqtt/mqtt_time.cpp index 01b8dd3483..b75325022a 100644 --- a/esphome/components/mqtt/mqtt_time.cpp +++ b/esphome/components/mqtt/mqtt_time.cpp @@ -39,7 +39,7 @@ void MQTTTimeComponent::dump_config() { LOG_MQTT_COMPONENT(true, true) } -std::string MQTTTimeComponent::component_type() const { return "time"; } +MQTT_COMPONENT_TYPE(MQTTTimeComponent, "time") const EntityBase *MQTTTimeComponent::get_entity() const { return this->time_; } void MQTTTimeComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { diff --git a/esphome/components/mqtt/mqtt_time.h b/esphome/components/mqtt/mqtt_time.h index 60345c37ae..cf5780da2d 100644 --- a/esphome/components/mqtt/mqtt_time.h +++ b/esphome/components/mqtt/mqtt_time.h @@ -31,7 +31,7 @@ class MQTTTimeComponent : public mqtt::MQTTComponent { bool publish_state(uint8_t hour, uint8_t minute, uint8_t second); protected: - std::string component_type() const override; + const char *component_type() const override; const EntityBase *get_entity() const override; datetime::TimeEntity *time_; diff --git a/esphome/components/mqtt/mqtt_update.cpp b/esphome/components/mqtt/mqtt_update.cpp index aedf2414c1..99e0c85509 100644 --- a/esphome/components/mqtt/mqtt_update.cpp +++ b/esphome/components/mqtt/mqtt_update.cpp @@ -52,7 +52,7 @@ void MQTTUpdateComponent::dump_config() { LOG_MQTT_COMPONENT(true, true); } -std::string MQTTUpdateComponent::component_type() const { return "update"; } +MQTT_COMPONENT_TYPE(MQTTUpdateComponent, "update") const EntityBase *MQTTUpdateComponent::get_entity() const { return this->update_; } } // namespace esphome::mqtt diff --git a/esphome/components/mqtt/mqtt_update.h b/esphome/components/mqtt/mqtt_update.h index d04d22d25f..ec1adb1fcd 100644 --- a/esphome/components/mqtt/mqtt_update.h +++ b/esphome/components/mqtt/mqtt_update.h @@ -27,7 +27,7 @@ class MQTTUpdateComponent : public mqtt::MQTTComponent { protected: /// "update" component type. - std::string component_type() const override; + const char *component_type() const override; const EntityBase *get_entity() const override; update::UpdateEntity *update_; diff --git a/esphome/components/mqtt/mqtt_valve.cpp b/esphome/components/mqtt/mqtt_valve.cpp index 8ee693121b..a4c893f84b 100644 --- a/esphome/components/mqtt/mqtt_valve.cpp +++ b/esphome/components/mqtt/mqtt_valve.cpp @@ -65,7 +65,7 @@ void MQTTValveComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConf } } -std::string MQTTValveComponent::component_type() const { return "valve"; } +MQTT_COMPONENT_TYPE(MQTTValveComponent, "valve") const EntityBase *MQTTValveComponent::get_entity() const { return this->valve_; } bool MQTTValveComponent::send_initial_state() { return this->publish_state(); } diff --git a/esphome/components/mqtt/mqtt_valve.h b/esphome/components/mqtt/mqtt_valve.h index 9e5221e495..d3b724a8ba 100644 --- a/esphome/components/mqtt/mqtt_valve.h +++ b/esphome/components/mqtt/mqtt_valve.h @@ -27,7 +27,7 @@ class MQTTValveComponent : public mqtt::MQTTComponent { void dump_config() override; protected: - std::string component_type() const override; + const char *component_type() const override; const EntityBase *get_entity() const override; valve::Valve *valve_; From 3234f446602be57fa59784c11c3ef10d63703137 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 7 Jan 2026 19:32:30 -1000 Subject: [PATCH 03/11] Address Copilot review comments --- esphome/components/mqtt/mqtt_component.cpp | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index bc209a7d11..8d04d376f6 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -27,21 +27,20 @@ inline char *append_char(char *p, char c) { // 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. +// MQTT_COMPONENT_TYPE_MAX_LEN and MQTT_SUFFIX_MAX_LEN are defined in mqtt_component.h. // OBJECT_ID_MAX_LEN is defined in entity_base.h and derived from friendly_name limits. // 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 COMPONENT_TYPE_MAX_LEN = 20; // Longest: "alarm_control_panel" = 19 -static constexpr size_t SUFFIX_MAX_LEN = 32; // Longest: "target_temperature_high/command" = 28 -static constexpr size_t SANITIZED_NAME_MAX_LEN = 64; // Same as topic_prefix (device name) +static constexpr size_t SANITIZED_NAME_MAX_LEN = 64; // Safe: hostname validation limits to 31 chars 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 + COMPONENT_TYPE_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 1 + SUFFIX_MAX_LEN + 1; + 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 -static constexpr size_t DISCOVERY_TOPIC_MAX_LEN = - DISCOVERY_PREFIX_MAX_LEN + 1 + COMPONENT_TYPE_MAX_LEN + 1 + SANITIZED_NAME_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 7 + 1; +static constexpr size_t DISCOVERY_TOPIC_MAX_LEN = DISCOVERY_PREFIX_MAX_LEN + 1 + MQTT_COMPONENT_TYPE_MAX_LEN + 1 + + SANITIZED_NAME_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 7 + 1; void MQTTComponent::set_qos(uint8_t qos) { this->qos_ = qos; } From 8b46610281a6faeb3ff7b2cc4e16946575bfa53b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 7 Jan 2026 19:39:11 -1000 Subject: [PATCH 04/11] tweaks --- esphome/components/mqtt/mqtt_component.cpp | 5 ++--- esphome/core/config.py | 2 ++ esphome/core/entity_base.h | 5 ++++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index 8d04d376f6..d838d1789f 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -28,10 +28,9 @@ inline char *append_char(char *p, char c) { // These limits are enforced at Python config validation time in mqtt/__init__.py // 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. -// OBJECT_ID_MAX_LEN is defined in entity_base.h and derived from friendly_name limits. +// 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. static constexpr size_t TOPIC_PREFIX_MAX_LEN = 64; // Validated in Python: cv.Length(max=64) -static constexpr size_t SANITIZED_NAME_MAX_LEN = 64; // Safe: hostname validation limits to 31 chars 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 @@ -40,7 +39,7 @@ 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 static constexpr size_t DISCOVERY_TOPIC_MAX_LEN = DISCOVERY_PREFIX_MAX_LEN + 1 + MQTT_COMPONENT_TYPE_MAX_LEN + 1 + - SANITIZED_NAME_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 7 + 1; + ESPHOME_DEVICE_NAME_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 7 + 1; void MQTTComponent::set_qos(uint8_t qos) { this->qos_ = qos; } diff --git a/esphome/core/config.py b/esphome/core/config.py index f9c3011507..b7e6ab9bee 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -76,6 +76,7 @@ VALID_INCLUDE_EXTS = {".h", ".hpp", ".tcc", ".ino", ".cpp", ".c"} def validate_hostname(config): + # Keep in sync with ESPHOME_DEVICE_NAME_MAX_LEN in esphome/core/entity_base.h max_length = 31 if config[CONF_NAME_ADD_MAC_SUFFIX]: max_length -= 7 # "-AABBCC" is appended when add mac suffix option is used @@ -207,6 +208,7 @@ CONFIG_SCHEMA = cv.All( cv.Schema( { cv.Required(CONF_NAME): cv.valid_name, + # Keep max=120 in sync with OBJECT_ID_MAX_LEN in esphome/core/entity_base.h cv.Optional(CONF_FRIENDLY_NAME, ""): cv.All( cv.string_no_slash, cv.Length(max=120) ), diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index a45c7795bf..1649077dd0 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -13,7 +13,10 @@ namespace esphome { -// Maximum size for object_id buffer (friendly_name max ~120 + margin) +// Maximum device name length - keep in sync with validate_hostname() in esphome/core/config.py +static constexpr size_t ESPHOME_DEVICE_NAME_MAX_LEN = 31; + +// Maximum size for object_id buffer - keep in sync with friendly_name cv.Length(max=120) in esphome/core/config.py static constexpr size_t OBJECT_ID_MAX_LEN = 128; enum EntityCategory : uint8_t { From 197cf6f445bf3bb70aa367f782c648028aa17bf9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 7 Jan 2026 19:56:16 -1000 Subject: [PATCH 05/11] [web_server] Use centralized length constants for buffer sizing --- esphome/components/web_server/web_server.cpp | 13 ++++++++----- esphome/core/entity_base.h | 8 +++++++- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index e5705d7b47..3a2ae79094 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -498,13 +498,16 @@ static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, J // Build id into stack buffer - ArduinoJson copies the string // Format: {prefix}/{device?}/{name} - // Buffer size guaranteed by schema validation (NAME_MAX_LENGTH=120): - // With devices: domain(20) + "/" + device(120) + "/" + name(120) + null = 263, rounded up to 280 for safety margin - // Without devices: domain(20) + "/" + name(120) + null = 142, rounded up to 150 for safety margin + // Buffer sizes use constants from entity_base.h validated in core/config.py #ifdef USE_DEVICES - char id_buf[280]; + // domain + "/" + device + "/" + name + null + static constexpr size_t ID_BUF_SIZE = + ESPHOME_DOMAIN_MAX_LEN + 1 + ESPHOME_DEVICE_NAME_MAX_LEN + 1 + ESPHOME_FRIENDLY_NAME_MAX_LEN + 1; + char id_buf[ID_BUF_SIZE]; #else - char id_buf[150]; + // domain + "/" + name + null + static constexpr size_t ID_BUF_SIZE = ESPHOME_DOMAIN_MAX_LEN + 1 + ESPHOME_FRIENDLY_NAME_MAX_LEN + 1; + char id_buf[ID_BUF_SIZE]; #endif char *p = id_buf; memcpy(p, prefix, prefix_len); diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 1649077dd0..7208618832 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -16,7 +16,13 @@ namespace esphome { // Maximum device name length - keep in sync with validate_hostname() in esphome/core/config.py static constexpr size_t ESPHOME_DEVICE_NAME_MAX_LEN = 31; -// Maximum size for object_id buffer - keep in sync with friendly_name cv.Length(max=120) in esphome/core/config.py +// Maximum friendly name length - keep in sync with friendly_name cv.Length(max=120) in esphome/core/config.py +static constexpr size_t ESPHOME_FRIENDLY_NAME_MAX_LEN = 120; + +// Maximum domain length (longest: "alarm_control_panel" = 19) +static constexpr size_t ESPHOME_DOMAIN_MAX_LEN = 20; + +// Maximum size for object_id buffer (friendly_name + margin for sanitization) static constexpr size_t OBJECT_ID_MAX_LEN = 128; enum EntityCategory : uint8_t { From ec5290ef80b599b78e755f2d957bdb8f4fe0ccc6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 7 Jan 2026 19:58:30 -1000 Subject: [PATCH 06/11] [web_server] Use centralized length constants for buffer sizing --- esphome/components/web_server/web_server.cpp | 5 +---- esphome/core/entity_base.h | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 3a2ae79094..413b0e82f6 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -500,15 +500,12 @@ static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, J // Format: {prefix}/{device?}/{name} // Buffer sizes use constants from entity_base.h validated in core/config.py #ifdef USE_DEVICES - // domain + "/" + device + "/" + name + null static constexpr size_t ID_BUF_SIZE = ESPHOME_DOMAIN_MAX_LEN + 1 + ESPHOME_DEVICE_NAME_MAX_LEN + 1 + ESPHOME_FRIENDLY_NAME_MAX_LEN + 1; - char id_buf[ID_BUF_SIZE]; #else - // domain + "/" + name + null static constexpr size_t ID_BUF_SIZE = ESPHOME_DOMAIN_MAX_LEN + 1 + ESPHOME_FRIENDLY_NAME_MAX_LEN + 1; - char id_buf[ID_BUF_SIZE]; #endif + char id_buf[ID_BUF_SIZE]; char *p = id_buf; memcpy(p, prefix, prefix_len); p += prefix_len; diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 7208618832..4ed6a8024a 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -22,7 +22,7 @@ static constexpr size_t ESPHOME_FRIENDLY_NAME_MAX_LEN = 120; // Maximum domain length (longest: "alarm_control_panel" = 19) static constexpr size_t ESPHOME_DOMAIN_MAX_LEN = 20; -// Maximum size for object_id buffer (friendly_name + margin for sanitization) +// Maximum size for object_id buffer (friendly_name + null + margin) static constexpr size_t OBJECT_ID_MAX_LEN = 128; enum EntityCategory : uint8_t { From 9a3d1f5accebf6176881cb58a0d7eb3c76e1c841 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 7 Jan 2026 19:59:45 -1000 Subject: [PATCH 07/11] [web_server] Use centralized length constants for buffer sizing --- esphome/components/web_server/web_server.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 413b0e82f6..8ea1cf98c2 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -499,9 +499,10 @@ static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, J // Build id into stack buffer - ArduinoJson copies the string // Format: {prefix}/{device?}/{name} // Buffer sizes use constants from entity_base.h validated in core/config.py + // Note: Device name (USE_DEVICES) uses ESPHOME_FRIENDLY_NAME_MAX_LEN, not ESPHOME_DEVICE_NAME_MAX_LEN #ifdef USE_DEVICES static constexpr size_t ID_BUF_SIZE = - ESPHOME_DOMAIN_MAX_LEN + 1 + ESPHOME_DEVICE_NAME_MAX_LEN + 1 + ESPHOME_FRIENDLY_NAME_MAX_LEN + 1; + ESPHOME_DOMAIN_MAX_LEN + 1 + ESPHOME_FRIENDLY_NAME_MAX_LEN + 1 + ESPHOME_FRIENDLY_NAME_MAX_LEN + 1; #else static constexpr size_t ID_BUF_SIZE = ESPHOME_DOMAIN_MAX_LEN + 1 + ESPHOME_FRIENDLY_NAME_MAX_LEN + 1; #endif From b8da3b32656fe8b7620ea68134550b6ac76f43ea Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 7 Jan 2026 20:00:40 -1000 Subject: [PATCH 08/11] [web_server] Use centralized length constants for buffer sizing --- esphome/components/web_server/web_server.cpp | 3 ++- esphome/core/entity_base.h | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 8ea1cf98c2..cab177c182 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -499,7 +499,8 @@ static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, J // Build id into stack buffer - ArduinoJson copies the string // Format: {prefix}/{device?}/{name} // Buffer sizes use constants from entity_base.h validated in core/config.py - // Note: Device name (USE_DEVICES) uses ESPHOME_FRIENDLY_NAME_MAX_LEN, not ESPHOME_DEVICE_NAME_MAX_LEN + // Note: Device name uses ESPHOME_FRIENDLY_NAME_MAX_LEN (sub-device max 120), not ESPHOME_DEVICE_NAME_MAX_LEN + // (hostname) #ifdef USE_DEVICES static constexpr size_t ID_BUF_SIZE = ESPHOME_DOMAIN_MAX_LEN + 1 + ESPHOME_FRIENDLY_NAME_MAX_LEN + 1 + ESPHOME_FRIENDLY_NAME_MAX_LEN + 1; diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 4ed6a8024a..dc961c3d84 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -16,7 +16,8 @@ namespace esphome { // Maximum device name length - keep in sync with validate_hostname() in esphome/core/config.py static constexpr size_t ESPHOME_DEVICE_NAME_MAX_LEN = 31; -// Maximum friendly name length - keep in sync with friendly_name cv.Length(max=120) in esphome/core/config.py +// Maximum friendly name length for entities and sub-devices - keep in sync with cv.Length(max=120) in +// esphome/core/config.py static constexpr size_t ESPHOME_FRIENDLY_NAME_MAX_LEN = 120; // Maximum domain length (longest: "alarm_control_panel" = 19) From 40cd6aa18bc353ea13d48a82d8edd156caa3de83 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 7 Jan 2026 20:01:23 -1000 Subject: [PATCH 09/11] [web_server] Use centralized length constants for buffer sizing --- esphome/core/config.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/esphome/core/config.py b/esphome/core/config.py index b7e6ab9bee..02ffd37b54 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -184,17 +184,24 @@ if "ESPHOME_DEFAULT_COMPILE_PROCESS_LIMIT" in os.environ: else: _compile_process_limit_default = cv.UNDEFINED +# Keep in sync with ESPHOME_FRIENDLY_NAME_MAX_LEN in esphome/core/entity_base.h +FRIENDLY_NAME_MAX_LEN = 120 + AREA_SCHEMA = cv.Schema( { cv.GenerateID(CONF_ID): cv.declare_id(Area), - cv.Required(CONF_NAME): cv.All(cv.string_no_slash, cv.Length(max=120)), + cv.Required(CONF_NAME): cv.All( + cv.string_no_slash, cv.Length(max=FRIENDLY_NAME_MAX_LEN) + ), } ) DEVICE_SCHEMA = cv.Schema( { cv.GenerateID(CONF_ID): cv.declare_id(Device), - cv.Required(CONF_NAME): cv.All(cv.string_no_slash, cv.Length(max=120)), + cv.Required(CONF_NAME): cv.All( + cv.string_no_slash, cv.Length(max=FRIENDLY_NAME_MAX_LEN) + ), cv.Optional(CONF_AREA_ID): cv.use_id(Area), } ) From aa3bed7089c2a65cde9de6a9d2e79a6c4fe1b46a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 7 Jan 2026 20:01:54 -1000 Subject: [PATCH 10/11] [web_server] Use centralized length constants for buffer sizing --- esphome/core/entity_base.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index dc961c3d84..5f75872a0f 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -16,7 +16,7 @@ namespace esphome { // Maximum device name length - keep in sync with validate_hostname() in esphome/core/config.py static constexpr size_t ESPHOME_DEVICE_NAME_MAX_LEN = 31; -// Maximum friendly name length for entities and sub-devices - keep in sync with cv.Length(max=120) in +// Maximum friendly name length for entities and sub-devices - keep in sync with FRIENDLY_NAME_MAX_LEN in // esphome/core/config.py static constexpr size_t ESPHOME_FRIENDLY_NAME_MAX_LEN = 120; From 735aca89eedf08ba7a3d734fbe763bc208f4f58b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 7 Jan 2026 20:03:18 -1000 Subject: [PATCH 11/11] [web_server] Use centralized length constants for buffer sizing --- esphome/core/config.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/core/config.py b/esphome/core/config.py index 02ffd37b54..a41b90317e 100644 --- a/esphome/core/config.py +++ b/esphome/core/config.py @@ -215,9 +215,8 @@ CONFIG_SCHEMA = cv.All( cv.Schema( { cv.Required(CONF_NAME): cv.valid_name, - # Keep max=120 in sync with OBJECT_ID_MAX_LEN in esphome/core/entity_base.h cv.Optional(CONF_FRIENDLY_NAME, ""): cv.All( - cv.string_no_slash, cv.Length(max=120) + cv.string_no_slash, cv.Length(max=FRIENDLY_NAME_MAX_LEN) ), cv.Optional(CONF_AREA): validate_area_config, cv.Optional(CONF_COMMENT): cv.All(cv.string, cv.Length(max=255)),