dfd1a26d69
Wake from Home Assistant counted as a tap: it reset last_touch_ms, the one clock that standby and Back to page 1 both read. An automation pressing Wake on every motion therefore held an open card or page 2 up for as long as someone moved in the room. Back to page 1 now has its own clock, last_use_ms, written only by the boot, the touchscreen triggers, open_settings from Home Assistant and the UI self test. Wake, an alert ending and Auto standby on still restart the standby time, and Wake still brings back the brightness and the second hand. The rule moved from the interval into the script back_to_page_1_when_due. Wake runs it right after waking a dimmed screen, so a page whose time ran out during standby lights up on page 1 instead of jumping there a second later. ESP Screens no longer opens the first screen by itself. Until one is picked the page says "Choose a screen", and with no screens it still offers the install; both cards start hidden so neither flashes while the page loads. Checked with 48 host API checks per board, both ESP32 builds, 314 Python and 16 C++ tests, and the editor on the demo home. Firmware 0.2.56. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
4643 lines
184 KiB
YAML
4643 lines
184 KiB
YAML
# GENERATED by tools/generate_packages.py; edit the source board profile.
|
||
# ESP Screens - Guition ESP32-S3-4848S040 board profile: 4-inch EU wallbox, 480x480 IPS + GT911.
|
||
# Source profile for packages/guition.yaml (tools/generate_packages.py); see docs/GUITION.md.
|
||
# A screen gets its tiles from the ESP Screen Manager add-on while it runs. The CYD is its own profile.
|
||
substitutions:
|
||
SCREEN_FIRMWARE_VERSION: "0.2.56"
|
||
# 0: no drift limit; LVGL decides, releasing inside the tile is a tap, leaving it is not
|
||
# (like iOS). The CYD keeps a limit because its resistive panel jumps.
|
||
TOUCH_MOVE_LIMIT_PX: "0"
|
||
# Shortest contact that counts as a tap; a resistive panel bounces, a capacitive one does not.
|
||
TOUCH_MIN_PRESS_MS: "20"
|
||
# Page swipe starts in this band along the left or right edge (about 5 mm) and needs
|
||
# this much travel inward (about 6 mm); a swipe from the middle of the screen does nothing.
|
||
EDGE_SWIPE_BAND_PX: "32"
|
||
EDGE_SWIPE_TRAVEL_PX: "40"
|
||
DASHBOARD_INBOX_INTERNAL: "false"
|
||
DEVICE_NAME: "guition-new"
|
||
DEVICE_FRIENDLY_NAME: "My Guition"
|
||
CALIBRATION_ON_BOOT: "false"
|
||
ROOM_NAME: "Choose tiles in HA" # shown top left until ESP Screens sends the screen name
|
||
|
||
## DIM settings
|
||
AUTO_DIM: "true" # true = enable auto dimming
|
||
AUTO_DIM_TIMEOUT: "600" # seconds after last touch
|
||
MAX_BRIGHTNESS: "100" # backlight brightness in percent
|
||
DIM_BRIGHTNESS: "20" # dimmed backlight brightness in percent
|
||
NIGHT_MODE: "true" # true = use separate dim level at night
|
||
NIGHT_START_HOUR: "22" # 0-23
|
||
NIGHT_END_HOUR: "7" # 0-23
|
||
NIGHT_DIM_BRIGHTNESS: "10" # backlight brightness in percent at night
|
||
|
||
DISPLAY_W: "480"
|
||
DISPLAY_H: "480"
|
||
LVGL_ROTATION: "0"
|
||
TOUCH_SWAP_XY: "false"
|
||
TOUCH_MIRROR_X: "false"
|
||
TOUCH_MIRROR_Y: "false"
|
||
GRID_COL1_X: "16"
|
||
GRID_COL2_X: "246"
|
||
GRID_ROW1_Y: "0"
|
||
GRID_ROW2_Y: "120"
|
||
GRID_ROW3_Y: "240"
|
||
GRID_ROW4_Y: "360"
|
||
GRID_ROW5_Y: "480"
|
||
SCROLL_Y: "72"
|
||
SCROLL_H: "348"
|
||
TILE_W: "218"
|
||
TILE_H: "108"
|
||
HEADER_Y: "22"
|
||
# Holding the top bar opens the settings page (0.2.44+). The two edge bands stay clear: a swipe
|
||
# that starts up there still changes the page.
|
||
SETTINGS_HOLD_X: "${EDGE_SWIPE_BAND_PX}"
|
||
SETTINGS_HOLD_W: "416"
|
||
SETTINGS_HOLD_DRIFT_PX: "32"
|
||
|
||
# Alert overlay (HA action show_alert), 480x480: one card centred on LVGL's top layer.
|
||
ALERT_CARD_W: "420"
|
||
ALERT_CARD_H: "320"
|
||
ALERT_ICON_X: "26"
|
||
ALERT_ICON_Y: "22"
|
||
ALERT_TEXT_X: "90"
|
||
ALERT_TEXT_W: "304"
|
||
ALERT_TITLE_Y: "26"
|
||
ALERT_TITLE_H: "32" # one line of headline; a DOT label only ends in an ellipsis with a height
|
||
ALERT_SUBTITLE_Y: "68"
|
||
ALERT_SUBTITLE_H: "150"
|
||
ALERT_BUTTON_W: "150"
|
||
ALERT_BUTTON_H: "60"
|
||
ALERT_BUTTON_INSET: "22"
|
||
ALERT_TITLE_MAX: "64" # bytes; the label ends one line with an ellipsis
|
||
ALERT_BUTTON_MAX: "16" # bytes on the OK button
|
||
ALERT_SUBTITLE_MAX: "240" # bytes; wraps over the rows the card has
|
||
OVERLAY_SLIDER_W: "144"
|
||
OVERLAY_SLIDER_H: "300"
|
||
OVERLAY_ICON_Y: "148"
|
||
TILE_ICON_X: "0"
|
||
TILE_ICON_Y: "12"
|
||
TILE_TITLE_X: "64"
|
||
TILE_TITLE_Y: "17"
|
||
TILE_VALUE_X: "64"
|
||
TILE_VALUE_Y: "45"
|
||
|
||
# ============================================================
|
||
# No need to edit below this line.
|
||
# All configuration is done via the substitutions above.
|
||
# Credentials are managed in secrets.yaml (see secrets.yaml.example).
|
||
# The Material Design Icons font is included at fonts/materialdesignicons-webfont.ttf (Apache 2.0).
|
||
# Source: https://github.com/Templarian/MaterialDesign-Webfont
|
||
# ============================================================
|
||
external_components:
|
||
- source:
|
||
type: git
|
||
url: https://github.com/MaxGramser/homeassistant_espscreen.git
|
||
ref: main
|
||
path: components
|
||
refresh: 0s
|
||
components: [smart_display]
|
||
|
||
esphome:
|
||
min_version: 2026.6.2
|
||
project:
|
||
name: maxgramser.esp_screen
|
||
version: "${SCREEN_FIRMWARE_VERSION}"
|
||
platformio_options:
|
||
build_flags:
|
||
- -DLV_USE_SNAPSHOT=1
|
||
- -DLV_USE_SWITCH=1
|
||
includes:
|
||
- <esphome/components/smart_display/cyd_ui.h>
|
||
- <esphome/components/smart_display/light_controls.h>
|
||
- <esphome/components/smart_display/guition_diagnostics.h>
|
||
- <esphome/components/smart_display/backlight_fade.h>
|
||
- <esphome/components/smart_display/alert_overlay.h>
|
||
- <esphome/components/smart_display/settings_screen.h>
|
||
name: ${DEVICE_NAME}
|
||
friendly_name: ${DEVICE_FRIENDLY_NAME}
|
||
on_boot:
|
||
priority: -100
|
||
then:
|
||
- lambda: |-
|
||
cyd::touch_guard.configure(${TOUCH_MOVE_LIMIT_PX}, ${TOUCH_MIN_PRESS_MS});
|
||
cyd::edge_swipe.configure(${DISPLAY_W}, ${DISPLAY_H}, ${EDGE_SWIPE_BAND_PX}, ${EDGE_SWIPE_TRAVEL_PX});
|
||
// The page and the tile container take no presses: the theme's pressed style would
|
||
// otherwise repaint nearly the whole screen twice per touch in the empty edge band,
|
||
// starving the touch polling for the whole length of a quick swipe.
|
||
lv_obj_remove_flag(id(home_page)->obj, LV_OBJ_FLAG_CLICKABLE);
|
||
lv_obj_remove_flag(id(tile_scroll), LV_OBJ_FLAG_CLICKABLE);
|
||
// A stray GT911 contact at exactly (0, 0) never reaches LVGL (cyd::GhostTouch): as the last
|
||
// sample of a drag it sent a slider to its end on release. LVGL still reads ESPHome's callback.
|
||
if (auto *indev = lv_indev_get_next(nullptr)) {
|
||
static lv_indev_read_cb_t esphome_read = nullptr;
|
||
static const lv_indev_read_cb_t filtered = [](lv_indev_t *input, lv_indev_data_t *data) {
|
||
esphome_read(input, data);
|
||
int32_t ghost_x = 0, ghost_y = 0;
|
||
id(screen_lvgl).rotate_coordinates(ghost_x, ghost_y);
|
||
const unsigned dropped = cyd::ghost_touch.dropped();
|
||
const auto read = cyd::ghost_touch.filter({data->state == LV_INDEV_STATE_PRESSED, (int) data->point.x, (int) data->point.y}, ghost_x, ghost_y);
|
||
if (cyd::ghost_touch.dropped() != dropped) ESP_LOGI("touch", "GT911 stray contact at (0,0) ignored");
|
||
data->state = read.pressed ? LV_INDEV_STATE_PRESSED : LV_INDEV_STATE_RELEASED;
|
||
data->point.x = read.x;
|
||
data->point.y = read.y;
|
||
};
|
||
if (lv_indev_get_read_cb(indev) != filtered) {
|
||
esphome_read = lv_indev_get_read_cb(indev);
|
||
lv_indev_set_read_cb(indev, filtered);
|
||
}
|
||
}
|
||
id(last_touch_ms) = millis();
|
||
id(last_use_ms) = millis();
|
||
id(display_dimmed) = false;
|
||
- lambda: |-
|
||
auto &settings = screen_settings::current;
|
||
settings.standby_enabled = std::string("${AUTO_DIM}") == "true";
|
||
settings.standby_seconds = atoi("${AUTO_DIM_TIMEOUT}");
|
||
settings.brightness = atoi("${MAX_BRIGHTNESS}");
|
||
settings.standby_brightness = atoi("${DIM_BRIGHTNESS}");
|
||
settings.night_enabled = std::string("${NIGHT_MODE}") == "true";
|
||
settings.night_start = atoi("${NIGHT_START_HOUR}") * 60;
|
||
settings.night_end = atoi("${NIGHT_END_HOUR}") * 60;
|
||
settings.night_brightness = atoi("${NIGHT_DIM_BRIGHTNESS}");
|
||
runtime_tiles::watch_font = id(headline)->get_lv_font();
|
||
runtime_tiles::watch_value_font = id(watch_value)->get_lv_font();
|
||
runtime_tiles::watch_icon_font = id(watch_icon)->get_lv_font();
|
||
runtime_tiles::clock_font = id(clock_digits)->get_lv_font();
|
||
runtime_tiles::now_time = []() { return id(homeassistant_time).now(); };
|
||
runtime_tiles::screen_awake = []() { return !id(display_dimmed); };
|
||
runtime_tiles::mini_icon_font = id(materialdesign_icons_mini)->get_lv_font();
|
||
runtime_tiles::control_font = id(sublabel_big)->get_lv_font();
|
||
runtime_tiles::small_font = id(sublabel)->get_lv_font();
|
||
// Top bar (0.2.32+): values in the text font, icons in the small icon font; the clock
|
||
// label lends its margin and drawing order and stays hidden in runtime mode.
|
||
runtime_tiles::header_text_font = id(sublabel_big)->get_lv_font();
|
||
runtime_tiles::header_icon_font = id(materialdesign_icons_mini)->get_lv_font();
|
||
runtime_tiles::time_label = id(lbl_time);
|
||
runtime_tiles::enabled = true;
|
||
runtime_tiles::rotation_supported = true;
|
||
runtime_tiles::load_settings();
|
||
// Colours (firmware 0.2.54+): the styles of this profile take their colours from theme.h. The list runs now,
|
||
// in the look the screen saved, before anything is drawn, and again after every change of look.
|
||
theme::paints = []() {
|
||
using theme::Paint;
|
||
for (auto *style : {id(style_room), id(style_time), id(style_title), id(style_value_big), id(paint_ink)}) theme::fill(style, Paint::ink);
|
||
theme::fill(id(style_page), Paint::page);
|
||
theme::fill(id(style_tile), Paint::card);
|
||
theme::fill(id(style_icon_circle), Paint::accent_tint);
|
||
theme::fill(id(style_value), Paint::muted);
|
||
theme::fill(id(paint_page_soft), Paint::page_soft);
|
||
theme::fill(id(paint_page_pressed), Paint::page_pressed);
|
||
theme::fill(id(paint_card), Paint::card);
|
||
theme::fill(id(paint_raised), Paint::raised);
|
||
theme::fill(id(paint_key), Paint::key);
|
||
theme::fill(id(paint_key_pressed), Paint::key_pressed);
|
||
theme::fill(id(paint_track), Paint::track);
|
||
theme::fill(id(paint_ink_soft), Paint::ink_soft);
|
||
theme::fill(id(paint_muted), Paint::muted);
|
||
theme::fill(id(paint_subtle), Paint::subtle);
|
||
theme::fill(id(paint_slate), Paint::slate);
|
||
theme::fill(id(paint_scrim), Paint::scrim);
|
||
theme::fill(id(paint_veil), Paint::veil);
|
||
theme::fill(id(paint_accent_tint), Paint::accent_tint);
|
||
theme::fill(id(paint_accent_bright), Paint::accent_bright);
|
||
theme::fill(id(paint_button_dark), Paint::button_dark);
|
||
theme::fill(id(paint_button_dark_pressed), Paint::button_dark_pressed);
|
||
};
|
||
theme::paints();
|
||
theme::set_dark(settings_screen::dark_mode != 0);
|
||
runtime_tiles::settings_changed = []() { id(apply_screen_settings).execute(); };
|
||
runtime_tiles::dismiss = []() {
|
||
runtime_tiles::hide_detail();
|
||
id(active_entity).clear();
|
||
lv_obj_add_flag(id(brightness_overlay), LV_OBJ_FLAG_HIDDEN);
|
||
lv_obj_add_flag(id(color_detail_overlay), LV_OBJ_FLAG_HIDDEN);
|
||
lv_obj_add_flag(id(climate_detail_overlay), LV_OBJ_FLAG_HIDDEN);
|
||
lv_obj_add_flag(id(climate_mode_overlay), LV_OBJ_FLAG_HIDDEN);
|
||
};
|
||
runtime_tiles::layout_changed = []() { id(show_tile_page).execute(); };
|
||
runtime_tiles::refresh = []() { if (!id(ui_refresh).is_running()) id(ui_refresh).execute(); };
|
||
runtime_tiles::bind(0, id(tile1), id(t1_title), id(t1_value), id(tile1_icon_circle), id(tile1_icon_lbl));
|
||
runtime_tiles::bind(1, id(tile2), id(t2_title), id(t2_value), id(tile2_icon_circle), id(tile2_icon_lbl));
|
||
runtime_tiles::bind(2, id(tile3), id(t3_title), id(t3_value), id(tile3_icon_circle), id(tile3_icon_lbl));
|
||
runtime_tiles::bind(3, id(tile4), id(t4_title), id(t4_value), id(tile4_icon_circle), id(tile4_icon_lbl));
|
||
runtime_tiles::bind(4, id(tile5), id(t5_title), id(t5_value), id(tile5_icon_circle), id(tile5_icon_lbl));
|
||
runtime_tiles::bind(5, id(tile6), id(t6_title), id(t6_value), id(tile6_icon_circle), id(tile6_icon_lbl));
|
||
runtime_tiles::bind(6, id(tile7), id(t7_title), id(t7_value), id(tile7_icon_circle), id(tile7_icon_lbl));
|
||
runtime_tiles::bind(7, id(tile8), id(t8_title), id(t8_value), id(tile8_icon_circle), id(tile8_icon_lbl));
|
||
runtime_tiles::bind(8, id(tile9), id(t9_title), id(t9_value), id(tile9_icon_circle), id(tile9_icon_lbl));
|
||
runtime_tiles::bind(9, id(tile10), id(t10_title), id(t10_value), id(tile10_icon_circle), id(tile10_icon_lbl));
|
||
runtime_tiles::detail = [](runtime_tiles::Tile &t) {
|
||
light_controls::fallback = light_controls::State{};
|
||
light_controls::fallback.hue = t.hue;
|
||
light_controls::fallback.kelvin = t.kelvin;
|
||
light_controls::fallback.minimum = t.min_kelvin;
|
||
light_controls::fallback.maximum = t.max_kelvin;
|
||
id(active_climate_fan_modes) = t.extra().fan_modes; id(active_climate_fan_mode) = t.extra().fan_mode;
|
||
id(active_climate_swing_modes) = t.extra().swing_modes; id(active_climate_swing_mode) = t.extra().swing_mode;
|
||
id(active_climate_action) = t.extra().hvac_action;
|
||
id(open_value_overlay).execute(t.name, t.entity, t.domain(), "auto", t.active(), 0,
|
||
t.brightness, t.percentage, t.position, runtime_tiles::icon_for(t),
|
||
"Off", "On", t.modes,
|
||
t.current, t.target, t.humidity, t.state, t.extra().hvac_modes,
|
||
std::to_string(t.minimum), std::to_string(t.maximum), std::to_string(t.step));
|
||
};
|
||
runtime_tiles::detail_update = [](runtime_tiles::Tile &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;
|
||
id(climate_edit_pending) = false;
|
||
id(active_climate_mode) = t.state;
|
||
id(active_climate_action) = t.extra().hvac_action;
|
||
id(active_climate_supported_modes) = t.extra().hvac_modes;
|
||
id(active_climate_fan_modes) = t.extra().fan_modes; id(active_climate_fan_mode) = t.extra().fan_mode;
|
||
id(active_climate_swing_modes) = t.extra().swing_modes; id(active_climate_swing_mode) = t.extra().swing_mode;
|
||
id(active_climate_current) = t.current;
|
||
id(active_climate_humidity) = std::isfinite(t.humidity) ? (int) lround(t.humidity) : -1;
|
||
if (std::isfinite(t.target)) {
|
||
id(active_climate_target_x10) = cyd::quantize_temperature(lround(t.target * 10), id(active_climate_min_x10), id(active_climate_max_x10), id(active_climate_step_x10));
|
||
id(active_climate_target_known) = true;
|
||
}
|
||
id(climate_card_refresh).execute();
|
||
}
|
||
};
|
||
- lambda: |-
|
||
light_controls::setup(id(color_detail_overlay), id(sublabel)->get_lv_font(), atoi("${DISPLAY_W}"), atoi("${DISPLAY_H}"), id(materialdesign_icons_mini)->get_lv_font());
|
||
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); };
|
||
climate_card::look.card = id(climate_settings_card);
|
||
climate_card::look.font = id(label)->get_lv_font();
|
||
climate_card::look.icon_font = id(materialdesign_icons_mini)->get_lv_font();
|
||
climate_card::choose = [](char kind, int index) {
|
||
if (kind == 'f') id(set_climate_fan_mode).execute(index);
|
||
else id(set_climate_swing_mode).execute(index);
|
||
};
|
||
# The settings page the screen opens on its own (0.2.44+): the same values ESP Screen Manager
|
||
# writes, reachable by holding the top bar. Fonts and hooks only; the page itself is built
|
||
# when it opens and thrown away when it closes, so it costs nothing while it is closed.
|
||
- lambda: |-
|
||
if (!runtime_tiles::enabled) return;
|
||
settings_screen::title_font = id(headline)->get_lv_font();
|
||
settings_screen::row_font = id(sublabel_big)->get_lv_font();
|
||
settings_screen::icon_font = id(materialdesign_icons_mini)->get_lv_font();
|
||
settings_screen::store = []() { runtime_tiles::persist_settings(); };
|
||
settings_screen::apply = []() { id(apply_screen_settings).execute(); };
|
||
settings_screen::report = [](const char *key, int32_t value) { runtime_tiles::setting_event(key, value); };
|
||
settings_screen::name_text = []() { return std::string("${DEVICE_FRIENDLY_NAME}"); };
|
||
settings_screen::address_text = []() { return id(wifi_ip).state.empty() ? std::string("No network") : id(wifi_ip).state; };
|
||
settings_screen::firmware_text = []() { return std::string("${SCREEN_FIRMWARE_VERSION}"); };
|
||
settings_screen::link_text = []() { return std::string(esphome::api_is_connected() ? "Connected" : "Not connected"); };
|
||
settings_screen::restart_device = []() { ESP_LOGI("settings", "restart from the screen"); App.safe_reboot(); };
|
||
// A dimmed screen, a calibration or an alert owns the glass; the page waits.
|
||
settings_screen::may_open = []() {
|
||
return runtime_tiles::enabled && !id(display_dimmed) && !id(calibration_active) && !id(alert_active);
|
||
};
|
||
settings_screen::drift = []() { return cyd::touch_guard.distance(); };
|
||
settings_screen::before_open = []() { id(close_cards).execute(); };
|
||
settings_screen::hold_limit = ${SETTINGS_HOLD_DRIFT_PX};
|
||
settings_screen::tap_limit = ${TOUCH_MOVE_LIMIT_PX};
|
||
// What a change of look draws again, now that everything is bound: the tiles, the top bar and an open card,
|
||
// the colour card, the settings page, an alert and the climate cards.
|
||
theme::redraw = []() {
|
||
runtime_tiles::restyle();
|
||
light_controls::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();
|
||
if (!lv_obj_has_flag(id(climate_mode_overlay), LV_OBJ_FLAG_HIDDEN)) id(open_climate_mode_picker).execute();
|
||
};
|
||
// Under the cards: brightness_overlay is the first of them on the page, so the back button and
|
||
// the action at the top right of a card keep their taps.
|
||
settings_screen::attach_hold(id(home_page)->obj, ${SETTINGS_HOLD_X}, 0, ${SETTINGS_HOLD_W}, ${SCROLL_Y}, id(brightness_overlay));
|
||
- light.turn_on:
|
||
id: back_light
|
||
brightness: !lambda "return screen_settings::current.brightness / 100.0f;"
|
||
- script.execute: show_tile_page
|
||
- lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();'
|
||
- if:
|
||
condition:
|
||
lambda: 'return std::string("${CALIBRATION_ON_BOOT}") == "true";'
|
||
then:
|
||
- lambda: 'id(calibration_active) = true;'
|
||
- lvgl.page.show: touch_cal_page
|
||
- lambda: |-
|
||
lv_obj_invalidate(id(touch_cal_page)->obj);
|
||
lv_refr_now(nullptr);
|
||
ESP_LOGI("ui_diag", "GT911 touch test ready; no tile actions");
|
||
|
||
|
||
esp32:
|
||
board: esp32-s3-devkitc-1
|
||
variant: esp32s3
|
||
flash_size: 16MB
|
||
cpu_frequency: 240MHz
|
||
framework:
|
||
type: esp-idf
|
||
version: recommended
|
||
advanced:
|
||
execute_from_psram: true
|
||
|
||
psram:
|
||
mode: octal
|
||
speed: 80MHz
|
||
|
||
logger:
|
||
tx_buffer_size: 2048
|
||
hardware_uart: UART0
|
||
# Set to DEBUG or VERBOSE temporarily for diagnosis. Note: ESPHome logs
|
||
# its configuration lines at CONFIG level, which are invisible at INFO.
|
||
level: DEBUG
|
||
logs:
|
||
sensor: WARN
|
||
text_sensor: WARN
|
||
xpt2046: WARN
|
||
homeassistant.sensor: WARN
|
||
homeassistant.text_sensor: WARN
|
||
|
||
|
||
# Credentials are loaded from secrets.yaml — copy secrets.yaml.example to secrets.yaml and fill in your values.
|
||
api:
|
||
actions:
|
||
- action: ui_snapshot
|
||
then:
|
||
- script.execute: capture_ui_snapshot
|
||
- action: touch_diagnostics
|
||
then:
|
||
- lambda: 'id(calibration_active) = true;'
|
||
- script.execute: wake_display
|
||
- lvgl.page.show: touch_cal_page
|
||
- lambda: |-
|
||
lv_obj_invalidate(id(touch_cal_page)->obj);
|
||
lv_refr_now(nullptr);
|
||
ESP_LOGI("ui_diag", "calibration page active=%d", lv_screen_active() == id(touch_cal_page)->obj);
|
||
- action: end_touch_diagnostics
|
||
then:
|
||
- lambda: 'id(calibration_active) = false;'
|
||
- lvgl.page.show: home_page
|
||
- action: ui_state
|
||
then:
|
||
- lambda: |-
|
||
const auto &cfg = screen_settings::current;
|
||
ESP_LOGI("ui_diag", "settings standby=%d timeout=%d brightness=%d dim=%d night=%d start=%d end=%d night_dim=%d clock=%d clock24=%d home=%d idle_ms=%u use_idle_ms=%u dimmed=%d",
|
||
cfg.standby_enabled, cfg.standby_seconds, cfg.brightness, cfg.standby_brightness,
|
||
cfg.night_enabled, cfg.night_start, cfg.night_end, cfg.night_brightness,
|
||
cfg.show_clock, cfg.clock_24h, cfg.home_on_standby,
|
||
(unsigned)(millis() - id(last_touch_ms)), (unsigned)(millis() - id(last_use_ms)), id(display_dimmed));
|
||
ESP_LOGI("ui_diag", "navigation page=%d runtime_detail=%d", id(tile_page),
|
||
runtime_tiles::detail_root && !lv_obj_has_flag(runtime_tiles::detail_root, LV_OBJ_FLAG_HIDDEN));
|
||
ESP_LOGI("ui_diag", "rotation=%u actual=%u", (unsigned)runtime_tiles::rotation, (unsigned)id(screen_lvgl).get_rotation());
|
||
lv_area_t nav, grid;
|
||
lv_obj_update_layout(id(home_page)->obj);
|
||
lv_obj_get_coords(id(page_next), &nav);
|
||
lv_obj_get_coords(id(tile_scroll), &grid);
|
||
ESP_LOGI("ui_diag", "home=%d calibration=%d grid=%d,%d,%d,%d next=%d,%d,%d,%d",
|
||
lv_screen_active() == id(home_page)->obj, lv_screen_active() == id(touch_cal_page)->obj,
|
||
grid.x1, grid.y1, grid.x2, grid.y2, nav.x1, nav.y1, nav.x2, nav.y2);
|
||
- action: preview_runtime_card
|
||
variables:
|
||
index: int
|
||
then:
|
||
- lambda: |-
|
||
if (index < 0) { runtime_tiles::hide_detail(); if (runtime_tiles::dismiss) runtime_tiles::dismiss(); return; }
|
||
if (runtime_tiles::enabled && index < (int)runtime_tiles::model.count) {
|
||
runtime_tiles::active_index = index;
|
||
auto &tile = runtime_tiles::model.tiles[index];
|
||
auto domain = tile.domain();
|
||
// Lights, fans and climate open the board's own card, like a long press does; covers (firmware 0.2.50+)
|
||
// open the runtime card, as a tap does.
|
||
if ((domain == "light" || domain == "fan" || domain == "climate") && runtime_tiles::detail) {
|
||
tile.begin(millis(), true);
|
||
runtime_tiles::detail(tile);
|
||
return;
|
||
}
|
||
runtime_tiles::show_detail(index);
|
||
}
|
||
# The settings page from Home Assistant: `page` 0 is its menu, 1-4 open a group straight away
|
||
# (Brightness, Night, Screen, This screen) and a negative page closes it and goes back to page 1.
|
||
# Handy for support ("look at Screen -> Rotation") and for the render tests.
|
||
- action: open_settings
|
||
variables:
|
||
page: int
|
||
then:
|
||
- lambda: |-
|
||
if (page < 0) { id(go_home).execute(); return; }
|
||
// Opened for someone to look at: Back to page 1 counts from now, as after a touch.
|
||
id(last_use_ms) = millis();
|
||
id(wake_display).execute();
|
||
settings_screen::open((uint8_t) page);
|
||
ESP_LOGI("settings", "page %d open from Home Assistant", page);
|
||
|
||
# Diagnostics: the climate mode picker and the busy sheet of a tile, without touching the glass or HA.
|
||
- action: preview_climate_picker
|
||
then:
|
||
- script.execute: open_climate_mode_picker
|
||
- action: preview_busy
|
||
variables:
|
||
index: int
|
||
then:
|
||
- lambda: |-
|
||
if (runtime_tiles::enabled && index >= 0 && index < (int)runtime_tiles::model.count) {
|
||
runtime_tiles::model.tiles[index].begin(millis());
|
||
if (runtime_tiles::refresh) runtime_tiles::refresh();
|
||
}
|
||
- action: light_controls_preview
|
||
then:
|
||
- script.execute: light_controls_preview
|
||
- action: ui_self_test
|
||
then:
|
||
- script.execute: ui_self_test
|
||
# Home Assistant automations: esphome.<device>_show_alert puts a card over the whole
|
||
# screen (README, "Alert from an automation"); dismiss_alert removes it again.
|
||
- action: show_alert
|
||
variables:
|
||
title: string
|
||
subtitle: string
|
||
icon: string
|
||
color: string
|
||
button_text: string
|
||
timeout: int
|
||
flash: bool
|
||
then:
|
||
- script.execute:
|
||
id: alert_show
|
||
title: !lambda 'return title;'
|
||
subtitle: !lambda 'return subtitle;'
|
||
icon: !lambda 'return icon;'
|
||
color: !lambda 'return color;'
|
||
button_text: !lambda 'return button_text;'
|
||
timeout: !lambda 'return timeout;'
|
||
flash: !lambda 'return flash;'
|
||
- action: dismiss_alert
|
||
then:
|
||
- script.execute:
|
||
id: alert_dismiss
|
||
reason: "remote"
|
||
# ESP Screen Manager 0.2.39+ hands a whole tile message over in one call instead of
|
||
# base64 chunks in the text inbox; the inbox keeps showing the result.
|
||
# Firmware 0.2.49+ also answers a caller that asks for it (app 0.2.57+ does for its pings): the same
|
||
# status as the inbox, plus the revision of the layout on the screen. A call without a call id
|
||
# (older apps) gets no answer, so ESPHome has nothing to warn about.
|
||
- action: screen_message
|
||
supports_response: optional
|
||
variables:
|
||
message: string
|
||
then:
|
||
- lambda: 'id(dashboard_inbox).publish_state(runtime_tiles::receive(message));'
|
||
- if:
|
||
condition:
|
||
lambda: 'return call_id != 0;'
|
||
then:
|
||
- api.respond:
|
||
data: !lambda |-
|
||
root["status"] = id(dashboard_inbox).state;
|
||
root["rev"] = runtime_tiles::layout_rev;
|
||
|
||
captive_portal:
|
||
|
||
interval:
|
||
- interval: 250ms
|
||
then:
|
||
- lambda: runtime_tiles::tick();
|
||
- interval: 1s
|
||
then:
|
||
- if:
|
||
condition:
|
||
lambda: 'return id(climate_edit_pending) && !id(touch_down) && millis() - id(climate_edit_ms) >= 3000;'
|
||
then:
|
||
# The edit window is over: show what Home Assistant reports now. A closed card has
|
||
# nothing to show, and a flag left set would redraw every tile each second.
|
||
- lambda: |-
|
||
if (lv_obj_has_flag(id(climate_detail_overlay), LV_OBJ_FLAG_HIDDEN)) { id(climate_edit_pending) = false; return; }
|
||
if (!runtime_tiles::enabled) { if (!id(ui_refresh).is_running()) id(ui_refresh).execute(); return; }
|
||
// Runtime tiles: a state that arrived during the window was held back.
|
||
id(climate_edit_pending) = false;
|
||
if (runtime_tiles::detail_update && runtime_tiles::active_index >= 0 &&
|
||
runtime_tiles::active_index < (int) runtime_tiles::model.count)
|
||
runtime_tiles::detail_update(runtime_tiles::model.tiles[runtime_tiles::active_index]);
|
||
- if:
|
||
condition:
|
||
lambda: |-
|
||
if (!screen_settings::current.standby_enabled) return false;
|
||
if (id(display_dimmed) || id(touch_down) || id(calibration_active) || id(alert_active)) return false;
|
||
return (millis() - id(last_touch_ms)) > (screen_settings::current.standby_seconds * 1000UL);
|
||
then:
|
||
- script.execute: dim_display
|
||
- script.execute: back_to_page_1_when_due
|
||
|
||
time:
|
||
- platform: homeassistant
|
||
id: homeassistant_time
|
||
# Refresh immediately on time sync; do not wait for an HA entity update.
|
||
update_interval: 60s
|
||
on_time_sync:
|
||
then:
|
||
- script.execute: apply_screen_settings
|
||
- lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();'
|
||
on_time:
|
||
- seconds: 0
|
||
minutes: /1
|
||
then:
|
||
- script.execute: apply_screen_settings
|
||
- lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();'
|
||
|
||
globals:
|
||
# Alert overlay (HA action show_alert): standby waits while one is up.
|
||
- id: alert_active
|
||
type: bool
|
||
initial_value: 'false'
|
||
- id: alert_title_text
|
||
type: std::string
|
||
# The alert's colour as the automation named it (a swatch's light value, 0 for the normal card).
|
||
- id: alert_card_color
|
||
type: uint32_t
|
||
initial_value: '0'
|
||
- id: alert_timeout_seconds
|
||
type: int
|
||
initial_value: '0'
|
||
- id: alert_flash_requested
|
||
type: bool
|
||
initial_value: 'false'
|
||
- id: rendered_frames
|
||
type: uint32_t
|
||
initial_value: '0'
|
||
- id: calibration_active
|
||
type: bool
|
||
initial_value: 'false'
|
||
- id: tile_page
|
||
type: int
|
||
initial_value: '0'
|
||
- id: touch_down
|
||
type: bool
|
||
initial_value: 'false'
|
||
- id: climate_edit_ms
|
||
type: uint32_t
|
||
initial_value: '0'
|
||
- id: climate_edit_pending
|
||
type: bool
|
||
initial_value: 'false'
|
||
- id: active_entity
|
||
type: std::string
|
||
restore_value: no
|
||
|
||
- id: active_control_kind
|
||
type: std::string
|
||
restore_value: no
|
||
|
||
- id: active_value_suffix
|
||
type: std::string
|
||
restore_value: no
|
||
|
||
# Two clocks. last_touch_ms is the standby time: a touch, Wake, an alert ending and Auto standby on restart it.
|
||
# last_use_ms is Back to page 1 (firmware 0.2.56+): only a touch, or Home Assistant opening the settings page.
|
||
- id: last_touch_ms
|
||
type: uint32_t
|
||
restore_value: no
|
||
initial_value: '0'
|
||
|
||
- id: last_use_ms
|
||
type: uint32_t
|
||
restore_value: no
|
||
initial_value: '0'
|
||
|
||
# Debounce for do_tile_action: protects against repeated events from
|
||
# multiple separate press/release cycles for one physical touch (noise in
|
||
# the resistive layer), causing the same HA action to fire multiple times
|
||
# within a few hundred ms (observed: script.ambient_morning 6x in 8s).
|
||
- id: last_action_entity
|
||
type: std::string
|
||
restore_value: no
|
||
|
||
- id: last_action_ms
|
||
type: uint32_t
|
||
restore_value: no
|
||
initial_value: '0'
|
||
|
||
- id: display_dimmed
|
||
type: bool
|
||
restore_value: no
|
||
initial_value: 'false'
|
||
|
||
# Sleep from Home Assistant (0.2.45+): standby that also holds with Auto standby off,
|
||
# until a tap, Wake or an alert wakes the screen.
|
||
- id: sleep_requested
|
||
type: bool
|
||
restore_value: no
|
||
initial_value: 'false'
|
||
|
||
- id: active_label_off
|
||
type: std::string
|
||
restore_value: no
|
||
|
||
- id: active_label_on
|
||
type: std::string
|
||
restore_value: no
|
||
|
||
- id: active_title
|
||
type: std::string
|
||
restore_value: no
|
||
|
||
- id: active_icon_text
|
||
type: std::string
|
||
restore_value: no
|
||
|
||
- id: active_brightness_pct
|
||
type: int
|
||
restore_value: no
|
||
initial_value: '100'
|
||
|
||
- id: active_supports_color
|
||
type: bool
|
||
restore_value: no
|
||
initial_value: 'false'
|
||
|
||
- id: active_supports_temperature
|
||
type: bool
|
||
restore_value: no
|
||
initial_value: 'false'
|
||
|
||
- id: active_color_detail_tab
|
||
type: int
|
||
restore_value: no
|
||
initial_value: '0'
|
||
|
||
- id: active_climate_current_x10
|
||
type: int
|
||
restore_value: no
|
||
initial_value: '0'
|
||
|
||
- id: active_climate_target_x10
|
||
type: int
|
||
restore_value: no
|
||
initial_value: '200'
|
||
|
||
- id: active_climate_humidity
|
||
type: int
|
||
restore_value: no
|
||
initial_value: '-1'
|
||
|
||
- id: active_climate_min_x10
|
||
type: int
|
||
restore_value: no
|
||
initial_value: '50'
|
||
|
||
- id: active_climate_max_x10
|
||
type: int
|
||
restore_value: no
|
||
initial_value: '300'
|
||
|
||
- id: active_climate_step_x10
|
||
type: int
|
||
restore_value: no
|
||
initial_value: '5'
|
||
|
||
- id: active_climate_mode
|
||
type: std::string
|
||
restore_value: no
|
||
|
||
- id: active_climate_power_on
|
||
type: bool
|
||
restore_value: no
|
||
initial_value: 'false'
|
||
|
||
- id: active_climate_fan_modes
|
||
type: std::string
|
||
restore_value: no
|
||
|
||
- id: active_climate_fan_mode
|
||
type: std::string
|
||
restore_value: no
|
||
|
||
- id: active_climate_swing_modes
|
||
type: std::string
|
||
restore_value: no
|
||
|
||
- id: active_climate_swing_mode
|
||
type: std::string
|
||
restore_value: no
|
||
|
||
- id: active_climate_supported_modes
|
||
type: std::string
|
||
restore_value: no
|
||
|
||
# Room temperature (NAN when unknown), what the device does (hvac_action, runtime tiles only),
|
||
# and whether Home Assistant reported a setpoint at all (heat_cool ranges do not).
|
||
- id: active_climate_current
|
||
type: float
|
||
restore_value: no
|
||
initial_value: 'NAN'
|
||
|
||
- id: active_climate_action
|
||
type: std::string
|
||
restore_value: no
|
||
|
||
- id: active_climate_target_known
|
||
type: bool
|
||
restore_value: no
|
||
initial_value: 'false'
|
||
|
||
font:
|
||
# The climate card's setpoint, large enough to read across the room.
|
||
- file: "https://raw.githubusercontent.com/MaxGramser/homeassistant_espscreen/main/fonts/Roboto-400.ttf"
|
||
id: setpoint_digits
|
||
size: 64
|
||
bpp: 4
|
||
glyphs: ['-','.','°','0','1','2','3','4','5','6','7','8','9']
|
||
- file: "https://raw.githubusercontent.com/MaxGramser/homeassistant_espscreen/main/fonts/Roboto-500.ttf"
|
||
id: time_label
|
||
size: 27
|
||
bpp: 4
|
||
glyphs: ['0','1','2','3','4','5','6','7','8','9',':',' ', 'A', 'M', 'P']
|
||
|
||
- file: "https://raw.githubusercontent.com/MaxGramser/homeassistant_espscreen/main/fonts/Roboto-500.ttf"
|
||
id: headline
|
||
size: 27
|
||
bpp: 4
|
||
glyphs: ['<','>','—','&','@','!',',','.','?','"','%','(',')','+','-','_',':','°',
|
||
'0','1','2','3','4','5','6','7','8','9',
|
||
'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z',
|
||
' ','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z',
|
||
'ä','ö','ü','Ä','Ö','Ü','ß','/','·']
|
||
|
||
- file: "https://raw.githubusercontent.com/MaxGramser/homeassistant_espscreen/main/fonts/Roboto-500.ttf"
|
||
id: watch_value
|
||
size: 38
|
||
bpp: 4
|
||
glyphs: ['<','>','—','&','@','!',',','.','?','"','%','(',')','+','-','_',':','°',
|
||
'0','1','2','3','4','5','6','7','8','9',
|
||
'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z',
|
||
' ','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z',
|
||
'ä','ö','ü','Ä','Ö','Ü','ß','/','·']
|
||
|
||
# Clock card digits only; the date line reuses the tile title font.
|
||
- file: "https://raw.githubusercontent.com/MaxGramser/homeassistant_espscreen/main/fonts/Roboto-500.ttf"
|
||
id: clock_digits
|
||
size: 48
|
||
bpp: 4
|
||
glyphs: [':','-','0','1','2','3','4','5','6','7','8','9']
|
||
|
||
- file: "https://raw.githubusercontent.com/MaxGramser/homeassistant_espscreen/main/fonts/Roboto-700.ttf"
|
||
id: label
|
||
size: 18
|
||
bpp: 4
|
||
glyphs: ['<','>','—','&','@','!',',','.','?','"','%','(',')','+','-','_',':','°',
|
||
'0','1','2','3','4','5','6','7','8','9',
|
||
'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z',
|
||
' ','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z',
|
||
'ä','ö','ü','Ä','Ö','Ü','ß','/','·']
|
||
|
||
- file: "https://raw.githubusercontent.com/MaxGramser/homeassistant_espscreen/main/fonts/Roboto-400.ttf"
|
||
id: sublabel
|
||
size: 16
|
||
bpp: 4
|
||
glyphs: ['<','>','—','&','@','!',',','.','?','"','%','(',')','+','-','_',':','°',
|
||
'0','1','2','3','4','5','6','7','8','9',
|
||
'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z',
|
||
' ','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z',
|
||
'ä','ö','ü','Ä','Ö','Ü','ß','/','…','·']
|
||
|
||
- file: "https://raw.githubusercontent.com/MaxGramser/homeassistant_espscreen/main/fonts/Roboto-400.ttf"
|
||
id: sublabel_big
|
||
size: 21
|
||
bpp: 4
|
||
glyphs: ['<','>','—','&','@','!',',','.','?','"','%','(',')','+','-','_',':','°',
|
||
'0','1','2','3','4','5','6','7','8','9',
|
||
'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z',
|
||
' ','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z',
|
||
'ä','ö','ü','Ä','Ö','Ü','ß','/','…','·',"'",'#','*','=',';','²','³','µ','é','è','ë','ê','ï','î','ó','ô','à','á','â','ç','ñ','ú','û','–']
|
||
|
||
# Material Design Icons (MDI) — included at fonts/materialdesignicons-webfont.ttf (Apache 2.0)
|
||
# Source: https://github.com/Templarian/MaterialDesign-Webfont
|
||
- file: "https://raw.githubusercontent.com/MaxGramser/homeassistant_espscreen/main/fonts/materialdesignicons-webfont.ttf"
|
||
id: materialdesign_icons
|
||
size: 42
|
||
glyphs: &tile_icons # GENERATED by tools/generate_icons.py from screen_manager/app/tile_icons.py
|
||
- "\U000F0335" # lightbulb
|
||
- "\U000F1253" # lightbulb-group
|
||
- "\U000F0769" # ceiling-light
|
||
- "\U000F1793" # chandelier
|
||
- "\U000F06B5" # lamp
|
||
- "\U000F08DD" # floor-lamp
|
||
- "\U000F095F" # desk-lamp
|
||
- "\U000F091D" # wall-sconce-flat
|
||
- "\U000F179B" # light-recessed
|
||
- "\U000F04C9" # spotlight-beam
|
||
- "\U000F1051" # led-strip-variant
|
||
- "\U000F1054" # outdoor-lamp
|
||
- "\U000F02DC" # home
|
||
- "\U000F04B9" # sofa
|
||
- "\U000F02E3" # bed
|
||
- "\U000F0A70" # silverware-fork-knife
|
||
- "\U000F0B7C" # chef-hat
|
||
- "\U000F09A0" # shower
|
||
- "\U000F1818" # bathtub
|
||
- "\U000F09AB" # toilet
|
||
- "\U000F1239" # desk
|
||
- "\U000F06D9" # garage
|
||
- "\U000F0E2E" # fireplace
|
||
- "\U000F050F" # thermometer
|
||
- "\U000F0393" # thermostat
|
||
- "\U000F001B" # air-conditioner
|
||
- "\U000F0438" # radiator
|
||
- "\U000F1A43" # heat-pump
|
||
- "\U000F0238" # fire
|
||
- "\U000F0717" # snowflake
|
||
- "\U000F058E" # water-percent
|
||
- "\U000F0210" # fan
|
||
- "\U000F1797" # ceiling-fan
|
||
- "\U000F0D44" # air-purifier
|
||
- "\U000F07E4" # molecule-co2
|
||
- "\U000F0599" # weather-sunny
|
||
- "\U000F0595" # weather-partly-cloudy
|
||
- "\U000F0590" # weather-cloudy
|
||
- "\U000F0597" # weather-rainy
|
||
- "\U000F0598" # weather-snowy
|
||
- "\U000F059D" # weather-windy
|
||
- "\U000F0594" # weather-night
|
||
- "\U000F059C" # weather-sunset-up
|
||
- "\U000F0387" # music-note
|
||
- "\U000F075A" # music
|
||
- "\U000F0CB8" # playlist-music
|
||
- "\U000F0025" # album
|
||
- "\U000F099A" # record-player
|
||
- "\U000F04C7" # spotify
|
||
- "\U000F0439" # radio
|
||
- "\U000F0994" # podcast
|
||
- "\U000F04C3" # speaker
|
||
- "\U000F0D38" # speaker-multiple
|
||
- "\U000F17DB" # soundbar
|
||
- "\U000F02CB" # headphones
|
||
- "\U000F067D" # piano
|
||
- "\U000F0771" # guitar-acoustic
|
||
- "\U000F0502" # television
|
||
- "\U000F042E" # projector
|
||
- "\U000F0FCE" # movie-open
|
||
- "\U000F0118" # cast
|
||
- "\U000F0297" # gamepad-variant
|
||
- "\U000F0EC5" # remote-tv
|
||
- "\U000F033E" # lock
|
||
- "\U000F0FC6" # lock-open-variant
|
||
- "\U000F068A" # shield-home
|
||
- "\U000F078F" # alarm-light
|
||
- "\U000F009A" # bell
|
||
- "\U000F12E6" # doorbell
|
||
- "\U000F07AE" # cctv
|
||
- "\U000F0D91" # motion-sensor
|
||
- "\U000F081B" # door-closed
|
||
- "\U000F081C" # door-open
|
||
- "\U000F11DB" # window-closed-variant
|
||
- "\U000F0392" # smoke-detector
|
||
- "\U000F1502" # water-alert
|
||
- "\U000F072A" # washing-machine
|
||
- "\U000F0917" # tumble-dryer
|
||
- "\U000F0AAC" # dishwasher
|
||
- "\U000F0290" # fridge
|
||
- "\U000F04DE" # stove
|
||
- "\U000F0C99" # microwave
|
||
- "\U000F0CD3" # toaster-oven
|
||
- "\U000F109F" # coffee-maker
|
||
- "\U000F05FA" # kettle
|
||
- "\U000F070D" # robot-vacuum
|
||
- "\U000F11F7" # robot-mower
|
||
- "\U000F042B" # printer-3d
|
||
- "\U000F06A5" # power-plug
|
||
- "\U000F07E7" # power-socket-eu
|
||
- "\U000F0322" # laptop
|
||
- "\U000F0469" # router-wireless
|
||
- "\U000F0241" # flash
|
||
- "\U000F0A72" # solar-power
|
||
- "\U000F0D9B" # solar-panel
|
||
- "\U000F1901" # home-battery
|
||
- "\U000F12A3" # battery-high
|
||
- "\U000F05F1" # ev-station
|
||
- "\U000F0B6C" # car-electric
|
||
- "\U000F1A57" # meter-electric
|
||
- "\U000F1A59" # meter-gas
|
||
- "\U000F058C" # water
|
||
- "\U000F0F92" # water-boiler
|
||
- "\U000F029A" # gauge
|
||
- "\U000F111C" # window-shutter
|
||
- "\U000F111E" # window-shutter-open
|
||
- "\U000F00AC" # blinds
|
||
- "\U000F1011" # blinds-open
|
||
- "\U000F1A6C" # roller-shade-closed
|
||
- "\U000F1846" # curtains
|
||
- "\U000F1847" # curtains-closed
|
||
- "\U000F1B88" # awning-outline
|
||
- "\U000F024A" # flower
|
||
- "\U000F0531" # tree
|
||
- "\U000F0E66" # sprout
|
||
- "\U000F1510" # grass
|
||
- "\U000F105F" # sprinkler
|
||
- "\U000F0606" # pool
|
||
- "\U000F0E45" # grill
|
||
- "\U000F06EE" # mailbox
|
||
- "\U000F0A79" # trash-can
|
||
- "\U000F044C" # recycle
|
||
- "\U000F0A43" # dog
|
||
- "\U000F011B" # cat
|
||
- "\U000F0004" # account
|
||
- "\U000F0849" # account-group
|
||
- "\U000F0A89" # account-child
|
||
- "\U000F0826" # home-account
|
||
- "\U000F04B2" # sleep
|
||
- "\U000F070E" # run
|
||
- "\U000F00D6" # briefcase
|
||
- "\U000F010B" # car
|
||
- "\U000F00A3" # bike
|
||
- "\U000F001D" # airplane
|
||
- "\U000F034E" # map-marker
|
||
- "\U000F0425" # power
|
||
- "\U000F0521" # toggle-switch
|
||
- "\U000F12A8" # gesture-tap-button
|
||
- "\U000F040A" # play
|
||
- "\U000F0BC2" # script-text
|
||
- "\U000F03D8" # palette
|
||
- "\U000F0020" # alarm
|
||
- "\U000F0150" # clock-outline
|
||
- "\U000F051B" # timer-outline
|
||
- "\U000F00ED" # calendar
|
||
- "\U000F009E" # bell-ring
|
||
- "\U000F02D1" # heart
|
||
- "\U000F002A" # alert-outline
|
||
- "\U000F0493" # cog
|
||
- "\U000F00E2" # broom
|
||
- "\U000F05D6" # alert-circle-outline
|
||
- "\U000F012C" # check
|
||
- "\U000F03E4" # pause
|
||
- "\U000F04DB" # stop
|
||
- "\U000F04AD" # skip-next
|
||
- "\U000F04AE" # skip-previous
|
||
- "\U000F057E" # volume-high
|
||
- "\U000F0581" # volume-off
|
||
- "\U000F005D" # arrow-up
|
||
- "\U000F0045" # arrow-down
|
||
- "\U000F084E" # arrow-expand-horizontal
|
||
- "\U000F084C" # arrow-collapse-horizontal
|
||
- "\U000F05F8" # home-map-marker
|
||
- "\U000F0415" # plus
|
||
- "\U000F0374" # minus
|
||
- "\U000F0141" # chevron-left
|
||
- "\U000F0142" # chevron-right
|
||
- "\U000F0156" # close
|
||
- "\U000F004D" # arrow-left
|
||
- "\U000F1A79" # sun-snowflake-variant
|
||
- "\U000F1B17" # thermostat-auto
|
||
- "\U000F0591" # weather-fog
|
||
- "\U000F0592" # weather-hail
|
||
- "\U000F0593" # weather-lightning
|
||
- "\U000F067E" # weather-lightning-rainy
|
||
- "\U000F0596" # weather-pouring
|
||
- "\U000F067F" # weather-snowy-rainy
|
||
- "\U000F059B" # weather-sunset-down
|
||
- "\U000F01D8" # dots-horizontal
|
||
- "\U000F1C91" # arrow-oscillating
|
||
- "\U000F0379" # monitor
|
||
- "\U000F02FD" # information-outline
|
||
- "\U000F0709" # restart
|
||
- "\U000F0E4F" # lightbulb-off
|
||
|
||
- file: "https://raw.githubusercontent.com/MaxGramser/homeassistant_espscreen/main/fonts/materialdesignicons-webfont.ttf"
|
||
id: materialdesign_icons_mini
|
||
size: 26
|
||
glyphs: *tile_icons
|
||
|
||
- file: "https://raw.githubusercontent.com/MaxGramser/homeassistant_espscreen/main/fonts/materialdesignicons-webfont.ttf"
|
||
id: watch_icon
|
||
size: 18
|
||
glyphs: *tile_icons
|
||
|
||
# Make memory visible: this board has no PSRAM, so the free heap determines
|
||
# how much UI still fits. Also shows up as an entity in Home Assistant.
|
||
debug:
|
||
# Heap figures every five minutes: enough to spot a leak, few recorder rows.
|
||
update_interval: 300s
|
||
|
||
sensor:
|
||
- platform: debug
|
||
free:
|
||
name: "Heap Free"
|
||
id: heap_free
|
||
on_value:
|
||
- lambda: 'ESP_LOGI("health", "free_heap=%.0f", x);'
|
||
fragmentation:
|
||
name: "Heap Fragmentation"
|
||
block:
|
||
name: "Heap Largest Block"
|
||
on_value:
|
||
- lambda: 'ESP_LOGI("health", "largest_block=%.0f", x);'
|
||
# The lowest free heap since boot: what the busiest moment (boot sync, a swipe) left over.
|
||
min_free:
|
||
name: "Heap Minimum Free"
|
||
on_value:
|
||
- lambda: 'ESP_LOGI("health", "min_free=%.0f", x);'
|
||
- platform: wifi_signal
|
||
name: "Wifi Signal"
|
||
update_interval: 600s
|
||
|
||
# When the screen booted, as a timestamp: one value per boot instead of a new recorder
|
||
# row every 15 s. A new entity on purpose: Home Assistant keeps the old Uptime's unit
|
||
# "s" in its registry and would refuse a timestamp under that name.
|
||
- platform: uptime
|
||
type: timestamp
|
||
name: "Last boot"
|
||
id: boot_time
|
||
|
||
|
||
binary_sensor:
|
||
- platform: status
|
||
name: "Node Status"
|
||
id: system_status
|
||
|
||
# ----------------------------------------------------------
|
||
# Text sensors
|
||
#
|
||
# smartdisplay_action:
|
||
# - Main touch event channel exported to Home Assistant
|
||
# - On short tap, publishes one of:
|
||
# tile1_press / tile2_press / tile3_press / tile4_press / tile5_press / tile6_press
|
||
# - The value is reset shortly after publishing
|
||
#
|
||
# tile*_preset:
|
||
# - Mirrors the Home Assistant preset_mode attribute for each tile entity
|
||
# - Used by the UI to display fan presets when applicable
|
||
# ----------------------------------------------------------
|
||
text_sensor:
|
||
- platform: template
|
||
name: "Guition screen type"
|
||
entity_category: diagnostic
|
||
update_interval: never
|
||
lambda: 'return {"guition"};'
|
||
- platform: template
|
||
name: "Screen firmware"
|
||
entity_category: diagnostic
|
||
lambda: 'return {"${SCREEN_FIRMWARE_VERSION}"};'
|
||
update_interval: 60s
|
||
# ESP Screen Manager matches the screen to its YAML profile and OTA address.
|
||
- platform: template
|
||
name: "Device name"
|
||
entity_category: diagnostic
|
||
lambda: 'return {"${DEVICE_NAME}"};'
|
||
update_interval: 60s
|
||
- platform: wifi_info
|
||
ip_address:
|
||
# `id` so the settings page on the screen can show the same address (0.2.44+).
|
||
id: wifi_ip
|
||
name: "IP address"
|
||
entity_category: diagnostic
|
||
|
||
switch:
|
||
- platform: restart
|
||
name: "Restart"
|
||
# The screen settings as Home Assistant entities (firmware 0.2.49+, Auto standby since 0.2.41). The screen
|
||
# owns them: the settings page, these entities and ESP Screens all change them through
|
||
# settings_screen::set(), which saves, applies and reports a real change only. The saved values keep
|
||
# their preference records. A template switch reads its lambda every loop and publishes a change itself;
|
||
# the numbers and times are published by apply_screen_settings. Auto standby off wakes a screen the
|
||
# standby time dimmed (a Sleep holds until a tap, Wake or an alert); on counts the standby time from that
|
||
# moment. Nothing runs at boot: every switch follows the saved setting.
|
||
- platform: template
|
||
id: setting_auto_standby
|
||
name: "Auto standby"
|
||
icon: "mdi:sleep"
|
||
entity_category: config
|
||
restore_mode: DISABLED
|
||
lambda: return screen_settings::current.standby_enabled;
|
||
turn_on_action:
|
||
- lambda: |-
|
||
id(last_touch_ms) = millis();
|
||
settings_screen::set("standby_enabled", 1);
|
||
turn_off_action:
|
||
- lambda: 'settings_screen::set("standby_enabled", 0);'
|
||
- platform: template
|
||
id: setting_night_mode
|
||
name: "Night mode"
|
||
icon: "mdi:weather-night"
|
||
entity_category: config
|
||
restore_mode: DISABLED
|
||
lambda: return screen_settings::current.night_enabled;
|
||
turn_on_action:
|
||
- lambda: 'settings_screen::set("night_enabled", 1);'
|
||
turn_off_action:
|
||
- lambda: 'settings_screen::set("night_enabled", 0);'
|
||
# The dark look for a screen beside a bed (firmware 0.2.54+): black page, graphite cards, soft white text.
|
||
- platform: template
|
||
id: setting_dark_mode
|
||
name: "Dark mode"
|
||
icon: "mdi:theme-light-dark"
|
||
entity_category: config
|
||
restore_mode: DISABLED
|
||
lambda: return settings_screen::dark_mode != 0;
|
||
turn_on_action:
|
||
- lambda: 'settings_screen::set("dark_mode", 1);'
|
||
turn_off_action:
|
||
- lambda: 'settings_screen::set("dark_mode", 0);'
|
||
- platform: template
|
||
id: setting_clock_24h
|
||
name: "24-hour clock"
|
||
icon: "mdi:clock-outline"
|
||
entity_category: config
|
||
restore_mode: DISABLED
|
||
lambda: return screen_settings::current.clock_24h;
|
||
turn_on_action:
|
||
- lambda: 'settings_screen::set("clock_24h", 1);'
|
||
turn_off_action:
|
||
- lambda: 'settings_screen::set("clock_24h", 0);'
|
||
- platform: template
|
||
id: setting_auto_home
|
||
name: "Back to page 1"
|
||
icon: "mdi:home-import-outline"
|
||
entity_category: config
|
||
restore_mode: DISABLED
|
||
lambda: return settings_screen::auto_home != 0;
|
||
turn_on_action:
|
||
- lambda: 'settings_screen::set("auto_home", 1);'
|
||
turn_off_action:
|
||
- lambda: 'settings_screen::set("auto_home", 0);'
|
||
- platform: template
|
||
id: setting_home_on_standby
|
||
name: "Back to page 1 on standby"
|
||
icon: "mdi:home-import-outline"
|
||
entity_category: config
|
||
restore_mode: DISABLED
|
||
lambda: return screen_settings::current.home_on_standby;
|
||
turn_on_action:
|
||
- lambda: 'settings_screen::set("home_on_standby", 1);'
|
||
turn_off_action:
|
||
- lambda: 'settings_screen::set("home_on_standby", 0);'
|
||
- platform: template
|
||
id: setting_swipe_pages
|
||
name: "Swipe between pages"
|
||
icon: "mdi:gesture-swipe-horizontal"
|
||
entity_category: config
|
||
restore_mode: DISABLED
|
||
lambda: return settings_screen::swipe_pages != 0;
|
||
turn_on_action:
|
||
- lambda: 'settings_screen::set("swipe_pages", 1);'
|
||
turn_off_action:
|
||
- lambda: 'settings_screen::set("swipe_pages", 0);'
|
||
|
||
# Night hours as Home Assistant times (firmware 0.2.49+); the screen keeps minutes since midnight.
|
||
datetime:
|
||
- platform: template
|
||
type: time
|
||
id: setting_night_start
|
||
name: "Night starts"
|
||
icon: "mdi:weather-sunset-down"
|
||
entity_category: config
|
||
update_interval: never
|
||
lambda: |-
|
||
ESPTime time{};
|
||
time.hour = screen_settings::current.night_start / 60;
|
||
time.minute = screen_settings::current.night_start % 60;
|
||
return time;
|
||
set_action:
|
||
- lambda: 'settings_screen::set("night_start", x.hour * 60 + x.minute);'
|
||
- platform: template
|
||
type: time
|
||
id: setting_night_end
|
||
name: "Night ends"
|
||
icon: "mdi:weather-sunset-up"
|
||
entity_category: config
|
||
update_interval: never
|
||
lambda: |-
|
||
ESPTime time{};
|
||
time.hour = screen_settings::current.night_end / 60;
|
||
time.minute = screen_settings::current.night_end % 60;
|
||
return time;
|
||
set_action:
|
||
- lambda: 'settings_screen::set("night_end", x.hour * 60 + x.minute);'
|
||
|
||
# Rotation as a Home Assistant choice (firmware 0.2.49+). Only the Guition turns; the CYD keeps its orientation.
|
||
select:
|
||
- platform: template
|
||
id: setting_rotation
|
||
name: "Rotation"
|
||
icon: "mdi:screen-rotation"
|
||
entity_category: config
|
||
options: ["0°", "90°", "180°", "270°"]
|
||
update_interval: never
|
||
lambda: |-
|
||
static const char *const names[] = {"0°", "90°", "180°", "270°"};
|
||
return std::string(names[std::clamp<int32_t>(settings_screen::rotation, 0, 270) / 90]);
|
||
set_action:
|
||
- lambda: |-
|
||
const std::string option(x.c_str(), x.size());
|
||
settings_screen::set("rotation", atoi(option.c_str()));
|
||
|
||
# Two SPI buses used by the CYD:
|
||
# - lcd: built-in display
|
||
# - my_touchscreen: built-in touch controller
|
||
# Pinout from ESPHome's GUITION-4848S040 preset. No relay outputs are configured.
|
||
spi:
|
||
id: lcd
|
||
clk_pin: GPIO48
|
||
mosi_pin: GPIO47
|
||
|
||
i2c:
|
||
id: touch_bus
|
||
sda: GPIO19
|
||
scl:
|
||
number: GPIO45
|
||
ignore_strapping_warning: true
|
||
scan: true
|
||
|
||
output:
|
||
- platform: ledc
|
||
pin: GPIO38
|
||
id: gpio_backlight_pwm
|
||
frequency: 20000Hz
|
||
|
||
light:
|
||
# The firmware owns the backlight: brightness, standby, night and alerts set it, and the minute tick
|
||
# restores it. Home Assistant uses the brightness numbers and the Wake and Sleep buttons (0.2.46+).
|
||
- platform: monochromatic
|
||
output: gpio_backlight_pwm
|
||
name: "Display Backlight"
|
||
id: back_light
|
||
internal: true
|
||
restore_mode: ALWAYS_ON
|
||
default_transition_length: 150ms
|
||
|
||
touchscreen:
|
||
platform: gt911
|
||
id: ts_touch
|
||
i2c_id: touch_bus
|
||
display: my_display
|
||
update_interval: 20ms
|
||
transform:
|
||
swap_xy: ${TOUCH_SWAP_XY}
|
||
mirror_x: ${TOUCH_MIRROR_X}
|
||
mirror_y: ${TOUCH_MIRROR_Y}
|
||
on_touch:
|
||
- lambda: |-
|
||
id(last_touch_ms) = millis();
|
||
id(last_use_ms) = millis();
|
||
id(touch_down) = true;
|
||
cyd::touch_guard.begin(millis(), touch.x, touch.y, touch.id);
|
||
cyd::edge_swipe.begin(touch.x, touch.y, static_cast<int>(id(screen_lvgl).get_rotation()));
|
||
ESP_LOGI("touch", "GT911 press x=%u y=%u id=%u test=%d", touch.x, touch.y, (unsigned) touch.id, id(calibration_active));
|
||
on_update:
|
||
- lambda: |-
|
||
id(last_touch_ms) = millis();
|
||
id(last_use_ms) = millis();
|
||
// Only the contact that started the touch counts; the guard ignores other ids.
|
||
for (const auto &point : touches) {
|
||
// Trace every sample of an edge touch: the log then shows how often the panel delivers.
|
||
if (cyd::edge_swipe.armed())
|
||
ESP_LOGI("touch", "swipe id=%u st=%u x=%u y=%u", (unsigned) point.id, (unsigned) point.state, point.x, point.y);
|
||
// The stray (0, 0) contact (cyd::GhostTouch) is not where the finger went.
|
||
if (point.x == 0 && point.y == 0) continue;
|
||
cyd::touch_guard.update(point.x, point.y, point.id);
|
||
if (point.id != cyd::touch_guard.contact()) continue;
|
||
// Swiping in from a side edge flips the page ("Swiping between pages"); the tap
|
||
// under the finger is consumed and LVGL waits for the release. LVGL 9.5 sends no
|
||
// PRESSING to the input device, hence the touchscreen trigger.
|
||
const int step = cyd::edge_swipe.update(point.x, point.y);
|
||
if (!step) continue;
|
||
const char *blocked = !runtime_tiles::enabled ? "no runtime tiles" : !runtime_tiles::swipe_pages ? "setting off"
|
||
: id(display_dimmed) ? "screen dimmed" : id(calibration_active) ? "calibration" : !id(active_entity).empty() ? "card open"
|
||
: (runtime_tiles::detail_root && !lv_obj_has_flag(runtime_tiles::detail_root, LV_OBJ_FLAG_HIDDEN)) ? "detail card open" : nullptr;
|
||
if (blocked) { ESP_LOGI("touch", "edge swipe ignored: %s", blocked); continue; }
|
||
cyd::touch_guard.consume();
|
||
for (auto *indev = lv_indev_get_next(nullptr); indev; indev = lv_indev_get_next(indev)) lv_indev_wait_release(indev);
|
||
ESP_LOGI("touch", "edge swipe: page %d -> %d", id(tile_page), id(tile_page) + step);
|
||
id(tile_page) += step;
|
||
id(show_tile_page).execute();
|
||
}
|
||
on_release:
|
||
- lambda: |-
|
||
id(last_touch_ms) = millis();
|
||
id(last_use_ms) = millis();
|
||
id(touch_down) = false;
|
||
if (cyd::edge_swipe.armed() && cyd::edge_swipe.inward() > 0)
|
||
ESP_LOGI("touch", "edge swipe not fired: %d px inward, %d px vertical", cyd::edge_swipe.inward(), cyd::edge_swipe.sideways());
|
||
cyd::edge_swipe.end();
|
||
|
||
|
||
# Native ESPHome driver with the working manufacturer's LVGL demo timings.
|
||
# See docs/GUITION_FACTORY_REFERENCE.md; no custom RGB component is required.
|
||
display:
|
||
- platform: st7701s
|
||
id: my_display
|
||
dimensions: {width: 480, height: 480}
|
||
spi_id: lcd
|
||
spi_mode: MODE0
|
||
data_rate: 2MHz
|
||
color_order: RGB
|
||
invert_colors: false
|
||
cs_pin: 39
|
||
de_pin: 18
|
||
hsync_pin: 16
|
||
vsync_pin: 17
|
||
pclk_pin: 21
|
||
pclk_frequency: 16MHz
|
||
pclk_inverted: false
|
||
hsync_front_porch: 10
|
||
hsync_pulse_width: 8
|
||
hsync_back_porch: 50
|
||
vsync_front_porch: 10
|
||
vsync_pulse_width: 8
|
||
vsync_back_porch: 20
|
||
transform:
|
||
mirror_x: false
|
||
mirror_y: false
|
||
init_sequence:
|
||
- 1
|
||
# Manufacturer's 16-bit bus configuration and normal scan direction.
|
||
- [0xFF, 0x77, 0x01, 0x00, 0x00, 0x10]
|
||
- [0xCD, 0x00]
|
||
- [0xC7, 0x00]
|
||
# Return to the standard register bank before pixel format / sleep-out.
|
||
- [0xFF, 0x77, 0x01, 0x00, 0x00, 0x00]
|
||
- [0x3A, 0x60]
|
||
- [0x36, 0x00]
|
||
- [0x20]
|
||
- [0x11]
|
||
- delay 120ms
|
||
data_pins:
|
||
red: [11, 12, 13, 14, 0]
|
||
green: [8, 20, 3, 46, 9, 10]
|
||
blue: [4, 5, 6, 7, 15]
|
||
update_interval: never
|
||
auto_clear_enabled: false
|
||
|
||
|
||
lvgl:
|
||
id: screen_lvgl
|
||
on_draw_start:
|
||
- lambda: 'id(rendered_frames)++;'
|
||
displays:
|
||
- my_display
|
||
touchscreens:
|
||
- touchscreen_id: ts_touch
|
||
|
||
# S3 octal PSRAM holds RGB framebuffer and LVGL draw buffers.
|
||
buffer_size: 25%
|
||
|
||
# Direct local feedback on the WHOLE UI, not just the tiles.
|
||
# A theme sets the style per widget type, so this covers all
|
||
# buttons, tabs, sliders and the thermostat ring in one go. Non-clickable
|
||
# widgets never enter the pressed state, so those stay untouched.
|
||
theme:
|
||
obj:
|
||
pressed:
|
||
# bg_opa and not opa: opa would also fade the icon and text. Styles that
|
||
# make LVGL draw into a buffer of its own (opa_layered, rotation/scale,
|
||
# clip_corner, a slider fill rounded less than its track) cost KB's per
|
||
# redraw that the CYD's heap can't spare; tests/test_layer_free.py.
|
||
bg_opa: 45%
|
||
slider:
|
||
knob:
|
||
pressed:
|
||
bg_opa: 65%
|
||
arc:
|
||
knob:
|
||
pressed:
|
||
bg_opa: 65%
|
||
|
||
# LVGL owns the rotation (display + touch) since ESPHome 2026.4.
|
||
rotation: ${LVGL_ROTATION}
|
||
|
||
style_definitions:
|
||
# The page behind the tiles and the cards: one colour from the theme (theme.h PAGE), no wallpaper and no
|
||
# gradient, so a redraw is a plain fill.
|
||
- id: style_page
|
||
bg_opa: COVER
|
||
border_width: 0
|
||
radius: 0
|
||
|
||
- id: style_tile
|
||
bg_opa: COVER
|
||
radius: 22
|
||
border_width: 1
|
||
border_opa: COVER
|
||
pad_left: 12
|
||
pad_right: 12
|
||
pad_top: 12
|
||
pad_bottom: 12
|
||
shadow_width: 0
|
||
shadow_opa: 40%
|
||
|
||
- id: style_icon_circle
|
||
bg_opa: COVER
|
||
radius: 999
|
||
border_width: 0
|
||
|
||
- id: style_room
|
||
text_font: headline
|
||
text_opa: 100%
|
||
|
||
- id: style_time
|
||
text_font: time_label
|
||
text_opa: 100%
|
||
|
||
- id: style_title
|
||
text_font: label
|
||
text_opa: 100%
|
||
|
||
- id: style_value
|
||
text_font: sublabel
|
||
text_opa: 100%
|
||
|
||
- id: style_value_big
|
||
text_font: sublabel_big
|
||
text_opa: 80%
|
||
|
||
# Paints: shared styles without a colour of their own. theme.h fills them (the list in on_boot, theme::paints)
|
||
# and fills them again when Dark mode changes, so a widget names a paint where it used to write a colour.
|
||
- id: paint_page_soft
|
||
- id: paint_page_pressed
|
||
- id: paint_card
|
||
- id: paint_raised
|
||
- id: paint_key
|
||
- id: paint_key_pressed
|
||
- id: paint_track
|
||
- id: paint_ink
|
||
- id: paint_ink_soft
|
||
- id: paint_muted
|
||
- id: paint_subtle
|
||
- id: paint_slate
|
||
- id: paint_scrim
|
||
- id: paint_veil
|
||
- id: paint_accent_tint
|
||
- id: paint_accent_bright
|
||
- id: paint_button_dark
|
||
- id: paint_button_dark_pressed
|
||
|
||
# ---------- ALERT OVERLAY (HA action show_alert) ----------
|
||
# LVGL's top layer sits above every page, card and the dim overlay; the card
|
||
# is filled by the alert_show script. Fixed positions, no layout engine.
|
||
top_layer:
|
||
widgets:
|
||
- obj:
|
||
id: alert_overlay
|
||
x: 0
|
||
y: 0
|
||
width: ${DISPLAY_W}
|
||
height: ${DISPLAY_H}
|
||
hidden: true
|
||
styles: paint_scrim
|
||
bg_opa: 60%
|
||
radius: 0
|
||
border_width: 0
|
||
pad_all: 0
|
||
clickable: true
|
||
scrollable: false
|
||
scrollbar_mode: 'OFF'
|
||
# Same look when pressed: a tap next to the card must not repaint the screen.
|
||
pressed:
|
||
bg_opa: 60%
|
||
widgets:
|
||
- obj:
|
||
id: alert_card
|
||
align: CENTER
|
||
width: ${ALERT_CARD_W}
|
||
height: ${ALERT_CARD_H}
|
||
styles: paint_raised
|
||
bg_opa: COVER
|
||
radius: 18
|
||
border_width: 1
|
||
border_opa: COVER
|
||
shadow_width: 0
|
||
pad_all: 0
|
||
clickable: false
|
||
scrollable: false
|
||
scrollbar_mode: 'OFF'
|
||
widgets:
|
||
- label:
|
||
id: alert_icon
|
||
x: ${ALERT_ICON_X}
|
||
y: ${ALERT_ICON_Y}
|
||
text: "\U000F002A"
|
||
text_font: materialdesign_icons
|
||
clickable: false
|
||
- label:
|
||
id: alert_title
|
||
x: ${ALERT_TEXT_X}
|
||
y: ${ALERT_TITLE_Y}
|
||
width: ${ALERT_TEXT_W}
|
||
height: ${ALERT_TITLE_H}
|
||
long_mode: DOT
|
||
text: "Notification"
|
||
text_font: headline
|
||
clickable: false
|
||
- label:
|
||
id: alert_subtitle
|
||
x: ${ALERT_TEXT_X}
|
||
y: ${ALERT_SUBTITLE_Y}
|
||
width: ${ALERT_TEXT_W}
|
||
height: ${ALERT_SUBTITLE_H}
|
||
long_mode: WRAP
|
||
text: ""
|
||
text_font: sublabel_big
|
||
styles: paint_ink_soft
|
||
clickable: false
|
||
- obj:
|
||
id: alert_ok
|
||
align: BOTTOM_RIGHT
|
||
x: -${ALERT_BUTTON_INSET}
|
||
y: -${ALERT_BUTTON_INSET}
|
||
width: ${ALERT_BUTTON_W}
|
||
height: ${ALERT_BUTTON_H}
|
||
radius: 999
|
||
styles: paint_button_dark
|
||
bg_opa: COVER
|
||
border_width: 0
|
||
shadow_width: 0
|
||
pad_all: 0
|
||
clickable: true
|
||
scrollable: false
|
||
pressed:
|
||
styles: paint_button_dark_pressed
|
||
bg_opa: COVER
|
||
on_click:
|
||
- if:
|
||
condition:
|
||
lambda: 'return cyd::touch_guard.accept(millis(), 13);'
|
||
then:
|
||
- script.execute:
|
||
id: alert_dismiss
|
||
reason: "ok"
|
||
widgets:
|
||
- label:
|
||
id: alert_ok_label
|
||
align: CENTER
|
||
text: "OK"
|
||
text_font: headline
|
||
clickable: false
|
||
|
||
pages:
|
||
- id: home_page
|
||
styles: style_page
|
||
pad_all: 0
|
||
scrollable: false
|
||
scrollbar_mode: 'OFF'
|
||
widgets:
|
||
# Never shown: it only makes ESPHome compile LVGL's spinner, which the busy sheet on a tile draws.
|
||
- spinner:
|
||
id: busy_spinner_seed
|
||
hidden: true
|
||
width: 2
|
||
height: 2
|
||
spin_time: 1s
|
||
arc_length: 60deg
|
||
- label:
|
||
id: lbl_room
|
||
x: 16
|
||
y: ${HEADER_Y}
|
||
text: "${ROOM_NAME}"
|
||
styles: style_room
|
||
|
||
# Enables LVGL's line widget for the runtime clock hands and sparklines.
|
||
- line:
|
||
id: runtime_line_enable
|
||
hidden: true
|
||
points:
|
||
- 0, 0
|
||
- 1, 1
|
||
|
||
- label:
|
||
id: lbl_time
|
||
align: TOP_RIGHT
|
||
x: -16
|
||
y: ${HEADER_Y}
|
||
text: ""
|
||
styles: style_time
|
||
|
||
# Six tiles on page 1, four on page 2. Fixed pages keep navigation predictable.
|
||
- obj:
|
||
id: tile_scroll
|
||
x: 0
|
||
y: ${SCROLL_Y}
|
||
width: ${DISPLAY_W}
|
||
height: ${SCROLL_H}
|
||
bg_opa: TRANSP
|
||
# A tap between two tiles presses this area: without its own pressed style the
|
||
# theme's 45 % lights it up as a pale panel behind the tiles.
|
||
pressed:
|
||
bg_opa: TRANSP
|
||
border_width: 0
|
||
pad_left: 0
|
||
pad_right: 0
|
||
pad_top: 0
|
||
pad_bottom: 0
|
||
scrollable: false
|
||
scrollbar_mode: 'OFF'
|
||
widgets:
|
||
# ---------- TILE 1 ----------
|
||
- obj:
|
||
id: tile1
|
||
x: ${GRID_COL1_X}
|
||
y: ${GRID_ROW1_Y}
|
||
width: ${TILE_W}
|
||
height: ${TILE_H}
|
||
styles: style_tile
|
||
scrollable: false
|
||
scrollbar_mode: 'OFF'
|
||
# Direct local feedback: LVGL applies this as soon as the finger
|
||
# touches the glass, in the same frame. Independent of Home Assistant.
|
||
pressed:
|
||
bg_opa: 45%
|
||
widgets:
|
||
- obj:
|
||
id: tile1_icon_circle
|
||
x: ${TILE_ICON_X}
|
||
y: ${TILE_ICON_Y}
|
||
width: 54
|
||
height: 54
|
||
styles: style_icon_circle
|
||
clickable: false
|
||
scrollable: false
|
||
scrollbar_mode: 'OFF'
|
||
widgets:
|
||
- label:
|
||
id: tile1_icon_lbl
|
||
align: CENTER
|
||
text_font: materialdesign_icons
|
||
text: "\U000F001B"
|
||
clickable: false
|
||
- label:
|
||
id: t1_title
|
||
height: 24
|
||
width: 130
|
||
long_mode: DOT
|
||
x: ${TILE_TITLE_X}
|
||
y: ${TILE_TITLE_Y}
|
||
text: "Lamp"
|
||
styles: style_title
|
||
clickable: false
|
||
- label:
|
||
id: t1_value
|
||
height: 22
|
||
width: 130
|
||
long_mode: DOT
|
||
x: ${TILE_VALUE_X}
|
||
y: ${TILE_VALUE_Y}
|
||
text: "—"
|
||
styles: style_value
|
||
clickable: false
|
||
|
||
# ---------- TILE 2 ----------
|
||
- obj:
|
||
id: tile2
|
||
x: ${GRID_COL2_X}
|
||
y: ${GRID_ROW1_Y}
|
||
width: ${TILE_W}
|
||
height: ${TILE_H}
|
||
styles: style_tile
|
||
scrollable: false
|
||
scrollbar_mode: 'OFF'
|
||
# Direct local feedback: LVGL applies this as soon as the finger
|
||
# touches the glass, in the same frame. Independent of Home Assistant.
|
||
pressed:
|
||
bg_opa: 45%
|
||
widgets:
|
||
- obj:
|
||
id: tile2_icon_circle
|
||
x: ${TILE_ICON_X}
|
||
y: ${TILE_ICON_Y}
|
||
width: 54
|
||
height: 54
|
||
styles: style_icon_circle
|
||
clickable: false
|
||
scrollable: false
|
||
scrollbar_mode: 'OFF'
|
||
widgets:
|
||
- label:
|
||
id: tile2_icon_lbl
|
||
align: CENTER
|
||
text_font: materialdesign_icons
|
||
text: "\U000F0387"
|
||
clickable: false
|
||
- label:
|
||
id: t2_title
|
||
height: 24
|
||
width: 130
|
||
long_mode: DOT
|
||
x: ${TILE_TITLE_X}
|
||
y: ${TILE_TITLE_Y}
|
||
text: "Plug"
|
||
styles: style_title
|
||
clickable: false
|
||
- label:
|
||
id: t2_value
|
||
height: 22
|
||
width: 130
|
||
long_mode: DOT
|
||
x: ${TILE_VALUE_X}
|
||
y: ${TILE_VALUE_Y}
|
||
text: "—"
|
||
styles: style_value
|
||
clickable: false
|
||
|
||
# ---------- TILE 3 ----------
|
||
- obj:
|
||
id: tile3
|
||
x: ${GRID_COL1_X}
|
||
y: ${GRID_ROW2_Y}
|
||
width: ${TILE_W}
|
||
height: ${TILE_H}
|
||
styles: style_tile
|
||
scrollable: false
|
||
scrollbar_mode: 'OFF'
|
||
# Direct local feedback: LVGL applies this as soon as the finger
|
||
# touches the glass, in the same frame. Independent of Home Assistant.
|
||
pressed:
|
||
bg_opa: 45%
|
||
widgets:
|
||
- obj:
|
||
id: tile3_icon_circle
|
||
x: ${TILE_ICON_X}
|
||
y: ${TILE_ICON_Y}
|
||
width: 54
|
||
height: 54
|
||
styles: style_icon_circle
|
||
clickable: false
|
||
scrollable: false
|
||
scrollbar_mode: 'OFF'
|
||
widgets:
|
||
- label:
|
||
id: tile3_icon_lbl
|
||
align: CENTER
|
||
text_font: materialdesign_icons
|
||
text: "\U000F02E3"
|
||
clickable: false
|
||
- label:
|
||
id: t3_title
|
||
height: 24
|
||
width: 130
|
||
long_mode: DOT
|
||
x: ${TILE_TITLE_X}
|
||
y: ${TILE_TITLE_Y}
|
||
text: "Evening"
|
||
styles: style_title
|
||
clickable: false
|
||
- label:
|
||
id: t3_value
|
||
height: 22
|
||
width: 130
|
||
long_mode: DOT
|
||
x: ${TILE_VALUE_X}
|
||
y: ${TILE_VALUE_Y}
|
||
text: "—"
|
||
styles: style_value
|
||
clickable: false
|
||
|
||
# ---------- TILE 4 ----------
|
||
- obj:
|
||
id: tile4
|
||
x: ${GRID_COL2_X}
|
||
y: ${GRID_ROW2_Y}
|
||
width: ${TILE_W}
|
||
height: ${TILE_H}
|
||
styles: style_tile
|
||
scrollable: false
|
||
scrollbar_mode: 'OFF'
|
||
# Direct local feedback: LVGL applies this as soon as the finger
|
||
# touches the glass, in the same frame. Independent of Home Assistant.
|
||
pressed:
|
||
bg_opa: 45%
|
||
widgets:
|
||
- obj:
|
||
id: tile4_icon_circle
|
||
x: ${TILE_ICON_X}
|
||
y: ${TILE_ICON_Y}
|
||
width: 54
|
||
height: 54
|
||
styles: style_icon_circle
|
||
clickable: false
|
||
scrollable: false
|
||
scrollbar_mode: 'OFF'
|
||
widgets:
|
||
- label:
|
||
id: tile4_icon_lbl
|
||
align: CENTER
|
||
text_font: materialdesign_icons
|
||
text: "\U000F059C"
|
||
clickable: false
|
||
- label:
|
||
id: t4_title
|
||
height: 24
|
||
width: 130
|
||
long_mode: DOT
|
||
x: ${TILE_TITLE_X}
|
||
y: ${TILE_TITLE_Y}
|
||
text: "All off"
|
||
styles: style_title
|
||
clickable: false
|
||
- label:
|
||
id: t4_value
|
||
height: 22
|
||
width: 130
|
||
long_mode: DOT
|
||
x: ${TILE_VALUE_X}
|
||
y: ${TILE_VALUE_Y}
|
||
text: "—"
|
||
styles: style_value
|
||
clickable: false
|
||
|
||
# ---------- TILE 5 ----------
|
||
- obj:
|
||
id: tile5
|
||
x: ${GRID_COL1_X}
|
||
y: ${GRID_ROW3_Y}
|
||
width: ${TILE_W}
|
||
height: ${TILE_H}
|
||
styles: style_tile
|
||
scrollable: false
|
||
scrollbar_mode: 'OFF'
|
||
# Direct local feedback: LVGL applies this as soon as the finger
|
||
# touches the glass, in the same frame. Independent of Home Assistant.
|
||
pressed:
|
||
bg_opa: 45%
|
||
widgets:
|
||
- obj:
|
||
id: tile5_icon_circle
|
||
x: ${TILE_ICON_X}
|
||
y: ${TILE_ICON_Y}
|
||
width: 54
|
||
height: 54
|
||
styles: style_icon_circle
|
||
clickable: false
|
||
scrollable: false
|
||
scrollbar_mode: 'OFF'
|
||
widgets:
|
||
- label:
|
||
id: tile5_icon_lbl
|
||
align: CENTER
|
||
text_font: materialdesign_icons
|
||
text: "\U000F0425"
|
||
clickable: false
|
||
- label:
|
||
id: t5_title
|
||
height: 24
|
||
width: 130
|
||
long_mode: DOT
|
||
x: ${TILE_TITLE_X}
|
||
y: ${TILE_TITLE_Y}
|
||
text: "AC"
|
||
styles: style_title
|
||
clickable: false
|
||
- label:
|
||
id: t5_value
|
||
height: 22
|
||
width: 130
|
||
long_mode: DOT
|
||
x: ${TILE_VALUE_X}
|
||
y: ${TILE_VALUE_Y}
|
||
text: "—"
|
||
styles: style_value
|
||
clickable: false
|
||
|
||
# ---------- TILE 6 ----------
|
||
- obj:
|
||
id: tile6
|
||
x: ${GRID_COL2_X}
|
||
y: ${GRID_ROW3_Y}
|
||
width: ${TILE_W}
|
||
height: ${TILE_H}
|
||
styles: style_tile
|
||
scrollable: false
|
||
scrollbar_mode: 'OFF'
|
||
# Direct local feedback: LVGL applies this as soon as the finger
|
||
# touches the glass, in the same frame. Independent of Home Assistant.
|
||
pressed:
|
||
bg_opa: 45%
|
||
widgets:
|
||
- obj:
|
||
id: tile6_icon_circle
|
||
x: ${TILE_ICON_X}
|
||
y: ${TILE_ICON_Y}
|
||
width: 54
|
||
height: 54
|
||
styles: style_icon_circle
|
||
clickable: false
|
||
scrollable: false
|
||
scrollbar_mode: 'OFF'
|
||
widgets:
|
||
- label:
|
||
id: tile6_icon_lbl
|
||
align: CENTER
|
||
text_font: materialdesign_icons
|
||
text: "\U000F070D"
|
||
clickable: false
|
||
- label:
|
||
id: t6_title
|
||
height: 24
|
||
width: 130
|
||
long_mode: DOT
|
||
x: ${TILE_TITLE_X}
|
||
y: ${TILE_TITLE_Y}
|
||
text: "Vacuum"
|
||
styles: style_title
|
||
clickable: false
|
||
- label:
|
||
id: t6_value
|
||
height: 22
|
||
width: 130
|
||
long_mode: DOT
|
||
x: ${TILE_VALUE_X}
|
||
y: ${TILE_VALUE_Y}
|
||
text: "—"
|
||
styles: style_value
|
||
clickable: false
|
||
|
||
# ---------- TILE 7 — Tafellamp ----------
|
||
# Brightness-only lamp: deliberately no percentage/cover/climate
|
||
# sensors were created for this tile, those would only cost
|
||
# HA subscriptions and heap. The overlay therefore gets
|
||
# NAN / "" for the fields a light never uses.
|
||
- obj:
|
||
id: tile7
|
||
hidden: true
|
||
x: ${GRID_COL1_X}
|
||
y: ${GRID_ROW4_Y}
|
||
width: ${TILE_W}
|
||
height: ${TILE_H}
|
||
styles: style_tile
|
||
scrollable: false
|
||
scrollbar_mode: 'OFF'
|
||
pressed:
|
||
bg_opa: 45%
|
||
widgets:
|
||
- obj:
|
||
id: tile7_icon_circle
|
||
x: ${TILE_ICON_X}
|
||
y: ${TILE_ICON_Y}
|
||
width: 54
|
||
height: 54
|
||
styles: style_icon_circle
|
||
clickable: false
|
||
scrollable: false
|
||
scrollbar_mode: 'OFF'
|
||
widgets:
|
||
- label:
|
||
id: tile7_icon_lbl
|
||
align: CENTER
|
||
text_font: materialdesign_icons
|
||
text: "\U000F06B5"
|
||
clickable: false
|
||
- label:
|
||
id: t7_title
|
||
height: 24
|
||
width: 130
|
||
long_mode: DOT
|
||
x: ${TILE_TITLE_X}
|
||
y: ${TILE_TITLE_Y}
|
||
text: "Tile 7"
|
||
styles: style_title
|
||
clickable: false
|
||
- label:
|
||
id: t7_value
|
||
height: 22
|
||
width: 130
|
||
long_mode: DOT
|
||
x: ${TILE_VALUE_X}
|
||
y: ${TILE_VALUE_Y}
|
||
text: "—"
|
||
styles: style_value
|
||
clickable: false
|
||
|
||
# ---------- TILE 8 — Brievenbus reset ----------
|
||
# input_select with options Full/Empty. The tile lights up as soon as
|
||
# the status is TILE8_ACTIVE_STATE ("Full"); a short tap sends
|
||
# input_select.select_option with option: "Empty".
|
||
# No slider mode, so this tile only needs ha_state_tile8 —
|
||
# no other HA subscription at all.
|
||
- obj:
|
||
id: tile8
|
||
hidden: true
|
||
x: ${GRID_COL2_X}
|
||
y: ${GRID_ROW4_Y}
|
||
width: ${TILE_W}
|
||
height: ${TILE_H}
|
||
styles: style_tile
|
||
scrollable: false
|
||
scrollbar_mode: 'OFF'
|
||
pressed:
|
||
bg_opa: 45%
|
||
widgets:
|
||
- obj:
|
||
id: tile8_icon_circle
|
||
x: ${TILE_ICON_X}
|
||
y: ${TILE_ICON_Y}
|
||
width: 54
|
||
height: 54
|
||
styles: style_icon_circle
|
||
clickable: false
|
||
scrollable: false
|
||
scrollbar_mode: 'OFF'
|
||
widgets:
|
||
- label:
|
||
id: tile8_icon_lbl
|
||
align: CENTER
|
||
text_font: materialdesign_icons
|
||
text: "\U000F06EE"
|
||
clickable: false
|
||
- label:
|
||
id: t8_title
|
||
height: 24
|
||
width: 130
|
||
long_mode: DOT
|
||
x: ${TILE_TITLE_X}
|
||
y: ${TILE_TITLE_Y}
|
||
text: "Tile 8"
|
||
styles: style_title
|
||
clickable: false
|
||
- label:
|
||
id: t8_value
|
||
height: 22
|
||
width: 130
|
||
long_mode: DOT
|
||
x: ${TILE_VALUE_X}
|
||
y: ${TILE_VALUE_Y}
|
||
text: "—"
|
||
styles: style_value
|
||
clickable: false
|
||
|
||
# ---------- TILE 9 — Living room (moved from tile 2) ----------
|
||
- obj:
|
||
id: tile9
|
||
hidden: true
|
||
x: ${GRID_COL1_X}
|
||
y: ${GRID_ROW5_Y}
|
||
width: ${TILE_W}
|
||
height: ${TILE_H}
|
||
styles: style_tile
|
||
scrollable: false
|
||
scrollbar_mode: 'OFF'
|
||
pressed:
|
||
bg_opa: 45%
|
||
widgets:
|
||
- obj:
|
||
id: tile9_icon_circle
|
||
x: ${TILE_ICON_X}
|
||
y: ${TILE_ICON_Y}
|
||
width: 54
|
||
height: 54
|
||
styles: style_icon_circle
|
||
clickable: false
|
||
scrollable: false
|
||
scrollbar_mode: 'OFF'
|
||
widgets:
|
||
- label:
|
||
id: tile9_icon_lbl
|
||
align: CENTER
|
||
text_font: materialdesign_icons
|
||
text: "\U000F04B9"
|
||
clickable: false
|
||
- label:
|
||
id: t9_title
|
||
height: 24
|
||
width: 130
|
||
long_mode: DOT
|
||
x: ${TILE_TITLE_X}
|
||
y: ${TILE_TITLE_Y}
|
||
text: "Tile 9"
|
||
styles: style_title
|
||
clickable: false
|
||
- label:
|
||
id: t9_value
|
||
height: 22
|
||
width: 130
|
||
long_mode: DOT
|
||
x: ${TILE_VALUE_X}
|
||
y: ${TILE_VALUE_Y}
|
||
text: "—"
|
||
styles: style_value
|
||
clickable: false
|
||
|
||
# ---------- TILE 10 — Alarm on/off ----------
|
||
- obj:
|
||
id: tile10
|
||
hidden: true
|
||
x: ${GRID_COL2_X}
|
||
y: ${GRID_ROW5_Y}
|
||
width: ${TILE_W}
|
||
height: ${TILE_H}
|
||
styles: style_tile
|
||
scrollable: false
|
||
scrollbar_mode: 'OFF'
|
||
pressed:
|
||
bg_opa: 45%
|
||
widgets:
|
||
- obj:
|
||
id: tile10_icon_circle
|
||
x: ${TILE_ICON_X}
|
||
y: ${TILE_ICON_Y}
|
||
width: 54
|
||
height: 54
|
||
styles: style_icon_circle
|
||
clickable: false
|
||
scrollable: false
|
||
scrollbar_mode: 'OFF'
|
||
widgets:
|
||
- label:
|
||
id: tile10_icon_lbl
|
||
align: CENTER
|
||
text_font: materialdesign_icons
|
||
text: "\U000F002A"
|
||
clickable: false
|
||
- label:
|
||
id: t10_title
|
||
height: 24
|
||
width: 130
|
||
long_mode: DOT
|
||
x: ${TILE_TITLE_X}
|
||
y: ${TILE_TITLE_Y}
|
||
text: "Tile 10"
|
||
styles: style_title
|
||
clickable: false
|
||
- label:
|
||
id: t10_value
|
||
height: 22
|
||
width: 130
|
||
long_mode: DOT
|
||
x: ${TILE_VALUE_X}
|
||
y: ${TILE_VALUE_Y}
|
||
text: "—"
|
||
styles: style_value
|
||
clickable: false
|
||
|
||
# ---------- VALUE SLIDER OVERLAY ----------
|
||
# ---------- PAGE NAVIGATION: one full-width bar ----------
|
||
- obj:
|
||
id: page_prev
|
||
align: BOTTOM_LEFT
|
||
x: 0
|
||
y: 0
|
||
width: 200
|
||
height: 44
|
||
pad_all: 0
|
||
radius: 0
|
||
bg_opa: TRANSP
|
||
border_width: 0
|
||
scrollable: false
|
||
pressed:
|
||
styles: paint_page_pressed
|
||
bg_opa: COVER
|
||
on_short_click:
|
||
- if:
|
||
condition:
|
||
lambda: 'return cyd::touch_guard.accept(millis(), 11);'
|
||
then:
|
||
- lambda: 'id(tile_page) = std::max(0, id(tile_page) - 1);'
|
||
- script.execute: show_tile_page
|
||
widgets:
|
||
- label:
|
||
align: LEFT_MID
|
||
x: 24
|
||
text: "< Previous"
|
||
text_font: label
|
||
styles: paint_ink
|
||
clickable: false
|
||
- label:
|
||
id: page_number
|
||
align: BOTTOM_MID
|
||
y: 0
|
||
width: 80
|
||
height: 44
|
||
pad_top: 12
|
||
border_width: 0
|
||
text: "1 / 2"
|
||
text_align: CENTER
|
||
text_font: sublabel
|
||
- obj:
|
||
id: page_next
|
||
align: BOTTOM_RIGHT
|
||
x: 0
|
||
y: 0
|
||
width: 200
|
||
height: 44
|
||
pad_all: 0
|
||
radius: 0
|
||
bg_opa: TRANSP
|
||
border_width: 0
|
||
scrollable: false
|
||
pressed:
|
||
styles: paint_page_pressed
|
||
bg_opa: COVER
|
||
on_short_click:
|
||
- if:
|
||
condition:
|
||
lambda: 'return cyd::touch_guard.accept(millis(), 12);'
|
||
then:
|
||
- lambda: 'id(tile_page) += 1;'
|
||
- script.execute: show_tile_page
|
||
widgets:
|
||
- label:
|
||
align: RIGHT_MID
|
||
x: -24
|
||
text: "Next >"
|
||
text_font: label
|
||
styles: paint_ink
|
||
clickable: false
|
||
|
||
- obj:
|
||
id: brightness_overlay
|
||
x: 0
|
||
y: 0
|
||
width: ${DISPLAY_W}
|
||
height: ${DISPLAY_H}
|
||
bg_opa: TRANSP
|
||
hidden: true
|
||
scrollable: false
|
||
scrollbar_mode: 'OFF'
|
||
pad_left: 0
|
||
pad_right: 0
|
||
pad_top: 0
|
||
pad_bottom: 0
|
||
border_width: 0
|
||
widgets:
|
||
- obj:
|
||
id: brightness_overlay_bg
|
||
x: 0
|
||
y: 0
|
||
width: ${DISPLAY_W}
|
||
height: ${DISPLAY_H}
|
||
styles: style_page
|
||
scrollable: false
|
||
clickable: true
|
||
on_click:
|
||
- script.execute: close_cards
|
||
|
||
- obj:
|
||
id: brightness_overlay_dim
|
||
x: 0
|
||
y: 0
|
||
width: ${DISPLAY_W}
|
||
height: ${DISPLAY_H}
|
||
styles: paint_veil
|
||
bg_opa: 18%
|
||
border_width: 0
|
||
clickable: false
|
||
scrollable: false
|
||
|
||
- obj:
|
||
id: overlay_back_button
|
||
align: TOP_LEFT
|
||
x: 16
|
||
y: 16
|
||
width: 60
|
||
height: 60
|
||
radius: 999
|
||
styles: paint_key
|
||
bg_opa: COVER
|
||
border_width: 0
|
||
clickable: true
|
||
scrollable: false
|
||
on_click:
|
||
- script.execute: close_cards
|
||
widgets:
|
||
- label:
|
||
align: CENTER
|
||
text: "\U000F004D"
|
||
text_font: materialdesign_icons_mini
|
||
clickable: false
|
||
|
||
- label:
|
||
id: overlay_title
|
||
align: TOP_MID
|
||
y: 30
|
||
text: "Light"
|
||
styles: style_room
|
||
|
||
- label:
|
||
id: overlay_value
|
||
align: TOP_MID
|
||
y: 70
|
||
text: "100 %"
|
||
styles: style_value_big
|
||
|
||
- slider:
|
||
id: overlay_slider
|
||
align: CENTER
|
||
y: 36
|
||
width: ${OVERLAY_SLIDER_W}
|
||
height: ${OVERLAY_SLIDER_H}
|
||
min_value: 0
|
||
max_value: 100
|
||
value: 100
|
||
adv_hittest: false
|
||
|
||
styles: paint_accent_tint
|
||
bg_opa: 100%
|
||
radius: 42
|
||
# The only widget with BOTH a radius AND a child (overlay_icon).
|
||
# LVGL then clips children to the rounding, and that's only
|
||
# possible via a contiguous 96x160 = 46KB ARGB layer.
|
||
# If that allocation fails, LVGL keeps trying until the
|
||
# task watchdog restarts the board. The rounding stays, the
|
||
# clipping goes.
|
||
clip_corner: false
|
||
border_width: 0
|
||
|
||
indicator:
|
||
styles: paint_accent_bright
|
||
bg_opa: 100%
|
||
radius: 42
|
||
|
||
knob:
|
||
bg_opa: 0%
|
||
border_width: 0
|
||
shadow_width: 0
|
||
pad_all: 15
|
||
|
||
on_value:
|
||
- script.execute:
|
||
id: slider_preview
|
||
value: !lambda "return x;"
|
||
|
||
on_release:
|
||
- script.execute:
|
||
id: slider_commit
|
||
value: !lambda "return lv_slider_get_value(id(overlay_slider));"
|
||
|
||
|
||
# The icon used to be a CHILD in the slider. A widget with a radius
|
||
# and children has to clip those children to the rounding, and
|
||
# LVGL can only do that via an ARGB layer of tens of KB's. Without
|
||
# PSRAM that sometimes fails, after which LVGL keeps retrying until
|
||
# the watchdog restarts the board. As a sibling on top, no layer
|
||
# is needed and it looks identical.
|
||
- label:
|
||
id: overlay_icon
|
||
align: CENTER
|
||
y: ${OVERLAY_ICON_Y}
|
||
text_font: materialdesign_icons
|
||
styles: paint_ink
|
||
text: "\U000F001B"
|
||
clickable: false
|
||
|
||
- obj:
|
||
id: overlay_color_button
|
||
align: BOTTOM_RIGHT
|
||
x: -20
|
||
y: -26
|
||
width: 66
|
||
height: 66
|
||
radius: 999
|
||
hidden: true
|
||
clickable: true
|
||
bg_opa: TRANSP
|
||
border_width: 0
|
||
pad_left: 0
|
||
pad_right: 0
|
||
pad_top: 0
|
||
pad_bottom: 0
|
||
scrollable: false
|
||
scrollbar_mode: 'OFF'
|
||
on_click:
|
||
- script.execute: open_color_detail
|
||
widgets:
|
||
- arc:
|
||
align: CENTER
|
||
width: 60
|
||
height: 60
|
||
start_angle: 0
|
||
end_angle: 45
|
||
min_value: 0
|
||
max_value: 100
|
||
value: 0
|
||
adjustable: false
|
||
arc_width: 15
|
||
arc_rounded: false
|
||
arc_color: 0xFF3B30
|
||
arc_opa: COVER
|
||
bg_opa: TRANSP
|
||
border_width: 0
|
||
clickable: false
|
||
indicator:
|
||
arc_opa: TRANSP
|
||
knob:
|
||
bg_opa: TRANSP
|
||
border_width: 0
|
||
- arc:
|
||
align: CENTER
|
||
width: 60
|
||
height: 60
|
||
start_angle: 45
|
||
end_angle: 90
|
||
min_value: 0
|
||
max_value: 100
|
||
value: 0
|
||
adjustable: false
|
||
arc_width: 15
|
||
arc_rounded: false
|
||
arc_color: 0xFF9500
|
||
arc_opa: COVER
|
||
bg_opa: TRANSP
|
||
border_width: 0
|
||
clickable: false
|
||
indicator:
|
||
arc_opa: TRANSP
|
||
knob:
|
||
bg_opa: TRANSP
|
||
border_width: 0
|
||
- arc:
|
||
align: CENTER
|
||
width: 60
|
||
height: 60
|
||
start_angle: 90
|
||
end_angle: 135
|
||
min_value: 0
|
||
max_value: 100
|
||
value: 0
|
||
adjustable: false
|
||
arc_width: 15
|
||
arc_rounded: false
|
||
arc_color: 0xFFD60A
|
||
arc_opa: COVER
|
||
bg_opa: TRANSP
|
||
border_width: 0
|
||
clickable: false
|
||
indicator:
|
||
arc_opa: TRANSP
|
||
knob:
|
||
bg_opa: TRANSP
|
||
border_width: 0
|
||
- arc:
|
||
align: CENTER
|
||
width: 60
|
||
height: 60
|
||
start_angle: 135
|
||
end_angle: 180
|
||
min_value: 0
|
||
max_value: 100
|
||
value: 0
|
||
adjustable: false
|
||
arc_width: 15
|
||
arc_rounded: false
|
||
arc_color: 0x30D158
|
||
arc_opa: COVER
|
||
bg_opa: TRANSP
|
||
border_width: 0
|
||
clickable: false
|
||
indicator:
|
||
arc_opa: TRANSP
|
||
knob:
|
||
bg_opa: TRANSP
|
||
border_width: 0
|
||
- arc:
|
||
align: CENTER
|
||
width: 60
|
||
height: 60
|
||
start_angle: 180
|
||
end_angle: 225
|
||
min_value: 0
|
||
max_value: 100
|
||
value: 0
|
||
adjustable: false
|
||
arc_width: 15
|
||
arc_rounded: false
|
||
arc_color: 0x32D6FF
|
||
arc_opa: COVER
|
||
bg_opa: TRANSP
|
||
border_width: 0
|
||
clickable: false
|
||
indicator:
|
||
arc_opa: TRANSP
|
||
knob:
|
||
bg_opa: TRANSP
|
||
border_width: 0
|
||
- arc:
|
||
align: CENTER
|
||
width: 60
|
||
height: 60
|
||
start_angle: 225
|
||
end_angle: 270
|
||
min_value: 0
|
||
max_value: 100
|
||
value: 0
|
||
adjustable: false
|
||
arc_width: 15
|
||
arc_rounded: false
|
||
arc_color: 0x0A84FF
|
||
arc_opa: COVER
|
||
bg_opa: TRANSP
|
||
border_width: 0
|
||
clickable: false
|
||
indicator:
|
||
arc_opa: TRANSP
|
||
knob:
|
||
bg_opa: TRANSP
|
||
border_width: 0
|
||
- arc:
|
||
align: CENTER
|
||
width: 60
|
||
height: 60
|
||
start_angle: 270
|
||
end_angle: 315
|
||
min_value: 0
|
||
max_value: 100
|
||
value: 0
|
||
adjustable: false
|
||
arc_width: 15
|
||
arc_rounded: false
|
||
arc_color: 0x5856D6
|
||
arc_opa: COVER
|
||
bg_opa: TRANSP
|
||
border_width: 0
|
||
clickable: false
|
||
indicator:
|
||
arc_opa: TRANSP
|
||
knob:
|
||
bg_opa: TRANSP
|
||
border_width: 0
|
||
- arc:
|
||
align: CENTER
|
||
width: 60
|
||
height: 60
|
||
start_angle: 315
|
||
end_angle: 360
|
||
min_value: 0
|
||
max_value: 100
|
||
value: 0
|
||
adjustable: false
|
||
arc_width: 15
|
||
arc_rounded: false
|
||
arc_color: 0xBF5AF2
|
||
arc_opa: COVER
|
||
bg_opa: TRANSP
|
||
border_width: 0
|
||
clickable: false
|
||
indicator:
|
||
arc_opa: TRANSP
|
||
knob:
|
||
bg_opa: TRANSP
|
||
border_width: 0
|
||
|
||
# ---------- LIGHT COLOR DETAIL OVERLAY ----------
|
||
- obj:
|
||
id: color_detail_overlay
|
||
x: 0
|
||
y: 0
|
||
width: ${DISPLAY_W}
|
||
height: ${DISPLAY_H}
|
||
styles: paint_page_soft
|
||
bg_opa: COVER
|
||
hidden: true
|
||
scrollable: false
|
||
scrollbar_mode: 'OFF'
|
||
pad_left: 0
|
||
pad_right: 0
|
||
pad_top: 0
|
||
pad_bottom: 0
|
||
border_width: 0
|
||
widgets:
|
||
- obj:
|
||
id: color_close_button
|
||
align: TOP_LEFT
|
||
x: 16
|
||
y: 16
|
||
width: 60
|
||
height: 60
|
||
radius: 999
|
||
styles: paint_key
|
||
bg_opa: COVER
|
||
border_width: 0
|
||
clickable: true
|
||
scrollable: false
|
||
on_click:
|
||
- script.execute: close_cards
|
||
widgets:
|
||
- label:
|
||
align: CENTER
|
||
text: "\U000F004D"
|
||
text_font: materialdesign_icons_mini
|
||
clickable: false
|
||
|
||
- label:
|
||
id: color_detail_title
|
||
width: 310
|
||
long_mode: DOT
|
||
text_align: CENTER
|
||
align: TOP_MID
|
||
y: 35
|
||
text: "Light Color"
|
||
styles: style_room
|
||
|
||
|
||
# ---------- CLIMATE DETAIL OVERLAY ----------
|
||
# The CYD's climate card at the Guition's size: nothing to drag. A setpoint card with big
|
||
# -/+ keys (the number changes at once, one call goes out once the taps stop) and one row
|
||
# of mode keys like Home Assistant's climate card; fan and swing stay in the picker behind
|
||
# "more". A tap redraws only a label or a key, never a dial.
|
||
- obj:
|
||
id: climate_detail_overlay
|
||
x: 0
|
||
y: 0
|
||
width: ${DISPLAY_W}
|
||
height: ${DISPLAY_H}
|
||
styles: style_page
|
||
bg_opa: COVER
|
||
hidden: true
|
||
scrollable: false
|
||
scrollbar_mode: 'OFF'
|
||
pad_left: 0
|
||
pad_right: 0
|
||
pad_top: 0
|
||
pad_bottom: 0
|
||
border_width: 0
|
||
widgets:
|
||
- obj:
|
||
id: climate_close_button
|
||
align: TOP_LEFT
|
||
x: 16
|
||
y: 16
|
||
width: 60
|
||
height: 60
|
||
radius: 999
|
||
styles: paint_key
|
||
bg_opa: COVER
|
||
border_width: 0
|
||
clickable: true
|
||
scrollable: false
|
||
on_click:
|
||
- script.execute: close_cards
|
||
widgets:
|
||
- label:
|
||
align: CENTER
|
||
text: "\U000F004D"
|
||
text_font: materialdesign_icons_mini
|
||
clickable: false
|
||
|
||
- obj:
|
||
id: climate_power_button
|
||
align: TOP_RIGHT
|
||
x: -16
|
||
y: 16
|
||
width: 60
|
||
height: 60
|
||
radius: 999
|
||
bg_opa: COVER
|
||
border_width: 0
|
||
clickable: true
|
||
scrollable: false
|
||
on_click:
|
||
- script.execute: climate_power_toggle
|
||
widgets:
|
||
- label:
|
||
id: climate_power_icon
|
||
align: CENTER
|
||
text: "\U000F0425"
|
||
text_font: materialdesign_icons_mini
|
||
clickable: false
|
||
|
||
- label:
|
||
id: climate_detail_title
|
||
align: TOP_MID
|
||
y: 30
|
||
width: 300
|
||
long_mode: DOT
|
||
text_align: CENTER
|
||
text: "Climate"
|
||
styles: style_room
|
||
|
||
# What the device does (or its mode), the room temperature and humidity.
|
||
- label:
|
||
id: climate_current_label
|
||
align: TOP_MID
|
||
y: 92
|
||
width: 94%
|
||
long_mode: DOT
|
||
text_align: CENTER
|
||
text: "--"
|
||
text_font: sublabel_big
|
||
styles: paint_muted
|
||
clickable: false
|
||
|
||
- obj:
|
||
id: climate_setpoint_card
|
||
align: TOP_MID
|
||
y: 136
|
||
width: 94%
|
||
height: 200
|
||
radius: 24
|
||
styles: paint_card
|
||
bg_opa: COVER
|
||
border_width: 1
|
||
pad_all: 0
|
||
clickable: false
|
||
scrollable: false
|
||
widgets:
|
||
# Every clean tap counts (a quick triple tap is three steps); holding keeps stepping.
|
||
- obj:
|
||
id: climate_minus_button
|
||
align: LEFT_MID
|
||
x: 18
|
||
width: 116
|
||
height: 116
|
||
radius: 58
|
||
styles: paint_track
|
||
bg_opa: COVER
|
||
border_width: 0
|
||
pad_all: 0
|
||
clickable: true
|
||
scrollable: false
|
||
pressed:
|
||
styles: paint_key_pressed
|
||
disabled:
|
||
opa: 40%
|
||
on_short_click:
|
||
- if:
|
||
condition:
|
||
lambda: 'return cyd::touch_guard.accept_repeat(millis(), 60);'
|
||
then:
|
||
- script.execute: { id: climate_adjust_target, direction: -1, held: false }
|
||
on_long_press_repeat:
|
||
- script.execute: { id: climate_adjust_target, direction: -1, held: true }
|
||
widgets:
|
||
- label:
|
||
id: climate_minus_icon
|
||
align: CENTER
|
||
text: "\U000F0374"
|
||
text_font: materialdesign_icons
|
||
clickable: false
|
||
- obj:
|
||
id: climate_plus_button
|
||
align: RIGHT_MID
|
||
x: -18
|
||
width: 116
|
||
height: 116
|
||
radius: 58
|
||
styles: paint_track
|
||
bg_opa: COVER
|
||
border_width: 0
|
||
pad_all: 0
|
||
clickable: true
|
||
scrollable: false
|
||
pressed:
|
||
styles: paint_key_pressed
|
||
disabled:
|
||
opa: 40%
|
||
on_short_click:
|
||
- if:
|
||
condition:
|
||
lambda: 'return cyd::touch_guard.accept_repeat(millis(), 61);'
|
||
then:
|
||
- script.execute: { id: climate_adjust_target, direction: 1, held: false }
|
||
on_long_press_repeat:
|
||
- script.execute: { id: climate_adjust_target, direction: 1, held: true }
|
||
widgets:
|
||
- label:
|
||
id: climate_plus_icon
|
||
align: CENTER
|
||
text: "\U000F0415"
|
||
text_font: materialdesign_icons
|
||
clickable: false
|
||
- label:
|
||
id: climate_target_label
|
||
align: CENTER
|
||
y: -14
|
||
text: "--"
|
||
text_font: setpoint_digits
|
||
clickable: false
|
||
- label:
|
||
id: climate_set_label
|
||
align: CENTER
|
||
y: 46
|
||
text: "Target"
|
||
text_font: sublabel
|
||
styles: paint_subtle
|
||
clickable: false
|
||
|
||
# One key per mode the device supports (off is the power key); the one in use is
|
||
# filled in Home Assistant's colour for it. "More" opens the picker with fan and swing.
|
||
- obj:
|
||
id: climate_mode_keys
|
||
align: TOP_MID
|
||
y: 364
|
||
width: 94%
|
||
height: 76
|
||
bg_opa: TRANSP
|
||
border_width: 0
|
||
pad_all: 0
|
||
clickable: false
|
||
scrollable: false
|
||
layout:
|
||
type: FLEX
|
||
flex_flow: ROW
|
||
flex_align_main: CENTER
|
||
flex_align_cross: CENTER
|
||
pad_column: 10
|
||
widgets:
|
||
- obj:
|
||
id: cmkey_auto
|
||
flex_grow: 1
|
||
height: 76
|
||
radius: 38
|
||
styles: paint_card
|
||
bg_opa: COVER
|
||
border_width: 1
|
||
pad_all: 0
|
||
clickable: true
|
||
scrollable: false
|
||
pressed:
|
||
styles: paint_key
|
||
on_click:
|
||
- script.execute: { id: set_climate_mode, mode: "auto" }
|
||
widgets:
|
||
- label:
|
||
align: CENTER
|
||
text: "\U000F1B17"
|
||
text_font: materialdesign_icons
|
||
clickable: false
|
||
- obj:
|
||
id: cmkey_heat_cool
|
||
flex_grow: 1
|
||
height: 76
|
||
radius: 38
|
||
styles: paint_card
|
||
bg_opa: COVER
|
||
border_width: 1
|
||
pad_all: 0
|
||
clickable: true
|
||
scrollable: false
|
||
pressed:
|
||
styles: paint_key
|
||
on_click:
|
||
- script.execute: { id: set_climate_mode, mode: "heat_cool" }
|
||
widgets:
|
||
- label:
|
||
align: CENTER
|
||
text: "\U000F1A79"
|
||
text_font: materialdesign_icons
|
||
clickable: false
|
||
- obj:
|
||
id: cmkey_heat
|
||
flex_grow: 1
|
||
height: 76
|
||
radius: 38
|
||
styles: paint_card
|
||
bg_opa: COVER
|
||
border_width: 1
|
||
pad_all: 0
|
||
clickable: true
|
||
scrollable: false
|
||
pressed:
|
||
styles: paint_key
|
||
on_click:
|
||
- script.execute: { id: set_climate_mode, mode: "heat" }
|
||
widgets:
|
||
- label:
|
||
align: CENTER
|
||
text: "\U000F0238"
|
||
text_font: materialdesign_icons
|
||
clickable: false
|
||
- obj:
|
||
id: cmkey_cool
|
||
flex_grow: 1
|
||
height: 76
|
||
radius: 38
|
||
styles: paint_card
|
||
bg_opa: COVER
|
||
border_width: 1
|
||
pad_all: 0
|
||
clickable: true
|
||
scrollable: false
|
||
pressed:
|
||
styles: paint_key
|
||
on_click:
|
||
- script.execute: { id: set_climate_mode, mode: "cool" }
|
||
widgets:
|
||
- label:
|
||
align: CENTER
|
||
text: "\U000F0717"
|
||
text_font: materialdesign_icons
|
||
clickable: false
|
||
- obj:
|
||
id: cmkey_dry
|
||
flex_grow: 1
|
||
height: 76
|
||
radius: 38
|
||
styles: paint_card
|
||
bg_opa: COVER
|
||
border_width: 1
|
||
pad_all: 0
|
||
clickable: true
|
||
scrollable: false
|
||
pressed:
|
||
styles: paint_key
|
||
on_click:
|
||
- script.execute: { id: set_climate_mode, mode: "dry" }
|
||
widgets:
|
||
- label:
|
||
align: CENTER
|
||
text: "\U000F058E"
|
||
text_font: materialdesign_icons
|
||
clickable: false
|
||
- obj:
|
||
id: cmkey_fan_only
|
||
flex_grow: 1
|
||
height: 76
|
||
radius: 38
|
||
styles: paint_card
|
||
bg_opa: COVER
|
||
border_width: 1
|
||
pad_all: 0
|
||
clickable: true
|
||
scrollable: false
|
||
pressed:
|
||
styles: paint_key
|
||
on_click:
|
||
- script.execute: { id: set_climate_mode, mode: "fan_only" }
|
||
widgets:
|
||
- label:
|
||
align: CENTER
|
||
text: "\U000F0210"
|
||
text_font: materialdesign_icons
|
||
clickable: false
|
||
- obj:
|
||
id: cmkey_more
|
||
flex_grow: 1
|
||
height: 76
|
||
radius: 38
|
||
styles: paint_card
|
||
bg_opa: COVER
|
||
border_width: 1
|
||
pad_all: 0
|
||
clickable: true
|
||
scrollable: false
|
||
pressed:
|
||
styles: paint_key
|
||
on_click:
|
||
- script.execute: open_climate_mode_picker
|
||
widgets:
|
||
- label:
|
||
align: CENTER
|
||
text: "\U000F01D8"
|
||
text_font: materialdesign_icons
|
||
styles: paint_slate
|
||
clickable: false
|
||
# Fan and swing, each a row of choices behind its icon (climate_card.h fills the card).
|
||
- obj:
|
||
id: climate_settings_card
|
||
align: TOP_MID
|
||
y: 340
|
||
width: 94%
|
||
height: 116
|
||
radius: 24
|
||
styles: paint_card
|
||
bg_opa: COVER
|
||
border_width: 1
|
||
pad_all: 0
|
||
hidden: true
|
||
clickable: false
|
||
scrollable: false
|
||
# ---------- CLIMATE MODE PICKER ----------
|
||
# Replaces cycling through modes. Cycling sends a set_hvac_mode on every
|
||
# tap, so to get from 'cool' to 'dry' a heat pump actually passes through
|
||
# 'heat' and 'fan_only' along the way. Here you pick directly, and exactly
|
||
# one command is sent.
|
||
- obj:
|
||
id: climate_mode_overlay
|
||
x: 0
|
||
y: 0
|
||
width: ${DISPLAY_W}
|
||
height: ${DISPLAY_H}
|
||
hidden: true
|
||
styles: style_page
|
||
bg_opa: COVER
|
||
border_width: 0
|
||
clickable: true
|
||
scrollable: false
|
||
scrollbar_mode: 'OFF'
|
||
widgets:
|
||
- label:
|
||
align: TOP_MID
|
||
y: 35
|
||
text: "Mode"
|
||
text_font: sublabel_big
|
||
clickable: false
|
||
|
||
# Flex ensures unsupported modes don't leave a gap:
|
||
# LVGL skips hidden children when arranging.
|
||
- obj:
|
||
id: climate_mode_list
|
||
align: TOP_MID
|
||
y: 92
|
||
width: 98%
|
||
height: 372
|
||
bg_opa: TRANSP
|
||
border_width: 0
|
||
scrollable: false
|
||
clickable: false
|
||
pad_all: 4
|
||
layout:
|
||
type: FLEX
|
||
pad_row: 6
|
||
pad_column: 6
|
||
flex_flow: ROW_WRAP
|
||
flex_align_main: CENTER
|
||
flex_align_cross: CENTER
|
||
flex_align_track: CENTER
|
||
widgets:
|
||
- obj:
|
||
id: cmode_off
|
||
width: 136
|
||
height: 58
|
||
radius: 22
|
||
styles: paint_card
|
||
bg_opa: COVER
|
||
border_width: 1
|
||
clickable: true
|
||
scrollable: false
|
||
pad_all: 0
|
||
on_click:
|
||
- script.execute: { id: set_climate_mode, mode: "off" }
|
||
widgets:
|
||
- label:
|
||
align: CENTER
|
||
text: "Off"
|
||
text_font: sublabel_big
|
||
clickable: false
|
||
- obj:
|
||
id: cmode_heat
|
||
width: 136
|
||
height: 58
|
||
radius: 22
|
||
styles: paint_card
|
||
bg_opa: COVER
|
||
border_width: 1
|
||
clickable: true
|
||
scrollable: false
|
||
pad_all: 0
|
||
on_click:
|
||
- script.execute: { id: set_climate_mode, mode: "heat" }
|
||
widgets:
|
||
- label:
|
||
align: CENTER
|
||
text: "Heat"
|
||
text_font: sublabel_big
|
||
clickable: false
|
||
- obj:
|
||
id: cmode_cool
|
||
width: 136
|
||
height: 58
|
||
radius: 22
|
||
styles: paint_card
|
||
bg_opa: COVER
|
||
border_width: 1
|
||
clickable: true
|
||
scrollable: false
|
||
pad_all: 0
|
||
on_click:
|
||
- script.execute: { id: set_climate_mode, mode: "cool" }
|
||
widgets:
|
||
- label:
|
||
align: CENTER
|
||
text: "Cool"
|
||
text_font: sublabel_big
|
||
clickable: false
|
||
- obj:
|
||
id: cmode_heat_cool
|
||
width: 136
|
||
height: 58
|
||
radius: 22
|
||
styles: paint_card
|
||
bg_opa: COVER
|
||
border_width: 1
|
||
clickable: true
|
||
scrollable: false
|
||
pad_all: 0
|
||
on_click:
|
||
- script.execute: { id: set_climate_mode, mode: "heat_cool" }
|
||
widgets:
|
||
- label:
|
||
align: CENTER
|
||
text: "Heat/Cool"
|
||
text_font: sublabel_big
|
||
clickable: false
|
||
- obj:
|
||
id: cmode_auto
|
||
width: 136
|
||
height: 58
|
||
radius: 22
|
||
styles: paint_card
|
||
bg_opa: COVER
|
||
border_width: 1
|
||
clickable: true
|
||
scrollable: false
|
||
pad_all: 0
|
||
on_click:
|
||
- script.execute: { id: set_climate_mode, mode: "auto" }
|
||
widgets:
|
||
- label:
|
||
align: CENTER
|
||
text: "Auto"
|
||
text_font: sublabel_big
|
||
clickable: false
|
||
- obj:
|
||
id: cmode_dry
|
||
width: 136
|
||
height: 58
|
||
radius: 22
|
||
styles: paint_card
|
||
bg_opa: COVER
|
||
border_width: 1
|
||
clickable: true
|
||
scrollable: false
|
||
pad_all: 0
|
||
on_click:
|
||
- script.execute: { id: set_climate_mode, mode: "dry" }
|
||
widgets:
|
||
- label:
|
||
align: CENTER
|
||
text: "Dry"
|
||
text_font: sublabel_big
|
||
clickable: false
|
||
- obj:
|
||
id: cmode_fan_only
|
||
width: 136
|
||
height: 58
|
||
radius: 22
|
||
styles: paint_card
|
||
bg_opa: COVER
|
||
border_width: 1
|
||
clickable: true
|
||
scrollable: false
|
||
pad_all: 0
|
||
on_click:
|
||
- script.execute: { id: set_climate_mode, mode: "fan_only" }
|
||
widgets:
|
||
- label:
|
||
align: CENTER
|
||
text: "Fan only"
|
||
text_font: sublabel_big
|
||
clickable: false
|
||
|
||
- label:
|
||
id: cfan_title
|
||
width: 100%
|
||
height: 24
|
||
text: "Fan"
|
||
text_font: sublabel
|
||
styles: paint_subtle
|
||
text_align: CENTER
|
||
clickable: false
|
||
- obj:
|
||
id: cfan_0
|
||
width: 98
|
||
height: 48
|
||
radius: 18
|
||
styles: paint_card
|
||
bg_opa: COVER
|
||
border_width: 1
|
||
clickable: true
|
||
scrollable: false
|
||
pad_all: 0
|
||
on_click:
|
||
- script.execute: { id: set_climate_fan_mode, index: 0 }
|
||
widgets:
|
||
- label:
|
||
id: cfan_0_label
|
||
align: CENTER
|
||
text: "-"
|
||
text_font: sublabel_big
|
||
clickable: false
|
||
- obj:
|
||
id: cfan_1
|
||
width: 98
|
||
height: 48
|
||
radius: 18
|
||
styles: paint_card
|
||
bg_opa: COVER
|
||
border_width: 1
|
||
clickable: true
|
||
scrollable: false
|
||
pad_all: 0
|
||
on_click:
|
||
- script.execute: { id: set_climate_fan_mode, index: 1 }
|
||
widgets:
|
||
- label:
|
||
id: cfan_1_label
|
||
align: CENTER
|
||
text: "-"
|
||
text_font: sublabel_big
|
||
clickable: false
|
||
- obj:
|
||
id: cfan_2
|
||
width: 98
|
||
height: 48
|
||
radius: 18
|
||
styles: paint_card
|
||
bg_opa: COVER
|
||
border_width: 1
|
||
clickable: true
|
||
scrollable: false
|
||
pad_all: 0
|
||
on_click:
|
||
- script.execute: { id: set_climate_fan_mode, index: 2 }
|
||
widgets:
|
||
- label:
|
||
id: cfan_2_label
|
||
align: CENTER
|
||
text: "-"
|
||
text_font: sublabel_big
|
||
clickable: false
|
||
- obj:
|
||
id: cfan_3
|
||
width: 98
|
||
height: 48
|
||
radius: 18
|
||
styles: paint_card
|
||
bg_opa: COVER
|
||
border_width: 1
|
||
clickable: true
|
||
scrollable: false
|
||
pad_all: 0
|
||
on_click:
|
||
- script.execute: { id: set_climate_fan_mode, index: 3 }
|
||
widgets:
|
||
- label:
|
||
id: cfan_3_label
|
||
align: CENTER
|
||
text: "-"
|
||
text_font: sublabel_big
|
||
clickable: false
|
||
- label:
|
||
id: cswing_title
|
||
width: 100%
|
||
height: 24
|
||
text: "Swing"
|
||
text_font: sublabel
|
||
styles: paint_subtle
|
||
text_align: CENTER
|
||
clickable: false
|
||
- obj:
|
||
id: cswing_0
|
||
width: 98
|
||
height: 48
|
||
radius: 18
|
||
styles: paint_card
|
||
bg_opa: COVER
|
||
border_width: 1
|
||
clickable: true
|
||
scrollable: false
|
||
pad_all: 0
|
||
on_click:
|
||
- script.execute: { id: set_climate_swing_mode, index: 0 }
|
||
widgets:
|
||
- label:
|
||
id: cswing_0_label
|
||
align: CENTER
|
||
text: "-"
|
||
text_font: sublabel_big
|
||
clickable: false
|
||
- obj:
|
||
id: cswing_1
|
||
width: 98
|
||
height: 48
|
||
radius: 18
|
||
styles: paint_card
|
||
bg_opa: COVER
|
||
border_width: 1
|
||
clickable: true
|
||
scrollable: false
|
||
pad_all: 0
|
||
on_click:
|
||
- script.execute: { id: set_climate_swing_mode, index: 1 }
|
||
widgets:
|
||
- label:
|
||
id: cswing_1_label
|
||
align: CENTER
|
||
text: "-"
|
||
text_font: sublabel_big
|
||
clickable: false
|
||
- obj:
|
||
id: cswing_2
|
||
width: 98
|
||
height: 48
|
||
radius: 18
|
||
styles: paint_card
|
||
bg_opa: COVER
|
||
border_width: 1
|
||
clickable: true
|
||
scrollable: false
|
||
pad_all: 0
|
||
on_click:
|
||
- script.execute: { id: set_climate_swing_mode, index: 2 }
|
||
widgets:
|
||
- label:
|
||
id: cswing_2_label
|
||
align: CENTER
|
||
text: "-"
|
||
text_font: sublabel_big
|
||
clickable: false
|
||
- obj:
|
||
id: cswing_3
|
||
width: 98
|
||
height: 48
|
||
radius: 18
|
||
styles: paint_card
|
||
bg_opa: COVER
|
||
border_width: 1
|
||
clickable: true
|
||
scrollable: false
|
||
pad_all: 0
|
||
on_click:
|
||
- script.execute: { id: set_climate_swing_mode, index: 3 }
|
||
widgets:
|
||
- label:
|
||
id: cswing_3_label
|
||
align: CENTER
|
||
text: "-"
|
||
text_font: sublabel_big
|
||
clickable: false
|
||
- obj:
|
||
id: climate_mode_overlay_x
|
||
align: TOP_LEFT
|
||
x: 16
|
||
y: 16
|
||
width: 60
|
||
height: 60
|
||
radius: 999
|
||
styles: paint_key
|
||
bg_opa: COVER
|
||
border_width: 0
|
||
clickable: true
|
||
scrollable: false
|
||
on_click:
|
||
- lvgl.widget.hide: climate_mode_overlay
|
||
widgets:
|
||
- label:
|
||
align: CENTER
|
||
text: "\U000F004D"
|
||
text_font: materialdesign_icons_mini
|
||
clickable: false
|
||
|
||
|
||
# ---------- VACUUM CONTROL ----------
|
||
# A vacuum is not a switch: start, pause, stop and return to
|
||
# the dock are separate commands, and suction power is a separate axis.
|
||
|
||
|
||
# ---------- DIM / WAKE OVERLAY ----------
|
||
- obj:
|
||
id: dim_wake_overlay
|
||
x: 0
|
||
y: 0
|
||
width: ${DISPLAY_W}
|
||
height: ${DISPLAY_H}
|
||
hidden: true
|
||
bg_opa: TRANSP
|
||
# The tap that wakes a dimmed screen must not light up a pale sheet over it.
|
||
pressed:
|
||
bg_opa: TRANSP
|
||
border_width: 0
|
||
scrollable: false
|
||
scrollbar_mode: 'OFF'
|
||
clickable: true
|
||
on_click:
|
||
- script.execute: wake_display
|
||
|
||
- id: touch_cal_page
|
||
styles: paint_page_soft
|
||
bg_opa: COVER
|
||
pad_all: 0
|
||
border_width: 0
|
||
scrollable: false
|
||
widgets:
|
||
- label:
|
||
align: TOP_MID
|
||
y: 110
|
||
text: "GT911 touch check"
|
||
text_font: headline
|
||
- obj:
|
||
x: 12
|
||
y: 12
|
||
width: 24
|
||
height: 24
|
||
pad_all: 0
|
||
border_width: 0
|
||
bg_opa: TRANSP
|
||
scrollable: false
|
||
widgets:
|
||
- label:
|
||
align: CENTER
|
||
text: "+"
|
||
text_font: headline
|
||
styles: paint_ink
|
||
- obj:
|
||
x: 443
|
||
y: 12
|
||
width: 24
|
||
height: 24
|
||
pad_all: 0
|
||
border_width: 0
|
||
bg_opa: TRANSP
|
||
scrollable: false
|
||
widgets:
|
||
- label:
|
||
align: CENTER
|
||
text: "+"
|
||
text_font: headline
|
||
styles: paint_ink
|
||
- obj:
|
||
x: 443
|
||
y: 443
|
||
width: 24
|
||
height: 24
|
||
pad_all: 0
|
||
border_width: 0
|
||
bg_opa: TRANSP
|
||
scrollable: false
|
||
widgets:
|
||
- label:
|
||
align: CENTER
|
||
text: "+"
|
||
text_font: headline
|
||
styles: paint_ink
|
||
- obj:
|
||
x: 12
|
||
y: 443
|
||
width: 24
|
||
height: 24
|
||
pad_all: 0
|
||
border_width: 0
|
||
bg_opa: TRANSP
|
||
scrollable: false
|
||
widgets:
|
||
- label:
|
||
align: CENTER
|
||
text: "+"
|
||
text_font: headline
|
||
styles: paint_ink
|
||
- obj:
|
||
x: 228
|
||
y: 228
|
||
width: 24
|
||
height: 24
|
||
pad_all: 0
|
||
border_width: 0
|
||
bg_opa: TRANSP
|
||
scrollable: false
|
||
widgets:
|
||
- label:
|
||
align: CENTER
|
||
text: "+"
|
||
text_font: headline
|
||
styles: paint_ink
|
||
|
||
script:
|
||
- id: capture_ui_snapshot
|
||
mode: single
|
||
then:
|
||
- lambda: guition::snapshot_begin();
|
||
- repeat:
|
||
count: 240
|
||
then:
|
||
- lambda: guition::snapshot_next_row();
|
||
- delay: 100ms
|
||
# Render every overlay and both pages repeatedly without controlling devices.
|
||
- id: ui_self_test
|
||
mode: single
|
||
then:
|
||
- logger.log: "UI_TEST START: page/overlay render stress, no HA actions"
|
||
- lambda: 'ESP_LOGI("ui_test", "Light sliders: %s", light_controls::self_test() ? "PASS" : "FAIL");'
|
||
- lambda: 'id(calibration_active) = false;'
|
||
- lvgl.page.show: home_page
|
||
- script.execute: wake_display
|
||
- repeat:
|
||
count: 10
|
||
then:
|
||
- lambda: |-
|
||
id(last_touch_ms) = millis();
|
||
id(last_use_ms) = millis();
|
||
static unsigned test_page=0;
|
||
id(tile_page) = runtime_tiles::enabled ? test_page++ % runtime_tiles::page_count() : 1;
|
||
- script.execute: show_tile_page
|
||
- delay: 300ms
|
||
- lambda: |-
|
||
lv_obj_update_layout(id(tile_scroll));
|
||
const int count = runtime_tiles::model.count;
|
||
// Wide cards take two slots, so visibility follows the runtime packing.
|
||
const bool paginated = runtime_tiles::enabled ? runtime_tiles::page_count() > 1 : count > 6;
|
||
lv_obj_t* tiles[] = {id(tile1), id(tile2), id(tile3), id(tile4), id(tile5),
|
||
id(tile6), id(tile7), id(tile8), id(tile9), id(tile10)};
|
||
bool ok = guition::check_geometry(id(home_page)->obj);
|
||
for (int i = 0; i < 10; ++i) {
|
||
bool visible = runtime_tiles::enabled ? i<6 && runtime_tiles::widgets[i].index<runtime_tiles::model.count : i < count && (paginated ? i >= 6 : i < 6);
|
||
ok = ok && (lv_obj_has_flag(tiles[i], LV_OBJ_FLAG_HIDDEN) != visible);
|
||
}
|
||
for (lv_obj_t* control : {id(page_prev), id(page_next), id(page_number)})
|
||
ok = ok && (lv_obj_has_flag(control, LV_OBJ_FLAG_HIDDEN) != paginated);
|
||
lv_area_t nav, grid;
|
||
lv_obj_get_coords(id(page_next), &nav);
|
||
lv_obj_get_coords(id(tile_scroll), &grid);
|
||
ok = ok && (!paginated || grid.y2 < nav.y1);
|
||
if(runtime_tiles::enabled)ok = runtime_tiles::check_tile_geometry() && ok;
|
||
ESP_LOGI("ui_test", "page_check=%s count=%d heap=%u frames=%u grid_bottom=%d nav_top=%d", ok ? "PASS" : "FAIL", count, (unsigned) esp_get_free_heap_size(), (unsigned) id(rendered_frames), grid.y2, nav.y1);
|
||
id(tile_page) = 0;
|
||
- script.execute: show_tile_page
|
||
- delay: 150ms
|
||
- lvgl.widget.show: brightness_overlay
|
||
- delay: 150ms
|
||
- lambda: guition::check_geometry(id(brightness_overlay));
|
||
- lvgl.widget.hide: brightness_overlay
|
||
- lvgl.widget.show: color_detail_overlay
|
||
- delay: 150ms
|
||
- lambda: guition::check_geometry(id(color_detail_overlay));
|
||
- lvgl.widget.hide: color_detail_overlay
|
||
- lvgl.widget.show: climate_detail_overlay
|
||
- delay: 150ms
|
||
- lambda: guition::check_geometry(id(climate_detail_overlay));
|
||
- lvgl.widget.hide: climate_detail_overlay
|
||
- lvgl.widget.show: climate_mode_overlay
|
||
- delay: 150ms
|
||
- lambda: guition::check_geometry(id(climate_mode_overlay));
|
||
- lvgl.widget.hide: climate_mode_overlay
|
||
- lvgl.widget.show: alert_overlay
|
||
- delay: 150ms
|
||
- lvgl.widget.hide: alert_overlay
|
||
- lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();'
|
||
- logger.log: "UI_TEST COMPLETE"
|
||
|
||
- id: show_tile_page
|
||
then:
|
||
- lambda: |-
|
||
if(runtime_tiles::enabled){
|
||
runtime_tiles::show_page(id(tile_page),id(page_prev),id(page_next),id(page_number));
|
||
return;
|
||
}
|
||
lv_obj_t* tiles[] = {id(tile1), id(tile2), id(tile3), id(tile4), id(tile5),
|
||
id(tile6), id(tile7), id(tile8), id(tile9), id(tile10)};
|
||
const int rows[] = {${GRID_ROW1_Y}, ${GRID_ROW2_Y}, ${GRID_ROW3_Y}};
|
||
const int count = runtime_tiles::model.count;
|
||
const bool paginated = count > 6;
|
||
if (!paginated) id(tile_page) = 0;
|
||
for (lv_obj_t* control : {id(page_prev), id(page_next), id(page_number)}) {
|
||
if (paginated) lv_obj_clear_flag(control, LV_OBJ_FLAG_HIDDEN);
|
||
else lv_obj_add_flag(control, LV_OBJ_FLAG_HIDDEN);
|
||
}
|
||
for (int i = 0; i < 10; ++i) {
|
||
bool visible = i < count && (id(tile_page) == 0 ? i < 6 : i >= 6);
|
||
if (visible) {
|
||
lv_obj_set_y(tiles[i], rows[(i < 6 ? i : i - 6) / 2]);
|
||
lv_obj_clear_flag(tiles[i], LV_OBJ_FLAG_HIDDEN);
|
||
} else {
|
||
lv_obj_add_flag(tiles[i], LV_OBJ_FLAG_HIDDEN);
|
||
}
|
||
}
|
||
lv_label_set_text(id(page_number), id(tile_page) == 0 ? "1 / 2" : "2 / 2");
|
||
if (id(tile_page) == 0) {
|
||
lv_obj_add_state(id(page_prev), LV_STATE_DISABLED);
|
||
lv_obj_clear_state(id(page_next), LV_STATE_DISABLED);
|
||
} else {
|
||
lv_obj_clear_state(id(page_prev), LV_STATE_DISABLED);
|
||
lv_obj_add_state(id(page_next), LV_STATE_DISABLED);
|
||
}
|
||
|
||
# Runtime changes do not count as touches and do not reset the idle timer.
|
||
- id: apply_screen_settings
|
||
mode: restart
|
||
then:
|
||
- lambda: |-
|
||
const auto &settings = screen_settings::current;
|
||
// Dark mode (firmware 0.2.54+): a look the screen already has costs nothing.
|
||
theme::set_dark(settings_screen::dark_mode != 0);
|
||
if(runtime_tiles::enabled && static_cast<unsigned>(id(screen_lvgl).get_rotation())!=runtime_tiles::rotation){
|
||
cyd::touch_guard.consume();
|
||
if(lv_indev_get_act())lv_indev_wait_release(lv_indev_get_act());
|
||
id(screen_lvgl).set_rotation(static_cast<esphome::display::DisplayRotation>(runtime_tiles::rotation));
|
||
}
|
||
// Auto standby off wakes the screen, but not out of a Sleep pressed in Home Assistant.
|
||
if (id(display_dimmed) && !settings.standby_enabled && !id(sleep_requested)) {
|
||
id(display_dimmed) = false;
|
||
}
|
||
auto now = id(homeassistant_time).now();
|
||
const int minute = now.is_valid() ? now.hour * 60 + now.minute : -1;
|
||
const int level = id(display_dimmed) ? settings.dim_level(minute) : settings.brightness;
|
||
// Standby dims slowly; the tile redraws below no longer interrupt the hardware fade.
|
||
id(set_backlight).execute(level, id(display_dimmed) ? 1500 : 300);
|
||
if (id(display_dimmed)) { lv_obj_clear_flag(id(dim_wake_overlay), LV_OBJ_FLAG_HIDDEN); lv_obj_move_foreground(id(dim_wake_overlay)); }
|
||
else lv_obj_add_flag(id(dim_wake_overlay), LV_OBJ_FLAG_HIDDEN);
|
||
// The setting numbers and times only publish a changed value (HA keeps identical states anyway); the
|
||
// switches publish their own changes. An open settings page shows a change from Home Assistant too.
|
||
// A number without a state publishes too: a night brightness of 0 equals the number's initial state and
|
||
// stayed "unknown" in Home Assistant before firmware 0.2.50.
|
||
for (auto [entity, value] : {std::make_pair(id(setting_brightness), settings.brightness),
|
||
std::make_pair(id(setting_standby_brightness), settings.standby_brightness),
|
||
std::make_pair(id(setting_night_brightness), settings.night_brightness),
|
||
std::make_pair(id(setting_standby_seconds), settings.standby_seconds),
|
||
std::make_pair(id(setting_auto_home_seconds), settings_screen::auto_home_seconds)}) {
|
||
if (!entity->has_state() || entity->state != value) entity->publish_state(value);
|
||
}
|
||
for (auto [entity, minutes] : {std::make_pair(id(setting_night_start), settings.night_start), std::make_pair(id(setting_night_end), settings.night_end)}) {
|
||
if (!entity->has_state() || entity->hour * 60 + entity->minute != minutes) entity->update();
|
||
}
|
||
const auto shown_rotation = id(setting_rotation)->active_index();
|
||
if (!id(setting_rotation)->has_state() || !shown_rotation.has_value() || *shown_rotation != (size_t) (settings_screen::rotation / 90)) id(setting_rotation)->update();
|
||
settings_screen::refresh();
|
||
if (!id(ui_refresh).is_running()) id(ui_refresh).execute();
|
||
|
||
# Every card closes through runtime_tiles::dismiss (set in on_boot): the runtime detail card, the light,
|
||
# colour and climate overlays, and active_entity, which the swipe guard reads.
|
||
- id: close_cards
|
||
then:
|
||
- lambda: 'if (runtime_tiles::dismiss) runtime_tiles::dismiss();'
|
||
|
||
# Everything back to the overview: the settings page, every card and the first page of tiles.
|
||
# Used by standby, by the "Back to page 1" timeout and by the Home Assistant action.
|
||
- id: go_home
|
||
then:
|
||
- lambda: 'settings_screen::close();'
|
||
- script.execute: close_cards
|
||
- lambda: 'id(tile_page) = 0;'
|
||
- script.execute: show_tile_page
|
||
|
||
# Back to page 1 by itself (0.2.44+). A card, a settings page or a second page left open keeps
|
||
# showing what someone looked at minutes ago; after this long the screen is a screen again.
|
||
# Independent of standby, which may be off or much later; paused while the screen is in standby.
|
||
# The time runs from the last touch (last_use_ms, firmware 0.2.56+): Wake, an alert or Auto standby
|
||
# light the screen and restart the standby time, but nobody used a page. Checked every second and by Wake.
|
||
- id: back_to_page_1_when_due
|
||
then:
|
||
- if:
|
||
condition:
|
||
lambda: |-
|
||
if (!runtime_tiles::auto_home || id(touch_down) || id(calibration_active) || id(alert_active)) return false;
|
||
if (id(display_dimmed)) return false;
|
||
bool away = settings_screen::visible() || !id(active_entity).empty() || id(tile_page) != 0 ||
|
||
(runtime_tiles::detail_root && !lv_obj_has_flag(runtime_tiles::detail_root, LV_OBJ_FLAG_HIDDEN));
|
||
return away && (millis() - id(last_use_ms)) > (uint32_t) runtime_tiles::auto_home_seconds * 1000UL;
|
||
then:
|
||
- script.execute: go_home
|
||
|
||
# ---------- ALERT (HA action show_alert) ----------
|
||
# The screen wakes, keeps its normal brightness and shows the card until OK or the
|
||
# timeout (0: until OK); the standby interval waits on alert_active. A new alert
|
||
# replaces the current one; every ending reports itself as esphome.screen_alert.
|
||
- id: alert_show
|
||
mode: restart
|
||
parameters:
|
||
title: string
|
||
subtitle: string
|
||
icon: string
|
||
color: string
|
||
button_text: string
|
||
timeout: int
|
||
flash: bool
|
||
then:
|
||
- lambda: |-
|
||
if (id(alert_active)) id(alert_report).execute("replaced");
|
||
// An alert is the one thing that outranks the settings page, so that closes first.
|
||
settings_screen::close();
|
||
const auto alert = screen_alert::make(title, subtitle, icon, color, button_text, timeout, flash, ${ALERT_TITLE_MAX}, ${ALERT_SUBTITLE_MAX}, ${ALERT_BUTTON_MAX});
|
||
id(alert_active) = true;
|
||
id(alert_title_text) = alert.title;
|
||
id(alert_timeout_seconds) = alert.timeout_seconds;
|
||
id(alert_flash_requested) = alert.flash;
|
||
lv_label_set_text(id(alert_icon), alert.icon.c_str());
|
||
lv_label_set_text(id(alert_title), alert.title.c_str());
|
||
lv_label_set_text(id(alert_subtitle), alert.subtitle.c_str());
|
||
lv_label_set_text(id(alert_ok_label), alert.button.c_str());
|
||
id(alert_card_color) = alert.color;
|
||
lv_obj_set_style_bg_color(id(alert_card), lv_color_hex(theme::surface(alert.color)), LV_PART_MAIN);
|
||
ESP_LOGI("alert", "show \"%s\" timeout=%d flash=%d", alert.title.c_str(), alert.timeout_seconds, (int) alert.flash);
|
||
- script.execute: wake_display
|
||
- lvgl.widget.show: alert_overlay
|
||
- script.stop: alert_timeout
|
||
- if:
|
||
condition:
|
||
lambda: 'return id(alert_flash_requested);'
|
||
then:
|
||
- script.execute: alert_flash
|
||
- if:
|
||
condition:
|
||
lambda: 'return id(alert_timeout_seconds) > 0;'
|
||
then:
|
||
- script.execute:
|
||
id: alert_timeout
|
||
seconds: !lambda 'return id(alert_timeout_seconds);'
|
||
|
||
- id: alert_dismiss
|
||
parameters:
|
||
reason: string
|
||
then:
|
||
- if:
|
||
condition:
|
||
lambda: 'return id(alert_active);'
|
||
then:
|
||
- lambda: |-
|
||
id(alert_active) = false;
|
||
id(last_touch_ms) = millis();
|
||
ESP_LOGI("alert", "dismissed: %s", reason.c_str());
|
||
- lvgl.widget.hide: alert_overlay
|
||
- script.stop: alert_timeout
|
||
- script.stop: alert_flash
|
||
- lambda: 'id(set_backlight).execute(screen_settings::current.brightness, 80);'
|
||
- script.execute:
|
||
id: alert_report
|
||
action: !lambda 'return reason;'
|
||
|
||
# Four backlight blinks when the alert arrives; the screen then stays on as usual.
|
||
- id: alert_flash
|
||
mode: restart
|
||
then:
|
||
- repeat:
|
||
count: 4
|
||
then:
|
||
- lambda: 'id(set_backlight).execute(0, 60);'
|
||
- delay: 140ms
|
||
- lambda: 'id(set_backlight).execute(screen_settings::current.brightness, 60);'
|
||
- delay: 180ms
|
||
|
||
- id: alert_timeout
|
||
mode: restart
|
||
parameters:
|
||
seconds: int
|
||
then:
|
||
- delay: !lambda 'return static_cast<uint32_t>(seconds) * 1000UL;'
|
||
- script.execute:
|
||
id: alert_dismiss
|
||
reason: "timeout"
|
||
|
||
# Home Assistant adds device_id; action is ok, timeout, replaced or remote.
|
||
- id: alert_report
|
||
parameters:
|
||
action: string
|
||
then:
|
||
- homeassistant.event:
|
||
event: esphome.screen_alert
|
||
data:
|
||
action: !lambda 'return action;'
|
||
title: !lambda 'return id(alert_title_text);'
|
||
screen: !lambda 'return App.get_name();'
|
||
|
||
- id: wake_display
|
||
mode: restart
|
||
then:
|
||
- lambda: |-
|
||
id(last_touch_ms) = millis();
|
||
id(display_dimmed) = false;
|
||
id(sleep_requested) = false;
|
||
// Almost instant brightness-up. ESPHome's default is 1s, and that feels sluggish
|
||
// when you touch the screen and are waiting on light.
|
||
id(set_backlight).execute(screen_settings::current.brightness, 80);
|
||
- lvgl.widget.hide: dim_wake_overlay
|
||
- script.execute: close_cards
|
||
|
||
# One owner for the backlight level: the LEDC fader steps the duty itself, so
|
||
# LVGL redraws cannot make the transition stutter. A new call interrupts the
|
||
# running fade from its current duty; afterwards ESPHome's light state is synced.
|
||
- id: set_backlight
|
||
mode: restart
|
||
parameters:
|
||
level: int
|
||
ms: int
|
||
then:
|
||
- lambda: |-
|
||
if (!backlight_fade::start(*id(gpio_backlight_pwm), *id(back_light), level / 100.0f, ms))
|
||
id(back_light).turn_on().set_transition_length(ms).set_brightness(level / 100.0f).perform();
|
||
- delay: !lambda "return ms + 40;"
|
||
- lambda: backlight_fade::sync(*id(gpio_backlight_pwm), *id(back_light), level / 100.0f);
|
||
|
||
# Standby: the standby time ran out (the 1 s interval checks Auto standby) or Sleep was pressed.
|
||
# The settings page and every card close; with "also on standby" the screen goes back to page 1 too.
|
||
- id: dim_display
|
||
mode: restart
|
||
then:
|
||
- lambda: |-
|
||
id(display_dimmed) = true;
|
||
id(apply_screen_settings).execute();
|
||
- if:
|
||
condition:
|
||
lambda: 'return screen_settings::current.home_on_standby;'
|
||
then:
|
||
- script.execute: go_home
|
||
else:
|
||
- lambda: 'settings_screen::close();'
|
||
- script.execute: close_cards
|
||
|
||
# A -/+ tap on the climate card: the number follows at once, the call waits for the last tap.
|
||
# Holding a key steps three times a second.
|
||
- id: climate_adjust_target
|
||
parameters:
|
||
direction: int
|
||
held: bool
|
||
then:
|
||
- lambda: |-
|
||
if (held && millis() - id(climate_edit_ms) < 300) return;
|
||
int step_x10 = std::max(5, id(active_climate_step_x10));
|
||
int next = cyd::quantize_temperature(id(active_climate_target_x10) + direction * step_x10,
|
||
id(active_climate_min_x10), id(active_climate_max_x10), step_x10);
|
||
id(climate_edit_ms) = millis();
|
||
id(climate_edit_pending) = true;
|
||
if (next == id(active_climate_target_x10) && id(active_climate_target_known)) return;
|
||
id(active_climate_target_x10) = next;
|
||
id(active_climate_target_known) = true;
|
||
id(climate_card_refresh).execute();
|
||
id(climate_target_send).execute(id(active_entity), next / 10.0f);
|
||
|
||
# One set_temperature for a series of taps: every tap restarts the wait with the newest value.
|
||
# Entity and value travel along, so leaving the card right after a tap still sends it.
|
||
- id: climate_target_send
|
||
mode: restart
|
||
parameters:
|
||
entity: string
|
||
value: float
|
||
then:
|
||
- delay: 700ms
|
||
- if:
|
||
condition:
|
||
lambda: 'return runtime_tiles::fresh() && !entity.empty();'
|
||
then:
|
||
- homeassistant.action:
|
||
action: climate.set_temperature
|
||
data:
|
||
entity_id: !lambda "return entity;"
|
||
temperature: !lambda "return value;"
|
||
|
||
# Draws the climate card from the active_climate_* globals: status line, setpoint, -/+ and mode keys.
|
||
- id: climate_card_refresh
|
||
then:
|
||
- lambda: |-
|
||
auto lower = [](std::string v) {
|
||
for (char &c : v) { if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a'); }
|
||
return v;
|
||
};
|
||
std::string mode = lower(id(active_climate_mode));
|
||
bool off = mode == "off" || mode.empty() || mode == "unknown" || mode == "unavailable";
|
||
// What the device does (Heating), else its mode (Heat); then the room.
|
||
std::string status = off ? std::string("Off") : std::string(tile_controls::climate_action_text(lower(id(active_climate_action))));
|
||
if (status.empty()) status = tile_controls::climate_mode_text(mode);
|
||
char part[32];
|
||
if (std::isfinite(id(active_climate_current))) { snprintf(part, sizeof(part), " · Now %.1f°", id(active_climate_current)); status += part; }
|
||
if (id(active_climate_humidity) >= 0) { snprintf(part, sizeof(part), " · %d%%", id(active_climate_humidity)); status += part; }
|
||
lv_label_set_text(id(climate_current_label), status.c_str());
|
||
|
||
int target = id(active_climate_target_x10);
|
||
std::string value = id(active_climate_target_known)
|
||
? tile_controls::format_value(target / 10.0f, id(active_climate_step_x10) / 10.0f, "°") : std::string("--");
|
||
lv_label_set_text(id(climate_target_label), value.c_str());
|
||
lv_obj_set_style_text_color(id(climate_target_label), theme::color(off ? theme::OFF : theme::INK), LV_PART_MAIN);
|
||
auto enable = [](lv_obj_t *key, bool on) {
|
||
if (on) lv_obj_remove_state(key, LV_STATE_DISABLED); else lv_obj_add_state(key, LV_STATE_DISABLED);
|
||
};
|
||
enable(id(climate_minus_button), !id(active_climate_target_known) || target > id(active_climate_min_x10));
|
||
enable(id(climate_plus_button), !id(active_climate_target_known) || target < id(active_climate_max_x10));
|
||
|
||
// One key per supported mode, in Home Assistant's order. Fan and swing add "more"; with it
|
||
// five mode keys fit, and when a sixth mode is in use it takes the last place.
|
||
std::string supported = lower(id(active_climate_supported_modes));
|
||
auto has = [&](const char *m) {
|
||
return supported.find(std::string("\"") + m + "\"") != std::string::npos ||
|
||
supported.find(std::string("'") + m + "'") != std::string::npos;
|
||
};
|
||
struct Key { lv_obj_t *obj; const char *mode; };
|
||
const Key keys[] = {{id(cmkey_auto), "auto"}, {id(cmkey_heat_cool), "heat_cool"}, {id(cmkey_heat), "heat"},
|
||
{id(cmkey_cool), "cool"}, {id(cmkey_dry), "dry"}, {id(cmkey_fan_only), "fan_only"}};
|
||
// The Guition shows fan and swing on their own card below, so no "more" key.
|
||
bool more = false;
|
||
const Key *visible[6];
|
||
unsigned count = 0, room = more ? 5 : 6;
|
||
for (const auto &k : keys) if (has(k.mode)) visible[count++] = &k;
|
||
if (count > room) {
|
||
for (unsigned i = room; i < count; ++i) if (mode == visible[i]->mode) visible[room - 1] = visible[i];
|
||
count = room;
|
||
}
|
||
for (const auto &k : keys) {
|
||
bool show = false;
|
||
for (unsigned i = 0; i < count; ++i) show = show || visible[i] == &k;
|
||
if (show) lv_obj_remove_flag(k.obj, LV_OBJ_FLAG_HIDDEN); else lv_obj_add_flag(k.obj, LV_OBJ_FLAG_HIDDEN);
|
||
bool selected = show && !off && mode == k.mode;
|
||
uint32_t color = tile_controls::mode_color(k.mode);
|
||
lv_obj_set_style_bg_color(k.obj, selected ? lv_color_hex(color) : theme::color(theme::CARD), LV_PART_MAIN);
|
||
lv_obj_set_style_border_color(k.obj, selected ? lv_color_hex(color) : theme::color(theme::LINE), LV_PART_MAIN);
|
||
lv_obj_set_style_text_color(lv_obj_get_child(k.obj, 0), theme::color(selected ? theme::ON_ACCENT : theme::SLATE), LV_PART_MAIN);
|
||
}
|
||
if (more) lv_obj_remove_flag(id(cmkey_more), LV_OBJ_FLAG_HIDDEN); else lv_obj_add_flag(id(cmkey_more), LV_OBJ_FLAG_HIDDEN);
|
||
// One mode is no choice: the power key already turns it on and off.
|
||
const bool show_keys = count > 1 || more;
|
||
if (show_keys) lv_obj_remove_flag(id(climate_mode_keys), LV_OBJ_FLAG_HIDDEN);
|
||
else lv_obj_add_flag(id(climate_mode_keys), LV_OBJ_FLAG_HIDDEN);
|
||
|
||
// Fan and swing rows, in the entity's own names and order.
|
||
climate_card::Row settings[2];
|
||
int rows = 0;
|
||
if (!cyd::list_item(id(active_climate_fan_modes), 0).empty())
|
||
settings[rows++] = {'f', "\U000F0210", id(active_climate_fan_modes), id(active_climate_fan_mode)};
|
||
if (!cyd::list_item(id(active_climate_swing_modes), 0).empty())
|
||
settings[rows++] = {'s', "\U000F1C91", id(active_climate_swing_modes), id(active_climate_swing_mode)};
|
||
// One layout per card: the setpoint gives room to the mode keys and the settings below it,
|
||
// and a card with nothing below keeps the setpoint centred.
|
||
static int laid_out = -1;
|
||
if (laid_out != rows * 2 + show_keys) {
|
||
laid_out = rows * 2 + show_keys;
|
||
struct Layout { int status, card_y, card_h, key, number_y, caption_y, modes_y, modes_h, settings_y, edge, row_h, gap; };
|
||
static const Layout layouts[6] = {
|
||
// no mode keys: 0, 1, 2 rows
|
||
{92, 180, 200, 116, -14, 46, 0, 0, 0, 14, 48, 10},
|
||
{92, 140, 200, 116, -14, 46, 0, 0, 364, 14, 48, 10},
|
||
{90, 130, 180, 108, -12, 42, 0, 0, 330, 14, 46, 10},
|
||
// mode keys: 0, 1, 2 rows
|
||
{92, 136, 200, 116, -14, 46, 364, 76, 0, 14, 48, 10},
|
||
{90, 126, 170, 108, -12, 42, 312, 68, 394, 12, 44, 8},
|
||
{86, 118, 140, 96, -10, 36, 270, 60, 342, 12, 42, 8},
|
||
};
|
||
const Layout &l = layouts[(show_keys ? 3 : 0) + rows];
|
||
lv_obj_set_y(id(climate_current_label), l.status);
|
||
lv_obj_set_y(id(climate_setpoint_card), l.card_y);
|
||
lv_obj_set_height(id(climate_setpoint_card), l.card_h);
|
||
for (lv_obj_t *button : {id(climate_minus_button), id(climate_plus_button)}) {
|
||
lv_obj_set_size(button, l.key, l.key);
|
||
lv_obj_set_style_radius(button, l.key / 2, LV_PART_MAIN);
|
||
}
|
||
lv_obj_set_y(id(climate_target_label), l.number_y);
|
||
lv_obj_set_y(id(climate_set_label), l.caption_y);
|
||
if (show_keys) {
|
||
lv_obj_set_y(id(climate_mode_keys), l.modes_y);
|
||
lv_obj_set_height(id(climate_mode_keys), l.modes_h);
|
||
for (lv_obj_t *mode_key : {id(cmkey_auto), id(cmkey_heat_cool), id(cmkey_heat), id(cmkey_cool), id(cmkey_dry), id(cmkey_fan_only), id(cmkey_more)}) {
|
||
lv_obj_set_height(mode_key, l.modes_h);
|
||
lv_obj_set_style_radius(mode_key, l.modes_h / 2, LV_PART_MAIN);
|
||
}
|
||
}
|
||
lv_obj_set_y(id(climate_settings_card), l.settings_y);
|
||
climate_card::look.edge = l.edge;
|
||
climate_card::look.row_h = l.row_h;
|
||
climate_card::look.gap = l.gap;
|
||
}
|
||
climate_card::show(settings, rows);
|
||
id(climate_power_refresh).execute();
|
||
|
||
# Opens the mode picker. Hides modes the entity doesn't support and marks
|
||
# the current one, so you see where you stand before you pick something.
|
||
# Opens the vacuum controls and remembers which entity it concerns.
|
||
|
||
# A standalone command: start, pause, stop or return_to_base.
|
||
|
||
# Suction power is a separate axis, apart from start/stop.
|
||
|
||
- id: open_climate_mode_picker
|
||
mode: restart
|
||
then:
|
||
- lambda: |-
|
||
auto lower = [](std::string v) {
|
||
for (char &c : v) { if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a'); }
|
||
return v;
|
||
};
|
||
std::string supported = lower(id(active_climate_supported_modes));
|
||
std::string current = lower(id(active_climate_mode));
|
||
|
||
auto has = [&](const char* m) {
|
||
std::string mode(m);
|
||
if (supported.empty() || supported == "unknown" || supported == "unavailable") return false;
|
||
return supported.find("'" + mode + "'") != std::string::npos ||
|
||
supported.find("\"" + mode + "\"") != std::string::npos;
|
||
};
|
||
auto apply = [&](lv_obj_t* btn, const char* m) {
|
||
if (has(m)) lv_obj_clear_flag(btn, LV_OBJ_FLAG_HIDDEN);
|
||
else lv_obj_add_flag(btn, LV_OBJ_FLAG_HIDDEN);
|
||
bool selected = (current == std::string(m));
|
||
lv_obj_set_style_bg_color(btn, theme::color(selected ? theme::ACCENT_BRIGHT : theme::CARD), LV_PART_MAIN);
|
||
lv_obj_set_style_text_color(lv_obj_get_child(btn, 0), theme::color(selected ? theme::ON_ACCENT : theme::INK), LV_PART_MAIN);
|
||
};
|
||
apply(id(cmode_off), "off");
|
||
apply(id(cmode_heat), "heat");
|
||
apply(id(cmode_cool), "cool");
|
||
apply(id(cmode_heat_cool), "heat_cool");
|
||
apply(id(cmode_auto), "auto");
|
||
apply(id(cmode_dry), "dry");
|
||
apply(id(cmode_fan_only), "fan_only");
|
||
// Fan and swing rows: the entity's own mode names, at most four each, as Home Assistant reports them.
|
||
auto pretty = [](std::string raw) { if (!raw.empty() && raw[0] >= 'a' && raw[0] <= 'z') raw[0] = (char)(raw[0] - 'a' + 'A'); return raw; };
|
||
auto fill = [&](lv_obj_t *title, lv_obj_t *chips[4], const std::string &modes, const std::string ¤t_mode) {
|
||
unsigned shown = 0;
|
||
for (unsigned i = 0; i < 4; ++i) {
|
||
std::string raw = cyd::list_item(modes, i);
|
||
if (raw.empty()) { lv_obj_add_flag(chips[i], LV_OBJ_FLAG_HIDDEN); continue; }
|
||
lv_obj_clear_flag(chips[i], LV_OBJ_FLAG_HIDDEN); ++shown;
|
||
lv_label_set_text(lv_obj_get_child(chips[i], 0), pretty(raw).c_str());
|
||
lv_obj_set_style_bg_color(chips[i], theme::color(raw == current_mode ? theme::ACCENT_BRIGHT : theme::CARD), LV_PART_MAIN);
|
||
lv_obj_set_style_text_color(lv_obj_get_child(chips[i], 0), theme::color(raw == current_mode ? theme::ON_ACCENT : theme::INK), LV_PART_MAIN);
|
||
}
|
||
if (shown) lv_obj_clear_flag(title, LV_OBJ_FLAG_HIDDEN); else lv_obj_add_flag(title, LV_OBJ_FLAG_HIDDEN);
|
||
};
|
||
lv_obj_t *fan_chips[4] = {id(cfan_0), id(cfan_1), id(cfan_2), id(cfan_3)};
|
||
lv_obj_t *swing_chips[4] = {id(cswing_0), id(cswing_1), id(cswing_2), id(cswing_3)};
|
||
fill(id(cfan_title), fan_chips, id(active_climate_fan_modes), id(active_climate_fan_mode));
|
||
fill(id(cswing_title), swing_chips, id(active_climate_swing_modes), id(active_climate_swing_mode));
|
||
- lvgl.widget.show: climate_mode_overlay
|
||
|
||
# Sets exactly one mode. No intermediate steps, so no unwanted
|
||
# commands to the heat pump along the way.
|
||
# Fan and swing modes from the picker: one call with the entity's own mode name.
|
||
- id: set_climate_fan_mode
|
||
mode: restart
|
||
parameters:
|
||
index: int
|
||
then:
|
||
- lambda: |-
|
||
id(active_climate_fan_mode) = cyd::list_item(id(active_climate_fan_modes), index);
|
||
id(climate_edit_ms) = millis();
|
||
id(climate_edit_pending) = true;
|
||
id(climate_card_refresh).execute();
|
||
- lvgl.widget.hide: climate_mode_overlay
|
||
- if:
|
||
condition:
|
||
lambda: 'return runtime_tiles::fresh() && !id(active_climate_fan_mode).empty();'
|
||
then:
|
||
- homeassistant.action:
|
||
action: climate.set_fan_mode
|
||
data:
|
||
entity_id: !lambda "return id(active_entity);"
|
||
fan_mode: !lambda "return id(active_climate_fan_mode);"
|
||
- id: set_climate_swing_mode
|
||
mode: restart
|
||
parameters:
|
||
index: int
|
||
then:
|
||
- lambda: |-
|
||
id(active_climate_swing_mode) = cyd::list_item(id(active_climate_swing_modes), index);
|
||
id(climate_edit_ms) = millis();
|
||
id(climate_edit_pending) = true;
|
||
id(climate_card_refresh).execute();
|
||
- lvgl.widget.hide: climate_mode_overlay
|
||
- if:
|
||
condition:
|
||
lambda: 'return runtime_tiles::fresh() && !id(active_climate_swing_mode).empty();'
|
||
then:
|
||
- homeassistant.action:
|
||
action: climate.set_swing_mode
|
||
data:
|
||
entity_id: !lambda "return id(active_entity);"
|
||
swing_mode: !lambda "return id(active_climate_swing_mode);"
|
||
|
||
# Quick on/off in the climate card; climate.turn_on restores the last mode in Home Assistant.
|
||
- id: climate_power_toggle
|
||
mode: restart
|
||
then:
|
||
- lambda: |-
|
||
std::string mode = id(active_climate_mode);
|
||
for (char &c : mode) if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a');
|
||
id(active_climate_power_on) = mode == "off" || mode.empty() || mode == "unknown";
|
||
id(climate_edit_ms) = millis();
|
||
id(climate_edit_pending) = true;
|
||
if (!id(active_climate_power_on)) id(active_climate_mode) = "off";
|
||
id(climate_card_refresh).execute();
|
||
- if:
|
||
condition:
|
||
lambda: 'return runtime_tiles::fresh() && id(active_climate_power_on);'
|
||
then:
|
||
- homeassistant.action:
|
||
action: climate.turn_on
|
||
data:
|
||
entity_id: !lambda "return id(active_entity);"
|
||
- if:
|
||
condition:
|
||
lambda: 'return runtime_tiles::fresh() && !id(active_climate_power_on);'
|
||
then:
|
||
- homeassistant.action:
|
||
action: climate.turn_off
|
||
data:
|
||
entity_id: !lambda "return id(active_entity);"
|
||
|
||
# The power key lights up while the device runs in any mode.
|
||
- id: climate_power_refresh
|
||
mode: restart
|
||
then:
|
||
- lambda: |-
|
||
std::string mode = id(active_climate_mode);
|
||
for (char &c : mode) if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a');
|
||
bool on = !(mode == "off" || mode.empty() || mode == "unknown" || mode == "unavailable");
|
||
if (id(climate_edit_pending)) on = id(active_climate_power_on);
|
||
lv_obj_set_style_bg_color(id(climate_power_button), theme::color(on ? theme::ACCENT_TINT : theme::KEY), LV_PART_MAIN);
|
||
lv_obj_set_style_text_color(id(climate_power_icon), theme::color(on ? theme::ACCENT_ICON : theme::ICON_OFF), LV_PART_MAIN);
|
||
|
||
- id: set_climate_mode
|
||
mode: restart
|
||
parameters:
|
||
mode: string
|
||
then:
|
||
- lambda: |-
|
||
id(climate_edit_ms) = millis();
|
||
id(climate_edit_pending) = true;
|
||
id(active_climate_mode) = mode;
|
||
// The action line would still say what the old mode did.
|
||
id(active_climate_action).clear();
|
||
id(active_climate_power_on) = mode != "off";
|
||
id(climate_card_refresh).execute();
|
||
- lvgl.widget.hide: climate_mode_overlay
|
||
- if:
|
||
condition:
|
||
lambda: 'return runtime_tiles::fresh();'
|
||
then:
|
||
- homeassistant.action:
|
||
action: climate.set_hvac_mode
|
||
data:
|
||
entity_id: !lambda "return id(active_entity);"
|
||
hvac_mode: !lambda "return id(active_climate_mode);"
|
||
|
||
# Isolated 40-second preview; every light commit is suppressed in the helper.
|
||
- id: light_controls_preview
|
||
mode: single
|
||
then:
|
||
- script.execute: wake_display
|
||
- lambda: |-
|
||
light_controls::demo = true;
|
||
light_controls::fallback.minimum = 2000;
|
||
light_controls::fallback.maximum = 6500;
|
||
light_controls::fallback.kelvin = 3000;
|
||
light_controls::fallback.hue = 200;
|
||
light_controls::open("diagnostic", true, true, 75);
|
||
lv_label_set_text(id(color_detail_title), "Example lamp");
|
||
- lvgl.widget.hide: color_close_button
|
||
- lvgl.widget.show: color_detail_overlay
|
||
- delay: 40s
|
||
- lvgl.widget.hide: color_detail_overlay
|
||
- lvgl.widget.show: color_close_button
|
||
- lambda: 'light_controls::demo = false; light_controls::fallback = light_controls::State{};'
|
||
|
||
- id: open_color_detail
|
||
mode: restart
|
||
then:
|
||
- 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));
|
||
- lvgl.widget.hide: brightness_overlay
|
||
- lvgl.widget.show: color_detail_overlay
|
||
|
||
- id: color_brightness_commit
|
||
parameters:
|
||
value: int
|
||
then:
|
||
- lambda: 'id(active_brightness_pct) = value;'
|
||
- if:
|
||
condition:
|
||
lambda: 'return runtime_tiles::fresh() && id(active_entity).rfind("light.", 0) == 0;'
|
||
then:
|
||
- homeassistant.action:
|
||
action: light.turn_on
|
||
data:
|
||
entity_id: !lambda "return id(active_entity);"
|
||
brightness_pct: !lambda "return value;"
|
||
|
||
- id: set_light_hue
|
||
parameters:
|
||
value: int
|
||
then:
|
||
- if:
|
||
condition:
|
||
lambda: 'return runtime_tiles::fresh() && id(active_supports_color) && id(active_entity).rfind("light.", 0) == 0;'
|
||
then:
|
||
- homeassistant.action:
|
||
action: light.turn_on
|
||
data:
|
||
entity_id: !lambda "return id(active_entity);"
|
||
data_template:
|
||
hs_color: "{{ [hue | int, 100] }}"
|
||
variables:
|
||
hue: !lambda "return light_controls::clamp(value, 0, 360);"
|
||
|
||
- id: set_light_color_temperature
|
||
parameters:
|
||
kelvin: int
|
||
then:
|
||
- if:
|
||
condition:
|
||
lambda: 'return runtime_tiles::fresh() && id(active_supports_temperature) && id(active_entity).rfind("light.", 0) == 0 && light_controls::active->temperature_ready();'
|
||
then:
|
||
- homeassistant.action:
|
||
action: light.turn_on
|
||
data:
|
||
entity_id: !lambda "return id(active_entity);"
|
||
color_temp_kelvin: !lambda "return light_controls::clamp(kelvin, light_controls::active->minimum, light_controls::active->maximum);"
|
||
|
||
- id: slider_preview
|
||
parameters:
|
||
value: int
|
||
then:
|
||
- lambda: |-
|
||
char buf[20];
|
||
snprintf(buf, sizeof(buf), "%d%s", value, id(active_value_suffix).c_str());
|
||
lv_label_set_text(id(overlay_value), buf);
|
||
int pct = value;
|
||
if (pct < 0) pct = 0;
|
||
if (pct > 100) pct = 100;
|
||
std::string value_text;
|
||
|
||
if (id(active_control_kind) == "cover_position") {
|
||
std::string closed_label = id(active_label_off).c_str();
|
||
std::string open_label = id(active_label_on).c_str();
|
||
|
||
if (pct <= 0) {
|
||
value_text = closed_label;
|
||
} else if (pct >= 100) {
|
||
value_text = open_label;
|
||
} else {
|
||
char buf[40];
|
||
snprintf(buf, sizeof(buf), "%s - %d%%", open_label.c_str(), pct);
|
||
value_text = buf;
|
||
}
|
||
} else {
|
||
if (pct <= 0) {
|
||
value_text = id(active_label_off).c_str();
|
||
} else {
|
||
char buf[20];
|
||
snprintf(buf, sizeof(buf), "%d%s", pct, id(active_value_suffix).c_str());
|
||
value_text = buf;
|
||
}
|
||
}
|
||
|
||
lv_label_set_text(id(overlay_value), value_text.c_str());
|
||
|
||
|
||
- id: slider_commit
|
||
parameters:
|
||
value: int
|
||
then:
|
||
- if:
|
||
condition:
|
||
lambda: |-
|
||
return id(active_control_kind) == "light_brightness";
|
||
then:
|
||
- homeassistant.action:
|
||
action: light.turn_on
|
||
data:
|
||
entity_id: !lambda "return id(active_entity);"
|
||
brightness_pct: !lambda "return value;"
|
||
else:
|
||
- if:
|
||
condition:
|
||
lambda: |-
|
||
return id(active_control_kind) == "fan_percentage";
|
||
then:
|
||
- if:
|
||
condition:
|
||
lambda: "return value <= 0;"
|
||
then:
|
||
- homeassistant.action:
|
||
action: fan.turn_off
|
||
data:
|
||
entity_id: !lambda "return id(active_entity);"
|
||
else:
|
||
- homeassistant.action:
|
||
action: fan.turn_on
|
||
data:
|
||
entity_id: !lambda "return id(active_entity);"
|
||
- delay: 150ms
|
||
- homeassistant.action:
|
||
action: fan.set_percentage
|
||
data:
|
||
entity_id: !lambda "return id(active_entity);"
|
||
percentage: !lambda "return value;"
|
||
else:
|
||
- if:
|
||
condition:
|
||
lambda: |-
|
||
return id(active_control_kind) == "cover_position";
|
||
then:
|
||
- homeassistant.action:
|
||
action: cover.set_cover_position
|
||
data:
|
||
entity_id: !lambda "return id(active_entity);"
|
||
position: !lambda "return value;"
|
||
|
||
# ----------------------------------------------------------
|
||
# do_tile_action
|
||
#
|
||
# Centralized handler for executing Home Assistant actions
|
||
# triggered by tile taps.
|
||
#
|
||
# Parameters
|
||
# - tile_type: light | fan | switch | scene | script | cover
|
||
# - tap_action: auto | toggle | activate | fan_toggle_preset
|
||
# - service: optional Home Assistant service override
|
||
# - entity_id: target entity
|
||
# - param_key / param_val: optional parameters for custom services
|
||
# - was_on: previous entity state (pass false for longpress actions)
|
||
#
|
||
# Behavior
|
||
#
|
||
# toggle
|
||
# -> toggles the entity
|
||
# light.toggle / fan.toggle / switch.toggle / cover.open_cover|close_cover (based on was_on)
|
||
#
|
||
# activate
|
||
# -> activates entity
|
||
# scene.turn_on / script.turn_on / light.turn_on / fan.turn_on / cover.open_cover
|
||
#
|
||
# auto
|
||
# -> resolves automatically based on tile_type:
|
||
# light/switch/fan/cover -> toggle
|
||
# scene/script -> activate
|
||
#
|
||
# fan_toggle_preset
|
||
# -> if fan was ON: fan.turn_off
|
||
# -> if fan was OFF:
|
||
# fan.turn_on
|
||
# wait 150ms
|
||
# fan.set_preset_mode(param_val)
|
||
#
|
||
# Custom service
|
||
# If "service" is set, it will be called directly.
|
||
# param_key / param_val may be used for parameters
|
||
# like preset_mode, brightness_pct, percentage, etc.
|
||
# ----------------------------------------------------------
|
||
|
||
- id: open_value_overlay
|
||
parameters:
|
||
title: string
|
||
entity: string
|
||
tile_type: string
|
||
longpress_mode: string
|
||
was_on: bool
|
||
off_value: int
|
||
brightness_value: float
|
||
percentage_value: float
|
||
position_value: float
|
||
icon_text: string
|
||
label_off: string
|
||
label_on: string
|
||
supported_color_modes: string
|
||
climate_current_temperature: float
|
||
climate_target_temperature: float
|
||
climate_humidity: float
|
||
climate_mode: string
|
||
climate_supported_modes: string
|
||
climate_min_temp: string
|
||
climate_max_temp: string
|
||
climate_step: string
|
||
then:
|
||
- lambda: |-
|
||
std::string mode = longpress_mode;
|
||
auto contains_mode = [](const std::string& modes, const std::string& needle) {
|
||
return modes.find(needle) != std::string::npos;
|
||
};
|
||
if (mode == "auto") {
|
||
if (tile_type == "light") mode = "brightness";
|
||
else if (tile_type == "fan") mode = "percentage";
|
||
else if (tile_type == "cover") mode = "cover_position";
|
||
else if (tile_type == "climate") mode = "climate_temperature";
|
||
else mode = "none";
|
||
}
|
||
|
||
if (mode == "none") return;
|
||
|
||
id(active_entity) = entity;
|
||
id(active_value_suffix) = " %";
|
||
id(active_label_off) = label_off;
|
||
id(active_label_on) = label_on;
|
||
id(active_title) = title;
|
||
id(active_icon_text) = icon_text;
|
||
id(active_supports_color) = false;
|
||
id(active_supports_temperature) = false;
|
||
lv_obj_add_flag(id(overlay_color_button), LV_OBJ_FLAG_HIDDEN);
|
||
|
||
if (mode == "climate_temperature") {
|
||
auto to_x10 = [](float value, int fallback) -> int {
|
||
if (isnan(value)) return fallback;
|
||
return (int) lround(value * 10.0f);
|
||
};
|
||
auto parse_x10 = [](const std::string& value, int fallback) -> int {
|
||
if (value.empty()) return fallback;
|
||
return (int) lround(atof(value.c_str()) * 10.0f);
|
||
};
|
||
id(climate_edit_pending) = false;
|
||
id(active_control_kind) = "climate_temperature";
|
||
id(active_climate_min_x10) = parse_x10(climate_min_temp, 50);
|
||
id(active_climate_max_x10) = parse_x10(climate_max_temp, 300);
|
||
id(active_climate_step_x10) = parse_x10(climate_step, 5);
|
||
if (id(active_climate_step_x10) < 5) id(active_climate_step_x10) = 5;
|
||
if (id(active_climate_max_x10) <= id(active_climate_min_x10)) {
|
||
id(active_climate_min_x10) = 50;
|
||
id(active_climate_max_x10) = 300;
|
||
}
|
||
|
||
id(active_climate_current_x10) = to_x10(climate_current_temperature, 0);
|
||
id(active_climate_current) = climate_current_temperature;
|
||
id(active_climate_target_known) = !isnan(climate_target_temperature);
|
||
int fallback_target = isnan(climate_current_temperature) ? 200 : id(active_climate_current_x10);
|
||
id(active_climate_target_x10) = to_x10(climate_target_temperature, fallback_target);
|
||
if (id(active_climate_target_x10) < id(active_climate_min_x10)) id(active_climate_target_x10) = id(active_climate_min_x10);
|
||
if (id(active_climate_target_x10) > id(active_climate_max_x10)) id(active_climate_target_x10) = id(active_climate_max_x10);
|
||
id(active_climate_target_x10) = id(active_climate_min_x10) + (int) lround((id(active_climate_target_x10) - id(active_climate_min_x10)) / (float) id(active_climate_step_x10)) * id(active_climate_step_x10);
|
||
if (id(active_climate_target_x10) < id(active_climate_min_x10)) id(active_climate_target_x10) = id(active_climate_min_x10);
|
||
if (id(active_climate_target_x10) > id(active_climate_max_x10)) id(active_climate_target_x10) = id(active_climate_max_x10);
|
||
id(active_climate_humidity) = isnan(climate_humidity) ? -1 : (int) lround(climate_humidity);
|
||
id(active_climate_mode) = climate_mode.empty() ? std::string("Auto") : climate_mode;
|
||
id(active_climate_supported_modes) = climate_supported_modes;
|
||
|
||
lv_label_set_text(id(climate_detail_title), title.c_str());
|
||
id(climate_card_refresh).execute();
|
||
|
||
lv_obj_add_flag(id(brightness_overlay), LV_OBJ_FLAG_HIDDEN);
|
||
lv_obj_clear_flag(id(climate_detail_overlay), LV_OBJ_FLAG_HIDDEN);
|
||
return;
|
||
}
|
||
|
||
int pct = off_value;
|
||
|
||
if (mode == "brightness") {
|
||
id(active_control_kind) = "light_brightness";
|
||
if (was_on) {
|
||
if (isnan(brightness_value)) pct = 100;
|
||
else pct = (int) lround((brightness_value / 255.0f) * 100.0f);
|
||
}
|
||
} else if (mode == "percentage") {
|
||
id(active_control_kind) = "fan_percentage";
|
||
if (was_on) {
|
||
if (isnan(percentage_value)) pct = 0;
|
||
else pct = (int) lround(percentage_value);
|
||
}
|
||
} else if (mode == "cover_position") {
|
||
id(active_control_kind) = "cover_position";
|
||
if (isnan(position_value)) pct = was_on ? 100 : 0;
|
||
else pct = (int) lround(position_value);
|
||
} else {
|
||
return;
|
||
}
|
||
|
||
if (pct < 0) pct = 0;
|
||
if (pct > 100) pct = 100;
|
||
id(active_brightness_pct) = pct;
|
||
|
||
if (tile_type == "light") {
|
||
id(active_supports_color) =
|
||
contains_mode(supported_color_modes, "hs") ||
|
||
contains_mode(supported_color_modes, "rgb") ||
|
||
contains_mode(supported_color_modes, "rgbw") ||
|
||
contains_mode(supported_color_modes, "rgbww") ||
|
||
contains_mode(supported_color_modes, "xy");
|
||
id(active_supports_temperature) = contains_mode(supported_color_modes, "color_temp");
|
||
|
||
if (id(active_supports_color) || id(active_supports_temperature)) {
|
||
lv_obj_clear_flag(id(overlay_color_button), LV_OBJ_FLAG_HIDDEN);
|
||
}
|
||
}
|
||
|
||
// Track and fill are the pale blue and blue paints of overlay_slider.
|
||
lv_obj_set_style_bg_opa(id(overlay_slider), LV_OPA_COVER, LV_PART_MAIN);
|
||
lv_obj_set_style_radius(id(overlay_slider), 28, LV_PART_MAIN);
|
||
lv_obj_set_style_radius(id(overlay_slider), 0, LV_PART_INDICATOR);
|
||
|
||
lv_label_set_text(id(overlay_title), title.c_str());
|
||
lv_label_set_text(id(overlay_icon), icon_text.c_str());
|
||
lv_slider_set_value(id(overlay_slider), pct, LV_ANIM_OFF);
|
||
|
||
std::string value_text;
|
||
|
||
if (tile_type == "cover") {
|
||
if (pct <= 0) {
|
||
value_text = label_off;
|
||
} else if (pct >= 100) {
|
||
value_text = label_on;
|
||
} else {
|
||
char buf[40];
|
||
snprintf(buf, sizeof(buf), "%s - %d%%", label_on.c_str(), pct);
|
||
value_text = buf;
|
||
}
|
||
} else {
|
||
if (pct <= 0) {
|
||
value_text = label_off;
|
||
} else {
|
||
char buf[20];
|
||
snprintf(buf, sizeof(buf), "%d%s", pct, id(active_value_suffix).c_str());
|
||
value_text = buf;
|
||
}
|
||
}
|
||
|
||
lv_label_set_text(id(overlay_value), value_text.c_str());
|
||
|
||
lv_obj_set_style_bg_opa(id(overlay_slider), LV_OPA_COVER, LV_PART_INDICATOR);
|
||
|
||
- if:
|
||
condition:
|
||
lambda: 'return id(active_control_kind) != "climate_temperature";'
|
||
then:
|
||
- if:
|
||
condition:
|
||
lambda: 'return id(active_supports_color) || id(active_supports_temperature);'
|
||
then:
|
||
- script.execute: open_color_detail
|
||
else:
|
||
- lvgl.widget.show: brightness_overlay
|
||
|
||
# UI refresh (values + time + styles) + render
|
||
- id: ui_refresh
|
||
mode: single
|
||
then:
|
||
- delay: 100ms
|
||
- lambda: |-
|
||
if (runtime_tiles::enabled) {
|
||
// The name and the top bar (clock included) draw together.
|
||
runtime_tiles::render(id(lbl_room));
|
||
return;
|
||
}
|
||
|
||
# HA blueprint transport; never publishes the JSON command as an HA state.
|
||
text:
|
||
- platform: smart_display
|
||
id: dashboard_inbox
|
||
name: "Tile settings"
|
||
internal: ${DASHBOARD_INBOX_INTERNAL}
|
||
|
||
button:
|
||
# Wake and Sleep for Home Assistant automations (0.2.45+). Wake is light, not a tap (0.2.56+): it
|
||
# lights a dimmed screen and counts the standby time from now, while Back to page 1 keeps counting
|
||
# from the last touch, so Wake on every motion no longer holds a card or page 2 up. Sleep is the
|
||
# standby time running out, right away and also with Auto standby off; it holds until a tap, Wake
|
||
# or an alert, and closes an alert that is up. Neither saves anything, so an automation may press
|
||
# them on every motion. No entity_category: they are controls, not settings.
|
||
- platform: template
|
||
id: wake_button
|
||
name: "Wake"
|
||
icon: "mdi:gesture-tap"
|
||
on_press:
|
||
- lambda: 'ESP_LOGI("standby", "Wake from Home Assistant, screen %s", id(display_dimmed) ? "in standby" : "on");'
|
||
- if:
|
||
condition:
|
||
lambda: 'return id(display_dimmed);'
|
||
then:
|
||
- script.execute: wake_display
|
||
# A page whose time ran out during standby: light up on page 1, not on the old page first.
|
||
- script.execute: back_to_page_1_when_due
|
||
else:
|
||
- lambda: 'id(last_touch_ms) = millis();'
|
||
- platform: template
|
||
id: sleep_button
|
||
name: "Sleep"
|
||
icon: "mdi:power-sleep"
|
||
on_press:
|
||
- if:
|
||
condition:
|
||
lambda: 'return id(calibration_active);'
|
||
then:
|
||
- lambda: 'ESP_LOGI("standby", "Sleep from Home Assistant ignored: touch calibration is running");'
|
||
else:
|
||
- lambda: |-
|
||
ESP_LOGI("standby", "Sleep from Home Assistant");
|
||
id(sleep_requested) = true;
|
||
- script.execute:
|
||
id: alert_dismiss
|
||
reason: "remote"
|
||
- script.execute: dim_display
|
||
|
||
number:
|
||
# Firmware 0.2.49+: every number goes through settings_screen::set(), like the settings page, so a value the
|
||
# screen already has writes and reports nothing (an automation may set it on every light change).
|
||
- platform: template
|
||
id: setting_brightness
|
||
name: "Normal brightness"
|
||
entity_category: config
|
||
min_value: 5
|
||
max_value: 100
|
||
step: 1
|
||
unit_of_measurement: "%"
|
||
update_interval: never
|
||
lambda: return screen_settings::current.brightness;
|
||
set_action:
|
||
- lambda: 'settings_screen::set("brightness", (int32_t) lround(x));'
|
||
- platform: template
|
||
id: setting_standby_brightness
|
||
name: "Standby brightness"
|
||
entity_category: config
|
||
min_value: 0
|
||
max_value: 100
|
||
step: 1
|
||
unit_of_measurement: "%"
|
||
update_interval: never
|
||
lambda: return screen_settings::current.standby_brightness;
|
||
set_action:
|
||
- lambda: 'settings_screen::set("standby_brightness", (int32_t) lround(x));'
|
||
- platform: template
|
||
id: setting_night_brightness
|
||
name: "Night brightness"
|
||
entity_category: config
|
||
min_value: 0
|
||
max_value: 100
|
||
step: 1
|
||
unit_of_measurement: "%"
|
||
update_interval: never
|
||
lambda: return screen_settings::current.night_brightness;
|
||
set_action:
|
||
- lambda: 'settings_screen::set("night_brightness", (int32_t) lround(x));'
|
||
- platform: template
|
||
id: setting_standby_seconds
|
||
name: "Standby after"
|
||
entity_category: config
|
||
min_value: 60
|
||
max_value: 86400
|
||
step: 60
|
||
unit_of_measurement: "s"
|
||
update_interval: never
|
||
lambda: return screen_settings::current.standby_seconds;
|
||
set_action:
|
||
- lambda: 'settings_screen::set("standby_seconds", (int32_t) lround(x));'
|
||
- platform: template
|
||
id: setting_auto_home_seconds
|
||
name: "Back to page 1 after"
|
||
icon: "mdi:timer-outline"
|
||
entity_category: config
|
||
min_value: 30
|
||
max_value: 3600
|
||
step: 30
|
||
mode: box
|
||
unit_of_measurement: "s"
|
||
update_interval: never
|
||
lambda: return settings_screen::auto_home_seconds;
|
||
set_action:
|
||
- lambda: 'settings_screen::set("auto_home_seconds", (int32_t) lround(x));'
|