Implement a simple LCD menu (#3406)

Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
Stanislav Meduna 2022-11-07 07:00:55 +01:00 committed by GitHub
parent 90683223dd
commit 2c76381fcd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 1611 additions and 0 deletions

View File

@ -65,6 +65,7 @@ esphome/components/debug/* @OttoWinter
esphome/components/delonghi/* @grob6000
esphome/components/dfplayer/* @glmnet
esphome/components/dht/* @OttoWinter
esphome/components/display_menu_base/* @numo68
esphome/components/dps310/* @kbx81
esphome/components/ds1307/* @badbadc0ffee
esphome/components/dsmr/* @glmnet @zuidwijk
@ -110,6 +111,7 @@ esphome/components/integration/* @OttoWinter
esphome/components/interval/* @esphome/core
esphome/components/json/* @OttoWinter
esphome/components/kalman_combinator/* @Cat-Ion
esphome/components/lcd_menu/* @numo68
esphome/components/ledc/* @OttoWinter
esphome/components/light/* @esphome/core
esphome/components/lilygo_t5_47/touchscreen/* @jesserockz

View File

@ -0,0 +1,430 @@
import re
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import automation, core
from esphome.const import (
CONF_ID,
CONF_TYPE,
CONF_TRIGGER_ID,
CONF_ON_VALUE,
CONF_COMMAND,
CONF_NUMBER,
CONF_FORMAT,
CONF_MODE,
CONF_ACTIVE,
)
from esphome.automation import maybe_simple_id
from esphome.components.select import Select
from esphome.components.number import Number
from esphome.components.switch import Switch
CODEOWNERS = ["@numo68"]
display_menu_base_ns = cg.esphome_ns.namespace("display_menu_base")
CONF_DISPLAY_ID = "display_id"
CONF_ROTARY = "rotary"
CONF_JOYSTICK = "joystick"
CONF_LABEL = "label"
CONF_MENU = "menu"
CONF_BACK = "back"
CONF_TEXT = "text"
CONF_SELECT = "select"
CONF_SWITCH = "switch"
CONF_CUSTOM = "custom"
CONF_ITEMS = "items"
CONF_ON_TEXT = "on_text"
CONF_OFF_TEXT = "off_text"
CONF_VALUE_LAMBDA = "value_lambda"
CONF_IMMEDIATE_EDIT = "immediate_edit"
CONF_ROOT_ITEM_ID = "root_item_id"
CONF_ON_ENTER = "on_enter"
CONF_ON_LEAVE = "on_leave"
CONF_ON_NEXT = "on_next"
CONF_ON_PREV = "on_prev"
DisplayMenuComponent = display_menu_base_ns.class_("DisplayMenuComponent", cg.Component)
MenuItem = display_menu_base_ns.class_("MenuItem")
MenuItemConstPtr = MenuItem.operator("ptr").operator("const")
MenuItemMenu = display_menu_base_ns.class_("MenuItemMenu")
MenuItemSelect = display_menu_base_ns.class_("MenuItemSelect")
MenuItemNumber = display_menu_base_ns.class_("MenuItemNumber")
MenuItemSwitch = display_menu_base_ns.class_("MenuItemSwitch")
MenuItemCommand = display_menu_base_ns.class_("MenuItemCommand")
MenuItemCustom = display_menu_base_ns.class_("MenuItemCustom")
UpAction = display_menu_base_ns.class_("UpAction", automation.Action)
DownAction = display_menu_base_ns.class_("DownAction", automation.Action)
LeftAction = display_menu_base_ns.class_("LeftAction", automation.Action)
RightAction = display_menu_base_ns.class_("RightAction", automation.Action)
EnterAction = display_menu_base_ns.class_("EnterAction", automation.Action)
ShowAction = display_menu_base_ns.class_("ShowAction", automation.Action)
HideAction = display_menu_base_ns.class_("HideAction", automation.Action)
ShowMainAction = display_menu_base_ns.class_("ShowMainAction", automation.Action)
IsActiveCondition = display_menu_base_ns.class_(
"IsActiveCondition", automation.Condition
)
MULTI_CONF = True
MenuItemType = display_menu_base_ns.enum("MenuItemType")
MENU_ITEM_TYPES = {
CONF_LABEL: MenuItemType.MENU_ITEM_LABEL,
CONF_MENU: MenuItemType.MENU_ITEM_MENU,
CONF_BACK: MenuItemType.MENU_ITEM_BACK,
CONF_SELECT: MenuItemType.MENU_ITEM_SELECT,
CONF_NUMBER: MenuItemType.MENU_ITEM_NUMBER,
CONF_SWITCH: MenuItemType.MENU_ITEM_SWITCH,
CONF_COMMAND: MenuItemType.MENU_ITEM_COMMAND,
CONF_CUSTOM: MenuItemType.MENU_ITEM_CUSTOM,
}
MENU_ITEMS_WITH_SPECIALIZED_CLASSES = (
CONF_MENU,
CONF_SELECT,
CONF_NUMBER,
CONF_SWITCH,
CONF_COMMAND,
CONF_CUSTOM,
)
MenuMode = display_menu_base_ns.enum("MenuMode")
MENU_MODES = {
CONF_ROTARY: MenuMode.MENU_MODE_ROTARY,
CONF_JOYSTICK: MenuMode.MENU_MODE_JOYSTICK,
}
DisplayMenuOnEnterTrigger = display_menu_base_ns.class_(
"DisplayMenuOnEnterTrigger", automation.Trigger
)
DisplayMenuOnLeaveTrigger = display_menu_base_ns.class_(
"DisplayMenuOnLeaveTrigger", automation.Trigger
)
DisplayMenuOnValueTrigger = display_menu_base_ns.class_(
"DisplayMenuOnValueTrigger", automation.Trigger
)
DisplayMenuOnNextTrigger = display_menu_base_ns.class_(
"DisplayMenuOnNextTrigger", automation.Trigger
)
DisplayMenuOnPrevTrigger = display_menu_base_ns.class_(
"DisplayMenuOnPrevTrigger", automation.Trigger
)
def validate_format(format):
if re.search(r"^%([+-])*(\d+)*(\.\d+)*[fg]$", format) is None:
raise cv.Invalid(
f"{CONF_FORMAT}: has to specify a printf-like format string specifying exactly one f or g type conversion, '{format}' provided"
)
return format
# Use a simple indirection to circumvent the recursion limitation
def menu_item_schema(value):
return MENU_ITEM_SCHEMA(value)
MENU_ITEM_COMMON_SCHEMA = cv.Schema(
{
cv.Optional(CONF_TEXT): cv.templatable(cv.string),
}
)
MENU_ITEM_ENTER_LEAVE_SCHEMA = MENU_ITEM_COMMON_SCHEMA.extend(
{
cv.Optional(CONF_ON_ENTER): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
DisplayMenuOnEnterTrigger
),
}
),
cv.Optional(CONF_ON_LEAVE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
DisplayMenuOnLeaveTrigger
),
}
),
}
)
MENU_ITEM_VALUE_SCHEMA = MENU_ITEM_COMMON_SCHEMA.extend(
{
cv.Optional(CONF_ON_VALUE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
DisplayMenuOnValueTrigger
),
}
),
}
)
MENU_ITEM_ENTER_LEAVE_VALUE_SCHEMA = MENU_ITEM_ENTER_LEAVE_SCHEMA.extend(
{
cv.Optional(CONF_ON_VALUE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
DisplayMenuOnValueTrigger
),
}
),
}
)
MENU_ITEM_SCHEMA = cv.typed_schema(
{
CONF_LABEL: MENU_ITEM_COMMON_SCHEMA.extend(
{
cv.GenerateID(CONF_ID): cv.declare_id(MenuItem),
}
),
CONF_BACK: MENU_ITEM_COMMON_SCHEMA.extend(
{
cv.GenerateID(CONF_ID): cv.declare_id(MenuItem),
}
),
CONF_MENU: MENU_ITEM_ENTER_LEAVE_SCHEMA.extend(
{
cv.GenerateID(CONF_ID): cv.declare_id(MenuItemMenu),
cv.Required(CONF_ITEMS): cv.All(
cv.ensure_list(menu_item_schema), cv.Length(min=1)
),
}
),
CONF_SELECT: MENU_ITEM_ENTER_LEAVE_VALUE_SCHEMA.extend(
{
cv.GenerateID(CONF_ID): cv.declare_id(MenuItemSelect),
cv.Required(CONF_SELECT): cv.use_id(Select),
cv.Optional(CONF_IMMEDIATE_EDIT, default=False): cv.boolean,
cv.Optional(CONF_VALUE_LAMBDA): cv.returning_lambda,
}
),
CONF_NUMBER: MENU_ITEM_ENTER_LEAVE_VALUE_SCHEMA.extend(
{
cv.GenerateID(CONF_ID): cv.declare_id(MenuItemNumber),
cv.Required(CONF_NUMBER): cv.use_id(Number),
cv.Optional(CONF_IMMEDIATE_EDIT, default=False): cv.boolean,
cv.Optional(CONF_FORMAT, default="%.1f"): cv.All(
cv.string_strict,
validate_format,
),
cv.Optional(CONF_VALUE_LAMBDA): cv.returning_lambda,
}
),
CONF_SWITCH: MENU_ITEM_ENTER_LEAVE_VALUE_SCHEMA.extend(
{
cv.GenerateID(CONF_ID): cv.declare_id(MenuItemSwitch),
cv.Required(CONF_SWITCH): cv.use_id(Switch),
cv.Optional(CONF_IMMEDIATE_EDIT, default=False): cv.boolean,
cv.Optional(CONF_ON_TEXT, default="On"): cv.string_strict,
cv.Optional(CONF_OFF_TEXT, default="Off"): cv.string_strict,
cv.Optional(CONF_VALUE_LAMBDA): cv.returning_lambda,
}
),
CONF_COMMAND: MENU_ITEM_VALUE_SCHEMA.extend(
{
cv.GenerateID(CONF_ID): cv.declare_id(MenuItemCommand),
}
),
CONF_CUSTOM: MENU_ITEM_ENTER_LEAVE_VALUE_SCHEMA.extend(
{
cv.GenerateID(CONF_ID): cv.declare_id(MenuItemCustom),
cv.Optional(CONF_IMMEDIATE_EDIT, default=False): cv.boolean,
cv.Optional(CONF_VALUE_LAMBDA): cv.returning_lambda,
cv.Optional(CONF_ON_NEXT): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
DisplayMenuOnNextTrigger
),
}
),
cv.Optional(CONF_ON_PREV): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
DisplayMenuOnPrevTrigger
),
}
),
}
),
},
default_type="label",
lower=True,
)
DISPLAY_MENU_BASE_SCHEMA = cv.Schema(
{
cv.Optional(CONF_ACTIVE, default=True): cv.boolean,
cv.GenerateID(CONF_ROOT_ITEM_ID): cv.declare_id(MenuItemMenu),
cv.Optional(CONF_MODE, default=CONF_ROTARY): cv.enum(MENU_MODES),
cv.Optional(CONF_ON_ENTER): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
DisplayMenuOnEnterTrigger
),
}
),
cv.Optional(CONF_ON_LEAVE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
DisplayMenuOnLeaveTrigger
),
}
),
cv.Required(CONF_ITEMS): cv.All(
cv.ensure_list(MENU_ITEM_SCHEMA), cv.Length(min=1)
),
}
).extend(cv.COMPONENT_SCHEMA)
MENU_ACTION_SCHEMA = maybe_simple_id(
{
cv.GenerateID(CONF_ID): cv.use_id(DisplayMenuComponent),
}
)
@automation.register_action("display_menu.up", UpAction, MENU_ACTION_SCHEMA)
async def menu_up_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action("display_menu.down", DownAction, MENU_ACTION_SCHEMA)
async def menu_down_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action("display_menu.left", LeftAction, MENU_ACTION_SCHEMA)
async def menu_left_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action("display_menu.right", RightAction, MENU_ACTION_SCHEMA)
async def menu_right_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action("display_menu.enter", EnterAction, MENU_ACTION_SCHEMA)
async def menu_enter_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action("display_menu.show", ShowAction, MENU_ACTION_SCHEMA)
async def menu_show_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action("display_menu.hide", HideAction, MENU_ACTION_SCHEMA)
async def menu_hide_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action(
"display_menu.show_main", ShowMainAction, MENU_ACTION_SCHEMA
)
async def menu_show_main_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_condition(
"display_menu.is_active",
IsActiveCondition,
automation.maybe_simple_id(
{
cv.GenerateID(CONF_ID): cv.use_id(DisplayMenuComponent),
}
),
)
async def display_menu_is_active_to_code(config, condition_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(condition_id, template_arg, paren)
async def menu_item_to_code(menu, config, parent):
if config[CONF_TYPE] in MENU_ITEMS_WITH_SPECIALIZED_CLASSES:
item = cg.new_Pvariable(config[CONF_ID])
else:
item = cg.new_Pvariable(config[CONF_ID], MENU_ITEM_TYPES[config[CONF_TYPE]])
cg.add(parent.add_item(item))
if CONF_TEXT in config:
if isinstance(config[CONF_TEXT], core.Lambda):
template_ = await cg.templatable(
config[CONF_TEXT], [(MenuItemConstPtr, "it")], cg.std_string
)
cg.add(item.set_text(template_))
else:
cg.add(item.set_text(config[CONF_TEXT]))
if CONF_VALUE_LAMBDA in config:
template_ = await cg.process_lambda(
config[CONF_VALUE_LAMBDA],
[(MenuItemConstPtr, "it")],
return_type=cg.std_string,
)
cg.add(item.set_value_lambda(template_))
if CONF_ITEMS in config:
for c in config[CONF_ITEMS]:
await menu_item_to_code(menu, c, item)
if CONF_IMMEDIATE_EDIT in config:
cg.add(item.set_immediate_edit(config[CONF_IMMEDIATE_EDIT]))
if config[CONF_TYPE] == CONF_SELECT:
var = await cg.get_variable(config[CONF_SELECT])
cg.add(item.set_select_variable(var))
if config[CONF_TYPE] == CONF_NUMBER:
var = await cg.get_variable(config[CONF_NUMBER])
cg.add(item.set_number_variable(var))
cg.add(item.set_format(config[CONF_FORMAT]))
if config[CONF_TYPE] == CONF_SWITCH:
var = await cg.get_variable(config[CONF_SWITCH])
cg.add(item.set_switch_variable(var))
cg.add(item.set_on_text(config[CONF_ON_TEXT]))
cg.add(item.set_off_text(config[CONF_OFF_TEXT]))
for conf in config.get(CONF_ON_ENTER, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], item)
await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf)
for conf in config.get(CONF_ON_LEAVE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], item)
await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf)
for conf in config.get(CONF_ON_VALUE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], item)
await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf)
for conf in config.get(CONF_ON_NEXT, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], item)
await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf)
for conf in config.get(CONF_ON_PREV, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], item)
await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf)
async def display_menu_to_code(menu, config):
cg.add(menu.set_active(config[CONF_ACTIVE]))
root_item = cg.new_Pvariable(config[CONF_ROOT_ITEM_ID])
cg.add(menu.set_root_item(root_item))
cg.add(menu.set_mode(config[CONF_MODE]))
for c in config[CONF_ITEMS]:
await menu_item_to_code(menu, c, root_item)
for conf in config.get(CONF_ON_ENTER, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], root_item)
await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf)
for conf in config.get(CONF_ON_LEAVE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], root_item)
await automation.build_automation(trigger, [(MenuItemConstPtr, "it")], conf)

View File

@ -0,0 +1,133 @@
#pragma once
#include "esphome/core/automation.h"
#include "display_menu_base.h"
namespace esphome {
namespace display_menu_base {
template<typename... Ts> class UpAction : public Action<Ts...> {
public:
explicit UpAction(DisplayMenuComponent *menu) : menu_(menu) {}
void play(Ts... x) override { this->menu_->up(); }
protected:
DisplayMenuComponent *menu_;
};
template<typename... Ts> class DownAction : public Action<Ts...> {
public:
explicit DownAction(DisplayMenuComponent *menu) : menu_(menu) {}
void play(Ts... x) override { this->menu_->down(); }
protected:
DisplayMenuComponent *menu_;
};
template<typename... Ts> class LeftAction : public Action<Ts...> {
public:
explicit LeftAction(DisplayMenuComponent *menu) : menu_(menu) {}
void play(Ts... x) override { this->menu_->left(); }
protected:
DisplayMenuComponent *menu_;
};
template<typename... Ts> class RightAction : public Action<Ts...> {
public:
explicit RightAction(DisplayMenuComponent *menu) : menu_(menu) {}
void play(Ts... x) override { this->menu_->right(); }
protected:
DisplayMenuComponent *menu_;
};
template<typename... Ts> class EnterAction : public Action<Ts...> {
public:
explicit EnterAction(DisplayMenuComponent *menu) : menu_(menu) {}
void play(Ts... x) override { this->menu_->enter(); }
protected:
DisplayMenuComponent *menu_;
};
template<typename... Ts> class ShowAction : public Action<Ts...> {
public:
explicit ShowAction(DisplayMenuComponent *menu) : menu_(menu) {}
void play(Ts... x) override { this->menu_->show(); }
protected:
DisplayMenuComponent *menu_;
};
template<typename... Ts> class HideAction : public Action<Ts...> {
public:
explicit HideAction(DisplayMenuComponent *menu) : menu_(menu) {}
void play(Ts... x) override { this->menu_->hide(); }
protected:
DisplayMenuComponent *menu_;
};
template<typename... Ts> class ShowMainAction : public Action<Ts...> {
public:
explicit ShowMainAction(DisplayMenuComponent *menu) : menu_(menu) {}
void play(Ts... x) override { this->menu_->show_main(); }
protected:
DisplayMenuComponent *menu_;
};
template<typename... Ts> class IsActiveCondition : public Condition<Ts...> {
public:
explicit IsActiveCondition(DisplayMenuComponent *menu) : menu_(menu) {}
bool check(Ts... x) override { return this->menu_->is_active(); }
protected:
DisplayMenuComponent *menu_;
};
class DisplayMenuOnEnterTrigger : public Trigger<const MenuItem *> {
public:
explicit DisplayMenuOnEnterTrigger(MenuItem *parent) {
parent->add_on_enter_callback([this, parent]() { this->trigger(parent); });
}
};
class DisplayMenuOnLeaveTrigger : public Trigger<const MenuItem *> {
public:
explicit DisplayMenuOnLeaveTrigger(MenuItem *parent) {
parent->add_on_leave_callback([this, parent]() { this->trigger(parent); });
}
};
class DisplayMenuOnValueTrigger : public Trigger<const MenuItem *> {
public:
explicit DisplayMenuOnValueTrigger(MenuItem *parent) {
parent->add_on_value_callback([this, parent]() { this->trigger(parent); });
}
};
class DisplayMenuOnNextTrigger : public Trigger<const MenuItem *> {
public:
explicit DisplayMenuOnNextTrigger(MenuItemCustom *parent) {
parent->add_on_next_callback([this, parent]() { this->trigger(parent); });
}
};
class DisplayMenuOnPrevTrigger : public Trigger<const MenuItem *> {
public:
explicit DisplayMenuOnPrevTrigger(MenuItemCustom *parent) {
parent->add_on_prev_callback([this, parent]() { this->trigger(parent); });
}
};
} // namespace display_menu_base
} // namespace esphome

View File

@ -0,0 +1,315 @@
#include "display_menu_base.h"
#include <algorithm>
namespace esphome {
namespace display_menu_base {
void DisplayMenuComponent::up() {
if (this->check_healthy_and_active_()) {
bool changed = false;
if (this->editing_) {
switch (this->mode_) {
case MENU_MODE_ROTARY:
changed = this->get_selected_item_()->select_prev();
break;
default:
break;
}
} else {
changed = this->cursor_up_();
}
if (changed)
this->draw_and_update();
}
}
void DisplayMenuComponent::down() {
if (this->check_healthy_and_active_()) {
bool changed = false;
if (this->editing_) {
switch (this->mode_) {
case MENU_MODE_ROTARY:
changed = this->get_selected_item_()->select_next();
break;
default:
break;
}
} else {
changed = this->cursor_down_();
}
if (changed)
this->draw_and_update();
}
}
void DisplayMenuComponent::left() {
if (this->check_healthy_and_active_()) {
bool changed = false;
switch (this->get_selected_item_()->get_type()) {
case MENU_ITEM_SELECT:
case MENU_ITEM_SWITCH:
case MENU_ITEM_NUMBER:
case MENU_ITEM_CUSTOM:
switch (this->mode_) {
case MENU_MODE_ROTARY:
if (this->editing_) {
this->finish_editing_();
changed = true;
}
break;
case MENU_MODE_JOYSTICK:
if (this->editing_ || this->get_selected_item_()->get_immediate_edit())
changed = this->get_selected_item_()->select_prev();
break;
default:
break;
}
break;
case MENU_ITEM_BACK:
changed = this->leave_menu_();
break;
default:
break;
}
if (changed)
this->draw_and_update();
}
}
void DisplayMenuComponent::right() {
if (this->check_healthy_and_active_()) {
bool changed = false;
switch (this->get_selected_item_()->get_type()) {
case MENU_ITEM_SELECT:
case MENU_ITEM_SWITCH:
case MENU_ITEM_NUMBER:
case MENU_ITEM_CUSTOM:
switch (this->mode_) {
case MENU_MODE_JOYSTICK:
if (this->editing_ || this->get_selected_item_()->get_immediate_edit())
changed = this->get_selected_item_()->select_next();
default:
break;
}
break;
case MENU_ITEM_MENU:
changed = this->enter_menu_();
break;
default:
break;
}
if (changed)
this->draw_and_update();
}
}
void DisplayMenuComponent::enter() {
if (this->check_healthy_and_active_()) {
bool changed = false;
MenuItem *item = this->get_selected_item_();
if (this->editing_) {
this->finish_editing_();
changed = true;
} else {
switch (item->get_type()) {
case MENU_ITEM_MENU:
changed = this->enter_menu_();
break;
case MENU_ITEM_BACK:
changed = this->leave_menu_();
break;
case MENU_ITEM_SELECT:
case MENU_ITEM_SWITCH:
case MENU_ITEM_CUSTOM:
if (item->get_immediate_edit()) {
changed = item->select_next();
} else {
this->editing_ = true;
item->on_enter();
changed = true;
}
break;
case MENU_ITEM_NUMBER:
// A number cannot be immediate in the rotary mode
if (!item->get_immediate_edit() || this->mode_ == MENU_MODE_ROTARY) {
this->editing_ = true;
item->on_enter();
changed = true;
}
break;
case MENU_ITEM_COMMAND:
changed = item->select_next();
break;
default:
break;
}
}
if (changed)
this->draw_and_update();
}
}
void DisplayMenuComponent::draw() {
if (this->check_healthy_and_active_())
this->draw_menu();
}
void DisplayMenuComponent::show_main() {
bool disp_changed = false;
if (this->is_failed())
return;
this->process_initial_();
if (this->active_ && this->editing_)
this->finish_editing_();
if (this->displayed_item_ != this->root_item_) {
this->displayed_item_->on_leave();
disp_changed = true;
}
this->reset_();
this->active_ = true;
if (disp_changed) {
this->displayed_item_->on_enter();
}
this->draw_and_update();
}
void DisplayMenuComponent::show() {
if (this->is_failed())
return;
this->process_initial_();
if (!this->active_) {
this->active_ = true;
this->draw_and_update();
}
}
void DisplayMenuComponent::hide() {
if (this->check_healthy_and_active_()) {
if (this->editing_)
this->finish_editing_();
this->active_ = false;
this->update();
}
}
void DisplayMenuComponent::reset_() {
this->displayed_item_ = this->root_item_;
this->cursor_index_ = this->top_index_ = 0;
this->selection_stack_.clear();
}
void DisplayMenuComponent::process_initial_() {
if (!this->root_on_enter_called_) {
this->root_item_->on_enter();
this->root_on_enter_called_ = true;
}
}
bool DisplayMenuComponent::check_healthy_and_active_() {
if (this->is_failed())
return false;
this->process_initial_();
return this->active_;
}
bool DisplayMenuComponent::cursor_up_() {
bool changed = false;
if (this->cursor_index_ > 0) {
changed = true;
--this->cursor_index_;
if (this->cursor_index_ < this->top_index_)
this->top_index_ = this->cursor_index_;
}
return changed;
}
bool DisplayMenuComponent::cursor_down_() {
bool changed = false;
if (this->cursor_index_ + 1 < this->displayed_item_->items_size()) {
changed = true;
++this->cursor_index_;
if (this->cursor_index_ >= this->top_index_ + this->rows_)
this->top_index_ = this->cursor_index_ - this->rows_ + 1;
}
return changed;
}
bool DisplayMenuComponent::enter_menu_() {
this->displayed_item_->on_leave();
this->displayed_item_ = static_cast<MenuItemMenu *>(this->get_selected_item_());
this->selection_stack_.push_front({this->top_index_, this->cursor_index_});
this->cursor_index_ = this->top_index_ = 0;
this->displayed_item_->on_enter();
return true;
}
bool DisplayMenuComponent::leave_menu_() {
bool changed = false;
if (this->displayed_item_->get_parent() != nullptr) {
this->displayed_item_->on_leave();
this->displayed_item_ = this->displayed_item_->get_parent();
this->top_index_ = this->selection_stack_.front().first;
this->cursor_index_ = this->selection_stack_.front().second;
this->selection_stack_.pop_front();
this->displayed_item_->on_enter();
changed = true;
}
return changed;
}
void DisplayMenuComponent::finish_editing_() {
switch (this->get_selected_item_()->get_type()) {
case MENU_ITEM_SELECT:
case MENU_ITEM_NUMBER:
case MENU_ITEM_SWITCH:
case MENU_ITEM_CUSTOM:
this->get_selected_item_()->on_leave();
break;
default:
break;
}
this->editing_ = false;
}
void DisplayMenuComponent::draw_menu() {
for (size_t i = 0; i < this->rows_ && this->top_index_ + i < this->displayed_item_->items_size(); ++i) {
this->draw_item(this->displayed_item_->get_item(this->top_index_ + i), i,
this->top_index_ + i == this->cursor_index_);
}
}
} // namespace display_menu_base
} // namespace esphome

View File

@ -0,0 +1,77 @@
#pragma once
#include "esphome/core/component.h"
#include "menu_item.h"
#include <forward_list>
namespace esphome {
namespace display_menu_base {
enum MenuMode {
MENU_MODE_ROTARY,
MENU_MODE_JOYSTICK,
};
class MenuItem;
/** Class to display a hierarchical menu.
*
*/
class DisplayMenuComponent : public Component {
public:
void set_root_item(MenuItemMenu *item) { this->displayed_item_ = this->root_item_ = item; }
void set_active(bool active) { this->active_ = active; }
void set_mode(MenuMode mode) { this->mode_ = mode; }
void set_rows(uint8_t rows) { this->rows_ = rows; }
float get_setup_priority() const override { return setup_priority::PROCESSOR; }
void up();
void down();
void left();
void right();
void enter();
void show_main();
void show();
void hide();
void draw();
bool is_active() const { return this->active_; }
protected:
void reset_();
void process_initial_();
bool check_healthy_and_active_();
MenuItem *get_selected_item_() { return this->displayed_item_->get_item(this->cursor_index_); }
bool cursor_up_();
bool cursor_down_();
bool enter_menu_();
bool leave_menu_();
void finish_editing_();
virtual void draw_menu();
virtual void draw_item(const MenuItem *item, uint8_t row, bool selected) = 0;
virtual void update() {}
virtual void draw_and_update() {
draw_menu();
update();
}
uint8_t rows_;
bool active_;
MenuMode mode_;
MenuItemMenu *root_item_{nullptr};
MenuItemMenu *displayed_item_{nullptr};
uint8_t top_index_{0};
uint8_t cursor_index_{0};
std::forward_list<std::pair<uint8_t, uint8_t>> selection_stack_{};
bool editing_{false};
bool root_on_enter_called_{false};
};
} // namespace display_menu_base
} // namespace esphome

