I’ve been playing with Node-Red for a few months now.  It’s a visual programming tool based on Nodejs, with an IOT focus.  I’ve found Nodejs to be an eye-opener with regard to OSS – most nodejs contributions are MIT or Apache licensed, which makes them usable in commercial as well as non commercial software; and it seems this model has inspired vast numbers of contributors, bringing OSS out of the ‘hobbyist’ arena and firmly into the mainstream.

Well, since Node-Red had an IOT focus, I asked for a PI3, a couple of sonoff wifi switches, and an Amazon Echo (Alexa) for xmas…  with the intent of having some voice controlled aspects to my home.

Inspired by Peter’s Site, I first went with driving wemo protocol from Echo.  In Node-Red, this is as easy as installing the wemo emulator node-red node.  What this does is responds to multicast upnp requests made by echo, pretending to be a wemo device.  Thus I could then ask Alexa to ‘turn on’ and ‘turn off’ my device(s) (at least; send this info to node-red).  (Actually, the first thing I did was have this trigger my Pi to speak ‘Alexa, turn on trevor’, and other phrases, running a feedback loop!!).

Once I got my ‘real’ sonoff devices, the first thing to do was open them up, fit a 5 way header, and re-program them with this firmware.  The sonoff devices are ESP-8266 based, a cpu that I have a number of now, and fairly easy to re-program via a serial interface (just make sure you have enough 3.3v whilst programming – my usb serial adaptor’s 3.3v output was not string enough).  The firmware from arendst provides lots of features that I would like in my wifi controlled appliances – OTA upgrade, wemo emulation built in, web interface and configuration – it just makes it so easy, and even on my lappy with no esp-8266 dev software on it, it was only 1/2 hour from start to finish to reprogram the device, following the wiki in the github link. (Update: now moved to Sonoff-Tasmota, the latest version of Theo Arends’s firmware).

At this point, I had a light in my living room called ‘Trevor’, controlled by saying ‘Alexa, turn trevor on’ or ‘Alexa, turn trevor off’.  But Alexa still did not know if trevor was on or off, and could tell me nothing about the state of anything in my house.

So, for the next step, I wanted to get more in-depth with Alexa; create a custom ‘Skill’. Peter described having done something similar in a couple of posts e.g. here

So I signed up for Alexa Dev on Amazon using our standard Amazon account (the one we bought the Echo with), and started to investigate….  Following Peter’s blogs, it seems Alexa will only talk to an HTTPS server, and I already have an HTTPS equipped webserver running from home, so could not easily additionally expose a dedicated one for Alexa to talk to.  Having dabbled with a bit of PHP in recent months, a quick google search gave me this PHP script:

<?php
$url = "http://192.168.1.101:1880/echo";
$body = file_get_contents('php://input');
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POST, 1);
 curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
 curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
 curl_setopt($ch, CURLOPT_HEADER, 0);
 curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
 curl_setopt($ch, CURLOPT_HTTPHEADER, array(
 'Content-Type: application/json',
 'Content-Length: ' . strlen($body))
 );
 $response = curl_exec($ch);
if (!isset($response))
 return null;
header('Content-Type: application/json');
echo $response;
?>

Which basically receives POST requests from Amazon Alexa, passes them on to node-red (on my LAN), and then passes the response back to Alexa.  Yeah – it’s not so sophisticated, and it should check that the request came from amazon, and that it’s a request from MY echo, maybe tomorrow…

UPDATE: The above code has been changed to add ‘application/json’ content headers – works more easily with node-red.

Also of note is that Alexa skills can now work with a self-signed certificate as well as an official HTTPS cert.

