Merge pull request #16 from Mason10198/develop

Develop
pull/17/head
Mason10198 3 years ago committed by GitHub
commit 674dd32926
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,113 +0,0 @@
#!/bin/bash
# CONTROL.sh
# A Control Script for SkywarnPlus v0.1.0
# by Mason Nelson (N5LSN/WRKF394)
#
#
# This script allows you to change the value of specific keys in the SkywarnPlus config.ini file.
# It's designed to enable or disable certain features of SkywarnPlus from the command line.
# It is case-insensitive, accepting both upper and lower case parameters.
#
# Usage: ./CONTROL.sh <key> <value>
# Example: ./CONTROL.sh sayalert false
# This will set 'SayAlert' to 'False' in the config.ini file.
#
# Supported keys:
# - enable: Enable or disable SkywarnPlus entirely. (Section: SKYWARNPLUS)
# - sayalert: Enable or disable instant alerting when weather alerts change. (Section: Alerting)
# - sayallclear: Enable or disable instant alerting when weather alerts are cleared. (Section: Alerting)
# - tailmessage: Enable or disable building of tail message. (Section: Tailmessage)
# - courtesytone: Enable or disable automatic courtesy tones. (Section: CourtesyTones)
#
# Supported values:
# - true: Enable the feature.
# - false: Disable the feature.
# - toggle: Toggle the feature.
#
# All changes will be made in the config.ini file located in the same directory as the script.
# First, we need to check if the correct number of arguments are passed
if [ "$#" -ne 2 ]; then
echo "Incorrect number of arguments. Please provide the key and the new value."
echo "Usage: $0 <key> <value>"
exit 1
fi
# Get the directory of the script
SCRIPT_DIR=$(dirname $(readlink -f $0))
CONFIG_FILE="${SCRIPT_DIR}/config.ini"
# Convert the input key into lowercase
KEY=$(echo "$1" | tr '[:upper:]' '[:lower:]')
# Convert the first character of the value to uppercase
VALUE=$(echo "$2" | awk '{for(i=1;i<=NF;i++)sub(/./,toupper(substr($i,1,1)),$i)}1')
# Make sure the provided value is either 'True' or 'False' or 'Toggle'
if [[ "${VALUE^^}" != "TRUE" && "${VALUE^^}" != "FALSE" && "${VALUE^^}" != "TOGGLE" ]]; then
echo "Invalid value. Please provide either 'true' or 'false' or 'toggle'."
exit 1
fi
# Define the command-line arguments and their corresponding keys in the configuration file
declare -A ARGUMENTS=( ["enable"]="Enable" ["sayalert"]="SayAlert" ["sayallclear"]="SayAllClear" ["tailmessage"]="Enable" ["courtesytone"]="Enable")
# Define the sections in the configuration file that each key belongs to
declare -A SECTIONS=( ["enable"]="SKYWARNPLUS" ["sayalert"]="Alerting" ["sayallclear"]="Alerting" ["tailmessage"]="Tailmessage" ["courtesytone"]="CourtesyTones")
# Define the audio files associated with each key
declare -A AUDIO_FILES_ENABLED=( ["enable"]="SWP85.wav" ["sayalert"]="SWP87.wav" ["sayallclear"]="SWP89.wav" ["tailmessage"]="SWP91.wav" ["courtesytone"]="SWP93.wav")
declare -A AUDIO_FILES_DISABLED=( ["enable"]="SWP86.wav" ["sayalert"]="SWP88.wav" ["sayallclear"]="SWP90.wav" ["tailmessage"]="SWP92.wav" ["courtesytone"]="SWP94.wav")
# Read the node number and path to SOUNDS directory from the config.ini
NODES=$(awk -F " = " '/^Nodes/ {print $2}' "${SCRIPT_DIR}/config.ini" | tr -d ' ' | tr ',' '\n')
# Check if the input key is valid
if [[ ${ARGUMENTS[$KEY]+_} ]]; then
# Get the corresponding key in the configuration file
CONFIG_KEY=${ARGUMENTS[$KEY]}
# Get the section that the key belongs to
SECTION=${SECTIONS[$KEY]}
if [[ "${VALUE^^}" = "TOGGLE" ]]; then
CONFIG_VALUE=$(awk -F "=" -v section="$SECTION" -v key="$KEY" '
BEGIN {RS=";"; FS="="}
$0 ~ "\\[" section "\\]" {flag=1}
flag && $1 ~ key {gsub(/ /, "", $2); print toupper($2); exit}
$0 ~ "\\[" && $0 !~ "\\[" section "\\]" {flag=0}' "$CONFIG_FILE")
# Remove leading and trailing whitespace
CURRENT_VALUE=$(echo $CONFIG_VALUE | xargs)
if [ "$CURRENT_VALUE" == "TRUE" ]; then
NEW_VALUE="False"
elif [ "$CURRENT_VALUE" == "FALSE" ]; then
NEW_VALUE="True"
else
echo "Could not determine current value. Exiting."
exit 1
fi
VALUE=$NEW_VALUE
fi
# Update the value of the key in the configuration file
sed -i "/^\[${SECTION}\]/,/^\[/{s/^${CONFIG_KEY} = .*/${CONFIG_KEY} = ${VALUE}/}" "${SCRIPT_DIR}/config.ini"
# Get the correct audio file based on the new value
if [ "$VALUE" = "True" ]; then
AUDIO_FILE=${AUDIO_FILES_ENABLED[$KEY]}
else
AUDIO_FILE=${AUDIO_FILES_DISABLED[$KEY]}
fi
# Play the corresponding audio message on all nodes
for NODE in $NODES; do
/usr/sbin/asterisk -rx "rpt localplay ${NODE} ${SCRIPT_DIR}/SOUNDS/ALERTS/${AUDIO_FILE%.*}"
done
else
echo "The provided key does not match any configurable item."
exit 1
fi

@ -1,48 +1,34 @@
# SkywarnPlus
# SkywarnPlus: Your Advanced Weather Alert System
SkywarnPlus is an optimized, powerful weather alert system designed for Asterisk/app_rpt repeater controller systems such as [AllStarLink](https://allstarlink.org/) and [HAMVOIP](https://hamvoip.org/). It's written in Python and utilizes the new [NWS CAP v1.2 JSON API](https://www.weather.gov/documentation/services-web-api). SkywarnPlus is optimized to be resource-efficient and offers customization options to suit various user needs.
SkywarnPlus is a sophisticated software solution that works hand-in-hand with your AllStarLink (Debian) or HAMVOIP (Arch) node to keep you informed and ready for whatever the weather brings. Combining weather data with intuitive features, SkywarnPlus optimizes the efficiency and functionality of your node.
Tested on ASL 1.01, ASL 2.0.0, and HAMVOIP 1.7-01.
## Key Features
## Features
- **Seamless Integration:** SkywarnPlus operates on a Debian (AllStarLink) or Arch (HAMVOIP) node.
- **Human Speech**: Provides a library of recorded human speech for clearer, more understandable alerts.
- **Performance**: Designed for minimal impact on internet bandwidth and storage, reducing unnecessary I/O operations.
- **Alert Coverage**: Allows specifying multiple counties for alerts, ensuring broad coverage.
- **Alert Priority**: Alerts are automatically sorted by severity (Warning, Watch, Advisory, Statement), so you always hear the most important alerts first.
- **Alert Filtering**: Provides advanced options to block or filter alerts from specific functions using regular expressions and wildcards.
- **Remote Control**: Includes a control script that can be mapped to DTMF commands, allowing instant over-the-air control of your system.
- **Automatic Courtesy Tones**: Changes repeater courtesy tones based on active alerts.
- **Duplicate Filtering**: Ensures the same alert is never broadcast twice.
- **Selective Broadcasting**: Broadcasts alerts on weather conditions' onset or dissipation.
- **Tailmessage Management**: Provides unobtrusive alerting if alert broadcasting is disabled.
- **Pushover Integration**: Sends alerts and debug messages directly to your phone.
- **Multiple Nodes**: Supports alert distribution to as many local node numbers as desired.
- **Developer Options**: Provides a testing environment to inject manually defined alerts for testing how the system functions.
- **Real-Time Weather Alerts:** The software checks the NWS CAP v1.2 API for live weather alerts for user-defined areas.
## How It Works
- **Unlimited Area & Node Numbers:** Users can define as many areas and local node numbers as desired.
SkywarnPlus is a Python-based weather alert system for Asterisk/app_rpt repeater controller systems, leveraging the National Weather Service's (NWS) CAP v1.2 JSON API. The system follows several key steps to deliver timely and accurate weather alerts:
- **Automatic Announcements:** Weather alerts, including when all warnings have been cleared, are announced automatically on the node.
1. **Data Fetching**: The system performs regular API calls to the NWS CAP v1.2 API, which provides comprehensive, real-time data on the latest weather conditions and alerts. The frequency of these calls can be adjusted according to user needs.
- **Tailmessage Creation:** The software generates tailmessages for the node to continuously inform listeners about active alerts after the initial broadcast.
2. **Data Parsing**: Upon receiving the API response, SkywarnPlus parses the JSON data to extract the information pertinent to weather alerts. This involves reading the structured JSON data and converting it into an internal format for further processing.
- **Dynamic Changes to Node:** Courtesy tones and node CW / voice ID automatically change according to user-defined alerts, optimizing communication during severe weather.
3. **Data Filtering**: The extracted data is then filtered based on user-defined criteria set in the configuration file. This includes narrowing down the information to specific counties of interest, as well as excluding certain types of alerts. The filtering mechanism supports regular expressions and wildcards for more sophisticated filtering rules.
- **Human Speech:** Announcements are delivered in a natural, human speech for easier understanding.
4. **Alert Management**: SkywarnPlus manages the filtered alerts intelligently, ensuring that each alert is unique and relevant. Duplicate alerts are automatically removed from the pool of active alerts to prevent repetition and alert fatigue.
- **Efficiency & Speed:** SkywarnPlus is optimized for speed and efficiency to provide real-time information without delay.
5. **Alert Broadcasting**: The system then broadcasts the alerts according to user-defined settings. You can customize these settings to broadcast alerts when new weather conditions are detected or when existing conditions dissipate. This ensures timely communication of weather changes.
- **Preserves Hardware:** SkywarnPlus limits I/O to the physical disk, preventing SD card burnout in Raspberry Pi devices.
6. **Tailmessage and Courtesy Tones**: In addition to broadcasting alerts, SkywarnPlus also automatically updates tailmessages and changes the repeater courtesy tones when specific alerts are active. These changes add a level of customization and context-awareness to the alert system and can be tailored to individual preferences.
- **Remote Control:** Functions can be mapped to DTMF commands for remote over-the-air control.
7. **Pushover Integration**: SkywarnPlus integrates with Pushover, a mobile notification service, to send alerts and debug messages directly to your phone. This provides a direct and immediate communication channel, keeping you constantly updated on the latest weather conditions.
- **Highly Customizable:** SkywarnPlus is extremely customizable, offering advanced filtering parameters to block certain alerts or types of alerts from different functions. Users can even map DTMF macros or shell commands to specified weather alerts, expanding the software's capabilities according to user needs.
8. **Real Human Speech**: To enhance clarity and improve user experience, SkywarnPlus uses a library of real female human speech recordings for alerts. This creates a more natural listening experience compared to synthetic speech and aids in clear communication of alert messages.
- **Pushover Integration:** With Pushover integration, SkywarnPlus can send weather alert notifications directly to your phone or other devices.
9. **Maintenance and Resource Management**: Designed with efficiency in mind, SkywarnPlus minimizes its impact on internet bandwidth and physical storage. The system conducts its operations mindful of resource usage, making it particularly suitable for devices with limited resources, such as Raspberry Pi.
This combination of steps ensures SkywarnPlus provides reliable, timely, and accurate weather alerts, while respecting your system's resources and providing extensive customization options.
Whether you wish to auto-link to a Skywarn net during severe weather, program your node to control an external device like a siren during a tornado warning, or simply want to stay updated on changing weather conditions, SkywarnPlus offers a comprehensive, efficient, and customizable solution for your weather alert needs.
# Installation
@ -60,7 +46,7 @@ Follow the steps below to install:
apt update
apt upgrade
apt install unzip python3 python3-pip ffmpeg
pip3 install requests python-dateutil pydub
pip3 install pyyaml requests python-dateutil pydub
```
**Arch (HAMVOIP)**
@ -71,7 +57,7 @@ Follow the steps below to install:
pacman -S ffmpeg
wget https://bootstrap.pypa.io/pip/3.5/get-pip.py
python get-pip.py
pip install requests python-dateutil pydub
pip install pyyaml requests python-dateutil pydub
```
2. **Download SkywarnPlus**
@ -85,21 +71,22 @@ Follow the steps below to install:
rm SkywarnPlus.zip
```
3. **Configure CONTROL.sh Permissions**
3. **Configure Permissions**
The CONTROL.sh script must be made executable. Use the chmod command to change the file permissions:
The scripts must be made executable. Use the chmod command to change the file permissions:
```bash
cd SkywarnPlus
chmod +x CONTROL.sh
chmod +x SkywarnPlus.py
chmod +x SkyControl.py
```
4. **Edit Configuration**
Edit the [config.ini](config.ini) file according to your needs. This is where you will enter your NWS codes, enable/disable specific functions, etc.
Edit the [config.yaml](config.yaml) file according to your needs. This is where you will enter your NWS codes, enable/disable specific functions, etc.
```bash
nano config.ini
nano config.yaml
```
You can find your area code(s) at https://alerts.weather.gov/. Select `County List` to the right of your state, and use the `County Code` associated with the area(s) you want SkywarnPlus to poll for WX alerts.
@ -121,18 +108,18 @@ Follow the steps below to install:
Add a crontab entry to call SkywarnPlus on an interval. Open your crontab file using the `crontab -e` command, and add the following line:
```bash
* * * * * /usr/bin/python3 /usr/local/bin/SkywarnPlus/SkywarnPlus.py
* * * * * /usr/local/bin/SkywarnPlus/SkywarnPlus.py
```
This command will execute SkywarnPlus (poll NWS API for data) every minute.
# Tailmessage and Automatic Courtesy Tones
# Tailmessage, Courtesy Tones, & IDs
SkywarnPlus offers functionalities such as Tailmessage management and Automatic Courtesy Tones, which require specific configurations in the `rpt.conf` file.
SkywarnPlus can automatically change and manage tailmessages, courtesy tones, and CW / voice IDs on your node. These functions require specific configurations in the `rpt.conf` file.
## Tailmessage
Tailmessage functionality requires the `rpt.conf` to be properly set up. Here's an example:
SkywarnPlus can automatically create, manage, and remove a tailmessage whenever certain weather alerts are active to keep listeners informed throught the duration of active alerts. The configuration for this is based on your `rpt.conf` file setup. Here's an example:
```ini
tailmessagetime = 600000
@ -140,9 +127,9 @@ tailsquashedtime = 30000
tailmessagelist = /usr/local/bin/SkywarnPlus/SOUNDS/wx-tail
```
## Automatic Courtesy Tones
## Courtesy Tones
SkywarnPlus can automatically change the repeater courtesy tone whenever certain weather alerts are active. The configuration for this is based on your `rpt.conf` file setup. Here's an example:
SkywarnPlus can automatically change the node courtesy tone whenever certain weather alerts are active. The configuration for this is based on your `rpt.conf` file setup. Here's an example:
```ini
[NODENUMBER]
@ -155,7 +142,12 @@ ct2 = /usr/local/bin/SkywarnPlus/SOUNDS/TONES/CT-LINK
remotetx = /usr/local/bin/SkywarnPlus/SOUNDS/TONES/CT-LOCAL
```
Courtesy tone files are located in `SOUNDS/TONES` by default and are configured through `config.ini` and `rpt.conf`.
## CW / Voice IDs
SkywarnPlus can automatically change the node ID whenever certain weather alerts are active. The configuration for this is based on your `rpt.conf` file setup. Here's an example:
```ini
[NODENUMBER]
idrecording = /usr/local/bin/SkywarnPlus/SOUNDS/ID/ID
```
# Pushover Integration
@ -164,15 +156,15 @@ SkywarnPlus can use the free Pushover API to send WX alert notifications and deb
1. Visit https://pushover.net/ to sign up for a free account.
2. Find your UserKey on your main dashboard
3. Scroll down and create an Application/API key for your node
4. Add UserKey & API Key to `config.ini`
4. Add UserKey & API Key to `config.yaml`
# Control Script
SkywarnPlus comes with a powerful control script (`CONTROL.sh`) that can be used to enable or disable certain SkywarnPlus functions. This script is particularly useful when you want to map DTMF control codes to these functions. An added advantage is that the script provides spoken feedback upon execution, making it even more suitable for DTMF control.
SkywarnPlus comes with a powerful control script (`SkyControl.py`) that can be used to enable or disable certain SkywarnPlus functions via shell, without manually editing `config.yaml`. This script is particularly useful when you want to map DTMF control codes to these functions. An added advantage is that the script provides spoken feedback upon execution, making it even more suitable for DTMF control.
## Usage
To use the CONTROL.sh script, you need to call it with two parameters:
To use the `SkyControl.py` script, you need to call it with two parameters:
1. The name of the setting you want to change (case insensitive).
@ -181,77 +173,122 @@ To use the CONTROL.sh script, you need to call it with two parameters:
- SayAllClear
- TailMessage
- CourtesyTone
-
2. The new value for the setting (either 'true' or 'false' or 'toggle').
For example, to completely disable SkywarnPlus, you would use:
```bash
/usr/local/bin/SkywarnPlus/CONTROL.sh enable false
/usr/local/bin/SkywarnPlus/SkyControl.py enable false
```
And to reenable it, you would use:
```bash
/usr/local/bin/SkywarnPlus/CONTROL.sh enable true
/usr/local/bin/SkywarnPlus/SkyControl.py enable true
```
And to toggle it, you would use:
```bash
/usr/local/bin/SkywarnPlus/CONTROL.sh enable toggle
/usr/local/bin/SkywarnPlus/SkyControl.py enable toggle
```
## Spoken Feedback
Upon the successful execution of a control command, the `CONTROL.sh` script will provide spoken feedback that corresponds to the change made. For instance, if you execute a command to enable the SayAlert function, the script will play an audio message stating that SayAlert has been enabled. This feature enhances user experience and confirms that the desired changes have been effected.
Upon the successful execution of a control command, the `SkyControl.py` script will provide spoken feedback that corresponds to the change made. For instance, if you execute a command to enable the SayAlert function, the script will play an audio message stating that SayAlert has been enabled. This feature enhances user experience and confirms that the desired changes have been effected.
## Mapping to DTMF Commands
You can map the CONTROL.sh script to DTMF commands in the `rpt.conf` file of your node. Here is an example of how to do this:
You can map the `SkyControl.py` script to DTMF commands in the `rpt.conf` file of your node. Here is an example of how to do this:
```bash
801 = cmd,/usr/local/bin/SkywarnPlus/CONTROL.sh enable toggle ; Toggles SkywarnPlus
802 = cmd,/usr/local/bin/SkywarnPlus/CONTROL.sh sayalert toggle ; Toggles SayAlert
803 = cmd,/usr/local/bin/SkywarnPlus/CONTROL.sh sayallclear toggle ; Toggles SayAllClear
804 = cmd,/usr/local/bin/SkywarnPlus/CONTROL.sh tailmessage toggle ; Toggles TailMessage
805 = cmd,/usr/local/bin/SkywarnPlus/CONTROL.sh courtesytone toggle ; Toggles CourtesyTone
801 = cmd,/usr/local/bin/SkywarnPlus/SkyControl.py enable toggle ; Toggles SkywarnPlus
802 = cmd,/usr/local/bin/SkywarnPlus/SkyControl.py sayalert toggle ; Toggles SayAlert
803 = cmd,/usr/local/bin/SkywarnPlus/SkyControl.py sayallclear toggle ; Toggles SayAllClear
804 = cmd,/usr/local/bin/SkywarnPlus/SkyControl.py tailmessage toggle ; Toggles TailMessage
805 = cmd,/usr/local/bin/SkywarnPlus/SkyControl.py courtesytone toggle ; Toggles CourtesyTone
806 = cmd,/usr/local/bin/SkywarnPlus/SkyControl.py alertscript toggle ; Toggles AlertScript
807 = cmd,/usr/local/bin/SkywarnPlus/SkyControl.py idchange toggle ; Toggles IDChange
```
With this setup, you can control SkywarnPlus' functionality using DTMF commands.
# AlertScript
SkywarnPlus's `AlertScript` feature is an immensely flexible tool that provides the ability to program your node to respond to specific alerts in unique ways. By enabling you to map alerts to DTMF commands or bash scripts, `AlertScript` offers you the versatility to design your own extensions to SkywarnPlus, modifying its functionalities to perfectly fit your needs.
With `AlertScript`, you can outline actions to be executed when specific alerts are activated. For instance, you might want to broadcast a unique sound, deliver a particular message, or initiate any other action your hardware can perform and that can be activated by a DTMF command or bash script.
## Understanding AlertScript
To utilize `AlertScript`, you define the mapping of alerts to either DTMF commands or bash scripts in the `config.yaml` file under the `AlertScript` section.
Here are examples of how to map alerts to DTMF commands or bash scripts:
```yaml
AlertScript:
Enable: true
Mappings:
- Type: DTMF
Nodes:
- <NODE_NUMBERS>
Commands:
- '<DTMF_COMMAND>'
Triggers:
- <ALERTS>
Match: ALL # or ANY
- Type: BASH
Commands:
- '<BASH_COMMAND>'
Triggers:
- <ALERTS>
```
In the examples above, `<NODE_NUMBERS>` are the nodes where you want the DTMF command to be dispatched, `<DTMF_COMMAND>` is the command to be executed, and `<ALERTS>` are the alerts to trigger this command. Likewise, for bash commands, `<BASH_COMMAND>` is the script to be executed and `<ALERTS>` are the alerts to trigger this script. Note that wildcards (`*`) can be used in `<ALERTS>` for broader matches.
## 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.
Fancy activating a siren when a tornado warning is received? You can do that. Want to send an email notification when there's a severe thunderstorm warning? You can do that too. The only limit is the capability of your node and connected systems.
In essence, `AlertScript` unleashes a world of customization possibilities, empowering you to add new capabilities to SkywarnPlus, create your own extensions, and modify your setup to align with your specific requirements and preferences. By giving you the authority to dictate how your system should react to various weather alerts, `AlertScript` makes SkywarnPlus a truly powerful tool for managing weather alerts on your node.
# Customizing the Audio Files
SkywarnPlus comes with a library of audio files that can be replaced with any 8kHz mono PCM16 WAV files you want. These are found in the `SOUNDS/` directory by default, along with `DICTIONARY.txt` which explains audio file assignments.
If you'd like to enable IDChange, you must create your own ID audio files. Follow **[this guide](https://wiki.allstarlink.org/images/d/dd/RecordingSoundFiles.pdf)** on how to create audio files for use with Asterisk/app_rpt.
# Testing
SkywarnPlus provides the ability to inject predefined alerts, bypassing the call to the NWS API. This feature is extremely useful for testing SkywarnPlus.
To enable this option, modify the following settings in the `[DEV]` section of your `config.ini` file:
To enable this option, modify the following settings in the `[DEV]` section of your `config.yaml` file:
```ini
; Enable to inject the below list of test alerts instead of calling the NWS API
INJECT = True
; CASE SENSITIVE, comma & newline separated list of alerts to inject
INJECTALERTS = Tornado Warning,
Tornado Watch,
Severe Thunderstorm Warning
```yaml
# Enable test alert injection instead of calling the NWS API by setting 'INJECT' to 'True'.
INJECT: false
# List the test alerts to inject. Use a case-sensitive list. One alert per line for better readability.
INJECTALERTS:
- Tornado Warning
- Tornado Watch
- Severe Thunderstorm Warning
```
# Debugging
Debugging is an essential part of diagnosing issues with SkywarnPlus. To facilitate this, SkywarnPlus provides a built-in debugging feature. Here's how to use it:
1. **Enable Debugging**: The debugging feature can be enabled in the `config.ini` file. Open this file and set the `debug` option under the `[SkywarnPlus]` section to `true`.
1. **Enable Debugging**: The debugging feature can be enabled in the `config.yaml` file. Open this file and set the `debug` option under the `[SkywarnPlus]` section to `true`.
```ini
; Logging Options
[Logging]
; Enable more verbose logging
; Either True or False
Debug = False
```yaml
Logging:
# Configuration for logging options.
# Enable verbose logging by setting 'Debug' to 'True'.
Debug: false
```
This will allow the program to output detailed information about its operations, which is helpful for identifying any issues or errors.

@ -68,6 +68,10 @@ SWP59.wav: Heat Watch
SWP60.wav: Freeze Watch
SWP61.wav: Dense Smoke Advisory
SWP62.wav: Avalanche Warning
SWP81.wav: AlertScript Enabled
SWP82.wav: AlertScript Disabled
SWP83.wav: IDChange Enabled
SWP84.wav: IDChange Disabled
SWP85.wav: SkywarnPlus Enabled
SWP86.wav: SkywarnPlus Disabled
SWP87.wav: SayAlert Enabled

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

@ -0,0 +1,81 @@
#!/usr/bin/python3
# SkyControl.py
# A Control Script for SkywarnPlus v0.2.0
# by Mason Nelson (N5LSN/WRKF394)
#
# This script allows you to change the value of specific keys in the SkywarnPlus config.yaml file.
# It's designed to enable or disable certain features of SkywarnPlus from the command line.
# It is case-insensitive, accepting both upper and lower case parameters.
#
# Usage: python3 SkyControl.py <key> <value>
# Example: python3 SkyControl.py sayalert false
# This will set 'SayAlert' to 'False' in the config.yaml file.
import sys
import yaml
import subprocess
from pathlib import Path
# Define valid keys and corresponding audio files
VALID_KEYS = {
"enable": {"key": "Enable", "section": "SKYWARNPLUS", "true_file": "SWP85.wav", "false_file": "SWP86.wav"},
"sayalert": {"key": "SayAlert", "section": "Alerting", "true_file": "SWP87.wav", "false_file": "SWP88.wav"},
"sayallclear": {"key": "SayAllClear", "section": "Alerting", "true_file": "SWP89.wav", "false_file": "SWP90.wav"},
"tailmessage": {"key": "Enable", "section": "Tailmessage", "true_file": "SWP91.wav", "false_file": "SWP92.wav"},
"courtesytone": {"key": "Enable", "section": "CourtesyTones", "true_file": "SWP93.wav", "false_file": "SWP94.wav"},
"alertscript": {"key": "Enable", "section": "AlertScript", "true_file": "SWP81.wav", "false_file": "SWP82.wav"},
"idchange": {"key": "Enable", "section": "IDChange", "true_file": "SWP83.wav", "false_file": "SWP84.wav"},
}
# Get the directory of the script
SCRIPT_DIR = Path(__file__).parent.absolute()
# Get the configuration file
CONFIG_FILE = SCRIPT_DIR / "config.yaml"
# Check if the correct number of arguments are passed
if len(sys.argv) != 3:
print("Incorrect number of arguments. Please provide the key and the new value.")
print("Usage: python3 {} <key> <value>".format(sys.argv[0]))
sys.exit(1)
# The input key and value
key, value = sys.argv[1:3]
# Make sure the provided key is valid
if key not in VALID_KEYS:
print("The provided key does not match any configurable item.")
sys.exit(1)
# Make sure the provided value is either 'true', 'false' or 'toggle'
if value not in ['true', 'false', 'toggle']:
print("Invalid value. Please provide either 'true' or 'false' or 'toggle'.")
sys.exit(1)
# Convert the input value to boolean if not 'toggle'
if value != 'toggle':
value = value.lower() == 'true'
# Load the config file
with open(str(CONFIG_FILE), 'r') as f:
config = yaml.safe_load(f)
# Check if toggle is required
if value == 'toggle':
current_value = config[VALID_KEYS[key]['section']][VALID_KEYS[key]['key']]
value = not current_value
# Update the key in the config
config[VALID_KEYS[key]['section']][VALID_KEYS[key]['key']] = value
# Save the updated config back to the file
with open(str(CONFIG_FILE), 'w') as f:
yaml.dump(config, f)
# Get the correct audio file based on the new value
audio_file = VALID_KEYS[key]['true_file'] if value else VALID_KEYS[key]['false_file']
# Play the corresponding audio message on all nodes
nodes = config['Asterisk']['Nodes']
for node in nodes:
subprocess.run(['/usr/sbin/asterisk', '-rx', 'rpt localplay {} {}/SOUNDS/ALERTS/{}'.format(node, SCRIPT_DIR, audio_file.rsplit(".", 1)[0])])

@ -1,7 +1,7 @@
#!/usr/bin/env python3
#!/usr/bin/python3
"""
SkywarnPlus v0.1.0 by Mason Nelson (N5LSN/WRKF394)
SkywarnPlus v0.2.0 by Mason Nelson (N5LSN/WRKF394)
==================================================
SkywarnPlus is a utility that retrieves severe weather alerts from the National
Weather Service and integrates these alerts with an Asterisk/app_rpt based
@ -24,58 +24,77 @@ import os
import json
import logging
import requests
import configparser
import shutil
import fnmatch
import subprocess
import time
import yaml
from datetime import datetime, timezone
from dateutil import parser
from pydub import AudioSegment
# Configuration file handling
# Directories and Paths
baseDir = os.path.dirname(os.path.realpath(__file__))
configPath = os.path.join(baseDir, "config.ini")
config = configparser.ConfigParser()
config.read_file(open(configPath, "r"))
configPath = os.path.join(baseDir, "config.yaml")
# Fetch values from configuration file
master_enable = config["SKYWARNPLUS"].getboolean("Enable", fallback=False)
# Open and read configuration file
with open(configPath, "r") as config_file:
config = yaml.safe_load(config_file)
# Check if SkywarnPlus is enabled
master_enable = config.get("SKYWARNPLUS", {}).get("Enable", False)
if not master_enable:
print("SkywarnPlus is disabled in config.ini, exiting...")
print("SkywarnPlus is disabled in config.yaml, exiting...")
exit()
tmp_dir = config["DEV"].get("TmpDir", fallback="/tmp/SkywarnPlus")
sounds_path = config["Alerting"].get("SoundsPath", fallback="./SOUNDS")
if sounds_path == "./SOUNDS":
sounds_path = os.path.join(baseDir, "SOUNDS")
countyCodes = config["Alerting"]["CountyCodes"].split(",")
# If temporary directory doesn't exist, create it
if not os.path.exists(tmp_dir):
os.makedirs(tmp_dir)
# List of blocked events
global_blocked_events = config["Blocking"].get("GlobalBlockedEvents").split(",")
sayalert_blocked_events = config["Blocking"].get("SayAlertBlockedEvents").split(",")
# Define tmp_dir and sounds_path
tmp_dir = config.get("DEV", {}).get("TmpDir", "/tmp/SkywarnPlus")
sounds_path = config.get("Alerting", {}).get("SoundsPath", os.path.join(baseDir, "SOUNDS"))
# Define countyCodes
countyCodes = config.get("Alerting", {}).get("CountyCodes", [])
# Create tmp_dir if it doesn't exist
if tmp_dir:
os.makedirs(tmp_dir, exist_ok=True)
else:
print("Error: tmp_dir is not set.")
# Define Blocked events
global_blocked_events = (
config.get("Blocking", {}).get("GlobalBlockedEvents", [])
)
if global_blocked_events is None:
global_blocked_events = []
sayalert_blocked_events = (
config.get("Blocking", {}).get("SayAlertBlockedEvents", [])
)
if sayalert_blocked_events is None:
sayalert_blocked_events = []
tailmessage_blocked_events = (
config["Blocking"].get("TailmessageBlockedEvents").split(",")
config.get("Blocking", {}).get("TailmessageBlockedEvents", [])
)
if tailmessage_blocked_events is None:
tailmessage_blocked_events = []
# Maximum number of alerts to process
max_alerts = config["Alerting"].getint("MaxAlerts", fallback=99)
# Define Max Alerts
max_alerts = config.get("Alerting", {}).get("MaxAlerts", 99)
# Configuration for tailmessage
tailmessage_config = config["Tailmessage"]
# Flag to enable/disable tailmessage
enable_tailmessage = tailmessage_config.getboolean("Enable", fallback=False)
# Path to tailmessage file
# Define Tailmessage configuration
tailmessage_config = config.get("Tailmessage", {})
enable_tailmessage = tailmessage_config.get("Enable", False)
tailmessage_file = tailmessage_config.get(
"TailmessagePath", fallback="./SOUNDS/wx-tail.wav"
"TailmessagePath", os.path.join(sounds_path, "wx-tail.wav")
)
if tailmessage_file == "./SOUNDS/wx-tail.wav":
tailmessage_file = os.path.join(baseDir, "SOUNDS/wx-tail.wav")
# Warning and announcement strings
# Define IDChange configuration
idchange_config = config.get("IDChange", {})
enable_idchange = idchange_config.get("Enable", False)
# Data file path
data_file = os.path.join(tmp_dir, "data.json")
# Define Warning and Announcement strings
WS = [
"Hurricane Force Wind Warning",
"Severe Thunderstorm Warning",
@ -235,34 +254,77 @@ WA = [
"99",
]
# Cleanup flag for testing
CLEANSLATE = config["DEV"].get("CLEANSLATE")
if CLEANSLATE == "True":
# Test if the script needs to start from a clean slate
CLEANSLATE = config.get("DEV", {}).get("CLEANSLATE", False)
if CLEANSLATE:
shutil.rmtree(tmp_dir)
os.mkdir(tmp_dir)
# Configure logging
log_config = config["Logging"]
enable_debug = log_config.getboolean("Debug", fallback=False)
log_file = log_config.get("LogPath", fallback="{}/SkywarnPlus.log".format(tmp_dir))
# Logging setup
log_config = config.get("Logging", {})
enable_debug = log_config.get("Debug", False)
log_file = log_config.get("LogPath", os.path.join(tmp_dir, "SkywarnPlus.log"))
# Set up logging
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG if enable_debug else logging.INFO)
# Set up log message formatting
log_formatter = logging.Formatter("%(asctime)s %(levelname)s %(message)s")
# Set up console log handler
c_handler = logging.StreamHandler()
f_handler = logging.FileHandler(log_file)
c_format = f_format = logging.Formatter("%(asctime)s %(levelname)s %(message)s")
c_handler.setFormatter(c_format)
f_handler.setFormatter(f_format)
c_handler.setFormatter(log_formatter)
logger.addHandler(c_handler)
# Set up file log handler
f_handler = logging.FileHandler(log_file)
f_handler.setFormatter(log_formatter)
logger.addHandler(f_handler)
# Debugging stuff
logger.debug("Base directory: {}".format(baseDir))
logger.debug("Temporary directory: {}".format(tmp_dir))
logger.debug("Sounds path: {}".format(sounds_path))
logger.debug("Tailmessage path: {}".format(tailmessage_file))
logger.debug("Global Blocked events: {}".format(global_blocked_events))
logger.debug("SayAlert Blocked events: {}".format(sayalert_blocked_events))
logger.debug("Tailmessage Blocked events: {}".format(tailmessage_blocked_events))
# Log some debugging information
logger.debug("Base directory: %s", baseDir)
logger.debug("Temporary directory: %s", tmp_dir)
logger.debug("Sounds path: %s", sounds_path)
logger.debug("Tailmessage path: %s", tailmessage_file)
logger.debug("Global Blocked events: %s", global_blocked_events)
logger.debug("SayAlert Blocked events: %s", sayalert_blocked_events)
logger.debug("Tailmessage Blocked events: %s", tailmessage_blocked_events)
def load_state():
"""
Load the state from the state file if it exists, else return an initial state.
Returns:
dict: A dictionary containing courtesy tone (ct), identifier (id) and alerts.
"""
if os.path.exists(data_file):
with open(data_file, "r") as file:
state = json.load(file)
# state["alertscript_alerts"] = set(state["alertscript_alerts"])
# state["last_alerts"] = set(state["last_alerts"])
return state
else:
return {
"ct": None,
"id": None,
"alertscript_alerts": set(),
"last_alerts": set(),
}
def save_state(state):
"""
Save the state to the state file.
Args:
state (dict): A dictionary containing courtesy tone (ct), identifier (id) and alerts.
"""
state["alertscript_alerts"] = list(state["alertscript_alerts"])
state["last_alerts"] = list(state["last_alerts"])
with open(data_file, "w") as file:
json.dump(state, file)
def getAlerts(countyCodes):
@ -274,8 +336,9 @@ def getAlerts(countyCodes):
Returns:
alerts (list): List of active weather alerts.
In case of alert injection from the config, return the injected alerts.
"""
# Severity mappings
# Mapping for severity for API response and the 'words' severity
severity_mapping_api = {
"Extreme": 4,
"Severe": 3,
@ -285,23 +348,22 @@ def getAlerts(countyCodes):
}
severity_mapping_words = {"Warning": 4, "Watch": 3, "Advisory": 2, "Statement": 1}
if config.getboolean("DEV", "INJECT", fallback=False):
logger.debug("DEV Alert Injection Enabled")
# Inject alerts if DEV INJECT is enabled in the config
if config.get("DEV", {}).get("INJECT", False):
logger.debug("getAlerts: DEV Alert Injection Enabled")
alerts = [
alert.strip() for alert in config["DEV"].get("INJECTALERTS").split(",")
alert.strip() for alert in config["DEV"].get("INJECTALERTS", [])
]
logger.debug("Injecting alerts: {}".format(alerts))
logger.debug("getAlerts: Injecting alerts: %s", alerts)
return 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)
logger.debug("Requesting {}".format(url))
logger.debug("getAlerts: Checking for alerts in %s at URL: %s", countyCode, url)
response = requests.get(url)
logger.debug("Response: {}\n\n".format(response.text))
if response.status_code == 200:
alert_data = response.json()
@ -316,15 +378,13 @@ def getAlerts(countyCodes):
for global_blocked_event in global_blocked_events:
if fnmatch.fnmatch(event, global_blocked_event):
logger.debug(
"Globally Blocking {} as per configuration".format(
event
)
"getAlerts: Globally Blocking %s as per configuration",
event,
)
break
else:
severity = feature["properties"].get("severity")
severity = feature["properties"].get("severity", None)
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:
@ -334,15 +394,14 @@ def getAlerts(countyCodes):
) # Add event to list as a tuple
else:
logger.error(
"Failed to retrieve alerts for {}, HTTP status code {}, response: {}".format(
countyCode, response.status_code, response.text
)
"Failed to retrieve alerts for %s, HTTP status code %s, response: %s",
countyCode,
response.status_code,
response.text,
)
# Eliminate duplicates in a way that preserves order
alerts = [x for i, x in enumerate(alerts) if alerts.index(x) == i]
alerts = list(dict.fromkeys(alerts))
# Sort by both API-provided severity and 'words' severity
alerts.sort(
key=lambda x: (
x[1], # API-provided severity
@ -351,14 +410,11 @@ def getAlerts(countyCodes):
reverse=True,
)
logger.debug("Sorted alerts: (alert), (severity)")
logger.debug("getAlerts: 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
alerts = [alert[0] for alert in alerts[:max_alerts]]
return alerts
@ -370,7 +426,9 @@ def sayAlert(alerts):
Args:
alerts (list): List of active weather alerts.
"""
# Define the path of the alert file
alert_file = "{}/alert.wav".format(sounds_path)
combined_sound = AudioSegment.from_wav(
os.path.join(sounds_path, "ALERTS", "SWP97.wav")
)
@ -378,15 +436,14 @@ def sayAlert(alerts):
os.path.join(sounds_path, "ALERTS", "SWP95.wav")
)
alert_count = 0 # Counter for alerts added to combined_sound
alert_count = 0
for alert in alerts:
# 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))
logger.debug("sayAlert: blocking %s as per configuration", alert)
continue
try:
@ -395,38 +452,39 @@ def sayAlert(alerts):
os.path.join(sounds_path, "ALERTS", "SWP{}.wav".format(WA[index]))
)
combined_sound += sound_effect + audio_file
logger.debug("Added {} (SWP{}.wav) to alert sound".format(alert, WA[index]))
alert_count += 1 # Increment the counter
logger.debug(
"sayAlert: Added %s (SWP%s.wav) to alert sound", alert, WA[index]
)
alert_count += 1
except ValueError:
logger.error("Alert not found: {}".format(alert))
logger.error("sayAlert: Alert not found: %s", alert)
except FileNotFoundError:
logger.error(
"Audio file not found: {}/ALERTS/SWP{}.wav".format(
sounds_path, WA[index]
)
"sayAlert: Audio file not found: %s/ALERTS/SWP%s.wav",
sounds_path,
WA[index],
)
if alert_count == 0: # Check the counter instead of combined_sound.empty()
logger.debug("SayAlert: All alerts were blocked, not broadcasting any alerts.")
if alert_count == 0:
logger.debug("sayAlert: All alerts were blocked, not broadcasting any alerts.")
else:
logger.debug("Exporting alert sound to {}".format(alert_file))
converted_combined_sound = convert_audio(combined_sound)
logger.debug("sayAlert: Exporting alert sound to %s", alert_file)
converted_combined_sound = convertAudio(combined_sound)
converted_combined_sound.export(alert_file, format="wav")
logger.debug("Replacing tailmessage with silence")
logger.debug("sayAlert: Replacing tailmessage with silence")
silence = AudioSegment.silent(duration=100)
converted_silence = convert_audio(silence)
converted_silence = convertAudio(silence)
converted_silence.export(tailmessage_file, format="wav")
node_numbers = config["Asterisk"]["Nodes"].split(",")
node_numbers = config.get("Asterisk", {}).get("Nodes", [])
for node_number in node_numbers:
logger.info("Broadcasting alert on node {}".format(node_number))
logger.info("Broadcasting alert on node %s", node_number)
command = '/usr/sbin/asterisk -rx "rpt localplay {} {}"'.format(
node_number.strip(), os.path.splitext(os.path.abspath(alert_file))[0]
node_number, os.path.splitext(os.path.abspath(alert_file))[0]
)
subprocess.run(command, shell=True)
# This keeps Asterisk from playing the tailmessage immediately after the alert
logger.info("Waiting 30 seconds for Asterisk to make announcement...")
time.sleep(30)
@ -436,12 +494,12 @@ def sayAllClear():
Generate and broadcast 'all clear' message on Asterisk.
"""
alert_clear = os.path.join(sounds_path, "ALERTS", "SWP96.wav")
node_numbers = config["Asterisk"]["Nodes"].split(",")
node_numbers = config.get("Asterisk", {}).get("Nodes", [])
for node_number in node_numbers:
logger.info("Broadcasting all clear message on node {}".format(node_number))
logger.info("Broadcasting all clear message on node %s", node_number)
command = '/usr/sbin/asterisk -rx "rpt localplay {} {}"'.format(
node_number.strip(), os.path.splitext(os.path.abspath(alert_clear))[0]
node_number, os.path.splitext(os.path.abspath(alert_clear))[0]
)
subprocess.run(command, shell=True)
@ -455,22 +513,25 @@ def buildTailmessage(alerts):
alerts (list): List of active weather alerts.
"""
if not alerts:
logger.debug("No alerts, creating silent tailmessage")
logger.debug("buildTailMessage: No alerts, creating silent tailmessage")
silence = AudioSegment.silent(duration=100)
converted_silence = convert_audio(silence)
converted_silence = convertAudio(silence)
converted_silence.export(tailmessage_file, format="wav")
return
combined_sound = AudioSegment.empty()
sound_effect = AudioSegment.from_wav(
os.path.join(sounds_path, "ALERTS", "SWP95.wav")
)
for alert in alerts:
# 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))
logger.debug(
"buildTailMessage: Alert blocked by TailmessageBlockedEvents: %s", alert
)
continue
try:
@ -479,99 +540,234 @@ def buildTailmessage(alerts):
os.path.join(sounds_path, "ALERTS", "SWP{}.wav".format(WA[index]))
)
combined_sound += sound_effect + audio_file
logger.debug("Added {} (SWP{}.wav) to tailmessage".format(alert, WA[index]))
logger.debug(
"buildTailMessage: Added %s (SWP%s.wav) to tailmessage",
alert,
WA[index],
)
except ValueError:
logger.error("Alert not found: {}".format(alert))
logger.error("Alert not found: %s", alert)
except FileNotFoundError:
logger.error(
"Audio file not found: {}/ALERTS/SWP{}.wav".format(
sounds_path, WA[index]
)
"Audio file not found: %s/ALERTS/SWP%s.wav",
sounds_path,
WA[index],
)
if combined_sound.empty():
logger.debug(
"BuildTailmessage: All alerts were blocked, creating silent tailmessage"
"buildTailMessage: All alerts were blocked, creating silent tailmessage"
)
combined_sound = AudioSegment.silent(duration=100)
logger.debug("Exporting tailmessage to {}".format(tailmessage_file))
converted_combined_sound = convert_audio(combined_sound)
logger.debug("buildTailMessage: Exporting tailmessage to %s", tailmessage_file)
converted_combined_sound = convertAudio(combined_sound)
converted_combined_sound.export(tailmessage_file, format="wav")
def changeCT(ct):
"""
Change the current Courtesy Tone (CT) to the one specified.
The function first checks if the specified CT is already in use, and if it is, it returns without making any changes.
This function first checks if the specified CT is already in use. If so, it does not make any changes.
If the CT needs to be changed, it replaces the current CT files with the new ones and updates the state file.
If no CT is specified, the function logs an error message and returns.
Args:
ct (str): The name of the new CT to use. This should be one of the CTs specified in the config file.
Returns:
bool: True if the CT was changed, False otherwise.
Raises:
FileNotFoundError: If the specified CT files are not found.
"""
tone_dir = config["CourtesyTones"].get("ToneDir", fallback="./SOUNDS/TONES")
if tone_dir == "./SOUNDS/TONES":
tone_dir = os.path.join(sounds_path, "TONES")
local_ct = config["CourtesyTones"].get("LocalCT")
link_ct = config["CourtesyTones"].get("LinkCT")
wx_ct = config["CourtesyTones"].get("WXCT")
rpt_local_ct = config["CourtesyTones"].get("RptLocalCT")
rpt_link_ct = config["CourtesyTones"].get("RptLinkCT")
ct_state_file = os.path.join(tmp_dir, "ct_state.txt")
logger.debug("Tone directory: {}".format(tone_dir))
logger.debug("CT argument: {}".format(ct))
state = load_state()
current_ct = state["ct"]
tone_dir = config["CourtesyTones"].get("ToneDir", os.path.join(sounds_path, "TONES"))
local_ct = config["CourtesyTones"]["Tones"]["LocalCT"]
link_ct = config["CourtesyTones"]["Tones"]["LinkCT"]
wx_ct = config["CourtesyTones"]["Tones"]["WXCT"]
rpt_local_ct = config["CourtesyTones"]["Tones"]["RptLocalCT"]
rpt_link_ct = config["CourtesyTones"]["Tones"]["RptLinkCT"]
logger.debug("changeCT: Tone directory: %s", tone_dir)
logger.debug("changeCT: Local CT: %s", local_ct)
logger.debug("changeCT: Link CT: %s", link_ct)
logger.debug("changeCT: WX CT: %s", wx_ct)
logger.debug("changeCT: Rpt Local CT: %s", rpt_local_ct)
logger.debug("changeCT: Rpt Link CT: %s", rpt_link_ct)
logger.debug("changeCT: CT argument: %s", ct)
if not ct:
logger.error("ChangeCT called with no CT specified")
logger.error("changeCT: called with no CT specified")
return
current_ct = None
if os.path.exists(ct_state_file):
with open(ct_state_file, "r") as file:
current_ct = file.read().strip()
if state:
current_ct = state["ct"]
logger.debug("Current CT: {}".format(current_ct))
logger.debug("changeCT: Current CT - %s", current_ct)
if ct == current_ct:
logger.debug("Courtesy tones are already {}, no changes made.".format(ct))
logger.debug("changeCT: Courtesy tones are already %s, no changes made.", ct)
return False
if ct == "NORMAL":
logger.info("Changing to NORMAL courtesy tones")
src_file = tone_dir + "/" + local_ct
dest_file = tone_dir + "/" + rpt_local_ct
logger.debug("Copying {} to {}".format(src_file, dest_file))
src_file = os.path.join(tone_dir, local_ct)
dest_file = os.path.join(tone_dir, rpt_local_ct)
logger.debug("changeCT: Copying %s to %s", src_file, dest_file)
shutil.copyfile(src_file, dest_file)
src_file = tone_dir + "/" + link_ct
dest_file = tone_dir + "/" + rpt_link_ct
logger.debug("Copying {} to {}".format(src_file, dest_file))
src_file = os.path.join(tone_dir, link_ct)
dest_file = os.path.join(tone_dir, rpt_link_ct)
logger.debug("changeCT: Copying %s to %s", src_file, dest_file)
shutil.copyfile(src_file, dest_file)
else:
logger.info("Changing to {} courtesy tone".format(ct))
src_file = tone_dir + "/" + wx_ct
dest_file = tone_dir + "/" + rpt_local_ct
logger.debug("Copying {} to {}".format(src_file, dest_file))
logger.info("Changing to %s courtesy tone", ct)
src_file = os.path.join(tone_dir, wx_ct)
dest_file = os.path.join(tone_dir, rpt_local_ct)
logger.debug("changeCT: Copying %s to %s", src_file, dest_file)
shutil.copyfile(src_file, dest_file)
src_file = os.path.join(tone_dir, wx_ct)
dest_file = os.path.join(tone_dir, rpt_link_ct)
logger.debug("changeCT: Copying %s to %s", src_file, dest_file)
shutil.copyfile(src_file, dest_file)
state["ct"] = ct
save_state(state)
return True
def changeID(id):
"""
Change the current Identifier (ID) to the one specified.
This function first checks if the specified ID is already in use. If so, it does not make any changes.
If the ID needs to be changed, it replaces the current ID files with the new ones and updates the state file.
Args:
id (str): The name of the new ID to use. This should be one of the IDs specified in the config file.
Returns:
bool: True if the ID was changed, False otherwise.
Raises:
FileNotFoundError: If the specified ID files are not found.
"""
state = load_state()
current_id = state["id"]
id_dir = config["IDChange"].get("IDDir", os.path.join(sounds_path, "ID"))
normal_id = config["IDChange"]["IDs"]["NormalID"]
wx_id = config["IDChange"]["IDs"]["WXID"]
rpt_id = config["IDChange"]["IDs"]["RptID"]
logger.debug("changeID: ID directory: %s", id_dir)
logger.debug("changeID: ID argument: %s", id)
if not id:
logger.error("changeID: called with no ID specified")
return
current_id = None
if state:
current_id = state["id"]
logger.debug("changeID: Current ID - %s", current_id)
if id == current_id:
logger.debug("changeID: ID is already %s, no changes made.", id)
return False
if id == "NORMAL":
logger.info("Changing to NORMAL ID")
src_file = os.path.join(id_dir, normal_id)
dest_file = os.path.join(id_dir, rpt_id)
logger.debug("changeID: Copying %s to %s", src_file, dest_file)
shutil.copyfile(src_file, dest_file)
src_file = tone_dir + "/" + wx_ct
dest_file = tone_dir + "/" + rpt_link_ct
logger.debug("Copying {} to {}".format(src_file, dest_file))
else:
logger.info("Changing to %s ID", id)
src_file = os.path.join(id_dir, wx_id)
dest_file = os.path.join(id_dir, rpt_id)
logger.debug("changeID: Copying %s to %s", src_file, dest_file)
shutil.copyfile(src_file, dest_file)
with open(ct_state_file, "w") as file:
file.write(ct)
state["id"] = id
save_state(state)
return True
def send_pushover_notification(message, title=None, priority=0):
def alertScript(alerts):
"""
Send a push notification via Pushover service.
The function constructs the payload for the request, including the user key, API token, message, title, and priority.
This function reads a list of alerts, then performs actions based
on the alert triggers defined in the global configuration file.
:param alerts: List of alerts to process
:type alerts: list[str]
"""
# Fetch AlertScript configuration from global_config
alertScript_config = config.get("AlertScript", {})
logger.debug("AlertScript configuration: %s", alertScript_config)
# Fetch Mappings from AlertScript configuration
mappings = alertScript_config.get("Mappings", [])
if mappings is None:
mappings = []
logger.debug("Mappings: %s", mappings)
# Iterate over each mapping
for mapping in mappings:
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 = []
for alert in alerts:
for trigger in triggers:
if fnmatch.fnmatch(alert, trigger):
logger.debug(
'Match found: Alert "%s" matches trigger "%s"', alert, trigger
)
matched_alerts.append(alert)
# Check if 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)
):
logger.debug(
'Alerts matched the triggers as per the match type "%s"', match_type
)
# Execute commands based on the Type of mapping
if mapping.get("Type") == "BASH":
logger.debug('Mapping type is "BASH"')
for cmd in commands:
logger.debug("Executing BASH command: %s", cmd)
subprocess.run(cmd, shell=True)
elif mapping.get("Type") == "DTMF":
logger.debug('Mapping type is "DTMF"')
for node in nodes:
for cmd in commands:
dtmf_cmd = 'asterisk -rx "rpt fun {} {}"'.format(node, cmd)
logger.debug("Executing DTMF command: %s", dtmf_cmd)
subprocess.run(dtmf_cmd, shell=True)
def sendPushover(message, title=None, priority=0):
"""
Send a push notification via the Pushover service.
This function constructs the payload for the request, including the user key, API token, message, title, and priority.
The payload is then sent to the Pushover API endpoint. If the request fails, an error message is logged.
Args:
@ -579,8 +775,8 @@ def send_pushover_notification(message, title=None, priority=0):
title (str, optional): The title of the push notification. Defaults to None.
priority (int, optional): The priority of the push notification. Defaults to 0.
Returns:
None
Raises:
requests.exceptions.HTTPError: If an error occurs while sending the notification.
"""
pushover_config = config["Pushover"]
user_key = pushover_config.get("UserKey")
@ -601,10 +797,10 @@ def send_pushover_notification(message, title=None, priority=0):
response = requests.post(url, data=payload)
if response.status_code != 200:
logger.error("Failed to send Pushover notification: {}".format(response.text))
logger.error("Failed to send Pushover notification: %s", response.text)
def convert_audio(audio):
def convertAudio(audio):
"""
Convert audio file to 8000Hz mono for compatibility with Asterisk.
@ -617,88 +813,149 @@ def convert_audio(audio):
return audio.set_frame_rate(8000).set_channels(1)
def main():
def change_and_log_CT_or_ID(
alerts,
specified_alerts,
auto_change_enabled,
alert_type,
pushover_debug,
pushover_message,
):
"""
Main function of the script, that fetches and processes severe weather
alerts, then integrates these alerts into an Asterisk/app_rpt based radio
repeater system.
Check whether the CT or ID needs to be changed, performs the change, and logs the process.
Args:
alerts (list): The new alerts that have been fetched.
specified_alerts (list): The alerts that require a change in CT or ID.
auto_change_enabled (bool): Whether auto change is enabled for CT or ID.
alert_type (str): "CT" for Courtesy Tones and "ID" for Identifiers.
pushover_debug (bool): Whether to include debug information in pushover notifications.
pushover_message (str): The current pushover message to which any updates will be added.
"""
say_alert_enabled = config["Alerting"].getboolean("SayAlert", fallback=False)
say_all_clear_enabled = config["Alerting"].getboolean("SayAllClear", fallback=False)
alerts = getAlerts(countyCodes)
tmp_file = "{}/alerts.json".format(tmp_dir)
if os.path.exists(tmp_file):
with open(tmp_file, "r") as file:
old_alerts = json.load(file)
if auto_change_enabled:
logger.debug(
"%s auto change is enabled, alerts that require a %s change: %s",
alert_type,
alert_type,
specified_alerts,
)
# Check if any alert matches specified_alerts
if set(alerts).intersection(specified_alerts):
for alert in alerts:
if alert in specified_alerts:
logger.debug("Alert %s requires a %s change", alert, alert_type)
if (
changeCT("WX") if alert_type == "CT" else changeID("WX")
): # If the CT/ID was actually changed
if pushover_debug:
pushover_message += "Changed {} to WX\n".format(alert_type)
break
else: # No alerts require a CT/ID change, revert back to normal
logger.debug(
"No alerts require a %s change, reverting to normal.", alert_type
)
if (
changeCT("NORMAL") if alert_type == "CT" else changeID("NORMAL")
): # If the CT/ID was actually changed
if pushover_debug:
pushover_message += "Changed {} to NORMAL\n".format(alert_type)
else:
old_alerts = ["init"]
logger.info("No previous alerts file found, starting fresh.")
logger.debug("%s auto change is not enabled", alert_type)
if old_alerts != alerts:
with open(tmp_file, "w") as file:
json.dump(alerts, file)
ct_alerts = [
alert.strip()
for alert in config["CourtesyTones"].get("CTAlerts").split(",")
]
enable_ct_auto_change = config["CourtesyTones"].getboolean(
"Enable", fallback=False
def main():
"""
The main function that orchestrates the entire process of fetching and
processing severe weather alerts, then integrating these alerts into
an Asterisk/app_rpt based radio repeater system.
Key Steps:
1. Fetch the configuration from the local setup.
2. Get the new alerts based on the provided county codes.
3. Compare the new alerts with the previously stored alerts.
4. If there's a change, store the new alerts and process them accordingly.
5. Check each alert against a set of specified alert types and perform actions accordingly.
6. Send notifications if enabled.
"""
# Fetch configurations
say_alert_enabled = config["Alerting"].get("SayAlert", False)
say_all_clear_enabled = config["Alerting"].get("SayAllClear", False)
alertscript_enabled = config["AlertScript"].get("Enable", False)
# Fetch state
state = load_state()
# Load old alerts
last_alerts = state["last_alerts"]
# Fetch new alerts
alerts = getAlerts(config.get("CountyCodes", []))
# If new alerts differ from old ones, process new alerts
if last_alerts != alerts:
state["last_alerts"] = alerts
save_state(state)
ct_alerts = config["CourtesyTones"].get("CTAlerts", [])
enable_ct_auto_change = config["CourtesyTones"].get("Enable", False)
id_alerts = config["IDChange"].get("IDAlerts", [])
enable_id_auto_change = config["IDChange"].get("Enable", False)
pushover_enabled = config["Pushover"].get("Enable", False)
pushover_debug = config["Pushover"].get("Debug", False)
# Initialize pushover message
pushover_message = (
"Alerts Cleared\n" if len(alerts) == 0 else "\n".join(alerts) + "\n"
)
pushover_enabled = config["Pushover"].getboolean("Enable", fallback=False)
pushover_debug = config["Pushover"].getboolean("Debug", fallback=False)
# Check if Courtesy Tones (CT) or ID needs to be changed
change_and_log_CT_or_ID(
alerts,
ct_alerts,
enable_ct_auto_change,
"CT",
pushover_debug,
pushover_message,
)
change_and_log_CT_or_ID(
alerts,
id_alerts,
enable_id_auto_change,
"ID",
pushover_debug,
pushover_message,
)
# Check if alerts need to be communicated
if len(alerts) == 0:
logger.info("No alerts found")
pushover_message = "Alerts Cleared\n"
if not os.path.exists(tmp_file):
with open(tmp_file, "w") as file:
json.dump([], file)
if say_all_clear_enabled:
sayAllClear()
else:
logger.info("Alerts found: {}".format(alerts))
logger.info("Alerts found: %s", alerts)
if alertscript_enabled:
alertScript(alerts)
if say_alert_enabled:
sayAlert(alerts)
pushover_message = "\n".join(alerts) + "\n"
if enable_ct_auto_change:
logger.debug(
"CT auto change is enabled, alerts that require a CT change: {}".format(
ct_alerts
)
)
# Check if any alert matches ct_alerts
if set(alerts).intersection(ct_alerts):
for alert in alerts:
if alert in ct_alerts:
logger.debug("Alert {} requires a CT change".format(alert))
if changeCT("WX"): # If the CT was actually changed
if pushover_debug:
pushover_message += "Changed courtesy tones to WX\n"
break
else: # No alerts require a CT change, revert back to normal
logger.debug("No alerts require a CT change, reverting to normal.")
if changeCT("NORMAL"): # If the CT was actually changed
if pushover_debug:
pushover_message += "Changed courtesy tones to NORMAL\n"
else:
logger.debug("CT auto change is not enabled")
# Check if tailmessage needs to be built
enable_tailmessage = config.get("Tailmessage", {}).get("Enable", False)
if enable_tailmessage:
buildTailmessage(alerts)
if pushover_debug:
if not alerts:
pushover_message += "WX tailmessage removed\n"
else:
pushover_message += "Built WX tailmessage\n"
pushover_message += (
"WX tailmessage removed\n"
if not alerts
else "Built WX tailmessage\n"
)
# Send pushover notification
if pushover_enabled:
pushover_message = pushover_message.rstrip("\n")
logger.debug("Sending pushover notification: {}".format(pushover_message))
send_pushover_notification(pushover_message, title="Alerts Changed")
logger.debug("Sending pushover notification: %s", pushover_message)
sendPushover(pushover_message, title="Alerts Changed")
else:
logger.debug("No change in alerts")

@ -1,176 +0,0 @@
; SkywarnPlus v0.1.0 Configuration File
; by Mason Nelson (N5LSN/WRKF394)
; Please update this file according to your setup and preferences
[SKYWARNPLUS]
; Completely enable/disable SkywarnPlus
Enable = True
; Asterisk Settings
[Asterisk]
; Comma separated list of node numbers to broadcast alerts on
; Example: Nodes = 1998, 1999
Nodes =
; Alerting settings
[Alerting]
; List of county codes to pull data for.
; FIND YOUR COUNTY CODE(S) at https://alerts.weather.gov/
; DO NOT USE ZONE CODES OR YOU WILL MISS ALERTS
; See README.md for more information
; Example: CountyCodes = ARC121,ARC021,ARC139,ARC027
CountyCodes =
; Enable instant alerting when weather alerts change
; Either True or False
SayAlert = True
; Enable instant alerting when weather alerts are cleared
; 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 =
; Optional alternate path to the directory where sound files are stored
; Default is SkywarnPlus/SOUNDS
; Example: SoundsPath = /home/repeater/
;SoundsPath =
; Blocking settings
[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
; 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
; 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
; CASE SENSITIVE list of events to ignore, comma separated.
; Example: TailmessageBlockedEvents = *Statement, *Advisory
TailmessageBlockedEvents =
; Tail message settings
[Tailmessage]
; REQUIRES SETUP IN RPT.CONF FIRST
; REFER TO README.MD FOR MORE INFO
; Enable building of tail message
; Either True or False
Enable = False
; Optional alternate path & filename where tail message should be saved
; Default is SkywarnPlus/SOUNDS/wx-tail.wav
; Example: TailmessagePath = /home/repeater/wx-tail.wav
;TailmessagePath =
; Courtesy tone settings
[CourtesyTones]
; REQUIRES SETUP IN RPT.CONF FIRST
; REFER TO README.MD FOR MORE INFO
; Enable Automatic Courtesy Tone changing
; Either True or False
Enable = False
; Optional alternate directory where tone files are located
; Default is SkywarnPlus/SOUNDS/TONES
; Example: ToneDir = /home/repeater/TONES
;ToneDir =
; Sound file to use for normal local courtesy tone
; Example: LocalCT = BOOP.ulaw
LocalCT = BOOP.ulaw
; Sound file to use for normal link courtesy tone
; Example: LinkCT = BEEP.ulaw
LinkCT = BEEP.ulaw
; Sound file to use for weather courtesy tone (local and link)
; Example: WXCT = WX-CT.ulaw
WXCT = WX-CT.ulaw
; Sound file rpt.conf is looking for as local courtesy tone
; Example: RptLocalCT = CT-LOCAL.ulaw
RptLocalCT = CT-LOCAL.ulaw
; Sound file rpt.conf is looking for as link courtesy tone
; Example: RptLinkCT = CT-LINK.ulaw
RptLinkCT = CT-LINK.ulaw
; CASE SENSITIVE, comma & newline separated list of alerts that trigger the WX courtesy tone
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,
Tornado Warning,
Tornado Watch
; Pushover settings
; Pushover is a free notification service that SkywarnPlus can use to send alerts
; to your phone or other devices. Visit https://pushover.net/ to sign up for free
[Pushover]
; Enable Pushover integration
; Either True or False
Enable = False
; Your Pushover User Key
UserKey =
; Your Pushover API Token
APIToken =
; Enable more verbose Pushover messaging
; Either True or False
Debug = False
; Logging Options
[Logging]
; Enable more verbose logging
; Either True or False
Debug = False
; Optional alternate log file path
; Default is /tmp/Skywarnplus/SkywarnPlus.log
; Example: LogPath = /path/to/log/file
;LogPath =
; Developer options
[DEV]
; Delete cached data on startup
; Either True or False
CLEANSLATE = False
; Optional alternate TMP directory
; Default is /tmp/SkywarnPlus
; Example: TmpDir = /home/repeater/tmp/SkywarnPlus
;TmpDir = /tmp/SkywarnPlus
; Enable to inject the below list of test alerts instead of calling the NWS API
INJECT = False
; CASE SENSITIVE, comma & newline separated list of alerts to inject
INJECTALERTS = Tornado Warning,
Tornado Watch,
Severe Thunderstorm Warning

@ -0,0 +1,257 @@
# SkywarnPlus v0.2.0 Configuration File
# Author: Mason Nelson (N5LSN/WRKF394)
# Please edit this file according to your specific requirements.
#
# This config file is structured YAML. Please be sure to maintain the structure when editing.
# YAML is very picky about indentation. Use spaces, not tabs.
################################################################################################################################
SKYWARNPLUS:
# Toggle the entire SkywarnPlus operation.
# Set to 'True' to activate or 'False' to disable.
# Example: Enable: true
Enable: true
################################################################################################################################
Asterisk:
# List of node numbers for broadcasting alerts. Multiple nodes are specified as a list.
# Example:
# Nodes:
# - 1998
# - 1999
Nodes:
- YOUR_NODE_NUMBER_HERE
################################################################################################################################
Alerting:
# Specify the county codes for which you want to pull weather data.
# Find your county codes at https://alerts.weather.gov/.
# Make sure to use county codes ONLY, NOT zone codes, otherwise you might miss out on alerts.
# Example:
# CountyCodes:
# - ARC121
# - ARC021
CountyCodes:
- YOUR_COUNTY_CODE_HERE
# Enable instant voice announcement when new weather alerts are issued.
# Set to 'True' for enabling or 'False' for disabling.
# Example: SayAlert: true
SayAlert: true
# Enable instant voice announcement when weather alerts are cleared.
# Set to 'True' for enabling or 'False' for disabling.
# Example: SayAllClear: true
SayAllClear: true
# Limit the maximum number of alerts to process in case of multiple alerts.
# SkywarnPlus fetches all alerts, orders them by severity, and processes only the 'n' most severe alerts, where 'n' is the MaxAlerts value.
# MaxAlerts:
# Specify an alternative path to the directory where sound files are located.
# Default is SkywarnPlus/SOUNDS.
# SoundsPath:
################################################################################################################################
Blocking:
# List of globally blocked events. These alerts are ignored across the entire SkywarnPlus operation.
# Use a case-sensitive list. Wildcards can be used.
# Example:
# GlobalBlockedEvents:
# - Flood Watch
# - *Statement
# - *Advisory
GlobalBlockedEvents:
# List of events blocked from being announced when received. These alerts will still be added to the tail message.
# Use a case-sensitive list.
SayAlertBlockedEvents:
# List of events blocked from being added to the tail message. These alerts will still be announced when received.
# Use a case-sensitive list.
TailmessageBlockedEvents:
################################################################################################################################
Tailmessage:
# Configuration for the tail message functionality. Requires initial setup in RPT.CONF.
# Set 'Enable' to 'True' for enabling or 'False' for disabling.
Enable: false
# Specify an alternative path and filename for saving the tail message.
# Default is SkywarnPlus/SOUNDS/wx-tail.wav.
# TailmessagePath:
################################################################################################################################
CourtesyTones:
# Configuration for the Courtesy Tones. Requires initial setup in RPT.CONF.
# Set 'Enable' to 'True' for enabling or 'False' for disabling.
Enable: false
# Specify an alternative directory where tone files are located.
# Default is SkywarnPlus/SOUNDS/TONES.
# ToneDir:
# Define the sound files for various types of courtesy tones.
Tones:
# Normal local courtesy tone.
LocalCT: BOOP.ulaw
# Normal link courtesy tone.
LinkCT: BEEP.ulaw
# Weather courtesy tone (both local and link).
WXCT: WX-CT.ulaw
# rpt.conf file's local courtesy tone.
RptLocalCT: CT-LOCAL.ulaw
# rpt.conf file's link courtesy tone.
RptLinkCT: CT-LINK.ulaw
# Define the alerts that trigger the weather courtesy tone.
# Use a case-sensitive list. One alert per line for better readability.
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
- Tornado Warning
- Tornado Watch
################################################################################################################################
IDChange:
# Configuration for Automatic ID Changing. Requires initial setup in RPT.CONF and manual creation of audio files.
Enable: false
# Specify an alternative directory where ID files are located.
# Default is SkywarnPlus/SOUNDS/ID.
# IDDir:
# Define the sound files for normal ID and weather ID.
IDs:
NormalID: ID.ulaw
WXID: WXID.ulaw
# Define the sound file rpt.conf is looking for as normal ID.
RptID: RPTID.ulaw
# Define the alerts that trigger the weather ID.
# Use a case-sensitive list. One alert per line for better readability.
IDAlerts:
- 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
- Tornado Warning
- Tornado Watch
################################################################################################################################
AlertScript:
# Completely enable/disable AlertScript
Enable: false
Mappings:
# Define the mapping of alerts to either DTMF commands or bash scripts here.
# Wildcards (*) can be used in the ALERTS for broader matches.
# Examples:
#
# This entry will execute the bash command 'asterisk -rx "rpt fun 1999 *123*456*789"'
# when the alerts "Tornado Warning" AND "Tornado Watch" are detected.
#
# - Type: DTMF
# Nodes:
# - 1999
# Commands:
# - '*123*456*789'
# Triggers:
# - Tornado Warning
# - Tornado Watch
# Match: ALL
#
# This entry will execute the bash command '/home/repeater/testscript.sh'
# and the bash command '/home/repeater/saytime.sh' when an alert whose
# title ends with "Statement" is detected.
#
# - Type: BASH
# Commands:
# - '/home/repeater/testscript.sh'
# - '/home/repeater/saytime.sh'
# Triggers:
# - *Statement
#
# This entry will execute the bash command 'asterisk -rx "rpt fun 1998 *123*456*789"'
# and the bash command 'asterisk -rx "rpt fun 1999 *123*456*789"' when an alert
# titled "Tornado Warning" OR "Tornado Watch" is detected.
#
# - Type: DTMF
# Nodes:
# - 1998
# - 1999
# Commands:
# - '*123*456*789'
# Triggers:
# - Tornado Warning
# - Tornado Watch
#
# This entry will execute the bash command 'asterisk -rx "rpt fun 1999 *123*456*789"'
# and the bash command 'asterisk -rx "rpt fun 1999 *987*654*321"'
# when an alert titled "Tornado Warning" OR "Tornado Watch" is detected.
#
# - Type: DTMF
# Nodes:
# - 1999
# Commands:
# - '*123*456*789'
# - '*987*654*321'
# Triggers:
# - Tornado Warning
# - Tornado Watch
# Match: ANY
#
- Type: BASH
Commands:
- 'echo "Tornado Warning detected!"'
Triggers:
- Tornado Warning
################################################################################################################################
Pushover:
# Configuration for Pushover integration. Pushover is a free notification service. Register at https://pushover.net/.
Enable: false
# Provide your user key obtained from Pushover.
UserKey:
# Provide the API token obtained from Pushover.
APIToken:
# Enable verbose messaging
Debug: false
################################################################################################################################
Logging:
# Enable verbose logging
Debug: false
# Specify an alternative log file path.
# LogPath:
################################################################################################################################
DEV:
# Delete cached data on startup
CLEANSLATE: false
# Specify the TMP directory.
TmpDir: /tmp/SkywarnPlus
# Enable test alert injection instead of calling the NWS API by setting 'INJECT' to 'True'.
INJECT: false
# List the test alerts to inject. Use a case-sensitive list. One alert per line for better readability.
INJECTALERTS:
- Tornado Warning
- Tornado Watch
- Severe Thunderstorm Warning
Loading…
Cancel
Save

Powered by TurnKey Linux.