diff --git a/esphome/components/lvgl/defines.py b/esphome/components/lvgl/defines.py index 7bfd26bb6ec..15a24f1ad2f 100644 --- a/esphome/components/lvgl/defines.py +++ b/esphome/components/lvgl/defines.py @@ -20,7 +20,6 @@ from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from esphome.types import Expression, SafeExpType LOGGER = logging.getLogger(__name__) -lvgl_ns = cg.esphome_ns.namespace("lvgl") DOMAIN = "lvgl" KEY_COLOR_FORMATS = "color_formats" @@ -400,6 +399,13 @@ LV_EVENT_MAP = { LV_PRESS_EVENTS = ("PRESS", "PRESSING", "RELEASE") +VALUE_ON_CHANGE = "on_change" +VALUE_ON_UPDATE = "on_update" +VALUE_ON_VALUE = "on_value" +VALUE_ON_RELEASE = "on_release" + +LV_VALUE_EVENTS = (VALUE_ON_CHANGE, VALUE_ON_UPDATE, VALUE_ON_VALUE, VALUE_ON_RELEASE) + def is_press_event(event: str) -> bool: return event.removeprefix("on_").upper() in LV_PRESS_EVENTS @@ -788,6 +794,7 @@ CONF_SKIP = "skip" CONF_SYMBOL = "symbol" CONF_TAB_ID = "tab_id" CONF_TABS = "tabs" +CONF_THEME = "theme" CONF_TICK_STYLE = "tick_style" CONF_TIME_FORMAT = "time_format" CONF_TILE = "tile" @@ -799,7 +806,7 @@ CONF_TOUCHSCREENS = "touchscreens" CONF_TRANSFORM_ROTATION = "transform_rotation" CONF_TRANSFORM_SCALE = "transform_scale" CONF_TRANSPARENCY_KEY = "transparency_key" -CONF_THEME = "theme" +CONF_TRIGGER = "trigger" CONF_UPDATE_ON_RELEASE = "update_on_release" CONF_UPDATE_WHEN_DISPLAY_IDLE = "update_when_display_idle" CONF_VISIBLE_ROW_COUNT = "visible_row_count" diff --git a/esphome/components/lvgl/lvcode.py b/esphome/components/lvgl/lvcode.py index 32fb02e3d21..de005937730 100644 --- a/esphome/components/lvgl/lvcode.py +++ b/esphome/components/lvgl/lvcode.py @@ -20,7 +20,8 @@ from esphome.cpp_generator import ( ) from esphome.yaml_util import ESPHomeDataBase -from .defines import literal, lvgl_ns +from .defines import literal +from .types import lvgl_ns LVGL_COMP = "lv_component" # used as a lambda argument in lvgl_comp() diff --git a/esphome/components/lvgl/number/__init__.py b/esphome/components/lvgl/number/__init__.py index 0c7e4b9524a..be51963ba12 100644 --- a/esphome/components/lvgl/number/__init__.py +++ b/esphome/components/lvgl/number/__init__.py @@ -1,10 +1,16 @@ import esphome.codegen as cg from esphome.components import number import esphome.config_validation as cv -from esphome.const import CONF_RESTORE_VALUE +from esphome.const import CONF_ON_RELEASE, CONF_RESTORE_VALUE from esphome.cpp_generator import MockObj -from ..defines import CONF_ANIMATED, CONF_UPDATE_ON_RELEASE, CONF_WIDGET +from ..defines import ( + CONF_ANIMATED, + CONF_TRIGGER, + CONF_UPDATE_ON_RELEASE, + CONF_WIDGET, + LOGGER, +) from ..lv_validation import animated from ..lvcode import ( EVENT_ARG, @@ -14,7 +20,8 @@ from ..lvcode import ( lv_obj, lvgl_static, ) -from ..types import LV_EVENT, LvNumber, lvgl_ns +from ..schemas import TRIGGER_EVENT_MAP, VALUE_TRIGGER_SCHEMA +from ..types import LvNumber, lvgl_ns from ..widgets import get_widgets, wait_for_widgets LVGLNumber = lvgl_ns.class_("LVGLNumber", number.Number, cg.Component) @@ -22,14 +29,22 @@ LVGLNumber = lvgl_ns.class_("LVGLNumber", number.Number, cg.Component) CONFIG_SCHEMA = number.number_schema(LVGLNumber).extend( { cv.Required(CONF_WIDGET): cv.use_id(LvNumber), + **VALUE_TRIGGER_SCHEMA, cv.Optional(CONF_ANIMATED, default=True): animated, - cv.Optional(CONF_UPDATE_ON_RELEASE, default=False): cv.boolean, + cv.Optional(CONF_UPDATE_ON_RELEASE): cv.boolean, cv.Optional(CONF_RESTORE_VALUE, default=False): cv.boolean, } ) async def to_code(config): + trigger = config[CONF_TRIGGER] + if CONF_UPDATE_ON_RELEASE in config: + LOGGER.warning( + "Option 'update_on_release' is deprecated and will be removed in 2026.11.0 - use 'trigger: on_release' instead" + ) + if config[CONF_UPDATE_ON_RELEASE]: + trigger = CONF_ON_RELEASE widget = await get_widgets(config, CONF_WIDGET) widget = widget[0] await wait_for_widgets() @@ -40,19 +55,13 @@ async def to_code(config): "value", MockObj("v") * MockObj(widget.get_scale()), config[CONF_ANIMATED] ) lv_obj.send_event(widget.obj, UPDATE_EVENT, cg.nullptr) - event_code = ( - LV_EVENT.VALUE_CHANGED - if not config[CONF_UPDATE_ON_RELEASE] - else LV_EVENT.RELEASED - ) var = await number.new_number( config, await control.get_lambda(), await value.get_lambda(), - event_code, config[CONF_RESTORE_VALUE], - max_value=widget.type.get_max(widget.config), - min_value=widget.type.get_min(widget.config), + max_value=await widget.type.get_max(widget.config), + min_value=await widget.type.get_min(widget.config), step=widget.type.get_step(widget.config), ) async with LambdaContext(EVENT_ARG) as event: @@ -60,6 +69,8 @@ async def to_code(config): await cg.register_component(var, config) cg.add( lvgl_static.add_event_cb( - widget.obj, await event.get_lambda(), UPDATE_EVENT, event_code + widget.obj, + await event.get_lambda(), + *TRIGGER_EVENT_MAP[trigger], ) ) diff --git a/esphome/components/lvgl/number/lvgl_number.h b/esphome/components/lvgl/number/lvgl_number.h index ba16b1f0b39..3fda9427c50 100644 --- a/esphome/components/lvgl/number/lvgl_number.h +++ b/esphome/components/lvgl/number/lvgl_number.h @@ -10,12 +10,8 @@ namespace esphome::lvgl { class LVGLNumber : public number::Number, public Component { public: - LVGLNumber(std::function control_lambda, std::function value_lambda, lv_event_code_t event, - bool restore) - : control_lambda_(std::move(control_lambda)), - value_lambda_(std::move(value_lambda)), - event_(event), - restore_(restore) {} + LVGLNumber(std::function control_lambda, std::function value_lambda, bool restore) + : control_lambda_(std::move(control_lambda)), value_lambda_(std::move(value_lambda)), restore_(restore) {} void setup() override { float value = this->value_lambda_(); @@ -42,7 +38,6 @@ class LVGLNumber : public number::Number, public Component { } std::function control_lambda_; std::function value_lambda_; - lv_event_code_t event_; bool restore_; ESPPreferenceObject pref_{}; }; diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index 6b71c2875e7..553e0f7398d 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -10,6 +10,7 @@ from esphome.const import ( CONF_GROUP, CONF_ID, CONF_ON_BOOT, + CONF_ON_UPDATE, CONF_ON_VALUE, CONF_STATE, CONF_TEXT, @@ -29,7 +30,13 @@ from .defines import ( CONF_SCROLL_SNAP_Y, CONF_SCROLLBAR_MODE, CONF_TIME_FORMAT, + CONF_TRIGGER, LV_GRAD_DIR, + LV_VALUE_EVENTS, + VALUE_ON_CHANGE, + VALUE_ON_RELEASE, + VALUE_ON_UPDATE, + VALUE_ON_VALUE, get_remapped_uses, is_press_event, ) @@ -41,8 +48,9 @@ from .layout import ( grid_alignments, ) from .lv_validation import lv_color, lv_font, lv_gradient, lv_image, opacity -from .lvcode import LvglComponent, lv_event_t_ptr +from .lvcode import UPDATE_EVENT, LvglComponent, lv_event_t_ptr from .types import ( + LV_EVENT, LVEncoderListener, LvType, lv_group_t, @@ -355,6 +363,19 @@ SET_STATE_SCHEMA = cv.Schema( FLAG_SCHEMA = cv.Schema({cv.Optional(flag): lvalid.lv_bool for flag in df.OBJ_FLAGS}) FLAG_LIST = cv.ensure_list(df.LV_OBJ_FLAG.one_of) +VALUE_TRIGGER_SCHEMA = { + cv.Optional(CONF_TRIGGER, default=CONF_ON_VALUE): cv.one_of( + *LV_VALUE_EVENTS, lower=True + ), +} + +TRIGGER_EVENT_MAP = { + VALUE_ON_CHANGE: (LV_EVENT.VALUE_CHANGED,), + VALUE_ON_UPDATE: (UPDATE_EVENT,), + VALUE_ON_VALUE: (LV_EVENT.VALUE_CHANGED, UPDATE_EVENT), + VALUE_ON_RELEASE: (LV_EVENT.RELEASED,), +} + def part_schema(parts): """ @@ -370,7 +391,7 @@ def part_schema(parts): def automation_schema(typ: LvType): events = df.LV_EVENT_TRIGGERS + df.SWIPE_TRIGGERS if typ.has_on_value: - events = events + (CONF_ON_VALUE,) + events = events + (CONF_ON_VALUE, CONF_ON_UPDATE) args = typ.get_arg_type() def get_trigger_args(event): diff --git a/esphome/components/lvgl/sensor/__init__.py b/esphome/components/lvgl/sensor/__init__.py index 682d84ede3e..e69ea9771a0 100644 --- a/esphome/components/lvgl/sensor/__init__.py +++ b/esphome/components/lvgl/sensor/__init__.py @@ -1,21 +1,16 @@ from esphome.components.sensor import Sensor, new_sensor, sensor_schema import esphome.config_validation as cv -from ..defines import CONF_WIDGET -from ..lvcode import ( - EVENT_ARG, - UPDATE_EVENT, - LambdaContext, - LvContext, - lv_add, - lvgl_static, -) -from ..types import LV_EVENT, LvNumber +from ..defines import CONF_TRIGGER, CONF_WIDGET +from ..lvcode import EVENT_ARG, LambdaContext, LvContext, lv_add, lvgl_static +from ..schemas import TRIGGER_EVENT_MAP, VALUE_TRIGGER_SCHEMA +from ..types import LvNumber from ..widgets import Widget, get_widgets, wait_for_widgets CONFIG_SCHEMA = sensor_schema(Sensor).extend( { cv.Required(CONF_WIDGET): cv.use_id(LvNumber), + **VALUE_TRIGGER_SCHEMA, } ) @@ -33,7 +28,6 @@ async def to_code(config): lvgl_static.add_event_cb( widget.obj, await lamb.get_lambda(), - LV_EVENT.VALUE_CHANGED, - UPDATE_EVENT, + *TRIGGER_EVENT_MAP[config[CONF_TRIGGER]], ) ) diff --git a/esphome/components/lvgl/trigger.py b/esphome/components/lvgl/trigger.py index 64590d56f61..5f524969e28 100644 --- a/esphome/components/lvgl/trigger.py +++ b/esphome/components/lvgl/trigger.py @@ -3,6 +3,7 @@ import esphome.codegen as cg from esphome.const import ( CONF_ID, CONF_ON_BOOT, + CONF_ON_UPDATE, CONF_ON_VALUE, CONF_TRIGGER_ID, CONF_X, @@ -92,6 +93,13 @@ async def generate_triggers(): UPDATE_EVENT, ) + for conf in config.get(CONF_ON_UPDATE, ()): + await add_trigger( + conf, + w, + UPDATE_EVENT, + ) + await add_on_boot_triggers(config.get(CONF_ON_BOOT, ())) diff --git a/esphome/components/lvgl/types.py b/esphome/components/lvgl/types.py index 1872ce2d32a..509d5cc7824 100644 --- a/esphome/components/lvgl/types.py +++ b/esphome/components/lvgl/types.py @@ -3,8 +3,6 @@ from esphome.const import CONF_TEXT, CONF_VALUE from esphome.cpp_generator import MockObj from esphome.cpp_types import Component, esphome_ns -from .defines import lvgl_ns - class LvType(cg.MockObjClass): def __init__(self, *args, **kwargs): @@ -47,6 +45,7 @@ PlainTrigger = esphome_ns.class_("Trigger<>", automation.Trigger.template()) DrawEndTrigger = esphome_ns.class_( "Trigger", automation.Trigger.template(cg.uint32, cg.uint32) ) +lvgl_ns = cg.esphome_ns.namespace("lvgl") IdleTrigger = lvgl_ns.class_("IdleTrigger", automation.Trigger.template()) ObjUpdateAction = lvgl_ns.class_("ObjUpdateAction", automation.Action) LvglCondition = lvgl_ns.class_("LvglCondition", automation.Condition) diff --git a/esphome/components/lvgl/widgets/__init__.py b/esphome/components/lvgl/widgets/__init__.py index 534ebe0b93d..ab1c61ff88f 100644 --- a/esphome/components/lvgl/widgets/__init__.py +++ b/esphome/components/lvgl/widgets/__init__.py @@ -48,6 +48,7 @@ from ..defines import ( join_enums, literal, ) +from ..lv_validation import lv_int from ..lvcode import ( LvConditional, add_line_marks, @@ -207,10 +208,10 @@ class WidgetType: """ return () - def get_max(self, config: dict): + async def get_max(self, config: dict): return sys.maxsize - def get_min(self, config: dict): + async def get_min(self, config: dict): return -sys.maxsize def get_step(self, config: dict): @@ -637,8 +638,8 @@ async def widget_to_code(w_cnfig, w_type: WidgetType | str, parent) -> Widget: class NumberType(WidgetType): - def get_max(self, config: dict): - return int(config.get(CONF_MAX_VALUE, 100)) + async def get_max(self, config: dict): + return await lv_int.process(config.get(CONF_MAX_VALUE, 100)) - def get_min(self, config: dict): - return int(config.get(CONF_MIN_VALUE, 0)) + async def get_min(self, config: dict): + return await lv_int.process(config.get(CONF_MIN_VALUE, 0)) diff --git a/esphome/components/lvgl/widgets/spinbox.py b/esphome/components/lvgl/widgets/spinbox.py index 58e3435c5c7..ab68a76e9cb 100644 --- a/esphome/components/lvgl/widgets/spinbox.py +++ b/esphome/components/lvgl/widgets/spinbox.py @@ -125,10 +125,10 @@ class SpinboxType(WidgetType): def get_uses(self): return CONF_TEXTAREA, CONF_LABEL - def get_max(self, config: dict): + async def get_max(self, config: dict): return config[CONF_RANGE_TO] - def get_min(self, config: dict): + async def get_min(self, config: dict): return config[CONF_RANGE_FROM] def get_step(self, config: dict): diff --git a/tests/components/lvgl/common.yaml b/tests/components/lvgl/common.yaml index 652ae7e7a13..f500002f401 100644 --- a/tests/components/lvgl/common.yaml +++ b/tests/components/lvgl/common.yaml @@ -38,7 +38,7 @@ number: - platform: lvgl widget: slider_id name: LVGL Slider Number - update_on_release: true + trigger: on_release restore_value: true - platform: lvgl widget: lv_arc diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 4bf5b9d494a..53984bb006d 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -309,6 +309,11 @@ lvgl: - logger.log: format: "Roller changed = %d: %s" args: [x, text.c_str()] + on_update: + then: + - logger.log: + format: "Roller updated = %d: %s" + args: [x, text.c_str()] - animimg: height: 60 id: anim_img @@ -630,6 +635,10 @@ lvgl: logger.log: format: "state now %d" args: [x] + on_update: + logger.log: + format: "button updated %d" + args: [x] on_short_click: lvgl.widget.hide: hello_label on_long_press: @@ -758,6 +767,9 @@ lvgl: lambda: return tile == id(tile_1); then: - logger.log: "tile 1 is now showing" + on_update: + then: + - logger.log: "tileview updated programmatically" tiles: - id: tile_1 scroll_snap_y: center @@ -983,6 +995,11 @@ lvgl: - logger.log: format: "Arc value is %f" args: [x] + on_update: + then: + - logger.log: + format: "Arc updated to %f" + args: [x] scroll_on_focus: true value: 75 min_value: 1 @@ -1085,6 +1102,11 @@ lvgl: - logger.log: format: "slider value %f" args: [x] + on_update: + then: + - logger.log: + format: "slider updated to %f" + args: [x] on_click: then: - lvgl.slider.update: @@ -1197,6 +1219,10 @@ lvgl: logger.log: format: "Dropdown changed = %d: %s" args: [x, text.c_str()] + on_update: + logger.log: + format: "Dropdown updated = %d: %s" + args: [x, text.c_str()] on_cancel: logger.log: format: "Dropdown closed = %d" @@ -1437,6 +1463,30 @@ color: blue_int: 64 white_int: 255 +sensor: + - platform: lvgl + widget: lv_arc_1 + id: lvgl_arc1_sensor_on_change + name: LVGL Arc1 Sensor on_change + trigger: on_change + - platform: lvgl + widget: bar_id + id: lvgl_bar_sensor_on_release + name: LVGL Bar Sensor on_release + trigger: on_release + +number: + - platform: lvgl + widget: lv_arc_1 + id: lvgl_arc1_number_on_update + name: LVGL Arc1 Number on_update + trigger: on_update + - platform: lvgl + widget: spinbox_id + id: lvgl_spinbox_number_on_change + name: LVGL Spinbox Number on_change + trigger: on_change + select: - platform: lvgl id: lv_roller_select