Aus Linux-Magazin 04/2007

Perl steuert Elektrogeräte auch aus der Ferne

© pixelquelle.de

Der Billig-Router hat sich aufgehängt, damit ist aus der Ferne auch kein Reset via Netzwerk mehr möglich. Einen Weg aus dem Dilemma gibt es dennoch: Ein X10-Modul, gesteuert von einem Web-GUI mit Ajax-Interface, betätigt den Hauptschalter.

Schon im vorigen Snapshot kam X10-Technologie zum Einsatz, die Schaltsignale über das heimische Stromnetz schickt. Ein solches Signal kann dann mit Hilfe eines Relais zum Beispiel beliebige Verbraucher mit Strom versorgen oder vom Netz trennen. Der heutige Perl-Snapshot erweitert den Kreis um drei neue Geräte mit X10-Empfängern: ein DSL-Modem (Abbildung 1), einen DSL-Router und den digitalen Videorekorder Tivo. Die Lampen im Schlaf- und im Wohnzimmer hängen sowieso schon an X10-Kästen.

 

Abbildung 2 führt die heutigen Skripte in Aktion vor. Wie zu sehen ist, zeigt der Browser die beteiligten Geräte mit lesbaren Namen an. In der rechten Spalte der Tabelle befindet sich pro Reihe ein Button, der je nach Einschaltzustand grün oder rot gefärbt ist. Nach einem Mausklick auf den Button wechselt das Gerät in den entgegengesetzten Zustand. Dabei kommt modernste Ajax-Technologie zum Einsatz, das heißt, der Browser lädt nicht nach jedem Umschalten die komplette Webseite neu, sondern aktualisiert nur die veränderten Felder.

 

Gerätetaufe

Jedes X10-Gerät ist auf einen eindeutigen House- und Unit-Code eingestellt, über den es im Stromnetz adressierbar ist. Kein Anwender möchte sich aber diese kryptischen Buchstaben und Nummern merken. Daher definiert die Datei »/etc/x10.conf« in Listing 1 alle erreichbaren X10-Geräte im Yaml-Format. Yaml steht für “Yaml Ain\’t Markup Language” und ist eine einfache, an XML angelehnte Sprache, die vor allem dann verwendet wird, wenn es um die Serialisierung von Daten geht.

Listing 1:
»x10.conf«

01 # x10.conf Configuration File
02 
03 - device: dslmodem
04      code:  K4
05      name:  DSL Modem
06 
07 - device: bedroom
08      code:  K9
09      name:  Bedroom Lights
10 
11 - device: office
12      code:  K10
13      name:  Office Back Light
14 
15 - device: dslrouter
16      code:  K14
17      name:  DSL Router
18 
19 - device: tivo
20      code:  K13
21      name:  TiVo
22 
23 - device: livingroom
24      code:  K1
25      name:  Living Room Lights

Ein voranstehender Bindestrich bedeutet in Yaml etwa Array-Element. Die Doppelpunktnotation trennt dagegen die Key-Value-Paare eines Hash. Die in Listing 1 angegebene Konfiguration gibt also einen Array von Geräten an. Jedes Gerät wird durch einen Hash repräsentiert, der unter den Schlüsseln »device«, »code« und »name« Werte für das Gerätekürzel, den House-/Unit-Code und einen lesbaren Gerätenamen enthält.

Das Skript aus Listing 2 erlaubt es dann, von der Kommandozeile aus bestimmte Geräte über ihr jeweiliges Kürzel anzusprechen:

# myx10 dslmodem on
# myx10 dslmodem status
on

Auf diese Weise lassen sie sich ein- oder ausschalten, genau so kann der Anwender auch ihren Status abfragen.

Listing 2:
»myx10«

01 #!/usr/bin/perl -w
02 use strict;
03 use MyX10;
04 my($device, $command) = @ARGV;
05 my $x10 = MyX10->new();
06 $x10->send($device, $command);

Billig-Trick

Mit billigen X10-Modulen ist aber leider nur Kommunikation in einer Richtung möglich: Man kann sie ansteuern, aber ihr Zustand lässt sich nicht abfragen. Der folgende Workaround entschärft dieses Problem: Wer einen Empfänger ausschließlich über das gezeigte Skript bedient, für den merkt sich das Skript in einer kleinen persistenten Dbm-Datei einfach, ob das Gerät gerade ein- oder ausgeschaltet ist.

