Alexa

I am currently working on an Alexa skill for OpenHAB. This will not be a universal Alexa skill that will be enableable (what a word) for the public in the ordinary Alexa app (there are other threads for that) - in this case every user will have to create an Amazon developer account and create their own skill, it will require you to setup your home server for public access on a static IP address (or at least a dynamic DNS name or very rarely changed dynamic IP address), and it will require a web server where you can add some PHP code.

So it will require quite a bit of technological knowledge to get things working - but most of OpenHAB does anyway, so I thought I might as well publish the details here. Especially it would be cool if some Java guru could take my PHP code and incorporate it into the OpenHAB java source one way or another.

I have already got a couple of PoC working - to turn switches on/off and to get temperature reports from different rooms.

I will do some more stuff, and then document it in a viable way before I add it here. So, more to come…

1 Like

Addition: The PHP code can be avoided by putting that code into AWS Lambda (using JS, Python or Java). I might create a JS Lambda example, but I prefer to keep the cloud dependencies to a minimum (even though total independence is impossible when using Alexa).

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.

  1. 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#/

  2. Create a new skill using the following settings:
    Type: Custom Interaction Model
    Name: OpenHAB
    Invocation name: Habbie

  3. 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"
            }
          ]
        }
      ]
    }
  1. Add a custom slot type, call it ROOM and put your rooms into it:
    Living room
    Kitchen
    Dining room
    Bed room
    Etc
  1. 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}
  1. Leave the rest of the skill be for now.

  2. 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/

  3. Change region to US East (North Virginia) - it is the only one that works with Alexa.

  4. 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.

  5. Go to the trigger tab and add a trigger for Alexa Skills Kit.

  6. 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,

  7. 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";

Hi, have u seen Amazon Echo Skill for OpenHAB available! ? Also I have a native alexa/lambda skill for OH2 that I am waiting for some changes to my.openhab to release (to the alexa platform and open source), this would just do home control (using their alexa smarthome api).

Hi Dan!

Yes, I saw that one (it’s in my bookmarks). I even think I tried it with the intention to adapt it to my environment, but never got it to work. I don’t remember where I got stuck, but I’m running OH1 on Windows without MyOpenHAB - so I guess there was one too many parameters that differed.

I chose not to go for the smart home API, since it only covers on, off and dim. But since your version can also do state reporting, then maybe that is an incorrect assumption?

Regards
/P

Dan: I read through your readmes and some of the code, and it should work perfectly fine with OH1 on Windows without MyOpenHAB (especially now, with the knowledge I gained by my little project above). I don’t know what I got stuck on last time, so maybe I should give it another shot.

Hello @Pal!

Alexa-HA should be able to run on Windows too. I’d be glad to help you get up and going with it - feel free to ping me for assistance if needed. Alexa-HA is designed to run on your own NodeJS server, but it should be possible to run it in AWS Lambda as well (currently untested!). Ironically I started out with a PoC in PHP too, but after studying the more extensive JS ASK SDK code examples, it made better sense to switch to NodeJS.

It would be great if we could all merge our efforts into a single project!

All the best,
.