Docker Hub Pull-Statistiken selbst auswerten

In diesem Artikel möchte ich beschreiben, wie man mit dem „zusammenstecken“ einiger Komponenten eine eigene grafische Auswertung über den Pull Verlauf von Docker Hub Images erstellen kann, die so aussehen könnte:

Der eigentliche Kniff ist nur, die Daten bei Docker Hub abzugreifen über die folgende URL:

https://hub.docker.com/v2/repositories/<namespace>/<name>

Architektur

Wie man in dem Schaubild sieht, wird ein Job zyklisch die Metriken von Docker Hub lesen und in einen MQTT Server pushen. Ein Dienst wird auf die entsprechenden Topics hören, ankommende Daten abgreifen und in einer Opensearch (oder Elasticsearch) Datenbank abspeichern. Ab da kann man diese bequem mit Grafana auslesen.

In meinem Tutorial werde ich Opensearch verwenden, aber die Verwendung von Elasticsearch ist analog möglich wird aber im nachfolgenden Tutorial nicht weiter erwähnt.

Warum so kompliziert? Nun ja, ich habe einen zentralen MQTT Server im Einsatz, der bei mir zu Hause dreh und angelpunkt für Datenaustausch darstellt, daher habe ich das entsprechend entkoppelt. Zudem gibt es anderen Diensten die Möglichkeit gleichzeitig die gesammelten Daten zu verarbeiten.

Umsetzung

Für die exemplartische Umsetzung verwende ich einen Linux Rechner mit vorinstalliertem Docker. Bitte achtet auf genügend Arbeitsspeicher, da die Opensearch Datenbank einiges benötigt. 4 GB sollten aber genügen.

Job

Um die Metriken abzugreifen, habe ich ein kleines Script geschrieben, welches mir die Metriken der Docker Hub Repositories abgreift und in einen MQTT Topic published. Das Script findet Ihr auch in meinem GitHub Repo: https://github.com/cybcon/docker.dockerhubstats2mqtt/blob/main/src/entrypoint.sh. Aus dem Repo heraus erstelle ich einen Docker Container, den Ihr hier findet: https://hub.docker.com/r/oitc/dockerhubstats2mqtt.Dieser ist einfach über Umgebungsvariablen zu konfigurieren.

Die Variable „REPOSITORIES“ enthält die Container Image Repositorie Namen auf die es einem ankommt bei der Visualisierung. Also von diesen werden die Daten gesammelt.Hierzu gibt man der Variable eine, mit Leerzeichen getrennte Liste mit der relevanten Repositories. Jeweils mit dem Docker Hub Namespace und dem eigentlichen Image Namen.

Docker-Compose Beispiel:

services:
  dockerhubstats2mqtt:
    restart: "no"
    user: 5241:5241
    image: oitc/dockerhubstats2mqtt:latest
    environment:
      MQTT_SERVER: test.mosquitto.org
      MQTT_PORT: 1883
      MQTT_TOPIC: com/docker/hub/repositories/metrics
      REPOSITORIES: oitc/dockerhubstats2mqtt oitc/mqtt2elasticsearch
      MQTT_RETAIN: true
      MQTT_TOPIC_REPO_EXTENSION: true

Beim starten des Containers wird das Skript die Daten sammeln und in den MQTT_TOPC publishen und sich dann wieder beenden. Das ganze triggere ich dann via Cron zyklisch, z.B. einmal am Tag:

0  1  *  *  * /usr/bin/docker compose -f /pathto/docker-compose.yml run --rm dockerhubstats2mqtt >/dev/null 2>&1

MQTT

Als MQTT Server nehm ich, zur einfachheit halber, in diesem Tutorial den Testserver von Eclipse Mosquitto. Dieser ist erreichbar auf test.mosquitto.org:1883.

In einem echten Szenario sollte man hier einen eigenen Server aufsetzen.

Service

Um Daten aus einem MQTT Topic auszulesen und in eine Opensearch Datenbank zu speichern, habe ich auch schon vor einiger Zeit ein kleines Python Skript geschrieben, dass genau das macht. Ihr findet das Skript in meinem GitHub Repo: https://github.com/cybcon/docker.mqtt2elasticsearch/blob/main/src/app/bin/mqtt2elasticsearch.py. Auch aus diesem Repo heraus erstelle ich einen Docker Container den Ihr hier findet: https://hub.docker.com/r/oitc/mqtt2elasticsearch.

