I mentioned last week that I’ve been working on building sort of a lightweight home security system for my house so my 4-year-old daughter won’t be able to sneak out of the house again.

I spent about a day researching possible solutions, to see if there was something simple that I could buy that would make me happy, and I couldn’t find anything on the market that was cheap and would tell me which of my 6 exterior doors had been opened loudly enough to hear from across the house. There’s no point in putting a door buzzer on the basement door if you can’t hear it from the master bedroom. The only solutions that I found were from professional alarm companies, and they probably would have charged me a couple thousand for installation plus $30-$50 per month for monitoring. I’m just not willing to pay that much, and it’s not really what I was looking for–it’s gross overkill for my problem.

So, I decided to build it myself. After a few hours’ searching, I decided to use Perceptive Automation’s Indigo home-automation software for the Mac. It’s commercial software, but I like its user interface and capabilities better then any of the open-source solutions that I’ve seen. It’s under active development, supports just about everything that I need, it’s client-server so I can access it from any Mac I own, and it comes with a nice web interface that I can get to via my phone. I figured that it’d be cheaper to pay the money for Indigo then to spend most of a week hacking away at one of the open-source packages to get them to do what I want. Plus, er, pretty much every open source home automation program that I could find was written in Perl, and I’ve been successfully avoiding Perl for almost 5 years now. The last thing I really want to do is spend a week modifying someone else’s Perl. That kind of thing gives me nightmares.

So I fired Indigo up on an old Powerbook that I had laying around and bought a bunch of hardware from MacHomeStore and SmartHome.com. The important bits are a W800RF32A wireless reciever and a whole bunch of $12 DS10a door and window sensors. The sensors broadcast their state over the air, and the receiver feeds them into my Mac.

That took care of the input side of the equation, but I still needed some relatively cheap way to play audio around my house. I poked around for a while looking at random X10 hardware and radio solutions before I realized that most VoIP phones support audio paging. Since my house is full of VoIP phones and I run my own Asterisk server, all I’d really need to do was write a couple dozen lines of Asterisk dialplan code, and everything should just work. I ended up ordering two new VoIP phones (Grandstream GXP-2020s) because two of the phones that I have are too old to be usable for paging. I probably could have used $45 Budgetones, but I have future plans for the GXP-2020’s big displays, so I decided to spend a bit more for them.

It took me about 30 minutes to unpack everything, install Indigo, and have it receiving data from a test sensor. Each sensor assigns itself a random 8-bit ID when it’s powered up, so the first problem was mapping semi-random sensor IDs onto logical names. Indigo comes with a blob of sample AppleScript for doing this, and it only took me 5 minutes to modify it so that one of Indigo’s internal variables changed state to reflect the state of the door sensor–true when the door is closed and false when opened. Five more minutes and I had a nifty web page with a green blob that turned red when the door opened. A half-hour after that, I had a PNG floorplan of my house that I could use as a backdrop for a bunch of little red/green blobs:

After this, it’s all just a matter of plumbing. First, I modified Indigo’s sensor-handling AppleScript example into something that knows how to talk to Asterisk, using one of the Asterisk/AppleScript integration examples on the voip-info.org Wiki. Here’s I ended up with:

(*
	Door sensor tracking code for Indigo and Asterisk.
	By Scott Laird <scott@sigkill.org>
	http://scottstuff.net
	
	This code handles incoming security events, maps each numeric
	device ID onto a logical name, and then tells Asterisk to page all
	VoIP phones with a device-specific message.
	
	Net effect: opening the front door causes "Front door opened" to echo
	throughout the house.
	
	I'm calling doDialOut directly rather then using an Indigo trigger for 4
	reasons:
	  1.  I have the extension name handy here, while I'd have to parse it back out of the variable name if I used triggers.
	  2.  Creating an identical trigger for each of 10+ sensors is a pain in the neck.
	  3.  We need to do *something* with unknown sensor events, but creating variables for them is pretty clearly wrong.
	  4.  My AppleScript is lousy, and I can't get triggers to call doDialOut correctly.
*)

