diff --git a/SkyControl.py b/SkyControl.py
index 7a11148..d17306c 100644
--- a/SkyControl.py
+++ b/SkyControl.py
@@ -1,7 +1,7 @@
#!/usr/bin/python3
"""
-SkyControl.py v0.7.0 by Mason Nelson
+SkyControl.py v0.8.0 by Mason Nelson
==================================
A Control Script for SkywarnPlus
diff --git a/SkyDescribe.py b/SkyDescribe.py
index 644e205..ff2b328 100644
--- a/SkyDescribe.py
+++ b/SkyDescribe.py
@@ -1,7 +1,7 @@
#!/usr/bin/python3
"""
-SkyDescribe.py v0.7.0 by Mason Nelson
+SkyDescribe.py v0.8.0 by Mason Nelson
==================================================
Text to Speech conversion for Weather Descriptions
diff --git a/SkywarnPlus.py b/SkywarnPlus.py
index a0f1083..7284316 100644
--- a/SkywarnPlus.py
+++ b/SkywarnPlus.py
@@ -1,7 +1,7 @@
#!/usr/bin/python3
"""
-SkywarnPlus.py v0.7.0 by Mason Nelson
+SkywarnPlus.py v0.8.0 by Mason Nelson
===============================================================================
SkywarnPlus is a utility that retrieves severe weather alerts from the National
Weather Service and integrates these alerts with an Asterisk/app_rpt based
@@ -60,11 +60,8 @@ with open(CONFIG_PATH, "r") as config_file:
config = yaml.load(config_file)
config = json.loads(json.dumps(config)) # Convert config to a normal dictionary
-# Check if SkywarnPlus is enabled
+# Define whether SkywarnPlus is enabled in config.yaml
MASTER_ENABLE = config.get("SKYWARNPLUS", {}).get("Enable", False)
-if not MASTER_ENABLE:
- print("SkywarnPlus is disabled in config.yaml, exiting...")
- exit()
# Define tmp_dir and sounds_path
TMP_DIR = config.get("DEV", {}).get("TmpDir", "/tmp/SkywarnPlus")
@@ -1501,39 +1498,307 @@ def change_id(id):
return True
-def supermon_back_compat(alerts):
+def supermon_back_compat(alerts, county_data):
"""
- Write alerts to a file for backward compatibility with supermon.
+ Write alerts to a file for backwards compatibility with Supermon.
+ Will NOT work on newer Debian systems (ASL3) where SkywarnPlus is not run as the root user.
"""
+
+ # Exit with a debug statement if we are not the root user
+ if os.getuid() != 0:
+ LOGGER.debug("supermon_back_compat: Not running as root, exiting function")
+ return
+
try:
# Ensure the target directory exists for /tmp/AUTOSKY
os.makedirs("/tmp/AUTOSKY", exist_ok=True)
- # Get alert titles (without severity levels)
- alert_titles = list(alerts.keys())
+ # Construct alert titles with county names
+ alert_titles_with_counties = [
+ "{} [{}]".format(
+ alert,
+ ", ".join(
+ replace_with_county_name(x["county_code"], county_data)
+ for x in alerts[alert]
+ ),
+ )
+ for alert in alerts
+ ]
- # 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))
+ # Check write permissions before writing to the file
+ if os.access("/tmp/AUTOSKY", os.W_OK):
+ with open("/tmp/AUTOSKY/warnings.txt", "w") as file:
+ file.write("
".join(alert_titles_with_counties))
+ LOGGER.debug("Successfully wrote alerts to /tmp/AUTOSKY/warnings.txt")
+ else:
+ LOGGER.error("No write permission for /tmp/AUTOSKY")
except Exception as e:
- print("An error occurred while writing to /tmp/AUTOSKY: {}".format(str(e)))
+ LOGGER.error("An error occurred while writing to /tmp/AUTOSKY: %s", str(e))
try:
# Ensure the target directory exists for /var/www/html/AUTOSKY
os.makedirs("/var/www/html/AUTOSKY", exist_ok=True)
- # Also write to other path sometimes used by Supermon
- with open("/var/www/html/AUTOSKY/warnings.txt", "w") as file:
- file.write("
".join(alert_titles))
+ # Check write permissions before writing to the file
+ if os.access("/var/www/html/AUTOSKY", os.W_OK):
+ with open("/var/www/html/AUTOSKY/warnings.txt", "w") as file:
+ file.write("
".join(alert_titles_with_counties))
+ LOGGER.debug(
+ "Successfully wrote alerts to /var/www/html/AUTOSKY/warnings.txt"
+ )
+ else:
+ LOGGER.error("No write permission for /var/www/html/AUTOSKY")
except Exception as e:
- print(
- "An error occurred while writing to /var/www/html/AUTOSKY: {}".format(
- str(e)
+ LOGGER.error(
+ "An error occurred while writing to /var/www/html/AUTOSKY: %s", str(e)
+ )
+
+
+def ast_var_update():
+ """
+ Function to mimic the behavior of the ast_var_update.sh script from Supermon2 in HamVoIP.
+ Updated Asterisk channel variables for nodes defined in node_info.ini, including CPU load, temperature, and more.
+ Supermon2 will display these variables in the Node Information section.
+ """
+ LOGGER.debug("ast_var_update: Starting function")
+
+ # Exit if Supermon2 directory does not exist
+ if not os.path.exists("/srv/http/supermon2"):
+ LOGGER.debug(
+ "ast_var_update: Supermon2 directory does not exist, exiting function"
+ )
+ return
+
+ node_info_path = "/usr/local/sbin/supermon/node_info.ini"
+ allstar_env_path = "/usr/local/etc/allstar.env"
+
+ WX_CODE = ""
+ WX_LOCATION = ""
+ NODE = ""
+
+ # Read allstar.env file and extract environment variables
+ env_vars = {}
+ try:
+ LOGGER.debug(
+ "ast_var_update: Reading environment variables from %s", allstar_env_path
+ )
+ with open(allstar_env_path, "r") as file:
+ for line in file:
+ if line.startswith("export "):
+ key, value = line.split("=", 1)
+ key = key.split()[1].strip()
+ value = value.strip().strip('"')
+ env_vars[key] = value
+ LOGGER.debug(
+ "ast_var_update: Found environment variable %s = %s", key, value
+ )
+ except Exception as e:
+ LOGGER.error("ast_var_update: Error reading %s: %s", allstar_env_path, e)
+ return
+
+ # Read node_info.ini file and process lines
+ try:
+ LOGGER.debug("ast_var_update: Reading node information from %s", node_info_path)
+ with open(node_info_path, "r") as file:
+ for line in file:
+ if line.startswith("NODE="):
+ node_value = line.split("=", 1)[1].strip().strip('"')
+ if node_value.startswith("$"):
+ env_var_name = node_value[1:]
+ NODE = env_vars.get(env_var_name, "")
+ else:
+ NODE = node_value
+ LOGGER.debug("ast_var_update: NODE set to %s", NODE)
+ elif line.startswith("WX_CODE="):
+ WX_CODE = line.split("=", 1)[1].strip().strip('"')
+ LOGGER.debug("ast_var_update: WX_CODE set to %s", WX_CODE)
+ elif line.startswith("WX_LOCATION="):
+ WX_LOCATION = line.split("=", 1)[1].strip().strip('"')
+ LOGGER.debug("ast_var_update: WX_LOCATION set to %s", WX_LOCATION)
+ except Exception as e:
+ LOGGER.error("ast_var_update: Error reading %s: %s", node_info_path, e)
+ return
+
+ if not NODE:
+ LOGGER.debug("ast_var_update: No NODE defined, exiting function")
+ return
+
+ try:
+ LOGGER.debug("ast_var_update: Retrieving Asterisk registrations")
+ registrations = (
+ subprocess.check_output(["/bin/asterisk", "-rx", "iax2 show registry"])
+ .decode("utf-8")
+ .splitlines()[1:]
+ )
+ except subprocess.CalledProcessError as e:
+ LOGGER.error("ast_var_update: Error getting Asterisk registrations: %s", e)
+ registrations = "No Registrations on this server"
+
+ if registrations == "No Registrations on this server" or not registrations:
+ LOGGER.debug("ast_var_update: No registrations found")
+ registrations = "No Registrations on this server"
+ else:
+ nodes = {}
+ for reg in registrations:
+ parts = reg.split()
+ node_number = parts[2].split("#")[0]
+ server_ip = parts[0].split(":")[0]
+ try:
+ server_domain = (
+ subprocess.check_output(["dig", "+short", "-x", server_ip])
+ .decode("utf-8")
+ .strip()
+ )
+ LOGGER.debug(
+ "ast_var_update: Resolved %s to %s", server_ip, server_domain
+ )
+ except subprocess.CalledProcessError as e:
+ LOGGER.error(
+ "ast_var_update: Error resolving domain for IP %s: %s", server_ip, e
+ )
+ server_domain = "Unknown"
+
+ node_value = "Node - {} is {}".format(parts[2], parts[5])
+ if "Registered" in node_value:
+ if "hamvoip" in server_domain:
+ nodes[node_number] = nodes.get(node_number, "") + "Hamvoip "
+ else:
+ nodes[node_number] = nodes.get(node_number, "") + "Allstar "
+
+ registered = ""
+ for key in nodes:
+ nodes[key] = nodes[key].replace(" ", ",")
+ registered += "{} Registered {} ".format(key, nodes[key])
+
+ registered = registered.strip().rstrip(",")
+ registrations = registered
+ LOGGER.debug("ast_var_update: Processed registrations: %s", registrations)
+
+ try:
+ LOGGER.debug("ast_var_update: Retrieving system uptime")
+ cpu_up = "Up since {}".format(
+ subprocess.check_output(["uptime", "-s"]).decode("utf-8").strip()
+ )
+
+ LOGGER.debug("ast_var_update: Retrieving CPU load")
+ cpu_load = (
+ subprocess.check_output(["uptime"])
+ .decode("utf-8")
+ .strip()
+ .split("load ")[1]
+ )
+
+ LOGGER.debug("ast_var_update: Retrieving CPU temperature")
+ cpu_temp = (
+ subprocess.check_output(["/opt/vc/bin/vcgencmd", "measure_temp"])
+ .decode("utf-8")
+ .strip()
+ .split("=")[1][:-2]
+ )
+ cpu_temp = float(cpu_temp)
+ except subprocess.CalledProcessError as e:
+ LOGGER.error("ast_var_update: Error retrieving system info: %s", e)
+ return
+
+ cputemp_disp = "{}C".format(
+ "lightgreen" if cpu_temp <= 50 else "yellow" if cpu_temp <= 60 else "#fa4c2d",
+ int(cpu_temp),
+ )
+ LOGGER.debug("ast_var_update: CPU temperature display: %s", cputemp_disp)
+
+ try:
+ LOGGER.debug("ast_var_update: Retrieving log size info")
+ logsz = (
+ subprocess.check_output(["df", "-h", "/var/log"])
+ .decode("utf-8")
+ .splitlines()[1]
+ .split()
+ )
+ logs = "Logs - {} {} used, {} remains".format(logsz[2], logsz[4], logsz[3])
+ except subprocess.CalledProcessError as e:
+ LOGGER.error("ast_var_update: Error retrieving log size info: %s", e)
+ return
+
+ wx = ""
+ if WX_CODE and WX_LOCATION:
+ try:
+ LOGGER.debug("ast_var_update: Retrieving weather info")
+ wx_info = (
+ subprocess.check_output(["/usr/local/sbin/weather.sh", WX_CODE, "v"])
+ .decode("utf-8")
+ .strip()
)
+ wx = "{} ({})".format(WX_LOCATION, wx_info)
+ except subprocess.CalledProcessError as e:
+ LOGGER.error("ast_var_update: Error retrieving weather info: %s", e)
+ wx = "{} (Weather info not available)".format(WX_LOCATION)
+
+ # Filter out the \xb0 character from the string
+ wx = wx.replace("\xb0", "")
+
+ LOGGER.debug("ast_var_update: Weather info display: %s", wx)
+
+ try:
+ LOGGER.debug("ast_var_update: Reading alert content from warnings.txt")
+ with open("/tmp/AUTOSKY/warnings.txt") as f:
+ alert_content = f.read().strip()
+ except Exception as e:
+ LOGGER.error("ast_var_update: Error reading warnings.txt: %s", e)
+ alert_content = ""
+
+ if not MASTER_ENABLE:
+ alert = "SkywarnPlus Disabled"
+ elif not alert_content:
+ alert = "SkywarnPlus Enabled
No Alerts"
+ else:
+ # Adjusted to remove both '[' and ']' correctly
+ alert_content_cleaned = alert_content.replace("[", "").replace("]", "")
+ alert = "SkywarnPlus Enabled
{}".format(
+ alert_content
)
+ LOGGER.debug("ast_var_update: Alert display: %s", alert)
+
+ for ni in NODE.split():
+ if ni:
+ grep_cmd = "grep -q '[[:blank:]]*\\[{}\\]' /etc/asterisk/rpt.conf".format(
+ ni
+ )
+ if subprocess.call(grep_cmd, shell=True) == 0:
+ main_cmd = (
+ 'asterisk -rx "rpt setvar {} cpu_up=\\"{}\\" cpu_load=\\"{}\\" cpu_temp=\\"{}\\" WX=\\"{}\\" LOGS=\\"{}\\" REGISTRATIONS=\\"{}\\""'
+ ).format(ni, cpu_up, cpu_load, cputemp_disp, wx, logs, registrations)
+ alert_cmd = ('asterisk -rx "rpt setvar {} ALERT=\\"{}\\""').format(
+ ni, alert
+ )
+
+ main_cmd = main_cmd.encode("ascii", "ignore").decode("ascii")
+ alert_cmd = alert_cmd.encode("ascii", "ignore").decode("ascii")
+
+ LOGGER.debug("ast_var_update: Running main command: %s", main_cmd)
+ try:
+ subprocess.call(main_cmd, shell=True)
+ except subprocess.CalledProcessError as e:
+ LOGGER.error(
+ "ast_var_update: Error running main command for node %s: %s",
+ ni,
+ e,
+ )
+
+ LOGGER.debug("ast_var_update: Running alert command: %s", alert_cmd)
+ try:
+ subprocess.call(alert_cmd, shell=True)
+ except subprocess.CalledProcessError as e:
+ LOGGER.error(
+ "ast_var_update: Error running alert command for node %s: %s",
+ ni,
+ e,
+ )
+
+ LOGGER.debug("ast_var_update: Function completed")
+
def detect_county_changes(old_alerts, new_alerts):
"""
@@ -1608,6 +1873,7 @@ def main():
processing severe weather alerts, then integrating these alerts into
an Asterisk/app_rpt based radio repeater system.
"""
+
# Fetch configurations
say_alert_enabled = config["Alerting"].get("SayAlert", False)
say_alert_all = config["Alerting"].get("SayAlertAll", False)
@@ -1622,10 +1888,20 @@ def main():
supermon_compat_enabled = config["DEV"].get("SupermonCompat", True)
say_alerts_changed = config["Alerting"].get("SayAlertsChanged", True)
+ # Check if SkywarnPlus is enabled
+ if not MASTER_ENABLE:
+ print("SkywarnPlus is disabled in config.yaml, exiting...")
+ if supermon_compat_enabled:
+ ast_var_update()
+ exit()
+
# Load previous alert data to compare changes
state = load_state()
last_alerts = state["last_alerts"]
+ # Load county names from YAML file so that county codes can be replaced with county names in messages
+ county_data = load_county_names(COUNTY_CODES_PATH)
+
# If data file does not exist, assume this is the first run and initialize data file and CT/ID/Tailmessage files if enabled
if not os.path.isfile(DATA_FILE):
LOGGER.info("Data file does not exist, assuming first run.")
@@ -1641,16 +1917,20 @@ def main():
LOGGER.info("Initializing Tailmessage file")
empty_alerts = OrderedDict()
build_tailmessage(empty_alerts)
+ if supermon_compat_enabled:
+ supermon_back_compat(last_alerts, county_data)
# Fetch new alert data
alerts = get_alerts(COUNTY_CODES)
- # Load county names from YAML file so that county codes can be replaced with county names in messages
- county_data = load_county_names(COUNTY_CODES_PATH)
-
# Placeholder for constructing a pushover message
pushover_message = ""
+ # Update HamVoIP Asterisk channel variables
+ if supermon_compat_enabled:
+ supermon_back_compat(alerts, county_data)
+ ast_var_update()
+
# Determine which alerts have been added since the last check
added_alerts = [alert for alert in alerts if alert not in last_alerts]
for alert in added_alerts:
@@ -1764,8 +2044,8 @@ def main():
# If alerts have been added, removed
if added_alerts or removed_alerts:
# Push alert titles to Supermon if enabled
- if supermon_compat_enabled:
- supermon_back_compat(alerts)
+ # if supermon_compat_enabled:
+ # supermon_back_compat(alerts)
# Change CT/ID if necessary and enabled
change_ct_id_helper(
diff --git a/config.yaml b/config.yaml
index 4219228..a277ae8 100644
--- a/config.yaml
+++ b/config.yaml
@@ -1,4 +1,4 @@
-# SkywarnPlus v0.7.0 Configuration File
+# SkywarnPlus v0.8.0 Configuration File
# Author: Mason Nelson (N5LSN/WRKF394)
# Please edit this file according to your specific requirements.
@@ -438,7 +438,16 @@ DEV:
# Specify the TMP directory.
TmpDir: /tmp/SkywarnPlus
- # Write alert titles to /tmp/AUTOSKY/warnings.txt for Supermon backwards compatibility.
+ # ATTEMPT to write alerts to the old AUTOSKY files so that Supermon can display them.
+ # This might not work due to there being several different versions of Supermon,
+ # file permissions issues, etc at no fault to SkywarnPlus. This is a best-effort feature.
+ #
+ # If you are using Supermon2 on HamVoIP, please add your location and ZIP
+ # to the file /usr/local/sbin/supermon/node_info.ini
+ #
+ # DO NOT ADD ast_var_update.sh TO CRON IF YOU ARE USING THIS FEATURE.
+ #
+ # Please encourage Supermon developers to add support for SkywarnPlus so this hack can be removed.
SupermonCompat: true
# Enable test alert injection instead of calling the NWS API by setting 'INJECT' to 'True'.
diff --git a/swp-install b/swp-install
index 6bc4dcd..d339963 100644
--- a/swp-install
+++ b/swp-install
@@ -201,9 +201,9 @@ setup_crontab() {
fi
else
if [ "$SYSTEM_TYPE" = "ASL3" ]; then
- CRONTAB_ENTRY="*/$CRONTAB_INTERVAL * * * * asterisk pgrep -f /usr/local/bin/SkywarnPlus/SkywarnPlus.py > /dev/null || /usr/local/bin/SkywarnPlus/SkywarnPlus.py"
+ CRONTAB_ENTRY="*/$CRONTAB_INTERVAL * * * * asterisk /usr/local/bin/SkywarnPlus/SkywarnPlus.py"
else
- CRONTAB_ENTRY="*/$CRONTAB_INTERVAL * * * * root pgrep -f /usr/local/bin/SkywarnPlus/SkywarnPlus.py > /dev/null || /usr/local/bin/SkywarnPlus/SkywarnPlus.py"
+ CRONTAB_ENTRY="*/$CRONTAB_INTERVAL * * * * root /usr/local/bin/SkywarnPlus/SkywarnPlus.py"
fi
CRON_FILE="/etc/cron.d/SkywarnPlus"