Erste IoT-Schritte: Laufende Temperaturmessung mit dem Arduino und ThingSpeak

Bei meinem letzten Beitrag zum Thema hatte ich schon beschrieben, wie man den Arduino mit Hilfe eines E32-Shields ins WLAN bringt. Nun sollte das Ganze einen nützlichen Zweck erfüllen. Für den Anfang reichte mir der Anschluss eines einfach DS18B20-Temperatursensors und die fortlaufende Dokumentation der Messung bei einem IoT-Server. Natürlich kann man auch selbst einen MQTT-Server (sprich: Mosquito-Server) aufsetzen und verwenden, aber ThingSpeak bietet sich hier einfach aus mehreren Gründen an. Vor allem auch deshalb, weil es bereits eine fertige Bibliothek für den Arduino gibt, sodass man die Befehle zum Senden der Werte auf einem hohen Abstraktionslevel belassen kann.

Wie so oft gilt auch hier: Natürlich gibt es das alles schon und es ist auch alles im Netz frei verfügbar und dokumentiert. Aber es kostet dann doch relativ viel Aufwand, alles zusammen zu tragen und im Detail zu verstehen. Daher schreibe ich meine Vorgehensweise hier strukturiert auf. Ebenfalls gilt: Natürlich braucht der wahre Profi den Arduino gar nicht dafür, ein kleiner ESP8266 genügt ebenso. Aber es geht ja auch ein wenig um den Spaß und um das Verständnis des Ganzen, und dafür ist der Arduino einfach besser geeignet. Natürlich wäre das Gleiche auch mit einem Raspberry Pi machbar, der große Vorteil hier liegt darin, dass man sich ein schönes Python-MQTT-Skript schreiben kann und die Fallen vom Arduino-C etwas umschifft.

Doch zurück zum Arduino-Beispiel. Die Verkabelung der Hardware ist – basierend auf der bereits bestehenden Kombination aus Arduino und ESP-Shield – denkbar einfach. Der Temperatursensor bringt drei Kabel mit, die an 3,3V- (rot), GND- (schwarz) und einen beliebigen Digital-PIN (gelb) des Arduino bzw. des aufgesteckten Shields angeschlossen werden. Fertig.

Die eigentliche Kunst liegt also in der Software. Als Basis nehme ich den verkürzten Sketch aus dem WLAN-Anschluss-Beispiel:

#include "WiFiEsp.h"
#include "SoftwareSerial.h"

SoftwareSerial Serial1(3, 2); // RX, TX

char ssid[] = "MeinTollesWLAN";
char pass[] = "**********";
int status = WL_IDLE_STATUS;
WiFiEspClient client;

void setup(void) {
Serial.begin(9600);
Serial1.begin(9600);
WiFi.init(&Serial1);

while ( status != WL_CONNECTED) {
Serial.print("Verbindungsaufbau zu ");
Serial.println(ssid);
status = WiFi.begin(ssid, pass);
}

Serial.println("Verbindung hergestellt!");
Serial.println();
printWLAN();
Serial.println();
}

void loop(void) {
}

void printWLAN()
{
IPAddress ip = WiFi.localIP();
Serial.print("IP-Adresse: ");
Serial.println(ip);
Serial.print("SSID: ");
Serial.println(WiFi.SSID());
long rssi = WiFi.RSSI();
Serial.print("Signalstaerke (RSSI): ");
Serial.println(rssi);
}

Wie man leicht sieht, macht der Sketch vorerst nichts abgesehen vom Verbindungsaufbau zum WLAN. Soweit so gut. Beginnen wir mit den Bibiotheken und Konstanten, die wir für alles Weitere benötigen. Die Bibliotheken heißen „OneWire“ (Temperatursensor) und „ThingSpeak“ (Verbindung zum IoT-Server).

ThingSpeak kann man auf der folgenden Seite herunterladen und dann der (sehr kurzen) Installationsanleitung folgen:

https://github.com/mathworks/thingspeak-arduino

Und wenn man gerade schon dabei ist, verfährt man ebenso mit der hier erhältlichen OneWire-Library.

Welche Konstanten werden nun benötigt? Zum einen die Nummer des PINs, an dem das gelbe Datenkabel des Sensors angeschlossen wurde. In meinem Beispiel ist das die Nr. 5. Um mit ThingSpeak arbeiten zu können, muss zudem ein Account bei dem Dienst angelegt werden. Nach dem Login kann dann ein einzelner Channel erstellt werden, der künftig die Daten entgegen nimmt. Die Nummer des Channels sowie unser APIKey von ThingSpeak sind die letzten benötigten Konstanten:

#include "OneWire.h"
#include "ThingSpeak.h"

int Sensor_Pin = 5;
unsigned long Channel = 123456789abcdef;
const char * APIKey = "************";

Bei unserer Messung verwenden wir ein Objekt aus der Klasse OneWire, dem als Parameter die Nummer des PINs übergeben wird:

OneWire ds(Sensor_Pin);

In der setup-Funktion wird die Kommunikation mit dem ThingSpeak-Server initialisiert, dabei wird die WLAN-Verbindung als Übertragungsweg übergeben:

ThingSpeak.begin(client);

Kommen wir zur Loop-Funktion. Diese soll im Grunde folgende Elemente enthalten: Messen, Ausgeben, Übertragen, Warten. Das Messen ist dabei mit riesigem Abstand die komplexeste Aufgabe und wird daher in eine eigene Funktion „getTemp“ ausgelagert. Der Rest ist relativ einfach. Damit nur echte Messwerte eingetragen werden, verwende ich „-100“ als Fehlerwert, alles darüber hinaus wird an ThingSpeak übertragen. Dabei müssen Channel, das Datenfeld (in unserem Fall einfach das einzige, also 1), der gemessene Wert sowie der APIKey übertragen werden. ThingSpeak kann man nicht mit beliebig vielen Werten fluten, 20 Sekunden Wartezeit zwischen den Messungen sind hier i.d.R. angemessen. Somit ergibt sich die Loop-Funktion:

