github dmellok/tesserae v0.45.5

latest releases: mcp-v0.10.1, mcp-v0.10.0, mcp-v0.9.1...
one month ago

Big day. Headline feature is Rotations, plus a stack of TRMNL / Home Assistant / timezone fixes that piled up across 0.44.7 to 0.45.5.

Added

Rotations — cycle dashboards on a wall-clock anchor

New top-nav entry next to Schedules. A rotation is an ordered list of (page, dwell_minutes) steps that loop on a daily anchor.

Common ask was "show A for 30 min, then B for 30 min, repeat" or "morning at 06:00, midday at 12:00, evening at 18:00." That now configures with a couple of clicks instead of needing six daily schedules.

  • Anchor reseeds at the configured HH:MM each local day, so DST flips don't desync long cycles.
  • Optional end_at field stops the cycle at a wall-clock time (e.g. 09:00 → 17:00 for a workday rotation that falls silent overnight).
  • Day-of-week filter mirrors Schedules.
  • Priority field lets existing daily schedules preempt the rotation, so a 9am "morning standup" daily schedule still fires over the rotation's current step.
  • First tick after enable fires the current step immediately; subsequent ticks within the same step are no-ops.
  • Each step's preview shows which physical devices that step's dashboard is bound to.
  • Manual "Fire now" button pushes whichever step the rotation is currently on.
  • History page filter strip exposes a Rotation tab with the shuffle icon.

Plugin context now carries cell_w / cell_h

The composer hydrates every widget's fetch() with the cell's actual pixel dimensions alongside panel_w / panel_h. Widgets that pull images from upstream APIs (e.g. the fal_image community widget) can now request an image at the exact size they'll be painted at instead of guessing from the panel's aspect ratio.

Derive battery_pct from battery_mv on every heartbeat

Native TRMNL kit firmware sends raw millivolts (Battery-Voltage) but no percentage. That meant TRMNL devices were absent from the topbar battery indicator AND from the HA MQTT auto-discovery battery sensor. The merge step now runs a LiPo curve (4200 mV = 100 %, 3300 mV = 0 %, clamped both ends, linear in between) when the firmware reports mV without explicit pct.

HA auto-discovery battery_voltage sensor

Lazy-published the first time a heartbeat carries battery_mv. Lets HA automations run off the raw value rather than the derived percentage.

Dev gallery sample for calendar_schedule community widget

The new tesserae-calendar-schedule community widget (Google-Calendar-style agenda view) would render blank in the dev gallery without an ICS feed configured. Bundled a synthetic school-week sample so /_test/widgets and the catalog preview pipeline show a representative frame.

Changed

Rendered frames now honour the app-level timezone setting

The Tesserae preview iframe paints in the user's browser, so it picks up the laptop's timezone. The frame pushed to the device is painted by a headless Chromium inside the Tesserae container, which previously read its timezone from the container's TZ env var (defaulting to UTC under Docker / the HA add-on). Users on Europe/London during BST saw clock + calendar widgets render an hour behind even with settings.app.timezone = "Europe/London" configured. RenderRequest now carries a timezone_id field; PushManager forwards the app setting to browser.new_context(timezone_id=...) on every push.

Page editor: text inputs defer preview refresh to blur, not keystroke

The live preview still updates on every slider tick / checkbox flip / dropdown change, but text and number fields wait for the change event (blur or Enter) before re-rendering the preview iframe. Matters for widgets whose fetch() calls a paid API (e.g. the new fal_image community widget): typing a prompt no longer fires a generation per character.

/api/display envelope alignment with Terminus

Three corrections so any TRMNL-compatible firmware reads identical fields off Tesserae as it does off the upstream BYOS reference server:

  • special_function defaults to \"sleep\" (was \"none\"). The prior value caused some firmware builds to stay in standby between polls, draining the LiPo.
  • New maximum_compatibility: false field; lets firmware use modern features (partial refresh, etc.) by default.
  • /api/log/ parses Terminus's documented payload shapes ({\"logs\": [...]} and the nested {\"log\": {\"logs_array\": [...]}}) and surfaces each entry as its own log line rather than dumping the raw request body.

Fixed

TRMNL Battery-Voltage accepts decimal volts

Some native TRMNL firmware sends voltage as a decimal string (e.g. \"3.86\") instead of the integer millivolt form \"3860\". The parser previously only accepted the integer form, so battery_mv came out as None and downstream sensors went dark. The parser now accepts both: any positive value below 100 is treated as volts and multiplied by 1000.

Test count

722 tests, ruff clean.

Upgrade notes

Pure additive. No migrations, no breaking config changes. Existing schedules, settings, devices, and feeds all keep working. The new Rotation model uses an additive JSON file under data/core/rotations.json; it's created on first save.

Full per-version detail in CHANGELOG.md.

Don't miss a new tesserae release

NewReleases is sending notifications on new releases.