Compare commits

..

9 Commits

Author SHA1 Message Date
Samuel Sieb
d014a95674 lint 2025-04-11 20:50:32 -07:00
Samuel Sieb
b6dce21154 check if the select is configured 2025-04-11 20:48:24 -07:00
Samuel Sieb
865157afba [ld2420] only use select if available 2025-04-11 20:41:54 -07:00
Jonathan Swoboda
7edf458898 [esp32] Allow pioarduino version 5.3.3 and 5.5.0 (#8526) 2025-04-11 21:34:43 -05:00
Clyde Stubbs
d9873e24a7 [lvgl] Fix use of image without canvas (Bugfix) (#8540) 2025-04-10 01:28:44 +00:00
dependabot[bot]
645bd490ba Bump pytest-cov from 6.0.0 to 6.1.1 (#8537)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-09 10:28:22 -10:00
dependabot[bot]
27f6d00e7a Bump ruff from 0.11.2 to 0.11.4 (#8538)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-09 10:28:07 -10:00
Jesse Hills
f9d668eeca Merge branch 'beta' into dev 2025-04-09 19:50:59 +12:00
Jesse Hills
6b930595e2 Bump version to 2025.5.0-dev 2025-04-09 14:19:05 +12:00
22 changed files with 109 additions and 157 deletions

View File

@@ -311,10 +311,6 @@ APIError APINoiseFrameHelper::state_action_() {
const std::string &name = App.get_name();
const uint8_t *name_ptr = reinterpret_cast<const uint8_t *>(name.c_str());
msg.insert(msg.end(), name_ptr, name_ptr + name.size() + 1);
// node mac, terminated by null byte
const std::string &mac = get_mac_address();
const uint8_t *mac_ptr = reinterpret_cast<const uint8_t *>(mac.c_str());
msg.insert(msg.end(), mac_ptr, mac_ptr + mac.size() + 1);
aerr = write_frame_(msg.data(), msg.size());
if (aerr != APIError::OK)

View File

@@ -30,12 +30,8 @@ void AXS15231Touchscreen::setup() {
this->interrupt_pin_->setup();
this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE);
}
if (this->x_raw_max_ == 0) {
this->x_raw_max_ = this->display_->get_native_width();
}
if (this->y_raw_max_ == 0) {
this->y_raw_max_ = this->display_->get_native_height();
}
this->x_raw_max_ = this->display_->get_native_width();
this->y_raw_max_ = this->display_->get_native_height();
ESP_LOGCONFIG(TAG, "AXS15231 Touchscreen setup complete");
}
@@ -48,7 +44,7 @@ void AXS15231Touchscreen::update_touches() {
err = this->read(data, sizeof(data));
ERROR_CHECK(err);
this->status_clear_warning();
if (data[0] != 0 || data[1] == 0) // no touches
if (data[0] != 0) // no touches
return;
uint16_t x = encode_uint16(data[2] & 0xF, data[3]);
uint16_t y = encode_uint16(data[4] & 0xF, data[5]);

View File

@@ -274,8 +274,10 @@ SUPPORTED_PLATFORMIO_ESP_IDF_5X = [
# pioarduino versions that don't require a release number
# List based on https://github.com/pioarduino/esp-idf/releases
SUPPORTED_PIOARDUINO_ESP_IDF_5X = [
cv.Version(5, 5, 0),
cv.Version(5, 4, 1),
cv.Version(5, 4, 0),
cv.Version(5, 3, 3),
cv.Version(5, 3, 2),
cv.Version(5, 3, 1),
cv.Version(5, 3, 0),

View File

@@ -67,8 +67,8 @@ float LD2420Component::get_setup_priority() const { return setup_priority::BUS;
void LD2420Component::dump_config() {
ESP_LOGCONFIG(TAG, "LD2420:");
ESP_LOGCONFIG(TAG, " Firmware Version : %7s", this->ld2420_firmware_ver_);
ESP_LOGCONFIG(TAG, "LD2420 Number:");
#ifdef USE_NUMBER
ESP_LOGCONFIG(TAG, "LD2420 Number:");
LOG_NUMBER(TAG, " Gate Timeout:", this->gate_timeout_number_);
LOG_NUMBER(TAG, " Gate Max Distance:", this->max_gate_distance_number_);
LOG_NUMBER(TAG, " Gate Min Distance:", this->min_gate_distance_number_);
@@ -84,8 +84,10 @@ void LD2420Component::dump_config() {
LOG_BUTTON(TAG, " Factory Reset:", this->factory_reset_button_);
LOG_BUTTON(TAG, " Restart Module:", this->restart_module_button_);
#endif
#ifdef USE_SELECT
ESP_LOGCONFIG(TAG, "LD2420 Select:");
LOG_SELECT(TAG, " Operating Mode", this->operating_selector_);
#endif
if (this->get_firmware_int_(ld2420_firmware_ver_) < CALIBRATE_VERSION_MIN) {
ESP_LOGW(TAG, "LD2420 Firmware Version %s and older are only supported in Simple Mode", ld2420_firmware_ver_);
}
@@ -137,12 +139,18 @@ void LD2420Component::setup() {
memcpy(&this->new_config, &this->current_config, sizeof(this->current_config));
if (get_firmware_int_(ld2420_firmware_ver_) < CALIBRATE_VERSION_MIN) {
this->set_operating_mode(OP_SIMPLE_MODE_STRING);
this->operating_selector_->publish_state(OP_SIMPLE_MODE_STRING);
#ifdef USE_SELECT
if (this->operating_selector_ != nullptr)
this->operating_selector_->publish_state(OP_SIMPLE_MODE_STRING);
#endif
this->set_mode_(CMD_SYSTEM_MODE_SIMPLE);
ESP_LOGW(TAG, "LD2420 Frimware Version %s and older are only supported in Simple Mode", ld2420_firmware_ver_);
} else {
this->set_mode_(CMD_SYSTEM_MODE_ENERGY);
this->operating_selector_->publish_state(OP_NORMAL_MODE_STRING);
#ifdef USE_SELECT
if (this->operating_selector_ != nullptr)
this->operating_selector_->publish_state(OP_NORMAL_MODE_STRING);
#endif
}
#ifdef USE_NUMBER
this->init_gate_config_numbers();
@@ -293,7 +301,10 @@ void LD2420Component::set_operating_mode(const std::string &state) {
if (get_firmware_int_(ld2420_firmware_ver_) >= CALIBRATE_VERSION_MIN) {
this->current_operating_mode = OP_MODE_TO_UINT.at(state);
// Entering Auto Calibrate we need to clear the privoiuos data collection
this->operating_selector_->publish_state(state);
#ifdef USE_SELECT
if (this->operating_selector_ != nullptr)
this->operating_selector_->publish_state(state);
#endif
if (current_operating_mode == OP_CALIBRATE_MODE) {
this->set_calibration_(true);
for (uint8_t gate = 0; gate < LD2420_TOTAL_GATES; gate++) {
@@ -312,7 +323,10 @@ void LD2420Component::set_operating_mode(const std::string &state) {
}
} else {
this->current_operating_mode = OP_SIMPLE_MODE;
this->operating_selector_->publish_state(OP_SIMPLE_MODE_STRING);
#ifdef USE_SELECT
if (this->operating_selector_ != nullptr)
this->operating_selector_->publish_state(OP_SIMPLE_MODE_STRING);
#endif
}
}

View File

@@ -4,7 +4,6 @@ from esphome import automation
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ACTION, CONF_GROUP, CONF_ID, CONF_TIMEOUT
from esphome.core import Lambda
from esphome.cpp_generator import TemplateArguments, get_variable
from esphome.cpp_types import nullptr
@@ -65,14 +64,7 @@ async def action_to_code(
action_id,
template_arg,
args,
config=None,
):
# Ensure all required ids have been processed, so our LambdaContext doesn't get context-switched.
if config:
for lamb in config.values():
if isinstance(lamb, Lambda):
for id_ in lamb.requires_ids:
await get_variable(id_)
await wait_for_widgets()
async with LambdaContext(parameters=args, where=action_id) as context:
for widget in widgets:
@@ -92,9 +84,7 @@ async def update_to_code(config, action_id, template_arg, args):
lv.event_send(widget.obj, UPDATE_EVENT, nullptr)
widgets = await get_widgets(config[CONF_ID])
return await action_to_code(
widgets, do_update, action_id, template_arg, args, config
)
return await action_to_code(widgets, do_update, action_id, template_arg, args)
@automation.register_condition(
@@ -358,6 +348,4 @@ async def obj_update_to_code(config, action_id, template_arg, args):
await set_obj_properties(widget, config)
widgets = await get_widgets(config[CONF_ID])
return await action_to_code(
widgets, do_update, action_id, template_arg, args, config
)
return await action_to_code(widgets, do_update, action_id, template_arg, args)

View File

@@ -18,7 +18,6 @@ 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
from .widgets import get_widgets
ENCODERS_CONFIG = cv.ensure_list(
ENCODER_SCHEMA.extend(
@@ -77,5 +76,5 @@ async def encoders_to_code(var, config, default_group):
async def initial_focus_to_code(config):
for enc_conf in config[CONF_ENCODERS]:
if default_focus := enc_conf.get(CONF_INITIAL_FOCUS):
widget = await get_widgets(default_focus)
lv.group_focus_obj(widget[0].obj)
obj = await cg.get_variable(default_focus)
lv.group_focus_obj(obj)

View File

@@ -173,8 +173,7 @@ class LambdaContext(CodeContext):
class LvContext(LambdaContext):
"""
Code generation into the LVGL initialisation code, called before setup() and loop()
Basically just does cg.add, so now fairly redundant.
Code generation into the LVGL initialisation code (called in `setup()`)
"""
added_lambda_count = 0

View File

@@ -1,7 +1,6 @@
import esphome.codegen as cg
from esphome.components import number
import esphome.config_validation as cv
from esphome.const import CONF_RESTORE_VALUE
from esphome.cpp_generator import MockObj
from ..defines import CONF_ANIMATED, CONF_UPDATE_ON_RELEASE, CONF_WIDGET
@@ -11,21 +10,21 @@ from ..lvcode import (
EVENT_ARG,
UPDATE_EVENT,
LambdaContext,
ReturnStatement,
LvContext,
lv,
lv_add,
lvgl_static,
)
from ..types import LV_EVENT, LvNumber, lvgl_ns
from ..widgets import get_widgets, wait_for_widgets
LVGLNumber = lvgl_ns.class_("LVGLNumber", number.Number, cg.Component)
LVGLNumber = lvgl_ns.class_("LVGLNumber", number.Number)
CONFIG_SCHEMA = number.number_schema(LVGLNumber).extend(
{
cv.Required(CONF_WIDGET): cv.use_id(LvNumber),
cv.Optional(CONF_ANIMATED, default=True): animated,
cv.Optional(CONF_UPDATE_ON_RELEASE, default=False): cv.boolean,
cv.Optional(CONF_RESTORE_VALUE, default=False): cv.boolean,
}
)
@@ -33,34 +32,32 @@ CONFIG_SCHEMA = number.number_schema(LVGLNumber).extend(
async def to_code(config):
widget = await get_widgets(config, CONF_WIDGET)
widget = widget[0]
var = await number.new_number(
config,
max_value=widget.get_max(),
min_value=widget.get_min(),
step=widget.get_step(),
)
await wait_for_widgets()
async with LambdaContext([], return_type=cg.float_) as value:
value.add(ReturnStatement(widget.get_value()))
async with LambdaContext([(cg.float_, "v")]) as control:
await widget.set_property(
"value", MockObj("v") * MockObj(widget.get_scale()), config[CONF_ANIMATED]
)
lv.event_send(widget.obj, API_EVENT, cg.nullptr)
control.add(var.publish_state(widget.get_value()))
async with LambdaContext(EVENT_ARG) as event:
event.add(var.publish_state(widget.get_value()))
event_code = (
LV_EVENT.VALUE_CHANGED
if not config[CONF_UPDATE_ON_RELEASE]
else LV_EVENT.RELEASED
)
var = await number.new_number(
config,
await control.get_lambda(),
await value.get_lambda(),
event_code,
config[CONF_RESTORE_VALUE],
max_value=widget.get_max(),
min_value=widget.get_min(),
step=widget.get_step(),
)
async with LambdaContext(EVENT_ARG) as event:
event.add(var.on_value())
await cg.register_component(var, config)
cg.add(
lvgl_static.add_event_cb(
widget.obj, await event.get_lambda(), UPDATE_EVENT, event_code
async with LvContext():
lv_add(var.set_control_lambda(await control.get_lambda()))
lv_add(
lvgl_static.add_event_cb(
widget.obj, await event.get_lambda(), UPDATE_EVENT, event_code
)
)
)
lv_add(var.publish_state(widget.get_value()))

View File

@@ -3,46 +3,33 @@
#include <utility>
#include "esphome/components/number/number.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/preferences.h"
namespace esphome {
namespace lvgl {
class LVGLNumber : public number::Number, public Component {
class LVGLNumber : public number::Number {
public:
LVGLNumber(std::function<void(float)> control_lambda, std::function<float()> value_lambda, lv_event_code_t event,
bool restore)
: control_lambda_(std::move(control_lambda)),
value_lambda_(std::move(value_lambda)),
event_(event),
restore_(restore) {}
void setup() override {
float value = this->value_lambda_();
if (this->restore_) {
this->pref_ = global_preferences->make_preference<float>(this->get_object_id_hash());
if (this->pref_.load(&value)) {
this->control_lambda_(value);
}
void set_control_lambda(std::function<void(float)> control_lambda) {
this->control_lambda_ = std::move(control_lambda);
if (this->initial_state_.has_value()) {
this->control_lambda_(this->initial_state_.value());
this->initial_state_.reset();
}
this->publish_state(value);
}
void on_value() { this->publish_state(this->value_lambda_()); }
protected:
void control(float value) override {
this->control_lambda_(value);
this->publish_state(value);
if (this->restore_)
this->pref_.save(&value);
if (this->control_lambda_ != nullptr) {
this->control_lambda_(value);
} else {
this->initial_state_ = value;
}
}
std::function<void(float)> control_lambda_;
std::function<float()> value_lambda_;
lv_event_code_t event_;
bool restore_;
ESPPreferenceObject pref_{};
std::function<void(float)> control_lambda_{};
optional<float> initial_state_{};
};
} // namespace lvgl

View File

@@ -81,9 +81,7 @@ ENCODER_SCHEMA = cv.Schema(
cv.declare_id(LVEncoderListener), requires_component("binary_sensor")
),
cv.Optional(CONF_GROUP): cv.declare_id(lv_group_t),
cv.Optional(df.CONF_INITIAL_FOCUS): cv.All(
LIST_ACTION_SCHEMA, cv.Length(min=1, max=1)
),
cv.Optional(df.CONF_INITIAL_FOCUS): cv.use_id(lv_obj_t),
cv.Optional(df.CONF_LONG_PRESS_TIME, default="400ms"): PRESS_TIME,
cv.Optional(df.CONF_LONG_PRESS_REPEAT_TIME, default="100ms"): PRESS_TIME,
}

View File

@@ -1,19 +1,18 @@
import esphome.codegen as cg
from esphome.components import select
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_OPTIONS, CONF_RESTORE_VALUE
from esphome.const import CONF_OPTIONS
from ..defines import CONF_ANIMATED, CONF_WIDGET, literal
from ..lvcode import LvContext
from ..types import LvSelect, lvgl_ns
from ..widgets import get_widgets
from ..widgets import get_widgets, wait_for_widgets
LVGLSelect = lvgl_ns.class_("LVGLSelect", select.Select, cg.Component)
LVGLSelect = lvgl_ns.class_("LVGLSelect", select.Select)
CONFIG_SCHEMA = select.select_schema(LVGLSelect).extend(
{
cv.Required(CONF_WIDGET): cv.use_id(LvSelect),
cv.Optional(CONF_ANIMATED, default=False): cv.boolean,
cv.Optional(CONF_RESTORE_VALUE, default=False): cv.boolean,
}
)
@@ -22,9 +21,12 @@ async def to_code(config):
widget = await get_widgets(config, CONF_WIDGET)
widget = widget[0]
options = widget.config.get(CONF_OPTIONS, [])
animated = literal("LV_ANIM_ON" if config[CONF_ANIMATED] else "LV_ANIM_OFF")
selector = cg.new_Pvariable(
config[CONF_ID], widget.var, animated, config[CONF_RESTORE_VALUE]
)
await select.register_select(selector, config, options=options)
await cg.register_component(selector, config)
selector = await select.new_select(config, options=options)
await wait_for_widgets()
async with LvContext() as ctx:
ctx.add(
selector.set_widget(
widget.var,
literal("LV_ANIM_ON" if config[CONF_ANIMATED] else "LV_ANIM_OFF"),
)
)

View File

@@ -11,20 +11,12 @@
namespace esphome {
namespace lvgl {
class LVGLSelect : public select::Select, public Component {
class LVGLSelect : public select::Select {
public:
LVGLSelect(LvSelectable *widget, lv_anim_enable_t anim, bool restore)
: widget_(widget), anim_(anim), restore_(restore) {}
void setup() override {
void set_widget(LvSelectable *widget, lv_anim_enable_t anim = LV_ANIM_OFF) {
this->widget_ = widget;
this->anim_ = anim;
this->set_options_();
if (this->restore_) {
size_t index;
this->pref_ = global_preferences->make_preference<size_t>(this->get_object_id_hash());
if (this->pref_.load(&index))
this->widget_->set_selected_index(index, LV_ANIM_OFF);
}
this->publish();
lv_obj_add_event_cb(
this->widget_->obj,
[](lv_event_t *e) {
@@ -32,6 +24,11 @@ class LVGLSelect : public select::Select, public Component {
it->set_options_();
},
LV_EVENT_REFRESH, this);
if (this->initial_state_.has_value()) {
this->control(this->initial_state_.value());
this->initial_state_.reset();
}
this->publish();
auto lamb = [](lv_event_t *e) {
auto *self = static_cast<LVGLSelect *>(e->user_data);
self->publish();
@@ -40,25 +37,21 @@ class LVGLSelect : public select::Select, public Component {
lv_obj_add_event_cb(this->widget_->obj, lamb, lv_update_event, this);
}
void publish() {
this->publish_state(this->widget_->get_selected_text());
if (this->restore_) {
auto index = this->widget_->get_selected_index();
this->pref_.save(&index);
}
}
void publish() { this->publish_state(this->widget_->get_selected_text()); }
protected:
void control(const std::string &value) override {
this->widget_->set_selected_text(value, this->anim_);
this->publish();
if (this->widget_ != nullptr) {
this->widget_->set_selected_text(value, this->anim_);
} else {
this->initial_state_ = value;
}
}
void set_options_() { this->traits.set_options(this->widget_->get_options()); }
LvSelectable *widget_;
lv_anim_enable_t anim_;
bool restore_;
ESPPreferenceObject pref_{};
LvSelectable *widget_{};
optional<std::string> initial_state_{};
lv_anim_enable_t anim_{LV_ANIM_OFF};
};
} // namespace lvgl

View File

@@ -250,7 +250,7 @@ async def button_update_to_code(config, action_id, template_arg, args):
widgets = await get_widgets(config[CONF_ID])
assert all(isinstance(w, MatrixButton) for w in widgets)
async def do_button_update(w):
async def do_button_update(w: MatrixButton):
if (width := config.get(CONF_WIDTH)) is not None:
lv.btnmatrix_set_btn_width(w.obj, w.index, width)
if config.get(CONF_SELECTED):
@@ -275,5 +275,5 @@ async def button_update_to_code(config, action_id, template_arg, args):
)
return await action_to_code(
widgets, do_button_update, action_id, template_arg, args, config
widgets, do_button_update, action_id, template_arg, args
)

View File

@@ -97,7 +97,7 @@ async def canvas_fill(config, action_id, template_arg, args):
async def do_fill(w: Widget):
lv.canvas_fill_bg(w.obj, color, opa)
return await action_to_code(widget, do_fill, action_id, template_arg, args, config)
return await action_to_code(widget, do_fill, action_id, template_arg, args)
@automation.register_action(
@@ -145,9 +145,7 @@ async def canvas_set_pixel(config, action_id, template_arg, args):
x, y = point
lv.canvas_set_px_opa(w.obj, x, y, opa_var)
return await action_to_code(
widget, do_set_pixels, action_id, template_arg, args, config
)
return await action_to_code(widget, do_set_pixels, action_id, template_arg, args)
DRAW_SCHEMA = cv.Schema(
@@ -183,9 +181,7 @@ async def draw_to_code(config, dsc_type, props, do_draw, action_id, template_arg
lv_assign(getattr(dsc, mapped_prop), value)
await do_draw(w, x, y, dsc_addr)
return await action_to_code(
widget, action_func, action_id, template_arg, args, config
)
return await action_to_code(widget, action_func, action_id, template_arg, args)
RECT_PROPS = {

View File

@@ -297,9 +297,7 @@ async def indicator_update_to_code(config, action_id, template_arg, args):
async def set_value(w: Widget):
await set_indicator_values(w.var, w.obj, config)
return await action_to_code(
widget, set_value, action_id, template_arg, args, config
)
return await action_to_code(widget, set_value, action_id, template_arg, args)
async def set_indicator_values(meter, indicator, config):

View File

@@ -441,10 +441,9 @@ void AudioPipeline::decode_task(void *params) {
pdFALSE, // Wait for all the bits,
portMAX_DELAY); // Block indefinitely until bit is set
xEventGroupClearBits(this_pipeline->event_group_,
EventGroupBits::DECODER_MESSAGE_FINISHED | EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE);
if (!(event_bits & EventGroupBits::PIPELINE_COMMAND_STOP)) {
xEventGroupClearBits(this_pipeline->event_group_,
EventGroupBits::DECODER_MESSAGE_FINISHED | EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE);
InfoErrorEvent event;
event.source = InfoErrorSource::DECODER;

View File

@@ -1,6 +1,6 @@
"""Constants used by esphome."""
__version__ = "2025.4.0b2"
__version__ = "2025.5.0-dev"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = (

View File

@@ -12,9 +12,9 @@ pyserial==3.5
platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
esptool==4.8.1
click==8.1.7
esphome-dashboard==20250415.0
aioesphomeapi==29.10.0
zeroconf==0.146.4
esphome-dashboard==20250212.0
aioesphomeapi==29.9.0
zeroconf==0.146.3
puremagic==1.28
ruamel.yaml==0.18.10 # dashboard_import
esphome-glyphsets==0.2.0

View File

@@ -1,12 +1,12 @@
pylint==3.3.6
flake8==7.2.0 # also change in .pre-commit-config.yaml when updating
ruff==0.11.2 # also change in .pre-commit-config.yaml when updating
ruff==0.11.4 # also change in .pre-commit-config.yaml when updating
pyupgrade==3.19.1 # also change in .pre-commit-config.yaml when updating
pre-commit
# Unit tests
pytest==8.3.5
pytest-cov==6.0.0
pytest-cov==6.1.1
pytest-mock==3.14.0
pytest-asyncio==0.26.0
asyncmock==0.4.2

View File

@@ -38,7 +38,6 @@ number:
widget: slider_id
name: LVGL Slider
update_on_release: true
restore_value: true
- platform: lvgl
widget: lv_arc
id: lvgl_arc_number

View File

@@ -990,13 +990,3 @@ color:
green_int: 123
blue_int: 64
white_int: 255
select:
- platform: lvgl
id: lv_roller_select
widget: lv_roller
restore_value: true
- platform: lvgl
id: lv_dropdown_select
widget: lv_dropdown
restore_value: false

View File

@@ -71,6 +71,5 @@ lvgl:
sensor: encoder
enter_button: pushbutton
group: general
initial_focus: lv_roller
<<: !include common.yaml