Jump to content

AWS Lambda Function REST Call to the Portal


btreinders

Recommended Posts

Posted

I have tried everything I found searching the internet with https calls in node.js to make a REST call to the portal from the AWS Lambda function and nothing will activate a program.  Does anyone know if this is possible?  I am using the URL from the portal with my username and password.  I am able to trigger a program from Chrome using the URL.  I don't know much about node.js so please be gentle.  :)  Thanks!

Posted

For sure, this is possible.

Here's untested code that could get you started. There are easier ways to make an http calls, using the request module for example, but this one below has the advantage that you can do inline editing of your lambda function, as opposed to always having to update your code via an upload.

 

"use strict"

const https = require('https');
const username = 'portal user'
const password = 'portal password'

https.globalAgent = new https.Agent({ keepAlive: true });

exports.handler = function (event, context) {
  // console.log('Input:', event);

  var options = {
    protocol: 'https:',
    hostname: 'my.isy.io',
    port: 443,
    path: '/set/your/path/here',
    method: 'GET',
    rejectUnauthorized: true,
    auth: username + ':' + password
  }
	
  var req = https.request(options);

  // when the response comes back
  req.on('response', function(res){
    res.body = '';
    res.setEncoding('utf-8');

    res.on('data', function(chunk) {
      res.body += chunk;
    });

    // when the response has finished
    res.on('end', function() {
      // console.log('DATA:', res.body)
      context.succeed(res.body);
    });

    res.on('error', function (err) {
      console.log('ERROR:', err);
      context.fail(JSON.stringify({ error: 'error' }));
    });
  });

  // write data to request body
  //req.write(body);
  req.end(); // Can't recall if you need this for a GET
}

 

If your lambda function only has to trigger a program, there's an easier and more secure way too. 

  1. In ISY Portal, go to Connectivity | IFTTT / Webhooks.
  2. Click "Set Key" to set an API key
  3. Create an entry for your program
  4. Click on the small blue icon, which will give you a URL

Use this URL to trigger the program. Please note that you need to use POST (Not GET)

  • Like 1
Posted

@bmercierThanks for help!  I tried this and I don't get any errors (it seems to execute fine) but the program in the ISY does not run.  I was seeing the same with other code from the web that I have tried including the request module.  I had also tried IFTTT before with the result.  Executes but the program does not run.  I have not tried the IFTTT way with this code because I was not sure what to do with the username and password.  Just leave them blank?  Any ideas why the rest call isn't working?  Thanks!

Posted

Uncomment this line and check the log:

// console.log('DATA:', res.body)

You may be getting an error message from ISY.

Also, what's the URL? Are you using runif or runthen?

If you use IFTTT, you don't need to pass a username or password. The key is what identifies you.

Benoit

Posted (edited)

@bmercierYes, it does get triggered, I put speech outputs after the call and it does execute them.  I am using speech outputs as debug but I do notice that it will only execute the first one it comes to.  I don't know if it stops at that point or continues to execute.  Right now I just have it say Made It right after the https code you gave me and it does say Made it.  Strangely if I do not have a speech output in there at all I get "There was a problem with the requested skill's response".  

What I am trying to accomplish is create a skill where Alexa will demo my home automation system.  It will ask the user what system do you want to demo or demo the whole house.  I would then have Alexa say something like, ok, demoing the lights, or here is a demo of cameras being cast to the TV, or I can lock the front door, etc....and make rest calls to execute the programs/devices/scenes etc.. in the ISY.

Edited by btreinders
Posted

I have not seen the code. But if you simply added code at the "end" to return some Alexa voice, then the call to portal probably does not go through at all.

If so, your code at the end probably does a context.succeed() which basically ends the lambda function, before the call to portal has a chance to go through.

Benoit

 

 

Posted

@bmercier  After more research I have found that the handler function is exiting because the this.emit is being executed. I have tried using async and promise with the same result.  The function is just exiting too soon.  Any ideas?  This is driving me crazy.  Thanks!

Posted (edited)
 // 1. Text strings =====================================================================================================
 //    Modify these strings and messages to change the behavior of your Lambda function


