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

Posted

技术标签:

【中文标题】将 Netatmo 气象站连接到 Amazon Echo (Alexa)【英文标题】:Linking Netatmo Weather Station to Amazon Echo (Alexa) 【发布时间】:2016-02-24 21:32:00 【问题描述】:

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

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

这是我的代码的 sn-p,http 请求是针对临时访问令牌完成的,请求正常但结果正文是 body: "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();

【问题讨论】:

您能否测试来自其他系统的 API 调用(例如直接从您的计算机,而不是通过 Lambda)?发送相同的body,看看是否有效。 我可以通过使用http-post.com 并填写必要的参数来确认凭据是否正确。请求生成所需的令牌... 会不会是编码错误?它可能会澄清为什么响应没有附加信息——因为原始请求甚至无法解析! 【参考方案1】:

我让它运行起来了! 这是一个快速演练:

    获取 Amazon AWS 的免费帐户。只要您的技能不是一直在运行(您将按 AWS 服务器上使用的运行时间和资源计费,每月大约有 700 个免费小时),您应该会很好并且它将保持免费。该技能一次运行需要1-3秒。

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

技能代码如下:

/**
*   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
    ;

    转到 netatmo 的开发人员站点 (https://dev.netatmo.com/) 并创建一个新应用程序。这将是您与 Netatmo 端传感器数据的接口。该应用程序将具有唯一的 ID(即:5653769769f7411515036a0b)和客户端密码(即:T4nHevTcRbs053TZsoLZiH1AFKLZGb83Fmw9q)。 (不,这些数字不代表有效的客户 ID 和密码,它们仅用于演示目的)

    在上面的代码中填写所需的凭据(netatmo 帐户用户和密码、客户端 ID 和密码)。

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

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

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

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

接下来,自定义插槽类型。单击添加插槽类型。给插槽命名

    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

    测试、描述和发布信息可以留空,除非您计划将您的技能发送到亚马逊以便公开。我把我的留空了。 :)

    差不多了。您只需要启用新技能。转到http://alexa.amazon.com/,然后在左侧菜单中选择技能。找到您的技能并单击启用。

    那令人敬畏的时刻。说“Alexa,打开 [你的技能名称]。”默认情况下,室内和室外温度应从 netatmo 云中获取并由 Alexa 大声读出。您也可以说“Alexa,打开 [您的技能名称] 并获取卧室的温度。”。您可能已经注意到,“在 [Location] 中获取温度”部分对应于您之前填写的示例话语。

    长命百岁

好吧,很抱歉这篇文章太长了。我希望这个小教程/演练有一天会对某人有所帮助。 :)

【讨论】:

感谢本教程。我在 github 上创建了一个德语版本:github.com/peerdavid/netatmo-skill 嗨,大卫。 Füge mal bitte einen 链接在 Github zu der Ursprungsquelle hinzu。 Danke und schöne Grüße,米海。 嗨 Mihai,我在 GitHub 上感谢 *** 上的链接。 Sollte ich noch etwas hinzufügen bzw。感谢umbenennen? 嗨,大卫。首要的! Danke 和快乐的编码! :) 我已经更新了 doCall(),因为我有时会收到来自 Alexa 的“连接技能的响应有问题”。问题是,当响应太长时,它会被分成块。我们需要在对它们进行 json-parse 之前连接这些块,而以前不是这样。因此,在单个不完整的块解析的情况下,解析将失败。

以上是关于将 Netatmo 气象站连接到 Amazon Echo (Alexa)的主要内容,如果未能解决你的问题,请参考以下文章

将 IntelliJ 连接到 Amazon Redshift

是否可以将alexa输出连接到amazon SQS

尝试使用 Postgres 连接到数据库,但无法将 Amazon 服务器上的地址转换为 Amazon 数据库

将 SQL Workbench/J 连接到 Amazon Athena 时出错

如何将 MySQL Workbench 连接到 Amazon RDS?

将 Openshift 上的 PostgreSQL 连接到 Amazon S3