将Netatmo气象站连接到Amazon Echo(Alexa)

时间:2015-11-22 20:30:57

标签: javascript amazon-web-services aws-lambda alexa-skill alexa-skills-kit

[以下回答问题的完整教程。欢迎反馈!]

我正在尝试创建一个AWS Lambda函数,用于Amazon Alexa技能从我的Netatmo weatherstation获取天气信息。基本上,我需要通过http请求连接到Netatmo云。

这是我的代码片段,http请求是针对临时访问令牌完成的,请求没问题,但结果正文是正文:{“error”:“invalid_request”}。这可能是什么问题?

var clientId = "";
var clientSecret = "";
var userId="a@google.ro"; 
var pass=""; 

function getNetatmoData(callback, cardTitle){
    var sessionAttributes = {};

    var formUserPass = { client_id: clientId, 
    client_secret: clientSecret, 
    username: userId, 
    password: pass, 
    scope: 'read_station', 
    grant_type: 'password' };

    shouldEndSession = false;
    cardTitle = "Welcome";
    speechOutput =""; 
    repromptText ="";

    var options = {
        host: 'api.netatmo.net',
        path: '/oauth2/token',
        method: 'POST',
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            'client_id': clientId,
            'client_secret': clientSecret,
            'username': userId, 
            'password': pass, 
            'scope': 'read_station', 
            'grant_type': 'password'
        }
    };
    var req = http.request(options, function(res) {
            res.setEncoding('utf8');
            res.on('data', function (chunk) {
                console.log("body: " + chunk);

            });

            res.on('error', function (chunk) {
                console.log('Error: '+chunk);
            });

            res.on('end', function() {

                speechOutput = "Request successfuly processed."
                console.log(speechOutput);
                repromptText = ""
                callback(sessionAttributes, buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
            });

        });

        req.on('error', function(e){console.log('error: '+e)});

        req.end();
}

1 个答案:

答案 0 :(得分:8)

