A remote control to reset your router

Home Switcher


My low-budget router has just crashed, and there is no way to reset the beast remotely via a network. Never fear - an X10 module, controlled by a web GUI with an Ajax interface, can actuate the mains switch.

By Michael Schilli

Joellen Armstrong, Fotolia

Regular readers will recall that last month's Perl column relied on X10 technology to transmit switching signals over normal power lines at home. In this article, I will extend the scenario to include three new devices with X10 receivers: my DSL modem (Figure 1), my DSL router, and my TiVo digital video recorder. The lights in the bedroom and living room are already connected to X10 boxes.

Figure 1: The DSL modem's power cord is plugged into an X10 receiver, allowing me to switch it on and off remotely, if necessary.

Figure 2 shows the results of running the scripts in this article. As you can see, the browser displays intuitive names for the devices in question, and a button in the right-hand column of the table for each row lights up green or red, depending on the current device status. Clicking the button toggles the device state. This all relies on state-of-the-art Ajax technology - the browser does not need to reload the whole web page, just single fields that have changed.

Figure 2: The interactive browser application controls multiple X10 devices at the push of a button via an intuitive GUI.

Every X10 device is set to a unique house and unit code that I can use to address the device via the power line. Users normally prefer to avoid having to memorize cryptic combinations of letters and numbers, which is why the /etc/x10.conf file (Listing 1) defines the accessible X10 devices in YAML format ("YAML Ain't Markup Language," a data serialization language modeled on XML).

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

A dash at the start of a name in YAML means "this is an array element." In contrast, the colon notation separates the key/value pairs in a hash.

The configuration that is detailed in Listing 1 thus defines an array of devices wherein each device is represented by a hash that specifies values for the device name, the house/unit code, and an intuitive name in the device, code, and name keys, respectively.

The script in Listing 2 allows us to address specific devices at the command line via their mnemonic names - that is, to switch the devices on or off or to query their status:

# myx10 dslmodem on
# myx10 dslmodem status
on
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);

Cheap Trick

Unfortunately, low-budget X10 modules only support unidirectional communications. You can control them, but you can not query their status. However, if you exclusively use the script to control the modem, the script will use a persistent DBM file to remember whether the receiver is switched on or off.

Of course, this can be confusing if you manually switch the device on or off without using the software, but you can easily resolve this issue by using the web GUI to switch the device on or off again, thereby returning everything back to a known state.

In Listing 3, myx10 relies on the MyX10.pm module, which then starts by first setting the baud rate and the serial interface for communications with the X10 transceiver, just as in last month's column. It then uses dbmopen() to set up a persistent DB_File-type DBM file in /var/local/myx10.db to store the assumed switching status of the device with the matching device key.

The DESTROY() method shown in line 56 closes the DBM file when the MyX10 object is destroyed.

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"
018    if $> != 0;
019
020  my $self = {
021   serial   => "/dev/ttyS0",
022   baudrate => 4800,
023   devices  => LoadFile(
024    "/etc/x10.conf"),
025   commands => {
026    on     => "J",
027    off    => "K",
028    status => undef,
029   },
030   dbm     => {},
031   dbmfile =>
032     "/var/local/myx10.db",
033   %options,
034  };
035
036  $self->{devhash} =
037    { map { $_->{device} => $_
038    } @{ $self->{devices} } };
039
040  dbmopen(%{ $self->{dbm} },
041   $self->{dbmfile}, 0644)
042    or LOGDIE
043 "Cant open $self->{dbmfile}";
044
045  for (
046   keys %{ $self->{devhash} })
047  {
048   $self->{dbm}->{$_} ||=
049     "off";
050  }
051
052  bless $self, $class;
053 }
054
055 #############################
056 sub DESTROY {
057 #############################
058  my ($self) = @_;
059  dbmclose(%{ $self->{dbm} });
060 }
061
062 #############################
063 sub send {
064 #############################
065  my ($self, $device, $cmd) =
066    @_;
067
068  LOGDIE(
069   "No device specified")
070    if !defined $device;
071
072  LOGDIE("Unknown device")
073    if !
074    exists $self->{devhash}
075    ->{$device};
076
077  LOGDIE(
078   "No command specified")
079    if !defined $cmd;
080
081  LOGDIE("Unknown command")
082    if !
083    exists $self->{commands}
084    ->{$cmd};
085
086  if ($cmd eq "status") {
087   print $self->status(
088    $device), "\n";
089   return 1;
090  }
091
092  my $serial =
093    Device::SerialPort->new(
094   $self->{serial}, undef);
095
096  $serial->baudrate(
097   $self->{baudrate});
098
099  my ($house_code, $unit_code)
100    = split //,
101    $self->{devhash}
102    ->{$device}->{code}, 2;
103
104  sleep(1);
105
106  # Address unit
107  DEBUG "Addressing ",
108  " HC=$house_code",
109  " UC=$unit_code";
110  ControlX10::CM11::send(
111   $serial,
112   $house_code . $unit_code);
113
114  DEBUG
115  "Sending command $cmd ",
116  "$self->{commands}->{$cmd}";
117  ControlX10::CM11::send(
118   $serial,
119   $house_code
120     . $self->{commands}
121     ->{$cmd}
122  );
123
124  $self->{dbm}->{$device} =
125    $cmd;
126 }
127
128 #############################
129 sub status {
130 #############################
131  my ($self, $device) = @_;
132  return $self->{dbm}
133    ->{$device};
134 }
135
136 1;

