############################################################ # 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.35" # A tap survives a finger that drifts up to this far (320 px over 58 mm: 1 cm); further is a swipe. TOUCH_MOVE_LIMIT_PX: "56" # Shortest contact that counts as a tap; a resistive panel bounces, a capacitive one does not. TOUCH_MIN_PRESS_MS: "60" DYNAMIC_TILES: "false" DASHBOARD_INBOX_INTERNAL: "true" DEVICE_NAME: "cyd-new" DEVICE_FRIENDLY_NAME: "My CYD" CALIBRATION_ON_BOOT: "false" TOUCH_AFFINE_XX: "1.000000000" TOUCH_AFFINE_XY: "0.000000000" TOUCH_AFFINE_XC: "0.000000000" TOUCH_AFFINE_YX: "0.000000000" TOUCH_AFFINE_YY: "1.000000000" TOUCH_AFFINE_YC: "0.000000000" TILE_COUNT: "6" # 1-10; <=6 hides page navigation DIRECT_ACTIONS: "false" # true = call Home Assistant services directly, false = only emit action events ROOM_NAME: "Living room" # room name shown in the top-left header TIME_24H: "true" # true = 24h format, false = 12h format ## DIM settings AUTO_DIM: "true" # true = enable auto dimming AUTO_DIM_TIMEOUT: "600" # seconds after last touch MAX_BRIGHTNESS: "100" # backlight brightness in percent DIM_BRIGHTNESS: "20" # dimmed backlight brightness in percent NIGHT_MODE: "true" # true = use separate dim level at night NIGHT_START_HOUR: "22" # 0-23 NIGHT_END_HOUR: "7" # 0-23 NIGHT_DIM_BRIGHTNESS: "10" # backlight brightness in percent at night # ============================================================ # ACCENT COLOURS — determined automatically from the Home Assistant domain # # The domain is the part before the dot in the entity_id: # script.good_morning -> "script" -> ACCENT_ACTION # light.living_room_2 -> "light" -> ACCENT_LIGHT # vacuum.s8 -> "vacuum" -> ACCENT_DEVICE # # That's Home Assistant's own grouping, so a tile automatically gets the # right colour as soon as you put an entity in it — you no longer have # to pick colours per tile. The distinction that matters: a script/scene # fires, a device you turn on or off. Those two shouldn't look the same. # # Set TILE*_CIRCLE_ACTIVE_COLOR / TILE*_ICON_DISABLED_COLOR to # ${ACCENT_AUTO} to use this, or to a real hex value to colour that # one tile by hand (see tile 8 — red as soon as there's mail). # ============================================================ ACCENT_AUTO: "0x1000000" # sentinel: deliberately outside the 24-bit colour range ACCENT_ACTION: "0x8B5CF6" # script / scene / button / input_button / automation ACCENT_LIGHT: "0xFEC600" # light ACCENT_DEVICE: "0x00C5EC" # switch, fan, cover, climate, vacuum, media_player, rest # ============================================================ # FONT GLYPHS # The icon fonts carry the full tile icon set from screen_manager/app/tile_icons.py # (tools/generate_icons.py). Pick TILEn_ICON from that set; no glyph list to maintain. # ============================================================ # ============================================================ # TILE 1 # ============================================================ # --- basic settings --- TILE1_ENTITY: "light.example_lamp" # main Home Assistant entity for this tile TILE1_STATE_ENTITY: "light.example_lamp" # entity used for active/inactive state styling TILE1_TITLE: "Lamp" # tile title shown in the UI TILE1_ICON: "\U000F001B" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) TILE1_TYPE: "light" # tile type: light | fan | switch | scene | script | cover | climate TILE1_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset | custom TILE1_LONGPRESS: "slider" # long press mode: none | slider | action TILE1_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text TILE1_LABEL_OFF: "Off" # text shown when entity is off TILE1_LABEL_ON: "On" # text shown for text-mode tiles when entity is on TILE1_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") # --- colors --- TILE1_CIRCLE_ACTIVE_COLOR: "${ACCENT_AUTO}" # icon circle color when active TILE1_CIRCLE_DISABLED_COLOR: "0x7B7B6F" # icon circle color when inactive TILE1_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active TILE1_ICON_DISABLED_COLOR: "${ACCENT_AUTO}" # icon color when inactive TILE1_BG_ACTIVE_COLOR: "0xFFFFFF" # tile background color when active TILE1_BG_DISABLED_COLOR: "0x939391" # tile background color when inactive TILE1_TITLE_ACTIVE_COLOR: "0x000000" # title color when active TILE1_TITLE_DISABLED_COLOR: "0xFFFFFF" # title color when inactive TILE1_VALUE_ACTIVE_COLOR: "0x7A7A7C" # value color when active TILE1_VALUE_DISABLED_COLOR: "0xD9D9D9" # value color when inactive # --- advanced settings --- TILE1_TAP_SERVICE: "" # optional service override, e.g. light.toggle TILE1_TAP_PARAM_KEY: "" # optional extra service parameter key TILE1_TAP_PARAM_VAL: "" # optional extra service parameter value # if LONGPRESS = slider: TILE1_LONGPRESS_SLIDER: "auto" # slider type: auto | brightness | percentage | cover_position TILE1_LONGPRESS_OFF_VALUE: "0" # initial slider value when entity is off # if LONGPRESS = action: TILE1_LONGPRESS_ACTION: "" # entity_id to fire on long press TILE1_LONGPRESS_ACTION_TYPE: "" # entity type: light | fan | switch | scene | script | cover | climate (empty = same as TILE*_TYPE) TILE1_LONGPRESS_ACTION_SERVICE: "" # service to call (default: activate/turn_on — set e.g. "light.toggle" to toggle instead) # if TYPE = climate: TILE1_CLIMATE_MIN_TEMP: "16.0" # minimum target temperature shown in the thermostat overlay TILE1_CLIMATE_MAX_TEMP: "32.0" # maximum target temperature shown in the thermostat overlay TILE1_CLIMATE_STEP: "1.0" # target temperature step size TILE1_CLIMATE_CURRENT_TEMP_ATTRIBUTE: "current_temperature" # HA attribute for current room temperature TILE1_CLIMATE_TARGET_TEMP_ATTRIBUTE: "temperature" # HA attribute for target temperature TILE1_CLIMATE_HUMIDITY_ATTRIBUTE: "current_humidity" # HA attribute for humidity; leave as-is if unavailable # ============================================================ # TILE 2 # ============================================================ # --- basic settings --- TILE2_ENTITY: "switch.example_plug" # main Home Assistant entity for this tile TILE2_STATE_ENTITY: "switch.example_plug" # entity used for active/inactive state styling TILE2_TITLE: "Plug" # tile title shown in the UI TILE2_ICON: "\U000F0387" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) TILE2_TYPE: "switch" # tile type: light | fan | switch | scene | script | cover | climate TILE2_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset | custom TILE2_LONGPRESS: "none" # long press mode: none | slider | action TILE2_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text TILE2_LABEL_OFF: "Off" # text shown when entity is off TILE2_LABEL_ON: "On" # text shown for text-mode tiles when entity is on TILE2_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") # --- colors --- TILE2_CIRCLE_ACTIVE_COLOR: "${ACCENT_AUTO}" # icon circle color when active TILE2_CIRCLE_DISABLED_COLOR: "0x7B7B6F" # icon circle color when inactive TILE2_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active TILE2_ICON_DISABLED_COLOR: "${ACCENT_AUTO}" # icon color when inactive TILE2_BG_ACTIVE_COLOR: "0xFFFFFF" # tile background color when active TILE2_BG_DISABLED_COLOR: "0x939391" # tile background color when inactive TILE2_TITLE_ACTIVE_COLOR: "0x000000" # title color when active TILE2_TITLE_DISABLED_COLOR: "0xFFFFFF" # title color when inactive TILE2_VALUE_ACTIVE_COLOR: "0x7A7A7C" # value color when active TILE2_VALUE_DISABLED_COLOR: "0xD9D9D9" # value color when inactive # --- advanced settings --- TILE2_TAP_SERVICE: "" # optional service override, e.g. light.toggle TILE2_TAP_PARAM_KEY: "" # optional extra service parameter key TILE2_TAP_PARAM_VAL: "" # optional extra service parameter value # if LONGPRESS = slider: TILE2_LONGPRESS_SLIDER: "auto" # slider type: auto | brightness | percentage | cover_position TILE2_LONGPRESS_OFF_VALUE: "0" # initial slider value when entity is off # if LONGPRESS = action: TILE2_LONGPRESS_ACTION: "" # entity_id to fire on long press TILE2_LONGPRESS_ACTION_TYPE: "" # entity type: light | fan | switch | scene | script | cover | climate (empty = same as TILE*_TYPE) TILE2_LONGPRESS_ACTION_SERVICE: "" # service to call (default: activate/turn_on — set e.g. "light.toggle" to toggle instead) # if TYPE = climate: TILE2_CLIMATE_MIN_TEMP: "5.0" # minimum target temperature shown in the thermostat overlay TILE2_CLIMATE_MAX_TEMP: "30.0" # maximum target temperature shown in the thermostat overlay TILE2_CLIMATE_STEP: "0.5" # target temperature step size TILE2_CLIMATE_CURRENT_TEMP_ATTRIBUTE: "current_temperature" # HA attribute for current room temperature TILE2_CLIMATE_TARGET_TEMP_ATTRIBUTE: "temperature" # HA attribute for target temperature TILE2_CLIMATE_HUMIDITY_ATTRIBUTE: "current_humidity" # HA attribute for humidity; leave as-is if unavailable # ============================================================ # TILE 3 # ============================================================ # --- basic settings --- TILE3_ENTITY: "scene.example_evening" # main Home Assistant entity for this tile TILE3_STATE_ENTITY: "scene.example_evening" # entity used for active/inactive state styling TILE3_TITLE: "Evening" # tile title shown in the UI TILE3_ICON: "\U000F02E3" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) TILE3_TYPE: "scene" # tile type: light | fan | switch | scene | script | cover | climate TILE3_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset | custom TILE3_LONGPRESS: "none" # long press mode: none | slider | action TILE3_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text TILE3_LABEL_OFF: "Off" # text shown when entity is off TILE3_LABEL_ON: "On" # text shown for text-mode tiles when entity is on TILE3_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") # --- colors --- TILE3_CIRCLE_ACTIVE_COLOR: "${ACCENT_AUTO}" # icon circle color when active TILE3_CIRCLE_DISABLED_COLOR: "0x7B7B6F" # icon circle color when inactive TILE3_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active TILE3_ICON_DISABLED_COLOR: "${ACCENT_AUTO}" # icon color when inactive TILE3_BG_ACTIVE_COLOR: "0xFFFFFF" # tile background color when active TILE3_BG_DISABLED_COLOR: "0x939391" # tile background color when inactive TILE3_TITLE_ACTIVE_COLOR: "0x000000" # title color when active TILE3_TITLE_DISABLED_COLOR: "0xFFFFFF" # title color when inactive TILE3_VALUE_ACTIVE_COLOR: "0x7A7A7C" # value color when active TILE3_VALUE_DISABLED_COLOR: "0xD9D9D9" # value color when inactive # --- advanced settings --- TILE3_TAP_SERVICE: "" # optional service override, e.g. light.toggle TILE3_TAP_PARAM_KEY: "" # optional extra service parameter key TILE3_TAP_PARAM_VAL: "" # optional extra service parameter value # if LONGPRESS = slider: TILE3_LONGPRESS_SLIDER: "auto" # slider type: auto | brightness | percentage | cover_position TILE3_LONGPRESS_OFF_VALUE: "0" # initial slider value when entity is off # if LONGPRESS = action: TILE3_LONGPRESS_ACTION: "" # entity_id to fire on long press TILE3_LONGPRESS_ACTION_TYPE: "" # entity type: light | fan | switch | scene | script | cover | climate (empty = same as TILE*_TYPE) TILE3_LONGPRESS_ACTION_SERVICE: "" # service to call (default: activate/turn_on — set e.g. "light.toggle" to toggle instead) # if TYPE = climate: TILE3_CLIMATE_MIN_TEMP: "5.0" # minimum target temperature shown in the thermostat overlay TILE3_CLIMATE_MAX_TEMP: "30.0" # maximum target temperature shown in the thermostat overlay TILE3_CLIMATE_STEP: "0.5" # target temperature step size TILE3_CLIMATE_CURRENT_TEMP_ATTRIBUTE: "current_temperature" # HA attribute for current room temperature TILE3_CLIMATE_TARGET_TEMP_ATTRIBUTE: "temperature" # HA attribute for target temperature TILE3_CLIMATE_HUMIDITY_ATTRIBUTE: "current_humidity" # HA attribute for humidity; leave as-is if unavailable # ============================================================ # TILE 4 # ============================================================ # --- basic settings --- TILE4_ENTITY: "script.example_off" # main Home Assistant entity for this tile TILE4_STATE_ENTITY: "script.example_off" # entity used for active/inactive state styling TILE4_TITLE: "All off" # tile title shown in the UI TILE4_ICON: "\U000F059C" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) TILE4_TYPE: "script" # tile type: light | fan | switch | scene | script | cover | climate TILE4_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset | custom TILE4_LONGPRESS: "none" # long press mode: none | slider | action TILE4_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text TILE4_LABEL_OFF: "Off" # text shown when entity is off TILE4_LABEL_ON: "On" # text shown for text-mode tiles when entity is on TILE4_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") # --- colors --- TILE4_CIRCLE_ACTIVE_COLOR: "${ACCENT_AUTO}" # icon circle color when active TILE4_CIRCLE_DISABLED_COLOR: "0x7B7B6F" # icon circle color when inactive TILE4_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active TILE4_ICON_DISABLED_COLOR: "${ACCENT_AUTO}" # icon color when inactive TILE4_BG_ACTIVE_COLOR: "0xFFFFFF" # tile background color when active TILE4_BG_DISABLED_COLOR: "0x939391" # tile background color when inactive TILE4_TITLE_ACTIVE_COLOR: "0x000000" # title color when active TILE4_TITLE_DISABLED_COLOR: "0xFFFFFF" # title color when inactive TILE4_VALUE_ACTIVE_COLOR: "0x7A7A7C" # value color when active TILE4_VALUE_DISABLED_COLOR: "0xD9D9D9" # value color when inactive # --- advanced settings --- TILE4_TAP_SERVICE: "" # optional service override, e.g. light.toggle TILE4_TAP_PARAM_KEY: "" # optional extra service parameter key TILE4_TAP_PARAM_VAL: "" # optional extra service parameter value # if LONGPRESS = slider: TILE4_LONGPRESS_SLIDER: "auto" # slider type: auto | brightness | percentage | cover_position TILE4_LONGPRESS_OFF_VALUE: "0" # initial slider value when entity is off # if LONGPRESS = action: TILE4_LONGPRESS_ACTION: "" # entity_id to fire on long press TILE4_LONGPRESS_ACTION_TYPE: "" # entity type: light | fan | switch | scene | script | cover | climate (empty = same as TILE*_TYPE) TILE4_LONGPRESS_ACTION_SERVICE: "" # service to call (default: activate/turn_on — set e.g. "light.toggle" to toggle instead) # if TYPE = climate: TILE4_CLIMATE_MIN_TEMP: "5.0" # minimum target temperature shown in the thermostat overlay TILE4_CLIMATE_MAX_TEMP: "30.0" # maximum target temperature shown in the thermostat overlay TILE4_CLIMATE_STEP: "0.5" # target temperature step size TILE4_CLIMATE_CURRENT_TEMP_ATTRIBUTE: "current_temperature" # HA attribute for current room temperature TILE4_CLIMATE_TARGET_TEMP_ATTRIBUTE: "temperature" # HA attribute for target temperature TILE4_CLIMATE_HUMIDITY_ATTRIBUTE: "current_humidity" # HA attribute for humidity; leave as-is if unavailable # ============================================================ # TILE 5 # ============================================================ # --- basic settings --- TILE5_ENTITY: "climate.example_ac" # main Home Assistant entity for this tile TILE5_STATE_ENTITY: "climate.example_ac" # entity used for active/inactive state styling TILE5_TITLE: "AC" # tile title shown in the UI TILE5_ICON: "\U000F0425" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) TILE5_TYPE: "climate" # tile type: light | fan | switch | scene | script | cover | climate TILE5_TAP_ACTION: "toggle" # tap action: auto | toggle | activate | fan_toggle_preset | custom TILE5_LONGPRESS: "slider" # long press mode: none | slider | action TILE5_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text TILE5_LABEL_OFF: "Off" # text shown when entity is off (closed) TILE5_LABEL_ON: "On" # text shown when entity is on (open) TILE5_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") # --- colors --- TILE5_CIRCLE_ACTIVE_COLOR: "${ACCENT_AUTO}" # icon circle color when active TILE5_CIRCLE_DISABLED_COLOR: "0x7B7B6F" # icon circle color when inactive TILE5_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active TILE5_ICON_DISABLED_COLOR: "${ACCENT_AUTO}" # icon color when inactive TILE5_BG_ACTIVE_COLOR: "0xFFFFFF" # tile background color when active TILE5_BG_DISABLED_COLOR: "0x939391" # tile background color when inactive TILE5_TITLE_ACTIVE_COLOR: "0x000000" # title color when active TILE5_TITLE_DISABLED_COLOR: "0xFFFFFF" # title color when inactive TILE5_VALUE_ACTIVE_COLOR: "0x7A7A7C" # value color when active TILE5_VALUE_DISABLED_COLOR: "0xD9D9D9" # value color when inactive # --- advanced settings --- TILE5_TAP_SERVICE: "climate.toggle" # optional service override, e.g. light.toggle TILE5_TAP_PARAM_KEY: "" # optional extra service parameter key TILE5_TAP_PARAM_VAL: "" # optional extra service parameter value # if LONGPRESS = slider: TILE5_LONGPRESS_SLIDER: "auto" # slider type: auto | brightness | percentage | cover_position TILE5_LONGPRESS_OFF_VALUE: "0" # initial slider value when entity is off # if LONGPRESS = action: TILE5_LONGPRESS_ACTION: "" # entity_id to fire on long press TILE5_LONGPRESS_ACTION_TYPE: "" # entity type: light | fan | switch | scene | script | cover | climate (empty = same as TILE*_TYPE) TILE5_LONGPRESS_ACTION_SERVICE: "" # service to call (default: activate/turn_on — set e.g. "light.toggle" to toggle instead) # if TYPE = climate: TILE5_CLIMATE_MIN_TEMP: "5.0" # minimum target temperature shown in the thermostat overlay TILE5_CLIMATE_MAX_TEMP: "30.0" # maximum target temperature shown in the thermostat overlay TILE5_CLIMATE_STEP: "0.5" # target temperature step size TILE5_CLIMATE_CURRENT_TEMP_ATTRIBUTE: "current_temperature" # HA attribute for current room temperature TILE5_CLIMATE_TARGET_TEMP_ATTRIBUTE: "temperature" # HA attribute for target temperature TILE5_CLIMATE_HUMIDITY_ATTRIBUTE: "current_humidity" # HA attribute for humidity; leave as-is if unavailable # ============================================================ # TILE 6 # ============================================================ # --- basic settings --- TILE6_ENTITY: "vacuum.example_robot" # main Home Assistant entity for this tile TILE6_STATE_ENTITY: "vacuum.example_robot" # entity used for active/inactive state styling TILE6_TITLE: "Vacuum" # tile title shown in the UI TILE6_ICON: "\U000F070D" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) TILE6_TYPE: "vacuum" # tile type: light | fan | switch | scene | script | cover | climate TILE6_TAP_ACTION: "custom" # tap action: auto | toggle | activate | fan_toggle_preset | custom TILE6_LONGPRESS: "vacuum" # long press mode: none | slider | action TILE6_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text TILE6_LABEL_OFF: "Off" # text shown when entity is off TILE6_LABEL_ON: "On" # text shown for text-mode tiles when entity is on TILE6_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") # --- colors --- TILE6_CIRCLE_ACTIVE_COLOR: "${ACCENT_AUTO}" # icon circle color when active TILE6_CIRCLE_DISABLED_COLOR: "0x7B7B6F" # icon circle color when inactive TILE6_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active TILE6_ICON_DISABLED_COLOR: "${ACCENT_AUTO}" # icon color when inactive TILE6_BG_ACTIVE_COLOR: "0xFFFFFF" # tile background color when active TILE6_BG_DISABLED_COLOR: "0x939391" # tile background color when inactive TILE6_TITLE_ACTIVE_COLOR: "0x000000" # title color when active TILE6_TITLE_DISABLED_COLOR: "0xFFFFFF" # title color when inactive TILE6_VALUE_ACTIVE_COLOR: "0x7A7A7C" # value color when active TILE6_VALUE_DISABLED_COLOR: "0xD9D9D9" # value color when inactive # --- advanced settings --- TILE6_TAP_SERVICE: "vacuum.start" # optional service override, e.g. fan.toggle TILE6_TAP_PARAM_KEY: "" # optional extra service parameter key TILE6_TAP_PARAM_VAL: "" # optional extra service parameter value # if LONGPRESS = slider: TILE6_LONGPRESS_SLIDER: "auto" # slider type: auto | brightness | percentage | cover_position TILE6_LONGPRESS_OFF_VALUE: "0" # initial slider value when entity is off # if LONGPRESS = action: TILE6_LONGPRESS_ACTION: "" # entity_id to fire on long press TILE6_LONGPRESS_ACTION_TYPE: "" # entity type: light | fan | switch | scene | script | cover | climate (empty = same as TILE*_TYPE) TILE6_LONGPRESS_ACTION_SERVICE: "" # service to call (default: activate/turn_on — set e.g. "light.toggle" to toggle instead) # if TYPE = climate: TILE6_CLIMATE_MIN_TEMP: "5.0" # minimum target temperature shown in the thermostat overlay TILE6_CLIMATE_MAX_TEMP: "30.0" # maximum target temperature shown in the thermostat overlay TILE6_CLIMATE_STEP: "0.5" # target temperature step size TILE6_CLIMATE_CURRENT_TEMP_ATTRIBUTE: "current_temperature" # HA attribute for current room temperature TILE6_CLIMATE_TARGET_TEMP_ATTRIBUTE: "temperature" # HA attribute for target temperature TILE6_CLIMATE_HUMIDITY_ATTRIBUTE: "current_humidity" # HA attribute for humidity; leave as-is if unavailable # ============================================================ # TILE 7 — Table lamp # Brightness-only lamp (HA reports supported_color_modes: ['brightness']), # so the colour button in the slider overlay stays hidden automatically. # ============================================================ # --- basic settings --- TILE7_ENTITY: "light.unused_7" # main Home Assistant entity for this tile TILE7_STATE_ENTITY: "light.unused_7" # entity used for active/inactive state styling TILE7_TITLE: "Tile 7" # tile title shown in the UI TILE7_ICON: "\U000F06B5" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) TILE7_TYPE: "light" # tile type: light | fan | switch | scene | script | cover | climate TILE7_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset TILE7_LONGPRESS: "none" # long press mode: none | slider | action | vacuum TILE7_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text TILE7_LABEL_OFF: "Off" # text shown when entity is off TILE7_LABEL_ON: "On" # text shown for text-mode tiles when entity is on TILE7_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") # --- colors --- TILE7_CIRCLE_ACTIVE_COLOR: "${ACCENT_AUTO}" # icon circle color when active TILE7_CIRCLE_DISABLED_COLOR: "0x7B7B6F" # icon circle color when inactive TILE7_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active TILE7_ICON_DISABLED_COLOR: "${ACCENT_AUTO}" # icon color when inactive TILE7_BG_ACTIVE_COLOR: "0xFFFFFF" # tile background color when active TILE7_BG_DISABLED_COLOR: "0x939391" # tile background color when inactive TILE7_TITLE_ACTIVE_COLOR: "0x000000" # title color when active TILE7_TITLE_DISABLED_COLOR: "0xFFFFFF" # title color when inactive TILE7_VALUE_ACTIVE_COLOR: "0x7A7A7C" # value color when active TILE7_VALUE_DISABLED_COLOR: "0xD9D9D9" # value color when inactive # --- advanced settings --- TILE7_TAP_SERVICE: "" # optional service override, e.g. light.toggle TILE7_TAP_PARAM_KEY: "" # optional extra service parameter key TILE7_TAP_PARAM_VAL: "" # optional extra service parameter value # if LONGPRESS = slider: TILE7_LONGPRESS_SLIDER: "auto" # auto = brightness for a light TILE7_LONGPRESS_OFF_VALUE: "0" # initial slider value when entity is off # if LONGPRESS = action: TILE7_LONGPRESS_ACTION: "" # entity_id to fire on long press TILE7_LONGPRESS_ACTION_TYPE: "" # entity type (empty = same as TILE*_TYPE) TILE7_LONGPRESS_ACTION_SERVICE: "" # service to call (default: activate/turn_on) # ============================================================ # TILE 8 — Mailbox reset # input_select.mailbox_status has options ['Full', 'Empty']. # The tile lights up as soon as the status is "Full" (ACTIVE_STATE), and a # short tap sets it back to "Empty" via input_select.select_option. # ============================================================ # --- basic settings --- TILE8_ENTITY: "light.unused_8" # main Home Assistant entity for this tile TILE8_STATE_ENTITY: "light.unused_8" # entity used for active/inactive state styling TILE8_TITLE: "Tile 8" # tile title shown in the UI TILE8_ICON: "\U000F06EE" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) TILE8_TYPE: "light" # generic type; the real behavior comes from TAP_SERVICE below TILE8_TAP_ACTION: "auto" # activate + TAP_SERVICE = a standalone service call TILE8_LONGPRESS: "none" # long press only sends the event to HA TILE8_VALUE_MODE: "auto" # shows LABEL_ON / LABEL_OFF TILE8_LABEL_OFF: "Off" # text shown when entity is off / inactive TILE8_LABEL_ON: "On" # text shown when entity is active TILE8_ACTIVE_STATE: "" # tile is "on" as soon as the status is exactly "Full" # --- colors --- TILE8_CIRCLE_ACTIVE_COLOR: "0xE5484D" # red as soon as there's mail TILE8_CIRCLE_DISABLED_COLOR: "0x7B7B6F" # icon circle color when inactive TILE8_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active TILE8_ICON_DISABLED_COLOR: "0xE5484D" # icon color when inactive TILE8_BG_ACTIVE_COLOR: "0xFFFFFF" # tile background color when active TILE8_BG_DISABLED_COLOR: "0x939391" # tile background color when inactive TILE8_TITLE_ACTIVE_COLOR: "0x000000" # title color when active TILE8_TITLE_DISABLED_COLOR: "0xFFFFFF" # title color when inactive TILE8_VALUE_ACTIVE_COLOR: "0x7A7A7C" # value color when active TILE8_VALUE_DISABLED_COLOR: "0xD9D9D9" # value color when inactive # --- advanced settings --- TILE8_TAP_SERVICE: "" # sets the input_select to a fixed option TILE8_TAP_PARAM_KEY: "" # parameter name for input_select.select_option TILE8_TAP_PARAM_VAL: "" # the "reset" value # if LONGPRESS = slider: TILE8_LONGPRESS_SLIDER: "auto" # not in use (LONGPRESS = none) TILE8_LONGPRESS_OFF_VALUE: "0" # not in use # if LONGPRESS = action: TILE8_LONGPRESS_ACTION: "" # entity_id to fire on long press TILE8_LONGPRESS_ACTION_TYPE: "" # entity type (empty = same as TILE*_TYPE) TILE8_LONGPRESS_ACTION_SERVICE: "" # service to call (default: activate/turn_on) # ============================================================ # TILE 9 — Living room # Moved from tile 2 to row 5 to make room for Ambient. # Colour lamp (color_temp / rgb / xy), so the colour button in the # slider overlay appears automatically. # ============================================================ # --- basic settings --- TILE9_ENTITY: "light.unused_9" # main Home Assistant entity for this tile TILE9_STATE_ENTITY: "light.unused_9" # entity used for active/inactive state styling TILE9_TITLE: "Tile 9" # tile title shown in the UI TILE9_ICON: "\U000F04B9" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) TILE9_TYPE: "light" # tile type: light | fan | switch | scene | script | cover | climate TILE9_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset TILE9_LONGPRESS: "none" # long press mode: none | slider | action | vacuum TILE9_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text TILE9_LABEL_OFF: "Off" # text shown when entity is off TILE9_LABEL_ON: "On" # text shown for text-mode tiles when entity is on TILE9_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") # --- colors --- TILE9_CIRCLE_ACTIVE_COLOR: "${ACCENT_AUTO}" # light -> ACCENT_LIGHT (amber) TILE9_CIRCLE_DISABLED_COLOR: "0x7B7B6F" # icon circle color when inactive TILE9_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active TILE9_ICON_DISABLED_COLOR: "${ACCENT_AUTO}" # icon color when inactive TILE9_BG_ACTIVE_COLOR: "0xFFFFFF" # tile background color when active TILE9_BG_DISABLED_COLOR: "0x939391" # tile background color when inactive TILE9_TITLE_ACTIVE_COLOR: "0x000000" # title color when active TILE9_TITLE_DISABLED_COLOR: "0xFFFFFF" # title color when inactive TILE9_VALUE_ACTIVE_COLOR: "0x7A7A7C" # value color when active TILE9_VALUE_DISABLED_COLOR: "0xD9D9D9" # value color when inactive # --- advanced settings --- TILE9_TAP_SERVICE: "" # optional service override, e.g. light.toggle TILE9_TAP_PARAM_KEY: "" # optional extra service parameter key TILE9_TAP_PARAM_VAL: "" # optional extra service parameter value # if LONGPRESS = slider: TILE9_LONGPRESS_SLIDER: "auto" # auto = brightness for a light TILE9_LONGPRESS_OFF_VALUE: "0" # initial slider value when entity is off # if LONGPRESS = action: TILE9_LONGPRESS_ACTION: "" # entity_id to fire on long press TILE9_LONGPRESS_ACTION_TYPE: "" # entity type (empty = same as TILE*_TYPE) TILE9_LONGPRESS_ACTION_SERVICE: "" # service to call (default: activate/turn_on) # ============================================================ # TILE 10 — Alarm on/off # input_boolean.alarm_engaged. An input_boolean doesn't respond to # switch.toggle, hence the explicit TAP_SERVICE. # ============================================================ # --- basic settings --- TILE10_ENTITY: "light.unused_10" # main Home Assistant entity for this tile TILE10_STATE_ENTITY: "light.unused_10" # entity used for active/inactive state styling TILE10_TITLE: "Tile 10" # tile title shown in the UI TILE10_ICON: "\U000F002A" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) TILE10_TYPE: "light" # generic on/off type; the service below does the real work TILE10_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset TILE10_LONGPRESS: "none" # long press only sends the event to HA TILE10_VALUE_MODE: "auto" # shows LABEL_ON / LABEL_OFF TILE10_LABEL_OFF: "Off" # text shown when entity is off TILE10_LABEL_ON: "On" # text shown when entity is on TILE10_ACTIVE_STATE: "" # input_boolean just uses on/off # --- colors --- TILE10_CIRCLE_ACTIVE_COLOR: "${ACCENT_AUTO}" # input_boolean -> ACCENT_DEVICE (cyan) TILE10_CIRCLE_DISABLED_COLOR: "0x7B7B6F" # icon circle color when inactive TILE10_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active TILE10_ICON_DISABLED_COLOR: "${ACCENT_AUTO}" # icon color when inactive TILE10_BG_ACTIVE_COLOR: "0xFFFFFF" # tile background color when active TILE10_BG_DISABLED_COLOR: "0x939391" # tile background color when inactive TILE10_TITLE_ACTIVE_COLOR: "0x000000" # title color when active TILE10_TITLE_DISABLED_COLOR: "0xFFFFFF" # title color when inactive TILE10_VALUE_ACTIVE_COLOR: "0x7A7A7C" # value color when active TILE10_VALUE_DISABLED_COLOR: "0xD9D9D9" # value color when inactive # --- advanced settings --- TILE10_TAP_SERVICE: "" # switch.toggle doesn't work on an input_boolean TILE10_TAP_PARAM_KEY: "" # optional extra service parameter key TILE10_TAP_PARAM_VAL: "" # optional extra service parameter value # if LONGPRESS = slider: TILE10_LONGPRESS_SLIDER: "auto" # not in use (LONGPRESS = none) TILE10_LONGPRESS_OFF_VALUE: "0" # not in use # if LONGPRESS = action: TILE10_LONGPRESS_ACTION: "" # entity_id to fire on long press TILE10_LONGPRESS_ACTION_TYPE: "" # entity type (empty = same as TILE*_TYPE) TILE10_LONGPRESS_ACTION_SERVICE: "" # service to call (default: activate/turn_on) # ============================================================ # ORIENTATION / LAYOUT # Uncomment ONE preset block below, comment out the others. # Touch calibration values (TOUCH_CAL_*) are device-specific — # portrait/flipped values are approximate and may need tuning. # # LVGL_ROTATION (0/90/180/270) drives the rotation — LVGL rotates both the # display output and the touch coordinates (ESPHome 2026.4+). DISPLAY_W/H are # the *logical* (post-rotation) canvas size used by the tile layout below; # the physical panel is always driven at its native 240×320 (see display:). # # Tile content layout: # Landscape — icon left, title + value to its right # Portrait — icon centered on top, title + value stacked below # ============================================================ # ----- 0° — Landscape 320×240 (default, USB on right) ----- ORIENTATION: "0" DISPLAY_W: "320" DISPLAY_H: "240" LVGL_ROTATION: "90" TOUCH_SWAP_XY: "false" # true on this board (resistive XPT2046) — was false on the 032c board # (capacitive GT911). This is the original upstream value for this panel. TOUCH_MIRROR_X: "true" TOUCH_MIRROR_Y: "false" # Measured on this board: median top-right raw=(523,3390), # bottom-left raw=(3487,482), at screen (299,20) and (20,219). # Extrapolated to panel edges; verify with the isolated corner test. TOUCH_CAL_X_MIN: "280" TOUCH_CAL_X_MAX: "3860" TOUCH_CAL_Y_MIN: "340" TOUCH_CAL_Y_MAX: "3860" GRID_COL1_X: "9" GRID_COL2_X: "164" GRID_ROW1_Y: "0" GRID_ROW2_Y: "56" GRID_ROW3_Y: "112" GRID_ROW4_Y: "168" GRID_ROW5_Y: "224" SCROLL_Y: "40" SCROLL_H: "164" TILE_W: "147" TILE_H: "52" HEADER_Y: "14" OVERLAY_SLIDER_W: "96" OVERLAY_SLIDER_H: "160" OVERLAY_ICON_Y: "74" # Alert overlay (HA action show_alert), landscape 320x240: one card centred on LVGL's top layer. ALERT_CARD_W: "292" ALERT_CARD_H: "196" ALERT_ICON_X: "18" ALERT_ICON_Y: "16" ALERT_TEXT_X: "62" ALERT_TEXT_W: "212" ALERT_TITLE_Y: "16" ALERT_SUBTITLE_Y: "44" ALERT_SUBTITLE_H: "88" ALERT_BUTTON_W: "100" ALERT_BUTTON_H: "40" ALERT_BUTTON_INSET: "14" ALERT_TITLE_MAX: "48" # bytes; the label ends one line with an ellipsis ALERT_BUTTON_MAX: "12" # bytes on the OK button ALERT_SUBTITLE_MAX: "160" # bytes; wraps over the rows the card has BG_TOP_COLOR: "0xE7E7E7" # light background, matching the Guition BG_BOTTOM_COLOR: "0xE7E7E7" TILE_ICON_X: "0" TILE_ICON_Y: "0" TILE_TITLE_X: "48" TILE_TITLE_Y: "4" TILE_VALUE_X: "48" TILE_VALUE_Y: "18" # ----- 90° — Portrait 240×320 (USB on bottom) ----- # ORIENTATION: "90" # DISPLAY_W: "240" # DISPLAY_H: "320" # LVGL_ROTATION: "180" # TOUCH_SWAP_XY: "false" # TOUCH_MIRROR_X: "true" # TOUCH_MIRROR_Y: "false" # TOUCH_CAL_X_MIN: "280" # TOUCH_CAL_X_MAX: "3860" # TOUCH_CAL_Y_MIN: "340" # TOUCH_CAL_Y_MAX: "3860" # GRID_COL1_X: "6" # GRID_COL2_X: "123" # GRID_ROW1_Y: "8" # GRID_ROW2_Y: "96" # GRID_ROW3_Y: "184" # GRID_ROW4_Y: "272" # GRID_ROW5_Y: "360" # SCROLL_Y: "44" # SCROLL_H: "276" # TILE_W: "111" # TILE_H: "80" # HEADER_Y: "14" # OVERLAY_SLIDER_W: "96" # OVERLAY_SLIDER_H: "200" # OVERLAY_ICON_Y: "94" # BG_TOP_COLOR: "0xAEC4D9" # background top (soft blue) # BG_BOTTOM_COLOR: "0xD6BB9E" # background bottom (warm sand) # TILE_ICON_X: "0" # TILE_ICON_Y: "0" # TILE_TITLE_X: "4" # TILE_TITLE_Y: "38" # TILE_VALUE_X: "4" # TILE_VALUE_Y: "52" # ----- 180° — Landscape flipped 320×240 (USB on left) ----- # ORIENTATION: "180" # DISPLAY_W: "320" # DISPLAY_H: "240" # LVGL_ROTATION: "270" # TOUCH_SWAP_XY: "false" # TOUCH_MIRROR_X: "true" # TOUCH_MIRROR_Y: "false" # TOUCH_CAL_X_MIN: "280" # TOUCH_CAL_X_MAX: "3860" # TOUCH_CAL_Y_MIN: "340" # TOUCH_CAL_Y_MAX: "3860" # GRID_COL1_X: "9" # GRID_COL2_X: "164" # GRID_ROW1_Y: "8" # GRID_ROW2_Y: "68" # GRID_ROW3_Y: "128" # GRID_ROW4_Y: "188" # GRID_ROW5_Y: "248" # SCROLL_Y: "44" # SCROLL_H: "196" # TILE_W: "147" # TILE_H: "52" # HEADER_Y: "14" # OVERLAY_SLIDER_W: "96" # OVERLAY_SLIDER_H: "160" # OVERLAY_ICON_Y: "74" # BG_TOP_COLOR: "0xAEC4D9" # background top (soft blue) # BG_BOTTOM_COLOR: "0xD6BB9E" # background bottom (warm sand) # TILE_ICON_X: "0" # TILE_ICON_Y: "0" # TILE_TITLE_X: "48" # TILE_TITLE_Y: "4" # TILE_VALUE_X: "48" # TILE_VALUE_Y: "18" # ----- 270° — Portrait flipped 240×320 (USB on top) ----- # ORIENTATION: "270" # DISPLAY_W: "240" # DISPLAY_H: "320" # LVGL_ROTATION: "0" # TOUCH_SWAP_XY: "false" # TOUCH_MIRROR_X: "true" # TOUCH_MIRROR_Y: "false" # TOUCH_CAL_X_MIN: "280" # TOUCH_CAL_X_MAX: "3860" # TOUCH_CAL_Y_MIN: "340" # TOUCH_CAL_Y_MAX: "3860" # GRID_COL1_X: "6" # GRID_COL2_X: "123" # GRID_ROW1_Y: "8" # GRID_ROW2_Y: "96" # GRID_ROW3_Y: "184" # GRID_ROW4_Y: "272" # GRID_ROW5_Y: "360" # SCROLL_Y: "44" # SCROLL_H: "276" # TILE_W: "111" # TILE_H: "80" # HEADER_Y: "14" # OVERLAY_SLIDER_W: "96" # OVERLAY_SLIDER_H: "200" # OVERLAY_ICON_Y: "94" # BG_TOP_COLOR: "0xAEC4D9" # background top (soft blue) # BG_BOTTOM_COLOR: "0xD6BB9E" # background bottom (warm sand) # TILE_ICON_X: "0" # TILE_ICON_Y: "0" # TILE_TITLE_X: "4" # TILE_TITLE_Y: "38" # TILE_VALUE_X: "4" # TILE_VALUE_Y: "52" # ============================================================ # No need to edit below this line. # All configuration is done via the substitutions above. # Credentials are managed in secrets.yaml (see secrets.yaml.example). # The Material Design Icons font is included at fonts/materialdesignicons-webfont.ttf (Apache 2.0). # Source: https://github.com/Templarian/MaterialDesign-Webfont # ============================================================ external_components: - source: type: local path: components components: [xpt2046, smart_display] esphome: min_version: 2026.6.2 project: name: maxgramser.esp_screen version: "${SCREEN_FIRMWARE_VERSION}" platformio_options: build_flags: - -DLV_USE_SWITCH=1 includes: - cyd_ui.h - light_controls.h - alert_overlay.h name: ${DEVICE_NAME} friendly_name: ${DEVICE_FRIENDLY_NAME} on_boot: priority: -100 then: - lambda: |- // Per-panel affine correction; shared defaults are neutral. id(ts_touch).set_raw_correction(${TOUCH_AFFINE_XX}f, ${TOUCH_AFFINE_XY}f, ${TOUCH_AFFINE_XC}f, ${TOUCH_AFFINE_YX}f, ${TOUCH_AFFINE_YY}f, ${TOUCH_AFFINE_YC}f); cyd::touch_guard.configure(${TOUCH_MOVE_LIMIT_PX}, ${TOUCH_MIN_PRESS_MS}); id(last_touch_ms) = millis(); id(display_dimmed) = false; lv_obj_set_style_pad_row(id(vacuum_actions), 4, 0); lv_obj_set_style_pad_column(id(vacuum_actions), 4, 0); lv_obj_set_style_pad_row(id(vacuum_speeds), 4, 0); lv_obj_set_style_pad_column(id(vacuum_speeds), 4, 0); - lambda: |- auto &settings = screen_settings::current; settings.standby_enabled = std::string("${AUTO_DIM}") == "true"; settings.standby_seconds = atoi("${AUTO_DIM_TIMEOUT}"); settings.brightness = atoi("${MAX_BRIGHTNESS}"); settings.standby_brightness = atoi("${DIM_BRIGHTNESS}"); settings.night_enabled = std::string("${NIGHT_MODE}") == "true"; settings.night_start = atoi("${NIGHT_START_HOUR}") * 60; settings.night_end = atoi("${NIGHT_END_HOUR}") * 60; settings.night_brightness = atoi("${NIGHT_DIM_BRIGHTNESS}"); runtime_tiles::watch_font = id(headline)->get_lv_font(); runtime_tiles::watch_value_font = id(watch_value)->get_lv_font(); runtime_tiles::watch_icon_font = id(watch_icon)->get_lv_font(); runtime_tiles::clock_font = id(clock_digits)->get_lv_font(); runtime_tiles::now_time = []() { return id(homeassistant_time).now(); }; runtime_tiles::mini_icon_font = id(materialdesign_icons_mini)->get_lv_font(); runtime_tiles::control_font = id(sublabel_big)->get_lv_font(); // Top bar (0.2.32+): values in the text font, icons in the small icon font; the clock // label lends its margin and drawing order and stays hidden in runtime mode. runtime_tiles::header_text_font = id(sublabel_big)->get_lv_font(); runtime_tiles::header_icon_font = id(materialdesign_icons_mini)->get_lv_font(); runtime_tiles::time_label = id(lbl_time); runtime_tiles::enabled = std::string("${DYNAMIC_TILES}") == "true"; if (!runtime_tiles::enabled) return; runtime_tiles::load_settings(); runtime_tiles::settings_changed = []() { id(apply_screen_settings).execute(); }; runtime_tiles::dismiss = []() { runtime_tiles::hide_detail(); id(active_entity).clear(); lv_obj_add_flag(id(brightness_overlay), LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(id(color_detail_overlay), LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(id(climate_detail_overlay), LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(id(climate_mode_overlay), LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(id(vacuum_overlay), LV_OBJ_FLAG_HIDDEN); }; runtime_tiles::layout_changed = []() { id(show_tile_page).execute(); }; lv_obj_add_event_cb(id(home_page)->obj, [](lv_event_t *event) { if(!runtime_tiles::enabled || !runtime_tiles::swipe_pages || id(display_dimmed) || id(calibration_active) || !id(active_entity).empty() || (runtime_tiles::detail_root && !lv_obj_has_flag(runtime_tiles::detail_root,LV_OBJ_FLAG_HIDDEN)))return; auto *input=lv_indev_active();if(!input)return; auto direction=lv_indev_get_gesture_dir(input); if(direction!=LV_DIR_LEFT && direction!=LV_DIR_RIGHT)return; cyd::touch_guard.consume(); lv_indev_wait_release(input); id(tile_page)+=direction==LV_DIR_LEFT?1:-1; id(show_tile_page).execute(); }, LV_EVENT_GESTURE, nullptr); runtime_tiles::refresh = []() { if (!id(ui_refresh).is_running()) id(ui_refresh).execute(); }; runtime_tiles::bind(0, id(tile1), id(t1_title), id(t1_value), id(tile1_icon_circle), id(tile1_icon_lbl)); runtime_tiles::bind(1, id(tile2), id(t2_title), id(t2_value), id(tile2_icon_circle), id(tile2_icon_lbl)); runtime_tiles::bind(2, id(tile3), id(t3_title), id(t3_value), id(tile3_icon_circle), id(tile3_icon_lbl)); runtime_tiles::bind(3, id(tile4), id(t4_title), id(t4_value), id(tile4_icon_circle), id(tile4_icon_lbl)); runtime_tiles::bind(4, id(tile5), id(t5_title), id(t5_value), id(tile5_icon_circle), id(tile5_icon_lbl)); runtime_tiles::bind(5, id(tile6), id(t6_title), id(t6_value), id(tile6_icon_circle), id(tile6_icon_lbl)); runtime_tiles::bind(6, id(tile7), id(t7_title), id(t7_value), id(tile7_icon_circle), id(tile7_icon_lbl)); runtime_tiles::bind(7, id(tile8), id(t8_title), id(t8_value), id(tile8_icon_circle), id(tile8_icon_lbl)); runtime_tiles::bind(8, id(tile9), id(t9_title), id(t9_value), id(tile9_icon_circle), id(tile9_icon_lbl)); runtime_tiles::bind(9, id(tile10), id(t10_title), id(t10_value), id(tile10_icon_circle), id(tile10_icon_lbl)); runtime_tiles::detail = [](runtime_tiles::Tile &t) { if (t.domain() == "vacuum") { lv_obj_t *buttons[] = {id(vacspd_quiet), id(vacspd_balanced), id(vacspd_turbo), id(vacspd_max)}; for (unsigned i = 0; i < 4; ++i) { if (i < t.fan_speed_count) { lv_obj_clear_flag(buttons[i], LV_OBJ_FLAG_HIDDEN); lv_label_set_text(lv_obj_get_child(buttons[i], 0), t.fan_speeds[i].c_str()); } else lv_obj_add_flag(buttons[i], LV_OBJ_FLAG_HIDDEN); } id(open_vacuum_overlay).execute(t.name, t.entity); return; } light_controls::fallback = light_controls::State{}; light_controls::fallback.hue = t.hue; light_controls::fallback.kelvin = t.kelvin; light_controls::fallback.minimum = t.min_kelvin; light_controls::fallback.maximum = t.max_kelvin; id(active_climate_fan_modes) = t.fan_modes; id(active_climate_fan_mode) = t.fan_mode; id(active_climate_swing_modes) = t.swing_modes; id(active_climate_swing_mode) = t.swing_mode; id(open_value_overlay).execute(t.name, t.entity, t.domain(), "auto", t.active(), 0, t.brightness, t.percentage, t.position, runtime_tiles::icon_for(t), 0xD5EEFC, 0x00A6ED, 0x1B1B1B, "Off", "On", t.modes, t.current, t.target, t.humidity, t.state, t.hvac_modes, std::to_string(t.minimum), std::to_string(t.maximum), std::to_string(t.step)); }; runtime_tiles::detail_update = [](runtime_tiles::Tile &t) { if (id(active_entity) != t.entity || id(touch_down)) return; if (t.domain() == "climate" && !lv_obj_has_flag(id(climate_detail_overlay), LV_OBJ_FLAG_HIDDEN)) { if (id(climate_edit_pending) && millis() - id(climate_edit_ms) < 3000) return; id(climate_edit_pending) = false; id(active_climate_mode) = t.state; id(active_climate_supported_modes) = t.hvac_modes; lv_label_set_text(id(climate_mode_label), tile_controls::climate_mode_text(t.state)); id(climate_power_refresh).execute(); id(active_climate_fan_modes) = t.fan_modes; id(active_climate_fan_mode) = t.fan_mode; id(active_climate_swing_modes) = t.swing_modes; id(active_climate_swing_mode) = t.swing_mode; char buf[72]; if (std::isfinite(t.current)) snprintf(buf, sizeof(buf), "Now: %.1f°", t.current); else snprintf(buf, sizeof(buf), "Now: --"); lv_label_set_text(id(climate_current_label), buf); if (std::isfinite(t.target)) { int target = cyd::quantize_temperature(lround(t.target * 10), id(active_climate_min_x10), id(active_climate_max_x10), id(active_climate_step_x10)); id(active_climate_target_x10) = target; snprintf(buf, sizeof(buf), "%.1f°", target / 10.0f); lv_label_set_text(id(climate_target_label), buf); lv_arc_set_value(id(climate_arc), target); } } }; - lambda: |- static const char *entities[] = {"${TILE1_ENTITY}", "${TILE2_ENTITY}", "${TILE3_ENTITY}", "${TILE4_ENTITY}", "${TILE5_ENTITY}", "${TILE6_ENTITY}", "${TILE7_ENTITY}", "${TILE9_ENTITY}"}; if (!runtime_tiles::enabled) light_controls::subscribe(entities, 8); light_controls::setup(id(color_detail_overlay), id(sublabel)->get_lv_font(), atoi("${DISPLAY_W}"), atoi("${DISPLAY_H}")); light_controls::commits[0] = [](int value) { id(set_light_hue).execute(value); }; light_controls::commits[1] = [](int value) { id(set_light_color_temperature).execute(value); }; light_controls::commits[2] = [](int value) { id(color_brightness_commit).execute(value); }; - lambda: |- if (!runtime_tiles::enabled) return; screen_calibration::apply = [](const screen_calibration::Calibration &cal) { id(ts_touch).set_raw_correction(cal.c[0],cal.c[1],cal.c[2],cal.c[3],cal.c[4],cal.c[5]); }; screen_calibration::isolation = [](bool active) { id(calibration_active) = active; }; screen_calibration::setup(id(home_page)->obj, id(sublabel)->get_lv_font(), id(headline)->get_lv_font(), ${TOUCH_CAL_X_MIN}, ${TOUCH_CAL_X_MAX}, ${TOUCH_CAL_Y_MIN}, ${TOUCH_CAL_Y_MAX}); - light.turn_on: id: back_light brightness: !lambda "return screen_settings::current.brightness / 100.0f;" - script.execute: show_tile_page - lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' - if: condition: lambda: 'return std::string("${CALIBRATION_ON_BOOT}") == "true";' then: - lambda: 'id(calibration_active) = true;' - lvgl.page.show: touch_cal_page - lambda: |- lv_obj_invalidate(id(touch_cal_page)->obj); lv_refr_now(nullptr); ESP_LOGI("ui_diag", "USB calibration ready; no tile actions"); esp32: board: esp32dev framework: type: esp-idf advanced: # ESPHome recommends this itself in the boot log. Yields ~40KB extra IRAM # and a smaller binary, and every KB counts on this board. sram1_as_iram: true # minimum_chip_revision deliberately left out: that was here for the # 032c board (rev 3.1 confirmed), but this is a different physical board # with an unknown chip revision. With an unjustifiably high minimum # the firmware simply refuses to boot. version: recommended variant: esp32 logger: # Set to DEBUG or VERBOSE temporarily for diagnosis. Note: ESPHome logs # its configuration lines at CONFIG level, which are invisible at INFO. level: DEBUG logs: sensor: WARN text_sensor: WARN xpt2046: WARN homeassistant.sensor: WARN homeassistant.text_sensor: WARN # Credentials are loaded from secrets.yaml — copy secrets.yaml.example to secrets.yaml and fill in your values. api: actions: - action: touch_diagnostics then: - lambda: 'id(calibration_active) = true;' - script.execute: wake_display - lvgl.page.show: touch_cal_page - lambda: |- lv_obj_invalidate(id(touch_cal_page)->obj); lv_refr_now(nullptr); ESP_LOGI("ui_diag", "calibration page active=%d", lv_screen_active() == id(touch_cal_page)->obj); - action: end_touch_diagnostics then: - lambda: 'id(calibration_active) = false;' - lvgl.page.show: home_page - action: ui_state then: - lambda: |- const auto &cfg = screen_settings::current; ESP_LOGI("ui_diag", "settings standby=%d timeout=%d brightness=%d dim=%d night=%d start=%d end=%d night_dim=%d clock=%d clock24=%d home=%d idle_ms=%u dimmed=%d", cfg.standby_enabled, cfg.standby_seconds, cfg.brightness, cfg.standby_brightness, cfg.night_enabled, cfg.night_start, cfg.night_end, cfg.night_brightness, cfg.show_clock, cfg.clock_24h, cfg.home_on_standby, (unsigned)(millis() - id(last_touch_ms)), id(display_dimmed)); ESP_LOGI("ui_diag", "navigation page=%d runtime_detail=%d", id(tile_page), runtime_tiles::detail_root && !lv_obj_has_flag(runtime_tiles::detail_root, LV_OBJ_FLAG_HIDDEN)); lv_area_t nav, grid; lv_obj_update_layout(id(home_page)->obj); lv_obj_get_coords(id(page_next), &nav); lv_obj_get_coords(id(tile_scroll), &grid); ESP_LOGI("ui_diag", "home=%d calibration=%d grid=%d,%d,%d,%d next=%d,%d,%d,%d", lv_screen_active() == id(home_page)->obj, lv_screen_active() == id(touch_cal_page)->obj, grid.x1, grid.y1, grid.x2, grid.y2, nav.x1, nav.y1, nav.x2, nav.y2); - action: preview_runtime_card variables: index: int then: - lambda: |- if (index < 0) { runtime_tiles::hide_detail(); if (runtime_tiles::dismiss) runtime_tiles::dismiss(); return; } if (runtime_tiles::enabled && index < (int)runtime_tiles::model.count) { runtime_tiles::active_index = index; auto &tile = runtime_tiles::model.tiles[index]; auto domain = tile.domain(); // Lights, fans, covers and climate open the board's own card, like a long press does. if ((domain == "light" || domain == "fan" || domain == "cover" || domain == "climate") && runtime_tiles::detail) { tile.begin(millis(), true); runtime_tiles::detail(tile); return; } runtime_tiles::show_detail(index); } # Diagnostics: the climate mode picker and the busy sheet of a tile, without touching the glass or HA. - action: preview_climate_picker then: - script.execute: open_climate_mode_picker - action: preview_busy variables: index: int then: - lambda: |- if (runtime_tiles::enabled && index >= 0 && index < (int)runtime_tiles::model.count) { runtime_tiles::model.tiles[index].begin(millis()); if (runtime_tiles::refresh) runtime_tiles::refresh(); } - action: light_controls_preview then: - script.execute: light_controls_preview - action: ui_self_test then: - script.execute: ui_self_test # Home Assistant automations: esphome._show_alert puts a card over the whole # screen (README, "Alert from an automation"); dismiss_alert removes it again. - action: show_alert variables: title: string subtitle: string icon: string color: string button_text: string timeout: int flash: bool then: - script.execute: id: alert_show title: !lambda 'return title;' subtitle: !lambda 'return subtitle;' icon: !lambda 'return icon;' color: !lambda 'return color;' button_text: !lambda 'return button_text;' timeout: !lambda 'return timeout;' flash: !lambda 'return flash;' - action: dismiss_alert then: - script.execute: id: alert_dismiss reason: "remote" # ESP Screen Manager 0.2.39+ hands a whole tile message over in one call instead of # base64 chunks in the text inbox; the inbox keeps showing the result. - action: screen_message variables: message: string then: - lambda: 'id(dashboard_inbox).publish_state(runtime_tiles::receive(message));' encryption: key: !secret api_encryption_key ota: - platform: esphome password: !secret ota_password wifi: ssid: !secret wifi_ssid password: !secret wifi_password # Mains-powered panel: no modem sleep, so HA updates and OTA arrive without beacon latency. power_save_mode: none ap: ssid: "Smartdisplay Fallback Hotspot" password: !secret ap_password captive_portal: interval: - interval: 250ms then: - lambda: runtime_tiles::tick(); - interval: 1s then: - if: condition: lambda: 'return id(climate_edit_pending) && !id(touch_down) && millis() - id(climate_edit_ms) >= 3000;' then: - lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' - if: condition: lambda: |- if (!screen_settings::current.standby_enabled) return false; if (id(display_dimmed) || id(touch_down) || id(calibration_active) || id(alert_active)) return false; return (millis() - id(last_touch_ms)) > (screen_settings::current.standby_seconds * 1000UL); then: - script.execute: dim_display time: - platform: homeassistant id: homeassistant_time # Refresh immediately on time sync; do not wait for an HA entity update. update_interval: 60s on_time_sync: then: - script.execute: apply_screen_settings - lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' on_time: - seconds: 0 minutes: /1 then: - script.execute: apply_screen_settings - lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' globals: # Alert overlay (HA action show_alert): standby waits while one is up. - id: alert_active type: bool initial_value: 'false' - id: alert_title_text type: std::string - id: alert_timeout_seconds type: int initial_value: '0' - id: alert_flash_requested type: bool initial_value: 'false' - id: rendered_frames type: uint32_t initial_value: '0' - id: calibration_active type: bool initial_value: 'false' - id: tile_page type: int initial_value: '0' - id: touch_down type: bool initial_value: 'false' - id: climate_edit_ms type: uint32_t initial_value: '0' - id: climate_edit_pending type: bool initial_value: 'false' - id: active_entity type: std::string restore_value: no - id: active_control_kind type: std::string restore_value: no - id: active_value_suffix type: std::string restore_value: no - id: last_touch_ms type: uint32_t restore_value: no initial_value: '0' # Debounce for do_tile_action: this resistive XPT2046 panel sometimes gives # multiple separate press/release cycles for one physical touch (noise in # the resistive layer), causing the same HA action to fire multiple times # within a few hundred ms (observed: script.ambient_morning 6x in 8s). - id: last_action_entity type: std::string restore_value: no - id: last_action_ms type: uint32_t restore_value: no initial_value: '0' - id: display_dimmed type: bool restore_value: no initial_value: 'false' - id: active_label_off type: std::string restore_value: no - id: active_label_on type: std::string restore_value: no - id: active_title type: std::string restore_value: no - id: active_icon_text type: std::string restore_value: no - id: active_brightness_pct type: int restore_value: no initial_value: '100' - id: active_supports_color type: bool restore_value: no initial_value: 'false' - id: active_supports_temperature type: bool restore_value: no initial_value: 'false' - id: active_color_detail_tab type: int restore_value: no initial_value: '0' - id: active_climate_current_x10 type: int restore_value: no initial_value: '0' - id: active_climate_target_x10 type: int restore_value: no initial_value: '200' - id: active_climate_humidity type: int restore_value: no initial_value: '-1' - id: active_climate_min_x10 type: int restore_value: no initial_value: '50' - id: active_climate_max_x10 type: int restore_value: no initial_value: '300' - id: active_climate_step_x10 type: int restore_value: no initial_value: '5' - id: active_climate_mode type: std::string restore_value: no - id: active_climate_power_on type: bool restore_value: no initial_value: 'false' - id: active_climate_fan_modes type: std::string restore_value: no - id: active_climate_fan_mode type: std::string restore_value: no - id: active_climate_swing_modes type: std::string restore_value: no - id: active_climate_swing_mode type: std::string restore_value: no - id: active_climate_supported_modes type: std::string restore_value: no font: - file: "fonts/Roboto-500.ttf" id: time_label size: 18 bpp: 4 glyphs: ['0','1','2','3','4','5','6','7','8','9',':',' ', 'A', 'M', 'P'] - file: "fonts/Roboto-500.ttf" id: headline size: 18 bpp: 4 glyphs: ['<','>','—','&','@','!',',','.','?','"','%','(',')','+','-','_',':','°', '0','1','2','3','4','5','6','7','8','9', 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z', ' ','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z', 'ä','ö','ü','Ä','Ö','Ü','ß','/','·'] - file: "fonts/Roboto-500.ttf" id: watch_value size: 22 bpp: 4 glyphs: ['<','>','—','&','@','!',',','.','?','"','%','(',')','+','-','_',':','°', '0','1','2','3','4','5','6','7','8','9', 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z', ' ','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z', 'ä','ö','ü','Ä','Ö','Ü','ß','/','·'] # Clock card digits only; the date line reuses the tile title font. - file: "fonts/Roboto-500.ttf" id: clock_digits size: 24 bpp: 4 glyphs: [':','-','0','1','2','3','4','5','6','7','8','9'] - file: "fonts/Roboto-700.ttf" id: label size: 11 bpp: 4 glyphs: ['<','>','—','&','@','!',',','.','?','"','%','(',')','+','-','_',':','°', '0','1','2','3','4','5','6','7','8','9', 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z', ' ','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z', 'ä','ö','ü','Ä','Ö','Ü','ß','/','·'] - file: "fonts/Roboto-400.ttf" id: sublabel size: 11 bpp: 4 glyphs: ['<','>','—','&','@','!',',','.','?','"','%','(',')','+','-','_',':','°', '0','1','2','3','4','5','6','7','8','9', 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z', ' ','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z', 'ä','ö','ü','Ä','Ö','Ü','ß','/','…','·'] - file: "fonts/Roboto-400.ttf" id: sublabel_big size: 14 bpp: 4 glyphs: ['<','>','—','&','@','!',',','.','?','"','%','(',')','+','-','_',':','°', '0','1','2','3','4','5','6','7','8','9', 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z', ' ','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z', 'ä','ö','ü','Ä','Ö','Ü','ß','/','…','·',"'",'#','*','=',';','²','³','µ','é','è','ë','ê','ï','î','ó','ô','à','á','â','ç','ñ','ú','û','–'] # Material Design Icons (MDI) — included at fonts/materialdesignicons-webfont.ttf (Apache 2.0) # Source: https://github.com/Templarian/MaterialDesign-Webfont - file: 'fonts/materialdesignicons-webfont.ttf' id: materialdesign_icons size: 28 glyphs: &tile_icons # GENERATED by tools/generate_icons.py from screen_manager/app/tile_icons.py - "\U000F0335" # lightbulb - "\U000F1253" # lightbulb-group - "\U000F0769" # ceiling-light - "\U000F1793" # chandelier - "\U000F06B5" # lamp - "\U000F08DD" # floor-lamp - "\U000F095F" # desk-lamp - "\U000F091D" # wall-sconce-flat - "\U000F179B" # light-recessed - "\U000F04C9" # spotlight-beam - "\U000F1051" # led-strip-variant - "\U000F1054" # outdoor-lamp - "\U000F02DC" # home - "\U000F04B9" # sofa - "\U000F02E3" # bed - "\U000F0A70" # silverware-fork-knife - "\U000F0B7C" # chef-hat - "\U000F09A0" # shower - "\U000F1818" # bathtub - "\U000F09AB" # toilet - "\U000F1239" # desk - "\U000F06D9" # garage - "\U000F0E2E" # fireplace - "\U000F050F" # thermometer - "\U000F0393" # thermostat - "\U000F001B" # air-conditioner - "\U000F0438" # radiator - "\U000F1A43" # heat-pump - "\U000F0238" # fire - "\U000F0717" # snowflake - "\U000F058E" # water-percent - "\U000F0210" # fan - "\U000F1797" # ceiling-fan - "\U000F0D44" # air-purifier - "\U000F07E4" # molecule-co2 - "\U000F0599" # weather-sunny - "\U000F0595" # weather-partly-cloudy - "\U000F0590" # weather-cloudy - "\U000F0597" # weather-rainy - "\U000F0598" # weather-snowy - "\U000F059D" # weather-windy - "\U000F0594" # weather-night - "\U000F059C" # weather-sunset-up - "\U000F0387" # music-note - "\U000F075A" # music - "\U000F0CB8" # playlist-music - "\U000F0025" # album - "\U000F099A" # record-player - "\U000F04C7" # spotify - "\U000F0439" # radio - "\U000F0994" # podcast - "\U000F04C3" # speaker - "\U000F0D38" # speaker-multiple - "\U000F17DB" # soundbar - "\U000F02CB" # headphones - "\U000F067D" # piano - "\U000F0771" # guitar-acoustic - "\U000F0502" # television - "\U000F042E" # projector - "\U000F0FCE" # movie-open - "\U000F0118" # cast - "\U000F0297" # gamepad-variant - "\U000F0EC5" # remote-tv - "\U000F033E" # lock - "\U000F0FC6" # lock-open-variant - "\U000F068A" # shield-home - "\U000F078F" # alarm-light - "\U000F009A" # bell - "\U000F12E6" # doorbell - "\U000F07AE" # cctv - "\U000F0D91" # motion-sensor - "\U000F081B" # door-closed - "\U000F081C" # door-open - "\U000F11DB" # window-closed-variant - "\U000F0392" # smoke-detector - "\U000F1502" # water-alert - "\U000F072A" # washing-machine - "\U000F0917" # tumble-dryer - "\U000F0AAC" # dishwasher - "\U000F0290" # fridge - "\U000F04DE" # stove - "\U000F0C99" # microwave - "\U000F0CD3" # toaster-oven - "\U000F109F" # coffee-maker - "\U000F05FA" # kettle - "\U000F070D" # robot-vacuum - "\U000F11F7" # robot-mower - "\U000F042B" # printer-3d - "\U000F06A5" # power-plug - "\U000F07E7" # power-socket-eu - "\U000F0322" # laptop - "\U000F0469" # router-wireless - "\U000F0241" # flash - "\U000F0A72" # solar-power - "\U000F0D9B" # solar-panel - "\U000F1901" # home-battery - "\U000F12A3" # battery-high - "\U000F05F1" # ev-station - "\U000F0B6C" # car-electric - "\U000F1A57" # meter-electric - "\U000F1A59" # meter-gas - "\U000F058C" # water - "\U000F0F92" # water-boiler - "\U000F029A" # gauge - "\U000F111C" # window-shutter - "\U000F111E" # window-shutter-open - "\U000F00AC" # blinds - "\U000F1011" # blinds-open - "\U000F1A6C" # roller-shade-closed - "\U000F1846" # curtains - "\U000F1847" # curtains-closed - "\U000F1B88" # awning-outline - "\U000F024A" # flower - "\U000F0531" # tree - "\U000F0E66" # sprout - "\U000F1510" # grass - "\U000F105F" # sprinkler - "\U000F0606" # pool - "\U000F0E45" # grill - "\U000F06EE" # mailbox - "\U000F0A79" # trash-can - "\U000F044C" # recycle - "\U000F0A43" # dog - "\U000F011B" # cat - "\U000F0004" # account - "\U000F0849" # account-group - "\U000F0A89" # account-child - "\U000F0826" # home-account - "\U000F04B2" # sleep - "\U000F070E" # run - "\U000F00D6" # briefcase - "\U000F010B" # car - "\U000F00A3" # bike - "\U000F001D" # airplane - "\U000F034E" # map-marker - "\U000F0425" # power - "\U000F0521" # toggle-switch - "\U000F12A8" # gesture-tap-button - "\U000F040A" # play - "\U000F0BC2" # script-text - "\U000F03D8" # palette - "\U000F0020" # alarm - "\U000F0150" # clock-outline - "\U000F051B" # timer-outline - "\U000F00ED" # calendar - "\U000F009E" # bell-ring - "\U000F02D1" # heart - "\U000F002A" # alert-outline - "\U000F0493" # cog - "\U000F00E2" # broom - "\U000F05D6" # alert-circle-outline - "\U000F012C" # check - "\U000F03E4" # pause - "\U000F04DB" # stop - "\U000F04AD" # skip-next - "\U000F04AE" # skip-previous - "\U000F057E" # volume-high - "\U000F0581" # volume-off - "\U000F005D" # arrow-up - "\U000F0045" # arrow-down - "\U000F084E" # arrow-expand-horizontal - "\U000F084C" # arrow-collapse-horizontal - "\U000F05F8" # home-map-marker - "\U000F0415" # plus - "\U000F0374" # minus - "\U000F0141" # chevron-left - "\U000F0142" # chevron-right - "\U000F0156" # close - "\U000F1A79" # sun-snowflake-variant - "\U000F1B17" # thermostat-auto - "\U000F0591" # weather-fog - "\U000F0592" # weather-hail - "\U000F0593" # weather-lightning - "\U000F067E" # weather-lightning-rainy - "\U000F0596" # weather-pouring - "\U000F067F" # weather-snowy-rainy - "\U000F059B" # weather-sunset-down - file: 'fonts/materialdesignicons-webfont.ttf' id: materialdesign_icons_mini size: 18 glyphs: *tile_icons - file: 'fonts/materialdesignicons-webfont.ttf' id: watch_icon size: 12 glyphs: *tile_icons # Make memory visible: this board has no PSRAM, so the free heap determines # how much UI still fits. Also shows up as an entity in Home Assistant. debug: # Heap figures every five minutes: enough to spot a leak, few recorder rows. update_interval: 300s sensor: - platform: debug free: name: "Heap Free" id: heap_free on_value: - lambda: 'ESP_LOGI("health", "free_heap=%.0f", x);' fragmentation: name: "Heap Fragmentation" block: name: "Heap Largest Block" on_value: - lambda: 'ESP_LOGI("health", "largest_block=%.0f", x);' - platform: wifi_signal name: "Wifi Signal" update_interval: 600s # When the screen booted, as a timestamp: one value per boot instead of a new recorder # row every 15 s. A new entity on purpose: Home Assistant keeps the old Uptime's unit # "s" in its registry and would refuse a timestamp under that name. - platform: uptime type: timestamp name: "Last boot" id: boot_time - platform: homeassistant id: tile1_brightness entity_id: ${TILE1_ENTITY} attribute: brightness on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile1_percentage entity_id: ${TILE1_ENTITY} attribute: percentage on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile2_brightness entity_id: ${TILE2_ENTITY} attribute: brightness on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile2_percentage entity_id: ${TILE2_ENTITY} attribute: percentage on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile3_brightness entity_id: ${TILE3_ENTITY} attribute: brightness on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile3_percentage entity_id: ${TILE3_ENTITY} attribute: percentage on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile4_brightness entity_id: ${TILE4_ENTITY} attribute: brightness on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile4_percentage entity_id: ${TILE4_ENTITY} attribute: percentage on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile5_brightness entity_id: ${TILE5_ENTITY} attribute: brightness on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile5_percentage entity_id: ${TILE5_ENTITY} attribute: percentage on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile6_brightness entity_id: ${TILE6_ENTITY} attribute: brightness on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile6_percentage entity_id: ${TILE6_ENTITY} attribute: percentage on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } # Tile 7 (Table lamp) is a brightness-only light: just brightness. # Deliberately no percentage / cover_position / climate sensors — every # HA subscription costs heap on a board without PSRAM. - platform: homeassistant id: tile7_brightness entity_id: ${TILE7_ENTITY} attribute: brightness on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } # Tile 9 (Living room) is a colour lamp: brightness + supported_color_modes. # No percentage / cover / climate — this tile never reads those. - platform: homeassistant id: tile9_brightness entity_id: ${TILE9_ENTITY} attribute: brightness on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile1_cover_position entity_id: ${TILE1_ENTITY} attribute: current_position on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile2_cover_position entity_id: ${TILE2_ENTITY} attribute: current_position on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile3_cover_position entity_id: ${TILE3_ENTITY} attribute: current_position on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile4_cover_position entity_id: ${TILE4_ENTITY} attribute: current_position on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile5_cover_position entity_id: ${TILE5_ENTITY} attribute: current_position on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile6_cover_position entity_id: ${TILE6_ENTITY} attribute: current_position on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile1_climate_current_temperature entity_id: ${TILE1_ENTITY} attribute: ${TILE1_CLIMATE_CURRENT_TEMP_ATTRIBUTE} on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile1_climate_target_temperature entity_id: ${TILE1_ENTITY} attribute: ${TILE1_CLIMATE_TARGET_TEMP_ATTRIBUTE} on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile1_climate_humidity entity_id: ${TILE1_ENTITY} attribute: ${TILE1_CLIMATE_HUMIDITY_ATTRIBUTE} on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile2_climate_current_temperature entity_id: ${TILE2_ENTITY} attribute: ${TILE2_CLIMATE_CURRENT_TEMP_ATTRIBUTE} on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile2_climate_target_temperature entity_id: ${TILE2_ENTITY} attribute: ${TILE2_CLIMATE_TARGET_TEMP_ATTRIBUTE} on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile2_climate_humidity entity_id: ${TILE2_ENTITY} attribute: ${TILE2_CLIMATE_HUMIDITY_ATTRIBUTE} on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile3_climate_current_temperature entity_id: ${TILE3_ENTITY} attribute: ${TILE3_CLIMATE_CURRENT_TEMP_ATTRIBUTE} on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile3_climate_target_temperature entity_id: ${TILE3_ENTITY} attribute: ${TILE3_CLIMATE_TARGET_TEMP_ATTRIBUTE} on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile3_climate_humidity entity_id: ${TILE3_ENTITY} attribute: ${TILE3_CLIMATE_HUMIDITY_ATTRIBUTE} on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile4_climate_current_temperature entity_id: ${TILE4_ENTITY} attribute: ${TILE4_CLIMATE_CURRENT_TEMP_ATTRIBUTE} on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile4_climate_target_temperature entity_id: ${TILE4_ENTITY} attribute: ${TILE4_CLIMATE_TARGET_TEMP_ATTRIBUTE} on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile4_climate_humidity entity_id: ${TILE4_ENTITY} attribute: ${TILE4_CLIMATE_HUMIDITY_ATTRIBUTE} on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile5_climate_current_temperature entity_id: ${TILE5_ENTITY} attribute: ${TILE5_CLIMATE_CURRENT_TEMP_ATTRIBUTE} on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile5_climate_target_temperature entity_id: ${TILE5_ENTITY} attribute: ${TILE5_CLIMATE_TARGET_TEMP_ATTRIBUTE} on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile5_climate_humidity entity_id: ${TILE5_ENTITY} attribute: ${TILE5_CLIMATE_HUMIDITY_ATTRIBUTE} on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile6_climate_current_temperature entity_id: ${TILE6_ENTITY} attribute: ${TILE6_CLIMATE_CURRENT_TEMP_ATTRIBUTE} on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile6_climate_target_temperature entity_id: ${TILE6_ENTITY} attribute: ${TILE6_CLIMATE_TARGET_TEMP_ATTRIBUTE} on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile6_climate_humidity entity_id: ${TILE6_ENTITY} attribute: ${TILE6_CLIMATE_HUMIDITY_ATTRIBUTE} on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } binary_sensor: - platform: status name: "Node Status" id: system_status # ---------------------------------------------------------- # Text sensors # # smartdisplay_action: # - Main touch event channel exported to Home Assistant # - On short tap, publishes one of: # tile1_press / tile2_press / tile3_press / tile4_press / tile5_press / tile6_press # - The value is reset shortly after publishing # # tile*_preset: # - Mirrors the Home Assistant preset_mode attribute for each tile entity # - Used by the UI to display fan presets when applicable # ---------------------------------------------------------- text_sensor: - platform: template name: "Screen firmware" entity_category: diagnostic lambda: 'return {"${SCREEN_FIRMWARE_VERSION}"};' update_interval: 60s # ESP Screen Manager matches the screen to its YAML profile and OTA address. - platform: template name: "Device name" entity_category: diagnostic lambda: 'return {"${DEVICE_NAME}"};' update_interval: 60s - platform: wifi_info ip_address: name: "IP address" entity_category: diagnostic - platform: template id: smartdisplay_action name: "SmartDisplay Action" icon: mdi:gesture-tap-button - platform: homeassistant id: ha_state_tile1 entity_id: ${TILE1_STATE_ENTITY} on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: ha_state_tile2 entity_id: ${TILE2_STATE_ENTITY} on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: ha_state_tile3 entity_id: ${TILE3_STATE_ENTITY} on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: ha_state_tile4 entity_id: ${TILE4_STATE_ENTITY} on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: ha_state_tile5 entity_id: ${TILE5_STATE_ENTITY} on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: ha_state_tile6 entity_id: ${TILE6_STATE_ENTITY} on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: ha_state_tile7 entity_id: ${TILE7_STATE_ENTITY} on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: ha_state_tile8 entity_id: ${TILE8_STATE_ENTITY} on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: ha_state_tile9 entity_id: ${TILE9_STATE_ENTITY} on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: ha_state_tile10 entity_id: ${TILE10_STATE_ENTITY} on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } # Living room reports color_temp/rgb/xy, so the colour button does appear here. - platform: homeassistant id: tile9_supported_color_modes entity_id: ${TILE9_ENTITY} attribute: supported_color_modes on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } # Used by the slider overlay to decide whether the colour button is needed. # This lamp reports ['brightness'], so the button stays hidden automatically. - platform: homeassistant id: tile7_supported_color_modes entity_id: ${TILE7_ENTITY} attribute: supported_color_modes on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile1_preset entity_id: ${TILE1_ENTITY} attribute: preset_mode on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile2_preset entity_id: ${TILE2_ENTITY} attribute: preset_mode on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile3_preset entity_id: ${TILE3_ENTITY} attribute: preset_mode on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile4_preset entity_id: ${TILE4_ENTITY} attribute: preset_mode on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile5_preset entity_id: ${TILE5_ENTITY} attribute: preset_mode on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile6_preset entity_id: ${TILE6_ENTITY} attribute: preset_mode on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile1_supported_color_modes entity_id: ${TILE1_ENTITY} attribute: supported_color_modes on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile2_supported_color_modes entity_id: ${TILE2_ENTITY} attribute: supported_color_modes on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile3_supported_color_modes entity_id: ${TILE3_ENTITY} attribute: supported_color_modes on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile4_supported_color_modes entity_id: ${TILE4_ENTITY} attribute: supported_color_modes on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile5_supported_color_modes entity_id: ${TILE5_ENTITY} attribute: supported_color_modes on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile6_supported_color_modes entity_id: ${TILE6_ENTITY} attribute: supported_color_modes on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile1_climate_mode entity_id: ${TILE1_ENTITY} on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile2_climate_mode entity_id: ${TILE2_ENTITY} on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile3_climate_mode entity_id: ${TILE3_ENTITY} on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile4_climate_mode entity_id: ${TILE4_ENTITY} on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile5_climate_mode entity_id: ${TILE5_ENTITY} on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile6_climate_mode entity_id: ${TILE6_ENTITY} on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile1_climate_supported_modes entity_id: ${TILE1_ENTITY} attribute: hvac_modes - platform: homeassistant id: tile2_climate_supported_modes entity_id: ${TILE2_ENTITY} attribute: hvac_modes - platform: homeassistant id: tile3_climate_supported_modes entity_id: ${TILE3_ENTITY} attribute: hvac_modes - platform: homeassistant id: tile4_climate_supported_modes entity_id: ${TILE4_ENTITY} attribute: hvac_modes - platform: homeassistant id: tile5_climate_supported_modes entity_id: ${TILE5_ENTITY} attribute: hvac_modes - platform: homeassistant id: tile6_climate_supported_modes entity_id: ${TILE6_ENTITY} attribute: hvac_modes - platform: homeassistant id: tile1_cover_state entity_id: ${TILE1_STATE_ENTITY} on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile2_cover_state entity_id: ${TILE2_STATE_ENTITY} on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile3_cover_state entity_id: ${TILE3_STATE_ENTITY} on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile4_cover_state entity_id: ${TILE4_STATE_ENTITY} on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile5_cover_state entity_id: ${TILE5_STATE_ENTITY} on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: homeassistant id: tile6_cover_state entity_id: ${TILE6_STATE_ENTITY} on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } switch: - platform: restart name: "Restart" # Two SPI buses used by the CYD: # - lcd: built-in display # - my_touchscreen: built-in touch controller # Two SPI buses on the 2432S028: one for the display, a separate one for the # resistive XPT2046 touch (no I2C on this board, unlike the 032c). spi: - id: lcd clk_pin: GPIO14 mosi_pin: GPIO13 - id: my_touchscreen clk_pin: GPIO25 mosi_pin: GPIO32 miso_pin: GPIO39 output: - platform: ledc pin: GPIO21 id: gpio_backlight_pwm # ESPHome's ledc default is 1kHz. You don't see that while still (well # above what the eye perceives as flicker), but moving the screen reveals # motion blur turning the on/off pulses into streaks (the same effect as # a PWM LED that "judders" when you move it). 20kHz sits well above the # speed at which motion still makes that visible, and still leaves ample # duty-cycle resolution left over for smooth dimming. frequency: 20000Hz - platform: ledc id: output_red pin: GPIO4 inverted: true - platform: ledc id: output_green pin: GPIO16 inverted: true - platform: ledc id: output_blue pin: GPIO17 inverted: true light: - platform: monochromatic output: gpio_backlight_pwm name: "Power Display Backlight" id: back_light restore_mode: ALWAYS_ON - platform: rgb name: LED red: output_red id: led green: output_green blue: output_blue restore_mode: ALWAYS_OFF # Touchscreen configuration (XPT2046, resistive, via SPI) # XPT2046 calibration is board-specific. Poll at 50Hz; LVGL rotates touch # with the display. GPIO36 IRQ is deliberately unused (driver requests a # pull-up that this input-only pin cannot provide). touchscreen: platform: xpt2046 id: ts_touch spi_id: my_touchscreen cs_pin: 33 # Polling avoids GPIO36 pull-up errors and noisy PENIRQ edges. update_interval: 20ms # Diagnosis caught a false corner contact at pressure 422. Deliberate # contacts measured 936-2643; reject the low-pressure tail on release. threshold: 800 calibration: x_min: ${TOUCH_CAL_X_MIN} x_max: ${TOUCH_CAL_X_MAX} y_min: ${TOUCH_CAL_Y_MIN} y_max: ${TOUCH_CAL_Y_MAX} transform: swap_xy: ${TOUCH_SWAP_XY} mirror_x: ${TOUCH_MIRROR_X} mirror_y: ${TOUCH_MIRROR_Y} on_touch: - lambda: |- id(last_touch_ms) = millis(); id(touch_down) = true; cyd::touch_guard.begin(millis(), touch.x, touch.y, touch.id); screen_calibration::press(); ESP_LOGI("touch", "press native=%u,%u raw=%d,%d pressure=%d calibration=%d", touch.x, touch.y, id(ts_touch).filtered_raw_x(), id(ts_touch).filtered_raw_y(), touch.z_raw, id(calibration_active)); on_update: - lambda: |- id(last_touch_ms) = millis(); // Only the contact that started the touch counts; the guard ignores other ids. for (const auto &point : touches) { cyd::touch_guard.update(point.x, point.y, point.id); } on_release: - lambda: |- id(last_touch_ms) = millis(); id(touch_down) = false; screen_calibration::release(id(ts_touch).filtered_raw_x(), id(ts_touch).filtered_raw_y()); # Display configuration (ILI9341) display: - platform: mipi_spi id: my_display spi_id: lcd # Standard single-Micro-USB CYD; 40MHz matches ESPHome's board preset. model: ILI9341 data_rate: 40MHz cs_pin: 15 dc_pin: 2 invert_colors: false update_interval: never auto_clear_enabled: false # Native panel dimensions (portrait). Do NOT swap these per orientation and # do NOT add a display transform — rotation is handled by LVGL below. # ESPHome 2026.6+ rejects a swapped transform here with "Invalid offsets". dimensions: width: 240 height: 320 lvgl: on_draw_start: - lambda: 'id(rendered_frames)++;' displays: - my_display touchscreens: - touchscreen_id: ts_touch # The ESP32-WROOM-32 has no PSRAM; a full framebuffer (320x240x2 # = 153600 bytes) doesn't fit in DRAM. 25% is the usual value. # 12% instead of 25%: frees up ~20KB of extra contiguous memory, # and LVGL scales the strips of an ARGB layer along with the available # drawing space, so the requested allocations get smaller too. buffer_size: 12% # Direct local feedback on the WHOLE UI, not just the tiles. # A theme sets the style per widget type, so this covers all # buttons, tabs, sliders and the thermostat ring in one go. Non-clickable # widgets never enter the pressed state, so those stay untouched. theme: obj: pressed: # bg_opa and NOT opa: `opa` at widget level forces LVGL to render the # element to a separate layer, and that layer costs tens of KB's # per pressed widget. That ran the memory out here. bg_opa: 45% slider: knob: pressed: bg_opa: 65% arc: knob: pressed: bg_opa: 65% # LVGL owns the rotation (display + touch) since ESPHome 2026.4. rotation: ${LVGL_ROTATION} style_definitions: # Background: two-stop vertical gradient instead of the 320x240 # RGB565 wallpaper. Saves ~150 KB flash and an image blit per redraw, # which you can see directly in how smoothly it scrolls. - id: style_bg_grad bg_color: ${BG_TOP_COLOR} bg_grad_color: ${BG_BOTTOM_COLOR} bg_grad_dir: VER bg_opa: COVER border_width: 0 radius: 0 - id: style_tile bg_color: 0xFFFFFF bg_opa: COVER radius: 18 border_width: 1 border_color: 0xDDDDDD border_opa: COVER pad_left: 8 pad_right: 8 pad_top: 8 pad_bottom: 8 shadow_width: 0 shadow_opa: 40% - id: style_tile_disabled bg_color: 0xFFFFFF bg_opa: COVER radius: 18 border_width: 1 border_color: 0xDDDDDD border_opa: COVER pad_left: 8 pad_right: 8 pad_top: 8 pad_bottom: 8 shadow_width: 0 shadow_opa: 25% - id: style_icon_circle bg_color: 0xD5EEFC bg_opa: COVER radius: 999 border_width: 0 - id: style_icon_circle_disabled bg_color: 0xEEEEEE bg_opa: COVER radius: 999 border_width: 0 - id: style_room text_font: headline text_color: 0x1B1B1B text_opa: 100% - id: style_time text_font: time_label text_color: 0x1B1B1B text_opa: 100% - id: style_title text_font: label text_color: 0x000000 text_opa: 100% - id: style_value text_font: sublabel text_color: 0x7A7A7C text_opa: 100% - id: style_value_big text_font: sublabel_big text_color: 0x1B1B1B text_opa: 80% - id: style_title_disabled text_font: label text_color: 0x1B1B1B text_opa: 100% - id: style_value_disabled text_font: sublabel text_color: 0x1B1B1B text_opa: 100% # ---------- ALERT OVERLAY (HA action show_alert) ---------- # LVGL's top layer sits above every page, card and the dim overlay; the card # is filled by the alert_show script. Fixed positions, no layout engine. top_layer: widgets: - obj: id: alert_overlay x: 0 y: 0 width: ${DISPLAY_W} height: ${DISPLAY_H} hidden: true bg_color: 0x101820 bg_opa: 60% radius: 0 border_width: 0 pad_all: 0 clickable: true scrollable: false scrollbar_mode: 'OFF' # Same look when pressed: a tap next to the card must not repaint the screen. pressed: bg_opa: 60% widgets: - obj: id: alert_card align: CENTER width: ${ALERT_CARD_W} height: ${ALERT_CARD_H} bg_color: 0xFFFFFF bg_opa: COVER radius: 18 border_width: 1 border_color: 0xDDDDDD border_opa: COVER shadow_width: 0 pad_all: 0 clickable: false scrollable: false scrollbar_mode: 'OFF' widgets: - label: id: alert_icon x: ${ALERT_ICON_X} y: ${ALERT_ICON_Y} text: "\U000F002A" text_font: materialdesign_icons text_color: 0x1B1B1B clickable: false - label: id: alert_title x: ${ALERT_TEXT_X} y: ${ALERT_TITLE_Y} width: ${ALERT_TEXT_W} long_mode: DOT text: "Notification" text_font: headline text_color: 0x1B1B1B clickable: false - label: id: alert_subtitle x: ${ALERT_TEXT_X} y: ${ALERT_SUBTITLE_Y} width: ${ALERT_TEXT_W} height: ${ALERT_SUBTITLE_H} long_mode: WRAP text: "" text_font: sublabel_big text_color: 0x3A3A3C clickable: false - obj: id: alert_ok align: BOTTOM_RIGHT x: -${ALERT_BUTTON_INSET} y: -${ALERT_BUTTON_INSET} width: ${ALERT_BUTTON_W} height: ${ALERT_BUTTON_H} radius: 999 bg_color: 0x1B1B1B bg_opa: COVER border_width: 0 shadow_width: 0 pad_all: 0 clickable: true scrollable: false pressed: bg_color: 0x3A3A3C bg_opa: COVER on_click: - if: condition: lambda: 'return cyd::touch_guard.accept(millis(), 13);' then: - script.execute: id: alert_dismiss reason: "ok" widgets: - label: id: alert_ok_label align: CENTER text: "OK" text_font: headline text_color: 0xFFFFFF clickable: false pages: - id: home_page styles: style_bg_grad pad_all: 0 scrollable: false scrollbar_mode: 'OFF' widgets: # Never shown: it only makes ESPHome compile LVGL's spinner, which the busy sheet on a tile draws. - spinner: id: busy_spinner_seed hidden: true width: 2 height: 2 spin_time: 1s arc_length: 60deg - label: id: lbl_room x: 11 y: ${HEADER_Y} text: "${ROOM_NAME}" styles: style_room # Enables LVGL's line widget for the runtime clock hands and sparklines. - line: id: runtime_line_enable hidden: true points: - 0, 0 - 1, 1 - label: id: lbl_time align: TOP_RIGHT x: -11 y: ${HEADER_Y} text: "" styles: style_time # Six tiles on page 1, four on page 2. No dragging on resistive glass. - obj: id: tile_scroll x: 0 y: ${SCROLL_Y} width: ${DISPLAY_W} height: ${SCROLL_H} bg_opa: TRANSP border_width: 0 pad_left: 0 pad_right: 0 pad_top: 0 pad_bottom: 0 scrollable: false scrollbar_mode: 'OFF' widgets: # ---------- TILE 1 ---------- - obj: id: tile1 x: ${GRID_COL1_X} y: ${GRID_ROW1_Y} width: ${TILE_W} height: ${TILE_H} styles: style_tile scrollable: false scrollbar_mode: 'OFF' # Direct local feedback: LVGL applies this as soon as the finger # touches the glass, in the same frame. Independent of Home Assistant. pressed: bg_opa: 45% widgets: - obj: id: tile1_icon_circle x: ${TILE_ICON_X} y: ${TILE_ICON_Y} width: 36 height: 36 styles: style_icon_circle clickable: false scrollable: false scrollbar_mode: 'OFF' widgets: - label: id: tile1_icon_lbl align: CENTER text_font: materialdesign_icons text_color: 0xFFFFFF text: "${TILE1_ICON}" clickable: false - label: id: t1_title x: ${TILE_TITLE_X} y: ${TILE_TITLE_Y} text: "${TILE1_TITLE}" styles: style_title clickable: false - label: id: t1_value x: ${TILE_VALUE_X} y: ${TILE_VALUE_Y} text: "—" styles: style_value clickable: false on_short_click: - if: condition: lambda: 'return !runtime_tiles::enabled && cyd::touch_guard.accept(millis(), 1);' then: - script.execute: { id: publish_action, action: "tile1_press" } - script.execute: id: do_tile_action tile_type: "${TILE1_TYPE}" tap_action: "${TILE1_TAP_ACTION}" service: "${TILE1_TAP_SERVICE}" entity_id: "${TILE1_ENTITY}" param_key: "${TILE1_TAP_PARAM_KEY}" param_val: "${TILE1_TAP_PARAM_VAL}" was_on: !lambda |- if (std::string("${TILE1_TYPE}") == "cover") return id(tile1_cover_state).state != "closed"; return std::string(id(ha_state_tile1).state.c_str()) == "on"; on_long_press: - if: condition: lambda: 'return !runtime_tiles::enabled && cyd::touch_guard.accept(millis(), 1);' then: - script.execute: { id: publish_action, action: "tile1_long_press" } - if: condition: lambda: "return std::string(\"${TILE1_LONGPRESS}\") == \"action\";" then: - script.execute: id: do_tile_action tile_type: !lambda |- std::string t = "${TILE1_LONGPRESS_ACTION_TYPE}"; return t.empty() ? std::string("${TILE1_TYPE}") : t; tap_action: "activate" service: "${TILE1_LONGPRESS_ACTION_SERVICE}" entity_id: "${TILE1_LONGPRESS_ACTION}" param_key: "" param_val: "" was_on: false else: - if: condition: lambda: "return std::string(\"${TILE1_LONGPRESS}\") == \"slider\";" then: - script.execute: id: open_value_overlay title: "${TILE1_TITLE}" entity: "${TILE1_ENTITY}" tile_type: "${TILE1_TYPE}" longpress_mode: "${TILE1_LONGPRESS_SLIDER}" was_on: !lambda |- if (std::string("${TILE1_TYPE}") == "cover") return id(tile1_cover_state).state != "closed"; return std::string(id(ha_state_tile1).state.c_str()) == "on"; off_value: !lambda "return atoi(\"${TILE1_LONGPRESS_OFF_VALUE}\");" brightness_value: !lambda "return id(tile1_brightness).state;" percentage_value: !lambda "return id(tile1_percentage).state;" position_value: !lambda "return id(tile1_cover_position).state;" icon_text: "${TILE1_ICON}" slider_bg_color: ${TILE1_BG_DISABLED_COLOR} slider_fill_color: ${TILE1_CIRCLE_ACTIVE_COLOR} slider_icon_color: ${TILE1_ICON_ACTIVE_COLOR} label_off: "${TILE1_LABEL_OFF}" label_on: "${TILE1_LABEL_ON}" supported_color_modes: !lambda "return std::string(id(tile1_supported_color_modes).state.c_str());" climate_current_temperature: !lambda "return id(tile1_climate_current_temperature).state;" climate_target_temperature: !lambda "return id(tile1_climate_target_temperature).state;" climate_humidity: !lambda "return id(tile1_climate_humidity).state;" climate_mode: !lambda "return std::string(id(tile1_climate_mode).state.c_str());" climate_supported_modes: !lambda "return std::string(id(tile1_climate_supported_modes).state.c_str());" climate_min_temp: "${TILE1_CLIMATE_MIN_TEMP}" climate_max_temp: "${TILE1_CLIMATE_MAX_TEMP}" climate_step: "${TILE1_CLIMATE_STEP}" # Long-press mode "vacuum": opens the vacuum controls. # Falls outside the action/slider chain, so that stays unchanged. - if: condition: lambda: "return std::string(\"${TILE1_LONGPRESS}\") == \"vacuum\";" then: - script.execute: id: open_vacuum_overlay title: "${TILE1_TITLE}" entity: "${TILE1_ENTITY}" # ---------- TILE 2 ---------- - obj: id: tile2 x: ${GRID_COL2_X} y: ${GRID_ROW1_Y} width: ${TILE_W} height: ${TILE_H} styles: style_tile scrollable: false scrollbar_mode: 'OFF' # Direct local feedback: LVGL applies this as soon as the finger # touches the glass, in the same frame. Independent of Home Assistant. pressed: bg_opa: 45% widgets: - obj: id: tile2_icon_circle x: ${TILE_ICON_X} y: ${TILE_ICON_Y} width: 36 height: 36 styles: style_icon_circle clickable: false scrollable: false scrollbar_mode: 'OFF' widgets: - label: id: tile2_icon_lbl align: CENTER text_font: materialdesign_icons text_color: 0xFFFFFF text: "${TILE2_ICON}" clickable: false - label: id: t2_title x: ${TILE_TITLE_X} y: ${TILE_TITLE_Y} text: "${TILE2_TITLE}" styles: style_title clickable: false - label: id: t2_value x: ${TILE_VALUE_X} y: ${TILE_VALUE_Y} text: "—" styles: style_value clickable: false on_short_click: - if: condition: lambda: 'return !runtime_tiles::enabled && cyd::touch_guard.accept(millis(), 2);' then: - script.execute: { id: publish_action, action: "tile2_press" } - script.execute: id: do_tile_action tile_type: "${TILE2_TYPE}" tap_action: "${TILE2_TAP_ACTION}" service: "${TILE2_TAP_SERVICE}" entity_id: "${TILE2_ENTITY}" param_key: "${TILE2_TAP_PARAM_KEY}" param_val: "${TILE2_TAP_PARAM_VAL}" was_on: !lambda |- if (std::string("${TILE2_TYPE}") == "cover") return id(tile2_cover_state).state != "closed"; return std::string(id(ha_state_tile2).state.c_str()) == "on"; on_long_press: - if: condition: lambda: 'return !runtime_tiles::enabled && cyd::touch_guard.accept(millis(), 2);' then: - script.execute: { id: publish_action, action: "tile2_long_press" } - if: condition: lambda: "return std::string(\"${TILE2_LONGPRESS}\") == \"action\";" then: - script.execute: id: do_tile_action tile_type: !lambda |- std::string t = "${TILE2_LONGPRESS_ACTION_TYPE}"; return t.empty() ? std::string("${TILE2_TYPE}") : t; tap_action: "activate" service: "${TILE2_LONGPRESS_ACTION_SERVICE}" entity_id: "${TILE2_LONGPRESS_ACTION}" param_key: "" param_val: "" was_on: false else: - if: condition: lambda: "return std::string(\"${TILE2_LONGPRESS}\") == \"slider\";" then: - script.execute: id: open_value_overlay title: "${TILE2_TITLE}" entity: "${TILE2_ENTITY}" tile_type: "${TILE2_TYPE}" longpress_mode: "${TILE2_LONGPRESS_SLIDER}" was_on: !lambda |- if (std::string("${TILE2_TYPE}") == "cover") return id(tile2_cover_state).state != "closed"; return std::string(id(ha_state_tile2).state.c_str()) == "on"; off_value: !lambda "return atoi(\"${TILE2_LONGPRESS_OFF_VALUE}\");" brightness_value: !lambda "return id(tile2_brightness).state;" percentage_value: !lambda "return id(tile2_percentage).state;" position_value: !lambda "return id(tile2_cover_position).state;" icon_text: "${TILE2_ICON}" slider_bg_color: ${TILE2_BG_DISABLED_COLOR} slider_fill_color: ${TILE2_CIRCLE_ACTIVE_COLOR} slider_icon_color: ${TILE2_ICON_ACTIVE_COLOR} label_off: "${TILE2_LABEL_OFF}" label_on: "${TILE2_LABEL_ON}" supported_color_modes: !lambda "return std::string(id(tile2_supported_color_modes).state.c_str());" climate_current_temperature: !lambda "return id(tile2_climate_current_temperature).state;" climate_target_temperature: !lambda "return id(tile2_climate_target_temperature).state;" climate_humidity: !lambda "return id(tile2_climate_humidity).state;" climate_mode: !lambda "return std::string(id(tile2_climate_mode).state.c_str());" climate_supported_modes: !lambda "return std::string(id(tile2_climate_supported_modes).state.c_str());" climate_min_temp: "${TILE2_CLIMATE_MIN_TEMP}" climate_max_temp: "${TILE2_CLIMATE_MAX_TEMP}" climate_step: "${TILE2_CLIMATE_STEP}" # Long-press mode "vacuum": opens the vacuum controls. # Falls outside the action/slider chain, so that stays unchanged. - if: condition: lambda: "return std::string(\"${TILE2_LONGPRESS}\") == \"vacuum\";" then: - script.execute: id: open_vacuum_overlay title: "${TILE2_TITLE}" entity: "${TILE2_ENTITY}" # ---------- TILE 3 ---------- - obj: id: tile3 x: ${GRID_COL1_X} y: ${GRID_ROW2_Y} width: ${TILE_W} height: ${TILE_H} styles: style_tile scrollable: false scrollbar_mode: 'OFF' # Direct local feedback: LVGL applies this as soon as the finger # touches the glass, in the same frame. Independent of Home Assistant. pressed: bg_opa: 45% widgets: - obj: id: tile3_icon_circle x: ${TILE_ICON_X} y: ${TILE_ICON_Y} width: 36 height: 36 styles: style_icon_circle clickable: false scrollable: false scrollbar_mode: 'OFF' widgets: - label: id: tile3_icon_lbl align: CENTER text_font: materialdesign_icons text_color: 0xFFFFFF text: "${TILE3_ICON}" clickable: false - label: id: t3_title x: ${TILE_TITLE_X} y: ${TILE_TITLE_Y} text: "${TILE3_TITLE}" styles: style_title clickable: false - label: id: t3_value x: ${TILE_VALUE_X} y: ${TILE_VALUE_Y} text: "—" styles: style_value clickable: false on_short_click: - if: condition: lambda: 'return !runtime_tiles::enabled && cyd::touch_guard.accept(millis(), 3);' then: - script.execute: { id: publish_action, action: "tile3_press" } - script.execute: id: do_tile_action tile_type: "${TILE3_TYPE}" tap_action: "${TILE3_TAP_ACTION}" service: "${TILE3_TAP_SERVICE}" entity_id: "${TILE3_ENTITY}" param_key: "${TILE3_TAP_PARAM_KEY}" param_val: "${TILE3_TAP_PARAM_VAL}" was_on: !lambda |- if (std::string("${TILE3_TYPE}") == "cover") return id(tile3_cover_state).state != "closed"; return std::string(id(ha_state_tile3).state.c_str()) == "on"; on_long_press: - if: condition: lambda: 'return !runtime_tiles::enabled && cyd::touch_guard.accept(millis(), 3);' then: - script.execute: { id: publish_action, action: "tile3_long_press" } - if: condition: lambda: "return std::string(\"${TILE3_LONGPRESS}\") == \"action\";" then: - script.execute: id: do_tile_action tile_type: !lambda |- std::string t = "${TILE3_LONGPRESS_ACTION_TYPE}"; return t.empty() ? std::string("${TILE3_TYPE}") : t; tap_action: "activate" service: "${TILE3_LONGPRESS_ACTION_SERVICE}" entity_id: "${TILE3_LONGPRESS_ACTION}" param_key: "" param_val: "" was_on: false else: - if: condition: lambda: "return std::string(\"${TILE3_LONGPRESS}\") == \"slider\";" then: - script.execute: id: open_value_overlay title: "${TILE3_TITLE}" entity: "${TILE3_ENTITY}" tile_type: "${TILE3_TYPE}" longpress_mode: "${TILE3_LONGPRESS_SLIDER}" was_on: !lambda |- if (std::string("${TILE3_TYPE}") == "cover") return id(tile3_cover_state).state != "closed"; return std::string(id(ha_state_tile3).state.c_str()) == "on"; off_value: !lambda "return atoi(\"${TILE3_LONGPRESS_OFF_VALUE}\");" brightness_value: !lambda "return id(tile3_brightness).state;" percentage_value: !lambda "return id(tile3_percentage).state;" position_value: !lambda "return id(tile3_cover_position).state;" icon_text: "${TILE3_ICON}" slider_bg_color: ${TILE3_BG_DISABLED_COLOR} slider_fill_color: ${TILE3_CIRCLE_ACTIVE_COLOR} slider_icon_color: ${TILE3_ICON_ACTIVE_COLOR} label_off: "${TILE3_LABEL_OFF}" label_on: "${TILE3_LABEL_ON}" supported_color_modes: !lambda "return std::string(id(tile3_supported_color_modes).state.c_str());" climate_current_temperature: !lambda "return id(tile3_climate_current_temperature).state;" climate_target_temperature: !lambda "return id(tile3_climate_target_temperature).state;" climate_humidity: !lambda "return id(tile3_climate_humidity).state;" climate_mode: !lambda "return std::string(id(tile3_climate_mode).state.c_str());" climate_supported_modes: !lambda "return std::string(id(tile3_climate_supported_modes).state.c_str());" climate_min_temp: "${TILE3_CLIMATE_MIN_TEMP}" climate_max_temp: "${TILE3_CLIMATE_MAX_TEMP}" climate_step: "${TILE3_CLIMATE_STEP}" # Long-press mode "vacuum": opens the vacuum controls. # Falls outside the action/slider chain, so that stays unchanged. - if: condition: lambda: "return std::string(\"${TILE3_LONGPRESS}\") == \"vacuum\";" then: - script.execute: id: open_vacuum_overlay title: "${TILE3_TITLE}" entity: "${TILE3_ENTITY}" # ---------- TILE 4 ---------- - obj: id: tile4 x: ${GRID_COL2_X} y: ${GRID_ROW2_Y} width: ${TILE_W} height: ${TILE_H} styles: style_tile scrollable: false scrollbar_mode: 'OFF' # Direct local feedback: LVGL applies this as soon as the finger # touches the glass, in the same frame. Independent of Home Assistant. pressed: bg_opa: 45% widgets: - obj: id: tile4_icon_circle x: ${TILE_ICON_X} y: ${TILE_ICON_Y} width: 36 height: 36 styles: style_icon_circle clickable: false scrollable: false scrollbar_mode: 'OFF' widgets: - label: id: tile4_icon_lbl align: CENTER text_font: materialdesign_icons text_color: 0xFFFFFF text: "${TILE4_ICON}" clickable: false - label: id: t4_title x: ${TILE_TITLE_X} y: ${TILE_TITLE_Y} text: "${TILE4_TITLE}" styles: style_title clickable: false - label: id: t4_value x: ${TILE_VALUE_X} y: ${TILE_VALUE_Y} text: "—" styles: style_value clickable: false on_short_click: - if: condition: lambda: 'return !runtime_tiles::enabled && cyd::touch_guard.accept(millis(), 4);' then: - script.execute: { id: publish_action, action: "tile4_press" } - script.execute: id: do_tile_action tile_type: "${TILE4_TYPE}" tap_action: "${TILE4_TAP_ACTION}" service: "${TILE4_TAP_SERVICE}" entity_id: "${TILE4_ENTITY}" param_key: "${TILE4_TAP_PARAM_KEY}" param_val: "${TILE4_TAP_PARAM_VAL}" was_on: !lambda |- if (std::string("${TILE4_TYPE}") == "cover") return id(tile4_cover_state).state != "closed"; return std::string(id(ha_state_tile4).state.c_str()) == "on"; on_long_press: - if: condition: lambda: 'return !runtime_tiles::enabled && cyd::touch_guard.accept(millis(), 4);' then: - script.execute: { id: publish_action, action: "tile4_long_press" } - if: condition: lambda: "return std::string(\"${TILE4_LONGPRESS}\") == \"action\";" then: - script.execute: id: do_tile_action tile_type: !lambda |- std::string t = "${TILE4_LONGPRESS_ACTION_TYPE}"; return t.empty() ? std::string("${TILE4_TYPE}") : t; tap_action: "activate" service: "${TILE4_LONGPRESS_ACTION_SERVICE}" entity_id: "${TILE4_LONGPRESS_ACTION}" param_key: "" param_val: "" was_on: false else: - if: condition: lambda: "return std::string(\"${TILE4_LONGPRESS}\") == \"slider\";" then: - script.execute: id: open_value_overlay title: "${TILE4_TITLE}" entity: "${TILE4_ENTITY}" tile_type: "${TILE4_TYPE}" longpress_mode: "${TILE4_LONGPRESS_SLIDER}" was_on: !lambda |- if (std::string("${TILE4_TYPE}") == "cover") return id(tile4_cover_state).state != "closed"; return std::string(id(ha_state_tile4).state.c_str()) == "on"; off_value: !lambda "return atoi(\"${TILE4_LONGPRESS_OFF_VALUE}\");" brightness_value: !lambda "return id(tile4_brightness).state;" percentage_value: !lambda "return id(tile4_percentage).state;" position_value: !lambda "return id(tile4_cover_position).state;" icon_text: "${TILE4_ICON}" slider_bg_color: ${TILE4_BG_DISABLED_COLOR} slider_fill_color: ${TILE4_CIRCLE_ACTIVE_COLOR} slider_icon_color: ${TILE4_ICON_ACTIVE_COLOR} label_off: "${TILE4_LABEL_OFF}" label_on: "${TILE4_LABEL_ON}" supported_color_modes: !lambda "return std::string(id(tile4_supported_color_modes).state.c_str());" climate_current_temperature: !lambda "return id(tile4_climate_current_temperature).state;" climate_target_temperature: !lambda "return id(tile4_climate_target_temperature).state;" climate_humidity: !lambda "return id(tile4_climate_humidity).state;" climate_mode: !lambda "return std::string(id(tile4_climate_mode).state.c_str());" climate_supported_modes: !lambda "return std::string(id(tile4_climate_supported_modes).state.c_str());" climate_min_temp: "${TILE4_CLIMATE_MIN_TEMP}" climate_max_temp: "${TILE4_CLIMATE_MAX_TEMP}" climate_step: "${TILE4_CLIMATE_STEP}" # Long-press mode "vacuum": opens the vacuum controls. # Falls outside the action/slider chain, so that stays unchanged. - if: condition: lambda: "return std::string(\"${TILE4_LONGPRESS}\") == \"vacuum\";" then: - script.execute: id: open_vacuum_overlay title: "${TILE4_TITLE}" entity: "${TILE4_ENTITY}" # ---------- TILE 5 ---------- - obj: id: tile5 x: ${GRID_COL1_X} y: ${GRID_ROW3_Y} width: ${TILE_W} height: ${TILE_H} styles: style_tile scrollable: false scrollbar_mode: 'OFF' # Direct local feedback: LVGL applies this as soon as the finger # touches the glass, in the same frame. Independent of Home Assistant. pressed: bg_opa: 45% widgets: - obj: id: tile5_icon_circle x: ${TILE_ICON_X} y: ${TILE_ICON_Y} width: 36 height: 36 styles: style_icon_circle clickable: false scrollable: false scrollbar_mode: 'OFF' widgets: - label: id: tile5_icon_lbl align: CENTER text_font: materialdesign_icons text_color: 0xFFFFFF text: "${TILE5_ICON}" clickable: false - label: id: t5_title x: ${TILE_TITLE_X} y: ${TILE_TITLE_Y} text: "${TILE5_TITLE}" styles: style_title clickable: false - label: id: t5_value x: ${TILE_VALUE_X} y: ${TILE_VALUE_Y} text: "—" styles: style_value clickable: false on_short_click: - if: condition: lambda: 'return !runtime_tiles::enabled && cyd::touch_guard.accept(millis(), 5);' then: - script.execute: { id: publish_action, action: "tile5_press" } - script.execute: id: do_tile_action tile_type: "${TILE5_TYPE}" tap_action: "${TILE5_TAP_ACTION}" service: "${TILE5_TAP_SERVICE}" entity_id: "${TILE5_ENTITY}" param_key: "${TILE5_TAP_PARAM_KEY}" param_val: "${TILE5_TAP_PARAM_VAL}" was_on: !lambda |- if (std::string("${TILE5_TYPE}") == "cover") return id(tile5_cover_state).state != "closed"; return std::string(id(ha_state_tile5).state.c_str()) == "on"; on_long_press: - if: condition: lambda: 'return !runtime_tiles::enabled && cyd::touch_guard.accept(millis(), 5);' then: - script.execute: { id: publish_action, action: "tile5_long_press" } - if: condition: lambda: "return std::string(\"${TILE5_LONGPRESS}\") == \"action\";" then: - script.execute: id: do_tile_action tile_type: !lambda |- std::string t = "${TILE5_LONGPRESS_ACTION_TYPE}"; return t.empty() ? std::string("${TILE5_TYPE}") : t; tap_action: "activate" service: "${TILE5_LONGPRESS_ACTION_SERVICE}" entity_id: "${TILE5_LONGPRESS_ACTION}" param_key: "" param_val: "" was_on: false else: - if: condition: lambda: "return std::string(\"${TILE5_LONGPRESS}\") == \"slider\";" then: - script.execute: id: open_value_overlay title: "${TILE5_TITLE}" entity: "${TILE5_ENTITY}" tile_type: "${TILE5_TYPE}" longpress_mode: "${TILE5_LONGPRESS_SLIDER}" was_on: !lambda |- if (std::string("${TILE5_TYPE}") == "cover") return id(tile5_cover_state).state != "closed"; return std::string(id(ha_state_tile5).state.c_str()) == "on"; off_value: !lambda "return atoi(\"${TILE5_LONGPRESS_OFF_VALUE}\");" brightness_value: !lambda "return id(tile5_brightness).state;" percentage_value: !lambda "return id(tile5_percentage).state;" position_value: !lambda "return id(tile5_cover_position).state;" icon_text: "${TILE5_ICON}" slider_bg_color: ${TILE5_BG_DISABLED_COLOR} slider_fill_color: ${TILE5_CIRCLE_ACTIVE_COLOR} slider_icon_color: ${TILE5_ICON_ACTIVE_COLOR} label_off: "${TILE5_LABEL_OFF}" label_on: "${TILE5_LABEL_ON}" supported_color_modes: !lambda "return std::string(id(tile5_supported_color_modes).state.c_str());" climate_current_temperature: !lambda "return id(tile5_climate_current_temperature).state;" climate_target_temperature: !lambda "return id(tile5_climate_target_temperature).state;" climate_humidity: !lambda "return id(tile5_climate_humidity).state;" climate_mode: !lambda "return std::string(id(tile5_climate_mode).state.c_str());" climate_supported_modes: !lambda "return std::string(id(tile5_climate_supported_modes).state.c_str());" climate_min_temp: "${TILE5_CLIMATE_MIN_TEMP}" climate_max_temp: "${TILE5_CLIMATE_MAX_TEMP}" climate_step: "${TILE5_CLIMATE_STEP}" # Long-press mode "vacuum": opens the vacuum controls. # Falls outside the action/slider chain, so that stays unchanged. - if: condition: lambda: "return std::string(\"${TILE5_LONGPRESS}\") == \"vacuum\";" then: - script.execute: id: open_vacuum_overlay title: "${TILE5_TITLE}" entity: "${TILE5_ENTITY}" # ---------- TILE 6 ---------- - obj: id: tile6 x: ${GRID_COL2_X} y: ${GRID_ROW3_Y} width: ${TILE_W} height: ${TILE_H} styles: style_tile scrollable: false scrollbar_mode: 'OFF' # Direct local feedback: LVGL applies this as soon as the finger # touches the glass, in the same frame. Independent of Home Assistant. pressed: bg_opa: 45% widgets: - obj: id: tile6_icon_circle x: ${TILE_ICON_X} y: ${TILE_ICON_Y} width: 36 height: 36 styles: style_icon_circle clickable: false scrollable: false scrollbar_mode: 'OFF' widgets: - label: id: tile6_icon_lbl align: CENTER text_font: materialdesign_icons text_color: 0xFFFFFF text: "${TILE6_ICON}" clickable: false - label: id: t6_title x: ${TILE_TITLE_X} y: ${TILE_TITLE_Y} text: "${TILE6_TITLE}" styles: style_title clickable: false - label: id: t6_value x: ${TILE_VALUE_X} y: ${TILE_VALUE_Y} text: "—" styles: style_value clickable: false on_short_click: - if: condition: lambda: 'return !runtime_tiles::enabled && cyd::touch_guard.accept(millis(), 6);' then: - script.execute: { id: publish_action, action: "tile6_press" } - script.execute: id: do_tile_action tile_type: "${TILE6_TYPE}" tap_action: "${TILE6_TAP_ACTION}" service: "${TILE6_TAP_SERVICE}" entity_id: "${TILE6_ENTITY}" param_key: "${TILE6_TAP_PARAM_KEY}" param_val: "${TILE6_TAP_PARAM_VAL}" was_on: !lambda |- if (std::string("${TILE6_TYPE}") == "cover") return id(tile6_cover_state).state != "closed"; return std::string(id(ha_state_tile6).state.c_str()) == "on"; on_long_press: - if: condition: lambda: 'return !runtime_tiles::enabled && cyd::touch_guard.accept(millis(), 6);' then: - script.execute: { id: publish_action, action: "tile6_long_press" } - if: condition: lambda: "return std::string(\"${TILE6_LONGPRESS}\") == \"action\";" then: - script.execute: id: do_tile_action tile_type: !lambda |- std::string t = "${TILE6_LONGPRESS_ACTION_TYPE}"; return t.empty() ? std::string("${TILE6_TYPE}") : t; tap_action: "activate" service: "${TILE6_LONGPRESS_ACTION_SERVICE}" entity_id: "${TILE6_LONGPRESS_ACTION}" param_key: "" param_val: "" was_on: false else: - if: condition: lambda: "return std::string(\"${TILE6_LONGPRESS}\") == \"slider\";" then: - script.execute: id: open_value_overlay title: "${TILE6_TITLE}" entity: "${TILE6_ENTITY}" tile_type: "${TILE6_TYPE}" longpress_mode: "${TILE6_LONGPRESS_SLIDER}" was_on: !lambda |- if (std::string("${TILE6_TYPE}") == "cover") return id(tile6_cover_state).state != "closed"; return std::string(id(ha_state_tile6).state.c_str()) == "on"; off_value: !lambda "return atoi(\"${TILE6_LONGPRESS_OFF_VALUE}\");" brightness_value: !lambda "return id(tile6_brightness).state;" percentage_value: !lambda "return id(tile6_percentage).state;" position_value: !lambda "return id(tile6_cover_position).state;" icon_text: "${TILE6_ICON}" slider_bg_color: ${TILE6_BG_DISABLED_COLOR} slider_fill_color: ${TILE6_CIRCLE_ACTIVE_COLOR} slider_icon_color: ${TILE6_ICON_ACTIVE_COLOR} label_off: "${TILE6_LABEL_OFF}" label_on: "${TILE6_LABEL_ON}" supported_color_modes: !lambda "return std::string(id(tile6_supported_color_modes).state.c_str());" climate_current_temperature: !lambda "return id(tile6_climate_current_temperature).state;" climate_target_temperature: !lambda "return id(tile6_climate_target_temperature).state;" climate_humidity: !lambda "return id(tile6_climate_humidity).state;" climate_mode: !lambda "return std::string(id(tile6_climate_mode).state.c_str());" climate_supported_modes: !lambda "return std::string(id(tile6_climate_supported_modes).state.c_str());" climate_min_temp: "${TILE6_CLIMATE_MIN_TEMP}" climate_max_temp: "${TILE6_CLIMATE_MAX_TEMP}" climate_step: "${TILE6_CLIMATE_STEP}" # Long-press mode "vacuum": opens the vacuum controls. # Falls outside the action/slider chain, so that stays unchanged. - if: condition: lambda: "return std::string(\"${TILE6_LONGPRESS}\") == \"vacuum\";" then: - script.execute: id: open_vacuum_overlay title: "${TILE6_TITLE}" entity: "${TILE6_ENTITY}" # ---------- TILE 7 — Table lamp ---------- # Brightness-only lamp: deliberately no percentage/cover/climate # sensors were created for this tile, those would only cost # HA subscriptions and heap. The overlay therefore gets # NAN / "" for the fields a light never uses. - obj: id: tile7 hidden: true x: ${GRID_COL1_X} y: ${GRID_ROW4_Y} width: ${TILE_W} height: ${TILE_H} styles: style_tile scrollable: false scrollbar_mode: 'OFF' pressed: bg_opa: 45% widgets: - obj: id: tile7_icon_circle x: ${TILE_ICON_X} y: ${TILE_ICON_Y} width: 36 height: 36 styles: style_icon_circle clickable: false scrollable: false scrollbar_mode: 'OFF' widgets: - label: id: tile7_icon_lbl align: CENTER text_font: materialdesign_icons text_color: 0xFFFFFF text: "${TILE7_ICON}" clickable: false - label: id: t7_title x: ${TILE_TITLE_X} y: ${TILE_TITLE_Y} text: "${TILE7_TITLE}" styles: style_title clickable: false - label: id: t7_value x: ${TILE_VALUE_X} y: ${TILE_VALUE_Y} text: "—" styles: style_value clickable: false on_short_click: - if: condition: lambda: 'return !runtime_tiles::enabled && cyd::touch_guard.accept(millis(), 7);' then: - script.execute: { id: publish_action, action: "tile7_press" } - script.execute: id: do_tile_action tile_type: "${TILE7_TYPE}" tap_action: "${TILE7_TAP_ACTION}" service: "${TILE7_TAP_SERVICE}" entity_id: "${TILE7_ENTITY}" param_key: "${TILE7_TAP_PARAM_KEY}" param_val: "${TILE7_TAP_PARAM_VAL}" was_on: !lambda "return std::string(id(ha_state_tile7).state.c_str()) == \"on\";" on_long_press: - if: condition: lambda: 'return !runtime_tiles::enabled && cyd::touch_guard.accept(millis(), 7);' then: - script.execute: { id: publish_action, action: "tile7_long_press" } - if: condition: lambda: "return std::string(\"${TILE7_LONGPRESS}\") == \"action\";" then: - script.execute: id: do_tile_action tile_type: !lambda |- std::string t = "${TILE7_LONGPRESS_ACTION_TYPE}"; return t.empty() ? std::string("${TILE7_TYPE}") : t; tap_action: "activate" service: "${TILE7_LONGPRESS_ACTION_SERVICE}" entity_id: "${TILE7_LONGPRESS_ACTION}" param_key: "" param_val: "" was_on: false else: - if: condition: lambda: "return std::string(\"${TILE7_LONGPRESS}\") == \"slider\";" then: - script.execute: id: open_value_overlay title: "${TILE7_TITLE}" entity: "${TILE7_ENTITY}" tile_type: "${TILE7_TYPE}" longpress_mode: "${TILE7_LONGPRESS_SLIDER}" was_on: !lambda "return std::string(id(ha_state_tile7).state.c_str()) == \"on\";" off_value: !lambda "return atoi(\"${TILE7_LONGPRESS_OFF_VALUE}\");" brightness_value: !lambda "return id(tile7_brightness).state;" percentage_value: !lambda "return NAN;" position_value: !lambda "return NAN;" icon_text: "${TILE7_ICON}" slider_bg_color: ${TILE7_BG_DISABLED_COLOR} slider_fill_color: ${TILE7_CIRCLE_ACTIVE_COLOR} slider_icon_color: ${TILE7_ICON_ACTIVE_COLOR} label_off: "${TILE7_LABEL_OFF}" label_on: "${TILE7_LABEL_ON}" supported_color_modes: !lambda "return std::string(id(tile7_supported_color_modes).state.c_str());" climate_current_temperature: !lambda "return NAN;" climate_target_temperature: !lambda "return NAN;" climate_humidity: !lambda "return NAN;" climate_mode: "" climate_supported_modes: "" climate_min_temp: "5.0" climate_max_temp: "30.0" climate_step: "0.5" # ---------- TILE 8 — Mailbox reset ---------- # input_select with options Full/Empty. The tile lights up as soon as # the status is TILE8_ACTIVE_STATE ("Full"); a short tap sends # input_select.select_option with option: "Empty". # No slider mode, so this tile only needs ha_state_tile8 — # no other HA subscription at all. - obj: id: tile8 hidden: true x: ${GRID_COL2_X} y: ${GRID_ROW4_Y} width: ${TILE_W} height: ${TILE_H} styles: style_tile scrollable: false scrollbar_mode: 'OFF' pressed: bg_opa: 45% widgets: - obj: id: tile8_icon_circle x: ${TILE_ICON_X} y: ${TILE_ICON_Y} width: 36 height: 36 styles: style_icon_circle clickable: false scrollable: false scrollbar_mode: 'OFF' widgets: - label: id: tile8_icon_lbl align: CENTER text_font: materialdesign_icons text_color: 0xFFFFFF text: "${TILE8_ICON}" clickable: false - label: id: t8_title x: ${TILE_TITLE_X} y: ${TILE_TITLE_Y} text: "${TILE8_TITLE}" styles: style_title clickable: false - label: id: t8_value x: ${TILE_VALUE_X} y: ${TILE_VALUE_Y} text: "—" styles: style_value clickable: false on_short_click: - if: condition: lambda: 'return !runtime_tiles::enabled && cyd::touch_guard.accept(millis(), 8);' then: - script.execute: { id: publish_action, action: "tile8_press" } - script.execute: id: do_tile_action tile_type: "${TILE8_TYPE}" tap_action: "${TILE8_TAP_ACTION}" service: "${TILE8_TAP_SERVICE}" entity_id: "${TILE8_ENTITY}" param_key: "${TILE8_TAP_PARAM_KEY}" param_val: "${TILE8_TAP_PARAM_VAL}" was_on: !lambda "return std::string(id(ha_state_tile8).state.c_str()) == \"${TILE8_ACTIVE_STATE}\";" on_long_press: - if: condition: lambda: 'return !runtime_tiles::enabled && cyd::touch_guard.accept(millis(), 8);' then: - script.execute: { id: publish_action, action: "tile8_long_press" } # Slider mode isn't possible here without first creating the # corresponding sensors; "action" works out of the box. - if: condition: lambda: "return std::string(\"${TILE8_LONGPRESS}\") == \"action\";" then: - script.execute: id: do_tile_action tile_type: !lambda |- std::string t = "${TILE8_LONGPRESS_ACTION_TYPE}"; return t.empty() ? std::string("${TILE8_TYPE}") : t; tap_action: "activate" service: "${TILE8_LONGPRESS_ACTION_SERVICE}" entity_id: "${TILE8_LONGPRESS_ACTION}" param_key: "" param_val: "" was_on: false # ---------- TILE 9 — Living room (moved from tile 2) ---------- - obj: id: tile9 hidden: true x: ${GRID_COL1_X} y: ${GRID_ROW5_Y} width: ${TILE_W} height: ${TILE_H} styles: style_tile scrollable: false scrollbar_mode: 'OFF' pressed: bg_opa: 45% widgets: - obj: id: tile9_icon_circle x: ${TILE_ICON_X} y: ${TILE_ICON_Y} width: 36 height: 36 styles: style_icon_circle clickable: false scrollable: false scrollbar_mode: 'OFF' widgets: - label: id: tile9_icon_lbl align: CENTER text_font: materialdesign_icons text_color: 0xFFFFFF text: "${TILE9_ICON}" clickable: false - label: id: t9_title x: ${TILE_TITLE_X} y: ${TILE_TITLE_Y} text: "${TILE9_TITLE}" styles: style_title clickable: false - label: id: t9_value x: ${TILE_VALUE_X} y: ${TILE_VALUE_Y} text: "—" styles: style_value clickable: false on_short_click: - if: condition: lambda: 'return !runtime_tiles::enabled && cyd::touch_guard.accept(millis(), 9);' then: - script.execute: { id: publish_action, action: "tile9_press" } - script.execute: id: do_tile_action tile_type: "${TILE9_TYPE}" tap_action: "${TILE9_TAP_ACTION}" service: "${TILE9_TAP_SERVICE}" entity_id: "${TILE9_ENTITY}" param_key: "${TILE9_TAP_PARAM_KEY}" param_val: "${TILE9_TAP_PARAM_VAL}" was_on: !lambda "return std::string(id(ha_state_tile9).state.c_str()) == \"on\";" on_long_press: - if: condition: lambda: 'return !runtime_tiles::enabled && cyd::touch_guard.accept(millis(), 9);' then: - script.execute: { id: publish_action, action: "tile9_long_press" } - if: condition: lambda: "return std::string(\"${TILE9_LONGPRESS}\") == \"action\";" then: - script.execute: id: do_tile_action tile_type: !lambda |- std::string t = "${TILE9_LONGPRESS_ACTION_TYPE}"; return t.empty() ? std::string("${TILE9_TYPE}") : t; tap_action: "activate" service: "${TILE9_LONGPRESS_ACTION_SERVICE}" entity_id: "${TILE9_LONGPRESS_ACTION}" param_key: "" param_val: "" was_on: false else: - if: condition: lambda: "return std::string(\"${TILE9_LONGPRESS}\") == \"slider\";" then: - script.execute: id: open_value_overlay title: "${TILE9_TITLE}" entity: "${TILE9_ENTITY}" tile_type: "${TILE9_TYPE}" longpress_mode: "${TILE9_LONGPRESS_SLIDER}" was_on: !lambda "return std::string(id(ha_state_tile9).state.c_str()) == \"on\";" off_value: !lambda "return atoi(\"${TILE9_LONGPRESS_OFF_VALUE}\");" brightness_value: !lambda "return id(tile9_brightness).state;" percentage_value: !lambda "return NAN;" position_value: !lambda "return NAN;" icon_text: "${TILE9_ICON}" slider_bg_color: ${TILE9_BG_DISABLED_COLOR} slider_fill_color: ${TILE9_CIRCLE_ACTIVE_COLOR} slider_icon_color: ${TILE9_ICON_ACTIVE_COLOR} label_off: "${TILE9_LABEL_OFF}" label_on: "${TILE9_LABEL_ON}" supported_color_modes: !lambda "return std::string(id(tile9_supported_color_modes).state.c_str());" climate_current_temperature: !lambda "return NAN;" climate_target_temperature: !lambda "return NAN;" climate_humidity: !lambda "return NAN;" climate_mode: "" climate_supported_modes: "" climate_min_temp: "5.0" climate_max_temp: "30.0" climate_step: "0.5" # ---------- TILE 10 — Alarm on/off ---------- - obj: id: tile10 hidden: true x: ${GRID_COL2_X} y: ${GRID_ROW5_Y} width: ${TILE_W} height: ${TILE_H} styles: style_tile scrollable: false scrollbar_mode: 'OFF' pressed: bg_opa: 45% widgets: - obj: id: tile10_icon_circle x: ${TILE_ICON_X} y: ${TILE_ICON_Y} width: 36 height: 36 styles: style_icon_circle clickable: false scrollable: false scrollbar_mode: 'OFF' widgets: - label: id: tile10_icon_lbl align: CENTER text_font: materialdesign_icons text_color: 0xFFFFFF text: "${TILE10_ICON}" clickable: false - label: id: t10_title x: ${TILE_TITLE_X} y: ${TILE_TITLE_Y} text: "${TILE10_TITLE}" styles: style_title clickable: false - label: id: t10_value x: ${TILE_VALUE_X} y: ${TILE_VALUE_Y} text: "—" styles: style_value clickable: false on_short_click: - if: condition: lambda: 'return !runtime_tiles::enabled && cyd::touch_guard.accept(millis(), 10);' then: - script.execute: { id: publish_action, action: "tile10_press" } - script.execute: id: do_tile_action tile_type: "${TILE10_TYPE}" tap_action: "${TILE10_TAP_ACTION}" service: "${TILE10_TAP_SERVICE}" entity_id: "${TILE10_ENTITY}" param_key: "${TILE10_TAP_PARAM_KEY}" param_val: "${TILE10_TAP_PARAM_VAL}" was_on: !lambda "return std::string(id(ha_state_tile10).state.c_str()) == \"on\";" on_long_press: - if: condition: lambda: 'return !runtime_tiles::enabled && cyd::touch_guard.accept(millis(), 10);' then: - script.execute: { id: publish_action, action: "tile10_long_press" } - if: condition: lambda: "return std::string(\"${TILE10_LONGPRESS}\") == \"action\";" then: - script.execute: id: do_tile_action tile_type: !lambda |- std::string t = "${TILE10_LONGPRESS_ACTION_TYPE}"; return t.empty() ? std::string("${TILE10_TYPE}") : t; tap_action: "activate" service: "${TILE10_LONGPRESS_ACTION_SERVICE}" entity_id: "${TILE10_LONGPRESS_ACTION}" param_key: "" param_val: "" was_on: false # ---------- VALUE SLIDER OVERLAY ---------- # ---------- PAGE NAVIGATION: one full-width bar ---------- - obj: id: page_prev align: BOTTOM_LEFT x: 0 y: 0 width: 130 height: 32 pad_all: 0 radius: 0 bg_opa: TRANSP border_width: 0 scrollable: false pressed: bg_color: 0xE3E3E3 bg_opa: COVER on_short_click: - if: condition: lambda: 'return cyd::touch_guard.accept(millis(), 11);' then: - lambda: 'id(tile_page) = std::max(0, id(tile_page) - 1);' - script.execute: show_tile_page widgets: - label: align: LEFT_MID x: 12 text: "< Previous" text_font: label text_color: 0x1B1B1B clickable: false - label: id: page_number align: BOTTOM_MID y: 0 width: 60 height: 32 pad_top: 9 border_width: 0 text: "1 / 2" text_align: CENTER text_font: sublabel text_color: 0x1B1B1B - obj: id: page_next align: BOTTOM_RIGHT x: 0 y: 0 width: 130 height: 32 pad_all: 0 radius: 0 bg_opa: TRANSP border_width: 0 scrollable: false pressed: bg_color: 0xE3E3E3 bg_opa: COVER on_short_click: - if: condition: lambda: 'return cyd::touch_guard.accept(millis(), 12);' then: - lambda: 'id(tile_page) += 1;' - script.execute: show_tile_page widgets: - label: align: RIGHT_MID x: -12 text: "Next >" text_font: label text_color: 0x1B1B1B clickable: false - obj: id: brightness_overlay x: 0 y: 0 width: ${DISPLAY_W} height: ${DISPLAY_H} bg_opa: TRANSP hidden: true scrollable: false scrollbar_mode: 'OFF' pad_left: 0 pad_right: 0 pad_top: 0 pad_bottom: 0 border_width: 0 widgets: - obj: id: brightness_overlay_bg x: 0 y: 0 width: ${DISPLAY_W} height: ${DISPLAY_H} styles: style_bg_grad scrollable: false clickable: true on_click: - script.execute: close_cards - obj: id: brightness_overlay_dim x: 0 y: 0 width: ${DISPLAY_W} height: ${DISPLAY_H} bg_color: 0xFFFFFF bg_opa: 18% border_width: 0 clickable: false scrollable: false - label: id: overlay_title align: TOP_MID y: 10 text: "Light" styles: style_room - label: id: overlay_value align: TOP_MID y: 35 text: "100 %" styles: style_value_big - slider: id: overlay_slider align: CENTER y: 18 width: ${OVERLAY_SLIDER_W} height: ${OVERLAY_SLIDER_H} min_value: 0 max_value: 100 value: 100 adv_hittest: false bg_color: 0xFFFFFF bg_opa: 100% radius: 28 # The only widget with BOTH a radius AND a child (overlay_icon). # LVGL then clips children to the rounding, and that's only # possible via a contiguous 96x160 = 46KB ARGB layer. # If that allocation fails, LVGL keeps trying until the # task watchdog restarts the board. The rounding stays, the # clipping goes. clip_corner: false border_width: 0 indicator: bg_color: 0xD5EEFC bg_opa: 100% radius: 28 knob: bg_opa: 0% border_width: 0 shadow_width: 0 pad_all: 10 on_value: - script.execute: id: slider_preview value: !lambda "return x;" on_release: - script.execute: id: slider_commit value: !lambda "return lv_slider_get_value(id(overlay_slider));" # The icon used to be a CHILD in the slider. A widget with a radius # and children has to clip those children to the rounding, and # LVGL can only do that via an ARGB layer of tens of KB's. Without # PSRAM that sometimes fails, after which LVGL keeps retrying until # the watchdog restarts the board. As a sibling on top, no layer # is needed and it looks identical. - label: id: overlay_icon align: CENTER y: ${OVERLAY_ICON_Y} text_font: materialdesign_icons text_color: 0x1B1B1B text: "${TILE1_ICON}" clickable: false - obj: id: overlay_color_button align: BOTTOM_RIGHT x: -13 y: -13 width: 44 height: 44 radius: 999 hidden: true clickable: true bg_opa: TRANSP border_width: 0 pad_left: 0 pad_right: 0 pad_top: 0 pad_bottom: 0 scrollable: false scrollbar_mode: 'OFF' on_click: - script.execute: open_color_detail widgets: - arc: align: CENTER width: 40 height: 40 start_angle: 0 end_angle: 45 min_value: 0 max_value: 100 value: 0 adjustable: false arc_width: 10 arc_rounded: false arc_color: 0xFF3B30 arc_opa: COVER bg_opa: TRANSP border_width: 0 clickable: false indicator: arc_opa: TRANSP knob: bg_opa: TRANSP border_width: 0 - arc: align: CENTER width: 40 height: 40 start_angle: 45 end_angle: 90 min_value: 0 max_value: 100 value: 0 adjustable: false arc_width: 10 arc_rounded: false arc_color: 0xFF9500 arc_opa: COVER bg_opa: TRANSP border_width: 0 clickable: false indicator: arc_opa: TRANSP knob: bg_opa: TRANSP border_width: 0 - arc: align: CENTER width: 40 height: 40 start_angle: 90 end_angle: 135 min_value: 0 max_value: 100 value: 0 adjustable: false arc_width: 10 arc_rounded: false arc_color: 0xFFD60A arc_opa: COVER bg_opa: TRANSP border_width: 0 clickable: false indicator: arc_opa: TRANSP knob: bg_opa: TRANSP border_width: 0 - arc: align: CENTER width: 40 height: 40 start_angle: 135 end_angle: 180 min_value: 0 max_value: 100 value: 0 adjustable: false arc_width: 10 arc_rounded: false arc_color: 0x30D158 arc_opa: COVER bg_opa: TRANSP border_width: 0 clickable: false indicator: arc_opa: TRANSP knob: bg_opa: TRANSP border_width: 0 - arc: align: CENTER width: 40 height: 40 start_angle: 180 end_angle: 225 min_value: 0 max_value: 100 value: 0 adjustable: false arc_width: 10 arc_rounded: false arc_color: 0x32D6FF arc_opa: COVER bg_opa: TRANSP border_width: 0 clickable: false indicator: arc_opa: TRANSP knob: bg_opa: TRANSP border_width: 0 - arc: align: CENTER width: 40 height: 40 start_angle: 225 end_angle: 270 min_value: 0 max_value: 100 value: 0 adjustable: false arc_width: 10 arc_rounded: false arc_color: 0x0A84FF arc_opa: COVER bg_opa: TRANSP border_width: 0 clickable: false indicator: arc_opa: TRANSP knob: bg_opa: TRANSP border_width: 0 - arc: align: CENTER width: 40 height: 40 start_angle: 270 end_angle: 315 min_value: 0 max_value: 100 value: 0 adjustable: false arc_width: 10 arc_rounded: false arc_color: 0x5856D6 arc_opa: COVER bg_opa: TRANSP border_width: 0 clickable: false indicator: arc_opa: TRANSP knob: bg_opa: TRANSP border_width: 0 - arc: align: CENTER width: 40 height: 40 start_angle: 315 end_angle: 360 min_value: 0 max_value: 100 value: 0 adjustable: false arc_width: 10 arc_rounded: false arc_color: 0xBF5AF2 arc_opa: COVER bg_opa: TRANSP border_width: 0 clickable: false indicator: arc_opa: TRANSP knob: bg_opa: TRANSP border_width: 0 # ---------- LIGHT COLOR DETAIL OVERLAY ---------- - obj: id: color_detail_overlay x: 0 y: 0 width: ${DISPLAY_W} height: ${DISPLAY_H} bg_color: 0xF7F7F7 bg_opa: COVER hidden: true scrollable: false scrollbar_mode: 'OFF' pad_left: 0 pad_right: 0 pad_top: 0 pad_bottom: 0 border_width: 0 widgets: - obj: id: color_detail_bg x: 0 y: 0 width: ${DISPLAY_W} height: ${DISPLAY_H} styles: style_bg_grad scrollable: false clickable: false - obj: id: color_detail_scrim x: 0 y: 0 width: ${DISPLAY_W} height: ${DISPLAY_H} bg_color: 0xFFFFFF bg_opa: 72% border_width: 0 clickable: false scrollable: false - obj: id: color_close_button align: TOP_LEFT x: 10 y: 9 width: 36 height: 36 radius: 999 bg_color: 0xEEEEEE bg_opa: COVER border_width: 1 border_color: 0xFFFFFF border_opa: 20% clickable: true scrollable: false on_click: - script.execute: close_cards widgets: - label: align: CENTER text: "x" text_font: sublabel_big text_color: 0x1B1B1B clickable: false - label: id: color_detail_title width: 210 long_mode: DOT text_align: CENTER align: TOP_MID y: 16 text: "Light Color" styles: style_room - obj: id: color_done_button align: TOP_RIGHT x: -10 y: 9 width: 36 height: 36 radius: 999 bg_color: 0xEEEEEE bg_opa: COVER border_width: 0 clickable: true scrollable: false on_click: - script.execute: close_cards widgets: - label: align: CENTER text: "\U000F012C" text_font: materialdesign_icons text_color: 0x1B1B1B clickable: false # ---------- CLIMATE DETAIL OVERLAY ---------- - obj: id: climate_detail_overlay x: 0 y: 0 width: ${DISPLAY_W} height: ${DISPLAY_H} bg_color: 0xF7F7F7 bg_opa: COVER hidden: true scrollable: false scrollbar_mode: 'OFF' pad_left: 0 pad_right: 0 pad_top: 0 pad_bottom: 0 border_width: 0 widgets: - obj: id: climate_detail_bg x: 0 y: 0 width: ${DISPLAY_W} height: ${DISPLAY_H} styles: style_bg_grad scrollable: false clickable: false - obj: id: climate_detail_scrim x: 0 y: 0 width: ${DISPLAY_W} height: ${DISPLAY_H} bg_color: 0xFFFFFF bg_opa: 62% border_width: 0 clickable: false scrollable: false - obj: id: climate_close_button align: TOP_LEFT x: 10 y: 9 # 44 instead of 36: slightly bigger tap target, grows away from the arc # (top-left anchor stays put) instead of towards it. width: 44 height: 44 radius: 999 bg_color: 0xEEEEEE bg_opa: COVER border_width: 1 border_color: 0xFFFFFF border_opa: 20% clickable: true scrollable: false on_click: - script.execute: close_cards widgets: - label: align: CENTER text: "x" text_font: sublabel_big text_color: 0x1B1B1B clickable: false - obj: id: climate_power_button align: TOP_RIGHT x: -10 y: 9 width: 44 height: 44 radius: 999 bg_color: 0xEEEEEE bg_opa: COVER border_width: 0 clickable: true scrollable: false on_click: - script.execute: climate_power_toggle widgets: - label: id: climate_power_icon align: CENTER text: "\U000F0425" text_font: materialdesign_icons_mini text_color: 0x1B1B1B clickable: false - label: id: climate_detail_title align: TOP_MID y: 16 text: "Climate" styles: style_room - label: id: climate_current_label align: TOP_MID y: 42 text: "Now: --" text_font: sublabel_big text_color: 0x1B1B1B text_opa: 85% clickable: false - arc: id: climate_arc align: CENTER y: 20 width: 184 height: 184 # Without this the arc claims an invisible extra # click area outside the 184x184 box — near the close # button top-left this creates overlap. ext_click_area: 0 start_angle: 135 end_angle: 45 min_value: 50 max_value: 300 value: 200 adjustable: true arc_width: 20 arc_rounded: true arc_color: 0x4B4B4F arc_opa: 80% bg_opa: TRANSP border_width: 0 indicator: arc_color: 0xFEC600 arc_opa: COVER arc_width: 20 arc_rounded: true knob: bg_color: 0x111111 bg_opa: COVER border_width: 0 shadow_width: 0 on_value: - script.execute: id: climate_target_preview value: !lambda "return x;" on_release: - script.execute: id: climate_target_commit value: !lambda "return lv_arc_get_value(id(climate_arc));" - label: id: climate_set_label align: CENTER y: -16 text: "Target" text_font: sublabel_big text_color: 0x1B1B1B text_opa: 68% clickable: false - label: id: climate_target_label align: CENTER y: 20 text: "20.0°" text_font: headline text_color: 0x1B1B1B clickable: false - obj: id: climate_minus_button align: BOTTOM_LEFT x: 38 y: -16 width: 48 height: 36 radius: 18 bg_color: 0xEEEEEE bg_opa: COVER border_width: 1 border_color: 0xFFFFFF border_opa: 16% clickable: true scrollable: false on_click: - script.execute: id: climate_adjust_target direction: -1 widgets: - label: align: CENTER text: "-" text_font: headline text_color: 0x1B1B1B clickable: false - obj: id: climate_plus_button align: BOTTOM_RIGHT x: -38 y: -16 width: 48 height: 36 radius: 18 bg_color: 0xEEEEEE bg_opa: COVER border_width: 1 border_color: 0xFFFFFF border_opa: 16% clickable: true scrollable: false on_click: - script.execute: id: climate_adjust_target direction: 1 widgets: - label: align: CENTER text: "+" text_font: headline text_color: 0x1B1B1B clickable: false - obj: id: climate_mode_pill align: BOTTOM_MID y: -18 width: 126 height: 34 radius: 17 bg_color: 0xF0F0F0 bg_opa: 82% border_width: 0 clickable: true scrollable: false on_click: - script.execute: open_climate_mode_picker widgets: - label: id: climate_mode_label align: CENTER text: "Auto" text_font: sublabel_big text_color: 0x1B1B1B clickable: false # ---------- CLIMATE MODE PICKER ---------- # Replaces cycling through modes. Cycling sends a set_hvac_mode on every # tap, so to get from 'cool' to 'dry' a heat pump actually passes through # 'heat' and 'fan_only' along the way. Here you pick directly, and exactly # one command is sent. - obj: id: climate_mode_overlay x: 0 y: 0 width: ${DISPLAY_W} height: ${DISPLAY_H} hidden: true bg_color: 0xE7E7E7 bg_opa: COVER border_width: 0 clickable: true scrollable: false scrollbar_mode: 'OFF' widgets: - label: align: TOP_MID y: 16 text: "Mode" text_font: sublabel_big text_color: 0x1B1B1B clickable: false # Flex ensures unsupported modes don't leave a gap: # LVGL skips hidden children when arranging. - obj: id: climate_mode_list align: TOP_MID y: 44 width: 98% height: 178 bg_opa: TRANSP border_width: 0 scrollable: false clickable: false pad_all: 3 layout: type: FLEX pad_row: 3 pad_column: 4 flex_flow: ROW_WRAP flex_align_main: CENTER flex_align_cross: CENTER flex_align_track: CENTER widgets: - obj: id: cmode_off width: 88 height: 26 radius: 13 bg_color: 0xFFFFFF bg_opa: COVER border_width: 1 border_color: 0xDDDDDD clickable: true scrollable: false pad_all: 0 on_click: - script.execute: { id: set_climate_mode, mode: "off" } widgets: - label: align: CENTER text: "Off" text_font: sublabel_big text_color: 0x1B1B1B clickable: false - obj: id: cmode_heat width: 88 height: 26 radius: 13 bg_color: 0xFFFFFF bg_opa: COVER border_width: 1 border_color: 0xDDDDDD clickable: true scrollable: false pad_all: 0 on_click: - script.execute: { id: set_climate_mode, mode: "heat" } widgets: - label: align: CENTER text: "Heat" text_font: sublabel_big text_color: 0x1B1B1B clickable: false - obj: id: cmode_cool width: 88 height: 26 radius: 13 bg_color: 0xFFFFFF bg_opa: COVER border_width: 1 border_color: 0xDDDDDD clickable: true scrollable: false pad_all: 0 on_click: - script.execute: { id: set_climate_mode, mode: "cool" } widgets: - label: align: CENTER text: "Cool" text_font: sublabel_big text_color: 0x1B1B1B clickable: false - obj: id: cmode_heat_cool width: 88 height: 26 radius: 13 bg_color: 0xFFFFFF bg_opa: COVER border_width: 1 border_color: 0xDDDDDD clickable: true scrollable: false pad_all: 0 on_click: - script.execute: { id: set_climate_mode, mode: "heat_cool" } widgets: - label: align: CENTER text: "Heat/Cool" text_font: sublabel_big text_color: 0x1B1B1B clickable: false - obj: id: cmode_auto width: 88 height: 26 radius: 13 bg_color: 0xFFFFFF bg_opa: COVER border_width: 1 border_color: 0xDDDDDD clickable: true scrollable: false pad_all: 0 on_click: - script.execute: { id: set_climate_mode, mode: "auto" } widgets: - label: align: CENTER text: "Auto" text_font: sublabel_big text_color: 0x1B1B1B clickable: false - obj: id: cmode_dry width: 88 height: 26 radius: 13 bg_color: 0xFFFFFF bg_opa: COVER border_width: 1 border_color: 0xDDDDDD clickable: true scrollable: false pad_all: 0 on_click: - script.execute: { id: set_climate_mode, mode: "dry" } widgets: - label: align: CENTER text: "Dry" text_font: sublabel_big text_color: 0x1B1B1B clickable: false - obj: id: cmode_fan_only width: 88 height: 26 radius: 13 bg_color: 0xFFFFFF bg_opa: COVER border_width: 1 border_color: 0xDDDDDD clickable: true scrollable: false pad_all: 0 on_click: - script.execute: { id: set_climate_mode, mode: "fan_only" } widgets: - label: align: CENTER text: "Fan only" text_font: sublabel_big text_color: 0x1B1B1B clickable: false - label: id: cfan_title width: 100% height: 11 text: "Fan" text_font: sublabel text_color: 0x6B6B6B text_align: CENTER clickable: false - obj: id: cfan_0 width: 62 height: 22 radius: 11 bg_color: 0xFFFFFF bg_opa: COVER border_width: 1 border_color: 0xDDDDDD clickable: true scrollable: false pad_all: 0 on_click: - script.execute: { id: set_climate_fan_mode, index: 0 } widgets: - label: id: cfan_0_label align: CENTER text: "-" text_font: sublabel text_color: 0x1B1B1B clickable: false - obj: id: cfan_1 width: 62 height: 22 radius: 11 bg_color: 0xFFFFFF bg_opa: COVER border_width: 1 border_color: 0xDDDDDD clickable: true scrollable: false pad_all: 0 on_click: - script.execute: { id: set_climate_fan_mode, index: 1 } widgets: - label: id: cfan_1_label align: CENTER text: "-" text_font: sublabel text_color: 0x1B1B1B clickable: false - obj: id: cfan_2 width: 62 height: 22 radius: 11 bg_color: 0xFFFFFF bg_opa: COVER border_width: 1 border_color: 0xDDDDDD clickable: true scrollable: false pad_all: 0 on_click: - script.execute: { id: set_climate_fan_mode, index: 2 } widgets: - label: id: cfan_2_label align: CENTER text: "-" text_font: sublabel text_color: 0x1B1B1B clickable: false - obj: id: cfan_3 width: 62 height: 22 radius: 11 bg_color: 0xFFFFFF bg_opa: COVER border_width: 1 border_color: 0xDDDDDD clickable: true scrollable: false pad_all: 0 on_click: - script.execute: { id: set_climate_fan_mode, index: 3 } widgets: - label: id: cfan_3_label align: CENTER text: "-" text_font: sublabel text_color: 0x1B1B1B clickable: false - label: id: cswing_title width: 100% height: 11 text: "Swing" text_font: sublabel text_color: 0x6B6B6B text_align: CENTER clickable: false - obj: id: cswing_0 width: 62 height: 22 radius: 11 bg_color: 0xFFFFFF bg_opa: COVER border_width: 1 border_color: 0xDDDDDD clickable: true scrollable: false pad_all: 0 on_click: - script.execute: { id: set_climate_swing_mode, index: 0 } widgets: - label: id: cswing_0_label align: CENTER text: "-" text_font: sublabel text_color: 0x1B1B1B clickable: false - obj: id: cswing_1 width: 62 height: 22 radius: 11 bg_color: 0xFFFFFF bg_opa: COVER border_width: 1 border_color: 0xDDDDDD clickable: true scrollable: false pad_all: 0 on_click: - script.execute: { id: set_climate_swing_mode, index: 1 } widgets: - label: id: cswing_1_label align: CENTER text: "-" text_font: sublabel text_color: 0x1B1B1B clickable: false - obj: id: cswing_2 width: 62 height: 22 radius: 11 bg_color: 0xFFFFFF bg_opa: COVER border_width: 1 border_color: 0xDDDDDD clickable: true scrollable: false pad_all: 0 on_click: - script.execute: { id: set_climate_swing_mode, index: 2 } widgets: - label: id: cswing_2_label align: CENTER text: "-" text_font: sublabel text_color: 0x1B1B1B clickable: false - obj: id: cswing_3 width: 62 height: 22 radius: 11 bg_color: 0xFFFFFF bg_opa: COVER border_width: 1 border_color: 0xDDDDDD clickable: true scrollable: false pad_all: 0 on_click: - script.execute: { id: set_climate_swing_mode, index: 3 } widgets: - label: id: cswing_3_label align: CENTER text: "-" text_font: sublabel text_color: 0x1B1B1B clickable: false - obj: id: climate_mode_overlay_x align: TOP_LEFT x: 10 y: 9 width: 36 height: 36 radius: 999 bg_color: 0xFFFFFF bg_opa: COVER border_width: 1 border_color: 0xDDDDDD border_opa: COVER clickable: true scrollable: false on_click: - lvgl.widget.hide: climate_mode_overlay widgets: - label: align: CENTER text: "x" text_font: sublabel_big text_color: 0x1B1B1B clickable: false # ---------- VACUUM CONTROL ---------- # A vacuum is not a switch: start, pause, stop and return to # the dock are separate commands, and suction power is a separate axis. - obj: id: vacuum_overlay x: 0 y: 0 width: ${DISPLAY_W} height: ${DISPLAY_H} hidden: true bg_color: 0xFFFFFF bg_opa: 90% border_width: 0 clickable: true scrollable: false scrollbar_mode: 'OFF' widgets: - label: id: vacuum_title align: TOP_MID y: 16 text: "Vacuum" text_font: sublabel_big text_color: 0x1B1B1B clickable: false - obj: id: vacuum_actions align: TOP_MID y: 52 width: 98% height: 80 bg_opa: TRANSP border_width: 0 scrollable: false pad_all: 3 layout: type: FLEX flex_flow: ROW_WRAP flex_align_main: CENTER flex_align_cross: CENTER flex_align_track: CENTER widgets: - obj: id: vac_start width: 72 height: 38 radius: 19 bg_color: 0xF0F0F0 bg_opa: 92% border_width: 0 clickable: true scrollable: false pad_all: 0 on_click: - script.execute: { id: vacuum_action, service: "vacuum.start" } widgets: - label: align: CENTER text: "Start" text_font: sublabel_big text_color: 0x1B1B1B clickable: false - obj: id: vac_pause width: 72 height: 38 radius: 19 bg_color: 0xF0F0F0 bg_opa: 92% border_width: 0 clickable: true scrollable: false pad_all: 0 on_click: - script.execute: { id: vacuum_action, service: "vacuum.pause" } widgets: - label: align: CENTER text: "Pause" text_font: sublabel_big text_color: 0x1B1B1B clickable: false - obj: id: vac_return_to_base width: 72 height: 38 radius: 19 bg_color: 0xF0F0F0 bg_opa: 92% border_width: 0 clickable: true scrollable: false pad_all: 0 on_click: - script.execute: { id: vacuum_action, service: "vacuum.return_to_base" } widgets: - label: align: CENTER text: "Dock" text_font: sublabel_big text_color: 0x1B1B1B clickable: false - obj: id: vac_stop width: 72 height: 38 radius: 19 bg_color: 0xF0F0F0 bg_opa: 92% border_width: 0 clickable: true scrollable: false pad_all: 0 on_click: - script.execute: { id: vacuum_action, service: "vacuum.stop" } widgets: - label: align: CENTER text: "Stop" text_font: sublabel_big text_color: 0x1B1B1B clickable: false - label: align: TOP_MID y: 138 text: "Suction" text_font: sublabel text_color: 0x1B1B1B clickable: false - obj: id: vacuum_speeds align: TOP_MID y: 156 width: 98% height: 74 bg_opa: TRANSP border_width: 0 scrollable: false pad_all: 3 layout: type: FLEX flex_flow: ROW_WRAP flex_align_main: CENTER flex_align_cross: CENTER flex_align_track: CENTER widgets: - obj: id: vacspd_quiet width: 72 height: 32 radius: 19 bg_color: 0xF0F0F0 bg_opa: 92% border_width: 0 clickable: true scrollable: false pad_all: 0 on_click: - script.execute: id: vacuum_fan_speed speed: !lambda 'return runtime_tiles::enabled ? runtime_tiles::vacuum_option(0) : "quiet";' widgets: - label: align: CENTER text: "Quiet" text_font: sublabel_big text_color: 0x1B1B1B clickable: false - obj: id: vacspd_balanced width: 72 height: 32 radius: 19 bg_color: 0xF0F0F0 bg_opa: 92% border_width: 0 clickable: true scrollable: false pad_all: 0 on_click: - script.execute: id: vacuum_fan_speed speed: !lambda 'return runtime_tiles::enabled ? runtime_tiles::vacuum_option(1) : "balanced";' widgets: - label: align: CENTER text: "Balanced" text_font: sublabel_big text_color: 0x1B1B1B clickable: false - obj: id: vacspd_turbo width: 72 height: 32 radius: 19 bg_color: 0xF0F0F0 bg_opa: 92% border_width: 0 clickable: true scrollable: false pad_all: 0 on_click: - script.execute: id: vacuum_fan_speed speed: !lambda 'return runtime_tiles::enabled ? runtime_tiles::vacuum_option(2) : "turbo";' widgets: - label: align: CENTER text: "Turbo" text_font: sublabel_big text_color: 0x1B1B1B clickable: false - obj: id: vacspd_max width: 72 height: 32 radius: 19 bg_color: 0xF0F0F0 bg_opa: 92% border_width: 0 clickable: true scrollable: false pad_all: 0 on_click: - script.execute: id: vacuum_fan_speed speed: !lambda 'return runtime_tiles::enabled ? runtime_tiles::vacuum_option(3) : "max";' widgets: - label: align: CENTER text: "Max" text_font: sublabel_big text_color: 0x1B1B1B clickable: false - obj: id: vacuum_overlay_x align: TOP_LEFT x: 10 y: 9 width: 36 height: 36 radius: 999 bg_color: 0xEEEEEE bg_opa: COVER border_width: 1 border_color: 0xFFFFFF border_opa: 20% clickable: true scrollable: false on_click: - script.execute: close_cards widgets: - label: align: CENTER text: "x" text_font: sublabel_big text_color: 0x1B1B1B clickable: false - obj: id: vacuum_overlay_ok align: TOP_RIGHT x: -10 y: 9 width: 36 height: 36 radius: 999 bg_color: 0xD5EEFC bg_opa: COVER border_width: 0 clickable: true scrollable: false on_click: - script.execute: close_cards widgets: - label: align: CENTER text: "\U000F012C" text_font: materialdesign_icons text_color: 0x1B1B1B clickable: false # ---------- DIM / WAKE OVERLAY ---------- - obj: id: dim_wake_overlay x: 0 y: 0 width: ${DISPLAY_W} height: ${DISPLAY_H} hidden: true bg_opa: TRANSP border_width: 0 scrollable: false scrollbar_mode: 'OFF' clickable: true on_click: - script.execute: wake_display - id: touch_cal_page bg_color: 0x101820 bg_opa: COVER pad_all: 0 border_width: 0 scrollable: false widgets: - label: align: TOP_MID y: 65 text: "Follow the calibration wizard" text_font: sublabel text_color: 0xFFFFFF - obj: x: 10 y: 10 width: 20 height: 20 pad_all: 0 border_width: 0 bg_opa: TRANSP scrollable: false widgets: - label: align: CENTER text: "+" text_font: headline text_color: 0xFFFFFF - obj: x: 289 y: 10 width: 20 height: 20 pad_all: 0 border_width: 0 bg_opa: TRANSP scrollable: false widgets: - label: align: CENTER text: "+" text_font: headline text_color: 0xFFFFFF - obj: x: 289 y: 209 width: 20 height: 20 pad_all: 0 border_width: 0 bg_opa: TRANSP scrollable: false widgets: - label: align: CENTER text: "+" text_font: headline text_color: 0xFFFFFF - obj: x: 10 y: 209 width: 20 height: 20 pad_all: 0 border_width: 0 bg_opa: TRANSP scrollable: false widgets: - label: align: CENTER text: "+" text_font: headline text_color: 0xFFFFFF - obj: x: 150 y: 110 width: 20 height: 20 pad_all: 0 border_width: 0 bg_opa: TRANSP scrollable: false widgets: - label: align: CENTER text: "+" text_font: headline text_color: 0xFFFFFF script: # Render every overlay and both pages repeatedly without controlling devices. - id: ui_self_test mode: single then: - logger.log: "UI_TEST START: page/overlay render stress, no HA actions" - lambda: 'ESP_LOGI("ui_test", "Light sliders: %s", light_controls::self_test() ? "PASS" : "FAIL");' - lambda: 'id(calibration_active) = false;' - lvgl.page.show: home_page - script.execute: wake_display - repeat: count: 10 then: - lambda: |- id(last_touch_ms) = millis(); static unsigned test_page=0; id(tile_page) = runtime_tiles::enabled ? test_page++ % runtime_tiles::page_count() : 1; - script.execute: show_tile_page - delay: 300ms - lambda: |- lv_obj_update_layout(id(tile_scroll)); const int count = runtime_tiles::enabled ? runtime_tiles::model.count : std::clamp(atoi("${TILE_COUNT}"), 1, 10); // Wide cards take two slots, so visibility follows the runtime packing. const bool paginated = runtime_tiles::enabled ? runtime_tiles::page_count() > 1 : count > 6; lv_obj_t* tiles[] = {id(tile1), id(tile2), id(tile3), id(tile4), id(tile5), id(tile6), id(tile7), id(tile8), id(tile9), id(tile10)}; bool ok = true; for (int i = 0; i < 10; ++i) { bool visible = runtime_tiles::enabled ? i<6 && runtime_tiles::widgets[i].index= 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(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();