diff --git a/.gitignore b/.gitignore index 45a0b12..c747387 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,7 @@ __pycache__/ /calibration*.yaml.bak-* /calibration*.yaml.tmp-* /.DS_Store + +/guition-device.yaml + +/diagnostics/*.png diff --git a/AGENTS.md b/AGENTS.md index 6c2c5e8..3465e11 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,3 +1,11 @@ +## Guition-branch + +De Guition 4848S040 heeft een apart profiel `guition-4848s040.yaml` met +480×480, ST7701S RGB en GT911. Lees docs/GUITION.md. Gebruik de eigen lokale +`guition-device.yaml`; neem geen CYD-layout of XPT2046-kalibratie over. +Controleer touch met tools/verify_gt911.py en houd de CYD-regressies groen. +Configureer geen wallbox-relais als onderdeel van displayondersteuning. + # Werkinstructies voor LLM's en developers Dit project bedient een ESP32-2432S028 met ILI9341 + XPT2046 (320×240, diff --git a/README.md b/README.md index a953151..d96cf70 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,6 @@ +> **Guition ESP32-S3 4-inch / 480×480:** gebruik [de Guition-handleiding](docs/GUITION.md) +> en `guition-device.yaml`. De handleiding hieronder betreft het 2,8-inch CYD. + # CYD Home Assistant-bedieningsscherm Van een nieuwe **ESP32-2432S028 met ILI9341 + XPT2046** naar een gekalibreerd diff --git a/diagnostics/capture_ui.py b/diagnostics/capture_ui.py new file mode 100644 index 0000000..65a1363 --- /dev/null +++ b/diagnostics/capture_ui.py @@ -0,0 +1,51 @@ +"""Save a Guition LVGL diagnostic snapshot as PNG through its encrypted API.""" +import argparse +import asyncio +from pathlib import Path +import re +import struct +import zlib +import yaml +from aioesphomeapi import APIClient, LogLevel + + +def png(rows): + pixels=bytearray() + for y in range(240): + pixels.append(0) + for x in range(240): + p=int(rows[y][x*4:x*4+4],16) + pixels.extend((((p>>11)&31)*255//31,((p>>5)&63)*255//63,(p&31)*255//31)) + def chunk(name,data): + return struct.pack('>I',len(data))+name+data+struct.pack('>I',zlib.crc32(name+data)&0xffffffff) + return b'\x89PNG\r\n\x1a\n'+chunk(b'IHDR',struct.pack('>2I5B',240,240,8,2,0,0,0))+chunk(b'IDAT',zlib.compress(pixels))+chunk(b'IEND',b'') + + +async def main(): + p=argparse.ArgumentParser(description=__doc__) + p.add_argument('--host',default='guition-wallbox.local') + p.add_argument('--name',default='guition-wallbox') + p.add_argument('--secrets',type=Path,default=Path(__file__).resolve().parents[1]/'secrets.yaml') + p.add_argument('--output',type=Path,required=True) + args=p.parse_args() + if args.output.exists():raise ValueError('Output bestaat al; kies een andere naam.') + client=APIClient(args.host,6053,noise_psk=yaml.safe_load(args.secrets.read_text())['api_encryption_key'],expected_name=args.name) + await client.connect(login=True) + try: + _,services=await client.list_entities_services() + done=asyncio.Event();rows={};errors=[] + def log(msg): + text=msg.message.decode(errors='replace') if isinstance(msg.message,bytes) else msg.message + m=re.search(r'ROW (\d{3}) ([0-9a-f]{960})',text) + if m:rows[int(m[1])]=m[2] + if 'IMAGE FAIL' in text:errors.append(text);done.set() + if 'IMAGE COMPLETE' in text:done.set() + client.subscribe_logs(log,log_level=LogLevel.LOG_LEVEL_INFO,dump_config=False) + await client.execute_service(next(s for s in services if s.name=='ui_snapshot'),{}) + await asyncio.wait_for(done.wait(),timeout=60) + if errors or set(rows)!=set(range(240)):raise ValueError(f'Onvolledig beeld: {len(rows)}/240 rijen; {errors}') + with args.output.open('xb') as f:f.write(png(rows)) + print(f'PASS: LVGL-render opgeslagen in {args.output}') + finally:await client.disconnect() + +if __name__=='__main__':asyncio.run(main()) diff --git a/docs/GUITION.md b/docs/GUITION.md new file mode 100644 index 0000000..1f9c4ba --- /dev/null +++ b/docs/GUITION.md @@ -0,0 +1,152 @@ +# Guition ESP32-S3 4-inch wallbox — 480 × 480 + +Deze branch voegt een apart profiel toe voor de **Guition 4848S040** met +ST7701S RGB-display en **GT911 capacitieve touch**, ESP32-S3, 16 MB flash en +8 MB octal PSRAM. De naam/2mm-wandplaat beschrijft ook de behuizing; controleer +altijd de elektronica. Dit profiel configureert geen relais. + +Hardwarebron: [ESPHome Guition-bordbeschrijving](https://devices.esphome.io/devices/guition-esp32-s3-4848s040/) +en het ingebouwde [MIPI RGB-model](https://esphome.io/components/display/mipi_rgb/). +De daadwerkelijk geïnstalleerde ESPHome 2026.6.2 bevat dit model. + +## Bestanden + +- `guition-4848s040.yaml`: neutrale hardware + 480×480-interface. +- `guition-device.example.yaml`: eigen apparaat-/tegelinstellingen. +- `guition-device.yaml`: jouw lokale profiel, buiten Git. +- `secrets.yaml`: lokale wifi/API/OTA-instellingen, buiten Git. +- `tools/verify_gt911.py`: fysieke pixelcontrole; geen resistieve ADC-kalibratie. + +Het 2,8-inch CYD-profiel blijft apart. Flash dat niet naar de Guition en neem +geen `calibration.yaml` of XPT2046-correctie over. + +## Interface + +Zes tegels van **218 × 108 pixels** per pagina, 12px tussenruimte en een +aparte navigatiestrook. Vanaf zeven tegels verschijnt pagina twee; tot zes +verdwijnt de navigatie. Grotere letters, iconen, dimmers en kleurkeuzes; de +climatekaart heeft een grotere temperatuurweergave. Een donkerblauwe +achtergrond, gedempte uitgeschakelde tegels en lichte actieve tegels maken +de status herkenbaar. Standby begint na tien minuten zonder aanraking. + +De bestaande tegelacties, climatebediening en vacuumkaart zijn behouden. +De vacuumkaart blijft gekoppeld aan tegel 6. Posities 8/10 zijn eenvoudige +actietegels; zie [TEGELS.md](TEGELS.md) voor de ondersteunde bindingen. + +## Nieuwe installatie + +Volg de Python-/USB-voorbereiding in [README.md](../README.md), in een verse +kopie. Maak dit profiel: + +```sh +python tools/new_device.py --board guition --name wallbox-keuken --friendly-name "Wallbox keuken" +``` + +Dit maakt `guition-device.yaml` en nieuwe secrets; bestaande bestanden worden +niet overschreven. Vul wifi in en wijzig de tegelentiteiten. Gebruik bij +meerdere borden een aparte map per apparaat. Het huidige lokale Guition-profiel +van de eigenaar mag diens bestaande secrets gebruiken. + +Controleer de seriële poort en chip voordat je uploadt. Voor het geteste bord +bleken hogere seriële snelheden bij uitlezen onbetrouwbaar. Gebruik 115200 baud: + +```sh +python -m esphome config guition-device.yaml +python -m esphome compile guition-device.yaml +``` + +Flash via `esphome run` als de USB-verbinding betrouwbaar is. Voor expliciet +115200 baud kun je de gecombineerde factory-image gebruiken. Vervang zowel +poort als apparaatnaam in het pad door jouw waarden: + +```sh +python -m esptool --chip esp32s3 --port --baud 115200 write-flash 0 .esphome/build/wallbox-keuken/.pioenvs/wallbox-keuken/firmware.factory.bin +``` + +De factory-image hoort bij dit S3-profiel en begint op adres **0**, anders dan +sommige klassieke ESP32-images. Bewaar een herstelkopie vóór vervanging van +bestaande firmware als die later nog nodig is. Houd binaries en logs lokaal. + +## GT911 en oriëntatie testen + +De GT911 geeft pixels door. Meestal is geen kalibratie nodig; een verkeerde +rotatie/transform mag niet met de CYD-affinewizard worden weggewerkt. + +Met een werkende wifi/API-verbinding: + +```sh +python diagnostics/control_ui.py touch_diagnostics --host wallbox-keuken.local --name wallbox-keuken +python tools/verify_gt911.py --port --output measurements-guition.json +python diagnostics/control_ui.py end_touch_diagnostics --host wallbox-keuken.local --name wallbox-keuken +``` + +Er verschijnen vijf kruisjes. De wizard vraagt drie afzonderlijke tikken per +punt; eerst Enter en daarna uitsluitend het genoemde kruisje aanraken. Fout +maximaal 16px, spreiding maximaal 18px. De test stuurt geen HA-acties. + +Zonder wifi kun je eerst een build maken met `-s CALIBRATION_ON_BOOT true`, +die na USB-upload direct het GT911-meetscherm toont. Na controle opnieuw +bouwen/flashen zonder die override. Deze compatibiliteitsnaam opent alleen +een pixeltest; hij voert geen ADC-kalibratie uit. + +Op het geteste paneel zijn beide GT911-assen gespiegeld: `TOUCH_MIRROR_X` en +`TOUCH_MIRROR_Y` staan op `true`. Standaard is `LVGL_ROTATION: "0"`. Bij een andere montage stel je de gewenste +LVGL-rotatie in en geef je dezelfde waarde aan de wizard, bijvoorbeeld +`--rotation 90`. Controleer alle hoeken; verander de GT911 `TOUCH_SWAP_XY` / +`TOUCH_MIRROR_X` / `TOUCH_MIRROR_Y` alleen als de fysieke meting dat vereist. + +## HA en acceptatie + +Voeg het nieuwe apparaat toe via de ESPHome-integratie (naam/IP, poort 6053, +API-sleutel uit secrets). Geef toestemming voor HA-acties en gebruik +`DIRECT_ACTIONS: "true"` zodra de juiste entiteiten gecontroleerd zijn. + +```sh +python diagnostics/run_ui_test.py --host wallbox-keuken.local --name wallbox-keuken +python -m unittest discover -s tests -p 'test_*.py' +``` + +Doorloop ook de fysieke controles uit [ACCEPTATIE.md](ACCEPTATIE.md), met de +GT911-wizard in plaats van de XPT2046-kalibratie. Controleer beeld, juiste +aanrakingen, paginering, lange druk, climate/vacuum, echte HA-terugmelding, +standby en herstel na herstart. Een compilatie/renderproef vervangt die +fysieke controle niet. De testuitkomst van het oorspronkelijke CYD-bord +zegt niets over dit nieuwe bord. + +## Gerenderde interface inspecteren + +```sh +python diagnostics/capture_ui.py --host wallbox-keuken.local --name wallbox-keuken --output diagnostics/guition-home.png +``` + +Dit maakt alleen op verzoek een LVGL-snapshot en stuurt een 240×240-preview +via de versleutelde API. De tijdelijke buffer wordt na ongeveer 24 seconden +vrijgegeven. Het beeld controleert de renderer; het bewijst niet dat de fysieke +RGB-signalen, schermkleuren of montageoriëntatie goed zijn. De renderproef +controleert ook of zichtbare klikgebieden binnen het 480×480-scherm vallen. + +## Herstelkopie van dit testbord + +De oorspronkelijke gebruikte firmware inclusief bootloader, partitietabel +en NVS is lokaal bewaard als `diagnostics/guition-original-recovery.bin`. +De lengtes van de vijf applicatiesegmenten bepaalden de benodigde 4.788.224 +bytes; dit is geen kopie van alle 16 MB flash. De stub controleerde de MD5 +van ieder uitgelezen blok. SHA-256 van het complete herstelbestand: + +```text +a656c793c422848b5ad72bc16545fb92c844b7dfba029f4e4399d22444a9e046 +``` + +Deze kopie is privé, hoort uitsluitend bij het geteste bord en wordt niet +meegecommit of geëxporteerd. Herstellen gebeurt op adres 0, met dezelfde +115200-baudopdracht als de factory-image, maar met dit herstelbestand. + +## RGB-geheugeninstellingen + +De pixelklok staat op 16 MHz: op het testbord verdween daarmee het zachte +flikkeren van de 12MHz-instelling. Voor de RGB-bouncebuffer worden code en +constante data vanuit octal PSRAM uitgevoerd, met 64KB datacache en 64-byte +cachelijnen; herstel van de RGB-stroom gebeurt op VSYNC. Deze instellingen +volgen [Espressifs RGB-LCD-aanbevelingen](https://docs.espressif.com/projects/esp-idf/en/v5.5.4/esp32s3/api-reference/peripherals/lcd/rgb_lcd.html). +Controleer fysiek op incidentele glitches onder wifi-/renderbelasting; een +software-snapshot kan een verstoring van het paneelsignaal niet bewijzen. diff --git a/docs/GUITION_TESTRESULTATEN.md b/docs/GUITION_TESTRESULTATEN.md new file mode 100644 index 0000000..425672c --- /dev/null +++ b/docs/GUITION_TESTRESULTATEN.md @@ -0,0 +1,50 @@ +# Guition hardwaretest — 12 september 2026 + +Getest op de aangesloten ESP32-S3 rev. 0.2, 16 MB flash en 8 MB PSRAM, +GT911 op I²C-adres 0x5D, GUITION-4848S040 RGB-display op 480×480. + +## Werkende versie + +ESPHome 2026.6.2 / ESP-IDF 5.5.4. De firmware rapporteert buildtijd +`2026-09-12 10:45:49 +0200`, config-hash `0xbaee760b`. + +- Beide GT911-assen gespiegeld. De gebruiker zag eerst linksboven als + rechtsonder; na correctie kwamen de vijf aangeraakte regio's in de juiste + volgorde binnen: linksboven, rechtsboven, rechtsonder, linksonder, midden. +- Pixelklok 16 MHz. De gebruiker bevestigde dat het zachte flikkeren daarmee + verdween, maar meldde nog af en toe een glitch. +- Vervolgens PSRAM-uitvoering voor code/constanten, 64KB datacache met + 64-byte cachelijnen en RGB-herstel op VSYNC ingeschakeld. Na deze update en + de herhaalde renderproef bevestigde de gebruiker: **“het werkt super!”** + +## Uitgevoerde controles + +- 23 Python-regressietests geslaagd, inclusief nieuw bordprofiel, tegelformaat, + navigatieruimte, GT911-coördinaten/rotatie en overdracht zonder lokale secrets. +- Beide C++17-testprogramma's geslaagd: eventguard/temperatuur en CYD-touchfilter. +- Volledige S3-build geslaagd. Eerste USB-upload op 115200 baud met verificatie + van de geschreven hash; vervolguploads via geauthenticeerde OTA geslaagd. +- De originele gebruikte applicatie, bootloader, partitietabel en NVS zijn + als lokale herstelkopie bewaard; zie GUITION.md voor lengte en SHA-256. +- Bootlogs bevestigen 8192KB beschikbare PSRAM, de GT911 en RGB-driver. +- **Tien paginacontroles en vijftig overlaycycli op de definitieve firmware** + geslaagd, inclusief controle dat zichtbare klikgebieden binnen 480×480 liggen. +- Tegelgebied eindigt op y=419; de volgende-knop begint op y=432. Geen overlap. +- Vrije heap bleef tijdens de definitieve renderproef rond **6,51 MB**. +- Een echte LVGL-snapshot via de versleutelde API opgehaald en visueel + gecontroleerd op tekst, kleuren en indeling. Deze screenshotfunctie bekijkt + de renderer; de fysieke signaalcontrole komt van de gebruiker. +- Een verse export uitgepakt en een neutraal Guition-profiel aangemaakt; + ESPHome-codegeneratie geslaagd zonder de persoonlijke tegelconfiguratie. + +## Grenzen van deze test + +De fysieke check gebruikte één tik per doel plus gebruik van de bediening; +het volledige drie-tikken-per-punt-protocol uit `verify_gt911.py` is niet op +het bord uitgevoerd. Stabiliteit is tijdens deze testsessie geobserveerd, +niet gedurende dagen. De tien-minutenstandby is behouden in de configuratie; +een nieuwe volledige standbyduurtest is niet uitgevoerd. + +Home Assistant ontdekte het nieuwe apparaat. De koppeldialoog wacht nog op de +lokale API-encryptiesleutel. Werkelijke HA-acties en de terugmelding van eigen +entiteiten zijn op dit nieuwe apparaat daarom **nog niet geverifieerd**. diff --git a/guition-4848s040.yaml b/guition-4848s040.yaml new file mode 100644 index 0000000..a8416a4 --- /dev/null +++ b/guition-4848s040.yaml @@ -0,0 +1,6136 @@ +# Guition ESP32-S3-4848S040 / 4-inch EU wallbox, 480x480 IPS + GT911. +# Use guition-device.yaml; see docs/GUITION.md. CYD hardware is a separate profile. +substitutions: + DEVICE_NAME: "guition-wallbox" + DEVICE_FRIENDLY_NAME: "Guition Wallbox" + CALIBRATION_ON_BOOT: "false" + + TILE_COUNT: "6" # 1-10; <=6 hides page navigation + + DIRECT_ACTIONS: "false" # true = call Home Assistant services directly, false = only emit action events + ROOM_NAME: "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: "600" # seconds after last touch + MAX_BRIGHTNESS: "100" # backlight brightness in percent + DIM_BRIGHTNESS: "20" # dimmed backlight brightness in percent + NIGHT_MODE: "true" # true = use separate dim level at night + NIGHT_START_HOUR: "22" # 0-23 + NIGHT_END_HOUR: "7" # 0-23 + NIGHT_DIM_BRIGHTNESS: "10" # backlight brightness in percent at night + + # ============================================================ + # ACCENTKLEUREN — automatisch bepaald uit het Home Assistant-domein + # + # Het domein is het stuk voor de punt in het entity_id: + # script.goede_morgen -> "script" -> ACCENT_ACTION + # light.woonkamer_2 -> "light" -> ACCENT_LIGHT + # vacuum.s8 -> "vacuum" -> ACCENT_DEVICE + # + # Dat is Home Assistant's eigen indeling, dus een tegel krijgt vanzelf de + # juiste kleur zodra je er een entity in zet — je hoeft per tegel geen + # kleuren meer te kiezen. Het onderscheid dat telt: een script/scene vuur + # je af, een apparaat zet je aan of uit. Die twee horen niet hetzelfde + # te zijn. + # + # Zet TILE*_CIRCLE_ACTIVE_COLOR / TILE*_ICON_DISABLED_COLOR op + # ${ACCENT_AUTO} om dit te gebruiken, of op een echte hexwaarde om die + # ene tegel handmatig te kleuren (zie tegel 8 — rood zodra er post ligt). + # ============================================================ + ACCENT_AUTO: "0x1000000" # sentinel: bewust buiten het 24-bits kleurbereik + ACCENT_ACTION: "0x8B5CF6" # script / scene / button / input_button / automation + ACCENT_LIGHT: "0xFEC600" # light + ACCENT_DEVICE: "0x00C5EC" # switch, fan, cover, climate, vacuum, media_player, rest + + # ============================================================ + # FONT GLYPHS + # List every unique MDI icon used across your tiles — each value must appear exactly once. + # Reusing the same icon on multiple tiles is fine: just list that glyph once here. + # If you use fewer than 6 unique icons, leave the extra slots with their default values + # (unused glyphs add a few bytes to the font but cause no errors). + # ============================================================ + MDI_GLYPH_1: "\U000F001B" + MDI_GLYPH_2: "\U000F0387" # mdi:music + MDI_GLYPH_3: "\U000F02E3" + MDI_GLYPH_4: "\U000F059C" + MDI_GLYPH_5: "\U000F0425" + MDI_GLYPH_6: "\U000F070D" + MDI_GLYPH_7: "\U000F06B5" # mdi:lamp + MDI_GLYPH_8: "\U000F06EE" # mdi:mailbox + MDI_GLYPH_9: "\U000F04B9" # mdi:sofa + MDI_GLYPH_10: "\U000F002A" # mdi:alarm-light + + # ============================================================ + # TILE 1 + # ============================================================ + + # --- basic settings --- + TILE1_ENTITY: "light.voorbeeld_lamp" # main Home Assistant entity for this tile + TILE1_STATE_ENTITY: "light.voorbeeld_lamp" # entity used for active/inactive state styling + TILE1_TITLE: "Lamp" # tile title shown in the UI + TILE1_ICON: "\U000F001B" # MDI glyph — also add to MDI_GLYPH_* list above if not already there + TILE1_TYPE: "light" # tile type: light | fan | switch | scene | script | cover | climate + TILE1_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset | custom + TILE1_LONGPRESS: "slider" # long press mode: none | slider | action + TILE1_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text + TILE1_LABEL_OFF: "Uit" # text shown when entity is off + TILE1_LABEL_ON: "Aan" # text shown for text-mode tiles when entity is on + TILE1_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") + + # --- colors --- + TILE1_CIRCLE_ACTIVE_COLOR: "${ACCENT_AUTO}" # icon circle color when active + TILE1_CIRCLE_DISABLED_COLOR: "0x1B2D3C" # icon circle color when inactive + TILE1_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active + TILE1_ICON_DISABLED_COLOR: "${ACCENT_AUTO}" # icon color when inactive + TILE1_BG_ACTIVE_COLOR: "0xF5F3EB" # tile background color when active + TILE1_BG_DISABLED_COLOR: "0x293F50" # tile background color when inactive + TILE1_TITLE_ACTIVE_COLOR: "0x142334" # title color when active + TILE1_TITLE_DISABLED_COLOR: "0xFFFFFF" # title color when inactive + TILE1_VALUE_ACTIVE_COLOR: "0x526477" # value color when active + TILE1_VALUE_DISABLED_COLOR: "0xB9CDDB" # value color when inactive + + # --- advanced settings --- + TILE1_TAP_SERVICE: "" # optional service override, e.g. light.toggle + TILE1_TAP_PARAM_KEY: "" # optional extra service parameter key + TILE1_TAP_PARAM_VAL: "" # optional extra service parameter value + # if LONGPRESS = slider: + TILE1_LONGPRESS_SLIDER: "auto" # slider type: auto | brightness | percentage | cover_position + TILE1_LONGPRESS_OFF_VALUE: "0" # initial slider value when entity is off + # if LONGPRESS = action: + TILE1_LONGPRESS_ACTION: "" # entity_id to fire on long press + TILE1_LONGPRESS_ACTION_TYPE: "" # entity type: light | fan | switch | scene | script | cover | climate (empty = same as TILE*_TYPE) + TILE1_LONGPRESS_ACTION_SERVICE: "" # service to call (default: activate/turn_on — set e.g. "light.toggle" to toggle instead) + # if TYPE = climate: + TILE1_CLIMATE_MIN_TEMP: "16.0" # minimum target temperature shown in the thermostat overlay + TILE1_CLIMATE_MAX_TEMP: "32.0" # maximum target temperature shown in the thermostat overlay + TILE1_CLIMATE_STEP: "1.0" # target temperature step size + TILE1_CLIMATE_CURRENT_TEMP_ATTRIBUTE: "current_temperature" # HA attribute for current room temperature + TILE1_CLIMATE_TARGET_TEMP_ATTRIBUTE: "temperature" # HA attribute for target temperature + TILE1_CLIMATE_HUMIDITY_ATTRIBUTE: "current_humidity" # HA attribute for humidity; leave as-is if unavailable + + + # ============================================================ + # TILE 2 + # ============================================================ + + # --- basic settings --- + TILE2_ENTITY: "switch.voorbeeld_stekker" # main Home Assistant entity for this tile + TILE2_STATE_ENTITY: "switch.voorbeeld_stekker" # entity used for active/inactive state styling + TILE2_TITLE: "Stekker" # tile title shown in the UI + TILE2_ICON: "\U000F0387" # MDI glyph — also add to MDI_GLYPH_* list above if not already there + TILE2_TYPE: "switch" # tile type: light | fan | switch | scene | script | cover | climate + TILE2_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset | custom + TILE2_LONGPRESS: "none" # long press mode: none | slider | action + TILE2_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text + TILE2_LABEL_OFF: "Uit" # text shown when entity is off + TILE2_LABEL_ON: "Aan" # text shown for text-mode tiles when entity is on + TILE2_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") + + # --- colors --- + TILE2_CIRCLE_ACTIVE_COLOR: "${ACCENT_AUTO}" # icon circle color when active + TILE2_CIRCLE_DISABLED_COLOR: "0x1B2D3C" # icon circle color when inactive + TILE2_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active + TILE2_ICON_DISABLED_COLOR: "${ACCENT_AUTO}" # icon color when inactive + TILE2_BG_ACTIVE_COLOR: "0xF5F3EB" # tile background color when active + TILE2_BG_DISABLED_COLOR: "0x293F50" # tile background color when inactive + TILE2_TITLE_ACTIVE_COLOR: "0x142334" # title color when active + TILE2_TITLE_DISABLED_COLOR: "0xFFFFFF" # title color when inactive + TILE2_VALUE_ACTIVE_COLOR: "0x526477" # value color when active + TILE2_VALUE_DISABLED_COLOR: "0xB9CDDB" # value color when inactive + + # --- advanced settings --- + TILE2_TAP_SERVICE: "" # optional service override, e.g. light.toggle + TILE2_TAP_PARAM_KEY: "" # optional extra service parameter key + TILE2_TAP_PARAM_VAL: "" # optional extra service parameter value + # if LONGPRESS = slider: + TILE2_LONGPRESS_SLIDER: "auto" # slider type: auto | brightness | percentage | cover_position + TILE2_LONGPRESS_OFF_VALUE: "0" # initial slider value when entity is off + # if LONGPRESS = action: + TILE2_LONGPRESS_ACTION: "" # entity_id to fire on long press + TILE2_LONGPRESS_ACTION_TYPE: "" # entity type: light | fan | switch | scene | script | cover | climate (empty = same as TILE*_TYPE) + TILE2_LONGPRESS_ACTION_SERVICE: "" # service to call (default: activate/turn_on — set e.g. "light.toggle" to toggle instead) + # if TYPE = climate: + TILE2_CLIMATE_MIN_TEMP: "5.0" # minimum target temperature shown in the thermostat overlay + TILE2_CLIMATE_MAX_TEMP: "30.0" # maximum target temperature shown in the thermostat overlay + TILE2_CLIMATE_STEP: "0.5" # target temperature step size + TILE2_CLIMATE_CURRENT_TEMP_ATTRIBUTE: "current_temperature" # HA attribute for current room temperature + TILE2_CLIMATE_TARGET_TEMP_ATTRIBUTE: "temperature" # HA attribute for target temperature + TILE2_CLIMATE_HUMIDITY_ATTRIBUTE: "current_humidity" # HA attribute for humidity; leave as-is if unavailable + + + # ============================================================ + # TILE 3 + # ============================================================ + + # --- basic settings --- + TILE3_ENTITY: "scene.voorbeeld_avond" # main Home Assistant entity for this tile + TILE3_STATE_ENTITY: "scene.voorbeeld_avond" # entity used for active/inactive state styling + TILE3_TITLE: "Avond" # tile title shown in the UI + TILE3_ICON: "\U000F02E3" # MDI glyph — also add to MDI_GLYPH_* list above if not already there + TILE3_TYPE: "scene" # tile type: light | fan | switch | scene | script | cover | climate + TILE3_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset | custom + TILE3_LONGPRESS: "none" # long press mode: none | slider | action + TILE3_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text + TILE3_LABEL_OFF: "Uit" # text shown when entity is off + TILE3_LABEL_ON: "Aan" # text shown for text-mode tiles when entity is on + TILE3_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") + + # --- colors --- + TILE3_CIRCLE_ACTIVE_COLOR: "${ACCENT_AUTO}" # icon circle color when active + TILE3_CIRCLE_DISABLED_COLOR: "0x1B2D3C" # icon circle color when inactive + TILE3_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active + TILE3_ICON_DISABLED_COLOR: "${ACCENT_AUTO}" # icon color when inactive + TILE3_BG_ACTIVE_COLOR: "0xF5F3EB" # tile background color when active + TILE3_BG_DISABLED_COLOR: "0x293F50" # tile background color when inactive + TILE3_TITLE_ACTIVE_COLOR: "0x142334" # title color when active + TILE3_TITLE_DISABLED_COLOR: "0xFFFFFF" # title color when inactive + TILE3_VALUE_ACTIVE_COLOR: "0x526477" # value color when active + TILE3_VALUE_DISABLED_COLOR: "0xB9CDDB" # value color when inactive + + # --- advanced settings --- + TILE3_TAP_SERVICE: "" # optional service override, e.g. light.toggle + TILE3_TAP_PARAM_KEY: "" # optional extra service parameter key + TILE3_TAP_PARAM_VAL: "" # optional extra service parameter value + # if LONGPRESS = slider: + TILE3_LONGPRESS_SLIDER: "auto" # slider type: auto | brightness | percentage | cover_position + TILE3_LONGPRESS_OFF_VALUE: "0" # initial slider value when entity is off + # if LONGPRESS = action: + TILE3_LONGPRESS_ACTION: "" # entity_id to fire on long press + TILE3_LONGPRESS_ACTION_TYPE: "" # entity type: light | fan | switch | scene | script | cover | climate (empty = same as TILE*_TYPE) + TILE3_LONGPRESS_ACTION_SERVICE: "" # service to call (default: activate/turn_on — set e.g. "light.toggle" to toggle instead) + # if TYPE = climate: + TILE3_CLIMATE_MIN_TEMP: "5.0" # minimum target temperature shown in the thermostat overlay + TILE3_CLIMATE_MAX_TEMP: "30.0" # maximum target temperature shown in the thermostat overlay + TILE3_CLIMATE_STEP: "0.5" # target temperature step size + TILE3_CLIMATE_CURRENT_TEMP_ATTRIBUTE: "current_temperature" # HA attribute for current room temperature + TILE3_CLIMATE_TARGET_TEMP_ATTRIBUTE: "temperature" # HA attribute for target temperature + TILE3_CLIMATE_HUMIDITY_ATTRIBUTE: "current_humidity" # HA attribute for humidity; leave as-is if unavailable + + + # ============================================================ + # TILE 4 + # ============================================================ + + # --- basic settings --- + TILE4_ENTITY: "script.voorbeeld_uit" # main Home Assistant entity for this tile + TILE4_STATE_ENTITY: "script.voorbeeld_uit" # entity used for active/inactive state styling + TILE4_TITLE: "Alles uit" # tile title shown in the UI + TILE4_ICON: "\U000F059C" # MDI glyph — also add to MDI_GLYPH_* list above if not already there + TILE4_TYPE: "script" # tile type: light | fan | switch | scene | script | cover | climate + TILE4_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset | custom + TILE4_LONGPRESS: "none" # long press mode: none | slider | action + TILE4_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text + TILE4_LABEL_OFF: "Uit" # text shown when entity is off + TILE4_LABEL_ON: "Aan" # text shown for text-mode tiles when entity is on + TILE4_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") + + # --- colors --- + TILE4_CIRCLE_ACTIVE_COLOR: "${ACCENT_AUTO}" # icon circle color when active + TILE4_CIRCLE_DISABLED_COLOR: "0x1B2D3C" # icon circle color when inactive + TILE4_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active + TILE4_ICON_DISABLED_COLOR: "${ACCENT_AUTO}" # icon color when inactive + TILE4_BG_ACTIVE_COLOR: "0xF5F3EB" # tile background color when active + TILE4_BG_DISABLED_COLOR: "0x293F50" # tile background color when inactive + TILE4_TITLE_ACTIVE_COLOR: "0x142334" # title color when active + TILE4_TITLE_DISABLED_COLOR: "0xFFFFFF" # title color when inactive + TILE4_VALUE_ACTIVE_COLOR: "0x526477" # value color when active + TILE4_VALUE_DISABLED_COLOR: "0xB9CDDB" # value color when inactive + + # --- advanced settings --- + TILE4_TAP_SERVICE: "" # optional service override, e.g. light.toggle + TILE4_TAP_PARAM_KEY: "" # optional extra service parameter key + TILE4_TAP_PARAM_VAL: "" # optional extra service parameter value + # if LONGPRESS = slider: + TILE4_LONGPRESS_SLIDER: "auto" # slider type: auto | brightness | percentage | cover_position + TILE4_LONGPRESS_OFF_VALUE: "0" # initial slider value when entity is off + # if LONGPRESS = action: + TILE4_LONGPRESS_ACTION: "" # entity_id to fire on long press + TILE4_LONGPRESS_ACTION_TYPE: "" # entity type: light | fan | switch | scene | script | cover | climate (empty = same as TILE*_TYPE) + TILE4_LONGPRESS_ACTION_SERVICE: "" # service to call (default: activate/turn_on — set e.g. "light.toggle" to toggle instead) + # if TYPE = climate: + TILE4_CLIMATE_MIN_TEMP: "5.0" # minimum target temperature shown in the thermostat overlay + TILE4_CLIMATE_MAX_TEMP: "30.0" # maximum target temperature shown in the thermostat overlay + TILE4_CLIMATE_STEP: "0.5" # target temperature step size + TILE4_CLIMATE_CURRENT_TEMP_ATTRIBUTE: "current_temperature" # HA attribute for current room temperature + TILE4_CLIMATE_TARGET_TEMP_ATTRIBUTE: "temperature" # HA attribute for target temperature + TILE4_CLIMATE_HUMIDITY_ATTRIBUTE: "current_humidity" # HA attribute for humidity; leave as-is if unavailable + + + # ============================================================ + # TILE 5 + # ============================================================ + + # --- basic settings --- + TILE5_ENTITY: "climate.voorbeeld_airco" # main Home Assistant entity for this tile + TILE5_STATE_ENTITY: "climate.voorbeeld_airco" # entity used for active/inactive state styling + TILE5_TITLE: "Airco" # tile title shown in the UI + TILE5_ICON: "\U000F0425" # MDI glyph — also add to MDI_GLYPH_* list above if not already there + TILE5_TYPE: "climate" # tile type: light | fan | switch | scene | script | cover | climate + TILE5_TAP_ACTION: "toggle" # tap action: auto | toggle | activate | fan_toggle_preset | custom + TILE5_LONGPRESS: "slider" # long press mode: none | slider | action + TILE5_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text + TILE5_LABEL_OFF: "Uit" # text shown when entity is off (closed) + TILE5_LABEL_ON: "Aan" # text shown when entity is on (open) + TILE5_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") + + # --- colors --- + TILE5_CIRCLE_ACTIVE_COLOR: "${ACCENT_AUTO}" # icon circle color when active + TILE5_CIRCLE_DISABLED_COLOR: "0x1B2D3C" # icon circle color when inactive + TILE5_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active + TILE5_ICON_DISABLED_COLOR: "${ACCENT_AUTO}" # icon color when inactive + TILE5_BG_ACTIVE_COLOR: "0xF5F3EB" # tile background color when active + TILE5_BG_DISABLED_COLOR: "0x293F50" # tile background color when inactive + TILE5_TITLE_ACTIVE_COLOR: "0x142334" # title color when active + TILE5_TITLE_DISABLED_COLOR: "0xFFFFFF" # title color when inactive + TILE5_VALUE_ACTIVE_COLOR: "0x526477" # value color when active + TILE5_VALUE_DISABLED_COLOR: "0xB9CDDB" # value color when inactive + + # --- advanced settings --- + TILE5_TAP_SERVICE: "climate.toggle" # optional service override, e.g. light.toggle + TILE5_TAP_PARAM_KEY: "" # optional extra service parameter key + TILE5_TAP_PARAM_VAL: "" # optional extra service parameter value + # if LONGPRESS = slider: + TILE5_LONGPRESS_SLIDER: "auto" # slider type: auto | brightness | percentage | cover_position + TILE5_LONGPRESS_OFF_VALUE: "0" # initial slider value when entity is off + # if LONGPRESS = action: + TILE5_LONGPRESS_ACTION: "" # entity_id to fire on long press + TILE5_LONGPRESS_ACTION_TYPE: "" # entity type: light | fan | switch | scene | script | cover | climate (empty = same as TILE*_TYPE) + TILE5_LONGPRESS_ACTION_SERVICE: "" # service to call (default: activate/turn_on — set e.g. "light.toggle" to toggle instead) + # if TYPE = climate: + TILE5_CLIMATE_MIN_TEMP: "5.0" # minimum target temperature shown in the thermostat overlay + TILE5_CLIMATE_MAX_TEMP: "30.0" # maximum target temperature shown in the thermostat overlay + TILE5_CLIMATE_STEP: "0.5" # target temperature step size + TILE5_CLIMATE_CURRENT_TEMP_ATTRIBUTE: "current_temperature" # HA attribute for current room temperature + TILE5_CLIMATE_TARGET_TEMP_ATTRIBUTE: "temperature" # HA attribute for target temperature + TILE5_CLIMATE_HUMIDITY_ATTRIBUTE: "current_humidity" # HA attribute for humidity; leave as-is if unavailable + + + # ============================================================ + # TILE 6 + # ============================================================ + + # --- basic settings --- + TILE6_ENTITY: "vacuum.voorbeeld_robot" # main Home Assistant entity for this tile + TILE6_STATE_ENTITY: "vacuum.voorbeeld_robot" # entity used for active/inactive state styling + TILE6_TITLE: "Stofzuiger" # tile title shown in the UI + TILE6_ICON: "\U000F070D" # MDI glyph — also add to MDI_GLYPH_* list above if not already there + TILE6_TYPE: "vacuum" # tile type: light | fan | switch | scene | script | cover | climate + TILE6_TAP_ACTION: "custom" # tap action: auto | toggle | activate | fan_toggle_preset | custom + TILE6_LONGPRESS: "vacuum" # long press mode: none | slider | action + TILE6_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text + TILE6_LABEL_OFF: "Uit" # text shown when entity is off + TILE6_LABEL_ON: "Aan" # text shown for text-mode tiles when entity is on + TILE6_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") + + # --- colors --- + TILE6_CIRCLE_ACTIVE_COLOR: "${ACCENT_AUTO}" # icon circle color when active + TILE6_CIRCLE_DISABLED_COLOR: "0x1B2D3C" # icon circle color when inactive + TILE6_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active + TILE6_ICON_DISABLED_COLOR: "${ACCENT_AUTO}" # icon color when inactive + TILE6_BG_ACTIVE_COLOR: "0xF5F3EB" # tile background color when active + TILE6_BG_DISABLED_COLOR: "0x293F50" # tile background color when inactive + TILE6_TITLE_ACTIVE_COLOR: "0x142334" # title color when active + TILE6_TITLE_DISABLED_COLOR: "0xFFFFFF" # title color when inactive + TILE6_VALUE_ACTIVE_COLOR: "0x526477" # value color when active + TILE6_VALUE_DISABLED_COLOR: "0xB9CDDB" # value color when inactive + + # --- advanced settings --- + TILE6_TAP_SERVICE: "vacuum.start" # optional service override, e.g. fan.toggle + TILE6_TAP_PARAM_KEY: "" # optional extra service parameter key + TILE6_TAP_PARAM_VAL: "" # optional extra service parameter value + # if LONGPRESS = slider: + TILE6_LONGPRESS_SLIDER: "auto" # slider type: auto | brightness | percentage | cover_position + TILE6_LONGPRESS_OFF_VALUE: "0" # initial slider value when entity is off + # if LONGPRESS = action: + TILE6_LONGPRESS_ACTION: "" # entity_id to fire on long press + TILE6_LONGPRESS_ACTION_TYPE: "" # entity type: light | fan | switch | scene | script | cover | climate (empty = same as TILE*_TYPE) + TILE6_LONGPRESS_ACTION_SERVICE: "" # service to call (default: activate/turn_on — set e.g. "light.toggle" to toggle instead) + # if TYPE = climate: + TILE6_CLIMATE_MIN_TEMP: "5.0" # minimum target temperature shown in the thermostat overlay + TILE6_CLIMATE_MAX_TEMP: "30.0" # maximum target temperature shown in the thermostat overlay + TILE6_CLIMATE_STEP: "0.5" # target temperature step size + TILE6_CLIMATE_CURRENT_TEMP_ATTRIBUTE: "current_temperature" # HA attribute for current room temperature + TILE6_CLIMATE_TARGET_TEMP_ATTRIBUTE: "temperature" # HA attribute for target temperature + TILE6_CLIMATE_HUMIDITY_ATTRIBUTE: "current_humidity" # HA attribute for humidity; leave as-is if unavailable + # ============================================================ + # TILE 7 — Tafellamp + # Alleen-helderheid lamp (HA meldt supported_color_modes: ['brightness']), + # dus de kleurenknop in de slider-overlay blijft vanzelf verborgen. + # ============================================================ + + # --- basic settings --- + TILE7_ENTITY: "light.ongebruikt_7" # main Home Assistant entity for this tile + TILE7_STATE_ENTITY: "light.ongebruikt_7" # entity used for active/inactive state styling + TILE7_TITLE: "Tegel 7" # tile title shown in the UI + TILE7_ICON: "\U000F06B5" # mdi:lamp — also listed in MDI_GLYPH_7 above + TILE7_TYPE: "light" # tile type: light | fan | switch | scene | script | cover | climate + TILE7_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset + TILE7_LONGPRESS: "none" # long press mode: none | slider | action | vacuum + TILE7_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text + TILE7_LABEL_OFF: "Uit" # text shown when entity is off + TILE7_LABEL_ON: "Aan" # text shown for text-mode tiles when entity is on + TILE7_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") + + # --- colors --- + TILE7_CIRCLE_ACTIVE_COLOR: "${ACCENT_AUTO}" # icon circle color when active + TILE7_CIRCLE_DISABLED_COLOR: "0x1B2D3C" # icon circle color when inactive + TILE7_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active + TILE7_ICON_DISABLED_COLOR: "${ACCENT_AUTO}" # icon color when inactive + TILE7_BG_ACTIVE_COLOR: "0xF5F3EB" # tile background color when active + TILE7_BG_DISABLED_COLOR: "0x293F50" # tile background color when inactive + TILE7_TITLE_ACTIVE_COLOR: "0x142334" # title color when active + TILE7_TITLE_DISABLED_COLOR: "0xFFFFFF" # title color when inactive + TILE7_VALUE_ACTIVE_COLOR: "0x526477" # value color when active + TILE7_VALUE_DISABLED_COLOR: "0xB9CDDB" # value color when inactive + + # --- advanced settings --- + TILE7_TAP_SERVICE: "" # optional service override, e.g. light.toggle + TILE7_TAP_PARAM_KEY: "" # optional extra service parameter key + TILE7_TAP_PARAM_VAL: "" # optional extra service parameter value + # if LONGPRESS = slider: + TILE7_LONGPRESS_SLIDER: "auto" # auto = brightness voor een light + TILE7_LONGPRESS_OFF_VALUE: "0" # initial slider value when entity is off + # if LONGPRESS = action: + TILE7_LONGPRESS_ACTION: "" # entity_id to fire on long press + TILE7_LONGPRESS_ACTION_TYPE: "" # entity type (empty = same as TILE*_TYPE) + TILE7_LONGPRESS_ACTION_SERVICE: "" # service to call (default: activate/turn_on) + + # ============================================================ + # TILE 8 — Brievenbus reset + # input_select.brievenbus_status heeft opties ['Vol', 'Leeg']. + # De tegel licht op zodra de status "Vol" is (ACTIVE_STATE), en een korte + # tik zet hem terug op "Leeg" via input_select.select_option. + # ============================================================ + + # --- basic settings --- + TILE8_ENTITY: "light.ongebruikt_8" # main Home Assistant entity for this tile + TILE8_STATE_ENTITY: "light.ongebruikt_8" # entity used for active/inactive state styling + TILE8_TITLE: "Tegel 8" # tile title shown in the UI + TILE8_ICON: "\U000F06EE" # mdi:mailbox — also listed in MDI_GLYPH_8 above + TILE8_TYPE: "light" # generiek type; het echte gedrag komt uit TAP_SERVICE hieronder + TILE8_TAP_ACTION: "auto" # activate + TAP_SERVICE = losse service call + TILE8_LONGPRESS: "none" # long press stuurt alleen het event naar HA + TILE8_VALUE_MODE: "auto" # toont LABEL_ON / LABEL_OFF + TILE8_LABEL_OFF: "Uit" # text shown when entity is off / inactive + TILE8_LABEL_ON: "Aan" # text shown when entity is active + TILE8_ACTIVE_STATE: "" # tegel is "aan" zodra de status exact "Vol" is + + # --- colors --- + TILE8_CIRCLE_ACTIVE_COLOR: "0xE5484D" # rood zodra er post ligt + TILE8_CIRCLE_DISABLED_COLOR: "0x1B2D3C" # 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: "0xF5F3EB" # tile background color when active + TILE8_BG_DISABLED_COLOR: "0x293F50" # tile background color when inactive + TILE8_TITLE_ACTIVE_COLOR: "0x142334" # title color when active + TILE8_TITLE_DISABLED_COLOR: "0xFFFFFF" # title color when inactive + TILE8_VALUE_ACTIVE_COLOR: "0x526477" # value color when active + TILE8_VALUE_DISABLED_COLOR: "0xB9CDDB" # value color when inactive + + # --- advanced settings --- + TILE8_TAP_SERVICE: "" # zet de input_select op een vaste optie + TILE8_TAP_PARAM_KEY: "" # parameternaam voor input_select.select_option + TILE8_TAP_PARAM_VAL: "" # de "reset"-waarde + # if LONGPRESS = slider: + TILE8_LONGPRESS_SLIDER: "auto" # niet in gebruik (LONGPRESS = none) + TILE8_LONGPRESS_OFF_VALUE: "0" # niet in gebruik + # if LONGPRESS = action: + TILE8_LONGPRESS_ACTION: "" # entity_id to fire on long press + TILE8_LONGPRESS_ACTION_TYPE: "" # entity type (empty = same as TILE*_TYPE) + TILE8_LONGPRESS_ACTION_SERVICE: "" # service to call (default: activate/turn_on) + + # ============================================================ + # TILE 9 — Woonkamer + # Verhuisd van tegel 2 naar rij 5 om plaats te maken voor Ambient. + # Kleurenlamp (color_temp / rgb / xy), dus de kleurknop in de + # slider-overlay verschijnt vanzelf. + # ============================================================ + + # --- basic settings --- + TILE9_ENTITY: "light.ongebruikt_9" # main Home Assistant entity for this tile + TILE9_STATE_ENTITY: "light.ongebruikt_9" # entity used for active/inactive state styling + TILE9_TITLE: "Tegel 9" # tile title shown in the UI + TILE9_ICON: "\U000F04B9" # mdi:sofa — also listed in MDI_GLYPH_9 above + TILE9_TYPE: "light" # tile type: light | fan | switch | scene | script | cover | climate + TILE9_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset + TILE9_LONGPRESS: "none" # long press mode: none | slider | action | vacuum + TILE9_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text + TILE9_LABEL_OFF: "Uit" # text shown when entity is off + TILE9_LABEL_ON: "Aan" # text shown for text-mode tiles when entity is on + TILE9_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") + + # --- colors --- + TILE9_CIRCLE_ACTIVE_COLOR: "${ACCENT_AUTO}" # light -> ACCENT_LIGHT (amber) + TILE9_CIRCLE_DISABLED_COLOR: "0x1B2D3C" # icon circle color when inactive + TILE9_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active + TILE9_ICON_DISABLED_COLOR: "${ACCENT_AUTO}" # icon color when inactive + TILE9_BG_ACTIVE_COLOR: "0xF5F3EB" # tile background color when active + TILE9_BG_DISABLED_COLOR: "0x293F50" # tile background color when inactive + TILE9_TITLE_ACTIVE_COLOR: "0x142334" # title color when active + TILE9_TITLE_DISABLED_COLOR: "0xFFFFFF" # title color when inactive + TILE9_VALUE_ACTIVE_COLOR: "0x526477" # value color when active + TILE9_VALUE_DISABLED_COLOR: "0xB9CDDB" # value color when inactive + + # --- advanced settings --- + TILE9_TAP_SERVICE: "" # optional service override, e.g. light.toggle + TILE9_TAP_PARAM_KEY: "" # optional extra service parameter key + TILE9_TAP_PARAM_VAL: "" # optional extra service parameter value + # if LONGPRESS = slider: + TILE9_LONGPRESS_SLIDER: "auto" # auto = brightness voor een light + TILE9_LONGPRESS_OFF_VALUE: "0" # initial slider value when entity is off + # if LONGPRESS = action: + TILE9_LONGPRESS_ACTION: "" # entity_id to fire on long press + TILE9_LONGPRESS_ACTION_TYPE: "" # entity type (empty = same as TILE*_TYPE) + TILE9_LONGPRESS_ACTION_SERVICE: "" # service to call (default: activate/turn_on) + + # ============================================================ + # TILE 10 — Alarm aan/uit + # input_boolean.alarm_engaged. Een input_boolean reageert niet op + # switch.toggle, vandaar de expliciete TAP_SERVICE. + # ============================================================ + + # --- basic settings --- + TILE10_ENTITY: "light.ongebruikt_10" # main Home Assistant entity for this tile + TILE10_STATE_ENTITY: "light.ongebruikt_10" # entity used for active/inactive state styling + TILE10_TITLE: "Tegel 10" # tile title shown in the UI + TILE10_ICON: "\U000F002A" # mdi:alarm-light — also listed in MDI_GLYPH_10 above + TILE10_TYPE: "light" # generiek aan/uit-type; de service hieronder doet het echte werk + TILE10_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset + TILE10_LONGPRESS: "none" # long press stuurt alleen het event naar HA + TILE10_VALUE_MODE: "auto" # toont LABEL_ON / LABEL_OFF + TILE10_LABEL_OFF: "Uit" # text shown when entity is off + TILE10_LABEL_ON: "Aan" # text shown when entity is on + TILE10_ACTIVE_STATE: "" # input_boolean gebruikt gewoon on/off + + # --- colors --- + TILE10_CIRCLE_ACTIVE_COLOR: "${ACCENT_AUTO}" # input_boolean -> ACCENT_DEVICE (cyaan) + TILE10_CIRCLE_DISABLED_COLOR: "0x1B2D3C" # icon circle color when inactive + TILE10_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active + TILE10_ICON_DISABLED_COLOR: "${ACCENT_AUTO}" # icon color when inactive + TILE10_BG_ACTIVE_COLOR: "0xF5F3EB" # tile background color when active + TILE10_BG_DISABLED_COLOR: "0x293F50" # tile background color when inactive + TILE10_TITLE_ACTIVE_COLOR: "0x142334" # title color when active + TILE10_TITLE_DISABLED_COLOR: "0xFFFFFF" # title color when inactive + TILE10_VALUE_ACTIVE_COLOR: "0x526477" # value color when active + TILE10_VALUE_DISABLED_COLOR: "0xB9CDDB" # value color when inactive + + # --- advanced settings --- + TILE10_TAP_SERVICE: "" # switch.toggle werkt niet op een input_boolean + TILE10_TAP_PARAM_KEY: "" # optional extra service parameter key + TILE10_TAP_PARAM_VAL: "" # optional extra service parameter value + # if LONGPRESS = slider: + TILE10_LONGPRESS_SLIDER: "auto" # niet in gebruik (LONGPRESS = none) + TILE10_LONGPRESS_OFF_VALUE: "0" # niet in gebruik + # if LONGPRESS = action: + TILE10_LONGPRESS_ACTION: "" # entity_id to fire on long press + TILE10_LONGPRESS_ACTION_TYPE: "" # entity type (empty = same as TILE*_TYPE) + TILE10_LONGPRESS_ACTION_SERVICE: "" # service to call (default: activate/turn_on) + + + # Square 480x480 layout; GT911 coordinates are already in pixels. + ORIENTATION: "0" + DISPLAY_W: "480" + DISPLAY_H: "480" + LVGL_ROTATION: "0" + TOUCH_SWAP_XY: "false" + TOUCH_MIRROR_X: "true" + TOUCH_MIRROR_Y: "true" + GRID_COL1_X: "16" + GRID_COL2_X: "246" + GRID_ROW1_Y: "0" + GRID_ROW2_Y: "120" + GRID_ROW3_Y: "240" + GRID_ROW4_Y: "360" + GRID_ROW5_Y: "480" + SCROLL_Y: "72" + SCROLL_H: "348" + TILE_W: "218" + TILE_H: "108" + HEADER_Y: "22" + OVERLAY_SLIDER_W: "144" + OVERLAY_SLIDER_H: "300" + OVERLAY_ICON_Y: "148" + BG_TOP_COLOR: "0x111D2B" + BG_BOTTOM_COLOR: "0x28485A" + TILE_ICON_X: "0" + TILE_ICON_Y: "12" + TILE_TITLE_X: "64" + TILE_TITLE_Y: "17" + TILE_VALUE_X: "64" + TILE_VALUE_Y: "45" + + +# ============================================================ +# 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: + platformio_options: + build_flags: + - -DLV_USE_SNAPSHOT=1 + includes: + - cyd_ui.h + - guition_diagnostics.h + name: ${DEVICE_NAME} + friendly_name: ${DEVICE_FRIENDLY_NAME} + on_boot: + priority: -100 + then: + - lambda: |- + id(last_touch_ms) = millis(); + id(display_dimmed) = false; + lv_obj_set_style_pad_row(id(vacuum_actions), 4, 0); + lv_obj_set_style_pad_column(id(vacuum_actions), 4, 0); + lv_obj_set_style_pad_row(id(vacuum_speeds), 4, 0); + lv_obj_set_style_pad_column(id(vacuum_speeds), 4, 0); + - light.turn_on: + id: back_light + brightness: !lambda "return atoi(\"${MAX_BRIGHTNESS}\") / 100.0f;" + - script.execute: show_tile_page + - lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' + - if: + condition: + lambda: 'return std::string("${CALIBRATION_ON_BOOT}") == "true";' + then: + - lambda: 'id(calibration_active) = true;' + - lvgl.page.show: touch_cal_page + - lambda: |- + lv_obj_invalidate(id(touch_cal_page)->obj); + lv_refr_now(nullptr); + ESP_LOGI("ui_diag", "GT911 touch test ready; no tile actions"); + + +esp32: + board: esp32-s3-devkitc-1 + variant: esp32s3 + flash_size: 16MB + cpu_frequency: 240MHz + framework: + type: esp-idf + version: recommended + advanced: + execute_from_psram: true + sdkconfig_options: + CONFIG_ESP32S3_DATA_CACHE_64KB: "y" + CONFIG_ESP32S3_DATA_CACHE_LINE_64B: "y" + CONFIG_LCD_RGB_RESTART_IN_VSYNC: "y" + +psram: + mode: octal + speed: 80MHz + +logger: + tx_buffer_size: 2048 + hardware_uart: UART0 + # Voor diagnose tijdelijk op DEBUG of VERBOSE zetten. Let op: ESPHome logt + # zijn configuratieregels op niveau CONFIG, die zijn op INFO onzichtbaar. + level: DEBUG + logs: + sensor: WARN + text_sensor: WARN + xpt2046: WARN + homeassistant.sensor: WARN + homeassistant.text_sensor: WARN + + +# Credentials are loaded from secrets.yaml — copy secrets.yaml.example to secrets.yaml and fill in your values. +api: + actions: + - action: ui_snapshot + then: + - script.execute: capture_ui_snapshot + - action: touch_diagnostics + then: + - lambda: 'id(calibration_active) = true;' + - script.execute: wake_display + - lvgl.page.show: touch_cal_page + - lambda: |- + lv_obj_invalidate(id(touch_cal_page)->obj); + lv_refr_now(nullptr); + ESP_LOGI("ui_diag", "calibration page active=%d", lv_screen_active() == id(touch_cal_page)->obj); + - action: end_touch_diagnostics + then: + - lambda: 'id(calibration_active) = false;' + - lvgl.page.show: home_page + - action: ui_state + then: + - lambda: |- + lv_area_t nav, grid; + lv_obj_update_layout(id(home_page)->obj); + lv_obj_get_coords(id(page_next), &nav); + lv_obj_get_coords(id(tile_scroll), &grid); + ESP_LOGI("ui_diag", "home=%d calibration=%d grid=%d,%d,%d,%d next=%d,%d,%d,%d", + lv_screen_active() == id(home_page)->obj, lv_screen_active() == id(touch_cal_page)->obj, + grid.x1, grid.y1, grid.x2, grid.y2, nav.x1, nav.y1, nav.x2, nav.y2); + - action: ui_self_test + then: + - script.execute: ui_self_test + encryption: + key: !secret api_encryption_key + +ota: + - platform: esphome + password: !secret ota_password + +wifi: + ssid: !secret wifi_ssid + password: !secret wifi_password + ap: + ssid: "Smartdisplay Fallback Hotspot" + password: !secret ap_password + +captive_portal: + +interval: + - interval: 1s + then: + - if: + condition: + lambda: 'return id(climate_edit_pending) && !id(touch_down) && millis() - id(climate_edit_ms) >= 3000;' + then: + - lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' + - if: + condition: + lambda: |- + if (std::string("${AUTO_DIM}") != "true") return false; + if (id(display_dimmed) || id(touch_down) || id(calibration_active)) return false; + return (millis() - id(last_touch_ms)) > (atoi("${AUTO_DIM_TIMEOUT}") * 1000UL); + then: + - script.execute: dim_display + +time: + - platform: homeassistant + id: homeassistant_time + # Refresh immediately on time sync; do not wait for an HA entity update. + update_interval: 60s + on_time_sync: + then: + - lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' + on_time: + - seconds: 0 + minutes: /1 + then: + - lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' + + - 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: rendered_frames + type: uint32_t + initial_value: '0' + - id: calibration_active + type: bool + initial_value: 'false' + - id: tile_page + type: int + initial_value: '0' + - id: touch_down + type: bool + initial_value: 'false' + - id: climate_edit_ms + type: uint32_t + initial_value: '0' + - id: climate_edit_pending + type: bool + initial_value: 'false' + - id: active_entity + type: std::string + restore_value: no + + - id: active_control_kind + type: std::string + restore_value: no + + - id: active_value_suffix + type: std::string + restore_value: no + + - id: last_touch_ms + type: uint32_t + restore_value: no + initial_value: '0' + + # Debounce voor do_tile_action: bescherm tegen herhaalde events bij + # meerdere losse press/release-cycli voor één fysieke aanraking (ruis in + # het weerstandslaagje), waardoor dezelfde HA-actie meerdere keren binnen + # een paar honderd ms vuurt (gezien: script.ambient_morning 6x in 8s). + - id: last_action_entity + type: std::string + restore_value: no + + - id: last_action_ms + type: uint32_t + restore_value: no + initial_value: '0' + + - id: display_dimmed + type: bool + restore_value: no + initial_value: 'false' + + - id: active_label_off + type: std::string + restore_value: no + + - id: active_label_on + type: std::string + restore_value: no + + - id: active_title + type: std::string + restore_value: no + + - id: active_icon_text + type: std::string + restore_value: no + + - id: active_brightness_pct + type: int + restore_value: no + initial_value: '100' + + - id: active_supports_color + type: bool + restore_value: no + initial_value: 'false' + + - id: active_supports_temperature + type: bool + restore_value: no + initial_value: 'false' + + - id: active_color_detail_tab + type: int + restore_value: no + initial_value: '0' + + - id: active_climate_current_x10 + type: int + restore_value: no + initial_value: '0' + + - id: active_climate_target_x10 + type: int + restore_value: no + initial_value: '200' + + - id: active_climate_humidity + type: int + restore_value: no + initial_value: '-1' + + - id: active_climate_min_x10 + type: int + restore_value: no + initial_value: '50' + + - id: active_climate_max_x10 + type: int + restore_value: no + initial_value: '300' + + - id: active_climate_step_x10 + type: int + restore_value: no + initial_value: '5' + + - id: active_climate_mode + type: std::string + restore_value: no + + - id: active_climate_supported_modes + type: std::string + restore_value: no + +font: + - file: "fonts/Roboto-700.ttf" + id: climate_number + size: 46 + bpp: 4 + glyphs: "0123456789.-° " + - file: "fonts/Roboto-500.ttf" + id: time_label + size: 27 + bpp: 4 + glyphs: ['0','1','2','3','4','5','6','7','8','9',':',' ', 'A', 'M', 'P'] + + - file: "fonts/Roboto-500.ttf" + id: headline + size: 27 + bpp: 4 + glyphs: ['<','>','—','&','@','!',',','.','?','"','%','(',')','+','-','_',':','°', + '0','1','2','3','4','5','6','7','8','9', + 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z', + ' ','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z', + 'ä','ö','ü','Ä','Ö','Ü','ß','/'] + + - file: "fonts/Roboto-700.ttf" + id: label + size: 18 + bpp: 4 + glyphs: ['<','>','—','&','@','!',',','.','?','"','%','(',')','+','-','_',':','°', + '0','1','2','3','4','5','6','7','8','9', + 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z', + ' ','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z', + 'ä','ö','ü','Ä','Ö','Ü','ß','/'] + + - file: "fonts/Roboto-400.ttf" + id: sublabel + 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: "fonts/Roboto-400.ttf" + id: sublabel_big + size: 21 + 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: 42 + 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}" + - "${MDI_GLYPH_9}" + - "${MDI_GLYPH_10}" + - "\U000F012C" # check + +# Geheugen zichtbaar maken: dit bord heeft geen PSRAM, dus de vrije heap +# bepaalt hoeveel UI erbij kan. Verschijnt ook als entity in Home Assistant. +debug: + update_interval: 30s + +sensor: + - platform: debug + free: + name: "Heap Free" + id: heap_free + on_value: + - lambda: 'ESP_LOGI("health", "free_heap=%.0f", x);' + fragmentation: + name: "Heap Fragmentation" + block: + name: "Heap Largest Block" + on_value: + - lambda: 'ESP_LOGI("health", "largest_block=%.0f", x);' + - platform: wifi_signal + name: "Wifi Signal" + update_interval: 600s + + - platform: uptime + name: "Uptime" + id: uptime_s + update_interval: 15s + + - platform: homeassistant + id: tile1_brightness + entity_id: ${TILE1_ENTITY} + attribute: brightness + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile1_percentage + entity_id: ${TILE1_ENTITY} + attribute: percentage + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile2_brightness + entity_id: ${TILE2_ENTITY} + attribute: brightness + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile2_percentage + entity_id: ${TILE2_ENTITY} + attribute: percentage + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile3_brightness + entity_id: ${TILE3_ENTITY} + attribute: brightness + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile3_percentage + entity_id: ${TILE3_ENTITY} + attribute: percentage + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile4_brightness + entity_id: ${TILE4_ENTITY} + attribute: brightness + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile4_percentage + entity_id: ${TILE4_ENTITY} + attribute: percentage + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile5_brightness + entity_id: ${TILE5_ENTITY} + attribute: brightness + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile5_percentage + entity_id: ${TILE5_ENTITY} + attribute: percentage + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile6_brightness + entity_id: ${TILE6_ENTITY} + attribute: brightness + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile6_percentage + entity_id: ${TILE6_ENTITY} + attribute: percentage + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + # 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: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + # Tegel 9 (Woonkamer) is een kleurenlamp: brightness + supported_color_modes. + # Geen percentage / cover / climate — die leest deze tegel nooit. + - platform: homeassistant + id: tile9_brightness + entity_id: ${TILE9_ENTITY} + attribute: brightness + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile1_cover_position + entity_id: ${TILE1_ENTITY} + attribute: current_position + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile2_cover_position + entity_id: ${TILE2_ENTITY} + attribute: current_position + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile3_cover_position + entity_id: ${TILE3_ENTITY} + attribute: current_position + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile4_cover_position + entity_id: ${TILE4_ENTITY} + attribute: current_position + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile5_cover_position + entity_id: ${TILE5_ENTITY} + attribute: current_position + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile6_cover_position + entity_id: ${TILE6_ENTITY} + attribute: current_position + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile1_climate_current_temperature + entity_id: ${TILE1_ENTITY} + attribute: ${TILE1_CLIMATE_CURRENT_TEMP_ATTRIBUTE} + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile1_climate_target_temperature + entity_id: ${TILE1_ENTITY} + attribute: ${TILE1_CLIMATE_TARGET_TEMP_ATTRIBUTE} + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile1_climate_humidity + entity_id: ${TILE1_ENTITY} + attribute: ${TILE1_CLIMATE_HUMIDITY_ATTRIBUTE} + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile2_climate_current_temperature + entity_id: ${TILE2_ENTITY} + attribute: ${TILE2_CLIMATE_CURRENT_TEMP_ATTRIBUTE} + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile2_climate_target_temperature + entity_id: ${TILE2_ENTITY} + attribute: ${TILE2_CLIMATE_TARGET_TEMP_ATTRIBUTE} + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile2_climate_humidity + entity_id: ${TILE2_ENTITY} + attribute: ${TILE2_CLIMATE_HUMIDITY_ATTRIBUTE} + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile3_climate_current_temperature + entity_id: ${TILE3_ENTITY} + attribute: ${TILE3_CLIMATE_CURRENT_TEMP_ATTRIBUTE} + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile3_climate_target_temperature + entity_id: ${TILE3_ENTITY} + attribute: ${TILE3_CLIMATE_TARGET_TEMP_ATTRIBUTE} + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile3_climate_humidity + entity_id: ${TILE3_ENTITY} + attribute: ${TILE3_CLIMATE_HUMIDITY_ATTRIBUTE} + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile4_climate_current_temperature + entity_id: ${TILE4_ENTITY} + attribute: ${TILE4_CLIMATE_CURRENT_TEMP_ATTRIBUTE} + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile4_climate_target_temperature + entity_id: ${TILE4_ENTITY} + attribute: ${TILE4_CLIMATE_TARGET_TEMP_ATTRIBUTE} + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile4_climate_humidity + entity_id: ${TILE4_ENTITY} + attribute: ${TILE4_CLIMATE_HUMIDITY_ATTRIBUTE} + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile5_climate_current_temperature + entity_id: ${TILE5_ENTITY} + attribute: ${TILE5_CLIMATE_CURRENT_TEMP_ATTRIBUTE} + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile5_climate_target_temperature + entity_id: ${TILE5_ENTITY} + attribute: ${TILE5_CLIMATE_TARGET_TEMP_ATTRIBUTE} + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile5_climate_humidity + entity_id: ${TILE5_ENTITY} + attribute: ${TILE5_CLIMATE_HUMIDITY_ATTRIBUTE} + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile6_climate_current_temperature + entity_id: ${TILE6_ENTITY} + attribute: ${TILE6_CLIMATE_CURRENT_TEMP_ATTRIBUTE} + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile6_climate_target_temperature + entity_id: ${TILE6_ENTITY} + attribute: ${TILE6_CLIMATE_TARGET_TEMP_ATTRIBUTE} + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile6_climate_humidity + entity_id: ${TILE6_ENTITY} + attribute: ${TILE6_CLIMATE_HUMIDITY_ATTRIBUTE} + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + +binary_sensor: + - platform: status + name: "Node Status" + id: system_status + +# ---------------------------------------------------------- +# Text sensors +# +# smartdisplay_action: +# - Main touch event channel exported to Home Assistant +# - On short tap, publishes one of: +# tile1_press / tile2_press / tile3_press / tile4_press / tile5_press / tile6_press +# - The value is reset shortly after publishing +# +# tile*_preset: +# - Mirrors the Home Assistant preset_mode attribute for each tile entity +# - Used by the UI to display fan presets when applicable +# ---------------------------------------------------------- +text_sensor: + - platform: template + id: smartdisplay_action + name: "SmartDisplay Action" + icon: mdi:gesture-tap-button + + - platform: homeassistant + id: ha_state_tile1 + entity_id: ${TILE1_STATE_ENTITY} + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: ha_state_tile2 + entity_id: ${TILE2_STATE_ENTITY} + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: ha_state_tile3 + entity_id: ${TILE3_STATE_ENTITY} + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: ha_state_tile4 + entity_id: ${TILE4_STATE_ENTITY} + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: ha_state_tile5 + entity_id: ${TILE5_STATE_ENTITY} + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: ha_state_tile6 + entity_id: ${TILE6_STATE_ENTITY} + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: ha_state_tile7 + entity_id: ${TILE7_STATE_ENTITY} + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: ha_state_tile8 + entity_id: ${TILE8_STATE_ENTITY} + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: ha_state_tile9 + entity_id: ${TILE9_STATE_ENTITY} + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: ha_state_tile10 + entity_id: ${TILE10_STATE_ENTITY} + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + # Woonkamer meldt color_temp/rgb/xy, dus hier verschijnt de kleurknop wel. + - platform: homeassistant + id: tile9_supported_color_modes + entity_id: ${TILE9_ENTITY} + attribute: supported_color_modes + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + # 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: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile1_preset + entity_id: ${TILE1_ENTITY} + attribute: preset_mode + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile2_preset + entity_id: ${TILE2_ENTITY} + attribute: preset_mode + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile3_preset + entity_id: ${TILE3_ENTITY} + attribute: preset_mode + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile4_preset + entity_id: ${TILE4_ENTITY} + attribute: preset_mode + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile5_preset + entity_id: ${TILE5_ENTITY} + attribute: preset_mode + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile6_preset + entity_id: ${TILE6_ENTITY} + attribute: preset_mode + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile1_supported_color_modes + entity_id: ${TILE1_ENTITY} + attribute: supported_color_modes + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile2_supported_color_modes + entity_id: ${TILE2_ENTITY} + attribute: supported_color_modes + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile3_supported_color_modes + entity_id: ${TILE3_ENTITY} + attribute: supported_color_modes + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile4_supported_color_modes + entity_id: ${TILE4_ENTITY} + attribute: supported_color_modes + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile5_supported_color_modes + entity_id: ${TILE5_ENTITY} + attribute: supported_color_modes + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile6_supported_color_modes + entity_id: ${TILE6_ENTITY} + attribute: supported_color_modes + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile1_climate_mode + entity_id: ${TILE1_ENTITY} + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile2_climate_mode + entity_id: ${TILE2_ENTITY} + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile3_climate_mode + entity_id: ${TILE3_ENTITY} + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile4_climate_mode + entity_id: ${TILE4_ENTITY} + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile5_climate_mode + entity_id: ${TILE5_ENTITY} + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile6_climate_mode + entity_id: ${TILE6_ENTITY} + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile1_climate_supported_modes + entity_id: ${TILE1_ENTITY} + attribute: hvac_modes + + - platform: homeassistant + id: tile2_climate_supported_modes + entity_id: ${TILE2_ENTITY} + attribute: hvac_modes + + - platform: homeassistant + id: tile3_climate_supported_modes + entity_id: ${TILE3_ENTITY} + attribute: hvac_modes + + - platform: homeassistant + id: tile4_climate_supported_modes + entity_id: ${TILE4_ENTITY} + attribute: hvac_modes + + - platform: homeassistant + id: tile5_climate_supported_modes + entity_id: ${TILE5_ENTITY} + attribute: hvac_modes + + - platform: homeassistant + id: tile6_climate_supported_modes + entity_id: ${TILE6_ENTITY} + attribute: hvac_modes + + - platform: homeassistant + id: tile1_cover_state + entity_id: ${TILE1_STATE_ENTITY} + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile2_cover_state + entity_id: ${TILE2_STATE_ENTITY} + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile3_cover_state + entity_id: ${TILE3_STATE_ENTITY} + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile4_cover_state + entity_id: ${TILE4_STATE_ENTITY} + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile5_cover_state + entity_id: ${TILE5_STATE_ENTITY} + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + + - platform: homeassistant + id: tile6_cover_state + entity_id: ${TILE6_STATE_ENTITY} + on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } + +switch: + - platform: restart + name: "Restart" + +# Two SPI buses used by the CYD: +# - lcd: built-in display +# - my_touchscreen: built-in touch controller +# Pinout from ESPHome's GUITION-4848S040 preset. No relay outputs are configured. +spi: + id: lcd + clk_pin: GPIO48 + mosi_pin: GPIO47 + +i2c: + id: touch_bus + sda: GPIO19 + scl: + number: GPIO45 + ignore_strapping_warning: true + scan: true + +output: + - platform: ledc + pin: GPIO38 + id: gpio_backlight_pwm + frequency: 20000Hz + +light: + - platform: monochromatic + output: gpio_backlight_pwm + name: "Display Backlight" + id: back_light + restore_mode: ALWAYS_ON + default_transition_length: 150ms + +touchscreen: + platform: gt911 + id: ts_touch + i2c_id: touch_bus + display: my_display + update_interval: 20ms + transform: + swap_xy: ${TOUCH_SWAP_XY} + mirror_x: ${TOUCH_MIRROR_X} + mirror_y: ${TOUCH_MIRROR_Y} + on_touch: + - lambda: |- + id(last_touch_ms) = millis(); + id(touch_down) = true; + cyd::touch_guard.begin(millis(), touch.x, touch.y); + ESP_LOGI("touch", "GT911 press x=%u y=%u test=%d", touch.x, touch.y, id(calibration_active)); + on_update: + - lambda: |- + id(last_touch_ms) = millis(); + for (const auto &point : touches) { + cyd::touch_guard.update(point.x, point.y); + } + on_release: + - lambda: |- + id(last_touch_ms) = millis(); + id(touch_down) = false; + + +display: + - platform: mipi_rgb + id: my_display + model: GUITION-4848S040 + # 16MHz reduces visible low-refresh flicker on this physical 480x480 panel. + pclk_frequency: 16MHz + spi_id: lcd + update_interval: never + auto_clear_enabled: false + + +lvgl: + on_draw_start: + - lambda: 'id(rendered_frames)++;' + displays: + - my_display + touchscreens: + - touchscreen_id: ts_touch + + # S3 octal PSRAM holds RGB framebuffer and LVGL draw buffers. + buffer_size: 25% + + # Directe lokale feedback op de HELE UI, niet alleen op de tegels. + # Een thema zet de stijl per widget-type, dus dit dekt in een klap alle + # knoppen, tabs, sliders en de thermostaatring. Niet-klikbare widgets + # komen nooit in de pressed-state, dus die blijven onaangeroerd. + theme: + obj: + pressed: + # bg_opa en NIET opa: `opa` op widget-niveau dwingt LVGL het element + # naar een aparte laag te renderen, en die laag kost tientallen KB's + # per ingedrukte widget. Dat liep hier het geheugen uit. + bg_opa: 45% + slider: + knob: + pressed: + bg_opa: 65% + arc: + knob: + pressed: + bg_opa: 65% + + # LVGL owns the rotation (display + touch) since ESPHome 2026.4. + rotation: ${LVGL_ROTATION} + + style_definitions: + # Achtergrond: twee-stops verticale gradient in plaats van de 320x240 + # RGB565-wallpaper. Scheelt ~150 KB flash en een image-blit per hertekening, + # wat je direct terugziet in de vloeiendheid van het scrollen. + - id: style_bg_grad + bg_color: ${BG_TOP_COLOR} + bg_grad_color: ${BG_BOTTOM_COLOR} + bg_grad_dir: VER + bg_opa: COVER + border_width: 0 + radius: 0 + + - id: style_tile + bg_color: 0xFFFFFF + bg_opa: COVER + radius: 22 + border_width: 0 + pad_left: 12 + pad_right: 12 + pad_top: 12 + pad_bottom: 12 + shadow_width: 0 + shadow_opa: 40% + + - id: style_tile_disabled + bg_color: 0x293F50 + bg_opa: COVER + radius: 22 + border_width: 0 + pad_left: 12 + pad_right: 12 + pad_top: 12 + pad_bottom: 12 + shadow_width: 0 + shadow_opa: 25% + + - id: style_icon_circle + bg_color: 0xFEC600 + bg_opa: COVER + radius: 999 + border_width: 0 + + - id: style_icon_circle_disabled + bg_color: 0x7B7B6F + bg_opa: COVER + radius: 999 + border_width: 0 + + - id: style_room + text_font: headline + text_color: 0xFFFFFF + text_opa: 100% + + - id: style_time + text_font: time_label + text_color: 0xFFFFFF + text_opa: 100% + + - id: style_title + text_font: label + text_color: 0x000000 + text_opa: 100% + + - id: style_value + text_font: sublabel + text_color: 0x7A7A7C + text_opa: 100% + + - id: style_value_big + text_font: sublabel_big + text_color: 0xFFFFFF + text_opa: 80% + + - id: style_title_disabled + text_font: label + text_color: 0xFFFFFF + text_opa: 100% + + - id: style_value_disabled + text_font: sublabel + text_color: 0xD9D9D9 + text_opa: 100% + + pages: + - id: home_page + styles: style_bg_grad + pad_all: 0 + scrollable: false + scrollbar_mode: 'OFF' + widgets: + - label: + id: lbl_room + x: 16 + y: ${HEADER_Y} + text: "${ROOM_NAME}" + styles: style_room + + - label: + id: lbl_time + align: TOP_RIGHT + x: -16 + y: ${HEADER_Y} + text: "" + styles: style_time + + # Six tiles on page 1, four on page 2. Fixed pages keep navigation predictable. + - obj: + id: tile_scroll + x: 0 + y: ${SCROLL_Y} + width: ${DISPLAY_W} + height: ${SCROLL_H} + bg_opa: TRANSP + border_width: 0 + pad_left: 0 + pad_right: 0 + pad_top: 0 + pad_bottom: 0 + scrollable: false + scrollbar_mode: 'OFF' + widgets: + # ---------- TILE 1 ---------- + - obj: + id: tile1 + x: ${GRID_COL1_X} + y: ${GRID_ROW1_Y} + width: ${TILE_W} + height: ${TILE_H} + styles: style_tile + scrollable: false + scrollbar_mode: 'OFF' + # Directe lokale feedback: LVGL past dit toe zodra de vinger het + # glas raakt, in dezelfde frame. Onafhankelijk van Home Assistant. + pressed: + bg_opa: 45% + widgets: + - obj: + id: tile1_icon_circle + x: ${TILE_ICON_X} + y: ${TILE_ICON_Y} + width: 54 + height: 54 + 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 + width: 130 + long_mode: DOT + x: ${TILE_TITLE_X} + y: ${TILE_TITLE_Y} + text: "${TILE1_TITLE}" + styles: style_title + clickable: false + - label: + id: t1_value + width: 130 + long_mode: DOT + x: ${TILE_VALUE_X} + y: ${TILE_VALUE_Y} + text: "—" + styles: style_value + clickable: false + on_short_click: + - if: + condition: + lambda: 'return cyd::touch_guard.accept(millis(), 1);' + then: + - script.execute: { id: publish_action, action: "tile1_press" } + - script.execute: + id: do_tile_action + tile_type: "${TILE1_TYPE}" + tap_action: "${TILE1_TAP_ACTION}" + service: "${TILE1_TAP_SERVICE}" + entity_id: "${TILE1_ENTITY}" + param_key: "${TILE1_TAP_PARAM_KEY}" + param_val: "${TILE1_TAP_PARAM_VAL}" + was_on: !lambda |- + if (std::string("${TILE1_TYPE}") == "cover") + return id(tile1_cover_state).state != "closed"; + return std::string(id(ha_state_tile1).state.c_str()) == "on"; + on_long_press: + - if: + condition: + lambda: 'return cyd::touch_guard.accept(millis(), 1);' + then: + - script.execute: { id: publish_action, action: "tile1_long_press" } + - if: + condition: + lambda: "return std::string(\"${TILE1_LONGPRESS}\") == \"action\";" + then: + - script.execute: + id: do_tile_action + tile_type: !lambda |- + std::string t = "${TILE1_LONGPRESS_ACTION_TYPE}"; + return t.empty() ? std::string("${TILE1_TYPE}") : t; + tap_action: "activate" + service: "${TILE1_LONGPRESS_ACTION_SERVICE}" + entity_id: "${TILE1_LONGPRESS_ACTION}" + param_key: "" + param_val: "" + was_on: false + else: + - if: + condition: + lambda: "return std::string(\"${TILE1_LONGPRESS}\") == \"slider\";" + then: + - script.execute: + id: open_value_overlay + title: "${TILE1_TITLE}" + entity: "${TILE1_ENTITY}" + tile_type: "${TILE1_TYPE}" + longpress_mode: "${TILE1_LONGPRESS_SLIDER}" + was_on: !lambda |- + if (std::string("${TILE1_TYPE}") == "cover") + return id(tile1_cover_state).state != "closed"; + return std::string(id(ha_state_tile1).state.c_str()) == "on"; + off_value: !lambda "return atoi(\"${TILE1_LONGPRESS_OFF_VALUE}\");" + brightness_value: !lambda "return id(tile1_brightness).state;" + percentage_value: !lambda "return id(tile1_percentage).state;" + position_value: !lambda "return id(tile1_cover_position).state;" + icon_text: "${TILE1_ICON}" + slider_bg_color: ${TILE1_BG_DISABLED_COLOR} + slider_fill_color: ${TILE1_CIRCLE_ACTIVE_COLOR} + slider_icon_color: ${TILE1_ICON_ACTIVE_COLOR} + label_off: "${TILE1_LABEL_OFF}" + label_on: "${TILE1_LABEL_ON}" + supported_color_modes: !lambda "return std::string(id(tile1_supported_color_modes).state.c_str());" + climate_current_temperature: !lambda "return id(tile1_climate_current_temperature).state;" + climate_target_temperature: !lambda "return id(tile1_climate_target_temperature).state;" + climate_humidity: !lambda "return id(tile1_climate_humidity).state;" + climate_mode: !lambda "return std::string(id(tile1_climate_mode).state.c_str());" + climate_supported_modes: !lambda "return std::string(id(tile1_climate_supported_modes).state.c_str());" + climate_min_temp: "${TILE1_CLIMATE_MIN_TEMP}" + climate_max_temp: "${TILE1_CLIMATE_MAX_TEMP}" + climate_step: "${TILE1_CLIMATE_STEP}" + # Long-press modus "vacuum": opent de stofzuigerbediening. + # Valt buiten de action/slider-keten, dus die blijft ongewijzigd. + - if: + condition: + lambda: "return std::string(\"${TILE1_LONGPRESS}\") == \"vacuum\";" + then: + - script.execute: + id: open_vacuum_overlay + title: "${TILE1_TITLE}" + entity: "${TILE1_ENTITY}" + + # ---------- TILE 2 ---------- + - obj: + id: tile2 + x: ${GRID_COL2_X} + y: ${GRID_ROW1_Y} + width: ${TILE_W} + height: ${TILE_H} + styles: style_tile + scrollable: false + scrollbar_mode: 'OFF' + # Directe lokale feedback: LVGL past dit toe zodra de vinger het + # glas raakt, in dezelfde frame. Onafhankelijk van Home Assistant. + pressed: + bg_opa: 45% + widgets: + - obj: + id: tile2_icon_circle + x: ${TILE_ICON_X} + y: ${TILE_ICON_Y} + width: 54 + height: 54 + 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 + width: 130 + long_mode: DOT + x: ${TILE_TITLE_X} + y: ${TILE_TITLE_Y} + text: "${TILE2_TITLE}" + styles: style_title + clickable: false + - label: + id: t2_value + width: 130 + long_mode: DOT + x: ${TILE_VALUE_X} + y: ${TILE_VALUE_Y} + text: "—" + styles: style_value + clickable: false + on_short_click: + - if: + condition: + lambda: 'return cyd::touch_guard.accept(millis(), 2);' + then: + - script.execute: { id: publish_action, action: "tile2_press" } + - script.execute: + id: do_tile_action + tile_type: "${TILE2_TYPE}" + tap_action: "${TILE2_TAP_ACTION}" + service: "${TILE2_TAP_SERVICE}" + entity_id: "${TILE2_ENTITY}" + param_key: "${TILE2_TAP_PARAM_KEY}" + param_val: "${TILE2_TAP_PARAM_VAL}" + was_on: !lambda |- + if (std::string("${TILE2_TYPE}") == "cover") + return id(tile2_cover_state).state != "closed"; + return std::string(id(ha_state_tile2).state.c_str()) == "on"; + on_long_press: + - if: + condition: + lambda: 'return cyd::touch_guard.accept(millis(), 2);' + then: + - script.execute: { id: publish_action, action: "tile2_long_press" } + - if: + condition: + lambda: "return std::string(\"${TILE2_LONGPRESS}\") == \"action\";" + then: + - script.execute: + id: do_tile_action + tile_type: !lambda |- + std::string t = "${TILE2_LONGPRESS_ACTION_TYPE}"; + return t.empty() ? std::string("${TILE2_TYPE}") : t; + tap_action: "activate" + service: "${TILE2_LONGPRESS_ACTION_SERVICE}" + entity_id: "${TILE2_LONGPRESS_ACTION}" + param_key: "" + param_val: "" + was_on: false + else: + - if: + condition: + lambda: "return std::string(\"${TILE2_LONGPRESS}\") == \"slider\";" + then: + - script.execute: + id: open_value_overlay + title: "${TILE2_TITLE}" + entity: "${TILE2_ENTITY}" + tile_type: "${TILE2_TYPE}" + longpress_mode: "${TILE2_LONGPRESS_SLIDER}" + was_on: !lambda |- + if (std::string("${TILE2_TYPE}") == "cover") + return id(tile2_cover_state).state != "closed"; + return std::string(id(ha_state_tile2).state.c_str()) == "on"; + off_value: !lambda "return atoi(\"${TILE2_LONGPRESS_OFF_VALUE}\");" + brightness_value: !lambda "return id(tile2_brightness).state;" + percentage_value: !lambda "return id(tile2_percentage).state;" + position_value: !lambda "return id(tile2_cover_position).state;" + icon_text: "${TILE2_ICON}" + slider_bg_color: ${TILE2_BG_DISABLED_COLOR} + slider_fill_color: ${TILE2_CIRCLE_ACTIVE_COLOR} + slider_icon_color: ${TILE2_ICON_ACTIVE_COLOR} + label_off: "${TILE2_LABEL_OFF}" + label_on: "${TILE2_LABEL_ON}" + supported_color_modes: !lambda "return std::string(id(tile2_supported_color_modes).state.c_str());" + climate_current_temperature: !lambda "return id(tile2_climate_current_temperature).state;" + climate_target_temperature: !lambda "return id(tile2_climate_target_temperature).state;" + climate_humidity: !lambda "return id(tile2_climate_humidity).state;" + climate_mode: !lambda "return std::string(id(tile2_climate_mode).state.c_str());" + climate_supported_modes: !lambda "return std::string(id(tile2_climate_supported_modes).state.c_str());" + climate_min_temp: "${TILE2_CLIMATE_MIN_TEMP}" + climate_max_temp: "${TILE2_CLIMATE_MAX_TEMP}" + climate_step: "${TILE2_CLIMATE_STEP}" + # Long-press modus "vacuum": opent de stofzuigerbediening. + # Valt buiten de action/slider-keten, dus die blijft ongewijzigd. + - if: + condition: + lambda: "return std::string(\"${TILE2_LONGPRESS}\") == \"vacuum\";" + then: + - script.execute: + id: open_vacuum_overlay + title: "${TILE2_TITLE}" + entity: "${TILE2_ENTITY}" + + # ---------- TILE 3 ---------- + - obj: + id: tile3 + x: ${GRID_COL1_X} + y: ${GRID_ROW2_Y} + width: ${TILE_W} + height: ${TILE_H} + styles: style_tile + scrollable: false + scrollbar_mode: 'OFF' + # Directe lokale feedback: LVGL past dit toe zodra de vinger het + # glas raakt, in dezelfde frame. Onafhankelijk van Home Assistant. + pressed: + bg_opa: 45% + widgets: + - obj: + id: tile3_icon_circle + x: ${TILE_ICON_X} + y: ${TILE_ICON_Y} + width: 54 + height: 54 + 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 + width: 130 + long_mode: DOT + x: ${TILE_TITLE_X} + y: ${TILE_TITLE_Y} + text: "${TILE3_TITLE}" + styles: style_title + clickable: false + - label: + id: t3_value + width: 130 + long_mode: DOT + x: ${TILE_VALUE_X} + y: ${TILE_VALUE_Y} + text: "—" + styles: style_value + clickable: false + on_short_click: + - if: + condition: + lambda: 'return cyd::touch_guard.accept(millis(), 3);' + then: + - script.execute: { id: publish_action, action: "tile3_press" } + - script.execute: + id: do_tile_action + tile_type: "${TILE3_TYPE}" + tap_action: "${TILE3_TAP_ACTION}" + service: "${TILE3_TAP_SERVICE}" + entity_id: "${TILE3_ENTITY}" + param_key: "${TILE3_TAP_PARAM_KEY}" + param_val: "${TILE3_TAP_PARAM_VAL}" + was_on: !lambda |- + if (std::string("${TILE3_TYPE}") == "cover") + return id(tile3_cover_state).state != "closed"; + return std::string(id(ha_state_tile3).state.c_str()) == "on"; + on_long_press: + - if: + condition: + lambda: 'return cyd::touch_guard.accept(millis(), 3);' + then: + - script.execute: { id: publish_action, action: "tile3_long_press" } + - if: + condition: + lambda: "return std::string(\"${TILE3_LONGPRESS}\") == \"action\";" + then: + - script.execute: + id: do_tile_action + tile_type: !lambda |- + std::string t = "${TILE3_LONGPRESS_ACTION_TYPE}"; + return t.empty() ? std::string("${TILE3_TYPE}") : t; + tap_action: "activate" + service: "${TILE3_LONGPRESS_ACTION_SERVICE}" + entity_id: "${TILE3_LONGPRESS_ACTION}" + param_key: "" + param_val: "" + was_on: false + else: + - if: + condition: + lambda: "return std::string(\"${TILE3_LONGPRESS}\") == \"slider\";" + then: + - script.execute: + id: open_value_overlay + title: "${TILE3_TITLE}" + entity: "${TILE3_ENTITY}" + tile_type: "${TILE3_TYPE}" + longpress_mode: "${TILE3_LONGPRESS_SLIDER}" + was_on: !lambda |- + if (std::string("${TILE3_TYPE}") == "cover") + return id(tile3_cover_state).state != "closed"; + return std::string(id(ha_state_tile3).state.c_str()) == "on"; + off_value: !lambda "return atoi(\"${TILE3_LONGPRESS_OFF_VALUE}\");" + brightness_value: !lambda "return id(tile3_brightness).state;" + percentage_value: !lambda "return id(tile3_percentage).state;" + position_value: !lambda "return id(tile3_cover_position).state;" + icon_text: "${TILE3_ICON}" + slider_bg_color: ${TILE3_BG_DISABLED_COLOR} + slider_fill_color: ${TILE3_CIRCLE_ACTIVE_COLOR} + slider_icon_color: ${TILE3_ICON_ACTIVE_COLOR} + label_off: "${TILE3_LABEL_OFF}" + label_on: "${TILE3_LABEL_ON}" + supported_color_modes: !lambda "return std::string(id(tile3_supported_color_modes).state.c_str());" + climate_current_temperature: !lambda "return id(tile3_climate_current_temperature).state;" + climate_target_temperature: !lambda "return id(tile3_climate_target_temperature).state;" + climate_humidity: !lambda "return id(tile3_climate_humidity).state;" + climate_mode: !lambda "return std::string(id(tile3_climate_mode).state.c_str());" + climate_supported_modes: !lambda "return std::string(id(tile3_climate_supported_modes).state.c_str());" + climate_min_temp: "${TILE3_CLIMATE_MIN_TEMP}" + climate_max_temp: "${TILE3_CLIMATE_MAX_TEMP}" + climate_step: "${TILE3_CLIMATE_STEP}" + # Long-press modus "vacuum": opent de stofzuigerbediening. + # Valt buiten de action/slider-keten, dus die blijft ongewijzigd. + - if: + condition: + lambda: "return std::string(\"${TILE3_LONGPRESS}\") == \"vacuum\";" + then: + - script.execute: + id: open_vacuum_overlay + title: "${TILE3_TITLE}" + entity: "${TILE3_ENTITY}" + + # ---------- TILE 4 ---------- + - obj: + id: tile4 + x: ${GRID_COL2_X} + y: ${GRID_ROW2_Y} + width: ${TILE_W} + height: ${TILE_H} + styles: style_tile + scrollable: false + scrollbar_mode: 'OFF' + # Directe lokale feedback: LVGL past dit toe zodra de vinger het + # glas raakt, in dezelfde frame. Onafhankelijk van Home Assistant. + pressed: + bg_opa: 45% + widgets: + - obj: + id: tile4_icon_circle + x: ${TILE_ICON_X} + y: ${TILE_ICON_Y} + width: 54 + height: 54 + 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 + width: 130 + long_mode: DOT + x: ${TILE_TITLE_X} + y: ${TILE_TITLE_Y} + text: "${TILE4_TITLE}" + styles: style_title + clickable: false + - label: + id: t4_value + width: 130 + long_mode: DOT + x: ${TILE_VALUE_X} + y: ${TILE_VALUE_Y} + text: "—" + styles: style_value + clickable: false + on_short_click: + - if: + condition: + lambda: 'return cyd::touch_guard.accept(millis(), 4);' + then: + - script.execute: { id: publish_action, action: "tile4_press" } + - script.execute: + id: do_tile_action + tile_type: "${TILE4_TYPE}" + tap_action: "${TILE4_TAP_ACTION}" + service: "${TILE4_TAP_SERVICE}" + entity_id: "${TILE4_ENTITY}" + param_key: "${TILE4_TAP_PARAM_KEY}" + param_val: "${TILE4_TAP_PARAM_VAL}" + was_on: !lambda |- + if (std::string("${TILE4_TYPE}") == "cover") + return id(tile4_cover_state).state != "closed"; + return std::string(id(ha_state_tile4).state.c_str()) == "on"; + on_long_press: + - if: + condition: + lambda: 'return cyd::touch_guard.accept(millis(), 4);' + then: + - script.execute: { id: publish_action, action: "tile4_long_press" } + - if: + condition: + lambda: "return std::string(\"${TILE4_LONGPRESS}\") == \"action\";" + then: + - script.execute: + id: do_tile_action + tile_type: !lambda |- + std::string t = "${TILE4_LONGPRESS_ACTION_TYPE}"; + return t.empty() ? std::string("${TILE4_TYPE}") : t; + tap_action: "activate" + service: "${TILE4_LONGPRESS_ACTION_SERVICE}" + entity_id: "${TILE4_LONGPRESS_ACTION}" + param_key: "" + param_val: "" + was_on: false + else: + - if: + condition: + lambda: "return std::string(\"${TILE4_LONGPRESS}\") == \"slider\";" + then: + - script.execute: + id: open_value_overlay + title: "${TILE4_TITLE}" + entity: "${TILE4_ENTITY}" + tile_type: "${TILE4_TYPE}" + longpress_mode: "${TILE4_LONGPRESS_SLIDER}" + was_on: !lambda |- + if (std::string("${TILE4_TYPE}") == "cover") + return id(tile4_cover_state).state != "closed"; + return std::string(id(ha_state_tile4).state.c_str()) == "on"; + off_value: !lambda "return atoi(\"${TILE4_LONGPRESS_OFF_VALUE}\");" + brightness_value: !lambda "return id(tile4_brightness).state;" + percentage_value: !lambda "return id(tile4_percentage).state;" + position_value: !lambda "return id(tile4_cover_position).state;" + icon_text: "${TILE4_ICON}" + slider_bg_color: ${TILE4_BG_DISABLED_COLOR} + slider_fill_color: ${TILE4_CIRCLE_ACTIVE_COLOR} + slider_icon_color: ${TILE4_ICON_ACTIVE_COLOR} + label_off: "${TILE4_LABEL_OFF}" + label_on: "${TILE4_LABEL_ON}" + supported_color_modes: !lambda "return std::string(id(tile4_supported_color_modes).state.c_str());" + climate_current_temperature: !lambda "return id(tile4_climate_current_temperature).state;" + climate_target_temperature: !lambda "return id(tile4_climate_target_temperature).state;" + climate_humidity: !lambda "return id(tile4_climate_humidity).state;" + climate_mode: !lambda "return std::string(id(tile4_climate_mode).state.c_str());" + climate_supported_modes: !lambda "return std::string(id(tile4_climate_supported_modes).state.c_str());" + climate_min_temp: "${TILE4_CLIMATE_MIN_TEMP}" + climate_max_temp: "${TILE4_CLIMATE_MAX_TEMP}" + climate_step: "${TILE4_CLIMATE_STEP}" + # Long-press modus "vacuum": opent de stofzuigerbediening. + # Valt buiten de action/slider-keten, dus die blijft ongewijzigd. + - if: + condition: + lambda: "return std::string(\"${TILE4_LONGPRESS}\") == \"vacuum\";" + then: + - script.execute: + id: open_vacuum_overlay + title: "${TILE4_TITLE}" + entity: "${TILE4_ENTITY}" + + # ---------- TILE 5 ---------- + - obj: + id: tile5 + x: ${GRID_COL1_X} + y: ${GRID_ROW3_Y} + width: ${TILE_W} + height: ${TILE_H} + styles: style_tile + scrollable: false + scrollbar_mode: 'OFF' + # Directe lokale feedback: LVGL past dit toe zodra de vinger het + # glas raakt, in dezelfde frame. Onafhankelijk van Home Assistant. + pressed: + bg_opa: 45% + widgets: + - obj: + id: tile5_icon_circle + x: ${TILE_ICON_X} + y: ${TILE_ICON_Y} + width: 54 + height: 54 + 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 + width: 130 + long_mode: DOT + x: ${TILE_TITLE_X} + y: ${TILE_TITLE_Y} + text: "${TILE5_TITLE}" + styles: style_title + clickable: false + - label: + id: t5_value + width: 130 + long_mode: DOT + x: ${TILE_VALUE_X} + y: ${TILE_VALUE_Y} + text: "—" + styles: style_value + clickable: false + on_short_click: + - if: + condition: + lambda: 'return cyd::touch_guard.accept(millis(), 5);' + then: + - script.execute: { id: publish_action, action: "tile5_press" } + - script.execute: + id: do_tile_action + tile_type: "${TILE5_TYPE}" + tap_action: "${TILE5_TAP_ACTION}" + service: "${TILE5_TAP_SERVICE}" + entity_id: "${TILE5_ENTITY}" + param_key: "${TILE5_TAP_PARAM_KEY}" + param_val: "${TILE5_TAP_PARAM_VAL}" + was_on: !lambda |- + if (std::string("${TILE5_TYPE}") == "cover") + return id(tile5_cover_state).state != "closed"; + return std::string(id(ha_state_tile5).state.c_str()) == "on"; + on_long_press: + - if: + condition: + lambda: 'return cyd::touch_guard.accept(millis(), 5);' + then: + - script.execute: { id: publish_action, action: "tile5_long_press" } + - if: + condition: + lambda: "return std::string(\"${TILE5_LONGPRESS}\") == \"action\";" + then: + - script.execute: + id: do_tile_action + tile_type: !lambda |- + std::string t = "${TILE5_LONGPRESS_ACTION_TYPE}"; + return t.empty() ? std::string("${TILE5_TYPE}") : t; + tap_action: "activate" + service: "${TILE5_LONGPRESS_ACTION_SERVICE}" + entity_id: "${TILE5_LONGPRESS_ACTION}" + param_key: "" + param_val: "" + was_on: false + else: + - if: + condition: + lambda: "return std::string(\"${TILE5_LONGPRESS}\") == \"slider\";" + then: + - script.execute: + id: open_value_overlay + title: "${TILE5_TITLE}" + entity: "${TILE5_ENTITY}" + tile_type: "${TILE5_TYPE}" + longpress_mode: "${TILE5_LONGPRESS_SLIDER}" + was_on: !lambda |- + if (std::string("${TILE5_TYPE}") == "cover") + return id(tile5_cover_state).state != "closed"; + return std::string(id(ha_state_tile5).state.c_str()) == "on"; + off_value: !lambda "return atoi(\"${TILE5_LONGPRESS_OFF_VALUE}\");" + brightness_value: !lambda "return id(tile5_brightness).state;" + percentage_value: !lambda "return id(tile5_percentage).state;" + position_value: !lambda "return id(tile5_cover_position).state;" + icon_text: "${TILE5_ICON}" + slider_bg_color: ${TILE5_BG_DISABLED_COLOR} + slider_fill_color: ${TILE5_CIRCLE_ACTIVE_COLOR} + slider_icon_color: ${TILE5_ICON_ACTIVE_COLOR} + label_off: "${TILE5_LABEL_OFF}" + label_on: "${TILE5_LABEL_ON}" + supported_color_modes: !lambda "return std::string(id(tile5_supported_color_modes).state.c_str());" + climate_current_temperature: !lambda "return id(tile5_climate_current_temperature).state;" + climate_target_temperature: !lambda "return id(tile5_climate_target_temperature).state;" + climate_humidity: !lambda "return id(tile5_climate_humidity).state;" + climate_mode: !lambda "return std::string(id(tile5_climate_mode).state.c_str());" + climate_supported_modes: !lambda "return std::string(id(tile5_climate_supported_modes).state.c_str());" + climate_min_temp: "${TILE5_CLIMATE_MIN_TEMP}" + climate_max_temp: "${TILE5_CLIMATE_MAX_TEMP}" + climate_step: "${TILE5_CLIMATE_STEP}" + # Long-press modus "vacuum": opent de stofzuigerbediening. + # Valt buiten de action/slider-keten, dus die blijft ongewijzigd. + - if: + condition: + lambda: "return std::string(\"${TILE5_LONGPRESS}\") == \"vacuum\";" + then: + - script.execute: + id: open_vacuum_overlay + title: "${TILE5_TITLE}" + entity: "${TILE5_ENTITY}" + + # ---------- TILE 6 ---------- + - obj: + id: tile6 + x: ${GRID_COL2_X} + y: ${GRID_ROW3_Y} + width: ${TILE_W} + height: ${TILE_H} + styles: style_tile + scrollable: false + scrollbar_mode: 'OFF' + # Directe lokale feedback: LVGL past dit toe zodra de vinger het + # glas raakt, in dezelfde frame. Onafhankelijk van Home Assistant. + pressed: + bg_opa: 45% + widgets: + - obj: + id: tile6_icon_circle + x: ${TILE_ICON_X} + y: ${TILE_ICON_Y} + width: 54 + height: 54 + 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 + width: 130 + long_mode: DOT + x: ${TILE_TITLE_X} + y: ${TILE_TITLE_Y} + text: "${TILE6_TITLE}" + styles: style_title + clickable: false + - label: + id: t6_value + width: 130 + long_mode: DOT + x: ${TILE_VALUE_X} + y: ${TILE_VALUE_Y} + text: "—" + styles: style_value + clickable: false + on_short_click: + - if: + condition: + lambda: 'return cyd::touch_guard.accept(millis(), 6);' + then: + - script.execute: { id: publish_action, action: "tile6_press" } + - script.execute: + id: do_tile_action + tile_type: "${TILE6_TYPE}" + tap_action: "${TILE6_TAP_ACTION}" + service: "${TILE6_TAP_SERVICE}" + entity_id: "${TILE6_ENTITY}" + param_key: "${TILE6_TAP_PARAM_KEY}" + param_val: "${TILE6_TAP_PARAM_VAL}" + was_on: !lambda |- + if (std::string("${TILE6_TYPE}") == "cover") + return id(tile6_cover_state).state != "closed"; + return std::string(id(ha_state_tile6).state.c_str()) == "on"; + on_long_press: + - if: + condition: + lambda: 'return cyd::touch_guard.accept(millis(), 6);' + then: + - script.execute: { id: publish_action, action: "tile6_long_press" } + - if: + condition: + lambda: "return std::string(\"${TILE6_LONGPRESS}\") == \"action\";" + then: + - script.execute: + id: do_tile_action + tile_type: !lambda |- + std::string t = "${TILE6_LONGPRESS_ACTION_TYPE}"; + return t.empty() ? std::string("${TILE6_TYPE}") : t; + tap_action: "activate" + service: "${TILE6_LONGPRESS_ACTION_SERVICE}" + entity_id: "${TILE6_LONGPRESS_ACTION}" + param_key: "" + param_val: "" + was_on: false + else: + - if: + condition: + lambda: "return std::string(\"${TILE6_LONGPRESS}\") == \"slider\";" + then: + - script.execute: + id: open_value_overlay + title: "${TILE6_TITLE}" + entity: "${TILE6_ENTITY}" + tile_type: "${TILE6_TYPE}" + longpress_mode: "${TILE6_LONGPRESS_SLIDER}" + was_on: !lambda |- + if (std::string("${TILE6_TYPE}") == "cover") + return id(tile6_cover_state).state != "closed"; + return std::string(id(ha_state_tile6).state.c_str()) == "on"; + off_value: !lambda "return atoi(\"${TILE6_LONGPRESS_OFF_VALUE}\");" + brightness_value: !lambda "return id(tile6_brightness).state;" + percentage_value: !lambda "return id(tile6_percentage).state;" + position_value: !lambda "return id(tile6_cover_position).state;" + icon_text: "${TILE6_ICON}" + slider_bg_color: ${TILE6_BG_DISABLED_COLOR} + slider_fill_color: ${TILE6_CIRCLE_ACTIVE_COLOR} + slider_icon_color: ${TILE6_ICON_ACTIVE_COLOR} + label_off: "${TILE6_LABEL_OFF}" + label_on: "${TILE6_LABEL_ON}" + supported_color_modes: !lambda "return std::string(id(tile6_supported_color_modes).state.c_str());" + climate_current_temperature: !lambda "return id(tile6_climate_current_temperature).state;" + climate_target_temperature: !lambda "return id(tile6_climate_target_temperature).state;" + climate_humidity: !lambda "return id(tile6_climate_humidity).state;" + climate_mode: !lambda "return std::string(id(tile6_climate_mode).state.c_str());" + climate_supported_modes: !lambda "return std::string(id(tile6_climate_supported_modes).state.c_str());" + climate_min_temp: "${TILE6_CLIMATE_MIN_TEMP}" + climate_max_temp: "${TILE6_CLIMATE_MAX_TEMP}" + climate_step: "${TILE6_CLIMATE_STEP}" + # Long-press modus "vacuum": opent de stofzuigerbediening. + # Valt buiten de action/slider-keten, dus die blijft ongewijzigd. + - if: + condition: + lambda: "return std::string(\"${TILE6_LONGPRESS}\") == \"vacuum\";" + then: + - script.execute: + id: open_vacuum_overlay + title: "${TILE6_TITLE}" + entity: "${TILE6_ENTITY}" + + # ---------- TILE 7 — Tafellamp ---------- + # Alleen-helderheid lamp: er zijn bewust geen percentage/cover/ + # climate-sensors voor deze tegel aangemaakt, die zouden alleen + # maar HA-subscriptions en heap kosten. De overlay krijgt daarom + # NAN / "" voor de velden die een light nooit gebruikt. + - obj: + id: tile7 + hidden: true + x: ${GRID_COL1_X} + y: ${GRID_ROW4_Y} + width: ${TILE_W} + height: ${TILE_H} + styles: style_tile + scrollable: false + scrollbar_mode: 'OFF' + pressed: + bg_opa: 45% + widgets: + - obj: + id: tile7_icon_circle + x: ${TILE_ICON_X} + y: ${TILE_ICON_Y} + width: 54 + height: 54 + 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 + width: 130 + long_mode: DOT + x: ${TILE_TITLE_X} + y: ${TILE_TITLE_Y} + text: "${TILE7_TITLE}" + styles: style_title + clickable: false + - label: + id: t7_value + width: 130 + long_mode: DOT + x: ${TILE_VALUE_X} + y: ${TILE_VALUE_Y} + text: "—" + styles: style_value + clickable: false + on_short_click: + - if: + condition: + lambda: 'return cyd::touch_guard.accept(millis(), 7);' + then: + - script.execute: { id: publish_action, action: "tile7_press" } + - script.execute: + id: do_tile_action + tile_type: "${TILE7_TYPE}" + tap_action: "${TILE7_TAP_ACTION}" + service: "${TILE7_TAP_SERVICE}" + entity_id: "${TILE7_ENTITY}" + param_key: "${TILE7_TAP_PARAM_KEY}" + param_val: "${TILE7_TAP_PARAM_VAL}" + was_on: !lambda "return std::string(id(ha_state_tile7).state.c_str()) == \"on\";" + on_long_press: + - if: + condition: + lambda: 'return cyd::touch_guard.accept(millis(), 7);' + then: + - script.execute: { id: publish_action, action: "tile7_long_press" } + - if: + condition: + lambda: "return std::string(\"${TILE7_LONGPRESS}\") == \"action\";" + then: + - script.execute: + id: do_tile_action + tile_type: !lambda |- + std::string t = "${TILE7_LONGPRESS_ACTION_TYPE}"; + return t.empty() ? std::string("${TILE7_TYPE}") : t; + tap_action: "activate" + service: "${TILE7_LONGPRESS_ACTION_SERVICE}" + entity_id: "${TILE7_LONGPRESS_ACTION}" + param_key: "" + param_val: "" + was_on: false + else: + - if: + condition: + lambda: "return std::string(\"${TILE7_LONGPRESS}\") == \"slider\";" + then: + - script.execute: + id: open_value_overlay + title: "${TILE7_TITLE}" + entity: "${TILE7_ENTITY}" + tile_type: "${TILE7_TYPE}" + longpress_mode: "${TILE7_LONGPRESS_SLIDER}" + was_on: !lambda "return std::string(id(ha_state_tile7).state.c_str()) == \"on\";" + off_value: !lambda "return atoi(\"${TILE7_LONGPRESS_OFF_VALUE}\");" + brightness_value: !lambda "return id(tile7_brightness).state;" + percentage_value: !lambda "return NAN;" + position_value: !lambda "return NAN;" + icon_text: "${TILE7_ICON}" + slider_bg_color: ${TILE7_BG_DISABLED_COLOR} + slider_fill_color: ${TILE7_CIRCLE_ACTIVE_COLOR} + slider_icon_color: ${TILE7_ICON_ACTIVE_COLOR} + label_off: "${TILE7_LABEL_OFF}" + label_on: "${TILE7_LABEL_ON}" + supported_color_modes: !lambda "return std::string(id(tile7_supported_color_modes).state.c_str());" + climate_current_temperature: !lambda "return NAN;" + climate_target_temperature: !lambda "return NAN;" + climate_humidity: !lambda "return NAN;" + climate_mode: "" + climate_supported_modes: "" + climate_min_temp: "5.0" + climate_max_temp: "30.0" + climate_step: "0.5" + + # ---------- TILE 8 — Brievenbus reset ---------- + # input_select met opties Vol/Leeg. De tegel licht op zodra de + # status TILE8_ACTIVE_STATE ("Vol") is; een korte tik stuurt + # input_select.select_option met option: "Leeg". + # Geen slider-modus, dus deze tegel heeft alleen ha_state_tile8 + # nodig — verder geen enkele HA-subscription. + - obj: + id: tile8 + hidden: true + x: ${GRID_COL2_X} + y: ${GRID_ROW4_Y} + width: ${TILE_W} + height: ${TILE_H} + styles: style_tile + scrollable: false + scrollbar_mode: 'OFF' + pressed: + bg_opa: 45% + widgets: + - obj: + id: tile8_icon_circle + x: ${TILE_ICON_X} + y: ${TILE_ICON_Y} + width: 54 + height: 54 + 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 + width: 130 + long_mode: DOT + x: ${TILE_TITLE_X} + y: ${TILE_TITLE_Y} + text: "${TILE8_TITLE}" + styles: style_title + clickable: false + - label: + id: t8_value + width: 130 + long_mode: DOT + x: ${TILE_VALUE_X} + y: ${TILE_VALUE_Y} + text: "—" + styles: style_value + clickable: false + on_short_click: + - if: + condition: + lambda: 'return cyd::touch_guard.accept(millis(), 8);' + then: + - script.execute: { id: publish_action, action: "tile8_press" } + - script.execute: + id: do_tile_action + tile_type: "${TILE8_TYPE}" + tap_action: "${TILE8_TAP_ACTION}" + service: "${TILE8_TAP_SERVICE}" + entity_id: "${TILE8_ENTITY}" + param_key: "${TILE8_TAP_PARAM_KEY}" + param_val: "${TILE8_TAP_PARAM_VAL}" + was_on: !lambda "return std::string(id(ha_state_tile8).state.c_str()) == \"${TILE8_ACTIVE_STATE}\";" + on_long_press: + - if: + condition: + lambda: 'return cyd::touch_guard.accept(millis(), 8);' + then: + - script.execute: { id: publish_action, action: "tile8_long_press" } + # Slider-modus kan hier niet zonder eerst de bijbehorende + # sensors aan te maken; "action" werkt wel out of the box. + - if: + condition: + lambda: "return std::string(\"${TILE8_LONGPRESS}\") == \"action\";" + then: + - script.execute: + id: do_tile_action + tile_type: !lambda |- + std::string t = "${TILE8_LONGPRESS_ACTION_TYPE}"; + return t.empty() ? std::string("${TILE8_TYPE}") : t; + tap_action: "activate" + service: "${TILE8_LONGPRESS_ACTION_SERVICE}" + entity_id: "${TILE8_LONGPRESS_ACTION}" + param_key: "" + param_val: "" + was_on: false + + # ---------- TILE 9 — Woonkamer (verhuisd van tegel 2) ---------- + - obj: + id: tile9 + hidden: true + x: ${GRID_COL1_X} + y: ${GRID_ROW5_Y} + width: ${TILE_W} + height: ${TILE_H} + styles: style_tile + scrollable: false + scrollbar_mode: 'OFF' + pressed: + bg_opa: 45% + widgets: + - obj: + id: tile9_icon_circle + x: ${TILE_ICON_X} + y: ${TILE_ICON_Y} + width: 54 + height: 54 + styles: style_icon_circle + clickable: false + scrollable: false + scrollbar_mode: 'OFF' + widgets: + - label: + id: tile9_icon_lbl + align: CENTER + text_font: materialdesign_icons + text_color: 0xFFFFFF + text: "${TILE9_ICON}" + clickable: false + - label: + id: t9_title + width: 130 + long_mode: DOT + x: ${TILE_TITLE_X} + y: ${TILE_TITLE_Y} + text: "${TILE9_TITLE}" + styles: style_title + clickable: false + - label: + id: t9_value + width: 130 + long_mode: DOT + x: ${TILE_VALUE_X} + y: ${TILE_VALUE_Y} + text: "—" + styles: style_value + clickable: false + on_short_click: + - if: + condition: + lambda: 'return cyd::touch_guard.accept(millis(), 9);' + then: + - script.execute: { id: publish_action, action: "tile9_press" } + - script.execute: + id: do_tile_action + tile_type: "${TILE9_TYPE}" + tap_action: "${TILE9_TAP_ACTION}" + service: "${TILE9_TAP_SERVICE}" + entity_id: "${TILE9_ENTITY}" + param_key: "${TILE9_TAP_PARAM_KEY}" + param_val: "${TILE9_TAP_PARAM_VAL}" + was_on: !lambda "return std::string(id(ha_state_tile9).state.c_str()) == \"on\";" + on_long_press: + - if: + condition: + lambda: 'return cyd::touch_guard.accept(millis(), 9);' + then: + - script.execute: { id: publish_action, action: "tile9_long_press" } + - if: + condition: + lambda: "return std::string(\"${TILE9_LONGPRESS}\") == \"action\";" + then: + - script.execute: + id: do_tile_action + tile_type: !lambda |- + std::string t = "${TILE9_LONGPRESS_ACTION_TYPE}"; + return t.empty() ? std::string("${TILE9_TYPE}") : t; + tap_action: "activate" + service: "${TILE9_LONGPRESS_ACTION_SERVICE}" + entity_id: "${TILE9_LONGPRESS_ACTION}" + param_key: "" + param_val: "" + was_on: false + else: + - if: + condition: + lambda: "return std::string(\"${TILE9_LONGPRESS}\") == \"slider\";" + then: + - script.execute: + id: open_value_overlay + title: "${TILE9_TITLE}" + entity: "${TILE9_ENTITY}" + tile_type: "${TILE9_TYPE}" + longpress_mode: "${TILE9_LONGPRESS_SLIDER}" + was_on: !lambda "return std::string(id(ha_state_tile9).state.c_str()) == \"on\";" + off_value: !lambda "return atoi(\"${TILE9_LONGPRESS_OFF_VALUE}\");" + brightness_value: !lambda "return id(tile9_brightness).state;" + percentage_value: !lambda "return NAN;" + position_value: !lambda "return NAN;" + icon_text: "${TILE9_ICON}" + slider_bg_color: ${TILE9_BG_DISABLED_COLOR} + slider_fill_color: ${TILE9_CIRCLE_ACTIVE_COLOR} + slider_icon_color: ${TILE9_ICON_ACTIVE_COLOR} + label_off: "${TILE9_LABEL_OFF}" + label_on: "${TILE9_LABEL_ON}" + supported_color_modes: !lambda "return std::string(id(tile9_supported_color_modes).state.c_str());" + climate_current_temperature: !lambda "return NAN;" + climate_target_temperature: !lambda "return NAN;" + climate_humidity: !lambda "return NAN;" + climate_mode: "" + climate_supported_modes: "" + climate_min_temp: "5.0" + climate_max_temp: "30.0" + climate_step: "0.5" + + # ---------- TILE 10 — Alarm aan/uit ---------- + - obj: + id: tile10 + hidden: true + x: ${GRID_COL2_X} + y: ${GRID_ROW5_Y} + width: ${TILE_W} + height: ${TILE_H} + styles: style_tile + scrollable: false + scrollbar_mode: 'OFF' + pressed: + bg_opa: 45% + widgets: + - obj: + id: tile10_icon_circle + x: ${TILE_ICON_X} + y: ${TILE_ICON_Y} + width: 54 + height: 54 + styles: style_icon_circle + clickable: false + scrollable: false + scrollbar_mode: 'OFF' + widgets: + - label: + id: tile10_icon_lbl + align: CENTER + text_font: materialdesign_icons + text_color: 0xFFFFFF + text: "${TILE10_ICON}" + clickable: false + - label: + id: t10_title + width: 130 + long_mode: DOT + x: ${TILE_TITLE_X} + y: ${TILE_TITLE_Y} + text: "${TILE10_TITLE}" + styles: style_title + clickable: false + - label: + id: t10_value + width: 130 + long_mode: DOT + x: ${TILE_VALUE_X} + y: ${TILE_VALUE_Y} + text: "—" + styles: style_value + clickable: false + on_short_click: + - if: + condition: + lambda: 'return cyd::touch_guard.accept(millis(), 10);' + then: + - script.execute: { id: publish_action, action: "tile10_press" } + - script.execute: + id: do_tile_action + tile_type: "${TILE10_TYPE}" + tap_action: "${TILE10_TAP_ACTION}" + service: "${TILE10_TAP_SERVICE}" + entity_id: "${TILE10_ENTITY}" + param_key: "${TILE10_TAP_PARAM_KEY}" + param_val: "${TILE10_TAP_PARAM_VAL}" + was_on: !lambda "return std::string(id(ha_state_tile10).state.c_str()) == \"on\";" + on_long_press: + - if: + condition: + lambda: 'return cyd::touch_guard.accept(millis(), 10);' + then: + - script.execute: { id: publish_action, action: "tile10_long_press" } + - if: + condition: + lambda: "return std::string(\"${TILE10_LONGPRESS}\") == \"action\";" + then: + - script.execute: + id: do_tile_action + tile_type: !lambda |- + std::string t = "${TILE10_LONGPRESS_ACTION_TYPE}"; + return t.empty() ? std::string("${TILE10_TYPE}") : t; + tap_action: "activate" + service: "${TILE10_LONGPRESS_ACTION_SERVICE}" + entity_id: "${TILE10_LONGPRESS_ACTION}" + param_key: "" + param_val: "" + was_on: false + + # ---------- VALUE SLIDER OVERLAY ---------- + - obj: + id: page_prev + align: BOTTOM_LEFT + x: 14 + y: -4 + width: 150 + height: 44 + pad_all: 0 + radius: 12 + bg_color: 0x3C3C40 + bg_opa: COVER + border_width: 0 + scrollable: false + on_short_click: + - if: + condition: + lambda: 'return cyd::touch_guard.accept(millis(), 11);' + then: + - lambda: 'id(tile_page) = 0;' + - script.execute: show_tile_page + widgets: + - label: + align: CENTER + text: "< Vorige" + text_font: sublabel + text_color: 0xFFFFFF + clickable: false + - label: + id: page_number + align: BOTTOM_MID + y: -18 + text: "1 / 2" + text_font: sublabel + text_color: 0xFFFFFF + - obj: + id: page_next + align: BOTTOM_RIGHT + x: -14 + y: -4 + width: 150 + height: 44 + pad_all: 0 + radius: 12 + bg_color: 0x3C3C40 + bg_opa: COVER + border_width: 0 + scrollable: false + on_short_click: + - if: + condition: + lambda: 'return cyd::touch_guard.accept(millis(), 12);' + then: + - lambda: 'id(tile_page) = 1;' + - script.execute: show_tile_page + widgets: + - label: + align: CENTER + text: "Volgende >" + text_font: sublabel + text_color: 0xFFFFFF + clickable: false + + - obj: + id: brightness_overlay + x: 0 + y: 0 + width: ${DISPLAY_W} + height: ${DISPLAY_H} + bg_opa: TRANSP + hidden: true + scrollable: false + scrollbar_mode: 'OFF' + pad_left: 0 + pad_right: 0 + pad_top: 0 + pad_bottom: 0 + border_width: 0 + widgets: + - obj: + id: brightness_overlay_bg + x: 0 + y: 0 + width: ${DISPLAY_W} + height: ${DISPLAY_H} + styles: style_bg_grad + scrollable: false + clickable: true + on_click: + - lvgl.widget.hide: brightness_overlay + + - obj: + id: brightness_overlay_dim + x: 0 + y: 0 + width: ${DISPLAY_W} + height: ${DISPLAY_H} + bg_color: 0x000000 + bg_opa: 18% + border_width: 0 + clickable: false + scrollable: false + + - label: + id: overlay_title + align: TOP_MID + y: 20 + text: "Light" + styles: style_room + + - label: + id: overlay_value + align: TOP_MID + y: 70 + text: "100 %" + styles: style_value_big + + - slider: + id: overlay_slider + align: CENTER + y: 36 + width: ${OVERLAY_SLIDER_W} + height: ${OVERLAY_SLIDER_H} + min_value: 0 + max_value: 100 + value: 100 + adv_hittest: false + + bg_color: 0x293F50 + bg_opa: 100% + radius: 42 + # 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: 42 + + knob: + bg_opa: 0% + border_width: 0 + shadow_width: 0 + pad_all: 15 + + 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: -20 + y: -26 + width: 66 + height: 66 + 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: 60 + height: 60 + start_angle: 0 + end_angle: 45 + min_value: 0 + max_value: 100 + value: 0 + adjustable: false + arc_width: 15 + 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: 60 + height: 60 + start_angle: 45 + end_angle: 90 + min_value: 0 + max_value: 100 + value: 0 + adjustable: false + arc_width: 15 + 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: 60 + height: 60 + start_angle: 90 + end_angle: 135 + min_value: 0 + max_value: 100 + value: 0 + adjustable: false + arc_width: 15 + 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: 60 + height: 60 + start_angle: 135 + end_angle: 180 + min_value: 0 + max_value: 100 + value: 0 + adjustable: false + arc_width: 15 + 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: 60 + height: 60 + start_angle: 180 + end_angle: 225 + min_value: 0 + max_value: 100 + value: 0 + adjustable: false + arc_width: 15 + 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: 60 + height: 60 + start_angle: 225 + end_angle: 270 + min_value: 0 + max_value: 100 + value: 0 + adjustable: false + arc_width: 15 + 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: 60 + height: 60 + start_angle: 270 + end_angle: 315 + min_value: 0 + max_value: 100 + value: 0 + adjustable: false + arc_width: 15 + 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: 60 + height: 60 + start_angle: 315 + end_angle: 360 + min_value: 0 + max_value: 100 + value: 0 + adjustable: false + arc_width: 15 + 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: 15 + y: 18 + width: 54 + height: 54 + 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: 32 + text: "Light Color" + styles: style_room + + - obj: + id: color_done_button + align: TOP_RIGHT + x: -15 + y: 18 + width: 54 + height: 54 + 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: 116 + width: 285 + height: 56 + radius: 21 + 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: 3 + y: 4 + width: 140 + height: 48 + radius: 18 + bg_color: 0x8E8E93 + bg_opa: COVER + border_width: 0 + clickable: false + scrollable: false + - obj: + x: 0 + y: 0 + width: 142 + height: 56 + 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: 142 + y: 0 + width: 142 + height: 56 + 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: 196 + width: 429 + height: 96 + 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: 21 + y: 16 + width: 45 + height: 45 + 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: 92 + y: 16 + width: 45 + height: 45 + 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: 162 + y: 16 + width: 45 + height: 45 + 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: 232 + y: 16 + width: 45 + height: 45 + 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: 303 + y: 16 + width: 45 + height: 45 + 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: 374 + y: 16 + width: 45 + height: 45 + 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: 196 + width: 429 + height: 96 + 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: 36 + y: 16 + width: 93 + height: 60 + radius: 22 + 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: 168 + y: 16 + width: 93 + height: 60 + radius: 22 + 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: 300 + y: 16 + width: 93 + height: 60 + radius: 22 + 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: 330 + width: 282 + height: 92 + bg_opa: TRANSP + border_width: 0 + scrollable: false + pad_left: 0 + pad_right: 0 + pad_top: 0 + pad_bottom: 0 + widgets: + - label: + x: 20 + y: 12 + text: "Brightness" + text_font: sublabel + text_color: 0xD1D1D6 + hidden: true + clickable: false + - slider: + id: color_brightness_slider + x: 0 + y: 0 + width: 282 + height: 92 + min_value: 1 + max_value: 100 + value: 100 + bg_color: 0x293F50 + bg_opa: COVER + radius: 34 + border_width: 0 + indicator: + bg_color: 0xFEC600 + bg_opa: COVER + radius: 34 + knob: + bg_opa: 0% + border_width: 0 + shadow_width: 0 + pad_all: 15 + 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: -20 + 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: 15 + y: 18 + # 44 i.p.v. 36: iets groter doelvlak, groeit weg van de arc + # (top-left anchor blijft staan) i.p.v. ernaartoe. + width: 66 + height: 66 + 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: -15 + y: 18 + width: 66 + height: 66 + 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: 32 + text: "Climate" + styles: style_room + + - label: + id: climate_current_label + align: TOP_MID + y: 84 + text: "Current: --" + text_font: sublabel_big + text_color: 0xD1E8FF + text_opa: 85% + clickable: false + + - arc: + id: climate_arc + align: CENTER + y: 40 + width: 276 + height: 276 + # Zonder dit claimt de arc een onzichtbare rand extra + # klikgebied buiten de 184x184 box — vlakbij de kruisje- + # knop linksboven ontstaat daardoor overlap. + ext_click_area: 0 + start_angle: 135 + end_angle: 45 + min_value: 50 + max_value: 300 + value: 200 + adjustable: true + arc_width: 30 + arc_rounded: true + arc_color: 0x4B4B4F + arc_opa: 80% + bg_opa: TRANSP + border_width: 0 + indicator: + arc_color: 0xFEC600 + arc_opa: COVER + arc_width: 30 + 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: -32 + text: "SET TO" + text_font: sublabel_big + text_color: 0xFFFFFF + text_opa: 68% + clickable: false + + - label: + id: climate_target_label + align: CENTER + y: 40 + text: "20.0°" + text_font: climate_number + text_color: 0xFFFFFF + clickable: false + + - obj: + id: climate_minus_button + align: BOTTOM_LEFT + x: 57 + y: -32 + width: 72 + height: 72 + radius: 22 + 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: -57 + y: -32 + width: 72 + height: 72 + radius: 22 + 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: -36 + width: 189 + height: 68 + radius: 26 + 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: 32 + 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: 100 + width: 98% + height: 356 + bg_opa: TRANSP + border_width: 0 + scrollable: false + pad_all: 4 + 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: 198 + height: 76 + radius: 28 + 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: 198 + height: 76 + radius: 28 + 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: 198 + height: 76 + radius: 28 + 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: 198 + height: 76 + radius: 28 + 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: 198 + height: 76 + radius: 28 + 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: 198 + height: 76 + radius: 28 + 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: 198 + height: 76 + radius: 28 + 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: 15 + y: 18 + width: 54 + height: 54 + 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: -15 + y: 18 + width: 54 + height: 54 + 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: 32 + text: "Vacuum" + text_font: sublabel_big + text_color: 0xFFFFFF + clickable: false + + - obj: + id: vacuum_actions + align: TOP_MID + y: 104 + width: 98% + height: 160 + bg_opa: TRANSP + border_width: 0 + scrollable: false + pad_all: 4 + 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: 108 + height: 76 + radius: 28 + 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: 108 + height: 76 + radius: 28 + 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: 108 + height: 76 + radius: 28 + 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: 108 + height: 76 + radius: 28 + 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: 276 + text: "Suction" + text_font: sublabel + text_color: 0xFFFFFF + clickable: false + + - obj: + id: vacuum_speeds + align: TOP_MID + y: 312 + width: 98% + height: 148 + bg_opa: TRANSP + border_width: 0 + scrollable: false + pad_all: 4 + 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: 108 + height: 44 + radius: 28 + 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: 108 + height: 44 + radius: 28 + 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: 108 + height: 44 + radius: 28 + 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: 108 + height: 44 + radius: 28 + 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: 15 + y: 18 + width: 54 + height: 54 + 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: -15 + y: 18 + width: 54 + height: 54 + radius: 999 + bg_color: 0xFEC600 + bg_opa: COVER + border_width: 0 + clickable: true + scrollable: false + on_click: + - lvgl.widget.hide: vacuum_overlay + widgets: + - label: + align: CENTER + text: "\U000F012C" + text_font: materialdesign_icons + text_color: 0xFFFFFF + clickable: false + + # ---------- DIM / WAKE OVERLAY ---------- + - obj: + id: dim_wake_overlay + x: 0 + y: 0 + width: ${DISPLAY_W} + height: ${DISPLAY_H} + hidden: true + bg_opa: TRANSP + border_width: 0 + scrollable: false + scrollbar_mode: 'OFF' + clickable: true + on_click: + - script.execute: wake_display + + - id: touch_cal_page + bg_color: 0x111D2B + bg_opa: COVER + pad_all: 0 + border_width: 0 + scrollable: false + widgets: + - label: + align: TOP_MID + y: 110 + text: "GT911 touchcontrole" + text_font: headline + text_color: 0xFFFFFF + - obj: + x: 12 + y: 12 + width: 24 + height: 24 + pad_all: 0 + border_width: 0 + bg_opa: TRANSP + scrollable: false + widgets: + - label: + align: CENTER + text: "+" + text_font: headline + text_color: 0xFFFFFF + - obj: + x: 443 + y: 12 + width: 24 + height: 24 + pad_all: 0 + border_width: 0 + bg_opa: TRANSP + scrollable: false + widgets: + - label: + align: CENTER + text: "+" + text_font: headline + text_color: 0xFFFFFF + - obj: + x: 443 + y: 443 + width: 24 + height: 24 + pad_all: 0 + border_width: 0 + bg_opa: TRANSP + scrollable: false + widgets: + - label: + align: CENTER + text: "+" + text_font: headline + text_color: 0xFFFFFF + - obj: + x: 12 + y: 443 + width: 24 + height: 24 + pad_all: 0 + border_width: 0 + bg_opa: TRANSP + scrollable: false + widgets: + - label: + align: CENTER + text: "+" + text_font: headline + text_color: 0xFFFFFF + - obj: + x: 228 + y: 228 + width: 24 + height: 24 + pad_all: 0 + border_width: 0 + bg_opa: TRANSP + scrollable: false + widgets: + - label: + align: CENTER + text: "+" + text_font: headline + text_color: 0xFFFFFF + +script: + - id: capture_ui_snapshot + mode: single + then: + - lambda: guition::snapshot_begin(); + - repeat: + count: 240 + then: + - lambda: guition::snapshot_next_row(); + - delay: 100ms + # Render every overlay and both pages repeatedly without controlling devices. + - id: ui_self_test + mode: single + then: + - logger.log: "UI_TEST START: page/overlay render stress, no HA actions" + - lambda: 'id(calibration_active) = false;' + - lvgl.page.show: home_page + - script.execute: wake_display + - repeat: + count: 10 + then: + - lambda: |- + id(last_touch_ms) = millis(); + id(tile_page) = 1; + - script.execute: show_tile_page + - delay: 150ms + - lambda: |- + lv_obj_update_layout(id(tile_scroll)); + const int count = std::clamp(atoi("${TILE_COUNT}"), 1, 10); + const bool paginated = count > 6; + lv_obj_t* tiles[] = {id(tile1), id(tile2), id(tile3), id(tile4), id(tile5), + id(tile6), id(tile7), id(tile8), id(tile9), id(tile10)}; + bool ok = guition::check_geometry(id(home_page)->obj); + for (int i = 0; i < 10; ++i) { + bool visible = i < count && (paginated ? i >= 6 : i < 6); + ok = ok && (lv_obj_has_flag(tiles[i], LV_OBJ_FLAG_HIDDEN) != visible); + } + for (lv_obj_t* control : {id(page_prev), id(page_next), id(page_number)}) + ok = ok && (lv_obj_has_flag(control, LV_OBJ_FLAG_HIDDEN) != paginated); + lv_area_t nav, grid; + lv_obj_get_coords(id(page_next), &nav); + lv_obj_get_coords(id(tile_scroll), &grid); + ok = ok && (!paginated || grid.y2 < nav.y1); + ESP_LOGI("ui_test", "page_check=%s count=%d heap=%u frames=%u grid_bottom=%d nav_top=%d", ok ? "PASS" : "FAIL", count, (unsigned) esp_get_free_heap_size(), (unsigned) id(rendered_frames), grid.y2, nav.y1); + id(tile_page) = 0; + - script.execute: show_tile_page + - delay: 150ms + - lvgl.widget.show: brightness_overlay + - delay: 150ms + - lambda: guition::check_geometry(id(brightness_overlay)); + - lvgl.widget.hide: brightness_overlay + - lvgl.widget.show: color_detail_overlay + - delay: 150ms + - lambda: guition::check_geometry(id(color_detail_overlay)); + - lvgl.widget.hide: color_detail_overlay + - lvgl.widget.show: climate_detail_overlay + - delay: 150ms + - lambda: guition::check_geometry(id(climate_detail_overlay)); + - lvgl.widget.hide: climate_detail_overlay + - lvgl.widget.show: climate_mode_overlay + - delay: 150ms + - lambda: guition::check_geometry(id(climate_mode_overlay)); + - lvgl.widget.hide: climate_mode_overlay + - lvgl.widget.show: vacuum_overlay + - delay: 150ms + - lambda: guition::check_geometry(id(vacuum_overlay)); + - lvgl.widget.hide: vacuum_overlay + - lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' + - logger.log: "UI_TEST COMPLETE" + + - id: show_tile_page + then: + - lambda: |- + lv_obj_t* tiles[] = {id(tile1), id(tile2), id(tile3), id(tile4), id(tile5), + id(tile6), id(tile7), id(tile8), id(tile9), id(tile10)}; + const int rows[] = {${GRID_ROW1_Y}, ${GRID_ROW2_Y}, ${GRID_ROW3_Y}}; + const int count = std::clamp(atoi("${TILE_COUNT}"), 1, 10); + const bool paginated = count > 6; + if (!paginated) id(tile_page) = 0; + for (lv_obj_t* control : {id(page_prev), id(page_next), id(page_number)}) { + if (paginated) lv_obj_clear_flag(control, LV_OBJ_FLAG_HIDDEN); + else lv_obj_add_flag(control, LV_OBJ_FLAG_HIDDEN); + } + for (int i = 0; i < 10; ++i) { + bool visible = i < count && (id(tile_page) == 0 ? i < 6 : i >= 6); + if (visible) { + lv_obj_set_y(tiles[i], rows[(i < 6 ? i : i - 6) / 2]); + lv_obj_clear_flag(tiles[i], LV_OBJ_FLAG_HIDDEN); + } else { + lv_obj_add_flag(tiles[i], LV_OBJ_FLAG_HIDDEN); + } + } + lv_label_set_text(id(page_number), id(tile_page) == 0 ? "1 / 2" : "2 / 2"); + if (id(tile_page) == 0) { + lv_obj_add_state(id(page_prev), LV_STATE_DISABLED); + lv_obj_clear_state(id(page_next), LV_STATE_DISABLED); + } else { + lv_obj_clear_state(id(page_prev), LV_STATE_DISABLED); + lv_obj_add_state(id(page_next), LV_STATE_DISABLED); + } + + - 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(); + // Vóór de eerste tijd-sync (zie time: hierboven) is now() + // epoch 0 — dan geen nachtmodus-beslissing nemen op basis + // van een ongeldig uur, gewoon normale DIM_BRIGHTNESS. + if (now.is_valid()) { + 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: restart + parameters: + action: string + then: + - logger.log: + level: INFO + format: "publish_action: %s" + args: ["action.c_str()"] + - text_sensor.template.publish: + id: smartdisplay_action + state: !lambda 'return action;' + - delay: 500ms + - text_sensor.template.publish: + id: smartdisplay_action + state: "" + + - id: climate_target_preview + parameters: + value: int + then: + - lambda: |- + id(climate_edit_ms) = millis(); + id(climate_edit_pending) = true; + int min_x10 = id(active_climate_min_x10); + int max_x10 = id(active_climate_max_x10); + value = cyd::quantize_temperature(value, min_x10, max_x10, + id(active_climate_step_x10)); + id(active_climate_target_x10) = value; + + char buf[16]; + snprintf(buf, sizeof(buf), "%.1f°", value / 10.0f); + lv_label_set_text(id(climate_target_label), buf); + lv_arc_set_value(id(climate_arc), value); + + - id: climate_target_commit + parameters: + value: int + then: + - script.execute: + id: climate_target_preview + value: !lambda "return value;" + - if: + condition: + lambda: 'return std::string("${DIRECT_ACTIONS}") == "true";' + then: + - homeassistant.action: + action: climate.set_temperature + data: + entity_id: !lambda "return id(active_entity);" + temperature: !lambda "return id(active_climate_target_x10) / 10.0f;" + + - id: climate_adjust_target + parameters: + direction: int + then: + - lambda: |- + int step_x10 = id(active_climate_step_x10); + if (step_x10 < 5) step_x10 = 5; + int next = id(active_climate_target_x10) + (direction * step_x10); + if (next < id(active_climate_min_x10)) next = id(active_climate_min_x10); + if (next > id(active_climate_max_x10)) next = id(active_climate_max_x10); + id(active_climate_target_x10) = next; + - script.execute: + id: climate_target_commit + value: !lambda "return id(active_climate_target_x10);" + + # Opent de moduskiezer. Verbergt modi die de entiteit niet ondersteunt en + # markeert de huidige, zodat je ziet waar je staat voor je iets kiest. + # Opent de stofzuigerbediening en onthoudt om welke entiteit het gaat. + - id: open_vacuum_overlay + mode: restart + parameters: + title: string + entity: string + then: + - lambda: |- + id(active_entity) = entity; + lv_label_set_text(id(vacuum_title), title.c_str()); + - lvgl.widget.show: vacuum_overlay + + # Een losse opdracht: start, pause, stop of return_to_base. + - id: vacuum_action + mode: queued + parameters: + service: string + then: + - if: + condition: + lambda: 'return 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() || supported == "unknown" || supported == "unavailable") return false; + return supported.find("'" + mode + "'") != std::string::npos || + supported.find("\"" + mode + "\"") != std::string::npos; + }; + auto apply = [&](lv_obj_t* btn, const char* m) { + if (has(m)) lv_obj_clear_flag(btn, LV_OBJ_FLAG_HIDDEN); + else lv_obj_add_flag(btn, LV_OBJ_FLAG_HIDDEN); + bool selected = (current == std::string(m)); + lv_obj_set_style_bg_color(btn, + lv_color_hex(selected ? 0xFF9F0A : 0x2C2C30), LV_PART_MAIN); + }; + apply(id(cmode_off), "off"); + apply(id(cmode_heat), "heat"); + apply(id(cmode_cool), "cool"); + apply(id(cmode_heat_cool), "heat_cool"); + apply(id(cmode_auto), "auto"); + apply(id(cmode_dry), "dry"); + apply(id(cmode_fan_only), "fan_only"); + - lvgl.widget.show: climate_mode_overlay + + # Zet precies een modus. Geen tussenstappen, dus geen ongewenste + # commando's naar de warmtepomp onderweg. + - id: set_climate_mode + mode: restart + parameters: + mode: string + then: + - lambda: |- + id(climate_edit_ms) = millis(); + id(climate_edit_pending) = true; + id(active_climate_mode) = mode; + if (mode == "off") lv_label_set_text(id(climate_mode_label), "Off"); + if (mode == "heat") lv_label_set_text(id(climate_mode_label), "Heat"); + if (mode == "cool") lv_label_set_text(id(climate_mode_label), "Cool"); + if (mode == "heat_cool") lv_label_set_text(id(climate_mode_label), "Heat/Cool"); + if (mode == "auto") lv_label_set_text(id(climate_mode_label), "Auto"); + if (mode == "dry") lv_label_set_text(id(climate_mode_label), "Dry"); + if (mode == "fan_only") lv_label_set_text(id(climate_mode_label), "Fan"); + - lvgl.widget.hide: climate_mode_overlay + - if: + condition: + lambda: 'return 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 ? 192 : 152; + lv_obj_set_size(id(color_palette_card), 225, 172); + lv_obj_set_y(id(color_palette_card), palette_y); + lv_obj_set_pos(id(color_preset_red), 20, 8); + lv_obj_set_pos(id(color_preset_orange), 90, 8); + lv_obj_set_pos(id(color_preset_yellow), 160, 8); + lv_obj_set_pos(id(color_preset_green), 20, 104); + lv_obj_set_pos(id(color_preset_blue), 90, 104); + lv_obj_set_pos(id(color_preset_purple), 160, 104); + + lv_obj_set_size(id(temp_palette_card), 225, 252); + lv_obj_set_y(id(temp_palette_card), palette_y); + lv_obj_set_pos(id(temp_preset_2700), 66, 8); + lv_obj_set_pos(id(temp_preset_4000), 66, 96); + lv_obj_set_pos(id(temp_preset_6500), 66, 184); + + lv_obj_set_y(id(color_brightness_card), 512); + } else { + int palette_y = show_tabs ? 196 : 156; + lv_obj_set_size(id(color_palette_card), 429, 96); + lv_obj_set_y(id(color_palette_card), palette_y); + lv_obj_set_pos(id(color_preset_red), 21, 16); + lv_obj_set_pos(id(color_preset_orange), 92, 16); + lv_obj_set_pos(id(color_preset_yellow), 162, 16); + lv_obj_set_pos(id(color_preset_green), 232, 16); + lv_obj_set_pos(id(color_preset_blue), 303, 16); + lv_obj_set_pos(id(color_preset_purple), 374, 16); + + lv_obj_set_size(id(temp_palette_card), 429, 96); + lv_obj_set_y(id(temp_palette_card), palette_y); + lv_obj_set_pos(id(temp_preset_2700), 36, 16); + lv_obj_set_pos(id(temp_preset_4000), 168, 16); + lv_obj_set_pos(id(temp_preset_6500), 300, 16); + + lv_obj_set_y(id(color_brightness_card), 330); + } + + 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), 3); + } 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), 142); + } 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: |- + if (std::string("${DIRECT_ACTIONS}") != "true") return false; + // Debounce: zelfde entity binnen 600ms genegeerd. Voorkomt dat + // touch-ruis dezelfde HA-actie meerdere keren afvuurt (zie + // globals hierboven voor waarom dit nodig is). + uint32_t now = millis(); + if (entity_id == id(last_action_entity) && (now - id(last_action_ms)) < 600) { + ESP_LOGD("tile", "do_tile_action: debounced repeat for %s (%ums na vorige)", + entity_id.c_str(), now - id(last_action_ms)); + return false; + } + id(last_action_entity) = entity_id; + id(last_action_ms) = now; + return true; + then: + - lambda: |- + ESP_LOGD("tile", "do_tile_action type=%s action=%s service=%s entity=%s key=%s val=%s was_on=%d", + tile_type.c_str(), tap_action.c_str(), service.c_str(), entity_id.c_str(), + param_key.c_str(), param_val.c_str(), (int)was_on); + + - if: + condition: + lambda: |- + return tap_action == "fan_toggle_preset"; + then: + - if: + condition: + lambda: |- + return was_on; + then: + - homeassistant.action: + action: fan.turn_off + data: + entity_id: !lambda 'return entity_id;' + else: + - homeassistant.action: + action: fan.turn_on + data: + entity_id: !lambda 'return entity_id;' + - delay: 150ms + - homeassistant.action: + action: fan.set_preset_mode + data: + entity_id: !lambda 'return entity_id;' + preset_mode: !lambda 'return param_val;' + + else: + - if: + condition: + lambda: |- + std::string resolved = tap_action; + if (resolved == "auto") { + if (tile_type == "scene" || tile_type == "script") resolved = "activate"; + else resolved = "toggle"; + } + return resolved == "toggle"; + then: + - if: + condition: + lambda: |- + return service.size() > 0; + then: + - homeassistant.action: + action: !lambda 'return service;' + data: + entity_id: !lambda 'return entity_id;' + else: + - if: + condition: + lambda: |- + return tile_type == "light"; + then: + - homeassistant.action: + action: light.toggle + data: + entity_id: !lambda 'return entity_id;' + else: + - if: + condition: + lambda: |- + return tile_type == "fan"; + then: + - homeassistant.action: + action: fan.toggle + data: + entity_id: !lambda 'return entity_id;' + else: + - if: + condition: + lambda: |- + return tile_type == "switch"; + then: + - homeassistant.action: + action: switch.toggle + data: + entity_id: !lambda 'return entity_id;' + else: + - if: + condition: + lambda: |- + return tile_type == "cover"; + then: + - homeassistant.action: + action: cover.set_cover_position + data: + entity_id: !lambda 'return entity_id;' + position: !lambda |- + return was_on ? 0 : 100; + + else: + - if: + condition: + lambda: |- + std::string resolved = tap_action; + if (resolved == "auto") { + if (tile_type == "scene" || tile_type == "script") resolved = "activate"; + else resolved = "toggle"; + } + return resolved == "activate"; + then: + - if: + condition: + lambda: |- + return service.size() > 0; + then: + - if: + condition: + lambda: |- + return param_key == "preset_mode"; + then: + - homeassistant.action: + action: !lambda 'return service;' + data: + entity_id: !lambda 'return entity_id;' + preset_mode: !lambda 'return param_val;' + else: + - if: + condition: + lambda: |- + return param_key == "brightness_pct"; + then: + - homeassistant.action: + action: !lambda 'return service;' + data: + entity_id: !lambda 'return entity_id;' + brightness_pct: !lambda 'return atoi(param_val.c_str());' + else: + - if: + condition: + lambda: |- + return param_key == "percentage"; + then: + - homeassistant.action: + action: !lambda 'return service;' + data: + entity_id: !lambda 'return entity_id;' + percentage: !lambda 'return atoi(param_val.c_str());' + else: + - if: + condition: + lambda: |- + return param_key == "option"; + then: + - homeassistant.action: + action: !lambda 'return service;' + data: + entity_id: !lambda 'return entity_id;' + option: !lambda 'return param_val;' + else: + - homeassistant.action: + action: !lambda 'return service;' + data: + entity_id: !lambda 'return entity_id;' + else: + - if: + condition: + lambda: |- + return tile_type == "scene"; + then: + - homeassistant.action: + action: scene.turn_on + data: + entity_id: !lambda 'return entity_id;' + else: + - if: + condition: + lambda: |- + return tile_type == "script"; + then: + - homeassistant.action: + action: script.turn_on + data: + entity_id: !lambda 'return entity_id;' + else: + - if: + condition: + lambda: |- + return tile_type == "light"; + then: + - homeassistant.action: + action: light.turn_on + data: + entity_id: !lambda 'return entity_id;' + else: + - if: + condition: + lambda: |- + return tile_type == "fan"; + then: + - homeassistant.action: + action: fan.turn_on + data: + entity_id: !lambda 'return entity_id;' + else: + - if: + condition: + lambda: |- + return tile_type == "switch"; + then: + - homeassistant.action: + action: switch.turn_on + data: + entity_id: !lambda 'return entity_id;' + else: + - if: + condition: + lambda: |- + return tile_type == "cover"; + then: + - homeassistant.action: + action: cover.open_cover + data: + entity_id: !lambda 'return entity_id;' + + - id: open_value_overlay + parameters: + title: string + entity: string + tile_type: string + longpress_mode: string + was_on: bool + off_value: int + brightness_value: float + percentage_value: float + position_value: float + icon_text: string + slider_bg_color: uint32_t + slider_fill_color: uint32_t + slider_icon_color: uint32_t + label_off: string + label_on: string + supported_color_modes: string + climate_current_temperature: float + climate_target_temperature: float + climate_humidity: float + climate_mode: string + climate_supported_modes: string + climate_min_temp: string + climate_max_temp: string + climate_step: string + then: + - lambda: |- + // Accentkleur uit het Home Assistant-domein: het stuk voor de punt in + // het entity_id. script/scene/button vuur je af (ACCENT_ACTION), een + // light is een light (ACCENT_LIGHT), de rest is apparaat. + // LET OP: deze helper staat twee keer in dit bestand — hier en in + // open_value_overlay. Pas ze samen aan. + auto accent_for = [](const std::string& entity_id) -> uint32_t { + std::string d = entity_id.substr(0, entity_id.find('.')); + if (d == "script" || d == "scene" || d == "button" || + d == "input_button" || d == "automation") return ${ACCENT_ACTION}; + if (d == "light") return ${ACCENT_LIGHT}; + return ${ACCENT_DEVICE}; + }; + + // De sliderkleur komt van de tegel, dus die kan ook ${ACCENT_AUTO} zijn. + uint32_t fill_color = (slider_fill_color == ${ACCENT_AUTO}) + ? accent_for(entity) : slider_fill_color; + + std::string mode = longpress_mode; + auto contains_mode = [](const std::string& modes, const std::string& needle) { + return modes.find(needle) != std::string::npos; + }; + if (mode == "auto") { + if (tile_type == "light") mode = "brightness"; + else if (tile_type == "fan") mode = "percentage"; + else if (tile_type == "cover") mode = "cover_position"; + else if (tile_type == "climate") mode = "climate_temperature"; + else mode = "none"; + } + + if (mode == "none") return; + + id(active_entity) = entity; + id(active_value_suffix) = " %"; + id(active_label_off) = label_off; + id(active_label_on) = label_on; + id(active_title) = title; + id(active_icon_text) = icon_text; + id(active_supports_color) = false; + id(active_supports_temperature) = false; + lv_obj_add_flag(id(overlay_color_button), LV_OBJ_FLAG_HIDDEN); + + if (mode == "climate_temperature") { + auto to_x10 = [](float value, int fallback) -> int { + if (isnan(value)) return fallback; + return (int) lround(value * 10.0f); + }; + auto parse_x10 = [](const std::string& value, int fallback) -> int { + if (value.empty()) return fallback; + return (int) lround(atof(value.c_str()) * 10.0f); + }; + id(climate_edit_pending) = false; + id(active_control_kind) = "climate_temperature"; + id(active_climate_min_x10) = parse_x10(climate_min_temp, 50); + id(active_climate_max_x10) = parse_x10(climate_max_temp, 300); + id(active_climate_step_x10) = parse_x10(climate_step, 5); + if (id(active_climate_step_x10) < 5) id(active_climate_step_x10) = 5; + if (id(active_climate_max_x10) <= id(active_climate_min_x10)) { + id(active_climate_min_x10) = 50; + id(active_climate_max_x10) = 300; + } + + id(active_climate_current_x10) = to_x10(climate_current_temperature, 0); + int fallback_target = isnan(climate_current_temperature) ? 200 : id(active_climate_current_x10); + id(active_climate_target_x10) = to_x10(climate_target_temperature, fallback_target); + if (id(active_climate_target_x10) < id(active_climate_min_x10)) id(active_climate_target_x10) = id(active_climate_min_x10); + if (id(active_climate_target_x10) > id(active_climate_max_x10)) id(active_climate_target_x10) = id(active_climate_max_x10); + id(active_climate_target_x10) = id(active_climate_min_x10) + (int) lround((id(active_climate_target_x10) - id(active_climate_min_x10)) / (float) id(active_climate_step_x10)) * id(active_climate_step_x10); + if (id(active_climate_target_x10) < id(active_climate_min_x10)) id(active_climate_target_x10) = id(active_climate_min_x10); + if (id(active_climate_target_x10) > id(active_climate_max_x10)) id(active_climate_target_x10) = id(active_climate_max_x10); + id(active_climate_humidity) = isnan(climate_humidity) ? -1 : (int) lround(climate_humidity); + id(active_climate_mode) = climate_mode.empty() ? std::string("Auto") : climate_mode; + id(active_climate_supported_modes) = climate_supported_modes; + + lv_label_set_text(id(climate_detail_title), title.c_str()); + + char current_buf[48]; + if (isnan(climate_current_temperature)) { + snprintf(current_buf, sizeof(current_buf), "Current: --"); + } else if (id(active_climate_humidity) >= 0) { + snprintf(current_buf, sizeof(current_buf), "Current: %.1f° - Humidity: %d %%", climate_current_temperature, id(active_climate_humidity)); + } else { + snprintf(current_buf, sizeof(current_buf), "Current: %.1f°", climate_current_temperature); + } + lv_label_set_text(id(climate_current_label), current_buf); + + char target_buf[16]; + snprintf(target_buf, sizeof(target_buf), "%.1f°", id(active_climate_target_x10) / 10.0f); + lv_label_set_text(id(climate_target_label), target_buf); + lv_label_set_text(id(climate_mode_label), id(active_climate_mode).c_str()); + + lv_arc_set_range(id(climate_arc), id(active_climate_min_x10), id(active_climate_max_x10)); + lv_arc_set_value(id(climate_arc), id(active_climate_target_x10)); + + if (atoi("${DISPLAY_W}") < atoi("${DISPLAY_H}")) { + lv_obj_set_size(id(climate_arc), 294, 294); + lv_obj_set_y(id(climate_arc), 16); + lv_obj_set_y(id(climate_current_label), 96); + lv_obj_set_y(id(climate_minus_button), -124); + lv_obj_set_y(id(climate_plus_button), -124); + lv_obj_set_y(id(climate_mode_pill), -36); + } else { + lv_obj_set_size(id(climate_arc), 264, 264); + lv_obj_set_y(id(climate_arc), 36); + lv_obj_set_y(id(climate_current_label), 80); + lv_obj_set_y(id(climate_minus_button), -28); + lv_obj_set_y(id(climate_plus_button), -28); + lv_obj_set_y(id(climate_mode_pill), -30); + } + + lv_obj_add_flag(id(brightness_overlay), LV_OBJ_FLAG_HIDDEN); + lv_obj_clear_flag(id(climate_detail_overlay), LV_OBJ_FLAG_HIDDEN); + return; + } + + int pct = off_value; + + if (mode == "brightness") { + id(active_control_kind) = "light_brightness"; + if (was_on) { + if (isnan(brightness_value)) pct = 100; + else pct = (int) lround((brightness_value / 255.0f) * 100.0f); + } + } else if (mode == "percentage") { + id(active_control_kind) = "fan_percentage"; + if (was_on) { + if (isnan(percentage_value)) pct = 0; + else pct = (int) lround(percentage_value); + } + } else if (mode == "cover_position") { + id(active_control_kind) = "cover_position"; + if (isnan(position_value)) pct = was_on ? 100 : 0; + else pct = (int) lround(position_value); + } else { + return; + } + + if (pct < 0) pct = 0; + if (pct > 100) pct = 100; + id(active_brightness_pct) = pct; + + if (tile_type == "light" && std::string("${DIRECT_ACTIONS}") == "true") { + id(active_supports_color) = + contains_mode(supported_color_modes, "hs") || + contains_mode(supported_color_modes, "rgb") || + contains_mode(supported_color_modes, "rgbw") || + contains_mode(supported_color_modes, "rgbww") || + contains_mode(supported_color_modes, "xy"); + id(active_supports_temperature) = contains_mode(supported_color_modes, "color_temp"); + + if (id(active_supports_color) || id(active_supports_temperature)) { + lv_obj_clear_flag(id(overlay_color_button), LV_OBJ_FLAG_HIDDEN); + } + } + + if (tile_type == "cover") { + lv_obj_set_style_bg_color(id(overlay_slider), lv_color_hex(fill_color), LV_PART_MAIN); + lv_obj_set_style_bg_color(id(overlay_slider), lv_color_hex(slider_bg_color), LV_PART_INDICATOR); + lv_obj_set_style_bg_opa(id(overlay_slider), LV_OPA_COVER, LV_PART_MAIN); + } + else{ + lv_obj_set_style_bg_color(id(overlay_slider), lv_color_hex(slider_bg_color), LV_PART_MAIN); + lv_obj_set_style_bg_color(id(overlay_slider), lv_color_hex(fill_color), LV_PART_INDICATOR); + lv_obj_set_style_bg_opa(id(overlay_slider), LV_OPA_COVER, LV_PART_MAIN); + } + lv_obj_set_style_radius(id(overlay_slider), 28, LV_PART_MAIN); + lv_obj_set_style_radius(id(overlay_slider), 0, LV_PART_INDICATOR); + + lv_label_set_text(id(overlay_title), title.c_str()); + lv_label_set_text(id(overlay_icon), icon_text.c_str()); + lv_slider_set_value(id(overlay_slider), pct, LV_ANIM_OFF); + + std::string value_text; + + if (tile_type == "cover") { + if (pct <= 0) { + value_text = label_off; + } else if (pct >= 100) { + value_text = label_on; + } else { + char buf[40]; + snprintf(buf, sizeof(buf), "%s - %d%%", label_on.c_str(), pct); + value_text = buf; + } + } else { + if (pct <= 0) { + value_text = label_off; + } else { + char buf[20]; + snprintf(buf, sizeof(buf), "%d%s", pct, id(active_value_suffix).c_str()); + value_text = buf; + } + } + + lv_label_set_text(id(overlay_value), value_text.c_str()); + + lv_obj_set_style_bg_opa(id(overlay_slider), LV_OPA_COVER, LV_PART_INDICATOR); + + lv_obj_set_style_text_color(id(overlay_icon), lv_color_hex(slider_icon_color), 0); + + - if: + condition: + lambda: 'return id(active_control_kind) != "climate_temperature";' + then: + - lvgl.widget.show: brightness_overlay + + # UI refresh (values + time + styles) + render + - id: ui_refresh + mode: single + then: + - delay: 100ms + - lambda: |- + auto set_lbl = [](lv_obj_t* lbl, const std::string& s) { + if (lbl && s != lv_label_get_text(lbl)) lv_label_set_text(lbl, s.c_str()); + }; + + auto set_value_auto = [&](lv_obj_t* lbl, + bool on, + const std::string& tile_type, + const std::string& value_mode, + float brightness, + float percentage, + const std::string& preset, + float climate_current_temperature, + float climate_target_temperature, + float climate_humidity, + const std::string& label_on, + const std::string& label_off) { + std::string mode = value_mode; + + if (mode == "auto" || mode.empty()) { + if (tile_type == "light") { + mode = "brightness"; + } else if (tile_type == "fan") { + mode = preset.empty() ? "percentage" : "preset"; + } else if (tile_type == "climate") { + mode = "climate"; + } else { + mode = "text"; + } + } + + if (mode == "brightness") { + if (!on) { + set_lbl(lbl, label_off); + return; + } + if (isnan(brightness)) { + set_lbl(lbl, "100 %"); + return; + } + int pct = (int) lround((brightness / 255.0f) * 100.0f); + char buf[8]; + snprintf(buf, sizeof(buf), "%d %%", pct); + set_lbl(lbl, std::string(buf)); + return; + } + + if (mode == "percentage") { + if (!on) { + set_lbl(lbl, label_off); + return; + } + int pct = isnan(percentage) ? 0 : (int) lround(percentage); + char buf[8]; + snprintf(buf, sizeof(buf), "%d %%", pct); + set_lbl(lbl, std::string(buf)); + return; + } + + if (mode == "preset") { + if (!on) { + set_lbl(lbl, label_off); + return; + } + if (!preset.empty()) { + set_lbl(lbl, preset); + } else { + set_lbl(lbl, label_on); + } + return; + } + + if (mode == "climate") { + if (!on) { + set_lbl(lbl, label_off); + return; + } + + float temp = isnan(climate_target_temperature) ? climate_current_temperature : climate_target_temperature; + if (isnan(temp)) { + set_lbl(lbl, label_on); + return; + } + + char buf[16]; + snprintf(buf, sizeof(buf), "%.1f °", temp); + set_lbl(lbl, std::string(buf)); + return; + } + + if (mode == "text") { + if (tile_type == "scene" || tile_type == "script") { + set_lbl(lbl, label_on); + } else { + set_lbl(lbl, on ? label_on : label_off); + } + return; + } + + set_lbl(lbl, "—"); + }; + + // active_state niet leeg? Dan telt alleen die exacte HA-state als "aan". + // Zo kan een input_select ("Vol"/"Leeg") ook een tegel laten oplichten. + auto is_active = [](const std::string& tile_type, const std::string& state, + const std::string& active_state = std::string()) { + if (!active_state.empty()) return state == active_state; + if (tile_type == "climate") { + return !state.empty() && state != "off" && state != "unavailable" && state != "unknown"; + } + if (tile_type == "vacuum") return state == "cleaning" || state == "returning"; + if (tile_type == "scene" || tile_type == "script") return true; + return state == "on"; + }; + + // Cover-aware active state: "closed" = off, anything else (open/opening/closing) = on + bool t1_on = (std::string("${TILE1_TYPE}") == "cover") ? (id(tile1_cover_state).state != "closed") : is_active("${TILE1_TYPE}", std::string(id(ha_state_tile1).state.c_str()), "${TILE1_ACTIVE_STATE}"); + bool t2_on = (std::string("${TILE2_TYPE}") == "cover") ? (id(tile2_cover_state).state != "closed") : is_active("${TILE2_TYPE}", std::string(id(ha_state_tile2).state.c_str()), "${TILE2_ACTIVE_STATE}"); + bool t3_on = (std::string("${TILE3_TYPE}") == "cover") ? (id(tile3_cover_state).state != "closed") : is_active("${TILE3_TYPE}", std::string(id(ha_state_tile3).state.c_str()), "${TILE3_ACTIVE_STATE}"); + bool t4_on = (std::string("${TILE4_TYPE}") == "cover") ? (id(tile4_cover_state).state != "closed") : is_active("${TILE4_TYPE}", std::string(id(ha_state_tile4).state.c_str()), "${TILE4_ACTIVE_STATE}"); + bool t5_on = (std::string("${TILE5_TYPE}") == "cover") ? (id(tile5_cover_state).state != "closed") : is_active("${TILE5_TYPE}", std::string(id(ha_state_tile5).state.c_str()), "${TILE5_ACTIVE_STATE}"); + bool t6_on = (std::string("${TILE6_TYPE}") == "cover") ? (id(tile6_cover_state).state != "closed") : is_active("${TILE6_TYPE}", std::string(id(ha_state_tile6).state.c_str()), "${TILE6_ACTIVE_STATE}"); + // Tegel 7/8 hebben geen cover_state-sensor, dus geen cover-tak nodig. + bool t7_on = is_active("${TILE7_TYPE}", std::string(id(ha_state_tile7).state.c_str()), "${TILE7_ACTIVE_STATE}"); + bool t8_on = is_active("${TILE8_TYPE}", std::string(id(ha_state_tile8).state.c_str()), "${TILE8_ACTIVE_STATE}"); + bool t9_on = is_active("${TILE9_TYPE}", std::string(id(ha_state_tile9).state.c_str()), "${TILE9_ACTIVE_STATE}"); + bool t10_on = is_active("${TILE10_TYPE}", std::string(id(ha_state_tile10).state.c_str()), "${TILE10_ACTIVE_STATE}"); + + set_value_auto(id(t1_value), t1_on, "${TILE1_TYPE}", "${TILE1_VALUE_MODE}", + id(tile1_brightness).state, id(tile1_percentage).state, std::string(id(tile1_preset).state.c_str()), + id(tile1_climate_current_temperature).state, id(tile1_climate_target_temperature).state, id(tile1_climate_humidity).state, + "${TILE1_LABEL_ON}", "${TILE1_LABEL_OFF}"); + + set_value_auto(id(t2_value), t2_on, "${TILE2_TYPE}", "${TILE2_VALUE_MODE}", + id(tile2_brightness).state, id(tile2_percentage).state, std::string(id(tile2_preset).state.c_str()), + id(tile2_climate_current_temperature).state, id(tile2_climate_target_temperature).state, id(tile2_climate_humidity).state, + "${TILE2_LABEL_ON}", "${TILE2_LABEL_OFF}"); + + set_value_auto(id(t3_value), t3_on, "${TILE3_TYPE}", "${TILE3_VALUE_MODE}", + id(tile3_brightness).state, id(tile3_percentage).state, std::string(id(tile3_preset).state.c_str()), + id(tile3_climate_current_temperature).state, id(tile3_climate_target_temperature).state, id(tile3_climate_humidity).state, + "${TILE3_LABEL_ON}", "${TILE3_LABEL_OFF}"); + + set_value_auto(id(t4_value), t4_on, "${TILE4_TYPE}", "${TILE4_VALUE_MODE}", + id(tile4_brightness).state, id(tile4_percentage).state, std::string(id(tile4_preset).state.c_str()), + id(tile4_climate_current_temperature).state, id(tile4_climate_target_temperature).state, id(tile4_climate_humidity).state, + "${TILE4_LABEL_ON}", "${TILE4_LABEL_OFF}"); + + set_value_auto(id(t5_value), t5_on, "${TILE5_TYPE}", "${TILE5_VALUE_MODE}", + id(tile5_brightness).state, id(tile5_percentage).state, std::string(id(tile5_preset).state.c_str()), + id(tile5_climate_current_temperature).state, id(tile5_climate_target_temperature).state, id(tile5_climate_humidity).state, + "${TILE5_LABEL_ON}", "${TILE5_LABEL_OFF}"); + + set_value_auto(id(t6_value), t6_on, "${TILE6_TYPE}", "${TILE6_VALUE_MODE}", + id(tile6_brightness).state, id(tile6_percentage).state, std::string(id(tile6_preset).state.c_str()), + id(tile6_climate_current_temperature).state, id(tile6_climate_target_temperature).state, id(tile6_climate_humidity).state, + "${TILE6_LABEL_ON}", "${TILE6_LABEL_OFF}"); + + // Tegel 7: light met alleen brightness. Tegel 8: puur tekst (Vol/Leeg). + set_value_auto(id(t7_value), t7_on, "${TILE7_TYPE}", "${TILE7_VALUE_MODE}", + id(tile7_brightness).state, NAN, std::string(), + NAN, NAN, NAN, + "${TILE7_LABEL_ON}", "${TILE7_LABEL_OFF}"); + + set_value_auto(id(t8_value), t8_on, "${TILE8_TYPE}", "${TILE8_VALUE_MODE}", + NAN, NAN, std::string(), + NAN, NAN, NAN, + "${TILE8_LABEL_ON}", "${TILE8_LABEL_OFF}"); + + // Tegel 9: kleurenlamp (brightness). Tegel 10: puur tekst (Aan/Uit). + set_value_auto(id(t9_value), t9_on, "${TILE9_TYPE}", "${TILE9_VALUE_MODE}", + id(tile9_brightness).state, NAN, std::string(), + NAN, NAN, NAN, + "${TILE9_LABEL_ON}", "${TILE9_LABEL_OFF}"); + + set_value_auto(id(t10_value), t10_on, "${TILE10_TYPE}", "${TILE10_VALUE_MODE}", + NAN, NAN, std::string(), + NAN, NAN, NAN, + "${TILE10_LABEL_ON}", "${TILE10_LABEL_OFF}"); + + // Missing state is not an off state. Preserve meaningful vacuum states. + const char* vacuum_state = id(ha_state_tile6).state.c_str(); + if (std::string("${TILE6_TYPE}") == "vacuum") { + std::string state(vacuum_state); + if (state == "cleaning") set_lbl(id(t6_value), "Bezig"); + else if (state == "docked") set_lbl(id(t6_value), "In dock"); + else if (state == "returning") set_lbl(id(t6_value), "Naar dock"); + else if (state == "paused") set_lbl(id(t6_value), "Gepauzeerd"); + else if (state == "error") set_lbl(id(t6_value), "Fout"); + else if (!state.empty()) set_lbl(id(t6_value), state); + } + if (!id(ha_state_tile1).has_state() || id(ha_state_tile1).state == "unknown" || id(ha_state_tile1).state == "unavailable") + set_lbl(id(t1_value), "Niet beschikbaar"); + if (!id(ha_state_tile2).has_state() || id(ha_state_tile2).state == "unknown" || id(ha_state_tile2).state == "unavailable") + set_lbl(id(t2_value), "Niet beschikbaar"); + if (!id(ha_state_tile3).has_state() || id(ha_state_tile3).state == "unknown" || id(ha_state_tile3).state == "unavailable") + set_lbl(id(t3_value), "Niet beschikbaar"); + if (!id(ha_state_tile4).has_state() || id(ha_state_tile4).state == "unknown" || id(ha_state_tile4).state == "unavailable") + set_lbl(id(t4_value), "Niet beschikbaar"); + if (!id(ha_state_tile5).has_state() || id(ha_state_tile5).state == "unknown" || id(ha_state_tile5).state == "unavailable") + set_lbl(id(t5_value), "Niet beschikbaar"); + if (!id(ha_state_tile6).has_state() || id(ha_state_tile6).state == "unknown" || id(ha_state_tile6).state == "unavailable") + set_lbl(id(t6_value), "Niet beschikbaar"); + if (!id(ha_state_tile7).has_state() || id(ha_state_tile7).state == "unknown" || id(ha_state_tile7).state == "unavailable") + set_lbl(id(t7_value), "Niet beschikbaar"); + if (!id(ha_state_tile8).has_state() || id(ha_state_tile8).state == "unknown" || id(ha_state_tile8).state == "unavailable") + set_lbl(id(t8_value), "Niet beschikbaar"); + if (!id(ha_state_tile9).has_state() || id(ha_state_tile9).state == "unknown" || id(ha_state_tile9).state == "unavailable") + set_lbl(id(t9_value), "Niet beschikbaar"); + if (!id(ha_state_tile10).has_state() || id(ha_state_tile10).state == "unknown" || id(ha_state_tile10).state == "unavailable") + set_lbl(id(t10_value), "Niet beschikbaar"); + if (id(active_control_kind) == "climate_temperature" && !lv_obj_has_flag(id(climate_detail_overlay), LV_OBJ_FLAG_HIDDEN)) { + auto label_for_climate_mode = [](std::string mode) { + for (char &c : mode) { + if (c >= 'A' && c <= 'Z') c = (char) (c - 'A' + 'a'); + } + if (mode == "off") return std::string("Off"); + if (mode == "heat") return std::string("Heat"); + if (mode == "cool") return std::string("Cool"); + if (mode == "heat_cool") return std::string("Heat/Cool"); + if (mode == "auto") return std::string("Auto"); + if (mode == "dry") return std::string("Dry"); + if (mode == "fan_only") return std::string("Fan"); + return mode.empty() ? std::string("Auto") : mode; + }; + auto sync_open_climate = [&](const std::string& entity, + float current_temperature, + float target_temperature, + float humidity, + const std::string& climate_mode) { + if (id(active_entity) != entity) return; + auto to_x10 = [](float value, int fallback) -> int { + if (isnan(value)) return fallback; + return (int) lround(value * 10.0f); + }; + + id(active_climate_current_x10) = to_x10(current_temperature, id(active_climate_current_x10)); + id(active_climate_humidity) = isnan(humidity) ? -1 : (int) lround(humidity); + if (!climate_mode.empty() && !id(touch_down) && + !(id(climate_edit_pending) && millis() - id(climate_edit_ms) < 3000)) { + id(active_climate_mode) = climate_mode; + lv_label_set_text(id(climate_mode_label), label_for_climate_mode(climate_mode).c_str()); + } + + const bool editing = id(touch_down) || + (id(climate_edit_pending) && millis() - id(climate_edit_ms) < 3000); + if (!editing) id(climate_edit_pending) = false; + int target_x10 = editing ? id(active_climate_target_x10) : + to_x10(target_temperature, id(active_climate_target_x10)); + if (target_x10 < id(active_climate_min_x10)) target_x10 = id(active_climate_min_x10); + if (target_x10 > id(active_climate_max_x10)) target_x10 = id(active_climate_max_x10); + int step_x10 = id(active_climate_step_x10); + if (step_x10 < 5) step_x10 = 5; + target_x10 = id(active_climate_min_x10) + (int) lround((target_x10 - id(active_climate_min_x10)) / (float) step_x10) * step_x10; + if (target_x10 < id(active_climate_min_x10)) target_x10 = id(active_climate_min_x10); + if (target_x10 > id(active_climate_max_x10)) target_x10 = id(active_climate_max_x10); + id(active_climate_target_x10) = target_x10; + + char current_buf[48]; + if (isnan(current_temperature)) { + snprintf(current_buf, sizeof(current_buf), "Current: --"); + } else if (id(active_climate_humidity) >= 0) { + snprintf(current_buf, sizeof(current_buf), "Current: %.1f° - Humidity: %d %%", current_temperature, id(active_climate_humidity)); + } else { + snprintf(current_buf, sizeof(current_buf), "Current: %.1f°", current_temperature); + } + lv_label_set_text(id(climate_current_label), current_buf); + + char target_buf[16]; + snprintf(target_buf, sizeof(target_buf), "%.1f°", id(active_climate_target_x10) / 10.0f); + lv_label_set_text(id(climate_target_label), target_buf); + lv_arc_set_value(id(climate_arc), id(active_climate_target_x10)); + }; + + if (std::string("${TILE1_TYPE}") == "climate") sync_open_climate("${TILE1_ENTITY}", id(tile1_climate_current_temperature).state, id(tile1_climate_target_temperature).state, id(tile1_climate_humidity).state, std::string(id(tile1_climate_mode).state.c_str())); + if (std::string("${TILE2_TYPE}") == "climate") sync_open_climate("${TILE2_ENTITY}", id(tile2_climate_current_temperature).state, id(tile2_climate_target_temperature).state, id(tile2_climate_humidity).state, std::string(id(tile2_climate_mode).state.c_str())); + if (std::string("${TILE3_TYPE}") == "climate") sync_open_climate("${TILE3_ENTITY}", id(tile3_climate_current_temperature).state, id(tile3_climate_target_temperature).state, id(tile3_climate_humidity).state, std::string(id(tile3_climate_mode).state.c_str())); + if (std::string("${TILE4_TYPE}") == "climate") sync_open_climate("${TILE4_ENTITY}", id(tile4_climate_current_temperature).state, id(tile4_climate_target_temperature).state, id(tile4_climate_humidity).state, std::string(id(tile4_climate_mode).state.c_str())); + if (std::string("${TILE5_TYPE}") == "climate") sync_open_climate("${TILE5_ENTITY}", id(tile5_climate_current_temperature).state, id(tile5_climate_target_temperature).state, id(tile5_climate_humidity).state, std::string(id(tile5_climate_mode).state.c_str())); + if (std::string("${TILE6_TYPE}") == "climate") sync_open_climate("${TILE6_ENTITY}", id(tile6_climate_current_temperature).state, id(tile6_climate_target_temperature).state, id(tile6_climate_humidity).state, std::string(id(tile6_climate_mode).state.c_str())); + } + + { + auto now = id(homeassistant_time).now(); + // Vóór de eerste tijd-sync is now() epoch 0 (1970) — dan geen + // misleidende "01:00" tonen, gewoon leeg laten tot er een + // echte tijd binnen is (na de eerste Home Assistant-tijdsynchronisatie). + if (now.is_valid()) { + char buf[10]; + + if (std::string("${TIME_24H}") == "true") { + snprintf(buf, sizeof(buf), "%02d:%02d", now.hour, now.minute); + } else { + int hour12 = now.hour % 12; + if (hour12 == 0) hour12 = 12; + snprintf(buf, sizeof(buf), "%d:%02d %s", hour12, now.minute, now.hour >= 12 ? "PM" : "AM"); + } + + set_lbl(id(lbl_time), std::string(buf)); + } + } + + // Accentkleur uit het Home Assistant-domein: het stuk voor de punt in + // het entity_id. script/scene/button vuur je af (ACCENT_ACTION), een + // light is een light (ACCENT_LIGHT), de rest is apparaat. + // LET OP: deze helper staat twee keer in dit bestand — hier en in + // open_value_overlay. Pas ze samen aan. + auto accent_for = [](const std::string& entity_id) -> uint32_t { + std::string d = entity_id.substr(0, entity_id.find('.')); + if (d == "script" || d == "scene" || d == "button" || + d == "input_button" || d == "automation") return ${ACCENT_ACTION}; + if (d == "light") return ${ACCENT_LIGHT}; + return ${ACCENT_DEVICE}; + }; + + auto apply_tile = [&](bool active, + const std::string& entity_id, + lv_obj_t* tile, + lv_obj_t* circle, + lv_obj_t* title, + lv_obj_t* value, + lv_obj_t* icon_lbl, + uint32_t circle_active, + uint32_t circle_disabled, + uint32_t icon_active, + uint32_t icon_disabled, + uint32_t bg_active, + uint32_t bg_disabled, + uint32_t title_active, + uint32_t title_disabled, + uint32_t value_active, + uint32_t value_disabled) + { + static lv_obj_t* cached_tiles[10] = {}; + static bool cached_active[10] = {}; + for (int i = 0; i < 10; ++i) { + if (cached_tiles[i] == tile) { + if (cached_active[i] == active) return; + cached_active[i] = active; + break; + } + if (cached_tiles[i] == nullptr) { + cached_tiles[i] = tile; + cached_active[i] = active; + break; + } + } + // ${ACCENT_AUTO} = "bepaal zelf"; een echte hexwaarde wint altijd. + uint32_t accent = accent_for(entity_id); + if (circle_active == ${ACCENT_AUTO}) circle_active = accent; + if (icon_disabled == ${ACCENT_AUTO}) icon_disabled = accent; + + lv_obj_set_style_bg_color(tile, lv_color_hex(active ? bg_active : bg_disabled), 0); + lv_obj_set_style_bg_color(circle, lv_color_hex(active ? circle_active : circle_disabled), 0); + lv_obj_set_style_text_color(icon_lbl, lv_color_hex(active ? icon_active : icon_disabled), 0); + lv_obj_set_style_text_color(title, lv_color_hex(active ? title_active : title_disabled), 0); + lv_obj_set_style_text_color(value, lv_color_hex(active ? value_active : value_disabled), 0); + }; + + apply_tile(t1_on, "${TILE1_ENTITY}", + id(tile1), id(tile1_icon_circle), id(t1_title), id(t1_value), id(tile1_icon_lbl), + ${TILE1_CIRCLE_ACTIVE_COLOR}, ${TILE1_CIRCLE_DISABLED_COLOR}, + ${TILE1_ICON_ACTIVE_COLOR}, ${TILE1_ICON_DISABLED_COLOR}, + ${TILE1_BG_ACTIVE_COLOR}, ${TILE1_BG_DISABLED_COLOR}, + ${TILE1_TITLE_ACTIVE_COLOR}, ${TILE1_TITLE_DISABLED_COLOR}, + ${TILE1_VALUE_ACTIVE_COLOR}, ${TILE1_VALUE_DISABLED_COLOR}); + + apply_tile(t2_on, "${TILE2_ENTITY}", + id(tile2), id(tile2_icon_circle), id(t2_title), id(t2_value), id(tile2_icon_lbl), + ${TILE2_CIRCLE_ACTIVE_COLOR}, ${TILE2_CIRCLE_DISABLED_COLOR}, + ${TILE2_ICON_ACTIVE_COLOR}, ${TILE2_ICON_DISABLED_COLOR}, + ${TILE2_BG_ACTIVE_COLOR}, ${TILE2_BG_DISABLED_COLOR}, + ${TILE2_TITLE_ACTIVE_COLOR}, ${TILE2_TITLE_DISABLED_COLOR}, + ${TILE2_VALUE_ACTIVE_COLOR}, ${TILE2_VALUE_DISABLED_COLOR}); + + apply_tile(t3_on, "${TILE3_ENTITY}", + id(tile3), id(tile3_icon_circle), id(t3_title), id(t3_value), id(tile3_icon_lbl), + ${TILE3_CIRCLE_ACTIVE_COLOR}, ${TILE3_CIRCLE_DISABLED_COLOR}, + ${TILE3_ICON_ACTIVE_COLOR}, ${TILE3_ICON_DISABLED_COLOR}, + ${TILE3_BG_ACTIVE_COLOR}, ${TILE3_BG_DISABLED_COLOR}, + ${TILE3_TITLE_ACTIVE_COLOR}, ${TILE3_TITLE_DISABLED_COLOR}, + ${TILE3_VALUE_ACTIVE_COLOR}, ${TILE3_VALUE_DISABLED_COLOR}); + + apply_tile(t4_on, "${TILE4_ENTITY}", + id(tile4), id(tile4_icon_circle), id(t4_title), id(t4_value), id(tile4_icon_lbl), + ${TILE4_CIRCLE_ACTIVE_COLOR}, ${TILE4_CIRCLE_DISABLED_COLOR}, + ${TILE4_ICON_ACTIVE_COLOR}, ${TILE4_ICON_DISABLED_COLOR}, + ${TILE4_BG_ACTIVE_COLOR}, ${TILE4_BG_DISABLED_COLOR}, + ${TILE4_TITLE_ACTIVE_COLOR}, ${TILE4_TITLE_DISABLED_COLOR}, + ${TILE4_VALUE_ACTIVE_COLOR}, ${TILE4_VALUE_DISABLED_COLOR}); + + apply_tile(t5_on, "${TILE5_ENTITY}", + id(tile5), id(tile5_icon_circle), id(t5_title), id(t5_value), id(tile5_icon_lbl), + ${TILE5_CIRCLE_ACTIVE_COLOR}, ${TILE5_CIRCLE_DISABLED_COLOR}, + ${TILE5_ICON_ACTIVE_COLOR}, ${TILE5_ICON_DISABLED_COLOR}, + ${TILE5_BG_ACTIVE_COLOR}, ${TILE5_BG_DISABLED_COLOR}, + ${TILE5_TITLE_ACTIVE_COLOR}, ${TILE5_TITLE_DISABLED_COLOR}, + ${TILE5_VALUE_ACTIVE_COLOR}, ${TILE5_VALUE_DISABLED_COLOR}); + + apply_tile(t6_on, "${TILE6_ENTITY}", + id(tile6), id(tile6_icon_circle), id(t6_title), id(t6_value), id(tile6_icon_lbl), + ${TILE6_CIRCLE_ACTIVE_COLOR}, ${TILE6_CIRCLE_DISABLED_COLOR}, + ${TILE6_ICON_ACTIVE_COLOR}, ${TILE6_ICON_DISABLED_COLOR}, + ${TILE6_BG_ACTIVE_COLOR}, ${TILE6_BG_DISABLED_COLOR}, + ${TILE6_TITLE_ACTIVE_COLOR}, ${TILE6_TITLE_DISABLED_COLOR}, + ${TILE6_VALUE_ACTIVE_COLOR}, ${TILE6_VALUE_DISABLED_COLOR}); + + apply_tile(t7_on, "${TILE7_ENTITY}", + id(tile7), id(tile7_icon_circle), id(t7_title), id(t7_value), id(tile7_icon_lbl), + ${TILE7_CIRCLE_ACTIVE_COLOR}, ${TILE7_CIRCLE_DISABLED_COLOR}, + ${TILE7_ICON_ACTIVE_COLOR}, ${TILE7_ICON_DISABLED_COLOR}, + ${TILE7_BG_ACTIVE_COLOR}, ${TILE7_BG_DISABLED_COLOR}, + ${TILE7_TITLE_ACTIVE_COLOR}, ${TILE7_TITLE_DISABLED_COLOR}, + ${TILE7_VALUE_ACTIVE_COLOR}, ${TILE7_VALUE_DISABLED_COLOR}); + + apply_tile(t8_on, "${TILE8_ENTITY}", + id(tile8), id(tile8_icon_circle), id(t8_title), id(t8_value), id(tile8_icon_lbl), + ${TILE8_CIRCLE_ACTIVE_COLOR}, ${TILE8_CIRCLE_DISABLED_COLOR}, + ${TILE8_ICON_ACTIVE_COLOR}, ${TILE8_ICON_DISABLED_COLOR}, + ${TILE8_BG_ACTIVE_COLOR}, ${TILE8_BG_DISABLED_COLOR}, + ${TILE8_TITLE_ACTIVE_COLOR}, ${TILE8_TITLE_DISABLED_COLOR}, + ${TILE8_VALUE_ACTIVE_COLOR}, ${TILE8_VALUE_DISABLED_COLOR}); + + apply_tile(t9_on, "${TILE9_ENTITY}", + id(tile9), id(tile9_icon_circle), id(t9_title), id(t9_value), id(tile9_icon_lbl), + ${TILE9_CIRCLE_ACTIVE_COLOR}, ${TILE9_CIRCLE_DISABLED_COLOR}, + ${TILE9_ICON_ACTIVE_COLOR}, ${TILE9_ICON_DISABLED_COLOR}, + ${TILE9_BG_ACTIVE_COLOR}, ${TILE9_BG_DISABLED_COLOR}, + ${TILE9_TITLE_ACTIVE_COLOR}, ${TILE9_TITLE_DISABLED_COLOR}, + ${TILE9_VALUE_ACTIVE_COLOR}, ${TILE9_VALUE_DISABLED_COLOR}); + + apply_tile(t10_on, "${TILE10_ENTITY}", + id(tile10), id(tile10_icon_circle), id(t10_title), id(t10_value), id(tile10_icon_lbl), + ${TILE10_CIRCLE_ACTIVE_COLOR}, ${TILE10_CIRCLE_DISABLED_COLOR}, + ${TILE10_ICON_ACTIVE_COLOR}, ${TILE10_ICON_DISABLED_COLOR}, + ${TILE10_BG_ACTIVE_COLOR}, ${TILE10_BG_DISABLED_COLOR}, + ${TILE10_TITLE_ACTIVE_COLOR}, ${TILE10_TITLE_DISABLED_COLOR}, + ${TILE10_VALUE_ACTIVE_COLOR}, ${TILE10_VALUE_DISABLED_COLOR}); diff --git a/guition-device.example.yaml b/guition-device.example.yaml new file mode 100644 index 0000000..30c170e --- /dev/null +++ b/guition-device.example.yaml @@ -0,0 +1,213 @@ +# Kopieer via: python tools/new_device.py --board guition --name display-keuken +# Bewerk alleen dit lokale profiel voor jouw scherm. +packages: + display: !include guition-4848s040.yaml + +substitutions: + DEVICE_NAME: "guition-new" + DEVICE_FRIENDLY_NAME: "Mijn Guition" + ROOM_NAME: "Woonkamer" + TILE_COUNT: "6" + DIRECT_ACTIONS: "false" # Pas na koppelen/testen op true zetten. + AUTO_DIM_TIMEOUT: "600" + CALIBRATION_ON_BOOT: "false" + + # Tegel 1 + TILE1_ENTITY: "light.voorbeeld_lamp" + TILE1_STATE_ENTITY: "light.voorbeeld_lamp" + TILE1_TITLE: "Lamp" + TILE1_TYPE: "light" + TILE1_TAP_ACTION: "auto" + TILE1_TAP_SERVICE: "" + TILE1_TAP_PARAM_KEY: "" + TILE1_TAP_PARAM_VAL: "" + TILE1_LONGPRESS: "slider" + TILE1_LONGPRESS_SLIDER: "auto" + TILE1_LONGPRESS_OFF_VALUE: "0" + TILE1_LONGPRESS_ACTION: "" + TILE1_LONGPRESS_ACTION_TYPE: "" + TILE1_LONGPRESS_ACTION_SERVICE: "" + TILE1_VALUE_MODE: "auto" + TILE1_LABEL_ON: "Aan" + TILE1_LABEL_OFF: "Uit" + TILE1_ACTIVE_STATE: "" + + # Tegel 2 + TILE2_ENTITY: "switch.voorbeeld_stekker" + TILE2_STATE_ENTITY: "switch.voorbeeld_stekker" + TILE2_TITLE: "Stekker" + TILE2_TYPE: "switch" + TILE2_TAP_ACTION: "auto" + TILE2_TAP_SERVICE: "" + TILE2_TAP_PARAM_KEY: "" + TILE2_TAP_PARAM_VAL: "" + TILE2_LONGPRESS: "none" + TILE2_LONGPRESS_SLIDER: "auto" + TILE2_LONGPRESS_OFF_VALUE: "0" + TILE2_LONGPRESS_ACTION: "" + TILE2_LONGPRESS_ACTION_TYPE: "" + TILE2_LONGPRESS_ACTION_SERVICE: "" + TILE2_VALUE_MODE: "auto" + TILE2_LABEL_ON: "Aan" + TILE2_LABEL_OFF: "Uit" + TILE2_ACTIVE_STATE: "" + + # Tegel 3 + TILE3_ENTITY: "scene.voorbeeld_avond" + TILE3_STATE_ENTITY: "scene.voorbeeld_avond" + TILE3_TITLE: "Avond" + TILE3_TYPE: "scene" + TILE3_TAP_ACTION: "auto" + TILE3_TAP_SERVICE: "" + TILE3_TAP_PARAM_KEY: "" + TILE3_TAP_PARAM_VAL: "" + TILE3_LONGPRESS: "none" + TILE3_LONGPRESS_SLIDER: "auto" + TILE3_LONGPRESS_OFF_VALUE: "0" + TILE3_LONGPRESS_ACTION: "" + TILE3_LONGPRESS_ACTION_TYPE: "" + TILE3_LONGPRESS_ACTION_SERVICE: "" + TILE3_VALUE_MODE: "auto" + TILE3_LABEL_ON: "Aan" + TILE3_LABEL_OFF: "Uit" + TILE3_ACTIVE_STATE: "" + + # Tegel 4 + TILE4_ENTITY: "script.voorbeeld_uit" + TILE4_STATE_ENTITY: "script.voorbeeld_uit" + TILE4_TITLE: "Alles uit" + TILE4_TYPE: "script" + TILE4_TAP_ACTION: "auto" + TILE4_TAP_SERVICE: "" + TILE4_TAP_PARAM_KEY: "" + TILE4_TAP_PARAM_VAL: "" + TILE4_LONGPRESS: "none" + TILE4_LONGPRESS_SLIDER: "auto" + TILE4_LONGPRESS_OFF_VALUE: "0" + TILE4_LONGPRESS_ACTION: "" + TILE4_LONGPRESS_ACTION_TYPE: "" + TILE4_LONGPRESS_ACTION_SERVICE: "" + TILE4_VALUE_MODE: "auto" + TILE4_LABEL_ON: "Aan" + TILE4_LABEL_OFF: "Uit" + TILE4_ACTIVE_STATE: "" + + # Tegel 5 + TILE5_ENTITY: "climate.voorbeeld_airco" + TILE5_STATE_ENTITY: "climate.voorbeeld_airco" + TILE5_TITLE: "Airco" + TILE5_TYPE: "climate" + TILE5_TAP_ACTION: "toggle" + TILE5_TAP_SERVICE: "climate.toggle" + TILE5_TAP_PARAM_KEY: "" + TILE5_TAP_PARAM_VAL: "" + TILE5_LONGPRESS: "slider" + TILE5_LONGPRESS_SLIDER: "auto" + TILE5_LONGPRESS_OFF_VALUE: "0" + TILE5_LONGPRESS_ACTION: "" + TILE5_LONGPRESS_ACTION_TYPE: "" + TILE5_LONGPRESS_ACTION_SERVICE: "" + TILE5_VALUE_MODE: "auto" + TILE5_LABEL_ON: "Aan" + TILE5_LABEL_OFF: "Uit" + TILE5_ACTIVE_STATE: "" + + # Tegel 6 + TILE6_ENTITY: "vacuum.voorbeeld_robot" + TILE6_STATE_ENTITY: "vacuum.voorbeeld_robot" + TILE6_TITLE: "Stofzuiger" + TILE6_TYPE: "vacuum" + TILE6_TAP_ACTION: "custom" + TILE6_TAP_SERVICE: "vacuum.start" + TILE6_TAP_PARAM_KEY: "" + TILE6_TAP_PARAM_VAL: "" + TILE6_LONGPRESS: "vacuum" + TILE6_LONGPRESS_SLIDER: "auto" + TILE6_LONGPRESS_OFF_VALUE: "0" + TILE6_LONGPRESS_ACTION: "" + TILE6_LONGPRESS_ACTION_TYPE: "" + TILE6_LONGPRESS_ACTION_SERVICE: "" + TILE6_VALUE_MODE: "auto" + TILE6_LABEL_ON: "Aan" + TILE6_LABEL_OFF: "Uit" + TILE6_ACTIVE_STATE: "" + + # Tegel 7 (verborgen zolang TILE_COUNT lager is) + TILE7_ENTITY: "light.ongebruikt_7" + TILE7_STATE_ENTITY: "light.ongebruikt_7" + TILE7_TITLE: "Tegel 7" + TILE7_TYPE: "light" + TILE7_TAP_ACTION: "auto" + TILE7_TAP_SERVICE: "" + TILE7_TAP_PARAM_KEY: "" + TILE7_TAP_PARAM_VAL: "" + TILE7_LONGPRESS: "none" + TILE7_LONGPRESS_SLIDER: "auto" + TILE7_LONGPRESS_OFF_VALUE: "0" + TILE7_LONGPRESS_ACTION: "" + TILE7_LONGPRESS_ACTION_TYPE: "" + TILE7_LONGPRESS_ACTION_SERVICE: "" + TILE7_VALUE_MODE: "auto" + TILE7_LABEL_ON: "Aan" + TILE7_LABEL_OFF: "Uit" + TILE7_ACTIVE_STATE: "" + + # Tegel 8 (verborgen zolang TILE_COUNT lager is) + TILE8_ENTITY: "light.ongebruikt_8" + TILE8_STATE_ENTITY: "light.ongebruikt_8" + TILE8_TITLE: "Tegel 8" + TILE8_TYPE: "light" + TILE8_TAP_ACTION: "auto" + TILE8_TAP_SERVICE: "" + TILE8_TAP_PARAM_KEY: "" + TILE8_TAP_PARAM_VAL: "" + TILE8_LONGPRESS: "none" + TILE8_LONGPRESS_SLIDER: "auto" + TILE8_LONGPRESS_OFF_VALUE: "0" + TILE8_LONGPRESS_ACTION: "" + TILE8_LONGPRESS_ACTION_TYPE: "" + TILE8_LONGPRESS_ACTION_SERVICE: "" + TILE8_VALUE_MODE: "auto" + TILE8_LABEL_ON: "Aan" + TILE8_LABEL_OFF: "Uit" + TILE8_ACTIVE_STATE: "" + + # Tegel 9 (verborgen zolang TILE_COUNT lager is) + TILE9_ENTITY: "light.ongebruikt_9" + TILE9_STATE_ENTITY: "light.ongebruikt_9" + TILE9_TITLE: "Tegel 9" + TILE9_TYPE: "light" + TILE9_TAP_ACTION: "auto" + TILE9_TAP_SERVICE: "" + TILE9_TAP_PARAM_KEY: "" + TILE9_TAP_PARAM_VAL: "" + TILE9_LONGPRESS: "none" + TILE9_LONGPRESS_SLIDER: "auto" + TILE9_LONGPRESS_OFF_VALUE: "0" + TILE9_LONGPRESS_ACTION: "" + TILE9_LONGPRESS_ACTION_TYPE: "" + TILE9_LONGPRESS_ACTION_SERVICE: "" + TILE9_VALUE_MODE: "auto" + TILE9_LABEL_ON: "Aan" + TILE9_LABEL_OFF: "Uit" + TILE9_ACTIVE_STATE: "" + + # Tegel 10 (verborgen zolang TILE_COUNT lager is) + TILE10_ENTITY: "light.ongebruikt_10" + TILE10_STATE_ENTITY: "light.ongebruikt_10" + TILE10_TITLE: "Tegel 10" + TILE10_TYPE: "light" + TILE10_TAP_ACTION: "auto" + TILE10_TAP_SERVICE: "" + TILE10_TAP_PARAM_KEY: "" + TILE10_TAP_PARAM_VAL: "" + TILE10_LONGPRESS: "none" + TILE10_LONGPRESS_SLIDER: "auto" + TILE10_LONGPRESS_OFF_VALUE: "0" + TILE10_LONGPRESS_ACTION: "" + TILE10_LONGPRESS_ACTION_TYPE: "" + TILE10_LONGPRESS_ACTION_SERVICE: "" + TILE10_VALUE_MODE: "auto" + TILE10_LABEL_ON: "Aan" + TILE10_LABEL_OFF: "Uit" + TILE10_ACTIVE_STATE: "" diff --git a/guition_diagnostics.h b/guition_diagnostics.h new file mode 100644 index 0000000..63e0d81 --- /dev/null +++ b/guition_diagnostics.h @@ -0,0 +1,57 @@ +#pragma once +#include "lvgl.h" +#include "esphome/core/log.h" + +// On-demand 240x240 diagnostic image of LVGL's rendered screen. Nothing is +// allocated or streamed during normal use. The RGB display signal itself +// still needs a physical visual check. +namespace guition { +inline bool check_geometry(lv_obj_t *obj) { + if (lv_obj_has_flag(obj, LV_OBJ_FLAG_HIDDEN)) return true; + bool ok = true; + lv_area_t area; + lv_obj_get_coords(obj, &area); + if (lv_obj_has_flag(obj, LV_OBJ_FLAG_CLICKABLE) && + (area.x1 < 0 || area.y1 < 0 || area.x2 >= 480 || area.y2 >= 480)) { + ESP_LOGE("ui_test", "GEOMETRY FAIL clickable=(%d,%d)-(%d,%d)", + area.x1, area.y1, area.x2, area.y2); + ok = false; + } + for (uint32_t i = 0; i < lv_obj_get_child_count(obj); ++i) + ok = check_geometry(lv_obj_get_child(obj, i)) && ok; + return ok; +} +inline lv_draw_buf_t *snapshot = nullptr; +inline int snapshot_row = 0; +inline void snapshot_begin() { + if (snapshot) lv_draw_buf_destroy(snapshot); + snapshot = lv_snapshot_take(lv_screen_active(), LV_COLOR_FORMAT_RGB565); + snapshot_row = 0; + if (!snapshot || snapshot->header.w != 480 || snapshot->header.h != 480) { + if (snapshot) lv_draw_buf_destroy(snapshot); + snapshot = nullptr; + ESP_LOGE("ui_image", "IMAGE FAIL allocation/dimensions"); + return; + } + ESP_LOGI("ui_image", "IMAGE BEGIN 240 240"); +} +inline void snapshot_next_row() { + if (!snapshot) return; + const auto *row = reinterpret_cast( + snapshot->data + snapshot_row * 2 * snapshot->header.stride); + static constexpr char hex[] = "0123456789abcdef"; + char line[961]; + for (int x = 0; x < 240; ++x) { + uint16_t pixel = row[x * 2]; + for (int digit = 0; digit < 4; ++digit) + line[x * 4 + digit] = hex[(pixel >> (12 - digit * 4)) & 15]; + } + line[960] = 0; + ESP_LOGI("ui_image", "ROW %03d %s", snapshot_row, line); + if (++snapshot_row == 240) { + lv_draw_buf_destroy(snapshot); + snapshot = nullptr; + ESP_LOGI("ui_image", "IMAGE COMPLETE"); + } +} +} // namespace guition diff --git a/tests/test_guition.py b/tests/test_guition.py new file mode 100644 index 0000000..ea68d49 --- /dev/null +++ b/tests/test_guition.py @@ -0,0 +1,71 @@ +"""Guition-specific geometry, hardware isolation and capacitive touch checks.""" +from pathlib import Path +import re +import sys +import tempfile +import unittest +ROOT=Path(__file__).resolve().parents[1] +sys.path.insert(0,str(ROOT/'tools')) +import verify_gt911 +import new_device +SOURCE=(ROOT/'guition-4848s040.yaml').read_text() +VALUES=dict(re.findall(r'^ (\w+): "([^"]*)"',SOURCE,re.M)) + +class GuitionTests(unittest.TestCase): + def test_hardware_is_s3_rgb_with_capacitive_touch(self): + self.assertIn('model: GUITION-4848S040',SOURCE) + self.assertIn('platform: gt911',SOURCE) + self.assertIn('mode: octal',SOURCE) + self.assertNotIn('platform: xpt2046',SOURCE) + self.assertNotIn('set_raw_correction',SOURCE) + self.assertEqual(VALUES['TOUCH_MIRROR_X'],'true') + self.assertEqual(VALUES['TOUCH_MIRROR_Y'],'true') + self.assertIn('pclk_frequency: 16MHz',SOURCE) + self.assertIn('execute_from_psram: true',SOURCE) + self.assertNotIn('id: output_red',SOURCE) + self.assertEqual(VALUES['DISPLAY_W'],'480') + self.assertEqual(VALUES['DISPLAY_H'],'480') + + def test_cards_fit_with_gutters_and_reserved_navigation(self): + v=lambda k:int(VALUES[k]) + rectangles=[] + for r in range(1,4): + for c in range(1,3): + x,y=v(f'GRID_COL{c}_X'),v('SCROLL_Y')+v(f'GRID_ROW{r}_Y') + w,h=v('TILE_W'),v('TILE_H') + self.assertGreaterEqual(x,16) + self.assertLessEqual(x+w,464) + self.assertLessEqual(y+h,420) + self.assertGreaterEqual(w*h,3*147*52) + for xx,yy,ww,hh in rectangles: + self.assertFalse(xxx and yyy) + rectangles.append((x,y,w,h)) + self.assertEqual(len(rectangles),6) + self.assertIn('height: 44',SOURCE.split('id: page_next',1)[1][:200]) + + def test_all_tiles_guard_events_and_clip_long_titles(self): + for i in range(1,11): + self.assertEqual(SOURCE.count(f'cyd::touch_guard.accept(millis(), {i})'),2) + self.assertRegex(SOURCE,rf'id: t{i}_title\n\s+width: 130\n\s+long_mode: DOT') + self.assertEqual(VALUES['AUTO_DIM_TIMEOUT'],'600') + + def test_gt911_verification_accepts_pixels_and_rejects_wrong_orientation(self): + data=dict(screen=[480,480],rotation=0,points=[dict(name=name,samples=[list(xy)]*3) for name,xy in verify_gt911.TARGETS]) + self.assertEqual(len(verify_gt911.verify(data)),5) + data['rotation']=180 + with self.assertRaises(ValueError):verify_gt911.verify(data) + + def test_gt911_rotations(self): + self.assertEqual(verify_gt911.screen_point(20,30,90),(30,459)) + self.assertEqual(verify_gt911.screen_point(20,30,270),(449,20)) + + def test_initializer_does_not_copy_resistive_calibration(self): + with tempfile.TemporaryDirectory() as tmp: + new_device.create(tmp,'wallbox-test','Wallbox Test','guition') + profile=(Path(tmp)/'guition-device.yaml').read_text() + self.assertIn('!include guition-4848s040.yaml',profile) + self.assertIn('DEVICE_NAME: "wallbox-test"',profile) + self.assertFalse((Path(tmp)/'calibration.yaml').exists()) + self.assertTrue((Path(tmp)/'secrets.yaml').exists()) + +if __name__=='__main__':unittest.main() diff --git a/tools/export_bundle.py b/tools/export_bundle.py index e6ca1e9..1d9fea8 100644 --- a/tools/export_bundle.py +++ b/tools/export_bundle.py @@ -7,7 +7,7 @@ import zipfile ROOT = Path(__file__).resolve().parents[1] FILES = ['.gitignore', 'README.md', 'AGENTS.md', 'requirements.txt', 'device.example.yaml', 'calibration.example.yaml', 'secrets.yaml.example', 'cyd_ui.h', 'LICENSE', - 'TILE_CONFIGURATION.md', 'CYD_STABILITY.md', 'TEST_RESULTS.md'] + 'guition_diagnostics.h', 'guition-4848s040.yaml', 'guition-device.example.yaml', 'TILE_CONFIGURATION.md', 'CYD_STABILITY.md', 'TEST_RESULTS.md'] FOLDERS = ['docs', 'tools', 'tests', 'components', 'diagnostics', 'fonts'] SUFFIXES = {'.py', '.cpp', '.h', '.md', '.ttf', '.txt'} diff --git a/tools/new_device.py b/tools/new_device.py index 028cb02..e4d8ee3 100644 --- a/tools/new_device.py +++ b/tools/new_device.py @@ -9,18 +9,20 @@ import secrets ROOT = Path(__file__).resolve().parents[1] -def create(directory, name, friendly_name): +def create(directory, name, friendly_name, board="cyd"): if not re.fullmatch(r'[a-z][a-z0-9-]{0,30}', name) or name.endswith('-'): raise ValueError('Gebruik een unieke naam van 1–31 tekens: kleine letters, cijfers en koppeltekens.') + if board not in ("cyd", "guition"): + raise ValueError("Onbekend bordprofiel") import json - device = (ROOT / 'device.example.yaml').read_text().replace('"cyd-new"', json.dumps(name), 1) - device = device.replace('"Mijn CYD"', json.dumps(friendly_name, ensure_ascii=False), 1) + device = (ROOT / ('guition-device.example.yaml' if board == 'guition' else 'device.example.yaml')).read_text().replace('"guition-new"' if board == 'guition' else '"cyd-new"', json.dumps(name), 1) + device = device.replace('"Mijn Guition"' if board == 'guition' else '"Mijn CYD"', json.dumps(friendly_name, ensure_ascii=False), 1) secret_text = (ROOT / 'secrets.yaml.example').read_text() replacements = [base64.b64encode(secrets.token_bytes(32)).decode(), secrets.token_urlsafe(24), secrets.token_urlsafe(18)] for value in replacements: secret_text = secret_text.replace('WORDT_UNIEK_GEGENEREERD', value, 1) - files = {'device.yaml': device, 'calibration.yaml': (ROOT / 'calibration.example.yaml').read_text(), - 'secrets.yaml': secret_text} + files = {'guition-device.yaml' if board == 'guition' else 'device.yaml': device, 'secrets.yaml': secret_text} + if board == 'cyd': files['calibration.yaml'] = (ROOT / 'calibration.example.yaml').read_text() directory = Path(directory) if not directory.is_dir(): raise ValueError('De doelmap moet al bestaan en de uitgepakte code bevatten.') @@ -44,15 +46,17 @@ def create(directory, name, friendly_name): def main(): parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--board', choices=['cyd', 'guition'], default='cyd') parser.add_argument('--name', required=True, help='Unieke ESPHome-naam, bijvoorbeeld display-keuken') - parser.add_argument('--friendly-name', default='Mijn CYD') + parser.add_argument('--friendly-name') parser.add_argument('--directory', type=Path, default=ROOT) args = parser.parse_args() + args.friendly_name = args.friendly_name or ('Mijn Guition' if args.board == 'guition' else 'Mijn CYD') try: - for path in create(args.directory, args.name, args.friendly_name): + for path in create(args.directory, args.name, args.friendly_name, args.board): print(f'Gemaakt: {path}') print('Vul wifi lokaal in secrets.yaml in. Sleutels zijn uniek gegenereerd en worden niet getoond.') - print('Volg nu README.md; flash device.yaml, niet rechtstreeks de basisconfiguratie.') + print('Volg docs/GUITION.md; flash guition-device.yaml.' if args.board == 'guition' else 'Volg README.md; flash device.yaml.') except (ValueError, OSError) as exc: parser.exit(1, f'Fout: {exc}\n') diff --git a/tools/verify_gt911.py b/tools/verify_gt911.py new file mode 100644 index 0000000..0c37bee --- /dev/null +++ b/tools/verify_gt911.py @@ -0,0 +1,83 @@ +"""Guided USB pixel verification for GT911, without resistive calibration or HA actions.""" +import argparse +import json +import math +from pathlib import Path +import re +import statistics +import time +from calibrate import write_file + +TARGETS = [('linksboven', (24, 24)), ('rechtsboven', (455, 24)), + ('rechtsonder', (455, 455)), ('linksonder', (24, 455)), ('midden', (240, 240))] +PATTERN = re.compile(r'GT911 press x=(\d+) y=(\d+) test=([01])') + + +def screen_point(x, y, rotation=0): + if rotation == 0: return x, y + if rotation == 90: return y, 479-x + if rotation == 180: return 479-x, 479-y + if rotation == 270: return 479-y, x + raise ValueError('Rotatie moet 0, 90, 180 of 270 zijn.') + + +def verify(data): + if data.get('screen') != [480,480] or len(data.get('points',[])) != 5: + raise ValueError('Verwacht vijf meetpunten op een 480x480 GT911-scherm.') + result=[] + for p,(name,target) in zip(data['points'],TARGETS): + if p.get('name') != name or len(p.get('samples',[])) != 3: + raise ValueError('Volg de vijf doelen; ieder doel vereist drie aparte tikken.') + samples=[] + for xy in p['samples']: + if len(xy)!=2 or any(type(v) is not int or not 0<=v<480 for v in xy): + raise ValueError('Touchcoördinaten vallen buiten 480x480.') + samples.append(screen_point(*xy, data.get('rotation',0))) + center=tuple(statistics.median(x[i] for x in samples) for i in (0,1)) + error=math.dist(center,target) + spread=max(math.dist(x,center) for x in samples) + result.append(dict(name=name,error_px=round(error,1),spread_px=round(spread,1))) + if error>16 or spread>18: + raise ValueError(f'{name}: fout {error:.1f}px / spreiding {spread:.1f}px. Controleer rotatie en GT911-transform; geen ADC-fit gebruiken.') + return result + + +def main(): + parser=argparse.ArgumentParser(description=__doc__) + parser.add_argument('--port', help='USB-poort; firmware moet het GT911-meetscherm tonen') + parser.add_argument('--output',type=Path,default=Path('measurements-guition.json')) + parser.add_argument('--input',type=Path,help='Controleer een bestaand meetbestand zonder USB') + parser.add_argument('--rotation',type=int,choices=(0,90,180,270),default=0) + args=parser.parse_args() + try: + if args.input: + data=json.loads(args.input.read_text()) + else: + import serial + if not args.port: raise ValueError('--port is nodig voor een fysieke meting.') + if args.output.exists(): raise ValueError('Meetbestand bestaat al; kies een andere naam.') + data=dict(screen=[480,480],rotation=args.rotation,points=[]) + with serial.Serial(args.port,115200,timeout=.2) as port: + for name,xy in TARGETS: + input(f'{name} {xy}: druk Enter en tik daarna drie keer alleen dit kruisje, met loslaten ertussen. ') + port.reset_input_buffer() + samples=[]; deadline=time.monotonic()+90; last=0 + while len(samples)<3 and time.monotonic()