1
0
mirror of https://github.com/esphome/esphome.git synced 2025-03-15 15:18:16 +00:00

Merge branch 'dev' into nrf52_core

This commit is contained in:
tomaszduda23 2024-08-06 16:37:30 +02:00 committed by GitHub
commit d8dadb6a22
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
81 changed files with 3715 additions and 250 deletions

View File

@ -277,6 +277,7 @@ esphome/components/noblex/* @AGalfra
esphome/components/nrf52/* @tomaszduda23 esphome/components/nrf52/* @tomaszduda23
esphome/components/number/* @esphome/core esphome/components/number/* @esphome/core
esphome/components/one_wire/* @ssieb esphome/components/one_wire/* @ssieb
esphome/components/online_image/* @guillempages
esphome/components/ota/* @esphome/core esphome/components/ota/* @esphome/core
esphome/components/output/* @esphome/core esphome/components/output/* @esphome/core
esphome/components/pca6416a/* @Mat931 esphome/components/pca6416a/* @Mat931

View File

@ -1,16 +1,17 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import web_server
from esphome import automation from esphome import automation
from esphome.automation import maybe_simple_id from esphome.automation import maybe_simple_id
from esphome.core import CORE, coroutine_with_priority import esphome.codegen as cg
from esphome.components import mqtt, web_server
import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_CODE,
CONF_ID, CONF_ID,
CONF_MQTT_ID,
CONF_ON_STATE, CONF_ON_STATE,
CONF_TRIGGER_ID, CONF_TRIGGER_ID,
CONF_CODE,
CONF_WEB_SERVER_ID, CONF_WEB_SERVER_ID,
) )
from esphome.core import CORE, coroutine_with_priority
from esphome.cpp_helpers import setup_entity from esphome.cpp_helpers import setup_entity
CODEOWNERS = ["@grahambrown11", "@hwstar"] CODEOWNERS = ["@grahambrown11", "@hwstar"]
@ -77,11 +78,15 @@ AlarmControlPanelCondition = alarm_control_panel_ns.class_(
"AlarmControlPanelCondition", automation.Condition "AlarmControlPanelCondition", automation.Condition
) )
ALARM_CONTROL_PANEL_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( ALARM_CONTROL_PANEL_SCHEMA = (
web_server.WEBSERVER_SORTING_SCHEMA cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
).extend( .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
.extend(
{ {
cv.GenerateID(): cv.declare_id(AlarmControlPanel), cv.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.Optional(CONF_ON_STATE): automation.validate_automation(
{ {
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger), cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger),
@ -139,6 +144,7 @@ ALARM_CONTROL_PANEL_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
), ),
} }
) )
)
ALARM_CONTROL_PANEL_ACTION_SCHEMA = maybe_simple_id( ALARM_CONTROL_PANEL_ACTION_SCHEMA = maybe_simple_id(
{ {
@ -192,6 +198,9 @@ async def setup_alarm_control_panel_core_(var, config):
if (webserver_id := config.get(CONF_WEB_SERVER_ID)) is not None: if (webserver_id := config.get(CONF_WEB_SERVER_ID)) is not None:
web_server_ = await cg.get_variable(webserver_id) web_server_ = await cg.get_variable(webserver_id)
web_server.add_entity_to_sorting_list(web_server_, var, config) web_server.add_entity_to_sorting_list(web_server_, var, config)
if mqtt_id := config.get(CONF_MQTT_ID):
mqtt_ = cg.new_Pvariable(mqtt_id, var)
await mqtt.register_mqtt_component(mqtt_, config)
async def register_alarm_control_panel(var, config): async def register_alarm_control_panel(var, config):

View File

@ -1,19 +1,22 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import display, font, color
from esphome.const import CONF_DISPLAY, CONF_ID, CONF_TRIGGER_ID
from esphome import automation, core from esphome import automation, core
import esphome.codegen as cg
from esphome.components import color, display, font
from esphome.components.display_menu_base import ( from esphome.components.display_menu_base import (
DISPLAY_MENU_BASE_SCHEMA, DISPLAY_MENU_BASE_SCHEMA,
DisplayMenuComponent, DisplayMenuComponent,
display_menu_to_code, display_menu_to_code,
) )
import esphome.config_validation as cv
from esphome.const import (
CONF_BACKGROUND_COLOR,
CONF_DISPLAY,
CONF_FOREGROUND_COLOR,
CONF_ID,
CONF_TRIGGER_ID,
)
CONF_FONT = "font" CONF_FONT = "font"
CONF_MENU_ITEM_VALUE = "menu_item_value" CONF_MENU_ITEM_VALUE = "menu_item_value"
CONF_FOREGROUND_COLOR = "foreground_color"
CONF_BACKGROUND_COLOR = "background_color"
CONF_ON_REDRAW = "on_redraw" CONF_ON_REDRAW = "on_redraw"
graphical_display_menu_ns = cg.esphome_ns.namespace("graphical_display_menu") graphical_display_menu_ns = cg.esphome_ns.namespace("graphical_display_menu")

View File

@ -236,7 +236,7 @@ void HydreonRGxxComponent::process_line_() {
} }
bool is_data_line = false; bool is_data_line = false;
for (int i = 0; i < NUM_SENSORS; i++) { for (int i = 0; i < NUM_SENSORS; i++) {
if (this->sensors_[i] != nullptr && this->buffer_starts_with_(PROTOCOL_NAMES[i])) { if (this->sensors_[i] != nullptr && this->buffer_.find(PROTOCOL_NAMES[i]) != std::string::npos) {
is_data_line = true; is_data_line = true;
break; break;
} }

View File

@ -233,6 +233,7 @@ void I2SAudioSpeaker::loop() {
switch (this->state_) { switch (this->state_) {
case speaker::STATE_STARTING: case speaker::STATE_STARTING:
this->start_(); this->start_();
[[fallthrough]];
case speaker::STATE_RUNNING: case speaker::STATE_RUNNING:
case speaker::STATE_STOPPING: case speaker::STATE_STOPPING:
this->watch_(); this->watch_();

View File

@ -21,22 +21,10 @@ from esphome.final_validate import full_config
from esphome.helpers import write_file_if_changed from esphome.helpers import write_file_if_changed
from . import defines as df, helpers, lv_validation as lvalid from . import defines as df, helpers, lv_validation as lvalid
from .animimg import animimg_spec
from .arc import arc_spec
from .automation import disp_update, update_to_code from .automation import disp_update, update_to_code
from .btn import btn_spec
from .checkbox import checkbox_spec
from .defines import CONF_SKIP from .defines import CONF_SKIP
from .img import img_spec
from .label import label_spec
from .led import led_spec
from .line import line_spec
from .lv_bar import bar_spec
from .lv_switch import switch_spec
from .lv_validation import lv_bool, lv_images_used from .lv_validation import lv_bool, lv_images_used
from .lvcode import LvContext, LvglComponent from .lvcode import LvContext, LvglComponent
from .obj import obj_spec
from .page import add_pages, page_spec
from .rotary_encoders import ROTARY_ENCODER_CONFIG, rotary_encoders_to_code from .rotary_encoders import ROTARY_ENCODER_CONFIG, rotary_encoders_to_code
from .schemas import ( from .schemas import (
DISP_BG_SCHEMA, DISP_BG_SCHEMA,
@ -51,8 +39,6 @@ from .schemas import (
grid_alignments, grid_alignments,
obj_schema, obj_schema,
) )
from .slider import slider_spec
from .spinner import spinner_spec
from .styles import add_top_layer, styles_to_code, theme_to_code from .styles import add_top_layer, styles_to_code, theme_to_code
from .touchscreens import touchscreen_schema, touchscreens_to_code from .touchscreens import touchscreen_schema, touchscreens_to_code
from .trigger import generate_triggers from .trigger import generate_triggers
@ -64,7 +50,31 @@ from .types import (
lv_style_t, lv_style_t,
lvgl_ns, lvgl_ns,
) )
from .widget import Widget, add_widgets, lv_scr_act, set_obj_properties from .widgets import Widget, add_widgets, lv_scr_act, set_obj_properties
from .widgets.animimg import animimg_spec
from .widgets.arc import arc_spec
from .widgets.button import button_spec
from .widgets.buttonmatrix import buttonmatrix_spec
from .widgets.checkbox import checkbox_spec
from .widgets.dropdown import dropdown_spec
from .widgets.img import img_spec
from .widgets.keyboard import keyboard_spec
from .widgets.label import label_spec
from .widgets.led import led_spec
from .widgets.line import line_spec
from .widgets.lv_bar import bar_spec
from .widgets.meter import meter_spec
from .widgets.msgbox import MSGBOX_SCHEMA, msgboxes_to_code
from .widgets.obj import obj_spec
from .widgets.page import add_pages, page_spec
from .widgets.roller import roller_spec
from .widgets.slider import slider_spec
from .widgets.spinbox import spinbox_spec
from .widgets.spinner import spinner_spec
from .widgets.switch import switch_spec
from .widgets.tabview import tabview_spec
from .widgets.textarea import textarea_spec
from .widgets.tileview import tileview_spec
DOMAIN = "lvgl" DOMAIN = "lvgl"
DEPENDENCIES = ["display"] DEPENDENCIES = ["display"]
@ -75,7 +85,7 @@ LOGGER = logging.getLogger(__name__)
for w_type in ( for w_type in (
label_spec, label_spec,
obj_spec, obj_spec,
btn_spec, button_spec,
bar_spec, bar_spec,
slider_spec, slider_spec,
arc_spec, arc_spec,
@ -86,6 +96,15 @@ for w_type in (
checkbox_spec, checkbox_spec,
img_spec, img_spec,
switch_spec, switch_spec,
tabview_spec,
buttonmatrix_spec,
meter_spec,
dropdown_spec,
roller_spec,
textarea_spec,
spinbox_spec,
keyboard_spec,
tileview_spec,
): ):
WIDGET_TYPES[w_type.name] = w_type WIDGET_TYPES[w_type.name] = w_type
@ -244,6 +263,7 @@ async def to_code(config):
await add_widgets(lv_scr_act, config) await add_widgets(lv_scr_act, config)
await add_pages(lv_component, config) await add_pages(lv_component, config)
await add_top_layer(config) await add_top_layer(config)
await msgboxes_to_code(config)
await disp_update(f"{lv_component}->get_disp()", config) await disp_update(f"{lv_component}->get_disp()", config)
Widget.set_completed() Widget.set_completed()
await generate_triggers(lv_component) await generate_triggers(lv_component)
@ -308,6 +328,7 @@ CONFIG_SCHEMA = (
cv.Exclusive(CONF_PAGES, CONF_PAGES): cv.ensure_list( cv.Exclusive(CONF_PAGES, CONF_PAGES): cv.ensure_list(
container_schema(page_spec) container_schema(page_spec)
), ),
cv.Optional(df.CONF_MSGBOXES): cv.ensure_list(MSGBOX_SCHEMA),
cv.Optional(df.CONF_PAGE_WRAP, default=True): lv_bool, cv.Optional(df.CONF_PAGE_WRAP, default=True): lv_bool,
cv.Optional(df.CONF_TOP_LAYER): container_schema(obj_spec), cv.Optional(df.CONF_TOP_LAYER): container_schema(obj_spec),
cv.Optional(df.CONF_TRANSPARENCY_KEY, default=0x000400): lvalid.lv_color, cv.Optional(df.CONF_TRANSPARENCY_KEY, default=0x000400): lvalid.lv_color,

View File

@ -38,7 +38,7 @@ from .types import (
lv_disp_t, lv_disp_t,
lv_obj_t, lv_obj_t,
) )
from .widget import Widget, get_widgets, lv_scr_act, set_obj_properties from .widgets import Widget, get_widgets, lv_scr_act, set_obj_properties
async def action_to_code( async def action_to_code(
@ -109,7 +109,7 @@ async def disp_update(disp, config: dict):
if CONF_DISP_BG_COLOR not in config and CONF_DISP_BG_IMAGE not in config: if CONF_DISP_BG_COLOR not in config and CONF_DISP_BG_IMAGE not in config:
return return
with LocalVariable("lv_disp_tmp", lv_disp_t, literal(disp)) as disp_temp: with LocalVariable("lv_disp_tmp", lv_disp_t, literal(disp)) as disp_temp:
if bg_color := config.get(CONF_DISP_BG_COLOR): if (bg_color := config.get(CONF_DISP_BG_COLOR)) is not None:
lv.disp_set_bg_color(disp_temp, await lv_color.process(bg_color)) lv.disp_set_bg_color(disp_temp, await lv_color.process(bg_color))
if bg_image := config.get(CONF_DISP_BG_IMAGE): if bg_image := config.get(CONF_DISP_BG_IMAGE):
lv.disp_set_bg_image(disp_temp, await lv_image.process(bg_image)) lv.disp_set_bg_image(disp_temp, await lv_image.process(bg_image))

View 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,
)
)

View File

@ -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()

View File

@ -304,7 +304,7 @@ OBJ_FLAGS = (
ARC_MODES = LvConstant("LV_ARC_MODE_", "NORMAL", "REVERSE", "SYMMETRICAL") ARC_MODES = LvConstant("LV_ARC_MODE_", "NORMAL", "REVERSE", "SYMMETRICAL")
BAR_MODES = LvConstant("LV_BAR_MODE_", "NORMAL", "SYMMETRICAL", "RANGE") BAR_MODES = LvConstant("LV_BAR_MODE_", "NORMAL", "SYMMETRICAL", "RANGE")
BTNMATRIX_CTRLS = LvConstant( BUTTONMATRIX_CTRLS = LvConstant(
"LV_BTNMATRIX_CTRL_", "LV_BTNMATRIX_CTRL_",
"HIDDEN", "HIDDEN",
"NO_REPEAT", "NO_REPEAT",

View 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))

View 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

View File

@ -146,12 +146,12 @@ LVEncoderListener::LVEncoderListener(lv_indev_type_t type, uint16_t lpt, uint16_
#endif // USE_LVGL_ROTARY_ENCODER #endif // USE_LVGL_ROTARY_ENCODER
#ifdef USE_LVGL_BUTTONMATRIX #ifdef USE_LVGL_BUTTONMATRIX
void LvBtnmatrixType::set_obj(lv_obj_t *lv_obj) { void LvButtonMatrixType::set_obj(lv_obj_t *lv_obj) {
LvCompound::set_obj(lv_obj); LvCompound::set_obj(lv_obj);
lv_obj_add_event_cb( lv_obj_add_event_cb(
lv_obj, lv_obj,
[](lv_event_t *event) { [](lv_event_t *event) {
auto *self = static_cast<LvBtnmatrixType *>(event->user_data); auto *self = static_cast<LvButtonMatrixType *>(event->user_data);
if (self->key_callback_.size() == 0) if (self->key_callback_.size() == 0)
return; return;
auto key_idx = lv_btnmatrix_get_selected_btn(self->obj); auto key_idx = lv_btnmatrix_get_selected_btn(self->obj);

View File

@ -1,13 +1,6 @@
#pragma once #pragma once
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#ifdef USE_LVGL_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif // USE_LVGL_BINARY_SENSOR
#ifdef USE_LVGL_ROTARY_ENCODER
#include "esphome/components/rotary_encoder/rotary_encoder.h"
#endif // USE_LVGL_ROTARY_ENCODER
// required for clang-tidy // required for clang-tidy
#ifndef LV_CONF_H #ifndef LV_CONF_H
#define LV_CONF_SKIP 1 // NOLINT #define LV_CONF_SKIP 1 // NOLINT
@ -19,6 +12,12 @@
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include <lvgl.h> #include <lvgl.h>
#include <vector> #include <vector>
#ifdef USE_LVGL_ROTARY_ENCODER
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/components/rotary_encoder/rotary_encoder.h"
#endif // USE_LVGL_ROTARY_ENCODER
#ifdef USE_LVGL_IMAGE #ifdef USE_LVGL_IMAGE
#include "esphome/components/image/image.h" #include "esphome/components/image/image.h"
#endif // USE_LVGL_IMAGE #endif // USE_LVGL_IMAGE
@ -246,7 +245,7 @@ class LVEncoderListener : public Parented<LvglComponent> {
}; };
#endif // USE_LVGL_ROTARY_ENCODER #endif // USE_LVGL_ROTARY_ENCODER
#ifdef USE_LVGL_BUTTONMATRIX #ifdef USE_LVGL_BUTTONMATRIX
class LvBtnmatrixType : public key_provider::KeyProvider, public LvCompound { class LvButtonMatrixType : public key_provider::KeyProvider, public LvCompound {
public: public:
void set_obj(lv_obj_t *lv_obj) override; void set_obj(lv_obj_t *lv_obj) override;
uint16_t get_selected() { return lv_btnmatrix_get_selected_btn(this->obj); } uint16_t get_selected() { return lv_btnmatrix_get_selected_btn(this->obj); }

View 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()))

View 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

View File

@ -16,7 +16,7 @@ from .helpers import lvgl_components_required
from .lvcode import lv, lv_add, lv_expr from .lvcode import lv, lv_add, lv_expr
from .schemas import ENCODER_SCHEMA from .schemas import ENCODER_SCHEMA
from .types import lv_indev_type_t from .types import lv_indev_type_t
from .widget import add_group from .widgets import add_group
ROTARY_ENCODER_CONFIG = cv.ensure_list( ROTARY_ENCODER_CONFIG = cv.ensure_list(
ENCODER_SCHEMA.extend( ENCODER_SCHEMA.extend(

View 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()))

View 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

View 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
)
)

View File

@ -12,10 +12,10 @@ from .defines import (
) )
from .helpers import add_lv_use from .helpers import add_lv_use
from .lvcode import LambdaContext, LocalVariable, lv, lv_assign, lv_variable from .lvcode import LambdaContext, LocalVariable, lv, lv_assign, lv_variable
from .obj import obj_spec
from .schemas import ALL_STYLES from .schemas import ALL_STYLES
from .types import lv_lambda_t, lv_obj_t, lv_obj_t_ptr from .types import lv_lambda_t, lv_obj_t, lv_obj_t_ptr
from .widget import Widget, add_widgets, set_obj_properties, theme_widget_map from .widgets import Widget, add_widgets, set_obj_properties, theme_widget_map
from .widgets.obj import obj_spec
TOP_LAYER = literal("lv_disp_get_layer_top(lv_component->get_disp())") TOP_LAYER = literal("lv_disp_get_layer_top(lv_component->get_disp())")
@ -26,7 +26,7 @@ async def styles_to_code(config):
svar = cg.new_Pvariable(style[CONF_ID]) svar = cg.new_Pvariable(style[CONF_ID])
lv.style_init(svar) lv.style_init(svar)
for prop, validator in ALL_STYLES.items(): for prop, validator in ALL_STYLES.items():
if value := style.get(prop): if (value := style.get(prop)) is not None:
if isinstance(validator, LValidator): if isinstance(validator, LValidator):
value = await validator.process(value) value = await validator.process(value)
if isinstance(value, list): if isinstance(value, list):

View 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()))

View 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

View 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()))

View 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

View 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,
)
)

View File

@ -34,7 +34,7 @@ def touchscreen_schema(config):
async def touchscreens_to_code(var, config): async def touchscreens_to_code(var, config):
for tconf in config.get(CONF_TOUCHSCREENS) or (): for tconf in config.get(CONF_TOUCHSCREENS, ()):
lvgl_components_required.add(CONF_TOUCHSCREEN) lvgl_components_required.add(CONF_TOUCHSCREEN)
touchscreen = await cg.get_variable(tconf[CONF_TOUCHSCREEN_ID]) touchscreen = await cg.get_variable(tconf[CONF_TOUCHSCREEN_ID])
lpt = tconf[CONF_LONG_PRESS_TIME].total_milliseconds lpt = tconf[CONF_LONG_PRESS_TIME].total_milliseconds

View File

@ -13,7 +13,7 @@ from .defines import (
) )
from .lvcode import EVENT_ARG, LambdaContext, LvConditional, lv, lv_add from .lvcode import EVENT_ARG, LambdaContext, LvConditional, lv, lv_add
from .types import LV_EVENT from .types import LV_EVENT
from .widget import widget_map from .widgets import widget_map
async def generate_triggers(lv_component): async def generate_triggers(lv_component):

View File

@ -8,7 +8,7 @@ from esphome.core import ID, TimePeriod
from esphome.coroutine import FakeAwaitable from esphome.coroutine import FakeAwaitable
from esphome.cpp_generator import AssignmentExpression, CallExpression, MockObj from esphome.cpp_generator import AssignmentExpression, CallExpression, MockObj
from .defines import ( from ..defines import (
CONF_DEFAULT, CONF_DEFAULT,
CONF_FLEX_ALIGN_CROSS, CONF_FLEX_ALIGN_CROSS,
CONF_FLEX_ALIGN_MAIN, CONF_FLEX_ALIGN_MAIN,
@ -32,8 +32,8 @@ from .defines import (
join_enums, join_enums,
literal, literal,
) )
from .helpers import add_lv_use from ..helpers import add_lv_use
from .lvcode import ( from ..lvcode import (
LvConditional, LvConditional,
add_line_marks, add_line_marks,
lv, lv,
@ -43,8 +43,8 @@ from .lvcode import (
lv_obj, lv_obj,
lv_Pvariable, lv_Pvariable,
) )
from .schemas import ALL_STYLES, STYLE_REMAP, WIDGET_TYPES from ..schemas import ALL_STYLES, STYLE_REMAP, WIDGET_TYPES
from .types import ( from ..types import (
LV_STATE, LV_STATE,
LvType, LvType,
WidgetType, WidgetType,
@ -282,13 +282,13 @@ async def set_obj_properties(w: Widget, config):
lv_obj.set_layout(w.obj, literal(f"LV_LAYOUT_{layout_type.upper()}")) lv_obj.set_layout(w.obj, literal(f"LV_LAYOUT_{layout_type.upper()}"))
if layout_type == TYPE_GRID: if layout_type == TYPE_GRID:
wid = config[CONF_ID] wid = config[CONF_ID]
rows = "{" + ",".join(layout[CONF_GRID_ROWS]) + ", LV_GRID_TEMPLATE_LAST}" rows = [str(x) for x in layout[CONF_GRID_ROWS]]
rows = "{" + ",".join(rows) + ", LV_GRID_TEMPLATE_LAST}"
row_id = ID(f"{wid}_row_dsc", is_declaration=True, type=lv_coord_t) row_id = ID(f"{wid}_row_dsc", is_declaration=True, type=lv_coord_t)
row_array = cg.static_const_array(row_id, cg.RawExpression(rows)) row_array = cg.static_const_array(row_id, cg.RawExpression(rows))
w.set_style("grid_row_dsc_array", row_array, 0) w.set_style("grid_row_dsc_array", row_array, 0)
columns = ( columns = [str(x) for x in layout[CONF_GRID_COLUMNS]]
"{" + ",".join(layout[CONF_GRID_COLUMNS]) + ", LV_GRID_TEMPLATE_LAST}" columns = "{" + ",".join(columns) + ", LV_GRID_TEMPLATE_LAST}"
)
column_id = ID(f"{wid}_column_dsc", is_declaration=True, type=lv_coord_t) column_id = ID(f"{wid}_column_dsc", is_declaration=True, type=lv_coord_t)
column_array = cg.static_const_array(column_id, cg.RawExpression(columns)) column_array = cg.static_const_array(column_id, cg.RawExpression(columns))
w.set_style("grid_column_dsc_array", column_array, 0) w.set_style("grid_column_dsc_array", column_array, 0)
@ -368,7 +368,7 @@ async def add_widgets(parent: Widget, config: dict):
:param config: The configuration :param config: The configuration
:return: :return:
""" """
for w in config.get(CONF_WIDGETS) or (): for w in config.get(CONF_WIDGETS, ()):
w_type, w_cnfig = next(iter(w.items())) w_type, w_cnfig = next(iter(w.items()))
await widget_to_code(w_cnfig, w_type, parent.obj) await widget_to_code(w_cnfig, w_type, parent.obj)

View File

@ -2,17 +2,17 @@ from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_DURATION, CONF_ID from esphome.const import CONF_DURATION, CONF_ID
from esphome.cpp_generator import MockObj
from ...cpp_generator import MockObj from ..automation import action_to_code
from .automation import action_to_code from ..defines import CONF_AUTO_START, CONF_MAIN, CONF_REPEAT_COUNT, CONF_SRC
from .defines import CONF_AUTO_START, CONF_MAIN, CONF_REPEAT_COUNT, CONF_SRC from ..helpers import lvgl_components_required
from .helpers import lvgl_components_required from ..lv_validation import lv_image, lv_milliseconds
from ..lvcode import lv, lv_expr
from ..types import LvType, ObjUpdateAction, void_ptr
from . import Widget, WidgetType, get_widgets
from .img import CONF_IMAGE from .img import CONF_IMAGE
from .label import CONF_LABEL from .label import CONF_LABEL
from .lv_validation import lv_image, lv_milliseconds
from .lvcode import lv, lv_expr
from .types import LvType, ObjUpdateAction, void_ptr
from .widget import Widget, WidgetType, get_widgets
CONF_ANIMIMG = "animimg" CONF_ANIMIMG = "animimg"
CONF_SRC_LIST_ID = "src_list_id" CONF_SRC_LIST_ID = "src_list_id"

View File

@ -8,7 +8,7 @@ from esphome.const import (
) )
from esphome.cpp_types import nullptr from esphome.cpp_types import nullptr
from .defines import ( from ..defines import (
ARC_MODES, ARC_MODES,
CONF_ADJUSTABLE, CONF_ADJUSTABLE,
CONF_CHANGE_RATE, CONF_CHANGE_RATE,
@ -19,10 +19,10 @@ from .defines import (
CONF_START_ANGLE, CONF_START_ANGLE,
literal, literal,
) )
from .lv_validation import angle, get_start_value, lv_float from ..lv_validation import angle, get_start_value, lv_float
from .lvcode import lv, lv_obj from ..lvcode import lv, lv_obj
from .types import LvNumber, NumberType from ..types import LvNumber, NumberType
from .widget import Widget from . import Widget
CONF_ARC = "arc" CONF_ARC = "arc"
ARC_SCHEMA = cv.Schema( ARC_SCHEMA = cv.Schema(

View 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()

View 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
)

View File

@ -1,9 +1,9 @@
from .defines import CONF_INDICATOR, CONF_MAIN, CONF_TEXT from ..defines import CONF_INDICATOR, CONF_MAIN, CONF_TEXT
from .lv_validation import lv_text from ..lv_validation import lv_text
from .lvcode import lv from ..lvcode import lv
from .schemas import TEXT_SCHEMA from ..schemas import TEXT_SCHEMA
from .types import LvBoolean from ..types import LvBoolean
from .widget import Widget, WidgetType from . import Widget, WidgetType
CONF_CHECKBOX = "checkbox" CONF_CHECKBOX = "checkbox"
@ -18,7 +18,7 @@ class CheckboxType(WidgetType):
) )
async def to_code(self, w: Widget, config): async def to_code(self, w: Widget, config):
if value := config.get(CONF_TEXT): if (value := config.get(CONF_TEXT)) is not None:
lv.checkbox_set_text(w.obj, await lv_text.process(value)) lv.checkbox_set_text(w.obj, await lv_text.process(value))

View 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()

View File

@ -1,7 +1,7 @@
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ANGLE, CONF_MODE from esphome.const import CONF_ANGLE, CONF_MODE
from .defines import ( from ..defines import (
CONF_ANTIALIAS, CONF_ANTIALIAS,
CONF_MAIN, CONF_MAIN,
CONF_OFFSET_X, CONF_OFFSET_X,
@ -12,11 +12,11 @@ from .defines import (
CONF_ZOOM, CONF_ZOOM,
LvConstant, LvConstant,
) )
from ..lv_validation import angle, lv_bool, lv_image, size, zoom
from ..lvcode import lv
from ..types import lv_img_t
from . import Widget, WidgetType
from .label import CONF_LABEL from .label import CONF_LABEL
from .lv_validation import angle, lv_bool, lv_image, size, zoom
from .lvcode import lv
from .types import lv_img_t
from .widget import Widget, WidgetType
CONF_IMAGE = "image" CONF_IMAGE = "image"
@ -65,16 +65,16 @@ class ImgType(WidgetType):
async def to_code(self, w: Widget, config): async def to_code(self, w: Widget, config):
if src := config.get(CONF_SRC): if src := config.get(CONF_SRC):
lv.img_set_src(w.obj, await lv_image.process(src)) lv.img_set_src(w.obj, await lv_image.process(src))
if cf_angle := config.get(CONF_ANGLE): if (cf_angle := config.get(CONF_ANGLE)) is not None:
pivot_x = config[CONF_PIVOT_X] pivot_x = config[CONF_PIVOT_X]
pivot_y = config[CONF_PIVOT_Y] pivot_y = config[CONF_PIVOT_Y]
lv.img_set_pivot(w.obj, pivot_x, pivot_y) lv.img_set_pivot(w.obj, pivot_x, pivot_y)
lv.img_set_angle(w.obj, cf_angle) lv.img_set_angle(w.obj, cf_angle)
if img_zoom := config.get(CONF_ZOOM): if (img_zoom := config.get(CONF_ZOOM)) is not None:
lv.img_set_zoom(w.obj, img_zoom) lv.img_set_zoom(w.obj, img_zoom)
if offset := config.get(CONF_OFFSET_X): if (offset := config.get(CONF_OFFSET_X)) is not None:
lv.img_set_offset_x(w.obj, offset) lv.img_set_offset_x(w.obj, offset)
if offset := config.get(CONF_OFFSET_Y): if (offset := config.get(CONF_OFFSET_Y)) is not None:
lv.img_set_offset_y(w.obj, offset) lv.img_set_offset_y(w.obj, offset)
if CONF_ANTIALIAS in config: if CONF_ANTIALIAS in config:
lv.img_set_antialias(w.obj, config[CONF_ANTIALIAS]) lv.img_set_antialias(w.obj, config[CONF_ANTIALIAS])

View 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()

View File

@ -1,6 +1,6 @@
import esphome.config_validation as cv import esphome.config_validation as cv
from .defines import ( from ..defines import (
CONF_LONG_MODE, CONF_LONG_MODE,
CONF_MAIN, CONF_MAIN,
CONF_RECOLOR, CONF_RECOLOR,
@ -9,10 +9,10 @@ from .defines import (
CONF_TEXT, CONF_TEXT,
LV_LONG_MODES, LV_LONG_MODES,
) )
from .lv_validation import lv_bool, lv_text from ..lv_validation import lv_bool, lv_text
from .schemas import TEXT_SCHEMA from ..schemas import TEXT_SCHEMA
from .types import LvText, WidgetType from ..types import LvText, WidgetType
from .widget import Widget from . import Widget
CONF_LABEL = "label" CONF_LABEL = "label"

View File

@ -1,11 +1,11 @@
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_BRIGHTNESS, CONF_COLOR, CONF_LED from esphome.const import CONF_BRIGHTNESS, CONF_COLOR, CONF_LED
from .defines import CONF_MAIN from ..defines import CONF_MAIN
from .lv_validation import lv_brightness, lv_color from ..lv_validation import lv_brightness, lv_color
from .lvcode import lv from ..lvcode import lv
from .types import LvType from ..types import LvType
from .widget import Widget, WidgetType from . import Widget, WidgetType
LED_SCHEMA = cv.Schema( LED_SCHEMA = cv.Schema(
{ {
@ -20,9 +20,9 @@ class LedType(WidgetType):
super().__init__(CONF_LED, LvType("lv_led_t"), (CONF_MAIN,), LED_SCHEMA) super().__init__(CONF_LED, LvType("lv_led_t"), (CONF_MAIN,), LED_SCHEMA)
async def to_code(self, w: Widget, config): async def to_code(self, w: Widget, config):
if color := config.get(CONF_COLOR): if (color := config.get(CONF_COLOR)) is not None:
lv.led_set_color(w.obj, await lv_color.process(color)) lv.led_set_color(w.obj, await lv_color.process(color))
if brightness := config.get(CONF_BRIGHTNESS): if (brightness := config.get(CONF_BRIGHTNESS)) is not None:
lv.led_set_brightness(w.obj, await lv_brightness.process(brightness)) lv.led_set_brightness(w.obj, await lv_brightness.process(brightness))

View File

@ -3,11 +3,10 @@ import functools
import esphome.codegen as cg import esphome.codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from . import defines as df from ..defines import CONF_MAIN, literal
from .defines import CONF_MAIN, literal from ..lvcode import lv
from .lvcode import lv from ..types import LvType
from .types import LvType from . import Widget, WidgetType
from .widget import Widget, WidgetType
CONF_LINE = "line" CONF_LINE = "line"
CONF_POINTS = "points" CONF_POINTS = "points"
@ -32,7 +31,7 @@ def cv_point_list(value):
LINE_SCHEMA = { LINE_SCHEMA = {
cv.Required(df.CONF_POINTS): cv_point_list, cv.Required(CONF_POINTS): cv_point_list,
cv.GenerateID(CONF_POINT_LIST_ID): cv.declare_id(lv_point_t), cv.GenerateID(CONF_POINT_LIST_ID): cv.declare_id(lv_point_t),
} }

View File

@ -1,11 +1,13 @@
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_MODE, CONF_VALUE from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_MODE, CONF_VALUE
from .defines import BAR_MODES, CONF_ANIMATED, CONF_INDICATOR, CONF_MAIN, literal from ..defines import BAR_MODES, CONF_ANIMATED, CONF_INDICATOR, CONF_MAIN, literal
from .lv_validation import animated, get_start_value, lv_float from ..lv_validation import animated, get_start_value, lv_float
from .lvcode import lv from ..lvcode import lv
from .types import LvNumber, NumberType from ..types import LvNumber, NumberType
from .widget import Widget from . import Widget
# Note this file cannot be called "bar.py" because that name is disallowed.
CONF_BAR = "bar" CONF_BAR = "bar"
BAR_MODIFY_SCHEMA = cv.Schema( BAR_MODIFY_SCHEMA = cv.Schema(

View 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)

View 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)

View File

@ -1,9 +1,9 @@
from esphome import automation from esphome import automation
from .automation import update_to_code from ..automation import update_to_code
from .defines import CONF_MAIN, CONF_OBJ from ..defines import CONF_MAIN, CONF_OBJ
from .schemas import create_modify_schema from ..schemas import create_modify_schema
from .types import ObjUpdateAction, WidgetType, lv_obj_t from ..types import ObjUpdateAction, WidgetType, lv_obj_t
class ObjType(WidgetType): class ObjType(WidgetType):

View File

@ -2,7 +2,7 @@ from esphome import automation, codegen as cg
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_PAGES, CONF_TIME from esphome.const import CONF_ID, CONF_PAGES, CONF_TIME
from .defines import ( from ..defines import (
CONF_ANIMATION, CONF_ANIMATION,
CONF_LVGL_ID, CONF_LVGL_ID,
CONF_PAGE, CONF_PAGE,
@ -10,11 +10,11 @@ from .defines import (
CONF_SKIP, CONF_SKIP,
LV_ANIM, LV_ANIM,
) )
from .lv_validation import lv_bool, lv_milliseconds from ..lv_validation import lv_bool, lv_milliseconds
from .lvcode import LVGL_COMP_ARG, LambdaContext, add_line_marks, lv_add, lvgl_comp from ..lvcode import LVGL_COMP_ARG, LambdaContext, add_line_marks, lv_add, lvgl_comp
from .schemas import LVGL_SCHEMA from ..schemas import LVGL_SCHEMA
from .types import LvglAction, lv_page_t from ..types import LvglAction, lv_page_t
from .widget import Widget, WidgetType, add_widgets, set_obj_properties from . import Widget, WidgetType, add_widgets, set_obj_properties
class PageType(WidgetType): class PageType(WidgetType):

View 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()

View File

@ -1,7 +1,7 @@
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_MODE, CONF_VALUE from esphome.const import CONF_MAX_VALUE, CONF_MIN_VALUE, CONF_MODE, CONF_VALUE
from .defines import ( from ..defines import (
BAR_MODES, BAR_MODES,
CONF_ANIMATED, CONF_ANIMATED,
CONF_INDICATOR, CONF_INDICATOR,
@ -9,12 +9,12 @@ from .defines import (
CONF_MAIN, CONF_MAIN,
literal, literal,
) )
from .helpers import add_lv_use from ..helpers import add_lv_use
from ..lv_validation import animated, get_start_value, lv_float
from ..lvcode import lv
from ..types import LvNumber, NumberType
from . import Widget
from .lv_bar import CONF_BAR from .lv_bar import CONF_BAR
from .lv_validation import animated, get_start_value, lv_float
from .lvcode import lv
from .types import LvNumber, NumberType
from .widget import Widget
CONF_SLIDER = "slider" CONF_SLIDER = "slider"
SLIDER_MODIFY_SCHEMA = cv.Schema( SLIDER_MODIFY_SCHEMA = cv.Schema(

View 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)

View File

@ -1,12 +1,12 @@
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.cpp_generator import MockObjClass from esphome.cpp_generator import MockObjClass
from ..defines import CONF_ARC_LENGTH, CONF_INDICATOR, CONF_MAIN, CONF_SPIN_TIME
from ..lv_validation import angle
from ..lvcode import lv_expr
from ..types import LvType
from . import Widget, WidgetType
from .arc import CONF_ARC from .arc import CONF_ARC
from .defines import CONF_ARC_LENGTH, CONF_INDICATOR, CONF_MAIN, CONF_SPIN_TIME
from .lv_validation import angle
from .lvcode import lv_expr
from .types import LvType
from .widget import Widget, WidgetType
CONF_SPINNER = "spinner" CONF_SPINNER = "spinner"

View File

@ -1,6 +1,6 @@
from .defines import CONF_INDICATOR, CONF_KNOB, CONF_MAIN from ..defines import CONF_INDICATOR, CONF_KNOB, CONF_MAIN
from .types import LvBoolean from ..types import LvBoolean
from .widget import WidgetType from . import WidgetType
CONF_SWITCH = "switch" CONF_SWITCH = "switch"

View 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)

View 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()

View 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)

View File

@ -1,10 +1,11 @@
import re import re
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import automation from esphome import automation
from esphome.automation import Condition from esphome.automation import Condition
import esphome.codegen as cg
from esphome.components import logger from esphome.components import logger
from esphome.components.esp32 import add_idf_sdkconfig_option
import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_AVAILABILITY, CONF_AVAILABILITY,
CONF_BIRTH_MESSAGE, CONF_BIRTH_MESSAGE,
@ -13,21 +14,21 @@ from esphome.const import (
CONF_CLIENT_CERTIFICATE, CONF_CLIENT_CERTIFICATE,
CONF_CLIENT_CERTIFICATE_KEY, CONF_CLIENT_CERTIFICATE_KEY,
CONF_CLIENT_ID, CONF_CLIENT_ID,
CONF_COMMAND_TOPIC,
CONF_COMMAND_RETAIN, CONF_COMMAND_RETAIN,
CONF_COMMAND_TOPIC,
CONF_DISCOVERY, CONF_DISCOVERY,
CONF_DISCOVERY_OBJECT_ID_GENERATOR,
CONF_DISCOVERY_PREFIX, CONF_DISCOVERY_PREFIX,
CONF_DISCOVERY_RETAIN, CONF_DISCOVERY_RETAIN,
CONF_DISCOVERY_UNIQUE_ID_GENERATOR, CONF_DISCOVERY_UNIQUE_ID_GENERATOR,
CONF_DISCOVERY_OBJECT_ID_GENERATOR,
CONF_ID, CONF_ID,
CONF_KEEPALIVE, CONF_KEEPALIVE,
CONF_LEVEL, CONF_LEVEL,
CONF_LOG_TOPIC, CONF_LOG_TOPIC,
CONF_ON_JSON_MESSAGE,
CONF_ON_MESSAGE,
CONF_ON_CONNECT, CONF_ON_CONNECT,
CONF_ON_DISCONNECT, CONF_ON_DISCONNECT,
CONF_ON_JSON_MESSAGE,
CONF_ON_MESSAGE,
CONF_PASSWORD, CONF_PASSWORD,
CONF_PAYLOAD, CONF_PAYLOAD,
CONF_PAYLOAD_AVAILABLE, CONF_PAYLOAD_AVAILABLE,
@ -45,12 +46,11 @@ from esphome.const import (
CONF_USE_ABBREVIATIONS, CONF_USE_ABBREVIATIONS,
CONF_USERNAME, CONF_USERNAME,
CONF_WILL_MESSAGE, CONF_WILL_MESSAGE,
PLATFORM_BK72XX,
PLATFORM_ESP32, PLATFORM_ESP32,
PLATFORM_ESP8266, PLATFORM_ESP8266,
PLATFORM_BK72XX,
) )
from esphome.core import coroutine_with_priority, CORE from esphome.core import CORE, coroutine_with_priority
from esphome.components.esp32 import add_idf_sdkconfig_option
DEPENDENCIES = ["network"] DEPENDENCIES = ["network"]
@ -110,6 +110,9 @@ MQTTDisconnectTrigger = mqtt_ns.class_(
MQTTComponent = mqtt_ns.class_("MQTTComponent", cg.Component) MQTTComponent = mqtt_ns.class_("MQTTComponent", cg.Component)
MQTTConnectedCondition = mqtt_ns.class_("MQTTConnectedCondition", Condition) MQTTConnectedCondition = mqtt_ns.class_("MQTTConnectedCondition", Condition)
MQTTAlarmControlPanelComponent = mqtt_ns.class_(
"MQTTAlarmControlPanelComponent", MQTTComponent
)
MQTTBinarySensorComponent = mqtt_ns.class_("MQTTBinarySensorComponent", MQTTComponent) MQTTBinarySensorComponent = mqtt_ns.class_("MQTTBinarySensorComponent", MQTTComponent)
MQTTClimateComponent = mqtt_ns.class_("MQTTClimateComponent", MQTTComponent) MQTTClimateComponent = mqtt_ns.class_("MQTTClimateComponent", MQTTComponent)
MQTTCoverComponent = mqtt_ns.class_("MQTTCoverComponent", MQTTComponent) MQTTCoverComponent = mqtt_ns.class_("MQTTCoverComponent", MQTTComponent)

View 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

View 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

View File

@ -1,12 +1,11 @@
from string import ascii_letters, digits from string import ascii_letters, digits
import esphome.config_validation as cv
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import color from esphome.components import color
from esphome.const import ( import esphome.config_validation as cv
CONF_VISIBLE, from esphome.const import CONF_BACKGROUND_COLOR, CONF_FOREGROUND_COLOR, CONF_VISIBLE
)
from . import CONF_NEXTION_ID from . import CONF_NEXTION_ID, Nextion
from . import Nextion
CONF_VARIABLE_NAME = "variable_name" CONF_VARIABLE_NAME = "variable_name"
CONF_COMPONENT_NAME = "component_name" CONF_COMPONENT_NAME = "component_name"
@ -24,9 +23,7 @@ CONF_WAKE_UP_PAGE = "wake_up_page"
CONF_START_UP_PAGE = "start_up_page" CONF_START_UP_PAGE = "start_up_page"
CONF_AUTO_WAKE_ON_TOUCH = "auto_wake_on_touch" CONF_AUTO_WAKE_ON_TOUCH = "auto_wake_on_touch"
CONF_WAVE_MAX_LENGTH = "wave_max_length" CONF_WAVE_MAX_LENGTH = "wave_max_length"
CONF_BACKGROUND_COLOR = "background_color"
CONF_BACKGROUND_PRESSED_COLOR = "background_pressed_color" CONF_BACKGROUND_PRESSED_COLOR = "background_pressed_color"
CONF_FOREGROUND_COLOR = "foreground_color"
CONF_FOREGROUND_PRESSED_COLOR = "foreground_pressed_color" CONF_FOREGROUND_PRESSED_COLOR = "foreground_pressed_color"
CONF_FONT_ID = "font_id" CONF_FONT_ID = "font_id"
CONF_EXIT_REPARSE_ON_START = "exit_reparse_on_start" CONF_EXIT_REPARSE_ON_START = "exit_reparse_on_start"

View 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)

View 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

View 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

View 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

View 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

View 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

View 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

View File

@ -19,6 +19,7 @@ std::unique_ptr<Socket> socket_ip(int type, int protocol) {
socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const std::string &ip_address, uint16_t port) { socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const std::string &ip_address, uint16_t port) {
#if USE_NETWORK_IPV6 #if USE_NETWORK_IPV6
if (ip_address.find(':') != std::string::npos) {
if (addrlen < sizeof(sockaddr_in6)) { if (addrlen < sizeof(sockaddr_in6)) {
errno = EINVAL; errno = EINVAL;
return 0; return 0;
@ -28,15 +29,12 @@ socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const std::stri
server->sin6_family = AF_INET6; server->sin6_family = AF_INET6;
server->sin6_port = htons(port); server->sin6_port = htons(port);
if (ip_address.find('.') != std::string::npos) {
server->sin6_addr.un.u32_addr[3] = inet_addr(ip_address.c_str());
} else {
ip6_addr_t ip6; ip6_addr_t ip6;
inet6_aton(ip_address.c_str(), &ip6); inet6_aton(ip_address.c_str(), &ip6);
memcpy(server->sin6_addr.un.u32_addr, ip6.addr, sizeof(ip6.addr)); memcpy(server->sin6_addr.un.u32_addr, ip6.addr, sizeof(ip6.addr));
}
return sizeof(sockaddr_in6); return sizeof(sockaddr_in6);
#else }
#endif /* USE_NETWORK_IPV6 */
if (addrlen < sizeof(sockaddr_in)) { if (addrlen < sizeof(sockaddr_in)) {
errno = EINVAL; errno = EINVAL;
return 0; return 0;
@ -47,7 +45,6 @@ socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const std::stri
server->sin_addr.s_addr = inet_addr(ip_address.c_str()); server->sin_addr.s_addr = inet_addr(ip_address.c_str());
server->sin_port = htons(port); server->sin_port = htons(port);
return sizeof(sockaddr_in); return sizeof(sockaddr_in);
#endif /* USE_NETWORK_IPV6 */
} }
socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t port) { socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t port) {

View File

@ -76,6 +76,7 @@ CONF_AWAY = "away"
CONF_AWAY_COMMAND_TOPIC = "away_command_topic" CONF_AWAY_COMMAND_TOPIC = "away_command_topic"
CONF_AWAY_CONFIG = "away_config" CONF_AWAY_CONFIG = "away_config"
CONF_AWAY_STATE_TOPIC = "away_state_topic" CONF_AWAY_STATE_TOPIC = "away_state_topic"
CONF_BACKGROUND_COLOR = "background_color"
CONF_BACKLIGHT_PIN = "backlight_pin" CONF_BACKLIGHT_PIN = "backlight_pin"
CONF_BASELINE = "baseline" CONF_BASELINE = "baseline"
CONF_BATTERY_LEVEL = "battery_level" CONF_BATTERY_LEVEL = "battery_level"
@ -311,6 +312,7 @@ CONF_FLOW = "flow"
CONF_FLOW_CONTROL_PIN = "flow_control_pin" CONF_FLOW_CONTROL_PIN = "flow_control_pin"
CONF_FOR = "for" CONF_FOR = "for"
CONF_FORCE_UPDATE = "force_update" CONF_FORCE_UPDATE = "force_update"
CONF_FOREGROUND_COLOR = "foreground_color"
CONF_FORMALDEHYDE = "formaldehyde" CONF_FORMALDEHYDE = "formaldehyde"
CONF_FORMAT = "format" CONF_FORMAT = "format"
CONF_FORWARD_ACTIVE_ENERGY = "forward_active_energy" CONF_FORWARD_ACTIVE_ENERGY = "forward_active_energy"

View File

@ -39,9 +39,12 @@
#define USE_LOCK #define USE_LOCK
#define USE_LOGGER #define USE_LOGGER
#define USE_LVGL #define USE_LVGL
#define USE_LVGL_ANIMIMG
#define USE_LVGL_BINARY_SENSOR #define USE_LVGL_BINARY_SENSOR
#define USE_LVGL_BUTTONMATRIX
#define USE_LVGL_FONT #define USE_LVGL_FONT
#define USE_LVGL_IMAGE #define USE_LVGL_IMAGE
#define USE_LVGL_KEYBOARD
#define USE_LVGL_KEY_LISTENER #define USE_LVGL_KEY_LISTENER
#define USE_LVGL_TOUCHSCREEN #define USE_LVGL_TOUCHSCREEN
#define USE_LVGL_ROTARY_ENCODER #define USE_LVGL_ROTARY_ENCODER
@ -50,6 +53,7 @@
#define USE_MQTT #define USE_MQTT
#define USE_NEXTION_TFT_UPLOAD #define USE_NEXTION_TFT_UPLOAD
#define USE_NUMBER #define USE_NUMBER
#define USE_ONLINE_IMAGE_PNG_SUPPORT
#define USE_OTA #define USE_OTA
#define USE_OTA_PASSWORD #define USE_OTA_PASSWORD
#define USE_OTA_STATE_CALLBACK #define USE_OTA_STATE_CALLBACK

View File

@ -686,7 +686,7 @@ template<class T> class ExternalRAMAllocator {
} }
private: private:
Flags flags_{Flags::NONE}; Flags flags_{Flags::ALLOW_FAILURE};
}; };
/// @} /// @}

