mirror of
https://github.com/esphome/esphome.git
synced 2025-03-15 07:08:20 +00:00
Merge branch 'dev' into nrf52_core
This commit is contained in:
commit
d8dadb6a22
@ -277,6 +277,7 @@ esphome/components/noblex/* @AGalfra
|
|||||||
esphome/components/nrf52/* @tomaszduda23
|
esphome/components/nrf52/* @tomaszduda23
|
||||||
esphome/components/number/* @esphome/core
|
esphome/components/number/* @esphome/core
|
||||||
esphome/components/one_wire/* @ssieb
|
esphome/components/one_wire/* @ssieb
|
||||||
|
esphome/components/online_image/* @guillempages
|
||||||
esphome/components/ota/* @esphome/core
|
esphome/components/ota/* @esphome/core
|
||||||
esphome/components/output/* @esphome/core
|
esphome/components/output/* @esphome/core
|
||||||
esphome/components/pca6416a/* @Mat931
|
esphome/components/pca6416a/* @Mat931
|
||||||
|
@ -1,16 +1,17 @@
|
|||||||
import esphome.codegen as cg
|
|
||||||
import esphome.config_validation as cv
|
|
||||||
from esphome.components import web_server
|
|
||||||
from esphome import automation
|
from esphome import automation
|
||||||
from esphome.automation import maybe_simple_id
|
from esphome.automation import maybe_simple_id
|
||||||
from esphome.core import CORE, coroutine_with_priority
|
import esphome.codegen as cg
|
||||||
|
from esphome.components import mqtt, web_server
|
||||||
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
|
CONF_CODE,
|
||||||
CONF_ID,
|
CONF_ID,
|
||||||
|
CONF_MQTT_ID,
|
||||||
CONF_ON_STATE,
|
CONF_ON_STATE,
|
||||||
CONF_TRIGGER_ID,
|
CONF_TRIGGER_ID,
|
||||||
CONF_CODE,
|
|
||||||
CONF_WEB_SERVER_ID,
|
CONF_WEB_SERVER_ID,
|
||||||
)
|
)
|
||||||
|
from esphome.core import CORE, coroutine_with_priority
|
||||||
from esphome.cpp_helpers import setup_entity
|
from esphome.cpp_helpers import setup_entity
|
||||||
|
|
||||||
CODEOWNERS = ["@grahambrown11", "@hwstar"]
|
CODEOWNERS = ["@grahambrown11", "@hwstar"]
|
||||||
@ -77,67 +78,72 @@ AlarmControlPanelCondition = alarm_control_panel_ns.class_(
|
|||||||
"AlarmControlPanelCondition", automation.Condition
|
"AlarmControlPanelCondition", automation.Condition
|
||||||
)
|
)
|
||||||
|
|
||||||
ALARM_CONTROL_PANEL_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
|
ALARM_CONTROL_PANEL_SCHEMA = (
|
||||||
web_server.WEBSERVER_SORTING_SCHEMA
|
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
|
||||||
).extend(
|
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
|
||||||
{
|
.extend(
|
||||||
cv.GenerateID(): cv.declare_id(AlarmControlPanel),
|
{
|
||||||
cv.Optional(CONF_ON_STATE): automation.validate_automation(
|
cv.GenerateID(): cv.declare_id(AlarmControlPanel),
|
||||||
{
|
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
|
||||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger),
|
mqtt.MQTTAlarmControlPanelComponent
|
||||||
}
|
),
|
||||||
),
|
cv.Optional(CONF_ON_STATE): automation.validate_automation(
|
||||||
cv.Optional(CONF_ON_TRIGGERED): automation.validate_automation(
|
{
|
||||||
{
|
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger),
|
||||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TriggeredTrigger),
|
}
|
||||||
}
|
),
|
||||||
),
|
cv.Optional(CONF_ON_TRIGGERED): automation.validate_automation(
|
||||||
cv.Optional(CONF_ON_ARMING): automation.validate_automation(
|
{
|
||||||
{
|
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TriggeredTrigger),
|
||||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmingTrigger),
|
}
|
||||||
}
|
),
|
||||||
),
|
cv.Optional(CONF_ON_ARMING): automation.validate_automation(
|
||||||
cv.Optional(CONF_ON_PENDING): automation.validate_automation(
|
{
|
||||||
{
|
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmingTrigger),
|
||||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PendingTrigger),
|
}
|
||||||
}
|
),
|
||||||
),
|
cv.Optional(CONF_ON_PENDING): automation.validate_automation(
|
||||||
cv.Optional(CONF_ON_ARMED_HOME): automation.validate_automation(
|
{
|
||||||
{
|
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PendingTrigger),
|
||||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedHomeTrigger),
|
}
|
||||||
}
|
),
|
||||||
),
|
cv.Optional(CONF_ON_ARMED_HOME): automation.validate_automation(
|
||||||
cv.Optional(CONF_ON_ARMED_NIGHT): automation.validate_automation(
|
{
|
||||||
{
|
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedHomeTrigger),
|
||||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedNightTrigger),
|
}
|
||||||
}
|
),
|
||||||
),
|
cv.Optional(CONF_ON_ARMED_NIGHT): automation.validate_automation(
|
||||||
cv.Optional(CONF_ON_ARMED_AWAY): automation.validate_automation(
|
{
|
||||||
{
|
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedNightTrigger),
|
||||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedAwayTrigger),
|
}
|
||||||
}
|
),
|
||||||
),
|
cv.Optional(CONF_ON_ARMED_AWAY): automation.validate_automation(
|
||||||
cv.Optional(CONF_ON_DISARMED): automation.validate_automation(
|
{
|
||||||
{
|
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedAwayTrigger),
|
||||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DisarmedTrigger),
|
}
|
||||||
}
|
),
|
||||||
),
|
cv.Optional(CONF_ON_DISARMED): automation.validate_automation(
|
||||||
cv.Optional(CONF_ON_CLEARED): automation.validate_automation(
|
{
|
||||||
{
|
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DisarmedTrigger),
|
||||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ClearedTrigger),
|
}
|
||||||
}
|
),
|
||||||
),
|
cv.Optional(CONF_ON_CLEARED): automation.validate_automation(
|
||||||
cv.Optional(CONF_ON_CHIME): automation.validate_automation(
|
{
|
||||||
{
|
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ClearedTrigger),
|
||||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ChimeTrigger),
|
}
|
||||||
}
|
),
|
||||||
),
|
cv.Optional(CONF_ON_CHIME): automation.validate_automation(
|
||||||
cv.Optional(CONF_ON_READY): automation.validate_automation(
|
{
|
||||||
{
|
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ChimeTrigger),
|
||||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ReadyTrigger),
|
}
|
||||||
}
|
),
|
||||||
),
|
cv.Optional(CONF_ON_READY): automation.validate_automation(
|
||||||
}
|
{
|
||||||
|
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ReadyTrigger),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
ALARM_CONTROL_PANEL_ACTION_SCHEMA = maybe_simple_id(
|
ALARM_CONTROL_PANEL_ACTION_SCHEMA = maybe_simple_id(
|
||||||
@ -192,6 +198,9 @@ async def setup_alarm_control_panel_core_(var, config):
|
|||||||
if (webserver_id := config.get(CONF_WEB_SERVER_ID)) is not None:
|
if (webserver_id := config.get(CONF_WEB_SERVER_ID)) is not None:
|
||||||
web_server_ = await cg.get_variable(webserver_id)
|
web_server_ = await cg.get_variable(webserver_id)
|
||||||
web_server.add_entity_to_sorting_list(web_server_, var, config)
|
web_server.add_entity_to_sorting_list(web_server_, var, config)
|
||||||
|
if mqtt_id := config.get(CONF_MQTT_ID):
|
||||||
|
mqtt_ = cg.new_Pvariable(mqtt_id, var)
|
||||||
|
await mqtt.register_mqtt_component(mqtt_, config)
|
||||||
|
|
||||||
|
|
||||||
async def register_alarm_control_panel(var, config):
|
async def register_alarm_control_panel(var, config):
|
||||||
|
@ -1,19 +1,22 @@
|
|||||||
import esphome.codegen as cg
|
|
||||||
import esphome.config_validation as cv
|
|
||||||
from esphome.components import display, font, color
|
|
||||||
from esphome.const import CONF_DISPLAY, CONF_ID, CONF_TRIGGER_ID
|
|
||||||
from esphome import automation, core
|
from esphome import automation, core
|
||||||
|
import esphome.codegen as cg
|
||||||
|
from esphome.components import color, display, font
|
||||||
from esphome.components.display_menu_base import (
|
from esphome.components.display_menu_base import (
|
||||||
DISPLAY_MENU_BASE_SCHEMA,
|
DISPLAY_MENU_BASE_SCHEMA,
|
||||||
DisplayMenuComponent,
|
DisplayMenuComponent,
|
||||||
display_menu_to_code,
|
display_menu_to_code,
|
||||||
)
|
)
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.const import (
|
||||||
|
CONF_BACKGROUND_COLOR,
|
||||||
|
CONF_DISPLAY,
|
||||||
|
CONF_FOREGROUND_COLOR,
|
||||||
|
CONF_ID,
|
||||||
|
CONF_TRIGGER_ID,
|
||||||
|
)
|
||||||
|
|
||||||
CONF_FONT = "font"
|
CONF_FONT = "font"
|
||||||
CONF_MENU_ITEM_VALUE = "menu_item_value"
|
CONF_MENU_ITEM_VALUE = "menu_item_value"
|
||||||
CONF_FOREGROUND_COLOR = "foreground_color"
|
|
||||||
CONF_BACKGROUND_COLOR = "background_color"
|
|
||||||
CONF_ON_REDRAW = "on_redraw"
|
CONF_ON_REDRAW = "on_redraw"
|
||||||
|
|
||||||
graphical_display_menu_ns = cg.esphome_ns.namespace("graphical_display_menu")
|
graphical_display_menu_ns = cg.esphome_ns.namespace("graphical_display_menu")
|
||||||
|
@ -236,7 +236,7 @@ void HydreonRGxxComponent::process_line_() {
|
|||||||
}
|
}
|
||||||
bool is_data_line = false;
|
bool is_data_line = false;
|
||||||
for (int i = 0; i < NUM_SENSORS; i++) {
|
for (int i = 0; i < NUM_SENSORS; i++) {
|
||||||
if (this->sensors_[i] != nullptr && this->buffer_starts_with_(PROTOCOL_NAMES[i])) {
|
if (this->sensors_[i] != nullptr && this->buffer_.find(PROTOCOL_NAMES[i]) != std::string::npos) {
|
||||||
is_data_line = true;
|
is_data_line = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -233,6 +233,7 @@ void I2SAudioSpeaker::loop() {
|
|||||||
switch (this->state_) {
|
switch (this->state_) {
|
||||||
case speaker::STATE_STARTING:
|
case speaker::STATE_STARTING:
|
||||||
this->start_();
|
this->start_();
|
||||||
|
[[fallthrough]];
|
||||||
case speaker::STATE_RUNNING:
|
case speaker::STATE_RUNNING:
|
||||||
case speaker::STATE_STOPPING:
|
case speaker::STATE_STOPPING:
|
||||||
this->watch_();
|
this->watch_();
|
||||||
|
@ -21,22 +21,10 @@ from esphome.final_validate import full_config
|
|||||||
from esphome.helpers import write_file_if_changed
|
from esphome.helpers import write_file_if_changed
|
||||||
|
|
||||||
from . import defines as df, helpers, lv_validation as lvalid
|
from . import defines as df, helpers, lv_validation as lvalid
|
||||||
from .animimg import animimg_spec
|
|
||||||
from .arc import arc_spec
|
|
||||||
from .automation import disp_update, update_to_code
|
from .automation import disp_update, update_to_code
|
||||||
from .btn import btn_spec
|
|
||||||
from .checkbox import checkbox_spec
|
|
||||||
from .defines import CONF_SKIP
|
from .defines import CONF_SKIP
|
||||||
from .img import img_spec
|
|
||||||
from .label import label_spec
|
|
||||||
from .led import led_spec
|
|
||||||
from .line import line_spec
|
|
||||||
from .lv_bar import bar_spec
|
|
||||||
from .lv_switch import switch_spec
|
|
||||||
from .lv_validation import lv_bool, lv_images_used
|
from .lv_validation import lv_bool, lv_images_used
|
||||||
from .lvcode import LvContext, LvglComponent
|
from .lvcode import LvContext, LvglComponent
|
||||||
from .obj import obj_spec
|
|
||||||
from .page import add_pages, page_spec
|
|
||||||
from .rotary_encoders import ROTARY_ENCODER_CONFIG, rotary_encoders_to_code
|
from .rotary_encoders import ROTARY_ENCODER_CONFIG, rotary_encoders_to_code
|
||||||
from .schemas import (
|
from .schemas import (
|
||||||
DISP_BG_SCHEMA,
|
DISP_BG_SCHEMA,
|
||||||
@ -51,8 +39,6 @@ from .schemas import (
|
|||||||
grid_alignments,
|
grid_alignments,
|
||||||
obj_schema,
|
obj_schema,
|
||||||
)
|
)
|
||||||
from .slider import slider_spec
|
|
||||||
from .spinner import spinner_spec
|
|
||||||
from .styles import add_top_layer, styles_to_code, theme_to_code
|
from .styles import add_top_layer, styles_to_code, theme_to_code
|
||||||
from .touchscreens import touchscreen_schema, touchscreens_to_code
|
from .touchscreens import touchscreen_schema, touchscreens_to_code
|
||||||
from .trigger import generate_triggers
|
from .trigger import generate_triggers
|
||||||
@ -64,7 +50,31 @@ from .types import (
|
|||||||
lv_style_t,
|
lv_style_t,
|
||||||
lvgl_ns,
|
lvgl_ns,
|
||||||
)
|
)
|
||||||
from .widget import Widget, add_widgets, lv_scr_act, set_obj_properties
|
from .widgets import Widget, add_widgets, lv_scr_act, set_obj_properties
|
||||||
|
from .widgets.animimg import animimg_spec
|
||||||
|
from .widgets.arc import arc_spec
|
||||||
|
from .widgets.button import button_spec
|
||||||
|
from .widgets.buttonmatrix import buttonmatrix_spec
|
||||||
|
from .widgets.checkbox import checkbox_spec
|
||||||
|
from .widgets.dropdown import dropdown_spec
|
||||||
|
from .widgets.img import img_spec
|
||||||
|
from .widgets.keyboard import keyboard_spec
|
||||||
|
from .widgets.label import label_spec
|
||||||
|
from .widgets.led import led_spec
|
||||||
|
from .widgets.line import line_spec
|
||||||
|
from .widgets.lv_bar import bar_spec
|
||||||
|
from .widgets.meter import meter_spec
|
||||||
|
from .widgets.msgbox import MSGBOX_SCHEMA, msgboxes_to_code
|
||||||
|
from .widgets.obj import obj_spec
|
||||||
|
from .widgets.page import add_pages, page_spec
|
||||||
|
from .widgets.roller import roller_spec
|
||||||
|
from .widgets.slider import slider_spec
|
||||||
|
from .widgets.spinbox import spinbox_spec
|
||||||
|
from .widgets.spinner import spinner_spec
|
||||||
|
from .widgets.switch import switch_spec
|
||||||
|
from .widgets.tabview import tabview_spec
|
||||||
|
from .widgets.textarea import textarea_spec
|
||||||
|
from .widgets.tileview import tileview_spec
|
||||||
|
|
||||||
DOMAIN = "lvgl"
|
DOMAIN = "lvgl"
|
||||||
DEPENDENCIES = ["display"]
|
DEPENDENCIES = ["display"]
|
||||||
@ -75,7 +85,7 @@ LOGGER = logging.getLogger(__name__)
|
|||||||
for w_type in (
|
for w_type in (
|
||||||
label_spec,
|
label_spec,
|
||||||
obj_spec,
|
obj_spec,
|
||||||
btn_spec,
|
button_spec,
|
||||||
bar_spec,
|
bar_spec,
|
||||||
slider_spec,
|
slider_spec,
|
||||||
arc_spec,
|
arc_spec,
|
||||||
@ -86,6 +96,15 @@ for w_type in (
|
|||||||
checkbox_spec,
|
checkbox_spec,
|
||||||
img_spec,
|
img_spec,
|
||||||
switch_spec,
|
switch_spec,
|
||||||
|
tabview_spec,
|
||||||
|
buttonmatrix_spec,
|
||||||
|
meter_spec,
|
||||||
|
dropdown_spec,
|
||||||
|
roller_spec,
|
||||||
|
textarea_spec,
|
||||||
|
spinbox_spec,
|
||||||
|
keyboard_spec,
|
||||||
|
tileview_spec,
|
||||||
):
|
):
|
||||||
WIDGET_TYPES[w_type.name] = w_type
|
WIDGET_TYPES[w_type.name] = w_type
|
||||||
|
|
||||||
@ -244,6 +263,7 @@ async def to_code(config):
|
|||||||
await add_widgets(lv_scr_act, config)
|
await add_widgets(lv_scr_act, config)
|
||||||
await add_pages(lv_component, config)
|
await add_pages(lv_component, config)
|
||||||
await add_top_layer(config)
|
await add_top_layer(config)
|
||||||
|
await msgboxes_to_code(config)
|
||||||
await disp_update(f"{lv_component}->get_disp()", config)
|
await disp_update(f"{lv_component}->get_disp()", config)
|
||||||
Widget.set_completed()
|
Widget.set_completed()
|
||||||
await generate_triggers(lv_component)
|
await generate_triggers(lv_component)
|
||||||
@ -308,6 +328,7 @@ CONFIG_SCHEMA = (
|
|||||||
cv.Exclusive(CONF_PAGES, CONF_PAGES): cv.ensure_list(
|
cv.Exclusive(CONF_PAGES, CONF_PAGES): cv.ensure_list(
|
||||||
container_schema(page_spec)
|
container_schema(page_spec)
|
||||||
),
|
),
|
||||||
|
cv.Optional(df.CONF_MSGBOXES): cv.ensure_list(MSGBOX_SCHEMA),
|
||||||
cv.Optional(df.CONF_PAGE_WRAP, default=True): lv_bool,
|
cv.Optional(df.CONF_PAGE_WRAP, default=True): lv_bool,
|
||||||
cv.Optional(df.CONF_TOP_LAYER): container_schema(obj_spec),
|
cv.Optional(df.CONF_TOP_LAYER): container_schema(obj_spec),
|
||||||
cv.Optional(df.CONF_TRANSPARENCY_KEY, default=0x000400): lvalid.lv_color,
|
cv.Optional(df.CONF_TRANSPARENCY_KEY, default=0x000400): lvalid.lv_color,
|
||||||
|
@ -38,7 +38,7 @@ from .types import (
|
|||||||
lv_disp_t,
|
lv_disp_t,
|
||||||
lv_obj_t,
|
lv_obj_t,
|
||||||
)
|
)
|
||||||
from .widget import Widget, get_widgets, lv_scr_act, set_obj_properties
|
from .widgets import Widget, get_widgets, lv_scr_act, set_obj_properties
|
||||||
|
|
||||||
|
|
||||||
async def action_to_code(
|
async def action_to_code(
|
||||||
@ -109,7 +109,7 @@ async def disp_update(disp, config: dict):
|
|||||||
if CONF_DISP_BG_COLOR not in config and CONF_DISP_BG_IMAGE not in config:
|
if CONF_DISP_BG_COLOR not in config and CONF_DISP_BG_IMAGE not in config:
|
||||||
return
|
return
|
||||||
with LocalVariable("lv_disp_tmp", lv_disp_t, literal(disp)) as disp_temp:
|
with LocalVariable("lv_disp_tmp", lv_disp_t, literal(disp)) as disp_temp:
|
||||||
if bg_color := config.get(CONF_DISP_BG_COLOR):
|
if (bg_color := config.get(CONF_DISP_BG_COLOR)) is not None:
|
||||||
lv.disp_set_bg_color(disp_temp, await lv_color.process(bg_color))
|
lv.disp_set_bg_color(disp_temp, await lv_color.process(bg_color))
|
||||||
if bg_image := config.get(CONF_DISP_BG_IMAGE):
|
if bg_image := config.get(CONF_DISP_BG_IMAGE):
|
||||||
lv.disp_set_bg_image(disp_temp, await lv_image.process(bg_image))
|
lv.disp_set_bg_image(disp_temp, await lv_image.process(bg_image))
|
||||||
|
43
esphome/components/lvgl/binary_sensor/__init__.py
Normal file
43
esphome/components/lvgl/binary_sensor/__init__.py
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import esphome.codegen as cg
|
||||||
|
from esphome.components.binary_sensor import (
|
||||||
|
BinarySensor,
|
||||||
|
binary_sensor_schema,
|
||||||
|
new_binary_sensor,
|
||||||
|
)
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
|
||||||
|
from ..defines import CONF_LVGL_ID, CONF_WIDGET
|
||||||
|
from ..lvcode import EVENT_ARG, LambdaContext, LvContext
|
||||||
|
from ..schemas import LVGL_SCHEMA
|
||||||
|
from ..types import LV_EVENT, lv_pseudo_button_t
|
||||||
|
from ..widgets import Widget, get_widgets
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = (
|
||||||
|
binary_sensor_schema(BinarySensor)
|
||||||
|
.extend(LVGL_SCHEMA)
|
||||||
|
.extend(
|
||||||
|
{
|
||||||
|
cv.Required(CONF_WIDGET): cv.use_id(lv_pseudo_button_t),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
sensor = await new_binary_sensor(config)
|
||||||
|
paren = await cg.get_variable(config[CONF_LVGL_ID])
|
||||||
|
widget = await get_widgets(config, CONF_WIDGET)
|
||||||
|
widget = widget[0]
|
||||||
|
assert isinstance(widget, Widget)
|
||||||
|
async with LambdaContext(EVENT_ARG) as pressed_ctx:
|
||||||
|
pressed_ctx.add(sensor.publish_state(widget.is_pressed()))
|
||||||
|
async with LvContext(paren) as ctx:
|
||||||
|
ctx.add(sensor.publish_initial_state(widget.is_pressed()))
|
||||||
|
ctx.add(
|
||||||
|
paren.add_event_cb(
|
||||||
|
widget.obj,
|
||||||
|
await pressed_ctx.get_lambda(),
|
||||||
|
LV_EVENT.PRESSING,
|
||||||
|
LV_EVENT.RELEASED,
|
||||||
|
)
|
||||||
|
)
|
@ -1,20 +0,0 @@
|
|||||||
from esphome.const import CONF_BUTTON
|
|
||||||
|
|
||||||
from .defines import CONF_MAIN
|
|
||||||
from .types import LvBoolean, WidgetType
|
|
||||||
|
|
||||||
lv_btn_t = LvBoolean("lv_btn_t")
|
|
||||||
|
|
||||||
|
|
||||||
class BtnType(WidgetType):
|
|
||||||
def __init__(self):
|
|
||||||
super().__init__(CONF_BUTTON, lv_btn_t, (CONF_MAIN,), lv_name="btn")
|
|
||||||
|
|
||||||
def get_uses(self):
|
|
||||||
return ("btn",)
|
|
||||||
|
|
||||||
async def to_code(self, w, config):
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
btn_spec = BtnType()
|
|
@ -304,7 +304,7 @@ OBJ_FLAGS = (
|
|||||||
ARC_MODES = LvConstant("LV_ARC_MODE_", "NORMAL", "REVERSE", "SYMMETRICAL")
|
ARC_MODES = LvConstant("LV_ARC_MODE_", "NORMAL", "REVERSE", "SYMMETRICAL")
|
||||||
BAR_MODES = LvConstant("LV_BAR_MODE_", "NORMAL", "SYMMETRICAL", "RANGE")
|
BAR_MODES = LvConstant("LV_BAR_MODE_", "NORMAL", "SYMMETRICAL", "RANGE")
|
||||||
|
|
||||||
BTNMATRIX_CTRLS = LvConstant(
|
BUTTONMATRIX_CTRLS = LvConstant(
|
||||||
"LV_BTNMATRIX_CTRL_",
|
"LV_BTNMATRIX_CTRL_",
|
||||||
"HIDDEN",
|
"HIDDEN",
|
||||||
"NO_REPEAT",
|
"NO_REPEAT",
|
||||||
|
32
esphome/components/lvgl/light/__init__.py
Normal file
32
esphome/components/lvgl/light/__init__.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import esphome.codegen as cg
|
||||||
|
from esphome.components import light
|
||||||
|
from esphome.components.light import LightOutput
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.const import CONF_GAMMA_CORRECT, CONF_LED, CONF_OUTPUT_ID
|
||||||
|
|
||||||
|
from ..defines import CONF_LVGL_ID
|
||||||
|
from ..lvcode import LvContext
|
||||||
|
from ..schemas import LVGL_SCHEMA
|
||||||
|
from ..types import LvType, lvgl_ns
|
||||||
|
from ..widgets import get_widgets
|
||||||
|
|
||||||
|
lv_led_t = LvType("lv_led_t")
|
||||||
|
LVLight = lvgl_ns.class_("LVLight", LightOutput)
|
||||||
|
CONFIG_SCHEMA = light.RGB_LIGHT_SCHEMA.extend(
|
||||||
|
{
|
||||||
|
cv.Optional(CONF_GAMMA_CORRECT, default=0.0): cv.positive_float,
|
||||||
|
cv.Required(CONF_LED): cv.use_id(lv_led_t),
|
||||||
|
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(LVLight),
|
||||||
|
}
|
||||||
|
).extend(LVGL_SCHEMA)
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
var = cg.new_Pvariable(config[CONF_OUTPUT_ID])
|
||||||
|
await light.register_light(var, config)
|
||||||
|
|
||||||
|
paren = await cg.get_variable(config[CONF_LVGL_ID])
|
||||||
|
widget = await get_widgets(config, CONF_LED)
|
||||||
|
widget = widget[0]
|
||||||
|
async with LvContext(paren) as ctx:
|
||||||
|
ctx.add(var.set_obj(widget.obj))
|
48
esphome/components/lvgl/light/lvgl_light.h
Normal file
48
esphome/components/lvgl/light/lvgl_light.h
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/components/light/light_output.h"
|
||||||
|
#include "../lvgl_esphome.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace lvgl {
|
||||||
|
|
||||||
|
class LVLight : public light::LightOutput {
|
||||||
|
public:
|
||||||
|
light::LightTraits get_traits() override {
|
||||||
|
auto traits = light::LightTraits();
|
||||||
|
traits.set_supported_color_modes({light::ColorMode::RGB});
|
||||||
|
return traits;
|
||||||
|
}
|
||||||
|
void write_state(light::LightState *state) override {
|
||||||
|
float red, green, blue;
|
||||||
|
state->current_values_as_rgb(&red, &green, &blue, false);
|
||||||
|
auto color = lv_color_make(red * 255, green * 255, blue * 255);
|
||||||
|
if (this->obj_ != nullptr) {
|
||||||
|
this->set_value_(color);
|
||||||
|
} else {
|
||||||
|
this->initial_value_ = color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_obj(lv_obj_t *obj) {
|
||||||
|
this->obj_ = obj;
|
||||||
|
if (this->initial_value_) {
|
||||||
|
lv_led_set_color(obj, this->initial_value_.value());
|
||||||
|
lv_led_on(obj);
|
||||||
|
this->initial_value_.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void set_value_(lv_color_t value) {
|
||||||
|
lv_led_set_color(this->obj_, value);
|
||||||
|
lv_led_on(this->obj_);
|
||||||
|
lv_event_send(this->obj_, lv_custom_event, nullptr);
|
||||||
|
}
|
||||||
|
lv_obj_t *obj_{};
|
||||||
|
optional<lv_color_t> initial_value_{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace lvgl
|
||||||
|
} // namespace esphome
|
@ -146,12 +146,12 @@ LVEncoderListener::LVEncoderListener(lv_indev_type_t type, uint16_t lpt, uint16_
|
|||||||
#endif // USE_LVGL_ROTARY_ENCODER
|
#endif // USE_LVGL_ROTARY_ENCODER
|
||||||
|
|
||||||
#ifdef USE_LVGL_BUTTONMATRIX
|
#ifdef USE_LVGL_BUTTONMATRIX
|
||||||
void LvBtnmatrixType::set_obj(lv_obj_t *lv_obj) {
|
void LvButtonMatrixType::set_obj(lv_obj_t *lv_obj) {
|
||||||
LvCompound::set_obj(lv_obj);
|
LvCompound::set_obj(lv_obj);
|
||||||
lv_obj_add_event_cb(
|
lv_obj_add_event_cb(
|
||||||
lv_obj,
|
lv_obj,
|
||||||
[](lv_event_t *event) {
|
[](lv_event_t *event) {
|
||||||
auto *self = static_cast<LvBtnmatrixType *>(event->user_data);
|
auto *self = static_cast<LvButtonMatrixType *>(event->user_data);
|
||||||
if (self->key_callback_.size() == 0)
|
if (self->key_callback_.size() == 0)
|
||||||
return;
|
return;
|
||||||
auto key_idx = lv_btnmatrix_get_selected_btn(self->obj);
|
auto key_idx = lv_btnmatrix_get_selected_btn(self->obj);
|
||||||
|
@ -1,13 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
#include "esphome/core/defines.h"
|
#include "esphome/core/defines.h"
|
||||||
|
|
||||||
#ifdef USE_LVGL_BINARY_SENSOR
|
|
||||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
|
||||||
#endif // USE_LVGL_BINARY_SENSOR
|
|
||||||
#ifdef USE_LVGL_ROTARY_ENCODER
|
|
||||||
#include "esphome/components/rotary_encoder/rotary_encoder.h"
|
|
||||||
#endif // USE_LVGL_ROTARY_ENCODER
|
|
||||||
|
|
||||||
// required for clang-tidy
|
// required for clang-tidy
|
||||||
#ifndef LV_CONF_H
|
#ifndef LV_CONF_H
|
||||||
#define LV_CONF_SKIP 1 // NOLINT
|
#define LV_CONF_SKIP 1 // NOLINT
|
||||||
@ -19,6 +12,12 @@
|
|||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
#include <lvgl.h>
|
#include <lvgl.h>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#ifdef USE_LVGL_ROTARY_ENCODER
|
||||||
|
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||||
|
#include "esphome/components/rotary_encoder/rotary_encoder.h"
|
||||||
|
#endif // USE_LVGL_ROTARY_ENCODER
|
||||||
|
|
||||||
#ifdef USE_LVGL_IMAGE
|
#ifdef USE_LVGL_IMAGE
|
||||||
#include "esphome/components/image/image.h"
|
#include "esphome/components/image/image.h"
|
||||||
#endif // USE_LVGL_IMAGE
|
#endif // USE_LVGL_IMAGE
|
||||||
@ -246,7 +245,7 @@ class LVEncoderListener : public Parented<LvglComponent> {
|
|||||||
};
|
};
|
||||||
#endif // USE_LVGL_ROTARY_ENCODER
|
#endif // USE_LVGL_ROTARY_ENCODER
|
||||||
#ifdef USE_LVGL_BUTTONMATRIX
|
#ifdef USE_LVGL_BUTTONMATRIX
|
||||||
class LvBtnmatrixType : public key_provider::KeyProvider, public LvCompound {
|
class LvButtonMatrixType : public key_provider::KeyProvider, public LvCompound {
|
||||||
public:
|
public:
|
||||||
void set_obj(lv_obj_t *lv_obj) override;
|
void set_obj(lv_obj_t *lv_obj) override;
|
||||||
uint16_t get_selected() { return lv_btnmatrix_get_selected_btn(this->obj); }
|
uint16_t get_selected() { return lv_btnmatrix_get_selected_btn(this->obj); }
|
||||||
|
52
esphome/components/lvgl/number/__init__.py
Normal file
52
esphome/components/lvgl/number/__init__.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import esphome.codegen as cg
|
||||||
|
from esphome.components import number
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.cpp_generator import MockObj
|
||||||
|
|
||||||
|
from ..defines import CONF_ANIMATED, CONF_LVGL_ID, CONF_WIDGET
|
||||||
|
from ..lv_validation import animated
|
||||||
|
from ..lvcode import CUSTOM_EVENT, EVENT_ARG, LambdaContext, LvContext, lv, lv_add
|
||||||
|
from ..schemas import LVGL_SCHEMA
|
||||||
|
from ..types import LV_EVENT, LvNumber, lvgl_ns
|
||||||
|
from ..widgets import get_widgets
|
||||||
|
|
||||||
|
LVGLNumber = lvgl_ns.class_("LVGLNumber", number.Number)
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = (
|
||||||
|
number.number_schema(LVGLNumber)
|
||||||
|
.extend(LVGL_SCHEMA)
|
||||||
|
.extend(
|
||||||
|
{
|
||||||
|
cv.Required(CONF_WIDGET): cv.use_id(LvNumber),
|
||||||
|
cv.Optional(CONF_ANIMATED, default=True): animated,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
paren = await cg.get_variable(config[CONF_LVGL_ID])
|
||||||
|
widget = await get_widgets(config, CONF_WIDGET)
|
||||||
|
widget = widget[0]
|
||||||
|
var = await number.new_number(
|
||||||
|
config,
|
||||||
|
max_value=widget.get_max(),
|
||||||
|
min_value=widget.get_min(),
|
||||||
|
step=widget.get_step(),
|
||||||
|
)
|
||||||
|
|
||||||
|
async with LambdaContext([(cg.float_, "v")]) as control:
|
||||||
|
await widget.set_property(
|
||||||
|
"value", MockObj("v") * MockObj(widget.get_scale()), config[CONF_ANIMATED]
|
||||||
|
)
|
||||||
|
lv.event_send(widget.obj, CUSTOM_EVENT, cg.nullptr)
|
||||||
|
async with LambdaContext(EVENT_ARG) as event:
|
||||||
|
event.add(var.publish_state(widget.get_value()))
|
||||||
|
async with LvContext(paren):
|
||||||
|
lv_add(var.set_control_lambda(await control.get_lambda()))
|
||||||
|
lv_add(
|
||||||
|
paren.add_event_cb(
|
||||||
|
widget.obj, await event.get_lambda(), LV_EVENT.VALUE_CHANGED
|
||||||
|
)
|
||||||
|
)
|
||||||
|
lv_add(var.publish_state(widget.get_value()))
|
33
esphome/components/lvgl/number/lvgl_number.h
Normal file
33
esphome/components/lvgl/number/lvgl_number.h
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/components/number/number.h"
|
||||||
|
#include "esphome/core/automation.h"
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/core/preferences.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace lvgl {
|
||||||
|
|
||||||
|
class LVGLNumber : public number::Number {
|
||||||
|
public:
|
||||||
|
void set_control_lambda(std::function<void(float)> control_lambda) {
|
||||||
|
this->control_lambda_ = control_lambda;
|
||||||
|
if (this->initial_state_.has_value()) {
|
||||||
|
this->control_lambda_(this->initial_state_.value());
|
||||||
|
this->initial_state_.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void control(float value) {
|
||||||
|
if (this->control_lambda_ != nullptr)
|
||||||
|
this->control_lambda_(value);
|
||||||
|
else
|
||||||
|
this->initial_state_ = value;
|
||||||
|
}
|
||||||
|
std::function<void(float)> control_lambda_{};
|
||||||
|
optional<float> initial_state_{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace lvgl
|
||||||
|
} // namespace esphome
|
@ -16,7 +16,7 @@ from .helpers import lvgl_components_required
|
|||||||
from .lvcode import lv, lv_add, lv_expr
|
from .lvcode import lv, lv_add, lv_expr
|
||||||
from .schemas import ENCODER_SCHEMA
|
from .schemas import ENCODER_SCHEMA
|
||||||
from .types import lv_indev_type_t
|
from .types import lv_indev_type_t
|
||||||
from .widget import add_group
|
from .widgets import add_group
|
||||||
|
|
||||||
ROTARY_ENCODER_CONFIG = cv.ensure_list(
|
ROTARY_ENCODER_CONFIG = cv.ensure_list(
|
||||||
ENCODER_SCHEMA.extend(
|
ENCODER_SCHEMA.extend(
|
||||||
|
46
esphome/components/lvgl/select/__init__.py
Normal file
46
esphome/components/lvgl/select/__init__.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import esphome.codegen as cg
|
||||||
|
from esphome.components import select
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.const import CONF_OPTIONS
|
||||||
|
|
||||||
|
from ..defines import CONF_ANIMATED, CONF_LVGL_ID, CONF_WIDGET
|
||||||
|
from ..lvcode import CUSTOM_EVENT, EVENT_ARG, LambdaContext, LvContext, lv, lv_add
|
||||||
|
from ..schemas import LVGL_SCHEMA
|
||||||
|
from ..types import LV_EVENT, LvSelect, lvgl_ns
|
||||||
|
from ..widgets import get_widgets
|
||||||
|
|
||||||
|
LVGLSelect = lvgl_ns.class_("LVGLSelect", select.Select)
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = (
|
||||||
|
select.select_schema(LVGLSelect)
|
||||||
|
.extend(LVGL_SCHEMA)
|
||||||
|
.extend(
|
||||||
|
{
|
||||||
|
cv.Required(CONF_WIDGET): cv.use_id(LvSelect),
|
||||||
|
cv.Optional(CONF_ANIMATED, default=False): cv.boolean,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
widget = await get_widgets(config, CONF_WIDGET)
|
||||||
|
widget = widget[0]
|
||||||
|
options = widget.config.get(CONF_OPTIONS, [])
|
||||||
|
selector = await select.new_select(config, options=options)
|
||||||
|
paren = await cg.get_variable(config[CONF_LVGL_ID])
|
||||||
|
async with LambdaContext(EVENT_ARG) as pub_ctx:
|
||||||
|
pub_ctx.add(selector.publish_index(widget.get_value()))
|
||||||
|
async with LambdaContext([(cg.uint16, "v")]) as control:
|
||||||
|
await widget.set_property("selected", "v", animated=config[CONF_ANIMATED])
|
||||||
|
lv.event_send(widget.obj, CUSTOM_EVENT, cg.nullptr)
|
||||||
|
async with LvContext(paren) as ctx:
|
||||||
|
lv_add(selector.set_control_lambda(await control.get_lambda()))
|
||||||
|
ctx.add(
|
||||||
|
paren.add_event_cb(
|
||||||
|
widget.obj,
|
||||||
|
await pub_ctx.get_lambda(),
|
||||||
|
LV_EVENT.VALUE_CHANGED,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
lv_add(selector.publish_index(widget.get_value()))
|
62
esphome/components/lvgl/select/lvgl_select.h
Normal file
62
esphome/components/lvgl/select/lvgl_select.h
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/components/select/select.h"
|
||||||
|
#include "esphome/core/automation.h"
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/core/preferences.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace lvgl {
|
||||||
|
|
||||||
|
static std::vector<std::string> split_string(const std::string &str) {
|
||||||
|
std::vector<std::string> strings;
|
||||||
|
auto delimiter = std::string("\n");
|
||||||
|
|
||||||
|
std::string::size_type pos;
|
||||||
|
std::string::size_type prev = 0;
|
||||||
|
while ((pos = str.find(delimiter, prev)) != std::string::npos) {
|
||||||
|
strings.push_back(str.substr(prev, pos - prev));
|
||||||
|
prev = pos + delimiter.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
// To get the last substring (or only, if delimiter is not found)
|
||||||
|
strings.push_back(str.substr(prev));
|
||||||
|
|
||||||
|
return strings;
|
||||||
|
}
|
||||||
|
|
||||||
|
class LVGLSelect : public select::Select {
|
||||||
|
public:
|
||||||
|
void set_control_lambda(std::function<void(size_t)> lambda) {
|
||||||
|
this->control_lambda_ = lambda;
|
||||||
|
if (this->initial_state_.has_value()) {
|
||||||
|
this->control(this->initial_state_.value());
|
||||||
|
this->initial_state_.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void publish_index(size_t index) {
|
||||||
|
auto value = this->at(index);
|
||||||
|
if (value)
|
||||||
|
this->publish_state(value.value());
|
||||||
|
}
|
||||||
|
|
||||||
|
void set_options(const char *str) { this->traits.set_options(split_string(str)); }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void control(const std::string &value) override {
|
||||||
|
if (this->control_lambda_ != nullptr) {
|
||||||
|
auto index = index_of(value);
|
||||||
|
if (index)
|
||||||
|
this->control_lambda_(index.value());
|
||||||
|
} else {
|
||||||
|
this->initial_state_ = value.c_str();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::function<void(size_t)> control_lambda_{};
|
||||||
|
optional<const char *> initial_state_{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace lvgl
|
||||||
|
} // namespace esphome
|
35
esphome/components/lvgl/sensor/__init__.py
Normal file
35
esphome/components/lvgl/sensor/__init__.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import esphome.codegen as cg
|
||||||
|
from esphome.components.sensor import Sensor, new_sensor, sensor_schema
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
|
||||||
|
from ..defines import CONF_LVGL_ID, CONF_WIDGET
|
||||||
|
from ..lvcode import EVENT_ARG, LVGL_COMP_ARG, LambdaContext, LvContext, lv_add
|
||||||
|
from ..schemas import LVGL_SCHEMA
|
||||||
|
from ..types import LV_EVENT, LvNumber
|
||||||
|
from ..widgets import Widget, get_widgets
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = (
|
||||||
|
sensor_schema(Sensor)
|
||||||
|
.extend(LVGL_SCHEMA)
|
||||||
|
.extend(
|
||||||
|
{
|
||||||
|
cv.Required(CONF_WIDGET): cv.use_id(LvNumber),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
sensor = await new_sensor(config)
|
||||||
|
paren = await cg.get_variable(config[CONF_LVGL_ID])
|
||||||
|
widget = await get_widgets(config, CONF_WIDGET)
|
||||||
|
widget = widget[0]
|
||||||
|
assert isinstance(widget, Widget)
|
||||||
|
async with LambdaContext(EVENT_ARG) as lamb:
|
||||||
|
lv_add(sensor.publish_state(widget.get_value()))
|
||||||
|
async with LvContext(paren, LVGL_COMP_ARG):
|
||||||
|
lv_add(
|
||||||
|
paren.add_event_cb(
|
||||||
|
widget.obj, await lamb.get_lambda(), LV_EVENT.VALUE_CHANGED
|
||||||
|
)
|
||||||
|
)
|
@ -12,10 +12,10 @@ from .defines import (
|
|||||||
)
|
)
|
||||||
from .helpers import add_lv_use
|
from .helpers import add_lv_use
|
||||||
from .lvcode import LambdaContext, LocalVariable, lv, lv_assign, lv_variable
|
from .lvcode import LambdaContext, LocalVariable, lv, lv_assign, lv_variable
|
||||||
from .obj import obj_spec
|
|
||||||
from .schemas import ALL_STYLES
|
from .schemas import ALL_STYLES
|
||||||
from .types import lv_lambda_t, lv_obj_t, lv_obj_t_ptr
|
from .types import lv_lambda_t, lv_obj_t, lv_obj_t_ptr
|
||||||
from .widget import Widget, add_widgets, set_obj_properties, theme_widget_map
|
from .widgets import Widget, add_widgets, set_obj_properties, theme_widget_map
|
||||||
|
from .widgets.obj import obj_spec
|
||||||
|
|
||||||
TOP_LAYER = literal("lv_disp_get_layer_top(lv_component->get_disp())")
|
TOP_LAYER = literal("lv_disp_get_layer_top(lv_component->get_disp())")
|
||||||
|
|
||||||
@ -26,7 +26,7 @@ async def styles_to_code(config):
|
|||||||
svar = cg.new_Pvariable(style[CONF_ID])
|
svar = cg.new_Pvariable(style[CONF_ID])
|
||||||
lv.style_init(svar)
|
lv.style_init(svar)
|
||||||
for prop, validator in ALL_STYLES.items():
|
for prop, validator in ALL_STYLES.items():
|
||||||
if value := style.get(prop):
|
if (value := style.get(prop)) is not None:
|
||||||
if isinstance(validator, LValidator):
|
if isinstance(validator, LValidator):
|
||||||
value = await validator.process(value)
|
value = await validator.process(value)
|
||||||
if isinstance(value, list):
|
if isinstance(value, list):
|
||||||
|
54
esphome/components/lvgl/switch/__init__.py
Normal file
54
esphome/components/lvgl/switch/__init__.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import esphome.codegen as cg
|
||||||
|
from esphome.components.switch import Switch, new_switch, switch_schema
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.cpp_generator import MockObj
|
||||||
|
|
||||||
|
from ..defines import CONF_LVGL_ID, CONF_WIDGET
|
||||||
|
from ..lvcode import (
|
||||||
|
CUSTOM_EVENT,
|
||||||
|
EVENT_ARG,
|
||||||
|
LambdaContext,
|
||||||
|
LvConditional,
|
||||||
|
LvContext,
|
||||||
|
lv,
|
||||||
|
lv_add,
|
||||||
|
)
|
||||||
|
from ..schemas import LVGL_SCHEMA
|
||||||
|
from ..types import LV_EVENT, LV_STATE, lv_pseudo_button_t, lvgl_ns
|
||||||
|
from ..widgets import get_widgets
|
||||||
|
|
||||||
|
LVGLSwitch = lvgl_ns.class_("LVGLSwitch", Switch)
|
||||||
|
CONFIG_SCHEMA = (
|
||||||
|
switch_schema(LVGLSwitch)
|
||||||
|
.extend(LVGL_SCHEMA)
|
||||||
|
.extend(
|
||||||
|
{
|
||||||
|
cv.Required(CONF_WIDGET): cv.use_id(lv_pseudo_button_t),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
switch = await new_switch(config)
|
||||||
|
paren = await cg.get_variable(config[CONF_LVGL_ID])
|
||||||
|
widget = await get_widgets(config, CONF_WIDGET)
|
||||||
|
widget = widget[0]
|
||||||
|
async with LambdaContext(EVENT_ARG) as checked_ctx:
|
||||||
|
checked_ctx.add(switch.publish_state(widget.get_value()))
|
||||||
|
async with LambdaContext([(cg.bool_, "v")]) as control:
|
||||||
|
with LvConditional(MockObj("v")) as cond:
|
||||||
|
widget.add_state(LV_STATE.CHECKED)
|
||||||
|
cond.else_()
|
||||||
|
widget.clear_state(LV_STATE.CHECKED)
|
||||||
|
lv.event_send(widget.obj, CUSTOM_EVENT, cg.nullptr)
|
||||||
|
async with LvContext(paren) as ctx:
|
||||||
|
lv_add(switch.set_control_lambda(await control.get_lambda()))
|
||||||
|
ctx.add(
|
||||||
|
paren.add_event_cb(
|
||||||
|
widget.obj,
|
||||||
|
await checked_ctx.get_lambda(),
|
||||||
|
LV_EVENT.VALUE_CHANGED,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
lv_add(switch.publish_state(widget.get_value()))
|
33
esphome/components/lvgl/switch/lvgl_switch.h
Normal file
33
esphome/components/lvgl/switch/lvgl_switch.h
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/components/switch/switch.h"
|
||||||
|
#include "esphome/core/automation.h"
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/core/preferences.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace lvgl {
|
||||||
|
|
||||||
|
class LVGLSwitch : public switch_::Switch {
|
||||||
|
public:
|
||||||
|
void set_control_lambda(std::function<void(bool)> state_lambda) {
|
||||||
|
this->state_lambda_ = state_lambda;
|
||||||
|
if (this->initial_state_.has_value()) {
|
||||||
|
this->state_lambda_(this->initial_state_.value());
|
||||||
|
this->initial_state_.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void write_state(bool value) {
|
||||||
|
if (this->state_lambda_ != nullptr)
|
||||||
|
this->state_lambda_(value);
|
||||||
|
else
|
||||||
|
this->initial_state_ = value;
|
||||||
|
}
|
||||||
|
std::function<void(bool)> state_lambda_{};
|
||||||
|
optional<bool> initial_state_{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace lvgl
|
||||||
|
} // namespace esphome
|
39
esphome/components/lvgl/text/__init__.py
Normal file
39
esphome/components/lvgl/text/__init__.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import esphome.codegen as cg
|
||||||
|
from esphome.components import text
|
||||||
|
from esphome.components.text import new_text
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
|
||||||
|
from ..defines import CONF_LVGL_ID, CONF_WIDGET
|
||||||
|
from ..lvcode import CUSTOM_EVENT, EVENT_ARG, LambdaContext, LvContext, lv, lv_add
|
||||||
|
from ..schemas import LVGL_SCHEMA
|
||||||
|
from ..types import LV_EVENT, LvText, lvgl_ns
|
||||||
|
from ..widgets import get_widgets
|
||||||
|
|
||||||
|
LVGLText = lvgl_ns.class_("LVGLText", text.Text)
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = text.TEXT_SCHEMA.extend(LVGL_SCHEMA).extend(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(LVGLText),
|
||||||
|
cv.Required(CONF_WIDGET): cv.use_id(LvText),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
textvar = await new_text(config)
|
||||||
|
paren = await cg.get_variable(config[CONF_LVGL_ID])
|
||||||
|
widget = await get_widgets(config, CONF_WIDGET)
|
||||||
|
widget = widget[0]
|
||||||
|
async with LambdaContext([(cg.std_string, "text_value")]) as control:
|
||||||
|
await widget.set_property("text", "text_value.c_str())")
|
||||||
|
lv.event_send(widget.obj, CUSTOM_EVENT, None)
|
||||||
|
async with LambdaContext(EVENT_ARG) as lamb:
|
||||||
|
lv_add(textvar.publish_state(widget.get_value()))
|
||||||
|
async with LvContext(paren):
|
||||||
|
widget.var.set_control_lambda(await control.get_lambda())
|
||||||
|
lv_add(
|
||||||
|
paren.add_event_cb(
|
||||||
|
widget.obj, await lamb.get_lambda(), LV_EVENT.VALUE_CHANGED
|
||||||
|
)
|
||||||
|
)
|
||||||
|
lv_add(textvar.publish_state(widget.get_value()))
|
33
esphome/components/lvgl/text/lvgl_text.h
Normal file
33
esphome/components/lvgl/text/lvgl_text.h
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/components/text/text.h"
|
||||||
|
#include "esphome/core/automation.h"
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/core/preferences.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace lvgl {
|
||||||
|
|
||||||
|
class LVGLText : public text::Text {
|
||||||
|
public:
|
||||||
|
void set_control_lambda(std::function<void(const std::string)> control_lambda) {
|
||||||
|
this->control_lambda_ = control_lambda;
|
||||||
|
if (this->initial_state_.has_value()) {
|
||||||
|
this->control_lambda_(this->initial_state_.value());
|
||||||
|
this->initial_state_.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void control(const std::string &value) {
|
||||||
|
if (this->control_lambda_ != nullptr)
|
||||||
|
this->control_lambda_(value);
|
||||||
|
else
|
||||||
|
this->initial_state_ = value;
|
||||||
|
}
|
||||||
|
std::function<void(const std::string)> control_lambda_{};
|
||||||
|
optional<std::string> initial_state_{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace lvgl
|
||||||
|
} // namespace esphome
|
40
esphome/components/lvgl/text_sensor/__init__.py
Normal file
40
esphome/components/lvgl/text_sensor/__init__.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import esphome.codegen as cg
|
||||||
|
from esphome.components.text_sensor import (
|
||||||
|
TextSensor,
|
||||||
|
new_text_sensor,
|
||||||
|
text_sensor_schema,
|
||||||
|
)
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
|
||||||
|
from ..defines import CONF_LVGL_ID, CONF_WIDGET
|
||||||
|
from ..lvcode import EVENT_ARG, LambdaContext, LvContext
|
||||||
|
from ..schemas import LVGL_SCHEMA
|
||||||
|
from ..types import LV_EVENT, LvText
|
||||||
|
from ..widgets import get_widgets
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = (
|
||||||
|
text_sensor_schema(TextSensor)
|
||||||
|
.extend(LVGL_SCHEMA)
|
||||||
|
.extend(
|
||||||
|
{
|
||||||
|
cv.Required(CONF_WIDGET): cv.use_id(LvText),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
sensor = await new_text_sensor(config)
|
||||||
|
paren = await cg.get_variable(config[CONF_LVGL_ID])
|
||||||
|
widget = await get_widgets(config, CONF_WIDGET)
|
||||||
|
widget = widget[0]
|
||||||
|
async with LambdaContext(EVENT_ARG) as pressed_ctx:
|
||||||
|
pressed_ctx.add(sensor.publish_state(widget.get_value()))
|
||||||
|
async with LvContext(paren) as ctx:
|
||||||
|
ctx.add(
|
||||||
|
paren.add_event_cb(
|
||||||
|
widget.obj,
|
||||||
|
await pressed_ctx.get_lambda(),
|
||||||
|
LV_EVENT.VALUE_CHANGED,
|
||||||
|
)
|
||||||
|
)
|
@ -34,7 +34,7 @@ def touchscreen_schema(config):
|
|||||||
|
|
||||||
|
|
||||||
async def touchscreens_to_code(var, config):
|
async def touchscreens_to_code(var, config):
|
||||||
for tconf in config.get(CONF_TOUCHSCREENS) or ():
|
for tconf in config.get(CONF_TOUCHSCREENS, ()):
|
||||||
lvgl_components_required.add(CONF_TOUCHSCREEN)
|
lvgl_components_required.add(CONF_TOUCHSCREEN)
|
||||||
touchscreen = await cg.get_variable(tconf[CONF_TOUCHSCREEN_ID])
|
touchscreen = await cg.get_variable(tconf[CONF_TOUCHSCREEN_ID])
|
||||||
lpt = tconf[CONF_LONG_PRESS_TIME].total_milliseconds
|
lpt = tconf[CONF_LONG_PRESS_TIME].total_milliseconds
|
||||||
|
@ -13,7 +13,7 @@ from .defines import (
|
|||||||
)
|
)
|
||||||
from .lvcode import EVENT_ARG, LambdaContext, LvConditional, lv, lv_add
|
from .lvcode import EVENT_ARG, LambdaContext, LvConditional, lv, lv_add
|
||||||
from .types import LV_EVENT
|
from .types import LV_EVENT
|
||||||
from .widget import widget_map
|
from .widgets import widget_map
|
||||||
|
|
||||||
|
|
||||||
async def generate_triggers(lv_component):
|
async def generate_triggers(lv_component):
|
||||||
|
@ -8,7 +8,7 @@ from esphome.core import ID, TimePeriod
|
|||||||
from esphome.coroutine import FakeAwaitable
|
from esphome.coroutine import FakeAwaitable
|
||||||
from esphome.cpp_generator import AssignmentExpression, CallExpression, MockObj
|
from esphome.cpp_generator import AssignmentExpression, CallExpression, MockObj
|
||||||
|
|
||||||
from .defines import (
|
from ..defines import (
|
||||||
CONF_DEFAULT,
|
CONF_DEFAULT,
|
||||||
CONF_FLEX_ALIGN_CROSS,
|
CONF_FLEX_ALIGN_CROSS,
|
||||||
CONF_FLEX_ALIGN_MAIN,
|
CONF_FLEX_ALIGN_MAIN,
|
||||||
@ -32,8 +32,8 @@ from .defines import (
|
|||||||
join_enums,
|
join_enums,
|
||||||
literal,
|
literal,
|
||||||
)
|
)
|
||||||
from .helpers import add_lv_use
|
from ..helpers import add_lv_use
|
||||||
from .lvcode import (
|
from ..lvcode import (
|
||||||
LvConditional,
|
LvConditional,
|
||||||
add_line_marks,
|
add_line_marks,
|
||||||
lv,
|
lv,
|
||||||
@ -43,8 +43,8 @@ from .lvcode import (
|
|||||||
lv_obj,
|
lv_obj,
|
||||||
lv_Pvariable,
|
lv_Pvariable,
|
||||||
)
|
)
|
||||||
from .schemas import ALL_STYLES, STYLE_REMAP, WIDGET_TYPES
|
from ..schemas import ALL_STYLES, STYLE_REMAP, WIDGET_TYPES
|
||||||
from .types import (
|
from ..types import (
|
||||||
LV_STATE,
|
LV_STATE,
|
||||||
LvType,
|
LvType,
|
||||||
WidgetType,
|
WidgetType,
|
||||||
@ -282,13 +282,13 @@ async def set_obj_properties(w: Widget, config):
|
|||||||
lv_obj.set_layout(w.obj, literal(f"LV_LAYOUT_{layout_type.upper()}"))
|
lv_obj.set_layout(w.obj, literal(f"LV_LAYOUT_{layout_type.upper()}"))
|
||||||
if layout_type == TYPE_GRID:
|
if layout_type == TYPE_GRID:
|
||||||
wid = config[CONF_ID]
|
wid = config[CONF_ID]
|
||||||
rows = "{" + ",".join(layout[CONF_GRID_ROWS]) + ", LV_GRID_TEMPLATE_LAST}"
|
rows = [str(x) for x in layout[CONF_GRID_ROWS]]
|
||||||
|
rows = "{" + ",".join(rows) + ", LV_GRID_TEMPLATE_LAST}"
|
||||||
row_id = ID(f"{wid}_row_dsc", is_declaration=True, type=lv_coord_t)
|
row_id = ID(f"{wid}_row_dsc", is_declaration=True, type=lv_coord_t)
|
||||||
row_array = cg.static_const_array(row_id, cg.RawExpression(rows))
|
row_array = cg.static_const_array(row_id, cg.RawExpression(rows))
|
||||||
w.set_style("grid_row_dsc_array", row_array, 0)
|
w.set_style("grid_row_dsc_array", row_array, 0)
|
||||||
columns = (
|
columns = [str(x) for x in layout[CONF_GRID_COLUMNS]]
|
||||||
"{" + ",".join(layout[CONF_GRID_COLUMNS]) + ", LV_GRID_TEMPLATE_LAST}"
|
columns = "{" + ",".join(columns) + ", LV_GRID_TEMPLATE_LAST}"
|
||||||
)
|
|
||||||
column_id = ID(f"{wid}_column_dsc", is_declaration=True, type=lv_coord_t)
|
column_id = ID(f"{wid}_column_dsc", is_declaration=True, type=lv_coord_t)
|
||||||
column_array = cg.static_const_array(column_id, cg.RawExpression(columns))
|
column_array = cg.static_const_array(column_id, cg.RawExpression(columns))
|
||||||
w.set_style("grid_column_dsc_array", column_array, 0)
|
w.set_style("grid_column_dsc_array", column_array, 0)
|
||||||
@ -368,7 +368,7 @@ async def add_widgets(parent: Widget, config: dict):
|
|||||||
:param config: The configuration
|
:param config: The configuration
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
for w in config.get(CONF_WIDGETS) or ():
|
for w in config.get(CONF_WIDGETS, ()):
|
||||||
w_type, w_cnfig = next(iter(w.items()))
|
w_type, w_cnfig = next(iter(w.items()))
|
||||||
await widget_to_code(w_cnfig, w_type, parent.obj)
|
await widget_to_code(w_cnfig, w_type, parent.obj)
|
||||||
|
|
@ -2,17 +2,17 @@ from esphome import automation
|
|||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_DURATION, CONF_ID
|
from esphome.const import CONF_DURATION, CONF_ID
|
||||||
|
from esphome.cpp_generator import MockObj
|
||||||
|
|
||||||
from ...cpp_generator import MockObj
|
from ..automation import action_to_code
|
||||||
from .automation import action_to_code
|
from ..defines import CONF_AUTO_START, CONF_MAIN, CONF_REPEAT_COUNT, CONF_SRC
|
||||||
from .defines import CONF_AUTO_START, CONF_MAIN, CONF_REPEAT_COUNT, CONF_SRC
|
from ..helpers import lvgl_components_required
|
||||||
from .helpers import lvgl_components_required
|
from ..lv_validation import lv_image, lv_milliseconds
|
||||||
|
from ..lvcode import lv, lv_expr
|
||||||
|
from ..types import LvType, ObjUpdateAction, void_ptr
|
||||||
|
from . import Widget, WidgetType, get_widgets
|
||||||
from .img import CONF_IMAGE
|
from .img import CONF_IMAGE
|
||||||
from .label import CONF_LABEL
|
from .label import CONF_LABEL
|
||||||
from .lv_validation import lv_image, lv_milliseconds
|
|
||||||
from .lvcode import lv, lv_expr
|
|
||||||
from .types import LvType, ObjUpdateAction, void_ptr
|
|
||||||
from .widget import Widget, WidgetType, get_widgets
|
|
||||||
|
|
||||||
CONF_ANIMIMG = "animimg"
|
CONF_ANIMIMG = "animimg"
|
||||||
CONF_SRC_LIST_ID = "src_list_id"
|
CONF_SRC_LIST_ID = "src_list_id"
|
@ -8,7 +8,7 @@ from esphome.const import (
|
|||||||
)
|
)
|
||||||
from esphome.cpp_types import nullptr
|
from esphome.cpp_types import nullptr
|
||||||
|
|
||||||
from .defines import (
|
from ..defines import (
|
||||||
ARC_MODES,
|
ARC_MODES,
|
||||||
CONF_ADJUSTABLE,
|
CONF_ADJUSTABLE,
|
||||||
CONF_CHANGE_RATE,
|
CONF_CHANGE_RATE,
|
||||||
@ -19,10 +19,10 @@ from .defines import (
|
|||||||
CONF_START_ANGLE,
|
CONF_START_ANGLE,
|
||||||
literal,
|
literal,
|
||||||
)
|
)
|
||||||
from .lv_validation import angle, get_start_value, lv_float
|
from ..lv_validation import angle, get_start_value, lv_float
|
||||||
from .lvcode import lv, lv_obj
|
from ..lvcode import lv, lv_obj
|
||||||
from .types import LvNumber, NumberType
|
from ..types import LvNumber, NumberType
|
||||||
from .widget import Widget
|
from . import Widget
|
||||||
|
|
||||||
CONF_ARC = "arc"
|
CONF_ARC = "arc"
|
||||||
ARC_SCHEMA = cv.Schema(
|
ARC_SCHEMA = cv.Schema(
|
20
esphome/components/lvgl/widgets/button.py
Normal file
20
esphome/components/lvgl/widgets/button.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
from esphome.const import CONF_BUTTON
|
||||||
|
|
||||||
|
from ..defines import CONF_MAIN
|
||||||
|
from ..types import LvBoolean, WidgetType
|
||||||
|
|
||||||
|
lv_button_t = LvBoolean("lv_btn_t")
|
||||||
|
|
||||||
|
|
||||||
|
class ButtonType(WidgetType):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(CONF_BUTTON, lv_button_t, (CONF_MAIN,), lv_name="btn")
|
||||||
|
|
||||||
|
def get_uses(self):
|
||||||
|
return ("btn",)
|
||||||
|
|
||||||
|
async def to_code(self, w, config):
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
button_spec = ButtonType()
|
277
esphome/components/lvgl/widgets/buttonmatrix.py
Normal file
277
esphome/components/lvgl/widgets/buttonmatrix.py
Normal file
@ -0,0 +1,277 @@
|
|||||||
|
from esphome import automation
|
||||||
|
import esphome.codegen as cg
|
||||||
|
from esphome.components.key_provider import KeyProvider
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.const import CONF_ID, CONF_WIDTH
|
||||||
|
from esphome.cpp_generator import MockObj
|
||||||
|
|
||||||
|
from ..automation import action_to_code
|
||||||
|
from ..defines import (
|
||||||
|
BUTTONMATRIX_CTRLS,
|
||||||
|
CONF_BUTTONS,
|
||||||
|
CONF_CONTROL,
|
||||||
|
CONF_ITEMS,
|
||||||
|
CONF_KEY_CODE,
|
||||||
|
CONF_MAIN,
|
||||||
|
CONF_ONE_CHECKED,
|
||||||
|
CONF_ROWS,
|
||||||
|
CONF_SELECTED,
|
||||||
|
CONF_TEXT,
|
||||||
|
)
|
||||||
|
from ..helpers import lvgl_components_required
|
||||||
|
from ..lv_validation import key_code, lv_bool
|
||||||
|
from ..lvcode import lv, lv_add, lv_expr
|
||||||
|
from ..schemas import automation_schema
|
||||||
|
from ..types import (
|
||||||
|
LV_BTNMATRIX_CTRL,
|
||||||
|
LV_STATE,
|
||||||
|
LvBoolean,
|
||||||
|
LvCompound,
|
||||||
|
LvType,
|
||||||
|
ObjUpdateAction,
|
||||||
|
char_ptr,
|
||||||
|
lv_pseudo_button_t,
|
||||||
|
)
|
||||||
|
from . import Widget, WidgetType, get_widgets, widget_map
|
||||||
|
from .button import lv_button_t
|
||||||
|
|
||||||
|
CONF_BUTTONMATRIX = "buttonmatrix"
|
||||||
|
CONF_BUTTON_TEXT_LIST_ID = "button_text_list_id"
|
||||||
|
|
||||||
|
LvButtonMatrixButton = LvBoolean(
|
||||||
|
str(cg.uint16),
|
||||||
|
parents=(lv_pseudo_button_t,),
|
||||||
|
)
|
||||||
|
BUTTONMATRIX_BUTTON_SCHEMA = cv.Schema(
|
||||||
|
{
|
||||||
|
cv.Optional(CONF_TEXT): cv.string,
|
||||||
|
cv.Optional(CONF_KEY_CODE): key_code,
|
||||||
|
cv.GenerateID(): cv.declare_id(LvButtonMatrixButton),
|
||||||
|
cv.Optional(CONF_WIDTH, default=1): cv.positive_int,
|
||||||
|
cv.Optional(CONF_CONTROL): cv.ensure_list(
|
||||||
|
cv.Schema(
|
||||||
|
{cv.Optional(k.lower()): cv.boolean for k in BUTTONMATRIX_CTRLS.choices}
|
||||||
|
)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
).extend(automation_schema(lv_button_t))
|
||||||
|
|
||||||
|
BUTTONMATRIX_SCHEMA = cv.Schema(
|
||||||
|
{
|
||||||
|
cv.Optional(CONF_ONE_CHECKED, default=False): lv_bool,
|
||||||
|
cv.GenerateID(CONF_BUTTON_TEXT_LIST_ID): cv.declare_id(char_ptr),
|
||||||
|
cv.Required(CONF_ROWS): cv.ensure_list(
|
||||||
|
cv.Schema(
|
||||||
|
{
|
||||||
|
cv.Required(CONF_BUTTONS): cv.ensure_list(
|
||||||
|
BUTTONMATRIX_BUTTON_SCHEMA
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ButtonmatrixButtonType(WidgetType):
|
||||||
|
"""
|
||||||
|
A pseudo-widget for the matrix buttons
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__("btnmatrix_btn", LvButtonMatrixButton, (), {}, {})
|
||||||
|
|
||||||
|
async def to_code(self, w, config: dict):
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
btn_btn_spec = ButtonmatrixButtonType()
|
||||||
|
|
||||||
|
|
||||||
|
class MatrixButton(Widget):
|
||||||
|
"""
|
||||||
|
Describes a button within a button matrix.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_button(id, parent, config: dict, index):
|
||||||
|
w = MatrixButton(id, parent, config, index)
|
||||||
|
widget_map[id] = w
|
||||||
|
return w
|
||||||
|
|
||||||
|
def __init__(self, id, parent: Widget, config, index):
|
||||||
|
super().__init__(id, btn_btn_spec, config)
|
||||||
|
self.parent = parent
|
||||||
|
self.index = index
|
||||||
|
self.obj = parent.obj
|
||||||
|
|
||||||
|
def is_selected(self):
|
||||||
|
return self.parent.var.get_selected() == MockObj(self.var)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def map_ctrls(state):
|
||||||
|
state = str(state).upper().removeprefix("LV_STATE_")
|
||||||
|
assert state in BUTTONMATRIX_CTRLS.choices
|
||||||
|
return getattr(LV_BTNMATRIX_CTRL, state)
|
||||||
|
|
||||||
|
def has_state(self, state):
|
||||||
|
state = self.map_ctrls(state)
|
||||||
|
return lv_expr.btnmatrix_has_btn_ctrl(self.obj, self.index, state)
|
||||||
|
|
||||||
|
def add_state(self, state):
|
||||||
|
state = self.map_ctrls(state)
|
||||||
|
return lv.btnmatrix_set_btn_ctrl(self.obj, self.index, state)
|
||||||
|
|
||||||
|
def clear_state(self, state):
|
||||||
|
state = self.map_ctrls(state)
|
||||||
|
return lv.btnmatrix_clear_btn_ctrl(self.obj, self.index, state)
|
||||||
|
|
||||||
|
def is_pressed(self):
|
||||||
|
return self.is_selected() & self.parent.has_state(LV_STATE.PRESSED)
|
||||||
|
|
||||||
|
def is_checked(self):
|
||||||
|
return self.has_state(LV_STATE.CHECKED)
|
||||||
|
|
||||||
|
def get_value(self):
|
||||||
|
return self.is_checked()
|
||||||
|
|
||||||
|
def check_null(self):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
async def get_button_data(config, buttonmatrix: Widget):
|
||||||
|
"""
|
||||||
|
Process a button matrix button list
|
||||||
|
:param config: The row list
|
||||||
|
:param buttonmatrix: The parent variable
|
||||||
|
:return: text array id, control list, width list
|
||||||
|
"""
|
||||||
|
text_list = []
|
||||||
|
ctrl_list = []
|
||||||
|
width_list = []
|
||||||
|
key_list = []
|
||||||
|
for row in config:
|
||||||
|
for button_conf in row.get(CONF_BUTTONS, ()):
|
||||||
|
bid = button_conf[CONF_ID]
|
||||||
|
index = len(width_list)
|
||||||
|
MatrixButton.create_button(bid, buttonmatrix, button_conf, index)
|
||||||
|
cg.new_variable(bid, index)
|
||||||
|
text_list.append(button_conf.get(CONF_TEXT) or "")
|
||||||
|
key_list.append(button_conf.get(CONF_KEY_CODE) or 0)
|
||||||
|
width_list.append(button_conf[CONF_WIDTH])
|
||||||
|
ctrl = ["LV_BTNMATRIX_CTRL_CLICK_TRIG"]
|
||||||
|
for item in button_conf.get(CONF_CONTROL, ()):
|
||||||
|
ctrl.extend([k for k, v in item.items() if v])
|
||||||
|
ctrl_list.append(await BUTTONMATRIX_CTRLS.process(ctrl))
|
||||||
|
text_list.append("\n")
|
||||||
|
text_list = text_list[:-1]
|
||||||
|
text_list.append(cg.nullptr)
|
||||||
|
return text_list, ctrl_list, width_list, key_list
|
||||||
|
|
||||||
|
|
||||||
|
lv_buttonmatrix_t = LvType(
|
||||||
|
"LvButtonMatrixType",
|
||||||
|
parents=(KeyProvider, LvCompound),
|
||||||
|
largs=[(cg.uint16, "x")],
|
||||||
|
lvalue=lambda w: w.var.get_selected(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ButtonMatrixType(WidgetType):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(
|
||||||
|
CONF_BUTTONMATRIX,
|
||||||
|
lv_buttonmatrix_t,
|
||||||
|
(CONF_MAIN, CONF_ITEMS),
|
||||||
|
BUTTONMATRIX_SCHEMA,
|
||||||
|
{},
|
||||||
|
lv_name="btnmatrix",
|
||||||
|
)
|
||||||
|
|
||||||
|
async def to_code(self, w: Widget, config):
|
||||||
|
lvgl_components_required.add("BUTTONMATRIX")
|
||||||
|
if CONF_ROWS not in config:
|
||||||
|
return []
|
||||||
|
text_list, ctrl_list, width_list, key_list = await get_button_data(
|
||||||
|
config[CONF_ROWS], w
|
||||||
|
)
|
||||||
|
text_id = config[CONF_BUTTON_TEXT_LIST_ID]
|
||||||
|
text_id = cg.static_const_array(text_id, text_list)
|
||||||
|
lv.btnmatrix_set_map(w.obj, text_id)
|
||||||
|
set_btn_data(w.obj, ctrl_list, width_list)
|
||||||
|
lv.btnmatrix_set_one_checked(w.obj, config[CONF_ONE_CHECKED])
|
||||||
|
for index, key in enumerate(key_list):
|
||||||
|
if key != 0:
|
||||||
|
lv_add(w.var.set_key(index, key))
|
||||||
|
|
||||||
|
def get_uses(self):
|
||||||
|
return ("btnmatrix",)
|
||||||
|
|
||||||
|
|
||||||
|
def set_btn_data(obj, ctrl_list, width_list):
|
||||||
|
for index, ctrl in enumerate(ctrl_list):
|
||||||
|
lv.btnmatrix_set_btn_ctrl(obj, index, ctrl)
|
||||||
|
for index, width in enumerate(width_list):
|
||||||
|
lv.btnmatrix_set_btn_width(obj, index, width)
|
||||||
|
|
||||||
|
|
||||||
|
buttonmatrix_spec = ButtonMatrixType()
|
||||||
|
|
||||||
|
|
||||||
|
@automation.register_action(
|
||||||
|
"lvgl.matrix.button.update",
|
||||||
|
ObjUpdateAction,
|
||||||
|
cv.Schema(
|
||||||
|
{
|
||||||
|
cv.Optional(CONF_WIDTH): cv.positive_int,
|
||||||
|
cv.Optional(CONF_CONTROL): cv.ensure_list(
|
||||||
|
cv.Schema(
|
||||||
|
{
|
||||||
|
cv.Optional(k.lower()): cv.boolean
|
||||||
|
for k in BUTTONMATRIX_CTRLS.choices
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
cv.Required(CONF_ID): cv.ensure_list(
|
||||||
|
cv.maybe_simple_value(
|
||||||
|
{
|
||||||
|
cv.Required(CONF_ID): cv.use_id(LvButtonMatrixButton),
|
||||||
|
},
|
||||||
|
key=CONF_ID,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
cv.Optional(CONF_SELECTED): lv_bool,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
async def button_update_to_code(config, action_id, template_arg, args):
|
||||||
|
widgets = await get_widgets(config[CONF_ID])
|
||||||
|
assert all(isinstance(w, MatrixButton) for w in widgets)
|
||||||
|
|
||||||
|
async def do_button_update(w: MatrixButton):
|
||||||
|
if (width := config.get(CONF_WIDTH)) is not None:
|
||||||
|
lv.btnmatrix_set_btn_width(w.obj, w.index, width)
|
||||||
|
if config.get(CONF_SELECTED):
|
||||||
|
lv.btnmatrix_set_selected_btn(w.obj, w.index)
|
||||||
|
if controls := config.get(CONF_CONTROL):
|
||||||
|
adds = []
|
||||||
|
clrs = []
|
||||||
|
for item in controls:
|
||||||
|
adds.extend(
|
||||||
|
[f"LV_BTNMATRIX_CTRL_{k.upper()}" for k, v in item.items() if v]
|
||||||
|
)
|
||||||
|
clrs.extend(
|
||||||
|
[f"LV_BTNMATRIX_CTRL_{k.upper()}" for k, v in item.items() if not v]
|
||||||
|
)
|
||||||
|
if adds:
|
||||||
|
lv.btnmatrix_set_btn_ctrl(
|
||||||
|
w.obj, w.index, await BUTTONMATRIX_CTRLS.process(adds)
|
||||||
|
)
|
||||||
|
if clrs:
|
||||||
|
lv.btnmatrix_clear_btn_ctrl(
|
||||||
|
w.obj, w.index, await BUTTONMATRIX_CTRLS.process(clrs)
|
||||||
|
)
|
||||||
|
|
||||||
|
return await action_to_code(
|
||||||
|
widgets, do_button_update, action_id, template_arg, args
|
||||||
|
)
|
@ -1,9 +1,9 @@
|
|||||||
from .defines import CONF_INDICATOR, CONF_MAIN, CONF_TEXT
|
from ..defines import CONF_INDICATOR, CONF_MAIN, CONF_TEXT
|
||||||
from .lv_validation import lv_text
|
from ..lv_validation import lv_text
|
||||||
from .lvcode import lv
|
from ..lvcode import lv
|
||||||
from .schemas import TEXT_SCHEMA
|
from ..schemas import TEXT_SCHEMA
|
||||||
from .types import LvBoolean
|
from ..types import LvBoolean
|
||||||
from .widget import Widget, WidgetType
|
from . import Widget, WidgetType
|
||||||
|
|
||||||
CONF_CHECKBOX = "checkbox"
|
CONF_CHECKBOX = "checkbox"
|
||||||
|
|
||||||
@ -18,7 +18,7 @@ class CheckboxType(WidgetType):
|
|||||||
)
|
)
|
||||||
|
|
||||||
async def to_code(self, w: Widget, config):
|
async def to_code(self, w: Widget, config):
|
||||||
if value := config.get(CONF_TEXT):
|
if (value := config.get(CONF_TEXT)) is not None:
|
||||||
lv.checkbox_set_text(w.obj, await lv_text.process(value))
|
lv.checkbox_set_text(w.obj, await lv_text.process(value))
|
||||||
|
|
||||||
|
|
76
esphome/components/lvgl/widgets/dropdown.py
Normal file
76
esphome/components/lvgl/widgets/dropdown.py
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import esphome.codegen as cg
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.const import CONF_OPTIONS
|
||||||
|
|
||||||
|
from ..defines import (
|
||||||
|
CONF_DIR,
|
||||||
|
CONF_INDICATOR,
|
||||||
|
CONF_MAIN,
|
||||||
|
CONF_SELECTED_INDEX,
|
||||||
|
CONF_SYMBOL,
|
||||||
|
DIRECTIONS,
|
||||||
|
literal,
|
||||||
|
)
|
||||||
|
from ..lv_validation import lv_int, lv_text, option_string
|
||||||
|
from ..lvcode import LocalVariable, lv, lv_expr
|
||||||
|
from ..schemas import part_schema
|
||||||
|
from ..types import LvSelect, LvType, lv_obj_t
|
||||||
|
from . import Widget, WidgetType, set_obj_properties
|
||||||
|
from .label import CONF_LABEL
|
||||||
|
|
||||||
|
CONF_DROPDOWN = "dropdown"
|
||||||
|
CONF_DROPDOWN_LIST = "dropdown_list"
|
||||||
|
|
||||||
|
lv_dropdown_t = LvSelect("lv_dropdown_t")
|
||||||
|
lv_dropdown_list_t = LvType("lv_dropdown_list_t")
|
||||||
|
dropdown_list_spec = WidgetType(CONF_DROPDOWN_LIST, lv_dropdown_list_t, (CONF_MAIN,))
|
||||||
|
|
||||||
|
DROPDOWN_BASE_SCHEMA = cv.Schema(
|
||||||
|
{
|
||||||
|
cv.Optional(CONF_SYMBOL): lv_text,
|
||||||
|
cv.Optional(CONF_SELECTED_INDEX): cv.templatable(cv.int_),
|
||||||
|
cv.Optional(CONF_DIR, default="BOTTOM"): DIRECTIONS.one_of,
|
||||||
|
cv.Optional(CONF_DROPDOWN_LIST): part_schema(dropdown_list_spec),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
DROPDOWN_SCHEMA = DROPDOWN_BASE_SCHEMA.extend(
|
||||||
|
{
|
||||||
|
cv.Required(CONF_OPTIONS): cv.ensure_list(option_string),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DropdownType(WidgetType):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(
|
||||||
|
CONF_DROPDOWN,
|
||||||
|
lv_dropdown_t,
|
||||||
|
(CONF_MAIN, CONF_INDICATOR),
|
||||||
|
DROPDOWN_SCHEMA,
|
||||||
|
DROPDOWN_BASE_SCHEMA,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def to_code(self, w: Widget, config):
|
||||||
|
if options := config.get(CONF_OPTIONS):
|
||||||
|
text = cg.safe_exp("\n".join(options))
|
||||||
|
lv.dropdown_set_options(w.obj, text)
|
||||||
|
if symbol := config.get(CONF_SYMBOL):
|
||||||
|
lv.dropdown_set_symbol(w.obj, await lv_text.process(symbol))
|
||||||
|
if (selected := config.get(CONF_SELECTED_INDEX)) is not None:
|
||||||
|
value = await lv_int.process(selected)
|
||||||
|
lv.dropdown_set_selected(w.obj, value)
|
||||||
|
if dirn := config.get(CONF_DIR):
|
||||||
|
lv.dropdown_set_dir(w.obj, literal(dirn))
|
||||||
|
if dlist := config.get(CONF_DROPDOWN_LIST):
|
||||||
|
with LocalVariable(
|
||||||
|
"dropdown_list", lv_obj_t, lv_expr.dropdown_get_list(w.obj)
|
||||||
|
) as dlist_obj:
|
||||||
|
dwid = Widget(dlist_obj, dropdown_list_spec, dlist)
|
||||||
|
await set_obj_properties(dwid, dlist)
|
||||||
|
|
||||||
|
def get_uses(self):
|
||||||
|
return (CONF_LABEL,)
|
||||||
|
|
||||||
|
|
||||||
|
dropdown_spec = DropdownType()
|
@ -1,7 +1,7 @@
|
|||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_ANGLE, CONF_MODE
|
from esphome.const import CONF_ANGLE, CONF_MODE
|
||||||
|
|
||||||
from .defines import (
|
from ..defines import (
|
||||||
CONF_ANTIALIAS,
|
CONF_ANTIALIAS,
|
||||||
CONF_MAIN,
|
CONF_MAIN,
|
||||||
CONF_OFFSET_X,
|
CONF_OFFSET_X,
|
||||||
@ -12,11 +12,11 @@ from .defines import (
|
|||||||
CONF_ZOOM,
|
CONF_ZOOM,
|
||||||
LvConstant,
|
LvConstant,
|
||||||
)
|
)
|
||||||
|
from ..lv_validation import angle, lv_bool, lv_image, size, zoom
|
||||||
|
from ..lvcode import lv
|
||||||
|
from ..types import lv_img_t
|
||||||
|
from . import Widget, WidgetType
|
||||||
from .label import CONF_LABEL
|
from .label import CONF_LABEL
|
||||||
from .lv_validation import angle, lv_bool, lv_image, size, zoom
|
|
||||||
from .lvcode import lv
|
|
||||||
from .types import lv_img_t
|
|
||||||
from .widget import Widget, WidgetType
|
|
||||||
|
|
||||||
CONF_IMAGE = "image"
|
CONF_IMAGE = "image"
|
||||||
|
|
||||||
@ -65,16 +65,16 @@ class ImgType(WidgetType):
|
|||||||
async def to_code(self, w: Widget, config):
|
async def to_code(self, w: Widget, config):
|
||||||
if src := config.get(CONF_SRC):
|
if src := config.get(CONF_SRC):
|
||||||
lv.img_set_src(w.obj, await lv_image.process(src))
|
lv.img_set_src(w.obj, await lv_image.process(src))
|
||||||
if cf_angle := config.get(CONF_ANGLE):
|
if (cf_angle := config.get(CONF_ANGLE)) is not None:
|
||||||
pivot_x = config[CONF_PIVOT_X]
|
pivot_x = config[CONF_PIVOT_X]
|
||||||
pivot_y = config[CONF_PIVOT_Y]
|
pivot_y = config[CONF_PIVOT_Y]
|
||||||
lv.img_set_pivot(w.obj, pivot_x, pivot_y)
|
lv.img_set_pivot(w.obj, pivot_x, pivot_y)
|
||||||
lv.img_set_angle(w.obj, cf_angle)
|
lv.img_set_angle(w.obj, cf_angle)
|
||||||
if img_zoom := config.get(CONF_ZOOM):
|
if (img_zoom := config.get(CONF_ZOOM)) is not None:
|
||||||
lv.img_set_zoom(w.obj, img_zoom)
|
lv.img_set_zoom(w.obj, img_zoom)
|
||||||
if offset := config.get(CONF_OFFSET_X):
|
if (offset := config.get(CONF_OFFSET_X)) is not None:
|
||||||
lv.img_set_offset_x(w.obj, offset)
|
lv.img_set_offset_x(w.obj, offset)
|
||||||
if offset := config.get(CONF_OFFSET_Y):
|
if (offset := config.get(CONF_OFFSET_Y)) is not None:
|
||||||
lv.img_set_offset_y(w.obj, offset)
|
lv.img_set_offset_y(w.obj, offset)
|
||||||
if CONF_ANTIALIAS in config:
|
if CONF_ANTIALIAS in config:
|
||||||
lv.img_set_antialias(w.obj, config[CONF_ANTIALIAS])
|
lv.img_set_antialias(w.obj, config[CONF_ANTIALIAS])
|
49
esphome/components/lvgl/widgets/keyboard.py
Normal file
49
esphome/components/lvgl/widgets/keyboard.py
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
from esphome.components.key_provider import KeyProvider
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.const import CONF_MODE
|
||||||
|
from esphome.cpp_types import std_string
|
||||||
|
|
||||||
|
from ..defines import CONF_ITEMS, CONF_MAIN, KEYBOARD_MODES, literal
|
||||||
|
from ..helpers import add_lv_use, lvgl_components_required
|
||||||
|
from ..types import LvCompound, LvType
|
||||||
|
from . import Widget, WidgetType, get_widgets
|
||||||
|
from .textarea import CONF_TEXTAREA, lv_textarea_t
|
||||||
|
|
||||||
|
CONF_KEYBOARD = "keyboard"
|
||||||
|
|
||||||
|
KEYBOARD_SCHEMA = {
|
||||||
|
cv.Optional(CONF_MODE, default="TEXT_UPPER"): KEYBOARD_MODES.one_of,
|
||||||
|
cv.Optional(CONF_TEXTAREA): cv.use_id(lv_textarea_t),
|
||||||
|
}
|
||||||
|
|
||||||
|
lv_keyboard_t = LvType(
|
||||||
|
"LvKeyboardType",
|
||||||
|
parents=(KeyProvider, LvCompound),
|
||||||
|
largs=[(std_string, "text")],
|
||||||
|
has_on_value=True,
|
||||||
|
lvalue=lambda w: literal(f"lv_textarea_get_text({w.obj})"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class KeyboardType(WidgetType):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(
|
||||||
|
CONF_KEYBOARD,
|
||||||
|
lv_keyboard_t,
|
||||||
|
(CONF_MAIN, CONF_ITEMS),
|
||||||
|
KEYBOARD_SCHEMA,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_uses(self):
|
||||||
|
return CONF_KEYBOARD, CONF_TEXTAREA
|
||||||
|
|
||||||
|
async def to_code(self, w: Widget, config: dict):
|
||||||
|
lvgl_components_required.add("KEY_LISTENER")
|
||||||
|
lvgl_components_required.add(CONF_KEYBOARD)
|
||||||
|
add_lv_use("btnmatrix")
|
||||||
|
await w.set_property(CONF_MODE, await KEYBOARD_MODES.process(config[CONF_MODE]))
|
||||||
|
if ta := await get_widgets(config, CONF_TEXTAREA):
|
||||||
|
await w.set_property(CONF_TEXTAREA, ta[0].obj)
|
||||||
|
|
||||||
|
|
||||||
|
keyboard_spec = KeyboardType()
|
@ -1,6 +1,6 @@
|
|||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
|
|
||||||
from .defines import (
|
from ..defines import (
|
||||||
CONF_LONG_MODE,
|
CONF_LONG_MODE,
|
||||||
CONF_MAIN,
|
CONF_MAIN,
|
||||||
CONF_RECOLOR,
|
CONF_RECOLOR,
|
||||||
@ -9,10 +9,10 @@ from .defines import (
|
|||||||
CONF_TEXT,
|
CONF_TEXT,
|
||||||
LV_LONG_MODES,
|
LV_LONG_MODES,
|
||||||
)
|
)
|
||||||
from .lv_validation import lv_bool, lv_text
|
from ..lv_validation import lv_bool, lv_text
|
||||||
from .schemas import TEXT_SCHEMA
|
from ..schemas import TEXT_SCHEMA
|
||||||
from .types import LvText, WidgetType
|
from ..types import LvText, WidgetType
|
||||||
from .widget import Widget
|
from . import Widget
|
||||||
|
|
||||||
CONF_LABEL = "label"
|
CONF_LABEL = "label"
|
||||||
|
|
@ -1,11 +1,11 @@
|
|||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_BRIGHTNESS, CONF_COLOR, CONF_LED
|
from esphome.const import CONF_BRIGHTNESS, CONF_COLOR, CONF_LED
|
||||||
|
|
||||||
from .defines import CONF_MAIN
|
from ..defines import CONF_MAIN
|
||||||
from .lv_validation import lv_brightness, lv_color
|
from ..lv_validation import lv_brightness, lv_color
|
||||||
from .lvcode import lv
|
from ..lvcode import lv
|
||||||
from .types import LvType
|
from ..types import LvType
|
||||||
from .widget import Widget, WidgetType
|
from . import Widget, WidgetType
|
||||||
|
|
||||||
LED_SCHEMA = cv.Schema(
|
LED_SCHEMA = cv.Schema(
|
||||||
{
|
{
|
||||||
@ -20,9 +20,9 @@ class LedType(WidgetType):
|
|||||||
super().__init__(CONF_LED, LvType("lv_led_t"), (CONF_MAIN,), LED_SCHEMA)
|
super().__init__(CONF_LED, LvType("lv_led_t"), (CONF_MAIN,), LED_SCHEMA)
|
||||||
|
|
||||||
async def to_code(self, w: Widget, config):
|
async def to_code(self, w: Widget, config):
|
||||||
if color := config.get(CONF_COLOR):
|
if (color := config.get(CONF_COLOR)) is not None:
|
||||||
lv.led_set_color(w.obj, await lv_color.process(color))
|
lv.led_set_color(w.obj, await lv_color.process(color))
|
||||||
if brightness := config.get(CONF_BRIGHTNESS):
|
if (brightness := config.get(CONF_BRIGHTNESS)) is not None:
|
||||||
lv.led_set_brightness(w.obj, await lv_brightness.process(brightness))
|
lv.led_set_brightness(w.obj, await lv_brightness.process(brightness))
|
||||||
|
|
||||||
|
|
@ -3,11 +3,10 @@ import functools
|
|||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
|
|
||||||
from . import defines as df
|
from ..defines import CONF_MAIN, literal
|
||||||
from .defines import CONF_MAIN, literal
|
from ..lvcode import lv
|
||||||
from .lvcode import lv
|
from ..types import LvType
|
||||||
from .types import LvType
|
from . import Widget, WidgetType
|
||||||
from .widget import Widget, WidgetType
|
|
||||||
|
|
||||||
CONF_LINE = "line"
|
CONF_LINE = "line"
|
||||||
CONF_POINTS = "points"
|
CONF_POINTS = "points"
|
||||||
@ -32,7 +31,7 @@ def cv_point_list(value):
|
|||||||
|
|
||||||
|
|
||||||
LINE_SCHEMA = {
|
LINE_SCHEMA = {
|
||||||
cv.Required(df.CONF_POINTS): cv_point_list,
|
cv.Required(CONF_POINTS): cv_point_list,
|
||||||
cv.GenerateID(CONF_POINT_LIST_ID): cv.declare_id(lv_point_t),
|
cv.GenerateID(CONF_POINT_LIST_ID): cv.declare_id(lv_point_t),
|
||||||
}
|
}
|
||||||
|
|
@ -1,11 +1,13 @@
|
|||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_MODE, CONF_VALUE
|
from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_MODE, CONF_VALUE
|
||||||
|
|
||||||
from .defines import BAR_MODES, CONF_ANIMATED, CONF_INDICATOR, CONF_MAIN, literal
|
from ..defines import BAR_MODES, CONF_ANIMATED, CONF_INDICATOR, CONF_MAIN, literal
|
||||||
from .lv_validation import animated, get_start_value, lv_float
|
from ..lv_validation import animated, get_start_value, lv_float
|
||||||
from .lvcode import lv
|
from ..lvcode import lv
|
||||||
from .types import LvNumber, NumberType
|
from ..types import LvNumber, NumberType
|
||||||
from .widget import Widget
|
from . import Widget
|
||||||
|
|
||||||
|
# Note this file cannot be called "bar.py" because that name is disallowed.
|
||||||
|
|
||||||
CONF_BAR = "bar"
|
CONF_BAR = "bar"
|
||||||
BAR_MODIFY_SCHEMA = cv.Schema(
|
BAR_MODIFY_SCHEMA = cv.Schema(
|
302
esphome/components/lvgl/widgets/meter.py
Normal file
302
esphome/components/lvgl/widgets/meter.py
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
from esphome import automation
|
||||||
|
import esphome.codegen as cg
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.const import (
|
||||||
|
CONF_COLOR,
|
||||||
|
CONF_COUNT,
|
||||||
|
CONF_ID,
|
||||||
|
CONF_LENGTH,
|
||||||
|
CONF_LOCAL,
|
||||||
|
CONF_RANGE_FROM,
|
||||||
|
CONF_RANGE_TO,
|
||||||
|
CONF_ROTATION,
|
||||||
|
CONF_VALUE,
|
||||||
|
CONF_WIDTH,
|
||||||
|
)
|
||||||
|
|
||||||
|
from ..automation import action_to_code
|
||||||
|
from ..defines import (
|
||||||
|
CONF_END_VALUE,
|
||||||
|
CONF_MAIN,
|
||||||
|
CONF_PIVOT_X,
|
||||||
|
CONF_PIVOT_Y,
|
||||||
|
CONF_SRC,
|
||||||
|
CONF_START_VALUE,
|
||||||
|
CONF_TICKS,
|
||||||
|
)
|
||||||
|
from ..helpers import add_lv_use
|
||||||
|
from ..lv_validation import (
|
||||||
|
angle,
|
||||||
|
get_end_value,
|
||||||
|
get_start_value,
|
||||||
|
lv_bool,
|
||||||
|
lv_color,
|
||||||
|
lv_float,
|
||||||
|
lv_image,
|
||||||
|
requires_component,
|
||||||
|
size,
|
||||||
|
)
|
||||||
|
from ..lvcode import LocalVariable, lv, lv_assign, lv_expr
|
||||||
|
from ..types import LvType, ObjUpdateAction
|
||||||
|
from . import Widget, WidgetType, get_widgets
|
||||||
|
from .arc import CONF_ARC
|
||||||
|
from .img import CONF_IMAGE
|
||||||
|
from .line import CONF_LINE
|
||||||
|
from .obj import obj_spec
|
||||||
|
|
||||||
|
CONF_ANGLE_RANGE = "angle_range"
|
||||||
|
CONF_COLOR_END = "color_end"
|
||||||
|
CONF_COLOR_START = "color_start"
|
||||||
|
CONF_INDICATORS = "indicators"
|
||||||
|
CONF_LABEL_GAP = "label_gap"
|
||||||
|
CONF_MAJOR = "major"
|
||||||
|
CONF_METER = "meter"
|
||||||
|
CONF_R_MOD = "r_mod"
|
||||||
|
CONF_SCALES = "scales"
|
||||||
|
CONF_STRIDE = "stride"
|
||||||
|
CONF_TICK_STYLE = "tick_style"
|
||||||
|
|
||||||
|
lv_meter_t = LvType("lv_meter_t")
|
||||||
|
lv_meter_indicator_t = cg.global_ns.struct("lv_meter_indicator_t")
|
||||||
|
lv_meter_indicator_t_ptr = lv_meter_indicator_t.operator("ptr")
|
||||||
|
|
||||||
|
|
||||||
|
def pixels(value):
|
||||||
|
"""A size in one axis in pixels"""
|
||||||
|
if isinstance(value, str) and value.lower().endswith("px"):
|
||||||
|
return cv.int_(value[:-2])
|
||||||
|
return cv.int_(value)
|
||||||
|
|
||||||
|
|
||||||
|
INDICATOR_LINE_SCHEMA = cv.Schema(
|
||||||
|
{
|
||||||
|
cv.Optional(CONF_WIDTH, default=4): size,
|
||||||
|
cv.Optional(CONF_COLOR, default=0): lv_color,
|
||||||
|
cv.Optional(CONF_R_MOD, default=0): size,
|
||||||
|
cv.Optional(CONF_VALUE): lv_float,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
INDICATOR_IMG_SCHEMA = cv.Schema(
|
||||||
|
{
|
||||||
|
cv.Required(CONF_SRC): lv_image,
|
||||||
|
cv.Required(CONF_PIVOT_X): pixels,
|
||||||
|
cv.Required(CONF_PIVOT_Y): pixels,
|
||||||
|
cv.Optional(CONF_VALUE): lv_float,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
INDICATOR_ARC_SCHEMA = cv.Schema(
|
||||||
|
{
|
||||||
|
cv.Optional(CONF_WIDTH, default=4): size,
|
||||||
|
cv.Optional(CONF_COLOR, default=0): lv_color,
|
||||||
|
cv.Optional(CONF_R_MOD, default=0): size,
|
||||||
|
cv.Exclusive(CONF_VALUE, CONF_VALUE): lv_float,
|
||||||
|
cv.Exclusive(CONF_START_VALUE, CONF_VALUE): lv_float,
|
||||||
|
cv.Optional(CONF_END_VALUE): lv_float,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
INDICATOR_TICKS_SCHEMA = cv.Schema(
|
||||||
|
{
|
||||||
|
cv.Optional(CONF_WIDTH, default=4): size,
|
||||||
|
cv.Optional(CONF_COLOR_START, default=0): lv_color,
|
||||||
|
cv.Optional(CONF_COLOR_END): lv_color,
|
||||||
|
cv.Exclusive(CONF_VALUE, CONF_VALUE): lv_float,
|
||||||
|
cv.Exclusive(CONF_START_VALUE, CONF_VALUE): lv_float,
|
||||||
|
cv.Optional(CONF_END_VALUE): lv_float,
|
||||||
|
cv.Optional(CONF_LOCAL, default=False): lv_bool,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
INDICATOR_SCHEMA = cv.Schema(
|
||||||
|
{
|
||||||
|
cv.Exclusive(CONF_LINE, CONF_INDICATORS): INDICATOR_LINE_SCHEMA.extend(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(lv_meter_indicator_t),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
cv.Exclusive(CONF_IMAGE, CONF_INDICATORS): cv.All(
|
||||||
|
INDICATOR_IMG_SCHEMA.extend(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(lv_meter_indicator_t),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
requires_component("image"),
|
||||||
|
),
|
||||||
|
cv.Exclusive(CONF_ARC, CONF_INDICATORS): INDICATOR_ARC_SCHEMA.extend(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(lv_meter_indicator_t),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
cv.Exclusive(CONF_TICK_STYLE, CONF_INDICATORS): INDICATOR_TICKS_SCHEMA.extend(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(lv_meter_indicator_t),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
SCALE_SCHEMA = cv.Schema(
|
||||||
|
{
|
||||||
|
cv.Optional(CONF_TICKS): cv.Schema(
|
||||||
|
{
|
||||||
|
cv.Optional(CONF_COUNT, default=12): cv.positive_int,
|
||||||
|
cv.Optional(CONF_WIDTH, default=2): size,
|
||||||
|
cv.Optional(CONF_LENGTH, default=10): size,
|
||||||
|
cv.Optional(CONF_COLOR, default=0x808080): lv_color,
|
||||||
|
cv.Optional(CONF_MAJOR): cv.Schema(
|
||||||
|
{
|
||||||
|
cv.Optional(CONF_STRIDE, default=3): cv.positive_int,
|
||||||
|
cv.Optional(CONF_WIDTH, default=5): size,
|
||||||
|
cv.Optional(CONF_LENGTH, default="15%"): size,
|
||||||
|
cv.Optional(CONF_COLOR, default=0): lv_color,
|
||||||
|
cv.Optional(CONF_LABEL_GAP, default=4): size,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
cv.Optional(CONF_RANGE_FROM, default=0.0): cv.float_,
|
||||||
|
cv.Optional(CONF_RANGE_TO, default=100.0): cv.float_,
|
||||||
|
cv.Optional(CONF_ANGLE_RANGE, default=270): cv.int_range(0, 360),
|
||||||
|
cv.Optional(CONF_ROTATION): angle,
|
||||||
|
cv.Optional(CONF_INDICATORS): cv.ensure_list(INDICATOR_SCHEMA),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
METER_SCHEMA = {cv.Optional(CONF_SCALES): cv.ensure_list(SCALE_SCHEMA)}
|
||||||
|
|
||||||
|
|
||||||
|
class MeterType(WidgetType):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(CONF_METER, lv_meter_t, (CONF_MAIN,), METER_SCHEMA)
|
||||||
|
|
||||||
|
async def to_code(self, w: Widget, config):
|
||||||
|
"""For a meter object, create and set parameters"""
|
||||||
|
|
||||||
|
var = w.obj
|
||||||
|
for scale_conf in config.get(CONF_SCALES, ()):
|
||||||
|
rotation = 90 + (360 - scale_conf[CONF_ANGLE_RANGE]) / 2
|
||||||
|
if CONF_ROTATION in scale_conf:
|
||||||
|
rotation = scale_conf[CONF_ROTATION] // 10
|
||||||
|
with LocalVariable(
|
||||||
|
"meter_var", "lv_meter_scale_t", lv_expr.meter_add_scale(var)
|
||||||
|
) as meter_var:
|
||||||
|
lv.meter_set_scale_range(
|
||||||
|
var,
|
||||||
|
meter_var,
|
||||||
|
scale_conf[CONF_RANGE_FROM],
|
||||||
|
scale_conf[CONF_RANGE_TO],
|
||||||
|
scale_conf[CONF_ANGLE_RANGE],
|
||||||
|
rotation,
|
||||||
|
)
|
||||||
|
if ticks := scale_conf.get(CONF_TICKS):
|
||||||
|
color = await lv_color.process(ticks[CONF_COLOR])
|
||||||
|
lv.meter_set_scale_ticks(
|
||||||
|
var,
|
||||||
|
meter_var,
|
||||||
|
ticks[CONF_COUNT],
|
||||||
|
ticks[CONF_WIDTH],
|
||||||
|
ticks[CONF_LENGTH],
|
||||||
|
color,
|
||||||
|
)
|
||||||
|
if CONF_MAJOR in ticks:
|
||||||
|
major = ticks[CONF_MAJOR]
|
||||||
|
color = await lv_color.process(major[CONF_COLOR])
|
||||||
|
lv.meter_set_scale_major_ticks(
|
||||||
|
var,
|
||||||
|
meter_var,
|
||||||
|
major[CONF_STRIDE],
|
||||||
|
major[CONF_WIDTH],
|
||||||
|
major[CONF_LENGTH],
|
||||||
|
color,
|
||||||
|
major[CONF_LABEL_GAP],
|
||||||
|
)
|
||||||
|
for indicator in scale_conf.get(CONF_INDICATORS, ()):
|
||||||
|
(t, v) = next(iter(indicator.items()))
|
||||||
|
iid = v[CONF_ID]
|
||||||
|
ivar = cg.new_variable(
|
||||||
|
iid, cg.nullptr, type_=lv_meter_indicator_t_ptr
|
||||||
|
)
|
||||||
|
# Enable getting the meter to which this belongs.
|
||||||
|
wid = Widget.create(iid, var, obj_spec, v)
|
||||||
|
wid.obj = ivar
|
||||||
|
if t == CONF_LINE:
|
||||||
|
color = await lv_color.process(v[CONF_COLOR])
|
||||||
|
lv_assign(
|
||||||
|
ivar,
|
||||||
|
lv_expr.meter_add_needle_line(
|
||||||
|
var, meter_var, v[CONF_WIDTH], color, v[CONF_R_MOD]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if t == CONF_ARC:
|
||||||
|
color = await lv_color.process(v[CONF_COLOR])
|
||||||
|
lv_assign(
|
||||||
|
ivar,
|
||||||
|
lv_expr.meter_add_arc(
|
||||||
|
var, meter_var, v[CONF_WIDTH], color, v[CONF_R_MOD]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if t == CONF_TICK_STYLE:
|
||||||
|
color_start = await lv_color.process(v[CONF_COLOR_START])
|
||||||
|
color_end = await lv_color.process(
|
||||||
|
v.get(CONF_COLOR_END) or color_start
|
||||||
|
)
|
||||||
|
lv_assign(
|
||||||
|
ivar,
|
||||||
|
lv_expr.meter_add_scale_lines(
|
||||||
|
var,
|
||||||
|
meter_var,
|
||||||
|
color_start,
|
||||||
|
color_end,
|
||||||
|
v[CONF_LOCAL],
|
||||||
|
v[CONF_WIDTH],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if t == CONF_IMAGE:
|
||||||
|
add_lv_use("img")
|
||||||
|
lv_assign(
|
||||||
|
ivar,
|
||||||
|
lv_expr.meter_add_needle_img(
|
||||||
|
var,
|
||||||
|
meter_var,
|
||||||
|
await lv_image.process(v[CONF_SRC]),
|
||||||
|
v[CONF_PIVOT_X],
|
||||||
|
v[CONF_PIVOT_Y],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
start_value = await get_start_value(v)
|
||||||
|
end_value = await get_end_value(v)
|
||||||
|
set_indicator_values(var, ivar, start_value, end_value)
|
||||||
|
|
||||||
|
|
||||||
|
meter_spec = MeterType()
|
||||||
|
|
||||||
|
|
||||||
|
@automation.register_action(
|
||||||
|
"lvgl.indicator.update",
|
||||||
|
ObjUpdateAction,
|
||||||
|
cv.Schema(
|
||||||
|
{
|
||||||
|
cv.Required(CONF_ID): cv.use_id(lv_meter_indicator_t),
|
||||||
|
cv.Exclusive(CONF_VALUE, CONF_VALUE): lv_float,
|
||||||
|
cv.Exclusive(CONF_START_VALUE, CONF_VALUE): lv_float,
|
||||||
|
cv.Optional(CONF_END_VALUE): lv_float,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
async def indicator_update_to_code(config, action_id, template_arg, args):
|
||||||
|
widget = await get_widgets(config)
|
||||||
|
start_value = await get_start_value(config)
|
||||||
|
end_value = await get_end_value(config)
|
||||||
|
|
||||||
|
async def set_value(w: Widget):
|
||||||
|
set_indicator_values(w.var, w.obj, start_value, end_value)
|
||||||
|
|
||||||
|
return await action_to_code(widget, set_value, action_id, template_arg, args)
|
||||||
|
|
||||||
|
|
||||||
|
def set_indicator_values(meter, indicator, start_value, end_value):
|
||||||
|
if start_value is not None:
|
||||||
|
if end_value is None:
|
||||||
|
lv.meter_set_indicator_value(meter, indicator, start_value)
|
||||||
|
else:
|
||||||
|
lv.meter_set_indicator_start_value(meter, indicator, start_value)
|
||||||
|
if end_value is not None:
|
||||||
|
lv.meter_set_indicator_end_value(meter, indicator, end_value)
|
135
esphome/components/lvgl/widgets/msgbox.py
Normal file
135
esphome/components/lvgl/widgets/msgbox.py
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
from esphome import config_validation as cv
|
||||||
|
from esphome.const import CONF_BUTTON, CONF_ID
|
||||||
|
from esphome.core import ID
|
||||||
|
from esphome.cpp_generator import new_Pvariable, static_const_array
|
||||||
|
from esphome.cpp_types import nullptr
|
||||||
|
|
||||||
|
from ..defines import (
|
||||||
|
CONF_BODY,
|
||||||
|
CONF_BUTTONS,
|
||||||
|
CONF_CLOSE_BUTTON,
|
||||||
|
CONF_MSGBOXES,
|
||||||
|
CONF_TEXT,
|
||||||
|
CONF_TITLE,
|
||||||
|
TYPE_FLEX,
|
||||||
|
literal,
|
||||||
|
)
|
||||||
|
from ..helpers import add_lv_use
|
||||||
|
from ..lv_validation import lv_bool, lv_pct, lv_text
|
||||||
|
from ..lvcode import (
|
||||||
|
EVENT_ARG,
|
||||||
|
LambdaContext,
|
||||||
|
LocalVariable,
|
||||||
|
lv_add,
|
||||||
|
lv_assign,
|
||||||
|
lv_expr,
|
||||||
|
lv_obj,
|
||||||
|
lv_Pvariable,
|
||||||
|
)
|
||||||
|
from ..schemas import STYLE_SCHEMA, STYLED_TEXT_SCHEMA, container_schema
|
||||||
|
from ..styles import TOP_LAYER
|
||||||
|
from ..types import LV_EVENT, char_ptr, lv_obj_t
|
||||||
|
from . import Widget, set_obj_properties
|
||||||
|
from .button import button_spec
|
||||||
|
from .buttonmatrix import (
|
||||||
|
BUTTONMATRIX_BUTTON_SCHEMA,
|
||||||
|
CONF_BUTTON_TEXT_LIST_ID,
|
||||||
|
buttonmatrix_spec,
|
||||||
|
get_button_data,
|
||||||
|
lv_buttonmatrix_t,
|
||||||
|
set_btn_data,
|
||||||
|
)
|
||||||
|
from .label import CONF_LABEL
|
||||||
|
from .obj import obj_spec
|
||||||
|
|
||||||
|
CONF_MSGBOX = "msgbox"
|
||||||
|
MSGBOX_SCHEMA = container_schema(
|
||||||
|
obj_spec,
|
||||||
|
STYLE_SCHEMA.extend(
|
||||||
|
{
|
||||||
|
cv.GenerateID(CONF_ID): cv.declare_id(lv_obj_t),
|
||||||
|
cv.Required(CONF_TITLE): STYLED_TEXT_SCHEMA,
|
||||||
|
cv.Optional(CONF_BODY): STYLED_TEXT_SCHEMA,
|
||||||
|
cv.Optional(CONF_BUTTONS): cv.ensure_list(BUTTONMATRIX_BUTTON_SCHEMA),
|
||||||
|
cv.Optional(CONF_CLOSE_BUTTON): lv_bool,
|
||||||
|
cv.GenerateID(CONF_BUTTON_TEXT_LIST_ID): cv.declare_id(char_ptr),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def msgbox_to_code(conf):
|
||||||
|
"""
|
||||||
|
Construct a message box. This consists of a full-screen translucent background enclosing a centered container
|
||||||
|
with an optional title, body, close button and a button matrix. And any other widgets the user cares to add
|
||||||
|
:param conf: The config data
|
||||||
|
:return: code to add to the init lambda
|
||||||
|
"""
|
||||||
|
add_lv_use(
|
||||||
|
TYPE_FLEX,
|
||||||
|
CONF_BUTTON,
|
||||||
|
CONF_LABEL,
|
||||||
|
CONF_MSGBOX,
|
||||||
|
*buttonmatrix_spec.get_uses(),
|
||||||
|
*button_spec.get_uses(),
|
||||||
|
)
|
||||||
|
messagebox_id = conf[CONF_ID]
|
||||||
|
outer = lv_Pvariable(lv_obj_t, messagebox_id.id)
|
||||||
|
buttonmatrix = new_Pvariable(
|
||||||
|
ID(
|
||||||
|
f"{messagebox_id.id}_buttonmatrix_",
|
||||||
|
is_declaration=True,
|
||||||
|
type=lv_buttonmatrix_t,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
msgbox = lv_Pvariable(lv_obj_t, f"{messagebox_id.id}_msgbox")
|
||||||
|
outer_widget = Widget.create(messagebox_id, outer, obj_spec, conf)
|
||||||
|
buttonmatrix_widget = Widget.create(
|
||||||
|
str(buttonmatrix), buttonmatrix, buttonmatrix_spec, conf
|
||||||
|
)
|
||||||
|
text_list, ctrl_list, width_list, _ = await get_button_data(
|
||||||
|
(conf,), buttonmatrix_widget
|
||||||
|
)
|
||||||
|
text_id = conf[CONF_BUTTON_TEXT_LIST_ID]
|
||||||
|
text_list = static_const_array(text_id, text_list)
|
||||||
|
if (text := conf.get(CONF_BODY)) is not None:
|
||||||
|
text = await lv_text.process(text.get(CONF_TEXT))
|
||||||
|
if (title := conf.get(CONF_TITLE)) is not None:
|
||||||
|
title = await lv_text.process(title.get(CONF_TEXT))
|
||||||
|
close_button = conf[CONF_CLOSE_BUTTON]
|
||||||
|
lv_assign(outer, lv_expr.obj_create(TOP_LAYER))
|
||||||
|
lv_obj.set_width(outer, lv_pct(100))
|
||||||
|
lv_obj.set_height(outer, lv_pct(100))
|
||||||
|
lv_obj.set_style_bg_opa(outer, 128, 0)
|
||||||
|
lv_obj.set_style_bg_color(outer, literal("lv_color_black()"), 0)
|
||||||
|
lv_obj.set_style_border_width(outer, 0, 0)
|
||||||
|
lv_obj.set_style_pad_all(outer, 0, 0)
|
||||||
|
lv_obj.set_style_radius(outer, 0, 0)
|
||||||
|
outer_widget.add_flag("LV_OBJ_FLAG_HIDDEN")
|
||||||
|
lv_assign(
|
||||||
|
msgbox, lv_expr.msgbox_create(outer, title, text, text_list, close_button)
|
||||||
|
)
|
||||||
|
lv_obj.set_style_align(msgbox, literal("LV_ALIGN_CENTER"), 0)
|
||||||
|
lv_add(buttonmatrix.set_obj(lv_expr.msgbox_get_btns(msgbox)))
|
||||||
|
await set_obj_properties(outer_widget, conf)
|
||||||
|
if close_button:
|
||||||
|
async with LambdaContext(EVENT_ARG, where=messagebox_id) as context:
|
||||||
|
outer_widget.add_flag("LV_OBJ_FLAG_HIDDEN")
|
||||||
|
with LocalVariable(
|
||||||
|
"close_btn_", lv_obj_t, lv_expr.msgbox_get_close_btn(msgbox)
|
||||||
|
) as close_btn:
|
||||||
|
lv_obj.remove_event_cb(close_btn, nullptr)
|
||||||
|
lv_obj.add_event_cb(
|
||||||
|
close_btn,
|
||||||
|
await context.get_lambda(),
|
||||||
|
LV_EVENT.CLICKED,
|
||||||
|
nullptr,
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(ctrl_list) != 0 or len(width_list) != 0:
|
||||||
|
set_btn_data(buttonmatrix.obj, ctrl_list, width_list)
|
||||||
|
|
||||||
|
|
||||||
|
async def msgboxes_to_code(config):
|
||||||
|
for conf in config.get(CONF_MSGBOXES, ()):
|
||||||
|
await msgbox_to_code(conf)
|
@ -1,9 +1,9 @@
|
|||||||
from esphome import automation
|
from esphome import automation
|
||||||
|
|
||||||
from .automation import update_to_code
|
from ..automation import update_to_code
|
||||||
from .defines import CONF_MAIN, CONF_OBJ
|
from ..defines import CONF_MAIN, CONF_OBJ
|
||||||
from .schemas import create_modify_schema
|
from ..schemas import create_modify_schema
|
||||||
from .types import ObjUpdateAction, WidgetType, lv_obj_t
|
from ..types import ObjUpdateAction, WidgetType, lv_obj_t
|
||||||
|
|
||||||
|
|
||||||
class ObjType(WidgetType):
|
class ObjType(WidgetType):
|
@ -2,7 +2,7 @@ from esphome import automation, codegen as cg
|
|||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_ID, CONF_PAGES, CONF_TIME
|
from esphome.const import CONF_ID, CONF_PAGES, CONF_TIME
|
||||||
|
|
||||||
from .defines import (
|
from ..defines import (
|
||||||
CONF_ANIMATION,
|
CONF_ANIMATION,
|
||||||
CONF_LVGL_ID,
|
CONF_LVGL_ID,
|
||||||
CONF_PAGE,
|
CONF_PAGE,
|
||||||
@ -10,11 +10,11 @@ from .defines import (
|
|||||||
CONF_SKIP,
|
CONF_SKIP,
|
||||||
LV_ANIM,
|
LV_ANIM,
|
||||||
)
|
)
|
||||||
from .lv_validation import lv_bool, lv_milliseconds
|
from ..lv_validation import lv_bool, lv_milliseconds
|
||||||
from .lvcode import LVGL_COMP_ARG, LambdaContext, add_line_marks, lv_add, lvgl_comp
|
from ..lvcode import LVGL_COMP_ARG, LambdaContext, add_line_marks, lv_add, lvgl_comp
|
||||||
from .schemas import LVGL_SCHEMA
|
from ..schemas import LVGL_SCHEMA
|
||||||
from .types import LvglAction, lv_page_t
|
from ..types import LvglAction, lv_page_t
|
||||||
from .widget import Widget, WidgetType, add_widgets, set_obj_properties
|
from . import Widget, WidgetType, add_widgets, set_obj_properties
|
||||||
|
|
||||||
|
|
||||||
class PageType(WidgetType):
|
class PageType(WidgetType):
|
77
esphome/components/lvgl/widgets/roller.py
Normal file
77
esphome/components/lvgl/widgets/roller.py
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import esphome.codegen as cg
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.const import CONF_MODE, CONF_OPTIONS
|
||||||
|
|
||||||
|
from ..defines import (
|
||||||
|
CONF_ANIMATED,
|
||||||
|
CONF_MAIN,
|
||||||
|
CONF_SELECTED,
|
||||||
|
CONF_SELECTED_INDEX,
|
||||||
|
CONF_VISIBLE_ROW_COUNT,
|
||||||
|
ROLLER_MODES,
|
||||||
|
literal,
|
||||||
|
)
|
||||||
|
from ..lv_validation import animated, lv_int, option_string
|
||||||
|
from ..lvcode import lv
|
||||||
|
from ..types import LvSelect
|
||||||
|
from . import WidgetType
|
||||||
|
from .label import CONF_LABEL
|
||||||
|
|
||||||
|
CONF_ROLLER = "roller"
|
||||||
|
lv_roller_t = LvSelect("lv_roller_t")
|
||||||
|
|
||||||
|
ROLLER_BASE_SCHEMA = cv.Schema(
|
||||||
|
{
|
||||||
|
cv.Optional(CONF_SELECTED_INDEX): cv.templatable(cv.int_),
|
||||||
|
cv.Optional(CONF_VISIBLE_ROW_COUNT): lv_int,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
ROLLER_SCHEMA = ROLLER_BASE_SCHEMA.extend(
|
||||||
|
{
|
||||||
|
cv.Required(CONF_OPTIONS): cv.ensure_list(option_string),
|
||||||
|
cv.Optional(CONF_MODE, default="NORMAL"): ROLLER_MODES.one_of,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
ROLLER_MODIFY_SCHEMA = ROLLER_BASE_SCHEMA.extend(
|
||||||
|
{
|
||||||
|
cv.Optional(CONF_ANIMATED, default=True): animated,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RollerType(WidgetType):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(
|
||||||
|
CONF_ROLLER,
|
||||||
|
lv_roller_t,
|
||||||
|
(CONF_MAIN, CONF_SELECTED),
|
||||||
|
ROLLER_SCHEMA,
|
||||||
|
ROLLER_MODIFY_SCHEMA,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def to_code(self, w, config):
|
||||||
|
if options := config.get(CONF_OPTIONS):
|
||||||
|
mode = await ROLLER_MODES.process(config[CONF_MODE])
|
||||||
|
text = cg.safe_exp("\n".join(options))
|
||||||
|
lv.roller_set_options(w.obj, text, mode)
|
||||||
|
animopt = literal(config.get(CONF_ANIMATED) or "LV_ANIM_OFF")
|
||||||
|
if CONF_SELECTED_INDEX in config:
|
||||||
|
if selected := config[CONF_SELECTED_INDEX]:
|
||||||
|
value = await lv_int.process(selected)
|
||||||
|
lv.roller_set_selected(w.obj, value, animopt)
|
||||||
|
await w.set_property(
|
||||||
|
CONF_VISIBLE_ROW_COUNT,
|
||||||
|
await lv_int.process(config.get(CONF_VISIBLE_ROW_COUNT)),
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def animated(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_uses(self):
|
||||||
|
return (CONF_LABEL,)
|
||||||
|
|
||||||
|
|
||||||
|
roller_spec = RollerType()
|
@ -1,7 +1,7 @@
|
|||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_MODE, CONF_VALUE
|
from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_MODE, CONF_VALUE
|
||||||
|
|
||||||
from .defines import (
|
from ..defines import (
|
||||||
BAR_MODES,
|
BAR_MODES,
|
||||||
CONF_ANIMATED,
|
CONF_ANIMATED,
|
||||||
CONF_INDICATOR,
|
CONF_INDICATOR,
|
||||||
@ -9,12 +9,12 @@ from .defines import (
|
|||||||
CONF_MAIN,
|
CONF_MAIN,
|
||||||
literal,
|
literal,
|
||||||
)
|
)
|
||||||
from .helpers import add_lv_use
|
from ..helpers import add_lv_use
|
||||||
|
from ..lv_validation import animated, get_start_value, lv_float
|
||||||
|
from ..lvcode import lv
|
||||||
|
from ..types import LvNumber, NumberType
|
||||||
|
from . import Widget
|
||||||
from .lv_bar import CONF_BAR
|
from .lv_bar import CONF_BAR
|
||||||
from .lv_validation import animated, get_start_value, lv_float
|
|
||||||
from .lvcode import lv
|
|
||||||
from .types import LvNumber, NumberType
|
|
||||||
from .widget import Widget
|
|
||||||
|
|
||||||
CONF_SLIDER = "slider"
|
CONF_SLIDER = "slider"
|
||||||
SLIDER_MODIFY_SCHEMA = cv.Schema(
|
SLIDER_MODIFY_SCHEMA = cv.Schema(
|
178
esphome/components/lvgl/widgets/spinbox.py
Normal file
178
esphome/components/lvgl/widgets/spinbox.py
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
from esphome import automation
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.const import CONF_ID, CONF_RANGE_FROM, CONF_RANGE_TO, CONF_STEP, CONF_VALUE
|
||||||
|
|
||||||
|
from ..automation import action_to_code, update_to_code
|
||||||
|
from ..defines import (
|
||||||
|
CONF_CURSOR,
|
||||||
|
CONF_DECIMAL_PLACES,
|
||||||
|
CONF_DIGITS,
|
||||||
|
CONF_MAIN,
|
||||||
|
CONF_ROLLOVER,
|
||||||
|
CONF_SCROLLBAR,
|
||||||
|
CONF_SELECTED,
|
||||||
|
CONF_TEXTAREA_PLACEHOLDER,
|
||||||
|
)
|
||||||
|
from ..lv_validation import lv_bool, lv_float
|
||||||
|
from ..lvcode import lv
|
||||||
|
from ..types import LvNumber, ObjUpdateAction
|
||||||
|
from . import Widget, WidgetType, get_widgets
|
||||||
|
from .label import CONF_LABEL
|
||||||
|
from .textarea import CONF_TEXTAREA
|
||||||
|
|
||||||
|
CONF_SPINBOX = "spinbox"
|
||||||
|
|
||||||
|
lv_spinbox_t = LvNumber("lv_spinbox_t")
|
||||||
|
|
||||||
|
SPIN_ACTIONS = (
|
||||||
|
"INCREMENT",
|
||||||
|
"DECREMENT",
|
||||||
|
"STEP_NEXT",
|
||||||
|
"STEP_PREV",
|
||||||
|
"CLEAR",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_spinbox(config):
|
||||||
|
max_val = 2**31 - 1
|
||||||
|
min_val = -1 - max_val
|
||||||
|
range_from = int(config[CONF_RANGE_FROM])
|
||||||
|
range_to = int(config[CONF_RANGE_TO])
|
||||||
|
step = int(config[CONF_STEP])
|
||||||
|
if (
|
||||||
|
range_from > max_val
|
||||||
|
or range_from < min_val
|
||||||
|
or range_to > max_val
|
||||||
|
or range_to < min_val
|
||||||
|
):
|
||||||
|
raise cv.Invalid("Range outside allowed limits")
|
||||||
|
if step <= 0 or step >= (range_to - range_from) / 2:
|
||||||
|
raise cv.Invalid("Invalid step value")
|
||||||
|
if config[CONF_DIGITS] <= config[CONF_DECIMAL_PLACES]:
|
||||||
|
raise cv.Invalid("Number of digits must exceed number of decimal places")
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
SPINBOX_SCHEMA = cv.Schema(
|
||||||
|
{
|
||||||
|
cv.Optional(CONF_VALUE): lv_float,
|
||||||
|
cv.Optional(CONF_RANGE_FROM, default=0): cv.float_,
|
||||||
|
cv.Optional(CONF_RANGE_TO, default=100): cv.float_,
|
||||||
|
cv.Optional(CONF_DIGITS, default=4): cv.int_range(1, 10),
|
||||||
|
cv.Optional(CONF_STEP, default=1.0): cv.positive_float,
|
||||||
|
cv.Optional(CONF_DECIMAL_PLACES, default=0): cv.int_range(0, 6),
|
||||||
|
cv.Optional(CONF_ROLLOVER, default=False): lv_bool,
|
||||||
|
}
|
||||||
|
).add_extra(validate_spinbox)
|
||||||
|
|
||||||
|
|
||||||
|
SPINBOX_MODIFY_SCHEMA = {
|
||||||
|
cv.Required(CONF_VALUE): lv_float,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class SpinboxType(WidgetType):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(
|
||||||
|
CONF_SPINBOX,
|
||||||
|
lv_spinbox_t,
|
||||||
|
(
|
||||||
|
CONF_MAIN,
|
||||||
|
CONF_SCROLLBAR,
|
||||||
|
CONF_SELECTED,
|
||||||
|
CONF_CURSOR,
|
||||||
|
CONF_TEXTAREA_PLACEHOLDER,
|
||||||
|
),
|
||||||
|
SPINBOX_SCHEMA,
|
||||||
|
SPINBOX_MODIFY_SCHEMA,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def to_code(self, w: Widget, config):
|
||||||
|
if CONF_DIGITS in config:
|
||||||
|
digits = config[CONF_DIGITS]
|
||||||
|
scale = 10 ** config[CONF_DECIMAL_PLACES]
|
||||||
|
range_from = int(config[CONF_RANGE_FROM]) * scale
|
||||||
|
range_to = int(config[CONF_RANGE_TO]) * scale
|
||||||
|
step = int(config[CONF_STEP]) * scale
|
||||||
|
w.scale = scale
|
||||||
|
w.step = step
|
||||||
|
w.range_to = range_to
|
||||||
|
w.range_from = range_from
|
||||||
|
lv.spinbox_set_range(w.obj, range_from, range_to)
|
||||||
|
await w.set_property(CONF_STEP, step)
|
||||||
|
await w.set_property(CONF_ROLLOVER, config)
|
||||||
|
lv.spinbox_set_digit_format(
|
||||||
|
w.obj, digits, digits - config[CONF_DECIMAL_PLACES]
|
||||||
|
)
|
||||||
|
if (value := config.get(CONF_VALUE)) is not None:
|
||||||
|
lv.spinbox_set_value(w.obj, await lv_float.process(value))
|
||||||
|
|
||||||
|
def get_scale(self, config):
|
||||||
|
return 10 ** config[CONF_DECIMAL_PLACES]
|
||||||
|
|
||||||
|
def get_uses(self):
|
||||||
|
return CONF_TEXTAREA, CONF_LABEL
|
||||||
|
|
||||||
|
def get_max(self, config: dict):
|
||||||
|
return config[CONF_RANGE_TO]
|
||||||
|
|
||||||
|
def get_min(self, config: dict):
|
||||||
|
return config[CONF_RANGE_FROM]
|
||||||
|
|
||||||
|
def get_step(self, config: dict):
|
||||||
|
return config[CONF_STEP]
|
||||||
|
|
||||||
|
|
||||||
|
spinbox_spec = SpinboxType()
|
||||||
|
|
||||||
|
|
||||||
|
@automation.register_action(
|
||||||
|
"lvgl.spinbox.increment",
|
||||||
|
ObjUpdateAction,
|
||||||
|
cv.maybe_simple_value(
|
||||||
|
{
|
||||||
|
cv.Required(CONF_ID): cv.use_id(lv_spinbox_t),
|
||||||
|
},
|
||||||
|
key=CONF_ID,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
async def spinbox_increment(config, action_id, template_arg, args):
|
||||||
|
widgets = await get_widgets(config)
|
||||||
|
|
||||||
|
async def do_increment(w: Widget):
|
||||||
|
lv.spinbox_increment(w.obj)
|
||||||
|
|
||||||
|
return await action_to_code(widgets, do_increment, action_id, template_arg, args)
|
||||||
|
|
||||||
|
|
||||||
|
@automation.register_action(
|
||||||
|
"lvgl.spinbox.decrement",
|
||||||
|
ObjUpdateAction,
|
||||||
|
cv.maybe_simple_value(
|
||||||
|
{
|
||||||
|
cv.Required(CONF_ID): cv.use_id(lv_spinbox_t),
|
||||||
|
},
|
||||||
|
key=CONF_ID,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
async def spinbox_decrement(config, action_id, template_arg, args):
|
||||||
|
widgets = await get_widgets(config)
|
||||||
|
|
||||||
|
async def do_increment(w: Widget):
|
||||||
|
lv.spinbox_decrement(w.obj)
|
||||||
|
|
||||||
|
return await action_to_code(widgets, do_increment, action_id, template_arg, args)
|
||||||
|
|
||||||
|
|
||||||
|
@automation.register_action(
|
||||||
|
"lvgl.spinbox.update",
|
||||||
|
ObjUpdateAction,
|
||||||
|
cv.Schema(
|
||||||
|
{
|
||||||
|
cv.Required(CONF_ID): cv.use_id(lv_spinbox_t),
|
||||||
|
cv.Required(CONF_VALUE): lv_float,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
async def spinbox_update_to_code(config, action_id, template_arg, args):
|
||||||
|
return await update_to_code(config, action_id, template_arg, args)
|
@ -1,12 +1,12 @@
|
|||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.cpp_generator import MockObjClass
|
from esphome.cpp_generator import MockObjClass
|
||||||
|
|
||||||
|
from ..defines import CONF_ARC_LENGTH, CONF_INDICATOR, CONF_MAIN, CONF_SPIN_TIME
|
||||||
|
from ..lv_validation import angle
|
||||||
|
from ..lvcode import lv_expr
|
||||||
|
from ..types import LvType
|
||||||
|
from . import Widget, WidgetType
|
||||||
from .arc import CONF_ARC
|
from .arc import CONF_ARC
|
||||||
from .defines import CONF_ARC_LENGTH, CONF_INDICATOR, CONF_MAIN, CONF_SPIN_TIME
|
|
||||||
from .lv_validation import angle
|
|
||||||
from .lvcode import lv_expr
|
|
||||||
from .types import LvType
|
|
||||||
from .widget import Widget, WidgetType
|
|
||||||
|
|
||||||
CONF_SPINNER = "spinner"
|
CONF_SPINNER = "spinner"
|
||||||
|
|
@ -1,6 +1,6 @@
|
|||||||
from .defines import CONF_INDICATOR, CONF_KNOB, CONF_MAIN
|
from ..defines import CONF_INDICATOR, CONF_KNOB, CONF_MAIN
|
||||||
from .types import LvBoolean
|
from ..types import LvBoolean
|
||||||
from .widget import WidgetType
|
from . import WidgetType
|
||||||
|
|
||||||
CONF_SWITCH = "switch"
|
CONF_SWITCH = "switch"
|
||||||
|
|
114
esphome/components/lvgl/widgets/tabview.py
Normal file
114
esphome/components/lvgl/widgets/tabview.py
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
from esphome import automation
|
||||||
|
import esphome.codegen as cg
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.const import CONF_ID, CONF_INDEX, CONF_NAME, CONF_POSITION, CONF_SIZE
|
||||||
|
from esphome.cpp_generator import MockObjClass
|
||||||
|
|
||||||
|
from ..automation import action_to_code
|
||||||
|
from ..defines import (
|
||||||
|
CONF_ANIMATED,
|
||||||
|
CONF_MAIN,
|
||||||
|
CONF_TAB_ID,
|
||||||
|
CONF_TABS,
|
||||||
|
DIRECTIONS,
|
||||||
|
TYPE_FLEX,
|
||||||
|
literal,
|
||||||
|
)
|
||||||
|
from ..lv_validation import animated, lv_int, size
|
||||||
|
from ..lvcode import LocalVariable, lv, lv_assign, lv_expr
|
||||||
|
from ..schemas import container_schema, part_schema
|
||||||
|
from ..types import LV_EVENT, LvType, ObjUpdateAction, lv_obj_t, lv_obj_t_ptr
|
||||||
|
from . import Widget, WidgetType, add_widgets, get_widgets, set_obj_properties
|
||||||
|
from .buttonmatrix import buttonmatrix_spec
|
||||||
|
from .obj import obj_spec
|
||||||
|
|
||||||
|
CONF_TABVIEW = "tabview"
|
||||||
|
CONF_TAB_STYLE = "tab_style"
|
||||||
|
|
||||||
|
lv_tab_t = LvType("lv_obj_t")
|
||||||
|
|
||||||
|
TABVIEW_SCHEMA = cv.Schema(
|
||||||
|
{
|
||||||
|
cv.Required(CONF_TABS): cv.ensure_list(
|
||||||
|
container_schema(
|
||||||
|
obj_spec,
|
||||||
|
{
|
||||||
|
cv.Required(CONF_NAME): cv.string,
|
||||||
|
cv.GenerateID(): cv.declare_id(lv_tab_t),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
),
|
||||||
|
cv.Optional(CONF_TAB_STYLE): part_schema(buttonmatrix_spec),
|
||||||
|
cv.Optional(CONF_POSITION, default="top"): DIRECTIONS.one_of,
|
||||||
|
cv.Optional(CONF_SIZE, default="10%"): size,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TabviewType(WidgetType):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(
|
||||||
|
CONF_TABVIEW,
|
||||||
|
LvType(
|
||||||
|
"lv_tabview_t",
|
||||||
|
largs=[(lv_obj_t_ptr, "tab")],
|
||||||
|
lvalue=lambda w: lv_expr.obj_get_child(
|
||||||
|
lv_expr.tabview_get_content(w.obj),
|
||||||
|
lv_expr.tabview_get_tab_act(w.obj),
|
||||||
|
),
|
||||||
|
has_on_value=True,
|
||||||
|
),
|
||||||
|
parts=(CONF_MAIN,),
|
||||||
|
schema=TABVIEW_SCHEMA,
|
||||||
|
modify_schema={},
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_uses(self):
|
||||||
|
return "btnmatrix", TYPE_FLEX
|
||||||
|
|
||||||
|
async def to_code(self, w: Widget, config: dict):
|
||||||
|
for tab_conf in config[CONF_TABS]:
|
||||||
|
w_id = tab_conf[CONF_ID]
|
||||||
|
tab_obj = cg.Pvariable(w_id, cg.nullptr, type_=lv_tab_t)
|
||||||
|
tab_widget = Widget.create(w_id, tab_obj, obj_spec)
|
||||||
|
lv_assign(tab_obj, lv_expr.tabview_add_tab(w.obj, tab_conf[CONF_NAME]))
|
||||||
|
await set_obj_properties(tab_widget, tab_conf)
|
||||||
|
await add_widgets(tab_widget, tab_conf)
|
||||||
|
if button_style := config.get(CONF_TAB_STYLE):
|
||||||
|
with LocalVariable(
|
||||||
|
"tabview_btnmatrix", lv_obj_t, rhs=lv_expr.tabview_get_tab_btns(w.obj)
|
||||||
|
) as btnmatrix_obj:
|
||||||
|
await set_obj_properties(Widget(btnmatrix_obj, obj_spec), button_style)
|
||||||
|
|
||||||
|
def obj_creator(self, parent: MockObjClass, config: dict):
|
||||||
|
return lv_expr.call(
|
||||||
|
"tabview_create",
|
||||||
|
parent,
|
||||||
|
literal(config[CONF_POSITION]),
|
||||||
|
literal(config[CONF_SIZE]),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
tabview_spec = TabviewType()
|
||||||
|
|
||||||
|
|
||||||
|
@automation.register_action(
|
||||||
|
"lvgl.tabview.select",
|
||||||
|
ObjUpdateAction,
|
||||||
|
cv.Schema(
|
||||||
|
{
|
||||||
|
cv.Required(CONF_ID): cv.use_id(tabview_spec.w_type),
|
||||||
|
cv.Optional(CONF_ANIMATED, default=False): animated,
|
||||||
|
cv.Required(CONF_INDEX): lv_int,
|
||||||
|
},
|
||||||
|
).add_extra(cv.has_at_least_one_key(CONF_INDEX, CONF_TAB_ID)),
|
||||||
|
)
|
||||||
|
async def tabview_select(config, action_id, template_arg, args):
|
||||||
|
widget = await get_widgets(config)
|
||||||
|
index = config[CONF_INDEX]
|
||||||
|
|
||||||
|
async def do_select(w: Widget):
|
||||||
|
lv.tabview_set_act(w.obj, index, literal(config[CONF_ANIMATED]))
|
||||||
|
lv.event_send(w.obj, LV_EVENT.VALUE_CHANGED, cg.nullptr)
|
||||||
|
|
||||||
|
return await action_to_code(widget, do_select, action_id, template_arg, args)
|
67
esphome/components/lvgl/widgets/textarea.py
Normal file
67
esphome/components/lvgl/widgets/textarea.py
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.const import CONF_MAX_LENGTH
|
||||||
|
|
||||||
|
from ..defines import (
|
||||||
|
CONF_ACCEPTED_CHARS,
|
||||||
|
CONF_CURSOR,
|
||||||
|
CONF_MAIN,
|
||||||
|
CONF_ONE_LINE,
|
||||||
|
CONF_PASSWORD_MODE,
|
||||||
|
CONF_PLACEHOLDER_TEXT,
|
||||||
|
CONF_SCROLLBAR,
|
||||||
|
CONF_SELECTED,
|
||||||
|
CONF_TEXT,
|
||||||
|
CONF_TEXTAREA_PLACEHOLDER,
|
||||||
|
)
|
||||||
|
from ..lv_validation import lv_bool, lv_int, lv_text
|
||||||
|
from ..schemas import TEXT_SCHEMA
|
||||||
|
from ..types import LvText
|
||||||
|
from . import Widget, WidgetType
|
||||||
|
|
||||||
|
CONF_TEXTAREA = "textarea"
|
||||||
|
|
||||||
|
lv_textarea_t = LvText("lv_textarea_t")
|
||||||
|
|
||||||
|
TEXTAREA_SCHEMA = TEXT_SCHEMA.extend(
|
||||||
|
{
|
||||||
|
cv.Optional(CONF_PLACEHOLDER_TEXT): lv_text,
|
||||||
|
cv.Optional(CONF_ACCEPTED_CHARS): lv_text,
|
||||||
|
cv.Optional(CONF_ONE_LINE): lv_bool,
|
||||||
|
cv.Optional(CONF_PASSWORD_MODE): lv_bool,
|
||||||
|
cv.Optional(CONF_MAX_LENGTH): lv_int,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TextareaType(WidgetType):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(
|
||||||
|
CONF_TEXTAREA,
|
||||||
|
lv_textarea_t,
|
||||||
|
(
|
||||||
|
CONF_MAIN,
|
||||||
|
CONF_SCROLLBAR,
|
||||||
|
CONF_SELECTED,
|
||||||
|
CONF_CURSOR,
|
||||||
|
CONF_TEXTAREA_PLACEHOLDER,
|
||||||
|
),
|
||||||
|
TEXTAREA_SCHEMA,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def to_code(self, w: Widget, config: dict):
|
||||||
|
for prop in (CONF_TEXT, CONF_PLACEHOLDER_TEXT, CONF_ACCEPTED_CHARS):
|
||||||
|
if (value := config.get(prop)) is not None:
|
||||||
|
await w.set_property(prop, await lv_text.process(value))
|
||||||
|
await w.set_property(
|
||||||
|
CONF_MAX_LENGTH, await lv_int.process(config.get(CONF_MAX_LENGTH))
|
||||||
|
)
|
||||||
|
await w.set_property(
|
||||||
|
CONF_PASSWORD_MODE,
|
||||||
|
await lv_bool.process(config.get(CONF_PASSWORD_MODE)),
|
||||||
|
)
|
||||||
|
await w.set_property(
|
||||||
|
CONF_ONE_LINE, await lv_bool.process(config.get(CONF_ONE_LINE))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
textarea_spec = TextareaType()
|
128
esphome/components/lvgl/widgets/tileview.py
Normal file
128
esphome/components/lvgl/widgets/tileview.py
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
from esphome import automation
|
||||||
|
import esphome.codegen as cg
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.const import CONF_ID, CONF_ON_VALUE, CONF_ROW, CONF_TRIGGER_ID
|
||||||
|
|
||||||
|
from ..automation import action_to_code
|
||||||
|
from ..defines import (
|
||||||
|
CONF_ANIMATED,
|
||||||
|
CONF_COLUMN,
|
||||||
|
CONF_DIR,
|
||||||
|
CONF_MAIN,
|
||||||
|
CONF_TILE_ID,
|
||||||
|
CONF_TILES,
|
||||||
|
TILE_DIRECTIONS,
|
||||||
|
literal,
|
||||||
|
)
|
||||||
|
from ..lv_validation import animated, lv_int
|
||||||
|
from ..lvcode import lv, lv_assign, lv_expr, lv_obj, lv_Pvariable
|
||||||
|
from ..schemas import container_schema
|
||||||
|
from ..types import LV_EVENT, LvType, ObjUpdateAction, lv_obj_t, lv_obj_t_ptr
|
||||||
|
from . import Widget, WidgetType, add_widgets, get_widgets, set_obj_properties
|
||||||
|
from .obj import obj_spec
|
||||||
|
|
||||||
|
CONF_TILEVIEW = "tileview"
|
||||||
|
|
||||||
|
lv_tile_t = LvType("lv_tileview_tile_t")
|
||||||
|
|
||||||
|
lv_tileview_t = LvType(
|
||||||
|
"lv_tileview_t",
|
||||||
|
largs=[(lv_obj_t_ptr, "tile")],
|
||||||
|
lvalue=lambda w: w.get_property("tile_act"),
|
||||||
|
)
|
||||||
|
|
||||||
|
tile_spec = WidgetType("lv_tileview_tile_t", lv_tile_t, (CONF_MAIN,), {})
|
||||||
|
|
||||||
|
TILEVIEW_SCHEMA = cv.Schema(
|
||||||
|
{
|
||||||
|
cv.Required(CONF_TILES): cv.ensure_list(
|
||||||
|
container_schema(
|
||||||
|
obj_spec,
|
||||||
|
{
|
||||||
|
cv.Required(CONF_ROW): lv_int,
|
||||||
|
cv.Required(CONF_COLUMN): lv_int,
|
||||||
|
cv.GenerateID(): cv.declare_id(lv_tile_t),
|
||||||
|
cv.Optional(CONF_DIR, default="ALL"): TILE_DIRECTIONS.several_of,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
),
|
||||||
|
cv.Optional(CONF_ON_VALUE): automation.validate_automation(
|
||||||
|
{
|
||||||
|
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||||
|
automation.Trigger.template(lv_obj_t_ptr)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TileviewType(WidgetType):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(
|
||||||
|
CONF_TILEVIEW,
|
||||||
|
lv_tileview_t,
|
||||||
|
(CONF_MAIN,),
|
||||||
|
schema=TILEVIEW_SCHEMA,
|
||||||
|
modify_schema={},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def to_code(self, w: Widget, config: dict):
|
||||||
|
for tile_conf in config.get(CONF_TILES, ()):
|
||||||
|
w_id = tile_conf[CONF_ID]
|
||||||
|
tile_obj = lv_Pvariable(lv_obj_t, w_id)
|
||||||
|
tile = Widget.create(w_id, tile_obj, tile_spec, tile_conf)
|
||||||
|
dirs = tile_conf[CONF_DIR]
|
||||||
|
if isinstance(dirs, list):
|
||||||
|
dirs = "|".join(dirs)
|
||||||
|
lv_assign(
|
||||||
|
tile_obj,
|
||||||
|
lv_expr.tileview_add_tile(
|
||||||
|
w.obj, tile_conf[CONF_COLUMN], tile_conf[CONF_ROW], literal(dirs)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
await set_obj_properties(tile, tile_conf)
|
||||||
|
await add_widgets(tile, tile_conf)
|
||||||
|
|
||||||
|
|
||||||
|
tileview_spec = TileviewType()
|
||||||
|
|
||||||
|
|
||||||
|
def tile_select_validate(config):
|
||||||
|
row = CONF_ROW in config
|
||||||
|
column = CONF_COLUMN in config
|
||||||
|
tile = CONF_TILE_ID in config
|
||||||
|
if tile and (row or column) or not tile and not (row and column):
|
||||||
|
raise cv.Invalid("Specify either a tile id, or both a row and a column")
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
@automation.register_action(
|
||||||
|
"lvgl.tileview.select",
|
||||||
|
ObjUpdateAction,
|
||||||
|
cv.Schema(
|
||||||
|
{
|
||||||
|
cv.Required(CONF_ID): cv.use_id(lv_tileview_t),
|
||||||
|
cv.Optional(CONF_ANIMATED, default=False): animated,
|
||||||
|
cv.Optional(CONF_ROW): lv_int,
|
||||||
|
cv.Optional(CONF_COLUMN): lv_int,
|
||||||
|
cv.Optional(CONF_TILE_ID): cv.use_id(lv_tile_t),
|
||||||
|
},
|
||||||
|
).add_extra(tile_select_validate),
|
||||||
|
)
|
||||||
|
async def tileview_select(config, action_id, template_arg, args):
|
||||||
|
widgets = await get_widgets(config)
|
||||||
|
|
||||||
|
async def do_select(w: Widget):
|
||||||
|
if tile := config.get(CONF_TILE_ID):
|
||||||
|
tile = await cg.get_variable(tile)
|
||||||
|
lv_obj.set_tile(w.obj, tile, literal(config[CONF_ANIMATED]))
|
||||||
|
else:
|
||||||
|
row = await lv_int.process(config[CONF_ROW])
|
||||||
|
column = await lv_int.process(config[CONF_COLUMN])
|
||||||
|
lv_obj.set_tile_id(
|
||||||
|
widgets[0].obj, column, row, literal(config[CONF_ANIMATED])
|
||||||
|
)
|
||||||
|
lv.event_send(w.obj, LV_EVENT.VALUE_CHANGED, cg.nullptr)
|
||||||
|
|
||||||
|
return await action_to_code(widgets, do_select, action_id, template_arg, args)
|
@ -1,10 +1,11 @@
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
import esphome.codegen as cg
|
|
||||||
import esphome.config_validation as cv
|
|
||||||
from esphome import automation
|
from esphome import automation
|
||||||
from esphome.automation import Condition
|
from esphome.automation import Condition
|
||||||
|
import esphome.codegen as cg
|
||||||
from esphome.components import logger
|
from esphome.components import logger
|
||||||
|
from esphome.components.esp32 import add_idf_sdkconfig_option
|
||||||
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
CONF_AVAILABILITY,
|
CONF_AVAILABILITY,
|
||||||
CONF_BIRTH_MESSAGE,
|
CONF_BIRTH_MESSAGE,
|
||||||
@ -13,21 +14,21 @@ from esphome.const import (
|
|||||||
CONF_CLIENT_CERTIFICATE,
|
CONF_CLIENT_CERTIFICATE,
|
||||||
CONF_CLIENT_CERTIFICATE_KEY,
|
CONF_CLIENT_CERTIFICATE_KEY,
|
||||||
CONF_CLIENT_ID,
|
CONF_CLIENT_ID,
|
||||||
CONF_COMMAND_TOPIC,
|
|
||||||
CONF_COMMAND_RETAIN,
|
CONF_COMMAND_RETAIN,
|
||||||
|
CONF_COMMAND_TOPIC,
|
||||||
CONF_DISCOVERY,
|
CONF_DISCOVERY,
|
||||||
|
CONF_DISCOVERY_OBJECT_ID_GENERATOR,
|
||||||
CONF_DISCOVERY_PREFIX,
|
CONF_DISCOVERY_PREFIX,
|
||||||
CONF_DISCOVERY_RETAIN,
|
CONF_DISCOVERY_RETAIN,
|
||||||
CONF_DISCOVERY_UNIQUE_ID_GENERATOR,
|
CONF_DISCOVERY_UNIQUE_ID_GENERATOR,
|
||||||
CONF_DISCOVERY_OBJECT_ID_GENERATOR,
|
|
||||||
CONF_ID,
|
CONF_ID,
|
||||||
CONF_KEEPALIVE,
|
CONF_KEEPALIVE,
|
||||||
CONF_LEVEL,
|
CONF_LEVEL,
|
||||||
CONF_LOG_TOPIC,
|
CONF_LOG_TOPIC,
|
||||||
CONF_ON_JSON_MESSAGE,
|
|
||||||
CONF_ON_MESSAGE,
|
|
||||||
CONF_ON_CONNECT,
|
CONF_ON_CONNECT,
|
||||||
CONF_ON_DISCONNECT,
|
CONF_ON_DISCONNECT,
|
||||||
|
CONF_ON_JSON_MESSAGE,
|
||||||
|
CONF_ON_MESSAGE,
|
||||||
CONF_PASSWORD,
|
CONF_PASSWORD,
|
||||||
CONF_PAYLOAD,
|
CONF_PAYLOAD,
|
||||||
CONF_PAYLOAD_AVAILABLE,
|
CONF_PAYLOAD_AVAILABLE,
|
||||||
@ -45,12 +46,11 @@ from esphome.const import (
|
|||||||
CONF_USE_ABBREVIATIONS,
|
CONF_USE_ABBREVIATIONS,
|
||||||
CONF_USERNAME,
|
CONF_USERNAME,
|
||||||
CONF_WILL_MESSAGE,
|
CONF_WILL_MESSAGE,
|
||||||
|
PLATFORM_BK72XX,
|
||||||
PLATFORM_ESP32,
|
PLATFORM_ESP32,
|
||||||
PLATFORM_ESP8266,
|
PLATFORM_ESP8266,
|
||||||
PLATFORM_BK72XX,
|
|
||||||
)
|
)
|
||||||
from esphome.core import coroutine_with_priority, CORE
|
from esphome.core import CORE, coroutine_with_priority
|
||||||
from esphome.components.esp32 import add_idf_sdkconfig_option
|
|
||||||
|
|
||||||
DEPENDENCIES = ["network"]
|
DEPENDENCIES = ["network"]
|
||||||
|
|
||||||
@ -110,6 +110,9 @@ MQTTDisconnectTrigger = mqtt_ns.class_(
|
|||||||
MQTTComponent = mqtt_ns.class_("MQTTComponent", cg.Component)
|
MQTTComponent = mqtt_ns.class_("MQTTComponent", cg.Component)
|
||||||
MQTTConnectedCondition = mqtt_ns.class_("MQTTConnectedCondition", Condition)
|
MQTTConnectedCondition = mqtt_ns.class_("MQTTConnectedCondition", Condition)
|
||||||
|
|
||||||
|
MQTTAlarmControlPanelComponent = mqtt_ns.class_(
|
||||||
|
"MQTTAlarmControlPanelComponent", MQTTComponent
|
||||||
|
)
|
||||||
MQTTBinarySensorComponent = mqtt_ns.class_("MQTTBinarySensorComponent", MQTTComponent)
|
MQTTBinarySensorComponent = mqtt_ns.class_("MQTTBinarySensorComponent", MQTTComponent)
|
||||||
MQTTClimateComponent = mqtt_ns.class_("MQTTClimateComponent", MQTTComponent)
|
MQTTClimateComponent = mqtt_ns.class_("MQTTClimateComponent", MQTTComponent)
|
||||||
MQTTCoverComponent = mqtt_ns.class_("MQTTCoverComponent", MQTTComponent)
|
MQTTCoverComponent = mqtt_ns.class_("MQTTCoverComponent", MQTTComponent)
|
||||||
|
128
esphome/components/mqtt/mqtt_alarm_control_panel.cpp
Normal file
128
esphome/components/mqtt/mqtt_alarm_control_panel.cpp
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
#include "mqtt_alarm_control_panel.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
#include "mqtt_const.h"
|
||||||
|
|
||||||
|
#ifdef USE_MQTT
|
||||||
|
#ifdef USE_ALARM_CONTROL_PANEL
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace mqtt {
|
||||||
|
|
||||||
|
static const char *const TAG = "mqtt.alarm_control_panel";
|
||||||
|
|
||||||
|
using namespace esphome::alarm_control_panel;
|
||||||
|
|
||||||
|
MQTTAlarmControlPanelComponent::MQTTAlarmControlPanelComponent(AlarmControlPanel *alarm_control_panel)
|
||||||
|
: alarm_control_panel_(alarm_control_panel) {}
|
||||||
|
void MQTTAlarmControlPanelComponent::setup() {
|
||||||
|
this->alarm_control_panel_->add_on_state_callback([this]() { this->publish_state(); });
|
||||||
|
this->subscribe(this->get_command_topic_(), [this](const std::string &topic, const std::string &payload) {
|
||||||
|
auto call = this->alarm_control_panel_->make_call();
|
||||||
|
if (strcasecmp(payload.c_str(), "ARM_AWAY") == 0) {
|
||||||
|
call.arm_away();
|
||||||
|
} else if (strcasecmp(payload.c_str(), "ARM_HOME") == 0) {
|
||||||
|
call.arm_home();
|
||||||
|
} else if (strcasecmp(payload.c_str(), "ARM_NIGHT") == 0) {
|
||||||
|
call.arm_night();
|
||||||
|
} else if (strcasecmp(payload.c_str(), "ARM_VACATION") == 0) {
|
||||||
|
call.arm_vacation();
|
||||||
|
} else if (strcasecmp(payload.c_str(), "ARM_CUSTOM_BYPASS") == 0) {
|
||||||
|
call.arm_custom_bypass();
|
||||||
|
} else if (strcasecmp(payload.c_str(), "DISARM") == 0) {
|
||||||
|
call.disarm();
|
||||||
|
} else if (strcasecmp(payload.c_str(), "PENDING") == 0) {
|
||||||
|
call.pending();
|
||||||
|
} else if (strcasecmp(payload.c_str(), "TRIGGERED") == 0) {
|
||||||
|
call.triggered();
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(TAG, "'%s': Received unknown command payload %s", this->friendly_name().c_str(), payload.c_str());
|
||||||
|
}
|
||||||
|
call.perform();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void MQTTAlarmControlPanelComponent::dump_config() {
|
||||||
|
ESP_LOGCONFIG(TAG, "MQTT alarm_control_panel '%s':", this->alarm_control_panel_->get_name().c_str());
|
||||||
|
LOG_MQTT_COMPONENT(true, true)
|
||||||
|
ESP_LOGCONFIG(TAG, " Supported Features: %" PRIu32, this->alarm_control_panel_->get_supported_features());
|
||||||
|
ESP_LOGCONFIG(TAG, " Requires Code to Disarm: %s", YESNO(this->alarm_control_panel_->get_requires_code()));
|
||||||
|
ESP_LOGCONFIG(TAG, " Requires Code To Arm: %s", YESNO(this->alarm_control_panel_->get_requires_code_to_arm()));
|
||||||
|
}
|
||||||
|
|
||||||
|
void MQTTAlarmControlPanelComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||||
|
JsonArray supported_features = root.createNestedArray(MQTT_SUPPORTED_FEATURES);
|
||||||
|
const uint32_t acp_supported_features = this->alarm_control_panel_->get_supported_features();
|
||||||
|
if (acp_supported_features & ACP_FEAT_ARM_AWAY) {
|
||||||
|
supported_features.add("arm_away");
|
||||||
|
}
|
||||||
|
if (acp_supported_features & ACP_FEAT_ARM_HOME) {
|
||||||
|
supported_features.add("arm_home");
|
||||||
|
}
|
||||||
|
if (acp_supported_features & ACP_FEAT_ARM_NIGHT) {
|
||||||
|
supported_features.add("arm_night");
|
||||||
|
}
|
||||||
|
if (acp_supported_features & ACP_FEAT_ARM_VACATION) {
|
||||||
|
supported_features.add("arm_vacation");
|
||||||
|
}
|
||||||
|
if (acp_supported_features & ACP_FEAT_ARM_CUSTOM_BYPASS) {
|
||||||
|
supported_features.add("arm_custom_bypass");
|
||||||
|
}
|
||||||
|
if (acp_supported_features & ACP_FEAT_TRIGGER) {
|
||||||
|
supported_features.add("trigger");
|
||||||
|
}
|
||||||
|
root[MQTT_CODE_DISARM_REQUIRED] = this->alarm_control_panel_->get_requires_code();
|
||||||
|
root[MQTT_CODE_ARM_REQUIRED] = this->alarm_control_panel_->get_requires_code_to_arm();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string MQTTAlarmControlPanelComponent::component_type() const { return "alarm_control_panel"; }
|
||||||
|
const EntityBase *MQTTAlarmControlPanelComponent::get_entity() const { return this->alarm_control_panel_; }
|
||||||
|
|
||||||
|
bool MQTTAlarmControlPanelComponent::send_initial_state() { return this->publish_state(); }
|
||||||
|
bool MQTTAlarmControlPanelComponent::publish_state() {
|
||||||
|
bool success = true;
|
||||||
|
const char *state_s = "";
|
||||||
|
switch (this->alarm_control_panel_->get_state()) {
|
||||||
|
case ACP_STATE_DISARMED:
|
||||||
|
state_s = "disarmed";
|
||||||
|
break;
|
||||||
|
case ACP_STATE_ARMED_HOME:
|
||||||
|
state_s = "armed_home";
|
||||||
|
break;
|
||||||
|
case ACP_STATE_ARMED_AWAY:
|
||||||
|
state_s = "armed_away";
|
||||||
|
break;
|
||||||
|
case ACP_STATE_ARMED_NIGHT:
|
||||||
|
state_s = "armed_night";
|
||||||
|
break;
|
||||||
|
case ACP_STATE_ARMED_VACATION:
|
||||||
|
state_s = "armed_vacation";
|
||||||
|
break;
|
||||||
|
case ACP_STATE_ARMED_CUSTOM_BYPASS:
|
||||||
|
state_s = "armed_custom_bypass";
|
||||||
|
break;
|
||||||
|
case ACP_STATE_PENDING:
|
||||||
|
state_s = "pending";
|
||||||
|
break;
|
||||||
|
case ACP_STATE_ARMING:
|
||||||
|
state_s = "arming";
|
||||||
|
break;
|
||||||
|
case ACP_STATE_DISARMING:
|
||||||
|
state_s = "disarming";
|
||||||
|
break;
|
||||||
|
case ACP_STATE_TRIGGERED:
|
||||||
|
state_s = "triggered";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
state_s = "unknown";
|
||||||
|
}
|
||||||
|
if (!this->publish(this->get_state_topic_(), state_s))
|
||||||
|
success = false;
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace mqtt
|
||||||
|
} // namespace esphome
|
||||||
|
|
||||||
|
#endif
|
||||||
|
#endif // USE_MQTT
|
39
esphome/components/mqtt/mqtt_alarm_control_panel.h
Normal file
39
esphome/components/mqtt/mqtt_alarm_control_panel.h
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/core/defines.h"
|
||||||
|
|
||||||
|
#ifdef USE_MQTT
|
||||||
|
#ifdef USE_ALARM_CONTROL_PANEL
|
||||||
|
|
||||||
|
#include "mqtt_component.h"
|
||||||
|
#include "esphome/components/alarm_control_panel/alarm_control_panel.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace mqtt {
|
||||||
|
|
||||||
|
class MQTTAlarmControlPanelComponent : public mqtt::MQTTComponent {
|
||||||
|
public:
|
||||||
|
explicit MQTTAlarmControlPanelComponent(alarm_control_panel::AlarmControlPanel *alarm_control_panel);
|
||||||
|
|
||||||
|
void setup() override;
|
||||||
|
|
||||||
|
void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override;
|
||||||
|
|
||||||
|
bool send_initial_state() override;
|
||||||
|
|
||||||
|
bool publish_state();
|
||||||
|
|
||||||
|
void dump_config() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
std::string component_type() const override;
|
||||||
|
const EntityBase *get_entity() const override;
|
||||||
|
|
||||||
|
alarm_control_panel::AlarmControlPanel *alarm_control_panel_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace mqtt
|
||||||
|
} // namespace esphome
|
||||||
|
|
||||||
|
#endif
|
||||||
|
#endif // USE_MQTT
|
@ -1,12 +1,11 @@
|
|||||||
from string import ascii_letters, digits
|
from string import ascii_letters, digits
|
||||||
import esphome.config_validation as cv
|
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components import color
|
from esphome.components import color
|
||||||
from esphome.const import (
|
import esphome.config_validation as cv
|
||||||
CONF_VISIBLE,
|
from esphome.const import CONF_BACKGROUND_COLOR, CONF_FOREGROUND_COLOR, CONF_VISIBLE
|
||||||
)
|
|
||||||
from . import CONF_NEXTION_ID
|
from . import CONF_NEXTION_ID, Nextion
|
||||||
from . import Nextion
|
|
||||||
|
|
||||||
CONF_VARIABLE_NAME = "variable_name"
|
CONF_VARIABLE_NAME = "variable_name"
|
||||||
CONF_COMPONENT_NAME = "component_name"
|
CONF_COMPONENT_NAME = "component_name"
|
||||||
@ -24,9 +23,7 @@ CONF_WAKE_UP_PAGE = "wake_up_page"
|
|||||||
CONF_START_UP_PAGE = "start_up_page"
|
CONF_START_UP_PAGE = "start_up_page"
|
||||||
CONF_AUTO_WAKE_ON_TOUCH = "auto_wake_on_touch"
|
CONF_AUTO_WAKE_ON_TOUCH = "auto_wake_on_touch"
|
||||||
CONF_WAVE_MAX_LENGTH = "wave_max_length"
|
CONF_WAVE_MAX_LENGTH = "wave_max_length"
|
||||||
CONF_BACKGROUND_COLOR = "background_color"
|
|
||||||
CONF_BACKGROUND_PRESSED_COLOR = "background_pressed_color"
|
CONF_BACKGROUND_PRESSED_COLOR = "background_pressed_color"
|
||||||
CONF_FOREGROUND_COLOR = "foreground_color"
|
|
||||||
CONF_FOREGROUND_PRESSED_COLOR = "foreground_pressed_color"
|
CONF_FOREGROUND_PRESSED_COLOR = "foreground_pressed_color"
|
||||||
CONF_FONT_ID = "font_id"
|
CONF_FONT_ID = "font_id"
|
||||||
CONF_EXIT_REPARSE_ON_START = "exit_reparse_on_start"
|
CONF_EXIT_REPARSE_ON_START = "exit_reparse_on_start"
|
||||||
|
161
esphome/components/online_image/__init__.py
Normal file
161
esphome/components/online_image/__init__.py
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from esphome import automation
|
||||||
|
import esphome.codegen as cg
|
||||||
|
from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent
|
||||||
|
from esphome.components.image import (
|
||||||
|
CONF_USE_TRANSPARENCY,
|
||||||
|
IMAGE_TYPE,
|
||||||
|
Image_,
|
||||||
|
validate_cross_dependencies,
|
||||||
|
)
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.const import (
|
||||||
|
CONF_BUFFER_SIZE,
|
||||||
|
CONF_FORMAT,
|
||||||
|
CONF_ID,
|
||||||
|
CONF_ON_ERROR,
|
||||||
|
CONF_RESIZE,
|
||||||
|
CONF_TRIGGER_ID,
|
||||||
|
CONF_TYPE,
|
||||||
|
CONF_URL,
|
||||||
|
)
|
||||||
|
|
||||||
|
AUTO_LOAD = ["image"]
|
||||||
|
DEPENDENCIES = ["display", "http_request"]
|
||||||
|
CODEOWNERS = ["@guillempages"]
|
||||||
|
MULTI_CONF = True
|
||||||
|
|
||||||
|
CONF_ON_DOWNLOAD_FINISHED = "on_download_finished"
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
online_image_ns = cg.esphome_ns.namespace("online_image")
|
||||||
|
|
||||||
|
ImageFormat = online_image_ns.enum("ImageFormat")
|
||||||
|
|
||||||
|
FORMAT_PNG = "PNG"
|
||||||
|
|
||||||
|
IMAGE_FORMAT = {FORMAT_PNG: ImageFormat.PNG} # Add new supported formats here
|
||||||
|
|
||||||
|
OnlineImage = online_image_ns.class_("OnlineImage", cg.PollingComponent, Image_)
|
||||||
|
|
||||||
|
# Actions
|
||||||
|
SetUrlAction = online_image_ns.class_(
|
||||||
|
"OnlineImageSetUrlAction", automation.Action, cg.Parented.template(OnlineImage)
|
||||||
|
)
|
||||||
|
ReleaseImageAction = online_image_ns.class_(
|
||||||
|
"OnlineImageReleaseAction", automation.Action, cg.Parented.template(OnlineImage)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Triggers
|
||||||
|
DownloadFinishedTrigger = online_image_ns.class_(
|
||||||
|
"DownloadFinishedTrigger", automation.Trigger.template()
|
||||||
|
)
|
||||||
|
DownloadErrorTrigger = online_image_ns.class_(
|
||||||
|
"DownloadErrorTrigger", automation.Trigger.template()
|
||||||
|
)
|
||||||
|
|
||||||
|
ONLINE_IMAGE_SCHEMA = cv.Schema(
|
||||||
|
{
|
||||||
|
cv.Required(CONF_ID): cv.declare_id(OnlineImage),
|
||||||
|
cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent),
|
||||||
|
#
|
||||||
|
# Common image options
|
||||||
|
#
|
||||||
|
cv.Optional(CONF_RESIZE): cv.dimensions,
|
||||||
|
cv.Optional(CONF_TYPE, default="BINARY"): cv.enum(IMAGE_TYPE, upper=True),
|
||||||
|
# Not setting default here on purpose; the default depends on the image type,
|
||||||
|
# and thus will be set in the "validate_cross_dependencies" validator.
|
||||||
|
cv.Optional(CONF_USE_TRANSPARENCY): cv.boolean,
|
||||||
|
#
|
||||||
|
# Online Image specific options
|
||||||
|
#
|
||||||
|
cv.Required(CONF_URL): cv.url,
|
||||||
|
cv.Required(CONF_FORMAT): cv.enum(IMAGE_FORMAT, upper=True),
|
||||||
|
cv.Optional(CONF_BUFFER_SIZE, default=2048): cv.int_range(256, 65536),
|
||||||
|
cv.Optional(CONF_ON_DOWNLOAD_FINISHED): automation.validate_automation(
|
||||||
|
{
|
||||||
|
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DownloadFinishedTrigger),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
cv.Optional(CONF_ON_ERROR): automation.validate_automation(
|
||||||
|
{
|
||||||
|
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DownloadErrorTrigger),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
).extend(cv.polling_component_schema("never"))
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = cv.Schema(
|
||||||
|
cv.All(
|
||||||
|
ONLINE_IMAGE_SCHEMA,
|
||||||
|
validate_cross_dependencies,
|
||||||
|
cv.require_framework_version(
|
||||||
|
# esp8266 not supported yet; if enabled in the future, minimum version of 2.7.0 is needed
|
||||||
|
# esp8266_arduino=cv.Version(2, 7, 0),
|
||||||
|
esp32_arduino=cv.Version(0, 0, 0),
|
||||||
|
esp_idf=cv.Version(4, 0, 0),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
SET_URL_SCHEMA = cv.Schema(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.use_id(OnlineImage),
|
||||||
|
cv.Required(CONF_URL): cv.templatable(cv.url),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
RELEASE_IMAGE_SCHEMA = automation.maybe_simple_id(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.use_id(OnlineImage),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@automation.register_action("online_image.set_url", SetUrlAction, SET_URL_SCHEMA)
|
||||||
|
@automation.register_action(
|
||||||
|
"online_image.release", ReleaseImageAction, RELEASE_IMAGE_SCHEMA
|
||||||
|
)
|
||||||
|
async def online_image_action_to_code(config, action_id, template_arg, args):
|
||||||
|
paren = await cg.get_variable(config[CONF_ID])
|
||||||
|
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||||
|
|
||||||
|
if CONF_URL in config:
|
||||||
|
template_ = await cg.templatable(config[CONF_URL], args, cg.const_char_ptr)
|
||||||
|
cg.add(var.set_url(template_))
|
||||||
|
return var
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
format = config[CONF_FORMAT]
|
||||||
|
if format in [FORMAT_PNG]:
|
||||||
|
cg.add_define("USE_ONLINE_IMAGE_PNG_SUPPORT")
|
||||||
|
cg.add_library("pngle", "1.0.2")
|
||||||
|
|
||||||
|
url = config[CONF_URL]
|
||||||
|
width, height = config.get(CONF_RESIZE, (0, 0))
|
||||||
|
transparent = config[CONF_USE_TRANSPARENCY]
|
||||||
|
|
||||||
|
var = cg.new_Pvariable(
|
||||||
|
config[CONF_ID],
|
||||||
|
url,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
format,
|
||||||
|
config[CONF_TYPE],
|
||||||
|
config[CONF_BUFFER_SIZE],
|
||||||
|
)
|
||||||
|
await cg.register_component(var, config)
|
||||||
|
await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID])
|
||||||
|
|
||||||
|
cg.add(var.set_transparency(transparent))
|
||||||
|
|
||||||
|
for conf in config.get(CONF_ON_DOWNLOAD_FINISHED, []):
|
||||||
|
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||||
|
await automation.build_automation(trigger, [], conf)
|
||||||
|
|
||||||
|
for conf in config.get(CONF_ON_ERROR, []):
|
||||||
|
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||||
|
await automation.build_automation(trigger, [], conf)
|
44
esphome/components/online_image/image_decoder.cpp
Normal file
44
esphome/components/online_image/image_decoder.cpp
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
#include "image_decoder.h"
|
||||||
|
#include "online_image.h"
|
||||||
|
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace online_image {
|
||||||
|
|
||||||
|
static const char *const TAG = "online_image.decoder";
|
||||||
|
|
||||||
|
void ImageDecoder::set_size(int width, int height) {
|
||||||
|
this->image_->resize_(width, height);
|
||||||
|
this->x_scale_ = static_cast<double>(this->image_->buffer_width_) / width;
|
||||||
|
this->y_scale_ = static_cast<double>(this->image_->buffer_height_) / height;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ImageDecoder::draw(int x, int y, int w, int h, const Color &color) {
|
||||||
|
auto width = std::min(this->image_->buffer_width_, static_cast<int>(std::ceil((x + w) * this->x_scale_)));
|
||||||
|
auto height = std::min(this->image_->buffer_height_, static_cast<int>(std::ceil((y + h) * this->y_scale_)));
|
||||||
|
for (int i = x * this->x_scale_; i < width; i++) {
|
||||||
|
for (int j = y * this->y_scale_; j < height; j++) {
|
||||||
|
this->image_->draw_pixel_(i, j, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t *DownloadBuffer::data(size_t offset) {
|
||||||
|
if (offset > this->size_) {
|
||||||
|
ESP_LOGE(TAG, "Tried to access beyond download buffer bounds!!!");
|
||||||
|
return this->buffer_;
|
||||||
|
}
|
||||||
|
return this->buffer_ + offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t DownloadBuffer::read(size_t len) {
|
||||||
|
this->unread_ -= len;
|
||||||
|
if (this->unread_ > 0) {
|
||||||
|
memmove(this->data(), this->data(len), this->unread_);
|
||||||
|
}
|
||||||
|
return this->unread_;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace online_image
|
||||||
|
} // namespace esphome
|
112
esphome/components/online_image/image_decoder.h
Normal file
112
esphome/components/online_image/image_decoder.h
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "esphome/core/defines.h"
|
||||||
|
#include "esphome/core/color.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace online_image {
|
||||||
|
|
||||||
|
class OnlineImage;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Class to abstract decoding different image formats.
|
||||||
|
*/
|
||||||
|
class ImageDecoder {
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief Construct a new Image Decoder object
|
||||||
|
*
|
||||||
|
* @param image The image to decode the stream into.
|
||||||
|
*/
|
||||||
|
ImageDecoder(OnlineImage *image) : image_(image) {}
|
||||||
|
virtual ~ImageDecoder() = default;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Initialize the decoder.
|
||||||
|
*
|
||||||
|
* @param download_size The total number of bytes that need to be download for the image.
|
||||||
|
*/
|
||||||
|
virtual void prepare(uint32_t download_size) { this->download_size_ = download_size; }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Decode a part of the image. It will try reading from the buffer.
|
||||||
|
* There is no guarantee that the whole available buffer will be read/decoded;
|
||||||
|
* the method will return the amount of bytes actually decoded, so that the
|
||||||
|
* unread content can be moved to the beginning.
|
||||||
|
*
|
||||||
|
* @param buffer The buffer to read from.
|
||||||
|
* @param size The maximum amount of bytes that can be read from the buffer.
|
||||||
|
* @return int The amount of bytes read. It can be 0 if the buffer does not have enough content to meaningfully
|
||||||
|
* decode anything, or negative in case of a decoding error.
|
||||||
|
*/
|
||||||
|
virtual int decode(uint8_t *buffer, size_t size);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Request the image to be resized once the actual dimensions are known.
|
||||||
|
* Called by the callback functions, to be able to access the parent Image class.
|
||||||
|
*
|
||||||
|
* @param width The image's width.
|
||||||
|
* @param height The image's height.
|
||||||
|
*/
|
||||||
|
void set_size(int width, int height);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Draw a rectangle on the display_buffer using the defined color.
|
||||||
|
* Will check the given coordinates for out-of-bounds, and clip the rectangle accordingly.
|
||||||
|
* In case of binary displays, the color will be converted to binary as well.
|
||||||
|
* Called by the callback functions, to be able to access the parent Image class.
|
||||||
|
*
|
||||||
|
* @param x The left-most coordinate of the rectangle.
|
||||||
|
* @param y The top-most coordinate of the rectangle.
|
||||||
|
* @param w The width of the rectangle.
|
||||||
|
* @param h The height of the rectangle.
|
||||||
|
* @param color The color to draw the rectangle with.
|
||||||
|
*/
|
||||||
|
void draw(int x, int y, int w, int h, const Color &color);
|
||||||
|
|
||||||
|
bool is_finished() const { return this->decoded_bytes_ == this->download_size_; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
OnlineImage *image_;
|
||||||
|
// Initializing to 1, to ensure it is different than initial "decoded_bytes_".
|
||||||
|
// Will be overwritten anyway once the download size is known.
|
||||||
|
uint32_t download_size_ = 1;
|
||||||
|
uint32_t decoded_bytes_ = 0;
|
||||||
|
double x_scale_ = 1.0;
|
||||||
|
double y_scale_ = 1.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DownloadBuffer {
|
||||||
|
public:
|
||||||
|
DownloadBuffer(size_t size) : size_(size) {
|
||||||
|
this->buffer_ = this->allocator_.allocate(size);
|
||||||
|
this->reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual ~DownloadBuffer() { this->allocator_.deallocate(this->buffer_, this->size_); }
|
||||||
|
|
||||||
|
uint8_t *data(size_t offset = 0);
|
||||||
|
|
||||||
|
uint8_t *append() { return this->data(this->unread_); }
|
||||||
|
|
||||||
|
size_t unread() const { return this->unread_; }
|
||||||
|
size_t size() const { return this->size_; }
|
||||||
|
size_t free_capacity() const { return this->size_ - this->unread_; }
|
||||||
|
|
||||||
|
size_t read(size_t len);
|
||||||
|
size_t write(size_t len) {
|
||||||
|
this->unread_ += len;
|
||||||
|
return this->unread_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset() { this->unread_ = 0; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
ExternalRAMAllocator<uint8_t> allocator_;
|
||||||
|
uint8_t *buffer_;
|
||||||
|
size_t size_;
|
||||||
|
/** Total number of downloaded bytes not yet read. */
|
||||||
|
size_t unread_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace online_image
|
||||||
|
} // namespace esphome
|
275
esphome/components/online_image/online_image.cpp
Normal file
275
esphome/components/online_image/online_image.cpp
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
#include "online_image.h"
|
||||||
|
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
static const char *const TAG = "online_image";
|
||||||
|
|
||||||
|
#include "image_decoder.h"
|
||||||
|
|
||||||
|
#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
|
||||||
|
#include "png_image.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace online_image {
|
||||||
|
|
||||||
|
using image::ImageType;
|
||||||
|
|
||||||
|
inline bool is_color_on(const Color &color) {
|
||||||
|
// This produces the most accurate monochrome conversion, but is slightly slower.
|
||||||
|
// return (0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b) > 127;
|
||||||
|
|
||||||
|
// Approximation using fast integer computations; produces acceptable results
|
||||||
|
// Equivalent to 0.25 * R + 0.5 * G + 0.25 * B
|
||||||
|
return ((color.r >> 2) + (color.g >> 1) + (color.b >> 2)) & 0x80;
|
||||||
|
}
|
||||||
|
|
||||||
|
OnlineImage::OnlineImage(const std::string &url, int width, int height, ImageFormat format, ImageType type,
|
||||||
|
uint32_t download_buffer_size)
|
||||||
|
: Image(nullptr, 0, 0, type),
|
||||||
|
buffer_(nullptr),
|
||||||
|
download_buffer_(download_buffer_size),
|
||||||
|
format_(format),
|
||||||
|
fixed_width_(width),
|
||||||
|
fixed_height_(height) {
|
||||||
|
this->set_url(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnlineImage::release() {
|
||||||
|
if (this->buffer_) {
|
||||||
|
ESP_LOGD(TAG, "Deallocating old buffer...");
|
||||||
|
this->allocator_.deallocate(this->buffer_, this->get_buffer_size_());
|
||||||
|
this->data_start_ = nullptr;
|
||||||
|
this->buffer_ = nullptr;
|
||||||
|
this->width_ = 0;
|
||||||
|
this->height_ = 0;
|
||||||
|
this->buffer_width_ = 0;
|
||||||
|
this->buffer_height_ = 0;
|
||||||
|
this->end_connection_();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OnlineImage::resize_(int width_in, int height_in) {
|
||||||
|
int width = this->fixed_width_;
|
||||||
|
int height = this->fixed_height_;
|
||||||
|
if (this->auto_resize_()) {
|
||||||
|
width = width_in;
|
||||||
|
height = height_in;
|
||||||
|
if (this->width_ != width && this->height_ != height) {
|
||||||
|
this->release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this->buffer_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
auto new_size = this->get_buffer_size_(width, height);
|
||||||
|
ESP_LOGD(TAG, "Allocating new buffer of %d Bytes...", new_size);
|
||||||
|
delay_microseconds_safe(2000);
|
||||||
|
this->buffer_ = this->allocator_.allocate(new_size);
|
||||||
|
if (this->buffer_) {
|
||||||
|
this->buffer_width_ = width;
|
||||||
|
this->buffer_height_ = height;
|
||||||
|
this->width_ = width;
|
||||||
|
ESP_LOGD(TAG, "New size: (%d, %d)", width, height);
|
||||||
|
} else {
|
||||||
|
#if defined(USE_ESP8266)
|
||||||
|
// NOLINTNEXTLINE(readability-static-accessed-through-instance)
|
||||||
|
int max_block = ESP.getMaxFreeBlockSize();
|
||||||
|
#elif defined(USE_ESP32)
|
||||||
|
int max_block = heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL);
|
||||||
|
#else
|
||||||
|
int max_block = -1;
|
||||||
|
#endif
|
||||||
|
ESP_LOGE(TAG, "allocation failed. Biggest block in heap: %d Bytes", max_block);
|
||||||
|
this->end_connection_();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnlineImage::update() {
|
||||||
|
if (this->decoder_) {
|
||||||
|
ESP_LOGW(TAG, "Image already being updated.");
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
ESP_LOGI(TAG, "Updating image");
|
||||||
|
}
|
||||||
|
|
||||||
|
this->downloader_ = this->parent_->get(this->url_);
|
||||||
|
|
||||||
|
if (this->downloader_ == nullptr) {
|
||||||
|
ESP_LOGE(TAG, "Download failed.");
|
||||||
|
this->end_connection_();
|
||||||
|
this->download_error_callback_.call();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int http_code = this->downloader_->status_code;
|
||||||
|
if (http_code == HTTP_CODE_NOT_MODIFIED) {
|
||||||
|
// Image hasn't changed on server. Skip download.
|
||||||
|
this->end_connection_();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (http_code != HTTP_CODE_OK) {
|
||||||
|
ESP_LOGE(TAG, "HTTP result: %d", http_code);
|
||||||
|
this->end_connection_();
|
||||||
|
this->download_error_callback_.call();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGD(TAG, "Starting download");
|
||||||
|
size_t total_size = this->downloader_->content_length;
|
||||||
|
|
||||||
|
#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
|
||||||
|
if (this->format_ == ImageFormat::PNG) {
|
||||||
|
this->decoder_ = esphome::make_unique<PngDecoder>(this);
|
||||||
|
}
|
||||||
|
#endif // ONLINE_IMAGE_PNG_SUPPORT
|
||||||
|
|
||||||
|
if (!this->decoder_) {
|
||||||
|
ESP_LOGE(TAG, "Could not instantiate decoder. Image format unsupported.");
|
||||||
|
this->end_connection_();
|
||||||
|
this->download_error_callback_.call();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->decoder_->prepare(total_size);
|
||||||
|
ESP_LOGI(TAG, "Downloading image");
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnlineImage::loop() {
|
||||||
|
if (!this->decoder_) {
|
||||||
|
// Not decoding at the moment => nothing to do.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this->downloader_ || this->decoder_->is_finished()) {
|
||||||
|
ESP_LOGD(TAG, "Image fully downloaded");
|
||||||
|
this->data_start_ = buffer_;
|
||||||
|
this->width_ = buffer_width_;
|
||||||
|
this->height_ = buffer_height_;
|
||||||
|
this->end_connection_();
|
||||||
|
this->download_finished_callback_.call();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (this->downloader_ == nullptr) {
|
||||||
|
ESP_LOGE(TAG, "Downloader not instantiated; cannot download");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
size_t available = this->download_buffer_.free_capacity();
|
||||||
|
if (available) {
|
||||||
|
auto len = this->downloader_->read(this->download_buffer_.append(), available);
|
||||||
|
if (len > 0) {
|
||||||
|
this->download_buffer_.write(len);
|
||||||
|
auto fed = this->decoder_->decode(this->download_buffer_.data(), this->download_buffer_.unread());
|
||||||
|
if (fed < 0) {
|
||||||
|
ESP_LOGE(TAG, "Error when decoding image.");
|
||||||
|
this->end_connection_();
|
||||||
|
this->download_error_callback_.call();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->download_buffer_.read(fed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnlineImage::draw_pixel_(int x, int y, Color color) {
|
||||||
|
if (!this->buffer_) {
|
||||||
|
ESP_LOGE(TAG, "Buffer not allocated!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (x < 0 || y < 0 || x >= this->buffer_width_ || y >= this->buffer_height_) {
|
||||||
|
ESP_LOGE(TAG, "Tried to paint a pixel (%d,%d) outside the image!", x, y);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uint32_t pos = this->get_position_(x, y);
|
||||||
|
switch (this->type_) {
|
||||||
|
case ImageType::IMAGE_TYPE_BINARY: {
|
||||||
|
const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u;
|
||||||
|
const uint32_t pos = x + y * width_8;
|
||||||
|
if ((this->has_transparency() && color.w > 127) || is_color_on(color)) {
|
||||||
|
this->buffer_[pos / 8u] |= (0x80 >> (pos % 8u));
|
||||||
|
} else {
|
||||||
|
this->buffer_[pos / 8u] &= ~(0x80 >> (pos % 8u));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ImageType::IMAGE_TYPE_GRAYSCALE: {
|
||||||
|
uint8_t gray = static_cast<uint8_t>(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b);
|
||||||
|
if (this->has_transparency()) {
|
||||||
|
if (gray == 1) {
|
||||||
|
gray = 0;
|
||||||
|
}
|
||||||
|
if (color.w < 0x80) {
|
||||||
|
gray = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this->buffer_[pos] = gray;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ImageType::IMAGE_TYPE_RGB565: {
|
||||||
|
uint16_t col565 = display::ColorUtil::color_to_565(color);
|
||||||
|
if (this->has_transparency()) {
|
||||||
|
if (col565 == 0x0020) {
|
||||||
|
col565 = 0;
|
||||||
|
}
|
||||||
|
if (color.w < 0x80) {
|
||||||
|
col565 = 0x0020;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this->buffer_[pos + 0] = static_cast<uint8_t>((col565 >> 8) & 0xFF);
|
||||||
|
this->buffer_[pos + 1] = static_cast<uint8_t>(col565 & 0xFF);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ImageType::IMAGE_TYPE_RGBA: {
|
||||||
|
this->buffer_[pos + 0] = color.r;
|
||||||
|
this->buffer_[pos + 1] = color.g;
|
||||||
|
this->buffer_[pos + 2] = color.b;
|
||||||
|
this->buffer_[pos + 3] = color.w;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ImageType::IMAGE_TYPE_RGB24:
|
||||||
|
default: {
|
||||||
|
if (this->has_transparency()) {
|
||||||
|
if (color.b == 1 && color.r == 0 && color.g == 0) {
|
||||||
|
color.b = 0;
|
||||||
|
}
|
||||||
|
if (color.w < 0x80) {
|
||||||
|
color.r = 0;
|
||||||
|
color.g = 0;
|
||||||
|
color.b = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this->buffer_[pos + 0] = color.r;
|
||||||
|
this->buffer_[pos + 1] = color.g;
|
||||||
|
this->buffer_[pos + 2] = color.b;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnlineImage::end_connection_() {
|
||||||
|
if (this->downloader_) {
|
||||||
|
this->downloader_->end();
|
||||||
|
this->downloader_ = nullptr;
|
||||||
|
}
|
||||||
|
this->decoder_.reset();
|
||||||
|
this->download_buffer_.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool OnlineImage::validate_url_(const std::string &url) {
|
||||||
|
if ((url.length() < 8) || (url.find("http") != 0) || (url.find("://") == std::string::npos)) {
|
||||||
|
ESP_LOGE(TAG, "URL is invalid and/or must be prefixed with 'http://' or 'https://'");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnlineImage::add_on_finished_callback(std::function<void()> &&callback) {
|
||||||
|
this->download_finished_callback_.add(std::move(callback));
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnlineImage::add_on_error_callback(std::function<void()> &&callback) {
|
||||||
|
this->download_error_callback_.add(std::move(callback));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace online_image
|
||||||
|
} // namespace esphome
|
184
esphome/components/online_image/online_image.h
Normal file
184
esphome/components/online_image/online_image.h
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/core/defines.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
#include "esphome/components/http_request/http_request.h"
|
||||||
|
#include "esphome/components/image/image.h"
|
||||||
|
|
||||||
|
#include "image_decoder.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace online_image {
|
||||||
|
|
||||||
|
using t_http_codes = enum {
|
||||||
|
HTTP_CODE_OK = 200,
|
||||||
|
HTTP_CODE_NOT_MODIFIED = 304,
|
||||||
|
HTTP_CODE_NOT_FOUND = 404,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Format that the image is encoded with.
|
||||||
|
*/
|
||||||
|
enum ImageFormat {
|
||||||
|
/** Automatically detect from MIME type. Not supported yet. */
|
||||||
|
AUTO,
|
||||||
|
/** JPEG format. Not supported yet. */
|
||||||
|
JPEG,
|
||||||
|
/** PNG format. */
|
||||||
|
PNG,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Download an image from a given URL, and decode it using the specified decoder.
|
||||||
|
* The image will then be stored in a buffer, so that it can be re-displayed without the
|
||||||
|
* need to re-download or re-decode.
|
||||||
|
*/
|
||||||
|
class OnlineImage : public PollingComponent,
|
||||||
|
public image::Image,
|
||||||
|
public Parented<esphome::http_request::HttpRequestComponent> {
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief Construct a new OnlineImage object.
|
||||||
|
*
|
||||||
|
* @param url URL to download the image from.
|
||||||
|
* @param width Desired width of the target image area.
|
||||||
|
* @param height Desired height of the target image area.
|
||||||
|
* @param format Format that the image is encoded in (@see ImageFormat).
|
||||||
|
* @param buffer_size Size of the buffer used to download the image.
|
||||||
|
*/
|
||||||
|
OnlineImage(const std::string &url, int width, int height, ImageFormat format, image::ImageType type,
|
||||||
|
uint32_t buffer_size);
|
||||||
|
|
||||||
|
void update() override;
|
||||||
|
void loop() override;
|
||||||
|
|
||||||
|
/** Set the URL to download the image from. */
|
||||||
|
void set_url(const std::string &url) {
|
||||||
|
if (this->validate_url_(url)) {
|
||||||
|
this->url_ = url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Release the buffer storing the image. The image will need to be downloaded again
|
||||||
|
* to be able to be displayed.
|
||||||
|
*/
|
||||||
|
void release();
|
||||||
|
|
||||||
|
void add_on_finished_callback(std::function<void()> &&callback);
|
||||||
|
void add_on_error_callback(std::function<void()> &&callback);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
bool validate_url_(const std::string &url);
|
||||||
|
|
||||||
|
using Allocator = ExternalRAMAllocator<uint8_t>;
|
||||||
|
Allocator allocator_{Allocator::Flags::ALLOW_FAILURE};
|
||||||
|
|
||||||
|
uint32_t get_buffer_size_() const { return get_buffer_size_(this->buffer_width_, this->buffer_height_); }
|
||||||
|
int get_buffer_size_(int width, int height) const {
|
||||||
|
return std::ceil(image::image_type_to_bpp(this->type_) * width * height / 8.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int get_position_(int x, int y) const {
|
||||||
|
return ((x + y * this->buffer_width_) * image::image_type_to_bpp(this->type_)) / 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESPHOME_ALWAYS_INLINE bool auto_resize_() const { return this->fixed_width_ == 0 || this->fixed_height_ == 0; }
|
||||||
|
|
||||||
|
bool resize_(int width, int height);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Draw a pixel into the buffer.
|
||||||
|
*
|
||||||
|
* This is used by the decoder to fill the buffer that will later be displayed
|
||||||
|
* by the `draw` method. This will internally convert the supplied 32 bit RGBA
|
||||||
|
* color into the requested image storage format.
|
||||||
|
*
|
||||||
|
* @param x Horizontal pixel position.
|
||||||
|
* @param y Vertical pixel position.
|
||||||
|
* @param color 32 bit color to put into the pixel.
|
||||||
|
*/
|
||||||
|
void draw_pixel_(int x, int y, Color color);
|
||||||
|
|
||||||
|
void end_connection_();
|
||||||
|
|
||||||
|
CallbackManager<void()> download_finished_callback_{};
|
||||||
|
CallbackManager<void()> download_error_callback_{};
|
||||||
|
|
||||||
|
std::shared_ptr<http_request::HttpContainer> downloader_{nullptr};
|
||||||
|
std::unique_ptr<ImageDecoder> decoder_{nullptr};
|
||||||
|
|
||||||
|
uint8_t *buffer_;
|
||||||
|
DownloadBuffer download_buffer_;
|
||||||
|
|
||||||
|
const ImageFormat format_;
|
||||||
|
|
||||||
|
std::string url_{""};
|
||||||
|
|
||||||
|
/** width requested on configuration, or 0 if non specified. */
|
||||||
|
const int fixed_width_;
|
||||||
|
/** height requested on configuration, or 0 if non specified. */
|
||||||
|
const int fixed_height_;
|
||||||
|
/**
|
||||||
|
* Actual width of the current image. If fixed_width_ is specified,
|
||||||
|
* this will be equal to it; otherwise it will be set once the decoding
|
||||||
|
* starts and the original size is known.
|
||||||
|
* This needs to be separate from "BaseImage::get_width()" because the latter
|
||||||
|
* must return 0 until the image has been decoded (to avoid showing partially
|
||||||
|
* decoded images).
|
||||||
|
*/
|
||||||
|
int buffer_width_;
|
||||||
|
/**
|
||||||
|
* Actual height of the current image. If fixed_height_ is specified,
|
||||||
|
* this will be equal to it; otherwise it will be set once the decoding
|
||||||
|
* starts and the original size is known.
|
||||||
|
* This needs to be separate from "BaseImage::get_height()" because the latter
|
||||||
|
* must return 0 until the image has been decoded (to avoid showing partially
|
||||||
|
* decoded images).
|
||||||
|
*/
|
||||||
|
int buffer_height_;
|
||||||
|
|
||||||
|
friend void ImageDecoder::set_size(int width, int height);
|
||||||
|
friend void ImageDecoder::draw(int x, int y, int w, int h, const Color &color);
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename... Ts> class OnlineImageSetUrlAction : public Action<Ts...> {
|
||||||
|
public:
|
||||||
|
OnlineImageSetUrlAction(OnlineImage *parent) : parent_(parent) {}
|
||||||
|
TEMPLATABLE_VALUE(const char *, url)
|
||||||
|
void play(Ts... x) override {
|
||||||
|
this->parent_->set_url(this->url_.value(x...));
|
||||||
|
this->parent_->update();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
OnlineImage *parent_;
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename... Ts> class OnlineImageReleaseAction : public Action<Ts...> {
|
||||||
|
public:
|
||||||
|
OnlineImageReleaseAction(OnlineImage *parent) : parent_(parent) {}
|
||||||
|
TEMPLATABLE_VALUE(const char *, url)
|
||||||
|
void play(Ts... x) override { this->parent_->release(); }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
OnlineImage *parent_;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DownloadFinishedTrigger : public Trigger<> {
|
||||||
|
public:
|
||||||
|
explicit DownloadFinishedTrigger(OnlineImage *parent) {
|
||||||
|
parent->add_on_finished_callback([this]() { this->trigger(); });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class DownloadErrorTrigger : public Trigger<> {
|
||||||
|
public:
|
||||||
|
explicit DownloadErrorTrigger(OnlineImage *parent) {
|
||||||
|
parent->add_on_error_callback([this]() { this->trigger(); });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace online_image
|
||||||
|
} // namespace esphome
|
68
esphome/components/online_image/png_image.cpp
Normal file
68
esphome/components/online_image/png_image.cpp
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
#include "png_image.h"
|
||||||
|
#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
|
||||||
|
|
||||||
|
#include "esphome/components/display/display_buffer.h"
|
||||||
|
#include "esphome/core/application.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
static const char *const TAG = "online_image.png";
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace online_image {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Callback method that will be called by the PNGLE engine when the basic
|
||||||
|
* data of the image is received (i.e. width and height);
|
||||||
|
*
|
||||||
|
* @param pngle The PNGLE object, including the context data.
|
||||||
|
* @param w The width of the image.
|
||||||
|
* @param h The height of the image.
|
||||||
|
*/
|
||||||
|
static void init_callback(pngle_t *pngle, uint32_t w, uint32_t h) {
|
||||||
|
PngDecoder *decoder = (PngDecoder *) pngle_get_user_data(pngle);
|
||||||
|
decoder->set_size(w, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Callback method that will be called by the PNGLE engine when a chunk
|
||||||
|
* of the image is decoded.
|
||||||
|
*
|
||||||
|
* @param pngle The PNGLE object, including the context data.
|
||||||
|
* @param x The X coordinate to draw the rectangle on.
|
||||||
|
* @param y The Y coordinate to draw the rectangle on.
|
||||||
|
* @param w The width of the rectangle to draw.
|
||||||
|
* @param h The height of the rectangle to draw.
|
||||||
|
* @param rgba The color to paint the rectangle in.
|
||||||
|
*/
|
||||||
|
static void draw_callback(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, uint32_t h, uint8_t rgba[4]) {
|
||||||
|
PngDecoder *decoder = (PngDecoder *) pngle_get_user_data(pngle);
|
||||||
|
Color color(rgba[0], rgba[1], rgba[2], rgba[3]);
|
||||||
|
decoder->draw(x, y, w, h, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PngDecoder::prepare(uint32_t download_size) {
|
||||||
|
ImageDecoder::prepare(download_size);
|
||||||
|
pngle_set_user_data(this->pngle_, this);
|
||||||
|
pngle_set_init_callback(this->pngle_, init_callback);
|
||||||
|
pngle_set_draw_callback(this->pngle_, draw_callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
int HOT PngDecoder::decode(uint8_t *buffer, size_t size) {
|
||||||
|
if (size < 256 && size < this->download_size_ - this->decoded_bytes_) {
|
||||||
|
ESP_LOGD(TAG, "Waiting for data");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
auto fed = pngle_feed(this->pngle_, buffer, size);
|
||||||
|
if (fed < 0) {
|
||||||
|
ESP_LOGE(TAG, "Error decoding image: %s", pngle_error(this->pngle_));
|
||||||
|
} else {
|
||||||
|
this->decoded_bytes_ += fed;
|
||||||
|
}
|
||||||
|
return fed;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace online_image
|
||||||
|
} // namespace esphome
|
||||||
|
|
||||||
|
#endif // USE_ONLINE_IMAGE_PNG_SUPPORT
|
33
esphome/components/online_image/png_image.h
Normal file
33
esphome/components/online_image/png_image.h
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "image_decoder.h"
|
||||||
|
#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
|
||||||
|
#include <pngle.h>
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace online_image {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Image decoder specialization for PNG images.
|
||||||
|
*/
|
||||||
|
class PngDecoder : public ImageDecoder {
|
||||||
|
public:
|
||||||
|
/**
|
||||||
|
* @brief Construct a new PNG Decoder object.
|
||||||
|
*
|
||||||
|
* @param display The image to decode the stream into.
|
||||||
|
*/
|
||||||
|
PngDecoder(OnlineImage *image) : ImageDecoder(image), pngle_(pngle_new()) {}
|
||||||
|
~PngDecoder() override { pngle_destroy(this->pngle_); }
|
||||||
|
|
||||||
|
void prepare(uint32_t download_size) override;
|
||||||
|
int HOT decode(uint8_t *buffer, size_t size) override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
pngle_t *pngle_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace online_image
|
||||||
|
} // namespace esphome
|
||||||
|
|
||||||
|
#endif // USE_ONLINE_IMAGE_PNG_SUPPORT
|
@ -19,24 +19,22 @@ std::unique_ptr<Socket> socket_ip(int type, int protocol) {
|
|||||||
|
|
||||||
socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const std::string &ip_address, uint16_t port) {
|
socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const std::string &ip_address, uint16_t port) {
|
||||||
#if USE_NETWORK_IPV6
|
#if USE_NETWORK_IPV6
|
||||||
if (addrlen < sizeof(sockaddr_in6)) {
|
if (ip_address.find(':') != std::string::npos) {
|
||||||
errno = EINVAL;
|
if (addrlen < sizeof(sockaddr_in6)) {
|
||||||
return 0;
|
errno = EINVAL;
|
||||||
}
|
return 0;
|
||||||
auto *server = reinterpret_cast<sockaddr_in6 *>(addr);
|
}
|
||||||
memset(server, 0, sizeof(sockaddr_in6));
|
auto *server = reinterpret_cast<sockaddr_in6 *>(addr);
|
||||||
server->sin6_family = AF_INET6;
|
memset(server, 0, sizeof(sockaddr_in6));
|
||||||
server->sin6_port = htons(port);
|
server->sin6_family = AF_INET6;
|
||||||
|
server->sin6_port = htons(port);
|
||||||
|
|
||||||
if (ip_address.find('.') != std::string::npos) {
|
|
||||||
server->sin6_addr.un.u32_addr[3] = inet_addr(ip_address.c_str());
|
|
||||||
} else {
|
|
||||||
ip6_addr_t ip6;
|
ip6_addr_t ip6;
|
||||||
inet6_aton(ip_address.c_str(), &ip6);
|
inet6_aton(ip_address.c_str(), &ip6);
|
||||||
memcpy(server->sin6_addr.un.u32_addr, ip6.addr, sizeof(ip6.addr));
|
memcpy(server->sin6_addr.un.u32_addr, ip6.addr, sizeof(ip6.addr));
|
||||||
|
return sizeof(sockaddr_in6);
|
||||||
}
|
}
|
||||||
return sizeof(sockaddr_in6);
|
#endif /* USE_NETWORK_IPV6 */
|
||||||
#else
|
|
||||||
if (addrlen < sizeof(sockaddr_in)) {
|
if (addrlen < sizeof(sockaddr_in)) {
|
||||||
errno = EINVAL;
|
errno = EINVAL;
|
||||||
return 0;
|
return 0;
|
||||||
@ -47,7 +45,6 @@ socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const std::stri
|
|||||||
server->sin_addr.s_addr = inet_addr(ip_address.c_str());
|
server->sin_addr.s_addr = inet_addr(ip_address.c_str());
|
||||||
server->sin_port = htons(port);
|
server->sin_port = htons(port);
|
||||||
return sizeof(sockaddr_in);
|
return sizeof(sockaddr_in);
|
||||||
#endif /* USE_NETWORK_IPV6 */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t port) {
|
socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t port) {
|
||||||
|
@ -76,6 +76,7 @@ CONF_AWAY = "away"
|
|||||||
CONF_AWAY_COMMAND_TOPIC = "away_command_topic"
|
CONF_AWAY_COMMAND_TOPIC = "away_command_topic"
|
||||||
CONF_AWAY_CONFIG = "away_config"
|
CONF_AWAY_CONFIG = "away_config"
|
||||||
CONF_AWAY_STATE_TOPIC = "away_state_topic"
|
CONF_AWAY_STATE_TOPIC = "away_state_topic"
|
||||||
|
CONF_BACKGROUND_COLOR = "background_color"
|
||||||
CONF_BACKLIGHT_PIN = "backlight_pin"
|
CONF_BACKLIGHT_PIN = "backlight_pin"
|
||||||
CONF_BASELINE = "baseline"
|
CONF_BASELINE = "baseline"
|
||||||
CONF_BATTERY_LEVEL = "battery_level"
|
CONF_BATTERY_LEVEL = "battery_level"
|
||||||
@ -311,6 +312,7 @@ CONF_FLOW = "flow"
|
|||||||
CONF_FLOW_CONTROL_PIN = "flow_control_pin"
|
CONF_FLOW_CONTROL_PIN = "flow_control_pin"
|
||||||
CONF_FOR = "for"
|
CONF_FOR = "for"
|
||||||
CONF_FORCE_UPDATE = "force_update"
|
CONF_FORCE_UPDATE = "force_update"
|
||||||
|
CONF_FOREGROUND_COLOR = "foreground_color"
|
||||||
CONF_FORMALDEHYDE = "formaldehyde"
|
CONF_FORMALDEHYDE = "formaldehyde"
|
||||||
CONF_FORMAT = "format"
|
CONF_FORMAT = "format"
|
||||||
CONF_FORWARD_ACTIVE_ENERGY = "forward_active_energy"
|
CONF_FORWARD_ACTIVE_ENERGY = "forward_active_energy"
|
||||||
|
@ -39,9 +39,12 @@
|
|||||||
#define USE_LOCK
|
#define USE_LOCK
|
||||||
#define USE_LOGGER
|
#define USE_LOGGER
|
||||||
#define USE_LVGL
|
#define USE_LVGL
|
||||||
|
#define USE_LVGL_ANIMIMG
|
||||||
#define USE_LVGL_BINARY_SENSOR
|
#define USE_LVGL_BINARY_SENSOR
|
||||||
|
#define USE_LVGL_BUTTONMATRIX
|
||||||
#define USE_LVGL_FONT
|
#define USE_LVGL_FONT
|
||||||
#define USE_LVGL_IMAGE
|
#define USE_LVGL_IMAGE
|
||||||
|
#define USE_LVGL_KEYBOARD
|
||||||
#define USE_LVGL_KEY_LISTENER
|
#define USE_LVGL_KEY_LISTENER
|
||||||
#define USE_LVGL_TOUCHSCREEN
|
#define USE_LVGL_TOUCHSCREEN
|
||||||
#define USE_LVGL_ROTARY_ENCODER
|
#define USE_LVGL_ROTARY_ENCODER
|
||||||
@ -50,6 +53,7 @@
|
|||||||
#define USE_MQTT
|
#define USE_MQTT
|
||||||
#define USE_NEXTION_TFT_UPLOAD
|
#define USE_NEXTION_TFT_UPLOAD
|
||||||
#define USE_NUMBER
|
#define USE_NUMBER
|
||||||
|
#define USE_ONLINE_IMAGE_PNG_SUPPORT
|
||||||
#define USE_OTA
|
#define USE_OTA
|
||||||
#define USE_OTA_PASSWORD
|
#define USE_OTA_PASSWORD
|
||||||
#define USE_OTA_STATE_CALLBACK
|
#define USE_OTA_STATE_CALLBACK
|
||||||
|
@ -686,7 +686,7 @@ template<class T> class ExternalRAMAllocator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
Flags flags_{Flags::NONE};
|
Flags flags_{Flags::ALLOW_FAILURE};
|
||||||
};
|
};
|
||||||
|
|
||||||
/// @}
|
/// @}
|
||||||
|
@ -40,6 +40,7 @@ lib_deps =
|
|||||||
wjtje/qr-code-generator-library@1.7.0 ; qr_code
|
wjtje/qr-code-generator-library@1.7.0 ; qr_code
|
||||||
functionpointer/arduino-MLX90393@1.0.0 ; mlx90393
|
functionpointer/arduino-MLX90393@1.0.0 ; mlx90393
|
||||||
pavlodn/HaierProtocol@0.9.31 ; haier
|
pavlodn/HaierProtocol@0.9.31 ; haier
|
||||||
|
kikuchan98/pngle@1.0.2 ; online_image
|
||||||
; This is using the repository until a new release is published to PlatformIO
|
; This is using the repository until a new release is published to PlatformIO
|
||||||
https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library
|
https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library
|
||||||
lvgl/lvgl@8.4.0 ; lvgl
|
lvgl/lvgl@8.4.0 ; lvgl
|
||||||
|
2
tests/components/lvgl/.gitattributes
vendored
Normal file
2
tests/components/lvgl/.gitattributes
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
*.ttf -text
|
||||||
|
|
@ -8,3 +8,121 @@ touchscreen:
|
|||||||
x_max: 240
|
x_max: 240
|
||||||
y_max: 320
|
y_max: 320
|
||||||
|
|
||||||
|
font:
|
||||||
|
- file: "$component_dir/roboto.ttf"
|
||||||
|
id: roboto20
|
||||||
|
size: 20
|
||||||
|
extras:
|
||||||
|
- file: '$component_dir/materialdesignicons-webfont.ttf'
|
||||||
|
glyphs: [
|
||||||
|
"\U000F004B",
|
||||||
|
"\U0000f0ed",
|
||||||
|
"\U000F006E",
|
||||||
|
"\U000F012C",
|
||||||
|
"\U000F179B",
|
||||||
|
"\U000F0748",
|
||||||
|
"\U000F1A1B",
|
||||||
|
"\U000F02DC",
|
||||||
|
"\U000F0A02",
|
||||||
|
"\U000F035F",
|
||||||
|
"\U000F0156",
|
||||||
|
"\U000F0C5F",
|
||||||
|
"\U000f0084",
|
||||||
|
"\U000f0091",
|
||||||
|
]
|
||||||
|
- file: "$component_dir/helvetica.ttf"
|
||||||
|
id: helvetica20
|
||||||
|
- file: "$component_dir/roboto.ttf"
|
||||||
|
id: roboto10
|
||||||
|
size: 10
|
||||||
|
bpp: 4
|
||||||
|
extras:
|
||||||
|
- file: '$component_dir/materialdesignicons-webfont.ttf'
|
||||||
|
glyphs: [
|
||||||
|
"\U000F004B",
|
||||||
|
"\U0000f0ed",
|
||||||
|
"\U000F006E",
|
||||||
|
"\U000F012C",
|
||||||
|
"\U000F179B",
|
||||||
|
"\U000F0748",
|
||||||
|
"\U000F1A1B",
|
||||||
|
"\U000F02DC",
|
||||||
|
"\U000F0A02",
|
||||||
|
"\U000F035F",
|
||||||
|
"\U000F0156",
|
||||||
|
"\U000F0C5F",
|
||||||
|
"\U000f0084",
|
||||||
|
"\U000f0091",
|
||||||
|
]
|
||||||
|
|
||||||
|
sensor:
|
||||||
|
- platform: lvgl
|
||||||
|
id: lvgl_sensor_id
|
||||||
|
name: "LVGL Arc Sensor"
|
||||||
|
widget: lv_arc
|
||||||
|
- platform: lvgl
|
||||||
|
widget: slider_id
|
||||||
|
name: LVGL Slider
|
||||||
|
- platform: lvgl
|
||||||
|
widget: bar_id
|
||||||
|
id: lvgl_bar_sensor
|
||||||
|
name: LVGL Bar
|
||||||
|
- platform: lvgl
|
||||||
|
widget: spinbox_id
|
||||||
|
name: LVGL Spinbox
|
||||||
|
|
||||||
|
number:
|
||||||
|
- platform: lvgl
|
||||||
|
widget: slider_id
|
||||||
|
name: LVGL Slider
|
||||||
|
- platform: lvgl
|
||||||
|
widget: lv_arc
|
||||||
|
id: lvgl_arc_number
|
||||||
|
name: LVGL Arc
|
||||||
|
- platform: lvgl
|
||||||
|
widget: bar_id
|
||||||
|
id: lvgl_bar_number
|
||||||
|
name: LVGL Bar
|
||||||
|
- platform: lvgl
|
||||||
|
widget: spinbox_id
|
||||||
|
id: lvgl_spinbox_number
|
||||||
|
name: LVGL Spinbox
|
||||||
|
|
||||||
|
light:
|
||||||
|
- platform: lvgl
|
||||||
|
name: LVGL LED
|
||||||
|
id: lv_light
|
||||||
|
led: lv_led
|
||||||
|
|
||||||
|
binary_sensor:
|
||||||
|
- platform: lvgl
|
||||||
|
id: lvgl_pressbutton
|
||||||
|
name: Pressbutton
|
||||||
|
widget: spin_up
|
||||||
|
publish_initial_state: true
|
||||||
|
- platform: lvgl
|
||||||
|
name: ButtonMatrix button
|
||||||
|
widget: button_a
|
||||||
|
- platform: lvgl
|
||||||
|
id: switch_d
|
||||||
|
name: Matrix switch D
|
||||||
|
widget: button_d
|
||||||
|
on_click:
|
||||||
|
then:
|
||||||
|
- lvgl.page.previous:
|
||||||
|
animation: move_right
|
||||||
|
time: 600ms
|
||||||
|
- platform: lvgl
|
||||||
|
id: button_checker
|
||||||
|
name: LVGL button
|
||||||
|
widget: spin_up
|
||||||
|
on_state:
|
||||||
|
then:
|
||||||
|
- lvgl.checkbox.update:
|
||||||
|
id: checkbox_id
|
||||||
|
state:
|
||||||
|
checked: !lambda return x;
|
||||||
|
text: Unchecked
|
||||||
|
- platform: lvgl
|
||||||
|
name: LVGL checkbox
|
||||||
|
widget: checkbox_id
|
||||||
|
BIN
tests/components/lvgl/helvetica.ttf
Normal file
BIN
tests/components/lvgl/helvetica.ttf
Normal file
Binary file not shown.
@ -1,6 +1,53 @@
|
|||||||
lvgl:
|
lvgl:
|
||||||
log_level: TRACE
|
log_level: TRACE
|
||||||
bg_color: light_blue
|
bg_color: light_blue
|
||||||
|
theme:
|
||||||
|
obj:
|
||||||
|
border_width: 1
|
||||||
|
|
||||||
|
style_definitions:
|
||||||
|
- id: style_test
|
||||||
|
bg_color: 0x2F8CD8
|
||||||
|
- id: header_footer
|
||||||
|
bg_color: 0x20214F
|
||||||
|
bg_grad_color: 0x005782
|
||||||
|
bg_grad_dir: VER
|
||||||
|
bg_opa: cover
|
||||||
|
border_width: 0
|
||||||
|
radius: 0
|
||||||
|
pad_all: 0
|
||||||
|
pad_row: 0
|
||||||
|
pad_column: 0
|
||||||
|
border_color: 0x0077b3
|
||||||
|
text_color: 0xFFFFFF
|
||||||
|
width: 100%
|
||||||
|
height: 30
|
||||||
|
border_side: [left, top]
|
||||||
|
text_decor: [underline, strikethrough]
|
||||||
|
- id: style_line
|
||||||
|
line_color: light_blue
|
||||||
|
line_width: 8
|
||||||
|
line_rounded: true
|
||||||
|
- id: date_style
|
||||||
|
text_font: roboto10
|
||||||
|
align: center
|
||||||
|
text_color: 0x000000
|
||||||
|
bg_opa: cover
|
||||||
|
radius: 4
|
||||||
|
pad_all: 2
|
||||||
|
- id: spin_button
|
||||||
|
height: 40
|
||||||
|
width: 40
|
||||||
|
- id: spin_label
|
||||||
|
align: center
|
||||||
|
text_align: center
|
||||||
|
text_font: space16
|
||||||
|
- id: bdr_style
|
||||||
|
border_color: 0x808080
|
||||||
|
border_width: 2
|
||||||
|
pad_all: 4
|
||||||
|
align: center
|
||||||
|
|
||||||
touchscreens:
|
touchscreens:
|
||||||
- touchscreen_id: tft_touch
|
- touchscreen_id: tft_touch
|
||||||
long_press_repeat_time: 200ms
|
long_press_repeat_time: 200ms
|
||||||
@ -9,6 +56,13 @@ lvgl:
|
|||||||
- id: page1
|
- id: page1
|
||||||
skip: true
|
skip: true
|
||||||
widgets:
|
widgets:
|
||||||
|
- animimg:
|
||||||
|
height: 60
|
||||||
|
id: anim_img
|
||||||
|
src: [cat_image, dog_image]
|
||||||
|
repeat_count: 10
|
||||||
|
duration: 1s
|
||||||
|
auto_start: true
|
||||||
- label:
|
- label:
|
||||||
id: hello_label
|
id: hello_label
|
||||||
text: Hello world
|
text: Hello world
|
||||||
@ -16,7 +70,9 @@ lvgl:
|
|||||||
align: center
|
align: center
|
||||||
text_font: montserrat_40
|
text_font: montserrat_40
|
||||||
border_post: true
|
border_post: true
|
||||||
|
on_click:
|
||||||
|
then:
|
||||||
|
- lvgl.animimg.stop: anim_img
|
||||||
- label:
|
- label:
|
||||||
text: "Hello shiny day"
|
text: "Hello shiny day"
|
||||||
text_color: 0xFFFFFF
|
text_color: 0xFFFFFF
|
||||||
@ -94,7 +150,65 @@ lvgl:
|
|||||||
width: 10px
|
width: 10px
|
||||||
x: 100
|
x: 100
|
||||||
y: 120
|
y: 120
|
||||||
|
- buttonmatrix:
|
||||||
|
on_press:
|
||||||
|
logger.log:
|
||||||
|
format: "matrix button pressed: %d"
|
||||||
|
args: ["x"]
|
||||||
|
on_long_press:
|
||||||
|
lvgl.matrix.button.update:
|
||||||
|
id: [button_a, button_e, button_c]
|
||||||
|
control:
|
||||||
|
disabled: true
|
||||||
|
on_click:
|
||||||
|
logger.log:
|
||||||
|
format: "matrix button clicked: %d, is button_a = %u"
|
||||||
|
args: ["x", "id(button_a) == x"]
|
||||||
|
items:
|
||||||
|
checked:
|
||||||
|
bg_color: 0xFFFF00
|
||||||
|
id: b_matrix
|
||||||
|
|
||||||
|
rows:
|
||||||
|
- buttons:
|
||||||
|
- id: button_a
|
||||||
|
text: home icon
|
||||||
|
width: 2
|
||||||
|
control:
|
||||||
|
checkable: true
|
||||||
|
on_value:
|
||||||
|
logger.log:
|
||||||
|
format: "button_a value %d"
|
||||||
|
args: [x]
|
||||||
|
- id: button_b
|
||||||
|
text: B
|
||||||
|
width: 1
|
||||||
|
on_value:
|
||||||
|
logger.log:
|
||||||
|
format: "button_b value %d"
|
||||||
|
args: [x]
|
||||||
|
on_click:
|
||||||
|
then:
|
||||||
|
- lvgl.page.previous:
|
||||||
|
control:
|
||||||
|
hidden: false
|
||||||
|
- buttons:
|
||||||
|
- id: button_c
|
||||||
|
text: C
|
||||||
|
control:
|
||||||
|
checkable: false
|
||||||
|
- id: button_d
|
||||||
|
text: menu left
|
||||||
|
on_long_press:
|
||||||
|
then:
|
||||||
|
logger.log: Long pressed
|
||||||
|
on_long_press_repeat:
|
||||||
|
then:
|
||||||
|
logger.log: Long pressed repeated
|
||||||
|
- buttons:
|
||||||
|
- id: button_e
|
||||||
- button:
|
- button:
|
||||||
|
id: button_button
|
||||||
width: 20%
|
width: 20%
|
||||||
height: 10%
|
height: 10%
|
||||||
pressed:
|
pressed:
|
||||||
@ -137,6 +251,7 @@ lvgl:
|
|||||||
on_long_press_repeat:
|
on_long_press_repeat:
|
||||||
logger.log: Button clicked
|
logger.log: Button clicked
|
||||||
- led:
|
- led:
|
||||||
|
id: lv_led
|
||||||
color: 0x00FF00
|
color: 0x00FF00
|
||||||
brightness: 50%
|
brightness: 50%
|
||||||
align: right_mid
|
align: right_mid
|
||||||
@ -151,6 +266,41 @@ lvgl:
|
|||||||
|
|
||||||
- id: page2
|
- id: page2
|
||||||
widgets:
|
widgets:
|
||||||
|
- button:
|
||||||
|
styles: spin_button
|
||||||
|
id: spin_up
|
||||||
|
on_click:
|
||||||
|
- lvgl.spinbox.increment: spinbox_id
|
||||||
|
widgets:
|
||||||
|
- label:
|
||||||
|
styles: spin_label
|
||||||
|
text: "+"
|
||||||
|
- spinbox:
|
||||||
|
text_font: space16
|
||||||
|
id: spinbox_id
|
||||||
|
align: center
|
||||||
|
width: 120
|
||||||
|
range_from: -10
|
||||||
|
range_to: 1000
|
||||||
|
step: 5.0
|
||||||
|
rollover: false
|
||||||
|
digits: 6
|
||||||
|
decimal_places: 2
|
||||||
|
value: 15
|
||||||
|
on_value:
|
||||||
|
then:
|
||||||
|
- logger.log:
|
||||||
|
format: "Spinbox value is %f"
|
||||||
|
args: [x]
|
||||||
|
- button:
|
||||||
|
styles: spin_button
|
||||||
|
id: spin_down
|
||||||
|
on_click:
|
||||||
|
- lvgl.spinbox.decrement: spinbox_id
|
||||||
|
widgets:
|
||||||
|
- label:
|
||||||
|
styles: spin_label
|
||||||
|
text: "-"
|
||||||
- arc:
|
- arc:
|
||||||
align: left_mid
|
align: left_mid
|
||||||
id: lv_arc
|
id: lv_arc
|
||||||
@ -160,7 +310,6 @@ lvgl:
|
|||||||
- logger.log:
|
- logger.log:
|
||||||
format: "Arc value is %f"
|
format: "Arc value is %f"
|
||||||
args: [x]
|
args: [x]
|
||||||
group: general
|
|
||||||
scroll_on_focus: true
|
scroll_on_focus: true
|
||||||
value: 75
|
value: 75
|
||||||
min_value: 1
|
min_value: 1
|
||||||
@ -201,6 +350,7 @@ lvgl:
|
|||||||
- switch:
|
- switch:
|
||||||
align: right_mid
|
align: right_mid
|
||||||
- checkbox:
|
- checkbox:
|
||||||
|
id: checkbox_id
|
||||||
text: Checkbox
|
text: Checkbox
|
||||||
align: bottom_right
|
align: bottom_right
|
||||||
- slider:
|
- slider:
|
||||||
@ -221,6 +371,78 @@ lvgl:
|
|||||||
- lvgl.slider.update:
|
- lvgl.slider.update:
|
||||||
id: slider_id
|
id: slider_id
|
||||||
value: !lambda return (int)((float)rand() / RAND_MAX * 100);
|
value: !lambda return (int)((float)rand() / RAND_MAX * 100);
|
||||||
|
- tabview:
|
||||||
|
id: tabview_id
|
||||||
|
width: 100%
|
||||||
|
height: 80%
|
||||||
|
position: top
|
||||||
|
on_value:
|
||||||
|
then:
|
||||||
|
- if:
|
||||||
|
condition:
|
||||||
|
lambda: return tab == id(tabview_tab_1);
|
||||||
|
then:
|
||||||
|
- logger.log: "Dog tab is now showing"
|
||||||
|
tabs:
|
||||||
|
- name: Dog
|
||||||
|
id: tabview_tab_1
|
||||||
|
border_width: 2
|
||||||
|
border_color: 0xff0000
|
||||||
|
width: 100%
|
||||||
|
pad_all: 8
|
||||||
|
layout:
|
||||||
|
type: grid
|
||||||
|
grid_row_align: end
|
||||||
|
grid_rows: [25px, fr(1), content]
|
||||||
|
grid_columns: [40, fr(1), fr(1)]
|
||||||
|
widgets:
|
||||||
|
- image:
|
||||||
|
grid_cell_row_pos: 0
|
||||||
|
grid_cell_column_pos: 0
|
||||||
|
src: dog_image
|
||||||
|
on_click:
|
||||||
|
then:
|
||||||
|
- lvgl.tabview.select:
|
||||||
|
id: tabview_id
|
||||||
|
index: 1
|
||||||
|
animated: true
|
||||||
|
- label:
|
||||||
|
styles: bdr_style
|
||||||
|
grid_cell_x_align: center
|
||||||
|
grid_cell_y_align: stretch
|
||||||
|
grid_cell_row_pos: 0
|
||||||
|
grid_cell_column_pos: 1
|
||||||
|
grid_cell_column_span: 1
|
||||||
|
text: "Grid cell 0/1"
|
||||||
|
- label:
|
||||||
|
grid_cell_x_align: end
|
||||||
|
styles: bdr_style
|
||||||
|
grid_cell_row_pos: 1
|
||||||
|
grid_cell_column_pos: 0
|
||||||
|
text: "Grid cell 1/0"
|
||||||
|
- label:
|
||||||
|
styles: bdr_style
|
||||||
|
grid_cell_row_pos: 1
|
||||||
|
grid_cell_column_pos: 1
|
||||||
|
text: "Grid cell 1/1"
|
||||||
|
- label:
|
||||||
|
id: cell_1_3
|
||||||
|
styles: bdr_style
|
||||||
|
grid_cell_row_pos: 1
|
||||||
|
grid_cell_column_pos: 2
|
||||||
|
text: "Grid cell 1/2"
|
||||||
|
- name: Cat
|
||||||
|
id: tabview_tab_2
|
||||||
|
widgets:
|
||||||
|
- image:
|
||||||
|
src: cat_image
|
||||||
|
on_click:
|
||||||
|
then:
|
||||||
|
- logger.log: Cat image clicked
|
||||||
|
- lvgl.tabview.select:
|
||||||
|
id: tabview_id
|
||||||
|
index: 0
|
||||||
|
animated: true
|
||||||
font:
|
font:
|
||||||
- file: "gfonts://Roboto"
|
- file: "gfonts://Roboto"
|
||||||
id: space16
|
id: space16
|
||||||
@ -230,7 +452,7 @@ image:
|
|||||||
- id: cat_image
|
- id: cat_image
|
||||||
resize: 256x48
|
resize: 256x48
|
||||||
file: $component_dir/logo-text.svg
|
file: $component_dir/logo-text.svg
|
||||||
- id: dog_img
|
- id: dog_image
|
||||||
file: $component_dir/logo-text.svg
|
file: $component_dir/logo-text.svg
|
||||||
resize: 256x48
|
resize: 256x48
|
||||||
type: TRANSPARENT_BINARY
|
type: TRANSPARENT_BINARY
|
||||||
|
BIN
tests/components/lvgl/materialdesignicons-webfont.ttf
Normal file
BIN
tests/components/lvgl/materialdesignicons-webfont.ttf
Normal file
Binary file not shown.
BIN
tests/components/lvgl/roboto.ttf
Normal file
BIN
tests/components/lvgl/roboto.ttf
Normal file
Binary file not shown.
@ -426,3 +426,9 @@ valve:
|
|||||||
} else {
|
} else {
|
||||||
return VALVE_CLOSED;
|
return VALVE_CLOSED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
alarm_control_panel:
|
||||||
|
- platform: template
|
||||||
|
name: Alarm Control Panel
|
||||||
|
binary_sensors:
|
||||||
|
- input: some_binary_sensor
|
||||||
|
18
tests/components/online_image/common-esp32.yaml
Normal file
18
tests/components/online_image/common-esp32.yaml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<<: !include common.yaml
|
||||||
|
|
||||||
|
spi:
|
||||||
|
- id: spi_main_lcd
|
||||||
|
clk_pin: 16
|
||||||
|
mosi_pin: 17
|
||||||
|
miso_pin: 15
|
||||||
|
|
||||||
|
display:
|
||||||
|
- platform: ili9xxx
|
||||||
|
id: main_lcd
|
||||||
|
model: ili9342
|
||||||
|
cs_pin: 12
|
||||||
|
dc_pin: 13
|
||||||
|
reset_pin: 21
|
||||||
|
lambda: |-
|
||||||
|
it.fill(Color(0, 0, 0));
|
||||||
|
it.image(0, 0, id(online_rgba_image));
|
18
tests/components/online_image/common-esp8266.yaml
Normal file
18
tests/components/online_image/common-esp8266.yaml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<<: !include common.yaml
|
||||||
|
|
||||||
|
spi:
|
||||||
|
- id: spi_main_lcd
|
||||||
|
clk_pin: 14
|
||||||
|
mosi_pin: 13
|
||||||
|
miso_pin: 12
|
||||||
|
|
||||||
|
display:
|
||||||
|
- platform: ili9xxx
|
||||||
|
id: main_lcd
|
||||||
|
model: ili9342
|
||||||
|
cs_pin: 15
|
||||||
|
dc_pin: 3
|
||||||
|
reset_pin: 1
|
||||||
|
lambda: |-
|
||||||
|
it.fill(Color(0, 0, 0));
|
||||||
|
it.image(0, 0, id(online_rgba_image));
|
37
tests/components/online_image/common.yaml
Normal file
37
tests/components/online_image/common.yaml
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
wifi:
|
||||||
|
ssid: MySSID
|
||||||
|
password: password1
|
||||||
|
|
||||||
|
# Purposely test that `online_image:` does auto-load `image:`
|
||||||
|
# Keep the `image:` undefined.
|
||||||
|
# image:
|
||||||
|
online_image:
|
||||||
|
- id: online_binary_image
|
||||||
|
url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png
|
||||||
|
format: PNG
|
||||||
|
type: BINARY
|
||||||
|
resize: 50x50
|
||||||
|
- id: online_binary_transparent_image
|
||||||
|
url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png
|
||||||
|
type: TRANSPARENT_BINARY
|
||||||
|
format: png
|
||||||
|
- id: online_rgba_image
|
||||||
|
url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png
|
||||||
|
format: PNG
|
||||||
|
type: RGBA
|
||||||
|
- id: online_rgb24_image
|
||||||
|
url: http://www.libpng.org/pub/png/img_png/pnglogo-blk-tiny.png
|
||||||
|
format: PNG
|
||||||
|
type: RGB24
|
||||||
|
use_transparency: true
|
||||||
|
|
||||||
|
# Check the set_url action
|
||||||
|
time:
|
||||||
|
- platform: sntp
|
||||||
|
on_time:
|
||||||
|
- at: "13:37:42"
|
||||||
|
then:
|
||||||
|
- online_image.set_url:
|
||||||
|
id: online_rgba_image
|
||||||
|
url: http://www.example.org/example.png
|
||||||
|
|
4
tests/components/online_image/test.esp32-ard.yaml
Normal file
4
tests/components/online_image/test.esp32-ard.yaml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<<: !include common-esp32.yaml
|
||||||
|
|
||||||
|
http_request:
|
||||||
|
verify_ssl: false
|
4
tests/components/online_image/test.esp32-idf.yaml
Normal file
4
tests/components/online_image/test.esp32-idf.yaml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<<: !include common-esp32.yaml
|
||||||
|
|
||||||
|
http_request:
|
||||||
|
|
Loading…
x
Reference in New Issue
Block a user