import re from typing import Any import esphome.codegen as cg from esphome.components import image from esphome.components.color import CONF_HEX, ColorStruct, from_rgbw from esphome.components.font import Font from esphome.components.image import Image_ import esphome.config_validation as cv from esphome.const import ( CONF_ARGS, CONF_COLOR, CONF_FORMAT, CONF_ID, CONF_TIME, CONF_VALUE, ) from esphome.core import CORE, ID, Lambda from esphome.cpp_generator import MockObj from esphome.cpp_types import ESPTime, int32, uint32 from esphome.helpers import cpp_string_escape from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from esphome.types import Expression, SafeExpType from . import types as ty from .defines import ( CONF_END_VALUE, CONF_START_VALUE, CONF_TIME_FORMAT, LV_FONTS, LValidator, LvConstant, StaticCastExpression, call_lambda, literal, ) from .helpers import ( CONF_IF_NAN, add_lv_use, esphome_fonts_used, lv_fonts_used, requires_component, ) from .types import lv_gradient_t, lv_opa_t LV_OPA = LvConstant("LV_OPA_", "TRANSP", "COVER") @schema_extractor("one_of") def opacity_validator(value): if value == SCHEMA_EXTRACT: return LV_OPA.choices value = cv.Any(cv.percentage, LV_OPA.one_of)(value) if value == str(LV_OPA.COVER): value = 1.0 if value == str(LV_OPA.TRANSP): value = 0.0 return cv.float_range(0.0, 1.0)(value) opacity = LValidator( opacity_validator, lv_opa_t, retmapper=lambda opa: StaticCastExpression(cg.uint8, opa * 255.0), ) COLOR_NAMES = { "aliceblue": 0xF0F8FF, "antiquewhite": 0xFAEBD7, "aqua": 0x00FFFF, "aquamarine": 0x7FFFD4, "azure": 0xF0FFFF, "beige": 0xF5F5DC, "bisque": 0xFFE4C4, "black": 0x000000, "blanchedalmond": 0xFFEBCD, "blue": 0x0000FF, "blueviolet": 0x8A2BE2, "brown": 0xA52A2A, "burlywood": 0xDEB887, "cadetblue": 0x5F9EA0, "chartreuse": 0x7FFF00, "chocolate": 0xD2691E, "coral": 0xFF7F50, "cornflowerblue": 0x6495ED, "cornsilk": 0xFFF8DC, "crimson": 0xDC143C, "cyan": 0x00FFFF, "darkblue": 0x00008B, "darkcyan": 0x008B8B, "darkgoldenrod": 0xB8860B, "darkgray": 0xA9A9A9, "darkgreen": 0x006400, "darkgrey": 0xA9A9A9, "darkkhaki": 0xBDB76B, "darkmagenta": 0x8B008B, "darkolivegreen": 0x556B2F, "darkorange": 0xFF8C00, "darkorchid": 0x9932CC, "darkred": 0x8B0000, "darksalmon": 0xE9967A, "darkseagreen": 0x8FBC8F, "darkslateblue": 0x483D8B, "darkslategray": 0x2F4F4F, "darkslategrey": 0x2F4F4F, "darkturquoise": 0x00CED1, "darkviolet": 0x9400D3, "deeppink": 0xFF1493, "deepskyblue": 0x00BFFF, "dimgray": 0x696969, "dimgrey": 0x696969, "dodgerblue": 0x1E90FF, "firebrick": 0xB22222, "floralwhite": 0xFFFAF0, "forestgreen": 0x228B22, "fuchsia": 0xFF00FF, "gainsboro": 0xDCDCDC, "ghostwhite": 0xF8F8FF, "goldenrod": 0xDAA520, "gold": 0xFFD700, "gray": 0x808080, "green": 0x008000, "greenyellow": 0xADFF2F, "grey": 0x808080, "honeydew": 0xF0FFF0, "hotpink": 0xFF69B4, "indianred": 0xCD5C5C, "indigo": 0x4B0082, "ivory": 0xFFFFF0, "khaki": 0xF0E68C, "lavenderblush": 0xFFF0F5, "lavender": 0xE6E6FA, "lawngreen": 0x7CFC00, "lemonchiffon": 0xFFFACD, "lightblue": 0xADD8E6, "lightcoral": 0xF08080, "lightcyan": 0xE0FFFF, "lightgoldenrodyellow": 0xFAFAD2, "lightgray": 0xD3D3D3, "lightgreen": 0x90EE90, "lightgrey": 0xD3D3D3, "lightpink": 0xFFB6C1, "lightsalmon": 0xFFA07A, "lightseagreen": 0x20B2AA, "lightskyblue": 0x87CEFA, "lightslategray": 0x778899, "lightslategrey": 0x778899, "lightsteelblue": 0xB0C4DE, "lightyellow": 0xFFFFE0, "lime": 0x00FF00, "limegreen": 0x32CD32, "linen": 0xFAF0E6, "magenta": 0xFF00FF, "maroon": 0x800000, "mediumaquamarine": 0x66CDAA, "mediumblue": 0x0000CD, "mediumorchid": 0xBA55D3, "mediumpurple": 0x9370DB, "mediumseagreen": 0x3CB371, "mediumslateblue": 0x7B68EE, "mediumspringgreen": 0x00FA9A, "mediumturquoise": 0x48D1CC, "mediumvioletred": 0xC71585, "midnightblue": 0x191970, "mintcream": 0xF5FFFA, "mistyrose": 0xFFE4E1, "moccasin": 0xFFE4B5, "navajowhite": 0xFFDEAD, "navy": 0x000080, "oldlace": 0xFDF5E6, "olive": 0x808000, "olivedrab": 0x6B8E23, "orange": 0xFFA500, "orangered": 0xFF4500, "orchid": 0xDA70D6, "palegoldenrod": 0xEEE8AA, "palegreen": 0x98FB98, "paleturquoise": 0xAFEEEE, "palevioletred": 0xDB7093, "papayawhip": 0xFFEFD5, "peachpuff": 0xFFDAB9, "peru": 0xCD853F, "pink": 0xFFC0CB, "plum": 0xDDA0DD, "powderblue": 0xB0E0E6, "purple": 0x800080, "rebeccapurple": 0x663399, "red": 0xFF0000, "rosybrown": 0xBC8F8F, "royalblue": 0x4169E1, "saddlebrown": 0x8B4513, "salmon": 0xFA8072, "sandybrown": 0xF4A460, "seagreen": 0x2E8B57, "seashell": 0xFFF5EE, "sienna": 0xA0522D, "silver": 0xC0C0C0, "skyblue": 0x87CEEB, "slateblue": 0x6A5ACD, "slategray": 0x708090, "slategrey": 0x708090, "snow": 0xFFFAFA, "springgreen": 0x00FF7F, "steelblue": 0x4682B4, "tan": 0xD2B48C, "teal": 0x008080, "thistle": 0xD8BFD8, "tomato": 0xFF6347, "turquoise": 0x40E0D0, "violet": 0xEE82EE, "wheat": 0xF5DEB3, "white": 0xFFFFFF, "whitesmoke": 0xF5F5F5, "yellow": 0xFFFF00, "yellowgreen": 0x9ACD32, } @schema_extractor("one_of") def color(value): if value == SCHEMA_EXTRACT: return ["hex color value", "color ID"] return cv.Any(cv.int_, cv.one_of(*COLOR_NAMES, lower=True), cv.use_id(ColorStruct))( value ) def color_retmapper(value): if isinstance(value, cv.Lambda): return cv.returning_lambda(value) if isinstance(value, str) and value in COLOR_NAMES: value = COLOR_NAMES[value] if isinstance(value, int): return literal( f"lv_color_make({(value >> 16) & 0xFF}, {(value >> 8) & 0xFF}, {value & 0xFF})" ) if isinstance(value, ID): cval = [x for x in CORE.config[CONF_COLOR] if x[CONF_ID] == value][0] if CONF_HEX in cval: r, g, b = cval[CONF_HEX] else: r, g, b, _ = from_rgbw(cval) return literal(f"lv_color_make({r}, {g}, {b})") assert False def option_string(value): value = cv.string(value).strip() if value.find("\n") != -1: raise cv.Invalid("Options strings must not contain newlines") return value class LvColor(LValidator): def __init__(self): super().__init__(color, ty.lv_color_t, retmapper=color_retmapper) def __getattr__(self, item): if item in COLOR_NAMES: return color_retmapper(COLOR_NAMES[item]) raise AttributeError(item) lv_color = LvColor() def pixels_or_percent_validator(value): """A length in one axis - either a number (pixels) or a percentage""" if value == SCHEMA_EXTRACT: return ["pixels", "..%"] if isinstance(value, str) and value.lower().endswith("px"): return cv.int_(value[:-2]) if isinstance(value, str) and re.match(r"^lv_pct\((\d+)\)$", value): return int(value[6:-1]) / 100.0 return cv.Any(cv.int_, cv.possibly_negative_percentage)(value) pixels_or_percent = LValidator( pixels_or_percent_validator, uint32, retmapper=lambda x: x if isinstance(x, int) else literal(f"lv_pct({int(x * 100)})"), ) def pixels_validator(value): if isinstance(value, str) and value.lower().endswith("px"): value = value[:-2] return cv.positive_int(value) pixels = LValidator(pixels_validator, uint32, retmapper=literal) def padding_validator(value): if isinstance(value, str) and value.lower().endswith("px"): value = value[:-2] return cv.int_(value) padding = LValidator(padding_validator, int32, retmapper=literal) def scale_validator(value): return cv.float_range(0.1, 10.0)(value) scale = LValidator(scale_validator, uint32, retmapper=lambda x: int(x * 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 cv.float_range(0.0, 360.0)(cv.angle(value)) # Validator for angles in LVGL expressed in 1/10 degree units. lv_angle = LValidator(angle, uint32, retmapper=lambda x: int(x * 10)) # Validator for angles in LVGL expressed in whole degrees lv_angle_degrees = LValidator(angle, uint32, retmapper=int) @schema_extractor("one_of") 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", "number of pixels", "percentage"] if isinstance(value, str) and value.lower().endswith("px"): value = cv.int_(value[:-2]) if isinstance(value, str) and value.upper() == "SIZE_CONTENT": return "LV_SIZE_CONTENT" return pixels_or_percent_validator(value) size = LValidator( size_validator, uint32, retmapper=lambda x: ( literal(x) if isinstance(x, str) else pixels_or_percent.retmapper(x) ), ) LV_RADIUS = LvConstant("LV_RADIUS_", "CIRCLE") @schema_extractor("one_of") def fraction_validator(value): if value == SCHEMA_EXTRACT: return LV_RADIUS.choices value = cv.Any(size, cv.percentage, LV_RADIUS.one_of)(value) if isinstance(value, float): return int(value * 255) return value lv_fraction = LValidator(fraction_validator, uint32, retmapper=literal) def id_name(value): if value == SCHEMA_EXTRACT: return "id" return cv.validate_id_name(value) def stop_value(value): return cv.int_range(0, 255)(value) lv_images_used = set() def image_validator(value): value = requires_component("image")(value) value = cv.use_id(Image_)(value) lv_images_used.add(value) add_lv_use("img", "label") return value lv_image = LValidator( image_validator, image.Image_.operator("ptr"), requires="image", ) lv_image_list = LValidator( cv.ensure_list(image_validator), cg.std_vector.template(image.Image_.operator("ptr")), requires="image", ) lv_bool = LValidator(cv.boolean, cg.bool_, retmapper=literal) 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.std_string, lambda s: cg.safe_exp(f"{s}")) def __call__(self, value): if isinstance(value, dict) and CONF_FORMAT in value: return value return super().__call__(value) async def process( self, value: Any, args: list[tuple[SafeExpType, str]] | None = None ) -> Expression: # Local import to avoid circular import at module level from .lvcode import get_lambda_context_args args = args or get_lambda_context_args() if isinstance(value, dict): if format_str := value.get(CONF_FORMAT): str_args = [str(x) for x in value[CONF_ARGS]] arg_expr = cg.RawExpression(",".join(str_args)) format_str = cpp_string_escape(format_str) # str_sprintf justified: user-defined format, can't optimize without permanent RAM cost sprintf_str = f"str_sprintf({format_str}, {arg_expr}).c_str()" if nanval := value.get(CONF_IF_NAN): nanval = cpp_string_escape(nanval) return literal( f"(std::isfinite({arg_expr}) ? {sprintf_str} : {nanval})" ) return literal(sprintf_str) if time_format := value.get(CONF_TIME_FORMAT): source = value[CONF_TIME] if isinstance(source, Lambda): source = MockObj( call_lambda( await cg.process_lambda(source, args, return_type=ESPTime) ) ) # must be an ID else: source = (await cg.get_variable(source)).now() return source.strftime(time_format).c_str() if isinstance(value, Lambda): value = call_lambda( await cg.process_lambda(value, args, return_type=self.rtype) ) textvalue = str(value) # Was the lambda call reduced to a string? if textvalue.endswith("c_str()") or ( textvalue.endswith('"') and textvalue.startswith('"') ): return value # Either a std::string or a lambda call returning that. We need const char* return MockObj(f"({value}).c_str()") return await super().process(value, args) lv_text = TextValidator() lv_float = LValidator(cv.float_, cg.float_) lv_int = LValidator(cv.int_, cg.int_) lv_positive_int = LValidator(cv.positive_int, cg.int_) def _percentage_validator(value): value = cv.Any(cv.percentage, cv.float_range(0.0, 1.0), cv.int_range(0, 255))(value) if isinstance(value, int): return value / 255.0 return value lv_percentage = LValidator( _percentage_validator, cg.float_, retmapper=lambda x: int(x * 255) ) lv_gradient = LValidator( validator=cv.use_id(lv_gradient_t), rtype=lv_gradient_t, retmapper=MockObj, ) def is_lv_font(font): return isinstance(font, str) and font.lower() in LV_FONTS 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 fontval def validator(value): if value == SCHEMA_EXTRACT: return LV_FONTS if is_lv_font(value): return lv_builtin_font(value) add_lv_use("font") fontval = cv.use_id(Font)(value) esphome_fonts_used.add(fontval) return requires_component("font")(fontval) # Use font::Font* as return type for lambdas returning ESPHome fonts # The inline overloads in lvgl_esphome.h handle conversion to lv_font_t* super().__init__(validator, Font.operator("ptr")) async def process(self, value, args=()): if is_lv_font(value): return literal(f"&lv_font_{value}") if isinstance(value, str): return literal(f"{value}") return await super().process(value, args) lv_font = LvFont() def animated(value): if isinstance(value, bool): value = "ON" if value else "OFF" return LvConstant("LV_ANIM_", "OFF", "ON").one_of(value) def key_code(value): value = cv.Any(cv.All(cv.string_strict, cv.Length(min=1, max=1)), cv.uint8_t)(value) if isinstance(value, str): return ord(value[0]) return value async def get_end_value(config): return await lv_int.process(config.get(CONF_END_VALUE)) async def get_start_value(config): if CONF_START_VALUE in config: value = config[CONF_START_VALUE] else: value = config.get(CONF_VALUE) return await lv_int.process(value)