View File

@ -0,0 +1,179 @@
#include "menu_item.h"
#include <cstdio>
namespace esphome {
namespace display_menu_base {
void MenuItem::on_enter() { this->on_enter_callbacks_.call(); }
void MenuItem::on_leave() { this->on_leave_callbacks_.call(); }
void MenuItem::on_value_() { this->on_value_callbacks_.call(); }
#ifdef USE_SELECT
std::string MenuItemSelect::get_value_text() const {
std::string result;
if (this->value_getter_.has_value()) {
result = this->value_getter_.value()(this);
} else {
if (this->select_var_ != nullptr) {
result = this->select_var_->state;
}
}
return result;
}
bool MenuItemSelect::select_next() {
bool changed = false;
if (this->select_var_ != nullptr) {
this->select_var_->make_call().select_next(true).perform();
changed = true;
}
return changed;
}
bool MenuItemSelect::select_prev() {
bool changed = false;
if (this->select_var_ != nullptr) {
this->select_var_->make_call().select_previous(true).perform();
changed = true;
}
return changed;
}
#endif // USE_SELECT
#ifdef USE_NUMBER
std::string MenuItemNumber::get_value_text() const {
std::string result;
if (this->value_getter_.has_value()) {
result = this->value_getter_.value()(this);
} else {
char data[32];
snprintf(data, sizeof(data), this->format_.c_str(), get_number_value_());
result = data;
}
return result;
}
bool MenuItemNumber::select_next() {
bool changed = false;
if (this->number_var_ != nullptr) {
float last = this->number_var_->state;
this->number_var_->make_call().number_increment(false).perform();
if (this->number_var_->state != last) {
this->on_value_();
changed = true;
}
}
return changed;
}
bool MenuItemNumber::select_prev() {
bool changed = false;
if (this->number_var_ != nullptr) {
float last = this->number_var_->state;
this->number_var_->make_call().number_decrement(false).perform();
if (this->number_var_->state != last) {
this->on_value_();
changed = true;
}
}
return changed;
}
float MenuItemNumber::get_number_value_() const {
float result = 0.0;
if (this->number_var_ != nullptr) {
if (!this->number_var_->has_state() || this->number_var_->state < this->number_var_->traits.get_min_value()) {
result = this->number_var_->traits.get_min_value();
} else if (this->number_var_->state > this->number_var_->traits.get_max_value()) {
result = this->number_var_->traits.get_max_value();
} else {
result = this->number_var_->state;
}
}
return result;
}
#endif // USE_NUMBER
#ifdef USE_SWITCH
std::string MenuItemSwitch::get_value_text() const {
std::string result;
if (this->value_getter_.has_value()) {
result = this->value_getter_.value()(this);
} else {
result = this->get_switch_state_() ? this->switch_on_text_ : this->switch_off_text_;
}
return result;
}
bool MenuItemSwitch::select_next() { return this->toggle_switch_(); }
bool MenuItemSwitch::select_prev() { return this->toggle_switch_(); }
bool MenuItemSwitch::get_switch_state_() const { return (this->switch_var_ != nullptr && this->switch_var_->state); }
bool MenuItemSwitch::toggle_switch_() {
bool changed = false;
if (this->switch_var_ != nullptr) {
this->switch_var_->toggle();
this->on_value_();
changed = true;
}
return changed;
}
#endif // USE_SWITCH
std::string MenuItemCustom::get_value_text() const {
return (this->value_getter_.has_value()) ? this->value_getter_.value()(this) : "";
}
bool MenuItemCommand::select_next() {
this->on_value_();
return true;
}
bool MenuItemCommand::select_prev() {
this->on_value_();
return true;
}
bool MenuItemCustom::select_next() {
this->on_next_();
this->on_value_();
return true;
}
bool MenuItemCustom::select_prev() {
this->on_prev_();
this->on_value_();
return true;
}
void MenuItemCustom::on_next_() { this->on_next_callbacks_.call(); }
void MenuItemCustom::on_prev_() { this->on_prev_callbacks_.call(); }
} // namespace display_menu_base
} // namespace esphome

