[core] Emit filter, action and condition lists one item at a time (#20116)

This commit is contained in:
J. Nick Koston
2026-10-11 15:12:59 -10:00
committed by GitHub
parent 5d64b65d72
commit ed6ab4565d
11 changed files with 151 additions and 26 deletions
+15 -9
View File
@@ -724,7 +724,7 @@ async def condition_group_to_code(
if len(conditions) == 1:
return conditions[0]
return cg.new_Pvariable(
condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions
condition_id, cg.TemplateArguments(len(conditions), *template_arg), *conditions
)
@@ -840,11 +840,13 @@ async def if_action_to_code(
condition = await build_condition(config[cond_conf], template_arg, args)
var = cg.new_Pvariable(action_id, if_template_arg, condition)
if CONF_THEN in config:
actions = await build_action_list(config[CONF_THEN], template_arg, args)
cg.add(var.add_then(actions))
for action in await build_action_list(config[CONF_THEN], template_arg, args):
cg.add(var.add_then_action(action))
cg.add(var.finish_then())
if has_else:
actions = await build_action_list(config[CONF_ELSE], template_arg, args)
cg.add(var.add_else(actions))
for action in await build_action_list(config[CONF_ELSE], template_arg, args):
cg.add(var.add_else_action(action))
cg.add(var.finish_else())
return var
@@ -867,8 +869,9 @@ async def while_action_to_code(
) -> MockObj:
condition = await build_condition(config[CONF_CONDITION], template_arg, args)
var = cg.new_Pvariable(action_id, template_arg, condition)
actions = await build_action_list(config[CONF_THEN], template_arg, args)
cg.add(var.add_then(actions))
for action in await build_action_list(config[CONF_THEN], template_arg, args):
cg.add(var.add_then_action(action))
cg.add(var.finish_then())
return var
@@ -897,7 +900,9 @@ async def repeat_action_to_code(
cg.TemplateArguments(cg.uint32, *template_arg.args),
[(cg.uint32, "iteration"), *args],
)
cg.add(var.add_then(actions))
for action in actions:
cg.add(var.add_then_action(action))
cg.add(var.finish_then())
return var
@@ -1113,7 +1118,8 @@ async def _new_automation(
templ = cg.TemplateArguments(*(arg[0] for arg in args))
obj = cg.new_Pvariable(config[CONF_AUTOMATION_ID], templ, *ctor_args)
actions = await build_action_list(config[CONF_THEN], templ, args)
cg.add(obj.add_actions(actions))
for action in actions:
cg.add(obj.add_action(action))
return obj
+1 -1
View File
@@ -664,7 +664,7 @@ async def to_code(config: ConfigType) -> None:
unregister_id,
var,
)
cg.add(auto.add_actions([unregister_action]))
cg.add(auto.add_action(unregister_action))
# Register all services at once - single allocation, no reallocations
cg.add(var.initialize_user_services(triggers))
if CORE.is_esp8266 and has_user_actions:
+2 -1
View File
@@ -607,7 +607,8 @@ async def setup_binary_sensor_core_(var, config):
if filters_config := config.get(CONF_FILTERS):
cg.add_define("USE_BINARY_SENSOR_FILTER")
filters = await cg.build_registry_list(FILTER_REGISTRY, filters_config)
cg.add(var.add_filters(filters))
for filter_ in filters:
cg.add(var.add_filter(filter_))
CORE.add_job(_build_binary_sensor_automations, var, config)
+3 -2
View File
@@ -672,7 +672,7 @@ async def delta_filter_to_code(config, filter_id):
@FILTER_REGISTRY.register("or", OrFilter, validate_filters)
async def or_filter_to_code(config, filter_id):
filters = await build_filters(config)
return cg.new_Pvariable(filter_id, cg.TemplateArguments(len(filters)), filters)
return cg.new_Pvariable(filter_id, cg.TemplateArguments(len(filters)), *filters)
@FILTER_REGISTRY.register(
@@ -1033,7 +1033,8 @@ async def setup_sensor_core_(var, config):
if config.get(CONF_FILTERS): # must exist and not be empty
cg.add_define("USE_SENSOR_FILTER")
filters = await _build_chain_filters(config[CONF_FILTERS])
cg.add(var.set_filters(filters))
for filter_ in filters:
cg.add(var.add_filter(filter_))
CORE.add_job(_build_sensor_automations, var, config)
+3
View File
@@ -545,6 +545,9 @@ optional<float> or_filter_new_value(Filter **filters, size_t count, float value,
template<size_t N> class OrFilter : public Filter {
public:
explicit OrFilter(std::initializer_list<Filter *> filters) { init_array_from(this->filters_, filters); }
// Codegen passes each filter as its own argument; a list literal would sit in .rodata (RAM on ESP8266).
template<typename... Fs>
requires(sizeof...(Fs) == N) explicit OrFilter(Fs *...filters) : filters_{filters...} {}
void initialize(Sensor *parent, Filter *next) override {
Filter::initialize(parent, next);
+2 -1
View File
@@ -208,7 +208,8 @@ async def setup_text_sensor_core_(var, config):
if config.get(CONF_FILTERS): # must exist and not be empty
cg.add_define("USE_TEXT_SENSOR_FILTER")
filters = await build_filters(config[CONF_FILTERS])
cg.add(var.set_filters(filters))
for filter_ in filters:
cg.add(var.add_filter(filter_))
CORE.add_job(_build_text_sensor_automations, var, config)
+27 -6
View File
@@ -22,6 +22,9 @@ template<size_t N, typename... Ts> class AndCondition : public Condition<Ts...>
explicit AndCondition(std::initializer_list<Condition<Ts...> *> conditions) {
init_array_from(this->conditions_, conditions);
}
// Codegen passes each condition as its own argument; a list literal would sit in .rodata (RAM on ESP8266).
template<typename... Cs>
requires(sizeof...(Cs) == N) explicit AndCondition(Cs *...conditions) : conditions_{conditions...} {}
bool check(const Ts &...x) override {
for (auto *condition : this->conditions_) {
if (!condition->check(x...))
@@ -40,6 +43,9 @@ template<size_t N, typename... Ts> class OrCondition : public Condition<Ts...> {
explicit OrCondition(std::initializer_list<Condition<Ts...> *> conditions) {
init_array_from(this->conditions_, conditions);
}
// Codegen passes each condition as its own argument; a list literal would sit in .rodata (RAM on ESP8266).
template<typename... Cs>
requires(sizeof...(Cs) == N) explicit OrCondition(Cs *...conditions) : conditions_{conditions...} {}
bool check(const Ts &...x) override {
for (auto *condition : this->conditions_) {
if (condition->check(x...))
@@ -67,6 +73,9 @@ template<size_t N, typename... Ts> class XorCondition : public Condition<Ts...>
explicit XorCondition(std::initializer_list<Condition<Ts...> *> conditions) {
init_array_from(this->conditions_, conditions);
}
// Codegen passes each condition as its own argument; a list literal would sit in .rodata (RAM on ESP8266).
template<typename... Cs>
requires(sizeof...(Cs) == N) explicit XorCondition(Cs *...conditions) : conditions_{conditions...} {}
bool check(const Ts &...x) override {
size_t result = 0;
for (auto *condition : this->conditions_) {
@@ -311,10 +320,9 @@ template<bool HasElse, typename... Ts> class IfAction : public Action<Ts...> {
public:
explicit IfAction(Condition<Ts...> *condition) : condition_(condition) {}
// Precondition: add_then/add_else must be called at most once per instance.
// Codegen always batches the full action list into a single call. Calling
// twice would re-append the same inline continuation pointer and form a
// self-loop in the next_ chain.
// Precondition: build each branch either with one add_then/add_else call, or with
// add_then_action/add_else_action calls followed by one finish_then/finish_else, never both.
// Appending the inline continuation twice forms a self-loop in the next_ chain.
void add_then(const std::initializer_list<Action<Ts...> *> &actions) {
this->then_.add_actions(actions);
this->then_.add_action(&this->then_continuation_);
@@ -325,6 +333,13 @@ template<bool HasElse, typename... Ts> class IfAction : public Action<Ts...> {
this->else_.add_action(&this->else_continuation_.action);
}
// Codegen appends branch actions one at a time, then finishes each branch once; a list literal
// would be a constant array in .rodata, which is RAM on ESP8266.
void add_then_action(Action<Ts...> *action) { this->then_.add_action(action); }
void finish_then() { this->then_.add_action(&this->then_continuation_); }
void add_else_action(Action<Ts...> *action) requires(HasElse) { this->else_.add_action(action); }
void finish_else() requires(HasElse) { this->else_.add_action(&this->else_continuation_.action); }
void play_complex(const Ts &...x) override {
this->num_running_++;
if (this->condition_->check(x...)) {
@@ -364,11 +379,14 @@ template<typename... Ts> class WhileAction : public Action<Ts...> {
public:
WhileAction(Condition<Ts...> *condition) : condition_(condition) {}
// Precondition: must be called at most once per instance (see IfAction::add_then).
// Precondition: use either add_then or add_then_action plus one finish_then, never both
// (see IfAction::add_then).
void add_then(const std::initializer_list<Action<Ts...> *> &actions) {
this->then_.add_actions(actions);
this->then_.add_action(&this->loop_continuation_);
}
void add_then_action(Action<Ts...> *action) { this->then_.add_action(action); }
void finish_then() { this->then_.add_action(&this->loop_continuation_); }
friend class WhileLoopContinuation<Ts...>;
@@ -428,11 +446,14 @@ template<typename... Ts> class RepeatAction : public Action<Ts...> {
public:
TEMPLATABLE_VALUE(uint32_t, count)
// Precondition: must be called at most once per instance (see IfAction::add_then).
// Precondition: use either add_then or add_then_action plus one finish_then, never both
// (see IfAction::add_then).
void add_then(const std::initializer_list<Action<uint32_t, Ts...> *> &actions) {
this->then_.add_actions(actions);
this->then_.add_action(&this->loop_continuation_);
}
void add_then_action(Action<uint32_t, Ts...> *action) { this->then_.add_action(action); }
void finish_then() { this->then_.add_action(&this->loop_continuation_); }
friend class RepeatLoopContinuation<Ts...>;
@@ -15,7 +15,7 @@ def test_single_condition_groups_are_unwrapped(
assert "IfAction<false>(lambdacondition_id);" in main_cpp
assert "IfAction<false>(lambdacondition_id_2);" in main_cpp
group = re.search(
r"new\((\w+)\) AndCondition<2>\(\{lambdacondition_id_3, lambdacondition_id_4\}\);",
r"new\((\w+)\) AndCondition<2>\(lambdacondition_id_3, lambdacondition_id_4\);",
main_cpp,
)
assert group is not None
@@ -15,10 +15,10 @@ def test_timeout_throttle_pair_is_merged(
len(re.findall(r"new\(\w+\) sensor::TimeoutThrottleFilter\(1000\);", main_cpp))
== 1
)
merged = re.search(r"merged->set_filters\(\{(\w+)\}\);", main_cpp)
assert merged is not None
assert f"new({merged.group(1)}) sensor::TimeoutThrottleFilter(1000);" in main_cpp
merged = re.findall(r"merged->add_filter\((\w+)\);", main_cpp)
assert len(merged) == 1
assert f"new({merged[0]}) sensor::TimeoutThrottleFilter(1000);" in main_cpp
# Every other shape keeps its two filters
for sensor_id in ("other_periods", "other_order", "value_list", "configured_value"):
call = re.search(rf"{sensor_id}->set_filters\(\{{([^}}]*)\}}\);", main_cpp)
assert call is not None and len(call.group(1).split(",")) == 2, sensor_id
calls = re.findall(rf"{sensor_id}->add_filter\(\w+\);", main_cpp)
assert len(calls) == 2, sensor_id
@@ -0,0 +1,62 @@
esphome:
name: test
esp32:
board: esp32dev
logger:
globals:
- id: counter
type: int
initial_value: "0"
sensor:
- platform: template
id: temp
lambda: return 1.0;
filters:
- multiply: 2.0
- offset: 1.0
- or:
- throttle: 2s
- delta: 1.0
binary_sensor:
- platform: template
id: button
lambda: return true;
filters:
- invert:
- delayed_on: 10ms
text_sensor:
- platform: template
id: label
lambda: return {"x"};
filters:
- to_upper:
- append: "!"
interval:
- interval: 10s
then:
- if:
condition:
and:
- lambda: return id(counter) == 0;
- binary_sensor.is_on: button
then:
- lambda: id(counter) += 1;
- logger.log: even
else:
- logger.log: odd
- repeat:
count: 2
then:
- logger.log: again
- while:
condition:
lambda: return id(counter) < 3;
then:
- lambda: id(counter) += 1;
@@ -0,0 +1,30 @@
"""Filters and action lists are added one item at a time, so no pointer list lands in .rodata."""
from collections.abc import Callable
from pathlib import Path
import re
def test_filters_and_actions_are_added_per_item(
generate_main: Callable[[str | Path], str],
component_config_path: Callable[[str], Path],
) -> None:
main_cpp = generate_main(component_config_path("per_item_lists.yaml"))
for list_call in (
"add_filters(",
"set_filters(",
"add_actions(",
"add_then(",
"add_else(",
):
assert list_call not in main_cpp
assert len(re.findall(r"temp->add_filter\(", main_cpp)) == 3
assert len(re.findall(r"button->add_filter\(", main_cpp)) == 2
assert len(re.findall(r"label->add_filter\(", main_cpp)) == 2
assert main_cpp.count("->add_then_action(") == 4
assert main_cpp.count("->add_else_action(") == 1
assert main_cpp.count("->finish_then();") == 3
assert main_cpp.count("->finish_else();") == 1
# and/or conditions and the sensor `or` filter take their items as separate arguments
assert not re.search(r"(Condition|OrFilter)<[^>]*>\(\{", main_cpp)