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: |
|---|
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: |
|---|
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: |
|---|
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: |
|---|
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. |





