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 | import logging | ||||||
|  | from typing import TYPE_CHECKING, Any | ||||||
|  |  | ||||||
| from esphome import codegen as cg, config_validation as cv | from esphome import codegen as cg, config_validation as cv | ||||||
| from esphome.const import CONF_ITEMS | 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_generator import LambdaExpression, MockObj | ||||||
| from esphome.cpp_types import uint32 | from esphome.cpp_types import uint32 | ||||||
| from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor | from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor | ||||||
|  | from esphome.types import Expression, SafeExpType | ||||||
|  |  | ||||||
| from .helpers import requires_component | from .helpers import requires_component | ||||||
|  |  | ||||||
| @@ -42,7 +44,13 @@ def static_cast(type, value): | |||||||
| def call_lambda(lamb: LambdaExpression): | def call_lambda(lamb: LambdaExpression): | ||||||
|     expr = lamb.content.strip() |     expr = lamb.content.strip() | ||||||
|     if expr.startswith("return") and expr.endswith(";"): |     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}()" |     return f"{lamb}()" | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -65,10 +73,20 @@ class LValidator: | |||||||
|             return cv.returning_lambda(value) |             return cv.returning_lambda(value) | ||||||
|         return self.validator(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: |         if value is None: | ||||||
|             return None |             return None | ||||||
|         if isinstance(value, Lambda): |         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( |             return cg.RawExpression( | ||||||
|                 call_lambda( |                 call_lambda( | ||||||
|                     await cg.process_lambda(value, args, return_type=self.rtype) |                     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 | import esphome.codegen as cg | ||||||
| from esphome.components import image | from esphome.components import image | ||||||
| from esphome.components.color import CONF_HEX, ColorStruct, from_rgbw | 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.cpp_types import ESPTime, int32, uint32 | ||||||
| from esphome.helpers import cpp_string_escape | from esphome.helpers import cpp_string_escape | ||||||
| from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor | from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor | ||||||
|  | from esphome.types import Expression, SafeExpType | ||||||
|  |  | ||||||
| from . import types as ty | from . import types as ty | ||||||
| from .defines import ( | from .defines import ( | ||||||
| @@ -388,11 +391,23 @@ class TextValidator(LValidator): | |||||||
|             return value |             return value | ||||||
|         return super().__call__(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 isinstance(value, dict): | ||||||
|             if format_str := value.get(CONF_FORMAT): |             if format_str := value.get(CONF_FORMAT): | ||||||
|                 args = [str(x) for x in value[CONF_ARGS]] |                 str_args = [str(x) for x in value[CONF_ARGS]] | ||||||
|                 arg_expr = cg.RawExpression(",".join(args)) |                 arg_expr = cg.RawExpression(",".join(str_args)) | ||||||
|                 format_str = cpp_string_escape(format_str) |                 format_str = cpp_string_escape(format_str) | ||||||
|                 return literal(f"str_sprintf({format_str}, {arg_expr}).c_str()") |                 return literal(f"str_sprintf({format_str}, {arg_expr}).c_str()") | ||||||
|             if time_format := value.get(CONF_TIME_FORMAT): |             if time_format := value.get(CONF_TIME_FORMAT): | ||||||
|   | |||||||
| @@ -164,6 +164,9 @@ class LambdaContext(CodeContext): | |||||||
|             code_text.append(text) |             code_text.append(text) | ||||||
|         return code_text |         return code_text | ||||||
|  |  | ||||||
|  |     def get_automation_parameters(self) -> list[tuple[SafeExpType, str]]: | ||||||
|  |         return self.parameters | ||||||
|  |  | ||||||
|     async def __aenter__(self): |     async def __aenter__(self): | ||||||
|         await super().__aenter__() |         await super().__aenter__() | ||||||
|         add_line_marks(self.where) |         add_line_marks(self.where) | ||||||
| @@ -178,9 +181,8 @@ class LvContext(LambdaContext): | |||||||
|  |  | ||||||
|     added_lambda_count = 0 |     added_lambda_count = 0 | ||||||
|  |  | ||||||
|     def __init__(self, args=None): |     def __init__(self): | ||||||
|         self.args = args or LVGL_COMP_ARG |         super().__init__(parameters=LVGL_COMP_ARG) | ||||||
|         super().__init__(parameters=self.args) |  | ||||||
|  |  | ||||||
|     async def __aexit__(self, exc_type, exc_val, exc_tb): |     async def __aexit__(self, exc_type, exc_val, exc_tb): | ||||||
|         await super().__aexit__(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) |         cg.add(expression) | ||||||
|         return 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): |     def __call__(self, *args): | ||||||
|         return self.add(*args) |         return self.add(*args) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -5,7 +5,6 @@ from ..defines import CONF_WIDGET | |||||||
| from ..lvcode import ( | from ..lvcode import ( | ||||||
|     API_EVENT, |     API_EVENT, | ||||||
|     EVENT_ARG, |     EVENT_ARG, | ||||||
|     LVGL_COMP_ARG, |  | ||||||
|     UPDATE_EVENT, |     UPDATE_EVENT, | ||||||
|     LambdaContext, |     LambdaContext, | ||||||
|     LvContext, |     LvContext, | ||||||
| @@ -30,7 +29,7 @@ async def to_code(config): | |||||||
|     await wait_for_widgets() |     await wait_for_widgets() | ||||||
|     async with LambdaContext(EVENT_ARG) as lamb: |     async with LambdaContext(EVENT_ARG) as lamb: | ||||||
|         lv_add(sensor.publish_state(widget.get_value())) |         lv_add(sensor.publish_state(widget.get_value())) | ||||||
|     async with LvContext(LVGL_COMP_ARG): |     async with LvContext(): | ||||||
|         lv_add( |         lv_add( | ||||||
|             lvgl_static.add_event_cb( |             lvgl_static.add_event_cb( | ||||||
|                 widget.obj, |                 widget.obj, | ||||||
|   | |||||||
| @@ -52,6 +52,19 @@ number: | |||||||
|     widget: spinbox_id |     widget: spinbox_id | ||||||
|     id: lvgl_spinbox_number |     id: lvgl_spinbox_number | ||||||
|     name: 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: | light: | ||||||
|   - platform: lvgl |   - platform: lvgl | ||||||
| @@ -110,3 +123,21 @@ text: | |||||||
|     platform: lvgl |     platform: lvgl | ||||||
|     widget: hello_label |     widget: hello_label | ||||||
|     mode: text |     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: "Hello shiny day" | ||||||
|             text_color: 0xFFFFFF |             text_color: 0xFFFFFF | ||||||
|             align: bottom_mid |             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 |             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: |         - obj: | ||||||
|             align: center |             align: center | ||||||
|             arc_opa: COVER |             arc_opa: COVER | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user