Das Skipt selbst wird gesteuert durch 2 Konfigurationsdateien. Die „mqtt2elasticsearch.json“ enthält dabei die Verbindungsdaten zu den jeweiligen Servern. Also dem MQTT Server von dem es die Daten bekommt und die Opensearch Datebank, in den es die Daten schrieben soll. Die andere Datei „mqtt2elasticsearch-mappings.json“ dagegen enthält die eigentliche Mapping von MQTT Daten zu Opensearch Index. Einstieg ist immer der MQTT Topic auf den das Tool hören soll und darin wird das Ziel spezifiziert, wohin die Daten geschrieben werden sollen und wie dies dann im Detail aussieht. Dieser „elasticBody“ Bereich ist aber optional. Er ist dafür da, der Datenbank die Entscheidung abzunehmen was es denn für Felder gibt und was für Daten darin enthalten sein werden. Wenn man das weglässt. entscheidet die Datenbak das selbst anhand der Daten.

Die Konfigurationsdatei für die Verbindungsinformationen sieht z.B. so aus (mqtt2elasticsearch.json):

{
"DEBUG": true,
"removeIndex": false,
"opensearch": {
  "hosts": [
     {
       "host": "opensearch-node1",
       "port": 9200
     }
  ],
  "tls": true,
  "verify_certs": false,
  "username": "admin",
  "password": "FooBarBaz-123"
  },
"mqtt": {
  "server": "test.mosquitto.org",
  "port": 1883,
  "tls": false,
  "protocol_version": 3
  }
}

Die Konfigurationsdatei für das Mapping von MQTT Topic und die Opensearch Datenbank sieht z.B. so aus (mqtt2elasticsearch-mappings.json):

{
  "com/docker/hub/repositories/metrics/#": {
    "elasticIndex": "dockerhub-repositories-metrics-{Y}-{m}",
    "elasticBody": {
      "settings": {
        "index": {
          "number_of_shards": 5,
          "number_of_replicas": 0
        }
      },
      "mappings": {
        "properties": {
          "timestamp": { "type": "date" },
          "user": { "type": "keyword" },
          "name": { "type": "keyword" },
          "repository_type": { "type": "keyword" },
          "status": { "type": "integer" },
          "status_description": { "type": "keyword" },
          "description": { "type": "text" },
          "is_private": { "type": "boolean" },
          "is_automated": { "type": "boolean" },
          "star_count": { "type": "integer" },
          "pull_count": { "type": "integer" },
          "last_updated": { "type": "date" },
          "last_modified": { "type": "date" },
          "date_registered": { "type": "date" },
          "collaborator_count": { "type": "integer" },
          "affiliation": { "type": "keyword" },
          "hub_user": { "type": "keyword" },
          "has_starred": { "type": "boolean" },
          "permissions": { "type": "object" },
          "media_types": { "type": "text" },
          "content_types": { "type": "text" },
          "categories": { "type": "object" },
          "immutable_tags": { "type": "boolean" },
          "immutable_tags_rules": { "type": "keyword" },
          "storage_size": { "type": "integer" }
        }
      }
    }
  }
}

Diese beiden Konfigurationsdateien legt man dann im Dateisystem ab und mounted diese als Volume in den Container.

Docker-Compose Beispiel:


services:
  mqtt2elasticsearch:
    restart: unless-stopped
    image: oitc/mqtt2elasticsearch:latest
    container_name: mqtt2elasticsearch
    volumes:
      - /path/to/mqtt2elasticsearch/etc:/app/etc:ro
    depends_on:
      - opensearch-node1

Opensearch / Elasticsearch

Ich glaube, die Opensearch Datenbank zu betreiben ist das kompizierteste an allem, da die ein wenig „Liebe“ benötigt und genügend Ressourcen (RAM). 😀

Docker-Compose Beispiel:

services:
  opensearch-node1:
    restart: unless-stopped
    image: opensearchproject/opensearch:3.4.0
    container_name: opensearch-node1
    environment:
      node.name: opensearch-node1
      discovery.type: single-node
      discovery.seed_hosts: opensearch-node1
      OPENSEARCH_INITIAL_ADMIN_PASSWORD: FooBarBaz-123
      bootstrap.memory_lock: true  # along with the memlock settings below, disables swapping
      OPENSEARCH_JAVA_OPTS: -Xms512m -Xmx512m # minimum and maximum Java heap size, recommend setting both to 50% of system RAM
    ulimits:
      memlock:
        soft: -1
        hard: -1
      nofile:
        soft: 65536
        hard: 65536