View File

@ -0,0 +1,187 @@
#pragma once
#include "esphome/core/defines.h"
#include "esphome/core/automation.h"
#ifdef USE_NUMBER
#include "esphome/components/number/number.h"
#endif
#ifdef USE_SELECT
#include "esphome/components/select/select.h"
#endif
#ifdef USE_SWITCH
#include "esphome/components/switch/switch.h"
#endif
#include <vector>
namespace esphome {
namespace display_menu_base {
enum MenuItemType {
MENU_ITEM_LABEL,
MENU_ITEM_MENU,
MENU_ITEM_BACK,
MENU_ITEM_SELECT,
MENU_ITEM_NUMBER,
MENU_ITEM_SWITCH,
MENU_ITEM_COMMAND,
MENU_ITEM_CUSTOM,
};
class MenuItem;
class MenuItemMenu;
using value_getter_t = std::function<std::string(const MenuItem *)>;
class MenuItem {
public:
explicit MenuItem(MenuItemType t) : item_type_(t) {}
void set_parent(MenuItemMenu *parent) { this->parent_ = parent; }
MenuItemMenu *get_parent() { return this->parent_; }
MenuItemType get_type() const { return this->item_type_; }
template<typename V> void set_text(V val) { this->text_ = val; }
void add_on_enter_callback(std::function<void()> &&cb) { this->on_enter_callbacks_.add(std::move(cb)); }
void add_on_leave_callback(std::function<void()> &&cb) { this->on_leave_callbacks_.add(std::move(cb)); }
void add_on_value_callback(std::function<void()> &&cb) { this->on_value_callbacks_.add(std::move(cb)); }
std::string get_text() const { return const_cast<MenuItem *>(this)->text_.value(this); }
virtual bool get_immediate_edit() const { return false; }
virtual bool has_value() const { return false; }
virtual std::string get_value_text() const { return ""; }
virtual bool select_next() { return false; }
virtual bool select_prev() { return false; }
void on_enter();
void on_leave();
protected:
void on_value_();
MenuItemType item_type_;
MenuItemMenu *parent_{nullptr};
TemplatableValue<std::string, const MenuItem *> text_;
CallbackManager<void()> on_enter_callbacks_{};
CallbackManager<void()> on_leave_callbacks_{};
CallbackManager<void()> on_value_callbacks_{};
};
class MenuItemMenu : public MenuItem {
public:
explicit MenuItemMenu() : MenuItem(MENU_ITEM_MENU) {}
void add_item(MenuItem *item) {
item->set_parent(this);
this->items_.push_back(item);
}
size_t items_size() const { return this->items_.size(); }
MenuItem *get_item(size_t i) { return this->items_[i]; }
protected:
std::vector<MenuItem *> items_;
};
class MenuItemEditable : public MenuItem {
public:
explicit MenuItemEditable(MenuItemType t) : MenuItem(t) {}
void set_immediate_edit(bool val) { this->immediate_edit_ = val; }
bool get_immediate_edit() const override { return this->immediate_edit_; }
void set_value_lambda(value_getter_t &&getter) { this->value_getter_ = getter; }
protected:
bool immediate_edit_{false};
optional<value_getter_t> value_getter_{};
};
#ifdef USE_SELECT
class MenuItemSelect : public MenuItemEditable {
public:
explicit MenuItemSelect() : MenuItemEditable(MENU_ITEM_SELECT) {}
void set_select_variable(select::Select *var) { this->select_var_ = var; }
bool has_value() const override { return true; }
std::string get_value_text() const override;
bool select_next() override;
bool select_prev() override;
protected:
select::Select *select_var_{nullptr};
};
#endif
#ifdef USE_NUMBER
class MenuItemNumber : public MenuItemEditable {
public:
explicit MenuItemNumber() : MenuItemEditable(MENU_ITEM_NUMBER) {}
void set_number_variable(number::Number *var) { this->number_var_ = var; }
void set_format(const std::string &fmt) { this->format_ = fmt; }
bool has_value() const override { return true; }
std::string get_value_text() const override;
bool select_next() override;
bool select_prev() override;
protected:
float get_number_value_() const;
number::Number *number_var_{nullptr};
std::string format_;
};
#endif
#ifdef USE_SWITCH
class MenuItemSwitch : public MenuItemEditable {
public:
explicit MenuItemSwitch() : MenuItemEditable(MENU_ITEM_SWITCH) {}
void set_switch_variable(switch_::Switch *var) { this->switch_var_ = var; }
void set_on_text(const std::string &t) { this->switch_on_text_ = t; }
void set_off_text(const std::string &t) { this->switch_off_text_ = t; }
bool has_value() const override { return true; }
std::string get_value_text() const override;
bool select_next() override;
bool select_prev() override;
protected:
bool get_switch_state_() const;
bool toggle_switch_();
switch_::Switch *switch_var_{nullptr};
std::string switch_on_text_;
std::string switch_off_text_;
};
#endif
class MenuItemCommand : public MenuItem {
public:
explicit MenuItemCommand() : MenuItem(MENU_ITEM_COMMAND) {}
bool select_next() override;
bool select_prev() override;
};
class MenuItemCustom : public MenuItemEditable {
public:
explicit MenuItemCustom() : MenuItemEditable(MENU_ITEM_CUSTOM) {}
void add_on_next_callback(std::function<void()> &&cb) { this->on_next_callbacks_.add(std::move(cb)); }
void add_on_prev_callback(std::function<void()> &&cb) { this->on_prev_callbacks_.add(std::move(cb)); }
bool has_value() const override { return this->value_getter_.has_value(); }
std::string get_value_text() const override;
bool select_next() override;
bool select_prev() override;
protected:
void on_next_();
void on_prev_();
CallbackManager<void()> on_next_callbacks_{};
CallbackManager<void()> on_prev_callbacks_{};
};
} // namespace display_menu_base
} // namespace esphome

