"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, }; }; /** * 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, }; }; /** * 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} 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 * @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, bldgSensorSamplingRateNestedArr) { try { if (!bldgSensorSamplingRateNestedArr) return; // Datastreams IDs const datastreamsIdsArr = bldgSensorSamplingRateNestedArr.map( (bldgSensorSamplingRateArr) => getDatastreamIdFromBuildingNumber(...bldgSensorSamplingRateArr) ); // 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 bldgSensorSamplingRateNestedArr = [ [buildingId, "vl", samplingRate], [buildingId, "rl", samplingRate], ]; const BUILDING_ID = buildingId; const SAMPLING_RATE = samplingRate; const observationsPlusMetadata = await getMetadataPlusObservationsFromSingleOrMultipleDatastreams( baseUrl, urlParams, bldgSensorSamplingRateNestedArr ); // Extract Vorlauf temperature, Ruecklauf temperature and metadata const [ [vorlaufTemperatureObsArr, ruecklaufTemperatureObsArr], [metadataVorlauf, metadataRuecklauf], ] = observationsPlusMetadata; // Extract the temperature values const vorlaufTemperatureValues = vorlaufTemperatureObsArr.map( (vlTempObs) => vlTempObs[1] ); const ruecklaufTemperatureValues = ruecklaufTemperatureObsArr.map( (rlTempObs) => rlTempObs[1] ); // The arrays have equal length, we need only use one of them for looping // Resulting array contains the following pairs (timestamp + dT) const vorlaufMinusRuecklaufTemperatureObs = vorlaufTemperatureObsArr.map( (vlTempObs, i) => [ vlTempObs[0], // timestamp vorlaufTemperatureValues[i] - ruecklaufTemperatureValues[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 [ vorlaufMinusRuecklaufTemperatureObs, { description, name, unitOfMeasurement, }, ]; } catch (err) { console.error(err); } }; export { formatDatastreamMetadataForChart, extractPropertiesFromFormattedDatastreamMetadata, getMetadataPlusObservationsFromSingleOrMultipleDatastreams, calculateVorlaufMinusRuecklaufTemperature, };