[lvgl] Allow setting text directly on a button (#11964)

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Clyde Stubbs
2025-11-27 09:06:40 +10:00
committed by GitHub
parent a2d9941c62
commit 927d3715c1
6 changed files with 115 additions and 26 deletions

View File

@@ -108,7 +108,7 @@ LV_CONF_H_FORMAT = """\
def generate_lv_conf_h():
definitions = [as_macro(m, v) for m, v in df.lv_defines.items()]
definitions = [as_macro(m, v) for m, v in df.get_data(df.KEY_LV_DEFINES).items()]
definitions.sort()
return LV_CONF_H_FORMAT.format("\n".join(definitions))
@@ -140,11 +140,11 @@ def multi_conf_validate(configs: list[dict]):
)
def final_validation(configs):
if len(configs) != 1:
multi_conf_validate(configs)
def final_validation(config_list):
if len(config_list) != 1:
multi_conf_validate(config_list)
global_config = full_config.get()
for config in configs:
for config in config_list:
if (pages := config.get(CONF_PAGES)) and all(p[df.CONF_SKIP] for p in pages):
raise cv.Invalid("At least one page must not be skipped")
for display_id in config[df.CONF_DISPLAYS]:
@@ -190,6 +190,14 @@ def final_validation(configs):
raise cv.Invalid(
f"Widget '{w}' does not have any dynamic properties to refresh",
)
# Do per-widget type final validation for update actions
for widget_type, update_configs in df.get_data(df.KEY_UPDATED_WIDGETS).items():
for conf in update_configs:
for id_conf in conf.get(CONF_ID, ()):
name = id_conf[CONF_ID]
path = global_config.get_path_for_id(name)
widget_conf = global_config.get_config_for_path(path[:-1])
widget_type.final_validate(name, conf, widget_conf, path[1:])
async def to_code(configs):

View File

@@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, Any
from esphome import codegen as cg, config_validation as cv
from esphome.const import CONF_ITEMS
from esphome.core import ID, Lambda
from esphome.core import CORE, ID, Lambda
from esphome.cpp_generator import LambdaExpression, MockObj
from esphome.cpp_types import uint32
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
@@ -20,11 +20,27 @@ from .helpers import requires_component
LOGGER = logging.getLogger(__name__)
lvgl_ns = cg.esphome_ns.namespace("lvgl")
lv_defines = {} # Dict of #defines to provide as build flags
DOMAIN = "lvgl"
KEY_LV_DEFINES = "lv_defines"
KEY_UPDATED_WIDGETS = "updated_widgets"
def get_data(key, default=None):
"""
Get a data structure from the global data store by key
:param key: A key for the data
:param default: The default data - the default is an empty dict
:return:
"""
return CORE.data.setdefault(DOMAIN, {}).setdefault(
key, default if default is not None else {}
)
def add_define(macro, value="1"):
if macro in lv_defines and lv_defines[macro] != value:
lv_defines = get_data(KEY_LV_DEFINES)
value = str(value)
if lv_defines.setdefault(macro, value) != value:
LOGGER.error(
"Redefinition of %s - was %s now %s", macro, lv_defines[macro], value
)

View File

@@ -1,3 +1,5 @@
from collections.abc import Callable
from esphome import config_validation as cv
from esphome.automation import Trigger, validate_automation
from esphome.components.time import RealTimeClock
@@ -311,19 +313,36 @@ def automation_schema(typ: LvType):
}
def base_update_schema(widget_type, parts):
def _update_widget(widget_type: WidgetType) -> Callable[[dict], dict]:
"""
Create a schema for updating a widgets style properties, states and flags
During validation of update actions, create a map of action types to affected widgets
for use in final validation.
:param widget_type:
:return:
"""
def validator(value: dict) -> dict:
df.get_data(df.KEY_UPDATED_WIDGETS).setdefault(widget_type, []).append(value)
return value
return validator
def base_update_schema(widget_type: WidgetType | LvType, parts):
"""
Create a schema for updating a widget's style properties, states and flags.
:param widget_type: The type of the ID
:param parts: The allowable parts to specify
:return:
"""
return part_schema(parts).extend(
w_type = widget_type.w_type if isinstance(widget_type, WidgetType) else widget_type
schema = part_schema(parts).extend(
{
cv.Required(CONF_ID): cv.ensure_list(
cv.maybe_simple_value(
{
cv.Required(CONF_ID): cv.use_id(widget_type),
cv.Required(CONF_ID): cv.use_id(w_type),
},
key=CONF_ID,
)
@@ -332,11 +351,9 @@ def base_update_schema(widget_type, parts):
}
)
def create_modify_schema(widget_type):
return base_update_schema(widget_type.w_type, widget_type.parts).extend(
widget_type.modify_schema
)
if isinstance(widget_type, WidgetType):
schema.add_extra(_update_widget(widget_type))
return schema
def obj_schema(widget_type: WidgetType):

View File

@@ -152,18 +152,18 @@ class WidgetType:
# Local import to avoid circular import
from .automation import update_to_code
from .schemas import WIDGET_TYPES, create_modify_schema
from .schemas import WIDGET_TYPES, base_update_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 the update action automatically, adding widget-specific properties
register_action(
f"lvgl.{self.name}.update",
ObjUpdateAction,
create_modify_schema(self),
base_update_schema(self, self.parts).extend(self.modify_schema),
)(update_to_code)
@property
@@ -182,7 +182,6 @@ class WidgetType:
Generate code for a given widget
:param w: The widget
:param config: Its configuration
:return: Generated code as a list of text lines
"""
async def obj_creator(self, parent: MockObjClass, config: dict):
@@ -228,6 +227,15 @@ class WidgetType:
"""
return value
def final_validate(self, widget, update_config, widget_config, path):
"""
Allow final validation for a given widget type update action
:param widget: A widget
:param update_config: The configuration for the update action
:param widget_config: The configuration for the widget itself
:param path: The path to the widget, for error reporting
"""
class NumberType(WidgetType):
def get_max(self, config: dict):

View File

@@ -1,20 +1,52 @@
from esphome.const import CONF_BUTTON
from esphome import config_validation as cv
from esphome.const import CONF_BUTTON, CONF_TEXT
from esphome.cpp_generator import MockObj
from ..defines import CONF_MAIN
from ..defines import CONF_MAIN, CONF_WIDGETS
from ..helpers import add_lv_use
from ..lv_validation import lv_text
from ..lvcode import lv, lv_expr
from ..schemas import TEXT_SCHEMA
from ..types import LvBoolean, WidgetType
from . import Widget
from .label import label_spec
lv_button_t = LvBoolean("lv_btn_t")
class ButtonType(WidgetType):
def __init__(self):
super().__init__(CONF_BUTTON, lv_button_t, (CONF_MAIN,), lv_name="btn")
super().__init__(
CONF_BUTTON, lv_button_t, (CONF_MAIN,), schema=TEXT_SCHEMA, lv_name="btn"
)
def validate(self, value):
if CONF_TEXT in value:
if CONF_WIDGETS in value:
raise cv.Invalid("Cannot use both text and widgets in a button")
add_lv_use("label")
return value
def get_uses(self):
return ("btn",)
async def to_code(self, w, config):
return []
def on_create(self, var: MockObj, config: dict):
if CONF_TEXT in config:
lv.label_create(var)
return var
async def to_code(self, w: Widget, config):
if text := config.get(CONF_TEXT):
label_widget = Widget.create(
None, lv_expr.obj_get_child(w.obj, 0), label_spec
)
await label_widget.set_property(CONF_TEXT, await lv_text.process(text))
def final_validate(self, widget, update_config, widget_config, path):
if CONF_TEXT in update_config and CONF_TEXT not in widget_config:
raise cv.Invalid(
"Button must have 'text:' configured to allow updating text", path
)
button_spec = ButtonType()

View File

@@ -426,6 +426,14 @@ lvgl:
logger.log: Long pressed repeated
- buttons:
- id: button_e
- button:
id: button_with_text
text: Button
on_click:
lvgl.button.update:
id: button_with_text
text: Clicked
- button:
layout: 2x1
id: button_button