#!/usr/bin/php
<?php
#
# Routine Execute
#
# (C) Copyright 2019-2026, Bogen Communications LLC. All rights reserved.
#
# This script executes the specified routine.
#
# Usage: routine-execute -r <routine.id> [options]...
#
# [options]:
#
# -c CALLERID from dial plan	- Caller's ID
#
# -C EXTEN from dial plan	- Called party
#
# -D				- Enable debug mode
#
# -j <filename>			- JSON file to read and parse for user data
#
# -k <routine_task.pid>		- kill Routine process (must include -r option)
#
# -P | -p			- Extra paramaters passed from routine-trigger
#
# -r <routine.id>		- Routine to execute (MUST BE FIRST PARAMETER)
#
# -S [seconds]			- Number of seconds to sleep before executing routine
#
# -s 1|0			- State (1=activated,0=deactivated,1 is default)
#
# -t <routine_trigger.type>	- Trigger type that executed routine
#
# -x <filename>			- XML file to read and parse for CAP data input via API
#
# -z <zone_numbers>		- Comma/Colon separated list of Zone numbers
#
# -1 through -9			- Additional arguments received from routine-trigger
#
# Notes:
#   1. -s value defaults to 1 if not specified on command line.
#   2. -z value defaults to 0 (all speakers) if not specified on command line.
#   3. -r option must be the first option on the command line.
#
require '/usr/local/bin/db_credentials' ;
require '/usr/local/bin/cap_parser' ;

$debug = FALSE;

$myPid = getmypid();

#
# Handle signals
#
pcntl_signal(SIGHUP,  "doExit");
pcntl_signal(SIGQUIT, "doExit");
pcntl_signal(SIGSEGV, "doExit");
pcntl_signal(SIGTERM, "doExit");
pcntl_signal(SIGUSR1, "doExit");
pcntl_signal(SIGUSR2, SIG_IGN);

define("ROUTINE_LOG_INTERNAL", 1);
define("ROUTINE_LOG_USER", 2);
define("ROUTINE_LOG_SYSLOG", 3);
define("QUERY_SUCCESSFUL", 999);

$amiSocket = FALSE;
$routineId = 0;
$routineName = "";
$routine_dtmf_code = 0;
$caller = "";
$origCaller = "";
$called = "";
$callerName = "Unknown";
$calledName = "Unknown";
$dashId = array();
$displayMsgId = array();
$autoDeleteDisplayMsgsUponExit = false;
$autoDeleteDashMsgsUponExit = false;
$killProcess = "";
$state = 1;
$zones = "0";
$zonesProvided = FALSE;
$cmdParam1 = "";
$cmdParam2 = "";
$callerChanged = FALSE;
$has_disable_audio = 0;
$has_enable_audio = 0;
$has_program_distribution = 0;
$has_manage_output_contacts = 0;
$gotCos = FALSE;
$trigger = "";
$addedPid = FALSE;
$numActions = 0;
$numActionsExecuted = 0;
$skipToActionNum = 0;
$gotoActionName = "";
$announceId = 0;
$announceTimesToPlay = 0;
$announceRepeatPeriod = 1;
$announceFilename = "";
$announceFiles = "";
$announce_number = "";
$alert_announce_played_count = 0;
$alert_announce_played_total_count = 0;
$routine_alert_executed_count = 0;
$routine_noalert_executed_count = 0;
$routine_alert_once = FALSE;
$routine_noalert_once = FALSE;
$post_dash_phone_text = FALSE;
$previous_alert_str = "";
$sleep_time = 0;
$extra_args = array('','','','','','','','','','');
$user_var = array('','','','','','','','','','','');
$cputemp = "";
$mergeDelete = FALSE;
$json_file = "";
$json_files_id = "";
$json_data = null;
$cap_xml_file = "";
$cap_data = null;
$cap_xml = null;
$action = "";
$webhook_return_str = "";
$db = NULL;

$cmd_options = getopt("C:c:DP:j:k:p:r:S:s:t:x:z:1:2:3:4:5:6:7:8:9:");

foreach (array_keys($cmd_options) as $opt)
{
	switch ($opt)
	{
		case 'C':
			$called = $cmd_options['C'];
			break;
		case 'c':
			$caller = $cmd_options['c'];
			break;
		case 'D':
			$debug = TRUE;
			break;
		case 'j':
			$json_file = $cmd_options['j'];
			break;
		case 'k':
			$killProcess = $cmd_options['k'];
			break;
		case 'P':
			$cmdParam1 = $cmd_options['P'];
			break;
		case 'p':
			$cmdParam2 = $cmd_options['p'];
			break;
		case 'r':
			$routineId = $cmd_options['r'];
			break;
		case 'S':
			$sleep_time = $cmd_options['S'];
			break;
		case 's':
			$state = $cmd_options['s'];
			break;
		case 'x':
			$cap_xml_file = $cmd_options['x'];
			break;
		case 't':
			$trigger = $cmd_options['t'];
			break;
		case 'z':
			$zones = $cmd_options['z'];
			$zonesProvided = TRUE;
			break;
		case '1':
		case '2':
		case '3':
		case '4':
		case '5':
		case '6':
		case '7':
		case '8':
		case '9':
			$extra_args[$opt] = $cmd_options[$opt];
			break;
		default:
			if ($debug) echo "Invalid option: $opt\n";
			break;
	}
}

if ($debug == FALSE) {
	error_reporting(0);
	mysqli_report(MYSQLI_REPORT_OFF);
}

if ($routineId == 0) doExit(1);

#
# sleep is used to implement seconds for scheduled routines,
# because crontab does not implement seconds to start on.
#
if ($sleep_time > 0)
{
	sleep($sleep_time);
}

$db = new mysqli($server, $user, $pass, $database);

if ($db->connect_errno > 0) {
	if ($debug) {
		die('Unable to connect to database ['.$db->connect_error.']');
	} else {
		doExit(1);
	}
}

#
# Kill Routine process; make sure it's in routine_task table!
#
if (($killProcess != "") && ($routineId != 0))
{
	if ($killProcess == 0) {
		$sql = "SELECT id, pid FROM routine_task
			WHERE routine_id='$routineId'";
	} else {
		$sql = "SELECT id, pid FROM routine_task
			WHERE routine_id='$routineId' AND pid='$killProcess'";
	}

	if (!$result = $db->query($sql)) {
		if ($debug)
			die('Query error ['.$db->error.']');
		else
			exit(1);
	}

	if ($result->num_rows <= 0) exit(1);

	while ($row = $result->fetch_assoc()) {
		$taskId  = $row['id'];
		$taskPid = $row['pid'];

		$retCode = killRoutineProcess($debug, $db, $taskId, $taskPid);
	}

	if (($result->num_rows == 1) && ($retCode == 1)) {
		exit(1);
	} else {
		exit(0);
	}
}

$time_zone = file_get_contents("/etc/timezone");
if ($time_zone != NULL) {
	$time_zone = trim($time_zone);
	date_default_timezone_set($time_zone);
}

#
# Load user supplied data from JSON file.
#
if ($json_file != "") {
	if (file_exists($json_file)) {
		#
		# Get json_files_id from json filename to be used for removing audio files after use,
		# and for removing json file and any unused uploaded audio/image files.
		#
		if (preg_match("/\/var\/www\/html\/laravel\/public\/exports\/json_(.*).json/",$json_file,$jparts)) {
			$json_files_id = $jparts[1];

			if ($debug) echo "JSON Files ID: $json_files_id\n";
		}

		#
		# Decode JSON data
		#
		$json_data = json_decode(file_get_contents($json_file),true);

		#
		# If JSON API was provided with bad JSON data, remove all uploaded files and exit.
		#
		if ($json_data == null) {
			if ($db != NULL) {
				if ($db->connect_errno == 0) {

					updateTaskList($debug, $db, $routineId, "DELETE", $myPid);

					# Get Routine name for LogText()
					if ($result = $db->query("SELECT name FROM routine WHERE id='$routineId'")) {
						if ($result->num_rows > 0) {
							$row = $result->fetch_assoc();
							$routineName = $row['name'];
						}
					}
				}
				$db->close();
			}
			if (!$debug) {
				if (strpos($json_file, '/var/www/html/laravel/public/exports/') == 0) {
					unlink($json_file);
				}
			}

			if ($json_files_id != "") {
				$json_files_id = str_replace(' ','',$json_files_id);
				$json_files_id = str_replace('*','',$json_files_id);
				$edir = "/var/www/html/laravel/public/exports";
				exec("rm -f $edir/*_$json_files_id.* >/dev/null 2>&1");
			}

			logText("Not started - bad JSON passed via API", ROUTINE_LOG_INTERNAL);

			exit(1);
		}

		#
		# Extract images and move them into place on behalf of Routine actions that use them.
		#
		if (isset($json_data['images']) && is_array($json_data['images'])) {
			$exports_dir = "/var/www/html/laravel/public/exports";
			$messages_dir = "/var/www/html/laravel/public/message_images";

			foreach ($json_data['images'] as $key => $fileName) {

				$sql = "SELECT id,file_name from message_image WHERE name='{\{images.$key\}}'";

				if ($result = $db->query($sql)) {
					if ($result->num_rows > 0) {
						$row = $result->fetch_assoc();
						$imageId = $row['id'];
						$prevFile = $row['file_name'];

						if (preg_match("/(.*).jpg$/", $fileName, $fparts)) {
							$imageType = "JPEG";
						} else {
							$imageType = "PNG";
						}
						unset($fparts);

						#
						# Get image width/height info
						#
						$iWidth = 0;
						$iHeight = 0;
						exec("/usr/bin/identify -format '%w,%h' $exports_dir/$fileName 2>/dev/null", $sizeData, $ret);
						if (isset($sizeData[0])) {
							echo "SizeInfo: $sizeInfo[0]\n";
							if (preg_match("/(.*),(.*)/", $sizeData[0], $sizeInfo)) {
								$iWidth = $sizeInfo[1];
								$iHeight = $sizeInfo[2];
							}
						}

						#
						# Update message_image table
						#
						$sql = "UPDATE message_image
							SET file_name='$fileName',type='$imageType',
								width='$iWidth',height='$iHeight'
							WHERE id='$imageId'";

						$db->query($sql);

						#
						# Move image file to new location
						#
						rename("$exports_dir/$fileName","$messages_dir/$fileName");

						#
						# If a file was already uploaded into this image slot, delete it.
						#
						if ($prevFile != "") {
							unlink("$messages_dir/$prevFile");
						}
					}
				}
			}
		}

		#
		# Extract API parameters from JSON data into $cmdParam1/$cmdParam2 if they exist.
		#
		if (($cmdParam1 == "") && isset($json_data['routineApiParams']['apiParam1'])) {
			$cmdParam1 = $json_data['routineApiParams']['apiParam1'];
		}

		if (($cmdParam2 == "") && isset($json_data['routineApiParams']['apiParam2'])) {
			$cmdParam2 = $json_data['routineApiParams']['apiParam2'];
		}

		#
		# Remove JSON file, we're finished using it.
		#
		if (!$debug) {
			if (strpos($json_file, '/var/www/html/laravel/public/exports/') == 0) {
				unlink($json_file);
			}
		}
	}
}

#
# If a CAP XML file is specified on command line, read the file and parse the CAP message
# into a CAP object for future use.
#
if ($cap_xml_file != "") {
	if (file_exists($cap_xml_file)) {

		$cap_xml = file_get_contents($cap_xml_file);

		try {
			$cap_data = new CAPParser($cap_xml);

			if ($debug) {
				print_r($cap_data);
			}
		} catch (Exception $e) {
			echo "Error: " . $e->getMessage();
		}
	}
}

# Need to save original caller for use in intercomWait() and ringWait()
$origCaller = $caller;

if ($trigger == "incoming-call") {
	$callerName = $cmdParam2;
}
else
if ($trigger == "pbx-incoming-call") {
	$callerName = base64_decode($extra_args[1]);
}
else
if ($caller != "") {
	if (($caller == 1) && ($cmdParam2 != "")) {
		$callerName = $cmdParam2;
	} else {
		$callerName = getStationName($debug, $db, $caller);
	}
}

if ($called != "") {
	$calledName = getStationName($debug, $db, $called);

	if (($calledName == "Unknown") && ($trigger == "pbx-incoming-call")) {
		$calledName = getNyquistFeature($debug, $db, "$called");
	}
}

#
# Retrieve Routine...
#

$sql = "SELECT enabled,name,allow_multiple,extension,use_callers_ext,allow_api,dtmf_code
	FROM routine WHERE id='$routineId'";

if (!$result = $db->query($sql)) {
	if ($debug)
		die('Query error ['.$db->error.']');
	else
		doExit(1);
}

if ($result->num_rows <= 0) {
	if ($debug) echo "Routine $routineId not found\n";

	$db->close();

	doExit(1);
}

$row = $result->fetch_assoc();

$routine_dtmf_code = $row['dtmf_code'];

$routineName = $row['name'];

#
# Make sure Routine is enabled,
# and if the $trigger is API, make sure we're allowed to execute routine via API
#
if (($row['enabled'] == 0) || (($trigger == "API") && ($row['allow_api'] == 0))) {
	if ($amiSocket) closeAmiConnection($debug);

	$db->close();

	$viaTrigger = ($trigger != "") ? $trigger : "DTMF/System";

	$reason = ($row['enabled'] == 0) ? "disabled" : "API not allowed";

	logText("Not started - $reason ($viaTrigger)", ROUTINE_LOG_INTERNAL);

	exit(1);
}

if ($row['allow_multiple'] == 0) {
	$sql = "SELECT id,pid FROM routine_task WHERE routine_id='$routineId'";

	if (!$result2 = $db->query($sql)) {
		if ($debug)
			die('Query error - routine_task ['.$db->error.']');
		else
			doExit(1);
	}

	if ($result2->num_rows > 0) {
	   while ($row2 = $result2->fetch_assoc()) {
		$taskId = $row2['id'];
		$taskPid = $row2['pid'];

		if (posix_getpgid($taskPid) != FALSE) {
			$cmdline = trim(file_get_contents("/proc/$taskPid/cmdline"));

			$cmdline = preg_replace("/[^a-z0-9\-]/", "", $cmdline);

			if (strpos($cmdline,"routine-execute-r$routineId") !== false) {
				if ($debug) echo "Not executing routine $routineId\n";

				if ($amiSocket) closeAmiConnection($debug);

				$db->close();

				$viaTrigger = ($trigger != "") ? $trigger : "DTMF/System";

				logText("Not started - already running ($viaTrigger)", ROUTINE_LOG_INTERNAL);

				exit(1);
			} else {
				$db->query("DELETE FROM routine_task WHERE id='$taskId'");
			}
		} else {
			$db->query("DELETE FROM routine_task WHERE id='$taskId'");
		}
	   }
	}
}

$viaTrigger = ($trigger != "") ? $trigger : "DTMF/System";

logText("Started ($viaTrigger)", ROUTINE_LOG_INTERNAL);

updateTaskList($debug, $db, $routineId, "ADD", $myPid);

if ($row['use_callers_ext'] != 1)
{
	$caller = $row['extension'];
}

startRoutineActions:

if ($gotoActionName == "REPEAT_ROUTINE")
{
	$gotoActionName = "";
}

if ($db == NULL) {
	$db = new mysqli($server, $user, $pass, $database);

	if ($db->connect_errno > 0) {
		if ($debug) {
			die('Unable to connect to database ['.$db->connect_error.']');
		} else {
			doExit(1);
		}
	}
}

#
# Retrieve list of actions to execute
#
if ($gotoActionName != "") {
	$sql = "SELECT id,name,type,finish_delay,param1,param2,param3,param4,action_condition";
} else {
	$sql = "SELECT id,type,finish_delay,param1,param2,param3,param4,action_condition";
}

$sql.= " FROM routine_action WHERE routine_id='$routineId' AND enabled='1' ORDER BY execute_order";

if ($skipToActionNum != 0) {
	$sql.= " LIMIT $skipToActionNum,999999999";

	$skipToActionNum = 0;
}

$tries = 0;

while ($tries++ < 120) {
	if (!$result = $db->query($sql)) {
		#
		# The only possible error is disconnected from MySQL server,
		# try to re-connect to MySQL server.
		#
		$db = new mysqli($server, $user, $pass, $database);

		if ($db->connect_errno > 0) {
			sleep(1);
		} else {
			logText("Reopened MySQL connection", ROUTINE_LOG_INTERNAL);
			$tries = 0;
		}
	} else {
		$tries = QUERY_SUCCESSFUL;
		break;
	}
}

#
# Exit if re-connect to MySQL server failed.
#
if ($tries != QUERY_SUCCESSFUL) {
	logText("Terminated - MySQL failure", ROUTINE_LOG_INTERNAL);
	if ($debug)
		die('MySQL connection failed');
	else
		doExit(1);
}

if ($result->num_rows <= 0) {
	updateTaskList($debug, $db, $routineId, "DELETE", $myPid);

	$db->close();

	$viaTrigger = ($trigger != "") ? $trigger : "DTMF/System";

	logText("Finished - no actions ($viaTrigger)", ROUTINE_LOG_INTERNAL);

	exit(1);
}

$numActions = $result->num_rows;

$numActionsExecuted = 0;;

#
# Execute actions...
#
while ($row = $result->fetch_assoc())
{
	$auto_repeat = 0;

	$routineActionId = $row['id'];

	#
	# If user requested to goto a specific Action via $gotoActionName,
	# skip actions until we find the specified Action.
	#
	if ($gotoActionName != "") {
		$routineActionName = $row['name'];

		if ($gotoActionName != $routineActionName) {
			$numActionsExecuted++;
			continue;
		}

		$gotoActionName = "";
	}

	$action = $row['type'];
	$delay  = $row['finish_delay'];
	$param1 = $row['param1'];
	$param2 = $row['param2'];
	$param3 = $row['param3'];
	$param4 = $row['param4'];
	$condition = $row['action_condition'];

	$delay = ($delay == "no-delay") ? "&" : "";

	if ($debug) echo "Action: $action P1: $param1 P2: $param2 P3: $param3 P4: $param4\n";

   doAutoRepeat:

	$pDate1 = date("Y-m-d");
	$pDate2 = date("m-d-Y");
	$pDate3 = date("d-m-Y");
	$pTime1 = date("h:i:s A");
	$pTime2 = date("H:i:s");

	pcntl_signal_dispatch();

	check_db_connection();

	executeAction($debug, $routineActionId, $caller, $origCaller, $action, $param1, $param2, $param3, $param4, $condition, $delay);

	if ($skipToActionNum != 0) {
		$result->free();
		goto startRoutineActions;
	}

	if ($gotoActionName != "") {
		$result->free();
		goto startRoutineActions;
	}

	$numActionsExecuted++;

	pcntl_signal_dispatch();

	if ($auto_repeat) {
		unset($pDate1);
		unset($pDate2);
		unset($pDate3);
		unset($pTime1);
		unset($pTime2);

		gc_collect_cycles();

		sleep($auto_repeat);

		pcntl_signal_dispatch();

		if (!stillEnabled($debug)) {
			$viaTrigger = ($trigger != "") ? $trigger : "DTMF/System";
			logText("Stopped - disabled ($viaTrigger)", ROUTINE_LOG_INTERNAL);
			doExit(1);
		}

		goto doAutoRepeat;
	}
}

if ($amiSocket) closeAmiConnection($debug);

if ($db != NULL) {
	updateTaskList($debug, $db, $routineId, "DELETE", $myPid);
	$db->close();
}

#
# If $announceFilename is not empty, we need to delete an announcement that was
# created but not played.
#
if ($announceFilename != "") {
	exec("/usr/local/bin/routine-rm-announcement $announceFilename > /dev/null 2>&1");
}

#
# Delete any unused files sent via JSON-based API
#
if ($json_files_id != "") {
	$json_files_id = str_replace(' ','',$json_files_id);
	$json_files_id = str_replace('*','',$json_files_id);
	$edir = "/var/www/html/laravel/public/exports";
	exec("rm -f $edir/*_$json_files_id.* >/dev/null 2>&1");
}

$viaTrigger = ($trigger != "") ? $trigger : "DTMF/System";

logText("Finished ($viaTrigger)", ROUTINE_LOG_INTERNAL);

exit(0);

###############
# END OF MAIN #
###############

function updateTaskList($debug, $db, $routineId, $sqlOp, $myPid)
{
	global $addedPid;

	if ($debug) echo "updateTaskList($routineId,$sqlOp,$myPid)\n";

	switch ($sqlOp)
	{
		case "ADD":

			$sql = "INSERT INTO routine_task
				SET routine_id='$routineId', pid='$myPid'";

			$addedPid = TRUE;

			break;

		case "DELETE":

			if ($addedPid == FALSE) return;

			$sql = "DELETE FROM routine_task
				WHERE routine_id='$routineId' AND pid='$myPid'";
			break;
	}

	$db->query($sql);
}

