fetchData.js 17.8 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
"use strict";

/**
 * Retrieve the datastream ID that corresponds to a particular building
 * @param {Number | String} buildingNumber Integer representing the building ID
 * @param {String} phenomenon String representing the phenomenon of interest
 * @param {String} samplingRate String representing the sampling rate of the observations
 * @returns {Number} Datastream corresponding to the input building
 */
const getDatastreamIdFromBuildingNumber = function (
  buildingNumber,
  phenomenon,
  samplingRate
) {
  const buildingToDatastreamMapping = {
    101: {
      vl: { "15min": "69", "60min": "75" },
      rl: { "15min": "81", "60min": "87" },

      // These Datastreams do not yet have Observations
      // flow: { "15min": "93", "60min": "99" },
      // power: { "15min": "105", "60min": "111" },
      // energy: { "15min": "117", "60min": "123" },
      // energy_verb: { "15min": "129", "60min": "135" },
    },

    102: {
      vl: { "15min": "70", "60min": "76" },
      rl: { "15min": "82", "60min": "88" },

      // These Datastreams do not yet have Observations
      // flow: { "15min": "94", "60min": "100" },
      // power: { "15min": "106", "60min": "112" },
      // energy: { "15min": "118", "60min": "124" },
      // energy_verb: { "15min": "130", "60min": "136" },
    },

    107: {
      vl: { "15min": "71", "60min": "77" },
      rl: { "15min": "83", "60min": "89" },

      // These Datastreams do not yet have Observations
      // flow: { "15min": "95", "60min": "101" },
      // power: { "15min": "107", "60min": "113" },
      // energy: { "15min": "119", "60min": "125" },
      // energy_verb: { "15min": "131", "60min": "137" },
    },

    "112, 118": {
      vl: { "15min": "72", "60min": "78" },
      rl: { "15min": "84", "60min": "90" },

      // These Datastreams do not yet have Observations
      // flow: { "15min": "96", "60min": "102" },
      // power: { "15min": "108", "60min": "114" },
      // energy: { "15min": "120", "60min": "126" },
      // energy_verb: { "15min": "132", "60min": "138" },
    },

    125: {
      vl: { "15min": "73", "60min": "79" },
      rl: { "15min": "85", "60min": "91" },

      // These Datastreams do not yet have Observations
      // flow: { "15min": "97", "60min": "103" },
      // power: { "15min": "109", "60min": "115" },
      // energy: { "15min": "121", "60min": "127" },
      // energy_verb: { "15min": "133", "60min": "139" },
    },

    225: {
      vl: { "15min": "74", "60min": "80" },
      rl: { "15min": "86", "60min": "92" },
      flow: { "15min": "98", "60min": "104" },
      power: { "15min": "110", "60min": "116" },
      energy: { "15min": "122", "60min": "128" },
      energy_verb: { "15min": "134", "60min": "140" },
    },

    weather_station_521: {
      outside_temp: { "15min": "141", "60min": "142" },
    },
  };

  if (
    buildingToDatastreamMapping?.[buildingNumber]?.[phenomenon]?.[
      samplingRate
    ] === undefined
  )
    return;

  return Number(
    buildingToDatastreamMapping[buildingNumber][phenomenon][samplingRate]
  );
};

/**
 * Create URL to fetch the details of single Datastream
 * @param {String} baseUrl Base URL of the STA server
 * @param {Number} datastreamID Integer representing the Datastream ID
 * @returns {String} URL string for fetching a single Datastream
 */
const createDatastreamUrl = function (baseUrl, datastreamID) {
  if (!datastreamID) return;
  return `${baseUrl}/Datastreams(${datastreamID})`;
};

/**
 * Create URL to fetch Observations
 * @param {String} baseUrl Base URL of the STA server
 * @param {Number} datastreamID Integer representing the Datastream ID
 * @returns {String} URL string for fetching Observations
 */
