2017/6/17

讓 Alexa到ThingSpeak讀取溫度-發出HTTPS GET請求


透過網頁瀏覽器對ThingSpeak IoT資料庫抓值非常簡單, 相信大家都會, 但是透過Alexa語音助理對ThingSpeak資料庫要資料就比較新鮮了! 如題, 底層是透過REST API方法, 國外許多腦筋動得快的年輕人, 他們立刻舉一反三開始透過WikipediaGoogleREST APIAlexa幫他們查詢/回報資料甚至自架Web Service APIAlexa幫他們「讀」書呢. 筆者心想, 再不跟著「Me to …」可能都已跟不上時代潮流, 更甭想談創新了!


基本原理
我們於「ESP8266存取IoT數據資料庫」中有討論到將DHT11/濕度測量值上傳到ThingSpeak IoT資料庫(亦就是拿ThingSpeak當我們的MQTT server), 也列舉幾種透過瀏覽器URL或以Javascript發出HTTPS/GET請求方式抓值. 其實在Alexa Skill Kit底層的方法/實作原理也是相同, 而本案例將試範如何在AWS/Lambda程式發出HTTPS/GET請求並將最新的溫/濕度測量值透過Alexa語音助理(Echo Dot)「說出來」. 整個概念如圖所示, 其中Echo設備(連接WAN)並不侷限在自己家裡.







程式架構
左半部則是新的Alexa Skill Kit (ASK) 開發者平台, 我們將在此為Alexa定義新的技能. 當使用者「調用(Invoke)」自己寫的程式名, 本例為「information, 例如語音指令: Alexa, ask information, …. 緊接著使用者的「命令語彙(utterance)」符合所預先例舉的樣本而觸發使用者自訂的「意圖(Intent), 本例為「GetTemperatureIntent, 並以JSON方式要求AWS/Lambda提供底層的服務(Intent Handler).






增加AlexaThinkSpeak發出HTTPS/GET請求的Skill

Step1: Skill Information
ASK網頁新增一個叫做「information」的Skill, Skill名稱目前對初學者而言不重要, 取一個好記的名稱即可. 只有將來我們功力進步想發佈到 public domain時可能會影響搜尋的順利, 因為你將發現Alexa Skill Store也是有許多不是我們要的App(取菜市場名稱可能很難找到我們自己開發的).


Step2: Interaction Mode

ASK/Intent Schema
請參閱程式架構圖, 我們想為Alexa添增/學會對ThingSpeak資料庫查詢資料的新技能. 意圖模式(Intent Schema)是以JSON的格式來定義. 如下, 我們為自己的Skill添增一個稱為「GetTemperatureIntent」的意圖(Intent). 其中我們還引用了一個預設的「AMAZON.HelpIntent, 目的是當我們調用自己的App卻沒有給任何語音指令時將觸發該Intent, 而其對應的處理函式為「getWelcomeResponse.

{
  "intents": [
    {
      "intent": "GetTemperatureIntent"
    },
    {
      "intent": "AMAZON.HelpIntent"
    }
  ]
}



ASK/Sample utterances
為了讓Alexa能成功辨識使用者語音指令, 並觸發我們所定義的「GetTemperatureIntent, 因此我們須先預想幾種可能的使用者語音輸入樣本以成功觸發該Intent, 例如:

GetTemperatureIntent get info
GetTemperatureIntent get information
GetTemperatureIntent get the temprature
GetTemperatureIntent check thingspeak
GetTemperatureIntent query thingspeak
GetTemperatureIntent what is the temperature
GetTemperatureIntent give me the temperature



Step3: 撰寫Lambda程式
這個步驟須將焦點暫時從ASK網頁移到AWS/Lambda網頁, 並為我們設定的「GetTemperatureIntent」實作底層的處理函式. 選擇Create建立新的Lambda Function, 並暫時選擇空白的藍圖, 因為我們將使用到外部的函式庫, 例如「request」或「https」等以建立HTTPS/GET請求.

網路上有許多人詢問關於「如何在Alexa Skill建立HTTPS/GET請求」這個議題, 筆者試了很多版本, 只有Kathryn Hodge的版本最簡單(所需要的外部軟件她也都幫我們打包好了). 請直接下載Archive.zip這個檔案並上傳到Lambda, 並依照下圖的步驟將該ZIP檔上載並按儲存.






檔案上載之後將下面筆者寫的樣板整個複製並貼上即大功告成. 讀者可以修改getWelcomeResponse函式以提供該App使用說明, 該函式由AMAZON.HelpIntent觸發. getTemperatureHandler為建立HTTPS/GET請求的主要函式, 其中引用的外部函式庫request為核心來完成. ThingSpek回傳的資料為JSON物件(內容為ESP-01所上傳的最新一筆溫/濕度值), 細部的設定請參閱「ESP8266存取IoT數據資料庫」文中對於建立Channel與溫/濕度欄位的說明, 其中field1為濕度值(humidity)field2為溫度值(temperature).

request.get的參數中, 請讀者填入自己申請的Channel IdAPI-KEY(read mode)與符合自己資料欄位定義的格式以順利建立語音資訊回傳. 除了「End of you code above …」以上的三個函式, 其它程式碼皆不需做任何修改.

// Start your code below
var request = require('request');

// Called when the user specifies an intent for this skill.
function onIntent(intentRequest, session, callback) {
    var intent = intentRequest.intent;
    var intentName = intentRequest.intent.name;

    // dispatch custom intents to handlers here
    if (intentName == "GetTemperatureIntent") {
        getTemperatureHandler(intent, session, callback);
    } else if (intentName === "AMAZON.HelpIntent") {
        getWelcomeResponse(callback);
    } else {
         throw "Invalid intent";
    }
}

