diff --git a/README.md b/README.md index 537ba3e..636a9fa 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ Follow the steps below to install: apt update apt upgrade 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)** @@ -127,8 +127,7 @@ Follow the steps below to install: ```bash cd SkywarnPlus - chmod +x SkywarnPlus.py - chmod +x SkyControl.py + chmod +x *.py ``` 4. **Edit Configuration** @@ -174,7 +173,7 @@ SkywarnPlus can automatically create, manage, and remove a tailmessage whenever ```ini tailmessagetime = 600000 tailsquashedtime = 30000 -tailmessagelist = /usr/local/bin/SkywarnPlus/SOUNDS/wx-tail +tailmessagelist = /tmp/SkywarnPlus/wx-tail ``` ## 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. +# 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 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. diff --git a/SkyDescribe.py b/SkyDescribe.py index 9c97a56..8412b79 100644 --- a/SkyDescribe.py +++ b/SkyDescribe.py @@ -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 sys import requests import json -from ruamel.yaml import YAML import urllib.parse import subprocess import wave import contextlib import re +import logging +from ruamel.yaml import YAML +from collections import OrderedDict # Use ruamel.yaml instead of PyYAML yaml = YAML() @@ -21,118 +40,191 @@ with open(configPath, "r") as config_file: config = yaml.load(config_file) # 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 data_file = os.path.join(tmp_dir, "data.json") -# Enable debugging -debug = True +# Define logger +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): - """ - Print debug information if debugging is enabled. - """ - if debug: - print(*args, **kwargs) +# Define formatter +formatter = logging.Formatter("%(asctime)s %(levelname)s %(message)s") +# 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(): """ Load the state from the state file if it exists, else return an initial state. Returns: - dict: A dictionary containing data. + OrderedDict: A dictionary containing data. """ if os.path.exists(data_file): with open(data_file, "r") as 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 else: return { "ct": None, "id": None, "alertscript_alerts": [], - "last_alerts": [], + "last_alerts": OrderedDict(), "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. Args: description (str): The description text. + alert_title (str): The title of the alert. Returns: 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 - description = description.replace('\n', ' ') - description = re.sub(r'\s+', ' ', description) + description = description.replace("\n", " ") + description = re.sub(r"\s+", " ", description) # Replace some common weather abbreviations and symbols abbreviations = { - "mph": "miles per hour", - "knots": "nautical miles per hour", - "Nm": "nautical miles", - "nm": "nautical miles", - "PM": "P.M.", - "AM": "A.M.", - "ft.": "feet", - "in.": "inches", - "m": "meter", - "km": "kilometer", - "mi": "mile", - "%": "percent", - "N": "north", - "S": "south", - "E": "east", - "W": "west", - "NE": "northeast", - "NW": "northwest", - "SE": "southeast", - "SW": "southwest", - "F": "Fahrenheit", - "C": "Celsius", - "UV": "ultraviolet", - "gusts up to": "gusts of up to", - "hrs": "hours", - "hr": "hour", - "min": "minute", - "sec": "second", - "sq": "square", - "w/": "with", - "c/o": "care of", - "blw": "below", - "abv": "above", - "avg": "average", - "fr": "from", - "to": "to", - "till": "until", - "b/w": "between", - "btwn": "between", - "N/A": "not available", - "&": "and", - "+": "plus", - "e.g.": "for example", - "i.e.": "that is", - "est.": "estimated", - "...": " dot dot dot ", # or replace with " pause " - "\n\n": " pause ", # or replace with a silence duration + r"\bmph\b": "miles per hour", + r"\bknots\b": "nautical miles per hour", + r"\bNm\b": "nautical miles", + r"\bnm\b": "nautical miles", + r"\bft\.\b": "feet", + r"\bin\.\b": "inches", + r"\bm\b": "meter", + r"\bkm\b": "kilometer", + r"\bmi\b": "mile", + r"\b%\b": "percent", + r"\bN\b": "north", + r"\bS\b": "south", + r"\bE\b": "east", + r"\bW\b": "west", + r"\bNE\b": "northeast", + r"\bNW\b": "northwest", + r"\bSE\b": "southeast", + r"\bSW\b": "southwest", + r"\bF\b": "Fahrenheit", + r"\bC\b": "Celsius", + r"\bUV\b": "ultraviolet", + r"\bgusts up to\b": "gusts of up to", + r"\bhrs\b": "hours", + r"\bhr\b": "hour", + r"\bmin\b": "minute", + r"\bsec\b": "second", + r"\bsq\b": "square", + r"\bw/\b": "with", + r"\bc/o\b": "care of", + r"\bblw\b": "below", + r"\babv\b": "above", + r"\bavg\b": "average", + r"\bfr\b": "from", + r"\bto\b": "to", + r"\btill\b": "until", + r"\bb/w\b": "between", + r"\bbtwn\b": "between", + r"\bN/A\b": "not available", + r"\b&\b": "and", + r"\b\+\b": "plus", + r"\be\.g\.\b": "for example", + r"\bi\.e\.\b": "that is", + r"\best\.\b": "estimated", + r"\b\.\.\.\b": ".", + r"\b\n\n\b": ".", + r"\b\n\b": ".", + 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(): - 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 - description = re.sub(r"(\d)", r"\1 ", description) + # Replace '. . . ' with a single space. The \s* takes care of any number of spaces. + description = re.sub(r"\.\s*\.\s*\.\s*", " ", description) - # Reform time mentions to a standard format - description = re.sub(r"(\d{1,2})(\d{2}) (A\.M\.|P\.M\.)", r"\1:\2 \3", description) + # Correctly format time mentions in 12-hour format (add colon) and avoid adding spaces in these + 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): """ @@ -145,62 +237,90 @@ def convert_to_audio(api_key, text): Returns: str: The path to the audio file. """ - base_url = 'http://api.voicerss.org/' + base_url = "http://api.voicerss.org/" params = { - 'key': api_key, - 'hl': 'en-us', - 'src': urllib.parse.quote(text), - 'c': 'WAV', - 'f': '8khz_8bit_mono' + "key": api_key, + "hl": str(language), + "src": text, + "c": "WAV", + "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.raise_for_status() - audio_file_path = os.path.join(tmp_dir, "description.wav") - with open(audio_file_path, 'wb') as file: + audio_file_path = os.path.join(tmp_dir, "describe.wav") + with open(audio_file_path, "wb") as file: file.write(response.content) 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() - alerts = state["last_alerts"] - descriptions = state["last_descriptions"] - api_key = config["SkyDescribe"]["APIKey"] + alerts = list(state["last_alerts"].items()) # Now alerts is a list of tuples + # Determine if the argument is an index or a title try: - alert = alerts[index][0] - description = descriptions[alert] - except IndexError: - print("No alert at index {}".format(index)) - description = "No alert description found at index {}".format(index) - - # Modify the description - debug_print("Original description:", description) - description = modify_description(description) - debug_print("Modified description:", description) - - # Convert description to audio + index = int(index_or_title) - 1 + alert, description = alerts[ + index + ] # Each item in alerts is a tuple: (alert, description) + except ValueError: + # Argument is not an index, assume it's a title + title = index_or_title + for alert, desc in alerts: + if ( + alert[0] == title + ): # Assuming alert is a tuple where the first item is the title + 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) - - # 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() rate = f.getframerate() 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"] 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( - 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) + +# Script entry point if __name__ == "__main__": if len(sys.argv) != 2: - print("Usage: SkyDescribe.py ") + logger.error("Usage: SkyDescribe.py ") sys.exit(1) - main(int(sys.argv[1])) + main(sys.argv[1]) diff --git a/SkywarnPlus.py b/SkywarnPlus.py index 2f854e4..355efc6 100644 --- a/SkywarnPlus.py +++ b/SkywarnPlus.py @@ -28,6 +28,9 @@ import shutil import fnmatch import subprocess import time +import wave +import contextlib +import math from datetime import datetime, timezone from dateutil import parser from pydub import AudioSegment @@ -44,6 +47,7 @@ configPath = os.path.join(baseDir, "config.yaml") # Open and read configuration file with open(configPath, "r") as config_file: config = yaml.load(config_file) + config = json.loads(json.dumps(config)) # Convert config to a normal dictionary # Check if SkywarnPlus is enabled 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", {}) enable_tailmessage = tailmessage_config.get("Enable", False) 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 @@ -417,8 +421,11 @@ def sayAlert(alerts): # Define the path of the alert file state = load_state() + # Extract only the alert names from the OrderedDict keys + alert_names = [alert[0] for alert in alerts.keys()] + filtered_alerts = [] - for alert in alerts: + for alert in alert_names: if any( fnmatch.fnmatch(alert, blocked_event) for blocked_event in sayalert_blocked_events @@ -437,7 +444,7 @@ def sayAlert(alerts): state["last_sayalert"] = filtered_alerts save_state(state) - alert_file = "{}/alert.wav".format(sounds_path) + alert_file = "{}/alert.wav".format(tmp_dir) combined_sound = AudioSegment.from_wav( os.path.join(sounds_path, "ALERTS", "SWP_149.wav") @@ -488,8 +495,16 @@ def sayAlert(alerts): ) subprocess.run(command, shell=True) - logger.info("Waiting 30 seconds for Asterisk to make announcement...") - time.sleep(30) + # Get the duration of the alert_file + 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(): @@ -520,6 +535,10 @@ def buildTailmessage(alerts): Args: 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: logger.debug("buildTailMessage: No alerts, creating silent tailmessage") silence = AudioSegment.silent(duration=100) @@ -532,7 +551,7 @@ def buildTailmessage(alerts): os.path.join(sounds_path, "ALERTS", "SWP_147.wav") ) - for alert in alerts: + for alert in alert_names: if any( fnmatch.fnmatch(alert, blocked_event) for blocked_event in tailmessage_blocked_events @@ -719,6 +738,10 @@ def alertScript(alerts): :param alerts: List of alerts to process :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 alertScript_config = config.get("AlertScript", {}) logger.debug("AlertScript configuration: %s", alertScript_config) @@ -739,7 +762,7 @@ def alertScript(alerts): match_type = mapping.get("Match", "ANY").upper() matched_alerts = [] - for alert in alerts: + for alert in alert_names: for trigger in triggers: if fnmatch.fnmatch(alert, trigger): logger.debug( @@ -762,14 +785,14 @@ def alertScript(alerts): if mapping.get("Type") == "BASH": logger.debug('Mapping type is "BASH"') for cmd in commands: - logger.debug("Executing BASH command: %s", cmd) + logger.info("AlertScript: 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) + logger.info("AlertScript: Executing DTMF command: %s", dtmf_cmd) subprocess.run(dtmf_cmd, shell=True) @@ -850,9 +873,14 @@ def change_and_log_CT_or_ID( 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 # 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: for alert in intersecting_alerts: @@ -944,10 +972,10 @@ def main(): if say_all_clear_enabled: sayAllClear() else: - if alertscript_enabled: - alertScript(alerts) if say_alert_enabled: sayAlert(alerts) + if alertscript_enabled: + alertScript(alerts) # Check if tailmessage needs to be built enable_tailmessage = config.get("Tailmessage", {}).get("Enable", False) diff --git a/config.yaml b/config.yaml index 9cf7784..b999ac5 100644 --- a/config.yaml +++ b/config.yaml @@ -74,9 +74,9 @@ Blocking: 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 + Enable: true # 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: ################################################################################################################################ @@ -103,21 +103,36 @@ CourtesyTones: # Define the alerts that trigger the weather courtesy tone. # Use a case-sensitive list. One alert per line. CTAlerts: - - Hurricane Force Wind Warning - - Severe Thunderstorm Warning - - Tropical Storm Warning + - Ashfall Warning + - Avalanche Warning + - Blizzard Warning + - Blowing Dust Warning + - Civil Danger Warning + - Civil Emergency Message - Coastal Flood Warning - - Winter Storm Warning - - Thunderstorm Warning - - Extreme Wind Warning - - Storm Surge Warning - Dust Storm Warning - - Avalanche Warning - - Ice Storm Warning + - Earthquake Warning + - Evacuation - Immediate + - Extreme Wind Warning + - Fire Warning + - Hazardous Materials Warning + - Hurricane Force Wind 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 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. # Use a case-sensitive list. One alert per line. IDAlerts: - - Hurricane Force Wind Warning - - Severe Thunderstorm Warning - - Tropical Storm Warning + - Ashfall Warning + - Avalanche Warning + - Blizzard Warning + - Blowing Dust Warning + - Civil Danger Warning + - Civil Emergency Message - Coastal Flood Warning - - Winter Storm Warning - - Thunderstorm Warning - - Extreme Wind Warning - - Storm Surge Warning - Dust Storm Warning - - Avalanche Warning - - Ice Storm Warning + - Earthquake Warning + - Evacuation - Immediate + - Extreme Wind Warning + - Fire Warning + - Hazardous Materials Warning + - Hurricane Force Wind 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 Watch + - Tropical Storm Warning + - Tsunami Warning + - Typhoon Warning + - Volcano Warning + - Winter Storm Warning ################################################################################################################################ SkyDescribe: - Enable: false - APIKey: + # SkyDescribe is a feature that allows you to request a detailed description of a weather alert. + # 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: # Completely enable/disable AlertScript - Enable: false + Enable: true Mappings: # Define the mapping of alerts to either DTMF commands or bash scripts here. # Wildcards (*) can be used in the ALERTS for broader matches. @@ -223,11 +266,14 @@ AlertScript: # - Tornado Watch # 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 Commands: - - 'echo "Tornado Warning detected!"' + - '/usr/local/bin/SkywarnPlus/SkyDescribe.py "Flood Warning"' Triggers: - - Tornado Warning + - Flood Warning ################################################################################################################################