mirror of
https://github.com/esphome/esphome.git
synced 2026-05-24 01:37:15 +08:00
[lvgl] Ensure that on_value events fire on checked change (#16119)
This commit is contained in:
@@ -47,9 +47,15 @@ from esphome.helpers import write_file_if_changed
|
||||
from esphome.writer import clean_build
|
||||
from esphome.yaml_util import load_yaml
|
||||
|
||||
from . import defines as df, helpers, lv_validation as lvalid, widgets
|
||||
from .automation import focused_widgets, layers_to_code, lvgl_update, refreshed_widgets
|
||||
from .defines import CONF_ALIGN_TO_LAMBDA_ID
|
||||
from . import defines as df, lv_validation as lvalid, widgets
|
||||
from .automation import layers_to_code, lvgl_update
|
||||
from .defines import (
|
||||
CONF_ALIGN_TO_LAMBDA_ID,
|
||||
get_focused_widgets,
|
||||
get_lv_images_used,
|
||||
get_refreshed_widgets,
|
||||
set_widgets_completed,
|
||||
)
|
||||
from .encoders import (
|
||||
ENCODERS_CONFIG,
|
||||
encoders_to_code,
|
||||
@@ -58,7 +64,7 @@ from .encoders import (
|
||||
)
|
||||
from .gradient import GRADIENT_SCHEMA, gradients_to_code
|
||||
from .keypads import KEYPADS_CONFIG, keypads_to_code
|
||||
from .lv_validation import lv_bool, lv_images_used
|
||||
from .lv_validation import lv_bool
|
||||
from .lvcode import LvContext, LvglComponent, lv_event_t_ptr, lvgl_static
|
||||
from .schemas import (
|
||||
DISP_BG_SCHEMA,
|
||||
@@ -89,7 +95,6 @@ from .widgets import (
|
||||
add_widgets,
|
||||
get_screen_active,
|
||||
set_obj_properties,
|
||||
styles_used,
|
||||
)
|
||||
|
||||
# Import only what we actually use directly in this file
|
||||
@@ -144,7 +149,7 @@ def generate_lv_conf_h():
|
||||
df.LV_DEFINES + tuple(f"LV_USE_{w.upper()}" for w in WIDGET_TYPES)
|
||||
)
|
||||
# Get the defines that are actually used based on the config
|
||||
lv_defines = df.get_data(df.KEY_LV_DEFINES)
|
||||
lv_defines = df.get_defines()
|
||||
unused_defines = all_defines - set(lv_defines)
|
||||
# Create the content of lv_conf.h with the used defines set to their value, and the unused defines disabled
|
||||
definitions = [as_macro(m, v) for m, v in lv_defines.items()] + [
|
||||
@@ -211,7 +216,7 @@ def final_validation(config_list):
|
||||
buffer_frac = config[CONF_BUFFER_SIZE]
|
||||
if CORE.is_esp32 and buffer_frac > 0.5 and PSRAM_DOMAIN not in global_config:
|
||||
df.LOGGER.warning("buffer_size: may need to be reduced without PSRAM")
|
||||
for w in focused_widgets:
|
||||
for w in get_focused_widgets():
|
||||
path = global_config.get_path_for_id(w)
|
||||
widget_conf = global_config.get_config_for_path(path[:-1])
|
||||
if (
|
||||
@@ -222,7 +227,7 @@ def final_validation(config_list):
|
||||
"A non adjustable arc may not be focused",
|
||||
path,
|
||||
)
|
||||
for w in refreshed_widgets:
|
||||
for w in get_refreshed_widgets():
|
||||
path = global_config.get_path_for_id(w)
|
||||
widget_conf = global_config.get_config_for_path(path[:-1])
|
||||
if not any(isinstance(v, (Lambda, dict)) for v in widget_conf.values()):
|
||||
@@ -230,7 +235,7 @@ def final_validation(config_list):
|
||||
f"Widget '{w}' does not have any dynamic properties to refresh",
|
||||
)
|
||||
# Do per-widget type final validation for update actions
|
||||
for widget_type, update_configs in df.get_data(df.KEY_UPDATED_WIDGETS).items():
|
||||
for widget_type, update_configs in df.get_updated_widgets().items():
|
||||
for conf in update_configs:
|
||||
for id_conf in conf.get(CONF_ID, ()):
|
||||
name = id_conf[CONF_ID]
|
||||
@@ -279,7 +284,7 @@ async def to_code(configs):
|
||||
cg.RawExpression(f"ESPHOME_LOG_LEVEL_{config_0[CONF_LOG_LEVEL]}"),
|
||||
)
|
||||
df.add_define("LV_COLOR_DEPTH", config_0[CONF_COLOR_DEPTH])
|
||||
for font in helpers.lv_fonts_used:
|
||||
for font in df.get_lv_fonts_used():
|
||||
df.add_define(f"LV_FONT_{font.upper()}")
|
||||
|
||||
if config_0[CONF_COLOR_DEPTH] == 16:
|
||||
@@ -294,7 +299,7 @@ async def to_code(configs):
|
||||
cg.add_build_flag("-Isrc")
|
||||
|
||||
cg.add_global(lvgl_ns.using)
|
||||
for font in helpers.esphome_fonts_used:
|
||||
for font in df.get_esphome_fonts_used():
|
||||
await cg.get_variable(font)
|
||||
default_font = config_0[df.CONF_DEFAULT_FONT]
|
||||
if not lvalid.is_lv_font(default_font):
|
||||
@@ -377,8 +382,8 @@ async def to_code(configs):
|
||||
await lvgl_update(lv_component, config)
|
||||
await msgboxes_to_code(lv_component, config)
|
||||
# await disp_update(lv_component.get_disp(), config)
|
||||
# Set this directly since we are limited in how many methods can be added to the Widget class.
|
||||
Widget.widgets_completed = True
|
||||
# Mark all widgets as completed so awaiters of ``wait_for_widgets`` proceed.
|
||||
set_widgets_completed(True)
|
||||
async with LvContext():
|
||||
await generate_triggers()
|
||||
await generate_align_tos(configs[0])
|
||||
@@ -404,9 +409,8 @@ async def to_code(configs):
|
||||
)
|
||||
|
||||
# This must be done after all widgets are created
|
||||
for comp in helpers.lvgl_components_required:
|
||||
cg.add_define(f"USE_LVGL_{comp.upper()}")
|
||||
for use in helpers.lv_uses:
|
||||
styles_used = df.get_styles_used()
|
||||
for use in df.get_lv_uses():
|
||||
df.add_define(f"LV_USE_{use.upper()}")
|
||||
cg.add_define(f"USE_LVGL_{use.upper()}")
|
||||
|
||||
@@ -433,7 +437,7 @@ async def to_code(configs):
|
||||
} & styles_used:
|
||||
lv_image_formats.add("A8")
|
||||
|
||||
for image_id in lv_images_used:
|
||||
for image_id in get_lv_images_used():
|
||||
await cg.get_variable(image_id)
|
||||
metadata = get_image_metadata(image_id.id)
|
||||
image_type = IMAGE_TYPE[metadata.image_type]
|
||||
|
||||
@@ -25,7 +25,9 @@ from .defines import (
|
||||
PARTS,
|
||||
StaticCastExpression,
|
||||
add_warning,
|
||||
get_focused_widgets,
|
||||
get_options,
|
||||
get_refreshed_widgets,
|
||||
)
|
||||
from .lv_validation import lv_bool, lv_milliseconds
|
||||
from .lvcode import (
|
||||
@@ -70,9 +72,9 @@ from .widgets import (
|
||||
wait_for_widgets,
|
||||
)
|
||||
|
||||
# Record widgets that are used in a focused action here
|
||||
focused_widgets = set()
|
||||
refreshed_widgets = set()
|
||||
# Widgets that are used in a focused/refreshed action are tracked in
|
||||
# ``CORE.data`` (under the lvgl domain) so the state is cleared between
|
||||
# successive compilations / unit tests via ``CORE.reset()``.
|
||||
|
||||
|
||||
async def layers_to_code(lv_component, config):
|
||||
@@ -316,7 +318,7 @@ async def resume_action_to_code(config, action_id, template_arg, args):
|
||||
)
|
||||
async def obj_disable_to_code(config, action_id, template_arg, args):
|
||||
async def do_disable(widget: Widget):
|
||||
widget.add_state(LV_STATE.DISABLED)
|
||||
widget.set_state(LV_STATE.DISABLED, True)
|
||||
|
||||
return await action_to_code(
|
||||
await get_widgets(config), do_disable, action_id, template_arg, args
|
||||
@@ -328,7 +330,7 @@ async def obj_disable_to_code(config, action_id, template_arg, args):
|
||||
)
|
||||
async def obj_enable_to_code(config, action_id, template_arg, args):
|
||||
async def do_enable(widget: Widget):
|
||||
widget.clear_state(LV_STATE.DISABLED)
|
||||
widget.set_state(LV_STATE.DISABLED, False)
|
||||
|
||||
return await action_to_code(
|
||||
await get_widgets(config), do_enable, action_id, template_arg, args
|
||||
@@ -361,7 +363,7 @@ async def obj_show_to_code(config, action_id, template_arg, args):
|
||||
|
||||
def focused_id(value):
|
||||
value = cv.use_id(lv_pseudo_button_t)(value)
|
||||
focused_widgets.add(value)
|
||||
get_focused_widgets().add(value)
|
||||
return value
|
||||
|
||||
|
||||
@@ -446,8 +448,9 @@ async def obj_update_to_code(config, action_id, template_arg, args):
|
||||
|
||||
|
||||
def validate_refresh_config(config):
|
||||
refreshed = get_refreshed_widgets()
|
||||
for w in config:
|
||||
refreshed_widgets.add(w[CONF_ID])
|
||||
refreshed.add(w[CONF_ID])
|
||||
return config
|
||||
|
||||
|
||||
|
||||
@@ -19,46 +19,134 @@ from esphome.cpp_generator import (
|
||||
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
|
||||
from esphome.types import Expression, SafeExpType
|
||||
|
||||
from .helpers import requires_component
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
lvgl_ns = cg.esphome_ns.namespace("lvgl")
|
||||
|
||||
DOMAIN = "lvgl"
|
||||
KEY_COLOR_FORMATS = "color_formats"
|
||||
KEY_ESPHOME_FONTS_USED = "esphome_fonts_used"
|
||||
KEY_FOCUSED_WIDGETS = "focused_widgets"
|
||||
KEY_LV_DEFINES = "lv_defines"
|
||||
KEY_LV_FONTS_USED = "lv_fonts_used"
|
||||
KEY_LV_IMAGES_USED = "lv_images_used"
|
||||
KEY_LV_USES = "lv_uses"
|
||||
KEY_NAMED_STYLES = "named_styles"
|
||||
KEY_REFRESHED_WIDGETS = "refreshed_widgets"
|
||||
KEY_REMAPPED_USES = "remapped_uses"
|
||||
KEY_STYLES_USED = "styles_used"
|
||||
KEY_THEME_WIDGET_MAP = "theme_widget_map"
|
||||
KEY_UPDATED_WIDGETS = "updated_widgets"
|
||||
KEY_WIDGET_MAP = "widget_map"
|
||||
KEY_WIDGETS_COMPLETED = "widgets_completed"
|
||||
KEY_OPTIONS = "options"
|
||||
KEY_WARNINGS = "warnings"
|
||||
|
||||
# Initial set of LVGL features that are always enabled.
|
||||
_INITIAL_LV_USES = frozenset(
|
||||
{
|
||||
"USER_DATA",
|
||||
"LOG",
|
||||
"STYLE",
|
||||
"FONT_PLACEHOLDER",
|
||||
"THEME_DEFAULT",
|
||||
}
|
||||
)
|
||||
|
||||
def get_data(key, default=None):
|
||||
|
||||
# These collections accumulate state across a single compilation run. They
|
||||
# are stored under ``CORE.data`` (which ``CORE.reset()`` clears between runs)
|
||||
# rather than as module-level globals, otherwise they would leak between
|
||||
# successive compilations / unit tests.
|
||||
|
||||
|
||||
def _get_data(key: str, default: Any) -> Any:
|
||||
"""
|
||||
Get a data structure from the global data store by key
|
||||
:param key: A key for the data
|
||||
:param default: The default data - the default is an empty dict
|
||||
:param default: The default data
|
||||
:return:
|
||||
"""
|
||||
return CORE.data.setdefault(DOMAIN, {}).setdefault(
|
||||
key, {} if default is None else default
|
||||
)
|
||||
return CORE.data.setdefault(DOMAIN, {}).setdefault(key, default)
|
||||
|
||||
|
||||
def get_warnings():
|
||||
return get_data(KEY_WARNINGS, set())
|
||||
def get_lv_images_used() -> set[ID]:
|
||||
return _get_data(KEY_LV_IMAGES_USED, set())
|
||||
|
||||
|
||||
def get_remapped_uses():
|
||||
return get_data(KEY_REMAPPED_USES, set())
|
||||
def get_lv_uses() -> set[str]:
|
||||
return _get_data(KEY_LV_USES, set(_INITIAL_LV_USES))
|
||||
|
||||
|
||||
def add_warning(msg: str):
|
||||
def get_lv_fonts_used() -> set[str]:
|
||||
return _get_data(KEY_LV_FONTS_USED, set())
|
||||
|
||||
|
||||
def get_esphome_fonts_used() -> set[ID]:
|
||||
return _get_data(KEY_ESPHOME_FONTS_USED, set())
|
||||
|
||||
|
||||
def add_lv_use(*names: str) -> None:
|
||||
uses = get_lv_uses()
|
||||
for name in names:
|
||||
uses.add(name)
|
||||
|
||||
|
||||
def get_warnings() -> set[str]:
|
||||
return _get_data(KEY_WARNINGS, set())
|
||||
|
||||
|
||||
def get_remapped_uses() -> set[str]:
|
||||
return _get_data(KEY_REMAPPED_USES, set())
|
||||
|
||||
|
||||
def add_warning(msg: str) -> None:
|
||||
get_warnings().add(msg)
|
||||
|
||||
|
||||
def get_options():
|
||||
return get_data(KEY_OPTIONS)
|
||||
def get_options() -> dict[str, Any]:
|
||||
return _get_data(KEY_OPTIONS, {})
|
||||
|
||||
|
||||
def get_defines() -> dict[str, str]:
|
||||
return _get_data(KEY_LV_DEFINES, {})
|
||||
|
||||
|
||||
def get_updated_widgets() -> dict:
|
||||
return _get_data(KEY_UPDATED_WIDGETS, {})
|
||||
|
||||
|
||||
def get_theme_widget_map() -> dict[str, Any]:
|
||||
return _get_data(KEY_THEME_WIDGET_MAP, {})
|
||||
|
||||
|
||||
def get_styles_used() -> set[str]:
|
||||
return _get_data(KEY_STYLES_USED, set())
|
||||
|
||||
|
||||
def get_widget_map() -> dict[str, Any]:
|
||||
return _get_data(KEY_WIDGET_MAP, {})
|
||||
|
||||
|
||||
def get_widgets_completed() -> bool:
|
||||
# ``[value]`` rather than the bare value so that we can mutate the
|
||||
# entry in place; ``CORE.data`` is reset for us between runs.
|
||||
return _get_data(KEY_WIDGETS_COMPLETED, [False])[0]
|
||||
|
||||
|
||||
def set_widgets_completed(value: bool) -> None:
|
||||
_get_data(KEY_WIDGETS_COMPLETED, [False])[0] = value
|
||||
|
||||
|
||||
def is_widget_completed(name: ID) -> bool:
|
||||
return name in get_widget_map()
|
||||
|
||||
|
||||
def get_focused_widgets() -> set:
|
||||
return _get_data(KEY_FOCUSED_WIDGETS, set())
|
||||
|
||||
|
||||
def get_refreshed_widgets() -> set:
|
||||
return _get_data(KEY_REFRESHED_WIDGETS, set())
|
||||
|
||||
|
||||
class StaticCastExpression(Expression):
|
||||
@@ -72,8 +160,8 @@ class StaticCastExpression(Expression):
|
||||
return f"static_cast<{self.type}>({self.exp})"
|
||||
|
||||
|
||||
def add_define(macro, value="1"):
|
||||
lv_defines = get_data(KEY_LV_DEFINES)
|
||||
def add_define(macro: str, value="1"):
|
||||
lv_defines = get_defines()
|
||||
value = str(value)
|
||||
if lv_defines.setdefault(macro, value) != value:
|
||||
LOGGER.error(
|
||||
@@ -82,8 +170,8 @@ def add_define(macro, value="1"):
|
||||
lv_defines[macro] = value
|
||||
|
||||
|
||||
def is_defined(macro):
|
||||
return macro in get_data(KEY_LV_DEFINES)
|
||||
def is_defined(macro) -> bool:
|
||||
return macro in get_defines()
|
||||
|
||||
|
||||
def literal(arg) -> MockObj:
|
||||
@@ -96,7 +184,7 @@ def addr(arg) -> MockObj:
|
||||
return MockObj(f"&{arg}")
|
||||
|
||||
|
||||
def call_lambda(lamb: LambdaExpression):
|
||||
def call_lambda(lamb: LambdaExpression) -> Expression:
|
||||
"""
|
||||
Given a lambda, either reduce to a simple expression or call it, possibly with parameters
|
||||
from the surrounding context
|
||||
@@ -135,7 +223,7 @@ class LValidator:
|
||||
|
||||
def __call__(self, value):
|
||||
if self.requires:
|
||||
value = requires_component(self.requires)(value)
|
||||
value = cv.requires_component(self.requires)(value)
|
||||
if isinstance(value, cv.Lambda):
|
||||
return cv.returning_lambda(value)
|
||||
return self.validator(value)
|
||||
@@ -196,7 +284,7 @@ class LvConstant(LValidator):
|
||||
cv.ensure_list(self.one_of), cg.uint32, retmapper=self.mapper
|
||||
)
|
||||
|
||||
def mapper(self, value):
|
||||
def mapper(self, value) -> Any:
|
||||
if not isinstance(value, list):
|
||||
value = [value]
|
||||
value = [
|
||||
|
||||
@@ -13,8 +13,8 @@ from .defines import (
|
||||
CONF_LONG_PRESS_REPEAT_TIME,
|
||||
CONF_LONG_PRESS_TIME,
|
||||
CONF_RIGHT_BUTTON,
|
||||
add_lv_use,
|
||||
)
|
||||
from .helpers import lvgl_components_required, requires_component
|
||||
from .lvcode import lv, lv_add, lv_assign, lv_expr, lv_Pvariable
|
||||
from .schemas import ENCODER_SCHEMA
|
||||
from .types import lv_group_t, lv_indev_type_t, lv_key_t
|
||||
@@ -26,7 +26,8 @@ ENCODERS_CONFIG = cv.ensure_list(
|
||||
cv.Required(CONF_ENTER_BUTTON): cv.use_id(BinarySensor),
|
||||
cv.Required(CONF_SENSOR): cv.Any(
|
||||
cv.All(
|
||||
cv.use_id(RotaryEncoderSensor), requires_component("rotary_encoder")
|
||||
cv.use_id(RotaryEncoderSensor),
|
||||
cv.requires_component("rotary_encoder"),
|
||||
),
|
||||
cv.Schema(
|
||||
{
|
||||
@@ -48,7 +49,7 @@ def get_default_group(config):
|
||||
|
||||
async def encoders_to_code(var, config, default_group):
|
||||
for enc_conf in config[CONF_ENCODERS]:
|
||||
lvgl_components_required.add("KEY_LISTENER")
|
||||
add_lv_use("KEY_LISTENER", "ROTARY_ENCODER")
|
||||
lpt = enc_conf[CONF_LONG_PRESS_TIME].total_milliseconds
|
||||
lprt = enc_conf[CONF_LONG_PRESS_REPEAT_TIME].total_milliseconds
|
||||
listener = cg.new_Pvariable(
|
||||
|
||||
@@ -12,8 +12,14 @@ from esphome.const import (
|
||||
from esphome.core import ID
|
||||
from esphome.cpp_generator import MockObj
|
||||
|
||||
from .defines import CONF_GRADIENTS, CONF_OPA, LV_DITHER, add_define, add_warning
|
||||
from .helpers import add_lv_use
|
||||
from .defines import (
|
||||
CONF_GRADIENTS,
|
||||
CONF_OPA,
|
||||
LV_DITHER,
|
||||
add_define,
|
||||
add_lv_use,
|
||||
add_warning,
|
||||
)
|
||||
from .lv_validation import lv_color, lv_percentage, opacity
|
||||
from .lvcode import lv
|
||||
from .types import lv_color_t, lv_gradient_t, lv_opa_t
|
||||
|
||||
@@ -5,23 +5,6 @@ from esphome.const import CONF_ARGS, CONF_FORMAT
|
||||
|
||||
CONF_IF_NAN = "if_nan"
|
||||
|
||||
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()
|
||||
lvgl_components_required = set()
|
||||
|
||||
# noqa
|
||||
f_regex = re.compile(
|
||||
@@ -66,11 +49,3 @@ def validate_printf(value):
|
||||
"Use of 'if_nan' requires a single valid printf-pattern of type %f"
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
def requires_component(comp):
|
||||
def validator(value):
|
||||
lvgl_components_required.add(comp)
|
||||
return cv.requires_component(comp)(value)
|
||||
|
||||
return validator
|
||||
|
||||
@@ -9,9 +9,9 @@ from .defines import (
|
||||
CONF_KEYPADS,
|
||||
CONF_LONG_PRESS_REPEAT_TIME,
|
||||
CONF_LONG_PRESS_TIME,
|
||||
add_lv_use,
|
||||
literal,
|
||||
)
|
||||
from .helpers import lvgl_components_required
|
||||
from .lvcode import lv, lv_assign, lv_expr, lv_Pvariable
|
||||
from .schemas import ENCODER_SCHEMA
|
||||
from .types import lv_group_t, lv_indev_type_t
|
||||
@@ -52,7 +52,7 @@ KEYPADS_CONFIG = cv.ensure_list(
|
||||
|
||||
async def keypads_to_code(var, config, default_group):
|
||||
for enc_conf in config[CONF_KEYPADS]:
|
||||
lvgl_components_required.add("KEY_LISTENER")
|
||||
add_lv_use("KEY_LISTENER")
|
||||
lpt = enc_conf[CONF_LONG_PRESS_TIME].total_milliseconds
|
||||
lprt = enc_conf[CONF_LONG_PRESS_REPEAT_TIME].total_milliseconds
|
||||
listener = cg.new_Pvariable(
|
||||
|
||||
@@ -37,7 +37,7 @@ class LVLight : public light::LightOutput {
|
||||
void set_value_(lv_color_t value) {
|
||||
lv_led_set_color(this->obj_, value);
|
||||
lv_led_on(this->obj_);
|
||||
lv_obj_send_event(this->obj_, lv_api_event, nullptr);
|
||||
lv_obj_send_event(this->obj_, lv_update_event, nullptr);
|
||||
}
|
||||
lv_obj_t *obj_{};
|
||||
optional<lv_color_t> initial_value_{};
|
||||
|
||||
@@ -31,16 +31,14 @@ from .defines import (
|
||||
LValidator,
|
||||
LvConstant,
|
||||
StaticCastExpression,
|
||||
add_lv_use,
|
||||
call_lambda,
|
||||
get_esphome_fonts_used,
|
||||
get_lv_fonts_used,
|
||||
get_lv_images_used,
|
||||
literal,
|
||||
)
|
||||
from .helpers import (
|
||||
CONF_IF_NAN,
|
||||
add_lv_use,
|
||||
esphome_fonts_used,
|
||||
lv_fonts_used,
|
||||
requires_component,
|
||||
)
|
||||
from .helpers import CONF_IF_NAN
|
||||
from .types import lv_coord_t, lv_gradient_t, lv_opa_t
|
||||
|
||||
LV_OPA = LvConstant("LV_OPA_", "TRANSP", "COVER")
|
||||
@@ -370,14 +368,11 @@ 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.requires_component("image")(value)
|
||||
value = cv.use_id(Image_)(value)
|
||||
lv_images_used.add(value)
|
||||
add_lv_use("img", "label")
|
||||
get_lv_images_used().add(value)
|
||||
add_lv_use("label")
|
||||
return value
|
||||
|
||||
|
||||
@@ -496,7 +491,7 @@ 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)
|
||||
get_lv_fonts_used().add(fontval)
|
||||
return fontval
|
||||
|
||||
def validator(value):
|
||||
@@ -506,8 +501,8 @@ class LvFont(LValidator):
|
||||
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)
|
||||
get_esphome_fonts_used().add(fontval)
|
||||
return cv.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*
|
||||
|
||||
@@ -29,10 +29,9 @@ LvglComponent = lvgl_ns.class_("LvglComponent", cg.PollingComponent)
|
||||
LVGL_COMP_ARG = [(LvglComponent.operator("ptr"), LVGL_COMP)]
|
||||
lv_event_t_ptr = cg.global_ns.namespace("lv_event_t").operator("ptr")
|
||||
EVENT_ARG = [(lv_event_t_ptr, "event")]
|
||||
# Two custom events; API_EVENT is fired when an entity is updated remotely by an API interaction;
|
||||
# One custom event;
|
||||
# UPDATE_EVENT is fired when an entity is programmatically updated locally.
|
||||
# VALUE_CHANGED is the event generated by LVGL when an entity's value changes through user interaction.
|
||||
API_EVENT = literal("lvgl::lv_api_event")
|
||||
UPDATE_EVENT = literal("lvgl::lv_update_event")
|
||||
|
||||
|
||||
|
||||
@@ -147,7 +147,6 @@ void LvglComponent::render_start_cb(lv_event_t *event) {
|
||||
comp->draw_start_();
|
||||
}
|
||||
|
||||
lv_event_code_t lv_api_event; // NOLINT
|
||||
lv_event_code_t lv_update_event; // NOLINT
|
||||
void LvglComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
@@ -194,7 +193,6 @@ void LvglComponent::esphome_lvgl_init() {
|
||||
LV_GLOBAL_DEFAULT()->font_draw_buf_handlers.buf_free_cb = lv_free_core;
|
||||
lv_tick_set_cb([] { return millis(); });
|
||||
lv_update_event = static_cast<lv_event_code_t>(lv_event_register_id());
|
||||
lv_api_event = static_cast<lv_event_code_t>(lv_event_register_id());
|
||||
}
|
||||
|
||||
void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event) {
|
||||
@@ -547,7 +545,7 @@ void LvSelectable::set_selected_text(const std::string &text, lv_anim_enable_t a
|
||||
auto index = std::find(this->options_.begin(), this->options_.end(), text);
|
||||
if (index != this->options_.end()) {
|
||||
this->set_selected_index(index - this->options_.begin(), anim);
|
||||
lv_obj_send_event(this->obj, lv_api_event, nullptr);
|
||||
lv_obj_send_event(this->obj, lv_update_event, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,6 @@ using lv_color_data = uint16_t;
|
||||
using lv_color_data = uint32_t;
|
||||
#endif
|
||||
|
||||
extern lv_event_code_t lv_api_event; // NOLINT
|
||||
extern lv_event_code_t lv_update_event; // NOLINT
|
||||
extern std::string lv_event_code_name_for(lv_event_t *event);
|
||||
|
||||
@@ -227,11 +226,43 @@ class LvglComponent : public PollingComponent {
|
||||
* Initialize the LVGL library and register custom events.
|
||||
*/
|
||||
static void esphome_lvgl_init();
|
||||
|
||||
// Convenience overloads for adding a callback for one or more events
|
||||
static void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event);
|
||||
static void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2);
|
||||
static void add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1, lv_event_code_t event2,
|
||||
lv_event_code_t event3);
|
||||
|
||||
// change the state of a widget and fire an event if changed (only needed for CHECKED)
|
||||
|
||||
static void lv_obj_set_state_value(lv_obj_t *obj, lv_state_t state, bool value) {
|
||||
if (value != lv_obj_has_state(obj, state)) {
|
||||
if (value) {
|
||||
lv_obj_add_state(obj, state);
|
||||
} else {
|
||||
lv_obj_remove_state(obj, state);
|
||||
}
|
||||
if (state == LV_STATE_CHECKED)
|
||||
lv_obj_send_event(obj, lv_update_event, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
// change the state of a buttonmatrix button and fire an event if changed (only needed for CHECKED)
|
||||
#ifdef USE_LVGL_BUTTONMATRIX
|
||||
static void lv_buttonmatrix_set_button_ctrl_value(lv_obj_t *obj, uint32_t index, lv_buttonmatrix_ctrl_t ctrl,
|
||||
bool value) {
|
||||
if (value != lv_buttonmatrix_has_button_ctrl(obj, index, ctrl)) {
|
||||
if (value) {
|
||||
lv_buttonmatrix_set_button_ctrl(obj, index, ctrl);
|
||||
} else {
|
||||
lv_buttonmatrix_clear_button_ctrl(obj, index, ctrl);
|
||||
}
|
||||
if (ctrl == LV_BUTTONMATRIX_CTRL_CHECKED)
|
||||
lv_obj_send_event(obj, lv_update_event, nullptr);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void add_page(LvPageType *page);
|
||||
void show_page(size_t index, lv_screen_load_anim_t anim, uint32_t time);
|
||||
void show_next_page(lv_screen_load_anim_t anim, uint32_t time);
|
||||
|
||||
@@ -7,7 +7,6 @@ from esphome.cpp_generator import MockObj
|
||||
from ..defines import CONF_ANIMATED, CONF_UPDATE_ON_RELEASE, CONF_WIDGET
|
||||
from ..lv_validation import animated
|
||||
from ..lvcode import (
|
||||
API_EVENT,
|
||||
EVENT_ARG,
|
||||
UPDATE_EVENT,
|
||||
LambdaContext,
|
||||
@@ -40,7 +39,7 @@ async def to_code(config):
|
||||
await widget.set_property(
|
||||
"value", MockObj("v") * MockObj(widget.get_scale()), config[CONF_ANIMATED]
|
||||
)
|
||||
lv_obj.send_event(widget.obj, API_EVENT, cg.nullptr)
|
||||
lv_obj.send_event(widget.obj, UPDATE_EVENT, cg.nullptr)
|
||||
event_code = (
|
||||
LV_EVENT.VALUE_CHANGED
|
||||
if not config[CONF_UPDATE_ON_RELEASE]
|
||||
|
||||
@@ -33,7 +33,7 @@ from .defines import (
|
||||
get_remapped_uses,
|
||||
is_press_event,
|
||||
)
|
||||
from .helpers import CONF_IF_NAN, requires_component, validate_printf
|
||||
from .helpers import CONF_IF_NAN, validate_printf
|
||||
from .layout import (
|
||||
FLEX_OBJ_SCHEMA,
|
||||
GRID_CELL_SCHEMA,
|
||||
@@ -112,7 +112,7 @@ PRESS_TIME = cv.All(
|
||||
ENCODER_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.All(
|
||||
cv.declare_id(LVEncoderListener), requires_component("binary_sensor")
|
||||
cv.declare_id(LVEncoderListener), cv.requires_component("binary_sensor")
|
||||
),
|
||||
cv.Optional(CONF_GROUP): cv.declare_id(lv_group_t),
|
||||
cv.Optional(df.CONF_INITIAL_FOCUS): cv.All(
|
||||
@@ -406,7 +406,7 @@ def _update_widget(widget_type: WidgetType) -> Callable[[dict], dict]:
|
||||
"""
|
||||
|
||||
def validator(value: dict) -> dict:
|
||||
df.get_data(df.KEY_UPDATED_WIDGETS).setdefault(widget_type, []).append(value)
|
||||
df.get_updated_widgets().setdefault(widget_type, []).append(value)
|
||||
return value
|
||||
|
||||
return validator
|
||||
@@ -573,7 +573,7 @@ def any_widget_schema(extras=None):
|
||||
container_validator = container_schema(widget_type, extras=extras)
|
||||
if required := widget_type.required_component:
|
||||
container_validator = cv.All(
|
||||
container_validator, requires_component(required)
|
||||
container_validator, cv.requires_component(required)
|
||||
)
|
||||
# Apply custom validation
|
||||
path = [key] if is_dict else [index, key]
|
||||
|
||||
@@ -3,7 +3,6 @@ import esphome.config_validation as cv
|
||||
|
||||
from ..defines import CONF_WIDGET
|
||||
from ..lvcode import (
|
||||
API_EVENT,
|
||||
EVENT_ARG,
|
||||
UPDATE_EVENT,
|
||||
LambdaContext,
|
||||
@@ -35,7 +34,6 @@ async def to_code(config):
|
||||
widget.obj,
|
||||
await lamb.get_lambda(),
|
||||
LV_EVENT.VALUE_CHANGED,
|
||||
API_EVENT,
|
||||
UPDATE_EVENT,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -4,12 +4,18 @@ import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.core import ID
|
||||
|
||||
from .defines import CONF_STYLE_DEFINITIONS, CONF_THEME, LValidator, literal
|
||||
from .helpers import add_lv_use
|
||||
from .defines import (
|
||||
CONF_STYLE_DEFINITIONS,
|
||||
CONF_THEME,
|
||||
LValidator,
|
||||
add_lv_use,
|
||||
get_theme_widget_map,
|
||||
literal,
|
||||
)
|
||||
from .lvcode import LambdaContext, lv
|
||||
from .schemas import ALL_STYLES, FULL_STYLE_SCHEMA, WIDGET_TYPES, remap_property
|
||||
from .types import ObjUpdateAction, lv_style_t
|
||||
from .widgets import collect_parts, theme_widget_map, wait_for_widgets
|
||||
from .widgets import collect_parts, wait_for_widgets
|
||||
|
||||
|
||||
def has_style_props(config) -> bool:
|
||||
@@ -97,4 +103,4 @@ async def theme_to_code(config):
|
||||
)
|
||||
for state, props in states.items()
|
||||
}
|
||||
theme_widget_map[w_name] = styles
|
||||
get_theme_widget_map()[w_name] = styles
|
||||
|
||||
@@ -7,14 +7,11 @@ from esphome.cpp_types import Component
|
||||
|
||||
from ..defines import CONF_WIDGET, literal
|
||||
from ..lvcode import (
|
||||
API_EVENT,
|
||||
EVENT_ARG,
|
||||
UPDATE_EVENT,
|
||||
LambdaContext,
|
||||
LvConditional,
|
||||
LvContext,
|
||||
lv_add,
|
||||
lv_obj,
|
||||
lvgl_static,
|
||||
)
|
||||
from ..types import LV_EVENT, LV_STATE, lv_pseudo_button_t, lvgl_ns
|
||||
@@ -35,11 +32,7 @@ async def to_code(config):
|
||||
switch_id = MockObj(config[CONF_ID], "->")
|
||||
v = literal("v")
|
||||
async with LambdaContext([(cg.bool_, "v")]) as control:
|
||||
with LvConditional(v) as cond:
|
||||
widget.add_state(LV_STATE.CHECKED)
|
||||
cond.else_()
|
||||
widget.clear_state(LV_STATE.CHECKED)
|
||||
lv_obj.send_event(widget.obj, API_EVENT, cg.nullptr)
|
||||
widget.set_state(LV_STATE.CHECKED, literal("v"))
|
||||
control.add(switch_id.publish_state(v))
|
||||
switch = cg.new_Pvariable(config[CONF_ID], await control.get_lambda())
|
||||
await cg.register_component(switch, config)
|
||||
|
||||
@@ -5,7 +5,6 @@ import esphome.config_validation as cv
|
||||
|
||||
from ..defines import CONF_WIDGET
|
||||
from ..lvcode import (
|
||||
API_EVENT,
|
||||
EVENT_ARG,
|
||||
UPDATE_EVENT,
|
||||
LambdaContext,
|
||||
@@ -33,7 +32,7 @@ async def to_code(config):
|
||||
await wait_for_widgets()
|
||||
async with LambdaContext([(cg.std_string, "text_value")]) as control:
|
||||
await widget.set_property("text", "text_value.c_str()")
|
||||
lv_obj.send_event(widget.obj, API_EVENT, cg.nullptr)
|
||||
lv_obj.send_event(widget.obj, UPDATE_EVENT, cg.nullptr)
|
||||
control.add(textvar.publish_state(widget.get_value()))
|
||||
async with LambdaContext(EVENT_ARG) as lamb:
|
||||
lv_add(textvar.publish_state(widget.get_value()))
|
||||
|
||||
@@ -6,14 +6,7 @@ from esphome.components.text_sensor import (
|
||||
import esphome.config_validation as cv
|
||||
|
||||
from ..defines import CONF_WIDGET
|
||||
from ..lvcode import (
|
||||
API_EVENT,
|
||||
EVENT_ARG,
|
||||
UPDATE_EVENT,
|
||||
LambdaContext,
|
||||
LvContext,
|
||||
lvgl_static,
|
||||
)
|
||||
from ..lvcode import EVENT_ARG, UPDATE_EVENT, LambdaContext, LvContext, lvgl_static
|
||||
from ..types import LV_EVENT, LvText
|
||||
from ..widgets import get_widgets, wait_for_widgets
|
||||
|
||||
@@ -37,7 +30,6 @@ async def to_code(config):
|
||||
widget.obj,
|
||||
await pressed_ctx.get_lambda(),
|
||||
LV_EVENT.VALUE_CHANGED,
|
||||
API_EVENT,
|
||||
UPDATE_EVENT,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -8,15 +8,17 @@ from .defines import (
|
||||
CONF_LONG_PRESS_REPEAT_TIME,
|
||||
CONF_LONG_PRESS_TIME,
|
||||
CONF_TOUCHSCREENS,
|
||||
add_lv_use,
|
||||
)
|
||||
from .helpers import lvgl_components_required
|
||||
from .schemas import PRESS_TIME
|
||||
from .types import LVTouchListener
|
||||
|
||||
CONF_TOUCHSCREEN = "touchscreen"
|
||||
TOUCHSCREENS_CONFIG = cv.maybe_simple_value(
|
||||
{
|
||||
cv.Required(CONF_TOUCHSCREEN_ID): cv.use_id(Touchscreen),
|
||||
cv.Required(CONF_TOUCHSCREEN_ID): cv.All(
|
||||
cv.use_id(Touchscreen), cv.requires_component(CONF_TOUCHSCREEN)
|
||||
),
|
||||
cv.Optional(CONF_LONG_PRESS_TIME, default="400ms"): PRESS_TIME,
|
||||
cv.Optional(CONF_LONG_PRESS_REPEAT_TIME, default="100ms"): PRESS_TIME,
|
||||
cv.GenerateID(): cv.declare_id(LVTouchListener),
|
||||
@@ -34,7 +36,7 @@ def touchscreen_schema(config):
|
||||
|
||||
async def touchscreens_to_code(lv_component, config):
|
||||
for tconf in config[CONF_TOUCHSCREENS]:
|
||||
lvgl_components_required.add(CONF_TOUCHSCREEN)
|
||||
add_lv_use(CONF_TOUCHSCREEN)
|
||||
touchscreen = await cg.get_variable(tconf[CONF_TOUCHSCREEN_ID])
|
||||
lpt = tconf[CONF_LONG_PRESS_TIME].total_milliseconds
|
||||
lprt = tconf[CONF_LONG_PRESS_REPEAT_TIME].total_milliseconds
|
||||
|
||||
@@ -24,11 +24,11 @@ from .defines import (
|
||||
LV_SCREEN_EVENT_MAP,
|
||||
LV_SCREEN_EVENT_TRIGGERS,
|
||||
SWIPE_TRIGGERS,
|
||||
get_widget_map,
|
||||
is_press_event,
|
||||
literal,
|
||||
)
|
||||
from .lvcode import (
|
||||
API_EVENT,
|
||||
EVENT_ARG,
|
||||
UPDATE_EVENT,
|
||||
LambdaContext,
|
||||
@@ -39,7 +39,7 @@ from .lvcode import (
|
||||
lvgl_static,
|
||||
)
|
||||
from .types import LV_EVENT, lv_point_t
|
||||
from .widgets import LvScrActType, get_screen_active, widget_map
|
||||
from .widgets import LvScrActType, get_screen_active
|
||||
|
||||
|
||||
async def add_on_boot_triggers(triggers):
|
||||
@@ -58,7 +58,7 @@ async def generate_triggers():
|
||||
all_triggers = (
|
||||
LV_EVENT_TRIGGERS + LV_DISPLAY_EVENT_TRIGGERS + LV_SCREEN_EVENT_TRIGGERS
|
||||
)
|
||||
for w in widget_map.values():
|
||||
for w in get_widget_map().values():
|
||||
config = w.config
|
||||
if isinstance(w.type, LvScrActType):
|
||||
w = get_screen_active(w.var)
|
||||
@@ -89,7 +89,6 @@ async def generate_triggers():
|
||||
conf,
|
||||
w,
|
||||
LV_EVENT.VALUE_CHANGED,
|
||||
API_EVENT,
|
||||
UPDATE_EVENT,
|
||||
)
|
||||
|
||||
@@ -104,6 +103,7 @@ async def generate_align_tos(config: dict):
|
||||
:param config:
|
||||
:return:
|
||||
"""
|
||||
widget_map = get_widget_map()
|
||||
align_tos = tuple(
|
||||
w for w in widget_map.values() if w.config and CONF_ALIGN_TO in w.config
|
||||
)
|
||||
|
||||
@@ -15,6 +15,7 @@ from esphome.const import (
|
||||
from esphome.core import ID, EsphomeError, TimePeriod
|
||||
from esphome.coroutine import FakeAwaitable
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import Expression
|
||||
|
||||
from ..defines import (
|
||||
CONF_FLEX_ALIGN_CROSS,
|
||||
@@ -38,11 +39,15 @@ from ..defines import (
|
||||
TYPE_FLEX,
|
||||
TYPE_GRID,
|
||||
LValidator,
|
||||
add_lv_use,
|
||||
call_lambda,
|
||||
get_styles_used,
|
||||
get_theme_widget_map,
|
||||
get_widget_map,
|
||||
get_widgets_completed,
|
||||
join_enums,
|
||||
literal,
|
||||
)
|
||||
from ..helpers import add_lv_use
|
||||
from ..lvcode import (
|
||||
LvConditional,
|
||||
add_line_marks,
|
||||
@@ -52,6 +57,7 @@ from ..lvcode import (
|
||||
lv_expr,
|
||||
lv_obj,
|
||||
lv_Pvariable,
|
||||
lvgl_static,
|
||||
)
|
||||
from ..types import (
|
||||
LV_STATE,
|
||||
@@ -65,9 +71,6 @@ from ..types import (
|
||||
|
||||
EVENT_LAMB = "event_lamb__"
|
||||
|
||||
theme_widget_map = {}
|
||||
styles_used = set()
|
||||
|
||||
|
||||
class WidgetType:
|
||||
"""
|
||||
@@ -157,7 +160,7 @@ class WidgetType:
|
||||
await self.on_create(var, config)
|
||||
|
||||
w = Widget.create(wid, var, self, config)
|
||||
if theme := theme_widget_map.get(self.name):
|
||||
if theme := get_theme_widget_map().get(self.name):
|
||||
for part, states in theme.items():
|
||||
part = "LV_PART_" + part.upper()
|
||||
for state, style in states.items():
|
||||
@@ -240,8 +243,6 @@ class Widget:
|
||||
This class has a lot of methods. Adding any more runs foul of lint checks ("too many public methods").
|
||||
"""
|
||||
|
||||
widgets_completed = False
|
||||
|
||||
def __init__(self, var, wtype: WidgetType, config: dict = None):
|
||||
self.var = var
|
||||
self.type = wtype
|
||||
@@ -262,21 +263,14 @@ class Widget:
|
||||
@staticmethod
|
||||
def create(name, var, wtype: WidgetType, config: dict = None):
|
||||
w = Widget(var, wtype, config)
|
||||
widget_map[name] = w
|
||||
get_widget_map()[name] = w
|
||||
return w
|
||||
|
||||
def add_state(self, state):
|
||||
if "|" in state:
|
||||
state = f"(lv_state_t)({state})"
|
||||
return lv_obj.add_state(self.obj, literal(state))
|
||||
def set_state(self, state: MockObj, value: bool | Expression):
|
||||
lv_add(lvgl_static.lv_obj_set_state_value(self.obj, state, value))
|
||||
|
||||
def clear_state(self, state):
|
||||
if "|" in state:
|
||||
state = f"(lv_state_t)({state})"
|
||||
return lv_obj.remove_state(self.obj, literal(state))
|
||||
|
||||
def has_state(self, state):
|
||||
return (lv_expr.obj_get_state(self.obj) & literal(state)) != 0
|
||||
def has_state(self, state: MockObj):
|
||||
return lv_expr.obj_has_state(self.obj, state)
|
||||
|
||||
def is_pressed(self):
|
||||
return self.has_state(LV_STATE.PRESSED)
|
||||
@@ -346,10 +340,10 @@ class Widget:
|
||||
ltype = ltype or self.__type_base()
|
||||
return cg.RawExpression(f"lv_{ltype}_get_{prop}({self.obj})")
|
||||
|
||||
def set_style(self, prop, value, state=LV_STATE.DEFAULT):
|
||||
def set_style(self, prop: str, value, state=LV_STATE.DEFAULT):
|
||||
if value is None:
|
||||
return
|
||||
styles_used.add(prop)
|
||||
get_styles_used().add(prop)
|
||||
if isinstance(value, str):
|
||||
value = literal(value)
|
||||
lv.call(f"obj_set_style_{prop}", self.obj, value, state)
|
||||
@@ -403,14 +397,6 @@ class Widget:
|
||||
return self.type.get_scale(self.config)
|
||||
|
||||
|
||||
# Map of widgets to their config, used for trigger generation
|
||||
widget_map: dict[ID, Widget] = {}
|
||||
|
||||
|
||||
def is_widget_completed(name: ID) -> bool:
|
||||
return name in widget_map
|
||||
|
||||
|
||||
class LvScrActType(WidgetType):
|
||||
"""
|
||||
A "widget" representing the active screen.
|
||||
@@ -433,10 +419,11 @@ def get_widget_generator(wid):
|
||||
:param wid:
|
||||
:return:
|
||||
"""
|
||||
widget_map = get_widget_map()
|
||||
while True:
|
||||
if obj := widget_map.get(wid):
|
||||
return obj
|
||||
if Widget.widgets_completed:
|
||||
if get_widgets_completed():
|
||||
raise Invalid(
|
||||
f"Widget {wid} not found, yet all widgets should be defined by now"
|
||||
)
|
||||
@@ -444,20 +431,20 @@ def get_widget_generator(wid):
|
||||
|
||||
|
||||
async def get_widget_(wid):
|
||||
if obj := widget_map.get(wid):
|
||||
if obj := get_widget_map().get(wid):
|
||||
return obj
|
||||
return await FakeAwaitable(get_widget_generator(wid))
|
||||
|
||||
|
||||
def widgets_wait_generator():
|
||||
while True:
|
||||
if Widget.widgets_completed:
|
||||
if get_widgets_completed():
|
||||
return
|
||||
yield
|
||||
|
||||
|
||||
async def wait_for_widgets():
|
||||
if Widget.widgets_completed:
|
||||
if get_widgets_completed():
|
||||
return
|
||||
await FakeAwaitable(widgets_wait_generator())
|
||||
|
||||
@@ -608,30 +595,14 @@ async def set_obj_properties(w: Widget, config):
|
||||
cond.else_()
|
||||
w.clear_flag(flag)
|
||||
|
||||
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:
|
||||
adds.add(key)
|
||||
else:
|
||||
clears.add(key)
|
||||
if adds:
|
||||
adds = join_enums(adds, "LV_STATE_")
|
||||
w.add_state(adds)
|
||||
if clears:
|
||||
clears = join_enums(clears, "LV_STATE_")
|
||||
w.clear_state(clears)
|
||||
for key, value in lambs.items():
|
||||
lamb = await cg.process_lambda(value, [], capture="=", return_type=cg.bool_)
|
||||
state = f"LV_STATE_{key.upper()}"
|
||||
with LvConditional(call_lambda(lamb)) as cond:
|
||||
w.add_state(state)
|
||||
cond.else_()
|
||||
w.clear_state(state)
|
||||
for key, value in config.get(CONF_STATE, {}).items():
|
||||
if isinstance(value, cv.Lambda):
|
||||
value = call_lambda(
|
||||
await cg.process_lambda(value, [], capture="=", return_type=cg.bool_)
|
||||
)
|
||||
state = getattr(LV_STATE, key.upper())
|
||||
w.set_state(state, value)
|
||||
|
||||
for property in OBJ_PROPERTIES:
|
||||
await w.set_property(property, config, lv_name="obj")
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ from esphome.const import CONF_DURATION, CONF_ID
|
||||
|
||||
from ..automation import action_to_code
|
||||
from ..defines import CONF_AUTO_START, CONF_MAIN, CONF_REPEAT_COUNT, CONF_SRC
|
||||
from ..helpers import lvgl_components_required
|
||||
from ..lv_validation import lv_image_list, lv_milliseconds
|
||||
from ..lvcode import lv
|
||||
from ..types import LvType, ObjUpdateAction
|
||||
@@ -55,8 +54,6 @@ class AnimimgType(WidgetType):
|
||||
)
|
||||
|
||||
async def to_code(self, w: Widget, config):
|
||||
lvgl_components_required.add(CONF_IMAGE)
|
||||
lvgl_components_required.add(CONF_ANIMIMG)
|
||||
if srcs := config.get(CONF_SRC):
|
||||
srcs = await lv_image_list.process(srcs)
|
||||
lv.animimg_set_src(w.obj, srcs)
|
||||
@@ -68,7 +65,7 @@ class AnimimgType(WidgetType):
|
||||
lv.animimg_start(w.obj)
|
||||
|
||||
def get_uses(self):
|
||||
return "img", CONF_IMAGE, CONF_LABEL
|
||||
return CONF_IMAGE, CONF_LABEL
|
||||
|
||||
|
||||
animimg_spec = AnimimgType()
|
||||
|
||||
@@ -2,8 +2,7 @@ from esphome import config_validation as cv
|
||||
from esphome.const import CONF_BUTTON, CONF_TEXT
|
||||
from esphome.cpp_generator import MockObj
|
||||
|
||||
from ..defines import CONF_MAIN, CONF_WIDGETS
|
||||
from ..helpers import add_lv_use
|
||||
from ..defines import CONF_MAIN, CONF_WIDGETS, add_lv_use
|
||||
from ..lv_validation import lv_text
|
||||
from ..lvcode import lv, lv_expr
|
||||
from ..schemas import TEXT_SCHEMA
|
||||
|
||||
@@ -5,6 +5,7 @@ from esphome.components.key_provider import KeyProvider
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_ITEMS, CONF_TEXT, CONF_WIDTH
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import Expression
|
||||
|
||||
from ..automation import action_to_code
|
||||
from ..defines import (
|
||||
@@ -17,10 +18,10 @@ from ..defines import (
|
||||
CONF_PAD_COLUMN,
|
||||
CONF_PAD_ROW,
|
||||
CONF_SELECTED,
|
||||
get_widget_map,
|
||||
)
|
||||
from ..helpers import lvgl_components_required
|
||||
from ..lv_validation import key_code, lv_bool, padding
|
||||
from ..lvcode import lv, lv_add, lv_expr
|
||||
from ..lvcode import lv, lv_add, lv_expr, lvgl_static
|
||||
from ..schemas import automation_schema
|
||||
from ..types import (
|
||||
LV_BTNMATRIX_CTRL,
|
||||
@@ -32,7 +33,7 @@ from ..types import (
|
||||
char_ptr,
|
||||
lv_pseudo_button_t,
|
||||
)
|
||||
from . import Widget, WidgetType, get_widgets, widget_map
|
||||
from . import Widget, WidgetType, get_widgets
|
||||
from .button import lv_button_t
|
||||
|
||||
CONF_BUTTONMATRIX = "buttonmatrix"
|
||||
@@ -98,7 +99,7 @@ class MatrixButton(Widget):
|
||||
@staticmethod
|
||||
def create_button(id, parent, config: dict, index):
|
||||
w = MatrixButton(id, parent, config, index)
|
||||
widget_map[id] = w
|
||||
get_widget_map()[id] = w
|
||||
return w
|
||||
|
||||
def __init__(self, id, parent: Widget, config, index):
|
||||
@@ -120,13 +121,13 @@ class MatrixButton(Widget):
|
||||
state = self.map_ctrls(state)
|
||||
return lv_expr.buttonmatrix_has_button_ctrl(self.obj, self.index, state)
|
||||
|
||||
def add_state(self, state):
|
||||
state = self.map_ctrls(state)
|
||||
return lv.buttonmatrix_set_button_ctrl(self.obj, self.index, state)
|
||||
|
||||
def clear_state(self, state):
|
||||
state = self.map_ctrls(state)
|
||||
return lv.buttonmatrix_clear_button_ctrl(self.obj, self.index, state)
|
||||
def set_state(self, state: MockObj, value: bool | Expression):
|
||||
ctrl = self.map_ctrls(state)
|
||||
lv_add(
|
||||
lvgl_static.lv_buttonmatrix_set_button_ctrl_value(
|
||||
self.obj, self.index, ctrl, value
|
||||
)
|
||||
)
|
||||
|
||||
def is_pressed(self):
|
||||
return self.is_selected() & self.parent.has_state(LV_STATE.PRESSED)
|
||||
@@ -191,7 +192,6 @@ class ButtonMatrixType(WidgetType):
|
||||
)
|
||||
|
||||
async def to_code(self, w: Widget, config):
|
||||
lvgl_components_required.add("BUTTONMATRIX")
|
||||
if CONF_ROWS not in config:
|
||||
return
|
||||
text_list, ctrl_list, width_list, key_list = await get_button_data(
|
||||
|
||||
@@ -14,7 +14,6 @@ from ..defines import (
|
||||
DIRECTIONS,
|
||||
literal,
|
||||
)
|
||||
from ..helpers import lvgl_components_required
|
||||
from ..lv_validation import lv_int, lv_text, option_string
|
||||
from ..lvcode import LocalVariable, lv, lv_add, lv_expr
|
||||
from ..schemas import part_schema
|
||||
@@ -95,7 +94,6 @@ class DropdownType(WidgetType):
|
||||
)
|
||||
|
||||
async def to_code(self, w: Widget, config):
|
||||
lvgl_components_required.add(CONF_DROPDOWN)
|
||||
if options := config.get(CONF_OPTIONS):
|
||||
lv_add(w.var.set_options(options))
|
||||
if symbol := config.get(CONF_SYMBOL):
|
||||
@@ -116,7 +114,7 @@ class DropdownType(WidgetType):
|
||||
await set_obj_properties(dwid, dlist)
|
||||
|
||||
def get_uses(self):
|
||||
return (CONF_LABEL,)
|
||||
return CONF_LABEL, CONF_DROPDOWN
|
||||
|
||||
|
||||
dropdown_spec = DropdownType()
|
||||
|
||||
@@ -5,10 +5,15 @@ from esphome.core import CORE
|
||||
from esphome.cpp_types import std_string
|
||||
|
||||
from .. import LvContext
|
||||
from ..defines import CONF_MAIN, KEYBOARD_MODES, literal
|
||||
from ..helpers import lvgl_components_required
|
||||
from ..defines import (
|
||||
CONF_MAIN,
|
||||
KEYBOARD_MODES,
|
||||
add_lv_use,
|
||||
is_widget_completed,
|
||||
literal,
|
||||
)
|
||||
from ..types import LvCompound, LvType
|
||||
from . import Widget, WidgetType, get_widgets, is_widget_completed
|
||||
from . import Widget, WidgetType, get_widgets
|
||||
from .buttonmatrix import CONF_BUTTONMATRIX
|
||||
from .textarea import CONF_TEXTAREA, lv_textarea_t
|
||||
|
||||
@@ -47,8 +52,7 @@ class KeyboardType(WidgetType):
|
||||
return CONF_KEYBOARD, CONF_TEXTAREA, CONF_BUTTONMATRIX
|
||||
|
||||
async def to_code(self, w: Widget, config: dict):
|
||||
lvgl_components_required.add("KEY_LISTENER")
|
||||
lvgl_components_required.add(CONF_KEYBOARD)
|
||||
add_lv_use("KEY_LISTENER")
|
||||
if mode := config.get(CONF_MODE):
|
||||
await w.set_property(CONF_MODE, await KEYBOARD_MODES.process(mode))
|
||||
if textarea := config.get(CONF_TEXTAREA):
|
||||
|
||||
@@ -41,10 +41,10 @@ from ..defines import (
|
||||
LV_OBJ_FLAG,
|
||||
LV_PART,
|
||||
LV_SCALE_MODE,
|
||||
add_lv_use,
|
||||
get_remapped_uses,
|
||||
get_warnings,
|
||||
)
|
||||
from ..helpers import add_lv_use
|
||||
from ..lv_validation import (
|
||||
LV_OPA,
|
||||
LV_RADIUS,
|
||||
@@ -61,7 +61,6 @@ from ..lv_validation import (
|
||||
padding,
|
||||
pixels,
|
||||
pixels_or_percent,
|
||||
requires_component,
|
||||
size,
|
||||
)
|
||||
from ..lvcode import LambdaContext, LocalVariable, lv, lv_add, lv_expr, lv_obj
|
||||
@@ -214,7 +213,7 @@ INDICATOR_SCHEMA = cv.Schema(
|
||||
cv.GenerateID(CONF_IMAGE_ID): cv.declare_id(lv_image_t),
|
||||
}
|
||||
),
|
||||
requires_component("image"),
|
||||
cv.requires_component("image"),
|
||||
),
|
||||
cv.Exclusive(CONF_ARC, CONF_INDICATORS): INDICATOR_ARC_SCHEMA.extend(
|
||||
{
|
||||
|
||||
@@ -16,10 +16,10 @@ from ..defines import (
|
||||
CONF_TITLE,
|
||||
LV_OBJ_FLAG,
|
||||
TYPE_FLEX,
|
||||
add_lv_use,
|
||||
add_warning,
|
||||
literal,
|
||||
)
|
||||
from ..helpers import add_lv_use
|
||||
from ..lv_validation import lv_bool, lv_image, lv_text, pixels_or_percent
|
||||
from ..lvcode import EVENT_ARG, LambdaContext, LocalVariable, lv, lv_expr, lv_obj
|
||||
from ..schemas import (
|
||||
|
||||
@@ -11,7 +11,6 @@ from ..defines import (
|
||||
ROLLER_MODES,
|
||||
literal,
|
||||
)
|
||||
from ..helpers import lvgl_components_required
|
||||
from ..lv_validation import animated, lv_int, lv_text, option_string
|
||||
from ..lvcode import lv_add
|
||||
from ..types import LvSelect
|
||||
@@ -55,7 +54,6 @@ class RollerType(WidgetType):
|
||||
)
|
||||
|
||||
async def to_code(self, w, config):
|
||||
lvgl_components_required.add(CONF_ROLLER)
|
||||
if mode := config.get(CONF_MODE):
|
||||
mode = await ROLLER_MODES.process(mode)
|
||||
lv_add(w.var.set_mode(mode))
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
spi:
|
||||
- id: spi_bus
|
||||
clk_pin: GPIO18
|
||||
mosi_pin: GPIO23
|
||||
|
||||
display:
|
||||
- platform: mipi_spi
|
||||
spi_id: spi_bus
|
||||
model: st7789v
|
||||
id: tft_display
|
||||
dimensions:
|
||||
width: 240
|
||||
height: 320
|
||||
cs_pin: GPIO22
|
||||
dc_pin: GPIO21
|
||||
auto_clear_enabled: false
|
||||
invert_colors: false
|
||||
update_interval: never
|
||||
|
||||
lvgl:
|
||||
id: lvgl_id
|
||||
displays: tft_display
|
||||
pages:
|
||||
- id: main_page
|
||||
widgets:
|
||||
# Widget with multiple static states; one true, one false.
|
||||
- button:
|
||||
id: btn_static
|
||||
state:
|
||||
checked: true
|
||||
disabled: false
|
||||
|
||||
# Widget with a templated (lambda) state.
|
||||
- button:
|
||||
id: btn_lambda
|
||||
state:
|
||||
pressed: !lambda return true;
|
||||
|
||||
# Button referenced by enable/disable actions; the on_click handler
|
||||
# exercises both branches of the obj_disable/obj_enable code path.
|
||||
- button:
|
||||
id: btn_actions
|
||||
on_click:
|
||||
- lvgl.widget.disable: btn_actions
|
||||
- lvgl.widget.enable: btn_actions
|
||||
|
||||
# Button matrix with two buttons; matrix_btn_a is targeted by
|
||||
# lvgl.widget.disable/enable actions to exercise the
|
||||
# MatrixButton.set_state code path.
|
||||
- buttonmatrix:
|
||||
id: matrix
|
||||
rows:
|
||||
- buttons:
|
||||
- id: matrix_btn_a
|
||||
text: A
|
||||
control:
|
||||
checkable: true
|
||||
- id: matrix_btn_b
|
||||
text: B
|
||||
control:
|
||||
checkable: true
|
||||
on_click:
|
||||
- lvgl.widget.disable: matrix_btn_a
|
||||
- lvgl.widget.enable: matrix_btn_a
|
||||
|
||||
# Switch derived from an LVGL switch widget – exercises
|
||||
# set_state(LV_STATE.CHECKED, v) inside the control lambda.
|
||||
- switch:
|
||||
id: switch_widget
|
||||
|
||||
switch:
|
||||
- platform: lvgl
|
||||
id: lvgl_switch
|
||||
name: LVGL Switch
|
||||
widget: switch_widget
|
||||
@@ -0,0 +1,167 @@
|
||||
"""Tests for LVGL widget state code generation.
|
||||
|
||||
These tests cover the change from the old ``add_state``/``clear_state`` helpers
|
||||
on :class:`Widget` (and on :class:`MatrixButton`) to a single ``set_state``
|
||||
method that delegates to the new C++ helpers
|
||||
``LvglComponent::lv_obj_set_state_value`` and
|
||||
``LvglComponent::lv_buttonmatrix_set_button_ctrl_value``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.__main__ import generate_cpp_contents
|
||||
from esphome.config import read_config
|
||||
from esphome.core import CORE
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def main_cpp(request: pytest.FixtureRequest) -> str:
|
||||
"""Generate the C++ output for the shared widget-state YAML config once
|
||||
per module.
|
||||
|
||||
Module-scoped so the (relatively expensive) codegen runs a single time;
|
||||
the function-scoped fixtures from ``conftest.py`` (e.g. ``generate_main``)
|
||||
can't be requested from a higher-scoped fixture, so the small amount of
|
||||
setup is inlined here. The captured string is independent of
|
||||
``CORE.reset()`` calls that the per-test autouse fixtures perform after
|
||||
this fixture has produced its value.
|
||||
"""
|
||||
config_path = Path(request.fspath).parent / "config" / "widget_state_test.yaml"
|
||||
original_path = CORE.config_path
|
||||
try:
|
||||
CORE.config_path = config_path
|
||||
CORE.config = read_config({})
|
||||
generate_cpp_contents(CORE.config)
|
||||
return CORE.cpp_global_section + CORE.cpp_main_section
|
||||
finally:
|
||||
CORE.config_path = original_path
|
||||
CORE.reset()
|
||||
|
||||
|
||||
def test_static_state_emits_set_state_value(main_cpp: str) -> None:
|
||||
"""A widget with ``state: { checked: true, disabled: false }`` should
|
||||
generate one ``lv_obj_set_state_value`` call per entry, with the
|
||||
appropriate boolean argument.
|
||||
"""
|
||||
assert (
|
||||
"LvglComponent::lv_obj_set_state_value(btn_static, LV_STATE_CHECKED, true)"
|
||||
in main_cpp
|
||||
)
|
||||
assert (
|
||||
"LvglComponent::lv_obj_set_state_value(btn_static, LV_STATE_DISABLED, false)"
|
||||
in main_cpp
|
||||
)
|
||||
|
||||
|
||||
def test_lambda_state_emits_set_state_value_with_lambda(main_cpp: str) -> None:
|
||||
"""A widget with ``state: { pressed: !lambda return true; }`` should
|
||||
generate ``lv_obj_set_state_value(..., LV_STATE_PRESSED, <expr>)`` where
|
||||
``<expr>`` is the lambda's return value (cast or inlined), not a static
|
||||
bool.
|
||||
"""
|
||||
# The set_state call is emitted for the templated state.
|
||||
assert (
|
||||
"LvglComponent::lv_obj_set_state_value(btn_lambda, LV_STATE_PRESSED,"
|
||||
in main_cpp
|
||||
)
|
||||
# And it must NOT have collapsed the lambda to a literal true/false.
|
||||
assert (
|
||||
"LvglComponent::lv_obj_set_state_value(btn_lambda, LV_STATE_PRESSED, true)"
|
||||
not in main_cpp
|
||||
)
|
||||
# The legacy if/else over add_state/remove_state is gone.
|
||||
assert "lv_obj_add_state(btn_lambda, LV_STATE_PRESSED)" not in main_cpp
|
||||
assert "lv_obj_remove_state(btn_lambda, LV_STATE_PRESSED)" not in main_cpp
|
||||
|
||||
|
||||
def test_widget_disable_action_uses_set_state_value(main_cpp: str) -> None:
|
||||
"""``lvgl.widget.disable: btn_actions`` should emit a
|
||||
``set_state_value(..., LV_STATE_DISABLED, true)`` call rather than the
|
||||
legacy ``lv_obj_add_state``.
|
||||
"""
|
||||
assert (
|
||||
"LvglComponent::lv_obj_set_state_value(btn_actions, LV_STATE_DISABLED, true)"
|
||||
in main_cpp
|
||||
)
|
||||
# No leftover legacy add_state for the DISABLED state of this widget.
|
||||
assert "lv_obj_add_state(btn_actions, LV_STATE_DISABLED)" not in main_cpp
|
||||
|
||||
|
||||
def test_widget_enable_action_uses_set_state_value(main_cpp: str) -> None:
|
||||
"""``lvgl.widget.enable: btn_actions`` should emit a
|
||||
``set_state_value(..., LV_STATE_DISABLED, false)`` call rather than the
|
||||
legacy ``lv_obj_remove_state``.
|
||||
"""
|
||||
assert (
|
||||
"LvglComponent::lv_obj_set_state_value(btn_actions, LV_STATE_DISABLED, false)"
|
||||
in main_cpp
|
||||
)
|
||||
assert "lv_obj_remove_state(btn_actions, LV_STATE_DISABLED)" not in main_cpp
|
||||
|
||||
|
||||
def test_buttonmatrix_disable_action_uses_helper(main_cpp: str) -> None:
|
||||
"""``lvgl.widget.disable: matrix_btn_a`` should route through the new
|
||||
``lv_buttonmatrix_set_button_ctrl_value`` helper for button index 0
|
||||
with the ``DISABLED`` control bit set to ``true``, instead of the
|
||||
legacy ``lv_buttonmatrix_set_button_ctrl``.
|
||||
|
||||
The button matrix obj is the compound's ``obj`` member and the index
|
||||
is the position of the button in the row layout.
|
||||
"""
|
||||
assert (
|
||||
"LvglComponent::lv_buttonmatrix_set_button_ctrl_value(matrix->obj, 0, "
|
||||
"LV_BUTTONMATRIX_CTRL_DISABLED, true)"
|
||||
) in main_cpp
|
||||
|
||||
|
||||
def test_buttonmatrix_enable_action_uses_helper(main_cpp: str) -> None:
|
||||
"""``lvgl.widget.enable: matrix_btn_a`` should route through the new
|
||||
``lv_buttonmatrix_set_button_ctrl_value`` helper for button index 0
|
||||
with the ``DISABLED`` control bit set to ``false``, instead of the
|
||||
legacy ``lv_buttonmatrix_clear_button_ctrl``.
|
||||
"""
|
||||
assert (
|
||||
"LvglComponent::lv_buttonmatrix_set_button_ctrl_value(matrix->obj, 0, "
|
||||
"LV_BUTTONMATRIX_CTRL_DISABLED, false)"
|
||||
) in main_cpp
|
||||
# The legacy clear_button_ctrl path is gone for the matrix button enable
|
||||
# action.
|
||||
assert (
|
||||
"lv_buttonmatrix_clear_button_ctrl(matrix->obj, 0, LV_BUTTONMATRIX_CTRL_DISABLED)"
|
||||
not in main_cpp
|
||||
)
|
||||
|
||||
|
||||
def test_lvgl_switch_control_calls_set_state_value(main_cpp: str) -> None:
|
||||
"""The LVGL switch platform installs a control lambda that mirrors the
|
||||
switch's bool value into ``LV_STATE_CHECKED`` via
|
||||
``lv_obj_set_state_value`` (replacing the previous if/else over
|
||||
``add_state``/``clear_state`` plus an explicit ``send_event`` of
|
||||
``lv_api_event``).
|
||||
"""
|
||||
# The control lambda calls the new helper with the bool ``v`` parameter.
|
||||
assert (
|
||||
"LvglComponent::lv_obj_set_state_value(switch_widget, LV_STATE_CHECKED, v)"
|
||||
in main_cpp
|
||||
)
|
||||
# The deprecated lv_api_event symbol must no longer appear anywhere.
|
||||
assert "lv_api_event" not in main_cpp
|
||||
|
||||
|
||||
def test_default_state_does_not_emit_set_state_value(main_cpp: str) -> None:
|
||||
"""A widget without a ``state:`` block must not generate any
|
||||
``lv_obj_set_state_value`` calls for it. (Sanity-check that the
|
||||
new code path is opt-in driven by the YAML.)
|
||||
"""
|
||||
assert (
|
||||
"LvglComponent::lv_obj_set_state_value(switch_widget, LV_STATE_DISABLED"
|
||||
not in main_cpp
|
||||
)
|
||||
assert (
|
||||
"LvglComponent::lv_obj_set_state_value(btn_static, LV_STATE_PRESSED"
|
||||
not in main_cpp
|
||||
)
|
||||
Reference in New Issue
Block a user