diff --git a/AGENTS.md b/AGENTS.md index 182b431..5273f8f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,80 +1,80 @@ -## Easy Setup en releases +## Easy Setup and releases -De voorkeursroute voor nieuwe gebruikers is docs/EASY_SETUP.md: ESP Screen Manager -plus remote ESPHome-pakketten. Geen token of blueprint nodig. Tegels staan in de -permanente add-ondata; wifi/API/OTA blijven in de eigen ESPHome-YAML. Lees -docs/RELEASING.md voordat je updates publiceert. Main distribueert beide borden. -Elke push naar GitHub is een release: verhoog dan ook altijd de add-onversie in -screen_manager/config.yaml (met CHANGELOG-regel), anders ziet HA geen update. -Genereer packages met tools/generate_packages.py; bewerk ze niet handmatig. -Runtime-modus ondersteunt alle kaarten op alle twintig posities. De positiebeperkingen -hieronder gelden uitsluitend voor de oude handmatige profielen. -Behoud gegevensschema, protocolcompatibiliteit, unieke sleutels en CYD-preferences. -Test updates met bestaande gegevens. Publiceer geen onbekende opslagversie zonder -migratie. Productie-Ingress heeft geen long-lived token of publieke poort nodig. +The preferred route for new users is docs/EASY_SETUP.md: ESP Screen Manager +plus remote ESPHome packages. No token or blueprint needed. Tiles live in the +persistent add-on data; Wi-Fi/API/OTA stay in the device's own ESPHome YAML. Read +docs/RELEASING.md before publishing updates. Main distributes both boards. +Every push to GitHub is a release: always also bump the add-on version in +screen_manager/config.yaml (with a CHANGELOG line), otherwise HA won't see an update. +Generate packages with tools/generate_packages.py; don't edit them by hand. +Runtime mode supports all cards in all twenty positions. The position constraints +below apply only to the old manual profiles. +Preserve the data schema, protocol compatibility, unique keys, and CYD preferences. +Test updates against existing data. Don't publish an unknown storage version without +a migration. Production Ingress needs no long-lived token or public port. -## Guition-branch +## Guition board -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. +The Guition 4848S040 has a separate profile `guition-4848s040.yaml` with +480×480, ST7701S RGB, and GT911. Read docs/GUITION.md. Use its own local +`guition-device.yaml`; don't carry over the CYD layout or XPT2046 calibration. +Verify touch with tools/verify_gt911.py and keep the CYD regressions green. +Don't configure wallbox relays as part of display support. -# Werkinstructies voor LLM's en developers +# Working instructions for LLMs and developers -Dit project bedient een ESP32-2432S028 met ILI9341 + XPT2046 (320×240, -LVGL 90°). Lees README.md en docs/ voordat je installeert. De eigenaar kan -fysiek tikken; een agent kan dat niet vervangen door softwarecoördinaten. +This project drives an ESP32-2432S028 with ILI9341 + XPT2046 (320×240, +LVGL 90°). Read README.md and docs/ before installing. The owner can +physically tap; an agent cannot replace that with software coordinates. -## Een nieuw scherm installeren +## Installing a new screen -1. Identificeer USB-poort en bordvariant. Gebruik ESPHome 2026.6.2 met Python - 3.11–3.14. Controleer eerst of er lokale configuratie bestaat. -2. Maak in een verse kopie met `tools/new_device.py` een eigen `device.yaml`, - `calibration.yaml`, `secrets.yaml`. Overschrijf nooit een werkend profiel. - Laat de eigenaar wifi lokaal invullen. Toon geen sleutels in logs/chat. -3. Flash `device.yaml` met de CLI-substitutie `CALIBRATION_ON_BOOT=true`. - Het geïsoleerde scherm heeft vijf kruisjes en werkt zonder HA. -4. Volg docs/CALIBREREN.md: geleide USB-capture, fit, flash, nieuwe capture, - onafhankelijke verify. Vraag fysieke tikken per doel. Wacht op bevestiging - dat het meetscherm werkelijk zichtbaar is; een geslaagde build is geen flash. -5. Flash zonder die override. Koppel de eigen HA via de ESPHome-integratie. - Lees echte entity-ID's en ondersteunde attributen; verzin geen entiteiten. -6. Configureer alle gebruikte tegels volgens docs/TEGELS.md. De vacuumkaart - zit op positie 6. Posities 8/10 hebben geen volledige slider/climatebinding. - Test geen echte apparaat-acties zonder toestemming van de eigenaar. -7. Doorloop docs/ACCEPTATIE.md en rapporteer werkelijk uitgevoerde tests, - beperkingen en de geobserveerde stabiliteitsduur. +1. Identify the USB port and board variant. Use ESPHome 2026.6.2 with Python + 3.11–3.14. First check whether local configuration already exists. +2. In a fresh copy, use `tools/new_device.py` to create your own `device.yaml`, + `calibration.yaml`, `secrets.yaml`. Never overwrite a working profile. + Let the owner fill in Wi-Fi locally. Don't show keys in logs/chat. +3. Flash `device.yaml` with the CLI substitution `CALIBRATION_ON_BOOT=true`. + The isolated screen shows five crosshairs and works without HA. +4. Follow docs/CALIBRATING.md: guided USB capture, fit, flash, new capture, + independent verification. Ask for physical taps per target. Wait for confirmation + that the measurement screen is actually visible; a successful build is not a flash. +5. Flash without that override. Pair with the owner's own HA via the ESPHome integration. + Read real entity IDs and supported attributes; don't make up entities. +6. Configure every tile you use according to docs/TILES.md. The vacuum card + sits at position 6. Positions 8/10 don't have a full slider/climate binding. + Don't test real device actions without the owner's permission. +7. Go through docs/ACCEPTANCE.md and report the tests actually carried out, + limitations, and the observed stability duration. -## Code en regressies +## Code and regressions -- Houd basishardware en UI in `home-like-2432s028.yaml`; eigen gegevens horen - in de genegeerde lokale profielen. `home-like.yaml` is een ander oud profiel. -- Behoud vaste pagina's, verborgen navigatie bij maximaal zes tegels en - minimaal 600 seconden standaard-standby. Geen vrij scrollen herintroduceren - zonder touch-/navigatieregressies fysiek te testen. -- Behoud touchfilter en eventguard vóór acties; kalibratie verwerkt gefilterde - fysieke ADC-waarden. Zet de affine correctie niet dubbel in driver en UI. -- Kalibratiewizard veronderstelt swap_xy=false, mirror_x=true, mirror_y=false - en LVGL 90°. Een gewijzigde oriëntatie vereist ook nieuwe projectie/tests. -- Run Python-tests, beide C++-tests en ESPHome-validatie/build bij codewijzigingen. - Firmwaretests en hardwareacceptatie zijn verschillende controles. -- `diagnostics/run_ui_test.py` rendert zonder HA-acties; raak tijdens die test - het scherm niet aan. Gebruik `--name` voor de verwachte apparaatidentiteit. - `diagnostics/send_layout.py` zet een demo-indeling met alle kaarttypen op een - scherm via de API-inbox (geen HA-acties); de manager herstelt de echte - indeling binnen ~25 s. Guition: `capture_ui.py` bewaart de LVGL-render als PNG. - Zonder scherm: `tools/render_topbar.py` rendert de echte bovenbalkcode van beide - borden via ESPHome host + SDL2 naar `.esphome/render-topbar/out/sheet.png`. -- Deel via `tools/export_bundle.py` of Git. Stage geen secrets, metingen, - binaries, logs, buildcache of lokale apparaatprofielen. -- Geen automatische firmware-upload naar een willekeurige aangesloten poort. - Bij meerdere borden eerst de bedoelde poort vaststellen. -- Profielen met dezelfde `DEVICE_NAME` (Easy Setup en handmatig) delen - `.esphome/build/`. Compileer of upload ze nooit gelijktijdig; controleer - in het uploadlog het pad van `firmware.bin` en daarna de compileertijd via - `device_info`. Een verkeerd profiel haalt het scherm uit ESP Screen Manager. +- Keep base hardware and UI in `home-like-2432s028.yaml`; personal data belongs + in the gitignored local profiles. +- Preserve fixed pages, hidden navigation at six tiles or fewer, and a + minimum default standby of 600 seconds. Don't reintroduce free scrolling + without physically testing for touch/navigation regressions. +- Preserve the touch filter and event guard before actions; calibration processes + filtered physical ADC values. Don't apply the affine correction twice, in both the driver and the UI. +- The calibration wizard assumes swap_xy=false, mirror_x=true, mirror_y=false, + and LVGL 90°. A changed orientation also requires a new projection/tests. +- Run the Python tests, both C++ test suites, and ESPHome validation/build on code changes. + Firmware tests and hardware acceptance are different checks. +- `diagnostics/run_ui_test.py` renders without HA actions; don't touch the screen + during that test. Use `--name` for the expected device identity. + `diagnostics/send_layout.py` pushes a demo layout with every card type to a + screen via the API inbox (no HA actions); the manager restores the real + layout within ~25 s. Guition: `capture_ui.py` saves the LVGL render as a PNG. + Without a screen: `tools/render_topbar.py` renders the real top-bar code for both + boards via the ESPHome host + SDL2 to `.esphome/render-topbar/out/sheet.png`. +- Share via `tools/export_bundle.py` or Git. Don't stage secrets, measurements, + binaries, logs, build caches, or local device profiles. +- No automatic firmware upload to an arbitrary connected port. + With multiple boards, first determine the intended port. +- Profiles with the same `DEVICE_NAME` (Easy Setup and manual) share + `.esphome/build/`. Never compile or upload them concurrently; check + the `firmware.bin` path in the upload log, and then the compile time via + `device_info`. A wrong profile knocks the screen out of ESP Screen Manager. -Historische diagnose is achtergrond, geen bewijs dat een nieuw paneel goed -werkt. Maak tijdens onboarding geen claims over niet-uitgevoerde fysieke tests. +Historical diagnostics are background context, not proof that a new panel +works correctly. During onboarding, don't make claims about physical tests that weren't actually performed. diff --git a/CYD_STABILITY.md b/CYD_STABILITY.md index 3e8fd31..f3ace08 100644 --- a/CYD_STABILITY.md +++ b/CYD_STABILITY.md @@ -1,60 +1,60 @@ -> Historisch onderzoek aan één paneel. Voor een nieuw scherm: [README.md](README.md) -> en [de vijfpuntskalibratie](docs/CALIBREREN.md); neem geen meetwaarden over. +> Historical investigation on a single panel. For a new screen, see [README.md](README.md) +> and [the five-point calibration](docs/CALIBRATING.md); don't reuse these measurements. -# ESP32-2432S028: stabiele bediening +# ESP32-2432S028: stable operation -Gebruik **home-like-2432s028.yaml** voor de aangesloten 2,8-inch CYD met -ILI9341 en XPT2046. `home-like.yaml` hoort bij het andere ST7789V/GT911-bord. -De wifi-, API- en OTA-geheimen blijven in `secrets.yaml`. +Use **home-like-2432s028.yaml** for the connected 2.8-inch CYD with +ILI9341 and XPT2046. The Wi-Fi, API, and OTA secrets stay in `secrets.yaml`. -## Bediening +## Operation -- Stel `TILE_COUNT` in op het aantal gebruikte tegels (1–10). Bij maximaal - zes tegels verdwijnen beide paginaknoppen en de pagina-aanduiding; hogere - tegelnummers zijn verborgen. -- Pagina 1 bevat tegels 1–6; pagina 2 bevat 7–10. Gebruik de knoppen - **Vorige** en **Volgende** onderaan. Slepen is uitgeschakeld. -- Kort tikken voert de bestaande tegelactie uit. Lang indrukken opent de - bestaande licht-, climate- of vacuumkaart, of de ingestelde lange actie. -- Standby begint na tien minuten zonder aanraking (`AUTO_DIM_TIMEOUT: "600"`). -- De eerste tik op het gedimde scherm maakt het scherm wakker. -- Contacten korter dan `TOUCH_MIN_PRESS_MS` (60 ms op de CYD, 20 ms op de - Guition) worden genegeerd. Eén aanraking kan maximaal één tegelactie geven; - een verplaatsing groter dan `TOUCH_MOVE_LIMIT_PX` (56 px, circa 1 cm, op de CYD; - 0 op de Guition: geen grens, LVGL beslist en loslaten binnen de tegel telt) ten - opzichte van het gesettelde eerste contactpunt annuleert de tik; alleen het eerste - contact telt; dezelfde tegel wordt 600 ms tegen contactdender beschermd. Een geweigerde tik staat met reden in het log (tag `touch`). -- De thermostaat verstuurt de afgeronde, begrensde temperatuur die hij toont. - Een lokale temperatuur- of moduskeuze krijgt drie seconden om met HA te - synchroniseren. Een ingedrukte bediening voorkomt automatisch dimmen. -- Pippa gebruikt de echte `vacuum.s8`-status, zodat docken, schoonmaken, - pauzeren en terugkeren onderscheiden worden. Ontbrekende HA-statussen - worden als `Niet beschikbaar` getoond. +- Set `TILE_COUNT` to the number of tiles in use (1–10). With six tiles or + fewer, both page buttons and the page indicator disappear; higher + tile numbers stay hidden. +- Page 1 holds tiles 1–6; page 2 holds 7–10. Use the **Previous** + and **Next** buttons at the bottom. Swiping is disabled. +- A short tap runs the tile's existing action. A long press opens the + existing light, climate, or vacuum card, or the configured long-press action. +- Standby starts after ten minutes without touch (`AUTO_DIM_TIMEOUT: "600"`). +- The first tap on the dimmed screen wakes the screen. +- Contacts shorter than `TOUCH_MIN_PRESS_MS` (60 ms on the CYD, 20 ms on the + Guition) are ignored. One touch can produce at most one tile action; + a movement greater than `TOUCH_MOVE_LIMIT_PX` (56 px, about 1 cm, on the CYD; + 0 on the Guition: no limit, LVGL decides and releasing inside the tile counts) + relative to the settled first contact point cancels the tap; only the first + contact counts; the same tile is protected against contact bounce for 600 ms. + A rejected tap is logged with its reason (tag `touch`). +- The thermostat sends the rounded, clamped temperature it displays. + A local temperature or mode choice gets three seconds to sync with HA. + An active control press prevents automatic dimming. +- Pippa uses the real `vacuum.s8` status, so docking, cleaning, + pausing, and returning are distinguished. Missing HA states + are shown as `Unavailable`. -## Technische keuzes +## Technical choices -ESPHome 2026.6.2 met ESP-IDF; er zijn geen Arduino-componenten nodig. -De displaybus draait op 40 MHz, zoals ESPHome's standaard CYD-profiel. -XPT2046 wordt elke 20 ms uitgelezen zonder GPIO36-interrupt. De drukdrempel -is 800, na een verdachte hoekmeting bij druk 422. Een lokale XPT2046-driver -vereist drie consistente ADC-samples voor een nieuwe aanraking, negeert losse -uitschieters en wacht twee drukloze samples af voor loslaten. Dit voegt circa -40–60 ms toe bij aanraken en voorkomt dat een onstabiele eerste meting meteen -als een andere knop wordt geïnterpreteerd. De driver staat onder `components/`. De geïnstalleerde -interruptdriver probeert op die input-only pin een niet-beschikbare interne -pull-up te activeren; de oude firmware logde hiervoor een GPIO-fout. +ESPHome 2026.6.2 with ESP-IDF; no Arduino components are needed. +The display bus runs at 40 MHz, like ESPHome's standard CYD profile. +XPT2046 is polled every 20 ms without the GPIO36 interrupt. The pressure +threshold is 800, after a suspect corner reading at pressure 422. A local XPT2046 +driver requires three consistent ADC samples for a new touch, ignores isolated +outliers, and waits for two pressure-free samples before release. This adds about +40–60 ms on touch and prevents an unstable first reading from being immediately +interpreted as a different button. The driver lives under `components/`. The stock +interrupt driver tries to enable an unavailable internal pull-up on that +input-only pin; the old firmware logged a GPIO error for this. -LVGL krijgt 12% tekenbuffer. Geen scrollanimaties en geen extra flitspuls na -elke tik. Tegelachtergronden zijn ondoorzichtig. Updates binnen 100 ms worden -samengevoegd; ongewijzigde tegellabels en kleuren worden niet opnieuw gezet. -De UI-initialisatie draait pas nadat LVGL is gestart. De klok ververst direct -bij tijdsynchronisatie. Roboto staat lokaal in `fonts/`, inclusief OFL-licentie. +LVGL gets a 12% draw buffer. No scroll animations and no extra flash pulse after +each tap. Tile backgrounds are opaque. Updates within 100 ms are +coalesced; unchanged tile labels and colors are not redrawn. +UI initialization only runs after LVGL has started. The clock refreshes immediately +on time sync. Roboto is bundled locally in `fonts/`, including the OFL license. -De actieve indeling is 320×240. De oude, uitgecommentarieerde portretpresets -zijn nog geen aangepaste paginalay-outs: reserveer bij een andere oriëntatie -ook onderaan 34 pixels voor de navigatie en controleer de tegelposities. +The active layout is 320×240. The old, commented-out portrait presets +are not adapted page layouts yet: with a different orientation, also reserve +34 pixels at the bottom for navigation and check the tile positions. -## Bouwen en flashen +## Building and flashing ```sh esphome compile home-like-2432s028.yaml @@ -62,12 +62,12 @@ esphome upload home-like-2432s028.yaml --device /dev/cu.usbserial-130 esphome logs home-like-2432s028.yaml --device /dev/cu.usbserial-130 ``` -Sluit een lopende seriële loglezer voordat je flasht. De vorige configuratie -staat lokaal in `diagnostics/before-2432s028.yaml.bak`; de bijbehorende vorige -factorybinary in `diagnostics/before-2432s028.factory.bin`. Deze lokale backups -worden niet in Git opgenomen. Teruggaan kan door de backup als configuratie -te herstellen en opnieuw te bouwen/flashen; herstel de oude Arduino-toolchain -als je de oude configuratie bouwt. +Close any running serial log reader before flashing. The previous configuration +is stored locally in `diagnostics/before-2432s028.yaml.bak`; the corresponding previous +factory binary is in `diagnostics/before-2432s028.factory.bin`. These local backups +are not included in Git. You can roll back by restoring the backup as the configuration +and rebuilding/flashing; restore the old Arduino toolchain +if you build the old configuration. ## Tests @@ -79,46 +79,46 @@ clang++ -std=c++17 -Wall -Wextra -Werror tests/test_touch_filter.cpp -o /tmp/tes /tmp/test_touch_filter ``` -De C++-test controleert contactdender, korte ruispulsen, opeenvolgende tegels, -herhaling binnen één aanraking, `millis()`-overflow en temperatuurafronding. -De indelingstest controleert overlap, schermgrenzen, ruimte voor navigatie -en het filter vóór beide handlers van alle tien tegels. +The C++ test checks contact bounce, short noise pulses, consecutive tiles, +repetition within a single touch, `millis()` overflow, and temperature rounding. +The layout test checks overlap, screen bounds, space for navigation, +and the filter ahead of both handlers for all ten tiles. -Met de Python-omgeving van ESPHome (met aioesphomeapi, PyYAML en pyserial): +With ESPHome's Python environment (with aioesphomeapi, PyYAML, and pyserial): ```sh python diagnostics/capture_serial.py --reset --seconds 120 > diagnostics/runtime.log python diagnostics/run_ui_test.py --host cyd-2432s028.local ``` -De API-test leest de encryptiesleutel intern uit `secrets.yaml` en drukt deze -niet af. Hij wisselt tien keer van pagina en tekent de vijf overlays in elke -ronde. Hij roept geen Home Assistant-acties aan. Raak het scherm tijdens -deze circa 11 seconden niet aan. Dit is een teken-/geheugentest; hij vervangt -geen fysieke toets van de touchkalibratie of een test van HA-service-effecten. +The API test reads the encryption key internally from `secrets.yaml` and doesn't +print it. It switches pages ten times and draws the five overlays on each +round. It doesn't call any Home Assistant actions. Don't touch the screen during +these roughly 11 seconds. This is a rendering/memory test; it doesn't replace +a physical check of the touch calibration or a test of HA service effects. -Diagnostiek logt iedere 30 seconden vrije heap en grootste vrije geheugenblok. -`DEBUG` is tijdelijk nuttig voor touch- en verbindingsdiagnose. Voor dagelijks -gebruik kan `logger.level` op `INFO` zodra de fysieke bediening goed is. +Diagnostics logs free heap and the largest free memory block every 30 seconds. +`DEBUG` is useful temporarily for touch and connection diagnostics. For everyday +use, `logger.level` can go back to `INFO` once physical operation is confirmed good. -## Touchdiagnose +## Touch diagnostics -`python diagnostics/control_ui.py touch_diagnostics` opent een apart scherm -met vier kruizen. Tijdens deze meting kunnen aanrakingen geen tegelactie -uitvoeren. Tik nauwkeurig achtereenvolgens linksboven, rechtsboven, -rechtsonder en linksonder. De ruwe en genormaliseerde coördinaten staan in -de seriële logs met tag `touch`. Sluit af met -`python diagnostics/control_ui.py end_touch_diagnostics`. De kruizen staan -op (20,20), (299,20), (299,219) en (20,219) voor landschap 320×240. +`python diagnostics/control_ui.py touch_diagnostics` opens a separate screen +with four crosshairs. During this measurement, touches can't trigger a tile +action. Tap accurately, in order: top-left, top-right, +bottom-right, and bottom-left. The raw and normalized coordinates appear in +the serial logs with tag `touch`. Exit with +`python diagnostics/control_ui.py end_touch_diagnostics`. The crosshairs sit +at (20,20), (299,20), (299,219), and (20,219) for 320×240 landscape. -De huidige kalibratie (X 225–3800, Y 274–3609) is afgeleid uit de drie -herhaalde metingen rechtsboven en linksonder op dit fysieke bord. De eerdere -metingen linksboven waren onstabiel en zijn bewust niet gebruikt om het -hele scherm naar een verkeerde positie te verschuiven. +The current calibration (X 225–3800, Y 274–3609) is derived from the three +repeated measurements at top-right and bottom-left on this physical board. The earlier +top-left measurements were unstable and were deliberately not used, to avoid +shifting the entire screen to the wrong position. -Daarbovenop corrigeert `set_raw_correction(...)` in `on_boot` de gemeten -scheefstand. De fit gebruikt de gefilterde hoekmedianen (830,556), (544,3461), -(3410,3391) en (3492,372). De vier medianen vallen na correctie binnen vijf -pixels van hun meetdoel in de regressietest. Dit zijn bord-specifieke -coëfficiënten; neem ze niet blind over op een ander paneel. De `raw=`-logs -blijven de fysieke, gefilterde ADC-metingen tonen. +On top of that, `set_raw_correction(...)` in `on_boot` corrects the measured +skew. The fit uses the filtered corner medians (830,556), (544,3461), +(3410,3391), and (3492,372). After correction, the four medians land within five +pixels of their measurement target in the regression test. These are board-specific +coefficients; don't carry them over blindly to another panel. The `raw=` logs +still show the physical, filtered ADC readings. diff --git a/README.md b/README.md index d2a155f..4b155e5 100644 --- a/README.md +++ b/README.md @@ -1,329 +1,328 @@ # Home Assistant ESP Screens -Een Home Assistant-bedieningsscherm dat je zelf indeelt. **ESP Screen Manager** -beheert je tegels én bouwt en installeert de ESPHome-firmware vanuit Home Assistant. -Geen blueprint, MQTT of long-lived token nodig voor normaal gebruik. +A Home Assistant control screen that you lay out yourself. **ESP Screen Manager** +manages your tiles and builds and installs the ESPHome firmware from Home Assistant. +No blueprint, MQTT, or long-lived token needed for normal use. -**Nieuw scherm? Volg de [complete installatiehandleiding](docs/EASY_SETUP.md).** -Voor elke nieuwe gebruiker en elk nieuw apparaat maak je een eigen profiel. +**New screen? Follow the [complete installation guide](docs/EASY_SETUP.md).** +For every new user and every new device, you create your own profile. -## Ondersteunde schermen +## Supported screens -| Scherm | Resolutie | Display / touch | +| Screen | Resolution | Display / touch | | --- | --- | --- | -| CYD ESP32-2432S028 | 320 × 240 | ILI9341 / resistieve XPT2046 | -| Guition ESP32-S3-4848S040, 4 inch | 480 × 480 | ST7701S RGB / capacitieve GT911 | +| CYD ESP32-2432S028 | 320 × 240 | ILI9341 / resistive XPT2046 | +| Guition ESP32-S3-4848S040, 4 inch | 480 × 480 | ST7701S RGB / capacitive GT911 | -Gebruik deze exacte bordvarianten: gelijkende productnamen kunnen andere -controllers of aansluitingen hebben. Wallbox-relais worden niet aangestuurd. -De firmware en ingebouwde CLI zijn getest met **ESPHome 2026.6.2**. +Use these exact board variants: similar-looking product names can have different +controllers or connectors. Wallbox relays are not controlled. +The firmware and built-in CLI are tested with **ESPHome 2026.6.2**. -## Wat je kunt instellen +## What you can configure -- **Tot twintig tegels**, verdeeld over maximaal vier vaste pagina's met zes - tegels. Zoek op entiteit, apparaat of ruimte en sleep om te ordenen. -- **Instellingen per tegel:** eigen naam, klikgedrag, een kleine slider waar - ondersteund, of een grote waarde voor bijvoorbeeld temperatuur en verbruik. - De grote waarde toont vanaf firmware 0.2.13 een klein domeinicoon naast de - titel; een te lang getal wordt met puntjes afgekort, de eenheid blijft staan. -- **Pastel achtergronden per tegel:** kies rood voor een alles-uit-script, - groen voor alles-aan, of een andere kleur. Titel en status blijven donker en - leesbaar. De kleur verschijnt ook in het schermvoorbeeld; **Standaard** herstelt - de normale kleuren. Vereist firmware 0.2.10 of nieuwer. **Geen** laat de kaart - helemaal weg: de inhoud staat dan even groot direct op de schermachtergrond +- **Up to twenty tiles**, spread across up to four fixed pages of six + tiles each. Search by entity, device, or room, and drag to reorder. +- **Per-tile settings:** a custom name, click behavior, a small slider where + supported, or a large value for things like temperature and power usage. + From firmware 0.2.13, the large value shows a small domain icon next to the + title; a number that's too long is truncated with an ellipsis, the unit stays visible. +- **Pastel backgrounds per tile:** choose red for an all-off script, + green for all-on, or any other color. The title and status stay dark and + readable. The color also appears in the screen preview; **Default** restores + the normal colors. Requires firmware 0.2.10 or newer. **None** drops the card + entirely: the content then sits at the same size directly on the screen background (firmware 0.2.16). -- **Klok:** digitaal of analoog. De analoge klok heeft streepjes met 12, 3, 6 en 9 - en toont op een enkele tegel een kalenderblok (weekdag, dag, maand) naast de - wijzerplaat; dubbelbreed staat de digitale tijd met datum ernaast. -- **Lampbediening:** helderheid, regenboogkleur en wittemperatuur volgens de - mogelijkheden van de lamp. Open de detailbediening met een lange aanraking. -- **Meer kaarten:** climate, vacuum, fan, cover, media player, sensoren, - select/input_select, number/input_number, schakelaars, scènes, scripts en - buttons. Een sensor kan een historiekaart voor 1, 6 of 24 uur openen. -- **Speciale kaarten (firmware 0.2.14+):** een **klok** (digitaal of analoog) - als ingebouwde tegel, een **weersvoorspelling** met vijf dagen op een - dubbelbrede kaart, een **grafiek** van de sensorgeschiedenis in de tegel, - een **zonnebaan** (`sun.sun`: horizon met de zon tussen opkomst en - ondergang), een **kookwekker** (`timer.*`, tikken start of pauzeert) en - **aanwezigheid** (`person.*`). Kies ze in de kiezer als elke andere tegel of - sleep ze direct in de schermmockup; **Dubbelbreed** is een optie voor iedere tegel. -- **Directe bediening op dubbelbrede tegels** (firmware 0.2.19+), zoals de rijen in - Home Assistant: temperatuur − / + of modusknoppen (klimaat), een toggle (schakelaar, - lamp, ventilator), start/stop/dock (stofzuiger), open/stop/dicht of een - positieschuif (zonwering), volume met dempen of vorige/play/volgende (media), - − / + of een schuif (getallen), vorige/volgende (keuzelijst), start/pauze en - annuleren (kookwekker) en één knop voor scènes, scripts en knoppen. Per tegel - instelbaar; **Geen** houdt de gewone kaart. -- **Weerkaart** met actueel weer, de komende uren en dagen inclusief regenkans of - millimeters; **klimaatkaart** met aan/uit-knop, modus-, ventilator- en zwenkstanden. - Scènes, scripts en knoppen tonen wanneer ze voor het laatst liepen. Een tegel die op - Home Assistant wacht toont een klein spinnertje op een lichte laag. -- **Bovenbalk per scherm** (firmware 0.2.32+): links de naam, rechts tot zes - onderdelen naar keuze: de tijd, een analoge klok, de datum, of een entiteit uit - Home Assistant met icoon, zoals temperatuur, luchtvochtigheid, verbruik, een deur - (open/dicht), het alarm, wie er thuis is of wanneer iemand of iets het laatst - veranderde ("5 min geleden", "Gisteren"). Zie [Bovenbalk](#bovenbalk). -- **Scherminstellingen:** standby-tijd, normale en gedimde helderheid, - nachturen, 24- of 12-uursklok, terug naar de hoofdpagina en optioneel swipen - tussen pagina's. -- **Guition-rotatie:** 0°, 90°, 180° of 270°, direct vanuit de beheerpagina. - De native LVGL-rotatie draait beeld en touch samen. De CYD behoudt zijn vaste - oriëntatie en eigen kalibratie. -- **Inspector:** controleer entiteiten, status en configuratie in ESP Screens. - Actiefeedback laat zien dat een opdracht onderweg is. +- **Clock:** digital or analog. The analog clock has tick marks at 12, 3, 6, and 9 + and, on a single tile, shows a calendar block (weekday, day, month) next to the + dial; double-width shows the digital time with the date beside it. +- **Light control:** brightness, rainbow color, and white temperature according to + the light's capabilities. Open the detailed control with a long touch. +- **More cards:** climate, vacuum, fan, cover, media player, sensors, + select/input_select, number/input_number, switches, scenes, scripts, and + buttons. A sensor can open a history card for 1, 6, or 24 hours. +- **Special cards (firmware 0.2.14+):** a **clock** (digital or analog) + as a built-in tile, a **weather forecast** with five days on a + double-width card, a **graph** of the sensor history in the tile, + a **sun path** (`sun.sun`: horizon with the sun between sunrise and + sunset), a **timer** (`timer.*`, tapping starts or pauses it), and + **presence** (`person.*`). Pick them in the picker like any other tile, or + drag them straight into the screen mockup; **Double-width** is an option for every tile. +- **Direct control on double-width tiles** (firmware 0.2.19+), like the rows in + Home Assistant: temperature − / + or mode buttons (climate), a toggle (switch, + light, fan), start/stop/dock (vacuum), open/stop/close or a + position slider (cover), volume with mute or previous/play/next (media), + − / + or a slider (numbers), previous/next (select), start/pause and + cancel (timer), and a single button for scenes, scripts, and buttons. Configurable + per tile; **None** keeps the regular card. +- **Weather card** with current weather, the coming hours and days including chance of + rain or millimeters; **climate card** with an on/off button, mode, fan, and swing settings. + Scenes, scripts, and buttons show when they last ran. A tile that's waiting on + Home Assistant shows a small spinner on a light overlay. +- **Top bar per screen** (firmware 0.2.32+): the name on the left, up to six + items of your choice on the right: the time, an analog clock, the date, or an + entity from Home Assistant with an icon, such as temperature, humidity, power usage, a door + (open/closed), the alarm, who's home, or when something or someone last + changed ("5 min ago", "Yesterday"). See [Top bar](#top-bar). +- **Screen settings:** standby time, normal and dimmed brightness, + night hours, 24- or 12-hour clock, return to the home page, and optional swiping + between pages. +- **Guition rotation:** 0°, 90°, 180°, or 270°, directly from the management page. + Native LVGL rotation turns the display and touch together. The CYD keeps its fixed + orientation and its own calibration. +- **Inspector:** check entities, status, and configuration in ESP Screens. + Action feedback shows that a command is on its way. -Functies hangen af van de mogelijkheden die Home Assistant voor een entiteit -meldt. De app moet blijven draaien om de schermen van actuele gegevens te voorzien. +Features depend on the capabilities Home Assistant reports for an entity. +The app must keep running to keep the screens supplied with current data. -Vanaf firmware **0.2.12** toont lang indrukken op een switch een grote -schakelaar. Een korte tik schakelt direct; de uitstand krijgt een grijs icoon. -De feedback stopt zodra Home Assistant de gewijzigde stand meldt, met een -minimum van 150 ms voor switches. Tegels met een mini-slider behouden hun -icoon; op CYD staan het icoon en tekstblok verticaal gecentreerd. +From firmware **0.2.12**, a long press on a switch shows a large +toggle. A short tap switches immediately; the off state gets a gray icon. +The feedback stops as soon as Home Assistant reports the changed state, with a +minimum of 150 ms for switches. Tiles with a mini-slider keep their +icon; on the CYD, the icon and text block are vertically centered. -## Alert vanuit een automatisering +## Alert from an automation -Elk scherm heeft de actie **`esphome._show_alert`** (firmware 0.2.31+). Die legt -een kaart over het hele scherm, wekt het scherm en houdt de backlight op de normale -helderheid tot iemand op **Oké** tikt. In een automatisering: +Every screen has the action **`esphome._show_alert`** (firmware 0.2.31+). It places +a card over the entire screen, wakes the screen, and keeps the backlight at normal +brightness until someone taps **OK**. In an automation: ```yaml -action: esphome.keuken_scherm_show_alert +action: esphome.kitchen_screen_show_alert data: - title: "Iemand belt aan" - subtitle: "Deur 3, achterkant" + title: "Someone is at the door" + subtitle: "Door 3, back" icon: doorbell color: orange - button_text: "Ik kom" + button_text: "Coming" timeout: 0 flash: true ``` -- **`title`** en **`subtitle`**: een titel op één regel (te lang krijgt puntjes) en een - toelichting die over meerdere regels loopt. Leeg mag; een lege titel wordt "Melding". -- **`icon`**: een naam uit de tegelkiezer, zoals `doorbell`, `bell`, `alert-outline`, - `lock`, `door-open`, `motion-sensor`, `smoke-detector`, `water-alert`, `mailbox`, `car` - of `account`. `mdi:doorbell` en de hex-codepoint (`F12E6`) mogen ook, zolang het glyph - in de firmware zit. Onbekend geeft de waarschuwingsdriehoek. -- **`color`**: `red`, `orange`, `yellow`, `green`, `mint`, `blue`, `purple`, `pink` of - `gray`, dezelfde pasteltinten als de tegels. Leeg geeft de witte kaart. -- **`button_text`**: de tekst op de knop; leeg is "Oké". -- **`timeout`**: seconden waarna de kaart vanzelf verdwijnt; `0` is tot de knop, hoe lang - dat ook duurt. De knop sluit de kaart altijd direct, ook met een timeout. Standby en - nachtstand wachten zolang de kaart staat. -- **`flash`**: `true` laat de backlight vier keer knipperen bij binnenkomst. +- **`title`** and **`subtitle`**: a single-line title (truncated with an ellipsis if too long) and an + explanation that wraps across multiple lines. Empty is allowed; an empty title becomes "Notification". +- **`icon`**: a name from the tile picker, such as `doorbell`, `bell`, `alert-outline`, + `lock`, `door-open`, `motion-sensor`, `smoke-detector`, `water-alert`, `mailbox`, `car`, + or `account`. `mdi:doorbell` and the hex codepoint (`F12E6`) also work, as long as the glyph + is included in the firmware. Unknown falls back to the warning triangle. +- **`color`**: `red`, `orange`, `yellow`, `green`, `mint`, `blue`, `purple`, `pink`, or + `gray` — the same pastel shades as the tiles. Empty gives the white card. +- **`button_text`**: the text on the button; empty is "OK". +- **`timeout`**: seconds after which the card disappears on its own; `0` means it waits for the button, + however long that takes. The button always closes the card immediately, even with a timeout. Standby and + night mode wait as long as the card is showing. +- **`flash`**: `true` makes the backlight blink four times when the alert arrives. -In ESP Screens opent de knop **Alerts** een cheatsheet met per scherm de exacte actienaam, -een voorbeeld om te plakken, alle velden, iconen en kleuren. Home Assistant vraagt alle zeven velden; laat een veld leeg (`""`, `0`, `false`) als je het -niet gebruikt. Een nieuwe alert vervangt de huidige. Elk einde meldt zich als event -**`esphome.screen_alert`** met `action` (`ok`, `timeout`, `replaced` of `remote`), `title`, -`screen` en het `device_id` dat Home Assistant toevoegt, zodat een automatisering op Oké -kan wachten. **`esphome._dismiss_alert`** haalt de kaart op afstand weg. +In ESP Screens, the **Alerts** button opens a cheatsheet with the exact action name for each screen, +a ready-to-paste example, and all fields, icons, and colors. Home Assistant asks for all seven fields; leave a field empty (`""`, `0`, `false`) if you +don't use it. A new alert replaces the current one. Every end is reported as the event +**`esphome.screen_alert`** with `action` (`ok`, `timeout`, `replaced`, or `remote`), `title`, +`screen`, and the `device_id` that Home Assistant adds, so an automation can wait for OK. +**`esphome._dismiss_alert`** clears the card remotely. -## Installeren vanuit Home Assistant +## Installing from Home Assistant -### Heb ik ESPHome nodig? +### Do I need ESPHome? -**Je hoeft de aparte ESPHome Device Builder-app niet te installeren.** -ESP Screen Manager bevat de ESPHome-CLI al en kan zelf firmware bouwen, -via USB installeren en later draadloos via OTA bijwerken. +**You don't need to install the separate ESPHome Device Builder app.** +ESP Screen Manager already includes the ESPHome CLI and can build firmware itself, +install it via USB, and later update it wirelessly over OTA. -**Je moet het geflashte scherm wel koppelen via de ESPHome-integratie in HA.** -Die koppeling staat onder **Instellingen → Apparaten & diensten**, niet in de -appwinkel. Voeg daar het ontdekte apparaat toe. Verschijnt het niet automatisch, -kies dan **Integratie toevoegen → ESPHome** en vul het IP-adres van het scherm in. -Gebruik bij een sleutelvraag de `api.encryption.key` uit je eigen apparaat-YAML -en geef het apparaat toestemming om Home Assistant-acties uit te voeren. +**You do need to pair the flashed screen via the ESPHome integration in HA.** +That pairing lives under **Settings → Devices & services**, not in the +App store. Add the discovered device there. If it doesn't appear automatically, +choose **Add integration → ESPHome** and enter the screen's IP address. +If asked for a key, use the `api.encryption.key` from your own device YAML, +and grant the device permission to perform Home Assistant actions. -| Onderdeel | Nodig? | Waarvoor? | +| Component | Needed? | What for? | | --- | --- | --- | -| ESP Screen Manager-app | Ja, voor deze installatieroute | Firmware installeren, tegels beheren en actuele gegevens naar het scherm sturen | -| ESPHome Device Builder-app | Nee, optioneel | Alternatieve editor en firmware-installatie; dezelfde CLI zit al in ESP Screens | -| ESPHome-integratie in HA | Ja, koppel ieder scherm | De verbinding tussen Home Assistant en het fysieke scherm | +| ESP Screen Manager app | Yes, for this installation route | Installing firmware, managing tiles, and sending current data to the screen | +| ESPHome Device Builder app | No, optional | Alternative editor and firmware installer; the same CLI is already in ESP Screens | +| ESPHome integration in HA | Yes, pair every screen | The connection between Home Assistant and the physical screen | -Een verse installatie zonder ESPHome Device Builder werkt dus ook. Als er nog -geen ESPHome `secrets.yaml` bestaat, vraagt onze wizard de wifi eenmalig en -bewaart die lokaal. Bestaande wifi-secrets worden hergebruikt. API- en OTA-sleutels -worden per nieuw scherm aangemaakt en blijven in het eigen apparaatprofiel. +So a fresh installation without ESPHome Device Builder also works. If there's +no ESPHome `secrets.yaml` yet, our wizard asks for Wi-Fi once and +stores it locally. Existing Wi-Fi secrets are reused. API and OTA keys +are generated per new screen and stay in that device's own profile. -### Stap voor stap +### Step by step -Voor Home Assistant OS met Apps/Add-ons op **aarch64 of amd64**: +For Home Assistant OS with Apps/Add-ons on **aarch64 or amd64**: -1. Open de appwinkel en voeg deze repository toe: +1. Open the App store and add this repository: `https://github.com/MaxGramser/homeassistant_espscreen`. -2. Installeer **ESP Screen Manager**, start de app en open **ESP Screens**. - ESPHome Device Builder is optioneel: de ESPHome-CLI zit al in deze app. -3. Sluit het scherm met een USB-datakabel aan op de **Home Assistant-machine** - en kies **Nieuw scherm**: CYD of Guition, een naam, de USB-poort en - **Installeren**. Ontbreekt wifi in de ESPHome `secrets.yaml`, dan vraagt het - venster die eenmalig en zet ESP Screens alleen de ontbrekende regels erin. - Het profiel met unieke API- en OTA-sleutels komt in de ESPHome-map; de build - en de flash lopen in hetzelfde venster (een eerste build duurt op een - Raspberry enkele minuten). Elk scherm krijgt zijn eigen profiel. -4. **CYD:** doorloop de kalibratie op het scherm. **Guition:** gebruikt GT911 - zonder resistieve kalibratie. Koppel daarna het ontdekte ESPHome-apparaat in - **Instellingen → Apparaten & diensten** met de API-sleutel die het venster - na de installatie toont (kopieerknop). Geef het apparaat toestemming om - Home Assistant-acties uit te voeren. -5. Selecteer het scherm in ESP Screens, kies je tegels en klik - **Opslaan & naar scherm**. Test vervolgens de fysieke bediening. +2. Install **ESP Screen Manager**, start the app, and open **ESP Screens**. + ESPHome Device Builder is optional: the ESPHome CLI is already in this app. +3. Connect the screen with a USB data cable to the **Home Assistant machine** + and choose **New screen**: CYD or Guition, a name, the USB port, and + **Install**. If Wi-Fi is missing from the ESPHome `secrets.yaml`, the window + asks for it once and ESP Screens only adds the missing lines. The profile + with unique API and OTA keys goes into the ESPHome folder; the build + and flash run in the same window (a first build takes a few minutes on a + Raspberry Pi). Each screen gets its own profile. +4. **CYD:** go through the calibration on the screen. **Guition:** uses GT911 + without resistive calibration. Then pair the discovered ESPHome device in + **Settings → Devices & services** using the API key the window + shows after installation (copy button). Grant the device permission to + perform Home Assistant actions. +5. Select the screen in ESP Screens, choose your tiles, and click + **Save & send to screen**. Then test the physical controls. -Je kunt later vanuit **Firmware & USB → Wifi / OTA** nieuwe firmware installeren. -Gebruik voor een bestaand scherm altijd het bestaande profiel; opnieuw een -installatieprofiel aanmaken genereert nieuwe sleutels. +You can install new firmware later from **Firmware & USB → Wi-Fi / OTA**. +For an existing screen, always use the existing profile; creating a new +installation profile generates new keys. -## Tegels en kleuren aanpassen +## Customizing tiles and colors -Klik een tegel in het schermvoorbeeld. Kies bij **Pastel achtergrond** een kleur, -zoals rood of groen. Pas eventueel de naam, klikactie, mini-slider of grote -waarde aan. Klik **Opslaan & naar scherm** om de wijzigingen toe te passen. -Dit vereist na de eerste ondersteunende firmware-update geen nieuwe flash. +Click a tile in the screen preview. Under **Pastel background**, choose a color, +such as red or green. Optionally adjust the name, click action, mini-slider, or large +value. Click **Save & send to screen** to apply the changes. +After the first supporting firmware update, this requires no new flash. -Een kleur is een vaste keuze voor die tegel: hij blijft dus bijvoorbeeld rood -wanneer je het alles-uit-script gebruikt. De entiteitsstatus en actiefeedback -blijven afzonderlijk zichtbaar. +A color is a fixed choice for that tile: it stays red, for example, +even when you run the all-off script. The entity status and action feedback +stay separately visible. -## Bovenbalk +## Top bar -Bovenaan de editor staat per scherm de **Bovenbalk**: links de naam, rechts tot -zes onderdelen. **+ Toevoegen** biedt de tijd, een analoge klok en de datum (die -tellen op het scherm zelf door, ook zonder Home Assistant), suggesties uit je eigen -huis (temperatuur en verbruik uit de ruimte van het scherm, het weer, aantal thuis, -zon op en onder) en een zoekveld voor elke entiteit, ook een telefoon -(`device_tracker`), een slot, het alarmpaneel of `zone.home`. Sleep de onderdelen -om de volgorde te wijzigen; tik erop om ze in te stellen: +At the top of the editor, each screen has its **Top bar**: the name on the left, up to +six items on the right. **+ Add** offers the time, an analog clock, and the date (which +keep ticking on the screen itself, even without Home Assistant), suggestions from your own +home (temperature and power usage from the screen's room, the weather, how many people are home, +sunrise and sunset), and a search field for any entity, including a phone +(`device_tracker`), a lock, the alarm panel, or `zone.home`. Drag the items +to change their order; tap one to configure it: -- **Wat laten zien:** de status zoals Home Assistant hem schrijft (21,3 °C, 65%, - 1.249 W, Open/Dicht, Thuis/Weg, Afwezig), of **Laatst gewijzigd**: "Zojuist", - "5 min geleden", "Gisteren". Een tijdstempel-sensor telt ook vooruit ("Over 2 uur"). -- **Icoon:** automatisch zoals Home Assistant (een open deur krijgt een open - deur-icoon), een eigen icoon uit de lijst of geen icoon. -- **Tonen:** altijd, of **alleen als actief**: het onderdeel verschijnt pas als het - aan, open, thuis of meer dan 0 is. Handig voor een open deur of een draaiende - wasmachine. Actieve onderdelen kleuren zoals in Home Assistant (open deur amber, - alarm ingeschakeld groen, alarm afgegaan rood). +- **What to show:** the state as Home Assistant writes it (21.3 °C, 65%, + 1,249 W, Open/Closed, Home/Away, Armed away), or **Last changed**: "Just now", + "5 min ago", "Yesterday". A timestamp sensor can also count forward ("In 2 hours"). +- **Icon:** automatic, matching Home Assistant (an open door gets an open- + door icon), a custom icon from the list, or no icon. +- **Show:** always, or **only when active**: the item only appears when it's + on, open, home, or greater than 0. Handy for an open door or a running + washing machine. Active items are colored like in Home Assistant (open door amber, + alarm armed green, alarm triggered red). -Het schermvoorbeeld tekent de balk met dezelfde letters en regels als het scherm: -alle waarden op één lijn met de naam, iconen op cijferhoogte, gelijke afstanden. -Past niet alles naast de naam, dan krijgt de naam puntjes en laat het scherm de -voorste onderdelen weg; de editor markeert die met strepen. Oudere firmware toont -tot de update alleen de naam en de tijd (als die in de balk staat). +The screen preview draws the bar with the same letters and rules as the screen: +all values on one line with the name, icons aligned to digit height, equal spacing. +If not everything fits next to the name, the name gets an ellipsis and the screen drops the +leading items; the editor marks those with dashes. Until updated, older firmware shows +only the name and the time (if that's in the bar). -## Updates en behoud van je instellingen +## Updates and keeping your settings -| Wijziging | Actie | +| Change | Action | | --- | --- | -| Tegels, namen, kleuren, volgorde of scherminstellingen | Opslaan in ESP Screens; geen firmwareflash | -| Nieuwe versie van de beheerpagina | ESP Screen Manager updaten in de HA-appwinkel | -| Nieuwe functie op het fysieke scherm | Knop **Bijwerken** bij het scherm (badge *Update x.y.z*), of het vinkje **Elke nacht automatisch bijwerken** | +| Tiles, names, colors, order, or screen settings | Save in ESP Screens; no firmware flash | +| New version of the management page | Update ESP Screen Manager in the HA App store | +| New feature on the physical screen | The **Update** button on the screen (badge *Update x.y.z*), or the **Update automatically every night** checkbox | -Elke appversie hoort bij één firmwareversie. Na een app-update toont de lijst -per scherm of er nieuwere firmware is. **Bijwerken** bouwt het eigen profiel -met de ingebouwde CLI, installeert draadloos en wacht tot het scherm terug is. -Met het vinkje gebeurt dat 's nachts vanzelf, één scherm tegelijk; een -mislukking stopt de ronde en geeft een melding in Home Assistant. Firmware -0.2.17+ meldt daarvoor zelf apparaatnaam en IP-adres; een ouder scherm vraagt -eenmalig het IP-adres. Handmatig kan nog steeds via Firmware & USB → Wifi / OTA. +Every app version belongs to one firmware version. After an app update, the list +shows per screen whether newer firmware is available. **Update** builds that screen's own profile +with the built-in CLI, installs it wirelessly, and waits until the screen is back. +With the checkbox enabled, that happens automatically at night, one screen at a time; a +failure stops the round and posts a notification in Home Assistant. Firmware +0.2.17+ reports its own device name and IP address for this; an older screen asks +for the IP address once. You can still do it manually via Firmware & USB → Wi-Fi / OTA. -De eigen YAML en wifi/API/OTA-instellingen blijven in de ESPHome-configmap. -Tegelindelingen en opties staan in de permanente appdata. CYD-kalibratie en -schermvoorkeuren blijven op het apparaat opgeslagen. Updates vervangen deze -gebruikersgegevens niet. Maak wel normale Home Assistant-back-ups en bewaar je -apparaatprofielen; een app verwijderen of flashgeheugen wissen is geen update. +The device's own YAML and Wi-Fi/API/OTA settings stay in the ESPHome config folder. +Tile layouts and options live in the app's persistent data. CYD calibration and +screen preferences stay stored on the device. Updates don't replace this +user data. Do still make normal Home Assistant backups and keep your +device profiles; removing an app or wiping flash memory is not an update. -**Zuinig, ook met veel schermen (0.2.39 / firmware 0.2.33).** De app stuurt een -scherm alleen de tegel die veranderde, als één actie -(`esphome._screen_message`) in plaats van blokjes in een tekstveld. -Elke twee minuten volgt een kleine ping met de revisie van de indeling; meldt het -scherm dat die niet klopt (na een herstart bijvoorbeeld), dan komt alles opnieuw. -Grafieken van sensortegels komen uit de statistieken van Home Assistant, in één -vraag voor alle schermen. Oudere firmware blijft werken via het tekstveld en de -volledige herhaling per twee minuten. De diagnostische sensor `Uptime` is -vervangen door het tijdstempel `Opgestart`. +**Efficient, even with many screens (0.2.39 / firmware 0.2.33).** The app sends a +screen only the tile that changed, as a single action +(`esphome._screen_message`) instead of chunks in a text field. +Every two minutes, a small ping follows with the layout revision; if the +screen reports that it doesn't match (after a restart, for example), everything is resent. +Graphs on sensor tiles come from Home Assistant's statistics, in one +query for all screens. Older firmware still works via the text field and the +full resend every two minutes. The diagnostic sensor `Uptime` has been +replaced by the `Last boot` timestamp. -Zie [releasegeschiedenis](screen_manager/CHANGELOG.md) en -[releases en protocolcompatibiliteit](docs/RELEASING.md). +See the [release history](screen_manager/CHANGELOG.md) and +[releases and protocol compatibility](docs/RELEASING.md). -**Voor wie zelf publiceert:** elke push naar GitHub is een release. Verhoog dan -altijd ook de add-onversie in `screen_manager/config.yaml` en zet de wijziging in -de CHANGELOG, anders biedt de HA-appwinkel geen update aan. Een wijziging aan het -scherm krijgt daarnaast een nieuwe `SCREEN_FIRMWARE_VERSION` in beide bordprofielen. +**If you publish your own fork:** every push to GitHub is a release. Always also +bump the add-on version in `screen_manager/config.yaml` and log the change in +the CHANGELOG, otherwise the HA App store won't offer an update. A change to the +screen also gets a new `SCREEN_FIRMWARE_VERSION` in both board profiles. -## Handleidingen en hulp bij installatie +## Guides and installation help -- [Complete installatie vanuit ESP Screens](docs/EASY_SETUP.md) -- [Guition-hardware, montage en rotatie](docs/GUITION.md) -- [CYD-kalibratie en USB-diagnose](docs/CALIBREREN.md) -- [Fysieke acceptatietest](docs/ACCEPTATIE.md) -- [Instructies voor developers en LLM's](AGENTS.md) +- [Complete installation from ESP Screens](docs/EASY_SETUP.md) +- [Guition hardware, mounting, and rotation](docs/GUITION.md) +- [CYD calibration and USB diagnostics](docs/CALIBRATING.md) +- [Physical acceptance test](docs/ACCEPTANCE.md) +- [Instructions for developers and LLMs](AGENTS.md) -Geef een developer of LLM een schone kopie van deze repository en bijvoorbeeld: +Give a developer or LLM a clean copy of this repository and, for example: -> Lees AGENTS.md, README.md en docs/EASY_SETUP.md. Help me dit CYD- of -> Guition-scherm via USB op mijn Home Assistant te installeren. Identificeer -> het bord en gebruik mijn bestaande profiel als dat er al is. Begeleid -> kalibratie, HA-koppeling, tegelkeuze en fysieke tests. Houd sleutels lokaal -> en geef aan welke controles werkelijk zijn uitgevoerd. +> Read AGENTS.md, README.md, and docs/EASY_SETUP.md. Help me install this CYD or +> Guition screen via USB on my Home Assistant. Identify +> the board and use my existing profile if one already exists. Guide me through +> calibration, HA pairing, tile selection, and physical tests. Keep keys local, +> and state which checks were actually carried out. -Een build bewijst niet dat fysieke touch of het paneelbeeld goed is. De eigenaar -moet het beeld controleren en de gevraagde tikken uitvoeren. +A successful build doesn't prove the physical touch or panel image is correct. The owner +must check the display and perform the requested taps.
-Oudere handmatige CYD-installatie via een computer +Older manual CYD installation via a computer -Onderstaande route gebruikt het oudere handmatige profiel met maximaal tien -tegels. De positiebeperkingen hiervan gelden niet voor de twintig runtime-tegels -van ESP Screen Manager. Gebruik voor nieuwe installaties bij voorkeur de route -hierboven; de handmatige instructies blijven beschikbaar voor onderhoud. +The route below uses the older manual profile with up to ten +tiles. Its position constraints don't apply to ESP Screen Manager's twenty +runtime tiles. For new installations, prefer the route +above; the manual instructions remain available for maintenance. -# CYD Home Assistant-bedieningsscherm +# CYD Home Assistant control screen -Van een nieuwe **ESP32-2432S028 met ILI9341 + XPT2046** naar een gekalibreerd -Home Assistant-scherm, via USB. Met lichtbediening, scènes/scripts, climate, -vacuum, maximaal tien tegels en vaste pagina's. Bij maximaal zes tegels -verdwijnt de paginering. Standby begint standaard na tien minuten. +From a fresh **ESP32-2432S028 with ILI9341 + XPT2046** to a calibrated +Home Assistant screen, over USB. With light control, scenes/scripts, climate, +vacuum, up to ten tiles, and fixed pages. With six tiles or fewer, +pagination disappears. Standby starts after ten minutes by default. -**Handmatige installatie via de computer:** je hoeft geen Home Assistant-token te maken -of een ESPHome-add-on te installeren om via de computer te flashen. +**Manual installation via a computer:** you don't need to create a Home Assistant token +or install an ESPHome add-on to flash from a computer. -## Met een LLM of developer werken +## Working with an LLM or developer -Geef de uitgepakte map en deze opdracht: +Give it the unpacked folder and this prompt: -> Lees eerst AGENTS.md en README.md. Begeleid mij bij een nieuwe installatie -> op een ESP32-2432S028 die via USB is aangesloten, met mijn eigen Home -> Assistant. Controleer het bord, de seriële poort en de Python-omgeving. -> Maak een lokaal device.yaml-profiel; gebruik geen bestaande persoonlijke -> entiteiten of paneelkalibratie. Doorloop USB-kalibratie, HA-koppeling, -> tegelconfiguratie en de acceptatietest. Vraag mij om fysieke tikken wanneer -> nodig. Houd sleutels en wachtwoorden in lokale bestanden en rapporteer -> welke controles echt zijn uitgevoerd. +> First read AGENTS.md and README.md. Guide me through a new installation +> on an ESP32-2432S028 connected via USB, with my own Home +> Assistant. Check the board, the serial port, and the Python environment. +> Create a local device.yaml profile; don't use any existing personal +> entities or panel calibration. Walk through USB calibration, HA pairing, +> tile configuration, and the acceptance test. Ask me for physical taps when +> needed. Keep keys and passwords in local files, and report +> which checks were actually carried out. -De begeleider kan de code en logs lezen, maar kan het fysieke scherm niet -vanzelf zien. Jij controleert het beeld en voert de gevraagde tikken uit. +The assistant can read the code and logs, but can't see the physical screen +on its own. You check the display and perform the requested taps. -## 1. Benodigdheden controleren +## 1. Check what you need -- Het bedoelde bord: ESP32-2432S028, 320×240, **ILI9341-display en resistieve - XPT2046-touch**. Er bestaan borden met bijna dezelfde naam en een andere - schermcontroller. Deze handleiding garandeert die varianten niet. -- Een USB-**datakabel** en een computer met macOS, Linux of Windows. -- Python **3.11–3.14**, internet voor de eerste build en enkele GB vrije ruimte. - De versiegrens komt uit het geteste ESPHome-pakket; gebruik geen Python 3.15. -- Een 2,4GHz-wifinetwerk; Home Assistant moet het bord via het netwerk kunnen - bereiken. Houd het wifiwachtwoord lokaal beschikbaar. -- Toegang tot Home Assistant om entiteiten te kiezen en ESPHome toe te voegen. +- The intended board: ESP32-2432S028, 320×240, **ILI9341 display and resistive + XPT2046 touch**. Boards exist with almost the same name and a different + display controller. This guide doesn't cover those variants. +- A USB **data cable** and a computer running macOS, Linux, or Windows. +- Python **3.11–3.14**, internet access for the first build, and a few GB of free space. + The version limit comes from the tested ESPHome package; don't use Python 3.15. +- A 2.4GHz Wi-Fi network; Home Assistant must be able to reach the board over + the network. Keep the Wi-Fi password available locally. +- Access to Home Assistant to choose entities and add ESPHome. -Gebruik bij voorkeur één aangesloten ESP-bord tegelijk. Noteer de variant en -het MAC-adres uit de boot-/uploadlogs. `home-like.yaml` en `buttons.yaml` uit -het oorspronkelijke project zijn **niet** de startersconfiguratie voor dit bord. +Preferably keep only one ESP board connected at a time. Note the variant and +the MAC address from the boot/upload logs. -## 2. De werkomgeving installeren +## 2. Set up the work environment -Kloon [de repository](https://github.com/MaxGramser/homeassistant_espscreen) -of pak de starter-ZIP uit. Open een terminal **in de codefolder**. Alle opdrachten hieronder -worden vanuit die folder uitgevoerd. +Clone [the repository](https://github.com/MaxGramser/homeassistant_espscreen) +or unpack the starter ZIP. Open a terminal **in the code folder**. All commands below +are run from that folder. macOS/Linux: @@ -343,9 +342,9 @@ python -m pip install --upgrade pip python -m pip install -r requirements.txt ``` -Als PowerShell activeren blokkeert, gebruik rechtstreeks -`.\.venv\Scripts\python.exe` in plaats van `python`; een systeemwijziging is -niet nodig. Controleer daarna: +If activating PowerShell is blocked, use +`.\.venv\Scripts\python.exe` directly instead of `python`; no system change is +needed. Then check: ```sh python --version @@ -353,97 +352,97 @@ python -m esphome version python -m serial.tools.list_ports ``` -Verwacht **ESPHome 2026.6.2**. Deze repo bevat een aangepaste touchdriver die -met deze versie is getest. Upgrade niet stilzwijgend tijdens een installatie. -Zie ook de [officiële ESPHome-installatiehandleiding](https://esphome.io/install/). +Expect **ESPHome 2026.6.2**. This repo includes a custom touch driver that's +tested against this version. Don't silently upgrade during an installation. +See also the [official ESPHome installation guide](https://esphome.io/install/). -Noteer de poort: bijvoorbeeld `/dev/cu.usbserial-130` op macOS, -`/dev/ttyUSB0` op Linux of `COM3` op Windows. Verderop staat ``; -vervang dat steeds door de werkelijke poort, zonder punthaken. +Note the port: for example `/dev/cu.usbserial-130` on macOS, +`/dev/ttyUSB0` on Linux, or `COM3` on Windows. Further down, `` +appears; always replace it with the actual port, without angle brackets. -## 3. Een eigen apparaatprofiel maken +## 3. Create your own device profile -Kies een unieke naam, bijvoorbeeld `display-keuken`: +Choose a unique name, for example `display-kitchen`: ```sh -python tools/new_device.py --name display-keuken --friendly-name "Display keuken" +python tools/new_device.py --name display-kitchen --friendly-name "Display kitchen" ``` -Dit maakt drie lokale bestanden en overschrijft niets: +This creates three local files and overwrites nothing: -| Bestand | Wat je erin aanpast | +| File | What you edit in it | |---|---| -| `device.yaml` | Apparaatnaam, ruimte, tegels en gedrag | -| `calibration.yaml` | Kalibratie van dit ene fysieke paneel; de wizard schrijft dit | -| `secrets.yaml` | Wifi, API-encryptiesleutel, OTA- en fallbackwachtwoord | +| `device.yaml` | Device name, room, tiles, and behavior | +| `calibration.yaml` | Calibration for this one physical panel; the wizard writes this | +| `secrets.yaml` | Wi-Fi, API encryption key, OTA and fallback password | -De sleutels worden uniek gegenereerd. Vul in `secrets.yaml` alleen jouw -`wifi_ssid` en `wifi_password` in. Laat de gegenereerde sleutels staan en -bewaar dit bestand bij je eigen configuratie. Deel het niet mee in de ZIP. -De API-encryptiesleutel heb je later nodig bij de HA-koppeling; kopieer die -lokaal uit het bestand, niet via een openbare chat. +The keys are generated uniquely. In `secrets.yaml`, only fill in your +`wifi_ssid` and `wifi_password`. Leave the generated keys as they are and +keep this file with your own configuration. Don't include it in the ZIP. +You'll need the API encryption key later for HA pairing; copy it +locally from the file, not through a public chat. -Bestaan deze bestanden al? Werk in een nieuwe kopie van de map voor een nieuw -scherm. Wis de sleutels/kalibratie van een werkend scherm niet. +Do these files already exist? Work in a new copy of the folder for a new +screen. Don't erase the keys/calibration of a working screen. -`device.yaml` importeert de basis en jouw kalibratie. Het overschrijft ook -**alle verborgen tegelposities**, zodat die geen entiteiten van iemand anders -blijven volgen. Begin met de zes voorbeeldtegels. `DIRECT_ACTIONS: "false"` -voorkomt directe HA-aansturing tijdens de eerste installatie. Maak in deze -fase nog geen HA-automations die op de actie-sensor reageren. +`device.yaml` imports the base configuration and your calibration. It also overwrites +**all hidden tile positions**, so they don't keep tracking someone else's +entities. Start with the six example tiles. `DIRECT_ACTIONS: "false"` +prevents direct HA control during the initial installation. At this +stage, don't yet create HA automations that react to the action sensor. -## 4. Eerste USB-flash en kalibratie +## 4. First USB flash and calibration -Controleer eerst de configuratie (ESPHome schermt secrets standaard af): +First check the configuration (ESPHome masks secrets by default): ```sh python -m esphome config device.yaml ``` -Flash de kalibratiemodus, die zonder HA-verbinding werkt: +Flash calibration mode, which works without an HA connection: ```sh -python -m esphome -s CALIBRATION_ON_BOOT true run device.yaml --device +python -m esphome -s CALIBRATION_ON_BOOT true run device.yaml --device ``` -De eerste build kan meerdere minuten duren. Na upload verschijnen vijf -kruisjes op een donker scherm. `run` blijft logs tonen: stop **alleen de -loglezer met Ctrl+C** voordat je de kalibratiewizard start. Het bord blijft aan. +The first build can take several minutes. After uploading, five +crosshairs appear on a dark screen. `run` keeps showing logs: stop **only the +log reader with Ctrl+C** before starting the calibration wizard. The board stays on. -Volg nu [docs/CALIBREREN.md](docs/CALIBREREN.md): per doel worden drie tikken -verzameld, de vier hoeken bepalen de correctie en het midden is een -onafhankelijke controle. Daarna flash je de uitkomst en meet je opnieuw. -**Kopieer nooit de meetwaarden van een ander paneel.** +Now follow [docs/CALIBRATING.md](docs/CALIBRATING.md): three taps are +collected per target, the four corners determine the correction, and the center is an +independent check. Then you flash the result and measure again. +**Never copy the measurements from another panel.** -## 5. Home Assistant koppelen +## 5. Pair with Home Assistant -Na een geslaagde kalibratie flash je de normale modus: +After a successful calibration, flash the normal mode: ```sh -python -m esphome run device.yaml --device +python -m esphome run device.yaml --device ``` -Controleer in de logs de wifi-IP, stabiele boot en apparaatnaam. Ctrl+C sluit -de loglezer. Het scherm moet nu de normale tegelpagina tonen. +Check the logs for the Wi-Fi IP, a stable boot, and the device name. Ctrl+C closes +the log reader. The screen should now show the normal tile page. -1. Open in HA **Instellingen → Apparaten & diensten**. -2. Kies het ontdekte ESPHome-apparaat, of **Integratie toevoegen → ESPHome**. -3. Gebruik `display-keuken.local` of het IP-adres uit de logs; API-poort **6053**. -4. Vul de `api_encryption_key` uit je lokale `secrets.yaml` in als daarom wordt gevraagd. -5. Controleer of de apparaatnaam klopt en de status online wordt. -6. Open de opties/configuratie van deze ESPHome-integratie en schakel - **Allow the device to perform Home Assistant actions** in. +1. In HA, open **Settings → Devices & services**. +2. Choose the discovered ESPHome device, or **Add integration → ESPHome**. +3. Use `display-kitchen.local` or the IP address from the logs; API port **6053**. +4. Enter the `api_encryption_key` from your local `secrets.yaml` if asked. +5. Check that the device name is correct and the status becomes online. +6. Open this ESPHome integration's options/configuration and enable + **Allow the device to perform Home Assistant actions**. -De precieze vertaling/plaats van de optie verschilt per HA-versie. Zie de -[officiële integratiehandleiding](https://www.home-assistant.io/integrations/esphome). -De HA-integratie verbindt met het bord; de ESPHome Device Builder/add-on is -optioneel en is een andere functie. +The exact wording/location of this option varies by HA version. See the +[official integration guide](https://www.home-assistant.io/integrations/esphome). +The HA integration connects to the board; the ESPHome Device Builder/add-on is +optional and is a separate feature. -## 6. Eigen tegels instellen +## 6. Configure your own tiles -Volg [docs/TEGELS.md](docs/TEGELS.md). Verzamel de echte entity-ID's in HA, -controleer ondersteunde attributen/acties en pas `device.yaml` aan. Gebruik -korte titels. Als je alles hebt gecontroleerd: +Follow [docs/TILES.md](docs/TILES.md). Collect the real entity IDs in HA, +check supported attributes/actions, and edit `device.yaml`. Use +short titles. Once you've checked everything: ```yaml substitutions: @@ -451,52 +450,52 @@ substitutions: TILE_COUNT: "6" ``` -Dit zijn wijzigingen in de **bestaande** substitutions-map, geen tweede map -onderaan het bestand. Bouw en flash opnieuw met `run device.yaml`. De tegels -volgen daarna de werkelijke HA-status. Een actieve API-verbinding alleen -bewijst nog niet dat een actie door HA is toegestaan. +These are changes in the **existing** substitutions map, not a second map +at the bottom of the file. Build and flash again with `run device.yaml`. The tiles +will then follow the real HA state. An active API connection alone +doesn't prove that HA has been granted permission for actions. -## 7. Opleveren en testen +## 7. Handover and testing -Voer [docs/ACCEPTATIE.md](docs/ACCEPTATIE.md) uit. Dat omvat een onafhankelijke -touchmeting, beide pagina's indien aanwezig, lange druk, sliders, climate, -vacuum, werkelijke HA-status en de juiste actie-effecten. De ingebouwde -renderproef stuurt zelf geen HA-apparaten aan: +Run through [docs/ACCEPTANCE.md](docs/ACCEPTANCE.md). That covers an independent +touch measurement, both pages if present, long press, sliders, climate, +vacuum, real HA state, and the correct action effects. The built-in +render test doesn't control any HA devices itself: ```sh -python diagnostics/run_ui_test.py --host display-keuken.local --name display-keuken +python diagnostics/run_ui_test.py --host display-kitchen.local --name display-kitchen ``` -Bewaar bij deze persoon: `device.yaml`, `calibration.yaml`, `secrets.yaml`, de -meetbestanden, de gebruikte ESPHome-versie en een kort opleverrapport. -Voor later bijwerken kun je USB blijven gebruiken of OTA gebruiken: +Keep with this person: `device.yaml`, `calibration.yaml`, `secrets.yaml`, the +measurement files, the ESPHome version used, and a short handover report. +For later updates you can keep using USB or use OTA: ```sh -python -m esphome run device.yaml --device display-keuken.local +python -m esphome run device.yaml --device display-kitchen.local ``` -Verander naam/API-sleutel niet zonder ook de HA-koppeling bij te werken. -Bij storing: [docs/PROBLEMEN.md](docs/PROBLEMEN.md). +Don't change the name/API key without also updating the HA pairing. +For troubleshooting: [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md). -## Deze code aan de volgende persoon geven +## Handing this code to the next person -Gebruik de exporteur in plaats van je hele werkmap te kopiëren: +Use the exporter instead of copying your entire working folder: ```sh python tools/export_bundle.py --output dist/cyd-starter.zip ``` -De ZIP bevat de code, lokale fonts, licenties, handleidingen en neutrale -voorbeelden. De basisdefaults worden voor de ontvanger geneutraliseerd. -Geen `secrets.yaml`, eigen `device.yaml`, metingen, logs, `.git`, `.esphome`, -Python-omgeving of firmwarebackups. De exporteur weigert een bestaande ZIP -te overschrijven; kies bij een nieuwe versie een andere bestandsnaam. +The ZIP contains the code, local fonts, licenses, guides, and neutral +examples. The base defaults are neutralized for the recipient. +No `secrets.yaml`, personal `device.yaml`, measurements, logs, `.git`, `.esphome`, +Python environment, or firmware backups. The exporter refuses to overwrite an +existing ZIP; choose a different filename for a new version. -De originele basis is van +The original base is from [akuehlewind/ESPHome-touch-display-mount](https://github.com/akuehlewind/ESPHome-touch-display-mount). -Project-, ESPHome-driver- en fontlicenties staan respectievelijk in `LICENSE`, -`components/xpt2046/LICENSE` en `fonts/`. Historische borddiagnose staat in -[CYD_STABILITY.md](CYD_STABILITY.md); volg voor een **nieuwe** installatie de -handleidingen hierboven. +Project, ESPHome driver, and font licenses are in `LICENSE`, +`components/xpt2046/LICENSE`, and `fonts/` respectively. Historical board diagnostics are in +[CYD_STABILITY.md](CYD_STABILITY.md); for a **new** installation, follow the +guides above.
diff --git a/TEST_RESULTS.md b/TEST_RESULTS.md index 90a8ffe..6c5d91d 100644 --- a/TEST_RESULTS.md +++ b/TEST_RESULTS.md @@ -1,68 +1,68 @@ -# Testresultaten ESP32-2432S028 — 12 september 2026 +# Test results — ESP32-2432S028 — September 12, 2026 -Getest op de via USB aangesloten `cyd-2432s028`, ESP32 rev3.1, ESPHome -2026.6.2 / ESP-IDF. De firmware meldt compilatietijd -`2026-09-12 09:54:02 +0200` en is via USB geschreven en geverifieerd. +Tested on the USB-connected `cyd-2432s028`, ESP32 rev3.1, ESPHome +2026.6.2 / ESP-IDF. The firmware reports compile time +`2026-09-12 09:54:02 +0200` and was written and verified over USB. -## Geslaagd +## Passed -- C++-regressietests voor ruis, contactdender, verspringende aanrakingen, - echte sleepbewegingen, temperatuurafronding en `millis()`-overflow. -- Vijf indelings-/configuratietests voor schermgrenzen, overlap, modale - lagen, voorwaardelijke paginering en guards vóór alle tegelhandlers. -- ESPHome-validatie met `TILE_COUNT: 6`; navigatie is in de code aan - `count > 6` gekoppeld en tegels boven het ingestelde aantal zijn verborgen. -- Volledige ESP-IDF-build en USB-upload met verificatie van de geschreven data. -- Verbinding met de bestaande Home Assistant-installatie en ontvangst van - entiteitsstatussen. -- **10 paginacontroles en 50 overlay-cycli op het echte apparaat.** - Render-startteller steeg van 38 naar 101 tussen de eerste en - laatste paginacontrole: de test controleert ook werkelijke tekenactiviteit. -- Runtime-geometrie: tegelgebied `(0,40)–(319,203)`, knop Volgende - `(211,206)–(310,237)`. De gebieden overlappen niet. -- Vrije heap tijdens de tien testmetingen: 86,416–139,696 bytes - volgens `esp_get_free_heap_size()`. Geen oplopend geheugenverlies of reset - in deze korte tekenproef. +- C++ regression tests for noise, contact bounce, jumping touches, + real drag movements, temperature rounding, and `millis()` overflow. +- Five layout/configuration tests for screen bounds, overlap, modal + layers, conditional pagination, and guards ahead of all tile handlers. +- ESPHome validation with `TILE_COUNT: 6`; navigation is tied in the code to + `count > 6`, and tiles above the configured count stay hidden. +- Full ESP-IDF build and USB upload with verification of the written data. +- Connection to the existing Home Assistant installation and receipt of + entity states. +- **10 page checks and 50 overlay cycles on the real device.** + The render-start counter rose from 38 to 101 between the first and + last page check: the test also verifies actual draw activity. +- Runtime geometry: tile area `(0,40)–(319,203)`, Next button + `(211,206)–(310,237)`. The areas don't overlap. +- Free heap during the ten test measurements: 86,416–139,696 bytes + per `esp_get_free_heap_size()`. No growing memory loss or reset + during this short render test. -## Fysieke touchdiagnose +## Physical touch diagnostics -De gebruiker tikte herhaaldelijk de vier schermhoeken aan. De oorspronkelijke -meting sprong linksboven soms meer dan honderd pixels weg. Na de driverfilter -bleven de samples in herkenbare hoekgroepen. De overblijvende scheefstand is -met een affine correctie op de hoekmedianen gecorrigeerd; de regressietest -plaatst die medianen binnen vijf pixels van het doel. De bronwaarden en -coëfficiënten staan in de configuratie, tests en CYD_STABILITY.md. +The user repeatedly tapped the four screen corners. The raw +measurement at top-left sometimes jumped more than a hundred pixels off. After the driver filter, +the samples stayed within recognizable corner clusters. The remaining skew was +corrected with an affine correction on the corner medians; the regression test +places those medians within five pixels of the target. The source values and +coefficients are in the configuration, tests, and CYD_STABILITY.md. -De gebruiker bevestigde na de laatste flash dat de juiste knoppen reageren -en de bediening nu goed werkt. +After the final flash, the user confirmed that the correct buttons respond +and the controls now work well. -## Grenzen +## Limitations -Dit is geen langdurige duurproef. Home Assistant-acties zijn niet automatisch -uitgevoerd tijdens de schermtest. De gebruiker heeft de fysieke bediening na de laatste kalibratiecorrectie -bevestigd: "Ja, dit werkt nu goed". De tien minuten -standby zijn ingesteld; er is niet tien minuten gewacht om de volledige -idle-timeout af te laten lopen. +This is not a long-duration endurance test. Home Assistant actions were not automatically +performed during the screen test. The user confirmed the physical controls after the final calibration correction +with: "Yes, this works well now". The ten-minute +standby is configured; nobody waited the full ten minutes to let the +complete idle timeout run out. -Lokale ruwe logs staan in `diagnostics/acceptance-ui-test.log` en -`diagnostics/acceptance-runtime.log` en worden niet in Git opgenomen. +Local raw logs are in `diagnostics/acceptance-ui-test.log` and +`diagnostics/acceptance-runtime.log` and are not included in Git. -## Overdraagbare installatie — aanvullende softwarecontrole +## Portable installation — additional software check -Op 12 september 2026 is de nieuwe onboarding getest: +On September 12, 2026, the new onboarding flow was tested: -- 17 Python-tests slagen: layout, kalibratie-fit, onafhankelijke verificatie, - foutieve/ruisende metingen, unieke secrets, niet overschrijven en export. -- Beide C++17-regressieprogramma's slagen (touchfilter en UI-guard). -- Starter-ZIP uitgepakt in een verse tijdelijke map; nieuw profiel met eigen - gegenereerde sleutels aangemaakt. Volledige ESPHome 2026.6.2 / ESP-IDF-build - met kalibratie-opstartmodus geslaagd. -- Gegenereerde C++ gecontroleerd op identiteitcorrectie voor het nieuwe bord. - Het aparte lokale profiel behoudt de eerder gemeten zes correctiecoëfficiënten. -- Git-index en bestaande historie gecontroleerd op de lokale credentials; - geen overeenkomsten. Lokale profielen en secrets zijn uitgesloten. +- 17 Python tests pass: layout, calibration fit, independent verification, + faulty/noisy measurements, unique secrets, not overwriting, and export. +- Both C++17 regression programs pass (touch filter and UI guard). +- Starter ZIP unpacked into a fresh temporary folder; a new profile was created with its own + generated keys. A full ESPHome 2026.6.2 / ESP-IDF build + with calibration boot mode succeeded. +- Generated C++ checked for identity correction on the new board. + The separate local profile retains the six previously measured correction coefficients. +- The Git index and existing history were checked for the local credentials; + no matches. Local profiles and secrets are excluded. -De vijfpuntswizard is met synthetische meetgegevens getest. Hij is in deze -ronde niet opnieuw fysiek uitgevoerd of naar het bestaande bord geflasht; -voor ieder nieuw paneel blijft de fysieke acceptatie verplicht. De bovenstaande -oudere hardwaretest betreft de eerder geïnstalleerde firmware. +The five-point wizard was tested with synthetic measurement data. In this +round it was not physically run again or flashed to the existing board; +physical acceptance remains mandatory for every new panel. The hardware test +above concerns the previously installed firmware. diff --git a/TILE_CONFIGURATION.md b/TILE_CONFIGURATION.md index 19bbfdd..c0f99da 100644 --- a/TILE_CONFIGURATION.md +++ b/TILE_CONFIGURATION.md @@ -1,14 +1,16 @@ -> Nieuwe ESP32-2432S028-installatie: begin bij [README.md](README.md) en -> [de actuele tegelhandleiding](docs/TEGELS.md). Hieronder staat ook legacygedrag. +> New ESP32-2432S028 installation: start at [README.md](README.md) and +> [the current tile guide](docs/TILES.md). Legacy behavior is also described below. # Tile Configuration Reference For the ESP32-2432S028 profile, use `home-like-2432s028.yaml` and [CYD_STABILITY.md](CYD_STABILITY.md). That profile uses fixed pages instead of scrolling, hides pagination when `TILE_COUNT <= 6`, and dims after ten minutes. -The scrolling description below applies to `home-like.yaml`. +The scrolling section below describes `home-like.yaml`, an early personal +profile that has since been removed from the repository; it's kept here only +as a historical record. -This document explains every substitution in `home-like.yaml` and provides copy-paste examples for all supported entity types. +This document explains every substitution used across the tile profiles and provides copy-paste examples for all supported entity types. --- @@ -55,11 +57,11 @@ of the entity id before the dot: | Entity | Domain | Accent | |--------|--------|--------| -| `script.goede_morgen` | `script` | `ACCENT_ACTION` (violet) | -| `scene.avondlicht` | `scene` | `ACCENT_ACTION` (violet) | -| `light.woonkamer_2` | `light` | `ACCENT_LIGHT` (amber) | +| `script.good_morning` | `script` | `ACCENT_ACTION` (violet) | +| `scene.evening_lights` | `scene` | `ACCENT_ACTION` (violet) | +| `light.living_room_2` | `light` | `ACCENT_LIGHT` (amber) | | `vacuum.s8` | `vacuum` | `ACCENT_DEVICE` (cyan) | -| `climate.airco` | `climate` | `ACCENT_DEVICE` (cyan) | +| `climate.ac` | `climate` | `ACCENT_DEVICE` (cyan) | The distinction that matters: a script or scene is something you *fire*, a device is something you switch *on and off*. Those should not look the same. @@ -74,7 +76,7 @@ Because the color follows the entity, dropping a different entity into a tile gives it the right color without touching any color key. > The domain lookup lives in a small `accent_for` lambda that appears **twice** -> in `home-like.yaml`: once in `ui_refresh` (for the tiles) and once in +> in each profile: once in `ui_refresh` (for the tiles) and once in > `open_value_overlay` (for the slider fill). Keep the two copies in sync. --- @@ -534,8 +536,10 @@ TILE1_LONGPRESS_ACTION_SERVICE: "light.toggle" # must set toggle explicitly, de ## Adding a tile All ten slots are filled. To grow the grid, add a `GRID_ROW6_Y` to every -orientation preset first. A tile is not a single block — it is six places in -`home-like.yaml`. Copy tile 9 (a light with a slider) or tile 10 (a service +orientation preset first (this applied to the now-removed `home-like.yaml`; +the maintained fixed-page profiles cap at ten tiles by design — see +[docs/TILES.md](docs/TILES.md)). A tile is not a single block — it is six +places. Copy tile 9 (a light with a slider) or tile 10 (a service call) and change the number: 1. **Substitutions** — a `TILE9_*` block. Copy an existing one wholesale. diff --git a/a.out b/a.out deleted file mode 100755 index 6b7a56a..0000000 Binary files a/a.out and /dev/null differ diff --git a/buttons.yaml b/buttons.yaml deleted file mode 100644 index 0acc0ad..0000000 --- a/buttons.yaml +++ /dev/null @@ -1,618 +0,0 @@ -############################################################ -# SmartDisplay (Cheap Yellow Display / CYD) – ESPHome + LVGL lockscreen UI -# -# Target hardware -# - Cheap Yellow Display (CYD) -# (ESP32 + ILI9341 320x240 display + XPT2046 touchscreen) -# -# Purpose -# - LVGL renders a simple “lockscreen” UI: time/date + 4 toggle buttons. -# -# Touch input → Home Assistant -# - Every touch publishes an “action” string via a text_sensor: -# sensor.smartdisplay_action = "btn1_press" / "btn2_press" / "btn3_press" / "btn4_press" -# - The sensor is auto-reset to "" after each press so repeated presses trigger reliably. -# -# Two supported control modes (choose what fits your setup) -# 1) Direct control (DEFAULT, simplest) -# - DIRECT_ACTIONS: "true" (default) -# - Each touch will: -# • publish the action string AND -# • directly call a Home Assistant service -# (default: light.toggle on BTN*_ENTITY) -# - Use this if you simply want to toggle lights without creating automations. -# -# 2) Automation-only control (custom logic) -# - DIRECT_ACTIONS: "false" -# - Each touch will ONLY publish the action string. -# - You implement the behavior in Home Assistant automations using: -# Trigger → State → sensor.smartdisplay_action → To: btn1_press (etc.) -# -# If you do NOT want to toggle lights -# - Either change the homeassistant.action blocks to a different service -# (e.g. switch.toggle, script.turn_on, scene.turn_on, ...) -# - Or set DIRECT_ACTIONS to "false" and handle everything via HA automations. -# -# UI updates -# - On boot, every minute, and whenever the configured HA entities change -# (mirrored into ESPHome to keep LVGL button states in sync). -# -# What you must adapt before flashing -# 1) Secrets: api.encryption.key, ota.password, WiFi credentials (prefer secrets.yaml) -# 2) BTN*_ENTITY: Home Assistant entity IDs you want to control/mirror -# 3) Touch calibration + transform: -# - calibration is panel-specific -# - touchscreen.transform MUST match display.transform exactly -# -# Home Assistant note -# - In the ESPHome device settings, enable: -# “Allow the device to perform Home Assistant actions” -# Otherwise homeassistant.action calls are blocked. -############################################################ - -# ========================================================== -# USER CONFIG (edit this section to match your HA setup) -# -# - DIRECT_ACTIONS: -# "true" -> touch publishes action + calls HA service (light.toggle by default) -# "false" -> touch only publishes action (all logic handled in HA) -# -# - BTN*_ENTITY: -# The Home Assistant entity to control and mirror for the button state. -# Must exist in Home Assistant. -# -# - BTN*_LABEL / BTN*_ICON: -# Only affects what is shown on the display. -# ========================================================== -substitutions: - # Enable/disable direct Home Assistant service calls on touch. - # - "true" -> touching a button also calls light.toggle (plug & play) - # - "false" -> touching a button ONLY publishes the action text_sensor - DIRECT_ACTIONS: "true" - - # Button 1 - BTN1_ENTITY: "light.desk_lamp" - BTN1_LABEL: "Desk" - BTN1_ICON: "\U000F095F" - - # Button 2 - BTN2_ENTITY: "light.ceiling_light" - BTN2_LABEL: "Ceiling" - BTN2_ICON: "\U000F0769" - - # Button 3 - BTN3_ENTITY: "light.globe_lamp" - BTN3_LABEL: "Globe" - BTN3_ICON: "\U000F08DD" - - # Button 4 - BTN4_ENTITY: "light.ambient_light" - BTN4_LABEL: "Ambient" - BTN4_ICON: "\U000F1051" -# ========================================================== - -# ============================================================ -# No need to edit below this line. -# All configuration is done via the substitutions above. -# Credentials are managed in secrets.yaml (see secrets.yaml.example). -# ============================================================ - -esphome: - name: smartdisplay - friendly_name: SmartDisplay - on_boot: - priority: 600 - then: - - script.execute: ui_refresh - -esp32: - board: esp32dev - framework: - type: arduino - -logger: - -# Credentials are loaded from secrets.yaml — copy secrets.yaml.example to secrets.yaml and fill in your values. -api: - encryption: - key: !secret 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: - -# Use Home Assistant time so the display always matches your HA timezone. -time: - - platform: homeassistant - id: homeassistant_time - on_time: - - seconds: 0 - minutes: /1 - then: - - script.execute: ui_refresh - -font: - - file: "gfonts://Roboto@500" - id: big_text - size: 60 - bpp: 4 - glyphs: ['&','@','!',',','.','?','"','%','(',')','+','-','_',':','°','0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z',' ','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','å','Ä','ä','Ö','ö','Ü','ü','/'] - - - file: "gfonts://Roboto@300" - id: notice_text - size: 16 - bpp: 4 - glyphs: ['&','@','!',',','.','?','"','%','(',')','+','-','_',':','°','0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z',' ','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','å','Ä','ä','Ö','ö','Ü','ü','/'] - - - file: "gfonts://Roboto@400" - id: label - size: 13 - bpp: 4 - glyphs: ['&','@','!',',','.','?','"','%','(',')','+','-','_',':','°','0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z',' ','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z','å','Ä','ä','Ö','ö','Ü','ü','/'] - - # Material Design Icons (MDI) — included at fonts/materialdesignicons-webfont.ttf (Apache 2.0) - # Source: https://github.com/Templarian/MaterialDesign-Webfont - - file: 'fonts/materialdesignicons-webfont.ttf' - id: materialdesign_icons - size: 35 - glyphs: ["\U000F095F", "\U000F0769", "\U000F08DD", "\U000F1051"] - -color: - - id: ACTIVE - hex: "FEC600" - - id: INACTIVE - hex: "808080" - -sensor: - - platform: wifi_signal - name: "Wifi Signal" - update_interval: 600s - - - platform: uptime - name: "Uptime" - id: uptime_s - update_interval: 15s - -binary_sensor: - - platform: status - name: "Node Status" - id: system_status - - # Mirror HA entities (used to render the "checked" state of each LVGL button) - # These are read-only "views" of HA state for UI sync. - - platform: homeassistant - id: ha_state_btn1 - entity_id: ${BTN1_ENTITY} - on_state: - then: - - script.execute: ui_refresh - - - platform: homeassistant - id: ha_state_btn2 - entity_id: ${BTN2_ENTITY} - on_state: - then: - - script.execute: ui_refresh - - - platform: homeassistant - id: ha_state_btn3 - entity_id: ${BTN3_ENTITY} - on_state: - then: - - script.execute: ui_refresh - - - platform: homeassistant - id: ha_state_btn4 - entity_id: ${BTN4_ENTITY} - on_state: - then: - - script.execute: ui_refresh - -text_sensor: - # ---------------------------------------------------------- - # SmartDisplay Action - # - # This is the main "touch trigger" exported to Home Assistant: - # - On touch, we publish one of: - # btn1_press / btn2_press / btn3_press / btn4_press - # - Then we auto-reset to "" so repeated presses still trigger. - # - # In HA automations, use a simple state trigger: - # - Entity: sensor.smartdisplay_action - # - To: btn1_press (etc.) - # ---------------------------------------------------------- - - platform: template - id: smartdisplay_action - name: "SmartDisplay Action" - icon: mdi:gesture-tap-button - - - platform: template - name: "Uptime (formatted)" - lambda: |- - uint32_t dur = (uint32_t) id(uptime_s).state; - - int dys = 0; - int hrs = 0; - int mnts = 0; - - if (dur >= 86400) { dys = dur / 86400; dur = dur - (dys * 86400); } - if (dur >= 3600) { hrs = dur / 3600; dur = dur - (hrs * 3600); } - if (dur >= 60) { mnts = dur / 60; dur = dur - (mnts * 60); } - - static char buffer[20]; - snprintf(buffer, sizeof(buffer), "%dd %02dh %02dm %02ds", dys, hrs, mnts, dur); - return {buffer}; - icon: mdi:clock-start - update_interval: 60s - -switch: - - platform: restart - name: "Restart" - -# Two SPI buses: -# - lcd: display -# - my_touchscreen: touch controller -spi: - - id: lcd - clk_pin: GPIO14 - mosi_pin: GPIO13 - - - id: my_touchscreen - clk_pin: GPIO25 - mosi_pin: GPIO32 - miso_pin: GPIO39 - -output: - - platform: ledc - pin: GPIO21 - id: gpio_backlight_pwm - - - platform: ledc - id: output_red - pin: GPIO4 - inverted: true - - platform: ledc - id: output_green - pin: GPIO16 - inverted: true - - platform: ledc - id: output_blue - pin: GPIO17 - inverted: true - -light: - - platform: monochromatic - output: gpio_backlight_pwm - name: "Power Display Backlight" - id: back_light - restore_mode: ALWAYS_ON - - - platform: rgb - name: LED - red: output_red - id: led - green: output_green - blue: output_blue - restore_mode: ALWAYS_OFF - -# Touchscreen configuration (XPT2046) -# IMPORTANT: -# - calibration values are device-specific -# - touchscreen.transform must match display.transform exactly -touchscreen: - platform: xpt2046 - id: ts_touch - spi_id: my_touchscreen - cs_pin: 33 - interrupt_pin: 36 - update_interval: 50ms - threshold: 400 - - calibration: - x_min: 280 - x_max: 3860 - y_min: 340 - y_max: 3860 - - transform: - swap_xy: true - mirror_x: false - mirror_y: false - -# Display configuration (ILI9341) -display: - - platform: mipi_spi - id: my_display - spi_id: lcd - model: ILI9341 - cs_pin: 15 - dc_pin: 2 - invert_colors: false - update_interval: never - auto_clear_enabled: false - - transform: - swap_xy: true - mirror_x: false - mirror_y: false - - dimensions: - width: 320 - height: 240 - -lvgl: - displays: - - my_display - touchscreens: - - touchscreen_id: ts_touch - - theme: - obj: - bg_color: 0x000000 - bg_opa: COVER - button: - radius: 30 - width: 60 - height: 60 - border_width: 0 - shadow_width: 0 - bg_color: !lambda return id(INACTIVE); - bg_opa: COVER - checked: - bg_color: 0xFFFFFF - bg_opa: COVER - - pages: - - id: lockscreen_page - bg_color: 0x000000 - bg_opa: COVER - widgets: - - label: - id: lbl_date - x: 0 - y: 30 - width: 320 - text_align: CENTER - text_font: notice_text - text_color: 0xFFFFFF - text: "" - - - label: - id: lbl_time - x: 0 - y: 55 - width: 320 - text_align: CENTER - text_font: big_text - text_color: 0xFFFFFF - text: "" - - # --------------------------------------------------- - # LVGL Button 1 - # - ALWAYS publishes a HA-friendly action: btn1_press - # - Optional direct action: light.toggle for BTN1_ENTITY - # --------------------------------------------------- - - button: - id: btn1 - x: 25 - y: 140 - checkable: true - widgets: - - label: - align: CENTER - text_font: materialdesign_icons - text_color: !lambda return id(ACTIVE); - text: "${BTN1_ICON}" - on_click: - # ALWAYS: publish action to Home Assistant (state trigger friendly) - - script.execute: - id: publish_action - action: "btn1_press" - - # OPTIONAL: direct HA service call (plug & play) - - if: - condition: - lambda: |- - return std::string("${DIRECT_ACTIONS}") == "true"; - then: - - homeassistant.action: - action: light.toggle - data: - entity_id: ${BTN1_ENTITY} - - - label: - x: 25 - y: 205 - width: 60 - text_align: CENTER - text_font: label - text_color: 0xFFFFFF - text: "${BTN1_LABEL}" - - # --------------------------------------------------- - # LVGL Button 2 - # - ALWAYS publishes a HA-friendly action: btn2_press - # --------------------------------------------------- - - button: - id: btn2 - x: 95 - y: 140 - checkable: true - widgets: - - label: - align: CENTER - text_font: materialdesign_icons - text_color: !lambda return id(ACTIVE); - text: "${BTN2_ICON}" - on_click: - - script.execute: - id: publish_action - action: "btn2_press" - - if: - condition: - lambda: |- - return std::string("${DIRECT_ACTIONS}") == "true"; - then: - - homeassistant.action: - action: light.toggle - data: - entity_id: ${BTN2_ENTITY} - - - label: - x: 95 - y: 205 - width: 60 - text_align: CENTER - text_font: label - text_color: 0xFFFFFF - text: "${BTN2_LABEL}" - - # --------------------------------------------------- - # LVGL Button 3 - # - ALWAYS publishes a HA-friendly action: btn3_press - # --------------------------------------------------- - - button: - id: btn3 - x: 165 - y: 140 - checkable: true - widgets: - - label: - align: CENTER - text_font: materialdesign_icons - text_color: !lambda return id(ACTIVE); - text: "${BTN3_ICON}" - on_click: - - script.execute: - id: publish_action - action: "btn3_press" - - if: - condition: - lambda: |- - return std::string("${DIRECT_ACTIONS}") == "true"; - then: - - homeassistant.action: - action: light.toggle - data: - entity_id: ${BTN3_ENTITY} - - - label: - x: 165 - y: 205 - width: 60 - text_align: CENTER - text_font: label - text_color: 0xFFFFFF - text: "${BTN3_LABEL}" - - # --------------------------------------------------- - # LVGL Button 4 - # - ALWAYS publishes a HA-friendly action: btn4_press - # --------------------------------------------------- - - button: - id: btn4 - x: 235 - y: 140 - checkable: true - widgets: - - label: - align: CENTER - text_font: materialdesign_icons - text_color: !lambda return id(ACTIVE); - text: "${BTN4_ICON}" - on_click: - - script.execute: - id: publish_action - action: "btn4_press" - - if: - condition: - lambda: |- - return std::string("${DIRECT_ACTIONS}") == "true"; - then: - - homeassistant.action: - action: light.toggle - data: - entity_id: ${BTN4_ENTITY} - - - label: - x: 235 - y: 205 - width: 60 - text_align: CENTER - text_font: label - text_color: 0xFFFFFF - text: "${BTN4_LABEL}" - -script: - # ---------------------------------------------------------- - # publish_action - # - # Publishes a string to the "SmartDisplay Action" text_sensor and - # then clears it again. - # ---------------------------------------------------------- - - id: publish_action - mode: queued - parameters: - action: string - then: - - text_sensor.template.publish: - id: smartdisplay_action - state: !lambda |- - return action; - - delay: 200ms - - text_sensor.template.publish: - id: smartdisplay_action - state: "" - - - id: ui_refresh - mode: queued - then: - # Sync LVGL "checked" state with the mirrored HA entity states - - lvgl.widget.update: - id: btn1 - state: - checked: !lambda return id(ha_state_btn1).state; - - - lvgl.widget.update: - id: btn2 - state: - checked: !lambda return id(ha_state_btn2).state; - - - lvgl.widget.update: - id: btn3 - state: - checked: !lambda return id(ha_state_btn3).state; - - - lvgl.widget.update: - id: btn4 - state: - checked: !lambda return id(ha_state_btn4).state; - - # Render current time (HH:MM) - - lvgl.label.update: - id: lbl_time - text: !lambda |- - auto now = id(homeassistant_time).now(); - static char buf[6]; - snprintf(buf, sizeof(buf), "%02d:%02d", now.hour, now.minute); - return std::string(buf); - - # Render localized date in German (adjust to your language if needed) - - lvgl.label.update: - id: lbl_date - text: !lambda |- - static const char* days[] = {"", "Sonntag","Montag","Dienstag","Mittwoch","Donnerstag","Freitag","Samstag"}; - static const char* months[] = {"","Januar","Februar","März","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"}; - auto now = id(homeassistant_time).now(); - static char buf[40]; - snprintf(buf, sizeof(buf), "%s, %d. %s", days[now.day_of_week], now.day_of_month, months[now.month]); - return std::string(buf); \ No newline at end of file diff --git a/calibration.example.yaml b/calibration.example.yaml index 233b609..162a96b 100644 --- a/calibration.example.yaml +++ b/calibration.example.yaml @@ -1,5 +1,5 @@ -# Identiteit: startpunt voor een NIEUW paneel, geen gemeten kalibratie. -# tools/calibrate.py schrijft een gemeten versie van dit bestand. +# Identity: starting point for a NEW panel, not a measured calibration. +# tools/calibrate.py writes a measured version of this file. substitutions: TOUCH_CAL_X_MIN: "280" TOUCH_CAL_X_MAX: "3860" diff --git a/components/smart_display/alert_overlay.h b/components/smart_display/alert_overlay.h index 4f2801c..d9c07ed 100644 --- a/components/smart_display/alert_overlay.h +++ b/components/smart_display/alert_overlay.h @@ -11,15 +11,15 @@ // tests/test_alert_overlay.cpp can check them; the profiles do the LVGL work. namespace screen_alert { constexpr const char *FALLBACK_ICON = "alert-outline"; -constexpr const char *FALLBACK_TITLE = "Melding"; -constexpr const char *FALLBACK_BUTTON = "Oké"; +constexpr const char *FALLBACK_TITLE = "Notification"; +constexpr const char *FALLBACK_BUTTON = "OK"; constexpr uint32_t DEFAULT_CARD_COLOR = 0xFFFFFF; constexpr int MAX_TIMEOUT_SECONDS = 86400; struct Alert { std::string title, subtitle, icon, button; // icon: UTF-8 glyph for the icon font uint32_t color = DEFAULT_CARD_COLOR; - int timeout_seconds = 0; // 0: stays until Oké + int timeout_seconds = 0; // 0: stays until OK bool flash = false; }; diff --git a/components/smart_display/cyd_calibration.h b/components/smart_display/cyd_calibration.h index 1510bc0..ecd1fcb 100644 --- a/components/smart_display/cyd_calibration.h +++ b/components/smart_display/cyd_calibration.h @@ -14,9 +14,9 @@ inline std::array,5> samples; inline esphome::ESPPreferenceObject preference; inline std::function apply; inline std::function isolation; -inline void prompt(const char *message = "Houd elke tik even vast") { +inline void prompt(const char *message = "Hold each tap for a moment") { lv_obj_set_pos(cross, TARGETS[point].x-12, TARGETS[point].y-12); - lv_label_set_text_fmt(instructions, "Tik op het kruisje\nPunt %u/5 - tik %u/3", point+1, sample+1); + lv_label_set_text_fmt(instructions, "Tap the crosshair\nPoint %u/5 - tap %u/3", point+1, sample+1); lv_label_set_text(status, message); } inline void begin() { @@ -54,13 +54,13 @@ inline void press() { if (active) { down_at = esphome::millis(); pressed = true; inline void release(int raw_x, int raw_y) { if (!active || !pressed) return; pressed = false; - if (esphome::millis()-down_at < 80 || raw_x < 0 || raw_x > 4095 || raw_y < 0 || raw_y > 4095) { prompt("Tik rustig, iets langer"); return; } + if (esphome::millis()-down_at < 80 || raw_x < 0 || raw_x > 4095 || raw_y < 0 || raw_y > 4095) { prompt("Tap gently, a bit longer"); return; } samples[point][sample++] = {double(raw_x), double(raw_y)}; if (sample < 3) { prompt(); return; } double xs[3], ys[3]; for (int i=0;i<3;++i) { xs[i]=samples[point][i].x; ys[i]=samples[point][i].y; } std::sort(xs,xs+3); std::sort(ys,ys+3); - if (xs[2]-xs[0]>180 || ys[2]-ys[0]>180) { sample=0; prompt("Tik 3x op hetzelfde kruisje"); return; } + if (xs[2]-xs[0]>180 || ys[2]-ys[0]>180) { sample=0; prompt("Tap the same crosshair 3 times"); return; } point++; sample=0; if (point < 5) { prompt(); return; } std::array raw; @@ -69,8 +69,8 @@ inline void release(int raw_x, int raw_y) { std::sort(xs,xs+3);std::sort(ys,ys+3);raw[n]={xs[1],ys[1]}; } Calibration next = calibration; - if (!fit(raw,next)) { point=0;prompt("Meting afgekeurd; probeer opnieuw");return; } - if (!preference.save(&next) || !esphome::global_preferences->sync()) { point=0;prompt("Opslaan mislukt; probeer opnieuw");return; } + if (!fit(raw,next)) { point=0;prompt("Measurement rejected; try again");return; } + if (!preference.save(&next) || !esphome::global_preferences->sync()) { point=0;prompt("Save failed; try again");return; } calibration=next; if (apply) apply(calibration); active=false; diff --git a/components/smart_display/cyd_ui.h b/components/smart_display/cyd_ui.h index c1f0a73..f9c7550 100644 --- a/components/smart_display/cyd_ui.h +++ b/components/smart_display/cyd_ui.h @@ -95,10 +95,10 @@ class TouchGuard { // Why the last accept() refused, for the touch log; empty after a success. std::string reason() const { switch (reject_) { - case MOVED: return "verplaatst " + std::to_string(distance_) + " px (grens " + std::to_string(move_limit_) + ")"; - case TOO_SHORT: return "te kort (" + std::to_string(duration_) + " ms, minimaal " + std::to_string(min_press_) + ")"; - case USED: return "al verwerkt in dit contact"; - case BOUNCE: return "dezelfde knop binnen de dendertijd"; + case MOVED: return "moved " + std::to_string(distance_) + " px (limit " + std::to_string(move_limit_) + ")"; + case TOO_SHORT: return "too short (" + std::to_string(duration_) + " ms, minimum " + std::to_string(min_press_) + ")"; + case USED: return "already handled in this contact"; + case BOUNCE: return "same button within the debounce window"; default: return ""; } } diff --git a/components/smart_display/header_bar.h b/components/smart_display/header_bar.h index 7589b32..5d4f4a5 100644 --- a/components/smart_display/header_bar.h +++ b/components/smart_display/header_bar.h @@ -6,7 +6,7 @@ namespace header_bar { // The top bar right of the screen name (firmware 0.2.32+). ESP Screen Manager decides what shows -// and writes an entity's text; the screen draws it, ticks its clocks and counts "5 min geleden" +// and writes an entity's text; the screen draws it, ticks its clocks and counts "5 min ago" // itself. Everything here is free of LVGL, so tests/test_header_bar.cpp covers it on a PC. constexpr size_t MAX_ITEMS = 6; constexpr size_t TEXT_BYTES = 48; @@ -64,32 +64,32 @@ inline uint32_t next_codepoint(const std::string &s, size_t &i) { return cp; } -// Relative time in the editor's words (app.js agoText): "Zojuist", "5 min geleden", "Gisteren", -// "Over 2 uur". `now` 0 means the clock is not set yet. +// Relative time in the editor's words (app.js agoText): "Just now", "5 min ago", "Yesterday", +// "In 2 hours". `now` 0 means the clock is not set yet. inline std::string ago_text(int64_t then, int64_t now) { if (now <= 0 || then <= 0) return "—"; int64_t seconds = now - then, span = seconds < 0 ? -seconds : seconds; auto n = [&](int64_t unit) { return std::to_string(span / unit); }; if (seconds < 0) { - if (span < 3600) return "Over " + std::to_string(std::max(1, span / 60)) + " min"; - if (span < 86400) return "Over " + n(3600) + " uur"; - if (span < 172800) return "Morgen"; - return "Over " + n(86400) + " dagen"; + if (span < 3600) return "In " + std::to_string(std::max(1, span / 60)) + " min"; + if (span < 86400) return span / 3600 == 1 ? "In 1 hour" : "In " + n(3600) + " hours"; + if (span < 172800) return "Tomorrow"; + return "In " + n(86400) + " days"; } - if (span < 60) return "Zojuist"; - if (span < 3600) return n(60) + " min geleden"; - if (span < 86400) return n(3600) + " uur geleden"; - if (span < 172800) return "Gisteren"; - if (span < 604800) return n(86400) + " dagen geleden"; - if (span < 2592000) return span / 604800 == 1 ? "1 week geleden" : n(604800) + " weken geleden"; - if (span < 31536000) return span / 2592000 == 1 ? "1 maand geleden" : n(2592000) + " maanden geleden"; - return n(31536000) + " jaar geleden"; + if (span < 60) return "Just now"; + if (span < 3600) return n(60) + " min ago"; + if (span < 86400) return span / 3600 == 1 ? "1 hour ago" : n(3600) + " hours ago"; + if (span < 172800) return "Yesterday"; + if (span < 604800) return n(86400) + " days ago"; + if (span < 2592000) return span / 604800 == 1 ? "1 week ago" : n(604800) + " weeks ago"; + if (span < 31536000) return span / 2592000 == 1 ? "1 month ago" : n(2592000) + " months ago"; + return span / 31536000 == 1 ? "1 year ago" : n(31536000) + " years ago"; } -// "ma 14 sep"; day_of_week 1 is Sunday, as ESPHome counts. +// "Mo 14 Sep"; day_of_week 1 is Sunday, as ESPHome counts. inline std::string date_text(int day_of_week, int day_of_month, int month) { - static const char *days[] = {"zo", "ma", "di", "wo", "do", "vr", "za"}; - static const char *months[] = {"jan", "feb", "mrt", "apr", "mei", "jun", "jul", "aug", "sep", "okt", "nov", "dec"}; + static const char *days[] = {"Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"}; + static const char *months[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; if (day_of_week < 1 || day_of_week > 7 || month < 1 || month > 12) return "—"; return std::string(days[day_of_week - 1]) + " " + std::to_string(day_of_month) + " " + months[month - 1]; } diff --git a/components/smart_display/light_controls.h b/components/smart_display/light_controls.h index 9a57cc8..137629e 100644 --- a/components/smart_display/light_controls.h +++ b/components/smart_display/light_controls.h @@ -108,7 +108,7 @@ inline void setup(lv_obj_t *parent, const lv_font_t *font, int width, int height int margin = large ? 24 : 16, w = width - 2 * margin; int track_x = 14, track_y = large ? 43 : 28, track_h = large ? 28 : 14; int track_w = w - 28; - const char *names[] = {"Kleur", "Wittemperatuur", "Helderheid"}; + const char *names[] = {"Color", "Color temperature", "Brightness"}; const uint32_t rainbow[] = {0xFF0000, 0xFFFF00, 0x00FF00, 0x00FFFF, 0x0000FF, 0xFF00FF, 0xFF0000}; for (unsigned i = 0; i < 3; ++i) { auto &row = rows[i]; row.index = i; @@ -170,7 +170,7 @@ inline void open(const std::string &entity, bool color, bool temperature, int br if (i == 1 && active->temperature_ready()) lv_slider_set_range(row.slider, active->minimum, active->maximum); lv_slider_set_value(row.slider, i == 0 ? active->hue : i == 1 ? active->kelvin : clamp(brightness, 1, 100), LV_ANIM_OFF); preview(row); row.dirty = false; - if (i == 1 && !active->temperature_ready()) lv_label_set_text(row.value, "Even wachten"); + if (i == 1 && !active->temperature_ready()) lv_label_set_text(row.value, "Waiting..."); } } inline bool self_test() { diff --git a/components/smart_display/runtime_model.h b/components/smart_display/runtime_model.h index 7e31929..2271542 100644 --- a/components/smart_display/runtime_model.h +++ b/components/smart_display/runtime_model.h @@ -67,7 +67,7 @@ struct Tile { uint32_t timer_end = 0; float battery = NAN, volume = NAN; uint32_t supported = 0, background = 0; - bool transparent = false; // "Achtergrond: geen": card fill and border hidden, contents unchanged. + bool transparent = false; // "Background: none": card fill and border hidden, contents unchanged. std::string icon; // UTF-8 glyph of a chosen icon the icon fonts contain; empty keeps the domain icon. std::string revision, pending_revision; uint32_t pending_since = 0; @@ -113,7 +113,7 @@ struct Model { // Pages the manager wants shown even when the last ones are still empty (0.2.26+). uint8_t pages = 1; size_t count = 0; - std::string title = "Kies tegels in HA"; + std::string title = "Choose tiles in HA"; bool configured = false; bool set_layout(const std::vector &entities, const std::string &name, bool &changed) { bool moved = false; @@ -140,7 +140,7 @@ struct Model { moved = !changed && (explicit_slots != !positions.empty()); for (size_t i = 0; i < positions.size() && !changed; ++i) if (slots[i] != positions[i]) moved = true; // A title-only update must not interrupt an open control card. - title = name.empty() ? "Thuis" : name; + title = name.empty() ? "Home" : name; if (changed) { for (auto &tile : tiles) tile = Tile{}; count = entities.size(); diff --git a/components/smart_display/runtime_tiles.h b/components/smart_display/runtime_tiles.h index 05207e0..969802a 100644 --- a/components/smart_display/runtime_tiles.h +++ b/components/smart_display/runtime_tiles.h @@ -123,7 +123,7 @@ inline bool fresh() { return model.ready() && ha_connected() && feed_alive(); } // instead of guessed: moved too far, too short, already used by this contact, or bounce. inline bool allowed(uint32_t now, int tile, const std::string &what) { if (cyd::touch_guard.accept(now, tile)) return true; - ESP_LOGI("touch", "tik op %s genegeerd: %s", what.c_str(), cyd::touch_guard.reason().c_str()); + ESP_LOGI("touch", "tap on %s ignored: %s", what.c_str(), cyd::touch_guard.reason().c_str()); return false; } inline float number(JsonVariant value, float fallback = NAN) { @@ -147,18 +147,18 @@ inline std::string list(JsonVariant value) { return out.size() <= 512 ? out : ""; } inline std::string receive(const std::string &payload) { - if (!enabled) return "Gebruik het Easy Setup-profiel"; - if (payload.size() > 4096) return "Fout: bericht te groot"; - std::string result = "Fout: ongeldig bericht"; + if (!enabled) return "Use the Easy Setup profile"; + if (payload.size() > 4096) return "Error: message too large"; + std::string result = "Error: invalid message"; esphome::json::parse_json(payload, [&](JsonObject root) -> bool { - if (root["v"].as() != 1) { result = "Fout: protocolversie"; return false; } + if (root["v"].as() != 1) { result = "Error: protocol version"; return false; } auto op = string(root["op"]); if (op == "layout") { if (!root["entities"].is() || !root["title"].is()) return false; auto settings = screen_settings::current; if (!root["settings"].isNull() && (!root["settings"].is() || !parse_settings(root["settings"].as(), settings))) { - result = "Fout: scherminstellingen"; return false; + result = "Error: screen settings"; return false; } std::vector entities; for (JsonVariant entity : root["entities"].as()) { @@ -198,14 +198,14 @@ inline std::string receive(const std::string &payload) { } if(rotation_changed && settings_changed)settings_changed(); if (changed) { active_index = -1; for (auto &w : widgets) w.cached_active = -1; if (dismiss) dismiss(); } - if (moved) ESP_LOGI("runtime", "tegels verplaatst: pagina's opnieuw ingedeeld"); + if (moved) ESP_LOGI("runtime", "tiles moved: pages rearranged"); if (root["keepalive"].is()) keepalive_seconds = root["keepalive"].as(); layout_rev = string(root["rev"], 16); last_received = esphome::millis(); if (layout_changed) layout_changed(); if (refresh) refresh(); // A repeat of the same layout keeps the inbox state as it is: no new recorder row. - result = changed || moved || !was_configured ? "Indeling ontvangen" : model.ready() ? "Gesynchroniseerd" : "Tegels laden"; + result = changed || moved || !was_configured ? "Layout received" : model.ready() ? "Synced" : "Loading tiles"; return true; } if (op == "ping") { @@ -215,8 +215,8 @@ inline std::string receive(const std::string &payload) { root["keepalive"].as()<5 || root["keepalive"].as()>3600))return false; if (root["keepalive"].is()) keepalive_seconds = root["keepalive"].as(); last_received = esphome::millis(); - if (!model.configured || layout_rev != string(root["rev"], 16)) { result = "Indeling opnieuw nodig"; return true; } - result = model.ready() ? "Gesynchroniseerd" : "Tegels laden"; + if (!model.configured || layout_rev != string(root["rev"], 16)) { result = "Resend needed"; return true; } + result = model.ready() ? "Synced" : "Loading tiles"; return true; } if (op == "header") { @@ -243,13 +243,13 @@ inline std::string receive(const std::string &payload) { last_received = esphome::millis(); if (!same && refresh) refresh(); // The same status as a tile state, so a changing value never flips the inbox entity. - result = model.ready() ? "Gesynchroniseerd" : "Tegels laden"; + result = model.ready() ? "Synced" : "Loading tiles"; return true; } if (op != "state" || !root["i"].is() || !root["a"].is()) return false; unsigned index = root["i"].as(); std::string entity = string(root["entity"], 120); - if (!model.accepts(index, entity)) { result = "Fout: verouderde tegel"; return false; } + if (!model.accepts(index, entity)) { result = "Error: outdated tile"; return false; } Tile &tile = model.tiles[index]; auto a = root["a"].as(); // ArduinoJson clears its destination string: serialize attributes first, then add state. @@ -354,7 +354,7 @@ inline std::string receive(const std::string &payload) { if (refresh) refresh(); if (active_index == static_cast(index) && detail_update) detail_update(tile); refresh_detail(index); - result = model.ready() ? "Gesynchroniseerd" : "Tegels laden"; + result = model.ready() ? "Synced" : "Loading tiles"; return true; }); return result; @@ -399,19 +399,19 @@ inline lv_obj_t *detail_label(lv_obj_t *parent,const std::string &text,int x,int lv_label_set_long_mode(label,LV_LABEL_LONG_DOT);lv_obj_set_height(label,lv_font_get_line_height(detail_font));return label; } inline std::string detail_state(const Tile &t){ - if(t.domain()=="person")return t.state=="home"?"Thuis":t.state=="not_home"?"Niet thuis":t.state; - if(t.domain()=="sun")return t.state=="above_horizon"?"Boven de horizon":"Onder de horizon"; + if(t.domain()=="person")return t.state=="home"?"Home":t.state=="not_home"?"Away":t.state; + if(t.domain()=="sun")return t.state=="above_horizon"?"Above the horizon":"Below the horizon"; if(t.domain()=="timer")return timer_text(t); - if(t.domain()=="script"||t.domain()=="scene"||t.domain()=="button"||t.domain()=="input_button")return t.state=="on"?"Bezig...":last_run_text(t.last_run); - if(t.state=="on")return "Aan"; - if(t.state=="off")return "Uit"; - if(t.state=="docked")return "In dock"; - if(t.state=="cleaning")return "Bezig met schoonmaken"; - if(t.state=="paused")return "Gepauzeerd"; - if(t.state=="returning")return "Onderweg naar dock"; - if(t.state=="idle")return "Klaar"; - if(t.state=="error")return "Controleer de robot in HA"; - if(!t.available())return "Niet beschikbaar"; + if(t.domain()=="script"||t.domain()=="scene"||t.domain()=="button"||t.domain()=="input_button")return t.state=="on"?"Running...":last_run_text(t.last_run); + if(t.state=="on")return "On"; + if(t.state=="off")return "Off"; + if(t.state=="docked")return "Docked"; + if(t.state=="cleaning")return "Cleaning"; + if(t.state=="paused")return "Paused"; + if(t.state=="returning")return "Returning to dock"; + if(t.state=="idle")return "Idle"; + if(t.state=="error")return "Check the robot in HA"; + if(!t.available())return "Unavailable"; return t.state; } inline void hide_detail(){if(detail_root)lv_obj_add_flag(detail_root,LV_OBJ_FLAG_HIDDEN);} @@ -457,7 +457,7 @@ inline lv_obj_t *detail_button(const char *text,int x,int y,int width,int height auto *label=detail_label(button,text,6,0,width-12);lv_obj_center(label);lv_obj_set_style_text_align(label,LV_TEXT_ALIGN_CENTER,0);if(command==0)lv_obj_set_style_text_color(label,lv_color_hex(0xFFFFFF),0);lv_obj_remove_flag(label,LV_OBJ_FLAG_CLICKABLE); lv_obj_add_event_cb(button,[](lv_event_t *e){ int cmd=(intptr_t)lv_event_get_user_data(e);if(cmd==-1){hide_detail();return;} - if(!fresh()||detail_index>=model.count || !allowed(esphome::millis(),300+cmd,"kaartknop "+model.tiles[detail_index].entity))return; + if(!fresh()||detail_index>=model.count || !allowed(esphome::millis(),300+cmd,"card button "+model.tiles[detail_index].entity))return; auto &t=model.tiles[detail_index];if(!t.available()||t.loading(esphome::millis()))return; if(cmd<4){const char *services[]={"vacuum.start","vacuum.pause","vacuum.return_to_base","vacuum.locate"};action(services[cmd],t.entity);} if(cmd>=10 && cmd<14 && cmd-10<(int)t.fan_speed_count)action("vacuum.set_fan_speed",t.entity,"fan_speed",t.fan_speeds[cmd-10]); @@ -533,9 +533,9 @@ inline void render_weather_detail(const Tile &t,bool large,int width,int height, detail_text(now,degrees(t.current),temp_x,cy+(hero-big_h)/2,temp_w,big,LV_TEXT_ALIGN_LEFT,ink); int text_x=temp_x+temp_w+(large?4:2),text_w=width-2*pad-card_pad-text_x; int lines_h=text_h+small_h+(large?2:0); - detail_text(now,t.available()?weather_text(t.state):"Niet beschikbaar",text_x,cy+(hero-lines_h)/2,text_w,detail_font,LV_TEXT_ALIGN_LEFT,ink); + detail_text(now,t.available()?weather_text(t.state):"Unavailable",text_x,cy+(hero-lines_h)/2,text_w,detail_font,LV_TEXT_ALIGN_LEFT,ink); std::string details; - if(std::isfinite(t.feels)){snprintf(b,sizeof(b),"Voelt als %.0f°",t.feels);details=b;} + if(std::isfinite(t.feels)){snprintf(b,sizeof(b),"Feels like %.0f°",t.feels);details=b;} if(std::isfinite(t.humidity)){snprintf(b,sizeof(b),"%d%%",(int)std::lround(t.humidity));details+=(details.empty()?"":" · ")+std::string(b);} if(std::isfinite(t.wind)){snprintf(b,sizeof(b),"%.0f %s",t.wind,t.wind_unit.empty()?"km/h":t.wind_unit.c_str());details+=(details.empty()?"":" · ")+std::string(b);} detail_text(now,details,text_x,cy+(hero-lines_h)/2+text_h+(large?2:0),text_w,small,LV_TEXT_ALIGN_LEFT,muted); @@ -552,8 +552,8 @@ inline void render_weather_detail(const Tile &t,bool large,int width,int height, } y+=card_a_h+(large?12:6); // Coming days: a heading and a card with one row per day. - if(!t.forecast.size()){detail_text(detail_root,"Geen dagvoorspelling van Home Assistant",pad,y,width-2*pad,small,LV_TEXT_ALIGN_LEFT,muted);return;} - if(large){detail_text(detail_root,"Komende dagen",pad+4,y,width-2*pad,detail_font,LV_TEXT_ALIGN_LEFT,muted);y+=text_h+8;} + if(!t.forecast.size()){detail_text(detail_root,"No daily forecast from Home Assistant",pad,y,width-2*pad,small,LV_TEXT_ALIGN_LEFT,muted);return;} + if(large){detail_text(detail_root,"Coming days",pad+4,y,width-2*pad,detail_font,LV_TEXT_ALIGN_LEFT,muted);y+=text_h+8;} int card_b_h=height-y-(large?10:4); auto *days=detail_card(pad,y,width-2*pad,card_b_h); int row_pad=large?8:4,row=(card_b_h-2*row_pad)/(int)t.forecast.size(); @@ -588,7 +588,7 @@ inline void show_detail(unsigned index){ detail_status=detail_label(detail_root,state+(t.unit.empty()?"":" "+t.unit),pad,large?60:35,width-2*pad); auto d=t.domain(); if(t.is_switch()){ - detail_label(detail_root,"Tik om te schakelen",pad,top,width-2*pad); + detail_label(detail_root,"Tap to toggle",pad,top,width-2*pad); detail_switch=lv_switch_create(detail_root); lv_obj_set_size(detail_switch,large?240:140,large?112:64); lv_obj_set_pos(detail_switch,(width-(large?240:140))/2,top+(large?65:30)); @@ -623,31 +623,31 @@ inline void show_detail(unsigned index){ lv_obj_set_style_border_width(robot,2,0);lv_obj_set_style_border_color(robot,lv_color_hex(0xCEDDE6),0); shape(robot,27,10,30,30,0xE3EBEF,15);shape(robot,35,18,14,14,0xA8BCC8,7); shape(robot,29,59,26,5,0x00A6ED,3); - detail_label(hero,t.state=="cleaning"?"Aan het werk":t.state=="returning"?"Even opladen":t.state=="paused"?"Even pauze":"Klaar voor je huis",154,24,260); + detail_label(hero,t.state=="cleaning"?"Working":t.state=="returning"?"Charging":t.state=="paused"?"Paused":"Ready for your home",154,24,260); auto *badge=shape(hero,154,59,240,32,muted,16); - auto *status=detail_label(badge,t.awaiting_action(esphome::millis())?"Opdracht verstuurd...":state,12,5,218); + auto *status=detail_label(badge,t.awaiting_action(esphome::millis())?"Command sent...":state,12,5,218); detail_badge_status=status; lv_obj_set_style_text_color(status,lv_color_hex(0x087BA8),0); - detail_label(hero,std::isfinite(t.battery)?"Batterij "+std::to_string((int)t.battery)+"%":"Verbonden via Home Assistant",154,107,260); - detail_button(t.state=="cleaning"?"Pauzeer schoonmaken":"Start schoonmaken",pad,260,width-2*pad,58,t.state=="cleaning"?1:0); - detail_button("Terug naar dock",pad,330,cw,48,2); - detail_button("Vind mijn robot",pad+cw+gap,330,cw,48,3); - detail_label(detail_root,"Zuigkracht",pad,394,width-2*pad); + detail_label(hero,std::isfinite(t.battery)?"Battery "+std::to_string((int)t.battery)+"%":"Connected via Home Assistant",154,107,260); + detail_button(t.state=="cleaning"?"Pause cleaning":"Start cleaning",pad,260,width-2*pad,58,t.state=="cleaning"?1:0); + detail_button("Return to dock",pad,330,cw,48,2); + detail_button("Find my robot",pad+cw+gap,330,cw,48,3); + detail_label(detail_root,"Suction power",pad,394,width-2*pad); top=424; }else{ auto *robot=shape(detail_root,pad,64,46,46,muted,23); shape(robot,16,8,14,14,0xA8BCC8,7);shape(robot,15,32,16,3,0x00A6ED,2); - detail_label(detail_root,std::isfinite(t.battery)?"Batterij "+std::to_string((int)t.battery)+"%":"Robotstofzuiger",pad+58,66,width-2*pad-58); - detail_label(detail_root,"Kies een actie",pad+58,87,width-2*pad-58); - detail_button(t.state=="cleaning"?"Pauzeren":"Schoonmaken",pad,120,cw,38,t.state=="cleaning"?1:0); - detail_button("Naar dock",pad+cw+gap,120,cw,38,2); - detail_label(detail_root,"Zuigkracht",pad,168,width-2*pad);top=194; + detail_label(detail_root,std::isfinite(t.battery)?"Battery "+std::to_string((int)t.battery)+"%":"Robot vacuum",pad+58,66,width-2*pad-58); + detail_label(detail_root,"Choose an action",pad+58,87,width-2*pad-58); + detail_button(t.state=="cleaning"?"Pause":"Clean",pad,120,cw,38,t.state=="cleaning"?1:0); + detail_button("To dock",pad+cw+gap,120,cw,38,2); + detail_label(detail_root,"Suction power",pad,168,width-2*pad);top=194; } - if(!t.fan_speed_count)detail_label(detail_root,"Automatische zuigkracht",pad,top,width-2*pad); + if(!t.fan_speed_count)detail_label(detail_root,"Automatic suction power",pad,top,width-2*pad); int count=std::max(1,(int)t.fan_speed_count),sw=(width-2*pad-gap*(count-1))/count; for(unsigned i=0;iminimum?8+(chart_h-8)*(t.history[i]-minimum)/(maximum-minimum):chart_h/2; auto *bar=lv_obj_create(detail_root);lv_obj_remove_style_all(bar);lv_obj_set_pos(bar,pad+i*bar_w,chart_y+chart_h-h);lv_obj_set_size(bar,std::max(2,bar_w-2),h);lv_obj_set_style_bg_color(bar,lv_color_hex(0x16A5E6),0);lv_obj_set_style_bg_opa(bar,LV_OPA_COVER,0);} - detail_label(detail_root,std::to_string(t.history_hours)+" uur geleden",pad,height-24,(width-2*pad)/2); - auto *now=detail_label(detail_root,"Nu",width/2,height-24,width/2-pad);lv_obj_set_style_text_align(now,LV_TEXT_ALIGN_RIGHT,0); + detail_label(detail_root,std::to_string(t.history_hours)+" hours ago",pad,height-24,(width-2*pad)/2); + auto *now=detail_label(detail_root,"Now",width/2,height-24,width/2-pad);lv_obj_set_style_text_align(now,LV_TEXT_ALIGN_RIGHT,0); }else if(d=="select"||d=="input_select"){ for(unsigned i=0;i now && now ? t.timer_end - now : 0); } - if (t.state == "paused") return "Pauze " + countdown(duration_seconds(t.remaining)); - return t.duration.empty() ? "Uit" : countdown(duration_seconds(t.duration)); + if (t.state == "paused") return "Paused " + countdown(duration_seconds(t.remaining)); + return t.duration.empty() ? "Off" : countdown(duration_seconds(t.duration)); } inline std::string weekday_text(const esphome::ESPTime &now) { - static const char *days[] = {"zondag", "maandag", "dinsdag", "woensdag", "donderdag", "vrijdag", "zaterdag"}; + static const char *days[] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}; return now.is_valid() && now.day_of_week >= 1 && now.day_of_week <= 7 ? days[now.day_of_week - 1] : ""; } inline std::string month_short(const esphome::ESPTime &now) { - static const char *months[] = {"jan", "feb", "mrt", "apr", "mei", "jun", "jul", "aug", "sep", "okt", "nov", "dec"}; + static const char *months[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; return now.is_valid() && now.month >= 1 && now.month <= 12 ? months[now.month - 1] : ""; } inline std::string date_text(const esphome::ESPTime &now) { - static const char *months[] = {"januari", "februari", "maart", "april", "mei", "juni", "juli", "augustus", "september", "oktober", "november", "december"}; + static const char *months[] = {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}; if (!now.is_valid() || now.day_of_week < 1 || now.day_of_week > 7 || now.month < 1 || now.month > 12) return ""; return weekday_text(now) + " " + std::to_string(now.day_of_month) + " " + months[now.month - 1]; } @@ -803,7 +803,7 @@ inline void event(lv_event_t *event) { if(d=="media_player" && tile.tap=="toggle" && code==LV_EVENT_SHORT_CLICKED){action("media_player.toggle",tile.entity);return;} if(d=="climate" && tile.tap=="toggle" && code==LV_EVENT_SHORT_CLICKED){action("climate.toggle",tile.entity);return;} if(tile.builtin())return; - // A short tap runs or pauses the kitchen timer; holding opens the card with a cancel button. + // A short tap runs or pauses the timer; holding opens the card with a cancel button. if(d=="timer" && !open){action(tile.state=="active"?"timer.pause":"timer.start",tile.entity);return;} if(d=="sensor" || d=="binary_sensor" || d=="weather" || d=="number" || d=="input_number" || d=="select" || d=="input_select" || d=="media_player" || d=="vacuum" || d=="sun" || d=="person" || d=="timer") { tile.begin(esphome::millis(),true); active_index=w.index; show_detail(w.index); return; } if (open) { @@ -1080,9 +1080,9 @@ inline void render_sunpath(Widgets &w,const Tile &t,bool large,int width,int hei const lv_font_t *title_font=lv_obj_get_style_text_font(w.title,LV_PART_MAIN); int title_h=lv_font_get_line_height(title_font),text_h=lv_font_get_line_height(w.value_font); int horizon=height-text_h-(large?4:2),top=title_h+(large?4:2),x0=large?14:8,x1=width-x0; - part_label(w,0,title_font,0,0,width,LV_TEXT_ALIGN_LEFT,t.name.empty()?"Zon":t.name); - part_label(w,1,w.value_font,0,horizon+(large?3:1),width/2,LV_TEXT_ALIGN_LEFT,"op "+t.sunrise); - part_label(w,2,w.value_font,width/2,horizon+(large?3:1),width/2,LV_TEXT_ALIGN_RIGHT,"onder "+t.sunset); + part_label(w,0,title_font,0,0,width,LV_TEXT_ALIGN_LEFT,t.name.empty()?"Sun":t.name); + part_label(w,1,w.value_font,0,horizon+(large?3:1),width/2,LV_TEXT_ALIGN_LEFT,"rise "+t.sunrise); + part_label(w,2,w.value_font,width/2,horizon+(large?3:1),width/2,LV_TEXT_ALIGN_RIGHT,"set "+t.sunset); auto now=now_time?now_time():esphome::ESPTime{}; int rise=minutes_of(t.sunrise),set=minutes_of(t.sunset),minute=now.is_valid()?now.hour*60+now.minute:-1; bool day=t.state=="above_horizon";float fraction=0.5f; @@ -1296,8 +1296,8 @@ inline void control_event(lv_event_t *e) { bool step=command==tile_controls::STEP_DOWN || command==tile_controls::STEP_UP; bool held=lv_event_get_code(e)==LV_EVENT_LONG_PRESSED_REPEAT; if(held){ if(!step || now-t.edit_since<300)return; } // three steps a second while holding - else if(step){ if(!cyd::touch_guard.accept_repeat(now,400+slot*16+n)){ESP_LOGI("touch","tik op bediening %u genegeerd: %s",(unsigned)slot,cyd::touch_guard.reason().c_str());return;} } - else if(!allowed(now,400+slot*16+n,"bediening "+std::to_string(slot)))return; + else if(step){ if(!cyd::touch_guard.accept_repeat(now,400+slot*16+n)){ESP_LOGI("touch","tap on control %u ignored: %s",(unsigned)slot,cyd::touch_guard.reason().c_str());return;} } + else if(!allowed(now,400+slot*16+n,"control "+std::to_string(slot)))return; if(!t.available())return; if(step){ // Local at once, tap after tap; tick() sends the last value after a short pause. @@ -1337,7 +1337,7 @@ inline void set_busy(Widgets &w,bool busy,bool large){ inline void render(lv_obj_t *room) { if (!enabled) return; room_label=room; - label(room, !model.configured ? "Kies tegels in HA" : !model.ready() ? "Tegels laden..." : !ha_connected() ? "HA niet verbonden" : !feed_alive() ? "ESP Screens niet actief" : model.title); + label(room, !model.configured ? "Choose tiles in HA" : !model.ready() ? "Loading tiles..." : !ha_connected() ? "HA not connected" : !feed_alive() ? "ESP Screens not active" : model.title); render_header(); for (size_t slot = 0; slot < 6; ++slot) { auto &w=widgets[slot]; @@ -1349,17 +1349,17 @@ inline void render(lv_obj_t *room) { bool watch=t.display=="watch"; std::string unit=watch?t.unit:""; std::string value = t.state; - if (!fresh() || !t.available()) value = "Niet beschikbaar"; + if (!fresh() || !t.available()) value = "Unavailable"; else if (d == "light" && t.state == "on" && std::isfinite(t.brightness)) value = std::to_string(static_cast(std::lround(std::clamp(t.brightness, 0.0f, 255.0f) * 100 / 255))) + " %"; else if (d == "climate" && std::isfinite(t.target)) { char b[32]; snprintf(b, sizeof(b), "%.1f°", t.target); value = b; } - else if (d == "person") value = t.state=="home"?"Thuis":t.state=="not_home"?"Weg":t.state; - else if (d == "sun") value = !t.sunrise.empty() && !t.sunset.empty() ? t.sunrise+" - "+t.sunset : t.state=="above_horizon"?"Boven de horizon":"Onder de horizon"; + else if (d == "person") value = t.state=="home"?"Home":t.state=="not_home"?"Away":t.state; + else if (d == "sun") value = !t.sunrise.empty() && !t.sunset.empty() ? t.sunrise+" - "+t.sunset : t.state=="above_horizon"?"Above the horizon":"Below the horizon"; else if (d == "timer") value = timer_text(t); - else if (d == "script" || d == "scene" || d == "button" || d == "input_button") value = t.state == "on" ? "Bezig..." : last_run_text(t.last_run); - else if (value == "on") value = "Aan"; - else if (value == "off") value = "Uit"; - else if (value == "cleaning") value = "Bezig"; - else if (value == "docked") value = "In dock"; + else if (d == "script" || d == "scene" || d == "button" || d == "input_button") value = t.state == "on" ? "Running..." : last_run_text(t.last_run); + else if (value == "on") value = "On"; + else if (value == "off") value = "Off"; + else if (value == "cleaning") value = "Cleaning"; + else if (value == "docked") value = "Docked"; else if (!t.unit.empty() && !watch) value += " " + t.unit; bool pending=t.loading(esphome::millis()); if(d=="weather" && std::isfinite(t.current)) {char b[32];snprintf(b,sizeof(b),"%.1f %s",t.current,t.unit.c_str());value=b;if(watch){snprintf(b,sizeof(b),"%.1f",t.current);value=b;}} @@ -1451,7 +1451,7 @@ inline void render(lv_obj_t *room) { lv_obj_set_style_bg_color(w.slider,lv_color_mix(color,lv_color_hex(0xFFFFFF),30),LV_PART_MAIN); lv_obj_set_style_bg_color(w.tile, lv_color_hex(t.background ? t.background : 0xFFFFFF), 0); lv_obj_set_style_border_width(w.tile, 1, 0); - // "Achtergrond: geen" hides only the card; geometry and padding stay identical, + // "Background: none" hides only the card; geometry and padding stay identical, // and the pressed flash still shows because it lives on the PRESSED state. lv_obj_set_style_bg_opa(w.tile, t.transparent ? LV_OPA_TRANSP : LV_OPA_COVER, 0); lv_obj_set_style_border_opa(w.tile, t.transparent ? LV_OPA_TRANSP : LV_OPA_COVER, 0); @@ -1821,7 +1821,7 @@ inline void tick() { if(detail_root && !lv_obj_has_flag(detail_root,LV_OBJ_FLAG_HIDDEN) && detail_indexpublish_state("Klaar voor tegelconfiguratie"); } + void setup() override { this->publish_state("Ready for tile configuration"); } protected: runtime_tiles::Chunks chunks_; void control(const std::string &value) override { - if (!chunks_.accept(value, millis())) { this->publish_state("Fout: onvolledig bericht"); return; } + if (!chunks_.accept(value, millis())) { this->publish_state("Error: incomplete message"); return; } if (!chunks_.complete) return; std::string decoded(chunks_.data.size() * 3 / 4 + 1, '\0'); size_t length = base64_decode(chunks_.data, reinterpret_cast(decoded.data()), decoded.size()); chunks_.reset(); - if (length == 0 || length > 4096) { this->publish_state("Fout: ongeldige codering"); return; } + if (length == 0 || length > 4096) { this->publish_state("Error: invalid encoding"); return; } decoded.resize(length); this->publish_state(runtime_tiles::receive(decoded)); } diff --git a/components/smart_display/tile_controls.h b/components/smart_display/tile_controls.h index d8d5674..e2690c0 100644 --- a/components/smart_display/tile_controls.h +++ b/components/smart_display/tile_controls.h @@ -85,41 +85,41 @@ inline std::string format_value(float value, float step, const char *suffix) { char b[24]; snprintf(b, sizeof(b), step >= 1 ? "%.0f%s" : "%.1f%s", value, suffix); return b; } inline const char *climate_mode_text(const std::string &mode) { - if (mode == "off") return "Uit"; - if (mode == "heat") return "Verwarmen"; - if (mode == "cool") return "Koelen"; - if (mode == "heat_cool") return "Verwarmen/koelen"; - if (mode == "auto") return "Automatisch"; - if (mode == "dry") return "Drogen"; - if (mode == "fan_only") return "Ventileren"; + if (mode == "off") return "Off"; + if (mode == "heat") return "Heat"; + if (mode == "cool") return "Cool"; + if (mode == "heat_cool") return "Heat/Cool"; + if (mode == "auto") return "Auto"; + if (mode == "dry") return "Dry"; + if (mode == "fan_only") return "Fan only"; return mode.c_str(); } inline const char *climate_action_text(const std::string &action) { - if (action == "heating") return "Verwarmt"; - if (action == "cooling") return "Koelt"; - if (action == "idle") return "Inactief"; - if (action == "off") return "Uit"; - if (action == "drying") return "Droogt"; - if (action == "fan") return "Ventileert"; - if (action == "preheating") return "Voorverwarmen"; - if (action == "defrosting") return "Ontdooit"; + if (action == "heating") return "Heating"; + if (action == "cooling") return "Cooling"; + if (action == "idle") return "Idle"; + if (action == "off") return "Off"; + if (action == "drying") return "Drying"; + if (action == "fan") return "Fan"; + if (action == "preheating") return "Preheating"; + if (action == "defrosting") return "Defrosting"; return ""; } inline const char *cover_state_text(const std::string &state) { if (state == "open") return "Open"; - if (state == "closed") return "Dicht"; - if (state == "opening") return "Gaat open"; - if (state == "closing") return "Gaat dicht"; + if (state == "closed") return "Closed"; + if (state == "opening") return "Opening"; + if (state == "closing") return "Closing"; return state.c_str(); } inline const char *media_state_text(const std::string &state) { - if (state == "playing") return "Speelt"; - if (state == "paused") return "Gepauzeerd"; - if (state == "idle") return "Inactief"; - if (state == "standby") return "Stand-by"; - if (state == "buffering") return "Laden"; - if (state == "on") return "Aan"; - if (state == "off") return "Uit"; + if (state == "playing") return "Playing"; + if (state == "paused") return "Paused"; + if (state == "idle") return "Idle"; + if (state == "standby") return "Standby"; + if (state == "buffering") return "Loading"; + if (state == "on") return "On"; + if (state == "off") return "Off"; return state.c_str(); } // Status line beside a control panel: what Home Assistant shows under the name. @@ -189,9 +189,9 @@ inline std::string neighbour_option(const Tile &t, int direction) { return t.options[next]; } inline const char *run_label(const std::string &domain) { - if (domain == "scene") return "Activeren"; - if (domain == "script") return "Uitvoeren"; - return "Indrukken"; + if (domain == "scene") return "Activate"; + if (domain == "script") return "Run"; + return "Press"; } // The Home Assistant action behind a key. STEP_* are handled locally (debounced) and return nothing. inline Action key_action(const Tile &t, int command, const std::string &arg = "") { diff --git a/components/smart_display/tile_palette.h b/components/smart_display/tile_palette.h index 6ef6627..4fc7a87 100644 --- a/components/smart_display/tile_palette.h +++ b/components/smart_display/tile_palette.h @@ -16,6 +16,6 @@ inline uint32_t color(const std::string &name) { if(name=="gray")return 0xE5E7EB; return 0; // Automatic/default, including unknown future palette names. } -// "Geen": no card behind the tile contents. Older firmware treats it as automatic. +// "None": no card behind the tile contents. Older firmware treats it as automatic. inline bool transparent(const std::string &name) { return name=="none"; } } diff --git a/device.example.yaml b/device.example.yaml index b65bcec..f0eb770 100644 --- a/device.example.yaml +++ b/device.example.yaml @@ -1,21 +1,21 @@ -# Kopieer via: python tools/new_device.py --name display-keuken -# Bewerk alleen dit lokale profiel en calibration.yaml voor jouw scherm. +# Copy via: python tools/new_device.py --name display-kitchen +# Only edit this local profile and calibration.yaml for your screen. packages: display: !include home-like-2432s028.yaml calibration: !include calibration.yaml substitutions: DEVICE_NAME: "cyd-new" - DEVICE_FRIENDLY_NAME: "Mijn CYD" - ROOM_NAME: "Woonkamer" + DEVICE_FRIENDLY_NAME: "My CYD" + ROOM_NAME: "Living room" TILE_COUNT: "6" - DIRECT_ACTIONS: "false" # Pas na koppelen/testen op true zetten. + DIRECT_ACTIONS: "false" # Set to true only after pairing/testing. AUTO_DIM_TIMEOUT: "600" CALIBRATION_ON_BOOT: "false" - # Tegel 1 - TILE1_ENTITY: "light.voorbeeld_lamp" - TILE1_STATE_ENTITY: "light.voorbeeld_lamp" + # Tile 1 + TILE1_ENTITY: "light.example_lamp" + TILE1_STATE_ENTITY: "light.example_lamp" TILE1_TITLE: "Lamp" TILE1_TYPE: "light" TILE1_TAP_ACTION: "auto" @@ -29,14 +29,14 @@ substitutions: TILE1_LONGPRESS_ACTION_TYPE: "" TILE1_LONGPRESS_ACTION_SERVICE: "" TILE1_VALUE_MODE: "auto" - TILE1_LABEL_ON: "Aan" - TILE1_LABEL_OFF: "Uit" + TILE1_LABEL_ON: "On" + TILE1_LABEL_OFF: "Off" TILE1_ACTIVE_STATE: "" - # Tegel 2 - TILE2_ENTITY: "switch.voorbeeld_stekker" - TILE2_STATE_ENTITY: "switch.voorbeeld_stekker" - TILE2_TITLE: "Stekker" + # Tile 2 + TILE2_ENTITY: "switch.example_plug" + TILE2_STATE_ENTITY: "switch.example_plug" + TILE2_TITLE: "Plug" TILE2_TYPE: "switch" TILE2_TAP_ACTION: "auto" TILE2_TAP_SERVICE: "" @@ -49,14 +49,14 @@ substitutions: TILE2_LONGPRESS_ACTION_TYPE: "" TILE2_LONGPRESS_ACTION_SERVICE: "" TILE2_VALUE_MODE: "auto" - TILE2_LABEL_ON: "Aan" - TILE2_LABEL_OFF: "Uit" + TILE2_LABEL_ON: "On" + TILE2_LABEL_OFF: "Off" TILE2_ACTIVE_STATE: "" - # Tegel 3 - TILE3_ENTITY: "scene.voorbeeld_avond" - TILE3_STATE_ENTITY: "scene.voorbeeld_avond" - TILE3_TITLE: "Avond" + # Tile 3 + TILE3_ENTITY: "scene.example_evening" + TILE3_STATE_ENTITY: "scene.example_evening" + TILE3_TITLE: "Evening" TILE3_TYPE: "scene" TILE3_TAP_ACTION: "auto" TILE3_TAP_SERVICE: "" @@ -69,14 +69,14 @@ substitutions: TILE3_LONGPRESS_ACTION_TYPE: "" TILE3_LONGPRESS_ACTION_SERVICE: "" TILE3_VALUE_MODE: "auto" - TILE3_LABEL_ON: "Aan" - TILE3_LABEL_OFF: "Uit" + TILE3_LABEL_ON: "On" + TILE3_LABEL_OFF: "Off" TILE3_ACTIVE_STATE: "" - # Tegel 4 - TILE4_ENTITY: "script.voorbeeld_uit" - TILE4_STATE_ENTITY: "script.voorbeeld_uit" - TILE4_TITLE: "Alles uit" + # Tile 4 + TILE4_ENTITY: "script.example_all_off" + TILE4_STATE_ENTITY: "script.example_all_off" + TILE4_TITLE: "All off" TILE4_TYPE: "script" TILE4_TAP_ACTION: "auto" TILE4_TAP_SERVICE: "" @@ -89,14 +89,14 @@ substitutions: TILE4_LONGPRESS_ACTION_TYPE: "" TILE4_LONGPRESS_ACTION_SERVICE: "" TILE4_VALUE_MODE: "auto" - TILE4_LABEL_ON: "Aan" - TILE4_LABEL_OFF: "Uit" + TILE4_LABEL_ON: "On" + TILE4_LABEL_OFF: "Off" TILE4_ACTIVE_STATE: "" - # Tegel 5 - TILE5_ENTITY: "climate.voorbeeld_airco" - TILE5_STATE_ENTITY: "climate.voorbeeld_airco" - TILE5_TITLE: "Airco" + # Tile 5 + TILE5_ENTITY: "climate.example_ac" + TILE5_STATE_ENTITY: "climate.example_ac" + TILE5_TITLE: "AC" TILE5_TYPE: "climate" TILE5_TAP_ACTION: "toggle" TILE5_TAP_SERVICE: "climate.toggle" @@ -109,14 +109,14 @@ substitutions: TILE5_LONGPRESS_ACTION_TYPE: "" TILE5_LONGPRESS_ACTION_SERVICE: "" TILE5_VALUE_MODE: "auto" - TILE5_LABEL_ON: "Aan" - TILE5_LABEL_OFF: "Uit" + TILE5_LABEL_ON: "On" + TILE5_LABEL_OFF: "Off" TILE5_ACTIVE_STATE: "" - # Tegel 6 - TILE6_ENTITY: "vacuum.voorbeeld_robot" - TILE6_STATE_ENTITY: "vacuum.voorbeeld_robot" - TILE6_TITLE: "Stofzuiger" + # Tile 6 + TILE6_ENTITY: "vacuum.example_robot" + TILE6_STATE_ENTITY: "vacuum.example_robot" + TILE6_TITLE: "Vacuum" TILE6_TYPE: "vacuum" TILE6_TAP_ACTION: "custom" TILE6_TAP_SERVICE: "vacuum.start" @@ -129,14 +129,14 @@ substitutions: TILE6_LONGPRESS_ACTION_TYPE: "" TILE6_LONGPRESS_ACTION_SERVICE: "" TILE6_VALUE_MODE: "auto" - TILE6_LABEL_ON: "Aan" - TILE6_LABEL_OFF: "Uit" + TILE6_LABEL_ON: "On" + TILE6_LABEL_OFF: "Off" 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" + # Tile 7 (hidden while TILE_COUNT is lower) + TILE7_ENTITY: "light.unused_7" + TILE7_STATE_ENTITY: "light.unused_7" + TILE7_TITLE: "Tile 7" TILE7_TYPE: "light" TILE7_TAP_ACTION: "auto" TILE7_TAP_SERVICE: "" @@ -149,14 +149,14 @@ substitutions: TILE7_LONGPRESS_ACTION_TYPE: "" TILE7_LONGPRESS_ACTION_SERVICE: "" TILE7_VALUE_MODE: "auto" - TILE7_LABEL_ON: "Aan" - TILE7_LABEL_OFF: "Uit" + TILE7_LABEL_ON: "On" + TILE7_LABEL_OFF: "Off" 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" + # Tile 8 (hidden while TILE_COUNT is lower) + TILE8_ENTITY: "light.unused_8" + TILE8_STATE_ENTITY: "light.unused_8" + TILE8_TITLE: "Tile 8" TILE8_TYPE: "light" TILE8_TAP_ACTION: "auto" TILE8_TAP_SERVICE: "" @@ -169,14 +169,14 @@ substitutions: TILE8_LONGPRESS_ACTION_TYPE: "" TILE8_LONGPRESS_ACTION_SERVICE: "" TILE8_VALUE_MODE: "auto" - TILE8_LABEL_ON: "Aan" - TILE8_LABEL_OFF: "Uit" + TILE8_LABEL_ON: "On" + TILE8_LABEL_OFF: "Off" 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" + # Tile 9 (hidden while TILE_COUNT is lower) + TILE9_ENTITY: "light.unused_9" + TILE9_STATE_ENTITY: "light.unused_9" + TILE9_TITLE: "Tile 9" TILE9_TYPE: "light" TILE9_TAP_ACTION: "auto" TILE9_TAP_SERVICE: "" @@ -189,14 +189,14 @@ substitutions: TILE9_LONGPRESS_ACTION_TYPE: "" TILE9_LONGPRESS_ACTION_SERVICE: "" TILE9_VALUE_MODE: "auto" - TILE9_LABEL_ON: "Aan" - TILE9_LABEL_OFF: "Uit" + TILE9_LABEL_ON: "On" + TILE9_LABEL_OFF: "Off" 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" + # Tile 10 (hidden while TILE_COUNT is lower) + TILE10_ENTITY: "light.unused_10" + TILE10_STATE_ENTITY: "light.unused_10" + TILE10_TITLE: "Tile 10" TILE10_TYPE: "light" TILE10_TAP_ACTION: "auto" TILE10_TAP_SERVICE: "" @@ -209,7 +209,7 @@ substitutions: TILE10_LONGPRESS_ACTION_TYPE: "" TILE10_LONGPRESS_ACTION_SERVICE: "" TILE10_VALUE_MODE: "auto" - TILE10_LABEL_ON: "Aan" - TILE10_LABEL_OFF: "Uit" + TILE10_LABEL_ON: "On" + TILE10_LABEL_OFF: "Off" TILE10_ACTIVE_STATE: "" diff --git a/diagnostics/capture_ui.py b/diagnostics/capture_ui.py index 65a1363..6a44263 100644 --- a/diagnostics/capture_ui.py +++ b/diagnostics/capture_ui.py @@ -28,7 +28,7 @@ async def main(): 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.') + if args.output.exists():raise ValueError('Output already exists; choose a different name.') 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: @@ -43,9 +43,9 @@ async def main(): 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}') + if errors or set(rows)!=set(range(240)):raise ValueError(f'Incomplete image: {len(rows)}/240 rows; {errors}') with args.output.open('xb') as f:f.write(png(rows)) - print(f'PASS: LVGL-render opgeslagen in {args.output}') + print(f'PASS: LVGL render saved to {args.output}') finally:await client.disconnect() if __name__=='__main__':asyncio.run(main()) diff --git a/diagnostics/control_ui.py b/diagnostics/control_ui.py index 6441285..4ac4b5d 100644 --- a/diagnostics/control_ui.py +++ b/diagnostics/control_ui.py @@ -9,7 +9,7 @@ async def main(): parser = argparse.ArgumentParser() parser.add_argument('action', choices=['touch_diagnostics', 'end_touch_diagnostics', 'ui_state', 'light_controls_preview']) parser.add_argument('--host', default='cyd-2432s028.local') - parser.add_argument('--name', default='cyd-2432s028', help='Verwachte DEVICE_NAME') + parser.add_argument('--name', default='cyd-2432s028', help='Expected DEVICE_NAME') parser.add_argument('--secrets', type=Path, default=Path(__file__).resolve().parents[1] / 'secrets.yaml') args = parser.parse_args() secret = yaml.safe_load(args.secrets.read_text()) diff --git a/diagnostics/run_ui_test.py b/diagnostics/run_ui_test.py index 2fa6349..f88b6cb 100644 --- a/diagnostics/run_ui_test.py +++ b/diagnostics/run_ui_test.py @@ -9,7 +9,7 @@ from aioesphomeapi import APIClient, LogLevel async def main(): parser = argparse.ArgumentParser() parser.add_argument('--host', default='cyd-2432s028.local') - parser.add_argument('--name', default='cyd-2432s028', help='Verwachte DEVICE_NAME') + parser.add_argument('--name', default='cyd-2432s028', help='Expected DEVICE_NAME') parser.add_argument('--secrets', type=Path, default=Path(__file__).resolve().parents[1] / 'secrets.yaml') args = parser.parse_args() secrets = yaml.safe_load(args.secrets.read_text()) diff --git a/diagnostics/send_layout.py b/diagnostics/send_layout.py index 05be553..73dd8df 100644 --- a/diagnostics/send_layout.py +++ b/diagnostics/send_layout.py @@ -17,71 +17,71 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'screen_manager/app from core import extras, packets, state_message, validate_layout # noqa: E402 def demo_layout(): - return validate_layout({'title': 'Demo kaarten', 'tiles': [ + return validate_layout({'title': 'Demo cards', 'tiles': [ {'entity': 'screen.clock', 'name': '', 'options': {'display': 'analog', 'background': 'none'}}, - {'entity': 'weather.demo', 'name': 'Buiten', 'options': {'display': 'forecast'}}, - {'entity': 'sensor.demo_temperatuur', 'name': 'Woonkamer', 'options': {'display': 'graph', 'icon': 'thermometer'}}, + {'entity': 'weather.demo', 'name': 'Outside', 'options': {'display': 'forecast'}}, + {'entity': 'sensor.demo_temperature', 'name': 'Living room', 'options': {'display': 'graph', 'icon': 'thermometer'}}, {'entity': 'person.demo', 'name': 'Max', 'options': {}}, - {'entity': 'timer.demo', 'name': 'Eieren', 'options': {'icon': 'chef-hat'}}, - {'entity': 'sun.sun', 'name': 'Zon', 'options': {'display': 'sunpath'}}, - {'entity': 'sensor.demo_energie', 'name': 'Verbruik vandaag', 'options': {'display': 'graph', 'size': 'wide'}}, - {'entity': 'light.demo', 'name': 'Tafellamp', 'options': {'display': 'watch', 'size': 'wide', 'icon': 'lamp'}}, - {'entity': 'weather.demo_standaard', 'name': 'Weer nu', 'options': {}}, + {'entity': 'timer.demo', 'name': 'Eggs', 'options': {'icon': 'chef-hat'}}, + {'entity': 'sun.sun', 'name': 'Sun', 'options': {'display': 'sunpath'}}, + {'entity': 'sensor.demo_energy', 'name': 'Usage today', 'options': {'display': 'graph', 'size': 'wide'}}, + {'entity': 'light.demo', 'name': 'Table lamp', 'options': {'display': 'watch', 'size': 'wide', 'icon': 'lamp'}}, + {'entity': 'weather.demo_default', 'name': 'Weather now', 'options': {}}, ]}) # Direct controls on wide cards (firmware 0.2.19+): one card per control set. def controls_layout(): - return validate_layout({'title': 'Directe bediening', 'tiles': [ - {'entity': 'cover.demo_gordijn', 'name': 'Gordijnen Raambekleding', 'options': {'size': 'wide', 'icon': 'curtains'}}, - {'entity': 'climate.demo_airco', 'name': 'Airco', 'options': {'size': 'wide'}}, + return validate_layout({'title': 'Direct control', 'tiles': [ + {'entity': 'cover.demo_curtain', 'name': 'Curtains Window covering', 'options': {'size': 'wide', 'icon': 'curtains'}}, + {'entity': 'climate.demo_ac', 'name': 'AC', 'options': {'size': 'wide'}}, {'entity': 'media_player.demo_sonos', 'name': 'Sonos', 'options': {'size': 'wide', 'icon': 'speaker'}}, - {'entity': 'climate.demo_cv', 'name': 'Verwarming', 'options': {'size': 'wide', 'controls': 'mode', 'icon': 'radiator'}}, - {'entity': 'media_player.demo_radio', 'name': 'Sonos slaapkamer', 'options': {'size': 'wide', 'controls': 'playback', 'icon': 'radio'}}, + {'entity': 'climate.demo_heating', 'name': 'Heating', 'options': {'size': 'wide', 'controls': 'mode', 'icon': 'radiator'}}, + {'entity': 'media_player.demo_radio', 'name': 'Sonos bedroom', 'options': {'size': 'wide', 'controls': 'playback', 'icon': 'radio'}}, {'entity': 'vacuum.demo_robot', 'name': 'Pippa', 'options': {'size': 'wide'}}, - {'entity': 'switch.demo_bureau', 'name': 'Bureau', 'options': {'size': 'wide', 'background': 'blue', 'icon': 'power-socket-eu'}}, - {'entity': 'light.demo_tafellamp', 'name': 'Tafellamp', 'options': {'size': 'wide', 'controls': 'brightness', 'icon': 'lamp'}}, - {'entity': 'number.demo_doel', 'name': 'Doel luchtvochtigheid', 'options': {'size': 'wide', 'icon': 'water-percent'}}, - {'entity': 'select.demo_stand', 'name': 'Verwarmingsstand', 'options': {'size': 'wide', 'icon': 'thermostat'}}, - {'entity': 'timer.demo_eieren', 'name': 'Eieren', 'options': {'size': 'wide', 'icon': 'chef-hat'}}, - {'entity': 'scene.demo_avond', 'name': 'Avond', 'options': {'size': 'wide', 'icon': 'sofa'}}, - {'entity': 'fan.demo_ventilator', 'name': 'Ventilator', 'options': {'size': 'wide'}}, - {'entity': 'cover.demo_rolluik', 'name': 'Rolluik', 'options': {'size': 'wide', 'controls': 'position', 'icon': 'window-shutter'}}, - {'entity': 'light.demo_plafond', 'name': 'Plafondlamp', 'options': {'size': 'wide', 'icon': 'ceiling-light'}}, - {'entity': 'weather.demo_buiten', 'name': 'Buiten', 'options': {'display': 'forecast'}}, - {'entity': 'script.demo_tv', 'name': 'TV aanzetten', 'options': {'icon': 'television'}}, - {'entity': 'scene.demo_ochtend', 'name': 'Ochtend', 'options': {'icon': 'weather-sunset-up'}}, + {'entity': 'switch.demo_desk', 'name': 'Desk', 'options': {'size': 'wide', 'background': 'blue', 'icon': 'power-socket-eu'}}, + {'entity': 'light.demo_table_lamp', 'name': 'Table lamp', 'options': {'size': 'wide', 'controls': 'brightness', 'icon': 'lamp'}}, + {'entity': 'number.demo_target', 'name': 'Target humidity', 'options': {'size': 'wide', 'icon': 'water-percent'}}, + {'entity': 'select.demo_mode', 'name': 'Heating mode', 'options': {'size': 'wide', 'icon': 'thermostat'}}, + {'entity': 'timer.demo_eggs', 'name': 'Eggs', 'options': {'size': 'wide', 'icon': 'chef-hat'}}, + {'entity': 'scene.demo_evening', 'name': 'Evening', 'options': {'size': 'wide', 'icon': 'sofa'}}, + {'entity': 'fan.demo_fan', 'name': 'Fan', 'options': {'size': 'wide'}}, + {'entity': 'cover.demo_shutter', 'name': 'Shutter', 'options': {'size': 'wide', 'controls': 'position', 'icon': 'window-shutter'}}, + {'entity': 'light.demo_ceiling', 'name': 'Ceiling light', 'options': {'size': 'wide', 'icon': 'ceiling-light'}}, + {'entity': 'weather.demo_outside', 'name': 'Outside', 'options': {'display': 'forecast'}}, + {'entity': 'script.demo_tv', 'name': 'Turn on TV', 'options': {'icon': 'television'}}, + {'entity': 'scene.demo_morning', 'name': 'Morning', 'options': {'icon': 'weather-sunset-up'}}, ]}) def controls_states(now): end = now + timedelta(minutes=4, seconds=32) return { - 'cover.demo_gordijn': {'state': 'open', 'attributes': {'current_position': 80, 'device_class': 'curtain', 'supported_features': 15}}, - 'climate.demo_airco': {'state': 'cool', 'attributes': {'current_temperature': 21.5, 'temperature': 20, 'min_temp': 16, 'max_temp': 32, 'target_temp_step': 1.0, 'hvac_modes': ['off', 'heat_cool', 'cool', 'heat', 'fan_only', 'dry'], 'fan_modes': ['auto', 'low', 'medium', 'high'], 'fan_mode': 'low', 'swing_modes': ['off', 'both', 'vertical', 'horizontal'], 'swing_mode': 'off', 'supported_features': 425}}, + 'cover.demo_curtain': {'state': 'open', 'attributes': {'current_position': 80, 'device_class': 'curtain', 'supported_features': 15}}, + 'climate.demo_ac': {'state': 'cool', 'attributes': {'current_temperature': 21.5, 'temperature': 20, 'min_temp': 16, 'max_temp': 32, 'target_temp_step': 1.0, 'hvac_modes': ['off', 'heat_cool', 'cool', 'heat', 'fan_only', 'dry'], 'fan_modes': ['auto', 'low', 'medium', 'high'], 'fan_mode': 'low', 'swing_modes': ['off', 'both', 'vertical', 'horizontal'], 'swing_mode': 'off', 'supported_features': 425}}, 'media_player.demo_sonos': {'state': 'playing', 'attributes': {'volume_level': 0.17, 'is_volume_muted': False, 'media_title': 'TV', 'supported_features': 8321599}}, - 'climate.demo_cv': {'state': 'heat', 'attributes': {'current_temperature': 19.5, 'temperature': 21, 'hvac_action': 'heating', 'hvac_modes': ['off', 'heat', 'auto']}}, + 'climate.demo_heating': {'state': 'heat', 'attributes': {'current_temperature': 19.5, 'temperature': 21, 'hvac_action': 'heating', 'hvac_modes': ['off', 'heat', 'auto']}}, 'media_player.demo_radio': {'state': 'playing', 'attributes': {'volume_level': 0.15, 'media_title': 'NPO Radio 2', 'supported_features': 8321599}}, 'vacuum.demo_robot': {'state': 'docked', 'attributes': {'battery_level': 100, 'fan_speed': 'max', 'fan_speed_list': ['quiet', 'balanced', 'turbo', 'max'], 'supported_features': 30524}}, - 'switch.demo_bureau': {'state': 'on', 'attributes': {}}, - 'light.demo_tafellamp': {'state': 'on', 'attributes': {'brightness': 163}}, - 'number.demo_doel': {'state': '55', 'attributes': {'min': 30, 'max': 70, 'step': 5, 'unit_of_measurement': '%'}}, - 'select.demo_stand': {'state': 'Comfort', 'attributes': {'options': ['Eco', 'Comfort', 'Boost']}}, - 'timer.demo_eieren': {'state': 'active', 'attributes': {'finishes_at': end.isoformat(), 'duration': '0:05:00', 'remaining': '0:05:00'}}, - 'scene.demo_avond': {'state': (now - timedelta(hours=3)).isoformat(), 'attributes': {}}, - 'fan.demo_ventilator': {'state': 'off', 'attributes': {'percentage': 0}}, - 'cover.demo_rolluik': {'state': 'open', 'attributes': {'current_position': 35, 'supported_features': 15}}, - 'light.demo_plafond': {'state': 'off', 'attributes': {}}, - 'weather.demo_buiten': {'state': 'rainy', 'attributes': {'temperature': 18.4, 'temperature_unit': '°C', 'humidity': 92, 'wind_speed': 12.2, 'wind_speed_unit': 'km/h', 'apparent_temperature': 17.1}}, + 'switch.demo_desk': {'state': 'on', 'attributes': {}}, + 'light.demo_table_lamp': {'state': 'on', 'attributes': {'brightness': 163}}, + 'number.demo_target': {'state': '55', 'attributes': {'min': 30, 'max': 70, 'step': 5, 'unit_of_measurement': '%'}}, + 'select.demo_mode': {'state': 'Comfort', 'attributes': {'options': ['Eco', 'Comfort', 'Boost']}}, + 'timer.demo_eggs': {'state': 'active', 'attributes': {'finishes_at': end.isoformat(), 'duration': '0:05:00', 'remaining': '0:05:00'}}, + 'scene.demo_evening': {'state': (now - timedelta(hours=3)).isoformat(), 'attributes': {}}, + 'fan.demo_fan': {'state': 'off', 'attributes': {'percentage': 0}}, + 'cover.demo_shutter': {'state': 'open', 'attributes': {'current_position': 35, 'supported_features': 15}}, + 'light.demo_ceiling': {'state': 'off', 'attributes': {}}, + 'weather.demo_outside': {'state': 'rainy', 'attributes': {'temperature': 18.4, 'temperature_unit': '°C', 'humidity': 92, 'wind_speed': 12.2, 'wind_speed_unit': 'km/h', 'apparent_temperature': 17.1}}, 'script.demo_tv': {'state': 'off', 'attributes': {'last_triggered': (now - timedelta(hours=2, minutes=8)).isoformat()}}, - 'scene.demo_ochtend': {'state': (now - timedelta(days=1, hours=5)).isoformat(), 'attributes': {}}, + 'scene.demo_morning': {'state': (now - timedelta(days=1, hours=5)).isoformat(), 'attributes': {}}, } def demo_states(now): end = now + timedelta(minutes=4, seconds=32) return { 'weather.demo': {'state': 'partlycloudy', 'attributes': {'temperature': 18.4, 'temperature_unit': '°C'}}, - 'weather.demo_standaard': {'state': 'rainy', 'attributes': {'temperature': 12.0, 'temperature_unit': '°C'}}, - 'sensor.demo_temperatuur': {'state': '21.5', 'attributes': {'unit_of_measurement': '°C'}}, - 'sensor.demo_energie': {'state': '7.42', 'attributes': {'unit_of_measurement': 'kWh'}}, + 'weather.demo_default': {'state': 'rainy', 'attributes': {'temperature': 12.0, 'temperature_unit': '°C'}}, + 'sensor.demo_temperature': {'state': '21.5', 'attributes': {'unit_of_measurement': '°C'}}, + 'sensor.demo_energy': {'state': '7.42', 'attributes': {'unit_of_measurement': 'kWh'}}, 'person.demo': {'state': 'home', 'attributes': {'icon': 'mdi:account-child'}}, 'timer.demo': {'state': 'active', 'attributes': {'finishes_at': end.isoformat(), 'duration': '0:05:00', 'remaining': '0:05:00'}}, 'sun.sun': {'state': 'above_horizon', 'attributes': {'next_rising': (now + timedelta(hours=9)).isoformat(), 'next_setting': (now + timedelta(hours=2)).isoformat()}}, @@ -122,19 +122,20 @@ def messages(inbox, rotate=0, digital=False, wide=False, controls=False): async def main(): p = argparse.ArgumentParser(description=__doc__) p.add_argument('--host', required=True) - p.add_argument('--name', required=True, help='Verwachte DEVICE_NAME') + p.add_argument('--name', required=True, help='Expected DEVICE_NAME') p.add_argument('--secrets', type=Path, default=Path(__file__).resolve().parents[1] / 'secrets.yaml') - p.add_argument('--rotate', type=int, default=0, help='Zet tegel N vooraan, zodat latere pagina\'s op pagina 1 komen') - p.add_argument('--digital', action='store_true', help='Digitale klok op een enkele tegel in plaats van de analoge kalenderkaart zonder achtergrond') - p.add_argument('--wide', action='store_true', help='Analoge klok dubbelbreed (wijzerplaat met digitale tijd en datum)') - p.add_argument('--controls', action='store_true', help='Dubbelbrede kaarten met directe bediening (firmware 0.2.19+), drie per pagina') + p.add_argument('--rotate', type=int, default=0, help='Put tile N first, so later pages land on page 1') + p.add_argument('--digital', action='store_true', help='Digital clock on a single tile instead of the analog calendar card without a background') + p.add_argument('--wide', action='store_true', help='Analog clock double-width (dial with digital time and date)') + p.add_argument('--controls', action='store_true', help='Double-width cards with direct control (firmware 0.2.19+), three per page') args = p.parse_args() client = APIClient(args.host, 6053, noise_psk=yaml.safe_load(args.secrets.read_text())['api_encryption_key'], client_info='Demo layout', expected_name=args.name) await client.connect(login=True) try: entities, _ = await client.list_entities_services() - inbox = next(e for e in entities if type(e).__name__ == 'TextInfo' and e.name == 'Tegelinstellingen') + # Firmware built before the English translation still names this entity in Dutch. + inbox = next(e for e in entities if type(e).__name__ == 'TextInfo' and e.name in ('Tile settings', 'Tegelinstellingen')) # The firmware answers through the inbox entity's state, not the log. replies = [] def state(update): @@ -148,9 +149,11 @@ async def main(): client.text_command(inbox.key, packet) await asyncio.sleep(0.05) await asyncio.sleep(1.5) - print(f'Demo-indeling verstuurd in {time.monotonic() - started:.1f}s; inbox meldt: {" / ".join(dict.fromkeys(replies)) or "(geen statuswijziging)"}') - if not any(r in ('Indeling ontvangen', 'Gesynchroniseerd', 'Tegels laden') for r in replies): - raise SystemExit('De firmware accepteerde de demo-indeling niet.') + print(f'Demo layout sent in {time.monotonic() - started:.1f}s; inbox reports: {" / ".join(dict.fromkeys(replies)) or "(no status change)"}') + # Firmware built before the English translation still reports the Dutch originals. + accepted = ('Layout received', 'Synced', 'Loading tiles', 'Indeling ontvangen', 'Gesynchroniseerd', 'Tegels laden') + if not any(r in accepted for r in replies): + raise SystemExit('The firmware did not accept the demo layout.') finally: await client.disconnect() diff --git a/diagnostics/test_runtime_transport.py b/diagnostics/test_runtime_transport.py index 8656c71..a01dccd 100644 --- a/diagnostics/test_runtime_transport.py +++ b/diagnostics/test_runtime_transport.py @@ -20,7 +20,8 @@ async def main(): await client.connect(login=True) try: entities, _ = await client.list_entities_services() - inbox = next(e for e in entities if isinstance(e, TextInfo) and e.name == 'Tegelinstellingen') + # Firmware built before the English translation still names this entity in Dutch. + inbox = next(e for e in entities if isinstance(e, TextInfo) and e.name in ('Tile settings', 'Tegelinstellingen')) assert inbox.max_length == 255 queue = asyncio.Queue() client.subscribe_states(lambda s: queue.put_nowait(s.state) if isinstance(s, TextState) and s.key == inbox.key else None) @@ -35,13 +36,13 @@ async def main(): client.text_command(inbox.key, secrets.token_hex(6) + '|0|1|' + encoded) while True: ack = await asyncio.wait_for(queue.get(), 5) - if ack.startswith('Fout:'): + if ack.startswith(('Error:', 'Fout:')): print('PASS: rejected invalid command — ' + ack) break client.text_command(inbox.key, secrets.token_hex(6) + '|2|1|e30=') while True: ack = await asyncio.wait_for(queue.get(), 5) - if ack == 'Fout: onvolledig bericht': + if ack in ('Error: incomplete message', 'Fout: onvolledig bericht'): print('PASS: missing chunks rejected') break finally: diff --git a/docs/ACCEPTANCE.md b/docs/ACCEPTANCE.md new file mode 100644 index 0000000..51fff33 --- /dev/null +++ b/docs/ACCEPTANCE.md @@ -0,0 +1,98 @@ +# Acceptance test and handover + +Note the date, board variant/MAC, Git commit, ESPHome version, device name, and the +names of the two measurement files. Don't include passwords in the report. + +## Without real HA actions + +1. Calibrate using [CALIBRATING.md](CALIBRATING.md); flash the result and run a + new, independent measurement with `verify`. A successful fit alone + doesn't prove the firmware coordinates are correct. +2. Flash normal mode. Check that the measurement screen disappears. +3. Check every visible tile and its edges: no other tile should respond. + Tap calmly and rapidly in succession, but expect no repeat within the + deliberate debounce period. A swipe should not trigger a tile action afterward. +4. With 1–6 tiles: no page button. With 7–10: repeat at least twenty times: + Next → tile → Previous. The bottom tile should not respond to the + page button. Open/close overlays; a tap inside one should not leak through. +5. Run the built-in render test via the encrypted API: + + ```sh + python diagnostics/run_ui_test.py --host display-kitchen.local --name display-kitchen + ``` + + Requires ten successful page checks, fifty overlay cycles, and progress + from real draw callbacks. This doesn't test physical touch or HA actions. +6. Leave the board on for at least 15 minutes. Check that it doesn't restart, + that standby only starts after ten minutes without touch, and that the first + tap wakes the screen without toggling a device. + +## With Home Assistant + +Only now enable direct actions, reflash, and choose safe test devices. +Check every configured tile with its real HA entity: + +- A tap performs exactly one intended action; the feedback is correct. +- Changes from HA also appear on the screen. +- Light slider/color, climate setpoint, and supported modes work where used. +- Vacuum actions and speeds work where supported. Note any missing functionality. +- After a power interruption, Wi-Fi and HA come back; name, calibration, and tiles persist. +- After a brief Wi-Fi/HA interruption, the connection recovers without a USB flash. + +Keep logs local only. Check for resets, watchdog messages, repeated +connection errors, and noticeably slow components. A short render test doesn't +prove stability over days; note the duration actually observed. + +## Automated development checks + +From the repo, with the Python environment active: + +```sh +python -m unittest discover -s tests -p 'test_*.py' +c++ -std=c++17 -Wall -Wextra -pedantic tests/test_cyd_ui.cpp -o /tmp/test_cyd_ui +/tmp/test_cyd_ui +c++ -std=c++17 -Wall -Wextra -pedantic tests/test_touch_filter.cpp -o /tmp/test_touch_filter +c++ -std=c++17 -Wall -Wextra -pedantic tests/test_light_controls.cpp -o /tmp/test_light_controls +/tmp/test_light_controls +/tmp/test_touch_filter +c++ -std=c++17 -Wall -Wextra -pedantic tests/test_light_controls.cpp -o /tmp/test_light_controls +/tmp/test_light_controls +python -m esphome config device.yaml +python -m esphome compile device.yaml +``` + +The C++ commands above are for macOS/Linux with a compiler. On Windows, +the developer can use whatever C++17 toolchain is available. Only flash the +checked profile to the identified board. + +## Handover report + +```text +Board / MAC: +Git commit / ESPHome version: +Device name / HA entities: +Calibration measurement + independent verification: +Render test: +Physical pagination/touch/standby: +Real HA actions: +Observed stable duration: +Limitations / not yet tested: +Local profile files safely stored: +``` + +Give the owner the three local YAML files and the measurement files through a +suitable private channel. For a different owner, use the clean repository +or starter ZIP, so they get their own keys and their own panel calibration. + +## Light card + +- RGB+white light: three sliders; RGB-only: color and brightness; white-only: + white temperature and brightness. A plain dimmer keeps the brightness card. +- Drag slowly across the rainbow, release, and check the physical light. + Repeat for white temperature and brightness. Brightness keeps the color mode. +- Reopen the card and check the received values. Test two lights with + different temperature ranges; no value should leak from the other light. +- The firmware render diagnostic includes `Light sliders: PASS`: twenty preview events + per slider, exactly one commit on release, no duplicate commit, no commit + after an aborted touch, and four capability combinations. This runs with + swapped-in callbacks and doesn't control any real lights. diff --git a/docs/ACCEPTATIE.md b/docs/ACCEPTATIE.md deleted file mode 100644 index 1dc7617..0000000 --- a/docs/ACCEPTATIE.md +++ /dev/null @@ -1,98 +0,0 @@ -# Acceptatietest en oplevering - -Noteer datum, bordvariant/MAC, Git-commit, ESPHome-versie, apparaatnaam en de -namen van de twee meetbestanden. Neem geen wachtwoorden in het rapport op. - -## Zonder echte HA-acties - -1. Kalibreer met [CALIBREREN.md](CALIBREREN.md); flash de uitkomst en voer een - nieuwe, onafhankelijke meting met `verify` uit. Een geslaagde fit alleen - bewijst niet dat de firmwarecoördinaten kloppen. -2. Flash de normale modus. Controleer dat het meetscherm verdwijnt. -3. Controleer elk zichtbaar vak en de randen: geen andere tegel mag reageren. - Tik rustig en snel achter elkaar, maar verwacht geen herhaling binnen de - bewuste debounceperiode. Een veeg mag geen tegelactie achteraf veroorzaken. -4. Bij 1–6 tegels: geen paginaknop. Bij 7–10: herhaal minstens twintig keer - Volgende → tegel → Vorige. De onderste tegel mag niet reageren op de - paginaknop. Open/sluit overlays; een tik daarin mag niet doorlekken. -5. Draai de ingebouwde renderproef via de versleutelde API: - - ```sh - python diagnostics/run_ui_test.py --host display-keuken.local --name display-keuken - ``` - - Vereist tien geslaagde paginacontroles, vijftig overlaycycli en voortgang - van echte draw-callbacks. Dit test geen fysieke touch of HA-acties. -6. Laat het bord minstens 15 minuten aan. Controleer dat het niet herstart, - dat standby pas na tien minuten zonder aanraking begint en dat de eerste - tik het scherm wekt zonder een apparaat te schakelen. - -## Met Home Assistant - -Schakel directe acties pas nu in, flash opnieuw en kies veilige testapparaten. -Controleer elke geconfigureerde tegel met zijn echte HA-entiteit: - -- Een tik voert precies één bedoelde actie uit; de terugmelding klopt. -- Veranderen vanuit HA wordt ook op het scherm zichtbaar. -- Lichtslider/kleur, climate-setpoint en ondersteunde modi werken waar gebruikt. -- Vacuumacties en snelheden werken waar ondersteund. Noteer ontbrekende functies. -- Na stroomonderbreking komen wifi en HA terug; naam, kalibratie en tegels blijven. -- Na een korte wifi-/HA-onderbreking herstelt de verbinding zonder USB-flash. - -Bewaar logs alleen lokaal. Controleer op resets, watchdogmeldingen, herhaalde -connectiefouten en opvallend trage componenten. Een korte renderproef bewijst -geen stabiliteit over dagen; noteer de werkelijk geobserveerde duur. - -## Geautomatiseerde ontwikkelchecks - -Vanuit de repo, met de Python-omgeving actief: - -```sh -python -m unittest discover -s tests -p 'test_*.py' -c++ -std=c++17 -Wall -Wextra -pedantic tests/test_cyd_ui.cpp -o /tmp/test_cyd_ui -/tmp/test_cyd_ui -c++ -std=c++17 -Wall -Wextra -pedantic tests/test_touch_filter.cpp -o /tmp/test_touch_filter -c++ -std=c++17 -Wall -Wextra -pedantic tests/test_light_controls.cpp -o /tmp/test_light_controls -/tmp/test_light_controls -/tmp/test_touch_filter -c++ -std=c++17 -Wall -Wextra -pedantic tests/test_light_controls.cpp -o /tmp/test_light_controls -/tmp/test_light_controls -python -m esphome config device.yaml -python -m esphome compile device.yaml -``` - -De C++-commando's hierboven zijn voor macOS/Linux met een compiler. Op Windows -kan de developer een beschikbare C++17-toolchain gebruiken. Flash alleen het -gecontroleerde profiel naar het geïdentificeerde bord. - -## Opleverrapport - -```text -Bord / MAC: -Git-commit / ESPHome-versie: -Apparaatnaam / HA-entiteiten: -Kalibratiemeting + onafhankelijke verificatie: -Renderproef: -Fysieke paginering/touch/standby: -Werkelijke HA-acties: -Geobserveerde stabiele duur: -Beperkingen / nog niet getest: -Lokale profielbestanden veilig bewaard: -``` - -Geef de eigenaar de drie lokale YAML-bestanden en de meetbestanden via een -passend privé-kanaal. Gebruik voor een andere eigenaar de schone repository -of starter-ZIP, zodat die eigen sleutels en eigen paneelkalibratie krijgt. - -## Lichtkaart - -- RGB+wit-lamp: drie schuiven; RGB-only: kleur en helderheid; wit-only: - wittemperatuur en helderheid. Een gewone dimmer behoudt de helderheidskaart. -- Sleep langzaam over de regenboog, laat los en controleer de fysieke lamp. - Herhaal voor wittemperatuur en helderheid. Helderheid behoudt de kleurmodus. -- Heropen de kaart en controleer de ontvangen waarden. Test twee lampen met - verschillende temperatuurbereiken; er mag geen waarde van de andere lamp lekken. -- Firmware-renderdiagnose bevat `Light sliders: PASS`: twintig preview-events - per schuif, precies één commit bij loslaten, geen dubbele commit, geen commit - na een afgebroken aanraking en vier capabilitycombinaties. Dit gebeurt met - vervangen callbacks en bedient geen echte lampen. diff --git a/docs/CALIBRATING.md b/docs/CALIBRATING.md new file mode 100644 index 0000000..09c8d01 --- /dev/null +++ b/docs/CALIBRATING.md @@ -0,0 +1,127 @@ +# Calibrating a new panel + +This protocol is for **320×240, LVGL rotation 90°, swap_xy=false, +mirror_x=true, mirror_y=false**. Other orientations, GT911 touch, and other +controllers require a separate profile. Don't change rotation or mirroring +partway through the measurement. + +The filter in this repo stabilizes the XPT2046 measurements. Calibration then +corrects offset, scale, and skew. Calibration does not fix a defective +panel, a loose connection, unstable power, or random outliers. + +## Preparing + +1. Use the `device.yaml` from `tools/new_device.py`, with your own + `calibration.yaml` that starts as identity. Don't carry over the personal + correction from the historical base profile. +2. Check the data cable and the USB port with `python -m serial.tools.list_ports`. +3. Flash from the repo root: + +```sh +python -m esphome -s CALIBRATION_ON_BOOT true run device.yaml --device +``` + +4. Physically check for the **dark screen with five white + marks**. + It stays on, and tile actions aren't accessible here. +5. Stop the ESPHome log reader with Ctrl+C. Leave the board and USB cable connected. + Only one program may use the serial port at a time. + +Wi-Fi/HA aren't needed for this USB route. If the board is already on Wi-Fi, +the screen can also be opened via the encrypted API: + +```sh +python diagnostics/control_ui.py touch_diagnostics --host display-kitchen.local --name display-kitchen +``` + +That doesn't change the boot mode. For a new installation, use the +USB route above, so calibration mode also stays active after a reset. + +## Measurement A: collecting data + +```sh +python tools/calibrate.py capture --port --device-name display-kitchen --output measurements-before.json +``` + +The wizard guides you through **one target at a time**: + +1. Read in the terminal which crosshair is up next. +2. Press Enter, then tap only that crosshair **three times**. +3. Hold each tap for about half a second and release fully in between. +4. Wait for the next terminal prompt. Don't move on to another corner yourself. + +Order: top-left, top-right, bottom-right, bottom-left, center. +Use a suitably blunt stylus or a small, deliberate fingertip tap; no +sharp point. The targets are at `(20,20)`, `(299,20)`, `(299,219)`, `(20,219)`, +and `(160,120)`. + +The wizard only reads touch logs with `calibration=1`. It doesn't flash anything, make +any network connection, or send any HA actions. An incomplete measurement is +not saved as a valid file. On a mistaken tap: Ctrl+C and start +over. Use a new filename if a measurement already exists. + +## Calculating a correction + +```sh +python tools/calibrate.py fit measurements-before.json --output calibration.yaml --replace +``` + +The script: + +- takes the median of the three raw ADC measurements per corner; +- determines an affine correction from the four corners; +- checks the **center point, which is not used for the fit**; +- rejects errors that are too large (>12 px), spread (>18 px), and unusable measurement geometry; +- saves the existing `calibration.yaml` as a dated `.bak` before + placing a new, complete version. + +The output contains only the four base bounds and six affine coefficients in +an ESPHome substitutions map. `device.yaml` imports this as the calibration +package. Don't change the coefficients by feel, and don't add a second correction +in the driver. The `raw=` logs contain the filtered **physical ADC values**, +even once a correction is already set; recalibrating therefore doesn't stack twice. + +A successful fit on existing data is not yet a successful physical test. + +## Measurement B: independently testing the flashed result + +Flash the correction, still with the measurement screen: + +```sh +python -m esphome -s CALIBRATION_ON_BOOT true run device.yaml --device +``` + +Close the log reader with Ctrl+C and collect **new taps**: + +```sh +python tools/calibrate.py capture --port --device-name display-kitchen --output measurements-after.json +python tools/calibrate.py verify measurements-after.json +``` + +`verify` uses the firmware's real `native=` coordinates, rotates them +to screen coordinates, and compares them with the targets. It does **not +run a new fit**. All five points must pass. Don't accidentally +use the pre-flash file for this check. + +Doesn't work? Check which point fails, check the order/pressure, measure again, +and read [TROUBLESHOOTING.md](TROUBLESHOOTING.md). Don't just raise the tolerance to get the +test to pass. Report any remaining hardware deviation honestly. + +## Back to normal operation + +```sh +python -m esphome run device.yaml --device +``` + +No `-s CALIBRATION_ON_BOOT true` is given here. In `device.yaml`, the +setting stays `false`. Check the actual tiles and navigation per +[ACCEPTANCE.md](ACCEPTANCE.md). + +If calibration mode was opened only via the API, you can close it: + +```sh +python diagnostics/control_ui.py end_touch_diagnostics --host display-kitchen.local --name display-kitchen +``` + +Keep both measurement files and `calibration.yaml` with this physical screen. +Start over after replacing the panel; give only the neutral +starter files to a next owner. diff --git a/docs/CALIBREREN.md b/docs/CALIBREREN.md deleted file mode 100644 index 10ea381..0000000 --- a/docs/CALIBREREN.md +++ /dev/null @@ -1,127 +0,0 @@ -# Een nieuw paneel kalibreren - -Dit protocol is voor **320×240, LVGL-rotatie 90°, swap_xy=false, -mirror_x=true, mirror_y=false**. Andere oriëntaties, GT911-touch en andere -controllers vereisen een apart profiel. Verander rotatie of mirroring niet -halverwege de meting. - -De filter in deze repo stabiliseert de XPT2046-metingen. Kalibratie corrigeert -vervolgens offset, schaal en scheefstand. Kalibratie repareert geen defect -paneel, losse verbinding, instabiele voeding of willekeurige uitschieters. - -## Voorbereiden - -1. Gebruik `device.yaml` van `tools/new_device.py`, met een eigen - `calibration.yaml` die begint als identiteit. De persoonlijke correctie - in het historische basisprofiel mag niet worden overgenomen. -2. Controleer de datakabel en de USB-poort met `python -m serial.tools.list_ports`. -3. Flash vanaf de reporoot: - -```sh -python -m esphome -s CALIBRATION_ON_BOOT true run device.yaml --device -``` - -4. Controleer fysiek het **donkere scherm met vijf witte + tekens**. - Het blijft aan en de tegelacties zijn hier niet toegankelijk. -5. Stop de ESPHome-loglezer met Ctrl+C. Laat het bord en de USB-kabel zitten. - Slechts één programma mag tegelijk de seriële poort gebruiken. - -Wifi/HA zijn voor deze USB-route niet nodig. Als het bord al op wifi staat, -kan het scherm ook via de versleutelde API worden geopend: - -```sh -python diagnostics/control_ui.py touch_diagnostics --host display-keuken.local --name display-keuken -``` - -Dat verandert de startmodus niet. Gebruik voor een nieuwe installatie de -USB-route hierboven, zodat de kalibratiemodus ook na een reset actief blijft. - -## Meting A: gegevens verzamelen - -```sh -python tools/calibrate.py capture --port --device-name display-keuken --output measurements-before.json -``` - -De wizard begeleidt **één doel tegelijk**: - -1. Lees in de terminal welk kruisje aan de beurt is. -2. Druk op Enter en tik daarna alleen dat kruisje **drie keer**. -3. Houd elke tik circa een halve seconde vast en laat tussendoor volledig los. -4. Wacht op de volgende terminalprompt. Ga niet zelf alvast naar een andere hoek. - -Volgorde: linksboven, rechtsboven, rechtsonder, linksonder, midden. -Gebruik een geschikte stompe stylus of een kleine, bewuste vingertik; geen -scherpe punt. De doelen staan op `(20,20)`, `(299,20)`, `(299,219)`, `(20,219)` -en `(160,120)`. - -De wizard leest alleen touchlogs met `calibration=1`. Hij flasht niets, maakt -geen netwerkverbinding en stuurt geen HA-acties. Een onvolledige meting wordt -niet als geldig bestand opgeslagen. Bij een verkeerde tik: Ctrl+C en opnieuw -beginnen. Gebruik een nieuwe bestandsnaam als een meting al bestaat. - -## Een correctie berekenen - -```sh -python tools/calibrate.py fit measurements-before.json --output calibration.yaml --replace -``` - -Het script: - -- neemt per hoek de mediaan van de drie ruwe ADC-metingen; -- bepaalt een affine correctie uit de vier hoeken; -- controleert het **niet voor de fit gebruikte middenpunt**; -- weigert te grote fouten (>12 px), spreiding (>18 px) en onbruikbare meetgeometrie; -- bewaart de bestaande `calibration.yaml` als een gedateerde `.bak` voordat - een nieuwe, complete versie wordt geplaatst. - -De output bevat alleen de vier basisgrenzen en zes affine coëfficiënten in -een ESPHome-substitutions-map. `device.yaml` importeert deze als calibration- -package. Verander de coëfficiënten niet op gevoel en voeg geen tweede correctie -in de driver toe. De `raw=`-logs bevatten de gefilterde **fysieke ADC-waarden**, -ook als al een correctie is ingesteld; opnieuw kalibreren telt dus niet dubbel op. - -Een geslaagde fit op bestaande data is nog geen geslaagde fysieke test. - -## Meting B: de geflashte uitkomst onafhankelijk testen - -Flash de correctie, nog steeds met het meetscherm: - -```sh -python -m esphome -s CALIBRATION_ON_BOOT true run device.yaml --device -``` - -Sluit de loglezer met Ctrl+C en verzamel **nieuwe tikken**: - -```sh -python tools/calibrate.py capture --port --device-name display-keuken --output measurements-after.json -python tools/calibrate.py verify measurements-after.json -``` - -`verify` gebruikt de echte `native=`-coördinaten van de firmware, draait die -naar de schermcoördinaten en vergelijkt ze met de doelen. Hij voert **geen -nieuwe fit** uit. Alle vijf punten moeten slagen. Gebruik niet per ongeluk -het bestand van vóór de flash voor deze controle. - -Lukt dit niet? Bekijk welk punt faalt, controleer de volgorde/druk, meet opnieuw -en lees [PROBLEMEN.md](PROBLEMEN.md). Verhoog niet alleen de tolerantie om de -test groen te krijgen. Rapporteer een resterende hardwareafwijking eerlijk. - -## Terug naar normale bediening - -```sh -python -m esphome run device.yaml --device -``` - -Hier is **geen** `-s CALIBRATION_ON_BOOT true` opgegeven. In `device.yaml` blijft -de instelling `false`. Controleer de werkelijke tegels en navigatie volgens -[ACCEPTATIE.md](ACCEPTATIE.md). - -Als de kalibratiemodus uitsluitend via de API was geopend, kun je hem sluiten: - -```sh -python diagnostics/control_ui.py end_touch_diagnostics --host display-keuken.local --name display-keuken -``` - -Bewaar beide meetbestanden en `calibration.yaml` bij dit fysieke scherm. -Na vervangen van het paneel opnieuw beginnen; geef alleen de neutrale -starterbestanden aan een volgende eigenaar. diff --git a/docs/CYD_UPGRADE_027.md b/docs/CYD_UPGRADE_027.md index 46e2b73..fa10fe5 100644 --- a/docs/CYD_UPGRADE_027.md +++ b/docs/CYD_UPGRADE_027.md @@ -1,66 +1,66 @@ -# Upgrade bestaand CYD-paneel naar runtime 0.2.7 +# Upgrading an existing CYD panel to runtime 0.2.7 -Bord: ESP32-2432S028, ESP32-D0WD-V3; MAC b0:cb:d8:e7:bd:7c. -Bestaande identiteit CYD 2.8in Display / cyd-2432s028 behouden. USB-poort werd -vooraf met esptool geïdentificeerd; Guition zat op een andere USB-poort. +Board: ESP32-2432S028, ESP32-D0WD-V3; MAC b0:cb:d8:e7:bd:7c. +Kept the existing identity CYD 2.8in Display / cyd-2432s028. The USB port was +identified beforehand with esptool; the Guition was on a different USB port. -Eigen profiel: genegeerd easy-cyd-device.yaml, met de bestaande lokale -calibration.yaml en API/OTA/wifi-secretverwijzingen. Een back-up van de eerste -64 KiB (partitie/configuratieruimte) is lokaal opgeslagen. Een volledige snelle -uitlezing gaf een seriële fout; de volledige langzame uitlezing is afgebroken. -Dit is dus geen volledige oude-firmwareback-up. USB-upload op 115200 geslaagd; -verdere correctie via de geverifieerde OTA-host. +Own profile: gitignored easy-cyd-device.yaml, with the existing local +calibration.yaml and API/OTA/Wi-Fi secret references. A backup of the first +64 KiB (partition/configuration space) is stored locally. A full fast +read gave a serial error; the full slow read was aborted. +So this is not a complete old-firmware backup. USB upload at 115200 succeeded; +further correction via the verified OTA host. -De bestaande affine kalibratie is eenmalig in preference-key 0x43594403 gezet. -Daarna gebruikt het definitieve profiel de standaard opstart en bestaande NVS. +The existing affine calibration was written once into preference key 0x43594403. +After that, the final profile uses the standard boot and existing NVS. -## Tijdens de upgrade gevonden +## Found during the upgrade -Een tijdelijk on_boot-list in het lokale migratieprofiel verving het on_boot- -object van het pakket. Daardoor ontbrak de normale UI-initialisatie en crashte -het begin van de renderdiagnose. De tijdelijke import is verwijderd nadat de -kalibratie was opgeslagen. In de definitieve gegenereerde C++ zijn runtime-bind, -light_controls::setup en screen_calibration::setup weer aanwezig. De volledige -renderdiagnose slaagt daarna (tien paginacontroles en vijftig overlays). -Gebruik dit tijdelijke migratieprofiel niet voor toekomstige flashes. +A temporary on_boot list in the local migration profile replaced the package's +on_boot object. As a result, the normal UI initialization was missing and +the start of the render diagnostic crashed. The temporary import was removed after the +calibration had been saved. In the final generated C++, runtime-bind, +light_controls::setup, and screen_calibration::setup are present again. The full +render diagnostic then passes (ten page checks and fifty overlays). +Don't use this temporary migration profile for future flashes. -HA ontdekte de nieuwe Tegelinstellingen-entity zonder nieuwe API-sleutel. De oude -tien entiteiten zijn overgezet; daarna heeft de gebruiker de indeling gewijzigd. -Die actuele indeling is behouden. Swipe is aan, standby 600 seconden. Bij maximaal -zes tegels blijft de paginering verborgen. +HA discovered the new Tegelinstellingen entity without a new API key. The old +ten entities were carried over; the user then changed the layout. +That current layout is preserved. Swipe is on, standby 600 seconds. With six +tiles or fewer, pagination stays hidden. -Definitieve build: ESPHome 2026.6.2, 2026-09-12 17:50:49 +0200, -config_hash=0x0a15a59f, broncommit a14a66f. +Final build: ESPHome 2026.6.2, 2026-09-12 17:50:49 +0200, +config_hash=0x0a15a59f, source commit a14a66f. -## Verificatie +## Verification -- Definitieve build en OTA-upload geslaagd; identiteit en firmwareversie via de - versleutelde API gecontroleerd. -- Renderdiagnose: tien geslaagde paginacontroles, vijftig overlaycycli en - `Light sliders: PASS`. De diagnose gebruikte de toen ingestelde twee tegels; - dit is geen fysieke swipecontrole van vier pagina's. -- Home Assistant en ESP Screen Manager melden het paneel online met versie 0.2.7. - Wijzigingen van de gebruiker in de tegelindeling blijven behouden. -- Bestaande kalibratie hergebruikt; tijdens deze upgrade is geen nieuwe - onafhankelijke fysieke kalibratiemeting uitgevoerd. +- Final build and OTA upload succeeded; identity and firmware version checked + via the encrypted API. +- Render diagnostic: ten successful page checks, fifty overlay cycles, and + `Light sliders: PASS`. The diagnostic used the two tiles configured at the time; + this is not a physical swipe check of four pages. +- Home Assistant and ESP Screen Manager report the panel online with version 0.2.7. + The user's changes to the tile layout are preserved. +- Existing calibration reused; no new independent physical calibration + measurement was performed during this upgrade. -De 0.2.7-observatie is vijftien minuten voltooid met een levende API-respons elke -dertig seconden, zonder herstart of verbindingsverlies. Vrij geheugen circa -111–119 kB. Door fysieke bediening werd tien minuten onafgebroken inactiviteit -niet bereikt; standby/ontwaken is in deze observatie niet bevestigd. +The 0.2.7 observation ran for fifteen minutes complete, with a live API response every +thirty seconds, with no restart or connection loss. Free memory around +111–119 kB. Because of physical use, ten minutes of uninterrupted inactivity +was not reached; standby/wake was not confirmed in this observation. -## Vervolg: tekstlayout 0.2.8 +## Follow-up: text layout 0.2.8 -De eigenaar meldde vervolgens afvallende titels, te vroeg afgekorte ondertitels -en een overlappende mini-slider. Stabiliteit alleen bevestigt dus niet de -bruikbaarheid van de layout. De titels hadden geen expliciete breedte en de -ondertitel nam de inhoudsbreedte van de titel over. De compacte kaart had daarnaast -onvoldoende verticale ruimte voor de extra slider. +The owner then reported titles running off the edge, subtitles truncated too early, +and an overlapping mini-slider. So stability alone doesn't confirm the +layout's usability. The titles had no explicit width, and the +subtitle inherited the title's content width. The compact card also had +insufficient vertical space for the extra slider. -Runtime 0.2.8 begrenst beide regels op de beschikbare inhoudsbreedte, vergroot die -bij een verborgen icoon en reserveert op de CYD aparte ruimte voor de schuif. -De renderdiagnose controleert nu de echte LVGL-coördinaten op afkapping buiten het -vak en overlapping van tekstregels/slider. Met de actuele vier tegels, waaronder -een lamp met mini-slider: tien paginacontroles en vijftig overlaycycli geslaagd. -De slidertest slaagt ook. Fysieke bevestiging van de nieuwe layout staat nog open. -De vijftien minuten hierboven gelden voor 0.2.7, niet voor deze vervolgbuild. +Runtime 0.2.8 bounds both lines to the available content width, expands that +when an icon is hidden, and reserves separate space for the slider on the CYD. +The render diagnostic now checks the real LVGL coordinates for clipping outside the +box and overlap between text lines/slider. With the current four tiles, including +a light with a mini-slider: ten page checks and fifty overlay cycles passed. +The slider test also passes. Physical confirmation of the new layout is still open. +The fifteen minutes above apply to 0.2.7, not to this follow-up build. diff --git a/docs/EASY_SETUP.md b/docs/EASY_SETUP.md index ecc754f..f6ab69a 100644 --- a/docs/EASY_SETUP.md +++ b/docs/EASY_SETUP.md @@ -1,260 +1,260 @@ -# Installeren en beheren vanuit ESP Screens +# Installing and managing from ESP Screens -Een nieuw scherm, van USB naar dagelijks gebruik +A new screen, from USB to everyday use -Met **ESP Screen Manager** kies je je tegels in Home Assistant. Je zoekt op -naam, apparaat, ruimte of entity-ID, zet ze in de gewenste volgorde en klikt -op **Opslaan & naar scherm**. Daarna blijft de status automatisch actueel. -Je hoeft voor andere tegels **niet opnieuw te flashen**. +With **ESP Screen Manager**, you choose your tiles in Home Assistant. You search by +name, device, room, or entity ID, arrange them in the order you want, and click +**Save & send to screen**. After that, the status stays automatically up to date. +You don't need to **reflash** for different tiles. -Dit werkt met Home Assistant OS op een 64-bits Raspberry Pi of een amd64-machine, -en een van deze exacte schermvarianten. De ESPHome-CLI zit in ESP Screen Manager; -ESPHome Device Builder is optioneel: +This works with Home Assistant OS on a 64-bit Raspberry Pi or an amd64 machine, +and one of these exact screen variants. The ESPHome CLI is included in ESP Screen Manager; +ESPHome Device Builder is optional: -| Keuze | Hardware | +| Choice | Hardware | | --- | --- | -| CYD | ESP32-2432S028, 320×240, ILI9341 en XPT2046 | -| Guition | ESP32-S3-4848S040, 480×480, ST7701S en GT911 | +| CYD | ESP32-2432S028, 320×240, ILI9341 and XPT2046 | +| Guition | ESP32-S3-4848S040, 480×480, ST7701S and GT911 | -Andere schermen met ongeveer dezelfde naam kunnen andere pinnen hebben. Gebruik -het bordprofiel dat bij de hardware hoort. Gebruik een USB-kabel die data ondersteunt. -De wallbox-relais worden niet gebruikt. +Other screens with roughly the same name can have different pins. Use +the board profile that matches the hardware. Use a USB cable that supports data. +The wallbox relays are not used. -## 1. Installeer ESP Screen Manager +## 1. Install ESP Screen Manager -1. Open **Instellingen → Apps → Installeer een app** (in oudere HA-versies: - **Instellingen → Add-ons → Add-onwinkel**). -2. Open het menu rechtsboven → **Repositories** en voeg toe: +1. Open **Settings → Apps → Install app** (on older HA versions: + **Settings → Add-ons → Add-on Store**). +2. Open the menu in the top right → **Repositories** and add: `https://github.com/MaxGramser/homeassistant_espscreen`. -3. Installeer en start **ESP Screen Manager**. Zet **Starten bij opstarten** - en **Tonen in zijbalk** aan. Open de webinterface **ESP Screens**. +3. Install and start **ESP Screen Manager**. Turn on **Start on boot** + and **Show in sidebar**. Open the **ESP Screens** web interface. -Deze app bevat de geteste ESPHome 2026.6.2-CLI en draait binnen je HA-aanmelding. -Een tweede ESPHome-beheerpagina, MQTT, blueprint of long-lived token is niet nodig. -Gebruik de GitHub-versie voor updates; een lokale test-add-on is een andere app. +This app includes the tested ESPHome 2026.6.2 CLI and runs within your HA login. +A second ESPHome management page, MQTT, blueprint, or long-lived token is not needed. +Use the GitHub version for updates; a local test add-on is a separate app. -## 2. Nieuw scherm: aansluiten en installeren +## 2. New screen: connecting and installing -1. Sluit het scherm met een **USB-datakabel** aan op de machine waarop Home - Assistant draait. Bij meerdere borden: sluit ze voor de eerste installatie - één voor één aan, of controleer welke poort bij dit scherm hoort. -2. Klik **Nieuw scherm**. Kies CYD of Guition en geef het scherm een naam, - bijvoorbeeld `Keuken`. De apparaatnaam (`keuken`) volgt daaruit; met - **aanpassen** kies je een andere. -3. Wifi: staan `wifi_ssid` en `wifi_password` al in de ESPHome `secrets.yaml`, - dan gebruikt het scherm die automatisch. Ontbreken ze, of bestaat het bestand - nog niet, dan vraagt het venster ze eenmalig en zet ESP Screens alleen de - ontbrekende regels in `secrets.yaml`; commentaar en andere secrets blijven - staan. Een `secrets.yaml` die geen geldige YAML is, herstel je eerst zelf. -4. Kies bij **Installeren via** de USB-poort van dit scherm en klik - **Installeren**. ESP Screens bewaart het profiel (`keuken.yaml`, met unieke - API- en OTA-sleutels) in de ESPHome-map, bouwt de firmware en schrijft die - via USB. Het ESPHome-log en de fase (bouwen, schrijven) staan in hetzelfde - venster; een eerste build duurt op een Raspberry enkele minuten. Je mag het - venster sluiten: de installatie loopt door en staat bij heropenen weer klaar. -5. Na **Klaar** toont het venster de API-sleutel met een kopieerknop en de - koppelstappen uit hoofdstuk 3. Mislukt de build, dan staat het log open en - kun je **Opnieuw proberen**. +1. Connect the screen with a **USB data cable** to the machine running Home + Assistant. With multiple boards: connect them one at a time for the first + installation, or check which port belongs to this screen. +2. Click **New screen**. Choose CYD or Guition and give the screen a name, + for example `Kitchen`. The device name (`kitchen`) follows from that; use + **customize** to choose a different one. +3. Wi-Fi: if `wifi_ssid` and `wifi_password` are already in the ESPHome `secrets.yaml`, + the screen uses them automatically. If they're missing, or the file doesn't + exist yet, the window asks for them once and ESP Screens only adds the + missing lines to `secrets.yaml`; comments and other secrets are left + alone. If `secrets.yaml` isn't valid YAML, fix that yourself first. +4. Under **Install via**, choose this screen's USB port and click + **Install**. ESP Screens stores the profile (`kitchen.yaml`, with unique + API and OTA keys) in the ESPHome folder, builds the firmware, and writes it + over USB. The ESPHome log and the current phase (building, writing) are in the same + window; a first build takes a few minutes on a Raspberry Pi. You can close the + window: the installation keeps running and picks back up when you reopen it. +5. After **Done**, the window shows the API key with a copy button and the + pairing steps from chapter 3. If the build fails, the log stays open and + you can **Retry**. -Elk scherm krijgt zijn eigen profiel: vier schermen betekent vier keer **Nieuw -scherm** met vier verschillende namen. Het gedeelde bordpakket is voor alle -schermen gelijk; het profiel bevat alleen naam, sleutels en de wifi-verwijzing. -Bewaar dat profiel en gebruik het opnieuw bij updates. Voor een bestaand scherm -opnieuw **Nieuw scherm** doorlopen maakt nieuwe sleutels aan en is niet de -updateroute. +Every screen gets its own profile: four screens means going through **New +screen** four times, with four different names. The shared board package is the +same for every screen; the profile only holds the name, keys, and the Wi-Fi reference. +Keep that profile and reuse it for updates. Going through **New screen** +again for an existing screen generates new keys and is not the +update route. -Geen USB-poort in de lijst? Een kabel aan je laptop is niet zichtbaar voor de -Raspberry. Kies dan **Later · alleen het profiel bewaren**: het profiel staat -daarna in dezelfde map die ESPHome Device Builder gebruikt, dus je kunt het daar -openen en vanuit je browser flashen (**Install → Plug into this computer**), of -het later via **Firmware & USB** alsnog aan de HA-machine installeren. +No USB port in the list? A cable plugged into your laptop isn't visible to the +Raspberry Pi. In that case, choose **Later · save profile only**: the profile then +sits in the same folder ESPHome Device Builder uses, so you can open it there +and flash from your browser (**Install → Plug into this computer**), or +install it to the HA machine later via **Firmware & USB**. -**CYD:** bij de eerste start verschijnt de kalibratie. Tik het zichtbare kruisje -drie keer rustig aan, houd elke tik kort vast en volg steeds het volgende kruisje. -Er zijn vijf posities. Het midden controleert de nauwkeurigheid. Bij een mislukte -meting vraagt het scherm opnieuw te beginnen. De correctie wordt lokaal opgeslagen -en blijft bij OTA-updates behouden. Via de HA-apparaatknop **Touch kalibreren** kun -je later opnieuw meten. Voor diagnose en de oudere handmatige installatie: -[CALIBREREN.md](CALIBREREN.md). +**CYD:** calibration appears on first boot. Calmly tap the visible crosshair +three times, hold each tap briefly, and follow each next crosshair in turn. +There are five positions. The center checks accuracy. On a failed +measurement, the screen asks you to start over. The correction is stored locally +and survives OTA updates. Use the HA device button **Calibrate touch** to +measure again later. For diagnostics and the older manual installation, see: +[CALIBRATING.md](CALIBRATING.md). -**Guition:** de GT911-touchmapping zit in het bordprofiel; er is geen ADC-kalibratie. +**Guition:** the GT911 touch mapping is baked into the board profile; there's no ADC calibration. -## 3. Koppel het scherm aan Home Assistant +## 3. Pair the screen with Home Assistant -Dit gebeurt in Home Assistant zelf, buiten ESP Screens. Zolang een profiel nog -niet aan Home Assistant is toegevoegd, staat het links onder **Mijn schermen** -als kaart *nog niet in Home Assistant*, met de knop **Open Apparaten & diensten** -en **Kopieer API-sleutel**; het klaar-scherm van **Nieuw scherm** heeft dezelfde -knop. De kaart verdwijnt zodra het scherm in de lijst staat. +This happens in Home Assistant itself, outside ESP Screens. As long as a profile +hasn't been added to Home Assistant yet, it appears on the left under **My screens** +as a *not yet in Home Assistant* card, with an **Open Devices & services** +button and **Copy API key**; the done screen of **New screen** has the same +button. The card disappears once the screen is in the list. -1. Open **Instellingen → Apparaten & diensten**. Voeg het ontdekte ESPHome-apparaat - toe. Niet ontdekt? Voeg de integratie **ESPHome** handmatig toe met het IP-adres - van het scherm, poort 6053. -2. Vraagt HA om een encryptiesleutel? Plak de API-sleutel die het venster na de - installatie toont (ook terug te vinden als **api → encryption → key** in het - profiel). Gebruik niet het OTA-wachtwoord. -3. Open bij de ESPHome-integratie **Configureren** en zet **Allow the device to - perform Home Assistant actions** aan. Zonder deze toestemming verschijnen - waarden wel, maar kan het scherm lampen en apparaten niet bedienen. -4. Open ESP Screens. Het scherm verschijnt binnen ongeveer 30 seconden. +1. Open **Settings → Devices & services**. Add the discovered ESPHome device. + Not discovered? Manually add the **ESPHome** integration with the screen's + IP address, port 6053. +2. Does HA ask for an encryption key? Paste the API key the window shows after + installation (also found as **api → encryption → key** in the + profile). Don't use the OTA password. +3. On the ESPHome integration, open **Configure** and enable **Allow the device to + perform Home Assistant actions**. Without this permission, values still show up, + but the screen can't control lights and devices. +4. Open ESP Screens. The screen appears within about 30 seconds. -## 4. Kies en wijzig je tegels +## 4. Choose and edit your tiles -Tip: zet in je ESPHome-YAML onder `wifi:` de regel `power_save_mode: none` (nieuwe -wizard-YAML's hebben hem al); het scherm hangt aan het lichtnet en reageert dan zonder -wifi-slaapvertraging op Home Assistant. +Tip: in your ESPHome YAML, add the line `power_save_mode: none` under `wifi:` (new +wizard YAMLs already have it); the screen is mains-powered, so this keeps it responding to +Home Assistant without Wi-Fi sleep delay. -Selecteer je scherm, vul de titel in en zoek entiteiten. De kiezer heeft -domeinfilters met gekleurde iconen. Je kunt maximaal twintig tegels toevoegen. -Het schermvoorbeeld toont hun plaats: twee kolommen, zes plekken per pagina, -maximaal acht pagina's. Elke tegel heeft een vaste plek die alleen verandert als -jij hem versleept; lege plekken blijven leeg, waar je ze ook laat. Sleep een tegel -op een lege plek en hij staat daar; sleep hem op een andere tegel en die twee -wisselen (de ander neemt de vrijgekomen plek, of anders de dichtstbijzijnde vrije -plek); de rest blijft staan. Tijdens het slepen zie je in het voorbeeld al waar -alles komt; onder de laatste pagina staat dan een lege pagina klaar. **Pagina -toevoegen** maakt een lege pagina die bewaard blijft; een lege pagina heeft -**Pagina weghalen**. Klik op een lege plek om de volgende tegel uit de kiezer -daar te zetten. Met de pijltjestoetsen verplaats je een gefocuste tegel. -Klik op een tegel voor een eigen naam en **Bediening & weergave instellen**: -klikgedrag, een mini-slider, een grote waarde, een grafiek (sensoren), een -weersvoorspelling (weer) of de breedte **Dubbelbreed**. Een dubbelbrede tegel van -een klimaat, schakelaar, lamp, ventilator, stofzuiger, zonwering, mediaspeler, -getal, keuzelijst, kookwekker, scène, script of knop krijgt rechts **directe -bediening** zoals de rijen in Home Assistant (bijvoorbeeld temperatuur − / +, -open/stop/dicht, volume met dempen, een toggle); kies onder **Directe bediening -op de tegel** welke set, of **Geen** (firmware 0.2.19+). **Bediening openen** op -een weertegel toont de weerkaart met de komende uren en dagen (regen inbegrepen); -op een klimaattegel de kaart met aan/uit-knop en de modus-, ventilator- en -zwenkstanden. De ingebouwde **Klok** -staat bovenaan de kiezer; zon, kookwekkers en personen vind je via de filters. Kies bij -**Pastel achtergrond** een eigen kleur met donkere tekst; **Standaard** herstelt -de normale weergave en **Geen** laat de kaart weg, zodat de inhoud even groot -direct op de schermachtergrond staat (firmware 0.2.16+). Hiervoor is firmware 0.2.10+ nodig. -Toevoegen zonder gekozen plek vult de eerste vrije plek. Vaste plekken en lege -plekken werken op het scherm vanaf firmware 0.2.26; oudere firmware schuift de -tegels aan tot de eerste vrije plek, de editor meldt dat onder het voorbeeld. -Het voorbeeld toont de indeling, geen live sensorwaarden. -Klik **Opslaan & naar scherm** om je wijzigingen door te sturen. +Select your screen, fill in the title, and search for entities. The picker has +domain filters with colored icons. You can add up to twenty tiles. +The screen preview shows their placement: two columns, six slots per page, +up to eight pages. Every tile has a fixed slot that only changes if +you drag it; empty slots stay empty, wherever you leave them. Drag a tile +onto an empty slot and it stays there; drag it onto another tile and the two +swap (the other tile takes the freed-up slot, or otherwise the nearest free +slot); everything else stays put. While dragging, the preview already shows where +everything will land; an empty page then stands ready below the last page. **Add +page** creates an empty page that's kept; an empty page gets +**Remove page**. Click an empty slot to place the next tile from the picker +there. Use the arrow keys to move a focused tile. +Click a tile for a custom name and **Configure control & display**: +click behavior, a mini-slider, a large value, a graph (sensors), a +weather forecast (weather), or the **Double-width** setting. A double-width tile for +a climate, switch, light, fan, vacuum, cover, media player, +number, select, timer, scene, script, or button gets **direct +control** on the right, like the rows in Home Assistant (for example temperature − / +, +open/stop/close, volume with mute, a toggle); under **Direct control +on the tile**, choose which set, or **None** (firmware 0.2.19+). **Open control** on +a weather tile shows the weather card with the coming hours and days (rain included); +on a climate tile, the card with an on/off button and the mode, fan, and +swing settings. The built-in **Clock** +sits at the top of the picker; find sun, timers, and people via the filters. Under +**Pastel background**, choose a custom color with dark text; **Default** restores +the normal look, and **None** drops the card, so the content sits the same size +directly on the screen background (firmware 0.2.16+). This requires firmware 0.2.10+. +Adding without a chosen slot fills the first free slot. Fixed slots and empty +slots work on the screen from firmware 0.2.26 on; older firmware shifts the +tiles up to the first free slot, and the editor notes that below the preview. +The preview shows the layout, not live sensor values. +Click **Save & send to screen** to send your changes. -- Lamp, switch, input_boolean en fan: tik om aan/uit te zetten. -- Lamp lang indrukken: helderheid, regenboogkleur en wittemperatuur, voor zover - de lamp die functies ondersteunt. -- Climate, vacuum en cover: tik om de bedieningskaart te openen. -- Fan lang indrukken: snelheid als het apparaat percentages ondersteunt. -- Scene/script: tik om uit te voeren; button/input_button: tik om in te drukken. -- Sensor: tik voor de historiekaart; stel bij de tegel 1, 6 of 24 uur in. -- Binary sensor: status bekijken. Select/input_select: open het keuzemenu. +- Light, switch, input_boolean, and fan: tap to turn on/off. +- Long press a light: brightness, rainbow color, and white temperature, as far as + the light supports those features. +- Climate, vacuum, and cover: tap to open the control card. +- Long press a fan: speed, if the device supports percentages. +- Scene/script: tap to run; button/input_button: tap to press. +- Sensor: tap for the history card; set 1, 6, or 24 hours on the tile. +- Binary sensor: view status. Select/input_select: open the picker menu. -Vanaf firmware 0.2.7 passen twintig tegels op maximaal vier pagina’s. Oudere -firmware houdt de limiet van tien totdat je bijwerkt. Via **Algemene instellingen -→ Vegen tussen pagina’s** kun je vegen inschakelen. Op de Guition (firmware -0.2.24+) veeg je dan vanaf de linker- of rechterrand naar binnen, zoals -terug-vegen op een telefoon; langzaam of snel, en een veeg die midden op het -scherm begint doet niets, zodat tikken en slepen op tegels nooit per ongeluk van -pagina wisselen. Op de CYD blijft het een snelle veeg over het scherm. Sliders -bedienen alleen hun waarde; detailmenu’s en standby wisselen niet van pagina. +From firmware 0.2.7, twenty tiles fit across up to four pages. Older +firmware keeps the limit of ten until you update. Under **General settings +→ Swipe between pages**, you can enable swiping. On the Guition (firmware +0.2.24+), you then swipe inward from the left or right edge, like the +back-swipe gesture on a phone; slow or fast, and a swipe starting in the +middle of the screen does nothing, so tapping and dragging tiles never +accidentally changes pages. On the CYD, it stays a quick swipe across the screen. Sliders +only control their value; detail menus and standby don't change pages. -Er staan zes tegels op een pagina. Bij maximaal zes verdwijnen Vorige/Volgende. -De standby-tijd is standaard tien minuten. Bij offline apparaten blokkeert het -scherm acties. Valt de verbinding met Home Assistant weg, dan meldt het scherm -direct "HA niet verbonden"; stuurt de app twee rondes (circa vijf minuten) niets, -dan "ESP Screens niet actief". In beide gevallen is de bediening geblokkeerd -totdat de gegevens opnieuw ontvangen zijn. +Six tiles sit on a page. With six or fewer, Previous/Next disappear. +The default standby time is ten minutes. For offline devices, the screen blocks +actions. If the connection to Home Assistant drops, the screen immediately shows +"HA not connected"; if the app sends nothing for two rounds (about five minutes), +it shows "ESP Screens not active". In both cases, control is blocked +until data is received again. -Je mag indelingen opslaan terwijl een scherm offline is. De app verstuurt ze -zodra het scherm terugkomt. De app moet blijven draaien voor actuele tegeldata. +You can save layouts while a screen is offline. The app sends them +as soon as the screen comes back. The app must keep running for current tile data. -## 5. Updates zonder je instellingen kwijt te raken +## 5. Updates without losing your settings -| Wat verandert? | Wat doe je? | Wat blijft behouden? | +| What changes? | What do you do? | What's kept? | | --- | --- | --- | -| Andere entiteiten, namen of volgorde | Opslaan in ESP Screens | Wifi, sleutels, kalibratie | -| Nieuwe beheerpagina/appversie | Appwinkel → ESP Screen Manager → Update | Alle indelingen in `/data/screens.json` | -| Nieuwe schermfunctie/kaart | Knop **Bijwerken** bij het scherm, of vinkje **Elke nacht automatisch bijwerken** (handmatig: Firmware & USB → Wifi / OTA) | Eigen YAML, sleutels en CYD-kalibratie; app stuurt tegels opnieuw | +| Different entities, names, or order | Save in ESP Screens | Wi-Fi, keys, calibration | +| New management page/app version | App store → ESP Screen Manager → Update | All layouts in `/data/screens.json` | +| New screen feature/card | The **Update** button on the screen, or the **Update automatically every night** checkbox (manually: Firmware & USB → Wi-Fi / OTA) | Own YAML, keys, and CYD calibration; the app resends tiles | -De eigen YAML verwijst naar de firmwarepakketten in `main`. Bij een nieuwe build -haalt ESPHome de nieuwste gepubliceerde pakket- en componentcode op. Je vervangt -je eigen YAML dus niet door een nieuw downloadbestand. Wifi, naam en sleutels -staan buiten het gedeelde pakket en blijven gelijk. +The device's own YAML references the firmware packages on `main`. On a new build, +ESPHome fetches the latest published package and component code. So you don't +replace your own YAML with a new downloaded file. Wi-Fi, name, and keys +live outside the shared package and stay the same. -Maak vóór updates een Home Assistant-back-up inclusief ESP Screen Manager en -de eigen ESPHome-configuraties. **Verwijderen/herinstalleren** van een app is niet hetzelfde -als updaten; daarmee kun je de gegevensmap wissen. Houd de apparaatnaam en de -entity-ID van **Tegelinstellingen** gelijk, zodat de bestaande indeling gekoppeld blijft. +Make a Home Assistant backup before updates, including ESP Screen Manager and +the device's own ESPHome configurations. **Removing/reinstalling** an app is not the same +as updating; that can wipe the data folder. Keep the device name and the +entity ID of **Tile settings** the same, so the existing layout stays linked. -Voor een nieuwe ondersteunde kaart update je eerst de app, daarna de firmware. -De beheerder moet protocol- en gegevensmigraties achterwaarts compatibel maken; -zie [RELEASING.md](RELEASING.md). Voor terugrollen kun je `ref: main` in je eigen -YAML tijdelijk vervangen door een eerdere releasetag, zonder de sleutels te wijzigen. +For a newly supported card, update the app first, then the firmware. +The maintainer keeps protocol and data migrations backward compatible; +see [RELEASING.md](RELEASING.md). To roll back, you can temporarily replace `ref: main` +in your own YAML with an earlier release tag, without changing the keys. -## Als iets niet werkt +## If something doesn't work -- **Geen scherm in de lijst:** controleer of de nieuwe Easy Setup-firmware draait, - de ESPHome-integratie verbonden is en de tekstentiteit **Tegelinstellingen** - niet uitgeschakeld is. De oude handmatige firmware publiceert die standaard niet. -- **Wel tegels, geen acties:** geef het apparaat toestemming voor HA-acties. -- **Niet beschikbaar:** controleer of de geselecteerde entiteit in HA bestaat en - beschikbaar is. Een hernoemde entity-ID moet je opnieuw kiezen. -- **Geen OTA:** controleer wifi/IP en het oorspronkelijke OTA-wachtwoord. Gebruik - zo nodig dezelfde eigen YAML via USB. Genereer geen nieuwe identiteit. -- **Build mislukt:** lees de eerste fout, controleer ESPHome-versie en internet voor - GitHub/fontdownloads. Gebruik bij te weinig Raspberry-geheugen tijdelijk een - krachtigere computer voor compileren; de YAML blijft hetzelfde. -- **Bestaand handmatig scherm migreren:** bewaar de oude YAML en neem de bestaande - apparaatnaam, API-sleutel en OTA-wachtwoord over in het nieuwe installatieprofiel. - Kies daarna de tegels in de app. De oude vaste tegel-substituties worden niet - automatisch in de nieuwe beheerpagina geïmporteerd. +- **No screen in the list:** check that the new Easy Setup firmware is running, + the ESPHome integration is connected, and the **Tile settings** text entity + isn't disabled. The old manual firmware doesn't publish that by default. +- **Tiles show but no actions:** grant the device permission for HA actions. +- **Unavailable:** check that the selected entity exists in HA and is + available. A renamed entity ID needs to be chosen again. +- **No OTA:** check Wi-Fi/IP and the original OTA password. If needed, + use the same own YAML over USB. Don't generate a new identity. +- **Build fails:** read the first error, check the ESPHome version and internet for + GitHub/font downloads. If the Raspberry Pi is low on memory, temporarily use a + more powerful computer to compile; the YAML stays the same. +- **Migrating an existing manual screen:** keep the old YAML and carry over the + existing device name, API key, and OTA password into the new installation profile. + Then choose the tiles in the app. The old fixed tile substitutions aren't + automatically imported into the new management page. -HA Container zonder Supervisor heeft geen appwinkel. Deze installatiehandleiding -richt zich op Home Assistant OS; de ontwikkelserver is geen productieroute voor -een los openbaar portal. +HA Container without Supervisor has no App store. This installation guide +targets Home Assistant OS; the development server is not a production route for +a standalone public portal. -Gebruikte HA-mechanismen: [Ingress](https://developers.home-assistant.io/docs/apps/presentation/), -[interne HA-API](https://developers.home-assistant.io/docs/apps/communication/) en -[ESPHome-pakketten](https://esphome.io/components/packages/). +HA mechanisms used: [Ingress](https://developers.home-assistant.io/docs/apps/presentation/), +[the internal HA API](https://developers.home-assistant.io/docs/apps/communication/), and +[ESPHome packages](https://esphome.io/components/packages/). -## Scherminstellingen aanpassen (vanaf 0.1.2) +## Adjusting screen settings (from 0.1.2) -Update ESP Screen Manager naar 0.1.2 en installeer één keer de nieuwe firmware -via je **bestaande** ESPHome-apparaat → Install → Wirelessly. Behoud de eigen YAML -met wifi en sleutels. Open daarna het scherm in ESP Screen Manager en klap -**Scherminstellingen** open. Klik na aanpassen op **Opslaan & naar scherm**. -Hierna vragen wijzigingen aan deze instellingen geen nieuwe firmwareflash. +Update ESP Screen Manager to 0.1.2 and install the new firmware once +via your **existing** ESPHome device → Install → Wirelessly. Keep your own YAML +with Wi-Fi and keys. Then open the screen in ESP Screen Manager and expand +**Screen settings**. After adjusting, click **Save & send to screen**. +After this, changes to these settings don't need a new firmware flash. -| Instelling | Mogelijkheden | Standaard | +| Setting | Options | Default | |---|---|---| -| Automatisch standby | Aan/uit | Aan | -| Standby na | 1–1440 minuten na de laatste aanraking | 10 minuten | -| Helderheid normaal | 5–100% | 100% | -| Helderheid standby | 0–100%, maximaal normale helderheid | 20% | -| Nachtstand | Aan/uit; geldt tijdens standby | Aan | -| Begin/einde nacht | Uur en minuut, ook over middernacht | 22:00–07:00 | -| Helderheid nacht | 0–100%, maximaal normale helderheid | 10% | -| Klok tonen | Aan/uit | Aan | -| Tijdnotatie | 24 of 12 uur, zonder AM/PM | 24 uur | -| Terug naar pagina 1 | Sluit ook detailmenu’s bij standby | Uit | -| Vegen tussen pagina’s | Native horizontale swipe, firmware 0.2.7+ | Uit | -| Guition draaien | 0°, 90°, 180°, 270°, firmware 0.2.9+ | 0° | +| Auto standby | On/off | On | +| Standby after | 1–1440 minutes after the last touch | 10 minutes | +| Normal brightness | 5–100% | 100% | +| Standby brightness | 0–100%, capped at normal brightness | 20% | +| Night mode | On/off; applies during standby | On | +| Night start/end | Hour and minute, can span midnight | 22:00–07:00 | +| Night brightness | 0–100%, capped at normal brightness | 10% | +| Show clock | On/off | On | +| Time format | 24 or 12 hour, without AM/PM | 24 hour | +| Return to page 1 | Also closes detail menus on standby | Off | +| Swipe between pages | Native horizontal swipe, firmware 0.2.7+ | Off | +| Guition rotation | 0°, 90°, 180°, 270°, firmware 0.2.9+ | 0° | -Nachturen gebruiken de tijdzone van het ESPHome-apparaat en de tijd uit HA. -Zonder geldige tijd gebruikt het scherm de gewone standbyhelderheid; gelijke -begin- en eindtijd schakelen het nachtvenster uit. Op 0% gaat alleen de -achtergrondverlichting uit: dit is geen deep sleep en geen schermbeveiliging. -De eerste tik wekt het scherm zonder een apparaat te bedienen. +Night hours use the ESPHome device's timezone and the time from HA. +Without a valid time, the screen uses the regular standby brightness; matching +start and end times turn the night window off. At 0%, only the +backlight turns off: this is not deep sleep and not a screensaver. +The first tap wakes the screen without controlling a device. -De add-on bewaart alles per scherm in zijn permanente gegevens. Het scherm bewaart -de laatst ontvangen instellingen ook in preferences; ESPHome bundelt die -schrijfacties (normaal maximaal een minuut). Trek daarom niet direct na opslaan -de voeding los. Bestaande CYD-kalibratie, tegels, API- en OTA-sleutels blijven staan. -Een offline scherm krijgt wijzigingen zodra het terugkomt. Gewone HA-statusupdates -wekken het scherm niet en veranderen de standbytimer niet. +The add-on stores everything per screen in its persistent data. The screen also +stores the last received settings in preferences; ESPHome batches those +write operations (normally up to a minute). So don't unplug the power right after +saving. Existing CYD calibration, tiles, API, and OTA keys are preserved. +An offline screen gets the changes as soon as it comes back. Regular HA status +updates don't wake the screen and don't reset the standby timer. -Bij oudere firmware toont de beheerpagina dat eerst een update nodig is. -De tegels blijven bruikbaar. Handmatige YAML-profielen blijven hun substitutions -gebruiken; voor beheer via deze app gebruik je het Easy Setup-pakket. +With older firmware, the management page shows that an update is needed first. +The tiles remain usable. Manual YAML profiles keep using their substitutions; +to manage through this app, use the Easy Setup package. -De huidige Guition gebruikt de native ST7701S-configuratie; zie -[de hardwarevergelijking](GUITION_FACTORY_REFERENCE.md). Instellingen en -tegelkleuren veranderen geen paneeltimings. Controleer beeld en touch fysiek. +The current Guition uses the native ST7701S configuration; see +[the hardware comparison](GUITION_FACTORY_REFERENCE.md). Settings and +tile colors don't change panel timings. Physically check the display and touch. diff --git a/docs/GUITION.md b/docs/GUITION.md index 0db53dd..b67c4e8 100644 --- a/docs/GUITION.md +++ b/docs/GUITION.md @@ -1,173 +1,173 @@ # 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. +This branch adds a separate profile for the **Guition 4848S040** with an +ST7701S RGB display and **GT911 capacitive touch**, ESP32-S3, 16 MB flash, and +8 MB octal PSRAM. The name/2mm wall plate also describes the enclosure; always +check the electronics. This profile doesn't configure any relay. -Hardwarebron: [ESPHome Guition-bordbeschrijving](https://devices.esphome.io/devices/guition-esp32-s3-4848s040/) -en de ingebouwde [ST7701S-driver](https://esphome.io/components/display/st7701s/). -De paneelconfiguratie volgt de originele, fysiek schoon bevonden fabrikantdemo; -zie [de vergelijking](GUITION_FACTORY_REFERENCE.md). Er is geen lokale RGB-driver -of periodiek herstel-script nodig. De GT911 gebruikt ongemirrorde coördinaten. +Hardware source: the [ESPHome Guition board writeup](https://devices.esphome.io/devices/guition-esp32-s3-4848s040/) +and the built-in [ST7701S driver](https://esphome.io/components/display/st7701s/). +The panel configuration follows the original manufacturer demo, found physically clean; +see [the comparison](GUITION_FACTORY_REFERENCE.md). No local RGB driver +or periodic recovery script is needed. The GT911 uses unmirrored coordinates. -## Bestanden +## Files -- `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. +- `guition-4848s040.yaml`: neutral hardware + 480×480 interface. +- `guition-device.example.yaml`: personal device/tile settings. +- `guition-device.yaml`: your local profile, outside Git. +- `secrets.yaml`: local Wi-Fi/API/OTA settings, outside Git. +- `tools/verify_gt911.py`: physical pixel check; no resistive ADC calibration. -Het 2,8-inch CYD-profiel blijft apart. Flash dat niet naar de Guition en neem -geen `calibration.yaml` of XPT2046-correctie over. +The 2.8-inch CYD profile stays separate. Don't flash that to the Guition, and don't +carry over `calibration.yaml` or the XPT2046 correction. ## Interface -### Montage en schermrotatie (runtime 0.2.9+) +### Mounting and screen rotation (runtime 0.2.9+) -Installeer Guition-firmware 0.2.9 en ESP Screen Manager 0.2.9. Open het scherm -in de beheerpagina, ga naar **Scherminstellingen** en kies bij **Scherm draaien** -0°, 90°, 180° of 270°, met de klok mee. Klik op opslaan. Een firmwareflash is -daarna niet meer nodig om de hoek te wijzigen. De instelling blijft in de appdata -en op het scherm bewaard na een herstart. +Install Guition firmware 0.2.9 and ESP Screen Manager 0.2.9. Open the screen +in the management page, go to **Screen settings**, and choose 0°, 90°, 180°, +or 270° (clockwise) under **Rotate screen**. Click save. After that, a +firmware flash is no longer needed to change the angle. The setting is kept +in the app data and on the screen after a restart. -Dit gebruikt de native ESPHome/LVGL-rotatie voor beeld én touch, zonder wijzigingen -aan de paneelinitialisatie of GT911-spiegeling. Controleer na montage de vier -hoeken en de navigatie fysiek; een renderproef test geen aanrakingen. De optie is -alleen zichtbaar voor de Guition nadat zijn nieuwe firmware door HA is ontdekt. -De CYD blijft op zijn bestaande oriëntatie met zijn eigen kalibratie. +This uses native ESPHome/LVGL rotation for both display and touch, with no changes +to the panel initialization or GT911 mirroring. After mounting, physically check the +four corners and navigation; a render test doesn't test touches. The option is +only visible for the Guition once its new firmware has been discovered by HA. +The CYD stays on its existing orientation with its own calibration. -Zes tegels van **218 × 108 pixels** per pagina, 12px tussenruimte en een -aparte navigatiestrook. In ESP Screen Manager passen twintig tegels op maximaal -vier pagina’s; tot zes verdwijnt de navigatie. De huidige interface heeft een -lichtgrijze achtergrond, witte kaarten en gekleurde domeiniconen. Vanaf 0.2.10 kun -je per tegel een pastel achtergrond kiezen met donkere tekst. Standby begint -standaard na tien minuten zonder aanraking en is instelbaar in de beheerpagina. -Vanaf 0.2.13 dimt de backlight via de LEDC-hardwarefader (`backlight_fade.h`, -1,5 s naar standby, 80 ms wakker): de volledige LVGL-herteken bij standby -onderbreekt de overgang dan niet meer. ESPHome's lichtstatus wordt na de fade -gesynchroniseerd, zodat de entity in HA en latere overgangen kloppen. +Six tiles of **218 × 108 pixels** per page, 12px spacing, and a +separate navigation strip. In ESP Screen Manager, twenty tiles fit across up to +four pages; navigation disappears at six or fewer. The current interface has a +light gray background, white cards, and colored domain icons. From 0.2.10, you +can choose a pastel background with dark text per tile. Standby starts +after ten minutes without touch by default and is adjustable in the management page. +From 0.2.13, the backlight dims via the LEDC hardware fader (`backlight_fade.h`, +1.5 s to standby, 80 ms to wake): the full LVGL redraw on standby no longer +interrupts the transition. ESPHome's light state is synced after the fade, +so the entity in HA and later transitions stay correct. -De bestaande tegelacties, climatebediening en vacuumkaart zijn behouden. -Alle kaarttypen zijn beschikbaar op alle twintig runtime-posities. Alleen in -het oude handmatige profiel blijft de vacuumkaart op tegel 6 en hebben posities -8/10 beperkingen; zie [TEGELS.md](TEGELS.md). +The existing tile actions, climate control, and vacuum card are preserved. +All card types are available at all twenty runtime positions. Only in +the old manual profile does the vacuum card stay on tile 6 and do positions +8/10 have limitations; see [TILES.md](TILES.md). -## Nieuwe installatie +## New installation -Volg de Python-/USB-voorbereiding in [README.md](../README.md), in een verse -kopie. Maak dit profiel: +Follow the Python/USB setup in [README.md](../README.md), in a fresh +copy. Create this profile: ```sh -python tools/new_device.py --board guition --name wallbox-keuken --friendly-name "Wallbox keuken" +python tools/new_device.py --board guition --name wallbox-kitchen --friendly-name "Wallbox kitchen" ``` -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. +This creates `guition-device.yaml` and new secrets; existing files are +not overwritten. Fill in Wi-Fi and change the tile entities. With +multiple boards, use a separate folder per device. The owner's current local +Guition profile may use their existing secrets. -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: +Check the serial port and chip before uploading. On the tested board, +higher serial speeds proved unreliable when reading. Use 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: +Flash via `esphome run` if the USB connection is reliable. For explicit +115200 baud, you can use the combined factory image. Replace both the +port and the device name in the path with your own values: ```sh -python -m esptool --chip esp32s3 --port --baud 115200 write-flash 0 .esphome/build/wallbox-keuken/.pioenvs/wallbox-keuken/firmware.factory.bin +python -m esptool --chip esp32s3 --port --baud 115200 write-flash 0 .esphome/build/wallbox-kitchen/.pioenvs/wallbox-kitchen/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. +The factory image belongs to this S3 profile and starts at address **0**, unlike +some classic ESP32 images. Keep a recovery copy before replacing +existing firmware if you might need it later. Keep binaries and logs local. -## GT911 en oriëntatie testen +## Testing GT911 and orientation -De GT911 geeft pixels door. Meestal is geen kalibratie nodig; een verkeerde -rotatie/transform mag niet met de CYD-affinewizard worden weggewerkt. +The GT911 reports pixels directly. Usually no calibration is needed; a wrong +rotation/transform should not be papered over with the CYD affine wizard. -Met een werkende wifi/API-verbinding: +With a working Wi-Fi/API connection: ```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 +python diagnostics/control_ui.py touch_diagnostics --host wallbox-kitchen.local --name wallbox-kitchen +python tools/verify_gt911.py --port --output measurements-guition.json +python diagnostics/control_ui.py end_touch_diagnostics --host wallbox-kitchen.local --name wallbox-kitchen ``` -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. +Five crosshairs appear. The wizard asks for three separate taps per +point; press Enter first, then touch only the named crosshair. Error +at most 16px, spread at most 18px. The test doesn't send any HA actions. -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. +Without Wi-Fi, you can first build with `-s CALIBRATION_ON_BOOT true`, +which shows the GT911 measurement screen right after the USB upload. After +checking, rebuild/reflash without that override. This compatibility name only +opens a pixel test; it doesn't perform any ADC calibration. -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. +On the tested panel, both GT911 axes are mirrored: `TOUCH_MIRROR_X` and +`TOUCH_MIRROR_Y` are set to `true`. The default is `LVGL_ROTATION: "0"`. For a different mounting, set the +desired LVGL rotation and give the wizard the same value, for example +`--rotation 90`. Check every corner; only change the GT911 `TOUCH_SWAP_XY` / +`TOUCH_MIRROR_X` / `TOUCH_MIRROR_Y` if the physical measurement requires it. -## HA en acceptatie +## HA and acceptance -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. +Add the new device via the ESPHome integration (name/IP, port 6053, +API key from secrets). Grant permission for HA actions and use +`DIRECT_ACTIONS: "true"` once the correct entities have been checked. ```sh -python diagnostics/run_ui_test.py --host wallbox-keuken.local --name wallbox-keuken +python diagnostics/run_ui_test.py --host wallbox-kitchen.local --name wallbox-kitchen 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. +Also go through the physical checks in [ACCEPTANCE.md](ACCEPTANCE.md), using the +GT911 wizard instead of the XPT2046 calibration. Check the display, correct +touches, pagination, long press, climate/vacuum, real HA feedback, +standby, and recovery after a restart. A compile/render test doesn't replace +that physical check. The original CYD board's test outcome +says nothing about this new board. -## Gerenderde interface inspecteren +## Inspecting the rendered interface ```sh -python diagnostics/capture_ui.py --host wallbox-keuken.local --name wallbox-keuken --output diagnostics/guition-home.png +python diagnostics/capture_ui.py --host wallbox-kitchen.local --name wallbox-kitchen --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. +This only takes an LVGL snapshot on request and sends a 240×240 preview +over the encrypted API. The temporary buffer is released after about 24 seconds. +The image verifies the renderer; it doesn't prove the physical +RGB signals, screen colors, or mounting orientation are correct. The render test +also checks that visible click areas fall within the 480×480 screen. -## Herstelkopie van dit testbord +## Recovery copy of this test board -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: +The original firmware in use, including bootloader, partition table, +and NVS, is stored locally as `diagnostics/guition-original-recovery.bin`. +The lengths of the five application segments determined the needed 4,788,224 +bytes; this is not a copy of the full 16 MB flash. The stub verified the MD5 +of every block read. SHA-256 of the complete recovery file: ```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. +This copy is private, belongs only to the tested board, and is not +committed or exported. Recovery happens at address 0, with the same +115200-baud command as the factory image, but with this recovery file. -## RGB-geheugeninstellingen +## RGB memory settings -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. +The pixel clock is set to 16 MHz: on the test board, this removed the faint +flicker seen at the 12MHz setting. For the RGB bounce buffer, code and +constant data run from octal PSRAM, with a 64KB data cache and 64-byte +cache lines; RGB stream recovery happens on VSYNC. These settings +follow [Espressif's RGB LCD recommendations](https://docs.espressif.com/projects/esp-idf/en/v5.5.4/esp32s3/api-reference/peripherals/lcd/rgb_lcd.html). +Physically check for occasional glitches under Wi-Fi/render load; a +software snapshot can't prove a disturbance in the panel signal. diff --git a/docs/GUITION_FACTORY_REFERENCE.md b/docs/GUITION_FACTORY_REFERENCE.md index af3693e..e5c56e2 100644 --- a/docs/GUITION_FACTORY_REFERENCE.md +++ b/docs/GUITION_FACTORY_REFERENCE.md @@ -1,99 +1,99 @@ -# Guition 4848S040: onafhankelijke fabrikanttest +# Guition 4848S040: independent manufacturer test -Deze test onderscheidt onze ESPHome/UI van de oorspronkelijke paneelaansturing. -Een passende demo is gevonden in het fabrikantpakket dat SpotPear aanbiedt: +This test distinguishes our ESPHome/UI from the original panel driving. +A matching demo was found in the manufacturer package offered by SpotPear: -- [Verkoper met downloadlink](https://spotpear.com/ESP32-S3-4-inch-LCD-Touchscreen-Display-SHT20-Temperature-Humidity-480x480-RS485-Relay-GC9503-ST7701-FT6336U-GT911-86-TVbox/forum-answer/357.html) -- [Origineel archief](https://cdn.static.spotpear.com/uploads/picture/learn/ESP32/ESP32-S3-4inch/4.0inch_ESP32-4848S040.zip) -- [Community-uitleg voor bouwen met LVGL 8/9](https://github.com/paulhamsh/LVGL_GUITION) +- [Vendor with download link](https://spotpear.com/ESP32-S3-4-inch-LCD-Touchscreen-Display-SHT20-Temperature-Humidity-480x480-RS485-Relay-GC9503-ST7701-FT6336U-GT911-86-TVbox/forum-answer/357.html) +- [Original archive](https://cdn.static.spotpear.com/uploads/picture/learn/ESP32/ESP32-S3-4inch/4.0inch_ESP32-4848S040.zip) +- [Community write-up for building with LVGL 8/9](https://github.com/paulhamsh/LVGL_GUITION) -In het archief: +In the archive: -- `1-Demo/Demo_Arduino/1_2_4.0_LvglWidgets/4.0_LvglWidgets/`: broncode. -- `1-Demo/Demo_Arduino/Libraries/Arduino_GFX-master/`: meegeleverde paneeldriver. -- `8-Burn operation/Burn files/4.0_LvglWidgets.bin`: samengevoegde firmware, - 652640 bytes, flashadres 0. Bootloader op 0, partitietabel op 0x8000, - applicatie op 0x10000. -- SHA256 van die demo: `9662c193fc52407bba2ed1462c582d40148f7d8eedfc806db4e8fff7685abba1`. +- `1-Demo/Demo_Arduino/1_2_4.0_LvglWidgets/4.0_LvglWidgets/`: source code. +- `1-Demo/Demo_Arduino/Libraries/Arduino_GFX-master/`: bundled panel driver. +- `8-Burn operation/Burn files/4.0_LvglWidgets.bin`: combined firmware, + 652640 bytes, flash address 0. Bootloader at 0, partition table at 0x8000, + application at 0x10000. +- SHA256 of that demo: `9662c193fc52407bba2ed1462c582d40148f7d8eedfc806db4e8fff7685abba1`. -Gebruik uitsluitend de Widgets-demo voor de displaytest. De andere meegeleverde -voorbeelden zijn relay- en muziektoepassingen. De Widgets-bron gebruikt alleen -paneel, GT911 en achtergrondverlichting en heeft geen HA-/wifi-configuratie. +Use only the Widgets demo for the display test. The other bundled +examples are relay and music applications. The Widgets source only uses the +panel, GT911, and backlight, and has no HA/Wi-Fi configuration. -## Opvallende referentie-instellingen +## Notable reference settings -De oorspronkelijke bron gebruikt ArduinoGFX, `st7701_type1_init_operations`, -16 MHz pixelklok, RGB-pinnen R=11/12/13/14/0, G=8/20/3/46/9/10, -B=4/5/6/7/15. Horizontaal front/pulse/back=10/8/50; verticaal=10/8/20. -De paneelmodus is 0x3A=0x60, RGB666 op een 16-bits bedrade bus, met 0xCD=0x00. -Rotatie 0 schrijft C7=0 in registerbank 0x10 en MADCTL=0 in bank 0. -De achtergrondverlichting staat via GPIO38 continu aan, zonder PWM. +The original source uses ArduinoGFX, `st7701_type1_init_operations`, +a 16 MHz pixel clock, RGB pins R=11/12/13/14/0, G=8/20/3/46/9/10, +B=4/5/6/7/15. Horizontal front/pulse/back=10/8/50; vertical=10/8/20. +The panel mode is 0x3A=0x60, RGB666 on a 16-bit wired bus, with 0xCD=0x00. +Rotation 0 writes C7=0 in register bank 0x10 and MADCTL=0 in bank 0. +The backlight is on continuously via GPIO38, without PWM. -De gamma- en spanningswaarden lijken op ESPHome's ST7701S-basis, maar de volgorde, -porches en rotatiecommando's verschillen. Dat is aanleiding voor vergelijking, -geen bewijs dat een specifieke wijziging strepen oplost. +The gamma and voltage values resemble ESPHome's ST7701S base, but the order, +porches, and rotation commands differ. That's grounds for comparison, +not proof that a specific change fixes banding. -## Back-up en terugkeer +## Backup and recovery -Verifieer eerst de chip/MAC en de echte USB-poort. Een volledige flashback-up is -het veiligst. Voor deze specifieke demo worden alleen sectoren binnen de eerste -1 MiB overschreven. Een exacte back-up van dat bereik is voldoende om die -overschrijving terug te draaien als de demo geen andere flashgebieden beschrijft. -Bewaar daarnaast de eigen firmware/YAML. Flashback-ups bevatten persoonlijke -sleutels: alleen lokaal opslaan, nooit in Git. +First verify the chip/MAC and the actual USB port. A full flash backup is +safest. For this specific demo, only sectors within the first +1 MiB are overwritten. An exact backup of that range is enough to +undo that overwrite, as long as the demo doesn't write to other flash regions. +Also keep the device's own firmware/YAML. Flash backups contain personal +keys: keep them local only, never in Git. -Gebruik geen erase-all. Herstel na de test de geback-upte sectoren en verifieer de -ESPHome-identiteit, versie, voorkeuren en HA-koppeling. Leg het fysieke resultaat -apart vast: een succesvolle upload zegt niets over het zichtbare beeld. +Don't use erase-all. After the test, restore the backed-up sectors and verify the +ESPHome identity, version, preferences, and HA pairing. Record the physical result +separately: a successful upload says nothing about the visible image. -## Uitgevoerde test op 12 september 2026 +## Test performed on September 12, 2026 -- Fabrikantarchief opgehaald en de Widgets-bron plus meegeleverde driver gelezen. -- De eerste 1 MiB (alle door deze demo overschreven sectoren) via USB geback-upt; - 921600/460800 baud gaven communicatieproblemen, 115200 baud werkte. -- Originele Widgets-binary ongewijzigd op adres 0 geschreven; hashverificatie geslaagd. -- USB-bootlog meldt `LVGL Widgets Demo` en `Setup done`, vervolgens GT911-polling. - Ook meldt de oorspronkelijke demo GPIO-fouten bij het opzetten; daarom blijft - fysieke bevestiging nodig. De eigenaar bevestigt daarna: de fabrikantdemo is schoon, zonder strepen. - Daarmee is goede beeldweergave op dit fysieke paneel aangetoond. Dit sluit - niet iedere hardware-/voedingsfactor uit, maar maakt onze aansturing de - eerste onderzoekslijn. +- Fetched the manufacturer archive and read the Widgets source plus bundled driver. +- Backed up the first 1 MiB (every sector this demo overwrites) over USB; + 921600/460800 baud caused communication problems, 115200 baud worked. +- Wrote the original Widgets binary unchanged at address 0; hash verification passed. +- The USB boot log reports `LVGL Widgets Demo` and `Setup done`, then GT911 polling. + The original demo also reports GPIO errors during setup; physical + confirmation therefore remains necessary. The owner then confirmed: the manufacturer demo is clean, no banding. + That demonstrates good image output on this physical panel. This doesn't + rule out every hardware/power factor, but it makes our own driving the + first line of investigation. -## Native ESPHome-kandidaat 0.1.3 +## Native ESPHome candidate 0.1.3 -De standaard `st7701s`-component krijgt dezelfde 16 MHz-klok en horizontale -10/8/50 en verticale 10/8/20 timing als de fabrikantdemo. SPI gebruikt MODE0; -kleurvolgorde RGB, geen inversie of hardwarematige spiegeling. Na de ingebouwde -initialisatie worden de 16-bits busconfiguratie CD=00 en de standaard registerbank -expliciet gekozen, gevolgd door 3A=60 en de wachttijd na sleep-out. De GT911 blijft -in dezelfde ongemirrorde oriëntatie. De ESPHome RGB-driver wordt niet gekopieerd -of gepatcht; de eerdere extra SDK-cache-/restart-instellingen zijn verwijderd. +The standard `st7701s` component gets the same 16 MHz clock and horizontal +10/8/50 and vertical 10/8/20 timing as the manufacturer demo. SPI uses MODE0; +color order RGB, no inversion or hardware mirroring. After the built-in +initialization, the 16-bit bus configuration CD=00 and the standard register bank +are explicitly selected, followed by 3A=60 and the wait after sleep-out. The GT911 stays +in the same unmirrored orientation. The ESPHome RGB driver isn't copied +or patched; the earlier extra SDK cache/restart settings have been removed. -De eigenaar meldt dat deze kandidaat schoner is, maar nog enkele vaste verticale -banden toont. Dit is dus geen afgeronde oplossing. De fabrikantdemo blijft de -schone referentie. Verschillen zijn onder meer ESP-IDF/LVGL-versies, framebuffer- -en bouncebuffergebruik, wifi/API, PWM en de extra initialisatiestappen van de -standaarddriver (inclusief software-reset). Native betekent hier de ingebouwde -ESPHome-driver, niet byte-voor-byte dezelfde uitvoering als de fabrikantdemo. +The owner reports that this candidate is cleaner but still shows a few fixed vertical +bands. So this is not a finished solution. The manufacturer demo remains the +clean reference. Differences include ESP-IDF/LVGL versions, framebuffer +and bounce buffer usage, Wi-Fi/API, PWM, and the extra initialization steps of the +standard driver (including a software reset). "Native" here means the built-in +ESPHome driver, not a byte-for-byte identical execution to the manufacturer demo. -Daarna is alleen de Guition-interface licht vormgegeven: achtergrond F7F7F7, -witte kaarten, rand DDDDDD, donkere tekst en blauwe accenten. Ook de detailkaarten -gebruiken lichte bedieningselementen. Geometrie, paginering, touch, HA-indeling en -opgeslagen instellingen blijven behouden. Deze stijlwijziging is geen strepenfix. +After that, only the Guition interface was given a light design: background F7F7F7, +white cards, border DDDDDD, dark text, and blue accents. The detail cards also +use light controls. Geometry, pagination, touch, HA layout, and +saved settings are preserved. This style change is not a banding fix. -### Lichte interface: softwarecontrole +### Light interface: software check -De Python-suite (33 tests), zeven C++-regressietests en packagegeneratorcontrole -slagen. De eerste lichte firmware is via OTA geïnstalleerd; tien paginacontroles -plus vijftig overlaywisselingen slagen zonder HA-acties. De schermopname toont de -nieuwe lichte kaarten met de bestaande HA-indeling. Lange namen kregen daarna -vaste regelhoogtes met afkapping, zodat titel en status niet overlappen. -Een interne schermopname bewijst geen storingsvrije fysieke paneeluitvoer. +The Python suite (33 tests), seven C++ regression tests, and the package generator check +pass. The first light firmware was installed over OTA; ten page checks +plus fifty overlay switches pass without HA actions. The screen capture shows the +new light cards with the existing HA layout. Long names then got +fixed line heights with truncation, so the title and status don't overlap. +An internal screen capture doesn't prove glitch-free physical panel output. -De definitieve lichte versie (config_hash 0x0a70a194) is via OTA geïnstalleerd. -Beide Easy Setup- en beide handmatige profielen bouwen succesvol. De eigenaar -bevestigt vervolgens: ‘Stijl goed, geen banden zichtbaar’. Dit is fysieke -acceptatie van het huidige beeld, geen duurtest na langdurige standby en geen -bewijs dat alleen een driverwijziging de oorzaak heeft opgelost. +The final light version (config_hash 0x0a70a194) was installed over OTA. +Both Easy Setup and both manual profiles build successfully. The owner +then confirmed: 'Style good, no bands visible'. This is physical +acceptance of the current image, not an endurance test after prolonged standby, and not +proof that a driver change alone fixed the root cause. diff --git a/docs/PROBLEMEN.md b/docs/PROBLEMEN.md deleted file mode 100644 index d44b426..0000000 --- a/docs/PROBLEMEN.md +++ /dev/null @@ -1,32 +0,0 @@ -# Problemen oplossen - -| Symptoom | Eerst controleren | -|---|---| -| Geen USB-poort | Datakabel, andere USB-poort, bordvoeding; vergelijk `python -m serial.tools.list_ports` voor/na aansluiten. Installeer zo nodig de driver van de USB-chipfabrikant. | -| Permission denied op Linux | Toegang tot de seriële devicegroep (vaak `dialout`); opnieuw inloggen na een groepswijziging. | -| Poort bezet | Stop ESPHome-logs, IDE-monitor en andere seriële lezers. Eén lezer tegelijk. | -| Upload blijft verbinden | Controleer juiste bord/poort. Houd BOOT ingedrukt tijdens verbinden en laat daarna los als het bord dat vereist. | -| Build faalt | Python 3.11–3.14 en ESPHome 2026.6.2; volledige uitgepakte map, lokale fonts/components aanwezig, internet en schijfruimte. Bewaar eerste echte fout uit de buildlog. | -| Wit/zwart of verkeerd gedraaid beeld | Controleer ILI9341 + XPT2046-variant. Niet willekeurig de pinnen, rotatie of SPI-instellingen van een ander CYD-model kopiëren. | -| Geen meetscherm | De upload moet klaar zijn, niet alleen de build. Flash met `-s CALIBRATION_ON_BOOT true`. Zoek in USB-logs `USB calibration ready`; normaal opstarten toont geen kruisjes. | -| Geen metingen | Vijf kruisjes zichtbaar, andere loglezer dicht, juiste poort. Druk eerst Enter in de wizard en tik daarna drie keer hetzelfde gevraagde punt, met loslaten ertussen. | -| Tik springt naar andere plek | Controleer onafhankelijke `verify`, eigen `calibration.yaml` en vaste oriëntatie. Niet dezelfde tikken gebruiken om zowel te fitten als te verifiëren. | -| Fit meldt spreiding/fout | Meet opnieuw volgens de prompts. Instabiele ADC-metingen niet oplossen door de tolerantie te verhogen; controleer voeding, kabel en touchhardware. | -| Dubbele HA-actie | Controleer zowel touchlogs als HA-automations. Gebruik niet tegelijk directe acties én een automation op de actie-sensor voor dezelfde handeling. | -| Wifi verbindt niet | 2,4GHz, juiste lokale secrets, netwerkbereik. USB-kalibratie heeft HA niet nodig. | -| HA ziet het bord niet | Handmatig IP uit logs gebruiken; poort 6053 bereikbaar, juiste encryptiesleutel, geen gastnetwerkisolatie. | -| Status werkt maar actie niet | HA-optie voor toegestane apparaat-acties, `DIRECT_ACTIONS: "true"`, echte entity-ID en ondersteunde actie controleren. | -| Vacuum/climate deels bruikbaar | Ondersteunde modi/attributen verschillen per integratie; zie TEGELS.md en test dezelfde actie eerst in HA. | -| Verkeerde paginering | `TILE_COUNT` is 1–10; tot zes verborgen knoppen, vanaf zeven twee pagina's. Opnieuw flashen na wijzigen. | -| Vastloper/reset | USB-log bewaren, resetreden/voeding controleren en renderproef draaien. Noteer handeling en tijdstip. | - -Voor een USB-log (stop andere lezers): - -```sh -python diagnostics/capture_serial.py --port --seconds 120 -``` - -De bestaande `CYD_STABILITY.md` en `TEST_RESULTS.md` beschrijven eerder -onderzoek aan één specifiek bord. Hun meetwaarden zijn geen kalibratie voor -een nieuw bord. Begin bij [README.md](../README.md), behoud een werkende -lokale configuratie en verander één oorzaak tegelijk. diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 2bffc66..18703f7 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -1,52 +1,51 @@ -# Updates publiceren zonder gebruikersconfiguratie te vervangen +# Publishing updates without replacing user configuration -## Indeling van de code en gegevens +## Layout of the code and data -- `main` is de distributiebranch voor de app **en beide** firmwarepakketten. - De Guition-branch blijft beschikbaar voor bordwerk; publiceer gedeelde releases - altijd ook naar main. -- `screen_manager/config.yaml` bevat de appversie. Verhoog die bij iedere apprelease. - Git push alleen is niet genoeg om een bestaande app een update aan te bieden. -- De appimage bevat uitsluitend code. Indelingen leven in `/data/screens.json` - (`version: 1`, `screens: {...}`). Een update/herbuild behoudt deze volumegegevens. -- De eigen ESPHome-YAML bevat naam, wifi-verwijzingen en unieke API/OTA-sleutels. - Gedeelde pakketten bevatten geen secrets, vaste eigenaarsentiteiten of wifi. -- CYD-kalibratie staat in ESP32-preferences. Behoud de preference-key, structuur - en partitie-indeling, of schrijf een expliciete migratie. -- Het tegelprotocol gebruikt `v: 1`. Houd oudere velden en domeinen bruikbaar. - Een gewijzigde opslagversie moet een geteste migratie met back-up krijgen. - De app weigert onbekende versies in plaats van de data leeg te overschrijven. +- `main` is the only distribution branch for the app **and both** firmware packages. + The old Guition branch is no longer updated; all board work goes to main. +- `screen_manager/config.yaml` holds the app version. Bump it on every app release. + A Git push alone isn't enough to offer an existing app an update. +- The app image contains only code. Layouts live in `/data/screens.json` + (`version: 1`, `screens: {...}`). An update/rebuild preserves this volume data. +- The device's own ESPHome YAML contains the name, Wi-Fi references, and unique API/OTA keys. + Shared packages contain no secrets, fixed owner entities, or Wi-Fi. +- CYD calibration lives in ESP32 preferences. Preserve the preference key, structure, + and partition layout, or write an explicit migration. +- The tile protocol uses `v: 1`. Keep older fields and domains usable. + A changed storage version must get a tested migration with a backup. + The app refuses unknown versions instead of overwriting the data blank. -## Voor elke release +## For every release -1. Pas de bronprofielen en gedeelde componenten aan. Draai - `python3 tools/generate_packages.py`; bewerk `packages/*.yaml` nooit rechtstreeks. -2. Draai alle Python-tests met aiohttp geïnstalleerd, alle `tests/*.cpp`, de - generator met `--check`, en compileer beide Easy Setup-profielen plus de - bestaande handmatige profielen. Doe dat na elkaar: profielen met dezelfde - `DEVICE_NAME` delen één build-map, en een parallelle build laat een upload - het verkeerde `firmware.bin` kiezen. Controleer dat er geen secrets in Git staan. -3. Test appstart, opslaan, opnieuw starten/updaten met bestaande indelingen, - opnieuw verbinden met HA en een ESP-herstart. Test een nieuwe kaart op echte - hardware. Een goede build vervangt fysieke touchacceptatie niet. -4. Verhoog appversie en firmwareprojectversie; schrijf CHANGELOG en concrete - testresultaten. Publiceer alleen compatibele wijzigingen rechtstreeks op main. -5. Commit en push main. Maak een onveranderlijke tag `screens-vX.Y.Z` van dezelfde - commit. Werk de Guition-branch bij. Test de remote YAML in een lege map: - alle componenten/fonts moeten via GitHub kunnen worden opgehaald. -6. De gebruiker controleert de appwinkel op updates en update ESP Screen Manager. - Voor nieuwe schermfuncties: bestaand ESPHome-apparaat → Install → Wirelessly. - De bestaande YAML blijft staan; `refresh: 0s` haalt bij elke build actuele code. +1. Update the source profiles and shared components. Run + `python3 tools/generate_packages.py`; never edit `packages/*.yaml` directly. +2. Run all Python tests with aiohttp installed, all `tests/*.cpp`, the + generator with `--check`, and compile both Easy Setup profiles plus the + existing manual profiles. Do that sequentially: profiles with the same + `DEVICE_NAME` share one build folder, and a parallel build can make an upload + pick the wrong `firmware.bin`. Check that no secrets are in Git. +3. Test app start, saving, restarting/updating with existing layouts, + reconnecting to HA, and an ESP restart. Test a new card on real + hardware. A good build doesn't replace physical touch acceptance. +4. Bump the app version and firmware project version; write the CHANGELOG and concrete + test results. Only publish compatible changes directly to main. +5. Commit and push main (the only release branch). Create an immutable tag + `screens-vX.Y.Z` from the same commit. Test the remote YAML in an empty folder: + all components/fonts must be fetchable via GitHub. +6. The user checks the App store for updates and updates ESP Screen Manager. + For new screen features: existing ESPHome device → Install → Wirelessly. + The existing YAML stays in place; `refresh: 0s` fetches current code on every build. -## Lokale ontwikkeling +## Local development -Kopieer alleen `screen_manager/` naar de gedeelde `addons/esp_screen_manager/`. -Herlaad de appwinkel, installeer de lokale versie en bouw na codewijzigingen opnieuw. -Een lokale testversie heeft een andere add-onidentiteit dan de GitHub-versie; de -gegevens verhuizen niet automatisch. Test voor de uiteindelijke installatie de -GitHub-versie en zet de lokale versie uit om twee schrijvers te voorkomen. +Copy only `screen_manager/` to the shared `addons/esp_screen_manager/`. +Reload the App store, install the local version, and rebuild after code changes. +A local test version has a different add-on identity than the GitHub version; the +data doesn't move over automatically. For the final installation, test the +GitHub version and turn off the local version to avoid two writers. -Voor backendtests op een ontwikkelcomputer: +For backend tests on a development machine: ```sh python3 -m venv .venv-portal @@ -54,371 +53,403 @@ python3 -m venv .venv-portal .venv-portal/bin/python -m unittest discover -s tests ``` -Een tijdelijke ontwikkelserver ondersteunt `SCREEN_DEV=1`, `HA_API` (eindigt op -`/api`), `HA_TOKEN_FILE` en `SCREEN_DATA`. Hij bindt alleen op localhost. Zet een -token nooit in broncode, URL's of Git. Productie gebruikt Supervisor en accepteert -alleen het Ingress-proxyadres; er is geen extra publieke poort. +A temporary development server supports `SCREEN_DEV=1`, `HA_API` (ending in +`/api`), `HA_TOKEN_FILE`, and `SCREEN_DATA`. It only binds on localhost. Never put a +token in source code, URLs, or Git. Production uses Supervisor and only accepts +the Ingress proxy address; there is no additional public port. -### Instellingencompatibiliteit 0.1.2 +### Settings compatibility 0.1.2 -`settings` is een optioneel object binnen een schermindeling en binnen het bestaande -`v: 1, op: layout`-bericht. Een ontbrekend object behoudt eerder gedrag. Oude -firmware negeert dit extra veld en blijft tegels ontvangen. De beheerpagina meldt -dat nieuwe firmware nodig is. Een oude browser die alleen tegels opslaat, wist -opgeslagen instellingen niet. Onbekende instellingen worden geweigerd. +`settings` is an optional object within a screen layout and within the existing +`v: 1, op: layout` message. A missing object preserves prior behavior. Old +firmware ignores this extra field and keeps receiving tiles. The management page reports +that new firmware is needed. An old browser that only saves tiles doesn't wipe +saved settings. Unknown settings are refused. -Firmware gebruikt een afzonderlijke preferences-key `0x53435231` met een vaste -versie-1 structuur (elf int32-velden en een uint32-versie). Wijzig key, structuur -of versie niet zonder migratie. Alleen gewijzigde instellingen worden opgeslagen; -periodieke herhaling veroorzaakt geen flashwrites en geen nieuwe idle-timer. +Firmware uses a separate preferences key `0x53435231` with a fixed +version-1 structure (eleven int32 fields and a uint32 version). Don't change the key, +structure, or version without a migration. Only changed settings are saved; +periodic repeats cause no flash writes and no new idle timer. -### Compatibiliteit 0.2.0 +### Compatibility 0.2.0 -Opslagversie en tegelprotocol blijven 1. Optionele `tiles[].options`, `o` in -statusberichten, `history` en `inbox` in het layoutbericht zijn additief. Een -oudere browser die opties niet meestuurt behoudt de bestaande opties voor dezelfde -entiteit. Oudere firmware negeert extra velden. Nieuwe entiteitsdomeinen vereisen -wel firmware 0.2.0; bij terugrollen naar een oudere app eerst de gegevensback-up -herstellen, omdat die app nieuwe domeinen nog niet kan laden. Geen oude opslag -stilzwijgend overschrijven. Bestaande preference-keys blijven ongewijzigd. +Storage version and tile protocol stay 1. Optional `tiles[].options`, `o` in +status messages, `history`, and `inbox` in the layout message are additive. An +older browser that doesn't send options keeps the existing options for the same +entity. Older firmware ignores extra fields. New entity domains do +require firmware 0.2.0; when rolling back to an older app, restore the data backup +first, since that app can't yet load new domains. Don't silently +overwrite old storage. Existing preference keys stay unchanged. -De app gebruikt nu de officiële ESPHome-container, mapped `homeassistant_config` -naar `/homeassistant` en vraagt UART/USB voor de door de gebruiker gekozen poort. -Config/secrets blijven in de eigen HA-configmap; builds in `/data`. App-updates -vervangen deze mappen niet. Native getalinstellingen melden wijzigingen via -`esphome.screen_setting`; de manager valideert inbox, sleutel en waarde. +The app now uses the official ESPHome container, maps `homeassistant_config` +to `/homeassistant`, and requests UART/USB for the port the user chose. +Config/secrets stay in the device's own HA config folder; builds in `/data`. App updates +don't replace these folders. Native number settings report changes via +`esphome.screen_setting`; the manager validates the inbox, key, and value. -De ingebouwde CLI bewaart opnieuw te downloaden caches onder `/data/build` en -`/data/platformio`. Alleen deze mappen zijn uitgesloten van appback-ups via -`backup_exclude`. `/data/screens.json` en eigen ESPHome-configuraties blijven -back-upgegevens; sluit nooit `/data` als geheel uit. +The built-in CLI stores re-downloadable caches under `/data/build` and +`/data/platformio`. Only these folders are excluded from app backups via +`backup_exclude`. `/data/screens.json` and the device's own ESPHome configurations remain +backup data; never exclude `/data` as a whole. -### Compatibiliteit 0.2.7 +### Compatibility 0.2.7 -Twintig runtime-tegels vereisen firmware 0.2.7+. De manager controleert die versie -voor opslaan en verzenden van grotere indelingen. Bestaande indelingen blijven -behouden bij oudere firmware. Oude appversies ondersteunen niet meer dan tien; -breng de indeling eerst terug tot tien of herstel hun gegevensback-up bij rollback. -`settings.swipe_pages` is additief in opslagversie 1. Op de draad staat het buiten -het ongewijzigde elfvelden-settingsobject. De bestaande preferences blijven gelijk; -alleen swipe gebruikt een nieuwe eigen uint32-key `0x53575031`, standaard uit. +Twenty runtime tiles require firmware 0.2.7+. The manager checks that version +before saving and sending larger layouts. Existing layouts are +preserved on older firmware. Old app versions don't support more than ten; +first bring the layout back down to ten, or restore their data backup on rollback. +`settings.swipe_pages` is additive in storage version 1. On the wire, it sits outside +the unchanged eleven-field settings object. Existing preferences stay the same; +only swipe uses a new, separate uint32 key `0x53575031`, off by default. -### Compatibiliteit 0.2.9 +### Compatibility 0.2.9 -Guition-rotatie is additief als `settings.rotation` in appdata, uitsluitend -0/90/180/270. Oude beheerpagina's die het veld weglaten behouden de opgeslagen -hoek. Op de draad staat `rotation` naast het ongewijzigde elfvelden-settingsobject. -De firmware bewaart de hoek als afzonderlijke uint32 op key `0x524F5431`; de -bestaande Settings-structuur en CYD-kalibratie blijven ongewijzigd. Alleen de -Guition activeert de rotatiecallback. De diagnostische entity `Guition schermtype` -maakt deze capability herkenbaar, ook als het scherm offline of hernoemd is. +Guition rotation is additive as `settings.rotation` in app data, restricted to +0/90/180/270. Old management pages that omit the field keep the stored +angle. On the wire, `rotation` sits alongside the unchanged eleven-field settings object. +The firmware stores the angle as a separate uint32 at key `0x524F5431`; the +existing Settings structure and CYD calibration stay unchanged. Only the +Guition activates the rotation callback. The diagnostic entity `Guition schermtype` +makes this capability recognizable, even when the screen is offline or renamed. -### Compatibiliteit 0.2.10 +### Compatibility 0.2.10 -`tiles[].options.background` is een optionele paletnaam binnen opslagversie 1. -Op de draad staat dit in het bestaande `o`-object. Oudere firmware negeert het -veld; nieuwe firmware valt bij onbekende namen terug op de normale kleuren. -De app accepteert uitsluitend het lichte palet uit `TILE_BACKGROUNDS`. Een oude -editor die `background` weglaat behoudt de opgeslagen kleur voor dezelfde entity; -expliciet `auto` herstelt de standaard. Geen gewijzigde voorkeurstructuur of -sleutels, geen nieuwe firmwareflash nodig voor latere kleurkeuzes. +`tiles[].options.background` is an optional palette name within storage version 1. +On the wire, this sits in the existing `o` object. Older firmware ignores the +field; newer firmware falls back to the normal colors for unknown names. +The app only accepts the light palette from `TILE_BACKGROUNDS`. An old +editor that omits `background` keeps the stored color for the same entity; +explicit `auto` restores the default. No changed preference structure or +keys, no new firmware flash needed for later color choices. -Een oude apprelease van vóór 0.2.10 kent dit optieveld niet. Bij terugrollen van -de app herstel je daarom de bijbehorende gegevensback-up; wis of negeer geen -onbekende opties om een oud opslagbestand toch te openen. +An old app release from before 0.2.10 doesn't know this option field. When rolling back +the app, restore the corresponding data backup; don't wipe or ignore +unknown options just to open an old storage file anyway. -### Compatibiliteit app 0.2.15 / firmware 0.2.16 +### Compatibility app 0.2.15 / firmware 0.2.16 -Opslagversie en tegelprotocol blijven 1. `tiles[].options.background` krijgt de -extra paletnaam `none` ("Geen"): geen kaart achter de tegelinhoud. Oudere -firmware kent de naam niet en valt terug op de normale kaart; omdat dat voor de -gebruiker onzichtbaar misgaat, stuurt de manager zo'n indeling pas na firmware -0.2.16 (`min_firmware`) en bewaart haar ondertussen. De firmware verbergt -uitsluitend vulling en rand (`bg_opa`/`border_opa`); maten, padding en de -pressed-feedback op de PRESSED-state blijven gelijk. De analoge klok wijzigt -alleen de tekening (streepjes, cijfers, kalenderblok op enkele tegels); er is -geen nieuw veld voor nodig. Geen gewijzigde preferences of sleutels. +Storage version and tile protocol stay 1. `tiles[].options.background` gets the +extra palette name `none` ("None"): no card behind the tile content. Older +firmware doesn't know the name and falls back to the normal card; because that fails +invisibly for the user, the manager only sends such a layout once firmware +0.2.16 or newer is detected (`min_firmware`), holding it back in the meantime. The firmware only hides +the fill and border (`bg_opa`/`border_opa`); sizes, padding, and the +pressed feedback on the PRESSED state stay the same. The analog clock only changes +the drawing (ticks, numerals, calendar block on single tiles); no +new field is needed for it. No changed preferences or keys. -### Compatibiliteit 0.2.16 / firmware 0.2.17 +### Compatibility 0.2.16 / firmware 0.2.17 -`FIRMWARE_VERSION` in `screen_manager/app/core.py` is de firmware die bij deze -app hoort; `tests/test_updates.py` eist dat hij gelijk is aan -`SCREEN_FIRMWARE_VERSION` in beide bordprofielen en pakketten. Verhoog ze samen. -Een scherm met een lagere `Schermfirmware` krijgt een update-aanbod; een build -haalt `main`, dus publiceer firmware en app in dezelfde commit. +`FIRMWARE_VERSION` in `screen_manager/app/core.py` is the firmware that belongs to this +app; `tests/test_updates.py` requires it to equal +`SCREEN_FIRMWARE_VERSION` in both board profiles and packages. Bump them together. +A screen with a lower `Schermfirmware` gets an update offer; a build +fetches `main`, so publish firmware and app in the same commit. -Firmware 0.2.17 voegt de diagnostische text sensors `Apparaatnaam` -(`${DEVICE_NAME}`) en `IP-adres` (`wifi_info`) toe. De app koppelt een scherm -via `Apparaatnaam` aan het profiel met dezelfde `esphome.name`; oudere firmware -valt terug op één profiel met gelijke `friendly_name` en een handmatig ingevuld -adres. Hernoem deze sensors niet zonder `core.discover` aan te passen. +Firmware 0.2.17 adds the diagnostic text sensors `Apparaatnaam` +(`${DEVICE_NAME}`) and `IP-adres` (`wifi_info`). The app links a screen +to the profile with the same `esphome.name` via `Apparaatnaam`; older firmware +falls back to a single profile with a matching `friendly_name` and a manually entered +address. Don't rename these sensors without also updating `core.discover`. -Updatestatus staat in `/data/updates.json` (`version: 1`: `auto`, `hosts`, -`results`, `last_round`), los van `screens.json`. Onbekende versies worden -geweigerd. De ronde draait één scherm tegelijk, wacht op `Schermfirmware >=` -doelversie en één minuut stabiliteit, en stopt bij de eerste fout. Alleen de -nachtelijke ronde schrijft een `persistent_notification` in HA. +Update status lives in `/data/updates.json` (`version: 1`: `auto`, `hosts`, +`results`, `last_round`), separate from `screens.json`. Unknown versions are +refused. The round runs one screen at a time, waits for `Schermfirmware >=` +target version and one minute of stability, and stops on the first error. Only the +nightly round writes a `persistent_notification` in HA. -### Compatibiliteit 0.2.18 / firmware 0.2.18 +### Compatibility 0.2.18 / firmware 0.2.18 -Opslagversie en tegelprotocol blijven 1. `tiles[].options.icon` is additief: -`auto` of een naam uit `screen_manager/app/tile_icons.py`. Op de draad stuurt de -manager in `o.icon` alleen de opgeloste codepoint in hex (`F06B5`): de gekozen -naam, of bij `auto` het `mdi:`-icoon uit de HA-attributen als dat in de set zit. -Oudere firmware negeert het veld; nieuwe firmware controleert of de glyph in de -font zit en valt anders terug op het domeinicoon. Daarom geen `min_firmware`. -Een oude editor die `icon` weglaat behoudt de opgeslagen keuze. +Storage version and tile protocol stay 1. `tiles[].options.icon` is additive: +`auto` or a name from `screen_manager/app/tile_icons.py`. On the wire, the +manager sends only the resolved codepoint in hex (`F06B5`) in `o.icon`: the chosen +name, or, for `auto`, the `mdi:` icon from the HA attributes if it's in the set. +Older firmware ignores the field; newer firmware checks whether the glyph is in the +font and otherwise falls back to the domain icon. Hence no `min_firmware`. +An old editor that omits `icon` keeps the stored choice. -`tile_icons.py` is de enige lijst. `tools/generate_icons.py` schrijft de glyphs -als YAML-anker in de drie MDI-fonts van beide bordprofielen en bouwt -`static/tile-icons.woff` voor de editor (met left bearing gelijk aan xMin, anders -staan iconen in de browser uit het midden); draai daarna `generate_packages.py`. -De substituties `MDI_GLYPH_*` zijn vervallen: `TILEn_ICON` in handmatige -profielen moet uit de set komen. Geen gewijzigde preferences of sleutels. +`tile_icons.py` is the single source list. `tools/generate_icons.py` writes the glyphs +as a YAML anchor into the three MDI fonts of both board profiles and builds +`static/tile-icons.woff` for the editor (with left bearing equal to xMin, otherwise +icons sit off-center in the browser); run `generate_packages.py` afterward. +The `MDI_GLYPH_*` substitutions are retired: `TILEn_ICON` in manual +profiles must come from the set. No changed preferences or keys. -### Compatibiliteit 0.2.38 / firmware 0.2.32 +### Compatibility 0.2.40 / firmware 0.2.34 -Opslagversie en tegelprotocol blijven 1. Een indeling mag een optioneel object -`header: {items: [...]}` hebben (maximaal zes): `{type: clock|analog|date}` of -`{type: entity, entity, content: state|last_changed, icon: auto|none|, show: -always|active}`. Zonder `header` geldt wat de firmware altijd deed (de klok van -`show_clock`); een oude editor die `header` weglaat behoudt de opgeslagen balk. Bij -opslaan volgt `settings.show_clock` de klok in de balk, zodat oudere firmware de tijd -alleen toont als hij erin staat. Entiteiten in de balk mogen naast de tegeldomeinen -ook `device_tracker`, `zone`, `lock`, `alarm_control_panel`, `counter`, `event`, -`input_datetime`, `input_text`, `water_heater` en `humidifier` zijn; de inventaris -markeert die met `tile: false` zodat de tegelkiezer ze overslaat. +Storage version and tile protocol stay 1; no new message fields, preferences or keys. +Firmware 0.2.34 changes only text: every on-screen label, log line and status reply is +English, and so are the entity names. The status replies the manager reacts to are now +`Ready for tile configuration`, `Loading tiles`, `Resend needed`, `Synced` and +`Error: …`; `RESEND_STATES` in `server.py` and `diagnostics/*.py` accept both +languages, so screens on firmware 0.2.33 and older keep working unchanged. -Op de draad stuurt de manager direct na het layoutbericht `{v:1, op: header, items}` -met per onderdeel `k` (`clock`, `analog`, `date`, `text`, `ago`), `i` (codepoint), -`t` (tekst, al in het Nederlands opgemaakt en beperkt tot de glyphs van -`sublabel_big`), `e` (unix-tijd voor `ago`) en `c` (accentkleur). Onderdelen met -`show: active` die niet actief zijn stuurt de manager niet. Alleen firmware 0.2.32+ -krijgt dit bericht (`HEADER_MIN_FIRMWARE`, op basis van `Schermfirmware`): oudere -firmware zou `op: header` weigeren met "Fout: ongeldig bericht". Het bericht wordt -apart vergeleken; een gewijzigde waarde verstuurt alleen de balk, niet de indeling. -De firmware antwoordt met dezelfde status als een tegelstatus, dus de -Tegelinstellingen-entiteit flipt niet bij iedere waarde. +The entity names are part of the compatibility contract. Home Assistant's ESPHome +integration builds the unique ID from the entity name (`/0/text_info/Tile settings`), +so a renamed entity gets a **new entity ID** and the old one is removed as soon as the +screen reconnects (seen on HA 2026.9.1: `text.cyd_2_8in_display_tegelinstellingen` → +`text.cyd_2_8in_display_tile_settings` within the same second). Renamed: `Tegelinstellingen` → +`Tile settings`, `Schermfirmware` → `Screen firmware`, `Apparaatnaam` → `Device name`, +`IP-adres` → `IP address`, `Guition schermtype` → `Guition screen type`, `Opgestart` → +`Last boot`, `Helderheid normaal/standby/nacht` → `Normal/Standby/Night brightness`, +`Standby na` → `Standby after`, `Touch kalibreren` → `Calibrate touch`. -Firmware: `header_bar.h` (LVGL-vrij: parsen, "5 min geleden", datum, afstanden, -plaatsing) en `runtime_tiles::draw_header()`. Het tijdlabel van het profiel blijft -bestaan als referentie (rechtermarge, tekenvolgorde) maar is in runtime-modus -verborgen. Waarden gebruiken `sublabel_big`, iconen `materialdesign_icons_mini`; -`sublabel_big` kreeg `'`, `#`, `*`, `=`, `;`, `²`, `³`, `µ`, `–` en veelvoorkomende -accenten; `tests/test_header_bar.py` houdt `header_bar.GLYPHS` gelijk aan die lijst. -De editor (`barLayout` in `app.js`) en de firmware (`header_bar::gaps/place`) delen -dezelfde gehele rekensommen; de test controleert dat. `tools/render_topbar.py` -rendert de echte `draw_header()` op een Mac/Linux-machine (ESPHome host + SDL2) naar -PNG, zonder scherm. Geen gewijzigde preferences of sleutels. +`core.discover_screens` matches both names (`NAME_*` pairs). Because the manager keys +`screens.json` and `updates.json` by the inbox entity ID, `Manager.follow_renamed_inboxes` +(run whenever the screen list is rebuilt) moves a screen's layout, update address and last +result to its new inbox ID: first by the HA device an inbox was seen on during this run, +otherwise (after a restart) when a stored ID's device part (`core.inbox_prefix`) matches +the entity ID prefix of one of the device's ESPHome entities (`core.device_prefixes`; the +unchanged `Node Status`, `Restart` and `Wifi Signal` entities keep the old prefix). An existing +layout on the new ID is never overwritten; layouts of other screens are never taken. +`Manager.aliases` maps old to new IDs so `save()`, the inspector and the updater +(`Updater.current_id`, `Updater.renamed`) follow a screen that is renamed mid-update: the +Update button and the nightly round verify the new ID and record success there. Don't rename +these entities again without keeping this path. Tests: `tests/test_renamed_inbox.py`. -### Compatibiliteit 0.2.35 / firmware 0.2.30 +### Compatibility 0.2.38 / firmware 0.2.32 -Alleen firmware (Guition). `on_boot` haalt `LV_OBJ_FLAG_CLICKABLE` van `home_page` en -`tile_scroll`: een druk buiten de tegels heeft geen LVGL-object meer (geen pressed-stijl, -geen hertekening, geen events); de randveeg loopt via de touchscreen-triggers en heeft dat -niet nodig, `lv_indev_wait_release` werkt ook zonder actief object. Trace-regels `veeg …` -op INFO alleen zolang `edge_swipe.armed()`. Verder ongewijzigd. +Storage version and tile protocol stay 1. A layout may have an optional object +`header: {items: [...]}` (six max): `{type: clock|analog|date}` or +`{type: entity, entity, content: state|last_changed, icon: auto|none|, show: +always|active}`. Without `header`, the firmware's prior always-on behavior applies (the clock from +`show_clock`); an old editor that omits `header` keeps the stored bar. When +saving, `settings.show_clock` follows the clock in the bar, so older firmware only shows the +time if it's in the bar. Besides the tile domains, entities in the bar may +also be `device_tracker`, `zone`, `lock`, `alarm_control_panel`, `counter`, `event`, +`input_datetime`, `input_text`, `water_heater`, and `humidifier`; the inventory +marks those with `tile: false` so the tile picker skips them. -### Compatibiliteit 0.2.34 / firmware 0.2.29 +On the wire, right after the layout message, the manager sends `{v:1, op: header, items}` +with, per item, `k` (`clock`, `analog`, `date`, `text`, `ago`), `i` (codepoint), +`t` (text, already formatted — in English since app 0.2.40 — and limited to the glyphs of +`sublabel_big`), `e` (unix time for `ago`), and `c` (accent color). Items with +`show: active` that aren't active are not sent by the manager. Only firmware 0.2.32+ +gets this message (`HEADER_MIN_FIRMWARE`, based on the reported screen firmware): older +firmware would refuse `op: header` with "Fout: ongeldig bericht" (that firmware still reports in Dutch). The message is +compared separately; a changed value sends only the bar, not the full layout. +The firmware replies with the same status as a tile status, so the +inbox entity doesn't flip on every value. -Alleen firmware. LVGL 9.5.0's `send_event()` in `lv_indev.c` stuurt het invoerapparaat -PRESSED, RELEASED, CLICKED, LONG_PRESSED en KEY, maar geen PRESSING; een -`lv_indev_add_event_cb` krijgt dus geen positie-updates tijdens een druk. `cyd::EdgeSwipe` -werkt daarom weer op de native touchpunten uit `on_touch`/`on_update`/`on_release` -(`configure(width, height, band, travel)`, `begin(x, y, rotation)` met ESPHome's -rotatiemapping, `update()`, `end()`), de indev-callback is weg. Verder ongewijzigd. +Firmware: `header_bar.h` (LVGL-free: parsing, "5 min ago", date, distances, +placement) and `runtime_tiles::draw_header()`. The profile's time label still +exists as a reference (right margin, draw order) but is hidden in runtime +mode. Values use `sublabel_big`, icons `materialdesign_icons_mini`; +`sublabel_big` gained `'`, `#`, `*`, `=`, `;`, `²`, `³`, `µ`, `–`, and common +accented characters; `tests/test_header_bar.py` keeps `header_bar.GLYPHS` matching that list. +The editor (`barLayout` in `app.js`) and the firmware (`header_bar::gaps/place`) share +the same integer math; the test checks that. `tools/render_topbar.py` +renders the real `draw_header()` on a Mac/Linux machine (ESPHome host + SDL2) to +a PNG, without a screen. No changed preferences or keys. -### Compatibiliteit 0.2.33 / firmware 0.2.28 +### Compatibility 0.2.35 / firmware 0.2.30 -Alleen firmware. `TouchGuard::configure(0, ...)` schakelt de verplaatsingsgrens uit -(`moved_` wordt dan nooit gezet; LVGL's press-lost bepaalt of een tik doorgaat). Het -Guition-profiel zet `TOUCH_MOVE_LIMIT_PX` op 0, de CYD houdt 56. Verder ongewijzigd. +Firmware only (Guition). `on_boot` removes `LV_OBJ_FLAG_CLICKABLE` from `home_page` and +`tile_scroll`: a press outside the tiles no longer has an LVGL object (no pressed style, +no redraw, no events); the edge swipe runs via the touchscreen triggers and doesn't need +that, `lv_indev_wait_release` also works without an active object. Trace lines `swipe …` +(`veeg …` before firmware 0.2.34) at INFO only while `edge_swipe.armed()`. Otherwise unchanged. -### Compatibiliteit 0.2.32 / firmware 0.2.27 +### Compatibility 0.2.34 / firmware 0.2.29 -Alleen firmware. `cyd::EdgeSwipe` werkt op LVGL-pointercoördinaten: `configure(band, travel)`, -`begin(x, y, width)` met `lv_display_get_horizontal_resolution()`, `update()` met de 45°-regel, -plus `inward()`/`sideways()` voor het log. De Guition registreert één `lv_indev_add_event_cb` -(PRESSED/PRESSING/RELEASED) in `on_boot`; de touchscreen-triggers voeden alleen nog de -`TouchGuard`. Beide bordprofielen krijgen het script `close_cards` (roept -`runtime_tiles::dismiss()` aan, anders overlays verbergen en `active_entity` wissen); de -kruisjes en achtergrondtikken van de kaarten en `wake_display` gebruiken het. Protocol, opslag, -preferences en sleutels ongewijzigd. +Firmware only. LVGL 9.5.0's `send_event()` in `lv_indev.c` sends the input device +PRESSED, RELEASED, CLICKED, LONG_PRESSED, and KEY, but no PRESSING; an +`lv_indev_add_event_cb` therefore gets no position updates during a press. `cyd::EdgeSwipe` +therefore works again on the native touch points from `on_touch`/`on_update`/`on_release` +(`configure(width, height, band, travel)`, `begin(x, y, rotation)` with ESPHome's +rotation mapping, `update()`, `end()`); the indev callback is gone. Otherwise unchanged. -### Compatibiliteit 0.2.31 / firmware 0.2.26 +### Compatibility 0.2.33 / firmware 0.2.28 -Opslagversie en tegelprotocol blijven 1. `tiles[].slot` is additief: de absolute plek -van een tegel (pagina × 6 + rij × 2 + kolom, 0–47, `MAX_SLOTS` in `core.py`); een -dubbelbrede tegel staat op een even plek en bedekt ook de plek rechts ervan. Lege plekken -zijn toegestaan. `validate_layout` eist plekken voor alle tegels of voor geen: zonder -plekken (oude editor, oude opslag) krijgt de indeling de plekken van de oude -volgorde-packing (`pack_slots`, hetzelfde als `pack()` in de firmware), zodat er op -het scherm niets verandert; met plekken worden overlap, oneven dubbelbreed en bereik -geweigerd en de tegels op plek gesorteerd. `save()` herhaalt de packing na het -terugzetten van opgeslagen opties (die kunnen een tegel verbreden). `pages` (1–8) is -een optioneel, additief veld: pagina's die de gebruiker leeg wil houden. +Firmware only. `TouchGuard::configure(0, ...)` disables the movement limit +(`moved_` is then never set; LVGL's press-lost decides whether a tap goes through). The +Guition profile sets `TOUCH_MOVE_LIMIT_PX` to 0, the CYD keeps 56. Otherwise unchanged. -Op de draad krijgt het layoutbericht `slots` (één plek per entity, in dezelfde -volgorde) en `pages`. Firmware 0.2.26 (`Model::set_layout` met posities, `place()` -naast het oude `pack()`, `MAX_PAGES` 8) tekent de tegels op die plekken; een wijziging -van alleen plekken (`moved`) herplaatst de pagina's zonder tegelstatussen te wissen. -Oudere firmware negeert beide velden en pakt de entities in volgorde; omdat de -entities op plek gesorteerd zijn is dat de oude weergave zonder gaten, daarom geen -`min_firmware`. De editor meldt het (`has_gaps`/`hasGaps`) bij firmware < 0.2.26. Geen -gewijzigde preferences of sleutels. +### Compatibility 0.2.32 / firmware 0.2.27 -### Compatibiliteit 0.2.30 / firmware 0.2.25 +Firmware only. `cyd::EdgeSwipe` works on LVGL pointer coordinates: `configure(band, travel)`, +`begin(x, y, width)` with `lv_display_get_horizontal_resolution()`, `update()` with the 45° rule, +plus `inward()`/`sideways()` for the log. The Guition registers a single `lv_indev_add_event_cb` +(PRESSED/PRESSING/RELEASED) in `on_boot`; the touchscreen triggers now only feed the +`TouchGuard`. Both board profiles get the `close_cards` script (calls +`runtime_tiles::dismiss()`, otherwise hiding overlays and clearing `active_entity`); the +cards' close buttons and background taps, and `wake_display`, use it. Protocol, storage, +preferences, and keys unchanged. -Alleen de editor: het statusbolletje in de schermlijst is een eigen `span.dot` -(groen met `.online`). Firmware krijgt alleen een nieuw versienummer; protocol, -opslag, preferences en sleutels ongewijzigd. +### Compatibility 0.2.31 / firmware 0.2.26 -### Compatibiliteit 0.2.29 / firmware 0.2.24 +Storage version and tile protocol stay 1. `tiles[].slot` is additive: the absolute slot +of a tile (page × 6 + row × 2 + column, 0–47, `MAX_SLOTS` in `core.py`); a +double-width tile sits on an even slot and also covers the slot to its right. Empty slots +are allowed. `validate_layout` requires slots for all tiles or for none: without +slots (old editor, old storage), the layout gets the slots from the old +order-based packing (`pack_slots`, the same as `pack()` in the firmware), so +nothing changes on the screen; with slots, overlap, odd double-width, and out-of-range are +refused, and the tiles are sorted by slot. `save()` repeats the packing after +restoring saved options (which can widen a tile). `pages` (1–8) is +an optional, additive field: pages the user wants to keep empty. -Alleen firmware (Guition). `cyd::EdgeSwipe` (in `cyd_ui.h`, getest in -`test_cyd_ui.cpp`) vervangt op de Guition de LVGL-gesture op `home_page`: `begin()` -in `on_touch` met de LVGL-rotatie, `update()` in `on_update` voor het eerste contact; -bij een treffer `touch_guard.consume()`, `lv_indev_wait_release()` op alle indevs en -`show_tile_page`. Substituties `EDGE_SWIPE_BAND_PX`/`EDGE_SWIPE_TRAVEL_PX`; -`DISPLAY_W`/`DISPLAY_H` bepalen de rotatiemapping (ESPHome: 90° `x=y, y=W-x-1`, -270° `x=H-y-1, y=x`, 180° spiegelt beide). De CYD houdt de LVGL-gesture. De -instelling `swipe_pages` blijft de schakelaar; protocol, opslag en sleutels ongewijzigd. +On the wire, the layout message gets `slots` (one slot per entity, in the same +order) and `pages`. Firmware 0.2.26 (`Model::set_layout` with positions, `place()` +alongside the old `pack()`, `MAX_PAGES` 8) draws the tiles at those slots; a change +to slots only (`moved`) repositions the pages without clearing tile statuses. +Older firmware ignores both fields and packs the entities in order; since the +entities are sorted by slot, that's the old gap-free display, hence no +`min_firmware`. The editor reports this (`has_gaps`/`hasGaps`) for firmware < 0.2.26. No +changed preferences or keys. -### Compatibiliteit 0.2.28 / firmware 0.2.23 +### Compatibility 0.2.30 / firmware 0.2.25 -Alleen firmware. `cyd::TouchGuard` krijgt `configure(move_limit_px, min_press_ms)` -(aangeroepen in `on_boot` uit de substituties `TOUCH_MOVE_LIMIT_PX` en -`TOUCH_MIN_PRESS_MS`; zonder aanroep gelden de oude 18 px en 60 ms), volgt alleen -het contact-id waarmee de aanraking begon, meet de verplaatsing als afstand tot een -over vier metingen gesetteld referentiepunt en meldt via `reason()` waarom een tik -is geweigerd; `runtime_tiles::allowed()` logt dat op INFO met tag `touch`. -Protocol, opslag, preferences en sleutels ongewijzigd. +Editor only: the status dot in the screen list is its own `span.dot` +(green with `.online`). Firmware only gets a new version number; protocol, +storage, preferences, and keys unchanged. -### Compatibiliteit 0.2.27 (firmware 0.2.22 blijft) +### Compatibility 0.2.29 / firmware 0.2.24 -Alleen de app. `POST /api/install` (YAML-download) is verwijderd; `POST -/api/firmware/profiles` schrijft het profiel, vult ontbrekende wifi-sleutels in -`secrets.yaml` aan (alleen de ontbrekende regels, geverifieerd door het resultaat -opnieuw te parsen; een ongeldig bestand blijft onaangeraakt) en start met `target` -(een gemelde USB-poort) direct `install`. Poort en bouwslot worden vóór het -schrijven gecontroleerd. Het antwoord bevat `api_key`; de pagina toont hem eenmalig -voor de HA-koppeling. Het jobobject krijgt `stage` (`config`, `compile`, `upload`). -`profile_meta` levert extra `screen` (profiel gebruikt het bordpakket van dit -project) en `api_key`; `/api/inventory` krijgt de additieve lijst `pending` met -zulke profielen zonder gekoppeld scherm. Opslag, protocol, preferences en sleutels -ongewijzigd. +Firmware only (Guition). `cyd::EdgeSwipe` (in `cyd_ui.h`, tested in +`test_cyd_ui.cpp`) replaces the LVGL gesture on `home_page` on the Guition: `begin()` +in `on_touch` with the LVGL rotation, `update()` in `on_update` for the first contact; +on a hit, `touch_guard.consume()`, `lv_indev_wait_release()` on all indevs, and +`show_tile_page`. Substitutions `EDGE_SWIPE_BAND_PX`/`EDGE_SWIPE_TRAVEL_PX`; +`DISPLAY_W`/`DISPLAY_H` determine the rotation mapping (ESPHome: 90° `x=y, y=W-x-1`, +270° `x=H-y-1, y=x`, 180° mirrors both). The CYD keeps the LVGL gesture. The +`swipe_pages` setting remains the toggle; protocol, storage, and keys unchanged. -### Compatibiliteit 0.2.26 / firmware 0.2.22 +### Compatibility 0.2.28 / firmware 0.2.23 -Opslagversie en tegelprotocol blijven 1. Het layoutbericht krijgt het additieve veld -`keepalive` (seconden, 5–3600): het interval waarin de app de hele indeling herhaalt -(`KEEPALIVE_SECONDS` in `server.py`, 120 s sinds 0.2.20). De firmware leidt daar zijn -bewaking van de gegevensstroom uit af (twee rondes plus 60 s) en meldt daarna -"ESP Screens niet actief"; "HA niet verbonden" komt nu van ESPHome's eigen -API-verbindingsstatus (`api_is_connected()`), niet meer uit de leeftijd van het laatste -bericht. Zonder het veld rekent firmware 0.2.22 met 120 s; firmware tot 0.2.21 negeert -het veld en houdt zijn vaste 95 s, wat met een keepalive van 120 s de bekende -"HA niet verbonden"-flikkering geeft. Verander `KEEPALIVE_SECONDS` daarom alleen samen -met een firmwareflash van bestaande schermen, of houd hem onder 40 s. Geen gewijzigde -preferences of sleutels. +Firmware only. `cyd::TouchGuard` gets `configure(move_limit_px, min_press_ms)` +(called in `on_boot` from the substitutions `TOUCH_MOVE_LIMIT_PX` and +`TOUCH_MIN_PRESS_MS`; without the call, the old 18 px and 60 ms apply), tracks only +the contact id the touch started with, measures displacement as distance to a +reference point settled over four measurements, and reports via `reason()` why a tap +was rejected; `runtime_tiles::allowed()` logs that at INFO with tag `touch`. +Protocol, storage, preferences, and keys unchanged. -### Compatibiliteit 0.2.24 / firmware 0.2.20 +### Compatibility 0.2.27 (firmware stays 0.2.22) -Alleen intern: `Tile::forecast` en `Tile::hours` zijn vectoren (voorheen vaste arrays in -elke tegel); protocol en opslag ongewijzigd. De wizard-YAML krijgt `power_save_mode: none` -onder `wifi:`; de gedeelde pakketten bevatten geen wifi-blok, dus bestaande schermen -veranderen pas als de gebruiker de regel zelf toevoegt. +App only. `POST /api/install` (YAML download) has been removed; `POST +/api/firmware/profiles` writes the profile, fills in missing Wi-Fi keys in +`secrets.yaml` (only the missing lines, verified by re-parsing the result; +an invalid file is left untouched), and, given `target` (a reported USB port), +immediately starts `install`. Port and build slot are checked before +writing. The response contains `api_key`; the page shows it once +for the HA pairing. The job object gets `stage` (`config`, `compile`, `upload`). +`profile_meta` provides extra `screen` (profile uses this project's board package) +and `api_key`; `/api/inventory` gets the additive list `pending`, with +such profiles that have no paired screen. Storage, protocol, preferences, and keys +unchanged. -### Compatibiliteit 0.2.23 / firmware 0.2.19 +### Compatibility 0.2.26 / firmware 0.2.22 -Opslagversie en tegelprotocol blijven 1. `tiles[].options.controls` is additief: -`none` of een set uit `CONTROLS` in `core.py` per domein (climate `setpoint`/`mode`, +Storage version and tile protocol stay 1. The layout message gets the additive field +`keepalive` (seconds, 5–3600): the interval at which the app repeats the entire layout +(`KEEPALIVE_SECONDS` in `server.py`, 120 s since 0.2.20). The firmware derives its +data-flow monitoring from that (two rounds plus 60 s) and then reports +"ESP Screens not active"; "HA not connected" now comes from ESPHome's own +API connection status (`api_is_connected()`), no longer from the age of the last +message. Without the field, firmware 0.2.22 assumes 120 s; firmware up to 0.2.21 ignores +the field and keeps its fixed 95 s, which, with a 120 s keepalive, causes the known +"HA not connected" flicker. So only change `KEEPALIVE_SECONDS` together +with a firmware flash of existing screens, or keep it under 40 s. No changed +preferences or keys. + +### Compatibility 0.2.24 / firmware 0.2.20 + +Internal only: `Tile::forecast` and `Tile::hours` are vectors (previously fixed arrays in +each tile); protocol and storage unchanged. The wizard YAML gets `power_save_mode: none` +under `wifi:`; the shared packages contain no Wi-Fi block, so existing screens +only change once the user adds the line themselves. + +### Compatibility 0.2.23 / firmware 0.2.19 + +Storage version and tile protocol stay 1. `tiles[].options.controls` is additive: +`none` or a set from `CONTROLS` in `core.py` per domain (climate `setpoint`/`mode`, switch/light/fan `toggle` plus `brightness`/`speed`, vacuum/cover/timer `buttons`, cover `position`, media_player `volume`/`playback`, number `stepper`/`slider`, -select `stepper`, scene/script/button `run`). Op de draad stuurt de manager in -`o.controls` alleen de set die de kaart werkelijk toont: uitsluitend dubbelbreed, -weergave standaard en zonder mini-schuif; zonder keuze de eerste set van het -domein, bij `none` niets. Statusberichten krijgen de attributen `device_class`, -`hvac_action` en `is_volume_muted` (de enige boolean die meegaat). Oudere -firmware negeert het veld en toont de gewone brede kaart, daarom geen -`min_firmware`; het tegelpaneel meldt vanaf welke firmware het werkt. Een oude -editor die `controls` weglaat behoudt de opgeslagen keuze (zoals `background`). -De firmware bouwt het paneel lui per set (`layout_panel`), stuurt −/+ na 700 ms -als één `set_temperature`/`set_value` en laat de lokale waarde staan tot HA hem -meldt (of 10 s). De iconfonts krijgen 18 vaste bedieningsglyphs -(`tile_icons.FIXED`). Geen gewijzigde preferences of sleutels. +select `stepper`, scene/script/button `run`). On the wire, the manager sends in +`o.controls` only the set the card actually shows: double-width only, +default display, and without a mini-slider; without a choice, the domain's first +set; with `none`, nothing. Status messages get the attributes `device_class`, +`hvac_action`, and `is_volume_muted` (the only boolean included). Older +firmware ignores the field and shows the regular wide card, hence no +`min_firmware`; the tile panel reports from which firmware it works. An old +editor that omits `controls` keeps the stored choice (like `background`). +The firmware builds the panel lazily per set (`layout_panel`), sends −/+ after 700 ms +as a single `set_temperature`/`set_value`, and leaves the local value in place until HA +reports it (or 10 s). The icon fonts get 18 fixed control glyphs +(`tile_icons.FIXED`). No changed preferences or keys. -Ook additief in dezelfde release: `x.hours` (maximaal acht uren `t`, `c`, `h`, -`p`, `r`) en `p`/`r` per dag in `x.days` voor de weerkaart; `x.last` (unix-tijd) -voor scènes, scripts en knoppen; de attributen `humidity`, `wind_speed`, -`wind_speed_unit`, `apparent_temperature`, `fan_modes`, `swing_modes`, `fan_mode` -en `swing_mode`. `supported_features` mag boven de grens van een miljoen. Oudere -firmware negeert al deze velden. De bezig-status gebruikt LVGL's spinner; beide -bordprofielen bevatten daarvoor een verborgen `spinner` (`busy_spinner_seed`), -anders compileert ESPHome `LV_USE_SPINNER` als 0. +Also additive in the same release: `x.hours` (up to eight hours of `t`, `c`, `h`, +`p`, `r`) and `p`/`r` per day in `x.days` for the weather card; `x.last` (unix time) +for scenes, scripts, and buttons; the attributes `humidity`, `wind_speed`, +`wind_speed_unit`, `apparent_temperature`, `fan_modes`, `swing_modes`, `fan_mode`, +and `swing_mode`. `supported_features` may exceed the one-million limit. Older +firmware ignores all these fields. The busy status uses LVGL's spinner; both +board profiles include a hidden `spinner` for that (`busy_spinner_seed`), +otherwise ESPHome compiles `LV_USE_SPINNER` as 0. -### Compatibiliteit 0.2.12 / firmware 0.2.14 +### Compatibility 0.2.12 / firmware 0.2.14 -Opslagversie en tegelprotocol blijven 1. Additief: `tiles[].options.size` -(`single`/`wide`), nieuwe `display`-waarden (`forecast` voor weer, `graph` voor -sensoren, `digital`/`analog` voor `screen.clock`), de ingebouwde entity -`screen.clock` en de HA-domeinen `sun`, `timer` en `person`. Statusberichten -krijgen een optioneel object `x` met door de manager berekende waarden -(voorspellingsdagen, zontijden in de HA-tijdzone, timer-eindtijd als epoch). -Oudere firmware negeert `size`, nieuwe displays en `x`, maar weigert onbekende -domeinen in de hele indeling; de manager stuurt zo'n indeling daarom pas na -firmware 0.2.14 (`min_firmware`) en bewaart haar ondertussen. Voorspellingen -komen via `weather.get_forecasts` met `return_response`; zonder antwoord blijft -de weerkaart de gewone kaart. Geen gewijzigde preferences of sleutels. +Storage version and tile protocol stay 1. Additive: `tiles[].options.size` +(`single`/`wide`), new `display` values (`forecast` for weather, `graph` for +sensors, `digital`/`analog` for `screen.clock`), the built-in entity +`screen.clock`, and the HA domains `sun`, `timer`, and `person`. Status messages +get an optional object `x` with values computed by the manager +(forecast days, sun times in the HA timezone, timer end time as epoch). +Older firmware ignores `size`, new displays, and `x`, but refuses unknown +domains across the whole layout; the manager therefore only sends such a layout once +firmware 0.2.14 or newer is detected (`min_firmware`), holding it back in the meantime. Forecasts +come via `weather.get_forecasts` with `return_response`; without a response, +the weather card stays the regular card. No changed preferences or keys. -### Compatibiliteit 0.2.39 / firmware 0.2.33 +### Compatibility 0.2.39 / firmware 0.2.33 -Opslagversie en tegelprotocol blijven 1; geen nieuwe velden in `screens.json`, geen -gewijzigde preferences of sleutels. Alles is additief en per scherm gekoppeld aan de -gemelde `Schermfirmware`: +Storage version and tile protocol stay 1; no new fields in `screens.json`, no +changed preferences or keys. Everything is additive and, per screen, gated on the +reported screen firmware: -- **Transport.** Firmware 0.2.33 heeft de API-actie `screen_message` (één string - `message`, het complete JSON-bericht, maximaal 4096 bytes). De manager roept - `esphome._screen_message` aan zodra het scherm 0.2.33+ meldt - (`TRANSPORT_MIN_FIRMWARE` in `core.py`, `Manager.transport`); daaronder blijven de - base64-blokjes in de tekstentiteit `Tegelinstellingen`. De actie publiceert het - resultaat op dezelfde inbox-entiteit, dus de status op de beheerpagina blijft gelijk. - Het API-frame van ESPHome op de ESP32 is 32 KiB; `receive()` weigert boven 4096. -- **Keepalive.** Het layoutbericht krijgt `rev` (twaalf hex-tekens, `core.revision` - van het layoutbericht zonder `rev`); oudere firmware negeert het veld. Firmware - 0.2.33 bewaart de revisie en beantwoordt `{v:1, op:"ping", rev, keepalive}` met - `Gesynchroniseerd`/`Tegels laden` (revisie gelijk) of `Indeling opnieuw nodig` - (anders, of na een herstart). Een ping ververst het feed-venster (`last_received`) - net als een layout. De manager pingt elke `KEEPALIVE_SECONDS` (120 s) en herhaalt - alles pas na `FULL_REPEAT_SECONDS` (3600 s), of direct wanneer de inbox een status - uit `RESEND_STATES` meldt (`Klaar voor tegelconfiguratie`, `Indeling opnieuw - nodig`, `Tegels laden`), met een guard van 120 s na de laatste volledige verzending. - Firmware onder 0.2.33 krijgt elke 120 s de volledige herhaling zoals voorheen. - Een herhaald, ongewijzigd layoutbericht antwoordt nu `Gesynchroniseerd` in plaats - van `Indeling ontvangen`; `diagnostics/send_layout.py` accepteert beide. -- **Incrementeel.** `HomeAssistant.dirty` verzamelt de gewijzigde entity-id's; - `Manager.sync_one(..., dirty=...)` bouwt alleen die tegels en, als een van hen in - de bovenbalk staat, de balk. `dirty=None` (eerste ronde, nieuw register, offline - geweest) bouwt alles en stuurt de verschillen; `force=True` stuurt alles. `sent` - bewaart per scherm `{'layout','header','states','rev'}`. `Manager.screens()` - vervangt `inventory()[0]` in de lus en de updater: alleen de ESPHome-schermentiteiten - worden gelezen, gecachet op register-identiteit plus de status van die entiteiten. - `inventory()` (volledig) blijft voor de editor en `save()`. -- **Historie.** `Manager.history_loop` (eigen taak in `main()`) haalt per venster - (1/6/24 uur) één `recorder/statistics_during_period` op (`hour` voor 24 uur, - `5minute` daaronder, `types: [mean, state]`; `mean`, anders `state`), valt per - sensor zonder rijen terug op `GET /history/period`, en markeert gewijzigde - entiteiten dirty. Het `history`-veld in het statusbericht is ongewijzigd (24 - waarden, `None` tot de eerste). Een sensortegel gaat de eerste keer zonder - `history` de deur uit en krijgt hem seconden later; firmware toont dan de grafiek. -- **Diagnostiek (firmware).** De sensor `Uptime` (seconden, elke 15 s) is weg; - ervoor in de plaats `Opgestart` (`platform: uptime`, `type: timestamp`, device - class timestamp, één waarde per opstart; `time:` is al aanwezig). Bewust een nieuwe - entiteit: het entity-register van Home Assistant houdt bij de oude de eenheid `s` - vast en weigert dan een tijdstempel ("has a unit of measurement ... non-numeric - device class: timestamp", gezien op 2026-09-14). De ESPHome-integratie verwijdert - de oude `sensor._uptime` zelf zodra het scherm met de nieuwe firmware - verbindt. Verder `debug: update_interval` 300 s en de vier template-numbers op - `update_interval: never`, gepubliceerd vanuit `apply_screen_settings` als de waarde - afwijkt van hun laatste state (dus ook eenmaal na opstarten). +- **Transport.** Firmware 0.2.33 has the API action `screen_message` (one string + `message`, the complete JSON message, 4096 bytes max). The manager calls + `esphome._screen_message` as soon as the screen reports 0.2.33+ + (`TRANSPORT_MIN_FIRMWARE` in `core.py`, `Manager.transport`); below that, the + base64 chunks in the inbox text entity are still used. The action publishes the + result to the same inbox entity, so the status on the management page stays the same. + ESPHome's API frame on the ESP32 is 32 KiB; `receive()` refuses anything above 4096. +- **Keepalive.** The layout message gets `rev` (twelve hex characters, `core.revision` + of the layout message without `rev`); older firmware ignores the field. Firmware + 0.2.33 stores the revision and replies to `{v:1, op:"ping", rev, keepalive}` with + `Synced`/`Loading tiles` (revision matches) or `Resend needed` + (otherwise, or after a restart; firmware before 0.2.34 says `Gesynchroniseerd`/`Tegels laden`/`Indeling opnieuw nodig`). A ping refreshes the feed window (`last_received`) + just like a layout does. The manager pings every `KEEPALIVE_SECONDS` (120 s) and only repeats + everything after `FULL_REPEAT_SECONDS` (3600 s), or immediately when the inbox reports a status + from `RESEND_STATES` (`Ready for tile configuration`, `Resend needed`, `Loading tiles`, + and the Dutch originals of older firmware), with a 120 s guard after the last full send. + Firmware below 0.2.33 still gets the full repeat every 120 s as before. + A repeated, unchanged layout message now replies `Synced` instead + of `Layout received`; `diagnostics/send_layout.py` accepts both, in both languages. +- **Incremental.** `HomeAssistant.dirty` collects the changed entity IDs; + `Manager.sync_one(..., dirty=...)` builds only those tiles and, if one of them is in + the top bar, the bar too. `dirty=None` (first round, new registry, was + offline) builds everything and sends the diffs; `force=True` sends everything. `sent` + stores `{'layout','header','states','rev'}` per screen. `Manager.screens()` + replaces `inventory()[0]` in the loop and the updater: only the ESPHome screen entities + are read, cached on registry identity plus the status of those entities. + `inventory()` (full) remains for the editor and `save()`. +- **History.** `Manager.history_loop` (its own task in `main()`) fetches one + `recorder/statistics_during_period` call per window (1/6/24 hours) (`hour` for 24 hours, + `5minute` below that, `types: [mean, state]`; `mean`, otherwise `state`), and, per + sensor with no rows, falls back to `GET /history/period`, marking changed + entities dirty. The `history` field in the status message is unchanged (24 + values, `None` until the first). A sensor tile goes out the door without + `history` the first time and gets it seconds later; the firmware then shows the graph. +- **Diagnostics (firmware).** The `Uptime` sensor (seconds, every 15 s) is gone; + replaced by `Opgestart` (`Last boot` since firmware 0.2.34; `platform: uptime`, `type: timestamp`, device + class timestamp, one value per boot; `time:` is already present). Deliberately a new + entity: Home Assistant's entity registry keeps the unit `s` for the old + one and then refuses a timestamp ("has a unit of measurement ... non-numeric + device class: timestamp", seen on 2026-09-14). The ESPHome integration removes + the old `sensor._uptime` itself as soon as the screen connects with the + new firmware. Also `debug: update_interval` 300 s, and the four template numbers set to + `update_interval: never`, published from `apply_screen_settings` when the value + differs from their last state (so also once after boot). diff --git a/docs/TEGELS.md b/docs/TEGELS.md deleted file mode 100644 index b7cd412..0000000 --- a/docs/TEGELS.md +++ /dev/null @@ -1,183 +0,0 @@ -# Tegels instellen voor jouw Home Assistant - -Bewerk `device.yaml`, binnen de bestaande `substitutions:`. De basiscode hoef -je voor de onderstaande voorbeelden niet aan te passen. Elke wijziging vraagt -opnieuw bouwen en flashen. Doorloop eerst de [installatie](../README.md). - -## Entity-ID's verzamelen - -Open in HA **Ontwikkelaarstools → Statussen**. Zoek de gewenste entiteit en -kopieer de volledige ID, bijvoorbeeld `light.keuken`. Controleer status en -attributen. De schermtitel is vrij te kiezen; de entity-ID moet exact kloppen. -Test onder **Ontwikkelaarstools → Acties** de gewenste actie met die entiteit. -Een actie kan het echte apparaat bedienen: kies een geschikte test. -Zie [HA-acties](https://www.home-assistant.io/docs/scripts/perform-actions/). - -Maak eerst een lijst met positie, titel, entity-ID, tikactie en lange-drukactie. -Gebruik bestaande HA-scripts/scènes voor meerdere apparaten of complexe logica. - -## Posities en grenzen - -| Posities | Bedoeld gebruik in deze firmware | -|---|---| -| 1–6 | Hoofdtegels; light, switch, scene, script, fan, cover en climate | -| 6 | Ook de vacuumkaart; de kaart is specifiek met tegel 6 verbonden | -| 7 en 9 | Extra lichttegels met helderheid/kleurbediening | -| 8 en 10 | Extra status-/actietegels; geen volledige numerieke/climatekaart | - -`TILE_COUNT: "1"` tot `"10"` bepaalt hoeveel **opeenvolgende** posities zichtbaar -zijn. Tot en met zes zijn Vorige/Volgende verborgen; vanaf zeven staat 1–6 op -pagina één en de rest op pagina twee. Vrij scrollen is vervangen door vaste -pagina's. Laat ongebruikte posities op de neutrale voorbeeldentiteiten staan. -Verander geen rastermaten om tegels toe te voegen. - -## Belangrijkste instellingen - -| Instelling (`N` = tegelnummer) | Betekenis | -|---|---| -| `TILEN_ENTITY` | Entiteit waarop de actie wordt uitgevoerd | -| `TILEN_STATE_ENTITY` | Entiteit voor status/attributen; meestal dezelfde | -| `TILEN_TITLE` | Korte titel, liefst maximaal circa 12 tekens | -| `TILEN_TYPE` | Domein, bijvoorbeeld `light`, `scene`, `climate` | -| `TILEN_TAP_ACTION` | `auto`, `toggle` of `custom` voor onderstaande voorbeelden | -| `TILEN_TAP_SERVICE` | Expliciete HA-actie bij custom, bijvoorbeeld `script.turn_on` | -| `TILEN_LONGPRESS` | `none`, `slider` of (alleen tegel 6) `vacuum` | -| `TILEN_LONGPRESS_SLIDER` | `auto`; kiest passende regeling op basis van type | -| `TILEN_LABEL_ON/OFF` | Tekst voor eenvoudige aan/uitstatus | - -De voorbeeldconfig vult ook de overige velden in. Laat die staan tenzij je -ze bewust gebruikt. `STATE_ENTITY` kan bij een scène/script een echte lamp of -schakelaar volgen; een scène heeft zelf geen betrouwbare aan/uitstatus. - -## Kleur, wittemperatuur en helderheid - -Een RGB-/kleurenlamp met `LONGPRESS: "slider"` opent direct de lampkaart. -Deze bevat maximaal drie horizontale schuiven: - -- **Kleur:** een doorlopende regenboog (0–360° hue, 100% verzadiging). -- **Wittemperatuur:** warm naar koel, binnen de minimum-/maximum-Kelvinwaarden - die de lamp in Home Assistant meldt. -- **Helderheid:** 1–100%; aan/uit blijft via de tegel beschikbaar. - -De kleur- en witschuif verschijnen alleen als `supported_color_modes` deze -ondersteunt. Een uitsluitend dimbare lamp houdt de gewone helderheidskaart. -Er zijn geen extra tegelinstellingen nodig. Gebruik voor een lamp dezelfde -`ENTITY` en `STATE_ENTITY`. De kleurkaart volgt `hs_color`, `color_temp_kelvin`, -`min_color_temp_kelvin` en `max_color_temp_kelvin` van de bediende entiteit. -Ontbreekt het temperatuurbereik, dan wacht de witschuif op HA-gegevens. - -De kaart neemt bij openen de laatst ontvangen lampwaarden over. Slepen toont -lokaal de gekozen waarde; pas loslaten verstuurt één opdracht. Kleur kiest de -kleurmodus, wittemperatuur kiest de witmodus. Helderheid verandert de kleurmodus -niet. De regenboog regelt hue; pastel/verzadiging is geen aparte regelaar. -De hue-lijst wordt via ESPHome `data_template` naar HA gestuurd; zie de -[officiële API-documentatie](https://esphome.io/components/api/#homeassistantaction-action). - -De gedeelde implementatie staat in `light_controls.h`. Beide schermprofielen -gebruiken deze; de Guition heeft grotere regelaars, de CYD een compacte kaart. -`diagnostics/run_ui_test.py` controleert ook de schuifevents met vervangen -callbacks: geen echte HA-acties. Voor een veilige 40-secondenvoorbeeldkaart: - -```sh -python diagnostics/control_ui.py light_controls_preview --host guition-wallbox.local --name guition-wallbox -``` - -Gebruik voor CYD de eigen host en naam. Tijdens deze voorbeeldkaart worden -lichtopdrachten onderdrukt en verdwijnt de kaart vanzelf. Test vervolgens de -werkelijke lamp: lang indrukken, kleur kiezen, wit kiezen, helderheid aanpassen, -sluiten en heropenen. Controleer de fysieke lamp en de waarden in HA. - -## Voorbeelden - -Wijzig deze sleutels in de bestaande map; plak niet nogmaals `substitutions:`. - -Dimbare lamp op positie 1 (tik schakelt, lang indrukken opent de regeling): - -```yaml - TILE1_ENTITY: "light.keuken" - TILE1_STATE_ENTITY: "light.keuken" - TILE1_TITLE: "Keuken" - TILE1_TYPE: "light" - TILE1_TAP_ACTION: "auto" - TILE1_LONGPRESS: "slider" -``` - -Een niet-dimbare lamp/schakelaar krijgt `LONGPRESS: "none"`. Kleurbediening -vereist dat de lamp de betreffende kleurmodus ondersteunt. - -Scène op positie 3: - -```yaml - TILE3_ENTITY: "scene.avond" - TILE3_STATE_ENTITY: "light.keuken" - TILE3_TITLE: "Avond" - TILE3_TYPE: "scene" - TILE3_TAP_ACTION: "auto" - TILE3_LONGPRESS: "none" -``` - -Script op positie 4: - -```yaml - TILE4_ENTITY: "script.alles_uit" - TILE4_STATE_ENTITY: "script.alles_uit" - TILE4_TITLE: "Alles uit" - TILE4_TYPE: "script" - TILE4_TAP_ACTION: "auto" - TILE4_LONGPRESS: "none" -``` - -Airco op positie 5 (lang indrukken opent climate): - -```yaml - TILE5_ENTITY: "climate.woonkamer" - TILE5_STATE_ENTITY: "climate.woonkamer" - TILE5_TITLE: "Airco" - TILE5_TYPE: "climate" - TILE5_TAP_ACTION: "toggle" - TILE5_TAP_SERVICE: "climate.toggle" - TILE5_LONGPRESS: "slider" -``` - -Controleer `min_temp`, `max_temp`, `target_temp_step`, `hvac_modes` en de -ondersteunde fan-/swingmodes in HA. Deze kaart is primair voor een **enkel -setpoint**, niet een thermostaat die uitsluitend een temperatuurinterval -gebruikt. Test de modi van jouw integratie afzonderlijk; een andere airco -kan andere mogelijkheden hebben. - -Robotstofzuiger op positie 6 (tik start; lang indrukken opent de kaart): - -```yaml - TILE6_ENTITY: "vacuum.robot" - TILE6_STATE_ENTITY: "vacuum.robot" - TILE6_TITLE: "Stofzuiger" - TILE6_TYPE: "vacuum" - TILE6_TAP_ACTION: "custom" - TILE6_TAP_SERVICE: "vacuum.start" - TILE6_LONGPRESS: "vacuum" -``` - -De vacuumkaart gebruikt start/pauze/stop/terugkeren/lokaliseren en de -snelheidsnamen `quiet`, `balanced`, `turbo`, `max`. Vergelijk deze met de -ondersteunde acties en `fan_speed_list` in HA. De snelheidsknoppen moeten in -de basiscode aangepast worden als jouw integratie andere namen verwacht. -Dit is geen universele kaart voor ieder robotmodel. - -## Acties inschakelen en controleren - -Zet na kalibratie en HA-koppeling `DIRECT_ACTIONS: "true"`. Geef in de -ESPHome-integratie toestemming voor Home Assistant-acties. Gebruik geen -HA-automation die dezelfde schermactie daarnaast nogmaals uitvoert; dat kan -een dubbele toggle veroorzaken. Bij `DIRECT_ACTIONS: "false"` kan de -actie-sensor nog gebeurtenissen publiceren voor eigen automations. - -Flash `device.yaml` en test één tegel tegelijk: juiste status, juiste fysieke -actie, terugmelding vanuit HA en lange druk. Controleer ook dat externe -wijzigingen in HA op het scherm verschijnen. Voer daarna de -[acceptatietest](ACCEPTATIE.md) uit. - -De iconfonts bevatten de tegeliconenset uit `screen_manager/app/tile_icons.py`; -kies `TILEn_ICON` daaruit. Een ander icoon voeg je daar toe en draai je -`tools/generate_icons.py`. De bundel bevat de fonts lokaal. De oudere uitgebreide -[TILE_CONFIGURATION.md](../TILE_CONFIGURATION.md) beschrijft ook het oude -scrollprofiel; gebruik die niet als hardware-/layoutinstructie voor dit bord. diff --git a/docs/TEST_RESULTS_0240.md b/docs/TEST_RESULTS_0240.md new file mode 100644 index 0000000..7edacdf --- /dev/null +++ b/docs/TEST_RESULTS_0240.md @@ -0,0 +1,93 @@ +# Test results app 0.2.40 / firmware 0.2.34 (2026-09-15) + +ESP Screens in English: the screens, the management page, the Home Assistant entities +and the docs. See CHANGELOG 0.2.40 and RELEASING "Compatibility 0.2.40". + +## What changed + +- Text only on both boards and in the app: every on-screen label, status reply, log + line, editor string and guide. Labels follow Home Assistant's English names (climate + modes Heat/Cool, actions Heating/Cooling, Away, Returning to dock, weather conditions), + dates use English abbreviations (Mo 14 Sep), the top bar writes 21.5 °C and 1,249 W. +- Entity names in English (Tile settings, Screen firmware, Device name, IP address, + Guition screen type, Last boot, Normal/Standby/Night brightness, Standby after, + Calibrate touch). Home Assistant gives renamed ESPHome entities new entity IDs. +- App: `core.NAME_*` pairs and `RESEND_STATES` accept both languages; + `Manager.follow_renamed_inboxes` moves a screen's layout and update history to its new + inbox ID (`core.inbox_prefix`, `core.device_prefixes`, `Manager.aliases`, + `Updater.current_id`/`renamed`). +- Docs use Home Assistant 2026.9's menu names (Settings → Apps, App store, Settings → Tools); + guides renamed to `docs/TILES.md`, `CALIBRATING.md`, `ACCEPTANCE.md`, `TROUBLESHOOTING.md`. + +## Review method + +- Token-level diff of every changed Python, JavaScript, C++ and YAML file with string + literals and comments neutralised (YAML compared as parsed trees, lambdas as C++): 33 + code changes, all intended (dual matching, singular hour/year, number format, a log + filter). Every changed string that sits in a comparison, key or subscript was checked. +- No Home Assistant identifier (entity ID, `domain.service`, state or attribute name, + `mdi:` icon, event type, YAML key in doc examples) disappeared from any translated string. + UI terms and state labels were checked against the English translations of the running + HA 2026.9.1 (`frontend/get_translations` and the frontend's own `en` files). +- The 0.2.39 test suites were run against the new code: 15 Python and 4 C++ failures, + every one an assertion on Dutch text; nothing behavioural. +- A dictionary sweep of all string literals and every `text:` value in both board + profiles found Dutch labels the translation had missed (page navigation, climate + card, vacuum status, calibration hints, one entity name); fixed. + +## Automated + +- Python (`.venv-portal`): 139 tests OK, 9 new in `test_renamed_inbox.py` (slugs and + prefixes, discovery in both languages, a reflash moves layout plus update address and + result, lookups and saves through the old ID, inventory, inspector and save endpoints + called with the old ID, restart after a reflash adopts by device + prefix, other screens' orphaned layouts stay put, an existing layout on the new ID is + never overwritten, an update round that renames mid-update records success under the new + ID, a failing nightly round still notifies). +- C++ (clang++ -std=c++17 -Wall -Wextra -Werror -I.): 13/13 PASS. +- `generate_packages.py --check` green. +- ESPHome 2026.6.2 compile SUCCESS: `easy-cyd-device.yaml` (RAM 31.5%, flash 77.3%), + `easy-guition-device.yaml` (RAM 32.5%, flash 18.6%), manual `device.yaml` and + `guition-device.yaml`. + +## Hardware and Home Assistant (Home Assistant Yellow, HA 2026.9.1) + +- Local add-on 0.2.40 (SMB + Supervisor), GitHub add-on 0.2.39 stopped with its data kept + (Supervisor backup first). +- Guition on firmware 0.2.33 (Dutch entities) with app 0.2.40: discovered by its Dutch + names (firmware, board, node, IP), layout sent, status `Gesynchroniseerd`, top bar in + English number format, update offer 0.2.34 with profile and address. +- USB flash of the CYD with English entities: HA removed `text.cyd_2_8in_display_tegelinstellingen` + and created `text.cyd_2_8in_display_tile_settings` in the same second (also the other + renamed entities); the inbox reported `Ready for tile configuration`. +- USB flash of the Guition to 0.2.34 while app 0.2.40 ran: log + `Screen text.guition_wallbox_tegelinstellingen now reports as text.guition_wallbox_tile_settings; moved its layout`; + all 10 tiles, 2 pages, settings and top bar back on screen within seconds. +- Both boards on 0.2.34 (`device_info` compile times match the builds), status `Synced`. +- Rollback: USB flash of the CYD back to 0.2.33 while app 0.2.40 ran; the app moved the layout + back to `text.cyd_2_8in_display_tegelinstellingen` (status `Gesynchroniseerd`, 8 tiles). +- **Update** button on the Yellow for that 0.2.33 CYD (profile pointed at a local copy of this + commit's package): ESPHome inside the add-on built and uploaded over Wi-Fi (23 min, first build + with a fresh toolchain); the screen came back as `text.cyd_2_8in_display_tile_settings` on 0.2.34, + the round followed it through verify and settle and recorded "Updated to firmware 0.2.34." under + the new ID; layout intact, no update offered afterwards. +- Guition LVGL captures of every card type and detail card (demo layout, direct controls, + weather forecast, sensor graphs, person, timer, sun path, climate, vacuum, media, + cover, fan, number, select, scene, script), the climate mode picker and light controls: + all English, nothing clipped except long weather names in the five-day list (ellipsis, as before). +- Firmware self-tests: CYD 10 page checks and 50 overlay cycles PASS; Guition page checks + PASS with 18 tiles, light sliders PASS. +- Alerts through Home Assistant: `show_alert` with a timeout (event `timeout`), empty + title falls back to "Notification", a new alert replaces the old one (`replaced`), + `dismiss_alert` (`remote`); every ending arrived as `esphome.screen_alert`. +- Settings: `number.guition_wallbox_normal_brightness` 100 → 90 → 100 in HA; the app + stored each value within a second. +- Editor through ingress: screens, top bar sheet (date preview "Tu 15 Sep"), tile sheet, + Firmware & USB, New screen, Alerts cheatsheet (action names, fields, icons), saving a + new title reached the screen and was restored; no console errors. + +## Not tested / for the owner + +- The physical look of the alert card: the LVGL capture does not include the top layer. +- Touch on the physical screens after the flash (the owner's check). +- The Supervisor reports an add-on as `error` after a stop; 0.2.39 does the same, not changed here. diff --git a/docs/TILES.md b/docs/TILES.md new file mode 100644 index 0000000..90d85cf --- /dev/null +++ b/docs/TILES.md @@ -0,0 +1,184 @@ +# Configuring tiles for your Home Assistant + +Edit `device.yaml`, within the existing `substitutions:`. You don't need to +touch the base code for the examples below. Every change requires a rebuild +and reflash. Go through the [installation](../README.md) first. + +## Collecting entity IDs + +In HA, open **Settings → Tools → States** (**Developer tools → States** on older +HA versions). Find the entity you want and +copy the full ID, for example `light.kitchen`. Check its state and +attributes. The tile's title is free to choose; the entity ID must be exact. +Under **Settings → Tools → Actions**, test the intended action with that entity. +An action can control the real device: choose a suitable test. +See [HA actions](https://www.home-assistant.io/docs/scripts/perform-actions/). + +First make a list of position, title, entity ID, tap action, and long-press action. +Use existing HA scripts/scenes for multiple devices or complex logic. + +## Positions and limits + +| Positions | Intended use in this firmware | +|---|---| +| 1–6 | Main tiles; light, switch, scene, script, fan, cover, and climate | +| 6 | Also the vacuum card; the card is specifically tied to tile 6 | +| 7 and 9 | Extra light tiles with brightness/color control | +| 8 and 10 | Extra status/action tiles; no full number/climate card | + +`TILE_COUNT: "1"` through `"10"` determines how many **consecutive** positions are +visible. Up to and including six, Previous/Next stay hidden; from seven on, 1–6 sit +on page one and the rest on page two. Free scrolling has been replaced by fixed +pages. Leave unused positions on the neutral example entities. +Don't change grid dimensions to add tiles. + +## Key settings + +| Setting (`N` = tile number) | Meaning | +|---|---| +| `TILEN_ENTITY` | Entity the action is performed on | +| `TILEN_STATE_ENTITY` | Entity for status/attributes; usually the same one | +| `TILEN_TITLE` | Short title, ideally about 12 characters max | +| `TILEN_TYPE` | Domain, for example `light`, `scene`, `climate` | +| `TILEN_TAP_ACTION` | `auto`, `toggle`, or `custom` for the examples below | +| `TILEN_TAP_SERVICE` | Explicit HA action when using custom, for example `script.turn_on` | +| `TILEN_LONGPRESS` | `none`, `slider`, or (tile 6 only) `vacuum` | +| `TILEN_LONGPRESS_SLIDER` | `auto`; picks a suitable control based on type | +| `TILEN_LABEL_ON/OFF` | Text for a simple on/off status | + +The example config also fills in the remaining fields. Leave those as they are unless +you deliberately use them. For a scene/script, `STATE_ENTITY` can follow a real light or +switch; a scene itself has no reliable on/off status. + +## Color, white temperature, and brightness + +An RGB/color light with `LONGPRESS: "slider"` opens the light card directly. +It contains up to three horizontal sliders: + +- **Color:** a continuous rainbow (0–360° hue, 100% saturation). +- **White temperature:** warm to cool, within the minimum/maximum Kelvin values + the light reports in Home Assistant. +- **Brightness:** 1–100%; on/off stays available via the tile. + +The color and white sliders only appear if `supported_color_modes` supports +them. A dimmable-only light keeps the regular brightness card. +No extra tile settings are needed. For a light, use the same +`ENTITY` and `STATE_ENTITY`. The color card follows `hs_color`, `color_temp_kelvin`, +`min_color_temp_kelvin`, and `max_color_temp_kelvin` of the controlled entity. +If the temperature range is missing, the white slider waits for HA data. + +When opened, the card takes over the last received light values. Dragging shows +the chosen value locally; only releasing sends a single command. Color selects the +color mode, white temperature selects the white mode. Brightness doesn't change the +color mode. The rainbow controls hue; pastel/saturation isn't a separate control. +The hue list is sent to HA via ESPHome `data_template`; see the +[official API documentation](https://esphome.io/components/api/#homeassistantaction-action). + +The shared implementation lives in `light_controls.h`. Both screen profiles +use it; the Guition has larger controls, the CYD a compact card. +`diagnostics/run_ui_test.py` also checks the slider events with swapped-in +callbacks: no real HA actions. For a safe 40-second preview card: + +```sh +python diagnostics/control_ui.py light_controls_preview --host guition-wallbox.local --name guition-wallbox +``` + +For the CYD, use its own host and name. During this preview card, +light commands are suppressed and the card disappears on its own. Then test the +actual light: long press, choose a color, choose white, adjust brightness, +close, and reopen. Check the physical light and the values in HA. + +## Examples + +Change these keys in the existing map; don't paste `substitutions:` a second time. + +Dimmable light at position 1 (tap toggles, long press opens the control): + +```yaml + TILE1_ENTITY: "light.kitchen" + TILE1_STATE_ENTITY: "light.kitchen" + TILE1_TITLE: "Kitchen" + TILE1_TYPE: "light" + TILE1_TAP_ACTION: "auto" + TILE1_LONGPRESS: "slider" +``` + +A non-dimmable light/switch gets `LONGPRESS: "none"`. Color control +requires that the light supports the relevant color mode. + +Scene at position 3: + +```yaml + TILE3_ENTITY: "scene.evening" + TILE3_STATE_ENTITY: "light.kitchen" + TILE3_TITLE: "Evening" + TILE3_TYPE: "scene" + TILE3_TAP_ACTION: "auto" + TILE3_LONGPRESS: "none" +``` + +Script at position 4: + +```yaml + TILE4_ENTITY: "script.all_off" + TILE4_STATE_ENTITY: "script.all_off" + TILE4_TITLE: "All off" + TILE4_TYPE: "script" + TILE4_TAP_ACTION: "auto" + TILE4_LONGPRESS: "none" +``` + +AC at position 5 (long press opens climate): + +```yaml + TILE5_ENTITY: "climate.living_room" + TILE5_STATE_ENTITY: "climate.living_room" + TILE5_TITLE: "AC" + TILE5_TYPE: "climate" + TILE5_TAP_ACTION: "toggle" + TILE5_TAP_SERVICE: "climate.toggle" + TILE5_LONGPRESS: "slider" +``` + +Check `min_temp`, `max_temp`, `target_temp_step`, `hvac_modes`, and the +supported fan/swing modes in HA. This card is primarily for a **single +setpoint**, not a thermostat that only uses a temperature range. +Test your integration's modes individually; a different AC unit +may have different capabilities. + +Robot vacuum at position 6 (tap starts; long press opens the card): + +```yaml + TILE6_ENTITY: "vacuum.robot" + TILE6_STATE_ENTITY: "vacuum.robot" + TILE6_TITLE: "Vacuum" + TILE6_TYPE: "vacuum" + TILE6_TAP_ACTION: "custom" + TILE6_TAP_SERVICE: "vacuum.start" + TILE6_LONGPRESS: "vacuum" +``` + +The vacuum card uses start/pause/stop/return/locate and the +speed names `quiet`, `balanced`, `turbo`, `max`. Compare these with the +supported actions and `fan_speed_list` in HA. The speed buttons need to be +adjusted in the base code if your integration expects different names. +This is not a universal card for every robot model. + +## Enabling and checking actions + +After calibration and HA pairing, set `DIRECT_ACTIONS: "true"`. In the +ESPHome integration, grant permission for Home Assistant actions. Don't use an +HA automation that also runs the same screen action again separately; that can +cause a double toggle. With `DIRECT_ACTIONS: "false"`, the action sensor can +still publish events for your own automations. + +Flash `device.yaml` and test one tile at a time: correct status, correct physical +action, feedback from HA, and long press. Also check that external +changes in HA appear on the screen. Then run the +[acceptance test](ACCEPTANCE.md). + +The icon fonts contain the tile icon set from `screen_manager/app/tile_icons.py`; +pick `TILEn_ICON` from there. To add a different icon, add it there and run +`tools/generate_icons.py`. The bundle includes the fonts locally. The older, more extensive +[TILE_CONFIGURATION.md](../TILE_CONFIGURATION.md) also describes the removed +scroll profile as a historical record; don't use it as hardware/layout instructions for this board. diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md new file mode 100644 index 0000000..9509d08 --- /dev/null +++ b/docs/TROUBLESHOOTING.md @@ -0,0 +1,32 @@ +# Troubleshooting + +| Symptom | Check first | +|---|---| +| No USB port | Data cable, a different USB port, board power; compare `python -m serial.tools.list_ports` before/after connecting. Install the USB chip vendor's driver if needed. | +| Permission denied on Linux | Access to the serial device group (often `dialout`); log in again after a group change. | +| Port busy | Stop ESPHome logs, the IDE monitor, and other serial readers. One reader at a time. | +| Upload keeps connecting | Check the correct board/port. Hold BOOT while connecting and release afterward if the board requires it. | +| Build fails | Python 3.11–3.14 and ESPHome 2026.6.2; the full unpacked folder, local fonts/components present, internet, and disk space. Save the first real error from the build log. | +| White/black or wrongly rotated image | Check the ILI9341 + XPT2046 variant. Don't blindly copy the pins, rotation, or SPI settings from a different CYD model. | +| No measurement screen | The upload must be complete, not just the build. Flash with `-s CALIBRATION_ON_BOOT true`. Look for `USB calibration ready` in the USB logs; a normal boot shows no crosshairs. | +| No measurements | Five crosshairs visible, other log reader closed, correct port. Press Enter in the wizard first, then tap the requested point three times, releasing in between. | +| Tap jumps to a different spot | Check the independent `verify`, your own `calibration.yaml`, and a fixed orientation. Don't use the same taps to both fit and verify. | +| Fit reports spread/error | Measure again following the prompts. Don't fix unstable ADC readings by raising the tolerance; check power, cable, and touch hardware. | +| Duplicate HA action | Check both the touch logs and HA automations. Don't use direct actions together with an automation on the action sensor for the same operation. | +| Wi-Fi doesn't connect | 2.4GHz, correct local secrets, network range. USB calibration doesn't need HA. | +| HA doesn't see the board | Manually use the IP from the logs; port 6053 reachable, correct encryption key, no guest network isolation. | +| Status works but action doesn't | Check the HA option for allowed device actions, `DIRECT_ACTIONS: "true"`, the real entity ID, and a supported action. | +| Vacuum/climate partly usable | Supported modes/attributes differ per integration; see TILES.md and test the same action in HA first. | +| Wrong pagination | `TILE_COUNT` is 1–10; hidden buttons up to six, two pages from seven on. Reflash after changing it. | +| Freeze/reset | Keep the USB log, check the reset reason/power, and run the render test. Note the action and time. | + +For a USB log (stop other readers): + +```sh +python diagnostics/capture_serial.py --port --seconds 120 +``` + +The existing `CYD_STABILITY.md` and `TEST_RESULTS.md` describe earlier +investigation on one specific board. Their measurements are not calibration for +a new board. Start at [README.md](../README.md), keep a working +local configuration, and change one cause at a time. diff --git a/guition-4848s040.yaml b/guition-4848s040.yaml index 15d501e..1f5d0a8 100644 --- a/guition-4848s040.yaml +++ b/guition-4848s040.yaml @@ -1,7 +1,7 @@ # 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: - SCREEN_FIRMWARE_VERSION: "0.2.33" + SCREEN_FIRMWARE_VERSION: "0.2.34" # 0: no drift limit; LVGL decides, releasing inside the tile is a tap, leaving it is not # (like iOS). The CYD keeps a limit because its resistive panel jumps. TOUCH_MOVE_LIMIT_PX: "0" @@ -20,7 +20,7 @@ substitutions: 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 + ROOM_NAME: "Living room" # room name shown in the top-left header TIME_24H: "true" # true = 24h format, false = 12h format ## DIM settings @@ -34,24 +34,23 @@ substitutions: NIGHT_DIM_BRIGHTNESS: "10" # backlight brightness in percent at night # ============================================================ - # ACCENTKLEUREN — automatisch bepaald uit het Home Assistant-domein + # ACCENT COLOURS — determined automatically from the Home Assistant domain # - # Het domein is het stuk voor de punt in het entity_id: - # script.goede_morgen -> "script" -> ACCENT_ACTION - # light.woonkamer_2 -> "light" -> ACCENT_LIGHT + # The domain is the part before the dot in the entity_id: + # script.good_morning -> "script" -> ACCENT_ACTION + # light.living_room_2 -> "light" -> ACCENT_LIGHT # vacuum.s8 -> "vacuum" -> ACCENT_DEVICE # - # 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. + # That's Home Assistant's own grouping, so a tile automatically gets the + # right colour as soon as you put an entity in it — you no longer have + # to pick colours per tile. The distinction that matters: a script/scene + # fires, a device you turn on or off. Those two shouldn't look the same. # - # 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). + # Set TILE*_CIRCLE_ACTIVE_COLOR / TILE*_ICON_DISABLED_COLOR to + # ${ACCENT_AUTO} to use this, or to a real hex value to colour that + # one tile by hand (see tile 8 — red as soon as there's mail). # ============================================================ - ACCENT_AUTO: "0x1000000" # sentinel: bewust buiten het 24-bits kleurbereik + ACCENT_AUTO: "0x1000000" # sentinel: deliberately outside the 24-bit colour range ACCENT_ACTION: "0x8B5CF6" # script / scene / button / input_button / automation ACCENT_LIGHT: "0xFEC600" # light ACCENT_DEVICE: "0x00C5EC" # switch, fan, cover, climate, vacuum, media_player, rest @@ -67,16 +66,16 @@ substitutions: # ============================================================ # --- 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_ENTITY: "light.example_lamp" # main Home Assistant entity for this tile + TILE1_STATE_ENTITY: "light.example_lamp" # entity used for active/inactive state styling TILE1_TITLE: "Lamp" # tile title shown in the UI TILE1_ICON: "\U000F001B" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) TILE1_TYPE: "light" # tile type: light | fan | switch | scene | script | cover | climate TILE1_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset | custom TILE1_LONGPRESS: "slider" # long press mode: none | slider | action TILE1_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text - TILE1_LABEL_OFF: "Uit" # text shown when entity is off - TILE1_LABEL_ON: "Aan" # text shown for text-mode tiles when entity is on + TILE1_LABEL_OFF: "Off" # text shown when entity is off + TILE1_LABEL_ON: "On" # text shown for text-mode tiles when entity is on TILE1_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") # --- colors --- @@ -116,16 +115,16 @@ substitutions: # ============================================================ # --- 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_ENTITY: "switch.example_plug" # main Home Assistant entity for this tile + TILE2_STATE_ENTITY: "switch.example_plug" # entity used for active/inactive state styling + TILE2_TITLE: "Plug" # tile title shown in the UI TILE2_ICON: "\U000F0387" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) TILE2_TYPE: "switch" # tile type: light | fan | switch | scene | script | cover | climate TILE2_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset | custom TILE2_LONGPRESS: "none" # long press mode: none | slider | action TILE2_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text - TILE2_LABEL_OFF: "Uit" # text shown when entity is off - TILE2_LABEL_ON: "Aan" # text shown for text-mode tiles when entity is on + TILE2_LABEL_OFF: "Off" # text shown when entity is off + TILE2_LABEL_ON: "On" # text shown for text-mode tiles when entity is on TILE2_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") # --- colors --- @@ -165,16 +164,16 @@ substitutions: # ============================================================ # --- 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_ENTITY: "scene.example_evening" # main Home Assistant entity for this tile + TILE3_STATE_ENTITY: "scene.example_evening" # entity used for active/inactive state styling + TILE3_TITLE: "Evening" # tile title shown in the UI TILE3_ICON: "\U000F02E3" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) TILE3_TYPE: "scene" # tile type: light | fan | switch | scene | script | cover | climate TILE3_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset | custom TILE3_LONGPRESS: "none" # long press mode: none | slider | action TILE3_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text - TILE3_LABEL_OFF: "Uit" # text shown when entity is off - TILE3_LABEL_ON: "Aan" # text shown for text-mode tiles when entity is on + TILE3_LABEL_OFF: "Off" # text shown when entity is off + TILE3_LABEL_ON: "On" # text shown for text-mode tiles when entity is on TILE3_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") # --- colors --- @@ -214,16 +213,16 @@ substitutions: # ============================================================ # --- 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_ENTITY: "script.example_off" # main Home Assistant entity for this tile + TILE4_STATE_ENTITY: "script.example_off" # entity used for active/inactive state styling + TILE4_TITLE: "All off" # tile title shown in the UI TILE4_ICON: "\U000F059C" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) TILE4_TYPE: "script" # tile type: light | fan | switch | scene | script | cover | climate TILE4_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset | custom TILE4_LONGPRESS: "none" # long press mode: none | slider | action TILE4_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text - TILE4_LABEL_OFF: "Uit" # text shown when entity is off - TILE4_LABEL_ON: "Aan" # text shown for text-mode tiles when entity is on + TILE4_LABEL_OFF: "Off" # text shown when entity is off + TILE4_LABEL_ON: "On" # text shown for text-mode tiles when entity is on TILE4_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") # --- colors --- @@ -263,16 +262,16 @@ substitutions: # ============================================================ # --- 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_ENTITY: "climate.example_ac" # main Home Assistant entity for this tile + TILE5_STATE_ENTITY: "climate.example_ac" # entity used for active/inactive state styling + TILE5_TITLE: "AC" # tile title shown in the UI TILE5_ICON: "\U000F0425" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) TILE5_TYPE: "climate" # tile type: light | fan | switch | scene | script | cover | climate TILE5_TAP_ACTION: "toggle" # tap action: auto | toggle | activate | fan_toggle_preset | custom TILE5_LONGPRESS: "slider" # long press mode: none | slider | action TILE5_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text - TILE5_LABEL_OFF: "Uit" # text shown when entity is off (closed) - TILE5_LABEL_ON: "Aan" # text shown when entity is on (open) + TILE5_LABEL_OFF: "Off" # text shown when entity is off (closed) + TILE5_LABEL_ON: "On" # text shown when entity is on (open) TILE5_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") # --- colors --- @@ -312,16 +311,16 @@ substitutions: # ============================================================ # --- 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_ENTITY: "vacuum.example_robot" # main Home Assistant entity for this tile + TILE6_STATE_ENTITY: "vacuum.example_robot" # entity used for active/inactive state styling + TILE6_TITLE: "Vacuum" # tile title shown in the UI TILE6_ICON: "\U000F070D" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) TILE6_TYPE: "vacuum" # tile type: light | fan | switch | scene | script | cover | climate TILE6_TAP_ACTION: "custom" # tap action: auto | toggle | activate | fan_toggle_preset | custom TILE6_LONGPRESS: "vacuum" # long press mode: none | slider | action TILE6_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text - TILE6_LABEL_OFF: "Uit" # text shown when entity is off - TILE6_LABEL_ON: "Aan" # text shown for text-mode tiles when entity is on + TILE6_LABEL_OFF: "Off" # text shown when entity is off + TILE6_LABEL_ON: "On" # text shown for text-mode tiles when entity is on TILE6_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") # --- colors --- @@ -355,22 +354,22 @@ substitutions: 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. + # TILE 7 — Table lamp + # Brightness-only lamp (HA reports supported_color_modes: ['brightness']), + # so the colour button in the slider overlay stays hidden automatically. # ============================================================ # --- basic settings --- - TILE7_ENTITY: "light.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_ENTITY: "light.unused_7" # main Home Assistant entity for this tile + TILE7_STATE_ENTITY: "light.unused_7" # entity used for active/inactive state styling + TILE7_TITLE: "Tile 7" # tile title shown in the UI TILE7_ICON: "\U000F06B5" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) TILE7_TYPE: "light" # tile type: light | fan | switch | scene | script | cover | climate TILE7_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset TILE7_LONGPRESS: "none" # long press mode: none | slider | action | vacuum TILE7_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text - TILE7_LABEL_OFF: "Uit" # text shown when entity is off - TILE7_LABEL_ON: "Aan" # text shown for text-mode tiles when entity is on + TILE7_LABEL_OFF: "Off" # text shown when entity is off + TILE7_LABEL_ON: "On" # text shown for text-mode tiles when entity is on TILE7_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") # --- colors --- @@ -390,7 +389,7 @@ substitutions: 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_SLIDER: "auto" # auto = brightness for a light TILE7_LONGPRESS_OFF_VALUE: "0" # initial slider value when entity is off # if LONGPRESS = action: TILE7_LONGPRESS_ACTION: "" # entity_id to fire on long press @@ -398,27 +397,27 @@ substitutions: 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. + # TILE 8 — Mailbox reset + # input_select.mailbox_status has options ['Full', 'Empty']. + # The tile lights up as soon as the status is "Full" (ACTIVE_STATE), and a + # short tap sets it back to "Empty" via input_select.select_option. # ============================================================ # --- basic settings --- - TILE8_ENTITY: "light.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_ENTITY: "light.unused_8" # main Home Assistant entity for this tile + TILE8_STATE_ENTITY: "light.unused_8" # entity used for active/inactive state styling + TILE8_TITLE: "Tile 8" # tile title shown in the UI TILE8_ICON: "\U000F06EE" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) - TILE8_TYPE: "light" # 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 + TILE8_TYPE: "light" # generic type; the real behavior comes from TAP_SERVICE below + TILE8_TAP_ACTION: "auto" # activate + TAP_SERVICE = a standalone service call + TILE8_LONGPRESS: "none" # long press only sends the event to HA + TILE8_VALUE_MODE: "auto" # shows LABEL_ON / LABEL_OFF + TILE8_LABEL_OFF: "Off" # text shown when entity is off / inactive + TILE8_LABEL_ON: "On" # text shown when entity is active + TILE8_ACTIVE_STATE: "" # tile is "on" as soon as the status is exactly "Full" # --- colors --- - TILE8_CIRCLE_ACTIVE_COLOR: "0xE5484D" # rood zodra er post ligt + TILE8_CIRCLE_ACTIVE_COLOR: "0xE5484D" # red as soon as there's mail TILE8_CIRCLE_DISABLED_COLOR: "0xF0F0F0" # icon circle color when inactive TILE8_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active TILE8_ICON_DISABLED_COLOR: "0x9E9E9E" # icon color when inactive @@ -430,35 +429,35 @@ substitutions: TILE8_VALUE_DISABLED_COLOR: "0x616161" # 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 + TILE8_TAP_SERVICE: "" # sets the input_select to a fixed option + TILE8_TAP_PARAM_KEY: "" # parameter name for input_select.select_option + TILE8_TAP_PARAM_VAL: "" # the "reset" value # if LONGPRESS = slider: - TILE8_LONGPRESS_SLIDER: "auto" # niet in gebruik (LONGPRESS = none) - TILE8_LONGPRESS_OFF_VALUE: "0" # niet in gebruik + TILE8_LONGPRESS_SLIDER: "auto" # not in use (LONGPRESS = none) + TILE8_LONGPRESS_OFF_VALUE: "0" # not in use # if LONGPRESS = action: TILE8_LONGPRESS_ACTION: "" # entity_id to fire on long press TILE8_LONGPRESS_ACTION_TYPE: "" # entity type (empty = same as TILE*_TYPE) TILE8_LONGPRESS_ACTION_SERVICE: "" # service to call (default: activate/turn_on) # ============================================================ - # TILE 9 — 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. + # TILE 9 — Living room + # Moved from tile 2 to row 5 to make room for Ambient. + # Colour lamp (color_temp / rgb / xy), so the colour button in the + # slider overlay appears automatically. # ============================================================ # --- basic settings --- - TILE9_ENTITY: "light.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_ENTITY: "light.unused_9" # main Home Assistant entity for this tile + TILE9_STATE_ENTITY: "light.unused_9" # entity used for active/inactive state styling + TILE9_TITLE: "Tile 9" # tile title shown in the UI TILE9_ICON: "\U000F04B9" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) TILE9_TYPE: "light" # tile type: light | fan | switch | scene | script | cover | climate TILE9_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset TILE9_LONGPRESS: "none" # long press mode: none | slider | action | vacuum TILE9_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text - TILE9_LABEL_OFF: "Uit" # text shown when entity is off - TILE9_LABEL_ON: "Aan" # text shown for text-mode tiles when entity is on + TILE9_LABEL_OFF: "Off" # text shown when entity is off + TILE9_LABEL_ON: "On" # text shown for text-mode tiles when entity is on TILE9_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") # --- colors --- @@ -478,7 +477,7 @@ substitutions: 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_SLIDER: "auto" # auto = brightness for a light TILE9_LONGPRESS_OFF_VALUE: "0" # initial slider value when entity is off # if LONGPRESS = action: TILE9_LONGPRESS_ACTION: "" # entity_id to fire on long press @@ -486,26 +485,26 @@ substitutions: 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. + # TILE 10 — Alarm on/off + # input_boolean.alarm_engaged. An input_boolean doesn't respond to + # switch.toggle, hence the explicit TAP_SERVICE. # ============================================================ # --- basic settings --- - TILE10_ENTITY: "light.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_ENTITY: "light.unused_10" # main Home Assistant entity for this tile + TILE10_STATE_ENTITY: "light.unused_10" # entity used for active/inactive state styling + TILE10_TITLE: "Tile 10" # tile title shown in the UI TILE10_ICON: "\U000F002A" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) - TILE10_TYPE: "light" # generiek aan/uit-type; de service hieronder doet het echte werk + TILE10_TYPE: "light" # generic on/off type; the service below does the real work TILE10_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset - TILE10_LONGPRESS: "none" # long press 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 + TILE10_LONGPRESS: "none" # long press only sends the event to HA + TILE10_VALUE_MODE: "auto" # shows LABEL_ON / LABEL_OFF + TILE10_LABEL_OFF: "Off" # text shown when entity is off + TILE10_LABEL_ON: "On" # text shown when entity is on + TILE10_ACTIVE_STATE: "" # input_boolean just uses on/off # --- colors --- - TILE10_CIRCLE_ACTIVE_COLOR: "${ACCENT_AUTO}" # input_boolean -> ACCENT_DEVICE (cyaan) + TILE10_CIRCLE_ACTIVE_COLOR: "${ACCENT_AUTO}" # input_boolean -> ACCENT_DEVICE (cyan) TILE10_CIRCLE_DISABLED_COLOR: "0xF0F0F0" # icon circle color when inactive TILE10_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active TILE10_ICON_DISABLED_COLOR: "0x9E9E9E" # icon color when inactive @@ -517,12 +516,12 @@ substitutions: TILE10_VALUE_DISABLED_COLOR: "0x616161" # value color when inactive # --- advanced settings --- - TILE10_TAP_SERVICE: "" # switch.toggle werkt niet op een input_boolean + TILE10_TAP_SERVICE: "" # switch.toggle doesn't work on an input_boolean TILE10_TAP_PARAM_KEY: "" # optional extra service parameter key TILE10_TAP_PARAM_VAL: "" # optional extra service parameter value # if LONGPRESS = slider: - TILE10_LONGPRESS_SLIDER: "auto" # niet in gebruik (LONGPRESS = none) - TILE10_LONGPRESS_OFF_VALUE: "0" # niet in gebruik + TILE10_LONGPRESS_SLIDER: "auto" # not in use (LONGPRESS = none) + TILE10_LONGPRESS_OFF_VALUE: "0" # not in use # if LONGPRESS = action: TILE10_LONGPRESS_ACTION: "" # entity_id to fire on long press TILE10_LONGPRESS_ACTION_TYPE: "" # entity type (empty = same as TILE*_TYPE) @@ -564,7 +563,7 @@ substitutions: ALERT_BUTTON_H: "60" ALERT_BUTTON_INSET: "22" ALERT_TITLE_MAX: "64" # bytes; the label ends one line with an ellipsis - ALERT_BUTTON_MAX: "16" # bytes on the Oké button + ALERT_BUTTON_MAX: "16" # bytes on the OK button ALERT_SUBTITLE_MAX: "240" # bytes; wraps over the rows the card has OVERLAY_SLIDER_W: "144" OVERLAY_SLIDER_H: "300" @@ -696,7 +695,7 @@ esphome: id(active_climate_swing_modes) = t.swing_modes; id(active_climate_swing_mode) = t.swing_mode; id(open_value_overlay).execute(t.name, t.entity, t.domain(), "auto", t.active(), 0, t.brightness, t.percentage, t.position, runtime_tiles::icon_for(t), - 0xD5EEFC, 0x00A6ED, 0x1B1B1B, "Uit", "Aan", t.modes, + 0xD5EEFC, 0x00A6ED, 0x1B1B1B, "Off", "On", t.modes, t.current, t.target, t.humidity, t.state, t.hvac_modes, std::to_string(t.minimum), std::to_string(t.maximum), std::to_string(t.step)); }; @@ -712,8 +711,8 @@ esphome: id(active_climate_fan_modes) = t.fan_modes; id(active_climate_fan_mode) = t.fan_mode; id(active_climate_swing_modes) = t.swing_modes; id(active_climate_swing_mode) = t.swing_mode; char buf[72]; - if (std::isfinite(t.current)) snprintf(buf, sizeof(buf), "Nu: %.1f°", t.current); - else snprintf(buf, sizeof(buf), "Nu: --"); + if (std::isfinite(t.current)) snprintf(buf, sizeof(buf), "Now: %.1f°", t.current); + else snprintf(buf, sizeof(buf), "Now: --"); lv_label_set_text(id(climate_current_label), buf); if (std::isfinite(t.target)) { int target = cyd::quantize_temperature(lround(t.target * 10), id(active_climate_min_x10), id(active_climate_max_x10), id(active_climate_step_x10)); @@ -766,8 +765,8 @@ psram: 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. + # Set to DEBUG or VERBOSE temporarily for diagnosis. Note: ESPHome logs + # its configuration lines at CONFIG level, which are invisible at INFO. level: DEBUG logs: sensor: WARN @@ -853,7 +852,7 @@ api: then: - script.execute: ui_self_test # Home Assistant automations: esphome._show_alert puts a card over the whole - # screen (README, "Alert vanuit een automatisering"); dismiss_alert removes it again. + # screen (README, "Alert from an automation"); dismiss_alert removes it again. - action: show_alert variables: title: string @@ -987,10 +986,10 @@ globals: 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). + # Debounce for do_tile_action: protects against repeated events from + # multiple separate press/release cycles for one physical touch (noise in + # the resistive layer), causing the same HA action to fire multiple times + # within a few hundred ms (observed: script.ambient_morning 6x in 8s). - id: last_action_entity type: std::string restore_value: no @@ -1363,8 +1362,8 @@ font: size: 18 glyphs: *tile_icons -# Geheugen zichtbaar maken: dit bord heeft geen PSRAM, dus de vrije heap -# bepaalt hoeveel UI erbij kan. Verschijnt ook als entity in Home Assistant. +# Make memory visible: this board has no PSRAM, so the free heap determines +# how much UI still fits. Also shows up as an entity in Home Assistant. debug: # Heap figures every five minutes: enough to spot a leak, few recorder rows. update_interval: 300s @@ -1391,7 +1390,7 @@ sensor: # "s" in its registry and would refuse a timestamp under that name. - platform: uptime type: timestamp - name: "Opgestart" + name: "Last boot" id: boot_time - platform: homeassistant @@ -1466,17 +1465,17 @@ sensor: 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. + # Tile 7 (Table lamp) is a brightness-only light: just brightness. + # Deliberately no percentage / cover_position / climate sensors — every + # HA subscription costs heap on a board without PSRAM. - platform: homeassistant id: tile7_brightness entity_id: ${TILE7_ENTITY} attribute: brightness on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - # Tegel 9 (Woonkamer) is een kleurenlamp: brightness + supported_color_modes. - # Geen percentage / cover / climate — die leest deze tegel nooit. + # Tile 9 (Living room) is a colour lamp: brightness + supported_color_modes. + # No percentage / cover / climate — this tile never reads those. - platform: homeassistant id: tile9_brightness entity_id: ${TILE9_ENTITY} @@ -1647,24 +1646,24 @@ binary_sensor: # ---------------------------------------------------------- text_sensor: - platform: template - name: "Guition schermtype" + name: "Guition screen type" entity_category: diagnostic update_interval: never lambda: 'return {"guition"};' - platform: template - name: "Schermfirmware" + name: "Screen firmware" entity_category: diagnostic lambda: 'return {"${SCREEN_FIRMWARE_VERSION}"};' update_interval: 60s # ESP Screen Manager matches the screen to its YAML profile and OTA address. - platform: template - name: "Apparaatnaam" + name: "Device name" entity_category: diagnostic lambda: 'return {"${DEVICE_NAME}"};' update_interval: 60s - platform: wifi_info ip_address: - name: "IP-adres" + name: "IP address" entity_category: diagnostic - platform: template id: smartdisplay_action @@ -1721,15 +1720,15 @@ text_sensor: 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. + # Living room reports color_temp/rgb/xy, so the colour button does appear here. - platform: homeassistant id: tile9_supported_color_modes entity_id: ${TILE9_ENTITY} attribute: supported_color_modes on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - # Gebruikt door de slider-overlay om te bepalen of de kleurknop nodig is. - # Deze lamp meldt ['brightness'], dus de knop blijft vanzelf verborgen. + # Used by the slider overlay to decide whether the colour button is needed. + # This lamp reports ['brightness'], so the button stays hidden automatically. - platform: homeassistant id: tile7_supported_color_modes entity_id: ${TILE7_ENTITY} @@ -1957,21 +1956,21 @@ touchscreen: for (const auto &point : touches) { // Trace every sample of an edge touch: the log then shows how often the panel delivers. if (cyd::edge_swipe.armed()) - ESP_LOGI("touch", "veeg id=%u st=%u x=%u y=%u", (unsigned) point.id, (unsigned) point.state, point.x, point.y); + ESP_LOGI("touch", "swipe id=%u st=%u x=%u y=%u", (unsigned) point.id, (unsigned) point.state, point.x, point.y); cyd::touch_guard.update(point.x, point.y, point.id); if (point.id != cyd::touch_guard.contact()) continue; - // Swiping in from a side edge flips the page ("Vegen tussen pagina's"); the tap + // Swiping in from a side edge flips the page ("Swiping between pages"); the tap // under the finger is consumed and LVGL waits for the release. LVGL 9.5 sends no // PRESSING to the input device, hence the touchscreen trigger. const int step = cyd::edge_swipe.update(point.x, point.y); if (!step) continue; - const char *blocked = !runtime_tiles::enabled ? "geen runtime-tegels" : !runtime_tiles::swipe_pages ? "instelling uit" - : id(display_dimmed) ? "scherm gedimd" : id(calibration_active) ? "kalibratie" : !id(active_entity).empty() ? "kaart open" - : (runtime_tiles::detail_root && !lv_obj_has_flag(runtime_tiles::detail_root, LV_OBJ_FLAG_HIDDEN)) ? "detailkaart open" : nullptr; - if (blocked) { ESP_LOGI("touch", "randveeg genegeerd: %s", blocked); continue; } + const char *blocked = !runtime_tiles::enabled ? "no runtime tiles" : !runtime_tiles::swipe_pages ? "setting off" + : id(display_dimmed) ? "screen dimmed" : id(calibration_active) ? "calibration" : !id(active_entity).empty() ? "card open" + : (runtime_tiles::detail_root && !lv_obj_has_flag(runtime_tiles::detail_root, LV_OBJ_FLAG_HIDDEN)) ? "detail card open" : nullptr; + if (blocked) { ESP_LOGI("touch", "edge swipe ignored: %s", blocked); continue; } cyd::touch_guard.consume(); for (auto *indev = lv_indev_get_next(nullptr); indev; indev = lv_indev_get_next(indev)) lv_indev_wait_release(indev); - ESP_LOGI("touch", "randveeg: pagina %d -> %d", id(tile_page), id(tile_page) + step); + ESP_LOGI("touch", "edge swipe: page %d -> %d", id(tile_page), id(tile_page) + step); id(tile_page) += step; id(show_tile_page).execute(); } @@ -1980,7 +1979,7 @@ touchscreen: id(last_touch_ms) = millis(); id(touch_down) = false; if (cyd::edge_swipe.armed() && cyd::edge_swipe.inward() > 0) - ESP_LOGI("touch", "randveeg niet gevuurd: %d px naar binnen, %d px verticaal", cyd::edge_swipe.inward(), cyd::edge_swipe.sideways()); + ESP_LOGI("touch", "edge swipe not fired: %d px inward, %d px vertical", cyd::edge_swipe.inward(), cyd::edge_swipe.sideways()); cyd::edge_swipe.end(); @@ -2044,16 +2043,16 @@ lvgl: # 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. + # Direct local feedback on the WHOLE UI, not just the tiles. + # A theme sets the style per widget type, so this covers all + # buttons, tabs, sliders and the thermostat ring in one go. Non-clickable + # widgets never enter the pressed state, so those stay untouched. theme: obj: pressed: - # bg_opa 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 and NOT opa: `opa` at widget level forces LVGL to render the + # element to a separate layer, and that layer costs tens of KB's + # per pressed widget. That ran the memory out here. bg_opa: 45% slider: knob: @@ -2068,9 +2067,9 @@ lvgl: 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. + # Background: two-stop vertical gradient instead of the 320x240 + # RGB565 wallpaper. Saves ~150 KB flash and an image blit per redraw, + # which you can see directly in how smoothly it scrolls. - id: style_bg_grad bg_color: ${BG_TOP_COLOR} bg_grad_color: ${BG_BOTTOM_COLOR} @@ -2209,7 +2208,7 @@ lvgl: y: ${ALERT_TITLE_Y} width: ${ALERT_TEXT_W} long_mode: DOT - text: "Melding" + text: "Notification" text_font: headline text_color: 0x1B1B1B clickable: false @@ -2254,7 +2253,7 @@ lvgl: - label: id: alert_ok_label align: CENTER - text: "Oké" + text: "OK" text_font: headline text_color: 0xFFFFFF clickable: false @@ -2323,8 +2322,8 @@ lvgl: 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. + # Direct local feedback: LVGL applies this as soon as the finger + # touches the glass, in the same frame. Independent of Home Assistant. pressed: bg_opa: 45% widgets: @@ -2439,8 +2438,8 @@ lvgl: 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. + # Long-press mode "vacuum": opens the vacuum controls. + # Falls outside the action/slider chain, so that stays unchanged. - if: condition: lambda: "return std::string(\"${TILE1_LONGPRESS}\") == \"vacuum\";" @@ -2460,8 +2459,8 @@ lvgl: 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. + # Direct local feedback: LVGL applies this as soon as the finger + # touches the glass, in the same frame. Independent of Home Assistant. pressed: bg_opa: 45% widgets: @@ -2576,8 +2575,8 @@ lvgl: 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. + # Long-press mode "vacuum": opens the vacuum controls. + # Falls outside the action/slider chain, so that stays unchanged. - if: condition: lambda: "return std::string(\"${TILE2_LONGPRESS}\") == \"vacuum\";" @@ -2597,8 +2596,8 @@ lvgl: 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. + # Direct local feedback: LVGL applies this as soon as the finger + # touches the glass, in the same frame. Independent of Home Assistant. pressed: bg_opa: 45% widgets: @@ -2713,8 +2712,8 @@ lvgl: 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. + # Long-press mode "vacuum": opens the vacuum controls. + # Falls outside the action/slider chain, so that stays unchanged. - if: condition: lambda: "return std::string(\"${TILE3_LONGPRESS}\") == \"vacuum\";" @@ -2734,8 +2733,8 @@ lvgl: 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. + # Direct local feedback: LVGL applies this as soon as the finger + # touches the glass, in the same frame. Independent of Home Assistant. pressed: bg_opa: 45% widgets: @@ -2850,8 +2849,8 @@ lvgl: 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. + # Long-press mode "vacuum": opens the vacuum controls. + # Falls outside the action/slider chain, so that stays unchanged. - if: condition: lambda: "return std::string(\"${TILE4_LONGPRESS}\") == \"vacuum\";" @@ -2871,8 +2870,8 @@ lvgl: 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. + # Direct local feedback: LVGL applies this as soon as the finger + # touches the glass, in the same frame. Independent of Home Assistant. pressed: bg_opa: 45% widgets: @@ -2987,8 +2986,8 @@ lvgl: 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. + # Long-press mode "vacuum": opens the vacuum controls. + # Falls outside the action/slider chain, so that stays unchanged. - if: condition: lambda: "return std::string(\"${TILE5_LONGPRESS}\") == \"vacuum\";" @@ -3008,8 +3007,8 @@ lvgl: 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. + # Direct local feedback: LVGL applies this as soon as the finger + # touches the glass, in the same frame. Independent of Home Assistant. pressed: bg_opa: 45% widgets: @@ -3124,8 +3123,8 @@ lvgl: 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. + # Long-press mode "vacuum": opens the vacuum controls. + # Falls outside the action/slider chain, so that stays unchanged. - if: condition: lambda: "return std::string(\"${TILE6_LONGPRESS}\") == \"vacuum\";" @@ -3136,10 +3135,10 @@ lvgl: 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. + # Brightness-only lamp: deliberately no percentage/cover/climate + # sensors were created for this tile, those would only cost + # HA subscriptions and heap. The overlay therefore gets + # NAN / "" for the fields a light never uses. - obj: id: tile7 hidden: true @@ -3260,11 +3259,11 @@ lvgl: 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. + # input_select with options Full/Empty. The tile lights up as soon as + # the status is TILE8_ACTIVE_STATE ("Full"); a short tap sends + # input_select.select_option with option: "Empty". + # No slider mode, so this tile only needs ha_state_tile8 — + # no other HA subscription at all. - obj: id: tile8 hidden: true @@ -3337,8 +3336,8 @@ lvgl: lambda: 'return !runtime_tiles::enabled && cyd::touch_guard.accept(millis(), 8);' then: - script.execute: { id: publish_action, action: "tile8_long_press" } - # Slider-modus kan hier niet zonder eerst de bijbehorende - # sensors aan te maken; "action" werkt wel out of the box. + # Slider mode isn't possible here without first creating the + # corresponding sensors; "action" works out of the box. - if: condition: lambda: "return std::string(\"${TILE8_LONGPRESS}\") == \"action\";" @@ -3355,7 +3354,7 @@ lvgl: param_val: "" was_on: false - # ---------- TILE 9 — Woonkamer (verhuisd van tegel 2) ---------- + # ---------- TILE 9 — Living room (moved from tile 2) ---------- - obj: id: tile9 hidden: true @@ -3475,7 +3474,7 @@ lvgl: climate_max_temp: "30.0" climate_step: "0.5" - # ---------- TILE 10 — Alarm aan/uit ---------- + # ---------- TILE 10 — Alarm on/off ---------- - obj: id: tile10 hidden: true @@ -3592,7 +3591,7 @@ lvgl: - label: align: LEFT_MID x: 24 - text: "< Vorige" + text: "< Previous" text_font: label text_color: 0x1B1B1B clickable: false @@ -3634,7 +3633,7 @@ lvgl: - label: align: RIGHT_MID x: -24 - text: "Volgende >" + text: "Next >" text_font: label text_color: 0x1B1B1B clickable: false @@ -3707,12 +3706,12 @@ lvgl: bg_color: 0xFFFFFF 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. + # The only widget with BOTH a radius AND a child (overlay_icon). + # LVGL then clips children to the rounding, and that's only + # possible via a contiguous 96x160 = 46KB ARGB layer. + # If that allocation fails, LVGL keeps trying until the + # task watchdog restarts the board. The rounding stays, the + # clipping goes. clip_corner: false border_width: 0 @@ -3738,12 +3737,12 @@ lvgl: 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. + # The icon used to be a CHILD in the slider. A widget with a radius + # and children has to clip those children to the rounding, and + # LVGL can only do that via an ARGB layer of tens of KB's. Without + # PSRAM that sometimes fails, after which LVGL keeps retrying until + # the watchdog restarts the board. As a sibling on top, no layer + # is needed and it looks identical. - label: id: overlay_icon align: CENTER @@ -4092,8 +4091,8 @@ lvgl: 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. + # 44 instead of 36: slightly bigger tap target, grows away from the arc + # (top-left anchor stays put) instead of towards it. width: 66 height: 66 radius: 999 @@ -4149,7 +4148,7 @@ lvgl: id: climate_current_label align: TOP_MID y: 84 - text: "Nu: --" + text: "Now: --" text_font: sublabel_big text_color: 0x1B1B1B text_opa: 85% @@ -4161,9 +4160,9 @@ lvgl: 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. + # Without this the arc claims an invisible extra + # click area outside the 184x184 box — near the close + # button top-left this creates overlap. ext_click_area: 0 start_angle: 135 end_angle: 45 @@ -4200,7 +4199,7 @@ lvgl: id: climate_set_label align: CENTER y: -32 - text: "Gewenst" + text: "Target" text_font: sublabel_big text_color: 0x1B1B1B text_opa: 68% @@ -4292,10 +4291,10 @@ lvgl: text_color: 0x1B1B1B 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. + # Replaces cycling through modes. Cycling sends a set_hvac_mode on every + # tap, so to get from 'cool' to 'dry' a heat pump actually passes through + # 'heat' and 'fan_only' along the way. Here you pick directly, and exactly + # one command is sent. - obj: id: climate_mode_overlay x: 0 @@ -4313,13 +4312,13 @@ lvgl: - label: align: TOP_MID y: 32 - text: "Modus" + text: "Mode" text_font: sublabel_big text_color: 0x1B1B1B clickable: false - # Flex zorgt dat niet-ondersteunde modi geen gat achterlaten: - # LVGL slaat verborgen kinderen over bij het schikken. + # Flex ensures unsupported modes don't leave a gap: + # LVGL skips hidden children when arranging. - obj: id: climate_mode_list align: TOP_MID @@ -4357,7 +4356,7 @@ lvgl: widgets: - label: align: CENTER - text: "Uit" + text: "Off" text_font: sublabel_big text_color: 0x1B1B1B clickable: false @@ -4378,7 +4377,7 @@ lvgl: widgets: - label: align: CENTER - text: "Verwarmen" + text: "Heat" text_font: sublabel_big text_color: 0x1B1B1B clickable: false @@ -4399,7 +4398,7 @@ lvgl: widgets: - label: align: CENTER - text: "Koelen" + text: "Cool" text_font: sublabel_big text_color: 0x1B1B1B clickable: false @@ -4420,7 +4419,7 @@ lvgl: widgets: - label: align: CENTER - text: "Verw./koelen" + text: "Heat/Cool" text_font: sublabel_big text_color: 0x1B1B1B clickable: false @@ -4462,7 +4461,7 @@ lvgl: widgets: - label: align: CENTER - text: "Drogen" + text: "Dry" text_font: sublabel_big text_color: 0x1B1B1B clickable: false @@ -4483,7 +4482,7 @@ lvgl: widgets: - label: align: CENTER - text: "Ventileren" + text: "Fan only" text_font: sublabel_big text_color: 0x1B1B1B clickable: false @@ -4492,7 +4491,7 @@ lvgl: id: cfan_title width: 100% height: 24 - text: "Ventilator" + text: "Fan" text_font: sublabel text_color: 0x6B6B6B text_align: CENTER @@ -4589,7 +4588,7 @@ lvgl: id: cswing_title width: 100% height: 24 - text: "Zwenken" + text: "Swing" text_font: sublabel text_color: 0x6B6B6B text_align: CENTER @@ -4709,8 +4708,8 @@ lvgl: # ---------- 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. + # A vacuum is not a switch: start, pause, stop and return to + # the dock are separate commands, and suction power is a separate axis. - obj: id: vacuum_overlay x: 0 @@ -5020,7 +5019,7 @@ lvgl: - label: align: TOP_MID y: 110 - text: "GT911 touchcontrole" + text: "GT911 touch check" text_font: headline text_color: 0x1B1B1B - obj: @@ -5258,8 +5257,8 @@ script: - lvgl.widget.hide: vacuum_overlay # ---------- ALERT (HA action show_alert) ---------- - # The screen wakes, keeps its normal brightness and shows the card until Oké or the - # timeout (0: until Oké); the standby interval waits on alert_active. A new alert + # The screen wakes, keeps its normal brightness and shows the card until OK or the + # timeout (0: until OK); the standby interval waits on alert_active. A new alert # replaces the current one; every ending reports itself as esphome.screen_alert. - id: alert_show mode: restart @@ -5361,8 +5360,8 @@ script: - lambda: |- id(last_touch_ms) = millis(); id(display_dimmed) = false; - // Bijna instant omhoog. De ESPHome-standaard is 1s, en dat voelt traag - // als je het scherm aanraakt en op licht staat te wachten. + // Almost instant brightness-up. ESPHome's default is 1s, and that feels sluggish + // when you touch the screen and are waiting on light. id(set_backlight).execute(screen_settings::current.brightness, 80); - lvgl.widget.hide: dim_wake_overlay - script.execute: close_cards @@ -5400,9 +5399,9 @@ script: id(show_tile_page).execute(); } - # Terug naar het basisscherm bij standby. Bewust via de ESPHome - # acties en NIET met lv_obj_add_flag in een lambda: dit script - # draait vanuit een interval, dus buiten LVGL's eigen afhandeling. + # Back to the home screen on standby. Deliberately via ESPHome's + # actions and NOT with lv_obj_add_flag in a lambda: this script + # runs from an interval, so outside LVGL's own handling. - lvgl.widget.hide: brightness_overlay - lvgl.widget.hide: color_detail_overlay - lvgl.widget.hide: climate_detail_overlay @@ -5476,9 +5475,9 @@ script: 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. + # Opens the mode picker. Hides modes the entity doesn't support and marks + # the current one, so you see where you stand before you pick something. + # Opens the vacuum controls and remembers which entity it concerns. - id: open_vacuum_overlay mode: restart parameters: @@ -5490,7 +5489,7 @@ script: 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. + # A standalone command: start, pause, stop or return_to_base. - id: vacuum_action mode: queued parameters: @@ -5505,7 +5504,7 @@ script: data: entity_id: !lambda "return id(active_entity);" - # Zuigkracht is een aparte as, los van start/stop. + # Suction power is a separate axis, apart from start/stop. - id: vacuum_fan_speed mode: queued parameters: @@ -5572,8 +5571,8 @@ script: fill(id(cswing_title), swing_chips, id(active_climate_swing_modes), id(active_climate_swing_mode)); - lvgl.widget.show: climate_mode_overlay - # Zet precies een modus. Geen tussenstappen, dus geen ongewenste - # commando's naar de warmtepomp onderweg. + # Sets exactly one mode. No intermediate steps, so no unwanted + # commands to the heat pump along the way. # Fan and swing modes from the picker: one call with the entity's own mode name. - id: set_climate_fan_mode mode: restart @@ -5690,7 +5689,7 @@ script: light_controls::fallback.kelvin = 3000; light_controls::fallback.hue = 200; light_controls::open("diagnostic", true, true, 75); - lv_label_set_text(id(color_detail_title), "Voorbeeld lamp"); + lv_label_set_text(id(color_detail_title), "Example lamp"); - lvgl.widget.hide: color_close_button - lvgl.widget.hide: color_done_button - lvgl.widget.show: color_detail_overlay @@ -5901,12 +5900,12 @@ script: 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). + // Debounce: same entity within 600ms ignored. Prevents + // touch noise from firing the same HA action multiple times + // (see globals above for why this is needed). uint32_t now = millis(); if (entity_id == id(last_action_entity) && (now - id(last_action_ms)) < 600) { - ESP_LOGD("tile", "do_tile_action: debounced repeat for %s (%ums na vorige)", + ESP_LOGD("tile", "do_tile_action: debounced repeat for %s (%ums after previous)", entity_id.c_str(), now - id(last_action_ms)); return false; } @@ -6161,11 +6160,11 @@ script: 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. + // Accent colour from the Home Assistant domain: the part before the dot + // in the entity_id. script/scene/button fire (ACCENT_ACTION), a + // light is a light (ACCENT_LIGHT), the rest is a device. + // NOTE: this helper appears twice in this file — here and in + // open_value_overlay. Update them together. auto accent_for = [](const std::string& entity_id) -> uint32_t { std::string d = entity_id.substr(0, entity_id.find('.')); if (d == "script" || d == "scene" || d == "button" || @@ -6174,7 +6173,7 @@ script: return ${ACCENT_DEVICE}; }; - // De sliderkleur komt van de tegel, dus die kan ook ${ACCENT_AUTO} zijn. + // The slider colour comes from the tile, so it can also be ${ACCENT_AUTO}. uint32_t fill_color = (slider_fill_color == ${ACCENT_AUTO}) ? accent_for(entity) : slider_fill_color; @@ -6238,11 +6237,11 @@ script: char current_buf[48]; if (isnan(climate_current_temperature)) { - snprintf(current_buf, sizeof(current_buf), "Nu: --"); + snprintf(current_buf, sizeof(current_buf), "Now: --"); } else if (id(active_climate_humidity) >= 0) { - snprintf(current_buf, sizeof(current_buf), "Nu: %.1f° · %d%%", climate_current_temperature, id(active_climate_humidity)); + snprintf(current_buf, sizeof(current_buf), "Now: %.1f° · %d%%", climate_current_temperature, id(active_climate_humidity)); } else { - snprintf(current_buf, sizeof(current_buf), "Nu: %.1f°", climate_current_temperature); + snprintf(current_buf, sizeof(current_buf), "Now: %.1f°", climate_current_temperature); } lv_label_set_text(id(climate_current_label), current_buf); @@ -6485,8 +6484,8 @@ script: 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. + // active_state not empty? Then only that exact HA state counts as "on". + // This way an input_select ("Full"/"Empty") can also light up a tile. auto is_active = [](const std::string& tile_type, const std::string& state, const std::string& active_state = std::string()) { if (!active_state.empty()) return state == active_state; @@ -6505,7 +6504,7 @@ script: 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. + // Tile 7/8 have no cover_state sensor, so no cover branch needed. bool t7_on = is_active("${TILE7_TYPE}", std::string(id(ha_state_tile7).state.c_str()), "${TILE7_ACTIVE_STATE}"); bool t8_on = is_active("${TILE8_TYPE}", std::string(id(ha_state_tile8).state.c_str()), "${TILE8_ACTIVE_STATE}"); bool t9_on = is_active("${TILE9_TYPE}", std::string(id(ha_state_tile9).state.c_str()), "${TILE9_ACTIVE_STATE}"); @@ -6541,7 +6540,7 @@ script: 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). + // Tile 7: light with only brightness. Tile 8: pure text (Full/Empty). set_value_auto(id(t7_value), t7_on, "${TILE7_TYPE}", "${TILE7_VALUE_MODE}", id(tile7_brightness).state, NAN, std::string(), NAN, NAN, NAN, @@ -6552,7 +6551,7 @@ script: NAN, NAN, NAN, "${TILE8_LABEL_ON}", "${TILE8_LABEL_OFF}"); - // Tegel 9: kleurenlamp (brightness). Tegel 10: puur tekst (Aan/Uit). + // Tile 9: colour lamp (brightness). Tile 10: pure text (On/Off). set_value_auto(id(t9_value), t9_on, "${TILE9_TYPE}", "${TILE9_VALUE_MODE}", id(tile9_brightness).state, NAN, std::string(), NAN, NAN, NAN, @@ -6567,33 +6566,33 @@ script: 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"); + if (state == "cleaning") set_lbl(id(t6_value), "Cleaning"); + else if (state == "docked") set_lbl(id(t6_value), "Docked"); + else if (state == "returning") set_lbl(id(t6_value), "Returning to dock"); + else if (state == "paused") set_lbl(id(t6_value), "Paused"); + else if (state == "error") set_lbl(id(t6_value), "Error"); else if (!state.empty()) set_lbl(id(t6_value), state); } if (!id(ha_state_tile1).has_state() || id(ha_state_tile1).state == "unknown" || id(ha_state_tile1).state == "unavailable") - set_lbl(id(t1_value), "Niet beschikbaar"); + set_lbl(id(t1_value), "Unavailable"); if (!id(ha_state_tile2).has_state() || id(ha_state_tile2).state == "unknown" || id(ha_state_tile2).state == "unavailable") - set_lbl(id(t2_value), "Niet beschikbaar"); + set_lbl(id(t2_value), "Unavailable"); if (!id(ha_state_tile3).has_state() || id(ha_state_tile3).state == "unknown" || id(ha_state_tile3).state == "unavailable") - set_lbl(id(t3_value), "Niet beschikbaar"); + set_lbl(id(t3_value), "Unavailable"); if (!id(ha_state_tile4).has_state() || id(ha_state_tile4).state == "unknown" || id(ha_state_tile4).state == "unavailable") - set_lbl(id(t4_value), "Niet beschikbaar"); + set_lbl(id(t4_value), "Unavailable"); if (!id(ha_state_tile5).has_state() || id(ha_state_tile5).state == "unknown" || id(ha_state_tile5).state == "unavailable") - set_lbl(id(t5_value), "Niet beschikbaar"); + set_lbl(id(t5_value), "Unavailable"); if (!id(ha_state_tile6).has_state() || id(ha_state_tile6).state == "unknown" || id(ha_state_tile6).state == "unavailable") - set_lbl(id(t6_value), "Niet beschikbaar"); + set_lbl(id(t6_value), "Unavailable"); if (!id(ha_state_tile7).has_state() || id(ha_state_tile7).state == "unknown" || id(ha_state_tile7).state == "unavailable") - set_lbl(id(t7_value), "Niet beschikbaar"); + set_lbl(id(t7_value), "Unavailable"); if (!id(ha_state_tile8).has_state() || id(ha_state_tile8).state == "unknown" || id(ha_state_tile8).state == "unavailable") - set_lbl(id(t8_value), "Niet beschikbaar"); + set_lbl(id(t8_value), "Unavailable"); if (!id(ha_state_tile9).has_state() || id(ha_state_tile9).state == "unknown" || id(ha_state_tile9).state == "unavailable") - set_lbl(id(t9_value), "Niet beschikbaar"); + set_lbl(id(t9_value), "Unavailable"); if (!id(ha_state_tile10).has_state() || id(ha_state_tile10).state == "unknown" || id(ha_state_tile10).state == "unavailable") - set_lbl(id(t10_value), "Niet beschikbaar"); + set_lbl(id(t10_value), "Unavailable"); if (id(active_control_kind) == "climate_temperature" && !lv_obj_has_flag(id(climate_detail_overlay), LV_OBJ_FLAG_HIDDEN)) { auto label_for_climate_mode = [](std::string mode) { for (char &c : mode) { @@ -6643,11 +6642,11 @@ script: char current_buf[48]; if (isnan(current_temperature)) { - snprintf(current_buf, sizeof(current_buf), "Nu: --"); + snprintf(current_buf, sizeof(current_buf), "Now: --"); } else if (id(active_climate_humidity) >= 0) { - snprintf(current_buf, sizeof(current_buf), "Nu: %.1f° · %d%%", current_temperature, id(active_climate_humidity)); + snprintf(current_buf, sizeof(current_buf), "Now: %.1f° · %d%%", current_temperature, id(active_climate_humidity)); } else { - snprintf(current_buf, sizeof(current_buf), "Nu: %.1f°", current_temperature); + snprintf(current_buf, sizeof(current_buf), "Now: %.1f°", current_temperature); } lv_label_set_text(id(climate_current_label), current_buf); @@ -6667,9 +6666,9 @@ script: { 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). + // Before the first time sync, now() is epoch 0 (1970) — don't show + // a misleading "01:00" then, just leave it blank until a + // real time comes in (after the first Home Assistant time sync). if (now.is_valid()) { char buf[10]; @@ -6685,11 +6684,11 @@ script: } } - // 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. + // Accent colour from the Home Assistant domain: the part before the dot + // in the entity_id. script/scene/button fire (ACCENT_ACTION), a + // light is a light (ACCENT_LIGHT), the rest is a device. + // NOTE: this helper appears twice in this file — here and in + // open_value_overlay. Update them together. auto accent_for = [](const std::string& entity_id) -> uint32_t { std::string d = entity_id.substr(0, entity_id.find('.')); if (d == "script" || d == "scene" || d == "button" || @@ -6730,7 +6729,7 @@ script: break; } } - // ${ACCENT_AUTO} = "bepaal zelf"; een echte hexwaarde wint altijd. + // ${ACCENT_AUTO} = "figure it out yourself"; a real hex value always wins. uint32_t accent = accent_for(entity_id); if (circle_active == ${ACCENT_AUTO}) circle_active = accent; if (icon_disabled == ${ACCENT_AUTO}) icon_disabled = accent; @@ -6826,13 +6825,13 @@ script: text: - platform: smart_display id: dashboard_inbox - name: "Tegelinstellingen" + name: "Tile settings" internal: ${DASHBOARD_INBOX_INTERNAL} number: - platform: template id: setting_brightness - name: "Helderheid normaal" + name: "Normal brightness" entity_category: config min_value: 5 max_value: 100 @@ -6851,7 +6850,7 @@ number: id(apply_screen_settings).execute(); - platform: template id: setting_standby_brightness - name: "Helderheid standby" + name: "Standby brightness" entity_category: config min_value: 0 max_value: 100 @@ -6870,7 +6869,7 @@ number: id(apply_screen_settings).execute(); - platform: template id: setting_night_brightness - name: "Helderheid nacht" + name: "Night brightness" entity_category: config min_value: 0 max_value: 100 @@ -6889,7 +6888,7 @@ number: id(apply_screen_settings).execute(); - platform: template id: setting_standby_seconds - name: "Standby na" + name: "Standby after" entity_category: config min_value: 60 max_value: 86400 diff --git a/guition-device.example.yaml b/guition-device.example.yaml index 30c170e..36297bc 100644 --- a/guition-device.example.yaml +++ b/guition-device.example.yaml @@ -1,20 +1,20 @@ -# Kopieer via: python tools/new_device.py --board guition --name display-keuken -# Bewerk alleen dit lokale profiel voor jouw scherm. +# Copy via: python tools/new_device.py --board guition --name display-kitchen +# Only edit this local profile for your screen. packages: display: !include guition-4848s040.yaml substitutions: DEVICE_NAME: "guition-new" - DEVICE_FRIENDLY_NAME: "Mijn Guition" - ROOM_NAME: "Woonkamer" + DEVICE_FRIENDLY_NAME: "My Guition" + ROOM_NAME: "Living room" TILE_COUNT: "6" - DIRECT_ACTIONS: "false" # Pas na koppelen/testen op true zetten. + DIRECT_ACTIONS: "false" # Set to true only after pairing/testing. AUTO_DIM_TIMEOUT: "600" CALIBRATION_ON_BOOT: "false" - # Tegel 1 - TILE1_ENTITY: "light.voorbeeld_lamp" - TILE1_STATE_ENTITY: "light.voorbeeld_lamp" + # Tile 1 + TILE1_ENTITY: "light.example_lamp" + TILE1_STATE_ENTITY: "light.example_lamp" TILE1_TITLE: "Lamp" TILE1_TYPE: "light" TILE1_TAP_ACTION: "auto" @@ -28,14 +28,14 @@ substitutions: TILE1_LONGPRESS_ACTION_TYPE: "" TILE1_LONGPRESS_ACTION_SERVICE: "" TILE1_VALUE_MODE: "auto" - TILE1_LABEL_ON: "Aan" - TILE1_LABEL_OFF: "Uit" + TILE1_LABEL_ON: "On" + TILE1_LABEL_OFF: "Off" TILE1_ACTIVE_STATE: "" - # Tegel 2 - TILE2_ENTITY: "switch.voorbeeld_stekker" - TILE2_STATE_ENTITY: "switch.voorbeeld_stekker" - TILE2_TITLE: "Stekker" + # Tile 2 + TILE2_ENTITY: "switch.example_plug" + TILE2_STATE_ENTITY: "switch.example_plug" + TILE2_TITLE: "Plug" TILE2_TYPE: "switch" TILE2_TAP_ACTION: "auto" TILE2_TAP_SERVICE: "" @@ -48,14 +48,14 @@ substitutions: TILE2_LONGPRESS_ACTION_TYPE: "" TILE2_LONGPRESS_ACTION_SERVICE: "" TILE2_VALUE_MODE: "auto" - TILE2_LABEL_ON: "Aan" - TILE2_LABEL_OFF: "Uit" + TILE2_LABEL_ON: "On" + TILE2_LABEL_OFF: "Off" TILE2_ACTIVE_STATE: "" - # Tegel 3 - TILE3_ENTITY: "scene.voorbeeld_avond" - TILE3_STATE_ENTITY: "scene.voorbeeld_avond" - TILE3_TITLE: "Avond" + # Tile 3 + TILE3_ENTITY: "scene.example_evening" + TILE3_STATE_ENTITY: "scene.example_evening" + TILE3_TITLE: "Evening" TILE3_TYPE: "scene" TILE3_TAP_ACTION: "auto" TILE3_TAP_SERVICE: "" @@ -68,14 +68,14 @@ substitutions: TILE3_LONGPRESS_ACTION_TYPE: "" TILE3_LONGPRESS_ACTION_SERVICE: "" TILE3_VALUE_MODE: "auto" - TILE3_LABEL_ON: "Aan" - TILE3_LABEL_OFF: "Uit" + TILE3_LABEL_ON: "On" + TILE3_LABEL_OFF: "Off" TILE3_ACTIVE_STATE: "" - # Tegel 4 - TILE4_ENTITY: "script.voorbeeld_uit" - TILE4_STATE_ENTITY: "script.voorbeeld_uit" - TILE4_TITLE: "Alles uit" + # Tile 4 + TILE4_ENTITY: "script.example_all_off" + TILE4_STATE_ENTITY: "script.example_all_off" + TILE4_TITLE: "All off" TILE4_TYPE: "script" TILE4_TAP_ACTION: "auto" TILE4_TAP_SERVICE: "" @@ -88,14 +88,14 @@ substitutions: TILE4_LONGPRESS_ACTION_TYPE: "" TILE4_LONGPRESS_ACTION_SERVICE: "" TILE4_VALUE_MODE: "auto" - TILE4_LABEL_ON: "Aan" - TILE4_LABEL_OFF: "Uit" + TILE4_LABEL_ON: "On" + TILE4_LABEL_OFF: "Off" TILE4_ACTIVE_STATE: "" - # Tegel 5 - TILE5_ENTITY: "climate.voorbeeld_airco" - TILE5_STATE_ENTITY: "climate.voorbeeld_airco" - TILE5_TITLE: "Airco" + # Tile 5 + TILE5_ENTITY: "climate.example_ac" + TILE5_STATE_ENTITY: "climate.example_ac" + TILE5_TITLE: "AC" TILE5_TYPE: "climate" TILE5_TAP_ACTION: "toggle" TILE5_TAP_SERVICE: "climate.toggle" @@ -108,14 +108,14 @@ substitutions: TILE5_LONGPRESS_ACTION_TYPE: "" TILE5_LONGPRESS_ACTION_SERVICE: "" TILE5_VALUE_MODE: "auto" - TILE5_LABEL_ON: "Aan" - TILE5_LABEL_OFF: "Uit" + TILE5_LABEL_ON: "On" + TILE5_LABEL_OFF: "Off" TILE5_ACTIVE_STATE: "" - # Tegel 6 - TILE6_ENTITY: "vacuum.voorbeeld_robot" - TILE6_STATE_ENTITY: "vacuum.voorbeeld_robot" - TILE6_TITLE: "Stofzuiger" + # Tile 6 + TILE6_ENTITY: "vacuum.example_robot" + TILE6_STATE_ENTITY: "vacuum.example_robot" + TILE6_TITLE: "Vacuum" TILE6_TYPE: "vacuum" TILE6_TAP_ACTION: "custom" TILE6_TAP_SERVICE: "vacuum.start" @@ -128,14 +128,14 @@ substitutions: TILE6_LONGPRESS_ACTION_TYPE: "" TILE6_LONGPRESS_ACTION_SERVICE: "" TILE6_VALUE_MODE: "auto" - TILE6_LABEL_ON: "Aan" - TILE6_LABEL_OFF: "Uit" + TILE6_LABEL_ON: "On" + TILE6_LABEL_OFF: "Off" 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" + # Tile 7 (hidden while TILE_COUNT is lower) + TILE7_ENTITY: "light.unused_7" + TILE7_STATE_ENTITY: "light.unused_7" + TILE7_TITLE: "Tile 7" TILE7_TYPE: "light" TILE7_TAP_ACTION: "auto" TILE7_TAP_SERVICE: "" @@ -148,14 +148,14 @@ substitutions: TILE7_LONGPRESS_ACTION_TYPE: "" TILE7_LONGPRESS_ACTION_SERVICE: "" TILE7_VALUE_MODE: "auto" - TILE7_LABEL_ON: "Aan" - TILE7_LABEL_OFF: "Uit" + TILE7_LABEL_ON: "On" + TILE7_LABEL_OFF: "Off" 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" + # Tile 8 (hidden while TILE_COUNT is lower) + TILE8_ENTITY: "light.unused_8" + TILE8_STATE_ENTITY: "light.unused_8" + TILE8_TITLE: "Tile 8" TILE8_TYPE: "light" TILE8_TAP_ACTION: "auto" TILE8_TAP_SERVICE: "" @@ -168,14 +168,14 @@ substitutions: TILE8_LONGPRESS_ACTION_TYPE: "" TILE8_LONGPRESS_ACTION_SERVICE: "" TILE8_VALUE_MODE: "auto" - TILE8_LABEL_ON: "Aan" - TILE8_LABEL_OFF: "Uit" + TILE8_LABEL_ON: "On" + TILE8_LABEL_OFF: "Off" 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" + # Tile 9 (hidden while TILE_COUNT is lower) + TILE9_ENTITY: "light.unused_9" + TILE9_STATE_ENTITY: "light.unused_9" + TILE9_TITLE: "Tile 9" TILE9_TYPE: "light" TILE9_TAP_ACTION: "auto" TILE9_TAP_SERVICE: "" @@ -188,14 +188,14 @@ substitutions: TILE9_LONGPRESS_ACTION_TYPE: "" TILE9_LONGPRESS_ACTION_SERVICE: "" TILE9_VALUE_MODE: "auto" - TILE9_LABEL_ON: "Aan" - TILE9_LABEL_OFF: "Uit" + TILE9_LABEL_ON: "On" + TILE9_LABEL_OFF: "Off" 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" + # Tile 10 (hidden while TILE_COUNT is lower) + TILE10_ENTITY: "light.unused_10" + TILE10_STATE_ENTITY: "light.unused_10" + TILE10_TITLE: "Tile 10" TILE10_TYPE: "light" TILE10_TAP_ACTION: "auto" TILE10_TAP_SERVICE: "" @@ -208,6 +208,6 @@ substitutions: TILE10_LONGPRESS_ACTION_TYPE: "" TILE10_LONGPRESS_ACTION_SERVICE: "" TILE10_VALUE_MODE: "auto" - TILE10_LABEL_ON: "Aan" - TILE10_LABEL_OFF: "Uit" + TILE10_LABEL_ON: "On" + TILE10_LABEL_OFF: "Off" TILE10_ACTIVE_STATE: "" diff --git a/home-like-2432s028.yaml b/home-like-2432s028.yaml index fe13620..0010e28 100644 --- a/home-like-2432s028.yaml +++ b/home-like-2432s028.yaml @@ -43,7 +43,6 @@ # light -> brightness, fan -> percentage/preset, scene/script -> text # # Device configuration: home-like-2432s028.yaml (ESP32-2432S028). -# home-like.yaml targets the separate ST7789V/GT911 board. # Credentials stay in secrets.yaml. Fonts are bundled for offline validation. # Start at README.md; docs/ covers calibration, HA setup and acceptance tests. # @@ -58,7 +57,7 @@ ############################################################ substitutions: - SCREEN_FIRMWARE_VERSION: "0.2.33" + SCREEN_FIRMWARE_VERSION: "0.2.34" # A tap survives a finger that drifts up to this far (320 px over 58 mm: 1 cm); further is a swipe. TOUCH_MOVE_LIMIT_PX: "56" # Shortest contact that counts as a tap; a resistive panel bounces, a capacitive one does not. @@ -66,7 +65,7 @@ substitutions: DYNAMIC_TILES: "false" DASHBOARD_INBOX_INTERNAL: "true" DEVICE_NAME: "cyd-new" - DEVICE_FRIENDLY_NAME: "Mijn CYD" + DEVICE_FRIENDLY_NAME: "My CYD" CALIBRATION_ON_BOOT: "false" TOUCH_AFFINE_XX: "1.000000000" TOUCH_AFFINE_XY: "0.000000000" @@ -78,7 +77,7 @@ substitutions: 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 + ROOM_NAME: "Living room" # room name shown in the top-left header TIME_24H: "true" # true = 24h format, false = 12h format ## DIM settings @@ -92,24 +91,23 @@ substitutions: NIGHT_DIM_BRIGHTNESS: "10" # backlight brightness in percent at night # ============================================================ - # ACCENTKLEUREN — automatisch bepaald uit het Home Assistant-domein + # ACCENT COLOURS — determined automatically from the Home Assistant domain # - # Het domein is het stuk voor de punt in het entity_id: - # script.goede_morgen -> "script" -> ACCENT_ACTION - # light.woonkamer_2 -> "light" -> ACCENT_LIGHT + # The domain is the part before the dot in the entity_id: + # script.good_morning -> "script" -> ACCENT_ACTION + # light.living_room_2 -> "light" -> ACCENT_LIGHT # vacuum.s8 -> "vacuum" -> ACCENT_DEVICE # - # 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. + # That's Home Assistant's own grouping, so a tile automatically gets the + # right colour as soon as you put an entity in it — you no longer have + # to pick colours per tile. The distinction that matters: a script/scene + # fires, a device you turn on or off. Those two shouldn't look the same. # - # 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). + # Set TILE*_CIRCLE_ACTIVE_COLOR / TILE*_ICON_DISABLED_COLOR to + # ${ACCENT_AUTO} to use this, or to a real hex value to colour that + # one tile by hand (see tile 8 — red as soon as there's mail). # ============================================================ - ACCENT_AUTO: "0x1000000" # sentinel: bewust buiten het 24-bits kleurbereik + ACCENT_AUTO: "0x1000000" # sentinel: deliberately outside the 24-bit colour range ACCENT_ACTION: "0x8B5CF6" # script / scene / button / input_button / automation ACCENT_LIGHT: "0xFEC600" # light ACCENT_DEVICE: "0x00C5EC" # switch, fan, cover, climate, vacuum, media_player, rest @@ -125,16 +123,16 @@ substitutions: # ============================================================ # --- 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_ENTITY: "light.example_lamp" # main Home Assistant entity for this tile + TILE1_STATE_ENTITY: "light.example_lamp" # entity used for active/inactive state styling TILE1_TITLE: "Lamp" # tile title shown in the UI TILE1_ICON: "\U000F001B" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) TILE1_TYPE: "light" # tile type: light | fan | switch | scene | script | cover | climate TILE1_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset | custom TILE1_LONGPRESS: "slider" # long press mode: none | slider | action TILE1_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text - TILE1_LABEL_OFF: "Uit" # text shown when entity is off - TILE1_LABEL_ON: "Aan" # text shown for text-mode tiles when entity is on + TILE1_LABEL_OFF: "Off" # text shown when entity is off + TILE1_LABEL_ON: "On" # text shown for text-mode tiles when entity is on TILE1_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") # --- colors --- @@ -174,16 +172,16 @@ substitutions: # ============================================================ # --- 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_ENTITY: "switch.example_plug" # main Home Assistant entity for this tile + TILE2_STATE_ENTITY: "switch.example_plug" # entity used for active/inactive state styling + TILE2_TITLE: "Plug" # tile title shown in the UI TILE2_ICON: "\U000F0387" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) TILE2_TYPE: "switch" # tile type: light | fan | switch | scene | script | cover | climate TILE2_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset | custom TILE2_LONGPRESS: "none" # long press mode: none | slider | action TILE2_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text - TILE2_LABEL_OFF: "Uit" # text shown when entity is off - TILE2_LABEL_ON: "Aan" # text shown for text-mode tiles when entity is on + TILE2_LABEL_OFF: "Off" # text shown when entity is off + TILE2_LABEL_ON: "On" # text shown for text-mode tiles when entity is on TILE2_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") # --- colors --- @@ -223,16 +221,16 @@ substitutions: # ============================================================ # --- 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_ENTITY: "scene.example_evening" # main Home Assistant entity for this tile + TILE3_STATE_ENTITY: "scene.example_evening" # entity used for active/inactive state styling + TILE3_TITLE: "Evening" # tile title shown in the UI TILE3_ICON: "\U000F02E3" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) TILE3_TYPE: "scene" # tile type: light | fan | switch | scene | script | cover | climate TILE3_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset | custom TILE3_LONGPRESS: "none" # long press mode: none | slider | action TILE3_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text - TILE3_LABEL_OFF: "Uit" # text shown when entity is off - TILE3_LABEL_ON: "Aan" # text shown for text-mode tiles when entity is on + TILE3_LABEL_OFF: "Off" # text shown when entity is off + TILE3_LABEL_ON: "On" # text shown for text-mode tiles when entity is on TILE3_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") # --- colors --- @@ -272,16 +270,16 @@ substitutions: # ============================================================ # --- 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_ENTITY: "script.example_off" # main Home Assistant entity for this tile + TILE4_STATE_ENTITY: "script.example_off" # entity used for active/inactive state styling + TILE4_TITLE: "All off" # tile title shown in the UI TILE4_ICON: "\U000F059C" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) TILE4_TYPE: "script" # tile type: light | fan | switch | scene | script | cover | climate TILE4_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset | custom TILE4_LONGPRESS: "none" # long press mode: none | slider | action TILE4_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text - TILE4_LABEL_OFF: "Uit" # text shown when entity is off - TILE4_LABEL_ON: "Aan" # text shown for text-mode tiles when entity is on + TILE4_LABEL_OFF: "Off" # text shown when entity is off + TILE4_LABEL_ON: "On" # text shown for text-mode tiles when entity is on TILE4_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") # --- colors --- @@ -321,16 +319,16 @@ substitutions: # ============================================================ # --- 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_ENTITY: "climate.example_ac" # main Home Assistant entity for this tile + TILE5_STATE_ENTITY: "climate.example_ac" # entity used for active/inactive state styling + TILE5_TITLE: "AC" # tile title shown in the UI TILE5_ICON: "\U000F0425" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) TILE5_TYPE: "climate" # tile type: light | fan | switch | scene | script | cover | climate TILE5_TAP_ACTION: "toggle" # tap action: auto | toggle | activate | fan_toggle_preset | custom TILE5_LONGPRESS: "slider" # long press mode: none | slider | action TILE5_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text - TILE5_LABEL_OFF: "Uit" # text shown when entity is off (closed) - TILE5_LABEL_ON: "Aan" # text shown when entity is on (open) + TILE5_LABEL_OFF: "Off" # text shown when entity is off (closed) + TILE5_LABEL_ON: "On" # text shown when entity is on (open) TILE5_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") # --- colors --- @@ -370,16 +368,16 @@ substitutions: # ============================================================ # --- 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_ENTITY: "vacuum.example_robot" # main Home Assistant entity for this tile + TILE6_STATE_ENTITY: "vacuum.example_robot" # entity used for active/inactive state styling + TILE6_TITLE: "Vacuum" # tile title shown in the UI TILE6_ICON: "\U000F070D" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) TILE6_TYPE: "vacuum" # tile type: light | fan | switch | scene | script | cover | climate TILE6_TAP_ACTION: "custom" # tap action: auto | toggle | activate | fan_toggle_preset | custom TILE6_LONGPRESS: "vacuum" # long press mode: none | slider | action TILE6_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text - TILE6_LABEL_OFF: "Uit" # text shown when entity is off - TILE6_LABEL_ON: "Aan" # text shown for text-mode tiles when entity is on + TILE6_LABEL_OFF: "Off" # text shown when entity is off + TILE6_LABEL_ON: "On" # text shown for text-mode tiles when entity is on TILE6_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") # --- colors --- @@ -413,22 +411,22 @@ substitutions: 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. + # TILE 7 — Table lamp + # Brightness-only lamp (HA reports supported_color_modes: ['brightness']), + # so the colour button in the slider overlay stays hidden automatically. # ============================================================ # --- basic settings --- - TILE7_ENTITY: "light.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_ENTITY: "light.unused_7" # main Home Assistant entity for this tile + TILE7_STATE_ENTITY: "light.unused_7" # entity used for active/inactive state styling + TILE7_TITLE: "Tile 7" # tile title shown in the UI TILE7_ICON: "\U000F06B5" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) TILE7_TYPE: "light" # tile type: light | fan | switch | scene | script | cover | climate TILE7_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset TILE7_LONGPRESS: "none" # long press mode: none | slider | action | vacuum TILE7_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text - TILE7_LABEL_OFF: "Uit" # text shown when entity is off - TILE7_LABEL_ON: "Aan" # text shown for text-mode tiles when entity is on + TILE7_LABEL_OFF: "Off" # text shown when entity is off + TILE7_LABEL_ON: "On" # text shown for text-mode tiles when entity is on TILE7_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") # --- colors --- @@ -448,7 +446,7 @@ substitutions: 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_SLIDER: "auto" # auto = brightness for a light TILE7_LONGPRESS_OFF_VALUE: "0" # initial slider value when entity is off # if LONGPRESS = action: TILE7_LONGPRESS_ACTION: "" # entity_id to fire on long press @@ -456,27 +454,27 @@ substitutions: 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. + # TILE 8 — Mailbox reset + # input_select.mailbox_status has options ['Full', 'Empty']. + # The tile lights up as soon as the status is "Full" (ACTIVE_STATE), and a + # short tap sets it back to "Empty" via input_select.select_option. # ============================================================ # --- basic settings --- - TILE8_ENTITY: "light.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_ENTITY: "light.unused_8" # main Home Assistant entity for this tile + TILE8_STATE_ENTITY: "light.unused_8" # entity used for active/inactive state styling + TILE8_TITLE: "Tile 8" # tile title shown in the UI TILE8_ICON: "\U000F06EE" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) - TILE8_TYPE: "light" # 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 + TILE8_TYPE: "light" # generic type; the real behavior comes from TAP_SERVICE below + TILE8_TAP_ACTION: "auto" # activate + TAP_SERVICE = a standalone service call + TILE8_LONGPRESS: "none" # long press only sends the event to HA + TILE8_VALUE_MODE: "auto" # shows LABEL_ON / LABEL_OFF + TILE8_LABEL_OFF: "Off" # text shown when entity is off / inactive + TILE8_LABEL_ON: "On" # text shown when entity is active + TILE8_ACTIVE_STATE: "" # tile is "on" as soon as the status is exactly "Full" # --- colors --- - TILE8_CIRCLE_ACTIVE_COLOR: "0xE5484D" # rood zodra er post ligt + TILE8_CIRCLE_ACTIVE_COLOR: "0xE5484D" # red as soon as there's mail TILE8_CIRCLE_DISABLED_COLOR: "0x7B7B6F" # icon circle color when inactive TILE8_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active TILE8_ICON_DISABLED_COLOR: "0xE5484D" # icon color when inactive @@ -488,35 +486,35 @@ substitutions: TILE8_VALUE_DISABLED_COLOR: "0xD9D9D9" # value color when inactive # --- advanced settings --- - TILE8_TAP_SERVICE: "" # zet de input_select op een vaste optie - TILE8_TAP_PARAM_KEY: "" # parameternaam voor input_select.select_option - TILE8_TAP_PARAM_VAL: "" # de "reset"-waarde + TILE8_TAP_SERVICE: "" # sets the input_select to a fixed option + TILE8_TAP_PARAM_KEY: "" # parameter name for input_select.select_option + TILE8_TAP_PARAM_VAL: "" # the "reset" value # if LONGPRESS = slider: - TILE8_LONGPRESS_SLIDER: "auto" # niet in gebruik (LONGPRESS = none) - TILE8_LONGPRESS_OFF_VALUE: "0" # niet in gebruik + TILE8_LONGPRESS_SLIDER: "auto" # not in use (LONGPRESS = none) + TILE8_LONGPRESS_OFF_VALUE: "0" # not in use # if LONGPRESS = action: TILE8_LONGPRESS_ACTION: "" # entity_id to fire on long press TILE8_LONGPRESS_ACTION_TYPE: "" # entity type (empty = same as TILE*_TYPE) TILE8_LONGPRESS_ACTION_SERVICE: "" # service to call (default: activate/turn_on) # ============================================================ - # TILE 9 — 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. + # TILE 9 — Living room + # Moved from tile 2 to row 5 to make room for Ambient. + # Colour lamp (color_temp / rgb / xy), so the colour button in the + # slider overlay appears automatically. # ============================================================ # --- basic settings --- - TILE9_ENTITY: "light.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_ENTITY: "light.unused_9" # main Home Assistant entity for this tile + TILE9_STATE_ENTITY: "light.unused_9" # entity used for active/inactive state styling + TILE9_TITLE: "Tile 9" # tile title shown in the UI TILE9_ICON: "\U000F04B9" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) TILE9_TYPE: "light" # tile type: light | fan | switch | scene | script | cover | climate TILE9_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset TILE9_LONGPRESS: "none" # long press mode: none | slider | action | vacuum TILE9_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text - TILE9_LABEL_OFF: "Uit" # text shown when entity is off - TILE9_LABEL_ON: "Aan" # text shown for text-mode tiles when entity is on + TILE9_LABEL_OFF: "Off" # text shown when entity is off + TILE9_LABEL_ON: "On" # text shown for text-mode tiles when entity is on TILE9_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") # --- colors --- @@ -536,7 +534,7 @@ substitutions: 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_SLIDER: "auto" # auto = brightness for a light TILE9_LONGPRESS_OFF_VALUE: "0" # initial slider value when entity is off # if LONGPRESS = action: TILE9_LONGPRESS_ACTION: "" # entity_id to fire on long press @@ -544,26 +542,26 @@ substitutions: 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. + # TILE 10 — Alarm on/off + # input_boolean.alarm_engaged. An input_boolean doesn't respond to + # switch.toggle, hence the explicit TAP_SERVICE. # ============================================================ # --- basic settings --- - TILE10_ENTITY: "light.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_ENTITY: "light.unused_10" # main Home Assistant entity for this tile + TILE10_STATE_ENTITY: "light.unused_10" # entity used for active/inactive state styling + TILE10_TITLE: "Tile 10" # tile title shown in the UI TILE10_ICON: "\U000F002A" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) - TILE10_TYPE: "light" # generiek aan/uit-type; de service hieronder doet het echte werk + TILE10_TYPE: "light" # generic on/off type; the service below does the real work TILE10_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset - TILE10_LONGPRESS: "none" # long press 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 + TILE10_LONGPRESS: "none" # long press only sends the event to HA + TILE10_VALUE_MODE: "auto" # shows LABEL_ON / LABEL_OFF + TILE10_LABEL_OFF: "Off" # text shown when entity is off + TILE10_LABEL_ON: "On" # text shown when entity is on + TILE10_ACTIVE_STATE: "" # input_boolean just uses on/off # --- colors --- - TILE10_CIRCLE_ACTIVE_COLOR: "${ACCENT_AUTO}" # input_boolean -> ACCENT_DEVICE (cyaan) + TILE10_CIRCLE_ACTIVE_COLOR: "${ACCENT_AUTO}" # input_boolean -> ACCENT_DEVICE (cyan) TILE10_CIRCLE_DISABLED_COLOR: "0x7B7B6F" # icon circle color when inactive TILE10_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active TILE10_ICON_DISABLED_COLOR: "${ACCENT_AUTO}" # icon color when inactive @@ -575,12 +573,12 @@ substitutions: TILE10_VALUE_DISABLED_COLOR: "0xD9D9D9" # value color when inactive # --- advanced settings --- - TILE10_TAP_SERVICE: "" # switch.toggle werkt niet op een input_boolean + TILE10_TAP_SERVICE: "" # switch.toggle doesn't work on an input_boolean TILE10_TAP_PARAM_KEY: "" # optional extra service parameter key TILE10_TAP_PARAM_VAL: "" # optional extra service parameter value # if LONGPRESS = slider: - TILE10_LONGPRESS_SLIDER: "auto" # niet in gebruik (LONGPRESS = none) - TILE10_LONGPRESS_OFF_VALUE: "0" # niet in gebruik + TILE10_LONGPRESS_SLIDER: "auto" # not in use (LONGPRESS = none) + TILE10_LONGPRESS_OFF_VALUE: "0" # not in use # if LONGPRESS = action: TILE10_LONGPRESS_ACTION: "" # entity_id to fire on long press TILE10_LONGPRESS_ACTION_TYPE: "" # entity type (empty = same as TILE*_TYPE) @@ -609,8 +607,8 @@ substitutions: DISPLAY_H: "240" LVGL_ROTATION: "90" TOUCH_SWAP_XY: "false" - # true op dit bord (resistieve XPT2046) — was false op het 032c-bord - # (capacitieve GT911). Dit is de originele upstream-waarde voor dit paneel. + # true on this board (resistive XPT2046) — was false on the 032c board + # (capacitive GT911). This is the original upstream value for this panel. TOUCH_MIRROR_X: "true" TOUCH_MIRROR_Y: "false" # Measured on this board: median top-right raw=(523,3390), @@ -650,9 +648,9 @@ substitutions: ALERT_BUTTON_H: "40" ALERT_BUTTON_INSET: "14" ALERT_TITLE_MAX: "48" # bytes; the label ends one line with an ellipsis - ALERT_BUTTON_MAX: "12" # bytes on the Oké button + ALERT_BUTTON_MAX: "12" # bytes on the OK button ALERT_SUBTITLE_MAX: "160" # bytes; wraps over the rows the card has - BG_TOP_COLOR: "0xE7E7E7" # lichte achtergrond, gelijk aan de Guition + BG_TOP_COLOR: "0xE7E7E7" # light background, matching the Guition BG_BOTTOM_COLOR: "0xE7E7E7" TILE_ICON_X: "0" TILE_ICON_Y: "0" @@ -688,8 +686,8 @@ substitutions: # OVERLAY_SLIDER_W: "96" # OVERLAY_SLIDER_H: "200" # OVERLAY_ICON_Y: "94" - # BG_TOP_COLOR: "0xAEC4D9" # achtergrond boven (zacht blauw) - # BG_BOTTOM_COLOR: "0xD6BB9E" # achtergrond onder (warm zand) + # BG_TOP_COLOR: "0xAEC4D9" # background top (soft blue) + # BG_BOTTOM_COLOR: "0xD6BB9E" # background bottom (warm sand) # TILE_ICON_X: "0" # TILE_ICON_Y: "0" # TILE_TITLE_X: "4" @@ -724,8 +722,8 @@ substitutions: # OVERLAY_SLIDER_W: "96" # OVERLAY_SLIDER_H: "160" # OVERLAY_ICON_Y: "74" - # BG_TOP_COLOR: "0xAEC4D9" # achtergrond boven (zacht blauw) - # BG_BOTTOM_COLOR: "0xD6BB9E" # achtergrond onder (warm zand) + # BG_TOP_COLOR: "0xAEC4D9" # background top (soft blue) + # BG_BOTTOM_COLOR: "0xD6BB9E" # background bottom (warm sand) # TILE_ICON_X: "0" # TILE_ICON_Y: "0" # TILE_TITLE_X: "48" @@ -760,8 +758,8 @@ substitutions: # OVERLAY_SLIDER_W: "96" # OVERLAY_SLIDER_H: "200" # OVERLAY_ICON_Y: "94" - # BG_TOP_COLOR: "0xAEC4D9" # achtergrond boven (zacht blauw) - # BG_BOTTOM_COLOR: "0xD6BB9E" # achtergrond onder (warm zand) + # BG_TOP_COLOR: "0xAEC4D9" # background top (soft blue) + # BG_BOTTOM_COLOR: "0xD6BB9E" # background bottom (warm sand) # TILE_ICON_X: "0" # TILE_ICON_Y: "0" # TILE_TITLE_X: "4" @@ -889,7 +887,7 @@ esphome: id(active_climate_swing_modes) = t.swing_modes; id(active_climate_swing_mode) = t.swing_mode; id(open_value_overlay).execute(t.name, t.entity, t.domain(), "auto", t.active(), 0, t.brightness, t.percentage, t.position, runtime_tiles::icon_for(t), - 0xD5EEFC, 0x00A6ED, 0x1B1B1B, "Uit", "Aan", t.modes, + 0xD5EEFC, 0x00A6ED, 0x1B1B1B, "Off", "On", t.modes, t.current, t.target, t.humidity, t.state, t.hvac_modes, std::to_string(t.minimum), std::to_string(t.maximum), std::to_string(t.step)); }; @@ -905,8 +903,8 @@ esphome: id(active_climate_fan_modes) = t.fan_modes; id(active_climate_fan_mode) = t.fan_mode; id(active_climate_swing_modes) = t.swing_modes; id(active_climate_swing_mode) = t.swing_mode; char buf[72]; - if (std::isfinite(t.current)) snprintf(buf, sizeof(buf), "Nu: %.1f°", t.current); - else snprintf(buf, sizeof(buf), "Nu: --"); + if (std::isfinite(t.current)) snprintf(buf, sizeof(buf), "Now: %.1f°", t.current); + else snprintf(buf, sizeof(buf), "Now: --"); lv_label_set_text(id(climate_current_label), buf); if (std::isfinite(t.target)) { int target = cyd::quantize_temperature(lround(t.target * 10), id(active_climate_min_x10), id(active_climate_max_x10), id(active_climate_step_x10)); @@ -954,19 +952,19 @@ esp32: framework: type: esp-idf advanced: - # ESPHome adviseert dit zelf in de boot-log. Levert ~40KB extra IRAM - # en een kleinere binary op, en op dit bord telt elke KB. + # ESPHome recommends this itself in the boot log. Yields ~40KB extra IRAM + # and a smaller binary, and every KB counts on this board. sram1_as_iram: true - # minimum_chip_revision bewust weggelaten: dat stond hier voor het - # 032c-bord (rev 3.1 bevestigd), maar dit is een ander fysiek bordje - # met een onbekende chip-revisie. Met een onterecht hoge minimum - # weigert de firmware simpelweg te booten. + # minimum_chip_revision deliberately left out: that was here for the + # 032c board (rev 3.1 confirmed), but this is a different physical board + # with an unknown chip revision. With an unjustifiably high minimum + # the firmware simply refuses to boot. version: recommended variant: esp32 logger: - # Voor diagnose tijdelijk op DEBUG of VERBOSE zetten. Let op: ESPHome logt - # zijn configuratieregels op niveau CONFIG, die zijn op INFO onzichtbaar. + # Set to DEBUG or VERBOSE temporarily for diagnosis. Note: ESPHome logs + # its configuration lines at CONFIG level, which are invisible at INFO. level: DEBUG logs: sensor: WARN @@ -1048,7 +1046,7 @@ api: then: - script.execute: ui_self_test # Home Assistant automations: esphome._show_alert puts a card over the whole - # screen (README, "Alert vanuit een automatisering"); dismiss_alert removes it again. + # screen (README, "Alert from an automation"); dismiss_alert removes it again. - action: show_alert variables: title: string @@ -1182,10 +1180,10 @@ globals: restore_value: no initial_value: '0' - # Debounce voor do_tile_action: dit resistieve XPT2046-paneel geeft soms - # meerdere losse press/release-cycli voor één fysieke aanraking (ruis in - # het weerstandslaagje), waardoor dezelfde HA-actie meerdere keren binnen - # een paar honderd ms vuurt (gezien: script.ambient_morning 6x in 8s). + # Debounce for do_tile_action: this resistive XPT2046 panel sometimes gives + # multiple separate press/release cycles for one physical touch (noise in + # the resistive layer), causing the same HA action to fire multiple times + # within a few hundred ms (observed: script.ambient_morning 6x in 8s). - id: last_action_entity type: std::string restore_value: no @@ -1553,8 +1551,8 @@ font: size: 12 glyphs: *tile_icons -# Geheugen zichtbaar maken: dit bord heeft geen PSRAM, dus de vrije heap -# bepaalt hoeveel UI erbij kan. Verschijnt ook als entity in Home Assistant. +# Make memory visible: this board has no PSRAM, so the free heap determines +# how much UI still fits. Also shows up as an entity in Home Assistant. debug: # Heap figures every five minutes: enough to spot a leak, few recorder rows. update_interval: 300s @@ -1581,7 +1579,7 @@ sensor: # "s" in its registry and would refuse a timestamp under that name. - platform: uptime type: timestamp - name: "Opgestart" + name: "Last boot" id: boot_time - platform: homeassistant @@ -1656,17 +1654,17 @@ sensor: 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. + # Tile 7 (Table lamp) is a brightness-only light: just brightness. + # Deliberately no percentage / cover_position / climate sensors — every + # HA subscription costs heap on a board without PSRAM. - platform: homeassistant id: tile7_brightness entity_id: ${TILE7_ENTITY} attribute: brightness on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - # Tegel 9 (Woonkamer) is een kleurenlamp: brightness + supported_color_modes. - # Geen percentage / cover / climate — die leest deze tegel nooit. + # Tile 9 (Living room) is a colour lamp: brightness + supported_color_modes. + # No percentage / cover / climate — this tile never reads those. - platform: homeassistant id: tile9_brightness entity_id: ${TILE9_ENTITY} @@ -1837,19 +1835,19 @@ binary_sensor: # ---------------------------------------------------------- text_sensor: - platform: template - name: "Schermfirmware" + name: "Screen firmware" entity_category: diagnostic lambda: 'return {"${SCREEN_FIRMWARE_VERSION}"};' update_interval: 60s # ESP Screen Manager matches the screen to its YAML profile and OTA address. - platform: template - name: "Apparaatnaam" + name: "Device name" entity_category: diagnostic lambda: 'return {"${DEVICE_NAME}"};' update_interval: 60s - platform: wifi_info ip_address: - name: "IP-adres" + name: "IP address" entity_category: diagnostic - platform: template id: smartdisplay_action @@ -1906,15 +1904,15 @@ text_sensor: 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. + # Living room reports color_temp/rgb/xy, so the colour button does appear here. - platform: homeassistant id: tile9_supported_color_modes entity_id: ${TILE9_ENTITY} attribute: supported_color_modes on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - # Gebruikt door de slider-overlay om te bepalen of de kleurknop nodig is. - # Deze lamp meldt ['brightness'], dus de knop blijft vanzelf verborgen. + # Used by the slider overlay to decide whether the colour button is needed. + # This lamp reports ['brightness'], so the button stays hidden automatically. - platform: homeassistant id: tile7_supported_color_modes entity_id: ${TILE7_ENTITY} @@ -2090,8 +2088,8 @@ switch: # Two SPI buses used by the CYD: # - lcd: built-in display # - my_touchscreen: built-in touch controller -# Twee SPI bussen op de 2432S028: een voor het display, een losse voor de -# resistieve XPT2046 touch (geen I2C op dit bord, in tegenstelling tot 032c). +# Two SPI buses on the 2432S028: one for the display, a separate one for the +# resistive XPT2046 touch (no I2C on this board, unlike the 032c). spi: - id: lcd clk_pin: GPIO14 @@ -2106,12 +2104,12 @@ output: - platform: ledc pin: GPIO21 id: gpio_backlight_pwm - # ESPHome's ledc default is 1kHz. Bij stilstand zie je dat niet (ruim - # boven wat het oog als flikkering waarneemt), maar bij beweging van het - # scherm onthult motion-blur de aan/uit-pulsen als strepen (zelfde effect - # als een PWM-LED die "schokt" als je hem beweegt). 20kHz zit ruim boven - # de snelheid waarbij beweging dat nog zichtbaar maakt, en laat nog altijd - # ruim voldoende duty-cycle resolutie over voor vloeiend dimmen. + # ESPHome's ledc default is 1kHz. You don't see that while still (well + # above what the eye perceives as flicker), but moving the screen reveals + # motion blur turning the on/off pulses into streaks (the same effect as + # a PWM LED that "judders" when you move it). 20kHz sits well above the + # speed at which motion still makes that visible, and still leaves ample + # duty-cycle resolution left over for smooth dimming. frequency: 20000Hz - platform: ledc @@ -2142,7 +2140,7 @@ light: blue: output_blue restore_mode: ALWAYS_OFF -# Touchscreen configuration (XPT2046, resistief, via SPI) +# Touchscreen configuration (XPT2046, resistive, via SPI) # XPT2046 calibration is board-specific. Poll at 50Hz; LVGL rotates touch # with the display. GPIO36 IRQ is deliberately unused (driver requests a # pull-up that this input-only pin cannot provide). @@ -2217,23 +2215,23 @@ lvgl: touchscreens: - touchscreen_id: ts_touch - # De ESP32-WROOM-32 heeft geen PSRAM; een volledige framebuffer (320x240x2 - # = 153600 bytes) past niet in DRAM. 25% is de gebruikelijke waarde. - # 12% in plaats van 25%: laat ~20KB extra aaneengesloten geheugen vrij, - # en LVGL schaalt de stroken van een ARGB-laag mee met de beschikbare - # tekenruimte, dus ook de gevraagde allocaties worden kleiner. + # The ESP32-WROOM-32 has no PSRAM; a full framebuffer (320x240x2 + # = 153600 bytes) doesn't fit in DRAM. 25% is the usual value. + # 12% instead of 25%: frees up ~20KB of extra contiguous memory, + # and LVGL scales the strips of an ARGB layer along with the available + # drawing space, so the requested allocations get smaller too. buffer_size: 12% - # 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. + # Direct local feedback on the WHOLE UI, not just the tiles. + # A theme sets the style per widget type, so this covers all + # buttons, tabs, sliders and the thermostat ring in one go. Non-clickable + # widgets never enter the pressed state, so those stay untouched. theme: obj: pressed: - # bg_opa 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 and NOT opa: `opa` at widget level forces LVGL to render the + # element to a separate layer, and that layer costs tens of KB's + # per pressed widget. That ran the memory out here. bg_opa: 45% slider: knob: @@ -2248,9 +2246,9 @@ lvgl: 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. + # Background: two-stop vertical gradient instead of the 320x240 + # RGB565 wallpaper. Saves ~150 KB flash and an image blit per redraw, + # which you can see directly in how smoothly it scrolls. - id: style_bg_grad bg_color: ${BG_TOP_COLOR} bg_grad_color: ${BG_BOTTOM_COLOR} @@ -2389,7 +2387,7 @@ lvgl: y: ${ALERT_TITLE_Y} width: ${ALERT_TEXT_W} long_mode: DOT - text: "Melding" + text: "Notification" text_font: headline text_color: 0x1B1B1B clickable: false @@ -2434,7 +2432,7 @@ lvgl: - label: id: alert_ok_label align: CENTER - text: "Oké" + text: "OK" text_font: headline text_color: 0xFFFFFF clickable: false @@ -2503,8 +2501,8 @@ lvgl: 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. + # Direct local feedback: LVGL applies this as soon as the finger + # touches the glass, in the same frame. Independent of Home Assistant. pressed: bg_opa: 45% widgets: @@ -2613,8 +2611,8 @@ lvgl: 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. + # Long-press mode "vacuum": opens the vacuum controls. + # Falls outside the action/slider chain, so that stays unchanged. - if: condition: lambda: "return std::string(\"${TILE1_LONGPRESS}\") == \"vacuum\";" @@ -2634,8 +2632,8 @@ lvgl: 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. + # Direct local feedback: LVGL applies this as soon as the finger + # touches the glass, in the same frame. Independent of Home Assistant. pressed: bg_opa: 45% widgets: @@ -2744,8 +2742,8 @@ lvgl: 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. + # Long-press mode "vacuum": opens the vacuum controls. + # Falls outside the action/slider chain, so that stays unchanged. - if: condition: lambda: "return std::string(\"${TILE2_LONGPRESS}\") == \"vacuum\";" @@ -2765,8 +2763,8 @@ lvgl: 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. + # Direct local feedback: LVGL applies this as soon as the finger + # touches the glass, in the same frame. Independent of Home Assistant. pressed: bg_opa: 45% widgets: @@ -2875,8 +2873,8 @@ lvgl: 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. + # Long-press mode "vacuum": opens the vacuum controls. + # Falls outside the action/slider chain, so that stays unchanged. - if: condition: lambda: "return std::string(\"${TILE3_LONGPRESS}\") == \"vacuum\";" @@ -2896,8 +2894,8 @@ lvgl: 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. + # Direct local feedback: LVGL applies this as soon as the finger + # touches the glass, in the same frame. Independent of Home Assistant. pressed: bg_opa: 45% widgets: @@ -3006,8 +3004,8 @@ lvgl: 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. + # Long-press mode "vacuum": opens the vacuum controls. + # Falls outside the action/slider chain, so that stays unchanged. - if: condition: lambda: "return std::string(\"${TILE4_LONGPRESS}\") == \"vacuum\";" @@ -3027,8 +3025,8 @@ lvgl: 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. + # Direct local feedback: LVGL applies this as soon as the finger + # touches the glass, in the same frame. Independent of Home Assistant. pressed: bg_opa: 45% widgets: @@ -3137,8 +3135,8 @@ lvgl: 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. + # Long-press mode "vacuum": opens the vacuum controls. + # Falls outside the action/slider chain, so that stays unchanged. - if: condition: lambda: "return std::string(\"${TILE5_LONGPRESS}\") == \"vacuum\";" @@ -3158,8 +3156,8 @@ lvgl: 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. + # Direct local feedback: LVGL applies this as soon as the finger + # touches the glass, in the same frame. Independent of Home Assistant. pressed: bg_opa: 45% widgets: @@ -3268,8 +3266,8 @@ lvgl: 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. + # Long-press mode "vacuum": opens the vacuum controls. + # Falls outside the action/slider chain, so that stays unchanged. - if: condition: lambda: "return std::string(\"${TILE6_LONGPRESS}\") == \"vacuum\";" @@ -3279,11 +3277,11 @@ lvgl: 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. + # ---------- TILE 7 — Table lamp ---------- + # Brightness-only lamp: deliberately no percentage/cover/climate + # sensors were created for this tile, those would only cost + # HA subscriptions and heap. The overlay therefore gets + # NAN / "" for the fields a light never uses. - obj: id: tile7 hidden: true @@ -3397,12 +3395,12 @@ lvgl: 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. + # ---------- TILE 8 — Mailbox reset ---------- + # input_select with options Full/Empty. The tile lights up as soon as + # the status is TILE8_ACTIVE_STATE ("Full"); a short tap sends + # input_select.select_option with option: "Empty". + # No slider mode, so this tile only needs ha_state_tile8 — + # no other HA subscription at all. - obj: id: tile8 hidden: true @@ -3469,8 +3467,8 @@ lvgl: lambda: 'return !runtime_tiles::enabled && cyd::touch_guard.accept(millis(), 8);' then: - script.execute: { id: publish_action, action: "tile8_long_press" } - # Slider-modus kan hier niet zonder eerst de bijbehorende - # sensors aan te maken; "action" werkt wel out of the box. + # Slider mode isn't possible here without first creating the + # corresponding sensors; "action" works out of the box. - if: condition: lambda: "return std::string(\"${TILE8_LONGPRESS}\") == \"action\";" @@ -3487,7 +3485,7 @@ lvgl: param_val: "" was_on: false - # ---------- TILE 9 — Woonkamer (verhuisd van tegel 2) ---------- + # ---------- TILE 9 — Living room (moved from tile 2) ---------- - obj: id: tile9 hidden: true @@ -3601,7 +3599,7 @@ lvgl: climate_max_temp: "30.0" climate_step: "0.5" - # ---------- TILE 10 — Alarm aan/uit ---------- + # ---------- TILE 10 — Alarm on/off ---------- - obj: id: tile10 hidden: true @@ -3712,7 +3710,7 @@ lvgl: - label: align: LEFT_MID x: 12 - text: "< Vorige" + text: "< Previous" text_font: label text_color: 0x1B1B1B clickable: false @@ -3754,7 +3752,7 @@ lvgl: - label: align: RIGHT_MID x: -12 - text: "Volgende >" + text: "Next >" text_font: label text_color: 0x1B1B1B clickable: false @@ -3827,12 +3825,12 @@ lvgl: bg_color: 0xFFFFFF bg_opa: 100% radius: 28 - # Enige widget met ZOWEL radius ALS een kind (overlay_icon). - # LVGL snijdt kinderen dan bij op de ronding, en dat kan - # alleen via een ARGB-laag van 96x160 = 46KB aaneengesloten. - # Lukt die allocatie niet, dan blijft LVGL het proberen tot - # de task watchdog het bord herstart. Ronding blijft, het - # bijsnijden gaat eraf. + # The only widget with BOTH a radius AND a child (overlay_icon). + # LVGL then clips children to the rounding, and that's only + # possible via a contiguous 96x160 = 46KB ARGB layer. + # If that allocation fails, LVGL keeps trying until the + # task watchdog restarts the board. The rounding stays, the + # clipping goes. clip_corner: false border_width: 0 @@ -3858,12 +3856,12 @@ lvgl: 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. + # The icon used to be a CHILD in the slider. A widget with a radius + # and children has to clip those children to the rounding, and + # LVGL can only do that via an ARGB layer of tens of KB's. Without + # PSRAM that sometimes fails, after which LVGL keeps retrying until + # the watchdog restarts the board. As a sibling on top, no layer + # is needed and it looks identical. - label: id: overlay_icon align: CENTER @@ -4212,8 +4210,8 @@ lvgl: align: TOP_LEFT x: 10 y: 9 - # 44 i.p.v. 36: iets groter doelvlak, groeit weg van de arc - # (top-left anchor blijft staan) i.p.v. ernaartoe. + # 44 instead of 36: slightly bigger tap target, grows away from the arc + # (top-left anchor stays put) instead of towards it. width: 44 height: 44 radius: 999 @@ -4269,7 +4267,7 @@ lvgl: id: climate_current_label align: TOP_MID y: 42 - text: "Nu: --" + text: "Now: --" text_font: sublabel_big text_color: 0x1B1B1B text_opa: 85% @@ -4281,9 +4279,9 @@ lvgl: y: 20 width: 184 height: 184 - # Zonder dit claimt de arc een onzichtbare rand extra - # klikgebied buiten de 184x184 box — vlakbij de kruisje- - # knop linksboven ontstaat daardoor overlap. + # Without this the arc claims an invisible extra + # click area outside the 184x184 box — near the close + # button top-left this creates overlap. ext_click_area: 0 start_angle: 135 end_angle: 45 @@ -4320,7 +4318,7 @@ lvgl: id: climate_set_label align: CENTER y: -16 - text: "Gewenst" + text: "Target" text_font: sublabel_big text_color: 0x1B1B1B text_opa: 68% @@ -4412,10 +4410,10 @@ lvgl: text_color: 0x1B1B1B 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. + # Replaces cycling through modes. Cycling sends a set_hvac_mode on every + # tap, so to get from 'cool' to 'dry' a heat pump actually passes through + # 'heat' and 'fan_only' along the way. Here you pick directly, and exactly + # one command is sent. - obj: id: climate_mode_overlay x: 0 @@ -4433,13 +4431,13 @@ lvgl: - label: align: TOP_MID y: 16 - text: "Modus" + text: "Mode" text_font: sublabel_big text_color: 0x1B1B1B clickable: false - # Flex zorgt dat niet-ondersteunde modi geen gat achterlaten: - # LVGL slaat verborgen kinderen over bij het schikken. + # Flex ensures unsupported modes don't leave a gap: + # LVGL skips hidden children when arranging. - obj: id: climate_mode_list align: TOP_MID @@ -4477,7 +4475,7 @@ lvgl: widgets: - label: align: CENTER - text: "Uit" + text: "Off" text_font: sublabel_big text_color: 0x1B1B1B clickable: false @@ -4498,7 +4496,7 @@ lvgl: widgets: - label: align: CENTER - text: "Verwarmen" + text: "Heat" text_font: sublabel_big text_color: 0x1B1B1B clickable: false @@ -4519,7 +4517,7 @@ lvgl: widgets: - label: align: CENTER - text: "Koelen" + text: "Cool" text_font: sublabel_big text_color: 0x1B1B1B clickable: false @@ -4540,7 +4538,7 @@ lvgl: widgets: - label: align: CENTER - text: "Verw./koelen" + text: "Heat/Cool" text_font: sublabel_big text_color: 0x1B1B1B clickable: false @@ -4582,7 +4580,7 @@ lvgl: widgets: - label: align: CENTER - text: "Drogen" + text: "Dry" text_font: sublabel_big text_color: 0x1B1B1B clickable: false @@ -4603,7 +4601,7 @@ lvgl: widgets: - label: align: CENTER - text: "Ventileren" + text: "Fan only" text_font: sublabel_big text_color: 0x1B1B1B clickable: false @@ -4612,7 +4610,7 @@ lvgl: id: cfan_title width: 100% height: 11 - text: "Ventilator" + text: "Fan" text_font: sublabel text_color: 0x6B6B6B text_align: CENTER @@ -4709,7 +4707,7 @@ lvgl: id: cswing_title width: 100% height: 11 - text: "Zwenken" + text: "Swing" text_font: sublabel text_color: 0x6B6B6B text_align: CENTER @@ -4829,8 +4827,8 @@ lvgl: # ---------- 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. + # A vacuum is not a switch: start, pause, stop and return to + # the dock are separate commands, and suction power is a separate axis. - obj: id: vacuum_overlay x: 0 @@ -5140,7 +5138,7 @@ lvgl: - label: align: TOP_MID y: 65 - text: "Volg de kalibratiewizard" + text: "Follow the calibration wizard" text_font: sublabel text_color: 0xFFFFFF - obj: @@ -5358,8 +5356,8 @@ script: - lvgl.widget.hide: vacuum_overlay # ---------- ALERT (HA action show_alert) ---------- - # The screen wakes, keeps its normal brightness and shows the card until Oké or the - # timeout (0: until Oké); the standby interval waits on alert_active. A new alert + # The screen wakes, keeps its normal brightness and shows the card until OK or the + # timeout (0: until OK); the standby interval waits on alert_active. A new alert # replaces the current one; every ending reports itself as esphome.screen_alert. - id: alert_show mode: restart @@ -5471,8 +5469,8 @@ script: 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. + # Almost instant brightness-up. ESPHome's default is 1s, and that feels sluggish + # when you touch the screen and are waiting on light. transition_length: 80ms brightness: !lambda "return screen_settings::current.brightness / 100.0f;" - lvgl.widget.hide: dim_wake_overlay @@ -5496,9 +5494,9 @@ script: id(show_tile_page).execute(); } - # Terug naar het basisscherm bij standby. Bewust via de ESPHome - # acties en NIET met lv_obj_add_flag in een lambda: dit script - # draait vanuit een interval, dus buiten LVGL's eigen afhandeling. + # Back to the home screen on standby. Deliberately via ESPHome's + # actions and NOT with lv_obj_add_flag in a lambda: this script + # runs from an interval, so outside LVGL's own handling. - lvgl.widget.hide: brightness_overlay - lvgl.widget.hide: color_detail_overlay - lvgl.widget.hide: climate_detail_overlay @@ -5572,9 +5570,9 @@ script: 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. + # Opens the mode picker. Hides modes the entity doesn't support and marks + # the current one, so you see where you stand before you pick something. + # Opens the vacuum controls and remembers which entity it concerns. - id: open_vacuum_overlay mode: restart parameters: @@ -5586,7 +5584,7 @@ script: 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. + # A standalone command: start, pause, stop or return_to_base. - id: vacuum_action mode: queued parameters: @@ -5601,7 +5599,7 @@ script: data: entity_id: !lambda "return id(active_entity);" - # Zuigkracht is een aparte as, los van start/stop. + # Suction power is a separate axis, apart from start/stop. - id: vacuum_fan_speed mode: queued parameters: @@ -5668,8 +5666,8 @@ script: fill(id(cswing_title), swing_chips, id(active_climate_swing_modes), id(active_climate_swing_mode)); - lvgl.widget.show: climate_mode_overlay - # Zet precies een modus. Geen tussenstappen, dus geen ongewenste - # commando's naar de warmtepomp onderweg. + # Sets exactly one mode. No intermediate steps, so no unwanted + # commands to the heat pump along the way. # Fan and swing modes from the picker: one call with the entity's own mode name. - id: set_climate_fan_mode mode: restart @@ -5786,7 +5784,7 @@ script: light_controls::fallback.kelvin = 3000; light_controls::fallback.hue = 200; light_controls::open("diagnostic", true, true, 75); - lv_label_set_text(id(color_detail_title), "Voorbeeld lamp"); + lv_label_set_text(id(color_detail_title), "Example lamp"); - lvgl.widget.hide: color_close_button - lvgl.widget.hide: color_done_button - lvgl.widget.show: color_detail_overlay @@ -5997,12 +5995,12 @@ script: 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). + // Debounce: same entity within 600ms ignored. Prevents + // touch noise from firing the same HA action multiple times + // (see globals above for why this is needed). uint32_t now = millis(); if (entity_id == id(last_action_entity) && (now - id(last_action_ms)) < 600) { - ESP_LOGD("tile", "do_tile_action: debounced repeat for %s (%ums na vorige)", + ESP_LOGD("tile", "do_tile_action: debounced repeat for %s (%ums after previous)", entity_id.c_str(), now - id(last_action_ms)); return false; } @@ -6257,11 +6255,11 @@ script: 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. + // Accent colour from the Home Assistant domain: the part before the dot + // in the entity_id. script/scene/button fire (ACCENT_ACTION), a + // light is a light (ACCENT_LIGHT), the rest is a device. + // NOTE: this helper appears twice in this file — here and in + // open_value_overlay. Update them together. auto accent_for = [](const std::string& entity_id) -> uint32_t { std::string d = entity_id.substr(0, entity_id.find('.')); if (d == "script" || d == "scene" || d == "button" || @@ -6270,7 +6268,7 @@ script: return ${ACCENT_DEVICE}; }; - // De sliderkleur komt van de tegel, dus die kan ook ${ACCENT_AUTO} zijn. + // The slider colour comes from the tile, so it can also be ${ACCENT_AUTO}. uint32_t fill_color = (slider_fill_color == ${ACCENT_AUTO}) ? accent_for(entity) : slider_fill_color; @@ -6334,11 +6332,11 @@ script: char current_buf[48]; if (isnan(climate_current_temperature)) { - snprintf(current_buf, sizeof(current_buf), "Nu: --"); + snprintf(current_buf, sizeof(current_buf), "Now: --"); } else if (id(active_climate_humidity) >= 0) { - snprintf(current_buf, sizeof(current_buf), "Nu: %.1f° · %d%%", climate_current_temperature, id(active_climate_humidity)); + snprintf(current_buf, sizeof(current_buf), "Now: %.1f° · %d%%", climate_current_temperature, id(active_climate_humidity)); } else { - snprintf(current_buf, sizeof(current_buf), "Nu: %.1f°", climate_current_temperature); + snprintf(current_buf, sizeof(current_buf), "Now: %.1f°", climate_current_temperature); } lv_label_set_text(id(climate_current_label), current_buf); @@ -6581,8 +6579,8 @@ script: 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. + // active_state not empty? Then only that exact HA state counts as "on". + // This way an input_select ("Full"/"Empty") can also light up a tile. auto is_active = [](const std::string& tile_type, const std::string& state, const std::string& active_state = std::string()) { if (!active_state.empty()) return state == active_state; @@ -6601,7 +6599,7 @@ script: 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. + // Tile 7/8 have no cover_state sensor, so no cover branch needed. bool t7_on = is_active("${TILE7_TYPE}", std::string(id(ha_state_tile7).state.c_str()), "${TILE7_ACTIVE_STATE}"); bool t8_on = is_active("${TILE8_TYPE}", std::string(id(ha_state_tile8).state.c_str()), "${TILE8_ACTIVE_STATE}"); bool t9_on = is_active("${TILE9_TYPE}", std::string(id(ha_state_tile9).state.c_str()), "${TILE9_ACTIVE_STATE}"); @@ -6637,7 +6635,7 @@ script: 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). + // Tile 7: light with only brightness. Tile 8: pure text (Full/Empty). set_value_auto(id(t7_value), t7_on, "${TILE7_TYPE}", "${TILE7_VALUE_MODE}", id(tile7_brightness).state, NAN, std::string(), NAN, NAN, NAN, @@ -6648,7 +6646,7 @@ script: NAN, NAN, NAN, "${TILE8_LABEL_ON}", "${TILE8_LABEL_OFF}"); - // Tegel 9: kleurenlamp (brightness). Tegel 10: puur tekst (Aan/Uit). + // Tile 9: colour lamp (brightness). Tile 10: pure text (On/Off). set_value_auto(id(t9_value), t9_on, "${TILE9_TYPE}", "${TILE9_VALUE_MODE}", id(tile9_brightness).state, NAN, std::string(), NAN, NAN, NAN, @@ -6663,33 +6661,33 @@ script: 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"); + if (state == "cleaning") set_lbl(id(t6_value), "Cleaning"); + else if (state == "docked") set_lbl(id(t6_value), "Docked"); + else if (state == "returning") set_lbl(id(t6_value), "Returning to dock"); + else if (state == "paused") set_lbl(id(t6_value), "Paused"); + else if (state == "error") set_lbl(id(t6_value), "Error"); else if (!state.empty()) set_lbl(id(t6_value), state); } if (!id(ha_state_tile1).has_state() || id(ha_state_tile1).state == "unknown" || id(ha_state_tile1).state == "unavailable") - set_lbl(id(t1_value), "Niet beschikbaar"); + set_lbl(id(t1_value), "Unavailable"); if (!id(ha_state_tile2).has_state() || id(ha_state_tile2).state == "unknown" || id(ha_state_tile2).state == "unavailable") - set_lbl(id(t2_value), "Niet beschikbaar"); + set_lbl(id(t2_value), "Unavailable"); if (!id(ha_state_tile3).has_state() || id(ha_state_tile3).state == "unknown" || id(ha_state_tile3).state == "unavailable") - set_lbl(id(t3_value), "Niet beschikbaar"); + set_lbl(id(t3_value), "Unavailable"); if (!id(ha_state_tile4).has_state() || id(ha_state_tile4).state == "unknown" || id(ha_state_tile4).state == "unavailable") - set_lbl(id(t4_value), "Niet beschikbaar"); + set_lbl(id(t4_value), "Unavailable"); if (!id(ha_state_tile5).has_state() || id(ha_state_tile5).state == "unknown" || id(ha_state_tile5).state == "unavailable") - set_lbl(id(t5_value), "Niet beschikbaar"); + set_lbl(id(t5_value), "Unavailable"); if (!id(ha_state_tile6).has_state() || id(ha_state_tile6).state == "unknown" || id(ha_state_tile6).state == "unavailable") - set_lbl(id(t6_value), "Niet beschikbaar"); + set_lbl(id(t6_value), "Unavailable"); if (!id(ha_state_tile7).has_state() || id(ha_state_tile7).state == "unknown" || id(ha_state_tile7).state == "unavailable") - set_lbl(id(t7_value), "Niet beschikbaar"); + set_lbl(id(t7_value), "Unavailable"); if (!id(ha_state_tile8).has_state() || id(ha_state_tile8).state == "unknown" || id(ha_state_tile8).state == "unavailable") - set_lbl(id(t8_value), "Niet beschikbaar"); + set_lbl(id(t8_value), "Unavailable"); if (!id(ha_state_tile9).has_state() || id(ha_state_tile9).state == "unknown" || id(ha_state_tile9).state == "unavailable") - set_lbl(id(t9_value), "Niet beschikbaar"); + set_lbl(id(t9_value), "Unavailable"); if (!id(ha_state_tile10).has_state() || id(ha_state_tile10).state == "unknown" || id(ha_state_tile10).state == "unavailable") - set_lbl(id(t10_value), "Niet beschikbaar"); + set_lbl(id(t10_value), "Unavailable"); if (id(active_control_kind) == "climate_temperature" && !lv_obj_has_flag(id(climate_detail_overlay), LV_OBJ_FLAG_HIDDEN)) { auto label_for_climate_mode = [](std::string mode) { for (char &c : mode) { @@ -6739,11 +6737,11 @@ script: char current_buf[48]; if (isnan(current_temperature)) { - snprintf(current_buf, sizeof(current_buf), "Nu: --"); + snprintf(current_buf, sizeof(current_buf), "Now: --"); } else if (id(active_climate_humidity) >= 0) { - snprintf(current_buf, sizeof(current_buf), "Nu: %.1f° · %d%%", current_temperature, id(active_climate_humidity)); + snprintf(current_buf, sizeof(current_buf), "Now: %.1f° · %d%%", current_temperature, id(active_climate_humidity)); } else { - snprintf(current_buf, sizeof(current_buf), "Nu: %.1f°", current_temperature); + snprintf(current_buf, sizeof(current_buf), "Now: %.1f°", current_temperature); } lv_label_set_text(id(climate_current_label), current_buf); @@ -6763,9 +6761,9 @@ script: { 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). + // Before the first time sync, now() is epoch 0 (1970) — don't show + // a misleading "01:00" then, just leave it blank until a + // real time comes in (after the first Home Assistant time sync). if (now.is_valid()) { char buf[10]; @@ -6781,11 +6779,11 @@ script: } } - // 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. + // Accent colour from the Home Assistant domain: the part before the dot + // in the entity_id. script/scene/button fire (ACCENT_ACTION), a + // light is a light (ACCENT_LIGHT), the rest is a device. + // NOTE: this helper appears twice in this file — here and in + // open_value_overlay. Update them together. auto accent_for = [](const std::string& entity_id) -> uint32_t { std::string d = entity_id.substr(0, entity_id.find('.')); if (d == "script" || d == "scene" || d == "button" || @@ -6826,7 +6824,7 @@ script: break; } } - // ${ACCENT_AUTO} = "bepaal zelf"; een echte hexwaarde wint altijd. + // ${ACCENT_AUTO} = "figure it out yourself"; a real hex value always wins. uint32_t accent = accent_for(entity_id); if (circle_active == ${ACCENT_AUTO}) circle_active = accent; if (icon_disabled == ${ACCENT_AUTO}) icon_disabled = accent; @@ -6923,12 +6921,12 @@ script: text: - platform: smart_display id: dashboard_inbox - name: "Tegelinstellingen" + name: "Tile settings" internal: ${DASHBOARD_INBOX_INTERNAL} button: - platform: template - name: "Touch kalibreren" + name: "Calibrate touch" internal: ${DASHBOARD_INBOX_INTERNAL} entity_category: config on_press: @@ -6937,7 +6935,7 @@ button: number: - platform: template id: setting_brightness - name: "Helderheid normaal" + name: "Normal brightness" entity_category: config min_value: 5 max_value: 100 @@ -6956,7 +6954,7 @@ number: id(apply_screen_settings).execute(); - platform: template id: setting_standby_brightness - name: "Helderheid standby" + name: "Standby brightness" entity_category: config min_value: 0 max_value: 100 @@ -6975,7 +6973,7 @@ number: id(apply_screen_settings).execute(); - platform: template id: setting_night_brightness - name: "Helderheid nacht" + name: "Night brightness" entity_category: config min_value: 0 max_value: 100 @@ -6994,7 +6992,7 @@ number: id(apply_screen_settings).execute(); - platform: template id: setting_standby_seconds - name: "Standby na" + name: "Standby after" entity_category: config min_value: 60 max_value: 86400 diff --git a/home-like.yaml b/home-like.yaml deleted file mode 100644 index 2a6628f..0000000 --- a/home-like.yaml +++ /dev/null @@ -1,5995 +0,0 @@ -############################################################ -# SmartDisplay – iOS Home app–inspired Tiles UI -# Target device: ESP32-2432S028 / Cheap Yellow Display (CYD) -# Resolution: configurable via ORIENTATION substitutions (landscape 320×240 default) -# -# Target hardware -# - Cheap Yellow Display (ESP32-2432S028 or compatible) -# - Built-in ILI9341 320x240 display -# - Built-in XPT2046 touchscreen -# -# What this config does -# - Renders a gradient background + a scrollable 2x5 grid of “tiles” (LVGL). -# - Each tile shows: icon (MDI font), title, and a value line. -# - Tile taps publish a short-lived text_sensor event (smartdisplay_action) -# so you can build Home Assistant automations on top. -# - Optional: the device can also call Home Assistant services directly -# (toggle lights/switches/fans/covers, activate scenes/scripts, set fan preset, -# set light brightness, set fan percentage, set cover position). -# -# The layout is inspired by common modern smart home UIs. -# -# How to customize -# 1) Edit the TILE1..TILE10 blocks below. -# 2) If you change icons, update the FONT GLYPHS block in the substitutions below. -# Each glyph must appear exactly once — same icon on multiple tiles is fine, just list it once. -# 3) The background is a gradient (BG_TOP_COLOR / BG_BOTTOM_COLOR), not an image. -# The only asset this config needs is fonts/materialdesignicons-webfont.ttf. -# -# Tile configuration model -# - TILE*_TYPE defines the entity kind: light | fan | switch | scene | script | cover -# - TILE*_TAP_ACTION defines what happens on short tap: -# auto | toggle | activate | fan_toggle_preset | custom -# - TILE*_LONGPRESS defines the long press mode: none | slider | action -# slider: opens a value overlay — configure with TILE*_LONGPRESS_SLIDER + TILE*_LONGPRESS_OFF_VALUE -# action: fires a different entity — configure with TILE*_LONGPRESS_ACTION* -# - TILE*_VALUE_MODE defines how the value line is rendered: -# auto | brightness | percentage | preset | text -# -# Notes -# - DIRECT_ACTIONS="true" enables direct HA service calls from the device. -# If "false", only the smartdisplay_action event is emitted. -# - In auto mode, values and long-press behavior are derived from TILE*_TYPE: -# light -> brightness, fan -> percentage/preset, scene/script -> text -# -# Secrets -# This config is kept byte-identical in two places: -# - this repository (flash with `esphome upload home-like.yaml`) -# - /config/esphome/smartdisplay.yaml on Home Assistant (flash from the -# ESPHome dashboard) -# Both read the same secret names, and the repo's secrets.yaml is a copy of the -# one in /config/esphome/. That way either route produces the same api key and -# OTA password, so the two never fight over the device. -# -# What you must adapt before flashing -# 1) Secrets: api_encryption_key, ota_password, WiFi credentials (see secrets.yaml) -# 2) Pins, touch calibration and transform if your CYD variant differs -# 3) TILE*_ENTITY / TILE*_STATE_ENTITY: Home Assistant entity IDs you want to control/mirror -# 4) Orientation + touch calibration: -# - rotation is set via LVGL_ROTATION in the ORIENTATION presets (LVGL -# rotates display + touch together, ESPHome 2026.4+) -# - calibration is device-specific -############################################################ - -substitutions: - - DIRECT_ACTIONS: "true" # true = call Home Assistant services directly, false = only emit action events - ROOM_NAME: "Woonkamer" # room name shown in the top-left header - TIME_24H: "true" # true = 24h format, false = 12h format - - ## DIM settings - AUTO_DIM: "true" # true = enable auto dimming - AUTO_DIM_TIMEOUT: "10" # seconds after last touch - MAX_BRIGHTNESS: "100" # backlight brightness in percent - DIM_BRIGHTNESS: "20" # dimmed backlight brightness in percent - NIGHT_MODE: "true" # true = use separate dim level at night - NIGHT_START_HOUR: "22" # 0-23 - NIGHT_END_HOUR: "7" # 0-23 - NIGHT_DIM_BRIGHTNESS: "10" # backlight brightness in percent at night - - # ============================================================ - # 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: "climate.airco_remeha_diva_ac" # main Home Assistant entity for this tile - TILE1_STATE_ENTITY: "climate.airco_remeha_diva_ac" # entity used for active/inactive state styling - TILE1_TITLE: "Airco" # tile title shown in the UI - TILE1_ICON: "\U000F001B" # MDI glyph — also add to MDI_GLYPH_* list above if not already there - TILE1_TYPE: "climate" # tile type: light | fan | switch | scene | script | cover | climate - TILE1_TAP_ACTION: "toggle" # tap action: auto | toggle | activate | fan_toggle_preset | custom - TILE1_LONGPRESS: "slider" # long press mode: none | slider | action - TILE1_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text - TILE1_LABEL_OFF: "Uit" # text shown when entity is off - TILE1_LABEL_ON: "Aan" # text shown for text-mode tiles when entity is on - TILE1_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") - - # --- colors --- - TILE1_CIRCLE_ACTIVE_COLOR: "${ACCENT_AUTO}" # icon circle color when active - TILE1_CIRCLE_DISABLED_COLOR: "0x7B7B6F" # icon circle color when inactive - TILE1_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active - TILE1_ICON_DISABLED_COLOR: "${ACCENT_AUTO}" # icon color when inactive - TILE1_BG_ACTIVE_COLOR: "0xFFFFFF" # tile background color when active - TILE1_BG_DISABLED_COLOR: "0x939391" # tile background color when inactive - TILE1_TITLE_ACTIVE_COLOR: "0x000000" # title color when active - TILE1_TITLE_DISABLED_COLOR: "0xFFFFFF" # title color when inactive - TILE1_VALUE_ACTIVE_COLOR: "0x7A7A7C" # value color when active - TILE1_VALUE_DISABLED_COLOR: "0xD9D9D9" # value color when inactive - - # --- advanced settings --- - TILE1_TAP_SERVICE: "climate.toggle" # optional service override, e.g. light.toggle - TILE1_TAP_PARAM_KEY: "" # optional extra service parameter key - TILE1_TAP_PARAM_VAL: "" # optional extra service parameter value - # if LONGPRESS = slider: - TILE1_LONGPRESS_SLIDER: "auto" # slider type: auto | brightness | percentage | cover_position - TILE1_LONGPRESS_OFF_VALUE: "0" # initial slider value when entity is off - # if LONGPRESS = action: - TILE1_LONGPRESS_ACTION: "" # entity_id to fire on long press - TILE1_LONGPRESS_ACTION_TYPE: "" # entity type: light | fan | switch | scene | script | cover | climate (empty = same as TILE*_TYPE) - TILE1_LONGPRESS_ACTION_SERVICE: "" # service to call (default: activate/turn_on — set e.g. "light.toggle" to toggle instead) - # if TYPE = climate: - TILE1_CLIMATE_MIN_TEMP: "16.0" # minimum target temperature shown in the thermostat overlay - TILE1_CLIMATE_MAX_TEMP: "32.0" # maximum target temperature shown in the thermostat overlay - TILE1_CLIMATE_STEP: "1.0" # target temperature step size - TILE1_CLIMATE_CURRENT_TEMP_ATTRIBUTE: "current_temperature" # HA attribute for current room temperature - TILE1_CLIMATE_TARGET_TEMP_ATTRIBUTE: "temperature" # HA attribute for target temperature - TILE1_CLIMATE_HUMIDITY_ATTRIBUTE: "current_humidity" # HA attribute for humidity; leave as-is if unavailable - - - # ============================================================ - # TILE 2 - # ============================================================ - - # --- basic settings --- - TILE2_ENTITY: "script.ambient_morning" # main Home Assistant entity for this tile - TILE2_STATE_ENTITY: "script.ambient_morning" # entity used for active/inactive state styling - TILE2_TITLE: "Ambient" # tile title shown in the UI - TILE2_ICON: "\U000F0387" # MDI glyph — also add to MDI_GLYPH_* list above if not already there - TILE2_TYPE: "script" # 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: "text" # value mode: auto | brightness | percentage | preset | text - TILE2_LABEL_OFF: "Start" # text shown when entity is off - TILE2_LABEL_ON: "Start" # text shown for text-mode tiles when entity is on - TILE2_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") - - # --- colors --- - TILE2_CIRCLE_ACTIVE_COLOR: "${ACCENT_AUTO}" # icon circle color when active - TILE2_CIRCLE_DISABLED_COLOR: "0x7B7B6F" # icon circle color when inactive - TILE2_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active - TILE2_ICON_DISABLED_COLOR: "${ACCENT_AUTO}" # icon color when inactive - TILE2_BG_ACTIVE_COLOR: "0xFFFFFF" # tile background color when active - TILE2_BG_DISABLED_COLOR: "0x939391" # tile background color when inactive - TILE2_TITLE_ACTIVE_COLOR: "0x000000" # title color when active - TILE2_TITLE_DISABLED_COLOR: "0xFFFFFF" # title color when inactive - TILE2_VALUE_ACTIVE_COLOR: "0x7A7A7C" # value color when active - TILE2_VALUE_DISABLED_COLOR: "0xD9D9D9" # value color when inactive - - # --- advanced settings --- - TILE2_TAP_SERVICE: "" # optional service override, e.g. light.toggle - TILE2_TAP_PARAM_KEY: "" # optional extra service parameter key - TILE2_TAP_PARAM_VAL: "" # optional extra service parameter value - # if LONGPRESS = slider: - TILE2_LONGPRESS_SLIDER: "auto" # slider type: auto | brightness | percentage | cover_position - TILE2_LONGPRESS_OFF_VALUE: "0" # initial slider value when entity is off - # if LONGPRESS = action: - TILE2_LONGPRESS_ACTION: "" # entity_id to fire on long press - TILE2_LONGPRESS_ACTION_TYPE: "" # entity type: light | fan | switch | scene | script | cover | climate (empty = same as TILE*_TYPE) - TILE2_LONGPRESS_ACTION_SERVICE: "" # service to call (default: activate/turn_on — set e.g. "light.toggle" to toggle instead) - # if TYPE = climate: - TILE2_CLIMATE_MIN_TEMP: "5.0" # minimum target temperature shown in the thermostat overlay - TILE2_CLIMATE_MAX_TEMP: "30.0" # maximum target temperature shown in the thermostat overlay - TILE2_CLIMATE_STEP: "0.5" # target temperature step size - TILE2_CLIMATE_CURRENT_TEMP_ATTRIBUTE: "current_temperature" # HA attribute for current room temperature - TILE2_CLIMATE_TARGET_TEMP_ATTRIBUTE: "temperature" # HA attribute for target temperature - TILE2_CLIMATE_HUMIDITY_ATTRIBUTE: "current_humidity" # HA attribute for humidity; leave as-is if unavailable - - - # ============================================================ - # TILE 3 - # ============================================================ - - # --- basic settings --- - TILE3_ENTITY: "script.1692558675845" # main Home Assistant entity for this tile - TILE3_STATE_ENTITY: "script.1692558675845" # entity used for active/inactive state styling - TILE3_TITLE: "Naar bed" # tile title shown in the UI - TILE3_ICON: "\U000F02E3" # MDI glyph — also add to MDI_GLYPH_* list above if not already there - TILE3_TYPE: "script" # tile type: light | fan | switch | scene | script | cover | climate - TILE3_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset | custom - TILE3_LONGPRESS: "none" # long press mode: none | slider | action - TILE3_VALUE_MODE: "text" # value mode: auto | brightness | percentage | preset | text - TILE3_LABEL_OFF: "Start" # text shown when entity is off - TILE3_LABEL_ON: "Start" # text shown for text-mode tiles when entity is on - TILE3_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") - - # --- colors --- - TILE3_CIRCLE_ACTIVE_COLOR: "${ACCENT_AUTO}" # icon circle color when active - TILE3_CIRCLE_DISABLED_COLOR: "0x7B7B6F" # icon circle color when inactive - TILE3_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active - TILE3_ICON_DISABLED_COLOR: "${ACCENT_AUTO}" # icon color when inactive - TILE3_BG_ACTIVE_COLOR: "0xFFFFFF" # tile background color when active - TILE3_BG_DISABLED_COLOR: "0x939391" # tile background color when inactive - TILE3_TITLE_ACTIVE_COLOR: "0x000000" # title color when active - TILE3_TITLE_DISABLED_COLOR: "0xFFFFFF" # title color when inactive - TILE3_VALUE_ACTIVE_COLOR: "0x7A7A7C" # value color when active - TILE3_VALUE_DISABLED_COLOR: "0xD9D9D9" # value color when inactive - - # --- advanced settings --- - TILE3_TAP_SERVICE: "" # optional service override, e.g. light.toggle - TILE3_TAP_PARAM_KEY: "" # optional extra service parameter key - TILE3_TAP_PARAM_VAL: "" # optional extra service parameter value - # if LONGPRESS = slider: - TILE3_LONGPRESS_SLIDER: "auto" # slider type: auto | brightness | percentage | cover_position - TILE3_LONGPRESS_OFF_VALUE: "0" # initial slider value when entity is off - # if LONGPRESS = action: - TILE3_LONGPRESS_ACTION: "" # entity_id to fire on long press - TILE3_LONGPRESS_ACTION_TYPE: "" # entity type: light | fan | switch | scene | script | cover | climate (empty = same as TILE*_TYPE) - TILE3_LONGPRESS_ACTION_SERVICE: "" # service to call (default: activate/turn_on — set e.g. "light.toggle" to toggle instead) - # if TYPE = climate: - TILE3_CLIMATE_MIN_TEMP: "5.0" # minimum target temperature shown in the thermostat overlay - TILE3_CLIMATE_MAX_TEMP: "30.0" # maximum target temperature shown in the thermostat overlay - TILE3_CLIMATE_STEP: "0.5" # target temperature step size - TILE3_CLIMATE_CURRENT_TEMP_ATTRIBUTE: "current_temperature" # HA attribute for current room temperature - TILE3_CLIMATE_TARGET_TEMP_ATTRIBUTE: "temperature" # HA attribute for target temperature - TILE3_CLIMATE_HUMIDITY_ATTRIBUTE: "current_humidity" # HA attribute for humidity; leave as-is if unavailable - - - # ============================================================ - # TILE 4 - # ============================================================ - - # --- basic settings --- - TILE4_ENTITY: "script.goede_morgen" # main Home Assistant entity for this tile - TILE4_STATE_ENTITY: "script.goede_morgen" # entity used for active/inactive state styling - TILE4_TITLE: "Goede morgen" # tile title shown in the UI - TILE4_ICON: "\U000F059C" # MDI glyph — also add to MDI_GLYPH_* list above if not already there - TILE4_TYPE: "script" # tile type: light | fan | switch | scene | script | cover | climate - TILE4_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset | custom - TILE4_LONGPRESS: "none" # long press mode: none | slider | action - TILE4_VALUE_MODE: "text" # value mode: auto | brightness | percentage | preset | text - TILE4_LABEL_OFF: "Start" # text shown when entity is off - TILE4_LABEL_ON: "Start" # text shown for text-mode tiles when entity is on - TILE4_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") - - # --- colors --- - TILE4_CIRCLE_ACTIVE_COLOR: "${ACCENT_AUTO}" # icon circle color when active - TILE4_CIRCLE_DISABLED_COLOR: "0x7B7B6F" # icon circle color when inactive - TILE4_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active - TILE4_ICON_DISABLED_COLOR: "${ACCENT_AUTO}" # icon color when inactive - TILE4_BG_ACTIVE_COLOR: "0xFFFFFF" # tile background color when active - TILE4_BG_DISABLED_COLOR: "0x939391" # tile background color when inactive - TILE4_TITLE_ACTIVE_COLOR: "0x000000" # title color when active - TILE4_TITLE_DISABLED_COLOR: "0xFFFFFF" # title color when inactive - TILE4_VALUE_ACTIVE_COLOR: "0x7A7A7C" # value color when active - TILE4_VALUE_DISABLED_COLOR: "0xD9D9D9" # value color when inactive - - # --- advanced settings --- - TILE4_TAP_SERVICE: "" # optional service override, e.g. light.toggle - TILE4_TAP_PARAM_KEY: "" # optional extra service parameter key - TILE4_TAP_PARAM_VAL: "" # optional extra service parameter value - # if LONGPRESS = slider: - TILE4_LONGPRESS_SLIDER: "auto" # slider type: auto | brightness | percentage | cover_position - TILE4_LONGPRESS_OFF_VALUE: "0" # initial slider value when entity is off - # if LONGPRESS = action: - TILE4_LONGPRESS_ACTION: "" # entity_id to fire on long press - TILE4_LONGPRESS_ACTION_TYPE: "" # entity type: light | fan | switch | scene | script | cover | climate (empty = same as TILE*_TYPE) - TILE4_LONGPRESS_ACTION_SERVICE: "" # service to call (default: activate/turn_on — set e.g. "light.toggle" to toggle instead) - # if TYPE = climate: - TILE4_CLIMATE_MIN_TEMP: "5.0" # minimum target temperature shown in the thermostat overlay - TILE4_CLIMATE_MAX_TEMP: "30.0" # maximum target temperature shown in the thermostat overlay - TILE4_CLIMATE_STEP: "0.5" # target temperature step size - TILE4_CLIMATE_CURRENT_TEMP_ATTRIBUTE: "current_temperature" # HA attribute for current room temperature - TILE4_CLIMATE_TARGET_TEMP_ATTRIBUTE: "temperature" # HA attribute for target temperature - TILE4_CLIMATE_HUMIDITY_ATTRIBUTE: "current_humidity" # HA attribute for humidity; leave as-is if unavailable - - - # ============================================================ - # TILE 5 - # ============================================================ - - # --- basic settings --- - TILE5_ENTITY: "script.1692529759669" # main Home Assistant entity for this tile - TILE5_STATE_ENTITY: "script.1692529759669" # entity used for active/inactive state styling - TILE5_TITLE: "Alles uit" # tile title shown in the UI - TILE5_ICON: "\U000F0425" # MDI glyph — also add to MDI_GLYPH_* list above if not already there - TILE5_TYPE: "script" # tile type: light | fan | switch | scene | script | cover | climate - TILE5_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset | custom - TILE5_LONGPRESS: "none" # long press mode: none | slider | action - TILE5_VALUE_MODE: "text" # value mode: auto | brightness | percentage | preset | text - TILE5_LABEL_OFF: "Start" # text shown when entity is off (closed) - TILE5_LABEL_ON: "Start" # text shown when entity is on (open) - TILE5_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") - - # --- colors --- - TILE5_CIRCLE_ACTIVE_COLOR: "${ACCENT_AUTO}" # icon circle color when active - TILE5_CIRCLE_DISABLED_COLOR: "0x7B7B6F" # icon circle color when inactive - TILE5_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active - TILE5_ICON_DISABLED_COLOR: "${ACCENT_AUTO}" # icon color when inactive - TILE5_BG_ACTIVE_COLOR: "0xFFFFFF" # tile background color when active - TILE5_BG_DISABLED_COLOR: "0x939391" # tile background color when inactive - TILE5_TITLE_ACTIVE_COLOR: "0x000000" # title color when active - TILE5_TITLE_DISABLED_COLOR: "0xFFFFFF" # title color when inactive - TILE5_VALUE_ACTIVE_COLOR: "0x7A7A7C" # value color when active - TILE5_VALUE_DISABLED_COLOR: "0xD9D9D9" # value color when inactive - - # --- advanced settings --- - TILE5_TAP_SERVICE: "" # optional service override, e.g. light.toggle - TILE5_TAP_PARAM_KEY: "" # optional extra service parameter key - TILE5_TAP_PARAM_VAL: "" # optional extra service parameter value - # if LONGPRESS = slider: - TILE5_LONGPRESS_SLIDER: "auto" # slider type: auto | brightness | percentage | cover_position - TILE5_LONGPRESS_OFF_VALUE: "0" # initial slider value when entity is off - # if LONGPRESS = action: - TILE5_LONGPRESS_ACTION: "" # entity_id to fire on long press - TILE5_LONGPRESS_ACTION_TYPE: "" # entity type: light | fan | switch | scene | script | cover | climate (empty = same as TILE*_TYPE) - TILE5_LONGPRESS_ACTION_SERVICE: "" # service to call (default: activate/turn_on — set e.g. "light.toggle" to toggle instead) - # if TYPE = climate: - TILE5_CLIMATE_MIN_TEMP: "5.0" # minimum target temperature shown in the thermostat overlay - TILE5_CLIMATE_MAX_TEMP: "30.0" # maximum target temperature shown in the thermostat overlay - TILE5_CLIMATE_STEP: "0.5" # target temperature step size - TILE5_CLIMATE_CURRENT_TEMP_ATTRIBUTE: "current_temperature" # HA attribute for current room temperature - TILE5_CLIMATE_TARGET_TEMP_ATTRIBUTE: "temperature" # HA attribute for target temperature - TILE5_CLIMATE_HUMIDITY_ATTRIBUTE: "current_humidity" # HA attribute for humidity; leave as-is if unavailable - - - # ============================================================ - # TILE 6 - # ============================================================ - - # --- basic settings --- - TILE6_ENTITY: "vacuum.s8" # main Home Assistant entity for this tile - TILE6_STATE_ENTITY: "binary_sensor.s8_schoonmaken" # entity used for active/inactive state styling - TILE6_TITLE: "Pippa" # tile title shown in the UI - TILE6_ICON: "\U000F070D" # MDI glyph — also add to MDI_GLYPH_* list above if not already there - TILE6_TYPE: "switch" # tile type: light | fan | switch | scene | script | cover | climate - TILE6_TAP_ACTION: "toggle" # tap action: auto | toggle | activate | fan_toggle_preset | custom - TILE6_LONGPRESS: "vacuum" # long press mode: none | slider | action - TILE6_VALUE_MODE: "text" # value mode: auto | brightness | percentage | preset | text - TILE6_LABEL_OFF: "Gedockt" # text shown when entity is off - TILE6_LABEL_ON: "Bezig" # text shown for text-mode tiles when entity is on - TILE6_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") - - # --- colors --- - TILE6_CIRCLE_ACTIVE_COLOR: "${ACCENT_AUTO}" # icon circle color when active - TILE6_CIRCLE_DISABLED_COLOR: "0x7B7B6F" # icon circle color when inactive - TILE6_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active - TILE6_ICON_DISABLED_COLOR: "${ACCENT_AUTO}" # icon color when inactive - TILE6_BG_ACTIVE_COLOR: "0xFFFFFF" # tile background color when active - TILE6_BG_DISABLED_COLOR: "0x939391" # tile background color when inactive - TILE6_TITLE_ACTIVE_COLOR: "0x000000" # title color when active - TILE6_TITLE_DISABLED_COLOR: "0xFFFFFF" # title color when inactive - TILE6_VALUE_ACTIVE_COLOR: "0x7A7A7C" # value color when active - TILE6_VALUE_DISABLED_COLOR: "0xD9D9D9" # value color when inactive - - # --- advanced settings --- - TILE6_TAP_SERVICE: "vacuum.start" # optional service override, e.g. fan.toggle - TILE6_TAP_PARAM_KEY: "" # optional extra service parameter key - TILE6_TAP_PARAM_VAL: "" # optional extra service parameter value - # if LONGPRESS = slider: - TILE6_LONGPRESS_SLIDER: "auto" # slider type: auto | brightness | percentage | cover_position - TILE6_LONGPRESS_OFF_VALUE: "0" # initial slider value when entity is off - # if LONGPRESS = action: - TILE6_LONGPRESS_ACTION: "vacuum.s8" # entity_id to fire on long press - TILE6_LONGPRESS_ACTION_TYPE: "switch" # entity type: light | fan | switch | scene | script | cover | climate (empty = same as TILE*_TYPE) - TILE6_LONGPRESS_ACTION_SERVICE: "vacuum.return_to_base" # service to call (default: activate/turn_on — set e.g. "light.toggle" to toggle instead) - # if TYPE = climate: - TILE6_CLIMATE_MIN_TEMP: "5.0" # minimum target temperature shown in the thermostat overlay - TILE6_CLIMATE_MAX_TEMP: "30.0" # maximum target temperature shown in the thermostat overlay - TILE6_CLIMATE_STEP: "0.5" # target temperature step size - TILE6_CLIMATE_CURRENT_TEMP_ATTRIBUTE: "current_temperature" # HA attribute for current room temperature - TILE6_CLIMATE_TARGET_TEMP_ATTRIBUTE: "temperature" # HA attribute for target temperature - TILE6_CLIMATE_HUMIDITY_ATTRIBUTE: "current_humidity" # HA attribute for humidity; leave as-is if unavailable - # ============================================================ - # TILE 7 — Tafellamp - # Alleen-helderheid lamp (HA meldt supported_color_modes: ['brightness']), - # dus de kleurenknop in de slider-overlay blijft vanzelf verborgen. - # ============================================================ - - # --- basic settings --- - TILE7_ENTITY: "light.tafellamp_licht" # main Home Assistant entity for this tile - TILE7_STATE_ENTITY: "light.tafellamp_licht" # entity used for active/inactive state styling - TILE7_TITLE: "Tafellamp" # tile title shown in the UI - TILE7_ICON: "\U000F06B5" # mdi:lamp — also listed in MDI_GLYPH_7 above - TILE7_TYPE: "light" # tile type: light | fan | switch | scene | script | cover | climate - TILE7_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset - TILE7_LONGPRESS: "slider" # long press mode: none | slider | action | vacuum - TILE7_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text - TILE7_LABEL_OFF: "Uit" # text shown when entity is off - TILE7_LABEL_ON: "Aan" # text shown for text-mode tiles when entity is on - TILE7_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") - - # --- colors --- - TILE7_CIRCLE_ACTIVE_COLOR: "${ACCENT_AUTO}" # icon circle color when active - TILE7_CIRCLE_DISABLED_COLOR: "0x7B7B6F" # icon circle color when inactive - TILE7_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active - TILE7_ICON_DISABLED_COLOR: "${ACCENT_AUTO}" # icon color when inactive - TILE7_BG_ACTIVE_COLOR: "0xFFFFFF" # tile background color when active - TILE7_BG_DISABLED_COLOR: "0x939391" # tile background color when inactive - TILE7_TITLE_ACTIVE_COLOR: "0x000000" # title color when active - TILE7_TITLE_DISABLED_COLOR: "0xFFFFFF" # title color when inactive - TILE7_VALUE_ACTIVE_COLOR: "0x7A7A7C" # value color when active - TILE7_VALUE_DISABLED_COLOR: "0xD9D9D9" # value color when inactive - - # --- advanced settings --- - TILE7_TAP_SERVICE: "" # optional service override, e.g. light.toggle - TILE7_TAP_PARAM_KEY: "" # optional extra service parameter key - TILE7_TAP_PARAM_VAL: "" # optional extra service parameter value - # if LONGPRESS = slider: - TILE7_LONGPRESS_SLIDER: "auto" # auto = brightness voor een light - TILE7_LONGPRESS_OFF_VALUE: "0" # initial slider value when entity is off - # if LONGPRESS = action: - TILE7_LONGPRESS_ACTION: "" # entity_id to fire on long press - TILE7_LONGPRESS_ACTION_TYPE: "" # entity type (empty = same as TILE*_TYPE) - TILE7_LONGPRESS_ACTION_SERVICE: "" # service to call (default: activate/turn_on) - - # ============================================================ - # TILE 8 — Brievenbus reset - # input_select.brievenbus_status heeft opties ['Vol', 'Leeg']. - # De tegel licht op zodra de status "Vol" is (ACTIVE_STATE), en een korte - # tik zet hem terug op "Leeg" via input_select.select_option. - # ============================================================ - - # --- basic settings --- - TILE8_ENTITY: "input_select.brievenbus_status" # main Home Assistant entity for this tile - TILE8_STATE_ENTITY: "input_select.brievenbus_status" # entity used for active/inactive state styling - TILE8_TITLE: "Brievenbus" # tile title shown in the UI - TILE8_ICON: "\U000F06EE" # mdi:mailbox — also listed in MDI_GLYPH_8 above - TILE8_TYPE: "switch" # generiek type; het echte gedrag komt uit TAP_SERVICE hieronder - TILE8_TAP_ACTION: "activate" # activate + TAP_SERVICE = losse service call - TILE8_LONGPRESS: "none" # long press stuurt alleen het event naar HA - TILE8_VALUE_MODE: "text" # toont LABEL_ON / LABEL_OFF - TILE8_LABEL_OFF: "Leeg" # text shown when entity is off / inactive - TILE8_LABEL_ON: "Vol" # text shown when entity is active - TILE8_ACTIVE_STATE: "Vol" # tegel is "aan" zodra de status exact "Vol" is - - # --- colors --- - TILE8_CIRCLE_ACTIVE_COLOR: "0xE5484D" # rood zodra er post ligt - TILE8_CIRCLE_DISABLED_COLOR: "0x7B7B6F" # icon circle color when inactive - TILE8_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active - TILE8_ICON_DISABLED_COLOR: "0xE5484D" # icon color when inactive - TILE8_BG_ACTIVE_COLOR: "0xFFFFFF" # tile background color when active - TILE8_BG_DISABLED_COLOR: "0x939391" # tile background color when inactive - TILE8_TITLE_ACTIVE_COLOR: "0x000000" # title color when active - TILE8_TITLE_DISABLED_COLOR: "0xFFFFFF" # title color when inactive - TILE8_VALUE_ACTIVE_COLOR: "0x7A7A7C" # value color when active - TILE8_VALUE_DISABLED_COLOR: "0xD9D9D9" # value color when inactive - - # --- advanced settings --- - TILE8_TAP_SERVICE: "input_select.select_option" # zet de input_select op een vaste optie - TILE8_TAP_PARAM_KEY: "option" # parameternaam voor input_select.select_option - TILE8_TAP_PARAM_VAL: "Leeg" # de "reset"-waarde - # if LONGPRESS = slider: - TILE8_LONGPRESS_SLIDER: "auto" # niet in gebruik (LONGPRESS = none) - TILE8_LONGPRESS_OFF_VALUE: "0" # niet in gebruik - # if LONGPRESS = action: - TILE8_LONGPRESS_ACTION: "" # entity_id to fire on long press - TILE8_LONGPRESS_ACTION_TYPE: "" # entity type (empty = same as TILE*_TYPE) - TILE8_LONGPRESS_ACTION_SERVICE: "" # service to call (default: activate/turn_on) - - # ============================================================ - # TILE 9 — 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.woonkamer_2" # main Home Assistant entity for this tile - TILE9_STATE_ENTITY: "light.woonkamer_2" # entity used for active/inactive state styling - TILE9_TITLE: "Woonkamer" # 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: "slider" # long press mode: none | slider | action | vacuum - TILE9_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text - TILE9_LABEL_OFF: "Uit" # text shown when entity is off - TILE9_LABEL_ON: "Aan" # text shown for text-mode tiles when entity is on - TILE9_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") - - # --- colors --- - TILE9_CIRCLE_ACTIVE_COLOR: "${ACCENT_AUTO}" # light -> ACCENT_LIGHT (amber) - TILE9_CIRCLE_DISABLED_COLOR: "0x7B7B6F" # icon circle color when inactive - TILE9_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active - TILE9_ICON_DISABLED_COLOR: "${ACCENT_AUTO}" # icon color when inactive - TILE9_BG_ACTIVE_COLOR: "0xFFFFFF" # tile background color when active - TILE9_BG_DISABLED_COLOR: "0x939391" # tile background color when inactive - TILE9_TITLE_ACTIVE_COLOR: "0x000000" # title color when active - TILE9_TITLE_DISABLED_COLOR: "0xFFFFFF" # title color when inactive - TILE9_VALUE_ACTIVE_COLOR: "0x7A7A7C" # value color when active - TILE9_VALUE_DISABLED_COLOR: "0xD9D9D9" # value color when inactive - - # --- advanced settings --- - TILE9_TAP_SERVICE: "" # optional service override, e.g. light.toggle - TILE9_TAP_PARAM_KEY: "" # optional extra service parameter key - TILE9_TAP_PARAM_VAL: "" # optional extra service parameter value - # if LONGPRESS = slider: - TILE9_LONGPRESS_SLIDER: "auto" # auto = brightness voor een light - TILE9_LONGPRESS_OFF_VALUE: "0" # initial slider value when entity is off - # if LONGPRESS = action: - TILE9_LONGPRESS_ACTION: "" # entity_id to fire on long press - TILE9_LONGPRESS_ACTION_TYPE: "" # entity type (empty = same as TILE*_TYPE) - TILE9_LONGPRESS_ACTION_SERVICE: "" # service to call (default: activate/turn_on) - - # ============================================================ - # TILE 10 — Alarm aan/uit - # input_boolean.alarm_engaged. Een input_boolean reageert niet op - # switch.toggle, vandaar de expliciete TAP_SERVICE. - # ============================================================ - - # --- basic settings --- - TILE10_ENTITY: "input_boolean.alarm_engaged" # main Home Assistant entity for this tile - TILE10_STATE_ENTITY: "input_boolean.alarm_engaged" # entity used for active/inactive state styling - TILE10_TITLE: "Alarm" # tile title shown in the UI - TILE10_ICON: "\U000F002A" # mdi:alarm-light — also listed in MDI_GLYPH_10 above - TILE10_TYPE: "switch" # generiek aan/uit-type; de service hieronder doet het echte werk - TILE10_TAP_ACTION: "toggle" # tap action: auto | toggle | activate | fan_toggle_preset - TILE10_LONGPRESS: "none" # long press stuurt alleen het event naar HA - TILE10_VALUE_MODE: "text" # toont LABEL_ON / LABEL_OFF - TILE10_LABEL_OFF: "Uit" # text shown when entity is off - TILE10_LABEL_ON: "Aan" # text shown when entity is on - TILE10_ACTIVE_STATE: "" # input_boolean gebruikt gewoon on/off - - # --- colors --- - TILE10_CIRCLE_ACTIVE_COLOR: "${ACCENT_AUTO}" # input_boolean -> ACCENT_DEVICE (cyaan) - TILE10_CIRCLE_DISABLED_COLOR: "0x7B7B6F" # icon circle color when inactive - TILE10_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active - TILE10_ICON_DISABLED_COLOR: "${ACCENT_AUTO}" # icon color when inactive - TILE10_BG_ACTIVE_COLOR: "0xFFFFFF" # tile background color when active - TILE10_BG_DISABLED_COLOR: "0x939391" # tile background color when inactive - TILE10_TITLE_ACTIVE_COLOR: "0x000000" # title color when active - TILE10_TITLE_DISABLED_COLOR: "0xFFFFFF" # title color when inactive - TILE10_VALUE_ACTIVE_COLOR: "0x7A7A7C" # value color when active - TILE10_VALUE_DISABLED_COLOR: "0xD9D9D9" # value color when inactive - - # --- advanced settings --- - TILE10_TAP_SERVICE: "input_boolean.toggle" # switch.toggle werkt niet op een input_boolean - TILE10_TAP_PARAM_KEY: "" # optional extra service parameter key - TILE10_TAP_PARAM_VAL: "" # optional extra service parameter value - # if LONGPRESS = slider: - TILE10_LONGPRESS_SLIDER: "auto" # niet in gebruik (LONGPRESS = none) - TILE10_LONGPRESS_OFF_VALUE: "0" # niet in gebruik - # if LONGPRESS = action: - TILE10_LONGPRESS_ACTION: "" # entity_id to fire on long press - TILE10_LONGPRESS_ACTION_TYPE: "" # entity type (empty = same as TILE*_TYPE) - TILE10_LONGPRESS_ACTION_SERVICE: "" # service to call (default: activate/turn_on) - - - # ============================================================ - # ORIENTATION / LAYOUT - # Uncomment ONE preset block below, comment out the others. - # Touch calibration values (TOUCH_CAL_*) are device-specific — - # portrait/flipped values are approximate and may need tuning. - # - # LVGL_ROTATION (0/90/180/270) drives the rotation — LVGL rotates both the - # display output and the touch coordinates (ESPHome 2026.4+). DISPLAY_W/H are - # the *logical* (post-rotation) canvas size used by the tile layout below; - # the physical panel is always driven at its native 240×320 (see display:). - # - # Tile content layout: - # Landscape — icon left, title + value to its right - # Portrait — icon centered on top, title + value stacked below - # ============================================================ - - # ----- 0° — Landscape 320×240 (default, USB on right) ----- - ORIENTATION: "0" - DISPLAY_W: "320" - DISPLAY_H: "240" - LVGL_ROTATION: "90" - TOUCH_SWAP_XY: "false" - TOUCH_MIRROR_X: "false" - TOUCH_MIRROR_Y: "false" - TOUCH_CAL_X_MIN: "280" - TOUCH_CAL_X_MAX: "3860" - TOUCH_CAL_Y_MIN: "340" - TOUCH_CAL_Y_MAX: "3860" - GRID_COL1_X: "9" - GRID_COL2_X: "164" - GRID_ROW1_Y: "8" - GRID_ROW2_Y: "68" - GRID_ROW3_Y: "128" - GRID_ROW4_Y: "188" - GRID_ROW5_Y: "248" - SCROLL_Y: "44" - SCROLL_H: "196" - TILE_W: "147" - TILE_H: "52" - HEADER_Y: "14" - OVERLAY_SLIDER_W: "96" - OVERLAY_SLIDER_H: "160" - OVERLAY_ICON_Y: "74" - BG_TOP_COLOR: "0xAEC4D9" # achtergrond boven (zacht blauw) - BG_BOTTOM_COLOR: "0xD6BB9E" # achtergrond onder (warm zand) - TILE_ICON_X: "0" - TILE_ICON_Y: "0" - TILE_TITLE_X: "48" - TILE_TITLE_Y: "4" - TILE_VALUE_X: "48" - TILE_VALUE_Y: "18" - - # ----- 90° — Portrait 240×320 (USB on bottom) ----- - # ORIENTATION: "90" - # DISPLAY_W: "240" - # DISPLAY_H: "320" - # LVGL_ROTATION: "180" - # TOUCH_SWAP_XY: "false" - # TOUCH_MIRROR_X: "true" - # TOUCH_MIRROR_Y: "false" - # TOUCH_CAL_X_MIN: "280" - # TOUCH_CAL_X_MAX: "3860" - # TOUCH_CAL_Y_MIN: "340" - # TOUCH_CAL_Y_MAX: "3860" - # GRID_COL1_X: "6" - # GRID_COL2_X: "123" - # GRID_ROW1_Y: "8" - # GRID_ROW2_Y: "96" - # GRID_ROW3_Y: "184" - # GRID_ROW4_Y: "272" - # GRID_ROW5_Y: "360" - # SCROLL_Y: "44" - # SCROLL_H: "276" - # TILE_W: "111" - # TILE_H: "80" - # HEADER_Y: "14" - # OVERLAY_SLIDER_W: "96" - # OVERLAY_SLIDER_H: "200" - # OVERLAY_ICON_Y: "94" - # BG_TOP_COLOR: "0xAEC4D9" # achtergrond boven (zacht blauw) - # BG_BOTTOM_COLOR: "0xD6BB9E" # achtergrond onder (warm zand) - # TILE_ICON_X: "0" - # TILE_ICON_Y: "0" - # TILE_TITLE_X: "4" - # TILE_TITLE_Y: "38" - # TILE_VALUE_X: "4" - # TILE_VALUE_Y: "52" - - # ----- 180° — Landscape flipped 320×240 (USB on left) ----- - # ORIENTATION: "180" - # DISPLAY_W: "320" - # DISPLAY_H: "240" - # LVGL_ROTATION: "270" - # TOUCH_SWAP_XY: "false" - # TOUCH_MIRROR_X: "true" - # TOUCH_MIRROR_Y: "false" - # TOUCH_CAL_X_MIN: "280" - # TOUCH_CAL_X_MAX: "3860" - # TOUCH_CAL_Y_MIN: "340" - # TOUCH_CAL_Y_MAX: "3860" - # GRID_COL1_X: "9" - # GRID_COL2_X: "164" - # GRID_ROW1_Y: "8" - # GRID_ROW2_Y: "68" - # GRID_ROW3_Y: "128" - # GRID_ROW4_Y: "188" - # GRID_ROW5_Y: "248" - # SCROLL_Y: "44" - # SCROLL_H: "196" - # TILE_W: "147" - # TILE_H: "52" - # HEADER_Y: "14" - # OVERLAY_SLIDER_W: "96" - # OVERLAY_SLIDER_H: "160" - # OVERLAY_ICON_Y: "74" - # BG_TOP_COLOR: "0xAEC4D9" # achtergrond boven (zacht blauw) - # BG_BOTTOM_COLOR: "0xD6BB9E" # achtergrond onder (warm zand) - # TILE_ICON_X: "0" - # TILE_ICON_Y: "0" - # TILE_TITLE_X: "48" - # TILE_TITLE_Y: "4" - # TILE_VALUE_X: "48" - # TILE_VALUE_Y: "18" - - # ----- 270° — Portrait flipped 240×320 (USB on top) ----- - # ORIENTATION: "270" - # DISPLAY_W: "240" - # DISPLAY_H: "320" - # LVGL_ROTATION: "0" - # TOUCH_SWAP_XY: "false" - # TOUCH_MIRROR_X: "true" - # TOUCH_MIRROR_Y: "false" - # TOUCH_CAL_X_MIN: "280" - # TOUCH_CAL_X_MAX: "3860" - # TOUCH_CAL_Y_MIN: "340" - # TOUCH_CAL_Y_MAX: "3860" - # GRID_COL1_X: "6" - # GRID_COL2_X: "123" - # GRID_ROW1_Y: "8" - # GRID_ROW2_Y: "96" - # GRID_ROW3_Y: "184" - # GRID_ROW4_Y: "272" - # GRID_ROW5_Y: "360" - # SCROLL_Y: "44" - # SCROLL_H: "276" - # TILE_W: "111" - # TILE_H: "80" - # HEADER_Y: "14" - # OVERLAY_SLIDER_W: "96" - # OVERLAY_SLIDER_H: "200" - # OVERLAY_ICON_Y: "94" - # BG_TOP_COLOR: "0xAEC4D9" # achtergrond boven (zacht blauw) - # BG_BOTTOM_COLOR: "0xD6BB9E" # achtergrond onder (warm zand) - # TILE_ICON_X: "0" - # TILE_ICON_Y: "0" - # TILE_TITLE_X: "4" - # TILE_TITLE_Y: "38" - # TILE_VALUE_X: "4" - # TILE_VALUE_Y: "52" - -# ============================================================ -# No need to edit below this line. -# All configuration is done via the substitutions above. -# Credentials are managed in secrets.yaml (see secrets.yaml.example). -# The Material Design Icons font is included at fonts/materialdesignicons-webfont.ttf (Apache 2.0). -# Source: https://github.com/Templarian/MaterialDesign-Webfont -# ============================================================ - -esphome: - name: smartdisplay - friendly_name: SmartDisplay - on_boot: - priority: 600 - then: - - lambda: |- - id(last_touch_ms) = millis(); - id(display_dimmed) = false; - - light.turn_on: - id: back_light - brightness: !lambda "return atoi(\"${MAX_BRIGHTNESS}\") / 100.0f;" - - script.execute: ui_refresh - -esp32: - board: esp32dev - framework: - type: arduino - advanced: - # ESPHome adviseert deze twee zelf in de boot-log. Samen leveren ze - # ~40KB extra IRAM en een kleinere binary, en op dit bord telt elke KB. - sram1_as_iram: true - # Let op: hiermee draait deze firmware niet meer op ESP32's ouder dan - # rev 3.1. Dit bord is 3.1, dus prima, maar niet blind overzetten. - minimum_chip_revision: "3.1" - version: recommended - variant: esp32 - -logger: - # Voor diagnose tijdelijk op DEBUG of VERBOSE zetten. Let op: ESPHome logt - # zijn configuratieregels op niveau CONFIG, die zijn op INFO onzichtbaar. - level: INFO - - -# Credentials are loaded from secrets.yaml — copy secrets.yaml.example to secrets.yaml and fill in your values. -api: - encryption: - key: !secret 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: |- - if (std::string("${AUTO_DIM}") != "true") return false; - if (id(display_dimmed)) return false; - return (millis() - id(last_touch_ms)) > (atoi("${AUTO_DIM_TIMEOUT}") * 1000UL); - then: - - script.execute: dim_display - -time: - - platform: homeassistant - id: homeassistant_time - on_time: - - seconds: 0 - minutes: /1 - then: - - script.execute: ui_refresh - - - seconds: 0 - minutes: 0 - hours: ${NIGHT_START_HOUR} - then: - - if: - condition: - lambda: 'return id(display_dimmed);' - then: - - script.execute: dim_display - - - seconds: 0 - minutes: 0 - hours: ${NIGHT_END_HOUR} - then: - - if: - condition: - lambda: 'return id(display_dimmed);' - then: - - script.execute: dim_display - -globals: - - id: active_entity - type: std::string - restore_value: no - - - id: active_control_kind - type: std::string - restore_value: no - - - id: active_value_suffix - type: std::string - restore_value: no - - - id: last_touch_ms - type: uint32_t - restore_value: no - initial_value: '0' - - - id: display_dimmed - type: bool - restore_value: no - initial_value: 'false' - - - id: active_label_off - type: std::string - restore_value: no - - - id: active_label_on - type: std::string - restore_value: no - - - id: active_title - type: std::string - restore_value: no - - - id: active_icon_text - type: std::string - restore_value: no - - - id: active_brightness_pct - type: int - restore_value: no - initial_value: '100' - - - id: active_supports_color - type: bool - restore_value: no - initial_value: 'false' - - - id: active_supports_temperature - type: bool - restore_value: no - initial_value: 'false' - - - id: active_color_detail_tab - type: int - restore_value: no - initial_value: '0' - - - id: active_climate_current_x10 - type: int - restore_value: no - initial_value: '0' - - - id: active_climate_target_x10 - type: int - restore_value: no - initial_value: '200' - - - id: active_climate_humidity - type: int - restore_value: no - initial_value: '-1' - - - id: active_climate_min_x10 - type: int - restore_value: no - initial_value: '50' - - - id: active_climate_max_x10 - type: int - restore_value: no - initial_value: '300' - - - id: active_climate_step_x10 - type: int - restore_value: no - initial_value: '5' - - - id: active_climate_mode - type: std::string - restore_value: no - - - id: active_climate_supported_modes - type: std::string - restore_value: no - -font: - - file: "gfonts://Roboto@500" - id: time_label - size: 18 - bpp: 4 - glyphs: ['0','1','2','3','4','5','6','7','8','9',':',' ', 'A', 'M', 'P'] - - - file: "gfonts://Roboto@500" - id: headline - size: 18 - bpp: 4 - glyphs: ['&','@','!',',','.','?','"','%','(',')','+','-','_',':','°', - '0','1','2','3','4','5','6','7','8','9', - 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z', - ' ','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z', - 'ä','ö','ü','Ä','Ö','Ü','ß','/'] - - - file: "gfonts://Roboto@700" - id: label - size: 11 - bpp: 4 - glyphs: ['&','@','!',',','.','?','"','%','(',')','+','-','_',':','°', - '0','1','2','3','4','5','6','7','8','9', - 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z', - ' ','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z', - 'ä','ö','ü','Ä','Ö','Ü','ß','/'] - - - file: "gfonts://Roboto@400" - id: sublabel - size: 11 - bpp: 4 - glyphs: ['&','@','!',',','.','?','"','%','(',')','+','-','_',':','°', - '0','1','2','3','4','5','6','7','8','9', - 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z', - ' ','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z', - 'ä','ö','ü','Ä','Ö','Ü','ß','/','…'] - - - file: "gfonts://Roboto@400" - id: sublabel_big - size: 14 - bpp: 4 - glyphs: ['&','@','!',',','.','?','"','%','(',')','+','-','_',':','°', - '0','1','2','3','4','5','6','7','8','9', - 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z', - ' ','a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z', - 'ä','ö','ü','Ä','Ö','Ü','ß','/','…'] - - # Material Design Icons (MDI) — included at fonts/materialdesignicons-webfont.ttf (Apache 2.0) - # Source: https://github.com/Templarian/MaterialDesign-Webfont - - file: 'fonts/materialdesignicons-webfont.ttf' - id: materialdesign_icons - size: 28 - glyphs: - - "${MDI_GLYPH_1}" - - "${MDI_GLYPH_2}" - - "${MDI_GLYPH_3}" - - "${MDI_GLYPH_4}" - - "${MDI_GLYPH_5}" - - "${MDI_GLYPH_6}" - - "${MDI_GLYPH_7}" - - "${MDI_GLYPH_8}" - - "${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 - fragmentation: - name: "Heap Fragmentation" - block: - name: "Heap Largest Block" - - platform: wifi_signal - name: "Wifi Signal" - update_interval: 600s - - - platform: uptime - name: "Uptime" - id: uptime_s - update_interval: 15s - - - platform: homeassistant - id: tile1_brightness - entity_id: ${TILE1_ENTITY} - attribute: brightness - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile1_percentage - entity_id: ${TILE1_ENTITY} - attribute: percentage - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile2_brightness - entity_id: ${TILE2_ENTITY} - attribute: brightness - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile2_percentage - entity_id: ${TILE2_ENTITY} - attribute: percentage - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile3_brightness - entity_id: ${TILE3_ENTITY} - attribute: brightness - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile3_percentage - entity_id: ${TILE3_ENTITY} - attribute: percentage - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile4_brightness - entity_id: ${TILE4_ENTITY} - attribute: brightness - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile4_percentage - entity_id: ${TILE4_ENTITY} - attribute: percentage - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile5_brightness - entity_id: ${TILE5_ENTITY} - attribute: brightness - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile5_percentage - entity_id: ${TILE5_ENTITY} - attribute: percentage - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile6_brightness - entity_id: ${TILE6_ENTITY} - attribute: brightness - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile6_percentage - entity_id: ${TILE6_ENTITY} - attribute: percentage - on_value: { then: [ script.execute: ui_refresh ] } - - # Tegel 7 (Tafellamp) is een alleen-helderheid light: alleen brightness. - # Bewust geen percentage / cover_position / climate-sensors — elke - # HA-subscription kost heap op een bord zonder PSRAM. - - platform: homeassistant - id: tile7_brightness - entity_id: ${TILE7_ENTITY} - attribute: brightness - on_value: { then: [ script.execute: ui_refresh ] } - - # 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: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile1_cover_position - entity_id: ${TILE1_ENTITY} - attribute: current_position - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile2_cover_position - entity_id: ${TILE2_ENTITY} - attribute: current_position - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile3_cover_position - entity_id: ${TILE3_ENTITY} - attribute: current_position - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile4_cover_position - entity_id: ${TILE4_ENTITY} - attribute: current_position - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile5_cover_position - entity_id: ${TILE5_ENTITY} - attribute: current_position - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile6_cover_position - entity_id: ${TILE6_ENTITY} - attribute: current_position - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile1_climate_current_temperature - entity_id: ${TILE1_ENTITY} - attribute: ${TILE1_CLIMATE_CURRENT_TEMP_ATTRIBUTE} - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile1_climate_target_temperature - entity_id: ${TILE1_ENTITY} - attribute: ${TILE1_CLIMATE_TARGET_TEMP_ATTRIBUTE} - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile1_climate_humidity - entity_id: ${TILE1_ENTITY} - attribute: ${TILE1_CLIMATE_HUMIDITY_ATTRIBUTE} - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile2_climate_current_temperature - entity_id: ${TILE2_ENTITY} - attribute: ${TILE2_CLIMATE_CURRENT_TEMP_ATTRIBUTE} - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile2_climate_target_temperature - entity_id: ${TILE2_ENTITY} - attribute: ${TILE2_CLIMATE_TARGET_TEMP_ATTRIBUTE} - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile2_climate_humidity - entity_id: ${TILE2_ENTITY} - attribute: ${TILE2_CLIMATE_HUMIDITY_ATTRIBUTE} - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile3_climate_current_temperature - entity_id: ${TILE3_ENTITY} - attribute: ${TILE3_CLIMATE_CURRENT_TEMP_ATTRIBUTE} - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile3_climate_target_temperature - entity_id: ${TILE3_ENTITY} - attribute: ${TILE3_CLIMATE_TARGET_TEMP_ATTRIBUTE} - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile3_climate_humidity - entity_id: ${TILE3_ENTITY} - attribute: ${TILE3_CLIMATE_HUMIDITY_ATTRIBUTE} - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile4_climate_current_temperature - entity_id: ${TILE4_ENTITY} - attribute: ${TILE4_CLIMATE_CURRENT_TEMP_ATTRIBUTE} - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile4_climate_target_temperature - entity_id: ${TILE4_ENTITY} - attribute: ${TILE4_CLIMATE_TARGET_TEMP_ATTRIBUTE} - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile4_climate_humidity - entity_id: ${TILE4_ENTITY} - attribute: ${TILE4_CLIMATE_HUMIDITY_ATTRIBUTE} - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile5_climate_current_temperature - entity_id: ${TILE5_ENTITY} - attribute: ${TILE5_CLIMATE_CURRENT_TEMP_ATTRIBUTE} - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile5_climate_target_temperature - entity_id: ${TILE5_ENTITY} - attribute: ${TILE5_CLIMATE_TARGET_TEMP_ATTRIBUTE} - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile5_climate_humidity - entity_id: ${TILE5_ENTITY} - attribute: ${TILE5_CLIMATE_HUMIDITY_ATTRIBUTE} - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile6_climate_current_temperature - entity_id: ${TILE6_ENTITY} - attribute: ${TILE6_CLIMATE_CURRENT_TEMP_ATTRIBUTE} - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile6_climate_target_temperature - entity_id: ${TILE6_ENTITY} - attribute: ${TILE6_CLIMATE_TARGET_TEMP_ATTRIBUTE} - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile6_climate_humidity - entity_id: ${TILE6_ENTITY} - attribute: ${TILE6_CLIMATE_HUMIDITY_ATTRIBUTE} - on_value: { then: [ script.execute: ui_refresh ] } - -binary_sensor: - - platform: status - name: "Node Status" - id: system_status - -# ---------------------------------------------------------- -# Text sensors -# -# smartdisplay_action: -# - Main touch event channel exported to Home Assistant -# - On short tap, publishes one of: -# tile1_press / tile2_press / tile3_press / tile4_press / tile5_press / tile6_press -# - The value is reset shortly after publishing -# -# tile*_preset: -# - Mirrors the Home Assistant preset_mode attribute for each tile entity -# - Used by the UI to display fan presets when applicable -# ---------------------------------------------------------- -text_sensor: - - platform: template - id: smartdisplay_action - name: "SmartDisplay Action" - icon: mdi:gesture-tap-button - - - platform: homeassistant - id: ha_state_tile1 - entity_id: ${TILE1_STATE_ENTITY} - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: ha_state_tile2 - entity_id: ${TILE2_STATE_ENTITY} - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: ha_state_tile3 - entity_id: ${TILE3_STATE_ENTITY} - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: ha_state_tile4 - entity_id: ${TILE4_STATE_ENTITY} - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: ha_state_tile5 - entity_id: ${TILE5_STATE_ENTITY} - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: ha_state_tile6 - entity_id: ${TILE6_STATE_ENTITY} - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: ha_state_tile7 - entity_id: ${TILE7_STATE_ENTITY} - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: ha_state_tile8 - entity_id: ${TILE8_STATE_ENTITY} - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: ha_state_tile9 - entity_id: ${TILE9_STATE_ENTITY} - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: ha_state_tile10 - entity_id: ${TILE10_STATE_ENTITY} - on_value: { then: [ script.execute: ui_refresh ] } - - # 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: [ script.execute: ui_refresh ] } - - # Gebruikt door de slider-overlay om te bepalen of de kleurknop nodig is. - # Deze lamp meldt ['brightness'], dus de knop blijft vanzelf verborgen. - - platform: homeassistant - id: tile7_supported_color_modes - entity_id: ${TILE7_ENTITY} - attribute: supported_color_modes - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile1_preset - entity_id: ${TILE1_ENTITY} - attribute: preset_mode - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile2_preset - entity_id: ${TILE2_ENTITY} - attribute: preset_mode - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile3_preset - entity_id: ${TILE3_ENTITY} - attribute: preset_mode - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile4_preset - entity_id: ${TILE4_ENTITY} - attribute: preset_mode - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile5_preset - entity_id: ${TILE5_ENTITY} - attribute: preset_mode - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile6_preset - entity_id: ${TILE6_ENTITY} - attribute: preset_mode - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile1_supported_color_modes - entity_id: ${TILE1_ENTITY} - attribute: supported_color_modes - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile2_supported_color_modes - entity_id: ${TILE2_ENTITY} - attribute: supported_color_modes - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile3_supported_color_modes - entity_id: ${TILE3_ENTITY} - attribute: supported_color_modes - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile4_supported_color_modes - entity_id: ${TILE4_ENTITY} - attribute: supported_color_modes - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile5_supported_color_modes - entity_id: ${TILE5_ENTITY} - attribute: supported_color_modes - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile6_supported_color_modes - entity_id: ${TILE6_ENTITY} - attribute: supported_color_modes - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile1_climate_mode - entity_id: ${TILE1_ENTITY} - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile2_climate_mode - entity_id: ${TILE2_ENTITY} - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile3_climate_mode - entity_id: ${TILE3_ENTITY} - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile4_climate_mode - entity_id: ${TILE4_ENTITY} - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile5_climate_mode - entity_id: ${TILE5_ENTITY} - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile6_climate_mode - entity_id: ${TILE6_ENTITY} - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile1_climate_supported_modes - entity_id: ${TILE1_ENTITY} - attribute: hvac_modes - - - platform: homeassistant - id: tile2_climate_supported_modes - entity_id: ${TILE2_ENTITY} - attribute: hvac_modes - - - platform: homeassistant - id: tile3_climate_supported_modes - entity_id: ${TILE3_ENTITY} - attribute: hvac_modes - - - platform: homeassistant - id: tile4_climate_supported_modes - entity_id: ${TILE4_ENTITY} - attribute: hvac_modes - - - platform: homeassistant - id: tile5_climate_supported_modes - entity_id: ${TILE5_ENTITY} - attribute: hvac_modes - - - platform: homeassistant - id: tile6_climate_supported_modes - entity_id: ${TILE6_ENTITY} - attribute: hvac_modes - - - platform: homeassistant - id: tile1_cover_state - entity_id: ${TILE1_STATE_ENTITY} - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile2_cover_state - entity_id: ${TILE2_STATE_ENTITY} - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile3_cover_state - entity_id: ${TILE3_STATE_ENTITY} - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile4_cover_state - entity_id: ${TILE4_STATE_ENTITY} - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile5_cover_state - entity_id: ${TILE5_STATE_ENTITY} - on_value: { then: [ script.execute: ui_refresh ] } - - - platform: homeassistant - id: tile6_cover_state - entity_id: ${TILE6_STATE_ENTITY} - on_value: { then: [ script.execute: ui_refresh ] } - -switch: - - platform: restart - name: "Restart" - -# Two SPI buses used by the CYD: -# - lcd: built-in display -# - my_touchscreen: built-in touch controller -spi: - - id: lcd - clk_pin: GPIO14 - mosi_pin: GPIO13 - - -# I2C bus voor de GT911 capacitieve touch (2432S032C). -# scan: true laat ESPHome bij het booten loggen welke adressen er echt zitten. -i2c: - - id: touch_i2c - sda: GPIO33 - scl: GPIO32 - scan: true - -output: - - platform: ledc - pin: GPIO27 - id: gpio_backlight_pwm - - - platform: ledc - id: output_red - pin: GPIO4 - inverted: true - - platform: ledc - id: output_green - pin: GPIO16 - inverted: true - - platform: ledc - id: output_blue - pin: GPIO17 - inverted: true - -light: - - platform: monochromatic - output: gpio_backlight_pwm - name: "Power Display Backlight" - id: back_light - restore_mode: ALWAYS_ON - - - platform: rgb - name: LED - red: output_red - id: led - green: output_green - blue: output_blue - restore_mode: ALWAYS_OFF - -# Touchscreen configuration (GT911, capacitief, via I2C) -# De GT911 levert absolute coordinaten, dus geen calibration nodig. -# LVGL regelt de rotatie (zie `rotation:` in het lvgl blok) en draait -# touch-coordinaten automatisch mee. -touchscreen: - platform: gt911 - id: ts_touch - i2c_id: touch_i2c - # interrupt_pin bewust weggelaten: met een hardware-interrupt zet ESPHome - # het pollen uit, en dan komt er niets binnen. Nu pollt hij elke 50ms. - update_interval: 50ms - - transform: - swap_xy: ${TOUCH_SWAP_XY} - mirror_x: ${TOUCH_MIRROR_X} - mirror_y: ${TOUCH_MIRROR_Y} - - on_touch: - - lambda: |- - id(last_touch_ms) = millis(); - -# Display configuration (ST7789V) -display: - - platform: mipi_spi - id: my_display - spi_id: lcd - model: ST7789V - # Zonder dit draait mipi_spi op zijn standaard van 10MHz, en dan duurt een - # volledig scherm ~123ms: dat zie je van boven naar beneden vullen. - # ESPHome's eigen CYD-modellen gebruiken 40MHz. 80MHz kan meestal ook. - data_rate: 40MHz - cs_pin: 15 - dc_pin: 2 - invert_colors: true - update_interval: never - auto_clear_enabled: false - - # Native panel dimensions (portrait). Do NOT swap these per orientation and - # do NOT add a display transform — rotation is handled by LVGL below. - # ESPHome 2026.6+ rejects a swapped transform here with "Invalid offsets". - dimensions: - width: 240 - height: 320 - -lvgl: - displays: - - my_display - touchscreens: - - touchscreen_id: ts_touch - - # De ESP32-WROOM-32 heeft geen PSRAM; een volledige framebuffer (320x240x2 - # = 153600 bytes) past niet in DRAM. 25% is de gebruikelijke waarde. - # 12% in plaats van 25%: laat ~20KB extra aaneengesloten geheugen vrij, - # en LVGL schaalt de stroken van een ARGB-laag mee met de beschikbare - # tekenruimte, dus ook de gevraagde allocaties worden kleiner. - buffer_size: 12% - - # Directe lokale feedback op de HELE UI, niet alleen op de tegels. - # Een thema zet de stijl per widget-type, dus dit dekt in een klap alle - # knoppen, tabs, sliders en de thermostaatring. Niet-klikbare widgets - # komen nooit in de pressed-state, dus die blijven onaangeroerd. - theme: - obj: - pressed: - # bg_opa en NIET opa: `opa` op widget-niveau dwingt LVGL het element - # naar een aparte laag te renderen, en die laag kost tientallen KB's - # per ingedrukte widget. Dat liep hier het geheugen uit. - bg_opa: 45% - slider: - knob: - pressed: - bg_opa: 65% - arc: - knob: - pressed: - bg_opa: 65% - - # LVGL owns the rotation (display + touch) since ESPHome 2026.4. - rotation: ${LVGL_ROTATION} - - style_definitions: - # Achtergrond: twee-stops verticale gradient in plaats van de 320x240 - # RGB565-wallpaper. Scheelt ~150 KB flash en een image-blit per hertekening, - # wat je direct terugziet in de vloeiendheid van het scrollen. - - id: style_bg_grad - bg_color: ${BG_TOP_COLOR} - bg_grad_color: ${BG_BOTTOM_COLOR} - bg_grad_dir: VER - bg_opa: COVER - border_width: 0 - radius: 0 - - - id: style_tile - bg_color: 0xFFFFFF - bg_opa: 95% - radius: 18 - border_width: 0 - pad_left: 8 - pad_right: 8 - pad_top: 8 - pad_bottom: 8 - shadow_width: 0 - shadow_opa: 40% - - - id: style_tile_disabled - bg_color: 0x939391 - bg_opa: 95% - radius: 18 - border_width: 0 - pad_left: 8 - pad_right: 8 - pad_top: 8 - pad_bottom: 8 - shadow_width: 0 - shadow_opa: 25% - - - id: style_icon_circle - bg_color: 0xFEC600 - bg_opa: COVER - radius: 999 - border_width: 0 - - - id: style_icon_circle_disabled - bg_color: 0x7B7B6F - bg_opa: 95% - radius: 999 - border_width: 0 - - - id: style_room - text_font: headline - text_color: 0xFFFFFF - text_opa: 100% - - - id: style_time - text_font: time_label - text_color: 0xFFFFFF - text_opa: 100% - - - id: style_title - text_font: label - text_color: 0x000000 - text_opa: 100% - - - id: style_value - text_font: sublabel - text_color: 0x7A7A7C - text_opa: 100% - - - id: style_value_big - text_font: sublabel_big - text_color: 0xFFFFFF - text_opa: 80% - - - id: style_title_disabled - text_font: label - text_color: 0xFFFFFF - text_opa: 100% - - - id: style_value_disabled - text_font: sublabel - text_color: 0xD9D9D9 - text_opa: 100% - - pages: - - id: home_page - styles: style_bg_grad - scrollable: false - scrollbar_mode: 'OFF' - widgets: - - label: - id: lbl_room - x: 11 - y: ${HEADER_Y} - text: "${ROOM_NAME}" - styles: style_room - - - label: - id: lbl_time - align: TOP_RIGHT - x: -11 - y: ${HEADER_Y} - text: "" - styles: style_time - - # ---------- SCROLLBAAR TEGELRASTER ---------- - # De pagina zelf scrollt niet: wallpaper-gradient, kamernaam en klok - # blijven staan. Alleen deze transparante container schuift. - # - # bg_opa: TRANSP is NIET hetzelfde als widget-`opa`: het maakt geen - # aparte renderlaag aan, dus het kost geen extra heap (zie de comment - # bij `theme:` hierboven). - # - # LVGL leidt de scroll-lengte zelf af uit de posities van de tegels; - # er is dus geen handmatige contenthoogte. Zodra een sleep de - # scroll-limiet (10 px) passeert annuleert LVGL de klik en de - # long-press, dus tikken en scrollen bijten elkaar niet. - - obj: - id: tile_scroll - x: 0 - y: ${SCROLL_Y} - width: ${DISPLAY_W} - height: ${SCROLL_H} - bg_opa: TRANSP - border_width: 0 - pad_left: 0 - pad_right: 0 - pad_top: 0 - pad_bottom: 8 - scrollable: true - scroll_dir: VER - scrollbar_mode: 'OFF' - # Geen elastisch doorveren aan de randen: dat kost extra - # hertekeningen op een scherm dat toch al niet vloeiend scrollt. - scroll_elastic: false - scroll_momentum: true - # Klikt netjes per rij in. Verbergt de lage framerate een stuk - # beter dan vrij scrollen. Zet op NONE voor vrij scrollen. - scroll_snap_y: START - widgets: - # ---------- TILE 1 ---------- - - obj: - id: tile1 - x: ${GRID_COL1_X} - y: ${GRID_ROW1_Y} - width: ${TILE_W} - height: ${TILE_H} - styles: style_tile - scrollable: false - scrollbar_mode: 'OFF' - # Directe lokale feedback: LVGL past dit toe zodra de vinger het - # glas raakt, in dezelfde frame. Onafhankelijk van Home Assistant. - pressed: - bg_opa: 45% - widgets: - - obj: - id: tile1_icon_circle - x: ${TILE_ICON_X} - y: ${TILE_ICON_Y} - width: 36 - height: 36 - styles: style_icon_circle - clickable: false - scrollable: false - scrollbar_mode: 'OFF' - widgets: - - label: - id: tile1_icon_lbl - align: CENTER - text_font: materialdesign_icons - text_color: 0xFFFFFF - text: "${TILE1_ICON}" - clickable: false - - label: - id: t1_title - x: ${TILE_TITLE_X} - y: ${TILE_TITLE_Y} - text: "${TILE1_TITLE}" - styles: style_title - clickable: false - - label: - id: t1_value - x: ${TILE_VALUE_X} - y: ${TILE_VALUE_Y} - text: "—" - styles: style_value - clickable: false - on_short_click: - - script.execute: { id: tile1_ack, is_long: false } - - script.execute: { id: publish_action, action: "tile1_press" } - - script.execute: - id: do_tile_action - tile_type: "${TILE1_TYPE}" - tap_action: "${TILE1_TAP_ACTION}" - service: "${TILE1_TAP_SERVICE}" - entity_id: "${TILE1_ENTITY}" - param_key: "${TILE1_TAP_PARAM_KEY}" - param_val: "${TILE1_TAP_PARAM_VAL}" - was_on: !lambda |- - if (std::string("${TILE1_TYPE}") == "cover") - return id(tile1_cover_state).state != "closed"; - return std::string(id(ha_state_tile1).state.c_str()) == "on"; - on_long_press: - - script.execute: { id: tile1_ack, is_long: true } - - script.execute: { id: publish_action, action: "tile1_long_press" } - - if: - condition: - lambda: "return std::string(\"${TILE1_LONGPRESS}\") == \"action\";" - then: - - script.execute: - id: do_tile_action - tile_type: !lambda |- - std::string t = "${TILE1_LONGPRESS_ACTION_TYPE}"; - return t.empty() ? std::string("${TILE1_TYPE}") : t; - tap_action: "activate" - service: "${TILE1_LONGPRESS_ACTION_SERVICE}" - entity_id: "${TILE1_LONGPRESS_ACTION}" - param_key: "" - param_val: "" - was_on: false - else: - - if: - condition: - lambda: "return std::string(\"${TILE1_LONGPRESS}\") == \"slider\";" - then: - - script.execute: - id: open_value_overlay - title: "${TILE1_TITLE}" - entity: "${TILE1_ENTITY}" - tile_type: "${TILE1_TYPE}" - longpress_mode: "${TILE1_LONGPRESS_SLIDER}" - was_on: !lambda |- - if (std::string("${TILE1_TYPE}") == "cover") - return id(tile1_cover_state).state != "closed"; - return std::string(id(ha_state_tile1).state.c_str()) == "on"; - off_value: !lambda "return atoi(\"${TILE1_LONGPRESS_OFF_VALUE}\");" - brightness_value: !lambda "return id(tile1_brightness).state;" - percentage_value: !lambda "return id(tile1_percentage).state;" - position_value: !lambda "return id(tile1_cover_position).state;" - icon_text: "${TILE1_ICON}" - slider_bg_color: ${TILE1_BG_DISABLED_COLOR} - slider_fill_color: ${TILE1_CIRCLE_ACTIVE_COLOR} - slider_icon_color: ${TILE1_ICON_ACTIVE_COLOR} - label_off: "${TILE1_LABEL_OFF}" - label_on: "${TILE1_LABEL_ON}" - supported_color_modes: !lambda "return std::string(id(tile1_supported_color_modes).state.c_str());" - climate_current_temperature: !lambda "return id(tile1_climate_current_temperature).state;" - climate_target_temperature: !lambda "return id(tile1_climate_target_temperature).state;" - climate_humidity: !lambda "return id(tile1_climate_humidity).state;" - climate_mode: !lambda "return std::string(id(tile1_climate_mode).state.c_str());" - climate_supported_modes: !lambda "return std::string(id(tile1_climate_supported_modes).state.c_str());" - climate_min_temp: "${TILE1_CLIMATE_MIN_TEMP}" - climate_max_temp: "${TILE1_CLIMATE_MAX_TEMP}" - climate_step: "${TILE1_CLIMATE_STEP}" - # Long-press modus "vacuum": opent de stofzuigerbediening. - # Valt buiten de action/slider-keten, dus die blijft ongewijzigd. - - if: - condition: - lambda: "return std::string(\"${TILE1_LONGPRESS}\") == \"vacuum\";" - then: - - script.execute: - id: open_vacuum_overlay - title: "${TILE1_TITLE}" - entity: "${TILE1_ENTITY}" - - # ---------- TILE 2 ---------- - - obj: - id: tile2 - x: ${GRID_COL2_X} - y: ${GRID_ROW1_Y} - width: ${TILE_W} - height: ${TILE_H} - styles: style_tile - scrollable: false - scrollbar_mode: 'OFF' - # Directe lokale feedback: LVGL past dit toe zodra de vinger het - # glas raakt, in dezelfde frame. Onafhankelijk van Home Assistant. - pressed: - bg_opa: 45% - widgets: - - obj: - id: tile2_icon_circle - x: ${TILE_ICON_X} - y: ${TILE_ICON_Y} - width: 36 - height: 36 - styles: style_icon_circle - clickable: false - scrollable: false - scrollbar_mode: 'OFF' - widgets: - - label: - id: tile2_icon_lbl - align: CENTER - text_font: materialdesign_icons - text_color: 0xFFFFFF - text: "${TILE2_ICON}" - clickable: false - - label: - id: t2_title - x: ${TILE_TITLE_X} - y: ${TILE_TITLE_Y} - text: "${TILE2_TITLE}" - styles: style_title - clickable: false - - label: - id: t2_value - x: ${TILE_VALUE_X} - y: ${TILE_VALUE_Y} - text: "—" - styles: style_value - clickable: false - on_short_click: - - script.execute: { id: tile2_ack, is_long: false } - - script.execute: { id: publish_action, action: "tile2_press" } - - script.execute: - id: do_tile_action - tile_type: "${TILE2_TYPE}" - tap_action: "${TILE2_TAP_ACTION}" - service: "${TILE2_TAP_SERVICE}" - entity_id: "${TILE2_ENTITY}" - param_key: "${TILE2_TAP_PARAM_KEY}" - param_val: "${TILE2_TAP_PARAM_VAL}" - was_on: !lambda |- - if (std::string("${TILE2_TYPE}") == "cover") - return id(tile2_cover_state).state != "closed"; - return std::string(id(ha_state_tile2).state.c_str()) == "on"; - on_long_press: - - script.execute: { id: tile2_ack, is_long: true } - - script.execute: { id: publish_action, action: "tile2_long_press" } - - if: - condition: - lambda: "return std::string(\"${TILE2_LONGPRESS}\") == \"action\";" - then: - - script.execute: - id: do_tile_action - tile_type: !lambda |- - std::string t = "${TILE2_LONGPRESS_ACTION_TYPE}"; - return t.empty() ? std::string("${TILE2_TYPE}") : t; - tap_action: "activate" - service: "${TILE2_LONGPRESS_ACTION_SERVICE}" - entity_id: "${TILE2_LONGPRESS_ACTION}" - param_key: "" - param_val: "" - was_on: false - else: - - if: - condition: - lambda: "return std::string(\"${TILE2_LONGPRESS}\") == \"slider\";" - then: - - script.execute: - id: open_value_overlay - title: "${TILE2_TITLE}" - entity: "${TILE2_ENTITY}" - tile_type: "${TILE2_TYPE}" - longpress_mode: "${TILE2_LONGPRESS_SLIDER}" - was_on: !lambda |- - if (std::string("${TILE2_TYPE}") == "cover") - return id(tile2_cover_state).state != "closed"; - return std::string(id(ha_state_tile2).state.c_str()) == "on"; - off_value: !lambda "return atoi(\"${TILE2_LONGPRESS_OFF_VALUE}\");" - brightness_value: !lambda "return id(tile2_brightness).state;" - percentage_value: !lambda "return id(tile2_percentage).state;" - position_value: !lambda "return id(tile2_cover_position).state;" - icon_text: "${TILE2_ICON}" - slider_bg_color: ${TILE2_BG_DISABLED_COLOR} - slider_fill_color: ${TILE2_CIRCLE_ACTIVE_COLOR} - slider_icon_color: ${TILE2_ICON_ACTIVE_COLOR} - label_off: "${TILE2_LABEL_OFF}" - label_on: "${TILE2_LABEL_ON}" - supported_color_modes: !lambda "return std::string(id(tile2_supported_color_modes).state.c_str());" - climate_current_temperature: !lambda "return id(tile2_climate_current_temperature).state;" - climate_target_temperature: !lambda "return id(tile2_climate_target_temperature).state;" - climate_humidity: !lambda "return id(tile2_climate_humidity).state;" - climate_mode: !lambda "return std::string(id(tile2_climate_mode).state.c_str());" - climate_supported_modes: !lambda "return std::string(id(tile2_climate_supported_modes).state.c_str());" - climate_min_temp: "${TILE2_CLIMATE_MIN_TEMP}" - climate_max_temp: "${TILE2_CLIMATE_MAX_TEMP}" - climate_step: "${TILE2_CLIMATE_STEP}" - # Long-press modus "vacuum": opent de stofzuigerbediening. - # Valt buiten de action/slider-keten, dus die blijft ongewijzigd. - - if: - condition: - lambda: "return std::string(\"${TILE2_LONGPRESS}\") == \"vacuum\";" - then: - - script.execute: - id: open_vacuum_overlay - title: "${TILE2_TITLE}" - entity: "${TILE2_ENTITY}" - - # ---------- TILE 3 ---------- - - obj: - id: tile3 - x: ${GRID_COL1_X} - y: ${GRID_ROW2_Y} - width: ${TILE_W} - height: ${TILE_H} - styles: style_tile - scrollable: false - scrollbar_mode: 'OFF' - # Directe lokale feedback: LVGL past dit toe zodra de vinger het - # glas raakt, in dezelfde frame. Onafhankelijk van Home Assistant. - pressed: - bg_opa: 45% - widgets: - - obj: - id: tile3_icon_circle - x: ${TILE_ICON_X} - y: ${TILE_ICON_Y} - width: 36 - height: 36 - styles: style_icon_circle - clickable: false - scrollable: false - scrollbar_mode: 'OFF' - widgets: - - label: - id: tile3_icon_lbl - align: CENTER - text_font: materialdesign_icons - text_color: 0xFFFFFF - text: "${TILE3_ICON}" - clickable: false - - label: - id: t3_title - x: ${TILE_TITLE_X} - y: ${TILE_TITLE_Y} - text: "${TILE3_TITLE}" - styles: style_title - clickable: false - - label: - id: t3_value - x: ${TILE_VALUE_X} - y: ${TILE_VALUE_Y} - text: "—" - styles: style_value - clickable: false - on_short_click: - - script.execute: { id: tile3_ack, is_long: false } - - script.execute: { id: publish_action, action: "tile3_press" } - - script.execute: - id: do_tile_action - tile_type: "${TILE3_TYPE}" - tap_action: "${TILE3_TAP_ACTION}" - service: "${TILE3_TAP_SERVICE}" - entity_id: "${TILE3_ENTITY}" - param_key: "${TILE3_TAP_PARAM_KEY}" - param_val: "${TILE3_TAP_PARAM_VAL}" - was_on: !lambda |- - if (std::string("${TILE3_TYPE}") == "cover") - return id(tile3_cover_state).state != "closed"; - return std::string(id(ha_state_tile3).state.c_str()) == "on"; - on_long_press: - - script.execute: { id: tile3_ack, is_long: true } - - script.execute: { id: publish_action, action: "tile3_long_press" } - - if: - condition: - lambda: "return std::string(\"${TILE3_LONGPRESS}\") == \"action\";" - then: - - script.execute: - id: do_tile_action - tile_type: !lambda |- - std::string t = "${TILE3_LONGPRESS_ACTION_TYPE}"; - return t.empty() ? std::string("${TILE3_TYPE}") : t; - tap_action: "activate" - service: "${TILE3_LONGPRESS_ACTION_SERVICE}" - entity_id: "${TILE3_LONGPRESS_ACTION}" - param_key: "" - param_val: "" - was_on: false - else: - - if: - condition: - lambda: "return std::string(\"${TILE3_LONGPRESS}\") == \"slider\";" - then: - - script.execute: - id: open_value_overlay - title: "${TILE3_TITLE}" - entity: "${TILE3_ENTITY}" - tile_type: "${TILE3_TYPE}" - longpress_mode: "${TILE3_LONGPRESS_SLIDER}" - was_on: !lambda |- - if (std::string("${TILE3_TYPE}") == "cover") - return id(tile3_cover_state).state != "closed"; - return std::string(id(ha_state_tile3).state.c_str()) == "on"; - off_value: !lambda "return atoi(\"${TILE3_LONGPRESS_OFF_VALUE}\");" - brightness_value: !lambda "return id(tile3_brightness).state;" - percentage_value: !lambda "return id(tile3_percentage).state;" - position_value: !lambda "return id(tile3_cover_position).state;" - icon_text: "${TILE3_ICON}" - slider_bg_color: ${TILE3_BG_DISABLED_COLOR} - slider_fill_color: ${TILE3_CIRCLE_ACTIVE_COLOR} - slider_icon_color: ${TILE3_ICON_ACTIVE_COLOR} - label_off: "${TILE3_LABEL_OFF}" - label_on: "${TILE3_LABEL_ON}" - supported_color_modes: !lambda "return std::string(id(tile3_supported_color_modes).state.c_str());" - climate_current_temperature: !lambda "return id(tile3_climate_current_temperature).state;" - climate_target_temperature: !lambda "return id(tile3_climate_target_temperature).state;" - climate_humidity: !lambda "return id(tile3_climate_humidity).state;" - climate_mode: !lambda "return std::string(id(tile3_climate_mode).state.c_str());" - climate_supported_modes: !lambda "return std::string(id(tile3_climate_supported_modes).state.c_str());" - climate_min_temp: "${TILE3_CLIMATE_MIN_TEMP}" - climate_max_temp: "${TILE3_CLIMATE_MAX_TEMP}" - climate_step: "${TILE3_CLIMATE_STEP}" - # Long-press modus "vacuum": opent de stofzuigerbediening. - # Valt buiten de action/slider-keten, dus die blijft ongewijzigd. - - if: - condition: - lambda: "return std::string(\"${TILE3_LONGPRESS}\") == \"vacuum\";" - then: - - script.execute: - id: open_vacuum_overlay - title: "${TILE3_TITLE}" - entity: "${TILE3_ENTITY}" - - # ---------- TILE 4 ---------- - - obj: - id: tile4 - x: ${GRID_COL2_X} - y: ${GRID_ROW2_Y} - width: ${TILE_W} - height: ${TILE_H} - styles: style_tile - scrollable: false - scrollbar_mode: 'OFF' - # Directe lokale feedback: LVGL past dit toe zodra de vinger het - # glas raakt, in dezelfde frame. Onafhankelijk van Home Assistant. - pressed: - bg_opa: 45% - widgets: - - obj: - id: tile4_icon_circle - x: ${TILE_ICON_X} - y: ${TILE_ICON_Y} - width: 36 - height: 36 - styles: style_icon_circle - clickable: false - scrollable: false - scrollbar_mode: 'OFF' - widgets: - - label: - id: tile4_icon_lbl - align: CENTER - text_font: materialdesign_icons - text_color: 0xFFFFFF - text: "${TILE4_ICON}" - clickable: false - - label: - id: t4_title - x: ${TILE_TITLE_X} - y: ${TILE_TITLE_Y} - text: "${TILE4_TITLE}" - styles: style_title - clickable: false - - label: - id: t4_value - x: ${TILE_VALUE_X} - y: ${TILE_VALUE_Y} - text: "—" - styles: style_value - clickable: false - on_short_click: - - script.execute: { id: tile4_ack, is_long: false } - - script.execute: { id: publish_action, action: "tile4_press" } - - script.execute: - id: do_tile_action - tile_type: "${TILE4_TYPE}" - tap_action: "${TILE4_TAP_ACTION}" - service: "${TILE4_TAP_SERVICE}" - entity_id: "${TILE4_ENTITY}" - param_key: "${TILE4_TAP_PARAM_KEY}" - param_val: "${TILE4_TAP_PARAM_VAL}" - was_on: !lambda |- - if (std::string("${TILE4_TYPE}") == "cover") - return id(tile4_cover_state).state != "closed"; - return std::string(id(ha_state_tile4).state.c_str()) == "on"; - on_long_press: - - script.execute: { id: tile4_ack, is_long: true } - - script.execute: { id: publish_action, action: "tile4_long_press" } - - if: - condition: - lambda: "return std::string(\"${TILE4_LONGPRESS}\") == \"action\";" - then: - - script.execute: - id: do_tile_action - tile_type: !lambda |- - std::string t = "${TILE4_LONGPRESS_ACTION_TYPE}"; - return t.empty() ? std::string("${TILE4_TYPE}") : t; - tap_action: "activate" - service: "${TILE4_LONGPRESS_ACTION_SERVICE}" - entity_id: "${TILE4_LONGPRESS_ACTION}" - param_key: "" - param_val: "" - was_on: false - else: - - if: - condition: - lambda: "return std::string(\"${TILE4_LONGPRESS}\") == \"slider\";" - then: - - script.execute: - id: open_value_overlay - title: "${TILE4_TITLE}" - entity: "${TILE4_ENTITY}" - tile_type: "${TILE4_TYPE}" - longpress_mode: "${TILE4_LONGPRESS_SLIDER}" - was_on: !lambda |- - if (std::string("${TILE4_TYPE}") == "cover") - return id(tile4_cover_state).state != "closed"; - return std::string(id(ha_state_tile4).state.c_str()) == "on"; - off_value: !lambda "return atoi(\"${TILE4_LONGPRESS_OFF_VALUE}\");" - brightness_value: !lambda "return id(tile4_brightness).state;" - percentage_value: !lambda "return id(tile4_percentage).state;" - position_value: !lambda "return id(tile4_cover_position).state;" - icon_text: "${TILE4_ICON}" - slider_bg_color: ${TILE4_BG_DISABLED_COLOR} - slider_fill_color: ${TILE4_CIRCLE_ACTIVE_COLOR} - slider_icon_color: ${TILE4_ICON_ACTIVE_COLOR} - label_off: "${TILE4_LABEL_OFF}" - label_on: "${TILE4_LABEL_ON}" - supported_color_modes: !lambda "return std::string(id(tile4_supported_color_modes).state.c_str());" - climate_current_temperature: !lambda "return id(tile4_climate_current_temperature).state;" - climate_target_temperature: !lambda "return id(tile4_climate_target_temperature).state;" - climate_humidity: !lambda "return id(tile4_climate_humidity).state;" - climate_mode: !lambda "return std::string(id(tile4_climate_mode).state.c_str());" - climate_supported_modes: !lambda "return std::string(id(tile4_climate_supported_modes).state.c_str());" - climate_min_temp: "${TILE4_CLIMATE_MIN_TEMP}" - climate_max_temp: "${TILE4_CLIMATE_MAX_TEMP}" - climate_step: "${TILE4_CLIMATE_STEP}" - # Long-press modus "vacuum": opent de stofzuigerbediening. - # Valt buiten de action/slider-keten, dus die blijft ongewijzigd. - - if: - condition: - lambda: "return std::string(\"${TILE4_LONGPRESS}\") == \"vacuum\";" - then: - - script.execute: - id: open_vacuum_overlay - title: "${TILE4_TITLE}" - entity: "${TILE4_ENTITY}" - - # ---------- TILE 5 ---------- - - obj: - id: tile5 - x: ${GRID_COL1_X} - y: ${GRID_ROW3_Y} - width: ${TILE_W} - height: ${TILE_H} - styles: style_tile - scrollable: false - scrollbar_mode: 'OFF' - # Directe lokale feedback: LVGL past dit toe zodra de vinger het - # glas raakt, in dezelfde frame. Onafhankelijk van Home Assistant. - pressed: - bg_opa: 45% - widgets: - - obj: - id: tile5_icon_circle - x: ${TILE_ICON_X} - y: ${TILE_ICON_Y} - width: 36 - height: 36 - styles: style_icon_circle - clickable: false - scrollable: false - scrollbar_mode: 'OFF' - widgets: - - label: - id: tile5_icon_lbl - align: CENTER - text_font: materialdesign_icons - text_color: 0xFFFFFF - text: "${TILE5_ICON}" - clickable: false - - label: - id: t5_title - x: ${TILE_TITLE_X} - y: ${TILE_TITLE_Y} - text: "${TILE5_TITLE}" - styles: style_title - clickable: false - - label: - id: t5_value - x: ${TILE_VALUE_X} - y: ${TILE_VALUE_Y} - text: "—" - styles: style_value - clickable: false - on_short_click: - - script.execute: { id: tile5_ack, is_long: false } - - script.execute: { id: publish_action, action: "tile5_press" } - - script.execute: - id: do_tile_action - tile_type: "${TILE5_TYPE}" - tap_action: "${TILE5_TAP_ACTION}" - service: "${TILE5_TAP_SERVICE}" - entity_id: "${TILE5_ENTITY}" - param_key: "${TILE5_TAP_PARAM_KEY}" - param_val: "${TILE5_TAP_PARAM_VAL}" - was_on: !lambda |- - if (std::string("${TILE5_TYPE}") == "cover") - return id(tile5_cover_state).state != "closed"; - return std::string(id(ha_state_tile5).state.c_str()) == "on"; - on_long_press: - - script.execute: { id: tile5_ack, is_long: true } - - script.execute: { id: publish_action, action: "tile5_long_press" } - - if: - condition: - lambda: "return std::string(\"${TILE5_LONGPRESS}\") == \"action\";" - then: - - script.execute: - id: do_tile_action - tile_type: !lambda |- - std::string t = "${TILE5_LONGPRESS_ACTION_TYPE}"; - return t.empty() ? std::string("${TILE5_TYPE}") : t; - tap_action: "activate" - service: "${TILE5_LONGPRESS_ACTION_SERVICE}" - entity_id: "${TILE5_LONGPRESS_ACTION}" - param_key: "" - param_val: "" - was_on: false - else: - - if: - condition: - lambda: "return std::string(\"${TILE5_LONGPRESS}\") == \"slider\";" - then: - - script.execute: - id: open_value_overlay - title: "${TILE5_TITLE}" - entity: "${TILE5_ENTITY}" - tile_type: "${TILE5_TYPE}" - longpress_mode: "${TILE5_LONGPRESS_SLIDER}" - was_on: !lambda |- - if (std::string("${TILE5_TYPE}") == "cover") - return id(tile5_cover_state).state != "closed"; - return std::string(id(ha_state_tile5).state.c_str()) == "on"; - off_value: !lambda "return atoi(\"${TILE5_LONGPRESS_OFF_VALUE}\");" - brightness_value: !lambda "return id(tile5_brightness).state;" - percentage_value: !lambda "return id(tile5_percentage).state;" - position_value: !lambda "return id(tile5_cover_position).state;" - icon_text: "${TILE5_ICON}" - slider_bg_color: ${TILE5_BG_DISABLED_COLOR} - slider_fill_color: ${TILE5_CIRCLE_ACTIVE_COLOR} - slider_icon_color: ${TILE5_ICON_ACTIVE_COLOR} - label_off: "${TILE5_LABEL_OFF}" - label_on: "${TILE5_LABEL_ON}" - supported_color_modes: !lambda "return std::string(id(tile5_supported_color_modes).state.c_str());" - climate_current_temperature: !lambda "return id(tile5_climate_current_temperature).state;" - climate_target_temperature: !lambda "return id(tile5_climate_target_temperature).state;" - climate_humidity: !lambda "return id(tile5_climate_humidity).state;" - climate_mode: !lambda "return std::string(id(tile5_climate_mode).state.c_str());" - climate_supported_modes: !lambda "return std::string(id(tile5_climate_supported_modes).state.c_str());" - climate_min_temp: "${TILE5_CLIMATE_MIN_TEMP}" - climate_max_temp: "${TILE5_CLIMATE_MAX_TEMP}" - climate_step: "${TILE5_CLIMATE_STEP}" - # Long-press modus "vacuum": opent de stofzuigerbediening. - # Valt buiten de action/slider-keten, dus die blijft ongewijzigd. - - if: - condition: - lambda: "return std::string(\"${TILE5_LONGPRESS}\") == \"vacuum\";" - then: - - script.execute: - id: open_vacuum_overlay - title: "${TILE5_TITLE}" - entity: "${TILE5_ENTITY}" - - # ---------- TILE 6 ---------- - - obj: - id: tile6 - x: ${GRID_COL2_X} - y: ${GRID_ROW3_Y} - width: ${TILE_W} - height: ${TILE_H} - styles: style_tile - scrollable: false - scrollbar_mode: 'OFF' - # Directe lokale feedback: LVGL past dit toe zodra de vinger het - # glas raakt, in dezelfde frame. Onafhankelijk van Home Assistant. - pressed: - bg_opa: 45% - widgets: - - obj: - id: tile6_icon_circle - x: ${TILE_ICON_X} - y: ${TILE_ICON_Y} - width: 36 - height: 36 - styles: style_icon_circle - clickable: false - scrollable: false - scrollbar_mode: 'OFF' - widgets: - - label: - id: tile6_icon_lbl - align: CENTER - text_font: materialdesign_icons - text_color: 0xFFFFFF - text: "${TILE6_ICON}" - clickable: false - - label: - id: t6_title - x: ${TILE_TITLE_X} - y: ${TILE_TITLE_Y} - text: "${TILE6_TITLE}" - styles: style_title - clickable: false - - label: - id: t6_value - x: ${TILE_VALUE_X} - y: ${TILE_VALUE_Y} - text: "—" - styles: style_value - clickable: false - on_short_click: - - script.execute: { id: tile6_ack, is_long: false } - - script.execute: { id: publish_action, action: "tile6_press" } - - script.execute: - id: do_tile_action - tile_type: "${TILE6_TYPE}" - tap_action: "${TILE6_TAP_ACTION}" - service: "${TILE6_TAP_SERVICE}" - entity_id: "${TILE6_ENTITY}" - param_key: "${TILE6_TAP_PARAM_KEY}" - param_val: "${TILE6_TAP_PARAM_VAL}" - was_on: !lambda |- - if (std::string("${TILE6_TYPE}") == "cover") - return id(tile6_cover_state).state != "closed"; - return std::string(id(ha_state_tile6).state.c_str()) == "on"; - on_long_press: - - script.execute: { id: tile6_ack, is_long: true } - - script.execute: { id: publish_action, action: "tile6_long_press" } - - if: - condition: - lambda: "return std::string(\"${TILE6_LONGPRESS}\") == \"action\";" - then: - - script.execute: - id: do_tile_action - tile_type: !lambda |- - std::string t = "${TILE6_LONGPRESS_ACTION_TYPE}"; - return t.empty() ? std::string("${TILE6_TYPE}") : t; - tap_action: "activate" - service: "${TILE6_LONGPRESS_ACTION_SERVICE}" - entity_id: "${TILE6_LONGPRESS_ACTION}" - param_key: "" - param_val: "" - was_on: false - else: - - if: - condition: - lambda: "return std::string(\"${TILE6_LONGPRESS}\") == \"slider\";" - then: - - script.execute: - id: open_value_overlay - title: "${TILE6_TITLE}" - entity: "${TILE6_ENTITY}" - tile_type: "${TILE6_TYPE}" - longpress_mode: "${TILE6_LONGPRESS_SLIDER}" - was_on: !lambda |- - if (std::string("${TILE6_TYPE}") == "cover") - return id(tile6_cover_state).state != "closed"; - return std::string(id(ha_state_tile6).state.c_str()) == "on"; - off_value: !lambda "return atoi(\"${TILE6_LONGPRESS_OFF_VALUE}\");" - brightness_value: !lambda "return id(tile6_brightness).state;" - percentage_value: !lambda "return id(tile6_percentage).state;" - position_value: !lambda "return id(tile6_cover_position).state;" - icon_text: "${TILE6_ICON}" - slider_bg_color: ${TILE6_BG_DISABLED_COLOR} - slider_fill_color: ${TILE6_CIRCLE_ACTIVE_COLOR} - slider_icon_color: ${TILE6_ICON_ACTIVE_COLOR} - label_off: "${TILE6_LABEL_OFF}" - label_on: "${TILE6_LABEL_ON}" - supported_color_modes: !lambda "return std::string(id(tile6_supported_color_modes).state.c_str());" - climate_current_temperature: !lambda "return id(tile6_climate_current_temperature).state;" - climate_target_temperature: !lambda "return id(tile6_climate_target_temperature).state;" - climate_humidity: !lambda "return id(tile6_climate_humidity).state;" - climate_mode: !lambda "return std::string(id(tile6_climate_mode).state.c_str());" - climate_supported_modes: !lambda "return std::string(id(tile6_climate_supported_modes).state.c_str());" - climate_min_temp: "${TILE6_CLIMATE_MIN_TEMP}" - climate_max_temp: "${TILE6_CLIMATE_MAX_TEMP}" - climate_step: "${TILE6_CLIMATE_STEP}" - # Long-press modus "vacuum": opent de stofzuigerbediening. - # Valt buiten de action/slider-keten, dus die blijft ongewijzigd. - - if: - condition: - lambda: "return std::string(\"${TILE6_LONGPRESS}\") == \"vacuum\";" - then: - - script.execute: - id: open_vacuum_overlay - title: "${TILE6_TITLE}" - entity: "${TILE6_ENTITY}" - - # ---------- TILE 7 — Tafellamp ---------- - # Alleen-helderheid lamp: er zijn bewust geen percentage/cover/ - # climate-sensors voor deze tegel aangemaakt, die zouden alleen - # maar HA-subscriptions en heap kosten. De overlay krijgt daarom - # NAN / "" voor de velden die een light nooit gebruikt. - - obj: - id: tile7 - x: ${GRID_COL1_X} - y: ${GRID_ROW4_Y} - width: ${TILE_W} - height: ${TILE_H} - styles: style_tile - scrollable: false - scrollbar_mode: 'OFF' - pressed: - bg_opa: 45% - widgets: - - obj: - id: tile7_icon_circle - x: ${TILE_ICON_X} - y: ${TILE_ICON_Y} - width: 36 - height: 36 - styles: style_icon_circle - clickable: false - scrollable: false - scrollbar_mode: 'OFF' - widgets: - - label: - id: tile7_icon_lbl - align: CENTER - text_font: materialdesign_icons - text_color: 0xFFFFFF - text: "${TILE7_ICON}" - clickable: false - - label: - id: t7_title - x: ${TILE_TITLE_X} - y: ${TILE_TITLE_Y} - text: "${TILE7_TITLE}" - styles: style_title - clickable: false - - label: - id: t7_value - x: ${TILE_VALUE_X} - y: ${TILE_VALUE_Y} - text: "—" - styles: style_value - clickable: false - on_short_click: - - script.execute: { id: tile7_ack, is_long: false } - - script.execute: { id: publish_action, action: "tile7_press" } - - script.execute: - id: do_tile_action - tile_type: "${TILE7_TYPE}" - tap_action: "${TILE7_TAP_ACTION}" - service: "${TILE7_TAP_SERVICE}" - entity_id: "${TILE7_ENTITY}" - param_key: "${TILE7_TAP_PARAM_KEY}" - param_val: "${TILE7_TAP_PARAM_VAL}" - was_on: !lambda "return std::string(id(ha_state_tile7).state.c_str()) == \"on\";" - on_long_press: - - script.execute: { id: tile7_ack, is_long: true } - - script.execute: { id: publish_action, action: "tile7_long_press" } - - if: - condition: - lambda: "return std::string(\"${TILE7_LONGPRESS}\") == \"action\";" - then: - - script.execute: - id: do_tile_action - tile_type: !lambda |- - std::string t = "${TILE7_LONGPRESS_ACTION_TYPE}"; - return t.empty() ? std::string("${TILE7_TYPE}") : t; - tap_action: "activate" - service: "${TILE7_LONGPRESS_ACTION_SERVICE}" - entity_id: "${TILE7_LONGPRESS_ACTION}" - param_key: "" - param_val: "" - was_on: false - else: - - if: - condition: - lambda: "return std::string(\"${TILE7_LONGPRESS}\") == \"slider\";" - then: - - script.execute: - id: open_value_overlay - title: "${TILE7_TITLE}" - entity: "${TILE7_ENTITY}" - tile_type: "${TILE7_TYPE}" - longpress_mode: "${TILE7_LONGPRESS_SLIDER}" - was_on: !lambda "return std::string(id(ha_state_tile7).state.c_str()) == \"on\";" - off_value: !lambda "return atoi(\"${TILE7_LONGPRESS_OFF_VALUE}\");" - brightness_value: !lambda "return id(tile7_brightness).state;" - percentage_value: !lambda "return NAN;" - position_value: !lambda "return NAN;" - icon_text: "${TILE7_ICON}" - slider_bg_color: ${TILE7_BG_DISABLED_COLOR} - slider_fill_color: ${TILE7_CIRCLE_ACTIVE_COLOR} - slider_icon_color: ${TILE7_ICON_ACTIVE_COLOR} - label_off: "${TILE7_LABEL_OFF}" - label_on: "${TILE7_LABEL_ON}" - supported_color_modes: !lambda "return std::string(id(tile7_supported_color_modes).state.c_str());" - climate_current_temperature: !lambda "return NAN;" - climate_target_temperature: !lambda "return NAN;" - climate_humidity: !lambda "return NAN;" - climate_mode: "" - climate_supported_modes: "" - climate_min_temp: "5.0" - climate_max_temp: "30.0" - climate_step: "0.5" - - # ---------- TILE 8 — Brievenbus reset ---------- - # input_select met opties Vol/Leeg. De tegel licht op zodra de - # status TILE8_ACTIVE_STATE ("Vol") is; een korte tik stuurt - # input_select.select_option met option: "Leeg". - # Geen slider-modus, dus deze tegel heeft alleen ha_state_tile8 - # nodig — verder geen enkele HA-subscription. - - obj: - id: tile8 - x: ${GRID_COL2_X} - y: ${GRID_ROW4_Y} - width: ${TILE_W} - height: ${TILE_H} - styles: style_tile - scrollable: false - scrollbar_mode: 'OFF' - pressed: - bg_opa: 45% - widgets: - - obj: - id: tile8_icon_circle - x: ${TILE_ICON_X} - y: ${TILE_ICON_Y} - width: 36 - height: 36 - styles: style_icon_circle - clickable: false - scrollable: false - scrollbar_mode: 'OFF' - widgets: - - label: - id: tile8_icon_lbl - align: CENTER - text_font: materialdesign_icons - text_color: 0xFFFFFF - text: "${TILE8_ICON}" - clickable: false - - label: - id: t8_title - x: ${TILE_TITLE_X} - y: ${TILE_TITLE_Y} - text: "${TILE8_TITLE}" - styles: style_title - clickable: false - - label: - id: t8_value - x: ${TILE_VALUE_X} - y: ${TILE_VALUE_Y} - text: "—" - styles: style_value - clickable: false - on_short_click: - - script.execute: { id: tile8_ack, is_long: false } - - script.execute: { id: publish_action, action: "tile8_press" } - - script.execute: - id: do_tile_action - tile_type: "${TILE8_TYPE}" - tap_action: "${TILE8_TAP_ACTION}" - service: "${TILE8_TAP_SERVICE}" - entity_id: "${TILE8_ENTITY}" - param_key: "${TILE8_TAP_PARAM_KEY}" - param_val: "${TILE8_TAP_PARAM_VAL}" - was_on: !lambda "return std::string(id(ha_state_tile8).state.c_str()) == \"${TILE8_ACTIVE_STATE}\";" - on_long_press: - - script.execute: { id: tile8_ack, is_long: true } - - script.execute: { id: publish_action, action: "tile8_long_press" } - # Slider-modus kan hier niet zonder eerst de bijbehorende - # sensors aan te maken; "action" werkt wel out of the box. - - if: - condition: - lambda: "return std::string(\"${TILE8_LONGPRESS}\") == \"action\";" - then: - - script.execute: - id: do_tile_action - tile_type: !lambda |- - std::string t = "${TILE8_LONGPRESS_ACTION_TYPE}"; - return t.empty() ? std::string("${TILE8_TYPE}") : t; - tap_action: "activate" - service: "${TILE8_LONGPRESS_ACTION_SERVICE}" - entity_id: "${TILE8_LONGPRESS_ACTION}" - param_key: "" - param_val: "" - was_on: false - - # ---------- TILE 9 — Woonkamer (verhuisd van tegel 2) ---------- - - obj: - id: tile9 - x: ${GRID_COL1_X} - y: ${GRID_ROW5_Y} - width: ${TILE_W} - height: ${TILE_H} - styles: style_tile - scrollable: false - scrollbar_mode: 'OFF' - pressed: - bg_opa: 45% - widgets: - - obj: - id: tile9_icon_circle - x: ${TILE_ICON_X} - y: ${TILE_ICON_Y} - width: 36 - height: 36 - styles: style_icon_circle - clickable: false - scrollable: false - scrollbar_mode: 'OFF' - widgets: - - label: - id: tile9_icon_lbl - align: CENTER - text_font: materialdesign_icons - text_color: 0xFFFFFF - text: "${TILE9_ICON}" - clickable: false - - label: - id: t9_title - x: ${TILE_TITLE_X} - y: ${TILE_TITLE_Y} - text: "${TILE9_TITLE}" - styles: style_title - clickable: false - - label: - id: t9_value - x: ${TILE_VALUE_X} - y: ${TILE_VALUE_Y} - text: "—" - styles: style_value - clickable: false - on_short_click: - - script.execute: { id: tile9_ack, is_long: false } - - 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: - - script.execute: { id: tile9_ack, is_long: true } - - 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 - x: ${GRID_COL2_X} - y: ${GRID_ROW5_Y} - width: ${TILE_W} - height: ${TILE_H} - styles: style_tile - scrollable: false - scrollbar_mode: 'OFF' - pressed: - bg_opa: 45% - widgets: - - obj: - id: tile10_icon_circle - x: ${TILE_ICON_X} - y: ${TILE_ICON_Y} - width: 36 - height: 36 - styles: style_icon_circle - clickable: false - scrollable: false - scrollbar_mode: 'OFF' - widgets: - - label: - id: tile10_icon_lbl - align: CENTER - text_font: materialdesign_icons - text_color: 0xFFFFFF - text: "${TILE10_ICON}" - clickable: false - - label: - id: t10_title - x: ${TILE_TITLE_X} - y: ${TILE_TITLE_Y} - text: "${TILE10_TITLE}" - styles: style_title - clickable: false - - label: - id: t10_value - x: ${TILE_VALUE_X} - y: ${TILE_VALUE_Y} - text: "—" - styles: style_value - clickable: false - on_short_click: - - script.execute: { id: tile10_ack, is_long: false } - - 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: - - script.execute: { id: tile10_ack, is_long: true } - - 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: brightness_overlay - x: 0 - y: 0 - width: ${DISPLAY_W} - height: ${DISPLAY_H} - bg_opa: TRANSP - hidden: true - scrollable: false - scrollbar_mode: 'OFF' - pad_left: 0 - pad_right: 0 - pad_top: 0 - pad_bottom: 0 - border_width: 0 - widgets: - - obj: - id: brightness_overlay_bg - x: 0 - y: 0 - width: ${DISPLAY_W} - height: ${DISPLAY_H} - styles: style_bg_grad - scrollable: false - clickable: true - on_click: - - lvgl.widget.hide: brightness_overlay - - - obj: - id: brightness_overlay_dim - x: 0 - y: 0 - width: ${DISPLAY_W} - height: ${DISPLAY_H} - bg_color: 0x000000 - bg_opa: 18% - border_width: 0 - clickable: false - scrollable: false - - - label: - id: overlay_title - align: TOP_MID - y: 10 - text: "Light" - styles: style_room - - - label: - id: overlay_value - align: TOP_MID - y: 35 - text: "100 %" - styles: style_value_big - - - slider: - id: overlay_slider - align: CENTER - y: 18 - width: ${OVERLAY_SLIDER_W} - height: ${OVERLAY_SLIDER_H} - min_value: 0 - max_value: 100 - value: 100 - adv_hittest: false - - bg_color: 0x939391 - bg_opa: 100% - radius: 28 - # Enige widget met ZOWEL radius ALS een kind (overlay_icon). - # LVGL snijdt kinderen dan bij op de ronding, en dat kan - # alleen via een ARGB-laag van 96x160 = 46KB aaneengesloten. - # Lukt die allocatie niet, dan blijft LVGL het proberen tot - # de task watchdog het bord herstart. Ronding blijft, het - # bijsnijden gaat eraf. - clip_corner: false - border_width: 0 - - indicator: - bg_color: 0xFEC600 - bg_opa: 100% - radius: 28 - - knob: - bg_opa: 0% - border_width: 0 - shadow_width: 0 - pad_all: 10 - - on_value: - - script.execute: - id: slider_preview - value: !lambda "return x;" - - on_release: - - script.execute: - id: slider_commit - value: !lambda "return lv_slider_get_value(id(overlay_slider));" - - - # Het icoon zat als KIND in de slider. Een widget met een radius - # en kinderen moet die kinderen bijsnijden op de ronding, en dat - # kan LVGL alleen via een ARGB-laag van tientallen KB's. Zonder - # PSRAM mislukt dat soms, waarna LVGL blijft hertesten tot de - # watchdog het bord herstart. Als broer eroverheen is er geen - # laag nodig en ziet het er identiek uit. - - label: - id: overlay_icon - align: CENTER - y: ${OVERLAY_ICON_Y} - text_font: materialdesign_icons - text_color: 0xFFFFFF - text: "${TILE1_ICON}" - clickable: false - - - obj: - id: overlay_color_button - align: BOTTOM_RIGHT - x: -13 - y: -13 - width: 44 - height: 44 - radius: 999 - hidden: true - clickable: true - bg_opa: TRANSP - border_width: 0 - pad_left: 0 - pad_right: 0 - pad_top: 0 - pad_bottom: 0 - scrollable: false - scrollbar_mode: 'OFF' - on_click: - - script.execute: open_color_detail - widgets: - - arc: - align: CENTER - width: 40 - height: 40 - start_angle: 0 - end_angle: 45 - min_value: 0 - max_value: 100 - value: 0 - adjustable: false - arc_width: 10 - arc_rounded: false - arc_color: 0xFF3B30 - arc_opa: COVER - bg_opa: TRANSP - border_width: 0 - clickable: false - indicator: - arc_opa: TRANSP - knob: - bg_opa: TRANSP - border_width: 0 - - arc: - align: CENTER - width: 40 - height: 40 - start_angle: 45 - end_angle: 90 - min_value: 0 - max_value: 100 - value: 0 - adjustable: false - arc_width: 10 - arc_rounded: false - arc_color: 0xFF9500 - arc_opa: COVER - bg_opa: TRANSP - border_width: 0 - clickable: false - indicator: - arc_opa: TRANSP - knob: - bg_opa: TRANSP - border_width: 0 - - arc: - align: CENTER - width: 40 - height: 40 - start_angle: 90 - end_angle: 135 - min_value: 0 - max_value: 100 - value: 0 - adjustable: false - arc_width: 10 - arc_rounded: false - arc_color: 0xFFD60A - arc_opa: COVER - bg_opa: TRANSP - border_width: 0 - clickable: false - indicator: - arc_opa: TRANSP - knob: - bg_opa: TRANSP - border_width: 0 - - arc: - align: CENTER - width: 40 - height: 40 - start_angle: 135 - end_angle: 180 - min_value: 0 - max_value: 100 - value: 0 - adjustable: false - arc_width: 10 - arc_rounded: false - arc_color: 0x30D158 - arc_opa: COVER - bg_opa: TRANSP - border_width: 0 - clickable: false - indicator: - arc_opa: TRANSP - knob: - bg_opa: TRANSP - border_width: 0 - - arc: - align: CENTER - width: 40 - height: 40 - start_angle: 180 - end_angle: 225 - min_value: 0 - max_value: 100 - value: 0 - adjustable: false - arc_width: 10 - arc_rounded: false - arc_color: 0x32D6FF - arc_opa: COVER - bg_opa: TRANSP - border_width: 0 - clickable: false - indicator: - arc_opa: TRANSP - knob: - bg_opa: TRANSP - border_width: 0 - - arc: - align: CENTER - width: 40 - height: 40 - start_angle: 225 - end_angle: 270 - min_value: 0 - max_value: 100 - value: 0 - adjustable: false - arc_width: 10 - arc_rounded: false - arc_color: 0x0A84FF - arc_opa: COVER - bg_opa: TRANSP - border_width: 0 - clickable: false - indicator: - arc_opa: TRANSP - knob: - bg_opa: TRANSP - border_width: 0 - - arc: - align: CENTER - width: 40 - height: 40 - start_angle: 270 - end_angle: 315 - min_value: 0 - max_value: 100 - value: 0 - adjustable: false - arc_width: 10 - arc_rounded: false - arc_color: 0x5856D6 - arc_opa: COVER - bg_opa: TRANSP - border_width: 0 - clickable: false - indicator: - arc_opa: TRANSP - knob: - bg_opa: TRANSP - border_width: 0 - - arc: - align: CENTER - width: 40 - height: 40 - start_angle: 315 - end_angle: 360 - min_value: 0 - max_value: 100 - value: 0 - adjustable: false - arc_width: 10 - arc_rounded: false - arc_color: 0xBF5AF2 - arc_opa: COVER - bg_opa: TRANSP - border_width: 0 - clickable: false - indicator: - arc_opa: TRANSP - knob: - bg_opa: TRANSP - border_width: 0 - - # ---------- LIGHT COLOR DETAIL OVERLAY ---------- - - obj: - id: color_detail_overlay - x: 0 - y: 0 - width: ${DISPLAY_W} - height: ${DISPLAY_H} - bg_color: 0x171719 - bg_opa: COVER - hidden: true - scrollable: false - scrollbar_mode: 'OFF' - pad_left: 0 - pad_right: 0 - pad_top: 0 - pad_bottom: 0 - border_width: 0 - widgets: - - obj: - id: color_detail_bg - x: 0 - y: 0 - width: ${DISPLAY_W} - height: ${DISPLAY_H} - styles: style_bg_grad - scrollable: false - clickable: false - - obj: - id: color_detail_scrim - x: 0 - y: 0 - width: ${DISPLAY_W} - height: ${DISPLAY_H} - bg_color: 0x000000 - bg_opa: 72% - border_width: 0 - clickable: false - scrollable: false - - - obj: - id: color_close_button - align: TOP_LEFT - x: 10 - y: 9 - width: 36 - height: 36 - radius: 999 - bg_color: 0x3C3C40 - bg_opa: COVER - border_width: 1 - border_color: 0xFFFFFF - border_opa: 20% - clickable: true - scrollable: false - on_click: - - lvgl.widget.hide: color_detail_overlay - - lvgl.widget.show: brightness_overlay - widgets: - - label: - align: CENTER - text: "x" - text_font: sublabel_big - text_color: 0xFFFFFF - clickable: false - - - label: - id: color_detail_title - align: TOP_MID - y: 16 - text: "Light Color" - styles: style_room - - - obj: - id: color_done_button - align: TOP_RIGHT - x: -10 - y: 9 - width: 36 - height: 36 - radius: 999 - bg_color: 0xFF9F0A - bg_opa: COVER - border_width: 0 - clickable: true - scrollable: false - on_click: - - lvgl.widget.hide: color_detail_overlay - widgets: - - label: - align: CENTER - text: "\U000F012C" - text_font: materialdesign_icons - text_color: 0xFFFFFF - clickable: false - - - obj: - id: color_mode_tabs - align: TOP_MID - y: 58 - width: 190 - height: 28 - radius: 14 - bg_color: 0x3C3C40 - bg_opa: COVER - border_width: 0 - scrollable: false - pad_left: 0 - pad_right: 0 - pad_top: 0 - pad_bottom: 0 - widgets: - - obj: - id: color_tab_indicator - x: 2 - y: 2 - width: 93 - height: 24 - radius: 12 - bg_color: 0x8E8E93 - bg_opa: COVER - border_width: 0 - clickable: false - scrollable: false - - obj: - x: 0 - y: 0 - width: 95 - height: 28 - bg_opa: TRANSP - border_width: 0 - clickable: true - scrollable: false - pad_left: 0 - pad_right: 0 - pad_top: 0 - pad_bottom: 0 - on_click: - - script.execute: - id: show_color_detail_tab - tab: 0 - widgets: - - label: - id: color_tab_label - align: CENTER - text: "Color" - text_font: sublabel_big - text_color: 0xFFFFFF - clickable: false - - obj: - x: 95 - y: 0 - width: 95 - height: 28 - bg_opa: TRANSP - border_width: 0 - clickable: true - scrollable: false - pad_left: 0 - pad_right: 0 - pad_top: 0 - pad_bottom: 0 - on_click: - - script.execute: - id: show_color_detail_tab - tab: 1 - widgets: - - label: - id: temp_tab_label - align: CENTER - text: "Temp." - text_font: sublabel_big - text_color: 0xFFFFFF - clickable: false - - - obj: - id: color_palette_card - align: TOP_MID - y: 98 - width: 286 - height: 48 - bg_opa: TRANSP - border_width: 0 - scrollable: false - pad_left: 0 - pad_right: 0 - pad_top: 0 - pad_bottom: 0 - widgets: - - obj: - id: color_preset_red - x: 14 - y: 8 - width: 30 - height: 30 - radius: 999 - bg_color: 0xFF3B30 - bg_opa: COVER - border_width: 0 - clickable: true - on_click: - - script.execute: { id: set_light_color_name, color: "red" } - - obj: - id: color_preset_orange - x: 61 - y: 8 - width: 30 - height: 30 - radius: 999 - bg_color: 0xFF9F0A - bg_opa: COVER - border_width: 0 - clickable: true - on_click: - - script.execute: { id: set_light_color_name, color: "orange" } - - obj: - id: color_preset_yellow - x: 108 - y: 8 - width: 30 - height: 30 - radius: 999 - bg_color: 0xFFD60A - bg_opa: COVER - border_width: 0 - clickable: true - on_click: - - script.execute: { id: set_light_color_name, color: "yellow" } - - obj: - id: color_preset_green - x: 155 - y: 8 - width: 30 - height: 30 - radius: 999 - bg_color: 0x30D158 - bg_opa: COVER - border_width: 0 - clickable: true - on_click: - - script.execute: { id: set_light_color_name, color: "green" } - - obj: - id: color_preset_blue - x: 202 - y: 8 - width: 30 - height: 30 - radius: 999 - bg_color: 0x0A84FF - bg_opa: COVER - border_width: 0 - clickable: true - on_click: - - script.execute: { id: set_light_color_name, color: "blue" } - - obj: - id: color_preset_purple - x: 249 - y: 8 - width: 30 - height: 30 - radius: 999 - bg_color: 0xBF5AF2 - bg_opa: COVER - border_width: 0 - clickable: true - on_click: - - script.execute: { id: set_light_color_name, color: "purple" } - - - obj: - id: temp_palette_card - align: TOP_MID - y: 98 - width: 286 - height: 48 - bg_opa: TRANSP - border_width: 0 - scrollable: false - hidden: true - pad_left: 0 - pad_right: 0 - pad_top: 0 - pad_bottom: 0 - widgets: - - obj: - id: temp_preset_2700 - x: 24 - y: 8 - width: 62 - height: 30 - radius: 15 - bg_color: 0xFFD4A3 - bg_opa: COVER - border_width: 0 - clickable: true - scrollable: false - scrollbar_mode: 'OFF' - pad_left: 0 - pad_right: 0 - pad_top: 0 - pad_bottom: 0 - on_click: - - script.execute: { id: set_light_color_temperature, kelvin: 2700 } - widgets: - - label: - align: CENTER - text: "2700K" - text_font: sublabel_big - text_color: 0x3A2A18 - clickable: false - - obj: - id: temp_preset_4000 - x: 112 - y: 8 - width: 62 - height: 30 - radius: 15 - bg_color: 0xFFE8C8 - bg_opa: COVER - border_width: 0 - clickable: true - scrollable: false - scrollbar_mode: 'OFF' - pad_left: 0 - pad_right: 0 - pad_top: 0 - pad_bottom: 0 - on_click: - - script.execute: { id: set_light_color_temperature, kelvin: 4000 } - widgets: - - label: - align: CENTER - text: "4000K" - text_font: sublabel_big - text_color: 0x3A2A18 - clickable: false - - obj: - id: temp_preset_6500 - x: 200 - y: 8 - width: 62 - height: 30 - radius: 15 - bg_color: 0xEAF4FF - bg_opa: COVER - border_width: 0 - clickable: true - scrollable: false - scrollbar_mode: 'OFF' - pad_left: 0 - pad_right: 0 - pad_top: 0 - pad_bottom: 0 - on_click: - - script.execute: { id: set_light_color_temperature, kelvin: 6500 } - widgets: - - label: - align: CENTER - text: "6500K" - text_font: sublabel_big - text_color: 0x1E2A36 - clickable: false - - - obj: - id: color_brightness_card - align: TOP_MID - y: 165 - width: 188 - height: 46 - bg_opa: TRANSP - border_width: 0 - scrollable: false - pad_left: 0 - pad_right: 0 - pad_top: 0 - pad_bottom: 0 - widgets: - - label: - x: 13 - y: 6 - text: "Brightness" - text_font: sublabel - text_color: 0xD1D1D6 - hidden: true - clickable: false - - slider: - id: color_brightness_slider - x: 0 - y: 0 - width: 188 - height: 46 - min_value: 1 - max_value: 100 - value: 100 - bg_color: 0x939391 - bg_opa: COVER - radius: 23 - border_width: 0 - indicator: - bg_color: 0xFEC600 - bg_opa: COVER - radius: 23 - knob: - bg_opa: 0% - border_width: 0 - shadow_width: 0 - pad_all: 10 - on_value: - - script.execute: - id: color_brightness_preview - value: !lambda "return x;" - on_release: - - script.execute: - id: color_brightness_commit - value: !lambda "return lv_slider_get_value(id(color_brightness_slider));" - - label: - id: color_brightness_value - align: RIGHT_MID - x: -13 - text: "100 %" - text_font: sublabel_big - text_color: 0xFFFFFF - hidden: true - clickable: false - - # ---------- CLIMATE DETAIL OVERLAY ---------- - - obj: - id: climate_detail_overlay - x: 0 - y: 0 - width: ${DISPLAY_W} - height: ${DISPLAY_H} - bg_color: 0x171719 - bg_opa: COVER - hidden: true - scrollable: false - scrollbar_mode: 'OFF' - pad_left: 0 - pad_right: 0 - pad_top: 0 - pad_bottom: 0 - border_width: 0 - widgets: - - obj: - id: climate_detail_bg - x: 0 - y: 0 - width: ${DISPLAY_W} - height: ${DISPLAY_H} - styles: style_bg_grad - scrollable: false - clickable: false - - obj: - id: climate_detail_scrim - x: 0 - y: 0 - width: ${DISPLAY_W} - height: ${DISPLAY_H} - bg_color: 0x000000 - bg_opa: 62% - border_width: 0 - clickable: false - scrollable: false - - - obj: - id: climate_close_button - align: TOP_LEFT - x: 10 - y: 9 - width: 36 - height: 36 - radius: 999 - bg_color: 0x3C3C40 - bg_opa: COVER - border_width: 1 - border_color: 0xFFFFFF - border_opa: 20% - clickable: true - scrollable: false - on_click: - - lvgl.widget.hide: climate_detail_overlay - widgets: - - label: - align: CENTER - text: "x" - text_font: sublabel_big - text_color: 0xFFFFFF - clickable: false - - - obj: - id: climate_done_button - align: TOP_RIGHT - x: -10 - y: 9 - width: 36 - height: 36 - radius: 999 - bg_color: 0xFEC600 - bg_opa: COVER - border_width: 0 - clickable: true - scrollable: false - on_click: - - lvgl.widget.hide: climate_detail_overlay - widgets: - - label: - align: CENTER - text: "\U000F012C" - text_font: materialdesign_icons - text_color: 0xFFFFFF - clickable: false - - - label: - id: climate_detail_title - align: TOP_MID - y: 16 - text: "Climate" - styles: style_room - - - label: - id: climate_current_label - align: TOP_MID - y: 42 - text: "Current: --" - text_font: sublabel_big - text_color: 0xD1E8FF - text_opa: 85% - clickable: false - - - arc: - id: climate_arc - align: CENTER - y: 20 - width: 184 - height: 184 - start_angle: 135 - end_angle: 45 - min_value: 50 - max_value: 300 - value: 200 - adjustable: true - arc_width: 20 - arc_rounded: true - arc_color: 0x4B4B4F - arc_opa: 80% - bg_opa: TRANSP - border_width: 0 - indicator: - arc_color: 0xFEC600 - arc_opa: COVER - arc_width: 20 - arc_rounded: true - knob: - bg_color: 0xFFFFFF - bg_opa: COVER - border_width: 0 - shadow_width: 0 - on_value: - - script.execute: - id: climate_target_preview - value: !lambda "return x;" - on_release: - - script.execute: - id: climate_target_commit - value: !lambda "return lv_arc_get_value(id(climate_arc));" - - - label: - id: climate_set_label - align: CENTER - y: -16 - text: "SET TO" - text_font: sublabel_big - text_color: 0xFFFFFF - text_opa: 68% - clickable: false - - - label: - id: climate_target_label - align: CENTER - y: 20 - text: "20.0°" - text_font: headline - text_color: 0xFFFFFF - clickable: false - - - obj: - id: climate_minus_button - align: BOTTOM_LEFT - x: 38 - y: -16 - width: 48 - height: 36 - radius: 18 - bg_color: 0x3C3C40 - bg_opa: COVER - border_width: 1 - border_color: 0xFFFFFF - border_opa: 16% - clickable: true - scrollable: false - on_click: - - script.execute: - id: climate_adjust_target - direction: -1 - widgets: - - label: - align: CENTER - text: "-" - text_font: headline - text_color: 0xFFFFFF - clickable: false - - - obj: - id: climate_plus_button - align: BOTTOM_RIGHT - x: -38 - y: -16 - width: 48 - height: 36 - radius: 18 - bg_color: 0x3C3C40 - bg_opa: COVER - border_width: 1 - border_color: 0xFFFFFF - border_opa: 16% - clickable: true - scrollable: false - on_click: - - script.execute: - id: climate_adjust_target - direction: 1 - widgets: - - label: - align: CENTER - text: "+" - text_font: headline - text_color: 0xFFFFFF - clickable: false - - - obj: - id: climate_mode_pill - align: BOTTOM_MID - y: -18 - width: 126 - height: 34 - radius: 17 - bg_color: 0x2C2C30 - bg_opa: 82% - border_width: 0 - clickable: true - scrollable: false - on_click: - - script.execute: open_climate_mode_picker - widgets: - - label: - id: climate_mode_label - align: CENTER - text: "Auto" - text_font: sublabel_big - text_color: 0xFFFFFF - clickable: false - # ---------- CLIMATE MODE PICKER ---------- - # Vervangt het doorcyclen van modi. Cyclen stuurt bij elke tik een - # set_hvac_mode, dus om van 'cool' naar 'dry' te komen zet je een - # warmtepomp onderweg ook echt in 'heat' en 'fan_only'. Hier kies je - # rechtstreeks, en wordt er precies een opdracht verstuurd. - - obj: - id: climate_mode_overlay - x: 0 - y: 0 - width: ${DISPLAY_W} - height: ${DISPLAY_H} - hidden: true - bg_color: 0x000000 - bg_opa: 90% - border_width: 0 - clickable: true - scrollable: false - scrollbar_mode: 'OFF' - widgets: - - label: - align: TOP_MID - y: 16 - text: "Mode" - text_font: sublabel_big - text_color: 0xFFFFFF - clickable: false - - # Flex zorgt dat niet-ondersteunde modi geen gat achterlaten: - # LVGL slaat verborgen kinderen over bij het schikken. - - obj: - id: climate_mode_list - align: TOP_MID - y: 50 - width: 98% - height: 178 - bg_opa: TRANSP - border_width: 0 - scrollable: false - pad_all: 3 - layout: - type: FLEX - flex_flow: ROW_WRAP - flex_align_main: CENTER - flex_align_cross: CENTER - flex_align_track: CENTER - widgets: - - obj: - id: cmode_off - width: 132 - height: 38 - radius: 19 - bg_color: 0x2C2C30 - bg_opa: 92% - border_width: 0 - clickable: true - scrollable: false - pad_all: 0 - on_click: - - script.execute: { id: set_climate_mode, mode: "off" } - widgets: - - label: - align: CENTER - text: "Off" - text_font: sublabel_big - text_color: 0xFFFFFF - clickable: false - - obj: - id: cmode_heat - width: 132 - height: 38 - radius: 19 - bg_color: 0x2C2C30 - bg_opa: 92% - border_width: 0 - clickable: true - scrollable: false - pad_all: 0 - on_click: - - script.execute: { id: set_climate_mode, mode: "heat" } - widgets: - - label: - align: CENTER - text: "Heat" - text_font: sublabel_big - text_color: 0xFFFFFF - clickable: false - - obj: - id: cmode_cool - width: 132 - height: 38 - radius: 19 - bg_color: 0x2C2C30 - bg_opa: 92% - border_width: 0 - clickable: true - scrollable: false - pad_all: 0 - on_click: - - script.execute: { id: set_climate_mode, mode: "cool" } - widgets: - - label: - align: CENTER - text: "Cool" - text_font: sublabel_big - text_color: 0xFFFFFF - clickable: false - - obj: - id: cmode_heat_cool - width: 132 - height: 38 - radius: 19 - bg_color: 0x2C2C30 - bg_opa: 92% - border_width: 0 - clickable: true - scrollable: false - pad_all: 0 - on_click: - - script.execute: { id: set_climate_mode, mode: "heat_cool" } - widgets: - - label: - align: CENTER - text: "Heat/Cool" - text_font: sublabel_big - text_color: 0xFFFFFF - clickable: false - - obj: - id: cmode_auto - width: 132 - height: 38 - radius: 19 - bg_color: 0x2C2C30 - bg_opa: 92% - border_width: 0 - clickable: true - scrollable: false - pad_all: 0 - on_click: - - script.execute: { id: set_climate_mode, mode: "auto" } - widgets: - - label: - align: CENTER - text: "Auto" - text_font: sublabel_big - text_color: 0xFFFFFF - clickable: false - - obj: - id: cmode_dry - width: 132 - height: 38 - radius: 19 - bg_color: 0x2C2C30 - bg_opa: 92% - border_width: 0 - clickable: true - scrollable: false - pad_all: 0 - on_click: - - script.execute: { id: set_climate_mode, mode: "dry" } - widgets: - - label: - align: CENTER - text: "Dry" - text_font: sublabel_big - text_color: 0xFFFFFF - clickable: false - - obj: - id: cmode_fan_only - width: 132 - height: 38 - radius: 19 - bg_color: 0x2C2C30 - bg_opa: 92% - border_width: 0 - clickable: true - scrollable: false - pad_all: 0 - on_click: - - script.execute: { id: set_climate_mode, mode: "fan_only" } - widgets: - - label: - align: CENTER - text: "Fan" - text_font: sublabel_big - text_color: 0xFFFFFF - clickable: false - - - obj: - id: climate_mode_overlay_x - align: TOP_LEFT - x: 10 - y: 9 - width: 36 - height: 36 - radius: 999 - bg_color: 0x3C3C40 - bg_opa: COVER - border_width: 1 - border_color: 0xFFFFFF - border_opa: 20% - clickable: true - scrollable: false - on_click: - - lvgl.widget.hide: climate_mode_overlay - widgets: - - label: - align: CENTER - text: "x" - text_font: sublabel_big - text_color: 0xFFFFFF - clickable: false - - - obj: - id: climate_mode_overlay_ok - align: TOP_RIGHT - x: -10 - y: 9 - width: 36 - height: 36 - radius: 999 - bg_color: 0xFEC600 - bg_opa: COVER - border_width: 0 - clickable: true - scrollable: false - on_click: - - lvgl.widget.hide: climate_mode_overlay - widgets: - - label: - align: CENTER - text: "\U000F012C" - text_font: materialdesign_icons - text_color: 0xFFFFFF - clickable: false - - # ---------- VACUUM CONTROL ---------- - # Een stofzuiger is geen schakelaar: start, pauze, stop en terug naar - # het dock zijn losse opdrachten, en de zuigkracht is een aparte as. - - obj: - id: vacuum_overlay - x: 0 - y: 0 - width: ${DISPLAY_W} - height: ${DISPLAY_H} - hidden: true - bg_color: 0x000000 - bg_opa: 90% - border_width: 0 - clickable: true - scrollable: false - scrollbar_mode: 'OFF' - widgets: - - label: - id: vacuum_title - align: TOP_MID - y: 16 - text: "Vacuum" - text_font: sublabel_big - text_color: 0xFFFFFF - clickable: false - - - obj: - id: vacuum_actions - align: TOP_MID - y: 52 - width: 98% - height: 80 - bg_opa: TRANSP - border_width: 0 - scrollable: false - pad_all: 3 - layout: - type: FLEX - flex_flow: ROW_WRAP - flex_align_main: CENTER - flex_align_cross: CENTER - flex_align_track: CENTER - widgets: - - obj: - id: vac_start - width: 72 - height: 38 - radius: 19 - bg_color: 0x2C2C30 - bg_opa: 92% - border_width: 0 - clickable: true - scrollable: false - pad_all: 0 - on_click: - - script.execute: { id: vacuum_action, service: "vacuum.start" } - widgets: - - label: - align: CENTER - text: "Start" - text_font: sublabel_big - text_color: 0xFFFFFF - clickable: false - - obj: - id: vac_pause - width: 72 - height: 38 - radius: 19 - bg_color: 0x2C2C30 - bg_opa: 92% - border_width: 0 - clickable: true - scrollable: false - pad_all: 0 - on_click: - - script.execute: { id: vacuum_action, service: "vacuum.pause" } - widgets: - - label: - align: CENTER - text: "Pause" - text_font: sublabel_big - text_color: 0xFFFFFF - clickable: false - - obj: - id: vac_return_to_base - width: 72 - height: 38 - radius: 19 - bg_color: 0x2C2C30 - bg_opa: 92% - border_width: 0 - clickable: true - scrollable: false - pad_all: 0 - on_click: - - script.execute: { id: vacuum_action, service: "vacuum.return_to_base" } - widgets: - - label: - align: CENTER - text: "Dock" - text_font: sublabel_big - text_color: 0xFFFFFF - clickable: false - - obj: - id: vac_stop - width: 72 - height: 38 - radius: 19 - bg_color: 0x2C2C30 - bg_opa: 92% - border_width: 0 - clickable: true - scrollable: false - pad_all: 0 - on_click: - - script.execute: { id: vacuum_action, service: "vacuum.stop" } - widgets: - - label: - align: CENTER - text: "Stop" - text_font: sublabel_big - text_color: 0xFFFFFF - clickable: false - - - label: - align: TOP_MID - y: 138 - text: "Suction" - text_font: sublabel - text_color: 0xFFFFFF - clickable: false - - - obj: - id: vacuum_speeds - align: TOP_MID - y: 156 - width: 98% - height: 74 - bg_opa: TRANSP - border_width: 0 - scrollable: false - pad_all: 3 - layout: - type: FLEX - flex_flow: ROW_WRAP - flex_align_main: CENTER - flex_align_cross: CENTER - flex_align_track: CENTER - widgets: - - obj: - id: vacspd_quiet - width: 72 - height: 32 - radius: 19 - bg_color: 0x1F1F23 - bg_opa: 92% - border_width: 0 - clickable: true - scrollable: false - pad_all: 0 - on_click: - - script.execute: { id: vacuum_fan_speed, speed: "quiet" } - widgets: - - label: - align: CENTER - text: "Quiet" - text_font: sublabel_big - text_color: 0xFFFFFF - clickable: false - - obj: - id: vacspd_balanced - width: 72 - height: 32 - radius: 19 - bg_color: 0x1F1F23 - bg_opa: 92% - border_width: 0 - clickable: true - scrollable: false - pad_all: 0 - on_click: - - script.execute: { id: vacuum_fan_speed, speed: "balanced" } - widgets: - - label: - align: CENTER - text: "Balanced" - text_font: sublabel_big - text_color: 0xFFFFFF - clickable: false - - obj: - id: vacspd_turbo - width: 72 - height: 32 - radius: 19 - bg_color: 0x1F1F23 - bg_opa: 92% - border_width: 0 - clickable: true - scrollable: false - pad_all: 0 - on_click: - - script.execute: { id: vacuum_fan_speed, speed: "turbo" } - widgets: - - label: - align: CENTER - text: "Turbo" - text_font: sublabel_big - text_color: 0xFFFFFF - clickable: false - - obj: - id: vacspd_max - width: 72 - height: 32 - radius: 19 - bg_color: 0x1F1F23 - bg_opa: 92% - border_width: 0 - clickable: true - scrollable: false - pad_all: 0 - on_click: - - script.execute: { id: vacuum_fan_speed, speed: "max" } - widgets: - - label: - align: CENTER - text: "Max" - text_font: sublabel_big - text_color: 0xFFFFFF - clickable: false - - - obj: - id: vacuum_overlay_x - align: TOP_LEFT - x: 10 - y: 9 - width: 36 - height: 36 - radius: 999 - bg_color: 0x3C3C40 - bg_opa: COVER - border_width: 1 - border_color: 0xFFFFFF - border_opa: 20% - clickable: true - scrollable: false - on_click: - - lvgl.widget.hide: vacuum_overlay - widgets: - - label: - align: CENTER - text: "x" - text_font: sublabel_big - text_color: 0xFFFFFF - clickable: false - - - obj: - id: vacuum_overlay_ok - align: TOP_RIGHT - x: -10 - y: 9 - width: 36 - height: 36 - radius: 999 - bg_color: 0xFEC600 - bg_opa: COVER - border_width: 0 - clickable: true - scrollable: false - on_click: - - lvgl.widget.hide: vacuum_overlay - widgets: - - label: - align: CENTER - text: "\U000F012C" - text_font: materialdesign_icons - text_color: 0xFFFFFF - clickable: false - - # ---------- DIM / WAKE OVERLAY ---------- - - obj: - id: dim_wake_overlay - x: 0 - y: 0 - width: ${DISPLAY_W} - height: ${DISPLAY_H} - hidden: true - bg_opa: TRANSP - border_width: 0 - scrollable: false - scrollbar_mode: 'OFF' - clickable: true - on_click: - - script.execute: wake_display - -script: - # Korte bevestigingspuls na het loslaten. Draait parallel, dus vertraagt - # de Home Assistant-actie of het openen van een overlay niet. - - id: tile1_ack - mode: restart - parameters: - is_long: bool - then: - - if: - condition: - lambda: 'return is_long;' - then: - - lvgl.widget.update: { id: tile1, bg_opa: 20% } - - delay: 170ms - else: - - lvgl.widget.update: { id: tile1, bg_opa: 40% } - - delay: 90ms - - lvgl.widget.update: { id: tile1, bg_opa: 95% } - - # Korte bevestigingspuls na het loslaten. Draait parallel, dus vertraagt - # de Home Assistant-actie of het openen van een overlay niet. - - id: tile2_ack - mode: restart - parameters: - is_long: bool - then: - - if: - condition: - lambda: 'return is_long;' - then: - - lvgl.widget.update: { id: tile2, bg_opa: 20% } - - delay: 170ms - else: - - lvgl.widget.update: { id: tile2, bg_opa: 40% } - - delay: 90ms - - lvgl.widget.update: { id: tile2, bg_opa: 95% } - - # Korte bevestigingspuls na het loslaten. Draait parallel, dus vertraagt - # de Home Assistant-actie of het openen van een overlay niet. - - id: tile3_ack - mode: restart - parameters: - is_long: bool - then: - - if: - condition: - lambda: 'return is_long;' - then: - - lvgl.widget.update: { id: tile3, bg_opa: 20% } - - delay: 170ms - else: - - lvgl.widget.update: { id: tile3, bg_opa: 40% } - - delay: 90ms - - lvgl.widget.update: { id: tile3, bg_opa: 95% } - - # Korte bevestigingspuls na het loslaten. Draait parallel, dus vertraagt - # de Home Assistant-actie of het openen van een overlay niet. - - id: tile4_ack - mode: restart - parameters: - is_long: bool - then: - - if: - condition: - lambda: 'return is_long;' - then: - - lvgl.widget.update: { id: tile4, bg_opa: 20% } - - delay: 170ms - else: - - lvgl.widget.update: { id: tile4, bg_opa: 40% } - - delay: 90ms - - lvgl.widget.update: { id: tile4, bg_opa: 95% } - - # Korte bevestigingspuls na het loslaten. Draait parallel, dus vertraagt - # de Home Assistant-actie of het openen van een overlay niet. - - id: tile5_ack - mode: restart - parameters: - is_long: bool - then: - - if: - condition: - lambda: 'return is_long;' - then: - - lvgl.widget.update: { id: tile5, bg_opa: 20% } - - delay: 170ms - else: - - lvgl.widget.update: { id: tile5, bg_opa: 40% } - - delay: 90ms - - lvgl.widget.update: { id: tile5, bg_opa: 95% } - - # Korte bevestigingspuls na het loslaten. Draait parallel, dus vertraagt - # de Home Assistant-actie of het openen van een overlay niet. - - id: tile6_ack - mode: restart - parameters: - is_long: bool - then: - - if: - condition: - lambda: 'return is_long;' - then: - - lvgl.widget.update: { id: tile6, bg_opa: 20% } - - delay: 170ms - else: - - lvgl.widget.update: { id: tile6, bg_opa: 40% } - - delay: 90ms - - lvgl.widget.update: { id: tile6, bg_opa: 95% } - - # Korte bevestigingspuls na het loslaten. Draait parallel, dus vertraagt - # de Home Assistant-actie of het openen van een overlay niet. - - id: tile7_ack - mode: restart - parameters: - is_long: bool - then: - - if: - condition: - lambda: 'return is_long;' - then: - - lvgl.widget.update: { id: tile7, bg_opa: 20% } - - delay: 170ms - else: - - lvgl.widget.update: { id: tile7, bg_opa: 40% } - - delay: 90ms - - lvgl.widget.update: { id: tile7, bg_opa: 95% } - - # Korte bevestigingspuls na het loslaten. Draait parallel, dus vertraagt - # de Home Assistant-actie of het openen van een overlay niet. - - id: tile8_ack - mode: restart - parameters: - is_long: bool - then: - - if: - condition: - lambda: 'return is_long;' - then: - - lvgl.widget.update: { id: tile8, bg_opa: 20% } - - delay: 170ms - else: - - lvgl.widget.update: { id: tile8, bg_opa: 40% } - - delay: 90ms - - lvgl.widget.update: { id: tile8, bg_opa: 95% } - - # Korte bevestigingspuls na het loslaten. Draait parallel, dus vertraagt - # de Home Assistant-actie of het openen van een overlay niet. - - id: tile9_ack - mode: restart - parameters: - is_long: bool - then: - - if: - condition: - lambda: 'return is_long;' - then: - - lvgl.widget.update: { id: tile9, bg_opa: 20% } - - delay: 170ms - else: - - lvgl.widget.update: { id: tile9, bg_opa: 40% } - - delay: 90ms - - lvgl.widget.update: { id: tile9, bg_opa: 95% } - - # Korte bevestigingspuls na het loslaten. Draait parallel, dus vertraagt - # de Home Assistant-actie of het openen van een overlay niet. - - id: tile10_ack - mode: restart - parameters: - is_long: bool - then: - - if: - condition: - lambda: 'return is_long;' - then: - - lvgl.widget.update: { id: tile10, bg_opa: 20% } - - delay: 170ms - else: - - lvgl.widget.update: { id: tile10, bg_opa: 40% } - - delay: 90ms - - lvgl.widget.update: { id: tile10, bg_opa: 95% } - - - id: wake_display - mode: restart - then: - - lambda: |- - id(last_touch_ms) = millis(); - id(display_dimmed) = false; - - light.turn_on: - id: back_light - # Bijna instant omhoog. De ESPHome-standaard is 1s, en dat voelt traag - # als je het scherm aanraakt en op licht staat te wachten. - transition_length: 80ms - brightness: !lambda "return atoi(\"${MAX_BRIGHTNESS}\") / 100.0f;" - - lvgl.widget.hide: dim_wake_overlay - - lvgl.widget.hide: color_detail_overlay - - lvgl.widget.hide: climate_detail_overlay - - lvgl.widget.hide: climate_mode_overlay - - lvgl.widget.hide: vacuum_overlay - - - id: dim_display - mode: restart - then: - - if: - condition: - lambda: 'return std::string("${AUTO_DIM}") == "true";' - then: - - lambda: |- - int dim_value = atoi("${DIM_BRIGHTNESS}"); - int max_value = atoi("${MAX_BRIGHTNESS}"); - - if (std::string("${NIGHT_MODE}") == "true") { - auto now = id(homeassistant_time).now(); - int hour = now.hour; - int start_hour = atoi("${NIGHT_START_HOUR}"); - int end_hour = atoi("${NIGHT_END_HOUR}"); - bool is_night = false; - if (start_hour < end_hour) { - is_night = (hour >= start_hour && hour < end_hour); - } else { - is_night = (hour >= start_hour || hour < end_hour); - } - if (is_night) { - dim_value = atoi("${NIGHT_DIM_BRIGHTNESS}"); - } - } - - // Rustig omlaag faden is prettig; omhoog moet juist snel. - id(back_light).turn_on().set_transition_length(900).set_brightness(dim_value / 100.0f).perform(); - - // Always mark as dimmed to stop the interval from re-firing - id(display_dimmed) = true; - // Only show the touch-blocking overlay when brightness actually changes - if (dim_value < max_value) { - lv_obj_clear_flag(id(dim_wake_overlay), LV_OBJ_FLAG_HIDDEN); - } - - # Terug naar het basisscherm bij standby. Bewust via de ESPHome - # acties en NIET met lv_obj_add_flag in een lambda: dit script - # draait vanuit een interval, dus buiten LVGL's eigen afhandeling. - - lvgl.widget.hide: brightness_overlay - - lvgl.widget.hide: color_detail_overlay - - lvgl.widget.hide: climate_detail_overlay - - lvgl.widget.hide: climate_mode_overlay - - lvgl.widget.hide: vacuum_overlay - - - id: publish_action - mode: queued - parameters: - action: string - then: - - logger.log: - level: INFO - format: "publish_action: %s" - args: ["action.c_str()"] - - text_sensor.template.publish: - id: smartdisplay_action - state: !lambda 'return action;' - - delay: 500ms - - text_sensor.template.publish: - id: smartdisplay_action - state: "" - - - id: climate_target_preview - parameters: - value: int - then: - - lambda: |- - int min_x10 = id(active_climate_min_x10); - int max_x10 = id(active_climate_max_x10); - if (value < min_x10) value = min_x10; - if (value > max_x10) value = max_x10; - int step_x10 = id(active_climate_step_x10); - if (step_x10 < 5) step_x10 = 5; - value = min_x10 + (int) lround((value - min_x10) / (float) step_x10) * step_x10; - if (value < min_x10) value = min_x10; - if (value > max_x10) value = max_x10; - id(active_climate_target_x10) = value; - - char buf[16]; - snprintf(buf, sizeof(buf), "%.1f°", value / 10.0f); - lv_label_set_text(id(climate_target_label), buf); - lv_arc_set_value(id(climate_arc), value); - - - id: climate_target_commit - parameters: - value: int - then: - - script.execute: - id: climate_target_preview - value: !lambda "return value;" - - if: - condition: - lambda: 'return std::string("${DIRECT_ACTIONS}") == "true";' - then: - - homeassistant.action: - action: climate.set_temperature - data: - entity_id: !lambda "return id(active_entity);" - temperature: !lambda "return value / 10.0f;" - - - id: climate_adjust_target - parameters: - direction: int - then: - - lambda: |- - int step_x10 = id(active_climate_step_x10); - if (step_x10 < 5) step_x10 = 5; - int next = id(active_climate_target_x10) + (direction * step_x10); - if (next < id(active_climate_min_x10)) next = id(active_climate_min_x10); - if (next > id(active_climate_max_x10)) next = id(active_climate_max_x10); - id(active_climate_target_x10) = next; - - script.execute: - id: climate_target_commit - value: !lambda "return id(active_climate_target_x10);" - - # Opent de moduskiezer. Verbergt modi die de entiteit niet ondersteunt en - # markeert de huidige, zodat je ziet waar je staat voor je iets kiest. - # Opent de stofzuigerbediening en onthoudt om welke entiteit het gaat. - - id: open_vacuum_overlay - mode: restart - parameters: - title: string - entity: string - then: - - lambda: |- - id(active_entity) = entity; - lv_label_set_text(id(vacuum_title), title.c_str()); - - lvgl.widget.show: vacuum_overlay - - # Een losse opdracht: start, pause, stop of return_to_base. - - id: vacuum_action - mode: queued - parameters: - service: string - then: - - if: - condition: - lambda: 'return std::string("${DIRECT_ACTIONS}") == "true";' - then: - - homeassistant.action: - action: !lambda "return service;" - data: - entity_id: !lambda "return id(active_entity);" - - # Zuigkracht is een aparte as, los van start/stop. - - id: vacuum_fan_speed - mode: queued - parameters: - speed: string - then: - - if: - condition: - lambda: 'return std::string("${DIRECT_ACTIONS}") == "true";' - then: - - homeassistant.action: - action: vacuum.set_fan_speed - data: - entity_id: !lambda "return id(active_entity);" - fan_speed: !lambda "return speed;" - - - id: open_climate_mode_picker - mode: restart - then: - - lambda: |- - auto lower = [](std::string v) { - for (char &c : v) { if (c >= 'A' && c <= 'Z') c = (char)(c - 'A' + 'a'); } - return v; - }; - std::string supported = lower(id(active_climate_supported_modes)); - std::string current = lower(id(active_climate_mode)); - - auto has = [&](const char* m) { - std::string mode(m); - if (supported.empty()) return true; - return supported.find("'" + mode + "'") != std::string::npos || - supported.find("\"" + mode + "\"") != std::string::npos; - }; - auto apply = [&](lv_obj_t* btn, const char* m) { - if (has(m)) lv_obj_clear_flag(btn, LV_OBJ_FLAG_HIDDEN); - else lv_obj_add_flag(btn, LV_OBJ_FLAG_HIDDEN); - bool selected = (current == std::string(m)); - lv_obj_set_style_bg_color(btn, - lv_color_hex(selected ? 0xFF9F0A : 0x2C2C30), LV_PART_MAIN); - }; - apply(id(cmode_off), "off"); - apply(id(cmode_heat), "heat"); - apply(id(cmode_cool), "cool"); - apply(id(cmode_heat_cool), "heat_cool"); - apply(id(cmode_auto), "auto"); - apply(id(cmode_dry), "dry"); - apply(id(cmode_fan_only), "fan_only"); - - lvgl.widget.show: climate_mode_overlay - - # Zet precies een modus. Geen tussenstappen, dus geen ongewenste - # commando's naar de warmtepomp onderweg. - - id: set_climate_mode - mode: restart - parameters: - mode: string - then: - - lambda: |- - id(active_climate_mode) = mode; - if (mode == "off") lv_label_set_text(id(climate_mode_label), "Off"); - if (mode == "heat") lv_label_set_text(id(climate_mode_label), "Heat"); - if (mode == "cool") lv_label_set_text(id(climate_mode_label), "Cool"); - if (mode == "heat_cool") lv_label_set_text(id(climate_mode_label), "Heat/Cool"); - if (mode == "auto") lv_label_set_text(id(climate_mode_label), "Auto"); - if (mode == "dry") lv_label_set_text(id(climate_mode_label), "Dry"); - if (mode == "fan_only") lv_label_set_text(id(climate_mode_label), "Fan"); - - lvgl.widget.hide: climate_mode_overlay - - if: - condition: - lambda: 'return std::string("${DIRECT_ACTIONS}") == "true";' - then: - - homeassistant.action: - action: climate.set_hvac_mode - data: - entity_id: !lambda "return id(active_entity);" - hvac_mode: !lambda "return id(active_climate_mode);" - - - id: open_color_detail - mode: restart - then: - - lambda: |- - lv_label_set_text(id(color_detail_title), id(active_title).c_str()); - - char buf[12]; - snprintf(buf, sizeof(buf), "%d %%", id(active_brightness_pct)); - lv_label_set_text(id(color_brightness_value), buf); - lv_slider_set_value(id(color_brightness_slider), id(active_brightness_pct), LV_ANIM_OFF); - - bool show_tabs = id(active_supports_color) && id(active_supports_temperature); - if (show_tabs) { - lv_obj_clear_flag(id(color_mode_tabs), LV_OBJ_FLAG_HIDDEN); - } else { - lv_obj_add_flag(id(color_mode_tabs), LV_OBJ_FLAG_HIDDEN); - } - - if (atoi("${DISPLAY_W}") < atoi("${DISPLAY_H}")) { - int palette_y = show_tabs ? 96 : 76; - lv_obj_set_size(id(color_palette_card), 150, 86); - lv_obj_set_y(id(color_palette_card), palette_y); - lv_obj_set_pos(id(color_preset_red), 13, 4); - lv_obj_set_pos(id(color_preset_orange), 60, 4); - lv_obj_set_pos(id(color_preset_yellow), 107, 4); - lv_obj_set_pos(id(color_preset_green), 13, 52); - lv_obj_set_pos(id(color_preset_blue), 60, 52); - lv_obj_set_pos(id(color_preset_purple), 107, 52); - - lv_obj_set_size(id(temp_palette_card), 150, 126); - lv_obj_set_y(id(temp_palette_card), palette_y); - lv_obj_set_pos(id(temp_preset_2700), 44, 4); - lv_obj_set_pos(id(temp_preset_4000), 44, 48); - lv_obj_set_pos(id(temp_preset_6500), 44, 92); - - lv_obj_set_y(id(color_brightness_card), 256); - } else { - int palette_y = show_tabs ? 98 : 78; - lv_obj_set_size(id(color_palette_card), 286, 48); - lv_obj_set_y(id(color_palette_card), palette_y); - lv_obj_set_pos(id(color_preset_red), 14, 8); - lv_obj_set_pos(id(color_preset_orange), 61, 8); - lv_obj_set_pos(id(color_preset_yellow), 108, 8); - lv_obj_set_pos(id(color_preset_green), 155, 8); - lv_obj_set_pos(id(color_preset_blue), 202, 8); - lv_obj_set_pos(id(color_preset_purple), 249, 8); - - lv_obj_set_size(id(temp_palette_card), 286, 48); - lv_obj_set_y(id(temp_palette_card), palette_y); - lv_obj_set_pos(id(temp_preset_2700), 24, 8); - lv_obj_set_pos(id(temp_preset_4000), 112, 8); - lv_obj_set_pos(id(temp_preset_6500), 200, 8); - - lv_obj_set_y(id(color_brightness_card), 165); - } - - id(active_color_detail_tab) = id(active_supports_color) ? 0 : 1; - - lvgl.widget.hide: brightness_overlay - - lvgl.widget.show: color_detail_overlay - - script.execute: - id: show_color_detail_tab - tab: !lambda "return id(active_color_detail_tab);" - - - id: show_color_detail_tab - mode: restart - parameters: - tab: int - then: - - lambda: |- - if (tab == 0 && !id(active_supports_color) && id(active_supports_temperature)) { - tab = 1; - } else if (tab == 1 && !id(active_supports_temperature) && id(active_supports_color)) { - tab = 0; - } - id(active_color_detail_tab) = tab; - - bool show_tabs = id(active_supports_color) && id(active_supports_temperature); - if (show_tabs) { - lv_obj_clear_flag(id(color_mode_tabs), LV_OBJ_FLAG_HIDDEN); - } else { - lv_obj_add_flag(id(color_mode_tabs), LV_OBJ_FLAG_HIDDEN); - } - - if (tab == 0 && id(active_supports_color)) { - lv_obj_clear_flag(id(color_palette_card), LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(id(temp_palette_card), LV_OBJ_FLAG_HIDDEN); - if (show_tabs) lv_obj_set_x(id(color_tab_indicator), 2); - } else if (tab == 1 && id(active_supports_temperature)) { - lv_obj_add_flag(id(color_palette_card), LV_OBJ_FLAG_HIDDEN); - lv_obj_clear_flag(id(temp_palette_card), LV_OBJ_FLAG_HIDDEN); - if (show_tabs) lv_obj_set_x(id(color_tab_indicator), 95); - } else { - lv_obj_add_flag(id(color_palette_card), LV_OBJ_FLAG_HIDDEN); - lv_obj_add_flag(id(temp_palette_card), LV_OBJ_FLAG_HIDDEN); - } - - lv_obj_set_style_text_color( - id(color_tab_label), - id(active_supports_color) ? lv_color_hex(0xFFFFFF) : lv_color_hex(0x8E8E93), - 0 - ); - lv_obj_set_style_text_color( - id(temp_tab_label), - id(active_supports_temperature) ? lv_color_hex(0xFFFFFF) : lv_color_hex(0x8E8E93), - 0 - ); - if (tab == 0 && id(active_supports_color)) { - lv_obj_set_style_text_color(id(color_tab_label), lv_color_hex(0xFFFFFF), 0); - lv_obj_set_style_text_color(id(temp_tab_label), lv_color_hex(0xD1D1D6), 0); - } else if (tab == 1 && id(active_supports_temperature)) { - lv_obj_set_style_text_color(id(color_tab_label), lv_color_hex(0xD1D1D6), 0); - lv_obj_set_style_text_color(id(temp_tab_label), lv_color_hex(0xFFFFFF), 0); - } - - - id: color_brightness_preview - parameters: - value: int - then: - - lambda: |- - id(active_brightness_pct) = value; - char buf[12]; - snprintf(buf, sizeof(buf), "%d %%", value); - lv_label_set_text(id(color_brightness_value), buf); - - - id: color_brightness_commit - parameters: - value: int - then: - - lambda: 'id(active_brightness_pct) = value;' - - if: - condition: - lambda: 'return std::string("${DIRECT_ACTIONS}") == "true";' - then: - - homeassistant.action: - action: light.turn_on - data: - entity_id: !lambda "return id(active_entity);" - brightness_pct: !lambda "return value;" - - - id: set_light_color_name - mode: queued - parameters: - color: string - then: - - if: - condition: - lambda: 'return std::string("${DIRECT_ACTIONS}") == "true";' - then: - - homeassistant.action: - action: light.turn_on - data: - entity_id: !lambda "return id(active_entity);" - color_name: !lambda "return color;" - brightness_pct: !lambda "return id(active_brightness_pct);" - - - id: set_light_color_temperature - mode: queued - parameters: - kelvin: int - then: - - if: - condition: - lambda: 'return std::string("${DIRECT_ACTIONS}") == "true";' - then: - - homeassistant.action: - action: light.turn_on - data: - entity_id: !lambda "return id(active_entity);" - color_temp_kelvin: !lambda "return kelvin;" - brightness_pct: !lambda "return id(active_brightness_pct);" - - - id: slider_preview - parameters: - value: int - then: - - lambda: |- - char buf[20]; - snprintf(buf, sizeof(buf), "%d%s", value, id(active_value_suffix).c_str()); - lv_label_set_text(id(overlay_value), buf); - int pct = value; - if (pct < 0) pct = 0; - if (pct > 100) pct = 100; - std::string value_text; - - if (id(active_control_kind) == "cover_position") { - std::string closed_label = id(active_label_off).c_str(); - std::string open_label = id(active_label_on).c_str(); - - if (pct <= 0) { - value_text = closed_label; - } else if (pct >= 100) { - value_text = open_label; - } else { - char buf[40]; - snprintf(buf, sizeof(buf), "%s - %d%%", open_label.c_str(), pct); - value_text = buf; - } - } else { - if (pct <= 0) { - value_text = id(active_label_off).c_str(); - } else { - char buf[20]; - snprintf(buf, sizeof(buf), "%d%s", pct, id(active_value_suffix).c_str()); - value_text = buf; - } - } - - lv_label_set_text(id(overlay_value), value_text.c_str()); - - - - id: slider_commit - parameters: - value: int - then: - - if: - condition: - lambda: |- - return id(active_control_kind) == "light_brightness"; - then: - - homeassistant.action: - action: light.turn_on - data: - entity_id: !lambda "return id(active_entity);" - brightness_pct: !lambda "return value;" - else: - - if: - condition: - lambda: |- - return id(active_control_kind) == "fan_percentage"; - then: - - if: - condition: - lambda: "return value <= 0;" - then: - - homeassistant.action: - action: fan.turn_off - data: - entity_id: !lambda "return id(active_entity);" - else: - - homeassistant.action: - action: fan.turn_on - data: - entity_id: !lambda "return id(active_entity);" - - delay: 150ms - - homeassistant.action: - action: fan.set_percentage - data: - entity_id: !lambda "return id(active_entity);" - percentage: !lambda "return value;" - else: - - if: - condition: - lambda: |- - return id(active_control_kind) == "cover_position"; - then: - - homeassistant.action: - action: cover.set_cover_position - data: - entity_id: !lambda "return id(active_entity);" - position: !lambda "return value;" - - # ---------------------------------------------------------- - # do_tile_action - # - # Centralized handler for executing Home Assistant actions - # triggered by tile taps. - # - # Parameters - # - tile_type: light | fan | switch | scene | script | cover - # - tap_action: auto | toggle | activate | fan_toggle_preset - # - service: optional Home Assistant service override - # - entity_id: target entity - # - param_key / param_val: optional parameters for custom services - # - was_on: previous entity state (pass false for longpress actions) - # - # Behavior - # - # toggle - # -> toggles the entity - # light.toggle / fan.toggle / switch.toggle / cover.open_cover|close_cover (based on was_on) - # - # activate - # -> activates entity - # scene.turn_on / script.turn_on / light.turn_on / fan.turn_on / cover.open_cover - # - # auto - # -> resolves automatically based on tile_type: - # light/switch/fan/cover -> toggle - # scene/script -> activate - # - # fan_toggle_preset - # -> if fan was ON: fan.turn_off - # -> if fan was OFF: - # fan.turn_on - # wait 150ms - # fan.set_preset_mode(param_val) - # - # Custom service - # If "service" is set, it will be called directly. - # param_key / param_val may be used for parameters - # like preset_mode, brightness_pct, percentage, etc. - # ---------------------------------------------------------- - - id: do_tile_action - mode: queued - parameters: - tile_type: string - tap_action: string - service: string - entity_id: string - param_key: string - param_val: string - was_on: bool - then: - - if: - condition: - lambda: |- - return std::string("${DIRECT_ACTIONS}") == "true"; - then: - - lambda: |- - ESP_LOGD("tile", "do_tile_action type=%s action=%s service=%s entity=%s key=%s val=%s was_on=%d", - tile_type.c_str(), tap_action.c_str(), service.c_str(), entity_id.c_str(), - param_key.c_str(), param_val.c_str(), (int)was_on); - - - if: - condition: - lambda: |- - return tap_action == "fan_toggle_preset"; - then: - - if: - condition: - lambda: |- - return was_on; - then: - - homeassistant.action: - action: fan.turn_off - data: - entity_id: !lambda 'return entity_id;' - else: - - homeassistant.action: - action: fan.turn_on - data: - entity_id: !lambda 'return entity_id;' - - delay: 150ms - - homeassistant.action: - action: fan.set_preset_mode - data: - entity_id: !lambda 'return entity_id;' - preset_mode: !lambda 'return param_val;' - - else: - - if: - condition: - lambda: |- - std::string resolved = tap_action; - if (resolved == "auto") { - if (tile_type == "scene" || tile_type == "script") resolved = "activate"; - else resolved = "toggle"; - } - return resolved == "toggle"; - then: - - if: - condition: - lambda: |- - return service.size() > 0; - then: - - homeassistant.action: - action: !lambda 'return service;' - data: - entity_id: !lambda 'return entity_id;' - else: - - if: - condition: - lambda: |- - return tile_type == "light"; - then: - - homeassistant.action: - action: light.toggle - data: - entity_id: !lambda 'return entity_id;' - else: - - if: - condition: - lambda: |- - return tile_type == "fan"; - then: - - homeassistant.action: - action: fan.toggle - data: - entity_id: !lambda 'return entity_id;' - else: - - if: - condition: - lambda: |- - return tile_type == "switch"; - then: - - homeassistant.action: - action: switch.toggle - data: - entity_id: !lambda 'return entity_id;' - else: - - if: - condition: - lambda: |- - return tile_type == "cover"; - then: - - homeassistant.action: - action: cover.set_cover_position - data: - entity_id: !lambda 'return entity_id;' - position: !lambda |- - return was_on ? 0 : 100; - - else: - - if: - condition: - lambda: |- - std::string resolved = tap_action; - if (resolved == "auto") { - if (tile_type == "scene" || tile_type == "script") resolved = "activate"; - else resolved = "toggle"; - } - return resolved == "activate"; - then: - - if: - condition: - lambda: |- - return service.size() > 0; - then: - - if: - condition: - lambda: |- - return param_key == "preset_mode"; - then: - - homeassistant.action: - action: !lambda 'return service;' - data: - entity_id: !lambda 'return entity_id;' - preset_mode: !lambda 'return param_val;' - else: - - if: - condition: - lambda: |- - return param_key == "brightness_pct"; - then: - - homeassistant.action: - action: !lambda 'return service;' - data: - entity_id: !lambda 'return entity_id;' - brightness_pct: !lambda 'return atoi(param_val.c_str());' - else: - - if: - condition: - lambda: |- - return param_key == "percentage"; - then: - - homeassistant.action: - action: !lambda 'return service;' - data: - entity_id: !lambda 'return entity_id;' - percentage: !lambda 'return atoi(param_val.c_str());' - else: - - if: - condition: - lambda: |- - return param_key == "option"; - then: - - homeassistant.action: - action: !lambda 'return service;' - data: - entity_id: !lambda 'return entity_id;' - option: !lambda 'return param_val;' - else: - - homeassistant.action: - action: !lambda 'return service;' - data: - entity_id: !lambda 'return entity_id;' - else: - - if: - condition: - lambda: |- - return tile_type == "scene"; - then: - - homeassistant.action: - action: scene.turn_on - data: - entity_id: !lambda 'return entity_id;' - else: - - if: - condition: - lambda: |- - return tile_type == "script"; - then: - - homeassistant.action: - action: script.turn_on - data: - entity_id: !lambda 'return entity_id;' - else: - - if: - condition: - lambda: |- - return tile_type == "light"; - then: - - homeassistant.action: - action: light.turn_on - data: - entity_id: !lambda 'return entity_id;' - else: - - if: - condition: - lambda: |- - return tile_type == "fan"; - then: - - homeassistant.action: - action: fan.turn_on - data: - entity_id: !lambda 'return entity_id;' - else: - - if: - condition: - lambda: |- - return tile_type == "switch"; - then: - - homeassistant.action: - action: switch.turn_on - data: - entity_id: !lambda 'return entity_id;' - else: - - if: - condition: - lambda: |- - return tile_type == "cover"; - then: - - homeassistant.action: - action: cover.open_cover - data: - entity_id: !lambda 'return entity_id;' - - - id: open_value_overlay - parameters: - title: string - entity: string - tile_type: string - longpress_mode: string - was_on: bool - off_value: int - brightness_value: float - percentage_value: float - position_value: float - icon_text: string - slider_bg_color: uint32_t - slider_fill_color: uint32_t - slider_icon_color: uint32_t - label_off: string - label_on: string - supported_color_modes: string - climate_current_temperature: float - climate_target_temperature: float - climate_humidity: float - climate_mode: string - climate_supported_modes: string - climate_min_temp: string - climate_max_temp: string - climate_step: string - then: - - lambda: |- - // 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(active_control_kind) = "climate_temperature"; - id(active_climate_min_x10) = parse_x10(climate_min_temp, 50); - id(active_climate_max_x10) = parse_x10(climate_max_temp, 300); - id(active_climate_step_x10) = parse_x10(climate_step, 5); - if (id(active_climate_step_x10) < 5) id(active_climate_step_x10) = 5; - if (id(active_climate_max_x10) <= id(active_climate_min_x10)) { - id(active_climate_min_x10) = 50; - id(active_climate_max_x10) = 300; - } - - id(active_climate_current_x10) = to_x10(climate_current_temperature, 0); - int fallback_target = isnan(climate_current_temperature) ? 200 : id(active_climate_current_x10); - id(active_climate_target_x10) = to_x10(climate_target_temperature, fallback_target); - if (id(active_climate_target_x10) < id(active_climate_min_x10)) id(active_climate_target_x10) = id(active_climate_min_x10); - if (id(active_climate_target_x10) > id(active_climate_max_x10)) id(active_climate_target_x10) = id(active_climate_max_x10); - id(active_climate_target_x10) = id(active_climate_min_x10) + (int) lround((id(active_climate_target_x10) - id(active_climate_min_x10)) / (float) id(active_climate_step_x10)) * id(active_climate_step_x10); - if (id(active_climate_target_x10) < id(active_climate_min_x10)) id(active_climate_target_x10) = id(active_climate_min_x10); - if (id(active_climate_target_x10) > id(active_climate_max_x10)) id(active_climate_target_x10) = id(active_climate_max_x10); - id(active_climate_humidity) = isnan(climate_humidity) ? -1 : (int) lround(climate_humidity); - id(active_climate_mode) = climate_mode.empty() ? std::string("Auto") : climate_mode; - id(active_climate_supported_modes) = climate_supported_modes; - - lv_label_set_text(id(climate_detail_title), title.c_str()); - - char current_buf[48]; - if (isnan(climate_current_temperature)) { - snprintf(current_buf, sizeof(current_buf), "Current: --"); - } else if (id(active_climate_humidity) >= 0) { - snprintf(current_buf, sizeof(current_buf), "Current: %.1f° - Humidity: %d %%", climate_current_temperature, id(active_climate_humidity)); - } else { - snprintf(current_buf, sizeof(current_buf), "Current: %.1f°", climate_current_temperature); - } - lv_label_set_text(id(climate_current_label), current_buf); - - char target_buf[16]; - snprintf(target_buf, sizeof(target_buf), "%.1f°", id(active_climate_target_x10) / 10.0f); - lv_label_set_text(id(climate_target_label), target_buf); - lv_label_set_text(id(climate_mode_label), id(active_climate_mode).c_str()); - - lv_arc_set_range(id(climate_arc), id(active_climate_min_x10), id(active_climate_max_x10)); - lv_arc_set_value(id(climate_arc), id(active_climate_target_x10)); - - if (atoi("${DISPLAY_W}") < atoi("${DISPLAY_H}")) { - lv_obj_set_size(id(climate_arc), 196, 196); - lv_obj_set_y(id(climate_arc), 8); - lv_obj_set_y(id(climate_current_label), 48); - lv_obj_set_y(id(climate_minus_button), -62); - lv_obj_set_y(id(climate_plus_button), -62); - lv_obj_set_y(id(climate_mode_pill), -18); - } else { - lv_obj_set_size(id(climate_arc), 176, 176); - lv_obj_set_y(id(climate_arc), 18); - lv_obj_set_y(id(climate_current_label), 40); - lv_obj_set_y(id(climate_minus_button), -14); - lv_obj_set_y(id(climate_plus_button), -14); - lv_obj_set_y(id(climate_mode_pill), -15); - } - - lv_obj_add_flag(id(brightness_overlay), LV_OBJ_FLAG_HIDDEN); - lv_obj_clear_flag(id(climate_detail_overlay), LV_OBJ_FLAG_HIDDEN); - return; - } - - int pct = off_value; - - if (mode == "brightness") { - id(active_control_kind) = "light_brightness"; - if (was_on) { - if (isnan(brightness_value)) pct = 100; - else pct = (int) lround((brightness_value / 255.0f) * 100.0f); - } - } else if (mode == "percentage") { - id(active_control_kind) = "fan_percentage"; - if (was_on) { - if (isnan(percentage_value)) pct = 0; - else pct = (int) lround(percentage_value); - } - } else if (mode == "cover_position") { - id(active_control_kind) = "cover_position"; - if (isnan(position_value)) pct = was_on ? 100 : 0; - else pct = (int) lround(position_value); - } else { - return; - } - - if (pct < 0) pct = 0; - if (pct > 100) pct = 100; - id(active_brightness_pct) = pct; - - if (tile_type == "light" && std::string("${DIRECT_ACTIONS}") == "true") { - id(active_supports_color) = - contains_mode(supported_color_modes, "hs") || - contains_mode(supported_color_modes, "rgb") || - contains_mode(supported_color_modes, "rgbw") || - contains_mode(supported_color_modes, "rgbww") || - contains_mode(supported_color_modes, "xy"); - id(active_supports_temperature) = contains_mode(supported_color_modes, "color_temp"); - - if (id(active_supports_color) || id(active_supports_temperature)) { - lv_obj_clear_flag(id(overlay_color_button), LV_OBJ_FLAG_HIDDEN); - } - } - - if (tile_type == "cover") { - lv_obj_set_style_bg_color(id(overlay_slider), lv_color_hex(fill_color), LV_PART_MAIN); - lv_obj_set_style_bg_color(id(overlay_slider), lv_color_hex(slider_bg_color), LV_PART_INDICATOR); - lv_obj_set_style_bg_opa(id(overlay_slider), LV_OPA_COVER, LV_PART_MAIN); - } - else{ - lv_obj_set_style_bg_color(id(overlay_slider), lv_color_hex(slider_bg_color), LV_PART_MAIN); - lv_obj_set_style_bg_color(id(overlay_slider), lv_color_hex(fill_color), LV_PART_INDICATOR); - lv_obj_set_style_bg_opa(id(overlay_slider), LV_OPA_COVER, LV_PART_MAIN); - } - lv_obj_set_style_radius(id(overlay_slider), 28, LV_PART_MAIN); - lv_obj_set_style_radius(id(overlay_slider), 0, LV_PART_INDICATOR); - - lv_label_set_text(id(overlay_title), title.c_str()); - lv_label_set_text(id(overlay_icon), icon_text.c_str()); - lv_slider_set_value(id(overlay_slider), pct, LV_ANIM_OFF); - - std::string value_text; - - if (tile_type == "cover") { - if (pct <= 0) { - value_text = label_off; - } else if (pct >= 100) { - value_text = label_on; - } else { - char buf[40]; - snprintf(buf, sizeof(buf), "%s - %d%%", label_on.c_str(), pct); - value_text = buf; - } - } else { - if (pct <= 0) { - value_text = label_off; - } else { - char buf[20]; - snprintf(buf, sizeof(buf), "%d%s", pct, id(active_value_suffix).c_str()); - value_text = buf; - } - } - - lv_label_set_text(id(overlay_value), value_text.c_str()); - - lv_obj_set_style_bg_opa(id(overlay_slider), LV_OPA_COVER, LV_PART_INDICATOR); - - lv_obj_set_style_text_color(id(overlay_icon), lv_color_hex(slider_icon_color), 0); - - - if: - condition: - lambda: 'return id(active_control_kind) != "climate_temperature";' - then: - - lvgl.widget.show: brightness_overlay - - # UI refresh (values + time + styles) + render - - id: ui_refresh - mode: queued - then: - - lambda: |- - auto set_lbl = [](lv_obj_t* lbl, const std::string& s) { - if (lbl) lv_label_set_text(lbl, s.c_str()); - }; - - auto set_value_auto = [&](lv_obj_t* lbl, - bool on, - const std::string& tile_type, - const std::string& value_mode, - float brightness, - float percentage, - const std::string& preset, - float climate_current_temperature, - float climate_target_temperature, - float climate_humidity, - const std::string& label_on, - const std::string& label_off) { - std::string mode = value_mode; - - if (mode == "auto" || mode.empty()) { - if (tile_type == "light") { - mode = "brightness"; - } else if (tile_type == "fan") { - mode = preset.empty() ? "percentage" : "preset"; - } else if (tile_type == "climate") { - mode = "climate"; - } else { - mode = "text"; - } - } - - if (mode == "brightness") { - if (!on) { - set_lbl(lbl, label_off); - return; - } - if (isnan(brightness)) { - set_lbl(lbl, "100 %"); - return; - } - int pct = (int) lround((brightness / 255.0f) * 100.0f); - char buf[8]; - snprintf(buf, sizeof(buf), "%d %%", pct); - set_lbl(lbl, std::string(buf)); - return; - } - - if (mode == "percentage") { - if (!on) { - set_lbl(lbl, label_off); - return; - } - int pct = isnan(percentage) ? 0 : (int) lround(percentage); - char buf[8]; - snprintf(buf, sizeof(buf), "%d %%", pct); - set_lbl(lbl, std::string(buf)); - return; - } - - if (mode == "preset") { - if (!on) { - set_lbl(lbl, label_off); - return; - } - if (!preset.empty()) { - set_lbl(lbl, preset); - } else { - set_lbl(lbl, label_on); - } - return; - } - - if (mode == "climate") { - if (!on) { - set_lbl(lbl, label_off); - return; - } - - float temp = isnan(climate_target_temperature) ? climate_current_temperature : climate_target_temperature; - if (isnan(temp)) { - set_lbl(lbl, label_on); - return; - } - - char buf[16]; - snprintf(buf, sizeof(buf), "%.1f °", temp); - set_lbl(lbl, std::string(buf)); - return; - } - - if (mode == "text") { - if (tile_type == "scene" || tile_type == "script") { - set_lbl(lbl, label_on); - } else { - set_lbl(lbl, on ? label_on : label_off); - } - return; - } - - set_lbl(lbl, "—"); - }; - - // active_state niet leeg? Dan telt alleen die exacte HA-state als "aan". - // Zo kan een input_select ("Vol"/"Leeg") ook een tegel laten oplichten. - auto is_active = [](const std::string& tile_type, const std::string& state, - const std::string& active_state = std::string()) { - if (!active_state.empty()) return state == active_state; - if (tile_type == "climate") { - return !state.empty() && state != "off" && state != "unavailable" && state != "unknown"; - } - if (tile_type == "scene" || tile_type == "script") return true; - return state == "on"; - }; - - // Cover-aware active state: "closed" = off, anything else (open/opening/closing) = on - bool t1_on = (std::string("${TILE1_TYPE}") == "cover") ? (id(tile1_cover_state).state != "closed") : is_active("${TILE1_TYPE}", std::string(id(ha_state_tile1).state.c_str()), "${TILE1_ACTIVE_STATE}"); - bool t2_on = (std::string("${TILE2_TYPE}") == "cover") ? (id(tile2_cover_state).state != "closed") : is_active("${TILE2_TYPE}", std::string(id(ha_state_tile2).state.c_str()), "${TILE2_ACTIVE_STATE}"); - bool t3_on = (std::string("${TILE3_TYPE}") == "cover") ? (id(tile3_cover_state).state != "closed") : is_active("${TILE3_TYPE}", std::string(id(ha_state_tile3).state.c_str()), "${TILE3_ACTIVE_STATE}"); - bool t4_on = (std::string("${TILE4_TYPE}") == "cover") ? (id(tile4_cover_state).state != "closed") : is_active("${TILE4_TYPE}", std::string(id(ha_state_tile4).state.c_str()), "${TILE4_ACTIVE_STATE}"); - bool t5_on = (std::string("${TILE5_TYPE}") == "cover") ? (id(tile5_cover_state).state != "closed") : is_active("${TILE5_TYPE}", std::string(id(ha_state_tile5).state.c_str()), "${TILE5_ACTIVE_STATE}"); - bool t6_on = (std::string("${TILE6_TYPE}") == "cover") ? (id(tile6_cover_state).state != "closed") : is_active("${TILE6_TYPE}", std::string(id(ha_state_tile6).state.c_str()), "${TILE6_ACTIVE_STATE}"); - // Tegel 7/8 hebben geen cover_state-sensor, dus geen cover-tak nodig. - bool t7_on = is_active("${TILE7_TYPE}", std::string(id(ha_state_tile7).state.c_str()), "${TILE7_ACTIVE_STATE}"); - bool t8_on = is_active("${TILE8_TYPE}", std::string(id(ha_state_tile8).state.c_str()), "${TILE8_ACTIVE_STATE}"); - 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}"); - - if (id(active_control_kind) == "climate_temperature" && !lv_obj_has_flag(id(climate_detail_overlay), LV_OBJ_FLAG_HIDDEN)) { - auto label_for_climate_mode = [](std::string mode) { - for (char &c : mode) { - if (c >= 'A' && c <= 'Z') c = (char) (c - 'A' + 'a'); - } - if (mode == "off") return std::string("Off"); - if (mode == "heat") return std::string("Heat"); - if (mode == "cool") return std::string("Cool"); - if (mode == "heat_cool") return std::string("Heat/Cool"); - if (mode == "auto") return std::string("Auto"); - if (mode == "dry") return std::string("Dry"); - if (mode == "fan_only") return std::string("Fan"); - return mode.empty() ? std::string("Auto") : mode; - }; - auto sync_open_climate = [&](const std::string& entity, - float current_temperature, - float target_temperature, - float humidity, - const std::string& climate_mode) { - if (id(active_entity) != entity) return; - auto to_x10 = [](float value, int fallback) -> int { - if (isnan(value)) return fallback; - return (int) lround(value * 10.0f); - }; - - id(active_climate_current_x10) = to_x10(current_temperature, id(active_climate_current_x10)); - id(active_climate_humidity) = isnan(humidity) ? -1 : (int) lround(humidity); - if (!climate_mode.empty()) { - id(active_climate_mode) = climate_mode; - lv_label_set_text(id(climate_mode_label), label_for_climate_mode(climate_mode).c_str()); - } - - int target_x10 = to_x10(target_temperature, id(active_climate_target_x10)); - if (target_x10 < id(active_climate_min_x10)) target_x10 = id(active_climate_min_x10); - if (target_x10 > id(active_climate_max_x10)) target_x10 = id(active_climate_max_x10); - int step_x10 = id(active_climate_step_x10); - if (step_x10 < 5) step_x10 = 5; - target_x10 = id(active_climate_min_x10) + (int) lround((target_x10 - id(active_climate_min_x10)) / (float) step_x10) * step_x10; - if (target_x10 < id(active_climate_min_x10)) target_x10 = id(active_climate_min_x10); - if (target_x10 > id(active_climate_max_x10)) target_x10 = id(active_climate_max_x10); - id(active_climate_target_x10) = target_x10; - - char current_buf[48]; - if (isnan(current_temperature)) { - snprintf(current_buf, sizeof(current_buf), "Current: --"); - } else if (id(active_climate_humidity) >= 0) { - snprintf(current_buf, sizeof(current_buf), "Current: %.1f° - Humidity: %d %%", current_temperature, id(active_climate_humidity)); - } else { - snprintf(current_buf, sizeof(current_buf), "Current: %.1f°", current_temperature); - } - lv_label_set_text(id(climate_current_label), current_buf); - - char target_buf[16]; - snprintf(target_buf, sizeof(target_buf), "%.1f°", id(active_climate_target_x10) / 10.0f); - lv_label_set_text(id(climate_target_label), target_buf); - lv_arc_set_value(id(climate_arc), id(active_climate_target_x10)); - }; - - if (std::string("${TILE1_TYPE}") == "climate") sync_open_climate("${TILE1_ENTITY}", id(tile1_climate_current_temperature).state, id(tile1_climate_target_temperature).state, id(tile1_climate_humidity).state, std::string(id(tile1_climate_mode).state.c_str())); - if (std::string("${TILE2_TYPE}") == "climate") sync_open_climate("${TILE2_ENTITY}", id(tile2_climate_current_temperature).state, id(tile2_climate_target_temperature).state, id(tile2_climate_humidity).state, std::string(id(tile2_climate_mode).state.c_str())); - if (std::string("${TILE3_TYPE}") == "climate") sync_open_climate("${TILE3_ENTITY}", id(tile3_climate_current_temperature).state, id(tile3_climate_target_temperature).state, id(tile3_climate_humidity).state, std::string(id(tile3_climate_mode).state.c_str())); - if (std::string("${TILE4_TYPE}") == "climate") sync_open_climate("${TILE4_ENTITY}", id(tile4_climate_current_temperature).state, id(tile4_climate_target_temperature).state, id(tile4_climate_humidity).state, std::string(id(tile4_climate_mode).state.c_str())); - if (std::string("${TILE5_TYPE}") == "climate") sync_open_climate("${TILE5_ENTITY}", id(tile5_climate_current_temperature).state, id(tile5_climate_target_temperature).state, id(tile5_climate_humidity).state, std::string(id(tile5_climate_mode).state.c_str())); - if (std::string("${TILE6_TYPE}") == "climate") sync_open_climate("${TILE6_ENTITY}", id(tile6_climate_current_temperature).state, id(tile6_climate_target_temperature).state, id(tile6_climate_humidity).state, std::string(id(tile6_climate_mode).state.c_str())); - } - - { - auto now = id(homeassistant_time).now(); - char buf[10]; - - if (std::string("${TIME_24H}") == "true") { - snprintf(buf, sizeof(buf), "%02d:%02d", now.hour, now.minute); - } else { - int hour12 = now.hour % 12; - if (hour12 == 0) hour12 = 12; - snprintf(buf, sizeof(buf), "%d:%02d %s", hour12, now.minute, now.hour >= 12 ? "PM" : "AM"); - } - - set_lbl(id(lbl_time), std::string(buf)); - } - - // 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) - { - // ${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}); - - - component.update: my_display diff --git a/installers/cyd.yaml b/installers/cyd.yaml index 1f43cf0..86c6c80 100644 --- a/installers/cyd.yaml +++ b/installers/cyd.yaml @@ -1,8 +1,8 @@ -# Handmatig alternatief: voeg dit blok toe aan de ESPHome-wizardconfiguratie. -# Verwijder het oude esp32:-blok; behoud je eigen esphome:, api:, ota: en wifi:. -# De makkelijke route genereert een compleet eigen bestand: ESP Screen Manager. +# Manual alternative: add this block to the ESPHome wizard configuration. +# Remove the old esp32: block; keep your own esphome:, api:, ota: and wifi:. +# The easy route generates a complete file of its own: ESP Screen Manager. packages: - scherm: + screen: url: https://github.com/MaxGramser/homeassistant_espscreen ref: main files: [packages/cyd.yaml] diff --git a/installers/guition.yaml b/installers/guition.yaml index 6306201..35347b7 100644 --- a/installers/guition.yaml +++ b/installers/guition.yaml @@ -1,8 +1,8 @@ -# Handmatig alternatief: voeg dit blok toe aan de ESPHome-wizardconfiguratie. -# Verwijder het oude esp32:-blok; behoud je eigen esphome:, api:, ota: en wifi:. -# De makkelijke route genereert een compleet eigen bestand: ESP Screen Manager. +# Manual alternative: add this block to the ESPHome wizard configuration. +# Remove the old esp32: block; keep your own esphome:, api:, ota: and wifi:. +# The easy route generates a complete file of its own: ESP Screen Manager. packages: - scherm: + screen: url: https://github.com/MaxGramser/homeassistant_espscreen ref: main files: [packages/guition.yaml] diff --git a/packages/cyd.yaml b/packages/cyd.yaml index fc35cb3..21b9167 100644 --- a/packages/cyd.yaml +++ b/packages/cyd.yaml @@ -44,7 +44,6 @@ # light -> brightness, fan -> percentage/preset, scene/script -> text # # Device configuration: home-like-2432s028.yaml (ESP32-2432S028). -# home-like.yaml targets the separate ST7789V/GT911 board. # Credentials stay in secrets.yaml. Fonts are bundled for offline validation. # Start at README.md; docs/ covers calibration, HA setup and acceptance tests. # @@ -59,7 +58,7 @@ ############################################################ substitutions: - SCREEN_FIRMWARE_VERSION: "0.2.33" + SCREEN_FIRMWARE_VERSION: "0.2.34" # A tap survives a finger that drifts up to this far (320 px over 58 mm: 1 cm); further is a swipe. TOUCH_MOVE_LIMIT_PX: "56" # Shortest contact that counts as a tap; a resistive panel bounces, a capacitive one does not. @@ -67,7 +66,7 @@ substitutions: DYNAMIC_TILES: "true" DASHBOARD_INBOX_INTERNAL: "false" DEVICE_NAME: "cyd-new" - DEVICE_FRIENDLY_NAME: "Mijn CYD" + DEVICE_FRIENDLY_NAME: "My CYD" CALIBRATION_ON_BOOT: "false" TOUCH_AFFINE_XX: "1.000000000" TOUCH_AFFINE_XY: "0.000000000" @@ -79,7 +78,7 @@ substitutions: TILE_COUNT: "1" # 1-10; <=6 hides page navigation DIRECT_ACTIONS: "true" # true = call Home Assistant services directly, false = only emit action events - ROOM_NAME: "Kies tegels in HA" # room name shown in the top-left header + ROOM_NAME: "Choose tiles in HA" # room name shown in the top-left header TIME_24H: "true" # true = 24h format, false = 12h format ## DIM settings @@ -93,24 +92,23 @@ substitutions: NIGHT_DIM_BRIGHTNESS: "10" # backlight brightness in percent at night # ============================================================ - # ACCENTKLEUREN — automatisch bepaald uit het Home Assistant-domein + # ACCENT COLOURS — determined automatically from the Home Assistant domain # - # Het domein is het stuk voor de punt in het entity_id: - # script.goede_morgen -> "script" -> ACCENT_ACTION - # light.woonkamer_2 -> "light" -> ACCENT_LIGHT + # The domain is the part before the dot in the entity_id: + # script.good_morning -> "script" -> ACCENT_ACTION + # light.living_room_2 -> "light" -> ACCENT_LIGHT # vacuum.s8 -> "vacuum" -> ACCENT_DEVICE # - # 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. + # That's Home Assistant's own grouping, so a tile automatically gets the + # right colour as soon as you put an entity in it — you no longer have + # to pick colours per tile. The distinction that matters: a script/scene + # fires, a device you turn on or off. Those two shouldn't look the same. # - # 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). + # Set TILE*_CIRCLE_ACTIVE_COLOR / TILE*_ICON_DISABLED_COLOR to + # ${ACCENT_AUTO} to use this, or to a real hex value to colour that + # one tile by hand (see tile 8 — red as soon as there's mail). # ============================================================ - ACCENT_AUTO: "0x1000000" # sentinel: bewust buiten het 24-bits kleurbereik + ACCENT_AUTO: "0x1000000" # sentinel: deliberately outside the 24-bit colour range ACCENT_ACTION: "0x8B5CF6" # script / scene / button / input_button / automation ACCENT_LIGHT: "0xFEC600" # light ACCENT_DEVICE: "0x00C5EC" # switch, fan, cover, climate, vacuum, media_player, rest @@ -126,16 +124,16 @@ substitutions: # ============================================================ # --- 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_ENTITY: "light.example_lamp" # main Home Assistant entity for this tile + TILE1_STATE_ENTITY: "light.example_lamp" # entity used for active/inactive state styling TILE1_TITLE: "Lamp" # tile title shown in the UI TILE1_ICON: "\U000F001B" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) TILE1_TYPE: "light" # tile type: light | fan | switch | scene | script | cover | climate TILE1_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset | custom TILE1_LONGPRESS: "slider" # long press mode: none | slider | action TILE1_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text - TILE1_LABEL_OFF: "Uit" # text shown when entity is off - TILE1_LABEL_ON: "Aan" # text shown for text-mode tiles when entity is on + TILE1_LABEL_OFF: "Off" # text shown when entity is off + TILE1_LABEL_ON: "On" # text shown for text-mode tiles when entity is on TILE1_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") # --- colors --- @@ -175,16 +173,16 @@ substitutions: # ============================================================ # --- 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_ENTITY: "switch.example_plug" # main Home Assistant entity for this tile + TILE2_STATE_ENTITY: "switch.example_plug" # entity used for active/inactive state styling + TILE2_TITLE: "Plug" # tile title shown in the UI TILE2_ICON: "\U000F0387" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) TILE2_TYPE: "switch" # tile type: light | fan | switch | scene | script | cover | climate TILE2_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset | custom TILE2_LONGPRESS: "none" # long press mode: none | slider | action TILE2_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text - TILE2_LABEL_OFF: "Uit" # text shown when entity is off - TILE2_LABEL_ON: "Aan" # text shown for text-mode tiles when entity is on + TILE2_LABEL_OFF: "Off" # text shown when entity is off + TILE2_LABEL_ON: "On" # text shown for text-mode tiles when entity is on TILE2_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") # --- colors --- @@ -224,16 +222,16 @@ substitutions: # ============================================================ # --- 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_ENTITY: "scene.example_evening" # main Home Assistant entity for this tile + TILE3_STATE_ENTITY: "scene.example_evening" # entity used for active/inactive state styling + TILE3_TITLE: "Evening" # tile title shown in the UI TILE3_ICON: "\U000F02E3" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) TILE3_TYPE: "scene" # tile type: light | fan | switch | scene | script | cover | climate TILE3_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset | custom TILE3_LONGPRESS: "none" # long press mode: none | slider | action TILE3_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text - TILE3_LABEL_OFF: "Uit" # text shown when entity is off - TILE3_LABEL_ON: "Aan" # text shown for text-mode tiles when entity is on + TILE3_LABEL_OFF: "Off" # text shown when entity is off + TILE3_LABEL_ON: "On" # text shown for text-mode tiles when entity is on TILE3_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") # --- colors --- @@ -273,16 +271,16 @@ substitutions: # ============================================================ # --- 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_ENTITY: "script.example_all_off" # main Home Assistant entity for this tile + TILE4_STATE_ENTITY: "script.example_all_off" # entity used for active/inactive state styling + TILE4_TITLE: "All off" # tile title shown in the UI TILE4_ICON: "\U000F059C" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) TILE4_TYPE: "script" # tile type: light | fan | switch | scene | script | cover | climate TILE4_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset | custom TILE4_LONGPRESS: "none" # long press mode: none | slider | action TILE4_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text - TILE4_LABEL_OFF: "Uit" # text shown when entity is off - TILE4_LABEL_ON: "Aan" # text shown for text-mode tiles when entity is on + TILE4_LABEL_OFF: "Off" # text shown when entity is off + TILE4_LABEL_ON: "On" # text shown for text-mode tiles when entity is on TILE4_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") # --- colors --- @@ -322,16 +320,16 @@ substitutions: # ============================================================ # --- 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_ENTITY: "climate.example_ac" # main Home Assistant entity for this tile + TILE5_STATE_ENTITY: "climate.example_ac" # entity used for active/inactive state styling + TILE5_TITLE: "AC" # tile title shown in the UI TILE5_ICON: "\U000F0425" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) TILE5_TYPE: "climate" # tile type: light | fan | switch | scene | script | cover | climate TILE5_TAP_ACTION: "toggle" # tap action: auto | toggle | activate | fan_toggle_preset | custom TILE5_LONGPRESS: "slider" # long press mode: none | slider | action TILE5_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text - TILE5_LABEL_OFF: "Uit" # text shown when entity is off (closed) - TILE5_LABEL_ON: "Aan" # text shown when entity is on (open) + TILE5_LABEL_OFF: "Off" # text shown when entity is off (closed) + TILE5_LABEL_ON: "On" # text shown when entity is on (open) TILE5_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") # --- colors --- @@ -371,16 +369,16 @@ substitutions: # ============================================================ # --- 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_ENTITY: "vacuum.example_robot" # main Home Assistant entity for this tile + TILE6_STATE_ENTITY: "vacuum.example_robot" # entity used for active/inactive state styling + TILE6_TITLE: "Vacuum" # tile title shown in the UI TILE6_ICON: "\U000F070D" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) TILE6_TYPE: "vacuum" # tile type: light | fan | switch | scene | script | cover | climate TILE6_TAP_ACTION: "custom" # tap action: auto | toggle | activate | fan_toggle_preset | custom TILE6_LONGPRESS: "vacuum" # long press mode: none | slider | action TILE6_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text - TILE6_LABEL_OFF: "Uit" # text shown when entity is off - TILE6_LABEL_ON: "Aan" # text shown for text-mode tiles when entity is on + TILE6_LABEL_OFF: "Off" # text shown when entity is off + TILE6_LABEL_ON: "On" # text shown for text-mode tiles when entity is on TILE6_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") # --- colors --- @@ -414,22 +412,22 @@ substitutions: 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. + # TILE 7 — Table lamp + # Brightness-only lamp (HA reports supported_color_modes: ['brightness']), + # so the colour button in the slider overlay stays hidden automatically. # ============================================================ # --- basic settings --- - TILE7_ENTITY: "light.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_ENTITY: "light.unused_7" # main Home Assistant entity for this tile + TILE7_STATE_ENTITY: "light.unused_7" # entity used for active/inactive state styling + TILE7_TITLE: "Tile 7" # tile title shown in the UI TILE7_ICON: "\U000F06B5" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) TILE7_TYPE: "light" # tile type: light | fan | switch | scene | script | cover | climate TILE7_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset TILE7_LONGPRESS: "none" # long press mode: none | slider | action | vacuum TILE7_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text - TILE7_LABEL_OFF: "Uit" # text shown when entity is off - TILE7_LABEL_ON: "Aan" # text shown for text-mode tiles when entity is on + TILE7_LABEL_OFF: "Off" # text shown when entity is off + TILE7_LABEL_ON: "On" # text shown for text-mode tiles when entity is on TILE7_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") # --- colors --- @@ -449,7 +447,7 @@ substitutions: 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_SLIDER: "auto" # auto = brightness for a light TILE7_LONGPRESS_OFF_VALUE: "0" # initial slider value when entity is off # if LONGPRESS = action: TILE7_LONGPRESS_ACTION: "" # entity_id to fire on long press @@ -457,27 +455,27 @@ substitutions: 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. + # TILE 8 — Mailbox reset + # input_select.mailbox_status has options ['Full', 'Empty']. + # The tile lights up as soon as the status is "Full" (ACTIVE_STATE), and a + # short tap sets it back to "Empty" via input_select.select_option. # ============================================================ # --- basic settings --- - TILE8_ENTITY: "light.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_ENTITY: "light.unused_8" # main Home Assistant entity for this tile + TILE8_STATE_ENTITY: "light.unused_8" # entity used for active/inactive state styling + TILE8_TITLE: "Tile 8" # tile title shown in the UI TILE8_ICON: "\U000F06EE" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) - TILE8_TYPE: "light" # 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 + TILE8_TYPE: "light" # generic type; the real behavior comes from TAP_SERVICE below + TILE8_TAP_ACTION: "auto" # activate + TAP_SERVICE = a standalone service call + TILE8_LONGPRESS: "none" # long press only sends the event to HA + TILE8_VALUE_MODE: "auto" # shows LABEL_ON / LABEL_OFF + TILE8_LABEL_OFF: "Off" # text shown when entity is off / inactive + TILE8_LABEL_ON: "On" # text shown when entity is active + TILE8_ACTIVE_STATE: "" # tile is "on" as soon as the status is exactly "Full" # --- colors --- - TILE8_CIRCLE_ACTIVE_COLOR: "0xE5484D" # rood zodra er post ligt + TILE8_CIRCLE_ACTIVE_COLOR: "0xE5484D" # red as soon as there's mail TILE8_CIRCLE_DISABLED_COLOR: "0x7B7B6F" # icon circle color when inactive TILE8_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active TILE8_ICON_DISABLED_COLOR: "0xE5484D" # icon color when inactive @@ -489,35 +487,35 @@ substitutions: TILE8_VALUE_DISABLED_COLOR: "0xD9D9D9" # value color when inactive # --- advanced settings --- - TILE8_TAP_SERVICE: "" # zet de input_select op een vaste optie - TILE8_TAP_PARAM_KEY: "" # parameternaam voor input_select.select_option - TILE8_TAP_PARAM_VAL: "" # de "reset"-waarde + TILE8_TAP_SERVICE: "" # sets the input_select to a fixed option + TILE8_TAP_PARAM_KEY: "" # parameter name for input_select.select_option + TILE8_TAP_PARAM_VAL: "" # the "reset" value # if LONGPRESS = slider: - TILE8_LONGPRESS_SLIDER: "auto" # niet in gebruik (LONGPRESS = none) - TILE8_LONGPRESS_OFF_VALUE: "0" # niet in gebruik + TILE8_LONGPRESS_SLIDER: "auto" # not in use (LONGPRESS = none) + TILE8_LONGPRESS_OFF_VALUE: "0" # not in use # if LONGPRESS = action: TILE8_LONGPRESS_ACTION: "" # entity_id to fire on long press TILE8_LONGPRESS_ACTION_TYPE: "" # entity type (empty = same as TILE*_TYPE) TILE8_LONGPRESS_ACTION_SERVICE: "" # service to call (default: activate/turn_on) # ============================================================ - # TILE 9 — 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. + # TILE 9 — Living room + # Moved from tile 2 to row 5 to make room for Ambient. + # Colour lamp (color_temp / rgb / xy), so the colour button in the + # slider overlay appears automatically. # ============================================================ # --- basic settings --- - TILE9_ENTITY: "light.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_ENTITY: "light.unused_9" # main Home Assistant entity for this tile + TILE9_STATE_ENTITY: "light.unused_9" # entity used for active/inactive state styling + TILE9_TITLE: "Tile 9" # tile title shown in the UI TILE9_ICON: "\U000F04B9" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) TILE9_TYPE: "light" # tile type: light | fan | switch | scene | script | cover | climate TILE9_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset TILE9_LONGPRESS: "none" # long press mode: none | slider | action | vacuum TILE9_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text - TILE9_LABEL_OFF: "Uit" # text shown when entity is off - TILE9_LABEL_ON: "Aan" # text shown for text-mode tiles when entity is on + TILE9_LABEL_OFF: "Off" # text shown when entity is off + TILE9_LABEL_ON: "On" # text shown for text-mode tiles when entity is on TILE9_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") # --- colors --- @@ -537,7 +535,7 @@ substitutions: 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_SLIDER: "auto" # auto = brightness for a light TILE9_LONGPRESS_OFF_VALUE: "0" # initial slider value when entity is off # if LONGPRESS = action: TILE9_LONGPRESS_ACTION: "" # entity_id to fire on long press @@ -545,26 +543,26 @@ substitutions: 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. + # TILE 10 — Alarm on/off + # input_boolean.alarm_engaged. An input_boolean doesn't respond to + # switch.toggle, hence the explicit TAP_SERVICE. # ============================================================ # --- basic settings --- - TILE10_ENTITY: "light.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_ENTITY: "light.unused_10" # main Home Assistant entity for this tile + TILE10_STATE_ENTITY: "light.unused_10" # entity used for active/inactive state styling + TILE10_TITLE: "Tile 10" # tile title shown in the UI TILE10_ICON: "\U000F002A" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) - TILE10_TYPE: "light" # generiek aan/uit-type; de service hieronder doet het echte werk + TILE10_TYPE: "light" # generic on/off type; the service below does the real work TILE10_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset - TILE10_LONGPRESS: "none" # long press 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 + TILE10_LONGPRESS: "none" # long press only sends the event to HA + TILE10_VALUE_MODE: "auto" # shows LABEL_ON / LABEL_OFF + TILE10_LABEL_OFF: "Off" # text shown when entity is off + TILE10_LABEL_ON: "On" # text shown when entity is on + TILE10_ACTIVE_STATE: "" # input_boolean just uses on/off # --- colors --- - TILE10_CIRCLE_ACTIVE_COLOR: "${ACCENT_AUTO}" # input_boolean -> ACCENT_DEVICE (cyaan) + TILE10_CIRCLE_ACTIVE_COLOR: "${ACCENT_AUTO}" # input_boolean -> ACCENT_DEVICE (cyan) TILE10_CIRCLE_DISABLED_COLOR: "0x7B7B6F" # icon circle color when inactive TILE10_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active TILE10_ICON_DISABLED_COLOR: "${ACCENT_AUTO}" # icon color when inactive @@ -576,12 +574,12 @@ substitutions: TILE10_VALUE_DISABLED_COLOR: "0xD9D9D9" # value color when inactive # --- advanced settings --- - TILE10_TAP_SERVICE: "" # switch.toggle werkt niet op een input_boolean + TILE10_TAP_SERVICE: "" # switch.toggle doesn't work on an input_boolean TILE10_TAP_PARAM_KEY: "" # optional extra service parameter key TILE10_TAP_PARAM_VAL: "" # optional extra service parameter value # if LONGPRESS = slider: - TILE10_LONGPRESS_SLIDER: "auto" # niet in gebruik (LONGPRESS = none) - TILE10_LONGPRESS_OFF_VALUE: "0" # niet in gebruik + TILE10_LONGPRESS_SLIDER: "auto" # not in use (LONGPRESS = none) + TILE10_LONGPRESS_OFF_VALUE: "0" # not in use # if LONGPRESS = action: TILE10_LONGPRESS_ACTION: "" # entity_id to fire on long press TILE10_LONGPRESS_ACTION_TYPE: "" # entity type (empty = same as TILE*_TYPE) @@ -610,8 +608,8 @@ substitutions: DISPLAY_H: "240" LVGL_ROTATION: "90" TOUCH_SWAP_XY: "false" - # true op dit bord (resistieve XPT2046) — was false op het 032c-bord - # (capacitieve GT911). Dit is de originele upstream-waarde voor dit paneel. + # true on this board (resistive XPT2046) — was false on the 032c board + # (capacitive GT911). This is the original upstream value for this panel. TOUCH_MIRROR_X: "true" TOUCH_MIRROR_Y: "false" # Measured on this board: median top-right raw=(523,3390), @@ -651,9 +649,9 @@ substitutions: ALERT_BUTTON_H: "40" ALERT_BUTTON_INSET: "14" ALERT_TITLE_MAX: "48" # bytes; the label ends one line with an ellipsis - ALERT_BUTTON_MAX: "12" # bytes on the Oké button + ALERT_BUTTON_MAX: "12" # bytes on the OK button ALERT_SUBTITLE_MAX: "160" # bytes; wraps over the rows the card has - BG_TOP_COLOR: "0xE7E7E7" # lichte achtergrond, gelijk aan de Guition + BG_TOP_COLOR: "0xE7E7E7" # light background, matching the Guition BG_BOTTOM_COLOR: "0xE7E7E7" TILE_ICON_X: "0" TILE_ICON_Y: "0" @@ -689,8 +687,8 @@ substitutions: # OVERLAY_SLIDER_W: "96" # OVERLAY_SLIDER_H: "200" # OVERLAY_ICON_Y: "94" - # BG_TOP_COLOR: "0xAEC4D9" # achtergrond boven (zacht blauw) - # BG_BOTTOM_COLOR: "0xD6BB9E" # achtergrond onder (warm zand) + # BG_TOP_COLOR: "0xAEC4D9" # background top (soft blue) + # BG_BOTTOM_COLOR: "0xD6BB9E" # background bottom (warm sand) # TILE_ICON_X: "0" # TILE_ICON_Y: "0" # TILE_TITLE_X: "4" @@ -725,8 +723,8 @@ substitutions: # OVERLAY_SLIDER_W: "96" # OVERLAY_SLIDER_H: "160" # OVERLAY_ICON_Y: "74" - # BG_TOP_COLOR: "0xAEC4D9" # achtergrond boven (zacht blauw) - # BG_BOTTOM_COLOR: "0xD6BB9E" # achtergrond onder (warm zand) + # BG_TOP_COLOR: "0xAEC4D9" # background top (soft blue) + # BG_BOTTOM_COLOR: "0xD6BB9E" # background bottom (warm sand) # TILE_ICON_X: "0" # TILE_ICON_Y: "0" # TILE_TITLE_X: "48" @@ -761,8 +759,8 @@ substitutions: # OVERLAY_SLIDER_W: "96" # OVERLAY_SLIDER_H: "200" # OVERLAY_ICON_Y: "94" - # BG_TOP_COLOR: "0xAEC4D9" # achtergrond boven (zacht blauw) - # BG_BOTTOM_COLOR: "0xD6BB9E" # achtergrond onder (warm zand) + # BG_TOP_COLOR: "0xAEC4D9" # background top (soft blue) + # BG_BOTTOM_COLOR: "0xD6BB9E" # background bottom (warm sand) # TILE_ICON_X: "0" # TILE_ICON_Y: "0" # TILE_TITLE_X: "4" @@ -893,7 +891,7 @@ esphome: id(active_climate_swing_modes) = t.swing_modes; id(active_climate_swing_mode) = t.swing_mode; id(open_value_overlay).execute(t.name, t.entity, t.domain(), "auto", t.active(), 0, t.brightness, t.percentage, t.position, runtime_tiles::icon_for(t), - 0xD5EEFC, 0x00A6ED, 0x1B1B1B, "Uit", "Aan", t.modes, + 0xD5EEFC, 0x00A6ED, 0x1B1B1B, "Off", "On", t.modes, t.current, t.target, t.humidity, t.state, t.hvac_modes, std::to_string(t.minimum), std::to_string(t.maximum), std::to_string(t.step)); }; @@ -909,8 +907,8 @@ esphome: id(active_climate_fan_modes) = t.fan_modes; id(active_climate_fan_mode) = t.fan_mode; id(active_climate_swing_modes) = t.swing_modes; id(active_climate_swing_mode) = t.swing_mode; char buf[72]; - if (std::isfinite(t.current)) snprintf(buf, sizeof(buf), "Nu: %.1f°", t.current); - else snprintf(buf, sizeof(buf), "Nu: --"); + if (std::isfinite(t.current)) snprintf(buf, sizeof(buf), "Now: %.1f°", t.current); + else snprintf(buf, sizeof(buf), "Now: --"); lv_label_set_text(id(climate_current_label), buf); if (std::isfinite(t.target)) { int target = cyd::quantize_temperature(lround(t.target * 10), id(active_climate_min_x10), id(active_climate_max_x10), id(active_climate_step_x10)); @@ -958,19 +956,19 @@ esp32: framework: type: esp-idf advanced: - # ESPHome adviseert dit zelf in de boot-log. Levert ~40KB extra IRAM - # en een kleinere binary op, en op dit bord telt elke KB. + # ESPHome recommends this itself in the boot log. Yields ~40KB extra IRAM + # and a smaller binary, and every KB counts on this board. sram1_as_iram: true - # minimum_chip_revision bewust weggelaten: dat stond hier voor het - # 032c-bord (rev 3.1 bevestigd), maar dit is een ander fysiek bordje - # met een onbekende chip-revisie. Met een onterecht hoge minimum - # weigert de firmware simpelweg te booten. + # minimum_chip_revision deliberately left out: that was here for the + # 032c board (rev 3.1 confirmed), but this is a different physical board + # with an unknown chip revision. With an unjustifiably high minimum + # the firmware simply refuses to boot. version: recommended variant: esp32 logger: - # Voor diagnose tijdelijk op DEBUG of VERBOSE zetten. Let op: ESPHome logt - # zijn configuratieregels op niveau CONFIG, die zijn op INFO onzichtbaar. + # Set to DEBUG or VERBOSE temporarily for diagnosis. Note: ESPHome logs + # its configuration lines at CONFIG level, which are invisible at INFO. level: DEBUG logs: sensor: WARN @@ -1052,7 +1050,7 @@ api: then: - script.execute: ui_self_test # Home Assistant automations: esphome._show_alert puts a card over the whole - # screen (README, "Alert vanuit een automatisering"); dismiss_alert removes it again. + # screen (README, "Alert from an automation"); dismiss_alert removes it again. - action: show_alert variables: title: string @@ -1171,10 +1169,10 @@ globals: restore_value: no initial_value: '0' - # Debounce voor do_tile_action: dit resistieve XPT2046-paneel geeft soms - # meerdere losse press/release-cycli voor één fysieke aanraking (ruis in - # het weerstandslaagje), waardoor dezelfde HA-actie meerdere keren binnen - # een paar honderd ms vuurt (gezien: script.ambient_morning 6x in 8s). + # Debounce for do_tile_action: this resistive XPT2046 panel sometimes gives + # multiple separate press/release cycles for one physical touch (noise in + # the resistive layer), causing the same HA action to fire multiple times + # within a few hundred ms (observed: script.ambient_morning 6x in 8s). - id: last_action_entity type: std::string restore_value: no @@ -1542,8 +1540,8 @@ font: size: 12 glyphs: *tile_icons -# Geheugen zichtbaar maken: dit bord heeft geen PSRAM, dus de vrije heap -# bepaalt hoeveel UI erbij kan. Verschijnt ook als entity in Home Assistant. +# Make memory visible: this board has no PSRAM, so the free heap determines +# how much UI still fits. Also shows up as an entity in Home Assistant. debug: # Heap figures every five minutes: enough to spot a leak, few recorder rows. update_interval: 300s @@ -1570,7 +1568,7 @@ sensor: # "s" in its registry and would refuse a timestamp under that name. - platform: uptime type: timestamp - name: "Opgestart" + name: "Last boot" id: boot_time - platform: template @@ -1633,16 +1631,16 @@ sensor: id: tile6_percentage on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - # Tegel 7 (Tafellamp) is een alleen-helderheid light: alleen brightness. - # Bewust geen percentage / cover_position / climate-sensors — elke - # HA-subscription kost heap op een bord zonder PSRAM. + # Tile 7 (Table lamp) is a brightness-only light: just brightness. + # Deliberately no percentage / cover_position / climate sensors — every + # HA subscription costs heap on a board without PSRAM. - platform: template update_interval: never id: tile7_brightness on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - # Tegel 9 (Woonkamer) is een kleurenlamp: brightness + supported_color_modes. - # Geen percentage / cover / climate — die leest deze tegel nooit. + # Tile 9 (Living room) is a colour lamp: brightness + supported_color_modes. + # No percentage / cover / climate — this tile never reads those. - platform: template update_interval: never id: tile9_brightness @@ -1788,19 +1786,19 @@ binary_sensor: # ---------------------------------------------------------- text_sensor: - platform: template - name: "Schermfirmware" + name: "Screen firmware" entity_category: diagnostic lambda: 'return {"${SCREEN_FIRMWARE_VERSION}"};' update_interval: 60s # ESP Screen Manager matches the screen to its YAML profile and OTA address. - platform: template - name: "Apparaatnaam" + name: "Device name" entity_category: diagnostic lambda: 'return {"${DEVICE_NAME}"};' update_interval: 60s - platform: wifi_info ip_address: - name: "IP-adres" + name: "IP address" entity_category: diagnostic - platform: template id: smartdisplay_action @@ -1857,14 +1855,14 @@ text_sensor: id: ha_state_tile10 on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - # Woonkamer meldt color_temp/rgb/xy, dus hier verschijnt de kleurknop wel. + # Living room reports color_temp/rgb/xy, so the colour button does appear here. - platform: template update_interval: never id: tile9_supported_color_modes on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - # Gebruikt door de slider-overlay om te bepalen of de kleurknop nodig is. - # Deze lamp meldt ['brightness'], dus de knop blijft vanzelf verborgen. + # Used by the slider overlay to decide whether the colour button is needed. + # This lamp reports ['brightness'], so the button stays hidden automatically. - platform: template update_interval: never id: tile7_supported_color_modes @@ -2021,8 +2019,8 @@ switch: # Two SPI buses used by the CYD: # - lcd: built-in display # - my_touchscreen: built-in touch controller -# Twee SPI bussen op de 2432S028: een voor het display, een losse voor de -# resistieve XPT2046 touch (geen I2C op dit bord, in tegenstelling tot 032c). +# Two SPI buses on the 2432S028: one for the display, a separate one for the +# resistive XPT2046 touch (no I2C on this board, unlike the 032c). spi: - id: lcd clk_pin: GPIO14 @@ -2037,12 +2035,12 @@ output: - platform: ledc pin: GPIO21 id: gpio_backlight_pwm - # ESPHome's ledc default is 1kHz. Bij stilstand zie je dat niet (ruim - # boven wat het oog als flikkering waarneemt), maar bij beweging van het - # scherm onthult motion-blur de aan/uit-pulsen als strepen (zelfde effect - # als een PWM-LED die "schokt" als je hem beweegt). 20kHz zit ruim boven - # de snelheid waarbij beweging dat nog zichtbaar maakt, en laat nog altijd - # ruim voldoende duty-cycle resolutie over voor vloeiend dimmen. + # ESPHome's ledc default is 1kHz. You don't see that while still (well + # above what the eye perceives as flicker), but moving the screen reveals + # motion blur turning the on/off pulses into streaks (the same effect as + # a PWM LED that "judders" when you move it). 20kHz sits well above the + # speed at which motion still makes that visible, and still leaves ample + # duty-cycle resolution left over for smooth dimming. frequency: 20000Hz - platform: ledc @@ -2073,7 +2071,7 @@ light: blue: output_blue restore_mode: ALWAYS_OFF -# Touchscreen configuration (XPT2046, resistief, via SPI) +# Touchscreen configuration (XPT2046, resistive, via SPI) # XPT2046 calibration is board-specific. Poll at 50Hz; LVGL rotates touch # with the display. GPIO36 IRQ is deliberately unused (driver requests a # pull-up that this input-only pin cannot provide). @@ -2148,23 +2146,23 @@ lvgl: touchscreens: - touchscreen_id: ts_touch - # De ESP32-WROOM-32 heeft geen PSRAM; een volledige framebuffer (320x240x2 - # = 153600 bytes) past niet in DRAM. 25% is de gebruikelijke waarde. - # 12% in plaats van 25%: laat ~20KB extra aaneengesloten geheugen vrij, - # en LVGL schaalt de stroken van een ARGB-laag mee met de beschikbare - # tekenruimte, dus ook de gevraagde allocaties worden kleiner. + # The ESP32-WROOM-32 has no PSRAM; a full framebuffer (320x240x2 + # = 153600 bytes) doesn't fit in DRAM. 25% is the usual value. + # 12% instead of 25%: frees up ~20KB of extra contiguous memory, + # and LVGL scales the strips of an ARGB layer along with the available + # drawing space, so the requested allocations get smaller too. buffer_size: 12% - # 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. + # Direct local feedback on the WHOLE UI, not just the tiles. + # A theme sets the style per widget type, so this covers all + # buttons, tabs, sliders and the thermostat ring in one go. Non-clickable + # widgets never enter the pressed state, so those stay untouched. theme: obj: pressed: - # bg_opa 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 and NOT opa: `opa` at widget level forces LVGL to render the + # element to a separate layer, and that layer costs tens of KB's + # per pressed widget. That ran the memory out here. bg_opa: 45% slider: knob: @@ -2179,9 +2177,9 @@ lvgl: 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. + # Background: two-stop vertical gradient instead of the 320x240 + # RGB565 wallpaper. Saves ~150 KB flash and an image blit per redraw, + # which you can see directly in how smoothly it scrolls. - id: style_bg_grad bg_color: ${BG_TOP_COLOR} bg_grad_color: ${BG_BOTTOM_COLOR} @@ -2320,7 +2318,7 @@ lvgl: y: ${ALERT_TITLE_Y} width: ${ALERT_TEXT_W} long_mode: DOT - text: "Melding" + text: "Notification" text_font: headline text_color: 0x1B1B1B clickable: false @@ -2365,7 +2363,7 @@ lvgl: - label: id: alert_ok_label align: CENTER - text: "Oké" + text: "OK" text_font: headline text_color: 0xFFFFFF clickable: false @@ -2434,8 +2432,8 @@ lvgl: 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. + # Direct local feedback: LVGL applies this as soon as the finger + # touches the glass, in the same frame. Independent of Home Assistant. pressed: bg_opa: 45% widgets: @@ -2544,8 +2542,8 @@ lvgl: 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. + # Long-press mode "vacuum": opens the vacuum controls. + # Falls outside the action/slider chain, so that stays unchanged. - if: condition: lambda: "return std::string(\"${TILE1_LONGPRESS}\") == \"vacuum\";" @@ -2565,8 +2563,8 @@ lvgl: 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. + # Direct local feedback: LVGL applies this as soon as the finger + # touches the glass, in the same frame. Independent of Home Assistant. pressed: bg_opa: 45% widgets: @@ -2675,8 +2673,8 @@ lvgl: 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. + # Long-press mode "vacuum": opens the vacuum controls. + # Falls outside the action/slider chain, so that stays unchanged. - if: condition: lambda: "return std::string(\"${TILE2_LONGPRESS}\") == \"vacuum\";" @@ -2696,8 +2694,8 @@ lvgl: 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. + # Direct local feedback: LVGL applies this as soon as the finger + # touches the glass, in the same frame. Independent of Home Assistant. pressed: bg_opa: 45% widgets: @@ -2806,8 +2804,8 @@ lvgl: 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. + # Long-press mode "vacuum": opens the vacuum controls. + # Falls outside the action/slider chain, so that stays unchanged. - if: condition: lambda: "return std::string(\"${TILE3_LONGPRESS}\") == \"vacuum\";" @@ -2827,8 +2825,8 @@ lvgl: 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. + # Direct local feedback: LVGL applies this as soon as the finger + # touches the glass, in the same frame. Independent of Home Assistant. pressed: bg_opa: 45% widgets: @@ -2937,8 +2935,8 @@ lvgl: 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. + # Long-press mode "vacuum": opens the vacuum controls. + # Falls outside the action/slider chain, so that stays unchanged. - if: condition: lambda: "return std::string(\"${TILE4_LONGPRESS}\") == \"vacuum\";" @@ -2958,8 +2956,8 @@ lvgl: 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. + # Direct local feedback: LVGL applies this as soon as the finger + # touches the glass, in the same frame. Independent of Home Assistant. pressed: bg_opa: 45% widgets: @@ -3068,8 +3066,8 @@ lvgl: 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. + # Long-press mode "vacuum": opens the vacuum controls. + # Falls outside the action/slider chain, so that stays unchanged. - if: condition: lambda: "return std::string(\"${TILE5_LONGPRESS}\") == \"vacuum\";" @@ -3089,8 +3087,8 @@ lvgl: 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. + # Direct local feedback: LVGL applies this as soon as the finger + # touches the glass, in the same frame. Independent of Home Assistant. pressed: bg_opa: 45% widgets: @@ -3199,8 +3197,8 @@ lvgl: 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. + # Long-press mode "vacuum": opens the vacuum controls. + # Falls outside the action/slider chain, so that stays unchanged. - if: condition: lambda: "return std::string(\"${TILE6_LONGPRESS}\") == \"vacuum\";" @@ -3210,11 +3208,11 @@ lvgl: 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. + # ---------- TILE 7 — Table lamp ---------- + # Brightness-only lamp: deliberately no percentage/cover/climate + # sensors were created for this tile, those would only cost + # HA subscriptions and heap. The overlay therefore gets + # NAN / "" for the fields a light never uses. - obj: id: tile7 hidden: true @@ -3328,12 +3326,12 @@ lvgl: 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. + # ---------- TILE 8 — Mailbox reset ---------- + # input_select with options Full/Empty. The tile lights up as soon as + # the status is TILE8_ACTIVE_STATE ("Full"); a short tap sends + # input_select.select_option with option: "Empty". + # No slider mode, so this tile only needs ha_state_tile8 — + # no other HA subscription at all. - obj: id: tile8 hidden: true @@ -3400,8 +3398,8 @@ lvgl: lambda: 'return !runtime_tiles::enabled && cyd::touch_guard.accept(millis(), 8);' then: - script.execute: { id: publish_action, action: "tile8_long_press" } - # Slider-modus kan hier niet zonder eerst de bijbehorende - # sensors aan te maken; "action" werkt wel out of the box. + # Slider mode isn't possible here without first creating the + # corresponding sensors; "action" works out of the box. - if: condition: lambda: "return std::string(\"${TILE8_LONGPRESS}\") == \"action\";" @@ -3418,7 +3416,7 @@ lvgl: param_val: "" was_on: false - # ---------- TILE 9 — Woonkamer (verhuisd van tegel 2) ---------- + # ---------- TILE 9 — Living room (moved from tile 2) ---------- - obj: id: tile9 hidden: true @@ -3532,7 +3530,7 @@ lvgl: climate_max_temp: "30.0" climate_step: "0.5" - # ---------- TILE 10 — Alarm aan/uit ---------- + # ---------- TILE 10 — Alarm on/off ---------- - obj: id: tile10 hidden: true @@ -3643,7 +3641,7 @@ lvgl: - label: align: LEFT_MID x: 12 - text: "< Vorige" + text: "< Previous" text_font: label text_color: 0x1B1B1B clickable: false @@ -3685,7 +3683,7 @@ lvgl: - label: align: RIGHT_MID x: -12 - text: "Volgende >" + text: "Next >" text_font: label text_color: 0x1B1B1B clickable: false @@ -3758,12 +3756,12 @@ lvgl: bg_color: 0xFFFFFF bg_opa: 100% radius: 28 - # Enige widget met ZOWEL radius ALS een kind (overlay_icon). - # LVGL snijdt kinderen dan bij op de ronding, en dat kan - # alleen via een ARGB-laag van 96x160 = 46KB aaneengesloten. - # Lukt die allocatie niet, dan blijft LVGL het proberen tot - # de task watchdog het bord herstart. Ronding blijft, het - # bijsnijden gaat eraf. + # The only widget with BOTH a radius AND a child (overlay_icon). + # LVGL then clips children to the rounding, and that's only + # possible via a contiguous 96x160 = 46KB ARGB layer. + # If that allocation fails, LVGL keeps trying until the + # task watchdog restarts the board. The rounding stays, the + # clipping goes. clip_corner: false border_width: 0 @@ -3789,12 +3787,12 @@ lvgl: 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. + # The icon used to be a CHILD in the slider. A widget with a radius + # and children has to clip those children to the rounding, and + # LVGL can only do that via an ARGB layer of tens of KB's. Without + # PSRAM that sometimes fails, after which LVGL keeps retrying until + # the watchdog restarts the board. As a sibling on top, no layer + # is needed and it looks identical. - label: id: overlay_icon align: CENTER @@ -4143,8 +4141,8 @@ lvgl: align: TOP_LEFT x: 10 y: 9 - # 44 i.p.v. 36: iets groter doelvlak, groeit weg van de arc - # (top-left anchor blijft staan) i.p.v. ernaartoe. + # 44 instead of 36: slightly bigger tap target, grows away from the arc + # (top-left anchor stays put) instead of towards it. width: 44 height: 44 radius: 999 @@ -4200,7 +4198,7 @@ lvgl: id: climate_current_label align: TOP_MID y: 42 - text: "Nu: --" + text: "Now: --" text_font: sublabel_big text_color: 0x1B1B1B text_opa: 85% @@ -4212,9 +4210,9 @@ lvgl: y: 20 width: 184 height: 184 - # Zonder dit claimt de arc een onzichtbare rand extra - # klikgebied buiten de 184x184 box — vlakbij de kruisje- - # knop linksboven ontstaat daardoor overlap. + # Without this the arc claims an invisible extra + # click area outside the 184x184 box — near the close + # button top-left this creates overlap. ext_click_area: 0 start_angle: 135 end_angle: 45 @@ -4251,7 +4249,7 @@ lvgl: id: climate_set_label align: CENTER y: -16 - text: "Gewenst" + text: "Target" text_font: sublabel_big text_color: 0x1B1B1B text_opa: 68% @@ -4343,10 +4341,10 @@ lvgl: text_color: 0x1B1B1B 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. + # Replaces cycling through modes. Cycling sends a set_hvac_mode on every + # tap, so to get from 'cool' to 'dry' a heat pump actually passes through + # 'heat' and 'fan_only' along the way. Here you pick directly, and exactly + # one command is sent. - obj: id: climate_mode_overlay x: 0 @@ -4364,13 +4362,13 @@ lvgl: - label: align: TOP_MID y: 16 - text: "Modus" + text: "Mode" text_font: sublabel_big text_color: 0x1B1B1B clickable: false - # Flex zorgt dat niet-ondersteunde modi geen gat achterlaten: - # LVGL slaat verborgen kinderen over bij het schikken. + # Flex ensures unsupported modes don't leave a gap: + # LVGL skips hidden children when arranging. - obj: id: climate_mode_list align: TOP_MID @@ -4408,7 +4406,7 @@ lvgl: widgets: - label: align: CENTER - text: "Uit" + text: "Off" text_font: sublabel_big text_color: 0x1B1B1B clickable: false @@ -4429,7 +4427,7 @@ lvgl: widgets: - label: align: CENTER - text: "Verwarmen" + text: "Heat" text_font: sublabel_big text_color: 0x1B1B1B clickable: false @@ -4450,7 +4448,7 @@ lvgl: widgets: - label: align: CENTER - text: "Koelen" + text: "Cool" text_font: sublabel_big text_color: 0x1B1B1B clickable: false @@ -4471,7 +4469,7 @@ lvgl: widgets: - label: align: CENTER - text: "Verw./koelen" + text: "Heat/Cool" text_font: sublabel_big text_color: 0x1B1B1B clickable: false @@ -4513,7 +4511,7 @@ lvgl: widgets: - label: align: CENTER - text: "Drogen" + text: "Dry" text_font: sublabel_big text_color: 0x1B1B1B clickable: false @@ -4534,7 +4532,7 @@ lvgl: widgets: - label: align: CENTER - text: "Ventileren" + text: "Fan only" text_font: sublabel_big text_color: 0x1B1B1B clickable: false @@ -4543,7 +4541,7 @@ lvgl: id: cfan_title width: 100% height: 11 - text: "Ventilator" + text: "Fan" text_font: sublabel text_color: 0x6B6B6B text_align: CENTER @@ -4640,7 +4638,7 @@ lvgl: id: cswing_title width: 100% height: 11 - text: "Zwenken" + text: "Swing" text_font: sublabel text_color: 0x6B6B6B text_align: CENTER @@ -4760,8 +4758,8 @@ lvgl: # ---------- 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. + # A vacuum is not a switch: start, pause, stop and return to + # the dock are separate commands, and suction power is a separate axis. - obj: id: vacuum_overlay x: 0 @@ -5071,7 +5069,7 @@ lvgl: - label: align: TOP_MID y: 65 - text: "Volg de kalibratiewizard" + text: "Follow the calibration wizard" text_font: sublabel text_color: 0xFFFFFF - obj: @@ -5289,8 +5287,8 @@ script: - lvgl.widget.hide: vacuum_overlay # ---------- ALERT (HA action show_alert) ---------- - # The screen wakes, keeps its normal brightness and shows the card until Oké or the - # timeout (0: until Oké); the standby interval waits on alert_active. A new alert + # The screen wakes, keeps its normal brightness and shows the card until OK or the + # timeout (0: until OK); the standby interval waits on alert_active. A new alert # replaces the current one; every ending reports itself as esphome.screen_alert. - id: alert_show mode: restart @@ -5402,8 +5400,8 @@ script: 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. + # Almost instant brightness-up. ESPHome's default is 1s, and that feels sluggish + # when you touch the screen and are waiting on light. transition_length: 80ms brightness: !lambda "return screen_settings::current.brightness / 100.0f;" - lvgl.widget.hide: dim_wake_overlay @@ -5427,9 +5425,9 @@ script: id(show_tile_page).execute(); } - # Terug naar het basisscherm bij standby. Bewust via de ESPHome - # acties en NIET met lv_obj_add_flag in een lambda: dit script - # draait vanuit een interval, dus buiten LVGL's eigen afhandeling. + # Back to the home screen on standby. Deliberately via ESPHome's + # actions and NOT with lv_obj_add_flag in a lambda: this script + # runs from an interval, so outside LVGL's own handling. - lvgl.widget.hide: brightness_overlay - lvgl.widget.hide: color_detail_overlay - lvgl.widget.hide: climate_detail_overlay @@ -5503,9 +5501,9 @@ script: 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. + # Opens the mode picker. Hides modes the entity doesn't support and marks + # the current one, so you see where you stand before you pick something. + # Opens the vacuum controls and remembers which entity it concerns. - id: open_vacuum_overlay mode: restart parameters: @@ -5517,7 +5515,7 @@ script: 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. + # A standalone command: start, pause, stop or return_to_base. - id: vacuum_action mode: queued parameters: @@ -5532,7 +5530,7 @@ script: data: entity_id: !lambda "return id(active_entity);" - # Zuigkracht is een aparte as, los van start/stop. + # Suction power is a separate axis, apart from start/stop. - id: vacuum_fan_speed mode: queued parameters: @@ -5599,8 +5597,8 @@ script: fill(id(cswing_title), swing_chips, id(active_climate_swing_modes), id(active_climate_swing_mode)); - lvgl.widget.show: climate_mode_overlay - # Zet precies een modus. Geen tussenstappen, dus geen ongewenste - # commando's naar de warmtepomp onderweg. + # Sets exactly one mode. No intermediate steps, so no unwanted + # commands to the heat pump along the way. # Fan and swing modes from the picker: one call with the entity's own mode name. - id: set_climate_fan_mode mode: restart @@ -5717,7 +5715,7 @@ script: light_controls::fallback.kelvin = 3000; light_controls::fallback.hue = 200; light_controls::open("diagnostic", true, true, 75); - lv_label_set_text(id(color_detail_title), "Voorbeeld lamp"); + lv_label_set_text(id(color_detail_title), "Example lamp"); - lvgl.widget.hide: color_close_button - lvgl.widget.hide: color_done_button - lvgl.widget.show: color_detail_overlay @@ -5928,12 +5926,12 @@ script: 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). + // Debounce: same entity within 600ms ignored. Prevents + // touch noise from firing the same HA action multiple times + // (see globals above for why this is needed). uint32_t now = millis(); if (entity_id == id(last_action_entity) && (now - id(last_action_ms)) < 600) { - ESP_LOGD("tile", "do_tile_action: debounced repeat for %s (%ums na vorige)", + ESP_LOGD("tile", "do_tile_action: debounced repeat for %s (%ums after previous)", entity_id.c_str(), now - id(last_action_ms)); return false; } @@ -6188,11 +6186,11 @@ script: 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. + // Accent colour from the Home Assistant domain: the part before the dot + // in the entity_id. script/scene/button fire (ACCENT_ACTION), a + // light is a light (ACCENT_LIGHT), the rest is a device. + // NOTE: this helper appears twice in this file — here and in + // open_value_overlay. Update them together. auto accent_for = [](const std::string& entity_id) -> uint32_t { std::string d = entity_id.substr(0, entity_id.find('.')); if (d == "script" || d == "scene" || d == "button" || @@ -6201,7 +6199,7 @@ script: return ${ACCENT_DEVICE}; }; - // De sliderkleur komt van de tegel, dus die kan ook ${ACCENT_AUTO} zijn. + // The slider colour comes from the tile, so it can also be ${ACCENT_AUTO}. uint32_t fill_color = (slider_fill_color == ${ACCENT_AUTO}) ? accent_for(entity) : slider_fill_color; @@ -6265,11 +6263,11 @@ script: char current_buf[48]; if (isnan(climate_current_temperature)) { - snprintf(current_buf, sizeof(current_buf), "Nu: --"); + snprintf(current_buf, sizeof(current_buf), "Now: --"); } else if (id(active_climate_humidity) >= 0) { - snprintf(current_buf, sizeof(current_buf), "Nu: %.1f° · %d%%", climate_current_temperature, id(active_climate_humidity)); + snprintf(current_buf, sizeof(current_buf), "Now: %.1f° · %d%%", climate_current_temperature, id(active_climate_humidity)); } else { - snprintf(current_buf, sizeof(current_buf), "Nu: %.1f°", climate_current_temperature); + snprintf(current_buf, sizeof(current_buf), "Now: %.1f°", climate_current_temperature); } lv_label_set_text(id(climate_current_label), current_buf); @@ -6512,8 +6510,8 @@ script: 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. + // active_state not empty? Then only that exact HA state counts as "on". + // This way an input_select ("Full"/"Empty") can also light up a tile. auto is_active = [](const std::string& tile_type, const std::string& state, const std::string& active_state = std::string()) { if (!active_state.empty()) return state == active_state; @@ -6532,7 +6530,7 @@ script: 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. + // Tile 7/8 have no cover_state sensor, so no cover branch needed. bool t7_on = is_active("${TILE7_TYPE}", std::string(id(ha_state_tile7).state.c_str()), "${TILE7_ACTIVE_STATE}"); bool t8_on = is_active("${TILE8_TYPE}", std::string(id(ha_state_tile8).state.c_str()), "${TILE8_ACTIVE_STATE}"); bool t9_on = is_active("${TILE9_TYPE}", std::string(id(ha_state_tile9).state.c_str()), "${TILE9_ACTIVE_STATE}"); @@ -6568,7 +6566,7 @@ script: 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). + // Tile 7: light with only brightness. Tile 8: pure text (Full/Empty). set_value_auto(id(t7_value), t7_on, "${TILE7_TYPE}", "${TILE7_VALUE_MODE}", id(tile7_brightness).state, NAN, std::string(), NAN, NAN, NAN, @@ -6579,7 +6577,7 @@ script: NAN, NAN, NAN, "${TILE8_LABEL_ON}", "${TILE8_LABEL_OFF}"); - // Tegel 9: kleurenlamp (brightness). Tegel 10: puur tekst (Aan/Uit). + // Tile 9: colour lamp (brightness). Tile 10: pure text (On/Off). set_value_auto(id(t9_value), t9_on, "${TILE9_TYPE}", "${TILE9_VALUE_MODE}", id(tile9_brightness).state, NAN, std::string(), NAN, NAN, NAN, @@ -6594,33 +6592,33 @@ script: 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"); + if (state == "cleaning") set_lbl(id(t6_value), "Cleaning"); + else if (state == "docked") set_lbl(id(t6_value), "Docked"); + else if (state == "returning") set_lbl(id(t6_value), "Returning to dock"); + else if (state == "paused") set_lbl(id(t6_value), "Paused"); + else if (state == "error") set_lbl(id(t6_value), "Error"); else if (!state.empty()) set_lbl(id(t6_value), state); } if (!id(ha_state_tile1).has_state() || id(ha_state_tile1).state == "unknown" || id(ha_state_tile1).state == "unavailable") - set_lbl(id(t1_value), "Niet beschikbaar"); + set_lbl(id(t1_value), "Unavailable"); if (!id(ha_state_tile2).has_state() || id(ha_state_tile2).state == "unknown" || id(ha_state_tile2).state == "unavailable") - set_lbl(id(t2_value), "Niet beschikbaar"); + set_lbl(id(t2_value), "Unavailable"); if (!id(ha_state_tile3).has_state() || id(ha_state_tile3).state == "unknown" || id(ha_state_tile3).state == "unavailable") - set_lbl(id(t3_value), "Niet beschikbaar"); + set_lbl(id(t3_value), "Unavailable"); if (!id(ha_state_tile4).has_state() || id(ha_state_tile4).state == "unknown" || id(ha_state_tile4).state == "unavailable") - set_lbl(id(t4_value), "Niet beschikbaar"); + set_lbl(id(t4_value), "Unavailable"); if (!id(ha_state_tile5).has_state() || id(ha_state_tile5).state == "unknown" || id(ha_state_tile5).state == "unavailable") - set_lbl(id(t5_value), "Niet beschikbaar"); + set_lbl(id(t5_value), "Unavailable"); if (!id(ha_state_tile6).has_state() || id(ha_state_tile6).state == "unknown" || id(ha_state_tile6).state == "unavailable") - set_lbl(id(t6_value), "Niet beschikbaar"); + set_lbl(id(t6_value), "Unavailable"); if (!id(ha_state_tile7).has_state() || id(ha_state_tile7).state == "unknown" || id(ha_state_tile7).state == "unavailable") - set_lbl(id(t7_value), "Niet beschikbaar"); + set_lbl(id(t7_value), "Unavailable"); if (!id(ha_state_tile8).has_state() || id(ha_state_tile8).state == "unknown" || id(ha_state_tile8).state == "unavailable") - set_lbl(id(t8_value), "Niet beschikbaar"); + set_lbl(id(t8_value), "Unavailable"); if (!id(ha_state_tile9).has_state() || id(ha_state_tile9).state == "unknown" || id(ha_state_tile9).state == "unavailable") - set_lbl(id(t9_value), "Niet beschikbaar"); + set_lbl(id(t9_value), "Unavailable"); if (!id(ha_state_tile10).has_state() || id(ha_state_tile10).state == "unknown" || id(ha_state_tile10).state == "unavailable") - set_lbl(id(t10_value), "Niet beschikbaar"); + set_lbl(id(t10_value), "Unavailable"); if (id(active_control_kind) == "climate_temperature" && !lv_obj_has_flag(id(climate_detail_overlay), LV_OBJ_FLAG_HIDDEN)) { auto label_for_climate_mode = [](std::string mode) { for (char &c : mode) { @@ -6670,11 +6668,11 @@ script: char current_buf[48]; if (isnan(current_temperature)) { - snprintf(current_buf, sizeof(current_buf), "Nu: --"); + snprintf(current_buf, sizeof(current_buf), "Now: --"); } else if (id(active_climate_humidity) >= 0) { - snprintf(current_buf, sizeof(current_buf), "Nu: %.1f° · %d%%", current_temperature, id(active_climate_humidity)); + snprintf(current_buf, sizeof(current_buf), "Now: %.1f° · %d%%", current_temperature, id(active_climate_humidity)); } else { - snprintf(current_buf, sizeof(current_buf), "Nu: %.1f°", current_temperature); + snprintf(current_buf, sizeof(current_buf), "Now: %.1f°", current_temperature); } lv_label_set_text(id(climate_current_label), current_buf); @@ -6694,9 +6692,9 @@ script: { 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). + // Before the first time sync, now() is epoch 0 (1970) — don't show + // a misleading "01:00" then, just leave it blank until a + // real time comes in (after the first Home Assistant time sync). if (now.is_valid()) { char buf[10]; @@ -6712,11 +6710,11 @@ script: } } - // 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. + // Accent colour from the Home Assistant domain: the part before the dot + // in the entity_id. script/scene/button fire (ACCENT_ACTION), a + // light is a light (ACCENT_LIGHT), the rest is a device. + // NOTE: this helper appears twice in this file — here and in + // open_value_overlay. Update them together. auto accent_for = [](const std::string& entity_id) -> uint32_t { std::string d = entity_id.substr(0, entity_id.find('.')); if (d == "script" || d == "scene" || d == "button" || @@ -6757,7 +6755,7 @@ script: break; } } - // ${ACCENT_AUTO} = "bepaal zelf"; een echte hexwaarde wint altijd. + // ${ACCENT_AUTO} = "figure it out yourself"; a real hex value always wins. uint32_t accent = accent_for(entity_id); if (circle_active == ${ACCENT_AUTO}) circle_active = accent; if (icon_disabled == ${ACCENT_AUTO}) icon_disabled = accent; @@ -6854,12 +6852,12 @@ script: text: - platform: smart_display id: dashboard_inbox - name: "Tegelinstellingen" + name: "Tile settings" internal: ${DASHBOARD_INBOX_INTERNAL} button: - platform: template - name: "Touch kalibreren" + name: "Calibrate touch" internal: ${DASHBOARD_INBOX_INTERNAL} entity_category: config on_press: @@ -6868,7 +6866,7 @@ button: number: - platform: template id: setting_brightness - name: "Helderheid normaal" + name: "Normal brightness" entity_category: config min_value: 5 max_value: 100 @@ -6887,7 +6885,7 @@ number: id(apply_screen_settings).execute(); - platform: template id: setting_standby_brightness - name: "Helderheid standby" + name: "Standby brightness" entity_category: config min_value: 0 max_value: 100 @@ -6906,7 +6904,7 @@ number: id(apply_screen_settings).execute(); - platform: template id: setting_night_brightness - name: "Helderheid nacht" + name: "Night brightness" entity_category: config min_value: 0 max_value: 100 @@ -6925,7 +6923,7 @@ number: id(apply_screen_settings).execute(); - platform: template id: setting_standby_seconds - name: "Standby na" + name: "Standby after" entity_category: config min_value: 60 max_value: 86400 diff --git a/packages/guition.yaml b/packages/guition.yaml index 4a6ce1b..1b7e70e 100644 --- a/packages/guition.yaml +++ b/packages/guition.yaml @@ -2,7 +2,7 @@ # 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: - SCREEN_FIRMWARE_VERSION: "0.2.33" + SCREEN_FIRMWARE_VERSION: "0.2.34" # 0: no drift limit; LVGL decides, releasing inside the tile is a tap, leaving it is not # (like iOS). The CYD keeps a limit because its resistive panel jumps. TOUCH_MOVE_LIMIT_PX: "0" @@ -15,13 +15,13 @@ substitutions: DYNAMIC_TILES: "true" DASHBOARD_INBOX_INTERNAL: "false" DEVICE_NAME: "guition-new" - DEVICE_FRIENDLY_NAME: "Mijn Guition" + DEVICE_FRIENDLY_NAME: "My Guition" CALIBRATION_ON_BOOT: "false" TILE_COUNT: "1" # 1-10; <=6 hides page navigation DIRECT_ACTIONS: "true" # true = call Home Assistant services directly, false = only emit action events - ROOM_NAME: "Kies tegels in HA" # room name shown in the top-left header + ROOM_NAME: "Choose tiles in HA" # room name shown in the top-left header TIME_24H: "true" # true = 24h format, false = 12h format ## DIM settings @@ -35,24 +35,23 @@ substitutions: NIGHT_DIM_BRIGHTNESS: "10" # backlight brightness in percent at night # ============================================================ - # ACCENTKLEUREN — automatisch bepaald uit het Home Assistant-domein + # ACCENT COLOURS — determined automatically from the Home Assistant domain # - # Het domein is het stuk voor de punt in het entity_id: - # script.goede_morgen -> "script" -> ACCENT_ACTION - # light.woonkamer_2 -> "light" -> ACCENT_LIGHT + # The domain is the part before the dot in the entity_id: + # script.good_morning -> "script" -> ACCENT_ACTION + # light.living_room_2 -> "light" -> ACCENT_LIGHT # vacuum.s8 -> "vacuum" -> ACCENT_DEVICE # - # 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. + # That's Home Assistant's own grouping, so a tile automatically gets the + # right colour as soon as you put an entity in it — you no longer have + # to pick colours per tile. The distinction that matters: a script/scene + # fires, a device you turn on or off. Those two shouldn't look the same. # - # 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). + # Set TILE*_CIRCLE_ACTIVE_COLOR / TILE*_ICON_DISABLED_COLOR to + # ${ACCENT_AUTO} to use this, or to a real hex value to colour that + # one tile by hand (see tile 8 — red as soon as there's mail). # ============================================================ - ACCENT_AUTO: "0x1000000" # sentinel: bewust buiten het 24-bits kleurbereik + ACCENT_AUTO: "0x1000000" # sentinel: deliberately outside the 24-bit colour range ACCENT_ACTION: "0x8B5CF6" # script / scene / button / input_button / automation ACCENT_LIGHT: "0xFEC600" # light ACCENT_DEVICE: "0x00C5EC" # switch, fan, cover, climate, vacuum, media_player, rest @@ -68,16 +67,16 @@ substitutions: # ============================================================ # --- 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_ENTITY: "light.example_lamp" # main Home Assistant entity for this tile + TILE1_STATE_ENTITY: "light.example_lamp" # entity used for active/inactive state styling TILE1_TITLE: "Lamp" # tile title shown in the UI TILE1_ICON: "\U000F001B" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) TILE1_TYPE: "light" # tile type: light | fan | switch | scene | script | cover | climate TILE1_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset | custom TILE1_LONGPRESS: "slider" # long press mode: none | slider | action TILE1_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text - TILE1_LABEL_OFF: "Uit" # text shown when entity is off - TILE1_LABEL_ON: "Aan" # text shown for text-mode tiles when entity is on + TILE1_LABEL_OFF: "Off" # text shown when entity is off + TILE1_LABEL_ON: "On" # text shown for text-mode tiles when entity is on TILE1_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") # --- colors --- @@ -117,16 +116,16 @@ substitutions: # ============================================================ # --- 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_ENTITY: "switch.example_plug" # main Home Assistant entity for this tile + TILE2_STATE_ENTITY: "switch.example_plug" # entity used for active/inactive state styling + TILE2_TITLE: "Plug" # tile title shown in the UI TILE2_ICON: "\U000F0387" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) TILE2_TYPE: "switch" # tile type: light | fan | switch | scene | script | cover | climate TILE2_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset | custom TILE2_LONGPRESS: "none" # long press mode: none | slider | action TILE2_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text - TILE2_LABEL_OFF: "Uit" # text shown when entity is off - TILE2_LABEL_ON: "Aan" # text shown for text-mode tiles when entity is on + TILE2_LABEL_OFF: "Off" # text shown when entity is off + TILE2_LABEL_ON: "On" # text shown for text-mode tiles when entity is on TILE2_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") # --- colors --- @@ -166,16 +165,16 @@ substitutions: # ============================================================ # --- 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_ENTITY: "scene.example_evening" # main Home Assistant entity for this tile + TILE3_STATE_ENTITY: "scene.example_evening" # entity used for active/inactive state styling + TILE3_TITLE: "Evening" # tile title shown in the UI TILE3_ICON: "\U000F02E3" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) TILE3_TYPE: "scene" # tile type: light | fan | switch | scene | script | cover | climate TILE3_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset | custom TILE3_LONGPRESS: "none" # long press mode: none | slider | action TILE3_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text - TILE3_LABEL_OFF: "Uit" # text shown when entity is off - TILE3_LABEL_ON: "Aan" # text shown for text-mode tiles when entity is on + TILE3_LABEL_OFF: "Off" # text shown when entity is off + TILE3_LABEL_ON: "On" # text shown for text-mode tiles when entity is on TILE3_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") # --- colors --- @@ -215,16 +214,16 @@ substitutions: # ============================================================ # --- 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_ENTITY: "script.example_all_off" # main Home Assistant entity for this tile + TILE4_STATE_ENTITY: "script.example_all_off" # entity used for active/inactive state styling + TILE4_TITLE: "All off" # tile title shown in the UI TILE4_ICON: "\U000F059C" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) TILE4_TYPE: "script" # tile type: light | fan | switch | scene | script | cover | climate TILE4_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset | custom TILE4_LONGPRESS: "none" # long press mode: none | slider | action TILE4_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text - TILE4_LABEL_OFF: "Uit" # text shown when entity is off - TILE4_LABEL_ON: "Aan" # text shown for text-mode tiles when entity is on + TILE4_LABEL_OFF: "Off" # text shown when entity is off + TILE4_LABEL_ON: "On" # text shown for text-mode tiles when entity is on TILE4_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") # --- colors --- @@ -264,16 +263,16 @@ substitutions: # ============================================================ # --- 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_ENTITY: "climate.example_ac" # main Home Assistant entity for this tile + TILE5_STATE_ENTITY: "climate.example_ac" # entity used for active/inactive state styling + TILE5_TITLE: "AC" # tile title shown in the UI TILE5_ICON: "\U000F0425" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) TILE5_TYPE: "climate" # tile type: light | fan | switch | scene | script | cover | climate TILE5_TAP_ACTION: "toggle" # tap action: auto | toggle | activate | fan_toggle_preset | custom TILE5_LONGPRESS: "slider" # long press mode: none | slider | action TILE5_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text - TILE5_LABEL_OFF: "Uit" # text shown when entity is off (closed) - TILE5_LABEL_ON: "Aan" # text shown when entity is on (open) + TILE5_LABEL_OFF: "Off" # text shown when entity is off (closed) + TILE5_LABEL_ON: "On" # text shown when entity is on (open) TILE5_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") # --- colors --- @@ -313,16 +312,16 @@ substitutions: # ============================================================ # --- 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_ENTITY: "vacuum.example_robot" # main Home Assistant entity for this tile + TILE6_STATE_ENTITY: "vacuum.example_robot" # entity used for active/inactive state styling + TILE6_TITLE: "Vacuum" # tile title shown in the UI TILE6_ICON: "\U000F070D" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) TILE6_TYPE: "vacuum" # tile type: light | fan | switch | scene | script | cover | climate TILE6_TAP_ACTION: "custom" # tap action: auto | toggle | activate | fan_toggle_preset | custom TILE6_LONGPRESS: "vacuum" # long press mode: none | slider | action TILE6_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text - TILE6_LABEL_OFF: "Uit" # text shown when entity is off - TILE6_LABEL_ON: "Aan" # text shown for text-mode tiles when entity is on + TILE6_LABEL_OFF: "Off" # text shown when entity is off + TILE6_LABEL_ON: "On" # text shown for text-mode tiles when entity is on TILE6_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") # --- colors --- @@ -356,22 +355,22 @@ substitutions: 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. + # TILE 7 — Table lamp + # Brightness-only lamp (HA reports supported_color_modes: ['brightness']), + # so the colour button in the slider overlay stays hidden automatically. # ============================================================ # --- basic settings --- - TILE7_ENTITY: "light.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_ENTITY: "light.unused_7" # main Home Assistant entity for this tile + TILE7_STATE_ENTITY: "light.unused_7" # entity used for active/inactive state styling + TILE7_TITLE: "Tile 7" # tile title shown in the UI TILE7_ICON: "\U000F06B5" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) TILE7_TYPE: "light" # tile type: light | fan | switch | scene | script | cover | climate TILE7_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset TILE7_LONGPRESS: "none" # long press mode: none | slider | action | vacuum TILE7_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text - TILE7_LABEL_OFF: "Uit" # text shown when entity is off - TILE7_LABEL_ON: "Aan" # text shown for text-mode tiles when entity is on + TILE7_LABEL_OFF: "Off" # text shown when entity is off + TILE7_LABEL_ON: "On" # text shown for text-mode tiles when entity is on TILE7_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") # --- colors --- @@ -391,7 +390,7 @@ substitutions: 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_SLIDER: "auto" # auto = brightness for a light TILE7_LONGPRESS_OFF_VALUE: "0" # initial slider value when entity is off # if LONGPRESS = action: TILE7_LONGPRESS_ACTION: "" # entity_id to fire on long press @@ -399,27 +398,27 @@ substitutions: 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. + # TILE 8 — Mailbox reset + # input_select.mailbox_status has options ['Full', 'Empty']. + # The tile lights up as soon as the status is "Full" (ACTIVE_STATE), and a + # short tap sets it back to "Empty" via input_select.select_option. # ============================================================ # --- basic settings --- - TILE8_ENTITY: "light.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_ENTITY: "light.unused_8" # main Home Assistant entity for this tile + TILE8_STATE_ENTITY: "light.unused_8" # entity used for active/inactive state styling + TILE8_TITLE: "Tile 8" # tile title shown in the UI TILE8_ICON: "\U000F06EE" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) - TILE8_TYPE: "light" # 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 + TILE8_TYPE: "light" # generic type; the real behavior comes from TAP_SERVICE below + TILE8_TAP_ACTION: "auto" # activate + TAP_SERVICE = a standalone service call + TILE8_LONGPRESS: "none" # long press only sends the event to HA + TILE8_VALUE_MODE: "auto" # shows LABEL_ON / LABEL_OFF + TILE8_LABEL_OFF: "Off" # text shown when entity is off / inactive + TILE8_LABEL_ON: "On" # text shown when entity is active + TILE8_ACTIVE_STATE: "" # tile is "on" as soon as the status is exactly "Full" # --- colors --- - TILE8_CIRCLE_ACTIVE_COLOR: "0xE5484D" # rood zodra er post ligt + TILE8_CIRCLE_ACTIVE_COLOR: "0xE5484D" # red as soon as there's mail TILE8_CIRCLE_DISABLED_COLOR: "0xF0F0F0" # icon circle color when inactive TILE8_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active TILE8_ICON_DISABLED_COLOR: "0x9E9E9E" # icon color when inactive @@ -431,35 +430,35 @@ substitutions: TILE8_VALUE_DISABLED_COLOR: "0x616161" # 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 + TILE8_TAP_SERVICE: "" # sets the input_select to a fixed option + TILE8_TAP_PARAM_KEY: "" # parameter name for input_select.select_option + TILE8_TAP_PARAM_VAL: "" # the "reset" value # if LONGPRESS = slider: - TILE8_LONGPRESS_SLIDER: "auto" # niet in gebruik (LONGPRESS = none) - TILE8_LONGPRESS_OFF_VALUE: "0" # niet in gebruik + TILE8_LONGPRESS_SLIDER: "auto" # not in use (LONGPRESS = none) + TILE8_LONGPRESS_OFF_VALUE: "0" # not in use # if LONGPRESS = action: TILE8_LONGPRESS_ACTION: "" # entity_id to fire on long press TILE8_LONGPRESS_ACTION_TYPE: "" # entity type (empty = same as TILE*_TYPE) TILE8_LONGPRESS_ACTION_SERVICE: "" # service to call (default: activate/turn_on) # ============================================================ - # TILE 9 — 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. + # TILE 9 — Living room + # Moved from tile 2 to row 5 to make room for Ambient. + # Colour lamp (color_temp / rgb / xy), so the colour button in the + # slider overlay appears automatically. # ============================================================ # --- basic settings --- - TILE9_ENTITY: "light.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_ENTITY: "light.unused_9" # main Home Assistant entity for this tile + TILE9_STATE_ENTITY: "light.unused_9" # entity used for active/inactive state styling + TILE9_TITLE: "Tile 9" # tile title shown in the UI TILE9_ICON: "\U000F04B9" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) TILE9_TYPE: "light" # tile type: light | fan | switch | scene | script | cover | climate TILE9_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset TILE9_LONGPRESS: "none" # long press mode: none | slider | action | vacuum TILE9_VALUE_MODE: "auto" # value mode: auto | brightness | percentage | preset | text - TILE9_LABEL_OFF: "Uit" # text shown when entity is off - TILE9_LABEL_ON: "Aan" # text shown for text-mode tiles when entity is on + TILE9_LABEL_OFF: "Off" # text shown when entity is off + TILE9_LABEL_ON: "On" # text shown for text-mode tiles when entity is on TILE9_ACTIVE_STATE: "" # optional: exact HA state that counts as "active" (empty = default "on") # --- colors --- @@ -479,7 +478,7 @@ substitutions: 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_SLIDER: "auto" # auto = brightness for a light TILE9_LONGPRESS_OFF_VALUE: "0" # initial slider value when entity is off # if LONGPRESS = action: TILE9_LONGPRESS_ACTION: "" # entity_id to fire on long press @@ -487,26 +486,26 @@ substitutions: 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. + # TILE 10 — Alarm on/off + # input_boolean.alarm_engaged. An input_boolean doesn't respond to + # switch.toggle, hence the explicit TAP_SERVICE. # ============================================================ # --- basic settings --- - TILE10_ENTITY: "light.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_ENTITY: "light.unused_10" # main Home Assistant entity for this tile + TILE10_STATE_ENTITY: "light.unused_10" # entity used for active/inactive state styling + TILE10_TITLE: "Tile 10" # tile title shown in the UI TILE10_ICON: "\U000F002A" # MDI glyph from the tile icon set (screen_manager/app/tile_icons.py) - TILE10_TYPE: "light" # generiek aan/uit-type; de service hieronder doet het echte werk + TILE10_TYPE: "light" # generic on/off type; the service below does the real work TILE10_TAP_ACTION: "auto" # tap action: auto | toggle | activate | fan_toggle_preset - TILE10_LONGPRESS: "none" # long press 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 + TILE10_LONGPRESS: "none" # long press only sends the event to HA + TILE10_VALUE_MODE: "auto" # shows LABEL_ON / LABEL_OFF + TILE10_LABEL_OFF: "Off" # text shown when entity is off + TILE10_LABEL_ON: "On" # text shown when entity is on + TILE10_ACTIVE_STATE: "" # input_boolean just uses on/off # --- colors --- - TILE10_CIRCLE_ACTIVE_COLOR: "${ACCENT_AUTO}" # input_boolean -> ACCENT_DEVICE (cyaan) + TILE10_CIRCLE_ACTIVE_COLOR: "${ACCENT_AUTO}" # input_boolean -> ACCENT_DEVICE (cyan) TILE10_CIRCLE_DISABLED_COLOR: "0xF0F0F0" # icon circle color when inactive TILE10_ICON_ACTIVE_COLOR: "0xFFFFFF" # icon color when active TILE10_ICON_DISABLED_COLOR: "0x9E9E9E" # icon color when inactive @@ -518,12 +517,12 @@ substitutions: TILE10_VALUE_DISABLED_COLOR: "0x616161" # value color when inactive # --- advanced settings --- - TILE10_TAP_SERVICE: "" # switch.toggle werkt niet op een input_boolean + TILE10_TAP_SERVICE: "" # switch.toggle doesn't work on an input_boolean TILE10_TAP_PARAM_KEY: "" # optional extra service parameter key TILE10_TAP_PARAM_VAL: "" # optional extra service parameter value # if LONGPRESS = slider: - TILE10_LONGPRESS_SLIDER: "auto" # niet in gebruik (LONGPRESS = none) - TILE10_LONGPRESS_OFF_VALUE: "0" # niet in gebruik + TILE10_LONGPRESS_SLIDER: "auto" # not in use (LONGPRESS = none) + TILE10_LONGPRESS_OFF_VALUE: "0" # not in use # if LONGPRESS = action: TILE10_LONGPRESS_ACTION: "" # entity_id to fire on long press TILE10_LONGPRESS_ACTION_TYPE: "" # entity type (empty = same as TILE*_TYPE) @@ -565,7 +564,7 @@ substitutions: ALERT_BUTTON_H: "60" ALERT_BUTTON_INSET: "22" ALERT_TITLE_MAX: "64" # bytes; the label ends one line with an ellipsis - ALERT_BUTTON_MAX: "16" # bytes on the Oké button + ALERT_BUTTON_MAX: "16" # bytes on the OK button ALERT_SUBTITLE_MAX: "240" # bytes; wraps over the rows the card has OVERLAY_SLIDER_W: "144" OVERLAY_SLIDER_H: "300" @@ -700,7 +699,7 @@ esphome: id(active_climate_swing_modes) = t.swing_modes; id(active_climate_swing_mode) = t.swing_mode; id(open_value_overlay).execute(t.name, t.entity, t.domain(), "auto", t.active(), 0, t.brightness, t.percentage, t.position, runtime_tiles::icon_for(t), - 0xD5EEFC, 0x00A6ED, 0x1B1B1B, "Uit", "Aan", t.modes, + 0xD5EEFC, 0x00A6ED, 0x1B1B1B, "Off", "On", t.modes, t.current, t.target, t.humidity, t.state, t.hvac_modes, std::to_string(t.minimum), std::to_string(t.maximum), std::to_string(t.step)); }; @@ -716,8 +715,8 @@ esphome: id(active_climate_fan_modes) = t.fan_modes; id(active_climate_fan_mode) = t.fan_mode; id(active_climate_swing_modes) = t.swing_modes; id(active_climate_swing_mode) = t.swing_mode; char buf[72]; - if (std::isfinite(t.current)) snprintf(buf, sizeof(buf), "Nu: %.1f°", t.current); - else snprintf(buf, sizeof(buf), "Nu: --"); + if (std::isfinite(t.current)) snprintf(buf, sizeof(buf), "Now: %.1f°", t.current); + else snprintf(buf, sizeof(buf), "Now: --"); lv_label_set_text(id(climate_current_label), buf); if (std::isfinite(t.target)) { int target = cyd::quantize_temperature(lround(t.target * 10), id(active_climate_min_x10), id(active_climate_max_x10), id(active_climate_step_x10)); @@ -770,8 +769,8 @@ psram: 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. + # Set to DEBUG or VERBOSE temporarily for diagnosis. Note: ESPHome logs + # its configuration lines at CONFIG level, which are invisible at INFO. level: DEBUG logs: sensor: WARN @@ -857,7 +856,7 @@ api: then: - script.execute: ui_self_test # Home Assistant automations: esphome._show_alert puts a card over the whole - # screen (README, "Alert vanuit een automatisering"); dismiss_alert removes it again. + # screen (README, "Alert from an automation"); dismiss_alert removes it again. - action: show_alert variables: title: string @@ -976,10 +975,10 @@ globals: 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). + # Debounce for do_tile_action: protects against repeated events from + # multiple separate press/release cycles for one physical touch (noise in + # the resistive layer), causing the same HA action to fire multiple times + # within a few hundred ms (observed: script.ambient_morning 6x in 8s). - id: last_action_entity type: std::string restore_value: no @@ -1352,8 +1351,8 @@ font: size: 18 glyphs: *tile_icons -# Geheugen zichtbaar maken: dit bord heeft geen PSRAM, dus de vrije heap -# bepaalt hoeveel UI erbij kan. Verschijnt ook als entity in Home Assistant. +# Make memory visible: this board has no PSRAM, so the free heap determines +# how much UI still fits. Also shows up as an entity in Home Assistant. debug: # Heap figures every five minutes: enough to spot a leak, few recorder rows. update_interval: 300s @@ -1380,7 +1379,7 @@ sensor: # "s" in its registry and would refuse a timestamp under that name. - platform: uptime type: timestamp - name: "Opgestart" + name: "Last boot" id: boot_time - platform: template @@ -1443,16 +1442,16 @@ sensor: id: tile6_percentage on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - # Tegel 7 (Tafellamp) is een alleen-helderheid light: alleen brightness. - # Bewust geen percentage / cover_position / climate-sensors — elke - # HA-subscription kost heap op een bord zonder PSRAM. + # Tile 7 (Table lamp) is a brightness-only light: just brightness. + # Deliberately no percentage / cover_position / climate sensors — every + # HA subscription costs heap on a board without PSRAM. - platform: template update_interval: never id: tile7_brightness on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - # Tegel 9 (Woonkamer) is een kleurenlamp: brightness + supported_color_modes. - # Geen percentage / cover / climate — die leest deze tegel nooit. + # Tile 9 (Living room) is a colour lamp: brightness + supported_color_modes. + # No percentage / cover / climate — this tile never reads those. - platform: template update_interval: never id: tile9_brightness @@ -1598,24 +1597,24 @@ binary_sensor: # ---------------------------------------------------------- text_sensor: - platform: template - name: "Guition schermtype" + name: "Guition screen type" entity_category: diagnostic update_interval: never lambda: 'return {"guition"};' - platform: template - name: "Schermfirmware" + name: "Screen firmware" entity_category: diagnostic lambda: 'return {"${SCREEN_FIRMWARE_VERSION}"};' update_interval: 60s # ESP Screen Manager matches the screen to its YAML profile and OTA address. - platform: template - name: "Apparaatnaam" + name: "Device name" entity_category: diagnostic lambda: 'return {"${DEVICE_NAME}"};' update_interval: 60s - platform: wifi_info ip_address: - name: "IP-adres" + name: "IP address" entity_category: diagnostic - platform: template id: smartdisplay_action @@ -1672,14 +1671,14 @@ text_sensor: id: ha_state_tile10 on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - # Woonkamer meldt color_temp/rgb/xy, dus hier verschijnt de kleurknop wel. + # Living room reports color_temp/rgb/xy, so the colour button does appear here. - platform: template update_interval: never id: tile9_supported_color_modes on_value: { then: [ lambda: 'if (!id(ui_refresh).is_running()) id(ui_refresh).execute();' ] } - # Gebruikt door de slider-overlay om te bepalen of de kleurknop nodig is. - # Deze lamp meldt ['brightness'], dus de knop blijft vanzelf verborgen. + # Used by the slider overlay to decide whether the colour button is needed. + # This lamp reports ['brightness'], so the button stays hidden automatically. - platform: template update_interval: never id: tile7_supported_color_modes @@ -1888,21 +1887,21 @@ touchscreen: for (const auto &point : touches) { // Trace every sample of an edge touch: the log then shows how often the panel delivers. if (cyd::edge_swipe.armed()) - ESP_LOGI("touch", "veeg id=%u st=%u x=%u y=%u", (unsigned) point.id, (unsigned) point.state, point.x, point.y); + ESP_LOGI("touch", "swipe id=%u st=%u x=%u y=%u", (unsigned) point.id, (unsigned) point.state, point.x, point.y); cyd::touch_guard.update(point.x, point.y, point.id); if (point.id != cyd::touch_guard.contact()) continue; - // Swiping in from a side edge flips the page ("Vegen tussen pagina's"); the tap + // Swiping in from a side edge flips the page ("Swiping between pages"); the tap // under the finger is consumed and LVGL waits for the release. LVGL 9.5 sends no // PRESSING to the input device, hence the touchscreen trigger. const int step = cyd::edge_swipe.update(point.x, point.y); if (!step) continue; - const char *blocked = !runtime_tiles::enabled ? "geen runtime-tegels" : !runtime_tiles::swipe_pages ? "instelling uit" - : id(display_dimmed) ? "scherm gedimd" : id(calibration_active) ? "kalibratie" : !id(active_entity).empty() ? "kaart open" - : (runtime_tiles::detail_root && !lv_obj_has_flag(runtime_tiles::detail_root, LV_OBJ_FLAG_HIDDEN)) ? "detailkaart open" : nullptr; - if (blocked) { ESP_LOGI("touch", "randveeg genegeerd: %s", blocked); continue; } + const char *blocked = !runtime_tiles::enabled ? "no runtime tiles" : !runtime_tiles::swipe_pages ? "setting off" + : id(display_dimmed) ? "screen dimmed" : id(calibration_active) ? "calibration" : !id(active_entity).empty() ? "card open" + : (runtime_tiles::detail_root && !lv_obj_has_flag(runtime_tiles::detail_root, LV_OBJ_FLAG_HIDDEN)) ? "detail card open" : nullptr; + if (blocked) { ESP_LOGI("touch", "edge swipe ignored: %s", blocked); continue; } cyd::touch_guard.consume(); for (auto *indev = lv_indev_get_next(nullptr); indev; indev = lv_indev_get_next(indev)) lv_indev_wait_release(indev); - ESP_LOGI("touch", "randveeg: pagina %d -> %d", id(tile_page), id(tile_page) + step); + ESP_LOGI("touch", "edge swipe: page %d -> %d", id(tile_page), id(tile_page) + step); id(tile_page) += step; id(show_tile_page).execute(); } @@ -1911,7 +1910,7 @@ touchscreen: id(last_touch_ms) = millis(); id(touch_down) = false; if (cyd::edge_swipe.armed() && cyd::edge_swipe.inward() > 0) - ESP_LOGI("touch", "randveeg niet gevuurd: %d px naar binnen, %d px verticaal", cyd::edge_swipe.inward(), cyd::edge_swipe.sideways()); + ESP_LOGI("touch", "edge swipe not fired: %d px inward, %d px vertical", cyd::edge_swipe.inward(), cyd::edge_swipe.sideways()); cyd::edge_swipe.end(); @@ -1975,16 +1974,16 @@ lvgl: # 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. + # Direct local feedback on the WHOLE UI, not just the tiles. + # A theme sets the style per widget type, so this covers all + # buttons, tabs, sliders and the thermostat ring in one go. Non-clickable + # widgets never enter the pressed state, so those stay untouched. theme: obj: pressed: - # bg_opa 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 and NOT opa: `opa` at widget level forces LVGL to render the + # element to a separate layer, and that layer costs tens of KB's + # per pressed widget. That ran the memory out here. bg_opa: 45% slider: knob: @@ -1999,9 +1998,9 @@ lvgl: 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. + # Background: two-stop vertical gradient instead of the 320x240 + # RGB565 wallpaper. Saves ~150 KB flash and an image blit per redraw, + # which you can see directly in how smoothly it scrolls. - id: style_bg_grad bg_color: ${BG_TOP_COLOR} bg_grad_color: ${BG_BOTTOM_COLOR} @@ -2140,7 +2139,7 @@ lvgl: y: ${ALERT_TITLE_Y} width: ${ALERT_TEXT_W} long_mode: DOT - text: "Melding" + text: "Notification" text_font: headline text_color: 0x1B1B1B clickable: false @@ -2185,7 +2184,7 @@ lvgl: - label: id: alert_ok_label align: CENTER - text: "Oké" + text: "OK" text_font: headline text_color: 0xFFFFFF clickable: false @@ -2254,8 +2253,8 @@ lvgl: 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. + # Direct local feedback: LVGL applies this as soon as the finger + # touches the glass, in the same frame. Independent of Home Assistant. pressed: bg_opa: 45% widgets: @@ -2370,8 +2369,8 @@ lvgl: 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. + # Long-press mode "vacuum": opens the vacuum controls. + # Falls outside the action/slider chain, so that stays unchanged. - if: condition: lambda: "return std::string(\"${TILE1_LONGPRESS}\") == \"vacuum\";" @@ -2391,8 +2390,8 @@ lvgl: 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. + # Direct local feedback: LVGL applies this as soon as the finger + # touches the glass, in the same frame. Independent of Home Assistant. pressed: bg_opa: 45% widgets: @@ -2507,8 +2506,8 @@ lvgl: 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. + # Long-press mode "vacuum": opens the vacuum controls. + # Falls outside the action/slider chain, so that stays unchanged. - if: condition: lambda: "return std::string(\"${TILE2_LONGPRESS}\") == \"vacuum\";" @@ -2528,8 +2527,8 @@ lvgl: 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. + # Direct local feedback: LVGL applies this as soon as the finger + # touches the glass, in the same frame. Independent of Home Assistant. pressed: bg_opa: 45% widgets: @@ -2644,8 +2643,8 @@ lvgl: 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. + # Long-press mode "vacuum": opens the vacuum controls. + # Falls outside the action/slider chain, so that stays unchanged. - if: condition: lambda: "return std::string(\"${TILE3_LONGPRESS}\") == \"vacuum\";" @@ -2665,8 +2664,8 @@ lvgl: 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. + # Direct local feedback: LVGL applies this as soon as the finger + # touches the glass, in the same frame. Independent of Home Assistant. pressed: bg_opa: 45% widgets: @@ -2781,8 +2780,8 @@ lvgl: 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. + # Long-press mode "vacuum": opens the vacuum controls. + # Falls outside the action/slider chain, so that stays unchanged. - if: condition: lambda: "return std::string(\"${TILE4_LONGPRESS}\") == \"vacuum\";" @@ -2802,8 +2801,8 @@ lvgl: 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. + # Direct local feedback: LVGL applies this as soon as the finger + # touches the glass, in the same frame. Independent of Home Assistant. pressed: bg_opa: 45% widgets: @@ -2918,8 +2917,8 @@ lvgl: 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. + # Long-press mode "vacuum": opens the vacuum controls. + # Falls outside the action/slider chain, so that stays unchanged. - if: condition: lambda: "return std::string(\"${TILE5_LONGPRESS}\") == \"vacuum\";" @@ -2939,8 +2938,8 @@ lvgl: 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. + # Direct local feedback: LVGL applies this as soon as the finger + # touches the glass, in the same frame. Independent of Home Assistant. pressed: bg_opa: 45% widgets: @@ -3055,8 +3054,8 @@ lvgl: 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. + # Long-press mode "vacuum": opens the vacuum controls. + # Falls outside the action/slider chain, so that stays unchanged. - if: condition: lambda: "return std::string(\"${TILE6_LONGPRESS}\") == \"vacuum\";" @@ -3067,10 +3066,10 @@ lvgl: 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. + # Brightness-only lamp: deliberately no percentage/cover/climate + # sensors were created for this tile, those would only cost + # HA subscriptions and heap. The overlay therefore gets + # NAN / "" for the fields a light never uses. - obj: id: tile7 hidden: true @@ -3191,11 +3190,11 @@ lvgl: 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. + # input_select with options Full/Empty. The tile lights up as soon as + # the status is TILE8_ACTIVE_STATE ("Full"); a short tap sends + # input_select.select_option with option: "Empty". + # No slider mode, so this tile only needs ha_state_tile8 — + # no other HA subscription at all. - obj: id: tile8 hidden: true @@ -3268,8 +3267,8 @@ lvgl: lambda: 'return !runtime_tiles::enabled && cyd::touch_guard.accept(millis(), 8);' then: - script.execute: { id: publish_action, action: "tile8_long_press" } - # Slider-modus kan hier niet zonder eerst de bijbehorende - # sensors aan te maken; "action" werkt wel out of the box. + # Slider mode isn't possible here without first creating the + # corresponding sensors; "action" works out of the box. - if: condition: lambda: "return std::string(\"${TILE8_LONGPRESS}\") == \"action\";" @@ -3286,7 +3285,7 @@ lvgl: param_val: "" was_on: false - # ---------- TILE 9 — Woonkamer (verhuisd van tegel 2) ---------- + # ---------- TILE 9 — Living room (moved from tile 2) ---------- - obj: id: tile9 hidden: true @@ -3406,7 +3405,7 @@ lvgl: climate_max_temp: "30.0" climate_step: "0.5" - # ---------- TILE 10 — Alarm aan/uit ---------- + # ---------- TILE 10 — Alarm on/off ---------- - obj: id: tile10 hidden: true @@ -3523,7 +3522,7 @@ lvgl: - label: align: LEFT_MID x: 24 - text: "< Vorige" + text: "< Previous" text_font: label text_color: 0x1B1B1B clickable: false @@ -3565,7 +3564,7 @@ lvgl: - label: align: RIGHT_MID x: -24 - text: "Volgende >" + text: "Next >" text_font: label text_color: 0x1B1B1B clickable: false @@ -3638,12 +3637,12 @@ lvgl: bg_color: 0xFFFFFF 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. + # The only widget with BOTH a radius AND a child (overlay_icon). + # LVGL then clips children to the rounding, and that's only + # possible via a contiguous 96x160 = 46KB ARGB layer. + # If that allocation fails, LVGL keeps trying until the + # task watchdog restarts the board. The rounding stays, the + # clipping goes. clip_corner: false border_width: 0 @@ -3669,12 +3668,12 @@ lvgl: 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. + # The icon used to be a CHILD in the slider. A widget with a radius + # and children has to clip those children to the rounding, and + # LVGL can only do that via an ARGB layer of tens of KB's. Without + # PSRAM that sometimes fails, after which LVGL keeps retrying until + # the watchdog restarts the board. As a sibling on top, no layer + # is needed and it looks identical. - label: id: overlay_icon align: CENTER @@ -4023,8 +4022,8 @@ lvgl: 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. + # 44 instead of 36: slightly bigger tap target, grows away from the arc + # (top-left anchor stays put) instead of towards it. width: 66 height: 66 radius: 999 @@ -4080,7 +4079,7 @@ lvgl: id: climate_current_label align: TOP_MID y: 84 - text: "Nu: --" + text: "Now: --" text_font: sublabel_big text_color: 0x1B1B1B text_opa: 85% @@ -4092,9 +4091,9 @@ lvgl: 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. + # Without this the arc claims an invisible extra + # click area outside the 184x184 box — near the close + # button top-left this creates overlap. ext_click_area: 0 start_angle: 135 end_angle: 45 @@ -4131,7 +4130,7 @@ lvgl: id: climate_set_label align: CENTER y: -32 - text: "Gewenst" + text: "Target" text_font: sublabel_big text_color: 0x1B1B1B text_opa: 68% @@ -4223,10 +4222,10 @@ lvgl: text_color: 0x1B1B1B 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. + # Replaces cycling through modes. Cycling sends a set_hvac_mode on every + # tap, so to get from 'cool' to 'dry' a heat pump actually passes through + # 'heat' and 'fan_only' along the way. Here you pick directly, and exactly + # one command is sent. - obj: id: climate_mode_overlay x: 0 @@ -4244,13 +4243,13 @@ lvgl: - label: align: TOP_MID y: 32 - text: "Modus" + text: "Mode" text_font: sublabel_big text_color: 0x1B1B1B clickable: false - # Flex zorgt dat niet-ondersteunde modi geen gat achterlaten: - # LVGL slaat verborgen kinderen over bij het schikken. + # Flex ensures unsupported modes don't leave a gap: + # LVGL skips hidden children when arranging. - obj: id: climate_mode_list align: TOP_MID @@ -4288,7 +4287,7 @@ lvgl: widgets: - label: align: CENTER - text: "Uit" + text: "Off" text_font: sublabel_big text_color: 0x1B1B1B clickable: false @@ -4309,7 +4308,7 @@ lvgl: widgets: - label: align: CENTER - text: "Verwarmen" + text: "Heat" text_font: sublabel_big text_color: 0x1B1B1B clickable: false @@ -4330,7 +4329,7 @@ lvgl: widgets: - label: align: CENTER - text: "Koelen" + text: "Cool" text_font: sublabel_big text_color: 0x1B1B1B clickable: false @@ -4351,7 +4350,7 @@ lvgl: widgets: - label: align: CENTER - text: "Verw./koelen" + text: "Heat/Cool" text_font: sublabel_big text_color: 0x1B1B1B clickable: false @@ -4393,7 +4392,7 @@ lvgl: widgets: - label: align: CENTER - text: "Drogen" + text: "Dry" text_font: sublabel_big text_color: 0x1B1B1B clickable: false @@ -4414,7 +4413,7 @@ lvgl: widgets: - label: align: CENTER - text: "Ventileren" + text: "Fan only" text_font: sublabel_big text_color: 0x1B1B1B clickable: false @@ -4423,7 +4422,7 @@ lvgl: id: cfan_title width: 100% height: 24 - text: "Ventilator" + text: "Fan" text_font: sublabel text_color: 0x6B6B6B text_align: CENTER @@ -4520,7 +4519,7 @@ lvgl: id: cswing_title width: 100% height: 24 - text: "Zwenken" + text: "Swing" text_font: sublabel text_color: 0x6B6B6B text_align: CENTER @@ -4640,8 +4639,8 @@ lvgl: # ---------- 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. + # A vacuum is not a switch: start, pause, stop and return to + # the dock are separate commands, and suction power is a separate axis. - obj: id: vacuum_overlay x: 0 @@ -4951,7 +4950,7 @@ lvgl: - label: align: TOP_MID y: 110 - text: "GT911 touchcontrole" + text: "GT911 touch check" text_font: headline text_color: 0x1B1B1B - obj: @@ -5189,8 +5188,8 @@ script: - lvgl.widget.hide: vacuum_overlay # ---------- ALERT (HA action show_alert) ---------- - # The screen wakes, keeps its normal brightness and shows the card until Oké or the - # timeout (0: until Oké); the standby interval waits on alert_active. A new alert + # The screen wakes, keeps its normal brightness and shows the card until OK or the + # timeout (0: until OK); the standby interval waits on alert_active. A new alert # replaces the current one; every ending reports itself as esphome.screen_alert. - id: alert_show mode: restart @@ -5292,8 +5291,8 @@ script: - lambda: |- id(last_touch_ms) = millis(); id(display_dimmed) = false; - // Bijna instant omhoog. De ESPHome-standaard is 1s, en dat voelt traag - // als je het scherm aanraakt en op licht staat te wachten. + // Almost instant brightness-up. ESPHome's default is 1s, and that feels sluggish + // when you touch the screen and are waiting on light. id(set_backlight).execute(screen_settings::current.brightness, 80); - lvgl.widget.hide: dim_wake_overlay - script.execute: close_cards @@ -5331,9 +5330,9 @@ script: id(show_tile_page).execute(); } - # Terug naar het basisscherm bij standby. Bewust via de ESPHome - # acties en NIET met lv_obj_add_flag in een lambda: dit script - # draait vanuit een interval, dus buiten LVGL's eigen afhandeling. + # Back to the home screen on standby. Deliberately via ESPHome's + # actions and NOT with lv_obj_add_flag in a lambda: this script + # runs from an interval, so outside LVGL's own handling. - lvgl.widget.hide: brightness_overlay - lvgl.widget.hide: color_detail_overlay - lvgl.widget.hide: climate_detail_overlay @@ -5407,9 +5406,9 @@ script: 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. + # Opens the mode picker. Hides modes the entity doesn't support and marks + # the current one, so you see where you stand before you pick something. + # Opens the vacuum controls and remembers which entity it concerns. - id: open_vacuum_overlay mode: restart parameters: @@ -5421,7 +5420,7 @@ script: 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. + # A standalone command: start, pause, stop or return_to_base. - id: vacuum_action mode: queued parameters: @@ -5436,7 +5435,7 @@ script: data: entity_id: !lambda "return id(active_entity);" - # Zuigkracht is een aparte as, los van start/stop. + # Suction power is a separate axis, apart from start/stop. - id: vacuum_fan_speed mode: queued parameters: @@ -5503,8 +5502,8 @@ script: fill(id(cswing_title), swing_chips, id(active_climate_swing_modes), id(active_climate_swing_mode)); - lvgl.widget.show: climate_mode_overlay - # Zet precies een modus. Geen tussenstappen, dus geen ongewenste - # commando's naar de warmtepomp onderweg. + # Sets exactly one mode. No intermediate steps, so no unwanted + # commands to the heat pump along the way. # Fan and swing modes from the picker: one call with the entity's own mode name. - id: set_climate_fan_mode mode: restart @@ -5621,7 +5620,7 @@ script: light_controls::fallback.kelvin = 3000; light_controls::fallback.hue = 200; light_controls::open("diagnostic", true, true, 75); - lv_label_set_text(id(color_detail_title), "Voorbeeld lamp"); + lv_label_set_text(id(color_detail_title), "Example lamp"); - lvgl.widget.hide: color_close_button - lvgl.widget.hide: color_done_button - lvgl.widget.show: color_detail_overlay @@ -5832,12 +5831,12 @@ script: 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). + // Debounce: same entity within 600ms ignored. Prevents + // touch noise from firing the same HA action multiple times + // (see globals above for why this is needed). uint32_t now = millis(); if (entity_id == id(last_action_entity) && (now - id(last_action_ms)) < 600) { - ESP_LOGD("tile", "do_tile_action: debounced repeat for %s (%ums na vorige)", + ESP_LOGD("tile", "do_tile_action: debounced repeat for %s (%ums after previous)", entity_id.c_str(), now - id(last_action_ms)); return false; } @@ -6092,11 +6091,11 @@ script: 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. + // Accent colour from the Home Assistant domain: the part before the dot + // in the entity_id. script/scene/button fire (ACCENT_ACTION), a + // light is a light (ACCENT_LIGHT), the rest is a device. + // NOTE: this helper appears twice in this file — here and in + // open_value_overlay. Update them together. auto accent_for = [](const std::string& entity_id) -> uint32_t { std::string d = entity_id.substr(0, entity_id.find('.')); if (d == "script" || d == "scene" || d == "button" || @@ -6105,7 +6104,7 @@ script: return ${ACCENT_DEVICE}; }; - // De sliderkleur komt van de tegel, dus die kan ook ${ACCENT_AUTO} zijn. + // The slider colour comes from the tile, so it can also be ${ACCENT_AUTO}. uint32_t fill_color = (slider_fill_color == ${ACCENT_AUTO}) ? accent_for(entity) : slider_fill_color; @@ -6169,11 +6168,11 @@ script: char current_buf[48]; if (isnan(climate_current_temperature)) { - snprintf(current_buf, sizeof(current_buf), "Nu: --"); + snprintf(current_buf, sizeof(current_buf), "Now: --"); } else if (id(active_climate_humidity) >= 0) { - snprintf(current_buf, sizeof(current_buf), "Nu: %.1f° · %d%%", climate_current_temperature, id(active_climate_humidity)); + snprintf(current_buf, sizeof(current_buf), "Now: %.1f° · %d%%", climate_current_temperature, id(active_climate_humidity)); } else { - snprintf(current_buf, sizeof(current_buf), "Nu: %.1f°", climate_current_temperature); + snprintf(current_buf, sizeof(current_buf), "Now: %.1f°", climate_current_temperature); } lv_label_set_text(id(climate_current_label), current_buf); @@ -6416,8 +6415,8 @@ script: 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. + // active_state not empty? Then only that exact HA state counts as "on". + // This way an input_select ("Full"/"Empty") can also light up a tile. auto is_active = [](const std::string& tile_type, const std::string& state, const std::string& active_state = std::string()) { if (!active_state.empty()) return state == active_state; @@ -6436,7 +6435,7 @@ script: 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. + // Tile 7/8 have no cover_state sensor, so no cover branch needed. bool t7_on = is_active("${TILE7_TYPE}", std::string(id(ha_state_tile7).state.c_str()), "${TILE7_ACTIVE_STATE}"); bool t8_on = is_active("${TILE8_TYPE}", std::string(id(ha_state_tile8).state.c_str()), "${TILE8_ACTIVE_STATE}"); bool t9_on = is_active("${TILE9_TYPE}", std::string(id(ha_state_tile9).state.c_str()), "${TILE9_ACTIVE_STATE}"); @@ -6472,7 +6471,7 @@ script: 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). + // Tile 7: light with only brightness. Tile 8: pure text (Full/Empty). set_value_auto(id(t7_value), t7_on, "${TILE7_TYPE}", "${TILE7_VALUE_MODE}", id(tile7_brightness).state, NAN, std::string(), NAN, NAN, NAN, @@ -6483,7 +6482,7 @@ script: NAN, NAN, NAN, "${TILE8_LABEL_ON}", "${TILE8_LABEL_OFF}"); - // Tegel 9: kleurenlamp (brightness). Tegel 10: puur tekst (Aan/Uit). + // Tile 9: colour lamp (brightness). Tile 10: pure text (On/Off). set_value_auto(id(t9_value), t9_on, "${TILE9_TYPE}", "${TILE9_VALUE_MODE}", id(tile9_brightness).state, NAN, std::string(), NAN, NAN, NAN, @@ -6498,33 +6497,33 @@ script: 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"); + if (state == "cleaning") set_lbl(id(t6_value), "Cleaning"); + else if (state == "docked") set_lbl(id(t6_value), "Docked"); + else if (state == "returning") set_lbl(id(t6_value), "Returning to dock"); + else if (state == "paused") set_lbl(id(t6_value), "Paused"); + else if (state == "error") set_lbl(id(t6_value), "Error"); else if (!state.empty()) set_lbl(id(t6_value), state); } if (!id(ha_state_tile1).has_state() || id(ha_state_tile1).state == "unknown" || id(ha_state_tile1).state == "unavailable") - set_lbl(id(t1_value), "Niet beschikbaar"); + set_lbl(id(t1_value), "Unavailable"); if (!id(ha_state_tile2).has_state() || id(ha_state_tile2).state == "unknown" || id(ha_state_tile2).state == "unavailable") - set_lbl(id(t2_value), "Niet beschikbaar"); + set_lbl(id(t2_value), "Unavailable"); if (!id(ha_state_tile3).has_state() || id(ha_state_tile3).state == "unknown" || id(ha_state_tile3).state == "unavailable") - set_lbl(id(t3_value), "Niet beschikbaar"); + set_lbl(id(t3_value), "Unavailable"); if (!id(ha_state_tile4).has_state() || id(ha_state_tile4).state == "unknown" || id(ha_state_tile4).state == "unavailable") - set_lbl(id(t4_value), "Niet beschikbaar"); + set_lbl(id(t4_value), "Unavailable"); if (!id(ha_state_tile5).has_state() || id(ha_state_tile5).state == "unknown" || id(ha_state_tile5).state == "unavailable") - set_lbl(id(t5_value), "Niet beschikbaar"); + set_lbl(id(t5_value), "Unavailable"); if (!id(ha_state_tile6).has_state() || id(ha_state_tile6).state == "unknown" || id(ha_state_tile6).state == "unavailable") - set_lbl(id(t6_value), "Niet beschikbaar"); + set_lbl(id(t6_value), "Unavailable"); if (!id(ha_state_tile7).has_state() || id(ha_state_tile7).state == "unknown" || id(ha_state_tile7).state == "unavailable") - set_lbl(id(t7_value), "Niet beschikbaar"); + set_lbl(id(t7_value), "Unavailable"); if (!id(ha_state_tile8).has_state() || id(ha_state_tile8).state == "unknown" || id(ha_state_tile8).state == "unavailable") - set_lbl(id(t8_value), "Niet beschikbaar"); + set_lbl(id(t8_value), "Unavailable"); if (!id(ha_state_tile9).has_state() || id(ha_state_tile9).state == "unknown" || id(ha_state_tile9).state == "unavailable") - set_lbl(id(t9_value), "Niet beschikbaar"); + set_lbl(id(t9_value), "Unavailable"); if (!id(ha_state_tile10).has_state() || id(ha_state_tile10).state == "unknown" || id(ha_state_tile10).state == "unavailable") - set_lbl(id(t10_value), "Niet beschikbaar"); + set_lbl(id(t10_value), "Unavailable"); if (id(active_control_kind) == "climate_temperature" && !lv_obj_has_flag(id(climate_detail_overlay), LV_OBJ_FLAG_HIDDEN)) { auto label_for_climate_mode = [](std::string mode) { for (char &c : mode) { @@ -6574,11 +6573,11 @@ script: char current_buf[48]; if (isnan(current_temperature)) { - snprintf(current_buf, sizeof(current_buf), "Nu: --"); + snprintf(current_buf, sizeof(current_buf), "Now: --"); } else if (id(active_climate_humidity) >= 0) { - snprintf(current_buf, sizeof(current_buf), "Nu: %.1f° · %d%%", current_temperature, id(active_climate_humidity)); + snprintf(current_buf, sizeof(current_buf), "Now: %.1f° · %d%%", current_temperature, id(active_climate_humidity)); } else { - snprintf(current_buf, sizeof(current_buf), "Nu: %.1f°", current_temperature); + snprintf(current_buf, sizeof(current_buf), "Now: %.1f°", current_temperature); } lv_label_set_text(id(climate_current_label), current_buf); @@ -6598,9 +6597,9 @@ script: { 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). + // Before the first time sync, now() is epoch 0 (1970) — don't show + // a misleading "01:00" then, just leave it blank until a + // real time comes in (after the first Home Assistant time sync). if (now.is_valid()) { char buf[10]; @@ -6616,11 +6615,11 @@ script: } } - // 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. + // Accent colour from the Home Assistant domain: the part before the dot + // in the entity_id. script/scene/button fire (ACCENT_ACTION), a + // light is a light (ACCENT_LIGHT), the rest is a device. + // NOTE: this helper appears twice in this file — here and in + // open_value_overlay. Update them together. auto accent_for = [](const std::string& entity_id) -> uint32_t { std::string d = entity_id.substr(0, entity_id.find('.')); if (d == "script" || d == "scene" || d == "button" || @@ -6661,7 +6660,7 @@ script: break; } } - // ${ACCENT_AUTO} = "bepaal zelf"; een echte hexwaarde wint altijd. + // ${ACCENT_AUTO} = "figure it out yourself"; a real hex value always wins. uint32_t accent = accent_for(entity_id); if (circle_active == ${ACCENT_AUTO}) circle_active = accent; if (icon_disabled == ${ACCENT_AUTO}) icon_disabled = accent; @@ -6757,13 +6756,13 @@ script: text: - platform: smart_display id: dashboard_inbox - name: "Tegelinstellingen" + name: "Tile settings" internal: ${DASHBOARD_INBOX_INTERNAL} number: - platform: template id: setting_brightness - name: "Helderheid normaal" + name: "Normal brightness" entity_category: config min_value: 5 max_value: 100 @@ -6782,7 +6781,7 @@ number: id(apply_screen_settings).execute(); - platform: template id: setting_standby_brightness - name: "Helderheid standby" + name: "Standby brightness" entity_category: config min_value: 0 max_value: 100 @@ -6801,7 +6800,7 @@ number: id(apply_screen_settings).execute(); - platform: template id: setting_night_brightness - name: "Helderheid nacht" + name: "Night brightness" entity_category: config min_value: 0 max_value: 100 @@ -6820,7 +6819,7 @@ number: id(apply_screen_settings).execute(); - platform: template id: setting_standby_seconds - name: "Standby na" + name: "Standby after" entity_category: config min_value: 60 max_value: 86400 diff --git a/screen_manager/CHANGELOG.md b/screen_manager/CHANGELOG.md index 3c23156..3dbabff 100644 --- a/screen_manager/CHANGELOG.md +++ b/screen_manager/CHANGELOG.md @@ -1,3 +1,14 @@ +## 0.2.40 (firmware 0.2.34) + +ESP Screens is now in English: the screens, the management page, the Home Assistant entities and the documentation. + +- **English everywhere.** Every label on both boards (tiles, detail cards, the climate and vacuum cards, page navigation, calibration), the ESP Screens editor, the Alerts cheatsheet, error messages and logs, and the README and guides. The top bar writes numbers the English way (21.5 °C, 1,249 W). State labels follow Home Assistant's own English names, for example Heat and Cool for climate modes, Heating and Cooling for what the device is doing, Away, Returning to dock, and weather conditions such as Lightning, rainy. Dates use English abbreviations (Mo 14 Sep). +- **English entity names.** Firmware 0.2.34 names its entities Tile settings, Screen firmware, Device name, IP address, Guition screen type, Last boot, Normal brightness, Standby brightness, Night brightness, Standby after and Calibrate touch. Home Assistant registers a renamed ESPHome entity under a new entity id and removes the old one, so `text.kitchen_screen_tegelinstellingen` becomes `text.kitchen_screen_tile_settings`. Dashboards or automations that use one of these diagnostic entities need the new id. +- **Your layouts move along.** ESP Screen Manager recognises a screen that comes back under a new inbox id and moves its tiles, settings, top bar and update history to it, also when the app restarts in between. The **Update** button and the nightly round follow the screen through the rename and report the update as successful. +- **Older firmware keeps working.** Screens on firmware 0.2.33 or older still report the Dutch entity names and status messages; the app recognises both until they are updated. +- Docs use Home Assistant's current menu names (**Settings → Apps**, **App store**, **Settings → Tools**), and the guides are now `docs/TILES.md`, `docs/CALIBRATING.md`, `docs/ACCEPTANCE.md` and `docs/TROUBLESHOOTING.md`. +- Firmware 0.2.34 for both boards; the app offers the update. Besides the English text and entity names it changes nothing on the screen. + ## 0.2.39 (firmware 0.2.33) Zuiniger in dagelijks gebruik, ook met tientallen schermen. Niets verandert aan wat je ziet of instelt. diff --git a/screen_manager/DOCS.md b/screen_manager/DOCS.md index d2074f2..ba4d05f 100644 --- a/screen_manager/DOCS.md +++ b/screen_manager/DOCS.md @@ -1,23 +1,23 @@ -# Installatie en dagelijks gebruik +# Installation and everyday use -Zie [de volledige 0.2.0-handleiding](https://github.com/MaxGramser/homeassistant_espscreen/blob/main/docs/WHATS_NEW_020.md). +See [the full installation guide](https://github.com/MaxGramser/homeassistant_espscreen/blob/main/docs/EASY_SETUP.md). -Open **Nieuw scherm** om een scherm te installeren: sluit het via USB aan op de -Home Assistant-machine, kies bord, naam en USB-poort en klik **Installeren**. -Het profiel met unieke sleutels komt in de ESPHome-map van de HA-configuratie, -ontbrekende wifi gaat in `secrets.yaml` (bestaande secrets blijven staan), en de -build en de flash lopen in hetzelfde venster; daarna toont het de API-sleutel -voor de koppeling. De koppeling zelf doe je in Home Assistant onder **Instellingen → Apparaten & diensten** (knop in het venster en op de kaart *nog niet in Home Assistant* onder Mijn schermen). Elk scherm heeft zijn eigen profiel. **Firmware & USB** is -voor bestaande profielen: controleren, bouwen of opnieuw installeren via USB of -het IP-adres (OTA). Bestaande ESPHome-profielen in de HA-configmap worden -automatisch gevonden. +Open **New screen** to install a screen: connect it via USB to the +Home Assistant machine, choose the board, name and USB port, then click **Install**. +The profile with unique keys goes into the ESPHome folder of the HA configuration, +any missing Wi-Fi goes into `secrets.yaml` (existing secrets are left untouched), and the +build and flash run in the same window; it then shows the API key +for pairing. Pairing itself happens in Home Assistant under **Settings → Devices & services** (a button in the window and on the *not yet in Home Assistant* card under My screens). Each screen has its own profile. **Firmware & USB** is +for existing profiles: check, build or reinstall via USB or +the IP address (OTA). Existing ESPHome profiles in the HA config folder are +found automatically. -Bovenaan de editor stel je per scherm de **Bovenbalk** in: links de naam, rechts -tot zes onderdelen (tijd, analoge klok, datum of een entiteit met icoon, zoals -temperatuur, een deur, het alarm of "laatst gewijzigd"). Sleep om te ordenen, tik -om in te stellen; firmware 0.2.32 of nieuwer tekent ze. +At the top of the editor you set the **Top bar** for each screen: the name on the left, up +to six items on the right (time, analog clock, date, or an entity with an icon, such as +temperature, a door, the alarm, or "last changed"). Drag to reorder, tap +to configure; firmware 0.2.32 or newer renders them. -Na koppeling via de HA ESPHome-integratie kies je in ESP Screens de tegels. -Onder **Tegelinstellingen** staan klikgedrag, grotere waarden en mini-schuiven. -Onder **Scherminstellingen** staan helderheid en standby. **Inspector** helpt bij -ontbrekende attributen of een offline scherm. Nieuwe kaarten vereisen firmware 0.2.0. +After pairing via the HA ESPHome integration, choose the tiles in ESP Screens. +**Tile settings** has click behavior, larger values and mini-sliders. +**Screen settings** has brightness and standby. **Inspector** helps with +missing attributes or an offline screen. New cards require firmware 0.2.0. diff --git a/screen_manager/README.md b/screen_manager/README.md index d056152..9e5da10 100644 --- a/screen_manager/README.md +++ b/screen_manager/README.md @@ -1,8 +1,8 @@ # ESP Screen Manager -Zoek je Home Assistant-entiteiten, orden maximaal twintig tegels en stuur wijzigingen -naar een CYD- of Guition-scherm zonder opnieuw flashen. Met installatiehulp, -unieke apparaat-YAML en ondersteuning voor meerdere schermen. +Find your Home Assistant entities, arrange up to twenty tiles, and push changes +to a CYD or Guition screen without reflashing. Includes installation help, +a unique per-device YAML, and support for multiple screens. -Open de webinterface na installatie. Zie **Documentatie** voor de eerste flash. -De app gebruikt Home Assistant Ingress en heeft geen eigen login of token nodig. +Open the web interface after installation. See **Documentation** for the first flash. +The app uses Home Assistant Ingress and needs no login or token of its own. diff --git a/screen_manager/app/core.py b/screen_manager/app/core.py index 993e7f1..6dbc286 100644 --- a/screen_manager/app/core.py +++ b/screen_manager/app/core.py @@ -11,31 +11,31 @@ import tile_icons DOMAINS = frozenset('light switch input_boolean scene script climate vacuum fan cover sensor binary_sensor input_select select number input_number weather media_player button input_button sun timer person screen'.split()) # Built-in cards without a Home Assistant entity; firmware 0.2.14+ renders them. -BUILTIN = {'screen.clock': 'Klok'} +BUILTIN = {'screen.clock': 'Clock'} NEW_DOMAINS = frozenset('sun timer person screen'.split()) -WEEKDAYS = ['ma', 'di', 'wo', 'do', 'vr', 'za', 'zo'] +WEEKDAYS = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'] REPO = 'https://github.com/MaxGramser/homeassistant_espscreen' REFS = {'cyd': 'main', 'guition': 'main'} # Firmware shipped with this app release; screens below it get an update offer. -FIRMWARE_VERSION = '0.2.33' +FIRMWARE_VERSION = '0.2.34' ATTRS = frozenset('brightness percentage current_position current_temperature temperature current_humidity min_temp max_temp target_temp_step supported_color_modes hvac_modes hvac_action hs_color color_temp_kelvin min_color_temp_kelvin max_color_temp_kelvin fan_speed_list unit_of_measurement battery_level fan_speed volume_level is_volume_muted media_title options min max step temperature_unit supported_features device_class next_rising next_setting finishes_at duration remaining humidity wind_speed wind_speed_unit apparent_temperature fan_modes swing_modes fan_mode swing_mode'.split()) # Attributes whose boolean value the screen needs; every other bool stays behind. BOOL_ATTRS = frozenset(['is_volume_muted']) TILE_BACKGROUNDS = { - 'auto': {'label': 'Standaard', 'color': None}, + 'auto': {'label': 'Default', 'color': None}, # No card behind the tile: contents keep their size and place on the screen background. - 'none': {'label': 'Geen', 'color': None}, - 'red': {'label': 'Rood', 'color': '#FADADD'}, - 'orange': {'label': 'Oranje', 'color': '#FFE1C6'}, - 'yellow': {'label': 'Geel', 'color': '#FFF0C2'}, - 'green': {'label': 'Groen', 'color': '#D9EEDC'}, + 'none': {'label': 'None', 'color': None}, + 'red': {'label': 'Red', 'color': '#FADADD'}, + 'orange': {'label': 'Orange', 'color': '#FFE1C6'}, + 'yellow': {'label': 'Yellow', 'color': '#FFF0C2'}, + 'green': {'label': 'Green', 'color': '#D9EEDC'}, 'mint': {'label': 'Mint', 'color': '#D5F0EA'}, - 'blue': {'label': 'Blauw', 'color': '#D9EAFB'}, - 'purple': {'label': 'Paars', 'color': '#E9DDF5'}, - 'pink': {'label': 'Roze', 'color': '#F7DDEC'}, - 'gray': {'label': 'Grijs', 'color': '#E5E7EB'}, + 'blue': {'label': 'Blue', 'color': '#D9EAFB'}, + 'purple': {'label': 'Purple', 'color': '#E9DDF5'}, + 'pink': {'label': 'Pink', 'color': '#F7DDEC'}, + 'gray': {'label': 'Gray', 'color': '#E5E7EB'}, } # Display modes per domain; everything else offers standard and watch (large value). @@ -77,28 +77,28 @@ def has_gaps(tiles): # Home Assistant's own entity rows. The first choice is what a wide card shows # when the tile has no explicit choice; 'none' keeps the plain card. CONTROLS = { - 'climate': (('setpoint', 'Temperatuur − / +'), ('mode', 'Uit, verwarmen, koelen')), - 'switch': (('toggle', 'Aan/uit-schakelaar'),), - 'input_boolean': (('toggle', 'Aan/uit-schakelaar'),), - 'light': (('toggle', 'Aan/uit-schakelaar'), ('brightness', 'Helderheidsschuif')), - 'fan': (('toggle', 'Aan/uit-schakelaar'), ('speed', 'Snelheidsschuif')), - 'vacuum': (('buttons', 'Start, stop, naar dock'),), - 'cover': (('buttons', 'Open, stop, dicht'), ('position', 'Positieschuif')), - 'media_player': (('volume', 'Volume en dempen'), ('playback', 'Vorige, play/pauze, volgende')), - 'number': (('stepper', 'Waarde − / +'), ('slider', 'Schuif')), - 'input_number': (('stepper', 'Waarde − / +'), ('slider', 'Schuif')), - 'select': (('stepper', 'Vorige / volgende keuze'),), - 'input_select': (('stepper', 'Vorige / volgende keuze'),), - 'timer': (('buttons', 'Start/pauze en annuleren'),), - 'scene': (('run', 'Knop Activeren'),), - 'script': (('run', 'Knop Uitvoeren'),), - 'button': (('run', 'Knop Indrukken'),), - 'input_button': (('run', 'Knop Indrukken'),), + 'climate': (('setpoint', 'Temperature − / +'), ('mode', 'Off, heat, cool')), + 'switch': (('toggle', 'On/off switch'),), + 'input_boolean': (('toggle', 'On/off switch'),), + 'light': (('toggle', 'On/off switch'), ('brightness', 'Brightness slider')), + 'fan': (('toggle', 'On/off switch'), ('speed', 'Speed slider')), + 'vacuum': (('buttons', 'Start, stop, dock'),), + 'cover': (('buttons', 'Open, stop, close'), ('position', 'Position slider')), + 'media_player': (('volume', 'Volume and mute'), ('playback', 'Previous, play/pause, next')), + 'number': (('stepper', 'Value − / +'), ('slider', 'Slider')), + 'input_number': (('stepper', 'Value − / +'), ('slider', 'Slider')), + 'select': (('stepper', 'Previous / next choice'),), + 'input_select': (('stepper', 'Previous / next choice'),), + 'timer': (('buttons', 'Start/pause and cancel'),), + 'scene': (('run', 'Activate button'),), + 'script': (('run', 'Run button'),), + 'button': (('run', 'Press button'),), + 'input_button': (('run', 'Press button'),), } def controls_catalogue(): """Editor choices per domain: the default first, then 'none'.""" - return {domain: {'default': choices[0][0], 'choices': [{'key': key, 'label': label} for key, label in choices] + [{'key': 'none', 'label': 'Geen'}]} + return {domain: {'default': choices[0][0], 'choices': [{'key': key, 'label': label} for key, label in choices] + [{'key': 'none', 'label': 'None'}]} for domain, choices in CONTROLS.items()} def resolve_controls(tile): @@ -111,7 +111,42 @@ def resolve_controls(tile): return None if choice == 'none' else choice # Diagnostic entities every ESP Screens firmware exposes; the manager watches them for screens. -SCREEN_ENTITY_NAMES = frozenset({'Tegelinstellingen', 'Schermfirmware', 'Guition schermtype', 'Apparaatnaam', 'IP-adres'}) +# Firmware built before the English translation still registers the Dutch originals, so both +# forms are recognised until every board has been reflashed with an English `name:`. +NAME_TILE_SETTINGS = ('Tile settings', 'Tegelinstellingen') +NAME_SCREEN_FIRMWARE = ('Screen firmware', 'Schermfirmware') +NAME_GUITION_TYPE = ('Guition screen type', 'Guition schermtype') +NAME_DEVICE_NAME = ('Device name', 'Apparaatnaam') +NAME_IP_ADDRESS = ('IP address', 'IP-adres') +SCREEN_ENTITY_NAMES = frozenset(NAME_TILE_SETTINGS + NAME_SCREEN_FIRMWARE + NAME_GUITION_TYPE + NAME_DEVICE_NAME + NAME_IP_ADDRESS) + +def entity_slug(name): + """The end of an entity id Home Assistant derives from an entity name (ASCII names).""" + return re.sub(r'[^a-z0-9]+', '_', str(name).lower()).strip('_') + +# An inbox entity id is text._tile_settings, or text._tegelinstellingen before firmware 0.2.34. +INBOX_SUFFIXES = tuple('_' + entity_slug(name) for name in NAME_TILE_SETTINGS) + +def inbox_prefix(entity): + """The device part of an inbox entity id ('office_1' for text.office_1_tile_settings), or None.""" + if isinstance(entity, str) and entity.startswith('text.'): + for suffix in INBOX_SUFFIXES: + if entity.endswith(suffix) and len(entity) > len('text.') + len(suffix): + return entity[len('text.'):-len(suffix)] + return None + +def device_prefixes(registry, device_id): + """Entity id prefixes Home Assistant gave the ESPHome entities of one device: the device name when each + entity was created. Entities whose name never changed keep the prefix an older inbox id carried.""" + prefixes = set() + for item in registry: + name = item.get('original_name') + if item.get('device_id') != device_id or item.get('platform') != 'esphome' or not isinstance(name, str): + continue + object_id, suffix = item['entity_id'].split('.', 1)[-1], '_' + entity_slug(name) + if len(suffix) > 1 and object_id.endswith(suffix) and len(object_id) > len(suffix): + prefixes.add(object_id[:-len(suffix)]) + return prefixes # Additive schema 1 extension. An absent object retains old firmware/YAML defaults. SETTING_RULES = { @@ -132,7 +167,7 @@ SETTING_RULES = { def validate_settings(data): if not isinstance(data, dict) or set(data) - SETTING_RULES.keys(): - raise ValueError('Onbekende scherminstellingen; vernieuw de beheerpagina.') + raise ValueError('Unknown screen settings; refresh the management page.') clean = {} for key, (default, minimum, maximum) in SETTING_RULES.items(): value = data.get(key, default) @@ -142,10 +177,10 @@ def validate_settings(data): valid = type(value) is int and minimum <= value <= maximum if key == "rotation": valid = valid and value in (0, 90, 180, 270) if not valid: - raise ValueError(f'Ongeldige waarde voor {key}.') + raise ValueError(f'Invalid value for {key}.') clean[key] = value if max(clean['standby_brightness'], clean['night_brightness']) > clean['brightness']: - raise ValueError('Helderheid in standby en nacht mag niet hoger zijn dan normaal.') + raise ValueError('Standby and night brightness may not be higher than normal.') return clean def entity_id(value): @@ -163,7 +198,7 @@ TRANSPORT_MIN_FIRMWARE = (0, 2, 33) MESSAGE_ACTION = 'screen_message' HEADER_MAX_ITEMS = 6 # Items the screen draws on its own clock, without Home Assistant. -HEADER_BUILTIN = {'clock': 'Tijd', 'analog': 'Analoge klok', 'date': 'Datum'} +HEADER_BUILTIN = {'clock': 'Time', 'analog': 'Analog clock', 'date': 'Date'} # Only shown, never controlled: the top bar takes these besides every tile domain. HEADER_ONLY_DOMAINS = frozenset('device_tracker zone lock alarm_control_panel counter event input_datetime input_text water_heater humidifier'.split()) HEADER_CONTENTS = ('state', 'last_changed') @@ -181,32 +216,32 @@ def header_items(layout): def validate_header(data): if not isinstance(data, dict) or set(data) - {'items'} or not isinstance(data.get('items'), list): - raise ValueError('Ongeldige bovenbalk; vernieuw de beheerpagina.') + raise ValueError('Invalid top bar; refresh the management page.') if len(data['items']) > HEADER_MAX_ITEMS: - raise ValueError(f'De bovenbalk heeft plaats voor maximaal {HEADER_MAX_ITEMS} onderdelen.') + raise ValueError(f'The top bar has room for at most {HEADER_MAX_ITEMS} items.') items, seen = [], set() for item in data['items']: kind = item.get('type') if isinstance(item, dict) else None if kind in HEADER_BUILTIN: if set(item) != {'type'}: - raise ValueError('Ongeldige instelling in de bovenbalk.') + raise ValueError('Invalid setting in the top bar.') clean = {'type': kind} elif kind == 'entity': if set(item) - {'type', 'entity', 'content', 'icon', 'show'}: - raise ValueError('Onbekende instelling in de bovenbalk; vernieuw de beheerpagina.') + raise ValueError('Unknown setting in the top bar; refresh the management page.') if not header_entity(item.get('entity')): - raise ValueError('Deze entiteit kan niet in de bovenbalk.') + raise ValueError("This entity can't go in the top bar.") clean = {'type': 'entity', 'entity': item['entity'], 'content': item.get('content', 'state'), 'icon': item.get('icon', 'auto'), 'show': item.get('show', 'always')} if clean['content'] not in HEADER_CONTENTS or clean['show'] not in HEADER_SHOWS: - raise ValueError('Ongeldige instelling in de bovenbalk.') + raise ValueError('Invalid setting in the top bar.') if not (clean['icon'] in ('auto', 'none') or isinstance(clean['icon'], str) and clean['icon'] in tile_icons.ICONS): - raise ValueError('Kies een icoon uit de lijst.') + raise ValueError('Choose an icon from the list.') else: - raise ValueError('Onbekend onderdeel in de bovenbalk; vernieuw de beheerpagina.') + raise ValueError('Unknown item in the top bar; refresh the management page.') key = json.dumps(clean, sort_keys=True) if key in seen: - raise ValueError('Dit onderdeel staat al in de bovenbalk.') + raise ValueError('This item is already in the top bar.') seen.add(key) items.append(clean) return {'items': items} @@ -226,52 +261,52 @@ def short(value, limit): def validate_layout(data): if not isinstance(data, dict): - raise ValueError('Ongeldige indeling.') + raise ValueError('Invalid layout.') title, tiles = data.get('title'), data.get('tiles') if not isinstance(title, str) or not title.strip() or len(title.encode()) > 96: - raise ValueError('Geef het scherm een titel van maximaal 96 bytes.') + raise ValueError('Give the screen a title of at most 96 bytes.') if not isinstance(tiles, list) or len(tiles) > 20: - raise ValueError('Kies maximaal 20 tegels.') + raise ValueError('Choose at most 20 tiles.') clean, seen = [], set() for tile in tiles: if not isinstance(tile, dict) or not entity_id(tile.get('entity')): - raise ValueError('Deze entiteit wordt niet ondersteund.') + raise ValueError("This entity isn't supported.") if tile['entity'] in seen: - raise ValueError('Een entiteit kan maar één keer op een scherm staan.') + raise ValueError('An entity can only appear once on a screen.') name = tile.get('name', '') if not isinstance(name, str) or len(name.encode()) > 80: - raise ValueError('Een tegelnaam mag maximaal 80 bytes bevatten.') + raise ValueError('A tile name may contain at most 80 bytes.') seen.add(tile['entity']) item = {'entity': tile['entity'], 'name': name.strip()} if 'options' in tile: options = tile['options'] if not isinstance(options, dict) or set(options) - {'tap', 'display', 'inline', 'history_hours', 'background', 'size', 'icon', 'controls'}: - raise ValueError('Onbekende tegelinstellingen.') + raise ValueError('Unknown tile settings.') if 'background' in options and (not isinstance(options['background'],str) or options['background'] not in TILE_BACKGROUNDS): - raise ValueError('Kies een pastel achtergrondkleur uit het palet.') + raise ValueError('Choose a pastel background color from the palette.') if 'icon' in options and not (options['icon'] == 'auto' or isinstance(options['icon'], str) and options['icon'] in tile_icons.ICONS): - raise ValueError('Kies een icoon uit de lijst.') + raise ValueError('Choose an icon from the list.') domain = tile['entity'].split('.')[0] displays = DISPLAYS.get(domain, ('standard', 'watch')) choices = {'tap': ('auto', 'detail', 'toggle', 'none'), 'display': displays, 'inline': ('none', 'slider'), 'size': ('single', 'wide')} for key, allowed in choices.items(): if key in options and options[key] not in allowed: - raise ValueError('Ongeldige tegelinstelling: ' + key) + raise ValueError('Invalid tile setting: ' + key) # The five-day strip and the sun path only fit a double-width card. if options.get('display') in WIDE_ONLY: options = {**options, 'size': 'wide'} if options.get('tap') == 'toggle' and domain not in {'light','switch','input_boolean','fan','media_player','climate'}: - raise ValueError('Deze entiteit ondersteunt geen aan/uit-actie.') + raise ValueError("This entity doesn't support an on/off action.") if options.get('inline') == 'slider' and domain not in {'light','fan','cover','number','input_number','media_player'}: - raise ValueError('Deze entiteit ondersteunt geen mini-schuif.') + raise ValueError("This entity doesn't support a mini-slider.") if 'history_hours' in options and (type(options['history_hours']) is not int or options['history_hours'] not in (1,6,24)): - raise ValueError('Geschiedenis: kies 1, 6 of 24 uur.') + raise ValueError('History: choose 1, 6, or 24 hours.') if options.get('display') == 'watch' and options.get('inline') == 'slider': - raise ValueError('Kies grote waarde of mini-schuif.') + raise ValueError('Choose large value or mini-slider.') if 'controls' in options: allowed = ('none',) + tuple(key for key, _ in CONTROLS.get(domain, ())) if not isinstance(options['controls'], str) or options['controls'] not in allowed: - raise ValueError('Deze entiteit ondersteunt die directe bediening niet.') + raise ValueError("This entity doesn't support that direct control.") item['options'] = dict(options) clean.append(item) # Positions: every tile or none (an older editor sends none and keeps its order). @@ -280,12 +315,12 @@ def validate_layout(data): occupied = set() for item, slot in zip(clean, given): if type(slot) is not int or not 0 <= slot < MAX_SLOTS: - raise ValueError('Ongeldige tegelpositie; vernieuw de beheerpagina.') + raise ValueError('Invalid tile position; refresh the management page.') if is_wide(item) and slot % 2: - raise ValueError('Een dubbelbrede tegel begint in de linkerkolom.') + raise ValueError('A double-width tile starts in the left column.') for cell in footprint(slot, is_wide(item)): if cell in occupied: - raise ValueError('Twee tegels staan op dezelfde plek.') + raise ValueError('Two tiles are in the same spot.') occupied.add(cell) item['slot'] = slot clean.sort(key=lambda item: item['slot']) @@ -296,7 +331,7 @@ def validate_layout(data): # Pages kept on purpose, empty ones included; the screen shows at least what the tiles need. if 'pages' in data: if type(data['pages']) is not int or not 1 <= data['pages'] <= MAX_PAGES: - raise ValueError(f'Een scherm heeft 1 tot {MAX_PAGES} pagina\'s.') + raise ValueError(f'A screen has 1 to {MAX_PAGES} pages.') result['pages'] = data['pages'] if 'settings' in data: result['settings'] = validate_settings(data['settings']) @@ -451,7 +486,7 @@ def encode(message): """The message as the firmware parses it: compact JSON, at most 4096 bytes.""" raw = json.dumps(message, ensure_ascii=False, separators=(',', ':'), allow_nan=False) if len(raw.encode()) > 4096: - raise ValueError('Het schermbericht is te groot.') + raise ValueError('The screen message is too large.') return raw def revision(message): @@ -473,20 +508,20 @@ def message_action(node): # keeps every value here equal to what the board profiles compile. ----- ALERT_MIN_FIRMWARE = '0.2.31' ALERT_EVENT = 'esphome.screen_alert' -ALERT_ENDINGS = (('ok', 'De knop is ingedrukt'), ('timeout', 'De timeout is verstreken'), - ('replaced', 'Een nieuwe alert kwam eroverheen'), ('remote', 'dismiss_alert vanuit Home Assistant')) +ALERT_ENDINGS = (('ok', 'The button was pressed'), ('timeout', 'The timeout ran out'), + ('replaced', 'A new alert came over it'), ('remote', 'dismiss_alert from Home Assistant')) ALERT_FALLBACK_ICON = 'alert-outline' # (field, ESPHome type, label, explanation, example) in the order Home Assistant shows them. ALERT_FIELDS = ( - ('title', 'string', 'Titel', 'Eén regel bovenaan de kaart; wat niet past krijgt puntjes. Leeg wordt "Melding".', 'Iemand belt aan'), - ('subtitle', 'string', 'Subtitle', 'Toelichting onder de titel, over meerdere regels; een regeleinde mag. Leeg mag.', 'Deur 3, achterkant'), - ('icon', 'string', 'Icoon', 'Een naam uit de lijst, ook als mdi:naam of als hex-codepoint (F12E6). Onbekend of leeg geeft de waarschuwingsdriehoek.', 'doorbell'), - ('color', 'string', 'Kleur', 'Een van de negen pastelkleuren van de tegels. Leeg geeft de witte kaart.', 'orange'), - ('button_text', 'string', 'Knoptekst', 'De tekst op de knop. Leeg is "Oké".', 'Ik kom'), - ('timeout', 'int', 'Timeout', 'Seconden waarna de kaart vanzelf verdwijnt. 0 wacht op de knop, hoe lang dat ook duurt. De knop sluit altijd direct, ook met een timeout.', 0), - ('flash', 'bool', 'Knipperen', 'Aan laat de backlight vier keer knipperen als de alert binnenkomt; daarna blijft het scherm gewoon aan.', True), + ('title', 'string', 'Title', 'A single line at the top of the card; what doesn\'t fit gets an ellipsis. Empty becomes "Notification".', 'Someone is at the door'), + ('subtitle', 'string', 'Subtitle', 'Explanation under the title, across multiple lines; a line break is fine. Empty is fine.', 'Door 3, back'), + ('icon', 'string', 'Icon', 'A name from the list, also as mdi:name or as a hex codepoint (F12E6). Unknown or empty gives the warning triangle.', 'doorbell'), + ('color', 'string', 'Color', 'One of the nine pastel colors of the tiles. Empty gives the white card.', 'orange'), + ('button_text', 'string', 'Button text', 'The text on the button. Empty is "OK".', 'Coming'), + ('timeout', 'int', 'Timeout', 'Seconds after which the card disappears on its own. 0 waits for the button, however long that takes. The button always closes it immediately, even with a timeout.', 0), + ('flash', 'bool', 'Blinking', 'On makes the backlight blink four times when the alert arrives; the screen then just stays on.', True), ) -# Bytes per field the firmware keeps (the profiles' ALERT_*_MAX); a Dutch letter with accent takes two. +# Bytes per field the firmware keeps (the profiles' ALERT_*_MAX); an accented letter takes two. ALERT_LIMITS = {'cyd': {'title': 48, 'subtitle': 160, 'button_text': 12}, 'guition': {'title': 64, 'subtitle': 240, 'button_text': 16}} ALERT_SUGGESTED_ICONS = ('doorbell', 'bell', 'bell-ring', 'alert-outline', 'alarm-light', 'lock', 'lock-open-variant', 'door-open', 'window-closed-variant', 'motion-sensor', 'cctv', 'smoke-detector', 'water-alert', 'fire', 'mailbox', 'car', @@ -515,35 +550,35 @@ def discover_screens(registry, states, devices, areas): """Paired screens: every enabled ESPHome inbox with the diagnostics of its device.""" device_map = {d['id']: d for d in devices} area_map = {a['area_id']: a['name'] for a in areas} - versions = {item.get('device_id'): states.get(item['entity_id'], {}).get('state', 'onbekend') for item in registry - if item.get('platform') == 'esphome' and item.get('original_name') == 'Schermfirmware'} + versions = {item.get('device_id'): states.get(item['entity_id'], {}).get('state', 'unknown') for item in registry + if item.get('platform') == 'esphome' and item.get('original_name') in NAME_SCREEN_FIRMWARE} boards = {item.get("device_id"): "guition" for item in registry - if item.get("platform") == "esphome" and item.get("original_name") == "Guition schermtype"} - def diagnostic(name, pattern): + if item.get("platform") == "esphome" and item.get("original_name") in NAME_GUITION_TYPE} + def diagnostic(names, pattern): found = {} for item in registry: - if item.get('platform') == 'esphome' and item.get('original_name') == name: + if item.get('platform') == 'esphome' and item.get('original_name') in names: value = states.get(item['entity_id'], {}).get('state', '') if isinstance(value, str) and re.fullmatch(pattern, value): found[item.get('device_id')] = value return found - nodes = diagnostic('Apparaatnaam', r'[a-z0-9][a-z0-9-]{0,30}') - addresses = diagnostic('IP-adres', r'\d{1,3}(\.\d{1,3}){3}') + nodes = diagnostic(NAME_DEVICE_NAME, r'[a-z0-9][a-z0-9-]{0,30}') + addresses = diagnostic(NAME_IP_ADDRESS, r'\d{1,3}(\.\d{1,3}){3}') screens = [] for item in registry: eid = item['entity_id'] - if not (item.get('platform') == 'esphome' and eid.startswith('text.') and item.get('original_name') == 'Tegelinstellingen' and not item.get('disabled_by')): + if not (item.get('platform') == 'esphome' and eid.startswith('text.') and item.get('original_name') in NAME_TILE_SETTINGS and not item.get('disabled_by')): continue device = device_map.get(item.get('device_id'), {}) state = states.get(eid, {}) area = area_map.get(item.get('area_id') or device.get('area_id'), '') screens.append({'id': eid, 'name': device.get('name_by_user') or device.get('name') or eid, - 'firmware': versions.get(item.get('device_id'), 'onbekend'), + 'firmware': versions.get(item.get('device_id'), 'unknown'), 'board': boards.get(item.get('device_id'), 'unknown'), 'node': nodes.get(item.get('device_id')), 'ip': addresses.get(item.get('device_id')), 'device': device.get('name') or '', 'area': area, 'online': state.get('state') not in (None, 'unknown', 'unavailable'), - 'status': state.get('state', 'Niet verbonden')}) + 'status': state.get('state', 'Not connected')}) return screens def discover(registry, states, devices, areas): @@ -578,13 +613,13 @@ def discover(registry, states, devices, areas): def installation_yaml(data): board, name, friendly = data.get('board'), data.get('name'), data.get('friendly_name') if board not in REFS or not isinstance(name, str) or not re.fullmatch(r'[a-z][a-z0-9-]{0,29}', name): - raise ValueError('Kies een bord en een unieke naam (kleine letters, cijfers, streepjes; maximaal 30 tekens).') + raise ValueError('Choose a board and a unique name (lowercase letters, digits, dashes; 30 characters max).') if not isinstance(friendly, str) or not friendly.strip() or len(friendly) > 60: - raise ValueError('Geef het scherm een herkenbare naam (maximaal 60 tekens).') + raise ValueError('Give the screen a recognizable name (60 characters max).') quote = lambda s: json.dumps(s, ensure_ascii=False) key, ota, ap = base64.b64encode(secrets.token_bytes(32)).decode(), secrets.token_urlsafe(24), secrets.token_urlsafe(12) - return f'''# Bewaar dit bestand: het bevat de unieke sleutels voor dit scherm. -# Wifi komt uit de secrets.yaml van ESPHome Device Builder. + return f'''# Keep this file safe: it contains the unique keys for this screen. +# Wi-Fi comes from the secrets.yaml of ESPHome Device Builder. substitutions: DEVICE_NAME: {quote(name)} DEVICE_FRIENDLY_NAME: {quote(friendly.strip())} diff --git a/screen_manager/app/firmware.py b/screen_manager/app/firmware.py index 7b094b0..0ac7a15 100644 --- a/screen_manager/app/firmware.py +++ b/screen_manager/app/firmware.py @@ -82,10 +82,10 @@ class Firmware: def profile(self, name): if not isinstance(name,str) or not re.fullmatch(r'[a-zA-Z0-9_-]+\.yaml', name): - raise ValueError('Kies een bestaand YAML-profiel.') + raise ValueError('Choose an existing YAML profile.') p = self.root / name if p.is_symlink() or p.resolve().parent != self.root or not p.is_file(): - raise ValueError('Profiel bestaat niet in de ESPHome-map.') + raise ValueError("Profile doesn't exist in the ESPHome folder.") return p def wifi_status(self): @@ -116,12 +116,12 @@ class Firmware: if wifi['state'] == 'ready': return if wifi['state'] == 'invalid': - raise ValueError('secrets.yaml in de ESPHome-map is geen geldige YAML. Herstel het bestand eerst; het wordt niet overschreven.') + raise ValueError("secrets.yaml in the ESPHome folder isn't valid YAML. Fix the file first; it won't be overwritten.") values = {} for key in wifi['missing']: value = data.get(key) if not isinstance(value, str) or (key == 'wifi_ssid' and not value.strip()): - raise ValueError('Vul de wifi-naam en het wachtwoord in; ze worden in ESPHome secrets.yaml bewaard.') + raise ValueError("Fill in the Wi-Fi name and password; they'll be saved in ESPHome secrets.yaml.") values[key] = value path = self.root / 'secrets.yaml' if wifi['state'] == 'new': @@ -140,7 +140,7 @@ class Firmware: text = text + ('' if not text or text.endswith('\n') else '\n') + line + '\n' check = yaml.safe_load(text) if not isinstance(check, dict) or any(check.get(key) != value for key, value in values.items()): - raise ValueError('Wifi kon niet in secrets.yaml worden gezet. Vul wifi_ssid en wifi_password daar zelf in.') + raise ValueError("Wi-Fi couldn't be set in secrets.yaml. Fill in wifi_ssid and wifi_password there yourself.") path.write_text(text) def create(self, data): @@ -149,13 +149,13 @@ class Firmware: self.root.mkdir(parents=True,exist_ok=True) profile = self.root / (data['name']+'.yaml') if profile.exists() or profile.is_symlink(): - raise ValueError('Deze naam bestaat al. Gebruik het bestaande profiel voor updates.') + raise ValueError('This name already exists. Use the existing profile for updates.') self.store_wifi(data) try: with profile.open('x') as f: os.chmod(profile,0o600);f.write(content) except FileExistsError: - raise ValueError('Deze naam bestaat al. Gebruik het bestaande profiel voor updates.') + raise ValueError('This name already exists. Use the existing profile for updates.') key = yaml.load(content, Loader=LenientLoader)['api']['encryption']['key'] # The key is what Home Assistant asks for when pairing; the page shows it once. return {'file': profile.name, 'node': data['name'], 'api_key': key} @@ -168,11 +168,11 @@ class Firmware: target = data.get('target') or '' if target: if not isinstance(target, str) or target not in self.ports(): - raise ValueError('Kies de aangesloten USB-poort uit de lijst.') + raise ValueError('Choose the connected USB port from the list.') if self.task and not self.task.done(): - raise ValueError('Er loopt al een build of installatie. Wacht tot die klaar is.') + raise ValueError('A build or installation is already running. Wait for it to finish.') if not shutil.which('esphome'): - raise ValueError('ESPHome CLI ontbreekt in deze installatie.') + raise ValueError('The ESPHome CLI is missing from this installation.') result = self.create(data) if target: result['job'] = self.start({'file': result['file'], 'action': 'install', 'target': target}) @@ -185,17 +185,17 @@ class Firmware: return re.sub(r'(?i)((?:password|encryption.key|token|ssid)\s*[:=]\s*).+',r'\1[redacted]',text)[:1500] def start(self, data): - if self.task and not self.task.done(): raise ValueError('Er loopt al een build of installatie.') + if self.task and not self.task.done(): raise ValueError('A build or installation is already running.') profile = self.profile(data.get('file')) action = data.get('action') - if action not in ('validate','build','install'): raise ValueError('Onbekende firmwareactie.') - if not shutil.which('esphome'): raise ValueError('ESPHome CLI ontbreekt in deze installatie.') + if action not in ('validate','build','install'): raise ValueError('Unknown firmware action.') + if not shutil.which('esphome'): raise ValueError('The ESPHome CLI is missing from this installation.') target = data.get('target','') if action == 'install': if target.startswith('/dev/'): - if target not in self.ports(): raise ValueError('Kies de aangesloten USB-poort uit de lijst.') + if target not in self.ports(): raise ValueError('Choose the connected USB port from the list.') elif not re.fullmatch(r'[a-zA-Z0-9][a-zA-Z0-9.-]{0,252}',target): - raise ValueError('Vul het IP-adres of de hostnaam van het bedoelde scherm in.') + raise ValueError('Enter the IP address or hostname of the intended screen.') self._secret_values=set() # Collect scalar literals, including !secret values, without executing YAML tags. def collect(node): @@ -232,8 +232,8 @@ class Firmware: async for line in self.process.stdout: self.logs.append(self.redact(line.decode(errors='replace').rstrip())) code=await self.process.wait() - if code: raise RuntimeError('ESPHome '+stage+' mislukt; bekijk het log.') - self.job['state']='success'; self.logs.append('Geslaagd: '+action) + if code: raise RuntimeError('ESPHome '+stage+' failed; see the log.') + self.job['state']='success'; self.logs.append('Succeeded: '+action) if action=='install': self.installed.add(profile.name) except asyncio.CancelledError: self.job['state']='interrupted' diff --git a/screen_manager/app/header_bar.py b/screen_manager/app/header_bar.py index b67d9a8..fc7a2ee 100644 --- a/screen_manager/app/header_bar.py +++ b/screen_manager/app/header_bar.py @@ -1,9 +1,9 @@ -"""Top bar of a screen ("bovenbalk"): what the firmware draws right of the screen name. +"""Top bar of a screen: what the firmware draws right of the screen name. Firmware 0.2.32+ only draws. This module decides everything that needs Home Assistant: an -entity's text in Dutch (number format, units, translated states), its icon, its accent colour +entity's text (number format, units, translated states), its icon, its accent colour and whether it shows at all. Clock, analog clock and date tick on the screen itself, and a -relative time ("5 min geleden") travels as a unix time, so the screen counts the minutes +relative time ("5 min ago") travels as a unix time, so the screen counts the minutes without any traffic. Wire format, one message after the layout: {"v":1,"op":"header","items":[...]} with per item @@ -21,33 +21,33 @@ from core import HEADER_BUILTIN, HEADER_CONTENTS, HEADER_MAX_ITEMS, HEADER_MIN_F # tests/test_header_bar.py keeps them equal). Anything else folds to its base letter or goes. GLYPHS = frozenset("<>—&@!,.?\"%()+-_:°0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyzäöüÄÖÜß/…·'#*=;²³µéèëêïîóôàáâçñúû–") TEXT_BYTES = 40 -MONTHS = ('jan', 'feb', 'mrt', 'apr', 'mei', 'jun', 'jul', 'aug', 'sep', 'okt', 'nov', 'dec') +MONTHS = ('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec') UNAVAILABLE = '—' -CONTENT_LABELS = {'state': 'Status', 'last_changed': 'Laatst gewijzigd'} -SHOW_LABELS = {'always': 'Altijd', 'active': 'Alleen als actief'} +CONTENT_LABELS = {'state': 'Status', 'last_changed': 'Last changed'} +SHOW_LABELS = {'always': 'Always', 'active': 'Only when active'} # Home Assistant's own words where they fit a small bar; the screen shows the rest as they come. STATES = { - 'on': 'Aan', 'off': 'Uit', 'home': 'Thuis', 'not_home': 'Weg', 'open': 'Open', 'closed': 'Dicht', - 'opening': 'Gaat open', 'closing': 'Gaat dicht', 'locked': 'Op slot', 'unlocked': 'Open', 'locking': 'Sluiten', - 'unlocking': 'Openen', 'jammed': 'Vast', 'playing': 'Speelt', 'paused': 'Pauze', 'idle': 'Inactief', - 'standby': 'Stand-by', 'buffering': 'Laden', 'cleaning': 'Bezig', 'docked': 'In dock', 'returning': 'Naar dock', - 'error': 'Fout', 'heat': 'Verwarmen', 'cool': 'Koelen', 'heat_cool': 'Auto', 'auto': 'Auto', 'dry': 'Drogen', - 'fan_only': 'Ventileren', 'disarmed': 'Uit', 'armed_home': 'Thuis', 'armed_away': 'Afwezig', 'armed_night': 'Nacht', - 'armed_vacation': 'Vakantie', 'armed_custom_bypass': 'Aan', 'arming': 'Inschakelen', 'pending': 'Wachten', - 'triggered': 'Alarm', 'active': 'Loopt', 'above_horizon': 'Op', 'below_horizon': 'Onder', + 'on': 'On', 'off': 'Off', 'home': 'Home', 'not_home': 'Away', 'open': 'Open', 'closed': 'Closed', + 'opening': 'Opening', 'closing': 'Closing', 'locked': 'Locked', 'unlocked': 'Unlocked', 'locking': 'Locking', + 'unlocking': 'Unlocking', 'jammed': 'Jammed', 'playing': 'Playing', 'paused': 'Paused', 'idle': 'Idle', + 'standby': 'Standby', 'buffering': 'Buffering', 'cleaning': 'Cleaning', 'docked': 'Docked', 'returning': 'Returning', + 'error': 'Error', 'heat': 'Heat', 'cool': 'Cool', 'heat_cool': 'Auto', 'auto': 'Auto', 'dry': 'Dry', + 'fan_only': 'Fan only', 'disarmed': 'Disarmed', 'armed_home': 'Armed home', 'armed_away': 'Armed away', 'armed_night': 'Armed night', + 'armed_vacation': 'Armed vacation', 'armed_custom_bypass': 'Armed', 'arming': 'Arming', 'pending': 'Pending', + 'triggered': 'Triggered', 'active': 'Active', 'above_horizon': 'Up', 'below_horizon': 'Down', } # device_class: (on, off) BINARY_STATES = { - 'battery': ('Laag', 'Normaal'), 'battery_charging': ('Laden', 'Niet laden'), 'carbon_monoxide': ('Gevaar', 'Veilig'), - 'cold': ('Koud', 'Normaal'), 'connectivity': ('Verbonden', 'Verbroken'), 'door': ('Open', 'Dicht'), - 'garage_door': ('Open', 'Dicht'), 'gas': ('Gevaar', 'Veilig'), 'heat': ('Heet', 'Normaal'), 'light': ('Licht', 'Donker'), - 'lock': ('Open', 'Op slot'), 'moisture': ('Nat', 'Droog'), 'motion': ('Beweging', 'Geen beweging'), - 'moving': ('Beweegt', 'Stil'), 'occupancy': ('Bezet', 'Vrij'), 'opening': ('Open', 'Dicht'), 'plug': ('Aangesloten', 'Los'), - 'power': ('Aan', 'Uit'), 'presence': ('Thuis', 'Weg'), 'problem': ('Probleem', 'OK'), 'running': ('Actief', 'Inactief'), - 'safety': ('Onveilig', 'Veilig'), 'smoke': ('Rook', 'Geen rook'), 'sound': ('Geluid', 'Stil'), 'tamper': ('Sabotage', 'OK'), - 'update': ('Update', 'Bijgewerkt'), 'vibration': ('Trilling', 'Stil'), 'window': ('Open', 'Dicht'), + 'battery': ('Low', 'Normal'), 'battery_charging': ('Charging', 'Not charging'), 'carbon_monoxide': ('Danger', 'Safe'), + 'cold': ('Cold', 'Normal'), 'connectivity': ('Connected', 'Disconnected'), 'door': ('Open', 'Closed'), + 'garage_door': ('Open', 'Closed'), 'gas': ('Danger', 'Safe'), 'heat': ('Hot', 'Normal'), 'light': ('Light', 'Dark'), + 'lock': ('Open', 'Locked'), 'moisture': ('Wet', 'Dry'), 'motion': ('Motion', 'No motion'), + 'moving': ('Moving', 'Still'), 'occupancy': ('Occupied', 'Clear'), 'opening': ('Open', 'Closed'), 'plug': ('Plugged in', 'Unplugged'), + 'power': ('On', 'Off'), 'presence': ('Home', 'Away'), 'problem': ('Problem', 'OK'), 'running': ('Active', 'Inactive'), + 'safety': ('Unsafe', 'Safe'), 'smoke': ('Smoke', 'No smoke'), 'sound': ('Sound', 'Silent'), 'tamper': ('Tampering', 'OK'), + 'update': ('Update', 'Up to date'), 'vibration': ('Vibration', 'Still'), 'window': ('Open', 'Closed'), } # Icon names from tile_icons; the fonts of both boards carry every one of them. BINARY_ICONS = { # device_class: (on, off) @@ -101,7 +101,7 @@ def clean_text(text): return short(''.join(out).strip(), TEXT_BYTES) def number_text(value, precision=None): - """A number as Home Assistant writes it in Dutch: decimal comma, dots between thousands.""" + """A number as Home Assistant writes it: decimal point, commas between thousands.""" if precision is None: # Without a display precision HA keeps integers whole and shows at most three decimals. digits = 0 if float(value).is_integer() else 3 @@ -112,13 +112,13 @@ def number_text(value, precision=None): text = f'{value:.{precision}f}' negative = text.startswith('-') whole, _, fraction = text.lstrip('-').partition('.') - grouped = f'{int(whole):,}'.replace(',', '.') - result = grouped + (',' + fraction if fraction else '') - # "-0" and "-0,0" are just zero. + grouped = f'{int(whole):,}' + result = grouped + ('.' + fraction if fraction else '') + # "-0" and "-0.0" are just zero. return '-' + result if negative and any(c not in '0.,' for c in result) else result def with_unit(text, unit): - """Home Assistant's spacing in Dutch: "21,3 °C", "65%", "18°".""" + """Home Assistant's spacing: "21.3 °C", "65%", "18°".""" if not unit: return text return text + unit if unit in ('%', '°') else f'{text} {unit}' @@ -152,7 +152,7 @@ def value(entity, state, entry=None, units=None, tz=None): return (None, epoch(raw)) if epoch(raw) else (UNAVAILABLE, None) if domain == 'script': moment = epoch(attrs.get('last_triggered')) - return ('Bezig', None) if raw == 'on' else (None, moment) if moment else ('Nog niet gestart', None) + return ('Running', None) if raw == 'on' else (None, moment) if moment else ('Never run', None) if domain == 'sun': # The next event: sunset while the sun is up, sunrise while it is down. clock = local_clock(attrs.get('next_setting' if raw == 'above_horizon' else 'next_rising'), tz) @@ -169,7 +169,7 @@ def value(entity, state, entry=None, units=None, tz=None): if domain in ('person', 'device_tracker'): return STATES.get(raw, raw), None if domain == 'binary_sensor': - pair = BINARY_STATES.get(attrs.get('device_class'), ('Aan', 'Uit')) + pair = BINARY_STATES.get(attrs.get('device_class'), ('On', 'Off')) return (pair[0] if raw == 'on' else pair[1] if raw == 'off' else raw), None if domain == 'input_datetime': # "2026-09-15 07:30:00", "2026-09-15" or just "07:30:00". @@ -197,7 +197,7 @@ def value(entity, state, entry=None, units=None, tz=None): return raw[:1].upper() + raw[1:], None def active(entity, state): - """"Alleen als actief": on, open, home, detected, playing, heating, or a number other than zero.""" + """"Only when active": on, open, home, detected, playing, heating, or a number other than zero.""" raw = state.get('state') if state else None domain = entity.split('.')[0] if raw in (None, '', 'unavailable', 'unknown'): @@ -324,13 +324,13 @@ def suggestions(screen, entities, states, registry=None, limit=8): def sensor(device_class): return lambda e: e['id'].startswith('sensor.') and attrs(e).get('device_class') == device_class and numeric(states[e['id']]['state']) is not None picks = [] - pick('Temperatuur', sensor('temperature')) - pick('Luchtvochtigheid', sensor('humidity')) - pick('Stroomverbruik', sensor('power')) - pick('Laatste beweging', lambda e: e['id'].startswith('binary_sensor.') and attrs(e).get('device_class') in ('motion', 'occupancy', 'presence'), 'last_changed') - pick('Weer', lambda e: e['id'].startswith('weather.')) - pick('Aantal thuis', lambda e: e['id'] == 'zone.home') - pick('Zon op en onder', lambda e: e['id'] == 'sun.sun') + pick('Temperature', sensor('temperature')) + pick('Humidity', sensor('humidity')) + pick('Power usage', sensor('power')) + pick('Last motion', lambda e: e['id'].startswith('binary_sensor.') and attrs(e).get('device_class') in ('motion', 'occupancy', 'presence'), 'last_changed') + pick('Weather', lambda e: e['id'].startswith('weather.')) + pick('People home', lambda e: e['id'] == 'zone.home') + pick('Sunrise and sunset', lambda e: e['id'] == 'sun.sun') pick('CO₂', sensor('carbon_dioxide')) return picks[:limit] diff --git a/screen_manager/app/server.py b/screen_manager/app/server.py index 3185cbf..e15dd76 100644 --- a/screen_manager/app/server.py +++ b/screen_manager/app/server.py @@ -14,7 +14,7 @@ import tile_icons from updates import Updater from aiohttp import ClientSession, ClientTimeout, WSMsgType, web -from core import BUILTIN, HEADER_MIN_FIRMWARE, TILE_BACKGROUNDS, TRANSPORT_MIN_FIRMWARE, alert_reference, alert_service, controls_catalogue, discover, discover_screens, encode, extras, header_items, message_action, min_firmware, pack_slots, packets, revision, screen_items, state_message, validate_header, validate_layout, validate_settings +from core import BUILTIN, HEADER_MIN_FIRMWARE, NAME_TILE_SETTINGS, TILE_BACKGROUNDS, TRANSPORT_MIN_FIRMWARE, alert_reference, alert_service, controls_catalogue, device_prefixes, discover, discover_screens, encode, extras, header_items, inbox_prefix, message_action, min_firmware, pack_slots, packets, revision, screen_items, state_message, validate_header, validate_layout, validate_settings import header_bar from zoneinfo import ZoneInfo @@ -31,7 +31,12 @@ FULL_REPEAT_SECONDS = 3600 HISTORY_SECONDS = 300 # Inbox states after which a screen needs the whole layout again: a restart, a ping that # did not match, or a tile state that never arrived. Guarded so a slow batch cannot loop. -RESEND_STATES = frozenset({'Klaar voor tegelconfiguratie', 'Indeling opnieuw nodig', 'Tegels laden'}) +# Firmware built before the English translation still reports the Dutch originals, so both +# forms are recognised until every board has been reflashed. +RESEND_STATES = frozenset({ + 'Ready for tile configuration', 'Resend needed', 'Loading tiles', + 'Klaar voor tegelconfiguratie', 'Indeling opnieuw nodig', 'Tegels laden', +}) RESEND_GUARD_SECONDS = 120 FORECAST_SECONDS = 1800 @@ -78,7 +83,7 @@ class HomeAssistant: async def request(self, kind, **data): if self.ws is None or self.ws.closed: - raise ConnectionError('Home Assistant is niet verbonden.') + raise ConnectionError("Home Assistant isn't connected.") self.next_id += 1 key = self.next_id future = asyncio.get_running_loop().create_future() @@ -100,7 +105,7 @@ class HomeAssistant: if data.get('success'): future.set_result(data.get('result')) else: - future.set_exception(ConnectionError('Home Assistant heeft de opdracht geweigerd.')) + future.set_exception(ConnectionError('Home Assistant refused the command.')) elif data.get('type') == 'event': event = data.get('event', {}) body = event.get('data', {}) @@ -118,7 +123,7 @@ class HomeAssistant: self.changed.set() elif event.get('event_type') in REGISTRY_EVENTS: self.registry_changed.set() - raise ConnectionError('Home Assistant-verbinding verbroken.') + raise ConnectionError('Home Assistant connection lost.') def describe_close(self, connected): """Why and after how long the websocket ended; helps explain unexpected reconnects in the log.""" @@ -126,7 +131,7 @@ class HomeAssistant: return '' ws = self.ws reason = ws.exception() if ws is not None else None - return ' na %d s, close-code %s%s' % (time.monotonic() - connected, ws.close_code if ws is not None else None, + return ' after %d s, close code %s%s' % (time.monotonic() - connected, ws.close_code if ws is not None else None, f', {type(reason).__name__}' if reason else '') async def registries(self): @@ -144,7 +149,7 @@ class HomeAssistant: await ws.receive_json(timeout=15) await ws.send_json({'type': 'auth', 'access_token': self.token}) if (await ws.receive_json(timeout=15)).get('type') != 'auth_ok': - raise ConnectionError('Home Assistant-authenticatie mislukt.') + raise ConnectionError('Home Assistant authentication failed.') reader = asyncio.create_task(self.read()) await self.request('subscribe_events', event_type='state_changed') await self.request('subscribe_events', event_type='esphome.screen_setting') @@ -161,7 +166,7 @@ class HomeAssistant: self.online = True self.changed.set() connected = time.monotonic() - LOG.info('Home Assistant verbonden') + LOG.info('Home Assistant connected') # Refresh the registry when HA reports a change (debounced), with a slow # fallback; a full fetch is about 1 MB of JSON and used to run every 30 s. fetched = time.monotonic() @@ -182,12 +187,12 @@ class HomeAssistant: fetched = time.monotonic() self.changed.set() except (ConnectionError, TimeoutError, OSError, ValueError) as error: - LOG.warning('Home Assistant tijdelijk niet beschikbaar (%s)%s', type(error).__name__, self.describe_close(connected)) + LOG.warning('Home Assistant temporarily unavailable (%s)%s', type(error).__name__, self.describe_close(connected)) except Exception as error: - LOG.warning('Verbinding opnieuw starten (%s)%s', type(error).__name__, self.describe_close(connected)) + LOG.warning('Restarting connection (%s)%s', type(error).__name__, self.describe_close(connected)) finally: if self.online: - LOG.info('Home Assistant-verbinding gesloten%s', self.describe_close(connected)) + LOG.info('Home Assistant connection closed%s', self.describe_close(connected)) self.online = False self.ws = None if reader: @@ -196,7 +201,7 @@ class HomeAssistant: await reader for future in self.pending.values(): if not future.done(): - future.set_exception(ConnectionError('Verbinding verbroken.')) + future.set_exception(ConnectionError('Connection lost.')) await asyncio.sleep(5) async def send(self, inbox, message, action=None): @@ -249,7 +254,7 @@ class HomeAssistant: raw = bytearray() async for chunk in response.content.iter_chunked(65536): raw.extend(chunk) - if len(raw)>2*1024*1024: raise ValueError('Geschiedenis te groot.') + if len(raw)>2*1024*1024: raise ValueError('History too large.') rows=json.loads(raw) events=[] begin=datetime.fromisoformat(start).timestamp(); span=hours*3600 @@ -278,11 +283,15 @@ class Manager: self._screens_key, self._screens = None, [] self._watched_key, self._watched = None, set() self._seen_registry = None + # Inbox entity ids seen this run with their Home Assistant device, and old inbox ids that a screen + # now reports under a new id (see follow_renamed_inboxes); the updater follows a screen through them. + self._inbox_devices, self.aliases = {}, {} + self._prefix_source, self._prefixes = None, {} if self.path.exists(): raw = json.loads(self.path.read_text()) # Versioned persistent data. Never silently overwrite an unknown schema. if raw.get('version') != 1 or not isinstance(raw.get('screens'), dict): - raise ValueError('Onbekende opslagversie; gegevens blijven ongewijzigd.') + raise ValueError('Unknown storage version; data stays unchanged.') self.layouts = {key: validate_layout(value) for key, value in raw['screens'].items()} def inventory(self): @@ -306,10 +315,74 @@ class Manager: key = (id(ha.registry), id(ha.devices), id(ha.areas), tuple(ha.states.get(item['entity_id'], {}).get('state') for item in items)) if key != self._screens_key: self._screens_key, self._screens = key, discover_screens(items, ha.states, ha.devices, ha.areas) + self.follow_renamed_inboxes(items) return [dict(screen) for screen in self._screens] def screen(self, inbox): - return next((s for s in self.screens() if s['id'] == inbox), None) + screens = self.screens() + inbox = self.aliases.get(inbox, inbox) + return next((s for s in screens if s['id'] == inbox), None) + + def device_prefixes(self, device): + registry = getattr(self.ha, 'registry', []) + if self._prefix_source is not registry: + self._prefix_source, self._prefixes = registry, {} + if device not in self._prefixes: + self._prefixes[device] = device_prefixes(registry, device) + return self._prefixes[device] + + def follow_renamed_inboxes(self, items): + """Keep a screen's layout and update history when its inbox entity gets a new entity id. + + Firmware 0.2.34 renamed the inbox from "Tegelinstellingen" to "Tile settings". Home Assistant then + removes the old entity and registers the renamed one under a new id, which would orphan the layout + stored under the old id. The screen is recognised by its device: an inbox this run saw on the same + device, or (after a restart) a stored inbox id that carries one of the device's entity id prefixes.""" + current = {item['entity_id']: item.get('device_id') for item in items + if item.get('platform') == 'esphome' and item['entity_id'].startswith('text.') and item.get('device_id') + and item.get('original_name') in NAME_TILE_SETTINGS and not item.get('disabled_by')} + stored = (set(self.layouts) | set(self.updates.hosts) | set(self.updates.results)) - set(current) - set(self.aliases) + for new, device in current.items(): + olds = {old for old, seen in self._inbox_devices.items() if seen == device and old not in current} + candidates = [old for old in stored if inbox_prefix(old) is not None] + if candidates: + prefixes = self.device_prefixes(device) + olds |= {old for old in candidates if inbox_prefix(old) in prefixes} + for old in sorted(olds - set(self.aliases)): + self.rename_inbox(old, new) + self._inbox_devices.update(current) + + def write_layouts(self, layouts): + self.path.parent.mkdir(parents=True, exist_ok=True) + temp = self.path.with_suffix('.tmp') + with open(temp, 'w', encoding='utf8') as handle: + os.chmod(temp, 0o600) + json.dump({'version': 1, 'screens': layouts}, handle, ensure_ascii=False) + handle.flush() + os.fsync(handle.fileno()) + temp.replace(self.path) + + def rename_inbox(self, old, new): + """Move everything kept under an old inbox id to the id the same screen reports under now.""" + moved = [] + if old in self.layouts and new not in self.layouts: + layouts = {(new if key == old else key): value for key, value in self.layouts.items()} + self.write_layouts(layouts) + self.layouts = layouts + moved.append('layout') + elif old in self.layouts: + LOG.warning('Screen %s already has a layout; the one stored under %s stays untouched', new, old) + if self.updates.renamed(old, new): + moved.append('update history') + for store in (self.sent, self.status, self.last, self.pinged): + store.pop(old, None) + self.aliases = {**{key: (new if value == old else value) for key, value in self.aliases.items()}, old: new} + self._inbox_devices.pop(old, None) + LOG.info('Screen %s now reports as %s%s', old, new, f"; moved its {' and '.join(moved)}" if moved else '') + if moved: + self.history_wake.set() + self.ha.changed.set() + self.notify() def firmware_version(self, inbox, screen=None): if screen is None: @@ -356,13 +429,15 @@ class Manager: def save(self, inbox, data): screens, entities = self.inventory() + # A page opened before a screen's inbox got a new id still saves to the right screen. + inbox = self.aliases.get(inbox, inbox) screen=next((s for s in screens if s['id']==inbox), None) if screen is None: - raise ValueError('Dit is geen gekoppeld ESP-scherm. Vernieuw het overzicht.') + raise ValueError("This isn't a paired ESP screen. Refresh the overview.") layout = validate_layout(data) needed = self.needs_firmware(inbox, layout, screen) if needed: - raise ValueError(f"Installeer eerst schermfirmware {needed} of nieuwer voor deze tegels.") + raise ValueError(f"Install screen firmware {needed} or newer first for these tiles.") # A still-open older UI may save tiles without the new optional settings or top bar. if 'settings' not in layout and 'settings' in self.layouts.get(inbox, {}): layout['settings'] = self.layouts[inbox]['settings'].copy() @@ -376,7 +451,7 @@ class Manager: if 'settings' in layout and 'rotation' not in data.get('settings',{}): layout['settings']['rotation']=self.layouts.get(inbox,{}).get('settings',{}).get('rotation',0) if layout.get('settings',{}).get('rotation',0) and screen.get('board')!='guition': - raise ValueError('Rotatie vereist een Guition met firmware 0.2.9 of nieuwer.') + raise ValueError('Rotation requires a Guition with firmware 0.2.9 or newer.') old_tiles = {t['entity']:t for t in self.layouts.get(inbox,{}).get('tiles',[])} for tile in layout['tiles']: old_options=old_tiles.get(tile['entity'],{}).get('options',{}) @@ -393,21 +468,14 @@ class Manager: layout = validate_layout(layout) known = {e['id'] for e in entities} | set(BUILTIN) if any(t['entity'] not in known for t in layout['tiles']): - raise ValueError('Een gekozen entiteit bestaat niet meer. Zoek de nieuwe entiteit op.') + raise ValueError('A chosen entity no longer exists. Look up the new entity.') if any(item['type'] == 'entity' and item['entity'] not in known and item['entity'] not in self.ha.states for item in header_items(layout)): - raise ValueError('Een entiteit in de bovenbalk bestaat niet meer. Kies een andere.') + raise ValueError('An entity in the top bar no longer exists. Choose a different one.') updated = {**self.layouts, inbox: layout} - self.path.parent.mkdir(parents=True, exist_ok=True) - temp = self.path.with_suffix('.tmp') - with open(temp, 'w', encoding='utf8') as handle: - os.chmod(temp, 0o600) - json.dump({'version': 1, 'screens': updated}, handle, ensure_ascii=False) - handle.flush() - os.fsync(handle.fileno()) - temp.replace(self.path) + self.write_layouts(updated) self.layouts = updated self.sent.pop(inbox, None) - self.status[inbox] = 'Opgeslagen; wacht op synchronisatie' + self.status[inbox] = 'Saved; waiting for sync' self.history_wake.set() self.ha.changed.set() self.notify() @@ -497,7 +565,7 @@ class Manager: screen = self.screen(inbox) or {} needed = self.needs_firmware(inbox, layout, screen) if needed: - self.status[inbox]=f"Indeling bewaard; firmware {needed}+ nodig voor deze tegels" + self.status[inbox]=f"Layout saved; firmware {needed}+ needed for these tiles" return False tiles = layout['tiles'] layout_msg = self.layout_message(inbox, layout, screen) @@ -540,7 +608,7 @@ class Manager: self.last[inbox] = time.monotonic() if outgoing: self.pinged[inbox] = time.monotonic() - self.status[inbox] = 'Verzonden naar Home Assistant' + self.status[inbox] = 'Sent to Home Assistant' return bool(outgoing) async def ping(self, inbox, screen): @@ -599,7 +667,7 @@ class Manager: before = self.status.get(inbox) if not screen['online']: self.sent.pop(inbox, None) - self.status[inbox] = 'Scherm offline; wijzigingen bewaard' + self.status[inbox] = 'Screen offline; changes saved' changed = changed or self.status[inbox] != before continue if inbox not in self.layouts: @@ -617,8 +685,8 @@ class Manager: await self.ping(inbox, screen) except Exception as error: self.sent.pop(inbox, None) - self.status[inbox] = 'Verzenden mislukt; automatisch opnieuw proberen' - LOG.warning('Schermsynchronisatie opnieuw proberen (%s)', type(error).__name__) + self.status[inbox] = 'Sending failed; retrying automatically' + LOG.warning('Retrying screen sync (%s)', type(error).__name__) await asyncio.sleep(1) changed = changed or self.status.get(inbox) != before if changed: @@ -647,7 +715,7 @@ class Manager: try: found.update({(entity, hours): values for entity, values in (await self.ha.statistics(entities, hours)).items()}) except Exception as error: - LOG.warning('Statistieken ophalen mislukt (%s); geschiedenis per sensor', type(error).__name__) + LOG.warning('Fetching statistics failed (%s); falling back to per-sensor history', type(error).__name__) changed = set() for key in due: values = found.get(key) @@ -677,7 +745,7 @@ class Manager: try: await self.refresh_histories() except Exception as error: - LOG.warning('Geschiedenis vernieuwen mislukt (%s)', type(error).__name__) + LOG.warning('Refreshing history failed (%s)', type(error).__name__) def create_app(manager, development=False): csrf = secrets.token_urlsafe(32) @@ -685,15 +753,15 @@ def create_app(manager, development=False): async def guard(request, handler): allowed = {'127.0.0.1', '::1'} if development else {'172.30.32.2'} if request.remote not in allowed: - raise web.HTTPForbidden(text='Open deze pagina via Home Assistant.') + raise web.HTTPForbidden(text='Open this page through Home Assistant.') if request.method not in {'GET', 'HEAD'} and request.headers.get('X-Screen-CSRF') != csrf: - raise web.HTTPForbidden(text='Vernieuw deze pagina en probeer opnieuw.') + raise web.HTTPForbidden(text='Refresh this page and try again.') try: response = await handler(request) except ValueError as error: return web.json_response({'error': str(error)}, status=400) except (TypeError, KeyError): - return web.json_response({'error': 'Ongeldige invoer. Controleer naam, bord en gekozen tegels.'}, status=400) + return web.json_response({'error': 'Invalid input. Check the name, board, and chosen tiles.'}, status=400) if response.prepared: return response # streamed (SSE) responses set their headers before prepare() response.headers['Cache-Control'] = 'no-store' @@ -712,8 +780,8 @@ def create_app(manager, development=False): screens = manager.screens() profiles = manager.firmware.profile_names() for screen in screens: - screen['layout'] = manager.layouts.get(screen['id'], {'title': 'Thuis', 'tiles': []}) - screen['delivery'] = manager.status.get(screen['id'], 'Kies je eerste tegels') + screen['layout'] = manager.layouts.get(screen['id'], {'title': 'Home', 'tiles': []}) + screen['delivery'] = manager.status.get(screen['id'], 'Choose your first tiles') screen['update'] = manager.updates.state_for(screen, profiles) screen['alert_action'] = alert_service(screen.get('node')) screen['dismiss_action'] = alert_service(screen.get('node'), 'dismiss_alert') @@ -731,7 +799,7 @@ def create_app(manager, development=False): payload['controls'] = controls_catalogue() payload['icons'] = tile_icons.editor() payload['alerts'] = alert_reference() - payload['builtin'] = [{'id': key, 'name': name, 'device': 'Ingebouwd op het scherm', 'area': '', 'state': 'ok'} for key, name in BUILTIN.items()] + payload['builtin'] = [{'id': key, 'name': name, 'device': 'Built into the screen', 'area': '', 'state': 'ok'} for key, name in BUILTIN.items()] payload['header'] = {**header_bar.catalogue(), 'suggestions': { screen['id']: header_bar.suggestions(screen, entities, manager.ha.states, manager.registry_index()) for screen in payload['screens']}} return web.json_response(payload) @@ -778,9 +846,9 @@ def create_app(manager, development=False): manager.updates.set_auto((await request.json()).get('auto')) return web.json_response(manager.updates.summary()) async def inspector(request): - inbox=request.match_info['inbox'] - screen=manager.screen(inbox) - if screen is None: raise ValueError('Onbekend scherm.') + screen=manager.screen(request.match_info['inbox']) + inbox=manager.aliases.get(request.match_info['inbox'], request.match_info['inbox']) + if screen is None: raise ValueError('Unknown screen.') layout=manager.layouts.get(inbox,{'tiles':[]}) return web.json_response({'screen':screen, 'delivery':manager.status.get(inbox), 'layout':layout, @@ -819,7 +887,7 @@ async def main(): if development and os.environ.get('HA_TOKEN_FILE'): token = Path(os.environ['HA_TOKEN_FILE']).read_text().strip() if not token: - raise SystemExit('Geen Home Assistant-toegang. Start de app via Supervisor.') + raise SystemExit('No Home Assistant access. Start the app via Supervisor.') async with ClientSession(timeout=ClientTimeout(total=20)) as session: ha = HomeAssistant(session, os.environ.get('HA_API', 'http://supervisor/core/api'), token) manager = Manager(ha, Path(os.environ.get('SCREEN_DATA', '/data')) / 'screens.json') diff --git a/screen_manager/app/static/app.js b/screen_manager/app/static/app.js index 2890b5a..17022f1 100644 --- a/screen_manager/app/static/app.js +++ b/screen_manager/app/static/app.js @@ -36,7 +36,7 @@ async function api(path, options = {}) { }, }); if (!response.ok) { - let message = "Dit lukte niet. Vernieuw de pagina en probeer opnieuw."; + let message = "That didn't work. Refresh the page and try again."; try { message = (await response.json()).error || message; } catch {} @@ -46,15 +46,15 @@ async function api(path, options = {}) { } function markDirty() { dirty = true; - $("#dirty").textContent = "Niet-opgeslagen wijzigingen"; - $("#save-detail").textContent = "Klik op opslaan om je scherm bij te werken."; + $("#dirty").textContent = "Unsaved changes"; + $("#save-detail").textContent = "Click save to update your screen."; } function select(id) { if ( id !== selected && dirty && !confirm( - "Je hebt niet-opgeslagen wijzigingen. Toch een ander scherm openen?", + "You have unsaved changes. Open a different screen anyway?", ) ) return; @@ -71,8 +71,8 @@ function select(id) { $("#screen-name").textContent = screen.name; $("#editor").hidden = false; $("#empty").hidden = true; - $("#dirty").textContent = "Alles opgeslagen"; - $("#save-detail").textContent = "Aanpassen kan zonder opnieuw flashen."; + $("#dirty").textContent = "All saved"; + $("#save-detail").textContent = "Changes apply without reflashing."; renderScreens(); renderTopbar(); renderTiles(); @@ -81,19 +81,19 @@ function select(id) { loadTopbarPreview(0); } const settingDefinitions = [ - ["standby_enabled", "Automatisch standby", "check", true], - ["standby_seconds", "Standby na", "minutes", 600, 1, 1440], - ["brightness", "Helderheid normaal", "range", 100, 5, 100], - ["standby_brightness", "Helderheid in standby", "range", 20, 0, 100], - ["night_enabled", "Nachtstand gebruiken", "check", true], - ["night_brightness", "Helderheid in nachtstand", "range", 10, 0, 100], - ["night_start", "Nachtstand vanaf", "time", 1320], - ["night_end", "Nachtstand tot", "time", 420], + ["standby_enabled", "Auto standby", "check", true], + ["standby_seconds", "Standby after", "minutes", 600, 1, 1440], + ["brightness", "Normal brightness", "range", 100, 5, 100], + ["standby_brightness", "Standby brightness", "range", 20, 0, 100], + ["night_enabled", "Use night mode", "check", true], + ["night_brightness", "Night brightness", "range", 10, 0, 100], + ["night_start", "Night starts", "time", 1320], + ["night_end", "Night ends", "time", 420], // Whether the clock shows is up to the top bar now; the add-on keeps show_clock in step for older firmware. - ["clock_24h", "24-uursklok (uit = 12 uur)", "check", true], - ["home_on_standby", "Na standby terug naar pagina 1", "check", false], - ["rotation", "Scherm draaien (met de klok mee)", "rotation", 0], - ["swipe_pages", "Vegen tussen pagina’s (Guition: vanaf de zijrand, firmware 0.2.24+; CYD: snelle veeg, 0.2.7+)", "check", false], + ["clock_24h", "24-hour clock (off = 12 hour)", "check", true], + ["home_on_standby", "Return to page 1 after standby", "check", false], + ["rotation", "Rotate screen (clockwise)", "rotation", 0], + ["swipe_pages", "Swipe between pages (Guition: from the edge, firmware 0.2.24+; CYD: quick swipe, 0.2.7+)", "check", false], ]; function renderSettings() { const values = { @@ -137,7 +137,7 @@ function renderSettings() { kind === "range" ? `${input.value}%` : kind === "minutes" - ? "minuten" + ? "minutes" : ""); updateOutput(); input.oninput = () => { @@ -176,14 +176,14 @@ function renderSettingsSupport() { Number(parts[2]) > 1 || (Number(parts[2]) === 1 && Number(parts[3]) >= 2)); $("#settings-support").textContent = supported - ? "Opslaan stuurt je instellingen direct naar dit scherm. Ze blijven ook na een herstart bewaard." - : "Eenmalig firmware 0.1.2 of nieuwer installeren via ESPHome. Je kunt de instellingen alvast bewaren; oudere firmware gebruikt ze nog niet."; + ? "Saving sends your settings straight to this screen. They're also kept after a restart." + : "Install firmware 0.1.2 or newer once via ESPHome. You can save the settings now; older firmware doesn't use them yet."; } const updating = new Set(); const PHASES = { - install: "Bouwen en installeren…", - verify: "Wachten tot het scherm terug is…", - settle: "Controleren of het stabiel blijft…", + install: "Building and installing…", + verify: "Waiting for the screen to come back…", + settle: "Checking that it stays stable…", }; async function startUpdate(screen, host) { updating.add(screen.id); @@ -207,13 +207,13 @@ function renderUpdate(screen) { if (running) { row.append( node("span", undefined, "spin"), - node("small", PHASES[u.phase] || "Update starten…"), + node("small", PHASES[u.phase] || "Starting update…"), ); } else if (u.state === "queued") { - row.append(node("small", "In de wachtrij voor de update")); + row.append(node("small", "Queued for the update")); } else if (u.available && screen.online) { row.append(node("span", `Update ${u.target}`, "badge update")); - const button = node("button", "Bijwerken", "mini"); + const button = node("button", "Update", "mini"); button.type = "button"; if (u.host && u.profile) { button.onclick = (e) => { @@ -222,19 +222,19 @@ function renderUpdate(screen) { }; } else if (!u.profile) { button.disabled = true; - button.title = "Geen ESPHome-profiel met deze apparaatnaam gevonden."; + button.title = "No ESPHome profile found with this device name."; } else { button.onclick = (e) => { e.stopPropagation(); const form = node("form", undefined, "screen-host"); const input = node("input"); - input.placeholder = "IP-adres, bijv. 192.168.1.50"; + input.placeholder = "IP address, e.g. 192.168.1.50"; input.required = true; input.pattern = "[A-Za-z0-9][A-Za-z0-9.\\-]*"; const go = node("button", "Start", "mini"); const cancel = node("button", "✕", "quiet"); cancel.type = "button"; - cancel.setAttribute("aria-label", "Annuleren"); + cancel.setAttribute("aria-label", "Cancel"); cancel.onclick = () => { form.remove(); renderScreens(); @@ -246,7 +246,7 @@ function renderUpdate(screen) { startUpdate(screen, input.value.trim()); }; row.replaceChildren( - node("small", "Eenmalig het IP-adres; nieuwe firmware meldt het zelf."), + node("small", "The IP address, once; new firmware reports it itself."), form, ); input.focus(); @@ -266,7 +266,7 @@ function renderUpdate(screen) { } function openIntegrations() { // Pairing happens in Home Assistant itself. This page lives in HA's ingress iframe, - // so send the top window to Apparaten & diensten (same origin); elsewhere open a tab. + // so send the top window to Devices & services (same origin); elsewhere open a tab. const path = "/config/integrations/dashboard"; try { window.top.location.assign(path); @@ -274,11 +274,11 @@ function openIntegrations() { window.open(path, "_blank"); } } -async function copyText(text, element, what = "API-sleutel") { +async function copyText(text, element, what = "API key") { try { if (!navigator.clipboard || !window.isSecureContext) throw new Error(); await navigator.clipboard.writeText(text); - toast(`${what} gekopieerd.`); + toast(`${what} copied.`); } catch { if (element) { const range = document.createRange(); @@ -287,11 +287,11 @@ async function copyText(text, element, what = "API-sleutel") { selection.removeAllRanges(); selection.addRange(range); } - toast(document.execCommand("copy") ? `${what} gekopieerd.` : `${what} is geselecteerd. Kopieer met Ctrl+C of Command+C.`); + toast(document.execCommand("copy") ? `${what} copied.` : `${what} is selected. Copy with Ctrl+C or Command+C.`); } } // Profiles that Home Assistant does not list yet: the freshly flashed screen is not lost, it -// still has to be added under Apparaten & diensten, outside this page. +// still has to be added under Devices & services, outside this page. function renderPending() { const box = $("#pending"); box.replaceChildren(); @@ -300,16 +300,16 @@ function renderPending() { card.append( node("strong", p.friendly), node("small", p.installed - ? "Geïnstalleerd, maar nog niet in Home Assistant. Dat doe je buiten ESP Screens: voeg het ontdekte ESPHome-apparaat toe onder Instellingen → Apparaten & diensten, plak daar de API-sleutel en zet bij Configureren “Allow the device to perform Home Assistant actions” aan." - : `Nog niet in Home Assistant. Al geflasht? Voeg het ESPHome-apparaat toe onder Instellingen → Apparaten & diensten en sta daarna bij Configureren de Home Assistant-acties toe. Nog niet geflasht? Firmware & USB → ${p.file}.`), + ? "Installed, but not yet in Home Assistant. That happens outside ESP Screens: add the discovered ESPHome device under Settings → Devices & services, paste the API key there, and turn on “Allow the device to perform Home Assistant actions” under Configure." + : `Not yet in Home Assistant. Already flashed? Add the ESPHome device under Settings → Devices & services, then allow the Home Assistant actions under Configure. Not flashed yet? Firmware & USB → ${p.file}.`), ); const actions = node("div", undefined, "pending-actions"); - const go = node("button", "Open Apparaten & diensten", "mini"); + const go = node("button", "Open Devices & services", "mini"); go.type = "button"; go.onclick = openIntegrations; actions.append(go); if (p.api_key) { - const copy = node("button", "Kopieer API-sleutel", "mini quiet"); + const copy = node("button", "Copy API key", "mini quiet"); copy.type = "button"; copy.onclick = () => copyText(p.api_key); actions.append(copy); @@ -338,7 +338,7 @@ function renderScreens() { const meta = node("small"); meta.append( node("span", screen.online ? "●" : "○", `dot ${screen.online ? "online" : ""}`), - ` ${screen.online ? "Online" : "Offline"}${screen.area ? " · " + screen.area : ""} · firmware ${screen.firmware || "onbekend"}`, + ` ${screen.online ? "Online" : "Offline"}${screen.area ? " · " + screen.area : ""} · firmware ${screen.firmware || "unknown"}`, ); b.append(node("strong", screen.name), meta); b.onclick = () => select(screen.id); @@ -351,7 +351,7 @@ function renderScreens() { if (screen) { $("#delivery").textContent = screen.online ? `${screen.delivery} · ${screen.status}` - : "Offline · wijzigingen worden bewaard"; + : "Offline · changes are saved"; $("#delivery").classList.toggle("online", screen.online); } renderUpdates(); @@ -362,12 +362,12 @@ function renderUpdates() { if (!u) return; const outdated = inventory.screens.filter((s) => s.update?.available).length; $("#updates-hint").textContent = u.busy - ? `Bezig met bijwerken naar firmware ${u.target}…` + ? `Updating to firmware ${u.target}…` : outdated - ? `Firmware ${u.target} is beschikbaar voor ${outdated} scherm${outdated === 1 ? "" : "en"}.` - : `Alle schermen hebben firmware ${u.target}.`; + ? `Firmware ${u.target} is available for ${outdated} screen${outdated === 1 ? "" : "s"}.` + : `All screens have firmware ${u.target}.`; $("#update-all").hidden = !u.pending || !!u.busy || u.pending < 2; - $("#update-all").textContent = `Alle ${u.pending} schermen bijwerken`; + $("#update-all").textContent = `Update all ${u.pending} screens`; if (document.activeElement !== $("#auto-update")) $("#auto-update").checked = u.auto; } @@ -387,8 +387,8 @@ $("#auto-update").onchange = async () => { }); toast( $("#auto-update").checked - ? "Schermen worden voortaan 's nachts bijgewerkt." - : "Automatisch bijwerken staat uit.", + ? "Screens will now update automatically at night." + : "Automatic updates are off.", ); } catch (e) { $("#auto-update").checked = !$("#auto-update").checked; @@ -396,30 +396,30 @@ $("#auto-update").onchange = async () => { } }; const domains = { - light: ["Licht", "☀", "#ad7600", "#fff3d3"], - climate: ["Klimaat", "❄", "#c86620", "#ffebdc"], - vacuum: ["Stofzuiger", "◉", "#008577", "#def3ed"], - fan: ["Ventilator", "✣", "#008aab", "#def5fa"], - cover: ["Zonwering", "▤", "#8053af", "#eee5f8"], + light: ["Light", "☀", "#ad7600", "#fff3d3"], + climate: ["Climate", "❄", "#c86620", "#ffebdc"], + vacuum: ["Vacuum", "◉", "#008577", "#def3ed"], + fan: ["Fan", "✣", "#008aab", "#def5fa"], + cover: ["Cover", "▤", "#8053af", "#eee5f8"], media_player: ["Media", "▶", "#007cad", "#def2fc"], sensor: ["Sensor", "⌁", "#3476b1", "#e5effa"], binary_sensor: ["Status", "◈", "#ad7600", "#fff3d3"], - switch: ["Schakelaar", "⏻", "#ad7600", "#fff3d3"], - input_boolean: ["Schakelaar", "⏻", "#ad7600", "#fff3d3"], - scene: ["Scène", "✦", "#8053af", "#eee5f8"], + switch: ["Switch", "⏻", "#ad7600", "#fff3d3"], + input_boolean: ["Switch", "⏻", "#ad7600", "#fff3d3"], + scene: ["Scene", "✦", "#8053af", "#eee5f8"], script: ["Script", "▷", "#8053af", "#eee5f8"], - weather: ["Weer", "☁", "#007cad", "#def2fc"], - number: ["Waarde", "±", "#008577", "#def3ed"], - input_number: ["Waarde", "±", "#008577", "#def3ed"], - select: ["Keuze", "≡", "#5862af", "#eaecfa"], - input_select: ["Keuze", "≡", "#5862af", "#eaecfa"], - button: ["Actie", "↗", "#5862af", "#eaecfa"], - screen: ["Klok", "◷", "#25282c", "#e9ecf1"], - sun: ["Zon", "☼", "#c86620", "#ffebdc"], - timer: ["Kookwekker", "⏱", "#008577", "#def3ed"], - person: ["Persoon", "☺", "#2f7d32", "#e1f2e2"], + weather: ["Weather", "☁", "#007cad", "#def2fc"], + number: ["Value", "±", "#008577", "#def3ed"], + input_number: ["Value", "±", "#008577", "#def3ed"], + select: ["Select", "≡", "#5862af", "#eaecfa"], + input_select: ["Select", "≡", "#5862af", "#eaecfa"], + button: ["Action", "↗", "#5862af", "#eaecfa"], + screen: ["Clock", "◷", "#25282c", "#e9ecf1"], + sun: ["Sun", "☼", "#c86620", "#ffebdc"], + timer: ["Timer", "⏱", "#008577", "#def3ed"], + person: ["Person", "☺", "#2f7d32", "#e1f2e2"], }; -const displayNames = { standard: "standaard", watch: "grote waarde", forecast: "weersvoorspelling", graph: "grafiek", digital: "digitale klok", analog: "analoge klok", sunpath: "zonnebaan" }; +const displayNames = { standard: "standard", watch: "large value", forecast: "weather forecast", graph: "graph", digital: "digital clock", analog: "analog clock", sunpath: "sun path" }; // Same rule as the add-on: only a wide card in the standard layout shows direct controls; // without a choice the domain's first control set applies. function effectiveControls(tile) { @@ -430,7 +430,7 @@ function effectiveControls(tile) { } function controlsLabel(tile) { const key = effectiveControls(tile); - if (!key) return "geen"; + if (!key) return "none"; return inventory.controls?.[tile.entity.split(".")[0]]?.choices.find((c) => c.key === key)?.label.toLocaleLowerCase() || key; } // Miniature of the control set on the mockup card: the same shapes the screen draws. @@ -450,7 +450,7 @@ function controlsPreview(tile) { else if (key === "buttons" && domain === "cover") buttons("arrow-expand-horizontal", "stop", "arrow-collapse-horizontal"); else if (key === "buttons" && domain === "vacuum") buttons("play", "stop", "home-map-marker"); else if (key === "buttons" && domain === "timer") buttons("play", "close"); - else if (key === "run") box.append(node("b", { scene: "Activeren", script: "Uitvoeren" }[domain] || "Indrukken", "preview-run")); + else if (key === "run") box.append(node("b", { scene: "Activate", script: "Run" }[domain] || "Press", "preview-run")); else slider(); return box; } @@ -576,7 +576,7 @@ function supportsFirmware(major, minor, patch) { return a > major || (a === major && (b > minor || (b === minor && c >= patch))); } function domainBadge(id) { - const [title, symbol, color, background] = domains[id.split(".")[0]] || ["Entiteit", "◇", "#637184", "#edf0f4"]; + const [title, symbol, color, background] = domains[id.split(".")[0]] || ["Entity", "◇", "#637184", "#edf0f4"]; const badge = node("span", symbol, "domain-icon"); badge.title = title; badge.setAttribute("aria-label", title); @@ -631,15 +631,15 @@ function renderPreview(preview) { for (let page = 0; page < shown; page++) { const frame = node("section", undefined, "screen-preview"); const heading = node("div", undefined, "preview-heading"); - heading.append(node("small", `Pagina ${page + 1}`)); + heading.append(node("small", `Page ${page + 1}`)); if (page >= pages) { frame.classList.add("new-page"); - heading.replaceChildren(node("small", `Pagina ${page + 1} · sleep hierheen voor een nieuwe pagina`)); + heading.replaceChildren(node("small", `Page ${page + 1} · drag here for a new page`)); } else if (pages > 1 && !entries.some((e) => pageOf(e.slot) === page)) { - heading.append(node("small", "leeg", "page-note")); - const drop = node("button", "Pagina weghalen", "mini"); + heading.append(node("small", "empty", "page-note")); + const drop = node("button", "Remove page", "mini"); drop.type = "button"; - drop.title = "De pagina's erna schuiven een plek op"; + drop.title = "The pages after this one shift up one slot"; drop.onclick = () => removePage(page); heading.append(drop); } @@ -665,7 +665,7 @@ function tileCard(tile, slot, placeholder) { const name = tile.name || entityName(tile.entity); const background = inventory.backgrounds?.[tile.options?.background]?.color; if (background) card.style.backgroundColor = background; - // "Geen": no card on the screen; the mockup keeps a dashed outline. + // "None": no card on the screen; the mockup keeps a dashed outline. if (tile.options?.background === "none") card.classList.add("bare"); if (isWide(tile)) card.classList.add("wide"); card.append(tileBadge(tile), node("strong", name)); @@ -677,7 +677,7 @@ function tileCard(tile, slot, placeholder) { if (placeholder || index < 0) { card.classList.add("placeholder"); return card; } card.tabIndex = 0; card.setAttribute("role", "button"); - card.setAttribute("aria-label", `${name}, plek ${(slot % SLOTS_PER_PAGE) + 1} op pagina ${pageOf(slot) + 1}. Enter: instellen, pijltjestoetsen: verplaatsen`); + card.setAttribute("aria-label", `${name}, slot ${(slot % SLOTS_PER_PAGE) + 1} on page ${pageOf(slot) + 1}. Enter: configure, arrow keys: move`); card.classList.toggle("chosen", selectedTile === tile.entity); enableDrag(card, { kind: "tile", tile }); card.onclick = () => openTileSheet(index); @@ -691,8 +691,8 @@ function tileCard(tile, slot, placeholder) { }; const remove = node("button", "✕", "preview-remove"); remove.type = "button"; - remove.title = "Tegel verwijderen"; - remove.setAttribute("aria-label", `${name} verwijderen`); + remove.title = "Remove tile"; + remove.setAttribute("aria-label", `Remove ${name}`); remove.onclick = (e) => { e.stopPropagation(); removeTile(index); }; card.append(remove); return card; @@ -704,9 +704,9 @@ function emptyCell(slot) { cell.dataset.slot = slot; const marked = insertAt === slot; cell.classList.toggle("insert-here", marked); - cell.append(node("span", "+"), node("small", marked ? "Volgende tegel komt hier" : "Leeg")); - cell.title = "Lege plek. Klik om hier een tegel toe te voegen, of sleep er een naartoe."; - cell.setAttribute("aria-label", `Lege plek ${(slot % SLOTS_PER_PAGE) + 1} op pagina ${pageOf(slot) + 1}: hier de volgende tegel toevoegen`); + cell.append(node("span", "+"), node("small", marked ? "Next tile goes here" : "Empty")); + cell.title = "Empty slot. Click to add a tile here, or drag one over."; + cell.setAttribute("aria-label", `Empty slot ${(slot % SLOTS_PER_PAGE) + 1} on page ${pageOf(slot) + 1}: add the next tile here`); cell.onclick = () => { insertAt = marked ? -1 : slot; renderPreview(); @@ -720,12 +720,12 @@ function renderTiles() { layout.pages = pageCount(liveEntries()); renderPreview(); $("#add-page").disabled = layout.pages >= MAX_PAGES; - $("#count").textContent = `${layout.tiles.length} / ${tileLimit()}${tileLimit()===10?" · update firmware voor 20":""}`; + $("#count").textContent = `${layout.tiles.length} / ${tileLimit()}${tileLimit()===10?" · update firmware for 20":""}`; $("#no-tiles").hidden = layout.tiles.length > 0; // Firmware below 0.2.26 ignores positions and packs the tiles in order, without gaps. const hint = $("#positions-hint"); hint.hidden = !hasGaps(layout.tiles) || supportsFirmware(0, 2, 26); - hint.textContent = `Lege plekken en vaste posities werken vanaf firmware 0.2.26. Dit scherm (firmware ${inventory.screens.find((s) => s.id === selected)?.firmware || "onbekend"}) schuift de tegels tot die update aan tot de eerste vrije plek.`; + hint.textContent = `Empty slots and fixed positions work from firmware 0.2.26. This screen (firmware ${inventory.screens.find((s) => s.id === selected)?.firmware || "unknown"}) shifts the tiles up to the first free slot until that update.`; if (sheetIndex >= 0) renderTileSheet(); } function removeTile(index) { @@ -735,8 +735,8 @@ function removeTile(index) { markDirty(); renderTiles(); renderResults(); - toast(`${tile.name || entityName(tile.entity)} verwijderd`, { - label: "Ongedaan maken", + toast(`${tile.name || entityName(tile.entity)} removed`, { + label: "Undo", run: () => placeTile(tile, tile.slot), }); } @@ -783,25 +783,25 @@ function iconField(tile, onChange) { return iconPicker({ selected: tile.options?.icon || "auto", automatic: automaticIcon(tile.entity), - autoLabel: `Automatisch (${fromHA ? "uit Home Assistant" : "standaard"})`, + autoLabel: `Automatic (${fromHA ? "from Home Assistant" : "default"})`, onPick: (name) => { tile.options = { ...tile.options, icon: name }; markDirty(); renderPreview(); onChange(); }, - note: supportsFirmware(0, 2, 18) ? "" : "Het scherm toont een gekozen icoon vanaf firmware 0.2.18.", + note: supportsFirmware(0, 2, 18) ? "" : "The screen shows a chosen icon from firmware 0.2.18.", }); } // The icon choice for tiles and top bar items: automatic, optionally none, or one from the set. function iconPicker({ selected, automatic, autoLabel, allowNone = false, onPick, note = "" }) { const wrap = node("div", undefined, "sheet-field"); - wrap.append(node("span", "Icoon")); + wrap.append(node("span", "Icon")); let picked = selected; const summary = node("button", undefined, "icon-current"); summary.type = "button"; summary.setAttribute("aria-expanded", String(iconPickerOpen)); - const current = node("span", undefined, "mdi"), text = node("span"), action = node("small", iconPickerOpen ? "Sluiten" : "Wijzigen"); + const current = node("span", undefined, "mdi"), text = node("span"), action = node("small", iconPickerOpen ? "Close" : "Change"); summary.append(current, text, action); const describe = () => { const chosen = iconNamed(picked); current.textContent = picked === "none" ? "" : glyph(chosen?.cp || automatic); - text.textContent = picked === "none" ? "Geen icoon" : chosen?.label || autoLabel; + text.textContent = picked === "none" ? "No icon" : chosen?.label || autoLabel; }; describe(); const panel = node("div", undefined, "icon-picker"); @@ -810,7 +810,7 @@ function iconPicker({ selected, automatic, autoLabel, allowNone = false, onPick, iconPickerOpen = panel.hidden; panel.hidden = !iconPickerOpen; summary.setAttribute("aria-expanded", String(iconPickerOpen)); - action.textContent = iconPickerOpen ? "Sluiten" : "Wijzigen"; + action.textContent = iconPickerOpen ? "Close" : "Change"; }; const choice = (name, cp, label, cls = "") => { const b = node("button", undefined, `icon-choice ${cls}`); @@ -818,7 +818,7 @@ function iconPicker({ selected, automatic, autoLabel, allowNone = false, onPick, b.dataset.icon = name; b.dataset.search = label.toLocaleLowerCase(); b.title = label; - b.setAttribute("aria-label", `Icoon: ${label}`); + b.setAttribute("aria-label", `Icon: ${label}`); b.setAttribute("aria-pressed", String(picked === name)); b.append(node("span", cp ? glyph(cp) : "", "mdi")); b.onclick = () => { @@ -831,13 +831,13 @@ function iconPicker({ selected, automatic, autoLabel, allowNone = false, onPick, }; const search = node("input"); search.type = "search"; - search.placeholder = "Zoek, bijvoorbeeld lamp, muziek of deur"; - search.setAttribute("aria-label", "Zoek een icoon"); + search.placeholder = "Search, for example lamp, music, or door"; + search.setAttribute("aria-label", "Search for an icon"); const auto = choice("auto", automatic, autoLabel, "icon-auto"); auto.append(node("span", autoLabel)); - const none = allowNone ? choice("none", "", "Geen icoon", "icon-auto icon-none") : null; - none?.append(node("span", "Geen icoon, alleen tekst")); - const list = node("div", undefined, "icon-list"), empty = node("p", "Geen icoon gevonden.", "hint"); + const none = allowNone ? choice("none", "", "No icon", "icon-auto icon-none") : null; + none?.append(node("span", "No icon, text only")); + const list = node("div", undefined, "icon-list"), empty = node("p", "No icon found.", "hint"); empty.hidden = true; const sections = inventory.icons.groups.map((group) => { const section = node("section"), grid = node("div", undefined, "icon-grid"); @@ -878,7 +878,7 @@ function renderTileSheet() { titles.append(node("strong", name), node("small", tile.entity)); const close = node("button", "✕", "quiet sheet-close"); close.type = "button"; - close.setAttribute("aria-label", "Sluiten"); + close.setAttribute("aria-label", "Close"); close.onclick = closeTileSheet; let badge = tileBadge(tile); head.append(badge, titles, close); @@ -888,16 +888,16 @@ function renderTileSheet() { nameInput.placeholder = name; nameInput.maxLength = 60; nameInput.oninput = () => { tile.name = nameInput.value; markDirty(); renderPreview(); }; - body.append(field("Naam op het scherm", nameInput)); + body.append(field("Name on the screen", nameInput)); // The clock, forecast and sun path cards draw no tile icon. if (inventory.icons && domain !== "screen" && !["forecast", "sunpath"].includes(tile.options?.display)) body.append(iconField(tile, () => { const next = tileBadge(tile); badge.replaceWith(next); badge = next; })); const displays = domain === "screen" - ? [["digital", "Digitale klok"], ["analog", "Analoge klok"]] - : [["standard", "Naam en status"], ["watch", "Grote waarde"]]; - if (domain === "weather") displays.push(["forecast", "Weersvoorspelling"]); - if (domain === "sensor") displays.push(["graph", "Grafiek"]); - if (domain === "sun") displays.push(["sunpath", "Zonnebaan"]); + ? [["digital", "Digital clock"], ["analog", "Analog clock"]] + : [["standard", "Name and status"], ["watch", "Large value"]]; + if (domain === "weather") displays.push(["forecast", "Weather forecast"]); + if (domain === "sensor") displays.push(["graph", "Graph"]); + if (domain === "sun") displays.push(["sunpath", "Sun path"]); const current = (key, fallback) => tile.options?.[key] ?? fallback; const set = (key, value) => { const wasWide = isWide(tile); @@ -917,32 +917,32 @@ function renderTileSheet() { } renderTiles(); }; - body.append(field("Weergave", segmented(displays, current("display", domain === "screen" ? "digital" : "standard"), (v) => set("display", v)))); - body.append(field("Breedte", segmented([["single", "Normaal"], ["wide", "Dubbelbreed"]], current("size", "single"), (v) => set("size", v)))); + body.append(field("Display", segmented(displays, current("display", domain === "screen" ? "digital" : "standard"), (v) => set("display", v)))); + body.append(field("Width", segmented([["single", "Normal"], ["wide", "Double-width"]], current("size", "single"), (v) => set("size", v)))); const catalogue = inventory.controls?.[domain]; if (catalogue && current("size", "single") === "wide") { - const wrap = field("Directe bediening op de tegel", segmented(catalogue.choices.map((c) => [c.key, c.label]), current("controls", catalogue.default), (v) => set("controls", v))); + const wrap = field("Direct control on the tile", segmented(catalogue.choices.map((c) => [c.key, c.label]), current("controls", catalogue.default), (v) => set("controls", v))); wrap.append(node("small", supportsFirmware(0, 2, 19) - ? "Rechts op de dubbelbrede tegel, zoals de rijen in Home Assistant. Tikken op de naam werkt zoals hieronder ingesteld." - : "Het scherm toont directe bediening vanaf firmware 0.2.19; tot die tijd blijft de tegel zoals hij was.", "field-hint")); + ? "On the right of the double-width tile, like the rows in Home Assistant. Tapping the name works as configured below." + : "The screen shows direct control from firmware 0.2.19; until then the tile stays as it was.", "field-hint")); body.append(wrap); } if (domain !== "screen") { - const taps = [["auto", "Automatisch"], ["detail", "Bediening openen"], ["none", "Alleen bekijken"]]; - if (["light", "switch", "input_boolean", "fan", "media_player", "climate"].includes(domain)) taps.push(["toggle", "Aan / uit"]); - body.append(field("Bij aantikken", segmented(taps, current("tap", "auto"), (v) => set("tap", v)))); + const taps = [["auto", "Automatic"], ["detail", "Open control"], ["none", "View only"]]; + if (["light", "switch", "input_boolean", "fan", "media_player", "climate"].includes(domain)) taps.push(["toggle", "On / off"]); + body.append(field("On tap", segmented(taps, current("tap", "auto"), (v) => set("tap", v)))); } if (["light", "fan", "cover", "number", "input_number", "media_player"].includes(domain)) - body.append(field("Kleine slider op de tegel", segmented([["none", "Nee"], ["slider", "Ja, direct bedienen"]], current("inline", "none"), (v) => set("inline", v)))); + body.append(field("Small slider on the tile", segmented([["none", "No"], ["slider", "Yes, control directly"]], current("inline", "none"), (v) => set("inline", v)))); if (domain === "sensor") - body.append(field("Geschiedenis", segmented([[1, "1 uur"], [6, "6 uur"], [24, "24 uur"]], current("history_hours", 24), (v) => set("history_hours", Number(v))))); + body.append(field("History", segmented([[1, "1 hour"], [6, "6 hours"], [24, "24 hours"]], current("history_hours", 24), (v) => set("history_hours", Number(v))))); const palette = node("div", undefined, "sheet-field"); - palette.append(node("span", "Pastel achtergrond")); + palette.append(node("span", "Pastel background")); const swatches = node("div", undefined, "palette-swatches"); for (const [key, choice] of Object.entries(inventory.backgrounds || {})) { const button = node("button", undefined, "palette-choice"); button.type = "button"; - button.setAttribute("aria-label", `Achtergrond: ${choice.label}`); + button.setAttribute("aria-label", `Background: ${choice.label}`); button.setAttribute("aria-pressed", String((tile.options?.background || "auto") === key)); const sample = node("span", undefined, "palette-sample"); if (choice.color) sample.style.backgroundColor = choice.color; @@ -959,17 +959,17 @@ function renderTileSheet() { palette.append(swatches); body.append(palette); const foot = node("div", undefined, "sheet-foot"); - const remove = node("button", "Verwijderen", "quiet danger"); + const remove = node("button", "Remove", "quiet danger"); remove.type = "button"; remove.onclick = () => removeTile(sheetIndex); foot.append(remove); if (domain !== "screen") { - const inspectButton = node("button", "Inspecteer", "quiet"); + const inspectButton = node("button", "Inspect", "quiet"); inspectButton.type = "button"; inspectButton.onclick = () => { const entity = tile.entity; closeTileSheet(); inspect(entity); }; foot.append(inspectButton); } - const done = node("button", "Klaar"); + const done = node("button", "Done"); done.type = "button"; done.onclick = closeTileSheet; foot.append(done); @@ -978,9 +978,9 @@ function renderTileSheet() { // ---- Top bar ---- // The name on the left; on the right up to six items: the time, an analog clock, the date, or an // entity's state or last change. The add-on formats entity text (POST header-preview) exactly as the -// screen gets it; the screen and this mockup tick clocks and "5 min geleden" themselves. -const MONTHS = ["jan", "feb", "mrt", "apr", "mei", "jun", "jul", "aug", "sep", "okt", "nov", "dec"]; -const WEEKDAYS = ["zo", "ma", "di", "wo", "do", "vr", "za"]; +// screen gets it; the screen and this mockup tick clocks and "5 min ago" themselves. +const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; +const WEEKDAYS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]; const BUILTIN_ICONS = { clock: "clock-outline", analog: "clock-outline", date: "calendar" }; let topbarPreviews = new Map(), topbarTimer = 0, topbarSheetIndex = null, topbarOverflow = new Set(), topbarAdded = null; const itemKey = (item) => JSON.stringify(item); @@ -1036,19 +1036,19 @@ const dateText = (now = new Date()) => `${WEEKDAYS[now.getDay()]} ${now.getDate( function agoText(then, now = Math.floor(Date.now() / 1000)) { const seconds = now - then, span = Math.abs(seconds), per = (unit) => Math.floor(span / unit); if (seconds < 0) { - if (span < 3600) return `Over ${Math.max(1, per(60))} min`; - if (span < 86400) return `Over ${per(3600)} uur`; - if (span < 172800) return "Morgen"; - return `Over ${per(86400)} dagen`; + if (span < 3600) return `In ${Math.max(1, per(60))} min`; + if (span < 86400) return per(3600) === 1 ? "In 1 hour" : `In ${per(3600)} hours`; + if (span < 172800) return "Tomorrow"; + return `In ${per(86400)} days`; } - if (span < 60) return "Zojuist"; - if (span < 3600) return `${per(60)} min geleden`; - if (span < 86400) return `${per(3600)} uur geleden`; - if (span < 172800) return "Gisteren"; - if (span < 604800) return `${per(86400)} dagen geleden`; - if (span < 2592000) return per(604800) === 1 ? "1 week geleden" : `${per(604800)} weken geleden`; - if (span < 31536000) return per(2592000) === 1 ? "1 maand geleden" : `${per(2592000)} maanden geleden`; - return `${per(31536000)} jaar geleden`; + if (span < 60) return "Just now"; + if (span < 3600) return `${per(60)} min ago`; + if (span < 86400) return per(3600) === 1 ? "1 hour ago" : `${per(3600)} hours ago`; + if (span < 172800) return "Yesterday"; + if (span < 604800) return `${per(86400)} days ago`; + if (span < 2592000) return per(604800) === 1 ? "1 week ago" : `${per(604800)} weeks ago`; + if (span < 31536000) return per(2592000) === 1 ? "1 month ago" : `${per(2592000)} months ago`; + return per(31536000) === 1 ? "1 year ago" : `${per(31536000)} years ago`; } function topbarLabel(item) { if (item.type === "entity") return entityName(item.entity); @@ -1088,7 +1088,7 @@ function inkOf(text, font) { } const barFonts = (m) => ({ name: `500 ${m.name}px "Bar Roboto"`, text: `400 ${m.text}px "Bar Roboto"`, icon: `${m.icon}px "Tile Icons"` }); // The fonts load on first use; measurements before that are wrong, so draw again once they are in. -Promise.all([document.fonts.load('500 27px "Bar Roboto"', "Studio 0"), document.fonts.load('400 21px "Bar Roboto"', "Weg 0"), document.fonts.load('26px "Tile Icons"', String.fromCodePoint(0xf0150))]) +Promise.all([document.fonts.load('500 27px "Bar Roboto"', "Studio 0"), document.fonts.load('400 21px "Bar Roboto"', "Away 0"), document.fonts.load('26px "Tile Icons"', String.fromCodePoint(0xf0150))]) .then(() => { inkCache.clear(); if (layout) { renderTopbar(); renderBars(); } }) .catch(() => {}); // Same integer arithmetic as header_bar::gaps() in the firmware, from the digit height in pixels. @@ -1096,7 +1096,7 @@ function barGaps(cap) { return { icon: Math.max(2, Math.floor((cap * 4 + 5) / 10)), item: Math.max(6, Math.floor((cap * 125 + 50) / 100)), name: Math.max(8, Math.floor((cap * 16 + 5) / 10)) }; } // The parts per item with their ink widths, the placement, and which items fall off. -function barLayout(items = topbarItems(), metrics = barMetrics(), nameText = $("#title").value || "Thuis") { +function barLayout(items = topbarItems(), metrics = barMetrics(), nameText = $("#title").value || "Home") { const fonts = barFonts(metrics); // The firmware reads the digit height as a whole number of pixels (the glyph box of "0"). const zero = inkOf("0", fonts.text), cap = Math.round(zero.bottom - zero.top), gaps = barGaps(cap); @@ -1180,9 +1180,9 @@ function topbarBar() { const group = svgNode("g"); drawParts(group, lay, lay.placed, m.top); svg.append(group); - svg.setAttribute("aria-label", `Bovenbalk: ${lay.nameText}`); + svg.setAttribute("aria-label", `Top bar: ${lay.nameText}`); const wrap = node("div", undefined, "preview-bar-wrap"); - wrap.title = "Bovenbalk aanpassen"; + wrap.title = "Edit top bar"; wrap.append(svg); wrap.onclick = () => { $("#topbar").scrollIntoView({ behavior: "smooth", block: "center" }); $("#topbar").classList.add("flash"); setTimeout(() => $("#topbar").classList.remove("flash"), 900); }; return wrap; @@ -1194,7 +1194,7 @@ function fitTopbars() { topbarOverflow = dropped; if ([...dropped].join() !== before) renderTopbar(); } -// One item on its own, drawn exactly as in the bar (the sheet's "zo staat het op het scherm"). +// One item on its own, drawn exactly as in the bar (the sheet's "how it looks on the screen"). function itemSample(item) { const lay = barLayout([item]), m = lay.metrics, part = lay.parts[0]; part.x = 0; @@ -1211,10 +1211,10 @@ function renderTopbar() { const items = topbarItems(), chips = $("#topbar-chips"); chips.replaceChildren(); items.forEach((item, index) => chips.append(topbarChip(item, index))); - const add = node("button", "+ Toevoegen", "topbar-add"); + const add = node("button", "+ Add", "topbar-add"); add.type = "button"; add.disabled = items.length >= topbarMax(); - add.title = add.disabled ? `Maximaal ${topbarMax()} onderdelen` : "Tijd, datum, analoge klok of een entiteit toevoegen"; + add.title = add.disabled ? `Maximum ${topbarMax()} items` : "Add the time, date, analog clock, or an entity"; add.onclick = () => openTopbarSheet(-1); chips.append(add); $("#topbar-count").textContent = `${items.length} / ${topbarMax()}`; @@ -1222,8 +1222,8 @@ function renderTopbar() { const [major, minor, patch] = needed.split(".").map(Number); const hint = $("#topbar-hint"); hint.textContent = supportsFirmware(major, minor, patch) - ? topbarOverflow.size ? "Niet alles past naast de naam: het scherm laat de gestreepte onderdelen weg. Haal er een weg of kies een kortere naam." : "Sleep om de volgorde te wijzigen; tik op een onderdeel om het in te stellen." - : `Sensoren, datum en de analoge klok verschijnen vanaf firmware ${needed}; tot die update toont dit scherm de naam en, als de tijd erin staat, de klok.`; + ? topbarOverflow.size ? "Not everything fits next to the name: the screen drops the dashed items. Remove one or choose a shorter name." : "Drag to reorder; tap an item to configure it." + : `Sensors, the date, and the analog clock appear from firmware ${needed}; until that update, this screen shows the name and, if the time is in the bar, the clock.`; hint.classList.toggle("warn", topbarOverflow.size > 0 && supportsFirmware(major, minor, patch)); } function topbarChip(item, index) { @@ -1241,14 +1241,14 @@ function topbarChip(item, index) { if (cp) icon.textContent = glyph(cp); if (view.color) icon.style.color = view.color; const texts = node("span", undefined, "chip-text"); - const detail = !view.shown ? "Verborgen: nu niet actief" : topbarOverflow.has(index) ? "Past niet naast de naam" : view.analog ? "Wijzerplaat" : view.text; + const detail = !view.shown ? "Hidden: not active right now" : topbarOverflow.has(index) ? "Doesn't fit next to the name" : view.analog ? "Dial" : view.text; texts.append(node("strong", topbarLabel(item)), node("small", detail)); const remove = node("button", "✕", "chip-remove"); remove.type = "button"; - remove.setAttribute("aria-label", `${topbarLabel(item)} uit de bovenbalk halen`); + remove.setAttribute("aria-label", `Remove ${topbarLabel(item)} from the top bar`); remove.onclick = (e) => { e.stopPropagation(); removeTopbarItem(index); }; chip.append(icon, texts, remove); - chip.setAttribute("aria-label", `${topbarLabel(item)}, plek ${index + 1}. Enter: instellen, pijltjes: verplaatsen`); + chip.setAttribute("aria-label", `${topbarLabel(item)}, slot ${index + 1}. Enter: configure, arrows: move`); chip.onclick = () => openTopbarSheet(index); chip.onkeydown = (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); openTopbarSheet(index); return; } @@ -1271,15 +1271,15 @@ function removeTopbarItem(index) { if (!item) return; if ($("#topbar-sheet").open) $("#topbar-sheet").close(); setTopbarItems(items); - toast(`${topbarLabel(item)} uit de bovenbalk gehaald`, { - label: "Ongedaan maken", + toast(`${topbarLabel(item)} removed from the top bar`, { + label: "Undo", run: () => { const back = [...topbarItems()]; back.splice(Math.min(index, back.length), 0, item); setTopbarItems(back); }, }); } function addTopbarItem(item) { const items = topbarItems(); - if (items.length >= topbarMax()) return toast(`De bovenbalk heeft plaats voor ${topbarMax()} onderdelen.`); - if (items.some((other) => itemKey(other) === itemKey(item))) return toast("Dit staat al in de bovenbalk."); + if (items.length >= topbarMax()) return toast(`The top bar has room for ${topbarMax()} items.`); + if (items.some((other) => itemKey(other) === itemKey(item))) return toast("This is already in the top bar."); // The new chip lights up briefly so the eye finds it. topbarAdded = { key: itemKey(item), time: Date.now() }; setTopbarItems([...items, item]); @@ -1363,7 +1363,7 @@ function sheetHead(badge, title, subtitle) { titles.append(node("strong", title), node("small", subtitle)); const close = node("button", "✕", "quiet sheet-close"); close.type = "button"; - close.setAttribute("aria-label", "Sluiten"); + close.setAttribute("aria-label", "Close"); close.onclick = () => $("#topbar-sheet").close(); head.append(badge, titles, close); return head; @@ -1403,28 +1403,28 @@ function renderTopbarSheet() { badge.textContent = next.icon ? glyph(next.icon) : ""; }; const content = segmented(inventory.header.contents.map((c) => [c.key, c.label]), item.content, (v) => update({ content: v })); - const contentField = group("Wat laten zien", content); - contentField.append(node("small", "Laatst gewijzigd telt op het scherm zelf door: “Zojuist”, “5 min geleden”, “Gisteren”.", "field-hint")); + const contentField = group("What to show", content); + contentField.append(node("small", "Last changed keeps counting on the screen itself: “Just now”, “5 min ago”, “Yesterday”.", "field-hint")); body.append(contentField); body.append(iconPicker({ selected: item.icon, automatic: topbarPreviews.get(itemKey(item))?.auto_icon || automaticIcon(item.entity), - autoLabel: "Automatisch (zoals Home Assistant)", + autoLabel: "Automatic (like Home Assistant)", allowNone: true, onPick: (name) => update({ icon: name }), })); const shows = segmented(inventory.header.shows.map((s) => [s.key, s.label]), item.show, (v) => update({ show: v })); - const showField = group("Tonen", shows); - showField.append(node("small", "Alleen als actief verbergt het onderdeel zolang het uit, dicht, weg of 0 is. Handig voor een open deur, een draaiende wasmachine of wie er thuis is.", "field-hint")); + const showField = group("Show", shows); + showField.append(node("small", "Only when active hides the item as long as it's off, closed, away, or 0. Handy for an open door, a running washing machine, or who's home.", "field-hint")); body.append(showField); } else { badge.textContent = glyph(iconNamed(BUILTIN_ICONS[item.type])?.cp || ""); - sheet.append(sheetHead(badge, topbarLabel(item), "Van het scherm zelf, werkt ook zonder Home Assistant")); + sheet.append(sheetHead(badge, topbarLabel(item), "From the screen itself, also works without Home Assistant")); const live = node("div", undefined, "topbar-live"); live.id = "topbar-live"; body.append(live); if (item.type !== "date") { - const format = segmented([["24", "24 uur"], ["12", "12 uur"]], (layout.settings?.clock_24h ?? true) ? "24" : "12", (v) => { + const format = segmented([["24", "24 hour"], ["12", "12 hour"]], (layout.settings?.clock_24h ?? true) ? "24" : "12", (v) => { layout.settings = { ...Object.fromEntries(settingDefinitions.map(([key, , , value]) => [key, value])), ...layout.settings, clock_24h: v === "24" }; markDirty(); renderSettings(); @@ -1432,60 +1432,60 @@ function renderTopbarSheet() { renderBars(); renderTopbarLive(); }); - const formatField = group("Notatie", format); - formatField.append(node("small", "Geldt voor elke klok op dit scherm, ook de kloktegels.", "field-hint")); + const formatField = group("Format", format); + formatField.append(node("small", "Applies to every clock on this screen, including the clock tiles.", "field-hint")); body.append(formatField); } } const foot = node("div", undefined, "sheet-foot"); - const remove = node("button", "Weghalen", "quiet danger"); + const remove = node("button", "Remove", "quiet danger"); remove.type = "button"; remove.onclick = () => removeTopbarItem(topbarSheetIndex); - const left = node("button", "← Naar links", "quiet"), right = node("button", "Naar rechts →", "quiet"); + const left = node("button", "← Move left", "quiet"), right = node("button", "Move right →", "quiet"); left.type = right.type = "button"; left.disabled = topbarSheetIndex === 0; right.disabled = topbarSheetIndex >= topbarItems().length - 1; left.onclick = () => { if (moveTopbarItem(topbarSheetIndex, topbarSheetIndex - 1)) { topbarSheetIndex--; renderTopbarSheet(); } }; right.onclick = () => { if (moveTopbarItem(topbarSheetIndex, topbarSheetIndex + 1)) { topbarSheetIndex++; renderTopbarSheet(); } }; - const done = node("button", "Klaar"); + const done = node("button", "Done"); done.type = "button"; done.onclick = () => sheet.close(); foot.append(remove, left, right, done); sheet.append(body, foot); renderTopbarLive(); } -// "Op het scherm": the item as the bar draws it, updated while choosing. +// "How it looks": the item as the bar draws it, updated while choosing. function renderTopbarLive() { const live = $("#topbar-live"), item = topbarItems()[topbarSheetIndex]; if (!live || !item) return; const view = topbarView(item); - const note = !view.shown ? "Nu verborgen: niet actief" : topbarOverflow.has(topbarSheetIndex) ? "Past nu niet naast de naam" : "Zo staat het op het scherm"; + const note = !view.shown ? "Hidden right now: not active" : topbarOverflow.has(topbarSheetIndex) ? "Doesn't currently fit next to the name" : "How it looks on the screen"; live.replaceChildren(node("small", note), itemSample(item)); } function renderTopbarAdd(sheet) { const badge = node("span", "+", "domain-icon"); - sheet.append(sheetHead(badge, "Toevoegen aan de bovenbalk", `${topbarItems().length} van ${topbarMax()} plekken gebruikt`)); + sheet.append(sheetHead(badge, "Add to the top bar", `${topbarItems().length} of ${topbarMax()} slots used`)); const body = node("div", undefined, "sheet-body"); const taken = new Set(topbarItems().map(itemKey)); const own = node("div", undefined, "topbar-options"); - const samples = { clock: clockText(), analog: "Kleine wijzerplaat met de tijd", date: dateText() }; + const samples = { clock: clockText(), analog: "Small dial with the time", date: dateText() }; for (const builtin of inventory.header?.builtin || []) { const item = { type: builtin.type }; - own.append(optionButton(iconNamed(BUILTIN_ICONS[builtin.type])?.cp, builtin.label, taken.has(itemKey(item)) ? "Staat er al in" : samples[builtin.type], () => addTopbarItem(item), taken.has(itemKey(item)))); + own.append(optionButton(iconNamed(BUILTIN_ICONS[builtin.type])?.cp, builtin.label, taken.has(itemKey(item)) ? "Already added" : samples[builtin.type], () => addTopbarItem(item), taken.has(itemKey(item)))); } - body.append(group("Van het scherm zelf", own)); + body.append(group("From the screen itself", own)); const suggested = inventory.header?.suggestions?.[selected] || []; if (suggested.length) { const list = node("div", undefined, "topbar-options"); for (const s of suggested) { const exists = taken.has(itemKey(s.item)); - list.append(optionButton(s.icon || automaticIcon(s.item.entity), s.label, exists ? "Staat er al in" : [s.name, s.area].filter(Boolean).join(" · "), () => addTopbarItem(s.item), exists)); + list.append(optionButton(s.icon || automaticIcon(s.item.entity), s.label, exists ? "Already added" : [s.name, s.area].filter(Boolean).join(" · "), () => addTopbarItem(s.item), exists)); } - body.append(group("Suggesties uit Home Assistant", list)); + body.append(group("Suggestions from Home Assistant", list)); } const search = node("input"); search.type = "search"; - search.placeholder = "Zoek op naam, ruimte of entiteit, bijv. temperatuur of deur"; + search.placeholder = "Search by name, room, or entity, e.g. temperature or door"; const results = node("div", undefined, "topbar-results"); const renderMatches = () => { const query = search.value.trim().toLocaleLowerCase(); @@ -1494,17 +1494,17 @@ function renderTopbarAdd(sheet) { const item = { type: "entity", entity: e.id, content: "state", icon: "auto", show: "always" }; return optionButton(automaticIcon(e.id), e.name, [e.area, e.id].filter(Boolean).join(" · "), () => addTopbarItem(item), taken.has(itemKey(item))); })); - if (!matches.length) results.append(node("p", "Geen entiteiten gevonden.", "hint")); - else if (matches.length > 40) results.append(node("p", `${matches.length} resultaten. Typ verder om te verfijnen.`, "hint")); + if (!matches.length) results.append(node("p", "No entities found.", "hint")); + else if (matches.length > 40) results.append(node("p", `${matches.length} results. Keep typing to narrow it down.`, "hint")); }; search.oninput = renderMatches; - search.setAttribute("aria-label", "Zoek een entiteit voor de bovenbalk"); - const searchField = group("Een entiteit", search); + search.setAttribute("aria-label", "Search for an entity for the top bar"); + const searchField = group("An entity", search); searchField.append(results); body.append(searchField); renderMatches(); const foot = node("div", undefined, "sheet-foot"); - const cancel = node("button", "Annuleren", "quiet"); + const cancel = node("button", "Cancel", "quiet"); cancel.type = "button"; cancel.onclick = () => sheet.close(); foot.append(cancel); @@ -1654,13 +1654,13 @@ function renderResults() { } if (!matches.length) $("#results").append( - node("p", "Geen entiteiten gevonden. Probeer een andere naam.", "hint"), + node("p", "No entities found. Try a different name.", "hint"), ); if (matches.length > 80) $("#results").append( node( "p", - `${matches.length} resultaten. Typ verder om te verfijnen.`, + `${matches.length} results. Keep typing to narrow it down.`, "hint", ), ); @@ -1671,8 +1671,8 @@ async function refresh(full = true) { // A light poll carries only screens and update status; keep the catalogues we have. inventory = full ? data : { ...inventory, ...data }; $("#connection").textContent = inventory.connected - ? "● Home Assistant verbonden" - : "Verbinding met Home Assistant herstellen…"; + ? "● Home Assistant connected" + : "Reconnecting to Home Assistant…"; $("#connection").classList.toggle("online", inventory.connected); renderScreens(); if (selected) renderSettingsSupport(); @@ -1680,7 +1680,7 @@ async function refresh(full = true) { if ($("#alerts-dialog").open) renderAlertScreens(); } catch { $("#connection").textContent = - "Beheerpagina niet bereikbaar · opnieuw proberen…"; + "Management page unreachable · retrying…"; } } $("#save").onclick = async () => { @@ -1697,10 +1697,10 @@ $("#save").onclick = async () => { body: JSON.stringify(layout), }); dirty = false; - $("#dirty").textContent = "Opgeslagen"; + $("#dirty").textContent = "Saved"; $("#save-detail").textContent = - "Synchronisatie volgt automatisch, ook na opnieuw verbinden."; - toast("Opgeslagen. Je scherm wordt bijgewerkt."); + "Sync follows automatically, even after reconnecting."; + toast("Saved. Your screen is being updated."); await refresh(); } catch (e) { toast(e.message); @@ -1714,25 +1714,25 @@ $("#add-page").onclick = addPage; $("#search").oninput = renderResults; $("#refresh").onclick = refresh; for (const [value, label] of [ - ["", "Alles"], - ["light", "Lampen"], - ["climate", "Klimaat"], - ["switch", "Schakelaars"], + ["", "All"], + ["light", "Lights"], + ["climate", "Climate"], + ["switch", "Switches"], ["binary_sensor", "Status"], - ["button", "Acties"], + ["button", "Actions"], ["script", "Scripts"], - ["fan", "Ventilatoren"], - ["cover", "Zonwering"], - ["scene", "Scènes"], + ["fan", "Fans"], + ["cover", "Covers"], + ["scene", "Scenes"], ["vacuum", "Vacuum"], - ["sensor", "Sensoren"], + ["sensor", "Sensors"], ["media_player", "Media"], - ["weather", "Weer"], - ["number", "Waarden"], - ["select", "Keuzelijsten"], - ["person", "Personen"], - ["timer", "Kookwekkers"], - ["screen", "Klok"], + ["weather", "Weather"], + ["number", "Values"], + ["select", "Selects"], + ["person", "People"], + ["timer", "Timers"], + ["screen", "Clock"], ]) { const b = node("button", label, value === "" ? "active" : ""); if (value) b.prepend(domainBadge(value + ".")); @@ -1746,7 +1746,7 @@ for (const [value, label] of [ }; $("#filters").append(b); } -// ----- Nieuw scherm: profiel, wifi en de eerste flash in één venster ----- +// ----- New screen: profile, Wi-Fi, and the first flash in one window ----- const installer = { poll: null, view: "setup", file: null, friendly: "", board: "cyd", target: "", apiKey: null, nodeEdited: false, ports: null, jobState: null, @@ -1755,7 +1755,7 @@ const installer = { function slug(text) { const clean = text.toLowerCase().normalize("NFD").replace(/[̀-ͯ]/g, "") .replace(/[^a-z0-9]+/g, "-").replace(/^[^a-z]+/, "").slice(0, 30).replace(/-+$/, ""); - return clean || "scherm"; + return clean || "screen"; } function portLabel(port) { const id = port.replace(/^\/dev\/serial\/by-id\/usb-/, "").replace(/-if\d+(-port\d+)?$/, "").replace(/_/g, " "); @@ -1772,7 +1772,7 @@ function renderTargets(ports) { if (installer.ports === key) return; installer.ports = key; const current = select.value; - const later = node("option", "Later · alleen het profiel bewaren"); + const later = node("option", "Later · save profile only"); later.value = ""; select.replaceChildren(...ports.map((p) => { const o = node("option", portLabel(p)); o.value = p; return o; }), later); select.value = ports.includes(current) ? current : ports[0] || ""; @@ -1782,13 +1782,13 @@ function renderTargetHint() { const select = $("#install-target"); const ports = select.options.length - 1; $("#target-hint").textContent = !ports - ? "Geen USB-poort gevonden. Sluit het scherm met een datakabel aan op de Home Assistant-machine; de lijst ververst vanzelf." + ? "No USB port found. Connect the screen with a data cable to the Home Assistant machine; the list refreshes on its own." : !select.value - ? "Het profiel komt in de ESPHome-map. Installeren kan later via Firmware & USB, of vanuit ESPHome Device Builder." + ? "The profile goes into the ESPHome folder. You can install later via Firmware & USB, or from ESPHome Device Builder." : ports > 1 - ? "Meer dan één bord aangesloten: kies de poort van dit scherm." - : "Eén keer via USB; daarna gaat alles draadloos."; - $("#install-go").textContent = select.value ? "Installeren" : "Profiel bewaren"; + ? "More than one board connected: choose this screen's port." + : "Once over USB; after that, everything is wireless."; + $("#install-go").textContent = select.value ? "Install" : "Save profile"; } function renderWifi(wifi) { const fields = $("#wifi-fields"); @@ -1800,13 +1800,13 @@ function renderWifi(wifi) { $("#wifi-ssid-label").hidden = !missing.includes("wifi_ssid"); $("#wifi-password-label").hidden = !missing.includes("wifi_password"); $("#wifi-status").textContent = wifi.state === "new" - ? "Eén keer invullen: ESP Screens bewaart dit in ESPHome secrets.yaml, volgende schermen gebruiken het automatisch." - : "Je ESPHome secrets.yaml mist nog wifi-gegevens. ESP Screens vult alleen de ontbrekende regels aan."; + ? "Fill in once: ESP Screens saves this in ESPHome secrets.yaml, later screens use it automatically." + : "Your ESPHome secrets.yaml is still missing Wi-Fi details. ESP Screens only fills in the missing lines."; } return wifi?.state === "ready" - ? "Wifi komt uit je ESPHome secrets.yaml." + ? "Wi-Fi comes from your ESPHome secrets.yaml." : wifi?.state === "invalid" - ? "secrets.yaml in de ESPHome-map is geen geldige YAML. Herstel het bestand eerst; het wordt niet overschreven." + ? "secrets.yaml in the ESPHome folder isn't valid YAML. Fix the file first; it won't be overwritten." : ""; } async function installerRefresh() { @@ -1827,9 +1827,9 @@ async function installerRefresh() { const busy = job?.state === "running"; const flashing = !!$("#install-target").value; $("#install-note").textContent = busy - ? `Er loopt al een build of installatie (${job.file}). Wacht tot die klaar is.` + ? `A build or installation is already running (${job.file}). Wait for it to finish.` : !data.available && flashing - ? "ESPHome CLI ontbreekt in deze installatie; alleen het profiel bewaren kan." + ? "The ESPHome CLI is missing from this installation; only saving the profile is possible." : wifiNote; $("#install-go").disabled = data.wifi?.state === "invalid" || (flashing && (busy || !data.available)); } else if (installer.view === "progress" && ours) { @@ -1857,17 +1857,17 @@ function renderProgress(job, logs) { $("#progress-spin").hidden = false; $("#progress-mark").hidden = true; } else outcome(ok); - $("#install-title").textContent = running ? "Even geduld…" : ok ? "Klaar." : "Dat lukte niet."; + $("#install-title").textContent = running ? "One moment…" : ok ? "Done." : "That didn't work."; $("#progress-title").textContent = running - ? job.stage === "upload" ? `Firmware naar ${installer.friendly} schrijven…` : "Firmware bouwen…" - : ok ? `Firmware staat op ${installer.friendly}` : "Installeren mislukt"; + ? job.stage === "upload" ? `Writing firmware to ${installer.friendly}…` : "Building firmware…" + : ok ? `Firmware is on ${installer.friendly}` : "Install failed"; $("#progress-detail").textContent = running ? job.stage === "upload" - ? "Haal de USB-kabel nog niet los." - : "Een eerste build duurt op een Raspberry enkele minuten. Je mag dit venster sluiten: de installatie loopt door en je vindt hem terug onder Nieuw scherm." + ? "Don't disconnect the USB cable yet." + : "A first build takes a few minutes on a Raspberry Pi. You can close this window: the installation keeps running and you'll find it again under New screen." : ok - ? `Het scherm start op en verbindt met je wifi.${installer.board === "cyd" ? " De CYD vraagt eerst om een touch-kalibratie: tik de kruisjes aan." : ""} Koppel het nu aan Home Assistant:` - : logs.filter((l) => /error/i.test(l)).pop() || logs.filter((l) => /failed|mislukt|fout/i.test(l)).pop() || "Bekijk het log hieronder."; + ? `The screen boots up and connects to your Wi-Fi.${installer.board === "cyd" ? " The CYD first asks for a touch calibration: tap the crosshairs." : ""} Pair it with Home Assistant now:` + : logs.filter((l) => /error/i.test(l)).pop() || logs.filter((l) => /failed/i.test(l)).pop() || "See the log below."; const pre = $("#install-log"); const stick = pre.scrollTop + pre.clientHeight >= pre.scrollHeight - 8; pre.textContent = logs.join("\n"); @@ -1876,7 +1876,7 @@ function renderProgress(job, logs) { $("#install-result").hidden = !ok; if (ok) renderSteps(); $("#install-retry").hidden = running || ok; - $("#install-close").textContent = ok ? "Klaar" : "Sluiten"; + $("#install-close").textContent = ok ? "Done" : "Close"; $("#install-close").classList.toggle("quiet", !ok); } function showSaved() { @@ -1884,30 +1884,30 @@ function showSaved() { $("#install-setup").hidden = true; $("#install-progress").hidden = false; outcome(true); - $("#install-title").textContent = "Profiel bewaard."; - $("#progress-title").textContent = `${installer.file} staat in de ESPHome-map`; + $("#install-title").textContent = "Profile saved."; + $("#progress-title").textContent = `${installer.file} is in the ESPHome folder`; $("#progress-detail").textContent = - "Installeren kan zodra het scherm aan de Home Assistant-machine hangt: Firmware & USB → dit profiel → USB-poort → Bouwen & installeren. Of open het profiel in ESPHome Device Builder (dezelfde map) en flash vanuit je browser. Bewaar de API-sleutel voor de koppeling:"; + "You can install once the screen is connected to the Home Assistant machine: Firmware & USB → this profile → USB port → Build & install. Or open the profile in ESPHome Device Builder (same folder) and flash from your browser. Save the API key for pairing:"; $("#install-result").hidden = false; renderSteps(); $("#install-log-wrap").hidden = true; $("#install-retry").hidden = true; - $("#install-close").textContent = "Klaar"; + $("#install-close").textContent = "Done"; $("#install-close").classList.remove("quiet"); } function renderSteps() { $("#api-key").textContent = installer.apiKey || ""; const steps = [ - ["Ga naar Home Assistant → Instellingen → Apparaten & diensten.", ` Dit gebeurt buiten ESP Screens. Home Assistant ontdekt ${installer.friendly} als ESPHome-apparaat; klik op Toevoegen. Niet ontdekt? Voeg ESPHome handmatig toe met het IP-adres van het scherm. `], - ["Plak de API-sleutel", " hierboven zodra Home Assistant om een encryptiesleutel vraagt."], - ["Sta HA-acties toe:", " ESPHome-integratie → Configureren → “Allow the device to perform Home Assistant actions”. Zonder dit ziet het scherm alles, maar bedient het niets."], - ["Kies je tegels.", " Terug in ESP Screens verschijnt het scherm binnen een halve minuut in de lijst links; tot die tijd staat het daar als “nog niet in Home Assistant”."], + ["Go to Home Assistant → Settings → Devices & services.", ` This happens outside ESP Screens. Home Assistant discovers ${installer.friendly} as an ESPHome device; click Add. Not discovered? Add ESPHome manually with the screen's IP address. `], + ["Paste the API key", " above when Home Assistant asks for an encryption key."], + ["Allow HA actions:", " ESPHome integration → Configure → “Allow the device to perform Home Assistant actions”. Without this, the screen sees everything but controls nothing."], + ["Choose your tiles.", " Back in ESP Screens, the screen appears in the list on the left within about thirty seconds; until then it's shown there as “not yet in Home Assistant”."], ]; $("#install-steps").replaceChildren(...steps.map(([b, t], i) => { const li = node("li"); li.append(node("b", b), t); if (i === 0) { - const go = node("button", "Open Apparaten & diensten", "mini"); + const go = node("button", "Open Devices & services", "mini"); go.type = "button"; go.onclick = openIntegrations; li.append(go); @@ -1925,7 +1925,7 @@ function resetInstaller() { $("#install-result").hidden = true; $("#install-log-wrap").open = false; $("#install-log").textContent = ""; - $("#install-title").textContent = "Aansluiten en installeren."; + $("#install-title").textContent = "Connect and install."; $("#install-status").textContent = ""; $("#install-note").textContent = ""; $("#install-target").replaceChildren(); @@ -2003,7 +2003,7 @@ refresh(); let pollTimer, lastFull = Date.now(), live = false, stream; function applyLive(data) { inventory = { ...inventory, ...data }; - $("#connection").textContent = inventory.connected ? "● Home Assistant verbonden" : "Verbinding met Home Assistant herstellen…"; + $("#connection").textContent = inventory.connected ? "● Home Assistant connected" : "Reconnecting to Home Assistant…"; $("#connection").classList.toggle("online", inventory.connected); renderScreens(); if (selected) renderSettingsSupport(); @@ -2058,7 +2058,7 @@ async function firmwareRefresh(initial = false) { }), ); $("#firmware-port").replaceChildren( - ...[["ota", "Wifi / OTA"], ...data.ports.map((p) => [p, p])].map( + ...[["ota", "Wi-Fi / OTA"], ...data.ports.map((p) => [p, p])].map( ([value, text]) => { const o = node("option", text); o.value = value; @@ -2072,10 +2072,10 @@ async function firmwareRefresh(initial = false) { $("#firmware-" + id).disabled = running || !data.available || !data.profiles.length; $("#firmware-status").textContent = !data.available - ? "ESPHome CLI ontbreekt. Werk de app bij naar 0.2.0." + ? "The ESPHome CLI is missing. Update the app to 0.2.0." : data.job ? `${data.job.file} · ${data.job.action} · ${data.job.state}` - : "Kies het bedoelde profiel en een USB-poort of IP-adres."; + : "Choose the intended profile and a USB port or IP address."; $("#firmware-log").textContent = data.logs.join("\n"); } catch (e) { toast(e.message); @@ -2134,7 +2134,7 @@ function openSection(id) { async function inspect(entity) { if (!selected) return; openSection("#inspector-section"); - $("#inspection-summary").textContent = "Actuele HA-status ophalen..."; + $("#inspection-summary").textContent = "Fetching current HA status..."; try { const data = await ( await api(`screens/${encodeURIComponent(selected)}/inspect`) @@ -2154,7 +2154,7 @@ async function inspect(entity) { card.append( node( "small", - `Kleine slider: ${options.inline === "slider" ? "ja" : "nee"} · Weergave: ${displayNames[options.display || "standard"] || options.display} · Breedte: ${options.size === "wide" ? "dubbel" : "normaal"} · Bediening: ${controlsLabel({ entity: tile.entity, options })} · Achtergrond: ${inventory.backgrounds?.[options.background || "auto"]?.label || "Standaard"}`, + `Small slider: ${options.inline === "slider" ? "yes" : "no"} · Display: ${displayNames[options.display || "standard"] || options.display} · Width: ${options.size === "wide" ? "double" : "normal"} · Control: ${controlsLabel({ entity: tile.entity, options })} · Background: ${inventory.backgrounds?.[options.background || "auto"]?.label || "Default"}`, ), ); $("#inspection-summary").append(card); @@ -2181,7 +2181,7 @@ const versionAtLeast = (version, minimum) => { }; const yamlString = (text) => `"${String(text).replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`; function copyChip(text, what) { - const b = node("button", "Kopieer", "mini"); + const b = node("button", "Copy", "mini"); b.type = "button"; b.onclick = () => copyText(text, undefined, what); return b; @@ -2189,19 +2189,19 @@ function copyChip(text, what) { function alertExampleYaml(action) { const fields = inventory.alerts?.fields || []; const lines = fields.map((f) => ` ${f.name}: ${f.type === "string" ? (/^[a-z][a-z0-9-]*$/.test(f.example) ? f.example : yamlString(f.example)) : f.example === true ? "true" : f.example === false ? "false" : f.example}`); - return `action: ${action || "esphome._show_alert"}\ndata:\n${lines.join("\n")}`; + return `action: ${action || "esphome._show_alert"}\ndata:\n${lines.join("\n")}`; } function alertWaitYaml(action) { return [ - `# Deurbel: toon de alert en wacht tot iemand op de knop drukt.`, + `# Doorbell: show the alert and wait until someone presses the button.`, `actions:`, - ` - action: ${action || "esphome._show_alert"}`, + ` - action: ${action || "esphome._show_alert"}`, ` data:`, - ` title: "Iemand belt aan"`, - ` subtitle: "Deur 3, achterkant"`, + ` title: "Someone is at the door"`, + ` subtitle: "Door 3, back"`, ` icon: doorbell`, ` color: orange`, - ` button_text: "Ik kom"`, + ` button_text: "Coming"`, ` timeout: 0`, ` flash: true`, ` - wait_for_trigger:`, @@ -2216,7 +2216,7 @@ function alertWaitYaml(action) { ` then:`, ` - action: notify.notify`, ` data:`, - ` message: "Iemand komt naar de deur."`, + ` message: "Someone is coming to the door."`, ].join("\n"); } function renderAlertScreens() { @@ -2225,20 +2225,20 @@ function renderAlertScreens() { $("#alerts-min-firmware").textContent = alerts.min_firmware; list.replaceChildren(); if (!inventory.screens.length) { - list.append(node("p", "Nog geen scherm gekoppeld. Installeer er een via Nieuw scherm; de actie verschijnt zodra Home Assistant het scherm ziet.", "hint")); + list.append(node("p", "No screen paired yet. Install one via New screen; the action appears once Home Assistant sees the screen.", "hint")); } for (const screen of inventory.screens) { const row = node("div", undefined, "alert-screen"); const head = node("div", undefined, "alert-screen-head"); head.append(node("strong", screen.name)); const ready = versionAtLeast(screen.firmware, alerts.min_firmware) && screen.alert_action; - const badge = node("span", ready ? `● firmware ${screen.firmware}` : screen.alert_action ? `Update nodig · firmware ${screen.firmware || "onbekend"}` : "Apparaatnaam onbekend · werk het scherm bij", `badge ${ready ? "online" : "update"}`); + const badge = node("span", ready ? `● firmware ${screen.firmware}` : screen.alert_action ? `Update needed · firmware ${screen.firmware || "unknown"}` : "Device name unknown · update the screen", `badge ${ready ? "online" : "update"}`); head.append(badge); row.append(head); - for (const [label, action] of [["Alert tonen", screen.alert_action], ["Alert sluiten", screen.dismiss_action]]) { + for (const [label, action] of [["Show alert", screen.alert_action], ["Dismiss alert", screen.dismiss_action]]) { const line = node("div", undefined, "copy-line"); - line.append(node("span", label, "copy-label"), node("code", action || "esphome._show_alert")); - if (action) line.append(copyChip(action, "Actienaam")); + line.append(node("span", label, "copy-label"), node("code", action || "esphome._show_alert")); + if (action) line.append(copyChip(action, "Action name")); row.append(line); } list.append(row); @@ -2251,7 +2251,7 @@ function renderAlertScreens() { select.append(option); } if (!select.options.length) { - const option = node("option", "een scherm (nog niet gekoppeld)"); + const option = node("option", "a screen (not paired yet)"); option.value = ""; select.append(option); } @@ -2266,10 +2266,10 @@ function renderAlertExamples() { function alertIconCard(icon, label) { const b = node("button", undefined, "alert-icon"); b.type = "button"; - b.title = `${icon.name} kopiëren`; + b.title = `Copy ${icon.name}`; b.append(node("span", glyph(icon.cp), "mdi"), node("code", icon.name)); if (label) b.append(node("small", label)); - b.onclick = () => copyText(icon.name, undefined, "Icoonnaam"); + b.onclick = () => copyText(icon.name, undefined, "Icon name"); return b; } function renderAlertIcons() { @@ -2281,13 +2281,13 @@ function renderAlertIcons() { const chip = node("button", undefined, "chip"); chip.type = "button"; chip.append(node("span", glyph(icon.cp), "mdi"), node("code", icon.name)); - chip.onclick = () => copyText(icon.name, undefined, "Icoonnaam"); + chip.onclick = () => copyText(icon.name, undefined, "Icon name"); suggested.append(chip); } const query = $("#alerts-icon-search").value.trim().toLowerCase(); const groups = $("#alerts-icon-groups"); groups.replaceChildren(); - const all = [...icons.groups, { label: "Ook beschikbaar: bedienings- en weericonen", icons: alerts.extra_icons }]; + const all = [...icons.groups, { label: "Also available: control and weather icons", icons: alerts.extra_icons }]; let shown = 0; for (const group of all) { const matches = group.icons.filter((i) => !query || i.name.includes(query) || (i.label || "").toLowerCase().includes(query)); @@ -2300,7 +2300,7 @@ function renderAlertIcons() { block.append(grid); groups.append(block); } - if (!shown) groups.append(node("p", `Geen icoon voor "${query}". Onbekende namen tonen de waarschuwingsdriehoek (${alerts.fallback_icon}).`, "hint")); + if (!shown) groups.append(node("p", `No icon for "${query}". Unknown names show the warning triangle (${alerts.fallback_icon}).`, "hint")); } function renderAlertColors() { const alerts = inventory.alerts, swatches = $("#alerts-swatches"); @@ -2311,8 +2311,8 @@ function renderAlertColors() { const blank = node("i"); blank.style.background = "#FFFFFF"; white.append(blank, node("span", undefined)); - white.lastChild.append(node("code", "leeg"), node("small", "Wit (standaard)")); - white.onclick = () => copyText("", undefined, "Lege kleur"); + white.lastChild.append(node("code", "empty"), node("small", "White (default)")); + white.onclick = () => copyText("", undefined, "Empty color"); swatches.append(white); for (const colour of alerts.colors) { const b = node("button", undefined, "swatch"); @@ -2322,18 +2322,18 @@ function renderAlertColors() { const text = node("span"); text.append(node("code", colour.name), node("small", `${colour.label} · ${colour.color}`)); b.append(dot, text); - b.onclick = () => copyText(colour.name, undefined, "Kleurnaam"); + b.onclick = () => copyText(colour.name, undefined, "Color name"); swatches.append(b); } } function renderAlertTables() { const alerts = inventory.alerts; if (!alerts) return; - const types = { string: "tekst", int: "getal", bool: "aan / uit" }; + const types = { string: "text", int: "number", bool: "on / off" }; const table = $("#alerts-field-table"); table.replaceChildren(); const head = node("tr"); - for (const title of ["Veld", "Type", "Wat het doet", "Voorbeeld", "Limiet"]) head.append(node("th", title)); + for (const title of ["Field", "Type", "What it does", "Example", "Limit"]) head.append(node("th", title)); table.append(head); for (const field of alerts.fields) { const row = node("tr"); @@ -2343,21 +2343,21 @@ function renderAlertTables() { example.append(node("code", typeof field.example === "string" ? field.example : String(field.example))); const limit = alerts.limits.cyd[field.name]; const cells = [name, node("td", types[field.type] || field.type), node("td", field.help), example, - node("td", limit ? `CYD ${limit} · Guition ${alerts.limits.guition[field.name]} bytes` : field.type === "int" ? "0 tot 86400 s" : "—")]; - cells.forEach((cell, index) => cell.dataset.label = ["Veld", "Type", "Wat het doet", "Voorbeeld", "Limiet"][index]); + node("td", limit ? `CYD ${limit} · Guition ${alerts.limits.guition[field.name]} bytes` : field.type === "int" ? "0 to 86400 s" : "—")]; + cells.forEach((cell, index) => cell.dataset.label = ["Field", "Type", "What it does", "Example", "Limit"][index]); row.append(...cells); table.append(row); } const endings = $("#alerts-ending-table"); endings.replaceChildren(); const endHead = node("tr"); - for (const title of ["action", "Wanneer"]) endHead.append(node("th", title)); + for (const title of ["action", "When"]) endHead.append(node("th", title)); endings.append(endHead); for (const ending of alerts.endings) { const row = node("tr"), cell = node("td"), when = node("td", ending.label); cell.append(node("code", ending.action)); cell.dataset.label = "action"; - when.dataset.label = "Wanneer"; + when.dataset.label = "When"; row.append(cell, when); endings.append(row); } @@ -2369,7 +2369,7 @@ function renderAlertTables() { } function renderAlerts() { if (!inventory.alerts) { - toast("De cheatsheet laadt nog; probeer het zo opnieuw."); + toast("The cheatsheet is still loading; try again in a moment."); return false; } renderAlertTables(); diff --git a/screen_manager/app/static/index.html b/screen_manager/app/static/index.html index 6e68d16..f49d36e 100644 --- a/screen_manager/app/static/index.html +++ b/screen_manager/app/static/index.html @@ -1,5 +1,5 @@ - + @@ -13,28 +13,28 @@ ▦
ESP ScreensTegels voor je CYD en Guition, zonder opnieuw flashen + >Tiles for your CYD and Guition, without reflashing
-