UNPKG

nomiku

Version:

Client library for controlling Nomiku WiFi devices

406 lines (370 loc) 11.3 kB
const mqtt = require("./mqtt"); const EventEmitter = require("events").EventEmitter; const api = require("./api"); const constants = require("./constants") const Device = require("./device") const lodash = require("lodash/core") /** * The Nomiku Client controls the connection to the Nomiku. Descends from * EventEmitter * * @constructor * * @param {Object} options - The option object. Can also be passed to connect. * @param {string} options.email - tender account email (for email, password login) * @param {string} options.password - tender account password (for email, password login) * @param {string} options.userID - ID of the user (for token login) * @param {string} options.apiToken - API token (for token login) * @param {boolean} options.optimistic - optimistic state updates */ function Client(options) { if (!(this instanceof Client)) { return new Client(); } EventEmitter.call(this); this.options = options; this.optimistic = true; if (options) { this.userID = options.userID; this.apiToken = options.apiToken; if (options.hasOwnProperty('optimistic')) { this.optimistic = options.optimistic; } } this.reconnectPeriod = constants.RECONNECT_PERIOD_MIN; this.clientId = "nomikujs/" + Math.random().toString(36).substr(2, 8); this.mqttClient = new mqtt.Client( api.MQTT_URL, api.MQTT_PORT, this.clientId ); //these are called by the Paho library so need to be bound this.connect = this.connect.bind(this) this.auth = this.auth.bind(this) this.listen = this.listen.bind(this) this.set = this.set.bind(this) this.getDefaultDevice = this.getDefaultDevice.bind(this) this.loadDevices = this.loadDevices.bind(this) this._subscribe = this._subscribe.bind(this) this._onFailure = this._onFailure.bind(this) this._onConnect = this._onConnect.bind(this) this._onMessage = this._onMessage.bind(this) this._onDisconnected = this._onDisconnected.bind(this) this._setState = this._setState.bind(this) this._clearProvisional = this._clearProvisional.bind(this) this.mqttClient.onConnectionLost = this._onDisconnected; this.mqttClient.onMessageArrived = this._onMessage; //map of Tender id to device (all known) this.devices={}; //map of hwid to device (all listening) this.listening={}; this.provisionalTimers={}; this.verboseState=false; this.connected=false; this.reconnect=true; } Client.prototype = Object.create(EventEmitter.prototype); function callIf(test, callback) { //assert(typeof callback === 'function') if (test) return callback else return function(arg) { return Promise.resolve(arg); } } /** * Connects to server for streaming data * * @method connect * @param {Object} options The option object * Options: * @param {string} options.email - tender account email (for email, password login) * @param {string} options.password - tender account password (for email, password login) * @param {string} options.userID - ID of the user (for token login) * @param {string} options.apiToken - API token (for token login) * @param {string|number} options.defaultDevice - (optional) default device to connect to * @param {array} options.devices - (optional) array of devices, {hwid,id,name} * @param {boolean} options.verboseState - (optional) will send state event with every message * @param {boolean} options.reconnect - (optional) will try to reconnect */ Client.prototype.connect = function connect(options) { this.reconnect=true; if (options) { this.userID = options.userID || this.userID; this.apiToken = options.apiToken || this.apiToken; this.defaultDevice = options.defaultDevice || this.defaultDevice; this.verboseState = options.verboseState || this.verboseState; if (options.devices) { for (index in options.devices) { let id=options.devices[index].id this.devices[id]=new Device(Object.assign({},options.devices[index],{optimistic:this.optimistic})) } } if (options.hasOwnProperty('reconnect')) { this.reconnect = options.reconnect; } } var that=this; return callIf(!(this.userID && this.apiToken), that.auth)(options) .then(callIf(!this.defaultDevice,this.getDefaultDevice)) .then(callIf(lodash.isEmpty(this.devices),this.loadDevices)) .then(function () { var mqttOptions={ onFailure:that._onFailure.bind(that), onSuccess:that._onConnect.bind(that), password:that.apiToken, userName:'user/'+that.userID, useSSL:api.MQTT_SSL, } return that.mqttClient.connect(mqttOptions); }) .catch(this._onFailure) }; /** * Disconnects from server * * @method disconnect */ Client.prototype.disconnect = function disconnect() { if (this.connected) { try { this.mqttClient.disconnect(); this.connected=false; this.reconnect=false; } catch (err) { this.emit('error',err); } } }; /** * Gets API token from tender * * @method auth * @param {object} options - Info needed to auth with Tender API (email/password) */ Client.prototype.auth = function auth(options) { let that=this; if (options.apiToken && options.userID) { this.apiToken = options.apiToken; this.userID = options.userID; return Promise.resolve(options) } return api.authenticate(options) .then(function(response) { that.userID=response.userID; that.apiToken=response.apiToken; return response; }) }; /** * Gets device list from Tender * * @method loadDevices */ Client.prototype.loadDevices = function loadDevices() { if (!(this.userID && this.apiToken)) { return Promise.reject(new Error("Not authenticated")); } var that=this; return api.getDevices({userID:this.userID,apiToken:this.apiToken}) .then(function (deviceList) { for (index in deviceList) { var id=deviceList[index].id.toString() that.devices[id]=new Device({ hwid:deviceList[index].hardware_device_id, id:id, name:deviceList[index].name, optimistic:that.optimistic }) } return that.devices }) }; /** * Gets default device from Tender * * @method getDefaultDevice */ Client.prototype.getDefaultDevice = function getDefaultDevice() { if (!(this.userID && this.apiToken)) { return Promise.reject(new Error("Not authenticated")); } var that=this; return api.getDefaultDeviceID({userID:this.userID,apiToken:this.apiToken}) .then(function (defaultDevice) { that.defaultDevice = defaultDevice return defaultDevice }) }; /** * Listens for state on device * * @method listen * @param {string|number} id - Tender ID of nomiku to listen to */ Client.prototype.listen = function listen(id) { id = id.toString() if (this.devices.hasOwnProperty(id)) { let hwid=this.devices[id].hwid; if (!this.listening.hasOwnProperty(hwid)) { //add to list this.listening[hwid]=this.devices[id] //then subscribe if connected if (this.connected) this._subscribe(this.devices[id]) } return true; } else { return new Error("Unknown device id") } }; /** * Set state on device * * @method set * @param {string|number} id - Tender ID of nomiku to set */ Client.prototype.set = function set(id) { if ((!id) && (!this.defaultDevice)) return new Error("Use an ID or get default device") var id=id ? id.toString() : this.defaultDevice.toString(); if (this.devices.hasOwnProperty(id)) { if (this.optimistic) { if (this.provisionalTimers[id]) { clearTimeout(this.provisionalTimers[id]) } this.provisionalTimers[id] = setTimeout(this._clearProvisional, constants.PROVISIONAL_TIMEOUT, id); } return this.devices[id].set(this._setState) } else { return new Error("Device not found") } }; /** * Called when MQTT is connected * * @method _onConnect * @private */ Client.prototype._onConnect = function _onConnect() { /** * Successfully connected * * @event Client#event:connect */ this.emit('connect'); this.connected=true; this.reconnectPeriod = constants.RECONNECT_PERIOD_MIN; if (this.devices.hasOwnProperty(this.defaultDevice)) { let hwid=this.devices[this.defaultDevice].hwid this.listening[hwid]=this.devices[this.defaultDevice] } for (key in this.listening) { this._subscribe(this.listening[key]) } }; /** * Called when MQTT fails to connect * * @method _onFailure * @private */ Client.prototype._onFailure = function _onFailure() { if (this.reconnectPeriod * 2 <= constants.RECONNECT_PERIOD_MAX) { this.reconnectPeriod *= 2; } if (this.reconnect) { this.reconnectTimer = setTimeout(this.connect, this.reconnectPeriod); } }; /** * Called when good MQTT connection is closed * * @method _onDisconnected * @private */ Client.prototype._onDisconnected = function _onDisconnected() { /** * Connection is closed * * @event Client#event:close */ this.emit('close') this.connected=false; if (this.reconnect) { this.reconnectTimer = setTimeout(this.connect, this.reconnectPeriod); } }; /** * Subscribe to device * * @method _subscribe * @private */ Client.prototype._subscribe = function _subscribe(device) { var topic=device.getTopic() try { this.mqttClient.subscribe(topic); } catch (err) { /** * Error occured * * @event Client#event:error * @type {object} * @property {error} error - Details on error */ console.log(`Subscribe to ${topic} failed, reason: ${err}`) this.emit('error', err); } }; /** * Called when MQTT message is received * * @method _onMessage * @private */ Client.prototype._onMessage = function _onMessage(message) { var topicArray=message.destinationName.split('/'); if (this.listening.hasOwnProperty(topicArray[1])) { if (api.MQTT_TOPICS.indexOf(topicArray[3]) > -1) { let newState=this.listening[topicArray[1]].updateState(topicArray[3],message.payloadString); if (this.verboseState || (newState.valid && newState.new)) { /** * New state * * @event Client#event:state * @type {object} * @property {number} id - Tender ID of device * @property {boolean} new - Indicates whether the state has been emitted before * @property {object} state - Latest state * @property {object} provisional - Dict with same keys as state, key is true if it is unconfirmed * @property {boolean} valid - Indicates whether the state is valid */ this.emit('state', newState); } } } }; /** * Set the state through Tender API * * @method _setState * @private */ Client.prototype._setState = function _setState(stateChange, fullState) { var auth={userID:this.userID,apiToken:this.apiToken}; var id=fullState.id; return api.setDevice(auth,id,stateChange); }; /** * Clear provisional state * * @method _setState * @private */ Client.prototype._clearProvisional = function _clearProvisional(id) { if (this.devices.hasOwnProperty(id)) { var newState = this.devices[id].endProvisional() this.emit('state',newState); } }; module.exports = Client;