View File

@ -40,6 +40,7 @@ lib_deps =
wjtje/qr-code-generator-library@1.7.0 ; qr_code wjtje/qr-code-generator-library@1.7.0 ; qr_code
functionpointer/arduino-MLX90393@1.0.0 ; mlx90393 functionpointer/arduino-MLX90393@1.0.0 ; mlx90393
pavlodn/HaierProtocol@0.9.31 ; haier pavlodn/HaierProtocol@0.9.31 ; haier
kikuchan98/pngle@1.0.2 ; online_image
; This is using the repository until a new release is published to PlatformIO ; This is using the repository until a new release is published to PlatformIO
https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library
lvgl/lvgl@8.4.0 ; lvgl lvgl/lvgl@8.4.0 ; lvgl

2
tests/components/lvgl/.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
*.ttf -text

View File

@ -8,3 +8,121 @@ touchscreen:
x_max: 240 x_max: 240
y_max: 320 y_max: 320
font:
- file: "$component_dir/roboto.ttf"
id: roboto20
size: 20
extras:
- file: '$component_dir/materialdesignicons-webfont.ttf'
glyphs: [
"\U000F004B",
"\U0000f0ed",
"\U000F006E",
"\U000F012C",
"\U000F179B",
"\U000F0748",
"\U000F1A1B",
"\U000F02DC",
"\U000F0A02",
"\U000F035F",
"\U000F0156",
"\U000F0C5F",
"\U000f0084",
"\U000f0091",
]
- file: "$component_dir/helvetica.ttf"
id: helvetica20
- file: "$component_dir/roboto.ttf"
id: roboto10
size: 10
bpp: 4
extras:
- file: '$component_dir/materialdesignicons-webfont.ttf'
glyphs: [
"\U000F004B",
"\U0000f0ed",
"\U000F006E",
"\U000F012C",
"\U000F179B",
"\U000F0748",
"\U000F1A1B",
"\U000F02DC",
"\U000F0A02",
"\U000F035F",
"\U000F0156",
"\U000F0C5F",
"\U000f0084",
"\U000f0091",
]
sensor:
- platform: lvgl
id: lvgl_sensor_id
name: "LVGL Arc Sensor"
widget: lv_arc
- platform: lvgl
widget: slider_id
name: LVGL Slider
- platform: lvgl
widget: bar_id
id: lvgl_bar_sensor
name: LVGL Bar
- platform: lvgl
widget: spinbox_id
name: LVGL Spinbox
number:
- platform: lvgl
widget: slider_id
name: LVGL Slider
- platform: lvgl
widget: lv_arc
id: lvgl_arc_number
name: LVGL Arc
- platform: lvgl
widget: bar_id
id: lvgl_bar_number
name: LVGL Bar
- platform: lvgl
widget: spinbox_id
id: lvgl_spinbox_number
name: LVGL Spinbox
light:
- platform: lvgl
name: LVGL LED
id: lv_light
led: lv_led
binary_sensor:
- platform: lvgl
id: lvgl_pressbutton
name: Pressbutton
widget: spin_up
publish_initial_state: true
- platform: lvgl
name: ButtonMatrix button
widget: button_a
- platform: lvgl
id: switch_d
name: Matrix switch D
widget: button_d
on_click:
then:
- lvgl.page.previous:
animation: move_right
time: 600ms
- platform: lvgl
id: button_checker
name: LVGL button
widget: spin_up
on_state:
then:
- lvgl.checkbox.update:
id: checkbox_id
state:
checked: !lambda return x;
text: Unchecked
- platform: lvgl
name: LVGL checkbox
widget: checkbox_id

Binary file not shown.

View File

@ -1,6 +1,53 @@
lvgl: lvgl:
log_level: TRACE log_level: TRACE
bg_color: light_blue bg_color: light_blue
theme:
obj:
border_width: 1
style_definitions:
- id: style_test
bg_color: 0x2F8CD8
- id: header_footer
bg_color: 0x20214F
bg_grad_color: 0x005782
bg_grad_dir: VER
bg_opa: cover
border_width: 0
radius: 0
pad_all: 0
pad_row: 0
pad_column: 0
border_color: 0x0077b3
text_color: 0xFFFFFF
width: 100%
height: 30
border_side: [left, top]
text_decor: [underline, strikethrough]
- id: style_line
line_color: light_blue
line_width: 8
line_rounded: true
- id: date_style
text_font: roboto10
align: center
text_color: 0x000000
bg_opa: cover
radius: 4
pad_all: 2
- id: spin_button
height: 40
width: 40
- id: spin_label
align: center
text_align: center
text_font: space16
- id: bdr_style
border_color: 0x808080
border_width: 2
pad_all: 4
align: center
touchscreens: touchscreens:
- touchscreen_id: tft_touch - touchscreen_id: tft_touch
long_press_repeat_time: 200ms long_press_repeat_time: 200ms
@ -9,6 +56,13 @@ lvgl:
- id: page1 - id: page1
skip: true skip: true
widgets: widgets:
- animimg:
height: 60
id: anim_img
src: [cat_image, dog_image]
repeat_count: 10
duration: 1s
auto_start: true
- label: - label:
id: hello_label id: hello_label
text: Hello world text: Hello world
@ -16,7 +70,9 @@ lvgl:
align: center align: center
text_font: montserrat_40 text_font: montserrat_40
border_post: true border_post: true
on_click:
then:
- lvgl.animimg.stop: anim_img
- label: - label:
text: "Hello shiny day" text: "Hello shiny day"
text_color: 0xFFFFFF text_color: 0xFFFFFF
@ -94,7 +150,65 @@ lvgl:
width: 10px width: 10px
x: 100 x: 100
y: 120 y: 120
- buttonmatrix:
on_press:
logger.log:
format: "matrix button pressed: %d"
args: ["x"]
on_long_press:
lvgl.matrix.button.update:
id: [button_a, button_e, button_c]
control:
disabled: true
on_click:
logger.log:
format: "matrix button clicked: %d, is button_a = %u"
args: ["x", "id(button_a) == x"]
items:
checked:
bg_color: 0xFFFF00
id: b_matrix
rows:
- buttons:
- id: button_a
text: home icon
width: 2
control:
checkable: true
on_value:
logger.log:
format: "button_a value %d"
args: [x]
- id: button_b
text: B
width: 1
on_value:
logger.log:
format: "button_b value %d"
args: [x]
on_click:
then:
- lvgl.page.previous:
control:
hidden: false
- buttons:
- id: button_c
text: C
control:
checkable: false
- id: button_d
text: menu left
on_long_press:
then:
logger.log: Long pressed
on_long_press_repeat:
then:
logger.log: Long pressed repeated
- buttons:
- id: button_e
- button: - button:
id: button_button
width: 20% width: 20%
height: 10% height: 10%
pressed: pressed:
@ -137,6 +251,7 @@ lvgl:
on_long_press_repeat: on_long_press_repeat:
logger.log: Button clicked logger.log: Button clicked
- led: - led:
id: lv_led
color: 0x00FF00 color: 0x00FF00
brightness: 50% brightness: 50%
align: right_mid align: right_mid
@ -151,6 +266,41 @@ lvgl:
- id: page2 - id: page2
widgets: widgets:
- button:
styles: spin_button
id: spin_up
on_click:
- lvgl.spinbox.increment: spinbox_id
widgets:
- label:
styles: spin_label
text: "+"
- spinbox:
text_font: space16
id: spinbox_id
align: center
width: 120
range_from: -10
range_to: 1000
step: 5.0
rollover: false
digits: 6
decimal_places: 2
value: 15
on_value:
then:
- logger.log:
format: "Spinbox value is %f"
args: [x]
- button:
styles: spin_button
id: spin_down
on_click:
- lvgl.spinbox.decrement: spinbox_id
widgets:
- label:
styles: spin_label
text: "-"
- arc: - arc:
align: left_mid align: left_mid
id: lv_arc id: lv_arc
@ -160,7 +310,6 @@ lvgl:
- logger.log: - logger.log:
format: "Arc value is %f" format: "Arc value is %f"
args: [x] args: [x]
group: general
scroll_on_focus: true scroll_on_focus: true
value: 75 value: 75
min_value: 1 min_value: 1
@ -201,6 +350,7 @@ lvgl:
- switch: - switch:
align: right_mid align: right_mid
- checkbox: - checkbox:
id: checkbox_id
text: Checkbox text: Checkbox
align: bottom_right align: bottom_right
- slider: - slider:
@ -221,6 +371,78 @@ lvgl:
- lvgl.slider.update: - lvgl.slider.update:
id: slider_id id: slider_id
value: !lambda return (int)((float)rand() / RAND_MAX * 100); value: !lambda return (int)((float)rand() / RAND_MAX * 100);
- tabview:
id: tabview_id
width: 100%
height: 80%
position: top
on_value:
then:
- if:
condition:
lambda: return tab == id(tabview_tab_1);
then:
- logger.log: "Dog tab is now showing"
tabs:
- name: Dog
id: tabview_tab_1
border_width: 2
border_color: 0xff0000
width: 100%
pad_all: 8
layout:
type: grid
grid_row_align: end
grid_rows: [25px, fr(1), content]
grid_columns: [40, fr(1), fr(1)]
widgets:
- image:
grid_cell_row_pos: 0
grid_cell_column_pos: 0
src: dog_image
on_click:
then:
- lvgl.tabview.select:
id: tabview_id
index: 1
animated: true
- label:
styles: bdr_style
grid_cell_x_align: center
grid_cell_y_align: stretch
grid_cell_row_pos: 0
grid_cell_column_pos: 1
grid_cell_column_span: 1
text: "Grid cell 0/1"
- label:
grid_cell_x_align: end
styles: bdr_style
grid_cell_row_pos: 1
grid_cell_column_pos: 0
text: "Grid cell 1/0"
- label:
styles: bdr_style
grid_cell_row_pos: 1
grid_cell_column_pos: 1
text: "Grid cell 1/1"
- label:
id: cell_1_3
styles: bdr_style
grid_cell_row_pos: 1
grid_cell_column_pos: 2
text: "Grid cell 1/2"
- name: Cat
id: tabview_tab_2
widgets:
- image:
src: cat_image
on_click:
then:
- logger.log: Cat image clicked
- lvgl.tabview.select:
id: tabview_id
index: 0
animated: true
font: font:
- file: "gfonts://Roboto" - file: "gfonts://Roboto"
id: space16 id: space16
@ -230,7 +452,7 @@ image:
- id: cat_image - id: cat_image
resize: 256x48 resize: 256x48
file: $component_dir/logo-text.svg file: $component_dir/logo-text.svg
- id: dog_img - id: dog_image
file: $component_dir/logo-text.svg file: $component_dir/logo-text.svg
resize: 256x48 resize: 256x48
type: TRANSPARENT_BINARY type: TRANSPARENT_BINARY

Binary file not shown.

Binary file not shown.

View File

@ -426,3 +426,9 @@ valve:
} else { } else {
return VALVE_CLOSED; return VALVE_CLOSED;
} }
alarm_control_panel:
- platform: template
name: Alarm Control Panel
binary_sensors:
- input: some_binary_sensor

View 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));

View 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));

View 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

View File

@ -0,0 +1,4 @@
<<: !include common-esp32.yaml
http_request:
verify_ssl: false

View File

@ -0,0 +1,4 @@
<<: !include common-esp32.yaml
http_request: