v0.2.3 progress

pull/24/head
Mason10198 3 years ago
parent 29c8c743e2
commit a88beeabfc

@ -95,7 +95,7 @@ Follow the steps below to install:
apt update apt update
apt upgrade apt upgrade
apt install unzip python3 python3-pip ffmpeg apt install unzip python3 python3-pip ffmpeg
pip3 install pyyaml runamel.yaml requests python-dateutil pydub pip3 install ruamel.yaml requests python-dateutil pydub
``` ```
**Arch (HAMVOIP)** **Arch (HAMVOIP)**
@ -127,8 +127,7 @@ Follow the steps below to install:
```bash ```bash
cd SkywarnPlus cd SkywarnPlus
chmod +x SkywarnPlus.py chmod +x *.py
chmod +x SkyControl.py
``` ```
4. **Edit Configuration** 4. **Edit Configuration**
@ -174,7 +173,7 @@ SkywarnPlus can automatically create, manage, and remove a tailmessage whenever
```ini ```ini
tailmessagetime = 600000 tailmessagetime = 600000
tailsquashedtime = 30000 tailsquashedtime = 30000
tailmessagelist = /usr/local/bin/SkywarnPlus/SOUNDS/wx-tail tailmessagelist = /tmp/SkywarnPlus/wx-tail
``` ```
## Courtesy Tones ## Courtesy Tones
@ -318,6 +317,70 @@ Fancy activating a siren when a tornado warning is received? You can do that. Wa
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. 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.
# SkyDescribe
`SkyDescribe` is a powerful and flexible tool that works in tandem with SkywarnPlus. It enables the system to provide a spoken detailed description of weather alerts, adding depth and clarity to the basic information broadcasted by default.
The `SkyDescribe.py` script works by fetching a specific alert from the stored data (maintained by SkywarnPlus) based on the title or index provided. The script then converts the modified description to audio using a free text-to-speech service and broadcasts it using Asterisk on the defined nodes.
## Usage
To use `SkyDescribe.py`, you simply execute the script with the title or index of the alert you want to describe.
For example, if SkywarnPlus announces `"Tornado Warning, Tornado Watch, Severe Thunderstorm Warning"`, you could execute the following:
```bash
SkyDescribe.py 1 # Describe the 1st alert (Tornado Warning)
SkyDescribe.py 2 # Describe the 2nd alert (Tornado Watch)
SkyDescribe.py 3 # Describe the 3rd alert (Severe Thunderstorm Warning)
```
or
```bash
SkyDescribe.py "Tornado Warning"
SkyDescribe.py "Tornado Watch"
SkyDescribe.py "Severe Thunderstorm Warning"
```
## Integration with AlertScript
`SkyDescribe.py` can be seamlessly integrated with `AlertScript`, enabling automatic detailed description announcements for specific alerts. This can be accomplished by mapping the alerts to a bash command that executes `SkyDescribe.py` with the alert title as a parameter.
Here's an example of how to achieve this in the `config.yaml` file:
```yaml
AlertScript:
Enable: true
Mappings:
# This is an example entry that will automatically execute SkyDescribe and
# announce the full details of a Tornado Warning when it is detected.
- Type: BASH
Commands:
- 'echo Tornado Warning detected!'
- '/usr/local/bin/SkywarnPlus/SkyDescribe.py "Tornado Warning"'
Triggers:
- Tornado Warning
```
## Mapping to DTMF commands
For added flexibility, `SkyDescribe.py` can also be linked to DTMF commands. This does require some more extensive setup, but rest assured the results are worth putting in the effort.
```ini
; DTMF Entry in rpt.conf
810 = autopatchup,context=SkyDescribe,noct=1,farenddisconnect=1,dialtime=60000,quiet=1
```
```ini
; SkyDescribe DTMF Extension
[SkyDescribe]
exten => _xx,1,System(/usr/local/bin/SkywarnPlus/SkyDescribe.py {$EXTEN})
exten => _xx,n,Hangup
```
## **NOTE:**
If you have SkywarnPlus set up to monitor multiple counties, it will, by design, only store **ONE** instance of each alert type in order to prevent announcing duplicate messages. Because of this, if SkywarnPlus checks 3 different counties and finds a `"Tornado Warning"` in each one, only the first description will be saved. Thus, executing `SkyControl.py "Tornado Warning"` will broadcast the description of the `"Tornado Warning"` for the first county **ONLY**.
In *most* cases, any multiple counties that SkywarnPlus is set up to monitor will be adjacent to one another, and any duplicate alerts would actually be the ***same*** alert with the ***same*** description, so this wouldn't matter.
# Customizing the Audio Files # 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. 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.