function executeAction($debug, $routineActionId, $caller, $origCaller, $action, $param1, $param2, $param3, $param4, $condition, $delay)
{
	global	$amiSocket,
		$auto_repeat,
		$alert_announce_played_count,
		$alert_announce_played_total_count,
		$called,
		$cap_data,
		$cmdParam1,
		$cmdParam2,
		$callerName,
		$calledName,
		$cputemp,
		$dashId,
		$db,
		$displayMsgId,
		$extra_args,
		$json_data,
		$has_disable_audio,
		$has_enable_audio,
		$has_program_distribution,
		$has_manage_output_contacts,
		$gotoActionName,
		$ga10pvExt,
		$mergeDelete,
		$myPid,
		$numActions,
		$numActionsExecuted,
		$pDate1,
		$pDate2,
		$pDate3,
		$pTime1,
		$pTime2,
		$post_dash_phone_text,
		$previous_alert_str,
		$routineId,
		$routine_alert_executed_count,
		$routine_noalert_executed_count,
		$routine_alert_once,
		$routine_noalert_once,
		$skipToActionNum,
		$trigger,
		$user_var,
		$webhook_return_str,
		$zonesProvided,
		$zones;

	if ($debug) echo "executeAction($caller, $origCaller, $action, $param1, $param2, $param3, $param4, $delay)\n";

	$cputemp = "";
	$ga10pvExt = "";
	$mergeDelete = FALSE;

	if ($condition != "") {
		if (!isActionConditionTrue($debug, $db, $condition, $trigger, $action,
					   $caller, $origCaller, $callerName, $called, $calledName,
					   $cmdParam1, $cmdParam2, $pTime2, $zones, $zonesProvided))
		{
			return;
		}
	}

	switch ($action)
	{
		case '911':
			if (($trigger == "API") || ($trigger == "scheduled-routine")) return;

			executeAmiCall($debug, $action, $caller, "911", "0", 86400, $delay);
			break;

		case 'add-announcement-audio':
			addAnnouncementAudio($debug, $trigger, $cmdParam1, $cmdParam2, $param1, $param2, $param3, $param4, $routineId, $myPid);
			break;

		case 'alarm':
			$play_alarm = true;

			$waitTime = ($param3 != "") ? $param3 : 86400;

			if ($json_data != null) {
				if (isset($json_data['alarmParams']) && is_array($json_data['alarmParams'])) {

					$actionName = getActionName($debug, $db, $routineActionId);

					foreach ($json_data['alarmParams'] as $alarmParameters) {
						if ($alarmParameters['name'] === $actionName) {
							$alarmParams = $alarmParameters;

							if (isset($alarmParams['alarmDtmf'])) {
								$new_value = $alarmParams['alarmDtmf'];
								if (!hasCommandInjection($new_value)) {
									if (toneDtmfExists($debug, $db, $new_value)) {
										$param1 = $new_value;
									} else {
										logText("Alarm: Invalid DTMF: $new_value", ROUTINE_LOG_INTERNAL);
										$play_alarm = false;
									}
								} else {
									logText("Alarm: Invalid DTMF: $new_value", ROUTINE_LOG_INTERNAL);
									$play_alarm = false;
								}
							}
							if (isset($alarmParams['alarmName'])) {
								$new_value = $alarmParams['alarmName'];
								if (!hasCommandInjection($new_value)) {
									if (toneNameExists($debug, $db, $new_value)) {
										$new_dtmf = getToneDtmfByName($debug, $db, $new_value);
										logText("Alarm: new_dtmf = $new_dtmf");
										if ($new_dtmf != 0) {
											$param1 = $new_dtmf;
										} else {
											logText("Alarm: Invalid Name: $new_value", ROUTINE_LOG_INTERNAL);
											$play_alarm = false;
										}
									} else {
										logText("Alarm: Invalid Name: $new_value", ROUTINE_LOG_INTERNAL);
										$play_alarm = false;
									}
								} else {
									logText("Alarm: Invalid Name: $new_value", ROUTINE_LOG_INTERNAL);
									$play_alarm = false;
								}
							}
							if (isset($alarmParams['duration'])) {
								$new_value = $alarmParams['duration'];
								if (is_numeric($new_value) && $new_value > 0 && $new_value < 999999) {
									$waitTime = $new_value;
								} else {
									logText("Alarm: Invalid duration: $new_value", ROUTINE_LOG_INTERNAL);
									$play_alarm = false;
								}
							}
						}
					}
				}
			}

			if ($play_alarm) {
				executeAmiCall($debug, $action, $caller, "0000091$param1", "0", $waitTime, $delay);
			}
			break;

		case 'all-call':
			if (($trigger == "API") || ($trigger == "scheduled-routine")) return;

			echo "#0";
			doExit(0);
			break;

		case 'announcement':
			#
			# $param1 = announcement.id, $param2 = Zones
			#
			$waitTime = ($param3 != "") ? $param3 : 86400;

			$play_announcement = true;

			if ($json_data != null) {
				if (isset($json_data['announcementParams']) && is_array($json_data['announcementParams'])) {

					$actionName = getActionName($debug, $db, $routineActionId);

					foreach ($json_data['announcementParams'] as $announcementParameters) {
						if ($announcementParameters['name'] === $actionName) {
							$announcementParams = $announcementParameters;

							if (isset($announcementParams['announcementDtmf'])) {
								$new_value = $announcementParams['announcementDtmf'];
								if (!hasCommandInjection($new_value)) {
									$param1 = getAnnouncementId($debug, $db, $new_value);
									if ($param1 == "") {
										logText("Announcement: Invalid DTMF: $new_value", ROUTINE_LOG_INTERNAL);
										$play_announcement = false;
									}
								} else {
									logText("Announcement: Invalid DTMF: $new_value", ROUTINE_LOG_INTERNAL);
									$play_announcement = false;
								}
							}
							if (isset($announcementParams['announcementName'])) {
								$new_value = $announcementParams['announcementName'];
								if (!hasCommandInjection($new_value)) {
									if (announcementNameExists($debug, $db, $new_value)) {
										$new_dtmf = getAnnouncementDtmfByName($debug, $db, $new_value);
										logText("Announcement: new_dtmf = $new_dtmf");
										if ($new_dtmf != 0) {
											$param1 = $new_dtmf;
										} else {
											logText("Announcement: Invalid Name: $new_value", ROUTINE_LOG_INTERNAL);
											$play_announcement = false;
										}
									} else {
										logText("Announcement: Invalid Name: $new_value", ROUTINE_LOG_INTERNAL);
										$play_announcement = false;
									}
								} else {
									logText("Announcement: Invalid Name: $new_value", ROUTINE_LOG_INTERNAL);
									$play_announcement = false;
								}
							}
							if (isset($announcementParams['announcementZone'])) {
								$new_value = $announcementParams['announcementZone'];
								if (!hasCommandInjection($new_value)) {
									# make sure zone type includes Time type (1,3,5,6)
									$zone_type = getZoneType($debug,$db,$new_value);
									$allowed_zone_types = [1,3,5,6];
									if (in_array($zone_type, $allowed_zone_types) || ($new_value == 0)) {
										$param2 = $new_value;
									} else {
										logText("Announcement: Invalid Zone: $new_value", ROUTINE_LOG_INTERNAL);
										$play_announcement = false;
									}
								} else {
									logText("Announcement: Invalid Zone: $new_value", ROUTINE_LOG_INTERNAL);
									$play_announcement = false;
								}
							}
							if (isset($announcementParams['duration'])) {
								$new_value = $announcementParams['duration'];
								if (is_numeric($new_value) && $new_value > 0 && $new_value < 999999) {
									$waitTime = $new_value;
								} else {
									logText("Announcement: Invalid duration: $new_value", ROUTINE_LOG_INTERNAL);
									$play_announcement = false;
								}
							}
						}
					}
				}
			}
			if ($play_announcement) {
				executeAmiCall($debug, $action, $caller, "0000092$param1*$param2", $param2, $waitTime, $delay);
			}
			break;

		case 'audio-dist-start':
			getCosParams($debug, $db, $origCaller);

			if ($has_program_distribution == 0) {
				if ($origCaller == $caller) return;

				getCosParams($debug, $db, $caller);

				if ($has_program_distribution == 0) {
					return;
				}
			}

			$audio_status = file_get_contents("/srv/tftp/audio_status");
			$audio_status = trim($audio_status);

			if ($audio_status != "disable")
			{
			$lineInputData = getLineInputData($debug, $db, $param1, "start");

			if (count($lineInputData) > 0) {

				#
				# Check for zone overlap before starting line input stream
				#
				exec("/usr/local/bin/input-control checkoverlap2 $param1", $out, $ret);

				if ($debug) print_r($out);

				if ($out[0] == "0") {
					$sql = "UPDATE prog_dist
						SET command='play'
						WHERE id='$param1'";

					$db->query($sql);

					exec("/usr/local/bin/input-control start2 $param1 $lineInputData[0] $lineInputData[1] > /dev/null 2>&1 $delay");
				}
			} else {
				exec("/usr/local/bin/pd-control startPdist-Routine-$routineId $param1 > /dev/null 2>&1 $delay");
			}
			unset($lineInputData);
			}
			break;

		case 'audio-dist-stop':
			getCosParams($debug, $db, $origCaller);

			if ($has_program_distribution == 0) {
				if ($origCaller == $caller) return;

				getCosParams($debug, $db, $caller);

				if ($has_program_distribution == 0)
				{
					return;
				}
			}

			if ($param1 == 0) {
				stopAllPdist($debug, $db, $routineId, $delay);
			} else {
				$lineInputData = getLineInputData($debug, $db, $param1, "stop");

				if (count($lineInputData) > 0) {
					exec("/usr/local/bin/input-control stop2 $param1 > /dev/null 2>&1 $delay");
				} else {
					exec("/usr/local/bin/pd-control stopPdist-Routine-$routineId $param1 > /dev/null 2>&1 $delay");
				}
				unset($lineInputData);
			}
			break;

		case 'call-and-announce':

			callAndAnnounce($debug, $caller, $param1, $param2, $param3);

			break;

		case 'change-setting':

			changeSetting($debug, $db, $caller, $trigger, $cmdParam1, $cmdParam2, $param1, $param2, $param3, $delay);
			break;

		case 'change-volume':
			#
			# $param1 = Volume Type:
			#    (1=All-Call, 2=Audio Distribution, 3=Emergency-All-Call, 4=Zone-Paging)
			# $param2 = Zone Number (0 = All Speakers)
			# $param3 = New Volume Cut Level
			#

			changeVolume($debug, $db, $caller, $trigger, $cmdParam1, $cmdParam2, $param1, $param2, $param3, $delay);
			break;

		case 'change-dsc01-setting':

			changeWbcSetting($debug, $db, $caller, $trigger, $cmdParam1, $cmdParam2, $param1, $param2, $param3, $delay, 73);
			break;

		case 'change-wbc-setting':

			changeWbcSetting($debug, $db, $caller, $trigger, $cmdParam1, $cmdParam2, $param1, $param2, $param3, $delay, 71);
			break;

		case 'check-in':

			switch($param1)
			{
				case 'station':
					$cmd = "-C \"$param2\"";
					break;

				case 'done':
					$cmd = "-d";
					break;

				case 'exclude-add':
					$cmd = "-X \"$param2\"";
					break;

				case 'exclude-delete':
					$cmd = "-x \"$param2\"";
					break;

				case 'finish':
					$cmd = "-f \"$caller\"";
					break;

				case 'vacancy-add':
					$cmd = "-V \"$param2\"";
					break;

				case 'vacancy-delete':
					$cmd = "-v \"$param2\"";
					break;

				case 'reset':
					$cmd = "-r \"$caller\"";
					break;

				case 'start':
					$cmd = "-S \"$caller\"";
					break;

				case 'stop':
					$cmd = "-s \"$caller\"";
					break;

				case 'wait':
					$delay = "";
					$cmd = "-w";
					break;

				default:
					$cmd = "";
					break;
			}

			exec("/usr/local/bin/check-in -e $caller $cmd > /dev/null 2>&1 $delay");
			break;

		case 'check-intercom':
			if (checkIntercom($debug, $db, $origCaller, $called, 1) == FALSE) {
				doExit(0);
			}
			break;

		case 'dash-delete':
			deleteDashMsg($debug, $db, $origCaller, $called, $param1, $param2);
			break;

		case 'dash-text':
			displayDashText($debug, $db, $origCaller, $called, $param1, $param2, $param3, $param4);
			break;

		case 'disable-audio':
			getCosParams($debug, $db, $origCaller);

			if ($has_disable_audio == 0) {
				if ($origCaller == $caller) return;

				getCosParams($debug, $db, $caller);

				if ($has_disable_audio == 0) {
					return;
				}
			}

			executeAmiCall($debug, $action, $caller, "0000097", "0", 10, $delay);
			break;

		case 'display-msg':

			if ($auto_repeat) {
				if ($amiSocket) {
					closeAmiConnection($debug);

					$amiSocket = FALSE;
				}

				$deleteId = array_pop($displayMsgId);

				if ($deleteId !== null) {
					exec("/usr/local/bin/display-message -r '$deleteId' > /dev/null 2>&1");
				}
			}

			$dzones = "";
			$dstations = "";

			$dzones = "-z \"$param1\"";

			if ($param3 != "") {
				$param3 = str_replace('$called',$called,$param3);
				$param3 = str_replace('$caller',$caller,$param3);

				$dstations = "-s \"$param3\"";
			}

			if ($ga10pvExt != "") {
				if (is_numeric($ga10pvExt)) {
					$dzones = "-z \"\"";
					$dstations = "-s \"$ga10pvExt\"";
				}
			}

			$duration = 86500;
			$priority = 0;
			$text = "Message not defined";

			$sBgColor = "#96ff73";
			$sFont = "Arial";
			$sFontStyle = "";
			$sFontSize = 70;
			$sFontColor = "#000000";
			$sMsgType = "text";
			$sImageType = "full";
			$sImageId = "0";
			$sImagePriority = "text";
			$wbcStationTypeFilter = "0";
			$wbcFlasherPattern = "no-change";
			$wbcFlasherCycles = "0";
			$wbcFlasherColor = "blue";

			if ($param4 != "") {
				#
				# Note: $param4 also contains NQ-S1810WBC-related parameters.
				#
				$styleParams = json_decode($param4,true);

				if ($debug) print_r($styleParams);

				$duration = $styleParams[0];

				$priority = $styleParams[1];

				$text = $styleParams[2];
				$text = addslashes($text);

				$sBgColor   = $styleParams[3];
				$sFont      = $styleParams[4];
				$sFontStyle = $styleParams[5];
				$sFontSize  = $styleParams[6];
				$sFontColor = $styleParams[7];

				if (count($styleParams) > 8) {
					$sMsgType   = $styleParams[8];
					if ($sMsgType == "json-uploaded-image") {
						$sMsgType = "image";
					}
					$sImageType = $styleParams[9];
					$sImageId   = $styleParams[10];
					$sImagePriority = $styleParams[11];
				}

				if (count($styleParams) > 12) {
					$wbcStationTypeFilter = $styleParams[12];
					$wbcFlasherPattern = $styleParams[13];
					$wbcFlasherCycles =  $styleParams[14];
					$wbcFlasherColor =  $styleParams[15];
				}

				unset($styleParams);
				unset($wbcParams);
			}

			#
			# Process dispaly message parameter overrides supplied via JSON API:
			#
			# Example:
			#	"displayMsgParams": [
			#		{
			#		"name":"WBC Message-01",
			#		"zones":"10",
			#		"stations":"",
			#		"priority":"4",
			#		"displayTime":"60",
			#		"wbcFlasherPattern":"fast",
			#		"wbcFlasherCycles":"10",
			#		"wbcFlasherColor":"blue"
			#		}
			#	]

			if ($json_data != null) {
				if (isset($json_data['displayMsgParams']) && is_array($json_data['displayMsgParams'])) {

					$actionName = getActionName($debug, $db, $routineActionId);

					foreach ($json_data['displayMsgParams'] as $displayMsgParameters) {
						if ($displayMsgParameters['name'] === $actionName) {
							$displayMsgParams = $displayMsgParameters;

							if (isset($displayMsgParams['zones'])) {
								$new_value = $displayMsgParams['zones'];
								if (!hasCommandInjection($new_value)) {
									$dzones = "-z \"$new_value\"";
								} else {
									logText("Display-Msg: Invalid zones: $new_value", ROUTINE_LOG_INTERNAL);
								}
							}
							if (isset($displayMsgParams['stations'])) {
								$new_value = $displayMsgParams['stations'];
								if (!hasCommandInjection($new_value)) {
									$dstations = "-s \"$new_value\"";
								} else {
									logText("Display-Msg: Invalid stations: $new_value", ROUTINE_LOG_INTERNAL);
								}
							}
							if (isset($displayMsgParams['deviceTypeFilter'])) {
								$new_value = $displayMsgParams['deviceTypeFilter'];
								if (is_numeric($new_value) && $new_value >= 0 && $new_value < 3) {
									$wbcStationTypeFilter = $new_value;
								} else {
									logText("Display-Msg: Invalid deviceTypeFilter: $new_value", ROUTINE_LOG_INTERNAL);
								}
							}
							if (isset($displayMsgParams['text'])) {
								$new_value = $displayMsgParams['text'];
								$text = addslashes($new_value);
							}
							if (isset($displayMsgParams['priority'])) {
								$new_value = $displayMsgParams['priority'];
								if (is_numeric($new_value) && $new_value >= 0 && $new_value < 7) {
									$priority = $new_value;
								} else {
									logText("Display-Msg: Invalid priority: $new_value", ROUTINE_LOG_INTERNAL);
								}
							}
							if (isset($displayMsgParams['displayTime'])) {
								$new_value = $displayMsgParams['displayTime'];
								if (is_numeric($new_value) && $new_value >= 0 && $new_value < 99999) {
									$duration = $new_value;
								} else {
									logText("Display-Msg: Invalid displayTime: $new_value", ROUTINE_LOG_INTERNAL);
								}
							}
							if (isset($displayMsgParams['imageType'])) {
								$new_value = $displayMsgParams['imageType'];
								$new_value = strtolower($new_value);
								$allowed_types = ['full','left','right'];
								if (in_array($new_value, $allowed_types)) {
									$sImageType = $new_value;
								} else {
									logText("Display-Msg: Invalid imageType: $new_value", ROUTINE_LOG_INTERNAL);
								}
							}
							if (isset($displayMsgParams['imagePriority'])) {
								$new_value = $displayMsgParams['imagePriority'];
								$new_value = strtolower($new_value);
								$allowed_types = ['text','image'];
								if (in_array($new_value, $allowed_types)) {
									$sImagePriority = $new_value;
								} else {
									logText("Display-Msg: Invalid imagePriority: $new_value", ROUTINE_LOG_INTERNAL);
								}
							}
							if (isset($displayMsgParams['wbcFlasherPattern'])) {
								$allowed_values = ['no-change','off','slow','fast','double','triple','quad'];
								$new_value = $displayMsgParams['wbcFlasherPattern'];
								$new_value = strtolower($new_value);
								if (in_array($new_value, $allowed_values)) {
									$wbcFlasherPattern = $new_value;
								} else {
									logText("Display-Msg: Invalid wbcFlasherPattern: $new_value", ROUTINE_LOG_INTERNAL);
								}
							}
							if (isset($displayMsgParams['wbcFlasherCycles'])) {
								$new_value = $displayMsgParams['wbcFlasherCycles'];
								if (is_numeric($new_value) && $new_value >= 0 && $new_value < 99999) {
									$wbcFlasherCycles = $new_value;
								} else {
									logText("Display-Msg: Invalid wbcFlasherCycles: $new_value", ROUTINE_LOG_INTERNAL);
								}
							}
							if (isset($displayMsgParams['wbcFlasherColor'])) {
								$allowed_colors = ['amber', 'blue', 'green', 'orange', 'red', 'violet', 'white', 'yellow'];
								$new_value = $displayMsgParams['wbcFlasherColor'];
								$new_value = strtolower($new_value);

								if (in_array($new_value, $allowed_colors)) {
									$wbcFlasherColor = $new_value;
								} else {
									logText("Display-Msg: Invalid wbcFlasherColor: $new_value", ROUTINE_LOG_INTERNAL);
								}
							}
							break;
						}
					}
				}
			}

			$styleParams = "-1 \"$sBgColor\" -2 \"$sFont\" -3 \"$sFontStyle\" -4 \"$sFontSize\" -5 \"$sFontColor\" -6 \"$sMsgType\" -7 \"$sImageType\" -8 \"$sImageId\" -9 \"$sImagePriority\"";

			$wbcParams = "-f $wbcStationTypeFilter -F \"$wbcFlasherPattern:$wbcFlasherCycles:$wbcFlasherColor\"";

			$idText = ($param2 != "") ? "-i \"$param2\"" : "";
			$idText = str_replace('$caller',$origCaller,$idText);
			$idText = str_replace('$called',$called,$idText);

			#
			# Replace user supplied variables with current values
			#
			switch ($trigger)
			{
				case 'API':
					if ($json_data != null) {
						for ($i = 20; $i > 0; $i--) {
							$paramKey = 'apiParam'.$i;
							if (isset($json_data['routineApiParams'][$paramKey])) {
								$placeholder = '$'.$paramKey;
								$text = str_replace($placeholder, $json_data['routineApiParams'][$paramKey], $text);
								$idText = str_replace($placeholder, $json_data['routineApiParams'][$paramKey], $idText);
							}
						}
					}
					$text = str_replace('$apiParam1',$cmdParam1,$text);
					$text = str_replace('$apiParam2',$cmdParam2,$text);
					$idText = str_replace('$apiParam1',$cmdParam1,$idText);
					$idText = str_replace('$apiParam2',$cmdParam2,$idText);

					#
					# Process CAP related variables
					#
					$text = replaceCapData($text);
					$idText = replaceCapData($idText);

					break;

				case 'input-contact-closed':
				case 'input-contact-opened':
					$text = str_replace('$contact',$cmdParam2,$text);
					$text = str_replace('$station',$cmdParam1,$text);
					$idText = str_replace('$contact',$cmdParam2,$idText);
					$idText = str_replace('$station',$cmdParam1,$idText);
					break;

				case 'emergency-call':
					$text = str_replace('$calltypechar','[E]',$text);
					$text = str_replace('$calltypeshort','[Emerg]',$text);
					$text = str_replace('$calltypelong','[Emergency]',$text);

					if (strpos($text, '$autobgcolor') !== false) {
						$text = str_replace(' $autobgcolor','',$text);
						$text = str_replace('$autobgcolor','',$text);
						$sBgColor = "rgb(255,0,0)";
						$styleParams = "-1 \"$sBgColor\" -2 \"$sFont\" -3 \"$sFontStyle\" -4 \"$sFontSize\" -5 \"$sFontColor\" -6 \"$sMsgType\" -7 \"$sImageType\" -8 \"$sImageId\" -9 \"$sImagePriority\"";
					}

					if (strpos($text, '$autofontcolor') !== false) {
						$text = str_replace(' $autofontcolor','',$text);
						$text = str_replace('$autofontcolor','',$text);
						$sFontColor = "rgb(255,0,0)";
						$styleParams = "-1 \"$sBgColor\" -2 \"$sFont\" -3 \"$sFontStyle\" -4 \"$sFontSize\" -5 \"$sFontColor\" -6 \"$sMsgType\" -7 \"$sImageType\" -8 \"$sImageId\" -9 \"$sImagePriority\"";
					}

					if (strpos($text, '$autopriority') !== false) {
						if (preg_match("/(.*)autopriority\((.*),(.*),(.*)\)(.*)/",$text,$parts)) {
							if ($debug) print_r($parts);
							$text = preg_replace('/\$autopriority\((.*),(.*),(.*)\)/','',$text);
							$priority = $parts[2];

							unset($parts);
						} else {
							$priority = 4;
							$text = str_replace('$autopriority','',$text);
						}
					}
					break;

				case 'facility-status-up':
				case 'facility-status-down':
				case 'facility-page':
					$fname = getFacilityName($debug, $db, $cmdParam1);
					$text = str_replace('$facility',$fname,$text);
					$idText = str_replace('$facility',$fname,$idText);
					break;

				case 'intercom-call':
					$removeVars = array(
					' $calltypechar',
					' $calltypeshort',
					' $calltypelong',
					'$calltypechar',
					'$calltypeshort',
					'$calltypelong',
					' $autobgcolor',
					'$autobgcolor',
					' $autofontcolor',
					'$autofontcolor');

					$text = str_replace($removeVars,'',$text);

					if (strpos($text, '$autopriority') !== false) {
						if (preg_match("/(.*)autopriority\((.*),(.*),(.*)\)(.*)/",$text,$parts)) {
							if ($debug) print_r($parts);
							$text = preg_replace('/\$autopriority\((.*),(.*),(.*)\)/','',$text);
							$priority = $parts[4];

							unset($parts);
						} else {
							$priority = 2;
							$text = str_replace('$autopriority','',$text);
						}
					}
					break;

				case 'amp-protection-mode':
					$devName = getDeviceNameByMacAddr($cmdParam1);
					$text = str_replace('$deviceMacAddr',$cmdParam1,$text);
					$text = str_replace('$deviceName',$devName,$text);

					$idText = str_replace('$deviceMacAddr',$cmdParam1,$idText);
					break;

				case 'nws-alert':
					$text = str_replace('$nwsAlertEvent', $cmdParam1,$text);
					$text = str_replace('$nwsAlertSeverity', $cmdParam2,$text);
					$text = str_replace('$nwsAlertCertainty', $extra_args[1],$text);
					$text = str_replace('$nwsAlertUrgency', $extra_args[2],$text);
					$text = str_replace('$nwsAlertResponse', $extra_args[3],$text);
					$text = str_replace('$nwsAlertZone', $extra_args[4],$text);

					$idText = str_replace('$nwsAlertEvent', $cmdParam1,$idText);
					$idText = str_replace('$nwsAlertSeverity', $cmdParam2,$idText);
					$idText = str_replace('$nwsAlertCertainty', $extra_args[1],$idText);
					$idText = str_replace('$nwsAlertUrgency', $extra_args[2],$idText);
					$idText = str_replace('$nwsAlertResponse', $extra_args[3],$idText);
					$idText = str_replace('$nwsAlertZone', $extra_args[4],$idText);
					break;

				case 'schedule-activated':
					$sname = getScheduleName($debug, $db, $cmdParam1);
					$text = str_replace('$schedulename',$sname,$text);
					$idText = str_replace('$scheduleid',$cmdParam1,$idText);
					break;

				case 'scheduled-routine':
					$text = str_replace('$schedParam1',$cmdParam1,$text);
					$text = str_replace('$schedParam2',$cmdParam2,$text);
					$idText = str_replace('$schedParam1',$cmdParam1,$idText);
					$idText = str_replace('$schedParam2',$cmdParam2,$idText);

					if ($zonesProvided) {
						$zname = getZoneName($debug, $db, $zones);
						$text = str_replace('$zone',$zname,$text);
						$idText = str_replace('$zone',$zname,$idText);
					}
					break;

				case 'sched-event':
					$ename = getEventName($debug, $db, $cmdParam1);
					$text = str_replace('$eventname',$ename,$text);
					$text = str_replace('$eventid',$cmdParam1,$text);
					$idText = str_replace('$eventid',$cmdParam1,$idText);
					$idText = str_replace('$eventname',$ename,$idText);
					break;

				case 'scheduled-tone':
					$tname = getToneName($debug, $db, $cmdParam1);
					$text = str_replace('$tonename',$tname,$text);
					$text = str_replace('$toneid',$cmdParam1,$text);
					$idText = str_replace('$toneid',$cmdParam1,$idText);
					$idText = str_replace('$tonename',$tname,$idText);
					break;

				case 'station-status-up':
				case 'station-status-down':
					$text = str_replace('$station',$cmdParam1,$text);
					$idText = str_replace('$station',$cmdParam1,$idText);
					break;

				case 'urgent-call':
					$text = str_replace('$calltypechar','[U]',$text);
					$text = str_replace('$calltypeshort','[Urg]',$text);
					$text = str_replace('$calltypelong','[Urgent]',$text);

					if (strpos($text, '$autobgcolor') !== false) {
						$text = str_replace(' $autobgcolor','',$text);
						$text = str_replace('$autobgcolor','',$text);
						$sBgColor = "rgb(255,255,0)";
						$styleParams = "-1 \"$sBgColor\" -2 \"$sFont\" -3 \"$sFontStyle\" -4 \"$sFontSize\" -5 \"$sFontColor\" -6 \"$sMsgType\" -7 \"$sImageType\" -8 \"$sImageId\" -9 \"$sImagePriority\"";
					}

					if (strpos($text, '$autofontcolor') !== false) {
						$text = str_replace(' $autofontcolor','',$text);
						$text = str_replace('$autofontcolor','',$text);
						$sFontColor = "rgb(255,255,0)";
						$styleParams = "-1 \"$sBgColor\" -2 \"$sFont\" -3 \"$sFontStyle\" -4 \"$sFontSize\" -5 \"$sFontColor\" -6 \"$sMsgType\" -7 \"$sImageType\" -8 \"$sImageId\" -9 \"$sImagePriority\"";
					}

					if (strpos($text, '$autopriority') !== false) {
						if (preg_match("/(.*)autopriority\((.*),(.*),(.*)\)(.*)/",$text,$parts)) {
							if ($debug) print_r($parts);
							$text = preg_replace('/\$autopriority\((.*),(.*),(.*)\)/','',$text);
							$priority = $parts[3];

							unset($parts);
						} else {
							$priority = 3;
							$text = str_replace('$autopriority','',$text);
						}
					}
					break;

				case 'announcement':
				case 'zone-page':
					$zname = getZoneName($debug, $db, $zones);
					$text = str_replace('$zone',$zname,$text);
					$idText = str_replace('$zone',$zname,$idText);
					break;

				default:
					$text = str_replace('$contact','N/A',$text);
					$text = str_replace('$station','N/A',$text);
					$text = str_replace('$facility','N/A',$text);
					$text = str_replace('$zone','N/A',$text);
					$text = str_replace('$eventid','N/A',$text);
					$text = str_replace('$eventname','N/A',$text);

					$removeVars = array(
					' $calltypechar',
					' $calltypeshort',
					' $calltypelong',
					'$calltypechar',
					'$calltypeshort',
					'$calltypelong',
					' $autobgcolor',
					'$autobgcolor',
					' $autofontcolor',
					'$autofontcolor',
					'apiParam1',
					'apiParam2',
					'$deviceMacAddr',
					'$deviceName');

					$text = str_replace($removeVars,'',$text);

					if (strpos($text, '$autopriority') !== false) {
						if (preg_match("/(.*)autopriority\((.*),(.*),(.*)\)(.*)/",$text,$parts)) {
							$text = preg_replace('/ \$autopriority\((.*),(.*),(.*)\)/','',$text);

							unset($parts);
						} else {
							$text = str_replace('$autopriority','',$text);
						}
					}
					break;
			}

			if ($trigger == "incoming-call") {
				$text = str_replace('$caller',$cmdParam1,$text);
				$text = str_replace('$namecaller',$cmdParam2,$text);
			}
			else
			if ($trigger == "pbx-incoming-call") {
				$text = str_replace('$caller',$cmdParam1,$text);
				$text = str_replace('$namecaller',$callerName,$text);
			}
			else
			if ($origCaller == 0) {
				$text = str_replace('$caller','System',$text);
				$text = str_replace('$namecaller','System',$text);
			} else {
				if ($trigger != "incoming-call") {
					$text = str_replace('$caller',$origCaller,$text);
					$text = str_replace('$namecaller',$callerName,$text);
				} else {
					$text = str_replace('$caller',$cmdParam1,$text);
					$text = str_replace('$namecaller',$cmdParam2,$text);
				}
			}
			$text = str_replace('$called',$called,$text);
			$text = str_replace('$namecalled',$calledName,$text);
			$text = str_replace('$date1', $pDate1, $text);
			$text = str_replace('$date2', $pDate2, $text);
			$text = str_replace('$date3', $pDate3, $text);
			$text = str_replace('$time1', $pTime1, $text);
			$text = str_replace('$time2', $pTime2, $text);

			foreach ($user_var as $key => $u_var) {
				$user_var_str = '$user_var['.$key.']';

				$text = str_replace("$user_var_str",$user_var[$key],$text);
				$idText = str_replace("$user_var_str",$user_var[$key],$idText);
			}

			#
			# Process user supplied data from JSON file
			#
			if ($json_data != null) {
				$text = preg_replace_callback('/{{\s*(\w+(\.\w+)*)\s*}}/',
					   function($matches) use ($json_data) {
						$variable = $matches[1];
						$value = getNestedValue($json_data, $variable);
						return $value !== null ? $value : $matches[0];
					   }, $text);
			}

			if (strpos($text, '$cputemp') !== false) {
				if ($cputemp == "") $cputemp = getMaxCpuTemp();

				$text = str_replace('$cputemp', $cputemp, $text);
			}

			if (strpos($text, '$df-avail') !== false) {
				exec("/bin/df -h --output=avail /", $dfout, $ret);
				if ($ret == 0) {
					$text = str_replace('$df-avail',trim($dfout[1]),$text);
				}
				unset($dfout);
			}

			if (strpos($text, '$uptime') !== false) {
				exec("/usr/bin/uptime -p", $uptimeout, $ret);
				if ($ret == 0) {
					$text = str_replace('$uptime',$uptimeout[0],$text);
				}
				unset($uptimeout);
			}

			#
			# Process $alerts variable
			#
			# Formats:
			#
			#    $alerts(OPTIONS,...)
			#
			#    Valid OPTIONS:
			#	<valid-county-code> e.g. FLC095 (Mandatory)
			#	AUTO_COMPRESS_<integer>
			#	AUTO_FIND_COUNTY
			#	UPDATE_CACHE
			#	INCLUDE_CODES
			#	INCLUDE_COUNTY
			#	INCLUDE_INSTRUCTIONS
			#	REPEAT_<seconds>
			#	ANNOUNCEMENT_DTMF_<integer>
			#	ANNOUNCEMENT_ZONE_<integer>
			#	ANNOUNCEMENT_BY_EVENT
			#	ROUTINE_ALERT_DTMF_<integer>
			#	ROUTINE_ALERT_ONCE
			#	ROUTINE_NOALERT_DTMF_<integer>
			#	ROUTINE_NOALERT_ONCE
			#	PLAY_ONCE
			#	PLAY_SKIP_<integer>
			#	PLAY_STOP_<integer>
			#	POST_DASH_PHONE_TEXT
			#	ZONE_DISPLAY_NAME_<name>
			#

			$wstr = "";

			$announcement_by_event = false;
			$announcement_dtmf = "";
			$announcement_default_dtmf = "";
			$announcement_zone = "";
			$routine_alert_dtmf = "";
			$routine_noalert_dtmf = "";
			$weather_alert_str = "";

			if (strpos($text, '$alerts') !== false) {
			   $weather_zone = "";
			   $auto_compress = 0;
			   $include_codes = "";
			   $include_county = "";
			   $include_instructions = "";
			   $alert_announce_play_once = FALSE;
			   $alert_announce_play_skip = 0;
			   $alert_announce_stop_count = 0;
			   $routine_alert_once = FALSE;
			   $routine_noalert_once = FALSE;
			   $post_dash_phone_text = FALSE;
			   $announcement_by_event = false;
			   $zone_display_name = "";

			   if (preg_match("/(.*)alerts\(([a-zA-Z0-9_,.\-: ]*)\)(.*)/",$text,$parts)) {
				if ($debug) print_r($parts);

				$options_str = trim($parts[2]);
				$options_str = str_replace(' ','',$options_str);

				$options = explode(',',$options_str);

				foreach ($options as $option) {

					#
					# Process ZONE_DISPLAY_NAME
					#
					if (preg_match('/ZONE_DISPLAY_NAME_(.*)/',$option,$zdname) != false) {
						$zone_display_name = $zdname[1];
						$zone_display_name = str_replace('_',' ',$zone_display_name);
						unset($zdname);
						continue;
					}

					#
					# Process all other options...
					#

					$option = strtolower($option);

					switch($option)
					{

						case "auto_find_county":
							$weather_zone = "AUTO_FIND_COUNTY";
							break;

						case "include_codes":
							$include_codes = "-C";
							break;

						case "include_county":
							$include_county = "-I";
							break;

						case "include_instructions":
							$include_instructions = "-i";
							break;

						case "play_once":
							$alert_announce_play_once = TRUE;
							break;

						case "routine_alert_once":
							$routine_alert_once = TRUE;
							break;

						case "routine_noalert_once":
							$routine_noalert_once = TRUE;
							break;

						case "post_dash_phone_text":
							$post_dash_phone_text = TRUE;
							break;

						case "update_cache":
							if (($weather_zone == "") || ($weather_zone == "AUTO_FIND_COUNTY")) {
								$weather_zone = "AUTO_FIND_COUNTY_UPDATE_CACHE";
							}
							break;

						case ( preg_match('/auto_compress_.*/',$option) ? true : false):
							if (preg_match('/auto_compress_(.*)/',$option,$acnum)) {
								if (is_numeric($acnum[1])) {
									$auto_compress = $acnum[1];
								}
								unset($acnum);
							}
							break;

						case ( preg_match('/repeat_.*/',$option) ? true : false):
							if (preg_match('/repeat_(.*)/',$option,$rnum)) {
								if (is_numeric($rnum[1])) {
									$auto_repeat = $rnum[1];

									# NWS recommends > 30 check interval to prevent blocking
									if ($auto_repeat < 30) $auto_repeat = 30;
								}
								unset($rnum);
							}
							break;

						case ( preg_match('/announcement_dtmf.*/',$option) ? true : false):
							if (preg_match('/announcement_dtmf_(.*)/',$option,$announce_dtmf)) {
								if (is_numeric($announce_dtmf[1])) {
									$announcement_dtmf = $announce_dtmf[1];
								}
								unset($announce_dtmf);
							}
							break;

						case ( preg_match('/announcement_default_dtmf.*/',$option) ? true : false):
							if (preg_match('/announcement_default_dtmf_(.*)/',$option,$announce_dtmf)) {
								if (is_numeric($announce_dtmf[1])) {
									$announcement_default_dtmf = $announce_dtmf[1];
								}
								unset($announce_dtmf);
							}
							break;

						case ( preg_match('/announcement_zone.*/',$option) ? true : false):
							if (preg_match('/announcement_zone_(.*)/',$option,$announce_zone)) {
								if (is_numeric($announce_zone[1])) {
									$announcement_zone = $announce_zone[1];
								}
								unset($announce_zone);
							}
							break;

						case ( preg_match('/announcement_by_event/',$option) ? true : false):
							$announcement_by_event = true;
							$announcement_dtmf = "";
							break;

						case ( preg_match('/routine_alert_dtmf.*/',$option) ? true : false):
							if (preg_match('/routine_alert_dtmf_(.*)/',$option,$routine_dtmf)) {
								if (is_numeric($routine_dtmf[1])) {
									$routine_alert_dtmf = $routine_dtmf[1];
								}
								unset($routine_dtmf);
							}
							break;

						case ( preg_match('/routine_noalert_dtmf.*/',$option) ? true : false):
							if (preg_match('/routine_noalert_dtmf_(.*)/',$option,$routine_dtmf)) {
								if (is_numeric($routine_dtmf[1])) {
									$routine_noalert_dtmf = $routine_dtmf[1];
								}
								unset($routine_dtmf);
							}
							break;

						case ( preg_match('/play_skip_.*/',$option) ? true : false):
							if (preg_match('/play_skip_(.*)/',$option,$pnum)) {
								if (is_numeric($pnum[1])) {
									$alert_announce_play_skip = $pnum[1];
								}
								unset($pnum);
							}
							break;

						case ( preg_match('/play_stop_.*/',$option) ? true : false):
							if (preg_match('/play_stop_(.*)/',$option,$snum)) {
								if (is_numeric($snum[1])) {
									$alert_announce_stop_count = $snum[1];
								}
								unset($snum);
							}
							break;

						case ( preg_match('/zone_display_name_.*/',$option) ? true : false):
							break;

						default:
							if ($debug) echo "\nOption: $option\n";
							if ($weather_zone == "") {
								$option = strtoupper($option);

								if (strpos($option, 'LATLON') !== false) {
									$option = str_replace('_',',', $option);
								}

								$weather_zone = $option;
							}
							break;
					}
				}

				if (($auto_repeat == 0) && ($post_dash_phone_text == TRUE)) {
					$text = str_replace('$alerts','Error, missing REPEAT_# parameter: alerts',$text);
					$sFontSize = 30;
					$styleParams = "-1 \"$sBgColor\" -2 \"$sFont\" -3 \"$sFontStyle\" -4 \"$sFontSize\" -5 \"$sFontColor\" -6 \"$sMsgType\" -7 \"$sImageType\" -8 \"$sImageId\" -9 \"$sImagePriority\"";
					$weather_zone = "";
				}

				if ($options_str == "") {
					$text = str_replace('$alerts','Syntax error, missing parameters: alerts',$text);
					$sFontSize = 30;
					$styleParams = "-1 \"$sBgColor\" -2 \"$sFont\" -3 \"$sFontStyle\" -4 \"$sFontSize\" -5 \"$sFontColor\" -6 \"$sMsgType\" -7 \"$sImageType\" -8 \"$sImageId\" -9 \"$sImagePriority\"";
					$weather_zone = "";
				}

				unset($parts);
				unset($options);

				if (($weather_zone == "AUTO_COMPRESS") || ($weather_zone == "AUTO_COMPRESS_")) {
					$text = str_replace('$alerts','Syntax error, AUTO_COMPRESS missing number: alerts ',$text);
					$sFontSize = 30;
					$styleParams = "-1 \"$sBgColor\" -2 \"$sFont\" -3 \"$sFontStyle\" -4 \"$sFontSize\" -5 \"$sFontColor\" -6 \"$sMsgType\" -7 \"$sImageType\" -8 \"$sImageId\" -9 \"$sImagePriority\"";
					$weather_zone = "";
				}

				if (($weather_zone == "REPEAT") || ($weather_zone == "REPEAT_")) {
					$text = str_replace('$alerts','Syntax error, REPEAT missing number: alerts ',$text);
					$sFontSize = 30;
					$styleParams = "-1 \"$sBgColor\" -2 \"$sFont\" -3 \"$sFontStyle\" -4 \"$sFontSize\" -5 \"$sFontColor\" -6 \"$sMsgType\" -7 \"$sImageType\" -8 \"$sImageId\" -9 \"$sImagePriority\"";
					$weather_zone = "";
				}
			   } else {
				$text = str_replace('$alerts','Syntax error, missing paren: alerts',$text);
				$sFontSize = 30;
				$styleParams = "-1 \"$sBgColor\" -2 \"$sFont\" -3 \"$sFontStyle\" -4 \"$sFontSize\" -5 \"$sFontColor\" -6 \"$sMsgType\" -7 \"$sImageType\" -8 \"$sImageId\" -9 \"$sImagePriority\"";
				$weather_zone = "";
			   }

			   if ($weather_zone != "") {
				if ($zone_display_name != "") {
					$zone_display_name = "-Z \"$zone_display_name\"";
				}

				if ($debug) {
					   echo "/usr/local/bin/weather-alert -b $include_codes $include_county $include_instructions -z $weather_zone $zone_display_name\n";
				}
				exec("/usr/local/bin/weather-alert -b $include_codes $include_county $include_instructions -z $weather_zone $zone_display_name", $wout, $ret);
			   } else {
				$ret = 1;

				$text = str_replace('$alerts','Syntax error, missing county code: alerts',$text);
				$sFontSize = 30;
				$styleParams = "-1 \"$sBgColor\" -2 \"$sFont\" -3 \"$sFontStyle\" -4 \"$sFontSize\" -5 \"$sFontColor\" -6 \"$sMsgType\" -7 \"$sImageType\" -8 \"$sImageId\" -9 \"$sImagePriority\"";
			   }

			   if ($post_dash_phone_text) {
				$db->query("DELETE FROM dashboard_msg WHERE identifier='Weather-Alert-$myPid'");

				$result = $db->query("UPDATE yealink_message SET extension='', message='' WHERE extension='WA-$myPid'");
				if (mysqli_affected_rows($db) > 0)
				{
					exec("/usr/local/bin/update-yealink-idlescreen > /dev/null 2>&1 &");
				}
			   }

			   if ($ret == 0) {

				$idx = 0;

				$num_lines = count($wout);

				if ($auto_compress) {
					while ($idx < count($wout)) {
						if ($wout[$idx] == "") unset($wout[$idx]);

						$idx++;
					}
				}

				$spacer = ($auto_compress && (count($wout) > $auto_compress)) ? " " : "<br>";

				$idx = 0;

				unset($save_alert_hdr2);

				while ($idx < $num_lines) {
					if (isset($wout[$idx])) {
						if ($wout[$idx] != "") {
							$wstr.= $wout[$idx].$spacer;

							if (strpos($wout[$idx], "--- ALERT ") !== false) {
								preg_match("/(.*)<br>/",$wout[$idx],$aparts);

								$save_alert_hdr2[] = substr($aparts[0], strpos($aparts[1], "--- ALERT "));
							}

						}
					}
					$idx++;
				}

				$wstr = addslashes($wstr);

				if ($debug) echo "\$wstr = $wstr\n";
			   }

			   unset($wout);

			   $text = preg_replace('/\$alerts\(([a-zA-Z0-9_,.\-: ]*)\)/',$wstr,$text);

			   $weather_alert_str = $wstr;
			}

			#
			# Process $weather variable
			#
			if (strpos($text, '$weather') !== false) {
				$wstr = "";

				$weather_units = "imperial";
				$weather_toggle_decimal_digit = "";

				if (preg_match("/weather_units_metric/",$text,$parts)) {
					$weather_units = "metric";
					$text = preg_replace('/weather_units_metric/','',$text);
				}

				if (preg_match("/weather_toggle_decimal_digit/",$text,$parts)) {
					$weather_toggle_decimal_digit = " -t";
					$text = preg_replace('/weather_toggle_decimal_digit/','',$text);
				}

				if (preg_match("/weather_zipcode_([0-9]{5})/",$text,$parts)) {
					$weather_zipcode = $parts[1];
					$text = preg_replace('/weather_zipcode_([0-9]{5})/','',$text);
					exec("/usr/local/bin/weather -u $weather_units -z $weather_zipcode".$weather_toggle_decimal_digit, $wout, $ret);
				} elseif (preg_match("/weather_latlon_(.*),(.*)#/",$text,$parts)) {
					$weather_lat = $parts[1];
					$weather_lon = $parts[2];
					$text = preg_replace('/weather_latlon_(.*),(.*)#/','',$text);
					exec("/usr/local/bin/weather -u $weather_units -L $weather_lat -l $weather_lon".$weather_toggle_decimal_digit, $wout, $ret);
				} else {
					exec("/usr/local/bin/weather -u $weather_units ".$weather_toggle_decimal_digit, $wout, $ret);
				}


				if ($ret == 0) {
					$wstr = "$wout[0]<br>$wout[1]<br>$wout[2]<br>$wout[3]";
					$text = str_replace('$weather',$wstr,$text);
				}
				unset($wout);
			}

			#
			# Process $auto_resize variable
			#
			if (strpos($text, '$auto_resize') !== false) {
				$min_font_size = 20;

				if (preg_match("/(.*)auto_resize\(([0-9]+)\)(.*)/",$text,$parts)) {
					if ($debug) print_r($parts);

					$min_font_size = $parts[2];

					$text = preg_replace('/\$auto_resize\(([0-9]+)\)/','',$text);

					$text_len = strlen($text);

					$decrease_mult = round($text_len / 115) - 1;

					if ($decrease_mult < 0) $decrease_mult = 0;

					$sFontSize = $sFontSize - ($decrease_mult * 10);

					if ($sFontSize < $min_font_size) {
						$sFontSize = $min_font_size;
					}

					if ($debug) echo "Decrease by: $decrease_mult\nNew Font Size: $sFontSize\n";

					$styleParams = "-1 \"$sBgColor\" -2 \"$sFont\" -3 \"$sFontStyle\" -4 \"$sFontSize\" -5 \"$sFontColor\" -6 \"$sMsgType\" -7 \"$sImageType\" -8 \"$sImageId\" -9 \"$sImagePriority\"";

					unset($parts);
				} else {
					$text = str_replace('$auto_resize','Syntax error, missing parameter: auto_resize',$text);
				}
			}

			#
			# If $text is empty, and station-type-filter is not equal to WBC-only (2),
			# don't create a display message.
			#
			if (($sMsgType == "text") && (strlen(trim($text)) == 0) && ($wbcStationTypeFilter != 2)) {
				unset($text);

				#
				# Reset Alert Announcement played counts and Routine alert executed count
				#
				$alert_announce_played_count = 0;
				$alert_announce_played_total_count = 0;
				$routine_alert_executed_count = 0;

				if (($routine_noalert_once == TRUE) && ($routine_noalert_executed_count > 0)) {
					$routine_noalert_dtmf = "";
				}

				if ($routine_noalert_dtmf != "") {
					$routine_id = getRoutineId($debug, $db, $routine_noalert_dtmf);

					exec("/usr/local/bin/routine-execute -r $routine_id $origCaller > /dev/null 2>&1 &");

					$routine_noalert_executed_count++;
				}

				#
				# If we had an alert but now don't, remove it.
				#
				if ($previous_alert_str != $weather_alert_str) {
					createAlertsXmlFile($debug, "");

					$previous_alert_str = $weather_alert_str;
				}

				return;
			}

			#
			# If message is too large, automatically drop font size to 20
			#
			if ($priority < 6) {
				if (strlen($text) > 1500) {
					if (strlen(preg_replace('/[^A-Z]+/','',$text)) > 500) {
						$styleParams = "-1 \"$sBgColor\" -2 \"$sFont\" -3 \"$sFontStyle\" -4 \"20\" -5 \"$sFontColor\" -6 \"$sMsgType\" -7 \"$sImageType\" -8 \"$sImageId\" -9 \"$sImagePriority\"";
					}
				}
			}

			$text = escapeshellarg($text);

			if ($debug) {
				echo "/usr/local/bin/display-message -c $caller $dzones $dstations -t $text -d $duration -p $priority $idText $wbcParams $styleParams\n";
			}

			exec("/usr/local/bin/display-message -c $caller $dzones $dstations -t $text -d $duration -p $priority $idText $wbcParams $styleParams", $out, $ret);

			if ($ret == 0) {
				$displayMsgId[] = $out[0];
			}

			unset($out);
			unset($text);

			if ($post_dash_phone_text && isset($save_alert_hdr2)) {
			   if ($debug) "Process post_dash_phone_text\n";

			   $sql = "DELETE FROM dashboard_msg WHERE identifier='Weather-Alert-$myPid'";
			   $db->query($sql);

			   $sql = "UPDATE yealink_message SET extension='', message='' WHERE extension='WA-$myPid'";
			   $db->query($sql);

			   foreach ($save_alert_hdr2 as $save_alert_hdr) {
				if ($debug) echo "Add event -> $save_alert_hdr\n";

				preg_match("/(.*)<br>/",$save_alert_hdr,$aparts);
				$save_alert_hdr = "$pDate1 $pTime2 ".$aparts[1];

				$sql = "INSERT INTO dashboard_msg
					SET type='text',message_style='warning',text='$save_alert_hdr',
					popup_title='Text',scope='Admin',action='Monitor',
					extension='$caller',identifier='Weather-Alert-$myPid'";
				$db->query($sql);

				# Build and post text message for Yealink admin phones
				preg_match("/--- ALERT(.*)--- (.*)/", $save_alert_hdr, $yea_parts);

				$yea_msg = ($yea_parts[1] == " " ? $yea_parts[2] : $yea_parts[1].$yea_parts[2]);

				#
				# Remove categories
				#
				unset($yea_parts);
				if (preg_match("/^(.*) \((.*)\)$/", $yea_msg, $yea_parts)) {
					$yea_msg = $yea_parts[1];
				}

				unset($yea_parts);

				$sql = "UPDATE yealink_message
					SET extension='WA-$myPid', message='$yea_msg'
					WHERE message='' AND extension!='000' LIMIT 1";
				$db->query($sql);

				exec("/usr/local/bin/update-yealink-idlescreen > /dev/null 2>&1 &");
			   }
			}

			#
			# If alert text has changed, update Yealink NWS Alerts text xml file,
			# reset announce played counters.
			#
			if ($previous_alert_str != $weather_alert_str) {
				createAlertsXmlFile($debug, $weather_alert_str);

				$previous_alert_str = $weather_alert_str;

				$alert_announce_played_count = 0;
				$alert_announce_played_total_count = 0;

			}

			if ($announcement_by_event || ($announcement_dtmf != "")) {
			   $played_announcement = false;
			   $first_event = true;
			   $incremented_played_count = false;
			   $skip_other_events = false;

			   foreach ($save_alert_hdr2 as $this_event) {

				if ($debug) echo "Event: $this_event\n";

				if ($skip_other_events) continue;

				#
				# Get Announcement to play by Event type
				#
				if ($announcement_by_event) {
					$announcement_dtmf = "";

					#
					# Get Event Type from ALERT header.
					#
					if ($include_codes != "") {
						preg_match("/--- ALERT(.*)--- (.*) \((.*)\)/", $this_event, $event_parts);
					} else {
						preg_match("/--- ALERT(.*)--- (.*)/", $this_event, $event_parts);
					}

					$alert_event = rtrim($event_parts[2], ' ');

					if (preg_match("/(.*) <br>/", $alert_event, $event_parts)) {
						$alert_event = $event_parts[1];
					}

					if ($debug) echo "Alert event: $alert_event\n";

					unset($event_parts);

					if ($debug) echo "SQL: $sql\n";

					$sql = "SELECT announcement_dtmf FROM weather_alert_filter WHERE event='$alert_event'";

					if ($resultA = $db->query($sql)) {
						if ($resultA->num_rows > 0) {
							$row = $resultA->fetch_assoc();
							$alert_filter_announcement_dtmf = $row['announcement_dtmf'];

							if ($alert_filter_announcement_dtmf != 0) {
								$announcement_dtmf = $alert_filter_announcement_dtmf;
							} else {
								if ($announcement_default_dtmf != "") {
									$announcement_dtmf = $announcement_default_dtmf;
								}
							}
						}
						$resultA->free();
					}
				}

				if ($announcement_dtmf != "") {
					#
					# If PLAY_STOP_# is set and total played >= stop count, disable playback.
					# If PLAY_ONCE is set and we already played the alert announcement, disable playback.
					# If PLAY_SKIP is >= played count, disable playing this time but increase played count.
					# If PLAY_SKIP is < played count, reset played count to force playing announcement again.
					#
					if (($alert_announce_stop_count > 0) && ($alert_announce_played_total_count >= $alert_announce_stop_count)) {
						$announcement_dtmf = "";
					}
					else
					if (($alert_announce_play_once == TRUE) && ($alert_announce_played_count > 0)) {
						$announcement_dtmf = "";
					}
					else
					if (($alert_announce_played_count > 0) && ($alert_announce_play_skip >= $alert_announce_played_count)) {
						$announcement_dtmf = "";
						if (!$incremented_played_count) {
							$alert_announce_played_count++;
							$incremented_played_count = true;
							$skip_other_events = true;
						}
					}
					else
					if (($alert_announce_play_skip > 0) && ($alert_announce_play_skip < $alert_announce_played_count)) {
						$alert_announce_played_count = 0;
					}
				}

				if ($announcement_dtmf != "") {
					$zones = getAnnounceZone($debug, $db, $announcement_dtmf);

					$announcement_id = getAnnouncementId($debug, $db, $announcement_dtmf);

					if ($announcement_id != "") {
						$announce_zone = ($announcement_zone != "") ? $announcement_zone : $zones;

						$announce_filename = getAnnounceFilename($debug, $db, $announcement_id);

						$announce_wait_time = getAudioDurationInSeconds($announce_filename);

						if ($announce_wait_time != false) {
							# Add some time to compensate for delay in play start.
							$announce_wait_time+=5;

							if ($announce_wait_time > 300) {
								$announce_wait_time = 300;
							}
						} else {
							$announce_wait_time = $auto_repeat > 0 ? ($auto_repeat - 10) : 60;
						}

						if ($debug) echo "Announce Wait: $announce_wait_time\n";

						executeAmiCall($debug,
							"announcement",
							$caller,
							"0000092$announcement_id*$announce_zone",
							$announce_zone,
							$waitTime = $announce_wait_time,
							$delay = "");

			   			$played_announcement = true;
					}
				}
			   }
				if ($played_announcement) {
					if ($first_event) {
						$alert_announce_played_count++;
						$alert_announce_played_total_count++;
						$first_event = false;
					}
				}
		   	}

			if ($debug) echo "Resume processing...\n";

			if (($routine_alert_once == TRUE) && ($routine_alert_executed_count > 0)) {
				$routine_alert_dtmf = "";
			}

			if ($routine_alert_dtmf != "") {
				$routine_id = getRoutineId($debug, $db, $routine_alert_dtmf);

				exec("/usr/local/bin/routine-execute -r $routine_id -c $origCaller > /dev/null 2>&1 &");

				$routine_alert_executed_count++;
			}

			$routine_noalert_executed_count = 0;

			break;

		case 'display-msg-delete':

			$deleteCmd = "";

			if (($param1 == "IDENT") && ($param2 != "")) {
				$deleteCmd = "-R -i \"$param2\"";

				$deleteCmd = str_replace('$caller',$origCaller,$deleteCmd);
				$deleteCmd = str_replace('$called',$called,$deleteCmd);

				foreach ($user_var as $key => $u_var) {
					$user_var_str = '$user_var['.$key.']';
					$deleteCmd = str_replace("$user_var_str",$user_var[$key],$deleteCmd);
				}

				#
				# Replace user supplied variables with current values
				#
				switch ($trigger)
				{
					case 'API':
						if ($json_data != null) {
							for ($i = 20; $i > 0; $i--) {
								$paramKey = 'apiParam'.$i;
								if (isset($json_data['routineApiParams'][$paramKey])) {
									$placeholder = '$'.$paramKey;
									$deleteCmd = str_replace($placeholder, $json_data['routineApiParams'][$paramKey], $deleteCmd);
								}
							}
						}
						$deleteCmd = str_replace('$apiParam1',$cmdParam1,$deleteCmd);
						$deleteCmd = str_replace('$apiParam2',$cmdParam2,$deleteCmd);
						$deleteCmd = replaceCapData($deleteCmd);
						break;

					case 'input-contact-closed':
					case 'input-contact-opened':
						$deleteCmd = str_replace('$contact',$cmdParam2,$deleteCmd);
						$deleteCmd = str_replace('$station',$cmdParam1,$deleteCmd);
						break;

					case 'facility-status-up':
					case 'facility-status-down':
					case 'facility-page':
						$fname = getFacilityName($debug, $db, $cmdParam1);
						$deleteCmd = str_replace('$facility',$fname,$deleteCmd);
						break;

					case 'amp-protection-mode':
						$deleteCmd = str_replace('$deviceMacAddr',$cmdParam1,$deleteCmd);
						break;

					case 'nws-alert':
						$deleteCmd = str_replace('$nwsAlertEvent', $cmdParam1,$deleteCmd);
						$deleteCmd = str_replace('$nwsAlertSeverity', $cmdParam2,$deleteCmd);
						$deleteCmd = str_replace('$nwsAlertCertainty', $extra_args[1],$deleteCmd);
						$deleteCmd = str_replace('$nwsAlertUrgency', $extra_args[2],$deleteCmd);
						$deleteCmd = str_replace('$nwsAlertResponse', $extra_args[3],$deleteCmd);
						$deleteCmd = str_replace('$nwsAlertZone', $extra_args[4],$deleteCmd);
						break;

					case 'schedule-activated':
						$deleteCmd = str_replace('$scheduleid',$cmdParam1,$deleteCmd);
						break;

					case 'scheduled-routine':
						$deleteCmd = str_replace('$schedParam1',$cmdParam1,$deleteCmd);
						$deleteCmd = str_replace('$schedParam2',$cmdParam2,$deleteCmd);

						if ($zonesProvided)
						{
							$zname = getZoneName($debug, $db, $zones);
							$deleteCmd = str_replace('$zone',$zname,$deleteCmd);
						}
						break;

					case 'sched-event':
						$ename = getEventName($debug, $db, $cmdParam1);
						$deleteCmd = str_replace('$eventname',$ename,$deleteCmd);
						$deleteCmd = str_replace('$eventid',$cmdParam1,$deleteCmd);
						break;

					case 'scheduled-tone':
						$tname = getToneName($debug, $db, $cmdParam1);
						$deleteCmd = str_replace('$tonename',$tname,$deleteCmd);
						$deleteCmd = str_replace('$toneid',$cmdParam1,$deleteCmd);
						break;

					case 'station-status-up':
					case 'station-status-down':
						$deleteCmd = str_replace('$station',$cmdParam1,$deleteCmd);
						break;

					case 'announcement':
					case 'zone-page':
						$zname = getZoneName($debug, $db, $zones);
						$deleteCmd = str_replace('$zone',$zname,$deleteCmd);
						break;

					default:
						break;
				}
			}
			else
			if ((count($displayMsgId) != 0)) {
				if ($param1 == 'LIFO') {
					if (($deleteId = array_pop($displayMsgId)) !== NULL) {
						$deleteCmd = "-r $deleteId";
					}
				}
				else
				if ($param1 == 'FIFO') {
					if (($deleteId = array_shift($displayMsgId)) !== NULL) {
						$deleteCmd = "-r $deleteId";
					}
				}
				else
				if ($param1 == 'PARAM') {
					if (($param3 == "") && ($param4 == "")) {
						$deleteCmd = "";
					}
					else
					if (($param3 == "0") && ($param4 == "0")) {
						$deleteCmd = "-R";
					}
					else
					if ((($param4 != "") && ($param4 != "0")) && ($param3 == "0")) {
						$deleteCmd = "-R -z \"-1\" -s \"$param4\"";
					}
					else
					if ((($param3 != "") && ($param3 != "0")) && ($param4 == "0")) {
						$deleteCmd = "-R -z \"$param3\" -A -s \"-1\"";
					} else {
						$deleteCmd = "-R -z \"$param3\" -A -s \"$param4\"";
					}
				}
				else
				if (is_numeric($param2) && ($param2 > 0)) {
					if (isset($displayMsgId[($param2 - 1)])) {
						$deleteId = $displayMsgId[($param2 - 1)];
						$deleteCmd = "-r $deleteId";
					}
				}
			}

			if ($deleteCmd != "") {
				if ($mergeDelete) {
					$deleteCmd.= " -m";
				}

				if ($debug) {
					echo "/usr/local/bin/display-message $deleteCmd > /dev/null 2>&1\n";
				}

				exec("/usr/local/bin/display-message $deleteCmd > /dev/null 2>&1");
			}

			break;

		case 'email':
			#
			# Replace user supplied variables with current values
			#
			# $param2 - Subject
			# $param3 - Message body
			#
			switch ($trigger)
			{
				case 'API':
					if ($json_data != null) {
						for ($i = 20; $i > 0; $i--) {
							$paramKey = 'apiParam'.$i;
							if (isset($json_data['routineApiParams'][$paramKey])) {
								$placeholder = '$'.$paramKey;
								$param2 = str_replace($placeholder, $json_data['routineApiParams'][$paramKey], $param2);
								$param3 = str_replace($placeholder, $json_data['routineApiParams'][$paramKey], $param3);
							}
						}
					}
					$param2 = str_replace('$apiParam1',$cmdParam1,$param2);
					$param2 = str_replace('$apiParam2',$cmdParam2,$param2);
					$param3 = str_replace('$apiParam1',$cmdParam1,$param3);
					$param3 = str_replace('$apiParam2',$cmdParam2,$param3);
					$param2 = replaceCapData($param2);
					$param3 = replaceCapData($param3);
					break;

				case 'input-contact-closed':
				case 'input-contact-opened':
					$param2 = str_replace('$contact',$cmdParam2,$param2);
					$param2 = str_replace('$station',$cmdParam1,$param2);
					$param3 = str_replace('$contact',$cmdParam2,$param3);
					$param3 = str_replace('$station',$cmdParam1,$param3);
					break;

				case 'facility-status-up':
				case 'facility-status-down':
				case 'facility-page':
					$fname = getFacilityName($debug, $db, $cmdParam1);
					$param2 = str_replace('$facility',$fname,$param2);
					$param3 = str_replace('$facility',$fname,$param3);
					break;

				case 'amp-protection-mode':
					$devName = getDeviceNameByMacAddr($cmdParam1);
					$param2 = str_replace('$deviceMacAddr',$cmdParam1,$param2);
					$param2 = str_replace('$deviceName',$devName,$param2);

					$param3 = str_replace('$deviceMacAddr',$cmdParam1,$param3);
					$param3 = str_replace('$deviceName',$devName,$param3);
					break;

				case 'nws-alert':
					$param2 = str_replace('$nwsAlertEvent', $cmdParam1,$param2);
					$param2 = str_replace('$nwsAlertSeverity', $cmdParam2,$param2);
					$param2 = str_replace('$nwsAlertCertainty', $extra_args[1],$param2);
					$param2 = str_replace('$nwsAlertUrgency', $extra_args[2],$param2);
					$param2 = str_replace('$nwsAlertResponse', $extra_args[3],$param2);
					$param2 = str_replace('$nwsAlertZone', $extra_args[4],$param2);

					$param3 = str_replace('$nwsAlertEvent', $cmdParam1,$param3);
					$param3 = str_replace('$nwsAlertSeverity', $cmdParam2,$param3);
					$param3 = str_replace('$nwsAlertCertainty', $extra_args[1],$param3);
					$param3 = str_replace('$nwsAlertUrgency', $extra_args[2],$param3);
					$param3 = str_replace('$nwsAlertResponse', $extra_args[3],$param3);
					$param3 = str_replace('$nwsAlertZone', $extra_args[4],$param3);
					break;

				case 'schedule-activated':
					$sname = getScheduleName($debug, $db, $cmdParam1);
					$param2 = str_replace('$schedulename',$sname,$param2);
					$param3 = str_replace('$schedulename',$sname,$param3);
					break;

				case 'scheduled-routine':
					$param2 = str_replace('$schedParam1',$cmdParam1,$param2);
					$param2 = str_replace('$schedParam2',$cmdParam2,$param2);
					$param3 = str_replace('$schedParam1',$cmdParam1,$param3);
					$param3 = str_replace('$schedParam2',$cmdParam2,$param3);

					if ($zonesProvided) {
						$zname = getZoneName($debug, $db, $zones);
						$param2 = str_replace('$zone',$zname,$param2);
						$param3 = str_replace('$zone',$zname,$param3);
					}
					break;

				case 'sched-event':
					$ename = getEventName($debug, $db, $cmdParam1);
					$param2 = str_replace('$eventid',$cmdParam1,$param2);
					$param2 = str_replace('$eventname',$ename,$param2);
					$param3 = str_replace('$eventid',$cmdParam1,$param3);
					$param3 = str_replace('$eventname',$ename,$param3);
					break;

				case 'scheduled-tone':
					$tname = getToneName($debug, $db, $cmdParam1);
					$param2 = str_replace('$toneid',$cmdParam1,$param2);
					$param2 = str_replace('$tonename',$tname,$param2);
					$param3 = str_replace('$toneid',$cmdParam1,$param3);
					$param3 = str_replace('$tonename',$tname,$param3);
					break;

				case 'station-status-up':
				case 'station-status-down':
					$param2 = str_replace('$station',$cmdParam1,$param2);
					$param3 = str_replace('$station',$cmdParam1,$param3);
					break;

				case 'announcement':
				case 'zone-page':
					$zname = getZoneName($debug, $db, $zones);
					$param2 = str_replace('$zone',$zname,$param2);
					$param3 = str_replace('$zone',$zname,$param3);
					break;

				default:
					$param2 = str_replace('$contact','N/A',$param2);
					$param2 = str_replace('$station','N/A',$param2);
					$param3 = str_replace('$contact','N/A',$param3);
					$param3 = str_replace('$station','N/A',$param3);
					$param2 = str_replace('$eventid','N/A',$param2);
					$param3 = str_replace('$eventid','N/A',$param3);
					$param2 = str_replace('$eventname','N/A',$param2);
					$param3 = str_replace('$eventname','N/A',$param3);
					$param2 = str_replace('$facility','N/A',$param2);
					$param3 = str_replace('$facility','N/A',$param3);
					$param2 = str_replace('$station','N/A',$param2);
					$param3 = str_replace('$station','N/A',$param3);
					break;
			}

			if ($trigger != "incoming-call") {
				$param2 = str_replace('$caller',$origCaller,$param2);
			} else {
				$param2 = str_replace('$caller',$cmdParam1,$param2);
			}
			$param2 = str_replace('$namecaller',$callerName,$param2);
			$param2 = str_replace('$called',$called,$param2);
			$param2 = str_replace('$namecalled',$calledName,$param2);
			$param2 = str_replace('$date1', $pDate1, $param2);
			$param2 = str_replace('$date2', $pDate2, $param2);
			$param2 = str_replace('$date3', $pDate3, $param2);
			$param2 = str_replace('$time1', $pTime1, $param2);
			$param2 = str_replace('$time2', $pTime2, $param2);

			foreach ($user_var as $key => $u_var) {
				$user_var_str = '$user_var['.$key.']';

				$param2 = str_replace("$user_var_str",$user_var[$key],$param2);
				$param3 = str_replace("$user_var_str",$user_var[$key],$param3);
			}

			#
			# Process user supplied data from JSON file
			#
			if ($json_data != null) {
				$param2 = preg_replace_callback('/{{\s*(\w+(\.\w+)*)\s*}}/',
					   function($matches) use ($json_data) {
						$variable = $matches[1];
						$value = getNestedValue($json_data, $variable);
						return $value !== null ? $value : $matches[0];
					   }, $param2);

				$param3 = preg_replace_callback('/{{\s*(\w+(\.\w+)*)\s*}}/',
					   function($matches) use ($json_data) {
						$variable = $matches[1];
						$value = getNestedValue($json_data, $variable);
						return $value !== null ? $value : $matches[0];
					   }, $param3);
			}

			if (strpos($param2, '$cputemp') !== false) {
				if ($cputemp == "") $cputemp = getMaxCpuTemp();

				$param2 = str_replace('$cputemp', $cputemp, $param2);
			}

			if (strpos($param3, '$cputemp') !== false) {
				if ($cputemp == "") $cputemp = getMaxCpuTemp();

				$param3 = str_replace('$cputemp', $cputemp, $param3);
			}

			if ($trigger != "incoming-call") {
				$param3 = str_replace('$caller',$origCaller,$param3);
			} else {
				$param3 = str_replace('$caller',$cmdParam1,$param3);
			}
			$param3 = str_replace('$namecaller',$callerName,$param3);
			$param3 = str_replace('$called',$called,$param3);
			$param3 = str_replace('$namecalled',$calledName,$param3);
			$param3 = str_replace('$date1', $pDate1, $param3);
			$param3 = str_replace('$date2', $pDate2, $param3);
			$param3 = str_replace('$date3', $pDate3, $param3);
			$param3 = str_replace('$time1', $pTime1, $param3);
			$param3 = str_replace('$time2', $pTime2, $param3);

			if ((strpos($param2, '$df-avail') !== false) || (strpos($param3, '$df-avail') !== false)) {
				exec("/bin/df -h --output=avail /", $dfout, $ret);
				if ($ret == 0)
				{
					$param2 = str_replace('$df-avail',trim($dfout[1]),$param2);
					$param3 = str_replace('$df-avail',trim($dfout[1]),$param3);
				}
				unset($dfout);
			}

			if ((strpos($param2, '$uptime') !== false) || (strpos($param3, '$uptime') !== false)) {
				exec("/usr/bin/uptime -p", $uptimeout, $ret);
				if ($ret == 0) {
					$param2 = str_replace('$uptime',$uptimeout[0],$param2);
					$param3 = str_replace('$uptime',$uptimeout[0],$param3);
				}
				unset($uptimeout);
			}

			#
			# Escape '$' so that PHP does not try variable substitution when creating exec command line.
			#
			$param2 = str_replace('$','\\$',$param2);
			$param3 = str_replace('$','\\$',$param3);
			$param2 = str_replace('`','',$param2);
			$param3 = str_replace('`','',$param3);

			// Account can be specified to left of ':' in $param1
			$params = explode(':',$param1);
			if (count($params) > 1) {
				$account = $params[0];
				$toAddress = $params[1];
				exec("/usr/local/bin/nyquist-email -a \"$account\" -s '$param2' -m '$param3' -t $toAddress > /dev/null 2>&1 $delay");
			} else {
				$toAddress = $params[0];
				exec("/usr/local/bin/nyquist-email -s '$param2' -m '$param3' -t $toAddress > /dev/null 2>&1 $delay");
			}
			break;

		case 'emerg-all-call':
			if (($trigger == "API") || ($trigger == "scheduled-routine")) return;

			echo "951";
			doExit(0);
			break;

		case 'emergency-call':
			$do_emergency_call = true;
			if ($json_data != null) {
				if (isset($json_data['emergencyCallParams'])
					&& is_array($json_data['emergencyCallParams'])) {

					$actionName = getActionName($debug, $db, $routineActionId);

					foreach ($json_data['emergencyCallParams'] as $emergencyCallParameters) {
						if ($emergencyCallParameters['name'] === $actionName) {
							$emergencyCallParams = $emergencyCallParameters;

							if (isset($emergencyCallParams['placedBy'])) {
								$new_value = $emergencyCallParams['placedBy'];
								if (!hasCommandInjection($new_value)) {
									if (getStationType($debug,$db,$param1) != 0) {
										$param1 = $new_value;
									} else {
										logText("Emergency-Call: Invalid Placed By extension: $new_value", ROUTINE_LOG_INTERNAL);
										$do_emergency_call = false;
									}
								} else {
									logText("Emergency-Call: Invalid Placed By extension: $new_value", ROUTINE_LOG_INTERNAL);
										$do_emergency_call = false;
								}
							}
						}
					}
				}
			}

			if ($param1 == "") {
				if (($trigger == "API") || ($trigger == "scheduled-routine")) return;

				echo "*0";
				doExit(0);
			}

			if ($do_emergency_call) {
				intercomCall($debug, $db, $param1, "*0");
			}
			break;

		case 'enable-audio':
			getCosParams($debug, $db, $origCaller);

			if ($has_enable_audio == 0) {
				if ($origCaller == $caller) return;

				getCosParams($debug, $db, $caller);

				if ($has_enable_audio == 0) {
					return;
				}
			}

			executeAmiCall($debug, $action, $caller, "0000098", "0", 10, $delay);
			break;

		case 'facility-announcement':
			#
			# $param1 = announcement DTMF Code
			# $param2 = facility_call_list.id
			#
			if (($param1 != "") && ($param2 != "")) {
				executeAmiCall($debug, $action, $caller, "*97$param1#$param2**", "", 86400, $delay);
			}
			break;

		case 'facility-page':
			if (($trigger == "API") || ($trigger == "scheduled-routine")) return;

			echo "##$param1";
			doExit(0);
			break;

		case 'feature-wait':
			if ($param2 == 0) {
				$waitTime = PHP_INT_MAX;
			}
			else
			if ($param2 == "") {
				$waitTime = 86400;
			} else {
				$waitTime = $param2;
			}
			waitForActionIdle($debug, $param1, $origCaller, $waitTime);
			break;

		case 'goto':
			if ($param1 != "") {
				$gotoActionName = $param1;

				#$gotoActionName = getActionName($debug, $db, $param1);
			}
			break;

		case 'intercom-call':
			$do_intercom_call = true;

			if ($json_data != null) {
				if (isset($json_data['intercomCallParams']) && is_array($json_data['intercomCallParams'])) {

					$actionName = getActionName($debug, $db, $routineActionId);

					foreach ($json_data['intercomCallParams'] as $intercomCallParameters) {
						if ($intercomCallParameters['name'] === $actionName) {
							$intercomCallParams = $intercomCallParameters;

							if (isset($intercomCallParams['placedBy'])) {
								$new_value = $intercomCallParams['placedBy'];
								if (!hasCommandInjection($new_value)) {
									$param1 = $new_value;
								} else {
									logText("Intercom-Call: Invalid Placed By extension: $new_value", ROUTINE_LOG_INTERNAL);
									$do_intercom_call = false;
								}
							}
							if (isset($intercomCallParams['receivedBy'])) {
								$new_value = $intercomCallParams['receivedBy'];
								if (!hasCommandInjection($new_value)) {
									$param2 = $new_value;
								} else {
									logText("Intercom-Call: Invalid Recieved By extension: $new_value", ROUTINE_LOG_INTERNAL);
									$do_intercom_call = false;
								}
							}
						}
					}
				}
			}

			if ($param1 == "") {
				if (($trigger == "API") || ($trigger == "scheduled-routine")) return;

				echo "$param2";
				doExit(0);
			}

			#
			# Make sure we have a valid station
			#
			if ($do_intercom_call && (getStationType($debug,$db,$param1) != 0) && (getStationType($debug,$db,$param2) != 0)) {
				#
				# Make sure that at least one side of the call is capable of hanging up
				#
				if (stationCanHangup($debug,$db,$param1) || stationCanHangup($debug,$db,$param2)) {
					intercomCall($debug, $db, $param1, $param2);
				} else {
					logText("Intercom-Call: neither station can hangup, aborting", ROUTINE_LOG_INTERNAL);
				}
			} else {
				logText("Intercom-Call: Invalid station, aborting", ROUTINE_LOG_INTERNAL);
			}
			break;

		case 'intercom-wait':
			intercomWait($debug, $db, $origCaller, $called);
			break;

		case 'log-text':
			if ($param1 != "") {
				logText($param1, ROUTINE_LOG_USER);
			}
			break;

		case 'multi-site-all-call':
			if (($trigger == "API") || ($trigger == "scheduled-routine")) return;

			echo "##0";
			doExit(0);
			break;

		case 'multi-site-e-all-call':
			if (($trigger == "API") || ($trigger == "scheduled-routine")) return;

			echo "##0911";
			doExit(0);
			break;

		case 'new-announcement':
			newAnnouncement($debug, $db, $caller, $param1, $param2, $param3, $param4, $routineId, $routineActionId, $myPid);
			# $param1 = Announcement Type ('normal','emergency')
			# $param2 = Zones
			# $param3 = Number of times to play
			# $param4 = Repeat period
			break;

		case 'play-ringtone':
			# Note: night_ring terms being used because the feature
			# uses existing pd-control to startNring/stopNring.
			$night_ring_operation = $param1;
			$night_ring_zones = $param2;
			$night_ring_timeout = $param3;

			if ($night_ring_zones == "") $night_ring_zones = "0";
			if ($night_ring_timeout == "") $night_ring_timeout = 0;

			switch ($night_ring_operation)
			{
				case 'start':
					exec("/usr/local/bin/pd-control startNring $night_ring_zones > /dev/null 2>&1 &");
					break;

				case 'stop':
					exec("/usr/local/bin/pd-control stopNring > /dev/null 2>&1 &");
					$night_ring_timeout = 0;
					break;

				default:
					$night_ring_timeout = 0;
					break;
			}

			if ($night_ring_timeout > 0) {
				sleep($night_ring_timeout);

				# TODO: Check to see if Night Ring is active
				exec("/usr/local/bin/pd-control stopNring > /dev/null 2>&1 &");
			}

			break;

		case 'output-contact-close':

			#
			# Look for parameter overrides in JSON data from Routine API (outputContactCloseParams)
			#
			# Example:
			#	"outputContactCloseParams": [
			#		{
			#			"name":"Close Contacts",
			#			"ioController": "601",
			#			"outputContacts": "1,2,3",
			#			"duration": ""
			#		}
			#	]
			#
			if ($json_data != null) {
				if (isset($json_data['outputContactCloseParams']) &&
					is_array($json_data['outputContactCloseParams'])) {

					$actionName = getActionName($debug, $db, $routineActionId);

					foreach ($json_data['outputContactCloseParams'] as $outputContactParams) {
						if ($outputContactParams['name'] === $actionName) {
							$contactParams = $outputContactParams;

							if (isset($outputContactParams['ioController'])) {
								$param1 = $outputContactParams['ioController'];
							}
							if (isset($outputContactParams['outputContacts'])) {
								$param2 = $outputContactParams['outputContacts'];
							}
							if (isset($outputContactParams['duration'])) {
								$param3 = $outputContactParams['duration'];
							}
							break;
						}
					}
				}
			}

			notifyContactStates($debug, $action, $param1, $param2, $param3);
			break;

		case 'output-contact-open':

			#
			# Look for parameter overrides in JSON data from Routine API (outputContactOpenParams)
			#
			# Example:
			#	"outputContactOpenParams": [
			#		{
			#			"name":"Open Contacts",
			#			"ioController": "601",
			#			"outputContacts": "1,2,3"
			#		}
			#	]
			#
			if ($json_data != null) {
				if (isset($json_data['outputContactOpenParams']) &&
					is_array($json_data['outputContactOpenParams'])) {

					$actionName = getActionName($debug, $db, $routineActionId);

					foreach ($json_data['outputContactOpenParams'] as $outputContactParams) {
						if ($outputContactParams['name'] === $actionName) {
							$contactParams = $outputContactParams;

							if (isset($outputContactParams['ioController'])) {
								$param1 = $outputContactParams['ioController'];
							}
							if (isset($outputContactParams['outputContacts'])) {
								$param2 = $outputContactParams['outputContacts'];
							}
							break;
						}
					}
				}
			}

			notifyContactStates($debug, $action, $param1, $param2, $param3);
			break;

		case 'page-exclusion':
			changePageExclusion($debug, $db, $param1, $param2, $delay);
			break;

		case 'pause':
			if ($param1 > 0) sleep($param1);
			break;

		case 'play-announcement':
			$waitTime = ($param3 != "") ? $param3 : 86400;

			playAnnouncement($debug, $action, $caller, "0000092", $waitTime, $delay);
			break;

		case 'repeat':
			$skipToActionNum = 1;
			break;

		case 'ring-wait':
			if ($trigger != "night-ring") {
				ringWait($debug, $db, $origCaller);
			} else {
				$waitTime = 86400;
				waitForActionIdle($debug, 'night-ring', $origCaller, $waitTime);
			}
			break;

		case 'routine':
			$otherParams = "";
			if ($called != "") {
				$otherParams.= "-C $called ";
			}
			if ($cmdParam1 != "") {
				$otherParams.= "-P $cmdParam1 ";
			}
			if ($cmdParam2 != "") {
				$otherParams.= "-p $cmdParam2 ";
			}
			if ($zonesProvided) {
				$otherParams.= "-z \"$zones\"";
			}

			$passTrigger = ($trigger != "") ? "-t $trigger" : "";

			if (($param2 == "") || ($param2 == "0")) {
				# Execute Routine on local Nyquist system
				if ($debug) {
					echo "/usr/local/bin/routine-execute -r $param1 $passTrigger -c $origCaller $otherParams $delay";
				}

				if ($amiSocket) {
					closeAmiConnection($debug);

					$amiSocket = FALSE;
				}

				#
				# If this is the last action, close the database
				# to prevent socket memory leak, otherwise;
				# close database and setup to skip already
				# executed actions.
				#
				if (($numActionsExecuted + 1) == $numActions) {
					updateTaskList($debug, $db, $routineId, "DELETE", $myPid);
					$db->close();
					$db = NULL;
				} else {
					$skipToActionNum = ($numActionsExecuted + 1);
					$db->close();
					$db = NULL;
				}

				exec("/usr/local/bin/routine-execute -r $param1 $passTrigger -c $origCaller $otherParams > /dev/null 2>&1 $delay");
			} else {
				# Execute Routine on remote Nyquist system(s)
				if ($param2 != "-1") {
					executeAmiCall($debug, $action, $caller, "##*$param2*0000094$param1", $zones = "", $waitTime = 4, $delay = "");
				} else {
					executeAmiAllRemote($debug, $caller, "*94$param1");
				}
			}
			break;

		case 'routine-disable':
			if ($param1 != "") {
				$sql = "UPDATE routine SET enabled='0' WHERE id='$param1'";
				$db->query($sql);

				#
				# Add row in routines_updated for Automatic Failover state
				#
				$sql = "DELETE FROM routines_updated WHERE routine_id='$param1'";
				$db->query($sql);
				$sql = "INSERT INTO routines_updated (routine_id, enabled) VALUES('$param1','0')";
				$db->query($sql);
			}
			break;

		case 'routine-enable':
			if ($param1 != "") {
				$sql = "UPDATE routine SET enabled='1' WHERE id='$param1'";
				$db->query($sql);

				#
				# Add row in routines_updated for Automatic Failover state
				#
				$sql = "DELETE FROM routines_updated WHERE routine_id='$param1'";
				$db->query($sql);
				$sql = "INSERT INTO routines_updated (routine_id, enabled) VALUES('$param1','1')";
				$db->query($sql);
			}
			break;

		case 'sm-scheduled-ramp':
			# $param1 = Zone
			# $param2 = Volume Adjustment (up|down)
			# $param3 = Duration (minutes)
			# $param4 = dB Range (numeric)
			exec("/usr/local/bin/sm-control -c scheduled-ramp -z $param1 -v $param2 -d $param3 -r $param4 > /dev/null 2>&1");
			break;

		case 'sm-disable-zone':
			exec("/usr/local/bin/sm-control -c disable-sm -z $param1 > /dev/null 2>&1");
			break;

		case 'sm-enable-zone':
			exec("/usr/local/bin/sm-control -c enable-sm -z $param1 > /dev/null 2>&1");
			break;

		case 'sm-set-output-gain':
			exec("/usr/local/bin/sm-control -c adjust-volume -z $param1 -v $param2 > /dev/null 2>&1");
			break;

		case 'stop-announcement':
			# $param1 = selected Zone to stop.
			if ($param1 != "") {
				executeAmiCall($debug, $action, $caller, "0000093$param1", "$param1", 5, $delay = "");
				# wait 5 seconds for announcement to stop ----------------------------^.
			}
			break;

		case 'tone':
			#
			# $param1 = tone.dial_number, $param2 = Zone
			#
			$waitTime = ($param3 != "") ? $param3 : 86400;

			$play_tone = true;

			if ($json_data != null) {
				if (isset($json_data['toneParams']) && is_array($json_data['toneParams'])) {

					$actionName = getActionName($debug, $db, $routineActionId);

					foreach ($json_data['toneParams'] as $toneParameters) {
						if ($toneParameters['name'] === $actionName) {
							$toneParams = $toneParameters;

							if (isset($toneParams['toneDtmf'])) {
								$new_value = $toneParams['toneDtmf'];
								if (!hasCommandInjection($new_value)) {
									if (toneDtmfExists($debug, $db, $new_value)) {
										$param1 = $new_value;
									} else {
										logText("Tone: Invalid DTMF: $new_value", ROUTINE_LOG_INTERNAL);
										$play_tone = false;
									}
								} else {
									logText("Tone: Invalid DTMF: $new_value", ROUTINE_LOG_INTERNAL);
									$play_tone = false;
								}
							}
							if (isset($toneParams['toneName'])) {
								$new_value = $toneParams['toneName'];
								if (!hasCommandInjection($new_value)) {
									if (toneNameExists($debug, $db, $new_value)) {
										$new_dtmf = getToneDtmfByName($debug, $db, $new_value);
										logText("Tone: new_dtmf = $new_dtmf");
										if ($new_dtmf != 0) {
											$param1 = $new_dtmf;
										} else {
											logText("Tone: Invalid Name: $new_value", ROUTINE_LOG_INTERNAL);
											$play_tone = false;
										}
									} else {
										logText("Tone: Invalid Name: $new_value", ROUTINE_LOG_INTERNAL);
										$play_tone = false;
									}
								} else {
									logText("Tone: Invalid Name: $new_value", ROUTINE_LOG_INTERNAL);
									$play_tone = false;
								}
							}
							if (isset($toneParams['toneZone'])) {
								$new_value = $toneParams['toneZone'];
								if (!hasCommandInjection($new_value)) {
									# make sure zone type includes Time type (2,3,6,7)
									$zone_type = getZoneType($debug,$db,$new_value);
									$allowed_zone_types = [2,3,6,7];
									if (in_array($zone_type, $allowed_zone_types) || ($new_value ==0)) {
										$param2 = $new_value;
									} else {
										logText("Tone: Invalid Zone: $new_value", ROUTINE_LOG_INTERNAL);
										$play_tone = false;
									}
								} else {
									logText("Tone: Invalid Zone: $new_value", ROUTINE_LOG_INTERNAL);
									$play_tone = false;
								}
							}
							if (isset($toneParams['duration'])) {
								$new_value = $toneParams['duration'];
								if (is_numeric($new_value) && $new_value > 0 && $new_value < 999999) {
									$waitTime = $new_value;
								} else {
									logText("Tone: Invalid duration: $new_value", ROUTINE_LOG_INTERNAL);
									$play_tone = false;
								}
							}
						}
					}
				}
			}

			if ($param2 != "") $param1.="*$param2";

			if ($play_tone) {
				executeAmiCall($debug, $action, $caller, "0000096$param1", $param2, $waitTime, $delay);
			}
			break;

		case 'urgent-call':
			$do_urgent_call = true;
			if ($json_data != null) {
				if (isset($json_data['urgentCallParams'])
					&& is_array($json_data['urgentCallParams'])) {

					$actionName = getActionName($debug, $db, $routineActionId);

					foreach ($json_data['urgentCallParams'] as $urgentCallParameters) {
						if ($urgentCallParameters['name'] === $actionName) {
							$urgentCallParams = $urgentCallParameters;

							if (isset($urgentCallParams['placedBy'])) {
								$new_value = $urgentCallParams['placedBy'];
								if (!hasCommandInjection($new_value)) {
									if (getStationType($debug,$db,$param1) != 0) {
										$param1 = $new_value;
									} else {
										logText("Urgent-Call: Invalid Placed By extension: $new_value", ROUTINE_LOG_INTERNAL);
										$do_urgent_call = false;
									}
								} else {
									logText("Urgent-Call: Invalid Placed By extension: $new_value", ROUTINE_LOG_INTERNAL);
									$do_urgent_call = false;
								}
							}
						}
					}
				}
			}
			if ($param1 == "") {
				if (($trigger == "API") || ($trigger == "scheduled-routine")) return;

				echo "*00";
				doExit(0);
			}

			if ($do_urgent_call) {
				intercomCall($debug, $db, $param1, "*00");
			}
			break;

		case 'webhook-post':
			webhookPost($debug, $db, $origCaller, $called, $param1, $param2, $param3, $param4);
			break;

		case 'zone-page':
			if (($trigger == "API") || ($trigger == "scheduled-routine")) return;

			echo "#$param1";
			doExit(0);
			break;

		default:
			if ($debug) echo "Action unknown: $action\n";
			break;
	}
}

