#!/usr/bin/php
<?php
#
# weather-alert
#
# Retrieve weather alerts from National Weather Service.
#
# Returns text for one or more weather alerts. Each alert starts with
# an alert header in the form: "--- ALERT --- (weather-codes)"
#
# (C) Copyright 2019-2026, Bogen Communications LLC. All rights reserved.
#
# Usage: weather-alert [-b] [-c] [-C] [-i] [-I] -z <county_code> | AUTO_FIND_COUNTY
#
# Where <county_code> is replaced by a valid county code,
#
#	Example: FLC095 is Orange County, FL
#
# Options:
#
# -b	- Append <br> to ALERT header (useful for display-msg actions).
#
# -c	- Return county code for host's IP address location and exit.
#
# -C	- Include (<Severity>,<Certainty>,<Urgency>,<Response>) codes
#	  in ALERT headers (by default, they are not included).
#
# -i	- Include "Instructions" provided by the NWS for the alert.
#
# -I	- Include County Code in ALERT header.
#
# -z <count_code> - Find alerts for <county_code>
#
# -Z <text to display> - Text to display with -I option is specified (optional, use with LATLON).
#
# Examples:
#
# To get alerts for county associated with host's public IP address, use:
#
#	weather-alert -z AUTO_FIND_COUNTY
#
#	Note:	The county code will be cached for subsequent use.
#		To update the cached value with a new county code
#		(find it again), use AUTO_FIND_COUNTY_UPDATE_CACHE
#
# To get alerts for Orange County, Florida, use:
#
#	weather-alert -z FLC095
#
# If no options or county code are supplied on command line, AUTO_FIND_COUNTY
# is the default action.
#
# Developer Notes:
#
#	['features']['0']['properties']['severity'] = Extreme, Severe, Moderate, Minor, Unknown
#	['features']['0']['properties']['certainty'] = Observed, Likely, Possible, Unlikely, Unknown
#	['features']['0']['properties']['urgency'] = Immediate, Expected, Future, Past, Unknown
#	['features']['0']['properties']['headline'] = Alert's Headline
#	['features']['0']['properties']['description'] = Alert's Description
#	['features']['0']['properties']['instruction'] = Alert's Instructions
#	['features']['0']['properties']['response'] = AllClear, Assess, Avoid, Execute, Evacuate, Monitor, None, Prepare, Shelter
#	['features']['0']['properties']['status'] = Actual, Exercise, System, Test, Draft
#	['features']['0']['properties']['messageType'] = Alert, Update, Cancel, Ack, Error
#	['features']['0']['properties']['category'] = Geo, Met, Safety, Security, Rescue, Fire, Health, Env, Transport, Infra, CBRNE, Other
#	['features']['0']['properties']['parameters']['tornadoDetection'] = RADAR INDICATED, OBSERVED
#	['features']['0']['properties']['parameters']['tornadoDamageThreat'] = CONSIDERABLE, CATASTROPHIC
#	['features']['0']['properties']['parameters']['hailSize'] = <float>
#	['features']['0']['properties']['parameters']['windGust'] = <int> MPH
#
#	certainty: Likely (p > ~50%), Possible (but not likely p <= ~50%), Unlikely (p ~ 0)
#
require '/usr/local/bin/db_credentials' ;
require '/usr/local/bin/get_public_ipaddr' ;

$debug = FALSE;
$db = NULL;
$zone = "AUTO_FIND_COUNTY";
$zone_display_name = "";
$display_county = FALSE;
$include_codes = FALSE;
$include_county = FALSE;
$include_instructions = FALSE;
$latlong_text = "";
$eol = "\n";

$cmd_options = getopt("bcCdiIz:Z:");

foreach (array_keys($cmd_options) as $opt)
{
        switch ($opt)
        {
		case 'b':
			$eol = "<br>";
			break;

		case 'c':
			$display_county = TRUE;
			break;

		case 'C':
			$include_codes = TRUE;
			break;

		case 'd':
			$debug = TRUE;
			break;

		case 'i':
			$include_instructions = TRUE;
			break;

		case 'I':
			$include_county = TRUE;
			break;
                case 'z':
                        $zone = $cmd_options['z'];
                        break;
                case 'Z':
                        $zone_display_name = $cmd_options['Z'];
                        break;

		default:
			break;
	}
}

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

if (file_exists("/etc/asterisk/serial_number")) {
	$unique_id = trim(file_get_contents("/etc/asterisk/serial_number"));

	if (strpos($unique_id, "CustomerServ") !== false) {
		$unique_id = gethostname();
	}

} else {
	$unique_id = gethostname();
}

