UNPKG

nomiku-js

Version:

A NodeJS module for interacting the a WiFi Nomiku and the Nomiku Tender service

650 lines (589 loc) 23.5 kB
#!/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 */