<?php
error_reporting(E_ALL);
libxml_use_internal_errors(true);
// ********************* //
// ***** FUNCTIONS ***** //
// ********************* //
class BDWeather {
// Extract a time-layout structure from the overall document ($X) for a
// given $Element. Basically, $Element has a time-layout attribute. The
// value of that attribute specifies a time-layout element to use for
// mapping the values in the element to a series of timestamps.
public static function getLayout($X, $Element) {
$layout = $Element->attributes()->{"time-layout"};
$times = self::getPath($X, "data/time-layout[layout-key='{$layout}']");
if (count($times) == 0) throw new Exception("Could not find time layout [{$layout}]");
return current($times);
}
// Do the basic rest call to get the weather data we want.
public static function restCall($zip) {
$params = array(
"whichClient" => "NDFDgenMultiZipCode",
"zipCodeList" => $zip,
"product" => "time-series",
"temp" => "temp",
"wspd" => "wspd",
"sky" => "sky",
"precipa_r" => "precipa_r",
);
$uri_base = "http://www.weather.gov/forecasts/xml/sample_products/browser_interface/ndfdXMLclient.php";
$uri = "{$uri_base}?" . http_build_query($params);
$result = file_get_contents($uri);
try {
$X = new SimpleXMLElement($result);
} catch (Exception $E) {
// Add the result into the exception and rethrow it, so we can see what the bad XML was later.
$E->result = $result;
throw $E;
}
return $X;
}
// Given a SimpleXMLElement object, and an XPath path, do the search, and deal with possible error conditions.
public static function getPath($X, $path) {
if (!($X instanceof SimpleXMLElement)) throw new Exception("X is not a SimpleXMLElement. It's a [" . gettype($X) . "].");
$elements = $X->xpath($path);
if (!is_array($elements)) throw new Exception("xpath call [{$path}] did not return an array. It returned a [" . gettype($elements) . "].");
return $elements;
}
// Standardized short time format.
public static function shortTime($time) {
return date("g", $time) . substr(date("a", $time), 0, 1);
}
// Given a zip code, return an array of formatted weather data.
public static function getWeather($zip) {
$X = self::restCall($zip);
$fp = fopen("~/logs/xml_weather", "a");
fwrite($fp, date("c") . "\n\n");
fwrite($fp, $X->asXML() . "\n\n");
fclose($fp);
// Get the 'temperature' element
$temps = self::getPath($X, "data/parameters/temperature[@type='hourly']");
if (count($temps) == 0) throw new Exception("Did not find an hourly 'temperature' element.");
// There will only be one, so fetch it out of the array.
$X_Temp = current($temps);
// Now get the temperature values.
$temp_values = self::getPath($X_Temp, 'value');
if (count($temp_values) == 0) throw new Exception("Did not find any hourly temperature values.");
// Get the 'wind-speed' element.
$winds = self::getPath($X, "data/parameters/wind-speed");
if (count($winds) == 0) throw new Exception("Did not find a 'wind-speed' element.");
$X_Wind = current($winds);
$wind_values = self::getPath($X_Wind, 'value');
if (count($wind_values) == 0) throw new Exception("Did not find any wind speed values.");
// Get the 'cloud-amount' element.
$clouds = self::getPath($X, "data/parameters/cloud-amount");
if (count($clouds) == 0) throw new Exception("Did not find a 'cloud-amount' element.");
$X_Cloud = current($clouds);
$cloud_values = self::getPath($X_Cloud, 'value');
if (!is_array($temp_values)) throw new Exception("Did not find any cloud cover values.");
$temps = array();
$winds = array();
$clouds = array();
$hours = array();
$start = null;
$max_wind = 0;
// What I want is to go through the array of dates, and for each one, get the next max/min temp. They correspond.
$Times = self::getLayout($X, $X_Temp);
foreach ($Times->{"start-valid-time"} as $Time) {
if ($start === null) $start = date("H:i:s", strtotime((string)$Time));
$hours[] = self::shortTime(strtotime((string)$Time));
$temps[] = current($temp_values);
$max_wind = max($max_wind, current($wind_values));
$winds[] = current($wind_values);
$clouds[] = current($cloud_values);
next($temp_values);
next($wind_values);
next($cloud_values);
if (count($temps) == 4) break;
}
$out = array();
$out["hours"] = "{$zip}: " . implode(" ", $hours) . "\n";
$out["temps"] = "F* " . implode(",", $temps) . "\n";
$out["clouds"] = "Cl: " . implode(",", $clouds) . "\n";
$out["winds"] = "Wn: " . implode(",", $winds) . "\n";
if ($max_wind >= 20) $out["windsev"] = "Very windy\n";
else if ($max_wind >= 10) $out["windsev"] = "Windy\n";
else $out["windsev"] = "";
return $out;
}
// Format getWeather parts in the way Alpha likes.
public static function formatAlpha($parts) {
$msg = $parts["hours"];
$msg .= $parts["temps"];
$msg .= $parts["clouds"];
$msg .= $parts["winds"];
if (array_key_exists('weathers', $parts)) $msg .= $parts["weathers"];
return $msg;
}
// Format getWeather parts in the way Bravo likes.
public static function formatBravo($parts) {
$msg = $parts["hours"];
$msg .= $parts["temps"];
$msg .= $parts["clouds"];
$msg .= $parts["windsev"];
if (array_key_exists('weathers', $parts)) $msg .= $parts["weathers"];
return $msg;
}
// Send a text message to Alpha.
public static function textAlpha($msg) {
echo("Texting Alpha...\n");
$gateway_address = "foo@bar.net";
self::sendSMS($gateway_address, $msg);
}
// Send a text message to Bravo.
public static function textBravo($msg) {
echo("Texting Bravo...\n");
$gateway_address = "foo@bar.net";
self::sendSMS($gateway_address, $msg);
}
public static function sendSMS($addr, $msg) {
$msg = date("c") . "\n" . substr($msg, 0, 140 - 37);
mail($addr, "", $msg, "From: a@b.c");
}
}
// ***************** //
// ***** LOGIC ***** //
// ***************** //
date_default_timezone_set('America/New_York');
$dump = false;
$text = false;
if (php_sapi_name() == "cli") {
if ($argc == 2 && $argv[1] == "dump") $dump = true;
else if ($argc == 2 && array_search('text', $argv)) $text = true;
} else if (php_sapi_name() == "cgi") {
if (array_key_exists('dump', $_GET) && $_GET['dump'] == '1') $dump = true;
else if (array_key_exists('text', $_GET) && $_GET['text'] == '1') $text = true;
} else {
echo("Only CLI and CGI are allowed. Exiting.\n");
exit;
}
if ($dump) {
try {
$X = BDWeather::restCall(10101);
} catch (Exception $E) {
$X = new SimpleXMLElement("<error>There was an error: [" . $E->getMessage() . "], [" . htmlspecialchars($E->result) . "]</error>");
}
header("Content-Type: text/xml");
echo($X->asXML());
exit;
} else {
$sent = false;
echo("<pre>\n");
$count = 0;
$max = 20;
$sec = 15;
while ($count < $max) {
$count++;
echo(date("c") . "\n\n");
try {
$msgs = array();
if (date("N") >= 6) {
$parts = BDWeather::getWeather(10101);
$msg = BDWeather::formatAlpha($parts);
$msgs["alpha"] = $msg;
echo($msg);
var_dump(strlen($msg));
echo("\n");
$msg = BDWeather::formatBravo($parts);
$msgs["bravo"] = $msg;
echo($msg);
var_dump(strlen($msg));
echo("\n");
} else {
$parts = BDWeather::getWeather(10102);
$msg = BDWeather::formatAlpha($parts);
$msgs["alpha"] = $msg;
echo($msg . "\n\n");
var_dump(strlen($msg));
echo("\n");
$parts = BDWeather::getWeather(10101);
$msg = BDWeather::formatBravo($parts);
$msgs["bravo"] = $msg;
echo($msg . "\n\n");
var_dump(strlen($msg));
echo("\n");
}
if ($text) {
foreach ($msgs as $target => $msg) {
if ($target == 'alpha') BDWeather::textAlpha($msg);
if ($target == 'bravo') BDWeather::textBravo($msg);
}
}
$count = $max;
$sent = true;
} catch (Exception $E) {
echo("Something went wrong: [" . $E->getMessage() . "].\nSleeping for {$sec} seconds and trying again (this was attempt {$count} of {$max}).\n");
echo($E->getTraceAsString());
flush();
sleep($sec);
}
}
if (!$sent) {
echo("Could not get weather report.<br>");
if ($text) {
echo("Sending failure text messages.<br>");
BDWeather::textAlpha("Could not get weather report. Check the log.");
BDWeather::textBravo("Sorry, I couldn't generate the weather report today. Tell Alpha!");
}
}
}