mirror of
https://github.com/esphome/esphome.git
synced 2025-10-31 23:21:54 +00:00
Stateless lambdas
This commit is contained in:
@@ -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_)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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_)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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_)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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_)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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_)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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) {}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
Reference in New Issue
Block a user