From 869678953dfbe22f2b784e8ec927bfa394676671 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Feb 2026 17:03:24 -0600 Subject: [PATCH 1/2] [core] Add pow10_int helper, replace powf in normalize_accuracy and sensor filters (#14114) Co-authored-by: Claude Opus 4.6 --- esphome/components/sensor/filter.cpp | 5 +++-- esphome/core/helpers.cpp | 11 +++++++++-- esphome/core/helpers.h | 15 +++++++++++++++ 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index cd4db984576..0fe1effe179 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -5,6 +5,7 @@ #include #include "esphome/core/application.h" #include "esphome/core/hal.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "sensor.h" @@ -240,7 +241,7 @@ ValueListFilter::ValueListFilter(std::initializer_list> bool ValueListFilter::value_matches_any_(float sensor_value) { int8_t accuracy = this->parent_->get_accuracy_decimals(); - float accuracy_mult = powf(10.0f, accuracy); + float accuracy_mult = pow10_int(accuracy); float rounded_sensor = roundf(accuracy_mult * sensor_value); for (auto &filter_value : this->values_) { @@ -472,7 +473,7 @@ optional ClampFilter::new_value(float value) { RoundFilter::RoundFilter(uint8_t precision) : precision_(precision) {} optional RoundFilter::new_value(float value) { if (std::isfinite(value)) { - float accuracy_mult = powf(10.0f, this->precision_); + float accuracy_mult = pow10_int(this->precision_); return roundf(accuracy_mult * value) / accuracy_mult; } return value; diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 9f850b5df8f..6d801e7ebc4 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -468,8 +468,15 @@ ParseOnOffState parse_on_off(const char *str, const char *on, const char *off) { static inline void normalize_accuracy_decimals(float &value, int8_t &accuracy_decimals) { if (accuracy_decimals < 0) { - auto multiplier = powf(10.0f, accuracy_decimals); - value = roundf(value * multiplier) / multiplier; + float divisor; + if (accuracy_decimals == -1) { + divisor = 10.0f; + } else if (accuracy_decimals == -2) { + divisor = 100.0f; + } else { + divisor = pow10_int(-accuracy_decimals); + } + value = roundf(value / divisor) * divisor; accuracy_decimals = 0; } } diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 298b93fbc46..b606e68df31 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -439,6 +439,21 @@ template class SmallBufferWithHeapFallb /// @name Mathematics ///@{ +/// Compute 10^exp using iterative multiplication/division. +/// Avoids pulling in powf/__ieee754_powf (~2.3KB flash) for small integer exponents. +/// Matches powf(10, exp) for the int8_t exponent range used by sensor accuracy_decimals. +inline float pow10_int(int8_t exp) { + float result = 1.0f; + if (exp >= 0) { + for (int8_t i = 0; i < exp; i++) + result *= 10.0f; + } else { + for (int8_t i = exp; i < 0; i++) + result /= 10.0f; + } + return result; +} + /// Remap \p value from the range (\p min, \p max) to (\p min_out, \p max_out). template T remap(U value, U min, U max, T min_out, T max_out) { return (value - min) * (max_out - min_out) / (max - min) + min_out; From 8db3f67be4c2331c4719b96ab1db953e6b2faf18 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Feb 2026 17:22:21 -0600 Subject: [PATCH 2/2] [light] Replace std::lerp with lightweight fast_lerp in LightColorValues::lerp std::lerp includes NaN/infinity edge-case handling per C++20 spec, which generates ~200 bytes of overhead per call. With 10 fields interpolated, this bloated the symbol to 2,038 bytes. Since all light color values are pre-clamped finite floats, a simple a + t * (b - a) is sufficient. Reduces LightColorValues::lerp from 2,038 B to 286 B (86% reduction). --- .../components/light/light_color_values.cpp | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/esphome/components/light/light_color_values.cpp b/esphome/components/light/light_color_values.cpp index 2f22bb3c688..391179acfc0 100644 --- a/esphome/components/light/light_color_values.cpp +++ b/esphome/components/light/light_color_values.cpp @@ -1,27 +1,29 @@ #include "light_color_values.h" -#include - namespace esphome::light { +// Lightweight lerp without std::lerp's NaN/infinity handling overhead. +// Safe here because all color values are pre-clamped finite floats. +static inline float fast_lerp(float a, float b, float t) { return a + t * (b - a); } + LightColorValues LightColorValues::lerp(const LightColorValues &start, const LightColorValues &end, float completion) { // Directly interpolate the raw values to avoid getter/setter overhead. // This is safe because: // - All LightColorValues have their values clamped when set via the setters - // - std::lerp guarantees output is in the same range as inputs + // - fast_lerp output stays in range when inputs are in range and 0 <= completion <= 1 // - Therefore the output doesn't need clamping, so we can skip the setters LightColorValues v; v.color_mode_ = end.color_mode_; - v.state_ = std::lerp(start.state_, end.state_, completion); - v.brightness_ = std::lerp(start.brightness_, end.brightness_, completion); - v.color_brightness_ = std::lerp(start.color_brightness_, end.color_brightness_, completion); - v.red_ = std::lerp(start.red_, end.red_, completion); - v.green_ = std::lerp(start.green_, end.green_, completion); - v.blue_ = std::lerp(start.blue_, end.blue_, completion); - v.white_ = std::lerp(start.white_, end.white_, completion); - v.color_temperature_ = std::lerp(start.color_temperature_, end.color_temperature_, completion); - v.cold_white_ = std::lerp(start.cold_white_, end.cold_white_, completion); - v.warm_white_ = std::lerp(start.warm_white_, end.warm_white_, completion); + v.state_ = fast_lerp(start.state_, end.state_, completion); + v.brightness_ = fast_lerp(start.brightness_, end.brightness_, completion); + v.color_brightness_ = fast_lerp(start.color_brightness_, end.color_brightness_, completion); + v.red_ = fast_lerp(start.red_, end.red_, completion); + v.green_ = fast_lerp(start.green_, end.green_, completion); + v.blue_ = fast_lerp(start.blue_, end.blue_, completion); + v.white_ = fast_lerp(start.white_, end.white_, completion); + v.color_temperature_ = fast_lerp(start.color_temperature_, end.color_temperature_, completion); + v.cold_white_ = fast_lerp(start.cold_white_, end.cold_white_, completion); + v.warm_white_ = fast_lerp(start.warm_white_, end.warm_white_, completion); return v; }