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."
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.