A collection of TRMNL plugins
  • C# 79.7%
  • Liquid 12.3%
  • JavaScript 8%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Lars Richter a32e2d7ba0 Add a tomorrow view to the timetable plugin
/timetable/tomorrow serves the same response shape one calendar day later,
so a second plugin instance can show tomorrow's lessons with the very same
markup. Both routes share one handler, so the token check and the
live/last-good/fallback chain exist once.

"Tomorrow" is strictly today + 1: on a Friday the view is a Saturday and
reads "Keine Schule morgen". The response carries a day_label (heute/morgen)
that the templates print in the title bar and the service uses in the
no-school note, which keeps the wording server-side and testable.

Today and tomorrow get separate last-good caches (TimetableCaches): sharing
one slot would let a stale today payload appear under the "morgen" heading
with the wrong lessons.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 20:56:03 +02:00
deploy Add deploy config and README; guard timetable startup against missing dir 2026-07-14 21:29:57 +02:00
docs/superpowers Redesign plugin templates for OG + TRMNL X, add Tagesschau photos 2026-07-18 23:27:22 +02:00
src/TrmnlPlugins.Web Add a tomorrow view to the timetable plugin 2026-09-10 20:56:03 +02:00
templates Add a tomorrow view to the timetable plugin 2026-09-10 20:56:03 +02:00
tests/TrmnlPlugins.Tests Add a tomorrow view to the timetable plugin 2026-09-10 20:56:03 +02:00
tools/preview Correct the plugin count and the preview tool's device docs 2026-08-21 08:38:56 +02:00
.gitignore Add local template preview tool 2026-07-17 23:57:38 +02:00
AGENTS.md Add a tomorrow view to the timetable plugin 2026-09-10 20:56:03 +02:00
global.json Scaffold consolidated TRMNL plugins service with /health 2026-07-14 21:16:26 +02:00
README.md Add a tomorrow view to the timetable plugin 2026-09-10 20:56:03 +02:00
TrmnlPlugins.sln Scaffold consolidated TRMNL plugins service with /health 2026-07-14 21:16:26 +02:00

my-trmnl-plugins

A single .NET service that backs four TRMNL plugins: garbage collection, school timetable, Bud Spencer & Terence Hill quotes, and Tagesschau news. They share the same tiny plumbing — a "never break the screen, always return HTTP 200 with a fallback" convention, a last-good response cache, and an optional ?token= shared secret — so they live in one service with one deployment.

Endpoints

Method Path Auth Data source
GET /health — static ok
GET /garbage optional ?token= CalDAV calendar (live)
GET /timetable optional ?token= timetable.json
GET /timetable/tomorrow optional ?token= timetable.json
GET /quote optional ?token= embedded quotes.json
GET /tagesschau optional ?token= Tagesschau API (live)

Each data endpoint always answers 200 — on upstream failure it serves the last good response, or a German fallback message. A missing or empty config section degrades only that plugin; the others keep working.

Features

  • Garbage collection (/garbage) — reads a CalDAV calendar, expands recurrences, and returns the soonest upcoming pickup per bin type with pre-formatted German date labels.
  • School timetable (/timetable) — per-kid daily lessons from a timetable.json config file, with subject emphasis/reminders and no-school (holiday) handling. /timetable/tomorrow returns the same shape for the next calendar day, so a second plugin instance can show tomorrow's lessons with the very same markup: the response carries a day_label (heute/morgen) that the template prints in the title bar and the service uses in the no-school note. Strictly today + 1, so on a Friday the tomorrow view is a Saturday and reads "Keine Schule morgen 🎉".
  • Quotes (/quote) — a random movie quote from the Bud Spencer & Terence Hill films (note the single-r "Terence"), drawn without back-to-back repeats.
  • Tagesschau news (/tagesschau) — the latest German Tagesschau headlines (topline, title, teaser, and a 16:9 teaser image) from the official Tagesschau API. Region and ressort (category) are chosen per request via URL query parameters. Non-story items (videos/webviews) are dropped; breaking news is flagged. The full-screen template shows the top two stories side by side with their photos; stories without an image fall back to text.

Configuration

One appsettings.json with a section per feature (Garbage, Timetable, Quotes, Tagesschau). Secrets and machine-specific paths go in a gitignored appsettings.Local.json (copy the shipped src/TrmnlPlugins.Web/appsettings.Local.json.example) or in Section__Key environment variables (e.g. Garbage__CalDavPassword).

