Added
-
Buttons: a press can name its physical button (
button_key) beside its action, and the device's button map binds that key first, so the four buttons on an Inky Impression can each show their own dashboard ({"a": "page:Calendar", …, "d": "page:Family"}) with tesserae-device-pi 0.9.3. Maps keyed byleft/right/refreshwork as before. -
Raspberry Pi: one
pi_clientkind for the unified tesserae-device-pi client. A Spectra 6 Impression gets the packed .bin, every other Inky panel the PNG, from the panel the Pi reports when it pairs; it stays awake by default. -
Device batteries: each chart has a 6h range too, and the second switch at the top of the page is gone.
-
Device batteries: a gear next to each frame's name opens its settings.
-
Lineups: a block that runs all day and carries on into tomorrow no longer counts down to an end; it says when quiet hours pause it.
-
Device page: no Next picture when the next step is the dashboard already showing, since its next picture isn't drawn yet.
-
Visionect frames: update firmware from the Firmware page, more frame settings and connection health on the device page, and a sleeping frame keeps its old picture with newer ones Queued.
-
Visionect firmware: the builds tested on a frame are checked against a known SHA-256 when downloaded or read from the cache, and a list entry whose checksum isn't plain hex is skipped.
-
Device page: summary tiles and the next picture in the left column, a Lineups link, accent group titles, Troubleshooting's buttons at its foot, and devices listed under Settings in the sidebar.
-
GPT Image from words alone (
gpt_image_t2i) for a widget that puts a
picture's whole layout in its prompt, such as a card with its own type,
where a reference picture would pull the composition towards itself. It
takes the same quality and size as the edit model, and goes straight to
OpenAI when only an OpenAI key is set. -
Device page: a summary rail beside Display, Schedule, Controls, Power, Troubleshooting and Remove sections, with compact field rows and Advanced disclosures.
-
Collapsible sections on the Server, System, About, Stats and other admin pages, remembered per page.
-
Sub-section titles are one heading style on every page, with a hairline above.
-
Paper's section title marker follows the theme's accent colour instead of a fixed red.
-
The Lineups page is centred like the other pages instead of sitting left.
-
Device batteries: the chart marks each wake-up with a dot and why it happened.
-
Device batteries: a Wakes / day tile, and time with no readings is shaded instead of drawn as a line.
-
Device batteries: each chart has its own 12h / 24h / 7d / 30d range and drag-to-zoom.
-
Device batteries: a readable dark tooltip, and Clear history is a corner link.
-
Device batteries: Visionect frames get drain and Reaches predictions too, and a short chart range predicts from at least a week of readings.
-
Device page: six collapsible sections instead of eleven, and the top tiles jump to their section.
-
Lineups: next update times are right for a display asleep in quiet hours, such as no more ~6:31 PM after a 9 AM wake.
-
Lineups: a display's top thumbnail always shows what is on the frame; what waits for it, from a lineup or a manual send, is a small tagged thumbnail on its corner.
-
Lineups: pushing a step from a lineup marks that lineup live, even one that does not run today; sending something by hand marks none.
-
Lineups: zoom a thumbnail into a larger view with Push now, and use previous and next arrows on a step to pick a photo or draw another picture before pushing it.
-
Quiet hours: a lineup step or interval skipped overnight now lands as quiet hours end, and a sleeping display wakes for it.
-
A widget that paints with GPT Image can ask for the quality (low, medium
or high) and the exact size of the picture, so a poster can be painted
sharp at its panel's own resolution (1200x1600 for a 13.3" or OpenPaper L
panel) instead of 768x1024 at medium. fal.ai takes the size as asked,
rounded to multiples of 16; a picture sent straight to OpenAI gets the
quality at OpenAI's nearest size. The daily painting budget is charged the
high-quality price, about 20 US cents, when high is asked for. Widgets that
ask for neither are unchanged. -
Paper's phone shell. The four places you go most (Send, Dashboards,
Lineups, History) sit in a tab bar along the bottom and More lifts the
rest up as a sheet, in place of the hamburger drawer. Once a page's title
scrolls under the top bar, the bar turns into the page bar: the title
where the brand name was, and the notices that scrolled away (restart
required, an update, new devices, a low battery) as chips. Those notices
sit in a band at the top of the page rather than the drawer. The battery
pill leaves the top bar; the charge list lives in the sheet. Desktop and
the classic design are unchanged. -
A
selectcell option's choice can listaliases, the values an earlier
manifest used for it. A cell that still holds one shows the choice it
became rather than "Unavailable: ",show_ifreads it the same way,
and the next save writes the current value, so a widget can rename a value
without stranding every cell set up before the rename. -
Lineups redesign, part 1 of 3: a view model for the Lineups page and the
endpoints the new page edits through.GET /decks/view.jsondescribes every
display (what is on screen, what is queued and when a sleeping frame picks it
up, the real wake time, quiet hours, a day bar per lineup, each step with a
small thumbnail).POST /decks/<id>/window,/daysand/editschange a
lineup's time window and weekdays in one locked write, refuse a change that
makes two lineups on one display overlap (409) and warn when a run falls in
quiet hours.POST /decks/<id>/push-steppushes one step now; with the
step'scomposition_digestit resends the saved picture the tile shows
instead of rendering again. Step thumbnails are small immutable WebP
snapshots keyed by the pushed composition, looked up in one query per page;
a dashboard never pushed is rendered once in the background. Lineup steps
whose dashboard was deleted are flagged (missing) and the lineup editor
lists them so they can be removed. Additive: the current Lineups page is
unchanged apart from using the same small thumbnails. -
Lineups redesign, part 3 of 3: a push to a sleeping frame says so. Every
push surface (Send, Push now on the Dashboards list and device page, the
canvas editor's Send, lineup steps and Push, History resend, calibration
and test patterns, the setup wizard) adds a notice per waiting display,
such as "Hallway Frame is asleep: it will pick this up at its next check-in
(~3:48 PM)", or names quiet hours, an overdue check-in or a lost
connection. The webhook, REST, canvas and MCP push responses gain an
additivedeliverylist with the same words andexpected_at, and a
Lineups step reads "Queued · picks up ~3:48 PM". Pickup times use one
formatter everywhere. -
A device with its own menu can list the dashboards bound to it and show
any of them.frames.jsonnow lists, after "Current dashboard", one item per
dashboard bound to the device that is not archived, with an ASCII title.
Picking one, or adding?page=<id>to/frameor/frame.bmp, shows that
dashboard on the device without moving its rotation or lineup position, the
same as a button mapped topage:<id>. A dashboard not bound to the device is
refused. -
The Dashboards list has a By display | All switch in its toolbar. All shows
every dashboard once, A to Z, with a chip for each display it is on (or
Unbound), instead of repeating it under each display; search, the Active and
Archived tabs and bulk actions work as before. By display stays the default.
?view=alllinks straight to it, and the choice is remembered. -
Widgets and data plugins, bundled ones included, can be disabled from the
Widgets page, and enabled again, without a restart. A disabled plugin leaves
the Admin pages menu and the add-a-widget pickers, its admin page and routes
answer 404 with a friendly page, and it moves to a Disabled section with an
Enable button. Dashboards that use it keep their settings and show a
"Widget X is disabled" placeholder. Disabling something in use asks first and
names the dashboards. Fonts, themes, renderers and devices cannot be
disabled, nor can Home Assistant, Weather and Calendar, which the app itself
relies on. Bundled plugins now carry a Bundled tag, since they can be
disabled but not removed. -
Every clock time in the app follows the language and region setting
(Settings › Server › App › Locale, the host's when unset) through one
formatter:3:48 PMin a 12-hour locale,15:48in a 24-hour one. That
covers the Settings › Devices "Next update" tile and detail, the device page
and smart-sync line, the Lineups page (next advance, next push, fires, now),
History and Events timestamps, rooms "busy until", schedule times, the
battery chart axis and tooltip, the timed-lineup wizard, Visionect and relay
notes, the generative-AI reset time and the Bluetooth/Wi-Fi reading stamps.
Home Assistant sensors, the REST/companion API, JSON fields, ISO stamps,
logs, saved settings and time inputs keep the 24-hour form. With no locale
set and none from the host (the usual container), times stay 24-hour. -
Visionect setup has an app-wide Server port setting (1024–65535), with
environment and optional Home Assistant App option overrides, immediate listener
rebinding, and instructions that show the configured port. Default: 11113.
A port below 1024 or an unreadableTESSERAE_VISIONECT_PORTnow turns the
listener off with an error on the setup card, where it used to fall back to
11113 silently. The Home Assistant App does not publish the port yet.
Changed
- Raspberry Pi: the docs, README and issue templates point to the unified tesserae-device-pi client; the pi-png and pi-bin repos are archived. The Pi cloud-init (
scripts/pi-client-cloud-init.yaml) installs it from PyPI with its update rollback guard, needs onlyserver_urlin meta-data (device_idoptional, no panel model), and reboots itself once so SPI and I²C take effect. - Lineups redesign, part 2 of 3: the Lineups page is rebuilt on the view model
from part 1. Each display gets a header that says what is on screen (the
picture itself), what is queued and when it lands, and when the frame next
wakes. Lineups show as Cards (a strip of each lineup's dashboards with Push
now on every step) or as a Timeline (one track per display across the day,
with repeated updates of one dashboard collapsed into one thumbnail and a
"x N updates" badge). Each lineup has a day bar with ticks for every
advance, instant tooltips, an "Update in N min" flag on the next one, start
labels under busy bars and a hatched strip where quiet hours cut into an
all-day block. Today | All switches between today's rows and every row; Fold
all, a settings gear by each display name, a span icon for the Full cycle
chip, drag and keyboard editing of windows and days, and a deleted
dashboard shown as a flagged step with Remove (also in the lineup editor).
Times follow the app locale's 12/24-hour style. Settings > Devices shows one
"Now" line per display (the live lineup and until when, or the next one)
instead of every lineup name. Each row's menu
holds Edit, Step back (by-hand lineups), Push now, Enable or Disable and
Delete; rows follow the order they next play in, and the Display order and
Z to A sorts are gone. - The device page's Lineups section lists the display's lineups (when, which
days, how many dashboards, Live or when it plays next), each linking to it
on the Lineups page, above any schedules. It used to list schedules only,
so a display run by lineups read "Nothing scheduled yet". - A lineup, schedule or rotation that names a dashboard bound only to another
display is now refused unless you agree to bind it. Before, the save went
through and every run failed with "dashboard targets none of the requested
device(s)". The lineup editor, setup wizard and schedule form show a Bind to
this display prompt (move it, or keep its other display too), and the save
binds in the same request, before the record is written. Without consent
every write path refuses: forms flash why, JSON and MCP answer 409
pages_not_boundwith the dashboards and abind_pagesfix, and the
companion API returns 400 (it has no consent field yet). Lineups already in
that state show a flagged "Not on this display" step with a one-click Bind
button. - Display-accurate previews: thumbnails show what the display can actually
show. A black-and-white screen previews in black and white instead of full
color, a grey screen in greyscale, and a color screen is unchanged. A
black-and-white display with a dithering mode previews as smooth grey, since
dithering averages out at thumbnail size; only dithernoneuses the hard
black and white threshold. Covers the Devices list and device page,
Dashboards list, History, Events, Send, the dashboard editor preview and the
lineup editor. Dashboards with no display, or shared across displays that
differ, stay in color. Previews only; what is sent to the panel is
unchanged. Seedocs/thumbnail-color.md.
Fixed
-
Home Assistant, Entities: a door, window or motion sensor reads Closed / Open / Clear the way Home Assistant words it, instead of Off / On.
-
Visionect frames: a saved Wi-Fi password now reaches the frame, is masked on the device page instead of sent back to the browser, and is hidden in the frame log if the frame echoes it.
-
History: 12-hour times no longer overlap the thumbnail.
-
A link now always sits left of a button, and the primary action is rightmost,
across the admin pages, dialogs and banners. -
Device batteries: a boot's reset reason no longer marks later status
changes as restarts. -
Lineups and Home Assistant's next update follow a wake interval the page
on the display declares. -
Device page: saving after an edit in quiet hours, the button map or battery
offsets lands back on that group; underscan keeps its old maximum. -
Widgets: Loader errors can't be collapsed out of sight.
-
A lineup step that lands a pre-rendered frame now shows up everywhere the
newest push is read. Home Assistant's Next and Previous, a physical button,
a tap, the companion API, the Lineups page's step and a home return could
swap the cached frame onto the display without writing a History row, so
the Lineups thumbnails, History and "from Home Assistant" labels kept the
frame from before. The promote now writes that row itself (with its
source), the timer advance no longer writes a second one, and a button
press's row links to it. -
Settings > Devices and the device page no longer show a stale or mislabelled
preview for a pull (REST) frame after a push. The page used to keep the old
thumbnail until reloaded, and after a reload showed the newest image under
"Showing" with the old image's fetch time. The thumbnail is now what the
frame last fetched, the image waiting for its next check-in sits under it
tagged "Queued", and a line says when it was queued and when the frame picks
it up ("Delivering now" for a connected frame). Thumbnail URLs carry the
render digest, and the open page follows pushes and fetches on its own.
Frames that fetch through another route (CrossPoint readers on
/frame.bmp, deck and album clients) show the latest render as before. -
The Mirror page (
/mirror/<device>) loads its image behind a URL prefix
such as Home Assistant's sidebar panel. The image URL was root-absolute, so
under ingress it never loaded and the page stayed black; it now carries the
prefix. The device name on the page is HTML-escaped too. -
Picture Gallery: replacing an image with a new file of the same name now
shows the new thumbnail. Thumbnail URLs carry a?v=<mtime>-<size>version;
a link without it is revalidated rather than cached for a year. -
Deleting a dashboard that a lineup uses now asks first, naming the lineups
("X is in the lineups A, B. Delete it and remove it from them?"), and once
approved it also takes the dashboard out of them: its step, any link to it,
the home, entry and fallback pointers, and a display's saved position on it.
A lineup left with no other dashboard is deleted, and the confirm names it
beforehand.
It applies to Delete and bulk delete on the Dashboards list (one confirm for
the whole selection; without JavaScript a confirm page does the same). The
canvas editor's delete route and the MCPDELETE /pages/<id>answer 409
page_in_lineupswith the lineup names (andwould_delete_lineups) until repeated with
remove_from_lineups=true; the canvas editor's own Delete asks the same
question. MCP bridge 0.19.0 addsremove_from_lineupsto
delete_canvas_page. Before, the lineup kept a step pointing at a dashboard
that no longer existed. -
The image lightbox (gallery, history, events, artwork) no longer scrolls the
page behind it: page scroll is locked while it is open (scrollbar width
compensated, no layout shift, position kept on close) and released on every
close path. Images now open fit to the screen; click, tap or Enter/Space
toggles a full-size view that pans inside the overlay, and a second click
returns to fit. Backdrop click closes in the fit view only; Escape always
closes.
Security
- certifi 2026.10.23 in the lockfile, for PYSEC-2026-4320 in 2026.7.22.
[0.496.1], 2026-10-31
Fixed
- Dashboards: New dashboard no longer hides the create form when it is
already open (an empty list, or?create=1); it opens the form and
focuses the name. The Grid / Freeform picker now says what each kind is. - A device id typed with spaces ("Kitchen Panel") becomes
kitchen_panel
instead of being rejected, in the setup wizard and Settings › Devices. - Setup wizard: the device step links to the web flasher, the MQTT choice
notes that battery ESP32 boards should use REST, and "Add a device by
hand" starts collapsed with a note that a flashed panel registers itself.
The empty Devices list links to the web flasher too.
[0.496.0], 2026-10-30
Added
- Gemini as a third provider for AI pictures. Settings › Generative AI takes
a Gemini API key from Google AI Studio, with a Check that lists the
models the key can use without painting anything. With it, Nano Banana
and Nano Banana Pro are painted straight by Google instead of being
relayed by fal.ai; an install with only a Gemini key can now paint, every
picture going to Nano Banana. Charges and the ledger show them as "Nano
Banana (Google)" and "Nano Banana Pro (Google)" at Google's price, inside
the same daily and monthly ceilings. Widgets need no change.
[0.495.0], 2026-10-30
Added
- A button or touch event with
button_event_id=0resets the device's
stored event id: it is never dropped as a repeat of the last id, so a
client that counts from 0 on every boot (the CircuitPython client) doesn't
lose the first press after a restart. A quick resend of 0 is still caught
by the time-window debounce. ESP32 firmware starts its ids at a random
point and is unaffected.
[0.494.1], 2026-10-30
Fixed
- Home Assistant notifications draw umlauts, accented letters, the euro sign
and Chinese or Japanese text instead of empty boxes. The notice card used
Pillow's built-in font, which only has ASCII; it now uses the bundled Inter,
falling back to a system font that has every character of the message.
Per #420.
[0.494.0], 2026-10-30
Added
- Home Assistant gets a Next update sensor per display, the same time the
Devices and Lineups pages show for when the next change reaches the frame
(push time weighed against a live connection or the frame's next check-in,
held by quiet hours or a pause). Its attributes carry thereason
(connected,check-in,held by quiet hours, ...) and the next check-in.
Changed
- Home Assistant's Next wake sensor no longer shows the raw telemetry
prediction, which read 24 minutes for a Visionect sign that was connected and
took pushes within seconds. It now means the next expected check-in of a
sleeping frame (from the cycle it has actually been seen to keep), and reads
unknown while the frame is connected or stays awake. A stale time left
retained from an earlier session is cleared.
Fixed
- The Devices page, the device page and the Lineups page now agree on when a
frame actually gets its next update. They used to show two numbers that
contradicted each other ("Next wake in 24 min" on one, "next advance in 1
min" on the other, when the push was in fact taken within seconds). The
Next update is the server's next push (lineup step, schedule, timer
advance, page refresh) weighed against when the frame can take it: a frame
holding a live connection or staying awake gets it right away, a sleeping
frame gets it at its first check-in at or after the push. Quiet hours and
pauses show as "held until 07:00". The Devices tile and Smart sync card read
e.g. "Next update ~18:24 (frame sleeps; next check-in ~18:24)", and every
lineup card with a next push says the same under "next advance". - Visionect signs: the check-in estimate follows the gap actually observed
between the sign's statuses instead of the configured interval (a 32" set to
30 min reports about every 15), so the prediction earns trust. A sign with a
live connection reads "connected" instead of counting down a wake. - The status dot on the Devices list, the device page header, the Dashboards
list's per-display dot and the dashboards' device chips follows a sleeping
device's own cycle. It went red 5 minutes after a heartbeat, so a battery
e-paper frame that wakes every 30 minutes read as stale for most of each
cycle. When the device's sleep cycle is known the dot stays green until a
little past one cycle, amber for a couple more, and red after that (and the
"online", "quiet" and "stale" wording beside it agrees). The Companion API's
freshnessflag and Home Assistant's online state use the same observed
cycle instead of only the configured interval. Devices with no known cycle,
and always-on ones, keep the old bands. - Thumbnails keep the screen's shape instead of cropping a portrait picture to
its middle. The latest-render thumbnail on the Devices list and the device
page was a fixed 4:3 box; it now follows the panel's aspect ratio (taller for
portrait, wider for landscape) and the whole picture fits. - The same goes for the Dashboards list (the small thumbnail on each row and
the hover preview, as well as the opened row's preview) and the Lineups page
(the dashboard cards, including "on the panel", and the page cards in the
lineup editor): a portrait dashboard is a tall card, a landscape one stays
wide, in the classic and Paper designs and at phone widths. The History
thumbnails fit rather than crop too. - A build that keeps the same version number (a branch, or a fork's add-on) no
longer serves stale stylesheets and scripts. The static URLs carried only
the package version and were cached for a year, so a changed CSS file never
reached a browser that had loaded the old one. The cache-busting version now
also carries a short digest of the static files, which changes whenever any
of them does. - The Timing block on the device page reads more clearly: Start and End sit
under the days the window applies on, the switches explain themselves, and
the override toggle says what On and Off mean.
[0.493.1], 2026-10-29
Fixed
- The widget-update check no longer lets a refresh started before a reset
mark the catalog as freshly fetched afterwards. Only the test suite reset
it, where the coverage job could see the next check skip its fetch.
[0.493.0], 2026-10-29
Added
- OpenAI as a second provider for AI pictures. Settings › Generative AI takes
an OpenAI key next to the fal.ai one, with a Check that lists the account's
models without painting anything. With it, GPT Image is painted straight
by OpenAI instead of being relayed by fal.ai; an install with only an
OpenAI key can now paint, every picture going to GPT Image. Charges and
the ledger show it as "GPT Image 2 (OpenAI)" at OpenAI's price, inside the
same daily and monthly ceilings. Widgets need no change.
[0.490.0], 2026-10-29
Added
- Update all on Browse catalog: one click updates every installed catalog
widget that the index lists a newer release for, skipping any whose new
release needs a newer Tesserae. One failure doesn't stop the rest; the
outcome names what was updated and what wasn't. Restart stays a separate
click, as for a single install. - The sidebar's Widgets entry carries a count of installed catalog widgets
with an update waiting, the way a phone's store icon does. It answers from
the catalog index the server already caches and refreshes that index in the
background at most once an hour, never on the request. Needs Online
features on. Settings → Widgets → Catalog updates switches the count off;
Browse catalog still marks each widget.
[0.489.0], 2026-10-29
Added
-
Each display gets a Pause number in Home Assistant (minutes). While it
runs, nothing automated reaches that display: lineup steps, daily and
interval lineups, rotations, schedules, page refreshes and returns to a
lineup's home page. Manual sends and physical buttons still go through.
When the pause ends, or is set to 0, the display gets what it missed
(waiting for quiet hours to end if need be), and is left alone if
nothing was missed. The other displays aren't repainted. Hold keeps its
existing meaning. -
Hardware kind
lilygo_t5s3_profor the LILYGO T5 E-Paper S3 Pro and Pro Lite
(H752-01), a 4.7" 960x540 16-level greyscale panel on an ESP32-S3 with a
TPS65185 power path. The same glass and 259200-byte esp32_gray_bin frame as
the PaperS3, used landscape; firmware is thelilygo-t5s3-probuild in
tesserae-device-firmware, confirmed on real hardware by its contributor on a
Pro Lite. Not auto-selected from a relay self-report, since on the wire it
matches the PaperS3. -
Visionect e-paper signs, driven directly with no Visionect Software Suite.
Tesserae listens on TCP 11113, where the sign dials in, and pushes each
frame as 16 greys over the sign's own protocol through thepyvisionect
library.- Adding a sign: Add device has a new Visionect tab. Leave the UUID blank
and the first unknown sign that connects within the hour is bound. Any
other sign shows up in the discovered devices banner, ready to register. - Models: the catalog has the Place & Play 32" and 13", Joan 6 and 13,
and the 9.7" boards. Each sign reports its own screen size on connect, and
the device adopts it. The Place & Play 32" declares"buttons": false, so
its device page has no Controls section and Home Assistant gets no Input
entity for it. - Delivery: one frame is in flight at a time and is tracked by the sign's
ACK. Refusals are retried. Quiet hours with sleep-through send the sign to
deep sleep only after the latest frame has landed. - Status: the device page shows whether Tesserae is listening and what
the sign is doing, with Report in, Full refresh and Reboot buttons. Battery,
signal, temperature and firmware reach the device card and Home Assistant.
The sign's check-in interval and sleep mode are shown but not changed. - Next wake: the device page estimates when the sign will check in next:
its last check-in plus its own check-in interval, or the end of a
quiet-hours sleep Tesserae just ordered. Sleep cycle, last offset and the
smart sync confidence follow from real check-ins. A sign set to stay awake
has no estimate. - Port and address: the listener runs while a Visionect device exists. It
binds the web server's address (TESSERAE_BIND_HOST), and
TESSERAE_VISIONECT_PORTmoves it off 11113. The protocol has no
authentication, so keep the port on your LAN.
See the Visionect quickstart.
- Adding a sign: Add device has a new Visionect tab. Leave the UUID blank
-
Device kinds can register hooks in
app.device_service(a check before a
device's config is stored, a callback after it is stored, when a device is
added from a discovery or removed), so the settings routes and the Home
Assistant config setter apply a kind's own rules without naming the kind. -
Adding a device now checks the config values the form supplies before the
device is created, instead of dropping invalid ones afterwards.
Fixed
- The Paper design's Settings dropdown marks the page you are on again when
Tesserae runs behind a URL prefix, such as Home Assistant's sidebar panel or
a reverse proxy's mount point. The links carried the prefix and the current
path didn't, so nothing matched and no item was highlighted.
[0.488.1], 2026-10-28
Added
- Companion supports conditional Dashboard fields, sliders with exact numeric
input, place search and coordinate entry, and native color editing. The
optional native-controls header preserves older clients' read-only behavior;
hidden saved values, opaque field scopes and revision checks remain intact.
[0.488.0], 2026-10-28
Added
- Each display gets Next and Previous buttons in Home Assistant. They
step whatever that display is showing right now, on that display only,
the way its own left and right buttons do. On a display with several
lineups bound (one per time slot, say), they step the lineup whose page
is on the glass. A one-page lineup, or a display that follows nothing,
is left alone. They work while automation is paused and during quiet
hours, like other manual sends.
Changed
- Home Assistant commands that render run one at a time on a background
worker, in the order they arrived, instead of on the MQTT connection's
own thread, so a slow render no longer stalls other commands and
heartbeats. Each waits at most a minute for the renderer and reports a
busy renderer, a failed push or a full queue on Last error. Saving
Settings no longer waits for a command in progress. - A newer push for a display only cancels an older one still waiting
while the newer one is itself still waiting or running; one that gave
up on its wait no longer cancels anything. - Button presses, Home Assistant's Next / Previous, lineup timer steps,
home returns and daily or interval lineup fires take turns per display,
so two of them can't leave the recorded position naming a page other
than the one on the glass.
Fixed
- A lineup's position is only recorded once its page has reached the
display. A button press, a timer step or a return to the home page that
failed, or that a newer send replaced, no longer leaves the position
naming a page the display never showed. Daily and interval lineup fires
now record their position too, so Next and Previous continue from the
page they put up.
[0.487.1], 2026-10-28
Fixed
- Interface text no longer changes typeface and apparent size after a page
appears. Common UI fonts are preloaded, bundled fonts stay cached between
pages, and a late font keeps the initial fallback for that page view.
UI font aliases keep this policy independent of dashboard content fonts. - Paper pages start at their final position instead of sliding all text
through fractional pixels on each navigation. - Icons reserve their final width while their font loads, keeping nearby
navigation, button and display labels in place.
[0.487.0], 2026-10-28
Added
- Two switches next to the Home Assistant discovery toggle, both on by
default: Publish lineup controls to Home Assistant (each lineup's
switch, Push now, Next and Previous, and the displays' Lineup picker)
and Publish a button per dashboard to Home Assistant. Turning one
off removes those entities from HA when you save; the Active dashboard
and per-display Dashboard pickers stay.
Fixed
- Removing a Home Assistant display or lineup no longer removes the
entities of another whose id starts the same way (kitchenand
kitchen_2). Each entity is torn down by the display or lineup that
published it. - Home Assistant discovery that starts at boot now has the physical-button
service, so each display's Input event entity appears straight away
instead of after the next settings save.
Fixed
- Home Assistant's per-display In quiet hours sensor follows the server's
time zone. It read the time zone before it was set at startup, so on the
Docker image and the HA add-on (both UTC) it switched on and off by the
UTC clock, hours away from the configured window. Quiet hours themselves
were not affected.
Changed
- Form posts that arrive through a reverse proxy are now refused unless the
proxy forwards the scheme and host (X-Forwarded-Protoand
X-Forwarded-Host) or Settings → Server → Public URL is set to the address
you use. If you reach Tesserae through a proxy that doesn't forward them,
saving a form will fail with a 403 until you set a Public URL or forward the
headers. The 403 says so.
Fixed
- Browser requests that change something are refused when they come from
another site (CSRF), whether or not they carry the session cookie. That
covers admin, setup, login and token-free loopback MCP calls. Devices,
webhooks and MCP clients with a valid token are unaffected, and a
configured Public URL keeps direct LAN form posts working. - Forwarded origins, prefixes and Home Assistant ingress headers are only
trusted from trusted proxies.TESSERAE_TRUSTED_PROXIESadds proxy CIDRs
outside the private defaults (public IPv6, CGNAT, VPN). - Creating a dashboard or canvas requires POST. The old GET URLs open the
form instead of creating one. - Malformed or non-ASCII MCP credentials are refused instead of raising a
server error.
Added
-
Companion can report phone-confirmed Bluetooth mode, battery percentage and
firmware version. Settings shows the saved observations with their collection
times; newer Wi-Fi heartbeat readings take precedence. Reports persist across
restarts without changing device reachability or delivery rules. -
Widgets are told which placement they are rendering for:
ctx.placement_id
names the page, the cell and the device. A widget that declares
render.per_placementin its manifest also gets its own fetch for each
placement instead of one shared by identical cells, so a rotation keeps a
separate turn on a big panel and a small one. Other widgets are unchanged. -
The push pipeline can now be asked whether a render is under way for a
device (PushManager.render_in_progress). It is true from the moment a
push for the device starts, through the settle wait, the queue for the
render lock, rendering and publishing, and a repaint after a renderer
change, and false once the new render is the device's latest. Code that
must not act on the old picture while a new one is on its way, such as
putting a device to sleep for quiet hours, can wait on it instead of
guessing. -
A device kind or hardware entry can say
"buttons": falsewhen it has no
physical buttons. Its device page then has no button map (and no Controls
section at all if it has no touch panel either), and Home Assistant gets no
Input event entity for it, instead of an entity that can never fire. Kinds
that don't say anything are treated as before.
Fixed
-
Two things saving the same device at once can no longer lose a change. A
device's file is read, patched and written back whole by many paths (a
rename, a transport switch, a token regeneration, quiet hours, a panel size
the device reports, a firmware-driven renderer switch). They now take turns,
and a save that read a device's file just before the device was removed can
no longer write it back. -
Saving a device's button map, update delivery or synchronized wake, turning
automatic firmware updates on or off, and removing a device now change only
that one setting. Each used to copy the device's whole settings entry and
write it back, which could undo something saved on the same device a moment
earlier by another request.
[0.486.1], 2026-10-28
Fixed
- Changing a dashboard's content-update cadence or device wake-up interval
from the Dashboards list stays on the list. The save used to redirect into
that dashboard's editor. (reported on tesserae-widget-underway#2)
[0.486.0], 2026-10-28
Added
- Hardware kind
m5stack_paperdiyfor the M5Stack PaperDIY (Complete Kit and
Assembly Kit), a 4.7" 960x540 16-level greyscale panel on an ESP32-S3 with
the M5PM1 PMIC and a 140 mAh cell. The same glass and 259200-byte
esp32_gray_bin frame as the PaperS3, used landscape; firmware is the
m5stack-paperdiybuild in tesserae-device-firmware, confirmed on real
hardware by its contributor including both keys. Not auto-selected from a
relay self-report, since on the wire it matches the PaperS3.
[0.485.0], 2026-10-28
Changed
- Paper design: Settings is a dropdown in the main sidebar, like Widgets,
instead of a second sidebar beside it. The dropdown holding the current
page opens with that page highlighted. Phones keep the tab strip, and
the classic design is unchanged.
[0.484.2], 2026-10-28
Fixed
- Quiet hours end at the right time across daylight-saving changes. A
window ending inside the skipped hour, or in the hour that repeats, used
to wake the device early or late. Devices also stay quiet through the
whole configured end minute, so device sleep and push gating agree.
[0.484.1], 2026-10-28
Fixed
- Outbound fetches that refuse private addresses (picture imports, page
assets, fonts, REST widgets, the companion and relay checks) check the
host the socket will actually reach. Encoded hosts like127%2e0%2e0%2e1,
numeric spellings like2130706433, IPv4 inside IPv6 and a DNS answer
that changes between the check and the connection no longer get through.
[0.484.0], 2026-10-28
Added
- Hardware kind
huessen_epf1301for the Huessen EPF1301, a 13.3" Spectra 6
picture frame (EL133UF1, 1200x1600) on an ESP32-S3. It takes the same
960000-byte esp32_bin frame as the reTerminal E1004; firmware is the
huessen-epf1301build in tesserae-device-firmware, confirmed on real
hardware by its contributor. Not auto-selected from a relay self-report,
since it is indistinguishable on the wire from the other 13.3" Spectra 6
kinds.
Fixed
- Smart sync predicts a silent device's next wake from the
next_poll_sthe
/statusresponse handed out, not from the configured sleep interval. A
device that published neithersleep_untilnornext_sleep_sand slept
for a lineup-driven 5 minutenext_poll_sunder a 30 minute interval
arrived "25 minutes early" every wake and never earned trust. (#387) - The calibration preview matches a palette profile to the panel's colour
slots by colour, as the renderer has since #298. Oninky_7colourpanels
(Inkplate 6COLOR, Inky Impression) the Palette Swatches pattern and the
live palette editor preview painted the calibrated yellow in the green
slot and the blue in the red one. (#386)
[0.483.2], 2026-10-27
Changed
- The paperlesspaper OpenPaper L is confirmed on real hardware: catalog notes,
tested data and the compatibility page say so.
[0.483.0], 2026-10-27
Added
- Hardware kind
waveshare_rlcd_42for Waveshare's ESP32-S3-RLCD-4.2, a 4.2"
400x300 reflective LCD (Sitronix ST7305). Not e-paper: it repaints instantly
and holds its image only while powered. Inheritsesp32_bw_clientand the
15000-byte mono frame the Waveshare 4.2" e-paper module takes; firmware is
thewaveshare-rlcd-42build in tesserae-device-firmware, confirmed on a
bench unit. Not auto-selected from a relay self-report because it shares
the mono 400x300 report with the e-paper module.
[0.482.0], 2026-10-27
Changed
- Settings builds only the cards the open tab shows. Every tab used to
assemble the full device view-model (status, timetable, palette and
firmware lookups for each display) and then discard it, which is what
made the Server tab slow to open on a Docker install with a few
devices. - The System tab's GitHub release check on Docker and other non-git
installs runs in the background. The page renders the cached result,
or a "checking" line that fills in once the check finishes, instead
of waiting on GitHub before it can load. - The per-device firmware "update available" lookup no longer waits on
api.tesserae.ink during a Settings render. A cold or expired cache
starts one background fetch per device kind, and the next render
shows the answer.
[0.481.2], 2026-10-27
Fixed
- The Proxmox VE helper script asks for the release channel on the host,
before the container dialog. Asking inside the container never showed
the question, because the engine treats install-time prompts as
unattended, so every install silently took stable.
[0.481.1], 2026-10-27
Changed
- The Proxmox VE helper script defaults to 4 CPUs, 3 GB RAM and a 16 GB
disk. The metadata record matches.
[0.481.0], 2026-10-27
Added
- A Proxmox VE helper script in the community-scripts format, under
scripts/proxmox/. It creates an unprivileged Debian 13 container,
installs Tesserae on a stable (tagged release) or edge (main branch)
channel with Chromium and a systemd service, and lands on the setup
wizard.updatein the container shell moves it forward on the chosen
channel. The LXC install page documents how to run it. - Installers that own the upgrade path can set
TESSERAE_INSTALL_METHOD;
Settings → System then points at their update command instead of
suggesting a re-run of install.sh. The helper script sets it.
[0.480.1], 2026-10-26
Fixed
- The battery route tests run against the onboarding check added in 0.479.1,
so CI passes again. No change to the app itself.