const createObservationsUrl = function (baseUrl, datastreamID) {
  if (!datastreamID) return;
  return `${baseUrl}/Datastreams(${datastreamID})/Observations`;
};

/**
 * Perform a GET request using the Axios library
 * @param {String} urlObservations A URL that fetches Observations from an STA instance
 * @param {Object} urlParamObj The URL parameters to be sent together with the GET request
 * @returns {Promise} A promise that contains the first page of results when fulfilled
 */
const performGetRequestUsingAxios = function (urlObservations, urlParamObj) {
  return axios.get(urlObservations, {
    params: urlParamObj,
  });
};

/**
 * Retrieve the metadata for a single datastream
 * @async
 * @param {String} urlDatastream A URL that fetches a Datastream from an STA instance
 * @returns {Promise} A promise that contains a metadata object for a Datastream when fulfilled
 */
const getMetadataFromSingleDatastream = async function (urlDatastream) {
  try {
    // Extract properties of interest
    const {
      data: { description, name, unitOfMeasurement },
    } = await performGetRequestUsingAxios(urlDatastream);

    return { description, name, unitOfMeasurement };
  } catch (err) {
    console.error(err);
  }
};

/**
 * Retrieve metadata from multiple datastreams
 * @async
 * @param {Array} datastreamsUrlArr An array that contains N Datastream URL strings
 * @returns {Promise} A promise that contains an array of N Datastream metadata objects when fulfilled
 */
const getMetadataFromMultipleDatastreams = async function (datastreamsUrlArr) {
  try {
    // Array to store our final result
    const datastreamMetadataArr = [];

    // Use for/of loop - we need to maintain the order of execution of the async operations
    for (const datastreamUrl of datastreamsUrlArr) {
      // Metadata from a single Datastream
      const datastreamMetadata = await getMetadataFromSingleDatastream(
        datastreamUrl
      );
      datastreamMetadataArr.push(datastreamMetadata);
    }

    return datastreamMetadataArr;
  } catch (err) {
    console.error(err);
  }
};

/**
 * Match the unitOfMeasurement's string representation of a symbol to an actual symbol, where necessary
 * @param {String} unitOfMeasurementSymbolString String representation of the unitOfMeasurement's symbol
 * @returns {String} The unitOfMeasurement's symbol
 */
const matchUnitOfMeasurementSymbolStringToSymbol = function (
  unitOfMeasurementSymbolString
) {
  const unicodeCodePointDegreeSymbol = "\u00B0";
  const unicodeCodePointSuperscriptThree = "\u00B3";

  if (unitOfMeasurementSymbolString === "degC")
    return `${unicodeCodePointDegreeSymbol}C`;

  if (unitOfMeasurementSymbolString === "m3/h")
    return `m${unicodeCodePointSuperscriptThree}/h`;

  // If no symbol exists
  return unitOfMeasurementSymbolString;
};

/**
 * Extract the phenomenon name from a Datastream's name
 * @param {String} datastreamName A string representing the Datastream's name
 * @returns {String} The extracted phenomenon name
 */
const extractPhenomenonNameFromDatastreamName = function (datastreamName) {
  const regex = /\/ (.*) DS/;
  return datastreamName.match(regex)[1]; // use second element in array
};

/**
 * Format the response containing a Datastream's metadata from Sensorthings API
 * @param {Object} datastreamMetadata An object containing a Datastream's metadata
 * @returns {Object} An object containing the formatted metadata that is suitable for use in a line chart or heatmap
 */
const formatDatastreamMetadataForChart = function (datastreamMetadata) {
  const {
    description: datastreamDescription,
    name: datastreamName,
    unitOfMeasurement,
  } = datastreamMetadata;

  // Extract phenomenon name from Datastream name
  const phenomenonName =
    extractPhenomenonNameFromDatastreamName(datastreamName);

  // Get the unitOfMeasurement's symbol
  const unitOfMeasurementSymbol = matchUnitOfMeasurementSymbolStringToSymbol(
    unitOfMeasurement.symbol
  );

  return {
    datastreamDescription,
    datastreamName,
    phenomenonName,
    unitOfMeasurementSymbol,
  };
};

