Ground control to Major Tom

Two nights ago, Chris Herbert (@hrbrt) asked me on the Twitter if I’d seen the International Space Station pass overhead. I hadn’t, but I saw it last night (very bright on a crystal clear night) and again tonight (in the half of the sky that wasn’t overcast) because I wrote a script to add noteworthy ISS passes to my calendar and alert me of them.

The script, called iss-calendar, didn’t take long to write because it’s pretty much the same as one I wrote a couple of years ago to add Iridium flare events to my calendar. Both scripts work by scraping a page from the Heavens Above website, extracting the pertinent information, and inserting that information into my “home” calendar in iCal.

Heavens Above, if you’ve never visited, is a wonderful site for amateur astronomy. Set yourself up with a free account there, give it your latitude and longitude, and it will provide you with lots of fun information on what you can see in the nighttime (and sometimes daytime) sky.1

Heavens Above ISS passes

Here’s the script that puts ISS passes into my calendar:

  1:  #!/usr/bin/env python
  2:  # -*- coding: UTF-8 -*-
  4:  import mechanize
  5:  from time import strftime
  6:  from BeautifulSoup import BeautifulSoup
  7:  from datetime import datetime, date, timedelta
  8:  from time import strptime
  9:  from appscript import *
 11:  # Personalization.
 12:  user = {'name': 'yourname', 'password': 'yourpw'}   # Heavens Above login
 13:  maxMag = 0      # show passes at least this bright
 14:  minAlt = 30     # show passes at least this high
 16:  # Add an event to my "home" calendar with an alarm 15 minutes before.
 17:  def makeiCalEvent(start, end, loc1, loc2, loc3, intensity):
 18:    info = "mag %.1f, %s to %s to %s" % (intensity, loc1, loc2, loc3)
 19:    evt = app('iCal').calendars['home'].events.end.make(new=k.event,
 20:      with_properties={k.summary:'ISS pass', k.start_date:start, k.end_date:end, k.location:info})
 21:    evt.sound_alarms.end.make(new=k.sound_alarm,
 22:      with_properties={k.sound_name:'Basso', k.trigger_interval:-15})
 24:  # Parse a row of Heavens Above data and return the start date (datetime),
 25:  # the intensity (integer), and the beginning, peak, and end sky positions
 26:  # (strings).
 27:  def parseRow(row):
 28:    cols = row.findAll('td')
 29:    dStr = cols[0].a.string
 30:    t1Str = ':'.join(cols[2].string.split(':')[0:2])
 31:    t3Str = ':'.join(cols[8].string.split(':')[0:2])
 32:    intensity = float(cols[1].string)
 33:    alt1 = cols[3].string.replace(u'°', '')
 34:    az1 = cols[4].string
 35:    alt2 = cols[6].string.replace(u'°', '')
 36:    az2 = cols[7].string
 37:    alt3 = cols[9].string.replace(u'°', '')
 38:    az3 = cols[10].string
 39:    loc1 = '%s-%s' % (az1, alt1)
 40:    loc2 = '%s-%s' % (az2, alt2)
 41:    loc3 = '%s-%s' % (az3, alt3)
 42:    startStr = '%s %s %s' % (dStr,, t1Str)
 43:    start = datetime(*strptime(startStr, '%d %b %Y %H:%M')[0:7])
 44:    endStr = '%s %s %s' % (dStr,, t3Str)
 45:    end = datetime(*strptime(endStr, '%d %b %Y %H:%M')[0:7])
 46:    return (start, end, intensity, loc1, loc2, loc3)
 48:  # This will be run weekly. The ISS information is given for a 10-day window.
 49:  # To avoid duplicating events, we'll filter out events more than 7 days
 50:  # in the future.
 51:  nextWeek = + timedelta(days=7)
 53:  # Heavens Above URLs and login information.
 54:  loginURL = ''
 55:  issURL = ''
 57:  # Create virtual browser and login.
 58:  br = mechanize.Browser()
 59:  br.set_handle_robots(False)
 61:  br.select_form(nr=0)    # the login form is the first on the page
 62:  br['UserName'] = user['name']
 63:  br['Password'] = user['password']
 64:  resp = br.submit()
 66:  # Get session ID from the end of the response URL.
 67:  sid = resp.geturl().split('=')[1]
 69:  # Get the 10-day ISS page.
 70:  iHtml = + sid).read()
 73:  # In the past, Beautiful Soup hasn't been able to parse the Heavens Above HTML.
 74:  # To get around this problem, we extract just the table of ISS data and set
 75:  # it in a well-formed HTML skeleton.
 76:  table = iHtml.split(r'<table id="ctl00_ContentPlaceHolder1_tblPasses"', 1)[1]
 77:  table = table.split(r'>', 1)[1]
 78:  table = table.split(r'</table>', 1)[0]
 80:  html = '''<html>
 81:  <head>
 82:  </head>
 83:  <body>
 84:  <table>
 85:  %s
 86:  </table>
 87:  </body>
 88:  </html>''' % table
 90:  # Parse the HTML.
 91:  soup = BeautifulSoup(html)
 93:  # Collect only the data rows of the table.
 94:  rows = soup.findAll('table')[0].findAll('tr')[2:]
 96:  # Go through the data rows, adding only bright, high events within
 97:  # the next week to my "home" calendar.
 98:  for row in rows:
 99:    (start, end, intensity, loc1, loc2, loc3) = parseRow(row)
