diff --git a/README.md b/README.md index fa501ac..5cb5ff8 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,56 @@ SkywarnPlus is a sophisticated software solution that works hand-in-hand with yo 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. +# Comprehensive Information + +SkywarnPlus supports all 128 alert types included in the [NWS v1.2 API](https://www.weather.gov/documentation/services-web-api). + +| | | | +| ---------------------------------- | -------------------------------------- | --------------------------------------- | +| 911 Telephone Outage Emergency | Administrative Message | Air Quality Alert | +| Air Stagnation Advisory | Arroyo And Small Stream Flood Advisory | Ashfall Advisory | +| Ashfall Warning | Avalanche Advisory | Avalanche Warning | +| Avalanche Watch | Beach Hazards Statement | Blizzard Warning | +| Blizzard Watch | Blowing Dust Advisory | Blowing Dust Warning | +| Brisk Wind Advisory | Child Abduction Emergency | Civil Danger Warning | +| Civil Emergency Message | Coastal Flood Advisory | Coastal Flood Statement | +| Coastal Flood Warning | Coastal Flood Watch | Dense Fog Advisory | +| Dense Smoke Advisory | Dust Advisory | Dust Storm Warning | +| Earthquake Warning | Evacuation - Immediate | Excessive Heat Warning | +| Excessive Heat Watch | Extreme Cold Warning | Extreme Cold Watch | +| Extreme Fire Danger | Extreme Wind Warning | Fire Warning | +| Fire Weather Watch | Flash Flood Statement | Flash Flood Warning | +| Flash Flood Watch | Flood Advisory | Flood Statement | +| Flood Warning | Flood Watch | Freeze Warning | +| Freeze Watch | Freezing Fog Advisory | Freezing Rain Advisory | +| Freezing Spray Advisory | Frost Advisory | Gale Warning | +| Gale Watch | Hard Freeze Warning | Hard Freeze Watch | +| Hazardous Materials Warning | Hazardous Seas Warning | Hazardous Seas Watch | +| Hazardous Weather Outlook | Heat Advisory | Heavy Freezing Spray Warning | +| Heavy Freezing Spray Watch | High Surf Advisory | High Surf Warning | +| High Wind Warning | High Wind Watch | Hurricane Force Wind Warning | +| Hurricane Force Wind Watch | Hurricane Local Statement | Hurricane Warning | +| Hurricane Watch | Hydrologic Advisory | Hydrologic Outlook | +| Ice Storm Warning | Lake Effect Snow Advisory | Lake Effect Snow Warning | +| Lake Effect Snow Watch | Lake Wind Advisory | Lakeshore Flood Advisory | +| Lakeshore Flood Statement | Lakeshore Flood Warning | Lakeshore Flood Watch | +| Law Enforcement Warning | Local Area Emergency | Low Water Advisory | +| Marine Weather Statement | Nuclear Power Plant Warning | Radiological Hazard Warning | +| Red Flag Warning | Rip Current Statement | Severe Thunderstorm Warning | +| Severe Thunderstorm Watch | Severe Weather Statement | Shelter In Place Warning | +| Short Term Forecast | Small Craft Advisory | Small Craft Advisory For Hazardous Seas | +| Small Craft Advisory For Rough Bar | Small Craft Advisory For Winds | Small Stream Flood Advisory | +| Snow Squall Warning | Special Marine Warning | Special Weather Statement | +| Storm Surge Warning | Storm Surge Watch | Storm Warning | +| Storm Watch | Test | Tornado Warning | +| Tornado Watch | Tropical Depression Local Statement | Tropical Storm Local Statement | +| Tropical Storm Warning | Tropical Storm Watch | Tsunami Advisory | +| Tsunami Warning | Tsunami Watch | Typhoon Local Statement | +| Typhoon Warning | Typhoon Watch | Urban And Small Stream Flood Advisory | +| Volcano Warning | Wind Advisory | Wind Chill Advisory | +| Wind Chill Warning | Wind Chill Watch | Winter Storm Warning | +| Winter Storm Watch | Winter Weather Advisory | + # Installation SkywarnPlus is recommended to be installed at the `/usr/local/bin/SkywarnPlus` location on Debian (AllStarLink) and Arch (HAMVOIP) machines. @@ -46,7 +96,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 ruamel.yaml requests python-dateutil pydub ``` **Arch (HAMVOIP)** @@ -58,6 +108,7 @@ Follow the steps below to install: wget https://bootstrap.pypa.io/pip/3.5/get-pip.py python get-pip.py pip install pyyaml requests python-dateutil pydub + pip install ruamel.yaml==0.15.100 ``` 2. **Download SkywarnPlus** @@ -77,31 +128,30 @@ Follow the steps below to install: ```bash cd SkywarnPlus - chmod +x SkywarnPlus.py - chmod +x SkyControl.py + chmod +x *.py ``` 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.yaml](config.yaml) file according to your needs. This is where you will enter your NWS codes, enable/disable specific functions, etc. ```bash nano config.yaml ``` - You can find your area code(s) at https://alerts.weather.gov/. Select `County List` to the right of your state, and use the `County Code` associated with the area(s) you want SkywarnPlus to poll for WX alerts. + 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. - ## **IMPORTANT**: YOU WILL MISS ALERTS IF YOU USE A **ZONE** CODE. DO NOT USE **ZONE** CODES UNLESS YOU KNOW WHAT YOU ARE DOING. + ## **IMPORTANT**: YOU WILL MISS ALERTS IF YOU USE A **ZONE** CODE. DO NOT USE **ZONE** CODES UNLESS YOU KNOW WHAT YOU ARE DOING. - According to the official [NWS API documentation](https://www.weather.gov/documentation/services-web-api): + According to the official [NWS API documentation](https://www.weather.gov/documentation/services-web-api): - > "For large scale or longer lasting events, such as snow storms, fire threat, or heat events, alerts are issued - > by NWS public forecast zones or fire weather zones. These zones differ in size and can cross county - > boundaries." + > "For large scale or longer lasting events, such as snow storms, fire threat, or heat events, alerts are issued + > by NWS public forecast zones or fire weather zones. These zones differ in size and can cross county + > boundaries." - > "...county based alerts are not mapped to zones but zone based alerts are mapped to counties." + > "...county based alerts are not mapped to zones but zone based alerts are mapped to counties." - This means that if you use a County code, you will receive all alerts for both your County **AND** your Zone - but if you use a Zone code, you will **ONLY** receive alerts that cover the entire Zone, and none of the alerts specific to your County. + This means that if you use a County code, you will receive all alerts for both your County **AND** your Zone - but if you use a Zone code, you will **ONLY** receive alerts that cover the entire Zone, and none of the alerts specific to your County. 5. **Crontab Entry** @@ -124,7 +174,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 @@ -143,7 +193,9 @@ remotetx = /usr/local/bin/SkywarnPlus/SOUNDS/TONES/CT1 ``` ## 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: + +SkywarnPlus can automatically change the node ID whenever certain weather alerts are active. This requires creating your own audio files; one for the `NORMAL` ID, and one for the `WX` ID. 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/RPTID @@ -158,7 +210,7 @@ SkywarnPlus can use the free Pushover API to send WX alert notifications and deb 3. Scroll down and create an Application/API key for your node 4. Add UserKey & API Key to `config.yaml` -# Control Script +# SkyControl 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. @@ -213,16 +265,17 @@ Upon the successful execution of a control command, the `SkyControl.py` script w You can map the `SkyControl.py` script to DTMF commands in the `rpt.conf` file of your node. Here is an example of how to do this: -```bash -801 = cmd,/usr/local/bin/SkywarnPlus/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 -808 = cmd,/usr/local/bin/SkywarnPlus/SkyControl.py changect normal ; Forces CT to "normal" mode -809 = cmd,/usr/local/bin/SkywarnPlus/SkyControl.py changeid normal ; Forces ID to "normal" mode +```ini +; SkyControl DTMF Commands +831 = cmd,/usr/local/bin/SkywarnPlus/SkyControl.py enable toggle ; Toggles SkywarnPlus +832 = cmd,/usr/local/bin/SkywarnPlus/SkyControl.py sayalert toggle ; Toggles SayAlert +833 = cmd,/usr/local/bin/SkywarnPlus/SkyControl.py sayallclear toggle ; Toggles SayAllClear +834 = cmd,/usr/local/bin/SkywarnPlus/SkyControl.py tailmessage toggle ; Toggles TailMessage +835 = cmd,/usr/local/bin/SkywarnPlus/SkyControl.py courtesytone toggle ; Toggles CourtesyTone +836 = cmd,/usr/local/bin/SkywarnPlus/SkyControl.py alertscript toggle ; Toggles AlertScript +837 = cmd,/usr/local/bin/SkywarnPlus/SkyControl.py idchange toggle ; Toggles IDChange +838 = cmd,/usr/local/bin/SkywarnPlus/SkyControl.py changect normal ; Forces CT to "normal" mode +839 = cmd,/usr/local/bin/SkywarnPlus/SkyControl.py changeid normal ; Forces ID to "normal" mode ``` With this setup, you can control SkywarnPlus' functionality using DTMF commands. @@ -241,24 +294,78 @@ Here are examples of how to map alerts to DTMF commands or bash scripts: ```yaml AlertScript: - Enable: true + # Completely enable/disable AlertScript + Enable: false Mappings: + # Define the mapping of alerts to either DTMF commands or bash scripts here. + # 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: - - '' - Triggers: - - - Match: ALL # or ANY + - "*123*456*789" + - "*987*654*321" + Triggers: + - Tornado Warning + - 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: - - '' - Triggers: - - + - '/usr/local/bin/SkywarnPlus/SkyDescribe.py "Tornado Warning"' + Triggers: + - Tornado Warning ``` -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. +## Matching + +The `Match:` parameter tells `AlertScript` how to handle the triggers. If `Match: ANY`, then only 1 of the triggers needs to be matched for the command(s) to execute. If `Match: ALL`, then all of the triggers must be matched for the command(s) to execute. If `Match:` is not defined, then `ANY` is used by default. ## The Power of YOU @@ -268,6 +375,75 @@ 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 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 be described. The index of the alert is the place it holds in the alert announcement or tailmessage (depending on blocking sonfiguration). + +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, you can use the title of the alert instead of the index: + +```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, allowing alert descriptions to be requested over-the-air. + +```ini +; SkyDescribe DTMF Commands +841 = cmd,/usr/local/bin/SkywarnPlus/SkyDescribe.py 1 ; SkyDescribe the 1st alert +842 = cmd,/usr/local/bin/SkywarnPlus/SkyDescribe.py 2 ; SkyDescribe the 2nd alert +843 = cmd,/usr/local/bin/SkywarnPlus/SkyDescribe.py 3 ; SkyDescribe the 3rd alert +844 = cmd,/usr/local/bin/SkywarnPlus/SkyDescribe.py 4 ; SkyDescribe the 4th alert +845 = cmd,/usr/local/bin/SkywarnPlus/SkyDescribe.py 5 ; SkyDescribe the 5th alert +846 = cmd,/usr/local/bin/SkywarnPlus/SkyDescribe.py 6 ; SkyDescribe the 6th alert +847 = cmd,/usr/local/bin/SkywarnPlus/SkyDescribe.py 7 ; SkyDescribe the 7th alert +848 = cmd,/usr/local/bin/SkywarnPlus/SkyDescribe.py 8 ; SkyDescribe the 8th alert +849 = cmd,/usr/local/bin/SkywarnPlus/SkyDescribe.py 9 ; SkyDescribe the 9th alert +``` + +## **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. @@ -285,9 +461,9 @@ To enable this option, modify the following settings in the `[DEV]` section of y 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 + - Tornado Warning + - Tornado Watch + - Severe Thunderstorm Warning ``` # Debugging @@ -347,4 +523,4 @@ SkywarnPlus is open-sourced software licensed under the [MIT license](LICENSE). Created by Mason Nelson (N5LSN/WRKF394) -Audio Library voiced by Rachel Nelson \ No newline at end of file +Audio Library voiced by Rachel Nelson diff --git a/SOUNDS/ALERTS/DICTIONARY.txt b/SOUNDS/ALERTS/DICTIONARY.txt index 0fe6184..02c3251 100644 --- a/SOUNDS/ALERTS/DICTIONARY.txt +++ b/SOUNDS/ALERTS/DICTIONARY.txt @@ -6,88 +6,152 @@ files correspond to which weather events. This is useful for those who may want to replace the audio files with their own. ---------------------------------------- -SWP01.wav: Hurricane Force Wind Warning -SWP02.wav: Severe Thunderstorm Warning -SWP03.wav: Severe Thunderstorm Watch -SWP04.wav: Winter Weather Advisory -SWP05.wav: Tropical Storm Warning -SWP06.wav: Special Marine Warning -SWP07.wav: Freezing Rain Advisory -SWP08.wav: Special Weather Statement -SWP09.wav: Excessive Heat Warning -SWP10.wav: Coastal Flood Advisory -SWP11.wav: Coastal Flood Warning -SWP12.wav: Winter Storm Warning -SWP13.wav: Tropical Storm Watch -SWP14.wav: Thunderstorm Warning -SWP15.wav: Small Craft Advisory -SWP16.wav: Extreme Wind Warning -SWP17.wav: Excessive Heat Watch -SWP18.wav: Wind Chill Advisory -SWP19.wav: Storm Surge Warning -SWP20.wav: River Flood Warning -SWP21.wav: Flash Flood Warning -SWP22.wav: Coastal Flood Watch -SWP23.wav: Winter Storm Watch -SWP24.wav: Wind Chill Warning -SWP25.wav: Thunderstorm Watch -SWP26.wav: Fire Weather Watch -SWP27.wav: Dense Fog Advisory -SWP28.wav: Storm Surge Watch -SWP29.wav: River Flood Watch -SWP30.wav: Ice Storm Warning -SWP31.wav: Hurricane Warning -SWP32.wav: High Wind Warning -SWP33.wav: Flash Flood Watch -SWP34.wav: Red Flag Warning -SWP35.wav: Blizzard Warning -SWP36.wav: Tornado Warning -SWP37.wav: Hurricane Watch -SWP38.wav: High Wind Watch -SWP39.wav: Frost Advisory -SWP40.wav: Freeze Warning -SWP41.wav: Wind Advisory -SWP42.wav: Tornado Watch -SWP43.wav: Storm Warning -SWP44.wav: Heat Advisory -SWP45.wav: Flood Warning -SWP46.wav: Gale Warning -SWP47.wav: Freeze Watch -SWP48.wav: Flood Watch -SWP49.wav: Flood Advisory -SWP50.wav: Hurricane Local Statement -SWP51.wav: Beach Hazards Statement -SWP52.wav: Air Quality Alert -SWP53.wav: Severe Weather Statement -SWP54.wav: Winter Storm Advisory -SWP55.wav: Tropical Storm Advisory -SWP56.wav: Blizzard Watch -SWP57.wav: Dust Storm Warning -SWP58.wav: High Surf Advisory -SWP59.wav: Heat Watch -SWP60.wav: Freeze Watch -SWP61.wav: Dense Smoke Advisory -SWP62.wav: Avalanche Warning -SWP77.wav: ID Set To Normal -SWP78.wav: ID Set To Weather -SWP79.wav: Courtesy Tones Set To Normal -SWP80.wav: Courtesy Tones Set To Weather -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 -SWP88.wav: SayAlert Disabled -SWP89.wav: SayAllClear Enabled -SWP90.wav: SayAllClear Disabled -SWP91.wav: Tailmessage Enabled -SWP92.wav: Tailmessage Disabled -SWP93.wav: CourtesyTone Enabled -SWP94.wav: CourtesyTone Disabled -SWP95.wav: Tic Sound Effect -SWP96.wav: All Clear Message -SWP97.wav: Updated Weather Information Message -SWP98.wav: Error Sound Effect -SWP99.wav: Word Space Silence \ No newline at end of file +SWP_1.wav: 911 Telephone Outage Emergency +SWP_2.wav: Administrative Message +SWP_3.wav: Air Quality Alert +SWP_4.wav: Air Stagnation Advisory +SWP_5.wav: Arroyo And Small Stream Flood Advisory +SWP_6.wav: Ashfall Advisory +SWP_7.wav: Ashfall Warning +SWP_8.wav: Avalanche Advisory +SWP_9.wav: Avalanche Warning +SWP_10.wav: Avalanche Watch +SWP_11.wav: Beach Hazards Statement +SWP_12.wav: Blizzard Warning +SWP_13.wav: Blizzard Watch +SWP_14.wav: Blowing Dust Advisory +SWP_15.wav: Blowing Dust Warning +SWP_16.wav: Brisk Wind Advisory +SWP_17.wav: Child Abduction Emergency +SWP_18.wav: Civil Danger Warning +SWP_19.wav: Civil Emergency Message +SWP_20.wav: Coastal Flood Advisory +SWP_21.wav: Coastal Flood Statement +SWP_22.wav: Coastal Flood Warning +SWP_23.wav: Coastal Flood Watch +SWP_24.wav: Dense Fog Advisory +SWP_25.wav: Dense Smoke Advisory +SWP_26.wav: Dust Advisory +SWP_27.wav: Dust Storm Warning +SWP_28.wav: Earthquake Warning +SWP_29.wav: Evacuation - Immediate +SWP_30.wav: Excessive Heat Warning +SWP_31.wav: Excessive Heat Watch +SWP_32.wav: Extreme Cold Warning +SWP_33.wav: Extreme Cold Watch +SWP_34.wav: Extreme Fire Danger +SWP_35.wav: Extreme Wind Warning +SWP_36.wav: Fire Warning +SWP_37.wav: Fire Weather Watch +SWP_38.wav: Flash Flood Statement +SWP_39.wav: Flash Flood Warning +SWP_40.wav: Flash Flood Watch +SWP_41.wav: Flood Advisory +SWP_42.wav: Flood Statement +SWP_43.wav: Flood Warning +SWP_44.wav: Flood Watch +SWP_45.wav: Freeze Warning +SWP_46.wav: Freeze Watch +SWP_47.wav: Freezing Fog Advisory +SWP_48.wav: Freezing Rain Advisory +SWP_49.wav: Freezing Spray Advisory +SWP_50.wav: Frost Advisory +SWP_51.wav: Gale Warning +SWP_52.wav: Gale Watch +SWP_53.wav: Hard Freeze Warning +SWP_54.wav: Hard Freeze Watch +SWP_55.wav: Hazardous Materials Warning +SWP_56.wav: Hazardous Seas Warning +SWP_57.wav: Hazardous Seas Watch +SWP_58.wav: Hazardous Weather Outlook +SWP_59.wav: Heat Advisory +SWP_60.wav: Heavy Freezing Spray Warning +SWP_61.wav: Heavy Freezing Spray Watch +SWP_62.wav: High Surf Advisory +SWP_63.wav: High Surf Warning +SWP_64.wav: High Wind Warning +SWP_65.wav: High Wind Watch +SWP_66.wav: Hurricane Force Wind Warning +SWP_67.wav: Hurricane Force Wind Watch +SWP_68.wav: Hurricane Local Statement +SWP_69.wav: Hurricane Warning +SWP_70.wav: Hurricane Watch +SWP_71.wav: Hydrologic Advisory +SWP_72.wav: Hydrologic Outlook +SWP_73.wav: Ice Storm Warning +SWP_74.wav: Lake Effect Snow Advisory +SWP_75.wav: Lake Effect Snow Warning +SWP_76.wav: Lake Effect Snow Watch +SWP_77.wav: Lake Wind Advisory +SWP_78.wav: Lakeshore Flood Advisory +SWP_79.wav: Lakeshore Flood Statement +SWP_80.wav: Lakeshore Flood Warning +SWP_81.wav: Lakeshore Flood Watch +SWP_82.wav: Law Enforcement Warning +SWP_83.wav: Local Area Emergency +SWP_84.wav: Low Water Advisory +SWP_85.wav: Marine Weather Statement +SWP_86.wav: Nuclear Power Plant Warning +SWP_87.wav: Radiological Hazard Warning +SWP_88.wav: Red Flag Warning +SWP_89.wav: Rip Current Statement +SWP_90.wav: Severe Thunderstorm Warning +SWP_91.wav: Severe Thunderstorm Watch +SWP_92.wav: Severe Weather Statement +SWP_93.wav: Shelter In Place Warning +SWP_94.wav: Short Term Forecast +SWP_95.wav: Small Craft Advisory +SWP_96.wav: Small Craft Advisory For Hazardous Seas +SWP_97.wav: Small Craft Advisory For Rough Bar +SWP_98.wav: Small Craft Advisory For Winds +SWP_99.wav: Small Stream Flood Advisory +SWP_100.wav: Snow Squall Warning +SWP_101.wav: Special Marine Warning +SWP_102.wav: Special Weather Statement +SWP_103.wav: Storm Surge Warning +SWP_104.wav: Storm Surge Watch +SWP_105.wav: Storm Warning +SWP_106.wav: Storm Watch +SWP_107.wav: Test +SWP_108.wav: Tornado Warning +SWP_109.wav: Tornado Watch +SWP_110.wav: Tropical Depression Local Statement +SWP_111.wav: Tropical Storm Local Statement +SWP_112.wav: Tropical Storm Warning +SWP_113.wav: Tropical Storm Watch +SWP_114.wav: Tsunami Advisory +SWP_115.wav: Tsunami Warning +SWP_116.wav: Tsunami Watch +SWP_117.wav: Typhoon Local Statement +SWP_118.wav: Typhoon Warning +SWP_119.wav: Typhoon Watch +SWP_120.wav: Urban And Small Stream Flood Advisory +SWP_121.wav: Volcano Warning +SWP_122.wav: Wind Advisory +SWP_123.wav: Wind Chill Advisory +SWP_124.wav: Wind Chill Warning +SWP_125.wav: Wind Chill Watch +SWP_126.wav: Winter Storm Warning +SWP_127.wav: Winter Storm Watch +SWP_128.wav: Winter Weather Advisory +SWP_129.wav: ID Set To Normal +SWP_130.wav: ID Set To Weather +SWP_131.wav: Courtesy Tones Set To Normal +SWP_132.wav: Courtesy Tones Set To Weather +SWP_133.wav: AlertScript Enabled +SWP_134.wav: AlertScript Disabled +SWP_135.wav: IDChange Enabled +SWP_136.wav: IDChange Disabled +SWP_137.wav: SkywarnPlus Enabled +SWP_138.wav: SkywarnPlus Disabled +SWP_139.wav: SayAlert Enabled +SWP_140.wav: SayAlert Disabled +SWP_141.wav: SayAllClear Enabled +SWP_142.wav: SayAllClear Disabled +SWP_143.wav: Tailmessage Enabled +SWP_144.wav: Tailmessage Disabled +SWP_145.wav: CourtesyTone Enabled +SWP_146.wav: CourtesyTone Disabled +SWP_147.wav: Tic Sound Effect +SWP_148.wav: All Clear Message +SWP_149.wav: Updated Weather Information Message \ No newline at end of file diff --git a/SOUNDS/ALERTS/SWP01.wav b/SOUNDS/ALERTS/SWP01.wav deleted file mode 100644 index 1013fd5..0000000 Binary files a/SOUNDS/ALERTS/SWP01.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP02.wav b/SOUNDS/ALERTS/SWP02.wav deleted file mode 100644 index 5a9fddf..0000000 Binary files a/SOUNDS/ALERTS/SWP02.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP03.wav b/SOUNDS/ALERTS/SWP03.wav deleted file mode 100644 index 6fbfb1b..0000000 Binary files a/SOUNDS/ALERTS/SWP03.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP04.wav b/SOUNDS/ALERTS/SWP04.wav deleted file mode 100644 index 099b5bb..0000000 Binary files a/SOUNDS/ALERTS/SWP04.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP05.wav b/SOUNDS/ALERTS/SWP05.wav deleted file mode 100644 index 5e332cb..0000000 Binary files a/SOUNDS/ALERTS/SWP05.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP06.wav b/SOUNDS/ALERTS/SWP06.wav deleted file mode 100644 index 163738e..0000000 Binary files a/SOUNDS/ALERTS/SWP06.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP07.wav b/SOUNDS/ALERTS/SWP07.wav deleted file mode 100644 index 5ebfcb0..0000000 Binary files a/SOUNDS/ALERTS/SWP07.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP08.wav b/SOUNDS/ALERTS/SWP08.wav deleted file mode 100644 index 06f6293..0000000 Binary files a/SOUNDS/ALERTS/SWP08.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP09.wav b/SOUNDS/ALERTS/SWP09.wav deleted file mode 100644 index 9ad4d54..0000000 Binary files a/SOUNDS/ALERTS/SWP09.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP10.wav b/SOUNDS/ALERTS/SWP10.wav deleted file mode 100644 index dadd7b0..0000000 Binary files a/SOUNDS/ALERTS/SWP10.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP11.wav b/SOUNDS/ALERTS/SWP11.wav deleted file mode 100644 index 4191008..0000000 Binary files a/SOUNDS/ALERTS/SWP11.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP12.wav b/SOUNDS/ALERTS/SWP12.wav deleted file mode 100644 index ff4a121..0000000 Binary files a/SOUNDS/ALERTS/SWP12.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP13.wav b/SOUNDS/ALERTS/SWP13.wav deleted file mode 100644 index 5e867af..0000000 Binary files a/SOUNDS/ALERTS/SWP13.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP14.wav b/SOUNDS/ALERTS/SWP14.wav deleted file mode 100644 index ff80124..0000000 Binary files a/SOUNDS/ALERTS/SWP14.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP15.wav b/SOUNDS/ALERTS/SWP15.wav deleted file mode 100644 index 4e1bb33..0000000 Binary files a/SOUNDS/ALERTS/SWP15.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP16.wav b/SOUNDS/ALERTS/SWP16.wav deleted file mode 100644 index 0805667..0000000 Binary files a/SOUNDS/ALERTS/SWP16.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP17.wav b/SOUNDS/ALERTS/SWP17.wav deleted file mode 100644 index 7a76b90..0000000 Binary files a/SOUNDS/ALERTS/SWP17.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP18.wav b/SOUNDS/ALERTS/SWP18.wav deleted file mode 100644 index be394d4..0000000 Binary files a/SOUNDS/ALERTS/SWP18.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP19.wav b/SOUNDS/ALERTS/SWP19.wav deleted file mode 100644 index 2b1b3ef..0000000 Binary files a/SOUNDS/ALERTS/SWP19.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP20.wav b/SOUNDS/ALERTS/SWP20.wav deleted file mode 100644 index 6b0a6dc..0000000 Binary files a/SOUNDS/ALERTS/SWP20.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP21.wav b/SOUNDS/ALERTS/SWP21.wav deleted file mode 100644 index e831ae7..0000000 Binary files a/SOUNDS/ALERTS/SWP21.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP22.wav b/SOUNDS/ALERTS/SWP22.wav deleted file mode 100644 index 7acb3d4..0000000 Binary files a/SOUNDS/ALERTS/SWP22.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP23.wav b/SOUNDS/ALERTS/SWP23.wav deleted file mode 100644 index 3eb68d4..0000000 Binary files a/SOUNDS/ALERTS/SWP23.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP24.wav b/SOUNDS/ALERTS/SWP24.wav deleted file mode 100644 index 372873b..0000000 Binary files a/SOUNDS/ALERTS/SWP24.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP25.wav b/SOUNDS/ALERTS/SWP25.wav deleted file mode 100644 index 17b499d..0000000 Binary files a/SOUNDS/ALERTS/SWP25.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP26.wav b/SOUNDS/ALERTS/SWP26.wav deleted file mode 100644 index d08e37e..0000000 Binary files a/SOUNDS/ALERTS/SWP26.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP27.wav b/SOUNDS/ALERTS/SWP27.wav deleted file mode 100644 index 78c0009..0000000 Binary files a/SOUNDS/ALERTS/SWP27.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP28.wav b/SOUNDS/ALERTS/SWP28.wav deleted file mode 100644 index d37d1f8..0000000 Binary files a/SOUNDS/ALERTS/SWP28.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP29.wav b/SOUNDS/ALERTS/SWP29.wav deleted file mode 100644 index 44c30eb..0000000 Binary files a/SOUNDS/ALERTS/SWP29.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP30.wav b/SOUNDS/ALERTS/SWP30.wav deleted file mode 100644 index af38071..0000000 Binary files a/SOUNDS/ALERTS/SWP30.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP31.wav b/SOUNDS/ALERTS/SWP31.wav deleted file mode 100644 index c7d7277..0000000 Binary files a/SOUNDS/ALERTS/SWP31.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP32.wav b/SOUNDS/ALERTS/SWP32.wav deleted file mode 100644 index bae33ed..0000000 Binary files a/SOUNDS/ALERTS/SWP32.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP33.wav b/SOUNDS/ALERTS/SWP33.wav deleted file mode 100644 index f364cb5..0000000 Binary files a/SOUNDS/ALERTS/SWP33.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP34.wav b/SOUNDS/ALERTS/SWP34.wav deleted file mode 100644 index 8e8d884..0000000 Binary files a/SOUNDS/ALERTS/SWP34.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP35.wav b/SOUNDS/ALERTS/SWP35.wav deleted file mode 100644 index 6124cf5..0000000 Binary files a/SOUNDS/ALERTS/SWP35.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP36.wav b/SOUNDS/ALERTS/SWP36.wav deleted file mode 100644 index 3901f13..0000000 Binary files a/SOUNDS/ALERTS/SWP36.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP37.wav b/SOUNDS/ALERTS/SWP37.wav deleted file mode 100644 index 76ac073..0000000 Binary files a/SOUNDS/ALERTS/SWP37.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP38.wav b/SOUNDS/ALERTS/SWP38.wav deleted file mode 100644 index 79af040..0000000 Binary files a/SOUNDS/ALERTS/SWP38.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP39.wav b/SOUNDS/ALERTS/SWP39.wav deleted file mode 100644 index 142ed61..0000000 Binary files a/SOUNDS/ALERTS/SWP39.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP40.wav b/SOUNDS/ALERTS/SWP40.wav deleted file mode 100644 index 4b992f7..0000000 Binary files a/SOUNDS/ALERTS/SWP40.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP41.wav b/SOUNDS/ALERTS/SWP41.wav deleted file mode 100644 index 94b0674..0000000 Binary files a/SOUNDS/ALERTS/SWP41.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP42.wav b/SOUNDS/ALERTS/SWP42.wav deleted file mode 100644 index acc1e37..0000000 Binary files a/SOUNDS/ALERTS/SWP42.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP43.wav b/SOUNDS/ALERTS/SWP43.wav deleted file mode 100644 index b9c679d..0000000 Binary files a/SOUNDS/ALERTS/SWP43.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP44.wav b/SOUNDS/ALERTS/SWP44.wav deleted file mode 100644 index ff2db49..0000000 Binary files a/SOUNDS/ALERTS/SWP44.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP45.wav b/SOUNDS/ALERTS/SWP45.wav deleted file mode 100644 index c49335b..0000000 Binary files a/SOUNDS/ALERTS/SWP45.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP46.wav b/SOUNDS/ALERTS/SWP46.wav deleted file mode 100644 index 11a4a36..0000000 Binary files a/SOUNDS/ALERTS/SWP46.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP47.wav b/SOUNDS/ALERTS/SWP47.wav deleted file mode 100644 index 6b95d3a..0000000 Binary files a/SOUNDS/ALERTS/SWP47.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP48.wav b/SOUNDS/ALERTS/SWP48.wav deleted file mode 100644 index 62245ef..0000000 Binary files a/SOUNDS/ALERTS/SWP48.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP49.wav b/SOUNDS/ALERTS/SWP49.wav deleted file mode 100644 index 51a6009..0000000 Binary files a/SOUNDS/ALERTS/SWP49.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP50.wav b/SOUNDS/ALERTS/SWP50.wav deleted file mode 100644 index 44965d2..0000000 Binary files a/SOUNDS/ALERTS/SWP50.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP51.wav b/SOUNDS/ALERTS/SWP51.wav deleted file mode 100644 index bce7c2b..0000000 Binary files a/SOUNDS/ALERTS/SWP51.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP52.wav b/SOUNDS/ALERTS/SWP52.wav deleted file mode 100644 index 1f588a8..0000000 Binary files a/SOUNDS/ALERTS/SWP52.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP53.wav b/SOUNDS/ALERTS/SWP53.wav deleted file mode 100644 index 84182ba..0000000 Binary files a/SOUNDS/ALERTS/SWP53.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP54.wav b/SOUNDS/ALERTS/SWP54.wav deleted file mode 100644 index 793356f..0000000 Binary files a/SOUNDS/ALERTS/SWP54.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP55.wav b/SOUNDS/ALERTS/SWP55.wav deleted file mode 100644 index 6606b77..0000000 Binary files a/SOUNDS/ALERTS/SWP55.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP56.wav b/SOUNDS/ALERTS/SWP56.wav deleted file mode 100644 index e8c1987..0000000 Binary files a/SOUNDS/ALERTS/SWP56.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP57.wav b/SOUNDS/ALERTS/SWP57.wav deleted file mode 100644 index dac0cb9..0000000 Binary files a/SOUNDS/ALERTS/SWP57.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP58.wav b/SOUNDS/ALERTS/SWP58.wav deleted file mode 100644 index ca6c515..0000000 Binary files a/SOUNDS/ALERTS/SWP58.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP59.wav b/SOUNDS/ALERTS/SWP59.wav deleted file mode 100644 index feac487..0000000 Binary files a/SOUNDS/ALERTS/SWP59.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP60.wav b/SOUNDS/ALERTS/SWP60.wav deleted file mode 100644 index 77ab521..0000000 Binary files a/SOUNDS/ALERTS/SWP60.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP61.wav b/SOUNDS/ALERTS/SWP61.wav deleted file mode 100644 index 0217104..0000000 Binary files a/SOUNDS/ALERTS/SWP61.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP62.wav b/SOUNDS/ALERTS/SWP62.wav deleted file mode 100644 index 06f41c7..0000000 Binary files a/SOUNDS/ALERTS/SWP62.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP77.wav b/SOUNDS/ALERTS/SWP77.wav deleted file mode 100644 index 8d21b49..0000000 Binary files a/SOUNDS/ALERTS/SWP77.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP78.wav b/SOUNDS/ALERTS/SWP78.wav deleted file mode 100644 index b90b047..0000000 Binary files a/SOUNDS/ALERTS/SWP78.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP79.wav b/SOUNDS/ALERTS/SWP79.wav deleted file mode 100644 index 612c9c6..0000000 Binary files a/SOUNDS/ALERTS/SWP79.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP80.wav b/SOUNDS/ALERTS/SWP80.wav deleted file mode 100644 index 00c9ab0..0000000 Binary files a/SOUNDS/ALERTS/SWP80.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP81.wav b/SOUNDS/ALERTS/SWP81.wav deleted file mode 100644 index 660f528..0000000 Binary files a/SOUNDS/ALERTS/SWP81.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP82.wav b/SOUNDS/ALERTS/SWP82.wav deleted file mode 100644 index 5a2b7e5..0000000 Binary files a/SOUNDS/ALERTS/SWP82.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP83.wav b/SOUNDS/ALERTS/SWP83.wav deleted file mode 100644 index 77dd413..0000000 Binary files a/SOUNDS/ALERTS/SWP83.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP84.wav b/SOUNDS/ALERTS/SWP84.wav deleted file mode 100644 index 8c03bda..0000000 Binary files a/SOUNDS/ALERTS/SWP84.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP85.wav b/SOUNDS/ALERTS/SWP85.wav deleted file mode 100644 index e48f617..0000000 Binary files a/SOUNDS/ALERTS/SWP85.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP86.wav b/SOUNDS/ALERTS/SWP86.wav deleted file mode 100644 index cd4c113..0000000 Binary files a/SOUNDS/ALERTS/SWP86.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP87.wav b/SOUNDS/ALERTS/SWP87.wav deleted file mode 100644 index a9011e9..0000000 Binary files a/SOUNDS/ALERTS/SWP87.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP88.wav b/SOUNDS/ALERTS/SWP88.wav deleted file mode 100644 index 8003461..0000000 Binary files a/SOUNDS/ALERTS/SWP88.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP89.wav b/SOUNDS/ALERTS/SWP89.wav deleted file mode 100644 index 5337989..0000000 Binary files a/SOUNDS/ALERTS/SWP89.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP90.wav b/SOUNDS/ALERTS/SWP90.wav deleted file mode 100644 index c5a36dd..0000000 Binary files a/SOUNDS/ALERTS/SWP90.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP91.wav b/SOUNDS/ALERTS/SWP91.wav deleted file mode 100644 index 9fae41c..0000000 Binary files a/SOUNDS/ALERTS/SWP91.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP92.wav b/SOUNDS/ALERTS/SWP92.wav deleted file mode 100644 index 00a68af..0000000 Binary files a/SOUNDS/ALERTS/SWP92.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP93.wav b/SOUNDS/ALERTS/SWP93.wav deleted file mode 100644 index 30b318a..0000000 Binary files a/SOUNDS/ALERTS/SWP93.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP94.wav b/SOUNDS/ALERTS/SWP94.wav deleted file mode 100644 index 4a38ad9..0000000 Binary files a/SOUNDS/ALERTS/SWP94.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP95.wav b/SOUNDS/ALERTS/SWP95.wav deleted file mode 100644 index bbc2203..0000000 Binary files a/SOUNDS/ALERTS/SWP95.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP96.wav b/SOUNDS/ALERTS/SWP96.wav deleted file mode 100644 index 6d73142..0000000 Binary files a/SOUNDS/ALERTS/SWP96.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP97.wav b/SOUNDS/ALERTS/SWP97.wav deleted file mode 100644 index 5376658..0000000 Binary files a/SOUNDS/ALERTS/SWP97.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP98.wav b/SOUNDS/ALERTS/SWP98.wav deleted file mode 100644 index 411c3a3..0000000 Binary files a/SOUNDS/ALERTS/SWP98.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP99.wav b/SOUNDS/ALERTS/SWP99.wav deleted file mode 100644 index 2d361d8..0000000 Binary files a/SOUNDS/ALERTS/SWP99.wav and /dev/null differ diff --git a/SOUNDS/ALERTS/SWP_1.wav b/SOUNDS/ALERTS/SWP_1.wav new file mode 100644 index 0000000..34de76d Binary files /dev/null and b/SOUNDS/ALERTS/SWP_1.wav differ diff --git a/SOUNDS/ALERTS/SWP_10.wav b/SOUNDS/ALERTS/SWP_10.wav new file mode 100644 index 0000000..9637ea3 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_10.wav differ diff --git a/SOUNDS/ALERTS/SWP_100.wav b/SOUNDS/ALERTS/SWP_100.wav new file mode 100644 index 0000000..dcbbd52 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_100.wav differ diff --git a/SOUNDS/ALERTS/SWP_101.wav b/SOUNDS/ALERTS/SWP_101.wav new file mode 100644 index 0000000..bced1d7 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_101.wav differ diff --git a/SOUNDS/ALERTS/SWP_102.wav b/SOUNDS/ALERTS/SWP_102.wav new file mode 100644 index 0000000..aba6d32 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_102.wav differ diff --git a/SOUNDS/ALERTS/SWP_103.wav b/SOUNDS/ALERTS/SWP_103.wav new file mode 100644 index 0000000..4dc2302 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_103.wav differ diff --git a/SOUNDS/ALERTS/SWP_104.wav b/SOUNDS/ALERTS/SWP_104.wav new file mode 100644 index 0000000..4742316 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_104.wav differ diff --git a/SOUNDS/ALERTS/SWP_105.wav b/SOUNDS/ALERTS/SWP_105.wav new file mode 100644 index 0000000..9c1f54f Binary files /dev/null and b/SOUNDS/ALERTS/SWP_105.wav differ diff --git a/SOUNDS/ALERTS/SWP_106.wav b/SOUNDS/ALERTS/SWP_106.wav new file mode 100644 index 0000000..7ea368d Binary files /dev/null and b/SOUNDS/ALERTS/SWP_106.wav differ diff --git a/SOUNDS/ALERTS/SWP_107.wav b/SOUNDS/ALERTS/SWP_107.wav new file mode 100644 index 0000000..89328e1 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_107.wav differ diff --git a/SOUNDS/ALERTS/SWP_108.wav b/SOUNDS/ALERTS/SWP_108.wav new file mode 100644 index 0000000..5d01848 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_108.wav differ diff --git a/SOUNDS/ALERTS/SWP_109.wav b/SOUNDS/ALERTS/SWP_109.wav new file mode 100644 index 0000000..eb63375 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_109.wav differ diff --git a/SOUNDS/ALERTS/SWP_11.wav b/SOUNDS/ALERTS/SWP_11.wav new file mode 100644 index 0000000..75e8d9e Binary files /dev/null and b/SOUNDS/ALERTS/SWP_11.wav differ diff --git a/SOUNDS/ALERTS/SWP_110.wav b/SOUNDS/ALERTS/SWP_110.wav new file mode 100644 index 0000000..c847c31 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_110.wav differ diff --git a/SOUNDS/ALERTS/SWP_111.wav b/SOUNDS/ALERTS/SWP_111.wav new file mode 100644 index 0000000..4735f2f Binary files /dev/null and b/SOUNDS/ALERTS/SWP_111.wav differ diff --git a/SOUNDS/ALERTS/SWP_112.wav b/SOUNDS/ALERTS/SWP_112.wav new file mode 100644 index 0000000..93b7ca8 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_112.wav differ diff --git a/SOUNDS/ALERTS/SWP_113.wav b/SOUNDS/ALERTS/SWP_113.wav new file mode 100644 index 0000000..1f8933d Binary files /dev/null and b/SOUNDS/ALERTS/SWP_113.wav differ diff --git a/SOUNDS/ALERTS/SWP_114.wav b/SOUNDS/ALERTS/SWP_114.wav new file mode 100644 index 0000000..131046f Binary files /dev/null and b/SOUNDS/ALERTS/SWP_114.wav differ diff --git a/SOUNDS/ALERTS/SWP_115.wav b/SOUNDS/ALERTS/SWP_115.wav new file mode 100644 index 0000000..7592e40 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_115.wav differ diff --git a/SOUNDS/ALERTS/SWP_116.wav b/SOUNDS/ALERTS/SWP_116.wav new file mode 100644 index 0000000..b001f01 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_116.wav differ diff --git a/SOUNDS/ALERTS/SWP_117.wav b/SOUNDS/ALERTS/SWP_117.wav new file mode 100644 index 0000000..4ef1ef5 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_117.wav differ diff --git a/SOUNDS/ALERTS/SWP_118.wav b/SOUNDS/ALERTS/SWP_118.wav new file mode 100644 index 0000000..c0b2659 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_118.wav differ diff --git a/SOUNDS/ALERTS/SWP_119.wav b/SOUNDS/ALERTS/SWP_119.wav new file mode 100644 index 0000000..bc94563 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_119.wav differ diff --git a/SOUNDS/ALERTS/SWP_12.wav b/SOUNDS/ALERTS/SWP_12.wav new file mode 100644 index 0000000..201b0cb Binary files /dev/null and b/SOUNDS/ALERTS/SWP_12.wav differ diff --git a/SOUNDS/ALERTS/SWP_120.wav b/SOUNDS/ALERTS/SWP_120.wav new file mode 100644 index 0000000..2ccfb76 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_120.wav differ diff --git a/SOUNDS/ALERTS/SWP_121.wav b/SOUNDS/ALERTS/SWP_121.wav new file mode 100644 index 0000000..3391280 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_121.wav differ diff --git a/SOUNDS/ALERTS/SWP_122.wav b/SOUNDS/ALERTS/SWP_122.wav new file mode 100644 index 0000000..1faf867 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_122.wav differ diff --git a/SOUNDS/ALERTS/SWP_123.wav b/SOUNDS/ALERTS/SWP_123.wav new file mode 100644 index 0000000..605183a Binary files /dev/null and b/SOUNDS/ALERTS/SWP_123.wav differ diff --git a/SOUNDS/ALERTS/SWP_124.wav b/SOUNDS/ALERTS/SWP_124.wav new file mode 100644 index 0000000..4331320 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_124.wav differ diff --git a/SOUNDS/ALERTS/SWP_125.wav b/SOUNDS/ALERTS/SWP_125.wav new file mode 100644 index 0000000..7e1c7e1 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_125.wav differ diff --git a/SOUNDS/ALERTS/SWP_126.wav b/SOUNDS/ALERTS/SWP_126.wav new file mode 100644 index 0000000..9787377 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_126.wav differ diff --git a/SOUNDS/ALERTS/SWP_127.wav b/SOUNDS/ALERTS/SWP_127.wav new file mode 100644 index 0000000..9b8f034 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_127.wav differ diff --git a/SOUNDS/ALERTS/SWP_128.wav b/SOUNDS/ALERTS/SWP_128.wav new file mode 100644 index 0000000..586148d Binary files /dev/null and b/SOUNDS/ALERTS/SWP_128.wav differ diff --git a/SOUNDS/ALERTS/SWP_129.wav b/SOUNDS/ALERTS/SWP_129.wav new file mode 100644 index 0000000..ba96162 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_129.wav differ diff --git a/SOUNDS/ALERTS/SWP_13.wav b/SOUNDS/ALERTS/SWP_13.wav new file mode 100644 index 0000000..e4c3387 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_13.wav differ diff --git a/SOUNDS/ALERTS/SWP_130.wav b/SOUNDS/ALERTS/SWP_130.wav new file mode 100644 index 0000000..950771a Binary files /dev/null and b/SOUNDS/ALERTS/SWP_130.wav differ diff --git a/SOUNDS/ALERTS/SWP_131.wav b/SOUNDS/ALERTS/SWP_131.wav new file mode 100644 index 0000000..4c20e7f Binary files /dev/null and b/SOUNDS/ALERTS/SWP_131.wav differ diff --git a/SOUNDS/ALERTS/SWP_132.wav b/SOUNDS/ALERTS/SWP_132.wav new file mode 100644 index 0000000..7cf7206 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_132.wav differ diff --git a/SOUNDS/ALERTS/SWP_133.wav b/SOUNDS/ALERTS/SWP_133.wav new file mode 100644 index 0000000..8306b3b Binary files /dev/null and b/SOUNDS/ALERTS/SWP_133.wav differ diff --git a/SOUNDS/ALERTS/SWP_134.wav b/SOUNDS/ALERTS/SWP_134.wav new file mode 100644 index 0000000..8dba013 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_134.wav differ diff --git a/SOUNDS/ALERTS/SWP_135.wav b/SOUNDS/ALERTS/SWP_135.wav new file mode 100644 index 0000000..a2b3318 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_135.wav differ diff --git a/SOUNDS/ALERTS/SWP_136.wav b/SOUNDS/ALERTS/SWP_136.wav new file mode 100644 index 0000000..a0d4be2 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_136.wav differ diff --git a/SOUNDS/ALERTS/SWP_137.wav b/SOUNDS/ALERTS/SWP_137.wav new file mode 100644 index 0000000..bbfaa22 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_137.wav differ diff --git a/SOUNDS/ALERTS/SWP_138.wav b/SOUNDS/ALERTS/SWP_138.wav new file mode 100644 index 0000000..fbcd6c6 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_138.wav differ diff --git a/SOUNDS/ALERTS/SWP_139.wav b/SOUNDS/ALERTS/SWP_139.wav new file mode 100644 index 0000000..d0ffad8 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_139.wav differ diff --git a/SOUNDS/ALERTS/SWP_14.wav b/SOUNDS/ALERTS/SWP_14.wav new file mode 100644 index 0000000..de08ac7 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_14.wav differ diff --git a/SOUNDS/ALERTS/SWP_140.wav b/SOUNDS/ALERTS/SWP_140.wav new file mode 100644 index 0000000..166e1c2 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_140.wav differ diff --git a/SOUNDS/ALERTS/SWP_141.wav b/SOUNDS/ALERTS/SWP_141.wav new file mode 100644 index 0000000..51a3942 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_141.wav differ diff --git a/SOUNDS/ALERTS/SWP_142.wav b/SOUNDS/ALERTS/SWP_142.wav new file mode 100644 index 0000000..7697085 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_142.wav differ diff --git a/SOUNDS/ALERTS/SWP_143.wav b/SOUNDS/ALERTS/SWP_143.wav new file mode 100644 index 0000000..4929ba0 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_143.wav differ diff --git a/SOUNDS/ALERTS/SWP_144.wav b/SOUNDS/ALERTS/SWP_144.wav new file mode 100644 index 0000000..32ab793 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_144.wav differ diff --git a/SOUNDS/ALERTS/SWP_145.wav b/SOUNDS/ALERTS/SWP_145.wav new file mode 100644 index 0000000..84bd83d Binary files /dev/null and b/SOUNDS/ALERTS/SWP_145.wav differ diff --git a/SOUNDS/ALERTS/SWP_146.wav b/SOUNDS/ALERTS/SWP_146.wav new file mode 100644 index 0000000..f6376f2 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_146.wav differ diff --git a/SOUNDS/ALERTS/SWP_147.wav b/SOUNDS/ALERTS/SWP_147.wav new file mode 100644 index 0000000..94dbf2e Binary files /dev/null and b/SOUNDS/ALERTS/SWP_147.wav differ diff --git a/SOUNDS/ALERTS/SWP_148.wav b/SOUNDS/ALERTS/SWP_148.wav new file mode 100644 index 0000000..f5999d6 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_148.wav differ diff --git a/SOUNDS/ALERTS/SWP_149.wav b/SOUNDS/ALERTS/SWP_149.wav new file mode 100644 index 0000000..2aa719c Binary files /dev/null and b/SOUNDS/ALERTS/SWP_149.wav differ diff --git a/SOUNDS/ALERTS/SWP_15.wav b/SOUNDS/ALERTS/SWP_15.wav new file mode 100644 index 0000000..76abe12 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_15.wav differ diff --git a/SOUNDS/ALERTS/SWP_16.wav b/SOUNDS/ALERTS/SWP_16.wav new file mode 100644 index 0000000..dd2767f Binary files /dev/null and b/SOUNDS/ALERTS/SWP_16.wav differ diff --git a/SOUNDS/ALERTS/SWP_17.wav b/SOUNDS/ALERTS/SWP_17.wav new file mode 100644 index 0000000..2bf4cd9 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_17.wav differ diff --git a/SOUNDS/ALERTS/SWP_18.wav b/SOUNDS/ALERTS/SWP_18.wav new file mode 100644 index 0000000..72ac109 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_18.wav differ diff --git a/SOUNDS/ALERTS/SWP_19.wav b/SOUNDS/ALERTS/SWP_19.wav new file mode 100644 index 0000000..fd4c1cd Binary files /dev/null and b/SOUNDS/ALERTS/SWP_19.wav differ diff --git a/SOUNDS/ALERTS/SWP_2.wav b/SOUNDS/ALERTS/SWP_2.wav new file mode 100644 index 0000000..d51ad7b Binary files /dev/null and b/SOUNDS/ALERTS/SWP_2.wav differ diff --git a/SOUNDS/ALERTS/SWP_20.wav b/SOUNDS/ALERTS/SWP_20.wav new file mode 100644 index 0000000..6ec9694 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_20.wav differ diff --git a/SOUNDS/ALERTS/SWP_21.wav b/SOUNDS/ALERTS/SWP_21.wav new file mode 100644 index 0000000..1c44d6a Binary files /dev/null and b/SOUNDS/ALERTS/SWP_21.wav differ diff --git a/SOUNDS/ALERTS/SWP_22.wav b/SOUNDS/ALERTS/SWP_22.wav new file mode 100644 index 0000000..5a83d0d Binary files /dev/null and b/SOUNDS/ALERTS/SWP_22.wav differ diff --git a/SOUNDS/ALERTS/SWP_23.wav b/SOUNDS/ALERTS/SWP_23.wav new file mode 100644 index 0000000..0c32634 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_23.wav differ diff --git a/SOUNDS/ALERTS/SWP_24.wav b/SOUNDS/ALERTS/SWP_24.wav new file mode 100644 index 0000000..721f9c7 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_24.wav differ diff --git a/SOUNDS/ALERTS/SWP_25.wav b/SOUNDS/ALERTS/SWP_25.wav new file mode 100644 index 0000000..3080b38 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_25.wav differ diff --git a/SOUNDS/ALERTS/SWP_26.wav b/SOUNDS/ALERTS/SWP_26.wav new file mode 100644 index 0000000..4dad96a Binary files /dev/null and b/SOUNDS/ALERTS/SWP_26.wav differ diff --git a/SOUNDS/ALERTS/SWP_27.wav b/SOUNDS/ALERTS/SWP_27.wav new file mode 100644 index 0000000..290c2d0 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_27.wav differ diff --git a/SOUNDS/ALERTS/SWP_28.wav b/SOUNDS/ALERTS/SWP_28.wav new file mode 100644 index 0000000..e146f81 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_28.wav differ diff --git a/SOUNDS/ALERTS/SWP_29.wav b/SOUNDS/ALERTS/SWP_29.wav new file mode 100644 index 0000000..cd609c6 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_29.wav differ diff --git a/SOUNDS/ALERTS/SWP_3.wav b/SOUNDS/ALERTS/SWP_3.wav new file mode 100644 index 0000000..0623f97 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_3.wav differ diff --git a/SOUNDS/ALERTS/SWP_30.wav b/SOUNDS/ALERTS/SWP_30.wav new file mode 100644 index 0000000..e55b443 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_30.wav differ diff --git a/SOUNDS/ALERTS/SWP_31.wav b/SOUNDS/ALERTS/SWP_31.wav new file mode 100644 index 0000000..b3fbd99 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_31.wav differ diff --git a/SOUNDS/ALERTS/SWP_32.wav b/SOUNDS/ALERTS/SWP_32.wav new file mode 100644 index 0000000..9e0d837 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_32.wav differ diff --git a/SOUNDS/ALERTS/SWP_33.wav b/SOUNDS/ALERTS/SWP_33.wav new file mode 100644 index 0000000..f8d312f Binary files /dev/null and b/SOUNDS/ALERTS/SWP_33.wav differ diff --git a/SOUNDS/ALERTS/SWP_34.wav b/SOUNDS/ALERTS/SWP_34.wav new file mode 100644 index 0000000..29c42c0 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_34.wav differ diff --git a/SOUNDS/ALERTS/SWP_35.wav b/SOUNDS/ALERTS/SWP_35.wav new file mode 100644 index 0000000..dadf973 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_35.wav differ diff --git a/SOUNDS/ALERTS/SWP_36.wav b/SOUNDS/ALERTS/SWP_36.wav new file mode 100644 index 0000000..adb6d72 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_36.wav differ diff --git a/SOUNDS/ALERTS/SWP_37.wav b/SOUNDS/ALERTS/SWP_37.wav new file mode 100644 index 0000000..4e70303 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_37.wav differ diff --git a/SOUNDS/ALERTS/SWP_38.wav b/SOUNDS/ALERTS/SWP_38.wav new file mode 100644 index 0000000..5e2b7ba Binary files /dev/null and b/SOUNDS/ALERTS/SWP_38.wav differ diff --git a/SOUNDS/ALERTS/SWP_39.wav b/SOUNDS/ALERTS/SWP_39.wav new file mode 100644 index 0000000..f83ddf0 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_39.wav differ diff --git a/SOUNDS/ALERTS/SWP_4.wav b/SOUNDS/ALERTS/SWP_4.wav new file mode 100644 index 0000000..742c63d Binary files /dev/null and b/SOUNDS/ALERTS/SWP_4.wav differ diff --git a/SOUNDS/ALERTS/SWP_40.wav b/SOUNDS/ALERTS/SWP_40.wav new file mode 100644 index 0000000..8b8f86e Binary files /dev/null and b/SOUNDS/ALERTS/SWP_40.wav differ diff --git a/SOUNDS/ALERTS/SWP_41.wav b/SOUNDS/ALERTS/SWP_41.wav new file mode 100644 index 0000000..5572c72 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_41.wav differ diff --git a/SOUNDS/ALERTS/SWP_42.wav b/SOUNDS/ALERTS/SWP_42.wav new file mode 100644 index 0000000..c0c5cd4 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_42.wav differ diff --git a/SOUNDS/ALERTS/SWP_43.wav b/SOUNDS/ALERTS/SWP_43.wav new file mode 100644 index 0000000..f4b8390 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_43.wav differ diff --git a/SOUNDS/ALERTS/SWP_44.wav b/SOUNDS/ALERTS/SWP_44.wav new file mode 100644 index 0000000..8cbc891 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_44.wav differ diff --git a/SOUNDS/ALERTS/SWP_45.wav b/SOUNDS/ALERTS/SWP_45.wav new file mode 100644 index 0000000..54a1c35 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_45.wav differ diff --git a/SOUNDS/ALERTS/SWP_46.wav b/SOUNDS/ALERTS/SWP_46.wav new file mode 100644 index 0000000..582330c Binary files /dev/null and b/SOUNDS/ALERTS/SWP_46.wav differ diff --git a/SOUNDS/ALERTS/SWP_47.wav b/SOUNDS/ALERTS/SWP_47.wav new file mode 100644 index 0000000..999602e Binary files /dev/null and b/SOUNDS/ALERTS/SWP_47.wav differ diff --git a/SOUNDS/ALERTS/SWP_48.wav b/SOUNDS/ALERTS/SWP_48.wav new file mode 100644 index 0000000..32b3d78 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_48.wav differ diff --git a/SOUNDS/ALERTS/SWP_49.wav b/SOUNDS/ALERTS/SWP_49.wav new file mode 100644 index 0000000..872e460 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_49.wav differ diff --git a/SOUNDS/ALERTS/SWP_5.wav b/SOUNDS/ALERTS/SWP_5.wav new file mode 100644 index 0000000..486a732 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_5.wav differ diff --git a/SOUNDS/ALERTS/SWP_50.wav b/SOUNDS/ALERTS/SWP_50.wav new file mode 100644 index 0000000..0ee16a1 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_50.wav differ diff --git a/SOUNDS/ALERTS/SWP_51.wav b/SOUNDS/ALERTS/SWP_51.wav new file mode 100644 index 0000000..3bd9880 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_51.wav differ diff --git a/SOUNDS/ALERTS/SWP_52.wav b/SOUNDS/ALERTS/SWP_52.wav new file mode 100644 index 0000000..6265a94 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_52.wav differ diff --git a/SOUNDS/ALERTS/SWP_53.wav b/SOUNDS/ALERTS/SWP_53.wav new file mode 100644 index 0000000..a34baec Binary files /dev/null and b/SOUNDS/ALERTS/SWP_53.wav differ diff --git a/SOUNDS/ALERTS/SWP_54.wav b/SOUNDS/ALERTS/SWP_54.wav new file mode 100644 index 0000000..56f431e Binary files /dev/null and b/SOUNDS/ALERTS/SWP_54.wav differ diff --git a/SOUNDS/ALERTS/SWP_55.wav b/SOUNDS/ALERTS/SWP_55.wav new file mode 100644 index 0000000..17270a6 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_55.wav differ diff --git a/SOUNDS/ALERTS/SWP_56.wav b/SOUNDS/ALERTS/SWP_56.wav new file mode 100644 index 0000000..1950c6c Binary files /dev/null and b/SOUNDS/ALERTS/SWP_56.wav differ diff --git a/SOUNDS/ALERTS/SWP_57.wav b/SOUNDS/ALERTS/SWP_57.wav new file mode 100644 index 0000000..29e037f Binary files /dev/null and b/SOUNDS/ALERTS/SWP_57.wav differ diff --git a/SOUNDS/ALERTS/SWP_58.wav b/SOUNDS/ALERTS/SWP_58.wav new file mode 100644 index 0000000..a93f95e Binary files /dev/null and b/SOUNDS/ALERTS/SWP_58.wav differ diff --git a/SOUNDS/ALERTS/SWP_59.wav b/SOUNDS/ALERTS/SWP_59.wav new file mode 100644 index 0000000..424f024 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_59.wav differ diff --git a/SOUNDS/ALERTS/SWP_6.wav b/SOUNDS/ALERTS/SWP_6.wav new file mode 100644 index 0000000..f9da3d6 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_6.wav differ diff --git a/SOUNDS/ALERTS/SWP_60.wav b/SOUNDS/ALERTS/SWP_60.wav new file mode 100644 index 0000000..4b586b7 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_60.wav differ diff --git a/SOUNDS/ALERTS/SWP_61.wav b/SOUNDS/ALERTS/SWP_61.wav new file mode 100644 index 0000000..2b69735 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_61.wav differ diff --git a/SOUNDS/ALERTS/SWP_62.wav b/SOUNDS/ALERTS/SWP_62.wav new file mode 100644 index 0000000..d401ffc Binary files /dev/null and b/SOUNDS/ALERTS/SWP_62.wav differ diff --git a/SOUNDS/ALERTS/SWP_63.wav b/SOUNDS/ALERTS/SWP_63.wav new file mode 100644 index 0000000..2d84f3a Binary files /dev/null and b/SOUNDS/ALERTS/SWP_63.wav differ diff --git a/SOUNDS/ALERTS/SWP_64.wav b/SOUNDS/ALERTS/SWP_64.wav new file mode 100644 index 0000000..227aa35 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_64.wav differ diff --git a/SOUNDS/ALERTS/SWP_65.wav b/SOUNDS/ALERTS/SWP_65.wav new file mode 100644 index 0000000..34110ff Binary files /dev/null and b/SOUNDS/ALERTS/SWP_65.wav differ diff --git a/SOUNDS/ALERTS/SWP_66.wav b/SOUNDS/ALERTS/SWP_66.wav new file mode 100644 index 0000000..1716470 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_66.wav differ diff --git a/SOUNDS/ALERTS/SWP_67.wav b/SOUNDS/ALERTS/SWP_67.wav new file mode 100644 index 0000000..86abc3b Binary files /dev/null and b/SOUNDS/ALERTS/SWP_67.wav differ diff --git a/SOUNDS/ALERTS/SWP_68.wav b/SOUNDS/ALERTS/SWP_68.wav new file mode 100644 index 0000000..2381e78 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_68.wav differ diff --git a/SOUNDS/ALERTS/SWP_69.wav b/SOUNDS/ALERTS/SWP_69.wav new file mode 100644 index 0000000..059f5c5 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_69.wav differ diff --git a/SOUNDS/ALERTS/SWP_7.wav b/SOUNDS/ALERTS/SWP_7.wav new file mode 100644 index 0000000..811523b Binary files /dev/null and b/SOUNDS/ALERTS/SWP_7.wav differ diff --git a/SOUNDS/ALERTS/SWP_70.wav b/SOUNDS/ALERTS/SWP_70.wav new file mode 100644 index 0000000..15bb9a3 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_70.wav differ diff --git a/SOUNDS/ALERTS/SWP_71.wav b/SOUNDS/ALERTS/SWP_71.wav new file mode 100644 index 0000000..0c298b1 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_71.wav differ diff --git a/SOUNDS/ALERTS/SWP_72.wav b/SOUNDS/ALERTS/SWP_72.wav new file mode 100644 index 0000000..d21b5a4 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_72.wav differ diff --git a/SOUNDS/ALERTS/SWP_73.wav b/SOUNDS/ALERTS/SWP_73.wav new file mode 100644 index 0000000..006420d Binary files /dev/null and b/SOUNDS/ALERTS/SWP_73.wav differ diff --git a/SOUNDS/ALERTS/SWP_74.wav b/SOUNDS/ALERTS/SWP_74.wav new file mode 100644 index 0000000..978df87 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_74.wav differ diff --git a/SOUNDS/ALERTS/SWP_75.wav b/SOUNDS/ALERTS/SWP_75.wav new file mode 100644 index 0000000..9faacae Binary files /dev/null and b/SOUNDS/ALERTS/SWP_75.wav differ diff --git a/SOUNDS/ALERTS/SWP_76.wav b/SOUNDS/ALERTS/SWP_76.wav new file mode 100644 index 0000000..54f0f12 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_76.wav differ diff --git a/SOUNDS/ALERTS/SWP_77.wav b/SOUNDS/ALERTS/SWP_77.wav new file mode 100644 index 0000000..0c5ad83 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_77.wav differ diff --git a/SOUNDS/ALERTS/SWP_78.wav b/SOUNDS/ALERTS/SWP_78.wav new file mode 100644 index 0000000..82a2620 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_78.wav differ diff --git a/SOUNDS/ALERTS/SWP_79.wav b/SOUNDS/ALERTS/SWP_79.wav new file mode 100644 index 0000000..e0dd018 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_79.wav differ diff --git a/SOUNDS/ALERTS/SWP_8.wav b/SOUNDS/ALERTS/SWP_8.wav new file mode 100644 index 0000000..1ccc7d2 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_8.wav differ diff --git a/SOUNDS/ALERTS/SWP_80.wav b/SOUNDS/ALERTS/SWP_80.wav new file mode 100644 index 0000000..3ce1a67 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_80.wav differ diff --git a/SOUNDS/ALERTS/SWP_81.wav b/SOUNDS/ALERTS/SWP_81.wav new file mode 100644 index 0000000..f2b33a3 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_81.wav differ diff --git a/SOUNDS/ALERTS/SWP_82.wav b/SOUNDS/ALERTS/SWP_82.wav new file mode 100644 index 0000000..c2bf060 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_82.wav differ diff --git a/SOUNDS/ALERTS/SWP_83.wav b/SOUNDS/ALERTS/SWP_83.wav new file mode 100644 index 0000000..c5d4a3e Binary files /dev/null and b/SOUNDS/ALERTS/SWP_83.wav differ diff --git a/SOUNDS/ALERTS/SWP_84.wav b/SOUNDS/ALERTS/SWP_84.wav new file mode 100644 index 0000000..d62f8cd Binary files /dev/null and b/SOUNDS/ALERTS/SWP_84.wav differ diff --git a/SOUNDS/ALERTS/SWP_85.wav b/SOUNDS/ALERTS/SWP_85.wav new file mode 100644 index 0000000..4fd64f7 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_85.wav differ diff --git a/SOUNDS/ALERTS/SWP_86.wav b/SOUNDS/ALERTS/SWP_86.wav new file mode 100644 index 0000000..6a6663a Binary files /dev/null and b/SOUNDS/ALERTS/SWP_86.wav differ diff --git a/SOUNDS/ALERTS/SWP_87.wav b/SOUNDS/ALERTS/SWP_87.wav new file mode 100644 index 0000000..b733502 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_87.wav differ diff --git a/SOUNDS/ALERTS/SWP_88.wav b/SOUNDS/ALERTS/SWP_88.wav new file mode 100644 index 0000000..a053a46 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_88.wav differ diff --git a/SOUNDS/ALERTS/SWP_89.wav b/SOUNDS/ALERTS/SWP_89.wav new file mode 100644 index 0000000..4855f70 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_89.wav differ diff --git a/SOUNDS/ALERTS/SWP_9.wav b/SOUNDS/ALERTS/SWP_9.wav new file mode 100644 index 0000000..f079089 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_9.wav differ diff --git a/SOUNDS/ALERTS/SWP_90.wav b/SOUNDS/ALERTS/SWP_90.wav new file mode 100644 index 0000000..c409256 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_90.wav differ diff --git a/SOUNDS/ALERTS/SWP_91.wav b/SOUNDS/ALERTS/SWP_91.wav new file mode 100644 index 0000000..1bae5a6 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_91.wav differ diff --git a/SOUNDS/ALERTS/SWP_92.wav b/SOUNDS/ALERTS/SWP_92.wav new file mode 100644 index 0000000..b99ffab Binary files /dev/null and b/SOUNDS/ALERTS/SWP_92.wav differ diff --git a/SOUNDS/ALERTS/SWP_93.wav b/SOUNDS/ALERTS/SWP_93.wav new file mode 100644 index 0000000..fdbfd96 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_93.wav differ diff --git a/SOUNDS/ALERTS/SWP_94.wav b/SOUNDS/ALERTS/SWP_94.wav new file mode 100644 index 0000000..2d8116c Binary files /dev/null and b/SOUNDS/ALERTS/SWP_94.wav differ diff --git a/SOUNDS/ALERTS/SWP_95.wav b/SOUNDS/ALERTS/SWP_95.wav new file mode 100644 index 0000000..e557fc3 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_95.wav differ diff --git a/SOUNDS/ALERTS/SWP_96.wav b/SOUNDS/ALERTS/SWP_96.wav new file mode 100644 index 0000000..ec4970d Binary files /dev/null and b/SOUNDS/ALERTS/SWP_96.wav differ diff --git a/SOUNDS/ALERTS/SWP_97.wav b/SOUNDS/ALERTS/SWP_97.wav new file mode 100644 index 0000000..a18a737 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_97.wav differ diff --git a/SOUNDS/ALERTS/SWP_98.wav b/SOUNDS/ALERTS/SWP_98.wav new file mode 100644 index 0000000..d65fdd9 Binary files /dev/null and b/SOUNDS/ALERTS/SWP_98.wav differ diff --git a/SOUNDS/ALERTS/SWP_99.wav b/SOUNDS/ALERTS/SWP_99.wav new file mode 100644 index 0000000..d7fb41c Binary files /dev/null and b/SOUNDS/ALERTS/SWP_99.wav differ diff --git a/SOUNDS/TONES/BEEP.ulaw b/SOUNDS/TONES/Beep.ulaw similarity index 100% rename from SOUNDS/TONES/BEEP.ulaw rename to SOUNDS/TONES/Beep.ulaw diff --git a/SOUNDS/TONES/BOOP.ulaw b/SOUNDS/TONES/Boop.ulaw similarity index 100% rename from SOUNDS/TONES/BOOP.ulaw rename to SOUNDS/TONES/Boop.ulaw diff --git a/SkyControl.py b/SkyControl.py index 5b21cb8..8e50dfc 100644 --- a/SkyControl.py +++ b/SkyControl.py @@ -1,6 +1,6 @@ #!/usr/bin/python3 # SkyControl.py -# A Control Script for SkywarnPlus v0.2.2 +# A Control Script for SkywarnPlus v0.2.3 # by Mason Nelson (N5LSN/WRKF394) # # This script allows you to change the value of specific keys in the SkywarnPlus config.yaml file. @@ -14,9 +14,12 @@ import os import shutil import sys -import yaml import subprocess from pathlib import Path +from ruamel.yaml import YAML + +# Use ruamel.yaml instead of PyYAML to preserve comments in the config file +yaml = YAML() # Define a function to change the CT @@ -55,7 +58,7 @@ def changeCT(ct): # Define a function to change the ID def changeID(id): - id_dir = config["IDChange"].get("IDDir", os.path.join(SCRIPT_DIR, "ID")) + id_dir = config["IDChange"].get("IDDir", os.path.join(str(SCRIPT_DIR), "ID")) normal_id = config["IDChange"]["IDs"]["NormalID"] wx_id = config["IDChange"]["IDs"]["WXID"] rpt_id = config["IDChange"]["IDs"]["RptID"] @@ -80,57 +83,57 @@ VALID_KEYS = { "enable": { "key": "Enable", "section": "SKYWARNPLUS", - "true_file": "SWP85.wav", - "false_file": "SWP86.wav", + "true_file": "SWP_137.wav", + "false_file": "SWP_138.wav", }, "sayalert": { "key": "SayAlert", "section": "Alerting", - "true_file": "SWP87.wav", - "false_file": "SWP88.wav", + "true_file": "SWP_139.wav", + "false_file": "SWP_140.wav", }, "sayallclear": { "key": "SayAllClear", "section": "Alerting", - "true_file": "SWP89.wav", - "false_file": "SWP90.wav", + "true_file": "SWP_141.wav", + "false_file": "SWP_142.wav", }, "tailmessage": { "key": "Enable", "section": "Tailmessage", - "true_file": "SWP91.wav", - "false_file": "SWP92.wav", + "true_file": "SWP_143.wav", + "false_file": "SWP_144.wav", }, "courtesytone": { "key": "Enable", "section": "CourtesyTones", - "true_file": "SWP93.wav", - "false_file": "SWP94.wav", + "true_file": "SWP_145.wav", + "false_file": "SWP_146.wav", }, "idchange": { "key": "Enable", "section": "IDChange", - "true_file": "SWP83.wav", - "false_file": "SWP84.wav", + "true_file": "SWP_135.wav", + "false_file": "SWP_136.wav", }, "alertscript": { "key": "Enable", "section": "AlertScript", - "true_file": "SWP81.wav", - "false_file": "SWP82.wav", + "true_file": "SWP_133.wav", + "false_file": "SWP_134.wav", }, "changect": { "key": "", "section": "", - "true_file": "SWP79.wav", - "false_file": "SWP80.wav", + "true_file": "SWP_131.wav", + "false_file": "SWP_132.wav", "available_values": ["wx", "normal"], }, "changeid": { "key": "", "section": "", - "true_file": "SWP77.wav", - "false_file": "SWP78.wav", + "true_file": "SWP_129.wav", + "false_file": "SWP_130.wav", "available_values": ["WX", "NORMAL"], }, } @@ -150,6 +153,10 @@ if len(sys.argv) != 3: # The input key and value key, value = sys.argv[1:3] +# Convert to lower case +key = key.lower() +value = value.lower() + # Make sure the provided key is valid if key not in VALID_KEYS: print("The provided key does not match any configurable item.") @@ -173,7 +180,7 @@ else: # Load the config file with open(str(CONFIG_FILE), "r") as f: - config = yaml.safe_load(f) + config = yaml.load(f) if key == "changect": value = changeCT(value) diff --git a/SkyDescribe.py b/SkyDescribe.py new file mode 100644 index 0000000..1841e8d --- /dev/null +++ b/SkyDescribe.py @@ -0,0 +1,336 @@ +#!/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 +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() + +# Directories and Paths +baseDir = os.path.dirname(os.path.realpath(__file__)) +configPath = os.path.join(baseDir, "config.yaml") + +# Open and read configuration file +with open(configPath, "r") as config_file: + config = yaml.load(config_file) + +# Define tmp_dir +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") + +# Define logger +logger = logging.getLogger(__name__) +if config.get("Logging", []).get("Debug", False): + logger.setLevel(logging.DEBUG) +else: + logger.setLevel(logging.INFO) + +# 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: + 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": OrderedDict(), + "last_sayalert": [], + } + + +def modify_description(description): + """ + 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. + """ + # Remove newline characters and replace multiple spaces with a single space + description = description.replace("\n", " ") + description = re.sub(r"\s+", " ", description) + + # Replace some common weather abbreviations and symbols + abbreviations = { + 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 = re.sub(abbr, full, description) + + # Remove '*' characters + description = description.replace("*", "") + + # Replace ' ' with a single space + description = re.sub(r"\s\s+", " ", description) + + # Replace '. . . ' with a single space. The \s* takes care of any number of spaces. + description = re.sub(r"\.\s*\.\s*\.\s*", " ", 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 + + +def convert_to_audio(api_key, text): + """ + Convert the given text to audio using the Voice RSS Text-to-Speech API. + + Args: + api_key (str): The API key. + text (str): The text to convert. + + Returns: + str: The path to the audio file. + """ + base_url = "http://api.voicerss.org/" + params = { + "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, "describe.wav") + with open(audio_file_path, "wb") as file: + file.write(response.content) + return audio_file_path + + +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 = list(state["last_alerts"].items()) # Now alerts is a list of tuples + + # Determine if the argument is an index or a title + try: + index = int(index_or_title) - 1 + if index >= len(alerts): + logger.error("SkyDescribe: No alert found at index %d.", index + 1) + description = "Sky Describe error, no alert found at index {}.".format( + index + 1 + ) + else: + 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) + description = "Sky Describe error, no alert found with title {}.".format( + title + ) + + logger.debug("\n\nSkyDescribe: Original description: %s", description) + + # If the description is not an error message, extract the alert title + if not "Sky Describe error" in description: + alert_title = alert[0] # Extract only the title from the alert tuple + logger.info("SkyDescribe: Generating description for alert: %s", alert_title) + # Add the alert title at the beginning + description = ( + "Detailed alert information for {}. ".format(alert_title) + description + ) + description = modify_description(description) + + logger.debug("\n\nSkyDescribe: Modified description: %s\n\n", description) + + audio_file = convert_to_audio(api_key, description) + + with contextlib.closing(wave.open(audio_file, "r")) as f: + frames = f.getnframes() + rate = f.getframerate() + duration = frames / float(rate) + logger.debug("SkyDescribe: Length of the audio file in seconds: %s", duration) + + nodes = config["Asterisk"]["Nodes"] + for node in nodes: + logger.info("SkyDescribe: Broadcasting description on node %s.", node) + command = "/usr/sbin/asterisk -rx 'rpt localplay {} {}'".format( + node, audio_file.rsplit(".", 1)[0] + ) + logger.debug("SkyDescribe: Running command: %s", command) + subprocess.run(command, shell=True) + + +# Script entry point +if __name__ == "__main__": + if len(sys.argv) != 2: + logger.error("Usage: SkyDescribe.py ") + sys.exit(1) + main(sys.argv[1]) diff --git a/SkywarnPlus.py b/SkywarnPlus.py index b689eb4..d33267a 100644 --- a/SkywarnPlus.py +++ b/SkywarnPlus.py @@ -1,7 +1,7 @@ #!/usr/bin/python3 """ -SkywarnPlus v0.2.2 by Mason Nelson (N5LSN/WRKF394) +SkywarnPlus v0.2.3 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 @@ -28,10 +28,17 @@ import shutil import fnmatch import subprocess import time -import yaml +import wave +import contextlib +import math from datetime import datetime, timezone from dateutil import parser from pydub import AudioSegment +from ruamel.yaml import YAML +from collections import OrderedDict + +# Use ruamel.yaml instead of PyYAML +yaml = YAML() # Directories and Paths baseDir = os.path.dirname(os.path.realpath(__file__)) @@ -39,7 +46,8 @@ configPath = os.path.join(baseDir, "config.yaml") # Open and read configuration file with open(configPath, "r") as config_file: - config = yaml.safe_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 master_enable = config.get("SKYWARNPLUS", {}).get("Enable", False) @@ -82,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 @@ -92,136 +100,141 @@ enable_idchange = idchange_config.get("Enable", False) # Data file path data_file = os.path.join(tmp_dir, "data.json") -# Define Warning and Announcement strings +# Define possible alert strings WS = [ - "Hurricane Force Wind Warning", - "Severe Thunderstorm Warning", - "Severe Thunderstorm Watch", - "Winter Weather Advisory", - "Tropical Storm Warning", - "Special Marine Warning", - "Freezing Rain Advisory", - "Special Weather Statement", - "Excessive Heat Warning", + "911 Telephone Outage Emergency", + "Administrative Message", + "Air Quality Alert", + "Air Stagnation Advisory", + "Arroyo And Small Stream Flood Advisory", + "Ashfall Advisory", + "Ashfall Warning", + "Avalanche Advisory", + "Avalanche Warning", + "Avalanche Watch", + "Beach Hazards Statement", + "Blizzard Warning", + "Blizzard Watch", + "Blowing Dust Advisory", + "Blowing Dust Warning", + "Brisk Wind Advisory", + "Child Abduction Emergency", + "Civil Danger Warning", + "Civil Emergency Message", "Coastal Flood Advisory", + "Coastal Flood Statement", "Coastal Flood Warning", - "Winter Storm Warning", - "Tropical Storm Watch", - "Thunderstorm Warning", - "Small Craft Advisory", - "Extreme Wind Warning", - "Excessive Heat Watch", - "Wind Chill Advisory", - "Storm Surge Warning", - "River Flood Warning", - "Flash Flood Warning", "Coastal Flood Watch", - "Winter Storm Watch", - "Wind Chill Warning", - "Thunderstorm Watch", - "Fire Weather Watch", "Dense Fog Advisory", - "Storm Surge Watch", - "River Flood Watch", - "Ice Storm Warning", - "Hurricane Warning", - "High Wind Warning", + "Dense Smoke Advisory", + "Dust Advisory", + "Dust Storm Warning", + "Earthquake Warning", + "Evacuation - Immediate", + "Excessive Heat Warning", + "Excessive Heat Watch", + "Extreme Cold Warning", + "Extreme Cold Watch", + "Extreme Fire Danger", + "Extreme Wind Warning", + "Fire Warning", + "Fire Weather Watch", + "Flash Flood Statement", + "Flash Flood Warning", "Flash Flood Watch", - "Red Flag Warning", - "Blizzard Warning", - "Tornado Warning", - "Hurricane Watch", - "High Wind Watch", - "Frost Advisory", - "Freeze Warning", - "Wind Advisory", - "Tornado Watch", - "Storm Warning", - "Heat Advisory", + "Flood Advisory", + "Flood Statement", "Flood Warning", - "Gale Warning", - "Freeze Watch", "Flood Watch", - "Flood Advisory", + "Freeze Warning", + "Freeze Watch", + "Freezing Fog Advisory", + "Freezing Rain Advisory", + "Freezing Spray Advisory", + "Frost Advisory", + "Gale Warning", + "Gale Watch", + "Hard Freeze Warning", + "Hard Freeze Watch", + "Hazardous Materials Warning", + "Hazardous Seas Warning", + "Hazardous Seas Watch", + "Hazardous Weather Outlook", + "Heat Advisory", + "Heavy Freezing Spray Warning", + "Heavy Freezing Spray Watch", + "High Surf Advisory", + "High Surf Warning", + "High Wind Warning", + "High Wind Watch", + "Hurricane Force Wind Warning", + "Hurricane Force Wind Watch", "Hurricane Local Statement", - "Beach Hazards Statement", - "Air Quality Alert", + "Hurricane Warning", + "Hurricane Watch", + "Hydrologic Advisory", + "Hydrologic Outlook", + "Ice Storm Warning", + "Lake Effect Snow Advisory", + "Lake Effect Snow Warning", + "Lake Effect Snow Watch", + "Lake Wind Advisory", + "Lakeshore Flood Advisory", + "Lakeshore Flood Statement", + "Lakeshore Flood Warning", + "Lakeshore Flood Watch", + "Law Enforcement Warning", + "Local Area Emergency", + "Low Water Advisory", + "Marine Weather Statement", + "Nuclear Power Plant Warning", + "Radiological Hazard Warning", + "Red Flag Warning", + "Rip Current Statement", + "Severe Thunderstorm Warning", + "Severe Thunderstorm Watch", "Severe Weather Statement", - "Winter Storm Advisory", - "Tropical Storm Advisory", - "Blizzard Watch", - "Dust Storm Warning", - "High Surf Advisory", - "Heat Watch", - "Freeze Watch", - "Dense Smoke Advisory", - "Avalanche Warning", -] -WA = [ - "01", - "02", - "03", - "04", - "05", - "06", - "07", - "08", - "09", - "10", - "11", - "12", - "13", - "14", - "15", - "16", - "17", - "18", - "19", - "20", - "21", - "22", - "23", - "24", - "25", - "26", - "27", - "28", - "29", - "30", - "31", - "32", - "33", - "34", - "35", - "36", - "37", - "38", - "39", - "40", - "41", - "42", - "43", - "44", - "45", - "46", - "47", - "48", - "49", - "50", - "51", - "52", - "53", - "54", - "55", - "56", - "57", - "58", - "59", - "60", - "61", - "62", + "Shelter In Place Warning", + "Short Term Forecast", + "Small Craft Advisory", + "Small Craft Advisory For Hazardous Seas", + "Small Craft Advisory For Rough Bar", + "Small Craft Advisory For Winds", + "Small Stream Flood Advisory", + "Snow Squall Warning", + "Special Marine Warning", + "Special Weather Statement", + "Storm Surge Warning", + "Storm Surge Watch", + "Storm Warning", + "Storm Watch", + "Test", + "Tornado Warning", + "Tornado Watch", + "Tropical Depression Local Statement", + "Tropical Storm Local Statement", + "Tropical Storm Warning", + "Tropical Storm Watch", + "Tsunami Advisory", + "Tsunami Warning", + "Tsunami Watch", + "Typhoon Local Statement", + "Typhoon Warning", + "Typhoon Watch", + "Urban And Small Stream Flood Advisory", + "Volcano Warning", + "Wind Advisory", + "Wind Chill Advisory", + "Wind Chill Warning", + "Wind Chill Watch", + "Winter Storm Warning", + "Winter Storm Watch", + "Winter Weather Advisory", ] +# Generate the WA list based on the length of WS +WA = [str(i + 1) for i in range(len(WS))] + # Test if the script needs to start from a clean slate CLEANSLATE = config.get("DEV", {}).get("CLEANSLATE", False) if CLEANSLATE: @@ -265,22 +278,29 @@ 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["alertscript_alerts"] - state["last_alerts"] = state["last_alerts"] + 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", []) + state["active_alerts"] = state.get("active_alerts", []) return state else: return { "ct": None, "id": None, "alertscript_alerts": [], - "last_alerts": [], + "last_alerts": OrderedDict(), "last_sayalert": [], + "active_alerts": [], } @@ -289,13 +309,14 @@ def save_state(state): Save the state to the state file. Args: - state (dict): A dictionary containing data. + state (OrderedDict): A dictionary containing data. """ state["alertscript_alerts"] = list(state["alertscript_alerts"]) - state["last_alerts"] = list(state["last_alerts"]) + state["last_alerts"] = list(state["last_alerts"].items()) state["last_sayalert"] = list(state["last_sayalert"]) + state["active_alerts"] = list(state["active_alerts"]) with open(data_file, "w") as file: - json.dump(state, file) + json.dump(state, file, ensure_ascii=False, indent=4) def getAlerts(countyCodes): @@ -307,7 +328,7 @@ def getAlerts(countyCodes): Returns: alerts (list): List of active weather alerts. - In case of alert injection from the config, return the injected alerts. + descriptions (dict): Dictionary of alert descriptions. """ # Mapping for severity for API response and the 'words' severity severity_mapping_api = { @@ -319,15 +340,24 @@ def getAlerts(countyCodes): } severity_mapping_words = {"Warning": 4, "Watch": 3, "Advisory": 2, "Statement": 1} - # Inject alerts if DEV INJECT is enabled in the config + alerts = OrderedDict() + seen_alerts = set() # Store seen alerts + current_time = datetime.now(timezone.utc) + + # Check if injection is enabled if config.get("DEV", {}).get("INJECT", False): logger.debug("getAlerts: DEV Alert Injection Enabled") - alerts = [alert.strip() for alert in config["DEV"].get("INJECTALERTS", [])] - logger.debug("getAlerts: Injecting alerts: %s", alerts) - return alerts + injected_alerts = config["DEV"].get("INJECTALERTS", []) + logger.debug("getAlerts: Injecting alerts: %s", injected_alerts) - alerts = [] - current_time = datetime.now(timezone.utc) + for event in injected_alerts: + last_word = event.split()[-1] + severity = severity_mapping_words.get(last_word, 0) + alerts[(event, severity)] = "Manually injected." + + alerts = OrderedDict(list(alerts.items())[:max_alerts]) + + return alerts for countyCode in countyCodes: url = "https://api.weather.gov/alerts/active?zone={}".format(countyCode) @@ -344,23 +374,35 @@ def getAlerts(countyCodes): expires_time = parser.isoparse(expires) if effective_time <= current_time < expires_time: event = feature["properties"]["event"] + description = feature["properties"].get("description", "") + severity = feature["properties"].get("severity", None) + # Check if alert has already been seen + if event in seen_alerts: + continue + + # Initialize a flag to check if the event is globally blocked + is_blocked = False 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, ) + is_blocked = True break + + # Skip to the next feature if the event is globally blocked + if is_blocked: + continue + + if severity is None: + last_word = event.split()[-1] + severity = severity_mapping_words.get(last_word, 0) else: - severity = feature["properties"].get("severity", None) - if severity is None: - last_word = event.split()[-1] - severity = severity_mapping_words.get(last_word, 0) - else: - severity = severity_mapping_api.get(severity, 0) - alerts.append( - (event, severity) - ) # Add event to list as a tuple + severity = severity_mapping_api.get(severity, 0) + alerts[(event, severity)] = description + seen_alerts.add(event) + else: logger.error( "Failed to retrieve alerts for %s, HTTP status code %s, response: %s", @@ -369,21 +411,18 @@ def getAlerts(countyCodes): response.text, ) - alerts = list(dict.fromkeys(alerts)) - - alerts.sort( - key=lambda x: ( - x[1], # API-provided severity - severity_mapping_words.get(x[0].split()[-1], 0), # 'words' severity - ), - reverse=True, + alerts = OrderedDict( + sorted( + alerts.items(), + key=lambda item: ( + item[0][1], # API-provided severity + severity_mapping_words.get(item[0][0].split()[-1], 0), # Words severity + ), + reverse=True, + ) ) - logger.debug("getAlerts: Sorted alerts - (alert), (severity)") - for alert in alerts: - logger.debug(alert) - - alerts = [alert[0] for alert in alerts[:max_alerts]] + alerts = OrderedDict(list(alerts.items())[:max_alerts]) return alerts @@ -393,13 +432,16 @@ def sayAlert(alerts): Generate and broadcast severe weather alert sounds on Asterisk. Args: - alerts (list): List of active weather alerts. + alerts (OrderedDict): OrderedDict of active weather alerts and their descriptions. """ # 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 @@ -418,13 +460,13 @@ 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", "SWP97.wav") + os.path.join(sounds_path, "ALERTS", "SWP_149.wav") ) sound_effect = AudioSegment.from_wav( - os.path.join(sounds_path, "ALERTS", "SWP95.wav") + os.path.join(sounds_path, "ALERTS", "SWP_147.wav") ) alert_count = 0 @@ -432,18 +474,18 @@ def sayAlert(alerts): try: index = WS.index(alert) audio_file = AudioSegment.from_wav( - os.path.join(sounds_path, "ALERTS", "SWP{}.wav".format(WA[index])) + 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] + "sayAlert: Added %s (SWP_%s.wav) to alert sound", alert, WA[index] ) alert_count += 1 except ValueError: logger.error("sayAlert: Alert not found: %s", alert) except FileNotFoundError: logger.error( - "sayAlert: Audio file not found: %s/ALERTS/SWP%s.wav", + "sayAlert: Audio file not found: %s/ALERTS/SWP_%s.wav", sounds_path, WA[index], ) @@ -469,8 +511,19 @@ 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( + "Waiting %s seconds for Asterisk to make announcement to avoid doubling alerts with tailmessage...", + wait_time, + ) + time.sleep(wait_time) def sayAllClear(): @@ -482,7 +535,7 @@ def sayAllClear(): state["last_sayalert"] = [] save_state(state) - alert_clear = os.path.join(sounds_path, "ALERTS", "SWP96.wav") + alert_clear = os.path.join(sounds_path, "ALERTS", "SWP_148.wav") node_numbers = config.get("Asterisk", {}).get("Nodes", []) for node_number in node_numbers: @@ -501,8 +554,12 @@ 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") + logger.info("buildTailMessage: No alerts, creating silent tailmessage") silence = AudioSegment.silent(duration=100) converted_silence = convertAudio(silence) converted_silence.export(tailmessage_file, format="wav") @@ -510,10 +567,10 @@ def buildTailmessage(alerts): combined_sound = AudioSegment.empty() sound_effect = AudioSegment.from_wav( - os.path.join(sounds_path, "ALERTS", "SWP95.wav") + 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 @@ -526,11 +583,11 @@ def buildTailmessage(alerts): try: index = WS.index(alert) audio_file = AudioSegment.from_wav( - os.path.join(sounds_path, "ALERTS", "SWP{}.wav".format(WA[index])) + 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", + "buildTailMessage: Added %s (SWP_%s.wav) to tailmessage", alert, WA[index], ) @@ -538,7 +595,7 @@ def buildTailmessage(alerts): logger.error("Alert not found: %s", alert) except FileNotFoundError: logger.error( - "Audio file not found: %s/ALERTS/SWP%s.wav", + "Audio file not found: %s/ALERTS/SWP_%s.wav", sounds_path, WA[index], ) @@ -549,6 +606,7 @@ def buildTailmessage(alerts): ) combined_sound = AudioSegment.silent(duration=100) + logger.info("Built new tailmessage") logger.debug("buildTailMessage: Exporting tailmessage to %s", tailmessage_file) converted_combined_sound = convertAudio(combined_sound) converted_combined_sound.export(tailmessage_file, format="wav") @@ -700,6 +758,21 @@ def alertScript(alerts): :param alerts: List of alerts to process :type alerts: list[str] """ + + # Load the saved state + state = load_state() + processed_alerts = state["alertscript_alerts"] + active_alerts = state.get("active_alerts", []) # Load active alerts from state + + # Extract only the alert names from the OrderedDict keys + alert_names = [alert[0] for alert in alerts.keys()] + + # New alerts are those that are in the current alerts but were not active before + new_alerts = list(set(alert_names) - set(active_alerts)) + + # Update the active alerts in the state + state["active_alerts"] = alert_names + # Fetch AlertScript configuration from global_config alertScript_config = config.get("AlertScript", {}) logger.debug("AlertScript configuration: %s", alertScript_config) @@ -710,6 +783,9 @@ def alertScript(alerts): mappings = [] logger.debug("Mappings: %s", mappings) + # A set to hold alerts that are processed in this run + currently_processed_alerts = set() + # Iterate over each mapping for mapping in mappings: logger.debug("Processing mapping: %s", mapping) @@ -720,7 +796,7 @@ def alertScript(alerts): match_type = mapping.get("Match", "ANY").upper() matched_alerts = [] - for alert in alerts: + for alert in new_alerts: # We only check the new alerts for trigger in triggers: if fnmatch.fnmatch(alert, trigger): logger.debug( @@ -740,18 +816,27 @@ def alertScript(alerts): ) # 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 alert in matched_alerts: + currently_processed_alerts.add(alert) + + if mapping.get("Type") == "BASH": + logger.debug('Mapping type is "BASH"') 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) + 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.info( + "AlertScript: Executing DTMF command: %s", dtmf_cmd + ) + subprocess.run(dtmf_cmd, shell=True) + + # Update the state with the alerts processed in this run + state["alertscript_alerts"] = list(currently_processed_alerts) + save_state(state) def sendPushover(message, title=None, priority=0): @@ -831,9 +916,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: @@ -857,19 +947,30 @@ def change_and_log_CT_or_ID( logger.debug("%s auto change is not enabled", alert_type) +def supermon_back_compat(alerts): + """ + Write alerts to a file for backward compatibility with supermon. + + Args: + alerts (OrderedDict): The alerts to write. + """ + + # Ensure the target directory exists + os.makedirs("/tmp/AUTOSKY", exist_ok=True) + + # Get alert titles (without severity levels) + alert_titles = [alert[0] for alert in alerts.keys()] + + # Write alert titles to a file, with each title on a new line + with open("/tmp/AUTOSKY/warnings.txt", "w") as file: + file.write("
".join(alert_titles)) + + def main(): """ The main function that orchestrates the entire process of fetching and processing severe weather alerts, then integrating these alerts into an Asterisk/app_rpt based radio repeater system. - - Key Steps: - 1. Fetch the configuration from the local setup. - 2. Get the new alerts based on the provided county codes. - 3. Compare the new alerts with the previously stored alerts. - 4. If there's a change, store the new alerts and process them accordingly. - 5. Check each alert against a set of specified alert types and perform actions accordingly. - 6. Send notifications if enabled. """ # Fetch configurations say_alert_enabled = config["Alerting"].get("SayAlert", False) @@ -886,9 +987,9 @@ def main(): alerts = getAlerts(countyCodes) # If new alerts differ from old ones, process new alerts - logger.debug("Last alerts: %s", last_alerts) - logger.debug("New alerts: %s", alerts) - if last_alerts != alerts: + + if last_alerts.keys() != alerts.keys(): + new_alerts = [x for x in alerts.keys() if x not in last_alerts.keys()] state["last_alerts"] = alerts save_state(state) @@ -901,9 +1002,15 @@ def main(): pushover_enabled = config["Pushover"].get("Enable", False) pushover_debug = config["Pushover"].get("Debug", False) + supermon_compat_enabled = config["DEV"].get("SupermonCompat", True) + if supermon_compat_enabled: + supermon_back_compat(alerts) + # Initialize pushover message pushover_message = ( - "Alerts Cleared\n" if len(alerts) == 0 else "\n".join(alerts) + "\n" + "Alerts Cleared\n" + if len(alerts) == 0 + else "\n".join(str(key) for key in alerts.keys()) + "\n" ) # Check if Courtesy Tones (CT) or ID needs to be changed @@ -926,15 +1033,15 @@ def main(): # Check if alerts need to be communicated if len(alerts) == 0: - logger.info("No alerts found") + logger.info("Alerts cleared") if say_all_clear_enabled: sayAllClear() else: - logger.info("Alerts found: %s", alerts) - if alertscript_enabled: - alertScript(alerts) + logger.info("New alerts: %s", new_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 c302958..4210839 100644 --- a/config.yaml +++ b/config.yaml @@ -1,4 +1,4 @@ -# SkywarnPlus v0.2.2 Configuration File +# SkywarnPlus v0.2.3 Configuration File # Author: Mason Nelson (N5LSN/WRKF394) # Please edit this file according to your specific requirements. # @@ -18,11 +18,11 @@ SKYWARNPLUS: Asterisk: # List of node numbers for broadcasting alerts. Multiple nodes are specified as a list. # Example: - # Nodes: + # Nodes: # - 1998 # - 1999 Nodes: - - YOUR_NODE_NUMBER_HERE + - YOUR_NODE_NUMBER ################################################################################################################################ @@ -35,7 +35,7 @@ Alerting: # - ARC121 # - ARC021 CountyCodes: - - YOUR_COUNTY_CODE_HERE + - YOUR_COUNTY_CODE # Enable instant voice announcement when new weather alerts are issued. # Set to 'True' for enabling or 'False' for disabling. # Example: SayAlert: true @@ -57,7 +57,7 @@ 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: + # GlobalBlockedEvents: # - Flood Watch # - '*Statement' # - '*Advisory' @@ -76,7 +76,7 @@ Tailmessage: # 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. + # 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,21 +153,55 @@ 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: + # 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 ################################################################################################################################ @@ -160,67 +209,69 @@ 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 - # + # Define the mapping of alerts to either DTMF commands or bash scripts here. + # 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 + # + # + # 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!"' - Triggers: + - '/usr/local/bin/SkywarnPlus/SkyDescribe.py "Tornado Warning"' + Triggers: - Tornado Warning ################################################################################################################################ @@ -240,7 +291,7 @@ Pushover: Logging: # Enable verbose logging Debug: false - # Specify an alternative log file path. + # Specify an alternative log file path. # LogPath: ################################################################################################################################ @@ -248,12 +299,14 @@ Logging: DEV: # Delete cached data on startup CLEANSLATE: false - # Specify the TMP directory. + # Specify the TMP directory. TmpDir: /tmp/SkywarnPlus + # Write alert titles to /tmp/AUTOSKY/alerts.txt for Supermon backwards compatibility. + SupermonCompat: true # 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. INJECTALERTS: - Tornado Warning - Tornado Watch - - Severe Thunderstorm Warning \ No newline at end of file + - Severe Thunderstorm Warning