Daumen im Wind (Linux-Magazin, Mai 2026)

Mike Schillis neue Wetterstation auf dem Hausdach liefert stetig Werte für Sonne, Wind und Regen. Zeit für ein eigenes Auswertungsprogramm.

Wetterstationen und -sensoren gibt es gar viele, aber die von Ecowitt hat den Vorteil, dass deren Messwerte nicht nur auf der hauseigenen App formschön erscheinen, sondern sich roh im Json-Format vom Webserver des Hubs abholen lassen. So kann der Hobbyist die Daten auch zur privaten Nutzung extrahieren.

Abbildung 1: Der Sensor auf dem Hausdach funkt Messwerte an den Hub.

Der Wetterfühler Wittboy WS90 (Abbildung 1) wird dazu im Freien montiert, am besten auf dem Hausdach, damit Regen ungehindert darauf fällt und Wind ungestört durchpfeifen kann. Er benötigt keinerlei Stromversorgung, da er mit zwei Mignon-Batterien als Puffer auskommt und nebenzu einen internen Akku über ein integriertes Solar-Panel auflädt. Der Fühler kommt völlig ohne mechanische Teile aus. Regen misst er mit Piezo-Elementen und die Windrichtung und -stärke mittels Ultraschall-Sensoren. Optional lässt sich der Fühler im Winter beheizen, damit darauf fallender Schnee wegschmilzt und nicht die Sensoren blockiert. Dazu ist dann allerdings eine Stromversorgung nötig.

Abbildung 2: Der Hub in der Wohnung steht in drahtlosem Kontakt mit dem Fühler und hängt im LAN.

Über eine drahtlose Verbindung gelangen die Messdaten zum Hub GW2000, der in der Wohnung am Strom und am Netzwerk hängt (Abbildung 2). Etwa alle 10 Sekunden holt der Hub neue Messwerte vom Fühler ein und schickt sie übers Internet an den Ecowitt-Server. Dort zapft die Ecowitt-App an und stellt die aggregierten Daten grafisch ansprechend dar (Abbildung 3 und Abbildung 4).

Abbildung 3: Die offizielle Ecowitt-Smartphone-App zeigt Temperaturen ...

Abbildung 4: ... sowie Windstärke, -richtung und Regenmenge an.

Abbildung 5: Wettersensor und Hub als Produkte auf Amazon

Wetter-Untergrund

Außerdem lässt sich der Empfänger leicht mit dem "Weather Underground"-Netzwerk ([2]) verbinden, einer Firma, die die Daten von tausenden von privaten Wetterstationen rund um den Globus sammelt und anzeigt (Abbildung 6 und [3]). So kann die ganze Welt die publizierten Wetterdaten einsehen, und individuelle Betreiber der Wetterfühler müssen sich nicht um die Darstellung kümmern, und leisten noch dazu einen Beitrag für die Allgemeinheit.

Abbildung 6: Die Wetterstation des Autors auf Wunderground

Konvertiert mit Muskelschmalz

Die frisch eingetrudelten Messdaten vom Fühler gibt der Hub allerdings auf Anfrage auch über einen eingebauten Webserver aus. Ein simpler HTTP-Request an die Hub-IP unter dem Pfad /get_livedata_info lässt den Hub diedie Daten im Json-Format ausspucken (Abbildung 7). Was da genau ankommt, ist zwar nicht offiziell dokumentiert aber mit etwas Reverse-Engineering lassen sich die hexadezimalen IDs der Sensoren sinnvollen Namen zuordnen.

Abbildung 7: Die API des Hubs liefert Messwerte im Json-Format

So meldet der Hub zum Beispiel unter dem Json-Pfad "common_list/0x02" die gemessene Außentemperatur und unter "wh25/intemp" die im Bereich des Hubs an der Wand gemessene Innentemperatur, jeweils in der in den USA üblichen Temperatureinheit Fahrenheit. Die Windgeschwindigkeit kommt in Meilen pro Stunde zurück und die Regenmenge in Inches, aber das ist alles leicht konvertierbar. Auf Github hat sich eine gute Seele (hust!) die Mühe gemacht, mit github.com/mschilli/go-ecowitt-hub-api ein simpleres Interface darüber zu legen.

Sicherheit scheint man bei Ecowitt übrigens klein zu schreiben, der Hub beherrscht weder SSL noch ist anfangs ein Passwort gesetzt. Ohne Verschlüsselung, mit Plaintext-HTTP wäre dies auch relativ sinnlos. Bleibt nur, den Hub auf einem als unsicher markierten LAN zu installieren.

Schnell im Terminal