function openAmiConnection($debug, $server)
{
	if (($amiSocket = fsockopen($server,"5038",$errno,$errstr,30)) == FALSE) {
		if ($debug)
			die("Can't connect to AMI");
		else
			doExit(1);
	}

	stream_set_blocking($amiSocket,TRUE);

	stream_set_timeout($amiSocket, 5);

	# Read Asterisk Call Manager banner (e.g. "Asterisk Call Manager/7.0.0")
	$retstr=fgets($amiSocket);

	fputs($amiSocket, "Action: Login\r\nUserName: routine\r\nSecret: BogenNyq1\r\n\r\n");

	#
	# Check login success
	#
	$retstr = fgets($amiSocket);

	if (strpos($retstr, "Response: Success") === false) {
		if ($debug)
			die("Login failed!");
		else
			doExit(1);
	}

	$retstr = fgets($amiSocket);

	if (strpos($retstr, "Message: Authentication accepted") === false) {
		if ($debug)
			die("Login failed!");
		else
			doExit(1);
	}

	return $amiSocket;
}


function notifyContactStates($debug, $action, $param1, $param2, $param3)
{
	global $amiSocket, $db, $server;

	$this_action = str_replace(' ', '-', ucwords(str_replace('-', ' ', $action)));

	if ($debug) echo "notifyContactStates($action, $param1, $param2, $param3)\n";

	$stationExt = $param1;

	if ($stationExt == "") {
		logText("$this_action: IO Controller not specified, aborting action", ROUTINE_LOG_INTERNAL);
		return;
	}

	if (!is_numeric($stationExt)) {
		logText("$this_action: Invalid IO Controller: $stationExt", ROUTINE_LOG_INTERNAL);
		return;
	}

	if (getStationType($debug, $db, $stationExt) != 15) {
		logText("$this_action: Invalid IO Controller: $stationExt", ROUTINE_LOG_INTERNAL);
		return;
	}

	if (!$amiSocket) {
		if (!($amiSocket = openAmiConnection($debug, $server)))
		{
			if ($debug) echo "AMI Connection not open!\n";
			return;
		}
	}

	stream_set_timeout($amiSocket, 1);

	$state = ($action == 'output-contact-close') ? 1 : 0;

	if (($param3 != "") && (!is_numeric($param3))) {
		logText("$this_action: Aborting, invalid duration: $param3", ROUTINE_LOG_INTERNAL);
		return;
	}

	$stateDuration = ($param3 != "") ? "2\;$param3" : "$state";

	$param2 = str_replace(' ', '', $param2);

	$contacts = explode(',' , $param2);

	foreach ($contacts as $contactNum) {
		if (!is_numeric($contactNum)) {
			logText("$this_action: Ignoring invalid contact number: $contactNum", ROUTINE_LOG_INTERNAL);
			continue;
		}
		if (($contactNum < 1) || ($contactNum > 8)) {
			logText("$this_action: Ignoring invalid contact number: $contactNum", ROUTINE_LOG_INTERNAL);
			continue;
		}
		#
		# Send NOTIFY to specified Nyquist IO Controller
		#
		fputs($amiSocket, "Action: PJSIPNotify\nActionID: 1\nEndpoint: $stationExt\nVariable: Event=$contactNum\;$stateDuration\n\n");

		$retstr=fgets($amiSocket);

		$loopCount = 0;
		while (strpos($retstr,": NOTIFY sent") === false) {
		        if (($retstr=fgets($amiSocket)) == FALSE) break;

			if (strpos($retstr,": Unable to retrieve endpoint") != 0) break;

		        # Prevent infinite loop
		        if ($loopCount++ > 20) break;
		}

	}
}

