Created
October 17, 2025 22:03
-
-
Save scottyob/576c2e94393be62b815fb334e3ee2cba to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* Plugin Name: Mission Roads | |
* Description: Checks recent rainfall hourly and reports whether the road is open. Dates and "current day" logic use America/Los_Angeles (San Francisco) time; current day includes hours up to the current hour plus minutes from the current hour. | |
* Version: 1.4.0 | |
* Author: Scott | |
*/ | |
if (!defined('ABSPATH')) exit; | |
// --- Configuration --- | |
define('MISSION_ROADS_LAT', 37.5483); | |
define('MISSION_ROADS_LON', -121.9886); | |
define('MISSION_ROADS_APPID', 'redacted'); | |
define('MISSION_ROADS_DAYS_BACK', 5); | |
// --- Adjustable rainfall thresholds (in inches) --- | |
define('MISSION_ROADS_LIMIT_3DAY', 0.05); | |
define('MISSION_ROADS_LIMIT_5DAY', 0.20); | |
// --- Misc --- | |
define('MISSION_ROADS_TIMEZONE', 'America/Los_Angeles'); // San Francisco | |
define('MISSION_ROADS_USER_AGENT', 'mission-roads-plugin/1.4'); | |
// --- Activation / Deactivation --- | |
register_activation_hook(__FILE__, function () { | |
if (!wp_next_scheduled('mission_roads_hourly_event')) { | |
wp_schedule_event(time(), 'hourly', 'mission_roads_hourly_event'); | |
} | |
}); | |
register_deactivation_hook(__FILE__, function () { | |
wp_clear_scheduled_hook('mission_roads_hourly_event'); | |
}); | |
// --- HTTP helper --- | |
function mission_roads_http_get_json($url) { | |
$args = [ | |
'timeout' => 15, | |
'headers' => [ | |
'User-Agent' => MISSION_ROADS_USER_AGENT, | |
], | |
]; | |
$resp = wp_remote_get($url, $args); | |
if (is_wp_error($resp)) return null; | |
$code = wp_remote_retrieve_response_code($resp); | |
if ($code < 200 || $code >= 300) return null; | |
$body = wp_remote_retrieve_body($resp); | |
$json = json_decode($body, true); | |
return is_array($json) ? $json : null; | |
} | |
function mission_roads_mm_to_inches($mm) { | |
return $mm === null ? 0.0 : $mm / 25.4; | |
} | |
// --- Fetch past day summary (previous days only) --- | |
function mission_roads_fetch_past_day_summary_mm($lat, $lon, $appid, $date_yyyy_mm_dd) { | |
$url = add_query_arg([ | |
'lat' => $lat, | |
'lon' => $lon, | |
'date'=> $date_yyyy_mm_dd, | |
'appid'=> $appid, | |
], 'https://api.openweathermap.org/data/3.0/onecall/day_summary'); | |
$data = mission_roads_http_get_json($url); | |
if (!$data) return null; | |
if (isset($data['precipitation']['total'])) return (float)$data['precipitation']['total']; | |
if (isset($data['rain']) && is_numeric($data['rain'])) return (float)$data['rain']; | |
if (isset($data['precip_mm']) && is_numeric($data['precip_mm'])) return (float)$data['precip_mm']; | |
return null; | |
} | |
// --- Fetch hourly + minutely for today --- | |
function mission_roads_fetch_today_summary_mm($lat, $lon, $appid) { | |
$tz = new DateTimeZone(MISSION_ROADS_TIMEZONE); | |
$now = new DateTime('now', $tz); | |
$current_ts = $now->getTimestamp(); | |
$current_hour = (int)$now->format('G'); | |
$current_date_str = $now->format('Y-m-d'); | |
// Fetch One Call 3.0 including minutely | |
$url = add_query_arg([ | |
'lat' => $lat, | |
'lon' => $lon, | |
'exclude' => 'daily,alerts,current', | |
'appid' => $appid, | |
'units' => 'metric', | |
], 'https://api.openweathermap.org/data/3.0/onecall'); | |
$data = mission_roads_http_get_json($url); | |
if (!$data) return 0.0; | |
$total_mm = 0.0; | |
// --- Hourly entries --- | |
if (isset($data['hourly']) && is_array($data['hourly'])) { | |
foreach ($data['hourly'] as $h) { | |
if (!isset($h['dt'])) continue; | |
$dt_utc = (int)$h['dt']; | |
$dt_local = (new DateTime('@'.$dt_utc))->setTimezone($tz); | |
$entry_date = $dt_local->format('Y-m-d'); | |
$entry_hour = (int)$dt_local->format('G'); | |
if ($entry_date !== $current_date_str) continue; | |
if ($entry_hour > $current_hour) continue; | |
if ($dt_utc > $current_ts) continue; | |
$mm = mission_roads_extract_hour_precip_mm($h); | |
$total_mm += $mm; | |
} | |
} | |
// --- Minutely entries (current hour only, up to now) --- | |
if (isset($data['minutely']) && is_array($data['minutely'])) { | |
foreach ($data['minutely'] as $m) { | |
if (!isset($m['dt'])) continue; | |
$dt_utc = (int)$m['dt']; | |
$dt_local = (new DateTime('@'.$dt_utc))->setTimezone($tz); | |
$entry_date = $dt_local->format('Y-m-d'); | |
$entry_hour = (int)$dt_local->format('G'); | |
if ($entry_date !== $current_date_str) continue; | |
if ($entry_hour !== $current_hour) continue; | |
if ($dt_utc > $current_ts) continue; | |
if (isset($m['precipitation'])) { | |
// mm per minute | |
$total_mm += (float)$m['precipitation']; | |
} | |
} | |
} | |
return $total_mm; | |
} | |
// --- Extract precipitation from hourly entry --- | |
function mission_roads_extract_hour_precip_mm($hour_entry) { | |
if (!$hour_entry || !is_array($hour_entry)) return 0.0; | |
if (isset($hour_entry['rain']['1h'])) return (float)$hour_entry['rain']['1h']; | |
if (isset($hour_entry['rain']) && is_numeric($hour_entry['rain'])) return (float)$hour_entry['rain']; | |
if (isset($hour_entry['snow']['1h'])) return (float)$hour_entry['snow']['1h']; | |
if (isset($hour_entry['snow']) && is_numeric($hour_entry['snow'])) return (float)$hour_entry['snow']; | |
if (isset($hour_entry['precipitation']) && is_numeric($hour_entry['precipitation'])) return (float)$hour_entry['precipitation']; | |
return 0.0; | |
} | |
// --- Hourly cron job --- | |
add_action('mission_roads_hourly_event', function () { | |
$tz = new DateTimeZone(MISSION_ROADS_TIMEZONE); | |
$now_local = new DateTime('now', $tz); | |
$current_date_str = $now_local->format('Y-m-d'); | |
$daily = []; | |
// --- Today --- | |
$today_mm = mission_roads_fetch_today_summary_mm(MISSION_ROADS_LAT, MISSION_ROADS_LON, MISSION_ROADS_APPID); | |
$daily[] = [ | |
'date' => $current_date_str, | |
'precip_mm' => $today_mm, | |
'precip_in' => mission_roads_mm_to_inches($today_mm), | |
]; | |
// --- Previous days --- | |
for ($i = 1; $i < MISSION_ROADS_DAYS_BACK; $i++) { | |
$day_dt = (clone $now_local)->sub(new DateInterval("P{$i}D")); | |
$date_str = $day_dt->format('Y-m-d'); | |
$mm = mission_roads_fetch_past_day_summary_mm(MISSION_ROADS_LAT, MISSION_ROADS_LON, MISSION_ROADS_APPID, $date_str); | |
if ($mm === null) $mm = 0.0; | |
$daily[] = [ | |
'date' => $date_str, | |
'precip_mm' => $mm, | |
'precip_in' => mission_roads_mm_to_inches($mm), | |
]; | |
} | |
// Compute sums | |
$precip5 = 0.0; | |
$precip3 = 0.0; | |
for ($i = 0; $i < count($daily); $i++) { | |
$precip5 += $daily[$i]['precip_in']; | |
if ($i < 3) $precip3 += $daily[$i]['precip_in']; | |
} | |
$status = 'OPEN'; | |
$reason = ''; | |
if ($precip3 > MISSION_ROADS_LIMIT_3DAY) { | |
$status = 'CLOSED'; | |
$reason = sprintf('%.2f inches over 3 days is more than our limit of %.2f inches.', $precip3, MISSION_ROADS_LIMIT_3DAY); | |
} elseif ($precip5 > MISSION_ROADS_LIMIT_5DAY) { | |
$status = 'CLOSED'; | |
$reason = sprintf('%.2f inches over 5 days is more than our limit of %.2f inches.', $precip5, MISSION_ROADS_LIMIT_5DAY); | |
} | |
// Build verbose report | |
$verbose = "Daily precipitation summary (inches):\n"; | |
foreach ($daily as $day) { | |
$verbose .= sprintf("%s: %.2f in (%.1f mm)\n", $day['date'], $day['precip_in'], $day['precip_mm']); | |
} | |
$verbose .= sprintf( | |
"\nTotal past 3 days: %.2f in (limit %.2f)\nTotal past %d days: %.2f in (limit %.2f)\n\nRoad status: %s\n%s\n", | |
$precip3, | |
MISSION_ROADS_LIMIT_3DAY, | |
min(MISSION_ROADS_DAYS_BACK, 5), | |
$precip5, | |
MISSION_ROADS_LIMIT_5DAY, | |
$status, | |
$reason | |
); | |
update_option('mission_roads_status', $status); | |
update_option('mission_roads_reason', $reason); | |
update_option('mission_roads_verbose', $verbose); | |
$last_run_local = $now_local->format('Y-m-d H:i:s'); | |
update_option('mission_roads_last_run', $last_run_local); | |
}); | |
// --- Shortcode --- | |
add_shortcode('mission_roads', function () { | |
$status = get_option('mission_roads_status', 'Unknown'); | |
$reason = get_option('mission_roads_reason', ''); | |
$verbose = get_option('mission_roads_verbose', 'No data yet.'); | |
$last_run = esc_html(get_option('mission_roads_last_run', 'never')); | |
$is_closed = strtoupper($status) === 'CLOSED'; | |
$bg = $is_closed ? '#ffe6e6' : '#e6ffe6'; | |
$border = $is_closed ? '#cc0000' : '#00a000'; | |
$title_color = $is_closed ? '#a00000' : '#007000'; | |
$title_text = "Road Status: " . strtoupper($status); | |
$description = "As the road to Mission is awaiting repair, we are asked <strong>NOT</strong> to drive on it after rain, and when the road is wet. We have put in place guidelines to help us follow this and are opening or closing the road based on recent recorded rain."; | |
$details_html = "<details style='margin-top:4px;'> | |
<summary style='cursor:pointer;font-weight:600;font-size:0.9em;'>View Rain Details</summary> | |
<pre style='background:#fafafa;border:1px solid #ddd;border-radius:4px;padding:6px;white-space:pre-wrap;margin-top:4px;font-size:0.9em;line-height:1.2em;'>" | |
. esc_html($verbose) . | |
"</pre> | |
<p style='font-size:0.8em;color:#666;margin-top:2px;padding-bottom:0px'>Last updated: {$last_run} (America/Los_Angeles)</p> | |
<p style='font-size:0.8em;color:#666;margin-top:1px;'>Data from <a href='https://openweathermap.org'>OpenWeatherMap</a></p> | |
</details>"; | |
return <<<HTML | |
<div style=" | |
background: {$bg}; | |
border: 1px solid {$border}; | |
border-radius: 6px; | |
padding: 8px 10px; | |
max-width: 400px; | |
margin: 0.5em auto; | |
font-family: system-ui, sans-serif; | |
font-size: 1em; | |
"> | |
<div style="color: {$title_color}; font-weight:700; font-size:1em;">{$title_text}</div> | |
<p style="margin:4px 0; line-height:1.3em;">{$description}</p> | |
<p style="margin:3px 0; font-style:italic; color:#444; font-size:0.85em;">{$reason}</p> | |
{$details_html} | |
</div> | |
HTML; | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment