Iridium flares in iCal

Last summer I wrote a script that logged in to my account at Heaven’s Above every Monday morning and emailed me a list of the week’s upcoming Iridium flares. The script has been working perfectly, but I’ve found myself doing the same things every time I read the email:

These are purely mechanical steps and are better done by a program than by me. So here’s my new Iridium script, called iridium-calendar, which automates the entire process.

 1:  #!/usr/bin/env python
 2:  
 3:  import mechanize
 4:  from time import strftime
 5:  from BeautifulSoup import BeautifulSoup
 6:  from datetime import datetime, date, timedelta
 7:  from time import strptime
 8:  from appscript import *
 9:  
10:  # Add an event to my "home" calendar with an alarm 15 minutes before.
11:  def makeiCalEvent(start, loc):
12:    end = start + timedelta(0, 60)
13:    evt = app('iCal').calendars['home'].events.end.make(new=k.event,
14:      with_properties={k.summary:'Iridium flare', k.start_date:start, k.end_date:end, k.location:loc})
15:    evt.sound_alarms.end.make(new=k.sound_alarm,
16:      with_properties={k.sound_name:'Basso', k.trigger_interval:-15})
17:  
18:  # Parse a row of Heaven's Above data and return the start date (datetime),
19:  # the intensity (integer), and the sky position (string).
20:  def parseRow(row):
21:    cols = row.findAll('td')
22:    dStr = cols[0].string
23:    tStr = ':'.join(cols[1].a.string.split(':')[0:2])
24:    intensity = int(cols[2].string)
25:    alt = cols[3].string.replace('°', '')
26:    az = cols[4].string.replace('°', '')
27:    loc = 'alt %s, az %s' % (alt, az)
28:    startStr = '%s %s %s' % (dStr, date.today().year, tStr)
29:    start = datetime(*strptime(startStr, '%d %b %Y %H:%M')[0:7])
30:    return (start, intensity, loc)
31:  
32:  # Heaven's Above URLs and login information.
33:  lURL = 'http://heavens-above.com/logon.asp'                       # login
34:  iURL = 'http://heavens-above.com/iridium.asp?Dur=7&Session='      # iridium flares
35:  user = {'name' : 'user',  'password' : 'seekret'}
36:  
37:    
38:  # Create virtual browser and login.
39:  br = mechanize.Browser()
40:  br.set_handle_robots(False)
41:  br.open(lURL)
42:  br.select_form(nr=0)    # the login form is the first on the page
43:  br['UserName'] = user['name']
44:  br['Password'] = user['password']
45:  resp = br.submit()
46:  
47:  # Get session ID from the end of the response URL.
48:  sid = resp.geturl().split('=')[1]
49:  
50:  # Get the 7-day Iridium page.
51:  iHtml = br.open(iURL + sid).read()
52:  
53:   
54:  # For some reason, Beautiful Soup can't parse the HTML on the Iridium page.
55:  # To get around this problem, we extract just the table of flare data and set
56:  # it in a well-formed HTML skeleton.
57:  table = iHtml.split(r'<table BORDER CELLPADDING=5>')[1]
58:  table = table.split(r'</table>')[0]
59:  
60:  html = '''<html>
61:  <head>
62:  </head>
63:  <body>
64:  <table>
65:  %s
66:  </table>
67:  </body>
68:  </html>''' % table
69:  
70:  # Parse the HTML.
71:  soup = BeautifulSoup(html)
72:  
73:  # Collect only the data rows of the table.
74:  rows = soup.findAll('table')[0].findAll('tr')[1:]
75:  
76:  # Go through the data rows, adding only bright evening events to my "home" calendar.
77:  for row in rows:
78:    (start, intensity, loc) = parseRow(row)
79:    if intensity <= -5 and start.hour > 12:
80:      makeiCalEvent(start, loc)

All the logging-in and data gathering in Lines 31-50 are the same as before. The new stuff is in the two functions, makeiCalEvent and parseRow, and in Lines 53-79 of the main program.

In writing makeiCalEvent, I leaned heavily on this article on making an iCal to-do at Clark’s Tech Blog and on the ASTranslate application from the appscript people. The code is not what you’d call elegant, mainly because iCal’s AppleScript dictionary is an awkward mess.

The parseRow function has lots of fiddly bits, because it needs to

The result, though is pretty clean: an iCal entry with an alarm and enough information to be looking in the right place when the flare appears.

The iCal entry itself isn’t so important, as I’m almost never at my computer when the Iridium alarm goes off. What’s important is that these entries get synced to my iPhone, and my iPhone sounds the alarm 15 minutes before the flare does its thing. Here’s what the alarm looks like on the phone:

I don’t run iridium-calendar myself—that’s something for the computer to remember. I have launchd run it every Monday morning at 6:00 AM via this plist file, called com.leancrew.iridium.plist, in my ~/Library/LaunchAgents folder:

 1:  <?xml version="1.0" encoding="UTF-8"?>
 2:  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 3:  <plist version="1.0">
 4:  <dict>
 5:   <key>KeepAlive</key>
 6:   <false/>
 7:   <key>Label</key>
 8:   <string>com.leancrew.iridium</string>
 9:   <key>ProgramArguments</key>
10:   <array>
11:     <string>/Users/drang/bin/iridium-calendar</string>
12:   </array>
13:   <key>StartCalendarInterval</key>
14:   <dict>
15:     <key>Hour</key>
16:     <integer>6</integer>
17:     <key>Minute</key>
18:     <integer>0</integer>
19:     <key>Weekday</key>
20:     <integer>1</integer>
21:   </dict>
22:  </dict>
23:  </plist>

I still have Lingon generate launchd configuration files, even though its author, Peter Borg, stopped developing it a while ago. It’s easy to use and doesn’t make typos. The one thing I dislike about Lingon is its message telling you to go through a log out/log in cycle to get your new agent installed. That’s not necessary; instead, use the launchctl command to load and unload agents as you develop them.

Update 12/15/10
I made a small update to the script that adds the magnitude of the flare to the location field. Also, if you want to modify it for your own use, you’ll need to install the mechanize, BeautifulSoup, and appscript libraries.