Translator API

As of Yggio version 3.4 the new Translator Service is now enabled. It enables developers to add their own Translators to Yggio. API details can be found in Swagger. This document describes in depth how the Translator Service works.

All translators run in a sandboxed NodeJS v16 environment. The translators are limited in that they may only access pure JS functions and can not access Node-specific features via import and require(). We also provide definitions of lodash (as "_") and Buffer.

Using translators

Which translators will be run for a device are chosen by the device's translatorPreferences.

Example:

translatorPreferences: [
  {
    name: 'sensative-strips',
    userId: '91698973-01ac-4879-a9f6-927611463f2e',
    version: '1.0.0',
    upgradePolicy: 'minor',
  },
  {
    name: 'celsius-to-fahrenheit',
    userId: '91698973-01ac-4879-a9f6-927611463f3a',
    version: '1.2.0',
    upgradePolicy: 'all',
  },
]

The properties of a translatorPreference are used to identify a translator. version together with upgradePolicy are used to select the version. The upgradePolicy is used to automatically select a newer version of a translator without the user needing to manually update to the new version.

upgradePolicy can be one of the four following:

  • none The selected version will always be used
  • patch If you've selected version 1.0.0 it will result in 1.^
  • minor If you've selected version 1.0.0 it will result in 1.0.^
  • all: The absolute newest version available will be used

Selecting translatorPreferences

translatorPreferences are an attribute of a device and can be set upon creation or when updating a device.

An API user can use the GET /translators route to get the available translators and use them to populate a device's translatorPreferences. You can supply deviceModelName as a query parameter in order to only get translators that are suitable for that deviceModelName.

There is also a simpler way to add translatorPreferences when creating a device. If you supply a deviceModelName, Yggio will automatically populate translatorPreferences with a suitable default translator (that matches the with the deviceModelName).

Running multiple translators

It is possible to run multiple translators. This can be used to perform powerful operations. For example, if a translator outputs the temperature in celsius a second translator can be used to translate the temperature to Fahrenheit.

The translators are then run in a sequential or "chanined" fashion. The input of a translator is the result of the previous translators, merged with the original iotnode.

This can be described with the following formula:

result = A(x) + B(x + A(x))

where A and B are translators, x is the origin iotnode data and + denotes merging.

Note that if a translator fails to run the loop is stopped and the succeeding translators will not be run.

Creating a Translator

The Translator Service is able to run any valid JavaScript code in a sandboxed environment. A translator can only interact with its environment by returning data from the translate function. The translator gets executed every time that new data for a iotnode is received.

Translator specification

Translators are JSON objects and have a couple required metadata fields that define where, when and how they will be applied.

name is the name of the translator. May only contain lowercase alphanumeric characters and dashes, must start with a letter. Regex: /[a-z][a-z0-9-]+/

version must be a correct semver version number.

apiVersion the version of the Yggio translator api that this translator is designed to work with.

description a user friendly description of the translator.

match an object that specifies which devices the translator is applicable for. May (currently) only contain the field deviceModelName with a string as value.

spec defines the type of data the translator will return. Every field inside this object must contain a string with a valid type, any of array, date, boolean, number, object, string. The object may specify many fields.

code is a field that must contain a string with valid javascript code (long strings allowed). The translators code must satisfy the following requirements.

Translator code

Translator code is written in JavaScript and must contain a function named translate. It runs every time a new payload is received for the related IoTNode. The translation function (function translate(iotnode) { ... }) can access any field on the iotnode to be translated. If you require the Date at which the payload was received, it is available as iotnode.reportedAt.

The translator must return an object or throw an Error. This object may be empty if the translator could not translate any values. If an error is encountered the translator should throw an apropriate Error.

The object (from here on called translated) returned by the translator will hold fields of semantic value. Any fields not specified here will be treated as an Error. Currently there are two possible fields for the translator to return. The translator may chose to return none, any or both of these fields.

If the translator returns an object with a translated.result field, this field must contain updated values for the iotnode. Only fields in the translators spec may be updated. Returned fields that are undefined or null will be removed.

Timeseries data can be returned via the translated.timeseries field. This field must contain an array of objects {timestamp, value} where timestamp must be a valid JavaScript Date object and value follows the same rules as the previously mentioned result field.

When a translator is uploaded it will belong to the uploader forever. This is stored in the translators userId field.

Example

// This is an example translator
function translate(iotnode) { // you can access the full iotnode object for the device

  /* translator code here */

  return {
    result: {
      someValue: 13,
      anotherValue: 37,
    },
    timeseries: [{
      timestamp: new Date("2021-11-12T14:30:00Z"),
      value: {
        someValue: 12,
      },
    }, {
      timestamp: new Date("2021-11-12T14:15:00Z"),
      value: {
        someValue: 11,
      },
    }],
  };
}

const translator = {
  name: 'my-translator',
  version: '0.2.1',
  apiVersion: '1.0',
  description: 'A translator for ExampleCorp DemoDevice™ 9000',
  match: {
    deviceModelName: 'examplecorp-demodevice9000',
  },
  spec: {
    someValue: 'number',
    anotherValue: 'number',
  },
  code: translate.toString(),
}

/* the translator object can now be POSTed to the API */

Translators for gateways or devices acting as gateways

A translator can return the field translated.additionalDeviceUpdates which is an array with updates for other devices. translated.additionalDeviceUpdates objects need to have two keys, translated.additionalDeviceUpdates.identifier and translated.additionalDeviceUpdates.result. translated.additionalDeviceUpdates.[i].identifier must be an object with key/s and value/s that can identify a specific device. An example for LoRa would be {devEui: '123456789012345'} and for wireless Mbus it would be {wMbusDeviceId: '12312312', manufacturer: 'BMT'}. translated.additionalDeviceUpdates.[i].result contains the updates for the device. The device or gateway may also post updates for itself in the same payload.

Example

// This is an example translator
function translate(iotnode) { // you can access the full iotnode object for the device

  /* translator code here */

  return {
    result: {
      internalTemperature: 13,
      anotherValue: 37,
    },
    additionalDeviceUpdates: [{
      identifier: {
        wMbusDeviceId: '12312312',
        manufacturer: 'BMT'
      },
      result: {
        someValue: 12,
      },
    }, {
      identifier: {
        devEui: '123456789012345'
      },
      result: {
        someValue: 11,
      },
    }],
  };
}

const translator = {
  name: 'my-gateway',
  version: '0.0.1',
  apiVersion: '0.1',
  description: 'A translator for ExampleCorp DemoGateway™ 9001',
  match: {
    deviceModelName: 'examplecorp-demogateway9001',
  },
  spec: {
    someValue: 'number',
    anotherValue: 'number',
  },
  code: translate.toString(),
}

Translator versions

Translators must have correct semver compliant version numbers: Major.Minor.Patch. When an Iotnode is updated, the translator with the highest version number will be automatically selected to do the translation.

If there is already a translator created by Sensative, that translator will be selected before other translators are considered.

Deleting Translators

Translators can currently not be deleted by a user. This is to avoid cases where a translator is in use by another user than the original creator.

A translator may be deleted by an Yggio administrator only under extraordinary circumstances, like a translator containing malicious code.

Yggio translator-service API-versions

Yggio 3.6 uses version 1.0 of the translator-service API, this is to be considered the basis for all future versions.