Alexa smart home skill: problem with discover devices - node.js

I have problems with the discovery of devices of my Alexa Smart Home skill.
Steps which work:
activate Alexa skill
OAuth login screen appears. After successful login, the discovery of devices is triggered
in the lambda function I get the bearer token which I use to call the .../devices endpoint
I get the devices from the REST endpoint and construct the payload as described in https://developer.amazon.com/de/docs/smarthome/steps-to-build-a-smart-home-skill.html
The payload (same structure as in the example) is provided to context.succeed
My problem:
After the Alexa Skill returns from discovery of devices task, no new devices are visible in the Alexa Skill.
When I use the code from the sample (where no request to an external Rest API happens), the device is visible in the Alexa skill after the Alexa discovery task.
var https = require('https');
const AWS = require('aws-sdk');
exports.handler = function(request, context) {
var options = {
method: 'GET',
hostname: 'xyz.azurewebsites.net',
path: '/devices',
headers: {
Authorization: 'Bearer ' + request.directive.payload.scope.token,
'Content-Type': 'application/json'
}
};
var req = https.get(options, (response) => {
var data = '';
response.setEncoding('utf8');
response.on('data', function(x) { data += x; } );
response.on('error', console.error);
response.on('end', () => {
var dataObj = JSON.parse(data);
console.log("Retrieved response: " + JSON.stringify(dataObj.items));
const payload = {
"endpoints": []
};
dataObj.items.forEach(item => {
const device = {
"endpointId": item.id,
"manufacturerName": item.manufacturer,
"friendlyName": item.displayName,
"description": item.description,
"displayCategories": ["SWITCH"],
"cookie": {
"key1": "arbitrary key/value pairs for skill to reference this endpoint.",
"key2": "There can be multiple entries",
"key3": "but they should only be used for reference purposes.",
"key4": "This is not a suitable place to maintain current endpoint state."
},
"capabilities":
[
{
"type": "AlexaInterface",
"interface": "Alexa",
"version": "3"
},
{
"interface": "Alexa.PowerController",
"version": "3",
"type": "AlexaInterface",
"properties": {
"supported": [{
"name": "powerState"
}],
"retrievable": true
}
}
]
};
payload.endpoints.push(device);
});
console.log('payload ' + JSON.stringify(payload));
var header = request.directive.header;
header.name = "Discover.Response";
console.log("DEBUG", "Discovery Response: ", JSON.stringify({ header: header, payload: payload }));
//NEXT LINE IS EXECUTED WITHOUT ANY ERROR
context.succeed({ event: { header: header, payload: payload } });
});
});
req.on('error', (e) => {
console.log('problem with request: ' + e.message);
});
};

I found the problem...
The value of the property 'endpointId' contained a '#'. Then I changed the name to only letters, and it worked.
Although in this article it says '#' can be used, the discovery of devices then has problems.
Hope this answer helps others from wasting time...

I found another cause for the same symptom: for the entity's additionalAttributes (manufacturer, model etc.), one cannot use non-English characters. You can actually use any character ASCII from 32 to 126 (space to tilde), but you cannot use the backslash. So, no accent characters (international or extended ASCII) allowed.
On the other hand, I could include a entity with '#' inside its endpointId. I cannot explain why you couldn't.

Related

Azure Cognitve Search upload document via browser cors issue

