304 lines
10 KiB
PHP
304 lines
10 KiB
PHP
<?php
|
||
/**
|
||
* MODULE: Weather (Open-Meteo, no API key)
|
||
* ---------------------------------------
|
||
* Provides three actions:
|
||
* @weather_today Lisbon
|
||
* @weather_tomorrow Lisbon
|
||
* @weather_this_week Lisbon
|
||
*
|
||
* Uses Open-Meteo Geocoding + Forecast APIs (free, public).
|
||
* Returns human-readable summaries (temps °C, precip, wind + weather emoji).
|
||
*/
|
||
|
||
// ───────────────────────────────────────────────────────────────────────────────
|
||
// 1) Register actions
|
||
// ───────────────────────────────────────────────────────────────────────────────
|
||
registerAction(
|
||
(new Action())
|
||
->setFunctionName("weather_today")
|
||
->setArgumentsTypes(["string"]) // location
|
||
);
|
||
|
||
registerAction(
|
||
(new Action())
|
||
->setFunctionName("weather_tomorrow")
|
||
->setArgumentsTypes(["string"]) // location
|
||
);
|
||
|
||
registerAction(
|
||
(new Action())
|
||
->setFunctionName("weather_this_week")
|
||
->setArgumentsTypes(["string"]) // location
|
||
);
|
||
|
||
// ───────────────────────────────────────────────────────────────────────────────
|
||
// 2) Public action functions
|
||
// ───────────────────────────────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* @param string $place e.g. "Lisbon"
|
||
* @return string
|
||
*/
|
||
function weather_today(string $place): string {
|
||
$ctx = wm_build_context($place);
|
||
if (isset($ctx['error'])) return $ctx['error'];
|
||
|
||
// index 0 = today
|
||
$i = 0;
|
||
$d = $ctx['daily'];
|
||
if (!isset($d['time'][$i])) return "No forecast found for today in {$ctx['name']}.";
|
||
|
||
$dateLabel = wm_pretty_date($d['time'][$i], $ctx['tz']);
|
||
return wm_format_day_summary("Today in {$ctx['name']}", $dateLabel, $d, $i);
|
||
}
|
||
|
||
/**
|
||
* @param string $place
|
||
* @return string
|
||
*/
|
||
function weather_tomorrow(string $place): string {
|
||
$ctx = wm_build_context($place);
|
||
if (isset($ctx['error'])) return $ctx['error'];
|
||
|
||
// index 1 = tomorrow
|
||
$i = 1;
|
||
$d = $ctx['daily'];
|
||
if (!isset($d['time'][$i])) return "No forecast found for tomorrow in {$ctx['name']}.";
|
||
|
||
$dateLabel = wm_pretty_date($d['time'][$i], $ctx['tz']);
|
||
return wm_format_day_summary("Tomorrow in {$ctx['name']}", $dateLabel, $d, $i);
|
||
}
|
||
|
||
/**
|
||
* @param string $place
|
||
* @return string
|
||
*/
|
||
function weather_this_week(string $place): string {
|
||
$ctx = wm_build_context($place);
|
||
if (isset($ctx['error'])) return $ctx['error'];
|
||
|
||
$d = $ctx['daily'];
|
||
if (empty($d['time'])) return "No weekly forecast found for {$ctx['name']}.";
|
||
|
||
$lines = [];
|
||
$lines[] = "This week in {$ctx['name']} ({$ctx['tz']}):";
|
||
|
||
$n = min(count($d['time']), 7);
|
||
for ($i = 0; $i < $n; $i++) {
|
||
$date = $d['time'][$i];
|
||
$label = wm_pretty_weekday($date, $ctx['tz']); // e.g., Mon 2025-09-15
|
||
$lines[] = "• " . wm_format_compact_line($label, $d, $i);
|
||
}
|
||
|
||
return implode("\n", $lines);
|
||
}
|
||
|
||
// ───────────────────────────────────────────────────────────────────────────────
|
||
// 3) Internals
|
||
// ───────────────────────────────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* Resolve place -> lat/lon via Open-Meteo Geocoding and fetch a 7-day daily forecast.
|
||
* @param string $place
|
||
* @return array context with:
|
||
* name (City, Country), tz, daily => [keys matching Open-Meteo daily params]
|
||
* or ['error' => 'message']
|
||
*/
|
||
function wm_build_context(string $place): array {
|
||
$place = trim($place);
|
||
if ($place === '') return ['error' => "Please provide a location (e.g., 'Lisbon')."];
|
||
|
||
$geo = wm_http_json("https://geocoding-api.open-meteo.com/v1/search?name=" . urlencode($place) . "&count=1&language=en&format=json");
|
||
if (!$geo || empty($geo['results'][0])) {
|
||
return ['error' => "Could not find location for '$place'."];
|
||
}
|
||
|
||
$g = $geo['results'][0];
|
||
$lat = $g['latitude'];
|
||
$lon = $g['longitude'];
|
||
$displayName = $g['name'] . (isset($g['country']) ? ", " . $g['country'] : "");
|
||
|
||
// Daily parameters
|
||
$dailyParams = [
|
||
'weathercode',
|
||
'temperature_2m_max',
|
||
'temperature_2m_min',
|
||
'precipitation_sum',
|
||
'precipitation_probability_max',
|
||
'windspeed_10m_max'
|
||
];
|
||
|
||
$forecastUrl = "https://api.open-meteo.com/v1/forecast?"
|
||
. http_build_query([
|
||
'latitude' => $lat,
|
||
'longitude' => $lon,
|
||
'daily' => implode(',', $dailyParams),
|
||
'forecast_days' => 7,
|
||
'timezone' => 'auto'
|
||
]);
|
||
|
||
$fc = wm_http_json($forecastUrl);
|
||
if (!$fc || empty($fc['daily'])) {
|
||
return ['error' => "Could not fetch forecast for '$displayName'."];
|
||
}
|
||
|
||
$tz = $fc['timezone'] ?? 'local';
|
||
return [
|
||
'name' => $displayName,
|
||
'tz' => $tz,
|
||
'daily' => $fc['daily'],
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Simple GET JSON helper with cURL
|
||
* @param string $url
|
||
* @return array|null
|
||
*/
|
||
function wm_http_json(string $url): ?array {
|
||
$ch = curl_init($url);
|
||
curl_setopt_array($ch, [
|
||
CURLOPT_RETURNTRANSFER => true,
|
||
CURLOPT_FOLLOWLOCATION => true,
|
||
CURLOPT_TIMEOUT => 10,
|
||
CURLOPT_USERAGENT => "WeatherModule/1.0 (+github.com/you)"
|
||
]);
|
||
$res = curl_exec($ch);
|
||
$err = curl_error($ch);
|
||
curl_close($ch);
|
||
|
||
if ($res === false || !$res) return null;
|
||
$data = json_decode($res, true);
|
||
return is_array($data) ? $data : null;
|
||
}
|
||
|
||
/**
|
||
* Format a single-day verbose summary.
|
||
*/
|
||
function wm_format_day_summary(string $title, string $dateLabel, array $d, int $i): string {
|
||
$code = $d['weathercode'][$i] ?? null;
|
||
$desc = wm_weathercode_text($code);
|
||
$icon = wm_weathercode_emoji($code);
|
||
|
||
$tmax = wm_num($d['temperature_2m_max'][$i] ?? null);
|
||
$tmin = wm_num($d['temperature_2m_min'][$i] ?? null);
|
||
$pp = wm_num($d['precipitation_probability_max'][$i] ?? null);
|
||
$pr = wm_num($d['precipitation_sum'][$i] ?? null);
|
||
$wmax = wm_num($d['windspeed_10m_max'][$i] ?? null);
|
||
|
||
$parts = [];
|
||
$parts[] = "$title — $dateLabel";
|
||
$parts[] = "$icon $desc";
|
||
if ($tmax !== null && $tmin !== null) $parts[] = "Temp: min {$tmin}°C / max {$tmax}°C";
|
||
if ($pp !== null) $parts[] = "Precip prob: {$pp}%";
|
||
if ($pr !== null) $parts[] = "Precip total: {$pr} mm";
|
||
if ($wmax !== null) $parts[] = "Wind up to: {$wmax} m/s";
|
||
|
||
return implode("\n", $parts);
|
||
}
|
||
|
||
/**
|
||
* Format a compact weekly line for a given day.
|
||
*/
|
||
function wm_format_compact_line(string $label, array $d, int $i): string {
|
||
$code = $d['weathercode'][$i] ?? null;
|
||
$icon = wm_weathercode_emoji($code);
|
||
|
||
$tmax = wm_num($d['temperature_2m_max'][$i] ?? null);
|
||
$tmin = wm_num($d['temperature_2m_min'][$i] ?? null);
|
||
$pp = wm_num($d['precipitation_probability_max'][$i] ?? null);
|
||
|
||
$temp = ($tmin !== null && $tmax !== null) ? "{$tmin}–{$tmax}°C" : "n/a";
|
||
$prob = ($pp !== null) ? "{$pp}%" : "n/a";
|
||
|
||
return sprintf("%s %s %s (rain prob %s)", $label, $icon, $temp, $prob);
|
||
}
|
||
|
||
/**
|
||
* Weather code -> short text
|
||
* Ref: https://open-meteo.com/en/docs
|
||
*/
|
||
function wm_weathercode_text(?int $code): string {
|
||
$map = [
|
||
0 => "Clear sky",
|
||
1 => "Mainly clear",
|
||
2 => "Partly cloudy",
|
||
3 => "Overcast",
|
||
45 => "Fog",
|
||
48 => "Depositing rime fog",
|
||
51 => "Light drizzle",
|
||
53 => "Moderate drizzle",
|
||
55 => "Dense drizzle",
|
||
56 => "Freezing drizzle (light)",
|
||
57 => "Freezing drizzle (dense)",
|
||
61 => "Slight rain",
|
||
63 => "Moderate rain",
|
||
65 => "Heavy rain",
|
||
66 => "Freezing rain (light)",
|
||
67 => "Freezing rain (heavy)",
|
||
71 => "Slight snow fall",
|
||
73 => "Moderate snow fall",
|
||
75 => "Heavy snow fall",
|
||
77 => "Snow grains",
|
||
80 => "Rain showers (slight)",
|
||
81 => "Rain showers (moderate)",
|
||
82 => "Rain showers (violent)",
|
||
85 => "Snow showers (slight)",
|
||
86 => "Snow showers (heavy)",
|
||
95 => "Thunderstorm (slight/moderate)",
|
||
96 => "Thunderstorm with slight hail",
|
||
99 => "Thunderstorm with heavy hail",
|
||
];
|
||
return $map[$code] ?? "Unknown conditions";
|
||
}
|
||
|
||
/**
|
||
* Weather code -> emoji
|
||
*/
|
||
function wm_weathercode_emoji(?int $code): string {
|
||
if ($code === null) return "🌡️";
|
||
if ($code === 0) return "☀️";
|
||
if (in_array($code, [1,2])) return "⛅";
|
||
if ($code === 3) return "☁️";
|
||
if (in_array($code, [45,48])) return "🌫️";
|
||
if (in_array($code, [51,53,55,61,63,65,80,81,82])) return "🌧️";
|
||
if (in_array($code, [66,67])) return "🌨️🧊";
|
||
if (in_array($code, [71,73,75,77,85,86])) return "❄️";
|
||
if (in_array($code, [95,96,99])) return "⛈️";
|
||
return "🌡️";
|
||
}
|
||
|
||
/**
|
||
* Pretty date label (e.g., 2025-09-09 → Tue 9 Sep)
|
||
*/
|
||
function wm_pretty_date(string $isoDate, string $tz): string {
|
||
try {
|
||
$dt = new DateTime($isoDate, new DateTimeZone($tz ?: 'UTC'));
|
||
return $dt->format('D j M Y');
|
||
} catch (Throwable $e) {
|
||
return $isoDate;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Weekday + date (e.g., Tue 2025-09-09)
|
||
*/
|
||
function wm_pretty_weekday(string $isoDate, string $tz): string {
|
||
try {
|
||
$dt = new DateTime($isoDate, new DateTimeZone($tz ?: 'UTC'));
|
||
return $dt->format('D Y-m-d');
|
||
} catch (Throwable $e) {
|
||
return $isoDate;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Round number nicely or return null
|
||
*/
|
||
function wm_num($v): ?string {
|
||
if ($v === null) return null;
|
||
if ($v === '' || !is_numeric($v)) return null;
|
||
return (string)round((float)$v, 1);
|
||
}
|