View File

@ -0,0 +1,74 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_DIMENSIONS,
)
from esphome.core.entity_helpers import inherit_property_from
from esphome.components import lcd_base
from esphome.components.display_menu_base import (
DISPLAY_MENU_BASE_SCHEMA,
DisplayMenuComponent,
display_menu_to_code,
)
CODEOWNERS = ["@numo68"]
AUTO_LOAD = ["display_menu_base"]
lcd_menu_ns = cg.esphome_ns.namespace("lcd_menu")
CONF_DISPLAY_ID = "display_id"
CONF_MARK_SELECTED = "mark_selected"
CONF_MARK_EDITING = "mark_editing"
CONF_MARK_SUBMENU = "mark_submenu"
CONF_MARK_BACK = "mark_back"
MINIMUM_COLUMNS = 12
LCDCharacterMenuComponent = lcd_menu_ns.class_(
"LCDCharacterMenuComponent", DisplayMenuComponent
)
MULTI_CONF = True
def validate_lcd_dimensions(config):
if config[CONF_DIMENSIONS][0] < MINIMUM_COLUMNS:
raise cv.Invalid(
f"LCD display must have at least {MINIMUM_COLUMNS} columns to be usable with the menu"
)
return config
CONFIG_SCHEMA = DISPLAY_MENU_BASE_SCHEMA.extend(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(LCDCharacterMenuComponent),
cv.GenerateID(CONF_DISPLAY_ID): cv.use_id(lcd_base.LCDDisplay),
cv.Optional(CONF_MARK_SELECTED, default=0x3E): cv.uint8_t,
cv.Optional(CONF_MARK_EDITING, default=0x2A): cv.uint8_t,
cv.Optional(CONF_MARK_SUBMENU, default=0x7E): cv.uint8_t,
cv.Optional(CONF_MARK_BACK, default=0x5E): cv.uint8_t,
}
)
)
FINAL_VALIDATE_SCHEMA = cv.All(
inherit_property_from(CONF_DIMENSIONS, CONF_DISPLAY_ID),
validate_lcd_dimensions,
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
disp = await cg.get_variable(config[CONF_DISPLAY_ID])
cg.add(var.set_display(disp))
cg.add(var.set_dimensions(config[CONF_DIMENSIONS][0], config[CONF_DIMENSIONS][1]))
await display_menu_to_code(var, config)
cg.add(var.set_mark_selected(config[CONF_MARK_SELECTED]))
cg.add(var.set_mark_editing(config[CONF_MARK_EDITING]))
cg.add(var.set_mark_submenu(config[CONF_MARK_SUBMENU]))
cg.add(var.set_mark_back(config[CONF_MARK_BACK]))

View File

@ -0,0 +1,74 @@
#include "lcd_menu.h"
#include "esphome/core/log.h"
#include <algorithm>
namespace esphome {
namespace lcd_menu {
static const char *const TAG = "lcd_menu";
void LCDCharacterMenuComponent::setup() {
if (this->display_->is_failed()) {
this->mark_failed();
return;
}
display_menu_base::DisplayMenuComponent::setup();
}
float LCDCharacterMenuComponent::get_setup_priority() const { return setup_priority::PROCESSOR - 1.0f; }
void LCDCharacterMenuComponent::dump_config() {
ESP_LOGCONFIG(TAG, "LCD Menu");
ESP_LOGCONFIG(TAG, " Columns: %u, Rows: %u", this->columns_, this->rows_);
ESP_LOGCONFIG(TAG, " Mark characters: %02x, %02x, %02x, %02x", this->mark_selected_, this->mark_editing_,
this->mark_submenu_, this->mark_back_);
if (this->is_failed()) {
ESP_LOGE(TAG, "The connected display failed, the menu is disabled!");
}
}
void LCDCharacterMenuComponent::draw_item(const display_menu_base::MenuItem *item, uint8_t row, bool selected) {
char data[this->columns_ + 1]; // Bounded to 65 through the config
memset(data, ' ', this->columns_);
if (selected) {
data[0] = (this->editing_ || (this->mode_ == display_menu_base::MENU_MODE_JOYSTICK && item->get_immediate_edit()))
? this->mark_editing_
: this->mark_selected_;
}
switch (item->get_type()) {
case display_menu_base::MENU_ITEM_MENU:
data[this->columns_ - 1] = this->mark_submenu_;
break;
case display_menu_base::MENU_ITEM_BACK:
data[this->columns_ - 1] = this->mark_back_;
break;
default:
break;
}
auto text = item->get_text();
size_t n = std::min(text.size(), (size_t) this->columns_ - 2);
memcpy(data + 1, item->get_text().c_str(), n);
if (item->has_value()) {
std::string value = item->get_value_text();
// Maximum: start mark, at least two chars of label, space, '[', value, ']',
// end mark. Config guarantees columns >= 12
size_t val_width = std::min((size_t) this->columns_ - 7, value.length());
memcpy(data + this->columns_ - val_width - 4, " [", 2);
memcpy(data + this->columns_ - val_width - 2, value.c_str(), val_width);
data[this->columns_ - 2] = ']';
}
data[this->columns_] = '\0';
this->display_->print(0, row, data);
}
} // namespace lcd_menu
} // namespace esphome

View File

@ -0,0 +1,45 @@
#pragma once
#include "esphome/components/lcd_base/lcd_display.h"
#include "esphome/components/display_menu_base/display_menu_base.h"
#include <forward_list>
#include <vector>
namespace esphome {
namespace lcd_menu {
/** Class to display a hierarchical menu.
*
*/
class LCDCharacterMenuComponent : public display_menu_base::DisplayMenuComponent {
public:
void set_display(lcd_base::LCDDisplay *display) { this->display_ = display; }
void set_dimensions(uint8_t columns, uint8_t rows) {
this->columns_ = columns;
set_rows(rows);
}
void set_mark_selected(uint8_t c) { this->mark_selected_ = c; }
void set_mark_editing(uint8_t c) { this->mark_editing_ = c; }
void set_mark_submenu(uint8_t c) { this->mark_submenu_ = c; }
void set_mark_back(uint8_t c) { this->mark_back_ = c; }
void setup() override;
float get_setup_priority() const override;
void dump_config() override;
protected:
void draw_item(const display_menu_base::MenuItem *item, uint8_t row, bool selected) override;
void update() override { this->display_->update(); }
lcd_base::LCDDisplay *display_;
uint8_t columns_;
char mark_selected_;
char mark_editing_;
char mark_submenu_;
char mark_back_;
};
} // namespace lcd_menu
} // namespace esphome

View File

@ -871,8 +871,10 @@ sensor:
value: !lambda "return -1;"
on_clockwise:
- logger.log: Clockwise
- display_menu.down:
on_anticlockwise:
- logger.log: Anticlockwise
- display_menu.up:
- platform: pulse_width
name: Pulse Width
pin: GPIO12
@ -1289,6 +1291,16 @@ binary_sensor:
pin: GPIO27
threshold: 1000
id: btn_left
on_press:
- if:
condition:
display_menu.is_active:
then:
- display_menu.enter:
else:
- display_menu.left:
- display_menu.right:
- display_menu.show:
- platform: template
name: Garage Door Open
id: garage_door
@ -2331,6 +2343,7 @@ color:
display:
- platform: lcd_gpio
id: my_lcd_gpio
dimensions: 18x4
data_pins:
- GPIO19
@ -3009,3 +3022,85 @@ button:
name: Midea Power Inverse
on_press:
midea_ac.power_toggle:
lcd_menu:
display_id: my_lcd_gpio
mark_back: 0x5e
mark_selected: 0x3e
mark_editing: 0x2a
mark_submenu: 0x7e
active: false
mode: rotary
on_enter:
then:
lambda: 'ESP_LOGI("lcd_menu", "root enter");'
on_leave:
then:
lambda: 'ESP_LOGI("lcd_menu", "root leave");'
items:
- type: back
text: 'Back'
- type: label
- type: menu
text: 'Submenu 1'
items:
- type: back
text: 'Back'
- type: menu
text: 'Submenu 21'
items:
- type: back
text: 'Back'
- type: command
text: 'Show Main'
on_value:
then:
- display_menu.show_main:
- type: select
text: 'Enum Item'
immediate_edit: true
select: test_select
on_enter:
then:
lambda: 'ESP_LOGI("lcd_menu", "select enter: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());'
on_leave:
then:
lambda: 'ESP_LOGI("lcd_menu", "select leave: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());'
on_value:
then:
lambda: 'ESP_LOGI("lcd_menu", "select value: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());'
- type: number
text: 'Number'
number: test_number
on_enter:
then:
lambda: 'ESP_LOGI("lcd_menu", "number enter: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());'
on_leave:
then:
lambda: 'ESP_LOGI("lcd_menu", "number leave: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());'
on_value:
then:
lambda: 'ESP_LOGI("lcd_menu", "number value: %s, %s", it->get_text().c_str(), it->get_value_text().c_str());'
- type: command
text: 'Hide'
on_value:
then:
- display_menu.hide:
- type: switch
text: 'Switch'
switch: my_switch
on_text: 'Bright'
off_text: 'Dark'
immediate_edit: false
on_value:
then:
lambda: 'ESP_LOGI("lcd_menu", "switch value: %s", it->get_value_text().c_str());'
- type: custom
text: !lambda 'return "Custom";'
value_lambda: 'return "Val";'
on_next:
then:
lambda: 'ESP_LOGI("lcd_menu", "custom next: %s", it->get_text().c_str());'
on_prev:
then:
lambda: 'ESP_LOGI("lcd_menu", "custom prev: %s", it->get_text().c_str());'