github dmellok/tesserae v0.415.0

4 hours ago

Added

  • Richer, more accurate install heartbeat. The daily heartbeat now
    reports the real release channel (stable for tag builds, edge for
    main-branch images, main for an untagged source checkout; it was
    hard-coded to stable), a bucketed install age, one object per device
    kind carrying the newest reported firmware plus the panel's declared
    gamut, resolution and bucketed wake cadence, a bucketed lineup count,
    four feature booleans (relay, touch, MCP, quiet hours) and a bucketed OTA
    rollout snapshot. Per-kind firmware had been sent in a shape the API
    ignored, so every install read as unknown firmware. The first heartbeat
    of an install's life now leaves within seconds of opting in (wizard,
    Settings toggle, or the first page view) instead of after the daemon's
    boot delay, which had left short-lived installs visible in the update
    check but absent from the heartbeat. The privacy page lists the exact
    payload.

  • Opt-in question moved to the front of the wizard, right after the
    welcome, so it is seen before the steps where setup is often abandoned.
    Installs that never answered (skipped the wizard, or predate it) get a
    one-click prompt in the header; either answer persists.

  • Docker images are stamped with TESSERAE_CHANNEL at build time.

  • M5Stack PaperMono hardware kind. New m5stack_papermono kind for the
    3.97" 480x800 4-level greyscale handheld (Pro and Lite share it): the
    reTerminal Sticky's wire contract (esp32_bw_client, gray_4, a
    96000-byte 2-bpp frame from esp32_gray2_bin at the portrait stride the
    firmware transposes), paired with the m5stack-papermono target in
    tesserae-device-firmware. auto_select: false because it is
    indistinguishable on the relay wire from the Sticky and the Xteink X4
    gray build. Confirmed on real hardware the same day (portal, register,
    dashboard frame, battery telemetry) and added to the test matrix.
    The kind now also declares touch (FT6336G) with the usual touch_enabled
    / touch_linger_s knobs, and a frontlight_pct setting (0-100) for the
    panel's built-in frontlight, which the firmware hands to the board's
    power-management chip.

  • Every bundled widget now translates its panel text. The 28 widgets that
    still painted hardcoded English (the Home Assistant set, clocks, countdown,
    year progress, todo, news, pictures, transit, status) moved their labels,
    empty states, legends and status words behind the widget ctx.t() contract
    with a generated strings/en.json, and the whole bundled set ships
    Czech, Dutch, French, German, Italian, Lithuanian, Norwegian Bokmål, Polish,
    Portuguese, Slovak, Spanish, Swedish, and Chinese (Simplified and
    Traditional). The set follows where installs actually report from; the
    language pickers list all of them automatically. A repo-wide test now
    pins every widget's locales to its shipped files, the English key set to
    every translation, and each literal t() call to its English text.
    The German strings for the transit, climate, entities, locks, automation
    history and status widgets were then reviewed by a native speaker (#315).

Fixed

  • A webhook tap no longer repaints the old dashboard before the real
    update.
    A touch wake whose action is a fire-and-forget webhook: (or
    webhook_refresh) now answers 304 for the frame the panel reports
    holding, even when a newer render of the same page has appeared, instead
    of serving that render and flashing an unchanged dashboard ahead of
    whatever the receiver pushes back. The pushed frame lands in the linger
    window as before, or on the next timer wake. Page, rotate and Home
    Assistant actions still repaint on the same wake (#274).

  • Upgrading a catalog bundle removes the widgets it dropped. When a
    new release of a bundle renames or drops a folder, the marketplace
    installer now deletes the old folder instead of leaving it behind as an
    untracked plugin that still loads against the new shared code and could
    never be uninstalled from the catalog page.

  • A sleeping REST device woken for a daily lineup no longer collects the
    previous day's frame. The device was told to poll 20 s after the lineup's
    target, but the scheduler fires on a 30 s tick and renders each due lineup
    in turn, so most mornings the poll landed before the new frame existed. The
    miss then stuck: the projection reported the lineup as due now, a
    fraction of a second in the past once truncated to the second, and the
    poll path skipped it as stale and sent the device to sleep for its whole
    interval on the old frame. The scheduler margin is now 70 s, and a change
    overdue by less than a tick means "poll again after the margin".

  • The Jarvis-Judice-Ninke and Stucki dithers no longer flood flat regions
    of a colour panel with a single colour. The numpy error-diffusion loop
    never bounded the value a pixel accumulated from its neighbours, so a
    field the palette cannot reach banked the same residual on every pixel
    and the wide kernels carried it into the next region as a solid swathe.
    The diffused value is now clamped to 0-255 before matching, as Pillow's
    Floyd-Steinberg does. Atkinson shifts imperceptibly.

  • The Location picker on the weather, sky, and sunrise widgets now accepts a
    pasted lat, lon pair. It only ever asked the city name search, which
    returned "No matches." for coordinates, so a spot with no nearby town could
    not be saved from the editor (#302). The pair is offered as a pick, Enter
    takes it, and the Label auto-fills with the coordinates.

  • Interval schedules whose page renders unchanged no longer re-fire every
    scheduler tick. A no_change push never recorded the fire, so a stable page
    paid a full render every 30 seconds and filled the History view; it now
    re-arms the interval like a sent push does.

  • The canvas editor's in-browser preview now paints cached webfonts. It built
    its own font list from the bundled set, so a code element naming a cached
    family showed the fallback in the editor while the device push had the real
    face.

Added

  • The week calendar widget has a "Show daily event counts" toggle, so the
    per-day count chip under each date can be turned off to save header
    height. Event locations, when shown, now wrap to as many whole lines as
    fit inside the block instead of being cut to one ellipsised line.

  • The weather_now, weather_now_scenic, weather_hourly and weather_forecast
    widgets now ship French, on the same locales contract as the calendar
    widgets: titles, legends, metric labels, WMO condition text and empty
    states go through ctx.t(), and the forecast's weekday abbreviations
    come from Intl in the panel's locale rather than a fixed English table.

  • The Dashboards page has Active and Archived tabs. Archive parks a dashboard
    you're not using without deleting it: it keeps its layout, device links,
    and history, but leaves the "go to page" and lineup pickers, drops its
    Home Assistant entities, and skips the background refresh passes. Restore
    brings it back; Duplicate on an archived dashboard makes a working copy.
    Only a dashboard no lineup references can be archived, so the button is
    disabled on lineup members with the lineup named in its tooltip, and bulk
    archive skips them and says so.

  • Hardware catalog entry for the original M5Stack M5Paper (pre-S3), the 4.7"
    960x540 16-level greyscale handheld on the classic ESP32-D0WDQ6-V3. Unlike
    the PaperS3 its glass is driven through an IT8951 controller over SPI, so it
    reuses the reTerminal E1003's esp32_gray_bin wire format at the PaperS3's
    geometry over the existing esp32_client protocol. Marked
    auto_select: false -- an M5Paper and a PaperS3 report the same model,
    gamut and scan on the wire, so relay pairing can't tell them apart and this
    kind is only ever set explicitly. Verified end-to-end on hardware
    2026-09-10: dashboard frame, battery telemetry, the three side buttons, the
    SHT30 sensor, and touch. touch: true with a touch_enabled /
    touch_linger_s form; the GT911 reports in composition space so taps
    hit-test directly, and it needs Stay awake because the classic ESP32 can't
    wake from deep sleep on the touch controller.

  • Timed lineups take a display of their own. The setup wizard's "set time"
    and "keep fresh" modes now ask which display to send to, defaulting to
    every display the dashboard is on, so a dashboard shared by several
    panels can be scheduled for just one of them. The full schedule form
    gains the same picker, schedules carry device_ids through the
    Lineups view and the MCP schedule API, and the scheduler already
    honoured a bound timed lineup (discussion #300).

  • Hardware catalog entries for the reMarkable 2, reMarkable 1 and reMarkable
    Paper Pro, driven by the community tesserae.remarkable AppLoad client over
    the v1 REST device API. The reMarkable 2 is confirmed on hardware; the
    other two are built from the same code paths and await confirmation.

  • Dynamic refresh_rate on the TRMNL BYOS /api/display path. It echoed the
    static configured refresh_rate_s; now it runs the same next-poll decision
    the v1 REST path's next_poll_s has used since #241 — the configured rate is
    the ceiling, pulled earlier for the next projected dashboard change or a
    widget staleness hint, reshaped onto a wake-alignment grid, and stretched to
    sleep through a quiet window the device asked to sleep through (#299). A Kobo
    or Kindle running KOReader with "Use server refresh interval" enabled now
    wakes once at the end of quiet hours instead of every interval overnight. The
    shared decision moved into app.device_poll; a fault anywhere in it degrades
    to the static configured value.

  • 16-level greyscale over TRMNL BYOS. A new trmnl_png_gray16 renderer: the
    same fit / flip / underscan / contrast / dither pipeline as trmnl_png, but
    quantised to a 16-entry grey ramp and saved as an 8-bit greyscale PNG. For
    panels that paint real greys over the BYOS /api/display path, jailbroken
    e-readers running KOReader (confirmed on a Kobo Clara HD) and, untested, the
    Seeed reTerminal E1003 / TRMNL X 16-level waveform under TRMNL firmware,
    where 1-bit output turns every chart fill and shaded band into
    coarse error-diffusion speckle. Honours a device's measured grey ramp when a
    greyscale calibration profile is applied (same _gray_ramp side channel
    esp32_gray_bin reads). trmnl_client now lists both renderers.

  • Renderer picker on the device card (Settings → Devices → General). Shown only
    when a device's kind offers more than one renderer of the same wire format
    that the extension-matching auto-select can't tell apart — today that is
    trmnl_client (1-bit trmnl_png vs 16-grey trmnl_png_gray16). Switching
    repins the instance and drops its stale render so the next poll re-renders.

  • Any webfont in canvas pages and code elements, cached server-side. A new
    add_font MCP tool (POST /api/mcp/fonts) fetches a Google Fonts family by
    name at chosen weights and styles, or one face from a direct .woff2 / .ttf
    URL, once at author time; only the latin subset is kept unless asked
    otherwise, so a CJK family stays small. Cached families then work exactly
    like the bundled ones: autolibs inlines the face into a code element's
    sandbox when its CSS names the family, the page font field accepts the
    cached id, and the appearance catalog lists them with source: "cached".
    Renders never touch the network; a missing cache entry degrades to the
    author's fallback stack. list_fonts and delete_font complete the set,
    and files are served from /page-fonts/<id>/<file> on the local origin.

  • Hardware catalog entries for the paperlesspaper OpenPaper 7 (7.3" Spectra 6,
    800x480) and OpenPaper L (13.3" Spectra 6, 1200x1600) picture frames, both
    ESP32-C6 boards running the Tesserae device firmware over the existing
    esp32_client protocol and esp32_bin renderer. The L entry is marked as
    not yet confirmed on hardware.

  • The OpenPaper 7 entry and the tested matrix now record the on-hardware
    confirmation.

  • The OpenPaper 7 kind composes portrait (480x800) by default, matching the
    frame standing upright; the renderer rotates onto the panel's 800x480 native
    stride when packing. Confirmed upright on hardware with the USB-C port at
    the bottom.

  • The GTFS widget gains a "GTFS-RT VehiclePositions URL" field, for agencies
    that publish vehicle positions as their own feed beside TripUpdates and
    ServiceAlerts. With "Show stops away" on, a row reads "2 stops" when the
    feed reports a stop sequence, and falls back to the straight-line distance
    from the stop ("1.4 km", or miles via the new "Distance units" option) when
    the feed only carries coordinates, which is all some agencies publish.
    Reports older than ten minutes are ignored. gtfs 0.12.0.

  • The History widget gains a "Current value" option (#282). "Large headline
    above the chart" draws the single-sensor reading in display type with the
    trend arrow and low / high beside it, so a weather panel shows the value at a
    glance with the history underneath for context. The default keeps the value
    in the legend strip under the chart. ha_history 0.8.0.

  • Quiet hours can be limited to chosen weekdays and can mark whole days quiet
    (#299). Both the app-level window and a device's override gain a "Window
    applies on" weekday picker and a "Quiet all day" picker, so an office panel
    can be quiet overnight on weekdays and all weekend. Each day is judged by its
    own clock: on a ticked day an overnight window covers that morning up to
    the end time and that evening from the start time. Existing settings keep
    their meaning: every day, no all-day days.

  • "Sleep through quiet hours", off by default, tells battery panels to sleep
    until the quiet window ends instead of waking on their interval inside it.
    The REST status response stretches next_poll_s (and wake_at) to the end
    of the window, capped at six days; always-on panels are unaffected. Wake
    alignment now searches five days ahead so a quiet weekend does not push an
    aligned device back onto its plain interval.

  • The Home Assistant History widget takes optional "Y-axis minimum" and
    "Y-axis maximum" cell options that pin the chart's value axis. Blank keeps
    the auto-fit range, and one side can be set on its own. The Sensor widget's
    description and sparkline help now point at the History widget for a full
    chart with labelled axes (#282).

Fixed

  • The Gallery's sequential rotation keeps a cursor per device (#209). Two
    panels showing the same folder used to share one position and advance it
    for each other, so a panel woken between the other's renders skipped
    photos. Each panel now walks the album at its own pace; the first render
    after upgrading carries on from the album's current position rather than
    restarting at the first photo, and a render with no device bound keeps the
    shared cursor it always had. The cursor file is named after the device, so
    deleting .sequential_index_<folder>_<device> restarts that one panel.
    Because the widget now declares render.per_device_id, a page carrying it
    is composed and captured once per bound device rather than once per panel
    group, in random mode as well. picture_gallery 0.1.3.

  • Installing or removing a widget from Browse no longer shows "Install failed:
    network error" under the success message. The success branch throws while
    adding the topbar "Restart required" button (the topbar holds two theme
    toggles and it picked the drawer's, which isn't a direct child), and the
    request's error handler reported that as a network failure. The install had
    succeeded, but the restart button only appeared after a reload. The button
    now targets the topbar's own toggle, and the network-error message is
    reserved for a failed request.

  • A device's last heartbeat now survives a server restart (#282). The battery,
    signal, temperature and humidity readings the status strip widget and the
    Devices card draw from lived only in memory, so after an upgrade they were
    blank until the device next reported, and a frame fetched on a button wake
    is rendered before that same wake's heartbeat lands. The last merged
    heartbeat is kept in data/core/device_status.json and seeds the cache at
    boot with its original timestamp, so the Devices card still shows the
    reading's real age.

  • The History widget's low and high now always bracket its current value
    (#282). The current reading comes from the live state while low and high
    came from the history samples, which ha_core caches for longer, so a fresh
    reading could sit below the window's low. A live state newer than the last
    sample now joins the series as its final point, so the chart also ends where
    the headline says. ha_history 0.8.1.

  • Min / max labels on the History widget's chart stay inside the canvas
    (#282). A maximum sitting on the top gridline lost its label off the top
    edge, and a marker at either end of the series lost half of it off the
    side. The chart now reserves room above the plot for the max label and
    keeps both labels within the drawing area, flipping a label to the other
    side of its point when it cannot fit.

  • The self-hosted relay container now sends an explicit Content-Length on
    every response instead of chunked transfer encoding. ESP32 firmware reads a
    sealed frame only when the response carries a length, so a remote panel
    paired against a container relay stayed on "waiting for first frame" even
    though pairing and heartbeats worked. Relay image 0.1.1.

  • Older x86-64 processors (before 2009, no SSE4.2) no longer get a bare
    Illegal instruction from the Docker image: the entrypoint explains that
    the bundled numpy needs x86-64-v2 and points at the fix. The image accepts a
    NUMPY_SPEC build argument (--build-arg NUMPY_SPEC='numpy<2.4') to pin the
    last numpy line whose wheels run on those machines, and the install docs
    carry the same note for a bare install.

Added

  • Hardware kind waveshare_esp32_driver_75 for Waveshare's E-Paper ESP32 Driver
    Board carrying a 7.5" mono panel (V2, 800×480). Inherits esp32_bw_client
    and the 48000-byte mono frame shared with the reTerminal E1001 and the XIAO
    7.5" boards; firmware is the waveshare-esp32-driver-75 build in
    tesserae-device-firmware. Not yet confirmed on real hardware.
  • Calendar Core can use each event's own colour where the feed sets one.
    A per-feed toggle on the feed row opts in; RFC 7986 COLOR names and
    X-APPLE-CALENDAR-COLOR hex values are read, and an event with no colour of
    its own keeps the feed colour. Off by default, so existing feeds render as
    before. (#222)
  • GET /api/mcp/appearance serves the theme, style and font lists. (#257)

Changed

  • PicPak frames now get the "update available" chip on the Devices card and
    the Firmware page. The update check (api.tesserae.ink) also follows
    community firmware repos, starting with picpak_client mapped to
    varanu5/picpak-tesserae-client. Such releases carry no OTA descriptor, so
    the chip links the release for a web-flasher update rather than queueing
    an over-the-air one.
  • /api/mcp/catalog reports a count of themes, styles and fonts and points at
    the appearance endpoint instead of inlining the lists, taking roughly a
    sixth off every list_widgets read. (#257)
  • tesserae-mcp 0.16.0. list_widgets(section="appearance") fetches the
    lists from the new endpoint, and the served tool docs point there. Upgrade
    with pipx upgrade tesserae-mcp; an older bridge returns counts where the
    docs promise lists.
  • The device setup guide gains an Update delivery section describing Auto
    and Always full refresh.
  • The cryptography dependency now allows the 50.x series, which carries the
    fix for PYSEC-2026-3552. The affected PKCS#7 API is not used here; the pin
    was the only reason the audit kept reporting it.

Fixed

  • Background loops can be stopped as a set. The scheduler, the
    relay-pairing poller, the OpenDisplay telemetry poller and HA discovery's
    ticker register with a small registry when they start, and the transport
    wiring keeps the two pollers threadless under the test suite. This is what
    the intermittent hour-long CI stall was: each test that built an app left a
    full set of these loops running, and the worker eventually wedged with a
    hundred of them alive. The suite now stops them after every test and fails
    the test that leaks one.
  • Palette profiles no longer swap primaries on 7-colour ACeP panels. A
    profile emits its colours in Spectra 6 slot order and was applied to the
    panel palette positionally, which on inky_7colour put the calibrated
    yellow where the panel paints green and the blue where it paints red. The
    override is now matched to the gamut's slots by colour. (#298)
  • The Gallery widget's Scale option now does something. Every choice
    rendered as Fill: the client never read the option, and the shared
    full-bleed rule crops to cover. Fit letterboxes, Stretch distorts, Center
    shows native pixels, and Fit with blurred background paints a blurred copy
    of the picture behind the letterboxed one. (#296)
  • CircuitPython BMP frames have a fixed colour table per gamut. The
    circuitpython_bmp renderer used to compact the table to the colours a
    page actually used, so a tri-colour page with no red on screen shipped as
    a 2-colour 1-bpp file and the next frame with red in it no longer matched
    a bitmap the client had sized from the earlier header. The table is now the
    gamut's whole palette in a fixed order on every frame, with biClrUsed
    set to its length, so bit depth and table are constant for a given gamut
    and adafruit_imageload sizes a 3-colour frame's bitmap at 3 values
    rather than 16. (#277)

Don't miss a new tesserae release

NewReleases is sending notifications on new releases.