mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	[lvgl] Fix nested lambdas in automations unable to access parameters (#11583)
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
This commit is contained in:
		| @@ -5,6 +5,7 @@ Constants already defined in esphome.const are not duplicated here and must be i | ||||
| """ | ||||
|  | ||||
| import logging | ||||
| from typing import TYPE_CHECKING, Any | ||||
|  | ||||
| from esphome import codegen as cg, config_validation as cv | ||||
| from esphome.const import CONF_ITEMS | ||||
| @@ -12,6 +13,7 @@ from esphome.core import ID, Lambda | ||||
| from esphome.cpp_generator import LambdaExpression, MockObj | ||||
| from esphome.cpp_types import uint32 | ||||
| from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor | ||||
| from esphome.types import Expression, SafeExpType | ||||
|  | ||||
| from .helpers import requires_component | ||||
|  | ||||
| @@ -42,7 +44,13 @@ def static_cast(type, value): | ||||
| def call_lambda(lamb: LambdaExpression): | ||||
|     expr = lamb.content.strip() | ||||
|     if expr.startswith("return") and expr.endswith(";"): | ||||
|         return expr[6:][:-1].strip() | ||||
|         return expr[6:-1].strip() | ||||
|     # If lambda has parameters, call it with those parameter names | ||||
|     # Parameter names come from hardcoded component code (like "x", "it", "event") | ||||
|     # not from user input, so they're safe to use directly | ||||
|     if lamb.parameters and lamb.parameters.parameters: | ||||
|         param_names = ", ".join(str(param.id) for param in lamb.parameters.parameters) | ||||
|         return f"{lamb}({param_names})" | ||||
|     return f"{lamb}()" | ||||
|  | ||||
|  | ||||
| @@ -65,10 +73,20 @@ class LValidator: | ||||
|             return cv.returning_lambda(value) | ||||
|         return self.validator(value) | ||||
|  | ||||
|     async def process(self, value, args=()): | ||||
|     async def process( | ||||
|         self, value: Any, args: list[tuple[SafeExpType, str]] | None = None | ||||
|     ) -> Expression: | ||||
|         if value is None: | ||||
|             return None | ||||
|         if isinstance(value, Lambda): | ||||
|             # Local import to avoid circular import | ||||
|             from .lvcode import CodeContext, LambdaContext | ||||
|  | ||||
|             if TYPE_CHECKING: | ||||
|                 # CodeContext does not have get_automation_parameters | ||||
|                 # so we need to assert the type here | ||||
|                 assert isinstance(CodeContext.code_context, LambdaContext) | ||||
|             args = args or CodeContext.code_context.get_automation_parameters() | ||||
|             return cg.RawExpression( | ||||
|                 call_lambda( | ||||
|                     await cg.process_lambda(value, args, return_type=self.rtype) | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| from typing import TYPE_CHECKING, Any | ||||
|  | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import image | ||||
| from esphome.components.color import CONF_HEX, ColorStruct, from_rgbw | ||||
| @@ -17,6 +19,7 @@ from esphome.cpp_generator import MockObj | ||||
| from esphome.cpp_types import ESPTime, int32, uint32 | ||||
| from esphome.helpers import cpp_string_escape | ||||
| from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor | ||||
| from esphome.types import Expression, SafeExpType | ||||
|  | ||||
| from . import types as ty | ||||
| from .defines import ( | ||||
| @@ -388,11 +391,23 @@ class TextValidator(LValidator): | ||||
|             return value | ||||
|         return super().__call__(value) | ||||
|  | ||||
|     async def process(self, value, args=()): | ||||
|     async def process( | ||||
|         self, value: Any, args: list[tuple[SafeExpType, str]] | None = None | ||||
|     ) -> Expression: | ||||
|         # Local import to avoid circular import at module level | ||||
|  | ||||
|         from .lvcode import CodeContext, LambdaContext | ||||
|  | ||||
|         if TYPE_CHECKING: | ||||
|             # CodeContext does not have get_automation_parameters | ||||
|             # so we need to assert the type here | ||||
|             assert isinstance(CodeContext.code_context, LambdaContext) | ||||
|         args = args or CodeContext.code_context.get_automation_parameters() | ||||
|  | ||||
|         if isinstance(value, dict): | ||||
|             if format_str := value.get(CONF_FORMAT): | ||||
|                 args = [str(x) for x in value[CONF_ARGS]] | ||||
|                 arg_expr = cg.RawExpression(",".join(args)) | ||||
|                 str_args = [str(x) for x in value[CONF_ARGS]] | ||||
|                 arg_expr = cg.RawExpression(",".join(str_args)) | ||||
|                 format_str = cpp_string_escape(format_str) | ||||
|                 return literal(f"str_sprintf({format_str}, {arg_expr}).c_str()") | ||||
|             if time_format := value.get(CONF_TIME_FORMAT): | ||||
|   | ||||
| @@ -164,6 +164,9 @@ class LambdaContext(CodeContext): | ||||
|             code_text.append(text) | ||||
|         return code_text | ||||
|  | ||||
|     def get_automation_parameters(self) -> list[tuple[SafeExpType, str]]: | ||||
|         return self.parameters | ||||
|  | ||||
|     async def __aenter__(self): | ||||
|         await super().__aenter__() | ||||
|         add_line_marks(self.where) | ||||
| @@ -178,9 +181,8 @@ class LvContext(LambdaContext): | ||||
|  | ||||
|     added_lambda_count = 0 | ||||
|  | ||||
|     def __init__(self, args=None): | ||||
|         self.args = args or LVGL_COMP_ARG | ||||
|         super().__init__(parameters=self.args) | ||||
|     def __init__(self): | ||||
|         super().__init__(parameters=LVGL_COMP_ARG) | ||||
|  | ||||
|     async def __aexit__(self, exc_type, exc_val, exc_tb): | ||||
|         await super().__aexit__(exc_type, exc_val, exc_tb) | ||||
| @@ -189,6 +191,11 @@ class LvContext(LambdaContext): | ||||
|         cg.add(expression) | ||||
|         return expression | ||||
|  | ||||
|     def get_automation_parameters(self) -> list[tuple[SafeExpType, str]]: | ||||
|         # When generating automations, we don't want the `lv_component` parameter to be passed | ||||
|         # to the lambda. | ||||
|         return [] | ||||
|  | ||||
|     def __call__(self, *args): | ||||
|         return self.add(*args) | ||||
|  | ||||
|   | ||||
| @@ -5,7 +5,6 @@ from ..defines import CONF_WIDGET | ||||
| from ..lvcode import ( | ||||
|     API_EVENT, | ||||
|     EVENT_ARG, | ||||
|     LVGL_COMP_ARG, | ||||
|     UPDATE_EVENT, | ||||
|     LambdaContext, | ||||
|     LvContext, | ||||
| @@ -30,7 +29,7 @@ async def to_code(config): | ||||
|     await wait_for_widgets() | ||||
|     async with LambdaContext(EVENT_ARG) as lamb: | ||||
|         lv_add(sensor.publish_state(widget.get_value())) | ||||
|     async with LvContext(LVGL_COMP_ARG): | ||||
|     async with LvContext(): | ||||
|         lv_add( | ||||
|             lvgl_static.add_event_cb( | ||||
|                 widget.obj, | ||||
|   | ||||
| @@ -52,6 +52,19 @@ number: | ||||
|     widget: spinbox_id | ||||
|     id: lvgl_spinbox_number | ||||
|     name: LVGL Spinbox Number | ||||
|   - platform: template | ||||
|     id: test_brightness | ||||
|     name: "Test Brightness" | ||||
|     min_value: 0 | ||||
|     max_value: 255 | ||||
|     step: 1 | ||||
|     optimistic: true | ||||
|     # Test lambda in automation accessing x parameter directly | ||||
|     # This is a real-world pattern from user configs | ||||
|     on_value: | ||||
|       - lambda: !lambda |- | ||||
|           // Direct use of x parameter in automation | ||||
|           ESP_LOGD("test", "Brightness: %.0f", x); | ||||
|  | ||||
| light: | ||||
|   - platform: lvgl | ||||
| @@ -110,3 +123,21 @@ text: | ||||
|     platform: lvgl | ||||
|     widget: hello_label | ||||
|     mode: text | ||||
|  | ||||
| text_sensor: | ||||
|   - platform: template | ||||
|     id: test_text_sensor | ||||
|     name: "Test Text Sensor" | ||||
|     # Test nested lambdas in LVGL actions can access automation parameters | ||||
|     on_value: | ||||
|       - lvgl.label.update: | ||||
|           id: hello_label | ||||
|           text: !lambda return x.c_str(); | ||||
|       - lvgl.label.update: | ||||
|           id: hello_label | ||||
|           text: !lambda |- | ||||
|             // Test complex lambda with conditionals accessing x parameter | ||||
|             if (x == "*") { | ||||
|               return "WILDCARD"; | ||||
|             } | ||||
|             return x.c_str(); | ||||
|   | ||||
| @@ -257,7 +257,30 @@ lvgl: | ||||
|             text: "Hello shiny day" | ||||
|             text_color: 0xFFFFFF | ||||
|             align: bottom_mid | ||||
|         - label: | ||||
|             id: setup_lambda_label | ||||
|             # Test lambda in widget property during setup (LvContext) | ||||
|             # Should NOT receive lv_component parameter | ||||
|             text: !lambda |- | ||||
|               char buf[32]; | ||||
|               snprintf(buf, sizeof(buf), "Setup: %d", 42); | ||||
|               return std::string(buf); | ||||
|             align: top_mid | ||||
|             text_font: space16 | ||||
|         - label: | ||||
|             id: chip_info_label | ||||
|             # Test complex setup lambda (real-world pattern) | ||||
|             # Should NOT receive lv_component parameter | ||||
|             text: !lambda |- | ||||
|               // Test conditional compilation and string formatting | ||||
|               char buf[64]; | ||||
|               #ifdef USE_ESP_IDF | ||||
|               snprintf(buf, sizeof(buf), "IDF: v%d.%d", ESP_IDF_VERSION_MAJOR, ESP_IDF_VERSION_MINOR); | ||||
|               #else | ||||
|               snprintf(buf, sizeof(buf), "Arduino"); | ||||
|               #endif | ||||
|               return std::string(buf); | ||||
|             align: top_left | ||||
|         - obj: | ||||
|             align: center | ||||
|             arc_opa: COVER | ||||
|   | ||||
		Reference in New Issue
	
	Block a user