[lvgl] Stage 5 (#7191)

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
Clyde Stubbs 2024-08-06 11:17:29 +10:00 committed by GitHub
parent acaec41bb7
commit 6b141102d6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 1716 additions and 27 deletions

View file

@ -24,10 +24,13 @@ from . import defines as df, helpers, lv_validation as lvalid
from .animimg import animimg_spec from .animimg import animimg_spec
from .arc import arc_spec from .arc import arc_spec
from .automation import disp_update, update_to_code from .automation import disp_update, update_to_code
from .btn import btn_spec from .button import button_spec
from .buttonmatrix import buttonmatrix_spec
from .checkbox import checkbox_spec from .checkbox import checkbox_spec
from .defines import CONF_SKIP from .defines import CONF_SKIP
from .dropdown import dropdown_spec
from .img import img_spec from .img import img_spec
from .keyboard import keyboard_spec
from .label import label_spec from .label import label_spec
from .led import led_spec from .led import led_spec
from .line import line_spec from .line import line_spec
@ -35,8 +38,11 @@ from .lv_bar import bar_spec
from .lv_switch import switch_spec from .lv_switch import switch_spec
from .lv_validation import lv_bool, lv_images_used from .lv_validation import lv_bool, lv_images_used
from .lvcode import LvContext, LvglComponent from .lvcode import LvContext, LvglComponent
from .meter import meter_spec
from .msgbox import MSGBOX_SCHEMA, msgboxes_to_code
from .obj import obj_spec from .obj import obj_spec
from .page import add_pages, page_spec from .page import add_pages, page_spec
from .roller import roller_spec
from .rotary_encoders import ROTARY_ENCODER_CONFIG, rotary_encoders_to_code from .rotary_encoders import ROTARY_ENCODER_CONFIG, rotary_encoders_to_code
from .schemas import ( from .schemas import (
DISP_BG_SCHEMA, DISP_BG_SCHEMA,
@ -52,8 +58,12 @@ from .schemas import (
obj_schema, obj_schema,
) )
from .slider import slider_spec from .slider import slider_spec
from .spinbox import spinbox_spec
from .spinner import spinner_spec from .spinner import spinner_spec
from .styles import add_top_layer, styles_to_code, theme_to_code from .styles import add_top_layer, styles_to_code, theme_to_code
from .tabview import tabview_spec
from .textarea import textarea_spec
from .tileview import tileview_spec
from .touchscreens import touchscreen_schema, touchscreens_to_code from .touchscreens import touchscreen_schema, touchscreens_to_code
from .trigger import generate_triggers from .trigger import generate_triggers
from .types import ( from .types import (
@ -75,7 +85,7 @@ LOGGER = logging.getLogger(__name__)
for w_type in ( for w_type in (
label_spec, label_spec,
obj_spec, obj_spec,
btn_spec, button_spec,
bar_spec, bar_spec,
slider_spec, slider_spec,
arc_spec, arc_spec,
@ -86,6 +96,15 @@ for w_type in (
checkbox_spec, checkbox_spec,
img_spec, img_spec,
switch_spec, switch_spec,
tabview_spec,
buttonmatrix_spec,
meter_spec,
dropdown_spec,
roller_spec,
textarea_spec,
spinbox_spec,
keyboard_spec,
tileview_spec,
): ):
WIDGET_TYPES[w_type.name] = w_type WIDGET_TYPES[w_type.name] = w_type
@ -244,6 +263,7 @@ async def to_code(config):
await add_widgets(lv_scr_act, config) await add_widgets(lv_scr_act, config)
await add_pages(lv_component, config) await add_pages(lv_component, config)
await add_top_layer(config) await add_top_layer(config)
await msgboxes_to_code(config)
await disp_update(f"{lv_component}->get_disp()", config) await disp_update(f"{lv_component}->get_disp()", config)
Widget.set_completed() Widget.set_completed()
await generate_triggers(lv_component) await generate_triggers(lv_component)
@ -308,6 +328,7 @@ CONFIG_SCHEMA = (
cv.Exclusive(CONF_PAGES, CONF_PAGES): cv.ensure_list( cv.Exclusive(CONF_PAGES, CONF_PAGES): cv.ensure_list(
container_schema(page_spec) container_schema(page_spec)
), ),
cv.Optional(df.CONF_MSGBOXES): cv.ensure_list(MSGBOX_SCHEMA),
cv.Optional(df.CONF_PAGE_WRAP, default=True): lv_bool, cv.Optional(df.CONF_PAGE_WRAP, default=True): lv_bool,
cv.Optional(df.CONF_TOP_LAYER): container_schema(obj_spec), cv.Optional(df.CONF_TOP_LAYER): container_schema(obj_spec),
cv.Optional(df.CONF_TRANSPARENCY_KEY, default=0x000400): lvalid.lv_color, cv.Optional(df.CONF_TRANSPARENCY_KEY, default=0x000400): lvalid.lv_color,

View file

@ -2,8 +2,8 @@ from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_DURATION, CONF_ID from esphome.const import CONF_DURATION, CONF_ID
from esphome.cpp_generator import MockObj
from ...cpp_generator import MockObj
from .automation import action_to_code from .automation import action_to_code
from .defines import CONF_AUTO_START, CONF_MAIN, CONF_REPEAT_COUNT, CONF_SRC from .defines import CONF_AUTO_START, CONF_MAIN, CONF_REPEAT_COUNT, CONF_SRC
from .helpers import lvgl_components_required from .helpers import lvgl_components_required

View file

@ -109,7 +109,7 @@ async def disp_update(disp, config: dict):
if CONF_DISP_BG_COLOR not in config and CONF_DISP_BG_IMAGE not in config: if CONF_DISP_BG_COLOR not in config and CONF_DISP_BG_IMAGE not in config:
return return
with LocalVariable("lv_disp_tmp", lv_disp_t, literal(disp)) as disp_temp: with LocalVariable("lv_disp_tmp", lv_disp_t, literal(disp)) as disp_temp:
if bg_color := config.get(CONF_DISP_BG_COLOR): if (bg_color := config.get(CONF_DISP_BG_COLOR)) is not None:
lv.disp_set_bg_color(disp_temp, await lv_color.process(bg_color)) lv.disp_set_bg_color(disp_temp, await lv_color.process(bg_color))
if bg_image := config.get(CONF_DISP_BG_IMAGE): if bg_image := config.get(CONF_DISP_BG_IMAGE):
lv.disp_set_bg_image(disp_temp, await lv_image.process(bg_image)) lv.disp_set_bg_image(disp_temp, await lv_image.process(bg_image))

View file

@ -3,12 +3,12 @@ from esphome.const import CONF_BUTTON
from .defines import CONF_MAIN from .defines import CONF_MAIN
from .types import LvBoolean, WidgetType from .types import LvBoolean, WidgetType
lv_btn_t = LvBoolean("lv_btn_t") lv_button_t = LvBoolean("lv_btn_t")
class BtnType(WidgetType): class ButtonType(WidgetType):
def __init__(self): def __init__(self):
super().__init__(CONF_BUTTON, lv_btn_t, (CONF_MAIN,), lv_name="btn") super().__init__(CONF_BUTTON, lv_button_t, (CONF_MAIN,), lv_name="btn")
def get_uses(self): def get_uses(self):
return ("btn",) return ("btn",)
@ -17,4 +17,4 @@ class BtnType(WidgetType):
return [] return []
btn_spec = BtnType() button_spec = ButtonType()

View file

@ -0,0 +1,277 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components.key_provider import KeyProvider
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_WIDTH
from esphome.cpp_generator import MockObj
from .automation import action_to_code
from .button import lv_button_t
from .defines import (
BUTTONMATRIX_CTRLS,
CONF_BUTTONS,
CONF_CONTROL,
CONF_ITEMS,
CONF_KEY_CODE,
CONF_MAIN,
CONF_ONE_CHECKED,
CONF_ROWS,
CONF_SELECTED,
CONF_TEXT,
)
from .helpers import lvgl_components_required
from .lv_validation import key_code, lv_bool
from .lvcode import lv, lv_add, lv_expr
from .schemas import automation_schema
from .types import (
LV_BTNMATRIX_CTRL,
LV_STATE,
LvBoolean,
LvCompound,
LvType,
ObjUpdateAction,
char_ptr,
lv_pseudo_button_t,
)
from .widget import Widget, WidgetType, get_widgets, widget_map
CONF_BUTTONMATRIX = "buttonmatrix"
CONF_BUTTON_TEXT_LIST_ID = "button_text_list_id"
LvButtonMatrixButton = LvBoolean(
str(cg.uint16),
parents=(lv_pseudo_button_t,),
)
BUTTONMATRIX_BUTTON_SCHEMA = cv.Schema(
{
cv.Optional(CONF_TEXT): cv.string,
cv.Optional(CONF_KEY_CODE): key_code,
cv.GenerateID(): cv.declare_id(LvButtonMatrixButton),
cv.Optional(CONF_WIDTH, default=1): cv.positive_int,
cv.Optional(CONF_CONTROL): cv.ensure_list(
cv.Schema(
{cv.Optional(k.lower()): cv.boolean for k in BUTTONMATRIX_CTRLS.choices}
)
),
}
).extend(automation_schema(lv_button_t))
BUTTONMATRIX_SCHEMA = cv.Schema(
{
cv.Optional(CONF_ONE_CHECKED, default=False): lv_bool,
cv.GenerateID(CONF_BUTTON_TEXT_LIST_ID): cv.declare_id(char_ptr),
cv.Required(CONF_ROWS): cv.ensure_list(
cv.Schema(
{
cv.Required(CONF_BUTTONS): cv.ensure_list(
BUTTONMATRIX_BUTTON_SCHEMA
),
}
)
),
}
)
class ButtonmatrixButtonType(WidgetType):
"""
A pseudo-widget for the matrix buttons
"""
def __init__(self):
super().__init__("btnmatrix_btn", LvButtonMatrixButton, (), {}, {})
async def to_code(self, w, config: dict):
return []
btn_btn_spec = ButtonmatrixButtonType()
class MatrixButton(Widget):
"""
Describes a button within a button matrix.
"""
@staticmethod
def create_button(id, parent, config: dict, index):
w = MatrixButton(id, parent, config, index)
widget_map[id] = w
return w
def __init__(self, id, parent: Widget, config, index):
super().__init__(id, btn_btn_spec, config)
self.parent = parent
self.index = index
self.obj = parent.obj
def is_selected(self):
return self.parent.var.get_selected() == MockObj(self.var)
@staticmethod
def map_ctrls(state):
state = str(state).upper().removeprefix("LV_STATE_")
assert state in BUTTONMATRIX_CTRLS.choices
return getattr(LV_BTNMATRIX_CTRL, state)
def has_state(self, state):
state = self.map_ctrls(state)
return lv_expr.btnmatrix_has_btn_ctrl(self.obj, self.index, state)
def add_state(self, state):
state = self.map_ctrls(state)
return lv.btnmatrix_set_btn_ctrl(self.obj, self.index, state)
def clear_state(self, state):
state = self.map_ctrls(state)
return lv.btnmatrix_clear_btn_ctrl(self.obj, self.index, state)
def is_pressed(self):
return self.is_selected() & self.parent.has_state(LV_STATE.PRESSED)
def is_checked(self):
return self.has_state(LV_STATE.CHECKED)
def get_value(self):
return self.is_checked()
def check_null(self):
return None
async def get_button_data(config, buttonmatrix: Widget):
"""
Process a button matrix button list
:param config: The row list
:param buttonmatrix: The parent variable
:return: text array id, control list, width list
"""
text_list = []
ctrl_list = []
width_list = []
key_list = []
for row in config:
for button_conf in row.get(CONF_BUTTONS) or ():
bid = button_conf[CONF_ID]
index = len(width_list)
MatrixButton.create_button(bid, buttonmatrix, button_conf, index)
cg.new_variable(bid, index)
text_list.append(button_conf.get(CONF_TEXT) or "")
key_list.append(button_conf.get(CONF_KEY_CODE) or 0)
width_list.append(button_conf[CONF_WIDTH])
ctrl = ["LV_BTNMATRIX_CTRL_CLICK_TRIG"]
for item in button_conf.get(CONF_CONTROL, ()):
ctrl.extend([k for k, v in item.items() if v])
ctrl_list.append(await BUTTONMATRIX_CTRLS.process(ctrl))
text_list.append("\n")
text_list = text_list[:-1]
text_list.append(cg.nullptr)
return text_list, ctrl_list, width_list, key_list
lv_buttonmatrix_t = LvType(
"LvButtonMatrixType",
parents=(KeyProvider, LvCompound),
largs=[(cg.uint16, "x")],
lvalue=lambda w: w.var.get_selected(),
)
class ButtonMatrixType(WidgetType):
def __init__(self):
super().__init__(
CONF_BUTTONMATRIX,
lv_buttonmatrix_t,
(CONF_MAIN, CONF_ITEMS),
BUTTONMATRIX_SCHEMA,
{},
lv_name="btnmatrix",
)
async def to_code(self, w: Widget, config):
lvgl_components_required.add("BUTTONMATRIX")
if CONF_ROWS not in config:
return []
text_list, ctrl_list, width_list, key_list = await get_button_data(
config[CONF_ROWS], w
)
text_id = config[CONF_BUTTON_TEXT_LIST_ID]
text_id = cg.static_const_array(text_id, text_list)
lv.btnmatrix_set_map(w.obj, text_id)
set_btn_data(w.obj, ctrl_list, width_list)
lv.btnmatrix_set_one_checked(w.obj, config[CONF_ONE_CHECKED])
for index, key in enumerate(key_list):
if key != 0:
lv_add(w.var.set_key(index, key))
def get_uses(self):
return ("btnmatrix",)
def set_btn_data(obj, ctrl_list, width_list):
for index, ctrl in enumerate(ctrl_list):
lv.btnmatrix_set_btn_ctrl(obj, index, ctrl)
for index, width in enumerate(width_list):
lv.btnmatrix_set_btn_width(obj, index, width)
buttonmatrix_spec = ButtonMatrixType()
@automation.register_action(
"lvgl.matrix.button.update",
ObjUpdateAction,
cv.Schema(
{
cv.Optional(CONF_WIDTH): cv.positive_int,
cv.Optional(CONF_CONTROL): cv.ensure_list(
cv.Schema(
{
cv.Optional(k.lower()): cv.boolean
for k in BUTTONMATRIX_CTRLS.choices
}
),
),
cv.Required(CONF_ID): cv.ensure_list(
cv.maybe_simple_value(
{
cv.Required(CONF_ID): cv.use_id(LvButtonMatrixButton),
},
key=CONF_ID,
)
),
cv.Optional(CONF_SELECTED): lv_bool,
}
),
)
async def button_update_to_code(config, action_id, template_arg, args):
widgets = await get_widgets(config[CONF_ID])
assert all(isinstance(w, MatrixButton) for w in widgets)
async def do_button_update(w: MatrixButton):
if (width := config.get(CONF_WIDTH)) is not None:
lv.btnmatrix_set_btn_width(w.obj, w.index, width)
if config.get(CONF_SELECTED):
lv.btnmatrix_set_selected_btn(w.obj, w.index)
if controls := config.get(CONF_CONTROL):
adds = []
clrs = []
for item in controls:
adds.extend(
[f"LV_BTNMATRIX_CTRL_{k.upper()}" for k, v in item.items() if v]
)
clrs.extend(
[f"LV_BTNMATRIX_CTRL_{k.upper()}" for k, v in item.items() if not v]
)
if adds:
lv.btnmatrix_set_btn_ctrl(
w.obj, w.index, await BUTTONMATRIX_CTRLS.process(adds)
)
if clrs:
lv.btnmatrix_clear_btn_ctrl(
w.obj, w.index, await BUTTONMATRIX_CTRLS.process(clrs)
)
return await action_to_code(
widgets, do_button_update, action_id, template_arg, args
)

View file

@ -18,7 +18,7 @@ class CheckboxType(WidgetType):
) )
async def to_code(self, w: Widget, config): async def to_code(self, w: Widget, config):
if value := config.get(CONF_TEXT): if (value := config.get(CONF_TEXT)) is not None:
lv.checkbox_set_text(w.obj, await lv_text.process(value)) lv.checkbox_set_text(w.obj, await lv_text.process(value))

View file

@ -304,7 +304,7 @@ OBJ_FLAGS = (
ARC_MODES = LvConstant("LV_ARC_MODE_", "NORMAL", "REVERSE", "SYMMETRICAL") ARC_MODES = LvConstant("LV_ARC_MODE_", "NORMAL", "REVERSE", "SYMMETRICAL")
BAR_MODES = LvConstant("LV_BAR_MODE_", "NORMAL", "SYMMETRICAL", "RANGE") BAR_MODES = LvConstant("LV_BAR_MODE_", "NORMAL", "SYMMETRICAL", "RANGE")
BTNMATRIX_CTRLS = LvConstant( BUTTONMATRIX_CTRLS = LvConstant(
"LV_BTNMATRIX_CTRL_", "LV_BTNMATRIX_CTRL_",
"HIDDEN", "HIDDEN",
"NO_REPEAT", "NO_REPEAT",

View file

@ -0,0 +1,76 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_OPTIONS
from .defines import (
CONF_DIR,
CONF_INDICATOR,
CONF_MAIN,
CONF_SELECTED_INDEX,
CONF_SYMBOL,
DIRECTIONS,
literal,
)
from .label import CONF_LABEL
from .lv_validation import lv_int, lv_text, option_string
from .lvcode import LocalVariable, lv, lv_expr
from .schemas import part_schema
from .types import LvSelect, LvType, lv_obj_t
from .widget import Widget, WidgetType, set_obj_properties
CONF_DROPDOWN = "dropdown"
CONF_DROPDOWN_LIST = "dropdown_list"
lv_dropdown_t = LvSelect("lv_dropdown_t")
lv_dropdown_list_t = LvType("lv_dropdown_list_t")
dropdown_list_spec = WidgetType(CONF_DROPDOWN_LIST, lv_dropdown_list_t, (CONF_MAIN,))
DROPDOWN_BASE_SCHEMA = cv.Schema(
{
cv.Optional(CONF_SYMBOL): lv_text,
cv.Optional(CONF_SELECTED_INDEX): cv.templatable(cv.int_),
cv.Optional(CONF_DIR, default="BOTTOM"): DIRECTIONS.one_of,
cv.Optional(CONF_DROPDOWN_LIST): part_schema(dropdown_list_spec),
}
)
DROPDOWN_SCHEMA = DROPDOWN_BASE_SCHEMA.extend(
{
cv.Required(CONF_OPTIONS): cv.ensure_list(option_string),
}
)
class DropdownType(WidgetType):
def __init__(self):
super().__init__(
CONF_DROPDOWN,
lv_dropdown_t,
(CONF_MAIN, CONF_INDICATOR),
DROPDOWN_SCHEMA,
DROPDOWN_BASE_SCHEMA,
)
async def to_code(self, w: Widget, config):
if options := config.get(CONF_OPTIONS):
text = cg.safe_exp("\n".join(options))
lv.dropdown_set_options(w.obj, text)
if symbol := config.get(CONF_SYMBOL):
lv.dropdown_set_symbol(w.obj, await lv_text.process(symbol))
if (selected := config.get(CONF_SELECTED_INDEX)) is not None:
value = await lv_int.process(selected)
lv.dropdown_set_selected(w.obj, value)
if dirn := config.get(CONF_DIR):
lv.dropdown_set_dir(w.obj, literal(dirn))
if dlist := config.get(CONF_DROPDOWN_LIST):
with LocalVariable(
"dropdown_list", lv_obj_t, lv_expr.dropdown_get_list(w.obj)
) as dlist_obj:
dwid = Widget(dlist_obj, dropdown_list_spec, dlist)
await set_obj_properties(dwid, dlist)
def get_uses(self):
return (CONF_LABEL,)
dropdown_spec = DropdownType()

View file

@ -65,16 +65,16 @@ class ImgType(WidgetType):
async def to_code(self, w: Widget, config): async def to_code(self, w: Widget, config):
if src := config.get(CONF_SRC): if src := config.get(CONF_SRC):
lv.img_set_src(w.obj, await lv_image.process(src)) lv.img_set_src(w.obj, await lv_image.process(src))
if cf_angle := config.get(CONF_ANGLE): if (cf_angle := config.get(CONF_ANGLE)) is not None:
pivot_x = config[CONF_PIVOT_X] pivot_x = config[CONF_PIVOT_X]
pivot_y = config[CONF_PIVOT_Y] pivot_y = config[CONF_PIVOT_Y]
lv.img_set_pivot(w.obj, pivot_x, pivot_y) lv.img_set_pivot(w.obj, pivot_x, pivot_y)
lv.img_set_angle(w.obj, cf_angle) lv.img_set_angle(w.obj, cf_angle)
if img_zoom := config.get(CONF_ZOOM): if (img_zoom := config.get(CONF_ZOOM)) is not None:
lv.img_set_zoom(w.obj, img_zoom) lv.img_set_zoom(w.obj, img_zoom)
if offset := config.get(CONF_OFFSET_X): if (offset := config.get(CONF_OFFSET_X)) is not None:
lv.img_set_offset_x(w.obj, offset) lv.img_set_offset_x(w.obj, offset)
if offset := config.get(CONF_OFFSET_Y): if (offset := config.get(CONF_OFFSET_Y)) is not None:
lv.img_set_offset_y(w.obj, offset) lv.img_set_offset_y(w.obj, offset)
if CONF_ANTIALIAS in config: if CONF_ANTIALIAS in config:
lv.img_set_antialias(w.obj, config[CONF_ANTIALIAS]) lv.img_set_antialias(w.obj, config[CONF_ANTIALIAS])

View file

@ -0,0 +1,49 @@
from esphome.components.key_provider import KeyProvider
import esphome.config_validation as cv
from esphome.const import CONF_MODE
from esphome.cpp_types import std_string
from .defines import CONF_ITEMS, CONF_MAIN, KEYBOARD_MODES, literal
from .helpers import add_lv_use, lvgl_components_required
from .textarea import CONF_TEXTAREA, lv_textarea_t
from .types import LvCompound, LvType
from .widget import Widget, WidgetType, get_widgets
CONF_KEYBOARD = "keyboard"
KEYBOARD_SCHEMA = {
cv.Optional(CONF_MODE, default="TEXT_UPPER"): KEYBOARD_MODES.one_of,
cv.Optional(CONF_TEXTAREA): cv.use_id(lv_textarea_t),
}
lv_keyboard_t = LvType(
"LvKeyboardType",
parents=(KeyProvider, LvCompound),
largs=[(std_string, "text")],
has_on_value=True,
lvalue=lambda w: literal(f"lv_textarea_get_text({w.obj})"),
)
class KeyboardType(WidgetType):
def __init__(self):
super().__init__(
CONF_KEYBOARD,
lv_keyboard_t,
(CONF_MAIN, CONF_ITEMS),
KEYBOARD_SCHEMA,
)
def get_uses(self):
return CONF_KEYBOARD, CONF_TEXTAREA
async def to_code(self, w: Widget, config: dict):
lvgl_components_required.add("KEY_LISTENER")
lvgl_components_required.add(CONF_KEYBOARD)
add_lv_use("btnmatrix")
await w.set_property(CONF_MODE, await KEYBOARD_MODES.process(config[CONF_MODE]))
if ta := await get_widgets(config, CONF_TEXTAREA):
await w.set_property(CONF_TEXTAREA, ta[0].obj)
keyboard_spec = KeyboardType()

View file

@ -20,9 +20,9 @@ class LedType(WidgetType):
super().__init__(CONF_LED, LvType("lv_led_t"), (CONF_MAIN,), LED_SCHEMA) super().__init__(CONF_LED, LvType("lv_led_t"), (CONF_MAIN,), LED_SCHEMA)
async def to_code(self, w: Widget, config): async def to_code(self, w: Widget, config):
if color := config.get(CONF_COLOR): if (color := config.get(CONF_COLOR)) is not None:
lv.led_set_color(w.obj, await lv_color.process(color)) lv.led_set_color(w.obj, await lv_color.process(color))
if brightness := config.get(CONF_BRIGHTNESS): if (brightness := config.get(CONF_BRIGHTNESS)) is not None:
lv.led_set_brightness(w.obj, await lv_brightness.process(brightness)) lv.led_set_brightness(w.obj, await lv_brightness.process(brightness))

View file

@ -146,12 +146,12 @@ LVEncoderListener::LVEncoderListener(lv_indev_type_t type, uint16_t lpt, uint16_
#endif // USE_LVGL_ROTARY_ENCODER #endif // USE_LVGL_ROTARY_ENCODER
#ifdef USE_LVGL_BUTTONMATRIX #ifdef USE_LVGL_BUTTONMATRIX
void LvBtnmatrixType::set_obj(lv_obj_t *lv_obj) { void LvButtonMatrixType::set_obj(lv_obj_t *lv_obj) {
LvCompound::set_obj(lv_obj); LvCompound::set_obj(lv_obj);
lv_obj_add_event_cb( lv_obj_add_event_cb(
lv_obj, lv_obj,
[](lv_event_t *event) { [](lv_event_t *event) {
auto *self = static_cast<LvBtnmatrixType *>(event->user_data); auto *self = static_cast<LvButtonMatrixType *>(event->user_data);
if (self->key_callback_.size() == 0) if (self->key_callback_.size() == 0)
return; return;
auto key_idx = lv_btnmatrix_get_selected_btn(self->obj); auto key_idx = lv_btnmatrix_get_selected_btn(self->obj);

View file

@ -246,7 +246,7 @@ class LVEncoderListener : public Parented<LvglComponent> {
}; };
#endif // USE_LVGL_ROTARY_ENCODER #endif // USE_LVGL_ROTARY_ENCODER
#ifdef USE_LVGL_BUTTONMATRIX #ifdef USE_LVGL_BUTTONMATRIX
class LvBtnmatrixType : public key_provider::KeyProvider, public LvCompound { class LvButtonMatrixType : public key_provider::KeyProvider, public LvCompound {
public: public:
void set_obj(lv_obj_t *lv_obj) override; void set_obj(lv_obj_t *lv_obj) override;
uint16_t get_selected() { return lv_btnmatrix_get_selected_btn(this->obj); } uint16_t get_selected() { return lv_btnmatrix_get_selected_btn(this->obj); }

View file

@ -0,0 +1,302 @@
from esphome import automation
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import (
CONF_COLOR,
CONF_COUNT,
CONF_ID,
CONF_LENGTH,
CONF_LOCAL,
CONF_RANGE_FROM,
CONF_RANGE_TO,
CONF_ROTATION,
CONF_VALUE,
CONF_WIDTH,
)
from .arc import CONF_ARC
from .automation import action_to_code
from .defines import (
CONF_END_VALUE,
CONF_MAIN,
CONF_PIVOT_X,
CONF_PIVOT_Y,
CONF_SRC,
CONF_START_VALUE,
CONF_TICKS,
)
from .helpers import add_lv_use
from .img import CONF_IMAGE
from .line import CONF_LINE
from .lv_validation import (
angle,
get_end_value,
get_start_value,
lv_bool,
lv_color,
lv_float,
lv_image,
requires_component,
size,
)
from .lvcode import LocalVariable, lv, lv_assign, lv_expr
from .obj import obj_spec
from .types import LvType, ObjUpdateAction
from .widget import Widget, WidgetType, get_widgets
CONF_ANGLE_RANGE = "angle_range"
CONF_COLOR_END = "color_end"
CONF_COLOR_START = "color_start"
CONF_INDICATORS = "indicators"
CONF_LABEL_GAP = "label_gap"
CONF_MAJOR = "major"
CONF_METER = "meter"
CONF_R_MOD = "r_mod"
CONF_SCALES = "scales"
CONF_STRIDE = "stride"
CONF_TICK_STYLE = "tick_style"
lv_meter_t = LvType("lv_meter_t")
lv_meter_indicator_t = cg.global_ns.struct("lv_meter_indicator_t")
lv_meter_indicator_t_ptr = lv_meter_indicator_t.operator("ptr")
def pixels(value):
"""A size in one axis in pixels"""
if isinstance(value, str) and value.lower().endswith("px"):
return cv.int_(value[:-2])
return cv.int_(value)
INDICATOR_LINE_SCHEMA = cv.Schema(
{
cv.Optional(CONF_WIDTH, default=4): size,
cv.Optional(CONF_COLOR, default=0): lv_color,
cv.Optional(CONF_R_MOD, default=0): size,
cv.Optional(CONF_VALUE): lv_float,
}
)
INDICATOR_IMG_SCHEMA = cv.Schema(
{
cv.Required(CONF_SRC): lv_image,
cv.Required(CONF_PIVOT_X): pixels,
cv.Required(CONF_PIVOT_Y): pixels,
cv.Optional(CONF_VALUE): lv_float,
}
)
INDICATOR_ARC_SCHEMA = cv.Schema(
{
cv.Optional(CONF_WIDTH, default=4): size,
cv.Optional(CONF_COLOR, default=0): lv_color,
cv.Optional(CONF_R_MOD, default=0): size,
cv.Exclusive(CONF_VALUE, CONF_VALUE): lv_float,
cv.Exclusive(CONF_START_VALUE, CONF_VALUE): lv_float,
cv.Optional(CONF_END_VALUE): lv_float,
}
)
INDICATOR_TICKS_SCHEMA = cv.Schema(
{
cv.Optional(CONF_WIDTH, default=4): size,
cv.Optional(CONF_COLOR_START, default=0): lv_color,
cv.Optional(CONF_COLOR_END): lv_color,
cv.Exclusive(CONF_VALUE, CONF_VALUE): lv_float,
cv.Exclusive(CONF_START_VALUE, CONF_VALUE): lv_float,
cv.Optional(CONF_END_VALUE): lv_float,
cv.Optional(CONF_LOCAL, default=False): lv_bool,
}
)
INDICATOR_SCHEMA = cv.Schema(
{
cv.Exclusive(CONF_LINE, CONF_INDICATORS): INDICATOR_LINE_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(lv_meter_indicator_t),
}
),
cv.Exclusive(CONF_IMAGE, CONF_INDICATORS): cv.All(
INDICATOR_IMG_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(lv_meter_indicator_t),
}
),
requires_component("image"),
),
cv.Exclusive(CONF_ARC, CONF_INDICATORS): INDICATOR_ARC_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(lv_meter_indicator_t),
}
),
cv.Exclusive(CONF_TICK_STYLE, CONF_INDICATORS): INDICATOR_TICKS_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(lv_meter_indicator_t),
}
),
}
)
SCALE_SCHEMA = cv.Schema(
{
cv.Optional(CONF_TICKS): cv.Schema(
{
cv.Optional(CONF_COUNT, default=12): cv.positive_int,
cv.Optional(CONF_WIDTH, default=2): size,
cv.Optional(CONF_LENGTH, default=10): size,
cv.Optional(CONF_COLOR, default=0x808080): lv_color,
cv.Optional(CONF_MAJOR): cv.Schema(
{
cv.Optional(CONF_STRIDE, default=3): cv.positive_int,
cv.Optional(CONF_WIDTH, default=5): size,
cv.Optional(CONF_LENGTH, default="15%"): size,
cv.Optional(CONF_COLOR, default=0): lv_color,
cv.Optional(CONF_LABEL_GAP, default=4): size,
}
),
}
),
cv.Optional(CONF_RANGE_FROM, default=0.0): cv.float_,
cv.Optional(CONF_RANGE_TO, default=100.0): cv.float_,
cv.Optional(CONF_ANGLE_RANGE, default=270): cv.int_range(0, 360),
cv.Optional(CONF_ROTATION): angle,
cv.Optional(CONF_INDICATORS): cv.ensure_list(INDICATOR_SCHEMA),
}
)
METER_SCHEMA = {cv.Optional(CONF_SCALES): cv.ensure_list(SCALE_SCHEMA)}
class MeterType(WidgetType):
def __init__(self):
super().__init__(CONF_METER, lv_meter_t, (CONF_MAIN,), METER_SCHEMA)
async def to_code(self, w: Widget, config):
"""For a meter object, create and set parameters"""
var = w.obj
for scale_conf in config.get(CONF_SCALES) or ():
rotation = 90 + (360 - scale_conf[CONF_ANGLE_RANGE]) / 2
if CONF_ROTATION in scale_conf:
rotation = scale_conf[CONF_ROTATION] // 10
with LocalVariable(
"meter_var", "lv_meter_scale_t", lv_expr.meter_add_scale(var)
) as meter_var:
lv.meter_set_scale_range(
var,
meter_var,
scale_conf[CONF_RANGE_FROM],
scale_conf[CONF_RANGE_TO],
scale_conf[CONF_ANGLE_RANGE],
rotation,
)
if ticks := scale_conf.get(CONF_TICKS):
color = await lv_color.process(ticks[CONF_COLOR])
lv.meter_set_scale_ticks(
var,
meter_var,
ticks[CONF_COUNT],
ticks[CONF_WIDTH],
ticks[CONF_LENGTH],
color,
)
if CONF_MAJOR in ticks:
major = ticks[CONF_MAJOR]
color = await lv_color.process(major[CONF_COLOR])
lv.meter_set_scale_major_ticks(
var,
meter_var,
major[CONF_STRIDE],
major[CONF_WIDTH],
major[CONF_LENGTH],
color,
major[CONF_LABEL_GAP],
)
for indicator in scale_conf.get(CONF_INDICATORS) or ():
(t, v) = next(iter(indicator.items()))
iid = v[CONF_ID]
ivar = cg.new_variable(
iid, cg.nullptr, type_=lv_meter_indicator_t_ptr
)
# Enable getting the meter to which this belongs.
wid = Widget.create(iid, var, obj_spec, v)
wid.obj = ivar
if t == CONF_LINE:
color = await lv_color.process(v[CONF_COLOR])
lv_assign(
ivar,
lv_expr.meter_add_needle_line(
var, meter_var, v[CONF_WIDTH], color, v[CONF_R_MOD]
),
)
if t == CONF_ARC:
color = await lv_color.process(v[CONF_COLOR])
lv_assign(
ivar,
lv_expr.meter_add_arc(
var, meter_var, v[CONF_WIDTH], color, v[CONF_R_MOD]
),
)
if t == CONF_TICK_STYLE:
color_start = await lv_color.process(v[CONF_COLOR_START])
color_end = await lv_color.process(
v.get(CONF_COLOR_END) or color_start
)
lv_assign(
ivar,
lv_expr.meter_add_scale_lines(
var,
meter_var,
color_start,
color_end,
v[CONF_LOCAL],
v[CONF_WIDTH],
),
)
if t == CONF_IMAGE:
add_lv_use("img")
lv_assign(
ivar,
lv_expr.meter_add_needle_img(
var,
meter_var,
await lv_image.process(v[CONF_SRC]),
v[CONF_PIVOT_X],
v[CONF_PIVOT_Y],
),
)
start_value = await get_start_value(v)
end_value = await get_end_value(v)
set_indicator_values(var, ivar, start_value, end_value)
meter_spec = MeterType()
@automation.register_action(
"lvgl.indicator.update",
ObjUpdateAction,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(lv_meter_indicator_t),
cv.Exclusive(CONF_VALUE, CONF_VALUE): lv_float,
cv.Exclusive(CONF_START_VALUE, CONF_VALUE): lv_float,
cv.Optional(CONF_END_VALUE): lv_float,
}
),
)
async def indicator_update_to_code(config, action_id, template_arg, args):
widget = await get_widgets(config)
start_value = await get_start_value(config)
end_value = await get_end_value(config)
async def set_value(w: Widget):
set_indicator_values(w.var, w.obj, start_value, end_value)
return await action_to_code(widget, set_value, action_id, template_arg, args)
def set_indicator_values(meter, indicator, start_value, end_value):
if start_value is not None:
if end_value is None:
lv.meter_set_indicator_value(meter, indicator, start_value)
else:
lv.meter_set_indicator_start_value(meter, indicator, start_value)
if end_value is not None:
lv.meter_set_indicator_end_value(meter, indicator, end_value)

View file

@ -0,0 +1,127 @@
from esphome import config_validation as cv
from esphome.const import CONF_BUTTON, CONF_ID
from esphome.core import ID
from esphome.cpp_generator import new_Pvariable, static_const_array
from esphome.cpp_types import nullptr
from .button import button_spec
from .buttonmatrix import (
BUTTONMATRIX_BUTTON_SCHEMA,
CONF_BUTTON_TEXT_LIST_ID,
buttonmatrix_spec,
get_button_data,
lv_buttonmatrix_t,
set_btn_data,
)
from .defines import (
CONF_BODY,
CONF_BUTTONS,
CONF_CLOSE_BUTTON,
CONF_MSGBOXES,
CONF_TEXT,
CONF_TITLE,
TYPE_FLEX,
literal,
)
from .helpers import add_lv_use
from .label import CONF_LABEL
from .lv_validation import lv_bool, lv_pct, lv_text
from .lvcode import (
EVENT_ARG,
LambdaContext,
LocalVariable,
lv_add,
lv_assign,
lv_expr,
lv_obj,
lv_Pvariable,
)
from .obj import obj_spec
from .schemas import STYLE_SCHEMA, STYLED_TEXT_SCHEMA, container_schema
from .styles import TOP_LAYER
from .types import LV_EVENT, char_ptr, lv_obj_t
from .widget import Widget, set_obj_properties
CONF_MSGBOX = "msgbox"
MSGBOX_SCHEMA = container_schema(
obj_spec,
STYLE_SCHEMA.extend(
{
cv.GenerateID(CONF_ID): cv.declare_id(lv_obj_t),
cv.Required(CONF_TITLE): STYLED_TEXT_SCHEMA,
cv.Optional(CONF_BODY): STYLED_TEXT_SCHEMA,
cv.Optional(CONF_BUTTONS): cv.ensure_list(BUTTONMATRIX_BUTTON_SCHEMA),
cv.Optional(CONF_CLOSE_BUTTON): lv_bool,
cv.GenerateID(CONF_BUTTON_TEXT_LIST_ID): cv.declare_id(char_ptr),
}
),
)
async def msgbox_to_code(conf):
"""
Construct a message box. This consists of a full-screen translucent background enclosing a centered container
with an optional title, body, close button and a button matrix. And any other widgets the user cares to add
:param conf: The config data
:return: code to add to the init lambda
"""
add_lv_use(
TYPE_FLEX,
CONF_BUTTON,
CONF_LABEL,
CONF_MSGBOX,
*buttonmatrix_spec.get_uses(),
*button_spec.get_uses(),
)
mbid = conf[CONF_ID]
outer = lv_Pvariable(lv_obj_t, mbid.id)
btnm = new_Pvariable(
ID(f"{mbid.id}_btnm_", is_declaration=True, type=lv_buttonmatrix_t)
)
msgbox = lv_Pvariable(lv_obj_t, f"{mbid.id}_msgbox")
outer_w = Widget.create(mbid, outer, obj_spec, conf)
btnm_widg = Widget.create(str(btnm), btnm, buttonmatrix_spec, conf)
text_list, ctrl_list, width_list, _ = await get_button_data((conf,), btnm_widg)
text_id = conf[CONF_BUTTON_TEXT_LIST_ID]
text_list = static_const_array(text_id, text_list)
if (text := conf.get(CONF_BODY)) is not None:
text = await lv_text.process(text.get(CONF_TEXT))
if (title := conf.get(CONF_TITLE)) is not None:
title = await lv_text.process(title.get(CONF_TEXT))
close_button = conf[CONF_CLOSE_BUTTON]
lv_assign(outer, lv_expr.obj_create(TOP_LAYER))
lv_obj.set_width(outer, lv_pct(100))
lv_obj.set_height(outer, lv_pct(100))
lv_obj.set_style_bg_opa(outer, 128, 0)
lv_obj.set_style_bg_color(outer, literal("lv_color_black()"), 0)
lv_obj.set_style_border_width(outer, 0, 0)
lv_obj.set_style_pad_all(outer, 0, 0)
lv_obj.set_style_radius(outer, 0, 0)
outer_w.add_flag("LV_OBJ_FLAG_HIDDEN")
lv_assign(
msgbox, lv_expr.msgbox_create(outer, title, text, text_list, close_button)
)
lv_obj.set_style_align(msgbox, literal("LV_ALIGN_CENTER"), 0)
lv_add(btnm.set_obj(lv_expr.msgbox_get_btns(msgbox)))
await set_obj_properties(outer_w, conf)
if close_button:
async with LambdaContext(EVENT_ARG, where=mbid) as context:
outer_w.add_flag("LV_OBJ_FLAG_HIDDEN")
with LocalVariable(
"close_btn_", lv_obj_t, lv_expr.msgbox_get_close_btn(msgbox)
) as close_btn:
lv_obj.remove_event_cb(close_btn, nullptr)
lv_obj.add_event_cb(
close_btn,
await context.get_lambda(),
LV_EVENT.CLICKED,
nullptr,
)
if len(ctrl_list) != 0 or len(width_list) != 0:
set_btn_data(btnm.obj, ctrl_list, width_list)
async def msgboxes_to_code(config):
for conf in config.get(CONF_MSGBOXES, ()):
await msgbox_to_code(conf)

View file

@ -0,0 +1,77 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_MODE, CONF_OPTIONS
from .defines import (
CONF_ANIMATED,
CONF_MAIN,
CONF_SELECTED,
CONF_SELECTED_INDEX,
CONF_VISIBLE_ROW_COUNT,
ROLLER_MODES,
literal,
)
from .label import CONF_LABEL
from .lv_validation import animated, lv_int, option_string
from .lvcode import lv
from .types import LvSelect
from .widget import WidgetType
CONF_ROLLER = "roller"
lv_roller_t = LvSelect("lv_roller_t")
ROLLER_BASE_SCHEMA = cv.Schema(
{
cv.Optional(CONF_SELECTED_INDEX): cv.templatable(cv.int_),
cv.Optional(CONF_VISIBLE_ROW_COUNT): lv_int,
}
)
ROLLER_SCHEMA = ROLLER_BASE_SCHEMA.extend(
{
cv.Required(CONF_OPTIONS): cv.ensure_list(option_string),
cv.Optional(CONF_MODE, default="NORMAL"): ROLLER_MODES.one_of,
}
)
ROLLER_MODIFY_SCHEMA = ROLLER_BASE_SCHEMA.extend(
{
cv.Optional(CONF_ANIMATED, default=True): animated,
}
)
class RollerType(WidgetType):
def __init__(self):
super().__init__(
CONF_ROLLER,
lv_roller_t,
(CONF_MAIN, CONF_SELECTED),
ROLLER_SCHEMA,
ROLLER_MODIFY_SCHEMA,
)
async def to_code(self, w, config):
if options := config.get(CONF_OPTIONS):
mode = await ROLLER_MODES.process(config[CONF_MODE])
text = cg.safe_exp("\n".join(options))
lv.roller_set_options(w.obj, text, mode)
animopt = literal(config.get(CONF_ANIMATED) or "LV_ANIM_OFF")
if CONF_SELECTED_INDEX in config:
if selected := config[CONF_SELECTED_INDEX]:
value = await lv_int.process(selected)
lv.roller_set_selected(w.obj, value, animopt)
await w.set_property(
CONF_VISIBLE_ROW_COUNT,
await lv_int.process(config.get(CONF_VISIBLE_ROW_COUNT)),
)
@property
def animated(self):
return True
def get_uses(self):
return (CONF_LABEL,)
roller_spec = RollerType()

View file

@ -0,0 +1,178 @@
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 .defines import (
CONF_CURSOR,
CONF_DECIMAL_PLACES,
CONF_DIGITS,
CONF_MAIN,
CONF_ROLLOVER,
CONF_SCROLLBAR,
CONF_SELECTED,
CONF_TEXTAREA_PLACEHOLDER,
)
from .label import CONF_LABEL
from .lv_validation import lv_bool, lv_float
from .lvcode import lv
from .textarea import CONF_TEXTAREA
from .types import LvNumber, ObjUpdateAction
from .widget import Widget, WidgetType, get_widgets
CONF_SPINBOX = "spinbox"
lv_spinbox_t = LvNumber("lv_spinbox_t")
SPIN_ACTIONS = (
"INCREMENT",
"DECREMENT",
"STEP_NEXT",
"STEP_PREV",
"CLEAR",
)
def validate_spinbox(config):
max_val = 2**31 - 1
min_val = -1 - max_val
range_from = int(config[CONF_RANGE_FROM])
range_to = int(config[CONF_RANGE_TO])
step = int(config[CONF_STEP])
if (
range_from > max_val
or range_from < min_val
or range_to > max_val
or range_to < min_val
):
raise cv.Invalid("Range outside allowed limits")
if step <= 0 or step >= (range_to - range_from) / 2:
raise cv.Invalid("Invalid step value")
if config[CONF_DIGITS] <= config[CONF_DECIMAL_PLACES]:
raise cv.Invalid("Number of digits must exceed number of decimal places")
return config
SPINBOX_SCHEMA = cv.Schema(
{
cv.Optional(CONF_VALUE): lv_float,
cv.Optional(CONF_RANGE_FROM, default=0): cv.float_,
cv.Optional(CONF_RANGE_TO, default=100): cv.float_,
cv.Optional(CONF_DIGITS, default=4): cv.int_range(1, 10),
cv.Optional(CONF_STEP, default=1.0): cv.positive_float,
cv.Optional(CONF_DECIMAL_PLACES, default=0): cv.int_range(0, 6),
cv.Optional(CONF_ROLLOVER, default=False): lv_bool,
}
).add_extra(validate_spinbox)
SPINBOX_MODIFY_SCHEMA = {
cv.Required(CONF_VALUE): lv_float,
}
class SpinboxType(WidgetType):
def __init__(self):
super().__init__(
CONF_SPINBOX,
lv_spinbox_t,
(
CONF_MAIN,
CONF_SCROLLBAR,
CONF_SELECTED,
CONF_CURSOR,
CONF_TEXTAREA_PLACEHOLDER,
),
SPINBOX_SCHEMA,
SPINBOX_MODIFY_SCHEMA,
)
async def to_code(self, w: Widget, config):
if CONF_DIGITS in config:
digits = config[CONF_DIGITS]
scale = 10 ** config[CONF_DECIMAL_PLACES]
range_from = int(config[CONF_RANGE_FROM]) * scale
range_to = int(config[CONF_RANGE_TO]) * scale
step = int(config[CONF_STEP]) * scale
w.scale = scale
w.step = step
w.range_to = range_to
w.range_from = range_from
lv.spinbox_set_range(w.obj, range_from, range_to)
await w.set_property(CONF_STEP, step)
await w.set_property(CONF_ROLLOVER, config)
lv.spinbox_set_digit_format(
w.obj, digits, digits - config[CONF_DECIMAL_PLACES]
)
if (value := config.get(CONF_VALUE)) is not None:
lv.spinbox_set_value(w.obj, await lv_float.process(value))
def get_scale(self, config):
return 10 ** config[CONF_DECIMAL_PLACES]
def get_uses(self):
return CONF_TEXTAREA, CONF_LABEL
def get_max(self, config: dict):
return config[CONF_RANGE_TO]
def get_min(self, config: dict):
return config[CONF_RANGE_FROM]
def get_step(self, config: dict):
return config[CONF_STEP]
spinbox_spec = SpinboxType()
@automation.register_action(
"lvgl.spinbox.increment",
ObjUpdateAction,
cv.maybe_simple_value(
{
cv.Required(CONF_ID): cv.use_id(lv_spinbox_t),
},
key=CONF_ID,
),
)
async def spinbox_increment(config, action_id, template_arg, args):
widgets = await get_widgets(config)
async def do_increment(w: Widget):
lv.spinbox_increment(w.obj)
return await action_to_code(widgets, do_increment, action_id, template_arg, args)
@automation.register_action(
"lvgl.spinbox.decrement",
ObjUpdateAction,
cv.maybe_simple_value(
{
cv.Required(CONF_ID): cv.use_id(lv_spinbox_t),
},
key=CONF_ID,
),
)
async def spinbox_decrement(config, action_id, template_arg, args):
widgets = await get_widgets(config)
async def do_increment(w: Widget):
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

@ -26,7 +26,7 @@ async def styles_to_code(config):
svar = cg.new_Pvariable(style[CONF_ID]) svar = cg.new_Pvariable(style[CONF_ID])
lv.style_init(svar) lv.style_init(svar)
for prop, validator in ALL_STYLES.items(): for prop, validator in ALL_STYLES.items():
if value := style.get(prop): if (value := style.get(prop)) is not None:
if isinstance(validator, LValidator): if isinstance(validator, LValidator):
value = await validator.process(value) value = await validator.process(value)
if isinstance(value, list): if isinstance(value, list):

View file

@ -0,0 +1,114 @@
from esphome import automation
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_INDEX, CONF_NAME, CONF_POSITION, CONF_SIZE
from esphome.cpp_generator import MockObjClass
from . import buttonmatrix_spec
from .automation import action_to_code
from .defines import (
CONF_ANIMATED,
CONF_MAIN,
CONF_TAB_ID,
CONF_TABS,
DIRECTIONS,
TYPE_FLEX,
literal,
)
from .lv_validation import animated, lv_int, size
from .lvcode import LocalVariable, lv, lv_assign, lv_expr
from .obj import obj_spec
from .schemas import container_schema, part_schema
from .types import LV_EVENT, LvType, ObjUpdateAction, lv_obj_t, lv_obj_t_ptr
from .widget import Widget, WidgetType, add_widgets, get_widgets, set_obj_properties
CONF_TABVIEW = "tabview"
CONF_TAB_STYLE = "tab_style"
lv_tab_t = LvType("lv_obj_t")
TABVIEW_SCHEMA = cv.Schema(
{
cv.Required(CONF_TABS): cv.ensure_list(
container_schema(
obj_spec,
{
cv.Required(CONF_NAME): cv.string,
cv.GenerateID(): cv.declare_id(lv_tab_t),
},
)
),
cv.Optional(CONF_TAB_STYLE): part_schema(buttonmatrix_spec),
cv.Optional(CONF_POSITION, default="top"): DIRECTIONS.one_of,
cv.Optional(CONF_SIZE, default="10%"): size,
}
)
class TabviewType(WidgetType):
def __init__(self):
super().__init__(
CONF_TABVIEW,
LvType(
"lv_tabview_t",
largs=[(lv_obj_t_ptr, "tab")],
lvalue=lambda w: lv_expr.obj_get_child(
lv_expr.tabview_get_content(w.obj),
lv_expr.tabview_get_tab_act(w.obj),
),
has_on_value=True,
),
parts=(CONF_MAIN,),
schema=TABVIEW_SCHEMA,
modify_schema={},
)
def get_uses(self):
return "btnmatrix", TYPE_FLEX
async def to_code(self, w: Widget, config: dict):
for tab_conf in config[CONF_TABS]:
w_id = tab_conf[CONF_ID]
tab_obj = cg.Pvariable(w_id, cg.nullptr, type_=lv_tab_t)
tab_widget = Widget.create(w_id, tab_obj, obj_spec)
lv_assign(tab_obj, lv_expr.tabview_add_tab(w.obj, tab_conf[CONF_NAME]))
await set_obj_properties(tab_widget, tab_conf)
await add_widgets(tab_widget, tab_conf)
if button_style := config.get(CONF_TAB_STYLE):
with LocalVariable(
"tabview_btnmatrix", lv_obj_t, rhs=lv_expr.tabview_get_tab_btns(w.obj)
) as btnmatrix_obj:
await set_obj_properties(Widget(btnmatrix_obj, obj_spec), button_style)
def obj_creator(self, parent: MockObjClass, config: dict):
return lv_expr.call(
"tabview_create",
parent,
literal(config[CONF_POSITION]),
literal(config[CONF_SIZE]),
)
tabview_spec = TabviewType()
@automation.register_action(
"lvgl.tabview.select",
ObjUpdateAction,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(tabview_spec.w_type),
cv.Optional(CONF_ANIMATED, default=False): animated,
cv.Required(CONF_INDEX): lv_int,
},
).add_extra(cv.has_at_least_one_key(CONF_INDEX, CONF_TAB_ID)),
)
async def tabview_select(config, action_id, template_arg, args):
widget = await get_widgets(config)
index = config[CONF_INDEX]
async def do_select(w: Widget):
lv.tabview_set_act(w.obj, index, literal(config[CONF_ANIMATED]))
lv.event_send(w.obj, LV_EVENT.VALUE_CHANGED, cg.nullptr)
return await action_to_code(widget, do_select, action_id, template_arg, args)

View file

@ -0,0 +1,67 @@
import esphome.config_validation as cv
from esphome.const import CONF_MAX_LENGTH
from .defines import (
CONF_ACCEPTED_CHARS,
CONF_CURSOR,
CONF_MAIN,
CONF_ONE_LINE,
CONF_PASSWORD_MODE,
CONF_PLACEHOLDER_TEXT,
CONF_SCROLLBAR,
CONF_SELECTED,
CONF_TEXT,
CONF_TEXTAREA_PLACEHOLDER,
)
from .lv_validation import lv_bool, lv_int, lv_text
from .schemas import TEXT_SCHEMA
from .types import LvText
from .widget import Widget, WidgetType
CONF_TEXTAREA = "textarea"
lv_textarea_t = LvText("lv_textarea_t")
TEXTAREA_SCHEMA = TEXT_SCHEMA.extend(
{
cv.Optional(CONF_PLACEHOLDER_TEXT): lv_text,
cv.Optional(CONF_ACCEPTED_CHARS): lv_text,
cv.Optional(CONF_ONE_LINE): lv_bool,
cv.Optional(CONF_PASSWORD_MODE): lv_bool,
cv.Optional(CONF_MAX_LENGTH): lv_int,
}
)
class TextareaType(WidgetType):
def __init__(self):
super().__init__(
CONF_TEXTAREA,
lv_textarea_t,
(
CONF_MAIN,
CONF_SCROLLBAR,
CONF_SELECTED,
CONF_CURSOR,
CONF_TEXTAREA_PLACEHOLDER,
),
TEXTAREA_SCHEMA,
)
async def to_code(self, w: Widget, config: dict):
for prop in (CONF_TEXT, CONF_PLACEHOLDER_TEXT, CONF_ACCEPTED_CHARS):
if (value := config.get(prop)) is not None:
await w.set_property(prop, await lv_text.process(value))
await w.set_property(
CONF_MAX_LENGTH, await lv_int.process(config.get(CONF_MAX_LENGTH))
)
await w.set_property(
CONF_PASSWORD_MODE,
await lv_bool.process(config.get(CONF_PASSWORD_MODE)),
)
await w.set_property(
CONF_ONE_LINE, await lv_bool.process(config.get(CONF_ONE_LINE))
)
textarea_spec = TextareaType()

View file

@ -0,0 +1,128 @@
from esphome import automation
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_ON_VALUE, CONF_ROW, CONF_TRIGGER_ID
from .automation import action_to_code
from .defines import (
CONF_ANIMATED,
CONF_COLUMN,
CONF_DIR,
CONF_MAIN,
CONF_TILE_ID,
CONF_TILES,
TILE_DIRECTIONS,
literal,
)
from .lv_validation import animated, lv_int
from .lvcode import lv, lv_assign, lv_expr, lv_obj, lv_Pvariable
from .obj import obj_spec
from .schemas import container_schema
from .types import LV_EVENT, LvType, ObjUpdateAction, lv_obj_t, lv_obj_t_ptr
from .widget import Widget, WidgetType, add_widgets, get_widgets, set_obj_properties
CONF_TILEVIEW = "tileview"
lv_tile_t = LvType("lv_tileview_tile_t")
lv_tileview_t = LvType(
"lv_tileview_t",
largs=[(lv_obj_t_ptr, "tile")],
lvalue=lambda w: w.get_property("tile_act"),
)
tile_spec = WidgetType("lv_tileview_tile_t", lv_tile_t, (CONF_MAIN,), {})
TILEVIEW_SCHEMA = cv.Schema(
{
cv.Required(CONF_TILES): cv.ensure_list(
container_schema(
obj_spec,
{
cv.Required(CONF_ROW): lv_int,
cv.Required(CONF_COLUMN): lv_int,
cv.GenerateID(): cv.declare_id(lv_tile_t),
cv.Optional(CONF_DIR, default="ALL"): TILE_DIRECTIONS.several_of,
},
)
),
cv.Optional(CONF_ON_VALUE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
automation.Trigger.template(lv_obj_t_ptr)
)
}
),
}
)
class TileviewType(WidgetType):
def __init__(self):
super().__init__(
CONF_TILEVIEW,
lv_tileview_t,
(CONF_MAIN,),
schema=TILEVIEW_SCHEMA,
modify_schema={},
)
async def to_code(self, w: Widget, config: dict):
for tile_conf in config.get(CONF_TILES) or ():
w_id = tile_conf[CONF_ID]
tile_obj = lv_Pvariable(lv_obj_t, w_id)
tile = Widget.create(w_id, tile_obj, tile_spec, tile_conf)
dirs = tile_conf[CONF_DIR]
if isinstance(dirs, list):
dirs = "|".join(dirs)
lv_assign(
tile_obj,
lv_expr.tileview_add_tile(
w.obj, tile_conf[CONF_COLUMN], tile_conf[CONF_ROW], literal(dirs)
),
)
await set_obj_properties(tile, tile_conf)
await add_widgets(tile, tile_conf)
tileview_spec = TileviewType()
def tile_select_validate(config):
row = CONF_ROW in config
column = CONF_COLUMN in config
tile = CONF_TILE_ID in config
if tile and (row or column) or not tile and not (row and column):
raise cv.Invalid("Specify either a tile id, or both a row and a column")
return config
@automation.register_action(
"lvgl.tileview.select",
ObjUpdateAction,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(lv_tileview_t),
cv.Optional(CONF_ANIMATED, default=False): animated,
cv.Optional(CONF_ROW): lv_int,
cv.Optional(CONF_COLUMN): lv_int,
cv.Optional(CONF_TILE_ID): cv.use_id(lv_tile_t),
},
).add_extra(tile_select_validate),
)
async def tileview_select(config, action_id, template_arg, args):
widgets = await get_widgets(config)
async def do_select(w: Widget):
if tile := config.get(CONF_TILE_ID):
tile = await cg.get_variable(tile)
lv_obj.set_tile(w.obj, tile, literal(config[CONF_ANIMATED]))
else:
row = await lv_int.process(config[CONF_ROW])
column = await lv_int.process(config[CONF_COLUMN])
lv_obj.set_tile_id(
widgets[0].obj, column, row, literal(config[CONF_ANIMATED])
)
lv.event_send(w.obj, LV_EVENT.VALUE_CHANGED, cg.nullptr)
return await action_to_code(widgets, do_select, action_id, template_arg, args)

