Full credit for this idea and post goes to my colleague Andy, who presented this approach at a recent lunch-and-learn at work. A few months after his presentation, I decided to try replicating what he did. This blog post represents what I learned from that effort. Thanks Andy!
For the 3 phones in our house, when one is misplaced, press an Amazon IoT button and have it dial the phone. A single click should call one phone number, double click another, and long click the third number. Then, the phone would ring, and I would listen for it, and I would find the phone, and that’d be swell.
This was most assuredly not an effort to create something terribly useful. My actual goal was just to learn more about Lambda and IoT, with some practical (ish) real-world utility.
In this post, I will not go into excruciating detail on every single step. I’ll get into the weeds where I had particular trouble or learned something useful. This is not an Introduction to Lambda post. It also assumes some familiarity with AWS and the AWS console.
First, an Amazon IoT button. The button supports single, double, and long click, and so ultimately I want my function to respond differently depending on the type of click. These things are ridiculously priced at $20 and get you about 2000 clicks before the battery dies.
Second, AWS Lambda. I used Python for the code (shown below). I also used nficano/python-lambda to deploy the code, although I definitely recommend learning how to use lambda without any such deploy library first.
Third, a Twilio account and phone number. You get $15 credit for a trial account, but I decided just to buy a number ($1/month). Total cost is $1 / month for the number + 1.3 cents per call. Once you create an account, you’ll get a phone number and “sid” and “token” values to use in the code.
Fourth, twimlets. In short, using a twimlet URL within the lambda function was the easiest way I could find to have the phone call say something (in a terrible robo-voice) when you pick up the phone. More on this later.
Tying it all together: Click Button –> Triggers lambda function –> uses Twilio API to dial a phone number –> that API call uses a twimlet URL that says something to the person who answers the phone call
Using the Twilio API
Before jumping in to the Lambda bits, let’s look at the meat of the code, which is using the Twilio API to make a phone call. You can do that with a single command.
pip install the Twilio library and run that in a python shell (set the variables, obviously). In a few seconds, you should get a phone call with a greeting and then some hold music.
The code, draft 1
I mentioned above that I’m using the nficano/python-lambda library to make developing lambda functions a bit simpler. I also mentioned that when you’re just getting started with Lambda — doing your Hello World version — I strongly recommend learning how to do it with the plain old Lambda console before bringing in any tooling.
But once you get that under your belt, a library such as python-lambda really helps.
All of this code lives at https://github.com/marcesher/twilio-lambda, and I’ll walk through it here, modifying it as I go.
First, I created a new local directory, created a virtualenv, and created requirements.txt file with these contents:
After activating the virtualenv, I installed those libraries into it:
pip install -r requirements.txt
Second, I ran
which generated several files, including skeletons for service.py and some config files.
Third, I knew I’d be configuring environment variables for twilio sid and token, the “from” phone number, and the various “to” phone numbers.
I keep the environment variables in a “.env” file:
lambda init also created an “event.json” file and added some dummy variables. That file will eventually become important for local testing when we want to simulate double and long button clicks on the button. But I’m going to hold off on that for now.
I then wiped out the stuff in the generated “service.py” and replaced it with this:
Then, using the python-lambda library, and ensuring that the service.py has the code above, I ran it like so:
If all goes well, then the number you’ve configured to get called should get called by your Twilio number. If you’re using a trial account, you’ll first hear a kindly gentleman’s voice telling you you’re using a trial account, and then when you press any key on your phone, you’ll hear some hold music. Later in this post, we’ll change that hold music to something more useful.
All this lambda-invoke thing is just a nice way of running your python function and having it simulate the event object using your event.json file. It’s quite similar to how you’d use the “Test” button in the AWS Lambda console, where you also can create a JSON event object for testing.
Changing the phone call
First, that deceptively simple, single Twilio API call was, for me, infuriating to get working as I wanted. The URL it uses in the sample code is to some hold music, and I wanted to change it to actually say something, which according to the docs means pointing to a URL that returns “Twiml” — Twilio XML — which tells twilio how to behave. Here’s a sample:
So I figured I’d just host a static XML file on github or S3. But when I tried those URLs in the client call, it failed, with an error indicating that the URL didn’t accept a POST request (GH and S3 only support GET). I got to the point where I thought I might actually have to set up a web server just to serve the damn XML file. But before I did that, I took one more look at the sample code that Twilio provides and that’s when I noticed that Twimlets thing:
So then I went and checked out Twimlets.com, and lo and behold, there are all manner of handy helpers. I read the one for “Simple Message”, and then used its “Twimlet Generator” UI to create a properly encoded URL that I could then use in my service. Here’s a sample that just says “Hello from twilio” when you pick up the phone
Just with that simple message twimlet, you can configure it to say any number of different things, or play any number of mp3s. It’s slick.
Getting it working in Lambda
The AWS Lambda function
With the function working locally — dialing a number with twilio and saying something when I picked up — it was time to start configuring the Lambda function. This is going to be a multi-step affair:
- create the function (and IAM role if it doesn’t exist)
- configure the environment variables
- deploy the code
- testing in the Lambda console
Wiring up the button, and modifying the service to respond to different types of clicks, will come last.
I used the AWS console to create a new Lambda function named “hello_twilio”. I configured it thusly in the console:
I want to talk about the “Role” stuff. When I first started with Lambda, I got hung up a bit there. Ultimately, it’s just adding an IAM role with appropriate permissions. Here’s the role I created, and which you see in that screenshot above, called “lambda_execution_role”. At a minimum, your role will require CloudWatch and Lambda execution privileges. I threw in a few more for good measure.
Configuring environment variables
On the “Code” tab of the Lambda function, I added environment variables for the twilio sid and token, and from and to numbers. They’re named identically to the variables shown in the .env file, above.
Deploying the function
Because this Lambda function has dependencies — in this case, the Twilio library — you’ll need to bundle your function up as a zip file and upload it to Lambda. The AWS documentation explains how to do this manually. But if you’re using the nficano/python-lambda library, you can use it to do that for you.
First, you’ll want to be sure that the config.yml file created via lambda init has the right values. In my case, those are the values that appear above in the console. Note that “service.handler” means “the handler function in the service.py file”. This will now overwrite anything you previously configured, so make sure this file has the correct values:
Then, it’s simply a matter of:
Testing in the Lambda console
With the code working locally, I wanted to then test it in the Lambda console. After ensuring that environment variables were set, I clicked the “Test” button and then inspected the CloudWatch log output below. Any invocations of your Lambda function — from the Test button, or from the real IoT button, will stuff the output into CloudWatch. This will become really important in a minute, when I discuss testing the function with the button and trying to change behavior based on the type of button click.
Wiring up the IoT button
The button comes with a tiny manual that walks you through how to activate the button, secure it, and so forth. My experience was that it was quite straightforward and only took a few minutes. Once the button is configured, you’ll be able to add it as a trigger to the Lambda function.
You wire up the IoT button in the Lambda console “Triggers” tab. Click “Add Trigger”, then “AWS IoT”. This will walk you through a wizard where it’ll create some files that you then have to add to your Button.
Once the button is all wired up, you’re ready to press!
You can then view the output in CloudWatch. Additionally, assuming that the code successfully invokes the Twilio API, you can also view the Twilio logs from your Twilio dashboard. This ended up being really helpful for me when I was trying to get that URL working as described above.
Changing behavior based on click type
Once I had the function successfully invoked with the button, I wanted to add one last thing: change the behavior of the function so that it would use a different number for a double and a long click.
This was way harder to figure out than I expected, even though the resultant answer is dead simple.
I simply could not figure out from reading documentation how to have the lambda function respond differently to double and long clicks. I knew they were supported, but no manner of googling “AWS Lambda IoT Double Click” or other such things led me to relevant docs. I went on an “IoT Rules” goose chase for a while, to no avail. Then, on a whim, I decided to just add a print statement of the event object to see if anything was in there.
After adding the print statement, re-deploying, clicking the button, and looking at the logs in CloudWatch, I noticed that there was a “clickType” key in the event object with a value of “SINGLE”. Naturally, I tried double-clicking, then long clicking, and saw in the logs the values of “DOUBLE” and “LONG”. So it ended up being really simple to respond to different click types, but it took a fair bit of time and just dumb luck to figure out how to do it.
And although I admittedly haven’t searched much since getting it working, I still haven’t found the documentation that spells out the clickType being added to the event object by the button.
The final code ended up looking like this:
Supporting clickType for local invocation and Lambda console testing
Once I figured out that different button click was simply a matter of creating a “clickType” key in the event object, it was fairly straightforward to mimic that locally and within the Lambda console.
For local development: that
lambda init way up above had created an “event.json” file. I opened that up and replace the contents, like so:
lambda invoke will automatically inject that as an entry in the event object.
For testing in the Lambda console, you need to configure the Test Event object. This is under the “Actions” dropdown menu.
Then, when you click the “Test” button in the console, it’ll use that as the event object.
Again, huge props to Andy for inspiring this little Lambda / Twilio journey. It was a lot of fun.
Finally, I know this post hand-waved over a bunch of stuff (“to wire up the button… wire up the button” 🙂 ). But if you have questions about any of the stuff I’ve left out, please do ask.
This started out as mostly just a silly-ish way to learn more about Lambda and IoT. But it certainly impressed the kids, and they get a kick out of pressing the button to annoy us. It’s like Leave it to Beaver: clean, wholesome fun the whole family can enjoy 😉