commit a5521ceaaaa72dcde65d64994bd6b879d54e42e3 Author: Max Gramser Date: Tue Sep 1 11:38:39 2026 +0200 Initial commit: SmartDisplay CYD config met scrollbaar tegelraster Eerste commit van het project. De config staat op het punt waar het tegelraster scrollbaar is en er acht tegels in staan. Wat er in deze ronde aan de bestaande config veranderd is: - Home screen scrollt. Wallpaper, kamernaam en klok staan op de pagina zelf en blijven staan; alle tegels zitten in een transparante `tile_scroll` container die verticaal scrollt. Ruimte voor 2x5 = 10 tegels, waarvan rij 1-3 zichtbaar is in rust. GRID_ROW*_Y is nu relatief aan die container; SCROLL_Y/SCROLL_H per orientatiepreset. LVGL annuleert klik en long-press zodra een sleep de scroll-limiet passeert, dus tikken en scrollen bijten elkaar niet. - Achtergrond is een twee-stops gradient in plaats van een 320x240 RGB565-wallpaper. Scheelt ~150 KB flash en een image-blit per hertekening, wat het scrollen bruikbaar maakt. Dezelfde gradient wordt hergebruikt als achtergrond van de drie overlays. - Tegel 7: Tafellamp (light.tafellamp_licht), tik = toggle, long-press = helderheidsslider. - Tegel 8: Brievenbus reset (input_select.brievenbus_status). Licht op zodra de status "Vol" is, tik zet hem terug op "Leeg". - TILE*_ACTIVE_STATE toegevoegd: een exacte HA-state die als "actief" telt, voor entiteiten die geen on/off gebruiken. Leeg = oud gedrag. - do_tile_action kent nu param_key "option", voor input_select.select_option. Tegels 7 en 8 abonneren alleen op de HA-entiteiten die ze echt lezen, in plaats van op alle elf per tegel. Na flashen: 148 KB vrije heap, grootste blok 94 KB. Co-Authored-By: Claude Opus 5 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fc5d49d --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# Gitignore settings for ESPHome +# This is an example and may include too much for your use-case. +# You can modify this file to suit your needs. +/.esphome/ +/secrets.yaml + + +# Personal device configuration (not part of the example) +testdevice.yaml + +# Handmatige backups van de config +*.yaml.bak* diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..30cf57e --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/esphome-cyd-display.iml b/.idea/esphome-cyd-display.iml new file mode 100644 index 0000000..c956989 --- /dev/null +++ b/.idea/esphome-cyd-display.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..c5cc3aa --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/php.xml b/.idea/php.xml new file mode 100644 index 0000000..f324872 --- /dev/null +++ b/.idea/php.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/TILE_CONFIGURATION.md b/TILE_CONFIGURATION.md new file mode 100644 index 0000000..53a81ec --- /dev/null +++ b/TILE_CONFIGURATION.md @@ -0,0 +1,527 @@ +# Tile Configuration Reference + +This document explains every substitution in `home-like.yaml` and provides copy-paste examples for all supported entity types. + +--- + +## How substitutions work + +ESPHome substitutions are compile-time text replacements. There is no runtime logic in the substitution block — every `${TILE1_...}` reference in the YAML is replaced with the value you set before the firmware is compiled and flashed. This means: + +- You cannot use `if/else` in substitutions +- Changes require a reflash +- The firmware is fully self-contained after flashing + +--- + +## The home screen scrolls + +The wallpaper, the room name and the clock sit on the page itself and stay put. +All tiles live inside one transparent container (`tile_scroll`) that scrolls +vertically, so the grid can be taller than the screen. + +- The grid is **2 columns x 5 rows = 10 tile slots**. Rows 1-3 are visible at + rest; dragging up reveals rows 4 and 5. +- `GRID_ROW*_Y` are **relative to the scroll container**, not to the page. + `SCROLL_Y` / `SCROLL_H` define where the container sits and how tall the + viewport is. Both are set per orientation preset. +- LVGL derives the scroll length from the tile positions, so there is no + content height to maintain by hand. +- Tapping and scrolling do not conflict: once a drag passes LVGL's scroll + limit (10 px) the short click *and* the long press are cancelled. +- `scroll_snap_y: START` makes the container click into place per row. Set it + to `NONE` on the `tile_scroll` object for free scrolling. + +## Background + +The background is a two-stop vertical gradient (`BG_TOP_COLOR` -> +`BG_BOTTOM_COLOR`), not an image. That saves ~150 KB of flash and removes an +image blit from every redraw, which is what makes scrolling usable on this +board. The same gradient is reused as the backdrop of the slider, color and +thermostat overlays through the `style_bg_grad` style. + +--- + +## Global settings + +| Key | Default | Description | +|-----|---------|-------------| +| `DIRECT_ACTIONS` | `"true"` | `true` = device calls HA services directly on tap. `false` = only publishes the action event, you handle it in HA automations. | +| `ROOM_NAME` | `"Office"` | Text shown in the top-left header. | +| `TIME_24H` | `"true"` | `true` = 24h clock, `false` = 12h with AM/PM. | +| `AUTO_DIM` | `"true"` | Enable backlight auto-dimming after inactivity. | +| `AUTO_DIM_TIMEOUT` | `"10"` | Seconds of no touch before dimming. | +| `MAX_BRIGHTNESS` | `"100"` | Backlight brightness (%) when active. | +| `DIM_BRIGHTNESS` | `"40"` | Backlight brightness (%) when dimmed. | +| `NIGHT_MODE` | `"true"` | Use a lower brightness level during night hours. | +| `NIGHT_START_HOUR` | `"22"` | Hour (0–23) when night mode starts. | +| `NIGHT_END_HOUR` | `"7"` | Hour (0–23) when night mode ends. | +| `NIGHT_DIM_BRIGHTNESS` | `"18"` | Backlight brightness (%) during night hours. | +| `BG_TOP_COLOR` | `"0xAEC4D9"` | Top color of the background gradient. | +| `BG_BOTTOM_COLOR` | `"0xD6BB9E"` | Bottom color of the background gradient. | + +--- + +## Per-tile substitutions + +Each configured tile has the same set of keys, just with a different number +(TILE1 … TILE8). Slots 9 and 10 are free — see [Adding a tile](#adding-a-tile). + +### Basic settings + +| Key | Description | +|-----|-------------| +| `TILE*_ENTITY` | The Home Assistant entity this tile controls (e.g. `light.living_room`). | +| `TILE*_STATE_ENTITY` | Entity used to determine active/inactive visual state. Usually the same as `TILE*_ENTITY`. Set to a different entity if you want the tile to reflect a group or a related sensor. | +| `TILE*_TITLE` | Text label shown on the tile. | +| `TILE*_ICON` | Material Design Icons Unicode glyph. Find icons at [pictogrammers.com/library/mdi](https://pictogrammers.com/library/mdi/) — click an icon and copy the Unicode value (e.g. `U+F0769` → `"\U000F0769"`). | +| `TILE*_TYPE` | Entity type: `light` \| `fan` \| `switch` \| `scene` \| `script` \| `cover` \| `climate`. Controls tap behavior, value display, and slider type in auto mode. | +| `TILE*_TAP_ACTION` | What happens on short tap — see [Tap actions](#tap-actions) below. | +| `TILE*_LONGPRESS` | What happens on long press — see [Long press modes](#long-press-modes) below. | +| `TILE*_VALUE_MODE` | How the value line below the title is rendered — see [Value modes](#value-modes) below. | +| `TILE*_LABEL_OFF` | Text shown in the value line when the entity is off / inactive. | +| `TILE*_LABEL_ON` | Text shown in the value line when `VALUE_MODE` is `text` and entity is on. | +| `TILE*_ACTIVE_STATE` | Optional. Exact Home Assistant state that counts as "active". Leave empty for the default behavior (`on`, or always-on for scenes/scripts). Used by tile 8, where the state is `Vol` / `Leeg` rather than `on` / `off`. | + +### Colors + +All colors are 24-bit hex in the format `"0xRRGGBB"`. + +| Key | Description | +|-----|-------------| +| `TILE*_CIRCLE_ACTIVE_COLOR` | Icon circle background when active. | +| `TILE*_CIRCLE_DISABLED_COLOR` | Icon circle background when inactive. | +| `TILE*_ICON_ACTIVE_COLOR` | Icon glyph color when active. | +| `TILE*_ICON_DISABLED_COLOR` | Icon glyph color when inactive. | +| `TILE*_BG_ACTIVE_COLOR` | Tile background when active. | +| `TILE*_BG_DISABLED_COLOR` | Tile background when inactive. | +| `TILE*_TITLE_ACTIVE_COLOR` | Title text color when active. | +| `TILE*_TITLE_DISABLED_COLOR` | Title text color when inactive. | +| `TILE*_VALUE_ACTIVE_COLOR` | Value text color when active. | +| `TILE*_VALUE_DISABLED_COLOR` | Value text color when inactive. | + +### Advanced settings (short tap) + +| Key | Default | Description | +|-----|---------|-------------| +| `TILE*_TAP_SERVICE` | `""` | Override the HA service called on tap (e.g. `"light.toggle"`). Leave empty to use the default for the tile type. | +| `TILE*_TAP_PARAM_KEY` | `""` | Extra parameter key for the service call. Supported keys: `preset_mode`, `brightness_pct`, `percentage`, `option`. Any other value is ignored and the service is called with `entity_id` only. | +| `TILE*_TAP_PARAM_VAL` | `""` | Extra parameter value (e.g. `"Auto"`). | + +### Advanced settings (long press — slider mode) + +Only relevant when `TILE*_LONGPRESS` is set to `"slider"`. + +| Key | Default | Description | +|-----|---------|-------------| +| `TILE*_LONGPRESS_SLIDER` | `"auto"` | Slider type: `auto` \| `brightness` \| `percentage` \| `cover_position` \| `climate_temperature`. `auto` picks the right type based on `TILE*_TYPE`. | +| `TILE*_LONGPRESS_OFF_VALUE` | `"0"` | Initial slider position (0–100) shown when the entity is currently off. | + +### Advanced settings (climate tiles) + +Only relevant when `TILE*_TYPE` is set to `"climate"`. + +| Key | Default | Description | +|-----|---------|-------------| +| `TILE*_CLIMATE_MIN_TEMP` | `"5.0"` | Minimum target temperature shown in the thermostat overlay. | +| `TILE*_CLIMATE_MAX_TEMP` | `"30.0"` | Maximum target temperature shown in the thermostat overlay. | +| `TILE*_CLIMATE_STEP` | `"0.5"` | Step size for the thermostat ring and +/- buttons. Values below `0.5` are rounded up to `0.5`, so the UI always adjusts in whole or half-degree steps. | +| `TILE*_CLIMATE_CURRENT_TEMP_ATTRIBUTE` | `"current_temperature"` | Home Assistant attribute used for the current room temperature. | +| `TILE*_CLIMATE_TARGET_TEMP_ATTRIBUTE` | `"temperature"` | Home Assistant attribute used for the target temperature. | +| `TILE*_CLIMATE_HUMIDITY_ATTRIBUTE` | `"current_humidity"` | Home Assistant attribute used for humidity. Leave the default if the entity does not provide humidity. | + +### Advanced settings (long press — action mode) + +Only relevant when `TILE*_LONGPRESS` is set to `"action"`. + +| Key | Default | Description | +|-----|---------|-------------| +| `TILE*_LONGPRESS_ACTION` | `""` | HA entity to call on long press. Can be completely different from `TILE*_ENTITY`. | +| `TILE*_LONGPRESS_ACTION_TYPE` | `""` | Entity type of the long press target: `light` \| `fan` \| `switch` \| `scene` \| `script` \| `cover`. Leave empty to inherit from `TILE*_TYPE`. | +| `TILE*_LONGPRESS_ACTION_SERVICE` | `""` | Service to call (default is `activate`/`turn_on`). Set e.g. `"light.toggle"` to toggle instead. | + +--- + +## Tap actions + +| Value | Behavior | +|-------|----------| +| `auto` | Picks the best action for the tile type: `toggle` for light/fan/switch/cover, `activate` for scene/script. Climate tiles do not have a default tap action; use `TILE*_TAP_SERVICE` if you want short tap behavior. | +| `toggle` | Toggles the entity (light.toggle / fan.toggle / switch.toggle / cover open↔close). | +| `activate` | Turns on / activates the entity (scene.turn_on / script.turn_on / light.turn_on / cover.open_cover). | +| `fan_toggle_preset` | If fan is ON: turns it off. If fan is OFF: turns it on and sets a preset mode (configure with `TAP_PARAM_KEY` / `TAP_PARAM_VAL`). | +| `custom` | Use `TAP_SERVICE` + `TAP_PARAM_KEY` / `TAP_PARAM_VAL` for a fully custom service call. | + +--- + +## Long press modes + +| Value | Behavior | +|-------|----------| +| `slider` | Opens a value overlay with a slider. Configure with `TILE*_LONGPRESS_SLIDER` and `TILE*_LONGPRESS_OFF_VALUE`. | +| `action` | Fires a different HA entity than the short tap. Configure with `TILE*_LONGPRESS_ACTION*`. | +| `none` | No direct action — only publishes `tileN_long_press` to HA so you can react in automations. | + +In all modes, the event `tileN_long_press` is always published to `sensor.smartdisplay_action` in Home Assistant. + +For light tiles in `slider` mode, the brightness overlay automatically shows a color-ring button when `DIRECT_ACTIONS` is `"true"` and Home Assistant reports a supported color mode (`hs`, `rgb`, `rgbw`, `rgbww`, `xy`) or color temperature mode (`color_temp`). The color detail view can send `light.turn_on` with `color_name`, `color_temp_kelvin`, and the current brightness percentage. If the light only supports color or only supports color temperature, the Color/Temp tab switch is hidden and only the supported controls are shown. No extra tile substitutions are required. + +For climate tiles in `slider` mode, the thermostat overlay sends `climate.set_temperature` with the configured target temperature. The min/max range, step size, and Home Assistant attribute names are configured through the `TILE*_CLIMATE_*` substitutions. The mode pill cycles through the climate entity's supported `hvac_modes` and sends `climate.set_hvac_mode` when `DIRECT_ACTIONS` is `"true"`. + +--- + +## Value modes + +| Value | Behavior | +|-------|----------| +| `auto` | Picks automatically: `brightness` for lights, `percentage` or `preset` for fans, `climate` for climate entities, `text` for everything else. | +| `brightness` | Shows brightness as a percentage (e.g. `80 %`). When off, shows `LABEL_OFF`. | +| `percentage` | Shows fan speed as a percentage. When off, shows `LABEL_OFF`. | +| `preset` | Shows the fan preset_mode attribute. When off, shows `LABEL_OFF`. | +| `climate` | Shows the target temperature, falling back to current temperature if no target is available. When off, shows `LABEL_OFF`. | +| `text` | Shows `LABEL_ON` when on, `LABEL_OFF` when off. For scenes/scripts, always shows `LABEL_ON`. | + +--- + +## Home Assistant events + +Every tap publishes a short-lived state to `sensor.smartdisplay_action`: + +| Event | Trigger | +|-------|---------| +| `tile1_press` … `tile8_press` | Short tap on tile 1–8 | +| `tile1_long_press` … `tile8_long_press` | Long press on tile 1–8 | + +The state resets to `""` after 500ms. In HA automations, trigger on `state` → `to: "tile1_press"` etc. + +--- + +## Complete examples + +### Light — toggle + brightness slider + +```yaml +TILE1_ENTITY: "light.living_room_ceiling" +TILE1_STATE_ENTITY: "light.living_room_ceiling" +TILE1_TITLE: "Ceiling" +TILE1_ICON: "\U000F0769" # mdi:ceiling-light +TILE1_TYPE: "light" +TILE1_TAP_ACTION: "auto" # toggles the light +TILE1_LONGPRESS: "slider" # long press opens brightness slider +TILE1_VALUE_MODE: "auto" # shows brightness % when on, "Off" when off +TILE1_LABEL_OFF: "Off" +TILE1_LABEL_ON: "On" +# colors +TILE1_CIRCLE_ACTIVE_COLOR: "0xFEC600" +TILE1_CIRCLE_DISABLED_COLOR: "0x7B7B6F" +TILE1_ICON_ACTIVE_COLOR: "0xFFFFFF" +TILE1_ICON_DISABLED_COLOR: "0xFEC600" +TILE1_BG_ACTIVE_COLOR: "0xFFFFFF" +TILE1_BG_DISABLED_COLOR: "0x939391" +TILE1_TITLE_ACTIVE_COLOR: "0x000000" +TILE1_TITLE_DISABLED_COLOR: "0xFFFFFF" +TILE1_VALUE_ACTIVE_COLOR: "0x7A7A7C" +TILE1_VALUE_DISABLED_COLOR: "0xD9D9D9" +# advanced +TILE1_TAP_SERVICE: "" +TILE1_TAP_PARAM_KEY: "" +TILE1_TAP_PARAM_VAL: "" +TILE1_LONGPRESS_SLIDER: "auto" # auto = brightness for lights +TILE1_LONGPRESS_OFF_VALUE: "0" +TILE1_LONGPRESS_ACTION: "" +TILE1_LONGPRESS_ACTION_TYPE: "" +TILE1_LONGPRESS_ACTION_SERVICE: "" +``` + +--- + +### Switch — simple toggle, no slider + +```yaml +TILE2_ENTITY: "switch.office_power_strip" +TILE2_STATE_ENTITY: "switch.office_power_strip" +TILE2_TITLE: "Power Strip" +TILE2_ICON: "\U000F06FF" # mdi:power-socket-eu +TILE2_TYPE: "switch" +TILE2_TAP_ACTION: "auto" # toggles the switch +TILE2_LONGPRESS: "none" # long press only sends event, no slider +TILE2_VALUE_MODE: "text" # shows "On" / "Off" +TILE2_LABEL_OFF: "Off" +TILE2_LABEL_ON: "On" +# colors +TILE2_CIRCLE_ACTIVE_COLOR: "0xFEC600" +TILE2_CIRCLE_DISABLED_COLOR: "0x7B7B6F" +TILE2_ICON_ACTIVE_COLOR: "0xFFFFFF" +TILE2_ICON_DISABLED_COLOR: "0xFEC600" +TILE2_BG_ACTIVE_COLOR: "0xFFFFFF" +TILE2_BG_DISABLED_COLOR: "0x939391" +TILE2_TITLE_ACTIVE_COLOR: "0x000000" +TILE2_TITLE_DISABLED_COLOR: "0xFFFFFF" +TILE2_VALUE_ACTIVE_COLOR: "0x7A7A7C" +TILE2_VALUE_DISABLED_COLOR: "0xD9D9D9" +# advanced +TILE2_TAP_SERVICE: "" +TILE2_TAP_PARAM_KEY: "" +TILE2_TAP_PARAM_VAL: "" +TILE2_LONGPRESS_SLIDER: "auto" +TILE2_LONGPRESS_OFF_VALUE: "0" +TILE2_LONGPRESS_ACTION: "" +TILE2_LONGPRESS_ACTION_TYPE: "" +TILE2_LONGPRESS_ACTION_SERVICE: "" +``` + +--- + +### Cover — open/close toggle + position slider + +```yaml +TILE3_ENTITY: "cover.living_room_blinds" +TILE3_STATE_ENTITY: "cover.living_room_blinds" +TILE3_TITLE: "Blinds" +TILE3_ICON: "\U000F111C" # mdi:blinds +TILE3_TYPE: "cover" +TILE3_TAP_ACTION: "auto" # opens if closed, closes if open +TILE3_LONGPRESS: "slider" # long press opens position slider +TILE3_VALUE_MODE: "auto" # shows "Open" / "Closed" (text mode for covers) +TILE3_LABEL_OFF: "Closed" +TILE3_LABEL_ON: "Open" +# colors — blue theme for covers +TILE3_CIRCLE_ACTIVE_COLOR: "0x00C5EC" +TILE3_CIRCLE_DISABLED_COLOR: "0x7B7B6F" +TILE3_ICON_ACTIVE_COLOR: "0xFFFFFF" +TILE3_ICON_DISABLED_COLOR: "0x00C5EC" +TILE3_BG_ACTIVE_COLOR: "0xFFFFFF" +TILE3_BG_DISABLED_COLOR: "0x939391" +TILE3_TITLE_ACTIVE_COLOR: "0x000000" +TILE3_TITLE_DISABLED_COLOR: "0xFFFFFF" +TILE3_VALUE_ACTIVE_COLOR: "0x7A7A7C" +TILE3_VALUE_DISABLED_COLOR: "0xD9D9D9" +# advanced +TILE3_TAP_SERVICE: "" +TILE3_TAP_PARAM_KEY: "" +TILE3_TAP_PARAM_VAL: "" +TILE3_LONGPRESS_SLIDER: "auto" # auto = cover_position for covers +TILE3_LONGPRESS_OFF_VALUE: "0" +TILE3_LONGPRESS_ACTION: "" +TILE3_LONGPRESS_ACTION_TYPE: "" +TILE3_LONGPRESS_ACTION_SERVICE: "" +``` + +--- + +### Fan — toggle preset on tap + speed slider on long press + +```yaml +TILE4_ENTITY: "fan.air_purifier" +TILE4_STATE_ENTITY: "fan.air_purifier" +TILE4_TITLE: "Purifier" +TILE4_ICON: "\U000F0D43" # mdi:air-purifier +TILE4_TYPE: "fan" +TILE4_TAP_ACTION: "fan_toggle_preset" # off→on with preset, on→off +TILE4_LONGPRESS: "slider" # long press opens speed slider +TILE4_VALUE_MODE: "auto" # shows preset name when on, "Off" when off +TILE4_LABEL_OFF: "Off" +TILE4_LABEL_ON: "On" +# colors — blue theme for fan +TILE4_CIRCLE_ACTIVE_COLOR: "0x00C5EC" +TILE4_CIRCLE_DISABLED_COLOR: "0x7B7B6F" +TILE4_ICON_ACTIVE_COLOR: "0xFFFFFF" +TILE4_ICON_DISABLED_COLOR: "0x00C5EC" +TILE4_BG_ACTIVE_COLOR: "0xFFFFFF" +TILE4_BG_DISABLED_COLOR: "0x939391" +TILE4_TITLE_ACTIVE_COLOR: "0x000000" +TILE4_TITLE_DISABLED_COLOR: "0xFFFFFF" +TILE4_VALUE_ACTIVE_COLOR: "0x7A7A7C" +TILE4_VALUE_DISABLED_COLOR: "0xD9D9D9" +# advanced — fan_toggle_preset requires SERVICE + PARAM +TILE4_TAP_SERVICE: "fan.toggle" +TILE4_TAP_PARAM_KEY: "preset_mode" +TILE4_TAP_PARAM_VAL: "Auto" # preset to activate when turning on +TILE4_LONGPRESS_SLIDER: "auto" # auto = percentage for fans +TILE4_LONGPRESS_OFF_VALUE: "0" +TILE4_LONGPRESS_ACTION: "" +TILE4_LONGPRESS_ACTION_TYPE: "" +TILE4_LONGPRESS_ACTION_SERVICE: "" +``` + +--- + +### Climate — thermostat ring on long press + +```yaml +TILE5_ENTITY: "climate.living_room" +TILE5_STATE_ENTITY: "climate.living_room" +TILE5_TITLE: "Living" +TILE5_ICON: "\U000F0438" # mdi:radiator +TILE5_TYPE: "climate" +TILE5_TAP_ACTION: "auto" # no default tap action; use TAP_SERVICE if desired +TILE5_LONGPRESS: "slider" # long press opens thermostat ring +TILE5_VALUE_MODE: "auto" # shows target temperature, or current temperature as fallback +TILE5_LABEL_OFF: "Off" +TILE5_LABEL_ON: "Heat" +# colors +TILE5_CIRCLE_ACTIVE_COLOR: "0xFF9F0A" +TILE5_CIRCLE_DISABLED_COLOR: "0x7B7B6F" +TILE5_ICON_ACTIVE_COLOR: "0xFFFFFF" +TILE5_ICON_DISABLED_COLOR: "0xFF9F0A" +TILE5_BG_ACTIVE_COLOR: "0xFFFFFF" +TILE5_BG_DISABLED_COLOR: "0x939391" +TILE5_TITLE_ACTIVE_COLOR: "0x000000" +TILE5_TITLE_DISABLED_COLOR: "0xFFFFFF" +TILE5_VALUE_ACTIVE_COLOR: "0x7A7A7C" +TILE5_VALUE_DISABLED_COLOR: "0xD9D9D9" +# advanced +TILE5_TAP_SERVICE: "" +TILE5_TAP_PARAM_KEY: "" +TILE5_TAP_PARAM_VAL: "" +TILE5_LONGPRESS_SLIDER: "auto" # auto = climate_temperature for climate tiles +TILE5_LONGPRESS_OFF_VALUE: "0" +TILE5_LONGPRESS_ACTION: "" +TILE5_LONGPRESS_ACTION_TYPE: "" +TILE5_LONGPRESS_ACTION_SERVICE: "" +TILE5_CLIMATE_MIN_TEMP: "5.0" +TILE5_CLIMATE_MAX_TEMP: "30.0" +TILE5_CLIMATE_STEP: "0.5" +TILE5_CLIMATE_CURRENT_TEMP_ATTRIBUTE: "current_temperature" +TILE5_CLIMATE_TARGET_TEMP_ATTRIBUTE: "temperature" +TILE5_CLIMATE_HUMIDITY_ATTRIBUTE: "current_humidity" +``` + +--- + +### Scene — activate on tap + +```yaml +TILE6_ENTITY: "scene.movie_night" +TILE6_STATE_ENTITY: "scene.movie_night" # scenes are always "on" so the tile stays active-styled +TILE6_TITLE: "Movie" +TILE6_ICON: "\U000F0E8B" # mdi:movie-open +TILE6_TYPE: "scene" +TILE6_TAP_ACTION: "auto" # activates the scene +TILE6_LONGPRESS: "none" +TILE6_VALUE_MODE: "text" # always shows LABEL_ON for scenes +TILE6_LABEL_OFF: "Scene" +TILE6_LABEL_ON: "Scene" +# colors — green theme +TILE6_CIRCLE_ACTIVE_COLOR: "0x4CAF50" +TILE6_CIRCLE_DISABLED_COLOR: "0x7B7B6F" +TILE6_ICON_ACTIVE_COLOR: "0xFFFFFF" +TILE6_ICON_DISABLED_COLOR: "0x4CAF50" +TILE6_BG_ACTIVE_COLOR: "0xFFFFFF" +TILE6_BG_DISABLED_COLOR: "0x939391" +TILE6_TITLE_ACTIVE_COLOR: "0x000000" +TILE6_TITLE_DISABLED_COLOR: "0xFFFFFF" +TILE6_VALUE_ACTIVE_COLOR: "0x7A7A7C" +TILE6_VALUE_DISABLED_COLOR: "0xD9D9D9" +# advanced +TILE6_TAP_SERVICE: "" +TILE6_TAP_PARAM_KEY: "" +TILE6_TAP_PARAM_VAL: "" +TILE6_LONGPRESS_SLIDER: "auto" +TILE6_LONGPRESS_OFF_VALUE: "0" +TILE6_LONGPRESS_ACTION: "" +TILE6_LONGPRESS_ACTION_TYPE: "" +TILE6_LONGPRESS_ACTION_SERVICE: "" +``` + +--- + +### Script — run on tap + +```yaml +TILE6_ENTITY: "script.good_morning" +TILE6_STATE_ENTITY: "script.good_morning" +TILE6_TITLE: "Morning" +TILE6_ICON: "\U000F0A72" # mdi:weather-sunny +TILE6_TYPE: "script" +TILE6_TAP_ACTION: "auto" # runs the script +TILE6_LONGPRESS: "none" +TILE6_VALUE_MODE: "text" +TILE6_LABEL_OFF: "Run" +TILE6_LABEL_ON: "Run" +# colors — orange theme +TILE6_CIRCLE_ACTIVE_COLOR: "0xFF6F00" +TILE6_CIRCLE_DISABLED_COLOR: "0x7B7B6F" +TILE6_ICON_ACTIVE_COLOR: "0xFFFFFF" +TILE6_ICON_DISABLED_COLOR: "0xFF6F00" +TILE6_BG_ACTIVE_COLOR: "0xFFFFFF" +TILE6_BG_DISABLED_COLOR: "0x939391" +TILE6_TITLE_ACTIVE_COLOR: "0x000000" +TILE6_TITLE_DISABLED_COLOR: "0xFFFFFF" +TILE6_VALUE_ACTIVE_COLOR: "0x7A7A7C" +TILE6_VALUE_DISABLED_COLOR: "0xD9D9D9" +# advanced +TILE6_TAP_SERVICE: "" +TILE6_TAP_PARAM_KEY: "" +TILE6_TAP_PARAM_VAL: "" +TILE6_LONGPRESS_SLIDER: "auto" +TILE6_LONGPRESS_OFF_VALUE: "0" +TILE6_LONGPRESS_ACTION: "" +TILE6_LONGPRESS_ACTION_TYPE: "" +TILE6_LONGPRESS_ACTION_SERVICE: "" +``` + +--- + +### Long press — call a different entity + +Short tap toggles a light; long press activates a scene. + +```yaml +TILE1_ENTITY: "light.living_room" +TILE1_TYPE: "light" +TILE1_TAP_ACTION: "auto" # short tap: toggle the light +TILE1_LONGPRESS: "action" # long press: fire a different entity +# ... +TILE1_LONGPRESS_ACTION: "scene.movie_night" +TILE1_LONGPRESS_ACTION_TYPE: "scene" +TILE1_LONGPRESS_ACTION_SERVICE: "" # empty = default activate for scene +``` + +Short tap toggles a light; long press also toggles a different light. + +```yaml +TILE1_LONGPRESS: "action" +TILE1_LONGPRESS_ACTION: "light.bedroom" +TILE1_LONGPRESS_ACTION_TYPE: "" # empty = inherits "light" from TILE1_TYPE +TILE1_LONGPRESS_ACTION_SERVICE: "light.toggle" # must set toggle explicitly, default is turn_on +``` + +--- + +## Adding a tile + +Slots 9 and 10 are free: `GRID_ROW5_Y` is already defined in every orientation +preset. A tile is not a single block — it is six places in `home-like.yaml`. +Copy tile 7 (a plain light) or tile 8 (a service call) and change the number: + +1. **Substitutions** — a `TILE9_*` block. Copy an existing one wholesale. +2. **Font glyph** — add `MDI_GLYPH_9` and list it under `materialdesign_icons`. + A glyph that is not listed renders as blank space. +3. **Sensors** — under `sensor:` / `text_sensor:`. `ha_state_tile9` is the only + required one. Add the rest only if the tile actually uses them: + `tile9_brightness` (light), `tile9_percentage` + `tile9_preset` (fan), + `tile9_cover_position` + `tile9_cover_state` (cover), + `tile9_climate_*` (climate), `tile9_supported_color_modes` (color light). + Every subscription costs heap, so skip what the tile does not read. +4. **Widget** — an `obj` under `tile_scroll`'s `widgets:`, at + `x: ${GRID_COL1_X}` / `y: ${GRID_ROW5_Y}`. Pass `NAN` (floats) or `""` + (strings) to `open_value_overlay` for any sensor you skipped in step 3. +5. **Ack script** — a `tile9_ack` copy under `script:`. +6. **Refresh lambda** — three lines in `ui_refresh`: a `t9_on` line next to the + others, a `set_value_auto(id(t9_value), …)` call, and an `apply_tile(t9_on, …)` + call. Climate tiles also need a `sync_open_climate` line. + +Watch `sensor.*_heap_free` in Home Assistant after flashing. This board has no +PSRAM, so free heap is the real ceiling on how many tiles fit. + +--- + +## Finding Material Design Icons + +1. Go to [pictogrammers.com/library/mdi](https://pictogrammers.com/library/mdi/) +2. Search for an icon name +3. Click the icon → copy the Unicode codepoint (e.g. `F0769`) +4. Format it as `"\U000F0769"` (always 8 hex digits, padded with leading zeros) + +Make sure the glyph is listed in the `glyphs:` section of the MDI font definition in the YAML (search for `materialdesign_icons`). If you use an icon that is not listed there, it will show as a blank space. diff --git a/buttons.yaml b/buttons.yaml new file mode 100644 index 0000000..a9e34b1 --- /dev/null +++ b/buttons.yaml @@ -0,0 +1,618 @@ +############################################################ +# SmartDisplay (Cheap Yellow Display / CYD) – ESPHome + LVGL lockscreen UI +# +# Target hardware +# - Cheap Yellow Display (CYD) +# (ESP32 + ILI9341 320x240 display + XPT2046 touchscreen) +# +# Purpose +# - LVGL renders a simple “lockscreen” UI: time/date + 4 toggle buttons. +# +# Touch input → Home Assistant +# - Every touch publishes an “action” string via a text_sensor: +# sensor.smartdisplay_action = "btn1_press" / "btn2_press" / "btn3_press" / "btn4_press" +# - The sensor is auto-reset to "" after each press so repeated presses trigger reliably. +# +# Two supported control modes (choose what fits your setup) +# 1) Direct control (DEFAULT, simplest) +# - DIRECT_ACTIONS: "true" (default) +# - Each touch will: +# • publish the action string AND +# • directly call a Home Assistant service +# (default: light.toggle on BTN*_ENTITY) +# - Use this if you simply want to toggle lights without creating automations. +# +# 2) Automation-only control (custom logic) +# - DIRECT_ACTIONS: "false" +# - Each touch will ONLY publish the action string. +# - You implement the behavior in Home Assistant automations using: +# Trigger → State → sensor.smartdisplay_action → To: btn1_press (etc.) +# +# If you do NOT want to toggle lights +# - Either change the homeassistant.action blocks to a different service +# (e.g. switch.toggle, script.turn_on, scene.turn_on, ...) +# - Or set DIRECT_ACTIONS to "false" and handle everything via HA automations. +# +# UI updates +# - On boot, every minute, and whenever the configured HA entities change +# (mirrored into ESPHome to keep LVGL button states in sync). +# +# What you must adapt before flashing +# 1) Secrets: api.encryption.key, ota.password, WiFi credentials (prefer secrets.yaml) +# 2) BTN*_ENTITY: Home Assistant entity IDs you want to control/mirror +# 3) Touch calibration + transform: +# - calibration is panel-specific +# - touchscreen.transform MUST match display.transform exactly +# +# Home Assistant note +# - In the ESPHome device settings, enable: +# “Allow the device to perform Home Assistant actions” +# Otherwise homeassistant.action calls are blocked. +############################################################ + +# ========================================================== +# USER CONFIG (edit this section to match your HA setup) +# +# - DIRECT_ACTIONS: +# "true" -> touch publishes action + calls HA service (light.toggle by default) +# "false" -> touch only publishes action (all logic handled in HA) +# +# - BTN*_ENTITY: +# The Home Assistant entity to control and mirror for the button state. +# Must exist in Home Assistant. +# +# - BTN*_LABEL / BTN*_ICON: +# Only affects what is shown on the display. +# ========================================================== +substitutions: + # Enable/disable direct Home Assistant service calls on touch. + # - "true" -> touching a button also calls light.toggle (plug & play) + # - "false" -> touching a button ONLY publishes the action text_sensor + DIRECT_ACTIONS: "true" + + # Button 1 + BTN1_ENTITY: "light.desk_lamp" + BTN1_LABEL: "Desk" + BTN1_ICON: "\U000F095F" + + # Button 2 + BTN2_ENTITY: "light.ceiling_light" + BTN2_LABEL: "Ceiling" + BTN2_ICON: "\U000F0769" + + # Button 3 + BTN3_ENTITY: "light.globe_lamp" + BTN3_LABEL: "Globe" + BTN3_ICON: "\U000F08DD" + + # Button 4 + BTN4_ENTITY: "light.ambient_light" + BTN4_LABEL: "Ambient" + BTN4_ICON: "\U000F1051" +# ========================================================== + +# ============================================================ +# 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). +# ============================================================ + +esphome: + name: smartdisplay + friendly_name: SmartDisplay + on_boot: + priority: 600 + then: + - script.execute: ui_refresh + +esp32: + board: esp32dev + framework: + type: arduino + +logger: + +# Credentials are loaded from secrets.yaml — copy secrets.yaml.example to secrets.yaml and fill in your values. +api: + encryption: + key: !secret smartdisplay_api_key + +ota: + - platform: esphome + password: !secret ota_password + +wifi: + ssid: !secret wifi_ssid + password: !secret wifi_password + ap: + ssid: "Smartdisplay Fallback Hotspot" + password: !secret wifi_ap_password + +captive_portal: + +# Use Home Assistant time so the display always matches your HA timezone. +time: + - platform: homeassistant + id: homeassistant_time + on_time: + - seconds: 0 + minutes: /1 + then: + - script.execute: ui_refresh + +font: + - file: "gfonts://Roboto@500" + id: big_text + size: 60 + 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: "gfonts://Roboto@300" + id: notice_text + size: 16 + 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: "gfonts://Roboto@400" + id: label + size: 13 + bpp: 4 + glyphs: ['&','@','!',',','.','?','"','%','(',')','+','-','_',':','°','0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z',' ','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','å','Ä','ä','Ö','ö','Ü','ü','/'] + + # Material Design Icons (MDI) — included at fonts/materialdesignicons-webfont.ttf (Apache 2.0) + # Source: https://github.com/Templarian/MaterialDesign-Webfont + - file: 'fonts/materialdesignicons-webfont.ttf' + id: materialdesign_icons + size: 35 + glyphs: ["\U000F095F", "\U000F0769", "\U000F08DD", "\U000F1051"] + +color: + - id: ACTIVE + hex: "FEC600" + - id: INACTIVE + hex: "808080" + +sensor: + - platform: wifi_signal + name: "Wifi Signal" + update_interval: 600s + + - platform: uptime + name: "Uptime" + id: uptime_s + update_interval: 15s + +binary_sensor: + - platform: status + name: "Node Status" + id: system_status + + # Mirror HA entities (used to render the "checked" state of each LVGL button) + # These are read-only "views" of HA state for UI sync. + - platform: homeassistant + id: ha_state_btn1 + entity_id: ${BTN1_ENTITY} + on_state: + then: + - script.execute: ui_refresh + + - platform: homeassistant + id: ha_state_btn2 + entity_id: ${BTN2_ENTITY} + on_state: + then: + - script.execute: ui_refresh + + - platform: homeassistant + id: ha_state_btn3 + entity_id: ${BTN3_ENTITY} + on_state: + then: + - script.execute: ui_refresh + + - platform: homeassistant + id: ha_state_btn4 + entity_id: ${BTN4_ENTITY} + on_state: + then: + - script.execute: ui_refresh + +text_sensor: + # ---------------------------------------------------------- + # SmartDisplay Action + # + # This is the main "touch trigger" exported to Home Assistant: + # - On touch, we publish one of: + # btn1_press / btn2_press / btn3_press / btn4_press + # - Then we auto-reset to "" so repeated presses still trigger. + # + # In HA automations, use a simple state trigger: + # - Entity: sensor.smartdisplay_action + # - To: btn1_press (etc.) + # ---------------------------------------------------------- + - platform: template + id: smartdisplay_action + name: "SmartDisplay Action" + icon: mdi:gesture-tap-button + + - platform: template + name: "Uptime (formatted)" + lambda: |- + uint32_t dur = (uint32_t) id(uptime_s).state; + + int dys = 0; + int hrs = 0; + int mnts = 0; + + if (dur >= 86400) { dys = dur / 86400; dur = dur - (dys * 86400); } + if (dur >= 3600) { hrs = dur / 3600; dur = dur - (hrs * 3600); } + if (dur >= 60) { mnts = dur / 60; dur = dur - (mnts * 60); } + + static char buffer[20]; + snprintf(buffer, sizeof(buffer), "%dd %02dh %02dm %02ds", dys, hrs, mnts, dur); + return {buffer}; + icon: mdi:clock-start + update_interval: 60s + +switch: + - platform: restart + name: "Restart" + +# Two SPI buses: +# - lcd: display +# - my_touchscreen: touch controller +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 + + - 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) +# IMPORTANT: +# - calibration values are device-specific +# - touchscreen.transform must match display.transform exactly +touchscreen: + platform: xpt2046 + id: ts_touch + spi_id: my_touchscreen + cs_pin: 33 + interrupt_pin: 36 + update_interval: 50ms + threshold: 400 + + calibration: + x_min: 280 + x_max: 3860 + y_min: 340 + y_max: 3860 + + transform: + swap_xy: true + mirror_x: false + mirror_y: false + +# Display configuration (ILI9341) +display: + - platform: mipi_spi + id: my_display + spi_id: lcd + model: ILI9341 + cs_pin: 15 + dc_pin: 2 + invert_colors: false + update_interval: never + auto_clear_enabled: false + + transform: + swap_xy: true + mirror_x: false + mirror_y: false + + dimensions: + width: 320 + height: 240 + +lvgl: + displays: + - my_display + touchscreens: + - touchscreen_id: ts_touch + + theme: + obj: + bg_color: 0x000000 + bg_opa: COVER + button: + radius: 30 + width: 60 + height: 60 + border_width: 0 + shadow_width: 0 + bg_color: !lambda return id(INACTIVE); + bg_opa: COVER + checked: + bg_color: 0xFFFFFF + bg_opa: COVER + + pages: + - id: lockscreen_page + bg_color: 0x000000 + bg_opa: COVER + widgets: + - label: + id: lbl_date + x: 0 + y: 30 + width: 320 + text_align: CENTER + text_font: notice_text + text_color: 0xFFFFFF + text: "" + + - label: + id: lbl_time + x: 0 + y: 55 + width: 320 + text_align: CENTER + text_font: big_text + text_color: 0xFFFFFF + text: "" + + # --------------------------------------------------- + # LVGL Button 1 + # - ALWAYS publishes a HA-friendly action: btn1_press + # - Optional direct action: light.toggle for BTN1_ENTITY + # --------------------------------------------------- + - button: + id: btn1 + x: 25 + y: 140 + checkable: true + widgets: + - label: + align: CENTER + text_font: materialdesign_icons + text_color: !lambda return id(ACTIVE); + text: "${BTN1_ICON}" + on_click: + # ALWAYS: publish action to Home Assistant (state trigger friendly) + - script.execute: + id: publish_action + action: "btn1_press" + + # OPTIONAL: direct HA service call (plug & play) + - if: + condition: + lambda: |- + return std::string("${DIRECT_ACTIONS}") == "true"; + then: + - homeassistant.action: + action: light.toggle + data: + entity_id: ${BTN1_ENTITY} + + - label: + x: 25 + y: 205 + width: 60 + text_align: CENTER + text_font: label + text_color: 0xFFFFFF + text: "${BTN1_LABEL}" + + # --------------------------------------------------- + # LVGL Button 2 + # - ALWAYS publishes a HA-friendly action: btn2_press + # --------------------------------------------------- + - button: + id: btn2 + x: 95 + y: 140 + checkable: true + widgets: + - label: + align: CENTER + text_font: materialdesign_icons + text_color: !lambda return id(ACTIVE); + text: "${BTN2_ICON}" + on_click: + - script.execute: + id: publish_action + action: "btn2_press" + - if: + condition: + lambda: |- + return std::string("${DIRECT_ACTIONS}") == "true"; + then: + - homeassistant.action: + action: light.toggle + data: + entity_id: ${BTN2_ENTITY} + + - label: + x: 95 + y: 205 + width: 60 + text_align: CENTER + text_font: label + text_color: 0xFFFFFF + text: "${BTN2_LABEL}" + + # --------------------------------------------------- + # LVGL Button 3 + # - ALWAYS publishes a HA-friendly action: btn3_press + # --------------------------------------------------- + - button: + id: btn3 + x: 165 + y: 140 + checkable: true + widgets: + - label: + align: CENTER + text_font: materialdesign_icons + text_color: !lambda return id(ACTIVE); + text: "${BTN3_ICON}" + on_click: + - script.execute: + id: publish_action + action: "btn3_press" + - if: + condition: + lambda: |- + return std::string("${DIRECT_ACTIONS}") == "true"; + then: + - homeassistant.action: + action: light.toggle + data: + entity_id: ${BTN3_ENTITY} + + - label: + x: 165 + y: 205 + width: 60 + text_align: CENTER + text_font: label + text_color: 0xFFFFFF + text: "${BTN3_LABEL}" + + # --------------------------------------------------- + # LVGL Button 4 + # - ALWAYS publishes a HA-friendly action: btn4_press + # --------------------------------------------------- + - button: + id: btn4 + x: 235 + y: 140 + checkable: true + widgets: + - label: + align: CENTER + text_font: materialdesign_icons + text_color: !lambda return id(ACTIVE); + text: "${BTN4_ICON}" + on_click: + - script.execute: + id: publish_action + action: "btn4_press" + - if: + condition: + lambda: |- + return std::string("${DIRECT_ACTIONS}") == "true"; + then: + - homeassistant.action: + action: light.toggle + data: + entity_id: ${BTN4_ENTITY} + + - label: + x: 235 + y: 205 + width: 60 + text_align: CENTER + text_font: label + text_color: 0xFFFFFF + text: "${BTN4_LABEL}" + +script: + # ---------------------------------------------------------- + # publish_action + # + # Publishes a string to the "SmartDisplay Action" text_sensor and + # then clears it again. + # ---------------------------------------------------------- + - id: publish_action + mode: queued + parameters: + action: string + then: + - text_sensor.template.publish: + id: smartdisplay_action + state: !lambda |- + return action; + - delay: 200ms + - text_sensor.template.publish: + id: smartdisplay_action + state: "" + + - id: ui_refresh + mode: queued + then: + # Sync LVGL "checked" state with the mirrored HA entity states + - lvgl.widget.update: + id: btn1 + state: + checked: !lambda return id(ha_state_btn1).state; + + - lvgl.widget.update: + id: btn2 + state: + checked: !lambda return id(ha_state_btn2).state; + + - lvgl.widget.update: + id: btn3 + state: + checked: !lambda return id(ha_state_btn3).state; + + - lvgl.widget.update: + id: btn4 + state: + checked: !lambda return id(ha_state_btn4).state; + + # Render current time (HH:MM) + - lvgl.label.update: + id: lbl_time + text: !lambda |- + auto now = id(homeassistant_time).now(); + static char buf[6]; + snprintf(buf, sizeof(buf), "%02d:%02d", now.hour, now.minute); + return std::string(buf); + + # Render localized date in German (adjust to your language if needed) + - lvgl.label.update: + id: lbl_date + text: !lambda |- + static const char* days[] = {"", "Sonntag","Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag"}; + static const char* months[] = {"","Januar","Februar","März","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"}; + auto now = id(homeassistant_time).now(); + static char buf[40]; + snprintf(buf, sizeof(buf), "%s, %d. %s", days[now.day_of_week], now.day_of_month, months[now.month]); + return std::string(buf); \ No newline at end of file diff --git a/fonts/materialdesignicons-webfont.ttf b/fonts/materialdesignicons-webfont.ttf new file mode 100644 index 0000000..bba7dcf Binary files /dev/null and b/fonts/materialdesignicons-webfont.ttf differ diff --git a/home-like.yaml b/home-like.yaml new file mode 100644 index 0000000..4fd5949 --- /dev/null +++ b/home-like.yaml @@ -0,0 +1,5571 @@ +############################################################ +# 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 wallpaper background + a 2x3 grid of “tiles” (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) Edit the TILE1..TILE6 blocks below. +# 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) Provide background images (see ORIENTATION section for which file each preset uses): +# - images/smartdisplay_background.png (0° and 180°) +# - images/smartdisplay_background_90.png (90° and 270°) +# +# 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 +# +# What you must adapt before flashing +# 1) Secrets: api.encryption.key, ota.password, WiFi credentials (prefer 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: + + DIRECT_ACTIONS: "true" # true = call Home Assistant services directly, false = only emit action events + ROOM_NAME: "Woonkamer" # 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: "10" # 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 + + # ============================================================ + # 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: "\U000F04B9" + 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 + + # ============================================================ + # TILE 1 + # ============================================================ + + # --- basic settings --- + TILE1_ENTITY: "climate.airco_remeha_diva_ac" # main Home Assistant entity for this tile + TILE1_STATE_ENTITY: "climate.airco_remeha_diva_ac" # entity used for active/inactive state styling + TILE1_TITLE: "Airco" # tile title shown in the UI + TILE1_ICON: "\U000F001B" # MDI glyph — also add to MDI_GLYPH_* list above if not already there + TILE1_TYPE: "climate" # tile type: light | fan | switch | scene | script | cover | climate + TILE1_TAP_ACTION: "toggle" # 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: "0x00C5EC" # 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: "0x00C5EC" # 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: "climate.toggle" # 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: "light.woonkamer_2" # main Home Assistant entity for this tile + TILE2_STATE_ENTITY: "light.woonkamer_2" # entity used for active/inactive state styling + TILE2_TITLE: "Woonkamer" # tile title shown in the UI + TILE2_ICON: "\U000F04B9" # MDI glyph — also add to MDI_GLYPH_* list above if not already there + TILE2_TYPE: "light" # tile type: light | fan | switch | scene | script | cover | climate + TILE2_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset | custom + TILE2_LONGPRESS: "slider" # 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: "0xFEC600" # 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: "0xFEC600" # 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: "script.1692558675845" # main Home Assistant entity for this tile + TILE3_STATE_ENTITY: "script.1692558675845" # entity used for active/inactive state styling + TILE3_TITLE: "Naar bed" # tile title shown in the UI + TILE3_ICON: "\U000F02E3" # MDI glyph — also add to MDI_GLYPH_* list above if not already there + TILE3_TYPE: "script" # 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: "text" # value mode: auto | brightness | percentage | preset | text + TILE3_LABEL_OFF: "Start" # text shown when entity is off + TILE3_LABEL_ON: "Start" # 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: "0xFEC600" # 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: "0xFEC600" # 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.goede_morgen" # main Home Assistant entity for this tile + TILE4_STATE_ENTITY: "script.goede_morgen" # entity used for active/inactive state styling + TILE4_TITLE: "Goede morgen" # 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: "text" # value mode: auto | brightness | percentage | preset | text + TILE4_LABEL_OFF: "Start" # text shown when entity is off + TILE4_LABEL_ON: "Start" # 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: "0xFEC600" # 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: "0xFEC600" # 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: "script.1692529759669" # main Home Assistant entity for this tile + TILE5_STATE_ENTITY: "script.1692529759669" # entity used for active/inactive state styling + TILE5_TITLE: "Alles uit" # tile title shown in the UI + TILE5_ICON: "\U000F0425" # MDI glyph — also add to MDI_GLYPH_* list above if not already there + TILE5_TYPE: "script" # tile type: light | fan | switch | scene | script | cover | climate + TILE5_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset | custom + TILE5_LONGPRESS: "none" # long press mode: none | slider | action + TILE5_VALUE_MODE: "text" # value mode: auto | brightness | percentage | preset | text + TILE5_LABEL_OFF: "Start" # text shown when entity is off (closed) + TILE5_LABEL_ON: "Start" # 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: "0xFEC600" # 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: "0xFEC600" # 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: "" # 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.s8" # main Home Assistant entity for this tile + TILE6_STATE_ENTITY: "binary_sensor.s8_schoonmaken" # entity used for active/inactive state styling + TILE6_TITLE: "Pippa" # tile title shown in the UI + TILE6_ICON: "\U000F070D" # MDI glyph — also add to MDI_GLYPH_* list above if not already there + TILE6_TYPE: "switch" # tile type: light | fan | switch | scene | script | cover | climate + TILE6_TAP_ACTION: "toggle" # tap action: auto | toggle | activate | fan_toggle_preset | custom + TILE6_LONGPRESS: "vacuum" # long press mode: none | slider | action + TILE6_VALUE_MODE: "text" # value mode: auto | brightness | percentage | preset | text + TILE6_LABEL_OFF: "Gedockt" # text shown when entity is off + TILE6_LABEL_ON: "Bezig" # 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: "0x00C5EC" # 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: "0x00C5EC" # 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: "vacuum.s8" # entity_id to fire on long press + TILE6_LONGPRESS_ACTION_TYPE: "switch" # entity type: light | fan | switch | scene | script | cover | climate (empty = same as TILE*_TYPE) + TILE6_LONGPRESS_ACTION_SERVICE: "vacuum.return_to_base" # 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.tafellamp_licht" # main Home Assistant entity for this tile + TILE7_STATE_ENTITY: "light.tafellamp_licht" # entity used for active/inactive state styling + TILE7_TITLE: "Tafellamp" # 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: "slider" # 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: "0xFEC600" # 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: "0xFEC600" # 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: "input_select.brievenbus_status" # main Home Assistant entity for this tile + TILE8_STATE_ENTITY: "input_select.brievenbus_status" # entity used for active/inactive state styling + TILE8_TITLE: "Brievenbus" # tile title shown in the UI + TILE8_ICON: "\U000F06EE" # mdi:mailbox — also listed in MDI_GLYPH_8 above + TILE8_TYPE: "switch" # generiek type; het echte gedrag komt uit TAP_SERVICE hieronder + TILE8_TAP_ACTION: "activate" # activate + TAP_SERVICE = losse service call + TILE8_LONGPRESS: "none" # long press stuurt alleen het event naar HA + TILE8_VALUE_MODE: "text" # toont LABEL_ON / LABEL_OFF + TILE8_LABEL_OFF: "Leeg" # text shown when entity is off / inactive + TILE8_LABEL_ON: "Vol" # text shown when entity is active + TILE8_ACTIVE_STATE: "Vol" # 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: "input_select.select_option" # zet de input_select op een vaste optie + TILE8_TAP_PARAM_KEY: "option" # parameternaam voor input_select.select_option + TILE8_TAP_PARAM_VAL: "Leeg" # 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 / TILE 10 — nog vrij + # Rij 5 van het raster (GRID_ROW5_Y) is al gedefinieerd en leeg. + # Zie TILE_CONFIGURATION.md, sectie "Een tegel toevoegen" voor de + # zes plekken die je per extra tegel moet invullen. + # ============================================================ + + + # ============================================================ + # 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" + TOUCH_MIRROR_X: "false" + 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" + + # ----- 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 +# ============================================================ + +esphome: + name: smartdisplay + friendly_name: SmartDisplay + on_boot: + priority: 600 + then: + - lambda: |- + id(last_touch_ms) = millis(); + id(display_dimmed) = false; + - light.turn_on: + id: back_light + brightness: !lambda "return atoi(\"${MAX_BRIGHTNESS}\") / 100.0f;" + - script.execute: ui_refresh + +esp32: + board: esp32dev + framework: + type: arduino + advanced: + # ESPHome adviseert deze twee zelf in de boot-log. Samen leveren ze + # ~40KB extra IRAM en een kleinere binary, en op dit bord telt elke KB. + sram1_as_iram: true + # Let op: hiermee draait deze firmware niet meer op ESP32's ouder dan + # rev 3.1. Dit bord is 3.1, dus prima, maar niet blind overzetten. + minimum_chip_revision: "3.1" + 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: INFO + + +# Credentials are loaded from secrets.yaml — copy secrets.yaml.example to secrets.yaml and fill in your values. +api: + encryption: + key: !secret smartdisplay_api_key + +ota: + - platform: esphome + password: !secret ota_password + +wifi: + ssid: !secret wifi_ssid + password: !secret wifi_password + ap: + ssid: "Smartdisplay Fallback Hotspot" + password: !secret wifi_ap_password + +captive_portal: + +interval: + - interval: 1s + then: + - if: + condition: + lambda: |- + if (std::string("${AUTO_DIM}") != "true") return false; + if (id(display_dimmed)) return false; + return (millis() - id(last_touch_ms)) > (atoi("${AUTO_DIM_TIMEOUT}") * 1000UL); + then: + - script.execute: dim_display + +time: + - platform: homeassistant + id: homeassistant_time + on_time: + - seconds: 0 + minutes: /1 + then: + - script.execute: ui_refresh + + - seconds: 0 + minutes: 0 + hours: ${NIGHT_START_HOUR} + then: + - if: + condition: + lambda: 'return id(display_dimmed);' + then: + - script.execute: dim_display + + - seconds: 0 + minutes: 0 + hours: ${NIGHT_END_HOUR} + then: + - if: + condition: + lambda: 'return id(display_dimmed);' + then: + - script.execute: dim_display + +globals: + - 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' + + - 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: "gfonts://Roboto@500" + id: time_label + size: 18 + bpp: 4 + glyphs: ['0','1','2','3','4','5','6','7','8','9',':',' ', 'A', 'M', 'P'] + + - file: "gfonts://Roboto@500" + 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: "gfonts://Roboto@700" + 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: "gfonts://Roboto@400" + 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: "gfonts://Roboto@400" + id: sublabel_big + size: 14 + bpp: 4 + glyphs: ['&','@','!',',','.','?','"','%','(',')','+','-','_',':','°', + '0','1','2','3','4','5','6','7','8','9', + 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z', + ' ','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z', + 'ä','ö','ü','Ä','Ö','Ü','ß','/','…'] + + # Material Design Icons (MDI) — included at fonts/materialdesignicons-webfont.ttf (Apache 2.0) + # Source: https://github.com/Templarian/MaterialDesign-Webfont + - file: 'fonts/materialdesignicons-webfont.ttf' + id: materialdesign_icons + size: 28 + glyphs: + - "${MDI_GLYPH_1}" + - "${MDI_GLYPH_2}" + - "${MDI_GLYPH_3}" + - "${MDI_GLYPH_4}" + - "${MDI_GLYPH_5}" + - "${MDI_GLYPH_6}" + - "${MDI_GLYPH_7}" + - "${MDI_GLYPH_8}" + - "\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 + fragmentation: + name: "Heap Fragmentation" + block: + name: "Heap Largest Block" + - platform: wifi_signal + name: "Wifi Signal" + update_interval: 600s + + - platform: uptime + name: "Uptime" + id: uptime_s + update_interval: 15s + + - platform: homeassistant + id: tile1_brightness + entity_id: ${TILE1_ENTITY} + attribute: brightness + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile1_percentage + entity_id: ${TILE1_ENTITY} + attribute: percentage + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile2_brightness + entity_id: ${TILE2_ENTITY} + attribute: brightness + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile2_percentage + entity_id: ${TILE2_ENTITY} + attribute: percentage + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile3_brightness + entity_id: ${TILE3_ENTITY} + attribute: brightness + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile3_percentage + entity_id: ${TILE3_ENTITY} + attribute: percentage + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile4_brightness + entity_id: ${TILE4_ENTITY} + attribute: brightness + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile4_percentage + entity_id: ${TILE4_ENTITY} + attribute: percentage + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile5_brightness + entity_id: ${TILE5_ENTITY} + attribute: brightness + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile5_percentage + entity_id: ${TILE5_ENTITY} + attribute: percentage + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile6_brightness + entity_id: ${TILE6_ENTITY} + attribute: brightness + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile6_percentage + entity_id: ${TILE6_ENTITY} + attribute: percentage + on_value: { then: [ script.execute: ui_refresh ] } + + # 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: homeassistant + id: tile7_brightness + entity_id: ${TILE7_ENTITY} + attribute: brightness + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile1_cover_position + entity_id: ${TILE1_ENTITY} + attribute: current_position + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile2_cover_position + entity_id: ${TILE2_ENTITY} + attribute: current_position + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile3_cover_position + entity_id: ${TILE3_ENTITY} + attribute: current_position + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile4_cover_position + entity_id: ${TILE4_ENTITY} + attribute: current_position + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile5_cover_position + entity_id: ${TILE5_ENTITY} + attribute: current_position + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile6_cover_position + entity_id: ${TILE6_ENTITY} + attribute: current_position + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile1_climate_current_temperature + entity_id: ${TILE1_ENTITY} + attribute: ${TILE1_CLIMATE_CURRENT_TEMP_ATTRIBUTE} + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile1_climate_target_temperature + entity_id: ${TILE1_ENTITY} + attribute: ${TILE1_CLIMATE_TARGET_TEMP_ATTRIBUTE} + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile1_climate_humidity + entity_id: ${TILE1_ENTITY} + attribute: ${TILE1_CLIMATE_HUMIDITY_ATTRIBUTE} + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile2_climate_current_temperature + entity_id: ${TILE2_ENTITY} + attribute: ${TILE2_CLIMATE_CURRENT_TEMP_ATTRIBUTE} + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile2_climate_target_temperature + entity_id: ${TILE2_ENTITY} + attribute: ${TILE2_CLIMATE_TARGET_TEMP_ATTRIBUTE} + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile2_climate_humidity + entity_id: ${TILE2_ENTITY} + attribute: ${TILE2_CLIMATE_HUMIDITY_ATTRIBUTE} + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile3_climate_current_temperature + entity_id: ${TILE3_ENTITY} + attribute: ${TILE3_CLIMATE_CURRENT_TEMP_ATTRIBUTE} + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile3_climate_target_temperature + entity_id: ${TILE3_ENTITY} + attribute: ${TILE3_CLIMATE_TARGET_TEMP_ATTRIBUTE} + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile3_climate_humidity + entity_id: ${TILE3_ENTITY} + attribute: ${TILE3_CLIMATE_HUMIDITY_ATTRIBUTE} + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile4_climate_current_temperature + entity_id: ${TILE4_ENTITY} + attribute: ${TILE4_CLIMATE_CURRENT_TEMP_ATTRIBUTE} + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile4_climate_target_temperature + entity_id: ${TILE4_ENTITY} + attribute: ${TILE4_CLIMATE_TARGET_TEMP_ATTRIBUTE} + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile4_climate_humidity + entity_id: ${TILE4_ENTITY} + attribute: ${TILE4_CLIMATE_HUMIDITY_ATTRIBUTE} + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile5_climate_current_temperature + entity_id: ${TILE5_ENTITY} + attribute: ${TILE5_CLIMATE_CURRENT_TEMP_ATTRIBUTE} + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile5_climate_target_temperature + entity_id: ${TILE5_ENTITY} + attribute: ${TILE5_CLIMATE_TARGET_TEMP_ATTRIBUTE} + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile5_climate_humidity + entity_id: ${TILE5_ENTITY} + attribute: ${TILE5_CLIMATE_HUMIDITY_ATTRIBUTE} + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile6_climate_current_temperature + entity_id: ${TILE6_ENTITY} + attribute: ${TILE6_CLIMATE_CURRENT_TEMP_ATTRIBUTE} + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile6_climate_target_temperature + entity_id: ${TILE6_ENTITY} + attribute: ${TILE6_CLIMATE_TARGET_TEMP_ATTRIBUTE} + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile6_climate_humidity + entity_id: ${TILE6_ENTITY} + attribute: ${TILE6_CLIMATE_HUMIDITY_ATTRIBUTE} + on_value: { then: [ script.execute: ui_refresh ] } + +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 + id: smartdisplay_action + name: "SmartDisplay Action" + icon: mdi:gesture-tap-button + + - platform: homeassistant + id: ha_state_tile1 + entity_id: ${TILE1_STATE_ENTITY} + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: ha_state_tile2 + entity_id: ${TILE2_STATE_ENTITY} + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: ha_state_tile3 + entity_id: ${TILE3_STATE_ENTITY} + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: ha_state_tile4 + entity_id: ${TILE4_STATE_ENTITY} + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: ha_state_tile5 + entity_id: ${TILE5_STATE_ENTITY} + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: ha_state_tile6 + entity_id: ${TILE6_STATE_ENTITY} + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: ha_state_tile7 + entity_id: ${TILE7_STATE_ENTITY} + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: ha_state_tile8 + entity_id: ${TILE8_STATE_ENTITY} + on_value: { then: [ script.execute: ui_refresh ] } + + # Gebruikt door de slider-overlay om te bepalen of de kleurknop nodig is. + # Deze lamp meldt ['brightness'], dus de knop blijft vanzelf verborgen. + - platform: homeassistant + id: tile7_supported_color_modes + entity_id: ${TILE7_ENTITY} + attribute: supported_color_modes + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile1_preset + entity_id: ${TILE1_ENTITY} + attribute: preset_mode + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile2_preset + entity_id: ${TILE2_ENTITY} + attribute: preset_mode + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile3_preset + entity_id: ${TILE3_ENTITY} + attribute: preset_mode + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile4_preset + entity_id: ${TILE4_ENTITY} + attribute: preset_mode + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile5_preset + entity_id: ${TILE5_ENTITY} + attribute: preset_mode + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile6_preset + entity_id: ${TILE6_ENTITY} + attribute: preset_mode + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile1_supported_color_modes + entity_id: ${TILE1_ENTITY} + attribute: supported_color_modes + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile2_supported_color_modes + entity_id: ${TILE2_ENTITY} + attribute: supported_color_modes + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile3_supported_color_modes + entity_id: ${TILE3_ENTITY} + attribute: supported_color_modes + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile4_supported_color_modes + entity_id: ${TILE4_ENTITY} + attribute: supported_color_modes + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile5_supported_color_modes + entity_id: ${TILE5_ENTITY} + attribute: supported_color_modes + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile6_supported_color_modes + entity_id: ${TILE6_ENTITY} + attribute: supported_color_modes + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile1_climate_mode + entity_id: ${TILE1_ENTITY} + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile2_climate_mode + entity_id: ${TILE2_ENTITY} + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile3_climate_mode + entity_id: ${TILE3_ENTITY} + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile4_climate_mode + entity_id: ${TILE4_ENTITY} + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile5_climate_mode + entity_id: ${TILE5_ENTITY} + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile6_climate_mode + entity_id: ${TILE6_ENTITY} + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile1_climate_supported_modes + entity_id: ${TILE1_ENTITY} + attribute: hvac_modes + + - platform: homeassistant + id: tile2_climate_supported_modes + entity_id: ${TILE2_ENTITY} + attribute: hvac_modes + + - platform: homeassistant + id: tile3_climate_supported_modes + entity_id: ${TILE3_ENTITY} + attribute: hvac_modes + + - platform: homeassistant + id: tile4_climate_supported_modes + entity_id: ${TILE4_ENTITY} + attribute: hvac_modes + + - platform: homeassistant + id: tile5_climate_supported_modes + entity_id: ${TILE5_ENTITY} + attribute: hvac_modes + + - platform: homeassistant + id: tile6_climate_supported_modes + entity_id: ${TILE6_ENTITY} + attribute: hvac_modes + + - platform: homeassistant + id: tile1_cover_state + entity_id: ${TILE1_STATE_ENTITY} + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile2_cover_state + entity_id: ${TILE2_STATE_ENTITY} + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile3_cover_state + entity_id: ${TILE3_STATE_ENTITY} + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile4_cover_state + entity_id: ${TILE4_STATE_ENTITY} + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile5_cover_state + entity_id: ${TILE5_STATE_ENTITY} + on_value: { then: [ script.execute: ui_refresh ] } + + - platform: homeassistant + id: tile6_cover_state + entity_id: ${TILE6_STATE_ENTITY} + on_value: { then: [ script.execute: ui_refresh ] } + +switch: + - platform: restart + name: "Restart" + +# Two SPI buses used by the CYD: +# - lcd: built-in display +# - my_touchscreen: built-in touch controller +spi: + - id: lcd + clk_pin: GPIO14 + mosi_pin: GPIO13 + + +# I2C bus voor de GT911 capacitieve touch (2432S032C). +# scan: true laat ESPHome bij het booten loggen welke adressen er echt zitten. +i2c: + - id: touch_i2c + sda: GPIO33 + scl: GPIO32 + scan: true + +output: + - platform: ledc + pin: GPIO27 + id: gpio_backlight_pwm + + - 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 (GT911, capacitief, via I2C) +# De GT911 levert absolute coordinaten, dus geen calibration nodig. +# LVGL regelt de rotatie (zie `rotation:` in het lvgl blok) en draait +# touch-coordinaten automatisch mee. +touchscreen: + platform: gt911 + id: ts_touch + i2c_id: touch_i2c + # interrupt_pin bewust weggelaten: met een hardware-interrupt zet ESPHome + # het pollen uit, en dan komt er niets binnen. Nu pollt hij elke 50ms. + update_interval: 50ms + + transform: + swap_xy: ${TOUCH_SWAP_XY} + mirror_x: ${TOUCH_MIRROR_X} + mirror_y: ${TOUCH_MIRROR_Y} + + on_touch: + - lambda: |- + id(last_touch_ms) = millis(); + +# Display configuration (ST7789V) +display: + - platform: mipi_spi + id: my_display + spi_id: lcd + model: ST7789V + # Zonder dit draait mipi_spi op zijn standaard van 10MHz, en dan duurt een + # volledig scherm ~123ms: dat zie je van boven naar beneden vullen. + # ESPHome's eigen CYD-modellen gebruiken 40MHz. 80MHz kan meestal ook. + data_rate: 40MHz + cs_pin: 15 + dc_pin: 2 + invert_colors: true + 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: + 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: 95% + 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: 95% + 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: 95% + 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 + 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 + + # ---------- SCROLLBAAR TEGELRASTER ---------- + # De pagina zelf scrollt niet: wallpaper-gradient, kamernaam en klok + # blijven staan. Alleen deze transparante container schuift. + # + # bg_opa: TRANSP is NIET hetzelfde als widget-`opa`: het maakt geen + # aparte renderlaag aan, dus het kost geen extra heap (zie de comment + # bij `theme:` hierboven). + # + # LVGL leidt de scroll-lengte zelf af uit de posities van de tegels; + # er is dus geen handmatige contenthoogte. Zodra een sleep de + # scroll-limiet (10 px) passeert annuleert LVGL de klik en de + # long-press, dus tikken en scrollen bijten elkaar niet. + - 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: 8 + scrollable: true + scroll_dir: VER + scrollbar_mode: 'OFF' + # Geen elastisch doorveren aan de randen: dat kost extra + # hertekeningen op een scherm dat toch al niet vloeiend scrollt. + scroll_elastic: false + scroll_momentum: true + # Klikt netjes per rij in. Verbergt de lage framerate een stuk + # beter dan vrij scrollen. Zet op NONE voor vrij scrollen. + scroll_snap_y: START + 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: + - script.execute: { id: tile1_ack, is_long: false } + - 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: + - script.execute: { id: tile1_ack, is_long: true } + - 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: + - script.execute: { id: tile2_ack, is_long: false } + - 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: + - script.execute: { id: tile2_ack, is_long: true } + - 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: + - script.execute: { id: tile3_ack, is_long: false } + - 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: + - script.execute: { id: tile3_ack, is_long: true } + - 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: + - script.execute: { id: tile4_ack, is_long: false } + - 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: + - script.execute: { id: tile4_ack, is_long: true } + - 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: + - script.execute: { id: tile5_ack, is_long: false } + - 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: + - script.execute: { id: tile5_ack, is_long: true } + - 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: + - script.execute: { id: tile6_ack, is_long: false } + - 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: + - script.execute: { id: tile6_ack, is_long: true } + - 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 + 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: + - script.execute: { id: tile7_ack, is_long: false } + - 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: + - script.execute: { id: tile7_ack, is_long: true } + - 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 + 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: + - script.execute: { id: tile8_ack, is_long: false } + - 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: + - script.execute: { id: tile8_ack, is_long: true } + - 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 + + # ---------- VALUE SLIDER OVERLAY ---------- + - 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 + - lvgl.widget.show: brightness_overlay + widgets: + - label: + align: CENTER + text: "x" + text_font: sublabel_big + text_color: 0xFFFFFF + clickable: false + + - label: + id: color_detail_title + 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 + + - obj: + id: color_mode_tabs + align: TOP_MID + y: 58 + width: 190 + height: 28 + radius: 14 + bg_color: 0x3C3C40 + bg_opa: COVER + border_width: 0 + scrollable: false + pad_left: 0 + pad_right: 0 + pad_top: 0 + pad_bottom: 0 + widgets: + - obj: + id: color_tab_indicator + x: 2 + y: 2 + width: 93 + height: 24 + radius: 12 + bg_color: 0x8E8E93 + bg_opa: COVER + border_width: 0 + clickable: false + scrollable: false + - obj: + x: 0 + y: 0 + width: 95 + height: 28 + bg_opa: TRANSP + border_width: 0 + clickable: true + scrollable: false + pad_left: 0 + pad_right: 0 + pad_top: 0 + pad_bottom: 0 + on_click: + - script.execute: + id: show_color_detail_tab + tab: 0 + widgets: + - label: + id: color_tab_label + align: CENTER + text: "Color" + text_font: sublabel_big + text_color: 0xFFFFFF + clickable: false + - obj: + x: 95 + y: 0 + width: 95 + height: 28 + bg_opa: TRANSP + border_width: 0 + clickable: true + scrollable: false + pad_left: 0 + pad_right: 0 + pad_top: 0 + pad_bottom: 0 + on_click: + - script.execute: + id: show_color_detail_tab + tab: 1 + widgets: + - label: + id: temp_tab_label + align: CENTER + text: "Temp." + text_font: sublabel_big + text_color: 0xFFFFFF + clickable: false + + - obj: + id: color_palette_card + align: TOP_MID + y: 98 + width: 286 + height: 48 + bg_opa: TRANSP + border_width: 0 + scrollable: false + pad_left: 0 + pad_right: 0 + pad_top: 0 + pad_bottom: 0 + widgets: + - obj: + id: color_preset_red + x: 14 + y: 8 + width: 30 + height: 30 + radius: 999 + bg_color: 0xFF3B30 + bg_opa: COVER + border_width: 0 + clickable: true + on_click: + - script.execute: { id: set_light_color_name, color: "red" } + - obj: + id: color_preset_orange + x: 61 + y: 8 + width: 30 + height: 30 + radius: 999 + bg_color: 0xFF9F0A + bg_opa: COVER + border_width: 0 + clickable: true + on_click: + - script.execute: { id: set_light_color_name, color: "orange" } + - obj: + id: color_preset_yellow + x: 108 + y: 8 + width: 30 + height: 30 + radius: 999 + bg_color: 0xFFD60A + bg_opa: COVER + border_width: 0 + clickable: true + on_click: + - script.execute: { id: set_light_color_name, color: "yellow" } + - obj: + id: color_preset_green + x: 155 + y: 8 + width: 30 + height: 30 + radius: 999 + bg_color: 0x30D158 + bg_opa: COVER + border_width: 0 + clickable: true + on_click: + - script.execute: { id: set_light_color_name, color: "green" } + - obj: + id: color_preset_blue + x: 202 + y: 8 + width: 30 + height: 30 + radius: 999 + bg_color: 0x0A84FF + bg_opa: COVER + border_width: 0 + clickable: true + on_click: + - script.execute: { id: set_light_color_name, color: "blue" } + - obj: + id: color_preset_purple + x: 249 + y: 8 + width: 30 + height: 30 + radius: 999 + bg_color: 0xBF5AF2 + bg_opa: COVER + border_width: 0 + clickable: true + on_click: + - script.execute: { id: set_light_color_name, color: "purple" } + + - obj: + id: temp_palette_card + align: TOP_MID + y: 98 + width: 286 + height: 48 + bg_opa: TRANSP + border_width: 0 + scrollable: false + hidden: true + pad_left: 0 + pad_right: 0 + pad_top: 0 + pad_bottom: 0 + widgets: + - obj: + id: temp_preset_2700 + x: 24 + y: 8 + width: 62 + height: 30 + radius: 15 + bg_color: 0xFFD4A3 + bg_opa: COVER + border_width: 0 + clickable: true + scrollable: false + scrollbar_mode: 'OFF' + pad_left: 0 + pad_right: 0 + pad_top: 0 + pad_bottom: 0 + on_click: + - script.execute: { id: set_light_color_temperature, kelvin: 2700 } + widgets: + - label: + align: CENTER + text: "2700K" + text_font: sublabel_big + text_color: 0x3A2A18 + clickable: false + - obj: + id: temp_preset_4000 + x: 112 + y: 8 + width: 62 + height: 30 + radius: 15 + bg_color: 0xFFE8C8 + bg_opa: COVER + border_width: 0 + clickable: true + scrollable: false + scrollbar_mode: 'OFF' + pad_left: 0 + pad_right: 0 + pad_top: 0 + pad_bottom: 0 + on_click: + - script.execute: { id: set_light_color_temperature, kelvin: 4000 } + widgets: + - label: + align: CENTER + text: "4000K" + text_font: sublabel_big + text_color: 0x3A2A18 + clickable: false + - obj: + id: temp_preset_6500 + x: 200 + y: 8 + width: 62 + height: 30 + radius: 15 + bg_color: 0xEAF4FF + bg_opa: COVER + border_width: 0 + clickable: true + scrollable: false + scrollbar_mode: 'OFF' + pad_left: 0 + pad_right: 0 + pad_top: 0 + pad_bottom: 0 + on_click: + - script.execute: { id: set_light_color_temperature, kelvin: 6500 } + widgets: + - label: + align: CENTER + text: "6500K" + text_font: sublabel_big + text_color: 0x1E2A36 + clickable: false + + - obj: + id: color_brightness_card + align: TOP_MID + y: 165 + width: 188 + height: 46 + bg_opa: TRANSP + border_width: 0 + scrollable: false + pad_left: 0 + pad_right: 0 + pad_top: 0 + pad_bottom: 0 + widgets: + - label: + x: 13 + y: 6 + text: "Brightness" + text_font: sublabel + text_color: 0xD1D1D6 + hidden: true + clickable: false + - slider: + id: color_brightness_slider + x: 0 + y: 0 + width: 188 + height: 46 + min_value: 1 + max_value: 100 + value: 100 + bg_color: 0x939391 + bg_opa: COVER + radius: 23 + border_width: 0 + indicator: + bg_color: 0xFEC600 + bg_opa: COVER + radius: 23 + knob: + bg_opa: 0% + border_width: 0 + shadow_width: 0 + pad_all: 10 + on_value: + - script.execute: + id: color_brightness_preview + value: !lambda "return x;" + on_release: + - script.execute: + id: color_brightness_commit + value: !lambda "return lv_slider_get_value(id(color_brightness_slider));" + - label: + id: color_brightness_value + align: RIGHT_MID + x: -13 + text: "100 %" + text_font: sublabel_big + text_color: 0xFFFFFF + hidden: true + 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 + 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_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: 36 + height: 36 + 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 + 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: 0xFFFFFF + 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: "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: "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: "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: "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 + +script: + # Korte bevestigingspuls na het loslaten. Draait parallel, dus vertraagt + # de Home Assistant-actie of het openen van een overlay niet. + - id: tile1_ack + mode: restart + parameters: + is_long: bool + then: + - if: + condition: + lambda: 'return is_long;' + then: + - lvgl.widget.update: { id: tile1, bg_opa: 20% } + - delay: 170ms + else: + - lvgl.widget.update: { id: tile1, bg_opa: 40% } + - delay: 90ms + - lvgl.widget.update: { id: tile1, bg_opa: 95% } + + # Korte bevestigingspuls na het loslaten. Draait parallel, dus vertraagt + # de Home Assistant-actie of het openen van een overlay niet. + - id: tile2_ack + mode: restart + parameters: + is_long: bool + then: + - if: + condition: + lambda: 'return is_long;' + then: + - lvgl.widget.update: { id: tile2, bg_opa: 20% } + - delay: 170ms + else: + - lvgl.widget.update: { id: tile2, bg_opa: 40% } + - delay: 90ms + - lvgl.widget.update: { id: tile2, bg_opa: 95% } + + # Korte bevestigingspuls na het loslaten. Draait parallel, dus vertraagt + # de Home Assistant-actie of het openen van een overlay niet. + - id: tile3_ack + mode: restart + parameters: + is_long: bool + then: + - if: + condition: + lambda: 'return is_long;' + then: + - lvgl.widget.update: { id: tile3, bg_opa: 20% } + - delay: 170ms + else: + - lvgl.widget.update: { id: tile3, bg_opa: 40% } + - delay: 90ms + - lvgl.widget.update: { id: tile3, bg_opa: 95% } + + # Korte bevestigingspuls na het loslaten. Draait parallel, dus vertraagt + # de Home Assistant-actie of het openen van een overlay niet. + - id: tile4_ack + mode: restart + parameters: + is_long: bool + then: + - if: + condition: + lambda: 'return is_long;' + then: + - lvgl.widget.update: { id: tile4, bg_opa: 20% } + - delay: 170ms + else: + - lvgl.widget.update: { id: tile4, bg_opa: 40% } + - delay: 90ms + - lvgl.widget.update: { id: tile4, bg_opa: 95% } + + # Korte bevestigingspuls na het loslaten. Draait parallel, dus vertraagt + # de Home Assistant-actie of het openen van een overlay niet. + - id: tile5_ack + mode: restart + parameters: + is_long: bool + then: + - if: + condition: + lambda: 'return is_long;' + then: + - lvgl.widget.update: { id: tile5, bg_opa: 20% } + - delay: 170ms + else: + - lvgl.widget.update: { id: tile5, bg_opa: 40% } + - delay: 90ms + - lvgl.widget.update: { id: tile5, bg_opa: 95% } + + # Korte bevestigingspuls na het loslaten. Draait parallel, dus vertraagt + # de Home Assistant-actie of het openen van een overlay niet. + - id: tile6_ack + mode: restart + parameters: + is_long: bool + then: + - if: + condition: + lambda: 'return is_long;' + then: + - lvgl.widget.update: { id: tile6, bg_opa: 20% } + - delay: 170ms + else: + - lvgl.widget.update: { id: tile6, bg_opa: 40% } + - delay: 90ms + - lvgl.widget.update: { id: tile6, bg_opa: 95% } + + # Korte bevestigingspuls na het loslaten. Draait parallel, dus vertraagt + # de Home Assistant-actie of het openen van een overlay niet. + - id: tile7_ack + mode: restart + parameters: + is_long: bool + then: + - if: + condition: + lambda: 'return is_long;' + then: + - lvgl.widget.update: { id: tile7, bg_opa: 20% } + - delay: 170ms + else: + - lvgl.widget.update: { id: tile7, bg_opa: 40% } + - delay: 90ms + - lvgl.widget.update: { id: tile7, bg_opa: 95% } + + # Korte bevestigingspuls na het loslaten. Draait parallel, dus vertraagt + # de Home Assistant-actie of het openen van een overlay niet. + - id: tile8_ack + mode: restart + parameters: + is_long: bool + then: + - if: + condition: + lambda: 'return is_long;' + then: + - lvgl.widget.update: { id: tile8, bg_opa: 20% } + - delay: 170ms + else: + - lvgl.widget.update: { id: tile8, bg_opa: 40% } + - delay: 90ms + - lvgl.widget.update: { id: tile8, bg_opa: 95% } + + - 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 atoi(\"${MAX_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 std::string("${AUTO_DIM}") == "true";' + then: + - lambda: |- + int dim_value = atoi("${DIM_BRIGHTNESS}"); + int max_value = atoi("${MAX_BRIGHTNESS}"); + + if (std::string("${NIGHT_MODE}") == "true") { + auto now = id(homeassistant_time).now(); + int hour = now.hour; + int start_hour = atoi("${NIGHT_START_HOUR}"); + int end_hour = atoi("${NIGHT_END_HOUR}"); + bool is_night = false; + if (start_hour < end_hour) { + is_night = (hour >= start_hour && hour < end_hour); + } else { + is_night = (hour >= start_hour || hour < end_hour); + } + if (is_night) { + dim_value = atoi("${NIGHT_DIM_BRIGHTNESS}"); + } + } + + // Rustig omlaag faden is prettig; omhoog moet juist snel. + id(back_light).turn_on().set_transition_length(900).set_brightness(dim_value / 100.0f).perform(); + + // Always mark as dimmed to stop the interval from re-firing + id(display_dimmed) = true; + // Only show the touch-blocking overlay when brightness actually changes + if (dim_value < max_value) { + lv_obj_clear_flag(id(dim_wake_overlay), LV_OBJ_FLAG_HIDDEN); + } + + # 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: queued + 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: |- + int min_x10 = id(active_climate_min_x10); + int max_x10 = id(active_climate_max_x10); + if (value < min_x10) value = min_x10; + if (value > max_x10) value = max_x10; + int step_x10 = id(active_climate_step_x10); + if (step_x10 < 5) step_x10 = 5; + value = min_x10 + (int) lround((value - min_x10) / (float) step_x10) * step_x10; + if (value < min_x10) value = min_x10; + if (value > max_x10) value = max_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 std::string("${DIRECT_ACTIONS}") == "true";' + then: + - homeassistant.action: + action: climate.set_temperature + data: + entity_id: !lambda "return id(active_entity);" + temperature: !lambda "return value / 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 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 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()) return true; + 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(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 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);" + + - id: open_color_detail + mode: restart + then: + - lambda: |- + lv_label_set_text(id(color_detail_title), id(active_title).c_str()); + + char buf[12]; + snprintf(buf, sizeof(buf), "%d %%", id(active_brightness_pct)); + lv_label_set_text(id(color_brightness_value), buf); + lv_slider_set_value(id(color_brightness_slider), id(active_brightness_pct), LV_ANIM_OFF); + + bool show_tabs = id(active_supports_color) && id(active_supports_temperature); + if (show_tabs) { + lv_obj_clear_flag(id(color_mode_tabs), LV_OBJ_FLAG_HIDDEN); + } else { + lv_obj_add_flag(id(color_mode_tabs), LV_OBJ_FLAG_HIDDEN); + } + + if (atoi("${DISPLAY_W}") < atoi("${DISPLAY_H}")) { + int palette_y = show_tabs ? 96 : 76; + lv_obj_set_size(id(color_palette_card), 150, 86); + lv_obj_set_y(id(color_palette_card), palette_y); + lv_obj_set_pos(id(color_preset_red), 13, 4); + lv_obj_set_pos(id(color_preset_orange), 60, 4); + lv_obj_set_pos(id(color_preset_yellow), 107, 4); + lv_obj_set_pos(id(color_preset_green), 13, 52); + lv_obj_set_pos(id(color_preset_blue), 60, 52); + lv_obj_set_pos(id(color_preset_purple), 107, 52); + + lv_obj_set_size(id(temp_palette_card), 150, 126); + lv_obj_set_y(id(temp_palette_card), palette_y); + lv_obj_set_pos(id(temp_preset_2700), 44, 4); + lv_obj_set_pos(id(temp_preset_4000), 44, 48); + lv_obj_set_pos(id(temp_preset_6500), 44, 92); + + lv_obj_set_y(id(color_brightness_card), 256); + } else { + int palette_y = show_tabs ? 98 : 78; + lv_obj_set_size(id(color_palette_card), 286, 48); + lv_obj_set_y(id(color_palette_card), palette_y); + lv_obj_set_pos(id(color_preset_red), 14, 8); + lv_obj_set_pos(id(color_preset_orange), 61, 8); + lv_obj_set_pos(id(color_preset_yellow), 108, 8); + lv_obj_set_pos(id(color_preset_green), 155, 8); + lv_obj_set_pos(id(color_preset_blue), 202, 8); + lv_obj_set_pos(id(color_preset_purple), 249, 8); + + lv_obj_set_size(id(temp_palette_card), 286, 48); + lv_obj_set_y(id(temp_palette_card), palette_y); + lv_obj_set_pos(id(temp_preset_2700), 24, 8); + lv_obj_set_pos(id(temp_preset_4000), 112, 8); + lv_obj_set_pos(id(temp_preset_6500), 200, 8); + + lv_obj_set_y(id(color_brightness_card), 165); + } + + id(active_color_detail_tab) = id(active_supports_color) ? 0 : 1; + - lvgl.widget.hide: brightness_overlay + - lvgl.widget.show: color_detail_overlay + - script.execute: + id: show_color_detail_tab + tab: !lambda "return id(active_color_detail_tab);" + + - id: show_color_detail_tab + mode: restart + parameters: + tab: int + then: + - lambda: |- + if (tab == 0 && !id(active_supports_color) && id(active_supports_temperature)) { + tab = 1; + } else if (tab == 1 && !id(active_supports_temperature) && id(active_supports_color)) { + tab = 0; + } + id(active_color_detail_tab) = tab; + + bool show_tabs = id(active_supports_color) && id(active_supports_temperature); + if (show_tabs) { + lv_obj_clear_flag(id(color_mode_tabs), LV_OBJ_FLAG_HIDDEN); + } else { + lv_obj_add_flag(id(color_mode_tabs), LV_OBJ_FLAG_HIDDEN); + } + + if (tab == 0 && id(active_supports_color)) { + lv_obj_clear_flag(id(color_palette_card), LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(id(temp_palette_card), LV_OBJ_FLAG_HIDDEN); + if (show_tabs) lv_obj_set_x(id(color_tab_indicator), 2); + } else if (tab == 1 && id(active_supports_temperature)) { + lv_obj_add_flag(id(color_palette_card), LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(id(temp_palette_card), LV_OBJ_FLAG_HIDDEN); + if (show_tabs) lv_obj_set_x(id(color_tab_indicator), 95); + } else { + lv_obj_add_flag(id(color_palette_card), LV_OBJ_FLAG_HIDDEN); + lv_obj_add_flag(id(temp_palette_card), LV_OBJ_FLAG_HIDDEN); + } + + lv_obj_set_style_text_color( + id(color_tab_label), + id(active_supports_color) ? lv_color_hex(0xFFFFFF) : lv_color_hex(0x8E8E93), + 0 + ); + lv_obj_set_style_text_color( + id(temp_tab_label), + id(active_supports_temperature) ? lv_color_hex(0xFFFFFF) : lv_color_hex(0x8E8E93), + 0 + ); + if (tab == 0 && id(active_supports_color)) { + lv_obj_set_style_text_color(id(color_tab_label), lv_color_hex(0xFFFFFF), 0); + lv_obj_set_style_text_color(id(temp_tab_label), lv_color_hex(0xD1D1D6), 0); + } else if (tab == 1 && id(active_supports_temperature)) { + lv_obj_set_style_text_color(id(color_tab_label), lv_color_hex(0xD1D1D6), 0); + lv_obj_set_style_text_color(id(temp_tab_label), lv_color_hex(0xFFFFFF), 0); + } + + - id: color_brightness_preview + parameters: + value: int + then: + - lambda: |- + id(active_brightness_pct) = value; + char buf[12]; + snprintf(buf, sizeof(buf), "%d %%", value); + lv_label_set_text(id(color_brightness_value), buf); + + - id: color_brightness_commit + parameters: + value: int + then: + - lambda: 'id(active_brightness_pct) = value;' + - if: + condition: + lambda: 'return std::string("${DIRECT_ACTIONS}") == "true";' + then: + - homeassistant.action: + action: light.turn_on + data: + entity_id: !lambda "return id(active_entity);" + brightness_pct: !lambda "return value;" + + - id: set_light_color_name + mode: queued + parameters: + color: string + then: + - if: + condition: + lambda: 'return std::string("${DIRECT_ACTIONS}") == "true";' + then: + - homeassistant.action: + action: light.turn_on + data: + entity_id: !lambda "return id(active_entity);" + color_name: !lambda "return color;" + brightness_pct: !lambda "return id(active_brightness_pct);" + + - id: set_light_color_temperature + mode: queued + parameters: + kelvin: int + then: + - if: + condition: + lambda: 'return std::string("${DIRECT_ACTIONS}") == "true";' + then: + - homeassistant.action: + action: light.turn_on + data: + entity_id: !lambda "return id(active_entity);" + color_temp_kelvin: !lambda "return kelvin;" + brightness_pct: !lambda "return id(active_brightness_pct);" + + - 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: |- + return std::string("${DIRECT_ACTIONS}") == "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: |- + 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(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(slider_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(slider_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: + - lvgl.widget.show: brightness_overlay + + # UI refresh (values + time + styles) + render + - id: ui_refresh + mode: queued + then: + - lambda: |- + auto set_lbl = [](lv_obj_t* lbl, const std::string& s) { + if (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 == "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}"); + + 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}"); + + 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(active_climate_mode) = climate_mode; + lv_label_set_text(id(climate_mode_label), label_for_climate_mode(climate_mode).c_str()); + } + + int 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(); + 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)); + } + + auto apply_tile = [&](bool active, + 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) + { + 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, + 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, + 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, + 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, + 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, + 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, + 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, + 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, + 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}); + + - component.update: my_display diff --git a/images/smartdisplay_background.png b/images/smartdisplay_background.png new file mode 100644 index 0000000..3fb678f Binary files /dev/null and b/images/smartdisplay_background.png differ diff --git a/images/smartdisplay_background_90.png b/images/smartdisplay_background_90.png new file mode 100644 index 0000000..63ad7a5 Binary files /dev/null and b/images/smartdisplay_background_90.png differ