View file

@ -282,13 +282,13 @@ async def set_obj_properties(w: Widget, config):
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 layout_type == TYPE_GRID: if layout_type == TYPE_GRID:
wid = config[CONF_ID] wid = config[CONF_ID]
rows = "{" + ",".join(layout[CONF_GRID_ROWS]) + ", LV_GRID_TEMPLATE_LAST}" rows = [str(x) for x in layout[CONF_GRID_ROWS]]
rows = "{" + ",".join(rows) + ", LV_GRID_TEMPLATE_LAST}"
row_id = ID(f"{wid}_row_dsc", is_declaration=True, type=lv_coord_t) row_id = ID(f"{wid}_row_dsc", is_declaration=True, type=lv_coord_t)
row_array = cg.static_const_array(row_id, cg.RawExpression(rows)) row_array = cg.static_const_array(row_id, cg.RawExpression(rows))
w.set_style("grid_row_dsc_array", row_array, 0) w.set_style("grid_row_dsc_array", row_array, 0)
columns = ( columns = [str(x) for x in layout[CONF_GRID_COLUMNS]]
"{" + ",".join(layout[CONF_GRID_COLUMNS]) + ", LV_GRID_TEMPLATE_LAST}" columns = "{" + ",".join(columns) + ", LV_GRID_TEMPLATE_LAST}"
)
column_id = ID(f"{wid}_column_dsc", is_declaration=True, type=lv_coord_t) column_id = ID(f"{wid}_column_dsc", is_declaration=True, type=lv_coord_t)
column_array = cg.static_const_array(column_id, cg.RawExpression(columns)) column_array = cg.static_const_array(column_id, cg.RawExpression(columns))
w.set_style("grid_column_dsc_array", column_array, 0) w.set_style("grid_column_dsc_array", column_array, 0)

