[sensor] Drop Component from timeout filters, use self-keyed scheduler

Migrates TimeoutFilterBase / TimeoutFilterLast / TimeoutFilterConfigured off
Component, mirroring the migration #16132 did for the other Component-based
sensor filters. They now arm/re-arm via App.scheduler.set_timeout(this, ...)
keyed on the filter pointer; the scheduler cancels and replaces any pending
arm in O(1) on each new_value(). Filters live for the program's lifetime, so
the self-key never dangles.

Net effect: this reverts the design from #11922, which moved timeout filters
off the scheduler specifically because the bounded SchedulerItem pool churned
on devices with many timers (LD2450 etc.). With #16172 replacing that pool
with an unbounded intrusive freelist, the original churn problem is gone and
the scheduler is once again the right primitive for this workload.

Per-instance footprint shrinks: lose Component (second vptr, packed bookkeeping
bytes, ComponentRuntimeStats block when runtime_stats: is enabled) and drop
the now-unused timeout_start_time_ field. get_setup_priority() and the loop()
poll go with it.

Sensor timeout-filter syntax is unchanged. No user-facing change.
This commit is contained in:
J. Nick Koston
2026-04-30 10:49:18 -05:00
parent 2758aa5517
commit 6ccc2b23b5
3 changed files with 17 additions and 51 deletions
+1 -2
View File
@@ -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
+7 -29
View File
@@ -322,41 +322,19 @@ optional<float> 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<float> 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<float> 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;
}
+9 -20
View File
@@ -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<float> 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<float> &new_value)
@@ -449,9 +440,7 @@ class TimeoutFilterConfigured : public TimeoutFilterBase {
optional<float> new_value(float value) override;
protected:
float get_output_value() override { return this->value_.value(); }
TemplatableFn<float> value_; // 4 bytes (configured output value, can be lambda)
// Total: 8 (base) + 4 = 12 bytes + vtable ptr + Component overhead
TemplatableFn<float> value_;
};
class DebounceFilter : public Filter, public Component {