#
# sendMessage() - sends "SMS" messages to Admin Phones.
#
function sendMessage($targetExt, $message)
{
	global $amiSocket, $server, $origCaller;

	if (($targetExt == "") || ($message == "")) {
		return;
	}

	if (!$amiSocket) {
		if (!($amiSocket = openAmiConnection($debug, $server))) {
			return;
		}
	}

	$caller = $origCaller;

	fputs($amiSocket, "Action: Originate\r\nActionID: 1\r\nChannel: local/$targetExt@messages\r\nCallerId: $caller\r\nVariable: Caller=$caller\r\nVariable: ViaRoutine=1\r\nVariable: RoutineMsgBody=$message\r\nExten: $caller\r\nContext: messages\r\nPriority: 1\r\nTimeout: 30000\r\n\r\n");

}

function executeAmiCall($debug, $action, $caller, $exten, $zones, $waitTime, $delay)
{
	global $amiSocket, $server, $origCaller, $routine_dtmf_code;

	if ($debug) echo "executeAmiCall($action, $caller, $exten, $zones, $waitTime)\n";

	if (!$amiSocket) {
		if (!($amiSocket = openAmiConnection($debug, $server))) {
			if ($debug) echo "AMI Connection not open!\n";

			return;
		}
	}

	fputs($amiSocket, "Action: Originate\r\nActionID: 1\r\nChannel: local/$exten@routine\r\nCallerId: $caller\r\nVariable: Caller=$caller\r\nVariable: Zones=$zones\r\nVariable: WaitTime=$waitTime\r\nVariable: ViaRoutine=1\r\nVariable: RoutineDtmfCode=$routine_dtmf_code\r\nExten: $caller\r\nContext: routine_wait\r\nPriority: 1\r\nTimeout: 30000\r\nCodecs: g722\r\n\r\n");

	#
	# If $delay is "", no '&' was passed to indicate no-delay, so we delay
	#
	if ($delay == "") {
		switch ($action)
		{
			case 'alarm':
			case 'announcement':
			case 'disable-audio':
			case 'tone':
				sleep(2);
				waitForActionIdle($debug, $action, $origCaller, $waitTime);
				break;
			default:
				sleep($waitTime);
				break;
		}
	}
}

function executeAmiAllRemote($debug, $caller, $exten)
{
	global $amiSocket, $server, $origCaller;

	if ($debug) echo "executeAmiAllRemote($caller, $exten)\n";

	if (!$amiSocket) {
		if (!($amiSocket = openAmiConnection($debug, $server))) {
			if ($debug) echo "AMI Connection not open!\n";

			return;
		}
	}

	fputs($amiSocket, "Action: Originate\r\nActionID: 1\r\nChannel: local/$exten@routine_remote_all\r\nCallerId: $caller\r\nVariable: Caller=$caller\r\nVariable: WaitTime=15\r\nVariable: ViaRoutine=1\r\nExten: $caller\r\nContext: routine_wait2\r\nPriority: 1\r\nTimeout: 30000\r\nCodecs: g722\r\n\r\n");

	sleep(1);
}

function waitForActionIdle($debug, $action, $caller, $waitTime)
{
	global $db, $called;

	if ($debug) echo "waitForActionIdle($action,$waitTime)\n";

	$keepWaiting = TRUE;

	switch ($action)
	{
		case '911':
			do911Wait($debug, $caller, $waitTime);
			return;
			break;

		case 'alarm':
			$globVar = "ALARM_ENABLED";
			break;

		case 'all-call':
			$globVar = "CURRENT_PRIORITY";
			break;

		case 'announcement':
			$globVar = "ANNOUNCE_";
			break;

		case 'disable-audio':
			$globVar = "AUDIO_DISABLED";
			break;

		case 'facility-page':
			intercomWait($debug, $db, $caller, $called);
			return;
			break;

		case 'emerg-all-call':
			$globVar = " EMERGENCY_ALLCALL";
			break;

		case 'multi-site-all-call':
			$globVar = "DIST_ALLCALL";
			break;

		case 'multi-site-e-all-call':
			$globVar = "DIST_EMERG_ALLCALL";
			break;

		case 'night-ring':
			$globVar = "NIGHTRING_ACTIVE";
			break;

		case 'tone':
			$globVar = "TONE_ENABLED";
			break;

		case 'zone-page':
			waitForZonePage($debug, $caller, $waitTime);
			return;
			break;

		default:
			sleep($waitTime);
			return;
			break;
	}

	$waitedTime = 1;

	do {
		sleep(1);

		exec("/usr/sbin/asterisk -rx \"dialplan show globals\" | grep $globVar", $out, $ret);

		if (count($out) == 0) return;

		if ($debug) {
			if (count($out) == 0) {
				echo "Global status: $out[0]\n";
			} else {
				$eline = 0;
				foreach ($out as $echoStatus) {
					echo "Global status($eline): $echoStatus\n";
					$eline++;
				}
			}
		}

		switch ($action)
		{
			case 'all-call':
				$status = explode('=',$out[0]);
				if ($status[1] != 50) $keepWaiting = FALSE;
				break;

			case 'zone-page':
				# Should not get here, using waitForZonePage()
				$status = explode('=',$out[0]);
				if ($status[1] != 40) $keepWaiting = FALSE;
				break;

			case 'announcement':
				$foundActive = FALSE;
				if (count($out) > 1) {
					foreach ($out as $globVarStatus) {
						$status = explode('=',$globVarStatus);
						if ($status[1] == 1) {
							$foundActive = TRUE;
						}
					}
				}
				if ($foundActive == FALSE) $keepWaiting = FALSE;
				break;

			case 'alarm':
			case 'disable-audio':
			case 'emerg-all-call':
			case 'multi-site-all-call':
			case 'multi-site-e-all-call':
			case 'night-ring':
			case 'tone':
				$status = explode('=',$out[0]);
				if ($status[1] == 0) $keepWaiting = FALSE;
				break;

			default:
				return;
				break;
		}

		unset($out);

		if ($waitedTime++ > $waitTime) {
			$keepWaiting = FALSE;
		}

	} while ($keepWaiting == TRUE);
}

function waitForZonePage($debug, $caller, $waitTime)
{
	global $db, $zones;

	$keepWaiting = TRUE;

	$sql = "SELECT zone_number FROM zone_status
		WHERE zone_number='$zones' AND priority='40'";

	$waitedTime = 0;

	do {
		if ($result = $db->query($sql)) {
			if ($result->num_rows == 0) {
				$keepWaiting = FALSE;
			} else {
				$result->free();

				sleep(1);

				if ($waitedTime++ > $waitTime) {
					$keepWaiting = FALSE;;
				}
			}
		} else {
			$keepWaiting = FALSE;
		}

	} while ($keepWaiting == TRUE);
}