function getWelcomeResponse(callback) {
    var speechOutput = "Welcome! I am ready to access ThingSpeak.";
    var reprompt = speechOutput;
    var header = "Query ThingSpeak”;
    var shouldEndSession = false;
    var sessionAttributes = {
        "speechOutput" : speechOutput,
        "repromptText" : reprompt
    };
    callback(sessionAttributes,
        buildSpeechletResponse(header, speechOutput, reprompt, shouldEndSession));
}

// get info from ThingSpeak (channel: DHT11)
function getTemperatureHandler(intent, session, callback) {
    var url = "https://api.thingspeak.com/channels/Channel/feeds/last.json?api_key=APIKEY";
    // sample return JSON {"created_at":"xxx","entry_id":xxx,"field1":"59.00","field2":"29.00"}
   
    request.get(url, function(error, response, body) {
        //console.log(body);
        var d = JSON.parse(body);
        var humidity = Math.round(d.field1);   
        var temperature = Math.round(d.field2);
        var speechOutput = "We have an error.";
        var reprompt = JSON.stringify(d);
        if ( humidity>0 ) {
            speechOutput = "The temperature is " + temperature + " degree, and " +
                         "the humidity is " + humidity + " %.";
        }
        callback(session.attributes,
            buildSpeechletResponseWithoutCard(speechOutput, reprompt, true));
    });
}
// End your code above
/////////////////////////////////////////////////////////////////////////////////////////

// Route the incoming request based on type (LaunchRequest, IntentRequest, etc.)
// The JSON body of the request is provided in the event parameter.
exports.handler = function (event, context) {
    try {
        console.log("event.session.application.applicationId=" +
            event.session.application.applicationId);

        if (event.session.new) {
            onSessionStarted({requestId: event.request.requestId}, event.session);
        }

        if (event.request.type === "LaunchRequest") {
            onLaunch(event.request,
                event.session,
                function callback(sessionAttributes, speechletResponse) {
                    context.succeed(buildResponse(sessionAttributes, speechletResponse));
                });
        } else if (event.request.type === "IntentRequest") {
            onIntent(event.request,
                event.session,
                function callback(sessionAttributes, speechletResponse) {
                    context.succeed(buildResponse(sessionAttributes, speechletResponse));
                });
        } else if (event.request.type === "SessionEndedRequest") {
            onSessionEnded(event.request, event.session);
            context.succeed();
        }
    } catch (e) {
        context.fail("Exception: " + e);
    }
};

// Called when the session starts.
function onSessionStarted(sessionStartedRequest, session) {
    // add any session init logic here
}

// Called when the user invokes the skill without specifying what they want.
function onLaunch(launchRequest, session, callback) {
    getWelcomeResponse(callback);
}


// Called when the user ends the session.
// Is not called when the skill returns shouldEndSession=true.
function onSessionEnded(sessionEndedRequest, session) {
}


// ------- Helper functions to build responses for Alexa -------
function buildSpeechletResponse(title, output, repromptText, shouldEndSession) {
    return {
        outputSpeech: {
            type: "PlainText",
            text: output
        },
        card: {
            type: "Simple",
            title: title,
            content: output
        },
        reprompt: {
            outputSpeech: {
                type: "PlainText",
                text: repromptText
            }
        },
        shouldEndSession: shouldEndSession
    };
}

function buildSpeechletResponseWithoutCard(output, repromptText, shouldEndSession) {
    return {
        outputSpeech: {
            type: "PlainText",
            text: output
        },
        reprompt: {
            outputSpeech: {
                type: "PlainText",
                text: repromptText
            }
        },
        shouldEndSession: shouldEndSession
    };
}

function buildResponse(sessionAttributes, speechletResponse) {
    return {
        version: "1.0",
        sessionAttributes: sessionAttributes,
        response: speechletResponse
    };
}

function capitalizeFirst(s) {
    return s.charAt(0).toUpperCase() + s.slice(1);
}





按儲存之後請將網頁編輯器最右上方的ARN碼複製下來, 它將用來綁定ASK的外部服務service endpoint, 它的格式為: arn:aws:lambda:us-east-1:xxxx:function:ThingSpeak


Step4: Configuration
完成Lambda程式並取得ARN碼之後我們須回到ASK的頁面繼續完成configuration, 如下圖, service enpoint中勾選AWS Lambda並填入ARN code.


Step5: 測試
ASK提供了文字轉語音輸出的模擬器與Echo語音助理的模擬器, 因此即使我們手邊並沒有真正的Echo硬體設備也能模擬真實的結果. 從模擬器中可以清楚看見底層資料以JSON的遞送/交換過程, 當使用者說出「get information, ASK從列舉的utternaces中成功辨識/匹配的「GetTemperatureIntent」並以JSON交由Lambda處理, 最後我們寫的Intent Handler函式處理完之後再以JSON傳回ASK接手後續的Echo發聲與手機文字顯示等服務. 當系統等不到語音指令則「AMAZON.HelpIntent」被觸發, 並交由Lambda/getWelcomeResponse函式處理.








測試步驟:
筆者: Alexa, ask information
(不給intent, 靜待ASK觸發AMAZON.HelpIntent, Alexa給出welcome訊息)

Alexa: Welcome! I am ready to access ThingSpeak

筆者: get the temperature
Alexa: The temperature is 29 degree, and the humidity is 59 %.