100:    if intensity <= maxMag and int(loc2.split('-')[1]) >= minAlt\
101:        and < nextWeek:
102:      makeiCalEvent(start, end, loc1, loc2, loc3, intensity)
103:      # print '%s, %.1f, %s to %s to %s' %\
104:      #   (start.strftime('%b %d %H:%M'), intensity, loc1, loc2, loc3)

There’s a lot of stuff packed in iss-calendar, but each section is pretty straightforward.

Lines 12-18 hold your Heavens Above login information, the brightness and altitude filters, and the name of the iCal calendar you want the passes added to. There’s no point in having every ISS pass in your calendar. By adjusting these filters, you can be assured that you’ll get alerts only for the high and bright passes.2

Lines 18-23 define the function that adds an event to iCal. The start and end times of the event are taken from the table, and the location field gets the magnitude and the path. For example, tonight’s pass, from the first line of the table, was “mag -3.4, SW-10 to SE-66 to ENE-10.”

The makeiCalEvent function is written in appscript because that’s what I was using when I wrote the Iridium flare script that iss-calendar is based on. With appscript deprecated, I’ll switch it to an AppleScript/Python hybrid when I get a chance.

Lines 28-47 define a function that parses a row of the passes table and returns a tuple of information concerning the date, time, magnitude, and path of the ISS pass. There’s a lot of repetition in this function that I could probably eliminate with a set of lists and some clever integer arithmetic for the indices, but I’m not feeling especially clever right now.

Lines 59-71 use the mechanize library to log into Heavens Above and navigate to the page with the ISS passes. If Heavens Above changes its page layout, this is where the script will break.

Lines 77-89 are a kludge that might not be necessary. When writing my earlier script, I learned that the Heavens Above page with the Iridium flare information couldn’t be parsed by the Beautiful Soup library. So I pulled out just the tabular data and put it in a simple HTML template that Beautiful Soup understood. I’m doing the same thing here, even though Beautiful Soup might be able to parse the ISS page, because it’s easier to just follow the old script.

Lines 92-95 parse the simplified HTML and extract just the data rows of the table.

Lines 98-102 work through the rows of the table, adding calendar events for each pass in the next week that meets the brightness and altitude criteria. Here’s what an ISS pass event looks like in Fantastical,

ISS pass in Fantastical

and here’s what it looks like on an iPhone,

ISS pass on iPhone

Lines 103-104 are a leftover from the debugging phase that I decided to comment out rather than delete.

Of course, we don’t want to have to run this script by hand. It’s better to schedule the computer run it every week. It would be fairly easy to add a repeating iCal event to run the script, but having iCal run a script that adds events to iCal is a little too recursive for my taste. I prefer to use the Mac’s launchd system. That means using Lingon or Lingon 3 to create and load a launchd control file, called com.leancrew.iss.plist, that runs the script every Sunday morning at 6:15:

Lingon ISS schedule

You can, of course, create the com.leancrew.iss.plist file by hand and install it through launchctl, but Lingon makes the creation of these files both simple and foolproof.

Well, as usual with these things, I’ve spent more time explaining it than doing it. I expect I’ll be fiddling with the maxMag and minAlt figures until I get a system that’s neither too chatty nor too aloof. That’ll probably take a few weeks, at least.

  1. Heavens Above can remember not only your home latitude and longitude, but any number of other locations: relative’s homes, vacation spots—wherever you might go and want to look in the sky. My family usually takes a summer trip to the Warren Dunes, so I have that as one of my saved locations. It’s amazing how dark the sky is when you have Lake Michigan between you and Chicago. 

  2. The brightness is given as an apparent magnitude number, a measurement system that goes back to the ancient Greeks. The lower the magnitude, the brighter the object. For reference, Vega has a magnitude of about 0, Sirius has a magnitude of about -1.5, and right now Venus has a magnitude of about -4.4.