Wie wäre es, die aktuellen Wetterdaten in der Unix-Shell anzuzeigen, damit der fleißige Programmierer nicht aufstehen oder gar das Tippterminal verlassen muss? Abbildung 8 zeigt die aktuell vom Hub eingelesenen Wetterdaten in einer Bubbletea-Terminal-UI, die sich alle 10 Sekunden selbständig auffrischt. Draußen (wo der Wetterfühler montiert ist) herrschen demnach gerade 15.1 Grad und eine Luftfeuchtigkeit von 92%. Als Innentemperatur zeigt der Hub 21.7 Grad Celsius an bei 61% Luftfeuchtigkeit, da schwitzt der Autor wohl gerade Blut und Wasser beim Verfassen seiner Kolumne.

Abbildung 8: Die Terminal-UI zeigt die aktuell gemessenen Werte

Draußen weht ein leichtes Lüfterl mit 4km/h von Norden (354 Grad). Es regnet nicht (wie oft der Fall in Kalifornien), aber die Spalte zu "Monthly Rain" zeigt mit 137.9mm an, dass es diesen Monat tatsächlich schon Niederschlag gab. Der absolute barometrische Luftdruck liegt bei 764 mmHg, der relative wurde weggelassen, da sich dieser nur in höheren Lagen unterscheidet, während San Francisco fast auf Meeresebene liegt.

Interessant ist auch noch der Taupunkt, als "Dew Point" mit 13.8 Grad Celsius eingezeichnet. Das ist die Temperatur, bei der die Feuchtigkeit in der Luft beim gegenwärtigen Luftdruck kondensieren würde, es also Nebel oder Tau gibt. Und ein Blick aus dem Fenster zeigt tatsächlich, dass noch einige morgendliche Nebelschwaden herumhängen. Die "Solar Radiation" mit 360 W/m2 und der "UV Index" mit 3 zeigen, dass die Sonne drauf und dran ist, die Wolkendecke aufzureißen. Hurra, ein neuer Tag voller Tatendrang!

UI mit Murmeltee

Die Go-Implementierung der Terminal-UI zeigen die Listings 1 und 2. Neue Sensordaten vom Hub liest Listing 1 mit SensorData() ab Zeile 5 ein. Die lange Liste von Konstanten (alles numerische Werte im Paket von Github) gibt an, welche Sensorwerte herauszufieseln sind. Dank FetchAsRows() kommen sie als Stringpaare mit lesbaren Sensornamen und zugehörigen Messwerten samt Einheiten an und Zeile 29 braucht sie nur ans Hauptprogramm von Listing 2 zurückzugeben.

Der Client muss die IP-Adresse des Hubs wissen, um dessen Webserver anzuzapfen. Leider gibt die Ecowitt-App diese nicht heraus, aber mit einem nmap -sn 192.168.1.0/24 auf dem LAN (oder WLAN) sollte das neu installierte Gerät und die ihm vom Router per DHCP zugewiesene IP mit auflisten.

Listing 1: wuidata.go

    01 package main
    02 import (
    03   ew "github.com/mschilli/go-ecowitt-hub-api"
    04 )
    05 func SensorData() [][]string {
    06   cli := ew.NewClient("192.168.133.184")
    07   rows, err := cli.FetchAsRows([]int{
    08     ew.Temperature,
    09     ew.IndoorTemperature,
    10     ew.Humidity,
    11     ew.IndoorHumidity,
    12     ew.DewPoint,
    13     ew.WindSpeed,
    14     ew.WindGust,
    15     ew.WindGustMaxToday,
    16     ew.SolarRadiation,
    17     ew.UVIndex,
    18     ew.WindDirection,
    19     ew.Rain,
    20     ew.RainRate,
    21     ew.RainDaily,
    22     ew.RainMonthly,
    23     ew.PressureAbsolute,
    24     ew.PressureDelta,
    25   })
    26   if err != nil {
    27     panic(err)
    28   }
    29   return rows
    30 }

Das Paket bubbletea von Github übernimmt die Darstellung der UI im Terminal. Dabei gliedert es den Code in drei Komponenten: Das "Model" definiert das Datenmodell hinter der Anzeige, im vorliegenden Fall enthält es ein Array rows, mit zwei Strings pro Element, für den Namen des Sensors und den zugehörigen Messwert. Die zweite Komponente ist die Update()-Funktion ab Zeile 23, die immer dann anspringt, wenn etwas außergewöhnliches passiert, wie dass der User die Q-Taste zum Abbruch drückt, oder der 10-Sekunden-Timer abläuft, und es Zeit zum Auffrischen der Anzeige mit neuen Messwerten ist.

