Aus Linux-Magazin 10/2010

Perl-Skript misst und bewertet Temperaturen

© Thomas-Max Müller, Pixelio.de

Hält sich die Hitze noch im Rahmen? Beim Überwachen der Zimmertemperatur mit einem preiswerten USB-Sensor bietet das Holt-Winters-Verfahren aus dem RRDTool-Werkzeugkasten einen Ansatz, um gewöhnliche Schwankungen von Ausreißern zu unterscheiden.

Per USB eingestöpselte Zusatzgeräte verlangten von experimentierfreudigen Linuxern noch vor einigen Jahren heftige Klimmzüge. Der Bastler musste passende Treiber aufstöbern oder selbst schreiben und sie dann in den Kernel einbinden. Heutzutage funktioniert das mit aktuellen Distributionen erstaunlicherweise oft völlig automatisch. Das neulich für 7 Dollar (inklusive Versand) auf Ebay gekaufte USB-Thermometer TEMPer (Abbildung 1, [2]) funktionierte sofort – ohne dass ich auch nur die Bedienungsanleitung durchlesen musste.

Abbildung 1: Den preisgünstige Temperaturfühler TEMPer USB bietet auch Amazon, zumindest das amerikanische.

Abbildung 1: Den preisgünstige Temperaturfühler TEMPer USB bietet auch Amazon, zumindest das amerikanische.

Sofort erkannt

Beim Einstöpseln des Fühlers in einen USB-Port erkennt der Kernel das Device als generisches HID (Human Interface Device) und weist ihm den ungeschliffenen Treiber »hidraw« zu (Abbildung 2). Dieser Allerweltstreiber kommuniziert mit unterschiedlichen Geräten, kennt aber deren Eigenheiten nicht. Die Bit-Pfriemelei, die zur Verständigung zwischen Hardwarekomponenten notwendig ist, läuft in diesem Fall über eine Software im Userspace ab, die das CPAN-Modul Device::USB::PCSensor::HidTEMPer implementiert.

Abbildung 2: Beim Einstöpseln des Temperaturfühlers erkennt Ubuntu das neue USB-Device korrekt und weist ihm einen generischen Treiber zu.

Abbildung 2: Beim Einstöpseln des Temperaturfühlers erkennt Ubuntu das neue USB-Device korrekt und weist ihm einen generischen Treiber zu.

Um die vom Fühler gemessene Temperatur auszulesen, sucht Listing 1 zunächst mit der Methode »device()« das zuständige Gerät im USB-Baum. Da nur ein Fühler eingestöpselt ist, kommt auch nur ein Eintrag zurück, bei mehreren Geräten gäbe die Methode »list_devices()« eine Liste aller gefundenen Fühler aus. Den internen Sensor des Geräts spricht die Methode »internal()« an und »celsius()« liefert die von ihm gemessene Temperatur als Fließkommazahl mit einer Auflösung von einem halben Grad zurück.

Listing 1:
»celsius«

01 #!/usr/bin/perl -w
02 use strict;
03 use local::lib;
04 use Device::USB::PCSensor::HidTEMPer;
05
06 my $temper =
07   Device::USB::PCSensor::HidTEMPer->new();
08
09 my $sensor = $temper->device();
10
11 if( defined $sensor->internal() ) {
12   print "Temperature: ",
13     $sensor->internal()->celsius(),
14     " Cn";
15 }

Ein Blick in den Sourcecode des CPAN-Moduls offenbart, dass hinter dem schnieken objektorientierten API, das einfach die ausgelesene Temperatur in Grad Celsius liefert, Bitwerte hin und her sausen, Datenpuffer zusammengestellt und zerlegt sowie Prüfsummen ermittelt werden. Damit Linux den Fühler beim Einstöpseln als solchen erkennt, liest es dessen Vendor-ID (»1130«) und Product-ID (»660C«) aus. So ist es egal, in welchem USB-Port oder an welchem USB-Hub der User das Kabel einsteckt. Das Perl-Modul Device::USB (beziehungsweise die dahintersteckende C-Library »libusb«) durchstöbert den gesamten USB-Baum, bis es ein Gerät mit der gesuchten Kombination aus Hersteller- und Produkt-ID findet.

Minutiöse Buchführung

