Writing Formulas

How to write formulas in calculations and alerts.

Edge processing takes sensor data, aggregates it, and computes all the Calculations and Alerts all according to your Thing Type Specifications.

The following section describes the syntax for writing these functions. The base syntax for all these formulas is JavaScript (specifically Node.js).

Function Basics

Let’s assume our Example IoT Connected Asset produces a message which looks like this:

interface thing_message {
  pressure_volts: number;
  temperature: number;
  state: string;
}

The Gateway starts building the following message:

const message = {
  sensor: {
    pressure_volts: 5,
    temperature_volts: 6,
    state: "on",
  },
};

Let’s write the function for a Calculation named pressure_psi:

(sensor.pressure_volts * 20) / 10;

The Gateway message now looks like:

const message = {
  sensor: {
    pressure_volts: 5,
    temperature_volts: 6,
    state: "on",
  },
  calculation: {
    pressure_psi: 10,
  },
};

Now let’s write a formula for an alert on pressure named overpressure:

sensor.state === "on" && calculation.pressure_psi > 15;

Our Gateway message now looks something like:

const message = {
  sensor: {
    pressure_volts: 5,
    temperature_volts: 6,
    state: "on",
  },
  calculation: {
    pressure_psi: 10,
  },
  alert: {
    overpressure: false,
  },
};

This is the basic structure you’re putting together when building your Thing Type specification.

Namespaces

There are 5 base namespaces which can be used to access data from these formulas:

sensor
Provides access to all the latest aggregated sensor values. Aggregation is defined by the Thing Type specification Salesforce, ServiceNow.
calculation
Provides access to all the latest calculated values. Calculations are defined by the Thing Type specification Salesforce, ServiceNow.
alert
Provides access to all the latest alert values. Alerts are defined by the Thing Type specification Salesforce, ServiceNow.
context
Provides access to all the latest context values. Context is defined by the Context settings Salesforce, ServiceNow.

Context settings.

twin
Provides access to all the the following values:
  • role
    the Digital Twin role for the referenced IoT Connected Asset

Priority

When specifying calculations and alerts in the thing-type specification (Salesforce, ServiceNow), the priority defines the execution order of each calculation/alert. Alerts are always evaluated after all calculations. Within each group (calculations and alerts), formulas are evaluated in priority order from smallest (0) to largest (infitinity). A calculatoin with priority 0 will be evaluated before a calculation with priority 1 and so on. For elements with equal priority, their order relative to each other is arbitrary. So in the following example:

  • calcA: priority = 0
  • calcB: priority = 10
  • calcC: priority = 10
  • calcD: priority = 11

calcA is evaluated first followed by calcC and calcB (in no defined order), then finally calcD is evaluated. Once all the calculations are evaluated, the alerts are then evaluated.

When referring to calculations which are evaluated later in the cycle from within calculations which happen earlier, there will be a unit-delay in the value retrieved.

Example:

  • calcA: priority = 0, formula = (calculation.calcB === undefined) ? 0 : calculation.calcB + 1
  • calcB: priority = 1, formula = calculation.calcA + 1
  1. On the first pass, calculation.calcB will be undefined, so calcA will be 0, calcB is then 1
  2. On the second pass, calcA will be 2, calcB will be 3
  3. … and so on

Specification Metadata Access

In general, when you reference a sensor, calculation, or alert within a formula, it implicitly retrieves it’s current value (i.e. calculation.calcAbc implicitly becomes calculation.calcAbc.value). Alternatively, there are other metadata which can be retrieved from the given element.