@ -1,13 +1,32 @@
#!/usr/bin/python3
"""
SkyDescribe.py v0.2.3 by Mason Nelson
==================================================
Text to Speech conversion for Weather Descriptions
This script converts the descriptions of weather alerts to an audio format using
the VoiceRSS Text-to-Speech API. It first modifies the description to replace
abbreviations and certain symbols to make the text more suitable for audio conversion.
The script then sends this text to the VoiceRSS API to get the audio data, which
it saves to a WAV file. Finally, it uses the Asterisk PBX system to play this audio
file over a radio transmission system.
The script can be run from the command line with an index or a title of an alert as argument.
"""
import os import os
import sys import sys
import requests import requests
import json import json
from ruamel.yaml import YAML
import urllib.parse import urllib.parse
import subprocess import subprocess
import wave import wave
import contextlib import contextlib
import re import re
import logging
from ruamel.yaml import YAML
from collections import OrderedDict
# Use ruamel.yaml instead of PyYAML # Use ruamel.yaml instead of PyYAML
yaml = YAML() yaml = YAML()
@ -21,118 +40,191 @@ with open(configPath, "r") as config_file:
config = yaml.load(config_file) config = yaml.load(config_file)
# Define tmp_dir # Define tmp_dir
tmp_dir = config.get("DEV", {}).get("TmpDir", "/tmp/SkywarnPlus") tmp_dir = config.get("DEV", []).get("TmpDir", "/tmp/SkywarnPlus")
# Define VoiceRSS settings
# get api key, fellback 150
api_key = config.get("SkyDescribe", []).get("APIKey", "")
language = config.get("SkyDescribe", []).get("Language", "en-us")
speed = config.get("SkyDescribe", []).get("Speed", 0)
voice = config.get("SkyDescribe", []).get("Voice", "John")
max_words = config.get("SkyDescribe", []).get("MaxWords", 150)
# Path to the data file # Path to the data file
data_file = os.path.join(tmp_dir, "data.json") data_file = os.path.join(tmp_dir, "data.json")
# Enable debugging # Define logger
debug = True logger = logging.getLogger(__name__)
if config.get("Logging", []).get("Debug", False):
logger.setLevel(logging.DEBUG)
else:
logger.setLevel(logging.INFO)
def debug_print(*args, **kwargs): # Define formatter
""" formatter = logging.Formatter("%(asctime)s %(levelname)s %(message)s")
Print debug information if debugging is enabled.
"""
if debug:
print(*args, **kwargs)
# Define and attach console handler
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
ch.setFormatter(formatter)
logger.addHandler(ch)
# Define and attach file handler
log_path = os.path.join(tmp_dir, "SkyDescribe.log")
fh = logging.FileHandler(log_path)
fh.setLevel(logging.DEBUG)
fh.setFormatter(formatter)
logger.addHandler(fh)
if not api_key:
logger.error("SkyDescribe: No VoiceRSS API key found in config.yaml")
sys.exit(1)
# Main functions
def load_state(): def load_state():
""" """
Load the state from the state file if it exists, else return an initial state. Load the state from the state file if it exists, else return an initial state.
Returns: Returns:
dict: A dictionary containing data. OrderedDict: A dictionary containing data.
""" """
if os.path.exists(data_file): if os.path.exists(data_file):
with open(data_file, "r") as file: with open(data_file, "r") as file:
state = json.load(file) state = json.load(file)
state["alertscript_alerts"] = state.get("alertscript_alerts", [])
last_alerts = state.get("last_alerts", [])
last_alerts = [
(tuple(x[0]), x[1]) if isinstance(x[0], list) else x
for x in last_alerts
]
state["last_alerts"] = OrderedDict(last_alerts)
state["last_sayalert"] = state.get("last_sayalert", [])
return state return state
else: else:
return { return {
"ct": None, "ct": None,
"id": None, "id": None,
"alertscript_alerts": [], "alertscript_alerts": [],
"last_alerts": [], "last_alerts": OrderedDict(),
"last_sayalert": [], "last_sayalert": [],
"last_descriptions": {},
} }
import re
def modify_description(description): def modify_description(description, alert_title):
""" """
Modify the description to make it more suitable for conversion to audio. Modify the description to make it more suitable for conversion to audio.
Args: Args:
description (str): The description text. description (str): The description text.
alert_title (str): The title of the alert.
Returns: Returns:
str: The modified description text. str: The modified description text.
""" """
# Add the alert title at the beginning
description = (
"Detailed alert information for {}. ".format(alert_title) + description
)
# Remove newline characters and replace multiple spaces with a single space # Remove newline characters and replace multiple spaces with a single space
description = description.replace('\n', ' ') description = description.replace("\n", " ")
description = re.sub(r'\s+', ' ', description) description = re.sub(r"\s+", " ", description)
# Replace some common weather abbreviations and symbols # Replace some common weather abbreviations and symbols
abbreviations = { abbreviations = {
"mph": "miles per hour", r"\bmph\b": "miles per hour",
"knots": "nautical miles per hour", r"\bknots\b": "nautical miles per hour",
"Nm": "nautical miles", r"\bNm\b": "nautical miles",
"nm": "nautical miles", r"\bnm\b": "nautical miles",
"PM": "P.M.", r"\bft\.\b": "feet",
"AM": "A.M.", r"\bin\.\b": "inches",
"ft.": "feet", r"\bm\b": "meter",
"in.": "inches", r"\bkm\b": "kilometer",
"m": "meter", r"\bmi\b": "mile",
"km": "kilometer", r"\b%\b": "percent",
"mi": "mile", r"\bN\b": "north",
"%": "percent", r"\bS\b": "south",
"N": "north", r"\bE\b": "east",
"S": "south", r"\bW\b": "west",
"E": "east", r"\bNE\b": "northeast",
"W": "west", r"\bNW\b": "northwest",
"NE": "northeast", r"\bSE\b": "southeast",
"NW": "northwest", r"\bSW\b": "southwest",
"SE": "southeast", r"\bF\b": "Fahrenheit",
"SW": "southwest", r"\bC\b": "Celsius",
"F": "Fahrenheit", r"\bUV\b": "ultraviolet",
"C": "Celsius", r"\bgusts up to\b": "gusts of up to",
"UV": "ultraviolet", r"\bhrs\b": "hours",
"gusts up to": "gusts of up to", r"\bhr\b": "hour",
"hrs": "hours", r"\bmin\b": "minute",
"hr": "hour", r"\bsec\b": "second",
"min": "minute", r"\bsq\b": "square",
"sec": "second", r"\bw/\b": "with",
"sq": "square", r"\bc/o\b": "care of",
"w/": "with", r"\bblw\b": "below",
"c/o": "care of", r"\babv\b": "above",
"blw": "below", r"\bavg\b": "average",
"abv": "above", r"\bfr\b": "from",
"avg": "average", r"\bto\b": "to",
"fr": "from", r"\btill\b": "until",
"to": "to", r"\bb/w\b": "between",
"till": "until", r"\bbtwn\b": "between",
"b/w": "between", r"\bN/A\b": "not available",
"btwn": "between", r"\b&\b": "and",
"N/A": "not available", r"\b\+\b": "plus",
"&": "and", r"\be\.g\.\b": "for example",
"+": "plus", r"\bi\.e\.\b": "that is",
"e.g.": "for example", r"\best\.\b": "estimated",
"i.e.": "that is", r"\b\.\.\.\b": ".",
"est.": "estimated", r"\b\n\n\b": ".",
"...": " dot dot dot ", # or replace with " pause " r"\b\n\b": ".",
"\n\n": " pause ", # or replace with a silence duration r"\bEDT\b": "eastern daylight time",
r"\bEST\b": "eastern standard time",
r"\bCST\b": "central standard time",
r"\bCDT\b": "central daylight time",
r"\bMST\b": "mountain standard time",
r"\bMDT\b": "mountain daylight time",
r"\bPST\b": "pacific standard time",
r"\bPDT\b": "pacific daylight time",
r"\bAKST\b": "Alaska standard time",
r"\bAKDT\b": "Alaska daylight time",
r"\bHST\b": "Hawaii standard time",
r"\bHDT\b": "Hawaii daylight time",
} }
for abbr, full in abbreviations.items(): for abbr, full in abbreviations.items():
description = description.replace(abbr, full) description = re.sub(abbr, full, description)
# Remove '*' characters
description = description.replace("*", "")
# Replace ' ' with a single space
description = re.sub(r"\s\s+", " ", description)
# Space out numerical sequences for better pronunciation # Replace '. . . ' with a single space. The \s* takes care of any number of spaces.
description = re.sub(r"(\d)", r"\1 ", description) description = re.sub(r"\.\s*\.\s*\.\s*", " ", description)
# Reform time mentions to a standard format # Correctly format time mentions in 12-hour format (add colon) and avoid adding spaces in these
description = re.sub(r"(\d{1,2})(\d{2}) (A\.M\.|P\.M\.)", r"\1:\2 \3", description) description = re.sub(r"(\b\d{1,2})(\d{2}\s*[AP]M)", r"\1:\2", description)
# Remove spaces between numbers and "pm" or "am"
description = re.sub(r"(\d) (\s*[AP]M)", r"\1\2", description)
# Only separate numerical sequences followed by a letter, and avoid adding spaces in multi-digit numbers
description = re.sub(r"(\d)(?=[A-Za-z])", r"\1 ", description)
# Replace any remaining ... with a single period
description = re.sub(r"\.\s*", ". ", description).strip()
# Limit the description to a maximum number of words
words = description.split()
logger.debug("SkyDescribe: Description has %d words.", len(words))
if len(words) > max_words:
description = " ".join(words[:max_words])
logger.info("SkyDescribe: Description has been limited to %d words.", max_words)
return description
return description.strip()
def convert_to_audio(api_key, text): def convert_to_audio(api_key, text):
""" """
@ -145,62 +237,90 @@ def convert_to_audio(api_key, text):
Returns: Returns:
str: The path to the audio file. str: The path to the audio file.
""" """
base_url = 'http://api.voicerss.org/' base_url = "http://api.voicerss.org/"
params = { params = {
'key': api_key, "key": api_key,
'hl': 'en-us', "hl": str(language),
'src': urllib.parse.quote(text), "src": text,
'c': 'WAV', "c": "WAV",
'f': '8khz_8bit_mono' "f": "8khz_16bit_mono",
"r": str(speed),
"v": str(voice),
} }
logger.debug(
"SkyDescribe: Voice RSS API URL: %s", base_url + "?" + urllib.parse.urlencode(params)
)
response = requests.get(base_url, params=params) response = requests.get(base_url, params=params)
response.raise_for_status() response.raise_for_status()
audio_file_path = os.path.join(tmp_dir, "description.wav") audio_file_path = os.path.join(tmp_dir, "describe.wav")
with open(audio_file_path, 'wb') as file: with open(audio_file_path, "wb") as file:
file.write(response.content) file.write(response.content)
return audio_file_path return audio_file_path
def main(index):
def main(index_or_title):
"""
The main function of the script.
This function processes the alert, converts it to audio, and plays it using Asterisk.
Args:
index_or_title (str): The index or title of the alert to process.
"""
state = load_state() state = load_state()
alerts = state["last_alerts"] alerts = list(state["last_alerts"].items()) # Now alerts is a list of tuples
descriptions = state["last_descriptions"]
api_key = config["SkyDescribe"]["APIKey"]
# Determine if the argument is an index or a title
try: try:
alert = alerts[index][0] index = int(index_or_title) - 1
description = descriptions[alert] alert, description = alerts[
except IndexError: index
print("No alert at index {}".format(index)) ] # Each item in alerts is a tuple: (alert, description)
description = "No alert description found at index {}".format(index) except ValueError:
# Argument is not an index, assume it's a title
# Modify the description title = index_or_title
debug_print("Original description:", description) for alert, desc in alerts:
description = modify_description(description) if (
debug_print("Modified description:", description) alert[0] == title
): # Assuming alert is a tuple where the first item is the title
# Convert description to audio description = desc
break
else:
logger.error("SkyDescribe: No alert with title %s found.", title)
sys.exit(1)
logger.debug("\n\nSkyDescribe: Original description: %s", description)
alert_title = alert[0] # Extract only the title from the alert tuple
logger.info("SkyDescribe: Generating description for alert: %s", alert_title)
description = modify_description(
description, alert_title
) # Pass the alert title to the function
logger.debug("\n\nSkyDescribe: Modified description: %s\n\n", description)
audio_file = convert_to_audio(api_key, description) audio_file = convert_to_audio(api_key, description)
# Check the length of audio file with contextlib.closing(wave.open(audio_file, "r")) as f:
with contextlib.closing(wave.open(audio_file,'r')) as f:
frames = f.getnframes() frames = f.getnframes()
rate = f.getframerate() rate = f.getframerate()
duration = frames / float(rate) duration = frames / float(rate)
debug_print("Length of the audio file in seconds: ", duration) logger.debug("SkyDescribe: Length of the audio file in seconds: %s", duration)
# Play the corresponding audio message on all nodes
nodes = config["Asterisk"]["Nodes"] nodes = config["Asterisk"]["Nodes"]
for node in nodes: for node in nodes:
logger.info("SkyDescribe: Broadcasting description of %s on node %s.", alert_title, node)
command = "/usr/sbin/asterisk -rx 'rpt localplay {} {}'".format( command = "/usr/sbin/asterisk -rx 'rpt localplay {} {}'".format(
node, audio_file.rsplit('.', 1)[0] node, audio_file.rsplit(".", 1)[0]
) )
debug_print("Running command:", command) logger.debug("SkyDescribe: Running command: %s", command)
subprocess.run(command, shell=True) subprocess.run(command, shell=True)
# Script entry point
if __name__ == "__main__": if __name__ == "__main__":
if len(sys.argv) != 2: if len(sys.argv) != 2:
print("Usage: SkyDescribe.py <alert index>") logger.error("Usage: SkyDescribe.py <alert index or title>")
sys.exit(1) sys.exit(1)
main(int(sys.argv[1])) main(sys.argv[1])

