Here is what you need for a PoC. Alexa will be able to answer questions about temperature, humidity and luminance in different rooms - but as you can see it is easy to extend it to lights or pretty much anything supported by the OpenHAB REST interface.
The code is rather flurry - I edited some code for another project - so I will clean it up as I go on adding support for more of my stuff.
You can of course change names and similar to your liking.
-
Go to the Amazon developer Alexa page and register an account if you don’t already have one: https://developer.amazon.com/edw/home.html#/
-
Create a new skill using the following settings:
Type: Custom Interaction Model
Name: OpenHAB
Invocation name: Habbie
-
Add the Intent schema:
{
"intents":[
{
"intent": "temperature",
"slots": [
{
"name": "room",
"type": "ROOM"
}
]
},
{
"intent": "humidity",
"slots": [
{
"name": "room",
"type": "ROOM"
}
]
},
{
"intent": "luminance",
"slots": [
{
"name": "room",
"type": "ROOM"
}
]
}
]
}
- Add a custom slot type, call it ROOM and put your rooms into it:
Living room
Kitchen
Dining room
Bed room
Etc
- Add some sample utterances (the more the merrier) - each line starting with the intent and then what comes after “Alexa, tell Habbie…”:
temperature what is the temperature for the {room}
temperature what is the temperature in the {room}
temperature about the temperature for the {room}
temperature about the temperature in the {room}
temperature how hot it is in the {room}
temperature how cold it is in the {room}
temperature what the temperature in the {room} is
temperature what the temperature is in the {room}
luminance what is the luminance for the {room}
luminance what is the luminance in the {room}
luminance about the luminance for the {room}
luminance about the luminance in the {room}
luminance how dark it is in the {room}
luminance how light it is in the {room}
luminance how bright it is in the {room}
luminance for the luminance in the {room}
luminance for the {room} luminance
luminance to give me the luminance in the {room}
humidity what is the humidity for the {room}
humidity what is the humidity in the {room}
humidity about the humidity for the {room}
humidity about the humidity in the {room}
humidity what the humidity in the {room} is
humidity what the humidity is in the {room}
humidity for the humidity in the {room}
humidity for the {room} humidity
humidity how wet it is in the {room}
humidity how dry it is in the {room}
-
Leave the rest of the skill be for now.
-
Create an AWS account if you don’t already have one (you can use the same account as for the skill): https://aws.amazon.com/lambda/
-
Change region to US East (North Virginia) - it is the only one that works with Alexa.
-
Click “Get started with AWS Lambda”, choose Lambda and create a new function, skip the blueprint, choose Alexa Skills Kit as trigger and create a new function with a cool name and Node.JS as engine. Choose upload as ZIP, leave index.handler as is, create a new custom role with IAM role lambda_basic_execution, click next and create the function.
-
Go to the trigger tab and add a trigger for Alexa Skills Kit.
-
Copy the ARN in the upper right corner. Go back to your Alexa Skills page, on the configuration tab, choose Lambda ARN and paste the ARN,
-
Create the file alexaSkill.js on you local hard disk and put this into it:
/**
Copyright 2014-2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with
the License. A copy of the License is located at http://aws.amazon.com/apache2.0/ or in the "license" file
accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations
under the License.
*/
'use strict';
function AlexaSkill(appId) {
this._appId = appId;
}
AlexaSkill.speechOutputType = {
PLAIN_TEXT: 'PlainText',
SSML: 'SSML'
}
AlexaSkill.prototype.requestHandlers = {
LaunchRequest: function (event, context, response) {
this.eventHandlers.onLaunch.call(this, event.request, event.session, response);
},
IntentRequest: function (event, context, response) {
this.eventHandlers.onIntent.call(this, event.request, event.session, response);
},
SessionEndedRequest: function (event, context) {
this.eventHandlers.onSessionEnded(event.request, event.session);
context.succeed();
}
};
/**
* Override any of the eventHandlers as needed
*/
AlexaSkill.prototype.eventHandlers = {
/**
* Called when the session starts.
* Subclasses could have overriden this function to open any necessary resources.
*/
onSessionStarted: function (sessionStartedRequest, session) {
},
/**
* Called when the user invokes the skill without specifying what they want.
* The subclass must override this function and provide feedback to the user.
*/
onLaunch: function (launchRequest, session, response) {
throw "onLaunch should be overriden by subclass";
},
/**
* Called when the user specifies an intent.
*/
onIntent: function (intentRequest, session, response) {
var intent = intentRequest.intent,
intentName = intentRequest.intent.name,
intentHandler = this.intentHandlers[intentName];
if (intentHandler) {
console.log('dispatch intent = ' + intentName);
intentHandler.call(this, intent, session, response);
} else {
throw 'Unsupported intent = ' + intentName;
}
},
/**
* Called when the user ends the session.
* Subclasses could have overriden this function to close any open resources.
*/
onSessionEnded: function (sessionEndedRequest, session) {
}
};
/**
* Subclasses should override the intentHandlers with the functions to handle specific intents.
*/
AlexaSkill.prototype.intentHandlers = {};
AlexaSkill.prototype.execute = function (event, context) {
try {
console.log("session applicationId: " + event.session.application.applicationId);
// Validate that this request originated from authorized source.
if (this._appId && event.session.application.applicationId !== this._appId) {
console.log("The applicationIds don't match : " + event.session.application.applicationId + " and "
+ this._appId);
throw "Invalid applicationId";
}
if (!event.session.attributes) {
event.session.attributes = {};
}
if (event.session.new) {
this.eventHandlers.onSessionStarted(event.request, event.session);
}
// Route the request to the proper handler which may have been overriden.
var requestHandler = this.requestHandlers[event.request.type];
requestHandler.call(this, event, context, new Response(context, event.session));
} catch (e) {
console.log("Unexpected exception " + e);
context.fail(e);
}
};
var Response = function (context, session) {
this._context = context;
this._session = session;
};
function createSpeechObject(optionsParam) {
if (optionsParam && optionsParam.type === 'SSML') {
return {
type: optionsParam.type,
ssml: optionsParam.speech
};
} else {
return {
type: optionsParam.type || 'PlainText',
text: optionsParam.speech || optionsParam
}
}
}
Response.prototype = (function () {
var buildSpeechletResponse = function (options) {
var alexaResponse = {
outputSpeech: createSpeechObject(options.output),
shouldEndSession: options.shouldEndSession
};
if (options.reprompt) {
alexaResponse.reprompt = {
outputSpeech: createSpeechObject(options.reprompt)
};
}
if (options.cardTitle && options.cardContent) {
alexaResponse.card = {
type: "Simple",
title: options.cardTitle,
content: options.cardContent
};
}
var returnResult = {
version: '1.0',
response: alexaResponse
};
if (options.session && options.session.attributes) {
returnResult.sessionAttributes = options.session.attributes;
}
return returnResult;
};
return {
tell: function (speechOutput) {
this._context.succeed(buildSpeechletResponse({
session: this._session,
output: speechOutput,
shouldEndSession: true
}));
},
tellWithCard: function (speechOutput, cardTitle, cardContent) {
this._context.succeed(buildSpeechletResponse({
session: this._session,
output: speechOutput,
cardTitle: cardTitle,
cardContent: cardContent,
shouldEndSession: true
}));
},
ask: function (speechOutput, repromptSpeech) {
this._context.succeed(buildSpeechletResponse({
session: this._session,
output: speechOutput,
reprompt: repromptSpeech,
shouldEndSession: false
}));
},
askWithCard: function (speechOutput, repromptSpeech, cardTitle, cardContent) {
this._context.succeed(buildSpeechletResponse({
session: this._session,
output: speechOutput,
reprompt: repromptSpeech,
cardTitle: cardTitle,
cardContent: cardContent,
shouldEndSession: false
}));
}
};
})();
module.exports = AlexaSkill;
13: Put this chunk of text into index.js in the same directory as the file above:
var APP_ID = "amzn1.ask.skill.[YOUR SKILL ID]";
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
var http = require('https');
var AlexaSkill = require('./alexaSkill');
var Habbie = function () {
AlexaSkill.call(this, APP_ID);
};
var ROOMS = {
"living room": {
"name": "living room",
"temperature": "livingroom_temperature",
"humidity": "livingroom_humidity",
"luminance": "livingroom_luminance"
},
"kitchen": {
"name": "kitchen",
"temperature": "kitchen_temperature",
"humidity": "kitchen_humidity",
"luminance": "kitchen_luminance"
},
"dining room": {
"name": "dining room",
"temperature": "diningroom_temperature",
"humidity": "diningroom_humidity",
"luminance": "diningroom_luminance"
},
"bed room": {
"name": "bed room",
"temperature": "bedroom_temperature",
"humidity": "bedroom_humidity",
"luminance": "bedroom_luminance"
}
};
Habbie.prototype = Object.create(AlexaSkill.prototype);
Habbie.prototype.constructor = Habbie;
Habbie.prototype.eventHandlers.onSessionStarted = function (sessionStartedRequest, session) {
console.log("onSessionStarted requestId: " + sessionStartedRequest.requestId + ", sessionId: " + session.sessionId);
};
Habbie.prototype.eventHandlers.onLaunch = function (launchRequest, session, response) {
console.log("onLaunch requestId: " + launchRequest.requestId + ", sessionId: " + session.sessionId);
handleWelcomeRequest(response);
};
Habbie.prototype.eventHandlers.onSessionEnded = function (sessionEndedRequest, session) {
console.log("onSessionEnded requestId: " + sessionEndedRequest.requestId + ", sessionId: " + session.sessionId);
};
Habbie.prototype.intentHandlers = {
"temperature": function (intent, session, response) {
console.log('Asked for temperature');
console.log(intent);
var roomSlot = intent.slots.room;
if (roomSlot && roomSlot.value) {
handleRequest(intent, session, response);
} else {
handleNoSlotRequest(intent, session, response);
}
},
"humidity": function (intent, session, response) {
console.log('Asked for humidity');
console.log(intent);
var roomSlot = intent.slots.room;
if (roomSlot && roomSlot.value) {
handleRequest(intent, session, response);
} else {
handleNoSlotRequest(intent, session, response);
}
},
"luminance": function (intent, session, response) {
console.log('Asked for luminance');
console.log(intent);
var roomSlot = intent.slots.room;
if (roomSlot && roomSlot.value) {
handleRequest(intent, session, response);
} else {
handleNoSlotRequest(intent, session, response);
}
},
"SupportedRoomsIntent": function (intent, session, response) {
console.log('Asked for rooms');
console.log(intent);
handleSupportedRoomsRequest(intent, session, response);
},
"AMAZON.HelpIntent": function (intent, session, response) {
console.log('Asked for help');
console.log(intent);
handleHelpRequest(response);
},
"AMAZON.StopIntent": function (intent, session, response) {
var speechOutput = "Thank you for making a simple Habbie very happy.";
response.tell(speechOutput);
},
"AMAZON.CancelIntent": function (intent, session, response) {
var speechOutput = "Thank you for making a simple Habbie very happy.";
response.tell(speechOutput);
}
};
function handleWelcomeRequest(response) {
var whichRoomPrompt = "Which room would you like information from?";
var speechOutput = {
speech: "Hi, I am Habbie! " + whichRoomPrompt,
type: AlexaSkill.speechOutputType.PLAIN_TEXT
},
repromptOutput = {
speech: "I need to know what room you would like information from. For a list of rooms, tell me to give you the rooms. " + whichRoomPrompt,
type: AlexaSkill.speechOutputType.PLAIN_TEXT
};
response.ask(speechOutput, repromptOutput);
}
function handleHelpRequest(response) {
var repromptText = "Which room would you like information from?";
var speechOutput = "I need to know what room you would like information from. For a list of rooms, tell me to give you the rooms. " + repromptText;
response.ask(speechOutput, repromptText);
}
function handleSupportedRoomsRequest(intent, session, response) {
// get room re-prompt
var repromptText = "Which room would you like information from?";
var speechOutput = "I have information from these rooms: " + getAllRoomsText() + repromptText;
response.ask(speechOutput, repromptText);
}
function handleRequest(intent, session, response) {
console.log('Handling!');
console.log(intent);
var room = getRoomFromIntent(intent, false),
repromptText,
speechOutput;
if (room.error) {
repromptText = "I have information from these rooms: " + getAllRoomsText() + "Which room would you like information from?";
speechOutput = room.name ? "I'm sorry, I don't have any data for " + room.name + ". " + repromptText : repromptText;
response.ask(speechOutput, repromptText);
return;
}
session.attributes.room = room;
getFinalHabbieResponse(intent, room, response);
}
function handleNoSlotRequest(intent, session, response) {
console.log('No slot');
console.log(intent);
if (session.attributes.room) {
var repromptText = "Please try again saying a room. ";
var speechOutput = repromptText;
response.ask(speechOutput, repromptText);
} else {
handleSupportedRoomsRequest(intent, session, response);
}
}
function getFinalHabbieResponse(intent, room, response) {
console.log('Get final response');
console.log(intent);
console.log(room);
makeHabbieRequest(room[intent.name], function habbieResponseCallback(err, habbieResponse) {
var speechOutput;
if (err) {
speechOutput = "Sorry, Habbie is having a headache. Please try again later.";
} else {
speechOutput = "The " + intent.name + " in the " + room.name + " is " + habbieResponse;
}
response.tellWithCard(speechOutput, "Habbie", speechOutput);
});
}
function makeHabbieRequest(item, habbieResponseCallback) {
console.log("Making request");
console.log(item);
var endpoint = 'https://user:pass@your.openhabserver.com:8443/rest/items/' + item + '?type=json';
http.get(endpoint, function (res) {
var habbieResponseString = '';
console.log('Status Code: ' + res.statusCode);
if (res.statusCode != 200) {
habbieResponseCallback(new Error("Non 200 Response"));
}
res.on('data', function (data) {
habbieResponseString += data;
});
res.on('end', function () {
var habbieResponseObject = JSON.parse(habbieResponseString);
if (habbieResponseObject.error) {
console.log("Habbie error: " + habbieResponseObject.error.message);
habbieResponseCallback(new Error(habbieResponseObject.error.message));
} else {
var habbieValue = findHabbieValue(habbieResponseObject);
if (habbieValue.error) {
console.log("Habbie error: " + habbieValue.error.message);
habbieResponseCallback(new Error(habbieValue.error.message));
} else {
console.log("Habbie value: " + habbieValue.value);
habbieResponseCallback(null, habbieValue.value);
}
}
});
}).on('error', function (e) {
console.log("Communications error: " + e.message);
habbieResponseCallback(new Error(e.message));
});
}
function findHabbieValue(habbieResponseObject) {
var habbieState = habbieResponseObject.state;
if (isNaN(habbieState)) {
return {
error: {
message: "Habbie did not return a number."
}
};
} else if (habbieState === '' || habbieState === true || habbieState === false || habbieState === null) {
return {
error: {
message: "Habbie did not return a number."
}
};
} else {
return {
value: habbieState
};
}
}
function getRoomFromIntent(intent, assignDefault) {
console.log("Finding room");
console.log(intent);
var roomSlot = intent.slots.room;
if (!roomSlot || !roomSlot.value) {
if (!assignDefault) {
console.log("No slot found");
return {
error: true
};
} else {
return ROOMS['Living room'];
}
} else {
var roomName = roomSlot.value;
console.log("Looking for " + roomName);
if (ROOMS[roomName]) {
console.log("Found room");
console.log(ROOMS[roomName]);
return ROOMS[roomName];
} else {
console.log("No room found");
return {
error: true,
room: roomName
};
}
}
}
function getAllRoomsText() {
var roomList = '';
Object.keys(ROOMS).forEach(function(key,index) {
var room = ROOMS[key];
roomList += room.name + ", ";
});
return roomList;
}
exports.handler = function (event, context) {
var habbie = new Habbie();
habbie.execute(event, context);
};
14: Go to your Alexa Skill page and click the information tab, copy the Application ID at the top and put it at “[YOUR SKILL ID]” at the top of the file above.
15: Change this line to your real values in the file above:
var endpoint = 'https://user:pass@your.openhabserver.com:8443/rest/items/' + item + '?type=json';
16: Zip the two files together, and upload them to your AWS Lambda function.
17: Thats it. Say “Alexa, ask Habbie what the temperature in the kitchen is” and she will answer.
18: Add rooms, add other sensors. It ought to be pretty obvious where things need to be added in the code above. Adding switches and dimmers might require a little more work, but not much.
19: If you have a real valid cert on your server, you can remove this line:
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";