Corona Inzidenz mit dem ESP32 anzeigen

Zwar ist die Inzidenz im Moment am Fallen, aber wer will schon sicher sagen, dass sie im Herbst nicht wieder am Steigen ist. Der Wert stellt eine der wichtigsten Informationen im Zusammenhang mit Corona dar und auch die Politik hat an der Inzidenz verschiedene Maßnahmen verankert, die mittlerweile automatisch wirken sollen und bis zum nächsten Lockdown reichen.

Darum interessiert es mich, jeden Morgen den aktuellen Wert zu kennen, um so für mich selbst mit den Werten extrapolieren zu können und frühzeitig zu erkennen, wenn es zum Beispiel eine Trendumkehr gibt. Den Wert will ich aber einfach nur irgendwo ablesen, ich möchte weder mit dem Smartphone danach suchen noch den großen Computer dafür anwerfen müssen.

Doch auch für Steuerungen kann das Verfahren interessant sein. Denkbar wäre zum Beispiel eine Tür, die nur aufgeht, wenn die Inzidenz einen bestimmten Wert unterschreitet.

Zum Glück habe ich noch einige ESP32 mit integriertem Display von Lilygo herum liegen. Die können ja auch ins Internet! Ob es nicht möglich ist, eines davon dazu zu bringen die Inzidenz abzufragen und auf dem Display darzustellen? Den Mikrocontroller gibt es bei Banggood:

In diesem Artikel zeige ich folgende Dinge auf:

  • Wo findet man die Daten?

  • Was ist JSON?

  • Das Display IPS ST7789V

  • Wie kann man die Daten downloaden?

  • Dokumentiertes Programm

In diesem Artikel stelle ich dar, wo Ihr die benötigten Informationen her bekommt und wie Ihr sie für Eure eigenen Zwecke abfragen könnt.

Das Robert Koch Institut (RKI) bietet eine ganze Reihe von Daten an, die von Entwicklern frei verwendet werden können. Zitat:

‚Der COVID-19 Datenhub ist ein durch Esri betriebenes und durch das RKI verantwortetes Kollaborationsportal, welches eine Datenplattform, Analysetools und Dashboards bereit stellt. Es werden Daten rund um die COVID-19 Pandemie für Behörden, die Presse aber auch eine breite Öffentlichkeit bereitgestellt. Die Daten können hier z.B. per APIs in Scripte oder eigene Anwendungen eingebunden werden. Des Weiteren können diese aber auch in diversen Datenformaten direkt heruntergeladen werden. Zusätzlich werden bereits fertige Anwendungen bereit gestellt wie z.B. das RKI COVID-19 Dashboard, das Dashboard der WHO oder das Intensivbettenregister.‘

ESRI Inc. ist ein US-amerikanischer Softwarehersteller von Geoinformationssystemen (GIS). Also: Das RKI liefert Daten an ESRI, die diese mit ihrem GIS mundgerecht aufbereiten und auf Webseiten visualisieren.

Wo findet man die Daten?

Die übergeordnete Seite zum Thema ist hier:

https://npgeo-corona-npgeo-de.hub.arcgis.com/

Auf einer der Unterseiten:

https://npgeo-corona-npgeo-de.hub.arcgis.com/datasets/917fc37a709542548cc3be077a786c17_0

findet Ihr eine Deutschlandkarte mit den einzelnen Landkreisen:

  

Wenn Ihr davon einen anklickt, poppt ein Fenster auf, indem Ihr unter ‚OBJECTID‘ eine Nummer findet:

Für meinen Landkreis ist das die Nummer 127. Die ist für die weitere Entwicklung wichtig!

Alternative für das Ermitteln der OBJECTID

Falls der gerade beschriebene Weg nicht funktioniert, zeige ich hier noch einen zweiten Weg auf. Denn alternativ könnt Ihr über dieses Icon:

eine CSV-Datei herunterladen und in dieser nach Eurem Landkreis suchen.

Ich verwende für das Bearbeiten bzw. Ansehen von CSV-Dateien LibreOffice Calc. Wenn Ihr damit die Datei öffnet, wird Euch sofort ein Import Assistent angeboten, in dem Ihr ‘Ab Zeile:’ auf 2 stellen müsst.

In der anschließend öffnenden Tabelle wird in der ersten Spalte die ‘OBJECTID’ und in der neunten Spalte der Landkreis aufgeführt!

Und weiter mit den nächsten Schritten

Also aufschreiben! Links unten findet sich ein blauer Balken ‚Verwenden‘. Wenn Ihr da drauf klickt, erscheint ein Popupmenü mit weiteren Möglichkeiten:

