*/ private const WMO_MAP = array( 0 => 'clear', 1 => 'mainly_clear', 2 => 'partly_cloudy', 3 => 'overcast', 45 => 'fog', 48 => 'fog', 51 => 'drizzle_light', 53 => 'drizzle', 55 => 'drizzle_heavy', 61 => 'rain_light', 63 => 'rain', 65 => 'rain_heavy', 71 => 'snow_light', 73 => 'snow', 75 => 'snow_heavy', 80 => 'rain_showers_light', 81 => 'rain_showers', 82 => 'rain_showers_heavy', 95 => 'thunderstorm', 96 => 'thunderstorm_hail', 99 => 'thunderstorm_hail', ); /** * Returns normalized current conditions. * * @return array|null */ public static function get_current(): ?array { $url = add_query_arg( array( 'latitude' => FONTDELBOSC_ELTEMPS_LAT, 'longitude' => FONTDELBOSC_ELTEMPS_LON, 'current' => 'temperature_2m,relative_humidity_2m,apparent_temperature,precipitation,weather_code,wind_speed_10m,wind_direction_10m', 'timezone' => FONTDELBOSC_ELTEMPS_TIMEZONE, 'forecast_days' => 1, ), self::FORECAST_URL ); $data = self::request( $url ); $time = Fontdelbosc_Payload::str( Fontdelbosc_Payload::path( $data, 'current', 'time' ) ); if ( null === $time ) { return null; } return array( 'temperature' => Fontdelbosc_Payload::float( Fontdelbosc_Payload::path( $data, 'current', 'temperature_2m' ) ), 'apparent' => Fontdelbosc_Payload::float( Fontdelbosc_Payload::path( $data, 'current', 'apparent_temperature' ) ), 'humidity' => Fontdelbosc_Payload::int( Fontdelbosc_Payload::path( $data, 'current', 'relative_humidity_2m' ) ), 'precipitation' => Fontdelbosc_Payload::float( Fontdelbosc_Payload::path( $data, 'current', 'precipitation' ) ), 'wind_speed' => Fontdelbosc_Payload::float( Fontdelbosc_Payload::path( $data, 'current', 'wind_speed_10m' ) ), 'wind_direction' => Fontdelbosc_Payload::int( Fontdelbosc_Payload::path( $data, 'current', 'wind_direction_10m' ) ), 'condition' => self::condition( Fontdelbosc_Payload::path( $data, 'current', 'weather_code' ) ), 'source' => 'open-meteo', 'time' => self::timestamp( $time ), ); } /** * Returns the normalized hourly forecast, from the current hour onwards. * * @return array|null */ public static function get_hourly(): ?array { $url = add_query_arg( array( 'latitude' => FONTDELBOSC_ELTEMPS_LAT, 'longitude' => FONTDELBOSC_ELTEMPS_LON, 'hourly' => 'temperature_2m,precipitation_probability,weather_code,wind_speed_10m', 'timezone' => FONTDELBOSC_ELTEMPS_TIMEZONE, 'forecast_days' => 3, ), self::FORECAST_URL ); $data = self::request( $url ); $times = Fontdelbosc_Payload::arr( Fontdelbosc_Payload::path( $data, 'hourly', 'time' ) ); $now = self::now_key(); $hours = array(); foreach ( $times as $index => $iso_time ) { $raw = Fontdelbosc_Payload::str( $iso_time ); if ( null === $raw ) { continue; } $time = str_replace( 'T', ' ', $raw ); if ( $time < $now ) { continue; } $hours[] = array( 'time' => self::timestamp( $time ), 'hour' => (int) substr( $time, 11, 2 ), 'temperature' => Fontdelbosc_Payload::float( Fontdelbosc_Payload::path( $data, 'hourly', 'temperature_2m', $index ) ), 'pop' => Fontdelbosc_Payload::int( Fontdelbosc_Payload::path( $data, 'hourly', 'precipitation_probability', $index ) ), 'condition' => self::condition( Fontdelbosc_Payload::path( $data, 'hourly', 'weather_code', $index ) ), 'wind' => Fontdelbosc_Payload::float( Fontdelbosc_Payload::path( $data, 'hourly', 'wind_speed_10m', $index ) ), ); } if ( array() === $hours ) { return null; } return array( 'source' => 'open-meteo', 'hours' => $hours, ); } /** * Returns the normalized daily forecast. * * @return array|null */ public static function get_daily(): ?array { $url = add_query_arg( array( 'latitude' => FONTDELBOSC_ELTEMPS_LAT, 'longitude' => FONTDELBOSC_ELTEMPS_LON, 'daily' => 'weather_code,temperature_2m_max,temperature_2m_min,precipitation_sum,precipitation_probability_max,wind_speed_10m_max', 'timezone' => FONTDELBOSC_ELTEMPS_TIMEZONE, 'forecast_days' => 7, ), self::FORECAST_URL ); $data = self::request( $url ); $times = Fontdelbosc_Payload::arr( Fontdelbosc_Payload::path( $data, 'daily', 'time' ) ); $days = array(); foreach ( $times as $index => $date ) { $date = Fontdelbosc_Payload::str( $date ); if ( null === $date ) { continue; } $days[] = array( 'date' => $date, 'condition' => self::condition( Fontdelbosc_Payload::path( $data, 'daily', 'weather_code', $index ) ), 'temp_max' => Fontdelbosc_Payload::float( Fontdelbosc_Payload::path( $data, 'daily', 'temperature_2m_max', $index ) ), 'temp_min' => Fontdelbosc_Payload::float( Fontdelbosc_Payload::path( $data, 'daily', 'temperature_2m_min', $index ) ), 'precipitation' => Fontdelbosc_Payload::float( Fontdelbosc_Payload::path( $data, 'daily', 'precipitation_sum', $index ) ), 'pop' => Fontdelbosc_Payload::int( Fontdelbosc_Payload::path( $data, 'daily', 'precipitation_probability_max', $index ) ), 'wind_max' => Fontdelbosc_Payload::float( Fontdelbosc_Payload::path( $data, 'daily', 'wind_speed_10m_max', $index ) ), ); } if ( array() === $days ) { return null; } return array( 'source' => 'open-meteo', 'days' => $days, ); } /** * Returns the normalized daily history for a given month (ERA5). * * @param int $year Year (1940 or later). * @param int $month Month (1-12). * @return array|null */ public static function get_historical( int $year, int $month ): ?array { try { $start = new DateTimeImmutable( sprintf( '%04d-%02d-01', $year, $month ), new DateTimeZone( FONTDELBOSC_ELTEMPS_TIMEZONE ) ); } catch ( Exception ) { return null; } $url = add_query_arg( array( 'latitude' => FONTDELBOSC_ELTEMPS_LAT, 'longitude' => FONTDELBOSC_ELTEMPS_LON, 'start_date' => $start->format( 'Y-m-d' ), 'end_date' => $start->modify( 'last day of this month' )->format( 'Y-m-d' ), 'daily' => 'temperature_2m_max,temperature_2m_min,precipitation_sum,wind_speed_10m_max', 'timezone' => FONTDELBOSC_ELTEMPS_TIMEZONE, ), self::ARCHIVE_URL ); $data = self::request( $url ); $times = Fontdelbosc_Payload::arr( Fontdelbosc_Payload::path( $data, 'daily', 'time' ) ); $days = array(); foreach ( $times as $index => $date ) { $date = Fontdelbosc_Payload::str( $date ); $max = Fontdelbosc_Payload::float( Fontdelbosc_Payload::path( $data, 'daily', 'temperature_2m_max', $index ) ); $min = Fontdelbosc_Payload::float( Fontdelbosc_Payload::path( $data, 'daily', 'temperature_2m_min', $index ) ); if ( null === $date || ( null === $max && null === $min ) ) { continue; } $days[] = array( 'date' => $date, 'temp_max' => $max, 'temp_min' => $min, 'precipitation' => Fontdelbosc_Payload::float( Fontdelbosc_Payload::path( $data, 'daily', 'precipitation_sum', $index ) ), 'wind_max' => Fontdelbosc_Payload::float( Fontdelbosc_Payload::path( $data, 'daily', 'wind_speed_10m_max', $index ) ), ); } if ( array() === $days ) { return null; } return array( 'source' => 'open-meteo', 'year' => $year, 'month' => $month, 'days' => $days, 'stats' => self::stats( $days ), ); } /** * Computes monthly statistics for a set of normalized history days. * * @param array> $days Normalized days. * @return array */ private static function stats( array $days ): array { $maxes = array(); $mins = array(); $precip_sum = 0.0; $wind_max = null; foreach ( $days as $day ) { $max = Fontdelbosc_Payload::float( Fontdelbosc_Payload::get( $day, 'temp_max' ) ); $min = Fontdelbosc_Payload::float( Fontdelbosc_Payload::get( $day, 'temp_min' ) ); $pre = Fontdelbosc_Payload::float( Fontdelbosc_Payload::get( $day, 'precipitation' ) ); $win = Fontdelbosc_Payload::float( Fontdelbosc_Payload::get( $day, 'wind_max' ) ); if ( null !== $max ) { $maxes[] = $max; } if ( null !== $min ) { $mins[] = $min; } if ( null !== $pre ) { $precip_sum += $pre; } if ( null !== $win ) { $wind_max = max( (float) $wind_max, $win ); } } return array( 'avg_max' => array() === $maxes ? null : round( array_sum( $maxes ) / count( $maxes ), 1 ), 'avg_min' => array() === $mins ? null : round( array_sum( $mins ) / count( $mins ), 1 ), 'total_precip' => round( $precip_sum, 1 ), 'max_wind' => $wind_max, ); } /** * Maps a WMO code to a condition key. * * @param mixed $code WMO weather code. * @return string|null */ private static function condition( mixed $code ): ?string { if ( ! is_numeric( $code ) ) { return null; } return self::WMO_MAP[ (int) $code ] ?? 'cloudy'; } /** * Converts a local (Europe/Madrid) ISO string to a Unix timestamp. * * @param string $iso ISO date-time string. * @return int */ private static function timestamp( string $iso ): int { try { return ( new DateTimeImmutable( $iso, new DateTimeZone( FONTDELBOSC_ELTEMPS_TIMEZONE ) ) )->getTimestamp(); } catch ( Exception ) { return time(); } } /** * Returns the current hour key used to filter hourly series. * * @return string Format: Y-m-d H:00. */ private static function now_key(): string { return ( new DateTimeImmutable( 'now', new DateTimeZone( FONTDELBOSC_ELTEMPS_TIMEZONE ) ) )->format( 'Y-m-d H:00' ); } /** * Performs a GET request and decodes the JSON response. * * @param string $url Request URL. * @return array|null */ private static function request( string $url ): ?array { $response = wp_remote_get( $url, array( 'timeout' => 5, 'headers' => array( 'Accept' => 'application/json', ), ) ); if ( is_wp_error( $response ) ) { self::log( 'Open-Meteo request failed: ' . $response->get_error_message() ); return null; } $code = wp_remote_retrieve_response_code( $response ); if ( 200 !== $code ) { self::log( 'Open-Meteo returned HTTP ' . $code ); return null; } $data = json_decode( (string) wp_remote_retrieve_body( $response ), true ); return is_array( $data ) ? $data : null; } /** * Writes a message to the error log with the plugin prefix. * * @param string $message Message. * @return void */ private static function log( string $message ): void { error_log( '[fontdelbosc-eltemps] ' . $message ); } }