function do911Wait($debug, $caller, $waitTime)
{
	global $db;

	$keepWaiting = TRUE;

	$sql = "SELECT id FROM dashboard_msg
		WHERE type='911-popup' AND extension='$caller'";

	$waitedTime = 0;

	do {
		if ($result = $db->query($sql)) {
			if ($result->num_rows == 0) {
				$keepWaiting = FALSE;
			} else {
				$result->free();

				sleep(1);

				if ($waitedTime++ > $waitTime) {
					$keepWaiting = FALSE;;
				}
			}
		} else {
			$keepWaiting = FALSE;
		}

	} while ($keepWaiting == TRUE);
}

function callAndAnnounce($debug, $caller, $extension, $announcement, $playCount)
{
	global	$myPid;

	if ($debug) echo "callAndAnnounce($extension, $announcement, $playCount)\n";

	if (!is_numeric($extension)) return;
	if (!is_numeric($playCount)) return;

	$callFilename = "call-and-announce-$caller-$myPid.call";

	if (preg_match("/\/var\/lib\/asterisk\/sounds\/user\/announcements\/(.*)\.(.*)/", $announcement, $parts)) {
		$announcement = "/var/lib/asterisk/sounds/user/announcements/gsm/$parts[1]";

		if ($debug) {
			echo "Announcement: $announcement\n";
			print_r($parts);
		}

		#
		# Bail out if announcement file does not exist
		#
		if (!file_exists("$announcement.g722")) return;
	} else {
		#
		# Bail out if we can't process the announcement file syntax
		#
		return;
	}

	$callData = "Channel: local/100@auto_announce_dial
Callerid: $caller
Setvar: DialExten=$extension
Setvar: Announcement=$announcement
Setvar: RepeatCount=$playCount
WaitTime: 60
MaxRetries: 5
RetryTime: 5
Context: auto_announce_play
Extension: 100
Priority: 1";

createCallFile($debug, $callData, $callFilename);

}

function createCallFile($debug, $callData, $callFilename)
{
	if ($debug) echo "createCallFile($callFilename):\n$callData\n";

	file_put_contents("/tmp/$callFilename", $callData);

	#
	# Note: mv is used to ensure that Asterisk has access to the entire
	#	file when it starts processing it, using other methods
	#	might result in Asterisk processing an incomplete portion
	#	of the file.
	#
	exec("/bin/mv -f /tmp/$callFilename /var/spool/asterisk/outgoing/$callFilename > /dev/null 2>&1");
}

function closeAmiConnection($debug)
{
	global $amiSocket;

	fputs($amiSocket, "Action: Logoff\r\n\r\n");

	$retstr=fgets($amiSocket);

	stream_set_timeout($amiSocket, 1);

	$loopCount = 0;
	while (strpos($retstr,"Goodbye") === false) {
		if (($retstr=fgets($amiSocket)) == FALSE) {
			if (feof($amiSocket) == TRUE) return NULL;
		}

		if ($debug) echo $retstr;

		if ($loopCount++ > 10) break;
	}

	fclose($amiSocket);
}

function webhookPost($debug, $db, $caller, $called, $param1, $param2, $param3, $param4)
{
	global	$cmdParam1,
		$cmdParam2,
		$callerName,
		$calledName,
		$extra_args,
		$json_data,
		$pDate1,
		$pDate2,
		$pDate3,
		$pTime1,
		$pTime2,
		$trigger,
		$user_var,
		$webhook_return_str,
		$zones,
		$zonesProvided;

	if ($debug) echo "webhookPost($caller,$called,$param1,$param2,$param3,$param4)\n";

	if ($param1 == "") {
		return;
	}

	$param1 = base64_decode($param1);

	if ($param1 == "") {
		return;
	}

	if ((strpos($param1,'`') !== false) || (strpos($param1,"$(") !== false)) {
		return;
	}

	if (strpos($param1, 'curl ') === false) {
		if (strpos($param1, 'wget ') === false) {
			return;
		} else {
			if (strpos($param1, 'wget ') !== 0) {
				return;
			}
		}
	} else {
		if (strpos($param1, 'curl ') !== 0) {
			return;
		}
	}

	if ($caller == 0) {
		$param1 = str_replace('$caller',"System",$param1);
		$param1 = str_replace('$namecaller',"System",$param1);
	} else {
		if ($trigger != "incoming-call") {
			$param1 = str_replace('$caller',$caller,$param1);
		} else {
			$param1 = str_replace('$caller',$cmdParam1,$param1);
		}

		$param1 = str_replace('$namecaller',$callerName,$param1);
	}

	$param1 = str_replace('$called',$called,$param1);
	$param1 = str_replace('$namecalled',$calledName,$param1);

	#
	# Replace user supplied variables with current values
	#
	# $param1 = Content to POST
	#
	switch ($trigger)
	{
		case 'API':
			if ($json_data != null) {
				for ($i = 20; $i > 0; $i--) {
					$paramKey = 'apiParam'.$i;
					if (isset($json_data['routineApiParams'][$paramKey])) {
						$placeholder = '$'.$paramKey;
						$param1 = str_replace($placeholder, $json_data['routineApiParams'][$paramKey], $param1);
					}
				}
			}
			$param1 = str_replace('$apiParam1',$cmdParam1,$param1);
			$param1 = str_replace('$apiParam2',$cmdParam2,$param1);
			$param1 = replaceCapData($param1);
			break;

		case 'input-contact-closed':
		case 'input-contact-opened':
			$param1 = str_replace('$station',$cmdParam1,$param1);
			$param1 = str_replace('$contact',$cmdParam2,$param1);
			break;

		case 'facility-status-up':
		case 'facility-status-down':
		case 'facility-page':
			$fname = getFacilityName($debug, $db, $cmdParam1);
			$param1 = str_replace('$facility',$fname,$param1);
			break;

		case 'amp-protection-mode':
			$devName = getDeviceNameByMacAddr($cmdParam1);
			$param1 = str_replace('$deviceMacAddr',$cmdParam1,$param1);
			$param1 = str_replace('$deviceName',$devName,$param1);
			break;

		case 'nws-alert':
			$param1 = str_replace('$nwsAlertEvent', $cmdParam1,$param1);
			$param1 = str_replace('$nwsAlertSeverity', $cmdParam2,$param1);
			$param1 = str_replace('$nwsAlertCertainty', $extra_args[1],$param1);
			$param1 = str_replace('$nwsAlertUrgency', $extra_args[2],$param1);
			$param1 = str_replace('$nwsAlertResponse', $extra_args[3],$param1);
			$param1 = str_replace('$nwsAlertZone', $extra_args[4],$param1);
			break;

		case 'schedule-activated':
			$sname = getScheduleName($debug, $db, $cmdParam1);
			$param1 = str_replace('$scheduleid',$cmdParam1,$param1);
			$param1 = str_replace('$schedulename',$sname,$param1);
			break;

		case 'scheduled-routine':
			$param1 = str_replace('$schedParam1',$cmdParam1,$param1);
			$param1 = str_replace('$schedParam2',$cmdParam2,$param1);

			if ($zonesProvided)
			{
				$zname = getZoneName($debug, $db, $zones);
				$param1 = str_replace('$zone',$zname,$param1);
			}
			break;

		case 'sched-event':
			$ename = getEventName($debug, $db, $cmdParam1);
			$param1 = str_replace('$eventid',$cmdParam1,$param1);
			$param1 = str_replace('$eventname',$ename,$param1);
			break;

		case 'scheduled-tone':
			$tname = getToneName($debug, $db, $cmdParam1);
			$param1 = str_replace('$toneid',$cmdParam1,$param1);
			$param1 = str_replace('$tonename',$tname,$param1);
			break;

		case 'station-status-up':
		case 'station-status-down':
			$param1 = str_replace('$station',$cmdParam1,$param1);
			break;

		case 'announcement':
		case 'zone-page':
			$zname = getZoneName($debug, $db, $zones);
			$param1 = str_replace('$zone',$zname,$param1);
			break;

		default:
			$param1 = str_replace('$station','N/A',$param1);
			$param1 = str_replace('$contact','N/A',$param1);
			$param1 = str_replace('$eventid','N/A',$param1);
			$param1 = str_replace('$eventname','N/A',$param1);
			$param1 = str_replace('$facility','N/A',$param1);
			$param1 = str_replace('$zone','N/A',$param1);
			break;
	}

	$param1 = str_replace('$date1', $pDate1, $param1);
	$param1 = str_replace('$date2', $pDate2, $param1);
	$param1 = str_replace('$date3', $pDate3, $param1);
	$param1 = str_replace('$time1', $pTime1, $param1);
	$param1 = str_replace('$time2', $pTime2, $param1);

	foreach ($user_var as $key => $u_var) {
		$user_var_str = '$user_var['.$key.']';

		$param1 = str_replace("$user_var_str",$user_var[$key],$param1);
	}

	#
	# Process user supplied data from JSON file
	#
	if ($json_data != null) {
		$param1 = preg_replace_callback('/{{\s*(\w+(\.\w+)*)\s*}}/',
			   function($matches) use ($json_data) {
				$variable = $matches[1];
				$value = getNestedValue($json_data, $variable);
				return $value !== null ? $value : $matches[0];
			   }, $param1);
	}

	if (strpos($param1, '$cputemp') !== false) {
		if ($cputemp == "") $cputemp = getMaxCpuTemp();

		$param1 = str_replace('$cputemp', $cputemp, $param1);
	}

	if (strpos($param1, '$df-avail') !== false) {
		exec("/bin/df -h --output=avail /", $dfout, $ret);
		if ($ret == 0) {
			$param1 = str_replace('$df-avail',trim($dfout[1]),$param1);
		}
		unset($dfout);
	}

	if (strpos($param1, '$uptime') !== false) {
		exec("/usr/bin/uptime -p", $uptimeout, $ret);
		if ($ret == 0) {
			$param1 = str_replace('$uptime',$uptimeout[0],$param1);
		}
		unset($uptimeout);
	}

	if ((strpos($param1,'`') !== false) || (strpos($param1,"$(") !== false)) {
		return;
	}

	if ($debug) {
		echo "POST Content:\n$param1";
	}

	#
	# Create "sandbox" to run webhook command:
	#
	exec("/bin/mktemp -d /tmp/webhook-execute-XXXXXXXXXX", $out, $ret);

	if ($ret !== 0) return;

	$dirName = $out[0];

	if ($dirName == "") return;

	chown($dirName, "nobody");

	symlink("/usr/bin/curl", "$dirName/curl");

	symlink("/usr/bin/wget", "$dirName/wget");

	$scriptName = $dirName."/dowebhook";

	file_put_contents($scriptName, "#!/bin/rbash\n$param1\n");

	chmod($scriptName, 0700);

	chown($scriptName, "nobody");

	#
	# Run webhook command:
	#
	exec("export PATH=$dirName;cd $dirName;/usr/bin/sudo --preserve-env=PATH,PWD --user=nobody $scriptName >post_return.txt 2>/dev/null");

	$webhook_return_str = "";

	if (file_exists("$dirName/post_return.txt")) {
		if (filesize("$dirName/post_return.txt") < 33554432) {
			$webhook_return_str = file_get_contents("$dirName/post_return.txt");
		}
	}

	if ($debug) echo "\nPOST return str: $webhook_return_str\n";

	#
	# Being paranoid removing files and dirctory.
	#
	if (strpos($dirName, "/tmp/webhook-execute-") !== false) {
		if (strpos($dirName, "/tmp/webhook-execute-") === 0) {
			if (strpos($dirName, " ") === false) {
				exec("/bin/rm -f $dirName/* > /dev/null 2>&1");
				rmdir($dirName);
			}
		}
	}
}

function displayDashText($debug, $db, $caller, $called, $param1, $param2, $param3, $param4)
{
	global	$dashId,
		$cap_data,
		$cmdParam1,
		$cmdParam2,
		$callerName,
		$calledName,
		$extra_args,
		$json_data,
		$pDate1,
		$pDate2,
		$pDate3,
		$pTime1,
		$pTime2,
		$trigger,
		$user_var,
		$zones,
		$zonesProvided;

	if ($debug) echo "displayDashText($caller,$called,$param1,$param2,$param3,$param4)\n";

	if ($param3 == "") $param3 = "All";

	if ($caller == 0) {
		$param4 = str_replace('$caller',"System",$param4);
		$param2 = str_replace('$caller',"System",$param2);
		$param4 = str_replace('$namecaller',"System",$param4);
		$param2 = str_replace('$namecaller',"System",$param2);
	} else {
		if ($trigger != "incoming-call") {
			$param4 = str_replace('$caller',$caller,$param4);
			$param2 = str_replace('$caller',$caller,$param2);
		} else {
			$param4 = str_replace('$caller',$cmdParam1,$param4);
			$param2 = str_replace('$caller',$cmdParam1,$param2);
		}

		$param4 = str_replace('$namecaller',$callerName,$param4);
		$param2 = str_replace('$namecaller',$callerName,$param2);
	}

	$param4 = str_replace('$called',$called,$param4);
	$param2 = str_replace('$called',$called,$param2);
	$param4 = str_replace('$namecalled',$calledName,$param4);
	$param2 = str_replace('$namecalled',$calledName,$param2);

	#
	# Replace user supplied variables with current values
	#
	# $param2 = Identifier (dashboard_msg.identifier)
	# $param4 = Text (dashboard_msg.text)
	#
	switch ($trigger)
	{
		case 'API':
			if ($json_data != null) {
				for ($i = 20; $i > 0; $i--) {
					$paramKey = 'apiParam'.$i;
					if (isset($json_data['routineApiParams'][$paramKey])) {
						$placeholder = '$'.$paramKey;
						$param2 = str_replace($placeholder, $json_data['routineApiParams'][$paramKey], $param2);
						$param4 = str_replace($placeholder, $json_data['routineApiParams'][$paramKey], $param4);
					}
				}
			}
			$param2 = str_replace('$apiParam1',$cmdParam1,$param2);
			$param2 = str_replace('$apiParam2',$cmdParam2,$param2);
			$param4 = str_replace('$apiParam1',$cmdParam1,$param4);
			$param4 = str_replace('$apiParam2',$cmdParam2,$param4);

			$param2 = replaceCapData($param2);
			$param4 = replaceCapData($param4);

			break;

		case 'input-contact-closed':
		case 'input-contact-opened':
			$param2 = str_replace('$station',$cmdParam1,$param2);
			$param2 = str_replace('$contact',$cmdParam2,$param2);
			$param4 = str_replace('$station',$cmdParam1,$param4);
			$param4 = str_replace('$contact',$cmdParam2,$param4);
			break;

		case 'facility-status-up':
		case 'facility-status-down':
		case 'facility-page':
			$fname = getFacilityName($debug, $db, $cmdParam1);
			$param2 = str_replace('$facility',$fname,$param2);
			$param4 = str_replace('$facility',$fname,$param4);
			break;

		case 'amp-protection-mode':
			$devName = getDeviceNameByMacAddr($cmdParam1);
			$param4 = str_replace('$deviceMacAddr',$cmdParam1,$param4);
			$param4 = str_replace('$deviceName',$devName,$param4);

			$param2 = str_replace('$deviceMacAddr',$cmdParam1,$param2);
			break;
		case 'nws-alert':
			$param2 = str_replace('$nwsAlertEvent', $cmdParam1,$param2);
			$param2 = str_replace('$nwsAlertSeverity', $cmdParam2,$param2);
			$param2 = str_replace('$nwsAlertCertainty', $extra_args[1],$param2);
			$param2 = str_replace('$nwsAlertUrgency', $extra_args[2],$param2);
			$param2 = str_replace('$nwsAlertResponse', $extra_args[3],$param2);
			$param2 = str_replace('$nwsAlertZone', $extra_args[4],$param2);

			$param4 = str_replace('$nwsAlertEvent', $cmdParam1,$param4);
			$param4 = str_replace('$nwsAlertSeverity', $cmdParam2,$param4);
			$param4 = str_replace('$nwsAlertCertainty', $extra_args[1],$param4);
			$param4 = str_replace('$nwsAlertUrgency', $extra_args[2],$param4);
			$param4 = str_replace('$nwsAlertResponse', $extra_args[3],$param4);
			$param4 = str_replace('$nwsAlertZone', $extra_args[4],$param4);
			break;

		case 'schedule-activated':
			$sname = getScheduleName($debug, $db, $cmdParam1);
			$param2 = str_replace('$scheduleid',$cmdParam1,$param2);
			$param4 = str_replace('$schedulename',$sname,$param4);
			break;

		case 'scheduled-routine':
			$param2 = str_replace('$schedParam1',$cmdParam1,$param2);
			$param2 = str_replace('$schedParam2',$cmdParam2,$param2);
			$param4 = str_replace('$schedParam1',$cmdParam1,$param4);
			$param4 = str_replace('$schedParam2',$cmdParam2,$param4);

			if ($zonesProvided)
			{
				$zname = getZoneName($debug, $db, $zones);
				$param2 = str_replace('$zone',$zname,$param2);
				$param4 = str_replace('$zone',$zname,$param4);
			}
			break;

		case 'sched-event':
			$ename = getEventName($debug, $db, $cmdParam1);
			$param2 = str_replace('$eventid',$cmdParam1,$param2);
			$param2 = str_replace('$eventname',$ename,$param2);
			$param4 = str_replace('$eventid',$cmdParam1,$param4);
			$param4 = str_replace('$eventname',$ename,$param4);
			break;

		case 'scheduled-tone':
			$tname = getToneName($debug, $db, $cmdParam1);
			$param2 = str_replace('$toneid',$cmdParam1,$param2);
			$param2 = str_replace('$tonename',$tname,$param2);
			$param4 = str_replace('$toneid',$cmdParam1,$param4);
			$param4 = str_replace('$tonename',$tname,$param4);
			break;

		case 'station-status-up':
		case 'station-status-down':
			$param2 = str_replace('$station',$cmdParam1,$param2);
			$param4 = str_replace('$station',$cmdParam1,$param4);
			break;

		case 'announcement':
		case 'zone-page':
			$zname = getZoneName($debug, $db, $zones);
			$param2 = str_replace('$zone',$zname,$param2);
			$param4 = str_replace('$zone',$zname,$param4);
			break;

		default:
			$param4 = str_replace('$station','N/A',$param4);
			$param4 = str_replace('$contact','N/A',$param4);
			$param4 = str_replace('$eventid','N/A',$param4);
			$param4 = str_replace('$eventname','N/A',$param4);
			$param4 = str_replace('$facility','N/A',$param4);
			$param4 = str_replace('$zone','N/A',$param4);
			break;
	}

	$param4 = str_replace('$date1', $pDate1, $param4);
	$param4 = str_replace('$date2', $pDate2, $param4);
	$param4 = str_replace('$date3', $pDate3, $param4);
	$param4 = str_replace('$time1', $pTime1, $param4);
	$param4 = str_replace('$time2', $pTime2, $param4);

	foreach ($user_var as $key => $u_var) {
		$user_var_str = '$user_var['.$key.']';

		$param2 = str_replace("$user_var_str",$user_var[$key],$param2);
		$param4 = str_replace("$user_var_str",$user_var[$key],$param4);
	}

	#
	# Process user supplied data from JSON file
	#
	if ($json_data != null) {
		$param4 = preg_replace_callback('/{{\s*(\w+(\.\w+)*)\s*}}/',
			   function($matches) use ($json_data) {
				$variable = $matches[1];
				$value = getNestedValue($json_data, $variable);
				return $value !== null ? $value : $matches[0];
			   }, $param4);
	}

	if (strpos($param4, '$cputemp') !== false) {
		if ($cputemp == "") $cputemp = getMaxCpuTemp();

		$param4 = str_replace('$cputemp', $cputemp, $param4);
	}

	if (strpos($param4, '$df-avail') !== false) {
		exec("/bin/df -h --output=avail /", $dfout, $ret);
		if ($ret == 0) {
			$param4 = str_replace('$df-avail',trim($dfout[1]),$param4);
		}
		unset($dfout);
	}

	if (strpos($param4, '$uptime') !== false) {
		exec("/usr/bin/uptime -p", $uptimeout, $ret);
		if ($ret == 0) {
			$param4 = str_replace('$uptime',$uptimeout[0],$param4);
		}
		unset($uptimeout);
	}

	#
	# Process $weather variable
	#
	if (strpos($param4, '$weather') !== false) {
		$wstr = "";

		$weather_units = "imperial";
		$weather_toggle_decimal_digit = "";

		if (preg_match("/weather_units_metric/",$param4,$parts)) {
			$weather_units = "metric";
			$param4 = preg_replace('/weather_units_metric/','',$param4);
		}

		if (preg_match("/weather_toggle_decimal_digit/",$param4,$parts)) {
			$weather_toggle_decimal_digit = " -t";
			$param4 = preg_replace('/weather_toggle_decimal_digit/','',$param4);
		}

		if (preg_match("/weather_zipcode_([0-9]{5})/",$param4,$parts)) {
			$weather_zipcode = $parts[1];
			$param4 = preg_replace('/weather_zipcode_([0-9]{5})/','',$param4);
			exec("/usr/local/bin/weather -u $weather_units -z $weather_zipcode".$weather_toggle_decimal_digit, $wout, $ret);
		} elseif (preg_match("/weather_latlon_(.*),(.*)#/",$param4,$parts)) {
			$weather_lat = $parts[1];
			$weather_lon = $parts[2];
			$param4 = preg_replace('/weather_latlon_(.*),(.*)#/','',$param4);
			exec("/usr/local/bin/weather -u $weather_units -L $weather_lat -l $weather_lon".$weather_toggle_decimal_digit, $wout, $ret);
		} else {
			exec("/usr/local/bin/weather -u $weather_units ".$weather_toggle_decimal_digit, $wout, $ret);
		}


		if ($ret == 0) {
			$wstr = "$wout[0], $wout[1], $wout[2], $wout[3]";
			$param4 = str_replace('$weather',$wstr,$param4);
		}
		unset($wout);
	}

	$sql = "INSERT INTO dashboard_msg
		SET type='text',message_style='$param1',text='$param4',
		popup_title='Text',scope='$param3',action='Delete',
		extension='$caller',identifier='$param2'";

	if ($debug) echo "DashSQL: $sql\n";

	$db->query($sql);

	$dashId[] = mysqli_insert_id($db);
}

function deleteDashMsg($debug, $db, $caller, $called, $param1, $param2)
{
	global	$dashId,
		$cmdParam1,
		$cmdParam2,
		$extra_args,
		$trigger,
		$user_var,
		$zones,
		$zonesProvided;

	$deleteId = "";

	if ($param1 == 'IDENT') {
		if ($param2 == "") return;

		if ($caller == 0) {
			$param2 = str_replace('$caller',"System",$param2);
		} else {
			$param2 = str_replace('$caller',$caller,$param2);
		}

		$param2 = str_replace('$called',$called,$param2);

		foreach ($user_var as $key => $u_var) {
			$user_var_str = '$user_var['.$key.']';
			$param2 = str_replace("$user_var_str",$user_var[$key],$param2);
		}

		#
		# Replace user supplied variables with current values
		#
		switch ($trigger)
		{
			case 'API':
				if ($json_data != null) {
					for ($i = 20; $i > 0; $i--) {
						$paramKey = 'apiParam'.$i;
						if (isset($json_data['routineApiParams'][$paramKey])) {
							$placeholder = '$'.$paramKey;
							$param2 = str_replace($placeholder, $json_data['routineApiParams'][$paramKey], $param2);
						}
					}
				}
				$param2 = str_replace('$apiParam1',$cmdParam1,$param2);
				$param2 = str_replace('$apiParam2',$cmdParam2,$param2);

				$param2 = replaceCapData($param2);
				break;

			case 'input-contact-closed':
			case 'input-contact-opened':
				$param2 = str_replace('$contact',$cmdParam2,$param2);
				$param2 = str_replace('$station',$cmdParam1,$param2);
				break;

			case 'facility-status-up':
			case 'facility-status-down':
			case 'facility-page':
				$fname = getFacilityName($debug, $db, $cmdParam1);
				$param2 = str_replace('$facility',$fname,$param2);
				break;

			case 'amp-protection-mode':
				$param2 = str_replace('$deviceMacAddr',$cmdParam1,$param2);
				break;

			case 'nws-alert':
				$param2 = str_replace('$nwsAlertEvent', $cmdParam1,$param2);
				$param2 = str_replace('$nwsAlertSeverity', $cmdParam2,$param2);
				$param2 = str_replace('$nwsAlertCertainty', $extra_args[1],$param2);
				$param2 = str_replace('$nwsAlertUrgency', $extra_args[2],$param2);
				$param2 = str_replace('$nwsAlertResponse', $extra_args[3],$param2);
				$param2 = str_replace('$nwsAlertZone', $extra_args[4],$param2);
				break;

			case 'schedule-activated':
				$param2 = str_replace('$scheduleid',$cmdParam1,$param2);
				break;

			case 'scheduled-routine':
				$param2 = str_replace('$schedParam1',$cmdParam1,$param2);
				$param2 = str_replace('$schedParam2',$cmdParam2,$param2);

				if ($zonesProvided)
				{
					$zname = getZoneName($debug, $db, $zones);
					$param2 = str_replace('$zone',$zname,$param2);
				}
				break;

			case 'sched-event':
				$ename = getEventName($debug, $db, $cmdParam1);
				$param2 = str_replace('$eventid',$cmdParam1,$param2);
				$param2 = str_replace('$eventname',$ename,$param2);
				break;

			case 'scheduled-tone':
				$tname = getToneName($debug, $db, $cmdParam1);
				$param2 = str_replace('$toneid',$cmdParam1,$param2);
				$param2 = str_replace('$tonename',$tname,$param2);
				break;

			case 'station-status-up':
			case 'station-status-down':
				$param2 = str_replace('$station',$cmdParam1,$param2);
				break;

			case 'announcement':
			case 'zone-page':
				$zname = getZoneName($debug, $db, $zones);
				$param2 = str_replace('$zone',$zname,$param2);
				break;

			default:
				break;
		}

		$sql="DELETE FROM dashboard_msg WHERE identifier='$param2'";

		$db->query($sql);

		return;
	}
	else
	if ((count($dashId) != 0)) {
		switch ($param1)
		{
			case 'FIFO':
				$deleteId = array_shift($dashId);
				break;

			case 'LIFO':
				$deleteId = array_pop($dashId);
				break;

			case 'NUMBER':
				if (is_numeric($param2) && ($param2 > 0)) {
					if (isset($dashId[($param2 - 1)])) {
						$deleteId = $dashId[($param2 - 1)];

						# The following code would
						# remove element and compress
						# array, changing numbering
						#unset($dashId[($param2 - 1)]);
						#$dashId = array_values($dashId);
					}
				}
				break;

			default:
				break;
		}
	}

	if ($deleteId != "") {

		$sql = "DELETE FROM dashboard_msg WHERE id='$deleteId'";

		$db->query($sql);
	}
}

#
# Log message to /var/log/syslog via logger
#
function logSyslog(int $id, string $facility, string $priority, string $text): void
{
    // Reject empty values
    if ($id <= 0 || $facility === '' || $priority === '' || $text === '') {
        return;
    }

    // Allowed facilities for your application
    $validFacilities = [
        'user','local0','local1','local2','local3','local4','local5','local6','local7'
    ];

    // Standard syslog priorities
    $validPriorities = [
        'emerg','alert','crit','err','warning','notice','info','debug'
    ];

    // Validate facility + priority
    if (!in_array($facility, $validFacilities, true)) {
        return;
    }
    if (!in_array($priority, $validPriorities, true)) {
        return;
    }

    // Build tag: nyquist-routine-<id>
    $tag = "nyquist-routine-$id";

    // Build logger command
    $cmd = sprintf(
        '/usr/bin/logger -t %s -p %s.%s %s',
        escapeshellarg($tag),
        escapeshellarg($facility),
        escapeshellarg($priority),
        escapeshellarg($text)
    );

    exec($cmd);
}

#
# Log message to nyquist_routines.log file or syslog.
#
# $logUser == ROUTINE_LOG_SYSLOG will log text to syslog by calling logSyslog()
# after variable substitutions.
#
# $param1 is text to log (including variables to be replaced with current values)
#
function logText(
	string $param1,
	int $logUser = ROUTINE_LOG_USER,
	?int $id = null,
	?string $facility = null,
	?string $priority = null): void
{
	global	$debug,
		$db,
		$caller,
		$called,
		$cmdParam1,
		$cmdParam2,
		$callerName,
		$calledName,
		$extra_args,
		$json_data,
		$routineName,
		$myPid,
		$trigger,
		$user_var,
		$zones,
		$zonesProvided;

	if ($debug) echo "logText($param1)\n";

	if ($param1 == "") return;

	#
	# If this is an internal log request (not user requested), then log
	# the message and return; no need to perform processing of variables.
	#
	if ($logUser == ROUTINE_LOG_INTERNAL) {
		$logText = date("Y-m-d H:i:s ").str_pad($myPid,6)."[$routineName] $param1\n";

		file_put_contents("/var/log/asterisk/nyquist_routines.log", $logText, FILE_APPEND);

		return;
	}

	#
	# Don't allow log file to grow larger than 1/2 GB
	#
	if (filesize("/var/log/asterisk/nyquist_routines.log") > 536870912) return;

	if ($caller == 0) {
		$param1 = str_replace('$caller',"System",$param1);
		$param1 = str_replace('$namecaller',"System",$param1);
	} else {
		if ($trigger != "incoming-call") {
			$param1 = str_replace('$caller',$caller,$param1);
		} else {
			$param1 = str_replace('$caller',$cmdParam1,$param1);
		}

		$param1 = str_replace('$namecaller',$callerName,$param1);
	}

	$param1 = str_replace('$called',$called,$param1);
	$param1 = str_replace('$namecalled',$calledName,$param1);

	#
	# Replace user supplied variables with current values
	#
	# $param1 = Text to log
	#
	switch ($trigger)
	{
		case 'API':
			if ($json_data != null) {
				for ($i = 20; $i > 0; $i--) {
					$paramKey = 'apiParam'.$i;
					if (isset($json_data['routineApiParams'][$paramKey])) {
						$placeholder = '$'.$paramKey;
						$param1 = str_replace($placeholder, $json_data['routineApiParams'][$paramKey], $param1);
					}
				}
			}
			$param1 = str_replace('$apiParam1',$cmdParam1,$param1);
			$param1 = str_replace('$apiParam2',$cmdParam2,$param1);
			$param1 = replaceCapData($param1);
			break;

		case 'input-contact-closed':
		case 'input-contact-opened':
			$param1 = str_replace('$station',$cmdParam1,$param1);
			$param1 = str_replace('$contact',$cmdParam2,$param1);
			break;

		case 'facility-status-up':
		case 'facility-status-down':
		case 'facility-page':
			$fname = getFacilityName($debug, $db, $cmdParam1);
			$param1 = str_replace('$facility',$fname,$param1);
			break;

		case 'amp-protection-mode':
			$devName = getDeviceNameByMacAddr($cmdParam1);
			$param1 = str_replace('$deviceMacAddr',$cmdParam1,$param1);
			$param1 = str_replace('$deviceName',$devName,$param1);
			break;

		case 'nws-alert':
			$param1 = str_replace('$nwsAlertEvent', $cmdParam1,$param1);
			$param1 = str_replace('$nwsAlertSeverity', $cmdParam2,$param1);
			$param1 = str_replace('$nwsAlertCertainty', $extra_args[1],$param1);
			$param1 = str_replace('$nwsAlertUrgency', $extra_args[2],$param1);
			$param1 = str_replace('$nwsAlertResponse', $extra_args[3],$param1);
			$param1 = str_replace('$nwsAlertZone', $extra_args[4],$param1);
			break;

		case 'schedule-activated':
			$sname = getScheduleName($debug, $db, $cmdParam1);
			$param1 = str_replace('$scheduleid',$cmdParam1,$param1);
			$param1 = str_replace('$schedulename',$sname,$param1);
			break;

		case 'scheduled-routine':
			$param1 = str_replace('$schedParam1',$cmdParam1,$param1);
			$param1 = str_replace('$schedParam2',$cmdParam2,$param1);

			if ($zonesProvided) {
				$zname = getZoneName($debug, $db, $zones);
				$param1 = str_replace('$zone',$zname,$param1);
			}
			break;

		case 'sched-event':
			$ename = getEventName($debug, $db, $cmdParam1);
			$param1 = str_replace('$eventid',$cmdParam1,$param1);
			$param1 = str_replace('$eventname',$ename,$param1);
			break;

		case 'scheduled-tone':
			$tname = getToneName($debug, $db, $cmdParam1);
			$param1 = str_replace('$toneid',$cmdParam1,$param1);
			$param1 = str_replace('$tonename',$tname,$param1);
			break;

		case 'station-status-up':
		case 'station-status-down':
			$param1 = str_replace('$station',$cmdParam1,$param1);
			break;

		case 'announcement':
		case 'zone-page':
			$zname = getZoneName($debug, $db, $zones);
			$param1 = str_replace('$zone',$zname,$param1);
			break;

		default:
			$param1 = str_replace('$station','N/A',$param1);
			$param1 = str_replace('$contact','N/A',$param1);
			$param1 = str_replace('$eventid','N/A',$param1);
			$param1 = str_replace('$eventname','N/A',$param1);
			$param1 = str_replace('$facility','N/A',$param1);
			$param1 = str_replace('$zone','N/A',$param1);
			break;
	}

	foreach ($user_var as $key => $u_var) {
		$user_var_str = '$user_var['.$key.']';

		$param1 = str_replace("$user_var_str",$user_var[$key],$param1);
	}

	#
	# Process user supplied data from JSON file
	#
	if ($json_data != null) {
		$param1 = preg_replace_callback('/{{\s*(\w+(\.\w+)*)\s*}}/',
			   function($matches) use ($json_data) {
				$variable = $matches[1];
				$value = getNestedValue($json_data, $variable);
				return $value !== null ? $value : $matches[0];
			   }, $param1);
	}

	if (strpos($param1, '$cputemp') !== false) {
		if ($cputemp == "") $cputemp = getMaxCpuTemp();

		$param1 = str_replace('$cputemp', $cputemp, $param1);
	}

	if (strpos($param1, '$df-avail') !== false) {
		exec("/bin/df -h --output=avail /", $dfout, $ret);
		if ($ret == 0) {
			$param1 = str_replace('$df-avail',trim($dfout[1]),$param1);
		}
		unset($dfout);
	}

	if (strpos($param1, '$uptime') !== false) {
		exec("/usr/bin/uptime -p", $uptimeout, $ret);
		if ($ret == 0) {
			$param1 = str_replace('$uptime',$uptimeout[0],$param1);
		}
		unset($uptimeout);
	}

	if ($logUser == ROUTINE_LOG_SYSLOG) {
		#
		# Log the message to syslog
		#
		if ($id === null || $facility === null || $priority === null) return;
		logSyslog($id, $facility, $priority, $param1);
	} else {
		#
		# Log the message to logfile
		#
		$logText = date("Y-m-d H:i:s ").str_pad($myPid,6)."[$routineName] $param1\n";

		file_put_contents("/var/log/asterisk/nyquist_routines.log", $logText, FILE_APPEND);
	}
}

function waitForIntercomEnd($debug, $db, $caller, $called)
{
	$sql = "SELECT value FROM data WHERE vkey='intercom/$called'";

	if ($debug) echo "SQL: $sql\n";

	while ($result = $db->query($sql)) {
		if ($result->num_rows == 0) break;

		$row = $result->fetch_assoc();

		$value = $row['value'];

		if ($debug) echo "Value: $value\n";

		if (strpos($value, "PJSIP/$caller-") !== FALSE) {
			sleep(2);
			continue;
		}

		$result->free();
	}
}

function ringWait($debug, $db, $caller)
{
	global $called, $callerChanged;

	if ($debug) echo "RingWait($caller, $called)\n";

	$ringFinished = FALSE;
	$foundCaller = FALSE;

	do {

		exec("/usr/sbin/asterisk -rx \"pjsip show channels\"",$out,$ret);

		$foundCaller = FALSE;

		foreach ($out as $line) {
			if (strpos($line, "  Channel: PJSIP/$caller-") !== FALSE) {
				$foundCaller = TRUE;

				if (strpos($line,"Ring") === FALSE) {
					$ringFinished = TRUE;
				}
			}
		}

		if (!$foundCaller) $ringFinished = TRUE;

		if (!$ringFinished) sleep(1);

		unset($out);

	} while ($ringFinished == FALSE);

	#
	# Update $called
	#

	exec("/usr/sbin/asterisk -rx \"pjsip show channelstats\"",$out,$ret);

	$bridgeId = array();
	$stations = array();

	foreach ($out as $line) {
		if ($debug) echo "PJSIP OUTPUT: $line\n";

		if (preg_match("/(.*) (.*)-(.*)/",$line,$parts)) {
			if ($debug) echo "BridgeId: $parts[1], Station: $parts[2]\n";

			$bridgeId[] = $parts[1];
			$stations[] = $parts[2];

			if ("$parts[2]" == "$caller") {
				$stationIdx = count($stations) - 1;

				$findBridge = $bridgeId[$stationIdx];

				if ($debug) echo "Station ($stations[$stationIdx])\n";
			}
			unset($parts);
		}


	}

	if (count($bridgeId) == 0) return;

	foreach ($bridgeId as $key => $bridge) {
		if ($key == $stationIdx) continue;

		if ($bridge == $findBridge) {
			$foundKey = $key;
			break;
		}
	}

	if ($debug) echo "Found Bridge: $bridgeId[$foundKey], Called: $stations[$foundKey]\n";

	if ($called != $stations[$foundKey]) {
		$called = $stations[$foundKey];

		$callerChanged = TRUE;

		if ($debug) echo "CALLER CHANGED\n";
	}

	if ($debug) echo "CALLED = $called\n";
}

function intercomWait($debug, $db, $caller, $called)
{

	$finished = FALSE;
	$foundCaller = FALSE;

	do {

		exec("/usr/sbin/asterisk -rx \"pjsip show channels\"",$out,$ret);

		$foundCaller = FALSE;

		foreach ($out as $line) {
			if (strpos($line, "  Channel: PJSIP/$caller-") !== FALSE) {
				$foundCaller = TRUE;
				$finished = TRUE;

				if (strpos($line,"Up") !== FALSE) {
					if ((strpos($line,"Dial") !== FALSE) ||
					    (strpos($line,"Page") !== FALSE) ||
					    (strpos($line,"ConfBridge") !== FALSE) ||
					    (strpos($line,"/ ") == 29))
					{
						$finished = FALSE;
					}
				}
				else
				if (strpos($line, "Ring") !== FALSE) {
					$finished = FALSE;
				}
			}
		}

		if (!$foundCaller) $finished = TRUE;

		if (!$finished) sleep(1);

		unset($out);

	} while ($finished == FALSE);
}

function checkIntercom($debug, $db, $caller, $called, $matchCalled)
{
	global $callerChanged;

	if ($debug) echo "checkIntercom($caller, $called, $matchCalled)\n";

	sleep(1);

	if (($matchCalled == 1) && ($callerChanged == TRUE)) {
		if ($debug) echo "Caller changed, return false\n";

		return FALSE;
	}

	$active = FALSE;
	$foundCaller = FALSE;

	exec("/usr/sbin/asterisk -rx \"pjsip show channels\"",$out,$ret);

	foreach ($out as $line) {
		if (strpos($line, "  Channel: PJSIP/$caller-") !== FALSE) {
			$foundCaller = TRUE;
			$active = TRUE;

			if (strpos($line,"Up") !== FALSE) {
				if ((strpos($line,"Dial") !== FALSE) ||
				    (strpos($line,"Page") !== FALSE) ||
				    (strpos($line,"ConfBridge") !== FALSE))
				{
					$active = TRUE;
				}
			}
			else
			if (strpos($line, "Ring") !== FALSE) {
				$active = FALSE;
			}
		}
	}

	if (!$foundCaller) $active = FALSE;

	return $active;

	#
	# $called must match station that answered, check...
	#
	exec("/usr/sbin/asterisk -rx \"pjsip show channelstats\"",$out,$ret);

	$bridgeId = array();
	$stations = array();

	foreach ($out as $line) {
		if ($debug) echo "PJSIP OUTPUT: $line\n";

		if (preg_match("/(.*) (.*)-(.*)/",$line,$parts)) {
			if ($debug) echo "BridgeId: $parts[1], Station: $parts[2]\n";

			$bridgeId[] = $parts[1];
			$stations[] = $parts[2];

			if ("$parts[2]" == "$caller") {
				$stationIdx = count($stations) - 1;

				$findBridge = $bridgeId[$stationIdx];

				if ($debug) echo "Station ($stations[$stationIdx])\n";
			}
			unset($parts);
		}
	}

	if (count($bridgeId) == 0) return;

	$foundKey = "";

	foreach ($bridgeId as $key => $bridge) {
		if ($key == $stationIdx) continue;

		if ($bridge == $findBridge) {
			$foundKey = $key;
			break;
		}
	}

	if ($foundKey == "") return FALSE;

	if ($debug) echo "Found Bridge: $bridgeId[$foundKey], Called: $stations[$foundKey]\n";

	if ($called == $stations[$foundKey]) {
		return TRUE;
	} else {
		return FALSE;
	}
}

function intercomCall($debug, $db, $param1, $param2)
{
	# $param1 = station.extension to place call from
	# $param2 = digits to dial to place the call

	if ($debug) echo "intercomCall($param1,$param2)\n";

	global $amiSocket, $server;

	if (($param1 == "") || ($param2 == "")) return;

	if (!$amiSocket) {
		if (!($amiSocket = openAmiConnection($debug, $server))) {
			if ($debug) echo "AMI Connection not open!\n";

			return;
		}
	}

	stream_set_timeout($amiSocket, 1);

	fputs($amiSocket, "Action: PJSIPNotify\nActionID: 1\nEndpoint: $param1\nVariable: Event=call\;dial=$param2\n\n");

	$loopCount = 0;

	$retstr=fgets($amiSocket);

	while (strpos($retstr,": NOTIFY sent") === false) {
	        if (($retstr=fgets($amiSocket)) == FALSE) break;
	        # Prevent infinite loop
	        if ($loopCount++ > 20) break;
	}
}

function getAnnouncementDtmfByName($debug, $db, $announcement_name)
{
	if ($announcement_name == "") return 0;

	$announcement_dtmf = 0;

	$sql = "SELECT dtmf_code FROM announcement WHERE name='$announcement_name'";

	if ($result = $db->query($sql)) {
		if ($result->num_rows > 0) {
			$row = $result->fetch_assoc();
			$announcement_dtmf = $row['dtmf_code'];
		}
	}

	return $announcement_dtmf;
}

function announcementNameExists($debug, $db, $announcement_name)
{
	if ($announcement_name == "") return false;

	$exists_status = false;

	$sql = "SELECT name FROM announcement WHERE name='$announcement_name'";

	if ($result = $db->query($sql)) {
		if ($result->num_rows > 0) {
			$exists_status = true;
		}
	}

	return $exists_status;
}

function getToneDtmfByName($debug, $db, $tone_name)
{
	if ($tone_name == "") return 0;

	$tone_dtmf = 0;

	$sql = "SELECT dial_number FROM tone WHERE name='$tone_name'";

	if ($result = $db->query($sql)) {
		if ($result->num_rows > 0) {
			$row = $result->fetch_assoc();
			$tone_dtmf = $row['dial_number'];
		}
	}

	return $tone_dtmf;
}

function toneDtmfExists($debug, $db, $tone_dtmf)
{
	if (!is_numeric($tone_dtmf)) return false;

	$exists_status = false;

	$sql = "SELECT dial_number FROM tone WHERE dial_number='$tone_dtmf'";

	if ($result = $db->query($sql)) {
		if ($result->num_rows > 0) {
			$exists_status = true;
		}
	}

	return $exists_status;
}

function toneNameExists($debug, $db, $tone_name)
{
	if ($tone_name == "") return false;

	$exists_status = false;

	$sql = "SELECT name FROM tone WHERE name='$tone_name'";

	if ($result = $db->query($sql)) {
		if ($result->num_rows > 0) {
			$exists_status = true;
		}
	}

	return $exists_status;
}

function getFacilityName($debug, $db, $facility)
{
	$facilityName = "Unknown";

	$sql = "SELECT name FROM server WHERE page_number='$facility'";

	if ($result = $db->query($sql)) {
		if ($result->num_rows > 0) {
			$row = $result->fetch_assoc();

			$facilityName = $row['name'];
		}
	}

	return $facilityName;
}

function getEventName($debug, $db, $eventId)
{
	$eventName = "Unknown";

	$sql = "SELECT name FROM schedule_events WHERE id='$eventId'";

	if ($result = $db->query($sql)) {
		if ($result->num_rows > 0) {
			$row = $result->fetch_assoc();

			$eventName = $row['name'];
		}
	}

	return $eventName;
}

function getToneName($debug, $db, $toneId)
{
	$toneName = "Unknown";

	$sql = "SELECT name FROM tone WHERE id='$toneId'";

	if ($result = $db->query($sql)) {
		if ($result->num_rows > 0) {
			$row = $result->fetch_assoc();

			$toneName = $row['name'];
		}
	}

	return $toneName;
}

function getToneDtmfCode($debug, $db, $toneId)
{
	$toneDtmfCode = "";

	$sql = "SELECT dial_number FROM tone WHERE id='$toneId'";

	if ($result = $db->query($sql)) {
		if ($result->num_rows > 0) {
			$row = $result->fetch_assoc();

			$toneDtmfCode = $row['dial_number'];
		}
	}

	return $toneDtmfCode;
}

function getScheduleName($debug, $db, $schedId)
{
	$date = date('Y-m-d');

	$schedName = "Unknown";

	if ($schedId == 0) {
		$sql = "SELECT id,name
			FROM `holidays`
			WHERE (NOW() BETWEEN start_date AND end_date)
			OR (start_date='$date') OR (end_date='$date')";

		if (!$result = $db->query($sql)) {
			return $schedName;
		}

		if ($result->num_rows != 0) {
			$sep_char = "";

			while ($row = $result->fetch_assoc()) {
				if ($sep_char == "") $schedName = "";

				$schedName = $schedName.$sep_char.$row['name'];

				$sep_char = ",";
			}
		}

		return $schedName;
	}

	$sql = "SELECT name, schedule_settings_id
		FROM schedules
		WHERE id='$schedId'";

	if ($result = $db->query($sql)) {
		if ($result->num_rows > 0) {
			$row = $result->fetch_assoc();

			$schedName = $row['name'];

			$schedSettingsId = $row['schedule_settings_id'];
		}
	}

	$result->free();

	$sql = "SELECT name
		FROM schedule_settings
		WHERE id='$schedSettingsId'";

	if ($result = $db->query($sql)) {
		if ($result->num_rows > 0) {
			$row = $result->fetch_assoc();

			$schedName = $row['name']." : ".$schedName;
		}
	}

	return $schedName;
}

function stationCanHangup($debug, $db, $extension)
{
	$station_can_hangup = 0;

	if ($extension == 0) return 1;

	if ($extension == "") return 0;

	$sql = "SELECT can_hangup FROM station_types_enum
		INNER JOIN station ON station_types_enum.id=station.type
		WHERE station.extension='$extension'";

	if ($result = $db->query($sql)) {
		if ($result->num_rows > 0) {
			$row = $result->fetch_assoc();

			$station_can_hangup = $row['can_hangup'];
		}
	}

	return $station_can_hangup;
}

function getStationType($debug, $db, $extension)
{
	if ($extension == "") return 0;

	$stationType = 0;

	$sql = "SELECT device_type FROM station WHERE extension='$extension'";

	if ($result = $db->query($sql)) {
		if ($result->num_rows > 0) {
			$row = $result->fetch_assoc();

			$stationType = $row['device_type'];
		}
	}

	return $stationType;
}

function getStationName($debug, $db, $extension)
{
	$stationName = "Unknown";

	if ($extension == "") return $stationName;

	$sql = "SELECT name FROM station WHERE extension='$extension'";

	if ($result = $db->query($sql)) {
		if ($result->num_rows > 0) {
			$row = $result->fetch_assoc();

			$stationName = $row['name'];
		}
	}

	return $stationName;
}

function getNyquistFeature($debug, $db, $called)
{
	# Note: Using '===' comparisions because switch does loose comparisions
	# that don't work correctly with mixed number and string text variables.

	switch(true)
	{
		case ($called === "#0"):
		case ($called === "#00"):
		case ($called === "0000000"):
			$NqFeature = "All-Call";
			break;

		case ($called === "#0911"):
		case ($called === "951"):
		case ($called === "0000911"):
			$NqFeature = "Emergency All-Call";
			break;

		case ($called === "##0"):
			$NqFeature = "Multi-Site All-Call";
			break;

		case ($called === "##0911"):
			$NqFeature = "Multi-Site Emergency All-Call";
			break;

		case ($called === "##0912"):
			$NqFeature = "Multi-Site Emergency All-Call (F:prompted)";
			break;

		case ($called === "*990"):
			$NqFeature = "Record Message";
			break;

		case ($called === "999"):
			$NqFeature = "Playback recorded calls";
			break;

		case ($called === "0"):
			$NqFeature = "Normal Call";
			break;

		case ($called === "*0"):
		case ($called === "****"):
			$NqFeature = "Emergency Call";
			break;

		case ($called === "*00"):
			$NqFeature = "Urgent Call";
			break;

		case ($called === "*000"):
			$NqFeature = "Outside line dialing admin";
			break;

		case ($called === "0000097"):
			$NqFeature = "Disable Audio";
			break;

		case ($called === "0000098"):
			$NqFeature = "Enable Audio";
			break;

		case ($called === "#*349"):
			$NqFeature = "Restart Server";
			break;

		case ($called === "00000349"):
			$NqFeature = "Restart Server";
			break;

		case ($called === "911"):
		case ($called === "98911"):
			$NqFeature = "911 Emergency Call";
			break;

		case (preg_match("/^\*94(.*)/", $called, $parts)):
			$NqFeature = "Routine ($parts[1])";
			break;

		case (preg_match("/^0000094(.*)/", $called, $parts)):
			$NqFeature = "Routine ($parts[1])";
			break;

		case (preg_match("/^##\*([0-9]*)\*(.*)/", $called, $parts)):
			$NqFeature = "Dial: $parts[2] @ Facility: $parts[1]";
			break;

		case (preg_match("/^##0912(.*)/", $called, $parts));
			print_r($parts);
			$flist = str_replace('*',',',$parts[1]);
			$NqFeature = "Multi-Facility Emergency All-Call (F:$flist)";
			break;

		case (preg_match("/^##([0-9]*)\*(.*)/", $called, $parts));
			$flist = $parts[1].",";
			$flist.= str_replace('*',',',$parts[2]);
			$NqFeature = "Multi-Facility All-Call (F:$flist)";
			break;

		case (preg_match("/^##(.*)#(.*)/", $called, $parts));
			$NqFeature = "Facility Zone Page (Z:$parts[2] @ F:$parts[1])";
			break;

		case (preg_match("/^##(.*)/", $called, $parts));
			$NqFeature = "Facility All-Call (F:$parts[1])";
			break;

		case (preg_match("/^#(.*)/", $called, $parts));
			$NqFeature = "Page (Z:$parts[1])";
			break;

		case (preg_match("/^0000099(.*)/", $called, $parts));
			$NqFeature = "Page (Z:$parts[1])";
			break;

		case (preg_match("/^\*991$/", $called, $parts));
			$NqFeature = "Record Page (Z:prompted)";
			break;

		case (preg_match("/^\*991\*(.*)/", $called, $parts));
			$NqFeature = "Record Page (Z:$parts[1])";
			break;

		case (preg_match("/^\*992\*(.*)/", $called, $parts));
			$NqFeature = "Cancel Recorded Page (Z:$parts[1])";
			break;

		case (preg_match("/^\*96(.*)\*(.*)/", $called, $parts)):
			$NqFeature = "Tone (T:$parts[1], Z: $parts[2])";
			break;

		case (preg_match("/^\*96(.*)/", $called, $parts)):
			$tone = ($parts[1] == "") ? "prompted" : $parts[1];
			$NqFeature = "Tone (T:$tone)";
			break;

		case (preg_match("/^0000096(.*)\*(.*)/", $called, $parts));
			$NqFeature = "Tone (T:$parts[1], Z: $parts[2])";
			break;

		case (preg_match("/^0000096(.*)/", $called, $parts));
			$tone = ($parts[1] == "") ? "prompted" : $parts[1];
			$NqFeature = "Tone (T:$tone)";
			break;

		case (preg_match("/^\*91(.*)/", $called, $parts)):
			$NqFeature = "Alarm (A:$parts[1])";
			break;

		case (preg_match("/^0000091(.*)/", $called, $parts));
			$NqFeature = "Alarm (A:$parts[1])";
			break;

		case (preg_match("/^\*92(.*)\*(.*)/", $called, $parts)):
			$NqFeature = "Announcement (A:$parts[1] Z:$parts[2])";
			break;

		case (preg_match("/^\*92(.*)$/", $called, $parts)):
			$NqFeature = "Announcement (A:$parts[1])";
			break;

		case (preg_match("/^0000092(.*)\*(.*)/", $called, $parts)):
			$NqFeature = "Announcement (A:$parts[1]) Z:$parts[2]";
			break;

		case (preg_match("/^987\*(.*)/", $called, $parts)):
			$NqFeature = "Start Audio Distribution ($parts[1])";
			break;

		case (preg_match("/^980\*(.*)/", $called, $parts)):
			$dtmf = ($parts[1] == "0") ? "All" : $parts[1];
			$NqFeature = "Stop Audio Distribution ($dtmf)";
			break;

		case (preg_match("/^978(.*)/", $called, $parts)):
			$NqFeature = "Monitor call or location ($parts[1])";
			break;

		case (preg_match("/^\*95(.*)/", $called, $parts)):
			$NqFeature = "Stop Routine ($parts[1])";
			break;

		case (preg_match("/^0000095(.*)/", $called, $parts)):
			$NqFeature = "Stop Routine ($parts[1])";
			break;

		case (preg_match("/^\*97(.*)#0$/", $called, $parts)):
			$NqFeature = "Multi-Facility Announcement (A:$parts[1], All Facilities)";
			break;

		case (preg_match("/^\*97(.*)#(.*)/", $called, $parts)):
			$flist.= str_replace('*',',',$parts[2]);
			$NqFeature = "Multi-Facility Announcement (A:$parts[1], F:$flist)";
			break;

		case (preg_match("/^942#(.*)#(.*)/", $called, $parts)):
			$NqFeature = "Close Contact (C:$parts[2] @ Ext:$parts[1])";
			break;

		case (preg_match("/^943#(.*)#(.*)/", $called, $parts)):
			$NqFeature = "Open Contact (C:$parts[2] @ Ext:$parts[1])";
			break;

		case (preg_match("/^\*\*\*(.*)/", $called, $parts)):
			$NqFeature = "Dial PBX: $parts[1]";
			break;
		default:
			$NqFeature = "Unknown";
			break;
	}

	return $NqFeature;
}

function getZoneName($debug, $db, $zone)
{
	$zoneName = "Unknown";

	if ($zone == "0") return "All Speakers";

	$sql = "SELECT name FROM pagingzones WHERE zone_number='$zone'";

	if ($result = $db->query($sql)) {
		if ($result->num_rows > 0) {
			$row = $result->fetch_assoc();

			$zoneName = "Zone $zone - ".$row['name'];
		}
	}

	return $zoneName;
}

function getZoneType($debug, $db, $zone)
{
	$zoneType = 0;

	if (!is_numeric($zone) || ($zone == 0) || ($zone == "")) return 0;

	$sql = "SELECT type FROM pagingzones WHERE zone_number='$zone'";

	if ($result = $db->query($sql)) {
		if ($result->num_rows > 0) {
			$row = $result->fetch_assoc();

			$zoneType = $row['type'];
		}
	}

	return $zoneType;
}

function getAnnounceZone($debug, $db, $announcement_dtmf)
{
	$zone = "";

	if ($announcement_dtmf == "") return $zone;

	$sql = "SELECT play_to_zone FROM announcement WHERE dtmf_code='$announcement_dtmf'";

	if ($result = $db->query($sql)) {
		if ($result->num_rows > 0) {
			$row = $result->fetch_assoc();

			$zone = $row['play_to_zone'];

			# Default to All Speakers if play_to_zone is blank
			if ($zone == "") $zone = "0";
		}

		$result->free();
	}

	return $zone;
}

function getAnnouncementId($debug, $db, $announcement_dtmf)
{
	if ($announcement_dtmf == "") return "";

	$announcement_id = "";

	$sql = "SELECT id FROM announcement WHERE dtmf_code='$announcement_dtmf'";

	if ($result = $db->query($sql)) {
		if ($result->num_rows > 0) {
			$row = $result->fetch_assoc();

			$announcement_id = $row['id'];
		}

		$result->free();
	}

	return $announcement_id;
}

function getAnnounceDtmfCode($debug, $db, $announce_id)
{
	if ($announce_id == "") return "";

	$announce_dtmf_code = "";

	$sql = "SELECT dtmf_code FROM announcement WHERE id='$announce_id'";

	if ($result = $db->query($sql)) {
		if ($result->num_rows > 0) {
			$row = $result->fetch_assoc();

			$announce_dtmf_code = $row['dtmf_code'];
		}

		$result->free();
	}

	return $announce_dtmf_code;
}

function getAnnounceFilename($debug, $db, $announce_id)
{
	if ($announce_id == "") return "";

	$announce_filename = "";

	$sql = "SELECT file_name FROM announcement WHERE id='$announce_id'";

	if ($result = $db->query($sql)) {
		if ($result->num_rows > 0) {
			$row = $result->fetch_assoc();

			$announce_filename = $row['file_name'];
		}

		$result->free();
	}

	return $announce_filename;
}

function getRoutineId($debug, $db, $routine_dtmf_code)
{
	$routine_id = "";

	if ($routine_dtmf_code == "") return $routine_id;

	$sql = "SELECT id FROM routine WHERE dtmf_code='$routine_dtmf_code'";

	if ($result = $db->query($sql)) {
		if ($result->num_rows > 0) {
			$row = $result->fetch_assoc();

			$routine_id = $row['id'];
		}

		$result->free();
	}

	return $routine_id;

}

function isPageExcluded($stationExt)
{
	global $db;

	if ($stationExt == "") return 0;

	$sql = "SELECT extension FROM paging_exclusion WHERE extension='$stationExt'";

	if ($result = $db->query($sql)) {
		if ($result->num_rows > 0) {
			$result->free();
			return 1;

		}
	}

	return 0;
}

function changePageExclusion($debug, $db, $param1, $param2, $delay)
{
	$cmd = $param1;

	if (($cmd != "add") && ($cmd != "delete")) return;

	$param2 = str_replace(':', ',', $param2);

	$stations = explode(',', $param2);

	$updateStations = array();
	$updateAmplifiers = array();
	$updateGateways = array();

	foreach ($stations as $stationExt) {
		$stationInTable = FALSE;

		$sql = "SELECT extension FROM paging_exclusion WHERE extension='$stationExt'";

		if ($result = $db->query($sql)) {
			if ($result->num_rows > 0) {
				$stationInTable = TRUE;

				$result->free();
			}
		}

		switch ($cmd)
		{
			case 'add':
				if ($stationInTable == TRUE) {
					continue 2;
				}

				$db->query("INSERT INTO paging_exclusion SET extension='$stationExt'");

				break;

			case 'delete':
				if ($stationInTable == FALSE) {
					continue 2;
				}

				$db->query("DELETE FROM paging_exclusion WHERE extension='$stationExt'");

				break;

			default:
				continue 2;
		}

		$sql = "SELECT amplifier_id, gateway_id FROM station WHERE extension='$stationExt'";

		if ($result = $db->query($sql)) {
			if ($result->num_rows > 0) {
				$row = $result->fetch_assoc();

				$amp_id = $row['amplifier_id'];
				$gw_id  = $row['gateway_id'];

				if ($amp_id > 0) {
					$updateAmplifiers[] = $amp_id;
				} elseif ($gw_id > 0) {
					$updateGateways[] = $gw_id;
				} else {
					$updateStations[] = $stationExt;
				}
			}
		}
	}

	foreach ($updateGateways as $gatewayId) {
		exec("/usr/local/bin/create-gateway-cfg add $gatewayId > /dev/null 2>&1 &");
	}

	foreach ($updateAmplifiers as $amplifierId) {
		exec("/usr/local/bin/create-amplifier-cfg add $amplifierId > /dev/null 2>&1 &");
	}

	foreach ($updateStations as $stationExt) {
		exec("/usr/local/bin/create-nyquist-cfg add $stationExt > /dev/null 2>&1 &");
	}
}

function stopAllPdist($debug, $db, $routineId, $delay)
{

	exec("/usr/local/bin/pd-control kill-Routine-$routineId > /dev/null 2>&1 $delay");

	exec("/usr/local/bin/ccm-control stop > /dev/null 2>&1 $delay");

	if ($db == NULL) {
		return;
	}

	$sql = "SELECT prog_dist_id FROM input_control
		WHERE prog_dist_id != '0'";

	if ($result = $db->query($sql)) {
		if ($result->num_rows == 0) return;

		while ($row = $result->fetch_assoc()) {
			$progDistId = $row['prog_dist_id'];

			exec("/usr/local/bin/input-control stop2 $progDistId > /dev/null 2>&1 $delay");
		}
	}

	if ($delay != "") sleep(1);
}

