text: #!/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"
   * @property {Array} states - All available device states exposed for easy translation of state value to human-readable state
   */ 
  var Nomiku = function() 
  {
  
      this._apiUserID = 0;
      this._apiURL = 'https://www.eattender.com/api/'
      this._deviceID = '';
      this._mqttURL = '';
      this._debug = false; 
  
      this.STATE_ON = '1';
      this.STATE_OFF = '0';
      this.STATE_OFFLINE = '-1';
      this.STATE_BOOTING_UP = '2';
      this.states = {"-1": "OFFLINE", "0": "OFF", "1": "ON", "2": "BOOTING UP"};
  
      /**
       * @function debug
       * @memberof Nomiku.prototype
       * @description Outputs a debugging message (if debugging is enabled)
       * @param {String|Object|Array} arguments - The different arguments to output
       */
      this.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 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
       */
      this.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
       */
      this.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
       */
      this.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
       */
      this.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
       */
      this.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.
       */
      this.auth = function(email, password, cb)
      {
          if(email && password)
          {
              // We are using the 'needle' module to perform http requests
              var needle = require('needle');
  
              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
              needle.post(this._apiURL + 'users/auth', {email: email, password: password}, function (error, response) {
                  if(error)
                  {
                      cb({error: 'BAD_HTTP_REQUEST', message: error});
                  }
                  else
                  {
                      if(response.statusCode == 201) {
                          var body = response.body;
                          self.setUserID(body.user_id);
                          self.setToken(body.api_token);
                          cb(false);
                      }
                      else
                      {
                          cb({error: "BAD_HTTP_RESPONSE_CODE", message: response.statusCode});
                      }
                  }
              });
          }
          else
          {
              cb({error: "BAD_FUNCTION_PARAMETERS", message: ("You failed to provide either an email or password to authenticate against. Email =", email, "& password =", password)});
          }
      }
  
  
      this.getUser = function(userID, type, cb)
      {
          type = type || 'basic';
  
          this.debug("Getting " + type + " user info for user " + userID);
  
          if(!userID)
          {
              cb({error: "BAD_FUNCTION_PARAMETERS", message: "No userID specified!"}, null);
              return;
          }
          else
          {
              if (userID == 'me')
              {
                  if(!this._apiUserID)
                  {
                      cb({error: "FAILURE_TO_AUTHENTICATE", message: "You must authenticate first! You cannot specify 'me' as your userID if you have not logged in using a username and password first"}, null);
                      return;
                  }
                  else
                  {
                      userID = this._apiUserID;
                  }
              }
              // We are using the 'needle' module to perform http requests
              var needle = require('needle');
  
              // This request does not require authentication (though if you provide it the request will return more information)
              var options = {};
              if(type == 'full')
              {
                  if(!this._apiToken)
                  {
                      cb({error: "FAILURE_TO_AUTHENTICATE", message: "You must authenticate first! You cannot specify 'full' as your type if you have not logged in or provided a apiToken first"}, null);                            
                      return;
                  }
                  else
                  {
                      options = {
                          headers: {
                              'X-Api-Token': this._apiToken
                          }
                      };
                  }
              }
  
              // Perform a HTTP GET to grab all the devices tied to the user with the access token we have
              needle.get(this._apiURL + 'users/' + userID, options, function (error, response) {
                  if(error)
                  {
                      cb({error: "BAD_HTTP_REQUEST", message: error});
                  }
                  else
                  {
                      if(response.statusCode == 200) {
                          var body = response.body;
                          cb(false, body.user);
                      }
                      else
                      {
                          cb({error:"BAD_HTTP_RESPONSE_CODE", message: response.statusCode});
                      }
                  }
              });
          }
      }
  
      /**
       * @function getDevices
       * @memberof Nomiku.prototype
       * @description Authenticates and grabs an access token
       * @param {devicesCallback} cb - The callback that handles the response.
       */
      this.getDevices = function(cb)
      {
          if(!this._apiToken)
          {
              cb({error: "FAILURE_TO_AUTHENTICATE", message: "You must authenticate first!"}, null);
          }
          else
          {
              // We are using the 'needle' module to perform http requests
              var needle = require('needle');
  
              var options = {
                  headers: {
                      'X-Api-Token': this._apiToken
                  }
              };
  
              this.debug("Getting device list");
  
              // Perform a HTTP GET to grab all the devices tied to the user with the access token we have
              needle.get(this._apiURL + 'devices', options, function (error, response) {
                  if(error)
                  {
                      cb({error: "BAD_HTTP_REQUEST", message: error});
                  }
                  else
                  {
                      if(response.statusCode == 200) {
                          var body = response.body;
                          cb(false, body.devices);
                      }
                      else
                      {
                          cb({error:"BAD_HTTP_RESPONSE_CODE", message: response.statusCode});
                      }
                  }
              });
          }
      }
  
      /**
       * @function getDeviceSession
       * @memberof Nomiku.prototype
       * @description Grabs the current session info for a device
       * @param {String|Number} deviceID - the device ID of the device (get it using a device list)
       * @param {deviceSession} cb - The callback that handles the response.
       */
      this.getDeviceSession = function(deviceID, cb)
      {
          if(!this._apiToken)
          {
              cb({error: "FAILURE_TO_AUTHENTICATE", message: "You must authenticate first!"}, null);
          }
          else
          { 
              var self = this;
              self.debug("Getting device '", deviceID, "' session");
  
              if(!deviceID)
              {
                  cb({error: "BAD_FUNCTION_PARAMETERS", message: "No deviceID specified!"}, null);
                  return;
              }
  
              var options = {
                  headers: {
                      'X-Api-Token': this._apiToken
                  }
              };
              var needle = require('needle');
              needle.get(this._apiURL + 'devices/' + deviceID + '/session', options, function (error, response) {
                  if(error)
                  {
                      cb({error: "BAD_HTTP_REQUEST", message: error});
                  }
                  else
                  {
                      if(response.statusCode == 200) 
                      {
                          cb(false, response.body);
                      }
                      else
                      {
                          cb({error: "BAD_HTTP_RESPONSE_CODE", message: response.statusCode});
                      }
                  }
              });
          }
      }
  
      /**
       * @function getDeviceState
       * @memberof Nomiku.prototype
       * @description Grabs the state of a device
       * @param {String|Number} deviceID - the device ID of the device (get it using a device list)
       * @param {deviceCallback} cb - The callback that handles the response.
       */
      this.getDeviceState = function(deviceID, cb)
      {
          if(!this._apiToken)
          {
              cb({error: "FAILURE_TO_AUTHENTICATE", message: "You must authenticate first!"}, null);
          }
          else
          { 
              var self = this;
              self.debug("Getting device '", deviceID, "' state");
              
              if(!deviceID)
              {
                  cb({error: "BAD_FUNCTION_PARAMETERS", message: "No deviceID specified!"}, null);
                  return;
              }
              else
                  {
  
                  // Step 1 - Get device session info
                  // Returns: 
                  //  {
                  //      "session_token": "{SESSION_TOKEN}",
                  //      "session_base_url": "{SESSION_BASE_URL}",
                  //      "session_path": "{SESSION_PATH}"
                  //  }
                  self.getDeviceSession(deviceID, function(error, session_info) {
                      if(error)
                      {
                          cb(error, null);
                      }
                      else
                      {
                          var options = {
                              headers: {
                                  'X-Api-Token': self._apiToken
                              }
                          };
                          var needle = require('needle');
                          // Step 2 - GET {SESSION_BASE_URL} + {SESSION_PATH} + "?auth=" + {SESSION_TOKEN}
                          self.debug("Getting device state info");
                          needle.get(session_info.session_base_url + session_info.session_path + '?auth=' + session_info.session_token, options, function (error, response) {
                              if(error)
                              {
                                  cb({error: true, message: error});
                              }
                              else
                              {
                                  if(response.statusCode == 200) 
                                  {
                                      // Returns 
                                      //  {
                                      //      recipeID: {RECIPE_ID},
                                      //      setpoint: {SETPOINT},
                                      //      showF: {SHOW_F},
                                      //      state: {STATE},
                                      //      temp: {TEMP},
                                      //      timerRunning: {TIMER_RUNNING},
                                      //      timerSecs: {TIMER_SECS}
                                      //  }
                                      cb(false, response.body);
                                  }
                                  else
                                  {
                                      cb({error: "BAD_HTTP_RESPONSE_CODE", message: response.statusCode});
                                  }
                                  
                              }
                          });
                      }
                  });
              }
          }
      }
  
  
      /**
       * @function setDeviceState
       * @memberof Nomiku.prototype
       * @description Sets the state of a device
       * @param {String|Number} deviceID - the device ID of the device (get it using a device list)
       * @param {Object} deviceState - The new device state. 
       * @param {deviceCallback} cb - The callback that handles the response.
       */
      this.setDeviceState = function(deviceID, deviceState, cb)
      {
          if(!this._apiToken)
          {
              cb({error: "FAILURE_TO_AUTHENTICATE", message: "You must authenticate first!"}, null);
          }
          else
          { 
              var self = this;
              self.debug("Set device '", deviceID, "' state to ", JSON.stringify(deviceState));
          
              if(!deviceID)
              {
                  cb({error: "BAD_FUNCTION_PARAMETERS", message: "No deviceID specified!"}, null);
                  return;
              }
  
              if(!deviceState)
              {    
                  cb({error: "BAD_FUNCTION_PARAMETERS", message: "No deviceState specified!"}, null);
                  return;
              }
  
              var needle = require('needle');
  
              var options = {
                  headers: {
                      'X-Api-Token': this._apiToken,
                      'Accept': 'application/json',
                      'Content-Type': 'application/json'
                  }
              };
  
              var data = JSON.stringify({state: deviceState});
              needle.post(this._apiURL + 'devices/' + deviceID + '/set', data, options, function (error, response) {
                  if(error)
                  {
                      cb({error: true, message: error});
                  }
                  else
                  {
                      if(response.statusCode == 201) 
                      {
                          cb(false, response.body);
                      }
                      else
                      {
                          cb({error: "BAD_HTTP_RESPONSE_CODE", message: response.statusCode});
                      }
                      
                  }
              });
          }
      }
  
      /**
       * @function setMQTT
       * @memberof Nomiku.prototype
       * @description Sets an arbitrary variable value
       * @param {String} hardwareID - The hardware ID of the device to update
       * @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.
       */
      this.setMQTT = function(hardwareID, variableName, value, cb)
      {
          if(!this._apiToken || !this._apiUserID)
          {
              cb({error: "FAILURE_TO_AUTHENTICATE", message: "You must authenticate first!"}, null);
          }
          else
          { 
              this.debug("Setting", variableName, 'to', value, 'on', hardwareID, 'using token', this._apiToken);
  
              if(!hardwareID)
              {
                  cb({error: "BAD_FUNCTION_PARAMETERS", message: "No hardwareID specified!"}, null);
                  return;
              }
  
              if(!variableName)
              {
                  cb({error: "BAD_FUNCTION_PARAMETERS", message: "No variableName specified!"}, null);
                  return;
              }
  
              if(!value)
              {
                  cb({error: "BAD_FUNCTION_PARAMETERS", message: "No value specified!"}, null);
                  return;
              }
              
              // 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();
  
              // 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/' + hardwareID + '/set/' + variableName, value);
                  client.subscribe('nom2/' + hardwareID + '/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 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
       */
      this.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
       */
      this.FtoC = function(f)
      {
          return (f-32)/1.8;
      }
  };
  
  /** @module nomiku-js */
  module.exports = 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
   */
  
   /**
   * @callback deviceCallback
   * @memberof Nomiku.prototype
   * @param {Boolean|object} error - False if no error occured; an error obejct if an error did occur
   * @param {String} device - A device object containing the device's state
   */
  
   /**
   * @callback devicesCallback
   * @memberof Nomiku.prototype
   * @param {Boolean|object} error - False if no error occured; an error obejct if an error did occur
   * @param {Array} devices - An array of devices containing basic device info (no state information)
   */
  
   /**
   * @callback deviceSession
   * @memberof Nomiku.prototype
   * @param {Boolean|object} error - False if no error occured; an error obejct if an error did occur
   * @param {Object} deviceSessionInfo - The device session information
   */
  
  