Config is not part of the publish output. appsettings.json and appsettings.Local.json are excluded from dotnet publish (see the CopyToPublishDirectory="Never" items in the csproj), so redeploying by copying the whole publish folder can never overwrite the server's production config — and local dev secrets are never shipped. Manage config on the server instead (see Deploy).

  • Garbage: CalDavUrl, CalDavUsername, CalDavPassword, LookaheadDays (default 90), optional AccessToken.
  • Timetable: Path to the timetable JSON (relative paths resolve against the content root; point it at a private file outside the repo on the server), optional AccessToken. The committed Features/Timetable/timetable.json is sample data.
    • Timetable:Holidays — optional live holiday/vacation source via OpenHolidays. Set Enabled: true and a SubdivisionCode (ISO 3166-2, e.g. DE-BY for Bavaria) plus CountryIsoCode and LanguageIsoCode. School vacations and public holidays are then fetched (cached CacheHours, default 12) and merged with your manual noSchool entries. Disabled by default; if the API is unreachable the timetable falls back to the last good fetch, then to the manual list — it never breaks.
  • Quotes: optional AccessToken. Quotes data is compiled in as an embedded resource.
  • Tagesschau: region and ressort are query parameters, not config — set them on the polling URL (?regions=2,3&ressort=inland). The Tagesschau section holds server-side settings only: BaseUrl (default https://www.tagesschau.de/api2u), optional AccessToken, DefaultRegions/DefaultRessort (used when the query omits them), MaxItems (default 4), and CacheMinutes (default 5, the freshness window before re-fetching). Region codes are 1–16; ressort is one of inland, ausland, wirtschaft, sport, video, investigativ, wissen.

Build & test

export PATH="$PATH:$HOME/.dotnet"
dotnet test
dotnet publish src/TrmnlPlugins.Web -c Release -o out

Deploy (Uberspace)

Follows https://blog.lars-richter.dev/deploying-net-services-on-uberspace/:

dotnet publish src/TrmnlPlugins.Web -c Release -r linux-x64 --self-contained false \
  -p:PublishReadyToRun=true -o publish
# copy publish/ to ~/bin/trmnl-plugins on Uberspace (config is NOT in publish, so
# this won't touch the server's appsettings.json), then add
#   ~/etc/services.d/trmnl.ini  (see deploy/supervisord.conf), then:
supervisorctl reread && supervisorctl update
uberspace web backend set <your-host> --http --port 5000

The flags precompile for the server to cut cold-start time on Uberspace's shared CPU: PublishReadyToRun compiles IL to native ahead of time (no JIT at startup), which requires a runtime identifier — hence -r linux-x64. Keep --self-contained false so it stays framework-dependent (small, uses the runtime installed on the server). The RID is required and must be linux-x64: ReadyToRun otherwise defaults to the host's RID, so publishing from a Mac without it would build macOS-native images that don't match the server. crossgen2 cross-compiles the linux-x64 images fine from any host, including Apple Silicon (the first such publish downloads the linux-x64 packages). dotnet run/build/test are unaffected — ReadyToRun only applies at publish time.

Config lives on the server, next to the binary in ~/bin/trmnl-plugins. First deploy only: create ~/bin/trmnl-plugins/appsettings.json there with your production settings (the app reads it from the content root at startup). Later deploys leave it in place. Prefer a clean publish folder (rm -rf publish first) so stale files from an earlier build — including any old appsettings*.json — don't get copied up.

The web backend terminates HTTPS (Let's Encrypt) and proxies to the local port, so the app binds http://localhost:5000. TZ=Europe/Berlin is required for correct German dates and is set in the supervisord program.

TRMNL setup

For each plugin, set its Polling URL to the new path and paste the matching Liquid:

  • Garbage: https://<host>/garbage — templates/garbage/markup_full.liquid and templates/garbage/markup_half_vertical.liquid; refresh every 12h.
  • Timetable: https://<host>/timetable — templates/timetable/markup_full.liquid and templates/timetable/markup_half_vertical.liquid (kids stacked instead of side by side; designed for the TRMNL X); refresh every 12h.
  • Quotes: https://<host>/quote — templates/quotes/template.liquid; refresh on your desired interval.
  • Tagesschau: https://<host>/tagesschau — templates/tagesschau/markup_full.liquid and templates/tagesschau/markup_half_vertical.liquid; refresh every 1–2h. Add TRMNL form fields for region/ressort and reference them in the polling URL, e.g. https://<host>/tagesschau?regions={{ regions }}&ressort={{ ressort }}.

Append ?token=… to the URL if you configured that feature's AccessToken. Use each templates/<plugin>/sample-response.json as the sample data in the TRMNL editor.