diff --git a/SkywarnPlus.py b/SkywarnPlus.py index b2f4507..92f4414 100644 --- a/SkywarnPlus.py +++ b/SkywarnPlus.py @@ -60,6 +60,10 @@ sayalert_blocked_events = config["Blocking"].get("SayAlertBlockedEvents").split( tailmessage_blocked_events = ( config["Blocking"].get("TailmessageBlockedEvents").split(",") ) + +# Maximum number of alerts to process +max_alerts = config["Alerting"].getint("MaxAlerts", fallback=99) + # Configuration for tailmessage tailmessage_config = config["Tailmessage"] # Flag to enable/disable tailmessage @@ -271,7 +275,10 @@ def getAlerts(countyCodes): Returns: alerts (list): List of active weather alerts. """ - # Check if we need to inject alerts from the config + # Severity mappings + severity_mapping_api = {"Extreme": 4, "Severe": 3, "Moderate": 2, "Minor": 1, "Unknown": 0} + severity_mapping_words = {"Warning": 4, "Watch": 3, "Advisory": 2, "Statement": 1} + if config.getboolean("DEV", "INJECT", fallback=False): logger.debug("DEV Alert Injection Enabled") alerts = [ @@ -280,12 +287,13 @@ def getAlerts(countyCodes): logger.debug("Injecting alerts: {}".format(alerts)) return alerts - alerts = set() # Change list to set to automatically avoid duplicate alerts + alerts = [] current_time = datetime.now(timezone.utc) logger.debug("Checking for alerts in {}".format(countyCodes)) for countyCode in countyCodes: logger.debug("Checking for alerts in {}".format(countyCode)) - url = "https://api.weather.gov/alerts/active?zone={}".format(countyCode) + # url = "https://api.weather.gov/alerts/active?zone={}".format(countyCode) + url = "https://api.weather.gov/alerts/active?area=AR" # THIS RETURNS ALL ACTIVE ALERTS IN THE US logger.debug("Requesting {}".format(url)) response = requests.get(url) logger.debug("Response: {}\n\n".format(response.text)) @@ -293,10 +301,12 @@ def getAlerts(countyCodes): if response.status_code == 200: alert_data = response.json() for feature in alert_data["features"]: + effective = feature["properties"].get("effective") expires = feature["properties"].get("expires") - if expires: + if effective and expires: + effective_time = parser.isoparse(effective) expires_time = parser.isoparse(expires) - if expires_time > current_time: + if effective_time <= current_time < expires_time: event = feature["properties"]["event"] for global_blocked_event in global_blocked_events: if fnmatch.fnmatch(event, global_blocked_event): @@ -307,8 +317,14 @@ def getAlerts(countyCodes): ) break else: - alerts.add(event) # Add event to set - logger.debug("{}: {}".format(countyCode, event)) + severity = feature["properties"].get("severity") + if severity is None: + # Determine severity from the last word of the event if not provided + last_word = event.split()[-1] + severity = severity_mapping_words.get(last_word, 0) + else: + severity = severity_mapping_api.get(severity, 0) + alerts.append((event, severity)) # Add event to list as a tuple else: logger.error( "Failed to retrieve alerts for {}, HTTP status code {}, response: {}".format( @@ -316,8 +332,25 @@ def getAlerts(countyCodes): ) ) - alerts = list(alerts) # Convert set back to list - alerts.sort(key=lambda x: WS.index(x) if x in WS else len(WS)) + # Convert list to set to eliminate duplicates, then convert back to list + alerts = list(set(alerts)) + + # Sort by both API-provided severity and 'words' severity + alerts.sort( + key=lambda x: ( + x[1], # API-provided severity + severity_mapping_words.get(x[0].split()[-1], 0) # 'words' severity + ), + reverse=True + ) + + logger.debug("Sorted alerts: (alert), (severity)") + for alert in alerts: + logger.debug(alert) + + # Only keep the events (not the severities) + alerts = [alert[0] for alert in alerts[:max_alerts]] # Only keep the first 'max_alerts' alerts + return alerts @@ -339,8 +372,8 @@ def sayAlert(alerts): alert_count = 0 # Counter for alerts added to combined_sound for alert in alerts: - # Check if alert is in the SayAlertBlockedEvents list - if alert in sayalert_blocked_events: + # Check if alert matches any pattern in the SayAlertBlockedEvents list + if any(fnmatch.fnmatch(alert, blocked_event) for blocked_event in sayalert_blocked_events): logger.debug("SayAlert blocking {} as per configuration".format(alert)) continue @@ -420,8 +453,8 @@ def buildTailmessage(alerts): os.path.join(sounds_path, "ALERTS", "SWP95.wav") ) for alert in alerts: - # Check if alert is in the TailmessageBlockedEvents list - if alert in tailmessage_blocked_events: + # Check if alert matches any pattern in the TailmessageBlockedEvents list + if any(fnmatch.fnmatch(alert, blocked_event) for blocked_event in tailmessage_blocked_events): logger.debug("Alert blocked by TailmessageBlockedEvents: {}".format(alert)) continue diff --git a/config.ini b/config.ini index 88c6d3b..a6cf2ac 100644 --- a/config.ini +++ b/config.ini @@ -28,6 +28,16 @@ SayAlert = True ; Either True or False SayAllClear = True +; Specify an optional maximum number of alerts to be processed. +; SkywarnPlus will retrieve all local alerts from the NWS API +; and order them by level of severity. This setting will cause +; SkywarnPlus to only process the N most severe alerts. +; e.g. If the alerts in your area are... +; [Tornado Warning, Severe Thunderstorm Warning, Flood Watch, Special Weather Statement], +; and MaxAlerts = 2, then SkywarnPlus will only process the Tornado Warning and Severe Thunderstorm Warning. +; Example: MaxAlerts = 3 +MaxAlerts = 5 + ; Optional alternate path to the directory where sound files are stored ; Default is SkywarnPlus/SOUNDS ; Example: SoundsPath = /home/repeater/ @@ -37,17 +47,22 @@ SayAllClear = True [Blocking] ; GLOBAL BLOCKING - These alerts will be completely ignored and filtered out of the entire SkywarnPlus workflow ; CASE SENSITIVE list of events to ignore, comma separated. Wildcards can be used, e.g. *Statement, *Advisory -GlobalBlockedEvents = +; Example: GlobalBlockedEvents = *Statement, *Advisory +GlobalBlockedEvents = ; SayAlert Blocking ; These alerts will be blocked from being spoken when they are received ; These alerts will still be added to the tailmessage -SayAlertBlockedEvents = +; CASE SENSITIVE list of events to ignore, comma separated. +; Example: SayAlertBlockedEvents = *Statement, *Advisory +SayAlertBlockedEvents = ; Tailmessage Blocking ; These alerts will be blocked from being added to the tailmessage ; These alerts will still be spoken when they are received -TailmessageBlockedEvents = +; CASE SENSITIVE list of events to ignore, comma separated. +; Example: TailmessageBlockedEvents = *Statement, *Advisory +TailmessageBlockedEvents = ; Tail message settings [Tailmessage] @@ -99,10 +114,13 @@ RptLinkCT = CT-LINK.ulaw CTAlerts = Hurricane Force Wind Warning, Severe Thunderstorm Warning, Tropical Storm Warning, + Coastal Flood Warning, Winter Storm Warning, Thunderstorm Warning, Extreme Wind Warning, Storm Surge Warning, + Dust Storm Warning, + Avalanche Warning, Ice Storm Warning, Hurricane Warning, Blizzard Warning,