Mit dem CPAN-Modul App::Daemon entsteht nun in Listing 2 ein Daemon-Prozess, den der Admin mit »logtemp start« und »logtemp stop« hoch- und wieder herunterfährt. Während er im Hintergrund läuft, legt er in der Logdatei »/var/log/temper.log« nach Abbildung 3 minütlich den aktuell ausgelesenen Temperaturwert ab. Das Log4perl-Framework schreibt noch einen Zeitstempel davor.

Listing 2:
»logtemp«

01 #!/usr/bin/perl -w
02 use strict;
03 use local::lib;
04 use Device::USB::PCSensor::HidTEMPer;
05 use App::Daemon 0.10 qw(daemonize);
06 use Log::Log4perl qw(:easy);
07 use Sysadm::Install qw(:all);
08 use File::Basename;
09
10 sudo_me();
11
12 $App::Daemon::logfile =
13   "/var/log/temper.log";
14 $App::Daemon::pidfile =
15   "/var/run/temper.pid";
16 $App::Daemon::as_user = $ENV{SUDO_USER};
17
18 daemonize();
19
20 while(1) {
21   my $temper =
22    Device::USB::PCSensor::HidTEMPer->new();
23
24   my $sensor = $temper->device();
25
26   if( defined $sensor->internal() ) {
27     INFO "READ ",
28          $sensor->internal()->celsius();
29   } else {
30     ERROR "No reading available";
31   }
32
33   sleep 60;
34 }

Der Befehl »sudo_me()« in Zeile 10 von Listing 2 stammt aus dem CPAN-Modul Sysadm::Install und stellt sicher, dass das Skript als Superuser läuft. Es startet sich selbst mit einem Sudo-Aufruf, falls dies noch nicht der Fall ist. Root-Rechte sind notwendig, damit der Daemon die Logdatei in »/var/log« anlegen und seine Prozess-ID in »/var/run/temper.pid« speichern kann.

Gleich darauf gibt App::Daemon die Privilegien aus Sicherheitsgründen ab und brummt anschließend unter der ID des in der Variablen »$App::Daemon::as_user« abgelegten Users weiter. Dessen ID bezieht das Skript aus der Environment-Variablen »SUDO_USER«, die das Sudo-Kommando mit der ID des aufrufenden Users füllt.

Der von App::Daemon bereitgestellte Befehl »daemonize()« schickt den Daemon in den Hintergrund, sodass der aufrufende User sich wieder dem Kommandozeilenprompt der Shell gegenüber sieht. Mit dem Befehl »tail -f /var/log/temper.log« kann er, wie in Abbildung 3 gezeigt, das Treiben des Daemon verfolgen. Zu Testzwecken lässt sich »logtemp« auch mit »logtemp -X« im Vordergrund hochfahren, dann erscheinen die Logmeldungen auf der Standardausgabe.

Abbildung 3: In der Logdatei legt der Thermo-Daemon nach dem Hochfahren minütlich einen Messwert ab.

Abbildung 3: In der Logdatei legt der Thermo-Daemon nach dem Hochfahren minütlich einen Messwert ab.

Graphen statt Zahlenreihen

Messreihen in Logdateien eignen sich leider nur selten dazu, Euphorie oder Gehaltserhöhungen auszulösen, und so nimmt es nicht wunder, dass als nächster Schritt gleich die Darstellung in einem Graphen folgt. Das Werkzeug RRDTool eignet sich hierfür hervorragend. Wem die etwas altertümliche Syntax des Oldtimers missfällt, der nutzt das Perl-Modul RRDTool::OO, das eine moderne objekt-orientierte Syntax bietet.

Um die Logmeldungen im menschenlesbaren Datumsformat Jahr/Monat/Tag/Stunde:Minute:Sekunde in das von RRDTool geforderte Sekundenformat umzuformen, nutzt das Listing 3 das CPAN-Modul DateTime::Format::Strptime und definiert in Zeile 13 ein entsprechendes Erkennungs-Pattern. Zeile 14 stellt die Zeitzone der erfassten Einträge auf die des lokalen Rechners ein.

Listing 3:
»rrdtemp«

