# GENERATED by tools/generate_packages.py; edit the source board profile. ############################################################ # SmartDisplay – iOS Home app–inspired Tiles UI # Target device: ESP32-2432S028 / Cheap Yellow Display (CYD) # Resolution: configurable via ORIENTATION substitutions (landscape 320×240 default) # # Target hardware # - Cheap Yellow Display (ESP32-2432S028 or compatible) # - Built-in ILI9341 320x240 display # - Built-in XPT2046 touchscreen # # What this config does # - Renders a gradient background + two fixed pages of “tiles” (6 + 4) (LVGL). # - Each tile shows: icon (MDI font), title, and a value line. # - Tile taps publish a short-lived text_sensor event (smartdisplay_action) # so you can build Home Assistant automations on top. # - Optional: the device can also call Home Assistant services directly # (toggle lights/switches/fans/covers, activate scenes/scripts, set fan preset, # set light brightness, set fan percentage, set cover position). # # The layout is inspired by common modern smart home UIs. # # How to customize # 1) Create device.yaml with tools/new_device.py; edit its TILE1..TILE10 overrides. # 2) If you change icons, update the FONT GLYPHS block in the substitutions below. # Each glyph must appear exactly once — same icon on multiple tiles is fine, just list it once. # 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). # home-like.yaml targets the separate ST7789V/GT911 board. # 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.2" DYNAMIC_TILES: "true" DASHBOARD_INBOX_INTERNAL: "false" DEVICE_NAME: "cyd-new" DEVICE_FRIENDLY_NAME: "Mijn CYD" CALIBRATION_ON_BOOT: "false" TOUCH_AFFINE_XX: "1.000000000" TOUCH_AFFINE_XY: "0.000000000" TOUCH_AFFINE_XC: "0.000000000" TOUCH_AFFINE_YX: "0.000000000" TOUCH_AFFINE_YY: "1.000000000" TOUCH_AFFINE_YC: "0.000000000" TILE_COUNT: "1" # 1-10; <=6 hides page navigation DIRECT_ACTIONS: "true" # true = call Home Assistant services directly, false = only emit action events ROOM_NAME: "Kies tegels in HA" # room name shown in the top-left header TIME_24H: "true" # true = 24h format, false = 12h format ## DIM settings AUTO_DIM: "true" # true = enable auto dimming AUTO_DIM_TIMEOUT: "600" # seconds after last touch MAX_BRIGHTNESS: "100" # backlight brightness in percent DIM_BRIGHTNESS: "20" # dimmed backlight brightness in percent NIGHT_MODE: "true" # true = use separate dim level at night NIGHT_START_HOUR: "22" # 0-23 NIGHT_END_HOUR: "7" # 0-23 NIGHT_DIM_BRIGHTNESS: "10" # backlight brightness in percent at night # ============================================================ # ACCENTKLEUREN — automatisch bepaald uit het Home Assistant-domein # # Het domein is het stuk voor de punt in het entity_id: # script.goede_morgen -> "script" -> ACCENT_ACTION # light.woonkamer_2 -> "light" -> ACCENT_LIGHT # vacuum.s8 -> "vacuum" -> ACCENT_DEVICE # # Dat is Home Assistant's eigen indeling, dus een tegel krijgt vanzelf de # juiste kleur zodra je er een entity in zet — je hoeft per tegel geen # kleuren meer te kiezen. Het onderscheid dat telt: een script/scene vuur # je af, een apparaat zet je aan of uit. Die twee horen niet hetzelfde # te zijn. # # Zet TILE*_CIRCLE_ACTIVE_COLOR / TILE*_ICON_DISABLED_COLOR op # ${ACCENT_AUTO} om dit te gebruiken, of op een echte hexwaarde om die # ene tegel handmatig te kleuren (zie tegel 8 — rood zodra er post ligt). # ============================================================ ACCENT_AUTO: "0x1000000" # sentinel: bewust buiten het 24-bits kleurbereik 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 # List every unique MDI icon used across your tiles — each value must appear exactly once. # Reusing the same icon on multiple tiles is fine: just list that glyph once here. # If you use fewer than 6 unique icons, leave the extra slots with their default values # (unused glyphs add a few bytes to the font but cause no errors). # ============================================================ MDI_GLYPH_1: "\U000F001B" MDI_GLYPH_2: "\U000F0387" # mdi:music MDI_GLYPH_3: "\U000F02E3" MDI_GLYPH_4: "\U000F059C" MDI_GLYPH_5: "\U000F0425" MDI_GLYPH_6: "\U000F070D" MDI_GLYPH_7: "\U000F06B5" # mdi:lamp MDI_GLYPH_8: "\U000F06EE" # mdi:mailbox MDI_GLYPH_9: "\U000F04B9" # mdi:sofa MDI_GLYPH_10: "\U000F002A" # mdi:alarm-light # ============================================================ # TILE 1 # ============================================================ # --- basic settings --- TILE1_ENTITY: "light.voorbeeld_lamp" # main Home Assistant entity for this tile TILE1_STATE_ENTITY: "light.voorbeeld_lamp" # entity used for active/inactive state styling TILE1_TITLE: "Lamp" # tile title shown in the UI TILE1_ICON: "\U000F001B" # MDI glyph — also add to MDI_GLYPH_* list above if not already there 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: "Uit" # text shown when entity is off TILE1_LABEL_ON: "Aan" # 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.voorbeeld_stekker" # main Home Assistant entity for this tile TILE2_STATE_ENTITY: "switch.voorbeeld_stekker" # entity used for active/inactive state styling TILE2_TITLE: "Stekker" # tile title shown in the UI TILE2_ICON: "\U000F0387" # MDI glyph — also add to MDI_GLYPH_* list above if not already there 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: "Uit" # text shown when entity is off TILE2_LABEL_ON: "Aan" # 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.voorbeeld_avond" # main Home Assistant entity for this tile TILE3_STATE_ENTITY: "scene.voorbeeld_avond" # entity used for active/inactive state styling TILE3_TITLE: "Avond" # tile title shown in the UI TILE3_ICON: "\U000F02E3" # MDI glyph — also add to MDI_GLYPH_* list above if not already there 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: "Uit" # text shown when entity is off TILE3_LABEL_ON: "Aan" # 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.voorbeeld_uit" # main Home Assistant entity for this tile TILE4_STATE_ENTITY: "script.voorbeeld_uit" # entity used for active/inactive state styling TILE4_TITLE: "Alles uit" # tile title shown in the UI TILE4_ICON: "\U000F059C" # MDI glyph — also add to MDI_GLYPH_* list above if not already there 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: "Uit" # text shown when entity is off TILE4_LABEL_ON: "Aan" # 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.voorbeeld_airco" # main Home Assistant entity for this tile TILE5_STATE_ENTITY: "climate.voorbeeld_airco" # entity used for active/inactive state styling TILE5_TITLE: "Airco" # tile title shown in the UI TILE5_ICON: "\U000F0425" # MDI glyph — also add to MDI_GLYPH_* list above if not already there 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: "Uit" # text shown when entity is off (closed) TILE5_LABEL_ON: "Aan" # 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.voorbeeld_robot" # main Home Assistant entity for this tile TILE6_STATE_ENTITY: "vacuum.voorbeeld_robot" # entity used for active/inactive state styling TILE6_TITLE: "Stofzuiger" # tile title shown in the UI TILE6_ICON: "\U000F070D" # MDI glyph — also add to MDI_GLYPH_* list above if not already there 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: "Uit" # text shown when entity is off TILE6_LABEL_ON: "Aan" # 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 — Tafellamp # Alleen-helderheid lamp (HA meldt supported_color_modes: ['brightness']), # dus de kleurenknop in de slider-overlay blijft vanzelf verborgen. # ============================================================ # --- basic settings --- TILE7_ENTITY: "light.ongebruikt_7" # main Home Assistant entity for this tile TILE7_STATE_ENTITY: "light.ongebruikt_7" # entity used for active/inactive state styling TILE7_TITLE: "Tegel 7" # tile title shown in the UI TILE7_ICON: "\U000F06B5" # mdi:lamp — also listed in MDI_GLYPH_7 above 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: "Uit" # text shown when entity is off TILE7_LABEL_ON: "Aan" # 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 voor een 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 — Brievenbus reset # input_select.brievenbus_status heeft opties ['Vol', 'Leeg']. # De tegel licht op zodra de status "Vol" is (ACTIVE_STATE), en een korte # tik zet hem terug op "Leeg" via input_select.select_option. # ============================================================ # --- basic settings --- TILE8_ENTITY: "light.ongebruikt_8" # main Home Assistant entity for this tile TILE8_STATE_ENTITY: "light.ongebruikt_8" # entity used for active/inactive state styling TILE8_TITLE: "Tegel 8" # tile title shown in the UI TILE8_ICON: "\U000F06EE" # mdi:mailbox — also listed in MDI_GLYPH_8 above TILE8_TYPE: "light" # generiek type; het echte gedrag komt uit TAP_SERVICE hieronder TILE8_TAP_ACTION: "auto" # activate + TAP_SERVICE = losse service call TILE8_LONGPRESS: "none" # long press stuurt alleen het event naar HA TILE8_VALUE_MODE: "auto" # toont LABEL_ON / LABEL_OFF TILE8_LABEL_OFF: "Uit" # text shown when entity is off / inactive TILE8_LABEL_ON: "Aan" # text shown when entity is active TILE8_ACTIVE_STATE: "" # tegel is "aan" zodra de status exact "Vol" is # --- colors --- TILE8_CIRCLE_ACTIVE_COLOR: "0xE5484D" # rood zodra er post ligt 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: "" # zet de input_select op een vaste optie TILE8_TAP_PARAM_KEY: "" # parameternaam voor input_select.select_option TILE8_TAP_PARAM_VAL: "" # de "reset"-waarde # if LONGPRESS = slider: TILE8_LONGPRESS_SLIDER: "auto" # niet in gebruik (LONGPRESS = none) TILE8_LONGPRESS_OFF_VALUE: "0" # niet in gebruik # 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 — Woonkamer # Verhuisd van tegel 2 naar rij 5 om plaats te maken voor Ambient. # Kleurenlamp (color_temp / rgb / xy), dus de kleurknop in de # slider-overlay verschijnt vanzelf. # ============================================================ # --- basic settings --- TILE9_ENTITY: "light.ongebruikt_9" # main Home Assistant entity for this tile TILE9_STATE_ENTITY: "light.ongebruikt_9" # entity used for active/inactive state styling TILE9_TITLE: "Tegel 9" # tile title shown in the UI TILE9_ICON: "\U000F04B9" # mdi:sofa — also listed in MDI_GLYPH_9 above 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: "Uit" # text shown when entity is off TILE9_LABEL_ON: "Aan" # 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 voor een 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 aan/uit # input_boolean.alarm_engaged. Een input_boolean reageert niet op # switch.toggle, vandaar de expliciete TAP_SERVICE. # ============================================================ # --- basic settings --- TILE10_ENTITY: "light.ongebruikt_10" # main Home Assistant entity for this tile TILE10_STATE_ENTITY: "light.ongebruikt_10" # entity used for active/inactive state styling TILE10_TITLE: "Tegel 10" # tile title shown in the UI TILE10_ICON: "\U000F002A" # mdi:alarm-light — also listed in MDI_GLYPH_10 above TILE10_TYPE: "light" # generiek aan/uit-type; de service hieronder doet het echte werk TILE10_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset TILE10_LONGPRESS: "none" # long press stuurt alleen het event naar HA TILE10_VALUE_MODE: "auto" # toont LABEL_ON / LABEL_OFF TILE10_LABEL_OFF: "Uit" # text shown when entity is off TILE10_LABEL_ON: "Aan" # text shown when entity is on TILE10_ACTIVE_STATE: "" # input_boolean gebruikt gewoon on/off # --- colors --- TILE10_CIRCLE_ACTIVE_COLOR: "${ACCENT_AUTO}" # input_boolean -> ACCENT_DEVICE (cyaan) 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 werkt niet op een 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" # niet in gebruik (LONGPRESS = none) TILE10_LONGPRESS_OFF_VALUE: "0" # niet in gebruik # 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 op dit bord (resistieve XPT2046) — was false op het 032c-bord # (capacitieve GT911). Dit is de originele upstream-waarde voor dit paneel. 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" BG_TOP_COLOR: "0xAEC4D9" # achtergrond boven (zacht blauw) BG_BOTTOM_COLOR: "0xD6BB9E" # achtergrond onder (warm zand) 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" # achtergrond boven (zacht blauw) # BG_BOTTOM_COLOR: "0xD6BB9E" # achtergrond onder (warm zand) # 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" # achtergrond boven (zacht blauw) # BG_BOTTOM_COLOR: "0xD6BB9E" # achtergrond onder (warm zand) # 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" # achtergrond boven (zacht blauw) # BG_BOTTOM_COLOR: "0xD6BB9E" # achtergrond onder (warm zand) # TILE_ICON_X: "0" # TILE_ICON_Y: "0" # TILE_TITLE_X: "4" # TILE_TITLE_Y: "38" # TILE_VALUE_X: "4" # TILE_VALUE_Y: "52" # ============================================================ # No need to edit below this line. # All configuration is done via the substitutions above. # Credentials are managed in secrets.yaml (see secrets.yaml.example). # The Material Design Icons font is included at fonts/materialdesignicons-webfont.ttf (Apache 2.0). # Source: https://github.com/Templarian/MaterialDesign-Webfont # ============================================================ external_components: - source: type: git url: https://github.com/MaxGramser/homeassistant_espscreen.git ref: main path: components refresh: 0s components: [xpt2046, smart_display] esphome: min_version: 2026.6.2 project: name: maxgramser.esp_screen version: "${SCREEN_FIRMWARE_VERSION}" includes: - - 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); 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::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(); }; 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(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), 0x565C66, 0xE4B23C, 0xFFFFFF, "Uit", "Aan", 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), t.state.c_str()); char buf[72]; if (std::isfinite(t.current)) snprintf(buf, sizeof(buf), "Nu: %.1f°", t.current); else snprintf(buf, sizeof(buf), "Nu: --"); 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 adviseert dit zelf in de boot-log. Levert ~40KB extra IRAM # en een kleinere binary op, en op dit bord telt elke KB. sram1_as_iram: true # minimum_chip_revision bewust weggelaten: dat stond hier voor het # 032c-bord (rev 3.1 bevestigd), maar dit is een ander fysiek bordje # met een onbekende chip-revisie. Met een onterecht hoge minimum # weigert de firmware simpelweg te booten. version: recommended variant: esp32 logger: # Voor diagnose tijdelijk op DEBUG of VERBOSE zetten. Let op: ESPHome logt # zijn configuratieregels op niveau CONFIG, die zijn op INFO onzichtbaar. 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(); return; } if (runtime_tiles::enabled && index < (int)runtime_tiles::model.count) { runtime_tiles::active_index = index; runtime_tiles::show_detail(index); } - action: light_controls_preview then: - script.execute: light_controls_preview - action: ui_self_test then: - script.execute: ui_self_test 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)) 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: - 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 voor do_tile_action: dit resistieve XPT2046-paneel geeft soms # meerdere losse press/release-cycli voor één fysieke aanraking (ruis in # het weerstandslaagje), waardoor dezelfde HA-actie meerdere keren binnen # een paar honderd ms vuurt (gezien: 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_supported_modes type: std::string restore_value: no font: - file: "https://raw.githubusercontent.com/MaxGramser/homeassistant_espscreen/main/fonts/Roboto-500.ttf" id: time_label size: 18 bpp: 4 glyphs: ['0','1','2','3','4','5','6','7','8','9',':',' ', 'A', 'M', 'P'] - file: "https://raw.githubusercontent.com/MaxGramser/homeassistant_espscreen/main/fonts/Roboto-500.ttf" id: headline size: 18 bpp: 4 glyphs: ['<','>','—','&','@','!',',','.','?','"','%','(',')','+','-','_',':','°', '0','1','2','3','4','5','6','7','8','9', 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z', ' ','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z', 'ä','ö','ü','Ä','Ö','Ü','ß','/'] - file: "https://raw.githubusercontent.com/MaxGramser/homeassistant_espscreen/main/fonts/Roboto-700.ttf" id: label size: 11 bpp: 4 glyphs: ['<','>','—','&','@','!',',','.','?','"','%','(',')','+','-','_',':','°', '0','1','2','3','4','5','6','7','8','9', 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z', ' ','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z', 'ä','ö','ü','Ä','Ö','Ü','ß','/'] - file: "https://raw.githubusercontent.com/MaxGramser/homeassistant_espscreen/main/fonts/Roboto-400.ttf" id: sublabel size: 11 bpp: 4 glyphs: ['<','>','—','&','@','!',',','.','?','"','%','(',')','+','-','_',':','°', '0','1','2','3','4','5','6','7','8','9', 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z', ' ','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z', 'ä','ö','ü','Ä','Ö','Ü','ß','/','…'] - file: "https://raw.githubusercontent.com/MaxGramser/homeassistant_espscreen/main/fonts/Roboto-400.ttf" id: sublabel_big size: 14 bpp: 4 glyphs: ['<','>','—','&','@','!',',','.','?','"','%','(',')','+','-','_',':','°', '0','1','2','3','4','5','6','7','8','9', 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z', ' ','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z', 'ä','ö','ü','Ä','Ö','Ü','ß','/','…'] # Material Design Icons (MDI) — included at fonts/materialdesignicons-webfont.ttf (Apache 2.0) # Source: https://github.com/Templarian/MaterialDesign-Webfont - file: "https://raw.githubusercontent.com/MaxGramser/homeassistant_espscreen/main/fonts/materialdesignicons-webfont.ttf" id: materialdesign_icons size: 28 glyphs: - "\U000F0335" - "\U000F0210" - "\U000F111C" - "\U000F029A" - "${MDI_GLYPH_1}" - "${MDI_GLYPH_2}" - "${MDI_GLYPH_3}" - "${MDI_GLYPH_4}" - "${MDI_GLYPH_5}" - "${MDI_GLYPH_6}" - "${MDI_GLYPH_7}" - "${MDI_GLYPH_8}" - "${MDI_GLYPH_9}" - "${MDI_GLYPH_10}" - "\U000F012C" # check # Geheugen zichtbaar maken: dit bord heeft geen PSRAM, dus de vrije heap # bepaalt hoeveel UI erbij kan. Verschijnt ook als entity in Home Assistant. debug: update_interval: 30s 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 - platform: uptime name: "Uptime" id: uptime_s update_interval: 15s - platform: template update_interval: never id: tile1_brightness on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile1_percentage on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile2_brightness on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile2_percentage on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile3_brightness on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile3_percentage on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile4_brightness on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile4_percentage on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile5_brightness on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile5_percentage on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile6_brightness on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile6_percentage on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } # Tegel 7 (Tafellamp) is een alleen-helderheid light: alleen brightness. # Bewust geen percentage / cover_position / climate-sensors — elke # HA-subscription kost heap op een bord zonder PSRAM. - platform: template update_interval: never id: tile7_brightness on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } # Tegel 9 (Woonkamer) is een kleurenlamp: brightness + supported_color_modes. # Geen percentage / cover / climate — die leest deze tegel nooit. - platform: template update_interval: never id: tile9_brightness on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile1_cover_position on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile2_cover_position on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile3_cover_position on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile4_cover_position on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile5_cover_position on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile6_cover_position on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile1_climate_current_temperature on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile1_climate_target_temperature on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile1_climate_humidity on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile2_climate_current_temperature on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile2_climate_target_temperature on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile2_climate_humidity on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile3_climate_current_temperature on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile3_climate_target_temperature on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile3_climate_humidity on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile4_climate_current_temperature on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile4_climate_target_temperature on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile4_climate_humidity on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile5_climate_current_temperature on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile5_climate_target_temperature on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile5_climate_humidity on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile6_climate_current_temperature on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile6_climate_target_temperature on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile6_climate_humidity on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } binary_sensor: - platform: status name: "Node Status" id: system_status # ---------------------------------------------------------- # Text sensors # # smartdisplay_action: # - Main touch event channel exported to Home Assistant # - On short tap, publishes one of: # tile1_press / tile2_press / tile3_press / tile4_press / tile5_press / tile6_press # - The value is reset shortly after publishing # # tile*_preset: # - Mirrors the Home Assistant preset_mode attribute for each tile entity # - Used by the UI to display fan presets when applicable # ---------------------------------------------------------- text_sensor: - platform: template name: "Schermfirmware" entity_category: diagnostic lambda: 'return {"${SCREEN_FIRMWARE_VERSION}"};' update_interval: 60s - platform: template id: smartdisplay_action name: "SmartDisplay Action" icon: mdi:gesture-tap-button - platform: template update_interval: never id: ha_state_tile1 on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: ha_state_tile2 on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: ha_state_tile3 on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: ha_state_tile4 on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: ha_state_tile5 on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: ha_state_tile6 on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: ha_state_tile7 on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: ha_state_tile8 on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: ha_state_tile9 on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: ha_state_tile10 on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } # Woonkamer meldt color_temp/rgb/xy, dus hier verschijnt de kleurknop wel. - platform: template update_interval: never id: tile9_supported_color_modes on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } # Gebruikt door de slider-overlay om te bepalen of de kleurknop nodig is. # Deze lamp meldt ['brightness'], dus de knop blijft vanzelf verborgen. - platform: template update_interval: never id: tile7_supported_color_modes on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile1_preset on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile2_preset on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile3_preset on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile4_preset on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile5_preset on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile6_preset on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile1_supported_color_modes on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile2_supported_color_modes on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile3_supported_color_modes on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile4_supported_color_modes on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile5_supported_color_modes on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile6_supported_color_modes on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile1_climate_mode on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile2_climate_mode on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile3_climate_mode on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile4_climate_mode on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile5_climate_mode on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile6_climate_mode on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile1_climate_supported_modes - platform: template update_interval: never id: tile2_climate_supported_modes - platform: template update_interval: never id: tile3_climate_supported_modes - platform: template update_interval: never id: tile4_climate_supported_modes - platform: template update_interval: never id: tile5_climate_supported_modes - platform: template update_interval: never id: tile6_climate_supported_modes - platform: template update_interval: never id: tile1_cover_state on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile2_cover_state on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile3_cover_state on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile4_cover_state on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile5_cover_state on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - platform: template update_interval: never id: tile6_cover_state on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } switch: - platform: restart name: "Restart" # Two SPI buses used by the CYD: # - lcd: built-in display # - my_touchscreen: built-in touch controller # Twee SPI bussen op de 2432S028: een voor het display, een losse voor de # resistieve XPT2046 touch (geen I2C op dit bord, in tegenstelling tot 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. Bij stilstand zie je dat niet (ruim # boven wat het oog als flikkering waarneemt), maar bij beweging van het # scherm onthult motion-blur de aan/uit-pulsen als strepen (zelfde effect # als een PWM-LED die "schokt" als je hem beweegt). 20kHz zit ruim boven # de snelheid waarbij beweging dat nog zichtbaar maakt, en laat nog altijd # ruim voldoende duty-cycle resolutie over voor vloeiend dimmen. 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, resistief, 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); 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(); for (const auto &point : touches) { cyd::touch_guard.update(point.x, point.y); } 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 # De ESP32-WROOM-32 heeft geen PSRAM; een volledige framebuffer (320x240x2 # = 153600 bytes) past niet in DRAM. 25% is de gebruikelijke waarde. # 12% in plaats van 25%: laat ~20KB extra aaneengesloten geheugen vrij, # en LVGL schaalt de stroken van een ARGB-laag mee met de beschikbare # tekenruimte, dus ook de gevraagde allocaties worden kleiner. buffer_size: 12% # Directe lokale feedback op de HELE UI, niet alleen op de tegels. # Een thema zet de stijl per widget-type, dus dit dekt in een klap alle # knoppen, tabs, sliders en de thermostaatring. Niet-klikbare widgets # komen nooit in de pressed-state, dus die blijven onaangeroerd. theme: obj: pressed: # bg_opa en NIET opa: `opa` op widget-niveau dwingt LVGL het element # naar een aparte laag te renderen, en die laag kost tientallen KB's # per ingedrukte widget. Dat liep hier het geheugen uit. 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: # Achtergrond: twee-stops verticale gradient in plaats van de 320x240 # RGB565-wallpaper. Scheelt ~150 KB flash en een image-blit per hertekening, # wat je direct terugziet in de vloeiendheid van het scrollen. - 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: 0 pad_left: 8 pad_right: 8 pad_top: 8 pad_bottom: 8 shadow_width: 0 shadow_opa: 40% - id: style_tile_disabled bg_color: 0x939391 bg_opa: COVER radius: 18 border_width: 0 pad_left: 8 pad_right: 8 pad_top: 8 pad_bottom: 8 shadow_width: 0 shadow_opa: 25% - id: style_icon_circle bg_color: 0xFEC600 bg_opa: COVER radius: 999 border_width: 0 - id: style_icon_circle_disabled bg_color: 0x7B7B6F bg_opa: COVER radius: 999 border_width: 0 - id: style_room text_font: headline text_color: 0xFFFFFF text_opa: 100% - id: style_time text_font: time_label text_color: 0xFFFFFF 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: 0xFFFFFF text_opa: 80% - id: style_title_disabled text_font: label text_color: 0xFFFFFF text_opa: 100% - id: style_value_disabled text_font: sublabel text_color: 0xD9D9D9 text_opa: 100% pages: - id: home_page styles: style_bg_grad pad_all: 0 scrollable: false scrollbar_mode: 'OFF' widgets: - label: id: lbl_room x: 11 y: ${HEADER_Y} text: "${ROOM_NAME}" styles: style_room - 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' # Directe lokale feedback: LVGL past dit toe zodra de vinger het # glas raakt, in dezelfde frame. Onafhankelijk van 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 modus "vacuum": opent de stofzuigerbediening. # Valt buiten de action/slider-keten, dus die blijft ongewijzigd. - 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' # Directe lokale feedback: LVGL past dit toe zodra de vinger het # glas raakt, in dezelfde frame. Onafhankelijk van 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 modus "vacuum": opent de stofzuigerbediening. # Valt buiten de action/slider-keten, dus die blijft ongewijzigd. - 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' # Directe lokale feedback: LVGL past dit toe zodra de vinger het # glas raakt, in dezelfde frame. Onafhankelijk van 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 modus "vacuum": opent de stofzuigerbediening. # Valt buiten de action/slider-keten, dus die blijft ongewijzigd. - 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' # Directe lokale feedback: LVGL past dit toe zodra de vinger het # glas raakt, in dezelfde frame. Onafhankelijk van 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 modus "vacuum": opent de stofzuigerbediening. # Valt buiten de action/slider-keten, dus die blijft ongewijzigd. - 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' # Directe lokale feedback: LVGL past dit toe zodra de vinger het # glas raakt, in dezelfde frame. Onafhankelijk van 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 modus "vacuum": opent de stofzuigerbediening. # Valt buiten de action/slider-keten, dus die blijft ongewijzigd. - 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' # Directe lokale feedback: LVGL past dit toe zodra de vinger het # glas raakt, in dezelfde frame. Onafhankelijk van 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 modus "vacuum": opent de stofzuigerbediening. # Valt buiten de action/slider-keten, dus die blijft ongewijzigd. - if: condition: lambda: "return std::string(\"${TILE6_LONGPRESS}\") == \"vacuum\";" then: - script.execute: id: open_vacuum_overlay title: "${TILE6_TITLE}" entity: "${TILE6_ENTITY}" # ---------- TILE 7 — Tafellamp ---------- # Alleen-helderheid lamp: er zijn bewust geen percentage/cover/ # climate-sensors voor deze tegel aangemaakt, die zouden alleen # maar HA-subscriptions en heap kosten. De overlay krijgt daarom # NAN / "" voor de velden die een light nooit gebruikt. - 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 — Brievenbus reset ---------- # input_select met opties Vol/Leeg. De tegel licht op zodra de # status TILE8_ACTIVE_STATE ("Vol") is; een korte tik stuurt # input_select.select_option met option: "Leeg". # Geen slider-modus, dus deze tegel heeft alleen ha_state_tile8 # nodig — verder geen enkele HA-subscription. - 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-modus kan hier niet zonder eerst de bijbehorende # sensors aan te maken; "action" werkt wel 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 — Woonkamer (verhuisd van tegel 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 aan/uit ---------- - 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 ---------- - obj: id: page_prev align: BOTTOM_LEFT x: 9 y: -2 width: 100 height: 32 pad_all: 0 radius: 8 bg_color: 0x3C3C40 bg_opa: COVER border_width: 0 scrollable: false on_short_click: - if: condition: lambda: 'return cyd::touch_guard.accept(millis(), 11);' then: - lambda: 'id(tile_page) = 0;' - script.execute: show_tile_page widgets: - label: align: CENTER text: "< Vorige" text_font: sublabel text_color: 0xFFFFFF clickable: false - label: id: page_number align: BOTTOM_MID y: -9 text: "1 / 2" text_font: sublabel text_color: 0xFFFFFF - obj: id: page_next align: BOTTOM_RIGHT x: -9 y: -2 width: 100 height: 32 pad_all: 0 radius: 8 bg_color: 0x3C3C40 bg_opa: COVER border_width: 0 scrollable: false 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: CENTER text: "Volgende >" text_font: sublabel text_color: 0xFFFFFF 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: - lvgl.widget.hide: brightness_overlay - obj: id: brightness_overlay_dim x: 0 y: 0 width: ${DISPLAY_W} height: ${DISPLAY_H} bg_color: 0x000000 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: 0x939391 bg_opa: 100% radius: 28 # Enige widget met ZOWEL radius ALS een kind (overlay_icon). # LVGL snijdt kinderen dan bij op de ronding, en dat kan # alleen via een ARGB-laag van 96x160 = 46KB aaneengesloten. # Lukt die allocatie niet, dan blijft LVGL het proberen tot # de task watchdog het bord herstart. Ronding blijft, het # bijsnijden gaat eraf. clip_corner: false border_width: 0 indicator: bg_color: 0xFEC600 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));" # Het icoon zat als KIND in de slider. Een widget met een radius # en kinderen moet die kinderen bijsnijden op de ronding, en dat # kan LVGL alleen via een ARGB-laag van tientallen KB's. Zonder # PSRAM mislukt dat soms, waarna LVGL blijft hertesten tot de # watchdog het bord herstart. Als broer eroverheen is er geen # laag nodig en ziet het er identiek uit. - label: id: overlay_icon align: CENTER y: ${OVERLAY_ICON_Y} text_font: materialdesign_icons text_color: 0xFFFFFF 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: 0x171719 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: 0x000000 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: 0x3C3C40 bg_opa: COVER border_width: 1 border_color: 0xFFFFFF border_opa: 20% clickable: true scrollable: false on_click: - lvgl.widget.hide: color_detail_overlay widgets: - label: align: CENTER text: "x" text_font: sublabel_big text_color: 0xFFFFFF 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: 0xFF9F0A bg_opa: COVER border_width: 0 clickable: true scrollable: false on_click: - lvgl.widget.hide: color_detail_overlay widgets: - label: align: CENTER text: "\U000F012C" text_font: materialdesign_icons text_color: 0xFFFFFF clickable: false # ---------- CLIMATE DETAIL OVERLAY ---------- - obj: id: climate_detail_overlay x: 0 y: 0 width: ${DISPLAY_W} height: ${DISPLAY_H} bg_color: 0x171719 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: 0x000000 bg_opa: 62% border_width: 0 clickable: false scrollable: false - obj: id: climate_close_button align: TOP_LEFT x: 10 y: 9 # 44 i.p.v. 36: iets groter doelvlak, groeit weg van de arc # (top-left anchor blijft staan) i.p.v. ernaartoe. width: 44 height: 44 radius: 999 bg_color: 0x3C3C40 bg_opa: COVER border_width: 1 border_color: 0xFFFFFF border_opa: 20% clickable: true scrollable: false on_click: - lvgl.widget.hide: climate_detail_overlay widgets: - label: align: CENTER text: "x" text_font: sublabel_big text_color: 0xFFFFFF clickable: false - obj: id: climate_done_button align: TOP_RIGHT x: -10 y: 9 width: 44 height: 44 radius: 999 bg_color: 0xFEC600 bg_opa: COVER border_width: 0 clickable: true scrollable: false on_click: - lvgl.widget.hide: climate_detail_overlay widgets: - label: align: CENTER text: "\U000F012C" text_font: materialdesign_icons text_color: 0xFFFFFF 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: "Current: --" text_font: sublabel_big text_color: 0xD1E8FF text_opa: 85% clickable: false - arc: id: climate_arc align: CENTER y: 20 width: 184 height: 184 # Zonder dit claimt de arc een onzichtbare rand extra # klikgebied buiten de 184x184 box — vlakbij de kruisje- # knop linksboven ontstaat daardoor 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: "SET TO" text_font: sublabel_big text_color: 0xFFFFFF text_opa: 68% clickable: false - label: id: climate_target_label align: CENTER y: 20 text: "20.0°" text_font: headline text_color: 0xFFFFFF clickable: false - obj: id: climate_minus_button align: BOTTOM_LEFT x: 38 y: -16 width: 48 height: 36 radius: 18 bg_color: 0x3C3C40 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: 0xFFFFFF clickable: false - obj: id: climate_plus_button align: BOTTOM_RIGHT x: -38 y: -16 width: 48 height: 36 radius: 18 bg_color: 0x3C3C40 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: 0xFFFFFF clickable: false - obj: id: climate_mode_pill align: BOTTOM_MID y: -18 width: 126 height: 34 radius: 17 bg_color: 0x2C2C30 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: 0xFFFFFF clickable: false # ---------- CLIMATE MODE PICKER ---------- # Vervangt het doorcyclen van modi. Cyclen stuurt bij elke tik een # set_hvac_mode, dus om van 'cool' naar 'dry' te komen zet je een # warmtepomp onderweg ook echt in 'heat' en 'fan_only'. Hier kies je # rechtstreeks, en wordt er precies een opdracht verstuurd. - obj: id: climate_mode_overlay x: 0 y: 0 width: ${DISPLAY_W} height: ${DISPLAY_H} hidden: true bg_color: 0x000000 bg_opa: 90% 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: 0xFFFFFF clickable: false # Flex zorgt dat niet-ondersteunde modi geen gat achterlaten: # LVGL slaat verborgen kinderen over bij het schikken. - obj: id: climate_mode_list align: TOP_MID y: 50 width: 98% height: 178 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: cmode_off width: 132 height: 38 radius: 19 bg_color: 0x2C2C30 bg_opa: 92% border_width: 0 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: 0xFFFFFF clickable: false - obj: id: cmode_heat width: 132 height: 38 radius: 19 bg_color: 0x2C2C30 bg_opa: 92% border_width: 0 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: 0xFFFFFF clickable: false - obj: id: cmode_cool width: 132 height: 38 radius: 19 bg_color: 0x2C2C30 bg_opa: 92% border_width: 0 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: 0xFFFFFF clickable: false - obj: id: cmode_heat_cool width: 132 height: 38 radius: 19 bg_color: 0x2C2C30 bg_opa: 92% border_width: 0 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: 0xFFFFFF clickable: false - obj: id: cmode_auto width: 132 height: 38 radius: 19 bg_color: 0x2C2C30 bg_opa: 92% border_width: 0 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: 0xFFFFFF clickable: false - obj: id: cmode_dry width: 132 height: 38 radius: 19 bg_color: 0x2C2C30 bg_opa: 92% border_width: 0 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: 0xFFFFFF clickable: false - obj: id: cmode_fan_only width: 132 height: 38 radius: 19 bg_color: 0x2C2C30 bg_opa: 92% border_width: 0 clickable: true scrollable: false pad_all: 0 on_click: - script.execute: { id: set_climate_mode, mode: "fan_only" } widgets: - label: align: CENTER text: "Fan" text_font: sublabel_big text_color: 0xFFFFFF clickable: false - obj: id: climate_mode_overlay_x align: TOP_LEFT x: 10 y: 9 width: 36 height: 36 radius: 999 bg_color: 0x3C3C40 bg_opa: COVER border_width: 1 border_color: 0xFFFFFF border_opa: 20% clickable: true scrollable: false on_click: - lvgl.widget.hide: climate_mode_overlay widgets: - label: align: CENTER text: "x" text_font: sublabel_big text_color: 0xFFFFFF clickable: false - obj: id: climate_mode_overlay_ok align: TOP_RIGHT x: -10 y: 9 width: 36 height: 36 radius: 999 bg_color: 0xFEC600 bg_opa: COVER border_width: 0 clickable: true scrollable: false on_click: - lvgl.widget.hide: climate_mode_overlay widgets: - label: align: CENTER text: "\U000F012C" text_font: materialdesign_icons text_color: 0xFFFFFF clickable: false # ---------- VACUUM CONTROL ---------- # Een stofzuiger is geen schakelaar: start, pauze, stop en terug naar # het dock zijn losse opdrachten, en de zuigkracht is een aparte as. - obj: id: vacuum_overlay x: 0 y: 0 width: ${DISPLAY_W} height: ${DISPLAY_H} hidden: true bg_color: 0x000000 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: 0xFFFFFF 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: 0x2C2C30 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: 0xFFFFFF clickable: false - obj: id: vac_pause width: 72 height: 38 radius: 19 bg_color: 0x2C2C30 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: 0xFFFFFF clickable: false - obj: id: vac_return_to_base width: 72 height: 38 radius: 19 bg_color: 0x2C2C30 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: 0xFFFFFF clickable: false - obj: id: vac_stop width: 72 height: 38 radius: 19 bg_color: 0x2C2C30 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: 0xFFFFFF clickable: false - label: align: TOP_MID y: 138 text: "Suction" text_font: sublabel text_color: 0xFFFFFF 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: 0x1F1F23 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: 0xFFFFFF clickable: false - obj: id: vacspd_balanced width: 72 height: 32 radius: 19 bg_color: 0x1F1F23 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: 0xFFFFFF clickable: false - obj: id: vacspd_turbo width: 72 height: 32 radius: 19 bg_color: 0x1F1F23 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: 0xFFFFFF clickable: false - obj: id: vacspd_max width: 72 height: 32 radius: 19 bg_color: 0x1F1F23 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: 0xFFFFFF clickable: false - obj: id: vacuum_overlay_x align: TOP_LEFT x: 10 y: 9 width: 36 height: 36 radius: 999 bg_color: 0x3C3C40 bg_opa: COVER border_width: 1 border_color: 0xFFFFFF border_opa: 20% clickable: true scrollable: false on_click: - lvgl.widget.hide: vacuum_overlay widgets: - label: align: CENTER text: "x" text_font: sublabel_big text_color: 0xFFFFFF clickable: false - obj: id: vacuum_overlay_ok align: TOP_RIGHT x: -10 y: 9 width: 36 height: 36 radius: 999 bg_color: 0xFEC600 bg_opa: COVER border_width: 0 clickable: true scrollable: false on_click: - lvgl.widget.hide: vacuum_overlay widgets: - label: align: CENTER text: "\U000F012C" text_font: materialdesign_icons text_color: 0xFFFFFF 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: "Volg de kalibratiewizard" 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(); id(tile_page) = 1; - script.execute: show_tile_page - delay: 150ms - 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); const bool paginated = 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 = i < count && (paginated ? i >= 6 : i < 6); ok = ok && (lv_obj_has_flag(tiles[i], LV_OBJ_FLAG_HIDDEN) != visible); } for (lv_obj_t* control : {id(page_prev), id(page_next), id(page_number)}) ok = ok && (lv_obj_has_flag(control, LV_OBJ_FLAG_HIDDEN) != paginated); lv_area_t nav, grid; lv_obj_get_coords(id(page_next), &nav); lv_obj_get_coords(id(tile_scroll), &grid); ok = ok && (!paginated || grid.y2 < nav.y1); 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 - lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' - logger.log: "UI_TEST COMPLETE" - id: show_tile_page then: - lambda: |- 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); 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); if (!id(ui_refresh).is_running()) id(ui_refresh).execute(); - id: wake_display mode: restart then: - lambda: |- id(last_touch_ms) = millis(); id(display_dimmed) = false; - light.turn_on: id: back_light # Bijna instant omhoog. De ESPHome-standaard is 1s, en dat voelt traag # als je het scherm aanraakt en op licht staat te wachten. transition_length: 80ms brightness: !lambda "return screen_settings::current.brightness / 100.0f;" - lvgl.widget.hide: dim_wake_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: 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(); } # Terug naar het basisscherm bij standby. Bewust via de ESPHome # acties en NIET met lv_obj_add_flag in een lambda: dit script # draait vanuit een interval, dus buiten LVGL's eigen afhandeling. - 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);" # Opent de moduskiezer. Verbergt modi die de entiteit niet ondersteunt en # markeert de huidige, zodat je ziet waar je staat voor je iets kiest. # Opent de stofzuigerbediening en onthoudt om welke entiteit het gaat. - 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 # Een losse opdracht: start, pause, stop of 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);" # Zuigkracht is een aparte as, los van 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 ? 0xFF9F0A : 0x2C2C30), 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"); - lvgl.widget.show: climate_mode_overlay # Zet precies een modus. Geen tussenstappen, dus geen ongewenste # commando's naar de warmtepomp onderweg. - 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; if (mode == "off") lv_label_set_text(id(climate_mode_label), "Off"); if (mode == "heat") lv_label_set_text(id(climate_mode_label), "Heat"); if (mode == "cool") lv_label_set_text(id(climate_mode_label), "Cool"); if (mode == "heat_cool") lv_label_set_text(id(climate_mode_label), "Heat/Cool"); if (mode == "auto") lv_label_set_text(id(climate_mode_label), "Auto"); if (mode == "dry") lv_label_set_text(id(climate_mode_label), "Dry"); if (mode == "fan_only") lv_label_set_text(id(climate_mode_label), "Fan"); - 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), "Voorbeeld 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: zelfde entity binnen 600ms genegeerd. Voorkomt dat // touch-ruis dezelfde HA-actie meerdere keren afvuurt (zie // globals hierboven voor waarom dit nodig is). 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 na vorige)", 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: |- // Accentkleur uit het Home Assistant-domein: het stuk voor de punt in // het entity_id. script/scene/button vuur je af (ACCENT_ACTION), een // light is een light (ACCENT_LIGHT), de rest is apparaat. // LET OP: deze helper staat twee keer in dit bestand — hier en in // open_value_overlay. Pas ze samen aan. 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}; }; // De sliderkleur komt van de tegel, dus die kan ook ${ACCENT_AUTO} zijn. 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), "Current: --"); } else if (id(active_climate_humidity) >= 0) { snprintf(current_buf, sizeof(current_buf), "Current: %.1f° - Humidity: %d %%", climate_current_temperature, id(active_climate_humidity)); } else { snprintf(current_buf, sizeof(current_buf), "Current: %.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), id(active_climate_mode).c_str()); 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) { runtime_tiles::render(id(lbl_room)); auto now = id(homeassistant_time).now(); if (screen_settings::current.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); if (now.is_valid()) runtime_tiles::label(id(lbl_time), now.strftime(screen_settings::current.clock_24h ? "%H:%M" : "%I:%M")); 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 niet leeg? Dan telt alleen die exacte HA-state als "aan". // Zo kan een input_select ("Vol"/"Leeg") ook een tegel laten oplichten. 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}"); // Tegel 7/8 hebben geen cover_state-sensor, dus geen cover-tak nodig. 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}"); // Tegel 7: light met alleen brightness. Tegel 8: puur tekst (Vol/Leeg). 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}"); // Tegel 9: kleurenlamp (brightness). Tegel 10: puur tekst (Aan/Uit). 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), "Bezig"); else if (state == "docked") set_lbl(id(t6_value), "In dock"); else if (state == "returning") set_lbl(id(t6_value), "Naar dock"); else if (state == "paused") set_lbl(id(t6_value), "Gepauzeerd"); else if (state == "error") set_lbl(id(t6_value), "Fout"); 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), "Niet beschikbaar"); if (!id(ha_state_tile2).has_state() || id(ha_state_tile2).state == "unknown" || id(ha_state_tile2).state == "unavailable") set_lbl(id(t2_value), "Niet beschikbaar"); if (!id(ha_state_tile3).has_state() || id(ha_state_tile3).state == "unknown" || id(ha_state_tile3).state == "unavailable") set_lbl(id(t3_value), "Niet beschikbaar"); if (!id(ha_state_tile4).has_state() || id(ha_state_tile4).state == "unknown" || id(ha_state_tile4).state == "unavailable") set_lbl(id(t4_value), "Niet beschikbaar"); if (!id(ha_state_tile5).has_state() || id(ha_state_tile5).state == "unknown" || id(ha_state_tile5).state == "unavailable") set_lbl(id(t5_value), "Niet beschikbaar"); if (!id(ha_state_tile6).has_state() || id(ha_state_tile6).state == "unknown" || id(ha_state_tile6).state == "unavailable") set_lbl(id(t6_value), "Niet beschikbaar"); if (!id(ha_state_tile7).has_state() || id(ha_state_tile7).state == "unknown" || id(ha_state_tile7).state == "unavailable") set_lbl(id(t7_value), "Niet beschikbaar"); if (!id(ha_state_tile8).has_state() || id(ha_state_tile8).state == "unknown" || id(ha_state_tile8).state == "unavailable") set_lbl(id(t8_value), "Niet beschikbaar"); if (!id(ha_state_tile9).has_state() || id(ha_state_tile9).state == "unknown" || id(ha_state_tile9).state == "unavailable") set_lbl(id(t9_value), "Niet beschikbaar"); if (!id(ha_state_tile10).has_state() || id(ha_state_tile10).state == "unknown" || id(ha_state_tile10).state == "unavailable") set_lbl(id(t10_value), "Niet beschikbaar"); 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), "Current: --"); } else if (id(active_climate_humidity) >= 0) { snprintf(current_buf, sizeof(current_buf), "Current: %.1f° - Humidity: %d %%", current_temperature, id(active_climate_humidity)); } else { snprintf(current_buf, sizeof(current_buf), "Current: %.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(); // Vóór de eerste tijd-sync is now() epoch 0 (1970) — dan geen // misleidende "01:00" tonen, gewoon leeg laten tot er een // echte tijd binnen is (na de eerste Home Assistant-tijdsynchronisatie). 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)); } } // Accentkleur uit het Home Assistant-domein: het stuk voor de punt in // het entity_id. script/scene/button vuur je af (ACCENT_ACTION), een // light is een light (ACCENT_LIGHT), de rest is apparaat. // LET OP: deze helper staat twee keer in dit bestand — hier en in // open_value_overlay. Pas ze samen aan. 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} = "bepaal zelf"; een echte hexwaarde wint altijd. 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: "Tegelinstellingen" internal: ${DASHBOARD_INBOX_INTERNAL} button: - platform: template name: "Touch kalibreren" internal: ${DASHBOARD_INBOX_INTERNAL} entity_category: config on_press: - lambda: 'if (runtime_tiles::enabled) screen_calibration::begin();' number: - platform: template id: setting_brightness name: "Helderheid normaal" entity_category: config min_value: 5 max_value: 100 step: 1 unit_of_measurement: "%" update_interval: 2s 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: "Helderheid standby" entity_category: config min_value: 0 max_value: 100 step: 1 unit_of_measurement: "%" update_interval: 2s 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: "Helderheid nacht" entity_category: config min_value: 0 max_value: 100 step: 1 unit_of_measurement: "%" update_interval: 2s 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 na" entity_category: config min_value: 60 max_value: 86400 step: 60 unit_of_measurement: "s" update_interval: 2s 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();