Skip to content
led-ticker

Widget scheduling

A schedule is a time window: a start, an end, and an optional list of days. led-ticker uses that one vocabulary for two different jobs:

  • Visibilityschedule = {...} on any widget or section. Outside the window, the widget or section is skipped this pass. This page.
  • Brightness[display.schedule]. Dims or darkens the whole panel by time of day, but never changes what is shown. See Display: Scheduling.

They share the same start/end/days semantics (including the overnight wrap), so once you know one you know the other — only the target differs: content vs. brightness.

The classic use case: swap an “OPEN” graphic for a “CLOSED” one at the same two times every day.

Two widgets, complementary windows

[[playlist.section]]
mode = "slideshow"
[[playlist.section.widget]]
type = "image"
path = "open.png"
schedule = { start = "09:00", end = "17:00" }
[[playlist.section.widget]]
type = "image"
path = "closed.png"
schedule = { start = "17:00", end = "09:00" }

Neither widget has days, so both are active every day of the week. open.png’s window runs 09:00 up to (not including) 17:00; closed.png’s window runs from 17:00 through 08:59 the next morning, wrapping past midnight because start > end. Every minute of every day falls in exactly one of the two windows, so the sign always has something to show — a gapless, 24/7 handoff between “open” and “closed.”

Real storefronts are usually closed on weekends, not just overnight. Add days to open.png to restrict it to weekdays:

open.png restricted to weekdays

[[playlist.section.widget]]
type = "image"
path = "open.png"
schedule = { start = "09:00", end = "17:00", days = ["mon", "tue", "wed", "thu", "fri"] }

This breaks the gapless coverage from the previous example: closed.png still has no days, so its window is still only 17:00–08:59 every day — it does not expand to cover Saturday and Sunday daytime. The result is a gap: Saturday and Sunday 09:00–17:00 is covered by neither widget, so the section has nothing to display and the panel goes dark for those eight hours (see Behavior notes below on why a scheduled-out sign goes dark on purpose). This is exactly the situation led-ticker validate’s blank-interval warning (described under One clock below) is built to catch — it will warn and list Sat 09:00-17:00 and Sun 09:00-17:00 so you can decide whether that’s intentional (a real weekend closure) or a bug (forgot to add a weekend closed.png window).

If a weekend closure is what you want, add a closed.png window (or a section-level schedule) that explicitly covers the weekend daytime hours too, so the gap is filled on purpose rather than by omission.

schedule also works on a section, gating the whole section (all of its widgets) at once:

[[playlist.section]]
mode = "slideshow"
schedule = { start = "09:00", end = "21:00" }

schedule is a core-owned field. The engine pops it off the widget’s config before the widget’s constructor ever runs, so a widget class — core or plugin — never sees it and never has to implement anything to support it. This also means a widget class can’t declare its own field named schedule; if one does, config-load rejects it with an error naming the conflict (rename the widget’s field, e.g. poll_schedule).

Because the check lives in the engine, schedule composes with a widget’s own visibility logic. The countdown widget already hides itself once its target date has passed (should_display()); add a schedule and the two conditions AND together — the countdown only shows when it’s both in its date range and inside its scheduled window. Neither check can force the other to show something it wants hidden.

Visibility schedules read the wall clock from exactly one place: the top-level [display] timezone field (an IANA name, e.g. "America/New_York"; empty string = the machine’s system local time). There is no per-widget or per-section timezone override.

The brightness scheduler ([display.schedule], see Display: Scheduling) reads the same top-level field too — but it also accepts its own [display.schedule] timezone, a per-feature override that, when set, wins for brightness only. Set that field alone and brightness moves to the new zone while visibility schedules stay on [display] timezone (or the system clock, in Docker). Set the top-level [display] timezone instead and both features share one clock.

led-ticker validate prints the resolved clock so you can catch this before deploying:

visibility schedules evaluate at 14:32 (America/New_York)

