mirror of
https://github.com/esphome/esphome.git
synced 2026-08-25 15:46:20 +00:00
Merge branch 'esp8266-native-toolchain-plumbing' into esp8266-native-build-infra
This commit is contained in:
@@ -55,8 +55,11 @@ int HOT IRAM_ATTR GPIOOneWireBus::reset_int() {
|
||||
delayMicroseconds(1);
|
||||
}
|
||||
|
||||
// delay J
|
||||
delayMicroseconds(start + 480 - micros());
|
||||
// delay J: finish the 480us slot, but never spin if it already elapsed
|
||||
// (unsigned wrap here would busy-wait for minutes with interrupts off)
|
||||
uint32_t elapsed = micros() - start;
|
||||
if (elapsed < 480)
|
||||
delayMicroseconds(480 - elapsed);
|
||||
this->pin_.digital_write(true);
|
||||
this->pin_.pin_mode(gpio::FLAG_OUTPUT);
|
||||
return r ? 1 : 0;
|
||||
|
||||
@@ -57,7 +57,6 @@ from .defines import (
|
||||
CONF_ALIGN_TO_LAMBDA_ID,
|
||||
CONF_ANIMATIONS,
|
||||
LOGGER,
|
||||
add_lv_use,
|
||||
get_focused_widgets,
|
||||
get_lv_images_used,
|
||||
get_refreshed_widgets,
|
||||
@@ -74,7 +73,6 @@ from .keypads import KEYPADS_CONFIG, keypads_to_code
|
||||
from .lv_validation import lv_bool
|
||||
from .lvcode import LvContext, LvglComponent, lv_event_t_ptr, lvgl_static
|
||||
from .schemas import (
|
||||
BASE_PROPS,
|
||||
DISP_BG_SCHEMA,
|
||||
FULL_STYLE_SCHEMA,
|
||||
SET_STATE_SCHEMA,
|
||||
@@ -83,6 +81,7 @@ from .schemas import (
|
||||
STYLE_SCHEMA,
|
||||
WIDGET_TYPES,
|
||||
any_widget_schema,
|
||||
apply_style_driven_defines,
|
||||
container_schema,
|
||||
container_schema_value,
|
||||
theme_schema,
|
||||
@@ -108,7 +107,6 @@ from .widgets import (
|
||||
get_screen_active,
|
||||
set_obj_properties,
|
||||
)
|
||||
from .widgets.img import CONF_IMAGE
|
||||
|
||||
# Import only what we actually use directly in this file
|
||||
from .widgets.msgbox import MSGBOX_SCHEMA, msgboxes_to_code
|
||||
@@ -455,6 +453,15 @@ async def to_code(configs):
|
||||
# Mark all widgets as completed so awaiters of ``wait_for_widgets`` proceed.
|
||||
set_widgets_completed(True)
|
||||
async with LvContext():
|
||||
# Local import: lv_list imports meter, which imports obj_spec/set_obj_properties
|
||||
# from this module's own namespace - a top-level import here would be circular.
|
||||
from .widgets.lv_list import finish_list_triggers
|
||||
|
||||
# Must run before generate_triggers(): that's what actually processes other
|
||||
# widgets' on_click etc. automations, which can include lvgl.list.add/remove/
|
||||
# clear actions that fire a list's on_add/on_remove triggers - those need to
|
||||
# already exist by then, not still be pending.
|
||||
await finish_list_triggers()
|
||||
await generate_triggers()
|
||||
await generate_align_tos(configs[0])
|
||||
for config in configs:
|
||||
@@ -481,34 +488,16 @@ async def to_code(configs):
|
||||
|
||||
# This must be done after all widgets are created
|
||||
styles_used = df.get_styles_used()
|
||||
if any(BASE_PROPS.get(x) is lvalid.lv_image for x in styles_used):
|
||||
add_lv_use(CONF_IMAGE)
|
||||
apply_style_driven_defines(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()}")
|
||||
|
||||
if {
|
||||
"transform_rotation",
|
||||
"transform_scale",
|
||||
"transform_scale_x",
|
||||
"transform_scale_y",
|
||||
} & styles_used:
|
||||
df.add_define("LV_COLOR_SCREEN_TRANSP", "1")
|
||||
|
||||
if configs[0].get(df.CONF_THEME, {}).get(df.CONF_DARK_MODE):
|
||||
df.add_define("LV_THEME_DEFAULT_DARK", "1")
|
||||
|
||||
# Currently always need RGB565 for the display buffer, and ARGB8888 is used for layer blending
|
||||
lv_image_formats = {"RGB565", "ARGB8888"}
|
||||
if {
|
||||
"drop_shadow_color",
|
||||
"drop_shadow_offset_x",
|
||||
"drop_shadow_offset_y",
|
||||
"drop_shadow_opa",
|
||||
"drop_shadow_quality",
|
||||
"drop_shadow_radius",
|
||||
} & styles_used:
|
||||
lv_image_formats.add("A8")
|
||||
|
||||
for image_id in get_lv_images_used():
|
||||
await cg.get_variable(image_id)
|
||||
|
||||
@@ -416,7 +416,7 @@ async def obj_set_z_index_to_code(config, action_id, template_arg, args):
|
||||
widget.obj, literal(f"{lv_expr.obj_get_index(widget.obj)} + 1")
|
||||
)
|
||||
elif position == "DOWN":
|
||||
with LvConditional(f"{lv_expr.obj_get_index(widget.obj)} > 0"):
|
||||
with LvConditional(literal(f"{lv_expr.obj_get_index(widget.obj)} > 0")):
|
||||
lv_obj.move_to_index(
|
||||
widget.obj, literal(f"{lv_expr.obj_get_index(widget.obj)} - 1")
|
||||
)
|
||||
|
||||
@@ -585,6 +585,21 @@ FLEX_FLOWS = LvConstant(
|
||||
"COLUMN_WRAP_REVERSE",
|
||||
)
|
||||
|
||||
TRANSFORM_STYLE_PROPS = frozenset(
|
||||
{"transform_rotation", "transform_scale", "transform_scale_x", "transform_scale_y"}
|
||||
)
|
||||
|
||||
DROP_SHADOW_STYLE_PROPS = frozenset(
|
||||
{
|
||||
"drop_shadow_color",
|
||||
"drop_shadow_offset_x",
|
||||
"drop_shadow_offset_y",
|
||||
"drop_shadow_opa",
|
||||
"drop_shadow_quality",
|
||||
"drop_shadow_radius",
|
||||
}
|
||||
)
|
||||
|
||||
OBJ_FLAGS = (
|
||||
"hidden",
|
||||
"clickable",
|
||||
|
||||
@@ -242,7 +242,7 @@ class LocalVariable(MockObj):
|
||||
self.base.type, self.modifier, self.base.id
|
||||
)
|
||||
)
|
||||
return MockObj(self.base)
|
||||
return MockObj(self.base, "->" if self.modifier == "*" else ".")
|
||||
|
||||
def __exit__(self, *args):
|
||||
CodeContext.end_block()
|
||||
@@ -283,7 +283,15 @@ class MockLv:
|
||||
|
||||
class LvConditional:
|
||||
def __init__(self, condition):
|
||||
self.condition = condition
|
||||
# Condition is embedded directly into a raw `if (...)` statement below, rather than
|
||||
# going through the argument-list machinery (ExpressionList) that would otherwise
|
||||
# convert a native Python value (e.g. a plain bool) to a proper Expression.
|
||||
if isinstance(condition, str):
|
||||
raise ValueError(
|
||||
"LvConditional condition must not be a raw str; wrap it in literal() "
|
||||
"if a string literal condition is really intended"
|
||||
)
|
||||
self.condition = cg.safe_exp(condition) if condition is not None else None
|
||||
|
||||
def __enter__(self):
|
||||
if self.condition is not None:
|
||||
@@ -303,6 +311,35 @@ class LvConditional:
|
||||
CodeContext.code_context.indent()
|
||||
|
||||
|
||||
class LvCountdown:
|
||||
"""
|
||||
Emits a C++ `for` loop that counts an int variable down from `count - 1` to `0` inclusive.
|
||||
Used to iterate over a widget's children in reverse, e.g. to fire a trigger once per child
|
||||
before they're all removed.
|
||||
"""
|
||||
|
||||
def __init__(self, var_name: str, count):
|
||||
self.var_name = var_name
|
||||
self.count = count
|
||||
|
||||
def __enter__(self):
|
||||
# Cast explicitly rather than relying on `count`'s (typically unsigned) type to wrap
|
||||
# and then narrow back to a negative int when count is 0 -- true in practice on every
|
||||
# toolchain ESPHome targets, but not worth leaning on.
|
||||
CodeContext.append(
|
||||
RawStatement(
|
||||
f"for (int {self.var_name} = (int) ({self.count}) - 1; {self.var_name} >= 0; "
|
||||
f"{self.var_name}--) {{"
|
||||
)
|
||||
)
|
||||
CodeContext.code_context.indent()
|
||||
return literal(self.var_name)
|
||||
|
||||
def __exit__(self, *args):
|
||||
CodeContext.code_context.detent()
|
||||
CodeContext.append(RawStatement("}"))
|
||||
|
||||
|
||||
class ReturnStatement(ExpressionStatement):
|
||||
def __str__(self):
|
||||
return f"return {self.expression};"
|
||||
|
||||
@@ -208,21 +208,21 @@ void LvglComponent::esphome_lvgl_init() {
|
||||
lv_update_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) {
|
||||
lv_obj_add_event_cb(obj, callback, event, nullptr);
|
||||
void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event, void *user_data) {
|
||||
lv_obj_add_event_cb(obj, callback, event, user_data);
|
||||
}
|
||||
|
||||
void LvglComponent::add_event_cb(lv_obj_t *obj, event_callback_t callback, lv_event_code_t event1,
|
||||
lv_event_code_t event2) {
|
||||
add_event_cb(obj, callback, event1);
|
||||
add_event_cb(obj, callback, event2);
|
||||
lv_event_code_t event2, void *user_data) {
|
||||
add_event_cb(obj, callback, event1, user_data);
|
||||
add_event_cb(obj, callback, event2, user_data);
|
||||
}
|
||||
|
||||
void LvglComponent::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) {
|
||||
add_event_cb(obj, callback, event1);
|
||||
add_event_cb(obj, callback, event2);
|
||||
add_event_cb(obj, callback, event3);
|
||||
lv_event_code_t event2, lv_event_code_t event3, void *user_data) {
|
||||
add_event_cb(obj, callback, event1, user_data);
|
||||
add_event_cb(obj, callback, event2, user_data);
|
||||
add_event_cb(obj, callback, event3, user_data);
|
||||
}
|
||||
|
||||
void LvglComponent::add_page(LvPageType *page) {
|
||||
@@ -963,6 +963,25 @@ lv_obj_t *lv_container_create(lv_obj_t *parent) {
|
||||
lv_obj_class_init_obj(obj);
|
||||
return obj;
|
||||
}
|
||||
|
||||
#ifdef USE_LVGL_LIST
|
||||
int lv_list_get_row_index(lv_obj_t *list, lv_obj_t *child) {
|
||||
for (lv_obj_t *obj = child; obj != nullptr; obj = lv_obj_get_parent(obj)) {
|
||||
if (lv_obj_get_parent(obj) == list)
|
||||
return lv_obj_get_index(obj);
|
||||
}
|
||||
ESP_LOGW(TAG, "lvgl.list: entry is not inside the list it was added to");
|
||||
return -1;
|
||||
}
|
||||
|
||||
lv_obj_t *lv_list_get_row_for_remove(lv_obj_t *list, int index) {
|
||||
lv_obj_t *child = index < 0 ? nullptr : lv_obj_get_child(list, index);
|
||||
if (child == nullptr) {
|
||||
ESP_LOGW(TAG, "lvgl.list.remove: index %d is out of range, ignoring", index);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
#endif // USE_LVGL_LIST
|
||||
} // namespace esphome::lvgl
|
||||
|
||||
lv_result_t lv_mem_test_core() { return LV_RESULT_OK; }
|
||||
|
||||
@@ -116,6 +116,18 @@ inline void lv_animimg_set_src(lv_obj_t *img, std::vector<image::Image *> images
|
||||
int16_t lv_get_needle_angle_for_value(lv_obj_t *obj, int32_t value);
|
||||
#endif
|
||||
|
||||
#ifdef USE_LVGL_LIST
|
||||
// Returns the index, within `list`, of the entry that contains `child`: `child` itself if it's a
|
||||
// direct child of `list`, or the ancestor of `child` that is, when `child` is nested inside a
|
||||
// widget hierarchy added via `lvgl.list.add`. Returns -1 if `child` isn't inside `list` at all.
|
||||
int lv_list_get_row_index(lv_obj_t *list, lv_obj_t *child);
|
||||
|
||||
// Returns the entry at `index` within `list`, or nullptr (logging why) if `index` is out of
|
||||
// range -- shared by every `lvgl.list.remove` call site, since a templatable index can go out of
|
||||
// range at runtime in ways config validation can't catch (e.g. driven by a sensor value).
|
||||
lv_obj_t *lv_list_get_row_for_remove(lv_obj_t *list, int index);
|
||||
#endif
|
||||
|
||||
#ifdef USE_LVGL_GRADIENT
|
||||
/**
|
||||
*
|
||||
@@ -135,6 +147,12 @@ class LvCompound {
|
||||
lv_obj_t *obj{};
|
||||
};
|
||||
|
||||
// Frees a heap-allocated LvCompound wrapper on LV_EVENT_DELETE, since lv_obj_del() only knows how to destroy LVGL's own
|
||||
// object tree, not a separate C++ object paired with one of its nodes.
|
||||
template<typename T> void delete_lv_compound_on_delete(lv_event_t *e) {
|
||||
delete static_cast<T *>(lv_event_get_user_data(e));
|
||||
}
|
||||
|
||||
class LvglComponent;
|
||||
|
||||
class LvPageType : public Parented<LvglComponent> {
|
||||
@@ -241,10 +259,11 @@ class LvglComponent final : public PollingComponent {
|
||||
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 event, void *user_data = nullptr);
|
||||
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);
|
||||
void *user_data = nullptr);
|
||||
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, void *user_data = nullptr);
|
||||
|
||||
// change the state of a widget and fire an event if changed (only needed for CHECKED)
|
||||
|
||||
|
||||
@@ -726,6 +726,26 @@ ALL_STYLES = {
|
||||
}
|
||||
|
||||
|
||||
def apply_style_driven_defines(props: set[str]) -> None:
|
||||
"""Given a set of style-property names in use, registers everything their use
|
||||
drives: add_lv_use(image) if any of them is image-typed (per BASE_PROPS), and
|
||||
the LV_COLOR_SCREEN_TRANSP / LV_DRAW_SW_SUPPORT_A8 defines. Shared between
|
||||
__init__.py (driven by df.get_styles_used(), for statically-declared widgets)
|
||||
and lv_list.py's _register_dynamic_widget_style_uses (driven by scanning a
|
||||
dynamically-added widget's own config), so a future style-driven define added
|
||||
to one can't be missed in the other.
|
||||
"""
|
||||
# Local import: avoids a module-load-time cycle (widgets.img -> ... -> schemas).
|
||||
from .widgets.img import CONF_IMAGE
|
||||
|
||||
if any(BASE_PROPS.get(prop) is lvalid.lv_image for prop in props):
|
||||
df.add_lv_use(CONF_IMAGE)
|
||||
if df.TRANSFORM_STYLE_PROPS & props:
|
||||
df.add_define("LV_COLOR_SCREEN_TRANSP", "1")
|
||||
if df.DROP_SHADOW_STYLE_PROPS & props:
|
||||
df.add_define("LV_DRAW_SW_SUPPORT_A8", "1")
|
||||
|
||||
|
||||
def strip_defaults(schema: cv.Schema):
|
||||
"""
|
||||
Take a schema and remove any default values, also convert Required to Optional.
|
||||
|
||||
@@ -59,7 +59,10 @@ async def generate_triggers():
|
||||
all_triggers = (
|
||||
LV_EVENT_TRIGGERS + LV_DISPLAY_EVENT_TRIGGERS + LV_SCREEN_EVENT_TRIGGERS
|
||||
)
|
||||
for w in get_widget_map().values():
|
||||
# Snapshot: building a trigger below can recurse into widget creation (e.g. a
|
||||
# buttonmatrix's or tabview's to_code registers its own child widgets), which
|
||||
# would otherwise mutate this dict mid-iteration.
|
||||
for w in list(get_widget_map().values()):
|
||||
config = w.config
|
||||
if isinstance(w.type, LvScrActType):
|
||||
w = get_screen_active(w.var)
|
||||
@@ -141,7 +144,21 @@ def _get_event_literal(trigger: str | MockObj) -> MockObj:
|
||||
return literal("LV_EVENT_" + TRIGGER_MAP[trigger.upper()])
|
||||
|
||||
|
||||
async def add_trigger(conf, w, *events: str | MockObj, is_selected=None):
|
||||
async def add_trigger(
|
||||
conf, w, *events: str | MockObj, is_selected=None, attach_obj=None, user_data=None
|
||||
):
|
||||
"""
|
||||
:param attach_obj: The object to actually register the callback on, if different
|
||||
from `w.obj` - used when `w.obj` isn't valid at the point the callback gets
|
||||
registered (e.g. a local variable that's only in scope inside the very
|
||||
block this is called from, not from within the callback body itself; see
|
||||
widgets/lv_list.py's dynamic widget creation). Defaults to `w.obj`.
|
||||
:param user_data: Opaque pointer passed through to the registered event callback,
|
||||
retrievable inside it via `lv_event_get_user_data(event)` - used to recover a
|
||||
compound widget's C++ wrapper, which a captureless callback has no other way
|
||||
to reach when it isn't a global variable (see widgets/lv_list.py). Defaults to
|
||||
`nullptr`.
|
||||
"""
|
||||
is_selected = is_selected or w.is_selected()
|
||||
tid = conf[CONF_TRIGGER_ID]
|
||||
trigger = cg.new_Pvariable(tid)
|
||||
@@ -158,12 +175,14 @@ async def add_trigger(conf, w, *events: str | MockObj, is_selected=None):
|
||||
lv_add(trigger.trigger(*value, literal("event")))
|
||||
callback = await context.get_lambda()
|
||||
event_literals = [_get_event_literal(event) for event in events]
|
||||
attach_obj = w.obj if attach_obj is None else attach_obj
|
||||
user_data = nullptr if user_data is None else user_data
|
||||
if str(events[0]) in DISPLAY_TRIGGERS:
|
||||
assert len(events) == 1
|
||||
lv.display_add_event_cb(
|
||||
lv_expr.obj_get_display(w.obj), callback, event_literals[0], nullptr
|
||||
lv_expr.obj_get_display(attach_obj), callback, event_literals[0], user_data
|
||||
)
|
||||
else:
|
||||
lv_add(
|
||||
lvgl_static.add_event_cb(w.obj, await context.get_lambda(), *event_literals)
|
||||
lvgl_static.add_event_cb(attach_obj, callback, *event_literals, user_data)
|
||||
)
|
||||
|
||||
@@ -190,18 +190,7 @@ class WidgetType:
|
||||
await self.on_create(var, config)
|
||||
|
||||
w = Widget.create(wid, var, self, config)
|
||||
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():
|
||||
state = "LV_STATE_" + state.upper()
|
||||
if state == "LV_STATE_DEFAULT":
|
||||
lv_state = literal(part)
|
||||
elif part == "LV_PART_MAIN":
|
||||
lv_state = literal(state)
|
||||
else:
|
||||
lv_state = join_enums((state, part))
|
||||
w.add_style(style, lv_state)
|
||||
apply_theme_styles(w)
|
||||
await set_obj_properties(w, config)
|
||||
await add_widgets(w, config)
|
||||
await self.to_code(w, config)
|
||||
@@ -230,7 +219,7 @@ class WidgetType:
|
||||
:param config: Its configuration
|
||||
"""
|
||||
|
||||
def get_uses(self):
|
||||
def get_uses(self) -> tuple:
|
||||
"""
|
||||
Get a list of other widgets used by this one
|
||||
:return:
|
||||
@@ -267,6 +256,21 @@ class WidgetType:
|
||||
"""
|
||||
|
||||
|
||||
def apply_theme_styles(w: "Widget") -> None:
|
||||
"""Apply the current theme's styles for this widget's type"""
|
||||
for part, states in get_theme_widget_map().get(w.type.name, {}).items():
|
||||
part = "LV_PART_" + part.upper()
|
||||
for state, style in states.items():
|
||||
state = "LV_STATE_" + state.upper()
|
||||
if state == "LV_STATE_DEFAULT":
|
||||
lv_state = literal(part)
|
||||
elif part == "LV_PART_MAIN":
|
||||
lv_state = literal(state)
|
||||
else:
|
||||
lv_state = join_enums((state, part))
|
||||
w.add_style(style, lv_state)
|
||||
|
||||
|
||||
class Widget:
|
||||
"""
|
||||
Represents a Widget.
|
||||
|
||||
@@ -0,0 +1,553 @@
|
||||
from collections.abc import Generator
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BUTTON,
|
||||
CONF_ID,
|
||||
CONF_INDEX,
|
||||
CONF_ON_BOOT,
|
||||
CONF_ON_UPDATE,
|
||||
CONF_ON_VALUE,
|
||||
CONF_TEXT,
|
||||
CONF_TRIGGER_ID,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.coroutine import FakeAwaitable
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
|
||||
|
||||
from ..automation import action_to_code
|
||||
from ..defines import (
|
||||
CONF_ALIGN_TO,
|
||||
CONF_MAIN,
|
||||
CONF_PAD_ROW,
|
||||
CONF_SCROLLBAR,
|
||||
CONF_WIDGETS,
|
||||
LV_EVENT_TRIGGERS,
|
||||
SWIPE_TRIGGERS,
|
||||
TYPE_FLEX,
|
||||
add_lv_use,
|
||||
literal,
|
||||
)
|
||||
from ..lv_validation import lv_int, lv_text, padding
|
||||
from ..lvcode import (
|
||||
UPDATE_EVENT,
|
||||
LocalVariable,
|
||||
LvConditional,
|
||||
LvCountdown,
|
||||
lv,
|
||||
lv_add,
|
||||
lv_expr,
|
||||
lv_obj,
|
||||
)
|
||||
from ..schemas import (
|
||||
ALL_STYLES,
|
||||
WIDGET_TYPES,
|
||||
any_widget_schema,
|
||||
apply_style_driven_defines,
|
||||
container_schema_value,
|
||||
remap_property,
|
||||
)
|
||||
from ..trigger import add_trigger
|
||||
from ..types import LV_EVENT, LvType, ObjUpdateAction, lv_obj_t
|
||||
from . import (
|
||||
Widget,
|
||||
WidgetType,
|
||||
apply_theme_styles,
|
||||
collect_parts,
|
||||
get_widgets,
|
||||
set_obj_properties,
|
||||
)
|
||||
from .buttonmatrix import CONF_BUTTONMATRIX
|
||||
from .canvas import CONF_CANVAS
|
||||
from .label import CONF_LABEL
|
||||
from .meter import CONF_METER
|
||||
from .tabview import CONF_TABVIEW
|
||||
from .tileview import CONF_TILEVIEW
|
||||
|
||||
CONF_LIST = "list"
|
||||
CONF_WIDGET = "widget"
|
||||
CONF_ON_ADD = "on_add"
|
||||
CONF_ON_REMOVE = "on_remove"
|
||||
|
||||
DOMAIN = "lvgl_list"
|
||||
|
||||
lv_list_t = LvType("lv_list_t")
|
||||
|
||||
|
||||
@dataclass
|
||||
class ListTriggers:
|
||||
on_add: list = field(default_factory=list)
|
||||
on_remove: list = field(default_factory=list)
|
||||
|
||||
|
||||
def _get_list_triggers(list_id) -> ListTriggers:
|
||||
"""
|
||||
Trigger Pvariables built for a given list's `on_add`/`on_remove` config, indexed by the
|
||||
list's own ID.
|
||||
"""
|
||||
triggers_by_list = CORE.data.setdefault(DOMAIN, {})
|
||||
return triggers_by_list.setdefault(list_id, ListTriggers())
|
||||
|
||||
|
||||
def _get_pending_list_triggers(list_id) -> ListTriggers:
|
||||
"""
|
||||
Same shape as _get_list_triggers(), but holding raw on_add/on_remove automation
|
||||
configs, not yet built.
|
||||
"""
|
||||
pending_by_list = CORE.data.setdefault(DOMAIN + "_pending", {})
|
||||
return pending_by_list.setdefault(list_id, ListTriggers())
|
||||
|
||||
|
||||
def _list_triggers_completed_flag() -> list[bool]:
|
||||
return CORE.data.setdefault(DOMAIN + "_completed", [False])
|
||||
|
||||
|
||||
def _list_triggers_completed_generator() -> Generator[None, None, None]:
|
||||
while True:
|
||||
if _list_triggers_completed_flag()[0]:
|
||||
return
|
||||
yield
|
||||
|
||||
|
||||
async def _wait_list_triggers_completed() -> None:
|
||||
"""Waits until finish_list_triggers() has built every list's on_add/on_remove automations."""
|
||||
if _list_triggers_completed_flag()[0]:
|
||||
return
|
||||
await FakeAwaitable(_list_triggers_completed_generator())
|
||||
|
||||
|
||||
async def finish_list_triggers() -> None:
|
||||
"""
|
||||
Builds every list's on_add/on_remove automations, collected by ListType.to_code()
|
||||
instead of being built there directly. Must run after set_widgets_completed(True).
|
||||
"""
|
||||
for list_id, pending in CORE.data.get(DOMAIN + "_pending", {}).items():
|
||||
triggers = _get_list_triggers(list_id)
|
||||
for conf in pending.on_add:
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
|
||||
await automation.build_automation(trigger, [(cg.int_, "list_index")], conf)
|
||||
triggers.on_add.append(trigger)
|
||||
for conf in pending.on_remove:
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
|
||||
await automation.build_automation(trigger, [(cg.int_, "list_index")], conf)
|
||||
triggers.on_remove.append(trigger)
|
||||
_list_triggers_completed_flag()[0] = True
|
||||
|
||||
|
||||
def _fire_index_triggers(triggers: list, index) -> None:
|
||||
for trigger in triggers:
|
||||
lv_add(trigger.trigger(index))
|
||||
|
||||
|
||||
async def _fire_on_add(list_id, list_obj, entry_obj) -> None:
|
||||
await _wait_list_triggers_completed()
|
||||
triggers = _get_list_triggers(list_id).on_add
|
||||
if not triggers:
|
||||
return
|
||||
index = cg.RawExpression(f"lvgl::lv_list_get_row_index({list_obj}, {entry_obj})")
|
||||
_fire_index_triggers(triggers, index)
|
||||
|
||||
|
||||
async def _fire_on_remove(list_id, index) -> None:
|
||||
await _wait_list_triggers_completed()
|
||||
_fire_index_triggers(_get_list_triggers(list_id).on_remove, index)
|
||||
|
||||
|
||||
LIST_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_PAD_ROW): padding,
|
||||
}
|
||||
)
|
||||
|
||||
LIST_CREATE_SCHEMA = LIST_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_ON_ADD): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
automation.Trigger.template(cg.int_)
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_REMOVE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
automation.Trigger.template(cg.int_)
|
||||
),
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class ListType(WidgetType):
|
||||
"""A plain wrapper around LVGL's native `lv_list`"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
CONF_LIST,
|
||||
lv_list_t,
|
||||
(CONF_MAIN, CONF_SCROLLBAR),
|
||||
LIST_CREATE_SCHEMA,
|
||||
modify_schema=LIST_SCHEMA,
|
||||
)
|
||||
|
||||
def get_uses(self):
|
||||
return TYPE_FLEX, CONF_LABEL, CONF_BUTTON
|
||||
|
||||
async def to_code(self, w: Widget, config: dict):
|
||||
on_add = config.get(CONF_ON_ADD, ())
|
||||
on_remove = config.get(CONF_ON_REMOVE, ())
|
||||
if not on_add and not on_remove:
|
||||
return
|
||||
pending = _get_pending_list_triggers(w.config[CONF_ID])
|
||||
pending.on_add.extend(on_add)
|
||||
pending.on_remove.extend(on_remove)
|
||||
|
||||
|
||||
list_spec = ListType()
|
||||
|
||||
LIST_ID_SCHEMA = cv.Schema({cv.Required(CONF_ID): cv.use_id(lv_list_t)})
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"lvgl.list.add_text",
|
||||
ObjUpdateAction,
|
||||
LIST_ID_SCHEMA.extend(
|
||||
{
|
||||
cv.Required(CONF_TEXT): lv_text,
|
||||
cv.Optional(CONF_INDEX): cv.templatable(cv.int_),
|
||||
}
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
async def list_add_text_to_code(config, action_id, template_arg, args):
|
||||
widgets = await get_widgets(config)
|
||||
|
||||
async def do_add_text(w: Widget):
|
||||
text = await lv_text.process(config[CONF_TEXT])
|
||||
with LocalVariable(
|
||||
"list_entry", lv_obj_t, lv_expr.list_add_text(w.obj, text)
|
||||
) as entry:
|
||||
if (idx := config.get(CONF_INDEX)) is not None:
|
||||
lv.obj_move_to_index(entry, await lv_int.process(idx))
|
||||
await _fire_on_add(config[CONF_ID], w.obj, entry)
|
||||
|
||||
return await action_to_code(
|
||||
widgets, do_add_text, action_id, template_arg, args, config
|
||||
)
|
||||
|
||||
|
||||
_DYNAMIC_WIDGET_UNSUPPORTED = (
|
||||
CONF_BUTTONMATRIX,
|
||||
CONF_TABVIEW,
|
||||
CONF_TILEVIEW,
|
||||
CONF_METER,
|
||||
CONF_CANVAS,
|
||||
)
|
||||
|
||||
|
||||
def _check_dynamic_widget_supported(w_type_name: str, w_conf: dict) -> None:
|
||||
# Each of these allocates a Pvariable, or registers children into the global widget
|
||||
# map, once at boot - rebuilding them on every lvgl.list.add call would break that.
|
||||
if w_type_name in _DYNAMIC_WIDGET_UNSUPPORTED:
|
||||
raise cv.Invalid(
|
||||
f"'{w_type_name}' cannot be used with lvgl.list.add - it manages its own "
|
||||
"child widgets in a way that isn't compatible with widgets created at runtime"
|
||||
)
|
||||
for child in w_conf.get(CONF_WIDGETS, ()):
|
||||
[(child_type, child_conf)] = child.items()
|
||||
_check_dynamic_widget_supported(child_type, child_conf)
|
||||
|
||||
|
||||
_UNSUPPORTED_DYNAMIC_KEYS = SWIPE_TRIGGERS + (CONF_ON_BOOT, CONF_ALIGN_TO)
|
||||
|
||||
|
||||
def _check_no_unsupported_triggers(w_type_name: str, w_conf: dict) -> None:
|
||||
# These triggers currently aren't supporte for dynamic widgets
|
||||
for key in _UNSUPPORTED_DYNAMIC_KEYS:
|
||||
if key in w_conf:
|
||||
raise cv.Invalid(
|
||||
f"'{key}' is not supported on a widget added via lvgl.list.add - it "
|
||||
"would validate but generate nothing, since it's only wired for "
|
||||
"widgets that exist at boot",
|
||||
path=[w_type_name, key],
|
||||
)
|
||||
for child in w_conf.get(CONF_WIDGETS, ()):
|
||||
[(child_type, child_conf)] = child.items()
|
||||
_check_no_unsupported_triggers(child_type, child_conf)
|
||||
|
||||
|
||||
def _check_no_explicit_widget_id(raw_value: dict) -> None:
|
||||
for w_type_name, w_conf in raw_value.items():
|
||||
if not isinstance(w_conf, dict):
|
||||
continue
|
||||
if CONF_ID in w_conf:
|
||||
raise cv.Invalid(
|
||||
"'id' is not allowed on a widget added via lvgl.list.add - it is "
|
||||
"rebuilt fresh on every call and never registered anywhere it "
|
||||
"could be looked up by",
|
||||
path=[w_type_name, CONF_ID],
|
||||
)
|
||||
for child in w_conf.get(CONF_WIDGETS, ()):
|
||||
if isinstance(child, dict):
|
||||
_check_no_explicit_widget_id(child)
|
||||
|
||||
|
||||
@schema_extractor("schema")
|
||||
def list_add_schema(value: Any) -> Any:
|
||||
# A plain cv.Schema can't express "id, an optional index, plus exactly one arbitrary
|
||||
# widget-type key", since the set of widget types isn't fixed until validation time.
|
||||
if value is SCHEMA_EXTRACT:
|
||||
return LIST_ID_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_INDEX): cv.templatable(cv.int_),
|
||||
**{
|
||||
cv.Optional(name): container_schema_value(widget_type)
|
||||
for name, widget_type in WIDGET_TYPES.items()
|
||||
},
|
||||
}
|
||||
)
|
||||
if not isinstance(value, dict):
|
||||
raise cv.Invalid("Expected a mapping")
|
||||
value = value.copy()
|
||||
if CONF_ID not in value:
|
||||
raise cv.Invalid(f"required key '{CONF_ID}' not provided")
|
||||
with cv.prepend_path([CONF_ID]):
|
||||
list_id = cv.use_id(lv_list_t)(value.pop(CONF_ID))
|
||||
result = {CONF_ID: list_id}
|
||||
if CONF_INDEX in value:
|
||||
with cv.prepend_path([CONF_INDEX]):
|
||||
result[CONF_INDEX] = cv.templatable(cv.int_)(value.pop(CONF_INDEX))
|
||||
if len(value) != 1:
|
||||
raise cv.Invalid(
|
||||
"lvgl.list.add takes exactly one widget definition, e.g. 'label:' or 'button:', alongside 'id' and optional 'index'"
|
||||
)
|
||||
_check_no_explicit_widget_id(value)
|
||||
result[CONF_WIDGET] = any_widget_schema()(value)
|
||||
[(w_type_name, w_conf)] = result[CONF_WIDGET][0].items()
|
||||
_check_dynamic_widget_supported(w_type_name, w_conf)
|
||||
_check_no_unsupported_triggers(w_type_name, w_conf)
|
||||
return result
|
||||
|
||||
|
||||
def _register_lv_uses(w_type_name: str, w_conf: dict) -> None:
|
||||
# Must run before this coroutine's first await.
|
||||
widget_type = WIDGET_TYPES[w_type_name]
|
||||
add_lv_use(w_type_name)
|
||||
add_lv_use(*widget_type.get_uses())
|
||||
for child in w_conf.get(CONF_WIDGETS, ()):
|
||||
[(child_type, child_conf)] = child.items()
|
||||
_register_lv_uses(child_type, child_conf)
|
||||
|
||||
|
||||
def _register_dynamic_widget_style_uses(w_conf: dict) -> None:
|
||||
props = {
|
||||
remap_property(prop)
|
||||
for part_states in collect_parts(w_conf).values()
|
||||
for state_props in part_states.values()
|
||||
for prop in state_props
|
||||
if prop in ALL_STYLES
|
||||
}
|
||||
apply_style_driven_defines(props)
|
||||
for child in w_conf.get(CONF_WIDGETS, ()):
|
||||
[(_, child_conf)] = child.items()
|
||||
_register_dynamic_widget_style_uses(child_conf)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"lvgl.list.add",
|
||||
ObjUpdateAction,
|
||||
list_add_schema,
|
||||
synchronous=True,
|
||||
)
|
||||
async def list_add_to_code(config, action_id, template_arg, args):
|
||||
[(w_type_name, w_conf)] = config[CONF_WIDGET][0].items()
|
||||
_register_lv_uses(w_type_name, w_conf)
|
||||
_register_dynamic_widget_style_uses(w_conf)
|
||||
widgets = await get_widgets(config)
|
||||
|
||||
async def do_add(w: Widget):
|
||||
index = None
|
||||
if (idx := config.get(CONF_INDEX)) is not None:
|
||||
index = await lv_int.process(idx)
|
||||
await _build_dynamic_widget(
|
||||
w_type_name,
|
||||
w_conf,
|
||||
w.obj,
|
||||
config[CONF_ID],
|
||||
w.obj,
|
||||
top_level=True,
|
||||
index=index,
|
||||
)
|
||||
|
||||
return await action_to_code(widgets, do_add, action_id, template_arg, args, config)
|
||||
|
||||
|
||||
async def _build_dynamic_widget(
|
||||
w_type_name: str,
|
||||
w_conf: dict,
|
||||
parent,
|
||||
list_id,
|
||||
list_obj,
|
||||
top_level: bool = False,
|
||||
index=None,
|
||||
depth: int = 0,
|
||||
) -> None:
|
||||
# Builds one widget (recursively, with children and triggers) as a LocalVariable
|
||||
# instead of a global Pvariable. Compound
|
||||
# widgets are heap-allocated and freed via LV_EVENT_DELETE.
|
||||
# `depth` suffixes the local variable's name below the row's top level.
|
||||
widget_type = WIDGET_TYPES[w_type_name]
|
||||
var_name = f"dyn_{w_type_name}" if depth == 0 else f"dyn_{w_type_name}_{depth}"
|
||||
add_lv_use(w_type_name)
|
||||
add_lv_use(*widget_type.get_uses())
|
||||
|
||||
async def finish_and_fire(w: Widget) -> None:
|
||||
# Shared tail for both branches below - must run while var's LocalVariable
|
||||
# block (opened by whichever branch calls this) is still open
|
||||
await _finish_dynamic_widget(w, w_conf, list_id, list_obj, depth)
|
||||
if top_level:
|
||||
if index is not None:
|
||||
lv.obj_move_to_index(w.obj, index)
|
||||
await _fire_on_add(list_id, list_obj, w.obj)
|
||||
|
||||
if widget_type.is_compound():
|
||||
with LocalVariable(
|
||||
var_name, widget_type.w_type, widget_type.w_type.new()
|
||||
) as var:
|
||||
creator = await widget_type.obj_creator(parent, w_conf)
|
||||
lv_add(var.set_obj(creator))
|
||||
w = Widget(var, widget_type, w_conf)
|
||||
lv_obj.add_event_cb(
|
||||
w.obj,
|
||||
literal(f"lvgl::delete_lv_compound_on_delete<{widget_type.w_type}>"),
|
||||
literal("LV_EVENT_DELETE"),
|
||||
var,
|
||||
)
|
||||
await finish_and_fire(w)
|
||||
else:
|
||||
creator = await widget_type.obj_creator(parent, w_conf)
|
||||
with LocalVariable(var_name, lv_obj_t, creator) as var:
|
||||
w = Widget(var, widget_type, w_conf)
|
||||
await finish_and_fire(w)
|
||||
|
||||
|
||||
async def _finish_dynamic_widget(
|
||||
w: Widget, w_conf: dict, list_id, list_obj, depth: int = 0
|
||||
) -> None:
|
||||
await w.type.on_create(w.obj, w_conf)
|
||||
apply_theme_styles(w)
|
||||
await set_obj_properties(w, w_conf)
|
||||
await w.type.to_code(w, w_conf)
|
||||
await _wire_dynamic_triggers(w, w_conf)
|
||||
for child in w_conf.get(CONF_WIDGETS, ()):
|
||||
[(child_type, child_conf)] = child.items()
|
||||
await _build_dynamic_widget(
|
||||
child_type, child_conf, w.obj, list_id, list_obj, depth=depth + 1
|
||||
)
|
||||
|
||||
|
||||
async def _wire_dynamic_triggers(w: Widget, config: dict) -> None:
|
||||
# Mirrors generate_triggers(), but runs immediately
|
||||
if w.type.is_compound():
|
||||
event_var = MockObj(
|
||||
f"static_cast<{w.type.w_type} *>(lv_event_get_user_data(event))", "->"
|
||||
)
|
||||
user_data = w.var
|
||||
else:
|
||||
event_var = literal("static_cast<lv_obj_t *>(lv_event_get_target(event))")
|
||||
user_data = None
|
||||
event_target = Widget(event_var, w.type, config)
|
||||
for event, conf in {
|
||||
event: conf for event, conf in config.items() if event in LV_EVENT_TRIGGERS
|
||||
}.items():
|
||||
w.add_flag("LV_OBJ_FLAG_CLICKABLE")
|
||||
await add_trigger(
|
||||
conf[0], event_target, event, attach_obj=w.obj, user_data=user_data
|
||||
)
|
||||
for conf in config.get(CONF_ON_VALUE, ()):
|
||||
await add_trigger(
|
||||
conf,
|
||||
event_target,
|
||||
LV_EVENT.VALUE_CHANGED,
|
||||
UPDATE_EVENT,
|
||||
attach_obj=w.obj,
|
||||
user_data=user_data,
|
||||
)
|
||||
for conf in config.get(CONF_ON_UPDATE, ()):
|
||||
await add_trigger(
|
||||
conf, event_target, UPDATE_EVENT, attach_obj=w.obj, user_data=user_data
|
||||
)
|
||||
|
||||
|
||||
LIST_REMOVE_SCHEMA = LIST_ID_SCHEMA.extend(
|
||||
{
|
||||
# positive_int, not int_: a negative index would silently delete the *last*
|
||||
# row (lv_obj_get_child() counts back from the end) while reporting that
|
||||
# same bogus value to on_remove's list_index.
|
||||
cv.Required(CONF_INDEX): cv.templatable(cv.positive_int),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"lvgl.list.remove",
|
||||
ObjUpdateAction,
|
||||
LIST_REMOVE_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def list_remove_to_code(config, action_id, template_arg, args):
|
||||
widgets = await get_widgets(config)
|
||||
|
||||
async def do_remove(w: Widget):
|
||||
index = await lv_int.process(config[CONF_INDEX])
|
||||
# Materialised into a local since index is needed at two call sites below, and
|
||||
# a lambda's body gets re-emitted (and re-run) at every point it's used.
|
||||
with (
|
||||
LocalVariable("list_index", cg.int_, index, modifier="") as idx,
|
||||
# Out-of-range lookup/log lives in a shared C++ helper, not inline here:
|
||||
# a config can have many lvgl.list.remove call sites.
|
||||
LocalVariable(
|
||||
"list_child",
|
||||
lv_obj_t,
|
||||
cg.RawExpression(f"lvgl::lv_list_get_row_for_remove({w.obj}, {idx})"),
|
||||
) as child,
|
||||
LvConditional(child),
|
||||
):
|
||||
await _fire_on_remove(config[CONF_ID], idx)
|
||||
# Recursively destroys the whole subtree
|
||||
lv.obj_del(child)
|
||||
|
||||
return await action_to_code(
|
||||
widgets, do_remove, action_id, template_arg, args, config
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"lvgl.list.clear",
|
||||
ObjUpdateAction,
|
||||
LIST_ID_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def list_clear_to_code(config, action_id, template_arg, args):
|
||||
widgets = await get_widgets(config)
|
||||
|
||||
async def do_clear(w: Widget):
|
||||
await _wait_list_triggers_completed()
|
||||
triggers = _get_list_triggers(config[CONF_ID]).on_remove
|
||||
if triggers:
|
||||
# Fire on_remove for every entry, newest to oldest, before wiping them all out,
|
||||
# so on_remove's semantics ("an entry left the list") hold
|
||||
with LvCountdown("list_index", lv_expr.obj_get_child_count(w.obj)) as index:
|
||||
_fire_index_triggers(triggers, index)
|
||||
# lv_obj_clean recursively destroys every child's whole subtree
|
||||
lv.obj_clean(w.obj)
|
||||
|
||||
return await action_to_code(
|
||||
widgets, do_clear, action_id, template_arg, args, config
|
||||
)
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "image_format.h"
|
||||
#include "image_decoder.h"
|
||||
|
||||
namespace esphome::runtime_image {
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ SCALE = "scale"
|
||||
CONF_ATTRIBUTE_ID = "attribute_id"
|
||||
KEY_ZIGBEE_EP = "zigbee_ep"
|
||||
KEY_ZIGBEE_EP_NO_NUM = "zigbee_ep_no_num"
|
||||
KEY_ZIGBEE_FIRST_EP_CL = "zigbee_first_ep_cl"
|
||||
|
||||
DEVICE_ID = {
|
||||
"RANGE_EXTENDER": cg.RawExpression("EZB_ZHA_RANGE_EXTENDER_DEVICE_ID"),
|
||||
@@ -18,11 +19,13 @@ DEVICE_ID = {
|
||||
cluster_id = cg.esphome_ns.enum("ezb_zcl_cluster_id_e")
|
||||
CLUSTER_ID = {
|
||||
"BASIC": cluster_id.EZB_ZCL_CLUSTER_ID_BASIC,
|
||||
"TIME": cluster_id.EZB_ZCL_CLUSTER_ID_TIME,
|
||||
"BINARY_INPUT": cluster_id.EZB_ZCL_CLUSTER_ID_BINARY_INPUT,
|
||||
"ANALOG_INPUT": cluster_id.EZB_ZCL_CLUSTER_ID_ANALOG_INPUT,
|
||||
}
|
||||
CLUSTER_ROLE = {
|
||||
"SERVER": cg.RawExpression("EZB_ZCL_CLUSTER_SERVER"),
|
||||
"CLIENT": cg.RawExpression("EZB_ZCL_CLUSTER_CLIENT"),
|
||||
}
|
||||
attr_type = cg.esphome_ns.enum("ezb_zcl_attr_type_e")
|
||||
ATTR_TYPE = {
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import time as time_
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.const import CONF_ID, CONF_UPDATE_INTERVAL
|
||||
from esphome.core import CORE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import consume_endpoint
|
||||
from ..const import zigbee_ns
|
||||
from ..const_esp32 import ROLE
|
||||
from ..const_zephyr import CONF_ZIGBEE_ID
|
||||
from ..zigbee_ep_esp32 import add_clusters_to_first_ep, get_first_ep_num
|
||||
from ..zigbee_zephyr import (
|
||||
ZigbeeClusterDesc,
|
||||
ZigbeeComponent,
|
||||
@@ -22,26 +24,52 @@ DEPENDENCIES = ["zigbee"]
|
||||
|
||||
ZigbeeTime = zigbee_ns.class_("ZigbeeTime", time_.RealTimeClock)
|
||||
|
||||
|
||||
def _validate_zigbee_time(config: ConfigType) -> ConfigType:
|
||||
if CORE.is_nrf52:
|
||||
return consume_endpoint(config)
|
||||
if CORE.is_esp32:
|
||||
cl = [
|
||||
{
|
||||
CONF_ID: "TIME",
|
||||
ROLE: "CLIENT",
|
||||
},
|
||||
{
|
||||
CONF_ID: "TIME",
|
||||
ROLE: "SERVER",
|
||||
},
|
||||
]
|
||||
add_clusters_to_first_ep(cl)
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
time_.TIME_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(ZigbeeTime),
|
||||
cv.OnlyWith(CONF_ZIGBEE_ID, ["nrf52", "zigbee"]): cv.use_id(
|
||||
ZigbeeComponent
|
||||
),
|
||||
cv.GenerateID(CONF_ZIGBEE_ID): cv.use_id(ZigbeeComponent),
|
||||
cv.SplitDefault(
|
||||
CONF_UPDATE_INTERVAL,
|
||||
nrf52="1s",
|
||||
esp32="15min",
|
||||
): cv.update_interval, # override default from TIME_SCHEMA. Remove once nrf52 implementation is aligned.
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(cv.polling_component_schema("1s")),
|
||||
consume_endpoint,
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
_validate_zigbee_time,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
CORE.add_job(_add_time, config)
|
||||
if CORE.using_zephyr:
|
||||
CORE.add_job(_add_time_zephyr, config)
|
||||
if CORE.is_esp32:
|
||||
zb = await cg.get_variable(config[CONF_ZIGBEE_ID])
|
||||
var = cg.new_Pvariable(config[CONF_ID], zb, get_first_ep_num())
|
||||
await cg.register_component(var, config)
|
||||
await time_.register_time(var, config)
|
||||
|
||||
|
||||
async def _add_time(config: ConfigType) -> None:
|
||||
async def _add_time_zephyr(config: ConfigType) -> None:
|
||||
slot_index = get_slot_index()
|
||||
|
||||
# Create unique names for this sensor's variables based on slot index
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
#include "zigbee_time_esp32.h"
|
||||
#if defined(USE_ZIGBEE) && defined(USE_ESP32) && defined(USE_TIME)
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
namespace esphome::zigbee {
|
||||
|
||||
static const char *const TAG = "zigbee.time";
|
||||
|
||||
// This time standard is the number of
|
||||
// seconds since 0 hrs 0 mins 0 sec on 1st January 2000 UTC (Universal Coordinated Time).
|
||||
constexpr time_t EPOCH_2000 = 946684800;
|
||||
|
||||
static ZigbeeTime *global_time = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
void ZigbeeTime::setup() {
|
||||
global_time = this;
|
||||
if (this->parent_->is_started()) {
|
||||
this->register_zb_time_();
|
||||
} else {
|
||||
this->parent_->add_on_start_callback([this]() { this->register_zb_time_(); });
|
||||
}
|
||||
}
|
||||
|
||||
void ZigbeeTime::register_zb_time_() {
|
||||
ezb_zcl_time_interface_t time_interface = {
|
||||
.get_utc_time = esphome::zigbee::ZigbeeTime::get_utc_time,
|
||||
.set_utc_time = esphome::zigbee::ZigbeeTime::set_utc_time,
|
||||
};
|
||||
ezb_err_t ret;
|
||||
if (!esp_zigbee_lock_acquire(10 / portTICK_PERIOD_MS)) {
|
||||
this->set_timeout("zb_time_register", 100, [this]() { this->register_zb_time_(); });
|
||||
return;
|
||||
}
|
||||
ret = ezb_zcl_time_server_interface_register(this->endpoint_, time_interface);
|
||||
esp_zigbee_lock_release();
|
||||
if (ret != EZB_ERR_NONE) {
|
||||
ESP_LOGW(TAG, "Setup failed: %d", ret);
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
this->registered_ = true;
|
||||
this->parent_->add_on_join_callback([this](bool x) { this->update(); });
|
||||
if (this->parent_->is_joined()) {
|
||||
this->update();
|
||||
}
|
||||
}
|
||||
|
||||
void ZigbeeTime::status_cb(ezb_err_t status) {
|
||||
if (status == EZB_ERR_NONE) {
|
||||
ESP_LOGV(TAG, "Time synchronization successful");
|
||||
} else if (status == EZB_ERR_TIMEOUT) {
|
||||
ESP_LOGW(TAG, "Time synchronization timed out");
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Time synchronization failed with error: %d", status);
|
||||
}
|
||||
}
|
||||
|
||||
void ZigbeeTime::update() {
|
||||
if (this->parent_->is_joined() && this->registered_) {
|
||||
if (esp_zigbee_lock_acquire(10 / portTICK_PERIOD_MS)) {
|
||||
ESP_LOGV(TAG, "Updating time sync from Zigbee network...");
|
||||
ezb_zcl_time_server_synchronize_time(this->endpoint_, 10, esphome::zigbee::ZigbeeTime::status_cb,
|
||||
EZB_ZCL_TIME_SERVER_RANK_MASTER);
|
||||
esp_zigbee_lock_release();
|
||||
this->retry_count_ = 0;
|
||||
} else {
|
||||
if (this->retry_count_ == 0) {
|
||||
ESP_LOGW(TAG, "Could not acquire Zigbee lock to synchronize time, will retry maximum 3 times");
|
||||
}
|
||||
if (this->retry_count_ < 3) {
|
||||
this->set_timeout("zb_time_sync", 100, [this]() { this->update(); });
|
||||
this->retry_count_++;
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Could not acquire Zigbee lock to synchronize time");
|
||||
this->retry_count_ = 0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ESP_LOGD(TAG, "Not connected to Zigbee network, cannot synchronize time");
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t ZigbeeTime::get_utc_time() {
|
||||
const time_t now = global_time->timestamp_now();
|
||||
if (now < EPOCH_2000) {
|
||||
return 0xFFFFFFFF; // ZCL invalid UTCTime
|
||||
}
|
||||
return (uint32_t) (now - EPOCH_2000);
|
||||
}
|
||||
|
||||
void ZigbeeTime::set_utc_time(uint32_t utc) {
|
||||
// prevent overflow
|
||||
if (utc <= (std::numeric_limits<uint32_t>::max() - EPOCH_2000)) {
|
||||
global_time->set_epoch_time(utc + EPOCH_2000);
|
||||
}
|
||||
}
|
||||
|
||||
void ZigbeeTime::set_epoch_time(uint32_t utc) {
|
||||
// called from zigbee task, defer to main loop
|
||||
this->defer([this, utc]() {
|
||||
ESP_LOGV(TAG, "Setting device time to UTC: %u", static_cast<unsigned>(utc));
|
||||
this->synchronize_epoch_(utc);
|
||||
});
|
||||
App.wake_loop_threadsafe();
|
||||
}
|
||||
|
||||
void ZigbeeTime::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Zigbee Time\n"
|
||||
" Endpoint: %u",
|
||||
this->endpoint_);
|
||||
RealTimeClock::dump_config();
|
||||
}
|
||||
|
||||
} // namespace esphome::zigbee
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
#include "esphome/core/defines.h"
|
||||
#if defined(USE_ZIGBEE) && defined(USE_ESP32) && defined(USE_TIME)
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/time/real_time_clock.h"
|
||||
#include "../zigbee_esp32.h"
|
||||
|
||||
namespace esphome::zigbee {
|
||||
|
||||
class ZigbeeComponent;
|
||||
|
||||
class ZigbeeTime final : public time::RealTimeClock {
|
||||
public:
|
||||
ZigbeeTime(ZigbeeComponent *parent, uint8_t ep) : parent_(parent), endpoint_(ep) {}
|
||||
void setup() override;
|
||||
void update() override;
|
||||
void dump_config() override;
|
||||
void set_epoch_time(uint32_t utc);
|
||||
|
||||
protected:
|
||||
void register_zb_time_();
|
||||
static void set_utc_time(uint32_t utc);
|
||||
static uint32_t get_utc_time();
|
||||
static void status_cb(ezb_err_t status);
|
||||
|
||||
ZigbeeComponent *parent_;
|
||||
uint8_t endpoint_;
|
||||
uint8_t retry_count_{0};
|
||||
bool registered_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::zigbee
|
||||
#endif
|
||||
@@ -18,6 +18,7 @@ from .const_esp32 import (
|
||||
DEVICE_TYPE,
|
||||
KEY_ZIGBEE_EP,
|
||||
KEY_ZIGBEE_EP_NO_NUM,
|
||||
KEY_ZIGBEE_FIRST_EP_CL,
|
||||
ROLE,
|
||||
)
|
||||
|
||||
@@ -95,11 +96,11 @@ def _get_next_ep_num(eps: list[int]) -> int:
|
||||
|
||||
|
||||
def _compare_clusters(
|
||||
existing_ep: dict[str, Any],
|
||||
ep: dict[str, Any],
|
||||
existing_cl_list: list[dict[str, Any]],
|
||||
cl_list: list[dict[str, Any]],
|
||||
) -> tuple[str | int, str] | None:
|
||||
existing_clusters = [(cl[CONF_ID], cl[ROLE]) for cl in existing_ep[CONF_CLUSTERS]]
|
||||
for cl in [(cl[CONF_ID], cl[ROLE]) for cl in ep[CONF_CLUSTERS]]:
|
||||
existing_clusters = [(cl[CONF_ID], cl[ROLE]) for cl in existing_cl_list]
|
||||
for cl in [(cl[CONF_ID], cl[ROLE]) for cl in cl_list]:
|
||||
if cl in existing_clusters:
|
||||
return cl
|
||||
return None
|
||||
@@ -110,7 +111,7 @@ def _merge_endpoints(
|
||||
ep: dict[str, Any],
|
||||
use_type: bool | None,
|
||||
) -> bool:
|
||||
if _compare_clusters(existing_ep, ep):
|
||||
if _compare_clusters(existing_ep.get(CONF_CLUSTERS, []), ep.get(CONF_CLUSTERS, [])):
|
||||
return False
|
||||
if (
|
||||
ep.get(DEVICE_TYPE)
|
||||
@@ -200,6 +201,17 @@ def create_ep(router: bool) -> None:
|
||||
|
||||
# clear list so that it is not processed again
|
||||
del zb_data[KEY_ZIGBEE_EP_NO_NUM]
|
||||
# Add clusters to first ep
|
||||
cl_list: list[dict] = zb_data.setdefault(KEY_ZIGBEE_FIRST_EP_CL, [])
|
||||
if cl_list:
|
||||
first_ep = ep_dict[get_first_ep_num()]
|
||||
first_ep.setdefault(CONF_CLUSTERS, [])
|
||||
if cl := _compare_clusters(first_ep[CONF_CLUSTERS], cl_list):
|
||||
raise cv.Invalid(
|
||||
f"Endpoint {get_first_ep_num()} has more than one cluster with cluster id {cl[0]} and role {cl[1]}."
|
||||
)
|
||||
first_ep[CONF_CLUSTERS] += cl_list
|
||||
del zb_data[KEY_ZIGBEE_FIRST_EP_CL]
|
||||
|
||||
# Add default device type to endpoints that have none
|
||||
for ep in ep_dict.values():
|
||||
@@ -207,6 +219,15 @@ def create_ep(router: bool) -> None:
|
||||
ep[DEVICE_TYPE] = "CUSTOM_ATTR"
|
||||
|
||||
|
||||
def get_first_ep_num() -> int | None:
|
||||
"""Return the number of the first endpoint."""
|
||||
zb_data = CORE.data.setdefault(KEY_ZIGBEE, {})
|
||||
ep_dict: dict[int, dict] = zb_data.setdefault(KEY_ZIGBEE_EP, {})
|
||||
if ep_dict:
|
||||
return min(ep_dict.keys())
|
||||
return None
|
||||
|
||||
|
||||
def add_ep(ep: dict[str, Any], ep_num: int | None, use_type: bool | None) -> None:
|
||||
"""Add a Zigbee endpoint configuration to CORE.data.
|
||||
|
||||
@@ -230,8 +251,8 @@ def add_ep(ep: dict[str, Any], ep_num: int | None, use_type: bool | None) -> Non
|
||||
# check if the existing endpoint has same clusters
|
||||
existing_ep = ep_dict[ep_num]
|
||||
if cl := _compare_clusters(
|
||||
existing_ep,
|
||||
ep,
|
||||
existing_ep.get(CONF_CLUSTERS, []),
|
||||
ep.get(CONF_CLUSTERS, []),
|
||||
):
|
||||
raise cv.Invalid(
|
||||
f"Endpoint {ep_num} has more than one cluster with cluster id {cl[0]} and role {cl[1]}."
|
||||
@@ -245,3 +266,21 @@ def add_ep(ep: dict[str, Any], ep_num: int | None, use_type: bool | None) -> Non
|
||||
if use_type or ep.get(DEVICE_TYPE):
|
||||
ep[CONF_USE_DEVICE_TYPE] = {ep.get(DEVICE_TYPE): use_type}
|
||||
ep_dict[ep_num] = ep
|
||||
|
||||
|
||||
def add_clusters_to_first_ep(cl: list[dict[str, Any]]) -> None:
|
||||
"""Add a list of Zigbee clusters to CORE.data.
|
||||
|
||||
Args:
|
||||
cl: list of cluster dictonaries.
|
||||
"""
|
||||
zb_data = CORE.data.setdefault(KEY_ZIGBEE, {})
|
||||
cl_list: list[dict] = zb_data.setdefault(KEY_ZIGBEE_FIRST_EP_CL, [])
|
||||
if cluster := _compare_clusters(
|
||||
cl_list,
|
||||
cl,
|
||||
):
|
||||
raise cv.Invalid(
|
||||
f"Only one cluster with cluster id {cluster[0]} and role {cluster[1]} can be added to first endpoint."
|
||||
)
|
||||
cl_list += cl
|
||||
|
||||
@@ -30,6 +30,8 @@ ezb_zcl_cluster_desc_t esphome_zb_default_cluster_dscr_create(uint16_t cluster_i
|
||||
return ezb_zcl_basic_create_cluster_desc(NULL, role_mask);
|
||||
case EZB_ZCL_CLUSTER_ID_IDENTIFY:
|
||||
return ezb_zcl_identify_create_cluster_desc(NULL, role_mask);
|
||||
case EZB_ZCL_CLUSTER_ID_TIME:
|
||||
return ezb_zcl_time_create_cluster_desc(NULL, role_mask);
|
||||
case EZB_ZCL_CLUSTER_ID_ANALOG_INPUT:
|
||||
return ezb_zcl_analog_input_create_cluster_desc(NULL, role_mask);
|
||||
case EZB_ZCL_CLUSTER_ID_BINARY_INPUT:
|
||||
@@ -49,6 +51,8 @@ ezb_err_t esphome_zb_cluster_add_attr(uint16_t cluster_id, ezb_zcl_cluster_desc_
|
||||
return ezb_zcl_basic_cluster_desc_add_attr(cluster_desc, attr_id, value_p);
|
||||
case EZB_ZCL_CLUSTER_ID_IDENTIFY:
|
||||
return ezb_zcl_identify_cluster_desc_add_attr(cluster_desc, attr_id, value_p);
|
||||
case EZB_ZCL_CLUSTER_ID_TIME:
|
||||
return ezb_zcl_time_cluster_desc_add_attr(cluster_desc, attr_id, value_p);
|
||||
case EZB_ZCL_CLUSTER_ID_ANALOG_INPUT:
|
||||
return ezb_zcl_analog_input_cluster_desc_add_attr(cluster_desc, attr_id, value_p);
|
||||
case EZB_ZCL_CLUSTER_ID_BINARY_INPUT:
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ pyserial==3.5
|
||||
platformio==6.1.19
|
||||
esptool==5.3.1
|
||||
click==8.3.3
|
||||
aioesphomeapi==46.1.0
|
||||
aioesphomeapi==46.2.0
|
||||
aiohappyeyeballs==2.7.1 # Happy Eyeballs for requests downloads; already pulled in by aioesphomeapi
|
||||
zeroconf==0.150.0
|
||||
puremagic==2.2.0
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
esphome:
|
||||
name: test-list-on-add-lvgl-action
|
||||
|
||||
esp32:
|
||||
board: lolin_c3_mini
|
||||
|
||||
spi:
|
||||
mosi_pin:
|
||||
number: GPIO2
|
||||
ignore_strapping_warning: true
|
||||
clk_pin: GPIO1
|
||||
|
||||
display:
|
||||
- platform: mipi_spi
|
||||
data_rate: 20MHz
|
||||
model: st7735
|
||||
cs_pin:
|
||||
number: GPIO8
|
||||
ignore_strapping_warning: true
|
||||
dc_pin: GPIO3
|
||||
|
||||
lvgl:
|
||||
widgets:
|
||||
- label:
|
||||
id: later_label
|
||||
text: orig
|
||||
- list:
|
||||
id: test_list
|
||||
on_add:
|
||||
- lvgl.label.update:
|
||||
id: later_label
|
||||
text: "changed"
|
||||
on_remove:
|
||||
- lvgl.label.update:
|
||||
id: later_label
|
||||
text: "removed"
|
||||
@@ -0,0 +1,41 @@
|
||||
esphome:
|
||||
name: test-list-outside-block
|
||||
on_boot:
|
||||
priority: -100
|
||||
then:
|
||||
- lvgl.list.add:
|
||||
id: test_list
|
||||
switch:
|
||||
transform_rotation: 100
|
||||
drop_shadow_color: 0x000000
|
||||
bg_image_src: my_image
|
||||
|
||||
esp32:
|
||||
board: lolin_c3_mini
|
||||
|
||||
spi:
|
||||
mosi_pin:
|
||||
number: GPIO2
|
||||
ignore_strapping_warning: true
|
||||
clk_pin: GPIO1
|
||||
|
||||
display:
|
||||
- platform: mipi_spi
|
||||
data_rate: 20MHz
|
||||
model: st7735
|
||||
cs_pin:
|
||||
number: GPIO8
|
||||
ignore_strapping_warning: true
|
||||
dc_pin: GPIO3
|
||||
|
||||
image:
|
||||
- platform: file
|
||||
file: mdi:battery
|
||||
id: my_image
|
||||
resize: 8x8
|
||||
type: binary
|
||||
|
||||
lvgl:
|
||||
widgets:
|
||||
- list:
|
||||
id: test_list
|
||||
@@ -0,0 +1,77 @@
|
||||
esphome:
|
||||
name: test-list
|
||||
|
||||
esp32:
|
||||
board: lolin_c3_mini
|
||||
|
||||
spi:
|
||||
mosi_pin:
|
||||
number: GPIO2
|
||||
ignore_strapping_warning: true
|
||||
clk_pin: GPIO1
|
||||
|
||||
display:
|
||||
- platform: mipi_spi
|
||||
data_rate: 20MHz
|
||||
model: st7735
|
||||
cs_pin:
|
||||
number: GPIO8
|
||||
ignore_strapping_warning: true
|
||||
dc_pin: GPIO3
|
||||
|
||||
lvgl:
|
||||
theme:
|
||||
label:
|
||||
bg_color: 0xFF0000
|
||||
widgets:
|
||||
- list:
|
||||
id: test_list
|
||||
pad_row: 4
|
||||
on_add:
|
||||
- delay: 10ms
|
||||
- delay: 20ms
|
||||
on_remove:
|
||||
- delay: 10ms
|
||||
- button:
|
||||
id: trigger_button
|
||||
text: "Trigger"
|
||||
on_click:
|
||||
- lvgl.list.add_text:
|
||||
id: test_list
|
||||
text: "Header"
|
||||
- lvgl.list.add_text:
|
||||
id: test_list
|
||||
text: "Pinned"
|
||||
index: 0
|
||||
- lvgl.list.add:
|
||||
id: test_list
|
||||
button:
|
||||
text: "Entry"
|
||||
checkable: true
|
||||
- lvgl.list.add:
|
||||
id: test_list
|
||||
index: 1
|
||||
obj:
|
||||
widgets:
|
||||
- label:
|
||||
text: "Nested"
|
||||
- dropdown:
|
||||
options:
|
||||
- "One"
|
||||
- "Two"
|
||||
- lvgl.list.add:
|
||||
id: test_list
|
||||
obj:
|
||||
widgets:
|
||||
- obj:
|
||||
widgets:
|
||||
- label:
|
||||
text: "Grandchild"
|
||||
- lvgl.list.remove:
|
||||
id: test_list
|
||||
index: 0
|
||||
- lvgl.list.clear:
|
||||
id: test_list
|
||||
- lvgl.list.update:
|
||||
id: test_list
|
||||
pad_row: 8
|
||||
@@ -0,0 +1,403 @@
|
||||
"""Tests for the LVGL ``list`` widget: schema validation for its actions
|
||||
(``lvgl.list.add_text``/``add``/``remove``/``clear``) and the code they generate.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.__main__ import generate_cpp_contents
|
||||
from esphome.components.lvgl.widgets.lv_list import (
|
||||
LIST_CREATE_SCHEMA,
|
||||
LIST_REMOVE_SCHEMA,
|
||||
LIST_SCHEMA,
|
||||
list_add_schema,
|
||||
)
|
||||
from esphome.config import read_config
|
||||
import esphome.config_validation as cv
|
||||
from esphome.core import CORE
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# lvgl.list.add schema: id + optional index + exactly one widget-type key
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestListAddSchema:
|
||||
def test_valid_single_widget(self) -> None:
|
||||
result = list_add_schema({"id": "my_list", "label": {"text": "hi"}})
|
||||
assert result["id"].id == "my_list"
|
||||
assert "widget" in result
|
||||
|
||||
def test_index_optional_and_templatable(self) -> None:
|
||||
result = list_add_schema({"id": "my_list", "index": 2, "label": {"text": "hi"}})
|
||||
assert result["index"] == 2
|
||||
|
||||
def test_index_omitted_when_not_given(self) -> None:
|
||||
result = list_add_schema({"id": "my_list", "label": {"text": "hi"}})
|
||||
assert "index" not in result
|
||||
|
||||
def test_missing_id_rejected(self) -> None:
|
||||
with pytest.raises(cv.Invalid, match="required key 'id' not provided"):
|
||||
list_add_schema({"label": {"text": "hi"}})
|
||||
|
||||
def test_no_widget_key_rejected(self) -> None:
|
||||
with pytest.raises(cv.Invalid, match="exactly one widget definition"):
|
||||
list_add_schema({"id": "my_list"})
|
||||
|
||||
def test_two_widget_keys_rejected(self) -> None:
|
||||
with pytest.raises(cv.Invalid, match="exactly one widget definition"):
|
||||
list_add_schema(
|
||||
{
|
||||
"id": "my_list",
|
||||
"label": {"text": "a"},
|
||||
"button": {"text": "b"},
|
||||
}
|
||||
)
|
||||
|
||||
def test_non_mapping_rejected(self) -> None:
|
||||
with pytest.raises(cv.Invalid, match="Expected a mapping"):
|
||||
list_add_schema("not_a_mapping")
|
||||
|
||||
def test_any_registered_widget_type_accepted(self) -> None:
|
||||
for widget_key, widget_conf in (
|
||||
("checkbox", {"text": "Option"}),
|
||||
("switch", {}),
|
||||
("spinner", {}),
|
||||
("obj", {}),
|
||||
("dropdown", {"options": ["a", "b"]}),
|
||||
):
|
||||
result = list_add_schema({"id": "my_list", widget_key: widget_conf})
|
||||
assert widget_key in result["widget"][0]
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("widget_key", "widget_conf"),
|
||||
[
|
||||
("buttonmatrix", {"rows": [{"buttons": [{"text": "A"}]}]}),
|
||||
("tabview", {"tabs": [{"name": "Tab1"}]}),
|
||||
("tileview", {"tiles": [{"row": 0, "column": 0}]}),
|
||||
("meter", {"scales": [{"range_from": 0, "range_to": 100}]}),
|
||||
("canvas", {"width": 20, "height": 20}),
|
||||
],
|
||||
)
|
||||
def test_dynamic_widget_unsupported_rejected(
|
||||
self, widget_key: str, widget_conf: dict
|
||||
) -> None:
|
||||
"""buttonmatrix/tabview/tileview all register their own child widgets into
|
||||
the global widget map from inside their to_code - fine for a widget built
|
||||
once at boot, but broken if lvgl.list.add re-enters that on every call.
|
||||
meter/canvas are rejected for a related but distinct reason: they declare a
|
||||
Pvariable (meter's scale/indicator objects; canvas's draw buffer) with
|
||||
cg.Pvariable()/cg.new_Pvariable(), which emits its assignment wherever code
|
||||
is currently being generated -- fine at the top level of a boot-time
|
||||
to_code, but lvgl.list.add's do_add runs inside a lambda. meter's assignment
|
||||
would then end up outside the very lambda that declares the local object it
|
||||
refers to (doesn't compile); canvas's Pvariable is declared once per config
|
||||
site rather than per call, so every call overwrites its one draw buffer
|
||||
(compiles, but leaks the old buffer and shares one buffer across every row).
|
||||
"""
|
||||
with pytest.raises(cv.Invalid, match="cannot be used with lvgl.list.add"):
|
||||
list_add_schema({"id": "my_list", widget_key: widget_conf})
|
||||
|
||||
def test_dynamic_widget_unsupported_rejected_when_nested(self) -> None:
|
||||
"""The check must recurse into `widgets:` so a tabview hidden a few levels
|
||||
deep inside another widget is caught too, not just at the top level.
|
||||
"""
|
||||
with pytest.raises(cv.Invalid, match="cannot be used with lvgl.list.add"):
|
||||
list_add_schema(
|
||||
{
|
||||
"id": "my_list",
|
||||
"obj": {"widgets": [{"tabview": {"tabs": [{"name": "Tab1"}]}}]},
|
||||
}
|
||||
)
|
||||
|
||||
def test_explicit_id_rejected(self) -> None:
|
||||
"""A dynamically-added widget is LocalVariable-scoped and rebuilt fresh
|
||||
on every call, never registered anywhere an id could be looked up by --
|
||||
an explicit id: would otherwise validate fine and then fail confusingly
|
||||
(an uncaught traceback, not a clean config error) the moment anything
|
||||
tries to reference it.
|
||||
"""
|
||||
with pytest.raises(cv.Invalid, match="'id' is not allowed"):
|
||||
list_add_schema(
|
||||
{"id": "my_list", "label": {"id": "dyn_label", "text": "hi"}}
|
||||
)
|
||||
|
||||
def test_explicit_id_rejected_when_nested(self) -> None:
|
||||
with pytest.raises(cv.Invalid, match="'id' is not allowed"):
|
||||
list_add_schema(
|
||||
{
|
||||
"id": "my_list",
|
||||
"obj": {"widgets": [{"label": {"id": "dyn_label", "text": "hi"}}]},
|
||||
}
|
||||
)
|
||||
|
||||
def test_no_explicit_id_still_valid(self) -> None:
|
||||
"""An id is auto-generated (and simply unused) when none is given --
|
||||
only an explicit one is rejected."""
|
||||
result = list_add_schema({"id": "my_list", "label": {"text": "hi"}})
|
||||
assert "id" in result["widget"][0]["label"]
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("key", "conf"),
|
||||
[
|
||||
("on_swipe_left", [{"logger.log": "swiped"}]),
|
||||
("on_swipe_right", [{"logger.log": "swiped"}]),
|
||||
("on_swipe_up", [{"logger.log": "swiped"}]),
|
||||
("on_swipe_down", [{"logger.log": "swiped"}]),
|
||||
("on_boot", [{"logger.log": "booted"}]),
|
||||
("align_to", {"id": "some_other_widget", "align": "OUT_LEFT_TOP"}),
|
||||
],
|
||||
)
|
||||
def test_unsupported_trigger_rejected(self, key: str, conf: list) -> None:
|
||||
"""_wire_dynamic_triggers only wires LV_EVENT_TRIGGERS/on_value/on_update --
|
||||
on_swipe_*/on_boot would otherwise validate fine and then silently generate
|
||||
nothing at all for a widget added via lvgl.list.add. align_to is in the same
|
||||
bucket: it's only ever consumed by generate_triggers() reading
|
||||
get_widget_map(), which a widget built via lvgl.list.add never enters.
|
||||
"""
|
||||
with pytest.raises(cv.Invalid, match="is not supported"):
|
||||
list_add_schema({"id": "my_list", "obj": {key: conf}})
|
||||
|
||||
def test_unsupported_trigger_rejected_when_nested(self) -> None:
|
||||
with pytest.raises(cv.Invalid, match="is not supported"):
|
||||
list_add_schema(
|
||||
{
|
||||
"id": "my_list",
|
||||
"obj": {
|
||||
"widgets": [
|
||||
{
|
||||
"label": {
|
||||
"text": "hi",
|
||||
"on_swipe_left": [{"logger.log": "swiped"}],
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# lvgl.list.remove: index must be non-negative -- LVGL treats a negative index as
|
||||
# counting back from the end, which would silently delete the wrong row while
|
||||
# reporting a list_index that matches nothing real to on_remove.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestListRemoveSchema:
|
||||
def test_negative_index_rejected(self) -> None:
|
||||
with pytest.raises(cv.Invalid, match="at least 0"):
|
||||
LIST_REMOVE_SCHEMA({"id": "my_list", "index": -1})
|
||||
|
||||
def test_zero_index_accepted(self) -> None:
|
||||
result = LIST_REMOVE_SCHEMA({"id": "my_list", "index": 0})
|
||||
assert result["index"] == 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The list widget's own schema: pad_row is shared between create/update, but
|
||||
# on_add/on_remove only make sense at creation time.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestListCreateVsModifySchema:
|
||||
def test_create_schema_has_pad_row_and_triggers(self) -> None:
|
||||
keys = {str(k) for k in LIST_CREATE_SCHEMA.schema}
|
||||
assert "pad_row" in keys
|
||||
assert "on_add" in keys
|
||||
assert "on_remove" in keys
|
||||
|
||||
def test_modify_schema_has_pad_row_but_not_triggers(self) -> None:
|
||||
"""``lvgl.list.update`` can change pad_row but can't (re-)declare triggers."""
|
||||
keys = {str(k) for k in LIST_SCHEMA.schema}
|
||||
assert "pad_row" in keys
|
||||
assert "on_add" not in keys
|
||||
assert "on_remove" not in keys
|
||||
|
||||
def test_on_add_single_automation_with_multiple_actions(self) -> None:
|
||||
"""A bare action list under on_add: is one automation with a multi-step
|
||||
`then:`, not multiple independent automations.
|
||||
"""
|
||||
config = LIST_CREATE_SCHEMA({"on_add": [{"delay": "10ms"}, {"delay": "20ms"}]})
|
||||
assert len(config["on_add"]) == 1
|
||||
assert len(config["on_add"][0]["then"]) == 2
|
||||
|
||||
def test_on_add_accepts_multiple_independent_automations(self) -> None:
|
||||
"""Each explicit `then:` entry gets its own Trigger, so on_add can fire
|
||||
more than one independent automation.
|
||||
"""
|
||||
config = LIST_CREATE_SCHEMA(
|
||||
{
|
||||
"on_add": [
|
||||
{"then": [{"delay": "10ms"}]},
|
||||
{"then": [{"delay": "20ms"}]},
|
||||
]
|
||||
}
|
||||
)
|
||||
assert len(config["on_add"]) == 2
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Code generation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def main_cpp(request: pytest.FixtureRequest) -> str:
|
||||
"""Generate the C++ output for the shared list-widget YAML config once per
|
||||
module -- see test_widget_state.py for why this is module-scoped and
|
||||
inlines the generate_main fixture logic rather than depending on it.
|
||||
"""
|
||||
config_path = Path(request.fspath).parent / "config" / "list_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_pad_row_set_at_creation(main_cpp: str) -> None:
|
||||
assert "lv_obj_set_style_pad_row(test_list, 4, LV_PART_MAIN);" in main_cpp
|
||||
|
||||
|
||||
def test_pad_row_updated_via_update_action(main_cpp: str) -> None:
|
||||
assert "lv_obj_set_style_pad_row(test_list, 8, LV_PART_MAIN);" in main_cpp
|
||||
|
||||
|
||||
def test_add_text_appends(main_cpp: str) -> None:
|
||||
assert 'lv_list_add_text(test_list, "Header");' in main_cpp
|
||||
|
||||
|
||||
def test_add_text_with_index_moves_before_firing_on_add(main_cpp: str) -> None:
|
||||
"""The index move must happen before on_add fires, so the reported
|
||||
list_index reflects the entry's final position, not where it was appended.
|
||||
"""
|
||||
assert (
|
||||
'lv_obj_t *list_entry_VAR_ = lv_list_add_text(test_list, "Pinned");\n'
|
||||
" lv_obj_move_to_index(list_entry_VAR_, 0);\n"
|
||||
" triggerint_id->trigger(lvgl::lv_list_get_row_index(test_list, list_entry_VAR_));"
|
||||
) in main_cpp
|
||||
|
||||
|
||||
def test_add_button_with_checkable_flag(main_cpp: str) -> None:
|
||||
assert "lv_obj_t *dyn_button_VAR_ = lv_btn_create(test_list);" in main_cpp
|
||||
assert (
|
||||
"lv_obj_add_flag(dyn_button_VAR_, (lv_obj_flag_t)(LV_OBJ_FLAG_CHECKABLE));"
|
||||
in main_cpp
|
||||
)
|
||||
assert (
|
||||
'lv_label_set_text(lv_obj_get_child(dyn_button_VAR_, 0), "Entry");' in main_cpp
|
||||
)
|
||||
|
||||
|
||||
def test_add_nested_hierarchy_with_compound_child(main_cpp: str) -> None:
|
||||
"""`obj: {widgets: [label, dropdown]}` builds a plain label child and a
|
||||
heap-allocated (compound) dropdown child, both parented to the new row.
|
||||
|
||||
The child variable names carry a `_1` (depth) suffix, distinguishing them
|
||||
from the row's own top-level variable -- necessary so that a child of the
|
||||
*same* widget type as its parent (e.g. `obj: {widgets: [{obj: {...}}]}`)
|
||||
doesn't declare a C++ variable that shadows its own not-yet-initialized
|
||||
self, silently parenting the child to garbage.
|
||||
"""
|
||||
assert "lv_obj_t *dyn_obj_VAR_ = lv_obj_create(test_list);" in main_cpp
|
||||
assert (
|
||||
"lv_obj_t *dyn_label_1_VAR_ = lv_label_create(dyn_obj_VAR_);\n"
|
||||
" lv_obj_add_style(dyn_label_1_VAR_, _lv_theme_style_label_main_default, "
|
||||
"(lv_state_t)(LV_PART_MAIN));\n"
|
||||
' lv_label_set_text(dyn_label_1_VAR_, "Nested");'
|
||||
) in main_cpp
|
||||
|
||||
|
||||
def test_add_applies_theme_styles_to_dynamic_widget(main_cpp: str) -> None:
|
||||
"""A widget added via lvgl.list.add must pick up the same `theme:` styling a
|
||||
statically-declared widget of the same type gets, not render unthemed.
|
||||
"""
|
||||
assert (
|
||||
"lv_obj_add_style(dyn_label_1_VAR_, _lv_theme_style_label_main_default, "
|
||||
"(lv_state_t)(LV_PART_MAIN));"
|
||||
) in main_cpp
|
||||
assert "LvDropdownType *dyn_dropdown_1_VAR_ = new LvDropdownType();" in main_cpp
|
||||
assert "lv_dropdown_create(dyn_obj_VAR_)" in main_cpp
|
||||
assert (
|
||||
"lvgl::delete_lv_compound_on_delete<LvDropdownType>, LV_EVENT_DELETE, "
|
||||
"dyn_dropdown_1_VAR_);"
|
||||
) in main_cpp
|
||||
|
||||
|
||||
def test_add_nested_same_type_child_does_not_shadow_parent(main_cpp: str) -> None:
|
||||
"""A child of the same widget type as its parent (`obj: {widgets: [{obj:
|
||||
...}]}`) must get a distinct C++ variable name (or the child's declaration
|
||||
would shadow its own not-yet-initialized self, parenting it to garbage --
|
||||
compiling clean but for a -Wuninitialized warning). A grandchild of a third
|
||||
type proves depth, not just type, drives the disambiguating suffix.
|
||||
"""
|
||||
assert "lv_obj_t *dyn_obj_VAR_ = lv_obj_create(test_list);" in main_cpp
|
||||
assert "lv_obj_t *dyn_obj_1_VAR_ = lv_obj_create(dyn_obj_VAR_);" in main_cpp
|
||||
assert (
|
||||
"lv_obj_t *dyn_label_2_VAR_ = lv_label_create(dyn_obj_1_VAR_);\n"
|
||||
" lv_obj_add_style(dyn_label_2_VAR_, _lv_theme_style_label_main_default, "
|
||||
"(lv_state_t)(LV_PART_MAIN));\n"
|
||||
' lv_label_set_text(dyn_label_2_VAR_, "Grandchild");'
|
||||
) in main_cpp
|
||||
|
||||
|
||||
def test_add_moves_row_to_given_index_before_firing_on_add(main_cpp: str) -> None:
|
||||
assert (
|
||||
"lv_obj_move_to_index(dyn_obj_VAR_, 1);\n"
|
||||
" triggerint_id->trigger(lvgl::lv_list_get_row_index(test_list, dyn_obj_VAR_));"
|
||||
) in main_cpp
|
||||
|
||||
|
||||
def test_on_add_fires_once_per_entry_via_shared_trigger(main_cpp: str) -> None:
|
||||
"""A single on_add: automation means a single Trigger instance, reused by
|
||||
every lvgl.list.add_text/add call site.
|
||||
"""
|
||||
assert main_cpp.count("triggerint_id->trigger(lvgl::lv_list_get_row_index(") == 5
|
||||
|
||||
|
||||
def test_remove_guards_against_missing_child_and_fires_before_delete(
|
||||
main_cpp: str,
|
||||
) -> None:
|
||||
"""The index is materialised into a local once (list_index_VAR_) and reused for
|
||||
both the child lookup and the on_remove trigger, so a templatable index isn't
|
||||
evaluated twice.
|
||||
"""
|
||||
assert (
|
||||
"int list_index_VAR_ = 0;\n"
|
||||
" {\n"
|
||||
" lv_obj_t *list_child_VAR_ = lvgl::lv_list_get_row_for_remove(test_list, list_index_VAR_);\n"
|
||||
" if (list_child_VAR_) {\n"
|
||||
" triggerint_id_2->trigger(list_index_VAR_);\n"
|
||||
" lv_obj_del(list_child_VAR_);"
|
||||
) in main_cpp
|
||||
|
||||
|
||||
def test_remove_out_of_range_lookup_uses_shared_cpp_helper(main_cpp: str) -> None:
|
||||
"""The out-of-range lookup (and its log line) live in a single C++ helper --
|
||||
lvgl::lv_list_get_row_for_remove() in lvgl_esphome.cpp -- rather than being
|
||||
generated inline at every lvgl.list.remove call site, since a config can
|
||||
have many of them and duplicating that logic (and its log string) at each
|
||||
one would waste flash for no benefit.
|
||||
"""
|
||||
assert (
|
||||
"lv_obj_t *list_child_VAR_ = lvgl::lv_list_get_row_for_remove(test_list, list_index_VAR_);"
|
||||
in main_cpp
|
||||
)
|
||||
assert "ESP_LOGV" not in main_cpp
|
||||
|
||||
|
||||
def test_clear_fires_on_remove_for_every_entry_then_cleans(main_cpp: str) -> None:
|
||||
assert (
|
||||
"for (int list_index = (int) (lv_obj_get_child_count(test_list)) - 1; "
|
||||
"list_index >= 0; list_index--) {\n"
|
||||
" triggerint_id_2->trigger(list_index);\n"
|
||||
" }\n"
|
||||
" lv_obj_clean(test_list);"
|
||||
) in main_cpp
|
||||
@@ -0,0 +1,47 @@
|
||||
"""Regression test: on_add:/on_remove: containing an lvgl action must not deadlock.
|
||||
|
||||
ListType.to_code() used to build the on_add/on_remove automations directly, during
|
||||
widget creation. Every lvgl action's to_code awaits wait_for_widgets(), which only
|
||||
resolves once *all* widgets - including the list itself - have finished being
|
||||
created. Building an automation containing an lvgl action from inside that same
|
||||
widget-creation walk therefore could never complete: codegen deadlocked with
|
||||
"Circular dependency detected!". Fixed by deferring the actual build_automation()
|
||||
call to finish_list_triggers(), run after set_widgets_completed(True) - and,
|
||||
critically, before generate_triggers(), which is what processes other widgets'
|
||||
on_click etc. automations that might reference this list (e.g. via lvgl.list.add),
|
||||
and which therefore need the list's own on_add/on_remove triggers to already exist.
|
||||
"""
|
||||
|
||||
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:
|
||||
config_path = (
|
||||
Path(request.fspath).parent / "config" / "list_on_add_lvgl_action_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_main_section
|
||||
finally:
|
||||
CORE.config_path = original_path
|
||||
CORE.reset()
|
||||
|
||||
|
||||
def test_on_add_with_lvgl_action_does_not_deadlock(main_cpp: str) -> None:
|
||||
assert 'lv_label_set_text(later_label, "changed");' in main_cpp
|
||||
|
||||
|
||||
def test_on_remove_with_lvgl_action_does_not_deadlock(main_cpp: str) -> None:
|
||||
assert 'lv_label_set_text(later_label, "removed");' in main_cpp
|
||||
@@ -0,0 +1,84 @@
|
||||
"""Regression test for lvgl.list.add called from outside the lvgl: block.
|
||||
|
||||
lv_list.py's list_add_to_code() must call _register_lv_uses() and
|
||||
_register_dynamic_widget_style_uses() before its first await (get_widgets(),
|
||||
which can block until the target list is defined) -- for an action referenced
|
||||
outside the lvgl: block, that wait can outlast lvgl's own to_code, which reads
|
||||
get_lv_uses()/get_styles_used() and flushes everything they drive (USE_LVGL_*
|
||||
defines, plus add_lv_use(image)/screen-transparency/A8-draw-support triggered
|
||||
by style properties) just once, near the end of its run. Every existing
|
||||
list_test.yaml call site lives inside lvgl: widgets:, so neither ordering
|
||||
requirement had any coverage.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome.__main__ import generate_cpp_contents
|
||||
from esphome.config import read_config
|
||||
from esphome.core import CORE
|
||||
|
||||
|
||||
@dataclass
|
||||
class GeneratedOutput:
|
||||
main_cpp: str
|
||||
define_names: set[str]
|
||||
lv_define_names: set[str]
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def generated(request: pytest.FixtureRequest) -> GeneratedOutput:
|
||||
config_path = (
|
||||
Path(request.fspath).parent / "config" / "list_outside_block_test.yaml"
|
||||
)
|
||||
original_path = CORE.config_path
|
||||
try:
|
||||
CORE.config_path = config_path
|
||||
CORE.config = read_config({})
|
||||
generate_cpp_contents(CORE.config)
|
||||
# Copy out before CORE.reset() below clears these out from under us.
|
||||
from esphome.components.lvgl import defines as df
|
||||
|
||||
return GeneratedOutput(
|
||||
main_cpp=CORE.cpp_global_section + CORE.cpp_main_section,
|
||||
define_names={d.name for d in CORE.defines},
|
||||
lv_define_names=set(df.get_defines()),
|
||||
)
|
||||
finally:
|
||||
CORE.config_path = original_path
|
||||
CORE.reset()
|
||||
|
||||
|
||||
def test_dynamic_widget_creates_correctly(generated: GeneratedOutput) -> None:
|
||||
assert (
|
||||
"lv_obj_t *dyn_switch_VAR_ = lv_switch_create(test_list);" in generated.main_cpp
|
||||
)
|
||||
|
||||
|
||||
def test_dynamic_widget_type_use_define_is_registered(
|
||||
generated: GeneratedOutput,
|
||||
) -> None:
|
||||
"""The switch type is only ever referenced via the on_boot lvgl.list.add call
|
||||
(never declared as a static widget), so USE_LVGL_SWITCH can only be present
|
||||
if _register_lv_uses() ran in time for lvgl's own to_code to flush it.
|
||||
"""
|
||||
assert "USE_LVGL_SWITCH" in generated.define_names
|
||||
assert "USE_LVGL_LIST" in generated.define_names
|
||||
|
||||
|
||||
def test_dynamic_widget_style_use_defines_are_registered(
|
||||
generated: GeneratedOutput,
|
||||
) -> None:
|
||||
"""bg_image_src/transform_rotation/drop_shadow_color are only ever set on
|
||||
the dynamically-added switch (never on a static widget), so
|
||||
USE_LVGL_IMAGE/LV_COLOR_SCREEN_TRANSP/LV_DRAW_SW_SUPPORT_A8 can only be
|
||||
present if _register_dynamic_widget_style_uses() ran in time for lvgl's own
|
||||
to_code to flush them.
|
||||
"""
|
||||
assert "USE_LVGL_IMAGE" in generated.define_names
|
||||
assert "LV_COLOR_SCREEN_TRANSP" in generated.lv_define_names
|
||||
assert "LV_DRAW_SW_SUPPORT_A8" in generated.lv_define_names
|
||||
@@ -1214,6 +1214,64 @@ lvgl:
|
||||
id: checkbox_id
|
||||
text: Checkbox
|
||||
align: bottom_right
|
||||
- list:
|
||||
id: test_list_id
|
||||
align: top_right
|
||||
width: 150px
|
||||
height: 120px
|
||||
pad_row: 4
|
||||
on_add:
|
||||
- logger.log:
|
||||
format: "list entry added at %d"
|
||||
args: [list_index]
|
||||
on_remove:
|
||||
- logger.log:
|
||||
format: "list entry removed at %d"
|
||||
args: [list_index]
|
||||
on_click:
|
||||
- lvgl.list.add_text:
|
||||
id: test_list_id
|
||||
text: !lambda return "Section";
|
||||
- lvgl.list.add_text:
|
||||
id: test_list_id
|
||||
text: "Pinned section"
|
||||
index: 0
|
||||
- lvgl.list.add:
|
||||
id: test_list_id
|
||||
button:
|
||||
text: "Entry"
|
||||
checkable: true
|
||||
- lvgl.list.add:
|
||||
id: test_list_id
|
||||
index: 1
|
||||
obj:
|
||||
widgets:
|
||||
- label:
|
||||
text: !lambda return "Dynamic row " + std::to_string(millis());
|
||||
- button:
|
||||
widgets:
|
||||
- label:
|
||||
text: "Tap"
|
||||
on_click:
|
||||
- lambda: |-
|
||||
ESP_LOGD("lvgl", "dynamic row button clicked, row %d",
|
||||
lvgl::lv_list_get_row_index(id(test_list_id), static_cast<lv_obj_t *>(lv_event_get_target(event))));
|
||||
- dropdown:
|
||||
options:
|
||||
- "One"
|
||||
- "Two"
|
||||
on_value:
|
||||
- lambda: |-
|
||||
ESP_LOGD("lvgl", "dynamic row dropdown changed, row %d",
|
||||
lvgl::lv_list_get_row_index(id(test_list_id), static_cast<lv_obj_t *>(lv_event_get_target(event))));
|
||||
- lvgl.list.remove:
|
||||
id: test_list_id
|
||||
index: 0
|
||||
- lvgl.list.clear:
|
||||
id: test_list_id
|
||||
- lvgl.list.update:
|
||||
id: test_list_id
|
||||
pad_row: 8
|
||||
- slider:
|
||||
id: slider_id
|
||||
align: top_mid
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
esphome:
|
||||
name: lvgl-list-validate
|
||||
|
||||
host:
|
||||
|
||||
logger:
|
||||
|
||||
display:
|
||||
- platform: sdl
|
||||
id: sdl0
|
||||
dimensions:
|
||||
width: 320
|
||||
height: 240
|
||||
|
||||
lvgl:
|
||||
displays: sdl0
|
||||
widgets:
|
||||
# Two independent lists, each with their own on_add/on_remove and, for list_a,
|
||||
# more than one automation under the same trigger key -- checks that the
|
||||
# per-list trigger bookkeeping is keyed correctly and doesn't require exactly
|
||||
# one automation.
|
||||
- list:
|
||||
id: validate_list_a
|
||||
align: center
|
||||
pad_row: 6
|
||||
on_add:
|
||||
- logger.log:
|
||||
format: "a: added %d"
|
||||
args: [list_index]
|
||||
- logger.log:
|
||||
format: "a: also added %d"
|
||||
args: [list_index]
|
||||
on_remove:
|
||||
- logger.log:
|
||||
format: "a: removed %d"
|
||||
args: [list_index]
|
||||
on_boot:
|
||||
# lvgl.list.add_text and lvgl.list.add both take an optional, templatable index.
|
||||
- lvgl.list.add_text:
|
||||
id: validate_list_a
|
||||
text: "Header"
|
||||
index: !lambda return 0;
|
||||
# any registered widget type is valid as the single lvgl.list.add key.
|
||||
- lvgl.list.add:
|
||||
id: validate_list_a
|
||||
checkbox:
|
||||
align: center
|
||||
text: "Option"
|
||||
- lvgl.list.add:
|
||||
id: validate_list_a
|
||||
index: !lambda return 0;
|
||||
switch:
|
||||
align: center
|
||||
- lvgl.list.add:
|
||||
id: validate_list_a
|
||||
spinner:
|
||||
align: center
|
||||
- lvgl.list.add:
|
||||
id: validate_list_a
|
||||
obj:
|
||||
align: center
|
||||
- lvgl.list.remove:
|
||||
id: validate_list_a
|
||||
index: !lambda return 0;
|
||||
- lvgl.list.clear:
|
||||
id: validate_list_a
|
||||
- list:
|
||||
id: validate_list_b
|
||||
align: center
|
||||
on_remove:
|
||||
- logger.log:
|
||||
format: "b: removed %d"
|
||||
args: [list_index]
|
||||
@@ -41,3 +41,6 @@ number:
|
||||
min_value: 2
|
||||
max_value: 100
|
||||
step: 1
|
||||
|
||||
time:
|
||||
- platform: zigbee
|
||||
|
||||
@@ -10,6 +10,3 @@ zigbee:
|
||||
on_start:
|
||||
then:
|
||||
- logger.log: "Started zigbee stack"
|
||||
|
||||
time:
|
||||
- platform: zigbee
|
||||
|
||||
@@ -5,3 +5,6 @@ zigbee:
|
||||
on_join:
|
||||
then:
|
||||
- logger.log: "Joined network"
|
||||
|
||||
time:
|
||||
- platform: zigbee
|
||||
|
||||
Reference in New Issue
Block a user