mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 02:02:51 +00:00
[sensor] Share filter_out and throttle_with_priority value tables in flash (#20078)
This commit is contained in:
@@ -113,7 +113,7 @@ from esphome.const import (
|
||||
DEVICE_CLASS_WIND_SPEED,
|
||||
ENTITY_CATEGORY_CONFIG,
|
||||
)
|
||||
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
|
||||
from esphome.core import CORE, ID, CoroPriority, Lambda, coroutine_with_priority
|
||||
from esphome.core.config import UNIT_OF_MEASUREMENT_MAX_LENGTH
|
||||
from esphome.core.entity_helpers import (
|
||||
SubEntities,
|
||||
@@ -421,16 +421,30 @@ async def multiply_filter_to_code(config, filter_id):
|
||||
return cg.new_Pvariable(filter_id, template_)
|
||||
|
||||
|
||||
TemplatableFloat = cg.esphome_ns.class_("TemplatableFn").template(cg.float_)
|
||||
|
||||
|
||||
async def _value_list_table(values: list[Any]) -> MockObj:
|
||||
"""PROGMEM value table, ended by an empty entry so filters store no count."""
|
||||
fns = [await cg.templatable(x, [], cg.float_) for x in values]
|
||||
# Identical lambdas (e.g. a YAML anchor) may keep static state, so only constants share.
|
||||
return cg.shared_progmem_array(
|
||||
"sensor_value_list",
|
||||
TemplatableFloat,
|
||||
[*fns, TemplatableFloat()],
|
||||
share=not any(isinstance(v, Lambda) for v in values),
|
||||
)
|
||||
|
||||
|
||||
@FILTER_REGISTRY.register(
|
||||
"filter_out",
|
||||
FilterOutValueFilter,
|
||||
cv.Any(cv.templatable(cv.float_), [cv.templatable(cv.float_)]),
|
||||
)
|
||||
async def filter_out_filter_to_code(config, filter_id):
|
||||
async def filter_out_filter_to_code(config: Any, filter_id: ID) -> MockObj:
|
||||
if not isinstance(config, list):
|
||||
config = [config]
|
||||
template_ = [await cg.templatable(x, [], cg.float_) for x in config]
|
||||
return cg.new_Pvariable(filter_id, cg.TemplateArguments(len(template_)), template_)
|
||||
return cg.new_Pvariable(filter_id, await _value_list_table(config))
|
||||
|
||||
|
||||
QUANTILE_SCHEMA = cv.All(
|
||||
@@ -704,7 +718,9 @@ def _is_nan_only(values: Any) -> bool:
|
||||
ThrottleWithPriorityFilter,
|
||||
THROTTLE_WITH_PRIORITY_SCHEMA,
|
||||
)
|
||||
async def throttle_with_priority_filter_to_code(config, filter_id):
|
||||
async def throttle_with_priority_filter_to_code(
|
||||
config: ConfigType, filter_id: ID
|
||||
) -> MockObj:
|
||||
values = config[CONF_VALUE]
|
||||
if not isinstance(values, list):
|
||||
values = [values]
|
||||
@@ -716,10 +732,8 @@ async def throttle_with_priority_filter_to_code(config, filter_id):
|
||||
filter_id = filter_id.copy()
|
||||
filter_id.type = ThrottleWithPriorityNanFilter
|
||||
return cg.new_Pvariable(filter_id, config[CONF_TIMEOUT])
|
||||
template_ = [await cg.templatable(x, [], cg.float_) for x in values]
|
||||
return cg.new_Pvariable(
|
||||
filter_id, cg.TemplateArguments(len(template_)), config[CONF_TIMEOUT], template_
|
||||
)
|
||||
table = await _value_list_table(values)
|
||||
return cg.new_Pvariable(filter_id, config[CONF_TIMEOUT], table)
|
||||
|
||||
|
||||
HEARTBEAT_SCHEMA = cv.Schema(
|
||||
|
||||
@@ -226,14 +226,14 @@ MultiplyFilter::MultiplyFilter(TemplatableFn<float> multiplier) : multiplier_(mu
|
||||
|
||||
optional<float> MultiplyFilter::new_value(float value) { return value * this->multiplier_.value(); }
|
||||
|
||||
// ValueListFilter helper (non-template, shared by all ValueListFilter<N> instantiations)
|
||||
bool value_list_matches_any(Sensor *parent, float sensor_value, const TemplatableFn<float> *values, size_t count) {
|
||||
int8_t accuracy = parent->get_accuracy_decimals();
|
||||
// ValueListFilter
|
||||
bool ValueListFilter::value_matches_any_(float sensor_value) {
|
||||
int8_t accuracy = this->parent_->get_accuracy_decimals();
|
||||
float accuracy_mult = pow10_int(accuracy);
|
||||
float rounded_sensor = roundf(accuracy_mult * sensor_value);
|
||||
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
float fv = values[i].value();
|
||||
for (const TemplatableFn<float> *v = this->values_; v->has_value(); v++) {
|
||||
float fv = v->value();
|
||||
|
||||
// Handle NaN comparison
|
||||
if (std::isnan(fv)) {
|
||||
@@ -261,13 +261,12 @@ optional<float> ThrottleFilter::new_value(float value) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// ThrottleWithPriorityFilter helper (non-template, keeps App access in .cpp)
|
||||
optional<float> throttle_with_priority_new_value(Sensor *parent, float value, const TemplatableFn<float> *values,
|
||||
size_t count, uint32_t &last_input, uint32_t min_time_between_inputs) {
|
||||
// ThrottleWithPriorityFilter
|
||||
optional<float> ThrottleWithPriorityFilter::new_value(float value) {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
if (last_input == 0 || now - last_input >= min_time_between_inputs ||
|
||||
value_list_matches_any(parent, value, values, count)) {
|
||||
last_input = now;
|
||||
if (this->last_input_ == 0 || now - this->last_input_ >= this->min_time_between_inputs_ ||
|
||||
this->value_matches_any_(value)) {
|
||||
this->last_input_ = now;
|
||||
return value;
|
||||
}
|
||||
return {};
|
||||
|
||||
@@ -330,36 +330,27 @@ class MultiplyFilter : public Filter {
|
||||
TemplatableFn<float> multiplier_;
|
||||
};
|
||||
|
||||
/// Non-template helper for value matching (implementation in filter.cpp)
|
||||
bool value_list_matches_any(Sensor *parent, float sensor_value, const TemplatableFn<float> *values, size_t count);
|
||||
// Value tables are read in place from flash, which ESP8266 only allows for whole words.
|
||||
static_assert(sizeof(TemplatableFn<float>) == sizeof(void *), "TemplatableFn<float> must stay a single pointer");
|
||||
|
||||
/** Base class for filters that compare sensor values against a fixed list of configured values.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
template<size_t N> class ValueListFilter : public Filter {
|
||||
/// Base class for filters that compare sensor values against a fixed list of configured values.
|
||||
class ValueListFilter : public Filter {
|
||||
protected:
|
||||
explicit ValueListFilter(std::initializer_list<TemplatableFn<float>> values) {
|
||||
init_array_from(this->values_, values);
|
||||
}
|
||||
explicit ValueListFilter(const TemplatableFn<float> *values) : values_(values) {}
|
||||
|
||||
/// Check if sensor value matches any configured value (with accuracy rounding)
|
||||
bool value_matches_any_(float sensor_value) {
|
||||
return value_list_matches_any(this->parent_, sensor_value, this->values_.data(), N);
|
||||
}
|
||||
bool value_matches_any_(float sensor_value);
|
||||
|
||||
std::array<TemplatableFn<float>, N> values_{};
|
||||
// Shared PROGMEM table ended by an empty entry, so no count is stored; entries are function
|
||||
// pointers, so aligned 32-bit loads from flash are safe on ESP8266.
|
||||
const TemplatableFn<float> *values_;
|
||||
};
|
||||
|
||||
/// A simple filter that only forwards the filter chain if it doesn't receive `value_to_filter_out`.
|
||||
template<size_t N> class FilterOutValueFilter : public ValueListFilter<N> {
|
||||
class FilterOutValueFilter : public ValueListFilter {
|
||||
public:
|
||||
explicit FilterOutValueFilter(std::initializer_list<TemplatableFn<float>> values_to_filter_out)
|
||||
: ValueListFilter<N>(values_to_filter_out) {}
|
||||
explicit FilterOutValueFilter(const TemplatableFn<float> *values_to_filter_out)
|
||||
: ValueListFilter(values_to_filter_out) {}
|
||||
|
||||
optional<float> new_value(float value) override {
|
||||
if (this->value_matches_any_(value))
|
||||
@@ -379,21 +370,13 @@ class ThrottleFilter : public Filter {
|
||||
uint32_t min_time_between_inputs_;
|
||||
};
|
||||
|
||||
/// Non-template helper for ThrottleWithPriorityFilter (implementation in filter.cpp)
|
||||
optional<float> throttle_with_priority_new_value(Sensor *parent, float value, const TemplatableFn<float> *values,
|
||||
size_t count, uint32_t &last_input, uint32_t min_time_between_inputs);
|
||||
|
||||
/// Same as 'throttle' but will immediately publish values contained in `value_to_prioritize`.
|
||||
template<size_t N> class ThrottleWithPriorityFilter : public ValueListFilter<N> {
|
||||
class ThrottleWithPriorityFilter : public ValueListFilter {
|
||||
public:
|
||||
explicit ThrottleWithPriorityFilter(uint32_t min_time_between_inputs,
|
||||
std::initializer_list<TemplatableFn<float>> prioritized_values)
|
||||
: ValueListFilter<N>(prioritized_values), min_time_between_inputs_(min_time_between_inputs) {}
|
||||
ThrottleWithPriorityFilter(uint32_t min_time_between_inputs, const TemplatableFn<float> *prioritized_values)
|
||||
: ValueListFilter(prioritized_values), min_time_between_inputs_(min_time_between_inputs) {}
|
||||
|
||||
optional<float> new_value(float value) override {
|
||||
return throttle_with_priority_new_value(this->parent_, value, this->values_.data(), N, this->last_input_,
|
||||
this->min_time_between_inputs_);
|
||||
}
|
||||
optional<float> new_value(float value) override;
|
||||
|
||||
protected:
|
||||
uint32_t last_input_{0};
|
||||
|
||||
@@ -19,11 +19,12 @@ namespace esphome {
|
||||
/// Codegen wraps constants in stateless lambdas so only a function pointer is needed.
|
||||
template<typename T, typename... X> class TemplatableFn {
|
||||
public:
|
||||
TemplatableFn() = default;
|
||||
constexpr TemplatableFn() = default;
|
||||
TemplatableFn(std::nullptr_t) = delete;
|
||||
|
||||
// Exact return type match — direct function pointer storage
|
||||
template<typename F> TemplatableFn(F f) requires std::convertible_to<F, T (*)(X...)> : f_(f) {}
|
||||
// constexpr so codegen can place arrays of these in flash.
|
||||
template<typename F> constexpr TemplatableFn(F f) requires std::convertible_to<F, T (*)(X...)> : f_(f) {}
|
||||
|
||||
// Convertible return type (e.g., int -> uint8_t) — casting trampoline.
|
||||
// Stateless lambdas are default-constructible in C++20, so F{} recreates the lambda inside
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
"""Tests for the sensor value list filter codegen."""
|
||||
|
||||
from collections.abc import Callable
|
||||
import re
|
||||
|
||||
|
||||
def test_value_list_filters_share_progmem_tables(
|
||||
generate_main: Callable[[str], str],
|
||||
) -> None:
|
||||
"""Identical value lists share one PROGMEM table; the NaN throttle stays array free."""
|
||||
main_cpp = generate_main("tests/component_tests/sensor/test_value_list.yaml")
|
||||
|
||||
tables = re.findall(
|
||||
r"static constexpr TemplatableFn<float> (\w+)\[\] PROGMEM", main_cpp
|
||||
)
|
||||
# Every table ends with an empty entry so the filters store no count.
|
||||
assert main_cpp.count("TemplatableFn<float>()}") == len(tables)
|
||||
assert (
|
||||
len(tables) == 5
|
||||
) # single, shared list, lambda, two anchored stateful lambdas
|
||||
single, shared, lam = tables[:3]
|
||||
assert f"sensor::FilterOutValueFilter({single});" in main_cpp
|
||||
assert main_cpp.count(f"sensor::FilterOutValueFilter({shared});") == 2
|
||||
assert f"sensor::FilterOutValueFilter({lam});" in main_cpp
|
||||
assert f"sensor::ThrottleWithPriorityFilter(1000, {shared});" in main_cpp
|
||||
assert "sensor::ThrottleWithPriorityNanFilter(1000);" in main_cpp
|
||||
assert "return 42.0;" in main_cpp
|
||||
|
||||
|
||||
def test_value_list_lambda_tables_are_not_shared(
|
||||
generate_main: Callable[[str], str],
|
||||
) -> None:
|
||||
"""An anchored lambda may keep static state, so each filter keeps its own table."""
|
||||
main_cpp = generate_main("tests/component_tests/sensor/test_value_list.yaml")
|
||||
|
||||
tables = re.findall(
|
||||
r"static constexpr TemplatableFn<float> (\w+)\[\] PROGMEM = (.*?)TemplatableFn<float>\(\)\}",
|
||||
main_cpp,
|
||||
re.DOTALL,
|
||||
)
|
||||
stateful = [name for name, body in tables if "static float n" in body]
|
||||
assert len(stateful) == 2
|
||||
assert stateful[0] != stateful[1]
|
||||
@@ -0,0 +1,51 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
|
||||
sensor:
|
||||
- platform: template
|
||||
id: fo_single
|
||||
lambda: return 1.0;
|
||||
filters:
|
||||
- filter_out: 85.0
|
||||
- platform: template
|
||||
id: fo_list_a
|
||||
lambda: return 1.0;
|
||||
filters:
|
||||
- filter_out: [0.0, 85.0]
|
||||
- platform: template
|
||||
id: fo_list_b
|
||||
lambda: return 1.0;
|
||||
filters:
|
||||
- filter_out: [0.0, 85.0]
|
||||
- platform: template
|
||||
id: fo_lambda
|
||||
lambda: return 1.0;
|
||||
filters:
|
||||
- filter_out: !lambda return 42.0;
|
||||
- platform: template
|
||||
id: twp
|
||||
lambda: return 1.0;
|
||||
filters:
|
||||
- throttle_with_priority:
|
||||
timeout: 1s
|
||||
value: [0.0, 85.0]
|
||||
- platform: template
|
||||
id: twp_nan
|
||||
lambda: return 1.0;
|
||||
filters:
|
||||
- throttle_with_priority: 1s
|
||||
- platform: template
|
||||
id: fo_stateful_a
|
||||
lambda: return 1.0;
|
||||
filters:
|
||||
- filter_out: &stateful !lambda |-
|
||||
static float n = 0;
|
||||
return ++n;
|
||||
- platform: template
|
||||
id: fo_stateful_b
|
||||
lambda: return 1.0;
|
||||
filters:
|
||||
- filter_out: *stateful
|
||||
Reference in New Issue
Block a user