I want to use the cognitive search to store usernames and levels of a browser game. There should be an option to create a new username in the web frontend. The idea is, that the user enters a username and submits via button. Then these username should be saved in a cognitive search document.
MyiIssue: I am getting a cors error in browser console:
Access to XMLHttpRequest at 'https://XXXXXXX.search.windows.net/indexes/users/docs/index' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
The same issue occurs when I deploy it to a azure static web app.
The CORS settings at azure allow all origins.
Regarding to Azure Search and CORS issue with PUT, it is not possible to modify the data in this way. I am new to programming, how can it be done in another way (serverless function?)?
My code:
OnSubmitCreate(){
console.log('Create started')
let data = JSON.stringify({
"value": [
{
"#search.action": "mergeOrUpload",
"id": this.createName,
"username": this.createName,
"level": 1
}
]
});
let config = {
method: 'post',
url: 'https://XXXXXXXXXXX.search.windows.net/indexes/users/docs/index?api-version=2020-06-30',
headers: {
'api-key': 'XXXXXXXXXXXXXXXXXxx',
'Content-Type': 'application/json'
},
data : data
};
Thanks in advance!
I`ve solved it with creating a azure function using javascript. When I call the URL, the write to cognitive search will be done.
So I technically am doing a http rest call (push).
index.js
var https = require('https');
module.exports = async function (context, req) {
context.log('JavaScript HTTP trigger function processed a request.');
var username = context.bindingData.username;
var level = context.bindingData.level;
let data = JSON.stringify({
"value": [
{
"#search.action": "mergeOrUpload",
"id": username,
"username": username,
"level": level
}
]
})
var options = {
host: 'XXXXXXXXXXX.search.windows.net',
path: '/indexes/users/docs/index?api-version=2020-06-30',
port: 443,
method: 'POST',
headers: {
'api-key': 'XXXXXXXXXXXX',
'Content-Type': 'application/json'
},
body: data
};
var req = https.request(options, function(res) {
console.log('STATUS: ' + res.statusCode);
console.log('HEADERS: ' + JSON.stringify(res.headers));
res.setEncoding('utf8');
res.on('data', function (chunk) {
console.log('BODY: ' + chunk);
});
});
req.on('error', function(e) {
console.log('problem with request: ' + e.message);
});
// write data to request body
req.write(data);
req.end();
context.res = {
// status: 200, /* Defaults to 200 */
body: req
};
}
function.json
{
"bindings": [
{
"authLevel": "function",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": [
"get",
"post"
],
"route": "data/{username:alpha}/{level:int?}"
},
{
"type": "http",
"direction": "out",
"name": "res"
}
]
}

Alexa Smart Home Skill - Need help to make an http request

I'm trying to build a home automation system by myself. For this, I use the Blynk server to control my hardware. I can control my hardware by requesting the URL of the blynk server.
For example: When I make a request to https://139.59.206.133/myprivatekey/update/V1?value=1 and update a Virtual Pin to "1" and my light turns on.
I used this to make an Alexa custom skill which is able to turn my light on by making an HTTPS request to the Blynk server. For example when I say "Alexa ask my Room to turn the light on". The custom Skill is working how it should.
But a Custom skill is not really what I was looking for, so I decided to build an Alexa Smart Home Skill. So I set one up and simply tried to make an HTTPS request when "TurnON" or "TurnOFF" is called.
My problem is that every time I try to make an HTTPS request Alexa says that the device is not responding.
I tried a lot of things, but I couldn't solve my problem on my own.
Code:
The Code is from the node.js Alexa Smart Home Example.
exports.handler = function (request, context) {
if (request.directive.header.namespace === 'Alexa.Discovery' && request.directive.header.name === 'Discover') {
log("DEBUG:", "Discover request", JSON.stringify(request));
handleDiscovery(request, context, "");
}
else if (request.directive.header.namespace === 'Alexa.PowerController') {
if (request.directive.header.name === 'TurnOn' || request.directive.header.name === 'TurnOff') {
log("DEBUG:", "TurnOn or TurnOff Request", JSON.stringify(request));
handlePowerControl(request, context);
}
}
function handleDiscovery(request, context) {
var payload = {
"endpoints":
[
{
"endpointId": "demo_id",
"manufacturerName": "Smart Device Company",
"friendlyName": "Zimmerlicht",
"description": "Smart Device Switch",
"displayCategories": ["SWITCH"],
"cookie": {
"key1": "arbitrary key/value pairs for skill to reference this endpoint.",
"key2": "There can be multiple entries",
"key3": "but they should only be used for reference purposes.",
"key4": "This is not a suitable place to maintain current endpoint state."
},
"capabilities":
[
{
"type": "AlexaInterface",
"interface": "Alexa",
"version": "3"
},
{
"interface": "Alexa.PowerController",
"version": "3",
"type": "AlexaInterface",
"properties": {
"supported": [{
"name": "powerState"
}],
"retrievable": true
}
}
]
}
]
};
var header = request.directive.header;
header.name = "Discover.Response";
log("DEBUG", "Discovery Response: ", JSON.stringify({ header: header, payload: payload }));
context.succeed({ event: { header: header, payload: payload } });
}
function log(message, message1, message2) {
console.log(message + message1 + message2);
}
function handlePowerControl(request, context) {
// get device ID passed in during discovery
var requestMethod = request.directive.header.name;
var responseHeader = request.directive.header;
responseHeader.namespace = "Alexa";
responseHeader.name = "Response";
responseHeader.messageId = responseHeader.messageId + "-R";
// get user token pass in request
var requestToken = request.directive.endpoint.scope.token;
var powerResult;
if (requestMethod === "TurnOn") {
const Http = new XMLHttpRequest();
const url='https://139.59.206.133/myprivatekey/update/V1?value=1';
Http.open("GET", url);
Http.send();
// Make the call to your device cloud for control
// powerResult = stubControlFunctionToYourCloud(endpointId, token, request);
powerResult = "ON";
}
else if (requestMethod === "TurnOff") {
var xmlHttp = new XMLHttpRequest();
xmlHttp.open( "GET", 'http://myprivatekey/update/V0?value=0', false ); // false for synchronous request
xmlHttp.send( null );
// Make the call to your device cloud for control and check for success
// powerResult = stubControlFunctionToYourCloud(endpointId, token, request);
powerResult = "OFF";
}
var contextResult = {
"properties": [{
"namespace": "Alexa.PowerController",
"name": "powerState",
"value": powerResult,
"timeOfSample": "2017-09-03T16:20:50.52Z", //retrieve from result.
"uncertaintyInMilliseconds": 50
}]
};
var response = {
context: contextResult,
event: {
header: responseHeader,
endpoint: {
scope: {
type: "BearerToken",
token: requestToken
},
endpointId: "demo_id"
},
payload: {}
}
};
log("DEBUG", "Alexa.PowerController ", JSON.stringify(response));
context.succeed(response);
}
};
My Requests:
if (requestMethod === "TurnOn") {
const Http = new XMLHttpRequest();
const url='https://139.59.206.133/myprivatekey/update/V1?value=1';
Http.open("GET", url);
Http.send();
// Make the call to your device cloud for control
// powerResult = stubControlFunctionToYourCloud(endpointId, token, request);
powerResult = "ON";
}
else if (requestMethod === "TurnOff") {
var xmlHttp = new XMLHttpRequest();
xmlHttp.open( "GET", 'http://myprivatekey/update/V0?value=0', false ); // false for synchronous request
xmlHttp.send( null );
// Make the call to your device cloud for control and check for success
// powerResult = stubControlFunctionToYourCloud(endpointId, token, request);
powerResult = "OFF";
}
Additional Info:
Programming language node.js
account linking, IAM role, and everything to create a smart home skill should be set up right
I am rather new to Alexa Skill building, and I'm also not very good at JavaScript.
If the request went through, I mean your code is being called, and light is being turned on/off, the problem is with the response.
My guess is that you are missing correlation id.
you need to add something like:
responseHeader.correlationToken = request.directive.header.correlationToken;
You should also implement the EndpointHealth: https://developer.amazon.com/en-US/docs/alexa/device-apis/alexa-endpointhealth.html

PUT API request returning JSON body error

Im trying to update a distribution list by sending a put request, when I run this code and test it in postman by giving it a JSON body I get this error in my node.js terminal stating SyntaxError: Unexpected end of JSON input ... any idea what I should change?
My PUT API request
app.put("/api/Dls/Add/:groupId" , (req, res) => {
const response = {
success: false
};
if (Authorized.myToken) {
response.success = true;
response.data = {};
var options = {
method: 'PUT',
url: 'https://SomeAPI.com/' + req.params.groupId,
headers:
{
Accept: 'application/json',
Authorization: 'Bearer' + ' ' + Authorized.myToken
},
body: JSON.stringify(req.body)
};
request(options, function (error, response, body){
if (error) {
console.log(error);
return;
}
const data = response.body;
const dls = JSON.parse(data)
return res.json(dls);
});
}
});
JSON body I'm passing through postman to test the API call
{
"groupId": "123456789",
"SomeField1": null,
"SomeField2": "xxxxxxxxx",
"SomeField3": true,
"SomeField4": "xxxxxxxxx",
"SomeField5": "xxxxxxxxx",
"SomeField6": [
"xxxxxxxxx"
],
"SomeField7": "xxxxxxxxx",
"SomeField8": "xxxxxxxxx",
"SomeField9": "xxxxxxxxx",
"SomeField10": "xxxxxxxxx",
"SomeField11": [],
"SomeField12": "xxxxxxxxx",
"SomeField13": null,
"SomeField14": false,
"SomeField15": ["xxxxxxxxx"]
}
Any feedback is appreciated!
If the JSON that you posted here is the real one that you pass via postman then, it is not the valid JSON as you have the same name properties. When I say valid it means you get something like this after posting to the endpoint.
{
"groupId": "123456789",
"SomeField": [
"xxxxxxxxx"
]
}
Request npm package is also deprecated so it is better to not use it and replace it with something like Axios. TBH I did not see any error in the code that causes the error that you mentioned, do you have access to the API to check the logs? Maybe something went wrong on the https://SomeAPI.com/ endpoint.
I figured out what the issue was, I needed to add the .end to the return statement
ex. return res.status(200).end()

GMail API using POST request, error code: 400, recipient address required

I am trying to send an email using POST request with just Node standard modules in NodeJS v8.10 via GMail API.
The sample code is given below.
I am getting an error:
{
"error": {
"errors": [
{
"domain": "global",
"reason": "invalidArgument",
"message": "Recipient address required"
}
],
"code": 400,
"message": "Recipient address required"
}
}
It says recipient address required but according to what I think, (I may be wrong), my base64url conversion is proper since I checked it in Google API Explorer but my problem is in passing the data, I am doing it properly as told in the guide i.e. inside body with 'raw' key but it still does not work and this is where my problem must be. Maybe I am missing something, maybe I do not know the proper structure.
Yes, there are multiple posts regarding this but none of them provided solution.
I referred the guide on https://developers.google.com/gmail/api/v1/reference/users/messages/send
but the given example is of with the use of client library.
I tried everything, passing the 'raw' with base64url encoded data into write function of the request, passing it as a data parameter in options, passing it through body parameters in options, everything I can think of.
Am I missing something? Where am I going wrong?
I am a newbie in nodejs so please explain and if possible, an example structure of solution would be most welcome.
Base64url produced is working fine, I guess. I copied the string produced by conversion and tried it at https://developers.google.com/gmail/api/v1/reference/users/messages/send?apix=true
It works fine and sends me the mail but it does not work on my code.
var email = (
"Content-Type: text/plain; charset=\"UTF-8\"\n" +
"Content-length: 5000\n" +
"MIME-Version: 1.0\n" +
"Content-Transfer-Encoding: message/rfc2822\n" +
"to: something#something.com\n" +
"from: \"Some Name\" <something#gmail.com>\n" +
"subject: Hello world\n\n" +
"The actual message text goes here"
);
async function sendMail(token,resp) {
return new Promise((resolve,reject) => {
var base64EncodedEmail = Buffer.from(email).toString('base64');
var base64urlEncodedEmail = base64EncodedEmail.replace(/\+/g, '-').replace(/\//g, '_');
var params = {
userId: 'me',
resource: {
'raw': base64urlEncodedEmail
}
};
var body2 = {
"raw": base64urlEncodedEmail,
}
var options = {
hostname: 'www.googleapis.com',
path:'/upload/gmail/v1/users/me/messages/send',
headers: {
'Authorization':'Bearer '+token,
'Content-Type':'message/rfc822',
},
body: {
"raw": base64urlEncodedEmail,
'resource': {
'raw': base64urlEncodedEmail,
}
},
data: JSON.stringify({
'raw': base64urlEncodedEmail,
'resource': {
'raw': base64urlEncodedEmail,
}
}),
message: {
'raw': base64urlEncodedEmail,
},
payload: {
"raw": base64urlEncodedEmail, //this is me trying everything I can think of
},
// body: raw,
// }
userId: 'me',
// resource: {
// 'raw': base64urlEncodedEmail
// },
method: 'POST',
};
var id='';
console.log(base64urlEncodedEmail);
const req = https.request(options, (res) => {
var body = '';
res.on('data', (d) => {
body += d;
});
res.on('end', () => {
var parsed = body;
console.log(parsed);
})
});
req.on('error', (e) => {
console.error(e);
});
req.write(JSON.stringify(body2));
req.end();
});
};
Thank you for your time and answers.
I found the solution.
It says everywhere to convert the rfc822 formatted string to Base64url to send and attach it to 'raw' property in the POST body but I don't know what has changed and you don't need to do that anymore.
First things first, the Content-Type in header should be
'Content-Type':'message/rfc822'
Now, since we are specifying the content-type as message/rfc822, we don't need to convert the data we want to send into base64url format anymore, I guess (Not sure of the reason because I have a very little knowledge about this.)
Only passing "To: something#any.com" as body works.
Here is the complete code of how to get it done for someone who is struggling for the same problem.
function makeBody(to, from, subject, message) {
let str = [
"to: ", to, "\n",
"from: ", from, "\n",
"subject: ", subject, "\n\n",
message,
].join('');
return str;
}
async function getIdAsync(token,resp) {
return new Promise((resolve,reject) => {
let raw = makeBody("something#gmail.com", "something#gmail.com", "Subject Here", "blah blah blah");
var options = {
hostname: 'www.googleapis.com',
path:'/upload/gmail/v1/users/me/messages/send',
headers: {
'Authorization':'Bearer '+token,
'Content-Type':'message/rfc822'
},
method: 'POST',
};
const req = https.request(options, (res) => {
var body = '';
res.on('data', (d) => {
body += d;
});
res.on('end', () => {
var parsed = body;
console.log(parsed);
})
});
req.on('error', (e) => {
console.error(e);
});
req.write(raw);
req.end();
});
};
Happy Coding :)

HTTP Post request not going through in AWS Lambda

I am trying to add an item to my Todoist project through an Alexa skill in AWS Lambda. I am very new to all of these technologies so forgive me if the fix is incredibly obvious. When I ask Alexa to invoke my addZipcode skill, it fails. This is what I have (excluding some stuff that is in all Alexa Lambda functions):
Alexa stuff
...
const handlers = {
'LaunchRequest': function() {
this.emit('AMAZON.HelpIntent');
},
'addZipcode': function() {
const newZip = this.event.request.intent.slots.zipcode.value;
const speechOutput = newZip;
var http = require("https");
function postZip(newZip) {
var options = {
"method": "POST",
"hostname": [
"beta",
"todoist",
"com"
],
"path": [
"API",
"v8",
"tasks"
],
"headers": {
"Content-Type": "application/json",
"Authorization": "Bearer " + token
}
};
var req = http.request(options, function(res) {
var chunks = [];
res.on("data", function(chunk) {
chunks.push(chunk);
});
res.on("end", function() {
var body = Buffer.concat(chunks);
console.log(body.toString());
});
});
req.write(JSON.stringify({ content: newZip, project_id: XXXXXXXXXX }));
req.end();
}
postZip(newZip);
this.response.cardRenderer(SKILL_NAME, newZip);
this.response.speak(speechOutput);
this.emit(':responseReady');
},
.... cont
I get the resulting error when I try to run the skill with Alexa:
Response:
{
"errorMessage": "hostHeader.replace is not a function",
"errorType": "TypeError",
"stackTrace": [
"Agent.addRequest (_http_agent.js:130:39)",
"new ClientRequest (_http_client.js:159:16)",
"Object.exports.request (http.js:31:10)",
"Object.exports.request (https.js:199:15)",
"postZip (/var/task/index.js:72:28)",
"Object.addZipcode (/var/task/index.js:88:9)",
"emitNone (events.js:86:13)",
"AlexaRequestEmitter.emit (events.js:185:7)",
"AlexaRequestEmitter.EmitEvent (/var/task/node_modules/alexa-sdk/lib/alexa.js:216:10)",
"AlexaRequestEmitter.ValidateRequest (/var/task/node_modules/alexa-sdk/lib/alexa.js:181:23)"
]
}
I tried searching for more information about hostHeader.replace or even just hostHeader but to no avail. When I surround my postZip function with
exports.handler = function(event, context, callback) {}
the skill actually works, but the Post request does not go through (as in, the new zipcode is not added as a new task on my Todoist). I'm pretty sure the Post request code itself is correct because I ran it through Postman and the zipcode was added.
Please help me understand why it doesn't work.
It's hard to tell what causes that error. But the node docs say, that hostname as well as path are supposed to be nothing but strings and not arrays as it's the case in your code.
So what I'd do first is to change your code to this:
var options = {
"method": "POST",
"hostname": "beta.todoist.com",
"path": "/API/v8/tasks",
"headers": {
"Content-Type": "application/json",
"Authorization": "Bearer " + token
}

Resources