Grafana

Nachdem nun alle Vorbereitungen erledigt sind, widmen wir uns der eigentlichen graifschen Auswertung. Hierfür verwende ich Grafana. Das Tool wird häufig auch im Unternehmensumfeld eingesetzt und kann zwischenzeitlich mit vielen Datenquellen interagieren.

Docker Compose Beispiel:

services:
  grafana:
    container_name: grafana
    image: grafana/grafana:12.3-ubuntu
    restart: unless-stopped
    ports:
      - 3000:3000

Nach dem Starten von Grafana, kann man sich mit dem Browser auf http://<server>:3000 verbinden und mit dem Benutzername admin und dem Passwort admin anmelden.

Zunächst brauchen richten wir die Datenquelle ein, also unsere Opensearch Datenbank. Hierfür gibt es im Grafana die Möglichkeit ein passendes Datenbank Plugin zu installieren. wir navigieren im Grafan im Menü zu „Connections“ > „Add new connection“. suchen dort nach „Opensearch“, wählen das Plugin aus und installieren dieses über den „Install“ Button.

Nach der Installation des Plugins können wir die Datenbankverbindung selbst anlegen. Hierzu klicken wir entweder auf den Button „Add a new datasource“ nach der Plugin Installation oder Navigieren im Menü zu „Connections“ > „Data sources“ und wählen dort „Add data source“ und wählen hier „Opensearch“. Hier machen wir folgende Konfigurationen:

Namedockerhub-repositories-metrics
HTTP: URLhttps://opensearch-node1:9200
Auth: Basic autheinschalten
Auth: With Credentialseinschalten
Auth: Skip TLS Verifyeinschalten
Auth: Basic Auth Details: Useradmin
Auth: Basic Auth Details: PasswordFooBarBaz-123
OpenSearch details: Index namedockerhub-repositories-metrics-*
OpenSearch details: Time field nametimestamp (ohne das @ Zeichen)
OpenSearch details: VersionAuf „Get Version and Save“ klicken sollte die Version entsprechend finden. (hier OpenSearch 3.4.0)
Grafana Konfiguration für Opensearch Datenquelle

Nach Eingabe der Daten, sollte über Klick auf „Save & test“ alles gün sein.

Danach richten wir das Dashboard ein. Hierzu navigieren wir im Grafana im Menü nach „Dashboards“ und Klicken hier auf „Create dashboard“. Nun haben wir die Wahl, ob wir komplett frisch anfangen wollen oder ob wir ein fertiges Dashboard importieren möchten. Das passende Dashboard habe ich bei Grafan hochgeladen und kann über ID: 24696 einfach importiert werden. Hier auch der Link zum Dashboard: https://grafana.com/grafana/dashboards/24696-docker-image-pulls/. Daher klicken wir im Bereich „Import a dashboard“ auf den Button „mport dashboard“.

Auf dem nächsten Screen können wir die Dashboard ID (24696) eingeben und per Klick auf „Load“ das Dashboard laden.

Auf dem daraufhin folgenden Screen müssen wir nur noch unsere vorher angelegte Datenquelle (dockerhub-repositories-metrics) auswählen und auf „Import“ klicken.

Dann sollte einem das Dashboard mit den Daten angezeigt werden. Sollten hier noch keine Daten auftauchen, kann man:

  1. das Tool dockerhubstats2mqtt einmal manuell aufrufen
  2. Den Zeitraum des Dashboards vom Default (30 Tage) auf zum Beispiel „Last 15 minutes“ einstellen.

Die gesamte Docker Compose Konfiguration

Da dies im Verlauf ein wenig zerstückelt war, hier nochmals die gesamt Docker Compose Konfiguration. Bitte beachtet, dass Ihr die beiden Konfigurationsdateien vom mqtt2ealsticsearch passend platzieren müsst und den Pfad bei „volumes“ dann richtig angeben müsst.

