modify( 'first day of previous month' ); self::get_historical( (int) $previous->format( 'Y' ), (int) $previous->format( 'n' ) ); } /** * Returns normalized current conditions. * * Chain: Meteocat XEMA station (when key and station are configured) * → Open-Meteo current. * * @return array|null */ public static function get_current(): ?array { $options = Fontdelbosc_Plugin::get_options(); $data = self::cached( 'fontdelbosc_current', $options['cache_ttl_current'], static function () use ( $options ): ?array { if ( '' !== $options['meteocat_api_key'] && '' !== $options['meteocat_station_code'] ) { $current = Fontdelbosc_Api_Meteocat::get_current( $options['meteocat_api_key'], $options['meteocat_station_code'] ); if ( null !== $current ) { $hourly = Fontdelbosc_Data_Manager::get_hourly( 1 ); $firsthour = Fontdelbosc_Payload::path( $hourly, 'hours', 0 ); if ( is_array( $firsthour ) && null !== Fontdelbosc_Payload::str( Fontdelbosc_Payload::get( $firsthour, 'condition' ) ) ) { $current['condition'] = Fontdelbosc_Payload::get( $firsthour, 'condition' ); } return $current; } } return Fontdelbosc_Api_OpenMeteo::get_current(); } ); if ( ! is_array( $data ) ) { return null; } return array( 'temperature' => Fontdelbosc_Payload::get( $data, 'temperature' ), 'apparent' => Fontdelbosc_Payload::get( $data, 'apparent' ), 'humidity' => Fontdelbosc_Payload::get( $data, 'humidity' ), 'precipitation' => Fontdelbosc_Payload::get( $data, 'precipitation' ), 'wind_speed' => Fontdelbosc_Payload::get( $data, 'wind_speed' ), 'wind_direction' => Fontdelbosc_Payload::get( $data, 'wind_direction' ), 'condition' => Fontdelbosc_Payload::get( $data, 'condition' ), 'source' => Fontdelbosc_Payload::get( $data, 'source' ), 'time' => Fontdelbosc_Payload::get( $data, 'time' ), ); } /** * Returns the hourly forecast sliced to the requested number of hours. * * Chain: Meteocat municipal → AEMET hourly → Open-Meteo hourly. * * @param int $hours Number of hours (1-48). * @return array|null */ public static function get_hourly( int $hours = 24 ): ?array { $options = Fontdelbosc_Plugin::get_options(); $data = self::cached( 'fontdelbosc_forecast_hourly', $options['cache_ttl_forecast'], static function () use ( $options ): ?array { if ( '' !== $options['meteocat_api_key'] ) { $data = Fontdelbosc_Api_Meteocat::get_hourly( $options['meteocat_api_key'] ); if ( null !== $data ) { return $data; } } if ( '' !== $options['aemet_api_key'] ) { $data = Fontdelbosc_Api_Aemet::get_hourly( $options['aemet_api_key'] ); if ( null !== $data ) { return $data; } } return Fontdelbosc_Api_OpenMeteo::get_hourly(); } ); $entries = array_values( array_filter( Fontdelbosc_Payload::arr( Fontdelbosc_Payload::get( $data, 'hours' ) ), 'is_array' ) ); $source = Fontdelbosc_Payload::str( Fontdelbosc_Payload::get( $data, 'source' ) ); if ( '' === $source || array() === $entries ) { return null; } return array( 'source' => $source, 'hours' => array_slice( $entries, 0, max( 1, min( 48, $hours ) ) ), ); } /** * Returns the daily forecast sliced to the requested number of days. * * Chain: AEMET daily → Open-Meteo daily. * * @param int $days Number of days (1-7). * @return array|null */ public static function get_daily( int $days = 7 ): ?array { $options = Fontdelbosc_Plugin::get_options(); $data = self::cached( 'fontdelbosc_forecast_daily', $options['cache_ttl_forecast'], static function () use ( $options ): ?array { if ( '' !== $options['aemet_api_key'] ) { $data = Fontdelbosc_Api_Aemet::get_daily( $options['aemet_api_key'] ); if ( null !== $data ) { return $data; } } return Fontdelbosc_Api_OpenMeteo::get_daily(); } ); $entries = array_values( array_filter( Fontdelbosc_Payload::arr( Fontdelbosc_Payload::get( $data, 'days' ) ), 'is_array' ) ); $source = Fontdelbosc_Payload::str( Fontdelbosc_Payload::get( $data, 'source' ) ); if ( '' === $source || array() === $entries ) { return null; } return array( 'source' => $source, 'days' => array_slice( $entries, 0, max( 1, min( 7, $days ) ) ), ); } /** * Returns the daily history for a month (Open-Meteo ERA5 archive). * * @param int $year Year (1940 onwards). * @param int $month Month (1-12). * @return array|null */ public static function get_historical( int $year, int $month ): ?array { $year = max( 1940, min( (int) gmdate( 'Y' ), $year ) ); $month = max( 1, min( 12, $month ) ); $options = Fontdelbosc_Plugin::get_options(); $data = self::cached( 'fontdelbosc_historical_' . $year . '_' . $month, $options['cache_ttl_historical'], static function () use ( $year, $month ): ?array { return Fontdelbosc_Api_OpenMeteo::get_historical( $year, $month ); } ); $entries = array_values( array_filter( Fontdelbosc_Payload::arr( Fontdelbosc_Payload::get( $data, 'days' ) ), 'is_array' ) ); $source = Fontdelbosc_Payload::str( Fontdelbosc_Payload::get( $data, 'source' ) ); if ( '' === $source || array() === $entries ) { return null; } return array( 'source' => $source, 'year' => $year, 'month' => $month, 'days' => $entries, 'stats' => Fontdelbosc_Payload::arr( Fontdelbosc_Payload::get( $data, 'stats' ) ), ); } /** * Returns the license attribution line for a data source slug. * * @param string $source Source slug (meteocat, aemet, open-meteo). * @return string */ public static function attribution( string $source ): string { $map = array( 'meteocat' => '© Servei Meteorològic de Catalunya. Generalitat de Catalunya', 'aemet' => '© AEMET. Gobierno de España', 'open-meteo' => 'Weather data by Open-Meteo.com (CC BY 4.0)', ); return $map[ $source ] ?? ''; } /** * Cache helper implementing stale-while-error. Fresh data is served * until its TTL expires; on fetch failure the last cached payload is * served for up to one extra day. * * @param string $key Transient key. * @param int $ttl Time-to-live in seconds. * @param callable $fetch Fetch callback returning array|null. * @return mixed */ private static function cached( string $key, int $ttl, callable $fetch ): mixed { $cache = get_transient( $key ); if ( is_array( $cache ) && (int) Fontdelbosc_Payload::int( Fontdelbosc_Payload::get( $cache, 'expires' ) ?? 0 ) > time() ) { return Fontdelbosc_Payload::get( $cache, 'data' ); } $data = $fetch(); if ( null !== $data ) { set_transient( $key, array( 'expires' => time() + $ttl, 'data' => $data, ), $ttl + DAY_IN_SECONDS ); return $data; } if ( is_array( $cache ) ) { return Fontdelbosc_Payload::get( $cache, 'data' ); } return null; } }