From f14d1edcc9154602c8afc710807630fff198ece8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 11 Jan 2026 14:33:31 -1000 Subject: [PATCH 1/3] [uptime] Format text sensor output on stack to avoid heap allocations --- .../uptime/text_sensor/uptime_text_sensor.cpp | 108 ++++++++++++------ .../uptime/text_sensor/uptime_text_sensor.h | 1 - 2 files changed, 75 insertions(+), 34 deletions(-) diff --git a/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp b/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp index 94585379fe..f4fe9b7e33 100644 --- a/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp +++ b/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp @@ -9,6 +9,19 @@ namespace uptime { static const char *const TAG = "uptime.sensor"; +// Cap position to prevent buffer overflow from snprintf return value +inline size_t clamp_buffer_pos(size_t pos, size_t buf_size) { return pos < buf_size ? pos : buf_size - 1; } + +static void append_unit(char *buf, size_t buf_size, size_t &pos, const char *separator, unsigned value, + const char *label) { + if (pos > 0) { + pos += snprintf(buf + pos, buf_size - pos, "%s", separator); + pos = clamp_buffer_pos(pos, buf_size); + } + pos += snprintf(buf + pos, buf_size - pos, "%u%s", value, label); + pos = clamp_buffer_pos(pos, buf_size); +} + void UptimeTextSensor::setup() { this->last_ms_ = millis(); if (this->last_ms_ < 60 * 1000) @@ -16,11 +29,6 @@ void UptimeTextSensor::setup() { this->update(); } -void UptimeTextSensor::insert_buffer_(std::string &buffer, const char *key, unsigned value) const { - buffer.insert(0, this->separator_); - buffer.insert(0, str_sprintf("%u%s", value, key)); -} - void UptimeTextSensor::update() { auto now = millis(); // get whole seconds since last update. Note that even if the millis count has overflowed between updates, @@ -29,36 +37,70 @@ void UptimeTextSensor::update() { this->last_ms_ = now - delta % 1000; // save remainder for next update delta /= 1000; this->uptime_ += delta; - auto uptime = this->uptime_; + uint32_t uptime = this->uptime_; unsigned interval = this->get_update_interval() / 1000; - std::string buffer{}; - // display from the largest unit that corresponds to the update interval, drop larger units that are zero. - while (true) { // enable use of break for early exit - unsigned remainder = uptime % 60; - uptime /= 60; - if (interval < 30) { - this->insert_buffer_(buffer, this->seconds_text_, remainder); - if (!this->expand_ && uptime == 0) - break; - } - remainder = uptime % 60; - uptime /= 60; - if (interval < 1800) { - this->insert_buffer_(buffer, this->minutes_text_, remainder); - if (!this->expand_ && uptime == 0) - break; - } - remainder = uptime % 24; - uptime /= 24; - if (interval < 12 * 3600) { - this->insert_buffer_(buffer, this->hours_text_, remainder); - if (!this->expand_ && uptime == 0) - break; - } - this->insert_buffer_(buffer, this->days_text_, (unsigned) uptime); - break; + + // Calculate all time units + unsigned seconds = uptime % 60; + uptime /= 60; + unsigned minutes = uptime % 60; + uptime /= 60; + unsigned hours = uptime % 24; + uptime /= 24; + unsigned days = uptime; + + // Determine which units to display based on interval thresholds + bool seconds_enabled = interval < 30; + bool minutes_enabled = interval < 1800; + bool hours_enabled = interval < 12 * 3600; + + // Determine which units to show + bool show_days, show_hours, show_minutes, show_seconds; + + if (this->expand_) { + // Show all enabled units + show_days = true; + show_hours = hours_enabled; + show_minutes = minutes_enabled; + show_seconds = seconds_enabled; + } else { + // Start with only the smallest enabled unit + show_seconds = seconds_enabled; + show_minutes = minutes_enabled && !show_seconds; + show_hours = hours_enabled && !show_minutes && !show_seconds; + show_days = !show_hours && !show_minutes && !show_seconds; + + // Add larger non-zero units + if (days > 0) + show_days = true; + if (hours > 0 && hours_enabled) + show_hours = true; + if (minutes > 0 && minutes_enabled) + show_minutes = true; + + // Fill in gaps (e.g., show 0h between 1d and 0m) + if (show_days && hours_enabled) + show_hours = true; + if (show_hours && minutes_enabled) + show_minutes = true; } - this->publish_state(buffer); + + // Build output string on stack + // Home Assistant max state length is 255 chars + null terminator + char buf[256]; + size_t pos = 0; + buf[0] = '\0'; // Initialize for empty case + + if (show_days) + append_unit(buf, sizeof(buf), pos, this->separator_, days, this->days_text_); + if (show_hours) + append_unit(buf, sizeof(buf), pos, this->separator_, hours, this->hours_text_); + if (show_minutes) + append_unit(buf, sizeof(buf), pos, this->separator_, minutes, this->minutes_text_); + if (show_seconds) + append_unit(buf, sizeof(buf), pos, this->separator_, seconds, this->seconds_text_); + + this->publish_state(buf); } float UptimeTextSensor::get_setup_priority() const { return setup_priority::HARDWARE; } diff --git a/esphome/components/uptime/text_sensor/uptime_text_sensor.h b/esphome/components/uptime/text_sensor/uptime_text_sensor.h index 8dd058998c..947d9c91e9 100644 --- a/esphome/components/uptime/text_sensor/uptime_text_sensor.h +++ b/esphome/components/uptime/text_sensor/uptime_text_sensor.h @@ -29,7 +29,6 @@ class UptimeTextSensor : public text_sensor::TextSensor, public PollingComponent void set_seconds(const char *seconds_text) { this->seconds_text_ = seconds_text; } protected: - void insert_buffer_(std::string &buffer, const char *key, unsigned value) const; const char *days_text_; const char *hours_text_; const char *minutes_text_; From c19e1298214ee435e86a1dc2a0674b890a2b7577 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 11 Jan 2026 14:40:50 -1000 Subject: [PATCH 2/3] another pass at reducing the logic --- .../uptime/text_sensor/uptime_text_sensor.cpp | 41 +++++++------------ 1 file changed, 14 insertions(+), 27 deletions(-) diff --git a/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp b/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp index f4fe9b7e33..368061093a 100644 --- a/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp +++ b/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp @@ -54,35 +54,22 @@ void UptimeTextSensor::update() { bool minutes_enabled = interval < 1800; bool hours_enabled = interval < 12 * 3600; - // Determine which units to show - bool show_days, show_hours, show_minutes, show_seconds; + // Show from highest non-zero unit (or all in expand mode) down to smallest enabled + bool show_days = this->expand_ || days > 0; + bool show_hours = hours_enabled && (show_days || hours > 0); + bool show_minutes = minutes_enabled && (show_hours || minutes > 0); + bool show_seconds = seconds_enabled && (show_minutes || seconds > 0); - if (this->expand_) { - // Show all enabled units - show_days = true; - show_hours = hours_enabled; - show_minutes = minutes_enabled; - show_seconds = seconds_enabled; - } else { - // Start with only the smallest enabled unit - show_seconds = seconds_enabled; - show_minutes = minutes_enabled && !show_seconds; - show_hours = hours_enabled && !show_minutes && !show_seconds; - show_days = !show_hours && !show_minutes && !show_seconds; - - // Add larger non-zero units - if (days > 0) + // If nothing shown, show smallest enabled unit + if (!show_days && !show_hours && !show_minutes && !show_seconds) { + if (seconds_enabled) + show_seconds = true; + else if (minutes_enabled) + show_minutes = true; + else if (hours_enabled) + show_hours = true; + else show_days = true; - if (hours > 0 && hours_enabled) - show_hours = true; - if (minutes > 0 && minutes_enabled) - show_minutes = true; - - // Fill in gaps (e.g., show 0h between 1d and 0m) - if (show_days && hours_enabled) - show_hours = true; - if (show_hours && minutes_enabled) - show_minutes = true; } // Build output string on stack From cdd09bdb94550d929ff11929e42f9b6f6d15cb51 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 11 Jan 2026 14:46:14 -1000 Subject: [PATCH 3/3] preen --- .../components/uptime/text_sensor/uptime_text_sensor.cpp | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp b/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp index 368061093a..e109576c62 100644 --- a/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp +++ b/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp @@ -62,14 +62,15 @@ void UptimeTextSensor::update() { // If nothing shown, show smallest enabled unit if (!show_days && !show_hours && !show_minutes && !show_seconds) { - if (seconds_enabled) + if (seconds_enabled) { show_seconds = true; - else if (minutes_enabled) + } else if (minutes_enabled) { show_minutes = true; - else if (hours_enabled) + } else if (hours_enabled) { show_hours = true; - else + } else { show_days = true; + } } // Build output string on stack