services:
  dockerhubstats2mqtt:
    restart: "no"
    user: 5241:5241
    image: oitc/dockerhubstats2mqtt:latest
    environment:
      MQTT_SERVER: test.mosquitto.org
      MQTT_PORT: 1883
      MQTT_TOPIC: com/docker/hub/repositories/metrics
      REPOSITORIES: oitc/dockerhubstats2mqtt oitc/mqtt2elasticsearch
      MQTT_RETAIN: true
      MQTT_TOPIC_REPO_EXTENSION: true

  mqtt2elasticsearch:
    restart: unless-stopped
    image: oitc/mqtt2elasticsearch:latest
    container_name: mqtt2elasticsearch
    volumes:
      - /path/to/mqtt2elasticsearch/etc:/app/etc:ro
    depends_on:
      - opensearch-node1

  opensearch-node1:
    restart: unless-stopped
    image: opensearchproject/opensearch:3.4.0
    container_name: opensearch-node1
    environment:
      node.name: opensearch-node1
      discovery.type: single-node
      discovery.seed_hosts: opensearch-node1
      OPENSEARCH_INITIAL_ADMIN_PASSWORD: FooBarBaz-123
      bootstrap.memory_lock: true  # along with the memlock settings below, disables swapping
      OPENSEARCH_JAVA_OPTS: -Xms512m -Xmx512m # minimum and maximum Java heap size, recommend setting both to 50% of system RAM
    ulimits:
      memlock:
        soft: -1
        hard: -1
      nofile:
        soft: 65536
        hard: 65536

  grafana:
    container_name: grafana
    image: grafana/grafana:12.3-ubuntu
    restart: unless-stopped
    ports:
      - 3000:3000

Hinweise

datenpersitenz

Dieses kleine Tutorial verwendet keinerlei Persistenz. D.b. wenn Ihr die Docker Container wegwerft oder erneuert, dann sind die gesammelten historischen Daten auch weg.

  • Bei Grafana sind die Daten im Container Image an der Stelle „/var/lib/grafana„.
  • Bei Opensearch sind die Daten im Container Image an der Stelle „/usr/share/opensearch/data

Passwörter und Zugänge

Das sind ganz einfache Beispiel, bitte verwendet richtige Passwörter. Im Unternehmenskontext sollte man auch nicht mit dem „admin“ user arbeiten sondern dedizierte Accounts anlegen die eben auch nur die Rechte haben, die benötigt werden.

TLS Zertifikate

Die Verbindungen sind alle unverschlüsselt bis auf die Verbindung zur Opensearch Datenbank. Und Opensearch verwendet ein selbst generiertes Zertifikat. In einer „echten“ Umgebung im Unternehmenskontext verwenden die Tools auch „echte“ Zertifikate um den Netzwerkverkehr abzusichern. Hier wird auch die TLS Validierung nicht deaktiviert.

Links

IoT mit ESP8266 und DHT22

Ziel des Projekts sollte es sein günstige IoT Sensoren zu erstellen und in FHEM einzubinden. Hierzu habe ich folgendes Szenario erstellt:

  1. Entwicklerboard (ESP8266)
  2. angeschlossener Temperatur und Luftfeuchte Sensor (DHT22)
  3. Connect via WLAN
  4. Ermittlung der Ablesezeit von einem NTP Server
  5. Erstellen eines JSON Strings
  6. Publish auf MQTT Server (Mosquitto)
  7. Verwenden des DeepSleep Modus beim ESP8266 um Strom zu sparen (für Batteriebetrieb)
  8. FHEM liest die Daten vom MQTT Server und visualisiert diese.

Mosquitto einrichten

Zunächst habe ich den MQTT Server eingerichtet. Ich habe mich hier für Mosquitto entschieden. Das Ganze läuft als Docker Container.

Da die IoT Devices in einem eigenen WLAN Netz befinden, musste die Firewall auf den MQTT Server freigegeben werden.

Zusätzlich wurde eine Benutzerauthentisierung eingerichtet.

das Mosquitto aclfile.conf sieht wie folgt aus:

# this only affects clients with username admin
user admin
topic read $SYS/#
topic readwrite #

# This only affects clients with username esp8266
user esp8266
topic write /environmental_sensors/#

# this affects all clients
pattern write $SYS/broker/connection/%c/stat