001 #!/usr/bin/perl -w
002 use strict;
003 use local::lib;
004 use RRDTool::OO;
005 use DateTime::Format::Strptime;
006
007 my $logfile     = "temper.log";
008 my @data_points = ();
009 my $rrd_file    = "data.rrd";
010
011 my $date_fmt =
012   DateTime::Format::Strptime->new(
013     pattern   => "%Y/%m/%d %H:%M:%S",
014     time_zone => "local",
015 );
016
017   # Read logged temperature data
018 open FILE, "$logfile" or
019     die "Cannot open $logfile ($!)";
020 while( <FILE> ) {
021   if( /(.*) READ (.*)/ ) {
022     my($datestr, $temp) = ($1, $2);
023
024     my $dt =
025       $date_fmt->parse_datetime($datestr);
026     push @data_points,
027          [$dt->epoch(), $temp];
028   }
029 }
030 close FILE;
031
032   # Create RRD
033 my $rrd = RRDTool::OO->new(
034   file        => $rrd_file,
035   raise_error => 1,
036 );
037
038 my $rows = 60*24*30;
039
040 $rrd->create(
041   step => 60*5,
042   data_source => {
043     name    => "temp",
044     type    => "GAUGE"
045   },
046   archive => {
047       rows => $rows,
048       cpoints => 1,
049       cfunc   => 'AVERAGE',
050   },
051   start => $data_points[0]->[0] - 60,
052   hwpredict   => {
053       rows            => $rows,
054       alpha           => 0.1,
055       beta            => 0.0035,
056       gamma           => 0.5,
057       seasonal_period => 24*60/5,
058       threshold       => 14,
059       window_length   => 18,
060   },
061 );
062
063 for my $data_point (@data_points) {
064   $rrd->update(
065       time  => $data_point->[0],
066       value => $data_point->[1],
067   );
068 }
069
070   # Draw Graph
071 $rrd->graph(
072   image => "bounds.png",
073   width  => 1600,
074   height => 800,
075   start => $data_points[0]->[0],
076   end   => $data_points[-1]->[0],
077   draw => {
078     type   => "line",
079     color  => '000000',
080     legend => "Temperature over Time",
081     thickness  => 2,
082     cfunc      => 'AVERAGE',
083   },
084   draw           => {
085     type   => "line",
086     color  => '00FF00',
087     cfunc  => 'HWPREDICT',
088     name   => 'predict',
089     legend => 'hwpredict',
090   },
091   draw           => {
092     type   => "hidden",
093     cfunc  => 'DEVPREDICT',
094     name   => 'dev',
095   },
096   draw           => {
097     type   => "hidden",
098     name   => "failures",
099     cfunc  => 'FAILURES',
100   },
101   tick => {
102     draw => "failures",
103     color  => '#FF0000',
104      legend => "Failures",
105   },
106   draw => {
107     type   => "line",
108     color  => '0000FF',
109     legend => "Upper Bound",
110     cdef   => "predict,dev,2,*,+",
111   },
112   draw => {
113     type   => "line",
114     color  => '0000FF',
115     legend => "Lower Bound",
116     cdef   => "predict,dev,2,*,-",
117   },
118 );

Die While-Schleife ab Zeile 20 iteriert durch die Logzeilen, und das Regex-Pattern in Zeile 21 erfasst Zeilen mit Temperatureinträgen und lässt andere, wie zum Beispiel die Start- und Stopp-Nachrichten, außen vor.

Alle so gefundenen Messwerte speichert Listing 3 in dem Array »@data_points« zusammen mit den jeweiligen Zeitstempeln. Ab Zeile 32 geht dann RRDTool zu Werke und definiert zunächst eine neue Round-Robin-Datenbank [8] mit genügend Einträgen für 5 Monate. Zur Glättung von Ausrutschern fasst es jeweils fünf Minutenwerte zu einem Datenbankwert zusammen, dazu dient der »step«-Wert in Zeile 41.

Als Datentyp definiert es »GAUGE«, den Allerweltstyp für numerische Werte in RRDTool. Die For-Schleife ab Zeile 63 füttert dann die in »@data_points« zwischengespeicherten Werte mit ihren Zeitstempeln mit Hilfe der Methode »update()« in die RRD-Datenbank. Der Aufruf der Methode »graph()« ab Zeile 71 zeichnet schließlich ein Diagramm (Abbildung 4). Er beschriftet auch gleich die Achsen und skaliert sie entsprechend den Messwerten und Datumsangaben.

Das Auf und Ab im Graphen spiegelt die täglichen Schwankungen der Zimmertemperatur wider. Um aber festzustellen, ob ein Ausreißer wegen unvorhergesehener Ereignisse vorliegt – wenn sich zum Beispiel die Katze auf den Sensor legt oder das Gebäude in Flammen steht -, genügt es nicht, die absoluten Werte zu vergleichen, da diese nicht konstant bleiben. RRDTool bietet deswegen die so genannte Aberrant Behavior Detection an, die mit Hilfe von vier Parametern normales Verhalten vorhersagt und dann eintreffende Werte mit der Prognose vergleicht. Das Tool lernt aus den vergangenen Ereignissen und gibt Prognosen für das künftige Verhalten ab.