let speechOutput;
let reprompt;
let welcomeOutput = "Welcome to my home automation demo, You can ask me to demo the whole house, or just certain systems like lights, locks, cameras, audio video systems, ceiling fans, etc...";
let welcomeReprompt = "You can say something like demo the whole home, or demo the lights.  I will give you short demo of the system of your choice.";
// 2. Skill Code =======================================================================================================
"use strict";
const Alexa = require('alexa-sdk');
const APP_ID = undefined;  // TODO replace with your app ID (OPTIONAL).
speechOutput = '';
const handlers = {
	'LaunchRequest': function () {
		this.emit(':ask', welcomeOutput, welcomeReprompt);
	},
	'AMAZON.HelpIntent': function () {
		speechOutput = 'Placeholder response for AMAZON.HelpIntent.';
		reprompt = '';
		this.emit(':ask', speechOutput, reprompt);
	},
   'AMAZON.CancelIntent': function () {
		speechOutput = 'Placeholder response for AMAZON.CancelIntent';
		this.emit(':tell', speechOutput);
	},
   'AMAZON.StopIntent': function () {
		speechOutput = 'Ok, stopping my home demo';
		this.emit(':tell', speechOutput);
   },
   'SessionEndedRequest': function () {
		speechOutput = 'Demo Ended';
		//this.emit(':saveState', true);//uncomment to save attributes to db on session end
		this.emit(':tell', speechOutput);
   },
	'AMAZON.NavigateHomeIntent': function () {
		speechOutput = '';

		//any intent slot variables are listed here for convenience


		//Your custom intent handling goes here
		speechOutput = "This is a place holder response for the intent named AMAZON.NavigateHomeIntent. This intent has no slots. Anything else?";
		this.emit(":ask", speechOutput, speechOutput);
    },
	'playdemointent': function () {
	
	    var speechOutput = '';
        var speechReprompt = '';


		//any intent slot variables are listed here for convenience

		
		let system_typeSlot = resolveCanonical(this.event.request.intent.slots.system_type);
		console.log(system_typeSlot);
		
		let lengthSlot = resolveCanonical(this.event.request.intent.slots.length);
		console.log(lengthSlot);
		
		var demos = {
		    'lights' : {
		        'short' : '/isy/<isy address>/rest/programs/0024/runthen',
		        'long': '/isy/<isy address>/rest/programs/0024/runthen'
		    },
		    'cameras' : {
		        'short' : '/isy/<isy address>/rest/programs/013F/runThen',
		        'long': '/isy/<isy address>/rest/programs/013F/runThen'
		    },
		    'fans' : {
		        'short' : '/isy/<isy address>/rest/programs/0367/runThen',
		        'long': '/isy/<isy address>/rest/programs/0367/runThen'
		    },
		    'pool' : {
		        'short' : '/isy/<isy address>/rest/programs/02C9/runThen',
		        'long': '/isy/<isy address>/rest/programs/02C9/runThen'
		    },
		    'Lock' : {
		        'short' : '/isy/<isy address>/rest/programs/0439/runIf',
		        'long': '/isy/<isy address>/rest/programs/0439/runIf'
		    }
		}
		
		var lengths = [
            'short',
            'long'
        ];
        
        var demo = '';
        var length = '';
		
		if(!system_typeSlot) {
            this.emit(':ask', 'I did not hear a system. Which system do you want to demo?', 'You can say something like cameras, lights, audio video system, etc..');
        } else {
            demo = system_typeSlot.toLowerCase();
            this.attributes['demo'] = demo;
        }
        
        if(lengthSlot) {
            length = lengthSlot.toLowerCase();
        }

        
        if(demos[demo]) {

            if(lengthSlot && lengths.indexOf(lengthSlot) > -1) {
                length = lengthSlot;    
            } else {
                length = 'short';
            }
            
        const url = demos[demo][length];
        
        
     "use strict";
     const https = require('https');
     const username = 'xxxxxxx@hotmail.com';
     const password = 'xxxxxxxx';

     https.globalAgent = new https.Agent({ keepAlive: true });

     exports.handler = function (event, context) {
     console.log('Input:', event);

  var options = {
    protocol: 'https:',
    hostname: 'my.isy.io',
    port: 443,
    path: url,
    method: 'GET',
    rejectUnauthorized: true,
    auth: username + ':' + password
  };
//	console.log();
  var req = https.request(options);

  // when the response comes back
  req.on('response', function(res){
    res.body = '';
    res.setEncoding('utf-8');

    res.on('data', function(chunk) {
      res.body += chunk;
    });

    // when the response has finished
  
    res.on('end', function() {
       console.log('DATA:', res.body);
      
       context.succeed(res.body);
    });

    res.on('error', function (err) {
      console.log('ERROR:', err);
      context.fail(JSON.stringify({ error: 'error' }));
    });
  });
  
  

  // write data to request body
  //req.write(body);
  req.end(); // Can't recall if you need this for a GET
};
        
            
 speechOutput = 'Ok, here is a ' + length + ' demo of the ' + demo;
       this.emit(":tell", speechOutput);
   
  

        } else {
            speechOutput = 'Sorry, the system you asked for is not supported yet.';
            speechReprompt = 'I support lights, locks, ceiling fans, audio video systems, cameras, pool equipment, etc..';
        }
        
        this.emit(":tell", speechOutput);
    },	
	'Unhandled': function () {
        speechOutput = "The skill didn't quite understand what you wanted.  Do you want to try something else?";
        this.emit(':ask', speechOutput, speechOutput);
    }
};