View file

@ -39,9 +39,12 @@
#define USE_LOCK #define USE_LOCK
#define USE_LOGGER #define USE_LOGGER
#define USE_LVGL #define USE_LVGL
#define USE_LVGL_ANIMIMG
#define USE_LVGL_BINARY_SENSOR #define USE_LVGL_BINARY_SENSOR
#define USE_LVGL_BUTTONMATRIX
#define USE_LVGL_FONT #define USE_LVGL_FONT
#define USE_LVGL_IMAGE #define USE_LVGL_IMAGE
#define USE_LVGL_KEYBOARD
#define USE_LVGL_KEY_LISTENER #define USE_LVGL_KEY_LISTENER
#define USE_LVGL_TOUCHSCREEN #define USE_LVGL_TOUCHSCREEN
#define USE_LVGL_ROTARY_ENCODER #define USE_LVGL_ROTARY_ENCODER

2
tests/components/lvgl/.gitattributes vendored Normal file
View file

@ -0,0 +1,2 @@
*.ttf -text

View file

@ -8,3 +8,49 @@ touchscreen:
x_max: 240 x_max: 240
y_max: 320 y_max: 320
font:
- file: "$component_dir/roboto.ttf"
id: roboto20
size: 20
extras:
- file: '$component_dir/materialdesignicons-webfont.ttf'
glyphs: [
"\U000F004B",
"\U0000f0ed",
"\U000F006E",
"\U000F012C",
"\U000F179B",
"\U000F0748",
"\U000F1A1B",
"\U000F02DC",
"\U000F0A02",
"\U000F035F",
"\U000F0156",
"\U000F0C5F",
"\U000f0084",
"\U000f0091",
]
- file: "$component_dir/helvetica.ttf"
id: helvetica20
- file: "$component_dir/roboto.ttf"
id: roboto10
size: 10
bpp: 4
extras:
- file: '$component_dir/materialdesignicons-webfont.ttf'
glyphs: [
"\U000F004B",
"\U0000f0ed",
"\U000F006E",
"\U000F012C",
"\U000F179B",
"\U000F0748",
"\U000F1A1B",
"\U000F02DC",
"\U000F0A02",
"\U000F035F",
"\U000F0156",
"\U000F0C5F",
"\U000f0084",
"\U000f0091",
]