Dort klickt Ihr auf in ‚In API-Explorer öffnen‘. Dann erscheint eine neue Seite, in der Ihr Eure URL zusammenklicken könnt.

In dem mit ‚Abfrage‘ bezeichneten Kasten könnt Ihr alle Kästchen, bis auf ‚cases7_per_100k‘‚ deaktivieren:

Rechts oben findet Ihr jetzt Eure Abfrage, die aber leider noch nicht auf den Landkreis eingeschränkt ist. Das könnt Ihr aber manuell machen. Einfach die URL heraus kopieren und den String ‚1%3D1‘ durch ‚OBJECTID=127‘ ersetzen. Wenn Ihr die dadurch entstandene URL in einem Browser absendet, bekommt Ihr eine, im Datenformat JavaScript Object Notation (JSON) formatierte, Ergebnismenge zurück, in der die Inzidenz zu finden ist.

Was Ihr zurück bekommt, sieht auf den ersten Blick wie ein einziger großen Datenhaufen aus. Wenn ihr von diesen Daten einen strukturierten Überblick bekommen wollt, so empfehle ich Euch, falls Ihr, wie ich, den Firefox Browser einsetzt, das Add-on JSON-Formatter von Alexei Moroz. Nach der Installation findet Ihr rechts unterhalb der Titelleiste von Firefox ein Icon ‚Click to stringify your JSON‘. Wenn Ihr darauf klickt, werden die Daten blitzschnell strukturiert dargestellt.

Und dort findet Ihr dann den gesuchten Wert unter ‚cases7_per_100k‚. Am Tag, an dem ich diesen Text schreibe, am 11.06.2021, ist das bei mir‚”cases7_per_100k”:19.6477875557118‘. Darunter findet Ihr eine Fülle von Zahlen, die Geokoordinaten sind und mit deren Hilfe Ihr den Landkreis zeichnen könnt. Das interessiert uns hier aber erst einmal nicht. Leider führen diese Zahlen noch zu einem Problem, aber davon gleich mehr.

Ihr habt jetzt eine URL, die Euch jeden Tag die aktuelle Inzidenz für Euren Landkreis zurück gibt! Bei mir ist es diese:

https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/RKI_Landkreisdaten/FeatureServer/0/query?where=OBJECTID=127&outFields=OBJECTID,cases7_per_100k&outSR=4326&f=json

Was ist JSON?

Immer wenn mehr als eine Handvoll Werte verarbeitet und weitergegeben werden muss, empfiehlt es sich, die Daten in einer strukturierten Form zu halten, so dass der Zugriff auf einzelne Werte möglichst einfach ist. Als Beispiel mag da eine CSV-Datei dienen, deren Format schon seit Jahrzehnten dazu dient, einfach strukturierte Daten untereinander auszutauschen. Das funktioniert mit Daten, deren Struktur sich tabellarisch darstellen lässt, sehr gut. Wenn es aber mehr Ebenen gibt, taugt das CSV-Format nichts mehr. Dafür ist die Auszeichnungssprache XML erfunden worden. Sie erlaubt es hierarchisch verschachtelte Informationen auszutauschen und noch vieles mehr. Ihr größter Nachteil ist der große Speicherplatzbedarf, da jedes Attribut von benannten Auszeichnungen umrahmt ist.

Darum hat sich in den letzten Jahren JSON durchgesetzt. JSON erlaubt bei minimalem Platzbedarf die Organisation von hierarchisch strukturierten Daten. JSON dient zum Beispiel bei der Entwicklung von Webanwendungen zum Übertragen von Daten.

Wer jetzt Angst hat, auf dem ESP32 für JSON einen eigenen Parser entwickeln zu müssen, sei beruhigt. Das hat schon jemand gemacht und es der Allgemeinheit zur Verfügung gestellt. Die ‚ArduinoJson‘-Bibliothek könnt Ihr Euch einfach über die Arduino IDE installieren. Mit Ihrer Hilfe ist der Zugriff auf einzelne Datenelemente im oben beschriebenen Datenwust kinderleicht.

Das Display IPS ST7789V

Das der Lilygo ESP32 schon gleich mit einem prachtvoll leuchtenden Display verkauft wird, macht die Lösung unserer Aufgabe sehr einfach, da wir noch nicht einmal ein Display an einen ESP32 Mikrocontroller löten müssen. Einzig die Ansteuerung des Displays müssen wir noch lösen. Zum Glück gibt es auch dafür eine Bibliothek, mit der dies relativ einfach zu machen ist. Sie heißt ‚TFT_eSPI‘ und ist hier zu bekommen:

https://github.com/Bodmer/TFT_eSPI

