Getting Alexa Intents in Node-RED

So after fending off with the nodes for a while I decided that I wanted to get whatever doesn’t match the “intent_script” section into Node-RED to handle it manually.

It was a pity to do but I figured out a skeletron that can work, so if anyone is interested here it is for you!

  • I assume that HA, Node-RED, the skill integration and everything else that is listed in Amazon Alexa Custom Skill - Home Assistant is already setup and working, if you’re gonna mess with alexa into Node-RED I expect you already have the basic stuffs done
  • Your Node-RED has to be exposed to internet, by default it works on port 1880, you can use a reverse proxy to keep everything available only through the HTTP/S port, and you should have SSL enabled; If it works with plain HTTP I don’t know, however I know that it would be INSECURE as your token is sent over in clear text if you’re not using HTTPS, and anyone who can see the traffic can get your token, having someone else have your token is bad, very bad, so use HTTPS
  • Again, either use a reverse proxy or set node-red to use HTTPS

So, preamble out, let’s see how I configured it, the first thing I had to do was to bend the AWS Lambda script a little, in it’s current form I have it as:

# -*- coding: utf-8 -*-
import os
import json
import logging
import urllib3

_debug = bool(os.environ.get('DEBUG'))

_logger = logging.getLogger('HomeAssistant-Intents')
_logger.setLevel(logging.DEBUG if _debug else logging.INFO)


def lambda_handler(event, context):
    """Handle incoming Alexa directive."""

    _logger.debug('Event: %s', event)

    base_url = os.environ.get('BASE_URL')
    assert base_url is not None, 'Please set BASE_URL environment variable'
    
    # Get the Node-RED URL from the variable environment
    red_url = os.environ.get('RED_URL')
    assert red_url is not None, 'Please set RED_URL environment variable'

    try:
        token = event.get('session', {}).get('user', {}).get('accessToken')
    except AttributeError:
        token = None

    if token is None and _debug:
        token = os.environ.get('LONG_LIVED_ACCESS_TOKEN')

    assert token, 'Could not get access token'

    verify_ssl = not bool(os.environ.get('NOT_VERIFY_SSL'))

    http = urllib3.PoolManager(
        cert_reqs='CERT_REQUIRED' if verify_ssl else 'CERT_NONE',
        timeout=urllib3.Timeout(connect=2.0, read=10.0)
    )

    response = http.request(
        'POST',
        '{}/api/alexa'.format(base_url),
        headers={
            'Authorization': 'Bearer {}'.format(token),
            'Content-Type': 'application/json',
        },
        body=json.dumps(event).encode('utf-8'),
    )
    if response.status >= 400:
        return {
            'event': {
                'payload': {
                    'type': 'INVALID_AUTHORIZATION_CREDENTIAL'
                    if response.status in (401, 403) else 'INTERNAL_ERROR',
                    'message': response.data.decode("utf-8"),
                }
            }
        }

    # Customization to send the request over to Node-RED
    if json.loads(response.data.decode('utf-8'))['response']['outputSpeech']['text'] == 'This intent is not yet configured within Home Assistant.':
        response = http.request(
            'POST',
            '{}/alexa'.format(red_url),
            headers={
                'Authorization': 'Bearer {}'.format(token),
                'Content-Type': 'application/json',
            },
            body=json.dumps(event).encode('utf-8'),
        )
    
    _logger.debug(response.data)
    
    return json.loads(response.data.decode('utf-8'))

Once you have loaded it, you need to edit the environment variable and add:

RED_URL = https://[your hostname]:1880/endpoint

Finally, head over to node-red and import:

[{"id":"282ecb2d233a4672","type":"http in","z":"65036ae6ac520d40","name":"","url":"/alexa","method":"post","upload":true,"swaggerDoc":"","x":420,"y":1200,"wires":[["cdcd9f1dcc908a62"]]},{"id":"848cd3d6cebb6a16","type":"debug","z":"65036ae6ac520d40","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1090,"y":1200,"wires":[]},{"id":"cdcd9f1dcc908a62","type":"json","z":"65036ae6ac520d40","name":"","property":"payload","action":"obj","pretty":false,"x":630,"y":1200,"wires":[["25e7385c47ab2553"]]},{"id":"25e7385c47ab2553","type":"change","z":"65036ae6ac520d40","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"payload.request.intent","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":800,"y":1200,"wires":[["848cd3d6cebb6a16","2acb0cba7ff38d71"]]},{"id":"6475757cca40a06d","type":"http response","z":"65036ae6ac520d40","name":"","statusCode":"200","headers":{},"x":960,"y":1320,"wires":[]},{"id":"2acb0cba7ff38d71","type":"change","z":"65036ae6ac520d40","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"{\"version\":\"1.0\",\"sessionAttributes\":{},\"response\":{\"shouldEndSession\":true,\"outputSpeech\":{\"type\":\"PlainText\",\"text\":\"Test ok\"}}}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":800,"y":1240,"wires":[["6475757cca40a06d"]]}]

Deploy it

Now, create in your Alexa Skill something that do not match the intent_script, I have made a catch all for my test:

Edit your “HomeAssistantIntentsSkill” → Intents → Add Intent → Name it (I called mine “passthrough”) → Create uttenance “{passthroughParam}”, add, set the slot type as Amazon.Streetname (or whatever fancy your boat) → Save model → Build module

Head back to Node-Red and call your Alexa, I use the translation of “Alexa, ask Home Assistant follow me”

It will respond “Test OK” and you will see the command in the payload coming in:

{
    "name":"passthrough",
    "confirmationStatus":"NONE",
    "slots":
        {
            "passthroughParam":
                {
                    "name":"passthroughParam",
                    "value":"follow me",
                    "confirmationStatus":"NONE",
                    "source":"USER",
                    "slotValue":
                        {
                            "type":"Simple",
                            "value":"follow me"
                        }
                }
    }
}

From here you can do anything, edit the responses dynamically, make it do certain things based on the parameters

A couple of final notes:

  • Your API is currently WIDE OPEN, you will have to handle the authentication yourself in Node-RED, I’m not sure if it’s possible to acquire the currently-valid API key(s) from HA and use them, otherwise injecting from the AWS Lambda function a key and check it from within Node-RED might be in order
  • I’m not sure if there’s a way to make HA itself “deflect” the request instead of responding with “This intent is not yet configured within Home Assistant.”, if it’s possible that would be a far better solution since it would solve the authentication issue i just described above, reduce the attack surface directly since only one port is exposed without the need of using a reverse proxy
  • Any contribution is welcome
  • The modified script, the nodes configuration and the instructions are released in the same way I received them, they are open-source code, if you find it useful and find ways to improve it please share them back to the community as a whole