Listing 2: wui.go

    01 package main
    02 import (
    03   "fmt"
    04   "strings"
    05   "text/tabwriter"
    06   "time"
    07   tea "github.com/charmbracelet/bubbletea"
    08   "github.com/charmbracelet/lipgloss"
    09   ew "github.com/mschilli/go-ecowitt-hub-api"
    10 )
    11 type tickMsg time.Time
    12 type model struct {
    13   rows [][]string
    14 }
    15 func tickCmd() tea.Cmd {
    16   return tea.Tick(10*time.Second, func(t time.Time) tea.Msg {
    17     return tickMsg(t)
    18   })
    19 }
    20 func (m model) Init() tea.Cmd {
    21   return tickCmd()
    22 }
    23 func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    24   switch msg := msg.(type) {
    25   case tickMsg:
    26     m.rows = SensorData()
    27     return m, tickCmd()
    28   case tea.KeyMsg:
    29     if msg.String() == "q" || msg.String() == "ctrl+c" {
    30       return m, tea.Quit
    31     }
    32   }
    33   return m, nil
    34 }
    35 func (m model) View() string {
    36   var b strings.Builder
    37   blue := lipgloss.NewStyle().Foreground(lipgloss.Color("12"))
    38   grey := lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
    39   title := lipgloss.NewStyle().
    40     Bold(true).
    41     Render("Ecowitt Weather Dashboard")
    42   header := grey.Render(
    43     "Press q to quit • Auto-refresh 10s")
    44   fmt.Fprintf(&b, "%s\n%s\n\n", title, header)
    45   w := tabwriter.NewWriter(&b, 0, 0, 2, ' ', 0)
    46   for _, row := range m.rows {
    47     val, unit := ew.ToMetric(row[1], row[2])
    48     fmt.Fprintf(w, "%s\t%s\n", row[0],
    49       blue.Render(val+" "+unit))
    50   }
    51   w.Flush()
    52   return b.String()
    53 }
    54 func main() {
    55   m := model{rows: SensorData()}
    56   _, err := tea.NewProgram(m).Run()
    57   if err != nil {
    58     panic(err)
    59   }
    60 }

Die dritte Komponente stellt die Funktion View() ab Zeile 35, die das neueste Datenmodell zur Anzeige bringt. Für schön formatierte Textspalten nutzt es text/tabwriter aus dem Standardfundus. Und da die Messdaten vom Hub in empirischen Größen wie Fahrenheit oder Meilen pro Stunde daherkommen, konvertiert ToMetric() sie in metrische Formate wie Grad Celsius oder km/h. Verwendete Fontfarben stellt das Paket lipgloss dar, falls das Terminal ANSI-Colors beherrscht.

Init, Update, View

Der zeitliche Ablauf der Applikation beginnt mit Run() im Hauptprogramm in Zeile 56. Definitionsgemäß springt das bubbletea-Paket sofort die am model hängende Funktion Init() in Zeile 20 an. Das Programm soll in einer Endlosschleife laufen und in vorgegebenen Intervallen von 10 Sekunden seine Anzeige mit neu eingeholten Daten auffrischen. Dazu gibt Init() ein Ticker-Kommando zurück, mit einer Funktion, die alle 10 Sekunden Alarm schlägt. Die zugehörige Nachricht reicht die Bubbletea-Zentrale nach dem Eintreffen an Update() ab Zeile 23 weiter.

Dort stellt der Selektor switch fest, dass ein Timer-Event eingetroffen ist und holt mit SensorData() in Zeile 26 die neuesten Wetterdaten vom Hub, und legt sie in rows in der model-Struktur ab. Nach Update() springt die bubbletea-Eventschleife die Funktion View() ab Zeile 35 an, die die Anzeige entsprechend der Änderungen am Modell auffrischt.

Dass die Applikation nach dem Programmstart nicht 10 Sekunden tatenlos herumhängt, bis der Timer zum ersten Mal abläuft, liegt übrigens daran, dass Zeile 55 im Hauptprogramm vor dem Start der Eventschleife schon mal das model zum ersten Mal mit Sensordaten füllt. So hat der erste Aufruf von View() frisches Futter zur Anzeige, und nach 10 Sekunden kommt Update() mit einer Timer-Message zum Zug, was wiederum einen Rendervorgang mit View() auslöst.

Der übliche Dreisprung go mod init wui; go mod tidy; go build; go wui.go wuidata.go linkt alles zu einem Binary wui zusammen, das sich unter diesem knackigen Namen von überall aufrufen lässt, falls es sich im PATH der Shell befindet.

