fetchData.js 16.3 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
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
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
368
369
370
371
372
373
374
375
376
377
378
379
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
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
"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,
  };
};

/**
 * 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
 * @param {Array} bldgSensorSamplingRateArr 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
 * @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 =
  async function (baseUrl, urlParamObj, bldgSensorSamplingRateArr) {
    try {
      if (!bldgSensorSamplingRateArr) return;

      // Datastreams IDs
      const datastreamsIdsArr = bldgSensorSamplingRateArr.map(
        (bldgSensorSamplingRate) =>
          getDatastreamIdFromBuildingNumber(...bldgSensorSamplingRate)
      );

      // 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 {
    const bldgSensorSamplingRateArr = [
      [buildingId, "vl", samplingRate],
      [buildingId, "rl", samplingRate],
    ];

    const BUILDING_ID = buildingId;
    const SAMPLING_RATE = samplingRate;

    const observationsPlusMetadata =
      await getMetadataPlusObservationsFromSingleOrMultipleDatastreams(
        baseUrl,
        urlParams,
        bldgSensorSamplingRateArr
      );

    // Extract Vorlauf temperature, Ruecklauf temperature and metadata
    const [[vorlaufTemp, ruecklaufTemp], [metadataVorlauf, metadataRuecklauf]] =
      observationsPlusMetadata;

    // Extract the temperature values
    const vorlaufTempValues = vorlaufTemp.map((obs) => obs[1]);
    const ruecklaufTempValues = ruecklaufTemp.map((obs) => obs[1]);

    // The arrays have equal length, we need only use one of them for looping
    // Resulting array contains the following pairs (timestamp + dT)
    const vorlaufMinusRuecklaufTemp = vorlaufTemp.map((obs, i) => [
      obs[0],
      vorlaufTempValues[i] - ruecklaufTempValues[i],
    ]);

    // 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 [
      vorlaufMinusRuecklaufTemp,
      {
        description,
        name,
        unitOfMeasurement,
      },
    ];
  } catch (err) {
    console.error(err);
  }
};

export {
  formatDatastreamMetadataForChart,
  getMetadataPlusObservationsFromSingleOrMultipleDatastreams,
  calculateVorlaufMinusRuecklaufTemperature,
};