mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 10:12:52 +00:00
[core] Emit filter, action and condition lists one item at a time (#20116)
This commit is contained in:
+15
-9
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user