Skip to main content

Translator API

Reference for the IoT platform's Translator Service - the object schema, the translate function contract, and how translators are selected and run. This is a reference; for a step-by-step guide with worked examples and a local test harness, see Translator Development.

API endpoints are documented in Swagger.

Runtime​

Translators run in a sandboxed JavaScript environment (isolated V8). All standard JavaScript built-ins are available (Date, Math, JSON, …), plus lodash as _, Buffer as Buffer, and console.log. The sandbox does not provide require/import, timers, or network/filesystem access. A translator interacts with the platform only through the value it returns from translate.

The translator object​

A translator is a JSON object with these fields:

FieldRequiredDescription
nameyesUnique name. Lowercase alphanumerics and dashes, must start with a letter (/^[a-z][a-z0-9-]*$/).
versionyesSemver Major.Minor.Patch.
apiVersionyesTranslator-service API version the translator targets (current: 1.0).
descriptionyesUser-facing Markdown description (shown in the IoT platform).
matchnoWhich devices it applies to - an object with deviceModelName (string).
regexnoStored on the translator as a sibling of match, not a key inside it. Matching is currently exact - see below.
specyesThe fields the translator may emit (see spec). Set spec: false to disable validation.
parametersnoUser-supplied inputs (see parameters).
codeyesA string of JavaScript defining a translate function (see code).

regex is accepted and stored, but device matching is currently an exact comparison of match.deviceModelName against the device model name. Set deviceModelName to the exact name rather than a pattern.

spec​

spec declares every field translate is allowed to emit; the sandbox rejects any emitted key not present in spec. Each entry is one of:

  • A bare type string - one of 'string', 'number', 'boolean', 'date', 'object', 'array', 'geoJsonPoint'. Use for identifiers, booleans, and opaque values:
    spec: { serialNumber: 'string', tamperAlarm: 'boolean' }
  • A measurement descriptor - {type, unit, quantity}, where unit is an SI symbol (or '' for dimensionless) and quantity is the semantic group:
    spec: { temperature: {type: 'number', unit: '°C', quantity: 'temperature'} }

Field names, units and quantities are defined by the canonical field catalogue (the IoT platform's data model) - see Data model for the common fields and the naming rules.

parameters​

parameters declares values the user supplies when selecting the translator. They are validated against the schema and passed as the second argument to translate.

parameters: {
// primitive
someNumber: 'number',
someBoolean: 'boolean',
someString: 'string',

// constrained
constrainedNumber: {type: 'number', min: 10, max: 30},
choice: {type: 'enum', values: ['apple', 'pear']},

// optional / default (both make the parameter optional to the user)
offset: {type: 'number', optional: true},
timeZone: {type: 'string', default: 'Europe/Stockholm'},
}

code​

code is a string of JavaScript that defines a function named translate (plus any helper functions it calls - helpers must live inside the same code string, since the whole string is what runs in the sandbox).

Data model (field catalogue)​

The IoT platform flattens every device onto one node using a shared vocabulary of canonical field names - the field catalogue. A field's meaning lives entirely in its name, unit and quantity, so translators for different vendors must emit the same field for the same measurement. Map the vendor's raw names to canonical fields in both spec and the returned result.

Rules:

  • lowerCamelCase, NGSI-LD flat names - no snake_case, no vendor jargon (TempC_SHT, BatV) in the output.
  • SI units (°C, %, Pa, V, A, W, kWh, m, m/s); dimensionless values use unit '' with a camelCase quantity. Convert in the translator (e.g. hPa → Pa is × 100, mV → V is / 1000).
  • Temperature carries its medium in the name: temperature = air/ambient; waterTemperature, soilTemperature, surfaceTemperature; externalTemperature/2/3 = probe of unspecified medium; internalTemperature/cpuTemperature = device electronics.
  • Indexed channels: the bare name is the primary, extras start at 2 (temperature, temperature2; a …1 form is a legacy alias).

Common fields​

FieldUnitQuantity
temperature°Ctemperature
internalTemperature°Ctemperature
externalTemperature°Ctemperature
waterTemperature°Ctemperature
soilTemperature°Ctemperature
relativeHumidity%relativeHumidity
pressurePapressure
co2ppmco2
tvocppbtvoc
pm2_5ug/m^3pm2_5
pm10ug/m^3pm10
illuminancelxilluminance
windSpeedm/svelocity
precipitationmmprecipitation
distancemlength
waterLevelmlength
soilMoisture%soilMoisture
batteryLevel%fill
batteryVoltageVvoltage
current1Acurrent
voltageL1NVvoltage
activeEnergyImportkWhenergy
motion''count
lnglat- (type geoJsonPoint)GNSS position

Boolean / status fields use a bare type: batteryLow, presence, digital, relayState ('boolean'); contact, occupied (small-enum 'number'). Alarm fields are subject-first booleans with an *Alarm suffix (temperatureLowAlarm, tamperAlarm), quantity: 'alarm'.

Before inventing a name, check the common fields above (or GET /api/translators) for an existing one.

Harmonization examples​

A translator's job is to rename the vendor's raw output to canonical fields.

When wrapping a manufacturer reference decoder, keep the decoder verbatim and perform the harmonization as the final step - map its raw output to canonical fields in the returned result, scaling units accurately (e.g. hPa → Pa, mV → V, cm → m). Do not change the vendor decoder's internal logic.

Dragino LHT65

RawCanonical
TempC_SHT (built-in)internalTemperature
TempC_DS (external probe)temperature
Hum_SHTrelativeHumidity
BatVbatteryVoltage

Milesight AM319 (note the unit fix)

RawCanonical
temperaturetemperature
humidityrelativeHumidity
batterybatteryLevel
co2co2
pressure (hPa)pressure (Pa, × 100)

Because both emit relativeHumidity (%, relativeHumidity), the same value means the same thing regardless of vendor - and the same downstream translators and views work across all devices.

The translate function​

translate runs every time new data arrives for the device's IoTNode. Signature:

function translate (iotnode, parameters) { /* ... */ }
  • iotnode - the node being translated. A translator can read any field on it. Raw uplink payloads for a hardware decoder are under iotnode.encodedData ({hexEncoded, port}); the payload receive time is iotnode.reportedAt. Prefer reportedAt over Date.now() for observation/event times - Date.now()/new Date() work, but the wall clock is the processing time and is non-deterministic (it breaks reproducibility if a translation is replayed). When translators are chained, iotnode also carries the fields produced by the previous translators.
  • parameters - the validated parameters object (or {}).

translate must return an object or throw an Error. The object may be empty ({}) when nothing could be translated. Recognized return fields - return any, all, or none:

Return fieldMeaning
resultUpdated values for the iotnode. Only keys declared in spec are allowed; undefined and NaN values are removed (null is not - see below).
timeseriesArray of {timestamp, value} - timestamp a JS Date, value following the same rules as result.
additionalDeviceUpdatesUpdates for other devices (gateways) - see below.

Because the platform removes undefined and NaN from result, a translator can emit a field conditionally and return the plain object without stripping it itself.

null is the exception, and it is not a harmless one. A null is kept rather than removed, and it then fails the declared type in spec, which rejects the whole translation rather than just that field. A vendor decoder that returns null for an absent reading therefore has to be cleaned up before the result is returned - leave the key out, or set it to undefined.

Errors and logging​

When a translator throws, the IoT platform records the error in the device's Logs and keeps it for 6 hours - filter Type = Debug, Category = System to find it. A successful translation logs nothing, so an empty log means the translator either ran cleanly or never triggered (check the device's Last reported time to tell which).

