Files
homeassistant_espscreen/home-like-2432s028.yaml
T
Max Gramser 3e959b3df9 Put ESP Screens in English and keep layouts when entity IDs change (0.2.40)
Everything a user sees or reads is now English: both boards' labels,
status replies and logs, the ESP Screens editor and Alerts cheatsheet,
and the README and guides (docs/TILES.md, CALIBRATING.md, ACCEPTANCE.md
and TROUBLESHOOTING.md replace the Dutch file names). Labels follow
Home Assistant's own English names (Heat/Cool modes, Heating/Cooling
actions, Away, Returning to dock, weather conditions), dates use English
abbreviations and the top bar writes numbers the English way. Docs use
HA 2026.9's menu names (Settings -> Apps, App store, Settings -> Tools).
Unused legacy files (home-like.yaml, buttons.yaml, a.out) are removed.

Firmware 0.2.34 names its entities in English. Home Assistant registers
a renamed ESPHome entity under a new entity ID, so the inbox
text.<screen>_tegelinstellingen becomes text.<screen>_tile_settings and
the layout stored under the old ID would be lost. The manager now
recognises the screen by its device, during a run or after a restart,
and moves its layout and update history along; the Update button and
the nightly round follow the screen through the rename. Screens that
still run 0.2.33 or older keep working: both languages are matched.

Main is the only release branch from now on.

Tested on a Home Assistant Yellow (HA 2026.9.1) with both bench boards:
see docs/TEST_RESULTS_0240.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 02:08:43 +02:00

