mirror of
https://github.com/esphome/esphome.git
synced 2024-11-24 16:08:10 +01:00
Implement a simple LCD menu (#3406)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
parent
90683223dd
commit
2c76381fcd
11 changed files with 1611 additions and 0 deletions
|
@ -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
|
||||
|
|
430
esphome/components/display_menu_base/__init__.py
Normal file
430
esphome/components/display_menu_base/__init__.py
Normal 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)
|
133
esphome/components/display_menu_base/automation.h
Normal file
133
esphome/components/display_menu_base/automation.h
Normal 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
|
315
esphome/components/display_menu_base/display_menu_base.cpp
Normal file
315
esphome/components/display_menu_base/display_menu_base.cpp
Normal 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
|
77
esphome/components/display_menu_base/display_menu_base.h
Normal file
77
esphome/components/display_menu_base/display_menu_base.h
Normal 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
|
179
esphome/components/display_menu_base/menu_item.cpp
Normal file
179
esphome/components/display_menu_base/menu_item.cpp
Normal 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
|
187
esphome/components/display_menu_base/menu_item.h
Normal file
187
esphome/components/display_menu_base/menu_item.h
Normal 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
|
74
esphome/components/lcd_menu/__init__.py
Normal file
74
esphome/components/lcd_menu/__init__.py
Normal 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]))
|
74
esphome/components/lcd_menu/lcd_menu.cpp
Normal file
74
esphome/components/lcd_menu/lcd_menu.cpp
Normal 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
|
45
esphome/components/lcd_menu/lcd_menu.h
Normal file
45
esphome/components/lcd_menu/lcd_menu.h
Normal 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
|
|
@ -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());'
|
||||
|
|
Loading…
Reference in a new issue