Digitaluhr mit Nordlicht-Hinweis in Python (Kp-Prognose der NOAA)

11. November 2026

Meine Digitaluhr, deren Hintergrund sich nach dem Stand der Sonne färbt, kann jetzt noch etwas: Sie sagt mir, wann in den nächsten 24 Stunden Nordlichter möglich sind. Dafür holt sich das Python-Programm die Prognose der Weltraumwetterbehörde NOAA und verrechnet sie mit der Dunkelheit am Standort. Ein kleines Hobbyprojekt ohne zusätzliche Bibliotheken, das auf jedem Rechner mit Python und Tkinter läuft.

Download als Zip-Datei: Digitaluhr mit Nordlichtanzeige in Pyton

Die Grundlage ist die Uhr, die ich schon länger auf dem Bildschirm laufen lasse: grüne Ziffern, darunter Wochentag und Datum, und ein Hintergrund, der bei Nacht schwarz ist, am Horizont rot und am Tag dunkelblau. Neu ist eine dritte Zeile unter dem Datum. Sie bleibt leer, solange nichts zu erwarten ist. Wenn es sich lohnt, nach draußen zu schauen, steht dort zum Beispiel „Nordlichter von 21:00 bis 03:00 Uhr (Kp 5)“. Läuft die Phase schon, heißt es „Nordlichter möglich bis 03:00 Uhr (Kp 5)“. Ich muss nicht mehr ständig auf irgendwelche Webseiten schauen, die Uhr steht ohnehin im Blickfeld.

Bildschirmfoto zu 2026 11 07 22 17
Die Uhr kündigt Nordlichter in der Nacht an.

Was ist der Kp-Wert? Der planetare Kp-Index beschreibt die Störung des Erdmagnetfeldes auf einer Skala von 0 bis 9. Je höher der Wert, desto weiter reichen die Nordlichter nach Süden. Ab Kp 5 spricht man von einem geomagnetischen Sturm der Stufe G1. Bei mir in Südschweden ist das etwa die Schwelle, ab der man mit etwas Glück und klarem Himmel Nordlichter sehen kann. Die NOAA veröffentlicht eine Prognose in Drei-Stunden-Blöcken als JSON-Datei, die sich ohne Anmeldung abrufen lässt. Genau diese Datei verwendet das Programm.

So funktioniert das Programm: Es besteht im Grunde aus drei Teilen, die unabhängig voneinander arbeiten.

1. Die Uhr und der Sonnenstand: Einmal pro Sekunde wird die Uhrzeit aktualisiert. Gleichzeitig berechnet das Programm aus Datum, Uhrzeit und den geografischen Koordinaten die Höhe der Sonne über dem Horizont. Dafür verwende ich die bekannte Näherungsformel der NOAA mit Zeitgleichung und Deklination der Sonne. Sie ist auf ein bis zwei Zehntelgrad genau, und das reicht für eine Hintergrundfarbe locker aus. Aus der Sonnenhöhe wird die Farbe berechnet. Es gibt drei Stützpunkte: bei minus 6 Grad Schwarz, bei etwa minus 0,8 Grad ein kräftiges Rot und bei plus 10 Grad ein dunkles Blau. Dazwischen wird linear übergeblendet. Das sieht man besonders schön bei Sonnenauf- und -untergang.

2. Die Kp-Prognose: Alle 30 Minuten lädt das Programm die Prognosedatei von der NOAA herunter. Das passiert in einem eigenen Thread, damit die Uhr nicht stehen bleibt, wenn die Verbindung einmal hakt. Schlägt der Abruf fehl, arbeitet das Programm einfach mit den zuletzt geladenen Daten weiter. Das Datenformat der NOAA hat sich in der Vergangenheit geändert. Deshalb versteht die Einleseroutine sowohl das alte Format, bei dem die erste Zeile die Spaltennamen enthält, als auch das neuere mit einer Liste von Objekten. Ich wollte nicht, dass die Uhr nach der nächsten Umstellung plötzlich nichts mehr anzeigt.

