5252 *
5353 * API documentation
5454 * @see https://help.opendatasoft.com/apis/ods-explore-v2/explore_v2.1.html
55+ *
56+ * About DST for this data source
57+ * GLPI must be set up with timezones enabled, set to the same timezone as the host system
58+ *
59+ * If this requirement is not met, then dates here DST change occurs will cause problems
60+ * Searching for gaps will find gaps that the algorithm will try to fill, but fail.
5561 */
5662class Client extends AbstractClient
5763{
@@ -159,23 +165,21 @@ public function fetchDay(DateTimeImmutable $day, string $zone): array
159165
160166 $ start = DateTime::createFromImmutable ($ day );
161167 $ stop = clone $ start ;
162- $ stop ->setTime ( 23 , 59 , 59 );
168+ $ stop ->add ( new DateInterval ( ' P1D ' ) );
163169
164170 $ format = DateTime::ATOM ;
165171 $ timezone = $ DB ->guessTimezone ();
166172 $ from = $ start ->format ($ format );
167173 $ to = $ stop ->format ($ format );
168174
169175 $ params = [
170- 'select ' => 'taux_co2, date_heure ' ,
171- 'where ' => "date_heure IN [date' $ from' TO date' $ to'] " ,
176+ 'select ' => 'date_heure,taux_co ' ,
177+ 'where ' => "date_heure IN [date' $ from' TO date' $ to'[ AND taux_co2 is not null " ,
172178 'order_by ' => 'date_heure asc ' ,
173- 'limit ' => 4 * 24 , // 4 samples per hour = 4 * 24 hours
174- 'offset ' => 0 ,
175179 'timezone ' => $ timezone ,
176180 ];
177181
178- $ response = $ this ->client ->request ('GET ' , $ this ->base_url . self ::RECORDS_URL , ['timeout ' => 8 , 'query ' => $ params ]);
182+ $ response = $ this ->client ->request ('GET ' , $ this ->base_url . self ::EXPORT_URL_REALTIME , ['timeout ' => 8 , 'query ' => $ params ]);
179183 if (!$ response ) {
180184 return [];
181185 }
@@ -185,26 +189,26 @@ public function fetchDay(DateTimeImmutable $day, string $zone): array
185189 }
186190
187191 // Drop data with no carbon intensity (may be returned by the provider)
188- $ response[ ' results ' ] = array_filter ($ response[ ' results ' ] , function ($ item ) {
192+ $ response = array_filter ($ response , function ($ item ) {
189193 return $ item ['taux_co2 ' ] != 0 ;
190194 });
191195
192196 // Drop last rows until we reach
193197 $ safety_count = 0 ;
194- while (($ last_item = end ($ response[ ' results ' ] )) !== false ) {
198+ while (($ last_item = end ($ response )) !== false ) {
195199 $ time = DateTime::createFromFormat (DateTimeInterface::ATOM , $ last_item ['date_heure ' ]);
196200 if ($ time ->format ('i ' ) === '45 ' ) {
197201 // We expect 15 minutes steps
198202 break ;
199203 }
200- array_pop ($ response[ ' results ' ] );
204+ array_pop ($ response );
201205 $ safety_count ++;
202206 if ($ safety_count > 3 ) {
203207 break ;
204208 }
205209 }
206210
207- return $ this ->formatOutput ($ response[ ' results ' ] , 15 );
211+ return $ this ->formatOutput ($ response , 15 );
208212 }
209213
210214 /**
@@ -227,26 +231,18 @@ public function fetchRange(DateTimeImmutable $start, DateTimeImmutable $stop, st
227231 $ consolidated_dir = $ base_path . '/consolidated ' ;
228232 $ realtime_dir = $ base_path . '/realtime ' ;
229233
230- // Set timezone to +00:00 and extend range by 12 hours on each side
234+ // Set timezone to +00:00 and extend range by -12/+14 hours
231235 $ request_start = $ start ->setTimezone (new DateTimeZone ('+0000 ' ))->sub (new DateInterval ('PT12H ' ));
232- $ request_stop = $ stop ->setTimezone (new DateTimeZone ('+0000 ' ))->add (new DateInterval ('PT12H ' ));
236+ $ request_stop = $ stop ->setTimezone (new DateTimeZone ('+0000 ' ))->add (new DateInterval ('PT14H ' ));
233237 $ format = DateTime::ATOM ;
234238 $ from = $ request_start ->format ($ format );
235239 $ to = $ request_stop ->format ($ format );
236240 $ interval = $ request_stop ->diff ($ request_start );
237241 $ expected_samples_count = (int ) ($ interval ->days * 24 )
238242 + (int ) ($ interval ->h )
239243 + (int ) ($ interval ->i / 60 );
240- $ timezone = $ DB ->guessTimezone ();
241- $ where = "date_heure IN [date' $ from' TO date' $ to'[ AND taux_co2 is not null " ;
242- $ params = [
243- 'select ' => 'date_heure,taux_co2 ' ,
244- 'where ' => $ where ,
245- 'order_by ' => 'date_heure asc ' ,
246- 'timezone ' => $ timezone
247- ];
248244
249- // Prepend base URL
245+ // Choose URL
250246 switch ($ dataset ) {
251247 case self ::DATASET_CONSOLIDATED :
252248 $ url = self ::EXPORT_URL_CONSOLIDATED ;
@@ -266,20 +262,29 @@ public function fetchRange(DateTimeImmutable $start, DateTimeImmutable $stop, st
266262 );
267263 break ;
268264 }
265+ $ url = $ this ->base_url . $ url ;
269266
270- // If cached file exists, use it
267+ // If a cached file exists, use it
271268 if (file_exists ($ cache_file )) {
272269 $ response = json_decode (file_get_contents ($ cache_file ), true );
273270 $ this ->step = $ this ->detectStep ($ response );
274271 return $ response ;
275272 }
276273 @mkdir (dirname ($ cache_file ), 0755 , true );
277274
278- $ url = $ this ->base_url . $ url ;
275+ // Prepare the HTTP request
276+ $ timezone = $ DB ->guessTimezone ();
277+ $ where = "date_heure IN [date' $ from' TO date' $ to'[ AND taux_co2 is not null " ;
278+ $ params = [
279+ 'select ' => 'date_heure,taux_co2 ' ,
280+ 'where ' => $ where ,
281+ 'order_by ' => 'date_heure asc ' ,
282+ 'timezone ' => $ timezone
283+ ];
279284 $ response = $ this ->client ->request ('GET ' , $ url , ['timeout ' => 8 , 'query ' => $ params ]);
280285 $ this ->step = $ this ->detectStep ($ response );
281286 $ expected_samples_count *= (60 / $ this ->step );
282- if (! $ response || ($ dataset === self ::DATASET_REALTIME && abs (count ($ response ) - $ expected_samples_count ) > 4 )) {
287+ if (($ dataset === self ::DATASET_REALTIME && abs (count ($ response ) - $ expected_samples_count ) > 4 )) {
283288 $ alt_response = $ this ->fetchRange ($ start , $ stop , $ zone , self ::DATASET_CONSOLIDATED );
284289 if (!isset ($ alt_response ['error_code ' ]) && count ($ alt_response ) > count ($ response )) {
285290 // Use the alternative response if more samples than the original response
@@ -318,16 +323,15 @@ protected function formatOutput(array $response, int $step): array
318323 // Even if we use UTC timezone.
319324 $ filtered_response = $ this ->deduplicate ($ response );
320325
321- // Convert string dates into datetime objects, shifting to local timezone
322- // and using timezone expressed as type Continent/City instead of offset
326+ // Convert string dates into datetime objects,
327+ // using timezone expressed as type Continent/City instead of offset
323328 // This is needed to detect later the switching to winter time
324- $ timezone = new DateTimeZone ('+0000 ' );
325329 $ local_timezone = new DateTimeZone ($ DB ->guessTimezone ());
326- foreach ($ filtered_response as & $ record ) {
327- $ record ['date_heure ' ] = DateTime::createFromFormat ('Y-m-d\TH:i:s?????? ' , $ record ['date_heure ' ], $ timezone )->setTimezone ($ local_timezone );
328- }
330+ array_walk ($ filtered_response, function (& $ item , $ key ) use ( $ local_timezone ) {
331+ $ item ['date_heure ' ] = DateTime::createFromFormat ('Y-m-d\TH:i:sP ' , $ item ['date_heure ' ])->setTimezone ($ local_timezone );
332+ });
329333
330- // Convert samples from 15 min to 1 hour
334+ // Convert samples from to 1 hour
331335 if ($ this ->step < 60 ) {
332336 $ intensities = $ this ->convertToHourly ($ filtered_response , $ this ->step );
333337 } else {
@@ -414,15 +418,16 @@ protected function convertToHourly(array $records, int $step): array
414418 // Ensure that current date is $step minutes ahead than previous record date
415419 $ diff = $ date ->getTimestamp () - $ previous_record_date ->getTimestamp ();
416420 if ($ diff !== $ step * 60 ) {
417- if ($ diff == 4500 && $ this ->switchToWinterTime ($ previous_record_date , $ date )) {
418- // 4500 = 1h + 15m
421+ if ($ this ->switchToWinterTime (clone $ previous_record_date , clone $ date )) {
419422 $ filled_date = DateTime::createFromFormat ('Y-m-d\TH:i:s ' , end ($ intensities )['datetime ' ]);
420423 $ filled_date ->add (new DateInterval ('PT1H ' ));
421424 $ intensities [] = [
422425 'datetime ' => $ filled_date ->format ('Y-m-d\TH:00:00 ' ),
423426 'intensity ' => (end ($ intensities )['intensity ' ] + $ record ['taux_co2 ' ]) / 2 ,
424427 'data_quality ' => AbstractTracked::DATA_QUALITY_RAW_REAL_TIME_MEASUREMENT_DOWNSAMPLED ,
425428 ];
429+ // } else if ($this->switchToSummerTime(clone $previous_record_date, clone $date)) {
430+ // $a = 1;
426431 } else {
427432 // Unexpected gap in the records. What to do with this ?
428433 $ date_1 = $ previous_record_date ->format (DateTimeInterface::ATOM );
@@ -433,6 +438,7 @@ protected function convertToHourly(array $records, int $step): array
433438 }
434439
435440 if ($ minute === (60 - $ step )) {
441+ // Finalizing an average of accumulated samples
436442 $ intensities [] = [
437443 'datetime ' => $ date ->format ('Y-m-d\TH:00:00 ' ),
438444 'intensity ' => (float ) $ intensity / $ count ,
@@ -449,12 +455,15 @@ protected function convertToHourly(array $records, int $step): array
449455 }
450456
451457 /**
452- * Detect if the given datetime matches a switching ot winter time (DST)
458+ * Detect if the given datetime matches a switching ot winter time (DST) for France
453459 *
454460 * @return bool
455461 */
456462 private function switchToWinterTime (DateTime $ previous , DateTime $ date ): bool
457463 {
464+ $ timezone_paris = new DateTimeZone ('Europe/Paris ' );
465+ $ previous ->setTimezone ($ timezone_paris );
466+ $ date ->setTimezone ($ timezone_paris );
458467 $ first_dst = $ previous ->format ('I ' );
459468 $ second_dst = $ date ->format ('I ' );
460469 return $ first_dst === '1 ' && $ second_dst === '0 ' ;
0 commit comments