diff --git a/CONTROL.sh b/CONTROL.sh new file mode 100644 index 0000000..fd49c2a --- /dev/null +++ b/CONTROL.sh @@ -0,0 +1,113 @@ +#!/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 +# 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 " + 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 \ No newline at end of file diff --git a/README.md b/README.md index 8edc008..7229408 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,48 @@ -# SkywarnPlus: Your Advanced Weather Alert System +# SkywarnPlus -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. +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. -## Key Features +Tested on ASL 1.01, ASL 2.0.0, and HAMVOIP 1.7-01. -- **Seamless Integration:** SkywarnPlus operates on a Debian (AllStarLink) or Arch (HAMVOIP) node. +## Features -- **Real-Time Weather Alerts:** The software checks the NWS CAP v1.2 API for live weather alerts for user-defined areas. +- **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. -- **Unlimited Area & Node Numbers:** Users can define as many areas and local node numbers as desired. +## How It Works -- **Automatic Announcements:** Weather alerts, including when all warnings have been cleared, are announced automatically on the node. +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: -- **Tailmessage Creation:** The software generates tailmessages for the node to continuously inform listeners about active alerts after the initial broadcast. +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. -- **Dynamic Changes to Node:** Courtesy tones and node CW / voice ID automatically change according to user-defined alerts, optimizing communication during severe weather. +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. -- **Human Speech:** Announcements are delivered in a natural, human speech for easier understanding. +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. -- **Efficiency & Speed:** SkywarnPlus is optimized for speed and efficiency to provide real-time information without delay. +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. -- **Preserves Hardware:** SkywarnPlus limits I/O to the physical disk, preventing SD card burnout in Raspberry Pi devices. +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. -- **Remote Control:** Functions can be mapped to DTMF commands for remote over-the-air control. +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. -- **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. +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. -- **Pushover Integration:** With Pushover integration, SkywarnPlus can send weather alert notifications directly to your phone or other devices. +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. -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. +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. # Installation @@ -46,7 +60,7 @@ Follow the steps below to install: apt update apt upgrade apt install unzip python3 python3-pip ffmpeg - pip3 install pyyaml requests python-dateutil pydub + pip3 install requests python-dateutil pydub ``` **Arch (HAMVOIP)** @@ -57,7 +71,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 pyyaml requests python-dateutil pydub + pip install requests python-dateutil pydub ``` 2. **Download SkywarnPlus** @@ -71,22 +85,21 @@ Follow the steps below to install: rm SkywarnPlus.zip ``` -3. **Configure Permissions** +3. **Configure CONTROL.sh Permissions** - The scripts must be made executable. Use the chmod command to change the file permissions: + The CONTROL.sh script must be made executable. Use the chmod command to change the file permissions: ```bash cd SkywarnPlus - chmod +x SkywarnPlus.py - chmod +x SkyControl.py + chmod +x CONTROL.sh ``` 4. **Edit Configuration** - 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. + 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. ```bash - nano config.yaml + nano config.ini ``` 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. @@ -108,18 +121,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/local/bin/SkywarnPlus/SkywarnPlus.py + * * * * * /usr/bin/python3 /usr/local/bin/SkywarnPlus/SkywarnPlus.py ``` This command will execute SkywarnPlus (poll NWS API for data) every minute. -# Tailmessage, Courtesy Tones, & IDs +# Tailmessage and Automatic Courtesy Tones -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. +SkywarnPlus offers functionalities such as Tailmessage management and Automatic Courtesy Tones, which require specific configurations in the `rpt.conf` file. ## Tailmessage -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: +Tailmessage functionality requires the `rpt.conf` to be properly set up. Here's an example: ```ini tailmessagetime = 600000 @@ -127,9 +140,9 @@ tailsquashedtime = 30000 tailmessagelist = /usr/local/bin/SkywarnPlus/SOUNDS/wx-tail ``` -## Courtesy Tones +## Automatic Courtesy Tones -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: +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: ```ini [NODENUMBER] @@ -142,12 +155,7 @@ ct2 = /usr/local/bin/SkywarnPlus/SOUNDS/TONES/CT-LINK remotetx = /usr/local/bin/SkywarnPlus/SOUNDS/TONES/CT-LOCAL ``` -## 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 -``` +Courtesy tone files are located in `SOUNDS/TONES` by default and are configured through `config.ini` and `rpt.conf`. # Pushover Integration @@ -156,15 +164,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.yaml` +4. Add UserKey & API Key to `config.ini` # Control Script -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. +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. ## Usage -To use the `SkyControl.py` script, you need to call it with two parameters: +To use the CONTROL.sh script, you need to call it with two parameters: 1. The name of the setting you want to change (case insensitive). @@ -173,122 +181,77 @@ To use the `SkyControl.py` 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/SkyControl.py enable false +/usr/local/bin/SkywarnPlus/CONTROL.sh enable false ``` And to reenable it, you would use: ```bash -/usr/local/bin/SkywarnPlus/SkyControl.py enable true +/usr/local/bin/SkywarnPlus/CONTROL.sh enable true ``` And to toggle it, you would use: ```bash -/usr/local/bin/SkywarnPlus/SkyControl.py enable toggle +/usr/local/bin/SkywarnPlus/CONTROL.sh enable toggle ``` ## Spoken Feedback -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. +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. ## Mapping to DTMF Commands -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: +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: ```bash -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 +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 ``` 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: - - - Commands: - - '' - Triggers: - - - Match: ALL # or ANY - - Type: BASH - Commands: - - '' - Triggers: - - -``` - -In the examples above, `` are the nodes where you want the DTMF command to be dispatched, `` is the command to be executed, and `` are the alerts to trigger this command. Likewise, for bash commands, `` is the script to be executed and `` are the alerts to trigger this script. Note that wildcards (`*`) can be used in `` 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.yaml` file: +To enable this option, modify the following settings in the `[DEV]` section of your `config.ini` file: -```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 +```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 ``` # 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.yaml` 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.ini` file. Open this file and set the `debug` option under the `[SkywarnPlus]` section to `true`. -```yaml -Logging: - # Configuration for logging options. - # Enable verbose logging by setting 'Debug' to 'True'. - Debug: false +```ini +; Logging Options +[Logging] +; Enable more verbose logging +; Either True or False +Debug = False ``` This will allow the program to output detailed information about its operations, which is helpful for identifying any issues or errors. diff --git a/SOUNDS/ALERTS/DICTIONARY.txt b/SOUNDS/ALERTS/DICTIONARY.txt index 4a43ec5..bbe317b 100644 --- a/SOUNDS/ALERTS/DICTIONARY.txt +++ b/SOUNDS/ALERTS/DICTIONARY.txt @@ -68,10 +68,6 @@ 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 diff --git a/SOUNDS/ALERTS/SWP81.wav b/SOUNDS/ALERTS/SWP81.wav deleted file mode 100644 index 660f528..0000000 Binary files a/SOUNDS/ALERTS/SWP81.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP82.wav b/SOUNDS/ALERTS/SWP82.wav deleted file mode 100644 index 5a2b7e5..0000000 Binary files a/SOUNDS/ALERTS/SWP82.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP83.wav b/SOUNDS/ALERTS/SWP83.wav deleted file mode 100644 index 77dd413..0000000 Binary files a/SOUNDS/ALERTS/SWP83.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP84.wav b/SOUNDS/ALERTS/SWP84.wav deleted file mode 100644 index 8c03bda..0000000 Binary files a/SOUNDS/ALERTS/SWP84.wav and /dev/null differ diff --git a/SkyControl.py b/SkyControl.py deleted file mode 100644 index b807b70..0000000 --- a/SkyControl.py +++ /dev/null @@ -1,81 +0,0 @@ -#!/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 -# 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 {} ".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])]) \ No newline at end of file diff --git a/SkywarnPlus.py b/SkywarnPlus.py index 3984855..0999ce2 100644 --- a/SkywarnPlus.py +++ b/SkywarnPlus.py @@ -1,7 +1,7 @@ -#!/usr/bin/python3 +#!/usr/bin/env python3 """ -SkywarnPlus v0.2.0 by Mason Nelson (N5LSN/WRKF394) +SkywarnPlus v0.1.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,77 +24,58 @@ 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 -# Directories and Paths +# Configuration file handling baseDir = os.path.dirname(os.path.realpath(__file__)) -configPath = os.path.join(baseDir, "config.yaml") +configPath = os.path.join(baseDir, "config.ini") +config = configparser.ConfigParser() +config.read_file(open(configPath, "r")) -# 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) +# Fetch values from configuration file +master_enable = config["SKYWARNPLUS"].getboolean("Enable", fallback=False) if not master_enable: - print("SkywarnPlus is disabled in config.yaml, exiting...") + print("SkywarnPlus is disabled in config.ini, exiting...") exit() - -# 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 = [] +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(",") tailmessage_blocked_events = ( - config.get("Blocking", {}).get("TailmessageBlockedEvents", []) + config["Blocking"].get("TailmessageBlockedEvents").split(",") ) -if tailmessage_blocked_events is None: - tailmessage_blocked_events = [] -# Define Max Alerts -max_alerts = config.get("Alerting", {}).get("MaxAlerts", 99) +# Maximum number of alerts to process +max_alerts = config["Alerting"].getint("MaxAlerts", fallback=99) -# Define Tailmessage configuration -tailmessage_config = config.get("Tailmessage", {}) -enable_tailmessage = tailmessage_config.get("Enable", False) +# Configuration for tailmessage +tailmessage_config = config["Tailmessage"] +# Flag to enable/disable tailmessage +enable_tailmessage = tailmessage_config.getboolean("Enable", fallback=False) +# Path to tailmessage file tailmessage_file = tailmessage_config.get( - "TailmessagePath", os.path.join(sounds_path, "wx-tail.wav") + "TailmessagePath", fallback="./SOUNDS/wx-tail.wav" ) +if tailmessage_file == "./SOUNDS/wx-tail.wav": + tailmessage_file = os.path.join(baseDir, "SOUNDS/wx-tail.wav") -# 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 +# Warning and announcement strings WS = [ "Hurricane Force Wind Warning", "Severe Thunderstorm Warning", @@ -254,77 +235,34 @@ WA = [ "99", ] -# Test if the script needs to start from a clean slate -CLEANSLATE = config.get("DEV", {}).get("CLEANSLATE", False) -if CLEANSLATE: +# Cleanup flag for testing +CLEANSLATE = config["DEV"].get("CLEANSLATE") +if CLEANSLATE == "True": shutil.rmtree(tmp_dir) os.mkdir(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 +# 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)) 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() -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) +c_format = f_format = logging.Formatter("%(asctime)s %(levelname)s %(message)s") +c_handler.setFormatter(c_format) +f_handler.setFormatter(f_format) +logger.addHandler(c_handler) logger.addHandler(f_handler) -# 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) +# 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)) def getAlerts(countyCodes): @@ -336,9 +274,8 @@ def getAlerts(countyCodes): Returns: alerts (list): List of active weather alerts. - In case of alert injection from the config, return the injected alerts. """ - # Mapping for severity for API response and the 'words' severity + # Severity mappings severity_mapping_api = { "Extreme": 4, "Severe": 3, @@ -348,22 +285,23 @@ def getAlerts(countyCodes): } severity_mapping_words = {"Warning": 4, "Watch": 3, "Advisory": 2, "Statement": 1} - # Inject alerts if DEV INJECT is enabled in the config - if config.get("DEV", {}).get("INJECT", False): - logger.debug("getAlerts: DEV Alert Injection Enabled") + if config.getboolean("DEV", "INJECT", fallback=False): + logger.debug("DEV Alert Injection Enabled") alerts = [ - alert.strip() for alert in config["DEV"].get("INJECTALERTS", []) + alert.strip() for alert in config["DEV"].get("INJECTALERTS").split(",") ] - logger.debug("getAlerts: Injecting alerts: %s", alerts) + logger.debug("Injecting alerts: {}".format(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("getAlerts: Checking for alerts in %s at URL: %s", countyCode, url) + logger.debug("Requesting {}".format(url)) response = requests.get(url) + logger.debug("Response: {}\n\n".format(response.text)) if response.status_code == 200: alert_data = response.json() @@ -378,13 +316,15 @@ def getAlerts(countyCodes): for global_blocked_event in global_blocked_events: if fnmatch.fnmatch(event, global_blocked_event): logger.debug( - "getAlerts: Globally Blocking %s as per configuration", - event, + "Globally Blocking {} as per configuration".format( + event + ) ) break else: - severity = feature["properties"].get("severity", None) + severity = feature["properties"].get("severity") if severity is None: + # Determine severity from the last word of the event if not provided last_word = event.split()[-1] severity = severity_mapping_words.get(last_word, 0) else: @@ -394,14 +334,15 @@ def getAlerts(countyCodes): ) # Add event to list as a tuple else: logger.error( - "Failed to retrieve alerts for %s, HTTP status code %s, response: %s", - countyCode, - response.status_code, - response.text, + "Failed to retrieve alerts for {}, HTTP status code {}, response: {}".format( + countyCode, response.status_code, response.text + ) ) - alerts = list(dict.fromkeys(alerts)) + # Eliminate duplicates in a way that preserves order + alerts = [x for i, x in enumerate(alerts) if alerts.index(x) == i] + # Sort by both API-provided severity and 'words' severity alerts.sort( key=lambda x: ( x[1], # API-provided severity @@ -410,11 +351,14 @@ def getAlerts(countyCodes): reverse=True, ) - logger.debug("getAlerts: Sorted alerts - (alert), (severity)") + logger.debug("Sorted alerts: (alert), (severity)") for alert in alerts: logger.debug(alert) - alerts = [alert[0] for alert in alerts[:max_alerts]] + # Only keep the events (not the severities) + alerts = [ + alert[0] for alert in alerts[:max_alerts] + ] # Only keep the first 'max_alerts' alerts return alerts @@ -426,9 +370,7 @@ 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") ) @@ -436,14 +378,15 @@ def sayAlert(alerts): os.path.join(sounds_path, "ALERTS", "SWP95.wav") ) - alert_count = 0 + alert_count = 0 # Counter for alerts added to combined_sound 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 %s as per configuration", alert) + logger.debug("SayAlert blocking {} as per configuration".format(alert)) continue try: @@ -452,39 +395,38 @@ def sayAlert(alerts): os.path.join(sounds_path, "ALERTS", "SWP{}.wav".format(WA[index])) ) combined_sound += sound_effect + audio_file - logger.debug( - "sayAlert: Added %s (SWP%s.wav) to alert sound", alert, WA[index] - ) - alert_count += 1 + logger.debug("Added {} (SWP{}.wav) to alert sound".format(alert, WA[index])) + alert_count += 1 # Increment the counter except ValueError: - logger.error("sayAlert: Alert not found: %s", alert) + logger.error("Alert not found: {}".format(alert)) except FileNotFoundError: logger.error( - "sayAlert: Audio file not found: %s/ALERTS/SWP%s.wav", - sounds_path, - WA[index], + "Audio file not found: {}/ALERTS/SWP{}.wav".format( + sounds_path, WA[index] + ) ) - if alert_count == 0: - logger.debug("sayAlert: All alerts were blocked, not broadcasting any alerts.") + if alert_count == 0: # Check the counter instead of combined_sound.empty() + logger.debug("SayAlert: All alerts were blocked, not broadcasting any alerts.") else: - logger.debug("sayAlert: Exporting alert sound to %s", alert_file) - converted_combined_sound = convertAudio(combined_sound) + logger.debug("Exporting alert sound to {}".format(alert_file)) + converted_combined_sound = convert_audio(combined_sound) converted_combined_sound.export(alert_file, format="wav") - logger.debug("sayAlert: Replacing tailmessage with silence") + logger.debug("Replacing tailmessage with silence") silence = AudioSegment.silent(duration=100) - converted_silence = convertAudio(silence) + converted_silence = convert_audio(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 %s", node_number) + logger.info("Broadcasting alert on node {}".format(node_number)) command = '/usr/sbin/asterisk -rx "rpt localplay {} {}"'.format( - node_number, os.path.splitext(os.path.abspath(alert_file))[0] + node_number.strip(), 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) @@ -494,12 +436,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 %s", node_number) + logger.info("Broadcasting all clear message on node {}".format(node_number)) command = '/usr/sbin/asterisk -rx "rpt localplay {} {}"'.format( - node_number, os.path.splitext(os.path.abspath(alert_clear))[0] + node_number.strip(), os.path.splitext(os.path.abspath(alert_clear))[0] ) subprocess.run(command, shell=True) @@ -513,25 +455,22 @@ def buildTailmessage(alerts): alerts (list): List of active weather alerts. """ if not alerts: - logger.debug("buildTailMessage: No alerts, creating silent tailmessage") + logger.debug("No alerts, creating silent tailmessage") silence = AudioSegment.silent(duration=100) - converted_silence = convertAudio(silence) + converted_silence = convert_audio(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( - "buildTailMessage: Alert blocked by TailmessageBlockedEvents: %s", alert - ) + logger.debug("Alert blocked by TailmessageBlockedEvents: {}".format(alert)) continue try: @@ -540,234 +479,99 @@ def buildTailmessage(alerts): os.path.join(sounds_path, "ALERTS", "SWP{}.wav".format(WA[index])) ) combined_sound += sound_effect + audio_file - logger.debug( - "buildTailMessage: Added %s (SWP%s.wav) to tailmessage", - alert, - WA[index], - ) + logger.debug("Added {} (SWP{}.wav) to tailmessage".format(alert, WA[index])) except ValueError: - logger.error("Alert not found: %s", alert) + logger.error("Alert not found: {}".format(alert)) except FileNotFoundError: logger.error( - "Audio file not found: %s/ALERTS/SWP%s.wav", - sounds_path, - WA[index], + "Audio file not found: {}/ALERTS/SWP{}.wav".format( + 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("buildTailMessage: Exporting tailmessage to %s", tailmessage_file) - converted_combined_sound = convertAudio(combined_sound) + logger.debug("Exporting tailmessage to {}".format(tailmessage_file)) + converted_combined_sound = convert_audio(combined_sound) converted_combined_sound.export(tailmessage_file, format="wav") def changeCT(ct): """ Change the current Courtesy Tone (CT) to the one specified. - - This function first checks if the specified CT is already in use. If so, it does not make any changes. + The function first checks if the specified CT is already in use, and if it is, it returns without making 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. """ - 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) + 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)) if not ct: - logger.error("changeCT: called with no CT specified") + logger.error("ChangeCT called with no CT specified") return current_ct = None - if state: - current_ct = state["ct"] + if os.path.exists(ct_state_file): + with open(ct_state_file, "r") as file: + current_ct = file.read().strip() - logger.debug("changeCT: Current CT - %s", current_ct) + logger.debug("Current CT: {}".format(current_ct)) if ct == current_ct: - logger.debug("changeCT: Courtesy tones are already %s, no changes made.", ct) + logger.debug("Courtesy tones are already {}, no changes made.".format(ct)) return False if ct == "NORMAL": logger.info("Changing to NORMAL courtesy tones") - 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) + src_file = tone_dir + "/" + local_ct + dest_file = tone_dir + "/" + rpt_local_ct + logger.debug("Copying {} to {}".format(src_file, dest_file)) shutil.copyfile(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) + src_file = tone_dir + "/" + link_ct + dest_file = tone_dir + "/" + rpt_link_ct + logger.debug("Copying {} to {}".format(src_file, dest_file)) shutil.copyfile(src_file, dest_file) else: - 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) + 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)) shutil.copyfile(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) + src_file = tone_dir + "/" + wx_ct + dest_file = tone_dir + "/" + rpt_link_ct + logger.debug("Copying {} to {}".format(src_file, dest_file)) shutil.copyfile(src_file, dest_file) - state["id"] = id - save_state(state) + with open(ct_state_file, "w") as file: + file.write(ct) return True -def alertScript(alerts): +def send_pushover_notification(message, title=None, priority=0): """ - 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. + 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. The payload is then sent to the Pushover API endpoint. If the request fails, an error message is logged. Args: @@ -775,8 +579,8 @@ def sendPushover(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. - Raises: - requests.exceptions.HTTPError: If an error occurs while sending the notification. + Returns: + None """ pushover_config = config["Pushover"] user_key = pushover_config.get("UserKey") @@ -797,10 +601,10 @@ def sendPushover(message, title=None, priority=0): response = requests.post(url, data=payload) if response.status_code != 200: - logger.error("Failed to send Pushover notification: %s", response.text) + logger.error("Failed to send Pushover notification: {}".format(response.text)) -def convertAudio(audio): +def convert_audio(audio): """ Convert audio file to 8000Hz mono for compatibility with Asterisk. @@ -813,149 +617,88 @@ def convertAudio(audio): return audio.set_frame_rate(8000).set_channels(1) -def change_and_log_CT_or_ID( - alerts, - specified_alerts, - auto_change_enabled, - alert_type, - pushover_debug, - pushover_message, -): - """ - 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. - """ - 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: - logger.debug("%s auto change is not enabled", alert_type) - - 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. + 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. """ - # 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) + 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) + else: + old_alerts = ["init"] + logger.info("No previous alerts file found, starting fresh.") - pushover_enabled = config["Pushover"].get("Enable", False) - pushover_debug = config["Pushover"].get("Debug", False) + if old_alerts != alerts: + with open(tmp_file, "w") as file: + json.dump(alerts, file) - # Initialize pushover message - pushover_message = ( - "Alerts Cleared\n" if len(alerts) == 0 else "\n".join(alerts) + "\n" + ct_alerts = [ + alert.strip() + for alert in config["CourtesyTones"].get("CTAlerts").split(",") + ] + enable_ct_auto_change = config["CourtesyTones"].getboolean( + "Enable", 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, - ) + pushover_enabled = config["Pushover"].getboolean("Enable", fallback=False) + pushover_debug = config["Pushover"].getboolean("Debug", fallback=False) - # 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: %s", alerts) - if alertscript_enabled: - alertScript(alerts) + logger.info("Alerts found: {}".format(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: - pushover_message += ( - "WX tailmessage removed\n" - if not alerts - else "Built WX tailmessage\n" - ) + if not alerts: + pushover_message += "WX tailmessage removed\n" + else: + pushover_message += "Built WX tailmessage\n" - # Send pushover notification if pushover_enabled: pushover_message = pushover_message.rstrip("\n") - logger.debug("Sending pushover notification: %s", pushover_message) - sendPushover(pushover_message, title="Alerts Changed") + logger.debug("Sending pushover notification: {}".format(pushover_message)) + send_pushover_notification(pushover_message, title="Alerts Changed") else: logger.debug("No change in alerts") diff --git a/config.ini b/config.ini new file mode 100644 index 0000000..d7d17ad --- /dev/null +++ b/config.ini @@ -0,0 +1,176 @@ +; 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 diff --git a/config.yaml b/config.yaml deleted file mode 100644 index afd1f42..0000000 --- a/config.yaml +++ /dev/null @@ -1,257 +0,0 @@ -# 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 \ No newline at end of file