Stimmt in einem vordefinierten Zeitfenster eine definierbare Anzahl von Vorhersagen nicht mit der Wirklichkeit überein, löst das System Fehler aus, die im Diagramm in Abbildung 4 rot eingezeichnet sind. Die Abbildung zeichnet die Messwerte schwarz, die Prognose grün und die erlaubte Bandbreite um die Prognose, in denen ein Messwert noch als normal gilt, blau. Alarme erscheinen als rote Linien am Fuß des Graphen.

Abbildung 4: Der Graph mit den Messwerten (schwarz), Holt-Winters-Forecasting (grün), dem erlaubten Wertebereich innerhalb der Prognose (blau) und den ausgelösten Alarmen (rot).

Abbildung 4: Der Graph mit den Messwerten (schwarz), Holt-Winters-Forecasting (grün), dem erlaubten Wertebereich innerhalb der Prognose (blau) und den ausgelösten Alarmen (rot).

Leider löst das Verfahren auch Fehlalarme aus (wie zum Beispiel am Mittag des dritten Tages) und auch richtige Fehler erkennt es nicht immer zuverlässig. Der Admin spielt dann so lange an den vier Knöpfen herum, bis sich ein zufriedenstellendes Ergebnis zeigt. Das ist natürlich keine Garantie dafür, dass nicht schon am nächsten Tag wieder ein Fehlalarm ausgelöst wird, und das Drehen an den Knöpfen gleicht eher Zauberei als einer Ingenieurswissenschaft.

An Knöpfen drehen

Drehen darf der Admin an den Parametern »alpha«, »beta« und »gamma« (jeweils zwischen 0 und 1, ausschließlich) sowie an der Länge der Seasonal Period, also jenem Zeitraum, in dem sich Ereignisse wiederholen, zum Beispiel einem Tagesturnus für Temperaturen. Kleine Werte – also nahe null – für »alpha«, »beta« und »gamma« richten das Augenmerk auf Ereignisse, die schon etwas zurückliegen, während bei Werten nahe 1 die Vorhersage nahe an kürzlich gesichteten Werten liegt.

Während »alpha« die Prognose des Basiswerts des Graphen kontrolliert, arbeitet »beta« mit der Steigung des Graphen, »gamma« bestimmt die Prognose bei Wiederholungen in definierten Zeitfenstern. Ein Beispiel könnte »alpha=0.1«, »beta= 0.0035«, »gamma=0.5« und die Länge des saisonalen Zeitrahmens auf die Zahl der von RRDTool im Laufe eines Tages gesammelten Messwerte setzen.

Einen Fehler meldet das System, falls während eines »window_length« langen Zeitfensters eine Anzahl »threshold« oder mehr Messpunkte außerhalb des Confidence Band, also des blauen Bandes um die Prognose, liegen. Wie breit dieses Akzeptanzband genau ist, ermittelt RRDTool automatisch und lässt sich dabei weder in die Karten sehen noch gar beeinflussen.

Wo bleibt der Ausreißer?

Einige Stellen in dem Graphen fallen auf: Während der ersten zwei Tage erstellt RRDTool keine Vorhersage, denn es benötigt einige Zyklen, bis die Seasonal Component und deren Einfluss auf die Prognose feststehen. Lustigerweise erwartet das System nach dem einen Ausreißer kurz vor Mittag des vierten Tages auch an den darauf folgenden Tagen zur gleichen Zeit einen Höhepunkt, der aber ausbleibt. An den folgenden Tagen geht deshalb die Erwartung schrittweise zurück, bis das System sich einige Zeit später wieder fängt.

Was »rrdtool« unter der Haube so treibt, lässt sich durch Einschalten des Log4perl-Framework herausfinden, denn RRDTool::OO unterstützt das Verfahren und wartet nur darauf, bis der User es aktiviert. Abbildung 5 zeigt einen Blick in den Maschinenraum von RRDTool::OO, erst das Kommando, um die Datenbank anzulegen, dann eine Auswahl der abgesetzten Update-Befehle für geloggte Temperaturwerte und schließlich das »graph«-Kommando, das das Diagramm zeichnet.

