bogdan.sv
(Bohdan)
December 7, 2025, 9:52pm
1
Here’s my config, it works. Can you suggest how to rework the focus between buttons using native LVGL methods instead of manual scripts?
esphome:
name: lilka
friendly_name: lilka
on_boot:
priority: 600
then:
- output.turn_on: display_power
- delay: 100ms
esp32:
board: esp32-s3-devkitc-1
framework:
type: esp-idf
version: recommended
sdkconfig_options:
CONFIG_ESP32S3_DATA_CACHE_64KB: y
CONFIG_ESP32S3_DATA_CACHE_LINE_64B: y
CONFIG_SPIRAM_MODE_OCT: y
CONFIG_SPIRAM: y
psram:
mode: octal
speed: 80MHz
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
power_save_mode: none
ap:
ssid: "Lilka Fallback Hotspot"
password: "hnYImYM4O6I0"
captive_portal:
web_server:
port: 80
api:
encryption:
key: "Yw3yq8lxsaze+jdggjAwxKpV4GD6gNZptvw4d6OPfOc="
logger:
level: INFO
logs:
lvgl: WARN
ota:
- platform: esphome
password: "bb198feb99159fdaefc8092f10039acd"
output:
- platform: gpio
pin: 46
id: display_power
inverted: false
spi:
clk_pin: 18
mosi_pin: 17
display:
- platform: mipi_spi
model: ST7789V
id: my_display
dc_pin: 15
cs_pin: 7
update_interval: 2s
rotation: 270
invert_colors: true
color_order: BGR
pixel_mode: 16bit
dimensions:
width: 280
height: 240
offset_width: 20
offset_height: 0
auto_clear_enabled: false
lvgl:
log_level: WARN
buffer_size: 25%
style_definitions:
- id: style_btn_off
bg_color: 0x808080
text_color: 0x000000
border_width: 0
radius: 6
- id: style_btn_on
bg_color: 0x00FF00
text_color: 0x000000
border_width: 0
radius: 6
- id: style_focused
border_color: 0xFFFFFF
border_width: 3
pages:
- id: main_page
bg_color: 0x000000
widgets:
- obj:
align: CENTER
width: 260
height: 220
bg_opa: TRANSP
layout:
type: FLEX
flex_flow: COLUMN
flex_align_main: CENTER
flex_align_cross: CENTER
pad_row: 10
widgets:
- button:
id: vana_button
width: 200
height: 50
checkable: true
styles: style_btn_off
widgets:
- label:
text: "Vana"
align: CENTER
on_click:
- homeassistant.action:
action: light.toggle
data:
entity_id: light.tz3000_qewo8dlz_ts0013_light
- button:
id: komora_button
width: 200
height: 50
checkable: true
styles: style_btn_off
widgets:
- label:
text: "Komora"
align: CENTER
on_click:
- homeassistant.action:
action: light.toggle
data:
entity_id: light.tz3000_qewo8dlz_ts0013_light_2
- button:
id: kitchen_button
width: 200
height: 50
checkable: true
styles: style_btn_off
widgets:
- label:
text: "Kitchen"
align: CENTER
on_click:
- homeassistant.action:
action: light.toggle
data:
entity_id: light.tz3000_qewo8dlz_ts0013_light_3
globals:
- id: current_button_index
type: int
restore_value: no
initial_value: '0'
script:
- id: update_focus
then:
- lvgl.widget.update:
id: vana_button
border_width: !lambda 'return id(current_button_index) == 0 ? 3 : 0;'
- lvgl.widget.update:
id: komora_button
border_width: !lambda 'return id(current_button_index) == 1 ? 3 : 0;'
- lvgl.widget.update:
id: kitchen_button
border_width: !lambda 'return id(current_button_index) == 2 ? 3 : 0;'
binary_sensor:
# Home Assistant світла
- platform: homeassistant
id: vana_light
entity_id: light.tz3000_qewo8dlz_ts0013_light
internal: true
on_state:
then:
- lvgl.widget.update:
id: vana_button
state:
checked: !lambda 'return id(vana_light).state;'
- platform: homeassistant
id: komora_light
entity_id: light.tz3000_qewo8dlz_ts0013_light_2
internal: true
on_state:
then:
- lvgl.widget.update:
id: komora_button
state:
checked: !lambda 'return id(komora_light).state;'
- platform: homeassistant
id: kitchen_light
entity_id: light.tz3000_qewo8dlz_ts0013_light_3
internal: true
on_state:
then:
- lvgl.widget.update:
id: kitchen_button
state:
checked: !lambda 'return id(kitchen_light).state;'
# Фізичні кнопки
- platform: gpio
pin:
number: 38
mode: INPUT_PULLUP
inverted: true
id: button_up
on_press:
- lambda: |-
id(current_button_index)--;
if (id(current_button_index) < 0) id(current_button_index) = 2;
- script.execute: update_focus
- platform: gpio
pin:
number: 41
mode: INPUT_PULLUP
inverted: true
id: button_down
on_press:
- lambda: |-
id(current_button_index)++;
if (id(current_button_index) > 2) id(current_button_index) = 0;
- script.execute: update_focus
- platform: gpio
pin:
number: 0
mode: INPUT_PULLUP
inverted: true
id: button_select
on_press:
then:
- homeassistant.action:
action: light.toggle
data:
entity_id: !lambda |-
if (id(current_button_index) == 0) return "light.tz3000_qewo8dlz_ts0013_light";
if (id(current_button_index) == 1) return "light.tz3000_qewo8dlz_ts0013_light_2";
return "light.tz3000_qewo8dlz_ts0013_light_3";
You would use an encoder - see the docs especially the tips following.
bogdan.sv
(Bohdan)
December 8, 2025, 9:04pm
3
Yes, I looked through it, but I don’t understand whether I can optimize my code without increasing the number of lines. I want the configuration to be clear. Any advice?
Yes, set up the encoder and remove all your custom focus handling.
1 Like
bogdan.sv
(Bohdan)
December 9, 2025, 5:34am
5
The issue is that I don’t have a hardware rotary encoder with A/B signals. I’m using three separate physical buttons (up, down, select). When I try to connect them via lvgl.encoders, ESPHome reports an error saying that the sensor parameter is required, and buttons can’t be used as an encoder.
So I can’t simply “set up the encoder” — ESPHome doesn’t allow emulating an encoder using buttons. If you have an example of how to properly connect three buttons as an input device for LVGL without using an encoder, I would really appreciate it.
esphome:
name: lilka
friendly_name: lilka
on_boot:
priority: 600
then:
- output.turn_on: display_power
- delay: 100ms
esp32:
board: esp32-s3-devkitc-1
framework:
type: esp-idf
version: recommended
sdkconfig_options:
CONFIG_ESP32S3_DATA_CACHE_64KB: y
CONFIG_ESP32S3_DATA_CACHE_LINE_64B: y
CONFIG_SPIRAM_MODE_OCT: y
CONFIG_SPIRAM: y
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
power_save_mode: none
ap:
ssid: "Lilka Fallback Hotspot"
password: "hnYImYM4O6I0"
captive_portal:
web_server:
port: 80
api:
encryption:
key: "Yw3yq8lxsaze+jdggjAwxKpV4GD6gNZptvw4d6OPfOc="
logger:
level: INFO
logs:
lvgl: WARN
ota:
- platform: esphome
password: "bb198feb99159fdaefc8092f10039acd"
output:
- platform: gpio
pin: 46
id: display_power
inverted: false
spi:
clk_pin: 18
mosi_pin: 17
display:
- platform: mipi_spi
model: ST7789V
id: my_display
dc_pin: 15
cs_pin: 7
update_interval: 2s
rotation: 270
invert_colors: true
color_order: BGR
pixel_mode: 16bit
dimensions:
width: 280
height: 240
offset_width: 20
offset_height: 0
auto_clear_enabled: false
lvgl:
log_level: WARN
buffer_size: 25%
encoders:
- group: main_group
enter_button: button_select
left_button: button_up
right_button: button_down
initial_focus: bathroom_button
style_definitions:
- id: style_btn_off
bg_color: 0x808080
text_color: 0x000000
border_width: 0
radius: 6
- id: style_btn_on
bg_color: 0x00FF00
text_color: 0x000000
border_width: 0
radius: 6
- id: style_focused
border_color: 0xFFFFFF
border_width: 3
pages:
- id: main_page
bg_color: 0x000000
widgets:
- obj:
align: CENTER
width: 260
height: 220
bg_opa: TRANSP
layout:
type: FLEX
flex_flow: COLUMN
flex_align_main: CENTER
flex_align_cross: CENTER
pad_row: 10
widgets:
- button:
id: bathroom_button
group: main_group
width: 200
height: 50
checkable: true
styles: [style_btn_off, style_focused]
widgets:
- label:
text: "Bathroom"
align: CENTER
on_click:
- homeassistant.action:
action: light.toggle
data:
entity_id: light.tz3000_qewo8dlz_ts0013_light
- button:
id: pantry_button
group: main_group
width: 200
height: 50
checkable: true
styles: [style_btn_off, style_focused]
widgets:
- label:
text: "Pantry"
align: CENTER
on_click:
- homeassistant.action:
action: light.toggle
data:
entity_id: light.tz3000_qewo8dlz_ts0013_light_2
- button:
id: kitchen_button
group: main_group
width: 200
height: 50
checkable: true
styles: [style_btn_off, style_focused]
widgets:
- label:
text: "Kitchen"
align: CENTER
on_click:
- homeassistant.action:
action: light.toggle
data:
entity_id: light.tz3000_qewo8dlz_ts0013_light_3
binary_sensor:
- platform: homeassistant
id: bathroom_light
entity_id: light.tz3000_qewo8dlz_ts0013_light
internal: true
on_state:
then:
- lvgl.widget.update:
id: bathroom_button
state:
checked: !lambda 'return id(bathroom_light).state;'
- platform: homeassistant
id: pantry_light
entity_id: light.tz3000_qewo8dlz_ts0013_light_2
internal: true
on_state:
then:
- lvgl.widget.update:
id: pantry_button
state:
checked: !lambda 'return id(pantry_light).state;'
- platform: homeassistant
id: kitchen_light
entity_id: light.tz3000_qewo8dlz_ts0013_light_3
internal: true
on_state:
then:
- lvgl.widget.update:
id: kitchen_button
state:
checked: !lambda 'return id(kitchen_light).state;'
- platform: gpio
pin:
number: 38
mode: INPUT_PULLUP
inverted: true
id: button_up
- platform: gpio
pin:
number: 41
mode: INPUT_PULLUP
inverted: true
id: button_down
- platform: gpio
pin:
number: 40
mode: INPUT_PULLUP
inverted: true
id: button_select
Failed config
lvgl: [source /config/lilka.yaml:76]
log_level: WARN
buffer_size: 25%
encoders:
'sensor' is a required option for [encoders].
- group: main_group
enter_button: button_select
[left_button] is an invalid option for [encoders]. Did you mean [enter_button]?
left_button: button_up
[right_button] is an invalid option for [encoders]. Did you mean [enter_button]?
right_button: button_down
initial_focus: bathroom_button
style_definitions:
encoders:
- enter_button: enter_key
sensor:
left_button: left_key
right_button: right_key
1 Like
bogdan.sv
(Bohdan)
December 9, 2025, 7:08am
7
Now my configuration with the encoder compiles. Thank you very much. Could you help me fix another issue? All the buttons on the screen are focused (surrounded by a white frame), and pressing the up or down buttons does nothing.
esphome:
name: lilka
friendly_name: lilka
on_boot:
priority: 600
then:
- output.turn_on: display_power
- delay: 100ms
esp32:
board: esp32-s3-devkitc-1
framework:
type: esp-idf
version: recommended
sdkconfig_options:
CONFIG_ESP32S3_DATA_CACHE_64KB: y
CONFIG_ESP32S3_DATA_CACHE_LINE_64B: y
CONFIG_SPIRAM_MODE_OCT: y
CONFIG_SPIRAM: y
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
power_save_mode: none
ap:
ssid: "Lilka Fallback Hotspot"
password: "hnYImYM4O6I0"
captive_portal:
web_server:
port: 80
api:
encryption:
key: "Yw3yq8lxsaze+jdggjAwxKpV4GD6gNZptvw4d6OPfOc="
logger:
level: INFO
logs:
lvgl: WARN
ota:
- platform: esphome
password: "bb198feb99159fdaefc8092f10039acd"
output:
- platform: gpio
pin: 46
id: display_power
inverted: false
spi:
clk_pin: 18
mosi_pin: 17
display:
- platform: mipi_spi
model: ST7789V
id: my_display
dc_pin: 15
cs_pin: 7
update_interval: 2s
rotation: 270
invert_colors: true
color_order: BGR
pixel_mode: 16bit
dimensions:
width: 280
height: 240
offset_width: 20
offset_height: 0
auto_clear_enabled: false
lvgl:
log_level: WARN
buffer_size: 25%
encoders:
- enter_button: enter_key
sensor:
left_button: left_key
right_button: right_key
default_group: main_group
style_definitions:
- id: style_btn_off
bg_color: 0x808080
text_color: 0x000000
border_width: 0
radius: 6
- id: style_btn_on
bg_color: 0x00FF00
text_color: 0x000000
border_width: 0
radius: 6
- id: style_focused
border_color: 0xFFFFFF
border_width: 3
pages:
- id: main_page
bg_color: 0x000000
widgets:
- obj:
align: CENTER
width: 260
height: 220
bg_opa: TRANSP
layout:
type: FLEX
flex_flow: COLUMN
flex_align_main: CENTER
flex_align_cross: CENTER
pad_row: 10
widgets:
- button:
id: bathroom_button
group: main_group
width: 200
height: 50
checkable: true
styles: [style_btn_off, style_focused]
widgets:
- label:
text: "Bathroom"
align: CENTER
on_click:
- homeassistant.action:
action: light.toggle
data:
entity_id: light.tz3000_qewo8dlz_ts0013_light
- button:
id: pantry_button
group: main_group
width: 200
height: 50
checkable: true
styles: [style_btn_off, style_focused]
widgets:
- label:
text: "Pantry"
align: CENTER
on_click:
- homeassistant.action:
action: light.toggle
data:
entity_id: light.tz3000_qewo8dlz_ts0013_light_2
- button:
id: kitchen_button
group: main_group
width: 200
height: 50
checkable: true
styles: [style_btn_off, style_focused]
widgets:
- label:
text: "Kitchen"
align: CENTER
on_click:
- homeassistant.action:
action: light.toggle
data:
entity_id: light.tz3000_qewo8dlz_ts0013_light_3
binary_sensor:
- platform: homeassistant
id: bathroom_light
entity_id: light.tz3000_qewo8dlz_ts0013_light
internal: true
on_state:
then:
- lvgl.widget.update:
id: bathroom_button
state:
checked: !lambda 'return id(bathroom_light).state;'
- platform: homeassistant
id: pantry_light
entity_id: light.tz3000_qewo8dlz_ts0013_light_2
internal: true
on_state:
then:
- lvgl.widget.update:
id: pantry_button
state:
checked: !lambda 'return id(pantry_light).state;'
- platform: homeassistant
id: kitchen_light
entity_id: light.tz3000_qewo8dlz_ts0013_light_3
internal: true
on_state:
then:
- lvgl.widget.update:
id: kitchen_button
state:
checked: !lambda 'return id(kitchen_light).state;'
- platform: gpio
pin:
number: 38
mode: INPUT_PULLUP
inverted: true
id: left_key
- platform: gpio
pin:
number: 41
mode: INPUT_PULLUP
inverted: true
id: right_key
- platform: gpio
pin:
number: 0
mode: INPUT_PULLUP
inverted: true
id: enter_key
You specified a default group but didn’t assign it to the encoder. Unless you need multiple groups, just remove all the group information and leave all the widgets and the encoder in the default group.
You don’t have any styling for the focused button, so you won’t see focus changes (you applied the “focused” style to all buttons unconditionally.)
Unrelated, but you don’t need those sdkconfig_options.
lvgl:
encoders:
- enter_button: enter_key
id: encoder_id
sensor:
left_button: left_key
right_button: right_key
theme:
label:
align: center
text_align: center
button:
width: 100%
bg_color: green
text_color: black
border_width: 2
border_opa: transp
radius: 6
align: center
focus_key:
border_color: white
border_opa: cover
pages:
- id: main_page
bg_color: black
widgets:
- container:
width: 80%
align: CENTER
bg_opa: TRANSP
pad_all: 10
layout:
type: FLEX
flex_flow: COLUMN
flex_align_main: CENTER
flex_align_track: CENTER
flex_align_cross: CENTER
pad_row: 10
widgets:
- button:
id: bathroom_button
checkable: true
widgets:
label:
text: "Bathroom"
on_click:
- homeassistant.action:
action: light.toggle
data:
entity_id: light.tz3000_qewo8dlz_ts0013_light
- button:
id: pantry_button
checkable: true
widgets:
label:
text: "Pantry"
on_click:
- homeassistant.action:
action: light.toggle
data:
entity_id: light.tz3000_qewo8dlz_ts0013_light_2
- button:
id: kitchen_button
checkable: true
widgets:
label:
text: "Kitchen"
on_click:
- homeassistant.action:
action: light.toggle
data:
entity_id: light.tz3000_qewo8dlz_ts0013_light_3
1 Like
bogdan.sv
(Bohdan)
December 11, 2025, 5:57pm
9
It works! Thank you for your help! https://youtu.be/m2ZnhPG_efg
esphome:
name: lilka
friendly_name: lilka
on_boot:
priority: 600
then:
- output.turn_on: display_power
- delay: 100ms
esp32:
board: esp32-s3-devkitc-1
framework:
type: esp-idf
version: recommended
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
power_save_mode: none
ap:
ssid: "Lilka Fallback Hotspot"
password: "hnYImYM4O6I0"
captive_portal:
web_server:
api:
encryption:
key: "Yw3yq8lxsaze+jdggjAwxKpV4GD6gNZptvw4d6OPfOc="
logger:
level: INFO
logs:
lvgl: WARN
ota:
- platform: esphome
password: "bb198feb99159fdaefc8092f10039acd"
output:
- platform: gpio
pin: 46
id: display_power
spi:
clk_pin: 18
mosi_pin: 17
display:
- platform: mipi_spi
model: ST7789V
id: my_display
dc_pin: 15
cs_pin: 7
update_interval: 2s
rotation: 270
invert_colors: true
color_order: BGR
pixel_mode: 16bit
dimensions:
width: 280
height: 240
offset_width: 20
auto_clear_enabled: false
lvgl:
encoders:
- enter_button: enter_key
id: encoder_id
sensor:
left_button: left_key
right_button: right_key
theme:
label:
align: center
text_align: center
text_font: MONTSERRAT_24
button:
width: 80%
bg_color: grey
text_color: black
border_width: 2
border_opa: transp
radius: 6
focus_key:
border_color: white
border_opa: cover
pages:
- id: main_page
bg_color: black
pad_all: 20
layout:
type: FLEX
flex_flow: COLUMN
flex_align_main: SPACE_EVENLY
flex_align_cross: CENTER
widgets:
- button:
id: bathroom_button
checkable: true
widgets:
- label:
text: "Bathroom"
on_click:
- homeassistant.action:
action: light.toggle
data:
entity_id: light.tz3000_qewo8dlz_ts0013_light
- button:
id: pantry_button
checkable: true
widgets:
- label:
text: "Pantry"
on_click:
- homeassistant.action:
action: light.toggle
data:
entity_id: light.tz3000_qewo8dlz_ts0013_light_2
- button:
id: kitchen_button
checkable: true
widgets:
- label:
text: "Kitchen"
on_click:
- homeassistant.action:
action: light.toggle
data:
entity_id: light.tz3000_qewo8dlz_ts0013_light_3
binary_sensor:
- platform: homeassistant
id: bathroom_light
entity_id: light.tz3000_qewo8dlz_ts0013_light
internal: true
on_state:
- lvgl.widget.update:
id: bathroom_button
state:
checked: !lambda 'return id(bathroom_light).state;'
- platform: homeassistant
id: pantry_light
entity_id: light.tz3000_qewo8dlz_ts0013_light_2
internal: true
on_state:
- lvgl.widget.update:
id: pantry_button
state:
checked: !lambda 'return id(pantry_light).state;'
- platform: homeassistant
id: kitchen_light
entity_id: light.tz3000_qewo8dlz_ts0013_light_3
internal: true
on_state:
- lvgl.widget.update:
id: kitchen_button
state:
checked: !lambda 'return id(kitchen_light).state;'
- platform: gpio
pin:
number: 38
mode: INPUT_PULLUP
inverted: true
id: left_key
- platform: gpio
pin:
number: 41
mode: INPUT_PULLUP
inverted: true
id: right_key
- platform: gpio
pin:
number: 0
mode: INPUT_PULLUP
inverted: true
id: enter_key
1 Like