Das führt zwar zu Verwirrung, falls der Benutzer Geräte an der Software vorbei manuell schaltet, doch ein solcher Fehler lässt sich durch einen Zustandswechsel über das Web-GUI leicht beheben. Danach ist wieder alles im Lot.

Das Skript »myx10« (Listing 2) nutzt dafür die Dienste des Perl-Moduls »MyX10.pm« in Listing 3, das zunächst, wie schon im vorigen Snapshot vorgestellt, die Baudrate und das serielle Interface für die Kommunikation mit dem X10-Transceiver einstellt. Unter »/var/local/myx10.db« legt es mit »dbmopen()« eine persistente Dbm-Datei vom Typ »DB_File« an, um unter den Geräteschlüsseln den vermuteten Einschaltzustand des zugehörigen Geräts abzuspeichern. Die Destroy-Methode ab Zeile 49 schließt die Dbm-Datei wieder, falls das MyX10-Objekt zerstört wird.

Listing 3:
»MyX10.pm«

001 ###########################################
002 package MyX10;
003 ###########################################
004 use strict;
005 use warnings;
006 use Device::SerialPort;
007 use ControlX10::CM11;
008 use YAML qw(LoadFile);
009 use Log::Log4perl qw(:easy);
010 use DB_File;
011
012 ###########################################
013 sub new {
014 ###########################################
015  my($class, %options) = @_;
016
017  LOGDIE "You must be root" if $> != 0;
018
019  my $self = {
020   serial  => "/dev/ttyS0",
021   baudrate => 4800,
022   devices => LoadFile("/etc/x10.conf"),
023   commands => {
024     on   => "J",
025     off  => "K",
026     status => undef,
027   },
028   dbm => {},
029   dbmfile => "/var/local/myx10.db",
030   %options,
031  };
032
033  $self->{devhash} = {
034    map { $_->{device} => $_ }
035    @{$self->{devices}} };
036
037  dbmopen(%{$self->{dbm}},
038      $self->{dbmfile}, 0644) or
039    LOGDIE "Cannot open $self->{dbmfile}";
040
041  for (keys %{$self->{devhash}}) {
042   $self->{dbm}->{$_} ||= "off";
043  }
044
045  bless $self, $class;
046 }
047
048 ###########################################
049 sub DESTROY {
050 ###########################################
051   my($self) = @_;
052   dbmclose(%{$self->{dbm}});
053 }
054
055 ###########################################
056 sub send {
057 ###########################################
058  my($self, $device, $cmd) = @_;
059
060  LOGDIE("No device specified") if
061    !defined $device;
062
063  LOGDIE("Unknown device") if
064    !exists $self->{devhash}->{$device};
065
066  LOGDIE("No command specified") if
067    !defined $cmd;
068
069  LOGDIE("Unknown command") if
070    !exists $self->{commands}->{$cmd};
071
072  if($cmd eq "status") {
073   print $self->status($device), "n";
074   return 1;
075  }
076
077  my $serial = Device::SerialPort->new(
078   $self->{serial}, undef);
079
080  $serial->baudrate($self->{baudrate});
081
082  my($house_code, $unit_code) = split //,
083   $self->{devhash}->{$device}->{code}, 2;
084
085  sleep(1);
086
087   # Address unit
088  DEBUG "Addressing HC=$house_code ",
089     "UC=$unit_code";
090  ControlX10::CM11::send($serial,
091        $house_code . $unit_code);
092
093  DEBUG "Sending command $cmd ",
094     "$self->{commands}->{$cmd}";
095  ControlX10::CM11::send($serial,
096   $house_code .
097   $self->{commands}->{$cmd});
098
099  $self->{dbm}->{$device} = $cmd;
100 }
101
102 ###########################################
103 sub status {
104 ###########################################
105   my($self, $device) = @_;
106   return $self->{dbm}->{$device};
107 }
108
109 1;

Hashes ohne Reihenfolge

Um schnell zu testen, ob ein angegebenes Device existiert, oder um sofort vom Device-Kürzel zu dessen House-/Unit-Code zu gelangen, wäre es sinnvoll, »/etc/x10.conf« in Hash-Form zu speichern. Doch geht in einem Hash leider die ursprünglich definierte Reihenfolge verloren – aber die ist für eine Anzeige im Browser wichtig. Wer will schon bunt durcheinander gewürfelte Bedienelemente?

