Index: /trunk/LocationDo/English.lproj/MainMenu.nib/classes.nib
===================================================================
--- /trunk/LocationDo/English.lproj/MainMenu.nib/classes.nib	(revision 196)
+++ /trunk/LocationDo/English.lproj/MainMenu.nib/classes.nib	(revision 196)
@@ -0,0 +1,4 @@
+{
+    IBClasses = ({CLASS = FirstResponder; LANGUAGE = ObjC; SUPERCLASS = NSObject; }); 
+    IBVersion = 1; 
+}
Index: /trunk/LocationDo/English.lproj/MainMenu.nib/info.nib
===================================================================
--- /trunk/LocationDo/English.lproj/MainMenu.nib/info.nib	(revision 196)
+++ /trunk/LocationDo/English.lproj/MainMenu.nib/info.nib	(revision 196)
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>IBDocumentLocation</key>
+	<string>69 10 356 240 0 0 1024 746 </string>
+	<key>IBFramework Version</key>
+	<string>439.0</string>
+	<key>IBSystem Version</key>
+	<string>8B15</string>
+</dict>
+</plist>
Index: /trunk/LocationDo/LocationDo.py
===================================================================
--- /trunk/LocationDo/LocationDo.py	(revision 196)
+++ /trunk/LocationDo/LocationDo.py	(revision 196)
@@ -0,0 +1,201 @@
+#!/usr/bin/pythonw
+# -*- coding: utf-8 -*-
+
+from AppKit import NSApplication, NSBeep, NSSystemDefined, NSURL, NSWorkspace
+from Foundation import NSDistributedNotificationCenter, NSLog
+from PyObjCTools import AppHelper
+from Carbon.CarbonEvt import RegisterEventHotKey, GetApplicationEventTarget
+from SystemConfiguration import *
+from appscript import app
+import re
+import sets
+
+import action
+
+GROWL_APP_NAME = 'LocationDo'
+NOTIFICATION_LOCATION = 'Location'
+NOTIFICATION_ARRIVE = 'Arrive'
+NOTIFICATION_DEPART = 'Depart'
+NOTIFICATION_CURRENT = 'Current location'
+NOTIFICATION_UNKNOWN = 'Unknown location'
+NOTIFICATION_READY = 'Ready'
+NOTIFICATIONS_ALL = [NOTIFICATION_LOCATION, NOTIFICATION_ARRIVE,
+                     NOTIFICATION_DEPART, NOTIFICATION_CURRENT,
+                     NOTIFICATION_UNKNOWN, NOTIFICATION_READY]
+
+DEBUG = True
+
+kEventHotKeyPressedSubtype = 6
+kEventHotKeyReleasedSubtype = 9
+
+RE_AIRPORT_STATUS = '/'.join([kSCDynamicStoreDomainState, kSCCompNetwork, kSCCompInterface, kSCCompAnyRegex, kSCEntNetAirPort])
+
+growl = app('GrowlHelperApp')
+
+growl.register(
+    as_application=GROWL_APP_NAME,
+    all_notifications=NOTIFICATIONS_ALL,
+    default_notifications=NOTIFICATIONS_ALL)
+
+def growlNotify(name, title, description, **kw):
+    params = dict(with_name=name,
+                  title=title,
+                  description=unicode(description),
+                  application_name=GROWL_APP_NAME,
+                  image_from_location='/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/GenericNetworkIcon.icns')
+    params.update(kw)
+    if not params.get('sticky', False):
+        params['identifier'] = name
+    if DEBUG:
+        NSLog("%s: %s" % (title, description))
+    growl.notify(**params)
+
+class Location(sets.Set):
+    def __str__(self):
+        if not self: return '(unknown)'
+        return ', '.join(self)
+
+currentLocation = Location()
+
+def arriveAtLocation(location):
+    growlNotify(NOTIFICATION_ARRIVE, 'Arriving at location', location)
+    if 'Siebel' in location:
+        action.ensureKerberosPrincipalsValid(['njriley@ACM.UIUC.EDU',
+                                              'njriley@AD.UIUC.EDU'])
+        action.setVolumePercent(0.15)
+    if '1115 SC' in location:
+        action.openInBackground('~/Documents/CS 423/CS 423 to do.oo3')
+        action.setDisplayBrightnessPercent(0.2)
+        action.setAdiumStatus('Working in 1115 SC')
+    if '4111 SC' in location:
+        action.openInBackground('~/Documents/MSSP/MSSP progress.oo3')
+        action.setDisplayBrightnessPercent(0.9)
+        action.setAdiumStatus('Working in 4111 SC')
+    if 'ACM office' in location:
+        action.setDisplayBrightnessPercent(0.2)
+        action.setAdiumStatus('At ACM')
+    if 'Champaign' in location:
+        action.openInBackground('~/Documents/To do.oo3')
+        action.setDisplayBrightnessPercent(0.2)
+        action.setVolumePercent(0.5)
+        action.setAdiumStatus('At home')
+    if '1320 DCL' in location:
+        action.setDisplayBrightnessPercent(0.2)
+        action.setVolumePercent(0)
+        action.setAdiumStatus('In 1320 DCL')
+    if 'UIUCnet' in location:
+        action.startVPNC('UIUCnet-oncampus')
+    if 'CSL' in location:
+        action.setDisplayBrightnessPercent(0.1)
+        action.setVolumePercent(0)
+        action.setAdiumStatus('At CSL')
+        action.openURL('http://wireless.csl.uiuc.edu/authenticate.html')
+    growlNotify(NOTIFICATION_ARRIVE, 'Arrived at location',
+                location)
+
+def departLocation(location):
+    growlNotify(NOTIFICATION_DEPART, 'Departing location', location)
+    if 'UIUCnet' in location:
+        # XXX should handle this more symmetrically
+        action.stopVPNC()
+    growlNotify(NOTIFICATION_DEPART, 'Departed location', location)
+
+def locationChanged(location):
+    if not location: return # unknown
+    global currentLocation
+    oldLocation = currentLocation - location
+    newLocation = location - currentLocation
+    if DEBUG:
+        NSLog("new %s | was %s | departing %s | arriving %s" % \
+              (location, currentLocation, oldLocation, newLocation))
+    if oldLocation:
+        departLocation(oldLocation)
+        currentLocation = location
+    if newLocation:
+        currentLocation = location
+        arriveAtLocation(newLocation)
+
+def formatMACAddress(data):
+    return ':'.join(re.findall('..', data.bytes()[:].encode('hex')))
+
+# XXX watch battery status, too, for display brightness
+
+airPortStatus = {'BSSID': None, 'SSID': None}
+
+class SCWatcher(NSObject):
+    def keysChanged_inDynamicStore_(self, changed, store):
+        location = Location()
+        for key in changed:
+            if re.match(RE_AIRPORT_STATUS, key):
+                st = store.valueForKey_(key)
+                bssid = formatMACAddress(st['BSSID'])
+                if airPortStatus['BSSID'] != bssid:
+                    airPortStatus['BSSID'] = bssid
+                    if bssid in ['00:0e:83:05:77:22', '00:0e:83:05:77:20',
+                                 '00:0e:83:05:75:d2']:
+                        location.add('4111 SC')
+                    elif bssid in ['00:0e:83:05:76:82', '00:0e:83:05:76:80']:
+                        location.add('ACM office')
+                    elif bssid == '00:11:24:0f:23:69':
+                        location.add('Champaign')
+                    elif bssid == '00:13:c4:ce:66:a0':
+                        location.add('1320 DCL')
+                    elif bssid in ['00:14:1c:ad:5c:80', '00:14:1c:ad:64:50']:
+                        location.add('MEB')
+                    elif bssid == '00:02:2d:2c:cb:34':
+                        location.add('B02 CSL')
+                    elif bssid == '44:44:44:44:44:44':
+                        location.add('No AirPort network')
+                    else:
+                        growlNotify(NOTIFICATION_UNKNOWN, 'Unknown BSSID', bssid,
+                                    sticky=True)
+                        NSLog('Unknown BSSID %s' % bssid)
+                ssid = st['SSID']
+                if airPortStatus['SSID'] != ssid:
+                    airPortStatus['SSID'] = ssid
+                    if ssid == '':
+                        pass
+                    elif ssid == 'dcs-wpa':
+                        location.add('Siebel')
+                    elif ssid == 'DCSnet':
+                        location.add('Siebel')
+                        location.add('UIUCnet')
+                    elif ssid == 'UIUCnet':
+                        location.add('UIUCnet')
+                    elif ssid == 'ACME Acres':
+                        location.add('Champaign')
+                    elif ssid == 'CSL Wireless':
+                        location.add('CSL')
+                    else:
+                        growlNotify(NOTIFICATION_UNKNOWN, 'Unknown SSID', ssid,
+                                    sticky=True)
+                locationChanged(location)
+
+class LocationDo(NSApplication):
+
+    hotKeyRef = None
+
+    def displayLocation(self):
+        growlNotify(NOTIFICATION_CURRENT, 'Current location', currentLocation)
+
+    def finishLaunching(self):
+        super(LocationDo, self).finishLaunching()
+        hotKeyRef = RegisterEventHotKey(98, 0, (0, 0),
+                                        GetApplicationEventTarget(), 0) # F7
+        growlNotify(NOTIFICATION_READY, 'LocationDo ready', '')
+
+    def sendEvent_(self, theEvent):
+        if theEvent.type() == NSSystemDefined and \
+               theEvent.subtype() == kEventHotKeyPressedSubtype:
+            self.displayLocation()
+        super(LocationDo, self).sendEvent_(theEvent)
+
+store = None
+
+if __name__ == "__main__":
+    store = UNSystemConfigurationDynamicStore.alloc().initWithName_('LocationDo')
+    watcher = SCWatcher.alloc().init()
+    store.setDelegate_(watcher)
+    store.notifyValuesForKeys_matchingPatterns_(None, [RE_AIRPORT_STATUS])
+    store.addToCurrentRunLoop()
+    AppHelper.runEventLoop()
Index: /trunk/LocationDo/SCNetworkReachabilitymodule.c
===================================================================
--- /trunk/LocationDo/SCNetworkReachabilitymodule.c	(revision 196)
+++ /trunk/LocationDo/SCNetworkReachabilitymodule.c	(revision 196)
@@ -0,0 +1,83 @@
+#include "Python.h"
+
+#include <SystemConfiguration/SystemConfiguration.h>
+
+#define SCNR_REACHABLE "SCNetworkIsReachable"
+static CFStringRef SCNR_Reachable = CFSTR(SCNR_REACHABLE);
+static SCNetworkReachabilityContext SCNR_Context = {0, NULL, NULL, NULL, NULL};
+
+static void
+SCNR_callback(SCNetworkReachabilityRef target,
+	      SCNetworkConnectionFlags flags, void *tagString);
+
+static Boolean
+SCNR_reachable(SCNetworkConnectionFlags flags) {
+  /* not reachable if it requires a non-automatic connection */
+  return ((flags & kSCNetworkFlagsReachable) &&
+	  (!(flags & kSCNetworkFlagsConnectionRequired) ||
+	   (flags & kSCNetworkFlagsConnectionAutomatic)));
+}
+
+static void
+SCNR_schedule(CFStringRef tagString,
+	      SCNetworkReachabilityRef target) {
+  SCNR_Context.info = (void *)tagString;
+  SCNetworkReachabilitySetCallback(target, SCNR_callback, &SCNR_Context);
+  SCNetworkReachabilityScheduleWithRunLoop(target,
+					   CFRunLoopGetCurrent(),
+					   kCFRunLoopDefaultMode);
+}
+
+static void
+SCNR_callback(SCNetworkReachabilityRef target,
+	      SCNetworkConnectionFlags flags, void *tagString) {
+  if (SCNR_reachable(flags)) {
+    CFNotificationCenterPostNotification(CFNotificationCenterGetLocalCenter(),
+					 SCNR_Reachable, tagString, NULL, false);
+    CFRelease((CFStringRef)tagString);
+  } else {
+    SCNR_schedule(tagString, target);
+  }
+}
+
+static PyObject *
+SCNR_notify_when_reachable(PyObject *self, PyObject *args) {
+  const char *hostname;
+  const char *tag;
+  if (!PyArg_ParseTuple(args, "ss", &hostname, &tag))
+    return NULL;
+
+  SCNetworkConnectionFlags flags;
+  if (SCNetworkCheckReachabilityByName(hostname, &flags) &&
+      SCNR_reachable(flags))
+    return PyBool_FromLong(0);
+
+  SCNetworkReachabilityRef target =
+    SCNetworkReachabilityCreateWithName(NULL, hostname);
+  CFStringRef tagString =
+    CFStringCreateWithCString(NULL, tag, kCFStringEncodingUTF8);
+
+  SCNR_schedule(tagString, target);
+
+  return PyBool_FromLong(1);
+}
+
+
+static PyMethodDef SCNetworkReachabilitymodule_methods[] = {
+  {"notify_when_reachable", SCNR_notify_when_reachable, METH_VARARGS,
+   "notify_when_reachable(hostname, tag) -> bool\n\n"
+   "Deliver the SCNetworkIsReachable runloop notification with object tag\n"
+   "when the host becomes reachable, or return false if it's currently\n"
+   "reachable."},
+  {NULL, NULL, 0, NULL}
+};
+
+PyMODINIT_FUNC
+initSCNetworkReachability(void) {
+  PyObject *module = Py_InitModule("SCNetworkReachability",
+				   SCNetworkReachabilitymodule_methods);
+  PyObject *dict = PyModule_GetDict(module);
+  PyObject *reachable = PyString_FromString(SCNR_REACHABLE);
+  PyDict_SetItemString(dict, SCNR_REACHABLE, reachable);
+  Py_DECREF(reachable);
+}
Index: /trunk/LocationDo/action.py
===================================================================
--- /trunk/LocationDo/action.py	(revision 196)
+++ /trunk/LocationDo/action.py	(revision 196)
@@ -0,0 +1,83 @@
+from Authorization import Authorization, kAuthorizationFlagDestroyRights
+from Foundation import NSBundle, NSNotificationCenter, NSObject, NSURL
+from AppKit import NSWorkspace
+from appscript import app, k, its
+import os, osax, subprocess, tempfile, SCNetworkReachability
+
+appBundle = NSBundle.mainBundle()
+
+def ensureKerberosPrincipalsValid(principals):
+    kerberosApp = app(id='edu.mit.Kerberos.KerberosApp')
+    validPrincipals = kerberosApp.caches.filter(its.time_remaining != 'Expired').principal.get()
+    for principal in principals:
+        if principal not in validPrincipals:
+            # XXX make these async
+            kerberosApp.renew_tickets(for_principal=principal)
+            # kerberosApp.get_tickets(for_principal=principal)
+
+def setVolumePercent(percent):
+    # XXX should use CoreAudio: see Pester/Source/NJRSoundManager.m
+    osax.setvolume(int(7 * percent))
+
+def _openURL(url):
+    ws = NSWorkspace.sharedWorkspace()
+    url = NSURL.URLWithString_(url)
+    return ws.openURL_(url)
+
+class _LDURLWatcher(NSObject):
+    def URLIsReachable_(self, notification):
+        _openURL(notification.object())
+
+_urlWatcher = _LDURLWatcher.alloc().init()
+NSNotificationCenter.defaultCenter().addObserver_selector_name_object_(
+    _urlWatcher, 'URLIsReachable:', SCNetworkReachability.SCNetworkIsReachable,
+    None)
+
+def openURL(url):
+    hostname = NSURL.URLWithString_(url).host()
+    if not SCNetworkReachability.notify_when_reachable(hostname, url):
+        _openURL(url)
+
+def openInBackground(path):
+    ws = NSWorkspace.sharedWorkspace()
+    path = os.path.expanduser(path)
+    return ws.openFile_withApplication_andDeactivate_(path, None, False)
+
+def setDisplayBrightnessPercent(percent):
+    # XXX create brightness module
+    pathToBrightness = appBundle.pathForResource_ofType_('brightness', None)
+    subprocess.call([pathToBrightness, str(percent)], stderr=subprocess.STDOUT,
+                    stdout=subprocess.PIPE)
+
+def setAdiumStatus(status):
+    # XXX doesn't match preset status if available
+    adiumApp = app(id='com.adiumX.adiumX')
+    if adiumApp.my_status_type() == k.offline:
+        adiumApp.my_status.status_message_string.set(status)
+    else:
+        adiumApp.my_status_message.set(status)
+    adiumApp.my_status_type.set(k.available)
+
+def terminalDo(command):
+    terminalApp = app(id='com.apple.Terminal')
+    terminalApp.do_script(command + '; exit')
+
+_auth = None
+
+def authorizationDo(command, *args):
+    global _auth
+    if not _auth:
+        _auth = Authorization(destroyflags=(kAuthorizationFlagDestroyRights,))
+    return _auth.executeWithPrivileges(command, *args)
+
+def stopVPNC():
+    authorizationDo('/usr/bin/killall', 'vpnc')
+
+def startVPNC(vpncProfile=None):
+    stopVPNC()
+    args = ['--kernel-ipsec']
+    if vpncProfile:
+        args.append('/etc/vpnc/%s.conf' % vpncProfile)
+    # XXX get password from keychain, then use:
+    # authorizationDo('/usr/local/sbin/vpnc', *args)
+    terminalDo('sudo /usr/local/sbin/vpnc %s' % ' '.join(args))
Index: /trunk/LocationDo/brightness.m
===================================================================
--- /trunk/LocationDo/brightness.m	(revision 196)
+++ /trunk/LocationDo/brightness.m	(revision 196)
@@ -0,0 +1,65 @@
+/* gcc -o brightness -framework Cocoa -framework DisplayServices -F/System/Library/PrivateFrameworks brightness.m */
+
+#import <Foundation/Foundation.h>
+
+@interface O3Manager : NSObject
++ (void)initialize;
++ (id)engineOfClass:(NSString *)cls forDisplayID:(CGDirectDisplayID)fp12;
+@end
+
+@protocol O3EngineWireProtocol
+@end
+
+@protocol BrightnessEngineWireProtocol <O3EngineWireProtocol>
+- (float)brightness;
+- (BOOL)setBrightness:(float)fp8;
+- (void)bumpBrightnessUp;
+- (void)bumpBrightnessDown;
+@end
+
+const int kMaxDisplays = 16;
+
+int main(int argc, const char *argv[])
+{
+  CGDirectDisplayID display[kMaxDisplays];
+  CGDisplayCount numDisplays;
+  CGDisplayCount i;
+  CGDisplayErr err;
+  
+  [[NSAutoreleasePool alloc] init];
+  [O3Manager initialize];
+
+  err = CGGetActiveDisplayList(kMaxDisplays, display, &numDisplays);
+  if (err != CGDisplayNoErr) {
+      NSLog(@"Cannot get displays (%d)", err);
+      exit(1);
+  }
+  printf("%d displays found", (int)numDisplays);
+  for ( i = 0; i < numDisplays; ++i ) {
+      CGDirectDisplayID dspy = display[i];
+      CFDictionaryRef originalMode;
+
+      originalMode = CGDisplayCurrentMode(dspy);
+      if (originalMode == NULL)
+	continue;
+
+      NSLog(@"Display 0x%x: %@", (unsigned int)dspy, originalMode);
+
+      if ([[(NSDictionary *)originalMode objectForKey: @"RefreshRate"] intValue] == 0) {
+	id<BrightnessEngineWireProtocol> engine =
+	  [O3Manager engineOfClass: @"BrightnessEngine" forDisplayID: dspy];
+	NSLog(@"Engine: %@", engine);
+	NSLog(@"Brightness was %f", [engine brightness]);
+	if (argc == 2) {
+	  float newBrightness = [[NSString stringWithCString: argv[1]] floatValue];
+	  if (newBrightness < 0. || newBrightness > 1.) {
+	    NSLog(@"Brightness should be between 0 and 1");
+	    exit(1);
+	  }
+	  [engine setBrightness: newBrightness];
+	  NSLog(@"Brightness is now %f", [engine brightness]);
+	}
+      }
+    }
+  exit(0);
+}
Index: /trunk/LocationDo/setup.py
===================================================================
--- /trunk/LocationDo/setup.py	(revision 196)
+++ /trunk/LocationDo/setup.py	(revision 196)
@@ -0,0 +1,26 @@
+"""
+Script for building LocationDo.
+
+Usage:
+    python setup.py py2app
+"""
+from distutils.core import setup, Extension
+import py2app
+
+plist = dict(
+    CFBundleIdentifier='net.sabi.LocationDo',
+    CFBundleName='LocationDo',
+    NSPrincipalClass='LocationDo',
+    LSUIElement=1,
+)
+
+setup(
+    app=["LocationDo.py"],
+    ext_modules=[Extension('SCNetworkReachability',
+                           sources=['SCNetworkReachabilitymodule.c'],
+                           extra_link_args=['-framework', 'SystemConfiguration',
+                                            '-framework', 'CoreFoundation'])],
+    data_files=["English.lproj"],
+    options=dict(py2app=dict(plist=plist,
+                             resources=['brightness'])),
+)
