Skip to content

Commit a4ead03

Browse files
committed
feat: require GLPI timezone enabled
1 parent febcff9 commit a4ead03

File tree

4 files changed

+441
-432
lines changed

4 files changed

+441
-432
lines changed

setup.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,11 @@ function plugin_carbon_check_prerequisites()
213213
$prerequisitesSuccess = false;
214214
}
215215

216+
if ($DB->use_timezones !== true) {
217+
echo "Enable timezones support<br>";
218+
$prerequisitesSuccess = false;
219+
}
220+
216221
if (getenv('CI') === false) {
217222
// only when not under test
218223
$version_string = $DB->getVersion();

src/DataSource/CarbonIntensity/Rte/Client.php

Lines changed: 43 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@
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
*/
5662
class 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

Comments
 (0)