Also wandeln die Zeilen 33 bis 35 in Listing 3 den Array mit Hash-Elementen in einen Hash um, dessen Schlüssel die Gerätekürzel sind und der als Werte die vorher definierten Geräte-Hashes führt. In der Instanzvariablen »devhash« wird eine Referenz auf diesen Schnellzugreifer für später abgelegt. Die Zeilen 41 bis 43 iterieren über alle Einträge und setzen den Zustand bislang unbekannter Geräte auf »off«. Das muss nicht stimmen – falls nicht, renkt der nächste Zustandswechsel den X10-Empfänger wieder ein.

Die Methode »send()« schickt über den am Linux-Rechner hängenden X10-Transceiver ein Kommando an einen per Gerätekürzel adressierten X10-Empfänger. Ist das übermittelte Kommando nicht »on« oder »off«, sondern »status«, verzweigt Zeile 72 zu der weiter unten definierten Methode »status()«, die den vermuteten Status des X10-Empfängers aus der Konserve (also der Dbm-Datei) abholt.

Zwischen der Initialisierung der seriellen Schnittstelle und dem Aufruf des X10-Kommandos schläft »MyX10.pm« eine Sekunde lang mit »sleep(1)«. Dafür ist eigentlich kein zwingender Grund zu erkennen. Lässt man diese Zwangspause jedoch weg, stellen sich nicht zu erklärende Timing-Probleme mit der X10-Ansteuerung ein.

Sudo ohne Passwort

Nur Root darf X10-Signale über die serielle Schnittstelle senden. Daher muss »myx10« unter der Benutzerkennung »root« laufen. Wer die Geräte über ein Web-GUI steuern will, bekommt damit aber ein Problem, denn das Eigentum am Webserver geht sicherheitshalber auf »nobody« über. Es bei »root« starten zu lassen wäre grob fahrlässig.

Eine Lösung bietet folgender Eintrag in »/etc/sudoers«, der ein kleines Loch öffnet, das es dem Webserver erlaubt, über »sudo« das Skript »myx10« als »root« auszuführen, ohne dass die Eingabe eines Passworts erforderlich wäre:

nobody ALL= NOPASSWD:/usr/bin/myx10

Das Schlüsselwort »ALL« links vom Gleichheitszeichen legt fest, dass die mit diesem Ausdruck definierten Einstellungen nicht auf einen bestimmten Host beschränkt sind. Das auf den Doppelpunkt folgende Kommando auf der rechten Seite grenzt mögliche Aktivitäten allerdings auf das angegebene Skript »myx10« ein.

So darf ein Einbrecher nach feindlicher Übernahme des Webservers höchstens die X10-Geräte ein- und ausschalten, nicht aber den Root-Account des Linux-Rechners übernehmen. Als Alternative ließe sich auch mit »chmod a+rw /dev/ttyS0« die serielle Schnittstelle für jeden Benutzer beschreibbar machen, das erspart den Sudo-Trick ganz.

Schwung mit CGI

Das CGI-Skript »myx10.cgi« (Listing 4) macht dann auch nicht viel mehr, als das Kommandozeilenskript »myx10« aufzurufen und dessen Ausgabe zurück zum Webclient zu senden. Es nutzt die Funktion »tap« des CPAN-Moduls »Sysadm::Install«, die einfach die Ausgaben eines Kommandos komfortabel abfängt.Wird »myx10.cgi« vom Browser allerdings ohne Device-Parameter aufgerufen, möchte der Webclient die in Abbildung 2 gezeigte Übersicht sehen.

Listing 4:
»myx10.cgi«

01 #!/usr/bin/perl -w
02 use strict;
03 use CGI qw(:all);
04 use Log::Log4perl qw(:easy);
05 use YAML qw(LoadFile);
06 use Template;
07
08 print header();
09
10 my $action = param("action");
11 my $device = param("device");
12
13 if(!defined $device) {
14  my $devices = LoadFile("/etc/x10.conf");
15
16  my $tpl = Template->new();
17  $tpl->process("myx10.tmpl", {
18   devices => $devices,
19  } ) or die $tpl->error();
20  exit 0;
21 }
22
23 if(!defined $action or
24  $action !~ /^(on|off|status)$/) {
25  print "Error: No/Invalid actionn";
26  exit 0;
27 }
28
29 if(!defined $device or $device =~ /W/) {
30  print "Error: use a proper 'device'n";
31  exit 0;
32 }
33
34 system "sudo", "/usr/bin/myx10",
35    $device, param("action");