Binary file not shown.

View file

@ -1,6 +1,53 @@
lvgl: lvgl:
log_level: TRACE log_level: TRACE
bg_color: light_blue bg_color: light_blue
theme:
obj:
border_width: 1
style_definitions:
- id: style_test
bg_color: 0x2F8CD8
- id: header_footer
bg_color: 0x20214F
bg_grad_color: 0x005782
bg_grad_dir: VER
bg_opa: cover
border_width: 0
radius: 0
pad_all: 0
pad_row: 0
pad_column: 0
border_color: 0x0077b3
text_color: 0xFFFFFF
width: 100%
height: 30
border_side: [left, top]
text_decor: [underline, strikethrough]
- id: style_line
line_color: light_blue
line_width: 8
line_rounded: true
- id: date_style
text_font: roboto10
align: center
text_color: 0x000000
bg_opa: cover
radius: 4
pad_all: 2
- id: spin_button
height: 40
width: 40
- id: spin_label
align: center
text_align: center
text_font: space16
- id: bdr_style
border_color: 0x808080
border_width: 2
pad_all: 4
align: center
touchscreens: touchscreens:
- touchscreen_id: tft_touch - touchscreen_id: tft_touch
long_press_repeat_time: 200ms long_press_repeat_time: 200ms
@ -9,6 +56,13 @@ lvgl:
- id: page1 - id: page1
skip: true skip: true
widgets: widgets:
- animimg:
height: 60
id: anim_img
src: [cat_image, dog_image]
repeat_count: 10
duration: 1s
auto_start: true
- label: - label:
id: hello_label id: hello_label
text: Hello world text: Hello world
@ -16,7 +70,9 @@ lvgl:
align: center align: center
text_font: montserrat_40 text_font: montserrat_40
border_post: true border_post: true
on_click:
then:
- lvgl.animimg.stop: anim_img
- label: - label:
text: "Hello shiny day" text: "Hello shiny day"
text_color: 0xFFFFFF text_color: 0xFFFFFF
@ -94,7 +150,65 @@ lvgl:
width: 10px width: 10px
x: 100 x: 100
y: 120 y: 120
- buttonmatrix:
on_press:
logger.log:
format: "matrix button pressed: %d"
args: ["x"]
on_long_press:
lvgl.matrix.button.update:
id: [button_a, button_e, button_c]
control:
disabled: true
on_click:
logger.log:
format: "matrix button clicked: %d, is button_a = %u"
args: ["x", "id(button_a) == x"]
items:
checked:
bg_color: 0xFFFF00
id: b_matrix
rows:
- buttons:
- id: button_a
text: home icon
width: 2
control:
checkable: true
on_value:
logger.log:
format: "button_a value %d"
args: [x]
- id: button_b
text: B
width: 1
on_value:
logger.log:
format: "button_b value %d"
args: [x]
on_click:
then:
- lvgl.page.previous:
control:
hidden: false
- buttons:
- id: button_c
text: C
control:
checkable: false
- id: button_d
text: menu left
on_long_press:
then:
logger.log: Long pressed
on_long_press_repeat:
then:
logger.log: Long pressed repeated
- buttons:
- id: button_e
- button: - button:
id: button_button
width: 20% width: 20%
height: 10% height: 10%
pressed: pressed:
@ -137,6 +251,7 @@ lvgl:
on_long_press_repeat: on_long_press_repeat:
logger.log: Button clicked logger.log: Button clicked
- led: - led:
id: lv_led
color: 0x00FF00 color: 0x00FF00
brightness: 50% brightness: 50%
align: right_mid align: right_mid
@ -151,6 +266,41 @@ lvgl:
- id: page2 - id: page2
widgets: widgets:
- button:
styles: spin_button
id: spin_up
on_click:
- lvgl.spinbox.increment: spinbox_id
widgets:
- label:
styles: spin_label
text: "+"
- spinbox:
text_font: space16
id: spinbox_id
align: center
width: 120
range_from: -10
range_to: 1000
step: 5.0
rollover: false
digits: 6
decimal_places: 2
value: 15
on_value:
then:
- logger.log:
format: "Spinbox value is %f"
args: [x]
- button:
styles: spin_button
id: spin_down
on_click:
- lvgl.spinbox.decrement: spinbox_id
widgets:
- label:
styles: spin_label
text: "-"
- arc: - arc:
align: left_mid align: left_mid
id: lv_arc id: lv_arc
@ -160,7 +310,6 @@ lvgl:
- logger.log: - logger.log:
format: "Arc value is %f" format: "Arc value is %f"
args: [x] args: [x]
group: general
scroll_on_focus: true scroll_on_focus: true
value: 75 value: 75
min_value: 1 min_value: 1
@ -201,6 +350,7 @@ lvgl:
- switch: - switch:
align: right_mid align: right_mid
- checkbox: - checkbox:
id: checkbox_id
text: Checkbox text: Checkbox
align: bottom_right align: bottom_right
- slider: - slider:
@ -221,6 +371,78 @@ lvgl:
- lvgl.slider.update: - lvgl.slider.update:
id: slider_id id: slider_id
value: !lambda return (int)((float)rand() / RAND_MAX * 100); value: !lambda return (int)((float)rand() / RAND_MAX * 100);
- tabview:
id: tabview_id
width: 100%
height: 80%
position: top
on_value:
then:
- if:
condition:
lambda: return tab == id(tabview_tab_1);
then:
- logger.log: "Dog tab is now showing"
tabs:
- name: Dog
id: tabview_tab_1
border_width: 2
border_color: 0xff0000
width: 100%
pad_all: 8
layout:
type: grid
grid_row_align: end
grid_rows: [25px, fr(1), content]
grid_columns: [40, fr(1), fr(1)]
widgets:
- image:
grid_cell_row_pos: 0
grid_cell_column_pos: 0
src: dog_image
on_click:
then:
- lvgl.tabview.select:
id: tabview_id
index: 1
animated: true
- label:
styles: bdr_style
grid_cell_x_align: center
grid_cell_y_align: stretch
grid_cell_row_pos: 0
grid_cell_column_pos: 1
grid_cell_column_span: 1
text: "Grid cell 0/1"
- label:
grid_cell_x_align: end
styles: bdr_style
grid_cell_row_pos: 1
grid_cell_column_pos: 0
text: "Grid cell 1/0"
- label:
styles: bdr_style
grid_cell_row_pos: 1
grid_cell_column_pos: 1
text: "Grid cell 1/1"
- label:
id: cell_1_3
styles: bdr_style
grid_cell_row_pos: 1
grid_cell_column_pos: 2
text: "Grid cell 1/2"
- name: Cat
id: tabview_tab_2
widgets:
- image:
src: cat_image
on_click:
then:
- logger.log: Cat image clicked
- lvgl.tabview.select:
id: tabview_id
index: 0
animated: true
font: font:
- file: "gfonts://Roboto" - file: "gfonts://Roboto"
id: space16 id: space16
@ -230,7 +452,7 @@ image:
- id: cat_image - id: cat_image
resize: 256x48 resize: 256x48
file: $component_dir/logo-text.svg file: $component_dir/logo-text.svg
- id: dog_img - id: dog_image
file: $component_dir/logo-text.svg file: $component_dir/logo-text.svg
resize: 256x48 resize: 256x48
type: TRANSPARENT_BINARY type: TRANSPARENT_BINARY

Binary file not shown.

Binary file not shown.