From e9bd84cb43f111d3fe4b68016d1d453667fd2923 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 30 Apr 2026 11:15:10 -0500 Subject: [PATCH] Revert "[sensor] Drop Component from timeout filters, use self-keyed scheduler" This reverts commit 6ccc2b23b5fcd20d477c2ec51522be8dc6dca673. --- esphome/components/sensor/__init__.py | 3 ++- esphome/components/sensor/filter.cpp | 36 +++++++++++++++++++++------ esphome/components/sensor/filter.h | 29 ++++++++++++++------- 3 files changed, 51 insertions(+), 17 deletions(-) diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index e4e7b97d43..ed02cc2543 100644 --- a/esphome/components/sensor/__init__.py +++ b/esphome/components/sensor/__init__.py @@ -280,7 +280,7 @@ ThrottleWithPriorityFilter = sensor_ns.class_( ThrottleWithPriorityNanFilter = sensor_ns.class_( "ThrottleWithPriorityNanFilter", Filter ) -TimeoutFilterBase = sensor_ns.class_("TimeoutFilterBase", Filter) +TimeoutFilterBase = sensor_ns.class_("TimeoutFilterBase", Filter, cg.Component) TimeoutFilterLast = sensor_ns.class_("TimeoutFilterLast", TimeoutFilterBase) TimeoutFilterConfigured = sensor_ns.class_("TimeoutFilterConfigured", TimeoutFilterBase) DebounceFilter = sensor_ns.class_("DebounceFilter", Filter) @@ -730,6 +730,7 @@ async def timeout_filter_to_code(config, filter_id): filter_id.type = TimeoutFilterConfigured template_ = await cg.templatable(config[CONF_VALUE], [], cg.float_) var = cg.new_Pvariable(filter_id, config[CONF_TIMEOUT], template_) + await cg.register_component(var, {}) return var diff --git a/esphome/components/sensor/filter.cpp b/esphome/components/sensor/filter.cpp index 485868b8a3..5f7f19769a 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -317,19 +317,41 @@ optional or_filter_new_value(Filter **filters, size_t count, float value, return {}; } -// TimeoutFilterLast - "last" mode: re-arm on every input; output the latest value if no further -// input arrives within time_period_. Self-keyed scheduler.set_timeout(this, ...) cancels and -// replaces any pending arm in O(1). +// TimeoutFilterBase - shared loop logic +void TimeoutFilterBase::loop() { + // Check if timeout period has elapsed + // Use cached loop start time to avoid repeated millis() calls + const uint32_t now = App.get_loop_component_start_time(); + if (now - this->timeout_start_time_ >= this->time_period_) { + // Timeout fired - get output value from derived class and output it + this->output(this->get_output_value()); + + // Disable loop until next value arrives + this->disable_loop(); + } +} + +float TimeoutFilterBase::get_setup_priority() const { return setup_priority::HARDWARE; } + +// TimeoutFilterLast - "last" mode implementation optional TimeoutFilterLast::new_value(float value) { + // Store the value to output when timeout fires this->pending_value_ = value; - App.scheduler.set_timeout(this, this->time_period_, [this]() { this->output(this->pending_value_); }); + + // Record when timeout started and enable loop + this->timeout_start_time_ = millis(); + this->enable_loop(); + return value; } -// TimeoutFilterConfigured - configured-value mode: re-arm on every input; output the configured -// value (static or lambda) if no further input arrives within time_period_. +// TimeoutFilterConfigured - configured value mode implementation optional TimeoutFilterConfigured::new_value(float value) { - App.scheduler.set_timeout(this, this->time_period_, [this]() { this->output(this->value_.value()); }); + // Record when timeout started and enable loop + // Note: we don't store the incoming value since we have a configured value + this->timeout_start_time_ = millis(); + this->enable_loop(); + return value; } diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index 43fe4e3be7..57a2386a7f 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -413,15 +413,22 @@ class ThrottleWithPriorityNanFilter : public Filter { uint32_t min_time_between_inputs_; }; -// Base class for timeout filters. Self-keyed scheduler timeout (`this` as key) re-arms on each -// new_value(). Filter instances live for the program's lifetime, so the scheduler key never dangles. -class TimeoutFilterBase : public Filter { +// Base class for timeout filters - contains common loop logic +class TimeoutFilterBase : public Filter, public Component { + public: + void loop() override; + float get_setup_priority() const override; + protected: - explicit TimeoutFilterBase(uint32_t time_period) : time_period_(time_period) {} - uint32_t time_period_; + explicit TimeoutFilterBase(uint32_t time_period) : time_period_(time_period) { this->disable_loop(); } + virtual float get_output_value() = 0; + + uint32_t time_period_; // 4 bytes (timeout duration in ms) + uint32_t timeout_start_time_{0}; // 4 bytes (when the timeout was started) + // Total base: 8 bytes }; -// "last" mode — outputs the most recent input after time_period_ ms of silence. +// Timeout filter for "last" mode - outputs the last received value after timeout class TimeoutFilterLast : public TimeoutFilterBase { public: explicit TimeoutFilterLast(uint32_t time_period) : TimeoutFilterBase(time_period) {} @@ -429,10 +436,12 @@ class TimeoutFilterLast : public TimeoutFilterBase { optional new_value(float value) override; protected: - float pending_value_{0}; + float get_output_value() override { return this->pending_value_; } + float pending_value_{0}; // 4 bytes (value to output when timeout fires) + // Total: 8 (base) + 4 = 12 bytes + vtable ptr + Component overhead }; -// Configured-value mode — outputs a static or lambda value after time_period_ ms of silence. +// Timeout filter with configured value - evaluates TemplatableValue after timeout class TimeoutFilterConfigured : public TimeoutFilterBase { public: explicit TimeoutFilterConfigured(uint32_t time_period, const TemplatableFn &new_value) @@ -441,7 +450,9 @@ class TimeoutFilterConfigured : public TimeoutFilterBase { optional new_value(float value) override; protected: - TemplatableFn value_; + float get_output_value() override { return this->value_.value(); } + TemplatableFn value_; // 4 bytes (configured output value, can be lambda) + // Total: 8 (base) + 4 = 12 bytes + vtable ptr + Component overhead }; class DebounceFilter : public Filter {