Home Automation with Perl
The Perl Journal February 2003
By Moshe Bar
Moshe is a systems administrator and operating-system researcher and has a M.Sc and a Ph.D. in computer science. He can be contacted at [email protected].
This is the story of how I automated my new home with Perl. You see, I am a bit of a geek and I love for computers to do things for me and to control tedious tasks. I recently moved into a new home, and instead of spending money on specialized appliances for security and control, I decided to go all wireless and use X10 (see http://www.x10.com/products/x10_ck11a .htm) for controlling analog devices like lamps, the fridge, the garage door, the entrance, shutters, phone messages, and the thermostat. Being of the Perl persuasion when it comes to automation and scripting, I naturally looked for ways to do it in Perl. Fortunately, there is a way to control the CM17 (a small RF remote that plugs into the serial port of your computer) through the SerialPort module and the X10 transceiver (a thing that plugs into the wall and receives RF signals from the CM17) through the X10 Perl module. Both modules are available from CPAN. More information about the X10 module can be found at http:// members.aol.com/Bbirthisel/x10.d.
With the SerialPort module, you can turn a lamp on and off like this:
#!/usr/bin/perl -w use strict; use Device::SerialPort; use ControlX10::CM17; my $sp = Device::SerialPort->new('/dev/ttyS0'); if ($ARGV[0] =~ /^on$/i) { ControlX10::CM17::send($sp,"G1J"); } elsif ($ARGV[0] =~ /^off$/i) { ControlX10::CM17::send($sp,"G1K"); } else { print "You can only turn the lamp 'on' or 'off'\n"; }
The above code works properly on a Linux box, as the /dev/ttyS0 type naming of the serial port suggests.
One could write a series of scripts for each and every device in the house and control them through cron jobs. That, however, would be tedious and would create a management nightmare once you hooked up a significant number of devices. Instead, a general scheduler for home automation coupled with a generalized front-end for input and output using the X10 remote and the Festival text-to-speech system for speech seems a much better strategy. (For more information on Festival, see http://www.cstr.ed.ac.uk/ projects/festival/.)
Before sitting down to invent the wheel, it is always a good idea to make sure that it hasn't already been invented by somebody else. A quick search of the Web revealed that somebody had actually done exactly what I needed to do for his own home and had put the source to his Perl software in the public domain. It's called "MisterHouse," and it is hosted on SourceForge at http:// misterhouse.sourceforge.net/. MisterHouse knows to fire events based on time, web access, socket, voice, and serial data.
Perl subroutines and objects are used to give a powerful programming interface. Here is some example code:
$fountain = new X10_Item 'B1'; set $fountain ON if time_now '6:00 PM'; $movement_sensor = new Serial_Item 'XA2', 'stair'; play(file => 'stairs_creek*.wav') if state_now $movement_sensor eq 'stair'; $v_bedroom_curtain = new Voice_Cmd '[open,close] the bedroom curtains'; curtain('bedroom', $state) if $state = said v_bed room_curtain;
In this example, we turn on the water fountain at 6:00PM and play a creaky sound if there is movement detected at a particular location. Finally, the bedroom curtains can be closed or opened depending on spoken commands via a voice-recognition system such as IBM's ViaVoice for Linux. MisterHouse can use Festival to talk back to the user.
MisterHouse was exactly what I was looking for as a foundation for my own home-automation system. I installed a house sound system with loudspeakers in most rooms of the house and had the wiring for it done by a professional installation company. I also had them install little X10 remote-control receivers in strategic locations around the house and, finally, put the whole environment on a sturdy IBM NetFinity server in the basement.
Through Perl scripts responding to input from the remote control, I hooked up all the doors and the main lights both inside and outside the house; I then hooked up flat-panel touch screens to some cheap Apple iMacs I bought on eBay. The Apple iMacs make no noise at all, don't look like computers, and have more than ample resources to run the web-based home control interface in the living room and kitchen.
The web interface is necessary for operations that are difficult to do with a remote control, such as choosing an MP3 track to play or entering a text string.
It is extremely easy to integrate Perl with Festival. A simple print to the standard input of Festival with the -tts switch will make Festival read the payload of stdin over the soundcard. Next to Festival's ability to act as a speech server of standard socket connections, there is also a Festival module for Perl available at CPAN. The module can be used for blocking and nonblocking speech mode and also knows to return the waveform of the text to be spoken (see http://www.cpan.org/modules/by-module/ Festival/Festival-Client-Async-0.0301.readme).
We use our MisterHouse extensions at home to have certain incoming e-mails read to us aloud over the house sound system if we are at home. Festival, however, doesn't know how to read certain characters; for instance, the @ in an e-mail address. For this, a little trickery is needed; see Listing 1.
Up to this stage, our home automation system could select and listen to MP3s, turn on and off most home appliances, listen to incoming e-mails, alert us when Portsentry (a port-scanning alerter for UNIX systems, including Linux) noticed suspicious activity on our DSL line, and control access to and from our home to the outside world.
The next stage involved making the system automate the house according to real-time events such as weather, time of day, and visitors. One example of such an event is when we drive away and forget to close the garage door. With MisterHouse, you can close the garage door automatically with a script like this:
# # Example of a door monitor # $timer_garage_door = new Timer(); $garage_door = new Serial_Item('DCCH', 'opened'); $garage_door -> add ('DCCL', 'closed''); if($state = state_now $garage_door) { set $timer_garage_door 120; play('rooms' => 'all', 'file' => "garage_door_" . $state . "*.wav"); } if ((time_cron('0,5,10,15,30,45 22,23 * * *') and ('opened' eq ($garage_door->{state})) and inactive $timer_garage_door)) { speak("The garage door has been left opened. I am now closing it."); set $garage_door_button ON; set $garage_door_button OFF; }
Quite obviously, this script could also be used to automate the opening and closing of curtains, to turn auxiliary computers on and off, and to have coffee ready for you after you get out of the shower in the morning by monitoring the shower's heater for activity (indicating someone is having a shower).
I don't usually need an alarm clock to wake me up in the morning, but when I have to catch an early flight, I like to be sure to be up on time. A few lines of Perl turn the television on to a music channel and switch the bedroom lights on:
set $left_bedroom_light ON; set $right_bedroom_light ON; runit("min", "ir_cmd TV,POWER,5,1");
Just as I was finishing this column, I noticed a very nice article by brian d foy at The O'Reilly Network (see http://www.oreillynet .com/). The article outlines how to remotely control iTunes, the excellent Mac OS X music player, through a mixture of Perl and OS X AppleScript.
brian d foy is the creator of the Mac::iTunes Perl module, which allows iTunes to be controlled from any computer in the network. Since iTunes itself, like many OS X applications, is an AppleScript-aware application, you can have Perl programs launch AppleScripts.
AppleScript can be controlled either interactively or in batch mode through the CLI tool osascript with the Mac::iTunes::AppleScript module, which wraps common AppleScripts in Perl functions. At the core of that module lies the _osascript routine, which creates an AppleScript string and then calls the OS X CLI tool osascript. It is very easy with the Mac::iTunes module to play, for example, a random MP3 track off the database; see Listing 2.
Apple actually provides a whole tarball of AppleScripts for iTunes, available for download from its web site. Some examples of the scripts provided are:
- Rewind Tracks
- Replace Text in Track Names
- Enable/Disable Selected Tracks
- eMusic Search by Song Title
- eMusic Search by Artist
- eMusic Search by Album
- Make Playlist by Artist
- Create Library Summary
- Remove Playlists from Source
- Remove Missing Tracks
- Mute On/Off
There are many more ready-made scripts from the Apple web site and almost unlimited possibilities exist to create others using the simple tools I describe here. In my home, I made use of these scripts to automate my MP3 jukebox, which is based on a Mac Cube connected to the stereo for good sound quality.
TPJ
Listing 1
# Modify this rule for use with get_email # Rename to get_email_rule.pl to enable # - $from_full has the full email address, not just the name portion. sub get_email_rule { my ($from, $to, $subject, $from_full) = @_; $from = 'The S F gals' if $to =~ /FEM-SF/; $from = 'The E C S guys' if $to =~ /ecs/; $from = 'The Mister House guys' if $to =~ /misterhouse/; $from = 'The perl guys' if $to =~ /Perl-Win32-Users/; $from = 'The phone guys' if $to =~ /ktx/ or $subject =~ /kx-t/i; # If we get a joe#### type address, assume it is junk mail. $from = 'junk mail' if $from =~ /\S+[0-9]{3,}/; return if $from =~ /X10 Newsletter/; $from =~ s/\./ Dot /g ; # ...change "." to the word "Dot" $from =~ s/\@/ At /g ; # ...change \@ to the word "At" return $from; } return 1;
Listing 2
sub _osascript { my $script = "tell source 'Library' tell playlist 'Library' set this_track to some track set this_name to the name of this_track set this_artist to the artist of this_track set this_album to the album of this_track play this_track end tell end tell"; require IPC::Open2; my( $read, $write ); my $pid = IPC::Open2::open2( $read, $write, 'osascript' ); print $write qq(tell application "iTunes"\n), $script, qq(\nend tell\n); close $write; my $data = do { local $/; <$read> }; return $data; }