Setting up amazon alexa to work with home assistant.
What you need
- Identify you home router’s public ip.
- the entity_id of things you can already turn on and off in homeassistant ie. light switches
Note: You don’t need an ssl cert for this approach. Its “probably” not a big deal but you must realize that all your http traffic is out in the open and not encrypted. The likelihood of someone snooping on your traffic is low, but it is a possibility. Were just here to make your stuff work. You can clean it up later. If you have cameras inside your house, take the time to setup an ssl cert and do this over https.
The overall gist of how this works
You talk -> Your Alexa Skill interprets your words and calls the corresponding aws lamda function -> lamda makes a restful service call to your home router ip/port which fowards the request to your internal HomeAssistant ip -> home assistant turns an entity on or off.
Why not just use the home assistant cloud module?
- If i wanted to pay a monthly fee i wouldn’t have made home automation my hobby.
- I really hate monthly fees
What do you give up by not using cloud?
- easy https
- Instead of saying “Alexa, turn off my lights” you will have to say, “Alexa, ask [whatever you want] to turn off my lights”
Lets do this.
- setup an api password and cors in your http config. The ip you need for cors is the internal ip of your router. Request will come from that addr because they are fowarded from your router from the routers external ip/port you configure in the next step.
http:
api_password: YOUR_PASSWORD
cors_allowed_origins:
- http://192.168.1.1
-
In your router administration setup a port forwarding rule to forward an external port (ie 9615) to your home assistant server’s internal address 198.0.1.xxx port 8123
-
Create a skill using amazon alexa developer.
- A skill defines the keywords necessary for alexa to interpret your spoken words into an action.
- Once she knows what to do she will call the necessary function to complete the action.
- The function to complete the action will be an aws lamda function.
Lets create the skill
- go to https://developer.amazon.com/edw/home.html#/
- click on “Alexa”
- Get Started with Alexa Skills Kit
- Name your skill. This doesn’t really matter what you call it. I called it home-assistant-skills-template.
- Create you invocation name. You will have to say this when commanding alexa to do something for home assistant, thus i recommend an invocation name of home assistant.
- Leave everything else on that page as a default and hit next.
- Now we must define our intent schema.
{ "intents": [ { "slots": [ { "name": "Scene", "type": "LIST_OF_SCENES" } ], "intent": "SceneTurnOnIntent" }, { "slots": [ { "name": "Scene", "type": "LIST_OF_SCENES" } ], "intent": "SceneTurnOffIntent" }, { "intent": "AMAZON.HelpIntent" } ] }
- Lets explain this. We want alexa to do two things. Turn things on and turn them off. The schema must contain an intent for each. Each intent will reference a list of scenes(or entities) that we would like to control. We will create that list in the next step. The same list will be used for both intents.
First i want to disect what you will eventually say so you understand each piece.
Alexa ask [home assistant] to [turn off the {master bedroom lights}]
Invocation name. Utterance. Scene
- Create a custom slot type called LIST_OF_SCENES. Add some entries you want to control: master_bedroom_lights, …
- Now lets move to aws lamda to create an endpoint for the skill to call.
-
Create an aws lamda function for your alexa skill that will call your home assistant endpoint.
- Create a function in aws lamda
- You will be given two options Author from scratch or create from blueprint. Create from blueprint
- Find the blueprint called alexa-skills-kit-color-expert and select it. (My example uses the node.js not the python vs)
- Select the blueprint and press configure
- Name it whatever you want. HATest.
- Now select Create Role from Template under “Role:”
- Name it HaRole or whatever you want and under Policy templates i chose Simple Microservice Permissions.
** I believe these roles can provide security separations if you need them for access to keys(KMS). I didn’t touch them besides adding this required field. - Scroll down and hit “Create Function”
- Now you should have an overview of your Alexa Skills kit wired to your Lamda outputting logs to Cloudwatch
- Now lets overwrite the template nodejs code with the below code
- You will need to overwite some values
** hostname: this will be your external router ip
** port: this will be the external router ip’s port you previously configured to be fowarded to internal address
** header value for x-ha-access: needs to be your configured api password you made in step 1.
** Lastly the getEntity function is to map what comes in as the scene/entity to modify to its actual entity_id in home assistant. If your alexa skill scene is called master_bedroom_lights it will come into the lamda function as master bedroom lights without the underscores. That needs to map to the ha entity called switch.master_bedroom_lights_switch
'use strict'; var http = require('http'); /** * This sample demonstrates a simple skill built with the Amazon Alexa Skills Kit. * The Intent Schema, Custom Slots, and Sample Utterances for this skill, as well as * testing instructions are located at http://amzn.to/1LzFrj6 * * For additional samples, visit the Alexa Skills Kit Getting Started guide at * http://amzn.to/1LGWsLG */ // --------------- Helpers that build all of the responses ----------------------- function buildSpeechletResponse(title, output, repromptText, shouldEndSession) { return { outputSpeech: { type: 'PlainText', text: output, }, card: { type: 'Simple', title: `SessionSpeechlet - ${title}`, content: `SessionSpeechlet - ${output}`, }, reprompt: { outputSpeech: { type: 'PlainText', text: repromptText, }, }, shouldEndSession, }; } function buildResponse(sessionAttributes, speechletResponse) { return { version: '1.0', sessionAttributes, response: speechletResponse, }; } // --------------- Functions that control the skill's behavior ----------------------- function getWelcomeResponse(callback) { // If we wanted to initialize the session to have some attributes we could add those here. const sessionAttributes = {}; const cardTitle = 'Welcome'; const speechOutput = 'Welcome to Home Assistant skills. ' + 'Please tell me the scene you would like to turn off or on, such as, turn off the master bedroom lights'; // If the user either does not reply to the welcome message or says something that is not // understood, they will be prompted again with this text. const repromptText = 'Please tell me a scene to turn on or off, '; const shouldEndSession = false; callback(sessionAttributes, buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession)); } function handleSessionEndRequest(callback) { const cardTitle = 'Session Ended'; const speechOutput = 'Thank you for trying my Home Assistant Skills Kit sample. Have a nice day!'; // Setting this to true ends the session and exits the skill. const shouldEndSession = true; callback({}, buildSpeechletResponse(cardTitle, speechOutput, null, shouldEndSession)); } function createSceneAttributes(sceneToControl) { return { sceneToControl, }; } /** * Sets the scene in the session */ function setSceneStateInSession(state, intent, session, callback) { const cardTitle = intent.name; const selectedSceneSlot = intent.slots.Scene; let repromptText = null; let sessionAttributes = {}; const shouldEndSession = true; let speechOutput = ''; if (selectedSceneSlot) { const selectedScene = selectedSceneSlot.value; speechOutput = `Requesting Home Assistant to turn ${selectedScene}, ${state}.`; } callScene(state, selectedSceneSlot.value, function(){ callback(sessionAttributes, buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession)); }); //callback(sessionAttributes, // buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession)); } // --------------- Events ----------------------- /** * Called when the session starts. */ function onSessionStarted(sessionStartedRequest, session) { console.log(`onSessionStarted requestId=${sessionStartedRequest.requestId}, sessionId=${session.sessionId}`); } /** * Called when the user launches the skill without specifying what they want. */ function onLaunch(launchRequest, session, callback) { console.log(`onLaunch requestId=${launchRequest.requestId}, sessionId=${session.sessionId}`); // Dispatch to your skill's launch. getWelcomeResponse(callback); } /** * Called when the user specifies an intent for this skill. */ function onIntent(intentRequest, session, callback) { console.log(`onIntent requestId=${intentRequest.requestId}, sessionId=${session.sessionId}`); const intent = intentRequest.intent; const intentName = intentRequest.intent.name; // Dispatch to your skill's intent handlers if (intentName === 'SceneTurnOnIntent') { setSceneStateInSession('on', intent, session, callback); } else if (intentName === 'SceneTurnOffIntent') { setSceneStateInSession('off', intent, session, callback); } else if (intentName === 'AMAZON.HelpIntent') { getWelcomeResponse(callback); } else if (intentName === 'AMAZON.StopIntent' || intentName === 'AMAZON.CancelIntent') { handleSessionEndRequest(callback); } else { throw new Error('Invalid intent'); } } /** * Called when the user ends the session. * Is not called when the skill returns shouldEndSession=true. */ function onSessionEnded(sessionEndedRequest, session) { console.log(`onSessionEnded requestId=${sessionEndedRequest.requestId}, sessionId=${session.sessionId}`); // Add cleanup logic here } // --------------- Main handler ----------------------- // Route the incoming request based on type (LaunchRequest, IntentRequest, // etc.) The JSON body of the request is provided in the event parameter. exports.handler = (event, context, callback) => { try { console.log(`event.session.application.applicationId=${event.session.application.applicationId}`); /** * Uncomment this if statement and populate with your skill's application ID to * prevent someone else from configuring a skill that sends requests to this function. */ /* if (event.session.application.applicationId !== 'amzn1.echo-sdk-ams.app.[unique-value-here]') { callback('Invalid Application ID'); } */ if (event.session.new) { onSessionStarted({ requestId: event.request.requestId }, event.session); } if (event.request.type === 'LaunchRequest') { onLaunch(event.request, event.session, (sessionAttributes, speechletResponse) => { callback(null, buildResponse(sessionAttributes, speechletResponse)); }); } else if (event.request.type === 'IntentRequest') { onIntent(event.request, event.session, (sessionAttributes, speechletResponse) => { callback(null, buildResponse(sessionAttributes, speechletResponse)); }); } else if (event.request.type === 'SessionEndedRequest') { onSessionEnded(event.request, event.session); callback(); } } catch (err) { callback(err); } }; function callScene(state, entity, callback){ var data = {"entity_id": getEntity(entity)}; var postData = JSON.stringify(data); var options = { hostname: 'xx.x.xxx.xxx', port: 9615, path: '/api/services/switch/turn_' + state, method: 'POST', headers: { 'Content-Type': 'application/json', 'x-ha-access': 'your_http_api_password', 'Content-Length': Buffer.byteLength(postData) } }; const req = http.request(options, (res) => { console.log(`STATUS: ${res.statusCode}`); console.log(`HEADERS: ${JSON.stringify(res.headers)}`); res.setEncoding('utf8'); res.on('data', (chunk) => { console.log(`BODY: ${chunk}`); }); res.on('end', () => { console.log('No more data in response.'); callback(); }); }); req.on('error', (e) => { console.error(`problem with request: ${e.message}`); }); // write data to request body req.write(postData); req.end(); } // This should be your LIST_OF_SCENES entries mapped to the entity_id listed in HomeAssistant // For example if i have a scene defined as master_bedroom_lights that will come in to lamda as "master bedroom lights" // This maps the scene master bedroom lights to the home assistant entity id we will call to turn on or off. function getEntity(entity){ var entities = { "master bedroom lights" : "switch.master_bedroom_lights_switch", "garage lights": "switch.outside_garage_lights_switch" } return entities[entity]; }
-
Cool now we need to paste our lamda address that looks like arn:aws:lambda:us-east-1:11122233334444:function:myTest into our alexa skill as the endpoint.
-
Test your alexa skill by typing and test that your lights turn on and off.
- When you test within the skill you will just type “turn on the master bedroom lights” because your testing directly against your skill
- When you speak to alexa you will have to say Alexa ask [invokation name] to turn on the master bedroom lights.
-
Impress your significant other with your powers.