v1.0.0
This commit is contained in:
commit
06153e387d
47 changed files with 5116 additions and 0 deletions
379
includes/class-fontdelbosc-api-openmeteo.php
Normal file
379
includes/class-fontdelbosc-api-openmeteo.php
Normal file
|
|
@ -0,0 +1,379 @@
|
|||
<?php
|
||||
/**
|
||||
* Open-Meteo API handler. No API key required; used as universal fallback
|
||||
* and as the only source for historical (ERA5) data.
|
||||
*
|
||||
* @package FontdelboscEltemps
|
||||
*/
|
||||
|
||||
defined( 'ABSPATH' ) || exit;
|
||||
|
||||
/**
|
||||
* Handles Open-Meteo forecast and ERA5 archive requests.
|
||||
*/
|
||||
final class Fontdelbosc_Api_OpenMeteo {
|
||||
|
||||
/**
|
||||
* Forecast endpoint.
|
||||
*/
|
||||
private const FORECAST_URL = 'https://api.open-meteo.com/v1/forecast';
|
||||
|
||||
/**
|
||||
* ERA5 archive endpoint.
|
||||
*/
|
||||
private const ARCHIVE_URL = 'https://archive-api.open-meteo.com/v1/archive';
|
||||
|
||||
/**
|
||||
* WMO weather interpretation code to condition key.
|
||||
*
|
||||
* @var array<int,string>
|
||||
*/
|
||||
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<string,mixed>|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<string,mixed>|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<string,mixed>|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<string,mixed>|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<int,array<string,mixed>> $days Normalized days.
|
||||
* @return array<string,float|null>
|
||||
*/
|
||||
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<mixed>|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 );
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue