简体   繁体   English

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

[英]Linking Netatmo Weather Station to Amazon Echo (Alexa)

[Full tutorial in the answered question below. [以下回答问题的完整教程。 Feedback welcome!] 欢迎反馈!]

I am trying to create an AWS Lambda function to use for an Amazon Alexa skill to fetch weather information from my Netatmo weatherstation. 我正在尝试创建一个AWS Lambda函数,用于Amazon Alexa技能从我的Netatmo weatherstation获取天气信息。 Basically, I need to connect to the Netatmo cloud via http request. 基本上,我需要通过http请求连接到Netatmo云。

Here's a snippet of my code, the http request is done for the temporary access token, the request is ok but the result body is body: {"error":"invalid_request"}. 这是我的代码片段,http请求是为临时访问令牌完成的,请求没问题,但结果正文是正文:{“error”:“invalid_request”}。 What could be the problem here? 这可能是什么问题?

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();
}

I got it running! 我跑了! Here's a quick walkthrough : 这是一个快速演练:

  1. Get a free account for Amazon AWS. 获取Amazon AWS的免费帐户。 As long as your skill is not constantly running (you will be billed by the run time and resources used on the AWS Servers with something like 700? free hours each month), you should be good and it will stay free. 只要您的技能不能持续运行(您将通过AWS服务器上使用的运行时间和资源(每月700个免费小时)收费,您应该保持良好状态并且保持免费。 The skill requires 1-3 seconds to run at a time. 该技能一次需要1-3秒才能运行。

  2. Set up a new lambda function in Amazon Web Services (AWS). 在Amazon Web Services(AWS)中设置新的lambda函数。 This function will execute every time the skill is invoked. 每次调用技能时都会执行此功能。

Here's the skill's code: 这是技能代码:

/**
*   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. Go to netatmo's developer site ( https://dev.netatmo.com/ ) and create a new application. 访问netatmo的开发者网站( https://dev.netatmo.com/ )并创建一个新的应用程序。 This will be your interface to the sensor data on the Netatmo side. 这将是您在Netatmo端传感器数据的接口。 The apllication will have a unique id (ie: 5653769769f7411515036a0b) and client secret (ie: T4nHevTcRbs053TZsoLZiH1AFKLZGb83Fmw9q). 应用程序将具有唯一的ID(即:5653769769f7411515036a0b)和客户端密钥(即:T4nHevTcRbs053TZsoLZiH1AFKLZGb83Fmw9q)。 (No, these numbers do not represent a valid client id and secret, they are only for demonstration purposes) (不,这些数字不代表有效的客户ID和秘密,它们仅用于演示目的)

  2. Fill in the required credentials (netatmo account user and pass, client id and secret) in the code above. 在上面的代码中填写所需的凭据(netatmo帐户用户和通行证,客户ID和秘密)。

  3. Go to Amazon Apps and Services ( https://developer.amazon.com/edw/home.html ). 转到Amazon Apps and Services( https://developer.amazon.com/edw/home.html )。 In the Menu, select Alexa and then Alexa Skills Kit (click on Get started) 在菜单中,选择Alexa,然后选择Alexa Skills Kit(点击开始使用)

  4. Now you need to create a new Skill. 现在您需要创建一个新技能。 Give your skill a name and invokation. 给你的技能一个名字和调用。 The name will be used to invoke (or start) the application. 该名称将用于调用(或启动)应用程序。 In the Endpoint field you need to give in the ARN id of your lambda function created earlier. 在Endpoint字段中,您需要提供之前创建的lambda函数的ARN id。 This number can be found on the webpage displaying your lambda function, on the top right corner. 这个号码可以在右上角显示lambda功能的网页上找到。 It should be something like : arn:aws:lambda:us-east-1:255569121831:function:[your function name]. 它应该是这样的:arn:aws:lambda:us-east-1:255569121831:function:[你的函数名]。 Once you completed this step, a green checkmark will appear to the left to indicate progress (progress menu). 完成此步骤后,左侧会出现绿色复选标记以指示进度(进度菜单)。

  5. The next phase involves setting up the interaction model. 下一阶段涉及建立交互模型。 It is responsible with mapping of utterences to intents and slots. 它负责将话语映射到意图和插槽。 First, the Intent Schema. 首先是意图架构。 Here's mine; 这是我的; copy-paste this code (and modify if needed): 复制粘贴此代码(并根据需要进行修改):

      { "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": [] } ] } 

Next, the Custom Slot Types. 接下来,自定义插槽类型。 Click on Add Slot Type. 单击“添加插槽类型”。 Give the slot the name 给插槽命名

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

(replace commas with newlines) (用换行符替换逗号)

Next, sample utterences: 接下来,样本说明:

    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. The Test, Description and Publishing information can be left blank, unless you plan to send your skill to amazon so as it can be made publicly available. 测试,描述和发布信息可以留空,除非您计划将您的技能发送到亚马逊,以便可以公开发布。 I left mine blank. 我把我的空白留了下来 :) :)

  2. Almost there. 快好了。 You just need to enable the new skill. 你只需要启用新技能。 Go to http://alexa.amazon.com/ and in the left menu, select Skills. 转到http://alexa.amazon.com/并在左侧菜单中选择技能。 Find your skill and click enable. 找到您的技能并单击启用。

  3. That awesome moment. 那个令人敬畏的时刻。 Say "Alexa, open [your skill name]." 说“Alexa,打开[你的技能名称]。” By default, the indoor and outdoor temperature should be fetched from the netatmo cloud and read out loud by Alexa. 默认情况下,室内和室外温度应从netatmo云中提取并由Alexa大声读出。 you can also say "Alexa, open [your skill name] and get the temperature in the bedroom.". 你也可以说“Alexa,打开[你的技能名称]并获得卧室的温度。” As you already might have noticed, the part " get the temperature in the [Location]" corresponds to the sample uttereces you filled in earlier. 正如您可能已经注意到的那样,“获取[位置]中的温度”部分对应于您之前填写的样本。

  4. Live long and prosper 健康长寿·繁荣昌盛

Well, sorry for the long post. 好吧,抱歉这篇长篇文章。 I hope this small tutorial/walkthrough will someday be helpful to somebody. 我希望这个小教程/演练有一天会对某人有所帮助。 :) :)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM