netgrowl.py

netgrowl.py is my implementation of the Growl 0.6 UDP network protocol, enabling me to send notifications from my Linux boxes to my Mac. The protocol has been kept stable up to version 1.0, so the source code posted here works for that too.

Requirements

Any Python above 2.4 should do (although it originally worked in 2.2). The module has been tested on the Mac (you don't have to have Growl or PyObjC installed to send notifications), Linux and Windows.

To receive notifications, you have to have Growl 0.6 or above installed (which you can get directly from their site or bundled with apps such as Adium).

Usage

The module does not send notifications by itself - it simply helps you format the packets, which are easy to send using trivial socket() calls.

Growl requires you to register the notifications your application sends (and set whether or not they're enabled on the GUI) before being able to actually send something to your Mac, so make sure you have "Allow application registration" enabled on Growl's preference pane. And, of course, make sure you set a password.

So you register your application first:

addr = ("192.168.0.42", GROWL_UDP_PORT)
s = socket(AF_INET,SOCK_DGRAM)
p = GrowlRegistrationPacket(application="Network Demo", password="secret")
p.addNotification("Stuff I Want To Know", enabled=True)
p.addNotification("Stuff I Might Not Want To Know") # But Can Enable In The GUI
s.sendto(p.payload(), addr)

...and then you send an actual notification:

p = GrowlNotificationPacket(application="Network Demo",
    notification="Stuff I Want To Know", title="The Knights Who Say Ni",
    description="We Want... a Shrubbery!!!", priority=1,
    sticky=True, password="secret")
s.sendto(p.payload(),addr)
s.close()

Of course, you must know where to send notifications to, which is another issue altogether. Growl servers currently announce themselves via Rendezvous as _growl._tcp., but that is likely to change since only the UDP port is currently being used (that I know of).

One of my next tricks was to implement a "follow me" feature on my home LAN - by re-broadcasting the same message to all machines that announce they're running Growl.

Network Protocol Format

The 0.6 protocol format is fairly straightforward, and the registration and notification packets look like this:

You can get this diagram in OpenOffice format here

The flags field contains a signed 3-bit value (-2 to 2) and a sticky flag in the lowest (rightmost) nibble (check the code below to figure it out).

The Source

Without further ado, here it is:

#!/usr/bin/env python

"""Growl 0.6 Network Protocol Client for Python"""
__version__ = "0.6.3"
__author__ = "Rui Carmo (http://the.taoofmac.com)"
__copyright__ = "(C) 2004 Rui Carmo. Code under BSD License."
__contributors__ = "Ingmar J Stein (Growl Team), John Morrissey (hashlib patch)"

try:
  import hashlib
  md5_constructor = hashlib.md5
except ImportError:
  import md5
  md5_constructor = md5.new

import struct
from socket import AF_INET, SOCK_DGRAM, socket

GROWL_UDP_PORT=9887
GROWL_PROTOCOL_VERSION=1
GROWL_TYPE_REGISTRATION=0
GROWL_TYPE_NOTIFICATION=1

class GrowlRegistrationPacket:
  """Builds a Growl Network Registration packet.
     Defaults to emulating the command-line growlnotify utility."""

  def __init__(self, application="growlnotify", password = None ):
    self.notifications = []
    self.defaults = [] # array of indexes into notifications
    self.application = application.encode("utf-8")
    self.password = password
  # end def


  def addNotification(self, notification="Command-Line Growl Notification", enabled=True):
    """Adds a notification type and sets whether it is enabled on the GUI"""
    self.notifications.append(notification)
    if enabled:
      self.defaults.append(len(self.notifications)-1)
  # end def


  def payload(self):
    """Returns the packet payload."""
    self.data = struct.pack( "!BBH",
                             GROWL_PROTOCOL_VERSION,
                             GROWL_TYPE_REGISTRATION,
                             len(self.application) )
    self.data += struct.pack( "BB",
                              len(self.notifications),
                              len(self.defaults) )
    self.data += self.application
    for notification in self.notifications:
      encoded = notification.encode("utf-8")
      self.data += struct.pack("!H", len(encoded))
      self.data += encoded
    for default in self.defaults:
      self.data += struct.pack("B", default)
    self.checksum = md5_constructor()
    self.checksum.update(self.data)
    if self.password:
       self.checksum.update(self.password)
    self.data += self.checksum.digest()
    return self.data
  # end def
# end class


class GrowlNotificationPacket:
  """Builds a Growl Network Notification packet.
     Defaults to emulating the command-line growlnotify utility."""

  def __init__(self, application="growlnotify",
               notification="Command-Line Growl Notification", title="Title",
               description="Description", priority = 0, sticky = False, password = None ):
    self.application  = application.encode("utf-8")
    self.notification = notification.encode("utf-8")
    self.title        = title.encode("utf-8")
    self.description  = description.encode("utf-8")
    flags = (priority & 0x07) * 2
    if priority < 0:
      flags |= 0x08
    if sticky:
      flags = flags | 0x0100
    self.data = struct.pack( "!BBHHHHH",
                             GROWL_PROTOCOL_VERSION,
                             GROWL_TYPE_NOTIFICATION,
                             flags,
                             len(self.notification),
                             len(self.title),
                             len(self.description),
                             len(self.application) )
    self.data += self.notification
    self.data += self.title
    self.data += self.description
    self.data += self.application
    self.checksum = md5_constructor()
    self.checksum.update(self.data)
    if password:
       self.checksum.update(password)
    self.data += self.checksum.digest()
  # end def

  def payload(self):
    """Returns the packet payload."""
    return self.data
  # end def
# end class


if __name__ == '__main__':
  print "Starting Unit Test"
  print " - please make sure Growl is listening for network notifications"
  addr = ("localhost", GROWL_UDP_PORT)
  s = socket(AF_INET,SOCK_DGRAM)
  print "Assembling registration packet like growlnotify's (no password)"
  p = GrowlRegistrationPacket()
  p.addNotification()
  print "Sending registration packet"
  s.sendto(p.payload(), addr)

  print "Assembling standard notification packet"
  p = GrowlNotificationPacket()
  print "Sending standard notification packet"
  s.sendto(p.payload(), addr)

  print "Assembling priority -2 (Very Low) notification packet"
  p = GrowlNotificationPacket(priority=-2)
  print "Sending priority -2 notification packet"
  s.sendto(p.payload(), addr)

  print "Assembling priority 2 (Very High) sticky notification packet"
  p = GrowlNotificationPacket(priority=2,sticky=True)
  print "Sending priority 2 (Very High) sticky notification packet"
  s.sendto(p.payload(), addr)
  s.close()
  print "Done."

Download

Ports/Equivalents for other languages:

People Using This:

There are a whole lot of people using this out there in several implementations of Python, including embedded Python interpreters. Although I don't make a point of keeping track of all of them, some are pretty interesting, and show the value of having a fully cross-platform implementation of the network protocol: