In the following tutorial, we connect a Dragino LDS02 LoRaWAN Door Sensor to the platform via KPN Things.
You can also find the supporting material to help you kick-start the integration.
type JsonObject = { [key: string]: Json };
const kpnDeviceTypes = {
dragino: {
lds01: "dragino-lds01",
},
};
const deviceTypeHashIds = {
dragino: {
lds01: "TO DO",
},
};
function handle(args: Arguments): Result {
const body = JSON.parse(JSON.stringify(args.request.body?.data));
let deviceTypeHashId;
switch (args.request.headers["device-type"]) {
case kpnDeviceTypes.dragino.lds01:
deviceTypeHashId = deviceTypeHashIds.dragino.lds01;
break;
default:
throw new Error(
"Device type hash id is not available " +
args.request.headers["device-type"]
);
}
const deviceIdentifier = body[0].bn.substring(15, 31);
if (typeof deviceIdentifier !== "string") {
throw new Error("Device identifier not found!");
}
return {
deviceTypeHashId,
deviceIdentifier,
};
}
/**
* Validate request.
*
* @throws Throws an error when the request is invalid.
*/
function validateRequest(request: WebRequest, data: JsonObject) {
if (request.body?.type !== "json") {
throw new Error("Body must be JSON.");
}
}
const doorReportTypeHashId: string = "TO DO";
interface LoraData {
payload: string;
port: number;
}
interface GenericPayloadResponse {
data: LoraData;
}
/**
* Handle is the required function. It has two overloads, one for incoming HTTP requests and one for internal events
* When this function is called for an incoming HTTP requests, the function should return information about the response
* that should be returned.
*/
function handle(
args: ArgumentsForIncomingRequestEvent,
exec: Exec
): IncomingRequestResponse;
function handle(args: ArgumentsForEventWithoutResponse, exec: Exec): void;
function handle(args: Arguments, exec: Exec): IncomingRequestResponse | void {
if (args.event.type === "incomingRequest") {
const request = args.event.webRequest;
if (request.method === "POST" && request.url.path === "/") {
if (request.body === undefined || request.body.type !== "json") {
return {
statusCode: 400,
body: ["parameter_error", "Body is not of type JSON"],
};
}
// Set receive timestamp
let generatedAt = new Date();
generatedAt.setMilliseconds(0);
const requestData: GenericPayloadResponse = createGenericPayload(
JSON.stringify(request.body.data)
);
exec.addLog(`Generic payload: ${JSON.stringify(requestData)}`);
// pass parsing of the report to the right report type parser
if (requestData.data !== undefined) {
parseReport(
exec,
doorReportTypeHashId,
generatedAt,
JSON.stringify(requestData.data)
);
}
return { statusCode: 204 };
}
return {
statusCode: 404,
body: ["not_found_error", "Route cannot be found"],
};
}
}
function parseReport(
exec: Exec,
reportTypeHashId: string,
generatedAt: Date,
payload: string
) {
exec.parseReport({
reportTypeHashId: reportTypeHashId,
payload: JSON.stringify({
generatedAt: generatedAt,
payload: payload,
}),
});
}
function createGenericPayload(body: string): GenericPayloadResponse {
let payload: string = "",
port: number = 0,
identifier: string;
body = JSON.parse(body);
// KPN Lora
if (Array.isArray(body) && body[0].bn != undefined) {
(payload = body[1].vs), (port = parseInt(body[2].v));
} else {
throw new Error(`Failed to parse generic payload`);
}
return {
data: {
payload,
port,
},
};
}
type JsonObject = { [key: string]: Json };
/**
* Source: http://www.dragino.com/downloads/downloads/LoRa_End_Node/LDS01/LDS01_LoRaWAN_Door_Sensor_UserManual_v1.2.0.pdf
*/
const operatingVoltageQuantityHashId: string = "TO DO";
const doorOpenStatusQuantityHashId: string = "TO DO";
const totalOpenDoorEventsQuantityHashId: string = "TO DO";
const lastDoorOpenDurationQuantityHashId: string = "TO DO";
const doorChannelIndex: number = 0;
const sensorConditionChannelIndex: number = 1;
function handle(args: Arguments, exec: Exec): Result {
const data = JSON.parse(args.payload);
if (typeof data !== "object" || data === null) {
throw new Error("Data is not a JSON-object");
}
const generatedAtMs = Date.parse(data.generatedAt);
if (Number.isNaN(generatedAtMs)) {
throw new Error("generatedAt cannot be parsed into a valid Date");
}
const generatedAt = new Date(generatedAtMs);
const parsedLoraData = JSON.parse(data.payload);
const payload = parsedLoraData.payload;
const port = parsedLoraData.port;
exec.addLog(
`Device payload and port as received by report parser: ${payload}, ${port}`
);
if (typeof payload != "string") {
throw new Error("Payload is not a string");
}
const decodedPayload = hexToBytes(payload);
exec.addLog(`Bytes: ${decodedPayload}`);
const operatingVoltage =
((decodedPayload[0] << 8) | decodedPayload[1]) & 0x3fff;
const doorOpenStatus = decodedPayload[0] & 0x80 ? 1 : 0;
const totalOpenDoorEvents =
(decodedPayload[3] << 16) | (decodedPayload[4] << 8) | decodedPayload[5];
const lastDoorOpenDuration =
(decodedPayload[6] << 16) | (decodedPayload[7] << 8) | decodedPayload[8];
const measurements: Result["measurements"] = [];
pushMeasurement(
measurements,
sensorConditionChannelIndex,
operatingVoltageQuantityHashId,
generatedAt,
-3,
operatingVoltage
);
pushMeasurement(
measurements,
doorChannelIndex,
doorOpenStatusQuantityHashId,
generatedAt,
0,
doorOpenStatus
);
pushMeasurement(
measurements,
doorChannelIndex,
totalOpenDoorEventsQuantityHashId,
generatedAt,
0,
totalOpenDoorEvents
);
pushMeasurement(
measurements,
doorChannelIndex,
lastDoorOpenDurationQuantityHashId,
generatedAt,
0,
lastDoorOpenDuration
);
return {
generatedAt,
measurements,
fields: {},
};
}
function pushMeasurement(
measurements: Result["measurements"],
channelIndex: number,
quantityHashId: string,
generatedAt: Date,
orderOfMagnitude: number,
significand: number
) {
measurements.push({
channelIndex: channelIndex,
quantityHashId: quantityHashId,
generatedAt: generatedAt,
orderOfMagnitude: orderOfMagnitude,
significand: significand,
});
}
function hexToBytes(hex: string): Uint8Array {
for (var bytes = [], c = 0; c < hex.length; c += 2)
bytes.push(parseInt(hex.substr(c, 2), 16));
return Uint8Array.from(bytes);
}
tyo
Sending downlinks
Each time, before we want to connect to the device via KPN Things API, we need to request an access token. The access token can be requested with the following code snippet.
In this last part, the user needs to provide the devEUI of the device of interest, port (e.g. 2), and a raw payload in a hexadecimal format.
To be able to send a downlink message to devices via KPN Things, it is important to correctly access the API. The "KPN Things Developer " and the "KPN Things API's public " provides a detailed information on how this can be done. Here, we conclude the most important steps required to send a raw payload.
The KPN kpnTenantID can be found by completing the following , while the steps to obtain kpnClientID (API key ID) and the kpnClientSecret (API key secret key) are explained .