Mit mosquitto_passwd kann man dann die Passwörter in die Passwortdatei speichern. Nach einem Reload des Mosquitto (kill -hup) akzeptiert der MQTT Broker die Schreibrequests der IoT Devices auf allen Topics die mit „/environmental_sensors/“ anfangen.

FHEM

Um FHEM an den MQTT anzubinden benötigt man folgende Konfigurationen:

#MQTT - https://haus-automatisierung.com/hardware/fhem/2017/02/13/fhem-tutorial-reihe-part-26-esp8266-arduino-mqtt-temperatur-an-fhem.html
define Mosquitto MQTT mqttServer:1883

# wandelt JSON Strings aus dem reading "data" im Device "mqtt01".
define Jason2Readings01 expandJSON mqtt01:data.*

# Erstellen Device "mqtt01" zum lesen aus dem MQTT Server
define mqtt01 MQTT_DEVICE
attr mqtt01 IODev Mosquitto
attr mqtt01 icon temp_temperature
attr mqtt01 room IoT
attr mqtt01 stateFormat Temperatur: temperature°C<br>Luftfeuchtigkeit: humidity%
attr mqtt01 subscribeReading_data /environmental_sensors/iotLocation

# Logging der Daten aus den readings "temperature" und "humidity"
define FileLog_mqtt01 FileLog /var/log/fhem/mqtt01-%Y-%m.log mqtt01:(temperature|humidity).*
attr FileLog_mqtt01 logtype esp8266-dht22:Plot,text

# Define GPlot zur Visualisierung
define wl_mqtt01 SVG FileLog_diningRoomServerRack:esp8266-dht22:CURRENT
attr wl_mqtt01 label "Klima IoT Device 01"
attr wl_mqtt01 room IoT
attr wl_mqtt01 title "Klima IoT Device 01

Die passende gplot Datei esp8266-dht22.gplot sieht dann so aus:

set terminal png transparent size <SIZE> crop
set output '<OUT>.png'
set xdata time
set timefmt "%Y-%m-%d_%H:%M:%S"
set xlabel " "
set ytics nomirror
set y2tics
set yrange [0:99]
set y2range [10:50]
set title '<L1>'
set grid xtics y2tics

set y2label "Temperatur in C"
set ylabel "Luftfeuchtigkeit in %"

#FileLog 4:temperature:0:
#FileLog 4:humidity:0:

plot \
  "< awk '/temperature/{print $1, $4}' <IN>"\
     using 1:2 axes x1y2 title 'Temperatur' with lines lw 2,\
  "< awk '/humidity/ {print $1, $4+0}' <IN>"\
     using 1:2 axes x1y1 title 'Luftfeuchtigkeit' with lines\ 

ESP8266

Nun zur eigentlichen Entwicklung des ESP8266. Ich verwende aktuell die Arduino IDE für das Flashen. Hier sind ein paar zusätzliche Bibliotheken zu installieren:

Da ich mit dem DeepSleep Modus des ESP8266 arbeite, ist der Quellcode nur in der standard Funktion „void setup()“ enthalten. Die Funktion „void loop()“ bleibt in dem Fall leer.

Der Kopfbereich des Programms sieht dann so aus:

/* ESP8266 + WiFi connection, DHT22 Humidity and Temperature Node reading
 * and send to MQTT Broker
 */

#include <PubSubClient.h> // @see: "https://pubsubclient.knolleary.net/api.html"
#include <ESP8266WiFi.h>  // @see: "https://arduino-esp8266.readthedocs.io/en/latest/esp8266wifi/readme.html"
#include <DHT.h>
#include <ArduinoJson.h>  // @see: "https://arduinojson.org/"
#include <NTPClient.h>    // @see: "https://diyprojects.io/esp8266-web-server-part-3-recover-time-time-server-ntp/#.W876FvZCSUk"
#include <WiFiUdp.h>
#include <ctime>          // to parse epoch into ISO 8601 Zulu Time String

// initialize the clients
WiFiClient espClient;
PubSubClient client(espClient);
#define DHTTYPE DHT22 // DHT11 or DHT22
#define DHTPIN  2
DHT dht(DHTPIN, DHTTYPE, 11);
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "ntpServerName", 0, 60000); // 

