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.2 should do. 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" # will always match Growl version __author__ = "Rui Carmo (http://the.taoofmac.com)" __copyright__ = "(C) 2004 Rui Carmo. Code under BSD License." __contributors__ = "Ingmar J Stein (Growl Team)" import struct import md5 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.new() 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 | 0x0001 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.new() 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."
Ports/Equivalents for other languages:
- A C# class library by Brian Dunnington
- a PHP/ version that can be used for sending notifications straight from a vanilla PHP/Apache installation
- a Perl module by
Nathan McFarland.
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:
- Mumbles is using this for network notifications, and even has a graphical UI for it (see the screenshots).
- Trey Harrel is using this to get notifications for Maya rendering jobs from Windows and Linux machines.