From bf917144c4719b96ce09cda0ac2becd800451f01 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Apr 2026 08:11:31 -0500 Subject: [PATCH] [sensor] Pack ThrottleAverageFilter have_nan_ into n_ bitfield MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Saves 4 B per ThrottleAverageFilter instance (28 B → 24 B on 32-bit). have_nan_ is a single boolean that previously cost 4 B due to padding; fold it into the high bit of n_ as a 31-bit + 1-bit bitfield. To guarantee n_ cannot overflow under realistic configurations, cap the YAML time_period at 24 h. At a pessimistic 1 kHz source rate the counter peaks at 86.4M, leaving 25x headroom against 2^31. Anyone needing multi-day rolling averages should be using a different filtering strategy anyway. Breaking change: configurations with throttle_average periods longer than 24 h will now fail validation. No real-world configs are expected to use such values. --- esphome/components/sensor/__init__.py | 7 ++++++- esphome/components/sensor/filter.h | 7 +++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 48b7d25d4d..82fa3ebc0d 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -564,7 +564,12 @@ async def exponential_moving_average_filter_to_code(config, filter_id): @FILTER_REGISTRY.register( - "throttle_average", ThrottleAverageFilter, cv.positive_time_period_milliseconds + "throttle_average", + ThrottleAverageFilter, + cv.All( + cv.positive_time_period_milliseconds, + cv.Range(max=cv.TimePeriod(hours=24)), + ), ) async def throttle_average_filter_to_code(config, filter_id): var = cg.new_Pvariable(filter_id, config) diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index 917a1ce7d5..ed5a31c361 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -266,9 +266,12 @@ class ThrottleAverageFilter : public Filter, public Component { protected: float sum_{0.0f}; - unsigned int n_{0}; uint32_t time_period_; - bool have_nan_{false}; + // Sample count packed with NaN-seen flag in a single 32-bit word. + // n_ is bounded by YAML cap on time_period_ (24 h) × max plausible source + // rate (1 kHz) = 86.4M ≪ 2^31, so 31 bits has 25x headroom. + uint32_t n_ : 31 {0}; + uint32_t have_nan_ : 1 {0}; }; using lambda_filter_t = std::function(float)>;