Abbildung 5: RRDTool-Kommandos für den Graphen in Abbildung 4, den das Skript mit RRDTool::OO erzeugt.

Abbildung 5: RRDTool-Kommandos für den Graphen in Abbildung 4, den das Skript mit RRDTool::OO erzeugt.

RRDTool nennt die Holt-Winters-Vorhersage »HWPREDICT« und die erwartete statistische Abweichung »DEVPREDICT«. Die Zeilen 91 bis 95 in Listing 3 definieren auf diese Abweichung einen Alias »dev«, den die Zeilen 110 und 116 jeweils aufgreifen, um das erlaubte Streuband zu zeichnen. In RRDTool-typischer RPM-Notation steht »predict,dev,2,*,+« für »predict-2 * dev« in algebraischer Notation, denn RRDTool erlaubt Abweichungen nach oben und unten, jeweils im Wert des doppelten »DEVPREDICT«-Werts.

Installation

Da das benötigte CPAN-Modul nicht als Ubuntu-Paket verfügbar ist, installiert es der auf Sauberkeit bedachte Systemadministrator nicht unter »/usr«, sondern nutzt »local::lib«, um es unter dem Homeverzeichnis einzupflanzen. Mit

sudo apt-get install liblocal-lib-perl

spielt er dazu unter Ubuntu Lucid Lynx das »local::lib«-Modul in »/usr« ein und ruft anschließend eine CPAN-Shell mit dem Kommando

perl -Mlocal::lib -MCPAN -eshell

auf. Darin startet der Befehl »install Device::USB::PCSensor::HidTEMPer« dann den Download und die Installation des Moduls unter dem Verzeichnis »perl5« im Homeverzeichnis. Das Skript in Listing 1 sucht wegen der Anweisung »use local::lib« in Zeile 3 auch dort nach dem Modul.

Ohne zusätzliche Tricks darf zunächst nur Root den Sensor auslesen. Wer aber unter »/etc/udev/rules.d« eine Datei »99-tempsensor.rules« anlegt und die in Abbildung 6 gezeigten Variablen einträgt, der stellt sicher, dass auch unprivilegierte User die Temperaturwerte abholen dürfen. Nach dem Editieren der Rules-Datei ist noch ein Neustart des Udev-Subsystems mit »sudo service restart udev« erforderlich – und schon können die Messungen beginnen. (jcb)

Abbildung 6: Eine neue Datei in »/etc/udev/rules.d« weist das Udev-System dazu an, neu erscheinende Temperaturfühler im Modus 666 bereitzustellen.

Abbildung 6: Eine neue Datei in »/etc/udev/rules.d« weist das Udev-System dazu an, neu erscheinende Temperaturfühler im Modus 666 bereitzustellen.

Infos

[1] Listings zu diesem Artikel: [ftp://www.linux-magazin.de/pub/listings/magazin/2010/10/Perl]

[2] TEMPer-USB-Thermometer: [http://www.amazon.com/dp/B002VA813U]

[3] Kyle Rankin, “Cool Projects edition”: Linux Journal, August 2010, Page 32-34

[4] Jake D. Brutlagg, “Aberrant Behavior Detection in Time Series for Network Service Monitoring”: [http://www.usenix.org/events/lisa00/brutlag.html]

[5] Jeffery Kline, David Plonka und Amos Ron, “A Signal Analysis of Network Traffic Anomalies”: [http://pages.cs.wisc.edu/~pb/paper_imw_02.pdf]

[6] Jeff Kline, Sangnam Nam, Paul Barford, David Plonka, Amos Ron, “Traffic Anomaly Detection at Fine Timescales with Bayes Nets”: [http://pages.cs.wisc.edu/~pb/icimp08_final.pdf]

[7] Alan Ott, “Libudev and Sysfs Tutorial”: [http://www.signal11.us/oss/udev]

[8] Michael Schilli, “Daten ausgesiebt”: Linux-Magazin 06/2004; [https://www.linux-magazin.de/Heft-Abo/Ausgaben/2004/06/Daten-ausgesiebt]

Der Autor

Michael Schilli arbeitet als Software-Engineer bei Yahoo in Sunnyvale, Kalifornien. Er hat die Bücher “Goto Perl 5” (auf Deutsch) und “Perl Power” (auf Englisch) für Addison-Wesley geschrieben und ist unter [mschilli@perlmeister.com] zu erreichen.

LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben