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/delonghi/* @grob6000
|
||||||
esphome/components/dfplayer/* @glmnet
|
esphome/components/dfplayer/* @glmnet
|
||||||
esphome/components/dht/* @OttoWinter
|
esphome/components/dht/* @OttoWinter
|
||||||
|
esphome/components/display_menu_base/* @numo68
|
||||||
esphome/components/dps310/* @kbx81
|
esphome/components/dps310/* @kbx81
|
||||||
esphome/components/ds1307/* @badbadc0ffee
|
esphome/components/ds1307/* @badbadc0ffee
|
||||||
esphome/components/dsmr/* @glmnet @zuidwijk
|
esphome/components/dsmr/* @glmnet @zuidwijk
|
||||||
|
@ -110,6 +111,7 @@ esphome/components/integration/* @OttoWinter
|
||||||
esphome/components/interval/* @esphome/core
|
esphome/components/interval/* @esphome/core
|
||||||
esphome/components/json/* @OttoWinter
|
esphome/components/json/* @OttoWinter
|
||||||
esphome/components/kalman_combinator/* @Cat-Ion
|
esphome/components/kalman_combinator/* @Cat-Ion
|
||||||
|
esphome/components/lcd_menu/* @numo68
|
||||||
esphome/components/ledc/* @OttoWinter
|
esphome/components/ledc/* @OttoWinter
|
||||||
esphome/components/light/* @esphome/core
|
esphome/components/light/* @esphome/core
|
||||||
esphome/components/lilygo_t5_47/touchscreen/* @jesserockz
|
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;"
|
value: !lambda "return -1;"
|
||||||
on_clockwise:
|
on_clockwise:
|
||||||
- logger.log: Clockwise
|
- logger.log: Clockwise
|
||||||
|
- display_menu.down:
|
||||||
on_anticlockwise:
|
on_anticlockwise:
|
||||||
- logger.log: Anticlockwise
|
- logger.log: Anticlockwise
|
||||||
|
- display_menu.up:
|
||||||
- platform: pulse_width
|
- platform: pulse_width
|
||||||
name: Pulse Width
|
name: Pulse Width
|
||||||
pin: GPIO12
|
pin: GPIO12
|
||||||
|
@ -1289,6 +1291,16 @@ binary_sensor:
|
||||||
pin: GPIO27
|
pin: GPIO27
|
||||||
threshold: 1000
|
threshold: 1000
|
||||||
id: btn_left
|
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
|
- platform: template
|
||||||
name: Garage Door Open
|
name: Garage Door Open
|
||||||
id: garage_door
|
id: garage_door
|
||||||
|
@ -2331,6 +2343,7 @@ color:
|
||||||
|
|
||||||
display:
|
display:
|
||||||
- platform: lcd_gpio
|
- platform: lcd_gpio
|
||||||
|
id: my_lcd_gpio
|
||||||
dimensions: 18x4
|
dimensions: 18x4
|
||||||
data_pins:
|
data_pins:
|
||||||
- GPIO19
|
- GPIO19
|
||||||
|
@ -3009,3 +3022,85 @@ button:
|
||||||
name: Midea Power Inverse
|
name: Midea Power Inverse
|
||||||
on_press:
|
on_press:
|
||||||
midea_ac.power_toggle:
|
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