• 1 Post
  • 1 Comment
Joined 7 months ago
cake
Cake day: January 23rd, 2025

help-circle
  • I don’t have this setup on github yet (and likely won’t since it was just a learning exercise), but here’s the code for tracking when the ISS will be visible:

    `

        # USAGE: python3 get_iss_pass.py <zip_code> [days_to_search]
        # This version provides all necessary data with explicit keys for n8n.
     
    import sys
    import os
    import pandas
    import pgeocode
    from skyfield.api import load, Topos, Loader
    from timezonefinder import TimezoneFinder
    from datetime import timezone, timedelta
    from zoneinfo import ZoneInfo
    
    def get_compass_direction(degrees):
     # This function converts an azimuth angle in degrees into a cardinal or intercardinal compass direction.
     # It divides the 360-degree circle into 16 segments and maps the given degrees to the corresponding direction.
     directions = ["N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE",
                   "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"]
     index = int((degrees + 11.25) / 22.5) % 16
     return directions[index]
    
    def get_iss_pass_details(zip_code, search_days=3):
     # Initialize pgeocode for US postal code lookups.
     geo = pgeocode.Nominatim('us')
     # Query the provided zip code to get geographical information.
     location_info = geo.query_postal_code(zip_code)
    
    # Check if the location information was successfully retrieved.
    if pandas.isna(location_info.latitude):
        print(f"Error: Could not find location for zip code {zip_code}")
        return
    
    # Extract latitude and longitude from the location information.
    lat, lon = location_info.latitude, location_info.longitude
    # Construct a human-readable place name.
    place_name = f"{location_info.place_name}, {location_info.state_code}"
    
    # Initialize TimezoneFinder to determine the timezone for the given coordinates.
    tf = TimezoneFinder()
    # Get the timezone string; default to "UTC" if not found.
    timezone_str = tf.timezone_at(lng=lon, lat=lat) or "UTC"
    # Create a ZoneInfo object for the local timezone.
    local_tz = ZoneInfo(timezone_str)
    
    # Print the calculated location and timezone information.
    print(f"Calculating for {place_name} (Lat: {lat:.2f}, Lon: {lon:.2f})")
    print(f"Using timezone: {timezone_str}\n")
    
    try:
        # Define the path for Skyfield data files.
        data_path = os.path.expanduser('~/skyfield-data')
        # Initialize the Skyfield data loader, specifying the data path.
        load = Loader(data_path)
        # Get the Skyfield timescale object, which handles time conversions.
        ts = load.timescale()
        # Load the ephemeris data (positions of celestial bodies) for accurate calculations.
        eph = load('de421.bsp')
        # Define the URL to download the latest Two-Line Elements (TLE) for the ISS.
        stations_url = 'https://celestrak.org/NORAD/elements/gp.php?GROUP=stations&FORMAT=tle'
        # Load the TLE file for satellites, reloading to ensure up-to-date data.
        satellites = load.tle_file(stations_url, reload=True)
    except Exception as e:
        # Handle any errors during data download or loading.
        print(f"Could not download astronomical data: {e}")
        return
    
    # Select the International Space Station (ISS), which is typically the first satellite in the 'stations' group.
    iss = satellites[0]
    # Get the Sun's ephemeris data.
    sun = eph['sun']
    # Confirm that ISS data has been loaded.
    print(f"Loaded satellite data for: {iss.name}")
    
    # Define the observer's location on Earth using Topos.
    earth_observer = Topos(latitude_degrees=lat, longitude_degrees=lon)
    # Define the observer's position relative to the Solar System Barycenter (SSB).
    ssb_observer = eph['earth'] + earth_observer
    
    # Define the start and end times for the ISS pass search.
    # t0 is the current time, t1 is the current time plus the specified search_days.
    t0 = ts.now()
    t1 = t0 + search_days
    
    # Find all events (starts, culminations, ends) where the ISS's altitude is above 10 degrees.
    times, events = iss.find_events(earth_observer, t0, t1, altitude_degrees=10.0)
    # Initialize a list to store details of visible ISS passes.
    found_passes = []
    
    # Iterate through the found events.
    for ti, event in zip(times, events):
        # Check if the event is an "appears" event (event code 0).
        if event == 0:
            # Determine if the observer is in darkness (Sun's altitude below -6 degrees, astronomical twilight).
            observer_is_in_dark = (sun - ssb_observer).at(ti).altaz()[0].degrees < -6.0
            # Determine if the ISS is sunlit at this time.
            iss_is_in_sunlit = iss.at(ti).is_sunlit(eph)
            
            # A pass is visible if the observer is in darkness AND the ISS is sunlit.
            if observer_is_in_dark and iss_is_in_sunlit:
                start_time = ti
                culmination_details = None
                end_time = None
                
                # Search for subsequent events (culmination and disappearance) within the pass.
                future_times, future_events = iss.find_events(earth_observer, ti, t1, altitude_degrees=10.0)
    
                for next_ti, next_event in zip(future_times, future_events):
                    # If the event is a "culmination" (event code 1), get its maximum elevation.
                    if next_event == 1:
                        alt, _, _ = (iss - earth_observer).at(next_ti).altaz()
                        culmination_details = f"Max Elevation: {int(alt.degrees)}°"
                    # If the event is a "disappears" (event code 2), mark it as the end of the pass and break the loop.
                    elif next_event == 2:
                        end_time = next_ti
                        break
    
                # If a valid end time was found for the pass.
                if end_time is not None:
                    # Convert start and end times to the local timezone.
                    appears_dt_local = start_time.utc_datetime().astimezone(local_tz)
                    disappears_dt_local = end_time.utc_datetime().astimezone(local_tz)
                    # Calculate a notification time (15 minutes before appearance).
                    notification_dt_local = appears_dt_local - timedelta(minutes=15)
    
                    # Get altitude and azimuth for the appearance and disappearance points.
                    alt_start, az_start, _ = (iss - earth_observer).at(start_time).altaz()
                    alt_end, az_end, _ = (iss - earth_observer).at(end_time).altaz()
    
                    # Format the pass information for output, including explicit keys for n8n.
                    pass_info = (
                        f"DURATION: {(end_time - start_time) * 24 * 60:.1f} minutes\n"
                        f"APPEARS_TIME: {appears_dt_local.strftime('%Y-%m-%d %I:%M:%S %p %Z')}\n"
                        f"APPEARS_DIRECTION: {int(az_start.degrees)}° ({get_compass_direction(az_start.degrees)})\n"
                        f"MAX_ELEVATION: {int(alt_start.degrees)}°\n"
                        f"CULMINATION: {culmination_details if culmination_details else 'N/A'}\n"
                        f"DISAPPEARS_TIME: {disappears_dt_local.strftime('%Y-%m-%d %I:%M:%S %p %Z')}\n"
                        f"DISAPPEARS_DIRECTION: {int(az_end.degrees)}° ({get_compass_direction(az_end.degrees)})\n"
                        f"NOTIFICATION_ISO: {notification_dt_local.isoformat()}"
                    )
                    # Add the formatted pass information to the list of found passes.
                    found_passes.append(pass_info)
    
    # Get the total number of visible passes found.
    num_passes = len(found_passes)
    # Calculate the total search duration in hours.
    search_hours = search_days * 24
    
    # Print a summary of the found passes or a message if none were found.
    if num_passes > 0:
        print(f"\n--- Found {num_passes} visible pass(es) in the next {search_hours} hours ---")
        for i, pass_detail in enumerate(found_passes, 1):
            print(f"\n--- Pass {i} of {num_passes} ---")
            print(pass_detail)
    else:
        print(f"\nNo visible ISS passes found in the next {search_hours} hours for this location.")
    
    # This block ensures the script runs only when executed directly (not imported as a module).
    if __name__ == "__main__":
    # Check if the correct number of command-line arguments is provided.
    if len(sys.argv) < 2:
        print("Usage: python3 get_iss_pass.py <zip_code> [days_to_search]")
        sys.exit(0)
    
    # Get the zip code from the first command-line argument.
    zip_code_arg = sys.argv[1]
    # Set a default search duration of 3 days.
    days_arg = 3
    
    # If a second argument is provided, try to interpret it as the search duration in days.
    if len(sys.argv) > 2:
        try:
            days_arg = int(sys.argv[2])
        except ValueError:
            # Handle the error if 'days_to_search' is not a valid integer.
            print("Error: 'days_to_search' must be an integer.")
            sys.exit(1)
    
    # Call the main function to get and display ISS pass details.
    get_iss_pass_details(zip_code_arg, days_arg) `
    

    Hope this helps!