$user_agent = "User-Agent: Bogen-Nyquist/11.0 $unique_id (weatherapi@bogen.com)";

if ($debug) {
	echo "User-Agent = $user_agent\n";
}

#$pid = getmypid();

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

#
# Changing to /tmp folder to force temporary files downloaded by wget to be
# stored in /tmp folder.
#
chdir("/tmp");

if (strpos($zone, 'ALERT_DEMO') !== false)
{
	chdir("/var/opt/nyquist/demo");

	goto demoBranch;
}

#
# Find county code...
#
if (($zone == "AUTO_FIND_COUNTY") || ($zone == "AUTO_FIND_COUNTY_UPDATE_CACHE"))
{
   if (($zone == "AUTO_FIND_COUNTY_UPDATE_CACHE") || ($zone = file_get_contents("/etc/asterisk/weather_county")) === false)
   {
	if ($debug) echo "Auto find county...\n";

	$myIp = get_public_ipaddr($debug, $use_cache = TRUE, $update_cache = FALSE);

	if ($debug) echo "MyIP: $myIp\n";

	$myIp = trim($myIp);

	#
	# Try to get lat/long from cache, otherwise from ipapi.co,
	# exit if we can't get lat/long
	#
	$latlong = "";

	$latlong = file_get_contents('/tmp/nyquist_my_latlong');

	if ($latlong != "") {
		$latlong = trim($latlong);

		if ($debug) echo "Got lat/long ($latlong) from cache\n";
	}
	else
	if (($latlong = file_get_contents('https://ipapi.co/'.$myIp.'/latlong/')) === false) {
		exit;
	} else {
		file_put_contents("/tmp/nyquist_my_latlong", $latlong);
	}

	exec("chown -f root:www-data /tmp/nyquist_my_latlong;chmod -f 660 /tmp/nyquist_my_latlong");

	if ($debug) echo "LAT-LONG: $latlong\n";

	if ($latlong == "") exit;

	$tmpfile = tempnam("/tmp", "POINTS_");

	$sout = shell_exec("wget --quiet --header=\"$user_agent\" --header='Accept: application/geo+json' --timeout=120 --output-document=$tmpfile https://api.weather.gov/points/$latlong > /dev/null 2>&1");

	$points = file_get_contents($tmpfile);

	unlink($tmpfile);

	$points_json = json_decode($points, true);

	if ($debug)
	{
		print_r($points_json);

		echo $points_json['properties']['county'];

		echo "\n";
	}

	if (isset($points_json['properties']['county']))
	{
		preg_match("/https:\/\/api.weather.gov\/zones\/county\/(.*)/",
				$points_json['properties']['county'],
				$county_array);

		if ($debug) echo "County: $county_array[1]\n";

		$zone = $county_array[1];
	}

	if ($zone != "")
	{
		file_put_contents("/etc/asterisk/weather_county", $zone);

                exec("chown -f root:www-data /etc/asterisk/weather_county;chmod -f 660 /etc/asterisk/weather_county");
	}

   }
   else
   {
	if ($debug) echo "Using cached county code: $zone\n";
   }

   $zone = trim($zone);

   if ($display_county)
   {
	echo $zone;
	exit;
   }
}

$zone = trim($zone);

if (isset($json)) {
	unset($json);
}

if (preg_match("/LATLON:(.*)/", $zone, $latlon)) {
	$zone = "LATLON.$latlon[1]";

	if ($debug) echo "LATLON Zone: $latlon[1]\n";

	$latlong_text = $latlon[1];

	$tmpfile = tempnam("/tmp", "LATLON_");

	$sout = shell_exec("wget --quiet --header=\"$user_agent\" --header='Accept: application/geo+json' --retry-on-host-error --retry-on-http-error=499,502,503,504 --timeout=120 --output-document=$tmpfile https://api.weather.gov/alerts/active?point=$latlon[1] > /dev/null 2>&1");

	$json = json_decode(file_get_contents($tmpfile), true);

	unlink($tmpfile);

	#
	# If no alert for LATLON, check zone.
	#
	if (isset($json['features'])
		&& is_array($json['features'])
		&& (count($json['features']) === 0)
	        && !file_exists('/var/opt/nyquist/weather_dont_try_zone_after_latlong')) {

		$zone = getNwsCountyCode($debug, $latlon[1]);

		if ($zone != "") {

			if ($debug) echo "Trying $zone from lat/long\n";

			$tmpfile2 = tempnam("/tmp", "{$zone}_");

			$sout = shell_exec("wget --quiet --header=\"$user_agent\" --header='Accept: application/geo+json' --retry-on-host-error --retry-on-http-error=500,502,503,504 --timeout=120 --output-document=$tmpfile2 https://api.weather.gov/alerts/active/zone/$zone > /dev/null 2>&1");

			$latlong_text = "";

			$json = json_decode(file_get_contents($tmpfile2), true);

			unlink($tmpfile2);
		}
	}

} else {
	$tmpfile = tempnam("/tmp", "{$zone}_");

	$sout = shell_exec("wget --quiet --header=\"$user_agent\" --retry-on-host-error  --header='Accept: application/geo+json' --retry-on-http-error=500,502,503,504 --timeout=120 --output-document=$tmpfile https://api.weather.gov/alerts/active/zone/$zone > /dev/null 2>&1");

	$json = json_decode(file_get_contents($tmpfile), true);

	unlink($tmpfile);
}

