Local-API integrations for the BUSY Bar — a 72×16 LED status display on USB or LAN.
- BUSY Bar on USB (default address
10.0.4.20) or LAN - Python 3.12+
- uv — fast Python package installer and resolver
Per-integration extras (e.g., macOS Calendar access for calendar_countdown) are noted in each integration's README.
-
Clone the repo:
git clone https://github.com/your-org/busybar-integrations.git cd busybar-integrations -
Sync dependencies:
uv sync
-
Copy and edit the configuration:
cp config.example.toml config.toml
Edit
config.tomlto set your BUSY Bar address and integration-specific settings. -
Test an integration with dry-run:
cd integrations uv run python -m calendar_countdown.main --once --dry-runOr for CI status:
uv run python -m ci_status.main --once --dry-run
The display is a shared 72×16 canvas. Each integration publishes text, shapes, or status via the busybar.client.BusyBarClient API (see src/busybar/client.py). The display arbitrates by priority, through the shared ladder in src/busybar/display.py:
| Priority | Tier | Occupied by |
|---|---|---|
| 20 | PRIORITY_AMBIENT |
calendar_countdown's baseline countdown (normal and in-progress) |
| 21 | PRIORITY_OVERLAY |
ci_status's rotation — running badge, GitHub GraphQL/REST quota gauges, and failure/stuck/quiet-green frames |
| 25 | PRIORITY_AMBIENT_RAISED |
calendar_countdown inside approach_minutes, outside notice_minutes — no longer interruptible by the overlay tier |
| 60 | PRIORITY_ALERT |
Reserved/unused — no in-repo integration currently draws here |
| 65 | PRIORITY_AMBIENT_URGENT |
calendar_countdown inside notice_minutes/warn_minutes |
| 90 | PRIORITY_SESSION |
An authenticated BUSY/CUSTOM work session on the device — outranks everything else |
Two firmware facts shape all of the above: equal priority from a different application_name is rejected (409), not a hand-off — only a strictly higher number preempts; and a preempted app's elements are evicted, not restored — the lower-priority app only reclaims the screen via its own next scheduled redraw, never automatically. Each element carries an optional timeout; if its source doesn't refresh within that window, the element self-clears rather than sticking on screen indefinitely.
Overlay dwell/rotation. ci_status's overlay-tier frames (failure/stuck/quiet-green frames, the running badge, and the GraphQL and REST quota gauges) each draw for one OVERLAY_DWELL_SECONDS (10s) dwell slot, then stay silent for at least one more dwell period before redrawing — giving calendar_countdown's own ambient-tier redraws (also tuned to a 10s cadence) a real chance to land in the resulting gap. Because eviction is one-way, the two integrations trade the panel back and forth rather than alternating cleanly; see each integration's README for the measured recovery rates.
No alert takeover. ci_status no longer draws at PRIORITY_ALERT; failure and stuck-queue frames now rotate at PRIORITY_OVERLAY (21) alongside the running badge and quota gauges, under the calendar's ambient tiers. As an upcoming calendar event gets closer, calendar_countdown climbs from PRIORITY_AMBIENT (20) through PRIORITY_AMBIENT_RAISED (25, inside approach_minutes) to PRIORITY_AMBIENT_URGENT (65, inside notice_minutes/warn_minutes), which already sits strictly above the overlay tier — so an imminent event naturally outranks a CI failure, and the failure frame alternates with the calendar's own redraws rather than camping the panel.
The application_name field tags each draw's source, letting the display track ownership and multi-instance behavior.
To add a new integration:
- Read
src/busybar/client.py— the public API for drawing to the display. - Follow the logic/adapter split:
- Logic module: your domain (e.g., polling a calendar or API, computing state).
- Adapter module (
main.py): connects logic to the BusyBar display, handles CLI args, and lifecycle.
- Add per-integration docs to your
README.md— document config options, API tokens, and any platform-specific setup (e.g., macOS Calendar permission prompts).
config.toml is not checked in (it's in .gitignore). This repo follows a no-secrets-by-construction policy:
- All secrets (API tokens, credentials) go in
config.toml, which you provide locally. - The repo ships only
config.example.toml, documenting all fields and defaults. - CI/CD can inject secrets via environment-variable expansion in config parsing if needed.
By default, BusyBarClient talks to your BUSY Bar directly over the LAN
([device].host). As of v1.6, it can automatically fall back to BUSY's
cloud relay if the local device becomes unreachable — USB unplugged,
Wi-Fi drop, the device off — and recover back to local on its own once
it's reachable again. This is entirely optional and off by default.
- Create a token at cloud.busy.app → API tokens tab → create a new token with the "BUSY Bar" scope. This scope grants full control of exactly one linked device — there's no separate device ID to configure; the token itself identifies which device it talks to.
- Add it to your
config.toml(neverconfig.example.toml, and never anything committed to the repo — see "Configuration" above):[device] host = "10.0.4.20" cloud_token = "paste-your-real-token-here"
- Optionally set
transport(default"auto"):"auto"— local first, cloud fallback when the local device is unreachable andcloud_tokenis set. Recovers back to local automatically."local"— local only, never falls back (identical to pre-v1.6 behavior; the default if you never setcloud_token)."cloud"— forced cloud only, never attempts local. Mainly useful for deliberately exercising/debugging the cloud path.
cloud_base_urldefaults tohttps://api.busy.app/busybarand normally doesn't need to change — see the base-URL note below.
Manage tokens from the same API tokens tab on cloud.busy.app. Revoking a token takes effect immediately and cannot be undone — if you're rotating, create and deploy the replacement token first, then revoke the old one, rather than revoking first.
Continuous status streaming (/api/status/ws) is local-only by design —
the cloud API has no equivalent, so a caller relying on the status
WebSocket will not get a cloud fallback for it. Everything else this
client uses (draw, clear, status, get_busy, set_busy_simple,
play_audio) is a synchronous request/response call and mirrors 1:1
over cloud.
The v1.6 launch tests were entirely mocked; the checklist that shipped with that round has since been run against a real device and a real token, with these results:
- Base URL confirmed.
https://api.busy.app/busybar(the shippedcloud_base_urldefault) is correct and working — thebusylib-pydiscrepancy noted during research (its own hardcoded default is the differently-hostedhttps://proxy.busy.app) does not apply to this client. No change needed tocloud_base_url. - Forced-cloud draw probe: 5/5
DRAWN. Round-trip latency 300–465ms, median 353ms — well inside the cloud transport's(5, 15)s timeout, and ample headroom undercalendar_countdown's 10s ambient redraw cadence (a cloud-relayed redraw comfortably completes well before the next one is due). - Auto-fallback is live in production. Running with
transport = "auto"; local→cloud degradation and cloud→local recovery transitions are logged atINFOexactly as designed (seeBusyBarClient's class docstring insrc/busybar/client.py).
| Integration | Description |
|---|---|
calendar_countdown |
Live countdown to your next macOS Calendar event. Four-stage escalation as an event approaches — approach_minutes (30m default), notice_minutes (15m, amber), warn_minutes (5m, red), and a final-minute LED blink — plus one audio chirp precisely at event start. The countdown itself turns teal while the event is in progress. Optional auto_busy starts a BUSY session automatically for the event's duration. |
ci_status |
GitHub Actions status via the REST API with ETag caching (near-zero steady-state quota cost). Failure and stuck-queue frames rotate calmly at the overlay tier — CI FAIL owner/repo #42 · workflow (PR number, or branch when there's no PR) — with a gentle red LED while a workflow is failing. While a run is active, the same rotation adds a running badge (ETA plus a "remain"/"left" label) alongside GitHub GraphQL/REST quota gauges. Optional account-wide watching auto-discovers and monitors every repo you own, not just an explicit list. |