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:
- Visibility —
schedule = {...}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 open/closed storefront
Section titled “The open/closed storefront”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.”
Weekday-only hours
Section titled “Weekday-only hours”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" }Works on every widget, including plugins
Section titled “Works on every widget, including plugins”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.
One clock: [display] timezone
Section titled “One clock: [display] timezone”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.
Behavior notes
Section titled “Behavior notes”-
Section-level granularity is one playlist cycle. A section’s
scheduleis (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(andshould_display()) are re-evaluated every pass through a section — including in aloop_count = 0(forever) section. Both checks live in_expand_sources, which runs once per pass regardless ofloop_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 athold_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
scheduleon 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 finiteloop_countthe playlist re-enters (and re-checks) the section every cycle, so staleness is bounded to roughly one section run. Forloop_count = 0the 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:
- Section-level
schedule+loop_count = 0, general case. The section-levelscheduleis checked once, at entry, and never again — the section keeps showing past itsendtime instead of going dark. - Section-level
schedule+loop_count = 0, all-widgets-scheduled case. If every widget in the section also carries its ownschedule = {...}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 ownend. 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 validatewarns on both shapes — the strong message for (1), a softened message for (2). In both cases the fix is the same: use a finiteloop_count(e.g.1) so the playlist re-enters the section — and re-evaluates its section-level schedule — every cycle. Note thatvalidate’s blank-interval sweep assumes a forever section closes on schedule; withloop_count = 0and no exit, it won’t, so the sweep’s “blank during X” claim can be wrong for that shape. Aloop_count = 0section 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. - Section-level