mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	Merge remote-tracking branch 'origin/dev' into nrf52_core
This commit is contained in:
		
							
								
								
									
										7
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							| @@ -13,6 +13,13 @@ updates: | |||||||
|     schedule: |     schedule: | ||||||
|       interval: daily |       interval: daily | ||||||
|     open-pull-requests-limit: 10 |     open-pull-requests-limit: 10 | ||||||
|  |     groups: | ||||||
|  |       docker-actions: | ||||||
|  |         applies-to: version-updates | ||||||
|  |         patterns: | ||||||
|  |           - "docker/setup-qemu-action" | ||||||
|  |           - "docker/login-action" | ||||||
|  |           - "docker/setup-buildx-action" | ||||||
|   - package-ecosystem: github-actions |   - package-ecosystem: github-actions | ||||||
|     directory: "/.github/actions/build-image" |     directory: "/.github/actions/build-image" | ||||||
|     schedule: |     schedule: | ||||||
|   | |||||||
| @@ -217,6 +217,7 @@ esphome/components/lock/* @esphome/core | |||||||
| esphome/components/logger/* @esphome/core | esphome/components/logger/* @esphome/core | ||||||
| esphome/components/ltr390/* @latonita @sjtrny | esphome/components/ltr390/* @latonita @sjtrny | ||||||
| esphome/components/ltr_als_ps/* @latonita | esphome/components/ltr_als_ps/* @latonita | ||||||
|  | esphome/components/lvgl/* @clydebarrow | ||||||
| esphome/components/m5stack_8angle/* @rnauber | esphome/components/m5stack_8angle/* @rnauber | ||||||
| esphome/components/matrix_keypad/* @ssieb | esphome/components/matrix_keypad/* @ssieb | ||||||
| esphome/components/max31865/* @DAVe3283 | esphome/components/max31865/* @DAVe3283 | ||||||
|   | |||||||
							
								
								
									
										212
									
								
								esphome/components/lvgl/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										212
									
								
								esphome/components/lvgl/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,212 @@ | |||||||
|  | import logging | ||||||
|  |  | ||||||
|  | import esphome.codegen as cg | ||||||
|  | from esphome.components.display import Display | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.const import ( | ||||||
|  |     CONF_AUTO_CLEAR_ENABLED, | ||||||
|  |     CONF_BUFFER_SIZE, | ||||||
|  |     CONF_ID, | ||||||
|  |     CONF_LAMBDA, | ||||||
|  |     CONF_PAGES, | ||||||
|  | ) | ||||||
|  | from esphome.core import CORE, ID, Lambda | ||||||
|  | from esphome.cpp_generator import MockObj | ||||||
|  | from esphome.final_validate import full_config | ||||||
|  | from esphome.helpers import write_file_if_changed | ||||||
|  |  | ||||||
|  | from . import defines as df, helpers, lv_validation as lvalid | ||||||
|  | from .label import label_spec | ||||||
|  | from .lvcode import ConstantLiteral, LvContext | ||||||
|  |  | ||||||
|  | # from .menu import menu_spec | ||||||
|  | from .obj import obj_spec | ||||||
|  | from .schemas import WIDGET_TYPES, any_widget_schema, obj_schema | ||||||
|  | from .types import FontEngine, LvglComponent, lv_disp_t_ptr, lv_font_t, lvgl_ns | ||||||
|  | from .widget import LvScrActType, Widget, add_widgets, set_obj_properties | ||||||
|  |  | ||||||
|  | DOMAIN = "lvgl" | ||||||
|  | DEPENDENCIES = ("display",) | ||||||
|  | AUTO_LOAD = ("key_provider",) | ||||||
|  | CODEOWNERS = ("@clydebarrow",) | ||||||
|  | LOGGER = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  | for widg in ( | ||||||
|  |     label_spec, | ||||||
|  |     obj_spec, | ||||||
|  | ): | ||||||
|  |     WIDGET_TYPES[widg.name] = widg | ||||||
|  |  | ||||||
|  | lv_scr_act_spec = LvScrActType() | ||||||
|  | lv_scr_act = Widget.create( | ||||||
|  |     None, ConstantLiteral("lv_scr_act()"), lv_scr_act_spec, {}, parent=None | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | WIDGET_SCHEMA = any_widget_schema() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def add_init_lambda(lv_component, init): | ||||||
|  |     if init: | ||||||
|  |         lamb = await cg.process_lambda(Lambda(init), [(lv_disp_t_ptr, "lv_disp")]) | ||||||
|  |         cg.add(lv_component.add_init_lambda(lamb)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | lv_defines = {}  # Dict of #defines to provide as build flags | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def add_define(macro, value="1"): | ||||||
|  |     if macro in lv_defines and lv_defines[macro] != value: | ||||||
|  |         LOGGER.error( | ||||||
|  |             "Redefinition of %s - was %s now %s", macro, lv_defines[macro], value | ||||||
|  |         ) | ||||||
|  |     lv_defines[macro] = value | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def as_macro(macro, value): | ||||||
|  |     if value is None: | ||||||
|  |         return f"#define {macro}" | ||||||
|  |     return f"#define {macro} {value}" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | LV_CONF_FILENAME = "lv_conf.h" | ||||||
|  | LV_CONF_H_FORMAT = """\ | ||||||
|  | #pragma once | ||||||
|  | {} | ||||||
|  | """ | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def generate_lv_conf_h(): | ||||||
|  |     definitions = [as_macro(m, v) for m, v in lv_defines.items()] | ||||||
|  |     definitions.sort() | ||||||
|  |     return LV_CONF_H_FORMAT.format("\n".join(definitions)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def final_validation(config): | ||||||
|  |     global_config = full_config.get() | ||||||
|  |     for display_id in config[df.CONF_DISPLAYS]: | ||||||
|  |         path = global_config.get_path_for_id(display_id)[:-1] | ||||||
|  |         display = global_config.get_config_for_path(path) | ||||||
|  |         if CONF_LAMBDA in display: | ||||||
|  |             raise cv.Invalid("Using lambda: in display config not compatible with LVGL") | ||||||
|  |         if display[CONF_AUTO_CLEAR_ENABLED]: | ||||||
|  |             raise cv.Invalid( | ||||||
|  |                 "Using auto_clear_enabled: true in display config not compatible with LVGL" | ||||||
|  |             ) | ||||||
|  |     buffer_frac = config[CONF_BUFFER_SIZE] | ||||||
|  |     if not CORE.is_host and buffer_frac > 0.5 and "psram" not in global_config: | ||||||
|  |         LOGGER.warning("buffer_size: may need to be reduced without PSRAM") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def to_code(config): | ||||||
|  |     cg.add_library("lvgl/lvgl", "8.4.0") | ||||||
|  |     CORE.add_define("USE_LVGL") | ||||||
|  |     # suppress default enabling of extra widgets | ||||||
|  |     add_define("_LV_KCONFIG_PRESENT") | ||||||
|  |     # Always enable - lots of things use it. | ||||||
|  |     add_define("LV_DRAW_COMPLEX", "1") | ||||||
|  |     add_define("LV_TICK_CUSTOM", "1") | ||||||
|  |     add_define("LV_TICK_CUSTOM_INCLUDE", '"esphome/components/lvgl/lvgl_hal.h"') | ||||||
|  |     add_define("LV_TICK_CUSTOM_SYS_TIME_EXPR", "(lv_millis())") | ||||||
|  |     add_define("LV_MEM_CUSTOM", "1") | ||||||
|  |     add_define("LV_MEM_CUSTOM_ALLOC", "lv_custom_mem_alloc") | ||||||
|  |     add_define("LV_MEM_CUSTOM_FREE", "lv_custom_mem_free") | ||||||
|  |     add_define("LV_MEM_CUSTOM_REALLOC", "lv_custom_mem_realloc") | ||||||
|  |     add_define("LV_MEM_CUSTOM_INCLUDE", '"esphome/components/lvgl/lvgl_hal.h"') | ||||||
|  |  | ||||||
|  |     add_define("LV_LOG_LEVEL", f"LV_LOG_LEVEL_{config[df.CONF_LOG_LEVEL]}") | ||||||
|  |     add_define("LV_COLOR_DEPTH", config[df.CONF_COLOR_DEPTH]) | ||||||
|  |     for font in helpers.lv_fonts_used: | ||||||
|  |         add_define(f"LV_FONT_{font.upper()}") | ||||||
|  |  | ||||||
|  |     if config[df.CONF_COLOR_DEPTH] == 16: | ||||||
|  |         add_define( | ||||||
|  |             "LV_COLOR_16_SWAP", | ||||||
|  |             "1" if config[df.CONF_BYTE_ORDER] == "big_endian" else "0", | ||||||
|  |         ) | ||||||
|  |     add_define( | ||||||
|  |         "LV_COLOR_CHROMA_KEY", | ||||||
|  |         await lvalid.lv_color.process(config[df.CONF_TRANSPARENCY_KEY]), | ||||||
|  |     ) | ||||||
|  |     CORE.add_build_flag("-Isrc") | ||||||
|  |  | ||||||
|  |     cg.add_global(lvgl_ns.using) | ||||||
|  |     lv_component = cg.new_Pvariable(config[CONF_ID]) | ||||||
|  |     await cg.register_component(lv_component, config) | ||||||
|  |     Widget.create(config[CONF_ID], lv_component, WIDGET_TYPES[df.CONF_OBJ], config) | ||||||
|  |     for display in config[df.CONF_DISPLAYS]: | ||||||
|  |         cg.add(lv_component.add_display(await cg.get_variable(display))) | ||||||
|  |  | ||||||
|  |     frac = config[CONF_BUFFER_SIZE] | ||||||
|  |     if frac >= 0.75: | ||||||
|  |         frac = 1 | ||||||
|  |     elif frac >= 0.375: | ||||||
|  |         frac = 2 | ||||||
|  |     elif frac > 0.19: | ||||||
|  |         frac = 4 | ||||||
|  |     else: | ||||||
|  |         frac = 8 | ||||||
|  |     cg.add(lv_component.set_buffer_frac(int(frac))) | ||||||
|  |     cg.add(lv_component.set_full_refresh(config[df.CONF_FULL_REFRESH])) | ||||||
|  |  | ||||||
|  |     for font in helpers.esphome_fonts_used: | ||||||
|  |         await cg.get_variable(font) | ||||||
|  |         cg.new_Pvariable(ID(f"{font}_engine", True, type=FontEngine), MockObj(font)) | ||||||
|  |     default_font = config[df.CONF_DEFAULT_FONT] | ||||||
|  |     if default_font not in helpers.lv_fonts_used: | ||||||
|  |         add_define( | ||||||
|  |             "LV_FONT_CUSTOM_DECLARE", f"LV_FONT_DECLARE(*{df.DEFAULT_ESPHOME_FONT})" | ||||||
|  |         ) | ||||||
|  |         globfont_id = ID( | ||||||
|  |             df.DEFAULT_ESPHOME_FONT, | ||||||
|  |             True, | ||||||
|  |             type=lv_font_t.operator("ptr").operator("const"), | ||||||
|  |         ) | ||||||
|  |         cg.new_variable(globfont_id, MockObj(default_font)) | ||||||
|  |         add_define("LV_FONT_DEFAULT", df.DEFAULT_ESPHOME_FONT) | ||||||
|  |     else: | ||||||
|  |         add_define("LV_FONT_DEFAULT", default_font) | ||||||
|  |  | ||||||
|  |     with LvContext(): | ||||||
|  |         await set_obj_properties(lv_scr_act, config) | ||||||
|  |         await add_widgets(lv_scr_act, config) | ||||||
|  |     Widget.set_completed() | ||||||
|  |     await add_init_lambda(lv_component, LvContext.get_code()) | ||||||
|  |     for comp in helpers.lvgl_components_required: | ||||||
|  |         CORE.add_define(f"USE_LVGL_{comp.upper()}") | ||||||
|  |     for use in helpers.lv_uses: | ||||||
|  |         add_define(f"LV_USE_{use.upper()}") | ||||||
|  |     lv_conf_h_file = CORE.relative_src_path(LV_CONF_FILENAME) | ||||||
|  |     write_file_if_changed(lv_conf_h_file, generate_lv_conf_h()) | ||||||
|  |     CORE.add_build_flag("-DLV_CONF_H=1") | ||||||
|  |     CORE.add_build_flag(f'-DLV_CONF_PATH="{LV_CONF_FILENAME}"') | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def display_schema(config): | ||||||
|  |     value = cv.ensure_list(cv.use_id(Display))(config) | ||||||
|  |     return value or [cv.use_id(Display)(config)] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | FINAL_VALIDATE_SCHEMA = final_validation | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = ( | ||||||
|  |     cv.polling_component_schema("1s") | ||||||
|  |     .extend(obj_schema("obj")) | ||||||
|  |     .extend( | ||||||
|  |         { | ||||||
|  |             cv.GenerateID(CONF_ID): cv.declare_id(LvglComponent), | ||||||
|  |             cv.GenerateID(df.CONF_DISPLAYS): display_schema, | ||||||
|  |             cv.Optional(df.CONF_COLOR_DEPTH, default=16): cv.one_of(16), | ||||||
|  |             cv.Optional(df.CONF_DEFAULT_FONT, default="montserrat_14"): lvalid.lv_font, | ||||||
|  |             cv.Optional(df.CONF_FULL_REFRESH, default=False): cv.boolean, | ||||||
|  |             cv.Optional(CONF_BUFFER_SIZE, default="100%"): cv.percentage, | ||||||
|  |             cv.Optional(df.CONF_LOG_LEVEL, default="WARN"): cv.one_of( | ||||||
|  |                 *df.LOG_LEVELS, upper=True | ||||||
|  |             ), | ||||||
|  |             cv.Optional(df.CONF_BYTE_ORDER, default="big_endian"): cv.one_of( | ||||||
|  |                 "big_endian", "little_endian" | ||||||
|  |             ), | ||||||
|  |             cv.Optional(df.CONF_WIDGETS): cv.ensure_list(WIDGET_SCHEMA), | ||||||
|  |             cv.Optional(df.CONF_TRANSPARENCY_KEY, default=0x000400): lvalid.lv_color, | ||||||
|  |         } | ||||||
|  |     ) | ||||||
|  | ).add_extra(cv.has_at_least_one_key(CONF_PAGES, df.CONF_WIDGETS)) | ||||||
							
								
								
									
										487
									
								
								esphome/components/lvgl/defines.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										487
									
								
								esphome/components/lvgl/defines.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,487 @@ | |||||||
|  | """ | ||||||
|  | This is the base of the import tree for LVGL. It contains constant definitions used elsewhere. | ||||||
|  | Constants already defined in esphome.const are not duplicated here and must be imported where used. | ||||||
|  |  | ||||||
|  | """ | ||||||
|  |  | ||||||
|  | from esphome import codegen as cg, config_validation as cv | ||||||
|  | from esphome.core import ID, Lambda | ||||||
|  | from esphome.cpp_types import uint32 | ||||||
|  | from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor | ||||||
|  |  | ||||||
|  | from .lvcode import ConstantLiteral | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LValidator: | ||||||
|  |     """ | ||||||
|  |     A validator for a particular type used in LVGL. Usable in configs as a validator, also | ||||||
|  |     has `process()` to convert a value during code generation | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     def __init__(self, validator, rtype, idtype=None, idexpr=None, retmapper=None): | ||||||
|  |         self.validator = validator | ||||||
|  |         self.rtype = rtype | ||||||
|  |         self.idtype = idtype | ||||||
|  |         self.idexpr = idexpr | ||||||
|  |         self.retmapper = retmapper | ||||||
|  |  | ||||||
|  |     def __call__(self, value): | ||||||
|  |         if isinstance(value, cv.Lambda): | ||||||
|  |             return cv.returning_lambda(value) | ||||||
|  |         if self.idtype is not None and isinstance(value, ID): | ||||||
|  |             return cv.use_id(self.idtype)(value) | ||||||
|  |         return self.validator(value) | ||||||
|  |  | ||||||
|  |     async def process(self, value, args=()): | ||||||
|  |         if value is None: | ||||||
|  |             return None | ||||||
|  |         if isinstance(value, Lambda): | ||||||
|  |             return cg.RawExpression( | ||||||
|  |                 f"{await cg.process_lambda(value, args, return_type=self.rtype)}()" | ||||||
|  |             ) | ||||||
|  |         if self.idtype is not None and isinstance(value, ID): | ||||||
|  |             return cg.RawExpression(f"{value}->{self.idexpr}") | ||||||
|  |         if self.retmapper is not None: | ||||||
|  |             return self.retmapper(value) | ||||||
|  |         return cg.safe_exp(value) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LvConstant(LValidator): | ||||||
|  |     """ | ||||||
|  |     Allow one of a list of choices, mapped to upper case, and prepend the choice with the prefix. | ||||||
|  |     It's also permitted to include the prefix in the value | ||||||
|  |     The property `one_of` has the single case validator, and `several_of` allows a list of constants. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     def __init__(self, prefix: str, *choices): | ||||||
|  |         self.prefix = prefix | ||||||
|  |         self.choices = choices | ||||||
|  |         prefixed_choices = [prefix + v for v in choices] | ||||||
|  |         prefixed_validator = cv.one_of(*prefixed_choices, upper=True) | ||||||
|  |  | ||||||
|  |         @schema_extractor("one_of") | ||||||
|  |         def validator(value): | ||||||
|  |             if value == SCHEMA_EXTRACT: | ||||||
|  |                 return self.choices | ||||||
|  |             if isinstance(value, str) and value.startswith(self.prefix): | ||||||
|  |                 return prefixed_validator(value) | ||||||
|  |             return self.prefix + cv.one_of(*choices, upper=True)(value) | ||||||
|  |  | ||||||
|  |         super().__init__(validator, rtype=uint32) | ||||||
|  |         self.one_of = LValidator(validator, uint32, retmapper=self.mapper) | ||||||
|  |         self.several_of = LValidator( | ||||||
|  |             cv.ensure_list(self.one_of), uint32, retmapper=self.mapper | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     def mapper(self, value, args=()): | ||||||
|  |         if isinstance(value, list): | ||||||
|  |             value = "|".join(value) | ||||||
|  |         return ConstantLiteral(value) | ||||||
|  |  | ||||||
|  |     def extend(self, *choices): | ||||||
|  |         """ | ||||||
|  |         Extend an LVCconstant with additional choices. | ||||||
|  |         :param choices: The extra choices | ||||||
|  |         :return: A new LVConstant instance | ||||||
|  |         """ | ||||||
|  |         return LvConstant(self.prefix, *(self.choices + choices)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # Widgets | ||||||
|  | CONF_LABEL = "label" | ||||||
|  |  | ||||||
|  | # Parts | ||||||
|  | CONF_MAIN = "main" | ||||||
|  | CONF_SCROLLBAR = "scrollbar" | ||||||
|  | CONF_INDICATOR = "indicator" | ||||||
|  | CONF_KNOB = "knob" | ||||||
|  | CONF_SELECTED = "selected" | ||||||
|  | CONF_ITEMS = "items" | ||||||
|  | CONF_TICKS = "ticks" | ||||||
|  | CONF_TICK_STYLE = "tick_style" | ||||||
|  | CONF_CURSOR = "cursor" | ||||||
|  | CONF_TEXTAREA_PLACEHOLDER = "textarea_placeholder" | ||||||
|  |  | ||||||
|  | LV_FONTS = list(f"montserrat_{s}" for s in range(8, 50, 2)) + [ | ||||||
|  |     "dejavu_16_persian_hebrew", | ||||||
|  |     "simsun_16_cjk", | ||||||
|  |     "unscii_8", | ||||||
|  |     "unscii_16", | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | LV_EVENT = { | ||||||
|  |     "PRESS": "PRESSED", | ||||||
|  |     "SHORT_CLICK": "SHORT_CLICKED", | ||||||
|  |     "LONG_PRESS": "LONG_PRESSED", | ||||||
|  |     "LONG_PRESS_REPEAT": "LONG_PRESSED_REPEAT", | ||||||
|  |     "CLICK": "CLICKED", | ||||||
|  |     "RELEASE": "RELEASED", | ||||||
|  |     "SCROLL_BEGIN": "SCROLL_BEGIN", | ||||||
|  |     "SCROLL_END": "SCROLL_END", | ||||||
|  |     "SCROLL": "SCROLL", | ||||||
|  |     "FOCUS": "FOCUSED", | ||||||
|  |     "DEFOCUS": "DEFOCUSED", | ||||||
|  |     "READY": "READY", | ||||||
|  |     "CANCEL": "CANCEL", | ||||||
|  | } | ||||||
|  |  | ||||||
|  | LV_EVENT_TRIGGERS = tuple(f"on_{x.lower()}" for x in LV_EVENT) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | LV_ANIM = LvConstant( | ||||||
|  |     "LV_SCR_LOAD_ANIM_", | ||||||
|  |     "NONE", | ||||||
|  |     "OVER_LEFT", | ||||||
|  |     "OVER_RIGHT", | ||||||
|  |     "OVER_TOP", | ||||||
|  |     "OVER_BOTTOM", | ||||||
|  |     "MOVE_LEFT", | ||||||
|  |     "MOVE_RIGHT", | ||||||
|  |     "MOVE_TOP", | ||||||
|  |     "MOVE_BOTTOM", | ||||||
|  |     "FADE_IN", | ||||||
|  |     "FADE_OUT", | ||||||
|  |     "OUT_LEFT", | ||||||
|  |     "OUT_RIGHT", | ||||||
|  |     "OUT_TOP", | ||||||
|  |     "OUT_BOTTOM", | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | LOG_LEVELS = ( | ||||||
|  |     "TRACE", | ||||||
|  |     "INFO", | ||||||
|  |     "WARN", | ||||||
|  |     "ERROR", | ||||||
|  |     "USER", | ||||||
|  |     "NONE", | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | LV_LONG_MODES = LvConstant( | ||||||
|  |     "LV_LABEL_LONG_", | ||||||
|  |     "WRAP", | ||||||
|  |     "DOT", | ||||||
|  |     "SCROLL", | ||||||
|  |     "SCROLL_CIRCULAR", | ||||||
|  |     "CLIP", | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | STATES = ( | ||||||
|  |     "default", | ||||||
|  |     "checked", | ||||||
|  |     "focused", | ||||||
|  |     "focus_key", | ||||||
|  |     "edited", | ||||||
|  |     "hovered", | ||||||
|  |     "pressed", | ||||||
|  |     "scrolled", | ||||||
|  |     "disabled", | ||||||
|  |     "user_1", | ||||||
|  |     "user_2", | ||||||
|  |     "user_3", | ||||||
|  |     "user_4", | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | PARTS = ( | ||||||
|  |     CONF_MAIN, | ||||||
|  |     CONF_SCROLLBAR, | ||||||
|  |     CONF_INDICATOR, | ||||||
|  |     CONF_KNOB, | ||||||
|  |     CONF_SELECTED, | ||||||
|  |     CONF_ITEMS, | ||||||
|  |     CONF_TICKS, | ||||||
|  |     CONF_CURSOR, | ||||||
|  |     CONF_TEXTAREA_PLACEHOLDER, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | KEYBOARD_MODES = LvConstant( | ||||||
|  |     "LV_KEYBOARD_MODE_", | ||||||
|  |     "TEXT_LOWER", | ||||||
|  |     "TEXT_UPPER", | ||||||
|  |     "SPECIAL", | ||||||
|  |     "NUMBER", | ||||||
|  | ) | ||||||
|  | ROLLER_MODES = LvConstant("LV_ROLLER_MODE_", "NORMAL", "INFINITE") | ||||||
|  | DIRECTIONS = LvConstant("LV_DIR_", "LEFT", "RIGHT", "BOTTOM", "TOP") | ||||||
|  | TILE_DIRECTIONS = DIRECTIONS.extend("HOR", "VER", "ALL") | ||||||
|  | CHILD_ALIGNMENTS = LvConstant( | ||||||
|  |     "LV_ALIGN_", | ||||||
|  |     "TOP_LEFT", | ||||||
|  |     "TOP_MID", | ||||||
|  |     "TOP_RIGHT", | ||||||
|  |     "LEFT_MID", | ||||||
|  |     "CENTER", | ||||||
|  |     "RIGHT_MID", | ||||||
|  |     "BOTTOM_LEFT", | ||||||
|  |     "BOTTOM_MID", | ||||||
|  |     "BOTTOM_RIGHT", | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | SIBLING_ALIGNMENTS = LvConstant( | ||||||
|  |     "LV_ALIGN_", | ||||||
|  |     "OUT_LEFT_TOP", | ||||||
|  |     "OUT_TOP_LEFT", | ||||||
|  |     "OUT_TOP_MID", | ||||||
|  |     "OUT_TOP_RIGHT", | ||||||
|  |     "OUT_RIGHT_TOP", | ||||||
|  |     "OUT_LEFT_MID", | ||||||
|  |     "OUT_RIGHT_MID", | ||||||
|  |     "OUT_LEFT_BOTTOM", | ||||||
|  |     "OUT_BOTTOM_LEFT", | ||||||
|  |     "OUT_BOTTOM_MID", | ||||||
|  |     "OUT_BOTTOM_RIGHT", | ||||||
|  |     "OUT_RIGHT_BOTTOM", | ||||||
|  | ) | ||||||
|  | ALIGN_ALIGNMENTS = CHILD_ALIGNMENTS.extend(*SIBLING_ALIGNMENTS.choices) | ||||||
|  |  | ||||||
|  | FLEX_FLOWS = LvConstant( | ||||||
|  |     "LV_FLEX_FLOW_", | ||||||
|  |     "ROW", | ||||||
|  |     "COLUMN", | ||||||
|  |     "ROW_WRAP", | ||||||
|  |     "COLUMN_WRAP", | ||||||
|  |     "ROW_REVERSE", | ||||||
|  |     "COLUMN_REVERSE", | ||||||
|  |     "ROW_WRAP_REVERSE", | ||||||
|  |     "COLUMN_WRAP_REVERSE", | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | OBJ_FLAGS = ( | ||||||
|  |     "hidden", | ||||||
|  |     "clickable", | ||||||
|  |     "click_focusable", | ||||||
|  |     "checkable", | ||||||
|  |     "scrollable", | ||||||
|  |     "scroll_elastic", | ||||||
|  |     "scroll_momentum", | ||||||
|  |     "scroll_one", | ||||||
|  |     "scroll_chain_hor", | ||||||
|  |     "scroll_chain_ver", | ||||||
|  |     "scroll_chain", | ||||||
|  |     "scroll_on_focus", | ||||||
|  |     "scroll_with_arrow", | ||||||
|  |     "snappable", | ||||||
|  |     "press_lock", | ||||||
|  |     "event_bubble", | ||||||
|  |     "gesture_bubble", | ||||||
|  |     "adv_hittest", | ||||||
|  |     "ignore_layout", | ||||||
|  |     "floating", | ||||||
|  |     "overflow_visible", | ||||||
|  |     "layout_1", | ||||||
|  |     "layout_2", | ||||||
|  |     "widget_1", | ||||||
|  |     "widget_2", | ||||||
|  |     "user_1", | ||||||
|  |     "user_2", | ||||||
|  |     "user_3", | ||||||
|  |     "user_4", | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | ARC_MODES = LvConstant("LV_ARC_MODE_", "NORMAL", "REVERSE", "SYMMETRICAL") | ||||||
|  | BAR_MODES = LvConstant("LV_BAR_MODE_", "NORMAL", "SYMMETRICAL", "RANGE") | ||||||
|  |  | ||||||
|  | BTNMATRIX_CTRLS = ( | ||||||
|  |     "HIDDEN", | ||||||
|  |     "NO_REPEAT", | ||||||
|  |     "DISABLED", | ||||||
|  |     "CHECKABLE", | ||||||
|  |     "CHECKED", | ||||||
|  |     "CLICK_TRIG", | ||||||
|  |     "POPOVER", | ||||||
|  |     "RECOLOR", | ||||||
|  |     "CUSTOM_1", | ||||||
|  |     "CUSTOM_2", | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | LV_BASE_ALIGNMENTS = ( | ||||||
|  |     "START", | ||||||
|  |     "CENTER", | ||||||
|  |     "END", | ||||||
|  | ) | ||||||
|  | LV_CELL_ALIGNMENTS = LvConstant( | ||||||
|  |     "LV_GRID_ALIGN_", | ||||||
|  |     *LV_BASE_ALIGNMENTS, | ||||||
|  | ) | ||||||
|  | LV_GRID_ALIGNMENTS = LV_CELL_ALIGNMENTS.extend( | ||||||
|  |     "STRETCH", | ||||||
|  |     "SPACE_EVENLY", | ||||||
|  |     "SPACE_AROUND", | ||||||
|  |     "SPACE_BETWEEN", | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | LV_FLEX_ALIGNMENTS = LvConstant( | ||||||
|  |     "LV_FLEX_ALIGN_", | ||||||
|  |     *LV_BASE_ALIGNMENTS, | ||||||
|  |     "SPACE_EVENLY", | ||||||
|  |     "SPACE_AROUND", | ||||||
|  |     "SPACE_BETWEEN", | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | LV_MENU_MODES = LvConstant( | ||||||
|  |     "LV_MENU_HEADER_", | ||||||
|  |     "TOP_FIXED", | ||||||
|  |     "TOP_UNFIXED", | ||||||
|  |     "BOTTOM_FIXED", | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | LV_CHART_TYPES = ( | ||||||
|  |     "NONE", | ||||||
|  |     "LINE", | ||||||
|  |     "BAR", | ||||||
|  |     "SCATTER", | ||||||
|  | ) | ||||||
|  | LV_CHART_AXES = ( | ||||||
|  |     "PRIMARY_Y", | ||||||
|  |     "SECONDARY_Y", | ||||||
|  |     "PRIMARY_X", | ||||||
|  |     "SECONDARY_X", | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | CONF_ACCEPTED_CHARS = "accepted_chars" | ||||||
|  | CONF_ADJUSTABLE = "adjustable" | ||||||
|  | CONF_ALIGN = "align" | ||||||
|  | CONF_ALIGN_TO = "align_to" | ||||||
|  | CONF_ANGLE_RANGE = "angle_range" | ||||||
|  | CONF_ANIMATED = "animated" | ||||||
|  | CONF_ANIMATION = "animation" | ||||||
|  | CONF_ANTIALIAS = "antialias" | ||||||
|  | CONF_ARC_LENGTH = "arc_length" | ||||||
|  | CONF_AUTO_START = "auto_start" | ||||||
|  | CONF_BACKGROUND_STYLE = "background_style" | ||||||
|  | CONF_DECIMAL_PLACES = "decimal_places" | ||||||
|  | CONF_COLUMN = "column" | ||||||
|  | CONF_DIGITS = "digits" | ||||||
|  | CONF_DISP_BG_COLOR = "disp_bg_color" | ||||||
|  | CONF_DISP_BG_IMAGE = "disp_bg_image" | ||||||
|  | CONF_BODY = "body" | ||||||
|  | CONF_BUTTONS = "buttons" | ||||||
|  | CONF_BYTE_ORDER = "byte_order" | ||||||
|  | CONF_CHANGE_RATE = "change_rate" | ||||||
|  | CONF_CLOSE_BUTTON = "close_button" | ||||||
|  | CONF_COLOR_DEPTH = "color_depth" | ||||||
|  | CONF_COLOR_END = "color_end" | ||||||
|  | CONF_COLOR_START = "color_start" | ||||||
|  | CONF_CONTROL = "control" | ||||||
|  | CONF_DEFAULT = "default" | ||||||
|  | CONF_DEFAULT_FONT = "default_font" | ||||||
|  | CONF_DIR = "dir" | ||||||
|  | CONF_DISPLAYS = "displays" | ||||||
|  | CONF_END_ANGLE = "end_angle" | ||||||
|  | CONF_END_VALUE = "end_value" | ||||||
|  | CONF_ENTER_BUTTON = "enter_button" | ||||||
|  | CONF_ENTRIES = "entries" | ||||||
|  | CONF_FLAGS = "flags" | ||||||
|  | CONF_FLEX_FLOW = "flex_flow" | ||||||
|  | CONF_FLEX_ALIGN_MAIN = "flex_align_main" | ||||||
|  | CONF_FLEX_ALIGN_CROSS = "flex_align_cross" | ||||||
|  | CONF_FLEX_ALIGN_TRACK = "flex_align_track" | ||||||
|  | CONF_FLEX_GROW = "flex_grow" | ||||||
|  | CONF_FULL_REFRESH = "full_refresh" | ||||||
|  | CONF_GRID_CELL_ROW_POS = "grid_cell_row_pos" | ||||||
|  | CONF_GRID_CELL_COLUMN_POS = "grid_cell_column_pos" | ||||||
|  | CONF_GRID_CELL_ROW_SPAN = "grid_cell_row_span" | ||||||
|  | CONF_GRID_CELL_COLUMN_SPAN = "grid_cell_column_span" | ||||||
|  | CONF_GRID_CELL_X_ALIGN = "grid_cell_x_align" | ||||||
|  | CONF_GRID_CELL_Y_ALIGN = "grid_cell_y_align" | ||||||
|  | CONF_GRID_COLUMN_ALIGN = "grid_column_align" | ||||||
|  | CONF_GRID_COLUMNS = "grid_columns" | ||||||
|  | CONF_GRID_ROW_ALIGN = "grid_row_align" | ||||||
|  | CONF_GRID_ROWS = "grid_rows" | ||||||
|  | CONF_HEADER_MODE = "header_mode" | ||||||
|  | CONF_HOME = "home" | ||||||
|  | CONF_INDICATORS = "indicators" | ||||||
|  | CONF_KEY_CODE = "key_code" | ||||||
|  | CONF_LABEL_GAP = "label_gap" | ||||||
|  | CONF_LAYOUT = "layout" | ||||||
|  | CONF_LEFT_BUTTON = "left_button" | ||||||
|  | CONF_LINE_WIDTH = "line_width" | ||||||
|  | CONF_LOG_LEVEL = "log_level" | ||||||
|  | CONF_LONG_PRESS_TIME = "long_press_time" | ||||||
|  | CONF_LONG_PRESS_REPEAT_TIME = "long_press_repeat_time" | ||||||
|  | CONF_LVGL_ID = "lvgl_id" | ||||||
|  | CONF_LONG_MODE = "long_mode" | ||||||
|  | CONF_MAJOR = "major" | ||||||
|  | CONF_MSGBOXES = "msgboxes" | ||||||
|  | CONF_OBJ = "obj" | ||||||
|  | CONF_OFFSET_X = "offset_x" | ||||||
|  | CONF_OFFSET_Y = "offset_y" | ||||||
|  | CONF_ONE_LINE = "one_line" | ||||||
|  | CONF_ON_SELECT = "on_select" | ||||||
|  | CONF_ONE_CHECKED = "one_checked" | ||||||
|  | CONF_NEXT = "next" | ||||||
|  | CONF_PAGE_WRAP = "page_wrap" | ||||||
|  | CONF_PASSWORD_MODE = "password_mode" | ||||||
|  | CONF_PIVOT_X = "pivot_x" | ||||||
|  | CONF_PIVOT_Y = "pivot_y" | ||||||
|  | CONF_PLACEHOLDER_TEXT = "placeholder_text" | ||||||
|  | CONF_POINTS = "points" | ||||||
|  | CONF_PREVIOUS = "previous" | ||||||
|  | CONF_REPEAT_COUNT = "repeat_count" | ||||||
|  | CONF_R_MOD = "r_mod" | ||||||
|  | CONF_RECOLOR = "recolor" | ||||||
|  | CONF_RIGHT_BUTTON = "right_button" | ||||||
|  | CONF_ROLLOVER = "rollover" | ||||||
|  | CONF_ROOT_BACK_BTN = "root_back_btn" | ||||||
|  | CONF_ROWS = "rows" | ||||||
|  | CONF_SCALES = "scales" | ||||||
|  | CONF_SCALE_LINES = "scale_lines" | ||||||
|  | CONF_SCROLLBAR_MODE = "scrollbar_mode" | ||||||
|  | CONF_SELECTED_INDEX = "selected_index" | ||||||
|  | CONF_SHOW_SNOW = "show_snow" | ||||||
|  | CONF_SPIN_TIME = "spin_time" | ||||||
|  | CONF_SRC = "src" | ||||||
|  | CONF_START_ANGLE = "start_angle" | ||||||
|  | CONF_START_VALUE = "start_value" | ||||||
|  | CONF_STATES = "states" | ||||||
|  | CONF_STRIDE = "stride" | ||||||
|  | CONF_STYLE = "style" | ||||||
|  | CONF_STYLE_ID = "style_id" | ||||||
|  | CONF_SKIP = "skip" | ||||||
|  | CONF_SYMBOL = "symbol" | ||||||
|  | CONF_TAB_ID = "tab_id" | ||||||
|  | CONF_TABS = "tabs" | ||||||
|  | CONF_TEXT = "text" | ||||||
|  | CONF_TILE = "tile" | ||||||
|  | CONF_TILE_ID = "tile_id" | ||||||
|  | CONF_TILES = "tiles" | ||||||
|  | CONF_TITLE = "title" | ||||||
|  | CONF_TOP_LAYER = "top_layer" | ||||||
|  | CONF_TRANSPARENCY_KEY = "transparency_key" | ||||||
|  | CONF_THEME = "theme" | ||||||
|  | CONF_VISIBLE_ROW_COUNT = "visible_row_count" | ||||||
|  | CONF_WIDGET = "widget" | ||||||
|  | CONF_WIDGETS = "widgets" | ||||||
|  | CONF_X = "x" | ||||||
|  | CONF_Y = "y" | ||||||
|  | CONF_ZOOM = "zoom" | ||||||
|  |  | ||||||
|  | # Keypad keys | ||||||
|  |  | ||||||
|  | LV_KEYS = LvConstant( | ||||||
|  |     "LV_KEY_", | ||||||
|  |     "UP", | ||||||
|  |     "DOWN", | ||||||
|  |     "RIGHT", | ||||||
|  |     "LEFT", | ||||||
|  |     "ESC", | ||||||
|  |     "DEL", | ||||||
|  |     "BACKSPACE", | ||||||
|  |     "ENTER", | ||||||
|  |     "NEXT", | ||||||
|  |     "PREV", | ||||||
|  |     "HOME", | ||||||
|  |     "END", | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # list of widgets and the parts allowed | ||||||
|  | WIDGET_PARTS = { | ||||||
|  |     CONF_LABEL: (CONF_MAIN, CONF_SCROLLBAR, CONF_SELECTED), | ||||||
|  |     CONF_OBJ: (CONF_MAIN,), | ||||||
|  | } | ||||||
|  |  | ||||||
|  | DEFAULT_ESPHOME_FONT = "esphome_lv_default_font" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def join_enums(enums, prefix=""): | ||||||
|  |     return "|".join(f"(int){prefix}{e.upper()}" for e in enums) | ||||||
							
								
								
									
										76
									
								
								esphome/components/lvgl/font.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								esphome/components/lvgl/font.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | |||||||
|  | #include "lvgl_esphome.h" | ||||||
|  |  | ||||||
|  | #ifdef USE_LVGL_FONT | ||||||
|  | namespace esphome { | ||||||
|  | namespace lvgl { | ||||||
|  |  | ||||||
|  | static const uint8_t *get_glyph_bitmap(const lv_font_t *font, uint32_t unicode_letter) { | ||||||
|  |   auto *fe = (FontEngine *) font->dsc; | ||||||
|  |   const auto *gd = fe->get_glyph_data(unicode_letter); | ||||||
|  |   if (gd == nullptr) | ||||||
|  |     return nullptr; | ||||||
|  |   // esph_log_d(TAG, "Returning bitmap @  %X", (uint32_t)gd->data); | ||||||
|  |  | ||||||
|  |   return gd->data; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | static bool get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, uint32_t unicode_letter, uint32_t next) { | ||||||
|  |   auto *fe = (FontEngine *) font->dsc; | ||||||
|  |   const auto *gd = fe->get_glyph_data(unicode_letter); | ||||||
|  |   if (gd == nullptr) | ||||||
|  |     return false; | ||||||
|  |   dsc->adv_w = gd->offset_x + gd->width; | ||||||
|  |   dsc->ofs_x = gd->offset_x; | ||||||
|  |   dsc->ofs_y = fe->height - gd->height - gd->offset_y - fe->baseline; | ||||||
|  |   dsc->box_w = gd->width; | ||||||
|  |   dsc->box_h = gd->height; | ||||||
|  |   dsc->is_placeholder = 0; | ||||||
|  |   dsc->bpp = fe->bpp; | ||||||
|  |   return true; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | FontEngine::FontEngine(font::Font *esp_font) : font_(esp_font) { | ||||||
|  |   this->bpp = esp_font->get_bpp(); | ||||||
|  |   this->lv_font_.dsc = this; | ||||||
|  |   this->lv_font_.line_height = this->height = esp_font->get_height(); | ||||||
|  |   this->lv_font_.base_line = this->baseline = this->lv_font_.line_height - esp_font->get_baseline(); | ||||||
|  |   this->lv_font_.get_glyph_dsc = get_glyph_dsc_cb; | ||||||
|  |   this->lv_font_.get_glyph_bitmap = get_glyph_bitmap; | ||||||
|  |   this->lv_font_.subpx = LV_FONT_SUBPX_NONE; | ||||||
|  |   this->lv_font_.underline_position = -1; | ||||||
|  |   this->lv_font_.underline_thickness = 1; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const lv_font_t *FontEngine::get_lv_font() { return &this->lv_font_; } | ||||||
|  |  | ||||||
|  | const font::GlyphData *FontEngine::get_glyph_data(uint32_t unicode_letter) { | ||||||
|  |   if (unicode_letter == last_letter_) | ||||||
|  |     return this->last_data_; | ||||||
|  |   uint8_t unicode[5]; | ||||||
|  |   memset(unicode, 0, sizeof unicode); | ||||||
|  |   if (unicode_letter > 0xFFFF) { | ||||||
|  |     unicode[0] = 0xF0 + ((unicode_letter >> 18) & 0x7); | ||||||
|  |     unicode[1] = 0x80 + ((unicode_letter >> 12) & 0x3F); | ||||||
|  |     unicode[2] = 0x80 + ((unicode_letter >> 6) & 0x3F); | ||||||
|  |     unicode[3] = 0x80 + (unicode_letter & 0x3F); | ||||||
|  |   } else if (unicode_letter > 0x7FF) { | ||||||
|  |     unicode[0] = 0xE0 + ((unicode_letter >> 12) & 0xF); | ||||||
|  |     unicode[1] = 0x80 + ((unicode_letter >> 6) & 0x3F); | ||||||
|  |     unicode[2] = 0x80 + (unicode_letter & 0x3F); | ||||||
|  |   } else if (unicode_letter > 0x7F) { | ||||||
|  |     unicode[0] = 0xC0 + ((unicode_letter >> 6) & 0x1F); | ||||||
|  |     unicode[1] = 0x80 + (unicode_letter & 0x3F); | ||||||
|  |   } else { | ||||||
|  |     unicode[0] = unicode_letter; | ||||||
|  |   } | ||||||
|  |   int match_length; | ||||||
|  |   int glyph_n = this->font_->match_next_glyph(unicode, &match_length); | ||||||
|  |   if (glyph_n < 0) | ||||||
|  |     return nullptr; | ||||||
|  |   this->last_data_ = this->font_->get_glyphs()[glyph_n].get_glyph_data(); | ||||||
|  |   this->last_letter_ = unicode_letter; | ||||||
|  |   return this->last_data_; | ||||||
|  | } | ||||||
|  | }  // namespace lvgl | ||||||
|  | }  // namespace esphome | ||||||
|  | #endif  // USES_LVGL_FONT | ||||||
							
								
								
									
										70
									
								
								esphome/components/lvgl/helpers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								esphome/components/lvgl/helpers.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,70 @@ | |||||||
|  | import re | ||||||
|  |  | ||||||
|  | from esphome import config_validation as cv | ||||||
|  | from esphome.config import Config | ||||||
|  | from esphome.const import CONF_ARGS, CONF_FORMAT | ||||||
|  | from esphome.core import CORE, ID | ||||||
|  | from esphome.yaml_util import ESPHomeDataBase | ||||||
|  |  | ||||||
|  | lv_uses = { | ||||||
|  |     "USER_DATA", | ||||||
|  |     "LOG", | ||||||
|  |     "STYLE", | ||||||
|  |     "FONT_PLACEHOLDER", | ||||||
|  |     "THEME_DEFAULT", | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def add_lv_use(*names): | ||||||
|  |     for name in names: | ||||||
|  |         lv_uses.add(name) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | lv_fonts_used = set() | ||||||
|  | esphome_fonts_used = set() | ||||||
|  | REQUIRED_COMPONENTS = {} | ||||||
|  | lvgl_components_required = set() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def validate_printf(value): | ||||||
|  |     cfmt = r""" | ||||||
|  |     (                                  # start of capture group 1 | ||||||
|  |     %                                  # literal "%" | ||||||
|  |     (?:[-+0 #]{0,5})                   # optional flags | ||||||
|  |     (?:\d+|\*)?                        # width | ||||||
|  |     (?:\.(?:\d+|\*))?                  # precision | ||||||
|  |     (?:h|l|ll|w|I|I32|I64)?            # size | ||||||
|  |     [cCdiouxXeEfgGaAnpsSZ]             # type | ||||||
|  |     ) | ||||||
|  |     """  # noqa | ||||||
|  |     matches = re.findall(cfmt, value[CONF_FORMAT], flags=re.X) | ||||||
|  |     if len(matches) != len(value[CONF_ARGS]): | ||||||
|  |         raise cv.Invalid( | ||||||
|  |             f"Found {len(matches)} printf-patterns ({', '.join(matches)}), but {len(value[CONF_ARGS])} args were given!" | ||||||
|  |         ) | ||||||
|  |     return value | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_line_marks(value) -> list: | ||||||
|  |     """ | ||||||
|  |     If possible, return a preprocessor directive to identify the line number where the given id was defined. | ||||||
|  |     :param id: The id in question | ||||||
|  |     :return: A list containing zero or more line directives | ||||||
|  |     """ | ||||||
|  |     path = None | ||||||
|  |     if isinstance(value, ESPHomeDataBase): | ||||||
|  |         path = value.esp_range | ||||||
|  |     elif isinstance(value, ID) and isinstance(CORE.config, Config): | ||||||
|  |         path = CORE.config.get_path_for_id(value)[:-1] | ||||||
|  |         path = CORE.config.get_deepest_document_range_for_path(path) | ||||||
|  |     if path is None: | ||||||
|  |         return [] | ||||||
|  |     return [path.start_mark.as_line_directive] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def requires_component(comp): | ||||||
|  |     def validator(value): | ||||||
|  |         lvgl_components_required.add(comp) | ||||||
|  |         return cv.requires_component(comp)(value) | ||||||
|  |  | ||||||
|  |     return validator | ||||||
							
								
								
									
										34
									
								
								esphome/components/lvgl/label.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								esphome/components/lvgl/label.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | |||||||
|  | import esphome.config_validation as cv | ||||||
|  |  | ||||||
|  | from .defines import CONF_LABEL, CONF_LONG_MODE, CONF_RECOLOR, CONF_TEXT, LV_LONG_MODES | ||||||
|  | from .lv_validation import lv_bool, lv_text | ||||||
|  | from .schemas import TEXT_SCHEMA | ||||||
|  | from .types import lv_label_t | ||||||
|  | from .widget import Widget, WidgetType | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LabelType(WidgetType): | ||||||
|  |     def __init__(self): | ||||||
|  |         super().__init__( | ||||||
|  |             CONF_LABEL, | ||||||
|  |             TEXT_SCHEMA.extend( | ||||||
|  |                 { | ||||||
|  |                     cv.Optional(CONF_RECOLOR): lv_bool, | ||||||
|  |                     cv.Optional(CONF_LONG_MODE): LV_LONG_MODES.one_of, | ||||||
|  |                 } | ||||||
|  |             ), | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def w_type(self): | ||||||
|  |         return lv_label_t | ||||||
|  |  | ||||||
|  |     async def to_code(self, w: Widget, config): | ||||||
|  |         """For a text object, create and set text""" | ||||||
|  |         if value := config.get(CONF_TEXT): | ||||||
|  |             w.set_property(CONF_TEXT, await lv_text.process(value)) | ||||||
|  |         w.set_property(CONF_LONG_MODE, config) | ||||||
|  |         w.set_property(CONF_RECOLOR, config) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | label_spec = LabelType() | ||||||
							
								
								
									
										170
									
								
								esphome/components/lvgl/lv_validation.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								esphome/components/lvgl/lv_validation.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,170 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  | from esphome.components.binary_sensor import BinarySensor | ||||||
|  | from esphome.components.color import ColorStruct | ||||||
|  | from esphome.components.font import Font | ||||||
|  | from esphome.components.sensor import Sensor | ||||||
|  | from esphome.components.text_sensor import TextSensor | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.const import CONF_ARGS, CONF_COLOR, CONF_FORMAT | ||||||
|  | from esphome.core import HexInt | ||||||
|  | from esphome.cpp_generator import MockObj | ||||||
|  | from esphome.helpers import cpp_string_escape | ||||||
|  | from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor | ||||||
|  |  | ||||||
|  | from . import types as ty | ||||||
|  | from .defines import LV_FONTS, LValidator, LvConstant | ||||||
|  | from .helpers import ( | ||||||
|  |     esphome_fonts_used, | ||||||
|  |     lv_fonts_used, | ||||||
|  |     lvgl_components_required, | ||||||
|  |     requires_component, | ||||||
|  | ) | ||||||
|  | from .lvcode import ConstantLiteral, lv_expr | ||||||
|  | from .types import lv_font_t | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @schema_extractor("one_of") | ||||||
|  | def color(value): | ||||||
|  |     if value == SCHEMA_EXTRACT: | ||||||
|  |         return ["hex color value", "color ID"] | ||||||
|  |     if isinstance(value, int): | ||||||
|  |         return value | ||||||
|  |     return cv.use_id(ColorStruct)(value) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def color_retmapper(value): | ||||||
|  |     if isinstance(value, cv.Lambda): | ||||||
|  |         return cv.returning_lambda(value) | ||||||
|  |     if isinstance(value, int): | ||||||
|  |         hexval = HexInt(value) | ||||||
|  |         return lv_expr.color_hex(hexval) | ||||||
|  |     # Must be an id | ||||||
|  |     lvgl_components_required.add(CONF_COLOR) | ||||||
|  |     return lv_expr.color_from(MockObj(value)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def pixels_or_percent(value): | ||||||
|  |     """A length in one axis - either a number (pixels) or a percentage""" | ||||||
|  |     if value == SCHEMA_EXTRACT: | ||||||
|  |         return ["pixels", "..%"] | ||||||
|  |     if isinstance(value, int): | ||||||
|  |         return str(cv.int_(value)) | ||||||
|  |     # Will throw an exception if not a percentage. | ||||||
|  |     return f"lv_pct({int(cv.percentage(value) * 100)})" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def zoom(value): | ||||||
|  |     value = cv.float_range(0.1, 10.0)(value) | ||||||
|  |     return int(value * 256) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def angle(value): | ||||||
|  |     """ | ||||||
|  |     Validation for an angle in degrees, converted to an integer representing 0.1deg units | ||||||
|  |     :param value: The input in the range 0..360 | ||||||
|  |     :return: An angle in 1/10 degree units. | ||||||
|  |     """ | ||||||
|  |     return int(cv.float_range(0.0, 360.0)(cv.angle(value)) * 10) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @schema_extractor("one_of") | ||||||
|  | def size(value): | ||||||
|  |     """A size in one axis - one of "size_content", a number (pixels) or a percentage""" | ||||||
|  |     if value == SCHEMA_EXTRACT: | ||||||
|  |         return ["size_content", "pixels", "..%"] | ||||||
|  |     if isinstance(value, str) and value.lower().endswith("px"): | ||||||
|  |         value = cv.int_(value[:-2]) | ||||||
|  |     if isinstance(value, str) and not value.endswith("%"): | ||||||
|  |         if value.upper() == "SIZE_CONTENT": | ||||||
|  |             return "LV_SIZE_CONTENT" | ||||||
|  |         raise cv.Invalid("must be 'size_content', a pixel position or a percentage") | ||||||
|  |     if isinstance(value, int): | ||||||
|  |         return str(cv.int_(value)) | ||||||
|  |     # Will throw an exception if not a percentage. | ||||||
|  |     return f"lv_pct({int(cv.percentage(value) * 100)})" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @schema_extractor("one_of") | ||||||
|  | def opacity(value): | ||||||
|  |     consts = LvConstant("LV_OPA_", "TRANSP", "COVER") | ||||||
|  |     if value == SCHEMA_EXTRACT: | ||||||
|  |         return consts.choices | ||||||
|  |     value = cv.Any(cv.percentage, consts.one_of)(value) | ||||||
|  |     if isinstance(value, float): | ||||||
|  |         return int(value * 255) | ||||||
|  |     return value | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def stop_value(value): | ||||||
|  |     return cv.int_range(0, 255)(value) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | lv_color = LValidator(color, ty.lv_color_t, retmapper=color_retmapper) | ||||||
|  | lv_bool = LValidator(cv.boolean, cg.bool_, BinarySensor, "get_state()") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def lvms_validator_(value): | ||||||
|  |     if value == "never": | ||||||
|  |         value = "2147483647ms" | ||||||
|  |     return cv.positive_time_period_milliseconds(value) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | lv_milliseconds = LValidator( | ||||||
|  |     lvms_validator_, | ||||||
|  |     cg.int32, | ||||||
|  |     retmapper=lambda x: x.total_milliseconds, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TextValidator(LValidator): | ||||||
|  |     def __init__(self): | ||||||
|  |         super().__init__( | ||||||
|  |             cv.string, | ||||||
|  |             cg.const_char_ptr, | ||||||
|  |             TextSensor, | ||||||
|  |             "get_state().c_str()", | ||||||
|  |             lambda s: cg.safe_exp(f"{s}"), | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     def __call__(self, value): | ||||||
|  |         if isinstance(value, dict): | ||||||
|  |             return value | ||||||
|  |         return super().__call__(value) | ||||||
|  |  | ||||||
|  |     async def process(self, value, args=()): | ||||||
|  |         if isinstance(value, dict): | ||||||
|  |             args = [str(x) for x in value[CONF_ARGS]] | ||||||
|  |             arg_expr = cg.RawExpression(",".join(args)) | ||||||
|  |             format_str = cpp_string_escape(value[CONF_FORMAT]) | ||||||
|  |             return f"str_sprintf({format_str}, {arg_expr}).c_str()" | ||||||
|  |         return await super().process(value, args) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | lv_text = TextValidator() | ||||||
|  | lv_float = LValidator(cv.float_, cg.float_, Sensor, "get_state()") | ||||||
|  | lv_int = LValidator(cv.int_, cg.int_, Sensor, "get_state()") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LvFont(LValidator): | ||||||
|  |     def __init__(self): | ||||||
|  |         def lv_builtin_font(value): | ||||||
|  |             fontval = cv.one_of(*LV_FONTS, lower=True)(value) | ||||||
|  |             lv_fonts_used.add(fontval) | ||||||
|  |             return "&lv_font_" + fontval | ||||||
|  |  | ||||||
|  |         def validator(value): | ||||||
|  |             if value == SCHEMA_EXTRACT: | ||||||
|  |                 return LV_FONTS | ||||||
|  |             if isinstance(value, str) and value.lower() in LV_FONTS: | ||||||
|  |                 return lv_builtin_font(value) | ||||||
|  |             fontval = cv.use_id(Font)(value) | ||||||
|  |             esphome_fonts_used.add(fontval) | ||||||
|  |             return requires_component("font")(f"{fontval}_engine->get_lv_font()") | ||||||
|  |  | ||||||
|  |         super().__init__(validator, lv_font_t) | ||||||
|  |  | ||||||
|  |     async def process(self, value, args=()): | ||||||
|  |         return ConstantLiteral(value) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | lv_font = LvFont() | ||||||
							
								
								
									
										237
									
								
								esphome/components/lvgl/lvcode.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										237
									
								
								esphome/components/lvgl/lvcode.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,237 @@ | |||||||
|  | import abc | ||||||
|  | import logging | ||||||
|  | from typing import Union | ||||||
|  |  | ||||||
|  | from esphome import codegen as cg | ||||||
|  | from esphome.core import ID, Lambda | ||||||
|  | from esphome.cpp_generator import ( | ||||||
|  |     AssignmentExpression, | ||||||
|  |     CallExpression, | ||||||
|  |     Expression, | ||||||
|  |     LambdaExpression, | ||||||
|  |     Literal, | ||||||
|  |     MockObj, | ||||||
|  |     RawExpression, | ||||||
|  |     RawStatement, | ||||||
|  |     SafeExpType, | ||||||
|  |     Statement, | ||||||
|  |     VariableDeclarationExpression, | ||||||
|  |     statement, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | from .helpers import get_line_marks | ||||||
|  |  | ||||||
|  | _LOGGER = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class CodeContext(abc.ABC): | ||||||
|  |     """ | ||||||
|  |     A class providing a context for code generation. Generated code will be added to the | ||||||
|  |     current context. A new context will stack on the current context, and restore it | ||||||
|  |     when done. Used with the `with` statement. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     code_context = None | ||||||
|  |  | ||||||
|  |     @abc.abstractmethod | ||||||
|  |     def add(self, expression: Union[Expression, Statement]): | ||||||
|  |         pass | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def append(expression: Union[Expression, Statement]): | ||||||
|  |         if CodeContext.code_context is not None: | ||||||
|  |             CodeContext.code_context.add(expression) | ||||||
|  |         return expression | ||||||
|  |  | ||||||
|  |     def __init__(self): | ||||||
|  |         self.previous: Union[CodeContext | None] = None | ||||||
|  |  | ||||||
|  |     def __enter__(self): | ||||||
|  |         self.previous = CodeContext.code_context | ||||||
|  |         CodeContext.code_context = self | ||||||
|  |  | ||||||
|  |     def __exit__(self, *args): | ||||||
|  |         CodeContext.code_context = self.previous | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class MainContext(CodeContext): | ||||||
|  |     """ | ||||||
|  |     Code generation into the main() function | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     def add(self, expression: Union[Expression, Statement]): | ||||||
|  |         return cg.add(expression) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LvContext(CodeContext): | ||||||
|  |     """ | ||||||
|  |     Code generation into the LVGL initialisation code (called in `setup()`) | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     lv_init_code: list["Statement"] = [] | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def lv_add(expression: Union[Expression, Statement]): | ||||||
|  |         if isinstance(expression, Expression): | ||||||
|  |             expression = statement(expression) | ||||||
|  |         if not isinstance(expression, Statement): | ||||||
|  |             raise ValueError( | ||||||
|  |                 f"Add '{expression}' must be expression or statement, not {type(expression)}" | ||||||
|  |             ) | ||||||
|  |         LvContext.lv_init_code.append(expression) | ||||||
|  |         _LOGGER.debug("LV Adding: %s", expression) | ||||||
|  |         return expression | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def get_code(): | ||||||
|  |         code = [] | ||||||
|  |         for exp in LvContext.lv_init_code: | ||||||
|  |             text = str(statement(exp)) | ||||||
|  |             text = text.rstrip() | ||||||
|  |             code.append(text) | ||||||
|  |         return "\n".join(code) + "\n\n" | ||||||
|  |  | ||||||
|  |     def add(self, expression: Union[Expression, Statement]): | ||||||
|  |         return LvContext.lv_add(expression) | ||||||
|  |  | ||||||
|  |     def set_style(self, prop): | ||||||
|  |         return MockObj("lv_set_style_{prop}", "") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LambdaContext(CodeContext): | ||||||
|  |     """ | ||||||
|  |     A context that will accumlate code for use in a lambda. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     def __init__( | ||||||
|  |         self, | ||||||
|  |         parameters: list[tuple[SafeExpType, str]], | ||||||
|  |         return_type: SafeExpType = None, | ||||||
|  |     ): | ||||||
|  |         super().__init__() | ||||||
|  |         self.code_list: list[Statement] = [] | ||||||
|  |         self.parameters = parameters | ||||||
|  |         self.return_type = return_type | ||||||
|  |  | ||||||
|  |     def add(self, expression: Union[Expression, Statement]): | ||||||
|  |         self.code_list.append(expression) | ||||||
|  |         return expression | ||||||
|  |  | ||||||
|  |     async def code(self) -> LambdaExpression: | ||||||
|  |         code_text = [] | ||||||
|  |         for exp in self.code_list: | ||||||
|  |             text = str(statement(exp)) | ||||||
|  |             text = text.rstrip() | ||||||
|  |             code_text.append(text) | ||||||
|  |         return await cg.process_lambda( | ||||||
|  |             Lambda("\n".join(code_text) + "\n\n"), | ||||||
|  |             self.parameters, | ||||||
|  |             return_type=self.return_type, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocalVariable(MockObj): | ||||||
|  |     """ | ||||||
|  |     Create a local variable and enclose the code using it within a block. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     def __init__(self, name, type, modifier=None, rhs=None): | ||||||
|  |         base = ID(name, True, type) | ||||||
|  |         super().__init__(base, "") | ||||||
|  |         self.modifier = modifier | ||||||
|  |         self.rhs = rhs | ||||||
|  |  | ||||||
|  |     def __enter__(self): | ||||||
|  |         CodeContext.append(RawStatement("{")) | ||||||
|  |         CodeContext.append( | ||||||
|  |             VariableDeclarationExpression(self.base.type, self.modifier, self.base.id) | ||||||
|  |         ) | ||||||
|  |         if self.rhs is not None: | ||||||
|  |             CodeContext.append(AssignmentExpression(None, "", self.base, self.rhs)) | ||||||
|  |         return self.base | ||||||
|  |  | ||||||
|  |     def __exit__(self, *args): | ||||||
|  |         CodeContext.append(RawStatement("}")) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class MockLv: | ||||||
|  |     """ | ||||||
|  |     A mock object that can be used to generate LVGL calls. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     def __init__(self, base): | ||||||
|  |         self.base = base | ||||||
|  |  | ||||||
|  |     def __getattr__(self, attr: str) -> "MockLv": | ||||||
|  |         return MockLv(f"{self.base}{attr}") | ||||||
|  |  | ||||||
|  |     def append(self, expression): | ||||||
|  |         CodeContext.append(expression) | ||||||
|  |  | ||||||
|  |     def __call__(self, *args: SafeExpType) -> "MockObj": | ||||||
|  |         call = CallExpression(self.base, *args) | ||||||
|  |         result = MockObj(call, "") | ||||||
|  |         self.append(result) | ||||||
|  |         return result | ||||||
|  |  | ||||||
|  |     def __str__(self): | ||||||
|  |         return str(self.base) | ||||||
|  |  | ||||||
|  |     def __repr__(self): | ||||||
|  |         return f"MockLv<{str(self.base)}>" | ||||||
|  |  | ||||||
|  |     def call(self, prop, *args): | ||||||
|  |         call = CallExpression(RawExpression(f"{self.base}{prop}"), *args) | ||||||
|  |         result = MockObj(call, "") | ||||||
|  |         self.append(result) | ||||||
|  |         return result | ||||||
|  |  | ||||||
|  |     def cond_if(self, expression: Expression): | ||||||
|  |         CodeContext.append(RawExpression(f"if({expression}) {{")) | ||||||
|  |  | ||||||
|  |     def cond_else(self): | ||||||
|  |         CodeContext.append(RawExpression("} else {")) | ||||||
|  |  | ||||||
|  |     def cond_endif(self): | ||||||
|  |         CodeContext.append(RawExpression("}")) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LvExpr(MockLv): | ||||||
|  |     def __getattr__(self, attr: str) -> "MockLv": | ||||||
|  |         return LvExpr(f"{self.base}{attr}") | ||||||
|  |  | ||||||
|  |     def append(self, expression): | ||||||
|  |         pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # Top level mock for generic lv_ calls to be recorded | ||||||
|  | lv = MockLv("lv_") | ||||||
|  | # Just generate an expression | ||||||
|  | lv_expr = LvExpr("lv_") | ||||||
|  | # Mock for lv_obj_ calls | ||||||
|  | lv_obj = MockLv("lv_obj_") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # equivalent to cg.add() for the lvgl init context | ||||||
|  | def lv_add(expression: Union[Expression, Statement]): | ||||||
|  |     return CodeContext.append(expression) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def add_line_marks(where): | ||||||
|  |     for mark in get_line_marks(where): | ||||||
|  |         lv_add(cg.RawStatement(mark)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def lv_assign(target, expression): | ||||||
|  |     lv_add(RawExpression(f"{target} = {expression}")) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ConstantLiteral(Literal): | ||||||
|  |     __slots__ = ("constant",) | ||||||
|  |  | ||||||
|  |     def __init__(self, constant: str): | ||||||
|  |         super().__init__() | ||||||
|  |         self.constant = constant | ||||||
|  |  | ||||||
|  |     def __str__(self): | ||||||
|  |         return self.constant | ||||||
							
								
								
									
										129
									
								
								esphome/components/lvgl/lvgl_esphome.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								esphome/components/lvgl/lvgl_esphome.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,129 @@ | |||||||
|  | #include "esphome/core/defines.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  | #include "esphome/core/helpers.h" | ||||||
|  | #include "esphome/core/hal.h" | ||||||
|  | #include "lvgl_hal.h" | ||||||
|  | #include "lvgl_esphome.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace lvgl { | ||||||
|  | static const char *const TAG = "lvgl"; | ||||||
|  |  | ||||||
|  | lv_event_code_t lv_custom_event;  // NOLINT | ||||||
|  | void LvglComponent::dump_config() { ESP_LOGCONFIG(TAG, "LVGL:"); } | ||||||
|  | void LvglComponent::draw_buffer_(const lv_area_t *area, const uint8_t *ptr) { | ||||||
|  |   for (auto *display : this->displays_) { | ||||||
|  |     display->draw_pixels_at(area->x1, area->y1, lv_area_get_width(area), lv_area_get_height(area), ptr, | ||||||
|  |                             display::COLOR_ORDER_RGB, LV_BITNESS, LV_COLOR_16_SWAP); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void LvglComponent::flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) { | ||||||
|  |   auto now = millis(); | ||||||
|  |   this->draw_buffer_(area, (const uint8_t *) color_p); | ||||||
|  |   ESP_LOGV(TAG, "flush_cb, area=%d/%d, %d/%d took %dms", area->x1, area->y1, lv_area_get_width(area), | ||||||
|  |            lv_area_get_height(area), (int) (millis() - now)); | ||||||
|  |   lv_disp_flush_ready(disp_drv); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void LvglComponent::setup() { | ||||||
|  |   ESP_LOGCONFIG(TAG, "LVGL Setup starts"); | ||||||
|  | #if LV_USE_LOG | ||||||
|  |   lv_log_register_print_cb(log_cb); | ||||||
|  | #endif | ||||||
|  |   lv_init(); | ||||||
|  |   lv_custom_event = static_cast<lv_event_code_t>(lv_event_register_id()); | ||||||
|  |   auto *display = this->displays_[0]; | ||||||
|  |   size_t buffer_pixels = display->get_width() * display->get_height() / this->buffer_frac_; | ||||||
|  |   auto buf_bytes = buffer_pixels * LV_COLOR_DEPTH / 8; | ||||||
|  |   auto *buf = lv_custom_mem_alloc(buf_bytes); | ||||||
|  |   if (buf == nullptr) { | ||||||
|  |     ESP_LOGE(TAG, "Malloc failed to allocate %zu bytes", buf_bytes); | ||||||
|  |     this->mark_failed(); | ||||||
|  |     this->status_set_error("Memory allocation failure"); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   lv_disp_draw_buf_init(&this->draw_buf_, buf, nullptr, buffer_pixels); | ||||||
|  |   lv_disp_drv_init(&this->disp_drv_); | ||||||
|  |   this->disp_drv_.draw_buf = &this->draw_buf_; | ||||||
|  |   this->disp_drv_.user_data = this; | ||||||
|  |   this->disp_drv_.full_refresh = this->full_refresh_; | ||||||
|  |   this->disp_drv_.flush_cb = static_flush_cb; | ||||||
|  |   this->disp_drv_.rounder_cb = rounder_cb; | ||||||
|  |   switch (display->get_rotation()) { | ||||||
|  |     case display::DISPLAY_ROTATION_0_DEGREES: | ||||||
|  |       break; | ||||||
|  |     case display::DISPLAY_ROTATION_90_DEGREES: | ||||||
|  |       this->disp_drv_.sw_rotate = true; | ||||||
|  |       this->disp_drv_.rotated = LV_DISP_ROT_90; | ||||||
|  |       break; | ||||||
|  |     case display::DISPLAY_ROTATION_180_DEGREES: | ||||||
|  |       this->disp_drv_.sw_rotate = true; | ||||||
|  |       this->disp_drv_.rotated = LV_DISP_ROT_180; | ||||||
|  |       break; | ||||||
|  |     case display::DISPLAY_ROTATION_270_DEGREES: | ||||||
|  |       this->disp_drv_.sw_rotate = true; | ||||||
|  |       this->disp_drv_.rotated = LV_DISP_ROT_270; | ||||||
|  |       break; | ||||||
|  |   } | ||||||
|  |   display->set_rotation(display::DISPLAY_ROTATION_0_DEGREES); | ||||||
|  |   this->disp_drv_.hor_res = (lv_coord_t) display->get_width(); | ||||||
|  |   this->disp_drv_.ver_res = (lv_coord_t) display->get_height(); | ||||||
|  |   ESP_LOGV(TAG, "sw_rotate = %d, rotated=%d", this->disp_drv_.sw_rotate, this->disp_drv_.rotated); | ||||||
|  |   this->disp_ = lv_disp_drv_register(&this->disp_drv_); | ||||||
|  |   for (const auto &v : this->init_lambdas_) | ||||||
|  |     v(this->disp_); | ||||||
|  |   lv_disp_trig_activity(this->disp_); | ||||||
|  |   ESP_LOGCONFIG(TAG, "LVGL Setup complete"); | ||||||
|  | } | ||||||
|  | }  // namespace lvgl | ||||||
|  | }  // namespace esphome | ||||||
|  |  | ||||||
|  | size_t lv_millis(void) { return esphome::millis(); } | ||||||
|  |  | ||||||
|  | #if defined(USE_HOST) || defined(USE_RP2040) || defined(USE_ESP8266) | ||||||
|  | void *lv_custom_mem_alloc(size_t size) { | ||||||
|  |   auto *ptr = malloc(size);  // NOLINT | ||||||
|  |   if (ptr == nullptr) { | ||||||
|  |     esphome::ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size); | ||||||
|  |   } | ||||||
|  |   return ptr; | ||||||
|  | } | ||||||
|  | void lv_custom_mem_free(void *ptr) { return free(ptr); }                            // NOLINT | ||||||
|  | void *lv_custom_mem_realloc(void *ptr, size_t size) { return realloc(ptr, size); }  // NOLINT | ||||||
|  | #else | ||||||
|  | static unsigned cap_bits = MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT;  // NOLINT | ||||||
|  |  | ||||||
|  | void *lv_custom_mem_alloc(size_t size) { | ||||||
|  |   void *ptr; | ||||||
|  |   ptr = heap_caps_malloc(size, cap_bits); | ||||||
|  |   if (ptr == nullptr) { | ||||||
|  |     cap_bits = MALLOC_CAP_8BIT; | ||||||
|  |     ptr = heap_caps_malloc(size, cap_bits); | ||||||
|  |   } | ||||||
|  |   if (ptr == nullptr) { | ||||||
|  |     esphome::ESP_LOGE(esphome::lvgl::TAG, "Failed to allocate %zu bytes", size); | ||||||
|  |     return nullptr; | ||||||
|  |   } | ||||||
|  | #ifdef ESPHOME_LOG_HAS_VERBOSE | ||||||
|  |   esphome::ESP_LOGV(esphome::lvgl::TAG, "allocate %zu - > %p", size, ptr); | ||||||
|  | #endif | ||||||
|  |   return ptr; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void lv_custom_mem_free(void *ptr) { | ||||||
|  | #ifdef ESPHOME_LOG_HAS_VERBOSE | ||||||
|  |   esphome::ESP_LOGV(esphome::lvgl::TAG, "free %p", ptr); | ||||||
|  | #endif | ||||||
|  |   if (ptr == nullptr) | ||||||
|  |     return; | ||||||
|  |   heap_caps_free(ptr); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void *lv_custom_mem_realloc(void *ptr, size_t size) { | ||||||
|  | #ifdef ESPHOME_LOG_HAS_VERBOSE | ||||||
|  |   esphome::ESP_LOGV(esphome::lvgl::TAG, "realloc %p: %zu", ptr, size); | ||||||
|  | #endif | ||||||
|  |   return heap_caps_realloc(ptr, size, cap_bits); | ||||||
|  | } | ||||||
|  | #endif | ||||||
							
								
								
									
										119
									
								
								esphome/components/lvgl/lvgl_esphome.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								esphome/components/lvgl/lvgl_esphome.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,119 @@ | |||||||
|  | #pragma once | ||||||
|  | #include "esphome/core/defines.h" | ||||||
|  | #ifdef USE_LVGL | ||||||
|  |  | ||||||
|  | // required for clang-tidy | ||||||
|  | #ifndef LV_CONF_H | ||||||
|  | #define LV_CONF_SKIP 1  // NOLINT | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  | #include "esphome/components/display/display.h" | ||||||
|  | #include "esphome/components/display/display_color_utils.h" | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  | #include "esphome/core/hal.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  | #include <lvgl.h> | ||||||
|  | #include <vector> | ||||||
|  |  | ||||||
|  | #ifdef USE_LVGL_FONT | ||||||
|  | #include "esphome/components/font/font.h" | ||||||
|  | #endif | ||||||
|  | namespace esphome { | ||||||
|  | namespace lvgl { | ||||||
|  |  | ||||||
|  | extern lv_event_code_t lv_custom_event;  // NOLINT | ||||||
|  | #ifdef USE_LVGL_COLOR | ||||||
|  | static lv_color_t lv_color_from(Color color) { return lv_color_make(color.red, color.green, color.blue); } | ||||||
|  | #endif | ||||||
|  | #if LV_COLOR_DEPTH == 16 | ||||||
|  | static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BITNESS_565; | ||||||
|  | #elif LV_COLOR_DEPTH == 32 | ||||||
|  | static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BITNESS_888; | ||||||
|  | #else | ||||||
|  | static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BITNESS_332; | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  | // Parent class for things that wrap an LVGL object | ||||||
|  | class LvCompound { | ||||||
|  |  public: | ||||||
|  |   virtual void set_obj(lv_obj_t *lv_obj) { this->obj = lv_obj; } | ||||||
|  |   lv_obj_t *obj{}; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | using LvLambdaType = std::function<void(lv_obj_t *)>; | ||||||
|  | using set_value_lambda_t = std::function<void(float)>; | ||||||
|  | using event_callback_t = void(_lv_event_t *); | ||||||
|  | using text_lambda_t = std::function<const char *()>; | ||||||
|  |  | ||||||
|  | #ifdef USE_LVGL_FONT | ||||||
|  | class FontEngine { | ||||||
|  |  public: | ||||||
|  |   FontEngine(font::Font *esp_font); | ||||||
|  |   const lv_font_t *get_lv_font(); | ||||||
|  |  | ||||||
|  |   const font::GlyphData *get_glyph_data(uint32_t unicode_letter); | ||||||
|  |   uint16_t baseline{}; | ||||||
|  |   uint16_t height{}; | ||||||
|  |   uint8_t bpp{}; | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   font::Font *font_{}; | ||||||
|  |   uint32_t last_letter_{}; | ||||||
|  |   const font::GlyphData *last_data_{}; | ||||||
|  |   lv_font_t lv_font_{}; | ||||||
|  | }; | ||||||
|  | #endif  // USE_LVGL_FONT | ||||||
|  |  | ||||||
|  | class LvglComponent : public PollingComponent { | ||||||
|  |   constexpr static const char *const TAG = "lvgl"; | ||||||
|  |  | ||||||
|  |  public: | ||||||
|  |   static void static_flush_cb(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p) { | ||||||
|  |     reinterpret_cast<LvglComponent *>(disp_drv->user_data)->flush_cb_(disp_drv, area, color_p); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   float get_setup_priority() const override { return setup_priority::PROCESSOR; } | ||||||
|  |   static void log_cb(const char *buf) { | ||||||
|  |     esp_log_printf_(ESPHOME_LOG_LEVEL_INFO, TAG, 0, "%.*s", (int) strlen(buf) - 1, buf); | ||||||
|  |   } | ||||||
|  |   static void rounder_cb(lv_disp_drv_t *disp_drv, lv_area_t *area) { | ||||||
|  |     // make sure all coordinates are even | ||||||
|  |     if (area->x1 & 1) | ||||||
|  |       area->x1--; | ||||||
|  |     if (!(area->x2 & 1)) | ||||||
|  |       area->x2++; | ||||||
|  |     if (area->y1 & 1) | ||||||
|  |       area->y1--; | ||||||
|  |     if (!(area->y2 & 1)) | ||||||
|  |       area->y2++; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void loop() override { lv_timer_handler_run_in_period(5); } | ||||||
|  |   void setup() override; | ||||||
|  |  | ||||||
|  |   void update() override {} | ||||||
|  |  | ||||||
|  |   void add_display(display::Display *display) { this->displays_.push_back(display); } | ||||||
|  |   void add_init_lambda(const std::function<void(lv_disp_t *)> &lamb) { this->init_lambdas_.push_back(lamb); } | ||||||
|  |   void dump_config() override; | ||||||
|  |   void set_full_refresh(bool full_refresh) { this->full_refresh_ = full_refresh; } | ||||||
|  |   void set_buffer_frac(size_t frac) { this->buffer_frac_ = frac; } | ||||||
|  |   lv_disp_t *get_disp() { return this->disp_; } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   void draw_buffer_(const lv_area_t *area, const uint8_t *ptr); | ||||||
|  |   void flush_cb_(lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p); | ||||||
|  |   std::vector<display::Display *> displays_{}; | ||||||
|  |   lv_disp_draw_buf_t draw_buf_{}; | ||||||
|  |   lv_disp_drv_t disp_drv_{}; | ||||||
|  |   lv_disp_t *disp_{}; | ||||||
|  |  | ||||||
|  |   std::vector<std::function<void(lv_disp_t *)>> init_lambdas_; | ||||||
|  |   size_t buffer_frac_{1}; | ||||||
|  |   bool full_refresh_{}; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace lvgl | ||||||
|  | }  // namespace esphome | ||||||
|  |  | ||||||
|  | #endif | ||||||
							
								
								
									
										21
									
								
								esphome/components/lvgl/lvgl_hal.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								esphome/components/lvgl/lvgl_hal.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | |||||||
|  | // | ||||||
|  | // Created by Clyde Stubbs on 20/9/2023. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #ifdef __cplusplus | ||||||
|  | #define EXTERNC extern "C" | ||||||
|  | #include <cstddef> | ||||||
|  | namespace esphome { | ||||||
|  | namespace lvgl {} | ||||||
|  | }  // namespace esphome | ||||||
|  | #else | ||||||
|  | #define EXTERNC extern | ||||||
|  | #include <stddef.h> | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  | EXTERNC size_t lv_millis(void); | ||||||
|  | EXTERNC void *lv_custom_mem_alloc(size_t size); | ||||||
|  | EXTERNC void lv_custom_mem_free(void *ptr); | ||||||
|  | EXTERNC void *lv_custom_mem_realloc(void *ptr, size_t size); | ||||||
							
								
								
									
										22
									
								
								esphome/components/lvgl/obj.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								esphome/components/lvgl/obj.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | |||||||
|  | from .defines import CONF_OBJ | ||||||
|  | from .types import lv_obj_t | ||||||
|  | from .widget import WidgetType | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ObjType(WidgetType): | ||||||
|  |     """ | ||||||
|  |     The base LVGL object. All other widgets inherit from this. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     def __init__(self): | ||||||
|  |         super().__init__(CONF_OBJ, schema={}, modify_schema={}) | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def w_type(self): | ||||||
|  |         return lv_obj_t | ||||||
|  |  | ||||||
|  |     async def to_code(self, w, config): | ||||||
|  |         return [] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | obj_spec = ObjType() | ||||||
							
								
								
									
										260
									
								
								esphome/components/lvgl/schemas.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										260
									
								
								esphome/components/lvgl/schemas.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,260 @@ | |||||||
|  | from esphome import config_validation as cv | ||||||
|  | from esphome.const import CONF_ARGS, CONF_FORMAT, CONF_ID, CONF_STATE, CONF_TYPE | ||||||
|  | from esphome.schema_extractors import SCHEMA_EXTRACT | ||||||
|  |  | ||||||
|  | from . import defines as df, lv_validation as lvalid, types as ty | ||||||
|  | from .defines import WIDGET_PARTS | ||||||
|  | from .helpers import ( | ||||||
|  |     REQUIRED_COMPONENTS, | ||||||
|  |     add_lv_use, | ||||||
|  |     requires_component, | ||||||
|  |     validate_printf, | ||||||
|  | ) | ||||||
|  | from .lv_validation import lv_font | ||||||
|  | from .types import WIDGET_TYPES, get_widget_type | ||||||
|  |  | ||||||
|  | # A schema for text properties | ||||||
|  | TEXT_SCHEMA = cv.Schema( | ||||||
|  |     { | ||||||
|  |         cv.Optional(df.CONF_TEXT): cv.Any( | ||||||
|  |             cv.All( | ||||||
|  |                 cv.Schema( | ||||||
|  |                     { | ||||||
|  |                         cv.Required(CONF_FORMAT): cv.string, | ||||||
|  |                         cv.Optional(CONF_ARGS, default=list): cv.ensure_list( | ||||||
|  |                             cv.lambda_ | ||||||
|  |                         ), | ||||||
|  |                     }, | ||||||
|  |                 ), | ||||||
|  |                 validate_printf, | ||||||
|  |             ), | ||||||
|  |             lvalid.lv_text, | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | # All LVGL styles and their validators | ||||||
|  | STYLE_PROPS = { | ||||||
|  |     "align": df.CHILD_ALIGNMENTS.one_of, | ||||||
|  |     "arc_opa": lvalid.opacity, | ||||||
|  |     "arc_color": lvalid.lv_color, | ||||||
|  |     "arc_rounded": lvalid.lv_bool, | ||||||
|  |     "arc_width": cv.positive_int, | ||||||
|  |     "anim_time": lvalid.lv_milliseconds, | ||||||
|  |     "bg_color": lvalid.lv_color, | ||||||
|  |     "bg_grad_color": lvalid.lv_color, | ||||||
|  |     "bg_dither_mode": df.LvConstant("LV_DITHER_", "NONE", "ORDERED", "ERR_DIFF").one_of, | ||||||
|  |     "bg_grad_dir": df.LvConstant("LV_GRAD_DIR_", "NONE", "HOR", "VER").one_of, | ||||||
|  |     "bg_grad_stop": lvalid.stop_value, | ||||||
|  |     "bg_img_opa": lvalid.opacity, | ||||||
|  |     "bg_img_recolor": lvalid.lv_color, | ||||||
|  |     "bg_img_recolor_opa": lvalid.opacity, | ||||||
|  |     "bg_main_stop": lvalid.stop_value, | ||||||
|  |     "bg_opa": lvalid.opacity, | ||||||
|  |     "border_color": lvalid.lv_color, | ||||||
|  |     "border_opa": lvalid.opacity, | ||||||
|  |     "border_post": lvalid.lv_bool, | ||||||
|  |     "border_side": df.LvConstant( | ||||||
|  |         "LV_BORDER_SIDE_", "NONE", "TOP", "BOTTOM", "LEFT", "RIGHT", "INTERNAL" | ||||||
|  |     ).several_of, | ||||||
|  |     "border_width": cv.positive_int, | ||||||
|  |     "clip_corner": lvalid.lv_bool, | ||||||
|  |     "height": lvalid.size, | ||||||
|  |     "img_recolor": lvalid.lv_color, | ||||||
|  |     "img_recolor_opa": lvalid.opacity, | ||||||
|  |     "line_width": cv.positive_int, | ||||||
|  |     "line_dash_width": cv.positive_int, | ||||||
|  |     "line_dash_gap": cv.positive_int, | ||||||
|  |     "line_rounded": lvalid.lv_bool, | ||||||
|  |     "line_color": lvalid.lv_color, | ||||||
|  |     "opa": lvalid.opacity, | ||||||
|  |     "opa_layered": lvalid.opacity, | ||||||
|  |     "outline_color": lvalid.lv_color, | ||||||
|  |     "outline_opa": lvalid.opacity, | ||||||
|  |     "outline_pad": lvalid.size, | ||||||
|  |     "outline_width": lvalid.size, | ||||||
|  |     "pad_all": lvalid.size, | ||||||
|  |     "pad_bottom": lvalid.size, | ||||||
|  |     "pad_column": lvalid.size, | ||||||
|  |     "pad_left": lvalid.size, | ||||||
|  |     "pad_right": lvalid.size, | ||||||
|  |     "pad_row": lvalid.size, | ||||||
|  |     "pad_top": lvalid.size, | ||||||
|  |     "shadow_color": lvalid.lv_color, | ||||||
|  |     "shadow_ofs_x": cv.int_, | ||||||
|  |     "shadow_ofs_y": cv.int_, | ||||||
|  |     "shadow_opa": lvalid.opacity, | ||||||
|  |     "shadow_spread": cv.int_, | ||||||
|  |     "shadow_width": cv.positive_int, | ||||||
|  |     "text_align": df.LvConstant( | ||||||
|  |         "LV_TEXT_ALIGN_", "LEFT", "CENTER", "RIGHT", "AUTO" | ||||||
|  |     ).one_of, | ||||||
|  |     "text_color": lvalid.lv_color, | ||||||
|  |     "text_decor": df.LvConstant( | ||||||
|  |         "LV_TEXT_DECOR_", "NONE", "UNDERLINE", "STRIKETHROUGH" | ||||||
|  |     ).several_of, | ||||||
|  |     "text_font": lv_font, | ||||||
|  |     "text_letter_space": cv.positive_int, | ||||||
|  |     "text_line_space": cv.positive_int, | ||||||
|  |     "text_opa": lvalid.opacity, | ||||||
|  |     "transform_angle": lvalid.angle, | ||||||
|  |     "transform_height": lvalid.pixels_or_percent, | ||||||
|  |     "transform_pivot_x": lvalid.pixels_or_percent, | ||||||
|  |     "transform_pivot_y": lvalid.pixels_or_percent, | ||||||
|  |     "transform_zoom": lvalid.zoom, | ||||||
|  |     "translate_x": lvalid.pixels_or_percent, | ||||||
|  |     "translate_y": lvalid.pixels_or_percent, | ||||||
|  |     "max_height": lvalid.pixels_or_percent, | ||||||
|  |     "max_width": lvalid.pixels_or_percent, | ||||||
|  |     "min_height": lvalid.pixels_or_percent, | ||||||
|  |     "min_width": lvalid.pixels_or_percent, | ||||||
|  |     "radius": cv.Any(lvalid.size, df.LvConstant("LV_RADIUS_", "CIRCLE").one_of), | ||||||
|  |     "width": lvalid.size, | ||||||
|  |     "x": lvalid.pixels_or_percent, | ||||||
|  |     "y": lvalid.pixels_or_percent, | ||||||
|  | } | ||||||
|  |  | ||||||
|  | # Complete object style schema | ||||||
|  | STYLE_SCHEMA = cv.Schema({cv.Optional(k): v for k, v in STYLE_PROPS.items()}).extend( | ||||||
|  |     { | ||||||
|  |         cv.Optional(df.CONF_SCROLLBAR_MODE): df.LvConstant( | ||||||
|  |             "LV_SCROLLBAR_MODE_", "OFF", "ON", "ACTIVE", "AUTO" | ||||||
|  |         ).one_of, | ||||||
|  |     } | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | # Object states. Top level properties apply to MAIN | ||||||
|  | STATE_SCHEMA = cv.Schema( | ||||||
|  |     {cv.Optional(state): STYLE_SCHEMA for state in df.STATES} | ||||||
|  | ).extend(STYLE_SCHEMA) | ||||||
|  | # Setting object states | ||||||
|  | SET_STATE_SCHEMA = cv.Schema( | ||||||
|  |     {cv.Optional(state): lvalid.lv_bool for state in df.STATES} | ||||||
|  | ) | ||||||
|  | # Setting object flags | ||||||
|  | FLAG_SCHEMA = cv.Schema({cv.Optional(flag): cv.boolean for flag in df.OBJ_FLAGS}) | ||||||
|  | FLAG_LIST = cv.ensure_list(df.LvConstant("LV_OBJ_FLAG_", *df.OBJ_FLAGS).one_of) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def part_schema(widget_type): | ||||||
|  |     """ | ||||||
|  |     Generate a schema for the various parts (e.g. main:, indicator:) of a widget type | ||||||
|  |     :param widget_type:  The type of widget to generate for | ||||||
|  |     :return: | ||||||
|  |     """ | ||||||
|  |     parts = WIDGET_PARTS.get(widget_type) | ||||||
|  |     if parts is None: | ||||||
|  |         parts = (df.CONF_MAIN,) | ||||||
|  |     return cv.Schema({cv.Optional(part): STATE_SCHEMA for part in parts}).extend( | ||||||
|  |         STATE_SCHEMA | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def obj_schema(widget_type: str): | ||||||
|  |     """ | ||||||
|  |     Create a schema for a widget type itself i.e. no allowance for children | ||||||
|  |     :param widget_type: | ||||||
|  |     :return: | ||||||
|  |     """ | ||||||
|  |     return ( | ||||||
|  |         part_schema(widget_type) | ||||||
|  |         .extend(FLAG_SCHEMA) | ||||||
|  |         .extend(ALIGN_TO_SCHEMA) | ||||||
|  |         .extend( | ||||||
|  |             cv.Schema( | ||||||
|  |                 { | ||||||
|  |                     cv.Optional(CONF_STATE): SET_STATE_SCHEMA, | ||||||
|  |                 } | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ALIGN_TO_SCHEMA = { | ||||||
|  |     cv.Optional(df.CONF_ALIGN_TO): cv.Schema( | ||||||
|  |         { | ||||||
|  |             cv.Required(CONF_ID): cv.use_id(ty.lv_obj_t), | ||||||
|  |             cv.Required(df.CONF_ALIGN): df.ALIGN_ALIGNMENTS.one_of, | ||||||
|  |             cv.Optional(df.CONF_X, default=0): lvalid.pixels_or_percent, | ||||||
|  |             cv.Optional(df.CONF_Y, default=0): lvalid.pixels_or_percent, | ||||||
|  |         } | ||||||
|  |     ) | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # A style schema that can include text | ||||||
|  | STYLED_TEXT_SCHEMA = cv.maybe_simple_value( | ||||||
|  |     STYLE_SCHEMA.extend(TEXT_SCHEMA), key=df.CONF_TEXT | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | ALL_STYLES = { | ||||||
|  |     **STYLE_PROPS, | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def container_validator(schema, widget_type): | ||||||
|  |     """ | ||||||
|  |     Create a validator for a container given the widget type | ||||||
|  |     :param schema: Base schema to extend | ||||||
|  |     :param widget_type: | ||||||
|  |     :return: | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     def validator(value): | ||||||
|  |         result = schema | ||||||
|  |         if w_sch := WIDGET_TYPES[widget_type].schema: | ||||||
|  |             result = result.extend(w_sch) | ||||||
|  |         if value and (layout := value.get(df.CONF_LAYOUT)): | ||||||
|  |             if not isinstance(layout, dict): | ||||||
|  |                 raise cv.Invalid("Layout value must be a dict") | ||||||
|  |             ltype = layout.get(CONF_TYPE) | ||||||
|  |             add_lv_use(ltype) | ||||||
|  |         if value == SCHEMA_EXTRACT: | ||||||
|  |             return result | ||||||
|  |         return result(value) | ||||||
|  |  | ||||||
|  |     return validator | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def container_schema(widget_type, extras=None): | ||||||
|  |     """ | ||||||
|  |     Create a schema for a container widget of a given type. All obj properties are available, plus | ||||||
|  |     the extras passed in, plus any defined for the specific widget being specified. | ||||||
|  |     :param widget_type:     The widget type, e.g. "img" | ||||||
|  |     :param extras:  Additional options to be made available, e.g. layout properties for children | ||||||
|  |     :return: The schema for this type of widget. | ||||||
|  |     """ | ||||||
|  |     lv_type = get_widget_type(widget_type) | ||||||
|  |     schema = obj_schema(widget_type).extend({cv.GenerateID(): cv.declare_id(lv_type)}) | ||||||
|  |     if extras: | ||||||
|  |         schema = schema.extend(extras) | ||||||
|  |     # Delayed evaluation for recursion | ||||||
|  |     return container_validator(schema, widget_type) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def widget_schema(widget_type, extras=None): | ||||||
|  |     """ | ||||||
|  |     Create a schema for a given widget type | ||||||
|  |     :param widget_type: The name of the widget | ||||||
|  |     :param extras: | ||||||
|  |     :return: | ||||||
|  |     """ | ||||||
|  |     validator = container_schema(widget_type, extras=extras) | ||||||
|  |     if required := REQUIRED_COMPONENTS.get(widget_type): | ||||||
|  |         validator = cv.All(validator, requires_component(required)) | ||||||
|  |     return cv.Exclusive(widget_type, df.CONF_WIDGETS), validator | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # All widget schemas must be defined before this is called. | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def any_widget_schema(extras=None): | ||||||
|  |     """ | ||||||
|  |     Generate schemas for all possible LVGL widgets. This is what implements the ability to have a list of any kind of | ||||||
|  |     widget under the widgets: key. | ||||||
|  |  | ||||||
|  |     :param extras: Additional schema to be applied to each generated one | ||||||
|  |     :return: | ||||||
|  |     """ | ||||||
|  |     return cv.Any(dict(widget_schema(wt, extras) for wt in WIDGET_PARTS)) | ||||||
							
								
								
									
										64
									
								
								esphome/components/lvgl/types.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								esphome/components/lvgl/types.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,64 @@ | |||||||
|  | from esphome import codegen as cg | ||||||
|  | from esphome.core import ID | ||||||
|  |  | ||||||
|  | from .defines import CONF_LABEL, CONF_OBJ, CONF_TEXT | ||||||
|  |  | ||||||
|  | uint16_t_ptr = cg.uint16.operator("ptr") | ||||||
|  | lvgl_ns = cg.esphome_ns.namespace("lvgl") | ||||||
|  | char_ptr = cg.global_ns.namespace("char").operator("ptr") | ||||||
|  | void_ptr = cg.void.operator("ptr") | ||||||
|  | LvglComponent = lvgl_ns.class_("LvglComponent", cg.PollingComponent) | ||||||
|  | lv_event_code_t = cg.global_ns.namespace("lv_event_code_t") | ||||||
|  | FontEngine = lvgl_ns.class_("FontEngine") | ||||||
|  | LvCompound = lvgl_ns.class_("LvCompound") | ||||||
|  | lv_font_t = cg.global_ns.class_("lv_font_t") | ||||||
|  | lv_style_t = cg.global_ns.struct("lv_style_t") | ||||||
|  | lv_pseudo_button_t = lvgl_ns.class_("LvPseudoButton") | ||||||
|  | lv_obj_base_t = cg.global_ns.class_("lv_obj_t", lv_pseudo_button_t) | ||||||
|  | lv_obj_t_ptr = lv_obj_base_t.operator("ptr") | ||||||
|  | lv_disp_t_ptr = cg.global_ns.struct("lv_disp_t").operator("ptr") | ||||||
|  | lv_color_t = cg.global_ns.struct("lv_color_t") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # this will be populated later, in __init__.py to avoid circular imports. | ||||||
|  | WIDGET_TYPES: dict = {} | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LvType(cg.MockObjClass): | ||||||
|  |     def __init__(self, *args, **kwargs): | ||||||
|  |         parens = kwargs.pop("parents", ()) | ||||||
|  |         super().__init__(*args, parents=parens + (lv_obj_base_t,)) | ||||||
|  |         self.args = kwargs.pop("largs", [(lv_obj_t_ptr, "obj")]) | ||||||
|  |         self.value = kwargs.pop("lvalue", lambda w: w.obj) | ||||||
|  |         self.has_on_value = kwargs.pop("has_on_value", False) | ||||||
|  |         self.value_property = None | ||||||
|  |  | ||||||
|  |     def get_arg_type(self): | ||||||
|  |         return self.args[0][0] if len(self.args) else None | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LvText(LvType): | ||||||
|  |     def __init__(self, *args, **kwargs): | ||||||
|  |         super().__init__( | ||||||
|  |             *args, | ||||||
|  |             largs=[(cg.std_string, "text")], | ||||||
|  |             lvalue=lambda w: w.get_property("text")[0], | ||||||
|  |             **kwargs, | ||||||
|  |         ) | ||||||
|  |         self.value_property = CONF_TEXT | ||||||
|  |  | ||||||
|  |  | ||||||
|  | lv_obj_t = LvType("lv_obj_t") | ||||||
|  | lv_label_t = LvText("lv_label_t") | ||||||
|  |  | ||||||
|  | LV_TYPES = { | ||||||
|  |     CONF_LABEL: lv_label_t, | ||||||
|  |     CONF_OBJ: lv_obj_t, | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_widget_type(typestr: str) -> LvType: | ||||||
|  |     return LV_TYPES[typestr] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | CUSTOM_EVENT = ID("lv_custom_event", False, type=lv_event_code_t) | ||||||
							
								
								
									
										347
									
								
								esphome/components/lvgl/widget.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										347
									
								
								esphome/components/lvgl/widget.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,347 @@ | |||||||
|  | import sys | ||||||
|  | from typing import Any | ||||||
|  |  | ||||||
|  | from esphome import codegen as cg, config_validation as cv | ||||||
|  | from esphome.config_validation import Invalid | ||||||
|  | from esphome.const import CONF_GROUP, CONF_ID, CONF_STATE | ||||||
|  | from esphome.core import ID, TimePeriod | ||||||
|  | from esphome.coroutine import FakeAwaitable | ||||||
|  | from esphome.cpp_generator import MockObjClass | ||||||
|  |  | ||||||
|  | from .defines import ( | ||||||
|  |     CONF_DEFAULT, | ||||||
|  |     CONF_MAIN, | ||||||
|  |     CONF_SCROLLBAR_MODE, | ||||||
|  |     CONF_WIDGETS, | ||||||
|  |     OBJ_FLAGS, | ||||||
|  |     PARTS, | ||||||
|  |     STATES, | ||||||
|  |     LValidator, | ||||||
|  |     join_enums, | ||||||
|  | ) | ||||||
|  | from .helpers import add_lv_use | ||||||
|  | from .lvcode import ConstantLiteral, add_line_marks, lv, lv_add, lv_assign, lv_obj | ||||||
|  | from .schemas import ALL_STYLES | ||||||
|  | from .types import WIDGET_TYPES, LvCompound, lv_obj_t | ||||||
|  |  | ||||||
|  | EVENT_LAMB = "event_lamb__" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class WidgetType: | ||||||
|  |     """ | ||||||
|  |     Describes a type of Widget, e.g. "bar" or "line" | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     def __init__(self, name, schema=None, modify_schema=None): | ||||||
|  |         """ | ||||||
|  |         :param name: The widget name, e.g. "bar" | ||||||
|  |         :param schema: The config schema for defining a widget | ||||||
|  |         :param modify_schema: A schema to update the widget | ||||||
|  |         """ | ||||||
|  |         self.name = name | ||||||
|  |         self.schema = schema or {} | ||||||
|  |         if modify_schema is None: | ||||||
|  |             self.modify_schema = schema | ||||||
|  |         else: | ||||||
|  |             self.modify_schema = modify_schema | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def animated(self): | ||||||
|  |         return False | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def w_type(self): | ||||||
|  |         """ | ||||||
|  |         Get the type associated with this widget | ||||||
|  |         :return: | ||||||
|  |         """ | ||||||
|  |         return lv_obj_t | ||||||
|  |  | ||||||
|  |     def is_compound(self): | ||||||
|  |         return self.w_type.inherits_from(LvCompound) | ||||||
|  |  | ||||||
|  |     async def to_code(self, w, config: dict): | ||||||
|  |         """ | ||||||
|  |         Generate code for a given widget | ||||||
|  |         :param w: The widget | ||||||
|  |         :param config: Its configuration | ||||||
|  |         :return: Generated code as a list of text lines | ||||||
|  |         """ | ||||||
|  |         raise NotImplementedError(f"No to_code defined for {self.name}") | ||||||
|  |  | ||||||
|  |     def obj_creator(self, parent: MockObjClass, config: dict): | ||||||
|  |         """ | ||||||
|  |         Create an instance of the widget type | ||||||
|  |         :param parent: The parent to which it should be attached | ||||||
|  |         :param config:  Its configuration | ||||||
|  |         :return: Generated code as a single text line | ||||||
|  |         """ | ||||||
|  |         return f"lv_{self.name}_create({parent})" | ||||||
|  |  | ||||||
|  |     def get_uses(self): | ||||||
|  |         """ | ||||||
|  |         Get a list of other widgets used by this one | ||||||
|  |         :return: | ||||||
|  |         """ | ||||||
|  |         return () | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LvScrActType(WidgetType): | ||||||
|  |     """ | ||||||
|  |     A "widget" representing the active screen. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     def __init__(self): | ||||||
|  |         super().__init__("lv_scr_act()") | ||||||
|  |  | ||||||
|  |     def obj_creator(self, parent: MockObjClass, config: dict): | ||||||
|  |         return [] | ||||||
|  |  | ||||||
|  |     async def to_code(self, w, config: dict): | ||||||
|  |         return [] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Widget: | ||||||
|  |     """ | ||||||
|  |     Represents a Widget. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     widgets_completed = False | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def set_completed(): | ||||||
|  |         Widget.widgets_completed = True | ||||||
|  |  | ||||||
|  |     def __init__(self, var, wtype: WidgetType, config: dict = None, parent=None): | ||||||
|  |         self.var = var | ||||||
|  |         self.type = wtype | ||||||
|  |         self.config = config | ||||||
|  |         self.scale = 1.0 | ||||||
|  |         self.step = 1.0 | ||||||
|  |         self.range_from = -sys.maxsize | ||||||
|  |         self.range_to = sys.maxsize | ||||||
|  |         self.parent = parent | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def create(name, var, wtype: WidgetType, config: dict = None, parent=None): | ||||||
|  |         w = Widget(var, wtype, config, parent) | ||||||
|  |         if name is not None: | ||||||
|  |             widget_map[name] = w | ||||||
|  |         return w | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def obj(self): | ||||||
|  |         if self.type.is_compound(): | ||||||
|  |             return f"{self.var}->obj" | ||||||
|  |         return self.var | ||||||
|  |  | ||||||
|  |     def add_state(self, *args): | ||||||
|  |         return lv_obj.add_state(self.obj, *args) | ||||||
|  |  | ||||||
|  |     def clear_state(self, *args): | ||||||
|  |         return lv_obj.clear_state(self.obj, *args) | ||||||
|  |  | ||||||
|  |     def add_flag(self, *args): | ||||||
|  |         return lv_obj.add_flag(self.obj, *args) | ||||||
|  |  | ||||||
|  |     def clear_flag(self, *args): | ||||||
|  |         return lv_obj.clear_flag(self.obj, *args) | ||||||
|  |  | ||||||
|  |     def set_property(self, prop, value, animated: bool = None, ltype=None): | ||||||
|  |         if isinstance(value, dict): | ||||||
|  |             value = value.get(prop) | ||||||
|  |         if value is None: | ||||||
|  |             return | ||||||
|  |         if isinstance(value, TimePeriod): | ||||||
|  |             value = value.total_milliseconds | ||||||
|  |         ltype = ltype or self.__type_base() | ||||||
|  |         if animated is None or self.type.animated is not True: | ||||||
|  |             lv.call(f"{ltype}_set_{prop}", self.obj, value) | ||||||
|  |         else: | ||||||
|  |             lv.call( | ||||||
|  |                 f"{ltype}_set_{prop}", | ||||||
|  |                 self.obj, | ||||||
|  |                 value, | ||||||
|  |                 "LV_ANIM_ON" if animated else "LV_ANIM_OFF", | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |     def get_property(self, prop, ltype=None): | ||||||
|  |         ltype = ltype or self.__type_base() | ||||||
|  |         return f"lv_{ltype}_get_{prop}({self.obj})" | ||||||
|  |  | ||||||
|  |     def set_style(self, prop, value, state): | ||||||
|  |         if value is None: | ||||||
|  |             return [] | ||||||
|  |         return lv.call(f"obj_set_style_{prop}", self.obj, value, state) | ||||||
|  |  | ||||||
|  |     def __type_base(self): | ||||||
|  |         wtype = self.type.w_type | ||||||
|  |         base = str(wtype) | ||||||
|  |         if base.startswith("Lv"): | ||||||
|  |             return f"{wtype}".removeprefix("Lv").removesuffix("Type").lower() | ||||||
|  |         return f"{wtype}".removeprefix("lv_").removesuffix("_t") | ||||||
|  |  | ||||||
|  |     def __str__(self): | ||||||
|  |         return f"({self.var}, {self.type})" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # Map of widgets to their config, used for trigger generation | ||||||
|  | widget_map: dict[Any, Widget] = {} | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_widget_generator(wid): | ||||||
|  |     """ | ||||||
|  |     Used to wait for a widget during code generation. | ||||||
|  |     :param wid: | ||||||
|  |     :return: | ||||||
|  |     """ | ||||||
|  |     while True: | ||||||
|  |         if obj := widget_map.get(wid): | ||||||
|  |             return obj | ||||||
|  |         if Widget.widgets_completed: | ||||||
|  |             raise Invalid( | ||||||
|  |                 f"Widget {wid} not found, yet all widgets should be defined by now" | ||||||
|  |             ) | ||||||
|  |         yield | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def get_widget(wid: ID) -> Widget: | ||||||
|  |     if obj := widget_map.get(wid): | ||||||
|  |         return obj | ||||||
|  |     return await FakeAwaitable(get_widget_generator(wid)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def collect_props(config): | ||||||
|  |     """ | ||||||
|  |     Collect all properties from a configuration | ||||||
|  |     :param config: | ||||||
|  |     :return: | ||||||
|  |     """ | ||||||
|  |     props = {} | ||||||
|  |     for prop in [*ALL_STYLES, *OBJ_FLAGS, CONF_GROUP]: | ||||||
|  |         if prop in config: | ||||||
|  |             props[prop] = config[prop] | ||||||
|  |     return props | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def collect_states(config): | ||||||
|  |     """ | ||||||
|  |     Collect prperties for each state of a widget | ||||||
|  |     :param config: | ||||||
|  |     :return: | ||||||
|  |     """ | ||||||
|  |     states = {CONF_DEFAULT: collect_props(config)} | ||||||
|  |     for state in STATES: | ||||||
|  |         if state in config: | ||||||
|  |             states[state] = collect_props(config[state]) | ||||||
|  |     return states | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def collect_parts(config): | ||||||
|  |     """ | ||||||
|  |     Collect properties and states for all widget parts | ||||||
|  |     :param config: | ||||||
|  |     :return: | ||||||
|  |     """ | ||||||
|  |     parts = {CONF_MAIN: collect_states(config)} | ||||||
|  |     for part in PARTS: | ||||||
|  |         if part in config: | ||||||
|  |             parts[part] = collect_states(config[part]) | ||||||
|  |     return parts | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def set_obj_properties(w: Widget, config): | ||||||
|  |     """Generate a list of C++ statements to apply properties to an lv_obj_t""" | ||||||
|  |     parts = collect_parts(config) | ||||||
|  |     for part, states in parts.items(): | ||||||
|  |         for state, props in states.items(): | ||||||
|  |             lv_state = ConstantLiteral( | ||||||
|  |                 f"(int)LV_STATE_{state.upper()}|(int)LV_PART_{part.upper()}" | ||||||
|  |             ) | ||||||
|  |             for prop, value in { | ||||||
|  |                 k: v for k, v in props.items() if k in ALL_STYLES | ||||||
|  |             }.items(): | ||||||
|  |                 if isinstance(ALL_STYLES[prop], LValidator): | ||||||
|  |                     value = await ALL_STYLES[prop].process(value) | ||||||
|  |                 w.set_style(prop, value, lv_state) | ||||||
|  |     flag_clr = set() | ||||||
|  |     flag_set = set() | ||||||
|  |     props = parts[CONF_MAIN][CONF_DEFAULT] | ||||||
|  |     for prop, value in {k: v for k, v in props.items() if k in OBJ_FLAGS}.items(): | ||||||
|  |         if value: | ||||||
|  |             flag_set.add(prop) | ||||||
|  |         else: | ||||||
|  |             flag_clr.add(prop) | ||||||
|  |     if flag_set: | ||||||
|  |         adds = join_enums(flag_set, "LV_OBJ_FLAG_") | ||||||
|  |         w.add_flag(adds) | ||||||
|  |     if flag_clr: | ||||||
|  |         clrs = join_enums(flag_clr, "LV_OBJ_FLAG_") | ||||||
|  |         w.clear_flag(clrs) | ||||||
|  |  | ||||||
|  |     if states := config.get(CONF_STATE): | ||||||
|  |         adds = set() | ||||||
|  |         clears = set() | ||||||
|  |         lambs = {} | ||||||
|  |         for key, value in states.items(): | ||||||
|  |             if isinstance(value, cv.Lambda): | ||||||
|  |                 lambs[key] = value | ||||||
|  |             elif value == "true": | ||||||
|  |                 adds.add(key) | ||||||
|  |             else: | ||||||
|  |                 clears.add(key) | ||||||
|  |         if adds: | ||||||
|  |             adds = ConstantLiteral(join_enums(adds, "LV_STATE_")) | ||||||
|  |             w.add_state(adds) | ||||||
|  |         if clears: | ||||||
|  |             clears = ConstantLiteral(join_enums(clears, "LV_STATE_")) | ||||||
|  |             w.clear_state(clears) | ||||||
|  |         for key, value in lambs.items(): | ||||||
|  |             lamb = await cg.process_lambda(value, [], return_type=cg.bool_) | ||||||
|  |             state = ConstantLiteral(f"LV_STATE_{key.upper}") | ||||||
|  |             lv.cond_if(lamb) | ||||||
|  |             w.add_state(state) | ||||||
|  |             lv.cond_else() | ||||||
|  |             w.clear_state(state) | ||||||
|  |             lv.cond_endif() | ||||||
|  |     if scrollbar_mode := config.get(CONF_SCROLLBAR_MODE): | ||||||
|  |         lv_obj.set_scrollbar_mode(w.obj, scrollbar_mode) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def add_widgets(parent: Widget, config: dict): | ||||||
|  |     """ | ||||||
|  |     Add all widgets to an object | ||||||
|  |     :param parent: The enclosing obj | ||||||
|  |     :param config: The configuration | ||||||
|  |     :return: | ||||||
|  |     """ | ||||||
|  |     for w in config.get(CONF_WIDGETS) or (): | ||||||
|  |         w_type, w_cnfig = next(iter(w.items())) | ||||||
|  |         await widget_to_code(w_cnfig, w_type, parent.obj) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def widget_to_code(w_cnfig, w_type, parent): | ||||||
|  |     """ | ||||||
|  |     Converts a Widget definition to C code. | ||||||
|  |     :param w_cnfig: The widget configuration | ||||||
|  |     :param w_type:  The Widget type | ||||||
|  |     :param parent: The parent to which the widget should be added | ||||||
|  |     :return: | ||||||
|  |     """ | ||||||
|  |     spec: WidgetType = WIDGET_TYPES[w_type] | ||||||
|  |     creator = spec.obj_creator(parent, w_cnfig) | ||||||
|  |     add_lv_use(spec.name) | ||||||
|  |     add_lv_use(*spec.get_uses()) | ||||||
|  |     wid = w_cnfig[CONF_ID] | ||||||
|  |     add_line_marks(wid) | ||||||
|  |     if spec.is_compound(): | ||||||
|  |         var = cg.new_Pvariable(wid) | ||||||
|  |         lv_add(var.set_obj(creator)) | ||||||
|  |     else: | ||||||
|  |         var = cg.Pvariable(wid, cg.nullptr, type_=lv_obj_t) | ||||||
|  |         lv_assign(var, creator) | ||||||
|  |  | ||||||
|  |     widget = Widget.create(wid, var, spec, w_cnfig, parent) | ||||||
|  |     await set_obj_properties(widget, w_cnfig) | ||||||
|  |     await add_widgets(widget, w_cnfig) | ||||||
|  |     await spec.to_code(widget, w_cnfig) | ||||||
| @@ -72,18 +72,18 @@ void PMWCS3Component::dump_config() { | |||||||
|   LOG_SENSOR("  ", "vwc", this->vwc_sensor_); |   LOG_SENSOR("  ", "vwc", this->vwc_sensor_); | ||||||
| } | } | ||||||
| void PMWCS3Component::read_data_() { | void PMWCS3Component::read_data_() { | ||||||
|   uint8_t data[8]; |  | ||||||
|   float e25, ec, temperature, vwc; |  | ||||||
|  |  | ||||||
|   /////// Super important !!!! first activate reading PMWCS3_REG_READ_START (if not, return always the same values) //// |   /////// Super important !!!! first activate reading PMWCS3_REG_READ_START (if not, return always the same values) //// | ||||||
|  |  | ||||||
|   if (!this->write_bytes(PMWCS3_REG_READ_START, nullptr, 0)) { |   if (!this->write_bytes(PMWCS3_REG_READ_START, nullptr, 0)) { | ||||||
|     this->status_set_warning(); |     this->status_set_warning(); | ||||||
|     ESP_LOGVV(TAG, "Failed to write into REG_READ_START register !!!"); |     ESP_LOGVV(TAG, "Failed to write into REG_READ_START register !!!"); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   // NOLINT  delay(100); |  | ||||||
|  |  | ||||||
|  |   // Wait for the sensor to be ready. | ||||||
|  |   // 80ms empirically determined (conservative). | ||||||
|  |   this->set_timeout(80, [this] { | ||||||
|  |     uint8_t data[8]; | ||||||
|  |     float e25, ec, temperature, vwc; | ||||||
|     if (!this->read_bytes(PMWCS3_REG_GET_DATA, (uint8_t *) &data, 8)) { |     if (!this->read_bytes(PMWCS3_REG_GET_DATA, (uint8_t *) &data, 8)) { | ||||||
|       ESP_LOGVV(TAG, "Error reading PMWCS3_REG_GET_DATA registers"); |       ESP_LOGVV(TAG, "Error reading PMWCS3_REG_GET_DATA registers"); | ||||||
|       this->mark_failed(); |       this->mark_failed(); | ||||||
| @@ -109,6 +109,7 @@ void PMWCS3Component::read_data_() { | |||||||
|       this->vwc_sensor_->publish_state(vwc); |       this->vwc_sensor_->publish_state(vwc); | ||||||
|       ESP_LOGVV(TAG, "vwc: data[6]=%d, data[7]=%d, result=%f", data[6], data[7], vwc); |       ESP_LOGVV(TAG, "vwc: data[6]=%d, data[7]=%d, result=%f", data[6], data[7], vwc); | ||||||
|     } |     } | ||||||
|  |   }); | ||||||
| } | } | ||||||
|  |  | ||||||
| }  // namespace pmwcs3 | }  // namespace pmwcs3 | ||||||
|   | |||||||
| @@ -38,6 +38,9 @@ | |||||||
| #define USE_LIGHT | #define USE_LIGHT | ||||||
| #define USE_LOCK | #define USE_LOCK | ||||||
| #define USE_LOGGER | #define USE_LOGGER | ||||||
|  | #define USE_LVGL | ||||||
|  | #define USE_LVGL_FONT | ||||||
|  | #define USE_LVGL_IMAGE | ||||||
| #define USE_MDNS | #define USE_MDNS | ||||||
| #define USE_MEDIA_PLAYER | #define USE_MEDIA_PLAYER | ||||||
| #define USE_MQTT | #define USE_MQTT | ||||||
|   | |||||||
| @@ -42,6 +42,7 @@ lib_deps = | |||||||
|     pavlodn/HaierProtocol@0.9.31           ; haier |     pavlodn/HaierProtocol@0.9.31           ; haier | ||||||
|     ; This is using the repository until a new release is published to PlatformIO |     ; This is using the repository until a new release is published to PlatformIO | ||||||
|     https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library |     https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library | ||||||
|  |     lvgl/lvgl@8.4.0                                       ; lvgl | ||||||
| build_flags = | build_flags = | ||||||
|     -DESPHOME_LOG_LEVEL=ESPHOME_LOG_LEVEL_VERY_VERBOSE |     -DESPHOME_LOG_LEVEL=ESPHOME_LOG_LEVEL_VERY_VERBOSE | ||||||
| src_filter = | src_filter = | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| async_timeout==4.0.3; python_version <= "3.10" | async_timeout==4.0.3; python_version <= "3.10" | ||||||
| cryptography==42.0.2 | cryptography==43.0.0 | ||||||
| voluptuous==0.14.2 | voluptuous==0.14.2 | ||||||
| PyYAML==6.0.1 | PyYAML==6.0.1 | ||||||
| paho-mqtt==1.6.1 | paho-mqtt==1.6.1 | ||||||
| @@ -13,7 +13,7 @@ platformio==6.1.15  # When updating platformio, also update Dockerfile | |||||||
| esptool==4.7.0 | esptool==4.7.0 | ||||||
| click==8.1.7 | click==8.1.7 | ||||||
| esphome-dashboard==20240620.0 | esphome-dashboard==20240620.0 | ||||||
| aioesphomeapi==24.3.0 | aioesphomeapi==24.6.2 | ||||||
| zeroconf==0.132.2 | zeroconf==0.132.2 | ||||||
| python-magic==0.4.27 | python-magic==0.4.27 | ||||||
| ruamel.yaml==0.18.6 # dashboard_import | ruamel.yaml==0.18.6 # dashboard_import | ||||||
|   | |||||||
							
								
								
									
										0
									
								
								tests/components/lvgl/common.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								tests/components/lvgl/common.yaml
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										24
									
								
								tests/components/lvgl/lvgl-package.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								tests/components/lvgl/lvgl-package.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | |||||||
|  | color: | ||||||
|  |   - id: light_blue | ||||||
|  |     hex: "3340FF" | ||||||
|  |  | ||||||
|  | lvgl: | ||||||
|  |   bg_color: light_blue | ||||||
|  |   widgets: | ||||||
|  |     - label: | ||||||
|  |         text: Hello world | ||||||
|  |         text_color: 0xFF8000 | ||||||
|  |         align: center | ||||||
|  |         text_font: montserrat_40 | ||||||
|  |         border_post: true | ||||||
|  |  | ||||||
|  |     - label: | ||||||
|  |         text: "Hello shiny day" | ||||||
|  |         text_color: 0xFFFFFF | ||||||
|  |         align: bottom_mid | ||||||
|  |         text_font: space16 | ||||||
|  |  | ||||||
|  | font: | ||||||
|  |   - file: "gfonts://Roboto" | ||||||
|  |     id: space16 | ||||||
|  |     bpp: 4 | ||||||
							
								
								
									
										30
									
								
								tests/components/lvgl/test.esp32-ard.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								tests/components/lvgl/test.esp32-ard.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | |||||||
|  | spi: | ||||||
|  |   clk_pin: 14 | ||||||
|  |   mosi_pin: 13 | ||||||
|  |  | ||||||
|  | i2c: | ||||||
|  |   sda: GPIO18 | ||||||
|  |   scl: GPIO19 | ||||||
|  |  | ||||||
|  | display: | ||||||
|  |   - platform: ili9xxx | ||||||
|  |     model: st7789v | ||||||
|  |     id: tft_display | ||||||
|  |     dimensions: | ||||||
|  |       width: 240 | ||||||
|  |       height: 320 | ||||||
|  |     transform: | ||||||
|  |       swap_xy: false | ||||||
|  |       mirror_x: true | ||||||
|  |       mirror_y: true | ||||||
|  |     data_rate: 80MHz | ||||||
|  |     cs_pin: GPIO22 | ||||||
|  |     dc_pin: GPIO21 | ||||||
|  |     auto_clear_enabled: false | ||||||
|  |     invert_colors: false | ||||||
|  |     update_interval: never | ||||||
|  |  | ||||||
|  | packages: | ||||||
|  |   lvgl: !include lvgl-package.yaml | ||||||
|  |  | ||||||
|  | <<: !include common.yaml | ||||||
							
								
								
									
										52
									
								
								tests/components/lvgl/test.esp32-idf.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								tests/components/lvgl/test.esp32-idf.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | |||||||
|  | spi: | ||||||
|  |   clk_pin: 14 | ||||||
|  |   mosi_pin: 13 | ||||||
|  |  | ||||||
|  | i2c: | ||||||
|  |   sda: GPIO18 | ||||||
|  |   scl: GPIO19 | ||||||
|  |  | ||||||
|  | display: | ||||||
|  |   - platform: ili9xxx | ||||||
|  |     model: st7789v | ||||||
|  |     id: second_display | ||||||
|  |     dimensions: | ||||||
|  |       width: 240 | ||||||
|  |       height: 320 | ||||||
|  |     transform: | ||||||
|  |       swap_xy: false | ||||||
|  |       mirror_x: true | ||||||
|  |       mirror_y: true | ||||||
|  |     data_rate: 80MHz | ||||||
|  |     cs_pin: GPIO20 | ||||||
|  |     dc_pin: GPIO15 | ||||||
|  |     auto_clear_enabled: false | ||||||
|  |     invert_colors: false | ||||||
|  |     update_interval: never | ||||||
|  |  | ||||||
|  |   - platform: ili9xxx | ||||||
|  |     model: st7789v | ||||||
|  |     id: tft_display | ||||||
|  |     dimensions: | ||||||
|  |       width: 240 | ||||||
|  |       height: 320 | ||||||
|  |     transform: | ||||||
|  |       swap_xy: false | ||||||
|  |       mirror_x: true | ||||||
|  |       mirror_y: true | ||||||
|  |     data_rate: 80MHz | ||||||
|  |     cs_pin: GPIO22 | ||||||
|  |     dc_pin: GPIO21 | ||||||
|  |     auto_clear_enabled: false | ||||||
|  |     invert_colors: false | ||||||
|  |     update_interval: never | ||||||
|  |  | ||||||
|  | packages: | ||||||
|  |   lvgl: !include lvgl-package.yaml | ||||||
|  |  | ||||||
|  | lvgl: | ||||||
|  |   displays: | ||||||
|  |     - tft_display | ||||||
|  |     - second_display | ||||||
|  |  | ||||||
|  | <<: !include common.yaml | ||||||
		Reference in New Issue
	
	Block a user