From 86a1b4cf694026ea77f2c37d406b55e84203b122 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 18 Jan 2026 19:51:11 -1000 Subject: [PATCH 01/19] [select][fan] Use StringRef for on_value/on_preset_set triggers to avoid heap allocation (#13324) --- esphome/codegen.py | 1 + esphome/components/fan/__init__.py | 4 +- esphome/components/fan/automation.h | 4 +- esphome/components/select/__init__.py | 4 +- esphome/components/select/automation.h | 4 +- esphome/core/string_ref.h | 64 ++++++++ esphome/cpp_types.py | 1 + .../fixtures/select_stringref_trigger.yaml | 85 +++++++++++ .../test_select_stringref_trigger.py | 143 ++++++++++++++++++ 9 files changed, 302 insertions(+), 8 deletions(-) create mode 100644 tests/integration/fixtures/select_stringref_trigger.yaml create mode 100644 tests/integration/test_select_stringref_trigger.py diff --git a/esphome/codegen.py b/esphome/codegen.py index 6d55c6023d2..4a2a5975c67 100644 --- a/esphome/codegen.py +++ b/esphome/codegen.py @@ -69,6 +69,7 @@ from esphome.cpp_types import ( # noqa: F401 JsonObjectConst, Parented, PollingComponent, + StringRef, arduino_json_ns, bool_, const_char_ptr, diff --git a/esphome/components/fan/__init__.py b/esphome/components/fan/__init__.py index 35a351e8f10..6010aa8ed46 100644 --- a/esphome/components/fan/__init__.py +++ b/esphome/components/fan/__init__.py @@ -77,7 +77,7 @@ FanSpeedSetTrigger = fan_ns.class_( "FanSpeedSetTrigger", automation.Trigger.template(cg.int_) ) FanPresetSetTrigger = fan_ns.class_( - "FanPresetSetTrigger", automation.Trigger.template(cg.std_string) + "FanPresetSetTrigger", automation.Trigger.template(cg.StringRef) ) FanIsOnCondition = fan_ns.class_("FanIsOnCondition", automation.Condition.template()) @@ -287,7 +287,7 @@ async def setup_fan_core_(var, config): await automation.build_automation(trigger, [(cg.int_, "x")], conf) for conf in config.get(CONF_ON_PRESET_SET, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [(cg.std_string, "x")], conf) + await automation.build_automation(trigger, [(cg.StringRef, "x")], conf) async def register_fan(var, config): diff --git a/esphome/components/fan/automation.h b/esphome/components/fan/automation.h index 77abc2f13ff..3c3b0ce519e 100644 --- a/esphome/components/fan/automation.h +++ b/esphome/components/fan/automation.h @@ -208,7 +208,7 @@ class FanSpeedSetTrigger : public Trigger { int last_speed_; }; -class FanPresetSetTrigger : public Trigger { +class FanPresetSetTrigger : public Trigger { public: FanPresetSetTrigger(Fan *state) { state->add_on_state_callback([this, state]() { @@ -216,7 +216,7 @@ class FanPresetSetTrigger : public Trigger { auto should_trigger = preset_mode != this->last_preset_mode_; this->last_preset_mode_ = preset_mode; if (should_trigger) { - this->trigger(std::string(preset_mode)); + this->trigger(preset_mode); } }); this->last_preset_mode_ = state->get_preset_mode(); diff --git a/esphome/components/select/__init__.py b/esphome/components/select/__init__.py index c51131a2922..84ad591ba13 100644 --- a/esphome/components/select/__init__.py +++ b/esphome/components/select/__init__.py @@ -33,7 +33,7 @@ SelectPtr = Select.operator("ptr") # Triggers SelectStateTrigger = select_ns.class_( "SelectStateTrigger", - automation.Trigger.template(cg.std_string, cg.size_t), + automation.Trigger.template(cg.StringRef, cg.size_t), ) # Actions @@ -100,7 +100,7 @@ async def setup_select_core_(var, config, *, options: list[str]): for conf in config.get(CONF_ON_VALUE, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) await automation.build_automation( - trigger, [(cg.std_string, "x"), (cg.size_t, "i")], conf + trigger, [(cg.StringRef, "x"), (cg.size_t, "i")], conf ) if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: diff --git a/esphome/components/select/automation.h b/esphome/components/select/automation.h index 81e8a3561db..ffdabd5f7c1 100644 --- a/esphome/components/select/automation.h +++ b/esphome/components/select/automation.h @@ -6,11 +6,11 @@ namespace esphome::select { -class SelectStateTrigger : public Trigger { +class SelectStateTrigger : public Trigger { public: explicit SelectStateTrigger(Select *parent) : parent_(parent) { parent->add_on_state_callback( - [this](size_t index) { this->trigger(std::string(this->parent_->option_at(index)), index); }); + [this](size_t index) { this->trigger(StringRef(this->parent_->option_at(index)), index); }); } protected: diff --git a/esphome/core/string_ref.h b/esphome/core/string_ref.h index 44ca79c81b0..d502c4d27fa 100644 --- a/esphome/core/string_ref.h +++ b/esphome/core/string_ref.h @@ -72,6 +72,7 @@ class StringRef { constexpr const char *c_str() const { return base_; } constexpr size_type size() const { return len_; } + constexpr size_type length() const { return len_; } constexpr bool empty() const { return len_ == 0; } constexpr const_reference operator[](size_type pos) const { return *(base_ + pos); } @@ -80,6 +81,32 @@ class StringRef { operator std::string() const { return str(); } + /// Find first occurrence of substring, returns std::string::npos if not found. + /// Note: Requires the underlying string to be null-terminated. + size_type find(const char *s, size_type pos = 0) const { + if (pos >= len_) + return std::string::npos; + const char *result = std::strstr(base_ + pos, s); + // Verify entire match is within bounds (strstr searches to null terminator) + if (result && result + std::strlen(s) <= base_ + len_) + return static_cast(result - base_); + return std::string::npos; + } + size_type find(char c, size_type pos = 0) const { + if (pos >= len_) + return std::string::npos; + const void *result = std::memchr(base_ + pos, static_cast(c), len_ - pos); + return result ? static_cast(static_cast(result) - base_) : std::string::npos; + } + + /// Return substring as std::string + std::string substr(size_type pos = 0, size_type count = std::string::npos) const { + if (pos >= len_) + return std::string(); + size_type actual_count = (count == std::string::npos || pos + count > len_) ? len_ - pos : count; + return std::string(base_ + pos, actual_count); + } + private: const char *base_; size_type len_; @@ -160,6 +187,43 @@ inline std::string operator+(const std::string &lhs, const StringRef &rhs) { str.append(rhs.c_str(), rhs.size()); return str; } +// String conversion functions for ADL compatibility (allows stoi(x) where x is StringRef) +// Must be in esphome namespace for ADL to find them. Uses strtol/strtod directly to avoid heap allocation. +namespace internal { +// NOLINTBEGIN(google-runtime-int) +template inline R parse_number(const StringRef &str, size_t *pos, F conv) { + char *end; + R result = conv(str.c_str(), &end); + // Set pos to 0 on conversion failure (when no characters consumed), otherwise index after number + if (pos) + *pos = (end == str.c_str()) ? 0 : static_cast(end - str.c_str()); + return result; +} +template inline R parse_number(const StringRef &str, size_t *pos, int base, F conv) { + char *end; + R result = conv(str.c_str(), &end, base); + // Set pos to 0 on conversion failure (when no characters consumed), otherwise index after number + if (pos) + *pos = (end == str.c_str()) ? 0 : static_cast(end - str.c_str()); + return result; +} +// NOLINTEND(google-runtime-int) +} // namespace internal +// NOLINTBEGIN(readability-identifier-naming,google-runtime-int) +inline int stoi(const StringRef &str, size_t *pos = nullptr, int base = 10) { + return static_cast(internal::parse_number(str, pos, base, std::strtol)); +} +inline long stol(const StringRef &str, size_t *pos = nullptr, int base = 10) { + return internal::parse_number(str, pos, base, std::strtol); +} +inline float stof(const StringRef &str, size_t *pos = nullptr) { + return internal::parse_number(str, pos, std::strtof); +} +inline double stod(const StringRef &str, size_t *pos = nullptr) { + return internal::parse_number(str, pos, std::strtod); +} +// NOLINTEND(readability-identifier-naming,google-runtime-int) + #ifdef USE_JSON // NOLINTNEXTLINE(readability-identifier-naming) inline void convertToJson(const StringRef &src, JsonVariant dst) { dst.set(src.c_str()); } diff --git a/esphome/cpp_types.py b/esphome/cpp_types.py index 0d1813f63b5..7001c38857a 100644 --- a/esphome/cpp_types.py +++ b/esphome/cpp_types.py @@ -44,3 +44,4 @@ gpio_Flags = gpio_ns.enum("Flags", is_class=True) EntityCategory = esphome_ns.enum("EntityCategory") Parented = esphome_ns.class_("Parented") ESPTime = esphome_ns.struct("ESPTime") +StringRef = esphome_ns.class_("StringRef") diff --git a/tests/integration/fixtures/select_stringref_trigger.yaml b/tests/integration/fixtures/select_stringref_trigger.yaml new file mode 100644 index 00000000000..bb1e1fd8433 --- /dev/null +++ b/tests/integration/fixtures/select_stringref_trigger.yaml @@ -0,0 +1,85 @@ +esphome: + name: select-stringref-test + friendly_name: Select StringRef Test + +host: + +logger: + level: DEBUG + +api: + +select: + - platform: template + name: "Test Select" + id: test_select + optimistic: true + options: + - "Option A" + - "Option B" + - "Option C" + initial_option: "Option A" + on_value: + then: + # Test 1: Log the value directly (StringRef -> const char* via c_str()) + - logger.log: + format: "Select value: %s" + args: ['x.c_str()'] + # Test 2: String concatenation (StringRef + const char* -> std::string) + - lambda: |- + std::string with_suffix = x + " selected"; + ESP_LOGI("test", "Concatenated: %s", with_suffix.c_str()); + # Test 3: Comparison (StringRef == const char*) + - lambda: |- + if (x == "Option B") { + ESP_LOGI("test", "Option B was selected"); + } + # Test 4: Use index parameter (variable name is 'i') + - lambda: |- + ESP_LOGI("test", "Select index: %d", (int)i); + # Test 5: StringRef.length() method + - lambda: |- + ESP_LOGI("test", "Length: %d", (int)x.length()); + # Test 6: StringRef.find() method with substring + - lambda: |- + if (x.find("Option") != std::string::npos) { + ESP_LOGI("test", "Found 'Option' in value"); + } + # Test 7: StringRef.find() method with character + - lambda: |- + size_t space_pos = x.find(' '); + if (space_pos != std::string::npos) { + ESP_LOGI("test", "Space at position: %d", (int)space_pos); + } + # Test 8: StringRef.substr() method + - lambda: |- + std::string prefix = x.substr(0, 6); + ESP_LOGI("test", "Substr prefix: %s", prefix.c_str()); + + # Second select with numeric options to test ADL functions + - platform: template + name: "Baud Rate" + id: baud_select + optimistic: true + options: + - "9600" + - "115200" + initial_option: "9600" + on_value: + then: + # Test 9: stoi via ADL + - lambda: |- + int baud = stoi(x); + ESP_LOGI("test", "stoi result: %d", baud); + # Test 10: stol via ADL + - lambda: |- + long baud_long = stol(x); + ESP_LOGI("test", "stol result: %ld", baud_long); + # Test 11: stof via ADL + - lambda: |- + float baud_float = stof(x); + ESP_LOGI("test", "stof result: %.0f", baud_float); + # Test 12: stod via ADL + - lambda: |- + double baud_double = stod(x); + ESP_LOGI("test", "stod result: %.0f", baud_double); diff --git a/tests/integration/test_select_stringref_trigger.py b/tests/integration/test_select_stringref_trigger.py new file mode 100644 index 00000000000..7fc72a22901 --- /dev/null +++ b/tests/integration/test_select_stringref_trigger.py @@ -0,0 +1,143 @@ +"""Integration test for select on_value trigger with StringRef parameter.""" + +from __future__ import annotations + +import asyncio +import re + +import pytest + +from .types import APIClientConnectedFactory, RunCompiledFunction + + +@pytest.mark.asyncio +async def test_select_stringref_trigger( + yaml_config: str, + run_compiled: RunCompiledFunction, + api_client_connected: APIClientConnectedFactory, +) -> None: + """Test select on_value trigger passes StringRef that works with string operations.""" + loop = asyncio.get_running_loop() + + # Track log messages to verify StringRef operations work + value_logged_future = loop.create_future() + concatenated_future = loop.create_future() + comparison_future = loop.create_future() + index_logged_future = loop.create_future() + length_future = loop.create_future() + find_substr_future = loop.create_future() + find_char_future = loop.create_future() + substr_future = loop.create_future() + # ADL functions + stoi_future = loop.create_future() + stol_future = loop.create_future() + stof_future = loop.create_future() + stod_future = loop.create_future() + + # Patterns to match in logs + value_pattern = re.compile(r"Select value: Option B") + concatenated_pattern = re.compile(r"Concatenated: Option B selected") + comparison_pattern = re.compile(r"Option B was selected") + index_pattern = re.compile(r"Select index: 1") + length_pattern = re.compile(r"Length: 8") # "Option B" is 8 chars + find_substr_pattern = re.compile(r"Found 'Option' in value") + find_char_pattern = re.compile(r"Space at position: 6") # space at index 6 + substr_pattern = re.compile(r"Substr prefix: Option") + # ADL function patterns (115200 from baud rate select) + stoi_pattern = re.compile(r"stoi result: 115200") + stol_pattern = re.compile(r"stol result: 115200") + stof_pattern = re.compile(r"stof result: 115200") + stod_pattern = re.compile(r"stod result: 115200") + + def check_output(line: str) -> None: + """Check log output for expected messages.""" + if not value_logged_future.done() and value_pattern.search(line): + value_logged_future.set_result(True) + if not concatenated_future.done() and concatenated_pattern.search(line): + concatenated_future.set_result(True) + if not comparison_future.done() and comparison_pattern.search(line): + comparison_future.set_result(True) + if not index_logged_future.done() and index_pattern.search(line): + index_logged_future.set_result(True) + if not length_future.done() and length_pattern.search(line): + length_future.set_result(True) + if not find_substr_future.done() and find_substr_pattern.search(line): + find_substr_future.set_result(True) + if not find_char_future.done() and find_char_pattern.search(line): + find_char_future.set_result(True) + if not substr_future.done() and substr_pattern.search(line): + substr_future.set_result(True) + # ADL functions + if not stoi_future.done() and stoi_pattern.search(line): + stoi_future.set_result(True) + if not stol_future.done() and stol_pattern.search(line): + stol_future.set_result(True) + if not stof_future.done() and stof_pattern.search(line): + stof_future.set_result(True) + if not stod_future.done() and stod_pattern.search(line): + stod_future.set_result(True) + + async with ( + run_compiled(yaml_config, line_callback=check_output), + api_client_connected() as client, + ): + # Verify device info + device_info = await client.device_info() + assert device_info is not None + assert device_info.name == "select-stringref-test" + + # List entities to find our select + entities, _ = await client.list_entities_services() + + select_entity = next( + (e for e in entities if hasattr(e, "options") and e.name == "Test Select"), + None, + ) + assert select_entity is not None, "Test Select entity not found" + + baud_entity = next( + (e for e in entities if hasattr(e, "options") and e.name == "Baud Rate"), + None, + ) + assert baud_entity is not None, "Baud Rate entity not found" + + # Change select to Option B - this should trigger on_value with StringRef + client.select_command(select_entity.key, "Option B") + # Change baud to 115200 - this tests ADL functions (stoi, stol, stof, stod) + client.select_command(baud_entity.key, "115200") + + # Wait for all log messages confirming StringRef operations work + try: + await asyncio.wait_for( + asyncio.gather( + value_logged_future, + concatenated_future, + comparison_future, + index_logged_future, + length_future, + find_substr_future, + find_char_future, + substr_future, + stoi_future, + stol_future, + stof_future, + stod_future, + ), + timeout=5.0, + ) + except TimeoutError: + results = { + "value_logged": value_logged_future.done(), + "concatenated": concatenated_future.done(), + "comparison": comparison_future.done(), + "index_logged": index_logged_future.done(), + "length": length_future.done(), + "find_substr": find_substr_future.done(), + "find_char": find_char_future.done(), + "substr": substr_future.done(), + "stoi": stoi_future.done(), + "stol": stol_future.done(), + "stof": stof_future.done(), + "stod": stod_future.done(), + } + pytest.fail(f"StringRef operations failed - received: {results}") From bfcc0e26a367625df7db7165172a4f0846b39421 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 18 Jan 2026 22:22:44 -1000 Subject: [PATCH 02/19] [dfrobot_sen0395][pipsolar][sim800l][wl_134] Replace sprintf with snprintf/buf_append_printf (#13301) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/dfrobot_sen0395/commands.h | 8 ++++---- esphome/components/pipsolar/output/pipsolar_output.cpp | 2 +- esphome/components/sim800l/sim800l.cpp | 5 +++-- esphome/components/wl_134/wl_134.cpp | 5 +++-- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/esphome/components/dfrobot_sen0395/commands.h b/esphome/components/dfrobot_sen0395/commands.h index cf3ba50be0a..3b0551b1843 100644 --- a/esphome/components/dfrobot_sen0395/commands.h +++ b/esphome/components/dfrobot_sen0395/commands.h @@ -75,8 +75,8 @@ class SetLatencyCommand : public Command { class SensorCfgStartCommand : public Command { public: SensorCfgStartCommand(bool startup_mode) : startup_mode_(startup_mode) { - char tmp_cmd[20] = {0}; - sprintf(tmp_cmd, "sensorCfgStart %d", startup_mode); + char tmp_cmd[20]; // "sensorCfgStart " (15) + "0/1" (1) + null = 17 + buf_append_printf(tmp_cmd, sizeof(tmp_cmd), 0, "sensorCfgStart %d", startup_mode); cmd_ = std::string(tmp_cmd); } uint8_t on_message(std::string &message) override; @@ -142,8 +142,8 @@ class SensitivityCommand : public Command { SensitivityCommand(uint8_t sensitivity) : sensitivity_(sensitivity) { if (sensitivity > 9) sensitivity_ = sensitivity = 9; - char tmp_cmd[20] = {0}; - sprintf(tmp_cmd, "setSensitivity %d", sensitivity); + char tmp_cmd[20]; // "setSensitivity " (15) + "0-9" (1) + null = 17 + buf_append_printf(tmp_cmd, sizeof(tmp_cmd), 0, "setSensitivity %d", sensitivity); cmd_ = std::string(tmp_cmd); }; uint8_t on_message(std::string &message) override; diff --git a/esphome/components/pipsolar/output/pipsolar_output.cpp b/esphome/components/pipsolar/output/pipsolar_output.cpp index ebfb9a7bbc9..60f63427591 100644 --- a/esphome/components/pipsolar/output/pipsolar_output.cpp +++ b/esphome/components/pipsolar/output/pipsolar_output.cpp @@ -8,7 +8,7 @@ namespace pipsolar { static const char *const TAG = "pipsolar.output"; void PipsolarOutput::write_state(float state) { - char tmp[10]; + char tmp[16]; snprintf(tmp, sizeof(tmp), this->set_command_, state); if (std::find(this->possible_values_.begin(), this->possible_values_.end(), state) != this->possible_values_.end()) { diff --git a/esphome/components/sim800l/sim800l.cpp b/esphome/components/sim800l/sim800l.cpp index e3edda0e72c..251e18648b6 100644 --- a/esphome/components/sim800l/sim800l.cpp +++ b/esphome/components/sim800l/sim800l.cpp @@ -1,4 +1,5 @@ #include "sim800l.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #include @@ -50,8 +51,8 @@ void Sim800LComponent::update() { } else if (state_ == STATE_RECEIVED_SMS) { // Serial Buffer should have flushed. // Send cmd to delete received sms - char delete_cmd[20]; - sprintf(delete_cmd, "AT+CMGD=%d", this->parse_index_); + char delete_cmd[20]; // "AT+CMGD=" (8) + uint8_t (max 3) + null = 12 <= 20 + buf_append_printf(delete_cmd, sizeof(delete_cmd), 0, "AT+CMGD=%d", this->parse_index_); this->send_cmd_(delete_cmd); this->state_ = STATE_CHECK_SMS; this->expect_ack_ = true; diff --git a/esphome/components/wl_134/wl_134.cpp b/esphome/components/wl_134/wl_134.cpp index 20a145d1839..a589f71c84c 100644 --- a/esphome/components/wl_134/wl_134.cpp +++ b/esphome/components/wl_134/wl_134.cpp @@ -1,4 +1,5 @@ #include "wl_134.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #include @@ -78,8 +79,8 @@ Wl134Component::Rfid134Error Wl134Component::read_packet_() { reading.id, reading.country, reading.isData ? "true" : "false", reading.isAnimal ? "true" : "false", reading.reserved0, reading.reserved1); - char buf[20]; - sprintf(buf, "%03d%012lld", reading.country, reading.id); + char buf[20]; // "%03d" (3) + "%012" PRId64 (12) + null = 16 max + buf_append_printf(buf, sizeof(buf), 0, "%03d%012" PRId64, reading.country, reading.id); this->publish_state(buf); if (this->do_reset_) { this->set_timeout(1000, [this]() { this->publish_state(""); }); From f8bd4ef57d67127914affeff02f1da6265f50c2a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 18 Jan 2026 22:22:57 -1000 Subject: [PATCH 03/19] [template][event] Use StringRef for set_action and on_event triggers (#13328) --- esphome/components/event/__init__.py | 4 +--- esphome/components/event/automation.h | 4 ++-- esphome/components/event/event.cpp | 4 ++-- esphome/components/event/event.h | 4 ++-- esphome/components/template/select/__init__.py | 2 +- esphome/components/template/select/template_select.cpp | 2 +- esphome/components/template/select/template_select.h | 5 +++-- 7 files changed, 12 insertions(+), 13 deletions(-) diff --git a/esphome/components/event/__init__.py b/esphome/components/event/__init__.py index e2b69ba8721..8fac7a279c4 100644 --- a/esphome/components/event/__init__.py +++ b/esphome/components/event/__init__.py @@ -90,9 +90,7 @@ async def setup_event_core_(var, config, *, event_types: list[str]): for conf in config.get(CONF_ON_EVENT, []): trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation( - trigger, [(cg.std_string, "event_type")], conf - ) + await automation.build_automation(trigger, [(cg.StringRef, "event_type")], conf) cg.add(var.set_event_types(event_types)) diff --git a/esphome/components/event/automation.h b/esphome/components/event/automation.h index 5bdba186871..7730506c108 100644 --- a/esphome/components/event/automation.h +++ b/esphome/components/event/automation.h @@ -14,10 +14,10 @@ template class TriggerEventAction : public Action, public void play(const Ts &...x) override { this->parent_->trigger(this->event_type_.value(x...)); } }; -class EventTrigger : public Trigger { +class EventTrigger : public Trigger { public: EventTrigger(Event *event) { - event->add_on_event_callback([this](const std::string &event_type) { this->trigger(event_type); }); + event->add_on_event_callback([this](StringRef event_type) { this->trigger(event_type); }); } }; diff --git a/esphome/components/event/event.cpp b/esphome/components/event/event.cpp index 8015f2255a0..667d4218f3c 100644 --- a/esphome/components/event/event.cpp +++ b/esphome/components/event/event.cpp @@ -23,7 +23,7 @@ void Event::trigger(const std::string &event_type) { } this->last_event_type_ = found; ESP_LOGD(TAG, "'%s' >> '%s'", this->get_name().c_str(), this->last_event_type_); - this->event_callback_.call(event_type); + this->event_callback_.call(StringRef(found)); #if defined(USE_EVENT) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_event(this); #endif @@ -45,7 +45,7 @@ void Event::set_event_types(const std::vector &event_types) { this->last_event_type_ = nullptr; // Reset when types change } -void Event::add_on_event_callback(std::function &&callback) { +void Event::add_on_event_callback(std::function &&callback) { this->event_callback_.add(std::move(callback)); } diff --git a/esphome/components/event/event.h b/esphome/components/event/event.h index f77ad326d97..b5519a05202 100644 --- a/esphome/components/event/event.h +++ b/esphome/components/event/event.h @@ -70,10 +70,10 @@ class Event : public EntityBase, public EntityBase_DeviceClass { /// Check if an event has been triggered. bool has_event() const { return this->last_event_type_ != nullptr; } - void add_on_event_callback(std::function &&callback); + void add_on_event_callback(std::function &&callback); protected: - LazyCallbackManager event_callback_; + LazyCallbackManager event_callback_; FixedVector types_; private: diff --git a/esphome/components/template/select/__init__.py b/esphome/components/template/select/__init__.py index 0e9c240547b..574f1f5fb7c 100644 --- a/esphome/components/template/select/__init__.py +++ b/esphome/components/template/select/__init__.py @@ -88,5 +88,5 @@ async def to_code(config): if CONF_SET_ACTION in config: await automation.build_automation( - var.get_set_trigger(), [(cg.std_string, "x")], config[CONF_SET_ACTION] + var.get_set_trigger(), [(cg.StringRef, "x")], config[CONF_SET_ACTION] ) diff --git a/esphome/components/template/select/template_select.cpp b/esphome/components/template/select/template_select.cpp index 9d2df0956b1..818abfc1d7c 100644 --- a/esphome/components/template/select/template_select.cpp +++ b/esphome/components/template/select/template_select.cpp @@ -41,7 +41,7 @@ void TemplateSelect::update() { } void TemplateSelect::control(size_t index) { - this->set_trigger_->trigger(std::string(this->option_at(index))); + this->set_trigger_->trigger(StringRef(this->option_at(index))); if (this->optimistic_) this->publish_state(index); diff --git a/esphome/components/template/select/template_select.h b/esphome/components/template/select/template_select.h index 2757c514053..114d25b9ce1 100644 --- a/esphome/components/template/select/template_select.h +++ b/esphome/components/template/select/template_select.h @@ -4,6 +4,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/preferences.h" +#include "esphome/core/string_ref.h" #include "esphome/core/template_lambda.h" namespace esphome::template_ { @@ -17,7 +18,7 @@ class TemplateSelect final : public select::Select, public PollingComponent { void dump_config() override; float get_setup_priority() const override { return setup_priority::HARDWARE; } - Trigger *get_set_trigger() const { return this->set_trigger_; } + Trigger *get_set_trigger() const { return this->set_trigger_; } void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void set_initial_option_index(size_t initial_option_index) { this->initial_option_index_ = initial_option_index; } void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; } @@ -27,7 +28,7 @@ class TemplateSelect final : public select::Select, public PollingComponent { bool optimistic_ = false; size_t initial_option_index_{0}; bool restore_value_ = false; - Trigger *set_trigger_ = new Trigger(); + Trigger *set_trigger_ = new Trigger(); TemplateLambda f_; ESPPreferenceObject pref_; From 892e9b006fa4c9fed028eeda51af08fbfa027e55 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 18 Jan 2026 23:57:27 -1000 Subject: [PATCH 04/19] [api] Use MAX_STATE_LEN constant for Home Assistant state buffer (#13278) --- esphome/components/api/api_connection.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 25512de4c7c..0364879ccd7 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1715,7 +1715,7 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes // HA state max length is 255 characters, but attributes can be much longer // Use stack buffer for common case (states), heap fallback for large attributes size_t state_len = msg.state.size(); - SmallBufferWithHeapFallback<256> state_buf_alloc(state_len + 1); + SmallBufferWithHeapFallback state_buf_alloc(state_len + 1); char *state_buf = reinterpret_cast(state_buf_alloc.get()); if (state_len > 0) { memcpy(state_buf, msg.state.c_str(), state_len); From ee264d0fd444c74be13bdb683a1f49c70617762b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 18 Jan 2026 23:57:42 -1000 Subject: [PATCH 05/19] [anova] Replace sprintf with bounds-checked alternatives (#13303) --- esphome/components/anova/anova_base.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/esphome/components/anova/anova_base.cpp b/esphome/components/anova/anova_base.cpp index ce4febbe379..fef4f1d8526 100644 --- a/esphome/components/anova/anova_base.cpp +++ b/esphome/components/anova/anova_base.cpp @@ -18,31 +18,31 @@ AnovaPacket *AnovaCodec::clean_packet_() { AnovaPacket *AnovaCodec::get_read_device_status_request() { this->current_query_ = READ_DEVICE_STATUS; - sprintf((char *) this->packet_.data, "%s", CMD_READ_DEVICE_STATUS); + snprintf((char *) this->packet_.data, sizeof(this->packet_.data), "%s", CMD_READ_DEVICE_STATUS); return this->clean_packet_(); } AnovaPacket *AnovaCodec::get_read_target_temp_request() { this->current_query_ = READ_TARGET_TEMPERATURE; - sprintf((char *) this->packet_.data, "%s", CMD_READ_TARGET_TEMP); + snprintf((char *) this->packet_.data, sizeof(this->packet_.data), "%s", CMD_READ_TARGET_TEMP); return this->clean_packet_(); } AnovaPacket *AnovaCodec::get_read_current_temp_request() { this->current_query_ = READ_CURRENT_TEMPERATURE; - sprintf((char *) this->packet_.data, "%s", CMD_READ_CURRENT_TEMP); + snprintf((char *) this->packet_.data, sizeof(this->packet_.data), "%s", CMD_READ_CURRENT_TEMP); return this->clean_packet_(); } AnovaPacket *AnovaCodec::get_read_unit_request() { this->current_query_ = READ_UNIT; - sprintf((char *) this->packet_.data, "%s", CMD_READ_UNIT); + snprintf((char *) this->packet_.data, sizeof(this->packet_.data), "%s", CMD_READ_UNIT); return this->clean_packet_(); } AnovaPacket *AnovaCodec::get_read_data_request() { this->current_query_ = READ_DATA; - sprintf((char *) this->packet_.data, "%s", CMD_READ_DATA); + snprintf((char *) this->packet_.data, sizeof(this->packet_.data), "%s", CMD_READ_DATA); return this->clean_packet_(); } @@ -50,25 +50,25 @@ AnovaPacket *AnovaCodec::get_set_target_temp_request(float temperature) { this->current_query_ = SET_TARGET_TEMPERATURE; if (this->fahrenheit_) temperature = ctof(temperature); - sprintf((char *) this->packet_.data, CMD_SET_TARGET_TEMP, temperature); + snprintf((char *) this->packet_.data, sizeof(this->packet_.data), CMD_SET_TARGET_TEMP, temperature); return this->clean_packet_(); } AnovaPacket *AnovaCodec::get_set_unit_request(char unit) { this->current_query_ = SET_UNIT; - sprintf((char *) this->packet_.data, CMD_SET_TEMP_UNIT, unit); + snprintf((char *) this->packet_.data, sizeof(this->packet_.data), CMD_SET_TEMP_UNIT, unit); return this->clean_packet_(); } AnovaPacket *AnovaCodec::get_start_request() { this->current_query_ = START; - sprintf((char *) this->packet_.data, CMD_START); + snprintf((char *) this->packet_.data, sizeof(this->packet_.data), "%s", CMD_START); return this->clean_packet_(); } AnovaPacket *AnovaCodec::get_stop_request() { this->current_query_ = STOP; - sprintf((char *) this->packet_.data, CMD_STOP); + snprintf((char *) this->packet_.data, sizeof(this->packet_.data), "%s", CMD_STOP); return this->clean_packet_(); } From a0d3d54d69a9f76eec891dbfb125083f533f1e25 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 20 Jan 2026 05:13:36 +1100 Subject: [PATCH 06/19] [mipi_spi] Add variants of ESP32-2432S028 displays (#13340) --- esphome/components/mipi_spi/mipi_spi.h | 9 ++-- .../components/mipi_spi/models/adafruit.py | 2 - esphome/components/mipi_spi/models/amoled.py | 3 -- esphome/components/mipi_spi/models/cyd.py | 43 +++++++++++++++++-- esphome/components/mipi_spi/models/ili.py | 30 ++++++++++++- esphome/components/mipi_spi/models/jc.py | 2 - esphome/components/mipi_spi/models/lanbon.py | 2 - esphome/components/mipi_spi/models/lilygo.py | 2 - 8 files changed, 70 insertions(+), 23 deletions(-) diff --git a/esphome/components/mipi_spi/mipi_spi.h b/esphome/components/mipi_spi/mipi_spi.h index a59cb8104b1..fd5bc975965 100644 --- a/esphome/components/mipi_spi/mipi_spi.h +++ b/esphome/components/mipi_spi/mipi_spi.h @@ -224,12 +224,9 @@ class MipiSpi : public display::Display, this->madctl_ & MADCTL_BGR ? "BGR" : "RGB", DISPLAYPIXEL * 8, IS_BIG_ENDIAN ? "Big" : "Little"); if (this->brightness_.has_value()) esph_log_config(TAG, " Brightness: %u", this->brightness_.value()); - if (this->cs_ != nullptr) - esph_log_config(TAG, " CS Pin: %s", this->cs_->dump_summary().c_str()); - if (this->reset_pin_ != nullptr) - esph_log_config(TAG, " Reset Pin: %s", this->reset_pin_->dump_summary().c_str()); - if (this->dc_pin_ != nullptr) - esph_log_config(TAG, " DC Pin: %s", this->dc_pin_->dump_summary().c_str()); + log_pin(TAG, " CS Pin: ", this->cs_); + log_pin(TAG, " Reset Pin: ", this->reset_pin_); + log_pin(TAG, " DC Pin: ", this->dc_pin_); esph_log_config(TAG, " SPI Mode: %d\n" " SPI Data rate: %dMHz\n" diff --git a/esphome/components/mipi_spi/models/adafruit.py b/esphome/components/mipi_spi/models/adafruit.py index 0e91107beed..26790b14934 100644 --- a/esphome/components/mipi_spi/models/adafruit.py +++ b/esphome/components/mipi_spi/models/adafruit.py @@ -26,5 +26,3 @@ ST7789V.extend( reset_pin=40, invert_colors=True, ) - -models = {} diff --git a/esphome/components/mipi_spi/models/amoled.py b/esphome/components/mipi_spi/models/amoled.py index 4d6c8da4b02..32cad70ac0b 100644 --- a/esphome/components/mipi_spi/models/amoled.py +++ b/esphome/components/mipi_spi/models/amoled.py @@ -105,6 +105,3 @@ CO5300 = DriverChip( (WCE, 0x00), ), ) - - -models = {} diff --git a/esphome/components/mipi_spi/models/cyd.py b/esphome/components/mipi_spi/models/cyd.py index a25ecf33a85..7229412f18c 100644 --- a/esphome/components/mipi_spi/models/cyd.py +++ b/esphome/components/mipi_spi/models/cyd.py @@ -1,10 +1,45 @@ -from .ili import ILI9341 +from .ili import ILI9341, ILI9342, ST7789V ILI9341.extend( + # ESP32-2432S028 CYD board with Micro USB, has ILI9341 controller "ESP32-2432S028", data_rate="40MHz", - cs_pin=15, - dc_pin=2, + cs_pin={"number": 15, "ignore_strapping_warning": True}, + dc_pin={"number": 2, "ignore_strapping_warning": True}, ) -models = {} +ST7789V.extend( + # ESP32-2432S028 CYD board with USB C + Micro USB, has ST7789V controller + "ESP32-2432S028-7789", + data_rate="40MHz", + cs_pin={"number": 15, "ignore_strapping_warning": True}, + dc_pin={"number": 2, "ignore_strapping_warning": True}, +) + +# fmt: off + +ILI9342.extend( + # ESP32-2432S028 CYD board with USB C + Micro USB, has ILI9342 controller + "ESP32-2432S028-9342", + data_rate="40MHz", + cs_pin={"number": 15, "ignore_strapping_warning": True}, + dc_pin={"number": 2, "ignore_strapping_warning": True}, + initsequence=( + (0xCB, 0x39, 0x2C, 0x00, 0x34, 0x02), # Power Control A + (0xCF, 0x00, 0xC1, 0x30), # Power Control B + (0xE8, 0x85, 0x00, 0x78), # Driver timing control A + (0xEA, 0x00, 0x00), # Driver timing control B + (0xED, 0x64, 0x03, 0x12, 0x81), # Power on sequence control + (0xF7, 0x20), # Pump ratio control + (0xC0, 0x23), # Power Control 1 + (0xC1, 0x10), # Power Control 2 + (0xC5, 0x3E, 0x28), # VCOM Control 1 + (0xC7, 0x86), # VCOM Control 2 + (0xB1, 0x00, 0x1B), # Frame Rate Control + (0xB6, 0x0A, 0xA2, 0x27, 0x00), # Display Function Control + (0xF2, 0x00), # Enable 3G + (0x26, 0x01), # Gamma Set + (0xE0, 0x00, 0x0C, 0x11, 0x04, 0x11, 0x08, 0x37, 0x89, 0x4C, 0x06, 0x0C, 0x0A, 0x2E, 0x34, 0x0F), # Positive Gamma Correction + (0xE1, 0x00, 0x0B, 0x11, 0x05, 0x13, 0x09, 0x33, 0x67, 0x48, 0x07, 0x0E, 0x0B, 0x23, 0x33, 0x0F), # Negative Gamma Correction + ) +) diff --git a/esphome/components/mipi_spi/models/ili.py b/esphome/components/mipi_spi/models/ili.py index 60a25c32a93..6b672b08594 100644 --- a/esphome/components/mipi_spi/models/ili.py +++ b/esphome/components/mipi_spi/models/ili.py @@ -148,6 +148,34 @@ ILI9341 = DriverChip( ), ), ) + +# fmt: off + +ILI9342 = DriverChip( + "ILI9342", + width=320, + height=240, + mirror_x=True, + initsequence=( + (0xCB, 0x39, 0x2C, 0x00, 0x34, 0x02), # Power Control A + (0xCF, 0x00, 0xC1, 0x30), # Power Control B + (0xE8, 0x85, 0x00, 0x78), # Driver timing control A + (0xEA, 0x00, 0x00), # Driver timing control B + (0xED, 0x64, 0x03, 0x12, 0x81), # Power on sequence control + (0xF7, 0x20), # Pump ratio control + (0xC0, 0x23), # Power Control 1 + (0xC1, 0x10), # Power Control 2 + (0xC5, 0x3E, 0x28), # VCOM Control 1 + (0xC7, 0x86), # VCOM Control 2 + (0xB1, 0x00, 0x1B), # Frame Rate Control + (0xB6, 0x0A, 0xA2, 0x27, 0x00), # Display Function Control + (0xF2, 0x00), # Enable 3G + (0x26, 0x01), # Gamma Set + (0xE0, 0x0F, 0x1F, 0x1C, 0x0C, 0x0F, 0x08, 0x48, 0x98, 0x37, 0x0A, 0x13, 0x04, 0x11, 0x0D, 0x00), # Positive Gamma + (0xE1, 0x0F, 0x32, 0x2E, 0x0B, 0x0D, 0x05, 0x47, 0x75, 0x37, 0x06, 0x10, 0x03, 0x24, 0x20, 0x00), # Negative Gamma + ), +) + # M5Stack Core2 uses ILI9341 chip - mirror_x disabled for correct orientation ILI9341.extend( "M5CORE2", @@ -758,5 +786,3 @@ ST7796.extend( dc_pin=0, invert_colors=True, ) - -models = {} diff --git a/esphome/components/mipi_spi/models/jc.py b/esphome/components/mipi_spi/models/jc.py index 5b936fd956f..854814f5727 100644 --- a/esphome/components/mipi_spi/models/jc.py +++ b/esphome/components/mipi_spi/models/jc.py @@ -588,5 +588,3 @@ DriverChip( (0x29, 0x00), ), ) - -models = {} diff --git a/esphome/components/mipi_spi/models/lanbon.py b/esphome/components/mipi_spi/models/lanbon.py index 6f9aa586749..8cec3c8317c 100644 --- a/esphome/components/mipi_spi/models/lanbon.py +++ b/esphome/components/mipi_spi/models/lanbon.py @@ -11,5 +11,3 @@ ST7789V.extend( dc_pin=21, reset_pin=18, ) - -models = {} diff --git a/esphome/components/mipi_spi/models/lilygo.py b/esphome/components/mipi_spi/models/lilygo.py index 13ddc67465d..46ec8090290 100644 --- a/esphome/components/mipi_spi/models/lilygo.py +++ b/esphome/components/mipi_spi/models/lilygo.py @@ -56,5 +56,3 @@ ST7796.extend( backlight_pin=48, invert_colors=True, ) - -models = {} From 1996bc425f27de225d2b9baf7b394c07ed3ac657 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 19 Jan 2026 14:46:24 -0500 Subject: [PATCH 07/19] [core] Fix state leakage and module caching when processing multiple configurations (#13368) Co-authored-by: Claude Opus 4.5 --- esphome/__main__.py | 112 ++++++++++++++++++++++------------ tests/unit_tests/test_main.py | 66 +++++++++++++++++++- 2 files changed, 139 insertions(+), 39 deletions(-) diff --git a/esphome/__main__.py b/esphome/__main__.py index 545464be101..09d2855eb1c 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -1,5 +1,6 @@ # PYTHON_ARGCOMPLETE_OK import argparse +from collections.abc import Callable from datetime import datetime import functools import getpass @@ -936,11 +937,21 @@ def command_dashboard(args: ArgsProtocol) -> int | None: return dashboard.start_dashboard(args) -def command_update_all(args: ArgsProtocol) -> int | None: +def run_multiple_configs( + files: list, command_builder: Callable[[str], list[str]] +) -> int: + """Run a command for each configuration file in a subprocess. + + Args: + files: List of configuration files to process. + command_builder: Callable that takes a file path and returns a command list. + + Returns: + Number of failed files. + """ import click success = {} - files = list_yaml_files(args.configuration) twidth = 60 def print_bar(middle_text): @@ -950,17 +961,19 @@ def command_update_all(args: ArgsProtocol) -> int | None: safe_print(f"{half_line}{middle_text}{half_line}") for f in files: - safe_print(f"Updating {color(AnsiFore.CYAN, str(f))}") + f_path = Path(f) if not isinstance(f, Path) else f + + if any(f_path.name == x for x in SECRETS_FILES): + _LOGGER.warning("Skipping secrets file %s", f_path) + continue + + safe_print(f"Processing {color(AnsiFore.CYAN, str(f))}") safe_print("-" * twidth) safe_print() - if CORE.dashboard: - rc = run_external_process( - "esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA" - ) - else: - rc = run_external_process( - "esphome", "run", f, "--no-logs", "--device", "OTA" - ) + + cmd = command_builder(f) + rc = run_external_process(*cmd) + if rc == 0: print_bar(f"[{color(AnsiFore.BOLD_GREEN, 'SUCCESS')}] {str(f)}") success[f] = True @@ -975,6 +988,8 @@ def command_update_all(args: ArgsProtocol) -> int | None: print_bar(f"[{color(AnsiFore.BOLD_WHITE, 'SUMMARY')}]") failed = 0 for f in files: + if f not in success: + continue # Skipped file if success[f]: safe_print(f" - {str(f)}: {color(AnsiFore.GREEN, 'SUCCESS')}") else: @@ -983,6 +998,17 @@ def command_update_all(args: ArgsProtocol) -> int | None: return failed +def command_update_all(args: ArgsProtocol) -> int | None: + files = list_yaml_files(args.configuration) + + def build_command(f): + if CORE.dashboard: + return ["esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA"] + return ["esphome", "run", f, "--no-logs", "--device", "OTA"] + + return run_multiple_configs(files, build_command) + + def command_idedata(args: ArgsProtocol, config: ConfigType) -> int: import json @@ -1533,38 +1559,48 @@ def run_esphome(argv): _LOGGER.info("ESPHome %s", const.__version__) - for conf_path in args.configuration: - conf_path = Path(conf_path) - if any(conf_path.name == x for x in SECRETS_FILES): - _LOGGER.warning("Skipping secrets file %s", conf_path) - continue + # Multiple configurations: use subprocesses to avoid state leakage + # between compilations (e.g., LVGL touchscreen state in module globals) + if len(args.configuration) > 1: + # Build command by reusing argv, replacing all configs with single file + # argv[0] is the program path, skip it since we prefix with "esphome" + def build_command(f): + return ( + ["esphome"] + + [arg for arg in argv[1:] if arg not in args.configuration] + + [str(f)] + ) - CORE.config_path = conf_path - CORE.dashboard = args.dashboard + return run_multiple_configs(args.configuration, build_command) - # For logs command, skip updating external components - skip_external = args.command == "logs" - config = read_config( - dict(args.substitution) if args.substitution else {}, - skip_external_update=skip_external, - ) - if config is None: - return 2 - CORE.config = config + # Single configuration + conf_path = Path(args.configuration[0]) + if any(conf_path.name == x for x in SECRETS_FILES): + _LOGGER.warning("Skipping secrets file %s", conf_path) + return 0 - if args.command not in POST_CONFIG_ACTIONS: - safe_print(f"Unknown command {args.command}") + CORE.config_path = conf_path + CORE.dashboard = args.dashboard - try: - rc = POST_CONFIG_ACTIONS[args.command](args, config) - except EsphomeError as e: - _LOGGER.error(e, exc_info=args.verbose) - return 1 - if rc != 0: - return rc + # For logs command, skip updating external components + skip_external = args.command == "logs" + config = read_config( + dict(args.substitution) if args.substitution else {}, + skip_external_update=skip_external, + ) + if config is None: + return 2 + CORE.config = config - CORE.reset() - return 0 + if args.command not in POST_CONFIG_ACTIONS: + safe_print(f"Unknown command {args.command}") + return 1 + + try: + return POST_CONFIG_ACTIONS[args.command](args, config) + except EsphomeError as e: + _LOGGER.error(e, exc_info=args.verbose) + return 1 def main(): diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index fd8f04ded5e..3268f7ee87e 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -34,6 +34,7 @@ from esphome.__main__ import ( has_non_ip_address, has_resolvable_address, mqtt_get_ip, + run_esphome, run_miniterm, show_logs, upload_program, @@ -1988,7 +1989,7 @@ esp32: clean_output = strip_ansi_codes(captured.out) assert "test-device_123.yaml" in clean_output - assert "Updating" in clean_output + assert "Processing" in clean_output assert "SUCCESS" in clean_output assert "SUMMARY" in clean_output @@ -3172,3 +3173,66 @@ def test_run_miniterm_buffer_limit_prevents_unbounded_growth() -> None: x_count = printed_line.count("X") assert x_count < 150, f"Expected truncation but got {x_count} X's" assert x_count == 95, f"Expected 95 X's after truncation but got {x_count}" + + +def test_run_esphome_multiple_configs_with_secrets( + tmp_path: Path, + mock_run_external_process: Mock, + capfd: CaptureFixture[str], + caplog: pytest.LogCaptureFixture, +) -> None: + """Test run_esphome with multiple configs and secrets file. + + Verifies: + - Multiple configs use subprocess isolation + - Secrets files are skipped with warning + - Secrets files don't appear in summary + """ + # Create two config files and a secrets file + yaml_file1 = tmp_path / "device1.yaml" + yaml_file1.write_text(""" +esphome: + name: device1 + +esp32: + board: nodemcu-32s +""") + yaml_file2 = tmp_path / "device2.yaml" + yaml_file2.write_text(""" +esphome: + name: device2 + +esp32: + board: nodemcu-32s +""") + secrets_file = tmp_path / "secrets.yaml" + secrets_file.write_text("wifi_password: secret123\n") + + setup_core(tmp_path=tmp_path) + mock_run_external_process.return_value = 0 + + # run_esphome expects argv[0] to be the program name (gets sliced off by parse_args) + with caplog.at_level(logging.WARNING): + result = run_esphome( + ["esphome", "compile", str(yaml_file1), str(secrets_file), str(yaml_file2)] + ) + + assert result == 0 + + # Check secrets file was skipped with warning + assert "Skipping secrets file" in caplog.text + assert "secrets.yaml" in caplog.text + + captured = capfd.readouterr() + clean_output = strip_ansi_codes(captured.out) + + # Both config files should be processed + assert "device1.yaml" in clean_output + assert "device2.yaml" in clean_output + assert "SUMMARY" in clean_output + + # Secrets should not appear in summary + summary_section = ( + clean_output.split("SUMMARY")[1] if "SUMMARY" in clean_output else "" + ) + assert "secrets.yaml" not in summary_section From 0193464f92e90f765242b6ce0837f009ee611cdb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 19 Jan 2026 17:34:49 -1000 Subject: [PATCH 08/19] [dsmr] Avoid std::string allocation for decryption key (#13375) --- esphome/components/dsmr/__init__.py | 22 +++------------------- esphome/components/dsmr/dsmr.cpp | 19 +++++++------------ esphome/components/dsmr/dsmr.h | 2 +- esphome/config_validation.py | 8 ++++---- 4 files changed, 15 insertions(+), 36 deletions(-) diff --git a/esphome/components/dsmr/__init__.py b/esphome/components/dsmr/__init__.py index 0ba68daf5d7..386da3ce212 100644 --- a/esphome/components/dsmr/__init__.py +++ b/esphome/components/dsmr/__init__.py @@ -25,29 +25,13 @@ dsmr_ns = cg.esphome_ns.namespace("esphome::dsmr") Dsmr = dsmr_ns.class_("Dsmr", cg.Component, uart.UARTDevice) -def _validate_key(value): - value = cv.string_strict(value) - parts = [value[i : i + 2] for i in range(0, len(value), 2)] - if len(parts) != 16: - raise cv.Invalid("Decryption key must consist of 16 hexadecimal numbers") - parts_int = [] - if any(len(part) != 2 for part in parts): - raise cv.Invalid("Decryption key must be format XX") - for part in parts: - try: - parts_int.append(int(part, 16)) - except ValueError: - # pylint: disable=raise-missing-from - raise cv.Invalid("Decryption key must be hex values from 00 to FF") - - return "".join(f"{part:02X}" for part in parts_int) - - CONFIG_SCHEMA = cv.All( cv.Schema( { cv.GenerateID(): cv.declare_id(Dsmr), - cv.Optional(CONF_DECRYPTION_KEY): _validate_key, + cv.Optional(CONF_DECRYPTION_KEY): lambda value: cv.bind_key( + value, name="Decryption key" + ), cv.Optional(CONF_CRC_CHECK, default=True): cv.boolean, cv.Optional(CONF_GAS_MBUS_ID, default=1): cv.int_, cv.Optional(CONF_WATER_MBUS_ID, default=2): cv.int_, diff --git a/esphome/components/dsmr/dsmr.cpp b/esphome/components/dsmr/dsmr.cpp index 5c62aa93ab1..c78d37bf5ef 100644 --- a/esphome/components/dsmr/dsmr.cpp +++ b/esphome/components/dsmr/dsmr.cpp @@ -1,4 +1,5 @@ #include "dsmr.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #include @@ -294,8 +295,8 @@ void Dsmr::dump_config() { DSMR_TEXT_SENSOR_LIST(DSMR_LOG_TEXT_SENSOR, ) } -void Dsmr::set_decryption_key(const std::string &decryption_key) { - if (decryption_key.empty()) { +void Dsmr::set_decryption_key(const char *decryption_key) { + if (decryption_key == nullptr || decryption_key[0] == '\0') { ESP_LOGI(TAG, "Disabling decryption"); this->decryption_key_.clear(); if (this->crypt_telegram_ != nullptr) { @@ -305,21 +306,15 @@ void Dsmr::set_decryption_key(const std::string &decryption_key) { return; } - if (decryption_key.length() != 32) { - ESP_LOGE(TAG, "Error, decryption key must be 32 character long"); + if (!parse_hex(decryption_key, this->decryption_key_, 16)) { + ESP_LOGE(TAG, "Error, decryption key must be 32 hex characters"); + this->decryption_key_.clear(); return; } - this->decryption_key_.clear(); ESP_LOGI(TAG, "Decryption key is set"); // Verbose level prints decryption key - ESP_LOGV(TAG, "Using decryption key: %s", decryption_key.c_str()); - - char temp[3] = {0}; - for (int i = 0; i < 16; i++) { - strncpy(temp, &(decryption_key.c_str()[i * 2]), 2); - this->decryption_key_.push_back(std::strtoul(temp, nullptr, 16)); - } + ESP_LOGV(TAG, "Using decryption key: %s", decryption_key); if (this->crypt_telegram_ == nullptr) { this->crypt_telegram_ = new uint8_t[this->max_telegram_len_]; // NOLINT diff --git a/esphome/components/dsmr/dsmr.h b/esphome/components/dsmr/dsmr.h index 56ba75b5fa9..b7e05a22b3e 100644 --- a/esphome/components/dsmr/dsmr.h +++ b/esphome/components/dsmr/dsmr.h @@ -63,7 +63,7 @@ class Dsmr : public Component, public uart::UARTDevice { void dump_config() override; - void set_decryption_key(const std::string &decryption_key); + void set_decryption_key(const char *decryption_key); void set_max_telegram_length(size_t length) { this->max_telegram_len_ = length; } void set_request_pin(GPIOPin *request_pin) { this->request_pin_ = request_pin; } void set_request_interval(uint32_t interval) { this->request_interval_ = interval; } diff --git a/esphome/config_validation.py b/esphome/config_validation.py index 8e2fadbea8f..b7ab02013d3 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -1046,20 +1046,20 @@ def mac_address(value): return core.MACAddress(*parts_int) -def bind_key(value): +def bind_key(value, *, name="Bind key"): value = string_strict(value) parts = [value[i : i + 2] for i in range(0, len(value), 2)] if len(parts) != 16: - raise Invalid("Bind key must consist of 16 hexadecimal numbers") + raise Invalid(f"{name} must consist of 16 hexadecimal numbers") parts_int = [] if any(len(part) != 2 for part in parts): - raise Invalid("Bind key must be format XX") + raise Invalid(f"{name} must be format XX") for part in parts: try: parts_int.append(int(part, 16)) except ValueError: # pylint: disable=raise-missing-from - raise Invalid("Bind key must be hex values from 00 to FF") + raise Invalid(f"{name} must be hex values from 00 to FF") return "".join(f"{part:02X}" for part in parts_int) From 8ec31dd7693028d1e987d39b67708c04133ebc66 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 19 Jan 2026 17:35:19 -1000 Subject: [PATCH 09/19] [voice_assistant] Deprecate Timer::to_string() in favor of heap-free to_str() (#13377) --- esphome/components/voice_assistant/voice_assistant.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index b1b3df7bbdd..d61a8fbbc1a 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -81,6 +81,8 @@ struct Timer { this->id.c_str(), this->name.c_str(), this->total_seconds, this->seconds_left, YESNO(this->is_active)); return buffer.data(); } + // Remove before 2026.8.0 + ESPDEPRECATED("Use to_str() instead. Removed in 2026.8.0", "2026.2.0") std::string to_string() const { char buffer[TO_STR_BUFFER_SIZE]; return this->to_str(buffer); From 8998ef0bc309ec1ed5f97dc8ab372a6c5007f332 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 19 Jan 2026 17:35:32 -1000 Subject: [PATCH 10/19] [network] Deprecate IPAddress::str() in favor of heap-free str_to() (#13378) --- esphome/components/network/ip_address.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/esphome/components/network/ip_address.h b/esphome/components/network/ip_address.h index 3dfcf0cb640..d0ac8164af2 100644 --- a/esphome/components/network/ip_address.h +++ b/esphome/components/network/ip_address.h @@ -60,6 +60,8 @@ struct IPAddress { } IPAddress(const std::string &in_address) { inet_aton(in_address.c_str(), &ip_addr_); } IPAddress(const ip_addr_t *other_ip) { ip_addr_ = *other_ip; } + // Remove before 2026.8.0 + ESPDEPRECATED("Use str_to() instead. Removed in 2026.8.0", "2026.2.0") std::string str() const { char buf[IP_ADDRESS_BUFFER_SIZE]; this->str_to(buf); @@ -147,6 +149,8 @@ struct IPAddress { bool is_ip4() const { return IP_IS_V4(&ip_addr_); } bool is_ip6() const { return IP_IS_V6(&ip_addr_); } bool is_multicast() const { return ip_addr_ismulticast(&ip_addr_); } + // Remove before 2026.8.0 + ESPDEPRECATED("Use str_to() instead. Removed in 2026.8.0", "2026.2.0") std::string str() const { char buf[IP_ADDRESS_BUFFER_SIZE]; this->str_to(buf); From 5d787e2512aecfb7ef434a00e3cca74d51c67f4d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 19 Jan 2026 17:35:58 -1000 Subject: [PATCH 11/19] [sprinkler] Eliminate std::string heap allocations (#13379) --- esphome/components/sprinkler/sprinkler.cpp | 20 +++++++++----------- esphome/components/sprinkler/sprinkler.h | 8 ++++---- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index 2813b4450b4..35310fa2f92 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -327,14 +327,13 @@ SprinklerValveOperator *SprinklerValveRunRequest::valve_operator() { return this SprinklerValveRunRequestOrigin SprinklerValveRunRequest::request_is_from() { return this->origin_; } -Sprinkler::Sprinkler() {} -Sprinkler::Sprinkler(const std::string &name) { - // The `name` is needed to set timers up, hence non-default constructor - // replaces `set_name()` method previously existed - this->name_ = name; +Sprinkler::Sprinkler() : Sprinkler("") {} +Sprinkler::Sprinkler(const char *name) : name_(name) { + // The `name` is stored for dump_config logging this->timer_.init(2); - this->timer_.push_back({this->name_ + "sm", false, 0, 0, std::bind(&Sprinkler::sm_timer_callback_, this)}); - this->timer_.push_back({this->name_ + "vs", false, 0, 0, std::bind(&Sprinkler::valve_selection_callback_, this)}); + // Timer names only need to be unique within this component instance + this->timer_.push_back({"sm", false, 0, 0, std::bind(&Sprinkler::sm_timer_callback_, this)}); + this->timer_.push_back({"vs", false, 0, 0, std::bind(&Sprinkler::valve_selection_callback_, this)}); } void Sprinkler::setup() { this->all_valves_off_(true); } @@ -1575,8 +1574,7 @@ const LogString *Sprinkler::state_as_str_(SprinklerState state) { void Sprinkler::start_timer_(const SprinklerTimerIndex timer_index) { if (this->timer_duration_(timer_index) > 0) { - // FixedVector ensures timer_ can't be resized, so .c_str() pointers remain valid - this->set_timeout(this->timer_[timer_index].name.c_str(), this->timer_duration_(timer_index), + this->set_timeout(this->timer_[timer_index].name, this->timer_duration_(timer_index), this->timer_cbf_(timer_index)); this->timer_[timer_index].start_time = millis(); this->timer_[timer_index].active = true; @@ -1587,7 +1585,7 @@ void Sprinkler::start_timer_(const SprinklerTimerIndex timer_index) { bool Sprinkler::cancel_timer_(const SprinklerTimerIndex timer_index) { this->timer_[timer_index].active = false; - return this->cancel_timeout(this->timer_[timer_index].name.c_str()); + return this->cancel_timeout(this->timer_[timer_index].name); } bool Sprinkler::timer_active_(const SprinklerTimerIndex timer_index) { return this->timer_[timer_index].active; } @@ -1618,7 +1616,7 @@ void Sprinkler::sm_timer_callback_() { } void Sprinkler::dump_config() { - ESP_LOGCONFIG(TAG, "Sprinkler Controller -- %s", this->name_.c_str()); + ESP_LOGCONFIG(TAG, "Sprinkler Controller -- %s", this->name_); if (this->manual_selection_delay_.has_value()) { ESP_LOGCONFIG(TAG, " Manual Selection Delay: %" PRIu32 " seconds", this->manual_selection_delay_.value_or(0)); } diff --git a/esphome/components/sprinkler/sprinkler.h b/esphome/components/sprinkler/sprinkler.h index 273c0e92085..04efa28031f 100644 --- a/esphome/components/sprinkler/sprinkler.h +++ b/esphome/components/sprinkler/sprinkler.h @@ -11,7 +11,7 @@ namespace esphome::sprinkler { -const std::string MIN_STR = "min"; +inline constexpr const char *MIN_STR = "min"; enum SprinklerState : uint8_t { // NOTE: these states are used by both SprinklerValveOperator and Sprinkler (the controller)! @@ -49,7 +49,7 @@ struct SprinklerQueueItem { }; struct SprinklerTimer { - const std::string name; + const char *name; bool active; uint32_t time; uint32_t start_time; @@ -176,7 +176,7 @@ class SprinklerValveRunRequest { class Sprinkler : public Component { public: Sprinkler(); - Sprinkler(const std::string &name); + Sprinkler(const char *name); void setup() override; void loop() override; void dump_config() override; @@ -504,7 +504,7 @@ class Sprinkler : public Component { uint32_t start_delay_{0}; uint32_t stop_delay_{0}; - std::string name_; + const char *name_{""}; /// Sprinkler controller state SprinklerState state_{IDLE}; From b5fe271d6bc9cc069d31e2c8706594c2b819eb09 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 19 Jan 2026 17:36:47 -1000 Subject: [PATCH 12/19] [sprinkler] Disable loops when idle to reduce CPU overhead (#13381) --- esphome/components/sprinkler/sprinkler.cpp | 36 +++++++++++++++------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index 35310fa2f92..369ee5e6ffe 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -43,13 +43,11 @@ SprinklerControllerSwitch::SprinklerControllerSwitch() : turn_on_trigger_(new Trigger<>()), turn_off_trigger_(new Trigger<>()) {} void SprinklerControllerSwitch::loop() { - if (!this->f_.has_value()) - return; + // Loop is only enabled when f_ has a value (see setup()) auto s = (*this->f_)(); - if (!s.has_value()) - return; - - this->publish_state(*s); + if (s.has_value()) { + this->publish_state(*s); + } } void SprinklerControllerSwitch::write_state(bool state) { @@ -74,7 +72,13 @@ float SprinklerControllerSwitch::get_setup_priority() const { return setup_prior Trigger<> *SprinklerControllerSwitch::get_turn_on_trigger() const { return this->turn_on_trigger_; } Trigger<> *SprinklerControllerSwitch::get_turn_off_trigger() const { return this->turn_off_trigger_; } -void SprinklerControllerSwitch::setup() { this->state = this->get_initial_state_with_restore_mode().value_or(false); } +void SprinklerControllerSwitch::setup() { + this->state = this->get_initial_state_with_restore_mode().value_or(false); + // Disable loop if no state lambda is set - nothing to poll + if (!this->f_.has_value()) { + this->disable_loop(); + } +} void SprinklerControllerSwitch::dump_config() { LOG_SWITCH("", "Sprinkler Switch", this); } @@ -336,15 +340,23 @@ Sprinkler::Sprinkler(const char *name) : name_(name) { this->timer_.push_back({"vs", false, 0, 0, std::bind(&Sprinkler::valve_selection_callback_, this)}); } -void Sprinkler::setup() { this->all_valves_off_(true); } +void Sprinkler::setup() { + this->all_valves_off_(true); + // Start with loop disabled - nothing to do when idle + this->disable_loop(); +} void Sprinkler::loop() { for (auto &vo : this->valve_op_) { vo.loop(); } - if (this->prev_req_.has_request() && this->prev_req_.has_valve_operator() && - this->prev_req_.valve_operator()->state() == IDLE) { - this->prev_req_.reset(); + if (this->prev_req_.has_request()) { + if (this->prev_req_.has_valve_operator() && this->prev_req_.valve_operator()->state() == IDLE) { + this->prev_req_.reset(); + } + } else if (this->state_ == IDLE) { + // Nothing more to do - disable loop until next activation + this->disable_loop(); } } @@ -1332,6 +1344,8 @@ void Sprinkler::start_valve_(SprinklerValveRunRequest *req) { if (!this->is_a_valid_valve(req->valve())) { return; // we can't do anything if the valve number isn't valid } + // Enable loop to monitor valve operator states + this->enable_loop(); for (auto &vo : this->valve_op_) { // find the first available SprinklerValveOperator, load it and start it up if (vo.state() == IDLE) { auto run_duration = req->run_duration() ? req->run_duration() : this->valve_run_duration_adjusted(req->valve()); From 62b6c9bf7cee3a67fab39f6dc9870d69add352dd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 19 Jan 2026 17:37:03 -1000 Subject: [PATCH 13/19] [esp32_ble] Deprecate ESPBTUUID::to_string() in favor of heap-free to_str() (#13376) --- esphome/components/esp32_ble/ble_uuid.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/esphome/components/esp32_ble/ble_uuid.h b/esphome/components/esp32_ble/ble_uuid.h index ae593955a44..6c8ef7bfd9f 100644 --- a/esphome/components/esp32_ble/ble_uuid.h +++ b/esphome/components/esp32_ble/ble_uuid.h @@ -46,6 +46,8 @@ class ESPBTUUID { esp_bt_uuid_t get_uuid() const; + // Remove before 2026.8.0 + ESPDEPRECATED("Use to_str() instead. Removed in 2026.8.0", "2026.2.0") std::string to_string() const; const char *to_str(std::span output) const; From 4e0e7796ded16e118bb95ff63fc956850d9b3e7f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 19 Jan 2026 17:37:19 -1000 Subject: [PATCH 14/19] [mqtt] Remove unnecessary defer in ESP8266 on_message callback (#13373) --- esphome/components/mqtt/mqtt_client.cpp | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 7d4050284f1..f8517c4f89c 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -610,18 +610,10 @@ static bool topic_match(const char *message, const char *subscription) { } void MQTTClientComponent::on_message(const std::string &topic, const std::string &payload) { -#ifdef USE_ESP8266 - // on ESP8266, this is called in lwIP/AsyncTCP task; some components do not like running - // from a different task. - this->defer([this, topic, payload]() { -#endif - for (auto &subscription : this->subscriptions_) { - if (topic_match(topic.c_str(), subscription.topic.c_str())) - subscription.callback(topic, payload); - } -#ifdef USE_ESP8266 - }); -#endif + for (auto &subscription : this->subscriptions_) { + if (topic_match(topic.c_str(), subscription.topic.c_str())) + subscription.callback(topic, payload); + } } // Setters From 8ade9dfc107b752bb5ac3cf03936f75bb85f5af6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 19 Jan 2026 17:37:33 -1000 Subject: [PATCH 15/19] [shtcx] Use LogString for type to_string to save RAM on ESP8266 (#13370) --- esphome/components/shtcx/shtcx.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/esphome/components/shtcx/shtcx.cpp b/esphome/components/shtcx/shtcx.cpp index 933dd9bde97..aca305b88db 100644 --- a/esphome/components/shtcx/shtcx.cpp +++ b/esphome/components/shtcx/shtcx.cpp @@ -13,14 +13,14 @@ static const uint16_t SHTCX_COMMAND_READ_ID_REGISTER = 0xEFC8; static const uint16_t SHTCX_COMMAND_SOFT_RESET = 0x805D; static const uint16_t SHTCX_COMMAND_POLLING_H = 0x7866; -inline const char *to_string(SHTCXType type) { +static const LogString *shtcx_type_to_string(SHTCXType type) { switch (type) { case SHTCX_TYPE_SHTC3: - return "SHTC3"; + return LOG_STR("SHTC3"); case SHTCX_TYPE_SHTC1: - return "SHTC1"; + return LOG_STR("SHTC1"); default: - return "UNKNOWN"; + return LOG_STR("UNKNOWN"); } } @@ -52,7 +52,7 @@ void SHTCXComponent::dump_config() { ESP_LOGCONFIG(TAG, "SHTCx:\n" " Model: %s (%04x)", - to_string(this->type_), this->sensor_id_); + LOG_STR_ARG(shtcx_type_to_string(this->type_)), this->sensor_id_); LOG_I2C_DEVICE(this); if (this->is_failed()) { ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); From b48d4ab785f1deb84d59865b2a0abb9adc69a44f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 19 Jan 2026 17:37:54 -1000 Subject: [PATCH 16/19] [mqtt] Reduce heap allocations in publish path (#13372) --- esphome/components/mqtt/mqtt_client.cpp | 51 ++++++++++++++++--------- esphome/components/mqtt/mqtt_client.h | 6 +++ 2 files changed, 40 insertions(+), 17 deletions(-) diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index f8517c4f89c..96eba37a579 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -5,6 +5,7 @@ #include #include "esphome/components/network/util.h" #include "esphome/core/application.h" +#include "esphome/core/entity_base.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "esphome/core/version.h" @@ -66,10 +67,13 @@ void MQTTClientComponent::setup() { "esphome/discover", [this](const std::string &topic, const std::string &payload) { this->send_device_info_(); }, 2); - std::string topic = "esphome/ping/"; - topic.append(App.get_name()); + // Format topic on stack - subscribe() copies it + // "esphome/ping/" (13) + name (ESPHOME_DEVICE_NAME_MAX_LEN) + null (1) + constexpr size_t ping_topic_buffer_size = 13 + ESPHOME_DEVICE_NAME_MAX_LEN + 1; + char ping_topic[ping_topic_buffer_size]; + buf_append_printf(ping_topic, sizeof(ping_topic), 0, "esphome/ping/%s", App.get_name().c_str()); this->subscribe( - topic, [this](const std::string &topic, const std::string &payload) { this->send_device_info_(); }, 2); + ping_topic, [this](const std::string &topic, const std::string &payload) { this->send_device_info_(); }, 2); } if (this->enable_on_boot_) { @@ -81,8 +85,11 @@ void MQTTClientComponent::send_device_info_() { if (!this->is_connected() or !this->is_discovery_ip_enabled()) { return; } - std::string topic = "esphome/discover/"; - topic.append(App.get_name()); + // Format topic on stack to avoid heap allocation + // "esphome/discover/" (17) + name (ESPHOME_DEVICE_NAME_MAX_LEN) + null (1) + constexpr size_t topic_buffer_size = 17 + ESPHOME_DEVICE_NAME_MAX_LEN + 1; + char topic[topic_buffer_size]; + buf_append_printf(topic, sizeof(topic), 0, "esphome/discover/%s", App.get_name().c_str()); // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson this->publish_json( @@ -500,39 +507,49 @@ bool MQTTClientComponent::publish(const std::string &topic, const std::string &p bool MQTTClientComponent::publish(const std::string &topic, const char *payload, size_t payload_length, uint8_t qos, bool retain) { - return publish({.topic = topic, .payload = std::string(payload, payload_length), .qos = qos, .retain = retain}); + return this->publish(topic.c_str(), payload, payload_length, qos, retain); } bool MQTTClientComponent::publish(const MQTTMessage &message) { + return this->publish(message.topic.c_str(), message.payload.c_str(), message.payload.length(), message.qos, + message.retain); +} +bool MQTTClientComponent::publish_json(const std::string &topic, const json::json_build_t &f, uint8_t qos, + bool retain) { + return this->publish_json(topic.c_str(), f, qos, retain); +} + +bool MQTTClientComponent::publish(const char *topic, const char *payload, size_t payload_length, uint8_t qos, + bool retain) { if (!this->is_connected()) { - // critical components will re-transmit their messages return false; } - bool logging_topic = this->log_message_.topic == message.topic; - bool ret = this->mqtt_backend_.publish(message); + size_t topic_len = strlen(topic); + bool logging_topic = (topic_len == this->log_message_.topic.size()) && + (memcmp(this->log_message_.topic.c_str(), topic, topic_len) == 0); + bool ret = this->mqtt_backend_.publish(topic, payload, payload_length, qos, retain); delay(0); if (!ret && !logging_topic && this->is_connected()) { delay(0); - ret = this->mqtt_backend_.publish(message); + ret = this->mqtt_backend_.publish(topic, payload, payload_length, qos, retain); delay(0); } if (!logging_topic) { if (ret) { - ESP_LOGV(TAG, "Publish(topic='%s' payload='%s' retain=%d qos=%d)", message.topic.c_str(), message.payload.c_str(), - message.retain, message.qos); + ESP_LOGV(TAG, "Publish(topic='%s' retain=%d qos=%d)", topic, retain, qos); + ESP_LOGVV(TAG, "Publish payload (len=%u): '%.*s'", payload_length, static_cast(payload_length), payload); } else { - ESP_LOGV(TAG, "Publish failed for topic='%s' (len=%u). Will retry", message.topic.c_str(), - message.payload.length()); + ESP_LOGV(TAG, "Publish failed for topic='%s' (len=%u). Will retry", topic, payload_length); this->status_momentary_warning("publish", 1000); } } return ret != 0; } -bool MQTTClientComponent::publish_json(const std::string &topic, const json::json_build_t &f, uint8_t qos, - bool retain) { + +bool MQTTClientComponent::publish_json(const char *topic, const json::json_build_t &f, uint8_t qos, bool retain) { std::string message = json::build_json(f); - return this->publish(topic, message, qos, retain); + return this->publish(topic, message.c_str(), message.length(), qos, retain); } void MQTTClientComponent::enable() { diff --git a/esphome/components/mqtt/mqtt_client.h b/esphome/components/mqtt/mqtt_client.h index 9e9db03b198..38bc0b4da37 100644 --- a/esphome/components/mqtt/mqtt_client.h +++ b/esphome/components/mqtt/mqtt_client.h @@ -229,6 +229,9 @@ class MQTTClientComponent : public Component bool publish(const std::string &topic, const char *payload, size_t payload_length, uint8_t qos = 0, bool retain = false); + /// Publish directly without creating MQTTMessage (avoids heap allocation for topic) + bool publish(const char *topic, const char *payload, size_t payload_length, uint8_t qos = 0, bool retain = false); + /** Construct and send a JSON MQTT message. * * @param topic The topic. @@ -237,6 +240,9 @@ class MQTTClientComponent : public Component */ bool publish_json(const std::string &topic, const json::json_build_t &f, uint8_t qos = 0, bool retain = false); + /// Publish JSON directly without heap allocation for topic + bool publish_json(const char *topic, const json::json_build_t &f, uint8_t qos = 0, bool retain = false); + /// Setup the MQTT client, registering a bunch of callbacks and attempting to connect. void setup() override; void dump_config() override; From e88093ca605411cdd5f500c90d888d9ab66428e2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 19 Jan 2026 17:38:08 -1000 Subject: [PATCH 17/19] [am43][lightwaverf][rf_bridge][spi_led_strip] Replace sprintf with safe alternatives (#13302) --- esphome/components/am43/am43_base.cpp | 18 ++++++------------ esphome/components/lightwaverf/lightwaverf.cpp | 8 ++++++-- esphome/components/rf_bridge/rf_bridge.cpp | 11 ++++++----- .../components/spi_led_strip/spi_led_strip.cpp | 14 +++++++------- 4 files changed, 25 insertions(+), 26 deletions(-) diff --git a/esphome/components/am43/am43_base.cpp b/esphome/components/am43/am43_base.cpp index af474dcb799..d70e6383829 100644 --- a/esphome/components/am43/am43_base.cpp +++ b/esphome/components/am43/am43_base.cpp @@ -1,21 +1,12 @@ #include "am43_base.h" +#include "esphome/core/helpers.h" #include -#include namespace esphome { namespace am43 { const uint8_t START_PACKET[5] = {0x00, 0xff, 0x00, 0x00, 0x9a}; -std::string pkt_to_hex(const uint8_t *data, uint16_t len) { - char buf[64]; - memset(buf, 0, 64); - for (int i = 0; i < len; i++) - sprintf(&buf[i * 2], "%02x", data[i]); - std::string ret = buf; - return ret; -} - Am43Packet *Am43Encoder::get_battery_level_request() { uint8_t data = 0x1; return this->encode_(0xA2, &data, 1); @@ -73,7 +64,9 @@ Am43Packet *Am43Encoder::encode_(uint8_t command, uint8_t *data, uint8_t length) memcpy(&this->packet_.data[7], data, length); this->packet_.length = length + 7; this->checksum_(); - ESP_LOGV("am43", "ENC(%d): 0x%s", packet_.length, pkt_to_hex(packet_.data, packet_.length).c_str()); + char hex_buf[format_hex_size(sizeof(this->packet_.data))]; + ESP_LOGV("am43", "ENC(%d): 0x%s", this->packet_.length, + format_hex_to(hex_buf, this->packet_.data, this->packet_.length)); return &this->packet_; } @@ -88,7 +81,8 @@ void Am43Decoder::decode(const uint8_t *data, uint16_t length) { this->has_set_state_response_ = false; this->has_position_ = false; this->has_pin_response_ = false; - ESP_LOGV("am43", "DEC(%d): 0x%s", length, pkt_to_hex(data, length).c_str()); + char hex_buf[format_hex_size(24)]; // Max expected packet size + ESP_LOGV("am43", "DEC(%d): 0x%s", length, format_hex_to(hex_buf, data, length)); if (length < 2 || data[0] != 0x9a) return; diff --git a/esphome/components/lightwaverf/lightwaverf.cpp b/esphome/components/lightwaverf/lightwaverf.cpp index 31ac1fc576d..2b44195c974 100644 --- a/esphome/components/lightwaverf/lightwaverf.cpp +++ b/esphome/components/lightwaverf/lightwaverf.cpp @@ -1,3 +1,4 @@ +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #ifdef USE_ESP8266 @@ -44,13 +45,16 @@ void LightWaveRF::send_rx(const std::vector &msg, uint8_t repeats, bool } void LightWaveRF::print_msg_(uint8_t *msg, uint8_t len) { - char buffer[65]; +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG + char buffer[65]; // max 10 entries * 6 chars + null ESP_LOGD(TAG, " Received code (len:%i): ", len); + size_t pos = 0; for (int i = 0; i < len; i++) { - sprintf(&buffer[i * 6], "0x%02x, ", msg[i]); + pos = buf_append_printf(buffer, sizeof(buffer), pos, "0x%02x, ", msg[i]); } ESP_LOGD(TAG, "[%s]", buffer); +#endif } void LightWaveRF::dump_config() { diff --git a/esphome/components/rf_bridge/rf_bridge.cpp b/esphome/components/rf_bridge/rf_bridge.cpp index 52ce037dbed..8105767485a 100644 --- a/esphome/components/rf_bridge/rf_bridge.cpp +++ b/esphome/components/rf_bridge/rf_bridge.cpp @@ -1,6 +1,7 @@ #include "rf_bridge.h" -#include "esphome/core/log.h" #include "esphome/core/application.h" +#include "esphome/core/helpers.h" +#include "esphome/core/log.h" #include #include @@ -72,9 +73,9 @@ bool RFBridgeComponent::parse_bridge_byte_(uint8_t byte) { data.length = raw[2]; data.protocol = raw[3]; - char next_byte[3]; + char next_byte[3]; // 2 hex chars + null for (uint8_t i = 0; i < data.length - 1; i++) { - sprintf(next_byte, "%02X", raw[4 + i]); + buf_append_printf(next_byte, sizeof(next_byte), 0, "%02X", raw[4 + i]); data.code += next_byte; } @@ -90,10 +91,10 @@ bool RFBridgeComponent::parse_bridge_byte_(uint8_t byte) { uint8_t buckets = raw[2] << 1; std::string str; - char next_byte[3]; + char next_byte[3]; // 2 hex chars + null for (uint32_t i = 0; i <= at; i++) { - sprintf(next_byte, "%02X", raw[i]); + buf_append_printf(next_byte, sizeof(next_byte), 0, "%02X", raw[i]); str += next_byte; if ((i > 3) && buckets) { buckets--; diff --git a/esphome/components/spi_led_strip/spi_led_strip.cpp b/esphome/components/spi_led_strip/spi_led_strip.cpp index afb51afe3a3..ff8d2e6ee0d 100644 --- a/esphome/components/spi_led_strip/spi_led_strip.cpp +++ b/esphome/components/spi_led_strip/spi_led_strip.cpp @@ -1,4 +1,5 @@ #include "spi_led_strip.h" +#include "esphome/core/helpers.h" namespace esphome { namespace spi_led_strip { @@ -47,15 +48,14 @@ void SpiLedStrip::dump_config() { void SpiLedStrip::write_state(light::LightState *state) { if (this->is_failed()) return; - if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE) { - char strbuf[49]; - size_t len = std::min(this->buffer_size_, (size_t) (sizeof(strbuf) - 1) / 3); - memset(strbuf, 0, sizeof(strbuf)); - for (size_t i = 0; i != len; i++) { - sprintf(strbuf + i * 3, "%02X ", this->buf_[i]); - } +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE + { + char strbuf[49]; // format_hex_pretty_size(16) = 48, fits 16 bytes + size_t len = std::min(this->buffer_size_, (size_t) 16); + format_hex_pretty_to(strbuf, sizeof(strbuf), this->buf_, len, ' '); esph_log_v(TAG, "write_state: buf = %s", strbuf); } +#endif this->enable(); this->write_array(this->buf_, this->buffer_size_); this->disable(); From 5d7b38b26164940883c2d45715d20b85cb7de874 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 19 Jan 2026 17:38:22 -1000 Subject: [PATCH 18/19] [ezo_pmp] Replace sprintf with bounds-checked snprintf (#13304) --- esphome/components/ezo_pmp/ezo_pmp.cpp | 47 ++++++++++++++------------ 1 file changed, 25 insertions(+), 22 deletions(-) diff --git a/esphome/components/ezo_pmp/ezo_pmp.cpp b/esphome/components/ezo_pmp/ezo_pmp.cpp index 61b601328a7..9d2f4fc6878 100644 --- a/esphome/components/ezo_pmp/ezo_pmp.cpp +++ b/esphome/components/ezo_pmp/ezo_pmp.cpp @@ -318,90 +318,93 @@ void EzoPMP::send_next_command_() { switch (this->next_command_) { // Read Commands case EZO_PMP_COMMAND_READ_DOSING: // Page 54 - command_buffer_length = sprintf((char *) command_buffer, "D,?"); + command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "D,?"); break; case EZO_PMP_COMMAND_READ_SINGLE_REPORT: // Single Report (page 53) - command_buffer_length = sprintf((char *) command_buffer, "R"); + command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "R"); break; case EZO_PMP_COMMAND_READ_MAX_FLOW_RATE: - command_buffer_length = sprintf((char *) command_buffer, "DC,?"); + command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "DC,?"); break; case EZO_PMP_COMMAND_READ_PAUSE_STATUS: - command_buffer_length = sprintf((char *) command_buffer, "P,?"); + command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "P,?"); break; case EZO_PMP_COMMAND_READ_TOTAL_VOLUME_DOSED: - command_buffer_length = sprintf((char *) command_buffer, "TV,?"); + command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "TV,?"); break; case EZO_PMP_COMMAND_READ_ABSOLUTE_TOTAL_VOLUME_DOSED: - command_buffer_length = sprintf((char *) command_buffer, "ATV,?"); + command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "ATV,?"); break; case EZO_PMP_COMMAND_READ_CALIBRATION_STATUS: - command_buffer_length = sprintf((char *) command_buffer, "Cal,?"); + command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "Cal,?"); break; case EZO_PMP_COMMAND_READ_PUMP_VOLTAGE: - command_buffer_length = sprintf((char *) command_buffer, "PV,?"); + command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "PV,?"); break; // Non-Read Commands case EZO_PMP_COMMAND_FIND: // Find (page 52) - command_buffer_length = sprintf((char *) command_buffer, "Find"); + command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "Find"); wait_time_for_command = 60000; // This command will block all updates for a minute break; case EZO_PMP_COMMAND_DOSE_CONTINUOUSLY: // Continuous Dispensing (page 54) - command_buffer_length = sprintf((char *) command_buffer, "D,*"); + command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "D,*"); break; case EZO_PMP_COMMAND_CLEAR_TOTAL_VOLUME_DOSED: // Clear Total Volume Dosed (page 64) - command_buffer_length = sprintf((char *) command_buffer, "Clear"); + command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "Clear"); break; case EZO_PMP_COMMAND_CLEAR_CALIBRATION: // Clear Calibration (page 65) - command_buffer_length = sprintf((char *) command_buffer, "Cal,clear"); + command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "Cal,clear"); break; case EZO_PMP_COMMAND_PAUSE_DOSING: // Pause (page 61) - command_buffer_length = sprintf((char *) command_buffer, "P"); + command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "P"); break; case EZO_PMP_COMMAND_STOP_DOSING: // Stop (page 62) - command_buffer_length = sprintf((char *) command_buffer, "X"); + command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "X"); break; // Non-Read commands with parameters case EZO_PMP_COMMAND_DOSE_VOLUME: // Volume Dispensing (page 55) - command_buffer_length = sprintf((char *) command_buffer, "D,%0.1f", this->next_command_volume_); + command_buffer_length = + snprintf((char *) command_buffer, sizeof(command_buffer), "D,%0.1f", this->next_command_volume_); break; case EZO_PMP_COMMAND_DOSE_VOLUME_OVER_TIME: // Dose over time (page 56) - command_buffer_length = - sprintf((char *) command_buffer, "D,%0.1f,%i", this->next_command_volume_, this->next_command_duration_); + command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "D,%0.1f,%i", + this->next_command_volume_, this->next_command_duration_); break; case EZO_PMP_COMMAND_DOSE_WITH_CONSTANT_FLOW_RATE: // Constant Flow Rate (page 57) - command_buffer_length = - sprintf((char *) command_buffer, "DC,%0.1f,%i", this->next_command_volume_, this->next_command_duration_); + command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "DC,%0.1f,%i", + this->next_command_volume_, this->next_command_duration_); break; case EZO_PMP_COMMAND_SET_CALIBRATION_VOLUME: // Set Calibration Volume (page 65) - command_buffer_length = sprintf((char *) command_buffer, "Cal,%0.2f", this->next_command_volume_); + command_buffer_length = + snprintf((char *) command_buffer, sizeof(command_buffer), "Cal,%0.2f", this->next_command_volume_); break; case EZO_PMP_COMMAND_CHANGE_I2C_ADDRESS: // Change I2C Address (page 73) - command_buffer_length = sprintf((char *) command_buffer, "I2C,%i", this->next_command_duration_); + command_buffer_length = + snprintf((char *) command_buffer, sizeof(command_buffer), "I2C,%i", this->next_command_duration_); break; case EZO_PMP_COMMAND_EXEC_ARBITRARY_COMMAND_ADDRESS: // Run an arbitrary command - command_buffer_length = sprintf((char *) command_buffer, this->arbitrary_command_, this->next_command_duration_); + command_buffer_length = snprintf((char *) command_buffer, sizeof(command_buffer), "%s", this->arbitrary_command_); ESP_LOGI(TAG, "Sending arbitrary command: %s", (char *) command_buffer); break; From ea70faf642e12f6d6cab14b70cee6861bda1cdc0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 19 Jan 2026 17:38:56 -1000 Subject: [PATCH 19/19] [debug] Use shared buf_append_printf helper from core (#13260) --- esphome/components/debug/debug_component.cpp | 2 +- esphome/components/debug/debug_component.h | 41 +------------------- esphome/components/debug/debug_esp32.cpp | 30 +++++++------- esphome/components/debug/debug_esp8266.cpp | 22 +++++------ esphome/components/debug/debug_libretiny.cpp | 12 +++--- esphome/components/debug/debug_rp2040.cpp | 2 +- esphome/components/debug/debug_zephyr.cpp | 20 +++++----- 7 files changed, 45 insertions(+), 84 deletions(-) diff --git a/esphome/components/debug/debug_component.cpp b/esphome/components/debug/debug_component.cpp index ae38fb2ccdc..15f68c3a3b1 100644 --- a/esphome/components/debug/debug_component.cpp +++ b/esphome/components/debug/debug_component.cpp @@ -30,7 +30,7 @@ void DebugComponent::dump_config() { char device_info_buffer[DEVICE_INFO_BUFFER_SIZE]; ESP_LOGD(TAG, "ESPHome version %s", ESPHOME_VERSION); - size_t pos = buf_append(device_info_buffer, DEVICE_INFO_BUFFER_SIZE, 0, "%s", ESPHOME_VERSION); + size_t pos = buf_append_printf(device_info_buffer, DEVICE_INFO_BUFFER_SIZE, 0, "%s", ESPHOME_VERSION); this->free_heap_ = get_free_heap_(); ESP_LOGD(TAG, "Free Heap Size: %" PRIu32 " bytes", this->free_heap_); diff --git a/esphome/components/debug/debug_component.h b/esphome/components/debug/debug_component.h index 6cf52d890c1..e4f4bb36eba 100644 --- a/esphome/components/debug/debug_component.h +++ b/esphome/components/debug/debug_component.h @@ -5,12 +5,6 @@ #include "esphome/core/helpers.h" #include "esphome/core/macros.h" #include -#include -#include -#include -#ifdef USE_ESP8266 -#include -#endif #ifdef USE_SENSOR #include "esphome/components/sensor/sensor.h" @@ -25,40 +19,7 @@ namespace debug { static constexpr size_t DEVICE_INFO_BUFFER_SIZE = 256; static constexpr size_t RESET_REASON_BUFFER_SIZE = 128; -#ifdef USE_ESP8266 -// ESP8266: Use vsnprintf_P to keep format strings in flash (PROGMEM) -// Format strings must be wrapped with PSTR() macro -inline size_t buf_append_p(char *buf, size_t size, size_t pos, PGM_P fmt, ...) { - if (pos >= size) { - return size; - } - va_list args; - va_start(args, fmt); - int written = vsnprintf_P(buf + pos, size - pos, fmt, args); - va_end(args); - if (written < 0) { - return pos; // encoding error - } - return std::min(pos + static_cast(written), size); -} -#define buf_append(buf, size, pos, fmt, ...) buf_append_p(buf, size, pos, PSTR(fmt), ##__VA_ARGS__) -#else -/// Safely append formatted string to buffer, returning new position (capped at size) -__attribute__((format(printf, 4, 5))) inline size_t buf_append(char *buf, size_t size, size_t pos, const char *fmt, - ...) { - if (pos >= size) { - return size; - } - va_list args; - va_start(args, fmt); - int written = vsnprintf(buf + pos, size - pos, fmt, args); - va_end(args); - if (written < 0) { - return pos; // encoding error - } - return std::min(pos + static_cast(written), size); -} -#endif +// buf_append_printf is now provided by esphome/core/helpers.h class DebugComponent : public PollingComponent { public: diff --git a/esphome/components/debug/debug_esp32.cpp b/esphome/components/debug/debug_esp32.cpp index 8c41011f7d9..aad4c7426c9 100644 --- a/esphome/components/debug/debug_esp32.cpp +++ b/esphome/components/debug/debug_esp32.cpp @@ -173,8 +173,8 @@ size_t DebugComponent::get_device_info_(std::span uint32_t flash_size = ESP.getFlashChipSize() / 1024; // NOLINT uint32_t flash_speed = ESP.getFlashChipSpeed() / 1000000; // NOLINT ESP_LOGD(TAG, "Flash Chip: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed, flash_mode); - pos = buf_append(buf, size, pos, "|Flash: %" PRIu32 "kB Speed:%" PRIu32 "MHz Mode:%s", flash_size, flash_speed, - flash_mode); + pos = buf_append_printf(buf, size, pos, "|Flash: %" PRIu32 "kB Speed:%" PRIu32 "MHz Mode:%s", flash_size, flash_speed, + flash_mode); #endif esp_chip_info_t info; @@ -182,52 +182,52 @@ size_t DebugComponent::get_device_info_(std::span const char *model = ESPHOME_VARIANT; // Build features string - pos = buf_append(buf, size, pos, "|Chip: %s Features:", model); + pos = buf_append_printf(buf, size, pos, "|Chip: %s Features:", model); bool first_feature = true; for (const auto &feature : CHIP_FEATURES) { if (info.features & feature.bit) { - pos = buf_append(buf, size, pos, "%s%s", first_feature ? "" : ", ", feature.name); + pos = buf_append_printf(buf, size, pos, "%s%s", first_feature ? "" : ", ", feature.name); first_feature = false; info.features &= ~feature.bit; } } if (info.features != 0) { - pos = buf_append(buf, size, pos, "%sOther:0x%" PRIx32, first_feature ? "" : ", ", info.features); + pos = buf_append_printf(buf, size, pos, "%sOther:0x%" PRIx32, first_feature ? "" : ", ", info.features); } ESP_LOGD(TAG, "Chip: Model=%s, Cores=%u, Revision=%u", model, info.cores, info.revision); - pos = buf_append(buf, size, pos, " Cores:%u Revision:%u", info.cores, info.revision); + pos = buf_append_printf(buf, size, pos, " Cores:%u Revision:%u", info.cores, info.revision); uint32_t cpu_freq_mhz = arch_get_cpu_freq_hz() / 1000000; ESP_LOGD(TAG, "CPU Frequency: %" PRIu32 " MHz", cpu_freq_mhz); - pos = buf_append(buf, size, pos, "|CPU Frequency: %" PRIu32 " MHz", cpu_freq_mhz); + pos = buf_append_printf(buf, size, pos, "|CPU Frequency: %" PRIu32 " MHz", cpu_freq_mhz); // Framework detection #ifdef USE_ARDUINO ESP_LOGD(TAG, "Framework: Arduino"); - pos = buf_append(buf, size, pos, "|Framework: Arduino"); + pos = buf_append_printf(buf, size, pos, "|Framework: Arduino"); #elif defined(USE_ESP32) ESP_LOGD(TAG, "Framework: ESP-IDF"); - pos = buf_append(buf, size, pos, "|Framework: ESP-IDF"); + pos = buf_append_printf(buf, size, pos, "|Framework: ESP-IDF"); #else ESP_LOGW(TAG, "Framework: UNKNOWN"); - pos = buf_append(buf, size, pos, "|Framework: UNKNOWN"); + pos = buf_append_printf(buf, size, pos, "|Framework: UNKNOWN"); #endif ESP_LOGD(TAG, "ESP-IDF Version: %s", esp_get_idf_version()); - pos = buf_append(buf, size, pos, "|ESP-IDF: %s", esp_get_idf_version()); + pos = buf_append_printf(buf, size, pos, "|ESP-IDF: %s", esp_get_idf_version()); uint8_t mac[6]; get_mac_address_raw(mac); ESP_LOGD(TAG, "EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); - pos = buf_append(buf, size, pos, "|EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], mac[4], - mac[5]); + pos = buf_append_printf(buf, size, pos, "|EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3], + mac[4], mac[5]); char reason_buffer[RESET_REASON_BUFFER_SIZE]; const char *reset_reason = get_reset_reason_(std::span(reason_buffer)); - pos = buf_append(buf, size, pos, "|Reset: %s", reset_reason); + pos = buf_append_printf(buf, size, pos, "|Reset: %s", reset_reason); const char *wakeup_cause = get_wakeup_cause_(std::span(reason_buffer)); - pos = buf_append(buf, size, pos, "|Wakeup: %s", wakeup_cause); + pos = buf_append_printf(buf, size, pos, "|Wakeup: %s", wakeup_cause); return pos; } diff --git a/esphome/components/debug/debug_esp8266.cpp b/esphome/components/debug/debug_esp8266.cpp index 274f77e20d9..19f15d7d988 100644 --- a/esphome/components/debug/debug_esp8266.cpp +++ b/esphome/components/debug/debug_esp8266.cpp @@ -53,8 +53,8 @@ size_t DebugComponent::get_device_info_(std::span uint32_t flash_size = ESP.getFlashChipSize() / 1024; // NOLINT uint32_t flash_speed = ESP.getFlashChipSpeed() / 1000000; // NOLINT ESP_LOGD(TAG, "Flash Chip: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed, flash_mode); - pos = buf_append(buf, size, pos, "|Flash: %" PRIu32 "kB Speed:%" PRIu32 "MHz Mode:%s", flash_size, flash_speed, - flash_mode); + pos = buf_append_printf(buf, size, pos, "|Flash: %" PRIu32 "kB Speed:%" PRIu32 "MHz Mode:%s", flash_size, flash_speed, + flash_mode); #if !defined(CLANG_TIDY) char reason_buffer[RESET_REASON_BUFFER_SIZE]; @@ -77,15 +77,15 @@ size_t DebugComponent::get_device_info_(std::span chip_id, ESP.getSdkVersion(), ESP.getCoreVersion().c_str(), boot_version, boot_mode, cpu_freq, flash_chip_id, reset_reason, ESP.getResetInfo().c_str()); - pos = buf_append(buf, size, pos, "|Chip: 0x%08" PRIX32, chip_id); - pos = buf_append(buf, size, pos, "|SDK: %s", ESP.getSdkVersion()); - pos = buf_append(buf, size, pos, "|Core: %s", ESP.getCoreVersion().c_str()); - pos = buf_append(buf, size, pos, "|Boot: %u", boot_version); - pos = buf_append(buf, size, pos, "|Mode: %u", boot_mode); - pos = buf_append(buf, size, pos, "|CPU: %u", cpu_freq); - pos = buf_append(buf, size, pos, "|Flash: 0x%08" PRIX32, flash_chip_id); - pos = buf_append(buf, size, pos, "|Reset: %s", reset_reason); - pos = buf_append(buf, size, pos, "|%s", ESP.getResetInfo().c_str()); + pos = buf_append_printf(buf, size, pos, "|Chip: 0x%08" PRIX32, chip_id); + pos = buf_append_printf(buf, size, pos, "|SDK: %s", ESP.getSdkVersion()); + pos = buf_append_printf(buf, size, pos, "|Core: %s", ESP.getCoreVersion().c_str()); + pos = buf_append_printf(buf, size, pos, "|Boot: %u", boot_version); + pos = buf_append_printf(buf, size, pos, "|Mode: %u", boot_mode); + pos = buf_append_printf(buf, size, pos, "|CPU: %u", cpu_freq); + pos = buf_append_printf(buf, size, pos, "|Flash: 0x%08" PRIX32, flash_chip_id); + pos = buf_append_printf(buf, size, pos, "|Reset: %s", reset_reason); + pos = buf_append_printf(buf, size, pos, "|%s", ESP.getResetInfo().c_str()); #endif return pos; diff --git a/esphome/components/debug/debug_libretiny.cpp b/esphome/components/debug/debug_libretiny.cpp index aae27c8ca26..14bbdb945a9 100644 --- a/esphome/components/debug/debug_libretiny.cpp +++ b/esphome/components/debug/debug_libretiny.cpp @@ -36,12 +36,12 @@ size_t DebugComponent::get_device_info_(std::span lt_get_version(), lt_cpu_get_model_name(), lt_cpu_get_model(), lt_cpu_get_freq_mhz(), mac_id, lt_get_board_code(), flash_kib, ram_kib, reset_reason); - pos = buf_append(buf, size, pos, "|Version: %s", LT_BANNER_STR + 10); - pos = buf_append(buf, size, pos, "|Reset Reason: %s", reset_reason); - pos = buf_append(buf, size, pos, "|Chip Name: %s", lt_cpu_get_model_name()); - pos = buf_append(buf, size, pos, "|Chip ID: 0x%06" PRIX32, mac_id); - pos = buf_append(buf, size, pos, "|Flash: %" PRIu32 " KiB", flash_kib); - pos = buf_append(buf, size, pos, "|RAM: %" PRIu32 " KiB", ram_kib); + pos = buf_append_printf(buf, size, pos, "|Version: %s", LT_BANNER_STR + 10); + pos = buf_append_printf(buf, size, pos, "|Reset Reason: %s", reset_reason); + pos = buf_append_printf(buf, size, pos, "|Chip Name: %s", lt_cpu_get_model_name()); + pos = buf_append_printf(buf, size, pos, "|Chip ID: 0x%06" PRIX32, mac_id); + pos = buf_append_printf(buf, size, pos, "|Flash: %" PRIu32 " KiB", flash_kib); + pos = buf_append_printf(buf, size, pos, "|RAM: %" PRIu32 " KiB", ram_kib); return pos; } diff --git a/esphome/components/debug/debug_rp2040.cpp b/esphome/components/debug/debug_rp2040.cpp index a426a73bc21..c9d41942dbc 100644 --- a/esphome/components/debug/debug_rp2040.cpp +++ b/esphome/components/debug/debug_rp2040.cpp @@ -19,7 +19,7 @@ size_t DebugComponent::get_device_info_(std::span uint32_t cpu_freq = rp2040.f_cpu(); ESP_LOGD(TAG, "CPU Frequency: %" PRIu32, cpu_freq); - pos = buf_append(buf, size, pos, "|CPU Frequency: %" PRIu32, cpu_freq); + pos = buf_append_printf(buf, size, pos, "|CPU Frequency: %" PRIu32, cpu_freq); return pos; } diff --git a/esphome/components/debug/debug_zephyr.cpp b/esphome/components/debug/debug_zephyr.cpp index 3f9af03b2be..6a88522b0dc 100644 --- a/esphome/components/debug/debug_zephyr.cpp +++ b/esphome/components/debug/debug_zephyr.cpp @@ -20,9 +20,9 @@ static size_t append_reset_reason(char *buf, size_t size, size_t pos, bool set, return pos; } if (pos > 0) { - pos = buf_append(buf, size, pos, ", "); + pos = buf_append_printf(buf, size, pos, ", "); } - return buf_append(buf, size, pos, "%s", reason); + return buf_append_printf(buf, size, pos, "%s", reason); } static inline uint32_t read_mem_u32(uintptr_t addr) { @@ -140,7 +140,7 @@ size_t DebugComponent::get_device_info_(std::span const char *supply_status = (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_NORMAL) ? "Normal voltage." : "High voltage."; ESP_LOGD(TAG, "Main supply status: %s", supply_status); - pos = buf_append(buf, size, pos, "|Main supply status: %s", supply_status); + pos = buf_append_printf(buf, size, pos, "|Main supply status: %s", supply_status); // Regulator stage 0 if (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_HIGH) { @@ -172,16 +172,16 @@ size_t DebugComponent::get_device_info_(std::span reg0_voltage = "???V"; } ESP_LOGD(TAG, "Regulator stage 0: %s, %s", reg0_type, reg0_voltage); - pos = buf_append(buf, size, pos, "|Regulator stage 0: %s, %s", reg0_type, reg0_voltage); + pos = buf_append_printf(buf, size, pos, "|Regulator stage 0: %s, %s", reg0_type, reg0_voltage); } else { ESP_LOGD(TAG, "Regulator stage 0: disabled"); - pos = buf_append(buf, size, pos, "|Regulator stage 0: disabled"); + pos = buf_append_printf(buf, size, pos, "|Regulator stage 0: disabled"); } // Regulator stage 1 const char *reg1_type = nrf_power_dcdcen_get(NRF_POWER) ? "DC/DC" : "LDO"; ESP_LOGD(TAG, "Regulator stage 1: %s", reg1_type); - pos = buf_append(buf, size, pos, "|Regulator stage 1: %s", reg1_type); + pos = buf_append_printf(buf, size, pos, "|Regulator stage 1: %s", reg1_type); // USB power state const char *usb_state; @@ -195,7 +195,7 @@ size_t DebugComponent::get_device_info_(std::span usb_state = "disconnected"; } ESP_LOGD(TAG, "USB power state: %s", usb_state); - pos = buf_append(buf, size, pos, "|USB power state: %s", usb_state); + pos = buf_append_printf(buf, size, pos, "|USB power state: %s", usb_state); // Power-fail comparator bool enabled; @@ -300,14 +300,14 @@ size_t DebugComponent::get_device_info_(std::span break; } ESP_LOGD(TAG, "Power-fail comparator: %s, VDDH: %s", pof_voltage, vddh_voltage); - pos = buf_append(buf, size, pos, "|Power-fail comparator: %s, VDDH: %s", pof_voltage, vddh_voltage); + pos = buf_append_printf(buf, size, pos, "|Power-fail comparator: %s, VDDH: %s", pof_voltage, vddh_voltage); } else { ESP_LOGD(TAG, "Power-fail comparator: %s", pof_voltage); - pos = buf_append(buf, size, pos, "|Power-fail comparator: %s", pof_voltage); + pos = buf_append_printf(buf, size, pos, "|Power-fail comparator: %s", pof_voltage); } } else { ESP_LOGD(TAG, "Power-fail comparator: disabled"); - pos = buf_append(buf, size, pos, "|Power-fail comparator: disabled"); + pos = buf_append_printf(buf, size, pos, "|Power-fail comparator: disabled"); } auto package = [](uint32_t value) {