After doing some research I have found the answer to my problem on Celestrak. They have a great article about exactly this that I will summarize here, though if your interested in doing this you should read it here.
There are 3 main requirements for a satellite to be visible:
• The satellite must be above the observer's horizon.
• The sun must be below the observer's horizon enough to darken the sky.
• The satellite must be illuminated by the sun.
The first point we can ignore because by definition a pass calculated by any library or website has to be over the horizon for it to be a pass over the specified location.
This second point is crucial for what I am interested in. The sun must be at least -6˚ below the horizon so that it is dark enough that the light from the sun reflected on the satellite is bright enough relative to the sky to stand out, this is called the nautical twilight. So the sun must have set, but it also must not be below -18˚ otherwise not enough sunlight can get to the satellite for it to be visible. This is why you can only see satellites for a few hours around dawn or dusk.
The third point is that the object must not be eclipsed by the earth. If you are not using pyephem or another library that calculates this then I highly suggest you read their article which details how to calculate it. Since pyephem does calculate wether the object is eclipsed I will not get into the math here and simply say that this can be found using iss.eclipsed for example.
To tie this all together, using pyephem one can find the next pass and wether it's visible like this:
def seconds_between(d1, d2):
return abs((d2 - d1).seconds)
def datetime_from_time(tr):
year, month, day, hour, minute, second = tr.tuple()
dt = datetime.datetime(year, month, day, hour, minute, int(second))
return dt
def get_next_pass(lon, lat, alt, tle):
sat = ephem.readtle(str(tle[0]), str(tle[1]), str(tle[2]))
observer = ephem.Observer()
observer.lat = str(lat)
observer.long = str(lon)
observer.elevation = alt
observer.pressure = 0
observer.horizon = '-0:34'
now = datetime.datetime.utcnow()
observer.date = now
tr, azr, tt, altt, ts, azs = observer.next_pass(sat)
duration = int((ts - tr) *60*60*24)
rise_time = datetime_from_time(tr)
max_time = datetime_from_time(tt)
set_time = datetime_from_time(ts)
observer.date = max_time
sun = ephem.Sun()
sun.compute(observer)
sat.compute(observer)
sun_alt = degrees(sun.alt)
visible = False
if sat.eclipsed is False and -18 < degrees(sun_alt) < -6 :
visible = True
return {
"rise_time": timegm(rise_time.timetuple()),
"rise_azimuth": degrees(azr),
"max_time": timegm(max_time.timetuple()),
"max_alt": degrees(altt),
"set_time": timegm(set_time.timetuple()),
"set_azimuth": degrees(azs),
"elevation": sat.elevation,
"sun_alt": sun_alt,
"duration": duration,
"visible": visible
}