demoBranch:

if (strpos($zone, 'ALERT_DEMO') !== false) {

	if (!file_exists("$zone")) exit;

	$json = json_decode(file_get_contents("$zone"), true);
}

if (!isset($json)) exit;

if ($debug) {
	print_r($json);
	echo "Output for consumption...\n";
}

$weather_alerts = "";

$feature_count = isset($json['features']) ? count($json['features']) : 0;

for ($idx = 0; $idx < $feature_count; $idx++) {

  if (!isset($json['features'][$idx]['properties'])) {
     continue;
  }

  if (isset($json['features']["$idx"]['properties']['description']))
  {
	if ($debug)
	{
		echo "\nEvent: ";
		echo $json['features']["$idx"]['properties']['event'];
		echo "\nSeverity: ";
		echo $json['features']["$idx"]['properties']['severity'];
		echo "\nUrgency: ";
		echo $json['features']["$idx"]['properties']['urgency'];
		echo "\n";
	}

	if (isset($json['features']["$idx"]['properties']['expires']))
	{
		$expires = trim($json['features']["$idx"]['properties']['expires']);

		if ($debug) echo "Expires: $expires\n";

		$timestamp = strtotime($expires);

		if (time() > $timestamp)
		{
			#
			# Skip the alert if expired, but allow DEMO alerts to display even if expired.
			#
			if (strpos($zone, 'ALERT_DEMO') === false)
			{
				if ($debug) echo "Alert has expired\n";

				# Not filtering based on expired because we are already looking at
				# the active list and "ends" may be later than "expires".
				#
				#$idx++;
				#continue;
			}
		}
	}

	if (isset($json['features']["$idx"]['properties']['event']))
	{
		$alert_event = trim($json['features']["$idx"]['properties']['event']);
	}
	else
	{
		$alert_event = "Event Unknown";
	}

	if (isset($json['features']["$idx"]['properties']['severity']))
	{
		$severity_str = $json['features']["$idx"]['properties']['severity'];
		$alert_severity = strtolower($severity_str);
		$alert_severity = "severity_".trim($alert_severity);
	}
	else
	{
		$alert_severity = "severity_notset";
		$severity_str = "";
	}

	if (isset($json['features']["$idx"]['properties']['certainty']))
	{
		$certainty_str = $json['features']["$idx"]['properties']['certainty'];
		$alert_certainty = strtolower($certainty_str);
		$alert_certainty = "certainty_".trim($alert_certainty);
	}
	else
	{
		$alert_certainty = "certainty_notset";
		$certainty_str = "";
	}

	if (isset($json['features']["$idx"]['properties']['urgency']))
	{
		$urgency_str = $json['features']["$idx"]['properties']['urgency'];
		$alert_urgency = strtolower($urgency_str);
		$alert_urgency = "urgency_".trim($alert_urgency);
	}
	else
	{
		$alert_urgency = "urgency_notset";
		$urgency_str = "";
	}

	if (isset($json['features']["$idx"]['properties']['response']))
	{
		$response_str = $json['features']["$idx"]['properties']['response'];
		$response_str_orig = $response_str;
		$alert_response = strtolower($response_str);
		$alert_response = "response_".trim($alert_response);

		switch ($response_str)
		{
			case "AllClear":
			case "Assess":
			case "Execute":
			case "None":
				$response_str = "";
				break;

			default:
				$response_str = ",$response_str";
				break;
		}
	}
	else
	{
		$alert_response = "response_notset";
		$response_str = "";
	}

	if ($debug) {
		echo "Alert categories:\nSeverity: $alert_severity\nCertainty: $alert_certainty\nUrgency: $alert_urgency\nReponse: $alert_response\n";
	}

	$sql = "SELECT id FROM weather_alert_filter
		WHERE enabled=1
		AND action_type='display-msg'
		AND event='$alert_event'
		AND $alert_severity='1'
		AND $alert_certainty='1'
		AND $alert_urgency='1'
		AND $alert_response='1'";

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

	#
	# Connect to MySQL (doSqlConnect exists if can't connect)
	#
	doSqlConnect();

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

		exit;
	}

	if ($result->num_rows == 0)
	{
		if ($debug) echo "Alert filtered\n";

		continue;
	}

	#
	# Filter out test messages
	#
	if (strpos($json['features']["$idx"]['properties']['description'], '...THIS_MESSAGE_IS_FOR_TEST_PURPOSES_ONLY') !== false) {
		if ($debug) echo "Alert filtered\n";

		continue;
	}


	if ($weather_alerts != "") $weather_alerts.= "<br>";

	if (isset($json['features']["$idx"]['properties']['event']))
	{
		$event_txt = " ".$json['features']["$idx"]['properties']['event']." ";
	}
	else
	{
		$event_txt = " ";
	}

	if ($include_county) {
		if ($zone_display_name !== "") {
			$county_text = "($zone_display_name) ";
		} elseif ($latlong_text !== "") {
			$county_text = "(LatLong: $latlong_text)";
		} else {
			$county_text = "($zone) ";
		}
	} else {
		$county_text = "";
	}

	#
	# WARNING:
	#
	# The "--- ALERT.*---" format is used by the Display-Msg action $alerts() variable to post weather alerts
	# to dashboard and admin phones; so do not change it without updating the routine-execute script.
	#
	if ($include_codes)
	{
		$weather_alerts.= "--- ALERT $county_text---$event_txt($severity_str,$certainty_str,$urgency_str$response_str)$eol";
	}
	else
	{
		$weather_alerts.= "--- ALERT $county_text---$event_txt$eol";
	}

	if (isset($json['features']["$idx"]['properties']['headline'])) {
		$weather_alerts.= $json['features']["$idx"]['properties']['headline']."\n";
	}

	$weather_alerts.= $json['features']["$idx"]['properties']['description'];

	if ($include_instructions && isset($json['features']["$idx"]['properties']['instruction']))
	{
		$weather_alerts.= $eol."Instructions:$eol";
		$weather_alerts.= $json['features']["$idx"]['properties']['instruction'];
	}

	#
	# Execute nws-alert trigger
	#
	$save_zone = $zone;
	if ($zone_display_name != "") {
		$zone = $zone_display_name;
	} elseif (strpos($zone, 'LATLON') !== false) {
		$zone = $latlon[1];
	}

	exec("/usr/local/bin/routine-trigger -t nws-alert -c 0 -P \"$alert_event\" -p \"$severity_str\" -1 \"$certainty_str\" -2 \"$urgency_str\" -3 \"$response_str_orig\" -4 \"$zone\" > /dev/null 2>&1 &");
  }
  else
  {
	if ($debug) echo "No Descripton for $idx, skipping\n";
  }

  if (isset($save_zone)) {
     $zone = $save_zone;
  }
}