Unordered Hashes

If you simply wanted to test whether a specified device exists or to access the house/unit code via the device mnemonic, it would make sense to store /etc/x10.conf in a hash. However, a hash will not keep the order we originally defined, and this order is important for rendering the device list in the browser.

Therefore, lines 36-38 in Listing 3 convert the YAML array into a hash with keys that represent the device names, which have the YAML device hashes as values. The instance variable devhash stores a reference to this data structure for quick lookups. Lines 45-50 iterate over all the entries and set the persistent states for any previously untouched devices to off. This needn't be true, but if not, the next state change will bring the X10 receiver back in line with the then newly saved state.

The send() method talks to the X10 transceiver attached to my Linux box to transmit a command to the X10 receiver specified by the device name. Valid commands are on and off. On a status command, line 86 will jump to the status() method defined further down to pull the assumed status of the X10 receiver out of the hat, er, data store.

Between initializing the serial port and calling X10, MyX10.pm sleeps for a second with sleep(1). The need for this is unknown, but before I put the break in, strange X10 timing-control problems kept occurring.

Sudo Without a Password

Only the root user is permitted to send X10 signals via the serial port, which is why myx10 has to run as root. If you intend to control devices via a web GUI, security concerns call for the web server to run as nobody, instead of root, which is asking for trouble.

The following entry in /etc/sudoers gives you a workaround, opening up a small hole that lets the web server run the myx10 script as root via sudo without requiring a password:

# /etc/sudoers
nobody ALL= NOPASSWD:/usr/bin/myx10

The ALL keyword to the left of the equals sign specifies no restriction to specific host names. The command following the colon restricts permitted activities to the specified script. This means that, should an attacker manage to compromise your web server, they would be able to switch X10 devices on and off, but they would not be able to compromise the root account on the Linux machine. As an alternative, you could avoid the sudo trick by running chmod a+rw /dev/ttyS0 to let everyone on the machine use the serial port.

The myx10.cgi CGI (Listing 4) simply calls the myx10 command-line script and sends the script's output back to the web client. To do so, myx10.cgi uses the tap function from the Sysadm::Install CPAN module, which basically gives us a convenient way of capturing the output from a command.

However, if the browser calls myx10.cgi without passing in any device parameters, the web client will want to see the overview shown in Figure 2. To allow this to happen, myx10.cgi loads the X10 configuration file and then calls the template toolkit processor to render the myx10.tmpl template (Figure 3). At this point, a foreach loop ensures that a column with a button is drawn for every configured device.

Figure 3: The HTML template for the web application.

The onClick action for each button calls the toggle() Javascript function defined later in myx10.js, not only to handle server communications, but also to change the button color to match the results. The id for each button is set to the device name, and class is set to "clicker" to allow a Javascript function to iterate over all elements tagged in this way later.

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 =
15    LoadFile("/etc/x10.conf");
16
17  my $tpl = Template->new();
18  $tpl->process("myx10.tmpl",
19   { devices => $devices, })
20    or die $tpl->error();
21  exit 0;
22 }
23
24 if (!defined $action
25  or $action !~
26  /^(on|off|status)$/)
27 {
28  print
29 "Error: No/Invalid action\n";
30  exit 0;
31 }
32
33 if (!defined $device
34  or $device =~ /\W/)
35 {
36  print
37 "Error: use proper device\n";
38  exit 0;
39 }
40
41 system "sudo",
42   "/usr/bin/myx10", $device,
43   param("action");

YUI

Modern web applications no longer reload whole pages when a user presses a button. Instead, web server communications are handled asynchronously via Ajax, and only those elements that have changed are redrawn. Because Ajax is fairly difficult to program and excessive use of Javascript can cause hair loss, a number of Javascript libraries simplify this task and guarantee browser compatibility at the same time. One example of this is the YUI library by my employer, Yahoo, which is available free of charge and without registration. A zip file [2] contains all the Javascript files you will need in its build directory.

