Beim Überwachen der Zimmertemperatur mit einem preiswerten USB-Temperaturfühler bietet das Holt-Winters-Verfahren aus dem rrdtool-Werkzeugkasten einen Ansatz, normale Schwankungen von Ausreißern zu unterscheiden.
Per USB eingestöpselte Zusatzgeräte verlangten von experimentierfreudigen Linuxlern noch vor einigen Jahren heftige Klimmzüge. Der Bastler musste passende Treiber aufstöbern oder selbst schreiben, und diese dann in den Kernel einbinden. Heutzutage funktioniert es mit aktuellen Distributionen erstaunlich oft völlig automatisch. Ein neulich für 7 Dollar (inklusive Versand) auf Ebay gekauftes TEMPer USB Thermometer [2] funktionierte sofort und ohne dass ich auch nur die Bedienungsanleitung durchlesen musste.
Abbildung 1: Der preisgünstige Temperaturfühler "TEMPer USB Thermometer" |
Beim Einstöpseln des Fühlers in einen USB-Port des Rechners erkennt der
Kernel das Device als generisches HID (Human Interface Device) und weist
ihm den "rohen" Treiber hidraw
zu. Dieser Allerweltstreiber
kommuniziert mit unterschiedlichen Geräten, kennt aber deren Eigenheiten
nicht. Die Bit-Pfrümelei, die
zur Verständigung zwischen Hardwarekomponenten notwendig ist, läuft in
diesem Fall über einen Treiber im Userspace ab, den das CPAN-Modul
Device::USB::PCSensor::HidTEMPer implementiert.
Abbildung 2: Beim Einstöpseln des Temperaturfühlers erkennt Ubuntu das neue USB-Device korrekt. |
Um die vom Fühler gemessene Temperatur auszulesen, sucht Listing 1 zunächst
mit der Methode device()
das zuständige Device 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 verfügbaren
Fühler zurück. 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 .5 Grad zurück.
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 " C\n"; 15 }
Ein Blick in den Source-Code des CPAN-Moduls offenbart, dass hinter der schnieken objektorientierten API, die einfach die ausgelesene Temperatur in Grad Celsius liefert, Bitwerte hin- und hersausen, Datenpuffer zusammengestellt und zerlegt, und 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 dazu den gesamten USB-Baum, bis es ein Gerät mit der gesuchten Kombination aus Hersteller- und Produkt-ID findet.
Mit dem CPAN-Modul App::Daemon entsteht nun in Listing 2 ein Dämon-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.
Der Befehl sudo_me()
in Zeile 10 stammt aus dem CPAN-Modul Sysadm::Install
und stellt sicher, dass das Skript als Superuser läuft, und startet sich
selbst mit einem sudo
-Aufruf, falls dies noch nicht der Fall ist.
Root-Rechte sind notwendig, damit der Dämon 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. Diesen User zieht
das Skript aus der Environment-Variablen SUDO_USER
, die das
sudo
-Kommando auf die ID des aufrufenden Users setzt.
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 $ENV{SUDO_USER} ||= "mschilli"; 13 14 $App::Daemon::logfile = 15 "/var/log/temper.log"; 16 $App::Daemon::pidfile = 17 "/var/run/temper.pid"; 18 $App::Daemon::as_user = $ENV{SUDO_USER}; 19 20 daemonize(); 21 22 while(1) { 23 my $temper = 24 Device::USB::PCSensor::HidTEMPer->new(); 25 26 my $sensor = $temper->device(); 27 28 if( defined $sensor->internal() ) { 29 INFO "READ ", 30 $sensor->internal()->celsius(); 31 } else { 32 ERROR "No reading available"; 33 } 34 35 sleep 60; 36 }
Der von App::Daemon bereitgestellte Befehl daemonize()
schickt den
Dämon in den Hintergrund, so dass 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 Dämons 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-Dämon nach dem Hochfahren minütlich einen Messwert ab. |
Messreihen in Logdateien eignen sich aber 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 Graph folgt. Das Werkzeug rrdtool eignet sich hierfür hervorragend und wem die etwas altertümliche Syntax des Old-Timers missfällt, nutzt das Perl-Modul RRDTool::OO, das eine moderne objektorientierte 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. 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 Start- und Stopp-Nachrichten, außen vor.
Alle so gefundenen Messwerte speichert Listing 3 in einem Array
@data_points
, zusammen mit den jeweiligen Zeitstempeln. Ab Zeile 32
geht dann RRDTool zu Werke und definiert dann 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 5 Minutenwerte zu einem
Datenbankwert zusammen, was sich über den step
-Wert in Zeile
41 manifestiert.
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
mittels der Methode update
in die RRD-Datenbank. Der Aufruf der
Methode graph()
ab Zeile 71 zeichnet schließlich ein Diagramm nach
Abbildung 4. Es beschriftet auch gleich die Achsen und sie skaliert
sie entsprechend der Meßwerte und Datumsangaben. Bequemer geht es kaum!
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, absolute Werte zu vergleichen, da diese nicht konstant bleiben. RRDTool bietet deswegen die sogenannte "Aberrant Behavior Detection" an, die mittels 4 Parametern "normales" Verhalten vorhersagt und dann eintreffende Werte mit der Prognose vergleicht. Es lernt aus vergangenen Ereignissen und prognostiziert mit ihrer Hilfe die Zukunft. Stimmen in einem vordefinierten Zeitfenster eine definierbare Zahl von Vorhersagen nicht mit der Wirklichkeit überein, löst das System Fehler aus, die im Diagramm in Abbildung 4 rot eingezeichnet sind.
Abbildung 4 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 Graphs.
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 solange an den 4 Knöpfen herum, bis sich ein zufriedenstellendes Ergebnis zeigt. Dies 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 Zauberkunst als einer Ingenieurswissenschaft.
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
dem Zeitraum, in dem sich Ereignisse wiederholen, wie 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 Progrose bei Wiederholungen in vordefinierten
Zeitfenstern der Länge seasonal_period
.
Listing 4 setzt alpha=0.1
, beta=0.0035
, gamma=0.5
und die
Länge des säsonalen Zeitrahmens, seasonal_period
, auf die
Gesamtzahl der von RRD im Laufe eines Tages erfassten Messwerte.
Einen Fehler meldet das System, falls während eines window_length
langen Zeitfensters eine Anzahl threshold
oder mehr Messpunkte
außerhalb des "confidence bands", 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
beieinflussen.
Abbildung 4: Der Graph mit Holt-Winters-Forecasting, dem erlaubten Wertebereich innerhalb der Prognose, und ausgelösten Alarmen. |
Einige Stellen im Graph stechen ins Auge: 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 feststeht.
Lustigerweise erwartet das System nach dem Ausreißer kurz vor Mittag des vierten Tages auch an den darauffolgenden Tagen zur gleichen Zeit einen Höhepunkt, der aber ausbleibt. An den darauffolgenden Tagen geht deshalb die Erwartung schrittweise zurück, bis das System sich einige Zeit später wieder fängt.
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 );
Was rrdtool
unter der Haube so treibt, lässt sich durch Einschalten
des Log4perl-Frameworks 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. |
Die Holt-Winters-Vorhersage nennt rrdtool HWPREDICT
und die
erwartete statistische Abweichung DEVPREDICT
. Zeile 92 in Listing 3
definiert auf letztere 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-Wertes.
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 Home-Verzeichnis einzupflanzen. Mit
sudo apt-get install liblocal-lib-perl
installiert er dazu unter Ubuntu Lucid Lynx das Modul local::lib unter /usr und ruft anschließend die CPAN-Shell mit
perl -Mlocal::lib -MCPAN -eshell
auf. Darin startet dann der Befehl "install Device::USB::PCSensor::HidTEMPer" den Download und die Installation des Modules unter dem Verzeichnis "perl5" im Home-Verzeichnis. Das Skript in Listing 1 sucht wegen der Anweisung "use local::lib" auch dort nach dem Modul.
Abbildung 6: Eine neue Datei in /etc/udev/rules.d weist das Udev-System an, neu erscheinende Temperaturfühler im Modus 666 bereitzustellen. |
Ohne zusätzliche Tricks darf 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, stellt sicher, dass auch
unpriviligierte User die Temperaturwerte abholen können. Nach dem
Editieren der Rules-Datei ist ein Neustart des udev-Subsystems erforderlich
(sudo service restart udev
) und die Messungen können beginnen.
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2010/10/Perl
TEMPer USB Thermometer, http://www.amazon.com/dp/B002VA813U
Kyle Rankin, "Cool Projects edition", Linux Journal August 2010, page 32-34.
"Aberrant Behavior Detection in Time Series for Network Service Monitoring", Jake D. Brutlagg, http://www.usenix.org/events/lisa00/brutlag.html
"A Signal Analysis of Network Traffic Anomalies", Paul Barford, Jeffery Kline, David Plonka und Amos Ron, http://pages.cs.wisc.edu/~pb/paper_imw_02.pdf
"Traffic Anomaly Detection at Fine Timescales with Bayes Nets", Jeff Kline, Sangnam Nam, Paul Barford, David Plonka, und Amos Ron, http://pages.cs.wisc.edu/~pb/icimp08_final.pdf
"libudev and Sysfs Tutorial", Alan Ott, http://www.signal11.us/oss/udev/
"Daten ausgesiebt", Michael Schilli, Linux-Magazin 06/2004, http://www.linux-magazin.de/Heft-Abo/Ausgaben/2004/06/Daten-ausgesiebt
Michael Schilliarbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com. |