function getLineInputData($debug, $db, $progDistId, $action)
{
	if ($debug) echo "getLineInputData($progDistId, $action)\n";

	$lineInputData = array();

	$sql = "SELECT command, filename FROM prog_dist WHERE id='$progDistId'";

	if ($result = $db->query($sql)) {
		if ($result->num_rows > 0) {
			$row = $result->fetch_assoc();

			$filename = $row['filename'];
			$command = $row['command'];

			if ($debug) echo "Filename: $filename, Command: $command\n";

			if (($action == "start") && ($command == "play")) return;

			if ((preg_match("/^Line-Input:(.*):(.*)$/", $filename, $data) != 0)) {
				if ($debug) print_r($data);

				$lineInputData[] = $data[1];
				$lineInputData[] = $data[2];

				unset($data);
			}

			if ($debug) print_r($lineInputData);
		}
	}

	return $lineInputData;
}

function getProgDistName($debug, $db, $progDistId)
{
	$progDistName = "NOT-FOUND";

	$sql = "SELECT name FROM prog_dist WHERE id='$progDistId'";

	if ($result = $db->query($sql)) {
		if ($result->num_rows > 0) {
			$row = $result->fetch_assoc();
			$progDistName = $row['name'];
		}
	}

	$result->free();

	return $progDistName;
}

function getActionName($debug, $db, $routineActionId)
{
	$actionName = "NOT-FOUND";

	$sql = "SELECT name FROM routine_action WHERE id='$routineActionId'";

	if ($result = $db->query($sql)) {
		if ($result->num_rows > 0) {
			$row = $result->fetch_assoc();

			$actionName = $row['name'];
		}
	}

	$result->free();

	return $actionName;
}

#
# getCosParams() - Get various CoS parameters for $caller
#                  The parameters are cached globally, so the actual
#                  database queries will only happen once.
#

function getCosParams($debug, $db, $caller)
{
	global	$gotCos,
		$has_disable_audio,
		$has_enable_audio,
		$has_program_distribution,
		$has_manage_output_contacts;

	if ($gotCos == TRUE) return;

	$day_start = "";
	$night_start = "";

	$sql = "SELECT day_start, night_start FROM system WHERE id='1'";

	if (!$result = $db->query($sql)) {
		$gotCos = FALSE;
        	return;
	}

	while ($row = $result->fetch_assoc()) {
		$day_start = $row['day_start'];
		$night_start = $row['night_start'];
	}

	if (($day_start == "") || ($night_start == "")) {
		$gotCos = FALSE;
        	return;
	}


	$cur_time = date("H:i:s");

	if ($cur_time > $day_start && $cur_time < $night_start) {
        	$current_cos = "day_cos";
	} else {
		$current_cos = "night_cos";
	}

	$sql = "SELECT has_disable_audio, has_enable_audio,
		has_program_distribution, has_manage_output_contacts
		FROM cos INNER JOIN station ON cos.id=station.$current_cos
		WHERE station.extension='$caller'";

	if (!$result = $db->query($sql)) {
		$gotCos = FALSE;
        	return;
	}

	if ($row = $result->fetch_assoc()) {
		$has_disable_audio = $row['has_disable_audio'];
		$has_enable_audio = $row['has_enable_audio'];
		$has_program_distribution = $row['has_program_distribution'];
		$has_manage_output_contacts = $row['has_manage_output_contacts'];

		$gotCos = TRUE;
	} else {
		$gotCos = FALSE;
		return;
	}
}

function newAnnouncement($debug, $db, $caller, $type, $zones, $times_to_play, $repeat_period, $routineId, $routineActionId, $myPid)
{
	global $announceFilename;
	global $announceFiles;
	global $announceId;
	global $announceZones;
	global $announceTimesToPlay;
	global $announceRepeatPeriod;
	global $json_data;

	if ($debug) {
		echo "newAnnouncement(caller=$caller,type=$type,zones=$zones,times-to-play=$times_to_play,repeat-period=$repeat_period)\n";
	}

	#
	# Look for parameter overrides in JSON data from Routine API (newAnnouncementParams)
	#
	# Example:
	#	"newAnnouncementParams": [
	#	{
	#		"name":"Announcement-01",
	#		"type":"emergency",
	#		"zone":"10",
	#		"numTimesToPlay":"2",
	#		"timeBetweenRepeats":"5"
	#	}
	#	]
	#
	if ($json_data != null) {
		if (isset($json_data['newAnnouncementParams']) && is_array($json_data['newAnnouncementParams'])) {

			$actionName = getActionName($debug, $db, $routineActionId);

			$this_action = "New-Announcement";

			foreach ($json_data['newAnnouncementParams'] as $announcementParams) {
				if ($announcementParams['name'] === $actionName) {
					$announceParams = $announcementParams;

					if (isset($announceParams['type'])) {
						$new_type = $announceParams['type'];
						$new_value = strtolower($new_value);
						if (($new_type == "normal") || ($new_type == "emergency")) {
							$type = $new_type;
						} else {
							logText("$this_action: Invalid type: $new_type", ROUTINE_LOG_INTERNAL);
						}
					}
					if (isset($announceParams['zone'])) {
						$new_zone = $announceParams['zone'];
						if (is_numeric($new_zone)) {
							if (!hasCommandInjection($new_zone)) {
								$zones = $new_zone;
							} else {
								logText("$this_action: Invalid zone: $new_zone", ROUTINE_LOG_INTERNAL);
							}
						} else {
							logText("$this_action: Invalid zone: $new_zone", ROUTINE_LOG_INTERNAL);
						}
					}
					if (isset($announceParams['numTimesToPlay'])) {
						$new_ttp = $announceParams['numTimesToPlay'];
						if (is_numeric($new_ttp) && $new_ttp > 0 && $new_ttp < 99) {
							$times_to_play = $new_ttp;
						} else {
							logText("$this_action: Invalid numTimesToPlay: $new_ttp", ROUTINE_LOG_INTERNAL);
						}
					}
					if (isset($announceParams['timeBetweenRepeats'])) {
						$new_rp = $announceParams['timeBetweenRepeats'];
						if (is_numeric($new_rp) && $new_rp > 0 && $new_rp < 11) {
							$repeat_period = $new_rp;
						} else {
							logText("$this_action: Invalid timeBetweenRepeats: $new_rp", ROUTINE_LOG_INTERNAL);
						}
					}
					break;
				}
			}
		}
	}

	$announceTimesToPlay = $times_to_play;

	if ($repeat_period > 9) $repeat_period = 9;
	$announceRepeatPeriod = $repeat_period;

	#
	# $type parameter is a string: 'emergency' or 'normal',
	# convert string type to numeric value for database type field.
	#
	switch ($type)
	{
		case 'emergency':
			$type = 2;
			break;
		case 'normal':
			$type = 1;
			break;
	}

	$announceFiles = "file /var/lib/asterisk/sounds/silence_0.5-16k.wav\n";

	$announcementName = "Routine created announcement";

	$sql = "SELECT name FROM routine_action WHERE id='$routineActionId'";

	if ($result = $db->query($sql)) {
		if ($result->num_rows > 0) {
			$row = $result->fetch_assoc();

			$announcementName = $row['name'];
		}
	}

	$date_c = date("m/d/Y");
	$time_c = date("H:i:s");

	$sql = "INSERT INTO announcement (name,file_name,date_created,time_created,description,creator_ext,type,play_to_zone,times_to_play,repeat_period)
		VALUES ('$announcementName','','$date_c','$time_c','','$caller','$type','$zones','$times_to_play','$repeat_period')";

	if (!$result = $db->query($sql)) {
		$announceFilename = "";
		$announceId = 0;
		return;
	}

	$announceId = mysqli_insert_id($db);

	if ($announceId == "") {
		$announceFilename = "";
		$announceId = 0;
		return;
	}

	$announceFilename = "/tmp/announcement_".$announceId."_".$routineId."_".$myPid;

	if ($debug) echo "announceFilename = $announceFilename\n";

	$sql = "UPDATE announcement SET file_name='$announceFilename' WHERE id='$announceId'";

	if (!$result = $db->query($sql)) {
		$announceFilename = "";
		$announceId = 0;
		return;
	}

	$announceZones = $zones;
}

#
# addAnnouncementAudio() - Adds new announcement audio to $announceFiles
#
function addAnnouncementAudio($debug, $trigger, $cmdParam1, $cmdParam2, $announce_type, $announce_data, $force_fraction, $number_type, $routineId, $myPid)
{
	# Incoming Parameters:                                          'announcement'  filename         0               'digits'
	#                                                                 OR             OR               OR               OR
	#                                                               'digits'        number           1               'numeric'
	#                                                               'json-uploaded-file' {{file-reference-param}}
	global $announce_number;
	global $announceFiles;
	global $announceId;
	global $json_data;
	global $origCaller;
	global $extra_args;
	global $user_var;

	if ($announceId == "") return;

	switch ($announce_type)
	{
		case 'announcement':
			if (preg_match("/(.*).mp3/", $announce_data, $fparts)) {
				$newfilename = $fparts[1].".wav";
					if (file_exists($newfilename)) {
						$announce_data = $newfilename;
					}
			}

			$announceFiles.= "file $announce_data\n";
			break;

		case 'digits':
		case 'number':
			$number = $announce_data;

			#
			# Fetch possible digits supplied by trigger.
			#
			switch ($trigger)
			{
				case 'API':
					if ($json_data != null) {
						for ($i = 20; $i > 0; $i--) {
							$paramKey = 'apiParam'.$i;
							if (isset($json_data['routineApiParams'][$paramKey])) {
								$placeholder = '$'.$paramKey;
								$number = str_replace($placeholder, $json_data['routineApiParams'][$paramKey], $number);
							}
						}
					}
					$number = str_replace('$apiParam1',$cmdParam1,$number);
					$number = str_replace('$apiParam2',$cmdParam2,$number);
					$number = replaceCapData($number);
					break;

				case 'input-contact-closed':
				case 'input-contact-opened':
					$number = str_replace('$contact',$cmdParam2,$number);
					$number = str_replace('$station',$cmdParam1,$number);
					break;

				case 'incoming-call':
				case 'pbx-incoming-call':
					$number = str_replace('$caller',$cmdParam1,$number);
					break;

				case 'scheduled-routine':
					$number = str_replace('$schedParam1',$cmdParam1,$number);
					$number = str_replace('$schedParam2',$cmdParam2,$number);
					break;

				case 'station-status-up':
				case 'station-status-down':
					$number = str_replace('$station',$cmdParam1,$number);
					break;

				default:
					break;
			}

			if (isset($origCaller)) {
				$number = str_replace('$caller',$origCaller,$number);
			}

			if (isset($called)) {
				$number = str_replace('$called',$called,$number);
			}

			#
			# If $number is not numeric, bail out.
			#
			if (!is_numeric($number)) return;

			#
			# number_announce() will update global $announce_number.
			#
			$announce_number = "";

			#
			# Make sure $number_type is valid, set to "digits" by default.
			#
			switch ($number_type)
			{
				case 'digits':
				case 'numeric':
					break;

				default:
					$number_type = "digits";
					break;
			}

			number_announce($number,$force_fraction,$number_type);

			$announceFiles.= $announce_number;

			$announce_number = "";

			break;

		case 'tts':
			switch ($trigger)
			{
				case 'API':
					if ($json_data != null) {
						for ($i = 20; $i > 0; $i--) {
							$paramKey = 'apiParam'.$i;
							if (isset($json_data['routineApiParams'][$paramKey])) {
								$placeholder = '$'.$paramKey;
								$announce_data = str_replace($placeholder, $json_data['routineApiParams'][$paramKey], $announce_data);
							}
						}
					}
					$announce_data = str_replace('$apiParam1',$cmdParam1,$announce_data);
					$announce_data = str_replace('$apiParam2',$cmdParam2,$announce_data);
					$announce_data = replaceCapData($announce_data);
					break;

				case 'input-contact-closed':
				case 'input-contact-opened':
					$announce_data = str_replace('$contact',$cmdParam2,$announce_data);
					$announce_data = str_replace('$station',$cmdParam1,$announce_data);
					break;

				case 'incoming-call':
				case 'pbx-incoming-call':
					$announce_data = str_replace('$caller',$cmdParam1,$announce_data);
					break;

				case 'nws-alert':
					$announce_data = str_replace('$nwsAlertEvent', $cmdParam1,$announce_data);
					$announce_data = str_replace('$nwsAlertSeverity', $cmdParam2,$announce_data);
					$announce_data = str_replace('$nwsAlertCertainty', $extra_args[1],$announce_data);
					$announce_data = str_replace('$nwsAlertUrgency', $extra_args[2],$announce_data);
					$announce_data = str_replace('$nwsAlertResponse', $extra_args[3],$announce_data);
					$announce_data = str_replace('$nwsAlertZone', $extra_args[4],$announce_data);
					break;

				case 'scheduled-routine':
					$announce_data = str_replace('$schedParam1',$cmdParam1,$announce_data);
					$announce_data = str_replace('$schedParam2',$cmdParam2,$announce_data);
					break;

				case 'station-status-up':
				case 'station-status-down':
					$announce_data = str_replace('$station',$cmdParam1,$announce_data);
					break;

				default:
					break;
			}

			if (isset($origCaller)) {
				$announce_data = str_replace('$caller',$origCaller,$announce_data);
			}

			if (isset($called)) {
				$announce_data = str_replace('$called',$called,$announce_data);
			}

			foreach ($user_var as $key => $u_var) {
				$user_var_str = '$user_var['.$key.']';

				$announce_data = str_replace("$user_var_str",$user_var[$key],$announce_data);
			}

			$removeVars = array(
					'$apiParam1',
					'$apiParam2',
					'$apiParam3',
					'$apiParam4',
					'$apiParam5',
					'$apiParam6',
					'$apiParam7',
					'$apiParam8',
					'$apiParam9',
					'$apiParam10',
					'$apiParam11',
					'$apiParam12',
					'$apiParam13',
					'$apiParam14',
					'$apiParam15',
					'$apiParam16',
					'$apiParam17',
					'$apiParam18',
					'$apiParam19',
					'$apiParam20',
					'$schedParam1',
					'$schedParam2',
					'$contact',
					'$caller',
					'$station',
					'$zone',
					'$scheduleid',
					'$eventid',
					'$eventname',
					'$called');

			$announce_data = str_replace($removeVars,'',$announce_data);

			#
			# Process user supplied data from JSON file
			#
			if ($json_data != null) {
				$announce_data = preg_replace_callback('/{{\s*(\w+(\.\w+)*)\s*}}/',
					   function($matches) use ($json_data) {
						$variable = $matches[1];
						$value = getNestedValue($json_data, $variable);
						return $value !== null ? $value : $matches[0];
					   }, $announce_data);
			}

			$tts_filename = createTTS($debug, $announceId, $announce_data, $routineId, $myPid);

			if ($tts_filename != "") {
				$announceFiles.= "file $tts_filename\n";
			}
			break;

		case 'json-uploaded-file':
			#
			# Get uploaded filename from JSON data:
			#
			if ($json_data != null) {
				$audio_filename = preg_replace_callback('/{{\s*(\w+(\.\w+)*)\s*}}/',
					   function($matches) use ($json_data) {
						$variable = $matches[1];
						$value = getNestedValue($json_data, $variable);
						return $value !== null ? $value : $matches[0];
					   }, $announce_data);
			}

			if ($audio_filename == "") return;

			$inuse_dir = "/var/www/html/laravel/public/exports/inuse";

			if (!file_exists($inuse_dir)) {
				mkdir($inuse_dir, 0777);
				chmod($inuse_dir,0777);
				chown($inuse_dir,"root");
				chgrp($inuse_dir,"www-data");

				copy("/var/www/html/laravel/public/exports/index.php","$inuse_dir/index.php");
				chown("$inuse_dir/index.php","root");
				chgrp("$inuse_dir/index.php","root");
			}

			$tmp_filename = trim("/tmp/T$audio_filename");
			$tmp_filename = str_replace('.mp3','.wav',$tmp_filename);

			$audio_filename_inuse = trim("$inuse_dir/$audio_filename");

			$audio_filename = trim("/var/www/html/laravel/public/exports/$audio_filename");

			if (file_exists($audio_filename)) {
				# Process incoming audio file to make sure we get a 16k wav for use;
				# this will work for incoming wav and mp3 files.
				exec("/usr/local/bin/ffmpeg -y -i '$audio_filename' -filter_complex 'aresample=16000' -ac 1 -vn '$tmp_filename' > /dev/null 2>&1");
				if (file_exists($tmp_filename)) {
					rename($tmp_filename,$audio_filename_inuse);
					chown($audio_filename_inuse,"www-data");
					chgrp($audio_filename_inuse,"www-data");
					chmod($audio_filename_inuse,0664);

					$announceFiles.= "file $audio_filename_inuse\n";
				}

				# Delete original audio file uploaded via API
				unlink($audio_filename);
			}

			break;
	}
}

function playAnnouncement($debug, $action, $caller, $exten, $waitTime, $delay)
{
	global $amiSocket, $server, $origCaller;
	global $announceFilename;
	global $announceFiles;
	global $announceId;
	global $announceZones;
	global $announceTimesToPlay;
	global $announceRepeatPeriod;

	if ($debug) echo "playAnnouncement($action, $caller, $exten, $waitTime)\n";

	if ($announceId == 0) return;

	if ($announceFilename == "") return;

	if ($announceFiles == "") return;

	if ($announceRepeatPeriod > 0) {
		$time_silence = $announceRepeatPeriod*1000000;

		if ($time_silence ==  500000) $time_silence = "0.5";

		if ($time_silence >= 1000000) $time_silence = $time_silence / 1000000;

		if ($time_silence > 0)
		{
			$silence_file = "file /var/lib/asterisk/sounds/silence_".$time_silence."-16k.wav\n";
		}
		else
		{
			$silence_file = "";
		}
	} else {
		$silence_file = "";
	}

	$announcementsToPlay = $announceFiles;

	#
	# Duplicate announcement to play specified number of times.
	#
	for ($ttp = 1; $ttp < $announceTimesToPlay; $ttp++) {
		if ($silence_file != "") {
			$announcementsToPlay.= "$silence_file";
		}

		$announcementsToPlay.= $announceFiles;
	}

	$exten.= $announceId."*".$announceZones;

	$announceFd = fopen($announceFilename, "w");

	fwrite($announceFd, $announcementsToPlay);

	fclose($announceFd);

	if ($debug) echo "AnnouncementsToPlay:\n$announcementsToPlay\n";

	if (!$amiSocket) {
		if (!($amiSocket = openAmiConnection($debug, $server))) {
			if ($debug) echo "AMI Connection not open!\n";

			return;
		}
	}

	fputs($amiSocket, "Action: Originate\r\nActionID: 1\r\nChannel: local/$exten@routine\r\nCallerId: $caller\r\nVariable: Caller=$caller\r\nVariable: Zones=$announceZones\r\nVariable: WaitTime=$waitTime\r\nVariable: ViaRoutine=1\r\nVariable: RoutineAnnounceFile=$announceFilename\r\nExten: $caller\r\nContext: routine_wait\r\nPriority: 1\r\nTimeout: 30000\r\nCodecs: g722\r\n\r\n");

	# If $delay is "", no '&' was passed to indicate no-delay, so we delay, but for a maximum of 1 hour (3600 seconds)
	#
	if ($delay == "") {
		$waited = 0;

		while ($waited < 3600) {
			if (!file_exists($announceFilename)) {
				$waited = 3601;
			} else {
				sleep(1);
				$waited++;
			}
		}
	}

	#
	# Clear $announceId and $announceFilename to prevent subsequent addAnnouncementAudio() calls
	# and prevent announcement file from getting deleted when routine script exits.
	#
	$announceId = 0;
	$announceFilename = "";
}

function stillEnabled($debug)
{
	define("QUERY_SUCCESSFUL", 999);

	global $db, $server, $user, $pass, $database, $routineId;

	$sql = "SELECT enabled FROM routine WHERE id='$routineId'";

	$tries = 0;

	while ($tries++ < 120) {
		if (!$result = $db->query($sql)) {
			#
			# The only possible error is disconnected from MySQL server,
			# try to re-connect to MySQL server.
			#
			$db = new mysqli($server, $user, $pass, $database);

			if ($db->connect_errno > 0) {
				sleep(1);
			} else {
				logText("Reopened MySQL connection", ROUTINE_LOG_INTERNAL);
				$tries = 0;
			}
		} else {
			$tries = QUERY_SUCCESSFUL;
			break;
		}
	}

	#
	# Exit if re-connect to MySQL server failed.
	#
	if ($tries != QUERY_SUCCESSFUL) {
		logText("Terminated - MySQL failure", ROUTINE_LOG_INTERNAL);
		if ($debug)
			die('MySQL connection failed');
		else
			doExit(1);
	}

	#
	# Exit if Routine no longer exists.
	#
	if ($result->num_rows <= 0) {
		$db->close();

		$db = NULL;

		doExit(1);
	}

	$row = $result->fetch_assoc();

	$enabledFlag = $row['enabled'];

	$result->free();

	return $enabledFlag;
}

function killRoutineProcess($debug, $db, $taskId, $taskPid)
{
	if (posix_getpgid($taskPid) != FALSE) {
		$cmdline = trim(file_get_contents("/proc/$taskPid/cmdline"));

		$cmdline = preg_replace("/[^a-z0-9\-]/", "", $cmdline);

		if (strpos($cmdline,"routine-execute-r$routineId") === false) {
			return(1);
		}

		posix_kill($taskPid, SIGUSR1);

		sleep(5);
	}

	if (posix_getpgid($taskPid) != FALSE) {
		exec("/bin/kill -USR1 $taskPid > /dev/null 2>&1");

		sleep(5);
	}

	if (posix_getpgid($taskPid) != FALSE) {
		exec("/bin/kill -9 $taskPid > /dev/null 2>&1");

		sleep(5);
	}

	if (posix_getpgid($taskPid) == FALSE) {
		$db->query("DELETE FROM routine_task WHERE id='$taskId'");
	}

	return(0);
}

function number_announce($number, $force_fraction, $number_type)
{
	global $announce_number;
	STATIC $digits = "/var/lib/asterisk/sounds/en/digits2";
	$fraction = 0;

	if (($number == 0) && ($number_type != "numeric_started")) {
		$announce_number.= "file $digits/0.wav\n";

		if ($force_fraction) {
			$announce_number.= "file $digits/point.wav\n";
			$announce_number.= "file $digits/0.wav\n";
		}

		return;
	}

	if ($number_type == "digits") {
		$charsArray = str_split($number);

		foreach ($charsArray as $char) {
			if ($char == ".") {
				$announce_number.= "file $digits/point.wav\n";
			} else {
				$announce_number.= "file $digits/$char.wav\n";
			}
		}

		return $number;
	}

	if ($number_type == "numeric") {
		if (preg_match("/(.*)\.(.*)/",$number,$parts)) {
			if (isset($parts[2])) {
				$fraction = $parts[2];
				$force_fraction = 1;
			}
		}

		$number = intval($number);
	}

	if ($number >= 1000) {
		$thousands = intdiv($number, 1000);

		number_announce($thousands,0,"numeric_started");

		$number = $number % 1000;

		$announce_number.= "file $digits/thousand.wav\n";

		$number = number_announce($number,0,"numeric_started");
	}
	else
	if ($number >= 100) {
		$hundreds = intdiv($number, 100);

		$number = $number % 100;

		$announce_number.= "file $digits/$hundreds.wav\nfile $digits/hundred.wav\n";

		$number = number_announce($number,0,"numeric_started");
	}
	else
	if ($number > 19) {
		$tens = intdiv($number, 10);

		$number = $number % 10;

		$tens = $tens * 10;

		$announce_number.= "file $digits/$tens.wav\n";

		$number = number_announce($number,0,"numeric_started");
	}
	else
	if ($number > 0) {
		$announce_number.= "file $digits/$number.wav\n";
	}

	if ($fraction > 0) {
		$announce_number.= "file $digits/point.wav\n";

		$charsArray = str_split($fraction);

		foreach ($charsArray as $char) {
			$announce_number.= "file $digits/$char.wav\n";
		}
	}
	else
	if ($force_fraction) {
		$announce_number.= "file $digits/point.wav\nfile $digits/0.wav\n";
	}

	return $number;
}

function createTTS($debug, $announceId, $announce_data, $routineId, $myPid)
{
	STATIC $ttsFileNumber = 1;
	STATIC $prevExecTime = 0;

	#
	# We need to sleep at least one second between calls to swift because we're
	# not using a concurrent swift license.
	#
	if ($ttsFileNumber > 1) {
		$nowTime = time();
		if (($nowTime - $prevExecTime) < 2) {
			sleep(1);
		}
	}

	$ttsOutputFilename = "/tmp/announceTTS_".$announceId."_".$routineId."_".$myPid."_".$ttsFileNumber.".wav";

	$ttsFileNumber++;

	$tmp_file = "/tmp/ttsInput_".$announceId."_".$routineId."_".$myPid.".txt";

	file_put_contents($tmp_file, $announce_data);

	if ($debug) {
		echo "TTS Cmd:\n/usr/local/bin/swift -f $tmp_file -o $ttsOutputFilename -p audio/sampling-rate=16000,audio/channels=1,audio/volume=500 2>/dev/null";
	}

	exec("/usr/local/bin/swift -f $tmp_file -o $ttsOutputFilename -p audio/sampling-rate=16000,audio/channels=1,audio/volume=500",$out,$ret);

	$prevExecTime = time();

	if ($debug) {
		echo "Swift output:";
		print_r($out);
		echo "Swift ret code:";
		echo $ret;
	}

	exec("rm -f $tmp_file > /dev/null 2>&1");

	if ($ret == 0) {
		chown("$ttsOutputFilename","www-data");
		chgrp("$ttsOutputFilename","www-data");
		chmod("$ttsOutputFilename",0664);
		return $ttsOutputFilename;
	} else {
		return "";
	}
}

function changeSetting($debug, $db, $caller, $trigger, $cmdParam1, $cmdParam2, $param1, $param2, $param3, $delay)
{
	#
	# This function will SET a new value ($param1) for the specified DB field ($param1).
	#
	# $param1 = routine_change_setting.id
	# $param2 = New value
	#
	if (($param1 == "") || !is_numeric($param1)) return;

	#
	# Get table and field name
	#
	$sql = "SELECT table_name,field_name FROM routine_change_setting WHERE id='$param1'";

	if (!$result = $db->query($sql)) return;

	if ($result->num_rows > 0) {
		$row = $result->fetch_assoc();

		$table_name = $row['table_name'];
		$field_name = $row['field_name'];
	}

	#
	# If field to set is cri_enabled and new value is disable(0), execute CRI disable to active CRI requests.
	# This must be done before disabling system.cri_enabled.
	#
	if (($field_name == "cri_enabled") && ($param2 == "0")) {
		if ($result = $db->query("SELECT dcs_extension FROM callback_requests")) {
			if ($result->num_rows > 0) {
				while ($row = $result->fetch_assoc())
				{
					$stationExt = $row['dcs_extension'];

					exec("/usr/local/bin/request-callback -d $StationExt > /dev/null 2>&1 &");
				}
			}
		}
	}

	#
	# Set the database field $table_name.$field_name to the new value $param2
	#
	$sql = "UPDATE `$table_name` SET `$field_name` = '$param2'";

	$db->query($sql);
}

function changeVolume($debug, $db, $caller, $trigger, $cmdParam1, $cmdParam2, $param1, $param2, $param3, $delay)
{
	#
	# $param1 = Volume Type:
	#    (1=All-Call, 2=Audio Distribution, 3=Emergency-All-Call, 4=Zone-Paging)
	# $param2 = Zone Number (0 = All Speakers)
	# $param3 = New Volume Cut Level
	#

	if (($param1 == "") || ($param2 == "") || ($param3 == "")) return;

	if ($trigger == "API") {
		if ($cmdParam1 != "") {
			$param3 = str_replace('$apiParam1',$cmdParam1,$param3);
		}

		if ($cmdParam2 != "") {
			$param3 = str_replace('$apiParam2',$cmdParam2,$param3);
		}
	}

	# Make sure supplied cut level is numeric
	if (!is_numeric($param3)) return;

	# Check for valid Cut Level (-42 to 0)
	if (($param3 < -42) || ($param3 > 0)) return;

	switch($param1)
	{
	case '1':
		# All-Call
		$sql = "UPDATE system SET allcall_volume='$param3'";
		$db->query($sql);
		$param2 = "0";
		break;

	case '2':
		# Audio Distribution
		if ($param2 == "0") {
			$sql = "UPDATE system SET audiodist_volume='$param3'";
		} else {
			$sql = "UPDATE pagingzones SET audiodist_volume='$param3' WHERE zone_number='$param2'";
		}
		$db->query($sql);
		break;

	case '3':
		# Emergency-All-Call
		$sql = "UPDATE system SET eallcall_volume='$param3'";
		$db->query($sql);
		$param2 = "0";
		break;

	case '4':
		# Zone Paging
		$sql = "UPDATE pagingzones SET volume='$param3' WHERE zone_number='$param2'";
		$db->query($sql);
		break;

	default:
		return;
		break;
	}

	exec("/usr/local/bin/update-stations-zone-volume -z $param2 > /dev/null 2>&1 $delay");
}

