This commit is contained in:
Javier Casares 2026-10-12 13:25:09 +00:00
commit e1f5a47c96
22 changed files with 3155 additions and 0 deletions

View file

@ -0,0 +1,211 @@
<?php
/**
* MITECO / MINETUR ServiciosRestCarburantes API client.
*
* @package FontdelboscCombustible
*/
declare( strict_types=1 );
defined( 'ABSPATH' ) || exit;
/**
* Fetches and normalizes province station data and the product catalog.
*/
final class Fontdelbosc_Combustible_Api {
/**
* Hardcoded province ID: Barcelona (08).
*/
public const PROVINCE_ID = '08';
/**
* Primary API base URL.
*/
private const BASE_PRIMARY = 'https://energia.serviciosmin.gob.es/ServiciosRestCarburantes/PreciosCarburantes/';
/**
* Fallback API base URL.
*/
private const BASE_FALLBACK = 'https://sedeaplicaciones.minetur.gob.es/ServiciosRESTCarburantes/PreciosCarburantes/';
/**
* Per-request timeout in seconds. Worst case (primary + fallback) stays bounded.
*/
private const TIMEOUT = 8;
/**
* Fetches all stations of the province, normalized with distance.
*
* @return array{fecha:string,stations:array<int,array<string,mixed>>,fetched_at:int,duration:float}|null Null on total failure.
*/
public static function fetch_province(): ?array {
$started = microtime( true );
$body = self::get( 'EstacionesTerrestres/FiltroProvincia/' . self::PROVINCE_ID );
if ( null === $body ) {
return null;
}
$decoded = json_decode( $body, true );
if ( ! is_array( $decoded ) ) {
return null;
}
$fecha = Fontdelbosc_Combustible_Payload::str( Fontdelbosc_Combustible_Payload::get( $decoded, 'Fecha' ) ) ?? '';
$raw_list = Fontdelbosc_Combustible_Payload::arr( Fontdelbosc_Combustible_Payload::get( $decoded, 'ListaEESSPrecio' ) );
$stations = array();
foreach ( $raw_list as $raw ) {
$station = self::normalize_station( $raw );
if ( null !== $station ) {
$stations[] = $station;
}
}
return array(
'fecha' => $fecha,
'stations' => $stations,
'fetched_at' => time(),
'duration' => microtime( true ) - $started,
);
}
/**
* Fetches the petroleum product catalog.
*
* @return array<int,array{id:int,name:string,abbr:string}>|null Null on total failure.
*/
public static function fetch_products(): ?array {
$body = self::get( 'Listados/ProductosPetroliferos/' );
if ( null === $body ) {
return null;
}
$decoded = json_decode( $body, true );
if ( ! is_array( $decoded ) ) {
return null;
}
$products = array();
foreach ( $decoded as $entry ) {
$id = Fontdelbosc_Combustible_Payload::int( Fontdelbosc_Combustible_Payload::get( $entry, 'IDProducto' ) );
if ( null === $id ) {
continue;
}
$products[] = array(
'id' => $id,
'name' => Fontdelbosc_Combustible_Payload::str( Fontdelbosc_Combustible_Payload::get( $entry, 'NombreProducto' ) ) ?? (string) $id,
'abbr' => Fontdelbosc_Combustible_Payload::str( Fontdelbosc_Combustible_Payload::get( $entry, 'NombreProductoAbreviatura' ) ) ?? '',
);
}
return $products;
}
/**
* GETs a path against the primary base URL, falling back to the legacy host.
*
* @param string $path API path relative to the base URL.
* @return string|null Response body, or null when both hosts fail.
*/
private static function get( string $path ): ?string {
foreach ( array( self::BASE_PRIMARY, self::BASE_FALLBACK ) as $base ) {
$response = wp_remote_get(
$base . $path,
array(
'timeout' => self::TIMEOUT,
'headers' => array( 'Accept' => 'application/json' ),
)
);
if ( is_wp_error( $response ) ) {
continue;
}
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
continue;
}
$body = wp_remote_retrieve_body( $response );
if ( '' !== $body ) {
return $body;
}
}
return null;
}
/**
* Normalizes one raw station entry; null when coordinates are unusable.
*
* @param mixed $raw Raw station.
* @return array<string,mixed>|null
*/
private static function normalize_station( mixed $raw ): ?array {
if ( ! is_array( $raw ) ) {
return null;
}
$lat = self::coord( Fontdelbosc_Combustible_Payload::get( $raw, 'Latitud' ) );
$lon = self::coord( Fontdelbosc_Combustible_Payload::get( $raw, 'Longitud (WGS84)' ) );
if ( null === $lat || null === $lon ) {
return null;
}
$prices = array();
foreach ( $raw as $key => $value ) {
if ( ! is_string( $key ) || ! str_starts_with( $key, 'Precio ' ) ) {
continue;
}
$text = Fontdelbosc_Combustible_Payload::str( $value );
$price = null === $text ? null : Fontdelbosc_Combustible_Payload::float( str_replace( ',', '.', $text ) );
if ( null !== $price && $price > 0.0 ) {
$prices[ substr( $key, 7 ) ] = $price;
}
}
return array(
'id' => Fontdelbosc_Combustible_Payload::int( Fontdelbosc_Combustible_Payload::get( $raw, 'IDEESS' ) ) ?? 0,
'brand' => Fontdelbosc_Combustible_Payload::str( Fontdelbosc_Combustible_Payload::get( $raw, 'Rótulo' ) ) ?? '',
'address' => Fontdelbosc_Combustible_Payload::str( Fontdelbosc_Combustible_Payload::get( $raw, 'Dirección' ) ) ?? '',
'locality' => Fontdelbosc_Combustible_Payload::str( Fontdelbosc_Combustible_Payload::get( $raw, 'Localidad' ) ) ?? '',
'municipality' => Fontdelbosc_Combustible_Payload::str( Fontdelbosc_Combustible_Payload::get( $raw, 'Municipio' ) ) ?? '',
'cp' => Fontdelbosc_Combustible_Payload::str( Fontdelbosc_Combustible_Payload::get( $raw, 'C.P.' ) ) ?? '',
'schedule' => Fontdelbosc_Combustible_Payload::str( Fontdelbosc_Combustible_Payload::get( $raw, 'Horario' ) ) ?? '',
'tipo_venta' => Fontdelbosc_Combustible_Payload::str( Fontdelbosc_Combustible_Payload::get( $raw, 'Tipo Venta' ) ) ?? '',
'lat' => $lat,
'lon' => $lon,
'distance' => Fontdelbosc_Combustible_Geo::haversine( $lat, $lon ),
'prices' => $prices,
);
}
/**
* Parses a comma-decimal coordinate string.
*
* @param mixed $value Raw coordinate value.
*/
private static function coord( mixed $value ): ?float {
$text = Fontdelbosc_Combustible_Payload::str( $value );
$float = null === $text ? null : Fontdelbosc_Combustible_Payload::float( str_replace( ',', '.', $text ) );
if ( null === $float || abs( $float ) > 180.0 ) {
return null;
}
return $float;
}
}