using terms from application "IndigoServer"
	on receive security event of eventType with code devID
		set extension to "unknown"
		
		-- Map sensor IDs onto logical names.  In a real language,
		-- I'd use some sort of hash and skip the if ... else if ... code,
		-- but I don't see anything suitable in AppleScript.  Sigh.  I'm
		-- stuck writing code in blub.
		if devID is 95 then
			set extension to "frontdoor"
		else if devID is 175 then
			set extension to "deckdoor"
		else if devID is 99 then
			set extension to "stairs"
		else if devID is 201 then
			set extension to "backyard"
		else if devID is 55 then
			set extension to "upgarage"
		else if devID is 155 then
			set extension to "downgarage"
		else if devID is 253 then
			set extension to "slider"
		end if
		
		-- If we get a request for an unknown devID, then send it on to Asterisk 
		-- so we get an audible indication that *something* happened.  Since batteries
		-- falling out of DS10 modules can cause the ID to change, I'd rather not ignore
		-- these.  YMMV, however.
		if extension is "unknown" then
			my doDialOut(devID)
		else
			-- Indigo variable names are derived from Asterisk extension names.
			
			-- In retrospect, doorClosed_ is a great name for doors, but not so hot
			-- for doorbells or motion sensors.  Feel free to change this.
			set var to ("doorClosed_" & extension)
			set seen_var to ("lastSeen_" & extension)
			set change_var to ("lastChanged_" & extension)
			set timestamp to (current date) as string
			
			-- Figure out if it opened or closed.  There are actually 4 different
			-- results that the sensors can return (normal/active X min/max),
			-- but we only care about normal/active.
			if eventType is sec_SensorNormal_min then
				set val to "true"
			else if eventType is sec_SensorNormal_max then
				set val to "true"
			else
				set val to "false"
			end if
			
			-- Create the Indigo variables if they don't already exist.
			if not (variable var exists) then
				make new variable with properties {name:var, value:val}
			end if
			
			if not (variable seen_var exists) then
				make new variable with properties {name:seen_var, value:timestamp}
			end if
			
			if not (variable change_var exists) then
				make new variable with properties {name:change_var, value:timestamp}
			end if
			
			-- Did the value of this variable just change?
			if not (value of variable var is val) then
				set value of variable change_var to timestamp
				
				-- If it just changed and it's now false, then send an alert.
				-- The DS10a sensors send a signal once per hour, even if nothing's changed.
				-- So we don't want to alert *unless* something's changed.
				if val is "false" then
					my doDialOut(extension)
				end if
			end if
			
			-- We need to keep track of the time that each DS10 was last seen.
			-- This way we can spot bad batteries.
			set value of variable seen_var to timestamp
			set value of variable var to val
		end if
	end receive security event
	
	-- doDialOut tells Asterisk to dial a specific extension using
	-- Asterisk's management interface.  This doesn't require that Asterisk
	-- runs on the same machine as Indigo.
	--
	-- To get this to work in your environment, you'll need to change the IP
	-- address, username, and password, and possibly the contexts and/or 
	-- extension names used at the bottom.
	on doDialOut(extension)
		log "Paging extension " & extension using type "Dialer"
		set expectscript to "set timeout 20;
spawn telnet 10.0.0.1 5038;
expect \"Asterisk Call Manager/1.0\";
send \"Action: login
username: my_username
secret: secretsecret

\";
expect \"Message: Authentication accepted\";

send \"Action: originate
Exten: " & extension & "
Context: security-paging
Channel: Local/all@paging
Priority: 1
Callerid: Security: " & extension & " <0>

\";

sleep 1;

send \"Action: logoff

\";
"
		set results to do shell script "/usr/bin/expect -c  '" & expectscript & "'"
	end doDialOut
end using terms from

In addition to simply monitoring the current state of the sensors, this also tracks their last change as well as the last time each sensor sent out a “nothing’s changed” report. Eventually I’ll add monitoring for this so I can spot failing batteries immediately, and not two months later when I discover that two doors aren’t monitored anymore. Indigo tracks each variable internally, and gives you a handy window for viewing and modifying them:

Once all of that was in place, it was time to add paging logic to Asterisk. I created two new contexts, security-paging to contain the messages that need to be played back and paging to handle the phones. The AppleScript above basically glues the two ends together, sending ‘deckdoor@security-paging’ to ‘all@paging’. Here’s the relevant bit of my Asterisk config:

[security-paging]
  exten => frontdoor,1,Playback(security/frontdoor)
  exten => deckdoor,1,Playback(security/deckdoor)
  exten => backyard,1,Playback(security/backyard)
  exten => stairs,1,Playback(security/stairs)
  exten => upgarage,1,Playback(security/upgarage)
  exten => downgarage,1,Playback(security/downgarage)
  exten => slider,1,Playback(security/slider)

  exten => _.,1,SayNumber(${EXTEN})

[paging]
  exten => all,1,Page(Local/203@paging&Local/204@paging&Local/205@paging&Local/206@paging)

  ; Cisco 7940; no Call-Info support, but you can create a new line and turn on auto-answer on the phone.
  exten => 203,1,Dial(SIP/203aa)

  ; Sipura 841.  It needs a semicolon before answer-after.
  exten => 204,1,SipAddHeader(Call-Info: \;answer-after=0)
  exten => 204,n,Dial(SIP/204)

  ; Grandstream GXP-2020s, although the same config will work for most modern phones.
  exten => 205,1,SipAddHeader(Call-Info: answer-after=0)
  exten => 205,n,Dial(SIP/205)

  exten => 206,1,SipAddHeader(Call-Info: answer-after=0)
  exten => 206,n,Dial(SIP/206)

Then I just had to record some audio files to use for annoucements. The easiest way to do this is just to call yourself up and leave voicemail, and then copy the VM files over into Asterisk’s sound file directory, usually /var/lib/asterisk/sounds. You could get better quality recordings with a good microphone and audio-processing app, but I’m not sure that there’s really a point, given the quality of most speakerphone speakers.

All told, it took me about 8 hours of research to put this all together, and maybe 10 hours to implement it all, including learning a bit of AppleScript. Now I have a programmable system for monitoring my house, and all of the pieces in place for adding X10 or Insteon components as they make sense. It’s all under my control; if I can code it, then I can make it happen.

So, does it all work? Yep–Monday morning it caught my daughter trying to sneak into the garage. Mission accomplished.