exports.handler = (event, context) => {
    const alexa = Alexa.handler(event, context);
    alexa.appId = APP_ID;
    // To enable string internationalization (i18n) features, set a resources object.
    //alexa.resources = languageStrings;
    alexa.registerHandlers(handlers);
	//alexa.dynamoDBTableName = 'DYNAMODB_TABLE_NAME'; //uncomment this line to save attributes to DB
    alexa.execute();
};

//    END of Intent Handlers {} ========================================================================================
// 3. Helper Function  =================================================================================================

function resolveCanonical(slot){
	//this function looks at the entity resolution part of request and returns the slot value if a synonyms is provided
	let canonical;
    try{
		canonical = slot.resolutions.resolutionsPerAuthority[0].values[0].value.name;
	}catch(err){
	    console.log(err.message);
	    canonical = slot.value;
	};
	return canonical;
};

function delegateSlotCollection(){
  console.log("in delegateSlotCollection");
  console.log("current dialogState: "+this.event.request.dialogState);
    if (this.event.request.dialogState === "STARTED") {
      console.log("in Beginning");
	  let updatedIntent= null;
	  // updatedIntent=this.event.request.intent;
      //optionally pre-fill slots: update the intent object with slot values for which
      //you have defaults, then return Dialog.Delegate with this updated intent
      // in the updatedIntent property
      //this.emit(":delegate", updatedIntent); //uncomment this is using ASK SDK 1.0.9 or newer
	  
	  //this code is necessary if using ASK SDK versions prior to 1.0.9 
	  if(this.isOverridden()) {
			return;
		}
		this.handler.response = buildSpeechletResponse({
			sessionAttributes: this.attributes,
			directives: getDialogDirectives('Dialog.Delegate', updatedIntent, null),
			shouldEndSession: false
		});
		this.emit(':responseReady', updatedIntent);
		
    } else if (this.event.request.dialogState !== "COMPLETED") {
      console.log("in not completed");
      // return a Dialog.Delegate directive with no updatedIntent property.
      //this.emit(":delegate"); //uncomment this is using ASK SDK 1.0.9 or newer
	  
	  //this code necessary is using ASK SDK versions prior to 1.0.9
		if(this.isOverridden()) {
			return;
		}
		this.handler.response = buildSpeechletResponse({
			sessionAttributes: this.attributes,
			directives: getDialogDirectives('Dialog.Delegate', null, null),
			shouldEndSession: false
		});
		this.emit(':responseReady');
		
    } else {
      console.log("in completed");
      console.log("returning: "+ JSON.stringify(this.event.request.intent));
      // Dialog is now complete and all required slots should be filled,
      // so call your normal intent handler.
      return this.event.request.intent;
    }
}


function randomPhrase(array) {
    // the argument is an array [] of words or phrases
    let i = 0;
    i = Math.floor(Math.random() * array.length);
    return(array[i]);
}
function isSlotValid(request, slotName){
        let slot = request.intent.slots[slotName];
        //console.log("request = "+JSON.stringify(request)); //uncomment if you want to see the request
        let slotValue;

        //if we have a slot, get the text and store it into speechOutput
        if (slot && slot.value) {
            //we have a value in the slot
            slotValue = slot.value.toLowerCase();
            return slotValue;
        } else {
            //we didn't get a value in the slot.
            return false;
        }
}

//These functions are here to allow dialog directives to work with SDK versions prior to 1.0.9
//will be removed once Lambda templates are updated with the latest SDK

function createSpeechObject(optionsParam) {
    if (optionsParam && optionsParam.type === 'SSML') {
        return {
            type: optionsParam.type,
            ssml: optionsParam['speech']
        };
    } else {
        return {
            type: optionsParam.type || 'PlainText',
            text: optionsParam['speech'] || optionsParam
        };
    }
}

function buildSpeechletResponse(options) {
    let alexaResponse = {
        shouldEndSession: options.shouldEndSession
    };

    if (options.output) {
        alexaResponse.outputSpeech = createSpeechObject(options.output);
    }

    if (options.reprompt) {
        alexaResponse.reprompt = {
            outputSpeech: createSpeechObject(options.reprompt)
        };
    }

    if (options.directives) {
        alexaResponse.directives = options.directives;
    }

    if (options.cardTitle && options.cardContent) {
        alexaResponse.card = {
            type: 'Simple',
            title: options.cardTitle,
            content: options.cardContent
        };

        if(options.cardImage && (options.cardImage.smallImageUrl || options.cardImage.largeImageUrl)) {
            alexaResponse.card.type = 'Standard';
            alexaResponse.card['image'] = {};

            delete alexaResponse.card.content;
            alexaResponse.card.text = options.cardContent;

            if(options.cardImage.smallImageUrl) {
                alexaResponse.card.image['smallImageUrl'] = options.cardImage.smallImageUrl;
            }

            if(options.cardImage.largeImageUrl) {
                alexaResponse.card.image['largeImageUrl'] = options.cardImage.largeImageUrl;
            }
        }
    } else if (options.cardType === 'LinkAccount') {
        alexaResponse.card = {
            type: 'LinkAccount'
        };
    } else if (options.cardType === 'AskForPermissionsConsent') {
        alexaResponse.card = {
            type: 'AskForPermissionsConsent',
            permissions: options.permissions
        };
    }

    let returnResult = {
        version: '1.0',
        response: alexaResponse
    };

    if (options.sessionAttributes) {
        returnResult.sessionAttributes = options.sessionAttributes;
    }
    return returnResult;
}

