Связывание метеостанции Netatmo с Amazon Echo (Alexa)

[Полное руководство в ответе на вопрос ниже. Приветствуется обратная связь!]

Я пытаюсь создать функцию AWS Lambda, чтобы использовать навык Amazon Alexa для получения информации о погоде с моей метеостанции Netatmo. По сути, мне нужно подключиться к облаку Netatmo через http-запрос.

Вот фрагмент моего кода, http-запрос выполняется для токена временного доступа, запрос в порядке, но тело результата — тело: {"error":"invalid_request"}. В чем здесь может быть проблема?

var clientId = "";
var clientSecret = "";
var userId="[email protected]"; 
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();
}

person Mihai Galos    schedule 22.11.2015    source источник
comment
Можете ли вы протестировать вызов API из какой-либо другой системы (например, непосредственно с вашего компьютера, а не через Lambda)? Отправьте тот же body, посмотрите, работает ли.   -  person John Rotenstein    schedule 23.11.2015
comment
Я могу подтвердить правильность учетных данных, используя http://http-post.com/ и заполнив необходимые параметры. Запрос генерирует нужный токен...   -  person Mihai Galos    schedule 23.11.2015
comment
может кодировка не та? Это может объяснить, почему в ответе нет дополнительной информации, потому что исходный запрос даже не может быть проанализирован!   -  person Mihai Galos    schedule 24.11.2015


Ответы (1)


Я запустил! Вот краткое руководство:

  1. Получите бесплатную учетную запись для Amazon AWS. Пока ваш навык не работает постоянно (вам будет выставляться счет за время работы и ресурсы, использованные на серверах AWS, примерно 700 часов бесплатных часов в месяц), у вас должно быть все в порядке, и он останется бесплатным. Навык требует 1-3 секунды для запуска за раз.

  2. Настройте новую лямбда-функцию в Amazon Web Services (AWS). Эта функция будет выполняться каждый раз при вызове навыка.

Вот код навыка:

/**
*   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. Приложение будет иметь уникальный идентификатор (например, 5653769769f7411515036a0b) и секрет клиента (например, T4nHevTcRbs053TZsoLZiH1AFKLZGb83Fmw9q). (Нет, эти числа не представляют действительный идентификатор и секрет клиента, они предназначены только для демонстрационных целей)

  2. Введите необходимые учетные данные (пользователь и пароль учетной записи netatmo, идентификатор клиента и секрет) в приведенном выше коде.

  3. Перейдите на страницу приложений и служб Amazon (https://developer.amazon.com/edw/home.html). ). В меню выберите Alexa, а затем Alexa Skills Kit (нажмите «Начать»)

  4. Теперь вам нужно создать новый навык. Дайте вашему навыку имя и вызов. Имя будет использоваться для вызова (или запуска) приложения. В поле «Конечная точка» вам нужно указать ARN-идентификатор вашей лямбда-функции, созданной ранее. Этот номер можно найти на веб-странице, отображающей вашу лямбда-функцию, в правом верхнем углу. Это должно быть что-то вроде: 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": []
            }
    
    
        ]
    }
    

Далее, пользовательские типы слотов. Нажмите «Добавить тип слота». Дайте слоту имя

    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. Информацию о тестировании, описании и публикации можно оставить пустыми, если только вы не планируете отправить свой навык на amazon, чтобы сделать его общедоступным. Я оставил свой пустым. :)

  2. Почти готово. Вам просто нужно включить новый навык. Перейдите на страницу http://alexa.amazon.com/ и в меню слева выберите Навыки. Найдите свой навык и нажмите «Включить».

  3. Тот потрясающий момент. Скажите «Алекса, открой [название твоего навыка]». По умолчанию температура в помещении и на улице должна быть получена из облака netatmo и прочитана Alexa вслух. вы также можете сказать «Алекса, открой [название твоего навыка] и измерь температуру в спальне». Как вы уже могли заметить, часть «получить температуру в [местоположении]» соответствует образцу, который вы заполнили ранее.

  4. Живи долго и процветай

Ну извините за длинный пост. Я надеюсь, что это небольшое руководство/пошаговое руководство когда-нибудь будет кому-нибудь полезно. :)

person Mihai Galos    schedule 28.11.2015
comment
Спасибо за этот урок. Я создал немецкую версию на github: github.com/peerdavid/netatmo-skill - person David Peer; 28.12.2016
comment
Привет, Дэвид. Füge mal bitte einen Link in Github zu der Ursprungsquelle hinzu. Danke und schöne Grüße, Михай. - person Mihai Galos; 28.12.2016
comment
Привет, Михай, я нашел его на GitHub с благодарностью за ссылку на StackOverflow. Sollte ich noch etwas hinzufügen bzw. Спасибо umbenennen? - person David Peer; 29.12.2016
comment
Привет, Дэвид. Аллес прима! Данке и счастливого кодирования! :) - person Mihai Galos; 29.12.2016
comment
Я обновил doCall(), потому что иногда у меня возникала проблема с ответом связанного навыка от Alexa. Проблема в том, что когда ответ слишком длинный, он разбивается на куски. Нам нужно объединить эти фрагменты перед их JSON-анализом, чего раньше не было. Таким образом, в случае одного незавершенного синтаксического анализа фрагмента синтаксический анализ завершится ошибкой. - person Mihai Galos; 06.01.2017