mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	Merge branch 'integration' into memory_api
This commit is contained in:
		| @@ -16,7 +16,12 @@ from esphome.const import ( | ||||
|     CONF_UPDATE_INTERVAL, | ||||
| ) | ||||
| from esphome.core import ID | ||||
| from esphome.cpp_generator import MockObj, MockObjClass, TemplateArgsType | ||||
| from esphome.cpp_generator import ( | ||||
|     LambdaExpression, | ||||
|     MockObj, | ||||
|     MockObjClass, | ||||
|     TemplateArgsType, | ||||
| ) | ||||
| from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor | ||||
| from esphome.types import ConfigType | ||||
| from esphome.util import Registry | ||||
| @@ -87,6 +92,7 @@ def validate_potentially_or_condition(value): | ||||
|  | ||||
| DelayAction = cg.esphome_ns.class_("DelayAction", Action, cg.Component) | ||||
| LambdaAction = cg.esphome_ns.class_("LambdaAction", Action) | ||||
| StatelessLambdaAction = cg.esphome_ns.class_("StatelessLambdaAction", Action) | ||||
| IfAction = cg.esphome_ns.class_("IfAction", Action) | ||||
| WhileAction = cg.esphome_ns.class_("WhileAction", Action) | ||||
| RepeatAction = cg.esphome_ns.class_("RepeatAction", Action) | ||||
| @@ -97,9 +103,40 @@ ResumeComponentAction = cg.esphome_ns.class_("ResumeComponentAction", Action) | ||||
| Automation = cg.esphome_ns.class_("Automation") | ||||
|  | ||||
| LambdaCondition = cg.esphome_ns.class_("LambdaCondition", Condition) | ||||
| StatelessLambdaCondition = cg.esphome_ns.class_("StatelessLambdaCondition", Condition) | ||||
| ForCondition = cg.esphome_ns.class_("ForCondition", Condition, cg.Component) | ||||
|  | ||||
|  | ||||
| def new_lambda_pvariable( | ||||
|     id_obj: ID, | ||||
|     lambda_expr: LambdaExpression, | ||||
|     stateless_class: MockObjClass, | ||||
|     template_arg: cg.TemplateArguments | None = None, | ||||
| ) -> MockObj: | ||||
|     """Create Pvariable for lambda, using stateless class if applicable. | ||||
|  | ||||
|     Combines ID selection and Pvariable creation in one call. For stateless | ||||
|     lambdas (empty capture), uses function pointer instead of std::function. | ||||
|  | ||||
|     Args: | ||||
|         id_obj: The ID object (action_id, condition_id, or filter_id) | ||||
|         lambda_expr: The lambda expression object | ||||
|         stateless_class: The stateless class to use for stateless lambdas | ||||
|         template_arg: Optional template arguments (for actions/conditions) | ||||
|  | ||||
|     Returns: | ||||
|         The created Pvariable | ||||
|     """ | ||||
|     # For stateless lambdas, use function pointer instead of std::function | ||||
|     if lambda_expr.capture == "": | ||||
|         id_obj = id_obj.copy() | ||||
|         id_obj.type = stateless_class | ||||
|  | ||||
|     if template_arg is not None: | ||||
|         return cg.new_Pvariable(id_obj, template_arg, lambda_expr) | ||||
|     return cg.new_Pvariable(id_obj, lambda_expr) | ||||
|  | ||||
|  | ||||
| def validate_automation(extra_schema=None, extra_validators=None, single=False): | ||||
|     if extra_schema is None: | ||||
|         extra_schema = {} | ||||
| @@ -240,7 +277,9 @@ async def lambda_condition_to_code( | ||||
|     args: TemplateArgsType, | ||||
| ) -> MockObj: | ||||
|     lambda_ = await cg.process_lambda(config, args, return_type=bool) | ||||
|     return cg.new_Pvariable(condition_id, template_arg, lambda_) | ||||
|     return new_lambda_pvariable( | ||||
|         condition_id, lambda_, StatelessLambdaCondition, template_arg | ||||
|     ) | ||||
|  | ||||
|  | ||||
| @register_condition( | ||||
| @@ -406,7 +445,7 @@ async def lambda_action_to_code( | ||||
|     args: TemplateArgsType, | ||||
| ) -> MockObj: | ||||
|     lambda_ = await cg.process_lambda(config, args, return_type=cg.void) | ||||
|     return cg.new_Pvariable(action_id, template_arg, lambda_) | ||||
|     return new_lambda_pvariable(action_id, lambda_, StatelessLambdaAction, template_arg) | ||||
|  | ||||
|  | ||||
| @register_action( | ||||
|   | ||||
| @@ -155,6 +155,7 @@ DelayedOffFilter = binary_sensor_ns.class_("DelayedOffFilter", Filter, cg.Compon | ||||
| InvertFilter = binary_sensor_ns.class_("InvertFilter", Filter) | ||||
| AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter, cg.Component) | ||||
| LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter) | ||||
| StatelessLambdaFilter = binary_sensor_ns.class_("StatelessLambdaFilter", Filter) | ||||
| SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter, cg.Component) | ||||
|  | ||||
| _LOGGER = getLogger(__name__) | ||||
| @@ -299,7 +300,7 @@ async def lambda_filter_to_code(config, filter_id): | ||||
|     lambda_ = await cg.process_lambda( | ||||
|         config, [(bool, "x")], return_type=cg.optional.template(bool) | ||||
|     ) | ||||
|     return cg.new_Pvariable(filter_id, lambda_) | ||||
|     return automation.new_lambda_pvariable(filter_id, lambda_, StatelessLambdaFilter) | ||||
|  | ||||
|  | ||||
| @register_filter( | ||||
|   | ||||
| @@ -111,6 +111,23 @@ class LambdaFilter : public Filter { | ||||
|   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 { | ||||
|  public: | ||||
|   optional<bool> new_value(bool value) override; | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import re | ||||
|  | ||||
| from esphome import automation | ||||
| from esphome.automation import LambdaAction | ||||
| from esphome.automation import LambdaAction, StatelessLambdaAction | ||||
| import esphome.codegen as cg | ||||
| from esphome.components.esp32 import add_idf_sdkconfig_option, get_esp32_variant | ||||
| from esphome.components.esp32.const import ( | ||||
| @@ -430,7 +430,9 @@ 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_))) | ||||
|  | ||||
|     lambda_ = await cg.process_lambda(Lambda(text), args, return_type=cg.void) | ||||
|     return cg.new_Pvariable(action_id, template_arg, lambda_) | ||||
|     return automation.new_lambda_pvariable( | ||||
|         action_id, lambda_, StatelessLambdaAction, template_arg | ||||
|     ) | ||||
|  | ||||
|  | ||||
| @automation.register_action( | ||||
| @@ -455,7 +457,9 @@ async def logger_set_level_to_code(config, action_id, template_arg, args): | ||||
|         text = str(cg.statement(logger.set_log_level(level))) | ||||
|  | ||||
|     lambda_ = await cg.process_lambda(Lambda(text), args, return_type=cg.void) | ||||
|     return cg.new_Pvariable(action_id, template_arg, lambda_) | ||||
|     return automation.new_lambda_pvariable( | ||||
|         action_id, lambda_, StatelessLambdaAction, template_arg | ||||
|     ) | ||||
|  | ||||
|  | ||||
| FILTER_SOURCE_FILES = filter_source_files_from_platform( | ||||
|   | ||||
| @@ -261,6 +261,7 @@ ExponentialMovingAverageFilter = sensor_ns.class_( | ||||
| ) | ||||
| ThrottleAverageFilter = sensor_ns.class_("ThrottleAverageFilter", Filter, cg.Component) | ||||
| LambdaFilter = sensor_ns.class_("LambdaFilter", Filter) | ||||
| StatelessLambdaFilter = sensor_ns.class_("StatelessLambdaFilter", Filter) | ||||
| OffsetFilter = sensor_ns.class_("OffsetFilter", Filter) | ||||
| MultiplyFilter = sensor_ns.class_("MultiplyFilter", Filter) | ||||
| ValueListFilter = sensor_ns.class_("ValueListFilter", Filter) | ||||
| @@ -573,7 +574,7 @@ async def lambda_filter_to_code(config, filter_id): | ||||
|     lambda_ = await cg.process_lambda( | ||||
|         config, [(float, "x")], return_type=cg.optional.template(float) | ||||
|     ) | ||||
|     return cg.new_Pvariable(filter_id, lambda_) | ||||
|     return automation.new_lambda_pvariable(filter_id, lambda_, StatelessLambdaFilter) | ||||
|  | ||||
|  | ||||
| DELTA_SCHEMA = cv.Schema( | ||||
|   | ||||
| @@ -296,6 +296,23 @@ class LambdaFilter : public 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. | ||||
| class OffsetFilter : public Filter { | ||||
|  public: | ||||
|   | ||||
| @@ -57,6 +57,7 @@ validate_filters = cv.validate_registry("filter", FILTER_REGISTRY) | ||||
| # Filters | ||||
| Filter = text_sensor_ns.class_("Filter") | ||||
| LambdaFilter = text_sensor_ns.class_("LambdaFilter", Filter) | ||||
| StatelessLambdaFilter = text_sensor_ns.class_("StatelessLambdaFilter", Filter) | ||||
| ToUpperFilter = text_sensor_ns.class_("ToUpperFilter", Filter) | ||||
| ToLowerFilter = text_sensor_ns.class_("ToLowerFilter", Filter) | ||||
| AppendFilter = text_sensor_ns.class_("AppendFilter", Filter) | ||||
| @@ -70,7 +71,7 @@ async def lambda_filter_to_code(config, filter_id): | ||||
|     lambda_ = await cg.process_lambda( | ||||
|         config, [(cg.std_string, "x")], return_type=cg.optional.template(cg.std_string) | ||||
|     ) | ||||
|     return cg.new_Pvariable(filter_id, lambda_) | ||||
|     return automation.new_lambda_pvariable(filter_id, lambda_, StatelessLambdaFilter) | ||||
|  | ||||
|  | ||||
| @FILTER_REGISTRY.register("to_upper", ToUpperFilter, {}) | ||||
|   | ||||
| @@ -62,6 +62,23 @@ class LambdaFilter : public 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 | ||||
| class ToUpperFilter : public Filter { | ||||
|  public: | ||||
|   | ||||
| @@ -79,6 +79,18 @@ template<typename... Ts> class LambdaCondition : public Condition<Ts...> { | ||||
|   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 { | ||||
|  public: | ||||
|   explicit ForCondition(Condition<> *condition) : condition_(condition) {} | ||||
| @@ -190,6 +202,19 @@ template<typename... Ts> class LambdaAction : public Action<Ts...> { | ||||
|   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...> { | ||||
|  public: | ||||
|   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 | ||||
|  | ||||
|     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: | ||||
|             cpp += f" -> {self.return_type}" | ||||
|         cpp += " {\n" | ||||
| @@ -700,6 +703,12 @@ async def process_lambda( | ||||
|             parts[i * 3 + 1] = var | ||||
|         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: | ||||
|         location = value.esp_range.start_mark | ||||
|         location.line += value.content_offset | ||||
|   | ||||
| @@ -58,7 +58,7 @@ def test_text_config_value_mode_set(generate_main): | ||||
|  | ||||
| def test_text_config_lamda_is_set(generate_main): | ||||
|     """ | ||||
|     Test if lambda is set for lambda mode | ||||
|     Test if lambda is set for lambda mode (optimized with stateless lambda) | ||||
|     """ | ||||
|     # Given | ||||
|  | ||||
| @@ -66,5 +66,5 @@ def test_text_config_lamda_is_set(generate_main): | ||||
|     main_cpp = generate_main("tests/component_tests/text/test_text.yaml") | ||||
|  | ||||
|     # Then | ||||
|     assert "it_4->set_template([=]() -> esphome::optional<std::string> {" in main_cpp | ||||
|     assert "it_4->set_template(+[]() -> esphome::optional<std::string> {" in main_cpp | ||||
|     assert 'return std::string{"Hello"};' in main_cpp | ||||
|   | ||||
| @@ -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: | ||||
|     @pytest.mark.parametrize( | ||||
|   | ||||
		Reference in New Issue
	
	Block a user