diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index 5c75269c64..480ba515d1 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -739,6 +739,7 @@ CONF_GRID_ROWS = "grid_rows" CONF_HEADER_BUTTONS = "header_buttons" CONF_HEADER_MODE = "header_mode" CONF_HOME = "home" +CONF_IMAGE = "image" CONF_INDICATORS = "indicators" CONF_INITIAL_FOCUS = "initial_focus" CONF_SELECTED_DIGIT = "selected_digit" @@ -752,6 +753,7 @@ CONF_LONG_PRESS_REPEAT_TIME = "long_press_repeat_time" CONF_LVGL_ID = "lvgl_id" CONF_LONG_MODE = "long_mode" CONF_MAJOR_TICKS_STYLE = "major_ticks_style" +CONF_MAPPING = "mapping" CONF_MSGBOXES = "msgboxes" CONF_OBJ = "obj" CONF_ONE_CHECKED = "one_checked" diff --git a/esphome/components/lvgl/lv_validation.py b/esphome/components/lvgl/lv_validation.py index d31c8324db..56ee3b47af 100644 --- a/esphome/components/lvgl/lv_validation.py +++ b/esphome/components/lvgl/lv_validation.py @@ -22,9 +22,12 @@ from esphome.helpers import cpp_string_escape from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from esphome.types import Expression, SafeExpType +from ..mapping import INDEX_TYPES, get_mapping_metadata from . import types as ty from .defines import ( CONF_END_VALUE, + CONF_IMAGE, + CONF_MAPPING, CONF_START_VALUE, CONF_TIME_FORMAT, LV_FONTS, @@ -375,21 +378,54 @@ def stop_value(value): return cv.int_range(0, 255)(value) -def image_validator(value): - value = cv.requires_component("image")(value) +def _image_validator(value): + if isinstance(value, dict) and CONF_MAPPING in value: + from .schemas import MAPPING_IMAGE_SCHEMA + + return MAPPING_IMAGE_SCHEMA(value) value = cv.use_id(Image_)(value) get_lv_images_used().add(value) add_lv_use("label") return value -lv_image = LValidator( - image_validator, - image.Image_.operator("ptr"), - requires="image", -) +class ImageValidator(LValidator): + def __init__(self): + super().__init__( + validator=_image_validator, + rtype=image.Image_.operator("ptr"), + requires=CONF_IMAGE, + ) + + 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 get_lambda_context_args + + args = args or get_lambda_context_args() + if isinstance(value, dict) and CONF_MAPPING in value: + mapping_id = value[CONF_MAPPING] + mapping_var = await cg.get_variable(mapping_id) + metadata = get_mapping_metadata(mapping_id.id) + index = value[CONF_VALUE] + if isinstance(index, Lambda): + index = call_lambda( + await cg.process_lambda( + index, args, return_type=metadata.from_.data_type + ) + ) + else: + index = await metadata.from_.convert_value(index) + return mapping_var.get(index) + + return await super().process(value, args) + + +lv_image = ImageValidator() + lv_image_list = LValidator( - cv.ensure_list(image_validator), + cv.ensure_list(_image_validator), cg.std_vector.template(image.Image_.operator("ptr")), requires="image", ) @@ -440,6 +476,24 @@ class TextValidator(LValidator): f"(std::isfinite({arg_expr}) ? {sprintf_str} : {nanval})" ) return literal(sprintf_str) + if mapping_id := value.get(CONF_MAPPING): + mapping_var = await cg.get_variable(mapping_id) + metadata = get_mapping_metadata(mapping_id.id) + if metadata.to_ != INDEX_TYPES["string"]: + raise ValueError( + f"Mapping {mapping_id} does not map to strings, cannot use in text" + ) + index = value[CONF_VALUE] + if isinstance(index, Lambda): + index = call_lambda( + await cg.process_lambda( + index, args, return_type=metadata.from_.data_type + ) + ) + else: + index = await metadata.from_.convert_value(index) + return mapping_var.get(index).c_str() + if time_format := value.get(CONF_TIME_FORMAT): source = value[CONF_TIME] if isinstance(source, Lambda): diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index d7df628907..13214d459d 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -3,6 +3,7 @@ from typing import Any from esphome import config_validation as cv from esphome.automation import Trigger, validate_automation +from esphome.components.mapping import mapping_class from esphome.components.time import RealTimeClock from esphome.config_validation import prepend_path from esphome.const import ( @@ -17,6 +18,7 @@ from esphome.const import ( CONF_TEXT, CONF_TIME, CONF_TRIGGER_ID, + CONF_VALUE, CONF_X, CONF_Y, ) @@ -31,6 +33,7 @@ from esphome.schema_extractors import ( from . import defines as df, lv_validation as lvalid from .defines import ( CONF_EXT_CLICK_AREA, + CONF_MAPPING, CONF_SCROLL_DIR, CONF_SCROLL_SNAP_X, CONF_SCROLL_SNAP_Y, @@ -89,6 +92,20 @@ PRINTF_TEXT_SCHEMA = cv.All( validate_printf, ) +MAPPING_TEXT_SCHEMA = cv.Schema( + { + cv.Required(CONF_MAPPING): cv.use_id(mapping_class), + cv.Required(CONF_VALUE): cv.templatable(cv.string), + } +) + +MAPPING_IMAGE_SCHEMA = cv.Schema( + { + cv.Required(CONF_MAPPING): cv.use_id(mapping_class), + cv.Required(CONF_VALUE): cv.templatable(cv.string), + } +) + def _validate_text(value): """ @@ -100,6 +117,8 @@ def _validate_text(value): if isinstance(value, dict): if CONF_TIME_FORMAT in value: return TIME_TEXT_SCHEMA(value) + if CONF_MAPPING in value: + return MAPPING_TEXT_SCHEMA(value) return PRINTF_TEXT_SCHEMA(value) return cv.templatable(cv.string)(value) diff --git a/esphome/components/lvgl/widgets/img.py b/esphome/components/lvgl/widgets/img.py index 8a046fea33..da81ab7737 100644 --- a/esphome/components/lvgl/widgets/img.py +++ b/esphome/components/lvgl/widgets/img.py @@ -1,3 +1,5 @@ +from esphome.components.image import INSTANCE_TYPE as IMAGE_TYPE +from esphome.components.mapping import get_mapping_metadata import esphome.config_validation as cv from esphome.const import ( CONF_ANGLE, @@ -9,7 +11,9 @@ from esphome.const import ( from ..defines import ( CONF_ANTIALIAS, + CONF_IMAGE, CONF_MAIN, + CONF_MAPPING, CONF_PIVOT_X, CONF_PIVOT_Y, CONF_SCALE, @@ -21,8 +25,6 @@ from ..types import lv_image_t from . import Widget, WidgetType from .label import CONF_LABEL -CONF_IMAGE = "image" - BASE_IMG_SCHEMA = cv.Schema( { cv.Optional(CONF_PIVOT_X): size, @@ -69,5 +71,16 @@ class ImgType(WidgetType): for prop, validator in BASE_IMG_SCHEMA.schema.items(): await w.set_property(prop, config, processor=validator) + def final_validate(self, widget, update_config, widget_config, path): + src = update_config.get(CONF_SRC) + if isinstance(src, dict) and CONF_MAPPING in src: + mapping_id = src[CONF_MAPPING] + metadata = get_mapping_metadata(mapping_id.id) + if str(metadata.to_.data_type) != str(IMAGE_TYPE): + raise cv.Invalid( + f"Mapping '{mapping_id}' does not map to an image type, but '{metadata.to_.data_type}'", + path=path + [CONF_SRC, CONF_MAPPING], + ) + img_spec = ImgType() diff --git a/tests/components/lvgl/common.yaml b/tests/components/lvgl/common.yaml index f500002f40..b4d5fe0387 100644 --- a/tests/components/lvgl/common.yaml +++ b/tests/components/lvgl/common.yaml @@ -91,8 +91,8 @@ binary_sensor: animation: move_right time: 600ms - platform: lvgl - id: button_checker - name: LVGL button + id: common_button_checker + name: Common button widget: spin_up on_state: then: diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 4b18b99848..d6cd3821f9 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -263,6 +263,9 @@ lvgl: bg_opa: !lambda return 0.5; - lvgl.image.update: id: lv_image + src: + mapping: image_map + value: !lambda return round(1.0); scale: !lambda return 512; rotation: !lambda return 100; pivot_x: !lambda return 20; @@ -388,9 +391,16 @@ lvgl: text_font: montserrat_40 border_post: true on_press: - lvgl.label.update: - id: hello_label - text: Goodbye + - lvgl.label.update: + id: hello_label + text: + mapping: lvgl_string_map + value: !lambda return 2; + - lvgl.label.update: + id: hello_label + text: + mapping: lvgl_string_map + value: 2 on_click: then: - lvgl.animimg.stop: anim_img @@ -1496,6 +1506,21 @@ image: invert_alpha: true transparency: alpha_channel +mapping: + - id: image_map + from: int + to: image + entries: + 0: cat_image + 1: dog_image + - id: lvgl_string_map + from: int + to: string + entries: + 0: "First" + 1: "Second" + 2: "Third" + color: - id: light_blue hex: "3340FF"