Mike Schillis neue Wetterstation auf dem Hausdach liefert stetig Werte zu Sonne, Wind und Regen. Zeit für ein eigenes Auswertungsprogramm.
Wetterstationen und -sensoren gibt es viele. Das Modell von Ecowitt besitzt den Vorteil, dass dessen Messwerte nicht nur auf der hauseigenen App formschön erscheinen, sondern sich roh im JSON-Format vom Webserver des Hubs abholen lassen. So extrahiert der Hobbymeteorologe die Daten auch zur privaten Nutzung.

Abbildung 1: Der Sensor auf dem Hausdach funkt Messwerte an den Hub.
Der Wetterfühler Wittboy WS90 (Abbildung 1) arbeitet dazu im Freien, am besten auf dem Hausdach, damit Regen ungehindert darauf fällt und der Wind ungestört durchpfeift. Er benötigt keine Stromversorgung, da er mit zwei Mignon-Batterien als Puffer auskommt und daneben einen internen Akku über ein integriertes Solar-Panel auflädt. Der Fühler kommt ohne mechanische Teile aus. Regen misst er mit Piezo-Elementen und die Windrichtung und -stärke mittels Ultraschallsensoren. Optional lässt sich der Fühler im Winter beheizen, damit Schnee wegschmilzt und die Sensoren nicht blockiert. Dazu braucht es dann allerdings eine Stromversorgung.
Über eine drahtlose Verbindung gelangen die Messdaten zum Hub GW2000 (Abbildung 2), der in der Wohnung am Stromnetz und am Netzwerk hängt. Etwa alle 10 Sekunden holt der Hub neue Messwerte vom Fühler ein und schickt sie übers Internet an den Ecowitt-Server. Den zapft die Ecowitt-App (Abbildung 3) an und gibt die aggregierten Daten grafisch ansprechend aus.
Außerdem lässt sich der Empfänger leicht mit dem Weather-Underground-Netzwerk [1] verbinden. Dahinter steckt eine Firma, die die Daten von Tausenden von privaten Wetterstationen rund um den Globus sammelt und anzeigt. So kann die ganze Welt die publizierten Wetterdaten [2] einsehen. Die Betreiber der individuellen Wetterfühler müssen sich nicht um die Darstellung kümmern und leisten noch dazu einen Beitrag für die Allgemeinheit.
Konvertiert mit Muskelschmalz
Die frisch eingetrudelten Messdaten vom Fühler gibt der Hub 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 die Daten im JSON-Format ausspucken (Abbildung 4). Was da genau ankommt, ist zwar nicht offiziell dokumentiert, aber mit etwas Reverse-Engineering lassen sich die hexadezimalen IDs der Sensoren sinnvollen Namen zuordnen.
Beispielsweise meldet der Hub 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. All das lässt sich einfach konvertieren. Auf GitHub hat sich eine gute Seele die Mühe gemacht, mit der »github.com/mschilli/go-ecowitt-hub-api« ein simpleres Interface darüber zu legen.
Sicherheit scheint man bei Ecowitt eher klein zu schreiben, der Hub beherrscht weder SSL noch ist anfangs ein Passwort gesetzt. Ohne Verschlüsselung, mit Plaintext-HTTP wäre das ohnehin 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 5 zeigt die aktuell vom Hub eingelesenen Wetterdaten in einer Bubbletea-Terminal-UI, die sich alle 10 Sekunden selbstständig auffrischt. Draußen, wo der Wetterfühler montiert ist, herrschen demnach 15,1 Grad Celsius und eine Luftfeuchtigkeit von 92 Prozent. Die Innentemperatur beziffert der Hub mit 21,7 Grad Celsius bei 61 Prozent Luftfeuchtigkeit.
Draußen weht ein leichtes Lüftchen mit 4 km/h von Norden (354 Grad). Es regnet nicht (wie so oft im Norden Kaliforniens), aber die Zeile Monthly Rain zeigt mit 137,9 Millimetern, dass es diesen Monat schon Niederschlag gab. Der absolute barometrische Luftdruck rangiert bei 764 mmHg (Millimeter-Quecksilbersäule), der relative fehlt hier, da er sich nur in höheren Lagen unterscheidet, während San Francisco fast auf Meeresebene liegt.
Interessant ist außerdem 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 Sonneneinstrahlung (Solar radiation) mit 360 W/m2 und der UV-Index mit dem Wert 3 verraten, dass die Sonne drauf und dran ist, die Wolkendecke aufzureißen.
UI mit Murmeltee
Listing 1 und Listing 2 zeigen die Go-Implementierung der Terminal-UI. 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 herauszusuchen sind. Dank »FetchAsRows()« kommen sie als String-Paare mit lesbaren Sensornamen und zugehörigen Messwerten samt Einheiten an. Zeile 29 braucht sie nur ans Hauptprogramm von Listing 2 zurückzugeben.
Der Client muss die IP-Adresse des Hubs kennen, um dessen Webserver anzuzapfen. Die Ecowitt-App gibt sie 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
package main
import (
ew "github.com/mschilli/go-ecowitt-hub-api"
)
func SensorData() [][]string {
cli := ew.NewClient("192.168.133.184")
rows, err := cli.FetchAsRows([]int{
ew.Temperature,
ew.IndoorTemperature,
ew.Humidity,
ew.IndoorHumidity,
ew.DewPoint,
ew.WindSpeed,
ew.WindGust,
ew.WindGustMaxToday,
ew.SolarRadiation,
ew.UVIndex,
ew.WindDirection,
ew.Rain,
ew.RainRate,
ew.RainDaily,
ew.RainMonthly,
ew.PressureAbsolute,
ew.PressureDelta,
})
if err != nil {
panic(err)
}
return rows
}
Das Paket »bubbletea« von GitHub übernimmt die Ausgabe des 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 Funktion »Update()« ab Zeile 23. Sie springt immer dann an, wenn etwas Außergewöhnliches passiert. Dazu gehört beispielsweise, 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
package main
import (
"fmt"
"strings"
"text/tabwriter"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
ew "github.com/mschilli/go-ecowitt-hub-api"
)
type tickMsg time.Time
type model struct {
rows [][]string
}
func tickCmd() tea.Cmd {
return tea.Tick(10*time.Second, func(t time.Time) tea.Msg {
return tickMsg(t)
})
}
func (m model) Init() tea.Cmd {
return tickCmd()
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tickMsg:
m.rows = SensorData()
return m, tickCmd()
case tea.KeyMsg:
if msg.String() == "q" || msg.String() == "ctrl+c" {
return m, tea.Quit
}
}
return m, nil
}
func (m model) View() string {
var b strings.Builder
blue := lipgloss.NewStyle().Foreground(lipgloss.Color("12"))
grey := lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
title := lipgloss.NewStyle().
Bold(true).
Render("Ecowitt Weather Dashboard")
header := grey.Render(
"Press q to quit Auto-refresh 10s")
fmt.Fprintf(&b, "%s\n%s\n\n", title, header)
w := tabwriter.NewWriter(&b, 0, 0, 2, ' ', 0)
for _, row := range m.rows {
val, unit := ew.ToMetric(row[1], row[2])
fmt.Fprintf(w, "%s\t%s\n", row[0],
blue.Render(val+" "+unit))
}
w.Flush()
return b.String()
}
func main() {
m := model{rows: SensorData()}
_, err := tea.NewProgram(m).Run()
if err != nil {
panic(err)
}
}
Die dritte Komponente stellt die Funktion »View()« ab Zeile 35. Sie bringt das neueste Datenmodell zur Anzeige. Für schön formatierte Textspalten kommt »text/tabwriter« aus dem Standardfundus zum Einsatz. 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 gibt das Paket »lipgloss« wieder – 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. Daraufhin holt er 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 modifiziert.
Dass die Applikation nach dem Programmstart nicht 10 Sekunden tatenlos herumhängt, bis der Timer zum ersten Mal abläuft, liegt daran, dass Zeile 55 im Hauptprogramm vor dem Start der Eventschleife schon mal das »model« 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 aus
go mod init wui; go mod tidy; go build wui.go wuidata.go
linkt alles zu einem Binary »wui« zusammen. Es lässt sich unter diesem knackigen Namen von überall aufrufen, falls es sich im PATH der Shell befindet.
Barometer gestern und heute
Was den Luftdruck angeht, 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 es schöner oder grauslicher? Noch im letzten Jahrhundert waren in Privatwohnungen mechanische Barometer üblich, die den Luftdruck mit einer Membran erfassten und via Zeiger 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 Zeitalter genügt freilich ein Blick auf den Pressure-Graphen über die Zeit, den die Ecowitt-App formschön zeichnet.
Windige Angelegenheit
Nachholbedarf zeigt die Ecowitt-App beim Darstellen der Windrichtung und -geschwindigkeit. Mir schwebt eine grafische Animation (Abbildung 6) vor, die eine rote Windfront mit entsprechender Geschwindigkeit und Winkel über eine Landkarte sausen lässt.
Die Windrichtung liefert der Sensor in Winkelgraden von 0 bis 360. Listing 3 fragt zudem 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
package main
import (
"strconv"
ew "github.com/mschilli/go-ecowitt-hub-api"
)
func Update() (int, float64) {
cli := ew.NewClient("192.168.1.123")
rows, err := cli.FetchAsRows([]int{ew.WindSpeed, ew.WindDirection})
if err != nil {
panic(err)
}
speed, _ := strconv.ParseFloat(rows[0][1], 64)
angle, _ := strconv.Atoi(rows[1][1])
return angle, speed
}
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
package main
import (
"image/color"
"math"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
)
type windLine struct {
line *canvas.Line
x, y float32
vx, vy float32
length float32
width float32
height float32
lastUpdate time.Time
}
func (m *windLine) setMotion(angleDeg float64, speed float64
rad := (angleDeg + 90) * math.Pi / 180
m.vx = float32(speed * 10 * math.Cos(rad))
m.vy = float32(speed * 10 * math.Sin(rad))
}
func (m *windLine) step() {
now := time.Now()
dt := float32(now.Sub(m.lastUpdate).Seconds())
m.lastUpdate = now
m.x += m.vx * dt
m.y += m.vy * dt
if m.x < 0 {
m.x += m.width
}
if m.x > m.width {
m.x -= m.width
}
if m.y < 0 {
m.y += m.height
}
if m.y > m.height {
m.y -= m.height
}
dx1 := float32(0)
if m.vx != 0 {
dx1 = -m.vy * m.width / m.vx
}
dy1 := m.width
dx2 := float32(0)
if m.vx != 0 {
dx2 = m.vy * m.height / m.vx
}
dy2 := -m.height
m.line.Position1 = fyne.NewPos(m.x+dx1, m.y+dy1)
m.line.Position2 = fyne.NewPos(m.x+dx2, m.y+dy2)
m.line.Refresh()
}
func main() {
a := app.New()
w := a.NewWindow("Wind Line")
w.Resize(fyne.NewSize(800, 600))
bg := canvas.NewImageFromFile("sf.png")
bg.FillMode = canvas.ImageFillStretch
bg.Resize(fyne.NewSize(800, 600))
line := canvas.NewLine(color.RGBA{255, 0, 0, 255})
line.StrokeWidth = 10
content := container.NewWithoutLayout(bg, line)
w.SetContent(content)
m := &windLine{
line: line,
x: 400,
y: 300,
length: 200,
width: 800,
height: 600,
lastUpdate: time.Now(),
}
angle, speed := Update()
m.setMotion(float64(angle), speed)
anim := fyne.NewAnimation(time.Second, func(_ float32) {
m.step()
})
anim.RepeatCount = -1 // infinite
anim.Start()
go func() {
for {
time.Sleep(10 * time.Second)
angle, speed := Update()
m.setMotion(float64(angle), speed)
}
}()
w.ShowAndRun()
}
Die Zeitdauer der Animation gibt der erste Parameter mit einer Sekunde an, aber das ist in Wahrheit nur die Zeitspanne des Animationszyklus. Der Aufruf des zugehörigen Callbacks erfolgt viel häufiger. Da der »RepeatCount« in Zeile 82 auf -1 gesetzt ist, 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.
Wie in Abbildung 7 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 sie mindestens zum Bildrand reicht. Meist schießt sie darüber hinaus, aber Fyne schneidet den Überschuss praktischerweise ab. Falls der zentrale Punkt der Geraden allerdings über die Bildfläche hinaus entschlüpfen will, prüfen das 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 Ruckeln, da Fyne das Canvas-Widget so schnell auffrischt, dass das menschliche Auge den Einzelschritten nicht zu folgen vermag. (uba)
Infos
- Weather Underground: https://en.wikipedia.org/wiki/Weather_Underground_(weather_service)
- Wetterstation des Autors auf Weather Underground: https://www.wunderground.com/dashboard/pws/KCASANFR2259












