Add AlertScript wildcard support; Active + Inactive commands

pull/140/head
Mason10198 2 years ago
parent 7115c8f005
commit 758715218b

@ -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.

@ -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):

Loading…
Cancel
Save

Powered by TurnKey Linux.