Skip to content

Commit 247f4ff

Browse files
committed
refactor(Datasource\CarbonIntensity\Rte): drop incremental download
properly handled by the full download process, and avoids overhead for caching handling
1 parent f48c889 commit 247f4ff

File tree

5 files changed

+119
-63
lines changed

5 files changed

+119
-63
lines changed

src/CarbonIntensity.php

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -257,24 +257,7 @@ public function downloadOneZone(ClientInterface $data_source, string $zone_name,
257257
}
258258
}
259259

260-
$first_known_intensity_date = $this->getFirstKnownDate($zone_name, $data_source->getSourceName());
261-
$incremental = false;
262-
if ($first_known_intensity_date !== null) {
263-
$incremental = ($start_date >= $first_known_intensity_date);
264-
}
265-
if ($first_known_intensity_date !== null && $first_known_intensity_date <= $data_source->getHardStartDate()) {
266-
// Cannot download older data than absolute start date of the source, then switch to incremetal mode
267-
$incremental = true;
268-
}
269-
if ($incremental) {
270-
$start_date = max($data_source->getMaxIncrementalAge(), $this->getLastKnownDate($zone_name, $data_source->getSourceName()));
271-
$start_date = $start_date->add(new DateInterval('PT1H'));
272-
$count = $data_source->incrementalDownload($zone_name, $start_date, $this, $limit);
273-
$total_count += $count;
274-
return $total_count;
275-
}
276-
277-
return $total_count;
260+
return 0;
278261
}
279262

280263
/**

src/DataSource/CarbonIntensity/AbstractClient.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ public function incrementalDownload(string $zone, DateTimeImmutable $start_date,
236236
} catch (AbortException $e) {
237237
throw $e;
238238
}
239+
$data = $this->formatOutput($data, $this->step);
239240
if (!isset($data[$zone])) {
240241
continue;
241242
}

src/DataSource/CarbonIntensity/ClientInterface.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,13 @@ public function getSourceName(): string;
129129
*/
130130
public function createZones(): int;
131131

132+
/**
133+
* Disable caching of data source
134+
*
135+
* @return void
136+
*/
137+
public function disableCache();
138+
132139
/**
133140
* Get the absolute starrt date of data from the source
134141
*

src/DataSource/CarbonIntensity/Rte/Client.php

Lines changed: 109 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,16 @@
5454
* @see https://help.opendatasoft.com/apis/ods-explore-v2/explore_v2.1.html
5555
*
5656
* 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-
*
57+
* GLPI must be set up with timezones enabled
5958
* 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.
59+
* Searching for gaps will find gaps that the algorithm will try to fill, but fail repeatidly.
60+
*
61+
* Queries to the provider uses dates intervals in the form [start, stop]. Those boundaries
62+
* are expresses with timezone +00:00 (aka Z or UTC). An extra timezone parameter is given
63+
* in the query and let the server respond with datetimes shifted to this timezone.
6164
*/
6265
class Client extends AbstractClient
6366
{
64-
const RECORDS_URL = '/eco2mix-national-tr/records';
6567
const EXPORT_URL_REALTIME = '/eco2mix-national-tr/exports/json';
6668
const EXPORT_URL_CONSOLIDATED = '/eco2mix-national-cons-def/exports/json';
6769

@@ -160,21 +162,18 @@ public function createZones(): int
160162
*/
161163
public function fetchDay(DateTimeImmutable $day, string $zone): array
162164
{
163-
/** @var DBmysql $DB */
164-
global $DB;
165-
166165
$stop = $day->add(new DateInterval('P1D'));
167166

168167
$format = DateTime::ATOM;
169-
$timezone = $DB->guessTimezone();
170168
$from = $day->format($format);
171169
$to = $stop->format($format);
172170

171+
$timezone = new DateTimeZone('Europe/Paris');
173172
$params = [
174173
'select' => 'date_heure,taux_co2',
175174
'where' => "date_heure IN [date'$from' TO date'$to'[ AND taux_co2 is not null",
176175
'order_by' => 'date_heure asc',
177-
'timezone' => $timezone,
176+
'timezone' => $timezone->getName(),
178177
];
179178

180179
$url = $this->base_url . self::EXPORT_URL_REALTIME;
@@ -187,6 +186,8 @@ public function fetchDay(DateTimeImmutable $day, string $zone): array
187186
return [];
188187
}
189188

189+
$this->step = $this->detectStep($response);
190+
190191
// Drop data with no carbon intensity (may be returned by the provider)
191192
$response = array_filter($response, function ($item) {
192193
return $item['taux_co2'] != 0;
@@ -207,7 +208,7 @@ public function fetchDay(DateTimeImmutable $day, string $zone): array
207208
}
208209
}
209210

210-
return $this->formatOutput($response, 15);
211+
return $response;
211212
}
212213