In addition to the implicit value, the following parameters are available:

  • Sensor
    • name: the calculation name (e.g. sensor.sensorAbc.name = "sensorAbc")
    • label: the calculation label (e.g. sensor.sensorAbc.label = "Sensor ABC")
    • dataType: the calculation data-type (e.g. sensor.sensorAbc.dataType = "Number")
    • aggregateType: the sensor’s aggregation-type (e.g. sensor.sensorAbc.aggregateType = "Seconds")
    • aggregateFunction: the sensor’s aggregation-function (e.g. sensor.sensorAbc.aggregateFunction = "MAX")
    • aggregateInterval: the sensor’s aggregation-interval (e.g. sensor.sensorAbc.aggregateType = 300)
    • isStored: true if the sensor is stored in the cloud (e.g. sensor.sensorAbc.isStored = true)
    • storageDataType: the cloud storage type (e.g. sensor.sensorAbc.isStored = "Double")
  • Calculation
    • name: the calculation name (e.g. calculation.calcAbc.name = "calcAbc")
    • label: the calculation label (e.g. calculation.calcAbc.label = "Calculation ABC")
    • formula: the calculation formula (e.g. calculation.calcAbc.formula = "1 + 1")
    • priority: the calculation priority (e.g. calculation.calcAbc.priority = 14)
    • isActive: true if the calculation is active (e.g. calculation.calcAbc.isActive = true)
    • isStored: true if the calculation is stored in the cloud (e.g. calculation.calcAbc.isStored = true)
    • storageDataType: the cloud storage type (e.g. calculation.calcAbc.isStored = "Double")
  • Alert
    • name: the alert name (e.g. alert.alertAbc.name = "alertAbc")
    • label: the alert label (e.g. alert.alertAbc.label = "Alert ABC")
    • alertType: the alert-type (e.g. alert.alertAbc.alertType = "Threshold")
    • alertInterval: the interval which the alert re-drives to ServiceNow (e.g. alert.alertAbc.alertInterval = 3600)
    • formula: the alert formula (e.g. alert.alertAbc.formula = "calculation.calcAbc > 1")
    • priority: the alert priority (e.g. alert.alertAbc.priority = 14)
    • isActive: true if the alert is active (e.g. alert.alertAbc.isActive = true)
    • isStored: true if the alert is stored in the cloud (e.g. alert.alertAbc.isStored = true)

Examples


Example 1 Constant

Name: example1

Description: Always return a constant value for the calculation or alert.

Formula:

"hello world";

Result: constant { ... example1: 'hello world' ... } in each message


Example 2 Basic Arithmetic

Name: example2

Description: Scale sensorA

Formula:

sensor.sensorA * 10 + 2;

Result:

{
  "sensorA": 3,
  "example2": 32
}

Example 3 Conditional

Name: example3

Description: If the IoT Connected Asset’s sensorA is over 10, calculate, otherwise return 0.

Formula:

sensor.sensorA > 10 ? sensor.sensorB * 50 : 0;

Results:

{
  sensorA: 3,
  sensorB: 6,
  example3: 0
}

{
  sensorA: 11,
  sensorB: 6,
  example3: 300
}

Alternate

if (sensor.sensorA > 10) {
  return sensor.sensorB * 50;
} else {
  return 0;
}

Example 4 Digital Twin

Name: example4

Description: If the IoT Connected Asset’s parent is powered on, calculate, otherwise return 0.

Formula:

$parent.calculation.powerOn ? sensor.sensorB / $parent.sensor.inputVoltage : 0;

Results:

parentId: {
  sensor: {
    inputVoltage: 12
  },
  calculation: {
    powerOn: true
  }
}
{
  "sensorB": 6,
  "example4": 0.5
}
parentId.sensor.inputVoltage: 0
parentId.calculation.powerOn: false
{
  "sensorB": 6,
  "example4": 0
}

Alternate

if ($parent.calculation.powerOn) {
  return sensor.sensorB / $parent.sensor.inputVoltage;
} else {
  return 0;
}

Example 5 Context

Name: example5

Description: If the IoT Connected Asset’s entitlement level is either Platinum or Gold, evaluate the threshold, otherwise return false.

Formula:

["platinum", "gold"].includes(context.entitlementLevel.toLowerCase())
  ? calculation.someValue > 100
  : false;

Results:

context.entitlementLevel = "Gold";
{
  "someValue": 101,
  "example5": true
}
context.entitlementLevel = "Silver";
{
  "someValue": 101,
  "example5": false
}

Alternates

if (["platinum", "gold"].includes(context.entitlementLevel.toLowerCase())) {
  return calculation.someValue > 100;
} else {
  return false;
}
function isCovered(level) {
  const coveredLevels = ["platinum", "gold"];
  return coveredLevels.includes(level.toLowerCase());
}

if (isCovered(context.entitlementLevel)) {
  return calculation.someValue > 100;
} else {
  return false;
}