// The MQTT server connection parameters
const char* mqtt_server = "mqttServerName";
const int mqtt_port = 1883;
const char mqttUser[] = "esp8266"; // MQTT broker user
const char mqttPass[] = "myIoTPasswordForMQTT"; // MQTT broker password
String clientName = ""; // MQTT client name
// The MQTT topics to publish messages to
String topicPrefix = "/environmental_sensors/";
String topicLocation = "iotLocation";
String topic = topicPrefix + topicLocation;

// DeepSleep time in us
long deepSleepTime = 6e8; // 60e6 is 60 Seconds 9e8 is 15 Minutes

Danach folgen ein paar Helfer Funktionen.

/**
 * macToStr - converts a MAC address to a String
 * @param mac: uint8_t* MAC address
 * @return: String the MAC address as String
 */
String macToStr(const uint8_t* mac)
  {
  String result;
  for (int i = 0; i < 6; ++i)
    {
    result += String(mac[i], 16);
    if (i < 5)
      result += ':';
    }
  return result;
  }

/**
 * floatToStr - converts a float to a String
 * @param f: float, the float to convert
 * @param p: int, the precission (number of decimals)
 * @return: String, a string representation of the float
 */
char *floatToStr(float f, int p)
  {
  char * pBuff;                         // use to remember which part of the buffer to use for dtostrf
  const int iSize = 10;                 // number of buffers, one for each float before wrapping around
  static char sBuff[iSize][20];         // space for 20 characters including NULL terminator for each float
  static int iCount = 0;                // keep a tab of next place in sBuff to use
  pBuff = sBuff[iCount];                // use this buffer
  if (iCount >= iSize - 1)              // check for wrap
    {
    iCount = 0;                         // if wrapping start again and reset
    }
  else
    {
    iCount++;                           // advance the counter
    }
  return dtostrf(f, 0, p, pBuff);       // call the library function
  }

Danach kommen die Funktionen für den Verbindungsaufbau und die Datenverarbeitung.

WLAN Verbindungsaufbau:

/**
 * setup_wifi - connects to a WiFi network
 * and log the IP address and MAC of the Device
 */
void setup_wifi()
  {
  const char* ssid = "iot-wlan";
  const char* password = "iotWLANpassword";
  const String clientNamePrefix = "esp8266-";

  delay(10);
  // We start by connecting to a WiFi network
  Serial.println();
  Serial.print("Connecting to ");
  Serial.println(ssid);

  // WiFi only as client
  WiFi.mode(WIFI_STA);
  // Authenticate to WiFi network
  WiFi.begin(ssid, password);

  while (WiFi.status() != WL_CONNECTED)
    {
    delay(500);
    Serial.print(".");
    }

  Serial.println("");
  Serial.println("WiFi connected");

  // get the MAC address of the device
  uint8_t mac[6];
  WiFi.macAddress(mac);

  // log MAC address and IP address
  Serial.print("MAC address: ");
  Serial.println(macToStr(mac));
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());

  // append MAC Address to clientName to connect to MQTT
  clientName = clientNamePrefix;
  clientName += macToStr(mac);
  }

MQTT Verbindungsaufbau

/**
 * setup_mqtt - connects to MQTT broker and subscribe to the topic
 */
void setup_mqtt(String clientName)
  {
  client.setServer(mqtt_server, mqtt_port);
  Serial.print("Connect to MQTT Broker ");
  Serial.println(mqtt_server);
  if (!client.connect((char*) clientName.c_str(), mqttUser, mqttPass))
    {
    Serial.print("failed, RC=");
    Serial.println(client.state());
    }

  // Loop until we're reconnected
  Serial.print(".");
  while (!client.connected())
    {
    delay(500);
    Serial.print(".");
    }

  Serial.println("");
  Serial.println("MQTT connected");

  // process incoming MQTT messages and maintain MQTT server connection
  client.loop();
  }

Ermitteln des Aktuellen Datums und Uhrzeit per NTP

/**
 * read_ntp - connects to ntp server, make an update and return epoch
 * @return: unsigned long, time in seconds since Jan. 1, 1970 (unix time) 
 */
unsigned long read_ntp()
  {
  // Start NTP client
  Serial.println("Start NTP Client.");
  timeClient.begin();

  // get time
  Serial.println("Get NTP datetime");
  timeClient.update();
  unsigned long epoch = timeClient.getEpochTime();

  // Stop NTP client
  Serial.println("Stop NTP Client.");
  timeClient.end();

  return(epoch);
  }