Minimal example​

function translate (iotnode) {
return {result: {temperature: 21.4, batteryVoltage: 3.01}};
}

const translator = {
name: 'examplecorp-demodevice9000',
version: '1.0.0',
apiVersion: '1.0',
description: 'A translator for ExampleCorp DemoDevice 9000.',
match: {deviceModelName: 'examplecorp-demodevice9000'},
spec: {
temperature: {type: 'number', unit: '°C', quantity: 'temperature'},
batteryVoltage: {type: 'number', unit: 'V', quantity: 'voltage'},
},
code: translate.toString(),
};

Timeseries example​

return {
timeseries: [
{timestamp: new Date('2021-11-12T14:30:00Z'), value: {temperature: 12}},
{timestamp: new Date('2021-11-12T14:15:00Z'), value: {temperature: 11}},
],
};

Selecting translators​

Which translators run for a device is set by the device's translatorPreferences (settable on device creation or update):

translatorPreferences: [
{name: 'sensative-strips', userId: '…', version: '1.0.0', upgradePolicy: 'minor'},
{name: 'celsius-to-fahrenheit', userId: '…', version: '1.2.0', upgradePolicy: 'all'},
]

version + upgradePolicy select which version runs:

upgradePolicyBehaviour (selected 1.0.0)
nonealways the selected version
patchlatest 1.0.x
minorlatest 1.x
alllatest available

Use GET /api/translators (optionally ?deviceModelName=…) to list translators suitable for a device and populate translatorPreferences. When a device is created with a deviceModelName, the IoT platform auto-populates a suitable default translator.

If a translator authored by Sensative matches, it is preferred over others.

Chaining (multiple translators)​

Translators run sequentially; each receives the previous results merged with the original iotnode:

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

where A, B are translators, x is the origin iotnode, and + is a merge. A translator therefore reads the fields produced by earlier translators. If a translator throws, the chain stops and later translators do not run.

Gateways​

A translator on a gateway (or gateway-like device) may update other devices via additionalDeviceUpdates - an array of {identifier, result}:

  • identifier - an object identifying the target device, e.g. {devEui: '123456789012345'} or {wMbusDeviceId: '12312312', manufacturer: 'BMT'}.
  • result - the updates for that device (same rules as result).

The device may also update itself in the same return via result.

return {
result: {internalTemperature: 13},
additionalDeviceUpdates: [
{identifier: {devEui: '123456789012345'}, result: {temperature: 11}},
],
};

Versioning​

Versions are semver Major.Minor.Patch. On update the version resolved by each device's upgradePolicy is used (with Sensative-authored translators preferred). You must bump the major version whenever the data model changes - a field added, renamed or removed, or its value semantics (unit, scaling, meaning) changed. Metadata-only changes or same-output bug fixes do not need a major bump.

Ownership and deletion​

An uploaded translator is owned by its uploader (userId) permanently. Users cannot delete translators - to avoid breaking other users who depend on them. A platform administrator may delete one only under extraordinary circumstances (e.g. malicious code).

API versions​

apiVersion 1.0 is the current translator-service API and the basis for future versions.


See Translator Development for the data-model rules and worked templates (hardware, calculate, set-alarm, analytics), plus the tooling to test and ship a translator without any of the IoT platform's infrastructure:

  • translator-harness.js - run your translate function locally against test payloads (recreates the sandbox with Node's vm; needs only npm install lodash).
  • translator-build.js - package your function + metadata into the translator.json to POST to the API.