mirror of
https://github.com/esphome/esphome.git
synced 2025-01-13 08:03:18 +01:00
Merge branch 'dev' into nrf52_core
This commit is contained in:
commit
d8dadb6a22
81 changed files with 3715 additions and 250 deletions
|
@ -277,6 +277,7 @@ esphome/components/noblex/* @AGalfra
|
|||
esphome/components/nrf52/* @tomaszduda23
|
||||
esphome/components/number/* @esphome/core
|
||||
esphome/components/one_wire/* @ssieb
|
||||
esphome/components/online_image/* @guillempages
|
||||
esphome/components/ota/* @esphome/core
|
||||
esphome/components/output/* @esphome/core
|
||||
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.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 (
|
||||
CONF_CODE,
|
||||
CONF_ID,
|
||||
CONF_MQTT_ID,
|
||||
CONF_ON_STATE,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_CODE,
|
||||
CONF_WEB_SERVER_ID,
|
||||
)
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
from esphome.cpp_helpers import setup_entity
|
||||
|
||||
CODEOWNERS = ["@grahambrown11", "@hwstar"]
|
||||
|
@ -77,67 +78,72 @@ AlarmControlPanelCondition = alarm_control_panel_ns.class_(
|
|||
"AlarmControlPanelCondition", automation.Condition
|
||||
)
|
||||
|
||||
ALARM_CONTROL_PANEL_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
|
||||
web_server.WEBSERVER_SORTING_SCHEMA
|
||||
).extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(AlarmControlPanel),
|
||||
cv.Optional(CONF_ON_STATE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_TRIGGERED): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TriggeredTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_ARMING): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmingTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_PENDING): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PendingTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_ARMED_HOME): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedHomeTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_ARMED_NIGHT): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedNightTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_ARMED_AWAY): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedAwayTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_DISARMED): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DisarmedTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_CLEARED): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ClearedTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_CHIME): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ChimeTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_READY): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ReadyTrigger),
|
||||
}
|
||||
),
|
||||
}
|
||||
ALARM_CONTROL_PANEL_SCHEMA = (
|
||||
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
|
||||
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
|
||||
.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(AlarmControlPanel),
|
||||
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
|
||||
mqtt.MQTTAlarmControlPanelComponent
|
||||
),
|
||||
cv.Optional(CONF_ON_STATE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_TRIGGERED): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TriggeredTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_ARMING): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmingTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_PENDING): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PendingTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_ARMED_HOME): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedHomeTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_ARMED_NIGHT): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedNightTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_ARMED_AWAY): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedAwayTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_DISARMED): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DisarmedTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_CLEARED): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ClearedTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_CHIME): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ChimeTrigger),
|
||||
}
|
||||
),
|
||||
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(
|
||||
|
@ -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:
|
||||
web_server_ = await cg.get_variable(webserver_id)
|
||||
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):
|
||||
|
|
|
@ -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
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import color, display, font
|
||||
from esphome.components.display_menu_base import (
|
||||
DISPLAY_MENU_BASE_SCHEMA,
|
||||
DisplayMenuComponent,
|
||||
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_MENU_ITEM_VALUE = "menu_item_value"
|
||||
CONF_FOREGROUND_COLOR = "foreground_color"
|
||||
CONF_BACKGROUND_COLOR = "background_color"
|
||||
CONF_ON_REDRAW = "on_redraw"
|
||||
|
||||
graphical_display_menu_ns = cg.esphome_ns.namespace("graphical_display_menu")
|
||||
|
|
|
@ -236,7 +236,7 @@ void HydreonRGxxComponent::process_line_() {
|
|||
}
|
||||
bool is_data_line = false;
|
||||
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;
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -233,6 +233,7 @@ void I2SAudioSpeaker::loop() {
|
|||
switch (this->state_) {
|
||||
case speaker::STATE_STARTING:
|
||||
this->start_();
|
||||
[[fallthrough]];
|
||||
case speaker::STATE_RUNNING:
|
||||
case speaker::STATE_STOPPING:
|
||||
this->watch_();
|
||||
|
|
|
@ -21,22 +21,10 @@ from esphome.final_validate import full_config
|
|||
from esphome.helpers import write_file_if_changed
|
||||
|
||||
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 .btn import btn_spec
|
||||
from .checkbox import checkbox_spec
|
||||
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 .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 .schemas import (
|
||||
DISP_BG_SCHEMA,
|
||||
|
@ -51,8 +39,6 @@ from .schemas import (
|
|||
grid_alignments,
|
||||
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 .touchscreens import touchscreen_schema, touchscreens_to_code
|
||||
from .trigger import generate_triggers
|
||||
|
@ -64,7 +50,31 @@ from .types import (
|
|||
lv_style_t,
|
||||
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"
|
||||
DEPENDENCIES = ["display"]
|
||||
|
@ -75,7 +85,7 @@ LOGGER = logging.getLogger(__name__)
|
|||
for w_type in (
|
||||
label_spec,
|
||||
obj_spec,
|
||||
btn_spec,
|
||||
button_spec,
|
||||
bar_spec,
|
||||
slider_spec,
|
||||
arc_spec,
|
||||
|
@ -86,6 +96,15 @@ for w_type in (
|
|||
checkbox_spec,
|
||||
img_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
|
||||
|
||||
|
@ -244,6 +263,7 @@ async def to_code(config):
|
|||
await add_widgets(lv_scr_act, config)
|
||||
await add_pages(lv_component, config)
|
||||
await add_top_layer(config)
|
||||
await msgboxes_to_code(config)
|
||||
await disp_update(f"{lv_component}->get_disp()", config)
|
||||
Widget.set_completed()
|
||||
await generate_triggers(lv_component)
|
||||
|
@ -308,6 +328,7 @@ CONFIG_SCHEMA = (
|
|||
cv.Exclusive(CONF_PAGES, CONF_PAGES): cv.ensure_list(
|
||||
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_TOP_LAYER): container_schema(obj_spec),
|
||||
cv.Optional(df.CONF_TRANSPARENCY_KEY, default=0x000400): lvalid.lv_color,
|
||||
|
|
|
@ -38,7 +38,7 @@ from .types import (
|
|||
lv_disp_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(
|
||||
|
@ -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:
|
||||
return
|
||||
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))
|
||||
if bg_image := config.get(CONF_DISP_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")
|
||||
BAR_MODES = LvConstant("LV_BAR_MODE_", "NORMAL", "SYMMETRICAL", "RANGE")
|
||||
|
||||
BTNMATRIX_CTRLS = LvConstant(
|
||||
BUTTONMATRIX_CTRLS = LvConstant(
|
||||
"LV_BTNMATRIX_CTRL_",
|
||||
"HIDDEN",
|
||||
"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
|
||||
|
||||
#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);
|
||||
lv_obj_add_event_cb(
|
||||
lv_obj,
|
||||
[](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)
|
||||
return;
|
||||
auto key_idx = lv_btnmatrix_get_selected_btn(self->obj);
|
||||
|
|
|
@ -1,13 +1,6 @@
|
|||
#pragma once
|
||||
#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
|
||||
#ifndef LV_CONF_H
|
||||
#define LV_CONF_SKIP 1 // NOLINT
|
||||
|
@ -19,6 +12,12 @@
|
|||
#include "esphome/core/log.h"
|
||||
#include <lvgl.h>
|
||||
#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
|
||||
#include "esphome/components/image/image.h"
|
||||
#endif // USE_LVGL_IMAGE
|
||||
|
@ -246,7 +245,7 @@ class LVEncoderListener : public Parented<LvglComponent> {
|
|||
};
|
||||
#endif // USE_LVGL_ROTARY_ENCODER
|
||||
#ifdef USE_LVGL_BUTTONMATRIX
|
||||
class LvBtnmatrixType : public key_provider::KeyProvider, public LvCompound {
|
||||
class LvButtonMatrixType : public key_provider::KeyProvider, public LvCompound {
|
||||
public:
|
||||
void set_obj(lv_obj_t *lv_obj) override;
|
||||
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 .schemas import ENCODER_SCHEMA
|
||||
from .types import lv_indev_type_t
|
||||
from .widget import add_group
|
||||
from .widgets import add_group
|
||||
|
||||
ROTARY_ENCODER_CONFIG = cv.ensure_list(
|
||||
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 .lvcode import LambdaContext, LocalVariable, lv, lv_assign, lv_variable
|
||||
from .obj import obj_spec
|
||||
from .schemas import ALL_STYLES
|
||||
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())")
|
||||
|
||||
|
@ -26,7 +26,7 @@ async def styles_to_code(config):
|
|||
svar = cg.new_Pvariable(style[CONF_ID])
|
||||
lv.style_init(svar)
|
||||
for prop, validator in ALL_STYLES.items():
|
||||
if value := style.get(prop):
|
||||
if (value := style.get(prop)) is not None:
|
||||
if isinstance(validator, LValidator):
|
||||
value = await validator.process(value)
|
||||
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):
|
||||
for tconf in config.get(CONF_TOUCHSCREENS) or ():
|
||||
for tconf in config.get(CONF_TOUCHSCREENS, ()):
|
||||
lvgl_components_required.add(CONF_TOUCHSCREEN)
|
||||
touchscreen = await cg.get_variable(tconf[CONF_TOUCHSCREEN_ID])
|
||||
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 .types import LV_EVENT
|
||||
from .widget import widget_map
|
||||
from .widgets import widget_map
|
||||
|
||||
|
||||
async def generate_triggers(lv_component):
|
||||
|
|
|
@ -8,7 +8,7 @@ from esphome.core import ID, TimePeriod
|
|||
from esphome.coroutine import FakeAwaitable
|
||||
from esphome.cpp_generator import AssignmentExpression, CallExpression, MockObj
|
||||
|
||||
from .defines import (
|
||||
from ..defines import (
|
||||
CONF_DEFAULT,
|
||||
CONF_FLEX_ALIGN_CROSS,
|
||||
CONF_FLEX_ALIGN_MAIN,
|
||||
|
@ -32,8 +32,8 @@ from .defines import (
|
|||
join_enums,
|
||||
literal,
|
||||
)
|
||||
from .helpers import add_lv_use
|
||||
from .lvcode import (
|
||||
from ..helpers import add_lv_use
|
||||
from ..lvcode import (
|
||||
LvConditional,
|
||||
add_line_marks,
|
||||
lv,
|
||||
|
@ -43,8 +43,8 @@ from .lvcode import (
|
|||
lv_obj,
|
||||
lv_Pvariable,
|
||||
)
|
||||
from .schemas import ALL_STYLES, STYLE_REMAP, WIDGET_TYPES
|
||||
from .types import (
|
||||
from ..schemas import ALL_STYLES, STYLE_REMAP, WIDGET_TYPES
|
||||
from ..types import (
|
||||
LV_STATE,
|
||||
LvType,
|
||||
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()}"))
|
||||
if layout_type == TYPE_GRID:
|
||||
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_array = cg.static_const_array(row_id, cg.RawExpression(rows))
|
||||
w.set_style("grid_row_dsc_array", row_array, 0)
|
||||
columns = (
|
||||
"{" + ",".join(layout[CONF_GRID_COLUMNS]) + ", LV_GRID_TEMPLATE_LAST}"
|
||||
)
|
||||
columns = [str(x) for x in layout[CONF_GRID_COLUMNS]]
|
||||
columns = "{" + ",".join(columns) + ", LV_GRID_TEMPLATE_LAST}"
|
||||
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))
|
||||
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
|
||||
:return:
|
||||
"""
|
||||
for w in config.get(CONF_WIDGETS) or ():
|
||||
for w in config.get(CONF_WIDGETS, ()):
|
||||
w_type, w_cnfig = next(iter(w.items()))
|
||||
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.config_validation as cv
|
||||
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 .defines import CONF_AUTO_START, CONF_MAIN, CONF_REPEAT_COUNT, CONF_SRC
|
||||
from .helpers import lvgl_components_required
|
||||
from ..automation import action_to_code
|
||||
from ..defines import CONF_AUTO_START, CONF_MAIN, CONF_REPEAT_COUNT, CONF_SRC
|
||||
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 .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_SRC_LIST_ID = "src_list_id"
|
|
@ -8,7 +8,7 @@ from esphome.const import (
|
|||
)
|
||||
from esphome.cpp_types import nullptr
|
||||
|
||||
from .defines import (
|
||||
from ..defines import (
|
||||
ARC_MODES,
|
||||
CONF_ADJUSTABLE,
|
||||
CONF_CHANGE_RATE,
|
||||
|
@ -19,10 +19,10 @@ from .defines import (
|
|||
CONF_START_ANGLE,
|
||||
literal,
|
||||
)
|
||||
from .lv_validation import angle, get_start_value, lv_float
|
||||
from .lvcode import lv, lv_obj
|
||||
from .types import LvNumber, NumberType
|
||||
from .widget import Widget
|
||||
from ..lv_validation import angle, get_start_value, lv_float
|
||||
from ..lvcode import lv, lv_obj
|
||||
from ..types import LvNumber, NumberType
|
||||
from . import Widget
|
||||
|
||||
CONF_ARC = "arc"
|
||||
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 .lv_validation import lv_text
|
||||
from .lvcode import lv
|
||||
from .schemas import TEXT_SCHEMA
|
||||
from .types import LvBoolean
|
||||
from .widget import Widget, WidgetType
|
||||
from ..defines import CONF_INDICATOR, CONF_MAIN, CONF_TEXT
|
||||
from ..lv_validation import lv_text
|
||||
from ..lvcode import lv
|
||||
from ..schemas import TEXT_SCHEMA
|
||||
from ..types import LvBoolean
|
||||
from . import Widget, WidgetType
|
||||
|
||||
CONF_CHECKBOX = "checkbox"
|
||||
|
||||
|
@ -18,7 +18,7 @@ class CheckboxType(WidgetType):
|
|||
)
|
||||
|
||||
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))
|
||||
|
||||
|
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
|
||||
from esphome.const import CONF_ANGLE, CONF_MODE
|
||||
|
||||
from .defines import (
|
||||
from ..defines import (
|
||||
CONF_ANTIALIAS,
|
||||
CONF_MAIN,
|
||||
CONF_OFFSET_X,
|
||||
|
@ -12,11 +12,11 @@ from .defines import (
|
|||
CONF_ZOOM,
|
||||
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 .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"
|
||||
|
||||
|
@ -65,16 +65,16 @@ class ImgType(WidgetType):
|
|||
async def to_code(self, w: Widget, config):
|
||||
if src := config.get(CONF_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_y = config[CONF_PIVOT_Y]
|
||||
lv.img_set_pivot(w.obj, pivot_x, pivot_y)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
if CONF_ANTIALIAS in config:
|
||||
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
|
||||
|
||||
from .defines import (
|
||||
from ..defines import (
|
||||
CONF_LONG_MODE,
|
||||
CONF_MAIN,
|
||||
CONF_RECOLOR,
|
||||
|
@ -9,10 +9,10 @@ from .defines import (
|
|||
CONF_TEXT,
|
||||
LV_LONG_MODES,
|
||||
)
|
||||
from .lv_validation import lv_bool, lv_text
|
||||
from .schemas import TEXT_SCHEMA
|
||||
from .types import LvText, WidgetType
|
||||
from .widget import Widget
|
||||
from ..lv_validation import lv_bool, lv_text
|
||||
from ..schemas import TEXT_SCHEMA
|
||||
from ..types import LvText, WidgetType
|
||||
from . import Widget
|
||||
|
||||
CONF_LABEL = "label"
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_BRIGHTNESS, CONF_COLOR, CONF_LED
|
||||
|
||||
from .defines import CONF_MAIN
|
||||
from .lv_validation import lv_brightness, lv_color
|
||||
from .lvcode import lv
|
||||
from .types import LvType
|
||||
from .widget import Widget, WidgetType
|
||||
from ..defines import CONF_MAIN
|
||||
from ..lv_validation import lv_brightness, lv_color
|
||||
from ..lvcode import lv
|
||||
from ..types import LvType
|
||||
from . import Widget, WidgetType
|
||||
|
||||
LED_SCHEMA = cv.Schema(
|
||||
{
|
||||
|
@ -20,9 +20,9 @@ class LedType(WidgetType):
|
|||
super().__init__(CONF_LED, LvType("lv_led_t"), (CONF_MAIN,), LED_SCHEMA)
|
||||
|
||||
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))
|
||||
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))
|
||||
|
||||
|
|
@ -3,11 +3,10 @@ import functools
|
|||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
|
||||
from . import defines as df
|
||||
from .defines import CONF_MAIN, literal
|
||||
from .lvcode import lv
|
||||
from .types import LvType
|
||||
from .widget import Widget, WidgetType
|
||||
from ..defines import CONF_MAIN, literal
|
||||
from ..lvcode import lv
|
||||
from ..types import LvType
|
||||
from . import Widget, WidgetType
|
||||
|
||||
CONF_LINE = "line"
|
||||
CONF_POINTS = "points"
|
||||
|
@ -32,7 +31,7 @@ def cv_point_list(value):
|
|||
|
||||
|
||||
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),
|
||||
}
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
import esphome.config_validation as cv
|
||||
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 .lv_validation import animated, get_start_value, lv_float
|
||||
from .lvcode import lv
|
||||
from .types import LvNumber, NumberType
|
||||
from .widget import Widget
|
||||
from ..defines import BAR_MODES, CONF_ANIMATED, CONF_INDICATOR, CONF_MAIN, literal
|
||||
from ..lv_validation import animated, get_start_value, lv_float
|
||||
from ..lvcode import lv
|
||||
from ..types import LvNumber, NumberType
|
||||
from . import Widget
|
||||
|
||||
# Note this file cannot be called "bar.py" because that name is disallowed.
|
||||
|
||||
CONF_BAR = "bar"
|
||||
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 .automation import update_to_code
|
||||
from .defines import CONF_MAIN, CONF_OBJ
|
||||
from .schemas import create_modify_schema
|
||||
from .types import ObjUpdateAction, WidgetType, lv_obj_t
|
||||
from ..automation import update_to_code
|
||||
from ..defines import CONF_MAIN, CONF_OBJ
|
||||
from ..schemas import create_modify_schema
|
||||
from ..types import ObjUpdateAction, WidgetType, lv_obj_t
|
||||
|
||||
|
||||
class ObjType(WidgetType):
|
|
@ -2,7 +2,7 @@ from esphome import automation, codegen as cg
|
|||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_PAGES, CONF_TIME
|
||||
|
||||
from .defines import (
|
||||
from ..defines import (
|
||||
CONF_ANIMATION,
|
||||
CONF_LVGL_ID,
|
||||
CONF_PAGE,
|
||||
|
@ -10,11 +10,11 @@ from .defines import (
|
|||
CONF_SKIP,
|
||||
LV_ANIM,
|
||||
)
|
||||
from .lv_validation import lv_bool, lv_milliseconds
|
||||
from .lvcode import LVGL_COMP_ARG, LambdaContext, add_line_marks, lv_add, lvgl_comp
|
||||
from .schemas import LVGL_SCHEMA
|
||||
from .types import LvglAction, lv_page_t
|
||||
from .widget import Widget, WidgetType, add_widgets, set_obj_properties
|
||||
from ..lv_validation import lv_bool, lv_milliseconds
|
||||
from ..lvcode import LVGL_COMP_ARG, LambdaContext, add_line_marks, lv_add, lvgl_comp
|
||||
from ..schemas import LVGL_SCHEMA
|
||||
from ..types import LvglAction, lv_page_t
|
||||
from . import Widget, WidgetType, add_widgets, set_obj_properties
|
||||
|
||||
|
||||
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
|
||||
from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_MODE, CONF_VALUE
|
||||
|
||||
from .defines import (
|
||||
from ..defines import (
|
||||
BAR_MODES,
|
||||
CONF_ANIMATED,
|
||||
CONF_INDICATOR,
|
||||
|
@ -9,12 +9,12 @@ from .defines import (
|
|||
CONF_MAIN,
|
||||
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_validation import animated, get_start_value, lv_float
|
||||
from .lvcode import lv
|
||||
from .types import LvNumber, NumberType
|
||||
from .widget import Widget
|
||||
|
||||
CONF_SLIDER = "slider"
|
||||
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
|
||||
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 .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"
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
from .defines import CONF_INDICATOR, CONF_KNOB, CONF_MAIN
|
||||
from .types import LvBoolean
|
||||
from .widget import WidgetType
|
||||
from ..defines import CONF_INDICATOR, CONF_KNOB, CONF_MAIN
|
||||
from ..types import LvBoolean
|
||||
from . import WidgetType
|
||||
|
||||
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 esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome import automation
|
||||
from esphome.automation import Condition
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import logger
|
||||
from esphome.components.esp32 import add_idf_sdkconfig_option
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_AVAILABILITY,
|
||||
CONF_BIRTH_MESSAGE,
|
||||
|
@ -13,21 +14,21 @@ from esphome.const import (
|
|||
CONF_CLIENT_CERTIFICATE,
|
||||
CONF_CLIENT_CERTIFICATE_KEY,
|
||||
CONF_CLIENT_ID,
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_COMMAND_RETAIN,
|
||||
CONF_COMMAND_TOPIC,
|
||||
CONF_DISCOVERY,
|
||||
CONF_DISCOVERY_OBJECT_ID_GENERATOR,
|
||||
CONF_DISCOVERY_PREFIX,
|
||||
CONF_DISCOVERY_RETAIN,
|
||||
CONF_DISCOVERY_UNIQUE_ID_GENERATOR,
|
||||
CONF_DISCOVERY_OBJECT_ID_GENERATOR,
|
||||
CONF_ID,
|
||||
CONF_KEEPALIVE,
|
||||
CONF_LEVEL,
|
||||
CONF_LOG_TOPIC,
|
||||
CONF_ON_JSON_MESSAGE,
|
||||
CONF_ON_MESSAGE,
|
||||
CONF_ON_CONNECT,
|
||||
CONF_ON_DISCONNECT,
|
||||
CONF_ON_JSON_MESSAGE,
|
||||
CONF_ON_MESSAGE,
|
||||
CONF_PASSWORD,
|
||||
CONF_PAYLOAD,
|
||||
CONF_PAYLOAD_AVAILABLE,
|
||||
|
@ -45,12 +46,11 @@ from esphome.const import (
|
|||
CONF_USE_ABBREVIATIONS,
|
||||
CONF_USERNAME,
|
||||
CONF_WILL_MESSAGE,
|
||||
PLATFORM_BK72XX,
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_ESP8266,
|
||||
PLATFORM_BK72XX,
|
||||
)
|
||||
from esphome.core import coroutine_with_priority, CORE
|
||||
from esphome.components.esp32 import add_idf_sdkconfig_option
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
|
||||
DEPENDENCIES = ["network"]
|
||||
|
||||
|
@ -110,6 +110,9 @@ MQTTDisconnectTrigger = mqtt_ns.class_(
|
|||
MQTTComponent = mqtt_ns.class_("MQTTComponent", cg.Component)
|
||||
MQTTConnectedCondition = mqtt_ns.class_("MQTTConnectedCondition", Condition)
|
||||
|
||||
MQTTAlarmControlPanelComponent = mqtt_ns.class_(
|
||||
"MQTTAlarmControlPanelComponent", MQTTComponent
|
||||
)
|
||||
MQTTBinarySensorComponent = mqtt_ns.class_("MQTTBinarySensorComponent", MQTTComponent)
|
||||
MQTTClimateComponent = mqtt_ns.class_("MQTTClimateComponent", 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
|
||||
import esphome.config_validation as cv
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import color
|
||||
from esphome.const import (
|
||||
CONF_VISIBLE,
|
||||
)
|
||||
from . import CONF_NEXTION_ID
|
||||
from . import Nextion
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_BACKGROUND_COLOR, CONF_FOREGROUND_COLOR, CONF_VISIBLE
|
||||
|
||||
from . import CONF_NEXTION_ID, Nextion
|
||||
|
||||
CONF_VARIABLE_NAME = "variable_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_AUTO_WAKE_ON_TOUCH = "auto_wake_on_touch"
|
||||
CONF_WAVE_MAX_LENGTH = "wave_max_length"
|
||||
CONF_BACKGROUND_COLOR = "background_color"
|
||||
CONF_BACKGROUND_PRESSED_COLOR = "background_pressed_color"
|
||||
CONF_FOREGROUND_COLOR = "foreground_color"
|
||||
CONF_FOREGROUND_PRESSED_COLOR = "foreground_pressed_color"
|
||||
CONF_FONT_ID = "font_id"
|
||||
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) {
|
||||
#if USE_NETWORK_IPV6
|
||||
if (addrlen < sizeof(sockaddr_in6)) {
|
||||
errno = EINVAL;
|
||||
return 0;
|
||||
}
|
||||
auto *server = reinterpret_cast<sockaddr_in6 *>(addr);
|
||||
memset(server, 0, sizeof(sockaddr_in6));
|
||||
server->sin6_family = AF_INET6;
|
||||
server->sin6_port = htons(port);
|
||||
if (ip_address.find(':') != std::string::npos) {
|
||||
if (addrlen < sizeof(sockaddr_in6)) {
|
||||
errno = EINVAL;
|
||||
return 0;
|
||||
}
|
||||
auto *server = reinterpret_cast<sockaddr_in6 *>(addr);
|
||||
memset(server, 0, sizeof(sockaddr_in6));
|
||||
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;
|
||||
inet6_aton(ip_address.c_str(), &ip6);
|
||||
memcpy(server->sin6_addr.un.u32_addr, ip6.addr, sizeof(ip6.addr));
|
||||
return sizeof(sockaddr_in6);
|
||||
}
|
||||
return sizeof(sockaddr_in6);
|
||||
#else
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
if (addrlen < sizeof(sockaddr_in)) {
|
||||
errno = EINVAL;
|
||||
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_port = htons(port);
|
||||
return sizeof(sockaddr_in);
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
}
|
||||
|
||||
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_CONFIG = "away_config"
|
||||
CONF_AWAY_STATE_TOPIC = "away_state_topic"
|
||||
CONF_BACKGROUND_COLOR = "background_color"
|
||||
CONF_BACKLIGHT_PIN = "backlight_pin"
|
||||
CONF_BASELINE = "baseline"
|
||||
CONF_BATTERY_LEVEL = "battery_level"
|
||||
|
@ -311,6 +312,7 @@ CONF_FLOW = "flow"
|
|||
CONF_FLOW_CONTROL_PIN = "flow_control_pin"
|
||||
CONF_FOR = "for"
|
||||
CONF_FORCE_UPDATE = "force_update"
|
||||
CONF_FOREGROUND_COLOR = "foreground_color"
|
||||
CONF_FORMALDEHYDE = "formaldehyde"
|
||||
CONF_FORMAT = "format"
|
||||
CONF_FORWARD_ACTIVE_ENERGY = "forward_active_energy"
|
||||
|
|
|
@ -39,9 +39,12 @@
|
|||
#define USE_LOCK
|
||||
#define USE_LOGGER
|
||||
#define USE_LVGL
|
||||
#define USE_LVGL_ANIMIMG
|
||||
#define USE_LVGL_BINARY_SENSOR
|
||||
#define USE_LVGL_BUTTONMATRIX
|
||||
#define USE_LVGL_FONT
|
||||
#define USE_LVGL_IMAGE
|
||||
#define USE_LVGL_KEYBOARD
|
||||
#define USE_LVGL_KEY_LISTENER
|
||||
#define USE_LVGL_TOUCHSCREEN
|
||||
#define USE_LVGL_ROTARY_ENCODER
|
||||
|
@ -50,6 +53,7 @@
|
|||
#define USE_MQTT
|
||||
#define USE_NEXTION_TFT_UPLOAD
|
||||
#define USE_NUMBER
|
||||
#define USE_ONLINE_IMAGE_PNG_SUPPORT
|
||||
#define USE_OTA
|
||||
#define USE_OTA_PASSWORD
|
||||
#define USE_OTA_STATE_CALLBACK
|
||||
|
|
|
@ -686,7 +686,7 @@ template<class T> class ExternalRAMAllocator {
|
|||
}
|
||||
|
||||
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
|
||||
functionpointer/arduino-MLX90393@1.0.0 ; mlx90393
|
||||
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
|
||||
https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library
|
||||
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
|
||||
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:
|
||||
log_level: TRACE
|
||||
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:
|
||||
- touchscreen_id: tft_touch
|
||||
long_press_repeat_time: 200ms
|
||||
|
@ -9,6 +56,13 @@ lvgl:
|
|||
- id: page1
|
||||
skip: true
|
||||
widgets:
|
||||
- animimg:
|
||||
height: 60
|
||||
id: anim_img
|
||||
src: [cat_image, dog_image]
|
||||
repeat_count: 10
|
||||
duration: 1s
|
||||
auto_start: true
|
||||
- label:
|
||||
id: hello_label
|
||||
text: Hello world
|
||||
|
@ -16,7 +70,9 @@ lvgl:
|
|||
align: center
|
||||
text_font: montserrat_40
|
||||
border_post: true
|
||||
|
||||
on_click:
|
||||
then:
|
||||
- lvgl.animimg.stop: anim_img
|
||||
- label:
|
||||
text: "Hello shiny day"
|
||||
text_color: 0xFFFFFF
|
||||
|
@ -94,7 +150,65 @@ lvgl:
|
|||
width: 10px
|
||||
x: 100
|
||||
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:
|
||||
id: button_button
|
||||
width: 20%
|
||||
height: 10%
|
||||
pressed:
|
||||
|
@ -137,6 +251,7 @@ lvgl:
|
|||
on_long_press_repeat:
|
||||
logger.log: Button clicked
|
||||
- led:
|
||||
id: lv_led
|
||||
color: 0x00FF00
|
||||
brightness: 50%
|
||||
align: right_mid
|
||||
|
@ -151,6 +266,41 @@ lvgl:
|
|||
|
||||
- id: page2
|
||||
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:
|
||||
align: left_mid
|
||||
id: lv_arc
|
||||
|
@ -160,7 +310,6 @@ lvgl:
|
|||
- logger.log:
|
||||
format: "Arc value is %f"
|
||||
args: [x]
|
||||
group: general
|
||||
scroll_on_focus: true
|
||||
value: 75
|
||||
min_value: 1
|
||||
|
@ -201,6 +350,7 @@ lvgl:
|
|||
- switch:
|
||||
align: right_mid
|
||||
- checkbox:
|
||||
id: checkbox_id
|
||||
text: Checkbox
|
||||
align: bottom_right
|
||||
- slider:
|
||||
|
@ -221,6 +371,78 @@ lvgl:
|
|||
- lvgl.slider.update:
|
||||
id: slider_id
|
||||
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:
|
||||
- file: "gfonts://Roboto"
|
||||
id: space16
|
||||
|
@ -230,7 +452,7 @@ image:
|
|||
- id: cat_image
|
||||
resize: 256x48
|
||||
file: $component_dir/logo-text.svg
|
||||
- id: dog_img
|
||||
- id: dog_image
|
||||
file: $component_dir/logo-text.svg
|
||||
resize: 256x48
|
||||
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 {
|
||||
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…
Reference in a new issue