[lvgl] Bug fixes (#7300)

This commit is contained in:
Clyde Stubbs 2024-08-19 08:43:23 +10:00 committed by Jesse Hills
parent 7464b440c0
commit 5c7d070307
No known key found for this signature in database
GPG key ID: BEAAE804EFD8E83A
6 changed files with 173 additions and 66 deletions

View file

@ -6,8 +6,8 @@ Constants already defined in esphome.const are not duplicated here and must be i
from esphome import codegen as cg, config_validation as cv from esphome import codegen as cg, config_validation as cv
from esphome.const import CONF_ITEMS from esphome.const import CONF_ITEMS
from esphome.core import ID, Lambda from esphome.core import Lambda
from esphome.cpp_generator import MockObj from esphome.cpp_generator import LambdaExpression, MockObj
from esphome.cpp_types import uint32 from esphome.cpp_types import uint32
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
@ -22,19 +22,22 @@ def literal(arg):
return arg return arg
def call_lambda(lamb: LambdaExpression):
expr = lamb.content.strip()
if expr.startswith("return") and expr.endswith(";"):
return expr[7:][:-1]
return f"{lamb}()"
class LValidator: class LValidator:
""" """
A validator for a particular type used in LVGL. Usable in configs as a validator, also A validator for a particular type used in LVGL. Usable in configs as a validator, also
has `process()` to convert a value during code generation has `process()` to convert a value during code generation
""" """
def __init__( def __init__(self, validator, rtype, retmapper=None, requires=None):
self, validator, rtype, idtype=None, idexpr=None, retmapper=None, requires=None
):
self.validator = validator self.validator = validator
self.rtype = rtype self.rtype = rtype
self.idtype = idtype
self.idexpr = idexpr
self.retmapper = retmapper self.retmapper = retmapper
self.requires = requires self.requires = requires
@ -43,8 +46,6 @@ class LValidator:
value = requires_component(self.requires)(value) value = requires_component(self.requires)(value)
if isinstance(value, cv.Lambda): if isinstance(value, cv.Lambda):
return cv.returning_lambda(value) return cv.returning_lambda(value)
if self.idtype is not None and isinstance(value, ID):
return cv.use_id(self.idtype)(value)
return self.validator(value) return self.validator(value)
async def process(self, value, args=()): async def process(self, value, args=()):
@ -52,10 +53,10 @@ class LValidator:
return None return None
if isinstance(value, Lambda): if isinstance(value, Lambda):
return cg.RawExpression( return cg.RawExpression(
f"{await cg.process_lambda(value, args, return_type=self.rtype)}()" call_lambda(
await cg.process_lambda(value, args, return_type=self.rtype)
)
) )
if self.idtype is not None and isinstance(value, ID):
return cg.RawExpression(f"{value}->{self.idexpr}")
if self.retmapper is not None: if self.retmapper is not None:
return self.retmapper(value) return self.retmapper(value)
return cg.safe_exp(value) return cg.safe_exp(value)
@ -89,7 +90,7 @@ class LvConstant(LValidator):
cv.ensure_list(self.one_of), uint32, retmapper=self.mapper cv.ensure_list(self.one_of), uint32, retmapper=self.mapper
) )
def mapper(self, value, args=()): def mapper(self, value):
if not isinstance(value, list): if not isinstance(value, list):
value = [value] value = [value]
return literal( return literal(
@ -103,7 +104,7 @@ class LvConstant(LValidator):
def extend(self, *choices): def extend(self, *choices):
""" """
Extend an LVCconstant with additional choices. Extend an LVconstant with additional choices.
:param choices: The extra choices :param choices: The extra choices
:return: A new LVConstant instance :return: A new LVConstant instance
""" """
@ -431,6 +432,8 @@ CONF_ONE_LINE = "one_line"
CONF_ON_SELECT = "on_select" CONF_ON_SELECT = "on_select"
CONF_ONE_CHECKED = "one_checked" CONF_ONE_CHECKED = "one_checked"
CONF_NEXT = "next" CONF_NEXT = "next"
CONF_PAD_ROW = "pad_row"
CONF_PAD_COLUMN = "pad_column"
CONF_PAGE = "page" CONF_PAGE = "page"
CONF_PAGE_WRAP = "page_wrap" CONF_PAGE_WRAP = "page_wrap"
CONF_PASSWORD_MODE = "password_mode" CONF_PASSWORD_MODE = "password_mode"
@ -462,6 +465,7 @@ CONF_SKIP = "skip"
CONF_SYMBOL = "symbol" CONF_SYMBOL = "symbol"
CONF_TAB_ID = "tab_id" CONF_TAB_ID = "tab_id"
CONF_TABS = "tabs" CONF_TABS = "tabs"
CONF_TIME_FORMAT = "time_format"
CONF_TILE = "tile" CONF_TILE = "tile"
CONF_TILE_ID = "tile_id" CONF_TILE_ID = "tile_id"
CONF_TILES = "tiles" CONF_TILES = "tiles"

View file

@ -1,17 +1,14 @@
from typing import Union from typing import Union
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components.binary_sensor import BinarySensor
from esphome.components.color import ColorStruct from esphome.components.color import ColorStruct
from esphome.components.font import Font from esphome.components.font import Font
from esphome.components.image import Image_ from esphome.components.image import Image_
from esphome.components.sensor import Sensor
from esphome.components.text_sensor import TextSensor
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ARGS, CONF_COLOR, CONF_FORMAT, CONF_VALUE from esphome.const import CONF_ARGS, CONF_COLOR, CONF_FORMAT, CONF_TIME, CONF_VALUE
from esphome.core import HexInt from esphome.core import HexInt, Lambda
from esphome.cpp_generator import MockObj from esphome.cpp_generator import MockObj
from esphome.cpp_types import uint32 from esphome.cpp_types import ESPTime, uint32
from esphome.helpers import cpp_string_escape from esphome.helpers import cpp_string_escape
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
@ -19,9 +16,11 @@ from . import types as ty
from .defines import ( from .defines import (
CONF_END_VALUE, CONF_END_VALUE,
CONF_START_VALUE, CONF_START_VALUE,
CONF_TIME_FORMAT,
LV_FONTS, LV_FONTS,
LValidator, LValidator,
LvConstant, LvConstant,
call_lambda,
literal, literal,
) )
from .helpers import ( from .helpers import (
@ -110,13 +109,13 @@ def angle(value):
def size_validator(value): def size_validator(value):
"""A size in one axis - one of "size_content", a number (pixels) or a percentage""" """A size in one axis - one of "size_content", a number (pixels) or a percentage"""
if value == SCHEMA_EXTRACT: if value == SCHEMA_EXTRACT:
return ["size_content", "pixels", "..%"] return ["SIZE_CONTENT", "number of pixels", "percentage"]
if isinstance(value, str) and value.lower().endswith("px"): if isinstance(value, str) and value.lower().endswith("px"):
value = cv.int_(value[:-2]) value = cv.int_(value[:-2])
if isinstance(value, str) and not value.endswith("%"): if isinstance(value, str) and not value.endswith("%"):
if value.upper() == "SIZE_CONTENT": if value.upper() == "SIZE_CONTENT":
return "LV_SIZE_CONTENT" return "LV_SIZE_CONTENT"
raise cv.Invalid("must be 'size_content', a pixel position or a percentage") raise cv.Invalid("must be 'size_content', a percentage or an integer (pixels)")
if isinstance(value, int): if isinstance(value, int):
return cv.int_(value) return cv.int_(value)
# Will throw an exception if not a percentage. # Will throw an exception if not a percentage.
@ -125,6 +124,15 @@ def size_validator(value):
size = LValidator(size_validator, uint32, retmapper=literal) size = LValidator(size_validator, uint32, retmapper=literal)
def pixels_validator(value):
if isinstance(value, str) and value.lower().endswith("px"):
return cv.int_(value[:-2])
return cv.int_(value)
pixels = LValidator(pixels_validator, uint32, retmapper=literal)
radius_consts = LvConstant("LV_RADIUS_", "CIRCLE") radius_consts = LvConstant("LV_RADIUS_", "CIRCLE")
@ -167,9 +175,7 @@ lv_image = LValidator(
retmapper=lambda x: lv_expr.img_from(MockObj(x)), retmapper=lambda x: lv_expr.img_from(MockObj(x)),
requires="image", requires="image",
) )
lv_bool = LValidator( lv_bool = LValidator(cv.boolean, cg.bool_, retmapper=literal)
cv.boolean, cg.bool_, BinarySensor, "get_state()", retmapper=literal
)
def lv_pct(value: Union[int, float]): def lv_pct(value: Union[int, float]):
@ -185,42 +191,60 @@ def lvms_validator_(value):
lv_milliseconds = LValidator( lv_milliseconds = LValidator(
lvms_validator_, lvms_validator_, cg.int32, retmapper=lambda x: x.total_milliseconds
cg.int32,
retmapper=lambda x: x.total_milliseconds,
) )
class TextValidator(LValidator): class TextValidator(LValidator):
def __init__(self): def __init__(self):
super().__init__( super().__init__(cv.string, cg.std_string, lambda s: cg.safe_exp(f"{s}"))
cv.string,
cg.const_char_ptr,
TextSensor,
"get_state().c_str()",
lambda s: cg.safe_exp(f"{s}"),
)
def __call__(self, value): def __call__(self, value):
if isinstance(value, dict): if isinstance(value, dict) and CONF_FORMAT in value:
return value return value
return super().__call__(value) return super().__call__(value)
async def process(self, value, args=()): async def process(self, value, args=()):
if isinstance(value, dict): if isinstance(value, dict):
args = [str(x) for x in value[CONF_ARGS]] if format_str := value.get(CONF_FORMAT):
arg_expr = cg.RawExpression(",".join(args)) args = [str(x) for x in value[CONF_ARGS]]
format_str = cpp_string_escape(value[CONF_FORMAT]) arg_expr = cg.RawExpression(",".join(args))
return literal(f"str_sprintf({format_str}, {arg_expr}).c_str()") format_str = cpp_string_escape(format_str)
return literal(f"str_sprintf({format_str}, {arg_expr}).c_str()")
if time_format := value.get(CONF_TIME_FORMAT):
source = value[CONF_TIME]
if isinstance(source, Lambda):
time_format = cpp_string_escape(time_format)
return cg.RawExpression(
call_lambda(
await cg.process_lambda(source, args, return_type=ESPTime)
)
+ f".strftime({time_format}).c_str()"
)
# must be an ID
source = await cg.get_variable(source)
return source.now().strftime(time_format).c_str()
if isinstance(value, Lambda):
value = call_lambda(
await cg.process_lambda(value, args, return_type=self.rtype)
)
# Was the lambda call reduced to a string?
if value.endswith("c_str()") or (
value.endswith('"') and value.startswith('"')
):
pass
else:
# Either a std::string or a lambda call returning that. We need const char*
value = f"({value}).c_str()"
return cg.RawExpression(value)
return await super().process(value, args) return await super().process(value, args)
lv_text = TextValidator() lv_text = TextValidator()
lv_float = LValidator(cv.float_, cg.float_, Sensor, "get_state()") lv_float = LValidator(cv.float_, cg.float_)
lv_int = LValidator(cv.int_, cg.int_, Sensor, "get_state()") lv_int = LValidator(cv.int_, cg.int_)
lv_brightness = LValidator( lv_brightness = LValidator(cv.percentage, cg.float_, retmapper=lambda x: int(x * 255))
cv.percentage, cg.float_, Sensor, "get_state()", retmapper=lambda x: int(x * 255)
)
def is_lv_font(font): def is_lv_font(font):

View file

@ -1,5 +1,6 @@
from esphome import config_validation as cv from esphome import config_validation as cv
from esphome.automation import Trigger, validate_automation from esphome.automation import Trigger, validate_automation
from esphome.components.time import RealTimeClock
from esphome.const import ( from esphome.const import (
CONF_ARGS, CONF_ARGS,
CONF_FORMAT, CONF_FORMAT,
@ -8,6 +9,7 @@ from esphome.const import (
CONF_ON_VALUE, CONF_ON_VALUE,
CONF_STATE, CONF_STATE,
CONF_TEXT, CONF_TEXT,
CONF_TIME,
CONF_TRIGGER_ID, CONF_TRIGGER_ID,
CONF_TYPE, CONF_TYPE,
) )
@ -15,6 +17,7 @@ from esphome.core import TimePeriod
from esphome.schema_extractors import SCHEMA_EXTRACT from esphome.schema_extractors import SCHEMA_EXTRACT
from . import defines as df, lv_validation as lvalid from . import defines as df, lv_validation as lvalid
from .defines import CONF_TIME_FORMAT
from .helpers import add_lv_use, requires_component, validate_printf from .helpers import add_lv_use, requires_component, validate_printf
from .lv_validation import lv_color, lv_font, lv_image from .lv_validation import lv_color, lv_font, lv_image
from .lvcode import LvglComponent from .lvcode import LvglComponent
@ -46,7 +49,13 @@ TEXT_SCHEMA = cv.Schema(
), ),
validate_printf, validate_printf,
), ),
lvalid.lv_text, cv.Schema(
{
cv.Required(CONF_TIME_FORMAT): cv.string,
cv.GenerateID(CONF_TIME): cv.templatable(cv.use_id(RealTimeClock)),
}
),
cv.templatable(cv.string),
) )
} }
) )
@ -116,15 +125,13 @@ STYLE_PROPS = {
"opa_layered": lvalid.opacity, "opa_layered": lvalid.opacity,
"outline_color": lvalid.lv_color, "outline_color": lvalid.lv_color,
"outline_opa": lvalid.opacity, "outline_opa": lvalid.opacity,
"outline_pad": lvalid.size, "outline_pad": lvalid.pixels,
"outline_width": lvalid.size, "outline_width": lvalid.pixels,
"pad_all": lvalid.size, "pad_all": lvalid.pixels,
"pad_bottom": lvalid.size, "pad_bottom": lvalid.pixels,
"pad_column": lvalid.size, "pad_left": lvalid.pixels,
"pad_left": lvalid.size, "pad_right": lvalid.pixels,
"pad_right": lvalid.size, "pad_top": lvalid.pixels,
"pad_row": lvalid.size,
"pad_top": lvalid.size,
"shadow_color": lvalid.lv_color, "shadow_color": lvalid.lv_color,
"shadow_ofs_x": cv.int_, "shadow_ofs_x": cv.int_,
"shadow_ofs_y": cv.int_, "shadow_ofs_y": cv.int_,
@ -304,6 +311,8 @@ LAYOUT_SCHEMA = {
cv.Required(df.CONF_GRID_COLUMNS): [grid_spec], cv.Required(df.CONF_GRID_COLUMNS): [grid_spec],
cv.Optional(df.CONF_GRID_COLUMN_ALIGN): grid_alignments, cv.Optional(df.CONF_GRID_COLUMN_ALIGN): grid_alignments,
cv.Optional(df.CONF_GRID_ROW_ALIGN): grid_alignments, cv.Optional(df.CONF_GRID_ROW_ALIGN): grid_alignments,
cv.Optional(df.CONF_PAD_ROW): lvalid.pixels,
cv.Optional(df.CONF_PAD_COLUMN): lvalid.pixels,
}, },
df.TYPE_FLEX: { df.TYPE_FLEX: {
cv.Optional( cv.Optional(
@ -312,6 +321,8 @@ LAYOUT_SCHEMA = {
cv.Optional(df.CONF_FLEX_ALIGN_MAIN, default="start"): flex_alignments, cv.Optional(df.CONF_FLEX_ALIGN_MAIN, default="start"): flex_alignments,
cv.Optional(df.CONF_FLEX_ALIGN_CROSS, default="start"): flex_alignments, cv.Optional(df.CONF_FLEX_ALIGN_CROSS, default="start"): flex_alignments,
cv.Optional(df.CONF_FLEX_ALIGN_TRACK, default="start"): flex_alignments, cv.Optional(df.CONF_FLEX_ALIGN_TRACK, default="start"): flex_alignments,
cv.Optional(df.CONF_PAD_ROW): lvalid.pixels,
cv.Optional(df.CONF_PAD_COLUMN): lvalid.pixels,
}, },
}, },
lower=True, lower=True,
@ -338,7 +349,6 @@ DISP_BG_SCHEMA = cv.Schema(
} }
) )
# A style schema that can include text # A style schema that can include text
STYLED_TEXT_SCHEMA = cv.maybe_simple_value( STYLED_TEXT_SCHEMA = cv.maybe_simple_value(
STYLE_SCHEMA.extend(TEXT_SCHEMA), key=CONF_TEXT STYLE_SCHEMA.extend(TEXT_SCHEMA), key=CONF_TEXT

View file

@ -20,6 +20,8 @@ from ..defines import (
CONF_GRID_ROWS, CONF_GRID_ROWS,
CONF_LAYOUT, CONF_LAYOUT,
CONF_MAIN, CONF_MAIN,
CONF_PAD_COLUMN,
CONF_PAD_ROW,
CONF_SCROLLBAR_MODE, CONF_SCROLLBAR_MODE,
CONF_STYLES, CONF_STYLES,
CONF_WIDGETS, CONF_WIDGETS,
@ -29,6 +31,7 @@ from ..defines import (
TYPE_FLEX, TYPE_FLEX,
TYPE_GRID, TYPE_GRID,
LValidator, LValidator,
call_lambda,
join_enums, join_enums,
literal, literal,
) )
@ -273,6 +276,10 @@ async def set_obj_properties(w: Widget, config):
layout_type: str = layout[CONF_TYPE] layout_type: str = layout[CONF_TYPE]
add_lv_use(layout_type) add_lv_use(layout_type)
lv_obj.set_layout(w.obj, literal(f"LV_LAYOUT_{layout_type.upper()}")) lv_obj.set_layout(w.obj, literal(f"LV_LAYOUT_{layout_type.upper()}"))
if (pad_row := layout.get(CONF_PAD_ROW)) is not None:
w.set_style(CONF_PAD_ROW, pad_row, 0)
if (pad_column := layout.get(CONF_PAD_COLUMN)) is not None:
w.set_style(CONF_PAD_COLUMN, pad_column, 0)
if layout_type == TYPE_GRID: if layout_type == TYPE_GRID:
wid = config[CONF_ID] wid = config[CONF_ID]
rows = [str(x) for x in layout[CONF_GRID_ROWS]] rows = [str(x) for x in layout[CONF_GRID_ROWS]]
@ -316,8 +323,13 @@ async def set_obj_properties(w: Widget, config):
flag_clr = set() flag_clr = set()
flag_set = set() flag_set = set()
props = parts[CONF_MAIN][CONF_DEFAULT] props = parts[CONF_MAIN][CONF_DEFAULT]
lambs = {}
flag_set = set()
flag_clr = set()
for prop, value in {k: v for k, v in props.items() if k in OBJ_FLAGS}.items(): for prop, value in {k: v for k, v in props.items() if k in OBJ_FLAGS}.items():
if value: if isinstance(value, cv.Lambda):
lambs[prop] = value
elif value:
flag_set.add(prop) flag_set.add(prop)
else: else:
flag_clr.add(prop) flag_clr.add(prop)
@ -327,6 +339,13 @@ async def set_obj_properties(w: Widget, config):
if flag_clr: if flag_clr:
clrs = join_enums(flag_clr, "LV_OBJ_FLAG_") clrs = join_enums(flag_clr, "LV_OBJ_FLAG_")
w.clear_flag(clrs) w.clear_flag(clrs)
for key, value in lambs.items():
lamb = await cg.process_lambda(value, [], return_type=cg.bool_)
flag = f"LV_OBJ_FLAG_{key.upper()}"
with LvConditional(call_lambda(lamb)) as cond:
w.add_flag(flag)
cond.else_()
w.clear_flag(flag)
if states := config.get(CONF_STATE): if states := config.get(CONF_STATE):
adds = set() adds = set()
@ -348,7 +367,7 @@ async def set_obj_properties(w: Widget, config):
for key, value in lambs.items(): for key, value in lambs.items():
lamb = await cg.process_lambda(value, [], return_type=cg.bool_) lamb = await cg.process_lambda(value, [], return_type=cg.bool_)
state = f"LV_STATE_{key.upper()}" state = f"LV_STATE_{key.upper()}"
with LvConditional(f"{lamb}()") as cond: with LvConditional(call_lambda(lamb)) as cond:
w.add_state(state) w.add_state(state)
cond.else_() cond.else_()
w.clear_state(state) w.clear_state(state)

View file

@ -127,3 +127,11 @@ binary_sensor:
- platform: lvgl - platform: lvgl
name: LVGL checkbox name: LVGL checkbox
widget: checkbox_id widget: checkbox_id
wifi:
ssid: SSID
password: PASSWORD123
time:
platform: sntp
id: time_id

View file

@ -16,8 +16,6 @@ lvgl:
border_width: 0 border_width: 0
radius: 0 radius: 0
pad_all: 0 pad_all: 0
pad_row: 0
pad_column: 0
border_color: 0x0077b3 border_color: 0x0077b3
text_color: 0xFFFFFF text_color: 0xFFFFFF
width: 100% width: 100%
@ -55,6 +53,13 @@ lvgl:
pages: pages:
- id: page1 - id: page1
skip: true skip: true
layout:
type: flex
pad_row: 4
pad_column: 4px
flex_align_main: center
flex_align_cross: start
flex_align_track: end
widgets: widgets:
- animimg: - animimg:
height: 60 height: 60
@ -118,10 +123,8 @@ lvgl:
outline_width: 10px outline_width: 10px
pad_all: 10px pad_all: 10px
pad_bottom: 10px pad_bottom: 10px
pad_column: 10px
pad_left: 10px pad_left: 10px
pad_right: 10px pad_right: 10px
pad_row: 10px
pad_top: 10px pad_top: 10px
shadow_color: light_blue shadow_color: light_blue
shadow_ofs_x: 5 shadow_ofs_x: 5
@ -221,10 +224,47 @@ lvgl:
- label: - label:
text: Button text: Button
on_click: on_click:
lvgl.label.update: - lvgl.label.update:
id: hello_label id: hello_label
bg_color: 0x123456 bg_color: 0x123456
text: clicked text: clicked
- lvgl.label.update:
id: hello_label
text: !lambda return "hello world";
- lvgl.label.update:
id: hello_label
text: !lambda |-
ESP_LOGD("label", "multi-line lambda");
return "hello world";
- lvgl.label.update:
id: hello_label
text: !lambda 'return str_sprintf("Hello space");'
- lvgl.label.update:
id: hello_label
text:
format: "sprintf format %s"
args: ['x ? "checked" : "unchecked"']
- lvgl.label.update:
id: hello_label
text:
time_format: "%c"
- lvgl.label.update:
id: hello_label
text:
time_format: "%c"
time: time_id
- lvgl.label.update:
id: hello_label
text:
time_format: "%c"
time: !lambda return id(time_id).now();
- lvgl.label.update:
id: hello_label
text:
time_format: "%c"
time: !lambda |-
ESP_LOGD("label", "multi-line lambda");
return id(time_id).now();
on_value: on_value:
logger.log: logger.log:
format: "state now %d" format: "state now %d"
@ -396,6 +436,8 @@ lvgl:
grid_row_align: end grid_row_align: end
grid_rows: [25px, fr(1), content] grid_rows: [25px, fr(1), content]
grid_columns: [40, fr(1), fr(1)] grid_columns: [40, fr(1), fr(1)]
pad_row: 6px
pad_column: 0
widgets: widgets:
- image: - image:
grid_cell_row_pos: 0 grid_cell_row_pos: 0