1
0
mirror of https://github.com/esphome/esphome.git synced 2025-10-31 23:21:54 +00:00

Stateless lambdas

This commit is contained in:
J. Nick Koston
2025-10-26 00:35:09 -07:00
parent e212ed024d
commit 73d510d502
11 changed files with 162 additions and 2 deletions

View File

@@ -87,6 +87,7 @@ def validate_potentially_or_condition(value):
DelayAction = cg.esphome_ns.class_("DelayAction", Action, cg.Component) DelayAction = cg.esphome_ns.class_("DelayAction", Action, cg.Component)
LambdaAction = cg.esphome_ns.class_("LambdaAction", Action) LambdaAction = cg.esphome_ns.class_("LambdaAction", Action)
StatelessLambdaAction = cg.esphome_ns.class_("StatelessLambdaAction", Action)
IfAction = cg.esphome_ns.class_("IfAction", Action) IfAction = cg.esphome_ns.class_("IfAction", Action)
WhileAction = cg.esphome_ns.class_("WhileAction", Action) WhileAction = cg.esphome_ns.class_("WhileAction", Action)
RepeatAction = cg.esphome_ns.class_("RepeatAction", Action) RepeatAction = cg.esphome_ns.class_("RepeatAction", Action)
@@ -97,6 +98,7 @@ ResumeComponentAction = cg.esphome_ns.class_("ResumeComponentAction", Action)
Automation = cg.esphome_ns.class_("Automation") Automation = cg.esphome_ns.class_("Automation")
LambdaCondition = cg.esphome_ns.class_("LambdaCondition", Condition) LambdaCondition = cg.esphome_ns.class_("LambdaCondition", Condition)
StatelessLambdaCondition = cg.esphome_ns.class_("StatelessLambdaCondition", Condition)
ForCondition = cg.esphome_ns.class_("ForCondition", Condition, cg.Component) ForCondition = cg.esphome_ns.class_("ForCondition", Condition, cg.Component)
@@ -240,6 +242,11 @@ async def lambda_condition_to_code(
args: TemplateArgsType, args: TemplateArgsType,
) -> MockObj: ) -> MockObj:
lambda_ = await cg.process_lambda(config, args, return_type=bool) lambda_ = await cg.process_lambda(config, args, return_type=bool)
# Use optimized StatelessLambdaCondition for lambdas with no capture
if lambda_.capture == "":
# Override the condition_id type to use StatelessLambdaCondition
condition_id = condition_id.copy()
condition_id.type = StatelessLambdaCondition
return cg.new_Pvariable(condition_id, template_arg, lambda_) return cg.new_Pvariable(condition_id, template_arg, lambda_)
@@ -406,6 +413,11 @@ async def lambda_action_to_code(
args: TemplateArgsType, args: TemplateArgsType,
) -> MockObj: ) -> MockObj:
lambda_ = await cg.process_lambda(config, args, return_type=cg.void) lambda_ = await cg.process_lambda(config, args, return_type=cg.void)
# Use optimized StatelessLambdaAction for lambdas with no capture
if lambda_.capture == "":
# Override the action_id type to use StatelessLambdaAction
action_id = action_id.copy()
action_id.type = StatelessLambdaAction
return cg.new_Pvariable(action_id, template_arg, lambda_) return cg.new_Pvariable(action_id, template_arg, lambda_)

View File

@@ -155,6 +155,7 @@ DelayedOffFilter = binary_sensor_ns.class_("DelayedOffFilter", Filter, cg.Compon
InvertFilter = binary_sensor_ns.class_("InvertFilter", Filter) InvertFilter = binary_sensor_ns.class_("InvertFilter", Filter)
AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter, cg.Component) AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter, cg.Component)
LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter) LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter)
StatelessLambdaFilter = binary_sensor_ns.class_("StatelessLambdaFilter", Filter)
SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter, cg.Component) SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter, cg.Component)
_LOGGER = getLogger(__name__) _LOGGER = getLogger(__name__)
@@ -299,6 +300,10 @@ async def lambda_filter_to_code(config, filter_id):
lambda_ = await cg.process_lambda( lambda_ = await cg.process_lambda(
config, [(bool, "x")], return_type=cg.optional.template(bool) config, [(bool, "x")], return_type=cg.optional.template(bool)
) )
# Use optimized StatelessLambdaFilter for lambdas with no capture
if lambda_.capture == "":
filter_id = filter_id.copy()
filter_id.type = StatelessLambdaFilter
return cg.new_Pvariable(filter_id, lambda_) return cg.new_Pvariable(filter_id, lambda_)