void loop(void) {
float temperatur = getTemp();
Serial.println(temperatur);
if ( temperatur > -100) {
ThingSpeak.writeField(Channel, 1, temperatur, APIKey);
}
delay(20000);
}

Tja, und nun geht’s ans Eingemachte, namentlich um die Funktion „getTemp“. Ich gebe zu, dass ich – der ich nie wirklich C gelernt habe – dann doch einige Zeit intensiv darüber nachdenken musste, um die gefundenen Programmierbeispiele zu verstehen. Ich habe sie hier auf das Nötigste gekürzt und versuche sie zu erläutern.

Wir benötigen zwei Byte-Arrays namens „addr“ (für die Adressdaten des Sensors, es könnte mehrere geben) und „data“ (für die Messwerte). Zudem gilt es, ein paar Fehler abzufangen, z.B. Fehler in der Prüfsumme (CRC) oder gar einen nicht gefundenen oder nicht unterstützten Adapter. In all diesen Fällen wird unser Fehlerwert „-100“ zurückgegeben:


byte data[12];
byte addr[8];

if ( !ds.search(addr)) {
ds.reset_search();
return -100;
}

if ( OneWire::crc8( addr, 7) != addr[7]) {
Serial.println("CRC fehlerhaft!");
return -100;
}

if ( addr[0] != 0x10 && addr[0] != 0x28) {
Serial.print("Kein Sensor erkannt");
return -100;
}

Durch den Aufruf von „ds.search(addr)“ wird der Array praktischerweise direkt mit den Adressdaten des Sensors gefüllt, sodass wir nun – da keine Fehler aufgetreten sind – damit arbeiten können. Die nächsten Schritte sind im Einzelnen: Reset der Kommunikation, Auswahl des Sensors, Durchführen einer Messung und schließlich das Auslesen der Werte aus einem Zwischenspeicher, Speichern der Werte in unserem Datenarray. Anschließend kann wieder ein Reset der Suche nach Sensoren erfolgen.

ds.reset();
ds.select(addr);
ds.write(0x44); // Kommando: Messung durchfuehren
ds.reset();
ds.select(addr);
ds.write(0xBE); // Kommando: Werte auslesen
for (int i = 0; i < 9; i++) {
data[i] = ds.read();
}
ds.reset_search();

Fast fertig. Doch unsere Messwerte sind noch ein wenig „kryptisch“ und entsprechen nicht gerade dem, was wir aufzeichnen wollen. Die eigentlich interessanten Werte „MSB“ (most significant byte) und „LSB“ (least significant byte) stecken in unseren Datenfeldern 1 bzw. 0:

byte MSB = data[1];
byte LSB = data[0];

Sie enthalten die gemessene Temperatur in Binärdarstellung, wie ein Blick in das Datenblatt des DS18B20 verrät:

Um daraus nun einen „gewohnten“ Temperaturwert zu erhalten, bedarf es einer bitweisen Verschiebung des MSB um 8 Stellen nach links und einer bitweisen Verknüpfung mit dem LSB (und gleichzeitig einer Umwandlung in eine Fließkommazahl zur Basis 10):

float tempRead = ((MSB << 8) | LSB);

Wie man dem Datenblatt entnehmen kann, enthält das Ganze aber auch Nachkommastellen, das wurde bei der Umwandlung nicht berücksichtigt. Durch welche Zahl muss nun geteilt werden? Da unsere eigentliche „Basis“ (die 2^0 – Stelle) an vierter Position befindet, ist die Zahl um den Faktor 2^4 = 16 zu hoch. Es folgt:

float TemperatureSum = tempRead / 16;
return TemperatureSum;

Fertig! Hier noch einmal der komplette Sketch, viel Spaß beim Ausprobieren:


#include "OneWire.h"
#include "ThingSpeak.h"
#include "WiFiEsp.h"
#include "SoftwareSerial.h"

int Sensor_Pin = 5;
unsigned long Channel = 123456789abcdef;
const char * APIKey = "************";

OneWire ds(Sensor_Pin);

SoftwareSerial Serial1(3, 2); // RX, TX

char ssid[] = "MeinTollesWLAN";
char pass[] = "**********";
int status = WL_IDLE_STATUS;
WiFiEspClient client;

void setup(void) {
Serial.begin(9600);
Serial1.begin(9600);
WiFi.init(&Serial1);
ThingSpeak.begin(client);

while ( status != WL_CONNECTED) {
Serial.print("Verbindungsaufbau zu ");
Serial.println(ssid);
status = WiFi.begin(ssid, pass);
}

Serial.println("Verbindung hergestellt!");
Serial.println();
printWLAN();
Serial.println();
}

void loop(void) {
float temperatur = getTemp();
Serial.println(temperatur);
if ( temperatur > -100) {
ThingSpeak.writeField(Channel, 1, temperatur, APIKey);
}
delay(20000);
}

