Install
$ agentstack add skill-narrowstacks-claude-homeassistant-plugins-esphome-lvgl ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
ESPHome LVGL Graphics Library
Generate ESPHome configurations using LVGL (Light and Versatile Graphics Library) for creating sophisticated, widget-based user interfaces on graphical displays.
Overview
LVGL is a powerful graphics library that provides pre-built widgets (buttons, sliders, labels, etc.) with built-in touch handling, layouts, and styling. It's ideal for interactive displays that need professional-looking UIs.
Key differences from native display rendering:
- Widget-based instead of pixel-by-pixel drawing
- Automatic layout management (Flex, Grid)
- Built-in touch/input handling per widget
- Theme and style system
- Direct integration with ESPHome components
Requirements:
- ESP32 or RP2040 (ESP8266 not supported)
- PSRAM recommended for large/color displays
- Display configured with
auto_clear_enabled: false - No
lambdain display configuration
Quick Start
Minimal configuration with automatic "Hello World":
display:
- platform: ili9xxx
id: my_display
# ... display config
auto_clear_enabled: false
update_interval: never
lvgl:
# Empty config shows default "Hello World" page
Basic configuration with custom content:
lvgl:
widgets:
- label:
align: CENTER
text: 'Hello World!'
Core Concepts
Widgets
Widgets are graphical elements (buttons, labels, sliders, etc.). Every widget has:
- Parent: The container it's created in (page, obj, etc.)
- Parts: Sub-elements that can be styled separately (main, indicator, knob, etc.)
- States: Visual states (default, pressed, checked, disabled, focused, etc.)
- ID: Optional identifier for referencing in automations
Pages
Pages are full-screen layouts. Only one page is active at a time:
lvgl:
pages:
- id: main_page
widgets:
- label:
text: 'Page 1'
- id: settings_page
widgets:
- label:
text: 'Settings'
Navigate between pages:
button:
- platform: gpio
on_press:
- lvgl.page.next:
animation: OVER_LEFT
time: 300ms
Layouts
Layouts automatically position widgets:
Flex: Like CSS Flexbox, arranges widgets in rows/columns
obj:
layout:
type: FLEX
flex_flow: ROW_WRAP
flex_align_main: SPACE_BETWEEN
widgets:
- button: { width: 50, height: 50 }
- button: { width: 50, height: 50 }
- button: { width: 50, height: 50 }
Grid: Like CSS Grid, arranges widgets in a 2D grid
obj:
layout:
type: GRID
grid_rows: [FR(1), FR(1), FR(1)]
grid_columns: [FR(1), FR(1)]
widgets:
- button:
grid_cell_row_pos: 0
grid_cell_column_pos: 0
Common Widgets
Label
Display text, supports multi-line and formatting:
label:
id: my_label
text: "Temperature: 25.5°C"
align: CENTER
text_color: 0xFF0000
Update dynamically:
sensor:
- platform: dht
temperature:
on_value:
- lvgl.label.update:
id: my_label
text:
format: "Temp: %.1f°C"
args: ['x']
Button
Interactive push button:
button:
id: my_button
width: 100
height: 50
checkable: true # Acts as toggle
on_click:
- homeassistant.action:
action: light.toggle
data:
entity_id: light.living_room
widgets:
- label:
text: "Toggle"
align: CENTER
States: Use pressed, checked, disabled for styling different states.
Switch
Toggle switch widget (native ESPHome component):
switch:
- platform: lvgl
widget: my_switch
name: "LVGL Switch"
lvgl:
widgets:
- switch:
id: my_switch
align: CENTER
Slider
Adjustable value slider:
slider:
id: brightness_slider
min_value: 0
max_value: 255
width: 200
on_release:
- homeassistant.action:
action: light.turn_on
data:
entity_id: light.bedroom
brightness: !lambda return int(x);
Update from sensor:
sensor:
- platform: homeassistant
entity_id: light.bedroom
attribute: brightness
on_value:
- lvgl.slider.update:
id: brightness_slider
value: !lambda return x;
Arc
Circular slider/gauge:
arc:
id: volume_arc
min_value: 0
max_value: 100
value: 50
width: 150
height: 150
on_value:
- homeassistant.action:
action: media_player.volume_set
data:
entity_id: media_player.living_room
volume_level: !lambda return x / 100.0;
Meter
Analog meter with scales and indicators:
meter:
height: 200
width: 200
scales:
- range_from: 0
range_to: 100
angle_range: 270
rotation: 135
indicators:
- line:
id: temp_needle
width: 4
color: 0xFF0000
r_mod: -10
value: 25
Spinner
Loading animation:
spinner:
align: CENTER
height: 50
width: 50
spin_time: 1s
arc_length: 60deg
Checkbox
Checkable box with label:
checkbox:
id: agree_checkbox
text: "I agree to terms"
on_value:
then:
- logger.log:
format: "Checkbox is %s"
args: ['x ? "checked" : "unchecked"']
Styling and Themes
Basic Styling
Style properties can be applied to any widget:
button:
bg_color: 0x2196F3
bg_grad_color: 0x1976D2
bg_grad_dir: VER
border_width: 2
border_color: 0x0D47A1
radius: 10
text_color: 0xFFFFFF
pressed:
bg_color: 0x1976D2
Colors
Specify colors in multiple formats:
color:
- id: my_red
hex: FF0000
lvgl:
widgets:
- label:
text_color: 0xFF0000 # Hex
# OR
text_color: springgreen # CSS name
# OR
text_color: id(my_red) # Reference
Opacity
Specify transparency:
obj:
bg_opa: COVER # Fully opaque
bg_opa: 0.5 # 50% transparent
bg_opa: 50% # 50% transparent
bg_opa: TRANSP # Fully transparent
Theme Configuration
Apply styles globally to all widgets of a type:
lvgl:
theme:
button:
bg_color: 0x2196F3
border_width: 2
pressed:
bg_color: 0x0D47A1
checked:
bg_color: 0x4CAF50
label:
text_color: 0x000000
text_font: my_font
Style Definitions
Reusable style sets:
lvgl:
style_definitions:
- id: card_style
bg_color: 0xFFFFFF
radius: 10
pad_all: 10
shadow_width: 5
shadow_color: 0x000000
shadow_opa: 30%
widgets:
- obj:
styles: card_style
# ... widget content
Layouts
Flex Layout
Automatically arrange widgets in rows or columns:
obj:
width: 240
height: 100
layout:
type: FLEX
flex_flow: ROW_WRAP
flex_align_main: SPACE_EVENLY
flex_align_cross: CENTER
pad_row: 5
pad_column: 5
widgets:
- button: { width: 70, height: 40, flex_grow: 1 }
- button: { width: 70, height: 40 }
- button: { width: 70, height: 40 }
Flex flow options:
ROW,COLUMN: Single row/columnROW_WRAP,COLUMN_WRAP: Multiple rows/columns with wrappingROW_REVERSE,COLUMN_REVERSE: Reversed order
Alignment options:
START,END,CENTERSPACE_EVENLY,SPACE_AROUND,SPACE_BETWEEN
Grid Layout
Arrange widgets in a 2D grid:
obj:
layout:
type: GRID
grid_rows: [50, 50, 50] # Fixed heights
grid_columns: [FR(1), FR(2)] # Proportional widths
pad_row: 5
pad_column: 5
widgets:
- label:
text: "Cell 0,0"
grid_cell_row_pos: 0
grid_cell_column_pos: 0
- button:
grid_cell_row_pos: 0
grid_cell_column_pos: 1
grid_cell_row_span: 2 # Spans 2 rows
Size units:
- Pixels:
50,100px - Content:
CONTENT(fits to widget size) - Free units:
FR(1),FR(2)(proportional distribution)
Input Devices
Touchscreen
Automatically used if configured:
touchscreen:
- platform: cst816
id: my_touch
lvgl:
touchscreens:
- touchscreen_id: my_touch
long_press_time: 400ms
Rotary Encoder
For navigation and value adjustment:
sensor:
- platform: rotary_encoder
id: my_encoder
pin_a: GPIO32
pin_b: GPIO33
binary_sensor:
- platform: gpio
id: encoder_button
pin: GPIO34
lvgl:
encoders:
- sensor: my_encoder
enter_button: encoder_button
group: default
Turning encoder: Focus next/previous widget Pressing encoder: Click/edit widget Long press: Exit edit mode
Keypad
Custom button-based input:
lvgl:
keypads:
- group: default
up: button_up
down: button_down
left: button_left
right: button_right
enter: button_enter
Integration with ESPHome Components
Binary Sensor
Sync LVGL switch with binary sensor:
binary_sensor:
- platform: gpio
id: motion_sensor
on_state:
- lvgl.widget.update:
id: motion_indicator
state:
checked: !lambda return x;
Sensor Values
Display sensor data:
sensor:
- platform: dht
temperature:
id: temp_sensor
on_value:
- lvgl.label.update:
id: temp_label
text:
format: "%.1f°C"
args: ['x']
Text Sensor
Display text values:
text_sensor:
- platform: homeassistant
entity_id: sensor.weather_condition
on_value:
- lvgl.label.update:
id: weather_label
text: !lambda return x.c_str();
Home Assistant Actions
Control Home Assistant from LVGL:
button:
on_click:
- homeassistant.action:
action: climate.set_temperature
data:
entity_id: climate.living_room
temperature: 22.5
Common Patterns
Temperature Dashboard
lvgl:
pages:
- id: main_page
widgets:
- obj:
align: CENTER
width: 200
height: 150
bg_color: 0xFFFFFF
radius: 10
pad_all: 15
widgets:
- label:
text: "Living Room"
align: TOP_MID
y: 5
- label:
id: temp_label
text: "--°C"
text_font: montserrat_48
align: CENTER
- label:
id: humidity_label
text: "--% RH"
align: BOTTOM_MID
y: -5
Control Panel with Grid
obj:
layout:
type: GRID
grid_rows: [FR(1), FR(1)]
grid_columns: [FR(1), FR(1)]
pad_row: 10
pad_column: 10
widgets:
- button:
grid_cell_row_pos: 0
grid_cell_column_pos: 0
widgets:
- label: { text: "Light 1", align: CENTER }
- button:
grid_cell_row_pos: 0
grid_cell_column_pos: 1
widgets:
- label: { text: "Light 2", align: CENTER }
Gauge with Indicator
meter:
height: 180
width: 180
align: CENTER
scales:
- range_from: 0
range_to: 100
angle_range: 270
rotation: 135
ticks:
count: 11
width: 2
length: 10
color: 0x808080
indicators:
- line:
id: gauge_needle
width: 4
color: 0xFF0000
r_mod: -10
value: 0
Multi-page with Navigation
lvgl:
pages:
- id: page1
widgets:
- label: { text: "Page 1", align: CENTER }
- id: page2
widgets:
- label: { text: "Page 2", align: CENTER }
top_layer:
widgets:
- obj:
align: BOTTOM_MID
width: 240
height: 40
layout:
type: FLEX
flex_flow: ROW
widgets:
- button:
flex_grow: 1
on_press:
- lvgl.page.previous:
widgets:
- label: { text: "◀", align: CENTER }
- button:
flex_grow: 1
on_press:
- lvgl.page.next:
widgets:
- label: { text: "▶", align: CENTER }
Actions and Conditions
Widget Update Actions
Update widget properties at runtime:
on_...:
- lvgl.widget.update:
id: my_label
text: "New text"
text_color: 0xFF0000
hidden: false
Page Navigation
# Show specific page
- lvgl.page.show: settings_page
# Next/previous page
- lvgl.page.next:
animation: OVER_LEFT
time: 300ms
- lvgl.page.previous:
Pause and Resume
# Pause LVGL (stop rendering)
- lvgl.pause:
show_snow: true # Show random pixels (anti-burn-in)
# Resume LVGL
- lvgl.resume:
Conditions
# Check if idle
- if:
condition:
lvgl.is_idle:
timeout: 30s
then:
- light.turn_off: backlight
# Check if paused
- if:
condition: lvgl.is_paused
then:
- lvgl.resume:
# Check active page
- if:
condition:
lvgl.page.is_showing: main_page
then:
- logger.log: "Main page active"
Fonts
ESPHome Fonts (Recommended)
Use custom fonts with specific glyphs:
font:
- file: "fonts/Roboto-Regular.ttf"
id: my_font
size: 20
bpp: 4
glyphs: "0123456789:.°C% ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
lvgl:
default_font: my_font
Material Design Icons
Add icons to fonts:
font:
- file: "fonts/Roboto-Regular.ttf"
id: font_with_icons
size: 24
bpp: 4
extras:
- file: "fonts/materialdesignicons-webfont.ttf"
glyphs: [
"\U000F0335", # mdi-lightbulb
"\U000F0594", # mdi-weather-sunny
"\U000F050F", # mdi-thermometer
]
label:
text: "Temperature \U000F050F 25°C"
text_font: font_with_icons
Built-in LVGL Fonts
Use pre-rendered Montserrat fonts:
label:
text_font: montserrat_20 # 8-48px available
Performance Tips
Buffer Size
Adjust buffer size for performance vs memory:
lvgl:
buffer_size: 25% # 12-100%, smaller uses less RAM
- 100%: Best performance, high RAM usage
- 25%: Good balance
- 12%: Minimal RAM, slower updates
Update Interval
display:
update_interval: never # LVGL handles updates
# Manual updates when needed
sensor:
on_value:
- component.update: my_display
Conditional Updates
Only update changed values:
sensor:
- platform: dht
temperature:
on_value:
then:
- if:
condition:
lambda: 'return abs(x - id(last_temp)) > 0.1;'
then:
- lvgl.label.update:
id: temp_label
text:
format: "%.1f°C"
args: ['x']
Troubleshooting
Display not updating
Check that:
auto_clear_enabled: falseon displayupdate_intervalnot set to incompatible value- LVGL configuration is valid
Widgets not visible
- Check widget
hiddenflag - Verify
opa(opacity) settings - Ensure widget is within parent bounds
- Check
bg_opais notTRANSP
Touch not working
- Verify touchscreen is configured in
lvgl.touchscreens - Check touchscreen driver configuration
- Test touchscreen separately
Out of memory
- Reduce
buffer_size - Use smaller fonts
- Simplify layouts
- Enable PSRAM if available
Advanced Features
See [references/cookbook.md](references/cookbook.md) for:
- Complete examples with Home Assistant integration
- Weather panels
- Climate controls
- Custom themes
- Animations
- Anti-burn-in prote
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: narrowstacks
- Source: narrowstacks/claude-homeassistant-plugins
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.