0

I'm creating a lambda function for an Alexa Smart Home skill to control my receiver. I'm using the boiler plate sample code that Amazon provides, but I'm new to node and I'm having a lot of trouble getting it to wait for my callback to set the response. I've been trying to operate as if their general structure is correct here.

I can call this and it executes the POST request just fine, but it always returns null (which Alexa doesn't like). It's not logging anything inside the response.on('end' loop either. Seems like it's blasting right through the marantzAPI call. I've tried using a callback in the marantzAPI function (commands, callback) and I end up with the same result. I think there's something I just don't get here.

Here's the lambda function:

'use strict';

var http = require('http');
var qs = require('querystring');

/**
 * We're not discovering our devices, we're just hardcoding them.  Easy!
 */
const USER_DEVICES = [
    {
        applianceId: 'marantz-sr6010-shield',
        manufacturerName: 'Marantz nVidia',
        modelName: 'SR6010 Shield',
        version: '1.0',
        friendlyName: 'Shield',
        friendlyDescription: 'nVidia Shield via Marantz SR6010',
        isReachable: true,
        actions: ['turnOn', 'turnOff'],
    }
];

/**
 * Utility functions
 */

function log(title, msg) {
    console.log(`[${title}] ${msg}`);
}

/**
 * Generate a unique message ID
 *
 * https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
 * This isn't UUID V4 but it's good enough for what we're doing
 */
function generateMessageID() {
    return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
        var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
        return v.toString(16);
    });
}

/**
 * Generate a response message
 *
 * @param {string} name - Directive name
 * @param {Object} payload - Any special payload required for the response
 * @returns {Object} Response object
 */
function generateResponse(name, payload) {
    return {
        header: {
            messageId: generateMessageID(),
            name: name,
            namespace: 'Alexa.ConnectedHome.Control',
            payloadVersion: '2',
        },
        payload: payload,
    };
}

/**
 * This is a lot easier when I'm just hard-coding my devices
 */
function getDevicesFromPartnerCloud() {
    return USER_DEVICES;
}


/**
 * The meat and potatoes, I'm butchering just the things I Need from the solid work done by Nathan Totten:
 * https://github.com/ntotten/marantz-avr/blob/master/lib/avreciever.js
 */

function marantzAPI(commands, apiCallback) {
    log('DEBUG', `MarantzAPI Invoked: ` + JSON.stringify(commands));
    var postData = {};

    // format commands for the Marantz POST (cmd0: cmd1: etc)
    // note: may need to send commands one at a time??
    for (var i=0; i<commands.length; i++) {
        postData['cmd' + i] = commands[i];
    }

    log('DEBUG', `MarantzAPI POST Data: ` + qs.stringify(postData));

    var serverError = function (e) {
        log('Error', e.message);
        apiCallback(generateResponse('UnexpectedInformationReceivedError', e.message));
    };    

    var httpCallback = function(response) {
        response.on('end', function () {
            log('DEBUG', `API Request Complete`);
            apiCallback(generateResponse('APIRequestComplete', postData));
        });        

        response.on('error', serverError); 
    };

    var apiRequest = http.request({
        hostname: process.env.receiverIp,
        path: '/MainZone/index.put.asp',
        port: process.env.receiverPort,
        method: 'POST',
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            'Content-Length': Buffer.byteLength(qs.stringify(postData))
        },
    }, httpCallback);  

    apiRequest.on('error', serverError);
    apiRequest.write(qs.stringify(postData));
    apiRequest.end();    
} 


/**
 * Main logic
 */

function handleDiscovery(request, callback) {
    log('DEBUG', `Discovery Request: ${JSON.stringify(request)}`);

    const userAccessToken = request.payload.accessToken.trim();

    const response = {
        header: {
            messageId: generateMessageID(),
            name: 'DiscoverAppliancesResponse',
            namespace: 'Alexa.ConnectedHome.Discovery',
            payloadVersion: '2',
        },
        payload: {
            discoveredAppliances: getDevicesFromPartnerCloud(userAccessToken),
        },
    };

    log('DEBUG', `Discovery Response: ${JSON.stringify(response)}`);

    callback(null, response);
}

