[lvgl] Add new trigger on_update and new number option (#16312)
CI / Create common environment (push) Has been cancelled
CI / Check pylint (push) Has been cancelled
CI / Run script/ci-custom (push) Has been cancelled
CI / Check import esphome.__main__ time (push) Has been cancelled
CI / Test downstream esphome/device-builder (push) Has been cancelled
CI / Run pytest (macOS-latest, 3.11) (push) Has been cancelled
CI / Run pytest (macOS-latest, 3.14) (push) Has been cancelled
CI / Run pytest (ubuntu-latest, 3.11) (push) Has been cancelled
CI / Run pytest (ubuntu-latest, 3.13) (push) Has been cancelled
CI / Run pytest (ubuntu-latest, 3.14) (push) Has been cancelled
CI / Run pytest (windows-latest, 3.11) (push) Has been cancelled
CI / Run pytest (windows-latest, 3.14) (push) Has been cancelled
CI / Determine which jobs to run (push) Has been cancelled
CI / Run integration tests (${{ matrix.bucket.name }}) (push) Has been cancelled
CI / Run C++ unit tests (push) Has been cancelled
CI / Run CodSpeed benchmarks (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 IDF (push) Has been cancelled
CI / Run script/clang-tidy for ESP8266 (push) Has been cancelled
CI / Run script/clang-tidy for ZEPHYR (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 1/4 (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 2/4 (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 3/4 (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 4/4 (push) Has been cancelled
CI / Test components batch (${{ matrix.components }}) (push) Has been cancelled
CI / Test components with native ESP-IDF (push) Has been cancelled
CI / pre-commit.ci lite (push) Has been cancelled
CI / Build target branch for memory impact (push) Has been cancelled
CI / Build PR branch for memory impact (push) Has been cancelled
CI / Comment memory impact (push) Has been cancelled
CI / CI Status (push) Has been cancelled

This commit is contained in:
Clyde Stubbs
2026-05-12 08:52:33 +10:00
committed by GitHub
parent 55ef66cc26
commit 4e31b71304
12 changed files with 135 additions and 48 deletions
+9 -2
View File
@@ -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"
+2 -1
View File
@@ -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()
+24 -13
View File
@@ -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],
)
)
+2 -7
View File
@@ -10,12 +10,8 @@ namespace esphome::lvgl {
class LVGLNumber : public number::Number, public Component {
public:
LVGLNumber(std::function<void(float)> control_lambda, std::function<float()> 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<void(float)> control_lambda, std::function<float()> 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<void(float)> control_lambda_;
std::function<float()> value_lambda_;
lv_event_code_t event_;
bool restore_;
ESPPreferenceObject pref_{};
};
+23 -2
View File
@@ -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):
+6 -12
View File
@@ -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]],
)
)
+8
View File
@@ -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, ()))
+1 -2
View File
@@ -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<uint32_t, uint32_t>", 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)
+7 -6
View File
@@ -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))
+2 -2
View File
@@ -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):
+1 -1
View File
@@ -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
+50
View File
@@ -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