diff --git a/esphome/components/sensor/__init__.py b/esphome/components/sensor/__init__.py index 48b7d25d4df..822fe33924d 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, cg.Component) +TimeoutFilterBase = sensor_ns.class_("TimeoutFilterBase", Filter) TimeoutFilterLast = sensor_ns.class_("TimeoutFilterLast", TimeoutFilterBase) TimeoutFilterConfigured = sensor_ns.class_("TimeoutFilterConfigured", TimeoutFilterBase) DebounceFilter = sensor_ns.class_("DebounceFilter", Filter, cg.Component) @@ -730,7 +730,6 @@ 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 4896757d3f3..44556f6ed73 100644 --- a/esphome/components/sensor/filter.cpp +++ b/esphome/components/sensor/filter.cpp @@ -322,41 +322,19 @@ optional or_filter_new_value(Filter **filters, size_t count, float value, return {}; } -// 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 +// 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). optional TimeoutFilterLast::new_value(float value) { - // Store the value to output when timeout fires this->pending_value_ = value; - - // Record when timeout started and enable loop - this->timeout_start_time_ = millis(); - this->enable_loop(); - + App.scheduler.set_timeout(this, this->time_period_, [this]() { this->output(this->pending_value_); }); return value; } -// TimeoutFilterConfigured - configured value mode implementation +// TimeoutFilterConfigured - configured-value mode: re-arm on every input; output the configured +// value (static or lambda) if no further input arrives within time_period_. optional TimeoutFilterConfigured::new_value(float 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(); - + App.scheduler.set_timeout(this, this->time_period_, [this]() { this->output(this->value_.value()); }); return value; } diff --git a/esphome/components/sensor/filter.h b/esphome/components/sensor/filter.h index 917a1ce7d5a..612c6aab3b0 100644 --- a/esphome/components/sensor/filter.h +++ b/esphome/components/sensor/filter.h @@ -412,22 +412,15 @@ class ThrottleWithPriorityNanFilter : public Filter { uint32_t min_time_between_inputs_; }; -// 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; - +// 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 { protected: - 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 + explicit TimeoutFilterBase(uint32_t time_period) : time_period_(time_period) {} + uint32_t time_period_; }; -// Timeout filter for "last" mode - outputs the last received value after timeout +// "last" mode — outputs the most recent input after time_period_ ms of silence. class TimeoutFilterLast : public TimeoutFilterBase { public: explicit TimeoutFilterLast(uint32_t time_period) : TimeoutFilterBase(time_period) {} @@ -435,12 +428,10 @@ class TimeoutFilterLast : public TimeoutFilterBase { optional new_value(float value) override; protected: - 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 + float pending_value_{0}; }; -// Timeout filter with configured value - evaluates TemplatableValue after timeout +// Configured-value mode — outputs a static or lambda value after time_period_ ms of silence. class TimeoutFilterConfigured : public TimeoutFilterBase { public: explicit TimeoutFilterConfigured(uint32_t time_period, const TemplatableFn &new_value) @@ -449,9 +440,7 @@ class TimeoutFilterConfigured : public TimeoutFilterBase { optional new_value(float value) override; protected: - 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 + TemplatableFn value_; }; class DebounceFilter : public Filter, public Component {