0ebb23a1f4
Page switches show the new page as empty cards at once and draw two cards per LVGL refresh, so touch is polled between the steps; on the Guition the longest loop gap during a swipe dropped from 276 to 74 ms (docs/SWIPE_PROFILE.md). Custom parts and control panels are reused instead of rebuilt, state updates and ticks redraw only their own card, clocks redraw once a minute, and a swipe past the last page draws nothing. Slider handles sit inside the fill (grey when the card is off), the busy sheet covers the whole card, and the light colour card is redesigned with rounded tracks and colour knobs. Firmware 0.2.36; SWIPE_PROFILE builds carry a per-swipe profiler for measurements. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
6943 lines
314 KiB
YAML
6943 lines
314 KiB
YAML
# GENERATED by tools/generate_packages.py; edit the source board profile.
|
||
############################################################
|
||
# 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.36"
|
||
# 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: "true"
|
||
DASHBOARD_INBOX_INTERNAL: "false"
|
||
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: "1" # 1-10; <=6 hides page navigation
|
||
|
||
DIRECT_ACTIONS: "true" # true = call Home Assistant services directly, false = only emit action events
|
||
ROOM_NAME: "Choose tiles in HA" # 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_all_off" # main Home Assistant entity for this tile
|
||
TILE4_STATE_ENTITY: "script.example_all_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: git
|
||
url: https://github.com/MaxGramser/homeassistant_espscreen.git
|
||
ref: main
|
||
path: components
|
||
refresh: 0s
|
||
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:
|
||
- <esphome/components/smart_display/cyd_ui.h>
|
||
- <esphome/components/smart_display/light_controls.h>
|
||
- <esphome/components/smart_display/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}"), id(materialdesign_icons_mini)->get_lv_font());
|
||
light_controls::commits[0] = [](int value) { id(set_light_hue).execute(value); };
|
||
light_controls::commits[1] = [](int value) { id(set_light_color_temperature).execute(value); };
|
||
light_controls::commits[2] = [](int value) { id(color_brightness_commit).execute(value); };
|
||
- 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));'
|
||
|
||
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: "https://raw.githubusercontent.com/MaxGramser/homeassistant_espscreen/main/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: "https://raw.githubusercontent.com/MaxGramser/homeassistant_espscreen/main/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: "https://raw.githubusercontent.com/MaxGramser/homeassistant_espscreen/main/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: "https://raw.githubusercontent.com/MaxGramser/homeassistant_espscreen/main/fonts/Roboto-500.ttf"
|
||
id: clock_digits
|
||
size: 24
|
||
bpp: 4
|
||
glyphs: [':','-','0','1','2','3','4','5','6','7','8','9']
|
||
|
||
- file: "https://raw.githubusercontent.com/MaxGramser/homeassistant_espscreen/main/fonts/Roboto-700.ttf"
|
||
id: label
|
||
size: 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: "https://raw.githubusercontent.com/MaxGramser/homeassistant_espscreen/main/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: "https://raw.githubusercontent.com/MaxGramser/homeassistant_espscreen/main/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: "https://raw.githubusercontent.com/MaxGramser/homeassistant_espscreen/main/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: "https://raw.githubusercontent.com/MaxGramser/homeassistant_espscreen/main/fonts/materialdesignicons-webfont.ttf"
|
||
id: materialdesign_icons_mini
|
||
size: 18
|
||
glyphs: *tile_icons
|
||
|
||
- file: "https://raw.githubusercontent.com/MaxGramser/homeassistant_espscreen/main/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: template
|
||
update_interval: never
|
||
id: tile1_brightness
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile1_percentage
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile2_brightness
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile2_percentage
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile3_brightness
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile3_percentage
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile4_brightness
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile4_percentage
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile5_brightness
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile5_percentage
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile6_brightness
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile6_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: template
|
||
update_interval: never
|
||
id: tile7_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: template
|
||
update_interval: never
|
||
id: tile9_brightness
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile1_cover_position
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile2_cover_position
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile3_cover_position
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile4_cover_position
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile5_cover_position
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile6_cover_position
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile1_climate_current_temperature
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile1_climate_target_temperature
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile1_climate_humidity
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile2_climate_current_temperature
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile2_climate_target_temperature
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile2_climate_humidity
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile3_climate_current_temperature
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile3_climate_target_temperature
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile3_climate_humidity
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile4_climate_current_temperature
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile4_climate_target_temperature
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile4_climate_humidity
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile5_climate_current_temperature
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile5_climate_target_temperature
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile5_climate_humidity
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile6_climate_current_temperature
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile6_climate_target_temperature
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile6_climate_humidity
|
||
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: template
|
||
update_interval: never
|
||
id: ha_state_tile1
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: ha_state_tile2
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: ha_state_tile3
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: ha_state_tile4
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: ha_state_tile5
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: ha_state_tile6
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: ha_state_tile7
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: ha_state_tile8
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: ha_state_tile9
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: ha_state_tile10
|
||
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: template
|
||
update_interval: never
|
||
id: tile9_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: template
|
||
update_interval: never
|
||
id: tile7_supported_color_modes
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile1_preset
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile2_preset
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile3_preset
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile4_preset
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile5_preset
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile6_preset
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile1_supported_color_modes
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile2_supported_color_modes
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile3_supported_color_modes
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile4_supported_color_modes
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile5_supported_color_modes
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile6_supported_color_modes
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile1_climate_mode
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile2_climate_mode
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile3_climate_mode
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile4_climate_mode
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile5_climate_mode
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile6_climate_mode
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile1_climate_supported_modes
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile2_climate_supported_modes
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile3_climate_supported_modes
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile4_climate_supported_modes
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile5_climate_supported_modes
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile6_climate_supported_modes
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile1_cover_state
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile2_cover_state
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile3_cover_state
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile4_cover_state
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile5_cover_state
|
||
on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] }
|
||
|
||
- platform: template
|
||
update_interval: never
|
||
id: tile6_cover_state
|
||
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 ¤t_mode) {
|
||
unsigned shown = 0;
|
||
for (unsigned i = 0; i < 4; ++i) {
|
||
std::string raw = cyd::list_item(modes, i);
|
||
if (raw.empty()) { lv_obj_add_flag(chips[i], LV_OBJ_FLAG_HIDDEN); continue; }
|
||
lv_obj_clear_flag(chips[i], LV_OBJ_FLAG_HIDDEN); ++shown;
|
||
lv_label_set_text(lv_obj_get_child(chips[i], 0), pretty(raw).c_str());
|
||
lv_obj_set_style_bg_color(chips[i], 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();
|