So, popped the above onto the webserver, created a skill in Amazon, told amazon where to find my server, blatantly stole Peter’s ‘intent’ schema (with one change – swap ‘LITERAL’ for a custom ‘slot type’ called ‘THING’:

{
 "intents": [
 {
 "intent":"inputIntent",
 "slots": 
 [
 {"name": "wa", "type": "THING"},
 {"name": "wb", "type": "THING"},
 {"name": "wc", "type": "THING"},
 {"name": "wd", "type": "THING"},
 {"name": "we", "type": "THING"},
 {"name": "wf", "type": "THING"},
 {"name": "wg", "type": "THING"},
 {"name": "wh", "type": "THING"},
 {"name": "wi", "type": "THING"},
 {"name": "wj", "type": "THING"},
 {"name": "wk", "type": "THING"},
 {"name": "wl", "type": "THING"},
 {"name": "wm", "type": "THING"},
 {"name": "wn", "type": "THING"},
 {"name": "wo", "type": "THING"}
 ]
 } 
 ]
}

Create the custom slot type called THING containing a few choice words you may wish Alexa to prefer.

alexaslot

And enter some sample utterance (I entered ‘inputIntent  {wa} {wb} {wc} {wd}’).

Once this is done, you can test your endpoint; but before you get any valid response, you would need something in node-red servicing the /echo endpoint!.

My flow is:

alexaflow

[{"id":"a5f00c80.1dc33","type":"http in","z":"69f54fb.6c12cb","name":"","url":"/echo","method":"post","swaggerDoc":"","x":125.82987976074219,"y":402.7812805175781,"wires":[["d7424a09.6217a8"]]},{"id":"72ecee69.c9204","type":"http response","z":"69f54fb.6c12cb","name":"","x":919.8298797607422,"y":363.9444885253906,"wires":[]},{"id":"25e8fd3.c574f02","type":"function","z":"69f54fb.6c12cb","name":"gather words","func":"var doStuff = {payload: msg.payload.length};\n\n\nswitch (msg.payload.request.type)\n    {\n    case \"IntentRequest\":\n    if (msg.payload.request.intent.name   === \"inputIntent\")\n        {\n        var word = [];\n        \n        word[0] = msg.payload.request.intent.slots.wa.value;\n        word[1] = msg.payload.request.intent.slots.wb.value;\n        word[2] = msg.payload.request.intent.slots.wc.value;\n        word[3] = msg.payload.request.intent.slots.wd.value;\n        word[4] = msg.payload.request.intent.slots.we.value;\n        word[5] = msg.payload.request.intent.slots.wf.value;\n        word[6] = msg.payload.request.intent.slots.wg.value;\n        word[7] = msg.payload.request.intent.slots.wh.value;\n        word[8] = msg.payload.request.intent.slots.wi.value;\n        word[9] = msg.payload.request.intent.slots.wj.value;\n        word[10] = msg.payload.request.intent.slots.wk.value;\n        word[11] = msg.payload.request.intent.slots.wl.value;\n        word[12] = msg.payload.request.intent.slots.wm.value;\n        word[13] = msg.payload.request.intent.slots.wn.value;\n        word[14] = msg.payload.request.intent.slots.wo.value;\n         \n        var thisone =0, processed = 0, total = word.length;\n         \n        for (;;)\n            {\n            var nxt = \"\";\n         \n            switch (word[thisone])\n                {\n                case \"cancel\" :\n                        msg.payload = \"\";\n                        node.warn(\"heard cancel\");\n                        return [null, msg];\n                        break;\n                case undefined:\n                case \"the\":    \n                case \"to\":\n                case \"thanks\":\n                case \"thank\":\n                case \"and\":\n                case \"turn\":\n                case \"a\":\n                case \"please\":\n                case \"you\":\n                case \"er\":\n                case \"erm\":\n                node.warn(\"dropped \" + word[thisone]);\n                word.splice(thisone,1);\n                break;\n                 \n                default:\n                ++thisone;\n                break;\n                }\n                 \n            if (++processed >= total)\n                break;\n            }\n             \n        msg.topic = \"\";\n        msg.payload = \"OK\";\n        doStuff.word = word;\n        msg.word = word;\n        msg.combined=\"\";\n        for (a = 0; a < word.length; a++)\n            {\n            msg.combined += word[a] + \" \";    \n            }\n        }\n    node.warn(\"heard \" + msg.combined);\n    return [msg, null];\n \n    case \"LaunchRequest\":\n    msg.payload = \"You need help\";\n    return [null, msg];\n     \n    case \"SessionEndedRequest\":\n    msg.payload = \"Session Ended\";\n    return [null, msg];\n     \n         \n    default:    \n    msg.payload = \"Unrecognised Intent\";\n    return [null, msg];\n    }","outputs":"2","noerr":0,"x":417.8368377685547,"y":499.361083984375,"wires":[["33eb433d.8504ec"],["b1e710d6.a5a34"]]},{"id":"d7424a09.6217a8","type":"function","z":"69f54fb.6c12cb","name":"Get proxied request","func":"var keys = Object.keys(msg.payload);\n\nvar req = JSON.parse(keys[0]);\n\nmsg.payload = {};\nmsg.payload = req;\n\nreturn msg;","outputs":1,"noerr":0,"x":196.826416015625,"y":499.2396240234375,"wires":[["25e8fd3.c574f02"]]},{"id":"b1e710d6.a5a34","type":"function","z":"69f54fb.6c12cb","name":"form response","func":"var resp = \n{\n  \"version\": \"1.0\",\n  \"sessionAttributes\": {\n    \"supportedHoriscopePeriods\": {\n      \"daily\": true,\n      \"weekly\": false,\n      \"monthly\": false\n    }\n  },\n  \"response\": {\n    \"outputSpeech\": {\n      \"type\": \"PlainText\",\n      \"text\": msg.payload\n    },\n    \"card\": {\n      \"type\": \"Simple\",\n      \"title\": \"My Pad\",\n      \"content\": msg.payload\n    },\n//    \"reprompt\": {\n//      \"outputSpeech\": {\n//        \"type\": \"PlainText\",\n//        \"text\": \"Can I help you with anything else?\"\n//      }\n//    },\n    \"shouldEndSession\": true\n  }\n}\n\nmsg.payload = resp;\n\nreturn msg;","outputs":1,"noerr":0,"x":765.8298797607422,"y":364.2430725097656,"wires":[["72ecee69.c9204"]]},{"id":"33eb433d.8504ec","type":"function","z":"69f54fb.6c12cb","name":"news","func":"msg.payload = \"I heard \" + msg.combined;\n\nif (msg.combined.indexOf('news') > -1){\n    msg.payload = \"This is some news\";\n    return [null, msg];\n}\n\nreturn [msg, null];","outputs":"2","noerr":0,"x":481.8298797607422,"y":448.2395935058594,"wires":[["73e775fe.1bf7cc"],["b1e710d6.a5a34"]]},{"id":"73e775fe.1bf7cc","type":"function","z":"69f54fb.6c12cb","name":"dog","func":"msg.payload = \"I heard \" + msg.combined;\n\nif (msg.combined.indexOf('dog') > -1){\n    msg.payload = \"I don't see a dog?\";\n    return [null, msg];\n}\n\nreturn [msg, null];","outputs":"2","noerr":0,"x":481.89573669433594,"y":411.8888854980469,"wires":[["6619dc.053b1624"],["b1e710d6.a5a34"]]},{"id":"26bb1453.d7e83c","type":"function","z":"69f54fb.6c12cb","name":"report unknown","func":"msg.payload = \"I did not understand \" + msg.combined;\n\nreturn msg;","outputs":1,"noerr":0,"x":508.8298797607422,"y":307.2361145019531,"wires":[["b1e710d6.a5a34"]]},{"id":"6619dc.053b1624","type":"function","z":"69f54fb.6c12cb","name":"devices states","func":"msg.payload = \"I heard \" + msg.combined;\n\nif (msg.combined.indexOf('devices') > -1){\n    \n    msg.payload = \"Device states are \";\n    \n    var state = global.get('state');\n    var devs = Object.keys(state);\n    \n    for (var i = 0; i < devs.length; i++){\n        msg.payload = msg.payload + devs[i];\n        msg.payload = msg.payload + \" is \";\n        msg.payload = msg.payload + state[devs[i]];\n        msg.payload = msg.payload + \", \";\n    }\n    \n    return [null, msg];\n}\n\nreturn [msg, null];","outputs":"2","noerr":0,"x":510.8957977294922,"y":372.8888854980469,"wires":[["26bb1453.d7e83c"],["b1e710d6.a5a34"]]}]

Basically, the flow services an http endpoint.  Alexa provides some words, Peter’s code extracts and combines these into a ‘sentence’, then I added a separate function node for each thing i was looking for, e.g. the word ‘news’, ‘dog’, or ‘devices’.  If ‘news’ or ‘dog’ is heard, then a static response if given.  If the word ‘devices’ is heard, then node-red produces a dynamic response containing the values in the global object ‘state’ (which I populate from listening to MQTT status from the sonoff switches!!! – which they send even if controlled by wemo protocol….).

UPDATE: with the ‘application/json’ modification on the php pass-through, the ‘Get proxied request node can be removed.

So, after just a few hours (less time that writing it down…), I have Alexa controlling lights over wifi (via wemo), and able to report the state of these lights via a (simple) custom skill.  The lights are also controllable from a nod-red dashboard switch, the button on the sonoff, or directly over MQTT.  In fact, the only thing which does not work is controlling them directly on the (new) sonoff firmware http api.

 

Â