November 17, 2013

iPhone Presence Detection with Bonjour Multicast DNS using python

An example python Bonjour script was modified to listen for Apple's iOS '_apple-mobdev2._tcp' Bonjour service. This is used in the Perceptive Automation Indigo Home Control System to track the presence of iPhones (smartphones) and hence presence of house occupants. The script runs on the same Macintosh as the Indigo server but can also be run on Linux systems and report iPhone status back to an Indigo server. It has been sucessfully tested on a Raspberry Pi in another city, reporting iPhone presence back to the Indigo server. So far it is all working well after a couple months of testing and a few weeks of use.

The only shortcoming observed is that it takes 30 minutes to be sure the iPhone is gone. Usually the Bonjour advertisements occur ever few seconds or minutes but sometimes they go as long as 25 minutes.

A few times an iPhone has stopped advertising the Bonjour service. A reboot of the iPhone usually remedies this. The '_apple-mobdev2._tcp' service is used by iTunes for Wi-Fi sync. The iPhone will not advertise this service unless iTunes has enabled Wi-Fi sync for that device. When the iPhone has stopped advertising, this impacts iTunes Wi-Fi sync also. Worst case, the Wi-Fi sync option in iTunes has to be cycled to restart the advertisements.

The program is based on and uses pybonjour, the Pure-Python interface to Apple Bonjour and compatible DNS-SD libraries available at googlecode. Follow the documentation for setup. If you are putting it on Linux, you will need to install the DNS-SD libraries per the documentation. The DNS-SD libraries built with no problem on a Raspberry Pi running debian.

The customized scripts were cobbled together without any programming elegance. It is the author's first attempt at python so be gentle. They need extreme improvements but they work.

There is a configuration file to define the Indigo variables and the specific iPhone names. The config file and text are included below. Feel free to use them as you wish with no warranty. The files are configured assuming the config file goes in /usr/local/etc and the executable goes in /usr/local/bin. A launchd plist can be configured to run the script at boot on a Mac. Redirect STDOUT to /dev/null, its quite noisy. It can also be tested from the command line and the output shows each detection of the service.

This seems to be a good way to determine presence and would be even better if an iPhone app advertised a specific service on a defined frequency of about 5 minutes. An iOS app could start the advertisements when the device attached to a Wi-Fi network.

Another improvement would be to link directly to the Indigo variables instead of using the RESTful method. Although the RESTful method should be an option for remote status reports.

The Configuration File


# radar.conf
# radar.py configuration

# Indigo host and authentication
Host = "http://localhost:8176/variables/"
Realm    = "Indigo Control Server"
Username = 'username'
Password = 'password'

# search string from device name and Indigo variable name
# search string is from the device Settings->General->About->Name
# define four devices even if you don't have that many
iOSone='myPhone', 'bonjourTimeOne'
iOStwo='herPhone', 'bonjourTimeTwo' 
iOSthree='thatPhone', 'bonjourTimeThree' 
iOSfour='anotherPhone','bonjourTimeFour' 

# Bonjour service name and timeout
regtype = '_apple-mobdev2._tcp'
timeout  = 5


Program Code


#!/usr/bin/python

# Sat Oct 19 14:42:08 EDT 2013
# based on https://code.google.com/p/pybonjour/

import select
import sys
import pybonjour
import datetime
import urllib2

timeout  = 5
resolved = []
config = {}
file_name = "/usr/local/etc/radar.conf"

########## read configuration 
config_file= open(file_name)
 
for line in config_file:
    line = line.strip()
    if line and line[0] is not "#" and line[-1] is not "=":
        var,val = line.rsplit("=",1)
# print "val is ",val
        config[var.strip()] = val.strip()
 
############## set variables
# Indigo host and authentication
Host = eval(config["Host"])
Realm = eval(config["Realm"])
Username = eval(config["Username"])
Password = eval(config["Password"])

# search string from device name and Indigo variable name
iOSone = eval(config["iOSone"])
iOStwo = eval(config["iOStwo"])
iOSthree = eval(config["iOSthree"])
iOSfour = eval(config["iOSfour"])

regtype = eval(config["regtype"])

################ functions
def toIndigo(varNameNow, justName, today):

 print 'updating ', justName[0]
 print
 URL = Host + varNameNow + '?_method=put&value=' + today
 authhandler = urllib2.HTTPDigestAuthHandler()
 authhandler.add_password(Realm, URL, Username, Password)
 opener = urllib2.build_opener(authhandler)
 urllib2.install_opener(opener)
 page_content = urllib2.urlopen(URL)

def resolve_callback(sdRef, flags, interfaceIndex, errorCode, fullname,
                     hosttarget, port, txtRecord):
    if errorCode == pybonjour.kDNSServiceErr_NoError:
 now = datetime.datetime.now()
 print str(now)
        print 'Resolved service:'
        print '  fullname   =', fullname
        print '  hosttarget =', hosttarget
        print '  port       =', port
        resolved.append(True)
 justName = hosttarget.split(".", 1)

 if iOSone[0] in justName[0]:
  varNameNow = iOSone[1]
  today = datetime.datetime.now().strftime("%Y-%m-%d-%H:%M:%S")
  toIndigo(varNameNow, justName, today)

 if iOStwo[0] in justName[0]:
  varNameNow = iOStwo[1]
  today = datetime.datetime.now().strftime("%Y-%m-%d-%H:%M:%S")
  toIndigo(varNameNow, justName, today)

 if iOSthree[0] in justName[0]:
  varNameNow = iOSthree[1]
  today = datetime.datetime.now().strftime("%Y-%m-%d-%H:%M:%S")
  toIndigo(varNameNow, justName, today)

 if iOSfour[0] in justName[0]:
  varNameNow = iOSfour[1]
  today = datetime.datetime.now().strftime("%Y-%m-%d-%H:%M:%S")
  toIndigo(varNameNow, justName, today)


def browse_callback(sdRef, flags, interfaceIndex, errorCode, serviceName,
                    regtype, replyDomain):
    if errorCode != pybonjour.kDNSServiceErr_NoError:
        return

    if not (flags & pybonjour.kDNSServiceFlagsAdd):
 now = datetime.datetime.now()
 print str(now)
        print 'Service removed'
        print '  serviceName =', serviceName
        print '  regtype     =', regtype
        return

    print 'Service added; resolving'

    resolve_sdRef = pybonjour.DNSServiceResolve(0,
                                                interfaceIndex,
                                                serviceName,
                                                regtype,
                                                replyDomain,
                                                resolve_callback)

    try:
        while not resolved:
            ready = select.select([resolve_sdRef], [], [], timeout)
            if resolve_sdRef not in ready[0]:
                print 'Resolve timed out'
                break
            pybonjour.DNSServiceProcessResult(resolve_sdRef)
        else:
            resolved.pop()
    finally:
        resolve_sdRef.close()


browse_sdRef = pybonjour.DNSServiceBrowse(regtype = regtype,
                                          callBack = browse_callback)

try:
    try:
        while True:
            ready = select.select([browse_sdRef], [], [])
            if browse_sdRef in ready[0]:
                pybonjour.DNSServiceProcessResult(browse_sdRef)
    except KeyboardInterrupt:
        pass
finally:
    browse_sdRef.close()


No comments:

Post a Comment