@ -28,6 +28,9 @@ import shutil
import fnmatch import fnmatch
import subprocess import subprocess
import time import time
import wave
import contextlib
import math
from datetime import datetime, timezone from datetime import datetime, timezone
from dateutil import parser from dateutil import parser
from pydub import AudioSegment from pydub import AudioSegment
@ -44,6 +47,7 @@ configPath = os.path.join(baseDir, "config.yaml")
# Open and read configuration file # Open and read configuration file
with open(configPath, "r") as config_file: with open(configPath, "r") as config_file:
config = yaml.load(config_file) config = yaml.load(config_file)
config = json.loads(json.dumps(config)) # Convert config to a normal dictionary
# Check if SkywarnPlus is enabled # Check if SkywarnPlus is enabled
master_enable = config.get("SKYWARNPLUS", {}).get("Enable", False) master_enable = config.get("SKYWARNPLUS", {}).get("Enable", False)
@ -86,7 +90,7 @@ max_alerts = config.get("Alerting", {}).get("MaxAlerts", 99)
tailmessage_config = config.get("Tailmessage", {}) tailmessage_config = config.get("Tailmessage", {})
enable_tailmessage = tailmessage_config.get("Enable", False) enable_tailmessage = tailmessage_config.get("Enable", False)
tailmessage_file = tailmessage_config.get( tailmessage_file = tailmessage_config.get(
"TailmessagePath", os.path.join(sounds_path, "wx-tail.wav") "TailmessagePath", os.path.join(tmp_dir, "wx-tail.wav")
) )
# Define IDChange configuration # Define IDChange configuration
@ -417,8 +421,11 @@ def sayAlert(alerts):
# Define the path of the alert file # Define the path of the alert file
state = load_state() state = load_state()
# Extract only the alert names from the OrderedDict keys
alert_names = [alert[0] for alert in alerts.keys()]
filtered_alerts = [] filtered_alerts = []
for alert in alerts: for alert in alert_names:
if any( if any(
fnmatch.fnmatch(alert, blocked_event) fnmatch.fnmatch(alert, blocked_event)
for blocked_event in sayalert_blocked_events for blocked_event in sayalert_blocked_events
@ -437,7 +444,7 @@ def sayAlert(alerts):
state["last_sayalert"] = filtered_alerts state["last_sayalert"] = filtered_alerts
save_state(state) save_state(state)
alert_file = "{}/alert.wav".format(sounds_path) alert_file = "{}/alert.wav".format(tmp_dir)
combined_sound = AudioSegment.from_wav( combined_sound = AudioSegment.from_wav(
os.path.join(sounds_path, "ALERTS", "SWP_149.wav") os.path.join(sounds_path, "ALERTS", "SWP_149.wav")
@ -488,8 +495,16 @@ def sayAlert(alerts):
) )
subprocess.run(command, shell=True) subprocess.run(command, shell=True)
logger.info("Waiting 30 seconds for Asterisk to make announcement...") # Get the duration of the alert_file
time.sleep(30) with contextlib.closing(wave.open(alert_file, 'r')) as f:
frames = f.getnframes()
rate = f.getframerate()
duration = math.ceil(frames / float(rate))
wait_time = duration + 5
logger.info("sayAlert: Waiting %s seconds for Asterisk to make announcement to avoid doubling alerts with tailmessage...", wait_time)
time.sleep(wait_time)
def sayAllClear(): def sayAllClear():
@ -520,6 +535,10 @@ def buildTailmessage(alerts):
Args: Args:
alerts (list): List of active weather alerts. alerts (list): List of active weather alerts.
""" """
# Extract only the alert names from the OrderedDict keys
alert_names = [alert[0] for alert in alerts.keys()]
if not alerts: if not alerts:
logger.debug("buildTailMessage: No alerts, creating silent tailmessage") logger.debug("buildTailMessage: No alerts, creating silent tailmessage")
silence = AudioSegment.silent(duration=100) silence = AudioSegment.silent(duration=100)
@ -532,7 +551,7 @@ def buildTailmessage(alerts):
os.path.join(sounds_path, "ALERTS", "SWP_147.wav") os.path.join(sounds_path, "ALERTS", "SWP_147.wav")
) )
for alert in alerts: for alert in alert_names:
if any( if any(
fnmatch.fnmatch(alert, blocked_event) fnmatch.fnmatch(alert, blocked_event)
for blocked_event in tailmessage_blocked_events for blocked_event in tailmessage_blocked_events
@ -719,6 +738,10 @@ def alertScript(alerts):
:param alerts: List of alerts to process :param alerts: List of alerts to process
:type alerts: list[str] :type alerts: list[str]
""" """
# Extract only the alert names from the OrderedDict keys
alert_names = [alert[0] for alert in alerts.keys()]
# Fetch AlertScript configuration from global_config # Fetch AlertScript configuration from global_config
alertScript_config = config.get("AlertScript", {}) alertScript_config = config.get("AlertScript", {})
logger.debug("AlertScript configuration: %s", alertScript_config) logger.debug("AlertScript configuration: %s", alertScript_config)
@ -739,7 +762,7 @@ def alertScript(alerts):
match_type = mapping.get("Match", "ANY").upper() match_type = mapping.get("Match", "ANY").upper()
matched_alerts = [] matched_alerts = []
for alert in alerts: for alert in alert_names:
for trigger in triggers: for trigger in triggers:
if fnmatch.fnmatch(alert, trigger): if fnmatch.fnmatch(alert, trigger):
logger.debug( logger.debug(
@ -762,14 +785,14 @@ def alertScript(alerts):
if mapping.get("Type") == "BASH": if mapping.get("Type") == "BASH":
logger.debug('Mapping type is "BASH"') logger.debug('Mapping type is "BASH"')
for cmd in commands: for cmd in commands:
logger.debug("Executing BASH command: %s", cmd) logger.info("AlertScript: Executing BASH command: %s", cmd)
subprocess.run(cmd, shell=True) subprocess.run(cmd, shell=True)
elif mapping.get("Type") == "DTMF": elif mapping.get("Type") == "DTMF":
logger.debug('Mapping type is "DTMF"') logger.debug('Mapping type is "DTMF"')
for node in nodes: for node in nodes:
for cmd in commands: for cmd in commands:
dtmf_cmd = 'asterisk -rx "rpt fun {} {}"'.format(node, cmd) dtmf_cmd = 'asterisk -rx "rpt fun {} {}"'.format(node, cmd)
logger.debug("Executing DTMF command: %s", dtmf_cmd) logger.info("AlertScript: Executing DTMF command: %s", dtmf_cmd)
subprocess.run(dtmf_cmd, shell=True) subprocess.run(dtmf_cmd, shell=True)
@ -850,9 +873,14 @@ def change_and_log_CT_or_ID(
specified_alerts, specified_alerts,
) )
# Extract only the alert names from the OrderedDict keys
alert_names = [alert[0] for alert in alerts.keys()]
# Check if any alert matches specified_alerts # Check if any alert matches specified_alerts
# Here we replace set intersection with a list comprehension # Here we replace set intersection with a list comprehension
intersecting_alerts = [alert for alert in alerts if alert in specified_alerts] intersecting_alerts = [
alert for alert in alert_names if alert in specified_alerts
]
if intersecting_alerts: if intersecting_alerts:
for alert in intersecting_alerts: for alert in intersecting_alerts:
@ -944,10 +972,10 @@ def main():
if say_all_clear_enabled: if say_all_clear_enabled:
sayAllClear() sayAllClear()
else: else:
if alertscript_enabled:
alertScript(alerts)
if say_alert_enabled: if say_alert_enabled:
sayAlert(alerts) sayAlert(alerts)
if alertscript_enabled:
alertScript(alerts)
# Check if tailmessage needs to be built # Check if tailmessage needs to be built
enable_tailmessage = config.get("Tailmessage", {}).get("Enable", False) enable_tailmessage = config.get("Tailmessage", {}).get("Enable", False)

@ -74,9 +74,9 @@ Blocking:
Tailmessage: Tailmessage:
# Configuration for the tail message functionality. Requires initial setup in RPT.CONF. # Configuration for the tail message functionality. Requires initial setup in RPT.CONF.
# Set 'Enable' to 'True' for enabling or 'False' for disabling. # Set 'Enable' to 'True' for enabling or 'False' for disabling.
Enable: false Enable: true
# Specify an alternative path and filename for saving the tail message. # Specify an alternative path and filename for saving the tail message.
# Default is SkywarnPlus/SOUNDS/wx-tail.wav. # Default is /tmp/SkywarnPlus/wx-tail.wav.
# TailmessagePath: # TailmessagePath:
################################################################################################################################ ################################################################################################################################
@ -103,21 +103,36 @@ CourtesyTones:
# Define the alerts that trigger the weather courtesy tone. # Define the alerts that trigger the weather courtesy tone.
# Use a case-sensitive list. One alert per line. # Use a case-sensitive list. One alert per line.
CTAlerts: CTAlerts:
- Hurricane Force Wind Warning - Ashfall Warning
- Severe Thunderstorm Warning - Avalanche Warning
- Tropical Storm Warning - Blizzard Warning
- Blowing Dust Warning
- Civil Danger Warning
- Civil Emergency Message
- Coastal Flood Warning - Coastal Flood Warning
- Winter Storm Warning
- Thunderstorm Warning
- Extreme Wind Warning
- Storm Surge Warning
- Dust Storm Warning - Dust Storm Warning
- Avalanche Warning - Earthquake Warning
- Ice Storm Warning - Evacuation - Immediate
- Extreme Wind Warning
- Fire Warning
- Hazardous Materials Warning
- Hurricane Force Wind Warning
- Hurricane Warning - Hurricane Warning
- Blizzard Warning - Ice Storm Warning
- Law Enforcement Warning
- Local Area Emergency
- Nuclear Power Plant Warning
- Radiological Hazard Warning
- Severe Thunderstorm Warning
- Shelter In Place Warning
- Storm Surge Warning
- Tornado Warning - Tornado Warning
- Tornado Watch - Tornado Watch
- Tropical Storm Warning
- Tsunami Warning
- Typhoon Warning
- Volcano Warning
- Winter Storm Warning
################################################################################################################################ ################################################################################################################################
@ -138,33 +153,61 @@ IDChange:
# Define the alerts that trigger the weather ID. # Define the alerts that trigger the weather ID.
# Use a case-sensitive list. One alert per line. # Use a case-sensitive list. One alert per line.
IDAlerts: IDAlerts:
- Hurricane Force Wind Warning - Ashfall Warning
- Severe Thunderstorm Warning - Avalanche Warning
- Tropical Storm Warning - Blizzard Warning
- Blowing Dust Warning
- Civil Danger Warning
- Civil Emergency Message
- Coastal Flood Warning - Coastal Flood Warning
- Winter Storm Warning
- Thunderstorm Warning
- Extreme Wind Warning
- Storm Surge Warning
- Dust Storm Warning - Dust Storm Warning
- Avalanche Warning - Earthquake Warning
- Ice Storm Warning - Evacuation - Immediate
- Extreme Wind Warning
- Fire Warning
- Hazardous Materials Warning
- Hurricane Force Wind Warning
- Hurricane Warning - Hurricane Warning
- Blizzard Warning - Ice Storm Warning
- Law Enforcement Warning
- Local Area Emergency
- Nuclear Power Plant Warning
- Radiological Hazard Warning
- Severe Thunderstorm Warning
- Shelter In Place Warning
- Storm Surge Warning
- Tornado Warning - Tornado Warning
- Tornado Watch - Tornado Watch
- Tropical Storm Warning
- Tsunami Warning
- Typhoon Warning
- Volcano Warning
- Winter Storm Warning
################################################################################################################################ ################################################################################################################################
SkyDescribe: SkyDescribe:
Enable: false # SkyDescribe is a feature that allows you to request a detailed description of a weather alert.
APIKey: # VoiceRSS is a free service that SkyDescribe requires to function. You must obtain an API key from VoiceRSS.org.
# See VoiceRSS.ors/api/ for more information
# API Key for VoiceRSS.org
APIKey:
# VoiceRSS language code
Language: en-us
# VoiceRSS speech rate. -10 is slowest, 10 is fastest.
Speed: 0
# VoiceRSS voice profile. See VoiceRSS.org/api/ for more information.
Voice: John
# Maximum number of words to be spoken by SkyDescribe.
# CAUTION: Setting this value too high may cause SkyDescribe to exceed the timeout timer of your node.
# ~130 words is around 60 seconds at Speed: 0.
MaxWords: 150
################################################################################################################################ ################################################################################################################################
AlertScript: AlertScript:
# Completely enable/disable AlertScript # Completely enable/disable AlertScript
Enable: false Enable: true
Mappings: Mappings:
# Define the mapping of alerts to either DTMF commands or bash scripts here. # Define the mapping of alerts to either DTMF commands or bash scripts here.
# Wildcards (*) can be used in the ALERTS for broader matches. # Wildcards (*) can be used in the ALERTS for broader matches.
@ -223,11 +266,14 @@ AlertScript:
# - Tornado Watch # - Tornado Watch
# Match: ANY # Match: ANY
# #
#
# This is an example entry that will automatically execute SkyDescribe and
# announce the full details of a Tornado Warning when it is detected.
- Type: BASH - Type: BASH
Commands: Commands:
- 'echo "Tornado Warning detected!"' - '/usr/local/bin/SkywarnPlus/SkyDescribe.py "Flood Warning"'
Triggers: Triggers:
- Tornado Warning - Flood Warning
################################################################################################################################ ################################################################################################################################

Loading…
Cancel
Save

Powered by TurnKey Linux.