[sensor] Use std::array in ValueList/FilterOut/ThrottleWithPriority filters

Template ValueListFilter, FilterOutValueFilter, and ThrottleWithPriorityFilter
on N (the number of configured values) so the value list is stored in
std::array<TemplatableValue<float>, N> instead of FixedVector.

Non-template helpers value_list_matches_any() and throttle_check_and_update()
keep the iteration loops in the .cpp to avoid template bloat.
This commit is contained in:
J. Nick Koston
2026-03-28 08:37:04 -10:00
parent b6abfec82e
commit c722ef0369
4 changed files with 74 additions and 47 deletions
+4 -2
View File
@@ -381,7 +381,7 @@ async def filter_out_filter_to_code(config, filter_id):
if not isinstance(config, list):
config = [config]
template_ = [await cg.templatable(x, [], float) for x in config]
return cg.new_Pvariable(filter_id, template_)
return cg.new_Pvariable(filter_id, cg.TemplateArguments(len(template_)), template_)
QUANTILE_SCHEMA = cv.All(
@@ -650,7 +650,9 @@ async def throttle_with_priority_filter_to_code(config, filter_id):
if not isinstance(config[CONF_VALUE], list):
config[CONF_VALUE] = [config[CONF_VALUE]]
template_ = [await cg.templatable(x, [], float) for x in config[CONF_VALUE]]
return cg.new_Pvariable(filter_id, config[CONF_TIMEOUT], template_)
return cg.new_Pvariable(
filter_id, cg.TemplateArguments(len(template_)), config[CONF_TIMEOUT], template_
)
HEARTBEAT_SCHEMA = cv.Schema(
+12 -31
View File
@@ -222,16 +222,14 @@ MultiplyFilter::MultiplyFilter(TemplatableValue<float> multiplier) : multiplier_
optional<float> MultiplyFilter::new_value(float value) { return value * this->multiplier_.value(); }
// ValueListFilter (base class)
ValueListFilter::ValueListFilter(std::initializer_list<TemplatableValue<float>> values) : values_(values) {}
bool ValueListFilter::value_matches_any_(float sensor_value) {
int8_t accuracy = this->parent_->get_accuracy_decimals();
// ValueListFilter helper (non-template, shared by all ValueListFilter<N> instantiations)
bool value_list_matches_any(Sensor *parent, float sensor_value, const TemplatableValue<float> *values, size_t count) {
int8_t accuracy = parent->get_accuracy_decimals();
float accuracy_mult = pow10_int(accuracy);
float rounded_sensor = roundf(accuracy_mult * sensor_value);
for (auto &filter_value : this->values_) {
float fv = filter_value.value();
for (size_t i = 0; i < count; i++) {
float fv = values[i].value();
// Handle NaN comparison
if (std::isnan(fv)) {
@@ -248,14 +246,13 @@ bool ValueListFilter::value_matches_any_(float sensor_value) {
return false;
}
// FilterOutValueFilter
FilterOutValueFilter::FilterOutValueFilter(std::initializer_list<TemplatableValue<float>> values_to_filter_out)
: ValueListFilter(values_to_filter_out) {}
optional<float> FilterOutValueFilter::new_value(float value) {
if (this->value_matches_any_(value))
return {}; // Filter out
return value; // Pass through
bool throttle_check_and_update(uint32_t &last_input, uint32_t min_time_between_inputs) {
const uint32_t now = App.get_loop_component_start_time();
if (last_input == 0 || now - last_input >= min_time_between_inputs) {
last_input = now;
return true;
}
return false;
}
// ThrottleFilter
@@ -269,22 +266,6 @@ optional<float> ThrottleFilter::new_value(float value) {
return {};
}
// ThrottleWithPriorityFilter
ThrottleWithPriorityFilter::ThrottleWithPriorityFilter(
uint32_t min_time_between_inputs, std::initializer_list<TemplatableValue<float>> prioritized_values)
: ValueListFilter(prioritized_values), min_time_between_inputs_(min_time_between_inputs) {}
optional<float> ThrottleWithPriorityFilter::new_value(float value) {
const uint32_t now = App.get_loop_component_start_time();
// Allow value through if: no previous input, time expired, or is prioritized
if (this->last_input_ == 0 || now - this->last_input_ >= min_time_between_inputs_ ||
this->value_matches_any_(value)) {
this->last_input_ = now;
return value;
}
return {};
}
// DeltaFilter
DeltaFilter::DeltaFilter(float min_a0, float min_a1, float max_a0, float max_a1)
: min_a0_(min_a0), min_a1_(min_a1), max_a0_(max_a0), max_a1_(max_a1) {}
+40 -14
View File
@@ -3,6 +3,7 @@
#include "esphome/core/defines.h"
#ifdef USE_SENSOR_FILTER
#include <array>
#include <utility>
#include <vector>
#include "esphome/core/automation.h"
@@ -328,28 +329,42 @@ class MultiplyFilter : public Filter {
TemplatableValue<float> multiplier_;
};
/** Base class for filters that compare sensor values against a list of configured values.
/// Non-template helper for value matching (implementation in filter.cpp)
bool value_list_matches_any(Sensor *parent, float sensor_value, const TemplatableValue<float> *values, size_t count);
/** Base class for filters that compare sensor values against a fixed list of configured values.
*
* This base class provides common functionality for filters that need to check if a sensor
* value matches any value in a configured list, with proper handling of NaN values and
* accuracy-based rounding for comparisons.
* Templated on N (the number of values) so the list is stored inline in a std::array,
* avoiding heap allocation and the overhead of FixedVector.
*
* @tparam N Number of values in the filter list, set by code generation to match
* the exact number of values configured in YAML.
*/
class ValueListFilter : public Filter {
template<size_t N> class ValueListFilter : public Filter {
protected:
explicit ValueListFilter(std::initializer_list<TemplatableValue<float>> values);
explicit ValueListFilter(std::initializer_list<TemplatableValue<float>> values) {
init_array_from(this->values_, values);
}
/// Check if sensor value matches any configured value (with accuracy rounding)
bool value_matches_any_(float sensor_value);
bool value_matches_any_(float sensor_value) {
return value_list_matches_any(this->parent_, sensor_value, this->values_.data(), N);
}
FixedVector<TemplatableValue<float>> values_;
std::array<TemplatableValue<float>, N> values_{};
};
/// A simple filter that only forwards the filter chain if it doesn't receive `value_to_filter_out`.
class FilterOutValueFilter : public ValueListFilter {
template<size_t N> class FilterOutValueFilter : public ValueListFilter<N> {
public:
explicit FilterOutValueFilter(std::initializer_list<TemplatableValue<float>> values_to_filter_out);
explicit FilterOutValueFilter(std::initializer_list<TemplatableValue<float>> values_to_filter_out)
: ValueListFilter<N>(values_to_filter_out) {}
optional<float> new_value(float value) override;
optional<float> new_value(float value) override {
if (this->value_matches_any_(value))
return {}; // Filter out
return value; // Pass through
}
};
class ThrottleFilter : public Filter {
@@ -363,13 +378,24 @@ class ThrottleFilter : public Filter {
uint32_t min_time_between_inputs_;
};
/// Returns true if throttle should allow the value through (time expired or first input).
/// Updates last_input in-place. Implementation in filter.cpp (accesses App without circular include).
bool throttle_check_and_update(uint32_t &last_input, uint32_t min_time_between_inputs);
/// Same as 'throttle' but will immediately publish values contained in `value_to_prioritize`.
class ThrottleWithPriorityFilter : public ValueListFilter {
template<size_t N> class ThrottleWithPriorityFilter : public ValueListFilter<N> {
public:
explicit ThrottleWithPriorityFilter(uint32_t min_time_between_inputs,
std::initializer_list<TemplatableValue<float>> prioritized_values);
std::initializer_list<TemplatableValue<float>> prioritized_values)
: ValueListFilter<N>(prioritized_values), min_time_between_inputs_(min_time_between_inputs) {}
optional<float> new_value(float value) override;
optional<float> new_value(float value) override {
if (throttle_check_and_update(this->last_input_, this->min_time_between_inputs_) ||
this->value_matches_any_(value)) {
return value;
}
return {};
}
protected:
uint32_t last_input_{0};
+18
View File
@@ -2,6 +2,7 @@
#include <algorithm>
#include <array>
#include <cassert>
#include <cmath>
#include <cstdarg>
#include <cstdint>
@@ -497,6 +498,23 @@ template<typename T, size_t MAX_CAPACITY = std::numeric_limits<uint16_t>::max()>
index_type capacity_{0};
};
/// Initialize a std::array from an initializer_list. Uses memcpy for trivially copyable types (optimal codegen),
/// falls back to element-wise copy for non-trivially copyable types (e.g. TemplatableValue).
/// N is set by code generation; assert catches mismatches in debug/integration tests.
template<typename T, size_t N> inline void init_array_from(std::array<T, N> &dest, std::initializer_list<T> src) {
#ifdef ESPHOME_DEBUG
assert(src.size() == N);
#endif
if constexpr (std::is_trivially_copyable_v<T>) {
__builtin_memcpy(dest.data(), src.begin(), N * sizeof(T));
} else {
size_t i = 0;
for (const auto &v : src) {
dest[i++] = v;
}
}
}
/// Fixed-capacity vector - allocates once at runtime, never reallocates
/// This avoids std::vector template overhead (_M_realloc_insert, _M_default_append)
/// when size is known at initialization but not at compile time