float getTemp(){

byte data[12];
byte addr[8];

if ( !ds.search(addr)) {
ds.reset_search();
return -100;
}

if ( OneWire::crc8( addr, 7) != addr[7]) {
Serial.println("CRC fehlerhaft!");
return -100;
}

if ( addr[0] != 0x10 && addr[0] != 0x28) {
Serial.print("Kein Sensor erkannt");
return -100;
}

ds.reset();
ds.select(addr);
ds.write(0x44); // Kommando: Messung durchfuehren
ds.reset();
ds.select(addr);
ds.write(0xBE); // Kommando: Werte auslesen

for (int i = 0; i < 9; i++) {
data[i] = ds.read();
}

ds.reset_search();

byte MSB = data[1];
byte LSB = data[0];

float tempRead = ((MSB << 8) | LSB);
float TemperatureSum = tempRead / 16;
return TemperatureSum;
}

void printWLAN()
{
IPAddress ip = WiFi.localIP();
Serial.print("IP-Adresse: ");
Serial.println(ip);
Serial.print("SSID: ");
Serial.println(WiFi.SSID());
long rssi = WiFi.RSSI();
Serial.print("Signalstaerke (RSSI): ");
Serial.println(rssi);
}

ESP-12E WLAN-Shield auf dem Arduino Uno

Im Bestreben, einen Arduino Uno für diverse Zwecke drahtlos ans Netz zu bringen, stolperte ich bei EBay über das ESP-12E-Shield, welches selbst wiederum auf dem bekannten ESP8266-Microcontroller basiert. Incl. Versand aus dem fernen Osten bekommt man das gute Stück für unter 10 Euro, sodass ich es auf einen Versuch ankommen ließ. Einige Wochen später kam das Päckchen an, allerdings nur das Board selbst, ohne jeglichen Beipackzettel. Nun gut, das allwissende Internet hält ja mit Sicherheit entsprechendes Knowhow bereit. Wie ich dann feststellen musste: Jein. Wie so oft findet man alle nötigen Informationen, muss sie sich aber einigermaßen mühsam zusammenstellen und sortieren. Daher möchte ich hier das Verfahren nochmal sauber dokumentieren, für alle die vor dem gleichen Problem stehen.

Warnhinweis: Das hier beschriebene Verfahren habe ich selbst mit meinen genannten Bauteilen durchgeführt und es klappte problemlos und offenbar ohne Schäden. Dennoch keine Garantie, dass das immer und überall so sein muss. Also: Alles geschieht auf eigene Gefahr!

Schritt 1: Anpassung der Baud-Rate

Quelle: https://claus.bloggt.es/2017/01/14/using-esp8266-shield-esp-12e-elecshop-ml-by-wangtongze-with-an-arduino-uno/

Offensichtlich kommunizieren Arduino und ESP-12E bzw. ESP8266 deutlich stabiler mit einer Baud-Rate von 9600. Daher muss noch vor dem Einsatz des Shields diese über den Debug-Port umgestellt werden. Dazu nimmt man vier kleine Jumper Wire m/w und schließt sie wie folgt an, ohne das Shield auf den Arduino zu stecken:

Debug Port TX => Uno Pin 1 (TX)
Debug Port RX => Uno Pin 0 (RX)
Debug Port 5V => Uno 5V
Debug Port GND => Uno GND

Die vier Dip-Schalter auf dem ESP-12E können auf „Off“ bleiben. Anschließend den Arduino per USB an den Rechner anschließen, in der IDE die serielle Konsole aufrufen (Option „Both NL & CL“ und Baud-Rate 115200) und dieses Kommando durchgeben:

AT+UART_DEF=9600,8,1,0,0

Anschließend alles schließen und den Arduino wieder vom PC lösen. Die Kabel werden nicht mehr benötigt und können abgezogen werden.

Schritt 2: Shield aufstecken und anschließen

Quelle: https://arduino.stackexchange.com/questions/24919/how-to-connect-wi-fi-shield-esp-12e-esp8266-uart-wifi-wireless-shield-with-ardui

Das Shield wird nun wie vorgesehen auf den Arduino gesteckt, sodass alle Pins in die Steckerleisten des Arduino passen. Dann benötigen wir wieder zwei Jumper Wire m/w und verbinden:

Debug Port RX => Pin 2 des Arduino (der sich ja nun auf dem Shield selbst befindet)
Debug Port TX => Pin 3 des Arduino (der sich ja nun auf dem Shield selbst befindet)

Hier zur Verdeutlichung ein Foto:

Schritt 3: Nötige Library einbinden

Quelle: https://github.com/bportaluri/WiFiEsp/wiki

Den Arduino nun wieder per USB an den Rechner anschließen und die IDE aufrufen. Es wird die passende Bibliothek benötigt, damit man das WLAN-Modul ansteuern kann. Dazu das Menü „Sketch -> Include Library -> Manage Libraries…“ aufrufen, dort nach „WiFiESP“ suchen und das Paket „WiFiEsp by bportaluri“ installieren.

Schritt 4: Testskript anpassen und mit WLAN verbinden

Quelle: https://github.com/bportaluri/WiFiEsp/blob/master/examples/ConnectWPA/ConnectWPA.ino

Das nötige Testskript kann man hier finden. Einfach per Copy&Paste in einen neuen Sketch einfügen und die folgenden Zeilen anpassen:

Zeile 16 => hier die korrekten Ports eintragen, in unserem Fall „Serial1(3, 2)“
Zeile 19 => die SSID des eigenen WLANs eintragen
Zeile 20 => den Key des eigenen WLANs eintragen

Den Sketch nun kompilieren, hochladen und mit dem seriellen Monitor das Ergebnis beobachten. Idealerweise wird dort nun der Erfolg und die vom WLAN-Router erhaltene IP vermeldet.

Verschlüsselung dank Let’s Encrypt

