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); }