Finally! I am surprised it didn’t even take as long as it usually does for me to figure shit out. Took a few weeks. As usual, I did not come up with the solution on my own but found it out on the web and slapped it all together. I wanted a custom Alexa Node-Red skill, to be able to take a command given to Alexa and have it read back data from one of my sensors. Things like temperature sensors, water level, etc. I wanted to be able to ask Alexa what the values are. What I got: exactly what I wanted. It all works. There is two parts to this: the Node-Red flow and the Alexa skill.
First off, to get any of this working you must have your Node-Red server accessible from the outside world. That means port-forwarding, DNS, domains, SSL, all that. It’s fun getting it all working. Not. Just like my previous post, I happened to have it already setup. Once your Node-Red install is available from the web you are good to go. Now you don’t need the entire NR setup opened up either. I just allowed a few NR served pages to be available. Not the entire NR itself.
Update: I made a new post about Node-Red behind a reverse-proxy/SSL
Let’s Begin
It starts with a regular HTTP node to a switch node. That switch node splits up Alexa’s requests to NR; LaunchRequest, IntentRequest, SessionEndedRequest. LaunchRequest gets invoked when the skill starts. You could have Alexa say “Hello what do you want?” for example. IntentRequest is the goods. Then theres SessionEndedRequest, I’m assuming this gets called at the end. Haven’t toyed with it. Then you pass those requests off to do other stuff, like the DoCommand where it grabs your intent. Then a function node to extract the commands, which gets passed off to another switch node to split up the possible commands you can give Alexa. Give her as many commands as you want, then there is a “device doesn’t exist” at the bottom. This is used if she didn’t hear you right or the device doesn’t exist. All that data gets passed to a template to format what Alexa will say and sticks the data in JSON. Bam! That wasn’t so hard right?
Here’s the whole flow (all standard nodes used):
1 |
[{"id":"84f88c01.3a741","type":"http in","z":"74af25a6.5c836c","name":"/alexa","url":"/alexa","method":"post","swaggerDoc":"","x":90,"y":1200,"wires":[["390e7e54.f5d7f2"]]},{"id":"390e7e54.f5d7f2","type":"switch","z":"74af25a6.5c836c","name":"Request Type","property":"payload.request.type","propertyType":"msg","rules":[{"t":"eq","v":"LaunchRequest","vt":"str"},{"t":"eq","v":"IntentRequest","vt":"str"},{"t":"eq","v":"SessionEndedRequest","vt":"str"}],"checkall":"true","outputs":3,"x":260,"y":1200,"wires":[[],["79e0966.d586568"],[]]},{"id":"79e0966.d586568","type":"switch","z":"74af25a6.5c836c","name":"Intents","property":"payload.request.intent.name","propertyType":"msg","rules":[{"t":"eq","v":"DoCommand","vt":"str"}],"checkall":"true","outputs":1,"x":450,"y":1200,"wires":[["6a5f2c6c.2f5fe4"]]},{"id":"6a5f2c6c.2f5fe4","type":"function","z":"74af25a6.5c836c","name":"Extract commands","func":"msg.payload = msg.payload.request.intent.slots.command.value;\nreturn msg;","outputs":1,"noerr":0,"x":650,"y":1200,"wires":[["74def98d.31a598","9265c084.10d21"]]},{"id":"74def98d.31a598","type":"debug","z":"74af25a6.5c836c","name":"spoken text","active":true,"console":"false","complete":"payload","x":670,"y":1240,"wires":[]},{"id":"9265c084.10d21","type":"switch","z":"74af25a6.5c836c","name":"request","property":"payload","propertyType":"msg","rules":[{"t":"eq","v":"computer temperature","vt":"str"},{"t":"eq","v":"garage temperature","vt":"str"},{"t":"eq","v":"inside temperature","vt":"str"},{"t":"eq","v":"i outside temperature","vt":"str"},{"t":"nnull"}],"checkall":"true","outputs":5,"x":880,"y":1200,"wires":[["1db561b.015c99e"],["dc21a191.9eeca"],["a3cf81a0.7f198"],["f9b89dbe.3fe29"],["46a05746.410fe8"]]},{"id":"1db561b.015c99e","type":"function","z":"74af25a6.5c836c","name":"Server Temperature","func":"msg.current = {\n server: context.global.servertemperature\n}\n\nreturn msg;","outputs":1,"noerr":0,"x":1070,"y":1140,"wires":[["80a3b45b.18ac88"]]},{"id":"dc21a191.9eeca","type":"function","z":"74af25a6.5c836c","name":"Garage Temperature","func":"msg.current = {\n garage: context.global.garagetemperature\n}\n\nreturn msg;","outputs":1,"noerr":0,"x":1080,"y":1180,"wires":[["973f5f3f.4274a"]]},{"id":"a3cf81a0.7f198","type":"function","z":"74af25a6.5c836c","name":"Inside Temperature","func":"msg.current = {\n inside: context.global.insidetemperature\n}\n\nreturn msg;","outputs":1,"noerr":0,"x":1070,"y":1220,"wires":[["315e3b6.ff8bdc4"]]},{"id":"f9b89dbe.3fe29","type":"function","z":"74af25a6.5c836c","name":"Outside Temperature","func":"msg.current = {\n outside: context.global.outsidetemperature\n}\n\nreturn msg;","outputs":1,"noerr":0,"x":1080,"y":1260,"wires":[["2888b889.6999c8"]]},{"id":"46a05746.410fe8","type":"function","z":"74af25a6.5c836c","name":"Unrecognized","func":"\nreturn msg;","outputs":1,"noerr":0,"x":1060,"y":1300,"wires":[["ade58298.2cf17"]]},{"id":"80a3b45b.18ac88","type":"template","z":"74af25a6.5c836c","name":"Format Alexa's Response","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"{\n \"version\": \"1.0\",\n \"response\": {\n \"outputSpeech\": {\n \"type\": \"PlainText\",\n \"text\": \"The server closet is currently {{current.server}} degrees\"\n },\n \"shouldEndSession\": true\n }\n}","x":1350,"y":1140,"wires":[["e636973f.8e1428"]]},{"id":"973f5f3f.4274a","type":"template","z":"74af25a6.5c836c","name":"Format Alexa's Response","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"{\n \"version\": \"1.0\",\n \"response\": {\n \"outputSpeech\": {\n \"type\": \"PlainText\",\n \"text\": \"The garage is currently {{current.garage}} degrees\"\n },\n \"shouldEndSession\": true\n }\n}","x":1350,"y":1180,"wires":[["e636973f.8e1428"]]},{"id":"315e3b6.ff8bdc4","type":"template","z":"74af25a6.5c836c","name":"Format Alexa's Response","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"{\n \"version\": \"1.0\",\n \"response\": {\n \"outputSpeech\": {\n \"type\": \"PlainText\",\n \"text\": \"The inside of the house is currently {{current.inside}} degrees\"\n },\n \"shouldEndSession\": true\n }\n}","x":1350,"y":1220,"wires":[["e636973f.8e1428"]]},{"id":"2888b889.6999c8","type":"template","z":"74af25a6.5c836c","name":"Format Alexa's Response","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"{\n \"version\": \"1.0\",\n \"response\": {\n \"outputSpeech\": {\n \"type\": \"PlainText\",\n \"text\": \"It is currently {{current.outside}} degrees outside\"\n },\n \"shouldEndSession\": true\n }\n}","x":1350,"y":1260,"wires":[["e636973f.8e1428"]]},{"id":"ade58298.2cf17","type":"template","z":"74af25a6.5c836c","name":"Format response","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"{\n \"version\": \"1.0\",\n \"response\": {\n \"outputSpeech\": {\n \"type\": \"PlainText\",\n \"text\": \"I'm sorry, there is no device named {{payload}}\"\n },\n \"shouldEndSession\": true\n }\n}","x":1330,"y":1300,"wires":[["e636973f.8e1428"]]},{"id":"e636973f.8e1428","type":"json","z":"74af25a6.5c836c","name":"","x":1570,"y":1220,"wires":[["348a8cc4.cc99a4"]]},{"id":"348a8cc4.cc99a4","type":"http response","z":"74af25a6.5c836c","name":"","x":1710,"y":1220,"wires":[]}] |
That’s the Node-Red half. You are not done yet. On to the Alexa skills half. This part is easy don’t worry. Login to your Amazon Dashboard and click Alexa. Choose “Get Started” with the Alexa Skills Kit, click add a new skill. Under Skill Information give it a name and choose the invocation word, what you will say to Alexa to start your skill. I chose “Node Red”, so I have to say “Alexa, ask Node Red….”. These can be changed at any time it seems. You won’t be publishing this skill, it stays beta for only you to use. For the Global Fields section, no you will not be using an audio player. Well, maybe you will but I didn’t, and it will probably change things for you.
Note about the flow: The NR flow works (for me) just fine however I noticed it throws an error in the debug tab whenever a command is called. If it is an unrecognized command response it doesn’t throw the error though. It complains about headers already being sent. I will update the flow if I find a fix for it.
Interaction Model
Intent Schema
This is the part of the Alexa skill where you tell it what to do. It is pretty straight forward. Just copy this to your “Intent Schema”. There are no custom slot types and no values to enter.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
{ "intents": [ { "slots": [ { "name": "command", "type": "LITERAL" } ], "intent": "DoCommand" } ] } |
Sample Utterances
This is where you list the invocation phrases that will activate Alexa. Normally (and in other online tutorials for Alexa skills) this is where you add a ton of different phrases. But we are not. Node-Red is going to handle that side for us. This box just gets one line of text.
1 |
DoCommand {script parameters|command} |
Configuration
Global Fields/Endpoint
For a service endpoint you are going to pick “HTTPS”. In a lot of other tutorials you will usually choose AWS Lambda but we are doing all of our own heavy lifting with NR. We don’t need no stinking Lambda. Choose your closest location and enter the URL that your Node-Red is accessible from (via the web remember). Say no to account linking and you can also leave Permissions alone.
SSL Certificate
Certificate for Endpoint
Choose the option that bests describes you. Most likely it will be the first option. For me I am using a subdomain that is already SSL’d with Let’s Encrypt so I choose the second option.
Test
Basically just leave the toggle flipped to enable the skill for you to use. you don’t need to do anything else on this page.
Publishing Information
Nothing to do here, you won’t be publishing this skill. Why? Because it requires too much setup on the users behalf. I don’t think Amazon would approve a half functioning skill that requires advanced user setup to get working. You could always try. Good luck.
Privacy & Compliance
Three no’s and one box to check, I mean as long as it all applies to you right 😉
Done
That should be it. With Node-Red available to the web and the flow implemented the new Alexa skill you just made you should be good to go. I hope you found this useful, I sure wish I had found a blog post like this. Now go test it out with your Amazon Echo/Dot!
At the time of this writing a beta product appeared in the Amazon Dashboard for a the “Skill Builder”, looks to be a new UI for building Alexa skills. If this gets implemented for everyone in the future things may be different than they are described in this blog post.
Revisited
Originally posted April 26, 2017 @ 17:56
I decided to come back to this post. I was adding and modifying some things on my flow and I was using this post as a reference and decided it was not cutting the mustard, it felt unfinished. So here we are.
The above section contains the main flow you will need and it steps you through the Amazon Developer side of things that needs to be setup. Once that is all finished you should have a working flow with Alexa responding accordingly. What I feel I left out was how to configure the flow itself. Many of you may already have figured it out or can see whats going on and thats cool. Heres for the ones that need the help (myself included).
The flow
HTTP node
The first node in the flow is the HTTP request. This is the page you will use to point your reverse proxy to. This is the page that Node-Red will server, that Amazon/Alexa will look for. This page needs to be accessible from outside your network.
Request Type
This contains the types of requests we can send to Alexa. We are only worried about IntentRequest right now. Play with the other later.
Intents & Extract Commands
There is no need to modify these nodes. They contain the code needed to send requests.
Request
Add your verbal question here. Add it how Alexa hears it. This may take a little trial and error depending on how you talk and how she hears you. Sometimes you can simply put exactly what you are going to say in there, like “garage temperature” works fine for me but if I ask Alexa what the outside temperature is she doesn’t know and I get the unrecognized question response. But this is ok, pay attention to the output of the unrecognized responses. It will spit out what Alexa heard and how she heard it. So when I ask what the outside temperature is she hears “i outside temperature” for some unknown reason. So if I modify my request to be the same, and it works.
Function node
This function is very simple. It just grabs the global variable name you put in it. So when I ask for the “garage temperature” it looks for the global I have specified, in this case contex.global.garagetemperature. Add whatever global you want. Just make sure it is initialized first (has some data to report).
Formatting Alexas response
This template node contains the information for Alexas verbal reply. Once the basic structure is there all you do is edit the “text” to the response you want to receive, in plain english. She will respond with “exactly” what you type in there.
JSON & HTTP Response
Move along, noting to see here. The data gets formatted as JSON and the HTTP node completes the whole flow.
Done.
There you go. I feel better now. I at least explained WTF is going on here rather than dumping the flow on you and walking away. Sorry about that. Now you can run off and play with Alexa and Node-Red to your hearts content. The only thing I have noticed with the whole flow is that after Alexa responds Node-Red throws an error in the debug log. It all works 100% and works well but it always sends this error, and I haven’t figured it out. But I also haven’t been looking for it. Just a little FYI.
"Error: Can't set headers after they are sent."
This is where I found the goodness, buried deep in comments on a (awesome) blog.