From 98ed679b1995d2b96ab85cdc9f93a4d593e8331c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Dec 2025 19:22:56 +0000 Subject: [PATCH 1/2] Bump ruff from 0.14.9 to 0.14.10 (#12572) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- .pre-commit-config.yaml | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2f5076a6e6..de7d30cfa2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.14.9 + rev: v0.14.10 hooks: # Run the linter. - id: ruff diff --git a/requirements_test.txt b/requirements_test.txt index bfb833e04d..f00bcd0a0d 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==4.0.4 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.14.9 # also change in .pre-commit-config.yaml when updating +ruff==0.14.10 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating pre-commit From b2a43a3a696f6e3d45768d640697f4cf2984eb36 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 Dec 2025 10:18:58 -1000 Subject: [PATCH 2/2] [web_server] Use stack buffers for value formatting to reduce flash usage --- esphome/components/web_server/web_server.cpp | 80 +++++++++++--------- esphome/core/helpers.cpp | 26 ++++--- esphome/core/helpers.h | 11 ++- 3 files changed, 68 insertions(+), 49 deletions(-) diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 0c22c2f08d..d0a00d7598 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -428,12 +428,19 @@ static void set_json_value(JsonObject &root, EntityBase *obj, const char *prefix } template -static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const char *prefix, const std::string &state, +static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const char *prefix, const char *state, const T &value, JsonDetail start_config) { set_json_value(root, obj, prefix, value, start_config); root[ESPHOME_F("state")] = state; } +// Macros for stack-based value formatting (avoid heap allocation) +#define VALUE_BUF char _vbuf_[VALUE_ACCURACY_MAX_LEN] +#define VALUE_OR_NA(value, decimals) \ + (std::isnan(value) ? "NA" : (value_accuracy_to_buf(_vbuf_, value, decimals), _vbuf_)) +#define VALUE_UOM_OR_NA(value, decimals, uom) \ + (std::isnan(value) ? "NA" : (value_accuracy_with_uom_to_buf(_vbuf_, value, decimals, uom), _vbuf_)) + // Helper to get request detail parameter static JsonDetail get_request_detail(AsyncWebServerRequest *request) { auto *param = request->getParam("detail"); @@ -472,9 +479,9 @@ std::string WebServer::sensor_json(sensor::Sensor *obj, float value, JsonDetail const auto uom_ref = obj->get_unit_of_measurement_ref(); - std::string state = - std::isnan(value) ? "NA" : value_accuracy_with_uom_to_string(value, obj->get_accuracy_decimals(), uom_ref); - set_json_icon_state_value(root, obj, "sensor", state, value, start_config); + VALUE_BUF; + set_json_icon_state_value(root, obj, "sensor", VALUE_UOM_OR_NA(value, obj->get_accuracy_decimals(), uom_ref), value, + start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); if (!uom_ref.empty()) @@ -518,7 +525,7 @@ std::string WebServer::text_sensor_json(text_sensor::TextSensor *obj, const std: json::JsonBuilder builder; JsonObject root = builder.root(); - set_json_icon_state_value(root, obj, "text_sensor", value, value, start_config); + set_json_icon_state_value(root, obj, "text_sensor", value.c_str(), value.c_str(), start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); } @@ -970,21 +977,21 @@ std::string WebServer::number_json(number::Number *obj, float value, JsonDetail JsonObject root = builder.root(); const auto uom_ref = obj->traits.get_unit_of_measurement_ref(); + const int8_t accuracy = step_to_accuracy_decimals(obj->traits.get_step()); - std::string val_str = std::isnan(value) - ? "\"NaN\"" - : value_accuracy_to_string(value, step_to_accuracy_decimals(obj->traits.get_step())); - std::string state_str = std::isnan(value) ? "NA" - : value_accuracy_with_uom_to_string( - value, step_to_accuracy_decimals(obj->traits.get_step()), uom_ref); - set_json_icon_state_value(root, obj, "number", state_str, val_str, start_config); + // Need two buffers: one for value, one for state with UOM + char val_buf[VALUE_ACCURACY_MAX_LEN]; + const char *val_str = std::isnan(value) ? "\"NaN\"" : (value_accuracy_to_buf(val_buf, value, accuracy), val_buf); + VALUE_BUF; + set_json_icon_state_value(root, obj, "number", VALUE_UOM_OR_NA(value, accuracy, uom_ref), val_str, start_config); if (start_config == DETAIL_ALL) { - root[ESPHOME_F("min_value")] = - value_accuracy_to_string(obj->traits.get_min_value(), step_to_accuracy_decimals(obj->traits.get_step())); - root[ESPHOME_F("max_value")] = - value_accuracy_to_string(obj->traits.get_max_value(), step_to_accuracy_decimals(obj->traits.get_step())); - root[ESPHOME_F("step")] = - value_accuracy_to_string(obj->traits.get_step(), step_to_accuracy_decimals(obj->traits.get_step())); + // Reuse val_buf for these - ArduinoJson copies the string + value_accuracy_to_buf(val_buf, obj->traits.get_min_value(), accuracy); + root[ESPHOME_F("min_value")] = val_buf; + value_accuracy_to_buf(val_buf, obj->traits.get_max_value(), accuracy); + root[ESPHOME_F("max_value")] = val_buf; + value_accuracy_to_buf(val_buf, obj->traits.get_step(), accuracy); + root[ESPHOME_F("step")] = val_buf; root[ESPHOME_F("mode")] = (int) obj->traits.get_mode(); if (!uom_ref.empty()) root[ESPHOME_F("uom")] = uom_ref; @@ -1043,7 +1050,7 @@ std::string WebServer::date_json(datetime::DateEntity *obj, JsonDetail start_con JsonObject root = builder.root(); std::string value = str_sprintf("%d-%02d-%02d", obj->year, obj->month, obj->day); - set_json_icon_state_value(root, obj, "date", value, value, start_config); + set_json_icon_state_value(root, obj, "date", value.c_str(), value.c_str(), start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); } @@ -1099,7 +1106,7 @@ std::string WebServer::time_json(datetime::TimeEntity *obj, JsonDetail start_con JsonObject root = builder.root(); std::string value = str_sprintf("%02d:%02d:%02d", obj->hour, obj->minute, obj->second); - set_json_icon_state_value(root, obj, "time", value, value, start_config); + set_json_icon_state_value(root, obj, "time", value.c_str(), value.c_str(), start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); } @@ -1156,7 +1163,7 @@ std::string WebServer::datetime_json(datetime::DateTimeEntity *obj, JsonDetail s std::string value = str_sprintf("%d-%02d-%02d %02d:%02d:%02d", obj->year, obj->month, obj->day, obj->hour, obj->minute, obj->second); - set_json_icon_state_value(root, obj, "datetime", value, value, start_config); + set_json_icon_state_value(root, obj, "datetime", value.c_str(), value.c_str(), start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); } @@ -1207,8 +1214,8 @@ std::string WebServer::text_json(text::Text *obj, const std::string &value, Json json::JsonBuilder builder; JsonObject root = builder.root(); - std::string state = obj->traits.get_mode() == text::TextMode::TEXT_MODE_PASSWORD ? "********" : value; - set_json_icon_state_value(root, obj, "text", state, value, start_config); + const char *state = obj->traits.get_mode() == text::TextMode::TEXT_MODE_PASSWORD ? "********" : value.c_str(); + set_json_icon_state_value(root, obj, "text", state, value.c_str(), start_config); root[ESPHOME_F("min_length")] = obj->traits.get_min_length(); root[ESPHOME_F("max_length")] = obj->traits.get_max_length(); root[ESPHOME_F("pattern")] = obj->traits.get_pattern_c_str(); @@ -1336,6 +1343,7 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals(); int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals(); char buf[PSTR_LOCAL_SIZE]; + VALUE_BUF; // For temperature formatting if (start_config == DETAIL_ALL) { JsonArray opt = root[ESPHOME_F("modes")].to(); @@ -1372,8 +1380,10 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf bool has_state = false; root[ESPHOME_F("mode")] = PSTR_LOCAL(climate_mode_to_string(obj->mode)); - root[ESPHOME_F("max_temp")] = value_accuracy_to_string(traits.get_visual_max_temperature(), target_accuracy); - root[ESPHOME_F("min_temp")] = value_accuracy_to_string(traits.get_visual_min_temperature(), target_accuracy); + root[ESPHOME_F("max_temp")] = + (value_accuracy_to_buf(_vbuf_, traits.get_visual_max_temperature(), target_accuracy), _vbuf_); + root[ESPHOME_F("min_temp")] = + (value_accuracy_to_buf(_vbuf_, traits.get_visual_min_temperature(), target_accuracy), _vbuf_); root[ESPHOME_F("step")] = traits.get_visual_target_temperature_step(); if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) { root[ESPHOME_F("action")] = PSTR_LOCAL(climate_action_to_string(obj->action)); @@ -1396,23 +1406,23 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf root[ESPHOME_F("swing_mode")] = PSTR_LOCAL(climate_swing_mode_to_string(obj->swing_mode)); } if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) { - if (!std::isnan(obj->current_temperature)) { - root[ESPHOME_F("current_temperature")] = value_accuracy_to_string(obj->current_temperature, current_accuracy); - } else { - root[ESPHOME_F("current_temperature")] = "NA"; - } + root[ESPHOME_F("current_temperature")] = VALUE_OR_NA(obj->current_temperature, current_accuracy); } if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) { - root[ESPHOME_F("target_temperature_low")] = value_accuracy_to_string(obj->target_temperature_low, target_accuracy); + root[ESPHOME_F("target_temperature_low")] = + (value_accuracy_to_buf(_vbuf_, obj->target_temperature_low, target_accuracy), _vbuf_); root[ESPHOME_F("target_temperature_high")] = - value_accuracy_to_string(obj->target_temperature_high, target_accuracy); + (value_accuracy_to_buf(_vbuf_, obj->target_temperature_high, target_accuracy), _vbuf_); if (!has_state) { - root[ESPHOME_F("state")] = value_accuracy_to_string( - (obj->target_temperature_high + obj->target_temperature_low) / 2.0f, target_accuracy); + root[ESPHOME_F("state")] = + (value_accuracy_to_buf(_vbuf_, (obj->target_temperature_high + obj->target_temperature_low) / 2.0f, + target_accuracy), + _vbuf_); } } else { - root[ESPHOME_F("target_temperature")] = value_accuracy_to_string(obj->target_temperature, target_accuracy); + root[ESPHOME_F("target_temperature")] = + (value_accuracy_to_buf(_vbuf_, obj->target_temperature, target_accuracy), _vbuf_); if (!has_state) root[ESPHOME_F("state")] = root[ESPHOME_F("target_temperature")]; } diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index bbe59e53f1..18cef6e0dc 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -385,23 +385,25 @@ static inline void normalize_accuracy_decimals(float &value, int8_t &accuracy_de } std::string value_accuracy_to_string(float value, int8_t accuracy_decimals) { - normalize_accuracy_decimals(value, accuracy_decimals); - char tmp[32]; // should be enough, but we should maybe improve this at some point. - snprintf(tmp, sizeof(tmp), "%.*f", accuracy_decimals, value); - return std::string(tmp); + char buf[VALUE_ACCURACY_MAX_LEN]; + value_accuracy_to_buf(buf, value, accuracy_decimals); + return std::string(buf); } -std::string value_accuracy_with_uom_to_string(float value, int8_t accuracy_decimals, StringRef unit_of_measurement) { +size_t value_accuracy_to_buf(std::span buf, float value, int8_t accuracy_decimals) { normalize_accuracy_decimals(value, accuracy_decimals); - // Buffer sized for float (up to ~15 chars) + space + typical UOM (usually <20 chars like "μS/cm") - // snprintf truncates safely if exceeded, though ESPHome UOMs are typically short - char tmp[64]; + int len = snprintf(buf.data(), buf.size(), "%.*f", accuracy_decimals, value); + return len > 0 ? std::min(static_cast(len), buf.size() - 1) : 0; +} + +size_t value_accuracy_with_uom_to_buf(std::span buf, float value, + int8_t accuracy_decimals, StringRef unit_of_measurement) { if (unit_of_measurement.empty()) { - snprintf(tmp, sizeof(tmp), "%.*f", accuracy_decimals, value); - } else { - snprintf(tmp, sizeof(tmp), "%.*f %s", accuracy_decimals, value, unit_of_measurement.c_str()); + return value_accuracy_to_buf(buf, value, accuracy_decimals); } - return std::string(tmp); + normalize_accuracy_decimals(value, accuracy_decimals); + int len = snprintf(buf.data(), buf.size(), "%.*f %s", accuracy_decimals, value, unit_of_measurement.c_str()); + return len > 0 ? std::min(static_cast(len), buf.size() - 1) : 0; } int8_t step_to_accuracy_decimals(float step) { diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index f9dcfccb45..29a6666eb0 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -867,8 +867,15 @@ ParseOnOffState parse_on_off(const char *str, const char *on = nullptr, const ch /// Create a string from a value and an accuracy in decimals. std::string value_accuracy_to_string(float value, int8_t accuracy_decimals); -/// Create a string from a value, an accuracy in decimals, and a unit of measurement. -std::string value_accuracy_with_uom_to_string(float value, int8_t accuracy_decimals, StringRef unit_of_measurement); + +/// Maximum buffer size for value_accuracy formatting (float ~15 chars + space + UOM ~40 chars + null) +static constexpr size_t VALUE_ACCURACY_MAX_LEN = 64; + +/// Format value with accuracy to buffer, returns chars written (excluding null) +size_t value_accuracy_to_buf(std::span buf, float value, int8_t accuracy_decimals); +/// Format value with accuracy and UOM to buffer, returns chars written (excluding null) +size_t value_accuracy_with_uom_to_buf(std::span buf, float value, + int8_t accuracy_decimals, StringRef unit_of_measurement); /// Derive accuracy in decimals from an increment step. int8_t step_to_accuracy_decimals(float step);