Pithon Kabiro's avatar
Pithon Kabiro committed
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
/**
 * Extract the properties that make up the formatted datastream metadata object(s)
 * @param {Array} formattedDatastreamsMetadataArr An array of formatted metadata object(s) from one or more datastreams
 * @returns {Object} An object that contains array(s) of formatted datastream metadata properties
 */
const extractPropertiesFromFormattedDatastreamMetadata = function (
  formattedDatastreamsMetadataArr
) {
  // Create arrays from the properties of the formatted datastream metadata
  const datastreamDescriptionsArr = formattedDatastreamsMetadataArr.map(
    (datastreamMetadata) => datastreamMetadata.datastreamDescription
  );

  const datastreamNamesArr = formattedDatastreamsMetadataArr.map(
    (datastreamMetadata) => datastreamMetadata.datastreamName
  );

  const phenomenonNamesArr = formattedDatastreamsMetadataArr.map(
    (datastreamMetadata) => datastreamMetadata.phenomenonName
  );

  const unitOfMeasurementSymbolsArr = formattedDatastreamsMetadataArr.map(
    (datastreamMetadata) => datastreamMetadata.unitOfMeasurementSymbol
  );

  return {
    datastreamDescriptionsArr,
    datastreamNamesArr,
    phenomenonNamesArr,
    unitOfMeasurementSymbolsArr,
  };
};

269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
/**
 * Traverses all the pages that make up the response from a SensorThingsAPI instance. The link to the next page, if present, is denoted by the presence of a "@iot.nextLink" property in the response object. This function concatenates all the values so that the complete results are returned in one array.
 * @async
 * @param {Promise} httpGetRequestPromise Promise object resulting from an Axios GET request
 * @returns {Promise} A promise that contains an object containing results from all the pages when fulfilled
 */
const combineResultsFromAllPages = async function (httpGetRequestPromise) {
  try {
    if (!httpGetRequestPromise) return;

    const lastSuccess = await httpGetRequestPromise;

    // The "success" objects contain a "data" object which in turn has a "value" property
    // If the "data" object in turn has a "@iot.nextLink" property, then a next page exists
    if (lastSuccess.data["@iot.nextLink"]) {
      const nextLinkSuccess = await combineResultsFromAllPages(
        axios.get(lastSuccess.data["@iot.nextLink"])
      );
      // The "data" object in turn has a "value" property
      // The "value" property's value is an array
      nextLinkSuccess.data.value = lastSuccess.data.value.concat(
        nextLinkSuccess.data.value
      );
      return nextLinkSuccess;
    } else {
      return lastSuccess;
    }
  } catch (err) {
    console.error(err);
  }
};

/**
 * Traverses all the pages that make up the response from a SensorThingsAPI instance and extracts the combined Observations
 * @async
 * @param {Promise} httpGetRequestPromise Promise object resulting from an Axios GET request
 * @returns {Promise} A promise that contains an array of Observations when fulfilled
 */
const extractCombinedObservationsFromAllPages = async function (
  httpGetRequestPromise
) {
  try {
    const successResponse = await combineResultsFromAllPages(
      httpGetRequestPromise
    );

    // Extract value array from the success response object
    const {
      data,
      data: { value: valueArr },
    } = successResponse;

    // Array that will hold the combined observations
    const combinedObservations = [];

    valueArr.forEach((val) => {
      // Each page of results will have a dataArray that holds the observations
      const { dataArray } = val;
      combinedObservations.push(...dataArray);
    });

    return new Promise((resolve, reject) => {
      resolve(combinedObservations);
    });
  } catch (err) {
    console.error(err);
  }
};

/**
 * Retrieve all the Observations from an array of Observations promises
 * @async
 * @param {Promise} observationPromiseArray An array that contains N observation promises
 * @returns {Promise} A promise that contains an array of Observations from multiple Datastreams when fulfilled
 */