Barometer, gestern und heute

Was den Luftdruck angeht, so zeigt ein "Tief" Schlechtwetter an (sprich: Regen) und ein "Hoch", dass die Sonne scheint. Wichtig ist hier aber die Tendenz: steigt oder fällt der Druck, wird's schöner oder grausliger?

Noch im letzten Jahrhundert waren in Privatwohnungen mechanische Barometer üblich, die den Luftdruck mit einer Membran erfassten und mittels eines Zeigers auf einer runden Skala anzeigten. Um die Druckdifferenz zum Vortag zu erfassen, drehte man an einem Schräubchen in der Mitte des Uhrglases, um den angezeigten Wert mit einem Stellzeiger zur Deckung zu bringen. Schritt man dann tags darauf zum Barometer, klopfte man mit dem Fingernagel leicht ans Uhrglas, und in welche Richtung der so aus seiner Trägheit entlassene mechanische Zeiger unter dem Stellzeiger herausschlüpfte, gab die Tendenz des Luftdrucks an, der besseres oder schlechteres Wetter verhieß. In unserem Atomzeitalter genügt freilich ein Blick auf den Pressure-Graphen über die Zeit, den die Ecowitt-App formschön zeichnet (Abbildung 9).

Abbildung 9: Schon die offizielle App zeigt augenschmeichlerische Temperatur- und Feuchtigkeitsgraphen.

Windige Angelegenheit

Wo die App aber noch Nachholungsbedarf zeigt, ist die Darstellung der Windrichtung und -geschwindigkeit. Mir schwebt eine grafische Animation nach in Abbildung 10 vor, die eine rote Windfront mit entsprechender Geschwindigkeit und Winkel über eine Landkarte sausen lässt.

Abbildung 10: Der Wind bläst gerade von Südwesten nach Nordosten

Die Windrichtung liefert der Sensor in Winkelgraden von 0 bis 360 und Listing 4 holt außerdem noch die gemessene Geschwindigkeit in km/h ab. Gibt der Sensor eine Windrichtung von 0 Grad an, bläst der Wind von Norden, bei 90 Grad herrscht Ostwind.

Listing 3: data.go

    01 package main
    02 import (
    03   "strconv"
    04   ew "github.com/mschilli/go-ecowitt-hub-api"
    05 )
    06 func Update() (int, float64) {
    07   cli := ew.NewClient("192.168.1.123")
    08   rows, err := cli.FetchAsRows([]int{ew.WindSpeed, ew.WindDirection})
    09   if err != nil {
    10     panic(err)
    11   }
    12   speed, _ := strconv.ParseFloat(rows[0][1], 64)
    13   angle, _ := strconv.Atoi(rows[1][1])
    14   return angle, speed
    15 }

Listing 4 holt mit dem Fyne-Paket zunächst eine nach Norden ausgerichtete Landkarte mit dem Fühlerstandort aus der Datei sf.png und zeigt sie in einem Canvas-Widget an. Zur Animation der roten Windfront bietet Fyne sogar eine eigene Komponente, die NewAnimation() in Zeile 79 initialisiert.

