Empty skies

For the past two nights, the calendar on my iPhone has alerted me to an Iridium flare that seemed like it was going to be spectacular: very bright, with a magnitude of -6 or -7; high enough to be out of the worst of the light pollution; and in a part of the sky that I have a good view of from the street in front of my house. As important, these two nights have been wonderfully clear. But each night I’ve come back in the house disappointed; tonight I realized why I hadn’t seen anything.

The alerts in my calendar come from this script, which

  1. logs onto my account at the Heavens Above website,
  2. gets the list of Iridium flares for my observing site for the coming week,
  3. collects from the list only the brightest flares that occur in the evening, and
  4. generates a set of iCal entries for the flares.

This script is run by a launchd process every Monday morning. The iCal entries then get transferred to my iPhone the next time I sync.

The script has been working perfectly since early this spring, and its previous incarnation—which emailed me a list of the coming week’s good flares every Monday—has been working well for about a year. So why fail now?

Last week, my family took a short trip to Michigan, and I set my Heavens Above observing site to the Warren Dunes, in case we’d be there on a clear night with something interesting in the sky. I’d forgotten to change it back to Naperville when we returned, so this week’s alerts in my calendar are for viewing flares in and around Sawyer, Michigan. I hope a few people there got out and took a look.

I made a small change to the script since writing about it this spring, so I might as well present the new version here. It now includes the magnitude of the flare in addition to its altitude and azimuth in the location field of the iCal event.

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

The only difference is an additional line to include the magnitude in the makeiCalEvent function—everything else is the same. If you want a more detailed description of the program, or to see the launchd plist file, go to my post from this spring.