View File

@@ -111,6 +111,23 @@ class LambdaFilter : public Filter {
std::function<optional<bool>(bool)> f_; std::function<optional<bool>(bool)> f_;
}; };
/** Optimized lambda filter for stateless lambdas (no capture).
*
* Uses function pointer instead of std::function to reduce memory overhead.
* Memory: 8 bytes (function pointer) vs 32 bytes (std::function).
*/
class StatelessLambdaFilter : public Filter {
public:
using stateless_lambda_filter_t = optional<bool> (*)(bool);
explicit StatelessLambdaFilter(stateless_lambda_filter_t f) : f_(f) {}
optional<bool> new_value(bool value) override { return this->f_(value); }
protected:
stateless_lambda_filter_t f_;
};
class SettleFilter : public Filter, public Component { class SettleFilter : public Filter, public Component {
public: public:
optional<bool> new_value(bool value) override; optional<bool> new_value(bool value) override;

View File

@@ -1,7 +1,7 @@
import re import re
from esphome import automation from esphome import automation
from esphome.automation import LambdaAction from esphome.automation import LambdaAction, StatelessLambdaAction
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant
from esphome.components.esp32.const import ( from esphome.components.esp32.const import (
@@ -430,6 +430,10 @@ async def logger_log_action_to_code(config, action_id, template_arg, args):
text = str(cg.statement(esp_log(config[CONF_TAG], config[CONF_FORMAT], *args_))) text = str(cg.statement(esp_log(config[CONF_TAG], config[CONF_FORMAT], *args_)))
lambda_ = await cg.process_lambda(Lambda(text), args, return_type=cg.void) lambda_ = await cg.process_lambda(Lambda(text), args, return_type=cg.void)
# Use optimized StatelessLambdaAction for lambdas with no capture
if lambda_.capture == "":
action_id = action_id.copy()
action_id.type = StatelessLambdaAction
return cg.new_Pvariable(action_id, template_arg, lambda_) return cg.new_Pvariable(action_id, template_arg, lambda_)
@@ -455,6 +459,10 @@ async def logger_set_level_to_code(config, action_id, template_arg, args):
text = str(cg.statement(logger.set_log_level(level))) text = str(cg.statement(logger.set_log_level(level)))
lambda_ = await cg.process_lambda(Lambda(text), args, return_type=cg.void) lambda_ = await cg.process_lambda(Lambda(text), args, return_type=cg.void)
# Use optimized StatelessLambdaAction for lambdas with no capture
if lambda_.capture == "":
action_id = action_id.copy()
action_id.type = StatelessLambdaAction
return cg.new_Pvariable(action_id, template_arg, lambda_) return cg.new_Pvariable(action_id, template_arg, lambda_)

View File

@@ -261,6 +261,7 @@ ExponentialMovingAverageFilter = sensor_ns.class_(
) )
ThrottleAverageFilter = sensor_ns.class_("ThrottleAverageFilter", Filter, cg.Component) ThrottleAverageFilter = sensor_ns.class_("ThrottleAverageFilter", Filter, cg.Component)
LambdaFilter = sensor_ns.class_("LambdaFilter", Filter) LambdaFilter = sensor_ns.class_("LambdaFilter", Filter)
StatelessLambdaFilter = sensor_ns.class_("StatelessLambdaFilter", Filter)
OffsetFilter = sensor_ns.class_("OffsetFilter", Filter) OffsetFilter = sensor_ns.class_("OffsetFilter", Filter)
MultiplyFilter = sensor_ns.class_("MultiplyFilter", Filter) MultiplyFilter = sensor_ns.class_("MultiplyFilter", Filter)
ValueListFilter = sensor_ns.class_("ValueListFilter", Filter) ValueListFilter = sensor_ns.class_("ValueListFilter", Filter)
@@ -573,6 +574,10 @@ async def lambda_filter_to_code(config, filter_id):
lambda_ = await cg.process_lambda( lambda_ = await cg.process_lambda(
config, [(float, "x")], return_type=cg.optional.template(float) config, [(float, "x")], return_type=cg.optional.template(float)
) )
# Use optimized StatelessLambdaFilter for lambdas with no capture
if lambda_.capture == "":
filter_id = filter_id.copy()
filter_id.type = StatelessLambdaFilter
return cg.new_Pvariable(filter_id, lambda_) return cg.new_Pvariable(filter_id, lambda_)

View File

@@ -296,6 +296,23 @@ class LambdaFilter : public Filter {
lambda_filter_t lambda_filter_; lambda_filter_t lambda_filter_;
}; };
/** Optimized lambda filter for stateless lambdas (no capture).
*
* Uses function pointer instead of std::function to reduce memory overhead.
* Memory: 8 bytes (function pointer) vs 32 bytes (std::function).
*/
class StatelessLambdaFilter : public Filter {
public:
using stateless_lambda_filter_t = optional<float> (*)(float);
explicit StatelessLambdaFilter(stateless_lambda_filter_t lambda_filter) : lambda_filter_(lambda_filter) {}
optional<float> new_value(float value) override { return this->lambda_filter_(value); }
protected:
stateless_lambda_filter_t lambda_filter_;
};
/// A simple filter that adds `offset` to each value it receives. /// A simple filter that adds `offset` to each value it receives.
class OffsetFilter : public Filter { class OffsetFilter : public Filter {
public: public:

View File

@@ -57,6 +57,7 @@ validate_filters = cv.validate_registry("filter", FILTER_REGISTRY)
# Filters # Filters
Filter = text_sensor_ns.class_("Filter") Filter = text_sensor_ns.class_("Filter")
LambdaFilter = text_sensor_ns.class_("LambdaFilter", Filter) LambdaFilter = text_sensor_ns.class_("LambdaFilter", Filter)
StatelessLambdaFilter = text_sensor_ns.class_("StatelessLambdaFilter", Filter)
ToUpperFilter = text_sensor_ns.class_("ToUpperFilter", Filter) ToUpperFilter = text_sensor_ns.class_("ToUpperFilter", Filter)
ToLowerFilter = text_sensor_ns.class_("ToLowerFilter", Filter) ToLowerFilter = text_sensor_ns.class_("ToLowerFilter", Filter)
AppendFilter = text_sensor_ns.class_("AppendFilter", Filter) AppendFilter = text_sensor_ns.class_("AppendFilter", Filter)
@@ -70,6 +71,10 @@ async def lambda_filter_to_code(config, filter_id):
lambda_ = await cg.process_lambda( lambda_ = await cg.process_lambda(
config, [(cg.std_string, "x")], return_type=cg.optional.template(cg.std_string) config, [(cg.std_string, "x")], return_type=cg.optional.template(cg.std_string)
) )
# Use optimized StatelessLambdaFilter for lambdas with no capture
if lambda_.capture == "":
filter_id = filter_id.copy()
filter_id.type = StatelessLambdaFilter
return cg.new_Pvariable(filter_id, lambda_) return cg.new_Pvariable(filter_id, lambda_)

View File

@@ -62,6 +62,23 @@ class LambdaFilter : public Filter {
lambda_filter_t lambda_filter_; lambda_filter_t lambda_filter_;
}; };
/** Optimized lambda filter for stateless lambdas (no capture).
*
* Uses function pointer instead of std::function to reduce memory overhead.
* Memory: 8 bytes (function pointer) vs 32 bytes (std::function).
*/
class StatelessLambdaFilter : public Filter {
public:
using stateless_lambda_filter_t = optional<std::string> (*)(std::string);
explicit StatelessLambdaFilter(stateless_lambda_filter_t lambda_filter) : lambda_filter_(lambda_filter) {}
optional<std::string> new_value(std::string value) override { return this->lambda_filter_(value); }
protected:
stateless_lambda_filter_t lambda_filter_;
};
/// A simple filter that converts all text to uppercase /// A simple filter that converts all text to uppercase
class ToUpperFilter : public Filter { class ToUpperFilter : public Filter {
public: public:

View File

@@ -79,6 +79,18 @@ template<typename... Ts> class LambdaCondition : public Condition<Ts...> {
std::function<bool(Ts...)> f_; std::function<bool(Ts...)> f_;
}; };
/// Optimized lambda condition for stateless lambdas (no capture).
/// Uses function pointer instead of std::function to reduce memory overhead.
/// Memory: 8 bytes (function pointer) vs 32 bytes (std::function).
template<typename... Ts> class StatelessLambdaCondition : public Condition<Ts...> {
public:
explicit StatelessLambdaCondition(bool (*f)(Ts...)) : f_(f) {}
bool check(Ts... x) override { return this->f_(x...); }
protected:
bool (*f_)(Ts...);
};
template<typename... Ts> class ForCondition : public Condition<Ts...>, public Component { template<typename... Ts> class ForCondition : public Condition<Ts...>, public Component {
public: public:
explicit ForCondition(Condition<> *condition) : condition_(condition) {} explicit ForCondition(Condition<> *condition) : condition_(condition) {}
@@ -190,6 +202,19 @@ template<typename... Ts> class LambdaAction : public Action<Ts...> {
std::function<void(Ts...)> f_; std::function<void(Ts...)> f_;
}; };
/// Optimized lambda action for stateless lambdas (no capture).
/// Uses function pointer instead of std::function to reduce memory overhead.
/// Memory: 8 bytes (function pointer) vs 32 bytes (std::function).
template<typename... Ts> class StatelessLambdaAction : public Action<Ts...> {
public:
explicit StatelessLambdaAction(void (*f)(Ts...)) : f_(f) {}
void play(Ts... x) override { this->f_(x...); }
protected:
void (*f_)(Ts...);
};
template<typename... Ts> class IfAction : public Action<Ts...> { template<typename... Ts> class IfAction : public Action<Ts...> {
public: public:
explicit IfAction(Condition<Ts...> *condition) : condition_(condition) {} explicit IfAction(Condition<Ts...> *condition) : condition_(condition) {}

View File

@@ -198,7 +198,10 @@ class LambdaExpression(Expression):
self.return_type = safe_exp(return_type) if return_type is not None else None self.return_type = safe_exp(return_type) if return_type is not None else None
def __str__(self): def __str__(self):
cpp = f"[{self.capture}]({self.parameters})" # Unary + converts stateless lambda to function pointer
# This allows implicit conversion to void (*)() or bool (*)()
prefix = "+" if self.capture == "" else ""
cpp = f"{prefix}[{self.capture}]({self.parameters})"
if self.return_type is not None: if self.return_type is not None:
cpp += f" -> {self.return_type}" cpp += f" -> {self.return_type}"
cpp += " {\n" cpp += " {\n"
@@ -700,6 +703,12 @@ async def process_lambda(
parts[i * 3 + 1] = var parts[i * 3 + 1] = var
parts[i * 3 + 2] = "" parts[i * 3 + 2] = ""
# All id() references are global variables in generated C++ code.
# Global variables should not be captured - they're accessible everywhere.
# Use empty capture instead of capture-by-value.
if capture == "=":
capture = ""
if isinstance(value, ESPHomeDataBase) and value.esp_range is not None: if isinstance(value, ESPHomeDataBase) and value.esp_range is not None:
location = value.esp_range.start_mark location = value.esp_range.start_mark
location.line += value.content_offset location.line += value.content_offset

View File

@@ -173,6 +173,46 @@ class TestLambdaExpression:
"}" "}"
) )
def test_str__stateless_no_return(self):
"""Test stateless lambda (empty capture) gets unary + prefix"""
target = cg.LambdaExpression(
('ESP_LOGD("main", "Test message");',),
(), # No parameters
"", # Empty capture (stateless)
)
actual = str(target)
assert actual == ('+[]() {\n ESP_LOGD("main", "Test message");\n}')
def test_str__stateless_with_return(self):
"""Test stateless lambda with return type gets unary + prefix"""
target = cg.LambdaExpression(
("return global_value > 0;",),
(), # No parameters
"", # Empty capture (stateless)
bool, # Return type
)
actual = str(target)
assert actual == ("+[]() -> bool {\n return global_value > 0;\n}")
def test_str__stateless_with_params(self):
"""Test stateless lambda with parameters gets unary + prefix"""
target = cg.LambdaExpression(
("return foo + bar;",),
((int, "foo"), (float, "bar")),
"", # Empty capture (stateless)
float,
)
actual = str(target)
assert actual == (
"+[](int32_t foo, float bar) -> float {\n return foo + bar;\n}"
)
class TestLiterals: class TestLiterals:
@pytest.mark.parametrize( @pytest.mark.parametrize(