Here at Losant, while we love devices and sensors and "things" (and all of their associated data) we understand that folks have non-IoT related data as well – data that probably won't be stored inside of Losant. And when there is data stored elsewhere, it ends up being almost guaranteed that to correctly deal with your IoT data, you will need access to that external data in order to run business logic. Losant workflows already have some nodes that make doing this possible (the HTTP node, the Webhook trigger), but we have now added another that lends itself almost perfectly to solving this kind of problem: the AWS Lambda node.
AWS Lambda is a service that lets you run predefined code blocks that can do whatever you want or need – and they are easily integrated with other AWS services. So if the data that you need to access is stored somewhere in AWS infrastructure, a Lambda node is the perfect way to pull that data into a Losant workflow. Today we are going to build such an example, where DynamoDB stores user data and device-to-user mappings, and we use a Lambda node to pull in the appropriate user when a Losant workflow runs.
Our devices in DynamoDB are stored by device serial numbers, not by Losant device IDs – the first step is to make sure that the serial numbers are available on our Losant devices. We do this with device tags, either through our bulk creation process (which would let you import a CSV of all of the serial numbers and create the devices), or just by adding tags manually. By having the serial as a tag, we will have the serial number available for use as a lookup key later. Below you can see one such tag, where the device has the the deviceSerial
tag of 64QujxoyzCsuqv
.
Now lets start building our workflow. The goal of our workflow is to alert the device owner when the current draw of the device goes over an acceptable value. Who the owner of a device is and how to contact them is stored in DynamoDB, which is where our Lambda node will come into play. So first, we start out with a trigger:
Here we are triggering this workflow using a device trigger. This workflow will run any time a device with the deviceSerial
tag set reports to Losant (that way we know we have something to do the lookup by). Next, the alert conditions:
Here we are using a latch node to define the alert condition. In this case, if current draw exceeds 20, this is an alertable condition (and the alert condition won't fire again until current draw goes back below 19). So now that we have a device, and we have an alertable condition, it is time to actually look up the owner to alert. Enter the Lambda node:
The first step when using a Lambda node is configuring AWS itself: this means the access key/secret as well as what AWS region. We recommend using workflow globals as the place to store things like access keys and secrets, which is why those two fields above are templated using global variables. Below you can see what the Globals tab of this workflow looks like:
As a note, when using any AWS API, you should always use an AWS key pair with the most restrictive permissions possible for safety's sake. Using Losant is no different; in the case above, the AWS key and secret should be a pair that only has permission to execute Lambda functions and nothing else.
Above is the rest of the configuration for the Lambda node. We are going to run the Lambda function named userLookup
, hand it the current deviceTags
(an array on the root of the workflow payload) as the function argument, and store the result at the JSON path data.userResult
. A sample workflow payload as the workflow enters this node might look like the following:
{
"time": Fri Feb 19 2016 17:26:00 GMT-0500 (EST),
"data": { "current": 25, "on": true, },
"globals": { "awsKey": "KEY_HERE", "awsSecret": "SECRET" },
"applicationId": "568beedeb436ab01007be53d",
"triggerId": "568bf74ab436ab01007be53e",
"triggerType": "deviceId",
"deviceTags": [ { "key": "deviceSerial", "value": "64QujxoyzCsuqv" } ],
"flowId": "56c74add04d0b50100043381"
}
So by handing the Lambda function the deviceTags
, the function will have the deviceSerial
value, which we will need to actually look up the user.
And at this point you might be wondering what this magical function userLookup
even does ... but wonder no more! Below is the code for the Lambda function:
The goal of the code above is to take a device serial number and find the owner. We have two DynamoDB tables that we care about here: one called deviceMapping
, which has rows that look like the following:
{"deviceSerial":"dqzBv2yB6sMkQQ", "userId":"568beedeb436ab01007be53d"}
And one called users, which has rows that look like this:
{
"email": "janedoe@example.com",
"firstName": "Jane",
"id": "568beedeb436ab01007be53d",
"lastName": "Doe",
"phoneNumber": "867-5309"
}
So first, in the Lambda function node, we extract the device serial number from the device tags. Once we have that, we attempt to look up the associated user in the deviceMapping
table. If we find a row there, we now have a user ID, which we can use to look up the user in the users table. And once we have that user, we call the callback function with that user. If at any point we didn't find something, we call the callback with null
.
Now something is getting returned from the Lambda function; so how does that appear back on the workflow side of things? Well, it looks something like the following:
{
"time": Fri Feb 19 2016 17:26:00 GMT-0500 (EST),
"data": {
"current": 25,
"on": true,
"userResult": {
"StatusCode": 200,
"LogResult": "START RequestId: ...lambda logs here...",
"Payload": {
"id": "56c74add04d0b50100043381",
"phoneNumber": "8128675309",
"email": "john@example.com",
"lastName": "Smith",
"firstName": "John"
}
}
},
"globals": { "awsKey": "KEY_HERE", "awsSecret": "SECRET" },
"applicationId": "568beedeb436ab01007be53d",
"triggerId": "568bf74ab436ab01007be53e",
"triggerType": "deviceId",
"deviceTags": [ { "key": "deviceSerial", "value": "64QujxoyzCsuqv" } ],
"flowId": "56c74add04d0b50100043381"
}
We now have a userResult
object on the payload with all the information needed to actually contact the user. Lambda also returns some other information to us, such as a status code and logging information, but for our purposes today, if we got a user back, we are good to go.
That check for a user is exactly what we are doing in this next node, a simple conditional. If you remember, there are multiple possible cases where the Lambda function might return null
(i.e., no user found). We need to make sure we deal with that case here by making sure there exists a userResult
object and a userResult.Payload
object. If so, then we can continue onward with actually notifying the owner:
First, lets send that user an email. It is a simple thing now to pull the user's email address out of the user result that we got back from the Lambda function call. But an email isn't enough; we can alert them even more:
A text message alert is just as simple as the email alert, since the phone number is also right there on the user object we got back from the userLookup
Lambda function call.
And that is everything! Using a Lambda node/function, we can easily integrate data outside of Losant with data flowing through Losant. While the particular example here might not cover your exact needs, there are a number of core ideas here that are common to many IoT applications, and hopefully you will be able to expand and build on these concepts to create a solution that fits your problem.
If you have any questions or comments, feel free to leave them below. Also, if anyone takes these ideas and runs with them to make something, please let us know! We love to hear how people are using Losant to do their own things.