The cached screen list only read the entities named in SCREEN_ENTITY_NAMES, which lacked the new sensor: the
updater waited in vain after a language update, the live page kept offering it, and the nightly round would have
rebuilt the screen every night. Found in the end-to-end test.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every error, delivery status, update result and label the editor shows is an `addon` text of en.json now, in the language
of the editor's request: its X-ESP-Screens-Language header, or ?language= for the live updates, which an EventSource sends
without headers. A text made in the background and read later (a delivery status, an update result in updates.json, why a
screen can't show an alert) is kept in English with its key (i18n.Text, i18n.english) and written in the editor's language
when it is shown (i18n.shown); updates.json keeps the English message next to the key, for an older app.
The words the app sends to the screens come in the screens' language (Settings -> Language & region): the names of the
built-in tiles, the top bar's words, dates and numbers, the history card's words, the vacuum chips, the forecast's day
names and the Identify card, and so does the notification of a stopped update round. The top bar and the history card
take a binary sensor's words from screen.ha.binary, as the tiles do, which makes test_binary_words pass again; three tests
follow Home Assistant's new words there (Detected, Clear).
What stays English: what Home Assistant gets as data (a tile event's answer, the layout sensors, entity names), the logs
and the firmware jobs' logs, the screens' protocol words, and the Claude skill, which is built from the English tables in
core.py and tile_icons.py; en.json repeats those and tests/test_addon_translations.py keeps both equal.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- The page takes the screens' words its mockup draws from their own texts: screen.ha (Home Assistant's words, filled
from Home Assistant), screen.time, screen.date, screen.number and screen.tile.page, per language next to _meta and
editor. The copies under editor.mockup go; the rest of `screen` and all of `addon` stay out of the build.
- Numbers in the mockup follow the screens' language (screen.number, with its minimum grouping) or the chosen style,
as the firmware writes them.
- @intlify/unplugin-vue-i18n compiles every text at build time, so the page runs vue-i18n's runtime-only build
without the message compiler; every language's _meta comes with the page. Empty texts stay out while building.
- The live updates carry the editor's language (api/events?language=), as an EventSource sends no headers.
- web/ needs Node 22.13 or newer: vue-i18n 11.4, Vite 7 and the unplugin ask for it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- vue-i18n 11 with the Composition API: English comes with the page, any other language loads the first time it is
needed from screen_manager/translations, and a text a language lacks shows in English. The build keeps only _meta
and editor of each file (vite.config.ts), never the screens' or the add-on's texts.
- The editor follows the language of the user's Home Assistant profile (<html lang> of Home Assistant's page), else
the browser's; plural forms follow the file's _meta.plural, with the screens' rules.
- Every request tells the add-on the editor's language (X-ESP-Screens-Language), so its own words can follow it.
- The mockup draws the screens' words (Unavailable, Page 2 ›, the date, "5 min ago") in the screens' language, with
their clock and number format.
- Settings → Language & region: the screen language, time format and number format for every screen, with the user's
own profile offered when it differs. The per-screen clock choice goes; the top bar points there. A screen that
still needs the new language says so in its update badge.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tools/i18n.py gets cldr (day and month names, date orders, numbers and the clock from Node's Intl), ha-words (the
words for states from Home Assistant's own translations, English included) and lint (English left in the firmware's
code). The screens write numbers from Home Assistant in the chosen format, and every text font carries the apostrophe.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The firmware takes its texts from screen_manager/translations/<code>.json at build time (components/smart_display:
one table in main.cpp, keys in screen_text_keys.h), and the fonts carry every letter European languages write with.
The app keeps Settings -> Language & region (language, clock, numbers) for every screen, writes LANGUAGE into the
screens' YAML and sends the clock and the number format in the layout message; the per-screen 24-hour clock goes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The add-on ships ESPHome 2026.9.0: it builds an ESP32 with ESP-IDF itself
and keeps a compiler cache. firmware.py gives the CLI its own storage in
/data (ESPHOME_DATA_DIR /data/esphome, which the ESPHome Device Builder no
longer wipes on start; ESP-IDF and ccache in /data/idf, at most 1 GB;
compilers limited to the core count as PlatformIO ran them), removes the
unused PlatformIO once before the first ESP-IDF build, and leaves the new
folders out of backups.
Firmware 0.2.75: assertion_level SILENT in every board's esp32 block, the
core logs from INFO, and clock_parts() in runtime_model.h replaces sscanf.
timer_left() caps a running timer at its duration (the screen's clock runs
up to a second behind Home Assistant's). The packages keep min_version
2026.6.2, the top-level online_image and OTA passwords for new screens, so
an add-on or Device Builder that isn't updated yet keeps building every
screen; CI now builds both boards with the add-on's ESPHome and with
min_version. ESP-IDF's error names stay on, and build_flags stay under
platformio_options, because ESPHome 2026.9's LVGL reads only those.
tools/check.sh: 478 Python tests, 19/19 C++, editor; --firmware PASS with
ESPHome 2026.6.2 (CYD 87.7 %), 2026.8.1 (88.3 %) and 2026.9.0 (86.4 %,
was 91.5 %). Both bench screens OTA'd and watched for half an hour;
ESP Screens as a local add-on on the Yellow built both profiles
(CYD 1,037 s first build, 41 s unchanged). Details:
docs/TEST_RESULTS_0289.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
packages/core.yaml sets wifi: power_save_mode: none. ESPHome merges it
under the screen's own wifi: block, which keeps the network, the password
and the hotspot, and the screen's own YAML still wins on the mode. The
YAML ESP Screens writes has had the line since app 0.2.24; screens with
an older or hand-written YAML ran ESPHome's ESP32 default light, and
their messages from Home Assistant waited 76-117 ms on the bench.
esphome config on the bench screens' Device Builder YAMLs: NONE for both
without a line of their own, LIGHT when the YAML says light. The bench
Guition, flashed from its Device Builder YAML: ping 15 ms average instead
of 117 ms. tools/check.sh: 474 Python tests (a new one keeps the core's
wifi: block to this one key), 19/19 C++, editor; firmware unchanged in
size (CYD 1,678,816 B, 91.5 %; Guition 2,055,280 B), since the checkout
profiles already had the line.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Starting screen: until the first layout, render() keeps the status text
("Connecting to Home Assistant", "Waiting for ESP Screens") and a spinner
as one block in the middle of the page; the first layout deletes both.
The room label stays empty meanwhile, and ROOM_NAME defaults to "".
Camera: the full screen shows a spinner instead of "Loading image". The
screen asks when the camera opens and loads a link as soon as it comes
(camera_load, never under a finger) instead of waiting for the 250 ms
tick twice, and online_image reads 16 KB per loop instead of 4 KB. On the
bench Guition with the EZVIZ camera: cold 5.8 -> 4.55 s, warm 3.4 -> 2.06 s.
Alert picture: the image takes the frame's radius (18, the alert card's);
LVGL's software renderer clips it row by row, without a layer.
One spinner_create() makes the busy sheet's, the starting screen's and the
camera's spinner; theme role CAMERA_TRACK colours the camera's ring. The
README gets a row of camera pictures; docs/CAMERA.md, RELEASING.md and
TEST_RESULTS_0287.md describe the measurements.
Checked with tools/check.sh (473 Python tests, 19/19 C++, editor) and
--firmware: CYD 1,678,816 B (91.5 %, +1,312 B), Guition 2,055,280 B
(25.3 %). Host renders of both boards in both looks; four OTA builds on
the bench Guition Wallbox, which runs this firmware.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A tap on the On/off switch of a wide or full-page tile moved the knob to on
and back, and the light stayed off. Since firmware 0.2.59 control_event drew
the new position first (Tile::optimistic writes it into state) and then asked
key_action for the action, which read that new position: an off light got
light.turn_off, an on light light.turn_on. A tap on the tile itself was never
affected.
tile_controls::press_key chooses the action first and then shows the new
position; control_event uses it. Unit tests cover a light, a fan, a sensor
and a cover key.
Checked with tools/check.sh (471 Python tests, 19/19 C++) and compiled for
both boards: CYD 1,677,504 B (91.4 %, +16 B), Guition 2,054,640 B (25.3 %).
On Max's Guition Wallbox (OTA from this branch) every tap on the switch sent
the right action and Home Assistant's logbook followed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rendered again from firmware 0.2.71 and the editor of 0.2.85 on the demo homes, with the render tooling
brought over to the core and board files of 0.2.84: the tile pages with 0.2.85's colours (a running timer
amber, a docked robot and idle scripts grey, the full-page speaker lit while it plays), the cards with the
icon edges of 0.2.75, and the editor with 48 tiles, the icon-left tiles of 0.2.76, Size and Page in the
tile drawer, Perform action on the curtains as the caption says, and every Screen settings row. The camera
pictures show a free Unsplash photo by Virginia Marinova, credited in the README. The two photos at the top
carry the current firmware on their screen: the page with scenes, scripts, the robot and the table lamp,
and the vacuum card with its cleaning mode.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
README_EXTENDED rendered everything from "Direct control" to "Override YAML" as raw text on
GitHub since 0.2.74: the full-page pictures ended without a blank line. It also gains the
media card, Page buttons, the ··· menu's Identify/Copy/Export/Import, Try it, and where
SCREEN_FIRMWARE_VERSION lives now. EASY_SETUP's settings table drops Show clock and gets
Back to page 1; SETTINGS.md points at web/src/store.ts and packages/core.yaml; GUITION.md
had the GT911 mirrored and cache settings that left the profile on 2026-09-12;
TROUBLESHOOTING still named TILE_COUNT and files deleted in 0.2.50; CALIBRATING put
Calibrate touch in ESP Screens instead of Home Assistant. RELEASING gets the compatibility
notes of 0.2.73 to 0.2.85 and the GitHub release step.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The airco tile stayed orange while the airco was off. The firmware kept its own
short list of what turns grey and gave an off climate Home Assistant's orange,
but Home Assistant has no colour for a climate that is off: its cards fall back
to the inactive grey. Other tiles had the same fault: an off fan, a closed
blind, a docked robot, a player in standby, an idle script, a paused timer and
a camera that isn't streaming all kept their colour.
Tile::active() is now a port of the frontend's stateActive() for the domains a
tile can show, and every tile that is not active is grey. An airco that is off
reads Off, with the room's temperature when the device reports one, as Home
Assistant's tile does. tile_controls::accent() follows stateColorCss(): alarm
binary sensors red, a battery green, orange or red by its charge, a running
script or timer and a streaming camera amber, the weather per condition, the
sun amber and indigo, someone in a zone blue. Scenes, selects, numbers and
sensors keep their own colour per kind. A full-page tile lights up only for
tiles that can be off (Tile::lights_up), and a closed blind's slider keeps its
colour. The top bar's alarm classes are Home Assistant's eleven.
Checked with tools/check.sh (471 Python tests, 19/19 C++ with 58 state cases
and every colour rule) and compiled for both boards: CYD 1,677,488 B (91.4 %,
+576 B), Guition 2,054,640 B (25.3 %).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The two board profiles (5,000 lines each, 87 % the same text) become packages/core.yaml and one file per
board under packages/boards/: the hardware, a table of 84 sizes and font sizes, 18 hooks with the lines of C++
only that board adds inside a shared lambda, and the board's own sections. packages/cyd.yaml and
packages/guition.yaml keep their names and places (a screen installed from ESP Screens builds as before);
home-like-2432s028.yaml and guition-4848s040.yaml become the same entries for a checkout.
The firmware stays 0.2.70: the C++ ESPHome generates for each board, through the checkout profile and
through the GitHub package route, was compared line by line with the old profiles' (docs/TEST_RESULTS_0284.md).
tools/check_packages.py replaces tools/generate_packages.py; tools/profiles.py gives the tests and tools the
three files as one; docs/PROFILES.md says how the files fit together and how a board is added.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A light that offers effects gets a sparkles key at the top right of its colour
card. It opens a page with a row per thing the lamp offers, all from Home
Assistant: the light's effect and the select entities of its device (a WLED's
colour palette, preset and playlist) with what they are set to, and a slider per
number entity of the device (speed, intensity). Names are Home Assistant's own
English entity translations, icons its frontend's; nothing about a brand is
hardcoded beyond a preferred order of translation keys.
A row opens LVGL's roller with every name Home Assistant lists at that moment,
asked for when the picker opens (esphome.screen_options, answered with
op: options, one page of names per message), the effects alphabetically with
Solid on top. The check key at the top right sends the choice as one action;
the back key sends nothing. A slider sends number.set_value when let go. While
an effect runs, the tile names it instead of the brightness.
Firmware 0.2.70: effects_page.h (built when it opens, gone when it closes),
Extra.effect/option_rows/number_rows, options_request, a hidden roller widget
so ESPHome compiles LVGL's roller, five firmware-only glyphs. App 0.2.83:
light_effects.py, the effect attribute, the device rows in the state message,
card_options_loop. Checked on the host with a virtual finger (35/35 on both
boards) and compiled for both ESP32 boards; CYD 1,675,424 B (91.3 %).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The pictures of tile pages still had the Previous and Next words and the "1 / 4" counter from
before 0.2.82. The 26 pictures that show the bar are rendered again from firmware 0.2.69 with the
chevrons and page dots, and pick up the smoother icons of 0.2.75 on the way. The Screen group of
the settings page gained a Page buttons row and now has a second page, so its picture shows the
first page with the pager, and its description says where the page buttons and the rotation went.
Rendered from the host firmware and the real add-on on the same demo homes as the earlier pictures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
GitHub issue #9 asked to drop the Previous/Next bar when Go to page tiles do the navigation and to give the tiles the whole screen. Page buttons is a setting the screen owns (on by default): a row in the Screen group of the settings page, switch.<screen>_page_buttons in Home Assistant, and a row in the editor's Screen settings. Without the bar, and on a screen with one page, place_page moves and sizes the three rows so they reach the bottom margin (Guition 108 -> 122 px, CYD 52 -> 61 px); a card keeps its circle, name and state in its middle and the single analog clock keeps its dial. With page buttons and swiping both off, the editor says where the Go to page tiles lead and which pages that leaves out of reach.
The bar itself: each half of the band under the tiles is one key with an MDI chevron and lights up under a finger, and a dot per page sits between them; the screen's settings page uses the same pager. App 0.2.82, firmware 0.2.69.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
On a CYD the strip at the bottom of a compact card is 8 px high, so a finger landing a little above it pressed the card: the light's card opened, the light toggled, or the page-swipe gesture turned the page without a log line. The strip's click area now covers its card (slider_zone); a press that never moves forwards the card's tap or hold and shows the card pressed; the CYD's page-swipe handler and the Guition's flick handler leave a captured slider alone and log what they do; a slider commit no longer bounces against its own previous send (TouchGuard::accept_within); the CYD's edge band is 20 px because its resistive panel reports up to the bezel. App 0.2.81, firmware 0.2.68.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A fast swipe off the right side of a Guition ended at 93-98 %: the GT911 reports the finger for the last time 33-48 px inside the edge while a wide tile's slider ends 29 px from it, and LVGL keeps the last point it saw. cyd::edge_snap() takes a release inside the band (SLIDER_EDGE_SNAP_PX: Guition 67, CYD 56) as the end that lies in that band, for the tile sliders and the light card's sliders; the log names each snap. App 0.2.80, firmware 0.2.67.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
On the tile (and the strip of a full-page blind) the small slider filled with the open part, while the blind's card
draws the blind hanging from the top: a closed blind looked open on one and closed on the other. Now both fill with
the closed part and dragging right closes; the text still gives the position (open %). The editor's mockup follows.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A test and code-analysis round of 0.2.72 to 0.2.76 (suites at every tag, sanitizers, ESP32 and GitHub builds, host
harnesses on both boards, the editor in a browser, best practice checked against ESPHome, LVGL, Home Assistant and
Vue) found these, fixed here with Max's choices for the product questions:
- Firmware 0.2.65: the second hand of a single analog clock moves again; on a page that is one full on/off tile the
waking push also switches it; on the Guition a flick is never a tap and a finger that slides off a tile doesn't tap
it; Previous/Next react to a firm press; the same Go to page tile may sit on several pages; a layout the screen has
no memory for is refused instead of crashing; 64-bit redraw mask for 48 tiles; Montserrat 14 no longer built.
- App: Save refuses a layout too long for one message or too big for eight pages; an offline screen keeps its
firmware version from the device registry; HA refusals reach the editor; 128 KB requests; changelog out of the
live stream; cached hashed assets; tini as init and a clean stop; changelog parsing without asterisks or crashes.
- Editor: clicking the open screen keeps unsaved edits, edits during a save stay unsaved, a Page row and phone
dragging to other pages, repeated page tiles, limits from the server, HA's error messages, a type-checked build.
- tools/check.sh + GitHub Actions + a CYD flash budget; docs match 48 tiles and the new editor.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Docs only, no version change: the add-on update and the OTA to firmware 0.2.64,
the media card with a real Sonos cover, and an alert with a camera over it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A download shares ESPHome's loop with touch and drawing. An alert already
closes every card, and the card's cover with it; a media tile over the whole
page stays under the alert. Its cover now waits while the alert's picture is
announced, loading or due for a retry, and an alert picture that arrives while
a cover is on its way (or a dropped cover's download is still being closed)
starts right after it. The host harness checks all three cases on the Guition.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The card behind a media tile shows the album cover, the title, the artist and
the album, a progress bar that runs while the track plays with the times at its
ends, round keys for previous, play or pause and next, and a volume row with a
mute key, a wide slider and the percentage. Keys the player lacks stay faded,
an off player shows one power key, a stream without a length shows no bar, and
the CYD shows the player's icon in the cover's place. A media tile over the
whole page is the same card under the tile's head. media_card.h holds the
geometry, tests/test_media_card.cpp checks every shape.
The cover comes the way camera images do: the screen asks with the size and
the colour behind the corners, the app fetches the picture from Home Assistant,
cuts it square, rounds the corners over that colour and serves it on port 8098;
the Guition loads it once and asks again only when the picture's mark in the
state changes. Firmware 0.2.64.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A thank-you line and the yellow Buy me a coffee button under both titles,
drawn by Buy Me a Coffee's own button image so it shows on GitHub and in the
Home Assistant add-on store, where a script tag does not run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both boards draw a normal tile with the icon on the left and the name and value beside it;
the Vue editor stacked icon, name and value in one column. On a CYD mockup a tile is 47 px
high and that stack needed 58 px (63 for a watch card), so the value line was cut in half and
a big value hid the name. A single tile is now a row (.head: icon + .tx), a watch card puts
the name row on top and the number under it, and the small slider runs underneath; on a CYD
a tile with a slider uses the screen's 4 px padding and 8 px slider. The Guition mockup
follows the screen the same way.
App 0.2.76, firmware stays 0.2.63. Test results in docs/TEST_RESULTS_0276.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Material Design Icons fonts of both boards had no bpp, so ESPHome rendered them at
its default of one bit per pixel while the text fonts already had four. On the CYD, at
18 and 28 px, a thin stroke such as the ring of mdi:power came out lumpy and uneven, and
every other icon had a ragged edge. All four icon fonts of both profiles now say bpp: 4;
the icon generator and its test keep that line. Costs the CYD 95 KB of flash (90 % of
the update slot in use on top of 0.2.74), no RAM.
App 0.2.75, firmware 0.2.63. Test results in docs/TEST_RESULTS_0275.md.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Max asked for many more tiles, a tile that takes the whole page as a light
switch you push without looking, and tiles that go to a page. The limit is
one tile per slot of the eight pages, 48, and the firmware keeps its tile
list on the heap sized to the layout (PSRAM on a Guition). A tile's size can
be `full`: one big button that lights up in the state colour while on, or the
double-width card's head with its small slider, direct controls or graph at
the bottom and the value the double-width card had no room for in between;
the clock, the forecast (with an hours strip) and the sun path fill the page.
Navigation tiles are the built-in entities screen.page_1 to screen.page_8:
an arrow or a chosen icon, the name and "Page n", a tap opens that page.
The app sends the three only to firmware 0.2.62+, the editor (the Vue port of
this round by the editor session, 00c9a4e) offers Normal / Double-width /
Full page and "Goes to page", and Claude in Home Assistant gets size: full and
the page tiles. Built-in tiles now send a chosen icon as its codepoint.
Found and fixed by the new host harness before release: the one-second tick
moved the analog clock's second hand (part 18) on a full forecast's hour label
during a page switch and corrupted it. Firmware 0.2.62; both C++ and Python
suites, host harness 37/37 on both boards, hardware pass on both bench boards
in docs/TEST_RESULTS_0274.md.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>