Write A Decoder¶
For each payload kind sent by your IoT devices, you have to write a custom decoder.
You can organize the classes and the source files as you want. For this documentation, we will simply name the decoder as AppDecoder and place its source file in the same folder as that of the application source file.
A decoder is a class inheriting from the Decoder class provided by the plugin, and must provide an implementation for the abstract decode() method.
AppDecoder.ts - Basic inheritance¶import type { JSONObject, KuzzleRequest } from 'kuzzle'
import { Decoder } from 'kuzzle-plugin-thing-manager'
import type { DecodedPayload } from 'kuzzle-plugin-thing-manager'
export class AppDecoder extends Decoder {
// deviceModel: inferred from the class name => 'App'
// action: inferred from the device model => 'app'
// routes: default applies => POST /_/thing-manager/payload/app
/* ... to be continued */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
override decode(
decodedPayload: DecodedPayload<AppDecoder>,
payload: JSONObject,
request: KuzzleRequest, // probably unused context
): void {
/* ... to be continued */
}
}
For our demonstration, let’s say that the incoming payload is something like this sample:
{
"timestamp": 1672247313635,
"type": "onEnter",
"major": 512, "minor": 16,
"distance": 12,
"latitude": 47.12345, "longitude": -1.54321, "geotime": 1672247313635
}
Measures expected by this decoder must be declared by overriding the measures property:
AppDecoder.ts - Measures declaration¶import type { NamedMeasures } from 'kuzzle-plugin-thing-manager'
import { APP_POSITION_TYPE } from './consts'
// by convention, to make it easier to read traces, use camelCase for *_NAME
// and PascalCase for *_TYPE
const APP_POSITION_NAME = 'appPosition'
/* ... */
/* inside the class ... */
override measures: NamedMeasures = [ // default is an empty array
{
name: APP_POSITION_NAME,
type: APP_POSITION_TYPE,
},
] as const
As seen in Plugin Instantiation, APP_POSITION_TYPE points to a registered measure model, potentially used many times and by other decoders. APP_POSITION_NAME names here a local usage of that model.
To better understand these two meanings, let’s see another example with temperatures:
import { TEMPERATURE_TYPE } from './consts'
const INTERIOR_TEMPERATURE_NAME = 'interiorTemperature'
const EXTERIOR_TEMPERATURE_NAME = 'exteriorTemperature'
/* ... */
override measures: NamedMeasures = [
{ name: INTERIOR_TEMPERATURE_NAME, type: TEMPERATURE_TYPE },
{ name: EXTERIOR_TEMPERATURE_NAME, type: TEMPERATURE_TYPE },
] as const
Here is how we can implement the decoding:
AppDecoder.ts - decode() implementation¶// structure of the measure values given to decodedPayload
interface AppEventMeasure {
acquiredAt: number
type: string
distance: number
position: {
lat: number
lon: number
}
}
// structure of an item received in payload
interface PayloadItem {
timestamp: number
type: string
major: number
minor: number
distance: number
latitude?: number // location may not be available
longitude?: number
geotime?: number // timestamp of the geolocation, may be outdated in a sparing last-known strategy
}
/* ... */
/* inside decode() ... */
const item: PayloadItem = payload
const deviceReference = `${item.major}.${item.minor}`
decodedPayload.addMeasure<AppEventMeasure>(deviceReference, {
name: APP_POSITION_NAME,
type: APP_POSITION_TYPE,
measuredAt: item.timestamp,
values: {
acquiredAt: item.geotime || 0,
type: item.type,
distance: item.distance,
position: { lat: item.latitude || 0, lon: item.longitude || 0 }, // a geo_point mapping type
},
})
The preceding code processes a payload whose content is supposed to be as expected. How can we have such confidence? By implementing a custom validate() method. By default, this method doesn’t check anything.
AppDecoder.ts - validate() implementation¶import { PreconditionError } from 'kuzzle'
/* ... */
/* inside the class ... */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
override async validate(
payload: JSONObject,
request: KuzzleRequest, // probably unused context
): Promise<boolean> | never {
const item: PayloadItem = payload
this.assertProperties(
item,
['timestamp', 'type', 'major', 'minor', 'distance'], // the other properties may be missing
)
if (typeof item.distance !== 'number')
throw new PreconditionError('Attribute "distance" in payload must be a number')
/* add any relevant checks ... */
return true
}
Note
The assertProperties() method is a helper provided by the base class to verify that some properties are present in an object.
Additional advanced features of decoders are documented in dedicated pages:
Multiple Measures in a payload
Metadata about the device
Content Types other than json