Den Inhalt der ZIP-Datei müsst Ihr nach:

C:\Users\Tom\Documents\Arduino\libraries

kopieren. ‚Tom‘ müsst Ihr natürlich durch Euren Namen ersetzen. Dort, in der Datei ‚User_Setup_Select.h‘ nehmt Ihr dann die Kommentarzeichen am Anfang der Zeile:

//#include <User_Setups/Setup135_ST7789.h> // Setup file for ESP8266 and ST7789 135 x 240 TFT

weg. Das sollte es gewesen sein.

Wie kann man die Daten herunterladen?

Im Moment bin ich mir nicht sicher wie man die Daten idealerweise abfragt. Aber ruft man die Daten im Browser ab, so wird eine SSL Verbindung verwendet. Also müsste auch der ESP32 entsprechend programmiert werden. Dazu benötige ich ein Zertifikat. Das habe ich mir so geholt:

Im Browser auf das Tab mit dem JSON-Code und dort auf das Schloss-Symbol neben der URL klicken.

 

Auf ‘Verbindung sicher’ klicken.

Auf ‘Weitere Informationen’ klicken.

Es erscheint folgendes neue Fenster- Dort auf ‘Zertifikat anzeigen’ klicken.

Es öffnet sich ein neues Browser Tab. Dort auf ‘Amazon Root CA 1′ und dann unten auf PEM (Zertifikat)’ klicken.

Ihr bekommt die Möglichkeit das Zertifikat in einem Editor zu öffnen oder es herunterzuladen.

Aus dem Editor könnt Ihr es mit Cut-and-copy in Euren Code kopieren!

Die JSON Daten müssen über eine verschlüsselte Verbindung heruntergeladen werden. Dazu habe ich die Bibliothek ‘WiFiClientSecure’ verwendet.

#include <WiFiClientSecure.h>

Mit Hilfe der genannten Bibliothek kann die durch die URL bestimmte Abfrage durchgeführt und das vom Server zurückgegebene JSON als String empfangen werden. Um auf einzelne Attribute der Daten zuzugreifen, müssen sie in ein JSON Objekt umgewandelt werden. Bei einem meiner ersten Versuche mit der JSON-Bibliothek bekam ich schon beim Kompilieren einen Speicherfehler und sann darüber nach wie ich den String so verkleinern konnte, dass es zu keiner Fehlermeldung kam. Die vom GIS zurück gegebene Datenmenge enthielt sehr viele Geokoordinaten, die ich für meinen Zweck überhaupt nicht benötigte. Darum kürzte ich den String mit folgenden Anweisungen:

long pos = payload.indexOf("geometry\"");
payload = payload.substring(0, pos - 2) + "}]}";

Mit folgenden 3 Zeilen bekomme ich daraus ein JSON Objekt:

DynamicJsonDocument doc(2048);
deserializeJson(doc, payload);
JsonObject obj = doc.as<JsonObject>();

Und mit einer einzigen weiteren Zeile den gesuchten Wert:

cases7_per_100k = round ((double) (obj["features"][0]["attributes"]["cases7_per_100k"]) * 10) / 10;

Es gibt ein paar zu viele Nachkommastellen, die man so entfernen kann:

char buffer[10];
dtostrf(cases7_per_100k, 3, 1, buffer);

In ‚buffer‘ steht der gewünschte Wert!

Sonstiges

Das RKI findet es sicher nicht lustig, wenn Ihr die Daten alle paar Sekunden anfordert, obwohl sie sich nur einmal täglich ändern. Darum müsst Ihr Euer Programm so gestalten, dass es ebenfalls nur einmal am Tag die Daten anfordert. Ideal wäre ein Zeitpunkt kurz nachdem das RKI die Daten aktualisiert hat. Dann müsstet Ihr aber erst einmal wissen, wann das ist. Und wenn Ihr das wisst, müsste Euer ESP32 wissen, wie viel Uhr es ist. Das kann man zwar machen, erfordert aber einiges an Aufwand. Um die Lösung möglichst einfach zu halten, schlage ich vor, einfach nur jede, oder jede zweite, Stunde die Daten abzurufen. Sollten die Daten in der Nacht upgedatet werden, so habt Ihr sie auf jeden Fall am nächsten Morgen.

Dokumentiertes Programm

#include <WiFiClientSecure.h>
#include "ArduinoJson.h"
#include <TFT_eSPI.h>

#define SCREENWIDTH 240
#define SCREENHEIGHT 135

#define DISPLAYREFRESH 3600000

const char* ssid     = "SSID";
const char* password = "Password";

