storefront plugin
The storefront plugin is provided by the storefront package in the led-ticker-plugins monorepo. It paints an always-visible OPEN or CLOSED badge as a corner overlay on top of whatever the ticker is showing, flipped by a weekly business-hours schedule and the Pi’s clock — no external service, works offline.
It’s the sibling of the core busy light overlay: same mechanism (a frame overlay hook + a background poller that flips shared state; paint() stays paint-only), richer content — text, colors, orientation, animated “neon” color.


Install
Section titled “Install”Add the package to your plugin manifest and restart:
# First plugin? Create your manifest from the example:cp -n config/requirements-plugins.example.txt config/requirements-plugins.txt# Add the package (one name per line), then restart:echo "led-ticker-storefront" >> config/requirements-plugins.txtdocker compose restartOther install paths (web UI Store tab, dev checkout) are on the Plugins page.
Minimal config
Section titled “Minimal config”Unlike a widget, storefront isn’t added to a playlist section — it’s a top-level [storefront] block, present anywhere in config.toml:
[storefront]corner = "top_right"
[storefront.open]text = "OPEN"color = [0, 255, 0]
[storefront.closed]text = "CLOSED"color = [255, 0, 0]
[storefront.schedule]mon = "09:00-17:00"tue = "09:00-17:00"wed = "09:00-17:00"thu = "09:00-17:00"fri = "09:00-17:00"sat = "10:00-14:00"# sun omitted = closed all dayThe badge appears the moment the config loads (an eager first evaluation runs at startup, before the first poll), and flips automatically as the clock crosses a schedule boundary.
Schedule syntax
Section titled “Schedule syntax”Each day of the week (mon … sun) maps to a schedule string:
- A single range:
"09:00-17:00"— 24-hourHH:MM-HH:MM. - Multiple ranges (e.g. a lunch closure):
"09:00-12:00,13:00-17:00"— comma-separated; the day is open if any range matches. - Closed:
"closed", or simply omit the day key — both mean closed all day. - Open all day:
"00:00-24:00"—24:00(1440 minutes) is only valid as a range end, meaning end-of-day. - Overnight wrap: a range whose end is less than or equal to its start (
"18:00-02:00") crosses midnight and belongs to its start day, per the caution above.
Exceptions
Section titled “Exceptions”Override specific dates — a holiday, a special event, a one-off closure — without touching the weekly [storefront.schedule]:
[storefront.exceptions]"12-25" = "closed" # recurring: every Christmas"2026-11-26" = "closed" # one-off"12-24" = "09:00-13:00" # recurring short day"2026-12-31" = "20:00-02:00" # special hours; wraps allowedKeys are "MM-DD" (recurring — matches that date every year) or "YYYY-MM-DD" (one specific date, zero-padded); values reuse the same day-string grammar as the weekly schedule ("closed", a single range, comma-separated multiple ranges, "00:00-24:00").
Precedence is specific date > recurring date > weekly day — a matching exception key replaces that day’s hours outright; it never merges with the weekly schedule underneath it. Quoting the keys is optional — TOML accepts them bare too (12-25 = "closed").
Every open/closed flip logs an enriched line naming the next change, so you can confirm an exception took effect without waiting for the next boundary:
storefront: OPEN (matched mon 09:00-17:00; closes 17:00; now 12:30 America/New_York)storefront: CLOSED (no matching range; opens 09:00 tomorrow; now 20:15 America/New_York)Full config reference
Section titled “Full config reference”[storefront]background = [0, 0, 0]padding = 2# font = "..."font_size = 16# timezone = "America/New_York"corner = "top_right"orientation = "horizontal"
[storefront.open]text = "OPEN"color = [0, 255, 0]# corner and orientation fall back to the shared values above
[storefront.closed]text = "CLOSED"color = [255, 0, 0]
[storefront.schedule]# mon = "09:00-17:00" ...one key per weekday (mon..sun); see above
[storefront.exceptions]# "12-25" = "closed" ...one key per date override ("MM-DD" or "YYYY-MM-DD"); see above| Field | Type | Default | Notes |
|---|---|---|---|
background | [r, g, b] or "none" | [0, 0, 0] | Opaque box behind the text. "none" paints only lit glyph pixels over live content (transparent). |
padding | int | 2 | Pixels of background box around the text. Only visible with an opaque background. |
font | string | bundled default | BDF font name or hi-res font name. Hi-res is supported on the bigsign. |
font_size | int | 16 | Real (physical) pixels — block-scaled for BDF, native size for hi-res. See Sizing on large panels for the vertical-badge cap. |
timezone | string (IANA name) | system local clock | e.g. "America/New_York". See Caveats. |
corner | top_left / top_right / bottom_left / bottom_right | "top_right" | Shared default corner for both badges. Same four-value vocabulary as busy light. |
orientation | horizontal / vertical | "horizontal" | Shared default orientation for both badges. |
[storefront.open].text | string | "OPEN" | |
[storefront.open].color | [r, g, b] / string / table | [0, 255, 0] (green) | Flat color or a color provider — see Neon glow. |
[storefront.open].corner | corner value | inherits shared corner | Only set this to split the two badges into different corners. |
[storefront.open].orientation | orientation value | inherits shared orientation | |
[storefront.closed].text | string | "CLOSED" | |
[storefront.closed].color | [r, g, b] / string / table | [255, 0, 0] (red) | |
[storefront.closed].corner | corner value | inherits shared corner | |
[storefront.closed].orientation | orientation value | inherits shared orientation | |
[storefront.schedule].<mon..sun> | schedule string | omitted = closed | See Schedule syntax. |
[storefront.exceptions].<key> | schedule string | omitted = no exceptions | Key is "MM-DD" (recurring) or "YYYY-MM-DD" (specific); value is a schedule string. Replaces the weekly day outright. See Exceptions. |
background, padding, font, font_size, and timezone are shared between the two badges — there’s no per-state override for those (a documented future).
Layout notes
Section titled “Layout notes”- Corner anchoring: the badge’s bounding box (text extent +
padding) anchors flush to its corner, same vocabulary as busy light’s four corners. orientation = "horizontal"draws the word on one line.orientation = "vertical"stacks each character upright, one per row, centered in the column — good for a narrow strip down the edge of the panel.- Opaque vs. transparent background: the default
background = [0, 0, 0]paints a solid box first for a clean reserved-strip look. Setbackground = "none"to paint only the glyph pixels directly over whatever else is on screen. - Hi-res fonts and
font_size: a hi-res font paints at its native real-pixel size; a BDF font block-scales up tofont_size. See Sizing on large panels for the vertical-badge ceiling and how to get past it. - Large hardware panels (bigsign): the plugin’s own
examples/*.bigsign.tomlsmoke fixtures use a simplified single-canvas[display]block for render-demo GIFs — they are not a wiring reference. For the real 8-panel chain andpixel_mapper_config, seeconfig/config.bigsign.example.tomlin the core repo.
Sizing on large panels
Section titled “Sizing on large panels”The badge computes its bounding box (text extent + padding) before drawing and, if it doesn’t fit the panel, logs a warning and skips the draw entirely rather than rendering a clipped or garbled badge:
storefront: badge 'OPEN' (16x64) does not fit the 64x32 panel at font_size=16; skipping. Use a smaller font_size or a shorter label.The trap this catches most often: vertical orientation + a large font_size + the default BDF font, on a 64px-tall bigsign. BDF block-scales in whole-cell-height multiples, so a multi-letter word like OPEN stacked vertically only fits up to roughly font_size = 16 on a 64px-tall panel — larger sizes overflow and get skipped. That ceiling is relative to the panel’s real height, not a fixed number: it’s the panel height that constrains BDF’s block-scale math, not font_size itself.
There is no font_size ceiling at all with a hi-res font — font = "Inter-Regular" (or any bundled/user hi-res font) paints at its native real-pixel size and isn’t bound by BDF’s cell-height math. This is verified directly: test_hires_badge_renders_on_large_panel in the plugin’s test suite draws a 64px hi-res badge on a 512×128 headless canvas and asserts it renders with no oversize warning — a size that would have been well past the BDF ceiling on a real bigsign. BDF badges at font_size 24 and 96 on that same large canvas also block-scale up without clipping (test_bdf_block_scale_badge_on_large_panel) — the constraint tracks panel height, not the font system.
So: if the skip-warning above fires and you actually want a large badge (rather than a smaller one), the fix it names — “a smaller font_size or a shorter label” — isn’t your only option. Switching to a hi-res font via font = "..." gets you the large badge you wanted instead of shrinking it.
Neon glow
Section titled “Neon glow”color on either badge accepts a flat [r, g, b] or a color provider string/table ("shimmer", "rainbow", "color_cycle", or an inline table), animated once per frame via a shared frame counter — the badge visibly breathes or sweeps rather than sitting static.
Caveats
Section titled “Caveats”-
Timezone / NTP dependency. All schedule comparisons happen against the configured
timezone(or the system local clock if omitted). A wrong clock — badtimezone, missing NTP sync, a DST edge — produces a wrong badge, and it will look like a plugin bug rather than a clock problem. The startup log line always states the time the plugin thinks it is:storefront: OPEN (matched mon 09:00-17:00; closes 17:00; now 14:23 America/New_York)storefront: CLOSED (no matching range; opens 09:00 tomorrow; now 18:05 America/New_York)and logs again on every state flip — if the badge looks wrong, check this line before anything else.
-
No
led-ticker validatecoverage.validateonly checks playlist widgets, not top-level plugin overlay blocks. A malformed[storefront]block (bad time string, unknown corner/orientation, unknown weekday key, a badge too large for the panel) is caught at startup instead: the error is logged, plugin-load isolation disables the badge, and the rest of the panel keeps running undisturbed. -
Not covered by config hot-reload. The plugin reads its
[storefront]block once at startup, so editing it while the display is running does not take effect — hot-reload reports “restart required” and names thestorefrontblock; restart the process to apply the change.