Edge Action Runner Reference

This topics provides details about the Action Runner scripting language that you can use to run scripts at the edge.

JavaScript Environment

The Action Runner runs the appropriate Actions when their configured triggers are received from the Data Hub.

ECMAScript 5

The environment that executes your code is in strict mode, a restricted subset of JavaScript 5.1. For more information, read the documentation.

With this version you should avoid declaring global variables (use var x = 1; instead of just simply x = 1;). For more information about strict mode, read the Mozilla Developer Network article.

We do not support most features of ECMAScript 6 and we also disallow the use of eval. If you are not familiar with JavaScript, get started in a few minutes with the following resources:

Format

Structure

The Action is a standard ECMAScript 5.1 function, with arity-1:

function ( payload ) {
}

Valid input payload consists of any valid JSON element:

  • JSON Boolean
  • JSON Number
  • JSON String
  • JSON Object
  • JSON Array

Output

Each Action should return a value. If no value is returned, no further work needs to be done. If a value is returned, Action Runner validates it to match the following JSON format:

result = {
     key1 : [ value1, value2, … , valueN],
     key2 : [ … ],
     …
     keyN : [ … ]
}

Each value must be in an array of key/value pairs.

The “keys” specify the target to deliver the payloads. Each key is a URI, comprised of a scheme (), a separator (://), and a path () - i.e. ://. The available key types are:

NameFormat (<destination>://<path>)
Datahub Resourcedh://<resource_value_path>
Virtual Resourcevr://<virtual_resource_name>
Cloud (Immediate)cl://
Cloud (Store and Forward)st://

The “values” are the data to output. Each “value” can be any JSON type (boolean, number, string, object, array).

📘

Note

Events sent from an Edge Action cannot be sent to a nammed Sream on the Cloud, they will be stored in the stream :/default of the device.

Each <destination>://<path> entry must be unique.

Errors and Failures

  • If an Action fails validation at compilation (load) time, it cannot be loaded. If it is the sole Action to specify that topic, Octave remove the callback handler for that topic.
  • If an Action fails while executing, an error should be logged, and a value should be written to the Data Hub.
  • If a single output “key” or “value” fails validation, Octave proceeds with the other keys and values and continue to process those which are valid.

Octave Functions

The following JavaScript functions are provided within the environment:

  • Datahub.read(): Performs a read operation on the Data Hub (e.g. to read the value of a resource).
  • Datahub.query(): Performs a query operation which reads the min, max, mean, or standard deviation of a value on the Data Hub.

Datahub.read()

The Datahub.read() function accepts a path and a timeout parameter, and proxy to the following Data Hub API function:

data = Datahub.read(path, 0)

path:

The path is checked to be a valid resource in the Data Hub and may one of two types:

  • Regular Resources:
  • Actual resources; must begin with a /.
  • These paths can be either absolute or relative. Absolute paths must begin with /app and are used without modification.
  • Relative paths are assumed to be with respect to /app and have /app prepended to them internally before use (e.g. The relative path argument: /myapp/mysensor/period is converted to the full path: /app/myapp/mysensor/period).
  • Virtual Resources: Virtual Resources are user-defined resources created from within an Action or from the Cloud via /virtual/config Resource. These are read using Datahub.read() and are written using the vr:// output tag in the return object. Each Virtual resource behaves in a similar way to that of a global variable, and is accessible from all actions which belong to a particular device. These resources are not visible between devices.
  • Variable names which do not contain any slashes / (i.e. no sub-directories).
  • When read, the terminating node /value is appended to the path.

timeout:

The timeout parameter has been deprecated and is always 0. Immediately upon being called, the read function will attempt to read from the resource.
The value read will be returned or NULL will be returned if the read failed.

data:

The resulting data will be in JSON format, with two field names:

{ value : <data value>, timestamp : <seconds> }

For regular resources, <data name> is the terminating node of the path provided. For example:

var result = Datahub.read('/io/ADC0/period', 0)

Will return:

{
    "period" : 300,
    timestamp : 1292947293
}

For user-defined resources, the terminating node of the path is always "value". For example:

var result = Datahub.read("/redSensor/light/value",0);

Will return:

{
    "value" : 254,
    timestamp : 1234567890
}

Datahub.query()

The Datahub.query() function accepts a path, query-type, and window size (in seconds) parameters:

Datahub.query(path, query - type, window - size)

path:

The path is validated to be a valid path in the Data Hub and may be either absolute or relative. Absolute paths must begin with /obs. Relative paths are assumed to be with respect to /obs and must begin with a /.

queryType:

The query type is a (case-insensitive) string matching one of: min, max, mean, or stddev.

timeWindowS:
The time window, in seconds, is the time from which to begin analyzing the data.

data:

The resulting data will be a scalar. For example:

var queryResult = Datahub.query('my_observation', 'mean', 10000)

Will return a scalar:
32

Octave.js Reference

JavaScript - Variables and Operators

var limit = 100 // number
var user = 'John' // string
var user = { first: 'John', last: 'Doe' } // object
var state = false // boolean
var measures = [2, 5, 10] // array
var a
typeof a // undefined
var a = null // value null

Operators

a = b + c - d // addition, substraction
a = b * (c / d) // multiplication, division
x = 100 % 48 // modulo. 100 / 48 remainder = 4
a++
b-- // postfix increment and decrement

Bitwise operators

Arithmetic

a * (b + c)         // grouping
person.age          // member
person[age]         // member
!(a == b)           // logical not
a != b              // not equal
typeof a            // type (number, object, function...)
x << 2  x >> 3      // minary shifting
a = b               // assignment
a == b              // equals
a != b              // unequal
a === b             // strict equal
a !== b             // strict unequal
a < b   a > b       // less and greater than
a <= b  a >= b      // less or equal, greater or eq
a += b              // a = a + b (works with - * %...)
a && b              // logical and
a || b              // logical or

Objects

var sensor = {
  // object name
  serialNumber: 'XXX-0001', // list of properties and values
  displayName: 'Device of John',
  temperature: 128,
  light: 170,
  report: function() {
    // object function
    return this.serialNumber + ' t:' + this.temperature + ' l:' + this.light
  },
}

sensor.light = 157 // setting value
sensor[light]++ // incrementing
measure = sensor.report() // call object function

Strings

var lower = 'abcdefghijklmnopqrstuvwxyz'
var escape = "I don't know" //\n new line
var len = lower.length // string length
lower.indexOf('lmno') // find substring, -1 if does not contain
lower.lastIndexOf('lmno') // last occurance
lower.slice(3, 6) // cuts out "def", negative values count from behind
lower.replace('lower', '123') // find and replace, takes regular expressions
lower.toUpperCase() // convert to upper case
lower.toLowerCase() // convert to lower case
lower.concat(' ', str2) // lower + " " + str2
lower.charAt(2) // character at index: "c"
lower[2] // unsafe, lower[2] = "C" doesn't work
lower.charCodeAt(2) // character code at index: "c" -> 99
lower.split(',') // splitting a string on commas gives an array
lower.split('') // splitting on characters
;(551).toString(16) // number to hex(16), octal (8) or binary (2)

Numbers

var pi = 3.141
pi.toFixed(0) // returns 3
pi.toFixed(2) // returns 3.14 - for working with money
pi.toPrecision(2) // returns 3.1
pi.valueOf() // returns number
Number(true) // converts to number
Number(new Date()) // number of milliseconds since 1970
parseInt('3 liters') // returns the first number: 3
parseFloat('3.5 liters') // returns 3.5

JavaScript - If/Else

if ((light >= 10) && (light < 200)) {  // logical condition
 status = "doorOpened.";  // executed if condition is true
} else { // else block is optional
 status = "doorClosed"; // executed if condition is false
}
Switch Statement

switch (new Date().getDay()) { // input is current day
 case 6:  // if (day == 6)
   text = "Saturday";
   break;
 case 0:  // if (day == 0)
   text = "Sunday";
   break;
 default: // else...
   text = "Week day";
}

JavaScript - Loops

For Loop

var sum = 0
for (var i = 0; i < measures.length; i++) {
  sum += measures[i]
} // parsing an array

While Loop

var light = []
var i = 1 // initialize
while (i < 100) {
  // enters the cycle if statement is true
  i++
  light[i] = measures[i] // increment to avoid infinite loop
}

Do While Loop

var i = 1 // initialize
do {
  // enters cycle at least once
  i++ // increment to avoid infinite loop
  light[i] = measures[i] // output
} while (i < 100) // repeats cycle if statement is true at the end

Break

for (var i = 0; i < 100; i++) {
  if (measures[i] > 100) {
    break
  } // stops and exits the cycle
}
light = measures[i]

Continue

for (var i = 0; i < 100; i++) {
  if (measures[i] > 100) {
    continue
  } // skips the rest of the cycle
  light[i] = measures[i]
}

JavaScript - JSON

var str =
  '{"names":[' + // crate JSON object
  '{"first":"Hakuna","lastN":"Matata" },' +
  '{"first":"Jane","lastN":"Doe" },' +
  '{"first":"Air","last":"Jordan" }]}'
var obj = JSON.parse(str) // parse
console.log(obj.names[1].first) // access

Send

var myObj = { name: 'John', age: 18, city: 'New York' } // create object
var myJSON = JSON.stringify(myObj) // stringify
userID = 'User ID' + myJSON

JavaScript - Regular Expressions

var a = str.search(/pattern/i)

Modifiers

i   Perform case-insensitive matching
g   Perform a global match
m   Perform multiline matching

Patterns

\         Escape character
d         Find a digit
s         Find a whitespace character
         Find match at beginning or end of a word
n+        Contains at least one n
n*        Contains zero or more occurrences of n
n?        Contains zero or one occurrences of n
^         Start of string
$         End of string
.         Any single character
(a|b)     a or b
(...)     Group section
[lower]   In range (a, b or c)
[0-9]     Any of the digits between the brackets
[^lower]  Not in range
s         White space
a?        Zero or one of a
a*        Zero or more of a
a*?       Zero or more, ungreedy
a+        One or more of a
a+?       One or more, ungreedy
a{2}      Exactly 2 of a
a{2,}     2 or more of a
a{,5}     Up to 5 of a
a{2,5}2   to 5 of a
a{2,5}?2  to 5 of a, ungreedy
[:punct:] Any punctu­ation symbol
[:space:] Any space character
[:blank:] Space or tab

JavaScript - Global Functions

eval() // executes a string as if it was a block of javascript
String(51) // return string from number
;(51).toString() // return string from number
Number('51') // return number from string
var sourceURI = 'https://mozilla.org/?x=шеллы'
// encode URI. Result: "https://mozilla.org/?x=%D1%88%D0%B5%D0%BB%D0%BB%D1%8B"
var encodedURI = encodeURI(sourceURI)
decodeURI(encodedURI) // decode URI. Result: "https://mozilla.org/?x=шеллы"
decodeURIComponent(encodedURI) // decode a URI component
encodeURIComponent(sourceURI) // encode a URI component
isFinite() // is variable a finite, legal number
isNaN() // is variable an illegal number
parseFloat() // returns floating point number of string
parseInt() // parses a string and returns an integer

JavaScript - Dates

//Mon Mar 18 2019 10:39:27 GMT+0100 (Central European Standard Time)
var d = new Date();
1552901967684 miliseconds passed since 1970
Number(d)
Date("2019-03-18");                 // create a date
Date("2019");                       // is set to Jan 01
Date("2019-03-18T10:39:27-09:45");  // date - time YYYY-MM-DDTHH:MM:SSZ
Date("March 18 2019");               // long date format
Date("Mar 18 2019 10:39:27 GMT+0100 (Central European Standard Time)"); // time zone

Get Times

var d = new Date()
a = d.getDay() // getting the weekday
d.getDate() // day as a number (1-31)
d.getDay() // weekday as a number (0-6)
d.getFullYear() // four digit year (yyyy)
d.getHours() // hour (0-23)
d.getMilliseconds() // milliseconds (0-999)
d.getMinutes() // minutes (0-59)
d.getMonth() // month (0-11)
d.getSeconds() // seconds (0-59)
d.getTime() // milliseconds since 1970

Setting Part of a Date

var d = new Date()
d.setDate(d.getDate() + 7) // adds a week to a date

d.setDate() // day as a number (1-31)
d.setFullYear() // year (optionally month and day)
d.setHours() // hour (0-23)
d.setMilliseconds() // milliseconds (0-999)
d.setMinutes() // minutes (0-59)
d.setMonth() // month (0-11)
d.setSeconds() // seconds (0-59)
d.setTime() // milliseconds since 1970)

JavaScripit - Maths

Other constants like Math.PI: E, SQRT2, SQRT1_2, LN2, LN10, LOG2E, Log10E.

var pi = Math.PI // 3.141592653589793
Math.round(4.4) // = 4 - rounded
Math.round(4.5) // = 5
Math.pow(2, 8) // = 256 - 2 to the power of 8
Math.sqrt(49) // = 7 - square root
Math.abs(-3.14) // = 3.14 - absolute, positive value
Math.ceil(3.14) // = 4 - rounded up
Math.floor(3.99) // = 3 - rounded down
Math.sin(0) // = 0 - sine
Math.cos(Math.PI) // OTHERS: tan,atan,asin,acos,
Math.min(0, 3, -2, 2) // = -2 - the lowest value
Math.max(0, 3, -2, 2) // = 3 - the highest value
Math.log(1) // = 0 natural logarithm
Math.exp(1) // = 2.7182pow(E,x)
Math.random() // random number between 0 and 1
Math.floor(Math.random() * 5) + 1 // random integer, from 1 to 5

JavaScript - Arrays

var lightStates = ['Low', 'Dim', 'High']
var lightStates = new Array('Low', 'Dim', 'High') // declaration

// access value at index, first item being [0]
var light = lightStates[1]
// change the second item, first item being [0]
lightStates[1] = 'Medium'
for (var i = 0; i < lightStates.length; i++) {
  // parsing with array.length
  console.log(lightStates[i])
}

Methods on Arrays

lightStates.toString() // convert to string: results "Low,Dim,High"
lightStates.join(' * ') // join: "Low * Dim * High"
lightStates.pop() // remove last element
lightStates.push('Bright') // add new element to the end
lightStates[lightStates.length] = 'Bright' // the same as push
lightStates.shift() // remove first element
lightStates.unshift('Dark') // add new element to the beginning

// change element to undefined (not recommended)
delete lightStates[0]
// add elements (where, how many to remove, element list)
lightStates.splice(2, 0, 'Bright', 'Extra bright')
// join two arrays (lightStates followed by tempStates and pressureStates)
var sensorStates = lightStates.concat(tempStates, pressureStates)

lightStates.slice(1, 4) // elements from [1] to [4-1]
lightStates.sort() // sort string alphabetically
lightStates.reverse() // sort string in descending order
x.sort(function(a, b) {
  return a - b
}) // numeric sort
x.sort(function(a, b) {
  return b - a
}) // numeric descending sort

// first item in sorted array is the lowest (or highest) value
highest = x[0]
// It's a random sort
x.sort(function(a, b) {
  return 0.5 - Math.random()
})

Manipulating Binary Data

Often times it necessary to manipulate data such as that read from Modbus (e.g., to change endianness). This is can be done in an Edge Action or, less commonly, in a Cloud Action, using JavaScript's support for bitwise operators.

For more information about bitwise operator support in JavaScript, see: Bitwise Operators.