const getObservationsFromMultipleDatastreams = async function (
  observationPromiseArray
) {
  try {
    // Array to store our final result
    const observationsAllDatastreamsArr = [];

    // Use for/of loop - we need to maintain the order of execution of the async operations
    for (const observationPromise of observationPromiseArray) {
      // Observations from a single Datastream
      const observations = await observationPromise;
      observationsAllDatastreamsArr.push(observations);
    }

    return observationsAllDatastreamsArr;
  } catch (err) {
    console.error(err);
  }
};

/**
 * Retrieve the metadata from a single Datastream or multiple Datastreams and the Observations corresponding to the Datastream(s)
 * @param {String} baseUrl Base URL of the STA server
 * @param {Object} urlParamObj The URL parameters to be sent together with the GET request
Pithon Kabiro's avatar
Pithon Kabiro committed
368
 * @param {Array} bldgSensorSamplingRateNestedArr A N*1 array (where N >= 1) containing a nested array of buildings, sensors & sampling rates as strings, i.e. [["101", "rl", "15min"]] or [["101", "rl", "15min"], ["102", "vl", "60min"]] or [["101", "rl", "15min"], ["102", "vl", "60min"], ["225", "vl", "60min"]], etc
369
370
371
 * @returns {Promise} A promise that contains a 1*2 array (the first element is an array that contans N Observations arrays; and the second element is an array of N Datastream metadata objects) when fulfilled
 */
const getMetadataPlusObservationsFromSingleOrMultipleDatastreams =
Pithon Kabiro's avatar
Pithon Kabiro committed
372
  async function (baseUrl, urlParamObj, bldgSensorSamplingRateNestedArr) {
373
    try {
Pithon Kabiro's avatar
Pithon Kabiro committed
374
      if (!bldgSensorSamplingRateNestedArr) return;
375
376

      // Datastreams IDs
Pithon Kabiro's avatar
Pithon Kabiro committed
377
378
379
      const datastreamsIdsArr = bldgSensorSamplingRateNestedArr.map(
        (bldgSensorSamplingRateArr) =>
          getDatastreamIdFromBuildingNumber(...bldgSensorSamplingRateArr)
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
      );

      // Observations URLs
      const observationsUrlArr = datastreamsIdsArr.map((datastreamId) =>
        createObservationsUrl(baseUrl, datastreamId)
      );

      // Datastreams URLs
      const datastreamsUrlArr = datastreamsIdsArr.map((datastreamId) =>
        createDatastreamUrl(baseUrl, datastreamId)
      );

      // Promise objects - Observations
      const observationsPromisesArr = observationsUrlArr.map((obsUrl) =>
        extractCombinedObservationsFromAllPages(
          performGetRequestUsingAxios(obsUrl, urlParamObj)
        )
      );

      // Observations array
      const observationsArr = await getObservationsFromMultipleDatastreams(
        observationsPromisesArr
      );

      // Metadata array
      const metadataArr = await getMetadataFromMultipleDatastreams(
        datastreamsUrlArr
      );

      return [observationsArr, metadataArr];
    } catch (err) {
      console.error(err);
    }
  };

/**
 * Calculate the temperature difference, dT, between Vorlauf temperature [VL] and Rücklauf temperature [RL] (i.e., dT = VL - RL)
 * @param {String} baseUrl Base URL of the STA server
 * @param {Object} urlParams The URL parameters to be sent together with the GET request
 * @param {String} buildingId The building ID as a string
 * @param {String} samplingRate The sampling rate as a string
 * @returns {Promise} A promise that contains an array (that is made up of a temperature difference array and a metadata object) when fulfilled
 */
