diff --git a/README.md b/README.md index 07c73f7..aba130a 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,15 @@ - [Understanding AlertScript](#understanding-alertscript) - [Matching](#matching) - [ClearCommands: Responding to Alert Clearance](#clearcommands-responding-to-alert-clearance) + - [Usage Note:](#usage-note) + - [Why Caution with Wildcards?](#why-caution-with-wildcards) + - [Best Practice:](#best-practice) + - [Transition-Based Commands](#transition-based-commands) + - [ActiveCommands](#activecommands) + - [Configuration Example for ActiveCommands:](#configuration-example-for-activecommands) + - [InactiveCommands](#inactivecommands) + - [Configuration Example for InactiveCommands:](#configuration-example-for-inactivecommands) + - [Implementing Transition-Based Commands](#implementing-transition-based-commands) - [The Power of YOU](#the-power-of-you) - [SkyDescribe](#skydescribe) - [Usage](#usage-1) @@ -641,6 +650,59 @@ For example: In the above configuration, when the alerts "Tornado Warning" AND "Tornado Watch" are detected, the DTMF macro `*123*456*789` will be executed. However, when there are no longer ANY alerts matching "Tornado Warning" OR "Tornado Watch", the DTMF macro `*987*654*321` will be executed. +### Usage Note: + +While `ClearCommands` enhances `AlertScript`'s functionality, it's important to exercise caution when using them with wildcard-based mappings (`*`). This is because such mappings may not behave as expected with `ClearCommands`, especially in complex alert scenarios where multiple alerts might be active simultaneously. + +### Why Caution with Wildcards? + +Wildcards offer broad matching capabilities, activating `Commands` for a wide range of alerts. However, when it comes to alert clearance (`ClearCommands`), the broad nature of wildcards can lead to unintended behaviors: + +- **Non-Specific Clearance**: Wildcards do not specify a single alert clearance that should trigger `ClearCommands`, potentially causing them to execute in unintended scenarios. +- **Overlap and Confusion**: In situations with multiple active alerts covered by a single wildcard trigger, identifying the precise moment for `ClearCommands` execution can become ambiguous and may not function as intended. + +### Best Practice: + +It's recommended to use specific mappings for `ClearCommands` to ensure precise and predictable behavior upon alert clearance. If using wildcards, be prepared for `ClearCommands` to potentially execute in broader circumstances than anticipated, and consider the overall context of your alert management strategy. + +## Transition-Based Commands + +`AlertScript` includes the capability to execute specific BASH or DTMF commands based on transitions in overall state of alert activity. + +### ActiveCommands + +`ActiveCommands` are designed to be executed when the system transitions from a state of having zero active weather alerts to a state where one or more alerts become active. This feature is particularly useful for signaling the onset of weather-related activities or conditions that warrant immediate attention or action. + +#### Configuration Example for ActiveCommands: + +```yaml +ActiveCommands: + - Type: BASH + Commands: + - 'echo "THE NUMBER OF ACTIVE ALERTS JUST CHANGED FROM ZERO TO NON-ZERO"' +``` + +In this example, a message is echoed whenever the system detects the first active weather alert after a period of no alerts. This could be adapted to activate lights, sounds, or other notification systems to alert of changing conditions. + +### InactiveCommands + +Conversely, `InactiveCommands` are triggered when the number of active weather alerts changes from one or more to zero. This transition indicates a return to a state of no immediate weather threats, and commands under this category can be used to deactivate alerts, reset systems, or notify personnel of the all-clear status. + +#### Configuration Example for InactiveCommands: + +```yaml +InactiveCommands: + - Type: BASH + Commands: + - 'echo "THE NUMBER OF ACTIVE ALERTS JUST CHANGED FROM NON-ZERO TO ZERO"' +``` + +This example would output a message signaling that all active weather alerts have been cleared. Similar to `ActiveCommands`, `InactiveCommands` can be customized to perform a wide range of actions, such as turning off alerting systems or sending an all-clear message through your communication channels. + +### Implementing Transition-Based Commands + +To utilize these new command types, simply add `ActiveCommands` and/or `InactiveCommands` to your `AlertScript` configuration in the `config.yaml` file, following the same format as other AlertScript mappings. This allows for both BASH and DTMF commands to be executed in response to changes in the alert status landscape, providing a dynamic and responsive alert management system. + ## The Power of YOU `AlertScript` derives its power from its versatility and extensibility. By providing the capacity to directly interface with your node's functionality through DTMF commands or bash scripts, you can effectively program the node to do virtually anything in response to a specific weather alert. diff --git a/SkywarnPlus.py b/SkywarnPlus.py index eb3b3b6..d17de9a 100644 --- a/SkywarnPlus.py +++ b/SkywarnPlus.py @@ -1119,108 +1119,147 @@ def alert_script(alerts): """ This function reads a list of alerts, then performs actions based on the alert triggers defined in the global configuration file. + It supports wildcard matching for triggers and includes functionality + to execute commands specifically when transitioning between zero and non-zero + active alerts. """ + LOGGER.debug("Starting alert_script with alerts: %s", alerts) # Load the saved state state = load_state() - processed_alerts = set( - state["alertscript_alerts"] - ) # Convert to a set for easier processing - active_alerts = set( - state.get("active_alerts", []) - ) # Load active alerts from state, also as a set + LOGGER.debug("Loaded state: %s", state) + + # Determine the previous and current count of active alerts + previous_active_count = len(state.get("active_alerts", [])) + LOGGER.debug("Previous active alerts count: %s", previous_active_count) + + processed_alerts = set(state["alertscript_alerts"]) # Convert to a set for easier processing + active_alerts = set(state.get("active_alerts", [])) # Load active alerts from state, also as a set + + LOGGER.debug("Processed alerts from state: %s", processed_alerts) + LOGGER.debug("Active alerts from state: %s", active_alerts) # Extract only the alert names from the OrderedDict keys alert_names = set([alert for alert in alerts.keys()]) + LOGGER.debug("Extracted alert names: %s", alert_names) # Identify new alerts and cleared alerts new_alerts = alert_names - active_alerts cleared_alerts = active_alerts - alert_names + LOGGER.debug("New alerts: %s", new_alerts) + LOGGER.debug("Cleared alerts: %s", cleared_alerts) + # Update the active alerts in the state - state["active_alerts"] = list( - alert_names - ) # Convert back to list for JSON serialization + state["active_alerts"] = list(alert_names) # Convert back to list for JSON serialization + LOGGER.debug("Updated active alerts in state: %s", state["active_alerts"]) # Fetch AlertScript configuration from global_config alertScript_config = config.get("AlertScript", {}) LOGGER.debug("AlertScript configuration: %s", alertScript_config) + # Determine the current count of active alerts after update + current_active_count = len(state["active_alerts"]) + LOGGER.debug("Current active alerts count: %s", current_active_count) + + # Check for transition from zero to non-zero active alerts and execute ActiveCommands + if previous_active_count == 0 and current_active_count > 0: + active_commands = alertScript_config.get("ActiveCommands", []) + if active_commands: + for command in active_commands: + if command["Type"].upper() == "BASH": + for cmd in command["Commands"]: + LOGGER.info("Executing Active BASH Command: %s", cmd) + subprocess.run(cmd, shell=True) + elif command["Type"].upper() == "DTMF": + for node in command["Nodes"]: + for cmd in command["Commands"]: + dtmf_cmd = 'asterisk -rx "rpt fun {} {}"'.format(node, cmd) + LOGGER.info("Executing Active DTMF Command: %s", dtmf_cmd) + subprocess.run(dtmf_cmd, shell=True) + + # Check for transition from non-zero to zero active alerts and execute InactiveCommands + if previous_active_count > 0 and current_active_count == 0: + inactive_commands = alertScript_config.get("InactiveCommands", []) + if inactive_commands: + for command in inactive_commands: + if command["Type"].upper() == "BASH": + for cmd in command["Commands"]: + LOGGER.info("Executing Inactive BASH Command: %s", cmd) + subprocess.run(cmd, shell=True) + elif command["Type"].upper() == "DTMF": + for node in command["Nodes"]: + for cmd in command["Commands"]: + dtmf_cmd = 'asterisk -rx "rpt fun {} {}"'.format(node, cmd) + LOGGER.info("Executing Inactive DTMF Command: %s", dtmf_cmd) + subprocess.run(dtmf_cmd, shell=True) + # Fetch Mappings from AlertScript configuration mappings = alertScript_config.get("Mappings", []) if mappings is None: mappings = [] LOGGER.debug("Mappings: %s", mappings) - # Process each mapping for new alerts + # Process each mapping for new alerts and issue a warning for wildcard clear commands for mapping in mappings: + if "*" in mapping.get("Triggers", []) and mapping.get("ClearCommands"): + LOGGER.warning("Using ClearCommands with wildcard-based mappings ('*') might not behave as expected for all alert clearances.") + + LOGGER.debug("Processing mapping: %s", mapping) triggers = mapping.get("Triggers", []) commands = mapping.get("Commands", []) nodes = mapping.get("Nodes", []) match_type = mapping.get("Match", "ANY").upper() - matched_alerts = [alert for alert in new_alerts if alert in triggers] + matched_alerts = [alert for alert in new_alerts if any(fnmatch.fnmatch(alert, trigger) for trigger in triggers)] + LOGGER.debug("Matched alerts for mapping: %s", matched_alerts) # Check if new alerts matched the triggers as per the match type - if ( - match_type == "ANY" - and matched_alerts - or match_type == "ALL" - and len(matched_alerts) == len(triggers) - ): + if (match_type == "ANY" and matched_alerts) or (match_type == "ALL" and len(matched_alerts) == len(triggers)): for alert in matched_alerts: processed_alerts.add(alert) + LOGGER.debug("Processing alert: %s", alert) if mapping.get("Type") == "BASH": for cmd in commands: - cmd = cmd.format( - alert_title=alert - ) # Replace placeholder with alert title + cmd = cmd.format(alert_title=alert) # Replace placeholder with alert title LOGGER.info("AlertScript: Executing BASH command: %s", cmd) subprocess.run(cmd, shell=True) elif mapping.get("Type") == "DTMF": for node in nodes: for cmd in commands: dtmf_cmd = 'asterisk -rx "rpt fun {} {}"'.format(node, cmd) - LOGGER.info( - "AlertScript: Executing DTMF command: %s", dtmf_cmd - ) + LOGGER.info("AlertScript: Executing DTMF command: %s", dtmf_cmd) subprocess.run(dtmf_cmd, shell=True) # Process each mapping for cleared alerts for mapping in mappings: + LOGGER.debug("Processing clear commands for mapping: %s", mapping) clear_commands = mapping.get("ClearCommands", []) triggers = mapping.get("Triggers", []) match_type = mapping.get("Match", "ANY").upper() - matched_cleared_alerts = [ - alert for alert in cleared_alerts if alert in triggers - ] + matched_cleared_alerts = [alert for alert in cleared_alerts if any(fnmatch.fnmatch(alert, trigger) for trigger in triggers)] + LOGGER.debug("Matched cleared alerts for mapping: %s", matched_cleared_alerts) # Check if cleared alerts matched the triggers as per the match type - if ( - match_type == "ANY" - and matched_cleared_alerts - or match_type == "ALL" - and len(matched_cleared_alerts) == len(triggers) - ): + if (match_type == "ANY" and matched_cleared_alerts) or (match_type == "ALL" and len(matched_cleared_alerts) == len(triggers)): for cmd in clear_commands: + LOGGER.debug("Executing clear command: %s", cmd) if mapping.get("Type") == "BASH": LOGGER.info("AlertScript: Executing BASH ClearCommand: %s", cmd) subprocess.run(cmd, shell=True) elif mapping.get("Type") == "DTMF": for node in mapping.get("Nodes", []): dtmf_cmd = 'asterisk -rx "rpt fun {} {}"'.format(node, cmd) - LOGGER.info( - "AlertScript: Executing DTMF ClearCommand: %s", dtmf_cmd - ) + LOGGER.info("AlertScript: Executing DTMF ClearCommand: %s", dtmf_cmd) subprocess.run(dtmf_cmd, shell=True) # Update the state with the alerts processed in this run - state["alertscript_alerts"] = list( - processed_alerts - ) # Convert back to list for JSON serialization + state["alertscript_alerts"] = list(processed_alerts) # Convert back to list for JSON serialization + LOGGER.debug("Saving state with processed alerts: %s", state["alertscript_alerts"]) save_state(state) + LOGGER.debug("Alert script execution completed.") def send_pushover(message, title=None, priority=0):