KPN
Learn how to integrate Withthegrid platform with KPN.
Connecting a device
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
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 Manual" and the "KPN Things API's public documentation" provides a detailed information on how this can be done. Here, we conclude the most important steps required to send a raw payload.
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.
function kpnGetBearerToken(exec: Exec): string {
const args: SendRequestArgs = {
url: kpnRequestTokenUrl,
method: 'post',
headers: {
"Content-Type": "application/json; charset=utf-8"
},
body: {
"grant_type": "client_credentials",
"audience": kpnAudience,
"client_id": kpnClientID,
"client_secret": kpnClientSecret
}
}
const response: any = exec.sendRequest(args);
if(response.body?.type != 'json') {
throw new Error("Failed to get KPN access token!");
}
return response.body.data.access_token;
}
The following inputs need to be defined.
const kpnTenantID = 'TO DO'
const kpnDownlinkURL = 'https://api.kpnthings.com/api/actuator/downlinks';
const kpnRequestTokenUrl = 'https://auth.grip-on-it.com/v2/' + kpnTenantID + '/oidc/idp/c1/token';
const kpnAudience = '4dc82561-f65f-523g-dek9-6c79ec314f02';
const kpnClientID = 'TO DO'
const kpnClientSecret = 'TO DO'
The KPN kpnTenantID
can be found by completing the following steps, while the steps to obtain kpnClientID
(API key ID) and the kpnClientSecret
(API key secret key) are explained here.
The last steps are to construct the request body, downlink URL, request header, and to execute the POST
request.
const body = '[{"bn":"urn:dev:DEVEUI:'+devEUI+':", "n":"payloadHex", "vs": "' + payload + '"}]';
const url = kpnDownlinkURL + "?port=" + port.toString(10);
const bearerToken = kpnGetBearerToken(exec);
if (bearerToken === null || bearerToken.length === 0) {
throw new Error(`BearerToken is null or length is ${bearerToken.length}`);
}
const header = {
"Authorization": "Bearer " + bearerToken,
"Accept": "application/vnd.kpnthings.actuator.v1.response+json",
"Content-Type": "application/json"
}
const response = exec.sendRequest({url, method: 'post', body: body, headers: header});
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.
Last updated