Dank der Initiative „Let’s Encrypt“ kann nun jede(r) Betreiber(in) von Webseiten diese kostenfrei auch per https bereitstellen, also den Transportweg mit SSL verschlüsseln. Dies war zwar bislang auch mit anderen Anbietern möglich, aber die damit erstellten Zertifikate waren nicht in den gängigen Browsern vertreten, sodass beim ersten Besuch der Seite stets eine Warnmeldung erschien. Bei „Let’s Encrypt“ ist dies nicht mehr so.

Dementsprechend habe ich auch die Domains auf meinem eigenen privaten Server nun per https verfügbar gemacht. Eigentlich wollte ich an dieser Stelle eine kleine Anleitung verfassen, da ich mir meinen Erfolgsweg bei diversen Blog- und Foreneinträgen zusammengesucht hatte. Inzwischen habe ich aber einen Beitrag entdeckt, der exakt meine Vorgehensweise beschreibt, incl. mehrerer Domains in einem Server und Auto-Renew per Cronjob. Daher verweise ich unten auf diesen Artikel von Dominic Pratt und ergänze lediglich, dass man inzwischen das Auto-Renew auch einfacher machen kann, nämlich per Crontab-Eintrag á la:

30 2 * * 1 /opt/letsencrypt/letsencrypt-auto renew >> /var/log/le-renew.log

Zudem sollte man unbedingt darauf achten, ausschließlich sichere Protokolle und Cipher Suites zu aktivieren. Leider unterstützen nicht alle Server und Clients automatisch die neuesten und besten Einstellungen. Ein guter Kompromiss ist derzeit im Apache z.B.:

SSLProtocol all -SSLv2 -SSLv3
SSLCipherSuite HIGH:MEDIUM:!ADH:!MD5:!RC4

