From df53ff7afed11a1fc18ad03484d77c9c64bf1024 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 12:13:12 -0600 Subject: [PATCH 1/8] [scheduler] Extract helper functions to improve code readability (#11730) --- esphome/core/scheduler.cpp | 45 ++++++++++++++++++++------------------ esphome/core/scheduler.h | 34 ++++++++++++++++++++-------- 2 files changed, 49 insertions(+), 30 deletions(-) diff --git a/esphome/core/scheduler.cpp b/esphome/core/scheduler.cpp index 11d59c2499..d285af2d0e 100644 --- a/esphome/core/scheduler.cpp +++ b/esphome/core/scheduler.cpp @@ -117,12 +117,8 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type item->set_name(name_cstr, !is_static_string); item->type = type; item->callback = std::move(func); - // Initialize remove to false (though it should already be from constructor) -#ifdef ESPHOME_THREAD_MULTI_ATOMICS - item->remove.store(false, std::memory_order_relaxed); -#else - item->remove = false; -#endif + // Reset remove flag - recycled items may have been cancelled (remove=true) in previous use + this->set_item_removed_(item.get(), false); item->is_retry = is_retry; #ifndef ESPHOME_THREAD_SINGLE @@ -153,21 +149,7 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type } #ifdef ESPHOME_DEBUG_SCHEDULER - // Validate static strings in debug mode - if (is_static_string && name_cstr != nullptr) { - validate_static_string(name_cstr); - } - - // Debug logging - const char *type_str = (type == SchedulerItem::TIMEOUT) ? "timeout" : "interval"; - if (type == SchedulerItem::TIMEOUT) { - ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ")", type_str, LOG_STR_ARG(item->get_source()), - name_cstr ? name_cstr : "(null)", type_str, delay); - } else { - ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ", offset=%" PRIu32 ")", type_str, LOG_STR_ARG(item->get_source()), - name_cstr ? name_cstr : "(null)", type_str, delay, - static_cast(item->get_next_execution() - now)); - } + this->debug_log_timer_(item.get(), is_static_string, name_cstr, type, delay, now); #endif /* ESPHOME_DEBUG_SCHEDULER */ // For retries, check if there's a cancelled timeout first @@ -787,4 +769,25 @@ void Scheduler::recycle_item_(std::unique_ptr item) { // else: unique_ptr will delete the item when it goes out of scope } +#ifdef ESPHOME_DEBUG_SCHEDULER +void Scheduler::debug_log_timer_(const SchedulerItem *item, bool is_static_string, const char *name_cstr, + SchedulerItem::Type type, uint32_t delay, uint64_t now) { + // Validate static strings in debug mode + if (is_static_string && name_cstr != nullptr) { + validate_static_string(name_cstr); + } + + // Debug logging + const char *type_str = (type == SchedulerItem::TIMEOUT) ? "timeout" : "interval"; + if (type == SchedulerItem::TIMEOUT) { + ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ")", type_str, LOG_STR_ARG(item->get_source()), + name_cstr ? name_cstr : "(null)", type_str, delay); + } else { + ESP_LOGD(TAG, "set_%s(name='%s/%s', %s=%" PRIu32 ", offset=%" PRIu32 ")", type_str, LOG_STR_ARG(item->get_source()), + name_cstr ? name_cstr : "(null)", type_str, delay, + static_cast(item->get_next_execution() - now)); + } +} +#endif /* ESPHOME_DEBUG_SCHEDULER */ + } // namespace esphome diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index f6ec07294d..fd16840240 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -266,6 +266,12 @@ class Scheduler { // Helper to perform full cleanup when too many items are cancelled void full_cleanup_removed_items_(); +#ifdef ESPHOME_DEBUG_SCHEDULER + // Helper for debug logging in set_timer_common_ - extracted to reduce code size + void debug_log_timer_(const SchedulerItem *item, bool is_static_string, const char *name_cstr, + SchedulerItem::Type type, uint32_t delay, uint64_t now); +#endif /* ESPHOME_DEBUG_SCHEDULER */ + #ifndef ESPHOME_THREAD_SINGLE // Helper to process defer queue - inline for performance in hot path inline void process_defer_queue_(uint32_t &now) { @@ -367,6 +373,24 @@ class Scheduler { #endif } + // Helper to set item removal flag (platform-specific) + // For ESPHOME_THREAD_MULTI_NO_ATOMICS platforms, the caller must hold the scheduler lock before calling this + // function. Uses memory_order_release when setting to true (for cancellation synchronization), + // and memory_order_relaxed when setting to false (for initialization). + void set_item_removed_(SchedulerItem *item, bool removed) { +#ifdef ESPHOME_THREAD_MULTI_ATOMICS + // Multi-threaded with atomics: use atomic store with appropriate ordering + // Release ordering when setting to true ensures cancellation is visible to other threads + // Relaxed ordering when setting to false is sufficient for initialization + item->remove.store(removed, removed ? std::memory_order_release : std::memory_order_relaxed); +#else + // Single-threaded (ESPHOME_THREAD_SINGLE) or + // multi-threaded without atomics (ESPHOME_THREAD_MULTI_NO_ATOMICS): direct write + // For ESPHOME_THREAD_MULTI_NO_ATOMICS, caller MUST hold lock! + item->remove = removed; +#endif + } + // Helper to mark matching items in a container as removed // Returns the number of items marked for removal // IMPORTANT: Caller must hold the scheduler lock before calling this function. @@ -383,15 +407,7 @@ class Scheduler { continue; if (this->matches_item_(item, component, name_cstr, type, match_retry)) { // Mark item for removal (platform-specific) -#ifdef ESPHOME_THREAD_MULTI_ATOMICS - // Multi-threaded with atomics: use atomic store - item->remove.store(true, std::memory_order_release); -#else - // Single-threaded (ESPHOME_THREAD_SINGLE) or - // multi-threaded without atomics (ESPHOME_THREAD_MULTI_NO_ATOMICS): direct write - // For ESPHOME_THREAD_MULTI_NO_ATOMICS, caller MUST hold lock! - item->remove = true; -#endif + this->set_item_removed_(item.get(), true); count++; } } From d36ef050a90d09a87e22b2cc5866b52007e9c875 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 12:15:50 -0600 Subject: [PATCH 2/8] [template] Mark all component classes as final (#11733) --- .../alarm_control_panel/template_alarm_control_panel.h | 2 +- .../template/binary_sensor/template_binary_sensor.h | 2 +- esphome/components/template/button/template_button.h | 2 +- esphome/components/template/cover/template_cover.h | 2 +- esphome/components/template/datetime/template_date.h | 2 +- esphome/components/template/datetime/template_datetime.h | 2 +- esphome/components/template/datetime/template_time.h | 2 +- esphome/components/template/event/template_event.h | 2 +- esphome/components/template/fan/template_fan.h | 2 +- esphome/components/template/lock/template_lock.h | 2 +- esphome/components/template/number/template_number.h | 2 +- esphome/components/template/output/template_output.h | 4 ++-- esphome/components/template/select/template_select.h | 2 +- esphome/components/template/sensor/template_sensor.h | 2 +- esphome/components/template/switch/template_switch.h | 2 +- esphome/components/template/text/template_text.h | 2 +- .../components/template/text_sensor/template_text_sensor.h | 2 +- esphome/components/template/valve/template_valve.h | 2 +- 18 files changed, 19 insertions(+), 19 deletions(-) diff --git a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h index 40a79004da..202dc7c13f 100644 --- a/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h +++ b/esphome/components/template/alarm_control_panel/template_alarm_control_panel.h @@ -49,7 +49,7 @@ struct SensorInfo { uint8_t store_index; }; -class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel, public Component { +class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControlPanel, public Component { public: TemplateAlarmControlPanel(); void dump_config() override; diff --git a/esphome/components/template/binary_sensor/template_binary_sensor.h b/esphome/components/template/binary_sensor/template_binary_sensor.h index bc591391b9..0af709b097 100644 --- a/esphome/components/template/binary_sensor/template_binary_sensor.h +++ b/esphome/components/template/binary_sensor/template_binary_sensor.h @@ -7,7 +7,7 @@ namespace esphome { namespace template_ { -class TemplateBinarySensor : public Component, public binary_sensor::BinarySensor { +class TemplateBinarySensor final : public Component, public binary_sensor::BinarySensor { public: template void set_template(F &&f) { this->f_.set(std::forward(f)); } diff --git a/esphome/components/template/button/template_button.h b/esphome/components/template/button/template_button.h index 68e976f64b..5bda82c58f 100644 --- a/esphome/components/template/button/template_button.h +++ b/esphome/components/template/button/template_button.h @@ -5,7 +5,7 @@ namespace esphome { namespace template_ { -class TemplateButton : public button::Button { +class TemplateButton final : public button::Button { public: // Implements the abstract `press_action` but the `on_press` trigger already handles the press. void press_action() override{}; diff --git a/esphome/components/template/cover/template_cover.h b/esphome/components/template/cover/template_cover.h index faff69f867..125c67bb86 100644 --- a/esphome/components/template/cover/template_cover.h +++ b/esphome/components/template/cover/template_cover.h @@ -14,7 +14,7 @@ enum TemplateCoverRestoreMode { COVER_RESTORE_AND_CALL, }; -class TemplateCover : public cover::Cover, public Component { +class TemplateCover final : public cover::Cover, public Component { public: TemplateCover(); diff --git a/esphome/components/template/datetime/template_date.h b/esphome/components/template/datetime/template_date.h index 7fed704d0e..fe64b0ba14 100644 --- a/esphome/components/template/datetime/template_date.h +++ b/esphome/components/template/datetime/template_date.h @@ -14,7 +14,7 @@ namespace esphome { namespace template_ { -class TemplateDate : public datetime::DateEntity, public PollingComponent { +class TemplateDate final : public datetime::DateEntity, public PollingComponent { public: template void set_template(F &&f) { this->f_.set(std::forward(f)); } diff --git a/esphome/components/template/datetime/template_datetime.h b/esphome/components/template/datetime/template_datetime.h index ec45bf0326..c44bd85265 100644 --- a/esphome/components/template/datetime/template_datetime.h +++ b/esphome/components/template/datetime/template_datetime.h @@ -14,7 +14,7 @@ namespace esphome { namespace template_ { -class TemplateDateTime : public datetime::DateTimeEntity, public PollingComponent { +class TemplateDateTime final : public datetime::DateTimeEntity, public PollingComponent { public: template void set_template(F &&f) { this->f_.set(std::forward(f)); } diff --git a/esphome/components/template/datetime/template_time.h b/esphome/components/template/datetime/template_time.h index ea7474c0ba..0c95330d27 100644 --- a/esphome/components/template/datetime/template_time.h +++ b/esphome/components/template/datetime/template_time.h @@ -14,7 +14,7 @@ namespace esphome { namespace template_ { -class TemplateTime : public datetime::TimeEntity, public PollingComponent { +class TemplateTime final : public datetime::TimeEntity, public PollingComponent { public: template void set_template(F &&f) { this->f_.set(std::forward(f)); } diff --git a/esphome/components/template/event/template_event.h b/esphome/components/template/event/template_event.h index 251ae9299b..5467a64141 100644 --- a/esphome/components/template/event/template_event.h +++ b/esphome/components/template/event/template_event.h @@ -6,7 +6,7 @@ namespace esphome { namespace template_ { -class TemplateEvent : public Component, public event::Event {}; +class TemplateEvent final : public Component, public event::Event {}; } // namespace template_ } // namespace esphome diff --git a/esphome/components/template/fan/template_fan.h b/esphome/components/template/fan/template_fan.h index b09352f4d4..052b385b93 100644 --- a/esphome/components/template/fan/template_fan.h +++ b/esphome/components/template/fan/template_fan.h @@ -6,7 +6,7 @@ namespace esphome { namespace template_ { -class TemplateFan : public Component, public fan::Fan { +class TemplateFan final : public Component, public fan::Fan { public: TemplateFan() {} void setup() override; diff --git a/esphome/components/template/lock/template_lock.h b/esphome/components/template/lock/template_lock.h index 14fca4635e..ac10794e4d 100644 --- a/esphome/components/template/lock/template_lock.h +++ b/esphome/components/template/lock/template_lock.h @@ -8,7 +8,7 @@ namespace esphome { namespace template_ { -class TemplateLock : public lock::Lock, public Component { +class TemplateLock final : public lock::Lock, public Component { public: TemplateLock(); diff --git a/esphome/components/template/number/template_number.h b/esphome/components/template/number/template_number.h index a9307e9246..876ec96b3b 100644 --- a/esphome/components/template/number/template_number.h +++ b/esphome/components/template/number/template_number.h @@ -9,7 +9,7 @@ namespace esphome { namespace template_ { -class TemplateNumber : public number::Number, public PollingComponent { +class TemplateNumber final : public number::Number, public PollingComponent { public: template void set_template(F &&f) { this->f_.set(std::forward(f)); } diff --git a/esphome/components/template/output/template_output.h b/esphome/components/template/output/template_output.h index 90de801a5c..9ecfc446b9 100644 --- a/esphome/components/template/output/template_output.h +++ b/esphome/components/template/output/template_output.h @@ -7,7 +7,7 @@ namespace esphome { namespace template_ { -class TemplateBinaryOutput : public output::BinaryOutput { +class TemplateBinaryOutput final : public output::BinaryOutput { public: Trigger *get_trigger() const { return trigger_; } @@ -17,7 +17,7 @@ class TemplateBinaryOutput : public output::BinaryOutput { Trigger *trigger_ = new Trigger(); }; -class TemplateFloatOutput : public output::FloatOutput { +class TemplateFloatOutput final : public output::FloatOutput { public: Trigger *get_trigger() const { return trigger_; } diff --git a/esphome/components/template/select/template_select.h b/esphome/components/template/select/template_select.h index 2dad059ade..cb5b546976 100644 --- a/esphome/components/template/select/template_select.h +++ b/esphome/components/template/select/template_select.h @@ -9,7 +9,7 @@ namespace esphome { namespace template_ { -class TemplateSelect : public select::Select, public PollingComponent { +class TemplateSelect final : public select::Select, public PollingComponent { public: template void set_template(F &&f) { this->f_.set(std::forward(f)); } diff --git a/esphome/components/template/sensor/template_sensor.h b/esphome/components/template/sensor/template_sensor.h index 793d754a0f..3ca965dde3 100644 --- a/esphome/components/template/sensor/template_sensor.h +++ b/esphome/components/template/sensor/template_sensor.h @@ -7,7 +7,7 @@ namespace esphome { namespace template_ { -class TemplateSensor : public sensor::Sensor, public PollingComponent { +class TemplateSensor final : public sensor::Sensor, public PollingComponent { public: template void set_template(F &&f) { this->f_.set(std::forward(f)); } diff --git a/esphome/components/template/switch/template_switch.h b/esphome/components/template/switch/template_switch.h index 18a374df35..35c18af448 100644 --- a/esphome/components/template/switch/template_switch.h +++ b/esphome/components/template/switch/template_switch.h @@ -8,7 +8,7 @@ namespace esphome { namespace template_ { -class TemplateSwitch : public switch_::Switch, public Component { +class TemplateSwitch final : public switch_::Switch, public Component { public: TemplateSwitch(); diff --git a/esphome/components/template/text/template_text.h b/esphome/components/template/text/template_text.h index c12021f80e..1a0a66ed5b 100644 --- a/esphome/components/template/text/template_text.h +++ b/esphome/components/template/text/template_text.h @@ -60,7 +60,7 @@ template class TextSaver : public TemplateTextSaverBase { } }; -class TemplateText : public text::Text, public PollingComponent { +class TemplateText final : public text::Text, public PollingComponent { public: template void set_template(F &&f) { this->f_.set(std::forward(f)); } diff --git a/esphome/components/template/text_sensor/template_text_sensor.h b/esphome/components/template/text_sensor/template_text_sensor.h index 0d01c72023..da5c518c7f 100644 --- a/esphome/components/template/text_sensor/template_text_sensor.h +++ b/esphome/components/template/text_sensor/template_text_sensor.h @@ -8,7 +8,7 @@ namespace esphome { namespace template_ { -class TemplateTextSensor : public text_sensor::TextSensor, public PollingComponent { +class TemplateTextSensor final : public text_sensor::TextSensor, public PollingComponent { public: template void set_template(F &&f) { this->f_.set(std::forward(f)); } diff --git a/esphome/components/template/valve/template_valve.h b/esphome/components/template/valve/template_valve.h index d6235f8e5c..c452648193 100644 --- a/esphome/components/template/valve/template_valve.h +++ b/esphome/components/template/valve/template_valve.h @@ -14,7 +14,7 @@ enum TemplateValveRestoreMode { VALVE_RESTORE_AND_CALL, }; -class TemplateValve : public valve::Valve, public Component { +class TemplateValve final : public valve::Valve, public Component { public: TemplateValve(); From b08419fa473533e28a7cd86f98b2b3f98f1b7f27 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 12:30:45 -0600 Subject: [PATCH 3/8] [mqtt] Use StringRef to avoid string copies in discovery (#11731) --- .../mqtt/mqtt_alarm_control_panel.cpp | 2 +- .../components/mqtt/mqtt_binary_sensor.cpp | 9 ++++--- esphome/components/mqtt/mqtt_button.cpp | 7 ++--- esphome/components/mqtt/mqtt_component.cpp | 26 +++++++++++-------- esphome/components/mqtt/mqtt_component.h | 8 +++--- esphome/components/mqtt/mqtt_cover.cpp | 9 ++++--- esphome/components/mqtt/mqtt_event.cpp | 8 ++++-- esphome/components/mqtt/mqtt_fan.cpp | 14 +++++----- esphome/components/mqtt/mqtt_lock.cpp | 2 +- esphome/components/mqtt/mqtt_number.cpp | 14 +++++++--- esphome/components/mqtt/mqtt_sensor.cpp | 14 ++++++---- esphome/components/mqtt/mqtt_switch.cpp | 2 +- esphome/components/mqtt/mqtt_text_sensor.cpp | 8 +++--- esphome/components/mqtt/mqtt_update.cpp | 2 +- esphome/components/mqtt/mqtt_valve.cpp | 8 +++--- 15 files changed, 81 insertions(+), 52 deletions(-) diff --git a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp index 94460c31a7..dd3df5f8aa 100644 --- a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp +++ b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp @@ -36,7 +36,7 @@ void MQTTAlarmControlPanelComponent::setup() { } else if (strcasecmp(payload.c_str(), "TRIGGERED") == 0) { call.triggered(); } else { - ESP_LOGW(TAG, "'%s': Received unknown command payload %s", this->friendly_name().c_str(), payload.c_str()); + ESP_LOGW(TAG, "'%s': Received unknown command payload %s", this->friendly_name_().c_str(), payload.c_str()); } call.perform(); }); diff --git a/esphome/components/mqtt/mqtt_binary_sensor.cpp b/esphome/components/mqtt/mqtt_binary_sensor.cpp index 2ce4928574..479cee205a 100644 --- a/esphome/components/mqtt/mqtt_binary_sensor.cpp +++ b/esphome/components/mqtt/mqtt_binary_sensor.cpp @@ -30,9 +30,12 @@ MQTTBinarySensorComponent::MQTTBinarySensorComponent(binary_sensor::BinarySensor } void MQTTBinarySensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { - // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - if (!this->binary_sensor_->get_device_class().empty()) - root[MQTT_DEVICE_CLASS] = this->binary_sensor_->get_device_class(); + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + const auto device_class = this->binary_sensor_->get_device_class_ref(); + if (!device_class.empty()) { + root[MQTT_DEVICE_CLASS] = device_class; + } + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) if (this->binary_sensor_->is_status_binary_sensor()) root[MQTT_PAYLOAD_ON] = mqtt::global_mqtt_client->get_availability().payload_available; if (this->binary_sensor_->is_status_binary_sensor()) diff --git a/esphome/components/mqtt/mqtt_button.cpp b/esphome/components/mqtt/mqtt_button.cpp index b3435edf38..f8eb0eab2d 100644 --- a/esphome/components/mqtt/mqtt_button.cpp +++ b/esphome/components/mqtt/mqtt_button.cpp @@ -20,7 +20,7 @@ void MQTTButtonComponent::setup() { if (payload == "PRESS") { this->button_->press(); } else { - ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name().c_str(), payload.c_str()); + ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name_().c_str(), payload.c_str()); this->status_momentary_warning("state", 5000); } }); @@ -33,8 +33,9 @@ void MQTTButtonComponent::dump_config() { void MQTTButtonComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson config.state_topic = false; - if (!this->button_->get_device_class().empty()) { - root[MQTT_DEVICE_CLASS] = this->button_->get_device_class(); + const auto device_class = this->button_->get_device_class_ref(); + if (!device_class.empty()) { + root[MQTT_DEVICE_CLASS] = device_class; } // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) } diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index eb6114008a..1cd818964e 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -64,11 +64,11 @@ bool MQTTComponent::send_discovery_() { const MQTTDiscoveryInfo &discovery_info = global_mqtt_client->get_discovery_info(); if (discovery_info.clean) { - ESP_LOGV(TAG, "'%s': Cleaning discovery", this->friendly_name().c_str()); + ESP_LOGV(TAG, "'%s': Cleaning discovery", this->friendly_name_().c_str()); return global_mqtt_client->publish(this->get_discovery_topic_(discovery_info), "", 0, this->qos_, true); } - ESP_LOGV(TAG, "'%s': Sending discovery", this->friendly_name().c_str()); + ESP_LOGV(TAG, "'%s': Sending discovery", this->friendly_name_().c_str()); // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson return global_mqtt_client->publish_json( @@ -85,12 +85,16 @@ bool MQTTComponent::send_discovery_() { } // Fields from EntityBase - root[MQTT_NAME] = this->get_entity()->has_own_name() ? this->friendly_name() : ""; + root[MQTT_NAME] = this->get_entity()->has_own_name() ? this->friendly_name_() : ""; - if (this->is_disabled_by_default()) + if (this->is_disabled_by_default_()) root[MQTT_ENABLED_BY_DEFAULT] = false; - if (!this->get_icon().empty()) - root[MQTT_ICON] = this->get_icon(); + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + const auto icon_ref = this->get_icon_ref_(); + if (!icon_ref.empty()) { + root[MQTT_ICON] = icon_ref; + } + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) const auto entity_category = this->get_entity()->get_entity_category(); switch (entity_category) { @@ -122,7 +126,7 @@ bool MQTTComponent::send_discovery_() { const MQTTDiscoveryInfo &discovery_info = global_mqtt_client->get_discovery_info(); 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())); + sprintf(friendly_name_hash, "%08" PRIx32, fnv1_hash(this->friendly_name_())); friendly_name_hash[8] = 0; // ensure the hash-string ends with null root[MQTT_UNIQUE_ID] = get_mac_address() + "-" + this->component_type() + "-" + friendly_name_hash; } else { @@ -184,7 +188,7 @@ bool MQTTComponent::is_discovery_enabled() const { } std::string MQTTComponent::get_default_object_id_() const { - return str_sanitize(str_snake_case(this->friendly_name())); + return str_sanitize(str_snake_case(this->friendly_name_())); } void MQTTComponent::subscribe(const std::string &topic, mqtt_callback_t callback, uint8_t qos) { @@ -268,9 +272,9 @@ void MQTTComponent::schedule_resend_state() { this->resend_state_ = true; } bool MQTTComponent::is_connected_() const { return global_mqtt_client->is_connected(); } // Pull these properties from EntityBase if not overridden -std::string MQTTComponent::friendly_name() const { return this->get_entity()->get_name(); } -std::string MQTTComponent::get_icon() const { return this->get_entity()->get_icon(); } -bool MQTTComponent::is_disabled_by_default() const { return this->get_entity()->is_disabled_by_default(); } +std::string MQTTComponent::friendly_name_() const { return this->get_entity()->get_name(); } +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() { if (this->has_custom_state_topic_) { // If the custom state_topic is null, return true as it is internal and should not publish diff --git a/esphome/components/mqtt/mqtt_component.h b/esphome/components/mqtt/mqtt_component.h index 851fdd842c..2f8dfcf64e 100644 --- a/esphome/components/mqtt/mqtt_component.h +++ b/esphome/components/mqtt/mqtt_component.h @@ -165,13 +165,13 @@ class MQTTComponent : public Component { virtual const EntityBase *get_entity() const = 0; /// Get the friendly name of this MQTT component. - virtual std::string friendly_name() const; + std::string friendly_name_() const; - /// Get the icon field of this component - virtual std::string get_icon() const; + /// Get the icon field of this component as StringRef + StringRef get_icon_ref_() const; /// Get whether the underlying Entity is disabled by default - virtual bool is_disabled_by_default() const; + bool is_disabled_by_default_() const; /// Get the MQTT topic that new states will be shared to. std::string get_state_topic_() const; diff --git a/esphome/components/mqtt/mqtt_cover.cpp b/esphome/components/mqtt/mqtt_cover.cpp index 6fb61ee469..b63aa66d29 100644 --- a/esphome/components/mqtt/mqtt_cover.cpp +++ b/esphome/components/mqtt/mqtt_cover.cpp @@ -67,9 +67,12 @@ void MQTTCoverComponent::dump_config() { } } void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { - // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - if (!this->cover_->get_device_class().empty()) - root[MQTT_DEVICE_CLASS] = this->cover_->get_device_class(); + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + const auto device_class = this->cover_->get_device_class_ref(); + if (!device_class.empty()) { + root[MQTT_DEVICE_CLASS] = device_class; + } + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) auto traits = this->cover_->get_traits(); if (traits.get_is_assumed_state()) { diff --git a/esphome/components/mqtt/mqtt_event.cpp b/esphome/components/mqtt/mqtt_event.cpp index f972d545c6..e206335446 100644 --- a/esphome/components/mqtt/mqtt_event.cpp +++ b/esphome/components/mqtt/mqtt_event.cpp @@ -21,8 +21,12 @@ void MQTTEventComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConf for (const auto &event_type : this->event_->get_event_types()) event_types.add(event_type); - if (!this->event_->get_device_class().empty()) - root[MQTT_DEVICE_CLASS] = this->event_->get_device_class(); + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + const auto device_class = this->event_->get_device_class_ref(); + if (!device_class.empty()) { + root[MQTT_DEVICE_CLASS] = device_class; + } + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) config.command_topic = false; } diff --git a/esphome/components/mqtt/mqtt_fan.cpp b/esphome/components/mqtt/mqtt_fan.cpp index 70e1ae3b4a..2aefc3a4db 100644 --- a/esphome/components/mqtt/mqtt_fan.cpp +++ b/esphome/components/mqtt/mqtt_fan.cpp @@ -24,15 +24,15 @@ void MQTTFanComponent::setup() { auto val = parse_on_off(payload.c_str()); switch (val) { case PARSE_ON: - ESP_LOGD(TAG, "'%s' Turning Fan ON.", this->friendly_name().c_str()); + ESP_LOGD(TAG, "'%s' Turning Fan ON.", this->friendly_name_().c_str()); this->state_->turn_on().perform(); break; case PARSE_OFF: - ESP_LOGD(TAG, "'%s' Turning Fan OFF.", this->friendly_name().c_str()); + ESP_LOGD(TAG, "'%s' Turning Fan OFF.", this->friendly_name_().c_str()); this->state_->turn_off().perform(); break; case PARSE_TOGGLE: - ESP_LOGD(TAG, "'%s' Toggling Fan.", this->friendly_name().c_str()); + ESP_LOGD(TAG, "'%s' Toggling Fan.", this->friendly_name_().c_str()); this->state_->toggle().perform(); break; case PARSE_NONE: @@ -48,11 +48,11 @@ void MQTTFanComponent::setup() { auto val = parse_on_off(payload.c_str(), "forward", "reverse"); switch (val) { case PARSE_ON: - ESP_LOGD(TAG, "'%s': Setting direction FORWARD", this->friendly_name().c_str()); + ESP_LOGD(TAG, "'%s': Setting direction FORWARD", this->friendly_name_().c_str()); this->state_->make_call().set_direction(fan::FanDirection::FORWARD).perform(); break; case PARSE_OFF: - ESP_LOGD(TAG, "'%s': Setting direction REVERSE", this->friendly_name().c_str()); + ESP_LOGD(TAG, "'%s': Setting direction REVERSE", this->friendly_name_().c_str()); this->state_->make_call().set_direction(fan::FanDirection::REVERSE).perform(); break; case PARSE_TOGGLE: @@ -75,11 +75,11 @@ void MQTTFanComponent::setup() { auto val = parse_on_off(payload.c_str(), "oscillate_on", "oscillate_off"); switch (val) { case PARSE_ON: - ESP_LOGD(TAG, "'%s': Setting oscillating ON", this->friendly_name().c_str()); + ESP_LOGD(TAG, "'%s': Setting oscillating ON", this->friendly_name_().c_str()); this->state_->make_call().set_oscillating(true).perform(); break; case PARSE_OFF: - ESP_LOGD(TAG, "'%s': Setting oscillating OFF", this->friendly_name().c_str()); + ESP_LOGD(TAG, "'%s': Setting oscillating OFF", this->friendly_name_().c_str()); this->state_->make_call().set_oscillating(false).perform(); break; case PARSE_TOGGLE: diff --git a/esphome/components/mqtt/mqtt_lock.cpp b/esphome/components/mqtt/mqtt_lock.cpp index 0412624983..0e15377ba4 100644 --- a/esphome/components/mqtt/mqtt_lock.cpp +++ b/esphome/components/mqtt/mqtt_lock.cpp @@ -24,7 +24,7 @@ void MQTTLockComponent::setup() { } else if (strcasecmp(payload.c_str(), "OPEN") == 0) { this->lock_->open(); } else { - ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name().c_str(), payload.c_str()); + ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name_().c_str(), payload.c_str()); this->status_momentary_warning("state", 5000); } }); diff --git a/esphome/components/mqtt/mqtt_number.cpp b/esphome/components/mqtt/mqtt_number.cpp index a44632ff30..f419eac130 100644 --- a/esphome/components/mqtt/mqtt_number.cpp +++ b/esphome/components/mqtt/mqtt_number.cpp @@ -44,8 +44,11 @@ void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon root[MQTT_MIN] = traits.get_min_value(); root[MQTT_MAX] = traits.get_max_value(); root[MQTT_STEP] = traits.get_step(); - if (!this->number_->traits.get_unit_of_measurement().empty()) - root[MQTT_UNIT_OF_MEASUREMENT] = this->number_->traits.get_unit_of_measurement(); + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + const auto unit_of_measurement = this->number_->traits.get_unit_of_measurement_ref(); + if (!unit_of_measurement.empty()) { + root[MQTT_UNIT_OF_MEASUREMENT] = unit_of_measurement; + } switch (this->number_->traits.get_mode()) { case NUMBER_MODE_AUTO: break; @@ -56,8 +59,11 @@ void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon root[MQTT_MODE] = "slider"; break; } - if (!this->number_->traits.get_device_class().empty()) - root[MQTT_DEVICE_CLASS] = this->number_->traits.get_device_class(); + const auto device_class = this->number_->traits.get_device_class_ref(); + if (!device_class.empty()) { + root[MQTT_DEVICE_CLASS] = device_class; + } + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) config.command_topic = true; } diff --git a/esphome/components/mqtt/mqtt_sensor.cpp b/esphome/components/mqtt/mqtt_sensor.cpp index 9e61f6ef3b..010ac3013e 100644 --- a/esphome/components/mqtt/mqtt_sensor.cpp +++ b/esphome/components/mqtt/mqtt_sensor.cpp @@ -44,13 +44,17 @@ void MQTTSensorComponent::set_expire_after(uint32_t expire_after) { this->expire void MQTTSensorComponent::disable_expire_after() { this->expire_after_ = 0; } void MQTTSensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { - // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - if (!this->sensor_->get_device_class().empty()) { - root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class(); + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + const auto device_class = this->sensor_->get_device_class_ref(); + if (!device_class.empty()) { + root[MQTT_DEVICE_CLASS] = device_class; } - if (!this->sensor_->get_unit_of_measurement().empty()) - root[MQTT_UNIT_OF_MEASUREMENT] = this->sensor_->get_unit_of_measurement(); + const auto unit_of_measurement = this->sensor_->get_unit_of_measurement_ref(); + if (!unit_of_measurement.empty()) { + root[MQTT_UNIT_OF_MEASUREMENT] = unit_of_measurement; + } + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) if (this->get_expire_after() > 0) root[MQTT_EXPIRE_AFTER] = this->get_expire_after() / 1000; diff --git a/esphome/components/mqtt/mqtt_switch.cpp b/esphome/components/mqtt/mqtt_switch.cpp index 8b1323bdb2..b3a35420b9 100644 --- a/esphome/components/mqtt/mqtt_switch.cpp +++ b/esphome/components/mqtt/mqtt_switch.cpp @@ -29,7 +29,7 @@ void MQTTSwitchComponent::setup() { break; case PARSE_NONE: default: - ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name().c_str(), payload.c_str()); + ESP_LOGW(TAG, "'%s': Received unknown status payload: %s", this->friendly_name_().c_str(), payload.c_str()); this->status_momentary_warning("state", 5000); break; } diff --git a/esphome/components/mqtt/mqtt_text_sensor.cpp b/esphome/components/mqtt/mqtt_text_sensor.cpp index 42260ed2a8..e6e7cf04e8 100644 --- a/esphome/components/mqtt/mqtt_text_sensor.cpp +++ b/esphome/components/mqtt/mqtt_text_sensor.cpp @@ -15,10 +15,12 @@ using namespace esphome::text_sensor; MQTTTextSensor::MQTTTextSensor(TextSensor *sensor) : sensor_(sensor) {} void MQTTTextSensor::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { - // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - if (!this->sensor_->get_device_class().empty()) { - root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class(); + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + const auto device_class = this->sensor_->get_device_class_ref(); + if (!device_class.empty()) { + root[MQTT_DEVICE_CLASS] = device_class; } + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) config.command_topic = false; } void MQTTTextSensor::setup() { diff --git a/esphome/components/mqtt/mqtt_update.cpp b/esphome/components/mqtt/mqtt_update.cpp index 5d4807c7f3..20f3a69a9e 100644 --- a/esphome/components/mqtt/mqtt_update.cpp +++ b/esphome/components/mqtt/mqtt_update.cpp @@ -20,7 +20,7 @@ void MQTTUpdateComponent::setup() { if (payload == "INSTALL") { this->update_->perform(); } else { - ESP_LOGW(TAG, "'%s': Received unknown update payload: %s", this->friendly_name().c_str(), payload.c_str()); + ESP_LOGW(TAG, "'%s': Received unknown update payload: %s", this->friendly_name_().c_str(), payload.c_str()); this->status_momentary_warning("state", 5000); } }); diff --git a/esphome/components/mqtt/mqtt_valve.cpp b/esphome/components/mqtt/mqtt_valve.cpp index 551398cf42..ae60670748 100644 --- a/esphome/components/mqtt/mqtt_valve.cpp +++ b/esphome/components/mqtt/mqtt_valve.cpp @@ -49,10 +49,12 @@ void MQTTValveComponent::dump_config() { } } void MQTTValveComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { - // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson - if (!this->valve_->get_device_class().empty()) { - root[MQTT_DEVICE_CLASS] = this->valve_->get_device_class(); + // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson + const auto device_class = this->valve_->get_device_class_ref(); + if (!device_class.empty()) { + root[MQTT_DEVICE_CLASS] = device_class; } + // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) auto traits = this->valve_->get_traits(); if (traits.get_is_assumed_state()) { From be006ecaddad5df7c2be5cfa3ffa2499dda5c203 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 12:31:19 -0600 Subject: [PATCH 4/8] [mdns] Eliminate redundant hostname copy to save heap memory (#11734) --- esphome/components/mdns/mdns_component.cpp | 4 +--- esphome/components/mdns/mdns_component.h | 1 - esphome/components/mdns/mdns_esp32.cpp | 6 ++++-- esphome/components/mdns/mdns_esp8266.cpp | 3 ++- esphome/components/mdns/mdns_libretiny.cpp | 3 ++- esphome/components/mdns/mdns_rp2040.cpp | 3 ++- 6 files changed, 11 insertions(+), 9 deletions(-) diff --git a/esphome/components/mdns/mdns_component.cpp b/esphome/components/mdns/mdns_component.cpp index d476136554..2c3150ff5d 100644 --- a/esphome/components/mdns/mdns_component.cpp +++ b/esphome/components/mdns/mdns_component.cpp @@ -37,8 +37,6 @@ MDNS_STATIC_CONST_CHAR(SERVICE_TCP, "_tcp"); MDNS_STATIC_CONST_CHAR(VALUE_VERSION, ESPHOME_VERSION); void MDNSComponent::compile_records_(StaticVector &services) { - this->hostname_ = App.get_name(); - // IMPORTANT: The #ifdef blocks below must match COMPONENTS_WITH_MDNS_SERVICES // in mdns/__init__.py. If you add a new service here, update both locations. @@ -179,7 +177,7 @@ void MDNSComponent::dump_config() { ESP_LOGCONFIG(TAG, "mDNS:\n" " Hostname: %s", - this->hostname_.c_str()); + App.get_name().c_str()); #ifdef USE_MDNS_STORE_SERVICES ESP_LOGV(TAG, " Services:"); for (const auto &service : this->services_) { diff --git a/esphome/components/mdns/mdns_component.h b/esphome/components/mdns/mdns_component.h index 35371fd739..f4237d5a69 100644 --- a/esphome/components/mdns/mdns_component.h +++ b/esphome/components/mdns/mdns_component.h @@ -76,7 +76,6 @@ class MDNSComponent : public Component { #ifdef USE_MDNS_STORE_SERVICES StaticVector services_{}; #endif - std::string hostname_; void compile_records_(StaticVector &services); }; diff --git a/esphome/components/mdns/mdns_esp32.cpp b/esphome/components/mdns/mdns_esp32.cpp index c02bfcbadb..ecdc926cc9 100644 --- a/esphome/components/mdns/mdns_esp32.cpp +++ b/esphome/components/mdns/mdns_esp32.cpp @@ -2,6 +2,7 @@ #if defined(USE_ESP32) && defined(USE_MDNS) #include +#include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" #include "mdns_component.h" @@ -27,8 +28,9 @@ void MDNSComponent::setup() { return; } - mdns_hostname_set(this->hostname_.c_str()); - mdns_instance_name_set(this->hostname_.c_str()); + const char *hostname = App.get_name().c_str(); + mdns_hostname_set(hostname); + mdns_instance_name_set(hostname); for (const auto &service : services) { auto txt_records = std::make_unique(service.txt_records.size()); diff --git a/esphome/components/mdns/mdns_esp8266.cpp b/esphome/components/mdns/mdns_esp8266.cpp index 25a3defa7b..9bbb406070 100644 --- a/esphome/components/mdns/mdns_esp8266.cpp +++ b/esphome/components/mdns/mdns_esp8266.cpp @@ -4,6 +4,7 @@ #include #include "esphome/components/network/ip_address.h" #include "esphome/components/network/util.h" +#include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" #include "mdns_component.h" @@ -20,7 +21,7 @@ void MDNSComponent::setup() { this->compile_records_(services); #endif - MDNS.begin(this->hostname_.c_str()); + MDNS.begin(App.get_name().c_str()); for (const auto &service : services) { // Strip the leading underscore from the proto and service_type. While it is diff --git a/esphome/components/mdns/mdns_libretiny.cpp b/esphome/components/mdns/mdns_libretiny.cpp index a3e317a2bf..fb2088f719 100644 --- a/esphome/components/mdns/mdns_libretiny.cpp +++ b/esphome/components/mdns/mdns_libretiny.cpp @@ -3,6 +3,7 @@ #include "esphome/components/network/ip_address.h" #include "esphome/components/network/util.h" +#include "esphome/core/application.h" #include "esphome/core/log.h" #include "mdns_component.h" @@ -20,7 +21,7 @@ void MDNSComponent::setup() { this->compile_records_(services); #endif - MDNS.begin(this->hostname_.c_str()); + MDNS.begin(App.get_name().c_str()); for (const auto &service : services) { // Strip the leading underscore from the proto and service_type. While it is diff --git a/esphome/components/mdns/mdns_rp2040.cpp b/esphome/components/mdns/mdns_rp2040.cpp index 791fa3934d..a9f5349f14 100644 --- a/esphome/components/mdns/mdns_rp2040.cpp +++ b/esphome/components/mdns/mdns_rp2040.cpp @@ -3,6 +3,7 @@ #include "esphome/components/network/ip_address.h" #include "esphome/components/network/util.h" +#include "esphome/core/application.h" #include "esphome/core/log.h" #include "mdns_component.h" @@ -20,7 +21,7 @@ void MDNSComponent::setup() { this->compile_records_(services); #endif - MDNS.begin(this->hostname_.c_str()); + MDNS.begin(App.get_name().c_str()); for (const auto &service : services) { // Strip the leading underscore from the proto and service_type. While it is From 00c0854323a79663a27243f05db911be9ad956e1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 12:50:35 -0600 Subject: [PATCH 5/8] [core] Deprecate get_icon(), get_device_class(), get_unit_of_measurement() and fix remaining non-MQTT usages (#11732) --- esphome/components/graph/graph.cpp | 4 ++-- esphome/components/prometheus/prometheus_handler.cpp | 2 +- esphome/components/sprinkler/sprinkler.cpp | 4 ++-- esphome/core/entity_base.h | 12 +++++++++--- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/esphome/components/graph/graph.cpp b/esphome/components/graph/graph.cpp index ac6ace96ee..88bb306408 100644 --- a/esphome/components/graph/graph.cpp +++ b/esphome/components/graph/graph.cpp @@ -235,7 +235,7 @@ void GraphLegend::init(Graph *g) { std::string valstr = value_accuracy_to_string(trace->sensor_->get_state(), trace->sensor_->get_accuracy_decimals()); if (this->units_) { - valstr += trace->sensor_->get_unit_of_measurement(); + valstr += trace->sensor_->get_unit_of_measurement_ref(); } this->font_value_->measure(valstr.c_str(), &fw, &fos, &fbl, &fh); if (fw > valw) @@ -371,7 +371,7 @@ void Graph::draw_legend(display::Display *buff, uint16_t x_offset, uint16_t y_of std::string valstr = value_accuracy_to_string(trace->sensor_->get_state(), trace->sensor_->get_accuracy_decimals()); if (legend_->units_) { - valstr += trace->sensor_->get_unit_of_measurement(); + valstr += trace->sensor_->get_unit_of_measurement_ref(); } buff->printf(xv, yv, legend_->font_value_, trace->get_line_color(), TextAlign::TOP_CENTER, "%s", valstr.c_str()); ESP_LOGV(TAG, " value: %s", valstr.c_str()); diff --git a/esphome/components/prometheus/prometheus_handler.cpp b/esphome/components/prometheus/prometheus_handler.cpp index 6e7ed6f79f..5cfcacf0cb 100644 --- a/esphome/components/prometheus/prometheus_handler.cpp +++ b/esphome/components/prometheus/prometheus_handler.cpp @@ -158,7 +158,7 @@ void PrometheusHandler::sensor_row_(AsyncResponseStream *stream, sensor::Sensor stream->print(ESPHOME_F("\",name=\"")); stream->print(relabel_name_(obj).c_str()); stream->print(ESPHOME_F("\",unit=\"")); - stream->print(obj->get_unit_of_measurement().c_str()); + stream->print(obj->get_unit_of_measurement_ref().c_str()); stream->print(ESPHOME_F("\"} ")); stream->print(value_accuracy_to_string(obj->state, obj->get_accuracy_decimals()).c_str()); stream->print(ESPHOME_F("\n")); diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index 7676e17468..8edb240a41 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -650,7 +650,7 @@ void Sprinkler::set_valve_run_duration(const optional valve_number, cons return; } auto call = this->valve_[valve_number.value()].run_duration_number->make_call(); - if (this->valve_[valve_number.value()].run_duration_number->traits.get_unit_of_measurement() == MIN_STR) { + if (this->valve_[valve_number.value()].run_duration_number->traits.get_unit_of_measurement_ref() == MIN_STR) { call.set_value(run_duration.value() / 60.0); } else { call.set_value(run_duration.value()); @@ -732,7 +732,7 @@ uint32_t Sprinkler::valve_run_duration(const size_t valve_number) { return 0; } if (this->valve_[valve_number].run_duration_number != nullptr) { - if (this->valve_[valve_number].run_duration_number->traits.get_unit_of_measurement() == MIN_STR) { + if (this->valve_[valve_number].run_duration_number->traits.get_unit_of_measurement_ref() == MIN_STR) { return static_cast(roundf(this->valve_[valve_number].run_duration_number->state * 60)); } else { return static_cast(roundf(this->valve_[valve_number].run_duration_number->state)); diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 80cd6b8e77..6e5362464f 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -61,7 +61,9 @@ class EntityBase { } // Get/set this entity's icon - std::string get_icon() const; + [[deprecated("Use get_icon_ref() instead for better performance (avoids string copy). Will stop working in ESPHome " + "2026.5.0")]] std::string + get_icon() const; void set_icon(const char *icon); StringRef get_icon_ref() const { static constexpr auto EMPTY_STRING = StringRef::from_lit(""); @@ -158,7 +160,9 @@ class EntityBase { class EntityBase_DeviceClass { // NOLINT(readability-identifier-naming) public: /// Get the device class, using the manual override if set. - std::string get_device_class(); + [[deprecated("Use get_device_class_ref() instead for better performance (avoids string copy). Will stop working in " + "ESPHome 2026.5.0")]] std::string + get_device_class(); /// Manually set the device class. void set_device_class(const char *device_class); /// Get the device class as StringRef @@ -174,7 +178,9 @@ class EntityBase_DeviceClass { // NOLINT(readability-identifier-naming) class EntityBase_UnitOfMeasurement { // NOLINT(readability-identifier-naming) public: /// Get the unit of measurement, using the manual override if set. - std::string get_unit_of_measurement(); + [[deprecated("Use get_unit_of_measurement_ref() instead for better performance (avoids string copy). Will stop " + "working in ESPHome 2026.5.0")]] std::string + get_unit_of_measurement(); /// Manually set the unit of measurement. void set_unit_of_measurement(const char *unit_of_measurement); /// Get the unit of measurement as StringRef From aa5795c019c055e08807f9943633d3a8582582e4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 13:17:34 -0600 Subject: [PATCH 6/8] [tests] Fix ID collision between bl0940 and nau7802 component tests (#11739) --- tests/components/bl0940/common.yaml | 6 +++--- tests/components/nau7802/common.yaml | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/components/bl0940/common.yaml b/tests/components/bl0940/common.yaml index 0b73fd6d55..e476ba10c0 100644 --- a/tests/components/bl0940/common.yaml +++ b/tests/components/bl0940/common.yaml @@ -1,11 +1,11 @@ button: - platform: bl0940 - bl0940_id: test_id + bl0940_id: bl0940_test_id name: Cal Reset sensor: - platform: bl0940 - id: test_id + id: bl0940_test_id voltage: name: BL0940 Voltage current: @@ -22,7 +22,7 @@ sensor: number: - platform: bl0940 id: bl0940_number_id - bl0940_id: test_id + bl0940_id: bl0940_test_id current_calibration: name: Cal Current min_value: -5 diff --git a/tests/components/nau7802/common.yaml b/tests/components/nau7802/common.yaml index 5c52c33dad..5251910df9 100644 --- a/tests/components/nau7802/common.yaml +++ b/tests/components/nau7802/common.yaml @@ -1,13 +1,13 @@ sensor: - platform: nau7802 i2c_id: i2c_bus - id: test_id + id: nau7802_test_id name: weight gain: 32 ldo_voltage: "3.0v" samples_per_second: 10 on_value: then: - - nau7802.calibrate_external_offset: test_id - - nau7802.calibrate_internal_offset: test_id - - nau7802.calibrate_gain: test_id + - nau7802.calibrate_external_offset: nau7802_test_id + - nau7802.calibrate_internal_offset: nau7802_test_id + - nau7802.calibrate_gain: nau7802_test_id From ce5e6088638d6b5cdc2becc34196205d3e03b2dc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Nov 2025 14:32:45 -0600 Subject: [PATCH 7/8] [ci] Skip memory impact analysis for release and beta branches (#11740) --- script/determine-jobs.py | 15 +++++++ script/helpers.py | 45 ++++++++++++++++--- tests/script/test_determine_jobs.py | 70 +++++++++++++++++++++++++++++ tests/script/test_helpers.py | 7 +++ 4 files changed, 130 insertions(+), 7 deletions(-) diff --git a/script/determine-jobs.py b/script/determine-jobs.py index 6f908b7150..e9d17d8fe5 100755 --- a/script/determine-jobs.py +++ b/script/determine-jobs.py @@ -63,6 +63,7 @@ from helpers import ( get_components_from_integration_fixtures, get_components_with_dependencies, get_cpp_changed_components, + get_target_branch, git_ls_files, parse_test_filename, root_path, @@ -471,6 +472,20 @@ def detect_memory_impact_config( - platform: platform name for the merged build - use_merged_config: "true" (always use merged config) """ + # Skip memory impact analysis for release* or beta* branches + # These branches typically contain many merged changes from dev, and building + # all components at once would produce nonsensical memory impact results. + # Memory impact analysis is most useful for focused PRs targeting dev. + target_branch = get_target_branch() + if target_branch and ( + target_branch.startswith("release") or target_branch.startswith("beta") + ): + print( + f"Memory impact: Skipping analysis for target branch {target_branch} " + f"(would try to build all components at once, giving nonsensical results)", + file=sys.stderr, + ) + return {"should_run": "false"} # Get actually changed files (not dependencies) files = changed_files(branch) diff --git a/script/helpers.py b/script/helpers.py index 5b2fe6cd06..1039ef39ac 100644 --- a/script/helpers.py +++ b/script/helpers.py @@ -196,6 +196,20 @@ def splitlines_no_ends(string: str) -> list[str]: return [s.strip() for s in string.splitlines()] +@cache +def _get_github_event_data() -> dict | None: + """Read and parse GitHub event file (cached). + + Returns: + Parsed event data dictionary, or None if not available + """ + github_event_path = os.environ.get("GITHUB_EVENT_PATH") + if github_event_path and os.path.exists(github_event_path): + with open(github_event_path) as f: + return json.load(f) + return None + + def _get_pr_number_from_github_env() -> str | None: """Extract PR number from GitHub environment variables. @@ -208,13 +222,30 @@ def _get_pr_number_from_github_env() -> str | None: return github_ref.split("/pull/")[1].split("/")[0] # Fallback to GitHub event file - github_event_path = os.environ.get("GITHUB_EVENT_PATH") - if github_event_path and os.path.exists(github_event_path): - with open(github_event_path) as f: - event_data = json.load(f) - pr_data = event_data.get("pull_request", {}) - if pr_number := pr_data.get("number"): - return str(pr_number) + if event_data := _get_github_event_data(): + pr_data = event_data.get("pull_request", {}) + if pr_number := pr_data.get("number"): + return str(pr_number) + + return None + + +def get_target_branch() -> str | None: + """Get the target branch from GitHub environment variables. + + Returns: + Target branch name (e.g., "dev", "release", "beta"), or None if not in PR context + """ + # First try GITHUB_BASE_REF (set for pull_request events) + if base_ref := os.environ.get("GITHUB_BASE_REF"): + return base_ref + + # Fallback to GitHub event file + if event_data := _get_github_event_data(): + pr_data = event_data.get("pull_request", {}) + base_data = pr_data.get("base", {}) + if ref := base_data.get("ref"): + return ref return None diff --git a/tests/script/test_determine_jobs.py b/tests/script/test_determine_jobs.py index e084e2e398..9f12d7ffcf 100644 --- a/tests/script/test_determine_jobs.py +++ b/tests/script/test_determine_jobs.py @@ -1240,3 +1240,73 @@ def test_detect_memory_impact_config_filters_incompatible_esp8266_on_esp32( ) assert result["use_merged_config"] == "true" + + +def test_detect_memory_impact_config_skips_release_branch(tmp_path: Path) -> None: + """Test that memory impact analysis is skipped for release* branches.""" + # Create test directory structure with components that have tests + tests_dir = tmp_path / "tests" / "components" + wifi_dir = tests_dir / "wifi" + wifi_dir.mkdir(parents=True) + (wifi_dir / "test.esp32-idf.yaml").write_text("test: wifi") + + with ( + patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object(helpers, "root_path", str(tmp_path)), + patch.object(determine_jobs, "changed_files") as mock_changed_files, + patch.object(determine_jobs, "get_target_branch", return_value="release"), + ): + mock_changed_files.return_value = ["esphome/components/wifi/wifi.cpp"] + determine_jobs._component_has_tests.cache_clear() + + result = determine_jobs.detect_memory_impact_config() + + # Memory impact should be skipped for release branch + assert result["should_run"] == "false" + + +def test_detect_memory_impact_config_skips_beta_branch(tmp_path: Path) -> None: + """Test that memory impact analysis is skipped for beta* branches.""" + # Create test directory structure with components that have tests + tests_dir = tmp_path / "tests" / "components" + wifi_dir = tests_dir / "wifi" + wifi_dir.mkdir(parents=True) + (wifi_dir / "test.esp32-idf.yaml").write_text("test: wifi") + + with ( + patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object(helpers, "root_path", str(tmp_path)), + patch.object(determine_jobs, "changed_files") as mock_changed_files, + patch.object(determine_jobs, "get_target_branch", return_value="beta"), + ): + mock_changed_files.return_value = ["esphome/components/wifi/wifi.cpp"] + determine_jobs._component_has_tests.cache_clear() + + result = determine_jobs.detect_memory_impact_config() + + # Memory impact should be skipped for beta branch + assert result["should_run"] == "false" + + +def test_detect_memory_impact_config_runs_for_dev_branch(tmp_path: Path) -> None: + """Test that memory impact analysis runs for dev branch.""" + # Create test directory structure with components that have tests + tests_dir = tmp_path / "tests" / "components" + wifi_dir = tests_dir / "wifi" + wifi_dir.mkdir(parents=True) + (wifi_dir / "test.esp32-idf.yaml").write_text("test: wifi") + + with ( + patch.object(determine_jobs, "root_path", str(tmp_path)), + patch.object(helpers, "root_path", str(tmp_path)), + patch.object(determine_jobs, "changed_files") as mock_changed_files, + patch.object(determine_jobs, "get_target_branch", return_value="dev"), + ): + mock_changed_files.return_value = ["esphome/components/wifi/wifi.cpp"] + determine_jobs._component_has_tests.cache_clear() + + result = determine_jobs.detect_memory_impact_config() + + # Memory impact should run for dev branch + assert result["should_run"] == "true" + assert result["components"] == ["wifi"] diff --git a/tests/script/test_helpers.py b/tests/script/test_helpers.py index 1bfffef51c..c51273f298 100644 --- a/tests/script/test_helpers.py +++ b/tests/script/test_helpers.py @@ -31,6 +31,13 @@ print_file_list = helpers.print_file_list get_all_dependencies = helpers.get_all_dependencies +@pytest.fixture(autouse=True) +def clear_helpers_cache() -> None: + """Clear cached functions before each test.""" + helpers._get_github_event_data.cache_clear() + helpers._get_changed_files_github_actions.cache_clear() + + @pytest.mark.parametrize( ("github_ref", "expected_pr_number"), [ From 20b6e0d5c239d949a89f61a528733772f5c3166b Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Thu, 6 Nov 2025 09:37:38 +1000 Subject: [PATCH 8/8] [lvgl] Allow text substitution for NaN (#11712) --- esphome/components/lvgl/helpers.py | 39 ++++++++++++++++++++---- esphome/components/lvgl/lv_validation.py | 16 ++++++++-- esphome/components/lvgl/schemas.py | 3 +- tests/components/lvgl/lvgl-package.yaml | 6 ++++ 4 files changed, 55 insertions(+), 9 deletions(-) diff --git a/esphome/components/lvgl/helpers.py b/esphome/components/lvgl/helpers.py index 8d5b6354bb..c2bd58f71c 100644 --- a/esphome/components/lvgl/helpers.py +++ b/esphome/components/lvgl/helpers.py @@ -3,6 +3,8 @@ import re from esphome import config_validation as cv from esphome.const import CONF_ARGS, CONF_FORMAT +CONF_IF_NAN = "if_nan" + lv_uses = { "USER_DATA", "LOG", @@ -21,23 +23,48 @@ lv_fonts_used = set() esphome_fonts_used = set() lvgl_components_required = set() - -def validate_printf(value): - cfmt = r""" +# noqa +f_regex = re.compile( + r""" ( # start of capture group 1 % # literal "%" - (?:[-+0 #]{0,5}) # optional flags + [-+0 #]{0,5} # optional flags + (?:\d+|\*)? # width + (?:\.(?:\d+|\*))? # precision + (?:h|l|ll|w|I|I32|I64)? # size + f # type + ) + """, + flags=re.VERBOSE, +) +# noqa +c_regex = re.compile( + r""" + ( # start of capture group 1 + % # literal "%" + [-+0 #]{0,5} # optional flags (?:\d+|\*)? # width (?:\.(?:\d+|\*))? # precision (?:h|l|ll|w|I|I32|I64)? # size [cCdiouxXeEfgGaAnpsSZ] # type ) - """ # noqa - matches = re.findall(cfmt, value[CONF_FORMAT], flags=re.VERBOSE) + """, + flags=re.VERBOSE, +) + + +def validate_printf(value): + format_string = value[CONF_FORMAT] + matches = c_regex.findall(format_string) if len(matches) != len(value[CONF_ARGS]): raise cv.Invalid( f"Found {len(matches)} printf-patterns ({', '.join(matches)}), but {len(value[CONF_ARGS])} args were given!" ) + + if value.get(CONF_IF_NAN) and len(f_regex.findall(format_string)) != 1: + raise cv.Invalid( + "Use of 'if_nan' requires a single valid printf-pattern of type %f" + ) return value diff --git a/esphome/components/lvgl/lv_validation.py b/esphome/components/lvgl/lv_validation.py index 9fe72128ce..045258555c 100644 --- a/esphome/components/lvgl/lv_validation.py +++ b/esphome/components/lvgl/lv_validation.py @@ -33,7 +33,13 @@ from .defines import ( call_lambda, literal, ) -from .helpers import add_lv_use, esphome_fonts_used, lv_fonts_used, requires_component +from .helpers import ( + CONF_IF_NAN, + add_lv_use, + esphome_fonts_used, + lv_fonts_used, + requires_component, +) from .types import lv_font_t, lv_gradient_t opacity_consts = LvConstant("LV_OPA_", "TRANSP", "COVER") @@ -412,7 +418,13 @@ class TextValidator(LValidator): str_args = [str(x) for x in value[CONF_ARGS]] arg_expr = cg.RawExpression(",".join(str_args)) format_str = cpp_string_escape(format_str) - return literal(f"str_sprintf({format_str}, {arg_expr}).c_str()") + sprintf_str = f"str_sprintf({format_str}, {arg_expr}).c_str()" + if nanval := value.get(CONF_IF_NAN): + nanval = cpp_string_escape(nanval) + return literal( + f"(std::isfinite({arg_expr}) ? {sprintf_str} : {nanval})" + ) + return literal(sprintf_str) if time_format := value.get(CONF_TIME_FORMAT): source = value[CONF_TIME] if isinstance(source, Lambda): diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index dd248d0b94..0dcf420f24 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -20,7 +20,7 @@ from esphome.core.config import StartupTrigger from . import defines as df, lv_validation as lvalid from .defines import CONF_TIME_FORMAT, LV_GRAD_DIR -from .helpers import requires_component, validate_printf +from .helpers import CONF_IF_NAN, requires_component, validate_printf from .layout import ( FLEX_OBJ_SCHEMA, GRID_CELL_SCHEMA, @@ -54,6 +54,7 @@ PRINTF_TEXT_SCHEMA = cv.All( { cv.Required(CONF_FORMAT): cv.string, cv.Optional(CONF_ARGS, default=list): cv.ensure_list(cv.lambda_), + cv.Optional(CONF_IF_NAN): cv.string, }, ), validate_printf, diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 8ac9a60e2d..b122d10f04 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -726,6 +726,12 @@ lvgl: - logger.log: format: "Spinbox value is %f" args: [x] + - lvgl.label.update: + id: hello_label + text: + format: "value is %.1f now" + args: [x] + if_nan: "Value unknown" - button: styles: spin_button id: spin_down