3. Der Nordlicht-Hinweis: Einmal pro Minute geht das Programm die nächsten 24 Stunden in Schritten von fünf Minuten durch. Für jeden Zeitpunkt prüft es zwei Bedingungen: Ist der prognostizierte Kp-Wert mindestens 5, und steht die Sonne tiefer als 12 Grad unter dem Horizont? Nur wenn beides zutrifft, ist ein Zeitpunkt „aktiv“. Das erste zusammenhängende Zeitfenster wird als Text ausgegeben, mit dem höchsten Kp-Wert darin. Die Sonnenbedingung ist wichtig, denn ein Kp-Wert von 6 mitten am Tag nützt mir nichts. Im Sommer spielt das bei uns eine große Rolle: Um die Sonnenwende sinkt die Sonne nachts gar nicht tief genug, und die Uhr zeigt dann auch bei hohem Kp nichts an. Das ist keine Fehlfunktion, sondern Physik.

1280px Aurora2
Polarlichter zeigen sich im Gegensatz den farbenfrohen Fotos meistens mit einem schwachen weißen Licht, da das Auge bei schwachem Licht farbenblind ist. ( Bildquelle: NASA https://commons.wikimedia.org/wiki/File:Aurora2.jpg )

Anpassen: Alle wichtigen Einstellungen stehen am Anfang des Programms und lassen sich ohne Programmierkenntnisse ändern:

    • BREITE und LAENGE: die Koordinaten des eigenen Standorts, Nord und Ost positiv. Sie bestimmen sowohl die Hintergrundfarbe als auch die Dunkelheit für den Nordlicht-Hinweis.
    • KP_SCHWELLE: ab welchem Kp-Wert gemeldet wird. Wer weiter südlich wohnt, sollte den Wert erhöhen, wer weiter nördlich wohnt, kann ihn senken.
    • SONNE_MAX: wie tief die Sonne mindestens stehen muss. Bei minus 12 Grad ist die nautische Dämmerung zu Ende. Wer es noch dunkler haben will, nimmt minus 18 Grad.
    • HORIZONT_STUNDEN: wie weit in die Zukunft geschaut wird.
    • FARBPUNKTE: die Farben und Sonnenhöhen für den Hintergrund, falls einem das Rot oder das Blau nicht gefällt.

Zum Ausprobieren: Wenn man die Anzeige testen will, muss man nicht auf einen Sonnensturm warten. Mit TEST_OFFSET_STUNDEN lässt sich die Zeit künstlich verschieben, zum Beispiel um sechs Stunden, und man sieht, wie sich der Hintergrund verändert. Mit TEST_KP tut das Programm so, als wäre immer ein bestimmter Kp-Wert aktiv, zum Beispiel 6. So kann ich prüfen, ob die Textzeile richtig erscheint. Beide Werte stehen im Normalbetrieb auf 0 beziehungsweise None.

Nachfolgend das Python-Script:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import tkinter as tk
import time
import locale
import math
import json
import threading
import urllib.request
from datetime import datetime, timedelta, timezone

# ---------------------------------------------------------------
# Einstellungen
# ---------------------------------------------------------------
BREITE = 58.71    # geografische Breite (Nord positiv)
LAENGE = 15.77    # geografische Länge (Ost positiv)

# Zum Ausprobieren: Zeit künstlich verschieben, z.B. 6 = sechs Stunden später
TEST_OFFSET_STUNDEN = 0

# Nordlicht-Hinweis
KP_SCHWELLE = 5            # ab diesem Kp-Wert wird gemeldet (5 = G1-Sturm)
SONNE_MAX = -12.0          # Sonne muss tiefer als so viele Grad unter dem Horizont stehen
KP_URL = "https://services.swpc.noaa.gov/products/noaa-planetary-k-index-forecast.json"
KP_ABRUF_MINUTEN = 30      # Prognose so oft neu laden
HORIZONT_STUNDEN = 24      # so weit voraus wird geschaut
TEST_KP = None            # z.B. 6: so tun, als wäre immer Kp 6 (nur zum Testen der Anzeige)

# Stützpunkte: (Sonnenhöhe in Grad, (R, G, B))
FARBPUNKTE = [
    (-6.0, (0, 0, 0)),        # Nacht: schwarz
    (-0.8, (170, 25, 10)),    # Horizont: rot
    (10.0, (5, 20, 110)),     # Tag: dunkelblau
]


def sonnenhoehe(breite, laenge, zeit_utc):
    """Sonnenhöhe über dem Horizont in Grad (NOAA-Näherung)."""
    tag = zeit_utc.timetuple().tm_yday
    stunden = zeit_utc.hour + zeit_utc.minute / 60 + zeit_utc.second / 3600
    g = 2 * math.pi / 365 * (tag - 1 + (stunden - 12) / 24)

    zeitgleichung = 229.18 * (0.000075 + 0.001868 * math.cos(g)
                              - 0.032077 * math.sin(g)
                              - 0.014615 * math.cos(2 * g)
                              - 0.040849 * math.sin(2 * g))
    deklination = (0.006918 - 0.399912 * math.cos(g) + 0.070257 * math.sin(g)
                   - 0.006758 * math.cos(2 * g) + 0.000907 * math.sin(2 * g)
                   - 0.002697 * math.cos(3 * g) + 0.00148 * math.sin(3 * g))

    wahre_sonnenzeit = stunden * 60 + zeitgleichung + 4 * laenge   # Minuten
    stundenwinkel = math.radians(wahre_sonnenzeit / 4 - 180)
    lat = math.radians(breite)

    sin_h = (math.sin(lat) * math.sin(deklination)
             + math.cos(lat) * math.cos(deklination) * math.cos(stundenwinkel))
    return math.degrees(math.asin(max(-1.0, min(1.0, sin_h))))


def hintergrundfarbe(hoehe):
    """Farbe (#rrggbb) passend zur Sonnenhöhe, linear interpoliert."""
    if hoehe <= FARBPUNKTE[0][0]:
        rgb = FARBPUNKTE[0][1]
    elif hoehe >= FARBPUNKTE[-1][0]:
        rgb = FARBPUNKTE[-1][1]
    else:
        rgb = FARBPUNKTE[-1][1]
        for (h1, c1), (h2, c2) in zip(FARBPUNKTE, FARBPUNKTE[1:]):
            if h1 <= hoehe <= h2:
                t = (hoehe - h1) / (h2 - h1)
                rgb = tuple(round(a + (b - a) * t) for a, b in zip(c1, c2))
                break
    return "#%02x%02x%02x" % rgb


# ---------------------------------------------------------------
# Kp-Prognose
# ---------------------------------------------------------------
def lese_kp(daten):
    """Wandelt die NOAA-JSON-Daten in {Slotbeginn_UTC: Kp} um.

    Versteht beide Formate: das alte (erste Zeile = Spaltennamen) und das
    neue (Liste von Objekten mit time_tag / kp).
    """
    if daten and isinstance(daten[0], list):
        kopf = daten[0]
        daten = [dict(zip(kopf, zeile)) for zeile in daten[1:]]
    slots = {}
    for eintrag in daten:
        try:
            t = datetime.strptime(str(eintrag["time_tag"]).replace("T", " ")[:19],
                                  "%Y-%m-%d %H:%M:%S").replace(tzinfo=timezone.utc)
            slots[t] = float(eintrag["kp"])
        except (KeyError, ValueError, TypeError):
            continue
    return slots


def kp_zu(slots, t):
    """Kp-Wert für den Zeitpunkt t (die Slots dauern 3 Stunden)."""
    if TEST_KP is not None:
        return TEST_KP
    schluessel = t.replace(hour=t.hour // 3 * 3, minute=0, second=0, microsecond=0)
    return slots.get(schluessel)


def nordlicht_text(slots, jetzt_utc):
    """Text wie 'Nordlichter von 21:00 bis 03:00 Uhr (Kp 5)' oder ''."""
    if not slots and TEST_KP is None:
        return ""
    schritt = timedelta(minutes=5)
    ende = jetzt_utc + timedelta(hours=HORIZONT_STUNDEN)
    t = jetzt_utc
    start = None
    kp_max = 0.0
    while t <= ende:
        kp = kp_zu(slots, t)
        aktiv = (kp is not None and kp >= KP_SCHWELLE
                 and sonnenhoehe(BREITE, LAENGE, t) < SONNE_MAX)
        if aktiv:
            if start is None:
                start = t
                kp_max = 0.0
            kp_max = max(kp_max, kp)
        elif start is not None:
            return _formatiere(start, t, jetzt_utc, kp_max)
        t += schritt
    if start is not None:
        return _formatiere(start, t, jetzt_utc, kp_max)
    return ""


def _formatiere(start, ende, jetzt_utc, kp_max):
    lokal = lambda z: z.astimezone().strftime("%H:%M")
    kp = ("%g" % kp_max).replace(".", ",")
    if start <= jetzt_utc + timedelta(minutes=5):
        return "Nordlichter möglich bis %s Uhr (Kp %s)" % (lokal(ende), kp)
    return "Nordlichter von %s bis %s Uhr (Kp %s)" % (lokal(start), lokal(ende), kp)


class DigitalClock:
    def __init__(self, root):
        self.root = root
        self.root.title("Digitaluhr")
        self.root.geometry("1024x500")
        self.root.configure(bg="black")
        self.root.resizable(True, True)

        self.kp_slots = {}

        # Setze die Locale für deutsche Wochentagsnamen
        try:
            locale.setlocale(locale.LC_TIME, 'de_DE.UTF-8')  # Für Linux/Mac
        except:
            try:
                locale.setlocale(locale.LC_TIME, 'deu_deu')  # Für Windows
            except:
                pass  # Falls keine deutsche Locale verfügbar ist, Standard benutzen

        # Frame für die Uhranzeige
        self.clock_frame = tk.Frame(root, bg="black")
        self.clock_frame.place(relx=0.5, rely=0.5, anchor="center")

        # Label für die Uhrzeit
        self.time_label = tk.Label(
            self.clock_frame,
            font=('Courier', 140, 'bold'),
            bg="black",
            fg="#00ff00"  # Grüne Schrift
        )
        self.time_label.pack()

        # Label für Wochentag und Datum
        self.date_label = tk.Label(
            self.clock_frame,
            font=('Courier', 40),  # Kleinere Schriftgröße
            bg="black",
            fg="#00ff00"  # Grüne Schrift
        )
        self.date_label.pack(pady=(10, 0))  # Etwas Abstand zur Uhrzeit

        # Label für den Nordlicht-Hinweis
        self.aurora_label = tk.Label(
            self.clock_frame,
            font=('Courier', 30),
            bg="black",
            fg="#7cffb2"  # helles Polarlicht-Grün
        )
        self.aurora_label.pack(pady=(10, 0))

        # Starte die Uhraktualisierung
        self.update_clock()
        self.hole_kp()
        self.update_aurora()

    def update_clock(self):
        # Aktuelle Zeit abrufen im 24-Stunden-Format (HH:MM:SS)
        current_time = time.strftime("%H:%M:%S")

        # Aktuelles Datum mit Wochentag abrufen
        current_date = time.strftime("%A, %d. %B %Y")

        # Label-Texte aktualisieren
        self.time_label.config(text=current_time)
        self.date_label.config(text=current_date)

        # Hintergrundfarbe nach Sonnenstand
        jetzt_utc = datetime.now(timezone.utc) + timedelta(hours=TEST_OFFSET_STUNDEN)
        farbe = hintergrundfarbe(sonnenhoehe(BREITE, LAENGE, jetzt_utc))
        for widget in (self.root, self.clock_frame, self.time_label,
                       self.date_label, self.aurora_label):
            widget.config(bg=farbe)

        # Aktualisierung alle 1000 ms (1 Sekunde)
        self.root.after(1000, self.update_clock)

    def hole_kp(self):
        """Lädt die Kp-Prognose im Hintergrund, damit die Uhr nicht stockt."""
        threading.Thread(target=self._lade_kp, daemon=True).start()
        self.root.after(KP_ABRUF_MINUTEN * 60 * 1000, self.hole_kp)

    def _lade_kp(self):
        try:
            with urllib.request.urlopen(KP_URL, timeout=15) as antwort:
                daten = json.load(antwort)
            slots = lese_kp(daten)
            if slots:
                self.kp_slots = slots     # bei Fehlern bleiben die alten Daten
        except Exception:
            pass

    def update_aurora(self):
        jetzt_utc = datetime.now(timezone.utc) + timedelta(hours=TEST_OFFSET_STUNDEN)
        self.aurora_label.config(text=nordlicht_text(self.kp_slots, jetzt_utc))
        self.root.after(60000, self.update_aurora)   # einmal pro Minute


if __name__ == "__main__":
    root = tk.Tk()
    app = DigitalClock(root)
    root.mainloop()

Was wird gebraucht? Ich habe versucht, die Abhängigkeiten so klein wie möglich zu halten:

  • Python 3: Ich verwende es auf meinem Ubuntu MATE, es läuft aber genauso auf einem Raspberry Pi oder unter Windows. Eine halbwegs aktuelle Version 3 genügt.
    • Tkinter für die Oberfläche. Unter Ubuntu und Raspberry Pi OS ist es nicht immer dabei und wird mit sudo apt install python3-tk nachinstalliert. Unter Windows ist es normalerweise schon in Python enthalten.
    • Nur Module aus der Standardbibliothek: time, locale, math, json, threading, urllib und datetime. Es muss also nichts mit pip installiert werden.
    • Eine Internetverbindung für den Abruf der Kp-Prognose. Ohne Verbindung läuft die Uhr mit der Hintergrundfarbe trotzdem weiter, nur der Nordlicht-Hinweis bleibt leer.
    • Die deutsche Locale (de_DE.UTF-8) für deutsche Wochentage und Monatsnamen. Ist sie nicht installiert, fängt das Programm den Fehler ab und zeigt Datum und Wochentag in der Standardsprache an. Dann hilft sudo locale-gen de_DE.UTF-8.
    • Die Schriftart Courier. Sie ist fast überall vorhanden. Sonst nimmt Tkinter eine Ersatzschrift.
    • Die Systemzeit und die Zeitzone des Rechners müssen stimmen, denn die Uhrzeiten im Hinweis werden in der lokalen Zeit angezeigt.

Grenzen: Das Programm ist eine Hobbylösung und kein Nordlicht-Alarm. Der Kp-Wert ist ein globaler Index und eine Prognose, die sich kurzfristig ändern kann. Ob man tatsächlich etwas sieht, hängt außerdem von der Bewölkung ab, vom Mond, vom Horizont und von der Lichtverschmutzung. Das alles weiß die Uhr nicht. Sie sagt nur: Jetzt wäre die Magnetosphäre aufgeregt genug, und draußen ist es dunkel genug. Der Blick aus dem Fenster bleibt trotzdem nötig. Außerdem verlasse ich mich auf einen Webdienst der NOAA, der sich jederzeit ändern kann. Dann muss das Programm angepasst werden.

Fazit: Mit wenigen hundert Zeilen Python und ohne zusätzliche Bibliotheken ist aus einer simplen Uhr ein kleiner Nordlicht-Wecker geworden. Besonders gefällt mir, dass der Hinweis nur erscheint, wenn er wirklich relevant ist. Die meiste Zeit bleibt die Zeile leer und die Uhr tut einfach, was eine Uhr tun soll.