Damit erreicht man im SSL-Test (https://www.ssllabs.com/ssltest) immerhin Grade A. Besser ginge es noch bspw. mit der Aktivierung von Forward Secrecy.

Hier nun der angesprochene Artikel:

Let’s Encrypt nutzen – eine Anleitung

Kampf dem Herzinfarkt – zu lauten Sound unter Linux beheben

Der kleine Quickie am Montagmorgen: Seit Jahren ärgert mich das Problem, dass unter Linux speziell USB-Soundkarten viel zu laut angesteuert werden. Entweder steigt die Lautstärke irgendwo zwischen Stufe 20 und 30 sprunghaft von „einsamer Bergsee bei Windstille“ auf „startender Düsenjet“ oder ist (mit dem altbekannten „ignore_dB=1“-Trick) schon bei Stufe 2 eigentlich viel zu laut.

Nun bin ich zufällig im Blog von Chris Jean über eine Alternativlösung gestolpert, die sehr einfach umzusetzen ist und einfach funktioniert. Es gilt in der Datei

/usr/share/pulseaudio/alsa-mixer/paths/analog-output.conf.common

im Block

[Element PCM]

die Zeile

volume = merge

zu ersetzen durch

volume = ignore
volume-limit = 0.0075

Und schön lässt sich das Ganze wieder wunderbar regeln. Zuvor kann man im Alsamixer noch die Soundkarten feinjustieren, damit die Master-Ausgabe optimal die eigenen Bedürfnisse abdeckt.

Projekt Mepevea – D’r Zoch kütt!

Als umweltbewusster oder zumindest geiziger Mitarbeiter des Öffentlichen Dienstes verzichtet man in der Regel bei längerer Anfahrt zum Arbeitsplatz auf den privaten PKW und nimmt freudig am öffentlichen Personennahverkehr (ÖPNV) teil. Sprich: Die Deutsche Bahn (und in Köln auch die KVB) ist unser Freund! Gerüchteweise sind die bereitgestellten Verkehrsmittel nicht immer dann vor Ort, wenn man es laut Fahrplan erwarten könnte. Damit man die daraus resultierende Wartezeit nicht am Bahnsteig, sondern am Frühstückstisch bzw. im bequemen Bürosessel verbringen kann, sind aktuelle Informationen über die Verspätungen unerlässlich.

Nun hat sich in der Vergangenheit der Service der DB dahingehend deutlich verbessert. So sind die Verspätungsinformationen inzwischen minutengenau und in Realzeit sowohl im Web als auch mittels der App „DB Navigator“ abrufbar. Der o.g. Mitarbeiter des Ö.D. ist allerdings nicht nur geizig (jaja, und umweltbewusst), sondern auch klickfaul und noch dazu ein Spielkind. So kam ich auf die Idee, sowohl in meinem trauten Heim als auch im Büro mittels ohnehin vorhandener Technik einen (für mich) optimalen Anzeigebildschirm zu basteln.

Dieser sollte nicht nur die aktuellen Verspätungen meiner Zugverbindungen, sondern auch weitere interessante Informationen anzeigen, genauer gesagt: Aktuelle Nachrichten, Wettervorhersage und (zuhause) zusätzlich das Kamerabild einer per WLAN verbundenen IP-Kamera. Als Hardware kamen ein günstiger und dank Notebook-Anschaffung ohnehin kaum noch gebrauchter PC-Bildschirm sowie zeitgemäß ein Raspberry Pi zum Einsatz. Das System sollte in jedem Fall ohne weitere Peripherie, speziell ohne Maus und Tastatur, auskommen. Softwareseitig setzte ich daher auf Google Chrome im Kiosk-Modus. Mittels der Erweiterung „Easy Auto Refresh“ kann man dafür sorgen, dass Chrome die angezeigte Seite automatisch einmal pro Minute neu lädt. Das Kamerabild läuft ohnehin im Streaming-Mode.

Der graphische Desktop des Raspi musste so eingestellt werden, dass er sich nicht automatisch abschaltet. Die Kontrolle über die Anzeige sollte ausschließlich per Ein/Aus-Knopf des Monitors ablaufen. Dies erreicht man über die eine Einstellung in LightDM.

Da ich mir die Installation und Konfiguration eines Webservers sparen wollte, verwende ich eine einfache lokale HTML-Seite auf dem Raspi. Die beiden gewünschten Elemente „Aktuelle Nachrichten“ und „Wettervorhersage“ sind sehr leicht über passende Widgets realisierbar. Ich habe hierzu die Angebote von wetterdienst.de und rp-online genutzt, es gibt jedoch zahlreiche weitere Anbieter.

mepevea

Richtig interessant wurde es dann bei der Einbindung der Verspätungsanzeige. Wie ich feststellen musste, bietet die Bahn leider keine geeignete API zu diesem Zweck. Mir blieb nichts anderes übrig als die entsprechende Webseite zu parsen. Diese Erkenntnis war die Geburtsstunde von Projekt „Mepevea“ (MEin PErsönlicher VErspätungsAnzeiger).

Wie erwähnt wollte ich auf die Installation und den Betrieb eines Webservers verzichten. Die Anzeige soll ja ohnehin nur für mich persönlich laufen. Daher musste ich die eigentliche Logik nebst Parser in ein Pythonskript packen, welches per Cronjob aufgerufen wird (ja, ich arbeite unter Linux und ignoriere Windows seit Jahren – die Portierung sollte aber kein großes Problem darstellen). Als Basismodul für den Parser dient natürlich „BeautifulSoup“, darüber hinaus werden urllib zum Abruf der Seite und einige weitere Module benötigt. Der Start lautet also:

#!/usr/bin/python
# -*- coding: utf-8 -*-
import bs4, urllib2, time, fileinput, sys, urllib

„fileinput“ verwende ich, um später den <div>-Block im HTML durch die korrekten Daten auszutauschen, z.B.:

for line in fileinput.FileInput("/home/pi/anzeige/bahnlinks.html",inplace=1):
if line.startswith('<div id="bahn">'):
   line = text
   sys.stdout.write(line)

Natürlich macht es Sinn, abhängig vom Wochentag und der Tageszeit die Anzeige zu variieren (Hinfahrt, Rückfahrt, Abend/Wochenende), also z.B.:

timestamp = time.localtime(time.time())
if timestamp[6] > 4:
   textlist.append("<b>Bahnanzeige erst am Montag wieder! Schönes Wochenende!</b>")

Hier wird schon klar: Individuelle Anpassung ist unerlässlich und ich kann die Beispiele nur anreißen. Keine Sorge: Am Ende werde ich als „großes Beispiel“ mein komplettes Skript bereitstellen.

Zentrales Element des Skriptes ist die Parserfunktion. Sie erhält als Parameter die URL der Bahn (dazu später) und jagt sie durch BeautifulSoup:

def parser(url):
   page = urllib2.urlopen(url).read()
   soup = bs4.BeautifulSoup(page)

Man möge mir an dieser Stelle glauben, dass wir die spannenden Inhalte erhalten, wenn wir nach den Keywords, genauer gesagt den <td>-Klassen „overview timelink“ und „overview tprt“ suchen:


zeilen = soup.find_all('td', {"class" : "overview timelink"})
verspaetungen = soup.find_all('td', {"class" : "overview tprt"})

Schon hier erkannt man, wo das größte Problem unserer schönen Bastelei liegt: Sollte die Bahn die Klassennamen aus irgendwelchen Gründen ändern, funktioniert natürlich nichts mehr. Das gleiche gilt für die URLs und die HTML-Struktur. Genau aus diesem Grund gibt es ja i.d.R. kapselnde APIs, aber die stehen hier wie gesagt nicht zur Verfügung.

Standardmäßig erhält man von der Bahn die nächsten drei Züge ab dem definierten Zeitpunkt. Ich habe die finale Version noch so erweitert, dass man dies variieren kann, aber das würde hier zu weit führen. Ebenso müsste ich nun eigentlich auf die Details von BeautifulSoup eingehen, um den folgenden Codeblock zu erläutern. Aber auch dies möchte ich mir sparen und auf die gute Online-Dokumentation des Moduls verweisen. Unsere Verbindungen sowie die aktuellen Verspätungen erhalten wir so:


parsedtext = ''
zaehler = 0
for zeile in zeilen:
   for zelle in zeile.children:
      parsedtext += zelle.contents[0]
   parsedtext += '<span style="color: red;">'
   for verspaetung in verspaetungen[zaehler].children:
      if str(verspaetungen[zaehler]).count("okmsg") > 1 or str(verspaetungen[zaehler]).count("red") > 1:
         parsedtext += verspaetung.contents[0]
         break
   parsedtext += '</span>'
   zaehler += 1

Ich bin mir zu 99% sicher, dass dies nicht die eleganteste Version ist, um die Informationen zu erhalten und aufzubereiten. Aber sie funktioniert. Wer das Ganze kürzer, schöner und verständlicher hinbekommt, ohne dass die Funktionalität leidet, möge sich bei mir melden.

Kommen wir nun zu den benötigten URLs. In einer ersten Version hatte ich pro Zug eine URL auf Basis des Bahntools „query2.exe“ verwendet, die auch deutlich einfacher zu parsen war (Anmerkung: Bitte von der Endung „.exe“ nicht täuschen lassen: Es handelt sich um einen Webservice, nicht um ein lokales Programm.). Leider musste ich feststellen, dass die Bahn bei jeder (geplanten) Mini-Fahrplanänderung die URL komplett verändert. Auf Dauer war das also leider keine Lösung. Stattdessen verwende ich nun die „Vorstufe“ namens „query.exe“. Diese hat klar definierte und – hoffentlich – dauerhaft beständige Parameter. Als Parameter benötigen wir den Code des Startbahnhofs, den Code des Zielbahnhofs und die Startzeit.

Während die Startzeit natürlich jedem selbst überlassen bleibt und einfach in der Form hh:mm verwendet wird, muss man sich die Codes (sog. IBNR) der Bahnhöfe einmalig heraussuchen. Dies geht zum Glück sehr einfach mittels einer Onlinesuche.

Lautet die IBNR des Startbahnhofs bspw. 8000208, die des Zielbahnhofs 8000133 und die gewünschte Startzeit ist 17:00 Uhr, lautet die gesuchte URL:

http://reiseauskunft.bahn.de/bin/query.exe/dox?S=8000208&Z=8000133&time=17:00&start=1

Damit lässt sich nun für jede beliebige Verbindung und Kombination von Tageszeiten ein passender Anzeiger (eben ein „Mepevea“) bauen.

Für weitere Ideen, Verbesserungsvorschläge etc. bin ich jederzeit dankbar. Und wenn jemand die Bahn überreden könnte, doch mal eine entsprechende API bereitzustellen, das wäre ein Traum. 😉

Wie versprochen: Den vollständigen Text des Skriptes sowie eine Beispiel-HTML-Seite findet man unter http://dl.distinguish.de/mepevea.zip

MySQL-Upgrade wie es sein sollte

Nicht, dass ich im Allgemeinen zu extremer Prokrastination neige, aber im Falle des netten kleinen ToDos „Update der MySQL-Server auf 5.5“ bin ich eindeutig schuldig. Das schob ich nämlich bereits seit Ende 2012 leise vor mir her, da ich den Aufwand für außerordentlich hoch hielt. Zur Ausgangssituation: Aus historischen Gründen hatten wir eine recht heterogene Landschaft im Bereich MySQL-DB-Server, bestehend aus:

* 1 Master-Slave-Cluster, Version 5.1, basierend auf original MySQL-RPMs

* 2 Master-Slave-Clustern, Version 5.1, basierend auf IUS-RPMs

* 1 Master-Slave-Cluster, Version 5.5, basierend auf original MySQL-RPMs

* 1 Master-Master-Cluster, Version 5.1, basierend auf original MySQL-RPMs

Wohlgemerkt: Alle auf RHEL5! Die nun alle auf eine homogene Basis zu stellen (nämlich Version 5.5, basierend auf IUS-RPMs) und dabei sowohl die Daten als auch die Replikation leben zu lassen, schien nur durch komplette Dump-Restores mit zwischenzeitlicher Neuinstallation der Pakete zu funktionieren. Da wir hier über eine Datenmenge von insgesamt etwa einem Terabyte sprechen, rechnete ich mit einer ziemlich großen Downtime.

Ein wenig Recherche nach der optimalen Vorgehensweise brachte jedoch zutage, dass die Jungs und Mädels der IUS Community (http://iuscommunity.org), deren Repositories wir ja auch oft und gerne benutzen, tatsächlich auch an solche wirren Zustände gedacht haben. Genauer gesagt gibt es das hübsche Plugin „replace“ für die Paketverwaltung yum. Es lässt sich – natürlich nach Integration des IUS-Repos – per „yum install yum-plugin-replace“ installieren und eröffnet die Möglichkeit, yum mit der Option „replace-with“ aufzurufen.

So führt das folgende Kommando bspw. dazu, dass ein bisheriger 5.1-Server auf IUS-Basis nahtlos durch einen 5.5-Server ersetzt wird:

yum replace mysql51-libs --replace-with mysql55-libs

Der Wechsel ist aber – und das ist das eigentliche Erstaunliche – auch aus den MySQL-RPMs heraus möglich:

yum replace MySQL-server-community --replace-with mysql55-server

yum bzw. das Plugin löst selbst alle nötigen Abhängigkeiten auf. Ggf. beschwert es sich, dass die Herkunft einiger beteiligter Pakete nicht ermittelt werden konnte, dies kann man aber problemlos ignorieren.

Letztendlich war es mir so möglich, das gefürchtete Upgrade für alle Server in einer Stunde durchzuführen, ohne dass ich bislang ein Problem feststellen konnte. Einige Hinweise gilt es aber noch zu beachten:

  • Laut MySQL sollte immer der Slave zuerst aktualisiert werden.
  • Nach dem Upgrade von Slave und Master muss auf dem Master das Kommando „mysql_upgrade“ ausgeführt werden, um fehlende MySQL-Tabellen zu ergänzen. Zudem werden bei der Gelegenheit alle Tabellen geprüft und ggf. repariert.
  • Beim Wechsel von MySQL 5.5 aus MySQL-RPMs auf MySQL 5.5 aus IUS-RPMs musste ich zuvor manuell das Paket „MySQL-shared-compat“ entfernen. Das konnte das Plugin aus irgendwelchen Gründen nicht lösen.
  • Bei Statement-basierter Replikation wirft MySQL 5.5 im Gegensatz zu 5.1 Warnungen, wenn Anweisungen bspw. nicht-deterministische Ergebnisse liefern. Diese Warnungen waren nicht zu unterdrücken (jedenfalls habe ich den Schalter nicht gefunden), obwohl wir uns der Tatsache bewusst waren und die entsprechende Datenbank ohnehin von der Replikation ausgenommen hatten. Ein somit ohnehin fälliger Wechsel zum „Mixed“-Format war also unvermeidlich (Schalter „binlog-format = MIXED“)
  • In MySQL 5.5 fallen einige Konfigurationsoptionen weg bzw. sollten durch ihre Nachfolger ersetzt werden. Die Ersetzung kann bereits vor dem Upgrade in der my.cnf durchgeführt werden. Aufgefallen sind bei mir:
    • skip-locking
    • log-err (ersetzt durch log-error)
    • key_buffer (ersetzt durch key_buffer_size)
    • thread_cache (ersetzt durch thread_cache_size)

Mediawiki und LDAP – „Wollen Sie sich wirklich sperren?“

Ein lange gehegter Wunsch war die Anbindung der zentralen Mediawiki-Installation an die LDAP-Authentifizierung. Dank der Mediawiki-Erweiterung „LDAPAuthentication“ war dies prinzipiell auch überhaupt kein Problem. Dann aber kamen seltsame Fehlermeldungen, dass Benutzer plötzlich gesperrt seien, die definitiv noch Zugang zum Wiki haben sollten. Hier muss man nun wissen, dass nicht alle Wiki-User im LDAP verzeichnet sind, sodass zusätzlich auch lokale Accounts parallel existieren müssen. Also gibt es auch unabhängig vom LDAP die Möglichkeit, Benutzer direkt in Mediawiki zu sperren.

Ein Test meinerseits ergab die schöne Fehlermeldung „Sie sind dabei sich selbst zu sperren. Wollen Sie das wirklich?“ Okay, irgendwie geraten also die User bei der Sperrung durcheinander. Aber warum? Die Lösung fand ich in den Tiefen des PHP-Codes von Mediawiki, indem ich alle beteiligten Funktionen nacheinander durchtestete. In der Datei „functions/User.php“ heißt es in der Funktion „newFromName“:

$name = $wgAuth->getCanonicalName( $t->getText() );

Der LDAP-Server wird also nach dem kanonischen Namen des Users gefragt. In der Regel einfach nur unnötig, führt es genau hier sogar zu einem sehr unerfreulichen Ergebnis: Existiert der User nämlich nicht (mehr) im LDAP, werden ungültige Werte zurückgegeben. Dies führt dann dazu, dass plötzlich ein ganz anderer User – in diesem Fall z.B. ich selbst – als „Target“ dient.

Eine Änderung der o.g. Zeile brachte dementsprechend das gewünschte Verhalten:

$name = $t->getText();

Prüfung von Sicherheitszertifikaten

Die Prüfung der von Servern verwendeten Zertifikate hinsichtlich ihrer Gültigkeit  und Vertrauenswürdigkeit ist heutzutage ein elementarer Bestandteil der persönlichen Sicherheit im Internet. Viel zu oft werden solche Hinweise zwar einfach weggeklickt, spätestens bei Shops, die die Angabe der Konto- oder Kreditkartendaten erfordern, und erst recht beim Online-Banking sollten aber wirklich alle Internetnutzer misstrauisch werden, sobald der Browser eine entsprechende Warnung anzeigt.

Aus diesem Grund ist auch die Uni Köln seit Jahren einer Certificate Authority angeschlossen, die in allen gängigen Browsern und Mailclients als vertrauenswürdig bekannt ist, namentlich der Root-CA der Deutschen Telekom. Denn auch an der Uni müssen z.T. sehr vertrauliche Daten über das Netz geschickt werden, z.B. Prüfungsergebnisse, Accountdaten oder auch E-Mails.

Umso wichtiger ist es, dass die entsprechende Prüfung der gemeldeten Sicherheitszertifikate möglichst automatisiert und zuverlässig erfolgt. Hierfür gibt es ein eigenes Netzprotokoll, das „Online Certificate Status Protocol“ (OCSP). Es prüft automatisch die Gültigkeit des vom aufgerufenen Server gesendeten Zertifikates. Dies ist leider nicht bei allen Browsern und Mailclients korrekt voreingestellt. Zum Teil wird nur auf den Servernamen geprüft, zum Teil aber auch gar nicht. Wir möchten daher hier für die wichtigsten Programme kurz beschreiben, wo die korrekten Einstellungen vorzunehmen sind.

Mozilla Firefox / Mozilla Thunderbird
Bei den beiden Mozilla-Programmen finden sich die Einstellung in einem übersichtlichen Menü unter „Einstellungen“ → „Zertifikate“ → „Validierung“.

Google Chrome
In Chrome muss unter „Einstellungen“ → „Erweiterte Einstellungen anzeigen“ → „HTTPS/SSL“ ein Häkchen bei „Serverzertifikate auf Sperrung prüfen“ gesetzt werden.

Internet Explorer / Windows Mail / Outlook
Alle drei sind fest mit dem Windows-Zertikatspeicher verbunden, den man im Internet Explorer unter „Extras“ → „Internetoptionen“ → „Inhalte“ → „Zertifikate“ erreicht (die Menüfolge mag in den verschiedenen Versionen etwas anders sein). OCSP wird von Windows erst seit Vista – und damit auch in Windows 7 und 8 – unterstützt, für XP fehlt die Funktion leider. Die Aktivierung erfolgt in „Extras“ → „Internetoptionen“ → „Erweitert“ und umfasst die Einstellungen „Auf gesperrte Zertifikate überprüfen“ und „Auf gesperrte Zertifikate von Herausgebern überprüfen“.

Apple Safari
Safari nutzt den zentralen Schlüsselbund von MacOSX. Diesen erreicht man in der Regel über das Anwendungsmenü im Bereich „Utilities“ oder „Werkzeuge“. Innerhalb des Schlüsselbunds kann man im Menü „Einstellungen“, Karteireiter „Zertifikate“ die passenden Einstellungen wählen. In aller Regel ist dies bei OCSP und CRL jeweils „Bester Versuch“, die Priorität sollte auf OCSP liegen.

Opera
In Opera (ab Version 9) muss man den Umweg über die Seite „about:config“ im Browser gehen. In den „Security Prefs“ kann man den Eintrag bei „OCSP Validate Certificates“ ändern. Da dieser aber standardmäßig aktiviert ist, muss man in der Regel nichts ändern.

Wenn der Provider streikt – VPN mit Android unterwegs

Kürzlich versuchte ich freudig mein neues Tablet (Odys Xelio, Android 4.0.3 ICS) mit dem VPN der Uni zu verbinden. Vom heimischen WLAN aus klappte das völlig problemlos. Die Probleme begannen an zwei anderen Orten: Dem Uni-WLAN und schließlich dem mobilen 3G-Netz. Also lief ich erstmal zu unserer Netzwerkabteilung, die aber nach eingehender Prüfung feststellte: Für Problem eins (Uni-WLAN) benutze ich den falschen – L2TP-basierten – Client, Problem zwei muss irgendwie am Tablet selbst oder am Provider  liegen. Den Provider habe ich kontaktiert, wurde aber leider mit einer Standardantwort á la „müsste gehen, supporten wir aber nicht weiter, im Zweifelsfall ist Ihre Netzabteilung schuld“ abgefrühstückt. Also musste das google’sche Orakel befragt werden, mit dem Ergebnis: Sehr wohl blockieren einige Provider Punkt-zu-Punkt-Verbindungen auf einer ganzen Reihe von Ports, darunter vermutlich auch der Port, der für L2TP verwendet wird. Für Businesskunden werden diese dann natürlich auf Wunsch freigeschaltet. Nun, Businesskunde bin ich nicht, aber immerhin hatte ich nun für beide Probleme einen gemeinsamen Lösungsweg: Wechsel zum AnyConnect-Client, der VPN über den nicht geblockten Port 443 (SSL) bereitstellt. Im Google Play Store gibt es gleich mehrere kostenfreie Varianten des Clients. Die „AnyConnect ICS+“-Version, die ich zuerst getestet hatte, scheiterte an einem Berechtigungsproblem. Nun gut, zum Glück ist mein Tablet ja ab Werk gerootet, also als nächstes die Version „Rooted AnyConnect“ ausprobiert. Ergebnis: „Der TUN/TAP-Treiber konnte nicht geladen werden.“ Grummel… google… Aha, der genannte Treiber fehlt auf vielen Tablets, Abhilfe schafft man mittels „TUN.ko Installer“, ebenfalls bei Google Play erhältlich. Die Installation eines passenden TUN-Treibers  war damit ganz einfach und siehe da, anschließend konnte sich auch AnyConnect aus jedem beliebigen Netzwerk heraus verbinden.

Ja, Gnome 3 ist die Pest. Aber ja, es ist heilbar.

Seit ein paar Monaten begrüßt mich allmorgendlich Gnome 3, auch „Gnome Shell“ genannt, als Desktop auf meinem LMDE (Linux Mint Debian Edition). Die Abneigung gegen diese Version, die ja niemand so schön formulieren kann wie Linus Torvalds höchstpersönlich, kann ich schon gut nachvollziehen. Im Grundzustand halte ich Gnome 3 ebenfalls für indiskutabel. Aber es dabei zu belassen, wäre einfach nicht zeitgemäß. Denn egal ob Browser, Handys oder CMS – letztlich kommt es darauf an, was man – mit vertretbarem Aufwand versteht sich – über die Nutzung von Erweiterungen aus dem System machen kann. Und da bietet Gnome 3 eine kleine, aber feine und vor allem sehr komfortabel installierbare Auswahl. Die zentrale Seite für Gnome-3-Erweiterungen ist hier zu finden:

GNOME Shell Extensions

Besucht man mit einem laufenden und halbwegs aktuellen (d.h. ab Version 3.2) Gnome diese Seite, kann man die dort angebotenen Erweiterungen auch gleich über den „On/Off“-Schalter in der Detailansicht der jeweiligen Erweiterung installieren und deinstallieren. Möchte man sich unabhängig von der Webseite einen Überblick über die aktuell installierten Erweiterungen verschaffen, empfiehlt sich die Installation des Gnome Tweak Tools, welches inzwischen bei Ubuntu, Linux Mint und Debian über die Standard-Repositories verfügbar ist.

Im Folgenden möchte ich einige Erweiterungen empfehlen, die aus meinem Gnome 3 wieder einen sehr schönen und komfortabel bedienbaren Desktop gemacht haben, ohne dass ich auf die Vorteile (ja, die gibt’s auch 😉 ) der neuen Version verzichten müsste:

Diese drei genügen aus meiner Sicht bereits, um Gnome 3 problemlos nutzbar zu machen. Alle nun folgenden Vorschläge sind kein Muss, sondern eher Geschmackssache:

  • Frippery Move Clock – verschiebt die Uhr an den Rand der Titelzeile
  • Frippery Shutdown Menu – fügt neben der „Suspend“-Funktion eine Option für echtes Herunterfahren hinzu
  • AlternateTab – macht den Wechsel zwischen Anwendungen per „Alt-Tab“ wesentlich komfortabler
  • Evil Status Icon Forever – bietet die Möglichkeit, Statusicons zurück in die Titelleiste zu holen (z.B. sehr sinnvoll für Pidgin)

Es gibt weitere, sehr nützliche Extensions, z.B. Statusmonitore, diverse Multimedia-Integrationen etc. Da kann man nur sagen: Viel Spaß beim Ausprobieren! Und selbstverständlich dürfen hier in den Kommentaren gerne weitere Empfehlungen gegeben werden.