function getDialogDirectives(dialogType, updatedIntent, slotName) {
    let directive = {
        type: dialogType
    };

    if (dialogType === 'Dialog.ElicitSlot') {
        directive.slotToElicit = slotName;
    } else if (dialogType === 'Dialog.ConfirmSlot') {
        directive.slotToConfirm = slotName;
    }

    if (updatedIntent) {
        directive.updatedIntent = updatedIntent;
    }
    return [directive];
}
 
 


 
 
 

 

Edited by btreinders
Posted

This code is not triggering the request to ISY because the request is not actually called.

At line 118, where you have:

exports.handler = function (event, context) {

You are reassigning a new function to exports.handler (The lambda starting point). You can't do that. It's already assigned at line 183, which is your main lambda function. 

And since you are just assigning a function to a variable, you are not calling the function at all.

So, just remove the line  "exports.handler = function (event, context) {", including the closing bracket, and try it again.

Benoit

Posted (edited)

@bmercier   I removed them and it still seems to return before the call is made.  I have tried using a Promise but I don't really understand it.  Sure appreciate all the help!  I am also having trouble making the call from Chrome now too so I am not sure if I am using the right url.  Does it look correct?  Thanks!

Edited by btreinders
Posted
On 3/6/2019 at 5:24 PM, btreinders said:

@bmercier   I removed them and it still seems to return before the call is made.  I have tried using a Promise but I don't really understand it.  Sure appreciate all the help!  I am also having trouble making the call from Chrome now too so I am not sure if I am using the right url.  Does it look correct?  Thanks!

Try the URL in a browser. Those are GET requests, so that's an easy way of test them.

Benoit

Posted (edited)

@bmercier  It does work in Firefox.  Chrome pops up the username and password box even those are in the URL and Edge does the same.  If I put a console.log after the res.on it never makes it there.

Edited by btreinders
Posted
On 3/7/2019 at 8:33 PM, btreinders said:

@bmercier  It does work in Firefox.  Chrome pops up the username and password box even those are in the URL and Edge does the same.  If I put a console.log after the res.on it never makes it there.

Then I don't know what it is. It may not reach the request at all either. Adding console.log just before would help to find where the issue is.

Benoit

Posted (edited)

@bmercier  I have successfully made a call to IFTTT using async, promise and await.  This is with the exact same code with just port changed to 80, https changed to http, username and password commented out and method changed to POST. 

Could it be something with the http versus https or my.isy.io just isn't responding?

Here is the response I get back with GET and POST:

<?xml version="1.0" encoding="UTF-8"?><RestResponse succeeded="false"><status>404</status></RestResponse>

With POST:

?xml version="1.0" encoding="UTF-8"?><s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"><s:Body><UDIDefaultResponse><status>715</status><info>n/a</info></UDIDefaultResponse></s:Body></s:Envelope>

Edited by btreinders
  • 2 weeks later...
Posted
On 3/10/2019 at 9:46 AM, btreinders said:

@bmercier  I have successfully made a call to IFTTT using async, promise and await.  This is with the exact same code with just port changed to 80, https changed to http, username and password commented out and method changed to POST. 

Could it be something with the http versus https or my.isy.io just isn't responding?

Here is the response I get back with GET and POST:

<?xml version="1.0" encoding="UTF-8"?><RestResponse succeeded="false"><status>404</status></RestResponse>

With POST:

?xml version="1.0" encoding="UTF-8"?><s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope"><s:Body><UDIDefaultResponse><status>715</status><info>n/a</info></UDIDefaultResponse></s:Body></s:Envelope>

 Those are responses from the ISY. The calls are getting throught.

Now it's a question of having the correct URL to achieve the result that you want. If you try the same URLs with GET and POST directly to the ISY, without portal, you should be getting the exact same result.

In the first case, you are getting a 404. I believe the URL is not valid. Try it in a browser.

In the second case, I assume it's a post to /service. If I recall correctly, the 715 is an error code related to subscriptions.

Benoit

  • Like 1

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • Create New...