echo $weather_alerts;

if ($db != NULL) $db->close();

exit;

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

#
# Open MySql database connection, keep trying upon failure to connect.
#
function doSqlConnect()
{
        global $debug, $db, $server, $user, $pass, $database;

	if ($db != NULL) return;

        if ($debug) echo "Creating SQL connection\n";

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

	$tries = 0;

        while (($db->connect_errno > 0))
        {
		if ($tries++ > 5) exit;

                sleep(5);

                pcntl_signal_dispatch();

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

#
# Return zone from lat,long
#
function getNwsCountyCode($debug, $latlong)
{
    global $user_agent;

    $zone = "";

    if ($debug) {
        echo "getNwsCountyCode($latlong)\n";
    }

    if (!preg_match('/^-?\d+(\.\d+)?,-?\d+(\.\d+)?$/', $latlong)) {
        return "";
    }

    $url = "https://api.weather.gov/points/" . rawurlencode($latlong);

    $context = stream_context_create([
        'http' => [
            'timeout' => 120,
            'header' => "$user_agent\r\n"
        ]
    ]);

    $points = file_get_contents($url, false, $context);

    if ($points === false) {
        return "";
    }

    $points_json = json_decode($points, true);

    if (!is_array($points_json)) {
        return "";
    }

    if ($debug) {
        print_r($points_json);
        echo "\n";
    }

    if (isset($points_json['properties']['county'])) {

        if (preg_match(
            '#^https://api\.weather\.gov/zones/county/([A-Z0-9]+)$#',
            $points_json['properties']['county'],
            $matches
        )) {
            $zone = $matches[1];

            if ($debug) {
                echo "County: $zone\n";
            }
        }
    }

    return $zone;
}

?>
