Hat sich ein Billig-Router aufgehängt, ist kein Reset mehr über das Ethernet möglich. Hängt seine Stromversorgung jedoch an einem X10-Modul, kann man ihm mit dem heute vorgestellten Ajax-Web-GUI den Saft ab- und wieder andrehen.
Abbildung 1: Das Netzteil des DSL-Modems hängt an einem X10-Empfänger, um es notfalls per Fernsteuerung aus- und wieder einzuschalten. |
Abbildung 2: Die interaktive Browserapplikation steuert verschiedene X10-Geräte auf Knopfdruck. |
Schon im letzten Snapshot kam X10-Technologie zum Einsatz, heute werden gleich drei weitere Geräte mit X10-Empfängern versehen: Das DSL- Modem (Abbildung 1), der DSL-Router und mein digitaler Videorekorder TiVo. Und die Lampen im Schlaf- und im Wohnzimmer hängen sowieso schon an X10- Kästen. Abbildung 2 zeigt die heute gezeigten Skripte im Browser in Aktion. Die Geräte werden mit leserlichen Namen angezeigt und in der rechten Spalte der Tabelle befindet sich in jeder Reihe ein Button, der entweder grün oder rot eingefärbt ist, je nach dem, ob der am jeweiligen Gerät hängende X10-Empfänger ein- oder ausgeschaltet ist. Ein Mausklick auf den Button, und ein ausgeschaltetes Gerät wird eingeschaltet und umgekehrt. Dabei kommt modernste Ajax-Technologie zum Einsatz, die Seite bleibt im Browser, nur veränderte Felder werden aufgefrischt.
Jedes X10-Gerät in einem Haushalt ist auf einen bestimmten House- und
Unit-Code eingestellt, damit es eindeutig im Stromnetz addressierbar
ist. Damit der User sich nicht diese Buchstaben und Nummern zu merken
muss, definiert die
Datei /etc/x10.conf
(Listing 1) einfach alle erreichbaren X10-Geräte
im YAML-Format.
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 heißt in YAML soviel wie
``Array-Element'', während die Doppelpunktnotation die Key/Value-Paare
eines Hashs trennt. Die in /etc/x10.conf
angegebene Konfiguration
gibt also einen Array von Geräten an. Jedes wird durch einen Hash
repräsentiert, der unter den Schlüsseln device
, code
und name
Werte für das Gerätekürzel, die House/Unit-Code-Kombination und
einen leserlichen Gerätenamen enthält.
1 #!/usr/bin/perl -w 2 use strict; 3 use MyX10; 4 my($device, $command) = @ARGV; 5 my $x10 = MyX10->new(); 6 $x10->send($device, $command);
Das Skript myx10
erlaubt es dann, von der Kommandozeile aus
bestimmte Geräte über ihr Kürzel anzusprechen, sie ein- (on) oder
auszuschalten (off) und ihren Status abzufragen:
# myx10 dslmodem on # myx10 dslmodem status on
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. Wird ein Empfänger aber ausschließlich über das heute gezeigte Skript bedient, merkt sich einfach das Skript in einer kleinen persistenten dbm-Datei, ob der Empfänger gerade ein- oder ausgeschaltet ist. Das führt zwar zu Verwirrungen, falls man Geräte von Hand oder mittels anderer Fernbedienungen bedient, doch ein kleiner Zustandsfehler lässt sich leicht beheben, indem man einen Wechsel über die Web-UI herbeiführt. Anschließend ist wieder alles im Lot.
myx10
nutzt dafür die Dienste des Perlmoduls MyX10.pm
in
Listing 3, das zunächst, wie schon im letzten 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 Zustand (on|off) des
zugehörigen Gerätes abzuspeichern. Die DESTROY-Methode ab Zeile 48
schließt die dbm-Datei wieder, falls das MyX10
-Objekt zerstört
wird.
001 package MyX10; 002 ########################################### 003 use strict; 004 use warnings; 005 use Device::SerialPort; 006 use ControlX10::CM11; 007 use YAML qw(LoadFile); 008 use Log::Log4perl qw(:easy); 009 use DB_File; 010 011 ########################################### 012 sub new { 013 ########################################### 014 my($class, %options) = @_; 015 016 LOGDIE "You must be root" if $> != 0; 017 018 my $self = { 019 serial => "/dev/ttyS0", 020 baudrate => 4800, 021 devices => LoadFile("/etc/x10.conf"), 022 commands => { 023 on => "J", 024 off => "K", 025 status => undef, 026 }, 027 dbm => {}, 028 dbmfile => "/var/local/myx10.db", 029 %options, 030 }; 031 032 $self->{devhash} = { 033 map { $_->{device} => $_ } 034 @{$self->{devices}} }; 035 036 dbmopen(%{$self->{dbm}}, 037 $self->{dbmfile}, 0644) or 038 LOGDIE "Cannot open $self->{dbmfile}"; 039 040 for (keys %{$self->{devhash}}) { 041 $self->{dbm}->{$_} ||= "off"; 042 } 043 044 bless $self, $class; 045 } 046 047 ########################################### 048 sub DESTROY { 049 ########################################### 050 my($self) = @_; 051 dbmclose(%{$self->{dbm}}); 052 } 053 054 ########################################### 055 sub send { 056 ########################################### 057 my($self, $device, $cmd) = @_; 058 059 LOGDIE("No device specified") if 060 !defined $device; 061 062 LOGDIE("Unknown device") if 063 !exists $self->{devhash}->{$device}; 064 065 LOGDIE("No command specified") if 066 !defined $cmd; 067 068 LOGDIE("Unknown command") if 069 !exists $self->{commands}->{$cmd}; 070 071 if($cmd eq "status") { 072 print $self->status($device), "\n"; 073 return 1; 074 } 075 076 my $serial = Device::SerialPort->new( 077 $self->{serial}, undef); 078 079 $serial->baudrate($self->{baudrate}); 080 081 my($house_code, $unit_code) = split //, 082 $self->{devhash}->{$device}->{code}, 2; 083 084 sleep(1); 085 086 # Address unit 087 DEBUG "Addressing HC=$house_code ", 088 "UC=$unit_code"; 089 ControlX10::CM11::send($serial, 090 $house_code . $unit_code); 091 092 DEBUG "Sending command $cmd ", 093 "$self->{commands}->{$cmd}"; 094 ControlX10::CM11::send($serial, 095 $house_code . 096 $self->{commands}->{$cmd}); 097 098 $self->{dbm}->{$device} = $cmd; 099 } 100 101 ########################################### 102 sub status { 103 ########################################### 104 my($self, $device) = @_; 105 return $self->{dbm}->{$device}; 106 } 107 108 1;
Für schnelle Tests, ob ein angegebenes Device existiert oder um
vom Device-Kürzel zu dessen House/Unit-Code zu gelangen, wäre es
sinnvoll, /etc/x10.conf
in Hashform zu speichern. Doch leider
geht in einem Hash die ursprünglich definierte Reihenfolge verloren,
und die ist für die im Browser geplante Anzeige wichtig. Wer will
schon bunt durcheinander gewürfelte Bedienelemente?
So machen sich die Zeilen 32-34 in MyX10.pm
daran, den Array mit
Hashelementen in einen Hash umzuwandeln, dessen Schlüssel die
Gerätekürzel sind, und der als Werte die vorher definierten
jeweiligen Geräte-Hashes führt. In der Instanzvariablen devhash
wird eine Referenz auf diesen Schnellzugreifer für später abgelegt.
Die Zeilen 40-42 iterieren über alle Einträge und setzen den Zustände
bislang unbekannter Geräte auf off
.
Das muss nicht unbedingt stimmen, aber falls nicht, renkt der nächste
Zustandswechsel den X10-Empfänger wieder ein.
Die Methode send()
schickt über den am Linux-Rechner angeschlossenen
X10-Transceiver ein Kommando an einen per Gerätekürzel angegebenen
X10-Empfänger. Ist das Kommando nicht ``on|off'', sondern ``status'',
verzweigt Zeile 71
stattdessen zu der weiter unten definierten Methode status()
, die den
vermuteten Status des X10-Empfängers aus der Konserve holt.
Zwischen der Initialisierung der seriellen Schnittstelle
und dem Aufruf des X10 schläft MyX10.pm
eine Sekunde
lang mit sleep(1)
. Dies wäre eigentlich unnötig, doch
beim Weglassen stellten sich unerklärliche Timing-Probleme
mit der X10-Ansteuerung ein.
Nur root
darf X10-Signale über die serielle Schnittstelle senden.
Deswegen muss myx10
als root
laufen. Will man die Geräte über ein
Web-GUI steuern, ergibt sich ein Problem:
Der Webserver läuft aber sicherheitshalber als nobody
, ihn als
root
laufen zu lassen, wäre grob fahrlässig.
Aber folgender
Eintrag in /etc/sudoers öffnet ein kleines Loch, das den Webserver
über sudo
das Skript myx10
als root
ausführen lässt, ohne
dass die Eingabe eines Passworts erforderlich wäre:
# /etc/sudoers nobody ALL= NOPASSWD:/usr/bin/myx10
Das Schlüsselwort ALL
links vom Gleichheitsszeichen legt fest,
dass keine Beschränkung über den Hostnamen erfolgt.
Das nach dem Doppelpunkt folgende Kommando beschränkt
mögliche Aktivitäten allerdings auf das angegebene Skript.
So kann 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.
Alternativ könnte man auch mit chmod a+rw /dev/ttyS0
die serielle
Schnittstelle für jedermann beschreibbar machen, dann könnte man sich
den sudo-Trick ganz sparen.
Das CGI-Skript myx10.cgi
macht dann auch nicht viel mehr, als
das Kommandozeilenskript myx10
aufzurufen und dessen Ausgabe zurück
zum Webclient zu senden. Es nutzt hierzu 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.
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
definierte Funktion toggle()
auf, die nicht nur
die Kommunikation mit dem Server abwickelt, sondern auch die Farbe
des Buttons entsprechend dem Ergebnis anpasst. Die id
jedes
Buttons wird auf das Gerätekürzel gesetzt, die Klasse class
auf den zufällig gewählten Namen "clicker"
, damit eine
JavaScript-Funktion später leicht über alle so ausgezeichneten Elemente
iterieren kann.
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 action\n"; 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");
Abbildung 3: Das HTML-Template der Webapplikation |
Moderne Web-Applikationen laden nicht mehr die ganze Seite nach, wenn
nur
ein Knöpferl gedrückt wurde. Die Kommunikation mit dem Webserver
findet auf asynchronem Weg über Ajax statt und nur tatsächlich
veränderte Elemente werden neu gezeichnet ([3]).
Da Ajax aber recht umständlich zu programmieren ist und
exzessiver JavaScript-Gebrauch
bekanntlich Haarausfall verursacht, gibt es eine Reihe von
JavaScript-Bibliotheken, die die Handhabung vereinfachen und
Browerkompatibilitä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 mit die .js-Dateien
zum Beispiel als src=/yui/yahoo/yahoo.js
einbinden.
Die am Ende von myx10.tmpl
eingebundene JavaScript-Datei myx10.js
(Abbildung 4) definiert die Funktion update_buttons()
, die der Browser
aufruft, gleich nachdem das Dokument geladen wurde.
Für jedes Gerät wird so nicht nur ein Eintrag in der in Abbildung 2 dargestellten HTML-Tabelle erzeugt, sondern auch der Aufruf
x10remote(device, 'status');
durchgeführt. Der JavaScript-Code nutzt hierfür die Methode
YAHOO.util.Dom.getElementsByClassName()
der YUI, die einfach alle
gefundenen DOM-Knoten liefert, die vorher mit dem Attribut
class="clicker"
gekennzeichnet wurden.
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=kuerzel
und action=status
auf. Daraufhin sieht
myx10.cgi
auf dem Server in seiner dbm-Datei nach und gibt den letzten dort
abgelegten Zustand des gewünschten X10-Gerätes als ``on'' oder ``off'' zurück.
Abbildung 4: Der JavaScript-Code in myx10.js |
Die JavaScript-Datei myx10.js
zeigt die Knöpfe der eingeschalteten
X10-Empfänger grün und die deaktivierten rot an. Dies erledigt die
Methode setStyle
der Klasse Yahoo.dom
, die den Namen eines Objektes
der Browser-DOM entgegennimmt, es heraussucht und das
BackgroundColor
-Attribut des CSS-Stylesheets modifiziert.
Beim ersten Laden der vom CGI
-Skript generierten HTML-Seite sind
die Knöpfe zunächst alle farblos, aber 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ätes vom Server holt.
Trifft die Antwort auf einen dieser asynchronen Requests ein, wird
sie auf "on"
oder "off"
überprüft und der entsprechende
Knopf eingefärbt.
Damit der JavaScript-Code auch bei dutzenden von gleichzeitig abgefeuerten Requests übersichtlich bleibt, kommt der ConnectionManager der YUI zum Einsatz.
Drückt der Benutzer mit der Maus auf einen der dargestellen Knöpfe, springt der Browser dessen OnClick()-Routine an. Diese frischt zunächst die Statuszeile mit einer Nachricht wie ``Request: device on'' auf, um dann mittels des ConnectionManagers einen Ajax-Request an den Server abzufeuern.
Der Request, um das DSL-Modem einzuschalten, heißt dann zum Beispiel
/cgi-bin/myx10.cgi?device=dslmodem&action=on
und an der später asynchron eintreffenden Antwort interessiert eigentlich
nur der HTTP-Statuscode. Ist er 200 (OK), springt der Browser die Routine
handleSuccess()
im JavaScript-Code an. Dort wird zunächst die Statuszeile
gelöscht und anschließend mittels der Funktion update_button()
dem
Button die entsprechende Farbe zugewiesen, denn die
die Zustandsänderung wurde offenbar ordnungsgemäß durchgeführt.
Wurde mit action=status
eine Statusabfrage durchgeführt, antwortet
der Server auf der zurückgeschickten Seite entweder mit "on"
oder
"off"
. Da der Antwort auch ein Newline-Zeichen anhängt, entfernt
der JavaScript-Code dieses, bevor update_button()
den Auftrag zur
Button-Aktualisierung erhält.
Tritt beim asynchronen Request hingegen ein Fehler auf, wird die
Funktion handleFailure
angesprungen. Dort stehen Status-Code und
eine lesbare Fehlermeldung bereit und der Fehler wird in der Statuszeile
angezeigt.
Diese Logik wird über das in myx10.js
definierte Callback-Objekt erzielt. Außer
den beiden Ansprungpunkten im Fehler- und im Erfolgsfall lassen sich
auch Argumente definieren, die diesen Funktionen am Ende eines
Requests übergeben werden. Die Zeilen
callback.argument.device = device; callback.argument.cmd = action;
setzen so das Kürzel des gerade modifizierten Geräts (bequemerweise
auch die ID des zugehörigen Buttons) und das zu sendende Kommando.
So weiss handleSuccess()
später genau, zu welchem der vielen asynchron
abgeschickten Requests die gerade eingetrudelte Antwort eigentlich gehört.
Beim ersten Laden der Seite sind 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 quasi gleichzeitig auslösen. Der ConnectionManager macht es einfach, Ordnung zu halten und eine eintrudelnde Antwort nach der anderen abzuarbeiten, ohne die Requests miteinander zu verquirlen.
Da das Serverseitige X10-Kommando einige Sekunden zum Ablaufen benötigt, bleibt ein Button nach dem Anklicken typischerweise eine zeitlang farblos. Das Schöne an asynchronen Requests ist dabei freilich, dass die Oberfläche bedienbar bleibt und der Benutzer zum Beispiel andere Knöpfe drücken kann. Der Connection-Manager bearbeitet so beliebig viele Verbindungen gleichzeitig.
Das Skript myx10
kommt ausführbar nach /usr/bin
und das Perl-Modul
MyX10.pm
in den Perl-Pfad, zum Beispiel nach /usr/lib/perl5/site_perl.
Die Konfigurationsdatei /etc/x10.conf
sollte mit den Namen und
Daten der lokal verwendeten Elektrogeräte bestückt werden, einschließlich
der House- und Unitcodes der daranhängenden X10-Empfänger.
Das CGI-Skript myx10.cgi
kommt ausführbar
in das cgi-bin
-Verzeichnis des
Webservers und das Template myx10.tmpl
sollte auch dorthin, damit
myx10.cgi
es findet. Die JavaScript-Datei myx10.js
sollte ins
htdocs
-Verzeichnis des Webservers, denn der Browser sucht sie
(letzte Zeile in myx10.tmpl
) dort.
Dann kann sich der Administrator 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!
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. |