"Ultimate" Arduino Doorbell - part 2 (Software)


As mentioned in the previous post about my arduino doorbell I wanted to get the doorbell and my Chumby talking.

As the Chumby runs Linux, is on the network and is able to run a recent version of Python (2.6) it seemed like it would be pretty easy to get it to send Growl notifications to the local network.

image1

This did indeed prove fairly easy and involved three main activities:

  • Listening to the serial port for the doorbell to ring
  • Using Bonjour (formally Rendezvous)/Zeroconf to find computers on the network
  • Sending network Growl notifications to those computers

Getting Python and PySerial running on the Chumby is pretty easy. The Chumby simply listens for the doorbell to send the string 'DING DONG' and can then react as needed.

def listen_on_serial_port(port, network):
    ser = serial.Serial(port, 9600, timeout=1)
    try:
        while True:
            line = ser.readline()
            if line is not None:
                line = line.strip()
            if line == 'DING DONG':
                network.send_notification()
    finally:
        if ser:
            ser.close()

port is the string representing the serial port (e.g. /dev/ttyUSB0). network is an object that handles the network notifications (see below).

The network object (an instance of Network) has two jobs. Firstly to find computers on the network (using PicoRendezvous - now called PicoBonjour) and secondly to send network growl notifications to those computers.

A background thread periodically calls Network.find, which uses multicast DNS (Bonjour/Zeroconf) to find the IP addresses of computers to notify:

class Network(object):
    title = 'Ding-Dong'
    description = 'Someone is at the door'
    password = None

    def __init__(self):
        self.growl_ips = []
        self.gntp_ips = []

    def _rendezvous(self, service):
        pr = PicoRendezvous()
        pr.replies = []
        return pr.query(service)

    def find(self):
        self.growl_ips = self._rendezvous('_growl._tcp.local.')
        self.gntp_ips  = self._rendezvous('_gntp._tcp.local.')

    def start(self):
        import threading
        t = threading.Thread(target=self._run)
        t.setDaemon(True)
        t.start()

    def _run(self):
        while True:
            self.find()
            time.sleep(30.0)

_growl._tcp.local. are computers that can handle Growl UDP packets and _gntp._tcp.local. those that can handle the newer GNTP protocol. Currently the former will be Mac OS X computers (running Growl) and the latter Windows computers (running Growl for Windows).

I had to tweak PicoRendezvous slightly to work round a bug on the Chumby version of Python, where socket.gethostname was returning the string '(None)', but otherwise this worked ok.

When the doorbell activates netgrowl is used to send Growl UDP packets to the IP addresses in growl_ips and the Python gntp library to notify those IP addresses in gntp_ips (but not those duplicated in growl_ips). For some reason my Macbook was appearing in both lists of IP addresses, so I made sure that the growl_ips took precedence.

def send_growl_notification(self):
    growl_ips = self.growl_ips

    reg = GrowlRegistrationPacket(password=self.password)
    reg.addNotification()

    notify = GrowlNotificationPacket(title=self.title,
                description=self.description,
                sticky=True, password=self.password)
    for ip in growl_ips:
        addr = (ip, GROWL_UDP_PORT)
        s = socket(AF_INET, SOCK_DGRAM)
        s.sendto(reg.payload(), addr)
        s.sendto(notify.payload(), addr)

def send_gntp_notification(self):
    growl_ips = self.growl_ips
    gntp_ips  = self.gntp_ips

    # don't send to gntp if we can use growl
    gntp_ips = [ip for ip in gntp_ips if (ip not in growl_ips)]

    for ip in gntp_ips:
        growl = GrowlNotifier(
            applicationName = 'Doorbell',
            notifications = ['doorbell'],
            defaultNotifications = ['doorbell'],
            hostname = ip,
            password = self.password,
        )
        result = growl.register()
        if not result:
            continue
        result = growl.notify(
            noteType = 'doorbell',
            title = self.title,
            description = self.description,
            sticky = True,
        )

def send_notification(self):
    '''
    send notification over the network
    '''
    self.send_growl_notification()
    self.send_gntp_notification()

The code is pretty simple and will need some more tweaking. The notification has failed for some reason a couple of times, but does usually seem to work. I'll need to start logging the doorbell activity to see what's going on, but I suspect the Bonjour/Zeroconf is sometimes finding no computers. If so I'll want to keep the previous found addresses hanging around for a little longer - just in case it's a temporary glitch.

You can see the full code in my doorbell repository.