[lvgl] Automatically register widget types (#11394)

Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
This commit is contained in:
Stuart Parmenter
2025-11-10 16:04:03 -08:00
committed by GitHub
parent 40e2976ba2
commit 0f8332fe3c
6 changed files with 64 additions and 93 deletions

View File

@@ -1,6 +1,8 @@
import importlib
import logging
import pkgutil
from esphome.automation import build_automation, register_action, validate_automation
from esphome.automation import build_automation, validate_automation
import esphome.codegen as cg
from esphome.components.const import CONF_COLOR_DEPTH, CONF_DRAW_ROUNDING
from esphome.components.display import Display
@@ -25,8 +27,8 @@ from esphome.cpp_generator import MockObj
from esphome.final_validate import full_config
from esphome.helpers import write_file_if_changed
from . import defines as df, helpers, lv_validation as lvalid
from .automation import disp_update, focused_widgets, refreshed_widgets, update_to_code
from . import defines as df, helpers, lv_validation as lvalid, widgets
from .automation import disp_update, focused_widgets, refreshed_widgets
from .defines import add_define
from .encoders import (
ENCODERS_CONFIG,
@@ -45,7 +47,6 @@ from .schemas import (
WIDGET_TYPES,
any_widget_schema,
container_schema,
create_modify_schema,
obj_schema,
)
from .styles import add_top_layer, styles_to_code, theme_to_code
@@ -54,7 +55,6 @@ from .trigger import add_on_boot_triggers, generate_triggers
from .types import (
FontEngine,
IdleTrigger,
ObjUpdateAction,
PlainTrigger,
lv_font_t,
lv_group_t,
@@ -69,33 +69,23 @@ from .widgets import (
set_obj_properties,
styles_used,
)
from .widgets.animimg import animimg_spec
from .widgets.arc import arc_spec
from .widgets.button import button_spec
from .widgets.buttonmatrix import buttonmatrix_spec
from .widgets.canvas import canvas_spec
from .widgets.checkbox import checkbox_spec
from .widgets.container import container_spec
from .widgets.dropdown import dropdown_spec
from .widgets.img import img_spec
from .widgets.keyboard import keyboard_spec
from .widgets.label import label_spec
from .widgets.led import led_spec
from .widgets.line import line_spec
from .widgets.lv_bar import bar_spec
from .widgets.meter import meter_spec
# Import only what we actually use directly in this file
from .widgets.msgbox import MSGBOX_SCHEMA, msgboxes_to_code
from .widgets.obj import obj_spec
from .widgets.page import add_pages, generate_page_triggers, page_spec
from .widgets.qrcode import qr_code_spec
from .widgets.roller import roller_spec
from .widgets.slider import slider_spec
from .widgets.spinbox import spinbox_spec
from .widgets.spinner import spinner_spec
from .widgets.switch import switch_spec
from .widgets.tabview import tabview_spec
from .widgets.textarea import textarea_spec
from .widgets.tileview import tileview_spec
from .widgets.obj import obj_spec # Used in LVGL_SCHEMA
from .widgets.page import ( # page_spec used in LVGL_SCHEMA
add_pages,
generate_page_triggers,
page_spec,
)
# Widget registration happens via WidgetType.__init__ in individual widget files
# The imports below trigger creation of the widget types
# Action registration (lvgl.{widget}.update) happens automatically
# in the WidgetType.__init__ method
for module_info in pkgutil.iter_modules(widgets.__path__):
importlib.import_module(f".widgets.{module_info.name}", package=__package__)
DOMAIN = "lvgl"
DEPENDENCIES = ["display"]
@@ -103,41 +93,6 @@ AUTO_LOAD = ["key_provider"]
CODEOWNERS = ["@clydebarrow"]
LOGGER = logging.getLogger(__name__)
for w_type in (
label_spec,
obj_spec,
button_spec,
bar_spec,
slider_spec,
arc_spec,
line_spec,
spinner_spec,
led_spec,
animimg_spec,
checkbox_spec,
img_spec,
switch_spec,
tabview_spec,
buttonmatrix_spec,
meter_spec,
dropdown_spec,
roller_spec,
textarea_spec,
spinbox_spec,
keyboard_spec,
tileview_spec,
qr_code_spec,
canvas_spec,
container_spec,
):
WIDGET_TYPES[w_type.name] = w_type
for w_type in WIDGET_TYPES.values():
register_action(
f"lvgl.{w_type.name}.update",
ObjUpdateAction,
create_modify_schema(w_type),
)(update_to_code)
SIMPLE_TRIGGERS = (
df.CONF_ON_PAUSE,
@@ -402,6 +357,15 @@ def add_hello_world(config):
return config
def _theme_schema(value):
return cv.Schema(
{
cv.Optional(name): obj_schema(w).extend(FULL_STYLE_SCHEMA)
for name, w in WIDGET_TYPES.items()
}
)(value)
FINAL_VALIDATE_SCHEMA = final_validation
LVGL_SCHEMA = cv.All(
@@ -454,12 +418,7 @@ LVGL_SCHEMA = cv.All(
cv.Optional(
df.CONF_TRANSPARENCY_KEY, default=0x000400
): lvalid.lv_color,
cv.Optional(df.CONF_THEME): cv.Schema(
{
cv.Optional(name): obj_schema(w).extend(FULL_STYLE_SCHEMA)
for name, w in WIDGET_TYPES.items()
}
),
cv.Optional(df.CONF_THEME): _theme_schema,
cv.Optional(df.CONF_GRADIENTS): GRADIENT_SCHEMA,
cv.Optional(df.CONF_TOUCHSCREENS, default=None): touchscreen_schema,
cv.Optional(df.CONF_ENCODERS, default=None): ENCODERS_CONFIG,

View File

@@ -411,6 +411,10 @@ def any_widget_schema(extras=None):
Dynamically generate schemas for all possible LVGL widgets. This is what implements the ability to have a list of any kind of
widget under the widgets: key.
This uses lazy evaluation - the schema is built when called during validation,
not at import time. This allows external components to register widgets
before schema validation begins.
:param extras: Additional schema to be applied to each generated one
:return: A validator for the Widgets key
"""

View File

@@ -1,8 +1,10 @@
import sys
from esphome import automation, codegen as cg
from esphome.automation import register_action
from esphome.config_validation import Schema
from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_TEXT, CONF_VALUE
from esphome.core import EsphomeError
from esphome.cpp_generator import MockObj, MockObjClass
from esphome.cpp_types import esphome_ns
@@ -124,13 +126,16 @@ class WidgetType:
schema=None,
modify_schema=None,
lv_name=None,
is_mock: bool = False,
):
"""
:param name: The widget name, e.g. "bar"
:param w_type: The C type of the widget
:param parts: What parts this widget supports
:param schema: The config schema for defining a widget
:param modify_schema: A schema to update the widget
:param modify_schema: A schema to update the widget, defaults to the same as the schema
:param lv_name: The name of the LVGL widget in the LVGL library, if different from the name
:param is_mock: Whether this widget is a mock widget, i.e. not a real LVGL widget
"""
self.name = name
self.lv_name = lv_name or name
@@ -146,6 +151,22 @@ class WidgetType:
self.modify_schema = modify_schema
self.mock_obj = MockObj(f"lv_{self.lv_name}", "_")
# Local import to avoid circular import
from .automation import update_to_code
from .schemas import WIDGET_TYPES, create_modify_schema
if not is_mock:
if self.name in WIDGET_TYPES:
raise EsphomeError(f"Duplicate definition of widget type '{self.name}'")
WIDGET_TYPES[self.name] = self
# Register the update action automatically
register_action(
f"lvgl.{self.name}.update",
ObjUpdateAction,
create_modify_schema(self),
)(update_to_code)
@property
def animated(self):
return False

View File

@@ -213,17 +213,14 @@ class LvScrActType(WidgetType):
"""
def __init__(self):
super().__init__("lv_scr_act()", lv_obj_t, ())
super().__init__("lv_scr_act()", lv_obj_t, (), is_mock=True)
async def to_code(self, w, config: dict):
return []
lv_scr_act_spec = LvScrActType()
def get_scr_act(lv_comp: MockObj) -> Widget:
return Widget.create(None, lv_comp.get_scr_act(), lv_scr_act_spec, {})
return Widget.create(None, lv_comp.get_scr_act(), LvScrActType(), {})
def get_widget_generator(wid):

View File

@@ -2,7 +2,7 @@ from esphome import automation
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_RANGE_FROM, CONF_RANGE_TO, CONF_STEP, CONF_VALUE
from ..automation import action_to_code, update_to_code
from ..automation import action_to_code
from ..defines import (
CONF_CURSOR,
CONF_DECIMAL_PLACES,
@@ -171,17 +171,3 @@ async def spinbox_decrement(config, action_id, template_arg, args):
lv.spinbox_decrement(w.obj)
return await action_to_code(widgets, do_increment, action_id, template_arg, args)
@automation.register_action(
"lvgl.spinbox.update",
ObjUpdateAction,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(lv_spinbox_t),
cv.Required(CONF_VALUE): lv_float,
}
),
)
async def spinbox_update_to_code(config, action_id, template_arg, args):
return await update_to_code(config, action_id, template_arg, args)

View File

@@ -700,6 +700,10 @@ lvgl:
width: 100%
height: 10%
align: top_mid
on_value:
- lvgl.spinbox.update:
id: spinbox_id
value: !lambda return x;
- button:
styles: spin_button
id: spin_up