Put ESP Screens in English and keep layouts when entity IDs change (0.2.40)

Everything a user sees or reads is now English: both boards' labels,
status replies and logs, the ESP Screens editor and Alerts cheatsheet,
and the README and guides (docs/TILES.md, CALIBRATING.md, ACCEPTANCE.md
and TROUBLESHOOTING.md replace the Dutch file names). Labels follow
Home Assistant's own English names (Heat/Cool modes, Heating/Cooling
actions, Away, Returning to dock, weather conditions), dates use English
abbreviations and the top bar writes numbers the English way. Docs use
HA 2026.9's menu names (Settings -> Apps, App store, Settings -> Tools).
Unused legacy files (home-like.yaml, buttons.yaml, a.out) are removed.

Firmware 0.2.34 names its entities in English. Home Assistant registers
a renamed ESPHome entity under a new entity ID, so the inbox
text.<screen>_tegelinstellingen becomes text.<screen>_tile_settings and
the layout stored under the old ID would be lost. The manager now
recognises the screen by its device, during a run or after a restart,
and moves its layout and update history along; the Update button and
the nightly round follow the screen through the rename. Screens that
still run 0.2.33 or older keep working: both languages are matched.

Main is the only release branch from now on.

Tested on a Home Assistant Yellow (HA 2026.9.1) with both bench boards:
see docs/TEST_RESULTS_0240.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Max Gramser
2026-09-15 02:08:43 +02:00
parent f60eed3ff6
commit 3e959b3df9
87 changed files with 5049 additions and 11167 deletions
+380 -349
View File
@@ -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|<naam>, 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 (`<MAC>/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|<name>, 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.<apparaatnaam>_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.<scherm>_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.<device_name>_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.<screen>_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).