This repository provides a modular framework for building LVGL user interfaces in ESPHome. It abstracts hardware complexity and provides a reusable component system. While designed for the Seeedstudio SenseCAP Indicator (D1x), the architecture is adaptable to any ESPHome-supported display.
Technical documentation is provided in INSTRUCTIONS.md and AGENTS.md to support development and widget creation.
hardware/: Device-specific configurations.src/: Custom C++ headers for advanced LVGL features (e.g., Charts).templates/: Reusable logic and UI components.core/: UI structural components, globals, and the Data Bridge.devices/: Virtual device definitions.layouts/: Page layout templates.scripts/: Shared scripts and packages.overlays/: Contextual UI layers.tabs/: Screen definitions.widgets/: Standalone UI elements.
theme/: UI styling and variables.
main-dashboard.yaml: Main entry point and dashboard configuration.templates/core/mapping-defaults.yaml: View 1 (Mapping) fallbacks and state defaults.secrets.yaml: WiFi and API credentials.theme/defaults.yaml: Global UI variables (colors, dimensions, spacing).theme/style.yaml: LVGL style definitions.templates/core/fonts.yaml: Font mappings.
The framework enforces a strict separation of concerns:
- View 1: User Configuration: Handled in
main-dashboard.yaml(ortesting.yaml). Maps HA entities to substitutions. - View 2: Design & Templates: Located in
templates/widgets/,layouts/, andtheme/. These are visual components that read from the Data Pool. - View 3: Core Logic (Data Bridge): Located in
layouts/*/data_bridge.yamlandwidget_logic.yaml. This layer connects HA to the Data Pool (Globals) and triggers UI refreshes.
Hardware-specific setup (ESP32-S3, PSRAM, display, touchscreen) is isolated in hardware/. Reference these templates in your main configuration.
Uses ESPHome packages to separate concerns:
hardware: Drivers and display initialization.core/globals: The "Data Pool" of global variables.core/data_bridge: Home Assistant entity connections to the Data Pool.core/widget_logic: Scripts that sync the Data Pool to LVGL objects.scripts/common: Shared UI bridge scripts and logic.
Tabs use grid_container.yaml to implement LVGL's grid layout. Dimensions and positions are passed via substitutions.
Widgets and Tiles are YAML snippets accepting:
id: Unique component identifier.grid_col_pos/grid_row_pos(orcol/row): Grid position.grid_x_align/grid_y_align: Cell alignment (defaults toSTRETCH).widget_clickable: Enable/disable touch events.widget_on_click: C++ lambda for touch actions.
theme/defaults.yaml centralizes UI parameters:
- Screen dimensions.
- Color palette (with auto-generated variants).
- Grid spacing and padding.
- Font IDs.
- Idle timeout.
- Decoupled Sync (Data Bridge): UI components don't talk directly to Home Assistant. They observe global variables, ensuring the UI remains responsive even if HA is offline.
- Dynamic Styling: Light tiles automatically tint their icons and backgrounds based on the RGB state of the Home Assistant entity.
- Idle Management: Configurable timeout to pause LVGL and disable backlight.
- Clickable Cards: Any widget container can be made clickable with theme-defined visual feedback.
- Advanced Data Visualization: Custom LVGL Chart widget for displaying sensor history with auto-scaling and value overlays.
- Copy
main-dashboard.yamlto a new file (e.g.,my-device.yaml). - Update the
substitutionsblock with your Home Assistant entity IDs (lights, scenes, weather). - Compile and flash:
esphome run my-device.yamlThe framework supports multiple independent graph widgets. To add a new one:
- Define a Data Buffer: In layouts/X/globals_extension.yaml, add a new
std::vector<float>to store the sensor history.- id: my_sensor_values type: std::vector<float>
- Add the Widget: In your tab definition (e.g., layouts/X/tabs/sensors.yaml), include the graph widget with a unique
widget_id.- <<: !include file: ../../../templates/widgets/graph.yaml vars: widget_id: my_new_graph graph_title: "My Sensor" graph_unit: "unit" # ...
- Initialize on Boot: In main-dashboard.yaml, add the initialization script to the
on_boottrigger.- script.execute: id: init_graph container_obj: !lambda "return id(my_new_graph_chart_container);" color: 0xHEXCODE
- Connect the Sensor: In layouts/X/data_bridge.yaml, update your sensor to push data to the buffer and trigger the UI update.
on_value: - lambda: |- id(my_sensor_values).push_back(x); if (id(my_sensor_values).size() > ${chart_max_points}) { id(my_sensor_values).erase(id(my_sensor_values).begin()); } - script.execute: id: my_refresh_script # Defined in widget_logic.yaml # Or call update_graph directly