const calculateVorlaufMinusRuecklaufTemperature = async function (
  baseUrl,
  urlParams,
  buildingId,
  samplingRate
) {
  try {
Pithon Kabiro's avatar
Pithon Kabiro committed
430
    const bldgSensorSamplingRateNestedArr = [
431
432
433
434
435
436
437
438
439
440
441
      [buildingId, "vl", samplingRate],
      [buildingId, "rl", samplingRate],
    ];

    const BUILDING_ID = buildingId;
    const SAMPLING_RATE = samplingRate;

    const observationsPlusMetadata =
      await getMetadataPlusObservationsFromSingleOrMultipleDatastreams(
        baseUrl,
        urlParams,
Pithon Kabiro's avatar
Pithon Kabiro committed
442
        bldgSensorSamplingRateNestedArr
443
444
445
      );

    // Extract Vorlauf temperature, Ruecklauf temperature and metadata
Pithon Kabiro's avatar
Pithon Kabiro committed
446
447
448
449
    const [
      [vorlaufTemperatureObsArr, ruecklaufTemperatureObsArr],
      [metadataVorlauf, metadataRuecklauf],
    ] = observationsPlusMetadata;
450
451

    // Extract the temperature values
Pithon Kabiro's avatar
Pithon Kabiro committed
452
453
454
455
456
457
    const vorlaufTemperatureValues = vorlaufTemperatureObsArr.map(
      (vlTempObs) => vlTempObs[1]
    );
    const ruecklaufTemperatureValues = ruecklaufTemperatureObsArr.map(
      (rlTempObs) => rlTempObs[1]
    );
458
459
460

    // The arrays have equal length, we need only use one of them for looping
    // Resulting array contains the following pairs (timestamp + dT)
Pithon Kabiro's avatar
Pithon Kabiro committed
461
462
463
464
465
466
    const vorlaufMinusRuecklaufTemperatureObs = vorlaufTemperatureObsArr.map(
      (vlTempObs, i) => [
        vlTempObs[0], // timestamp
        vorlaufTemperatureValues[i] - ruecklaufTemperatureValues[i],
      ]
    );
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502

    // From Vorlauf metadata, extract `name` and `unitOfMeasurement`
    const {
      name: datastreamNameVorlauf,
      unitOfMeasurement: unitOfMeasurementVorlauf,
    } = metadataVorlauf;

    // From Ruecklauf metadata, extract `name`
    const { name: datastreamNameRuecklauf } = metadataRuecklauf;

    // Extract the phenomenon names from the Datastream names
    const phenomenonNameVorlauf = extractPhenomenonNameFromDatastreamName(
      datastreamNameVorlauf
    );
    const phenomenonNameRuecklauf = extractPhenomenonNameFromDatastreamName(
      datastreamNameRuecklauf
    );

    // Create our custom datastream description text
    // The resulting datastream description string has two `temperature` substrings;
    // replace the first occurence with an empty string
    const descriptionTempDifference =
      `Computed dT: ${phenomenonNameVorlauf} minus ${phenomenonNameRuecklauf}`.replace(
        "temperature",
        ""
      );

    // Create our custom datastream name text
    const nameTempDifference = `BOSCH_${BUILDING_ID} / dT Temperature difference (VL-RL) DS:${SAMPLING_RATE}`;

    // The datastream object that we return needs to have these property names
    const description = descriptionTempDifference;
    const name = nameTempDifference;
    const unitOfMeasurement = unitOfMeasurementVorlauf;

    return [
Pithon Kabiro's avatar
Pithon Kabiro committed
503
      vorlaufMinusRuecklaufTemperatureObs,
504
505
506
507
508
509
510
511
512
513
514
515
516
      {
        description,
        name,
        unitOfMeasurement,
      },
    ];
  } catch (err) {
    console.error(err);
  }
};

export {
  formatDatastreamMetadataForChart,
Pithon Kabiro's avatar
Pithon Kabiro committed
517
  extractPropertiesFromFormattedDatastreamMetadata,
518
519
520
  getMetadataPlusObservationsFromSingleOrMultipleDatastreams,
  calculateVorlaufMinusRuecklaufTemperature,
};