@@ -59,13 +59,13 @@ class Client extends AbstractClient
5959 const EXPORT_URL_REALTIME = '/eco2mix-national-tr/exports/json ' ;
6060 const EXPORT_URL_CONSOLIDATED = '/eco2mix-national-cons-def/exports/json ' ;
6161
62+ const DATASET_REALTIME = 0 ;
63+ const DATASET_CONSOLIDATED = 1 ;
64+
6265 private RestApiClientInterface $ client ;
6366
6467 private string $ base_url ;
6568
66- /** @var bool Use consolidated dataset (true) or realtime dataset (false) */
67- private bool $ use_consolidated = false ;
68-
6969 public function __construct (RestApiClientInterface $ client , string $ url = '' )
7070 {
7171 $ this ->client = $ client ;
@@ -208,91 +208,96 @@ public function fetchDay(DateTimeImmutable $day, string $zone): array
208208 }
209209
210210 /**
211- * Fetch carbon intensities from Opendata Réseaux-Énergies using export dataset.
212- *
213- * See https://odre.opendatasoft.com/explore/dataset/eco2mix-national-tr/api/?disjunctive.nature
211+ * Fetch range from cached data or online database. Assume that $start is the beginning of a month (Y-m-1 00:00:00)
212+ * and $stop is the beginning of the next month (Y-m+1-1 00:00:00).
214213 *
215- * The method fetches the intensities for the date range specified in argument.
216214 * @param DateTimeImmutable $start
217215 * @param DateTimeImmutable $stop
218216 * @param string $zone
217+ * @param int $dataset
219218 * @return array
220219 */
221- public function fetchRange (DateTimeImmutable $ start , DateTimeImmutable $ stop , string $ zone ): array
220+ public function fetchRange (DateTimeImmutable $ start , DateTimeImmutable $ stop , string $ zone, int $ dataset = self :: DATASET_REALTIME ): array
222221 {
223- /** @var DBmysql $DB */
224- global $ DB ;
225-
222+ // Build realtime and consolidated paths
223+ $ base_path = GLPI_PLUGIN_DOC_DIR . '/carbon/carbon_intensity/ ' . $ this ->getSourceName () . '/ ' . $ zone ;
224+ $ consolidated_dir = $ base_path . '/consolidated ' ;
225+ $ realtime_dir = $ base_path . '/realtime ' ;
226+
227+ // Set timezone to +00:00 and extend range by 12 hours on each side
228+ $ request_start = $ start ->setTimezone (new DateTimeZone ('+0000 ' ))->sub (new DateInterval ('PT12H ' ));
229+ $ request_stop = $ stop ->setTimezone (new DateTimeZone ('+0000 ' ))->add (new DateInterval ('PT12H ' ));
226230 $ format = DateTime::ATOM ;
227- $ from = $ start ->format ($ format );
228- $ to = $ stop ->format ($ format );
229-
230- $ timezone = $ DB ->guessTimezone ();
231- $ interval = $ stop ->diff ($ start );
232- $ where = "date_heure IN [date' $ from' TO date' $ to' " ;
233- if ($ interval ->y === 0 && $ interval ->m === 0 && $ interval ->d === 0 && $ interval ->h === 1 ) {
234- $ where .= '] ' ;
235- } else {
236- $ where .= '[ ' ;
237- }
231+ $ from = $ request_start ->format ($ format );
232+ $ to = $ request_stop ->format ($ format );
233+ $ interval = $ request_stop ->diff ($ request_start );
234+ $ expected_samples_count = (int ) ($ interval ->days * 24 )
235+ + (int ) ($ interval ->h )
236+ + (int ) ($ interval ->i / 60 );
237+ $ where = "date_heure IN [date' $ from' TO date' $ to'[ AND taux_co2 is not null " ;
238238 $ params = [
239- 'select ' => 'taux_co2, date_heure ' ,
239+ 'select ' => 'date_heure,taux_co2 ' ,
240240 'where ' => $ where ,
241241 'order_by ' => 'date_heure asc ' ,
242- 'timezone ' => $ timezone ,
242+ 'timezone ' => ' +0000 '
243243 ];
244- // convert to 15 minutes interval
245- if ($ this ->use_consolidated ) {
246- $ this ->step = 60 ;
247- $ url = $ this ->base_url . self ::EXPORT_URL_CONSOLIDATED ;
248- } else {
249- $ this ->step = 15 ;
250- $ url = $ this ->base_url . self ::EXPORT_URL_REALTIME ;
244+
245+ // Prepend base URL
246+ switch ($ dataset ) {
247+ case self ::DATASET_CONSOLIDATED :
248+ $ url = self ::EXPORT_URL_CONSOLIDATED ;
249+ $ cache_file = $ this ->getCacheFilename (
250+ $ consolidated_dir ,
251+ $ start ,
252+ $ stop
253+ );
254+ break ;
255+ case self ::DATASET_REALTIME :
256+ default :
257+ $ url = self ::EXPORT_URL_REALTIME ;
258+ $ cache_file = $ this ->getCacheFilename (
259+ $ realtime_dir ,
260+ $ start ,
261+ $ stop
262+ );
263+ break ;
251264 }
252- $ expected_samples_count = (int ) ($ interval ->days * 24 )
253- + (int ) ($ interval ->h )
254- + (int ) ($ interval ->i / 60 );
255265
256- $ alt_response = [];
257- $ response = $ this ->client ->request ('GET ' , $ url , ['timeout ' => 8 , 'query ' => $ params ]);
258- if ($ response ) {
259- // Adjust step as consolidated dataset may return data for each 15 minutes !
266+ // If cached file exists, use it
267+ if (file_exists ($ cache_file )) {
268+ $ response = json_decode (file_get_contents ($ cache_file ), true );
260269 $ this ->step = $ this ->detectStep ($ response );
261- $ expected_samples_count *= ( 60 / $ this -> step ) ;
270+ return $ response ;
262271 }
272+ @mkdir (dirname ($ cache_file ), 0755 , true );
263273
264- // Tolerate DST switching issues with 15 minutes samples (4 missing samples or too many samples)
265- if (!$ response || abs (count ($ response ) - $ expected_samples_count ) > 4 ) {
266- // Retry with realtime dataset
267- if (!$ this ->use_consolidated ) {
268- $ this ->use_consolidated = true ;
269- $ alt_response = $ this ->fetchRange ($ start , $ stop , $ zone );
270-
271- if (!isset ($ alt_response ['error_code ' ]) && count ($ alt_response ) > count ($ response )) {
272- // Use the alternative response if more samples than the original response
273- $ response = $ alt_response ;
274- }
274+ $ url = $ this ->base_url . $ url ;
275+ $ response = $ this ->client ->request ('GET ' , $ url , ['timeout ' => 8 , 'query ' => $ params ]);
276+ $ this ->step = $ this ->detectStep ($ response );
277+ $ expected_samples_count *= (60 / $ this ->step );
278+ if (!$ response || ($ dataset === self ::DATASET_REALTIME && abs (count ($ response ) - $ expected_samples_count ) > 4 )) {
279+ $ alt_response = $ this ->fetchRange ($ start , $ stop , $ zone , self ::DATASET_CONSOLIDATED );
280+ if (!isset ($ alt_response ['error_code ' ]) && count ($ alt_response ) > count ($ response )) {
281+ // Use the alternative response if more samples than the original response
282+ $ response = $ alt_response ;
275283 }
284+ } else {
285+ $ json = json_encode ($ response );
286+ file_put_contents ($ cache_file , $ json );
276287 }
277-
278- if (!$ response ) {
279- trigger_error ('No response from RTE API for ' . $ zone , E_USER_WARNING );
280- return [];
281- }
282- if (count ($ response ) === 0 ) {
283- trigger_error ('Empty response from RTE API for ' . $ zone , E_USER_WARNING );
284- return [];
285- }
286- if (abs (count ($ response ) - $ expected_samples_count ) > 4 ) {
287- trigger_error ('Not enough samples from RTE API for ' . $ zone . ' (expected: ' . $ expected_samples_count . ', got: ' . count ($ response ) . ') ' , E_USER_WARNING );
288- }
289- if (isset ($ response ['error_code ' ])) {
290- trigger_error ($ this ->formatError ($ response ));
291- }
292-
293288 return $ response ;
294289 }
295290
291+ protected function getCacheFilename (string $ base_dir , DateTimeImmutable $ start , DateTimeImmutable $ end ): string
292+ {
293+ return sprintf (
294+ '%s/%s_%s.json ' ,
295+ $ base_dir ,
296+ $ start ->format ('Y-m-d ' ),
297+ $ end ->format ('Y-m-d ' )
298+ );
299+ }
300+
296301 protected function formatOutput (array $ response , int $ step ): array
297302 {
298303 /** @var DBMysql $DB */
@@ -309,11 +314,13 @@ protected function formatOutput(array $response, int $step): array
309314 // Even if we use UTC timezone.
310315 $ filtered_response = $ this ->deduplicate ($ response );
311316
312- // Convert string dates into datetime objects, restoring timezone as type Continent/City instead of offset
317+ // Convert string dates into datetime objects, shifting to local timezone
318+ // and using timezone expressed as type Continent/City instead of offset
313319 // This is needed to detect later the switching to winter time
314- $ timezone = new DateTimeZone ($ DB ->guessTimezone ());
320+ $ timezone = new DateTimeZone ('+0000 ' );
321+ $ local_timezone = new DateTimeZone ($ DB ->guessTimezone ());
315322 foreach ($ filtered_response as &$ record ) {
316- $ record ['date_heure ' ] = DateTime::createFromFormat ('Y-m-d\TH:i:s?????? ' , $ record ['date_heure ' ], $ timezone );
323+ $ record ['date_heure ' ] = DateTime::createFromFormat ('Y-m-d\TH:i:s?????? ' , $ record ['date_heure ' ], $ timezone )-> setTimezone ( $ local_timezone ) ;
317324 }
318325
319326 // Convert samples from 15 min to 1 hour
@@ -354,6 +361,12 @@ protected function deduplicate(array $records): array
354361 return $ filtered_response ;
355362 }
356363
364+ /**
365+ * Get the temporal distance between records
366+ *
367+ * @param array $records
368+ * @return integer step in minutes
369+ */
357370 protected function detectStep (array $ records ): ?int
358371 {
359372 if (count ($ records ) < 2 ) {
@@ -366,9 +379,8 @@ protected function detectStep(array $records): ?int
366379
367380 if ($ diff ->h === 1 ) {
368381 return 60 ; // 1 hour step
369- } else {
370- return $ diff ->i ; // Return the minutes step
371382 }
383+ return $ diff ->i ; // Return the minutes step
372384 }
373385
374386 /**
@@ -387,8 +399,11 @@ protected function convertToHourly(array $records, int $step): array
387399
388400 foreach ($ records as $ record ) {
389401 $ date = $ record ['date_heure ' ];
390- $ count ++;
391402 $ intensity += $ record ['taux_co2 ' ];
403+ if ($ record ['taux_co2 ' ] === null ) {
404+ continue ;
405+ }
406+ $ count ++;
392407 $ minute = (int ) $ date ->format ('i ' );
393408
394409 if ($ previous_record_date !== null ) {
0 commit comments