Auslesen des DHT22 Sensors

/**
 * read_dht - read the DHT22 sensor values and return results
 * @return: String, the mqtt payload of the readings
 */
String read_dht()
  {
  String payload;
  float t, h;

  Serial.println("Try to read from DHT sensor!");
  h = dht.readHumidity();
  t = dht.readTemperature();
  // Check if any reads failed and exit early (to try again).
  while (isnan(h) || isnan(t))
    {
    Serial.println("Failed to read from DHT sensor!");
    Serial.println("Try again in 5 seconds");
    // Wait 5 seconds before retrying
    delay(5000);
    h = dht.readHumidity();
    t = dht.readTemperature();
    }

  Serial.println("DHT Sensor readings:");
  Serial.print(t);
  Serial.println("°C");
  Serial.print(h);
  Serial.println("%");

  // adjust readings
  //h = h * 1.23;
  //t = t * 1.1;

  // convert float to String with precission of 1
  String tPayload = floatToStr(t, 1);
  String hPayload = floatToStr(h, 1);

  // get NTP Time
  unsigned long epoch = read_ntp();
  Serial.print("Zeit von NTP: ");
  Serial.println(epoch);
  // convert to datetime String
  char timestamp[64] = {0};
  const time_t epochTime = epoch;
  strftime(timestamp, sizeof(timestamp), "%Y-%m-%dT%H:%M:%SZ", localtime(&epochTime));
  Serial.println(timestamp);

  // create a JSON object
  StaticJsonBuffer<200> jsonBuffer; // Buffer calculation see "https://arduinojson.org/v5/assistant/"
  JsonObject& root = jsonBuffer.createObject();
  root["datetime"] = timestamp;
  root["location"] = topicLocation;
  root["temperature"] = tPayload;
  root["humidity"] = hPayload;
  // generate json string for MQTT publishing
  root.printTo(payload);
  Serial.println(payload);

  return(payload);
  }

Die Funktion void setup()

/**
 * setup - the main function after booting the microcontroller
 */
void setup()
  {
  // initialize serial console with 115200 bauts
  Serial.begin(115200);
  Serial.setTimeout(2000);
  // Wait for serial to initialize.
  while(!Serial) { }

  // connect to WiFi network
  setup_wifi();

  // connect to MQTT Broker
  setup_mqtt(clientName);

  // read DHT22 temperature and humidity
  String payload = read_dht();
  Serial.println("MQTT message payload to send to topic.");
  Serial.print("Payload: ");
  Serial.println(payload);

  // send payload to topic
  bool topicRetained = true;
  if (client.publish((char*) topic.c_str(), (char*) payload.c_str(), topicRetained))
    {
    Serial.print("published environmental data to topic ");
    Serial.println(topic);
    }
  else
    {
    Serial.print("FAILED to publish environmental data to topic ");
    Serial.println(topic);
    Serial.print("MQTT state: RC=");
    Serial.println(client.state());
    }

  // Wait 20 seconds before proceeding
  Serial.println("Waiting for 5 seconds");
  for (int i = 0; i < 5; ++i)
    {
    Serial.print(".");
    client.loop(); // process incoming MQTT messages and maintain MQTT server connection
    delay(1000); // 1 Second
    }
  Serial.println();

  // disconnecting from MQTT broker
  Serial.println("Disconnecting from MQTT Broker.");
  client.disconnect();

  // Wait 20 seconds before proceeding
  Serial.println("Waiting for 5 seconds");
  for (int i = 0; i < 5; ++i)
    {
    Serial.print(".");
    client.loop(); // process incoming MQTT messages and maintain MQTT server connection
    delay(1000); // 1 Second
    }
  Serial.println();

  // close WiFi connection
  Serial.println("Closing WiFi connection");
  espClient.stop();

  // going into deep sleep
  Serial.println("Going into deep sleep.");
  ESP.deepSleep(deepSleepTime);
  }


/**
 * loop - the ESP loop while the microcontroller is running
 */
void loop()
  {
  }

Ergebnis

Nachdem alles sauber aufgesetzt ist purzeln dann munter im MQTT Topic die Nachrichten rein:

MQTT.fx

Und werden im FHEM dargestellt:

FHEM

Die Ausgabe an der seriellen Console der Arduino IDE sieht so aus:

Arduino IDE

Und so sieht das Board aus (noch nicht zugeschnitten und das passende Gehäuse fehlt auch noch):

 

MQTT mit Eclipse Mosquitto

Zwischenzeitlich läuft das Zugangssystem stabil. In der Zwischenzeit sind einige Umstellungen erfolgt:

  • Umstellung auf Maven für die Java Builds
  • Veröffentlichung einiger OpenSource Artefakte auf Maven Central
  • Installation eines neuen Docker Hosts
  • Installation eines Eclipse Mosquitto MQTT Servers (als Docker Container)

Den MQTT Server hatte ich ursprünglich für die geplante IoT Infrastruktur (mit ESP8266 und ESP32 basierten Sensoren – hier werde ich auch berichten sobald die ersten Sensoren in Betrieb gehen) installiert. Ich habe diesen auf einem alten Raspberry Pi 1 Model A aufgesetzt als Docker Container.

Für das User Management System hatte ich bereits länger geplant einen Access Monitor geplant. Dieser sollte die letzten Zutritte visuell darstellen. Also wer hat wann zuletzt einen Zugang angefragt und wie war der Status dazu. Hier soll unter anderem auch ein Bild des Benutzers kommen angezeigt werden das aus dem LDAP Server geladen wird.

Ich hatte lange überlegt wie man einen solchen Monitor implementieren könnte. Vorallem wie dieser die Daten übermittelt bekommt.

Mit MQTT ist die Sache nun relativ einfach. Das Zugangskontrollsystem muss nur den Status eines Zutritts in einen Topic des MQTT Servers Publishen. Der Zutrittsmonitor subscribed auf diesen Topic und erhält so alle notwendigen Informationen zur Visualisierung.

Für den Publish der Nachricht aus dem Zugangskontrollsystem verwende ich nun den Eclipse Paho Java Client. Die Bibliothek war unkompliziert zu integrieren.
Der Zutritts Monitor ist eine Webanwendung. Für den subscripe verwende ich aktuell Eclipse Paho JavaScript Client. Dieser war etwas Tricky, denn das Beispiel auf der Seite hat bei mir nicht funktioniert.

Folgender JavaScript Sourcecode führte dann zum Erfolg:

<head>
<script> // configuration var mqtt; var reconnectTimeout=2000; var host="myMQTTServer"; var port=9001; var clientId="oitc-acs-monitor"; var topic="/oitc-acs" // called when the client connects function onConnect() { // Once a connection has been made, make a subscription and send a message. console.log("Successfully connected to " + host + ":" + port); console.log("Subscribing to topic " + topic); mqtt.subscribe(topic); } // called when a message arrives function onMessageArrived(message) { // console.log(message.payloadString); obj = JSON.parse(message.payloadString); console.log(obj); } function MQTTconnect() { console.log("Try to open connection to " + host + ":" + port); mqtt = new Paho.MQTT.Client(host,port,clientId); mqtt.onMessageArrived = onMessageArrived; // Valid properties are: timeout userName password willMessage // keepAliveInterval cleanSession useSSL // invocationContext onSuccess onFailure // hosts ports mqttVersion mqttVersionExplicit // uris var options = { timeout: 3, userName: "acsMonitorUser", password: "myUsersPass", keepAliveInterval: 60, useSSL: true, onSuccess: onConnect, mqttVersion: 3, }; mqtt.connect(options); } </script> </head> <body>
<script>
MQTTconnect(); </script>
</body>

Wenn die Seite lädt, verbindet sich die Webanwendung mit dem MQTT websocket und subscribed den topic. Da die Nachrichten „retained“ vom Zugangskontrollsystem gesendet werden, wird auf jeden Fall der letzte Zugriff im JavaScript consolen Log angezeigt. Bei weiteren Zugängen erscheinen diese ebenfalls im Log.

Die nächsten Schritte sind nun die Anzeige auf der HTML Seite. Ich werde hier mit jQuery arbeiten um mir das Leben etwas einfacher zu machen. Einen ersten Prototypen habe ich bereits am Laufen. Die nächste Herausforderung ist nun, das Bild der Person noch aus dem LDAP Server zu laden. Ich werde berichten, sobald ich hier wieder ein Stück weiter gekommen bin.

Abschließend noch das aktuelle Architektur Schaubild: