Source: nomiku.js

#!/usr/bin/env node

 /**
 * @overview Provides an API wrapper for the WiFi Nomiku/Tender service
 * @see {@link http://github.com/harrisonhjones/}
 * @author Harrison Jones (harrison@hhj.me)
 * @copyright Harrison Jones 2016
 * @license
 * The MIT License (MIT)
 * 
 * Copyright (c) 2016 Harrison Jones
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

/**
 * @class
 * @description Creates a new Nomiku adapter
 * @property {String} STATE_ON - The value to indicate that a unit is "ON"
 * @property {String} STATE_OFF - The value to indicate that a unit is "OFF"
 * @property {String} STATE_OFFLINE - The value to indicate that a unit is "OFFLINE"
 */ 
var Nomiku = (function() {

    var _ = require('private-parts').createKey();

    
    function Nomiku(token) {
        if(token)
            _(this).apiToken = token;
        else
            _(this).apiToken = null;

        _(this).apiUserID = 0;
        _(this).apiURL = 'https://www.eattender.com/api/'
        _(this).deviceID = '';
        _(this).mqttURL = '';
        _(this).debug = false; 
    }

    Nomiku.prototype.STATE_ON = '1';
    Nomiku.prototype.STATE_OFF = '0';
    Nomiku.prototype.STATE_OFFLINE = '-1';

    /**
     * @function setDebug
     * @memberof Nomiku.prototype
     * @description Manually sets if debugging is on or off
     * @param {Boolean} arg - The future state of the debugger's on state
     */
    Nomiku.prototype.debug = function() {
        if (_(this).debug)
        {
            args = ["[nomiku-js]"];
            for (var i = 0; i < arguments.length; i++) {
                args.push(arguments[i]);
            }
            console.log(args.join(" "));
        } 
    }

    /**
     * @function debug
     * @memberof Nomiku.prototype
     * @description Manually sets if debugging is on or off
     * @param {Boolean} arg - The future state of the debugger's on state
     */
    Nomiku.prototype.setDebug = function(arg) {
        if(typeof arg === 'boolean') {
            _(this).debug = arg;
            this.debug("debugging " + (arg ? 'enabled' : 'disabled'));
        }
    }

    /**
     * @function setToken
     * @memberof Nomiku.prototype
     * @description Manually sets a token
     * @param {String} apiToken - the API token to use
     * @returns {Boolean} true if a token is provided. False otherwise
     */
    Nomiku.prototype.setToken = function(token)
    {
        if(token)
        {
            _(this).apiToken = token;
            return true;
        }
        else
        {
            return false;
        }
    }

    /**
     * @function setUserID
     * @memberof Nomiku.prototype
     * @description Manually sets a userID
     * @param {String} id - the user ID to use
     * @returns {Boolean} true if a id is provided. False otherwise
     */
    Nomiku.prototype.setUserID = function(id)
    {
        if(id)
        {
            _(this).apiUserID = id;
            return true;
        }
        else
        {
            return false;
        }
    }

    /**
     * @function setDeviceID
     * @memberof Nomiku.prototype
     * @description Manually sets the device ID
     * @param {String} id - the device ID to use
     * @returns {Boolean} true if a id is provided. False otherwise
     */
    Nomiku.prototype.setDeviceID = function(id)
    {
        this.debug("Device ID set to", id);

        if(id)
        {
            _(this).deviceID = id;
            return true;
        }
        else
        {
            return false;
        }
    }

    /**
     * @function getToken
     * @memberof Nomiku.prototype
     * @description Returns the current apiToken
     * @returns {String} The current apiToken / access token
     */
    Nomiku.prototype.getToken = function()
    {
        return _(this).apiToken;
    }



    /**
     * @function auth
     * @memberof Nomiku.prototype
     * @description Authenticates and grabs an access token
     * @param {String} email - email to login to the Nomiku API with
     * @param {String} password - password for the login
     * @param {authenticateCallback} cb - The callback that handles the response.
     */
    Nomiku.prototype.auth = function(email, password, cb)
    {
        if(email && password)
        {
            // We are using the 'request' module to http requests
            var request = require('request');

            this.debug("Authenticating against url=" + (_(this).apiURL + 'users/auth'), "with username =", email, "& password =", password);

            // See http://www.toptal.com/javascript/10-most-common-javascript-mistakes for why this is done
            var self = this;

            // Set a HTTP POST request to the auth endpoint with the required login information. If we login correctly grab the returned userID and apiToken and set them; call the callback with `false` to indicate that no error has occured. If not, call the callback with the error
            request.post(_(this).apiURL + 'users/auth', {form:{email: email, password: password}}, function (error, response, body) {
                // console.log("Auth success", error, response.statusCode);
                if (!error && response.statusCode == 201) {
                    body = JSON.parse(body);
                    //this.setUserID();
                    self.setUserID(body.user_id);
                    self.setToken(body.api_token);
                    cb(false);
                }
                else
                {
                    cb(error);
                }
            });
        }
        else
        {
            cb("You failed to provide either an email or password to authenticate against. Email =", email, "& password =", password);
        }
    }

    /**
     * @function getDevices
     * @memberof Nomiku.prototype
     * @description Authenticates and grabs an access token
     * @param {String} email - email to login to the Nomiku API with
     * @param {String} password - password for the login
     * @param {authenticateCallback} cb - The callback that handles the response.
     */
    Nomiku.prototype.getDevices = function(cb)
    {
        if(!_(this).apiToken || !_(this).apiUserID)
            return cb("You must authenticate first!",null);
        // We are using the 'request' module to http requests
        var request = require('request');

        var options = {
            url: _(this).apiURL + 'devices',
            headers: {
            'X-Api-Token': _(this).apiToken
            }
        };

        this.debug("Getting device list");

        // Set a HTTP POST request to the auth endpoint with the required login information. If we login correctly grab the returned userID and apiToken and set them; call the callback with `false` to indicate that no error has occured. If not, call the callback with the error
        request.get(options, function (error, response, body) {
            // console.log("Auth success", error, response.statusCode);
            if (!error && response.statusCode == 200) {
                body = JSON.parse(body);
                // this.debug("Found the following devices:", body.devices);
                cb(false, body.devices);
            }
            else
            {
                this.debug("Unable to pull the device list. Error =", error);
                cb(error, null);
            }
        });

    }

    /**
     * @function get
     * @memberof Nomiku.prototype
     * @description Grabs an arbitrary variable value
     * @param {String} variableName - the variable name to grab
     * @param {getSetCallback} cb - The callback that handles the response.
     */
    Nomiku.prototype.get = function(variableName, cb)
    {
        this.debug("Getting", variableName, 'from', _(this).deviceID, 'using token', _(this).apiToken);

        if(!variableName)
            return cb("You must provide a variable name",null);

        // If the `variableName` variable name isn't a string convert it to one (required by the API)
        if (!(typeof variableName === 'string') || !(variableName instanceof String))
            variableName = variableName.toString();

        // If we haven't set the apiToken, userID, or deviceID return with an error because we need those 3 things to successfully execute the command
        if(!_(this).apiToken || !_(this).apiUserID)
            return cb("You must authenticate first!",null);
        
        if(!_(this).deviceID)
            return cb("You must set a device ID!",null);

        // We are using the mqtt library to talk to the API. Connect to the API with the apiUserID and API Token
        var mqtt    = require('mqtt'),
            client  = mqtt.connect('https://mq.nomiku.com/mqtt',{username: 'user/'+_(this).apiUserID, password: _(this).apiToken});
        
        // When we connect subscribe to the desired variable
        var self = this;

        client.on('connect', function () {
            self.debug("Subscribing to ", 'nom2/' + _(self).deviceID + '/get/' + variableName)
            client.subscribe('nom2/' + _(self).deviceID + '/get/' + variableName);
        });
        
        // Once we get the desired variable value kill the conneciton and call the callback with the variable value
        client.on('message', function (topic, message) {
          client.end();
          cb(false, message.toString());
        });

        client.on('error', function (error) {
            self.debug("An MQTT error occured.",error);
            cb(error.toString(),null);
            client.end();
        });
    }

    /**
     * @function set
     * @memberof Nomiku.prototype
     * @description Sets an arbitrary variable value
     * @param {String} variableName - the variable name to set
     * @param {String} value - the value to set the variable to
     * @param {getSetCallback} cb - The callback that handles the response.
     */
    Nomiku.prototype.set = function(variableName, value, cb)
    {
        this.debug("Setting", variableName, 'to', value, 'on', _(this).deviceID, 'using token', _(this).apiToken);

        if(!variableName)
            return cb("You must provide a variable name. You provided ", variableName,null);
        
        if(!value)
            return cb("You must provide a variable value. You provided ", value,null);
        
        // If the `variableName` variable name isn't a string convert it to one (required by the API)
        if (!(typeof variableName === 'string') || !(variableName instanceof String))
            variableName = variableName.toString();
        
        // If the `value` variable name isn't a string convert it to one (required by the API)
        if (!(typeof value === 'string') || !(value instanceof String))
            value = value.toString();

        // If we haven't set the apiToken, userID, or deviceID return with an error because we need those 3 things to successfully execute the command
        if(!_(this).apiToken || !_(this).apiUserID)
            return cb("You must authenticate first!",null);

        if(!_(this).deviceID)
            return cb("You must set a device ID!",null);

        // We are using the mqtt library to talk to the API. Connect to the API with the apiUserID and API Token
        var mqtt    = require('mqtt'),
            client  = mqtt.connect('https://mq.nomiku.com/mqtt',{username: 'user/'+_(this).apiUserID, password: _(this).apiToken});

        // When we connect publish the desired variable value and subscribe to the value (not really useful since it returns an old value)
        var self = this;

        client.on('connect', function () {
            client.publish('nom2/' + _(self).deviceID + '/set/' + variableName, value);
            client.subscribe('nom2/' + _(self).deviceID + '/get/' + variableName);
        });
        
        // Once we get the desired variable value kill the conneciton and call the callback with the variable value. Again; this is almost always the old value
        client.on('message', function (topic, message) {
            client.end();
            cb(false, message.toString());
        });

        client.on('error', function (error) {
            self.debug("An MQTT error occured.",error);
            cb(error.toString(),null);
            client.end();
        });
    }

    /**
     * @function getState
     * @memberof Nomiku.prototype
     * @description Grabs the most recent state (0 = off, 1 = on, -1 = offline)
     * @param {getSetCallback} cb - The callback that handles the response.
     */
    Nomiku.prototype.getState = function(cb)
    {
        this.get('state', cb);
    }

    /**
     * @function getTemp
     * @memberof Nomiku.prototype
     * @description Grabs the most recent temperature in Celcius
     * @param {getSetCallback} cb - The callback that handles the response.
     */
    Nomiku.prototype.getTemp = function(cb)
    {
        this.get('temp', cb);
    }

    /**
     * @function getSetPoint
     * @memberof Nomiku.prototype
     * @description Grabs the most recent set point temperature in Celcius
     * @param {getSetCallback} cb - The callback that handles the response.
     */
    Nomiku.prototype.getSetPoint = function(cb)
    {
        this.get('setpoint', cb);
    }

    /**
     * @function getReceipeID
     * @memberof Nomiku.prototype
     * @description Grabs the most recent recipe ID
     * @param {getSetCallback} cb - The callback that handles the response.
     */
    Nomiku.prototype.getReceipeID = function(cb)
    {
        this.get('recipeID', cb);
    }

    /**
     * @function getVersion
     * @memberof Nomiku.prototype
     * @description Grabs the most recent version
     * @param {getSetCallback} cb - The callback that handles the response.
     */
    Nomiku.prototype.getVersion = function(cb)
    {
        this.get('version', cb);
    }

    /**
     * @function setState
     * @memberof Nomiku.prototype
     * @description Sets the current state of the device
     * @param {getSetCallback} cb - The callback that handles the response.
     */
    Nomiku.prototype.setState = function(value, cb)
    {
        this.set('state', value, cb);
    }

    /**
     * @function setSetPoint
     * @memberof Nomiku.prototype
     * @description Sets the current temperature set point in Celcius of the device
     * @param {getSetCallback} cb - The callback that handles the response.
     */
    Nomiku.prototype.setSetPoint = function(value, cb)
    {
        this.set('setpoint', value, cb);
    }

    /**
     * @function CtoF
     * @memberof Nomiku.prototype
     * @description Converts temperatures in Celcius to Farenheight 
     * @param {String} c - The temp value in degrees Celcius
     * @returns {Number} The converted temperature in Farenheight
     */
    Nomiku.prototype.CtoF = function(c)
    {
        return c*1.8+32;
    }

    /**
     * @function FtoC
     * @memberof Nomiku.prototype
     * @description Converts temperatures in Farenheight to Celcius 
     * @param {String} f - The temp value in degrees Farenheight
     * @returns {Number} The converted temperature in Celcius
     */
    Nomiku.prototype.FtoC = function(f)
    {
        return (f-32)/1.8;
    }

    return Nomiku;
}());

/** @module nomiku-js */
module.exports = new Nomiku;

/**
 * @callback authenticateCallback
 * @memberof Nomiku.prototype
 * @param {Boolean|object} error - False if no error occured; an error obejct if an error did occur
 */

/**
 * @callback getSetCallback
 * @memberof Nomiku.prototype
 * @param {Boolean|object} error - False if no error occured; an error obejct if an error did occur
 * @param {String} value - the value returned from the get request if an error did not occur
 */