def get_color_from_condition( category, metar=None ): """ From a condition, returns the color it should be rendered as, and if it should flash. Arguments: category {string} -- The weather category (VFR, IFR, et al.) Returns: [tuple] -- The color (also a tuple) and if it should blink. """ is_old = False metar_age = None if metar is not None and metar != weather.INVALID: metar_age = weather.get_metar_age(metar) if metar_age is not None: metar_age_minutes = metar_age.total_seconds() / 60.0 safe_log( LOGGER, "{} - Issued {:.1f} minutes ago".format(category, metar_age_minutes)) is_old = metar_age_minutes > weather.DEFAULT_METAR_INVALIDATE_MINUTES is_inactive = metar_age_minutes > weather.DEFAULT_METAR_STATION_INACTIVE else: is_inactive = True # No report for a while? # Count the station as INOP. # The default is to follow what ForeFlight and SkyVector # do and just turn it off. if is_inactive: return (weather.INOP, False) should_blink = is_old and configuration.get_blink_station_if_old_data() if category == weather.VFR: return (weather.GREEN, should_blink) elif category == weather.MVFR: return (weather.BLUE, should_blink) elif category == weather.IFR: return (weather.RED, should_blink) elif category == weather.LIFR: # Only blink for normal LEDs. # PWM and WS2801 have their own color. return (weather.LOW, configuration.get_mode() == configuration.STANDARD) elif category == weather.NIGHT: return (weather.YELLOW, False) elif category == weather.SMOKE: return (weather.GRAY, should_blink) # Error return (weather.OFF, False)
def get_airport_category( airport, metar, utc_offset ): """ Gets the category of a single airport. Arguments: airport {string} -- The airport identifier. utc_offset {int} -- The offset from UTC to local for the airport. Returns: string -- The weather category for the airport. """ category = weather.INVALID try: safe_log( LOGGER, 'get_airport_category({}, {}, {})'.format( airport, metar, utc_offset)) try: category = weather.get_category(airport, metar, logger=LOGGER) twilight = weather.get_civil_twilight(airport, logger=LOGGER) safe_log( LOGGER, "{} - Rise(UTC):{}, Set(UTC):{}".format( airport, twilight[1], twilight[4])) safe_log( LOGGER, "{} - Rise(HERE):{}, Set(HERE):{}".format( airport, twilight[1] - utc_offset, twilight[4] - utc_offset)) except Exception as e: safe_log_warning( LOGGER, "Exception while attempting to categorize METAR:{} EX:{}".format(metar, e)) except Exception as e: safe_log( LOGGER, "Captured EX while attempting to get category for {} EX:{}".format(airport, e)) category = weather.INVALID safe_log(LOGGER, '~get_airport_category() => {}'.format(category)) return category
def render_airport( airport, airport_flasher ): """ Renders an airport. Arguments: airport {string} -- The identifier of the station. airport_flasher {bool} -- Is this a flash (off) cycle? """ condition, blink = get_airport_condition(airport) color_by_category = color_by_rules[condition] if blink and airport_flasher: color_by_category = colors[weather.OFF] proportions, color_to_render = get_mix_and_color( color_by_category, airport) log = airport not in airport_render_last_logged_by_station if airport in airport_render_last_logged_by_station: time_since_last = datetime.utcnow() \ - airport_render_last_logged_by_station[airport] log = time_since_last.total_seconds() > 60 if log: message_format = 'STATION={}, CAT={:5}, BLINK={}, COLOR={:3}:{:3}:{:3}, P_O2N={:.1f}, P_N2C={:.1f}, RENDER={:3}:{:3}:{:3}' message = message_format.format( airport, condition, blink, color_by_category[0], color_by_category[1], color_by_category[2], proportions[0], proportions[1], color_to_render[0], color_to_render[1], color_to_render[2]) safe_log(LOGGER, message) airport_render_last_logged_by_station[airport] = datetime.utcnow() if renderer is not None: renderer.set_led( airport_render_config[airport], color_to_render)
def update_all_station_categorizations(): """ Takes the latest reports (probably in cache) and then updates the categorization of the airports. """ utc_offset = datetime.utcnow() - datetime.now() safe_log(LOGGER, "update_all_station_categorizations(LOCAL={}, UTC={})".format( datetime.now(), datetime.utcnow())) [update_station_categorization(airport, utc_offset) for airport in airport_render_config] safe_log(LOGGER, '~update_all_station_categorizations()')
def render_thread(): """ Main logic loop for rendering the lights. """ safe_log(LOGGER, "Starting rendering thread") while True: try: render_airport_displays(True) time.sleep(1) render_airport_displays(False) except KeyboardInterrupt: quit() finally: time.sleep(1)
def get_category(airport_icao_code, metar, logger=None): """ Returns the flight rules classification based on the entire RAW metar. Arguments: airport_icao_code -- The airport or weather station that we want to get a category for. metar {string} -- The RAW weather report in METAR format. return_night {boolean} -- Should we return a category for NIGHT? Returns: string -- The flight rules classification, or INVALID in case of an error. """ if metar is None: return INOP if metar == INVALID: return INVALID metar_age = get_metar_age(metar) if metar_age is not None: metar_age_minutes = metar_age.total_seconds() / 60.0 safe_log( logger, "{} - Issued {:.1f} minutes ago".format(airport_icao_code, metar_age_minutes)) if metar_age_minutes > DEFAULT_METAR_STATION_INACTIVE: return INOP else: safe_log_warning(logger, "{} - Unknown METAR age".format(airport_icao_code)) vis = get_visibility(metar) ceiling = get_ceiling_category(get_ceiling(metar, logger=logger)) if vis == SMOKE: return SMOKE if vis == LIFR or ceiling == LIFR: return LIFR if vis == IFR or ceiling == IFR: return IFR if ceiling == INVALID or vis == INVALID: return INVALID if vis == MVFR or ceiling == MVFR: return MVFR return VFR
def get_metars(airport_iaco_codes, logger=None): """ Returns the (RAW) METAR for the given station Arguments: airport_iaco_codes {string} -- The list of IACO code for the weather station. Returns: dictionary - A dictionary (keyed by airport code) of the RAW metars. Returns INVALID as the value for the key if an error occurs. """ metars = {} safe_log(logger, 'get_metars([{}])'.format(','.join(airport_iaco_codes))) try: metars = get_metar_reports_from_web(airport_iaco_codes) except Exception as e: safe_log_warning(logger, 'get_metars EX:{}'.format(e)) metars = {} safe_log(logger, 'Attempting to reconcile METARs not returned with cache.') # For the airports and identifiers that we were not able to get # a result for, see if we can fill in the results. for identifier in airport_iaco_codes: if identifier in metars and metars[identifier] is not None: safe_log(logger, '{} had result, using it'.format(identifier)) continue # If we did not get a report, but do # still have an old report, then use the old # report. if identifier in __metar_report_cache__: safe_log_warning( logger, 'Falling back to cached METAR for {}'.format(identifier)) metars[identifier] = __metar_report_cache__[identifier][1] # Fall back to an "INVALID" if everything else failed. else: safe_log_warning( logger, 'METAR for {} being set to INVALID'.format(identifier)) metars[identifier] = INVALID safe_log(logger, '~get_metars() => [{}]'.format(','.join(metars))) return metars
def set_airport_display( airport, category, metar=None ): """ Sets the given airport to have the given flight rules category. Arguments: airport {str} -- The airport identifier. category {string} -- The flight rules category. Returns: bool -- True if the flight category changed (or was set for the first time). """ safe_log( LOGGER, 'set_airport_display({}, {}, {})'.format( airport, category, metar)) changed = False try: color_and_flash = get_color_from_condition(category, metar=metar) should_flash = color_and_flash[1] thread_lock_object.acquire() if airport in airport_conditions: changed = airport_conditions[airport][0] != category else: changed = True airport_conditions[airport] = (category, should_flash) except Exception as ex: safe_log_warning( LOGGER, 'set_airport_display() - {} - EX:{}'.format(airport, ex)) finally: thread_lock_object.release() if changed: safe_log(LOGGER, '{} NOW {}'.format(airport, category)) safe_log(LOGGER, '~set_airport_display() => {}'.format(changed)) return changed
category = get_airport_category(airport, metar, utc_offset) airport_conditions[airport] = (category, False) except: airport_conditions[airport] = (weather.INVALID, False) safe_log_warning( LOGGER, "Error while initializing with airport=" + airport) finally: thread_lock_object.release() return True if __name__ == '__main__': # Start loading the METARs in the background # while going through the self-test safe_log(LOGGER, "Initialize weather for all airports") weather.get_metars(airport_render_config.keys(), logger=LOGGER) # Test LEDS on startup colors_to_init = ( weather.LOW, weather.RED, weather.BLUE, weather.GREEN, weather.YELLOW, weather.WHITE, weather.GRAY, weather.DARK_YELLOW, weather.OFF )
try: airport_render_config = configuration.get_airport_configs() except Exception as e: terminal_error( 'Unable to fetch the airport configuration. Please check the JSON files. Error={}' .format(e)) if len(airport_render_config) == 0: terminal_error('No airports found in the configuration file.') stations_unable_to_fetch_weather = [] for station_id in airport_render_config: safe_log(LOGGER, 'Checking configuration for {}'.format(station_id)) led_index = airport_render_config[station_id] # Validate the index for the LED is within bounds if led_index < 0: terminal_error('Found {} has an LED at a negative position {}'.format( station_id, led_index)) # Validate that the station is in the CSV file try: data_file_icao_code = weather.get_faa_csv_identifier(station_id) except Exception as e: terminal_error( 'Unable to fetch the station {} from the CSV data file. Please check that the station is in the CSV file. Error={}' .format(station_id, e))
def get_metar(airport_icao_code, logger=None, use_cache=True): """ Returns the (RAW) METAR for the given station Arguments: airport_icao_code {string} -- The ICAO code for the weather station. Keyword Arguments: use_cache {bool} -- Should we use the cache? Set to false to bypass the cache. (default: {True}) """ safe_log(logger, 'get_metar({})'.format(airport_icao_code)) if airport_icao_code is None or len(airport_icao_code) < 1: safe_log(logger, 'Invalid or empty airport code') is_cache_valid, cached_metar = __is_cache_valid__(airport_icao_code, __metar_report_cache__) safe_log( logger, 'Cache for {} is {}, {}'.format(airport_icao_code, is_cache_valid, cached_metar)) # Make sure that we used the most recent reports we can. # Metars are normally updated hourly. if is_cache_valid \ and cached_metar != INVALID \ and use_cache \ and (get_metar_age(cached_metar).total_seconds() / 60.0) < DEFAULT_METAR_LIFESPAN_MINUTES: safe_log( logger, 'Immediately returning cached METAR for {}'.format( airport_icao_code)) safe_log(logger, '~get_metar() => {}'.format(cached_metar)) return cached_metar try: safe_log(logger, 'Getting single metar for {}'.format(airport_icao_code)) metars = get_metars([airport_icao_code], logger=logger) if metars is None: safe_log( logger, 'Get a None while attempting to get METAR for {}'.format( airport_icao_code)) safe_log(logger, '~get_metar() => None') return None if airport_icao_code not in metars: safe_log( logger, 'Got a result, but {} was not in results package'.format( airport_icao_code)) safe_log(logger, '~get_metar() => None') return None safe_log(logger, 'Returning METAR {}'.format(metars[airport_icao_code])) safe_log(logger, '~get_metar() => {}'.format(metars[airport_icao_code])) return metars[airport_icao_code] except Exception as e: safe_log(logger, 'get_metar got EX:{}'.format(e)) safe_log(logger, '~get_metar() => None') return None
if ceiling == INVALID or vis == INVALID: return INVALID if vis == SMOKE: return SMOKE if vis == LIFR or ceiling == LIFR: return LIFR if vis == IFR or ceiling == IFR: return IFR if vis == MVFR or ceiling == MVFR: return MVFR return VFR if __name__ == '__main__': safe_log(None, 'Starting self-test') airports_to_test = ['KW29', 'KMSN', 'KAWO', 'KOSH', 'KBVS', 'KDOESNTEXIST'] starting_date_time = datetime.utcnow() utc_offset = starting_date_time - datetime.now() get_category( 'KVOK', 'KVOK 251453Z 34004KT 10SM SCT008 OVC019 21/21 A2988 RMK AO2A SCT V BKN SLP119 53012' ) metars = get_metars(airports_to_test) get_metar('KAWO', use_cache=False) light_times = get_civil_twilight('KAWO', starting_date_time)
def get_civil_twilight(airport_icao_code, current_utc_time=None, use_cache=True, logger=None): """ Gets the civil twilight time for the given airport Arguments: airport_icao_code {string} -- The ICAO code of the airport. Returns: An array that describes the following: 0 - When sunrise starts 1 - when sunrise is 2 - when full light starts 3 - when full light ends 4 - when sunset starts 5 - when it is full dark """ safe_log( logger, 'get_civil_twilight({}, {}, {})'.format(airport_icao_code, current_utc_time, use_cache)) __light_fetch_lock__.acquire() try: if current_utc_time is None: current_utc_time = datetime.utcnow() is_cache_valid, cached_value = __is_cache_valid__( airport_icao_code, __daylight_cache__, 4 * 60) # Make sure that the sunrise time we are using is still valid... if is_cache_valid: hours_since_sunrise = (current_utc_time - cached_value[1]).total_seconds() / 3600 if hours_since_sunrise > 24: is_cache_valid = False safe_log_warning( logger, "Twilight cache for {} had a HARD miss with delta={}". format(airport_icao_code, hours_since_sunrise)) current_utc_time += timedelta(hours=1) if is_cache_valid and use_cache: safe_log(logger, 'Using cached value') safe_log(logger, '~get_civil_twilight() => {}'.format(cached_value)) return cached_value faa_code = get_faa_csv_identifier(airport_icao_code) if faa_code is None: safe_log( logger, 'Fall through due to the identifier not being in the FAA CSV file.' ) safe_log(logger, '~get_civil_twilight() => None') return None # Using "formatted=0" returns the times in a full datetime format # Otherwise you need to do some silly math to figure out the date # of the sunrise or sunset. url = "http://api.sunrise-sunset.org/json?lat=" + \ str(__airport_locations__[faa_code]["lat"]) + \ "&lng=" + str(__airport_locations__[faa_code]["long"]) + \ "&date=" + str(current_utc_time.year) + "-" + str(current_utc_time.month) + "-" + str(current_utc_time.day) + \ "&formatted=0" json_result = [] try: json_result = __rest_session__.get( url, timeout=DEFAULT_READ_SECONDS).json() except Exception as ex: safe_log_warning(logger, '~get_civil_twilight() => None; EX:{}'.format(ex)) return [] if json_result is not None and "status" in json_result and json_result[ "status"] == "OK" and "results" in json_result: sunrise = __get_utc_datetime__(json_result["results"]["sunrise"]) sunset = __get_utc_datetime__(json_result["results"]["sunset"]) sunrise_start = __get_utc_datetime__( json_result["results"]["civil_twilight_begin"]) sunset_end = __get_utc_datetime__( json_result["results"]["civil_twilight_end"]) sunrise_length = sunrise - sunrise_start sunset_length = sunset_end - sunset avg_transition_time = timedelta( seconds=(sunrise_length.seconds + sunset_length.seconds) / 2) sunrise_and_sunset = [ sunrise_start, sunrise, sunrise + avg_transition_time, sunset - avg_transition_time, sunset, sunset_end ] __set_cache__(airport_icao_code, __daylight_cache__, sunrise_and_sunset) safe_log(logger, 'Returning new value.') safe_log( logger, '~get_civil_twilight() => ({}, {}, {}, {}, {}, {})'.format( sunrise_and_sunset[0], sunrise_and_sunset[1], sunrise_and_sunset[2], sunrise_and_sunset[3], sunrise_and_sunset[4], sunrise_and_sunset[5])) return sunrise_and_sunset safe_log(logger, 'Fall through.') safe_log(logger, '~get_civil_twilight() => None') return None finally: __light_fetch_lock__.release()
spi_device = configuration.CONFIG["spi_device"] return ws2801.Ws2801Renderer(pixel_count, spi_port, spi_device) elif configuration.get_mode() == configuration.PWM: return led_pwm.LedPwmRenderer(airport_render_config) else: # "Normal" LEDs return led.LedRenderer(airport_render_config) renderer = get_test_renderer() if __name__ == '__main__': # Start loading the METARs in the background # while going through the self-test safe_log(LOGGER, "Testing all colors for all airports.") # Test LEDS on startup colors_to_test = (weather.LOW, weather.RED, weather.YELLOW, weather.GREEN, weather.BLUE, weather.WHITE, weather.GRAY, weather.DARK_YELLOW, weather.OFF) for color in colors_to_test: safe_log(LOGGER, "Setting to {}".format(color)) [ renderer.set_led(airport_render_config[airport], colors[color]) for airport in airport_render_config ] time.sleep(0.5)