Hierzu lädt »myx10.cgi« die X10-Konfigurationsdatei und ruft anschließend den Prozessor des Template-Toolkits auf, um das Template »myx10.tmpl« zu rendern (Abbildung 3). Dort sorgt eine Foreach-Schleife dafür, dass für jedes konfigurierte Gerät eine Tabellenspalte mit Druckknopf entsteht.

 

Die »onClick«-Aktion eines jeden Buttons ruft die später in »myx10.js« (Abbildung 4) definierte Funktion »toggle()« auf, die nicht nur die Kommunikation mit dem Server abwickelt, sondern auch die Farbe des Buttons anpasst. Die »id« jedes Buttons setzt »myx10.js« auf das Gerätekürzel, die Klasse »class« auf den zufälligen Namen »clicker«, damit eine Javascript-Funktion anschließend über alle derartigen Elemente iterieren kann.

 

YUI

Moderne Webapplikationen laden nicht mehr die ganze Seite nach, wenn der Surfer nur ein Knöpferl drückt. Die Kommunikation mit dem Webserver findet asynchron über Ajax statt, nur tatsächlich veränderte Elemente zeichnet der Browser neu [3]. Da Ajax aber recht umständlich zu programmieren ist und exzessiver Javascript-Gebrauch bekanntlich Haarausfall verursacht, gibt es etliche Javascript-Bibliotheken, die die Handhabung vereinfachen und Browserkompatibilität gewährleisten.

Ein Beispiel ist die YUI-Library meines Arbeitgebers Yahoo, die kostenlos und ohne Registrierungspflicht verfügbar ist. Auf [2] liegt eine Zip-Datei, die im Verzeichnis »build« alle notwendigen Javascript-Dateien enthält.

Nach dem Download entpackt man einfach das Zip-Archiv und kopiert das »build«-Verzeichnis zum Beispiel unter »htdocs/yui« auf den lokalen Webserver. Ab dann können Javascript-Applikationen die ».js«-Dateien zum Beispiel als »src=/yui/yahoo/yahoo.js« einbinden.

Dynamisch gepatchtes HTML

Die am Ende von »myx10.tmpl« in Abbildung 3 eingebundene Javascript-Datei »myx10.js« (Abbildung 4) definiert die Funktion »update_buttons()«. Der Browser ruft sie gleich nach dem Laden des Dokuments auf. Jedes Gerät erhält damit nicht nur einen Eintrag in der HTML-Tabelle (Abbildung 2), sondern wird mit

x10remote(device, 'status');

auch angesprochen. Der Javascript-Code nutzt dafür die Methode »YAHOO.util.Dom.getElementsByClassName()« der YUI, die alle gefundenen DOM-Knoten liefert, die mit dem Attribut »class=”clicker”« gekennzeichnet sind.

Um den Status eines in »/etc/x10.conf« konfigurierten X10-Empfängers zu erhalten, ruft der Browser für jeden definierten Button asynchron das CGI-Skript mit den Parametern »device=Kürzel« und »action=Status« auf. Danach sieht »myx10.cgi« auf dem Server in seiner Dbm-Datei nach und gibt den letzten dort hinterlegten Zustand des gewünschten X10-Geräts entweder als »on« oder »off« zurück.

Dynamisch einfärben

Die Javascript-Datei »myx10.js« zeigt die Knöpfe der eingeschalteten X10-Empfänger grün, die der deaktivierten dagegen rot an. Das ist die Hauptaufgabe der Methode »setStyle()« der Klasse »Yahoo.dom«, die den Namen eines Objekts der Browser-DOM zuerst entgegennimmt, dann das fragliche Objekt heraussucht und anschließend das »BackgroundColor«-Attribut des CSS-Stylesheet modifiziert.

Beim ersten Laden der vom CGI-Skript generierten HTML-Seite sind die Knöpfe zunächst alle farblos. Erst die Methode »update_buttons()« setzt für jeden Knopf einen Ajax-Request an den Server ab, der den im Dbm-File gespeicherten Zustand des zugehörigen Geräts holt. Trifft die Antwort auf einen dieser asynchronen Requests ein, überprüft die Methode, ob sie den Statuscode »on« beziehungsweise »off« enthält. Entsprechend wird der zugehörige Knopf eingefärbt.

Damit der Javascript-Code auch bei Dutzenden gleichzeitiger Requests übersichtlich bleibt, kommt der Connection-Manager der YUI zum Einsatz. Drückt der Benutzer mit der Maus auf einen der dargestellten Knöpfe, springt der Browser dessen »OnClick()«-Routine an. Die frischt zunächst die Statuszeile mit einer Nachricht wie »Request: device on« auf, um dann mit Hilfe des Connection-Managers einen Ajax-Request an den Server abzufeuern.