Listing 4: wind.go

    01 package main
    02 import (
    03   "image/color"
    04   "math"
    05   "time"
    06   "fyne.io/fyne/v2"
    07   "fyne.io/fyne/v2/app"
    08   "fyne.io/fyne/v2/canvas"
    09   "fyne.io/fyne/v2/container"
    10 )
    11 type windLine struct {
    12   line       *canvas.Line
    13   x, y       float32
    14   vx, vy     float32
    15   length     float32
    16   width      float32
    17   height     float32
    18   lastUpdate time.Time
    19 }
    20 func (m *windLine) setMotion(angleDeg float64, speed float64) {
    21   rad := (angleDeg + 90) * math.Pi / 180
    22   m.vx = float32(speed * 10 * math.Cos(rad))
    23   m.vy = float32(speed * 10 * math.Sin(rad))
    24 }
    25 func (m *windLine) step() {
    26   now := time.Now()
    27   dt := float32(now.Sub(m.lastUpdate).Seconds())
    28   m.lastUpdate = now
    29   m.x += m.vx * dt
    30   m.y += m.vy * dt
    31   if m.x < 0 {
    32     m.x += m.width
    33   }
    34   if m.x > m.width {
    35     m.x -= m.width
    36   }
    37   if m.y < 0 {
    38     m.y += m.height
    39   }
    40   if m.y > m.height {
    41     m.y -= m.height
    42   }
    43   dx1 := float32(0)
    44   if m.vx != 0 {
    45     dx1 = -m.vy * m.width / m.vx
    46   }
    47   dy1 := m.width
    48   dx2 := float32(0)
    49   if m.vx != 0 {
    50     dx2 = m.vy * m.height / m.vx
    51   }
    52   dy2 := -m.height
    53   m.line.Position1 = fyne.NewPos(m.x+dx1, m.y+dy1)
    54   m.line.Position2 = fyne.NewPos(m.x+dx2, m.y+dy2)
    55   m.line.Refresh()
    56 }
    57 func main() {
    58   a := app.New()
    59   w := a.NewWindow("Wind Line")
    60   w.Resize(fyne.NewSize(800, 600))
    61   bg := canvas.NewImageFromFile("sf.png")
    62   bg.FillMode = canvas.ImageFillStretch
    63   bg.Resize(fyne.NewSize(800, 600))
    64   line := canvas.NewLine(color.RGBA{255, 0, 0, 255})
    65   line.StrokeWidth = 10
    66   content := container.NewWithoutLayout(bg, line)
    67   w.SetContent(content)
    68   m := &windLine{
    69     line:       line,
    70     x:          400,
    71     y:          300,
    72     length:     200,
    73     width:      800,
    74     height:     600,
    75     lastUpdate: time.Now(),
    76   }
    77   angle, speed := Update()
    78   m.setMotion(float64(angle), speed)
    79   anim := fyne.NewAnimation(time.Second, func(_ float32) {
    80     m.step()
    81   })
    82   anim.RepeatCount = -1 // infinite
    83   anim.Start()
    84   go func() {
    85     for {
    86       time.Sleep(10 * time.Second)
    87       angle, speed := Update()
    88       m.setMotion(float64(angle), speed)
    89     }
    90   }()
    91   w.ShowAndRun()
    92 }

Die Zeitdauer der Animation gibt der erste Parameter mit einer Sekunde an, aber das ist in Wahrheit nur die Zeitspanne des Animations-Zyklus und der zugehörige Callback wird viel öfter aufgerufen. Da der RepeatCount in Zeile 82 auf -1 gesetzt wird, wiederholt sich die Animation endlos. Die pro Durchgang im Callback aufgerufene Step-Funktion step() ist ab Zeile 25 definiert und berechnet die aktuelle Lage der Windfront anhand der seit dem letzten Frame verstrichenen Zeit und der gemessenen Windgeschwindigkeit und -richtung.

Abbildung 11: Berechnung der Windfront anhand der Windgeschwindigkeit

Wie in Abbildung 11 illustriert, merkt sich der Algorithmus jeweils den zentralen Punkt der roten Geraden und rechnet anhand der aktuellen Windgeschwindigkeit aus, in welche Richtung sich die Front vorwärts schiebt. Die Endpunkte der Geraden pos1 und pos2 ergeben sich geometrisch aus dem Winkel des Bewegungsvektors. Das Verhältnis von Vy zu Vx ist hier gleich dem Verhältnis von dx1 zu dy1 beziehungsweise dx2 zu dy2. Die Y-Werte ergeben sich aus einer großzügig gestalteten Länge der Geraden, sodass diese mindestens zum Bildrand reicht. Meist schießt sie darüber, aber Fyne schneidet den Überschuss praktischerweise ohne Fissematenten ab. Falls der zentrale Punkt der Geraden allerdings über die Bildfläche hinaus entschlüpfen will, prüfen dies die if-Statements in den Zeilen 31 bis 42 und bugsieren den Cursor wieder zum Bildrand zurück.

Kompiliert wird die App mit go build wind.go data.go, auch wieder nach dem weiter oben erläuterten Zweisprung, der die benötigten Zusatzpakete von Github abholt. Das Ergebnis ist eine äußerst flüssige Animation ohne jegliches Ruckeln, da Fyne das Canvas-Widget so schnell auffrischt, dass das menschliche Auge den Einzelschritten nicht folgen kann.

Infos

[1]

Listings zu diesem Artikel: http://www.linux-magazin.de/static/listings/magazin/2026/05/snapshot/

[2]

Weather Underground, https://en.wikipedia.org/wiki/Weather_Underground_(weather_service)

[3]

Wetterstation des Autors auf Weather Underground, https://www.wunderground.com/dashboard/pws/KCASANFR2259

Michael Schilli

arbeitet als Software-Engineer in der San Francisco Bay Area in Kalifornien. In seiner seit 1997 laufenden Kolumne forscht er jeden Monat nach praktischen Anwendungen verschiedener Programmiersprachen. Unter mschilli@perlmeister.com beantwortet er gerne Ihre Fragen.