mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +00:00 
			
		
		
		
	[lvgl] Stage 5 (#7191)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
		| @@ -24,10 +24,13 @@ from . import defines as df, helpers, lv_validation as lvalid | ||||
| from .animimg import animimg_spec | ||||
| from .arc import arc_spec | ||||
| 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 .defines import CONF_SKIP | ||||
| from .dropdown import dropdown_spec | ||||
| from .img import img_spec | ||||
| from .keyboard import keyboard_spec | ||||
| from .label import label_spec | ||||
| from .led import led_spec | ||||
| from .line import line_spec | ||||
| @@ -35,8 +38,11 @@ from .lv_bar import bar_spec | ||||
| from .lv_switch import switch_spec | ||||
| from .lv_validation import lv_bool, lv_images_used | ||||
| from .lvcode import LvContext, LvglComponent | ||||
| from .meter import meter_spec | ||||
| from .msgbox import MSGBOX_SCHEMA, msgboxes_to_code | ||||
| from .obj import obj_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 .schemas import ( | ||||
|     DISP_BG_SCHEMA, | ||||
| @@ -52,8 +58,12 @@ from .schemas import ( | ||||
|     obj_schema, | ||||
| ) | ||||
| from .slider import slider_spec | ||||
| from .spinbox import spinbox_spec | ||||
| from .spinner import spinner_spec | ||||
| 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 .trigger import generate_triggers | ||||
| from .types import ( | ||||
| @@ -75,7 +85,7 @@ LOGGER = logging.getLogger(__name__) | ||||
| for w_type in ( | ||||
|     label_spec, | ||||
|     obj_spec, | ||||
|     btn_spec, | ||||
|     button_spec, | ||||
|     bar_spec, | ||||
|     slider_spec, | ||||
|     arc_spec, | ||||
| @@ -86,6 +96,15 @@ for w_type in ( | ||||
|     checkbox_spec, | ||||
|     img_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 | ||||
|  | ||||
| @@ -244,6 +263,7 @@ async def to_code(config): | ||||
|         await add_widgets(lv_scr_act, config) | ||||
|         await add_pages(lv_component, config) | ||||
|         await add_top_layer(config) | ||||
|         await msgboxes_to_code(config) | ||||
|         await disp_update(f"{lv_component}->get_disp()", config) | ||||
|         Widget.set_completed() | ||||
|         await generate_triggers(lv_component) | ||||
| @@ -308,6 +328,7 @@ CONFIG_SCHEMA = ( | ||||
|             cv.Exclusive(CONF_PAGES, CONF_PAGES): cv.ensure_list( | ||||
|                 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_TOP_LAYER): container_schema(obj_spec), | ||||
|             cv.Optional(df.CONF_TRANSPARENCY_KEY, default=0x000400): lvalid.lv_color, | ||||
|   | ||||
| @@ -2,8 +2,8 @@ from esphome import automation | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| 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 .defines import CONF_AUTO_START, CONF_MAIN, CONF_REPEAT_COUNT, CONF_SRC | ||||
| from .helpers import lvgl_components_required | ||||
|   | ||||
| @@ -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: | ||||
|         return | ||||
|     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)) | ||||
|         if bg_image := config.get(CONF_DISP_BG_IMAGE): | ||||
|             lv.disp_set_bg_image(disp_temp, await lv_image.process(bg_image)) | ||||
|   | ||||
| @@ -3,12 +3,12 @@ from esphome.const import CONF_BUTTON | ||||
| from .defines import CONF_MAIN | ||||
| 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): | ||||
|         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): | ||||
|         return ("btn",) | ||||
| @@ -17,4 +17,4 @@ class BtnType(WidgetType): | ||||
|         return [] | ||||
| 
 | ||||
| 
 | ||||
| btn_spec = BtnType() | ||||
| button_spec = ButtonType() | ||||
							
								
								
									
										277
									
								
								esphome/components/lvgl/buttonmatrix.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										277
									
								
								esphome/components/lvgl/buttonmatrix.py
									
									
									
									
									
										Normal 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 | ||||
|     ) | ||||
| @@ -18,7 +18,7 @@ class CheckboxType(WidgetType): | ||||
|         ) | ||||
|  | ||||
|     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)) | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -304,7 +304,7 @@ OBJ_FLAGS = ( | ||||
| ARC_MODES = LvConstant("LV_ARC_MODE_", "NORMAL", "REVERSE", "SYMMETRICAL") | ||||
| BAR_MODES = LvConstant("LV_BAR_MODE_", "NORMAL", "SYMMETRICAL", "RANGE") | ||||
|  | ||||
| BTNMATRIX_CTRLS = LvConstant( | ||||
| BUTTONMATRIX_CTRLS = LvConstant( | ||||
|     "LV_BTNMATRIX_CTRL_", | ||||
|     "HIDDEN", | ||||
|     "NO_REPEAT", | ||||
|   | ||||
							
								
								
									
										76
									
								
								esphome/components/lvgl/dropdown.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								esphome/components/lvgl/dropdown.py
									
									
									
									
									
										Normal 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() | ||||
| @@ -65,16 +65,16 @@ class ImgType(WidgetType): | ||||
|     async def to_code(self, w: Widget, config): | ||||
|         if src := config.get(CONF_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_y = config[CONF_PIVOT_Y] | ||||
|             lv.img_set_pivot(w.obj, pivot_x, pivot_y) | ||||
|             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) | ||||
|         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) | ||||
|         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) | ||||
|         if CONF_ANTIALIAS in config: | ||||
|             lv.img_set_antialias(w.obj, config[CONF_ANTIALIAS]) | ||||
|   | ||||
							
								
								
									
										49
									
								
								esphome/components/lvgl/keyboard.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								esphome/components/lvgl/keyboard.py
									
									
									
									
									
										Normal 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() | ||||
| @@ -20,9 +20,9 @@ class LedType(WidgetType): | ||||
|         super().__init__(CONF_LED, LvType("lv_led_t"), (CONF_MAIN,), LED_SCHEMA) | ||||
|  | ||||
|     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)) | ||||
|         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)) | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -146,12 +146,12 @@ LVEncoderListener::LVEncoderListener(lv_indev_type_t type, uint16_t lpt, uint16_ | ||||
| #endif  // USE_LVGL_ROTARY_ENCODER | ||||
|  | ||||
| #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); | ||||
|   lv_obj_add_event_cb( | ||||
|       lv_obj, | ||||
|       [](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) | ||||
|           return; | ||||
|         auto key_idx = lv_btnmatrix_get_selected_btn(self->obj); | ||||
|   | ||||
| @@ -246,7 +246,7 @@ class LVEncoderListener : public Parented<LvglComponent> { | ||||
| }; | ||||
| #endif  // USE_LVGL_ROTARY_ENCODER | ||||
| #ifdef USE_LVGL_BUTTONMATRIX | ||||
| class LvBtnmatrixType : public key_provider::KeyProvider, public LvCompound { | ||||
| class LvButtonMatrixType : public key_provider::KeyProvider, public LvCompound { | ||||
|  public: | ||||
|   void set_obj(lv_obj_t *lv_obj) override; | ||||
|   uint16_t get_selected() { return lv_btnmatrix_get_selected_btn(this->obj); } | ||||
|   | ||||
							
								
								
									
										302
									
								
								esphome/components/lvgl/meter.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										302
									
								
								esphome/components/lvgl/meter.py
									
									
									
									
									
										Normal 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) | ||||
							
								
								
									
										127
									
								
								esphome/components/lvgl/msgbox.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								esphome/components/lvgl/msgbox.py
									
									
									
									
									
										Normal 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) | ||||
							
								
								
									
										77
									
								
								esphome/components/lvgl/roller.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								esphome/components/lvgl/roller.py
									
									
									
									
									
										Normal 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() | ||||
							
								
								
									
										178
									
								
								esphome/components/lvgl/spinbox.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								esphome/components/lvgl/spinbox.py
									
									
									
									
									
										Normal 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) | ||||
| @@ -26,7 +26,7 @@ async def styles_to_code(config): | ||||
|         svar = cg.new_Pvariable(style[CONF_ID]) | ||||
|         lv.style_init(svar) | ||||
|         for prop, validator in ALL_STYLES.items(): | ||||
|             if value := style.get(prop): | ||||
|             if (value := style.get(prop)) is not None: | ||||
|                 if isinstance(validator, LValidator): | ||||
|                     value = await validator.process(value) | ||||
|                 if isinstance(value, list): | ||||
|   | ||||
							
								
								
									
										114
									
								
								esphome/components/lvgl/tabview.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								esphome/components/lvgl/tabview.py
									
									
									
									
									
										Normal 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) | ||||
							
								
								
									
										67
									
								
								esphome/components/lvgl/textarea.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								esphome/components/lvgl/textarea.py
									
									
									
									
									
										Normal 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() | ||||
							
								
								
									
										128
									
								
								esphome/components/lvgl/tileview.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								esphome/components/lvgl/tileview.py
									
									
									
									
									
										Normal 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) | ||||