我跑了! 这是一个快速演练:

  1. 获取Amazon AWS的免费帐户。只要您的技能不能持续运行(您将通过AWS服务器上使用的运行时间和资源(每月700个免费小时)收费,您应该保持良好状态并且保持免费。该技能一次需要1-3秒。

  2. 在Amazon Web Services(AWS)中设置新的lambda函数。每次调用技能时都会执行此功能。

  3. 以下是技能代码:

    /**
    *   Author: Mihai GALOS
    *   Timestamp: 17:17:00, November 1st 2015  
    */
    
    var http = require('https'); 
    var https = require('https');
    var querystring = require('querystring');
    
    var clientId = ''; // create an application at https://dev.netatmo.com/ and fill in the generated clientId here
    var clientSecret = ''; // fill in the client secret for the application
    var userId= '' // your registration email address
    var pass = '' // your account password
    
    
    // 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);
    
            /**
             * 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]") {
                 context.fail("Invalid Application ID");
             }
            */
    
            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);
        }
    };
    
    
    function onSessionStarted(sessionStartedRequest, session) {
        console.log("onSessionStarted requestId=" + sessionStartedRequest.requestId +
                ", sessionId=" + session.sessionId);
    }
    
    
    function onLaunch(launchRequest, session, callback) {
        console.log("onLaunch requestId=" + launchRequest.requestId +
                ", sessionId=" + session.sessionId);
    
        // Dispatch to your skill's launch.
    
        getData(callback);
    
    }
    
    
    function onIntent(intentRequest, session, callback) {
        console.log("onIntent requestId=" + intentRequest.requestId +
                ", sessionId=" + session.sessionId);
    
        var intent = intentRequest.intent,
            intentName = intentRequest.intent.name;
        var intentSlots ;
    
        console.log("intentRequest: "+ intentRequest);  
        if (typeof intentRequest.intent.slots !== 'undefined') {
            intentSlots = intentRequest.intent.slots;
        }
    
    
         getData(callback,intentName, intentSlots);
    
    
    }
    
    
    function onSessionEnded(sessionEndedRequest, session) {
        console.log("onSessionEnded requestId=" + sessionEndedRequest.requestId +
                ", sessionId=" + session.sessionId);
        // Add cleanup logic here
    }
    
    // --------------- Functions that control the skill's behavior -----------------------
    
    function doCall(payload, options, onResponse,
                callback, intentName, intentSlots){
        var response = ''
        var req = https.request(options, function(res) {
                res.setEncoding('utf8');
    
                 console.log("statusCode: ", res.statusCode);
                 console.log("headers: ", res.headers);
    
    
                res.on('data', function (chunk) {
                    console.log("body: " + chunk);
                    response += chunk;
                });
    
                res.on('error', function (chunk) {
                    console.log('Error: '+chunk);
                });
    
                res.on('end', function() {
                    var parsedResponse= JSON.parse(response);
                    if (typeof onResponse !== 'undefined') {
                        onResponse(parsedResponse, callback, intentName, intentSlots);
                    }
                });
    
            });
    
            req.on('error', function(e){console.log('error: '+e)});
            req.write(payload);
    
            req.end();
    
    }
    
    function getData(callback, intentName, intentSlots){
    
    
    
            console.log("sending request to netatmo...")
    
            var payload = querystring.stringify({
                'grant_type'    : 'password',
                'client_id'     : clientId,
                'client_secret' : clientSecret,
                'username'      : userId,
                'password'      : pass,
                'scope'         : 'read_station'
          });
    
            var options = {
                host: 'api.netatmo.net',
                path: '/oauth2/token',
                method: 'POST',
               headers: {
                    'Content-Type': 'application/x-www-form-urlencoded',
                    'Content-Length': Buffer.byteLength(payload)
                }
    
            };
    
            //console.log('making request with data: ',options);
    
            // get token and set callbackmethod to get measure 
            doCall(payload, options, onReceivedTokenResponse, callback, intentName, intentSlots);
    }
    
    function onReceivedTokenResponse(parsedResponse, callback, intentName, intentSlots){
    
            var payload = querystring.stringify({
                'access_token'  : parsedResponse.access_token
          });
    
            var options = {
                host: 'api.netatmo.net',
                path: '/api/devicelist',
                method: 'POST',
               headers: {
                    'Content-Type': 'application/x-www-form-urlencoded',
                    'Content-Length': Buffer.byteLength(payload)
                }
    
            };
    
        doCall(payload, options, getMeasure, callback, intentName, intentSlots);
    
    }
    
    function getMeasure(parsedResponse, callback, intentName, intentSlots){
    
    
             var data = {
                    tempOut         : parsedResponse.body.modules[0].dashboard_data.Temperature,
                    humOut          : parsedResponse.body.modules[0].dashboard_data.Humidity,
                    rfStrengthOut   : parsedResponse.body.modules[0].rf_status,
                    batteryOut      : parsedResponse.body.modules[0].battery_vp,
    
                    tempIn      : parsedResponse.body.devices[0].dashboard_data.Temperature,
                    humIn       : parsedResponse.body.devices[0].dashboard_data.Humidity,
                    co2         : parsedResponse.body.devices[0].dashboard_data.CO2,
                    press       : parsedResponse.body.devices[0].dashboard_data.Pressure,
    
                    tempBedroom         : parsedResponse.body.modules[2].dashboard_data.Temperature,
                    humBedroom          : parsedResponse.body.modules[2].dashboard_data.Temperature,
                    co2Bedroom          : parsedResponse.body.modules[2].dashboard_data.CO2,
                    rfStrengthBedroom   : parsedResponse.body.modules[2].rf_status,
                    batteryBedroom      : parsedResponse.body.modules[2].battery_vp,
    
                    rainGauge           : parsedResponse.body.modules[1].dashboard_data,
                    rainGaugeBattery    : parsedResponse.body.modules[1].battery_vp
                   };
    
        var repromptText = null;
        var sessionAttributes = {};
        var shouldEndSession = true;
        var speechOutput ;
    
        if( "AskTemperature" === intentName)  {
    
            console.log("Intent: AskTemperature, Slot:"+intentSlots.Location.value);
    
            if("bedroom" ===intentSlots.Location.value){
                speechOutput = "There are "+data.tempBedroom+" degrees in the bedroom.";
    
            }
            else if ("defaultall" === intentSlots.Location.value){
                speechOutput = "There are "+data.tempIn+" degrees inside and "+data.tempOut+" outside.";
            }
    
            if(data.rainGauge.Rain > 0) speechOutput += "It is raining.";
        } else if ("AskRain" === intentName){
            speechOutput = "It is currently ";
            if(data.rainGauge.Rain > 0) speechOutput += "raining.";
            else speechOutput += "not raining. ";
    
            speechOutput += "Last hour it has rained "+data.rainGauge.sum_rain_1+" millimeters, "+data.rainGauge.sum_rain_1+" in total today.";
        } else { // AskTemperature
            speechOutput = "Ok. There are "+data.tempIn+" degrees inside and "+data.tempOut+" outside.";
    
            if(data.rainGauge.Rain > 0) speechOutput += "It is raining.";
        }
    
            callback(sessionAttributes,
                 buildSpeechletResponse("", speechOutput, repromptText, shouldEndSession));
    
    }
    
    // --------------- 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: shouldEndSession
        };
    }
    
    function buildResponse(sessionAttributes, speechletResponse) {
        return {
            version: "1.0",
            sessionAttributes: sessionAttributes,
            response: speechletResponse
        };
    }
    
    1. 转到netatmo的开发者网站(https://dev.netatmo.com/)并创建一个新应用程序。这将是您在Netatmo端传感器数据的接口。该应用程序将具有唯一的id(即:5653769769f7411515036a0b)和客户机密码(即:T4nHevTcRbs053TZsoLZiH1AFKLZGb83Fmw9q)。 (不,这些数字不代表有效的客户ID和秘密,它们仅用于演示目的)

    2. 在上面的代码中填写所需的凭据(netatmo帐户用户和通行证,客户ID和密码)。

    3. 转到亚马逊应用和服务(https://developer.amazon.com/edw/home.html)。在菜单中,选择Alexa,然后选择Alexa Skills Kit(点击“开始使用”)

    4. 现在您需要创建一个新技能。给你的技能一个名字和调用。该名称将用于调用(或启动)应用程序。在Endpoint字段中,您需要提供之前创建的lambda函数的ARN id。这个号码可以在右上角显示lambda功能的网页上找到。它应该是这样的:arn:aws:lambda:us-east-1:255569121831:function:[你的函数名]。完成此步骤后,左侧将显示绿色复选标记以指示进度(进度菜单)。

    5. 下一阶段涉及设置交互模型。它负责将话语映射到意图和插槽。 首先是意图架构。这是我的;复制粘贴此代码(并根据需要进行修改):

          {
      "intents": 
          [
              {
                  "intent": "AskTemperature",
                  "slots": [
                          {
                          "name": "Location",
                          "type": "LIST_OF_LOCATIONS"
                          }
                  ]
              },
      
              {
                  "intent": "AskCarbonDioxide",
                  "slots": [
                          {
                          "name": "Location",
                          "type": "LIST_OF_LOCATIONS"
                          }
                  ]
              },
               {
                  "intent": "AskHumidity",
                  "slots": [
                          {
                          "name": "Location",
                          "type": "LIST_OF_LOCATIONS"
                          }
                  ]
              },
      
              {
                  "intent": "AskRain",
                  "slots": []
              },
      
              {
                  "intent": "AskSound",
                  "slots": []
              },
              {
                  "intent": "AskWind",
                  "slots": []
              },
      
              {
                  "intent": "AskPressure",
                  "slots": []
              }
      
      
          ]
      }
      
    6. 接下来,自定义插槽类型。单击“添加插槽类型”。为插槽命名

          LIST_OF_LOCATIONS and newline-separated : DefaultAll, Inside, Outside, Living, Bedroom, Kitchen, Bathroom, Alpha, Beta 
      

      (用换行符替换逗号)

      接下来,示例:

          AskTemperature what's the temperature {Location}
          AskTemperature what's the temperature in {Location}
          AskTemperature what's the temperature in the {Location}
          AskTemperature get the temperature {Location}
          AskTemperature get the temperature in {Location}
          AskTemperature get the temperature in the {Location}
      
          AskCarbonDioxide what's the comfort level {Location}
          AskCarbonDioxide what's the comfort level in {Location}
          AskCarbonDioxide what's the comfort level in the {Location}
      
          AskCarbonDioxide get the comfort level {Location}
          AskCarbonDioxide get the comfort level in {Location}
          AskCarbonDioxide get the comfort level in the {Location}
      
      
          AskHumidity what's the humidity {Location}
          AskHumidity what's the humidity in {Location}
          AskHumidity what's the humidity in the {Location}
          AskHumidity get the humidity {Location}
          AskHumidity get the humidity from {Location}
          AskHumidity get the humidity in {Location}
          AskHumidity get the humidity in the {Location}
          AskHumidity get humidity
      
      
          AskRain is it raining 
          AskRain did it rain
          AskRain did it rain today
          AskRain get rain millimeter count
          AskRain get rain
      
          AskSound get sound level
          AskSound tell me how loud it is
      
          AskWind is it windy 
          AskWind get wind
          AskWind get wind measures
          AskWind get direction
          AskWind get speed
      
          AskPressure get pressure
          AskPressure what's the pressure
      
      1. 测试,说明和发布信息可以留空,除非您打算将您的技能发送到亚马逊,以便公开发布。我把我的空白留了下来:)

      2. 几乎就在那里。你只需要启用新技能。转到http://alexa.amazon.com/,然后在左侧菜单中选择技能。找到您的技能并单击启用。

      3. 真是太棒了。说" Alexa,打开[你的技能名称]。"默认情况下,室内和室外温度应从netatmo云中提取并由Alexa大声读出。你也可以说" Alexa,打开[你的技能名称]并在卧室里获得温度。"。正如您已经注意到的那样,部分"得到[位置]"的温度对应于您之前填写的示例文件。

      4. 活得长久繁荣

      5. 对不起这篇长篇文章感到抱歉。我希望这个小教程/演练有一天会对某人有所帮助。 :)