After completing the download, just unpack the zip archive and copy the build directory to, for example, htdocs/yui on the local web server. From now on, Javascript applications can load .js files, such as src=/yui/yahoo/yahoo.js, for example.

Dynamically Patched HTML

The myx10.js file (Figure 4), pulled in at the end of myx10.tmpl, defines the update_buttons() function, which the browser calls after loading the document. This ensures that the browser calls the server to obtain the status of every configured device at page load time:

x10remote(device, 'status');

To do this, the Javascript code uses YAHOO.util.Dom.getElementsByClassName() from YUI, which is called to return any DOM nodes tagged with the class="clicker" attribute.

Figure 4: The Javascript code in myx10.js.

To query the status of an X10 receiver configured in /etc/x10.conf, the browser asynchronously calls the CGI script for each defined button passing in the device=mnemonic and action=status parameters. myx10.cgi then checks its DBM file on the server and returns the last known state of the specified X10 device as on or off.

Dynamic Coloring

The Javascript file myx10.js displays the buttons for actuated X10 receivers in green and de-actuated receivers in red.

This action is handled by the Yahoo.dom class's setStyle method, which accepts a browser DOM object name, searches for the object, and modifies the BackgroundColor attribute of the CSS style sheet. The first time the HTML page generated by the CGI script is loaded, the buttons do not have a color assignment, but update_buttons() sends an Ajax request for each button to the server, which then checks the DBM file to query the status of the device that is in question.

When a response to an asynchronous request arrives, the button is checked to see whether it is on or off, and the button in question is painted accordingly.

The YUI Connection Manager is used here to keep the Javascript code manageable despite dozens of simultaneous requests.

At My Command

When a user clicks one of the buttons, the browser jumps to the OnClick() routine for the button, which first refreshes the status line with a message such as Request: device on before going on to send an Ajax request to the server via the Connection Manager.

For example, the request for switching on the DSL modem looks like this:

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

The only interesting thing about the asynchronous response that arrives later is the HTTP status code.

If the status code is 200 (OK), the browser jumps to the Javascript handleSuccess() routine, where it first deletes the status line before calling update_button() to change the color assignment for the button because a state change has occurred.

After querying the status with action=status, the server will respond with either on or off on the page it returns. Because the response is terminated by a newline character, the Javascript code first removes the newline before passing it on to the update_button() function.

Errors Happen

If an error occurs on handling the asynchronous request, the YUI connection manager will jump to handleFailure().

The function prints the status code and a legible error message on the status line to alert the user.

This logic is provided by a callback object that is defined in myx10.js. Besides the two jump points for the error and success cases, myx10.js also defines arguments to pass to the callback functions at the end of a request.

The following lines

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

set the mnemonic for the device that has just been modified (conveniently used as the id setting for the corresponding button as well) and the command to be transmitted. This helps handleSuccess() link the response to one of the many asynchronous requests that might have been sent.

The first time the page is loaded, half a dozen Ajax requests can occur simultaneously, and it can take a while for the buttons to change to reflect the assumed device status.

Users can also trigger multiple quasi-simultaneous requests by clicking buttons in quick succession.

The Connection Manager makes it easy to keep track of and to process responses one after another without mixing up the requests.

Because the server-side X10 command takes a couple of seconds to execute, a button will typically not have a color assignment for a couple of seconds after you click it.

The nice thing about asynchronous requests is that you can carry on using the GUI (clicking buttons, that is), and the GUI will keep responding happily.

Installation

Store the myx10 script in /usr/bin and make the script executable. Make sure the script is owned by root and that it cannot be modified by anyone else. Then store the MyX10.pm Perl module in your Perl path (for example, /usr/lib/perl5/site_perl).

Add the names and data for your local devices to your /etc/x10.conf configuration file, not forgetting the house and unit codes for the X10 receivers.

You need to make the myx10.cgi CGI script executable and drop it into your web server's cgi-bin directory, which is also where you will need the myx10.tmpl template so that myx10.cgi can find it.

You need the myx10.js Javascript file in your web server's htdocs directory because the browser will look for it at this location (last line of myx10.tmpl).

Conclusion

After completing all of these tasks, you can sit back, relax, and press the buttons on the web interface to switch the corresponding devices on and off.

If you are near enough, you will even hear the relays on the X10 appliance modules clicking. Now that's what I call convenient!

INFO
[1] Listings for this article: http://www.linux-magazine.com/Magazine/Downloads
[2] Yahoo YUI library: http://developer.yahoo.com/yui