7012 lines
316 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
############################################################
# SmartDisplay – iOS Home app–inspired Tiles UI
# Target device: ESP32-2432S028 / Cheap Yellow Display (CYD)
# Resolution: configurable via ORIENTATION substitutions (landscape 320×240 default)
#
# Target hardware
# - Cheap Yellow Display (ESP32-2432S028 or compatible)
# - Built-in ILI9341 320x240 display
# - Built-in XPT2046 touchscreen
#
# What this config does
# - Renders a gradient background + two fixed pages of “tiles” (6 + 4) (LVGL).
# - Each tile shows: icon (MDI font), title, and a value line.
# - Tile taps publish a short-lived text_sensor event (smartdisplay_action)
# so you can build Home Assistant automations on top.
# - Optional: the device can also call Home Assistant services directly
# (toggle lights/switches/fans/covers, activate scenes/scripts, set fan preset,
# set light brightness, set fan percentage, set cover position).
#
# The layout is inspired by common modern smart home UIs.
#
# How to customize
# 1) Create device.yaml with tools/new_device.py; edit its TILE1..TILE10 overrides.
# 2) Icons come from the tile icon set in screen_manager/app/tile_icons.py; the
# icon fonts already contain all of them (tools/generate_icons.py).
# 3) The background is a gradient (BG_TOP_COLOR / BG_BOTTOM_COLOR), not an image.
# Required fonts are bundled in fonts/ (Roboto and Material Design Icons).
#
# Tile configuration model
# - TILE*_TYPE defines the entity kind: light | fan | switch | scene | script | cover
# - TILE*_TAP_ACTION defines what happens on short tap:
# auto | toggle | activate | fan_toggle_preset | custom
# - TILE*_LONGPRESS defines the long press mode: none | slider | action
# slider: opens a value overlay — configure with TILE*_LONGPRESS_SLIDER + TILE*_LONGPRESS_OFF_VALUE
# action: fires a different entity — configure with TILE*_LONGPRESS_ACTION*
# - TILE*_VALUE_MODE defines how the value line is rendered:
# auto | brightness | percentage | preset | text
#
# Notes
# - DIRECT_ACTIONS="true" enables direct HA service calls from the device.
# If "false", only the smartdisplay_action event is emitted.
# - In auto mode, values and long-press behavior are derived from TILE*_TYPE:
# light -> brightness, fan -> percentage/preset, scene/script -> text
#
# Device configuration: home-like-2432s028.yaml (ESP32-2432S028).
# Credentials stay in secrets.yaml. Fonts are bundled for offline validation.
# Start at README.md; docs/ covers calibration, HA setup and acceptance tests.
#
# What you must adapt before flashing
# 1) Secrets: api_encryption_key, ota_password, WiFi credentials (see secrets.yaml)
# 2) Pins, touch calibration and transform if your CYD variant differs
# 3) TILE*_ENTITY / TILE*_STATE_ENTITY: Home Assistant entity IDs you want to control/mirror
# 4) Orientation + touch calibration:
# - rotation is set via LVGL_ROTATION in the ORIENTATION presets (LVGL
# rotates display + touch together, ESPHome 2026.4+)
# - calibration is device-specific
############################################################
substitutions:
SCREEN_FIRMWARE_VERSION: "0.2.34"
# A tap survives a finger that drifts up to this far (320 px over 58 mm: 1 cm); further is a swipe.
TOUCH_MOVE_LIMIT_PX: "56"
# Shortest contact that counts as a tap; a resistive panel bounces, a capacitive one does not.
TOUCH_MIN_PRESS_MS: "60"
DYNAMIC_TILES: "false"
DASHBOARD_INBOX_INTERNAL: "true"
DEVICE_NAME: "cyd-new"
DEVICE_FRIENDLY_NAME: "My CYD"
CALIBRATION_ON_BOOT: "false"
TOUCH_AFFINE_XX: "1.000000000"
TOUCH_AFFINE_XY: "0.000000000"
TOUCH_AFFINE_XC: "0.000000000"
TOUCH_AFFINE_YX: "0.000000000"
TOUCH_AFFINE_YY: "1.000000000"
TOUCH_AFFINE_YC: "0.000000000"
TILE_COUNT: "6" # 1-10; <=6 hides page navigation
DIRECT_ACTIONS: "false" # true = call Home Assistant services directly, false = only emit action events
ROOM_NAME: "Living room" # room name shown in the top-left header
TIME_24H: "true" # true = 24h format, false = 12h format
## 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
# ============================================================
# ACCENT COLOURS — determined automatically from the Home Assistant domain
#
# The domain is the part before the dot in the entity_id:
# script.good_morning -> "script" -> ACCENT_ACTION
# light.living_room_2 -> "light" -> ACCENT_LIGHT
# vacuum.s8 -> "vacuum" -> ACCENT_DEVICE
#
# That's Home Assistant's own grouping, so a tile automatically gets the
# right colour as soon as you put an entity in it — you no longer have
# to pick colours per tile. The distinction that matters: a script/scene
# fires, a device you turn on or off. Those two shouldn't look the same.
#
# Set TILE*_CIRCLE_ACTIVE_COLOR / TILE*_ICON_DISABLED_COLOR to
# ${ACCENT_AUTO} to use this, or to a real hex value to colour that
# one tile by hand (see tile 8 — red as soon as there's mail).
# ============================================================
ACCENT_AUTO: "0x1000000" # sentinel: deliberately outside the 24-bit colour range
ACCENT_ACTION: "0x8B5CF6" # script / scene / button / input_button / automation
ACCENT_LIGHT: "0xFEC600" # light
ACCENT_DEVICE: "0x00C5EC" # switch, fan, cover, climate, vacuum, media_player, rest
# ============================================================
# FONT GLYPHS
# The icon fonts carry the full tile icon set from screen_manager/app/tile_icons.py
# (tools/generate_icons.py). Pick TILEn_ICON from that set; no glyph list to maintain.
# ============================================================
# ============================================================
# TILE 1
# ============================================================
# --- basic settings ---
TILE1_ENTITY: "light.example_lamp" # main Home Assistant entity for this tile
TILE1_STATE_ENTITY: "light.example_lamp" # entity used for active/inactive state styling
TILE1_TITLE: "Lamp" # tile title shown in the UI
TILE1_ICON: "\U000F001B" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py)
TILE1_TYPE: "light" # tile type: light | fan | switch | scene | script | cover | climate
TILE1_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset | custom
TILE1_LONGPRESS: "slider" # long press mode: none | slider | action
TILE1_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text
TILE1_LABEL_OFF: "Off" # text shown when entity is off
TILE1_LABEL_ON: "On" # text shown for text-mode tiles when entity is on
TILE1_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on")
# --- colors ---
TILE1_CIRCLE_ACTIVE_COLOR: "${ACCENT_AUTO}" # icon circle color when active
TILE1_CIRCLE_DISABLED_COLOR: "0x7B7B6F" # icon circle color when inactive
TILE1_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active
TILE1_ICON_DISABLED_COLOR: "${ACCENT_AUTO}" # icon color when inactive
TILE1_BG_ACTIVE_COLOR: "0xFFFFFF" # tile background color when active
TILE1_BG_DISABLED_COLOR: "0x939391" # tile background color when inactive
TILE1_TITLE_ACTIVE_COLOR: "0x000000" # title color when active
TILE1_TITLE_DISABLED_COLOR: "0xFFFFFF" # title color when inactive
TILE1_VALUE_ACTIVE_COLOR: "0x7A7A7C" # value color when active
TILE1_VALUE_DISABLED_COLOR: "0xD9D9D9" # value color when inactive
# --- advanced settings ---
TILE1_TAP_SERVICE: "" # optional service override, e.g. light.toggle
TILE1_TAP_PARAM_KEY: "" # optional extra service parameter key
TILE1_TAP_PARAM_VAL: "" # optional extra service parameter value
# if LONGPRESS = slider:
TILE1_LONGPRESS_SLIDER: "auto" # slider type: auto | brightness | percentage | cover_position
TILE1_LONGPRESS_OFF_VALUE: "0" # initial slider value when entity is off
# if LONGPRESS = action:
TILE1_LONGPRESS_ACTION: "" # entity_id to fire on long press
TILE1_LONGPRESS_ACTION_TYPE: "" # entity type: light | fan | switch | scene | script | cover | climate (empty = same as TILE*_TYPE)
TILE1_LONGPRESS_ACTION_SERVICE: "" # service to call (default: activate/turn_on — set e.g. "light.toggle" to toggle instead)
# if TYPE = climate:
TILE1_CLIMATE_MIN_TEMP: "16.0" # minimum target temperature shown in the thermostat overlay
TILE1_CLIMATE_MAX_TEMP: "32.0" # maximum target temperature shown in the thermostat overlay
TILE1_CLIMATE_STEP: "1.0" # target temperature step size
TILE1_CLIMATE_CURRENT_TEMP_ATTRIBUTE: "current_temperature" # HA attribute for current room temperature
TILE1_CLIMATE_TARGET_TEMP_ATTRIBUTE: "temperature" # HA attribute for target temperature
TILE1_CLIMATE_HUMIDITY_ATTRIBUTE: "current_humidity" # HA attribute for humidity; leave as-is if unavailable
# ============================================================
# TILE 2
# ============================================================
# --- basic settings ---
TILE2_ENTITY: "switch.example_plug" # main Home Assistant entity for this tile
TILE2_STATE_ENTITY: "switch.example_plug" # entity used for active/inactive state styling
TILE2_TITLE: "Plug" # tile title shown in the UI
TILE2_ICON: "\U000F0387" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py)
TILE2_TYPE: "switch" # tile type: light | fan | switch | scene | script | cover | climate
TILE2_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset | custom
TILE2_LONGPRESS: "none" # long press mode: none | slider | action
TILE2_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text
TILE2_LABEL_OFF: "Off" # text shown when entity is off
TILE2_LABEL_ON: "On" # text shown for text-mode tiles when entity is on
TILE2_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on")
# --- colors ---
TILE2_CIRCLE_ACTIVE_COLOR: "${ACCENT_AUTO}" # icon circle color when active
TILE2_CIRCLE_DISABLED_COLOR: "0x7B7B6F" # icon circle color when inactive
TILE2_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active
TILE2_ICON_DISABLED_COLOR: "${ACCENT_AUTO}" # icon color when inactive
TILE2_BG_ACTIVE_COLOR: "0xFFFFFF" # tile background color when active
TILE2_BG_DISABLED_COLOR: "0x939391" # tile background color when inactive
TILE2_TITLE_ACTIVE_COLOR: "0x000000" # title color when active
TILE2_TITLE_DISABLED_COLOR: "0xFFFFFF" # title color when inactive
TILE2_VALUE_ACTIVE_COLOR: "0x7A7A7C" # value color when active
TILE2_VALUE_DISABLED_COLOR: "0xD9D9D9" # value color when inactive
# --- advanced settings ---
TILE2_TAP_SERVICE: "" # optional service override, e.g. light.toggle
TILE2_TAP_PARAM_KEY: "" # optional extra service parameter key
TILE2_TAP_PARAM_VAL: "" # optional extra service parameter value
# if LONGPRESS = slider:
TILE2_LONGPRESS_SLIDER: "auto" # slider type: auto | brightness | percentage | cover_position
TILE2_LONGPRESS_OFF_VALUE: "0" # initial slider value when entity is off
# if LONGPRESS = action:
TILE2_LONGPRESS_ACTION: "" # entity_id to fire on long press
TILE2_LONGPRESS_ACTION_TYPE: "" # entity type: light | fan | switch | scene | script | cover | climate (empty = same as TILE*_TYPE)
TILE2_LONGPRESS_ACTION_SERVICE: "" # service to call (default: activate/turn_on — set e.g. "light.toggle" to toggle instead)
# if TYPE = climate:
TILE2_CLIMATE_MIN_TEMP: "5.0" # minimum target temperature shown in the thermostat overlay
TILE2_CLIMATE_MAX_TEMP: "30.0" # maximum target temperature shown in the thermostat overlay
TILE2_CLIMATE_STEP: "0.5" # target temperature step size
TILE2_CLIMATE_CURRENT_TEMP_ATTRIBUTE: "current_temperature" # HA attribute for current room temperature
TILE2_CLIMATE_TARGET_TEMP_ATTRIBUTE: "temperature" # HA attribute for target temperature
TILE2_CLIMATE_HUMIDITY_ATTRIBUTE: "current_humidity" # HA attribute for humidity; leave as-is if unavailable
# ============================================================
# TILE 3
# ============================================================
# --- basic settings ---
TILE3_ENTITY: "scene.example_evening" # main Home Assistant entity for this tile
TILE3_STATE_ENTITY: "scene.example_evening" # entity used for active/inactive state styling
TILE3_TITLE: "Evening" # tile title shown in the UI
TILE3_ICON: "\U000F02E3" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py)
TILE3_TYPE: "scene" # tile type: light | fan | switch | scene | script | cover | climate
TILE3_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset | custom
TILE3_LONGPRESS: "none" # long press mode: none | slider | action
TILE3_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text
TILE3_LABEL_OFF: "Off" # text shown when entity is off
TILE3_LABEL_ON: "On" # text shown for text-mode tiles when entity is on
TILE3_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on")
# --- colors ---
TILE3_CIRCLE_ACTIVE_COLOR: "${ACCENT_AUTO}" # icon circle color when active
TILE3_CIRCLE_DISABLED_COLOR: "0x7B7B6F" # icon circle color when inactive
TILE3_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active
TILE3_ICON_DISABLED_COLOR: "${ACCENT_AUTO}" # icon color when inactive
TILE3_BG_ACTIVE_COLOR: "0xFFFFFF" # tile background color when active
TILE3_BG_DISABLED_COLOR: "0x939391" # tile background color when inactive
TILE3_TITLE_ACTIVE_COLOR: "0x000000" # title color when active
TILE3_TITLE_DISABLED_COLOR: "0xFFFFFF" # title color when inactive
TILE3_VALUE_ACTIVE_COLOR: "0x7A7A7C" # value color when active
TILE3_VALUE_DISABLED_COLOR: "0xD9D9D9" # value color when inactive
# --- advanced settings ---
TILE3_TAP_SERVICE: "" # optional service override, e.g. light.toggle
TILE3_TAP_PARAM_KEY: "" # optional extra service parameter key
TILE3_TAP_PARAM_VAL: "" # optional extra service parameter value
# if LONGPRESS = slider:
TILE3_LONGPRESS_SLIDER: "auto" # slider type: auto | brightness | percentage | cover_position
TILE3_LONGPRESS_OFF_VALUE: "0" # initial slider value when entity is off
# if LONGPRESS = action:
TILE3_LONGPRESS_ACTION: "" # entity_id to fire on long press
TILE3_LONGPRESS_ACTION_TYPE: "" # entity type: light | fan | switch | scene | script | cover | climate (empty = same as TILE*_TYPE)
TILE3_LONGPRESS_ACTION_SERVICE: "" # service to call (default: activate/turn_on — set e.g. "light.toggle" to toggle instead)
# if TYPE = climate:
TILE3_CLIMATE_MIN_TEMP: "5.0" # minimum target temperature shown in the thermostat overlay
TILE3_CLIMATE_MAX_TEMP: "30.0" # maximum target temperature shown in the thermostat overlay
TILE3_CLIMATE_STEP: "0.5" # target temperature step size
TILE3_CLIMATE_CURRENT_TEMP_ATTRIBUTE: "current_temperature" # HA attribute for current room temperature
TILE3_CLIMATE_TARGET_TEMP_ATTRIBUTE: "temperature" # HA attribute for target temperature
TILE3_CLIMATE_HUMIDITY_ATTRIBUTE: "current_humidity" # HA attribute for humidity; leave as-is if unavailable
# ============================================================
# TILE 4
# ============================================================
# --- basic settings ---
TILE4_ENTITY: "script.example_off" # main Home Assistant entity for this tile
TILE4_STATE_ENTITY: "script.example_off" # entity used for active/inactive state styling
TILE4_TITLE: "All off" # tile title shown in the UI
TILE4_ICON: "\U000F059C" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py)
TILE4_TYPE: "script" # tile type: light | fan | switch | scene | script | cover | climate
TILE4_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset | custom
TILE4_LONGPRESS: "none" # long press mode: none | slider | action
TILE4_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text
TILE4_LABEL_OFF: "Off" # text shown when entity is off
TILE4_LABEL_ON: "On" # text shown for text-mode tiles when entity is on
TILE4_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on")
# --- colors ---
TILE4_CIRCLE_ACTIVE_COLOR: "${ACCENT_AUTO}" # icon circle color when active
TILE4_CIRCLE_DISABLED_COLOR: "0x7B7B6F" # icon circle color when inactive
TILE4_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active
TILE4_ICON_DISABLED_COLOR: "${ACCENT_AUTO}" # icon color when inactive
TILE4_BG_ACTIVE_COLOR: "0xFFFFFF" # tile background color when active
TILE4_BG_DISABLED_COLOR: "0x939391" # tile background color when inactive
TILE4_TITLE_ACTIVE_COLOR: "0x000000" # title color when active
TILE4_TITLE_DISABLED_COLOR: "0xFFFFFF" # title color when inactive
TILE4_VALUE_ACTIVE_COLOR: "0x7A7A7C" # value color when active
TILE4_VALUE_DISABLED_COLOR: "0xD9D9D9" # value color when inactive
# --- advanced settings ---
TILE4_TAP_SERVICE: "" # optional service override, e.g. light.toggle
TILE4_TAP_PARAM_KEY: "" # optional extra service parameter key
TILE4_TAP_PARAM_VAL: "" # optional extra service parameter value
# if LONGPRESS = slider:
TILE4_LONGPRESS_SLIDER: "auto" # slider type: auto | brightness | percentage | cover_position
TILE4_LONGPRESS_OFF_VALUE: "0" # initial slider value when entity is off
# if LONGPRESS = action:
TILE4_LONGPRESS_ACTION: "" # entity_id to fire on long press
TILE4_LONGPRESS_ACTION_TYPE: "" # entity type: light | fan | switch | scene | script | cover | climate (empty = same as TILE*_TYPE)
TILE4_LONGPRESS_ACTION_SERVICE: "" # service to call (default: activate/turn_on — set e.g. "light.toggle" to toggle instead)
# if TYPE = climate:
TILE4_CLIMATE_MIN_TEMP: "5.0" # minimum target temperature shown in the thermostat overlay
TILE4_CLIMATE_MAX_TEMP: "30.0" # maximum target temperature shown in the thermostat overlay
TILE4_CLIMATE_STEP: "0.5" # target temperature step size
TILE4_CLIMATE_CURRENT_TEMP_ATTRIBUTE: "current_temperature" # HA attribute for current room temperature
TILE4_CLIMATE_TARGET_TEMP_ATTRIBUTE: "temperature" # HA attribute for target temperature
TILE4_CLIMATE_HUMIDITY_ATTRIBUTE: "current_humidity" # HA attribute for humidity; leave as-is if unavailable
# ============================================================
# TILE 5
# ============================================================
# --- basic settings ---
TILE5_ENTITY: "climate.example_ac" # main Home Assistant entity for this tile
TILE5_STATE_ENTITY: "climate.example_ac" # entity used for active/inactive state styling
TILE5_TITLE: "AC" # tile title shown in the UI
TILE5_ICON: "\U000F0425" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py)
TILE5_TYPE: "climate" # tile type: light | fan | switch | scene | script | cover | climate
TILE5_TAP_ACTION: "toggle" # tap action: auto | toggle | activate | fan_toggle_preset | custom
TILE5_LONGPRESS: "slider" # long press mode: none | slider | action
TILE5_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text
TILE5_LABEL_OFF: "Off" # text shown when entity is off (closed)
TILE5_LABEL_ON: "On" # text shown when entity is on (open)
TILE5_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on")
# --- colors ---
TILE5_CIRCLE_ACTIVE_COLOR: "${ACCENT_AUTO}" # icon circle color when active
TILE5_CIRCLE_DISABLED_COLOR: "0x7B7B6F" # icon circle color when inactive
TILE5_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active
TILE5_ICON_DISABLED_COLOR: "${ACCENT_AUTO}" # icon color when inactive
TILE5_BG_ACTIVE_COLOR: "0xFFFFFF" # tile background color when active
TILE5_BG_DISABLED_COLOR: "0x939391" # tile background color when inactive
TILE5_TITLE_ACTIVE_COLOR: "0x000000" # title color when active
TILE5_TITLE_DISABLED_COLOR: "0xFFFFFF" # title color when inactive
TILE5_VALUE_ACTIVE_COLOR: "0x7A7A7C" # value color when active
TILE5_VALUE_DISABLED_COLOR: "0xD9D9D9" # value color when inactive
# --- advanced settings ---
TILE5_TAP_SERVICE: "climate.toggle" # optional service override, e.g. light.toggle
TILE5_TAP_PARAM_KEY: "" # optional extra service parameter key
TILE5_TAP_PARAM_VAL: "" # optional extra service parameter value
# if LONGPRESS = slider:
TILE5_LONGPRESS_SLIDER: "auto" # slider type: auto | brightness | percentage | cover_position
TILE5_LONGPRESS_OFF_VALUE: "0" # initial slider value when entity is off
# if LONGPRESS = action:
TILE5_LONGPRESS_ACTION: "" # entity_id to fire on long press
TILE5_LONGPRESS_ACTION_TYPE: "" # entity type: light | fan | switch | scene | script | cover | climate (empty = same as TILE*_TYPE)
TILE5_LONGPRESS_ACTION_SERVICE: "" # service to call (default: activate/turn_on — set e.g. "light.toggle" to toggle instead)
# if TYPE = climate:
TILE5_CLIMATE_MIN_TEMP: "5.0" # minimum target temperature shown in the thermostat overlay
TILE5_CLIMATE_MAX_TEMP: "30.0" # maximum target temperature shown in the thermostat overlay
TILE5_CLIMATE_STEP: "0.5" # target temperature step size
TILE5_CLIMATE_CURRENT_TEMP_ATTRIBUTE: "current_temperature" # HA attribute for current room temperature
TILE5_CLIMATE_TARGET_TEMP_ATTRIBUTE: "temperature" # HA attribute for target temperature
TILE5_CLIMATE_HUMIDITY_ATTRIBUTE: "current_humidity" # HA attribute for humidity; leave as-is if unavailable
# ============================================================
# TILE 6
# ============================================================
# --- basic settings ---
TILE6_ENTITY: "vacuum.example_robot" # main Home Assistant entity for this tile
TILE6_STATE_ENTITY: "vacuum.example_robot" # entity used for active/inactive state styling
TILE6_TITLE: "Vacuum" # tile title shown in the UI
TILE6_ICON: "\U000F070D" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py)
TILE6_TYPE: "vacuum" # tile type: light | fan | switch | scene | script | cover | climate
TILE6_TAP_ACTION: "custom" # tap action: auto | toggle | activate | fan_toggle_preset | custom
TILE6_LONGPRESS: "vacuum" # long press mode: none | slider | action
TILE6_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text
TILE6_LABEL_OFF: "Off" # text shown when entity is off
TILE6_LABEL_ON: "On" # text shown for text-mode tiles when entity is on
TILE6_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on")
# --- colors ---
TILE6_CIRCLE_ACTIVE_COLOR: "${ACCENT_AUTO}" # icon circle color when active
TILE6_CIRCLE_DISABLED_COLOR: "0x7B7B6F" # icon circle color when inactive
TILE6_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active
TILE6_ICON_DISABLED_COLOR: "${ACCENT_AUTO}" # icon color when inactive
TILE6_BG_ACTIVE_COLOR: "0xFFFFFF" # tile background color when active
TILE6_BG_DISABLED_COLOR: "0x939391" # tile background color when inactive
TILE6_TITLE_ACTIVE_COLOR: "0x000000" # title color when active
TILE6_TITLE_DISABLED_COLOR: "0xFFFFFF" # title color when inactive
TILE6_VALUE_ACTIVE_COLOR: "0x7A7A7C" # value color when active
TILE6_VALUE_DISABLED_COLOR: "0xD9D9D9" # value color when inactive
# --- advanced settings ---
TILE6_TAP_SERVICE: "vacuum.start" # optional service override, e.g. fan.toggle
TILE6_TAP_PARAM_KEY: "" # optional extra service parameter key
TILE6_TAP_PARAM_VAL: "" # optional extra service parameter value
# if LONGPRESS = slider:
TILE6_LONGPRESS_SLIDER: "auto" # slider type: auto | brightness | percentage | cover_position
TILE6_LONGPRESS_OFF_VALUE: "0" # initial slider value when entity is off
# if LONGPRESS = action:
TILE6_LONGPRESS_ACTION: "" # entity_id to fire on long press
TILE6_LONGPRESS_ACTION_TYPE: "" # entity type: light | fan | switch | scene | script | cover | climate (empty = same as TILE*_TYPE)
TILE6_LONGPRESS_ACTION_SERVICE: "" # service to call (default: activate/turn_on — set e.g. "light.toggle" to toggle instead)
# if TYPE = climate:
TILE6_CLIMATE_MIN_TEMP: "5.0" # minimum target temperature shown in the thermostat overlay
TILE6_CLIMATE_MAX_TEMP: "30.0" # maximum target temperature shown in the thermostat overlay
TILE6_CLIMATE_STEP: "0.5" # target temperature step size
TILE6_CLIMATE_CURRENT_TEMP_ATTRIBUTE: "current_temperature" # HA attribute for current room temperature
TILE6_CLIMATE_TARGET_TEMP_ATTRIBUTE: "temperature" # HA attribute for target temperature
TILE6_CLIMATE_HUMIDITY_ATTRIBUTE: "current_humidity" # HA attribute for humidity; leave as-is if unavailable
# ============================================================
# TILE 7 — Table lamp
# Brightness-only lamp (HA reports supported_color_modes: ['brightness']),
# so the colour button in the slider overlay stays hidden automatically.
# ============================================================
# --- basic settings ---
TILE7_ENTITY: "light.unused_7" # main Home Assistant entity for this tile
TILE7_STATE_ENTITY: "light.unused_7" # entity used for active/inactive state styling
TILE7_TITLE: "Tile 7" # tile title shown in the UI
TILE7_ICON: "\U000F06B5" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py)
TILE7_TYPE: "light" # tile type: light | fan | switch | scene | script | cover | climate
TILE7_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset
TILE7_LONGPRESS: "none" # long press mode: none | slider | action | vacuum
TILE7_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text
TILE7_LABEL_OFF: "Off" # text shown when entity is off
TILE7_LABEL_ON: "On" # text shown for text-mode tiles when entity is on
TILE7_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on")
# --- colors ---
TILE7_CIRCLE_ACTIVE_COLOR: "${ACCENT_AUTO}" # icon circle color when active
TILE7_CIRCLE_DISABLED_COLOR: "0x7B7B6F" # icon circle color when inactive
TILE7_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active
TILE7_ICON_DISABLED_COLOR: "${ACCENT_AUTO}" # icon color when inactive
TILE7_BG_ACTIVE_COLOR: "0xFFFFFF" # tile background color when active
TILE7_BG_DISABLED_COLOR: "0x939391" # tile background color when inactive
TILE7_TITLE_ACTIVE_COLOR: "0x000000" # title color when active
TILE7_TITLE_DISABLED_COLOR: "0xFFFFFF" # title color when inactive
TILE7_VALUE_ACTIVE_COLOR: "0x7A7A7C" # value color when active
TILE7_VALUE_DISABLED_COLOR: "0xD9D9D9" # value color when inactive
# --- advanced settings ---
TILE7_TAP_SERVICE: "" # optional service override, e.g. light.toggle
TILE7_TAP_PARAM_KEY: "" # optional extra service parameter key
TILE7_TAP_PARAM_VAL: "" # optional extra service parameter value
# if LONGPRESS = slider:
TILE7_LONGPRESS_SLIDER: "auto" # auto = brightness for a light
TILE7_LONGPRESS_OFF_VALUE: "0" # initial slider value when entity is off
# if LONGPRESS = action:
TILE7_LONGPRESS_ACTION: "" # entity_id to fire on long press
TILE7_LONGPRESS_ACTION_TYPE: "" # entity type (empty = same as TILE*_TYPE)
TILE7_LONGPRESS_ACTION_SERVICE: "" # service to call (default: activate/turn_on)
# ============================================================
# TILE 8 — Mailbox reset
# input_select.mailbox_status has options ['Full', 'Empty'].
# The tile lights up as soon as the status is "Full" (ACTIVE_STATE), and a
# short tap sets it back to "Empty" via input_select.select_option.
# ============================================================
# --- basic settings ---
TILE8_ENTITY: "light.unused_8" # main Home Assistant entity for this tile
TILE8_STATE_ENTITY: "light.unused_8" # entity used for active/inactive state styling
TILE8_TITLE: "Tile 8" # tile title shown in the UI
TILE8_ICON: "\U000F06EE" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py)
TILE8_TYPE: "light" # generic type; the real behavior comes from TAP_SERVICE below
TILE8_TAP_ACTION: "auto" # activate + TAP_SERVICE = a standalone service call
TILE8_LONGPRESS: "none" # long press only sends the event to HA
TILE8_VALUE_MODE: "auto" # shows LABEL_ON / LABEL_OFF
TILE8_LABEL_OFF: "Off" # text shown when entity is off / inactive
TILE8_LABEL_ON: "On" # text shown when entity is active
TILE8_ACTIVE_STATE: "" # tile is "on" as soon as the status is exactly "Full"
# --- colors ---
TILE8_CIRCLE_ACTIVE_COLOR: "0xE5484D" # red as soon as there's mail
TILE8_CIRCLE_DISABLED_COLOR: "0x7B7B6F" # icon circle color when inactive
TILE8_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active
TILE8_ICON_DISABLED_COLOR: "0xE5484D" # icon color when inactive
TILE8_BG_ACTIVE_COLOR: "0xFFFFFF" # tile background color when active
TILE8_BG_DISABLED_COLOR: "0x939391" # tile background color when inactive
TILE8_TITLE_ACTIVE_COLOR: "0x000000" # title color when active
TILE8_TITLE_DISABLED_COLOR: "0xFFFFFF" # title color when inactive
TILE8_VALUE_ACTIVE_COLOR: "0x7A7A7C" # value color when active
TILE8_VALUE_DISABLED_COLOR: "0xD9D9D9" # value color when inactive
# --- advanced settings ---
TILE8_TAP_SERVICE: "" # sets the input_select to a fixed option
TILE8_TAP_PARAM_KEY: "" # parameter name for input_select.select_option
TILE8_TAP_PARAM_VAL: "" # the "reset" value
# if LONGPRESS = slider:
TILE8_LONGPRESS_SLIDER: "auto" # not in use (LONGPRESS = none)
TILE8_LONGPRESS_OFF_VALUE: "0" # not in use
# if LONGPRESS = action:
TILE8_LONGPRESS_ACTION: "" # entity_id to fire on long press
TILE8_LONGPRESS_ACTION_TYPE: "" # entity type (empty = same as TILE*_TYPE)
TILE8_LONGPRESS_ACTION_SERVICE: "" # service to call (default: activate/turn_on)
# ============================================================
# TILE 9 — Living room
# Moved from tile 2 to row 5 to make room for Ambient.
# Colour lamp (color_temp / rgb / xy), so the colour button in the
# slider overlay appears automatically.
# ============================================================
# --- basic settings ---
TILE9_ENTITY: "light.unused_9" # main Home Assistant entity for this tile
TILE9_STATE_ENTITY: "light.unused_9" # entity used for active/inactive state styling
TILE9_TITLE: "Tile 9" # tile title shown in the UI
TILE9_ICON: "\U000F04B9" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py)
TILE9_TYPE: "light" # tile type: light | fan | switch | scene | script | cover | climate
TILE9_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset
TILE9_LONGPRESS: "none" # long press mode: none | slider | action | vacuum
TILE9_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text
TILE9_LABEL_OFF: "Off" # text shown when entity is off
TILE9_LABEL_ON: "On" # text shown for text-mode tiles when entity is on
TILE9_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on")
# --- colors ---
TILE9_CIRCLE_ACTIVE_COLOR: "${ACCENT_AUTO}" # light -> ACCENT_LIGHT (amber)
TILE9_CIRCLE_DISABLED_COLOR: "0x7B7B6F" # icon circle color when inactive
TILE9_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active
TILE9_ICON_DISABLED_COLOR: "${ACCENT_AUTO}" # icon color when inactive
TILE9_BG_ACTIVE_COLOR: "0xFFFFFF" # tile background color when active
TILE9_BG_DISABLED_COLOR: "0x939391" # tile background color when inactive
TILE9_TITLE_ACTIVE_COLOR: "0x000000" # title color when active
TILE9_TITLE_DISABLED_COLOR: "0xFFFFFF" # title color when inactive
TILE9_VALUE_ACTIVE_COLOR: "0x7A7A7C" # value color when active
TILE9_VALUE_DISABLED_COLOR: "0xD9D9D9" # value color when inactive
# --- advanced settings ---
TILE9_TAP_SERVICE: "" # optional service override, e.g. light.toggle
TILE9_TAP_PARAM_KEY: "" # optional extra service parameter key
TILE9_TAP_PARAM_VAL: "" # optional extra service parameter value
# if LONGPRESS = slider:
TILE9_LONGPRESS_SLIDER: "auto" # auto = brightness for a light
TILE9_LONGPRESS_OFF_VALUE: "0" # initial slider value when entity is off
# if LONGPRESS = action:
TILE9_LONGPRESS_ACTION: "" # entity_id to fire on long press
TILE9_LONGPRESS_ACTION_TYPE: "" # entity type (empty = same as TILE*_TYPE)
TILE9_LONGPRESS_ACTION_SERVICE: "" # service to call (default: activate/turn_on)
# ============================================================
# TILE 10 — Alarm on/off
# input_boolean.alarm_engaged. An input_boolean doesn't respond to
# switch.toggle, hence the explicit TAP_SERVICE.
# ============================================================
# --- basic settings ---
TILE10_ENTITY: "light.unused_10" # main Home Assistant entity for this tile
TILE10_STATE_ENTITY: "light.unused_10" # entity used for active/inactive state styling
TILE10_TITLE: "Tile 10" # tile title shown in the UI
TILE10_ICON: "\U000F002A" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py)
TILE10_TYPE: "light" # generic on/off type; the service below does the real work
TILE10_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset
TILE10_LONGPRESS: "none" # long press only sends the event to HA
TILE10_VALUE_MODE: "auto" # shows LABEL_ON / LABEL_OFF
TILE10_LABEL_OFF: "Off" # text shown when entity is off
TILE10_LABEL_ON: "On" # text shown when entity is on
TILE10_ACTIVE_STATE: "" # input_boolean just uses on/off
# --- colors ---
TILE10_CIRCLE_ACTIVE_COLOR: "${ACCENT_AUTO}" # input_boolean -> ACCENT_DEVICE (cyan)
TILE10_CIRCLE_DISABLED_COLOR: "0x7B7B6F" # icon circle color when inactive
TILE10_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active
TILE10_ICON_DISABLED_COLOR: "${ACCENT_AUTO}" # icon color when inactive
TILE10_BG_ACTIVE_COLOR: "0xFFFFFF" # tile background color when active
TILE10_BG_DISABLED_COLOR: "0x939391" # tile background color when inactive
TILE10_TITLE_ACTIVE_COLOR: "0x000000" # title color when active
TILE10_TITLE_DISABLED_COLOR: "0xFFFFFF" # title color when inactive
TILE10_VALUE_ACTIVE_COLOR: "0x7A7A7C" # value color when active
TILE10_VALUE_DISABLED_COLOR: "0xD9D9D9" # value color when inactive
# --- advanced settings ---
TILE10_TAP_SERVICE: "" # switch.toggle doesn't work on an input_boolean
TILE10_TAP_PARAM_KEY: "" # optional extra service parameter key
TILE10_TAP_PARAM_VAL: "" # optional extra service parameter value
# if LONGPRESS = slider:
TILE10_LONGPRESS_SLIDER: "auto" # not in use (LONGPRESS = none)
TILE10_LONGPRESS_OFF_VALUE: "0" # not in use
# if LONGPRESS = action:
TILE10_LONGPRESS_ACTION: "" # entity_id to fire on long press
TILE10_LONGPRESS_ACTION_TYPE: "" # entity type (empty = same as TILE*_TYPE)
TILE10_LONGPRESS_ACTION_SERVICE: "" # service to call (default: activate/turn_on)
# ============================================================
# ORIENTATION / LAYOUT
# Uncomment ONE preset block below, comment out the others.
# Touch calibration values (TOUCH_CAL_*) are device-specific —
# portrait/flipped values are approximate and may need tuning.
#
# LVGL_ROTATION (0/90/180/270) drives the rotation — LVGL rotates both the
# display output and the touch coordinates (ESPHome 2026.4+). DISPLAY_W/H are
# the *logical* (post-rotation) canvas size used by the tile layout below;
# the physical panel is always driven at its native 240×320 (see display:).
#
# Tile content layout:
# Landscape — icon left, title + value to its right
# Portrait — icon centered on top, title + value stacked below
# ============================================================
# ----- 0° — Landscape 320×240 (default, USB on right) -----
ORIENTATION: "0"
DISPLAY_W: "320"
DISPLAY_H: "240"
LVGL_ROTATION: "90"
TOUCH_SWAP_XY: "false"
# true on this board (resistive XPT2046) — was false on the 032c board
# (capacitive GT911). This is the original upstream value for this panel.
TOUCH_MIRROR_X: "true"
TOUCH_MIRROR_Y: "false"
# Measured on this board: median top-right raw=(523,3390),
# bottom-left raw=(3487,482), at screen (299,20) and (20,219).
# Extrapolated to panel edges; verify with the isolated corner test.
TOUCH_CAL_X_MIN: "280"
TOUCH_CAL_X_MAX: "3860"
TOUCH_CAL_Y_MIN: "340"
TOUCH_CAL_Y_MAX: "3860"
GRID_COL1_X: "9"
GRID_COL2_X: "164"
GRID_ROW1_Y: "0"
GRID_ROW2_Y: "56"
GRID_ROW3_Y: "112"
GRID_ROW4_Y: "168"
GRID_ROW5_Y: "224"
SCROLL_Y: "40"
SCROLL_H: "164"
TILE_W: "147"
TILE_H: "52"
HEADER_Y: "14"
OVERLAY_SLIDER_W: "96"
OVERLAY_SLIDER_H: "160"
OVERLAY_ICON_Y: "74"
# Alert overlay (HA action show_alert), landscape 320x240: one card centred on LVGL's top layer.
ALERT_CARD_W: "292"
ALERT_CARD_H: "196"
ALERT_ICON_X: "18"
ALERT_ICON_Y: "16"
ALERT_TEXT_X: "62"
ALERT_TEXT_W: "212"
ALERT_TITLE_Y: "16"
ALERT_SUBTITLE_Y: "44"
ALERT_SUBTITLE_H: "88"
ALERT_BUTTON_W: "100"
ALERT_BUTTON_H: "40"
ALERT_BUTTON_INSET: "14"
ALERT_TITLE_MAX: "48" # bytes; the label ends one line with an ellipsis
ALERT_BUTTON_MAX: "12" # bytes on the OK button
ALERT_SUBTITLE_MAX: "160" # bytes; wraps over the rows the card has
BG_TOP_COLOR: "0xE7E7E7" # light background, matching the Guition
BG_BOTTOM_COLOR: "0xE7E7E7"
TILE_ICON_X: "0"
TILE_ICON_Y: "0"
TILE_TITLE_X: "48"
TILE_TITLE_Y: "4"
TILE_VALUE_X: "48"
TILE_VALUE_Y: "18"
# ----- 90° — Portrait 240×320 (USB on bottom) -----
# ORIENTATION: "90"
# DISPLAY_W: "240"
# DISPLAY_H: "320"
# LVGL_ROTATION: "180"
# TOUCH_SWAP_XY: "false"
# TOUCH_MIRROR_X: "true"
# TOUCH_MIRROR_Y: "false"
# TOUCH_CAL_X_MIN: "280"
# TOUCH_CAL_X_MAX: "3860"
# TOUCH_CAL_Y_MIN: "340"
# TOUCH_CAL_Y_MAX: "3860"
# GRID_COL1_X: "6"
# GRID_COL2_X: "123"
# GRID_ROW1_Y: "8"
# GRID_ROW2_Y: "96"
# GRID_ROW3_Y: "184"
# GRID_ROW4_Y: "272"
# GRID_ROW5_Y: "360"
# SCROLL_Y: "44"
# SCROLL_H: "276"
# TILE_W: "111"
# TILE_H: "80"
# HEADER_Y: "14"
# OVERLAY_SLIDER_W: "96"
# OVERLAY_SLIDER_H: "200"
# OVERLAY_ICON_Y: "94"
# BG_TOP_COLOR: "0xAEC4D9" # background top (soft blue)
# BG_BOTTOM_COLOR: "0xD6BB9E" # background bottom (warm sand)
# TILE_ICON_X: "0"
# TILE_ICON_Y: "0"
# TILE_TITLE_X: "4"
# TILE_TITLE_Y: "38"
# TILE_VALUE_X: "4"
# TILE_VALUE_Y: "52"
# ----- 180° — Landscape flipped 320×240 (USB on left) -----
# ORIENTATION: "180"
# DISPLAY_W: "320"
# DISPLAY_H: "240"
# LVGL_ROTATION: "270"
# TOUCH_SWAP_XY: "false"
# TOUCH_MIRROR_X: "true"
# TOUCH_MIRROR_Y: "false"
# TOUCH_CAL_X_MIN: "280"
# TOUCH_CAL_X_MAX: "3860"
# TOUCH_CAL_Y_MIN: "340"
# TOUCH_CAL_Y_MAX: "3860"
# GRID_COL1_X: "9"
# GRID_COL2_X: "164"
# GRID_ROW1_Y: "8"
# GRID_ROW2_Y: "68"
# GRID_ROW3_Y: "128"
# GRID_ROW4_Y: "188"
# GRID_ROW5_Y: "248"
# SCROLL_Y: "44"
# SCROLL_H: "196"
# TILE_W: "147"
# TILE_H: "52"
# HEADER_Y: "14"
# OVERLAY_SLIDER_W: "96"
# OVERLAY_SLIDER_H: "160"
# OVERLAY_ICON_Y: "74"
# BG_TOP_COLOR: "0xAEC4D9" # background top (soft blue)
# BG_BOTTOM_COLOR: "0xD6BB9E" # background bottom (warm sand)
# TILE_ICON_X: "0"
# TILE_ICON_Y: "0"
# TILE_TITLE_X: "48"
# TILE_TITLE_Y: "4"
# TILE_VALUE_X: "48"
# TILE_VALUE_Y: "18"
# ----- 270° — Portrait flipped 240×320 (USB on top) -----
# ORIENTATION: "270"
# DISPLAY_W: "240"
# DISPLAY_H: "320"
# LVGL_ROTATION: "0"
# TOUCH_SWAP_XY: "false"
# TOUCH_MIRROR_X: "true"
# TOUCH_MIRROR_Y: "false"
# TOUCH_CAL_X_MIN: "280"
# TOUCH_CAL_X_MAX: "3860"
# TOUCH_CAL_Y_MIN: "340"
# TOUCH_CAL_Y_MAX: "3860"
# GRID_COL1_X: "6"
# GRID_COL2_X: "123"
# GRID_ROW1_Y: "8"
# GRID_ROW2_Y: "96"
# GRID_ROW3_Y: "184"
# GRID_ROW4_Y: "272"
# GRID_ROW5_Y: "360"
# SCROLL_Y: "44"
# SCROLL_H: "276"
# TILE_W: "111"
# TILE_H: "80"
# HEADER_Y: "14"
# OVERLAY_SLIDER_W: "96"
# OVERLAY_SLIDER_H: "200"
# OVERLAY_ICON_Y: "94"
# BG_TOP_COLOR: "0xAEC4D9" # background top (soft blue)
# BG_BOTTOM_COLOR: "0xD6BB9E" # background bottom (warm sand)
# TILE_ICON_X: "0"
# TILE_ICON_Y: "0"
# TILE_TITLE_X: "4"
# TILE_TITLE_Y: "38"
# TILE_VALUE_X: "4"
# TILE_VALUE_Y: "52"
# ============================================================
# No need to edit below this line.
# All configuration is done via the substitutions above.
# Credentials are managed in secrets.yaml (see secrets.yaml.example).
# The Material Design Icons font is included at fonts/materialdesignicons-webfont.ttf (Apache 2.0).
# Source: https://github.com/Templarian/MaterialDesign-Webfont
# ============================================================
external_components:
- source:
type: local
path: components
components: [xpt2046, smart_display]
esphome:
min_version: 2026.6.2
project:
name: maxgramser.esp_screen
version: "${SCREEN_FIRMWARE_VERSION}"
platformio_options:
build_flags:
- -DLV_USE_SWITCH=1
includes:
- cyd_ui.h
- light_controls.h
- alert_overlay.h
name: ${DEVICE_NAME}
friendly_name: ${DEVICE_FRIENDLY_NAME}
on_boot:
priority: -100
then:
- lambda: |-
// Per-panel affine correction; shared defaults are neutral.
id(ts_touch).set_raw_correction(${TOUCH_AFFINE_XX}f, ${TOUCH_AFFINE_XY}f, ${TOUCH_AFFINE_XC}f, ${TOUCH_AFFINE_YX}f, ${TOUCH_AFFINE_YY}f, ${TOUCH_AFFINE_YC}f);
cyd::touch_guard.configure(${TOUCH_MOVE_LIMIT_PX}, ${TOUCH_MIN_PRESS_MS});
id(last_touch_ms) = millis();
id(display_dimmed) = false;
lv_obj_set_style_pad_row(id(vacuum_actions), 4, 0);
lv_obj_set_style_pad_column(id(vacuum_actions), 4, 0);
lv_obj_set_style_pad_row(id(vacuum_speeds), 4, 0);
lv_obj_set_style_pad_column(id(vacuum_speeds), 4, 0);
- 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::mini_icon_font = id(materialdesign_icons_mini)->get_lv_font();
runtime_tiles::control_font = id(sublabel_big)->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 = std::string("${DYNAMIC_TILES}") == "true";
if (!runtime_tiles::enabled) return;
runtime_tiles::load_settings();
runtime_tiles::settings_changed = []() { id(apply_screen_settings).execute(); };
runtime_tiles::dismiss = []() {
runtime_tiles::hide_detail();
id(active_entity).clear();
lv_obj_add_flag(id(brightness_overlay), LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(id(color_detail_overlay), LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(id(climate_detail_overlay), LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(id(climate_mode_overlay), LV_OBJ_FLAG_HIDDEN);
lv_obj_add_flag(id(vacuum_overlay), LV_OBJ_FLAG_HIDDEN);
};
runtime_tiles::layout_changed = []() { id(show_tile_page).execute(); };
lv_obj_add_event_cb(id(home_page)->obj, [](lv_event_t *event) {
if(!runtime_tiles::enabled || !runtime_tiles::swipe_pages || id(display_dimmed) || id(calibration_active) ||
!id(active_entity).empty() || (runtime_tiles::detail_root && !lv_obj_has_flag(runtime_tiles::detail_root,LV_OBJ_FLAG_HIDDEN)))return;
auto *input=lv_indev_active();if(!input)return;
auto direction=lv_indev_get_gesture_dir(input);
if(direction!=LV_DIR_LEFT && direction!=LV_DIR_RIGHT)return;
cyd::touch_guard.consume();
lv_indev_wait_release(input);
id(tile_page)+=direction==LV_DIR_LEFT?1:-1;
id(show_tile_page).execute();
}, LV_EVENT_GESTURE, nullptr);
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) {
if (t.domain() == "vacuum") {
lv_obj_t *buttons[] = {id(vacspd_quiet), id(vacspd_balanced), id(vacspd_turbo), id(vacspd_max)};
for (unsigned i = 0; i < 4; ++i) {
if (i < t.fan_speed_count) {
lv_obj_clear_flag(buttons[i], LV_OBJ_FLAG_HIDDEN);
lv_label_set_text(lv_obj_get_child(buttons[i], 0), t.fan_speeds[i].c_str());
} else lv_obj_add_flag(buttons[i], LV_OBJ_FLAG_HIDDEN);
}
id(open_vacuum_overlay).execute(t.name, t.entity);
return;
}
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.fan_modes; id(active_climate_fan_mode) = t.fan_mode;
id(active_climate_swing_modes) = t.swing_modes; id(active_climate_swing_mode) = t.swing_mode;
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),
0xD5EEFC, 0x00A6ED, 0x1B1B1B, "Off", "On", t.modes,
t.current, t.target, t.humidity, t.state, t.hvac_modes,
std::to_string(t.minimum), std::to_string(t.maximum), std::to_string(t.step));
};
runtime_tiles::detail_update = [](runtime_tiles::Tile &t) {
if (id(active_entity) != t.entity || id(touch_down)) return;
if (t.domain() == "climate" && !lv_obj_has_flag(id(climate_detail_overlay), LV_OBJ_FLAG_HIDDEN)) {
if (id(climate_edit_pending) && millis() - id(climate_edit_ms) < 3000) return;
id(climate_edit_pending) = false;
id(active_climate_mode) = t.state;
id(active_climate_supported_modes) = t.hvac_modes;
lv_label_set_text(id(climate_mode_label), tile_controls::climate_mode_text(t.state));
id(climate_power_refresh).execute();
id(active_climate_fan_modes) = t.fan_modes; id(active_climate_fan_mode) = t.fan_mode;
id(active_climate_swing_modes) = t.swing_modes; id(active_climate_swing_mode) = t.swing_mode;
char buf[72];
if (std::isfinite(t.current)) snprintf(buf, sizeof(buf), "Now: %.1f°", t.current);
else snprintf(buf, sizeof(buf), "Now: --");
lv_label_set_text(id(climate_current_label), buf);
if (std::isfinite(t.target)) {
int target = 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_x10) = target;
snprintf(buf, sizeof(buf), "%.1f°", target / 10.0f);
lv_label_set_text(id(climate_target_label), buf);
lv_arc_set_value(id(climate_arc), target);
}
}
};
- lambda: |-
static const char *entities[] = {"${TILE1_ENTITY}", "${TILE2_ENTITY}", "${TILE3_ENTITY}", "${TILE4_ENTITY}", "${TILE5_ENTITY}", "${TILE6_ENTITY}", "${TILE7_ENTITY}", "${TILE9_ENTITY}"};
if (!runtime_tiles::enabled) light_controls::subscribe(entities, 8);
light_controls::setup(id(color_detail_overlay), id(sublabel)->get_lv_font(), atoi("${DISPLAY_W}"), atoi("${DISPLAY_H}"));
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); };
- lambda: |-
if (!runtime_tiles::enabled) return;
screen_calibration::apply = [](const screen_calibration::Calibration &cal) {
id(ts_touch).set_raw_correction(cal.c[0],cal.c[1],cal.c[2],cal.c[3],cal.c[4],cal.c[5]);
};
screen_calibration::isolation = [](bool active) { id(calibration_active) = active; };
screen_calibration::setup(id(home_page)->obj, id(sublabel)->get_lv_font(), id(headline)->get_lv_font(),
${TOUCH_CAL_X_MIN}, ${TOUCH_CAL_X_MAX}, ${TOUCH_CAL_Y_MIN}, ${TOUCH_CAL_Y_MAX});
- 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", "USB calibration ready; no tile actions");
esp32:
board: esp32dev
framework:
type: esp-idf
advanced:
# ESPHome recommends this itself in the boot log. Yields ~40KB extra IRAM
# and a smaller binary, and every KB counts on this board.
sram1_as_iram: true
# minimum_chip_revision deliberately left out: that was here for the
# 032c board (rev 3.1 confirmed), but this is a different physical board
# with an unknown chip revision. With an unjustifiably high minimum
# the firmware simply refuses to boot.
version: recommended
variant: esp32
logger:
# Set to DEBUG or VERBOSE temporarily for diagnosis. Note: ESPHome logs
# its configuration lines at CONFIG level, which are invisible at INFO.
level: DEBUG
logs:
sensor: WARN
text_sensor: WARN
xpt2046: WARN
homeassistant.sensor: WARN
homeassistant.text_sensor: WARN
# Credentials are loaded from secrets.yaml — copy secrets.yaml.example to secrets.yaml and fill in your values.
api:
actions:
- action: 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 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)), 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));
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, covers and climate open the board's own card, like a long press does.
if ((domain == "light" || domain == "fan" || domain == "cover" || domain == "climate") && runtime_tiles::detail) {
tile.begin(millis(), true);
runtime_tiles::detail(tile);
return;
}
runtime_tiles::show_detail(index);
}
# 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.
- action: screen_message
variables:
message: string
then:
- lambda: 'id(dashboard_inbox).publish_state(runtime_tiles::receive(message));'
encryption:
key: !secret api_encryption_key
ota:
- platform: esphome
password: !secret ota_password
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# Mains-powered panel: no modem sleep, so HA updates and OTA arrive without beacon latency.
power_save_mode: none
ap:
ssid: "Smartdisplay Fallback Hotspot"
password: !secret ap_password
captive_portal:
interval:
- interval: 250ms
then:
- lambda: runtime_tiles::tick();
- interval: 1s
then:
- if:
condition:
lambda: 'return id(climate_edit_pending) && !id(touch_down) && millis() - id(climate_edit_ms) >= 3000;'
then:
- lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();'
- 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
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
- 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
- id: last_touch_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'
- 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
font:
- file: "fonts/Roboto-500.ttf"
id: time_label
size: 18
bpp: 4
glyphs: ['0','1','2','3','4','5','6','7','8','9',':',' ', 'A', 'M', 'P']
- file: "fonts/Roboto-500.ttf"
id: headline
size: 18
bpp: 4
glyphs: ['<','>','—','&','@','!',',','.','?','"','%','(',')','+','-','_',':','°',
'0','1','2','3','4','5','6','7','8','9',
'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z',
' ','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z',
'ä','ö','ü','Ä','Ö','Ü','ß','/','·']
- file: "fonts/Roboto-500.ttf"
id: watch_value
size: 22
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: "fonts/Roboto-500.ttf"
id: clock_digits
size: 24
bpp: 4
glyphs: [':','-','0','1','2','3','4','5','6','7','8','9']
- file: "fonts/Roboto-700.ttf"
id: label
size: 11
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: "fonts/Roboto-400.ttf"
id: sublabel
size: 11
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: "fonts/Roboto-400.ttf"
id: sublabel_big
size: 14
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: 'fonts/materialdesignicons-webfont.ttf'
id: materialdesign_icons
size: 28
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
- "\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
- file: 'fonts/materialdesignicons-webfont.ttf'
id: materialdesign_icons_mini
size: 18
glyphs: *tile_icons
- file: 'fonts/materialdesignicons-webfont.ttf'
id: watch_icon
size: 12
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);'
- 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
- platform: homeassistant
id: tile1_brightness
entity_id: ${TILE1_ENTITY}
attribute: brightness
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile1_percentage
entity_id: ${TILE1_ENTITY}
attribute: percentage
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile2_brightness
entity_id: ${TILE2_ENTITY}
attribute: brightness
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile2_percentage
entity_id: ${TILE2_ENTITY}
attribute: percentage
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile3_brightness
entity_id: ${TILE3_ENTITY}
attribute: brightness
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile3_percentage
entity_id: ${TILE3_ENTITY}
attribute: percentage
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile4_brightness
entity_id: ${TILE4_ENTITY}
attribute: brightness
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile4_percentage
entity_id: ${TILE4_ENTITY}
attribute: percentage
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile5_brightness
entity_id: ${TILE5_ENTITY}
attribute: brightness
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile5_percentage
entity_id: ${TILE5_ENTITY}
attribute: percentage
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile6_brightness
entity_id: ${TILE6_ENTITY}
attribute: brightness
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile6_percentage
entity_id: ${TILE6_ENTITY}
attribute: percentage
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
# Tile 7 (Table lamp) is a brightness-only light: just brightness.
# Deliberately no percentage / cover_position / climate sensors — every
# HA subscription costs heap on a board without PSRAM.
- platform: homeassistant
id: tile7_brightness
entity_id: ${TILE7_ENTITY}
attribute: brightness
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
# Tile 9 (Living room) is a colour lamp: brightness + supported_color_modes.
# No percentage / cover / climate — this tile never reads those.
- platform: homeassistant
id: tile9_brightness
entity_id: ${TILE9_ENTITY}
attribute: brightness
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile1_cover_position
entity_id: ${TILE1_ENTITY}
attribute: current_position
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile2_cover_position
entity_id: ${TILE2_ENTITY}
attribute: current_position
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile3_cover_position
entity_id: ${TILE3_ENTITY}
attribute: current_position
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile4_cover_position
entity_id: ${TILE4_ENTITY}
attribute: current_position
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile5_cover_position
entity_id: ${TILE5_ENTITY}
attribute: current_position
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile6_cover_position
entity_id: ${TILE6_ENTITY}
attribute: current_position
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile1_climate_current_temperature
entity_id: ${TILE1_ENTITY}
attribute: ${TILE1_CLIMATE_CURRENT_TEMP_ATTRIBUTE}
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile1_climate_target_temperature
entity_id: ${TILE1_ENTITY}
attribute: ${TILE1_CLIMATE_TARGET_TEMP_ATTRIBUTE}
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile1_climate_humidity
entity_id: ${TILE1_ENTITY}
attribute: ${TILE1_CLIMATE_HUMIDITY_ATTRIBUTE}
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile2_climate_current_temperature
entity_id: ${TILE2_ENTITY}
attribute: ${TILE2_CLIMATE_CURRENT_TEMP_ATTRIBUTE}
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile2_climate_target_temperature
entity_id: ${TILE2_ENTITY}
attribute: ${TILE2_CLIMATE_TARGET_TEMP_ATTRIBUTE}
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile2_climate_humidity
entity_id: ${TILE2_ENTITY}
attribute: ${TILE2_CLIMATE_HUMIDITY_ATTRIBUTE}
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile3_climate_current_temperature
entity_id: ${TILE3_ENTITY}
attribute: ${TILE3_CLIMATE_CURRENT_TEMP_ATTRIBUTE}
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile3_climate_target_temperature
entity_id: ${TILE3_ENTITY}
attribute: ${TILE3_CLIMATE_TARGET_TEMP_ATTRIBUTE}
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile3_climate_humidity
entity_id: ${TILE3_ENTITY}
attribute: ${TILE3_CLIMATE_HUMIDITY_ATTRIBUTE}
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile4_climate_current_temperature
entity_id: ${TILE4_ENTITY}
attribute: ${TILE4_CLIMATE_CURRENT_TEMP_ATTRIBUTE}
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile4_climate_target_temperature
entity_id: ${TILE4_ENTITY}
attribute: ${TILE4_CLIMATE_TARGET_TEMP_ATTRIBUTE}
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile4_climate_humidity
entity_id: ${TILE4_ENTITY}
attribute: ${TILE4_CLIMATE_HUMIDITY_ATTRIBUTE}
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile5_climate_current_temperature
entity_id: ${TILE5_ENTITY}
attribute: ${TILE5_CLIMATE_CURRENT_TEMP_ATTRIBUTE}
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile5_climate_target_temperature
entity_id: ${TILE5_ENTITY}
attribute: ${TILE5_CLIMATE_TARGET_TEMP_ATTRIBUTE}
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile5_climate_humidity
entity_id: ${TILE5_ENTITY}
attribute: ${TILE5_CLIMATE_HUMIDITY_ATTRIBUTE}
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile6_climate_current_temperature
entity_id: ${TILE6_ENTITY}
attribute: ${TILE6_CLIMATE_CURRENT_TEMP_ATTRIBUTE}
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile6_climate_target_temperature
entity_id: ${TILE6_ENTITY}
attribute: ${TILE6_CLIMATE_TARGET_TEMP_ATTRIBUTE}
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile6_climate_humidity
entity_id: ${TILE6_ENTITY}
attribute: ${TILE6_CLIMATE_HUMIDITY_ATTRIBUTE}
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
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
# 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:
name: "IP address"
entity_category: diagnostic
- platform: template
id: smartdisplay_action
name: "SmartDisplay Action"
icon: mdi:gesture-tap-button
- platform: homeassistant
id: ha_state_tile1
entity_id: ${TILE1_STATE_ENTITY}
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: ha_state_tile2
entity_id: ${TILE2_STATE_ENTITY}
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: ha_state_tile3
entity_id: ${TILE3_STATE_ENTITY}
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: ha_state_tile4
entity_id: ${TILE4_STATE_ENTITY}
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: ha_state_tile5
entity_id: ${TILE5_STATE_ENTITY}
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: ha_state_tile6
entity_id: ${TILE6_STATE_ENTITY}
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: ha_state_tile7
entity_id: ${TILE7_STATE_ENTITY}
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: ha_state_tile8
entity_id: ${TILE8_STATE_ENTITY}
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: ha_state_tile9
entity_id: ${TILE9_STATE_ENTITY}
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: ha_state_tile10
entity_id: ${TILE10_STATE_ENTITY}
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
# Living room reports color_temp/rgb/xy, so the colour button does appear here.
- platform: homeassistant
id: tile9_supported_color_modes
entity_id: ${TILE9_ENTITY}
attribute: supported_color_modes
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
# Used by the slider overlay to decide whether the colour button is needed.
# This lamp reports ['brightness'], so the button stays hidden automatically.
- platform: homeassistant
id: tile7_supported_color_modes
entity_id: ${TILE7_ENTITY}
attribute: supported_color_modes
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile1_preset
entity_id: ${TILE1_ENTITY}
attribute: preset_mode
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile2_preset
entity_id: ${TILE2_ENTITY}
attribute: preset_mode
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile3_preset
entity_id: ${TILE3_ENTITY}
attribute: preset_mode
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile4_preset
entity_id: ${TILE4_ENTITY}
attribute: preset_mode
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile5_preset
entity_id: ${TILE5_ENTITY}
attribute: preset_mode
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile6_preset
entity_id: ${TILE6_ENTITY}
attribute: preset_mode
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile1_supported_color_modes
entity_id: ${TILE1_ENTITY}
attribute: supported_color_modes
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile2_supported_color_modes
entity_id: ${TILE2_ENTITY}
attribute: supported_color_modes
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile3_supported_color_modes
entity_id: ${TILE3_ENTITY}
attribute: supported_color_modes
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile4_supported_color_modes
entity_id: ${TILE4_ENTITY}
attribute: supported_color_modes
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile5_supported_color_modes
entity_id: ${TILE5_ENTITY}
attribute: supported_color_modes
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile6_supported_color_modes
entity_id: ${TILE6_ENTITY}
attribute: supported_color_modes
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile1_climate_mode
entity_id: ${TILE1_ENTITY}
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile2_climate_mode
entity_id: ${TILE2_ENTITY}
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile3_climate_mode
entity_id: ${TILE3_ENTITY}
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile4_climate_mode
entity_id: ${TILE4_ENTITY}
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile5_climate_mode
entity_id: ${TILE5_ENTITY}
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile6_climate_mode
entity_id: ${TILE6_ENTITY}
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile1_climate_supported_modes
entity_id: ${TILE1_ENTITY}
attribute: hvac_modes
- platform: homeassistant
id: tile2_climate_supported_modes
entity_id: ${TILE2_ENTITY}
attribute: hvac_modes
- platform: homeassistant
id: tile3_climate_supported_modes
entity_id: ${TILE3_ENTITY}
attribute: hvac_modes
- platform: homeassistant
id: tile4_climate_supported_modes
entity_id: ${TILE4_ENTITY}
attribute: hvac_modes
- platform: homeassistant
id: tile5_climate_supported_modes
entity_id: ${TILE5_ENTITY}
attribute: hvac_modes
- platform: homeassistant
id: tile6_climate_supported_modes
entity_id: ${TILE6_ENTITY}
attribute: hvac_modes
- platform: homeassistant
id: tile1_cover_state
entity_id: ${TILE1_STATE_ENTITY}
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile2_cover_state
entity_id: ${TILE2_STATE_ENTITY}
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile3_cover_state
entity_id: ${TILE3_STATE_ENTITY}
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile4_cover_state
entity_id: ${TILE4_STATE_ENTITY}
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile5_cover_state
entity_id: ${TILE5_STATE_ENTITY}
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
- platform: homeassistant
id: tile6_cover_state
entity_id: ${TILE6_STATE_ENTITY}
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
switch:
- platform: restart
name: "Restart"
# Two SPI buses used by the CYD:
# - lcd: built-in display
# - my_touchscreen: built-in touch controller
# Two SPI buses on the 2432S028: one for the display, a separate one for the
# resistive XPT2046 touch (no I2C on this board, unlike the 032c).
spi:
- id: lcd
clk_pin: GPIO14
mosi_pin: GPIO13
- id: my_touchscreen
clk_pin: GPIO25
mosi_pin: GPIO32
miso_pin: GPIO39
output:
- platform: ledc
pin: GPIO21
id: gpio_backlight_pwm
# ESPHome's ledc default is 1kHz. You don't see that while still (well
# above what the eye perceives as flicker), but moving the screen reveals
# motion blur turning the on/off pulses into streaks (the same effect as
# a PWM LED that "judders" when you move it). 20kHz sits well above the
# speed at which motion still makes that visible, and still leaves ample
# duty-cycle resolution left over for smooth dimming.
frequency: 20000Hz
- platform: ledc
id: output_red
pin: GPIO4
inverted: true
- platform: ledc
id: output_green
pin: GPIO16
inverted: true
- platform: ledc
id: output_blue
pin: GPIO17
inverted: true
light:
- platform: monochromatic
output: gpio_backlight_pwm
name: "Power Display Backlight"
id: back_light
restore_mode: ALWAYS_ON
- platform: rgb
name: LED
red: output_red
id: led
green: output_green
blue: output_blue
restore_mode: ALWAYS_OFF
# Touchscreen configuration (XPT2046, resistive, via SPI)
# XPT2046 calibration is board-specific. Poll at 50Hz; LVGL rotates touch
# with the display. GPIO36 IRQ is deliberately unused (driver requests a
# pull-up that this input-only pin cannot provide).
touchscreen:
platform: xpt2046
id: ts_touch
spi_id: my_touchscreen
cs_pin: 33
# Polling avoids GPIO36 pull-up errors and noisy PENIRQ edges.
update_interval: 20ms
# Diagnosis caught a false corner contact at pressure 422. Deliberate
# contacts measured 936-2643; reject the low-pressure tail on release.
threshold: 800
calibration:
x_min: ${TOUCH_CAL_X_MIN}
x_max: ${TOUCH_CAL_X_MAX}
y_min: ${TOUCH_CAL_Y_MIN}
y_max: ${TOUCH_CAL_Y_MAX}
transform:
swap_xy: ${TOUCH_SWAP_XY}
mirror_x: ${TOUCH_MIRROR_X}
mirror_y: ${TOUCH_MIRROR_Y}
on_touch:
- lambda: |-
id(last_touch_ms) = millis();
id(touch_down) = true;
cyd::touch_guard.begin(millis(), touch.x, touch.y, touch.id);
screen_calibration::press();
ESP_LOGI("touch", "press native=%u,%u raw=%d,%d pressure=%d calibration=%d", touch.x, touch.y, id(ts_touch).filtered_raw_x(), id(ts_touch).filtered_raw_y(), touch.z_raw, id(calibration_active));
on_update:
- lambda: |-
id(last_touch_ms) = millis();
// Only the contact that started the touch counts; the guard ignores other ids.
for (const auto &point : touches) {
cyd::touch_guard.update(point.x, point.y, point.id);
}
on_release:
- lambda: |-
id(last_touch_ms) = millis();
id(touch_down) = false;
screen_calibration::release(id(ts_touch).filtered_raw_x(), id(ts_touch).filtered_raw_y());
# Display configuration (ILI9341)
display:
- platform: mipi_spi
id: my_display
spi_id: lcd
# Standard single-Micro-USB CYD; 40MHz matches ESPHome's board preset.
model: ILI9341
data_rate: 40MHz
cs_pin: 15
dc_pin: 2
invert_colors: false
update_interval: never
auto_clear_enabled: false
# Native panel dimensions (portrait). Do NOT swap these per orientation and
# do NOT add a display transform — rotation is handled by LVGL below.
# ESPHome 2026.6+ rejects a swapped transform here with "Invalid offsets".
dimensions:
width: 240
height: 320
lvgl:
on_draw_start:
- lambda: 'id(rendered_frames)++;'
displays:
- my_display
touchscreens:
- touchscreen_id: ts_touch
# The ESP32-WROOM-32 has no PSRAM; a full framebuffer (320x240x2
# = 153600 bytes) doesn't fit in DRAM. 25% is the usual value.
# 12% instead of 25%: frees up ~20KB of extra contiguous memory,
# and LVGL scales the strips of an ARGB layer along with the available
# drawing space, so the requested allocations get smaller too.
buffer_size: 12%
# 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` at widget level forces LVGL to render the
# element to a separate layer, and that layer costs tens of KB's
# per pressed widget. That ran the memory out here.
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:
# Background: two-stop vertical gradient instead of the 320x240
# RGB565 wallpaper. Saves ~150 KB flash and an image blit per redraw,
# which you can see directly in how smoothly it scrolls.
- id: style_bg_grad
bg_color: ${BG_TOP_COLOR}
bg_grad_color: ${BG_BOTTOM_COLOR}
bg_grad_dir: VER
bg_opa: COVER
border_width: 0
radius: 0
- id: style_tile
bg_color: 0xFFFFFF
bg_opa: COVER
radius: 18
border_width: 1
border_color: 0xDDDDDD
border_opa: COVER
pad_left: 8
pad_right: 8
pad_top: 8
pad_bottom: 8
shadow_width: 0
shadow_opa: 40%
- id: style_tile_disabled
bg_color: 0xFFFFFF
bg_opa: COVER
radius: 18
border_width: 1
border_color: 0xDDDDDD
border_opa: COVER
pad_left: 8
pad_right: 8
pad_top: 8
pad_bottom: 8
shadow_width: 0
shadow_opa: 25%
- id: style_icon_circle
bg_color: 0xD5EEFC
bg_opa: COVER
radius: 999
border_width: 0
- id: style_icon_circle_disabled
bg_color: 0xEEEEEE
bg_opa: COVER
radius: 999
border_width: 0
- id: style_room
text_font: headline
text_color: 0x1B1B1B
text_opa: 100%
- id: style_time
text_font: time_label
text_color: 0x1B1B1B
text_opa: 100%
- id: style_title
text_font: label
text_color: 0x000000
text_opa: 100%
- id: style_value
text_font: sublabel
text_color: 0x7A7A7C
text_opa: 100%
- id: style_value_big
text_font: sublabel_big
text_color: 0x1B1B1B
text_opa: 80%
- id: style_title_disabled
text_font: label
text_color: 0x1B1B1B
text_opa: 100%
- id: style_value_disabled
text_font: sublabel
text_color: 0x1B1B1B
text_opa: 100%
# ---------- 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
bg_color: 0x101820
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}
bg_color: 0xFFFFFF
bg_opa: COVER
radius: 18
border_width: 1
border_color: 0xDDDDDD
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
text_color: 0x1B1B1B
clickable: false
- label:
id: alert_title
x: ${ALERT_TEXT_X}
y: ${ALERT_TITLE_Y}
width: ${ALERT_TEXT_W}
long_mode: DOT
text: "Notification"
text_font: headline
text_color: 0x1B1B1B
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
text_color: 0x3A3A3C
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
bg_color: 0x1B1B1B
bg_opa: COVER
border_width: 0
shadow_width: 0
pad_all: 0
clickable: true
scrollable: false
pressed:
bg_color: 0x3A3A3C
bg_opa: COVER
on_click:
- if:
condition:
lambda: 'return cyd::touch_guard.accept(millis(), 13);'
then:
- script.execute:
id: alert_dismiss
reason: "ok"
widgets:
- label:
id: alert_ok_label
align: CENTER
text: "OK"
text_font: headline
text_color: 0xFFFFFF
clickable: false
pages:
- id: home_page
styles: style_bg_grad
pad_all: 0
scrollable: false
scrollbar_mode: 'OFF'
widgets:
# Never shown: it only makes ESPHome compile LVGL's spinner, which the busy sheet on a tile draws.
- spinner:
id: busy_spinner_seed
hidden: true
width: 2
height: 2
spin_time: 1s
arc_length: 60deg
- label:
id: lbl_room
x: 11
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: -11
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
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: 36
height: 36
styles: style_icon_circle
clickable: false
scrollable: false
scrollbar_mode: 'OFF'
widgets:
- label:
id: tile1_icon_lbl
align: CENTER
text_font: materialdesign_icons
text_color: 0xFFFFFF
text: "${TILE1_ICON}"
clickable: false
- label:
id: t1_title
x: ${TILE_TITLE_X}
y: ${TILE_TITLE_Y}
text: "${TILE1_TITLE}"
styles: style_title
clickable: false
- label:
id: t1_value
x: ${TILE_VALUE_X}
y: ${TILE_VALUE_Y}
text: "—"
styles: style_value
clickable: false
on_short_click:
- if:
condition:
lambda: 'return !runtime_tiles::enabled && cyd::touch_guard.accept(millis(), 1);'
then:
- script.execute: { id: publish_action, action: "tile1_press" }
- script.execute:
id: do_tile_action
tile_type: "${TILE1_TYPE}"
tap_action: "${TILE1_TAP_ACTION}"
service: "${TILE1_TAP_SERVICE}"
entity_id: "${TILE1_ENTITY}"
param_key: "${TILE1_TAP_PARAM_KEY}"
param_val: "${TILE1_TAP_PARAM_VAL}"
was_on: !lambda |-
if (std::string("${TILE1_TYPE}") == "cover")
return id(tile1_cover_state).state != "closed";
return std::string(id(ha_state_tile1).state.c_str()) == "on";
on_long_press:
- if:
condition:
lambda: 'return !runtime_tiles::enabled && cyd::touch_guard.accept(millis(), 1);'
then:
- script.execute: { id: publish_action, action: "tile1_long_press" }
- if:
condition:
lambda: "return std::string(\"${TILE1_LONGPRESS}\") == \"action\";"
then:
- script.execute:
id: do_tile_action
tile_type: !lambda |-
std::string t = "${TILE1_LONGPRESS_ACTION_TYPE}";
return t.empty() ? std::string("${TILE1_TYPE}") : t;
tap_action: "activate"
service: "${TILE1_LONGPRESS_ACTION_SERVICE}"
entity_id: "${TILE1_LONGPRESS_ACTION}"
param_key: ""
param_val: ""
was_on: false
else:
- if:
condition:
lambda: "return std::string(\"${TILE1_LONGPRESS}\") == \"slider\";"
then:
- script.execute:
id: open_value_overlay
title: "${TILE1_TITLE}"
entity: "${TILE1_ENTITY}"
tile_type: "${TILE1_TYPE}"
longpress_mode: "${TILE1_LONGPRESS_SLIDER}"
was_on: !lambda |-
if (std::string("${TILE1_TYPE}") == "cover")
return id(tile1_cover_state).state != "closed";
return std::string(id(ha_state_tile1).state.c_str()) == "on";
off_value: !lambda "return atoi(\"${TILE1_LONGPRESS_OFF_VALUE}\");"
brightness_value: !lambda "return id(tile1_brightness).state;"
percentage_value: !lambda "return id(tile1_percentage).state;"
position_value: !lambda "return id(tile1_cover_position).state;"
icon_text: "${TILE1_ICON}"
slider_bg_color: ${TILE1_BG_DISABLED_COLOR}
slider_fill_color: ${TILE1_CIRCLE_ACTIVE_COLOR}
slider_icon_color: ${TILE1_ICON_ACTIVE_COLOR}
label_off: "${TILE1_LABEL_OFF}"
label_on: "${TILE1_LABEL_ON}"
supported_color_modes: !lambda "return std::string(id(tile1_supported_color_modes).state.c_str());"
climate_current_temperature: !lambda "return id(tile1_climate_current_temperature).state;"
climate_target_temperature: !lambda "return id(tile1_climate_target_temperature).state;"
climate_humidity: !lambda "return id(tile1_climate_humidity).state;"
climate_mode: !lambda "return std::string(id(tile1_climate_mode).state.c_str());"
climate_supported_modes: !lambda "return std::string(id(tile1_climate_supported_modes).state.c_str());"
climate_min_temp: "${TILE1_CLIMATE_MIN_TEMP}"
climate_max_temp: "${TILE1_CLIMATE_MAX_TEMP}"
climate_step: "${TILE1_CLIMATE_STEP}"
# Long-press mode "vacuum": opens the vacuum controls.
# Falls outside the action/slider chain, so that stays unchanged.
- if:
condition:
lambda: "return std::string(\"${TILE1_LONGPRESS}\") == \"vacuum\";"
then:
- script.execute:
id: open_vacuum_overlay
title: "${TILE1_TITLE}"
entity: "${TILE1_ENTITY}"
# ---------- 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: 36
height: 36
styles: style_icon_circle
clickable: false
scrollable: false
scrollbar_mode: 'OFF'
widgets:
- label:
id: tile2_icon_lbl
align: CENTER
text_font: materialdesign_icons
text_color: 0xFFFFFF
text: "${TILE2_ICON}"
clickable: false
- label:
id: t2_title
x: ${TILE_TITLE_X}
y: ${TILE_TITLE_Y}
text: "${TILE2_TITLE}"
styles: style_title
clickable: false
- label:
id: t2_value
x: ${TILE_VALUE_X}
y: ${TILE_VALUE_Y}
text: "—"
styles: style_value
clickable: false
on_short_click:
- if:
condition:
lambda: 'return !runtime_tiles::enabled && cyd::touch_guard.accept(millis(), 2);'
then:
- script.execute: { id: publish_action, action: "tile2_press" }
- script.execute:
id: do_tile_action
tile_type: "${TILE2_TYPE}"
tap_action: "${TILE2_TAP_ACTION}"
service: "${TILE2_TAP_SERVICE}"
entity_id: "${TILE2_ENTITY}"
param_key: "${TILE2_TAP_PARAM_KEY}"
param_val: "${TILE2_TAP_PARAM_VAL}"
was_on: !lambda |-
if (std::string("${TILE2_TYPE}") == "cover")
return id(tile2_cover_state).state != "closed";
return std::string(id(ha_state_tile2).state.c_str()) == "on";
on_long_press:
- if:
condition:
lambda: 'return !runtime_tiles::enabled && cyd::touch_guard.accept(millis(), 2);'
then:
- script.execute: { id: publish_action, action: "tile2_long_press" }
- if:
condition:
lambda: "return std::string(\"${TILE2_LONGPRESS}\") == \"action\";"
then:
- script.execute:
id: do_tile_action
tile_type: !lambda |-
std::string t = "${TILE2_LONGPRESS_ACTION_TYPE}";
return t.empty() ? std::string("${TILE2_TYPE}") : t;
tap_action: "activate"
service: "${TILE2_LONGPRESS_ACTION_SERVICE}"
entity_id: "${TILE2_LONGPRESS_ACTION}"
param_key: ""
param_val: ""
was_on: false
else:
- if:
condition:
lambda: "return std::string(\"${TILE2_LONGPRESS}\") == \"slider\";"
then:
- script.execute:
id: open_value_overlay
title: "${TILE2_TITLE}"
entity: "${TILE2_ENTITY}"
tile_type: "${TILE2_TYPE}"
longpress_mode: "${TILE2_LONGPRESS_SLIDER}"
was_on: !lambda |-
if (std::string("${TILE2_TYPE}") == "cover")
return id(tile2_cover_state).state != "closed";
return std::string(id(ha_state_tile2).state.c_str()) == "on";
off_value: !lambda "return atoi(\"${TILE2_LONGPRESS_OFF_VALUE}\");"
brightness_value: !lambda "return id(tile2_brightness).state;"
percentage_value: !lambda "return id(tile2_percentage).state;"
position_value: !lambda "return id(tile2_cover_position).state;"
icon_text: "${TILE2_ICON}"
slider_bg_color: ${TILE2_BG_DISABLED_COLOR}
slider_fill_color: ${TILE2_CIRCLE_ACTIVE_COLOR}
slider_icon_color: ${TILE2_ICON_ACTIVE_COLOR}
label_off: "${TILE2_LABEL_OFF}"
label_on: "${TILE2_LABEL_ON}"
supported_color_modes: !lambda "return std::string(id(tile2_supported_color_modes).state.c_str());"
climate_current_temperature: !lambda "return id(tile2_climate_current_temperature).state;"
climate_target_temperature: !lambda "return id(tile2_climate_target_temperature).state;"
climate_humidity: !lambda "return id(tile2_climate_humidity).state;"
climate_mode: !lambda "return std::string(id(tile2_climate_mode).state.c_str());"
climate_supported_modes: !lambda "return std::string(id(tile2_climate_supported_modes).state.c_str());"
climate_min_temp: "${TILE2_CLIMATE_MIN_TEMP}"
climate_max_temp: "${TILE2_CLIMATE_MAX_TEMP}"
climate_step: "${TILE2_CLIMATE_STEP}"
# Long-press mode "vacuum": opens the vacuum controls.
# Falls outside the action/slider chain, so that stays unchanged.
- if:
condition:
lambda: "return std::string(\"${TILE2_LONGPRESS}\") == \"vacuum\";"
then:
- script.execute:
id: open_vacuum_overlay
title: "${TILE2_TITLE}"
entity: "${TILE2_ENTITY}"
# ---------- 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: 36
height: 36
styles: style_icon_circle
clickable: false
scrollable: false
scrollbar_mode: 'OFF'
widgets:
- label:
id: tile3_icon_lbl
align: CENTER
text_font: materialdesign_icons
text_color: 0xFFFFFF
text: "${TILE3_ICON}"
clickable: false
- label:
id: t3_title
x: ${TILE_TITLE_X}
y: ${TILE_TITLE_Y}
text: "${TILE3_TITLE}"
styles: style_title
clickable: false
- label:
id: t3_value
x: ${TILE_VALUE_X}
y: ${TILE_VALUE_Y}
text: "—"
styles: style_value
clickable: false
on_short_click:
- if:
condition:
lambda: 'return !runtime_tiles::enabled && cyd::touch_guard.accept(millis(), 3);'
then:
- script.execute: { id: publish_action, action: "tile3_press" }
- script.execute:
id: do_tile_action
tile_type: "${TILE3_TYPE}"
tap_action: "${TILE3_TAP_ACTION}"
service: "${TILE3_TAP_SERVICE}"
entity_id: "${TILE3_ENTITY}"
param_key: "${TILE3_TAP_PARAM_KEY}"
param_val: "${TILE3_TAP_PARAM_VAL}"
was_on: !lambda |-
if (std::string("${TILE3_TYPE}") == "cover")
return id(tile3_cover_state).state != "closed";
return std::string(id(ha_state_tile3).state.c_str()) == "on";
on_long_press:
- if:
condition:
lambda: 'return !runtime_tiles::enabled && cyd::touch_guard.accept(millis(), 3);'
then:
- script.execute: { id: publish_action, action: "tile3_long_press" }
- if:
condition:
lambda: "return std::string(\"${TILE3_LONGPRESS}\") == \"action\";"
then:
- script.execute:
id: do_tile_action
tile_type: !lambda |-
std::string t = "${TILE3_LONGPRESS_ACTION_TYPE}";
return t.empty() ? std::string("${TILE3_TYPE}") : t;
tap_action: "activate"
service: "${TILE3_LONGPRESS_ACTION_SERVICE}"
entity_id: "${TILE3_LONGPRESS_ACTION}"
param_key: ""
param_val: ""
was_on: false
else:
- if:
condition:
lambda: "return std::string(\"${TILE3_LONGPRESS}\") == \"slider\";"
then:
- script.execute:
id: open_value_overlay
title: "${TILE3_TITLE}"
entity: "${TILE3_ENTITY}"
tile_type: "${TILE3_TYPE}"
longpress_mode: "${TILE3_LONGPRESS_SLIDER}"
was_on: !lambda |-
if (std::string("${TILE3_TYPE}") == "cover")
return id(tile3_cover_state).state != "closed";
return std::string(id(ha_state_tile3).state.c_str()) == "on";
off_value: !lambda "return atoi(\"${TILE3_LONGPRESS_OFF_VALUE}\");"
brightness_value: !lambda "return id(tile3_brightness).state;"
percentage_value: !lambda "return id(tile3_percentage).state;"
position_value: !lambda "return id(tile3_cover_position).state;"
icon_text: "${TILE3_ICON}"
slider_bg_color: ${TILE3_BG_DISABLED_COLOR}
slider_fill_color: ${TILE3_CIRCLE_ACTIVE_COLOR}
slider_icon_color: ${TILE3_ICON_ACTIVE_COLOR}
label_off: "${TILE3_LABEL_OFF}"
label_on: "${TILE3_LABEL_ON}"
supported_color_modes: !lambda "return std::string(id(tile3_supported_color_modes).state.c_str());"
climate_current_temperature: !lambda "return id(tile3_climate_current_temperature).state;"
climate_target_temperature: !lambda "return id(tile3_climate_target_temperature).state;"
climate_humidity: !lambda "return id(tile3_climate_humidity).state;"
climate_mode: !lambda "return std::string(id(tile3_climate_mode).state.c_str());"
climate_supported_modes: !lambda "return std::string(id(tile3_climate_supported_modes).state.c_str());"
climate_min_temp: "${TILE3_CLIMATE_MIN_TEMP}"
climate_max_temp: "${TILE3_CLIMATE_MAX_TEMP}"
climate_step: "${TILE3_CLIMATE_STEP}"
# Long-press mode "vacuum": opens the vacuum controls.
# Falls outside the action/slider chain, so that stays unchanged.
- if:
condition:
lambda: "return std::string(\"${TILE3_LONGPRESS}\") == \"vacuum\";"
then:
- script.execute:
id: open_vacuum_overlay
title: "${TILE3_TITLE}"
entity: "${TILE3_ENTITY}"
# ---------- 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: 36
height: 36
styles: style_icon_circle
clickable: false
scrollable: false
scrollbar_mode: 'OFF'
widgets:
- label:
id: tile4_icon_lbl
align: CENTER
text_font: materialdesign_icons
text_color: 0xFFFFFF
text: "${TILE4_ICON}"
clickable: false
- label:
id: t4_title
x: ${TILE_TITLE_X}
y: ${TILE_TITLE_Y}
text: "${TILE4_TITLE}"
styles: style_title
clickable: false
- label:
id: t4_value
x: ${TILE_VALUE_X}
y: ${TILE_VALUE_Y}
text: "—"
styles: style_value
clickable: false
on_short_click:
- if:
condition:
lambda: 'return !runtime_tiles::enabled && cyd::touch_guard.accept(millis(), 4);'
then:
- script.execute: { id: publish_action, action: "tile4_press" }
- script.execute:
id: do_tile_action
tile_type: "${TILE4_TYPE}"
tap_action: "${TILE4_TAP_ACTION}"
service: "${TILE4_TAP_SERVICE}"
entity_id: "${TILE4_ENTITY}"
param_key: "${TILE4_TAP_PARAM_KEY}"
param_val: "${TILE4_TAP_PARAM_VAL}"
was_on: !lambda |-
if (std::string("${TILE4_TYPE}") == "cover")
return id(tile4_cover_state).state != "closed";
return std::string(id(ha_state_tile4).state.c_str()) == "on";
on_long_press:
- if:
condition:
lambda: 'return !runtime_tiles::enabled && cyd::touch_guard.accept(millis(), 4);'
then:
- script.execute: { id: publish_action, action: "tile4_long_press" }
- if:
condition:
lambda: "return std::string(\"${TILE4_LONGPRESS}\") == \"action\";"
then:
- script.execute:
id: do_tile_action
tile_type: !lambda |-
std::string t = "${TILE4_LONGPRESS_ACTION_TYPE}";
return t.empty() ? std::string("${TILE4_TYPE}") : t;
tap_action: "activate"
service: "${TILE4_LONGPRESS_ACTION_SERVICE}"
entity_id: "${TILE4_LONGPRESS_ACTION}"
param_key: ""
param_val: ""
was_on: false
else:
- if:
condition:
lambda: "return std::string(\"${TILE4_LONGPRESS}\") == \"slider\";"
then:
- script.execute:
id: open_value_overlay
title: "${TILE4_TITLE}"
entity: "${TILE4_ENTITY}"
tile_type: "${TILE4_TYPE}"
longpress_mode: "${TILE4_LONGPRESS_SLIDER}"
was_on: !lambda |-
if (std::string("${TILE4_TYPE}") == "cover")
return id(tile4_cover_state).state != "closed";
return std::string(id(ha_state_tile4).state.c_str()) == "on";
off_value: !lambda "return atoi(\"${TILE4_LONGPRESS_OFF_VALUE}\");"
brightness_value: !lambda "return id(tile4_brightness).state;"
percentage_value: !lambda "return id(tile4_percentage).state;"
position_value: !lambda "return id(tile4_cover_position).state;"
icon_text: "${TILE4_ICON}"
slider_bg_color: ${TILE4_BG_DISABLED_COLOR}
slider_fill_color: ${TILE4_CIRCLE_ACTIVE_COLOR}
slider_icon_color: ${TILE4_ICON_ACTIVE_COLOR}
label_off: "${TILE4_LABEL_OFF}"
label_on: "${TILE4_LABEL_ON}"
supported_color_modes: !lambda "return std::string(id(tile4_supported_color_modes).state.c_str());"
climate_current_temperature: !lambda "return id(tile4_climate_current_temperature).state;"
climate_target_temperature: !lambda "return id(tile4_climate_target_temperature).state;"
climate_humidity: !lambda "return id(tile4_climate_humidity).state;"
climate_mode: !lambda "return std::string(id(tile4_climate_mode).state.c_str());"
climate_supported_modes: !lambda "return std::string(id(tile4_climate_supported_modes).state.c_str());"
climate_min_temp: "${TILE4_CLIMATE_MIN_TEMP}"
climate_max_temp: "${TILE4_CLIMATE_MAX_TEMP}"
climate_step: "${TILE4_CLIMATE_STEP}"
# Long-press mode "vacuum": opens the vacuum controls.
# Falls outside the action/slider chain, so that stays unchanged.
- if:
condition:
lambda: "return std::string(\"${TILE4_LONGPRESS}\") == \"vacuum\";"
then:
- script.execute:
id: open_vacuum_overlay
title: "${TILE4_TITLE}"
entity: "${TILE4_ENTITY}"
# ---------- 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: 36
height: 36
styles: style_icon_circle
clickable: false
scrollable: false
scrollbar_mode: 'OFF'
widgets:
- label:
id: tile5_icon_lbl
align: CENTER
text_font: materialdesign_icons
text_color: 0xFFFFFF
text: "${TILE5_ICON}"
clickable: false
- label:
id: t5_title
x: ${TILE_TITLE_X}
y: ${TILE_TITLE_Y}
text: "${TILE5_TITLE}"
styles: style_title
clickable: false
- label:
id: t5_value
x: ${TILE_VALUE_X}
y: ${TILE_VALUE_Y}
text: "—"
styles: style_value
clickable: false
on_short_click:
- if:
condition:
lambda: 'return !runtime_tiles::enabled && cyd::touch_guard.accept(millis(), 5);'
then:
- script.execute: { id: publish_action, action: "tile5_press" }
- script.execute:
id: do_tile_action
tile_type: "${TILE5_TYPE}"
tap_action: "${TILE5_TAP_ACTION}"
service: "${TILE5_TAP_SERVICE}"
entity_id: "${TILE5_ENTITY}"
param_key: "${TILE5_TAP_PARAM_KEY}"
param_val: "${TILE5_TAP_PARAM_VAL}"
was_on: !lambda |-
if (std::string("${TILE5_TYPE}") == "cover")
return id(tile5_cover_state).state != "closed";
return std::string(id(ha_state_tile5).state.c_str()) == "on";
on_long_press:
- if:
condition:
lambda: 'return !runtime_tiles::enabled && cyd::touch_guard.accept(millis(), 5);'
then:
- script.execute: { id: publish_action, action: "tile5_long_press" }
- if:
condition:
lambda: "return std::string(\"${TILE5_LONGPRESS}\") == \"action\";"
then:
- script.execute:
id: do_tile_action
tile_type: !lambda |-
std::string t = "${TILE5_LONGPRESS_ACTION_TYPE}";
return t.empty() ? std::string("${TILE5_TYPE}") : t;
tap_action: "activate"
service: "${TILE5_LONGPRESS_ACTION_SERVICE}"
entity_id: "${TILE5_LONGPRESS_ACTION}"
param_key: ""
param_val: ""
was_on: false
else:
- if:
condition:
lambda: "return std::string(\"${TILE5_LONGPRESS}\") == \"slider\";"
then:
- script.execute:
id: open_value_overlay
title: "${TILE5_TITLE}"
entity: "${TILE5_ENTITY}"
tile_type: "${TILE5_TYPE}"
longpress_mode: "${TILE5_LONGPRESS_SLIDER}"
was_on: !lambda |-
if (std::string("${TILE5_TYPE}") == "cover")
return id(tile5_cover_state).state != "closed";
return std::string(id(ha_state_tile5).state.c_str()) == "on";
off_value: !lambda "return atoi(\"${TILE5_LONGPRESS_OFF_VALUE}\");"
brightness_value: !lambda "return id(tile5_brightness).state;"
percentage_value: !lambda "return id(tile5_percentage).state;"
position_value: !lambda "return id(tile5_cover_position).state;"
icon_text: "${TILE5_ICON}"
slider_bg_color: ${TILE5_BG_DISABLED_COLOR}
slider_fill_color: ${TILE5_CIRCLE_ACTIVE_COLOR}
slider_icon_color: ${TILE5_ICON_ACTIVE_COLOR}
label_off: "${TILE5_LABEL_OFF}"
label_on: "${TILE5_LABEL_ON}"
supported_color_modes: !lambda "return std::string(id(tile5_supported_color_modes).state.c_str());"
climate_current_temperature: !lambda "return id(tile5_climate_current_temperature).state;"
climate_target_temperature: !lambda "return id(tile5_climate_target_temperature).state;"
climate_humidity: !lambda "return id(tile5_climate_humidity).state;"
climate_mode: !lambda "return std::string(id(tile5_climate_mode).state.c_str());"
climate_supported_modes: !lambda "return std::string(id(tile5_climate_supported_modes).state.c_str());"
climate_min_temp: "${TILE5_CLIMATE_MIN_TEMP}"
climate_max_temp: "${TILE5_CLIMATE_MAX_TEMP}"
climate_step: "${TILE5_CLIMATE_STEP}"
# Long-press mode "vacuum": opens the vacuum controls.
# Falls outside the action/slider chain, so that stays unchanged.
- if:
condition:
lambda: "return std::string(\"${TILE5_LONGPRESS}\") == \"vacuum\";"
then:
- script.execute:
id: open_vacuum_overlay
title: "${TILE5_TITLE}"
entity: "${TILE5_ENTITY}"
# ---------- 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: 36
height: 36
styles: style_icon_circle
clickable: false
scrollable: false
scrollbar_mode: 'OFF'
widgets:
- label:
id: tile6_icon_lbl
align: CENTER
text_font: materialdesign_icons
text_color: 0xFFFFFF
text: "${TILE6_ICON}"
clickable: false
- label:
id: t6_title
x: ${TILE_TITLE_X}
y: ${TILE_TITLE_Y}
text: "${TILE6_TITLE}"
styles: style_title
clickable: false
- label:
id: t6_value
x: ${TILE_VALUE_X}
y: ${TILE_VALUE_Y}
text: "—"
styles: style_value
clickable: false
on_short_click:
- if:
condition:
lambda: 'return !runtime_tiles::enabled && cyd::touch_guard.accept(millis(), 6);'
then:
- script.execute: { id: publish_action, action: "tile6_press" }
- script.execute:
id: do_tile_action
tile_type: "${TILE6_TYPE}"
tap_action: "${TILE6_TAP_ACTION}"
service: "${TILE6_TAP_SERVICE}"
entity_id: "${TILE6_ENTITY}"
param_key: "${TILE6_TAP_PARAM_KEY}"
param_val: "${TILE6_TAP_PARAM_VAL}"
was_on: !lambda |-
if (std::string("${TILE6_TYPE}") == "cover")
return id(tile6_cover_state).state != "closed";
return std::string(id(ha_state_tile6).state.c_str()) == "on";
on_long_press:
- if:
condition:
lambda: 'return !runtime_tiles::enabled && cyd::touch_guard.accept(millis(), 6);'
then:
- script.execute: { id: publish_action, action: "tile6_long_press" }
- if:
condition:
lambda: "return std::string(\"${TILE6_LONGPRESS}\") == \"action\";"
then:
- script.execute:
id: do_tile_action
tile_type: !lambda |-
std::string t = "${TILE6_LONGPRESS_ACTION_TYPE}";
return t.empty() ? std::string("${TILE6_TYPE}") : t;
tap_action: "activate"
service: "${TILE6_LONGPRESS_ACTION_SERVICE}"
entity_id: "${TILE6_LONGPRESS_ACTION}"
param_key: ""
param_val: ""
was_on: false
else:
- if:
condition:
lambda: "return std::string(\"${TILE6_LONGPRESS}\") == \"slider\";"
then:
- script.execute:
id: open_value_overlay
title: "${TILE6_TITLE}"
entity: "${TILE6_ENTITY}"
tile_type: "${TILE6_TYPE}"
longpress_mode: "${TILE6_LONGPRESS_SLIDER}"
was_on: !lambda |-
if (std::string("${TILE6_TYPE}") == "cover")
return id(tile6_cover_state).state != "closed";
return std::string(id(ha_state_tile6).state.c_str()) == "on";
off_value: !lambda "return atoi(\"${TILE6_LONGPRESS_OFF_VALUE}\");"
brightness_value: !lambda "return id(tile6_brightness).state;"
percentage_value: !lambda "return id(tile6_percentage).state;"
position_value: !lambda "return id(tile6_cover_position).state;"
icon_text: "${TILE6_ICON}"
slider_bg_color: ${TILE6_BG_DISABLED_COLOR}
slider_fill_color: ${TILE6_CIRCLE_ACTIVE_COLOR}
slider_icon_color: ${TILE6_ICON_ACTIVE_COLOR}
label_off: "${TILE6_LABEL_OFF}"
label_on: "${TILE6_LABEL_ON}"
supported_color_modes: !lambda "return std::string(id(tile6_supported_color_modes).state.c_str());"
climate_current_temperature: !lambda "return id(tile6_climate_current_temperature).state;"
climate_target_temperature: !lambda "return id(tile6_climate_target_temperature).state;"
climate_humidity: !lambda "return id(tile6_climate_humidity).state;"
climate_mode: !lambda "return std::string(id(tile6_climate_mode).state.c_str());"
climate_supported_modes: !lambda "return std::string(id(tile6_climate_supported_modes).state.c_str());"
climate_min_temp: "${TILE6_CLIMATE_MIN_TEMP}"
climate_max_temp: "${TILE6_CLIMATE_MAX_TEMP}"
climate_step: "${TILE6_CLIMATE_STEP}"
# Long-press mode "vacuum": opens the vacuum controls.
# Falls outside the action/slider chain, so that stays unchanged.
- if:
condition:
lambda: "return std::string(\"${TILE6_LONGPRESS}\") == \"vacuum\";"
then:
- script.execute:
id: open_vacuum_overlay
title: "${TILE6_TITLE}"
entity: "${TILE6_ENTITY}"
# ---------- 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: 36
height: 36
styles: style_icon_circle
clickable: false
scrollable: false
scrollbar_mode: 'OFF'
widgets:
- label:
id: tile7_icon_lbl
align: CENTER
text_font: materialdesign_icons
text_color: 0xFFFFFF
text: "${TILE7_ICON}"
clickable: false
- label:
id: t7_title
x: ${TILE_TITLE_X}
y: ${TILE_TITLE_Y}
text: "${TILE7_TITLE}"
styles: style_title
clickable: false
- label:
id: t7_value
x: ${TILE_VALUE_X}
y: ${TILE_VALUE_Y}
text: "—"
styles: style_value
clickable: false
on_short_click:
- if:
condition:
lambda: 'return !runtime_tiles::enabled && cyd::touch_guard.accept(millis(), 7);'
then:
- script.execute: { id: publish_action, action: "tile7_press" }
- script.execute:
id: do_tile_action
tile_type: "${TILE7_TYPE}"
tap_action: "${TILE7_TAP_ACTION}"
service: "${TILE7_TAP_SERVICE}"
entity_id: "${TILE7_ENTITY}"
param_key: "${TILE7_TAP_PARAM_KEY}"
param_val: "${TILE7_TAP_PARAM_VAL}"
was_on: !lambda "return std::string(id(ha_state_tile7).state.c_str()) == \"on\";"
on_long_press:
- if:
condition:
lambda: 'return !runtime_tiles::enabled && cyd::touch_guard.accept(millis(), 7);'
then:
- script.execute: { id: publish_action, action: "tile7_long_press" }
- if:
condition:
lambda: "return std::string(\"${TILE7_LONGPRESS}\") == \"action\";"
then:
- script.execute:
id: do_tile_action
tile_type: !lambda |-
std::string t = "${TILE7_LONGPRESS_ACTION_TYPE}";
return t.empty() ? std::string("${TILE7_TYPE}") : t;
tap_action: "activate"
service: "${TILE7_LONGPRESS_ACTION_SERVICE}"
entity_id: "${TILE7_LONGPRESS_ACTION}"
param_key: ""
param_val: ""
was_on: false
else:
- if:
condition:
lambda: "return std::string(\"${TILE7_LONGPRESS}\") == \"slider\";"
then:
- script.execute:
id: open_value_overlay
title: "${TILE7_TITLE}"
entity: "${TILE7_ENTITY}"
tile_type: "${TILE7_TYPE}"
longpress_mode: "${TILE7_LONGPRESS_SLIDER}"
was_on: !lambda "return std::string(id(ha_state_tile7).state.c_str()) == \"on\";"
off_value: !lambda "return atoi(\"${TILE7_LONGPRESS_OFF_VALUE}\");"
brightness_value: !lambda "return id(tile7_brightness).state;"
percentage_value: !lambda "return NAN;"
position_value: !lambda "return NAN;"
icon_text: "${TILE7_ICON}"
slider_bg_color: ${TILE7_BG_DISABLED_COLOR}
slider_fill_color: ${TILE7_CIRCLE_ACTIVE_COLOR}
slider_icon_color: ${TILE7_ICON_ACTIVE_COLOR}
label_off: "${TILE7_LABEL_OFF}"
label_on: "${TILE7_LABEL_ON}"
supported_color_modes: !lambda "return std::string(id(tile7_supported_color_modes).state.c_str());"
climate_current_temperature: !lambda "return NAN;"
climate_target_temperature: !lambda "return NAN;"
climate_humidity: !lambda "return NAN;"
climate_mode: ""
climate_supported_modes: ""
climate_min_temp: "5.0"
climate_max_temp: "30.0"
climate_step: "0.5"
# ---------- 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: 36
height: 36
styles: style_icon_circle
clickable: false
scrollable: false
scrollbar_mode: 'OFF'
widgets:
- label:
id: tile8_icon_lbl
align: CENTER
text_font: materialdesign_icons
text_color: 0xFFFFFF
text: "${TILE8_ICON}"
clickable: false
- label:
id: t8_title
x: ${TILE_TITLE_X}
y: ${TILE_TITLE_Y}
text: "${TILE8_TITLE}"
styles: style_title
clickable: false
- label:
id: t8_value
x: ${TILE_VALUE_X}
y: ${TILE_VALUE_Y}
text: "—"
styles: style_value
clickable: false
on_short_click:
- if:
condition:
lambda: 'return !runtime_tiles::enabled && cyd::touch_guard.accept(millis(), 8);'
then:
- script.execute: { id: publish_action, action: "tile8_press" }
- script.execute:
id: do_tile_action
tile_type: "${TILE8_TYPE}"
tap_action: "${TILE8_TAP_ACTION}"
service: "${TILE8_TAP_SERVICE}"
entity_id: "${TILE8_ENTITY}"
param_key: "${TILE8_TAP_PARAM_KEY}"
param_val: "${TILE8_TAP_PARAM_VAL}"
was_on: !lambda "return std::string(id(ha_state_tile8).state.c_str()) == \"${TILE8_ACTIVE_STATE}\";"
on_long_press:
- if:
condition:
lambda: 'return !runtime_tiles::enabled && cyd::touch_guard.accept(millis(), 8);'
then:
- script.execute: { id: publish_action, action: "tile8_long_press" }
# Slider mode isn't possible here without first creating the
# corresponding sensors; "action" works out of the box.
- if:
condition:
lambda: "return std::string(\"${TILE8_LONGPRESS}\") == \"action\";"
then:
- script.execute:
id: do_tile_action
tile_type: !lambda |-
std::string t = "${TILE8_LONGPRESS_ACTION_TYPE}";
return t.empty() ? std::string("${TILE8_TYPE}") : t;
tap_action: "activate"
service: "${TILE8_LONGPRESS_ACTION_SERVICE}"
entity_id: "${TILE8_LONGPRESS_ACTION}"
param_key: ""
param_val: ""
was_on: 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: 36
height: 36
styles: style_icon_circle
clickable: false
scrollable: false
scrollbar_mode: 'OFF'
widgets:
- label:
id: tile9_icon_lbl
align: CENTER
text_font: materialdesign_icons
text_color: 0xFFFFFF
text: "${TILE9_ICON}"
clickable: false
- label:
id: t9_title
x: ${TILE_TITLE_X}
y: ${TILE_TITLE_Y}
text: "${TILE9_TITLE}"
styles: style_title
clickable: false
- label:
id: t9_value
x: ${TILE_VALUE_X}
y: ${TILE_VALUE_Y}
text: "—"
styles: style_value
clickable: false
on_short_click:
- if:
condition:
lambda: 'return !runtime_tiles::enabled && cyd::touch_guard.accept(millis(), 9);'
then:
- script.execute: { id: publish_action, action: "tile9_press" }
- script.execute:
id: do_tile_action
tile_type: "${TILE9_TYPE}"
tap_action: "${TILE9_TAP_ACTION}"
service: "${TILE9_TAP_SERVICE}"
entity_id: "${TILE9_ENTITY}"
param_key: "${TILE9_TAP_PARAM_KEY}"
param_val: "${TILE9_TAP_PARAM_VAL}"
was_on: !lambda "return std::string(id(ha_state_tile9).state.c_str()) == \"on\";"
on_long_press:
- if:
condition:
lambda: 'return !runtime_tiles::enabled && cyd::touch_guard.accept(millis(), 9);'
then:
- script.execute: { id: publish_action, action: "tile9_long_press" }
- if:
condition:
lambda: "return std::string(\"${TILE9_LONGPRESS}\") == \"action\";"
then:
- script.execute:
id: do_tile_action
tile_type: !lambda |-
std::string t = "${TILE9_LONGPRESS_ACTION_TYPE}";
return t.empty() ? std::string("${TILE9_TYPE}") : t;
tap_action: "activate"
service: "${TILE9_LONGPRESS_ACTION_SERVICE}"
entity_id: "${TILE9_LONGPRESS_ACTION}"
param_key: ""
param_val: ""
was_on: false
else:
- if:
condition:
lambda: "return std::string(\"${TILE9_LONGPRESS}\") == \"slider\";"
then:
- script.execute:
id: open_value_overlay
title: "${TILE9_TITLE}"
entity: "${TILE9_ENTITY}"
tile_type: "${TILE9_TYPE}"
longpress_mode: "${TILE9_LONGPRESS_SLIDER}"
was_on: !lambda "return std::string(id(ha_state_tile9).state.c_str()) == \"on\";"
off_value: !lambda "return atoi(\"${TILE9_LONGPRESS_OFF_VALUE}\");"
brightness_value: !lambda "return id(tile9_brightness).state;"
percentage_value: !lambda "return NAN;"
position_value: !lambda "return NAN;"
icon_text: "${TILE9_ICON}"
slider_bg_color: ${TILE9_BG_DISABLED_COLOR}
slider_fill_color: ${TILE9_CIRCLE_ACTIVE_COLOR}
slider_icon_color: ${TILE9_ICON_ACTIVE_COLOR}
label_off: "${TILE9_LABEL_OFF}"
label_on: "${TILE9_LABEL_ON}"
supported_color_modes: !lambda "return std::string(id(tile9_supported_color_modes).state.c_str());"
climate_current_temperature: !lambda "return NAN;"
climate_target_temperature: !lambda "return NAN;"
climate_humidity: !lambda "return NAN;"
climate_mode: ""
climate_supported_modes: ""
climate_min_temp: "5.0"
climate_max_temp: "30.0"
climate_step: "0.5"
# ---------- 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: 36
height: 36
styles: style_icon_circle
clickable: false
scrollable: false
scrollbar_mode: 'OFF'
widgets:
- label:
id: tile10_icon_lbl
align: CENTER
text_font: materialdesign_icons
text_color: 0xFFFFFF
text: "${TILE10_ICON}"
clickable: false
- label:
id: t10_title
x: ${TILE_TITLE_X}
y: ${TILE_TITLE_Y}
text: "${TILE10_TITLE}"
styles: style_title
clickable: false
- label:
id: t10_value
x: ${TILE_VALUE_X}
y: ${TILE_VALUE_Y}
text: "—"
styles: style_value
clickable: false
on_short_click:
- if:
condition:
lambda: 'return !runtime_tiles::enabled && cyd::touch_guard.accept(millis(), 10);'
then:
- script.execute: { id: publish_action, action: "tile10_press" }
- script.execute:
id: do_tile_action
tile_type: "${TILE10_TYPE}"
tap_action: "${TILE10_TAP_ACTION}"
service: "${TILE10_TAP_SERVICE}"
entity_id: "${TILE10_ENTITY}"
param_key: "${TILE10_TAP_PARAM_KEY}"
param_val: "${TILE10_TAP_PARAM_VAL}"
was_on: !lambda "return std::string(id(ha_state_tile10).state.c_str()) == \"on\";"
on_long_press:
- if:
condition:
lambda: 'return !runtime_tiles::enabled && cyd::touch_guard.accept(millis(), 10);'
then:
- script.execute: { id: publish_action, action: "tile10_long_press" }
- if:
condition:
lambda: "return std::string(\"${TILE10_LONGPRESS}\") == \"action\";"
then:
- script.execute:
id: do_tile_action
tile_type: !lambda |-
std::string t = "${TILE10_LONGPRESS_ACTION_TYPE}";
return t.empty() ? std::string("${TILE10_TYPE}") : t;
tap_action: "activate"
service: "${TILE10_LONGPRESS_ACTION_SERVICE}"
entity_id: "${TILE10_LONGPRESS_ACTION}"
param_key: ""
param_val: ""
was_on: false
# ---------- VALUE SLIDER OVERLAY ----------
# ---------- PAGE NAVIGATION: one full-width bar ----------
- obj:
id: page_prev
align: BOTTOM_LEFT
x: 0
y: 0
width: 130
height: 32
pad_all: 0
radius: 0
bg_opa: TRANSP
border_width: 0
scrollable: false
pressed:
bg_color: 0xE3E3E3
bg_opa: COVER
on_short_click:
- if:
condition:
lambda: 'return cyd::touch_guard.accept(millis(), 11);'
then:
- lambda: 'id(tile_page) = std::max(0, id(tile_page) - 1);'
- script.execute: show_tile_page
widgets:
- label:
align: LEFT_MID
x: 12
text: "< Previous"
text_font: label
text_color: 0x1B1B1B
clickable: false
- label:
id: page_number
align: BOTTOM_MID
y: 0
width: 60
height: 32
pad_top: 9
border_width: 0
text: "1 / 2"
text_align: CENTER
text_font: sublabel
text_color: 0x1B1B1B
- obj:
id: page_next
align: BOTTOM_RIGHT
x: 0
y: 0
width: 130
height: 32
pad_all: 0
radius: 0
bg_opa: TRANSP
border_width: 0
scrollable: false
pressed:
bg_color: 0xE3E3E3
bg_opa: COVER
on_short_click:
- if:
condition:
lambda: 'return cyd::touch_guard.accept(millis(), 12);'
then:
- lambda: 'id(tile_page) += 1;'
- script.execute: show_tile_page
widgets:
- label:
align: RIGHT_MID
x: -12
text: "Next >"
text_font: label
text_color: 0x1B1B1B
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_bg_grad
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}
bg_color: 0xFFFFFF
bg_opa: 18%
border_width: 0
clickable: false
scrollable: false
- label:
id: overlay_title
align: TOP_MID
y: 10
text: "Light"
styles: style_room
- label:
id: overlay_value
align: TOP_MID
y: 35
text: "100 %"
styles: style_value_big
- slider:
id: overlay_slider
align: CENTER
y: 18
width: ${OVERLAY_SLIDER_W}
height: ${OVERLAY_SLIDER_H}
min_value: 0
max_value: 100
value: 100
adv_hittest: false
bg_color: 0xFFFFFF
bg_opa: 100%
radius: 28
# 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:
bg_color: 0xD5EEFC
bg_opa: 100%
radius: 28
knob:
bg_opa: 0%
border_width: 0
shadow_width: 0
pad_all: 10
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
text_color: 0x1B1B1B
text: "${TILE1_ICON}"
clickable: false
- obj:
id: overlay_color_button
align: BOTTOM_RIGHT
x: -13
y: -13
width: 44
height: 44
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: 40
height: 40
start_angle: 0
end_angle: 45
min_value: 0
max_value: 100
value: 0
adjustable: false
arc_width: 10
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: 40
height: 40
start_angle: 45
end_angle: 90
min_value: 0
max_value: 100
value: 0
adjustable: false
arc_width: 10
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: 40
height: 40
start_angle: 90
end_angle: 135
min_value: 0
max_value: 100
value: 0
adjustable: false
arc_width: 10
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: 40
height: 40
start_angle: 135
end_angle: 180
min_value: 0
max_value: 100
value: 0
adjustable: false
arc_width: 10
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: 40
height: 40
start_angle: 180
end_angle: 225
min_value: 0
max_value: 100
value: 0
adjustable: false
arc_width: 10
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: 40
height: 40
start_angle: 225
end_angle: 270
min_value: 0
max_value: 100
value: 0
adjustable: false
arc_width: 10
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: 40
height: 40
start_angle: 270
end_angle: 315
min_value: 0
max_value: 100
value: 0
adjustable: false
arc_width: 10
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: 40
height: 40
start_angle: 315
end_angle: 360
min_value: 0
max_value: 100
value: 0
adjustable: false
arc_width: 10
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}
bg_color: 0xF7F7F7
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_detail_bg
x: 0
y: 0
width: ${DISPLAY_W}
height: ${DISPLAY_H}
styles: style_bg_grad
scrollable: false
clickable: false
- obj:
id: color_detail_scrim
x: 0
y: 0
width: ${DISPLAY_W}
height: ${DISPLAY_H}
bg_color: 0xFFFFFF
bg_opa: 72%
border_width: 0
clickable: false
scrollable: false
- obj:
id: color_close_button
align: TOP_LEFT
x: 10
y: 9
width: 36
height: 36
radius: 999
bg_color: 0xEEEEEE
bg_opa: COVER
border_width: 1
border_color: 0xFFFFFF
border_opa: 20%
clickable: true
scrollable: false
on_click:
- script.execute: close_cards
widgets:
- label:
align: CENTER
text: "x"
text_font: sublabel_big
text_color: 0x1B1B1B
clickable: false
- label:
id: color_detail_title
width: 210
long_mode: DOT
text_align: CENTER
align: TOP_MID
y: 16
text: "Light Color"
styles: style_room
- obj:
id: color_done_button
align: TOP_RIGHT
x: -10
y: 9
width: 36
height: 36
radius: 999
bg_color: 0xEEEEEE
bg_opa: COVER
border_width: 0
clickable: true
scrollable: false
on_click:
- script.execute: close_cards
widgets:
- label:
align: CENTER
text: "\U000F012C"
text_font: materialdesign_icons
text_color: 0x1B1B1B
clickable: false
# ---------- CLIMATE DETAIL OVERLAY ----------
- obj:
id: climate_detail_overlay
x: 0
y: 0
width: ${DISPLAY_W}
height: ${DISPLAY_H}
bg_color: 0xF7F7F7
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_detail_bg
x: 0
y: 0
width: ${DISPLAY_W}
height: ${DISPLAY_H}
styles: style_bg_grad
scrollable: false
clickable: false
- obj:
id: climate_detail_scrim
x: 0
y: 0
width: ${DISPLAY_W}
height: ${DISPLAY_H}
bg_color: 0xFFFFFF
bg_opa: 62%
border_width: 0
clickable: false
scrollable: false
- obj:
id: climate_close_button
align: TOP_LEFT
x: 10
y: 9
# 44 instead of 36: slightly bigger tap target, grows away from the arc
# (top-left anchor stays put) instead of towards it.
width: 44
height: 44
radius: 999
bg_color: 0xEEEEEE
bg_opa: COVER
border_width: 1
border_color: 0xFFFFFF
border_opa: 20%
clickable: true
scrollable: false
on_click:
- script.execute: close_cards
widgets:
- label:
align: CENTER
text: "x"
text_font: sublabel_big
text_color: 0x1B1B1B
clickable: false
- obj:
id: climate_power_button
align: TOP_RIGHT
x: -10
y: 9
width: 44
height: 44
radius: 999
bg_color: 0xEEEEEE
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
text_color: 0x1B1B1B
clickable: false
- label:
id: climate_detail_title
align: TOP_MID
y: 16
text: "Climate"
styles: style_room
- label:
id: climate_current_label
align: TOP_MID
y: 42
text: "Now: --"
text_font: sublabel_big
text_color: 0x1B1B1B
text_opa: 85%
clickable: false
- arc:
id: climate_arc
align: CENTER
y: 20
width: 184
height: 184
# Without this the arc claims an invisible extra
# click area outside the 184x184 box — near the close
# button top-left this creates overlap.
ext_click_area: 0
start_angle: 135
end_angle: 45
min_value: 50
max_value: 300
value: 200
adjustable: true
arc_width: 20
arc_rounded: true
arc_color: 0x4B4B4F
arc_opa: 80%
bg_opa: TRANSP
border_width: 0
indicator:
arc_color: 0xFEC600
arc_opa: COVER
arc_width: 20
arc_rounded: true
knob:
bg_color: 0x111111
bg_opa: COVER
border_width: 0
shadow_width: 0
on_value:
- script.execute:
id: climate_target_preview
value: !lambda "return x;"
on_release:
- script.execute:
id: climate_target_commit
value: !lambda "return lv_arc_get_value(id(climate_arc));"
- label:
id: climate_set_label
align: CENTER
y: -16
text: "Target"
text_font: sublabel_big
text_color: 0x1B1B1B
text_opa: 68%
clickable: false
- label:
id: climate_target_label
align: CENTER
y: 20
text: "20.0°"
text_font: headline
text_color: 0x1B1B1B
clickable: false
- obj:
id: climate_minus_button
align: BOTTOM_LEFT
x: 38
y: -16
width: 48
height: 36
radius: 18
bg_color: 0xEEEEEE
bg_opa: COVER
border_width: 1
border_color: 0xFFFFFF
border_opa: 16%
clickable: true
scrollable: false
on_click:
- script.execute:
id: climate_adjust_target
direction: -1
widgets:
- label:
align: CENTER
text: "-"
text_font: headline
text_color: 0x1B1B1B
clickable: false
- obj:
id: climate_plus_button
align: BOTTOM_RIGHT
x: -38
y: -16
width: 48
height: 36
radius: 18
bg_color: 0xEEEEEE
bg_opa: COVER
border_width: 1
border_color: 0xFFFFFF
border_opa: 16%
clickable: true
scrollable: false
on_click:
- script.execute:
id: climate_adjust_target
direction: 1
widgets:
- label:
align: CENTER
text: "+"
text_font: headline
text_color: 0x1B1B1B
clickable: false
- obj:
id: climate_mode_pill
align: BOTTOM_MID
y: -18
width: 126
height: 34
radius: 17
bg_color: 0xF0F0F0
bg_opa: 82%
border_width: 0
clickable: true
scrollable: false
on_click:
- script.execute: open_climate_mode_picker
widgets:
- label:
id: climate_mode_label
align: CENTER
text: "Auto"
text_font: sublabel_big
text_color: 0x1B1B1B
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
bg_color: 0xE7E7E7
bg_opa: COVER
border_width: 0
clickable: true
scrollable: false
scrollbar_mode: 'OFF'
widgets:
- label:
align: TOP_MID
y: 16
text: "Mode"
text_font: sublabel_big
text_color: 0x1B1B1B
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: 44
width: 98%
height: 178
bg_opa: TRANSP
border_width: 0
scrollable: false
clickable: false
pad_all: 3
layout:
type: FLEX
pad_row: 3
pad_column: 4
flex_flow: ROW_WRAP
flex_align_main: CENTER
flex_align_cross: CENTER
flex_align_track: CENTER
widgets:
- obj:
id: cmode_off
width: 88
height: 26
radius: 13
bg_color: 0xFFFFFF
bg_opa: COVER
border_width: 1
border_color: 0xDDDDDD
clickable: true
scrollable: false
pad_all: 0
on_click:
- script.execute: { id: set_climate_mode, mode: "off" }
widgets:
- label:
align: CENTER
text: "Off"
text_font: sublabel_big
text_color: 0x1B1B1B
clickable: false
- obj:
id: cmode_heat
width: 88
height: 26
radius: 13
bg_color: 0xFFFFFF
bg_opa: COVER
border_width: 1
border_color: 0xDDDDDD
clickable: true
scrollable: false
pad_all: 0
on_click:
- script.execute: { id: set_climate_mode, mode: "heat" }
widgets:
- label:
align: CENTER
text: "Heat"
text_font: sublabel_big
text_color: 0x1B1B1B
clickable: false
- obj:
id: cmode_cool
width: 88
height: 26
radius: 13
bg_color: 0xFFFFFF
bg_opa: COVER
border_width: 1
border_color: 0xDDDDDD
clickable: true
scrollable: false
pad_all: 0
on_click:
- script.execute: { id: set_climate_mode, mode: "cool" }
widgets:
- label:
align: CENTER
text: "Cool"
text_font: sublabel_big
text_color: 0x1B1B1B
clickable: false
- obj:
id: cmode_heat_cool
width: 88
height: 26
radius: 13
bg_color: 0xFFFFFF
bg_opa: COVER
border_width: 1
border_color: 0xDDDDDD
clickable: true
scrollable: false
pad_all: 0
on_click:
- script.execute: { id: set_climate_mode, mode: "heat_cool" }
widgets:
- label:
align: CENTER
text: "Heat/Cool"
text_font: sublabel_big
text_color: 0x1B1B1B
clickable: false
- obj:
id: cmode_auto
width: 88
height: 26
radius: 13
bg_color: 0xFFFFFF
bg_opa: COVER
border_width: 1
border_color: 0xDDDDDD
clickable: true
scrollable: false
pad_all: 0
on_click:
- script.execute: { id: set_climate_mode, mode: "auto" }
widgets:
- label:
align: CENTER
text: "Auto"
text_font: sublabel_big
text_color: 0x1B1B1B
clickable: false
- obj:
id: cmode_dry
width: 88
height: 26
radius: 13
bg_color: 0xFFFFFF
bg_opa: COVER
border_width: 1
border_color: 0xDDDDDD
clickable: true
scrollable: false
pad_all: 0
on_click:
- script.execute: { id: set_climate_mode, mode: "dry" }
widgets:
- label:
align: CENTER
text: "Dry"
text_font: sublabel_big
text_color: 0x1B1B1B
clickable: false
- obj:
id: cmode_fan_only
width: 88
height: 26
radius: 13
bg_color: 0xFFFFFF
bg_opa: COVER
border_width: 1
border_color: 0xDDDDDD
clickable: true
scrollable: false
pad_all: 0
on_click:
- script.execute: { id: set_climate_mode, mode: "fan_only" }
widgets:
- label:
align: CENTER
text: "Fan only"
text_font: sublabel_big
text_color: 0x1B1B1B
clickable: false
- label:
id: cfan_title
width: 100%
height: 11
text: "Fan"
text_font: sublabel
text_color: 0x6B6B6B
text_align: CENTER
clickable: false
- obj:
id: cfan_0
width: 62
height: 22
radius: 11
bg_color: 0xFFFFFF
bg_opa: COVER
border_width: 1
border_color: 0xDDDDDD
clickable: true
scrollable: false
pad_all: 0
on_click:
- script.execute: { id: set_climate_fan_mode, index: 0 }
widgets:
- label:
id: cfan_0_label
align: CENTER
text: "-"
text_font: sublabel
text_color: 0x1B1B1B
clickable: false
- obj:
id: cfan_1
width: 62
height: 22
radius: 11
bg_color: 0xFFFFFF
bg_opa: COVER
border_width: 1
border_color: 0xDDDDDD
clickable: true
scrollable: false
pad_all: 0
on_click:
- script.execute: { id: set_climate_fan_mode, index: 1 }
widgets:
- label:
id: cfan_1_label
align: CENTER
text: "-"
text_font: sublabel
text_color: 0x1B1B1B
clickable: false
- obj:
id: cfan_2
width: 62
height: 22
radius: 11
bg_color: 0xFFFFFF
bg_opa: COVER
border_width: 1
border_color: 0xDDDDDD
clickable: true
scrollable: false
pad_all: 0
on_click:
- script.execute: { id: set_climate_fan_mode, index: 2 }
widgets:
- label:
id: cfan_2_label
align: CENTER
text: "-"
text_font: sublabel
text_color: 0x1B1B1B
clickable: false
- obj:
id: cfan_3
width: 62
height: 22
radius: 11
bg_color: 0xFFFFFF
bg_opa: COVER
border_width: 1
border_color: 0xDDDDDD
clickable: true
scrollable: false
pad_all: 0
on_click:
- script.execute: { id: set_climate_fan_mode, index: 3 }
widgets:
- label:
id: cfan_3_label
align: CENTER
text: "-"
text_font: sublabel
text_color: 0x1B1B1B
clickable: false
- label:
id: cswing_title
width: 100%
height: 11
text: "Swing"
text_font: sublabel
text_color: 0x6B6B6B
text_align: CENTER
clickable: false
- obj:
id: cswing_0
width: 62
height: 22
radius: 11
bg_color: 0xFFFFFF
bg_opa: COVER
border_width: 1
border_color: 0xDDDDDD
clickable: true
scrollable: false
pad_all: 0
on_click:
- script.execute: { id: set_climate_swing_mode, index: 0 }
widgets:
- label:
id: cswing_0_label
align: CENTER
text: "-"
text_font: sublabel
text_color: 0x1B1B1B
clickable: false
- obj:
id: cswing_1
width: 62
height: 22
radius: 11
bg_color: 0xFFFFFF
bg_opa: COVER
border_width: 1
border_color: 0xDDDDDD
clickable: true
scrollable: false
pad_all: 0
on_click:
- script.execute: { id: set_climate_swing_mode, index: 1 }
widgets:
- label:
id: cswing_1_label
align: CENTER
text: "-"
text_font: sublabel
text_color: 0x1B1B1B
clickable: false
- obj:
id: cswing_2
width: 62
height: 22
radius: 11
bg_color: 0xFFFFFF
bg_opa: COVER
border_width: 1
border_color: 0xDDDDDD
clickable: true
scrollable: false
pad_all: 0
on_click:
- script.execute: { id: set_climate_swing_mode, index: 2 }
widgets:
- label:
id: cswing_2_label
align: CENTER
text: "-"
text_font: sublabel
text_color: 0x1B1B1B
clickable: false
- obj:
id: cswing_3
width: 62
height: 22
radius: 11
bg_color: 0xFFFFFF
bg_opa: COVER
border_width: 1
border_color: 0xDDDDDD
clickable: true
scrollable: false
pad_all: 0
on_click:
- script.execute: { id: set_climate_swing_mode, index: 3 }
widgets:
- label:
id: cswing_3_label
align: CENTER
text: "-"
text_font: sublabel
text_color: 0x1B1B1B
clickable: false
- obj:
id: climate_mode_overlay_x
align: TOP_LEFT
x: 10
y: 9
width: 36
height: 36
radius: 999
bg_color: 0xFFFFFF
bg_opa: COVER
border_width: 1
border_color: 0xDDDDDD
border_opa: COVER
clickable: true
scrollable: false
on_click:
- lvgl.widget.hide: climate_mode_overlay
widgets:
- label:
align: CENTER
text: "x"
text_font: sublabel_big
text_color: 0x1B1B1B
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.
- obj:
id: vacuum_overlay
x: 0
y: 0
width: ${DISPLAY_W}
height: ${DISPLAY_H}
hidden: true
bg_color: 0xFFFFFF
bg_opa: 90%
border_width: 0
clickable: true
scrollable: false
scrollbar_mode: 'OFF'
widgets:
- label:
id: vacuum_title
align: TOP_MID
y: 16
text: "Vacuum"
text_font: sublabel_big
text_color: 0x1B1B1B
clickable: false
- obj:
id: vacuum_actions
align: TOP_MID
y: 52
width: 98%
height: 80
bg_opa: TRANSP
border_width: 0
scrollable: false
pad_all: 3
layout:
type: FLEX
flex_flow: ROW_WRAP
flex_align_main: CENTER
flex_align_cross: CENTER
flex_align_track: CENTER
widgets:
- obj:
id: vac_start
width: 72
height: 38
radius: 19
bg_color: 0xF0F0F0
bg_opa: 92%
border_width: 0
clickable: true
scrollable: false
pad_all: 0
on_click:
- script.execute: { id: vacuum_action, service: "vacuum.start" }
widgets:
- label:
align: CENTER
text: "Start"
text_font: sublabel_big
text_color: 0x1B1B1B
clickable: false
- obj:
id: vac_pause
width: 72
height: 38
radius: 19
bg_color: 0xF0F0F0
bg_opa: 92%
border_width: 0
clickable: true
scrollable: false
pad_all: 0
on_click:
- script.execute: { id: vacuum_action, service: "vacuum.pause" }
widgets:
- label:
align: CENTER
text: "Pause"
text_font: sublabel_big
text_color: 0x1B1B1B
clickable: false
- obj:
id: vac_return_to_base
width: 72
height: 38
radius: 19
bg_color: 0xF0F0F0
bg_opa: 92%
border_width: 0
clickable: true
scrollable: false
pad_all: 0
on_click:
- script.execute: { id: vacuum_action, service: "vacuum.return_to_base" }
widgets:
- label:
align: CENTER
text: "Dock"
text_font: sublabel_big
text_color: 0x1B1B1B
clickable: false
- obj:
id: vac_stop
width: 72
height: 38
radius: 19
bg_color: 0xF0F0F0
bg_opa: 92%
border_width: 0
clickable: true
scrollable: false
pad_all: 0
on_click:
- script.execute: { id: vacuum_action, service: "vacuum.stop" }
widgets:
- label:
align: CENTER
text: "Stop"
text_font: sublabel_big
text_color: 0x1B1B1B
clickable: false
- label:
align: TOP_MID
y: 138
text: "Suction"
text_font: sublabel
text_color: 0x1B1B1B
clickable: false
- obj:
id: vacuum_speeds
align: TOP_MID
y: 156
width: 98%
height: 74
bg_opa: TRANSP
border_width: 0
scrollable: false
pad_all: 3
layout:
type: FLEX
flex_flow: ROW_WRAP
flex_align_main: CENTER
flex_align_cross: CENTER
flex_align_track: CENTER
widgets:
- obj:
id: vacspd_quiet
width: 72
height: 32
radius: 19
bg_color: 0xF0F0F0
bg_opa: 92%
border_width: 0
clickable: true
scrollable: false
pad_all: 0
on_click:
- script.execute:
id: vacuum_fan_speed
speed: !lambda 'return runtime_tiles::enabled ? runtime_tiles::vacuum_option(0) : "quiet";'
widgets:
- label:
align: CENTER
text: "Quiet"
text_font: sublabel_big
text_color: 0x1B1B1B
clickable: false
- obj:
id: vacspd_balanced
width: 72
height: 32
radius: 19
bg_color: 0xF0F0F0
bg_opa: 92%
border_width: 0
clickable: true
scrollable: false
pad_all: 0
on_click:
- script.execute:
id: vacuum_fan_speed
speed: !lambda 'return runtime_tiles::enabled ? runtime_tiles::vacuum_option(1) : "balanced";'
widgets:
- label:
align: CENTER
text: "Balanced"
text_font: sublabel_big
text_color: 0x1B1B1B
clickable: false
- obj:
id: vacspd_turbo
width: 72
height: 32
radius: 19
bg_color: 0xF0F0F0
bg_opa: 92%
border_width: 0
clickable: true
scrollable: false
pad_all: 0
on_click:
- script.execute:
id: vacuum_fan_speed
speed: !lambda 'return runtime_tiles::enabled ? runtime_tiles::vacuum_option(2) : "turbo";'
widgets:
- label:
align: CENTER
text: "Turbo"
text_font: sublabel_big
text_color: 0x1B1B1B
clickable: false
- obj:
id: vacspd_max
width: 72
height: 32
radius: 19
bg_color: 0xF0F0F0
bg_opa: 92%
border_width: 0
clickable: true
scrollable: false
pad_all: 0
on_click:
- script.execute:
id: vacuum_fan_speed
speed: !lambda 'return runtime_tiles::enabled ? runtime_tiles::vacuum_option(3) : "max";'
widgets:
- label:
align: CENTER
text: "Max"
text_font: sublabel_big
text_color: 0x1B1B1B
clickable: false
- obj:
id: vacuum_overlay_x
align: TOP_LEFT
x: 10
y: 9
width: 36
height: 36
radius: 999
bg_color: 0xEEEEEE
bg_opa: COVER
border_width: 1
border_color: 0xFFFFFF
border_opa: 20%
clickable: true
scrollable: false
on_click:
- script.execute: close_cards
widgets:
- label:
align: CENTER
text: "x"
text_font: sublabel_big
text_color: 0x1B1B1B
clickable: false
- obj:
id: vacuum_overlay_ok
align: TOP_RIGHT
x: -10
y: 9
width: 36
height: 36
radius: 999
bg_color: 0xD5EEFC
bg_opa: COVER
border_width: 0
clickable: true
scrollable: false
on_click:
- script.execute: close_cards
widgets:
- label:
align: CENTER
text: "\U000F012C"
text_font: materialdesign_icons
text_color: 0x1B1B1B
clickable: false
# ---------- DIM / WAKE OVERLAY ----------
- obj:
id: dim_wake_overlay
x: 0
y: 0
width: ${DISPLAY_W}
height: ${DISPLAY_H}
hidden: true
bg_opa: TRANSP
border_width: 0
scrollable: false
scrollbar_mode: 'OFF'
clickable: true
on_click:
- script.execute: wake_display
- id: touch_cal_page
bg_color: 0x101820
bg_opa: COVER
pad_all: 0
border_width: 0
scrollable: false
widgets:
- label:
align: TOP_MID
y: 65
text: "Follow the calibration wizard"
text_font: sublabel
text_color: 0xFFFFFF
- obj:
x: 10
y: 10
width: 20
height: 20
pad_all: 0
border_width: 0
bg_opa: TRANSP
scrollable: false
widgets:
- label:
align: CENTER
text: "+"
text_font: headline
text_color: 0xFFFFFF
- obj:
x: 289
y: 10
width: 20
height: 20
pad_all: 0
border_width: 0
bg_opa: TRANSP
scrollable: false
widgets:
- label:
align: CENTER
text: "+"
text_font: headline
text_color: 0xFFFFFF
- obj:
x: 289
y: 209
width: 20
height: 20
pad_all: 0
border_width: 0
bg_opa: TRANSP
scrollable: false
widgets:
- label:
align: CENTER
text: "+"
text_font: headline
text_color: 0xFFFFFF
- obj:
x: 10
y: 209
width: 20
height: 20
pad_all: 0
border_width: 0
bg_opa: TRANSP
scrollable: false
widgets:
- label:
align: CENTER
text: "+"
text_font: headline
text_color: 0xFFFFFF
- obj:
x: 150
y: 110
width: 20
height: 20
pad_all: 0
border_width: 0
bg_opa: TRANSP
scrollable: false
widgets:
- label:
align: CENTER
text: "+"
text_font: headline
text_color: 0xFFFFFF
script:
# Render every overlay and both pages repeatedly without controlling devices.
- id: ui_self_test
mode: single
then:
- logger.log: "UI_TEST START: page/overlay render stress, no HA actions"
- lambda: 'ESP_LOGI("ui_test", "Light sliders: %s", light_controls::self_test() ? "PASS" : "FAIL");'
- lambda: 'id(calibration_active) = false;'
- lvgl.page.show: home_page
- script.execute: wake_display
- repeat:
count: 10
then:
- lambda: |-
id(last_touch_ms) = millis();
static unsigned test_page=0;
id(tile_page) = runtime_tiles::enabled ? test_page++ % runtime_tiles::page_count() : 1;
- script.execute: show_tile_page
- delay: 300ms
- lambda: |-
lv_obj_update_layout(id(tile_scroll));
const int count = runtime_tiles::enabled ? runtime_tiles::model.count : std::clamp(atoi("${TILE_COUNT}"), 1, 10);
// Wide cards take two slots, so visibility follows the runtime packing.
const bool paginated = runtime_tiles::enabled ? runtime_tiles::page_count() > 1 : count > 6;
lv_obj_t* tiles[] = {id(tile1), id(tile2), id(tile3), id(tile4), id(tile5),
id(tile6), id(tile7), id(tile8), id(tile9), id(tile10)};
bool ok = true;
for (int i = 0; i < 10; ++i) {
bool visible = runtime_tiles::enabled ? i<6 && runtime_tiles::widgets[i].index<runtime_tiles::model.count : i < count && (paginated ? i >= 6 : i < 6);
ok = ok && (lv_obj_has_flag(tiles[i], LV_OBJ_FLAG_HIDDEN) != visible);
}
for (lv_obj_t* control : {id(page_prev), id(page_next), id(page_number)})
ok = ok && (lv_obj_has_flag(control, LV_OBJ_FLAG_HIDDEN) != paginated);
lv_area_t nav, grid;
lv_obj_get_coords(id(page_next), &nav);
lv_obj_get_coords(id(tile_scroll), &grid);
ok = ok && (!paginated || grid.y2 < nav.y1);
if(runtime_tiles::enabled)ok = runtime_tiles::check_tile_geometry() && ok;
ESP_LOGI("ui_test", "page_check=%s count=%d heap=%u frames=%u grid_bottom=%d nav_top=%d", ok ? "PASS" : "FAIL", count, (unsigned) esp_get_free_heap_size(), (unsigned) id(rendered_frames), grid.y2, nav.y1);
id(tile_page) = 0;
- script.execute: show_tile_page
- delay: 150ms
- lvgl.widget.show: brightness_overlay
- delay: 150ms
- lvgl.widget.hide: brightness_overlay
- lvgl.widget.show: color_detail_overlay
- delay: 150ms
- lvgl.widget.hide: color_detail_overlay
- lvgl.widget.show: climate_detail_overlay
- delay: 150ms
- lvgl.widget.hide: climate_detail_overlay
- lvgl.widget.show: climate_mode_overlay
- delay: 150ms
- lvgl.widget.hide: climate_mode_overlay
- lvgl.widget.show: vacuum_overlay
- delay: 150ms
- lvgl.widget.hide: vacuum_overlay
- lvgl.widget.show: alert_overlay
- delay: 150ms
- lvgl.widget.hide: alert_overlay
- lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();'
- logger.log: "UI_TEST COMPLETE"
- id: show_tile_page
then:
- lambda: |-
if(runtime_tiles::enabled){
runtime_tiles::show_page(id(tile_page),id(page_prev),id(page_next),id(page_number));
return;
}
lv_obj_t* tiles[] = {id(tile1), id(tile2), id(tile3), id(tile4), id(tile5),
id(tile6), id(tile7), id(tile8), id(tile9), id(tile10)};
const int rows[] = {${GRID_ROW1_Y}, ${GRID_ROW2_Y}, ${GRID_ROW3_Y}};
const int count = runtime_tiles::enabled ? runtime_tiles::model.count : std::clamp(atoi("${TILE_COUNT}"), 1, 10);
const bool paginated = count > 6;
if (!paginated) id(tile_page) = 0;
for (lv_obj_t* control : {id(page_prev), id(page_next), id(page_number)}) {
if (paginated) lv_obj_clear_flag(control, LV_OBJ_FLAG_HIDDEN);
else lv_obj_add_flag(control, LV_OBJ_FLAG_HIDDEN);
}
for (int i = 0; i < 10; ++i) {
bool visible = i < count && (id(tile_page) == 0 ? i < 6 : i >= 6);
if (visible) {
lv_obj_set_y(tiles[i], rows[(i < 6 ? i : i - 6) / 2]);
lv_obj_clear_flag(tiles[i], LV_OBJ_FLAG_HIDDEN);
} else {
lv_obj_add_flag(tiles[i], LV_OBJ_FLAG_HIDDEN);
}
}
lv_label_set_text(id(page_number), id(tile_page) == 0 ? "1 / 2" : "2 / 2");
if (id(tile_page) == 0) {
lv_obj_add_state(id(page_prev), LV_STATE_DISABLED);
lv_obj_clear_state(id(page_next), LV_STATE_DISABLED);
} else {
lv_obj_clear_state(id(page_prev), LV_STATE_DISABLED);
lv_obj_add_state(id(page_next), LV_STATE_DISABLED);
}
# Runtime changes do not count as touches and do not reset the idle timer.
- id: apply_screen_settings
mode: restart
then:
- lambda: |-
const auto &settings = screen_settings::current;
if (id(display_dimmed) && !settings.standby_enabled) {
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;
id(back_light).turn_on().set_transition_length(300).set_brightness(level / 100.0f).perform();
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);
// Runtime mode draws the clock in the top bar; the manual profile keeps its own label.
if (!runtime_tiles::enabled) {
if (settings.show_clock) lv_obj_clear_flag(id(lbl_time), LV_OBJ_FLAG_HIDDEN);
else lv_obj_add_flag(id(lbl_time), LV_OBJ_FLAG_HIDDEN);
}
// The four setting entities only publish a changed value (HA keeps identical states anyway).
if (id(setting_brightness).state != settings.brightness) id(setting_brightness).publish_state(settings.brightness);
if (id(setting_standby_brightness).state != settings.standby_brightness) id(setting_standby_brightness).publish_state(settings.standby_brightness);
if (id(setting_night_brightness).state != settings.night_brightness) id(setting_night_brightness).publish_state(settings.night_brightness);
if (id(setting_standby_seconds).state != settings.standby_seconds) id(setting_standby_seconds).publish_state(settings.standby_seconds);
if (!id(ui_refresh).is_running()) id(ui_refresh).execute();
- id: close_cards
then:
- lambda: |-
// Runtime cards and the legacy overlays close through one path; the swipe guard
// reads active_entity, so a hidden card must never leave it behind.
if (runtime_tiles::dismiss) { runtime_tiles::dismiss(); return; }
id(active_entity).clear();
- lvgl.widget.hide: brightness_overlay
- lvgl.widget.hide: color_detail_overlay
- lvgl.widget.hide: climate_detail_overlay
- lvgl.widget.hide: climate_mode_overlay
- lvgl.widget.hide: vacuum_overlay
# ---------- 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");
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());
lv_obj_set_style_bg_color(id(alert_card), lv_color_hex(alert.color), LV_PART_MAIN);
ESP_LOGI("alert", "show \"%s\" timeout=%d flash=%d", alert.title.c_str(), alert.timeout_seconds, (int) alert.flash);
- script.execute: wake_display
- lvgl.widget.show: alert_overlay
- script.stop: alert_timeout
- if:
condition:
lambda: 'return id(alert_flash_requested);'
then:
- script.execute: alert_flash
- if:
condition:
lambda: 'return id(alert_timeout_seconds) > 0;'
then:
- script.execute:
id: alert_timeout
seconds: !lambda 'return id(alert_timeout_seconds);'
- id: alert_dismiss
parameters:
reason: string
then:
- if:
condition:
lambda: 'return id(alert_active);'
then:
- lambda: |-
id(alert_active) = false;
id(last_touch_ms) = millis();
ESP_LOGI("alert", "dismissed: %s", reason.c_str());
- lvgl.widget.hide: alert_overlay
- script.stop: alert_timeout
- script.stop: alert_flash
- light.turn_on:
id: back_light
transition_length: 80ms
brightness: !lambda "return screen_settings::current.brightness / 100.0f;"
- script.execute:
id: alert_report
action: !lambda 'return reason;'
# Four backlight blinks when the alert arrives; the screen then stays on as usual.
- id: alert_flash
mode: restart
then:
- repeat:
count: 4
then:
- light.turn_off:
id: back_light
transition_length: 0s
- delay: 120ms
- light.turn_on:
id: back_light
transition_length: 0s
brightness: !lambda "return screen_settings::current.brightness / 100.0f;"
- delay: 170ms
- id: alert_timeout
mode: restart
parameters:
seconds: int
then:
- delay: !lambda 'return static_cast<uint32_t>(seconds) * 1000UL;'
- script.execute:
id: alert_dismiss
reason: "timeout"
# Home Assistant adds device_id; action is ok, timeout, replaced or remote.
- id: alert_report
parameters:
action: string
then:
- homeassistant.event:
event: esphome.screen_alert
data:
action: !lambda 'return action;'
title: !lambda 'return id(alert_title_text);'
screen: !lambda 'return App.get_name();'
- id: wake_display
mode: restart
then:
- lambda: |-
id(last_touch_ms) = millis();
id(display_dimmed) = false;
- light.turn_on:
id: back_light
# Almost instant brightness-up. ESPHome's default is 1s, and that feels sluggish
# when you touch the screen and are waiting on light.
transition_length: 80ms
brightness: !lambda "return screen_settings::current.brightness / 100.0f;"
- lvgl.widget.hide: dim_wake_overlay
- script.execute: close_cards
- id: dim_display
mode: restart
then:
- if:
condition:
lambda: 'return screen_settings::current.standby_enabled;'
then:
- lambda: |-
id(display_dimmed) = true;
id(apply_screen_settings).execute();
if (screen_settings::current.home_on_standby) {
// Runtime detail cards live above the tile grid, so paging alone cannot close them.
runtime_tiles::hide_detail();
id(active_entity).clear();
id(tile_page) = 0;
id(show_tile_page).execute();
}
# Back to the home screen on standby. Deliberately via ESPHome's
# actions and NOT with lv_obj_add_flag in a lambda: this script
# runs from an interval, so outside LVGL's own handling.
- lvgl.widget.hide: brightness_overlay
- lvgl.widget.hide: color_detail_overlay
- lvgl.widget.hide: climate_detail_overlay
- lvgl.widget.hide: climate_mode_overlay
- lvgl.widget.hide: vacuum_overlay
- id: publish_action
mode: restart
parameters:
action: string
then:
- logger.log:
level: INFO
format: "publish_action: %s"
args: ["action.c_str()"]
- text_sensor.template.publish:
id: smartdisplay_action
state: !lambda 'return action;'
- delay: 500ms
- text_sensor.template.publish:
id: smartdisplay_action
state: ""
- id: climate_target_preview
parameters:
value: int
then:
- lambda: |-
id(climate_edit_ms) = millis();
id(climate_edit_pending) = true;
int min_x10 = id(active_climate_min_x10);
int max_x10 = id(active_climate_max_x10);
value = cyd::quantize_temperature(value, min_x10, max_x10,
id(active_climate_step_x10));
id(active_climate_target_x10) = value;
char buf[16];
snprintf(buf, sizeof(buf), "%.1f°", value / 10.0f);
lv_label_set_text(id(climate_target_label), buf);
lv_arc_set_value(id(climate_arc), value);
- id: climate_target_commit
parameters:
value: int
then:
- script.execute:
id: climate_target_preview
value: !lambda "return value;"
- if:
condition:
lambda: 'return (!runtime_tiles::enabled || runtime_tiles::fresh()) && std::string("${DIRECT_ACTIONS}") == "true";'
then:
- homeassistant.action:
action: climate.set_temperature
data:
entity_id: !lambda "return id(active_entity);"
temperature: !lambda "return id(active_climate_target_x10) / 10.0f;"
- id: climate_adjust_target
parameters:
direction: int
then:
- lambda: |-
int step_x10 = id(active_climate_step_x10);
if (step_x10 < 5) step_x10 = 5;
int next = id(active_climate_target_x10) + (direction * step_x10);
if (next < id(active_climate_min_x10)) next = id(active_climate_min_x10);
if (next > id(active_climate_max_x10)) next = id(active_climate_max_x10);
id(active_climate_target_x10) = next;
- script.execute:
id: climate_target_commit
value: !lambda "return id(active_climate_target_x10);"
# 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.
- id: open_vacuum_overlay
mode: restart
parameters:
title: string
entity: string
then:
- lambda: |-
id(active_entity) = entity;
lv_label_set_text(id(vacuum_title), title.c_str());
- lvgl.widget.show: vacuum_overlay
# A standalone command: start, pause, stop or return_to_base.
- id: vacuum_action
mode: queued
parameters:
service: string
then:
- if:
condition:
lambda: 'return (!runtime_tiles::enabled || runtime_tiles::fresh()) && std::string("${DIRECT_ACTIONS}") == "true";'
then:
- homeassistant.action:
action: !lambda "return service;"
data:
entity_id: !lambda "return id(active_entity);"
# Suction power is a separate axis, apart from start/stop.
- id: vacuum_fan_speed
mode: queued
parameters:
speed: string
then:
- if:
condition:
lambda: 'return (!runtime_tiles::enabled || runtime_tiles::fresh()) && std::string("${DIRECT_ACTIONS}") == "true";'
then:
- homeassistant.action:
action: vacuum.set_fan_speed
data:
entity_id: !lambda "return id(active_entity);"
fan_speed: !lambda "return speed;"
- 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, lv_color_hex(selected ? 0x00A6ED : 0xFFFFFF), LV_PART_MAIN);
lv_obj_set_style_text_color(lv_obj_get_child(btn, 0), lv_color_hex(selected ? 0xFFFFFF : 0x1B1B1B), LV_PART_MAIN);
};
apply(id(cmode_off), "off");
apply(id(cmode_heat), "heat");
apply(id(cmode_cool), "cool");
apply(id(cmode_heat_cool), "heat_cool");
apply(id(cmode_auto), "auto");
apply(id(cmode_dry), "dry");
apply(id(cmode_fan_only), "fan_only");
// Fan and swing rows: the entity's own mode names, at most four each, as Home Assistant reports them.
auto pretty = [](std::string raw) { if (!raw.empty() && raw[0] >= 'a' && raw[0] <= 'z') raw[0] = (char)(raw[0] - 'a' + 'A'); return raw; };
auto fill = [&](lv_obj_t *title, lv_obj_t *chips[4], const std::string &modes, const std::string &current_mode) {
unsigned shown = 0;
for (unsigned i = 0; i < 4; ++i) {
std::string raw = cyd::list_item(modes, i);
if (raw.empty()) { lv_obj_add_flag(chips[i], LV_OBJ_FLAG_HIDDEN); continue; }
lv_obj_clear_flag(chips[i], LV_OBJ_FLAG_HIDDEN); ++shown;
lv_label_set_text(lv_obj_get_child(chips[i], 0), pretty(raw).c_str());
lv_obj_set_style_bg_color(chips[i], lv_color_hex(raw == current_mode ? 0x00A6ED : 0xFFFFFF), LV_PART_MAIN);
lv_obj_set_style_text_color(lv_obj_get_child(chips[i], 0), lv_color_hex(raw == current_mode ? 0xFFFFFF : 0x1B1B1B), LV_PART_MAIN);
}
if (shown) lv_obj_clear_flag(title, LV_OBJ_FLAG_HIDDEN); else lv_obj_add_flag(title, LV_OBJ_FLAG_HIDDEN);
};
lv_obj_t *fan_chips[4] = {id(cfan_0), id(cfan_1), id(cfan_2), id(cfan_3)};
lv_obj_t *swing_chips[4] = {id(cswing_0), id(cswing_1), id(cswing_2), id(cswing_3)};
fill(id(cfan_title), fan_chips, id(active_climate_fan_modes), id(active_climate_fan_mode));
fill(id(cswing_title), swing_chips, id(active_climate_swing_modes), id(active_climate_swing_mode));
- lvgl.widget.show: climate_mode_overlay
# Sets exactly one mode. No intermediate steps, so no unwanted
# commands to the heat pump along the way.
# Fan and swing modes from the picker: one call with the entity's own mode name.
- id: set_climate_fan_mode
mode: restart
parameters:
index: int
then:
- lambda: |-
id(active_climate_fan_mode) = cyd::list_item(id(active_climate_fan_modes), index);
id(climate_edit_ms) = millis();
id(climate_edit_pending) = true;
- lvgl.widget.hide: climate_mode_overlay
- if:
condition:
lambda: 'return (!runtime_tiles::enabled || runtime_tiles::fresh()) && std::string("${DIRECT_ACTIONS}") == "true" && !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;
- lvgl.widget.hide: climate_mode_overlay
- if:
condition:
lambda: 'return (!runtime_tiles::enabled || runtime_tiles::fresh()) && std::string("${DIRECT_ACTIONS}") == "true" && !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"; lv_label_set_text(id(climate_mode_label), "Off"); }
id(climate_power_refresh).execute();
- if:
condition:
lambda: 'return (!runtime_tiles::enabled || runtime_tiles::fresh()) && std::string("${DIRECT_ACTIONS}") == "true" && 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::enabled || runtime_tiles::fresh()) && std::string("${DIRECT_ACTIONS}") == "true" && !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), lv_color_hex(on ? 0xD5EEFC : 0xEEEEEE), LV_PART_MAIN);
lv_obj_set_style_text_color(id(climate_power_icon), lv_color_hex(on ? 0x0B6E99 : 0x8A8A8A), 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;
id(active_climate_power_on) = mode != "off";
id(climate_power_refresh).execute();
lv_label_set_text(id(climate_mode_label), tile_controls::climate_mode_text(mode));
- lvgl.widget.hide: climate_mode_overlay
- if:
condition:
lambda: 'return (!runtime_tiles::enabled || runtime_tiles::fresh()) && std::string("${DIRECT_ACTIONS}") == "true";'
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.hide: color_done_button
- lvgl.widget.show: color_detail_overlay
- delay: 40s
- lvgl.widget.hide: color_detail_overlay
- lvgl.widget.show: color_close_button
- lvgl.widget.show: color_done_button
- lambda: 'light_controls::demo = false; light_controls::fallback = light_controls::State{};'
- id: open_color_detail
mode: restart
then:
- lambda: |-
lv_label_set_text(id(color_detail_title), id(active_title).c_str());
light_controls::open(id(active_entity), id(active_supports_color), id(active_supports_temperature), id(active_brightness_pct));
- lvgl.widget.hide: brightness_overlay
- lvgl.widget.show: color_detail_overlay
- id: color_brightness_commit
parameters:
value: int
then:
- lambda: 'id(active_brightness_pct) = value;'
- if:
condition:
lambda: 'return (!runtime_tiles::enabled || runtime_tiles::fresh()) && std::string("${DIRECT_ACTIONS}") == "true" && 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::enabled || runtime_tiles::fresh()) && std::string("${DIRECT_ACTIONS}") == "true" && 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::enabled || runtime_tiles::fresh()) && std::string("${DIRECT_ACTIONS}") == "true" && id(active_supports_temperature) && id(active_entity).rfind("light.", 0) == 0 && light_controls::active->temperature_ready();'
then:
- homeassistant.action:
action: light.turn_on
data:
entity_id: !lambda "return id(active_entity);"
color_temp_kelvin: !lambda "return light_controls::clamp(kelvin, light_controls::active->minimum, light_controls::active->maximum);"
- id: slider_preview
parameters:
value: int
then:
- lambda: |-
char buf[20];
snprintf(buf, sizeof(buf), "%d%s", value, id(active_value_suffix).c_str());
lv_label_set_text(id(overlay_value), buf);
int pct = value;
if (pct < 0) pct = 0;
if (pct > 100) pct = 100;
std::string value_text;
if (id(active_control_kind) == "cover_position") {
std::string closed_label = id(active_label_off).c_str();
std::string open_label = id(active_label_on).c_str();
if (pct <= 0) {
value_text = closed_label;
} else if (pct >= 100) {
value_text = open_label;
} else {
char buf[40];
snprintf(buf, sizeof(buf), "%s - %d%%", open_label.c_str(), pct);
value_text = buf;
}
} else {
if (pct <= 0) {
value_text = id(active_label_off).c_str();
} else {
char buf[20];
snprintf(buf, sizeof(buf), "%d%s", pct, id(active_value_suffix).c_str());
value_text = buf;
}
}
lv_label_set_text(id(overlay_value), value_text.c_str());
- id: slider_commit
parameters:
value: int
then:
- if:
condition:
lambda: |-
return id(active_control_kind) == "light_brightness";
then:
- homeassistant.action:
action: light.turn_on
data:
entity_id: !lambda "return id(active_entity);"
brightness_pct: !lambda "return value;"
else:
- if:
condition:
lambda: |-
return id(active_control_kind) == "fan_percentage";
then:
- if:
condition:
lambda: "return value <= 0;"
then:
- homeassistant.action:
action: fan.turn_off
data:
entity_id: !lambda "return id(active_entity);"
else:
- homeassistant.action:
action: fan.turn_on
data:
entity_id: !lambda "return id(active_entity);"
- delay: 150ms
- homeassistant.action:
action: fan.set_percentage
data:
entity_id: !lambda "return id(active_entity);"
percentage: !lambda "return value;"
else:
- if:
condition:
lambda: |-
return id(active_control_kind) == "cover_position";
then:
- homeassistant.action:
action: cover.set_cover_position
data:
entity_id: !lambda "return id(active_entity);"
position: !lambda "return value;"
# ----------------------------------------------------------
# do_tile_action
#
# Centralized handler for executing Home Assistant actions
# triggered by tile taps.
#
# Parameters
# - tile_type: light | fan | switch | scene | script | cover
# - tap_action: auto | toggle | activate | fan_toggle_preset
# - service: optional Home Assistant service override
# - entity_id: target entity
# - param_key / param_val: optional parameters for custom services
# - was_on: previous entity state (pass false for longpress actions)
#
# Behavior
#
# toggle
# -> toggles the entity
# light.toggle / fan.toggle / switch.toggle / cover.open_cover|close_cover (based on was_on)
#
# activate
# -> activates entity
# scene.turn_on / script.turn_on / light.turn_on / fan.turn_on / cover.open_cover
#
# auto
# -> resolves automatically based on tile_type:
# light/switch/fan/cover -> toggle
# scene/script -> activate
#
# fan_toggle_preset
# -> if fan was ON: fan.turn_off
# -> if fan was OFF:
# fan.turn_on
# wait 150ms
# fan.set_preset_mode(param_val)
#
# Custom service
# If "service" is set, it will be called directly.
# param_key / param_val may be used for parameters
# like preset_mode, brightness_pct, percentage, etc.
# ----------------------------------------------------------
- id: do_tile_action
mode: queued
parameters:
tile_type: string
tap_action: string
service: string
entity_id: string
param_key: string
param_val: string
was_on: bool
then:
- if:
condition:
lambda: |-
if (std::string("${DIRECT_ACTIONS}") != "true") return false;
// Debounce: same entity within 600ms ignored. Prevents
// touch noise from firing the same HA action multiple times
// (see globals above for why this is needed).
uint32_t now = millis();
if (entity_id == id(last_action_entity) && (now - id(last_action_ms)) < 600) {
ESP_LOGD("tile", "do_tile_action: debounced repeat for %s (%ums after previous)",
entity_id.c_str(), now - id(last_action_ms));
return false;
}
id(last_action_entity) = entity_id;
id(last_action_ms) = now;
return true;
then:
- lambda: |-
ESP_LOGD("tile", "do_tile_action type=%s action=%s service=%s entity=%s key=%s val=%s was_on=%d",
tile_type.c_str(), tap_action.c_str(), service.c_str(), entity_id.c_str(),
param_key.c_str(), param_val.c_str(), (int)was_on);
- if:
condition:
lambda: |-
return tap_action == "fan_toggle_preset";
then:
- if:
condition:
lambda: |-
return was_on;
then:
- homeassistant.action:
action: fan.turn_off
data:
entity_id: !lambda 'return entity_id;'
else:
- homeassistant.action:
action: fan.turn_on
data:
entity_id: !lambda 'return entity_id;'
- delay: 150ms
- homeassistant.action:
action: fan.set_preset_mode
data:
entity_id: !lambda 'return entity_id;'
preset_mode: !lambda 'return param_val;'
else:
- if:
condition:
lambda: |-
std::string resolved = tap_action;
if (resolved == "auto") {
if (tile_type == "scene" || tile_type == "script") resolved = "activate";
else resolved = "toggle";
}
return resolved == "toggle";
then:
- if:
condition:
lambda: |-
return service.size() > 0;
then:
- homeassistant.action:
action: !lambda 'return service;'
data:
entity_id: !lambda 'return entity_id;'
else:
- if:
condition:
lambda: |-
return tile_type == "light";
then:
- homeassistant.action:
action: light.toggle
data:
entity_id: !lambda 'return entity_id;'
else:
- if:
condition:
lambda: |-
return tile_type == "fan";
then:
- homeassistant.action:
action: fan.toggle
data:
entity_id: !lambda 'return entity_id;'
else:
- if:
condition:
lambda: |-
return tile_type == "switch";
then:
- homeassistant.action:
action: switch.toggle
data:
entity_id: !lambda 'return entity_id;'
else:
- if:
condition:
lambda: |-
return tile_type == "cover";
then:
- homeassistant.action:
action: cover.set_cover_position
data:
entity_id: !lambda 'return entity_id;'
position: !lambda |-
return was_on ? 0 : 100;
else:
- if:
condition:
lambda: |-
std::string resolved = tap_action;
if (resolved == "auto") {
if (tile_type == "scene" || tile_type == "script") resolved = "activate";
else resolved = "toggle";
}
return resolved == "activate";
then:
- if:
condition:
lambda: |-
return service.size() > 0;
then:
- if:
condition:
lambda: |-
return param_key == "preset_mode";
then:
- homeassistant.action:
action: !lambda 'return service;'
data:
entity_id: !lambda 'return entity_id;'
preset_mode: !lambda 'return param_val;'
else:
- if:
condition:
lambda: |-
return param_key == "brightness_pct";
then:
- homeassistant.action:
action: !lambda 'return service;'
data:
entity_id: !lambda 'return entity_id;'
brightness_pct: !lambda 'return atoi(param_val.c_str());'
else:
- if:
condition:
lambda: |-
return param_key == "percentage";
then:
- homeassistant.action:
action: !lambda 'return service;'
data:
entity_id: !lambda 'return entity_id;'
percentage: !lambda 'return atoi(param_val.c_str());'
else:
- if:
condition:
lambda: |-
return param_key == "option";
then:
- homeassistant.action:
action: !lambda 'return service;'
data:
entity_id: !lambda 'return entity_id;'
option: !lambda 'return param_val;'
else:
- homeassistant.action:
action: !lambda 'return service;'
data:
entity_id: !lambda 'return entity_id;'
else:
- if:
condition:
lambda: |-
return tile_type == "scene";
then:
- homeassistant.action:
action: scene.turn_on
data:
entity_id: !lambda 'return entity_id;'
else:
- if:
condition:
lambda: |-
return tile_type == "script";
then:
- homeassistant.action:
action: script.turn_on
data:
entity_id: !lambda 'return entity_id;'
else:
- if:
condition:
lambda: |-
return tile_type == "light";
then:
- homeassistant.action:
action: light.turn_on
data:
entity_id: !lambda 'return entity_id;'
else:
- if:
condition:
lambda: |-
return tile_type == "fan";
then:
- homeassistant.action:
action: fan.turn_on
data:
entity_id: !lambda 'return entity_id;'
else:
- if:
condition:
lambda: |-
return tile_type == "switch";
then:
- homeassistant.action:
action: switch.turn_on
data:
entity_id: !lambda 'return entity_id;'
else:
- if:
condition:
lambda: |-
return tile_type == "cover";
then:
- homeassistant.action:
action: cover.open_cover
data:
entity_id: !lambda 'return entity_id;'
- 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
slider_bg_color: uint32_t
slider_fill_color: uint32_t
slider_icon_color: uint32_t
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: |-
// Accent colour from the Home Assistant domain: the part before the dot
// in the entity_id. script/scene/button fire (ACCENT_ACTION), a
// light is a light (ACCENT_LIGHT), the rest is a device.
// NOTE: this helper appears twice in this file — here and in
// open_value_overlay. Update them together.
auto accent_for = [](const std::string& entity_id) -> uint32_t {
std::string d = entity_id.substr(0, entity_id.find('.'));
if (d == "script" || d == "scene" || d == "button" ||
d == "input_button" || d == "automation") return ${ACCENT_ACTION};
if (d == "light") return ${ACCENT_LIGHT};
return ${ACCENT_DEVICE};
};
// The slider colour comes from the tile, so it can also be ${ACCENT_AUTO}.
uint32_t fill_color = (slider_fill_color == ${ACCENT_AUTO})
? accent_for(entity) : slider_fill_color;
std::string mode = longpress_mode;
auto contains_mode = [](const std::string& modes, const std::string& needle) {
return modes.find(needle) != std::string::npos;
};
if (mode == "auto") {
if (tile_type == "light") mode = "brightness";
else if (tile_type == "fan") mode = "percentage";
else if (tile_type == "cover") mode = "cover_position";
else if (tile_type == "climate") mode = "climate_temperature";
else mode = "none";
}
if (mode == "none") return;
id(active_entity) = entity;
id(active_value_suffix) = " %";
id(active_label_off) = label_off;
id(active_label_on) = label_on;
id(active_title) = title;
id(active_icon_text) = icon_text;
id(active_supports_color) = false;
id(active_supports_temperature) = false;
lv_obj_add_flag(id(overlay_color_button), LV_OBJ_FLAG_HIDDEN);
if (mode == "climate_temperature") {
auto to_x10 = [](float value, int fallback) -> int {
if (isnan(value)) return fallback;
return (int) lround(value * 10.0f);
};
auto parse_x10 = [](const std::string& value, int fallback) -> int {
if (value.empty()) return fallback;
return (int) lround(atof(value.c_str()) * 10.0f);
};
id(climate_edit_pending) = false;
id(active_control_kind) = "climate_temperature";
id(active_climate_min_x10) = parse_x10(climate_min_temp, 50);
id(active_climate_max_x10) = parse_x10(climate_max_temp, 300);
id(active_climate_step_x10) = parse_x10(climate_step, 5);
if (id(active_climate_step_x10) < 5) id(active_climate_step_x10) = 5;
if (id(active_climate_max_x10) <= id(active_climate_min_x10)) {
id(active_climate_min_x10) = 50;
id(active_climate_max_x10) = 300;
}
id(active_climate_current_x10) = to_x10(climate_current_temperature, 0);
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());
char current_buf[48];
if (isnan(climate_current_temperature)) {
snprintf(current_buf, sizeof(current_buf), "Now: --");
} else if (id(active_climate_humidity) >= 0) {
snprintf(current_buf, sizeof(current_buf), "Now: %.1f° · %d%%", climate_current_temperature, id(active_climate_humidity));
} else {
snprintf(current_buf, sizeof(current_buf), "Now: %.1f°", climate_current_temperature);
}
lv_label_set_text(id(climate_current_label), current_buf);
char target_buf[16];
snprintf(target_buf, sizeof(target_buf), "%.1f°", id(active_climate_target_x10) / 10.0f);
lv_label_set_text(id(climate_target_label), target_buf);
lv_label_set_text(id(climate_mode_label), tile_controls::climate_mode_text(id(active_climate_mode)));
id(climate_power_refresh).execute();
lv_arc_set_range(id(climate_arc), id(active_climate_min_x10), id(active_climate_max_x10));
lv_arc_set_value(id(climate_arc), id(active_climate_target_x10));
if (atoi("${DISPLAY_W}") < atoi("${DISPLAY_H}")) {
lv_obj_set_size(id(climate_arc), 196, 196);
lv_obj_set_y(id(climate_arc), 8);
lv_obj_set_y(id(climate_current_label), 48);
lv_obj_set_y(id(climate_minus_button), -62);
lv_obj_set_y(id(climate_plus_button), -62);
lv_obj_set_y(id(climate_mode_pill), -18);
} else {
lv_obj_set_size(id(climate_arc), 176, 176);
lv_obj_set_y(id(climate_arc), 18);
lv_obj_set_y(id(climate_current_label), 40);
lv_obj_set_y(id(climate_minus_button), -14);
lv_obj_set_y(id(climate_plus_button), -14);
lv_obj_set_y(id(climate_mode_pill), -15);
}
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" && std::string("${DIRECT_ACTIONS}") == "true") {
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);
}
}
if (tile_type == "cover") {
lv_obj_set_style_bg_color(id(overlay_slider), lv_color_hex(fill_color), LV_PART_MAIN);
lv_obj_set_style_bg_color(id(overlay_slider), lv_color_hex(slider_bg_color), LV_PART_INDICATOR);
lv_obj_set_style_bg_opa(id(overlay_slider), LV_OPA_COVER, LV_PART_MAIN);
}
else{
lv_obj_set_style_bg_color(id(overlay_slider), lv_color_hex(slider_bg_color), LV_PART_MAIN);
lv_obj_set_style_bg_color(id(overlay_slider), lv_color_hex(fill_color), LV_PART_INDICATOR);
lv_obj_set_style_bg_opa(id(overlay_slider), LV_OPA_COVER, LV_PART_MAIN);
}
lv_obj_set_style_radius(id(overlay_slider), 28, LV_PART_MAIN);
lv_obj_set_style_radius(id(overlay_slider), 0, LV_PART_INDICATOR);
lv_label_set_text(id(overlay_title), title.c_str());
lv_label_set_text(id(overlay_icon), icon_text.c_str());
lv_slider_set_value(id(overlay_slider), pct, LV_ANIM_OFF);
std::string value_text;
if (tile_type == "cover") {
if (pct <= 0) {
value_text = label_off;
} else if (pct >= 100) {
value_text = label_on;
} else {
char buf[40];
snprintf(buf, sizeof(buf), "%s - %d%%", label_on.c_str(), pct);
value_text = buf;
}
} else {
if (pct <= 0) {
value_text = label_off;
} else {
char buf[20];
snprintf(buf, sizeof(buf), "%d%s", pct, id(active_value_suffix).c_str());
value_text = buf;
}
}
lv_label_set_text(id(overlay_value), value_text.c_str());
lv_obj_set_style_bg_opa(id(overlay_slider), LV_OPA_COVER, LV_PART_INDICATOR);
lv_obj_set_style_text_color(id(overlay_icon), lv_color_hex(slider_icon_color), 0);
- 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;
}
auto set_lbl = [](lv_obj_t* lbl, const std::string& s) {
if (lbl && s != lv_label_get_text(lbl)) lv_label_set_text(lbl, s.c_str());
};
auto set_value_auto = [&](lv_obj_t* lbl,
bool on,
const std::string& tile_type,
const std::string& value_mode,
float brightness,
float percentage,
const std::string& preset,
float climate_current_temperature,
float climate_target_temperature,
float climate_humidity,
const std::string& label_on,
const std::string& label_off) {
std::string mode = value_mode;
if (mode == "auto" || mode.empty()) {
if (tile_type == "light") {
mode = "brightness";
} else if (tile_type == "fan") {
mode = preset.empty() ? "percentage" : "preset";
} else if (tile_type == "climate") {
mode = "climate";
} else {
mode = "text";
}
}
if (mode == "brightness") {
if (!on) {
set_lbl(lbl, label_off);
return;
}
if (isnan(brightness)) {
set_lbl(lbl, "100 %");
return;
}
int pct = (int) lround((brightness / 255.0f) * 100.0f);
char buf[8];
snprintf(buf, sizeof(buf), "%d %%", pct);
set_lbl(lbl, std::string(buf));
return;
}
if (mode == "percentage") {
if (!on) {
set_lbl(lbl, label_off);
return;
}
int pct = isnan(percentage) ? 0 : (int) lround(percentage);
char buf[8];
snprintf(buf, sizeof(buf), "%d %%", pct);
set_lbl(lbl, std::string(buf));
return;
}
if (mode == "preset") {
if (!on) {
set_lbl(lbl, label_off);
return;
}
if (!preset.empty()) {
set_lbl(lbl, preset);
} else {
set_lbl(lbl, label_on);
}
return;
}
if (mode == "climate") {
if (!on) {
set_lbl(lbl, label_off);
return;
}
float temp = isnan(climate_target_temperature) ? climate_current_temperature : climate_target_temperature;
if (isnan(temp)) {
set_lbl(lbl, label_on);
return;
}
char buf[16];
snprintf(buf, sizeof(buf), "%.1f °", temp);
set_lbl(lbl, std::string(buf));
return;
}
if (mode == "text") {
if (tile_type == "scene" || tile_type == "script") {
set_lbl(lbl, label_on);
} else {
set_lbl(lbl, on ? label_on : label_off);
}
return;
}
set_lbl(lbl, "—");
};
// active_state not empty? Then only that exact HA state counts as "on".
// This way an input_select ("Full"/"Empty") can also light up a tile.
auto is_active = [](const std::string& tile_type, const std::string& state,
const std::string& active_state = std::string()) {
if (!active_state.empty()) return state == active_state;
if (tile_type == "climate") {
return !state.empty() && state != "off" && state != "unavailable" && state != "unknown";
}
if (tile_type == "vacuum") return state == "cleaning" || state == "returning";
if (tile_type == "scene" || tile_type == "script") return true;
return state == "on";
};
// Cover-aware active state: "closed" = off, anything else (open/opening/closing) = on
bool t1_on = (std::string("${TILE1_TYPE}") == "cover") ? (id(tile1_cover_state).state != "closed") : is_active("${TILE1_TYPE}", std::string(id(ha_state_tile1).state.c_str()), "${TILE1_ACTIVE_STATE}");
bool t2_on = (std::string("${TILE2_TYPE}") == "cover") ? (id(tile2_cover_state).state != "closed") : is_active("${TILE2_TYPE}", std::string(id(ha_state_tile2).state.c_str()), "${TILE2_ACTIVE_STATE}");
bool t3_on = (std::string("${TILE3_TYPE}") == "cover") ? (id(tile3_cover_state).state != "closed") : is_active("${TILE3_TYPE}", std::string(id(ha_state_tile3).state.c_str()), "${TILE3_ACTIVE_STATE}");
bool t4_on = (std::string("${TILE4_TYPE}") == "cover") ? (id(tile4_cover_state).state != "closed") : is_active("${TILE4_TYPE}", std::string(id(ha_state_tile4).state.c_str()), "${TILE4_ACTIVE_STATE}");
bool t5_on = (std::string("${TILE5_TYPE}") == "cover") ? (id(tile5_cover_state).state != "closed") : is_active("${TILE5_TYPE}", std::string(id(ha_state_tile5).state.c_str()), "${TILE5_ACTIVE_STATE}");
bool t6_on = (std::string("${TILE6_TYPE}") == "cover") ? (id(tile6_cover_state).state != "closed") : is_active("${TILE6_TYPE}", std::string(id(ha_state_tile6).state.c_str()), "${TILE6_ACTIVE_STATE}");
// Tile 7/8 have no cover_state sensor, so no cover branch needed.
bool t7_on = is_active("${TILE7_TYPE}", std::string(id(ha_state_tile7).state.c_str()), "${TILE7_ACTIVE_STATE}");
bool t8_on = is_active("${TILE8_TYPE}", std::string(id(ha_state_tile8).state.c_str()), "${TILE8_ACTIVE_STATE}");
bool t9_on = is_active("${TILE9_TYPE}", std::string(id(ha_state_tile9).state.c_str()), "${TILE9_ACTIVE_STATE}");
bool t10_on = is_active("${TILE10_TYPE}", std::string(id(ha_state_tile10).state.c_str()), "${TILE10_ACTIVE_STATE}");
set_value_auto(id(t1_value), t1_on, "${TILE1_TYPE}", "${TILE1_VALUE_MODE}",
id(tile1_brightness).state, id(tile1_percentage).state, std::string(id(tile1_preset).state.c_str()),
id(tile1_climate_current_temperature).state, id(tile1_climate_target_temperature).state, id(tile1_climate_humidity).state,
"${TILE1_LABEL_ON}", "${TILE1_LABEL_OFF}");
set_value_auto(id(t2_value), t2_on, "${TILE2_TYPE}", "${TILE2_VALUE_MODE}",
id(tile2_brightness).state, id(tile2_percentage).state, std::string(id(tile2_preset).state.c_str()),
id(tile2_climate_current_temperature).state, id(tile2_climate_target_temperature).state, id(tile2_climate_humidity).state,
"${TILE2_LABEL_ON}", "${TILE2_LABEL_OFF}");
set_value_auto(id(t3_value), t3_on, "${TILE3_TYPE}", "${TILE3_VALUE_MODE}",
id(tile3_brightness).state, id(tile3_percentage).state, std::string(id(tile3_preset).state.c_str()),
id(tile3_climate_current_temperature).state, id(tile3_climate_target_temperature).state, id(tile3_climate_humidity).state,
"${TILE3_LABEL_ON}", "${TILE3_LABEL_OFF}");
set_value_auto(id(t4_value), t4_on, "${TILE4_TYPE}", "${TILE4_VALUE_MODE}",
id(tile4_brightness).state, id(tile4_percentage).state, std::string(id(tile4_preset).state.c_str()),
id(tile4_climate_current_temperature).state, id(tile4_climate_target_temperature).state, id(tile4_climate_humidity).state,
"${TILE4_LABEL_ON}", "${TILE4_LABEL_OFF}");
set_value_auto(id(t5_value), t5_on, "${TILE5_TYPE}", "${TILE5_VALUE_MODE}",
id(tile5_brightness).state, id(tile5_percentage).state, std::string(id(tile5_preset).state.c_str()),
id(tile5_climate_current_temperature).state, id(tile5_climate_target_temperature).state, id(tile5_climate_humidity).state,
"${TILE5_LABEL_ON}", "${TILE5_LABEL_OFF}");
set_value_auto(id(t6_value), t6_on, "${TILE6_TYPE}", "${TILE6_VALUE_MODE}",
id(tile6_brightness).state, id(tile6_percentage).state, std::string(id(tile6_preset).state.c_str()),
id(tile6_climate_current_temperature).state, id(tile6_climate_target_temperature).state, id(tile6_climate_humidity).state,
"${TILE6_LABEL_ON}", "${TILE6_LABEL_OFF}");
// Tile 7: light with only brightness. Tile 8: pure text (Full/Empty).
set_value_auto(id(t7_value), t7_on, "${TILE7_TYPE}", "${TILE7_VALUE_MODE}",
id(tile7_brightness).state, NAN, std::string(),
NAN, NAN, NAN,
"${TILE7_LABEL_ON}", "${TILE7_LABEL_OFF}");
set_value_auto(id(t8_value), t8_on, "${TILE8_TYPE}", "${TILE8_VALUE_MODE}",
NAN, NAN, std::string(),
NAN, NAN, NAN,
"${TILE8_LABEL_ON}", "${TILE8_LABEL_OFF}");
// Tile 9: colour lamp (brightness). Tile 10: pure text (On/Off).
set_value_auto(id(t9_value), t9_on, "${TILE9_TYPE}", "${TILE9_VALUE_MODE}",
id(tile9_brightness).state, NAN, std::string(),
NAN, NAN, NAN,
"${TILE9_LABEL_ON}", "${TILE9_LABEL_OFF}");
set_value_auto(id(t10_value), t10_on, "${TILE10_TYPE}", "${TILE10_VALUE_MODE}",
NAN, NAN, std::string(),
NAN, NAN, NAN,
"${TILE10_LABEL_ON}", "${TILE10_LABEL_OFF}");
// Missing state is not an off state. Preserve meaningful vacuum states.
const char* vacuum_state = id(ha_state_tile6).state.c_str();
if (std::string("${TILE6_TYPE}") == "vacuum") {
std::string state(vacuum_state);
if (state == "cleaning") set_lbl(id(t6_value), "Cleaning");
else if (state == "docked") set_lbl(id(t6_value), "Docked");
else if (state == "returning") set_lbl(id(t6_value), "Returning to dock");
else if (state == "paused") set_lbl(id(t6_value), "Paused");
else if (state == "error") set_lbl(id(t6_value), "Error");
else if (!state.empty()) set_lbl(id(t6_value), state);
}
if (!id(ha_state_tile1).has_state() || id(ha_state_tile1).state == "unknown" || id(ha_state_tile1).state == "unavailable")
set_lbl(id(t1_value), "Unavailable");
if (!id(ha_state_tile2).has_state() || id(ha_state_tile2).state == "unknown" || id(ha_state_tile2).state == "unavailable")
set_lbl(id(t2_value), "Unavailable");
if (!id(ha_state_tile3).has_state() || id(ha_state_tile3).state == "unknown" || id(ha_state_tile3).state == "unavailable")
set_lbl(id(t3_value), "Unavailable");
if (!id(ha_state_tile4).has_state() || id(ha_state_tile4).state == "unknown" || id(ha_state_tile4).state == "unavailable")
set_lbl(id(t4_value), "Unavailable");
if (!id(ha_state_tile5).has_state() || id(ha_state_tile5).state == "unknown" || id(ha_state_tile5).state == "unavailable")
set_lbl(id(t5_value), "Unavailable");
if (!id(ha_state_tile6).has_state() || id(ha_state_tile6).state == "unknown" || id(ha_state_tile6).state == "unavailable")
set_lbl(id(t6_value), "Unavailable");
if (!id(ha_state_tile7).has_state() || id(ha_state_tile7).state == "unknown" || id(ha_state_tile7).state == "unavailable")
set_lbl(id(t7_value), "Unavailable");
if (!id(ha_state_tile8).has_state() || id(ha_state_tile8).state == "unknown" || id(ha_state_tile8).state == "unavailable")
set_lbl(id(t8_value), "Unavailable");
if (!id(ha_state_tile9).has_state() || id(ha_state_tile9).state == "unknown" || id(ha_state_tile9).state == "unavailable")
set_lbl(id(t9_value), "Unavailable");
if (!id(ha_state_tile10).has_state() || id(ha_state_tile10).state == "unknown" || id(ha_state_tile10).state == "unavailable")
set_lbl(id(t10_value), "Unavailable");
if (id(active_control_kind) == "climate_temperature" && !lv_obj_has_flag(id(climate_detail_overlay), LV_OBJ_FLAG_HIDDEN)) {
auto label_for_climate_mode = [](std::string mode) {
for (char &c : mode) {
if (c >= 'A' && c <= 'Z') c = (char) (c - 'A' + 'a');
}
if (mode == "off") return std::string("Off");
if (mode == "heat") return std::string("Heat");
if (mode == "cool") return std::string("Cool");
if (mode == "heat_cool") return std::string("Heat/Cool");
if (mode == "auto") return std::string("Auto");
if (mode == "dry") return std::string("Dry");
if (mode == "fan_only") return std::string("Fan");
return mode.empty() ? std::string("Auto") : mode;
};
auto sync_open_climate = [&](const std::string& entity,
float current_temperature,
float target_temperature,
float humidity,
const std::string& climate_mode) {
if (id(active_entity) != entity) return;
auto to_x10 = [](float value, int fallback) -> int {
if (isnan(value)) return fallback;
return (int) lround(value * 10.0f);
};
id(active_climate_current_x10) = to_x10(current_temperature, id(active_climate_current_x10));
id(active_climate_humidity) = isnan(humidity) ? -1 : (int) lround(humidity);
if (!climate_mode.empty() && !id(touch_down) &&
!(id(climate_edit_pending) && millis() - id(climate_edit_ms) < 3000)) {
id(active_climate_mode) = climate_mode;
lv_label_set_text(id(climate_mode_label), label_for_climate_mode(climate_mode).c_str());
}
const bool editing = id(touch_down) ||
(id(climate_edit_pending) && millis() - id(climate_edit_ms) < 3000);
if (!editing) id(climate_edit_pending) = false;
int target_x10 = editing ? id(active_climate_target_x10) :
to_x10(target_temperature, id(active_climate_target_x10));
if (target_x10 < id(active_climate_min_x10)) target_x10 = id(active_climate_min_x10);
if (target_x10 > id(active_climate_max_x10)) target_x10 = id(active_climate_max_x10);
int step_x10 = id(active_climate_step_x10);
if (step_x10 < 5) step_x10 = 5;
target_x10 = id(active_climate_min_x10) + (int) lround((target_x10 - id(active_climate_min_x10)) / (float) step_x10) * step_x10;
if (target_x10 < id(active_climate_min_x10)) target_x10 = id(active_climate_min_x10);
if (target_x10 > id(active_climate_max_x10)) target_x10 = id(active_climate_max_x10);
id(active_climate_target_x10) = target_x10;
char current_buf[48];
if (isnan(current_temperature)) {
snprintf(current_buf, sizeof(current_buf), "Now: --");
} else if (id(active_climate_humidity) >= 0) {
snprintf(current_buf, sizeof(current_buf), "Now: %.1f° · %d%%", current_temperature, id(active_climate_humidity));
} else {
snprintf(current_buf, sizeof(current_buf), "Now: %.1f°", current_temperature);
}
lv_label_set_text(id(climate_current_label), current_buf);
char target_buf[16];
snprintf(target_buf, sizeof(target_buf), "%.1f°", id(active_climate_target_x10) / 10.0f);
lv_label_set_text(id(climate_target_label), target_buf);
lv_arc_set_value(id(climate_arc), id(active_climate_target_x10));
};
if (std::string("${TILE1_TYPE}") == "climate") sync_open_climate("${TILE1_ENTITY}", id(tile1_climate_current_temperature).state, id(tile1_climate_target_temperature).state, id(tile1_climate_humidity).state, std::string(id(tile1_climate_mode).state.c_str()));
if (std::string("${TILE2_TYPE}") == "climate") sync_open_climate("${TILE2_ENTITY}", id(tile2_climate_current_temperature).state, id(tile2_climate_target_temperature).state, id(tile2_climate_humidity).state, std::string(id(tile2_climate_mode).state.c_str()));
if (std::string("${TILE3_TYPE}") == "climate") sync_open_climate("${TILE3_ENTITY}", id(tile3_climate_current_temperature).state, id(tile3_climate_target_temperature).state, id(tile3_climate_humidity).state, std::string(id(tile3_climate_mode).state.c_str()));
if (std::string("${TILE4_TYPE}") == "climate") sync_open_climate("${TILE4_ENTITY}", id(tile4_climate_current_temperature).state, id(tile4_climate_target_temperature).state, id(tile4_climate_humidity).state, std::string(id(tile4_climate_mode).state.c_str()));
if (std::string("${TILE5_TYPE}") == "climate") sync_open_climate("${TILE5_ENTITY}", id(tile5_climate_current_temperature).state, id(tile5_climate_target_temperature).state, id(tile5_climate_humidity).state, std::string(id(tile5_climate_mode).state.c_str()));
if (std::string("${TILE6_TYPE}") == "climate") sync_open_climate("${TILE6_ENTITY}", id(tile6_climate_current_temperature).state, id(tile6_climate_target_temperature).state, id(tile6_climate_humidity).state, std::string(id(tile6_climate_mode).state.c_str()));
}
{
auto now = id(homeassistant_time).now();
// Before the first time sync, now() is epoch 0 (1970) — don't show
// a misleading "01:00" then, just leave it blank until a
// real time comes in (after the first Home Assistant time sync).
if (now.is_valid()) {
char buf[10];
if (std::string("${TIME_24H}") == "true") {
snprintf(buf, sizeof(buf), "%02d:%02d", now.hour, now.minute);
} else {
int hour12 = now.hour % 12;
if (hour12 == 0) hour12 = 12;
snprintf(buf, sizeof(buf), "%d:%02d %s", hour12, now.minute, now.hour >= 12 ? "PM" : "AM");
}
set_lbl(id(lbl_time), std::string(buf));
}
}
// Accent colour from the Home Assistant domain: the part before the dot
// in the entity_id. script/scene/button fire (ACCENT_ACTION), a
// light is a light (ACCENT_LIGHT), the rest is a device.
// NOTE: this helper appears twice in this file — here and in
// open_value_overlay. Update them together.
auto accent_for = [](const std::string& entity_id) -> uint32_t {
std::string d = entity_id.substr(0, entity_id.find('.'));
if (d == "script" || d == "scene" || d == "button" ||
d == "input_button" || d == "automation") return ${ACCENT_ACTION};
if (d == "light") return ${ACCENT_LIGHT};
return ${ACCENT_DEVICE};
};
auto apply_tile = [&](bool active,
const std::string& entity_id,
lv_obj_t* tile,
lv_obj_t* circle,
lv_obj_t* title,
lv_obj_t* value,
lv_obj_t* icon_lbl,
uint32_t circle_active,
uint32_t circle_disabled,
uint32_t icon_active,
uint32_t icon_disabled,
uint32_t bg_active,
uint32_t bg_disabled,
uint32_t title_active,
uint32_t title_disabled,
uint32_t value_active,
uint32_t value_disabled)
{
static lv_obj_t* cached_tiles[10] = {};
static bool cached_active[10] = {};
for (int i = 0; i < 10; ++i) {
if (cached_tiles[i] == tile) {
if (cached_active[i] == active) return;
cached_active[i] = active;
break;
}
if (cached_tiles[i] == nullptr) {
cached_tiles[i] = tile;
cached_active[i] = active;
break;
}
}
// ${ACCENT_AUTO} = "figure it out yourself"; a real hex value always wins.
uint32_t accent = accent_for(entity_id);
if (circle_active == ${ACCENT_AUTO}) circle_active = accent;
if (icon_disabled == ${ACCENT_AUTO}) icon_disabled = accent;
lv_obj_set_style_bg_color(tile, lv_color_hex(active ? bg_active : bg_disabled), 0);
lv_obj_set_style_bg_color(circle, lv_color_hex(active ? circle_active : circle_disabled), 0);
lv_obj_set_style_text_color(icon_lbl, lv_color_hex(active ? icon_active : icon_disabled), 0);
lv_obj_set_style_text_color(title, lv_color_hex(active ? title_active : title_disabled), 0);
lv_obj_set_style_text_color(value, lv_color_hex(active ? value_active : value_disabled), 0);
};
apply_tile(t1_on, "${TILE1_ENTITY}",
id(tile1), id(tile1_icon_circle), id(t1_title), id(t1_value), id(tile1_icon_lbl),
${TILE1_CIRCLE_ACTIVE_COLOR}, ${TILE1_CIRCLE_DISABLED_COLOR},
${TILE1_ICON_ACTIVE_COLOR}, ${TILE1_ICON_DISABLED_COLOR},
${TILE1_BG_ACTIVE_COLOR}, ${TILE1_BG_DISABLED_COLOR},
${TILE1_TITLE_ACTIVE_COLOR}, ${TILE1_TITLE_DISABLED_COLOR},
${TILE1_VALUE_ACTIVE_COLOR}, ${TILE1_VALUE_DISABLED_COLOR});
apply_tile(t2_on, "${TILE2_ENTITY}",
id(tile2), id(tile2_icon_circle), id(t2_title), id(t2_value), id(tile2_icon_lbl),
${TILE2_CIRCLE_ACTIVE_COLOR}, ${TILE2_CIRCLE_DISABLED_COLOR},
${TILE2_ICON_ACTIVE_COLOR}, ${TILE2_ICON_DISABLED_COLOR},
${TILE2_BG_ACTIVE_COLOR}, ${TILE2_BG_DISABLED_COLOR},
${TILE2_TITLE_ACTIVE_COLOR}, ${TILE2_TITLE_DISABLED_COLOR},
${TILE2_VALUE_ACTIVE_COLOR}, ${TILE2_VALUE_DISABLED_COLOR});
apply_tile(t3_on, "${TILE3_ENTITY}",
id(tile3), id(tile3_icon_circle), id(t3_title), id(t3_value), id(tile3_icon_lbl),
${TILE3_CIRCLE_ACTIVE_COLOR}, ${TILE3_CIRCLE_DISABLED_COLOR},
${TILE3_ICON_ACTIVE_COLOR}, ${TILE3_ICON_DISABLED_COLOR},
${TILE3_BG_ACTIVE_COLOR}, ${TILE3_BG_DISABLED_COLOR},
${TILE3_TITLE_ACTIVE_COLOR}, ${TILE3_TITLE_DISABLED_COLOR},
${TILE3_VALUE_ACTIVE_COLOR}, ${TILE3_VALUE_DISABLED_COLOR});
apply_tile(t4_on, "${TILE4_ENTITY}",
id(tile4), id(tile4_icon_circle), id(t4_title), id(t4_value), id(tile4_icon_lbl),
${TILE4_CIRCLE_ACTIVE_COLOR}, ${TILE4_CIRCLE_DISABLED_COLOR},
${TILE4_ICON_ACTIVE_COLOR}, ${TILE4_ICON_DISABLED_COLOR},
${TILE4_BG_ACTIVE_COLOR}, ${TILE4_BG_DISABLED_COLOR},
${TILE4_TITLE_ACTIVE_COLOR}, ${TILE4_TITLE_DISABLED_COLOR},
${TILE4_VALUE_ACTIVE_COLOR}, ${TILE4_VALUE_DISABLED_COLOR});
apply_tile(t5_on, "${TILE5_ENTITY}",
id(tile5), id(tile5_icon_circle), id(t5_title), id(t5_value), id(tile5_icon_lbl),
${TILE5_CIRCLE_ACTIVE_COLOR}, ${TILE5_CIRCLE_DISABLED_COLOR},
${TILE5_ICON_ACTIVE_COLOR}, ${TILE5_ICON_DISABLED_COLOR},
${TILE5_BG_ACTIVE_COLOR}, ${TILE5_BG_DISABLED_COLOR},
${TILE5_TITLE_ACTIVE_COLOR}, ${TILE5_TITLE_DISABLED_COLOR},
${TILE5_VALUE_ACTIVE_COLOR}, ${TILE5_VALUE_DISABLED_COLOR});
apply_tile(t6_on, "${TILE6_ENTITY}",
id(tile6), id(tile6_icon_circle), id(t6_title), id(t6_value), id(tile6_icon_lbl),
${TILE6_CIRCLE_ACTIVE_COLOR}, ${TILE6_CIRCLE_DISABLED_COLOR},
${TILE6_ICON_ACTIVE_COLOR}, ${TILE6_ICON_DISABLED_COLOR},
${TILE6_BG_ACTIVE_COLOR}, ${TILE6_BG_DISABLED_COLOR},
${TILE6_TITLE_ACTIVE_COLOR}, ${TILE6_TITLE_DISABLED_COLOR},
${TILE6_VALUE_ACTIVE_COLOR}, ${TILE6_VALUE_DISABLED_COLOR});
apply_tile(t7_on, "${TILE7_ENTITY}",
id(tile7), id(tile7_icon_circle), id(t7_title), id(t7_value), id(tile7_icon_lbl),
${TILE7_CIRCLE_ACTIVE_COLOR}, ${TILE7_CIRCLE_DISABLED_COLOR},
${TILE7_ICON_ACTIVE_COLOR}, ${TILE7_ICON_DISABLED_COLOR},
${TILE7_BG_ACTIVE_COLOR}, ${TILE7_BG_DISABLED_COLOR},
${TILE7_TITLE_ACTIVE_COLOR}, ${TILE7_TITLE_DISABLED_COLOR},
${TILE7_VALUE_ACTIVE_COLOR}, ${TILE7_VALUE_DISABLED_COLOR});
apply_tile(t8_on, "${TILE8_ENTITY}",
id(tile8), id(tile8_icon_circle), id(t8_title), id(t8_value), id(tile8_icon_lbl),
${TILE8_CIRCLE_ACTIVE_COLOR}, ${TILE8_CIRCLE_DISABLED_COLOR},
${TILE8_ICON_ACTIVE_COLOR}, ${TILE8_ICON_DISABLED_COLOR},
${TILE8_BG_ACTIVE_COLOR}, ${TILE8_BG_DISABLED_COLOR},
${TILE8_TITLE_ACTIVE_COLOR}, ${TILE8_TITLE_DISABLED_COLOR},
${TILE8_VALUE_ACTIVE_COLOR}, ${TILE8_VALUE_DISABLED_COLOR});
apply_tile(t9_on, "${TILE9_ENTITY}",
id(tile9), id(tile9_icon_circle), id(t9_title), id(t9_value), id(tile9_icon_lbl),
${TILE9_CIRCLE_ACTIVE_COLOR}, ${TILE9_CIRCLE_DISABLED_COLOR},
${TILE9_ICON_ACTIVE_COLOR}, ${TILE9_ICON_DISABLED_COLOR},
${TILE9_BG_ACTIVE_COLOR}, ${TILE9_BG_DISABLED_COLOR},
${TILE9_TITLE_ACTIVE_COLOR}, ${TILE9_TITLE_DISABLED_COLOR},
${TILE9_VALUE_ACTIVE_COLOR}, ${TILE9_VALUE_DISABLED_COLOR});
apply_tile(t10_on, "${TILE10_ENTITY}",
id(tile10), id(tile10_icon_circle), id(t10_title), id(t10_value), id(tile10_icon_lbl),
${TILE10_CIRCLE_ACTIVE_COLOR}, ${TILE10_CIRCLE_DISABLED_COLOR},
${TILE10_ICON_ACTIVE_COLOR}, ${TILE10_ICON_DISABLED_COLOR},
${TILE10_BG_ACTIVE_COLOR}, ${TILE10_BG_DISABLED_COLOR},
${TILE10_TITLE_ACTIVE_COLOR}, ${TILE10_TITLE_DISABLED_COLOR},
${TILE10_VALUE_ACTIVE_COLOR}, ${TILE10_VALUE_DISABLED_COLOR});
# 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:
- platform: template
name: "Calibrate touch"
internal: ${DASHBOARD_INBOX_INTERNAL}
entity_category: config
on_press:
- lambda: 'if (runtime_tiles::enabled) screen_calibration::begin();'
number:
- 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: |-
auto &s = screen_settings::current;
s.brightness = (int)x;
s.standby_brightness = std::min(s.standby_brightness,s.brightness);
s.night_brightness = std::min(s.night_brightness,s.brightness);
if (runtime_tiles::enabled) runtime_tiles::settings_preference.save(&s);
runtime_tiles::setting_event("brightness",s.brightness);
id(apply_screen_settings).execute();
- 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: |-
auto &s = screen_settings::current;
s.standby_brightness = (int)x;
s.standby_brightness = std::min(s.standby_brightness,s.brightness);
s.night_brightness = std::min(s.night_brightness,s.brightness);
if (runtime_tiles::enabled) runtime_tiles::settings_preference.save(&s);
runtime_tiles::setting_event("standby_brightness",s.standby_brightness);
id(apply_screen_settings).execute();
- 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: |-
auto &s = screen_settings::current;
s.night_brightness = (int)x;
s.standby_brightness = std::min(s.standby_brightness,s.brightness);
s.night_brightness = std::min(s.night_brightness,s.brightness);
if (runtime_tiles::enabled) runtime_tiles::settings_preference.save(&s);
runtime_tiles::setting_event("night_brightness",s.night_brightness);
id(apply_screen_settings).execute();
- 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: |-
auto &s = screen_settings::current;
s.standby_seconds = (int)x;
s.standby_brightness = std::min(s.standby_brightness,s.brightness);
s.night_brightness = std::min(s.night_brightness,s.brightness);
if (runtime_tiles::enabled) runtime_tiles::settings_preference.save(&s);
runtime_tiles::setting_event("standby_seconds",s.standby_seconds);
id(apply_screen_settings).execute();