Wie aus einem ausgemisteten Raspberry Pi unser Familienkalender wurde


Es gibt diese Tage, an denen man im Keller eine Schublade aufmacht und denkt: Wer hat das alles gekauft? Ich war's. Ich war das alles. Zwischen einem halben USB-C-Hub, drei nicht zueinander passenden HDMI-Adaptern und einer noch verschweißten Lochrasterplatine lag er dann — ein Raspberry Pi 3. Daneben, ebenso unschuldig, ein offizielles 7-Zoll-Touchdisplay. Originalverpackung, Folie noch dran. Ich habe das mal gekauft, weil es für ein tolles Projekt brauchte. Welches genau, weiß ich nicht mehr.
Und genau in dem Moment, in dem man sowas findet, hat man immer ein Problem, das man eigentlich seit Monaten ignoriert. Bei uns: der Familienkalender.
Das Problem, das keiner haben will
Familienkalender sind seltsam. Es gibt sie als App, im Browser, als Whiteboard mit lustigen Magneten, als geteilten Google-Kalender, als Notion-Workspace mit drei Spalten und KI-Workflow. Die haben alle ein gemeinsames Problem: die Hürde, einen Termin einzutragen, ist zu hoch. App öffnen. Einloggen. Auf Plus tippen. Datum auswählen. Zeit auswählen. Titel tippen. Kategorie. Erinnerung. Speichern. Bestätigen.
Mir sind die alle zu kompliziert - Ich wollte ein System, bei dem man eine Mail schreibt mit dem Subject:
Pia Zahnarzt 22.5. 14:30
Fertig. Keine App. Kein Login. Kein Workflow. Du tippst das Subject ins Mailprogramm, das du sowieso schon offen hast, drückst auf Senden, und der Termin steht. Das wars. Die Mail braucht nicht mal einen Body.
Genau dafür war der Rasberry auf einmal nicht mehr Ausmist-Material, sondern Ausgangspunkt.


Der Plan

Das Ding soll an der Wand hängen, eine analoge Uhr zeigen, und daneben eine Liste „heute" und „morgen" mit den anstehenden Terminen. Drauf laufen soll eine Blazor-Server-App in .NET 9, weil ich mit dem Stack mein Geld verdiene und die Lernkurve gerade hier null ist. Im Hintergrund ein kleiner Worker, der alle paar Minuten ein IMAP-Postfach abfragt, eingehende Mails parst und neue Termine in eine SQLite anlegt. Drei Wege, einen Termin einzutragen:
1.    .ics-Anhang. Wer aus iOS-Kalender oder Outlook einen Termin „teilt" und an die Familienadresse schickt, hat eine standardisierte Einladung im Anhang. Parsen, fertig.
2.    Subject-Konvention. Der oben beschriebene Schnellschuss. Wer, was, wann — Heuristik mit deutscher Locale.
3.    (später vielleicht ein LLM für Freitext und das „sexy“ wird. Aber erst, wenn 1 und 2 nicht reichen.)
Und weil so ein Display nicht 24/7 leuchten muss, dazu ein PIR-Bewegungssensor. Niemand im Raum, Display aus. Jemand kommt rein, Display an. Nicht weil's Strom spart (ein paar Watt), sondern weil ein dauerleuchtendes Display im Wohnzimmer einfach nervt.
Soweit der Plan. Realität: ein bisschen anders.
Die Sache mit dem braunen Riegel
Den Pi auspacken, das Display auspacken, das Flachbandkabel zwischen die beiden — und dann sitzt man vor diesem winzigen ZIF-Stecker und überlegt, ob die braune Klappe nun nach oben oder nach hinten geht. Beim ersten Versuch will man sie rausziehen. Beim zweiten draufdrücken. Beim dritten merkt man, dass sie an einer Seite angeschanniert ist und nach hinten weggeklappt werden will. Mit Fingernägeln. Vorsichtig. Wer hier mit einem Schraubenzieher hebelt, hat ein 50-Euro-Display gegen ein 20-Euro-Adapterboard mit kaputtem Stecker eingetauscht.
Geschafft. Kabel rein, blaue Lasche nach oben, Klappe wieder zu. Drei Jumperkabel von der Adapterplatine auf die GPIO-Leiste — VCC auf Pin 4, GND auf Pin 6 — und das Display geht an. Pinker Hintergrund. „No signal" auf Englisch.

Weil noch ein Betriebssystem fehlt.

Windows IoT, kurz angedacht
Pi OS Lite plus .NET 9 ist 2026 das richtige Stack.
Also Raspberry Pi Imager, Pi OS Lite 64-bit, SSH aktivieren — und dann doch eine Stunde verschwenden, weil ich SSH genau nicht aktiviert habe. Mit einem leeren ssh-File auf der Boot-Partition aber easy zu retten, ohne nochmal flashen zu müssen.


.NET will einfach nicht runter