function handleControl(request, callback) {
    log('DEBUG', `Control Request: ${JSON.stringify(request)}`);

    const userAccessToken = request.payload.accessToken.trim();
    const applianceId = request.payload.appliance.applianceId;

    let response;
    var commands = [];

    switch (request.header.name) {
        case 'TurnOnRequest':
            // turn on the device
            commands.push('PutZone_OnOff/ON');

            // set the input
            switch (applianceId) {
                case 'marantz-sr6010-shield':
                    commands.push('PutZone_InputFunction/MPLAY');
                    break;
            }

            // I guess?  Not even sure if it actually does all this.
            commands.push('aspMainZone_WebUpdateStatus/');

            marantzAPI(commands, function(response) {
                callback(null, response);
            });
            break;

        default: {
            log('ERROR', `No supported directive name: ${request.header.name}`);
            callback(null, generateResponse('UnsupportedOperationError', {}));
            return;
        }
    }

    // I think I need to remove these, because response is not set at execution time
    // log('DEBUG', `Control Confirmation: ${JSON.stringify(response)}`);
    // callback(null, response);
}

exports.handler = (request, context, callback) => {

    switch (request.header.namespace) {
        case 'Alexa.ConnectedHome.Discovery':
            handleDiscovery(request, callback);
            break;

        case 'Alexa.ConnectedHome.Control':
            handleControl(request, callback);
            break;

        default: {
            const errorMessage = `No supported namespace: ${request.header.namespace}`;
            log('ERROR', errorMessage);
            callback(new Error(errorMessage));
        }
    }
};
johndoe
  • 4,387
  • 2
  • 25
  • 40
Matt Brunmeier
  • 1,310
  • 2
  • 11
  • 22
  • that's quite a bit of code... where within it is the problem happening? at first glance the callback usage seems correct, this question would benefit from having an MCVE – Kevin B Jun 13 '17 at 16:04
  • I've cleaned it up - mostly this is boilerplate from the Amazon Smart Home skill, but I've included it for context. I *think* the problem is that the handleControl event is calling my marantzAPI function but the script is simply terminating. Nothing is logged out of the response.on('end' loop within the marantzAPI HTTP callback. – Matt Brunmeier Jun 13 '17 at 18:20

2 Answers2

0

Node js environment is by default an async environment, and your function marantzAPI(commands) method is getting executed asynchronously so while this function is getting executed the control doesn't stop just there and continues to next functions and eventually executes apiRequest.end(); while the previouse functions is not yet completed resulting in blank response. That is the sortest possible answer I would recommend you to visit This SO thread to have a detailed description and possibly a solution.

Jeet
  • 5,569
  • 8
  • 43
  • 75
  • I've seen that answer, and I tried at one point to make my marantz function implement callbacks correctly, but it didn't change the returned result. I've updated the node function above to show my attempt. I think there's something I'm still missing fundamentally on callbacks... – Matt Brunmeier Jun 13 '17 at 15:21
0

I've got it working. Here's my updated marantzAPI function. Looks like the response.on('data' is crucial to success of the HTTP request? Had no idea.

function marantzAPI(commands, apiCallback) {
    var postData = {};

    // format commands for the Marantz POST (cmd0: cmd1: etc)
    // note: may need to send commands one at a time??
    for (var i=0; i<commands.length; i++) {
        postData['cmd' + i] = commands[i];
    }

    log('DEBUG', `MarantzAPI Called w Data: ` + qs.stringify(postData));

    var serverError = function (e) {
        log('Error', e.message);
        apiCallback(false, e.message);
    };    

    var apiRequest = http.request({
        hostname: process.env.receiverIp,
        path: '/MainZone/index.put.asp',
        port: process.env.receiverPort,
        method: 'POST',
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            'Content-Length': Buffer.byteLength(qs.stringify(postData))
        },
    }, function(response) {

        response.setEncoding('utf8');
        response.on('data', function (chunk) {
            log('DEBUG', 'CHUNK RECEIVED');
        });

        response.on('end', function () {
            log('DEBUG', `API Request Complete`);
            apiCallback(true, '');
        });        

        response.on('error', serverError); 
    });

    apiRequest.on('error', serverError);
    apiRequest.write(qs.stringify(postData));
    apiRequest.end();    
} 
Matt Brunmeier
  • 1,310
  • 2
  • 11
  • 22