function changeWbcSetting($debug, $db, $caller, $trigger, $cmdParam1, $cmdParam2, $param1, $param2, $param3, $delay, $device_type)
{
	#
	# This function will SET a new value ($param3) for the specified WBC/DSC01 Display field ($param2)
	# on the specified WBC/DSC01 device(s) ($param1).
	#
	# $param1 = WBC/DSC01 Extensions (0 for all WBC/DSC01 devices)
	# $param2 = wbc_display field to change
	# $param3 = New value
	#
	if (($param1 == "") || ($param2 == "") || ($param3 == "")) return;

	#
	# Set the database field $table_name.$field_name to the new value $param2
	#
	if ($param1 == "0") {
		$sql = "UPDATE wbc_display SET `$param2` = '$param3'";
		$db->query($sql);

		$sql = "SELECT extension FROM station WHERE device_type='$device_type'";
		$result = $db->query($sql);
		if ($result->num_rows > 0) {
			while($row = $result->fetch_assoc()) {
				$wbc_station = $row['extension'];
				shell_exec("/usr/local/bin/create-nyquist-cfg add \"$wbc_station\" >/dev/null 2>&1");
			}
		}

	} elseif (strpos($param1, ',') !== false) {
		$station_list = "";
		$wbc_stations = explode(',',$param1);
		$sep = "";
		foreach ($wbc_stations as $wbc_station) {
			$station_list.= "$sep'$wbc_station'";
			$sep = ",";
		}
		$sql = "UPDATE wbc_display SET `$param2` = '$param3' WHERE extension IN ($station_list)";
		$db->query($sql);

		foreach ($wbc_stations as $wbc_station) {
			shell_exec("/usr/local/bin/create-nyquist-cfg add \"$wbc_station\" >/dev/null 2>&1 &");
		}

	} else {
		$sql = "UPDATE wbc_display SET `$param2` = '$param3' WHERE extension='$param1'";
		$db->query($sql);

		shell_exec("/usr/local/bin/create-nyquist-cfg add \"$param1\" >/dev/null 2>&1");
	}
}
function createAlertsXmlFile($debug, $weather_alert_str)
{
	global $myPid;

	$filenamePattern = '/^\/srv\/tftp\/msg\/NWS-Alerts\.(\d+)$/';

	if ($debug) echo "createAlertsXmlFile:\n";

	$alerts_xml_file = "/srv/tftp/Alerts.xml";

	$tmpAlertFileBase = "/srv/tftp/msg/NWS-Alerts";

	if (trim($weather_alert_str) == "") {
		if (file_exists("$tmpAlertFileBase.$myPid")) {
			unlink("$tmpAlertFileBase.$myPid");
		}
	} else {
		$wstr = str_replace("<br>","\n",$weather_alert_str);

		file_put_contents("$tmpAlertFileBase.$myPid", $wstr);

		chown("$tmpAlertFileBase.$myPid", "www-data");
		chgrp("$tmpAlertFileBase.$myPid", "www-data");
		chmod("$tmpAlertFileBase.$myPid", 0664);
	}

	$all_alerts = "";

	foreach (glob("$tmpAlertFileBase.*") as $file) {
		if (preg_match($filenamePattern, $file, $matches)) {
			$pid = $matches[1];

			if (file_exists("/proc/$pid")) {
				$all_alerts.= file_get_contents($file);
			} else {
				unlink($file);
			}
		}
		unset($matches);
	}

	if ($all_alerts != "") {
		$new_all_alerts = substr($all_alerts, 0, 1985);

		if ($new_all_alerts != $all_alerts) {
			$new_all_alerts.= "-TRUNCATED";
		}

		$all_alerts = $new_all_alerts;
	} else {
		$all_alerts = "No alerts";
	}

	$alerts_xml = "<AlertsTextScreen>
<Title>National Weather Service Alerts</Title>
<Text>$all_alerts


</Text>
</AlertsTextScreen>";

	$current_alerts_xml = file_get_contents($alerts_xml_file);

	if ($current_alerts_xml == $alerts_xml) return;

	file_put_contents($alerts_xml_file, $alerts_xml);

	chown($alerts_xml_file, "www-data");
	chgrp($alerts_xml_file, "www-data");
	chmod($alerts_xml_file, 0664);

	$fanvil_alerts_xml_file = "/var/lib/asterisk/static-http/Alerts.xml";
	copy($alerts_xml_file,$fanvil_alerts_xml_file);
	chown($fanvil_alerts_xml_file, "www-data");
	chgrp($fanvil_alerts_xml_file, "www-data");
	chmod($fanvil_alerts_xml_file, 0664);
	$fanvil_softkey = "
<SoftKey index=\"1\"><Label>Exit</Label><URI>SoftKey:Exit</URI></SoftKey>
";
	file_put_contents($fanvil_alerts_xml_file,$fanvil_softkey,FILE_APPEND);
}

#
# isActionConditionTrue() - Returns true if $condition is satisfied; otherwise returns false.
#
# Allows the user to specify a condition that must be true to execute the action.
#
# Also allows code injection via CODE: section.
#
# The following variables can be used in the PHP condition statement:
#
# $apiParam1, $apiParam2, $alarmDtmfCode, $announceType, $audioDistCmd, $audioDistName,
# $curtime, $cputemp, $caller, $called, $callerName, $calledName, $origCaller, $station,
# $ioContactAction, $ioContactStation, $ioContactNumber, $eventId, $eventName,
# $scheduleId, $scheduleName, $schedParam1, $schedParam2, $toneDtmfCode, $toneName,
# $facilityName, $facilityPageNumber, $stationStatus, $weekday, $zoneName, $zoneNumber
# $nwsAlertEvent, $nwsAlertSeverity, $nwsAlertCertainty, $nwsAlertUrgency, $nwsAlertResponse
#
function isActionConditionTrue(	$debug, $db, $condition, $trigger, $action_type,
				$caller, $origCaller, $callerName, $called, $calledName,
				$cmdParam1, $cmdParam2, $pTime2, $zones, $zonesProvided)
{
	global	$cputemp,
		$dfAvail,
		$gotoActionName,
		$ga10pvExt,
		$autoDeleteDisplayMsgsUponExit,
		$autoDeleteDashMsgsUponExit,
		$mergeDelete,
		$pDate1,
		$pDate2,
		$pDate3,
		$pTime1,
		$pTime2,
		$routineId,
		$routineName,
		$extra_args,
		$json_data,
		$user_var,
		$webhook_return_str;

	if ($condition == "") {
		return true;
	}

	$condition = base64_decode($condition);

	if ($condition == "") {
		return true;
	}

	if ($debug) echo "Eval Condition: $condition\n";

	$DOACTION = false;

	$eval_str = "";

	$curtime = $pTime2;

	if (strpos($condition, '$weekday') !== false) {
		$weekday = date("D");
	}

	#
	# If requested, get maximum CPU temperature ($cputemp).
	#
	if (strpos($condition,'$cputemp') !== false) {
		$cputemp = getMaxCpuTemp();
	}

	#
	# If requested, get disk available size.
	#
	if (strpos($condition,'$dfAvail') !== false) {
		exec("/bin/df -h --output=avail /", $dfout, $dfRet);
		if ($dfRet == 0) {
			$dfAvail = trim($dfout[1]);
		}
		unset($dfout);
	}

	#
	# Validate security of $condition string, blocking dangerous functions.
	#
	$block_functions = array('exec','shell_exec','pcntl_exec','system','passthru','popen','fopen','proc_open','copy',
				 'kill','killall','file_put_contents','unlink','chown','chmod','chgrp','mkdir','xmlrpc_entity_decode',
				 'delete','flock','ftruncate','lchgrp','lchown','rename','rmdir','symlink','eval','mysqli',
				 'tempnam','tmpfile','touch','posix_kill','proc_open','proc_close','proc_nice','ftp_connect',
				 'apache_child_terminate','define_syslog_variables','fsockopen','stream_context_create',
				 'highlight_file','ini_set','ini_alter','ini_restore','mysql_pconnect','openlog','syslog',
			 	 'posix_mkfifo','posix_setpgid','posix_setsid','posix_setuid','posix_setuid','proc_terminate');

	$test_condition = preg_replace("/[\s\t\n\r]*/",'',$condition);

	foreach ($block_functions as $block_function) {
		if (preg_match("/$block_function\((.*)\)/", $test_condition)) {
			if ($debug) echo "Block function $block_function\n";

			doExit(1);
		}
	}

	#
	# Don't allow ` in $condition, it would allow external code execution.
	#
	if (strpos($condition, '`') !== false) {
		doExit(1);
	}

	#
	# Create eval string from $condition
	#
	# CODE: Allows code injection.
	# STOP: If condition is true, Routine will stop and exit.
	# REPEAT_ROUTINE: Repeat the Routine, after conditionally executing the Action.
	#
	if (strpos($condition, 'CODE:') === 0) {
		$eval_str = substr($condition,5);

		if ($eval_str == "") return true;

	} else {
		if (strpos($condition, 'STOP:') === 0) {
			$condition = substr($condition,5);
			if ($condition == "") return true;
			$eval_str = "if ($condition) {doExit(0);}";
		} else {
			if (strpos($condition, 'REPEAT_ROUTINE:') === 0) {
				$gotoActionName = "REPEAT_ROUTINE";

				$condition = substr($condition,15);
				if ($condition == "") return true;
				$eval_str = "if ($condition) {\$DOACTION=true;}";
			} else {
				$eval_str = "if ($condition) {\$DOACTION=true;}";
			}
		}
	}


	if ($debug) echo "eval_str: $eval_str\n";

	#
	# Prepare available variables based on trigger and action type.
	# Initializing all of them because we don't know which ones the user
	# may reference in their code.
	#
	$alarmDtmfCode = 0;
	$apiParam1 = "";
	$apiParam2 = "";
	$audioDistCmd = "";
	$audioDistName = "";
	$announceType = "";
	$announceDtmfCode = 0;
	$deviceMacAddr = "";
	$deviceName = "";
	$ioContactAction = "";
	$ioContactStation = 0;
	$ioContactNumber = 0;
	$eventId = 0;
	$eventName = "";
	$station = 0;
	$facilityPageNumber = 0;
	$facilityName = "";
	$nwsAlertEvent = "";
	$nwsAlertSeverity = "";
	$nwsAlertCertainty = "";
	$nwsAlertUrgency = "";
	$nwsAlertResponse = "";
	$schedParam1 = "";
	$schedParam2 = "";
	$scheduleId = 0;
	$scheduleName = "";
	$scheduledParam1 = "";
	$scheduledParam2 = "";
	$stationStatus = "";
	$toneDtmfCode = 0;
	$toneName = "";
	$zoneNumber = 0;
	$zoneName = "";

	switch ($trigger)
	{
	case 'API':
		# Note: API trigger is created by Web-UI via routine-execute, not by routine-trigger.
		$apiParam1 = $cmdParam1;
		$apiParam2 = $cmdParam2;

		#
		# Create apiParam variables from incoming JSON routineApiParams variables.
		#
		if ($json_data != null) {
			for ($i = 20; $i > 0; $i--) {
				$paramKey = 'apiParam'.$i;
				if (isset($json_data['routineApiParams'][$paramKey])) {
					${'apiParam'.$i} = $json_data['routineApiParams'][$paramKey];
				}
			}
		}
		break;
	case 'alarm':
		$alarmid = $cmdParam1;
		$alarmDtmfCode = getToneDtmfCode($debug, $db, $alarmid);
		$alarmName = getToneName($debug, $db, $alarmid);
		break;
	case 'announcement':
		$announce_id = "";

		if (preg_match("/emerg-(.*)/", $cmdParam1, $aparts)) {
			$announceType = "Emergency";
			$announce_id = $aparts[1];
			unset($aparts);
		}
		else
		if (preg_match("/normal-(.*)/", $cmdParam1, $aparts)) {
			$announceType = "Normal";
			$announce_id = $aparts[1];
			unset($aparts);
		}

		if (($announce_id != "") && (is_numeric($announce_id))) {
			$announceDtmfCode = getAnnounceDtmfCode($debug, $db, $announce_id);
		}

		if ($zonesProvided) {
			$zoneNumber = $zones;
			$zoneName = getZoneName($debug, $db, $zoneNumber);
		}
		break;
	case 'audio-dist':
		$audioDistCmd = $cmdParam1;
		$audiodistid  = $cmdParam2;
		$audioDistName = getProgDistName($debug, $db, $audiodistid);
		break;
	case 'emergency-call':
	case 'intercom-call':
	case 'urgent-call':
		if ($called == "") $called = $cmdParam2;
		$calledName = getStationName($debug, $db, $called);
		break;
	case 'input-contact-closed':
		$ioContactAction = "Closed";
		$ioContactStation = $cmdParam1;
		$ioContactNumber  = $cmdParam2;
		break;
	case 'input-contact-opened':
		$ioContactAction = "Opened";
		$ioContactStation = $cmdParam1;
		$ioContactNumber  = $cmdParam2;
		break;
	case 'facility-status-up':
	case 'facility-status-down':
	case 'facility-page':
		$facilityPageNumber = $cmdParam1;
		$facilityName = getFacilityName($debug, $db, $facilityPageNumber);
		break;
	case 'amp-protection-mode':
		$deviceMacAddr = $cmdParam1;
		$deviceName = getDeviceNameByMacAddr($cmdParam1);
		break;
	case 'nws-alert':
		$nwsAlertEvent = $cmdParam1;
		$nwsAlertSeverity = $cmdParam2;
		$nwsAlertCertainty = $extra_args[1];
		$nwsAlertUrgency = $extra_args[2];
		$nwsAlertResponse = $extra_args[3];
		$nwsAlertZone = $extra_args[4];
		break;
	case 'schedule-activated':
		$scheduleId = $cmdParam1;
		if ($scheduleId == 0) {
			$scheduleName = "Holiday";
		} else {
			$scheduleName = getScheduleName($debug, $db, $scheduleId);
		}
		break;
	case 'scheduled-routine':
		$schedParam1 = $cmdParam1;
		$schedParam2 = $cmdParam2;
		if ($zonesProvided) {
			$zoneNumber = $zones;
			$zoneName = getZoneName($debug, $db, $zoneNumber);
		}
		break;
	case 'sched-event':
		$eventId = $cmdParam1;
		$eventName = getEventName($debug, $db, $eventId);
		break;
	case 'scheduled-tone':
	case 'tone':
		$toneId = $cmdParam1;
		$toneDtmfCode = getToneDtmfCode($debug, $db, $toneId);
		$toneName = getToneName($debug, $db, $toneId);
		break;
	case 'station-status-up':
		$stationStatus = "Up";
		$station = $cmdParam1;
		break;
	case 'station-status-down':
		$stationStatus = "Down";
		$station = $cmdParam1;
		break;
	case 'zone-page':
		$zoneNumber = $zones;
		$zoneName = getZoneName($debug, $db, $zoneNumber);
		break;
	default:
		break;
	}

	if ($trigger == "incoming-call") {
		$caller = $cmdParam1;
		#$callerName = $cmdParam2; // Already set before function execution.
		$namecaller = $callerName;

	}
	else
	if ($trigger == "pbx-incoming-call") {
		$caller = $cmdParam1;
		# $callerName is set before function execution.
		$namecaller = $callerName;
	}
	else
	if ($origCaller == 0) {
		$caller = "System";
		$namecaller = "System";
		$callerName = "System";
	} else {
		if ($trigger != "incoming-call") {
			$caller = $origCaller;
			$namecaller = $callerName;
		}
		#else
		#{
		#	$caller = $cmdParam1;
		#	$namecaller = $cmdParam2;
		#}
	}

	if ($caller == 1) {
		$caller = "System";
		if ($callerName != "") {
			$namecaller = $callerName;
		} else {
			$namecaller = "System";
		}
	}
	else
	if ($caller == 0) {
		$caller = "System";
		$namecaller = "System";
		$callerName = "System";
	}

	#
	# Validate security of $condition string, blocking dangerous functions.
	#
	$test_condition = preg_replace("/[\s\t\n\r]*/",'',$condition);

	foreach ($block_functions as $block_function) {
		if (preg_match("/$block_function\((.*)\)/", $test_condition)) {
			if ($debug) echo "Block function $block_function\n";

			doExit(1);
		}
	}

	#
	# Don't allow ` in $condition, it would allow external code execution.
	#
	if (strpos($condition, '`') !== false) {
		doExit(1);
	}


	#
	# Perform eval for $condition
	# Set CPU time limit to 60 seconds to prevent run away loops.
	#
	set_time_limit(60);

	try {
		eval($eval_str);
	} catch (Throwable $t) {
		logText($t->getMessage(), ROUTINE_LOG_INTERNAL);
		if ($debug) {
			echo $t->getMessage(), "\n";
		}
		doExit(6);
	}

	set_time_limit(0);

	if ($debug) {
		print_r($user_var);
		echo "DoAction = $DOACTION\n";
	}

	return $DOACTION;
}

function getDeviceNameByMacAddr($deviceMacAddr)
{
	global $debug, $db;

	$device_name = "";

	$sql = "SELECT name FROM amplifier WHERE mac_address='$deviceMacAddr'";

	if (!$result = $db->query($sql)) return ""; // return should not happen

	if ($result->num_rows > 0) {
		$row = $result->fetch_assoc();
		$device_name = $row['name'];

	} else {

		$sql = "SELECT name FROM gateway WHERE mac_address='$deviceMacAddr'";

		if (!$result = $db->query($sql)) return ""; // return should not happen

		if ($result->num_rows > 0) {
			$row = $result->fetch_assoc();
			$device_name = $row['name'];
		}
	}

	return $device_name;
}

#
# dbPut() can be used in an Action Condition CODE: segment to insert or update
#         a row in the routine_db table.
#
function dbPut($key,$value)
{
	global $debug, $db, $routineId;

	if ($db == NULL) return;

	$key = $db->real_escape_string($key);

	$sql = "SELECT rdb_value FROM routine_db WHERE rdb_key='$key'";

	if (!$result = $db->query($sql)) return; // return should not happen

	$value = $db->real_escape_string($value);

	if ($result->num_rows > 0) {
		$sql = "UPDATE routine_db
			SET rdb_value='$value', routine_id='$routineId'
			WHERE rdb_key='$key'";
	} else {
		$sql = "INSERT INTO routine_db (routine_id,rdb_key,rdb_value)
			VALUES ('$routineId','$key','$value')";
	}

	$db->query($sql);
}

#
# dbGet() can be used in an Action Condition CODE: segment to retrieve the value
#         stored in the $key row.
#
function dbGet($key)
{
	global $debug, $db, $routineId;

	if (($db == NULL) || ($key == "")) return "";

	$value = "";

	$key = $db->real_escape_string($key);

	$sql = "SELECT routine_id, rdb_value FROM routine_db WHERE rdb_key='$key'";

	if (!$result = $db->query($sql)) return ""; // return should not happen

	if ($result->num_rows > 0) {
		$row = $result->fetch_assoc();
		$value = $row['rdb_value'];

		if ($row['routine_id'] == 0) {
			$db->query("UPDATE routine_db SET routine_id='$routineId' WHERE rdb_key='$key'");
		}
	}

	$result->free();

	$value = $db->real_escape_string($value);

	return $value;
}

#
# dbDelete() can be used in an Action Condition CODE: segment to delete the row
#            referenced by $key.
#
function dbDelete($key)
{
	global $debug, $db;

	if (($db == NULL) || ($key == "")) return;

	$key = $db->real_escape_string($key);

	$sql = "DELETE FROM routine_db WHERE rdb_key='$key'";

	$db->query($sql);
}

#
# dbTruncate() can be used in an Action Condition CODE: segment to truncate
#              the routine_db table (remove all rows).
#
function dbTruncate()
{
	global $debug, $db;

	if ($db == NULL) return;

	$db->query("TRUNCATE routine_db");
}

function getMaxCpuTemp()
{
	$cputemp = "";

	exec("/usr/bin/sensors", $sensor_out, $ret);

	foreach ($sensor_out as $line) {
		if (preg_match("/^Core (.*)\+([.0-9]+)(.*) \(high /", $line, $sensor_parts)) {
			if ($sensor_parts[2] > $cputemp) $cputemp = $sensor_parts[2];
		}
	}

	return $cputemp;
}

function check_db_connection()
{
	global $db, $server, $user, $pass, $database;

	if (mysqli_ping($db)) {
		return;
	}

	#
	# Try to reconnect to MySQL database server.
	#
	$tries = 0;

	while ($tries < 120) {

		$db = new mysqli($server, $user, $pass, $database);

		if ($db->connect_errno > 0) {
			sleep(1);
		} else {
			logText("Reopened MySQL connection", ROUTINE_LOG_INTERNAL);
			return;
		}

		$tries++;
	}

	#
	# If we get here it means we failed to reconnect to MySQL after 120 seconds, doExit().
	#
	$db = NULL;

	logText("Failed to reconnect to MySQL server", ROUTINE_LOG_INTERNAL);

	doExit(1);
}

# Function to get value from a nested array using a dot-separated key
function getNestedValue($array, $key) {
	$keys = explode('.', $key);
	foreach ($keys as $k) {
		if (isset($array[$k])) {
			$array = $array[$k];
		} else {
			return null; // Return null if the key doesn't exist
		}
	}
	return $array;
}

function hasCommandInjection($input)
{
	$pattern = '/[`$|&;<>]/';

	if (preg_match($pattern, $input)) {
		return true;
	} else {
		return false;
	}
}

function getCapAlertElement($name)
{
	global $cap_data;

	if ($cap_data == null) return "";

	return $cap_data->getCapAlert($name);
}

function getCapInfoElement($name)
{
	global $cap_data;

	if ($cap_data == null) return "";

	return $cap_data->getCapInfo($name);
}

function getCapEventCode($valueName)
{
	global $cap_data;

	if ($cap_data == null) return "";

	return $cap_data->getCapEventCode($valueName);
}

function getCapParameter($valueName)
{
	global $cap_data;

	if ($cap_data == null) return "";

	return $cap_data->getCapParameter($valueName);
}

function replaceCapData($input_string)
{
	global $cap_data;

	if ($cap_data == null) return $input_string;

	$new_string = $input_string;

	#
	# Process matches for eventCodes variable format: $CAP_EventCode_valueName
	#
	preg_match_all('/\$CAP_EventCode_([A-Za-z0-9_]+)/', $new_string, $matches);

	if (!empty($matches[0])) {
		foreach ($matches[1] as $index => $name) {
			$placeholder = $matches[0][$index];
			$replacement = $cap_data->getCapEventCode($name);
			$new_string = str_replace($placeholder, $replacement, $new_string);
		}
	}

	#
	# Process matches for parameter variable format: $CAP_Parameter_valueName
	#
	preg_match_all('/\$CAP_Parameter_([A-Za-z0-9_]+)/', $new_string, $matches);

	if (!empty($matches[0])) {
		foreach ($matches[1] as $index => $name) {
			$placeholder = $matches[0][$index];
			$replacement = $cap_data->getCapParameter($name);
			$new_string = str_replace($placeholder, $replacement, $new_string);
		}
	}

	#
	# Process matches for alert variable format: $CAP_Alert_valueName
	#
	preg_match_all('/\$CAP_Alert_([A-Za-z0-9_]+)/', $new_string, $matches);

	if (!empty($matches[0])) {
		foreach ($matches[1] as $index => $name) {
			$placeholder = $matches[0][$index];
			$replacement = $cap_data->getCapAlert($name);
			$new_string = str_replace($placeholder, $replacement, $new_string);
		}
	}

	#
	# Process matches for info variable format: $CAP_Info_valueName
	#
	preg_match_all('/\$CAP_Info_([A-Za-z0-9_]+)/', $new_string, $matches);

	if (!empty($matches[0])) {
		foreach ($matches[1] as $index => $name) {
			$placeholder = $matches[0][$index];
			$replacement = $cap_data->getCapInfo($name);
			$new_string = str_replace($placeholder, $replacement, $new_string);
		}
	}

	return $new_string;
}

#
# Return true if latitude, longitude in within an Area of the CAP message.
# Thank you chatGPT.
#
function isPointInCapArea($latitude, $longitude)
{
    global $cap_xml;

    if ($cap_xml == null) return false;

    $xml = simplexml_load_string($cap_xml);
    if (!$xml) {
        logText("Invalid CAP XML", ROUTINE_LOG_INTERNAL);
	return false;
    }

    $infoElements = $xml->info;

    foreach ($infoElements as $info) {
        foreach ($info->area as $area) {
            // Check all <polygon> elements in the <area>
            foreach ($area->polygon as $polygonStr) {
                $polygonPoints = [];

                foreach (explode(' ', trim((string)$polygonStr)) as $pointStr) {
                    list($lat, $lon) = explode(',', $pointStr);
                    $polygonPoints[] = [floatval($lat), floatval($lon)];
                }

                // Ensure polygon is closed
                if ($polygonPoints[0] !== end($polygonPoints)) {
                    $polygonPoints[] = $polygonPoints[0];
                }

                if (pointInPolygon($latitude, $longitude, $polygonPoints)) {
                    return true;
                }
            }

            // Check all <circle> elements in the <area>
            foreach ($area->circle as $circleStr) {
                // Format: "latitude,longitude radius" where radius is in kilometers
                if (preg_match('/^\s*([+-]?\d+(\.\d+)?),([+-]?\d+(\.\d+)?)\s+(\d+(\.\d+)?)\s*$/', trim((string)$circleStr), $matches)) {
                    $centerLat = floatval($matches[1]);
                    $centerLon = floatval($matches[3]);
                    $radiusKm  = floatval($matches[5]);

                    if (haversineDistance($latitude, $longitude, $centerLat, $centerLon) <= $radiusKm) {
                        return true;
                    }
                }
            }
        }
    }

    return false;
}

#
# Point-in-polygon test using the ray-casting algorithm.
#
function pointInPolygon($lat, $lon, $polygon)
{
    $inside = false;
    $n = count($polygon);

    for ($i = 0, $j = $n - 1; $i < $n; $j = $i++) {
        $lat_i = $polygon[$i][0];
        $lon_i = $polygon[$i][1];
        $lat_j = $polygon[$j][0];
        $lon_j = $polygon[$j][1];

        $intersect = (($lon_i > $lon) !== ($lon_j > $lon)) &&
                     ($lat < ($lat_j - $lat_i) * ($lon - $lon_i) / ($lon_j - $lon_i + 1e-10) + $lat_i);
        if ($intersect) {
            $inside = !$inside;
        }
    }

    return $inside;
}

#
# Calculates the Haversine distance (in kilometers) between two lat/lon points.
#
function haversineDistance($lat1, $lon1, $lat2, $lon2) {
    $earthRadius = 6371; // in kilometers

    $dLat = deg2rad($lat2 - $lat1);
    $dLon = deg2rad($lon2 - $lon1);

    $a = sin($dLat / 2) ** 2 +
         cos(deg2rad($lat1)) * cos(deg2rad($lat2)) *
         sin($dLon / 2) ** 2;

    $c = 2 * atan2(sqrt($a), sqrt(1 - $a));

    return $earthRadius * $c;
}

function getAudioDurationInSeconds($filename)
{
	if ($filename == "") return false;

	if (!file_exists($filename)) {
		return false;
	}

	$escapedFile = escapeshellarg($filename);

	$cmd = "ffmpeg -i $escapedFile 2>&1";
	$output = shell_exec($cmd);

	if (preg_match('/Duration: (\d+):(\d+):(\d+\.\d+)/', $output, $matches)) {
		$hours = (int)$matches[1];
		$minutes = (int)$matches[2];
		$seconds = (int)$matches[3];

		return ($hours * 3600) + ($minutes * 60) + $seconds;
	}

	return false;
}

function getWebhookReturnData()
{
	global $webhook_return_str;

	return $webhook_return_str;
}

function getWebhookReturnValue($tag)
{
	# TODO: Support XML format. Sanatize data.

	global $webhook_return_str;

	$data = json_decode($webhook_return_str,true);

	if ($data == null) return null;

	if (strpos($tag,'{') !== false) {
		$found = preg_replace_callback('/{{\s*(\w+(\.\w+)*)\s*}}/',
			function($matches) use ($data) {
				$variable = $matches[1];
				$value = getNestedValue($data, $variable);
				return $value !== null ? $value : $matches[0];
			}, $tag);

		return ($tag != $found) ? $found : null;
	}

	if (array_key_exists($tag, $data)) {
		return $data[$tag];
	}

	// Search recursively if nested
	foreach ($data as $key => $value) {
		if (is_array($value)) {
			$result = getWebhookReturnValueNested($value, $tag);
			if ($result !== null) {
				return $result;
			}
		}
	}
	return null; // Tag not found
}

function getWebhookReturnValueNested($data, $tag)
{
	if (array_key_exists($tag, $data)) {
		return $data[$tag];
	}

	// Search recursively if nested
	foreach ($data as $key => $value) {
		if (is_array($value)) {
			$result = getWebhookReturnValueNested($value, $tag);
			if ($result !== null) {
				return $result;
			}
		}
	}
	return null; // Tag not found
}

function doExit($signo)
{
	global	$debug,
		$db,
		$routineId,
		$amiSocket,
		$myPid,
		$auto_repeat,
		$announceFilename,
		$dashId,
		$displayMsgId,
		$autoDeleteDisplayMsgsUponExit,
		$autoDeleteDashMsgsUponExit,
		$post_dash_phone_text,
		$json_files_id,
		$trigger;

	if ($db != NULL) {
		if ($db->connect_errno == 0) {
			updateTaskList($debug, $db, $routineId, "DELETE", $myPid);
		}
	}

	#
	# Delete any Weather alerts posted to dashboard or admin phones.
	#
	if ($post_dash_phone_text && ($db != NULL)) {
		$db->query("DELETE FROM dashboard_msg WHERE identifier='Weather-Alert-$myPid'");

		$db->query("UPDATE yealink_message SET extension='', message='' WHERE extension='WA-$myPid'");

		if (mysqli_affected_rows($db) > 0) {
			exec("/usr/local/bin/update-yealink-idlescreen > /dev/null 2>&1 &");

			createAlertsXmlFile($debug, "");
		}
	}

	#
	# If $announceFilename is not empty, we need to delete an announcement that was
	# created but not played.
	#
	if ($announceFilename != "") {
		exec("/usr/local/bin/routine-rm-announcement $announceFilename > /dev/null 2>&1");
	}

	if ($auto_repeat) {
		$deleteId = array_pop($displayMsgId);

		if ($deleteId !== null)
		{
			exec("/usr/local/bin/display-message -r '$deleteId' > /dev/null 2>&1 &");
		}
	}

	if ($autoDeleteDisplayMsgsUponExit) {
		foreach ($displayMsgId as $deleteId) {
			exec("/usr/local/bin/display-message -r $deleteId > /dev/null 2>&1 &");
		}
	}

	if (($autoDeleteDashMsgsUponExit == true) && ($db != NULL)) {
		foreach ($dashId as $deleteId) {
			$db->query("DELETE FROM dashboard_msg WHERE id='$deleteId'");
		}
	}

	if ($amiSocket) closeAmiConnection($debug);

	if ($db != NULL) $db->close();

	#
	# Delete any unused files sent via JSON-based API
	#
	if ($json_files_id != "") {
		$json_files_id = str_replace(' ','',$json_files_id);
		$json_files_id = str_replace('*','',$json_files_id);
		$edir = "/var/www/html/laravel/public/exports";
		exec("rm -f $edir/*_$json_files_id.* >/dev/null 2>&1");
	}


	$viaTrigger = ($trigger != "") ? $trigger : "DTMF/System";

	if ($signo == 6) {
		logtext("Exited - terminated due to CODE segment error ($viaTrigger)", ROUTINE_LOG_INTERNAL);
	} else {
		logtext("Exited - terminated ($viaTrigger)", ROUTINE_LOG_INTERNAL);
	}

	exit($signo);
}

?>