| @@ -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()}")) | ||||
|         if layout_type == TYPE_GRID: | ||||
|             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_array = cg.static_const_array(row_id, cg.RawExpression(rows)) | ||||
|             w.set_style("grid_row_dsc_array", row_array, 0) | ||||
|             columns = ( | ||||
|                 "{" + ",".join(layout[CONF_GRID_COLUMNS]) + ", LV_GRID_TEMPLATE_LAST}" | ||||
|             ) | ||||
|             columns = [str(x) for x in layout[CONF_GRID_COLUMNS]] | ||||
|             columns = "{" + ",".join(columns) + ", LV_GRID_TEMPLATE_LAST}" | ||||
|             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)) | ||||
|             w.set_style("grid_column_dsc_array", column_array, 0) | ||||
|   | ||||
| @@ -39,9 +39,12 @@ | ||||
| #define USE_LOCK | ||||
| #define USE_LOGGER | ||||
| #define USE_LVGL | ||||
| #define USE_LVGL_ANIMIMG | ||||
| #define USE_LVGL_BINARY_SENSOR | ||||
| #define USE_LVGL_BUTTONMATRIX | ||||
| #define USE_LVGL_FONT | ||||
| #define USE_LVGL_IMAGE | ||||
| #define USE_LVGL_KEYBOARD | ||||
| #define USE_LVGL_KEY_LISTENER | ||||
| #define USE_LVGL_TOUCHSCREEN | ||||
| #define USE_LVGL_ROTARY_ENCODER | ||||
|   | ||||
							
								
								
									
										2
									
								
								tests/components/lvgl/.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								tests/components/lvgl/.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| *.ttf        -text | ||||
