mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	[lvgl] Bug fixes (#7300)
This commit is contained in:
		
				
					committed by
					
						 Jesse Hills
						Jesse Hills
					
				
			
			
				
	
			
			
			
						parent
						
							7464b440c0
						
					
				
				
					commit
					5c7d070307
				
			| @@ -6,8 +6,8 @@ Constants already defined in esphome.const are not duplicated here and must be i | ||||
|  | ||||
| from esphome import codegen as cg, config_validation as cv | ||||
| from esphome.const import CONF_ITEMS | ||||
| from esphome.core import ID, Lambda | ||||
| from esphome.cpp_generator import MockObj | ||||
| from esphome.core import Lambda | ||||
| from esphome.cpp_generator import LambdaExpression, MockObj | ||||
| from esphome.cpp_types import uint32 | ||||
| from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor | ||||
|  | ||||
| @@ -22,19 +22,22 @@ def literal(arg): | ||||
|     return arg | ||||
|  | ||||
|  | ||||
| def call_lambda(lamb: LambdaExpression): | ||||
|     expr = lamb.content.strip() | ||||
|     if expr.startswith("return") and expr.endswith(";"): | ||||
|         return expr[7:][:-1] | ||||
|     return f"{lamb}()" | ||||
|  | ||||
|  | ||||
| class LValidator: | ||||
|     """ | ||||
|     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, requires=None | ||||
|     ): | ||||
|     def __init__(self, validator, rtype, retmapper=None, requires=None): | ||||
|         self.validator = validator | ||||
|         self.rtype = rtype | ||||
|         self.idtype = idtype | ||||
|         self.idexpr = idexpr | ||||
|         self.retmapper = retmapper | ||||
|         self.requires = requires | ||||
|  | ||||
| @@ -43,8 +46,6 @@ class LValidator: | ||||
|             value = requires_component(self.requires)(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=()): | ||||
| @@ -52,10 +53,10 @@ class LValidator: | ||||
|             return None | ||||
|         if isinstance(value, Lambda): | ||||
|             return cg.RawExpression( | ||||
|                 f"{await cg.process_lambda(value, args, return_type=self.rtype)}()" | ||||
|                 call_lambda( | ||||
|                     await cg.process_lambda(value, args, return_type=self.rtype) | ||||
|                 ) | ||||
|             ) | ||||
|         if self.idtype is not None and isinstance(value, ID): | ||||
|             return cg.RawExpression(f"{value}->{self.idexpr}") | ||||
|         if self.retmapper is not None: | ||||
|             return self.retmapper(value) | ||||
|         return cg.safe_exp(value) | ||||
| @@ -89,7 +90,7 @@ class LvConstant(LValidator): | ||||
|             cv.ensure_list(self.one_of), uint32, retmapper=self.mapper | ||||
|         ) | ||||
|  | ||||
|     def mapper(self, value, args=()): | ||||
|     def mapper(self, value): | ||||
|         if not isinstance(value, list): | ||||
|             value = [value] | ||||
|         return literal( | ||||
| @@ -103,7 +104,7 @@ class LvConstant(LValidator): | ||||
|  | ||||
|     def extend(self, *choices): | ||||
|         """ | ||||
|         Extend an LVCconstant with additional choices. | ||||
|         Extend an LVconstant with additional choices. | ||||
|         :param choices: The extra choices | ||||
|         :return: A new LVConstant instance | ||||
|         """ | ||||
| @@ -431,6 +432,8 @@ CONF_ONE_LINE = "one_line" | ||||
| CONF_ON_SELECT = "on_select" | ||||
| CONF_ONE_CHECKED = "one_checked" | ||||
| CONF_NEXT = "next" | ||||
| CONF_PAD_ROW = "pad_row" | ||||
| CONF_PAD_COLUMN = "pad_column" | ||||
| CONF_PAGE = "page" | ||||
| CONF_PAGE_WRAP = "page_wrap" | ||||
| CONF_PASSWORD_MODE = "password_mode" | ||||
| @@ -462,6 +465,7 @@ CONF_SKIP = "skip" | ||||
| CONF_SYMBOL = "symbol" | ||||
| CONF_TAB_ID = "tab_id" | ||||
| CONF_TABS = "tabs" | ||||
| CONF_TIME_FORMAT = "time_format" | ||||
| CONF_TILE = "tile" | ||||
| CONF_TILE_ID = "tile_id" | ||||
| CONF_TILES = "tiles" | ||||
|   | ||||
| @@ -1,17 +1,14 @@ | ||||
| from typing import Union | ||||
|  | ||||
| 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.image import Image_ | ||||
| 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, CONF_VALUE | ||||
| from esphome.core import HexInt | ||||
| from esphome.const import CONF_ARGS, CONF_COLOR, CONF_FORMAT, CONF_TIME, CONF_VALUE | ||||
| from esphome.core import HexInt, Lambda | ||||
| from esphome.cpp_generator import MockObj | ||||
| from esphome.cpp_types import uint32 | ||||
| from esphome.cpp_types import ESPTime, uint32 | ||||
| from esphome.helpers import cpp_string_escape | ||||
| from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor | ||||
|  | ||||
| @@ -19,9 +16,11 @@ from . import types as ty | ||||
| from .defines import ( | ||||
|     CONF_END_VALUE, | ||||
|     CONF_START_VALUE, | ||||
|     CONF_TIME_FORMAT, | ||||
|     LV_FONTS, | ||||
|     LValidator, | ||||
|     LvConstant, | ||||
|     call_lambda, | ||||
|     literal, | ||||
| ) | ||||
| from .helpers import ( | ||||
| @@ -110,13 +109,13 @@ def angle(value): | ||||
| def size_validator(value): | ||||
|     """A size in one axis - one of "size_content", a number (pixels) or a percentage""" | ||||
|     if value == SCHEMA_EXTRACT: | ||||
|         return ["size_content", "pixels", "..%"] | ||||
|         return ["SIZE_CONTENT", "number of pixels", "percentage"] | ||||
|     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") | ||||
|         raise cv.Invalid("must be 'size_content', a percentage or an integer (pixels)") | ||||
|     if isinstance(value, int): | ||||
|         return cv.int_(value) | ||||
|     # Will throw an exception if not a percentage. | ||||
| @@ -125,6 +124,15 @@ def size_validator(value): | ||||
|  | ||||
| size = LValidator(size_validator, uint32, retmapper=literal) | ||||
|  | ||||
|  | ||||
| def pixels_validator(value): | ||||
|     if isinstance(value, str) and value.lower().endswith("px"): | ||||
|         return cv.int_(value[:-2]) | ||||
|     return cv.int_(value) | ||||
|  | ||||
|  | ||||
| pixels = LValidator(pixels_validator, uint32, retmapper=literal) | ||||
|  | ||||
| radius_consts = LvConstant("LV_RADIUS_", "CIRCLE") | ||||
|  | ||||
|  | ||||
| @@ -167,9 +175,7 @@ lv_image = LValidator( | ||||
|     retmapper=lambda x: lv_expr.img_from(MockObj(x)), | ||||
|     requires="image", | ||||
| ) | ||||
| lv_bool = LValidator( | ||||
|     cv.boolean, cg.bool_, BinarySensor, "get_state()", retmapper=literal | ||||
| ) | ||||
| lv_bool = LValidator(cv.boolean, cg.bool_, retmapper=literal) | ||||
|  | ||||
|  | ||||
| def lv_pct(value: Union[int, float]): | ||||
| @@ -185,42 +191,60 @@ def lvms_validator_(value): | ||||
|  | ||||
|  | ||||
| lv_milliseconds = LValidator( | ||||
|     lvms_validator_, | ||||
|     cg.int32, | ||||
|     retmapper=lambda x: x.total_milliseconds, | ||||
|     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}"), | ||||
|         ) | ||||
|         super().__init__(cv.string, cg.std_string, lambda s: cg.safe_exp(f"{s}")) | ||||
|  | ||||
|     def __call__(self, value): | ||||
|         if isinstance(value, dict): | ||||
|         if isinstance(value, dict) and CONF_FORMAT in value: | ||||
|             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 literal(f"str_sprintf({format_str}, {arg_expr}).c_str()") | ||||
|             if format_str := value.get(CONF_FORMAT): | ||||
|                 args = [str(x) for x in value[CONF_ARGS]] | ||||
|                 arg_expr = cg.RawExpression(",".join(args)) | ||||
|                 format_str = cpp_string_escape(format_str) | ||||
|                 return literal(f"str_sprintf({format_str}, {arg_expr}).c_str()") | ||||
|             if time_format := value.get(CONF_TIME_FORMAT): | ||||
|                 source = value[CONF_TIME] | ||||
|                 if isinstance(source, Lambda): | ||||
|                     time_format = cpp_string_escape(time_format) | ||||
|                     return cg.RawExpression( | ||||
|                         call_lambda( | ||||
|                             await cg.process_lambda(source, args, return_type=ESPTime) | ||||
|                         ) | ||||
|                         + f".strftime({time_format}).c_str()" | ||||
|                     ) | ||||
|                 # must be an ID | ||||
|                 source = await cg.get_variable(source) | ||||
|                 return source.now().strftime(time_format).c_str() | ||||
|         if isinstance(value, Lambda): | ||||
|             value = call_lambda( | ||||
|                 await cg.process_lambda(value, args, return_type=self.rtype) | ||||
|             ) | ||||
|  | ||||
|             # Was the lambda call reduced to a string? | ||||
|             if value.endswith("c_str()") or ( | ||||
|                 value.endswith('"') and value.startswith('"') | ||||
|             ): | ||||
|                 pass | ||||
|             else: | ||||
|                 # Either a std::string or a lambda call returning that. We need const char* | ||||
|                 value = f"({value}).c_str()" | ||||
|             return cg.RawExpression(value) | ||||
|         return await super().process(value, args) | ||||
|  | ||||
|  | ||||
| lv_text = TextValidator() | ||||
| lv_float = LValidator(cv.float_, cg.float_, Sensor, "get_state()") | ||||
| lv_int = LValidator(cv.int_, cg.int_, Sensor, "get_state()") | ||||
| lv_brightness = LValidator( | ||||
|     cv.percentage, cg.float_, Sensor, "get_state()", retmapper=lambda x: int(x * 255) | ||||
| ) | ||||
| lv_float = LValidator(cv.float_, cg.float_) | ||||
| lv_int = LValidator(cv.int_, cg.int_) | ||||
| lv_brightness = LValidator(cv.percentage, cg.float_, retmapper=lambda x: int(x * 255)) | ||||
|  | ||||
|  | ||||
| def is_lv_font(font): | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| from esphome import config_validation as cv | ||||
| from esphome.automation import Trigger, validate_automation | ||||
| from esphome.components.time import RealTimeClock | ||||
| from esphome.const import ( | ||||
|     CONF_ARGS, | ||||
|     CONF_FORMAT, | ||||
| @@ -8,6 +9,7 @@ from esphome.const import ( | ||||
|     CONF_ON_VALUE, | ||||
|     CONF_STATE, | ||||
|     CONF_TEXT, | ||||
|     CONF_TIME, | ||||
|     CONF_TRIGGER_ID, | ||||
|     CONF_TYPE, | ||||
| ) | ||||
| @@ -15,6 +17,7 @@ from esphome.core import TimePeriod | ||||
| from esphome.schema_extractors import SCHEMA_EXTRACT | ||||
|  | ||||
| from . import defines as df, lv_validation as lvalid | ||||
| from .defines import CONF_TIME_FORMAT | ||||
| from .helpers import add_lv_use, requires_component, validate_printf | ||||
| from .lv_validation import lv_color, lv_font, lv_image | ||||
| from .lvcode import LvglComponent | ||||
| @@ -46,7 +49,13 @@ TEXT_SCHEMA = cv.Schema( | ||||
|                 ), | ||||
|                 validate_printf, | ||||
|             ), | ||||
|             lvalid.lv_text, | ||||
|             cv.Schema( | ||||
|                 { | ||||
|                     cv.Required(CONF_TIME_FORMAT): cv.string, | ||||
|                     cv.GenerateID(CONF_TIME): cv.templatable(cv.use_id(RealTimeClock)), | ||||
|                 } | ||||
|             ), | ||||
|             cv.templatable(cv.string), | ||||
|         ) | ||||
|     } | ||||
| ) | ||||
| @@ -116,15 +125,13 @@ STYLE_PROPS = { | ||||
|     "opa_layered": lvalid.opacity, | ||||
|     "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, | ||||
|     "outline_pad": lvalid.pixels, | ||||
|     "outline_width": lvalid.pixels, | ||||
|     "pad_all": lvalid.pixels, | ||||
|     "pad_bottom": lvalid.pixels, | ||||
|     "pad_left": lvalid.pixels, | ||||
|     "pad_right": lvalid.pixels, | ||||
|     "pad_top": lvalid.pixels, | ||||
|     "shadow_color": lvalid.lv_color, | ||||
|     "shadow_ofs_x": cv.int_, | ||||
|     "shadow_ofs_y": cv.int_, | ||||
| @@ -304,6 +311,8 @@ LAYOUT_SCHEMA = { | ||||
|                 cv.Required(df.CONF_GRID_COLUMNS): [grid_spec], | ||||
|                 cv.Optional(df.CONF_GRID_COLUMN_ALIGN): grid_alignments, | ||||
|                 cv.Optional(df.CONF_GRID_ROW_ALIGN): grid_alignments, | ||||
|                 cv.Optional(df.CONF_PAD_ROW): lvalid.pixels, | ||||
|                 cv.Optional(df.CONF_PAD_COLUMN): lvalid.pixels, | ||||
|             }, | ||||
|             df.TYPE_FLEX: { | ||||
|                 cv.Optional( | ||||
| @@ -312,6 +321,8 @@ LAYOUT_SCHEMA = { | ||||
|                 cv.Optional(df.CONF_FLEX_ALIGN_MAIN, default="start"): flex_alignments, | ||||
|                 cv.Optional(df.CONF_FLEX_ALIGN_CROSS, default="start"): flex_alignments, | ||||
|                 cv.Optional(df.CONF_FLEX_ALIGN_TRACK, default="start"): flex_alignments, | ||||
|                 cv.Optional(df.CONF_PAD_ROW): lvalid.pixels, | ||||
|                 cv.Optional(df.CONF_PAD_COLUMN): lvalid.pixels, | ||||
|             }, | ||||
|         }, | ||||
|         lower=True, | ||||
| @@ -338,7 +349,6 @@ DISP_BG_SCHEMA = cv.Schema( | ||||
|     } | ||||
| ) | ||||
|  | ||||
|  | ||||
| # A style schema that can include text | ||||
| STYLED_TEXT_SCHEMA = cv.maybe_simple_value( | ||||
|     STYLE_SCHEMA.extend(TEXT_SCHEMA), key=CONF_TEXT | ||||
|   | ||||
| @@ -20,6 +20,8 @@ from ..defines import ( | ||||
|     CONF_GRID_ROWS, | ||||
|     CONF_LAYOUT, | ||||
|     CONF_MAIN, | ||||
|     CONF_PAD_COLUMN, | ||||
|     CONF_PAD_ROW, | ||||
|     CONF_SCROLLBAR_MODE, | ||||
|     CONF_STYLES, | ||||
|     CONF_WIDGETS, | ||||
| @@ -29,6 +31,7 @@ from ..defines import ( | ||||
|     TYPE_FLEX, | ||||
|     TYPE_GRID, | ||||
|     LValidator, | ||||
|     call_lambda, | ||||
|     join_enums, | ||||
|     literal, | ||||
| ) | ||||
| @@ -273,6 +276,10 @@ async def set_obj_properties(w: Widget, config): | ||||
|         layout_type: str = layout[CONF_TYPE] | ||||
|         add_lv_use(layout_type) | ||||
|         lv_obj.set_layout(w.obj, literal(f"LV_LAYOUT_{layout_type.upper()}")) | ||||
|         if (pad_row := layout.get(CONF_PAD_ROW)) is not None: | ||||
|             w.set_style(CONF_PAD_ROW, pad_row, 0) | ||||
|         if (pad_column := layout.get(CONF_PAD_COLUMN)) is not None: | ||||
|             w.set_style(CONF_PAD_COLUMN, pad_column, 0) | ||||
|         if layout_type == TYPE_GRID: | ||||
|             wid = config[CONF_ID] | ||||
|             rows = [str(x) for x in layout[CONF_GRID_ROWS]] | ||||
| @@ -316,8 +323,13 @@ async def set_obj_properties(w: Widget, config): | ||||
|     flag_clr = set() | ||||
|     flag_set = set() | ||||
|     props = parts[CONF_MAIN][CONF_DEFAULT] | ||||
|     lambs = {} | ||||
|     flag_set = set() | ||||
|     flag_clr = set() | ||||
|     for prop, value in {k: v for k, v in props.items() if k in OBJ_FLAGS}.items(): | ||||
|         if value: | ||||
|         if isinstance(value, cv.Lambda): | ||||
|             lambs[prop] = value | ||||
|         elif value: | ||||
|             flag_set.add(prop) | ||||
|         else: | ||||
|             flag_clr.add(prop) | ||||
| @@ -327,6 +339,13 @@ async def set_obj_properties(w: Widget, config): | ||||
|     if flag_clr: | ||||
|         clrs = join_enums(flag_clr, "LV_OBJ_FLAG_") | ||||
|         w.clear_flag(clrs) | ||||
|     for key, value in lambs.items(): | ||||
|         lamb = await cg.process_lambda(value, [], return_type=cg.bool_) | ||||
|         flag = f"LV_OBJ_FLAG_{key.upper()}" | ||||
|         with LvConditional(call_lambda(lamb)) as cond: | ||||
|             w.add_flag(flag) | ||||
|             cond.else_() | ||||
|             w.clear_flag(flag) | ||||
|  | ||||
|     if states := config.get(CONF_STATE): | ||||
|         adds = set() | ||||
| @@ -348,7 +367,7 @@ async def set_obj_properties(w: Widget, config): | ||||
|         for key, value in lambs.items(): | ||||
|             lamb = await cg.process_lambda(value, [], return_type=cg.bool_) | ||||
|             state = f"LV_STATE_{key.upper()}" | ||||
|             with LvConditional(f"{lamb}()") as cond: | ||||
|             with LvConditional(call_lambda(lamb)) as cond: | ||||
|                 w.add_state(state) | ||||
|                 cond.else_() | ||||
|                 w.clear_state(state) | ||||
|   | ||||
| @@ -127,3 +127,11 @@ binary_sensor: | ||||
|   - platform: lvgl | ||||
|     name: LVGL checkbox | ||||
|     widget: checkbox_id | ||||
|  | ||||
| wifi: | ||||
|   ssid: SSID | ||||
|   password: PASSWORD123 | ||||
|  | ||||
| time: | ||||
|   platform: sntp | ||||
|   id: time_id | ||||
|   | ||||
| @@ -16,8 +16,6 @@ lvgl: | ||||
|       border_width: 0 | ||||
|       radius: 0 | ||||
|       pad_all: 0 | ||||
|       pad_row: 0 | ||||
|       pad_column: 0 | ||||
|       border_color: 0x0077b3 | ||||
|       text_color: 0xFFFFFF | ||||
|       width: 100% | ||||
| @@ -55,6 +53,13 @@ lvgl: | ||||
|   pages: | ||||
|     - id: page1 | ||||
|       skip: true | ||||
|       layout: | ||||
|         type: flex | ||||
|         pad_row: 4 | ||||
|         pad_column: 4px | ||||
|         flex_align_main: center | ||||
|         flex_align_cross: start | ||||
|         flex_align_track: end | ||||
|       widgets: | ||||
|         - animimg: | ||||
|             height: 60 | ||||
| @@ -118,10 +123,8 @@ lvgl: | ||||
|             outline_width: 10px | ||||
|             pad_all: 10px | ||||
|             pad_bottom: 10px | ||||
|             pad_column: 10px | ||||
|             pad_left: 10px | ||||
|             pad_right: 10px | ||||
|             pad_row: 10px | ||||
|             pad_top: 10px | ||||
|             shadow_color: light_blue | ||||
|             shadow_ofs_x: 5 | ||||
| @@ -221,10 +224,47 @@ lvgl: | ||||
|               - label: | ||||
|                   text: Button | ||||
|             on_click: | ||||
|               lvgl.label.update: | ||||
|                 id: hello_label | ||||
|                 bg_color: 0x123456 | ||||
|                 text: clicked | ||||
|               - lvgl.label.update: | ||||
|                   id: hello_label | ||||
|                   bg_color: 0x123456 | ||||
|                   text: clicked | ||||
|               - lvgl.label.update: | ||||
|                   id: hello_label | ||||
|                   text: !lambda return "hello world"; | ||||
|               - lvgl.label.update: | ||||
|                   id: hello_label | ||||
|                   text: !lambda |- | ||||
|                         ESP_LOGD("label", "multi-line lambda"); | ||||
|                         return "hello world"; | ||||
|               - lvgl.label.update: | ||||
|                   id: hello_label | ||||
|                   text: !lambda 'return str_sprintf("Hello space");' | ||||
|               - lvgl.label.update: | ||||
|                   id: hello_label | ||||
|                   text: | ||||
|                     format: "sprintf format %s" | ||||
|                     args: ['x ? "checked" : "unchecked"'] | ||||
|               - lvgl.label.update: | ||||
|                   id: hello_label | ||||
|                   text: | ||||
|                     time_format: "%c" | ||||
|               - lvgl.label.update: | ||||
|                   id: hello_label | ||||
|                   text: | ||||
|                     time_format: "%c" | ||||
|                     time: time_id | ||||
|               - lvgl.label.update: | ||||
|                   id: hello_label | ||||
|                   text: | ||||
|                     time_format: "%c" | ||||
|                     time: !lambda return id(time_id).now(); | ||||
|               - lvgl.label.update: | ||||
|                   id: hello_label | ||||
|                   text: | ||||
|                     time_format: "%c" | ||||
|                     time: !lambda |- | ||||
|                         ESP_LOGD("label", "multi-line lambda"); | ||||
|                         return id(time_id).now(); | ||||
|             on_value: | ||||
|               logger.log: | ||||
|                 format: "state now %d" | ||||
| @@ -396,6 +436,8 @@ lvgl: | ||||
|                   grid_row_align: end | ||||
|                   grid_rows: [25px, fr(1), content] | ||||
|                   grid_columns: [40, fr(1), fr(1)] | ||||
|                   pad_row: 6px | ||||
|                   pad_column: 0 | ||||
|                 widgets: | ||||
|                   - image: | ||||
|                       grid_cell_row_pos: 0 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user