Give a lamp's modes a page: the effects, palettes, presets, speed and intensity of a WLED behind its colour card, with a drum picker and the effect's name on the tile (0.2.83)

A light that offers effects gets a sparkles key at the top right of its colour
card. It opens a page with a row per thing the lamp offers, all from Home
Assistant: the light's effect and the select entities of its device (a WLED's
colour palette, preset and playlist) with what they are set to, and a slider per
number entity of the device (speed, intensity). Names are Home Assistant's own
English entity translations, icons its frontend's; nothing about a brand is
hardcoded beyond a preferred order of translation keys.

A row opens LVGL's roller with every name Home Assistant lists at that moment,
asked for when the picker opens (esphome.screen_options, answered with
op: options, one page of names per message), the effects alphabetically with
Solid on top. The check key at the top right sends the choice as one action;
the back key sends nothing. A slider sends number.set_value when let go. While
an effect runs, the tile names it instead of the brightness.

Firmware 0.2.70: effects_page.h (built when it opens, gone when it closes),
Extra.effect/option_rows/number_rows, options_request, a hidden roller widget
so ESPHome compiles LVGL's roller, five firmware-only glyphs. App 0.2.83:
light_effects.py, the effect attribute, the device rows in the state message,
card_options_loop. Checked on the host with a virtual finger (35/35 on both
boards) and compiled for both ESP32 boards; CYD 1,675,424 B (91.3 %).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Max Gramser
2026-09-18 21:38:20 +02:00
parent 86e6ce79fe
commit e7e173d3b7
33 changed files with 1409 additions and 17 deletions
+62 -1
View File
@@ -17,7 +17,7 @@
############################################################
substitutions:
SCREEN_FIRMWARE_VERSION: "0.2.69"
SCREEN_FIRMWARE_VERSION: "0.2.70"
# A tap survives a finger that drifts up to this far (320 px over 58 mm: 1 cm); further is a swipe.
TOUCH_MOVE_LIMIT_PX: "56"
# Shortest contact that counts as a tap; a resistive panel bounces, a capacitive one does not.
@@ -314,6 +314,7 @@ esphome:
theme::set_dark(settings_screen::dark_mode != 0);
runtime_tiles::settings_changed = []() { id(apply_screen_settings).execute(); };
runtime_tiles::dismiss = []() {
effects_page::close();
runtime_tiles::hide_detail();
id(active_entity).clear();
lv_obj_add_flag(id(brightness_overlay), LV_OBJ_FLAG_HIDDEN);
@@ -363,6 +364,7 @@ esphome:
std::to_string(t.minimum), std::to_string(t.maximum), std::to_string(t.step));
};
runtime_tiles::detail_update = [](runtime_tiles::Tile &t) {
effects_page::updated(t);
if (id(active_entity) != t.entity || id(touch_down)) return;
if (t.domain() == "climate" && !lv_obj_has_flag(id(climate_detail_overlay), LV_OBJ_FLAG_HIDDEN)) {
if (id(climate_edit_pending) && millis() - id(climate_edit_ms) < 3000) return;
@@ -386,6 +388,24 @@ esphome:
light_controls::commits[0] = [](int value) { id(set_light_hue).execute(value); };
light_controls::commits[1] = [](int value) { id(set_light_color_temperature).execute(value); };
light_controls::commits[2] = [](int value) { id(color_brightness_commit).execute(value); };
// The effects page behind the colour card's sparkles key (firmware 0.2.70+): fonts, the tile it shows, and
// the ways out: actions to Home Assistant and the question for a picker's names.
effects_page::title_font = id(headline)->get_lv_font();
effects_page::row_font = id(sublabel_big)->get_lv_font();
effects_page::roller_font = id(sublabel_big)->get_lv_font();
effects_page::icon_font = id(materialdesign_icons_mini)->get_lv_font();
effects_page::tile_of = [](const std::string &entity) -> const runtime_tiles::Tile * {
for (size_t i = 0; i < runtime_tiles::model.count; ++i) if (runtime_tiles::model.tiles[i].entity == entity) return &runtime_tiles::model.tiles[i];
return nullptr;
};
effects_page::send = [](const std::string &service, const std::string &entity, const std::string &key, const std::string &value) {
runtime_tiles::action(service, entity, key, value);
};
effects_page::ask = [](const std::string &entity, unsigned page) { runtime_tiles::options_request(entity, page); };
effects_page::drift = []() { return cyd::touch_guard.distance(); };
effects_page::tap_limit = ${TOUCH_MOVE_LIMIT_PX};
effects_page::now = []() { return (uint32_t) millis(); };
runtime_tiles::options_received = effects_page::received;
- lambda: |-
if (!runtime_tiles::enabled) return;
screen_calibration::apply = [](const screen_calibration::Calibration &cal) {
@@ -423,6 +443,7 @@ esphome:
theme::redraw = []() {
runtime_tiles::restyle();
light_controls::restyle();
effects_page::restyle();
settings_screen::restyle();
if (id(alert_active)) lv_obj_set_style_bg_color(id(alert_card), lv_color_hex(theme::surface(id(alert_card_color))), LV_PART_MAIN);
if (!lv_obj_has_flag(id(climate_detail_overlay), LV_OBJ_FLAG_HIDDEN)) id(climate_card_refresh).execute();
@@ -1231,6 +1252,11 @@ font:
- "\U000F1B73" # speaker-pause
- "\U000F1B72" # speaker-play
- "\U000F04C5" # speedometer
- "\U000F0674" # creation
- "\U000F0E0C" # palette-outline
- "\U000F0411" # playlist-play
- "\U000F08FF" # play-speed
- "\U000F00DF" # brightness-6
- "\U000F1429" # spoon-sugar
- "\U000F0E67" # sprout-outline
- "\U000F0764" # square
@@ -1879,6 +1905,14 @@ lvgl:
height: 2
spin_time: 1s
arc_length: 60deg
# Never shown either: it makes ESPHome compile LVGL's roller, the drum a light's effects page picks from
# (firmware 0.2.70+).
- roller:
id: roller_seed
hidden: true
width: 2
height: 2
options: ["-"]
- label:
id: lbl_room
x: 11
@@ -2849,6 +2883,31 @@ lvgl:
text_font: materialdesign_icons_mini
clickable: false
# The sparkles key (firmware 0.2.70+): a light with effects, or with modes on its device (a WLED's palette,
# preset, speed), opens its effects page. Hidden for every other light.
- obj:
id: color_effects_button
align: TOP_RIGHT
x: -10
y: 8
width: 40
height: 40
radius: 999
styles: paint_key
bg_opa: COVER
border_width: 0
hidden: true
clickable: true
scrollable: false
on_click:
- lambda: 'effects_page::open(id(active_entity));'
widgets:
- label:
align: CENTER
text: "\U000F0674"
text_font: materialdesign_icons_mini
clickable: false
- label:
id: color_detail_title
width: 210
@@ -4318,6 +4377,8 @@ script:
- lambda: |-
lv_label_set_text(id(color_detail_title), id(active_title).c_str());
light_controls::open(id(active_entity), id(active_supports_color), id(active_supports_temperature), id(active_brightness_pct));
if (effects_page::offered(id(active_entity))) lv_obj_remove_flag(id(color_effects_button), LV_OBJ_FLAG_HIDDEN);
else lv_obj_add_flag(id(color_effects_button), LV_OBJ_FLAG_HIDDEN);
- lvgl.widget.hide: brightness_overlay
- lvgl.widget.show: color_detail_overlay