9857cc3bda
A board file for the Elecrow CrowPanel 7.0" (DIS08070H, ESP32-S3-WROOM-1-N4R8): 800x480 through mipi_rgb, GT911 on the ESP32-S3's USB pins (elecrow_usb_phy.h frees them, behind a flag only this board sets), the Guition's vertical sizes across a wider page, no rotation. Its 4 MB of flash take the CYD's budget: a partition table of the component's (smart_display: partitions: 4mb_wide_slots, 1,984 KB slots), the icon fonts at 2 bits a pixel and the TLS client only bring the firmware to 1,865,440 bytes, 91.8 % of a slot, with camera images kept. docs/ELECROW.md has the numbers and the trade. The add-on reads a large board's name from the type sensor's value, the editor offers the board under New screen and draws the wide preview, tools/check.sh builds it with the budget, render_topbar and capture_ui take the panel's size. Firmware 0.2.79; the CYD image is unchanged (1,633,760 bytes). This fork builds from gittea.trippedonmy.name/Xandrossian/homeassistant_espscreen: the packages, the fonts, the component and the add-on repository point there instead of GitHub.
4343 lines
178 KiB
YAML
4343 lines
178 KiB
YAML
############################################################
|
||
# ESP Screens - the shared core: what every board's screen is made of.
|
||
#
|
||
# The LVGL tree the runtime fills (tiles, cards, overlays, the touch test page),
|
||
# the scripts, the API actions, the entities, the fonts and the globals. It carries
|
||
# no number that depends on the board: every size, font size and board-only line of
|
||
# C++ is a ${NAME} that the board file under packages/boards/ defines, next to the
|
||
# board's hardware. An entry file includes both: packages/<board>.yaml for a screen
|
||
# that builds over GitHub, <profile>.yaml in the repository root for a checkout.
|
||
# docs/PROFILES.md says how the three fit together and how a board is added.
|
||
#
|
||
# A screen gets its tiles from the ESP Screen Manager add-on while it runs, so no
|
||
# Home Assistant entity lives in here. The Material Design Icons font comes from
|
||
# fonts/materialdesignicons-webfont.ttf (Apache 2.0, github.com/Templarian/MaterialDesign-Webfont).
|
||
############################################################
|
||
substitutions:
|
||
SCREEN_FIRMWARE_VERSION: "0.2.79"
|
||
DASHBOARD_INBOX_INTERNAL: "false"
|
||
CALIBRATION_ON_BOOT: "false" # a bench build: start on the touch test page
|
||
ROOM_NAME: "" # top left until ESP Screens sends the screen name
|
||
# The language of every text on the screen (app 0.2.90): a file in screen_manager/translations, English for any
|
||
# text it lacks. ESP Screens writes it into the screen's own YAML, from Home Assistant's language or its Settings.
|
||
LANGUAGE: "en"
|
||
|
||
## 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
|
||
|
||
# ============================================================
|
||
# No need to edit below this line for a screen of your own: the sizes are the
|
||
# board file's, the credentials the entry file's (secrets.yaml, see secrets.yaml.example).
|
||
# ============================================================
|
||
esphome:
|
||
# The oldest ESPHome these packages build with, not the one the add-on ships (2026.9.0, screen_manager/Dockerfile): an
|
||
# add-on or ESPHome Device Builder that isn't updated yet keeps building every screen (firmware 0.2.75+). Raise it in a
|
||
# release of its own, with the camera images' `image:` form (before ESPHome 2027.1) and OTA with the api key for new
|
||
# screens; docs/RELEASING.md, Compatibility 0.2.89.
|
||
min_version: 2026.6.2
|
||
project:
|
||
name: maxgramser.esp_screen
|
||
version: "${SCREEN_FIRMWARE_VERSION}"
|
||
# Still under platformio_options, although ESPHome 2026.9 calls that deprecated and offers `esphome: build_flags`:
|
||
# its LVGL only reads these flags when it writes lv_conf.h, and writes `LV_USE_SWITCH 0` over the other form.
|
||
platformio_options:
|
||
build_flags:
|
||
- -DLV_USE_SWITCH=1
|
||
includes:
|
||
- <esphome/components/smart_display/cyd_ui.h>
|
||
- <esphome/components/smart_display/light_controls.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: |-
|
||
${BOOT_TOUCH}
|
||
- 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::big_icon_font = id(materialdesign_icons_big)->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;
|
||
${BOOT_ENABLED_HOOK}
|
||
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);
|
||
${BOARD_PAINT_FILL}
|
||
};
|
||
theme::paints();
|
||
theme::set_dark(settings_screen::dark_mode != 0);
|
||
runtime_tiles::settings_changed = []() { id(apply_screen_settings).execute(); };
|
||
runtime_tiles::dismiss = []() {
|
||
effects_page::close();
|
||
runtime_tiles::hide_detail();
|
||
${CLOSE_CARDS_HOOK}
|
||
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(); };
|
||
${BOOT_PAGE_GESTURE}
|
||
${BOOT_CAMERA_HOOKS}
|
||
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),
|
||
screen_text::tr(screen_text::txt::ha_off), screen_text::tr(screen_text::txt::ha_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) {
|
||
effects_page::updated(t);
|
||
if (id(active_entity) != t.entity || id(touch_down)) return;
|
||
if (t.domain() == "climate" && !lv_obj_has_flag(id(climate_detail_overlay), LV_OBJ_FLAG_HIDDEN)) {
|
||
if (id(climate_edit_pending) && millis() - id(climate_edit_ms) < 3000) return;
|
||
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); };
|
||
// The effects page behind the colour card's sparkles key (firmware 0.2.70+): fonts, the tile it shows, and
|
||
// the ways out: actions to Home Assistant and the question for a picker's names.
|
||
effects_page::title_font = id(headline)->get_lv_font();
|
||
effects_page::row_font = id(${EFFECTS_ROW_FONT})->get_lv_font();
|
||
effects_page::roller_font = id(sublabel_big)->get_lv_font();
|
||
effects_page::icon_font = id(materialdesign_icons_mini)->get_lv_font();
|
||
effects_page::tile_of = [](const std::string &entity) -> const runtime_tiles::Tile * {
|
||
for (size_t i = 0; i < runtime_tiles::model.count; ++i) if (runtime_tiles::model.tiles[i].entity == entity) return &runtime_tiles::model.tiles[i];
|
||
return nullptr;
|
||
};
|
||
effects_page::send = [](const std::string &service, const std::string &entity, const std::string &key, const std::string &value) {
|
||
runtime_tiles::action(service, entity, key, value);
|
||
};
|
||
effects_page::ask = [](const std::string &entity, unsigned page) { runtime_tiles::options_request(entity, page); };
|
||
effects_page::drift = []() { return cyd::touch_guard.distance(); };
|
||
effects_page::tap_limit = ${TOUCH_MOVE_LIMIT_PX};
|
||
effects_page::now = []() { return (uint32_t) millis(); };
|
||
runtime_tiles::options_received = effects_page::received;
|
||
${BOOT_CARDS_TAIL}
|
||
# 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(screen_text::tr(screen_text::txt::status_no_network)) : id(wifi_ip).state; };
|
||
settings_screen::firmware_text = []() { return std::string("${SCREEN_FIRMWARE_VERSION}"); };
|
||
settings_screen::link_text = []() {
|
||
return std::string(screen_text::tr(esphome::api_is_connected() ? screen_text::txt::settings_connected : screen_text::txt::settings_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)${MAY_OPEN_EXTRA};
|
||
};
|
||
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();
|
||
effects_page::restyle();
|
||
settings_screen::restyle();
|
||
if (id(alert_active)) lv_obj_set_style_bg_color(id(alert_card), lv_color_hex(theme::surface(id(alert_card_color))), LV_PART_MAIN);
|
||
if (!lv_obj_has_flag(id(climate_detail_overlay), LV_OBJ_FLAG_HIDDEN)) id(climate_card_refresh).execute();
|
||
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", "${TOUCH_TEST_READY_LOG}");
|
||
|
||
|
||
logger:
|
||
# INFO (firmware 0.2.75+): the lines below INFO, ESPHome's configuration dump and its per-state debug lines, are left
|
||
# out of the firmware, 9 KB on the CYD. For a diagnosis, give one screen `logger: level: DEBUG` in its Override YAML.
|
||
level: INFO
|
||
logs:
|
||
sensor: WARN
|
||
text_sensor: WARN
|
||
xpt2046: WARN
|
||
homeassistant.sensor: WARN
|
||
homeassistant.text_sensor: WARN
|
||
|
||
# A screen hangs on the mains, so its Wi-Fi never dozes (firmware 0.2.74+). With ESPHome's default for the ESP32,
|
||
# `light`, every message from Home Assistant waits at the router for the next beacon: 76-117 ms on the bench screens
|
||
# against about 20 ms, and an image download held a loop up to 0.1 s now and then (docs/TEST_RESULTS_0287.md). The
|
||
# network and its password stay in the screen's own YAML, and a `power_save_mode` there wins over this one.
|
||
wifi:
|
||
power_save_mode: none
|
||
|
||
|
||
# Credentials are loaded from secrets.yaml — copy secrets.yaml.example to secrets.yaml and fill in your values.
|
||
api:
|
||
actions:
|
||
- 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));
|
||
${UI_STATE_HOOK}
|
||
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;
|
||
|
||
interval:
|
||
- interval: 250ms
|
||
then:
|
||
- lambda: |-
|
||
runtime_tiles::tick();
|
||
${TICK_HOOK}
|
||
- 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: this resistive XPT2046 panel sometimes gives
|
||
# 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:
|
||
- file: "${FONT_DIR}/Roboto-500.ttf"
|
||
id: time_label
|
||
size: ${FONT_TIME_SIZE}
|
||
bpp: 4
|
||
glyphs: ['0','1','2','3','4','5','6','7','8','9',':',' ', 'A', 'M', 'P']
|
||
|
||
- file: "${FONT_DIR}/Roboto-500.ttf"
|
||
id: headline
|
||
size: ${FONT_HEADLINE_SIZE}
|
||
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: "${FONT_DIR}/Roboto-500.ttf"
|
||
id: watch_value
|
||
size: ${FONT_WATCH_VALUE_SIZE}
|
||
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: "${FONT_DIR}/Roboto-500.ttf"
|
||
id: clock_digits
|
||
size: ${FONT_CLOCK_DIGITS_SIZE}
|
||
bpp: 4
|
||
glyphs: [':','-','0','1','2','3','4','5','6','7','8','9']
|
||
|
||
# The climate card's setpoint, large enough to read across the room.
|
||
- file: "${FONT_DIR}/Roboto-400.ttf"
|
||
id: setpoint_digits
|
||
size: ${FONT_SETPOINT_SIZE}
|
||
bpp: 4
|
||
glyphs: ['-','.',',','°','0','1','2','3','4','5','6','7','8','9']
|
||
|
||
- file: "${FONT_DIR}/Roboto-700.ttf"
|
||
id: label
|
||
size: ${FONT_LABEL_SIZE}
|
||
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: "${FONT_DIR}/Roboto-400.ttf"
|
||
id: sublabel
|
||
size: ${FONT_SUBLABEL_SIZE}
|
||
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: "${FONT_DIR}/Roboto-400.ttf"
|
||
id: sublabel_big
|
||
size: ${FONT_SUBLABEL_BIG_SIZE}
|
||
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: "${FONT_DIR}/materialdesignicons-webfont.ttf"
|
||
id: materialdesign_icons
|
||
size: ${FONT_ICON_SIZE}
|
||
bpp: 4
|
||
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
|
||
- "\U000F0B53" # account-arrow-right
|
||
- "\U000F0D43" # air-filter
|
||
- "\U000F1099" # air-humidifier
|
||
- "\U000F1466" # air-humidifier-off
|
||
- "\U000F0028" # alert-circle
|
||
- "\U000F0937" # angle-acute
|
||
- "\U000F0042" # arrow-bottom-left
|
||
- "\U000F0043" # arrow-bottom-right
|
||
- "\U000F06C0" # arrow-down-box
|
||
- "\U000F0E73" # arrow-left-right
|
||
- "\U000F0054" # arrow-right
|
||
- "\U000F093C" # arrow-split-vertical
|
||
- "\U000F005B" # arrow-top-left
|
||
- "\U000F005C" # arrow-top-right
|
||
- "\U000F06C3" # arrow-up-box
|
||
- "\U000F093D" # audio-video
|
||
- "\U000F11B6" # audio-video-off
|
||
- "\U000F0079" # battery
|
||
- "\U000F007A" # battery-10
|
||
- "\U000F007B" # battery-20
|
||
- "\U000F007C" # battery-30
|
||
- "\U000F007D" # battery-40
|
||
- "\U000F007E" # battery-50
|
||
- "\U000F007F" # battery-60
|
||
- "\U000F0080" # battery-70
|
||
- "\U000F0081" # battery-80
|
||
- "\U000F0082" # battery-90
|
||
- "\U000F0083" # battery-alert
|
||
- "\U000F0084" # battery-charging
|
||
- "\U000F008E" # battery-outline
|
||
- "\U000F0091" # battery-unknown
|
||
- "\U000F1A2B" # blinds-horizontal
|
||
- "\U000F1A2C" # blinds-horizontal-closed
|
||
- "\U000F00AF" # bluetooth
|
||
- "\U000F00B1" # bluetooth-connect
|
||
- "\U000F00DE" # brightness-5
|
||
- "\U000F00E0" # brightness-7
|
||
- "\U000F1B50" # button-pointer
|
||
- "\U000F010C" # car-battery
|
||
- "\U000F1019" # car-coolant-level
|
||
- "\U000F0114" # cash
|
||
- "\U000F0119" # cast-connected
|
||
- "\U000F078A" # cast-off
|
||
- "\U000F05E0" # check-circle
|
||
- "\U000F05E1" # check-circle-outline
|
||
- "\U000F0C54" # check-network-outline
|
||
- "\U000F0133" # checkbox-marked-circle
|
||
- "\U000F0AA5" # circle-slice-8
|
||
- "\U000F0954" # clock
|
||
- "\U000F0155" # clock-start
|
||
- "\U000F015A" # close-circle-outline
|
||
- "\U000F0C5F" # close-network-outline
|
||
- "\U000F1382" # compass-rose
|
||
- "\U000F01A1" # crop-portrait
|
||
- "\U000F1136" # crosshairs-question
|
||
- "\U000F1480" # current-ac
|
||
- "\U000F01BC" # database
|
||
- "\U000F07C5" # ear-hearing
|
||
- "\U000F0208" # eye
|
||
- "\U000F0D04" # eye-check
|
||
- "\U000F081D" # fan-off
|
||
- "\U000F0279" # format-list-bulleted
|
||
- "\U000F06DA" # garage-open
|
||
- "\U000F0299" # gate
|
||
- "\U000F116A" # gate-open
|
||
- "\U000F06A1" # home-outline
|
||
- "\U000F0318" # lan-connect
|
||
- "\U000F0319" # lan-disconnect
|
||
- "\U000F140B" # lightning-bolt
|
||
- "\U000F08EE" # lock-alert
|
||
- "\U000F097F" # lock-clock
|
||
- "\U000F033F" # lock-open
|
||
- "\U000F0BAC" # molecule
|
||
- "\U000F12FE" # molecule-co
|
||
- "\U000F1435" # motion-sensor-off
|
||
- "\U000F038A" # music-note-off
|
||
- "\U000F03C3" # octagon
|
||
- "\U000F03D3" # package
|
||
- "\U000F03D5" # package-up
|
||
- "\U000F17C5" # ph
|
||
- "\U000F184D" # pipe-valve
|
||
- "\U000F06A6" # power-plug-off
|
||
- "\U000F0996" # progress-clock
|
||
- "\U000F1A23" # projector-off
|
||
- "\U000F043C" # radioactive
|
||
- "\U000F0445" # ray-vertex
|
||
- "\U000F1A6B" # roller-shade
|
||
- "\U000F1727" # script-text-play
|
||
- "\U000F0483" # security
|
||
- "\U000F0498" # shield
|
||
- "\U000F06BB" # shield-airplane
|
||
- "\U000F099D" # shield-lock
|
||
- "\U000F1828" # shield-moon
|
||
- "\U000F099E" # shield-off
|
||
- "\U000F0499" # shield-outline
|
||
- "\U000F095B" # sine-wave
|
||
- "\U000F192E" # smoke-detector-alert
|
||
- "\U000F180B" # smoke-detector-variant
|
||
- "\U000F1930" # smoke-detector-variant-alert
|
||
- "\U000F04C4" # speaker-off
|
||
- "\U000F1B73" # speaker-pause
|
||
- "\U000F1B72" # speaker-play
|
||
- "\U000F04C5" # speedometer
|
||
- "\U000F0674" # creation
|
||
- "\U000F0E0C" # palette-outline
|
||
- "\U000F0411" # playlist-play
|
||
- "\U000F08FF" # play-speed
|
||
- "\U000F00DF" # brightness-6
|
||
- "\U000F1429" # spoon-sugar
|
||
- "\U000F0E67" # sprout-outline
|
||
- "\U000F0764" # square
|
||
- "\U000F0763" # square-outline
|
||
- "\U000F1A75" # storage-tank
|
||
- "\U000F17FE" # sun-wireless
|
||
- "\U000F083B" # television-off
|
||
- "\U000F0F89" # television-pause
|
||
- "\U000F0ECF" # television-play
|
||
- "\U000F0FE6" # texture-box
|
||
- "\U000F0510" # thermometer-lines
|
||
- "\U000F1A25" # toggle-switch-variant
|
||
- "\U000F1A26" # toggle-switch-variant-off
|
||
- "\U000F0D3E" # transmission-tower
|
||
- "\U000F0566" # vibrate
|
||
- "\U000F11B4" # water-boiler-off
|
||
- "\U000F058D" # water-off
|
||
- "\U000F1855" # water-opacity
|
||
- "\U000F05A1" # weight
|
||
- "\U000F05A9" # wifi
|
||
- "\U000F05AE" # window-closed
|
||
- "\U000F05B1" # window-open
|
||
|
||
- file: "${FONT_DIR}/materialdesignicons-webfont.ttf"
|
||
id: materialdesign_icons_mini
|
||
size: ${FONT_ICON_MINI_SIZE}
|
||
bpp: 4
|
||
glyphs: *tile_icons
|
||
|
||
# The large icon of a card that takes the whole page (firmware 0.2.62+): the domain icons only,
|
||
# so the flash stays free for other things; a chosen icon outside this set keeps its usual size.
|
||
- file: "${FONT_DIR}/materialdesignicons-webfont.ttf"
|
||
id: materialdesign_icons_big
|
||
size: ${FONT_ICON_BIG_SIZE}
|
||
bpp: 4
|
||
glyphs: &tile_icons_big # GENERATED by tools/generate_icons.py from screen_manager/app/tile_icons.py
|
||
- "\U000F0335" # lightbulb
|
||
- "\U000F001B" # air-conditioner
|
||
- "\U000F070D" # robot-vacuum
|
||
- "\U000F0210" # fan
|
||
- "\U000F111C" # window-shutter
|
||
- "\U000F04B9" # sofa
|
||
- "\U000F029A" # gauge
|
||
- "\U000F051B" # timer-outline
|
||
- "\U000F0004" # account
|
||
- "\U000F07AE" # cctv
|
||
- "\U000F0150" # clock-outline
|
||
- "\U000F0493" # cog
|
||
- "\U000F0054" # arrow-right
|
||
- "\U000F0E4F" # lightbulb-off
|
||
- "\U000F0425" # power
|
||
- "\U000F0599" # weather-sunny
|
||
- "\U000F0594" # weather-night
|
||
- "\U000F0590" # weather-cloudy
|
||
- "\U000F0595" # weather-partly-cloudy
|
||
- "\U000F0597" # weather-rainy
|
||
- "\U000F0596" # weather-pouring
|
||
- "\U000F0598" # weather-snowy
|
||
- "\U000F067F" # weather-snowy-rainy
|
||
- "\U000F0591" # weather-fog
|
||
- "\U000F0592" # weather-hail
|
||
- "\U000F0593" # weather-lightning
|
||
- "\U000F067E" # weather-lightning-rainy
|
||
- "\U000F059D" # weather-windy
|
||
- "\U000F05D6" # alert-circle-outline
|
||
- "\U000F059C" # weather-sunset-up
|
||
- "\U000F059B" # weather-sunset-down
|
||
|
||
- file: "${FONT_DIR}/materialdesignicons-webfont.ttf"
|
||
id: watch_icon
|
||
size: ${FONT_WATCH_ICON_SIZE}
|
||
bpp: 4
|
||
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: "Screen firmware"
|
||
entity_category: diagnostic
|
||
lambda: 'return {"${SCREEN_FIRMWARE_VERSION}"};'
|
||
update_interval: 60s
|
||
# The language this firmware was built in (0.2.90+): ESP Screens offers an update when it differs from the chosen one.
|
||
- platform: template
|
||
name: "Screen language"
|
||
entity_category: diagnostic
|
||
lambda: 'return {screen_text::LANGUAGE};'
|
||
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_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);'
|
||
# Off, the Previous and Next bar under the tiles goes and the tiles take its room (firmware 0.2.69+).
|
||
- platform: template
|
||
id: setting_page_buttons
|
||
name: "Page buttons"
|
||
icon: "mdi:page-next-outline"
|
||
entity_category: config
|
||
restore_mode: DISABLED
|
||
lambda: return settings_screen::page_buttons != 0;
|
||
turn_on_action:
|
||
- lambda: 'settings_screen::set("page_buttons", 1);'
|
||
turn_off_action:
|
||
- lambda: 'settings_screen::set("page_buttons", 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);'
|
||
|
||
lvgl:
|
||
# The theme's font for text without a font of its own. Every label names its font, so this only keeps
|
||
# ESPHome from building LVGL's Montserrat 14 for nothing (firmware 0.2.65+).
|
||
default_font: ${LVGL_DEFAULT_FONT}
|
||
on_draw_start:
|
||
- lambda: 'id(rendered_frames)++;'
|
||
displays:
|
||
- my_display
|
||
touchscreens:
|
||
- touchscreen_id: ts_touch
|
||
|
||
buffer_size: ${LVGL_BUFFER_SIZE}
|
||
|
||
# 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: ${TILE_RADIUS}
|
||
border_width: 1
|
||
border_opa: COVER
|
||
pad_left: ${TILE_PAD}
|
||
pad_right: ${TILE_PAD}
|
||
pad_top: ${TILE_PAD}
|
||
pad_bottom: ${TILE_PAD}
|
||
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: !lambda 'return std::string(screen_text::tr(screen_text::txt::alert_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: !lambda 'return std::string(screen_text::tr(screen_text::txt::alert_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
|
||
# Never shown either: it makes ESPHome compile LVGL's roller, the drum a light's effects page picks from
|
||
# (firmware 0.2.70+).
|
||
- roller:
|
||
id: roller_seed
|
||
hidden: true
|
||
width: 2
|
||
height: 2
|
||
options: ["-"]
|
||
- label:
|
||
id: lbl_room
|
||
x: ${HEADER_INSET}
|
||
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: -${HEADER_INSET}
|
||
y: ${HEADER_Y}
|
||
text: ""
|
||
styles: style_time
|
||
|
||
# Six tiles on page 1, four on page 2. No dragging on resistive glass.
|
||
- 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: ${TILE_ICON_SIZE}
|
||
height: ${TILE_ICON_SIZE}
|
||
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
|
||
x: ${TILE_TITLE_X}
|
||
y: ${TILE_TITLE_Y}
|
||
text: "Lamp"
|
||
styles: style_title
|
||
clickable: false
|
||
- label:
|
||
id: t1_value
|
||
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: ${TILE_ICON_SIZE}
|
||
height: ${TILE_ICON_SIZE}
|
||
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
|
||
x: ${TILE_TITLE_X}
|
||
y: ${TILE_TITLE_Y}
|
||
text: "Plug"
|
||
styles: style_title
|
||
clickable: false
|
||
- label:
|
||
id: t2_value
|
||
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: ${TILE_ICON_SIZE}
|
||
height: ${TILE_ICON_SIZE}
|
||
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
|
||
x: ${TILE_TITLE_X}
|
||
y: ${TILE_TITLE_Y}
|
||
text: "Evening"
|
||
styles: style_title
|
||
clickable: false
|
||
- label:
|
||
id: t3_value
|
||
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: ${TILE_ICON_SIZE}
|
||
height: ${TILE_ICON_SIZE}
|
||
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
|
||
x: ${TILE_TITLE_X}
|
||
y: ${TILE_TITLE_Y}
|
||
text: "All off"
|
||
styles: style_title
|
||
clickable: false
|
||
- label:
|
||
id: t4_value
|
||
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: ${TILE_ICON_SIZE}
|
||
height: ${TILE_ICON_SIZE}
|
||
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
|
||
x: ${TILE_TITLE_X}
|
||
y: ${TILE_TITLE_Y}
|
||
text: "AC"
|
||
styles: style_title
|
||
clickable: false
|
||
- label:
|
||
id: t5_value
|
||
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: ${TILE_ICON_SIZE}
|
||
height: ${TILE_ICON_SIZE}
|
||
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
|
||
x: ${TILE_TITLE_X}
|
||
y: ${TILE_TITLE_Y}
|
||
text: "Vacuum"
|
||
styles: style_title
|
||
clickable: false
|
||
- label:
|
||
id: t6_value
|
||
x: ${TILE_VALUE_X}
|
||
y: ${TILE_VALUE_Y}
|
||
text: "—"
|
||
styles: style_value
|
||
clickable: false
|
||
|
||
# ---------- TILE 7 — Table lamp ----------
|
||
# 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: ${TILE_ICON_SIZE}
|
||
height: ${TILE_ICON_SIZE}
|
||
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
|
||
x: ${TILE_TITLE_X}
|
||
y: ${TILE_TITLE_Y}
|
||
text: "Tile 7"
|
||
styles: style_title
|
||
clickable: false
|
||
- label:
|
||
id: t7_value
|
||
x: ${TILE_VALUE_X}
|
||
y: ${TILE_VALUE_Y}
|
||
text: "—"
|
||
styles: style_value
|
||
clickable: false
|
||
|
||
# ---------- TILE 8 — Mailbox 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: ${TILE_ICON_SIZE}
|
||
height: ${TILE_ICON_SIZE}
|
||
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
|
||
x: ${TILE_TITLE_X}
|
||
y: ${TILE_TITLE_Y}
|
||
text: "Tile 8"
|
||
styles: style_title
|
||
clickable: false
|
||
- label:
|
||
id: t8_value
|
||
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: ${TILE_ICON_SIZE}
|
||
height: ${TILE_ICON_SIZE}
|
||
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
|
||
x: ${TILE_TITLE_X}
|
||
y: ${TILE_TITLE_Y}
|
||
text: "Tile 9"
|
||
styles: style_title
|
||
clickable: false
|
||
- label:
|
||
id: t9_value
|
||
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: ${TILE_ICON_SIZE}
|
||
height: ${TILE_ICON_SIZE}
|
||
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
|
||
x: ${TILE_TITLE_X}
|
||
y: ${TILE_TITLE_Y}
|
||
text: "Tile 10"
|
||
styles: style_title
|
||
clickable: false
|
||
- label:
|
||
id: t10_value
|
||
x: ${TILE_VALUE_X}
|
||
y: ${TILE_VALUE_Y}
|
||
text: "—"
|
||
styles: style_value
|
||
clickable: false
|
||
|
||
# ---------- VALUE SLIDER OVERLAY ----------
|
||
# ---------- PAGE NAVIGATION: the band under the tiles ----------
|
||
# Each half of the band is one key (firmware 0.2.69+): a chevron in its corner, lit up under a finger, and the
|
||
# page dots between them. The band reaches from the tile area's end (SCROLL_Y + SCROLL_H) to the bottom edge.
|
||
- obj:
|
||
id: page_prev
|
||
align: BOTTOM_LEFT
|
||
x: 0
|
||
y: 0
|
||
width: ${PAGE_KEY_W}
|
||
height: ${PAGE_BAR_H}
|
||
pad_all: 0
|
||
radius: 0
|
||
bg_opa: TRANSP
|
||
border_width: 0
|
||
scrollable: false
|
||
pressed:
|
||
styles: paint_page_pressed
|
||
bg_opa: COVER
|
||
# on_click: LVGL sends no short click after a press of 400 ms, so a firm or slow press did nothing
|
||
# (firmware 0.2.65+). The bar has no hold of its own; accept_repeat still drops bounce and a swipe.
|
||
on_click:
|
||
- if:
|
||
condition:
|
||
lambda: 'return cyd::touch_guard.accept_repeat(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: ${PAGE_CHEVRON_INSET}
|
||
text: "\U000F0141" # chevron-left
|
||
text_font: materialdesign_icons_mini
|
||
styles: paint_ink
|
||
clickable: false
|
||
- obj:
|
||
id: page_number
|
||
align: BOTTOM_MID
|
||
y: 0
|
||
width: ${PAGE_NUMBER_W}
|
||
height: ${PAGE_BAR_H}
|
||
pad_all: 0
|
||
bg_opa: TRANSP
|
||
border_width: 0
|
||
clickable: false
|
||
scrollable: false
|
||
- obj:
|
||
id: page_next
|
||
align: BOTTOM_RIGHT
|
||
x: 0
|
||
y: 0
|
||
width: ${PAGE_KEY_W}
|
||
height: ${PAGE_BAR_H}
|
||
pad_all: 0
|
||
radius: 0
|
||
bg_opa: TRANSP
|
||
border_width: 0
|
||
scrollable: false
|
||
pressed:
|
||
styles: paint_page_pressed
|
||
bg_opa: COVER
|
||
on_click:
|
||
- if:
|
||
condition:
|
||
lambda: 'return cyd::touch_guard.accept_repeat(millis(), 12);'
|
||
then:
|
||
- lambda: 'id(tile_page) += 1;'
|
||
- script.execute: show_tile_page
|
||
widgets:
|
||
- label:
|
||
align: RIGHT_MID
|
||
x: -${PAGE_CHEVRON_INSET}
|
||
text: "\U000F0142" # chevron-right
|
||
text_font: materialdesign_icons_mini
|
||
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: ${CARD_KEY_X}
|
||
y: ${CARD_KEY_Y}
|
||
width: ${CARD_KEY_SIZE}
|
||
height: ${CARD_KEY_SIZE}
|
||
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: ${OVERLAY_TITLE_Y}
|
||
text: "Light"
|
||
styles: style_room
|
||
|
||
- label:
|
||
id: overlay_value
|
||
align: TOP_MID
|
||
y: ${OVERLAY_VALUE_Y}
|
||
text: "100 %"
|
||
styles: style_value_big
|
||
|
||
- slider:
|
||
id: overlay_slider
|
||
align: CENTER
|
||
y: ${OVERLAY_SLIDER_Y}
|
||
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: ${OVERLAY_SLIDER_RADIUS}
|
||
# 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: ${OVERLAY_SLIDER_RADIUS}
|
||
|
||
knob:
|
||
bg_opa: 0%
|
||
border_width: 0
|
||
shadow_width: 0
|
||
pad_all: ${OVERLAY_KNOB_PAD}
|
||
|
||
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: ${COLOR_KEY_X}
|
||
y: ${COLOR_KEY_Y}
|
||
width: ${COLOR_KEY_SIZE}
|
||
height: ${COLOR_KEY_SIZE}
|
||
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: ${COLOR_ARC_SIZE}
|
||
height: ${COLOR_ARC_SIZE}
|
||
start_angle: 0
|
||
end_angle: 45
|
||
min_value: 0
|
||
max_value: 100
|
||
value: 0
|
||
adjustable: false
|
||
arc_width: ${COLOR_ARC_W}
|
||
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: ${COLOR_ARC_SIZE}
|
||
height: ${COLOR_ARC_SIZE}
|
||
start_angle: 45
|
||
end_angle: 90
|
||
min_value: 0
|
||
max_value: 100
|
||
value: 0
|
||
adjustable: false
|
||
arc_width: ${COLOR_ARC_W}
|
||
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: ${COLOR_ARC_SIZE}
|
||
height: ${COLOR_ARC_SIZE}
|
||
start_angle: 90
|
||
end_angle: 135
|
||
min_value: 0
|
||
max_value: 100
|
||
value: 0
|
||
adjustable: false
|
||
arc_width: ${COLOR_ARC_W}
|
||
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: ${COLOR_ARC_SIZE}
|
||
height: ${COLOR_ARC_SIZE}
|
||
start_angle: 135
|
||
end_angle: 180
|
||
min_value: 0
|
||
max_value: 100
|
||
value: 0
|
||
adjustable: false
|
||
arc_width: ${COLOR_ARC_W}
|
||
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: ${COLOR_ARC_SIZE}
|
||
height: ${COLOR_ARC_SIZE}
|
||
start_angle: 180
|
||
end_angle: 225
|
||
min_value: 0
|
||
max_value: 100
|
||
value: 0
|
||
adjustable: false
|
||
arc_width: ${COLOR_ARC_W}
|
||
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: ${COLOR_ARC_SIZE}
|
||
height: ${COLOR_ARC_SIZE}
|
||
start_angle: 225
|
||
end_angle: 270
|
||
min_value: 0
|
||
max_value: 100
|
||
value: 0
|
||
adjustable: false
|
||
arc_width: ${COLOR_ARC_W}
|
||
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: ${COLOR_ARC_SIZE}
|
||
height: ${COLOR_ARC_SIZE}
|
||
start_angle: 270
|
||
end_angle: 315
|
||
min_value: 0
|
||
max_value: 100
|
||
value: 0
|
||
adjustable: false
|
||
arc_width: ${COLOR_ARC_W}
|
||
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: ${COLOR_ARC_SIZE}
|
||
height: ${COLOR_ARC_SIZE}
|
||
start_angle: 315
|
||
end_angle: 360
|
||
min_value: 0
|
||
max_value: 100
|
||
value: 0
|
||
adjustable: false
|
||
arc_width: ${COLOR_ARC_W}
|
||
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: ${CARD_KEY_X}
|
||
y: ${CARD_KEY_Y}
|
||
width: ${CARD_KEY_SIZE}
|
||
height: ${CARD_KEY_SIZE}
|
||
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
|
||
|
||
# The sparkles key (firmware 0.2.70+): a light with effects, or with modes on its device (a WLED's palette,
|
||
# preset, speed), opens its effects page. Hidden for every other light.
|
||
- obj:
|
||
id: color_effects_button
|
||
align: TOP_RIGHT
|
||
x: -${CARD_KEY_X}
|
||
y: ${CARD_KEY_Y}
|
||
width: ${CARD_KEY_SIZE}
|
||
height: ${CARD_KEY_SIZE}
|
||
radius: 999
|
||
styles: paint_key
|
||
bg_opa: COVER
|
||
border_width: 0
|
||
hidden: true
|
||
clickable: true
|
||
scrollable: false
|
||
on_click:
|
||
- lambda: 'effects_page::open(id(active_entity));'
|
||
widgets:
|
||
- label:
|
||
align: CENTER
|
||
text: "\U000F0674"
|
||
text_font: materialdesign_icons_mini
|
||
clickable: false
|
||
|
||
- label:
|
||
id: color_detail_title
|
||
width: ${COLOR_TITLE_W}
|
||
long_mode: DOT
|
||
text_align: CENTER
|
||
align: TOP_MID
|
||
y: ${COLOR_TITLE_Y}
|
||
text: "Light Color"
|
||
styles: style_room
|
||
|
||
|
||
# ---------- CLIMATE DETAIL OVERLAY ----------
|
||
# Made for the small resistive panel: 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, so the SPI panel keeps up.
|
||
- 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: ${CARD_KEY_X}
|
||
y: ${CARD_KEY_Y}
|
||
width: ${CARD_KEY_SIZE}
|
||
height: ${CARD_KEY_SIZE}
|
||
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: -${CARD_KEY_X}
|
||
y: ${CARD_KEY_Y}
|
||
width: ${CARD_KEY_SIZE}
|
||
height: ${CARD_KEY_SIZE}
|
||
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: ${CLIMATE_TITLE_Y}
|
||
width: ${CLIMATE_TITLE_W}
|
||
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: ${CLIMATE_STATUS_Y}
|
||
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: ${CLIMATE_CARD_Y}
|
||
width: 94%
|
||
height: ${CLIMATE_CARD_H}
|
||
radius: ${CLIMATE_CARD_RADIUS}
|
||
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: ${CLIMATE_KEY_INSET}
|
||
width: ${CLIMATE_KEY_SIZE}
|
||
height: ${CLIMATE_KEY_SIZE}
|
||
radius: ${CLIMATE_KEY_RADIUS}
|
||
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: -${CLIMATE_KEY_INSET}
|
||
width: ${CLIMATE_KEY_SIZE}
|
||
height: ${CLIMATE_KEY_SIZE}
|
||
radius: ${CLIMATE_KEY_RADIUS}
|
||
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: ${CLIMATE_NUMBER_Y}
|
||
text: "--"
|
||
text_font: setpoint_digits
|
||
clickable: false
|
||
- label:
|
||
id: climate_set_label
|
||
align: CENTER
|
||
y: ${CLIMATE_CAPTION_Y}
|
||
text: !lambda 'return std::string(screen_text::tr(screen_text::txt::climate_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: ${CLIMATE_MODES_Y}
|
||
width: 94%
|
||
height: ${CLIMATE_MODES_H}
|
||
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: ${CLIMATE_MODES_GAP}
|
||
widgets:
|
||
- obj:
|
||
id: cmkey_auto
|
||
flex_grow: 1
|
||
height: ${CLIMATE_MODES_H}
|
||
radius: ${CLIMATE_MODE_RADIUS}
|
||
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: ${CLIMATE_MODES_H}
|
||
radius: ${CLIMATE_MODE_RADIUS}
|
||
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: ${CLIMATE_MODES_H}
|
||
radius: ${CLIMATE_MODE_RADIUS}
|
||
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: ${CLIMATE_MODES_H}
|
||
radius: ${CLIMATE_MODE_RADIUS}
|
||
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: ${CLIMATE_MODES_H}
|
||
radius: ${CLIMATE_MODE_RADIUS}
|
||
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: ${CLIMATE_MODES_H}
|
||
radius: ${CLIMATE_MODE_RADIUS}
|
||
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: ${CLIMATE_MODES_H}
|
||
radius: ${CLIMATE_MODE_RADIUS}
|
||
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
|
||
# ---------- 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: ${PICKER_TITLE_Y}
|
||
text: !lambda 'return std::string(screen_text::tr(screen_text::txt::climate_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: ${PICKER_LIST_Y}
|
||
width: 98%
|
||
height: ${PICKER_LIST_H}
|
||
bg_opa: TRANSP
|
||
border_width: 0
|
||
scrollable: false
|
||
clickable: false
|
||
pad_all: ${PICKER_PAD}
|
||
layout:
|
||
type: FLEX
|
||
pad_row: ${PICKER_ROW_GAP}
|
||
pad_column: ${PICKER_COL_GAP}
|
||
flex_flow: ROW_WRAP
|
||
flex_align_main: CENTER
|
||
flex_align_cross: CENTER
|
||
flex_align_track: CENTER
|
||
widgets:
|
||
- obj:
|
||
id: cmode_off
|
||
width: ${PICKER_MODE_W}
|
||
height: ${PICKER_MODE_H}
|
||
radius: ${PICKER_MODE_RADIUS}
|
||
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: !lambda 'return std::string(screen_text::tr(screen_text::txt::ha_climate_off));'
|
||
text_font: sublabel_big
|
||
clickable: false
|
||
- obj:
|
||
id: cmode_heat
|
||
width: ${PICKER_MODE_W}
|
||
height: ${PICKER_MODE_H}
|
||
radius: ${PICKER_MODE_RADIUS}
|
||
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: !lambda 'return std::string(screen_text::tr(screen_text::txt::ha_climate_heat));'
|
||
text_font: sublabel_big
|
||
clickable: false
|
||
- obj:
|
||
id: cmode_cool
|
||
width: ${PICKER_MODE_W}
|
||
height: ${PICKER_MODE_H}
|
||
radius: ${PICKER_MODE_RADIUS}
|
||
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: !lambda 'return std::string(screen_text::tr(screen_text::txt::ha_climate_cool));'
|
||
text_font: sublabel_big
|
||
clickable: false
|
||
- obj:
|
||
id: cmode_heat_cool
|
||
width: ${PICKER_MODE_W}
|
||
height: ${PICKER_MODE_H}
|
||
radius: ${PICKER_MODE_RADIUS}
|
||
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: !lambda 'return std::string(screen_text::tr(screen_text::txt::ha_climate_heat_cool));'
|
||
text_font: sublabel_big
|
||
clickable: false
|
||
- obj:
|
||
id: cmode_auto
|
||
width: ${PICKER_MODE_W}
|
||
height: ${PICKER_MODE_H}
|
||
radius: ${PICKER_MODE_RADIUS}
|
||
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: !lambda 'return std::string(screen_text::tr(screen_text::txt::ha_climate_auto));'
|
||
text_font: sublabel_big
|
||
clickable: false
|
||
- obj:
|
||
id: cmode_dry
|
||
width: ${PICKER_MODE_W}
|
||
height: ${PICKER_MODE_H}
|
||
radius: ${PICKER_MODE_RADIUS}
|
||
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: !lambda 'return std::string(screen_text::tr(screen_text::txt::ha_climate_dry));'
|
||
text_font: sublabel_big
|
||
clickable: false
|
||
- obj:
|
||
id: cmode_fan_only
|
||
width: ${PICKER_MODE_W}
|
||
height: ${PICKER_MODE_H}
|
||
radius: ${PICKER_MODE_RADIUS}
|
||
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: !lambda 'return std::string(screen_text::tr(screen_text::txt::ha_climate_fan_only));'
|
||
text_font: sublabel_big
|
||
clickable: false
|
||
|
||
- label:
|
||
id: cfan_title
|
||
width: 100%
|
||
height: ${PICKER_TITLE_H}
|
||
text: !lambda 'return std::string(screen_text::tr(screen_text::txt::climate_fan));'
|
||
text_font: sublabel
|
||
styles: paint_subtle
|
||
text_align: CENTER
|
||
clickable: false
|
||
- obj:
|
||
id: cfan_0
|
||
width: ${PICKER_CHIP_W}
|
||
height: ${PICKER_CHIP_H}
|
||
radius: ${PICKER_CHIP_RADIUS}
|
||
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: ${PICKER_CHIP_FONT}
|
||
clickable: false
|
||
- obj:
|
||
id: cfan_1
|
||
width: ${PICKER_CHIP_W}
|
||
height: ${PICKER_CHIP_H}
|
||
radius: ${PICKER_CHIP_RADIUS}
|
||
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: ${PICKER_CHIP_FONT}
|
||
clickable: false
|
||
- obj:
|
||
id: cfan_2
|
||
width: ${PICKER_CHIP_W}
|
||
height: ${PICKER_CHIP_H}
|
||
radius: ${PICKER_CHIP_RADIUS}
|
||
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: ${PICKER_CHIP_FONT}
|
||
clickable: false
|
||
- obj:
|
||
id: cfan_3
|
||
width: ${PICKER_CHIP_W}
|
||
height: ${PICKER_CHIP_H}
|
||
radius: ${PICKER_CHIP_RADIUS}
|
||
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: ${PICKER_CHIP_FONT}
|
||
clickable: false
|
||
- label:
|
||
id: cswing_title
|
||
width: 100%
|
||
height: ${PICKER_TITLE_H}
|
||
text: !lambda 'return std::string(screen_text::tr(screen_text::txt::climate_swing));'
|
||
text_font: sublabel
|
||
styles: paint_subtle
|
||
text_align: CENTER
|
||
clickable: false
|
||
- obj:
|
||
id: cswing_0
|
||
width: ${PICKER_CHIP_W}
|
||
height: ${PICKER_CHIP_H}
|
||
radius: ${PICKER_CHIP_RADIUS}
|
||
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: ${PICKER_CHIP_FONT}
|
||
clickable: false
|
||
- obj:
|
||
id: cswing_1
|
||
width: ${PICKER_CHIP_W}
|
||
height: ${PICKER_CHIP_H}
|
||
radius: ${PICKER_CHIP_RADIUS}
|
||
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: ${PICKER_CHIP_FONT}
|
||
clickable: false
|
||
- obj:
|
||
id: cswing_2
|
||
width: ${PICKER_CHIP_W}
|
||
height: ${PICKER_CHIP_H}
|
||
radius: ${PICKER_CHIP_RADIUS}
|
||
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: ${PICKER_CHIP_FONT}
|
||
clickable: false
|
||
- obj:
|
||
id: cswing_3
|
||
width: ${PICKER_CHIP_W}
|
||
height: ${PICKER_CHIP_H}
|
||
radius: ${PICKER_CHIP_RADIUS}
|
||
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: ${PICKER_CHIP_FONT}
|
||
clickable: false
|
||
- obj:
|
||
id: climate_mode_overlay_x
|
||
align: TOP_LEFT
|
||
x: ${CARD_KEY_X}
|
||
y: ${CARD_KEY_Y}
|
||
width: ${CARD_KEY_SIZE}
|
||
height: ${CARD_KEY_SIZE}
|
||
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
|
||
# A page that is one full tile switching something on or off switches with this push as well
|
||
# (firmware 0.2.65+, runtime_tiles::wake_tap); on every other page the first touch only wakes.
|
||
- lambda: 'runtime_tiles::wake_tap();'
|
||
|
||
- id: touch_cal_page
|
||
styles: ${TOUCH_TEST_PAGE_PAINT}
|
||
bg_opa: COVER
|
||
pad_all: 0
|
||
border_width: 0
|
||
scrollable: false
|
||
widgets:
|
||
- label:
|
||
align: TOP_MID
|
||
y: ${TOUCH_TEST_TITLE_Y}
|
||
text: !lambda 'return std::string(screen_text::tr(screen_text::txt::${TOUCH_TEST_TITLE}));'
|
||
text_font: ${TOUCH_TEST_TITLE_FONT}
|
||
- obj:
|
||
x: ${TOUCH_TEST_INSET}
|
||
y: ${TOUCH_TEST_INSET}
|
||
width: ${TOUCH_TEST_SIZE}
|
||
height: ${TOUCH_TEST_SIZE}
|
||
pad_all: 0
|
||
border_width: 0
|
||
bg_opa: TRANSP
|
||
scrollable: false
|
||
widgets:
|
||
- label:
|
||
align: CENTER
|
||
text: "+"
|
||
text_font: headline
|
||
styles: ${TOUCH_TEST_PAINT}
|
||
- obj:
|
||
x: ${TOUCH_TEST_FAR_X}
|
||
y: ${TOUCH_TEST_INSET}
|
||
width: ${TOUCH_TEST_SIZE}
|
||
height: ${TOUCH_TEST_SIZE}
|
||
pad_all: 0
|
||
border_width: 0
|
||
bg_opa: TRANSP
|
||
scrollable: false
|
||
widgets:
|
||
- label:
|
||
align: CENTER
|
||
text: "+"
|
||
text_font: headline
|
||
styles: ${TOUCH_TEST_PAINT}
|
||
- obj:
|
||
x: ${TOUCH_TEST_FAR_X}
|
||
y: ${TOUCH_TEST_FAR_Y}
|
||
width: ${TOUCH_TEST_SIZE}
|
||
height: ${TOUCH_TEST_SIZE}
|
||
pad_all: 0
|
||
border_width: 0
|
||
bg_opa: TRANSP
|
||
scrollable: false
|
||
widgets:
|
||
- label:
|
||
align: CENTER
|
||
text: "+"
|
||
text_font: headline
|
||
styles: ${TOUCH_TEST_PAINT}
|
||
- obj:
|
||
x: ${TOUCH_TEST_INSET}
|
||
y: ${TOUCH_TEST_FAR_Y}
|
||
width: ${TOUCH_TEST_SIZE}
|
||
height: ${TOUCH_TEST_SIZE}
|
||
pad_all: 0
|
||
border_width: 0
|
||
bg_opa: TRANSP
|
||
scrollable: false
|
||
widgets:
|
||
- label:
|
||
align: CENTER
|
||
text: "+"
|
||
text_font: headline
|
||
styles: ${TOUCH_TEST_PAINT}
|
||
- obj:
|
||
x: ${TOUCH_TEST_MID_X}
|
||
y: ${TOUCH_TEST_MID_Y}
|
||
width: ${TOUCH_TEST_SIZE}
|
||
height: ${TOUCH_TEST_SIZE}
|
||
pad_all: 0
|
||
border_width: 0
|
||
bg_opa: TRANSP
|
||
scrollable: false
|
||
widgets:
|
||
- label:
|
||
align: CENTER
|
||
text: "+"
|
||
text_font: headline
|
||
styles: ${TOUCH_TEST_PAINT}
|
||
|
||
script:
|
||
- 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);
|
||
}
|
||
}
|
||
settings_screen::page_dots(id(page_number), id(tile_page), 2, ${PAGE_DOTS_LARGE});
|
||
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);
|
||
${APPLY_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;
|
||
${APPLY_BACKLIGHT}
|
||
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();
|
||
}
|
||
${APPLY_ROTATION_ENTITY}
|
||
// The Page buttons setting moves the cards into the bar's room or back (firmware 0.2.69+).
|
||
if (runtime_tiles::enabled) runtime_tiles::page_buttons_changed();
|
||
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() ||${AWAY_EXTRA} !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();
|
||
${ALERT_SHOW_HOOK}
|
||
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_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();'
|
||
|
||
# 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.
|
||
# 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 modes, at most four each, in Home Assistant's words where it names them.
|
||
auto fill = [&](char kind, 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), tile_controls::climate_setting_text(kind, 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('f', id(cfan_title), fan_chips, id(active_climate_fan_modes), id(active_climate_fan_mode));
|
||
fill('s', 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;
|
||
${CLIMATE_MODE_CHOSEN_HOOK}
|
||
- 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;
|
||
${CLIMATE_MODE_CHOSEN_HOOK}
|
||
- 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));
|
||
if (effects_page::offered(id(active_entity))) lv_obj_remove_flag(id(color_effects_button), LV_OBJ_FLAG_HIDDEN);
|
||
else lv_obj_add_flag(id(color_effects_button), LV_OBJ_FLAG_HIDDEN);
|
||
- lvgl.widget.hide: brightness_overlay
|
||
- lvgl.widget.show: color_detail_overlay
|
||
|
||
- 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 {
|
||
value_text = open_label + " - " + screen_text::percent(pct);
|
||
}
|
||
} 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) = screen_text::percent_sign();
|
||
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());
|
||
${OPEN_VALUE_OVERLAY_TAIL}
|
||
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 {
|
||
value_text = label_on + " - " + screen_text::percent(pct);
|
||
}
|
||
} 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;
|
||
}
|
||
|
||
|
||
# The screen's texts in the LANGUAGE substitution's language: the build puts that one language in the firmware
|
||
# (components/smart_display).
|
||
smart_display:
|
||
language: ${LANGUAGE}
|
||
|
||
# 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));'
|