const char root_ca[] PROGMEM = R"=====(
-----BEGIN CERTIFICATE-----
MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF
ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6
b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL
MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv
b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj
ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM
9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw
IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6
VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L
93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm
jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC
AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA
A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI
U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs
N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv
o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU
5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy
rqXRfboQnoZsG4q5WTP468SQvvG5
-----END CERTIFICATE-----
)=====" ;

WiFiClientSecure client;
TFT_eSPI tft = TFT_eSPI();

int lastDisplayRefresh;

void setup() {
  Serial.begin (115200);

  // Set up graphic
  tft.init();
  tft.setRotation(3);
  tft.fillScreen(TFT_BLACK);
  
  // Titel auf dem Display ausgeben
  tft.setTextSize (2);
  tft.setTextColor(TFT_WHITE);
  tft.drawString("Odenwald Inzidenz", 20, 0);

  lastDisplayRefresh = millis() - DISPLAYREFRESH;
}

double cases7_per_100k;
double deaths;

void loop() {
  if (millis () - lastDisplayRefresh > DISPLAYREFRESH) {
    
    // Mit dem lokalen WiFi verbinden
    Serial.println ("Warte auf Verbindung");
    WiFi.begin (ssid, password);
    while (WiFi.status () != WL_CONNECTED) {
      delay (500);
      Serial.print (".");
    }
    Serial.println ("");
    Serial.print ("IP Addresse: ");
    Serial.println (WiFi.localIP());

    client.setCACert(root_ca);

    // Daten vom RKI GIS System holen
    Serial.println ("\nVerbinde mit dem Server...");
    if (!client.connect ("services7.arcgis.com", 443)) { // Starte SSL Verbindung
      Serial.println ("Nicht verbunden!");
    }  else {
      Serial.println ("Verbunden!");
      client.println ("GET https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/RKI_Landkreisdaten/FeatureServer/0/query?where=OBJECTID=127&outFields=OBJECTID,cases7_per_100k,deaths&outSR=4326&f=json"); // Bilde GET Anfrage über HTTPS
      String payload;
      while (client.connected ()) {
        // Den JSON String holen
        String payload = client.readStringUntil('\n');
        // Im String nach 'geometry' suchen, alles dahinter abschneiden
        // und wieder mit einem korrekten Ende versehen
        long pos = payload.indexOf("geometry\"");
        payload = payload.substring(0, pos - 2) + "}]}";
        // Den JSON formatierten String in eine JSON Struktur verwandeln
        DynamicJsonDocument doc(2048);
        deserializeJson(doc, payload);
        JsonObject obj = doc.as<JsonObject>();
        // Die Inzidenz abfragen
        cases7_per_100k = round ((double) (obj["features"][0]["attributes"]["cases7_per_100k"]) * 10) / 10;
      }
      client.stop ();
    }
    // WiFi und Funk beenden
    WiFi.mode(WIFI_OFF);
    btStop();
    // Alle Nachkommastellen bis auf eine abschneiden
    char buffer[10];
    dtostrf(cases7_per_100k, 3, 1, buffer);
    // Die Inzidenz auf der seriellen Schnittstelle ausgeben
    Serial.println (cases7_per_100k, 1);
    // Die Inzidenz auf dem Display ausgeben
    tft.setTextSize (5);
    tft.setTextColor(TFT_WHITE);
    tft.fillRect (65, 30, 120, 105, TFT_BLACK);
    int textPos = (SCREENWIDTH - tft.textWidth(buffer, 12)) / 2;
    tft.drawString(buffer, textPos, 50);

    lastDisplayRefresh = millis();
  }

  delay (100);
}

5 Kommentare

  1. Hallo Tinkerpete,
    vielen Dank für Deine ausführliche und erhellende Beschreibung!
    Alles funktioniert bestens. Ich möchte nun, nachdem die Zahlen für Landkreis und Bundesland korrekt heruntergeladen werden, auch die bundesweite 7-Tage-Inzidenz bekommen. Aber ich finde sie nicht auf dem Datenhub; bei den Daten für Landkreis und bei den Daten für die Bundesländer finde ich sie nicht unter den für den Json-String angegebenen Variablen. Was habe ich übersehen?
    Viele Grüße
    Holger

  2. Glückwunsch zur golem.de – Veröffentlichung, schöner Artikel!

    Aber in deinem Code fragst du nun alle 60 Sekunden nach neuen Daten. Da viele Nutzer das wahrscheinlich nicht merken und einfach copypasten, solltest du vielleicht DISPLAYREFRESH auf eine Stunde (=3600000) erhöhen, wie du ja im Artikel selbst anmerkst.

Kommentar hinterlassen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert