Projekt: Wetter-Anzeige

Kennst du das auch? Man muss früh morgens zur Schule oder vielleicht sogar schon zu Arbeit und kann sich nicht entscheiden, ob man heute die dicke oder doch eher die dünne Jacke anziehen soll? Dann haben wir jetzt die perfekte Lösung für dich:
In diesem Projekt möchten wir dir zeigen, wie du ganz einfach Daten aus dem Internet abfragen und auf deinem Display anzeigen lassen kannst. So kannst du dir zum Beispiel immer das aktuelle Wetter und eine Prognose anzeigen lassen.

Um standartisiert Daten aus dem Internet abzurufen haben sich schon vor Jahren kluge Köpfe Gedanken gemacht und das JSON-Format entwickelt. JSON steht dabei für „JavaScript Object Notation“. Falls du schonmal was von der Programmier-Sprache JavaScript gehört hast, kommt dir das vielleicht schon etwas bekannt vor. JSON ist quasi eine Möglichkeit JavaScript-Objekte als Text darzustellen. Mehr dazu erfährst du hier: https://de.wikipedia.org/wiki/JavaScript_Object_Notation

Im Internet gibt es viele sogenannte Schnittstellen – oftmals auch API genannt, die mit diesem JSON angesprochen werden können.
Zu unserem Glück gibt es auch genau eine Solche Schnittstelle um sich kostenlos die aktuellen Wetterdaten zu holen: Die OpenWeatherMap API.
Bevor du allerdings deine ersten Daten holen kannst, brauchst du einen API-Key. Dazu kannst du dich einfach auf der Webseite von OpenWeatherMap registreieren (https://openweathermap.org). Wenn du das gemacht hast, kannst du dir hier deinen Key generieren lassen: https://home.openweathermap.org/api_keys
Zum testen haben wir dir einen Key von uns bereitgestellt: d3355b38ac0d56b2e91cefcd5fd744fb
Für dein Projekt solltest du später aber besser deinen eigenen nutzen.
So, jetzt haben wir viel gelernt. Jetzt wollen wir auch endlich mal die ersten Daten abrufen. Dazu kannst du curl nutzen. Öffne dafür das Terminal auf dem Pi.

curl "https://api.openweathermap.org/data/2.5/weather\
?q=Flensburg\
&appid=d3355b38ac0d56b2e91cefcd5fd744fb\
&units=metric\
&lang=de" | json_pp

Du kannst bei der Konsoleneingabe in Bash auch Absätze machen, um nicht den langen Befehl in eine Zeile tippen zu müssen. Wie du im Beispiel siehst, machst du dazu einfach ein Backslash \ (Alt Gr + ß) am Ende einer Zeile und drückst dann Enter. Tippe dann einfach wie gewohnt weiter.

Die Konsolen-Ausgabe sollte dann in etwa so aussehen:

pi@raspberrypi:~/ $ curl "https://api.openweathermap.org/data/2.5/weather\
> ?q=Flensburg\
> &appid=d3355b38ac0d56b2e91cefcd5fd744fb\
> &units=metric\
> &lang=de" | json_pp
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   462  100   462    0     0   3882      0 --:--:-- --:--:-- --:--:--  3915
{
   "timezone" : 3600,
   "wind" : {
      "speed" : 7.72,
      "deg" : 160
   },
   "sys" : {
      "sunset" : 1638975382,
      "country" : "DE",
      "sunrise" : 1638948740,
      "type" : 1,
      "id" : 1823
   },
   "base" : "stations",
   "name" : "Flensburg",
   "weather" : [
      {
         "description" : "Bedeckt",
         "main" : "Clouds",
         "icon" : "04d",
         "id" : 804
      }
   ],
   "main" : {
      "feels_like" : -3.58,
      "pressure" : 998,
      "temp_max" : 2.84,
      "temp" : 2.03,
      "humidity" : 93,
      "temp_min" : 0.97
   },
   "clouds" : {
      "all" : 100
   },
   "coord" : {
      "lat" : 54.7833,
      "lon" : 9.4333
   },
   "dt" : 1638964572,
   "cod" : 200,
   "id" : 2926271,
   "visibility" : 10000
}

Das, was du da in den geschwungenen Klammern siehst, ist dieses sogenannte JSON, dass wir von der API bekommen haben.
Wie du siehst, sind es bei uns in Flensburg gerade gemütliche 2,03°C bei 7,72m/s und einer Luftfeuchtigkeit von 93%.

Jetzt haben wir zwar die Daten, aber eigentlich wollten wir die doch auf dem Display anzeigen lassen. Also fangen wir mit unserem Skript an. Öffne dazu eine neue Datei, wie du das schon kennst.
Zunächst importieren wir wie immer die Bibliotheken, die wir benötogen:

import socket, fcntl, struct, board, digitalio, requests
import os.path,  time
from PIL import Image, ImageDraw, ImageFont
import adafruit_ssd1306
from io import BytesIO
import cairosvg

Dann binden wir wieder das Display ein:

RESET_PIN = digitalio.DigitalInOut(board.D4)
i2c = board.I2C()
oled = adafruit_ssd1306.SSD1306_I2C(128, 64, i2c, addr=0x3C,
    reset=RESET_PIN)

oled.fill(0)
oled.show()

Und importieren paar Schriftarten:

font1 = ImageFont.truetype(
    "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 10)
font2 = ImageFont.truetype(
    "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 14)
font3 = ImageFont.truetype(
    "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 16)

Damit wir später auch Wetter-Icons ausgeben können und nicht nur langweiligen Text, laden wir uns noch paar Icons runter:

git clone https://github.com/coding-world/open-weather-icons.git

Erstelle dann einen Ordner icons und kopiere die Icons dorthin:

cp ./open-weather-icons/src/svg/* ./icons

Jetzt da wir die Icons haben, können wir mit dem Skript weiter machen. Und zwar erstellen wir uns nämlich eine Funktion, mit der wir später die Icons abrufen können:

def get_icon(id, size=32):
    url = './icons/'+id+'.svg'
    if os.path.isfile(url):
        return Image.open(
            BytesIO(cairosvg.svg2png(url=url))  # svg zu png
        ).resize((size, size)).convert('1')  # skaliere Bild
    else:
        return Image.new("1", (size, size))  # leeres Bild

Die Funktion hat zwei parameter: Die ID des Icons und die Größe, die das Bild haben soll. Das size=32 bedutet, dass der Parameter automatisch auf 32 gesetzt wird, falls er beim Aufrufen den Funktion nicht gesetzt wird. Zunächst setzen wir uns anhand der übergebnen ID den Dateipfad zusammen. Dann überprüfen wir mit os.path.isfile(url) ob unter diesem Pfad auch eine Datei existiert. Falls ja öffnen wir die Datei und geben das Bild zurück.
Haaalt stop, nicht so schnell: Die Icons, die wir vorhin runtergeladen haben, sind in dem .svg Format gespeichert. Dieses Format können wir leider aber so noch nicht auf dem Display darstellen. Desshalb siehst du, dass der Funktion Image.open() nicht direkt der Dateipfad übergeben wird, sondern dieser erst der Funktion cairosvg.svg2png(). Diese Konvertiert uns die SVG-Datei in ein PNG-Bild, welches wir dann mittels BytesIO() an Image.open() übergeben können. Nachdem das PNG dann geöffnet wurde, wird dieses mit der .resize()-Methode noch auf die richtige Größe skaliert und mit der .convert('1')-Methode auf den richitigen Farbraum angepasst – das Display kann nämlich nur Schwarz und Weiß anzeigen. Erst wenn das alles getan ist, wird das Bild durch das return am Anfang zurückgegeben.
Erinnerst du dich noch dan die if-Abfrage zu Beginn der Funktion? Dort hatten wir überprüft, ob es überhaupt eine Datei gibt, die wir öffnen und konvertieren können. Kommen wir zum else: Hier erstellen wir ein leeres Bild, welches wir zurückgeben, damit kein Fehler auftritt, falls mal ein Icon fehlen sollte.

Pouh, wie komplex doch so ein kurzer Code-Block sein kann. Aber keine Angst, jetzt geht es etwas einfacher weiter:
Der folgende Teil soll immer wiederholt ausgeführt werden – wir wollen uns das Wetter ja nicht nur einmal anzeigen lassen. Also schreiben wir while True:. Denk daran, den nächsten Teil einzurücken, sonst wird dieser nur einmal ausgeführt.
So, bevor wir jeztzt allerdings Daten anzeigen können, müssen wir erstmal die Daten von der API abholen. Das sieht ganz so ähnlich aus, wie vorhin:

    data = requests.get(
        url='https://api.openweathermap.org/data/2.5/onecall'
            '?appid=d3355b38ac0d56b2e91cefcd5fd744fb'  # API-Key
            '&units=metric'
            '&lang=de'
            '&lat=54.788'  # Breitengrad
            '&lon=9.43701',  # Längengrad
        timeout=10
    ).json()

Achtung, wir nutzen hier nicht exakt die gleiche API wie vorhin. Bei dieser bekommen wir mehr Daten zurück, die wir gleich brauchen, um sie anzuzeigen. Allerdings brauchen wir dafür die Dezimal-Koordinaten, um die richtigen Daten zu bekommen. Dezimal-Koordinaten, werden anders als Sekundar-Koordinaten im Dezimalsystem dargestellt. Um deine genauen Koordinaten herauszufinden, kannst du auf https://www.openstreetmap.org/ gehen und an einer Stelle auf der Karte im Kontextmenü unter „zeige Addresse“ die entsprechenden Koordinaten anzeigen lassen. Aktualisiere die Koordinaten in der URL dann einfach mit deinen. Wenn du schon dabei bist, kannst du auch gleich deinen API-Key eintragen 😉

Mit der Methode requests.get() Rufen wir die Daten von der API ab. Bis jetzt ist das aber noch ein einfacher String. Damit wir mit den Daten aber auch vernünftig arbeiten können übersetzen wir diesen JSON-String mit der Methode .json() in ein Python-Dictionary. Das speichern wir dann in der Variable data. In einem solchen Dictionary können wir wie folgt die verschiedenen Elemente zugreifen: data['hourly']. In diesem Fall greifen wir auf das Element hourly in dem Dictionary data zu. hourly ist ein Array mit den Wetterdaten für die nächsten Stunden, die sich wiederrum in einem Dictionary befinden. Zum Beispiel: Auf die Temperatur in einer Stunde können wir so zugreifen:

data['hourly'][1]['temp']. Die Datenstruktur ist dabei genau die gleiche wie in dem JSON-Objekt, aus dem wir ja das Python Dictionary erstellt haben 😉

Zunächst zeigen wir alle 4 Sekunden das Jetzige und das Wetter in 1, 2, 3 und 6 Stunden an. Du kannst das später natürlich alles noch nach Belieben anpassen.
Die Folgende for-Schleife führen wir auf ein Array aus, dass wir uns selbst zusammenstellen:

    for step in [
        {'title': 'Jetzt:', 'data': data['current']},
        {'title': 'in einer Stunde:', 'data': data['hourly'][1]},
        {'title': 'in zwei Stunden:', 'data': data['hourly'][2]},
        {'title': 'in 3 Stunden:', 'data': data['hourly'][3]},
        {'title': 'in 6 Stunden:', 'data': data['hourly'][6]},
    ]:

Wie du hier siehst muss das Array nicht unbedingt zuerst in einer Variable gespeichert werden, damit wir es in einer for-Schleife verwenden können. Da wir das Array nur einmal benötigen, sparen wir uns das also. In dem Array sind insgesamt 5 Ditionaries mit den Elementen title und data. In title übergeben wir uns den Titel, der auf jeder Seite angezeigt werden soll und in data die dazugehörigen Daten. Wenn wir dann die for-Schleife durchlaufen, nimmt die Variable step dabei immer das aktuelle Dictionary als Wert an. So sparen wir uns viel Schreibarbeit und müssen nicht alles Fünf-mal kopieren, was jetzt folgt.

Kommen wir zum Teil in der for-Schleife:

        image = Image.new("1", (oled.width, oled.height))
        draw = ImageDraw.Draw(image)

        draw.text((0, 0),
            step['title'], font=font3, fill=255)
        draw.text((0, 16),
            step['data']['weather'][0]['description'],
            font=font2, fill=255)
        draw.text((48, 32),
            str(step['data']['temp'])+'°C', font=font3, fill=255)
        draw.text((48, 48),
            str(step['data']['humidity'])+'%', font=font3, fill=255)
        image.paste(
            get_icon(step['data']['weather'][0]['icon']), (8, 32)
        )

        oled.image(image)
        oled.show()
        time.sleep(4)

Der Teil kommt dir wahrscheinlich schon bekannt vor. Sonst guck doch nochmal bei der OLED Einleitung.
Neu für dich ist aber die image.paste() Methode. Hiermit können wir ein andres Bild in das Bild hinzufügen. Das nutzen, wir um die Icons hinzuzufügen. Als ersten Parameter will die Methode das Bild haben, das eingefügt werden soll, und als zweiten die Position dafür.
Als Bildquelle benutzen wir dazu unsere get_icon(id, size) Funktion, die wir vorhin erstellt haben. Hier übergeben wir die Icon-ID, die wir von der API erhalten haben, an die get_icon()-Funktion und erhalten dann das passende Bild dazu, welches wir einfügen können.
Wenn wir das getan haben, laden wir das Bild auf das OLED Display und warten 4 Sekunden, bis wir zum nächsten Schritt in der for-Schleife kommen.

Wenn die for-Schleife durchgelaufen ist, können wir uns auch noch das Wetter für die nächsten Tage in einer Tabelle anzeigen lassen. Auch dabei können wir uns mittels einer for-Schleife etwas Schreibarbeit sparen:

    image = Image.new("1", (oled.width, oled.height))
    draw = ImageDraw.Draw(image)

    draw.text((0, 0), 'nächste Tage', font=font3, fill=255)
    for i in range(1, 4):
        draw.text((24, 16 * i),
             str(data['daily'][i]['temp']['day'])[:4] + '°C',
             font=font2, fill=255)
        draw.text((76, 16 * i),
            str(data['daily'][i]['temp']['night'])[:4] + '°C',
            font=font2, fill=255)
        image.paste(
            get_icon(data['daily'][i]['weather'][0]['icon'], 16),
        (0, 16 * i))

    oled.image(image)
    oled.show()
    time.sleep(8)

Auch hier erstellen wir uns zunächst wieder ein leeres Bild. Darauf fügen wir den Titel hinzu. Die Zeilen der Tabelle schreiben wir in einer for-Schleife i in range(1, 4). Die Variable i nimmt also die Werte 1 bis 3 an. Das entspricht den nächsten drei Tagen in dem Array data['daily'].
Die y-Position multiplizieren wir einfach mit i, damit die Zeilen später nicht übereinander liegen. In jeder Zeile zeigen wir die Tag- und Nacht-Temperatur für den Tag an und fügen auch noch für jede Zeile ein Icon ein. Die Stringlänge der Temperatur limiteren wir auf vier Zeichen: [:4].
Jetzt müssen wir das Bild nur noch anzeigen lassen und für 8 Sekunden warten.

Das wars auch schon. Falls etwas noch nicht ganz so funktionieren sollte, wie gewünscht, kannst du dir das fertige Skript von uns auch herunterladen:

wget https://cw42.de/p/oled-wetteranzeige.py