Auf Kommando

Der Request, um das DSL-Modem einzuschalten, heißt dann zum Beispiel:

/cgi-bin/myx10.cgi?device=dslmodem&action=on

An der später asynchron eintreffenden Antwort interessiert eigentlich nur der HTTP-Statuscode. Ist er »200 (OK)«, dann springt der Browser die Routine »handleSuccess()« im Javascript-Code an, löscht zunächst die Statuszeile und weist anschließend mit Hilfe der Funktion »update_button()« dem Button die entsprechende Farbe zu, die Zustandsänderung wurde offenbar ordnungsgemäß durchgeführt.

Auf eine Status-Abfrage mit »action= status«, antwortet der Server entweder mit »on« oder »off«. Das Newline-Zeichen, das der Antwort anhängt, entfernt der Javascript-Code, bevor »update_button()« den Auftrag zur Button-Aktualisierung erhält.

Tritt beim asynchronen Request ein Fehler auf, kommt die Funktion »handleFailure()« zum Zuge. Dort stehen Statuscode und eine lesbare Fehlermeldung bereit, mit denen die Statuszeile auf den Fehler aufmerksam macht.

Fehler passieren

Diese Logik implementiert das in »myx10.js« definierte Callback-Objekt. Außer den beiden Ansprungspunkten im Fehler- und im Erfolgsfall lassen sich auch Argumente definieren, die diese Funktionen am Ende eines Requests erhalten. Die Zeilen

callback.argument.device = device;
callback.argument.cmd  = action;

setzen das Kürzel des gerade modifizierten Geräts (bequemerweise auch die ID des zugehörigen Buttons) und das zu sendende Kommando. So weiß »handleSuccess()« später genau, zu welchem der vielen asynchron abgeschickten Requests die gerade eingetrudelte Antwort eigentlich gehört.

Beim ersten Laden der Seite ist nämlich schnell ein halbes Dutzend Ajax-Zugriffe gleichzeitig unterwegs, bis sämtliche Knöpfe nach und nach an den auf dem Server vermuteten Gerätezustand angepasst sind. Und auch der Benutzer kann durch schnelles Klicken mehrere Requests fast gleichzeitig auslösen. Der Connection-Manager macht es einfach, Ordnung zu halten und eine eintreffende Antwort nach der anderen abzuarbeiten, ohne die Requests dabei miteinander zu verquirlen.

Da das Server-seitige X10-Kommando einige Sekunden zum Ablaufen benötigt, bleibt ein Button nach dem Anklicken typischerweise eine kurze Zeit farblos. Schön an asynchronen Requests ist, dass die Oberfläche weiter bedienbar bleibt und der Benutzer zum Beispiel problemlos andere Knöpfe betätigen kann. Der Connection-Manager bearbeitet so beliebig viele Verbindungen zugleich.

Installation

Das Skript »myx10« kommt ausführbar nach »/usr/bin«, das Perl-Modul »MyX10.pm« in den Perl-Pfad, etwa nach »/usr/lib/perl5/site_perl«. Die Konfigurationsdatei »/etc/x10.conf« bestückt man mit den Namen und Daten der lokal verwendeten Elektrogeräte, einschließlich der House- und Unit-Codes der daran hängenden X10-Empfänger.

Das CGI-Skript »myx10.cgi« kommt ausführbar in das »cgi-bin«-Verzeichnis des Webservers, das Template »myx10.tmpl« sollte ebenfalls dorthin, damit »myx10.cgi« es findet. Die Javascript-Datei »myx10.js« gehört ins »htdocs«-Verzeichnis des Webservers, denn der Browser sucht sie dort (siehe letzte Zeile in »myx10.tmpl«).

Dann kann sich der Administrator beruhigt zurücklehnen, auf den Knöpfen der Weboberfläche herumdrücken und die entsprechenden Elektrogeräte ein- und ausschalten. Die Relays der angesteuerten X10-Appliance-Module klicken jeweils zur Bestätigung. Das ist Bedienkomfort! (jcb)

Infos

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

[2] Yahoo YUI Library: [http://developer.yahoo.com/yui]

[3] Michael Schilli, “Browser-Turbo”: [https://www.linux-magazin.de/Artikel/ausgabe/2005/12/perl/perl.html]

Der Autor

Michael Schilli arbeitet 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.

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