A TRMNL e-ink plugin that shows your Beszel server fleet at a glance — CPU, memory, disk, uptime, load average, and temperature for every host you monitor.
A small Python script polls your Beszel hub every 5 minutes, transforms the latest stats into a compact JSON payload, and pushes it to a TRMNL Private Plugin via webhook. TRMNL renders the supplied Liquid template into a 1‑bit BMP and your device picks it up on its next refresh.
[ supercronic ] -> [ push.py ] -> [ Beszel API ] -> [ TRMNL webhook ] -> [ device ]
No data leaves your network except the rendered payload (servers' names, percentages, uptime). Beszel credentials stay in a local .env.
- A TRMNL device (Private Plugins work on every TRMNL plan)
- A running Beszel hub with at least one agent reporting
- Docker + Docker Compose, or Python 3.11+ if you'd rather run the script directly
In your TRMNL account, go to Plugins → Private Plugin → New and choose:
| Field | Value |
|---|---|
| Strategy | Webhook |
| Name | Beszel Servers (or whatever you like) |
| Framework CSS version | latest (v3+) |
| Remove bleed margin | No |
| Dark mode | No |
Save. The plugin's edit page now exposes a Webhook URL that looks like https://trmnl.com/api/custom_plugins/<UUID> — copy it.
On the same page, click Edit Markup → Full, paste the contents of template.liquid, Save. Hit Preview — at first it'll be empty; once the script pushes once it'll show real data.
cp .env.example .env
$EDITOR .envBESZEL_URL=https://beszel.example.com # your Beszel hub
BESZEL_EMAIL=you@example.com # a Beszel user with read access
BESZEL_PASSWORD=... # password
TRMNL_WEBHOOK_URL=https://trmnl.com/api/custom_plugins/<UUID>
TZ=Asia/Kolkata # timezone for the timestamp on the card
With Docker (recommended) — runs every 5 minutes via supercronic:
docker compose up -d
docker compose logs -fYou should see ok: N/M up, payload XXXB @ HH:MM lines on each cron tick.
Without Docker — wire python3 push.py into your scheduler of choice (cron, systemd timer, launchd):
set -a; . ./.env; set +a
python3 push.pyThe script has zero dependencies beyond Python's standard library.
- Refresh cadence — edit
crontab. Default is*/5. TRMNL's free tier rate-limits to 12 webhook posts per hour (every 5 min); TRMNL+ allows 30/hour. - Layout —
template.liquiduses TRMNL Framework v3 classes (progress-bar,divider,flex--between, etc.). All classes are documented at trmnl.com/framework/docs/v3. Tweak directly in the TRMNL UI; your changes are saved server-side. - Other view sizes — the template targets
view--full(800×480). Addhalf_horizontal,half_vertical, orquadrantviews in the TRMNL markup editor if you want this plugin to share screen space with other plugins. - Payload size — the JSON push is capped at 2 KB by TRMNL. The script uses short keys (
n,s,cpu,mem,dsk,up,la,t) to keep room for ~30 servers. If you exceed the cap you'll see a warning on stderr.
Beszel's systems collection embeds the latest snapshot in a record's info field, so a single GET /api/collections/systems/records covers everything we display — no need to query system_stats rollups. The script picks out:
| Beszel key | Card field |
|---|---|
cpu |
CPU % |
mp |
Memory % |
dp |
Disk % |
u |
Uptime (formatted as Xh / X.Xd / Xd) |
la[0] |
1‑min load average |
dt |
Dashboard temperature (°C) |
t |
Thread count (sent but currently unused on the card) |
trmnl error 403with a Cloudflare error — your User-Agent was rejected. The script sendsbeszel-trmnl/1.0; if you fork and rename, keep some identifying UA.beszel error 400/401— Beszel uses PocketBase auth. Confirm the email + password againsthttps://<your-beszel>/_/. Superusers and regular users both work; a dedicated read-only user is cleanest.oklines but device doesn't update — TRMNL devices refresh on their own cadence (set per-device onhttps://usetrmnl.com/devices). Use the Preview button on the plugin page to confirm the markup renders.- Payload >2 KB warning — too many servers; trim with
?filter=...inbeszel_systems()or shorten the displayed names.
MIT. See LICENSE.