[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. Basically, I need to connect to the Netatmo cloud via http request.
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"}. 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 :
Get a free account for 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. The skill requires 1-3 seconds to run at a time.
Set up a new lambda function in Amazon Web Services (AWS). 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
};
}
Go to netatmo's developer site (https://dev.netatmo.com/) and create a new application. This will be your interface to the sensor data on the Netatmo side. The apllication will have a unique id (i.e: 5653769769f7411515036a0b) and client secret (i.e: T4nHevTcRbs053TZsoLZiH1AFKLZGb83Fmw9q). (No, these numbers do not represent a valid client id and secret, they are only for demonstration purposes)
Fill in the required credentials (netatmo account user and pass, client id and secret) in the code above.
Go to 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)
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. This number can be found on the webpage displaying your lambda function, on the top right corner. It should be something like : arn:aws:lambda:us-east-1:255569121831:function:[your function name]. Once you completed this step, a green checkmark will appear to the left to indicate progress (progress menu).
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
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. :)
Almost there. You just need to enable the new skill. Go to http://alexa.amazon.com/ and in the left menu, select Skills. Find your skill and click enable.
That awesome moment. Say "Alexa, open [your skill name]." By default, the indoor and outdoor temperature should be fetched from the netatmo cloud and read out loud by Alexa. you can also say "Alexa, open [your skill name] and get the temperature in the bedroom.". As you already might have noticed, the part " get the temperature in the [Location]" corresponds to the sample uttereces you filled in earlier.
Live long and prosper
Well, sorry for the long post. I hope this small tutorial/walkthrough will someday be helpful to somebody. :)
来源:https://stackoverflow.com/questions/33859826/linking-netatmo-weather-station-to-amazon-echo-alexa