ESPHOME LVGL
ESPHome LVGL is “simply” another pre-defined component in ESPHome. Getting started is trivial, like creating any new ESPHome device. Only a single YAML configuration file defines everything, from the display and touchscreen use, the GUI design, to the HA sensor and devices that are monitored and controlled. If you are already familiar with ESPHome, the principle is very familiar.
But man, is it ever verbose!
esphome:
name: breezeway-plate
friendly_name: Breezeway Plate
esp32:
board: esp32dev
framework:
type: arduino
# Enable logging
logger:
# Enable Home Assistant API
api:
encryption:
key: *redacted*
on_client_connected:
lvgl.widget.hide: boot_screen
ota:
- platform: esphome
password: *redacted*
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Breezeway-Plate Fallback Hotspot"
password: *redacted*
captive_portal:
psram:
mode: octal
speed: 80MHz
output:
- platform: ledc
pin: GPIO5
id: backlight_pwm
- platform: ledc
pin: GPIO26
id: moodRed
- platform: ledc
pin: GPIO32
id: moodGreen
- platform: ledc
pin: GPIO33
id: moodBlue
- platform: gpio
pin: GPIO12
id: relay_1
- platform: gpio
pin: GPIO14
id: relay_2
- platform: gpio
pin: GPIO27
id: relay_3
light:
- platform: rgb
name: "Mood Light"
red: moodRed
green: moodGreen
blue: moodBlue
- platform: monochromatic
name: "Backlight"
id: backlight
output: backlight_pwm
restore_mode: ALWAYS_ON
spi:
clk_pin: GPIO19
mosi_pin: GPIO23
miso_pin: GPIO25
i2c:
sda: GPIO4
scl: GPIO0
display:
- id: langbon_L8
platform: ili9xxx
model: ST7789V
invert_colors: false
dimensions: 240x320
cs_pin: GPIO22
dc_pin: GPIO21
reset_pin: GPIO18
auto_clear_enabled: false
update_interval: never
rotation: 180
touchscreen:
platform: ft63x6
calibration:
x_min: 0
y_min: 0
x_max: 230
y_max: 312
on_touch:
- if:
condition: lvgl.is_paused
then:
- logger.log: "LVGL resuming"
- lvgl.resume:
- lvgl.page.show: main_page
- lvgl.widget.redraw
- light.turn_on: backlight
font:
- file: "fonts/RobotoCondensed-Regular.ttf"
id: roboto_icons_28
size: 28
bpp: 4
extras:
- file: "fonts/materialdesignicons-webfont.ttf"
glyphs: [
"\U000F02DC", # mdi-home
"\U000F0335", # mdi-lightbulb
"\U000F0425", # mdi-power
"\U000F0493", # mdi-cog
"\U000F091D", # mdi-wall-sconce-flat
"\U000F12BA", # mdi-string-lights
"\U000F1849", # mdi-wallterfall
"\U000F179B", # mdi-light-recessed
]
- file: "fonts/RobotoCondensed-Regular.ttf"
id: header_font
size: 12
bpp: 4
image:
- file: https://esphome.io/_static/favicon-512x512.png
id: boot_logo
resize: 200x200
type: RGB565
use_transparency: true
time:
- platform: homeassistant
id: time_comp
on_time_sync:
- script.execute: time_update
on_time:
- minutes: '*'
seconds: 0
then:
- script.execute: time_update
script:
- id: time_update
then:
- lvgl.label.update:
id: current_time
text: !lambda |-
static char hhss[8];
auto now = id(time_comp).now();
snprintf(hhss, sizeof(hhss), "%02d:%02d", now.hour, now.minute);
return hhss;
binary_sensor:
- platform: homeassistant
id: gate_sconces_state
entity_id: light.gate_sconces
on_state:
- lvgl.label.update:
id: gate_sconces_button_label
text_color: !lambda |-
return (x) ? lv_color_hex(0xffd700) : lv_color_white();
- lvgl.label.update:
id: gate_sconces_panel_label
text_color: !lambda |-
return (x) ? lv_color_hex(0xffd700) : lv_color_white();
- lvgl.button.update:
id: gate_sconces_panel_button
state:
checked: !lambda return x;
- platform: homeassistant
id: eaves_patio_state
entity_id: light.eaves_downlights_patio
on_state:
- lvgl.label.update:
id: eaves_patio_button_label
text_color: !lambda |-
return (x) ? lv_color_hex(0xffd700) : lv_color_white();
- lvgl.label.update:
id: eaves_patio_panel_label
text_color: !lambda |-
return (x) ? lv_color_hex(0xffd700) : lv_color_white();
- lvgl.button.update:
id: eaves_patio_panel_button
state:
checked: !lambda return x;
- platform: homeassistant
id: party_state
entity_id: light.patio_party_lights
on_state:
- lvgl.label.update:
id: party_button_label
text_color: !lambda |-
return (x) ? lv_color_hex(0xffd700) : lv_color_white();
- lvgl.label.update:
id: party_panel_label
text_color: !lambda |-
return (x) ? lv_color_hex(0xffd700) : lv_color_white();
- lvgl.button.update:
id: party_panel_button
state:
checked: !lambda return x;
sensor:
- platform: homeassistant
id: current_temp
entity_id: sensor.weatherflow_temperature
on_value:
- lvgl.label.update:
id: thermometer
text:
format: "%.1f°F"
args: [ 'x' ]
- platform: homeassistant
id: gate_sconces_brightness
entity_id: light.gate_sconces
attribute: brightness
on_value:
- lvgl.slider.update:
id: gate_sconces_panel_slider
value: !lambda return x;
- platform: homeassistant
id: eaves_patio_brightness
entity_id: light.eaves_downlights_patio
attribute: brightness
on_value:
- lvgl.slider.update:
id: eaves_patio_panel_slider
value: !lambda return x;
- platform: homeassistant
id: party_brightness
entity_id: light.patio_party_lights
attribute: brightness
on_value:
- lvgl.slider.update:
id: party_panel_slider
value: !lambda return x;
lvgl:
on_idle:
timeout: !lambda "return 10000;" # Turn off back light if idle for 10 secs
then:
- logger.log: "LVGL is idle"
- light.turn_off: backlight
- lvgl.pause:
show_snow: true # Prevent burn-in
bg_color: black
theme:
label:
bg_opa: TRANSP
align: CENTER
text_align: CENTER
text_color: 0xFFFFFF
text_font: roboto_icons_28
border_width: 0
outline_width: 0
pad_all: 0
button:
bg_color: dodgerblue
radius: 4
border_width: 0
outline_width: 0
pad_all: 0
checked:
bg_color: gold
slider:
bg_color: dodgerblue
bg_opa: 50%
indicator:
bg_color: dodgerblue
knob:
bg_color: dodgerblue
obj:
bg_opa: TRANSP
border_width: 0
outline_width: 0
pad_all: 0
top_layer: # Show the current time and temperature at the top of every page
widgets:
- obj:
id: header
width: 100%
height: 20
bg_opa: TRANSP
align: TOP_MID
border_width: 0
outline_width: 0
widgets:
- label:
id: current_time
text_font: header_font
text: "00:00"
align: TOP_LEFT
text_align: LEFT
- label:
id: thermometer
text_font: header_font
text: "00.0°F"
align: TOP_RIGHT
text_align: RIGHT
- obj:
id: boot_screen
x: 0
y: 0
width: 100%
height: 100%
bg_color: 0xffffff
bg_opa: COVER
radius: 0
pad_all: 0
border_width: 0
widgets:
- image:
align: CENTER
src: boot_logo
y: -40
- spinner:
align: CENTER
y: 95
height: 50
width: 50
spin_time: 1s
arc_length: 60deg
arc_width: 8
indicator:
arc_color: 0x18bcf2
arc_width: 8
on_press:
- lvgl.widget.hide: boot_screen
pages:
- id: main_page
widgets:
- obj: # a properly placed container object for all these controls
align: BOTTOM_MID
width: 240
height: 300
bg_opa: TRANSP
border_opa: TRANSP
pad_all: 6
layout: # enable the GRID layout for the children widgets
type: GRID # split the rows and the columns proportionally
grid_columns: [FR(1), FR(1)] # equal
grid_rows: [FR(1), FR(1), FR(1)]
widgets:
- button:
id: button_11
grid_cell_column_pos: 0
grid_cell_row_pos: 0
grid_cell_x_align: STRETCH
grid_cell_y_align: STRETCH
on_short_click:
- homeassistant.action:
action: light.toggle
data:
entity_id: light.breezeway_sconce
widgets:
- label:
id: breezeway_sconce_label
text: "\U000F0335\nB'way"
- button:
id: button_12
grid_cell_column_pos: 1
grid_cell_row_pos: 0
grid_cell_x_align: STRETCH
grid_cell_y_align: STRETCH
on_short_click:
- homeassistant.action:
action: light.toggle
data:
entity_id: light.gate_sconces
on_long_press:
- lvgl.page.show: gate_dimmer_page
widgets:
- label:
id: gate_sconces_button_label
text: "\U000F0335\nGate"
- label:
align: TOP_RIGHT
x: -2
y: +2
text_font: montserrat_18
text: "\uF013"
- button:
id: button_21
grid_cell_column_pos: 0
grid_cell_row_pos: 1
grid_cell_x_align: STRETCH
grid_cell_y_align: STRETCH
on_short_click:
- homeassistant.action:
action: light.toggle
data:
entity_id: light.eaves_downlights_patio
on_long_press:
- lvgl.page.show: eaves_dimmer_page
widgets:
- label:
id: eaves_patio_button_label
text: "\U000F179B\nPatio"
- label:
align: TOP_RIGHT
x: -2
y: +2
text_font: montserrat_18
text: "\uF013"
- button:
id: button_22
grid_cell_column_pos: 1
grid_cell_row_pos: 1
grid_cell_x_align: STRETCH
grid_cell_y_align: STRETCH
on_short_click:
- homeassistant.action:
action: light.toggle
data:
entity_id: light.retaining_wall_lights
widgets:
- label:
text: "\U000F091D\nWall"
- button:
id: button_31
grid_cell_column_pos: 0
grid_cell_row_pos: 2
grid_cell_x_align: STRETCH
grid_cell_y_align: STRETCH
on_short_click:
- homeassistant.action:
action: light.toggle
data:
entity_id: light.patio_party_lights
on_long_press:
- lvgl.page.show: party_dimmer_page
widgets:
- label:
id: party_button_label
text: "\U000F12BA\nParty"
- label:
align: TOP_RIGHT
x: -2
y: +2
text_font: montserrat_18
text: "\uF013"
- button:
id: button_32
grid_cell_column_pos: 1
grid_cell_row_pos: 2
grid_cell_x_align: STRETCH
grid_cell_y_align: STRETCH
on_short_click:
- homeassistant.action:
action: light.toggle
data:
entity_id: light.water_wall_pump
widgets:
- label:
text: "\U000F1849\nW'Wall"
- id: gate_dimmer_page
widgets:
- obj: # a properly placed container object for all these controls
align: BOTTOM_MID
width: 240
height: 300
bg_opa: TRANSP
border_opa: TRANSP
pad_all: 10
layout: # enable the GRID layout for the children widgets
type: GRID # split the rows and the columns proportionally
grid_columns: [FR(1), FR(1)] # equal
grid_rows: [FR(135), FR(135), FR(30)]
widgets:
- label:
id: gate_sconces_panel_label
grid_cell_column_pos: 0
grid_cell_row_pos: 0
grid_cell_x_align: STRETCH
grid_cell_y_align: STRETCH
pad_top: 30
text: "\U000F0335\nGate"
- button:
id: gate_sconces_panel_button
checkable: true
grid_cell_column_pos: 0
grid_cell_row_pos: 1
grid_cell_x_align: STRETCH
grid_cell_y_align: STRETCH
widgets:
- label:
text_font: montserrat_32
text: "\uF011"
on_click:
- homeassistant.action:
action: light.toggle
data:
entity_id: light.gate_sconces
- obj:
grid_cell_column_pos: 1
grid_cell_row_pos: 0
grid_cell_row_span: 2
grid_cell_x_align: STRETCH
grid_cell_y_align: STRETCH
pad_top: 10
pad_bottom: 20
pad_left: 40
pad_right: 40
widgets:
- slider:
id: gate_sconces_panel_slider
width: 50%
height: 100%
align: CENTER
max_value: 255
on_release:
- homeassistant.action:
action: light.turn_on
data:
entity_id: light.gate_sconces
brightness: !lambda return int(x);
- button:
grid_cell_column_pos: 0
grid_cell_row_pos: 2
grid_cell_column_span: 2
grid_cell_x_align: STRETCH
grid_cell_y_align: STRETCH
on_click:
- lvgl.page.show: main_page
widgets:
- label:
text: "\U000F02DC"
- id: eaves_dimmer_page
widgets:
- obj: # a properly placed container object for all these controls
align: BOTTOM_MID
width: 240
height: 300
bg_opa: TRANSP
border_opa: TRANSP
pad_all: 10
layout: # enable the GRID layout for the children widgets
type: GRID # split the rows and the columns proportionally
grid_columns: [FR(1), FR(1)] # equal
grid_rows: [FR(135), FR(135), FR(30)]
widgets:
- label:
id: eaves_patio_panel_label
grid_cell_column_pos: 0
grid_cell_row_pos: 0
grid_cell_x_align: STRETCH
grid_cell_y_align: STRETCH
pad_top: 30
text: "\U000F179B\nPatio"
- button:
id: eaves_patio_panel_button
checkable: true
grid_cell_column_pos: 0
grid_cell_row_pos: 1
grid_cell_x_align: STRETCH
grid_cell_y_align: STRETCH
widgets:
- label:
text_font: montserrat_32
text: "\uF011"
on_click:
- homeassistant.action:
action: light.toggle
data:
entity_id: light.eaves_downlights_patio
- obj:
grid_cell_column_pos: 1
grid_cell_row_pos: 0
grid_cell_row_span: 2
grid_cell_x_align: STRETCH
grid_cell_y_align: STRETCH
pad_top: 10
pad_bottom: 20
pad_left: 40
pad_right: 40
widgets:
- slider:
id: eaves_patio_panel_slider
width: 50%
height: 100%
align: CENTER
max_value: 255
on_release:
- homeassistant.action:
action: light.turn_on
data:
entity_id: light.eaves_downlights_patio
brightness: !lambda return int(x);
- button:
grid_cell_column_pos: 0
grid_cell_row_pos: 2
grid_cell_column_span: 2
grid_cell_x_align: STRETCH
grid_cell_y_align: STRETCH
on_click:
- lvgl.page.show: main_page
widgets:
- label:
text: "\U000F02DC"
- id: party_dimmer_page
widgets:
- obj: # a properly placed container object for all these controls
align: BOTTOM_MID
width: 240
height: 300
bg_opa: TRANSP
border_opa: TRANSP
pad_all: 10
layout: # enable the GRID layout for the children widgets
type: GRID # split the rows and the columns proportionally
grid_columns: [FR(1), FR(1)] # equal
grid_rows: [FR(135), FR(135), FR(30)]
widgets:
- label:
id: party_panel_label
grid_cell_column_pos: 0
grid_cell_row_pos: 0
grid_cell_x_align: STRETCH
grid_cell_y_align: STRETCH
pad_top: 30
text: "\U000F12BA\nParty"
- button:
id: party_panel_button
checkable: true
grid_cell_column_pos: 0
grid_cell_row_pos: 1
grid_cell_x_align: STRETCH
grid_cell_y_align: STRETCH
widgets:
- label:
text_font: montserrat_32
text: "\uF011"
on_click:
- homeassistant.action:
action: light.toggle
data:
entity_id: light.patio_party_lights
- obj:
grid_cell_column_pos: 1
grid_cell_row_pos: 0
grid_cell_row_span: 2
grid_cell_x_align: STRETCH
grid_cell_y_align: STRETCH
pad_top: 10
pad_bottom: 20
pad_left: 40
pad_right: 40
widgets:
- slider:
id: party_panel_slider
width: 50%
height: 100%
align: CENTER
max_value: 255
on_release:
- homeassistant.action:
action: light.turn_on
data:
entity_id: light.patio_party_lights
brightness: !lambda return int(x);
- button:
grid_cell_column_pos: 0
grid_cell_row_pos: 2
grid_cell_column_span: 2
grid_cell_x_align: STRETCH
grid_cell_y_align: STRETCH
on_click:
- lvgl.page.show: main_page
widgets:
- label:
text: "\U000F02DC"
A good portion is boilerplate declaration that defines the device itself: the display, the touchscreen, the backlight and mood light controls (thanks to @Jon_White for providing a complete solution) – stuff that is built in the openHASP you use.
Because I’m controlling six lights with the same switch plate – three of them as dimmers, there is a lot of repetitive code which I am sure will re-open the debate of integrating jinja in ESPHome YAML. I for one would have appreciated it.
You are a lot closer to LVGL so there is a lot of power and flexibility. For example, I was able to use long-press to trigger the dimmer sub-panel rather than using a sub-button like I had to do in openHASP. I ran into a few issues that were not detected by the YAML editor, and some that caused errors in the generated C++ code rather than being caught in YAML time. Being an experienced C++ programmer, it was easy for me to figure out how to fix the problem.
The main frustration is that ESPHome LVGL is lacking in creating pre-defined bindings between HA entities and LGVL widgets. You have to do everything: trigger a widget state update whenever the HA entity changes and trigger the HA entity whenever an event happens on a widget. For example, it would be nice if it were possible to bind a light entity to a button and have it handle the ON/OFF transitions back and forth on its own. Same with a slider widget and any numerical entity like a dimmer.