mirror of
https://github.com/esphome/esphome.git
synced 2025-10-31 23:21:54 +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