|  | ||||
| @@ -8,3 +8,49 @@ touchscreen: | ||||
|       x_max: 240 | ||||
|       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", | ||||
|         ] | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								tests/components/lvgl/helvetica.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								tests/components/lvgl/helvetica.ttf
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| @@ -1,6 +1,53 @@ | ||||
| lvgl: | ||||
|   log_level: TRACE | ||||
|   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: | ||||
|     - touchscreen_id: tft_touch | ||||
|       long_press_repeat_time: 200ms | ||||
| @@ -9,6 +56,13 @@ lvgl: | ||||
|     - id: page1 | ||||
|       skip: true | ||||
|       widgets: | ||||
|         - animimg: | ||||
|             height: 60 | ||||
|             id: anim_img | ||||
|             src: [cat_image, dog_image] | ||||
|             repeat_count: 10 | ||||
|             duration: 1s | ||||
|             auto_start: true | ||||
|         - label: | ||||
|             id: hello_label | ||||
|             text: Hello world | ||||
| @@ -16,7 +70,9 @@ lvgl: | ||||
|             align: center | ||||
|             text_font: montserrat_40 | ||||
|             border_post: true | ||||
|  | ||||
|             on_click: | ||||
|               then: | ||||
|                 - lvgl.animimg.stop: anim_img | ||||
|         - label: | ||||
|             text: "Hello shiny day" | ||||
|             text_color: 0xFFFFFF | ||||
| @@ -94,7 +150,65 @@ lvgl: | ||||
|             width: 10px | ||||
|             x: 100 | ||||
|             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: | ||||
|             id: button_button | ||||
|             width: 20% | ||||
|             height: 10% | ||||
|             pressed: | ||||
| @@ -137,6 +251,7 @@ lvgl: | ||||
|             on_long_press_repeat: | ||||
|               logger.log: Button clicked | ||||
|         - led: | ||||
|             id: lv_led | ||||
|             color: 0x00FF00 | ||||
|             brightness: 50% | ||||
|             align: right_mid | ||||
| @@ -151,6 +266,41 @@ lvgl: | ||||
|  | ||||
|     - id: page2 | ||||
|       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: | ||||
|             align: left_mid | ||||
|             id: lv_arc | ||||
| @@ -160,7 +310,6 @@ lvgl: | ||||
|                 - logger.log: | ||||
|                     format: "Arc value is %f" | ||||
|                     args: [x] | ||||
|             group: general | ||||
|             scroll_on_focus: true | ||||
|             value: 75 | ||||
|             min_value: 1 | ||||
| @@ -201,6 +350,7 @@ lvgl: | ||||
|         - switch: | ||||
|             align: right_mid | ||||
|         - checkbox: | ||||
|             id: checkbox_id | ||||
|             text: Checkbox | ||||
|             align: bottom_right | ||||
|         - slider: | ||||
| @@ -221,6 +371,78 @@ lvgl: | ||||
|                 - lvgl.slider.update: | ||||
|                     id: slider_id | ||||
|                     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: | ||||
|   - file: "gfonts://Roboto" | ||||
|     id: space16 | ||||
| @@ -230,7 +452,7 @@ image: | ||||
|   - id: cat_image | ||||
|     resize: 256x48 | ||||
|     file: $component_dir/logo-text.svg | ||||
|   - id: dog_img | ||||
|   - id: dog_image | ||||
|     file: $component_dir/logo-text.svg | ||||
|     resize: 256x48 | ||||
|     type: TRANSPARENT_BINARY | ||||
|   | ||||
							
								
								
									
										
											BIN
										
									
								
								tests/components/lvgl/materialdesignicons-webfont.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								tests/components/lvgl/materialdesignicons-webfont.ttf
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								tests/components/lvgl/roboto.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								tests/components/lvgl/roboto.ttf
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
		Reference in New Issue
	
	Block a user