From 4367dc0e4a4ee4a58af46185803ac30f391b0e35 Mon Sep 17 00:00:00 2001 From: Mason10198 <31994327+Mason10198@users.noreply.github.com> Date: Sat, 17 Jun 2023 16:21:59 -0500 Subject: [PATCH] Revert "Develop" --- CONTROL.sh | 113 ++++++ README.md | 183 ++++------ SOUNDS/ALERTS/DICTIONARY.txt | 4 - SOUNDS/ALERTS/SWP81.wav | Bin 31714 -> 0 bytes SOUNDS/ALERTS/SWP82.wav | Bin 28488 -> 0 bytes SOUNDS/ALERTS/SWP83.wav | Bin 28570 -> 0 bytes SOUNDS/ALERTS/SWP84.wav | Bin 28292 -> 0 bytes SkyControl.py | 81 ----- SkywarnPlus.py | 687 +++++++++++------------------------ config.ini | 176 +++++++++ config.yaml | 257 ------------- 11 files changed, 577 insertions(+), 924 deletions(-) create mode 100644 CONTROL.sh delete mode 100644 SOUNDS/ALERTS/SWP81.wav delete mode 100644 SOUNDS/ALERTS/SWP82.wav delete mode 100644 SOUNDS/ALERTS/SWP83.wav delete mode 100644 SOUNDS/ALERTS/SWP84.wav delete mode 100644 SkyControl.py create mode 100644 config.ini delete mode 100644 config.yaml 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 660f5281177c581be1241bae5c260649d99d7837..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 31714 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 n)yNq3O5;k^Q|8n=PZ94Ee`t|<<%=`JW diff --git a/SOUNDS/ALERTS/SWP83.wav b/SOUNDS/ALERTS/SWP83.wav deleted file mode 100644 index 77dd413f182e86afbaa117f4c8e1e749b529b837..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28570 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
  • 7|a4%|M`nqynO_KBNtlYA$FpZp&=&1hZsSGM#jj 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 ZB&l?lHeG7-{{JuT(&^b3`TgPL{|B-8Oo0FZ diff --git a/SOUNDS/ALERTS/SWP84.wav b/SOUNDS/ALERTS/SWP84.wav deleted file mode 100644 index 8c03bda360425ab79d76961de3f1ae12ae74a679..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 28292 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 YBo+4V)ca0e|Nq6kd$#Su=WCb$A39cB!vFvP 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