213214
/**
@@ -222,17 +223,15 @@ public function fetchDay(DateTimeImmutable $day, string $zone): array
222223
*/
223224
public function fetchRange(DateTimeImmutable $start, DateTimeImmutable $stop, string $zone, int $dataset = self::DATASET_REALTIME): array
224225
{
225-
/** @var DBmysql $DB */
226-
global $DB;
227-
228226
// Build realtime and consolidated paths
229227
$base_path = GLPI_PLUGIN_DOC_DIR . '/carbon/carbon_intensity/' . $this->getSourceName() . '/' . $zone;
230228
$consolidated_dir = $base_path . '/consolidated';
231229
$realtime_dir = $base_path . '/realtime';
232230

233231
// Set timezone to +00:00 and extend range by -12/+14 hours
234-
$request_start = $start->setTimezone(new DateTimeZone('+0000'))->sub(new DateInterval('PT12H'));
235-
$request_stop = $stop->setTimezone(new DateTimeZone('+0000'))->add(new DateInterval('PT14H'));
232+
$timezone_z = new DateTimeZone('+0000');
233+
$request_start = $start->setTimezone($timezone_z)->sub(new DateInterval('PT12H'));
234+
$request_stop = $stop->setTimezone($timezone_z)->add(new DateInterval('PT14H'));
236235
$format = DateTime::ATOM;
237236
$from = $request_start->format($format);
238237
$to = $request_stop->format($format);
@@ -272,13 +271,13 @@ public function fetchRange(DateTimeImmutable $start, DateTimeImmutable $stop, st
272271
@mkdir(dirname($cache_file), 0755, true);
273272

274273
// Prepare the HTTP request
275-
$timezone = $DB->guessTimezone();
274+
$timezone = new DateTimeZone('Europe/Paris'); // Optimal timezone to avoid DST mess in the response
276275
$where = "date_heure IN [date'$from' TO date'$to'[ AND taux_co2 is not null";
277276
$params = [
278277
'select' => 'date_heure,taux_co2',
279278
'where' => $where,
280279
'order_by' => 'date_heure asc',
281-
'timezone' => $timezone
280+
'timezone' => $timezone->getName()
282281
];
283282
$response = $this->client->request('GET', $url, ['timeout' => 8, 'query' => $params]);
284283
$this->step = $this->detectStep($response);
@@ -301,46 +300,44 @@ public function fetchRange(DateTimeImmutable $start, DateTimeImmutable $stop, st
301300

302301
protected function getCacheFilename(string $base_dir, DateTimeImmutable $start, DateTimeImmutable $end): string
303302
{
303+
$timezone_name = $start->getTimezone()->getName();
304+
$timezone_name = str_replace('/', '-', $timezone_name);
304305
return sprintf(
305-
'%s/%s_%s.json',
306+
'%s/%s_%s_%s.json',
306307
$base_dir,
308+
$timezone_name,
307309
$start->format('Y-m-d'),
308310
$end->format('Y-m-d')
309311
);
310312
}
311313

314+
/**
315+
* Format the records before saving them in DB
316+
* It is assumed that the records are chronologically sorted
317+
*
318+
* @param array $response
319+
* @param integer $step
320+
* @return array
321+
*/
312322
protected function formatOutput(array $response, int $step): array
313323
{
314324
/** @var DBMysql $DB */
315325
global $DB;
316326

317-
// array sort records, just in case
318-
usort($response, function ($a, $b) {
319-
return $a['date_heure'] <=> $b['date_heure'];
320-
});
321-
322327
$this->step = $this->detectStep($response);
323-
// Deduplicate entries (solves switching from winter time to summer time)
324-
// because there are 2 samples at same date time, during 1 hour
325-
// Even if we use UTC timezone.
326-
$filtered_response = $this->deduplicate($response);
327-
328328
// Convert string dates into datetime objects,
329329
// using timezone expressed as type Continent/City instead of offset
330330
// This is needed to detect later the switching to winter time
331-
$local_timezone = new DateTimeZone($DB->guessTimezone());
332-
array_walk($filtered_response, function (&$item, $key) use ($local_timezone) {
333-
$item['date_heure'] = DateTime::createFromFormat('Y-m-d\TH:i:sP', $item['date_heure'])->setTimezone($local_timezone);
334-
});
331+
$response = $this->shiftToLocalTimezone($response);
335332

336333
// Convert samples from to 1 hour
337334
if ($this->step < 60) {
338-
$intensities = $this->convertToHourly($filtered_response, $this->step);
335+
$intensities = $this->downsample($response, $this->step);
339336
} else {
340337
$intensities = [];
341-
foreach ($filtered_response as $record) {
338+
foreach ($response as $local_datetime => $record) {
342339
$intensities[] = [
343-
'datetime' => $record['date_heure']->format('Y-m-d\TH:00:00'),
340+
'datetime' => $record['date_heure']->format('Y-m-d\TH:00:00??????'),
344341
'intensity' => (float) $record['taux_co2'],
345342
'data_quality' => AbstractTracked::DATA_QUALITY_RAW_REAL_TIME_MEASUREMENT,
346343
];
@@ -353,22 +350,56 @@ protected function formatOutput(array $response, int $step): array
353350
];
354351
}
355352

353+
/**
354+
* convert dates to the timezone of GLPI
355+
*
356+
* @param array $response
357+
* @return array array of records: ['date_heure' => string, 'taux_co2' => number, 'datetime' => DateTime]
358+
*/
359+
protected function shiftToLocalTimezone(array $response): array
360+
{
361+
/** @var DBMysql $DB */
362+
global $DB;
363+
364+
$shifted_response = [];
365+
$local_timezone = new DateTimeZone($DB->guessTimezone());
366+
array_walk($response, function ($item, $key) use (&$shifted_response, $local_timezone) {
367+
$shifted_date_object = DateTime::createFromFormat('Y-m-d\TH:i:sP', $item['date_heure'])
368+
->setTimezone($local_timezone);
369+
$shifted_date_string = $shifted_date_object->format('Y-m-d H:i:sP');
370+
if (isset($shifted_response[$shifted_date_string]) && $shifted_response['taux_co2'] !== $item['taux_co2']) {
371+
trigger_error("Duplicate record with different carbon intensity detected.");
372+
}
373+
$item['datetime'] = $shifted_date_object;
374+
$shifted_response[$shifted_date_string] = $item;
375+
});
376+
377+
return $shifted_response;
378+
}
379+
380+
/**
381+
* Deduplicates records
382+
*
383+
* @param array $records Records in the format ['date_heure' => DateTime, 'taux_co2' => number]
384+
* @return array Array of records where key is the string formatted datetime and value is the carbon intensity
385+
*/
356386
protected function deduplicate(array $records): array
357387
{
358-
$filtered_response = [];
388+
$deduplicated = [];
359389
foreach ($records as $record) {
360-
if (isset($filtered_response[$record['date_heure']])) {
361-
if ($filtered_response[$record['date_heure']]['taux_co2'] != $record['taux_co2']) {
390+
$date = $record['date_heure'];
391+
if (key_exists($date, $deduplicated)) {
392+
if ($deduplicated[$date]['taux_co2'] != $record['taux_co2']) {
362393
// Inconsistency detected. What to do with this record?
363394
continue;
364395
}
365396
continue;
366397
}
367398

368-
$filtered_response[$record['date_heure']] = $record;
399+
$deduplicated[$date] = $record;
369400
}
370401

371-
return $filtered_response;
402+
return $deduplicated;
372403
}
373404

374405
/**
@@ -402,6 +433,10 @@ protected function detectStep(array $records): ?int
402433
*/
403434
protected function convertToHourly(array $records, int $step): array
404435
{
436+
if ($step === 60) {
437+
return $records;
438+
}
439+
405440
$intensities = [];
406441
$intensity = 0.0;
407442
$count = 0;
@@ -454,6 +489,40 @@ protected function convertToHourly(array $records, int $step): array
454489
return $intensities;
455490
}
456491

492+
/**
493+
* Downsample records to a new set of records at the given frequency in minutes.
494+
* The records may have irregular interval between samples due to filtered out null elements
495+
*
496+
* @param array $records The records to downsample
497+
* @param int $step The step of output records in minutes
498+
* @return array The downsampled records
499+
*/
500+
protected function downsample(array $records, int $step): array
501+
{
502+
$downsampled = [];
503+
$intensity = 0.0;
504+
$count = 0;
505+
foreach ($records as $record) {
506+
$date = $record['datetime'];
507+
$intensity += $record['taux_co2'];
508+
$count++;
509+
$minute = (int) $date->format('i');
510+
511+
if ($minute === (60 - $step)) {
512+
// Finalizing an average of accumulated samples
513+
$downsampled[] = [
514+
'datetime' => $date->format('Y-m-d\TH:00:00'),
515+
'intensity' => (float) $intensity / $count,
516+
'data_quality' => AbstractTracked::DATA_QUALITY_RAW_REAL_TIME_MEASUREMENT_DOWNSAMPLED,
517+
];
518+
$intensity = 0.0;
519+
$count = 0;
520+
}
521+
}
522+
523+
return $downsampled;
524+
}
525+
457526
/**
458527
* Detect if the given datetime matches a switching ot winter time (DST) for France
459528
*

tests/units/DataSource/CarbonIntensity/Rte/ClientTest.php

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,7 @@ public function testFetchDay()
5757
$intensities = $source->fetchDay($date, '');
5858

5959
$this->assertIsArray($intensities);
60-
$this->assertArrayHasKey('source', $intensities);
61-
$this->assertEquals('RTE', $intensities['source']);
62-
$this->assertArrayHasKey('France', $intensities);
63-
$this->assertIsArray($intensities['France']);
64-
$this->assertEquals(24, count($intensities['France']));
60+
$this->assertEquals(96, count($intensities));
6561
}
6662

6763
public function testFetchRange()

0 commit comments

Comments
 (0)