Was dann folgte, war der nervigste Teil. .NET 9 SDK installieren, 213 MB Download. Beim ersten Versuch hat das offizielle Install-Script versucht, über /tmp zu entpacken — und das ist auf Pi OS ein tmpfs mit 453 MB. Klassisches „no space left", obwohl die SD-Karte 9 GB frei hatte.
Lösung: manuell wget und direkt entpacken. Erster Versuch: 70% Download, dann WLAN-Wackler, abgebrochen. Zweiter Versuch: 70%, wieder abgebrochen. Dritter Versuch: Pi mit Netzwerkkabel angeschlossen, in 30 Sekunden durch, ein einziges Mal sauber entpackt. Note to self: Bei größeren Downloads auf dem Pi 3 immer LAN. Das WLAN-Modul ist okay, aber nicht großartig.
Dann die Sache mit der Display-Rotation. Das Display wollte ich später ins Gehäuse einbauen, und dabei steht das Bild kopf. Kernel-Parameter video=DSI-1:panel_orientation=upside_down in die cmdline.txt — und prompt bootet der Pi nicht mehr. Weil ich versehentlich ein Zeilenumbruch reingehauen habe. Wichtig zu wissen: cmdline.txt ist eine einzige Zeile, mit Leerzeichen getrennt, kein Semikolon, kein Newline, sonst Boot kaputt.
SD-Karte raus, im Hauptrechner editiert, zurück in den Pi. Zweiter Versuch, alles in einer Zeile, bootet, Bild ist gedreht. Nur das Display selbst ist plötzlich superdunkel. Helligkeit nachgeschaut: 12 von 255. Keine Ahnung wo das herkommt, aber echo 255 > /sys/class/backlight/*/brightness hat's gerichtet.
Übrigens: vcgencmd display_power 0/1, der klassische Befehl zum Backlight-Schalten, funktioniert auf Bookworm mit KMS-Treiber nicht mehr. Liefert display_power=-1 und lacht dich aus. Stattdessen direkt nach /sys/class/backlight/.../bl_power schreiben, 0 ist an, 1 ist aus, weil historische Gründe. Wenn man's einmal weiß, ist's leicht.


Der eigentliche Code

Die Blazor-App ist überraschend kompakt. Eine SVG-Uhr-Komponente mit 60 Sekundenpunkten außenrum, die sich pro Minute füllt und dann die Farbe wechselt — rot, gelb, rot, gelb — und in der Mitte digital die Uhrzeit. Sieht aus wie eine Uhr, die langsam atmet.
Im Hintergrund läuft ein BackgroundService, der per MailKit ein IMAP-Postfach abfragt. Findet er einen .ics-Anhang, parst er den mit Ical.Net. Findet er keinen, versucht ein Regex/Parser-Service das Subject zu zerlegen: Wer ist das erste Wort, dann sucht er nach einem Datum (relativ wie „morgen" oder absolut wie „22.5."), dann nach einer Uhrzeit. Klappt es, kommt der Termin in die SQLite und der Absender bekommt eine Bestätigungsmail zurück. Klappt es nicht, kriegt er eine kurze Format-Hilfe.
Der PIR-Sensor (HC-SR501, der Klassiker) hängt an GPIO17 und feuert ein Rising-Event bei Bewegung. Ein zweiter BackgroundService hört darauf, weckt das Display, setzt einen Timer. Drei Minuten ohne Aktivität → Backlight aus. Zwischen 22 und 6 Uhr eh dauerhaft aus, das Wohnzimmer braucht nachts keinen Familienkalender.
Wenn man schon dabei ist
Sobald das System lief, fielen mir natürlich Dinge auf, die da auch noch reinkönnten. Zwei haben es geschafft: die nächsten Busse von der Haltestelle „Feldkirch Finanzamt" — zweihundert Meter vor der Haustür — und das Wetter, also was anziehen und Regenschirm ja oder nein. Beide Sachen, für die man sonst zwei verschiedene Apps öffnet oder kurz googelt. Auf einem Familien-Display gehören sie hin.
Bus-Abfahrten von vmobil
vmobil.at hat keine öffentliche API mit Anmeldeformular und API-Key. Die cleVVVer-mobil-App spricht aber natürlich mit irgendwas. Ein Blick in die Open-Source-Bibliothek hafas-client auf GitHub verrät: vmobil nutzt HAFAS — dasselbe System wie ÖBB und VOR. Der Endpoint ist https://fahrplan.vmobil.at/hamm/gate, die Auth-AID, das Salt und die Protokollversion liegen im vvv-Profil der Library offen. Alles legal, alles dokumentiert, nur nicht offiziell beworben.
HAFAS-Requests sind POSTs mit JSON-Body und einer MIC/MAC-Signatur in den Query-Parametern:
var mic = MD5.HashData(bodyBytes);
var mac = MD5.HashData(mic_hex_utf8 + saltBytes);
// URL: https://fahrplan.vmobil.at/hamm/gate?mic=&mac=
Der Body enthält ein svcReqL mit StationBoard-Operation, dem Stop-Identifier A=1@L=480127100@ (das ist Feldkirch Finanzamt — intern bei HAFAS heißt die Haltestelle „Levis Finanzamt", was außerhalb von HAFAS niemand so nennt) und einem 30-Minuten-Fenster.
Die Response liefert pro Abfahrt dTimeS (Soll), dTimeR (Ist mit Verspätung), dirTxt (fertig formatierter Richtungstext) und einen Index in prodL für den Liniennamen — „Landbus 445", „Stadtbus 401". Eine zweite Methode LocMatch macht Stop-Lookup per Klartext, sodass im Config nur der Anzeige-Name steht und nicht die kryptische ExtId.
Wetter von Open-Meteo
Beim Wetter war die Entscheidung schneller getroffen. Open-Meteo bietet keine Anmeldung, keinen API-Key, ist auch kommerziell kostenlos, und nutzt unter der Haube die offiziellen Modelle (DWD ICON, ECMWF). Ein einziger GET reicht:
https://api.open-meteo.com/v1/forecast
  ?latitude=47.2411&longitude=9.6062
  &daily=weather_code,temperature_2m_max,temperature_2m_min,
         precipitation_sum,precipitation_probability_max
  &timezone=Europe/Vienna&forecast_days=2
Aus den Rohwerten leiten wir zwei für die Familie nützliche Aussagen ab:
RegenAmpel = (RegenWahrscheinlichkeit >= 50 || NiederschlagMm >= 1.0)
    ? "Regen" : "trocken";

AnziehTipp = TempMax switch
{
    < 5  => "Wintermantel",
    < 10 => "Mantel",
    < 15 => "Jacke",
    < 20 => "Pulli",
    _    => "T-Shirt"
};
Auf dem Display steht dann unter „Heute" und „Morgen" jeweils eine Zeile wie „trocken und Pulli" oder „Regen und Jacke", darunter Min/Max-Temperatur und der Wetter-Code als deutscher Text („Regenschauer", „leicht bewölkt", „Gewitter").
Gleiches Muster, zwei Datenquellen
Beide Integrationen folgen exakt demselben Aufbau — und das ist eigentlich die Pointe:
•    Ein Client — ein HttpClient-Wrapper, der via AddHttpClient() registriert wird und genau einen Endpoint kennt.
•    Ein Cache — ein thread-safer Singleton mit der letzten Response und einem Aktualisiert-Event.
•    Ein PollingService — ein BackgroundService, der in konfigurierbarem Intervall pollt und pausiert, wenn das Display aus ist (über IDisplayPower.IsAwake). Nachts und während Idle-Phasen werden weder Pi noch externe API belastet.
•    Eine .razor-Komponente — injiziert den Cache, abonniert das Event, ruft InvokeAsync(StateHasChanged) auf. Live-Update ohne Page-Reload.
Damit ist eine neue Datenquelle — Müllabfuhr-Termine, Strompreise, was auch immer — in unter 200 Zeilen Code dazu. Genau dieser Effekt ist das, was so ein Projekt nach dem ersten Erfolgserlebnis am Leben hält: nicht „ich muss noch", sondern „wäre eigentlich easy". Und dann sitzt du am Samstagvormittag beim Kaffee und integrierst die Müllabfuhr.

Was am Ende rauskommt

Ein gerahmtes 7-Zoll-Display an der Wand. Eine Uhr. Eine Liste mit den nächsten Terminen für heute und morgen. Darunter die Abfahrten der Buslinien, die hier vorbeifahren — minutengenau, mit Verspätung. Daneben das Wetter mit Anzieh-Tipp.
Und das Beste an dem ganzen Ding: jedes Familienmitglied kann jederzeit von überall einen Termin eintragen, indem es eine Mail mit einem dreizeiligen Subject schickt. Meine Töchter aus dem Bus, ich aus der Küche. Keine App, kein Login. Wer das mit einer App schon mal versucht hat, weiß warum das ein Befreiungsschlag ist.
War es das alles wert, für ein bisschen Familienorganisation? Vermutlich nicht. Aber das war auch nicht der Punkt. Der Punkt war, dass im Keller ein Pi lag, der eine Aufgabe brauchte, und im Familienleben eine Aufgabe, die jemanden brauchte. Manchmal passen die zwei Dinge zusammen, und du baust einen Feiertag lang an etwas, das danach jahrelang einfach da hängt und seinen Job macht.
Den nächsten Pi 3 im Keller hebe ich auch wieder auf. Für die nächste Idee, die mir noch nicht eingefallen ist, oder vielleicht für die Idee, die ich hatte wie ich das alles gekauft habe.

Source Code: gpiwonka/familienuhr

Zuletzt bearbeitet: 16.05.2026 18:21