计算的测量
本页详细介绍计算的测量值。
概述
如此处所述,计算的测量是从其他数据源生成的一种数据。
计算的测量的核心是一个计算脚本,该脚本接受输入的度量,执行逻辑和计算,并输出一组派生参数作为计算的度量。
要使用计算的测量,您必须:
- 选择输入测量
- 选择输出参数
- 以输入测量的参数来编写脚本,计算输出参数。
计算的测量的脚本在创建后,将立即初始化,本将一直有效运行,直到删除该计算的测量。每当来自所选输入测量的测量数据到达时,Senfi将执行脚本。
设计
在设计计算的测量时,首要做的是确定所需的输入/输出数据。计算的测量可以将多个测量指定为输入。这些测量中的参数和标签将可用于负责生成输出的函数。
输入测量
以您在内容管理系统 中的权限所能访问的任何测量都可用作计算测量的输入测量。
您应该只选择您所需并具有参数的测量,作为您派生计算的测量的输入测量。
输出测量
您要使用计算的测量创建的任何数据都应打包在输出测量中。输出测量与Senfi中的其他测量具有相同的组成(参数,标签,时间戳)。
设计输出测量参数和标签时,应考虑以下事项:
- 参数:您想要的参数,以及如何根据输入的测量进行计算。
- 标签:哪些标签可让您区分源自不同传感器的计算的测量值。您可以重复使用输入测量中的标签,也可以根据计算出的测量结果来指定不同的标签。
- 时间戳:是使用输入测量的时间戳,还是输出计算的测量的时间。
实施
要生成计算的测量值,您需要编写一个可以处理所选的输入测量值,执行任何必要的计算或逻辑并输出计算出的测量数据的脚本。
JavaScript是用于编写计算的测量脚本的脚本语言。
计算的测量脚本包含两个函数:
- 初始化:设置脚本
- 计算:处理来自输入测量值的数据,执行计算并输出您计算出的测量值
脚本中必须同时包含初始化和计算函数。
init():初始化函数
脚本初始化函数可用于初始化脚本逻辑和状态管理所需的任何数据结构。在创建,修改脚本或系统维护之后重新启动服务,该脚本仅执行一次。
这是初始化函数的脚本模板:
/**
* @name: init
* @description: Perform one-time initialization of your script
* param {string}
**/
async function init() {
// Perform initialization of script here
// TODO
}
如果您在计算的测量不需要跟踪多个或连续输入测量的数据,例如测量值的总和或检查一对输入度量之间的差异,则可以选择将此函数保留为空。
async function init() {
// Initialization not required
}
如果您在计算的测量需要跟踪多个或连续输入测量的数据,则应在此处创建适当的数据结构。
您也可以声明全局变量。 例如:
// Declare global variable for a summation value
let sum;
async function init() {
// Initialize sum to zero
sum = 0;
}
compute():计算函数
每当来自指定输入测量的新测量数据或一批新测量数据到达时,都会执行脚本计算函数。在此函数中,您将从这些输入测量中读取数据,执行所需的任何逻辑或计算,然后输出计算出的测量数据。
这是计算函数的脚本模板:
/**
* @name: compute
* @description: Perform computation on input measurements. This function is called when new measurements arrive
* @param {string} measurement - The input measurement name
* @param {Array.<object>} data - The measurement array data
**/
function compute(measurement, data) {
// Perform calculation on data here
// TODO
// Example output
const outputData = {
metric1: 1,
metric2: true,
metric3: 1,
};
// Output computed measurement. See API docs below
output(outputData);
}
// Output API function
/**
* @function output
* @description: User-invoked function to output a computed measurement
* @param {object} outputData - Object containing output metrics (required tags, optional tags, measurement metrics)
* @param {string} outputData.lsid - [Example] lsid tag (for lifts)
* @param {string} outputData.country - [Example] country tag
* @param {number} outputData.metric1 - [Example] metric1
* @param {boolean} outputData.metric2 - [Example] metric2
* @param {integer} outputData.metric3 - [Example] metric3
**/
请注意,因为使用的数据是测量数据的数组,因此通常您需要遍历输入的数据,才能对每个变量执行计算任务,如下所示:
function compute(measurement, data) {
// Loop through the array of incoming measurements
for (let i = 0; i < data.length; i++) {
const inputMeasurement = data[i];
// Perform calculation and output
// ...
// Output
output(...);
}
}
At the end of your computations, once you have constructed the output data for the computed measurement, you should call output(), with the output data, to output your computed measurement. This will inform the script engine to ingest your computed measurement into Senfi.
output(): Script Output
The script output function is a pre-defined function call that you should invoke whenever you want to output your computed measurement.
You must invoke the output function with a valid measurement data (e.g. all declared metrics and tags) for your computed measurement. Note the output measurement will automatically be attributed to the computed measurement name you have chosen in the CMS. It is not possible for the script to output another computed measurement.
Testing
You can test your computed measurement in the CMS when you are creating or editing the computed measurement.
Your testing data should be identical in format and values to the actual data from your input measurements. You can test with single or multiple sets of data.
The test data format is in JSON, as an Array of Objects of the form:
{ "measurement": "data": [] }
where measurement is the name of the input measurement, and data is a list of one or more measurement data from the input measurement following the Senfi data message format.
Example of sending 1 set of measurement data from the measurement temperature_v1. The script computation function will be executed once during the test:
[ { "measurement": "temperature_v1", "data": [{ "tm_source": xxxxxxxxxx, "site_id" xxxxxxxx, "tag1": "xxxxxxxx", "tag2": "xxxxxxxx", "temp": xxxxxxxx, }] } ]
Abbreviated example of sending a batch of 3 sets of measurement data from the measurement temperature_v1. The script computation function will be executed once during the test:
[ { "measurement": "temperature_v1", "data": [{ ... "temp": xxxxxxxx, },{ ... "temp": xxxxxxxx, },{ ... "temp": xxxxxxxx, }] } ]
Abbreviated example of sending 3 sets of measurement data from the measurement temperature_v1. The script computation function will be executed 3 times during the test:
[ { "measurement": "temperature_v1", "data": [{ ... "temp": xxxxxxxx, }] }, { "measurement": "temperature_v1", "data": [{ ... "temp": xxxxxxxx, }] }, { "measurement": "temperature_v1", "data": [{ ... "temp": xxxxxxxx, }] } ]
If your script performs computation with multiple input measurements, you can specify multiple measurements. In this example, the computation function will be executed twice, once for temperature_v1 and once for humidity_v1:
[ { "measurement": "temperature_v1", "data": [{ ... "temp": xxxxxxxx, }] }, { "measurement": "humidity_v1", "data": [{ ... "humidity": xxxxxxxx, }] } ]
Execution
After you create or edit your computed measurement in the CMS, your computed measurement script will be compiled and the initialization and computation functions called:
- When you finish creating the script: The initialization function will be run
- When you finish editing the script: The initialization function will be run
- When data from each of your input measurements arrives: The computation function will be run.
Errors and Debugging
Errors that occur during the initialization or execution of your script will show up in the Debugger in the Developer tab in the CMS.
Console messages logged by your script also appear in the Debugger.
Examples
Example 1: Temperature Scale Conversion
This example shows a simple usage of computed measurement.
Imagine you have a temperature sensor that sends raw temperature values in degrees Fahrenheit (℉) and you want to show the temperature in degrees Celsius (°C) instead. This is the temperature measurement from the temperature sensor that you have as input, where temperatureF is the temperature data in ℉:
{ "tm_source": xxxxxxxxxx, "site_id" xxxxxxxx, "tag1": "xxxxxxxx", "tag2": "xxxxxxxx", "temperatureF": xxxxxxxxx }
And this is the computed measurement that you want to output, where temperatureC is the temperature metric in °C. You will retain the input measurement's tags in order to correctly attribute the computed measurement to the original temperature sensor:
{ "tm_source": xxxxxxxxxx, "site_id" xxxxxxxx, "tag1": "xxxxxxxx", "tag2": "xxxxxxxx", "temperatureC": xxxxxxxxx }
To convert the temperatureF temperature metric in ℉ into temperatureC in °C, you would use the ℉ to °C formula:
temperatureC = (temperatureF − 32) * 5/9;
As you are just performing conversion when data arrives, there is no need to perform any initialization tasks or declare any global variables. So you can leave the initialization function empty:
async function init() {
// No initialization tasks required for this simple example
}
Putting everything together, the full script would be similar to:
/**
* @name: init
* @description: Perform one-time initialization of your script
* param {string}
**/
async function init() {
// No initialization tasks required for this simple example
}
/**
* @name: compute
* @description: Perform computation on input measurements. This function is called when new measurements arrive
* @param {string} measurement - The input measurement name
* @param {Array.<object>} data - The measurement array data
**/
function compute(measurement, data) {
// Loop through the array of incoming measurements,
// and convert 'temperatureF' to 'temperatureC'
for (let i = 0; i < data.length; i++) {
const inputMeasurement = data[i];
const temperatureF = inputMeasurement.temperatureF;
// Calculate the temperature in C
const temperatureC = (temperatureF − 32) * 5/9;
const outputMeasurement = {
tm_source: inputMeasurement.tm_source,
site_id: inputMeasurement.site_id,
tag1: inputMeasurement.tag1,
tag2: inputMeasurement.tag2,
temperatureC: temperatureC,
};
// Output computed measurement
output(outputMeasurement);
}
}
/**
* @function output
* @description: User-invoked function to output a computed measurement
* @param {Object} outputData - Object containing output metrics (required tags, optional tags, measurement metrics)
* @param {string} outputData.tm_source - Timestamp (Same as input measurement)
* @param {string} outputData.site_id - Site Id (Same as input measurement)
* @param {string} outputData.tag1 - Tag1 (Same as input measurement)
* @param {number} outputData.tag2 - Tag2 (Same as input measurement)
* @param {boolean} outputData.temperatureC - Calculated temperature in C
**/
Example 2: Lift Total Floors Moved
This example shows a more complex usage of computed measurement.
Imagine you have a lift controller that sends floor position values (i.e. 1 when the lift car is at the 1st Floor; 2 when it is at the 2nd floor, etc). You want to use the lift car position data to calculate the cumulative total number of floors it has moved.
For example, if the lift was previously reported to be at the 1st Floor and is now reported to be at the 5th Floor, the total number of floors moved is 5 - 1 = 4. If it is next reported to be at the 7th Floor, the new total number of floors moved is (7 - 5) + 4 = 6. Etc
This is the (abbreviated) measurement lift_controller_v1 from the lift controller that you have as input, where lsid is the lift ID tag, and position is the lift car position data:
{ "tm_source": xxxxxxxxxx, "site_id" xxxxxxxx, "lsid": "xxxxxxxx", "position": xxxxxxxxx }
And this is the computed measurement that you want to output, where floorsMoved is to cumulative total number of floors moved. You will retain the input measurement's tags in order to correctly attribute the computed measurement to the original lift:
{ "tm_source": xxxxxxxxxx, "site_id" xxxxxxxx, "lsid": "xxxxxxxx", "floorsMoved": xxxxxxxxx }
As you want to accumulate the total number of floors moved over time, and not just performing per-measurement-data calculations, you will declare a global variable totalFloorsMoved for this purpose:
let totalFloorsMoved;
Your computation function will perform calculation across consecutive lift_controller_v1 input measurements in the computation function. You will declare a global variable prevFloors to store the previous value of the floorsMoved metric, so the next time the computation function is executed on the next lift_controller_v1 input measurement, you can perform the calculation:
let prevFloors;
You can also use the API functions getStoredValue and setStoredValue to store accumulated values to persistent storage. This is so you will not lose the accumulated data if the Senfi undergoes system maintenance where computed measurements are terminated and reinitialized.
For example, to read previously calculated total floors moved:
let prevTotalFloorsMoved = await getStoredValue('floorsMoved');
Thus, the completed initialization function would be:
let totalFloorsMoved;
let prevFloor;
/**
* @name: init
* @description: Perform one-time initialization of your script
* param {string}
**/
async function init() {
// Perform initialization of script here
let _totalFloorsMovedVal = await getStoredValue('floorsMoved');
let _prevFloorVal = await getStoredValue('prevFloor');
// Return value from getStoredValue is a JSON string.
// We need to convert it back to the actual values (an integer)
// or null if no value was previously stored
let _totalFloorsMoved = JSON.parse(_totalFloorsMovedVal);
let _prevFloor = JSON.parse(_prevFloorVal);
if (_totalFloorsMoved == null) {
totalFloorsMoved = 0; // This is the first time we run the script. Set to 0
} else {
totalFloorsMoved= _totalFloorsMoved ; // Set to the previously stored value
}
if (_prevFloor == null) {
prevFloor = 0; // This is the first time we run the script. Set to 0
} else {
prevFloor = _prevFloor ; // Set to the previously stored value
}
}
Now we need to write the computation logic:
- Upon a new batch input measurements, for each individual measurement, calculate the absolute number of floors moved since the last measurement: floors moved = abs(current position - previous position)
- Add the absolute number of floors moved to the cumulative total number of floors moved: total floors moved += floors moved
- Construct and output the computed measurement
- Store current position as previous position, for the next iteration
The computation function would be:
/**
* @name: compute
* @description: Perform computation on watching measurements. This function is called when new measurements arrive
* @param {string} measurement - The source measurement name
* @param {Array.<object>} data - The measurement array data
**/
function compute(measurement, data) {
// Perform calculation on data here
for (let i = 0; i < data.length; i++) {
const d = data[i];
// On the very first measurement where _prevFloor is 0, set _prevFloor to current position
if (_prevFloor == 0) {
_prevFloor = d.positon;
}
// Calculate the difference in floors moved
const floorsMoved = Math.abs(d.position - _prevFloor) + _totalFloorsMoved;
// Constuct the output measurement
const outputMeasurement = {
site_id: d.site_id,
country: d.country,
lsid: d.lsid,
floorsMoved: floorsMoved
};
// Output computed measurement
output(outputMeasurement );
// Store current values
_prevFloor = d.position;
_totalFloorsMoved = floorsMoved;
}
// Persist global values, to survive script restart
await setStoredValue('floorsMoved', _totalFloorsMoved);
await setStoredValue('prevFloor', _prevFloor);
}
Computed Measurement Script API
Output API function
- output
/**
* @function output
* @description: User-invoked function to output a computed measurement
* @param {object} outputData - Object containing output metrics (required tags, optional tags, measurement metrics)
* @param {string} outputData.lsid - [Example] lsid tag (for lifts)
* @param {string} outputData.country - [Example] country tag
* @param {number} outputData.metric1 - [Example] metric1
* @param {boolean} outputData.metric2 - [Example] metric2
* @param {integer} outputData.metric3 - [Example] metric3
**/
Persistent Storage API functions
- setStoredValue
/**
* @async - Use await to wait for the function to complete
* @function setStoredValue
* @description Persist a value to senfi database. You may retrieve this value using 'getStoreValue'
* @param {string} key
* @param {any} value
**/
- getStoredValue
/**
* @async - Use await to wait for the function to complete
* @function getStoredValue
* @description Retrieve a value previously stored in senfi database.
* @param {string} key
* @return {Promise.<string>} stringified value. Use JSON.parse() to restore the value to original form
**/
Timing API functions
- setInterval
/**
* @function setInterval
* @param {function} fn
* @param {integer} interval value, in miliseconds
* @returns {string} token
**/
- clearInterval
/**
* @function clearInterval
* @param {string} token - The return value from setInterval()
**/
- setTimeout
/**
* @function setTimeout
* @param {function} fn
* @param {integer} timeout value, in miliseconds
* @returns {string} token
**/
- clearTimeout
/**
* @function clearTimeout
* @param {string} token - The return value from setTimeout()
**/
Logging API functions
The following functions allow you to log messages which will show up in the Debugger in the CMS Developer Tab.
- console.log
- console.error
- console.warn
- console.info
- console.debug