I have been working for a long time to make lights in my house follow me around with accurate proximity detection. I always have my iPhone on me, so I’ve been using Bluetooth to handle this. Originally, I hacked around with Proximity to accomplish this, but ultimately found EventScripts to be a better solution. It still wasn’t as accurate as I wanted it to be.
I started playing with iBeacons. iBeacons use Bluetooth Low Energy (BTLE) and can provide more precise Bluetooth proximity detection than any other method I’ve tried. It’s also easier on mobile device batteries than Bluetooth, which is relevant in this case because a setup like this requires polling and broadcasting on the mobile device.
The technique I’m using can be set up on any web host, depending on what actions you want to trigger. My scenario requires a local web host, so the examples provided will only be of interest to Mac users who have the knowledge (or search engine skills) to set up a local server. It’s also tailored to AppleScript and Indigo, but you can use this method to perform any actions you want on a computer when your phone comes into range.
Choosing your weapon
I’ve been through many iterations at this point. I’m using Bleu Station beacons for the most part, but I learned you can also set up a BTLE-equipped Mac as a transmitting beacon with little effort. I’ve gotten far enough to have written my own prototype apps for triggering my lights, but the most constructive solution I’ve found so far is to use the GeoHopper app.
With both the Bleu beacons and the BeaconOSX method, you can set the power of the signal to control the range of the beacon (within a margin of error1). With the Bleu beacons, you use the iOS setup app to control the power, and with a homebrew Mac-beacon, you can set the transmit power in the code.
Triggering scripts
The trick is to get events to trigger when a device enters the beacon’s region. I’m using GeoHopper on my iPhone to trigger events when I come into or leave the region of a beacon. There’s a GeoHopper Mac app that can trigger scripts when devices enter and exit, but I’ve found it a bit “crashy.” The iOS app has been quite reliable, though.
You can add iBeacons to GeoHopper on your iPhone (also see the url scheme), and when the device running it enters or exits a beacon’s region, it can send JSON payloads to webhooks. This can be used with services like IFTTT, or — as I’ve done — you can set up your own CGI for it.
Writing the CGI
This requires a local web server. I’m not going to go into details on how to set that up, but I highly recommend MAMP Pro as an easy way to handle virtual hosts and settings. I set mine up to handle Ruby scripts as CGIs and built out a handler for the JSON messages.
To make a virtual host under Apache run Ruby scripts, make sure ExecCGI is enabled and add this to the virtual host directory settings:
Now you can make any Ruby script executable and have it use the ‘cgi’ library to handle tasks without building out an entire API. Of course, all of this can be done with PHP or whatever language your preferred platform supports.
I built my script to handle query strings first, so I can ping an address such as “http://myserver.com?a=lightson” directly from any web browser and turn lights on. Then I added a handler for the GeoHopper webhook.
My script just checks to make sure the sending device is authorized, then takes the event — “LocationEnter” or “LocationExit” — and runs AppleScripts (using osascript) for Indigo based on which event occurred. It currently only handles my default location (“office”), but the “location” key can provide a pivot for setting up different scripts to execute for multiple locations from the same web host.
Example webhook
Since anyone who wants to set this up will have to customize the script to some extent, I’m just providing my version below as an example.
#!/usr/bin/env ruby# Requires json: `sudo gem install json`require'cgi'require'rubygems'require'json'# CONFIG# The email address or domain from which GeoHopper events will be acceptedgeohopper_email="brettterpstra.com"# END CONFIG# hash to hold JSON responsesresult={}cgi=CGI.newprintcgi.header('type'=>'application/json','expires'=>Time.now-(180))# Hash of query parameter keysp=cgi.keys[0]# if the query comes from GeoHopper, this is the JSON payload# Otherwise it uses the "a" parameter to define the actionifp.nil?# No query params or payloadprint"No action specified"Process.exitendresult["request"]=pscript=""### MANUAL QUERY STRING# if the key "a" exists in the query string, check# its value for an available actionifp=="a"# create a 'when' statement for each available action# script:# The action to run. By default, these are the names# of Indigo Actions to run# result["action"]:# status message for the JSON responsecasecgi.params['a'][0]when"officelightsoff"script="All Office Lights Off"result["action"]="Turning office lights OFF"when"officelightson"script="All Office Lights On"result["action"]="Turning office lights ON"elseresult["action"]="No action recognized"end### GEOHOPPER# if there's no "a" key in the query string, assume# JSON payload from GeoHopperelsebegin# Parse the first key as JSONjson=JSON.parse(p)result["valid"]=truerescue# if we can't parse the key as JSON, return an errorresult["valid"]=falseresult["result"]="Invalid JSON"printresult.to_jsonProcess.exitend# GeoHopper device's registered email (or just domain)ifjson["sender"]=~/#{geohopper_email}$/# script:# The action to run. By default, these are the names# of Indigo Actions to run# result["action"]:# status message for the JSON response## Action to take on exit eventifjson["event"]=="LocationExit"script="All Office Lights Off"result["action"]="Turning office lights OFF"## Action to take on enter eventelsifjson["event"]=="LocationEnter"script="All Office Lights On"result["action"]="Turning office lights ON"elseresult["action"]="No action recognized"endendend# Run applescript and store any response in "result" key# If osascript exits cleanly, the result will be empty# If the script variable has not been defined by an event# above, this is bypassedresult["result"]=%x{osascript -e 'tell application "IndigoServer.app" to «event INDOExeG» "#{script}"'}unlessscript==""# return the response object as JSONprintresult.to_json
I’m not providing support for this, but if you know enough about web servers and Ruby to get started and run into specific problems, feel free to drop me a line.
proximity is determined by a ratio of transmit and reception strength, which can fluctuate significantly. ↩