If every section in your config ends up scheduled out for some stretch of the week, validate also warns about it — listing the blank interval(s) — so you can confirm that’s what you meant before it happens live on the panel.

  • Section-level granularity is one playlist cycle. A section’s schedule is (re-)checked once per pass through the playlist, not on every render tick. For a flip that should land promptly after the boundary — a tight 17:00:00 open→closed cutover; see the latency bound below — put both widgets in one section — see the storefront example above — rather than splitting them across two sections that each get checked on their own cycle.

  • A fully scheduled-out sign goes dark on purpose. If every section’s window is inactive at the same moment, the panel goes dark after two consecutive all-out cycles (~2 s), debouncing content flaps (e.g. a container emptying briefly on a failed poll) instead of visibly blanking on every one, then idles rather than showing stale content or freezing. This is treated as correct behavior — a closed storefront going dark — not a bug. It logs once when going dark and once when waking, not on every idle loop — but it keeps re-blanking the panel once a second the whole time it’s dark, so overlays (like the busy-light corner dot) and liveness monitoring keep working overnight instead of freezing on whatever was on screen at the moment it went dark. The engine recycles its framebuffers; a dark panel allocates nothing, so a config that flaps in and out of “all scheduled out” repeatedly doesn’t grow memory use over time.

  • A widget can appear one last time after its window closes. If a widget was on screen when its window closes, it can still render as the outgoing frame of the transition already in flight — a sub-second artifact, once per exit, and not something to work around.

  • Section titles don’t take schedule. [playlist.section.title] is not schedulable — schedule the section itself instead ([[playlist.section]] schedule = {...}).

  • Widget-level schedule (and should_display()) are re-evaluated every pass through a section — including in a loop_count = 0 (forever) section. Both checks live in _expand_sources, which runs once per pass regardless of loop_count, so a widget’s window closing (or a countdown rolling out of range) reaches the panel within about three in-flight items plus the remainder of the already-expanded pass — up to ~4 widget shows for a small section (roughly 15–30 s at hold_time = 5), or up to roughly one full pass for a section with many widgets or a large container, since each pass’s rotation is gate-checked once when it is expanded — not “whenever the section is next entered.” This holds with or without a section-level schedule.

  • A SECTION-level schedule on a forever section is a different story: it’s still only checked at entry. [[playlist.section]] schedule = {...} is evaluated once, when the section is entered. For a finite loop_count the playlist re-enters (and re-checks) the section every cycle, so staleness is bounded to roughly one section run. For loop_count = 0 the section, once entered, has no natural re-entry point — unless the widget rotation itself empties out and hands the forever cycle an exit (see shape 2 below), the section-level schedule is locked in for good.

    This has two consequences, depending on shape:

    1. Section-level schedule + loop_count = 0, general case. The section-level schedule is checked once, at entry, and never again — the section keeps showing past its end time instead of going dark.
    2. Section-level schedule + loop_count = 0, all-widgets-scheduled case. If every widget in the section also carries its own schedule = {...} and those widget windows leave a gap somewhere in the week, the widget rotation empties out at that gap, which gives the section a fresh entry — re-checking the section-level schedule too, just at the widget-window boundary rather than exactly at the section’s own end. That re-check reaches the panel within a couple of queued items once the rotation empties, which can lag the widget-window close by the remainder of the already-expanded pass — not instant, but fast. Unless the widget windows jointly cover the full week (e.g. complementary AM/PM windows with no gap, the same gapless pattern used for the storefront example above), the rotation never actually empties out, so this degrades to shape 1 — the strong warning — despite every widget nominally having its own schedule.

    led-ticker validate warns on both shapes — the strong message for (1), a softened message for (2). In both cases the fix is the same: use a finite loop_count (e.g. 1) so the playlist re-enters the section — and re-evaluates its section-level schedule — every cycle. Note that validate’s blank-interval sweep assumes a forever section closes on schedule; with loop_count = 0 and no exit, it won’t, so the sweep’s “blank during X” claim can be wrong for that shape. A loop_count = 0 section with widget-level schedules but no section-level schedule isn’t warned about at all — per the first bullet above, that shape is already gated at display time.