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:
| Field | Required | Description |
|---|---|---|
name | yes | Unique name. Lowercase alphanumerics and dashes, must start with a letter (/^[a-z][a-z0-9-]*$/). |
version | yes | Semver Major.Minor.Patch. |
apiVersion | yes | Translator-service API version the translator targets (current: 1.0). |
description | yes | User-facing Markdown description (shown in the IoT platform). |
match | no | Which devices it applies to - an object with deviceModelName (string). |
regex | no | Stored on the translator as a sibling of match, not a key inside it. Matching is currently exact - see below. |
spec | yes | The fields the translator may emit (see spec). Set spec: false to disable validation. |
parameters | no | User-supplied inputs (see parameters). |
code | yes | A 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}, whereunitis an SI symbol (or''for dimensionless) andquantityis 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 camelCasequantity. Convert in the translator (e.g.hPa → Pais× 100,mV → Vis/ 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…1form is a legacy alias).
Common fields
| Field | Unit | Quantity |
|---|---|---|
temperature | °C | temperature |
internalTemperature | °C | temperature |
externalTemperature | °C | temperature |
waterTemperature | °C | temperature |
soilTemperature | °C | temperature |
relativeHumidity | % | relativeHumidity |
pressure | Pa | pressure |
co2 | ppm | co2 |
tvoc | ppb | tvoc |
pm2_5 | ug/m^3 | pm2_5 |
pm10 | ug/m^3 | pm10 |
illuminance | lx | illuminance |
windSpeed | m/s | velocity |
precipitation | mm | precipitation |
distance | m | length |
waterLevel | m | length |
soilMoisture | % | soilMoisture |
batteryLevel | % | fill |
batteryVoltage | V | voltage |
current1 | A | current |
voltageL1N | V | voltage |
activeEnergyImport | kWh | energy |
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
| Raw | Canonical |
|---|---|
TempC_SHT (built-in) | internalTemperature |
TempC_DS (external probe) | temperature |
Hum_SHT | relativeHumidity |
BatV | batteryVoltage |
Milesight AM319 (note the unit fix)
| Raw | Canonical |
|---|---|
temperature | temperature |
humidity | relativeHumidity |
battery | batteryLevel |
co2 | co2 |
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 underiotnode.encodedData({hexEncoded, port}); the payload receive time isiotnode.reportedAt. PreferreportedAtoverDate.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,iotnodealso carries the fields produced by the previous translators.parameters- the validatedparametersobject (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 field | Meaning |
|---|---|
result | Updated values for the iotnode. Only keys declared in spec are allowed; undefined and NaN values are removed (null is not - see below). |
timeseries | Array of {timestamp, value} - timestamp a JS Date, value following the same rules as result. |
additionalDeviceUpdates | Updates 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:
upgradePolicy | Behaviour (selected 1.0.0) |
|---|---|
none | always the selected version |
patch | latest 1.0.x |
minor | latest 1.x |
all | latest 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 asresult).
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 yourtranslatefunction locally against test payloads (recreates the sandbox with Node'svm; needs onlynpm install lodash).translator-build.js- package your function + metadata into thetranslator.jsontoPOSTto the API.