diff --git a/.eslintignore b/.eslintignore index 75840b8e889..95cf0b07a4a 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,4 +1,3 @@ /build/ /config/ -/dist/ -/*.js \ No newline at end of file +/dist/ \ No newline at end of file diff --git a/app.js b/app.js index f351eaa6273..2d5b89f3254 100644 --- a/app.js +++ b/app.js @@ -1,23 +1,26 @@ -var express = require('express') -var reqlib = require('app-root-path').require -var logger = require('morgan') -var cookieParser = require('cookie-parser') -var bodyParser = require('body-parser') -var app = express() -var SerialPort = require('serialport') -var jsonStore = reqlib('/lib/jsonStore.js') -var cors = require('cors') -var ZWaveClient = reqlib('/lib/ZwaveClient') -var MqttClient = reqlib('/lib/MqttClient') -var Gateway = reqlib('/lib/Gateway') -var store = reqlib('config/store.js') -var debug = reqlib('/lib/debug')('App') -var history = require('connect-history-api-fallback') -var utils = reqlib('/lib/utils.js') +const express = require('express') +const reqlib = require('app-root-path').require +const logger = require('morgan') +const cookieParser = require('cookie-parser') +const bodyParser = require('body-parser') +const app = express() +const SerialPort = require('serialport') +const jsonStore = reqlib('/lib/jsonStore.js') +const cors = require('cors') +const ZWaveClient = reqlib('/lib/ZwaveClient') +const MqttClient = reqlib('/lib/MqttClient') +const Gateway = reqlib('/lib/Gateway') +const store = reqlib('config/store.js') +const debug = reqlib('/lib/debug')('App') +const history = require('connect-history-api-fallback') +const utils = reqlib('/lib/utils.js') const renderIndex = reqlib('/lib/renderIndex') -var gw // the gateway instance + +let gw // the gateway instance let io +let restarting = false + debug('zwavejs2mqtt version: ' + require('./package.json').version) debug('Application path:' + utils.getPath(true)) @@ -44,10 +47,15 @@ app.use(cors()) app.use(history()) +function hasProperty (obj, prop) { + return Object.prototype.hasOwnProperty.call(obj, prop) +} + function startGateway () { - var settings = jsonStore.get(store.settings) + const settings = jsonStore.get(store.settings) - var mqtt, zwave + let mqtt + let zwave if (settings.mqtt) { mqtt = new MqttClient(settings.mqtt) @@ -58,6 +66,10 @@ function startGateway () { } gw = new Gateway(settings.gateway, zwave, mqtt) + + gw.start() + + restarting = false } app.startSocket = function (server) { @@ -82,7 +94,7 @@ app.startSocket = function (server) { socket.on('ZWAVE_API', async function (data) { debug('Zwave api call:', data.api, data.args) if (gw.zwave) { - var result = await gw.zwave.callApi(data.api, ...data.args) + const result = await gw.zwave.callApi(data.api, ...data.args) result.api = data.api socket.emit(gw.zwave.socketEvents.api, result) } @@ -133,22 +145,22 @@ app.startSocket = function (server) { // ----- APIs ------ app.get('/health', async function (req, res) { - var mqtt = false - var zwave = false + let mqtt = false + let zwave = false if (gw) { mqtt = gw.mqtt ? gw.mqtt.getStatus().status : false zwave = gw.zwave ? gw.zwave.getStatus().status : false } - var status = mqtt && zwave + const status = mqtt && zwave res.status(status ? 200 : 500).send(status ? 'Ok' : 'Error') }) app.get('/health/:client', async function (req, res) { - var client = req.params.client - var status + const client = req.params.client + let status if (client !== 'zwave' && client !== 'mqtt') { res.status(500).send("Requested client doesn 't exist") @@ -161,15 +173,17 @@ app.get('/health/:client', async function (req, res) { // get settings app.get('/api/settings', async function (req, res) { - var data = { + const data = { success: true, settings: jsonStore.get(store.settings), devices: gw.zwave ? gw.zwave.devices : {}, serial_ports: [] } + + let ports if (process.platform !== 'sunos') { try { - var ports = await SerialPort.list() + ports = await SerialPort.list() } catch (error) { debug(error) } @@ -190,7 +204,7 @@ app.get('/api/exportConfig', function (req, res) { // import config app.post('/api/importConfig', async function (req, res) { - var config = req.body.data + const config = req.body.data try { if (!gw.zwave) throw Error('Zwave client not inited') @@ -198,13 +212,14 @@ app.post('/api/importConfig', async function (req, res) { else { for (let i = 0; i < config.length; i++) { const e = config[i] - if (e && (!e.hasOwnProperty('name') || !e.hasOwnProperty('loc'))) { + if (e && (!hasProperty(e, 'name') || !hasProperty(e, 'loc'))) { throw Error('Configuration not valid') } else if (e) { await gw.zwave.callApi('_setNodeName', i, e.name || '') await gw.zwave.callApi('_setNodeLocation', i, e.loc || '') - if (e.hassDevices) + if (e.hassDevices) { await gw.zwave.storeDevices(e.hassDevices, i, false) + } } } } @@ -217,29 +232,33 @@ app.post('/api/importConfig', async function (req, res) { }) // update settings -app.post('/api/settings', function (req, res) { - jsonStore - .put(store.settings, req.body) - .then(data => { - res.json({ success: true, message: 'Configuration updated successfully' }) - return gw.close() - }) - .then(() => startGateway()) - .catch(err => { - debug(err) - res.json({ success: false, message: err.message }) - }) +app.post('/api/settings', async function (req, res) { + try { + if (restarting) { + throw Error( + 'Gateway is restarting, wait a moment before doing another request' + ) + } + restarting = true + await jsonStore.put(store.settings, req.body) + await gw.close() + startGateway() + res.json({ success: true, message: 'Configuration updated successfully' }) + } catch (error) { + debug(error) + res.json({ success: false, message: error.message }) + } }) // catch 404 and forward to error handler app.use(function (req, res, next) { - var err = new Error('Not Found') + const err = new Error('Not Found') err.status = 404 next(err) }) // error handler -app.use(function (err, req, res, next) { +app.use(function (err, req, res) { // set locals, only providing error in development res.locals.message = err.message res.locals.error = req.app.get('env') === 'development' ? err : {} diff --git a/lib/Gateway.js b/lib/Gateway.js index 7b7e77ed487..7aa51dc69e8 100755 --- a/lib/Gateway.js +++ b/lib/Gateway.js @@ -114,20 +114,19 @@ function Gateway (config, zwave, mqtt) { if (!(this instanceof Gateway)) { return new Gateway(config) } + this.config = config || { type: 1 } + // clients + this.mqtt = mqtt + this.zwave = zwave EventEmitter.call(this) - init.call(this, config, zwave, mqtt) } inherits(Gateway, EventEmitter) -function init (config, zwave, mqtt) { +Gateway.prototype.start = async function () { // gateway configuration - this.config = config || { type: 1 } - this.config.values = this.config.values || [] - // clients - this.mqtt = mqtt - this.zwave = zwave + this.config.values = this.config.values || [] // Object where keys are topic and values can be both zwave valueId object // or a valueConf if the topic is a broadcast topic @@ -138,27 +137,28 @@ function init (config, zwave, mqtt) { // topic levels for subscribes using wildecards this.topicLevels = [] - if (mqtt) { - mqtt.on('writeRequest', onWriteRequest.bind(this)) - mqtt.on('broadcastRequest', onBroadRequest.bind(this)) - mqtt.on('apiCall', onApiRequest.bind(this)) - mqtt.on('hassStatus', onHassStatus.bind(this)) - mqtt.on('brokerStatus', onBrokerStatus.bind(this)) + if (this.mqtt) { + this.mqtt.on('writeRequest', onWriteRequest.bind(this)) + this.mqtt.on('broadcastRequest', onBroadRequest.bind(this)) + this.mqtt.on('apiCall', onApiRequest.bind(this)) + this.mqtt.on('hassStatus', onHassStatus.bind(this)) + this.mqtt.on('brokerStatus', onBrokerStatus.bind(this)) } - if (zwave) { - zwave.on('valueChanged', onValueChanged.bind(this)) - zwave.on('nodeStatus', onNodeStatus.bind(this)) - zwave.on('notification', onNotification.bind(this)) - zwave.on('scanComplete', onScanComplete.bind(this)) - zwave.on('nodeSceneEvent', onNodeSceneEvent.bind(this)) - zwave.on('nodeRemoved', onNodeRemoved.bind(this)) - - if (config.sendEvents) { - zwave.on('event', onEvent.bind(this)) + if (this.zwave) { + this.zwave.on('valueChanged', onValueChanged.bind(this)) + this.zwave.on('nodeStatus', onNodeStatus.bind(this)) + this.zwave.on('notification', onNotification.bind(this)) + this.zwave.on('scanComplete', onScanComplete.bind(this)) + this.zwave.on('nodeSceneEvent', onNodeSceneEvent.bind(this)) + this.zwave.on('nodeRemoved', onNodeRemoved.bind(this)) + + if (this.config.sendEvents) { + this.zwave.on('event', onEvent.bind(this)) } - zwave.connect() + // this is async but doesn't need to be awaited + this.zwave.connect() } else { debug('Zwave settings are not valid') } @@ -691,7 +691,7 @@ Gateway.prototype.close = async function () { } if (this.zwave) { - this.zwave.close() + await this.zwave.close() } } diff --git a/lib/ZwaveClient.js b/lib/ZwaveClient.js index bbfcfbff733..f10faa535a8 100644 --- a/lib/ZwaveClient.js +++ b/lib/ZwaveClient.js @@ -51,29 +51,26 @@ function ZwaveClient (config, socket) { if (!(this instanceof ZwaveClient)) { return new ZwaveClient(config) } - EventEmitter.call(this) - init.call(this, config, socket) -} -inherits(ZwaveClient, EventEmitter) + EventEmitter.call(this) -function init (cfg, socket) { - this.cfg = cfg + this.cfg = config this.socket = socket this.closed = false this.driverReady = false this.scenes = jsonStore.get(store.scenes) - cfg.networkKey = cfg.networkKey || process.env.OZW_NETWORK_KEY + config.networkKey = config.networkKey || process.env.OZW_NETWORK_KEY + // TODO: Handle this by using `logOptions` (PR waiting to be merged) if (process.env.LOGLEVEL === 'undefined') process.env.LOGLEVEL = null // https://github.com/zwave-js/node-zwave-js/blob/master/packages/core/src/log/shared.ts#L13 // https://github.com/winstonjs/triple-beam/blob/master/config/npm.js#L14-L15 - process.env.LOGLEVEL = process.env.LOGLEVEL || cfg.logLevel + process.env.LOGLEVEL = process.env.LOGLEVEL || config.logLevel - if (process.env.LOGTOFILE || cfg.logToFile) { + if (process.env.LOGTOFILE || config.logToFile) { process.env.LOGTOFILE = 'true' } @@ -86,6 +83,8 @@ function init (cfg, socket) { this.status = ZWAVE_STATUS.closed } +inherits(ZwaveClient, EventEmitter) + // ---------- DRIVER EVENTS ------------------------------------- function driverReady () { @@ -989,6 +988,7 @@ ZwaveClient.prototype.storeDevices = async function (devices, nodeId, remove) { */ ZwaveClient.prototype.close = async function () { this.status = ZWAVE_STATUS.closed + this.closed = true if (this.commandsTimeout) { clearTimeout(this.commandsTimeout) @@ -1000,8 +1000,6 @@ ZwaveClient.prototype.close = async function () { this.reconnectTimeout = null } - this.closed = true - if (this.healTimeout) { clearTimeout(this.healTimeout) this.healTimeout = null @@ -1250,6 +1248,9 @@ ZwaveClient.prototype.refreshNeighbors = function () { */ ZwaveClient.prototype.connect = async function () { if (!this.driverReady) { + // this could happen when the driver fails the connect and a reconnect timeout triggers + if (this.closed) return + let networkKey if (this.cfg.networkKey && this.cfg.networkKey.length === 32) { @@ -1274,6 +1275,10 @@ ZwaveClient.prototype.connect = async function () { this.status = ZWAVE_STATUS.connected this.connected = true } catch (error) { + // destroy diver instance when it fails + this.driver.destroy().catch(err => { + debug('Error while destroing driver ' + err.message) + }) driverError.call(this, error) debug('Retry connection in 3 seconds...') this.reconnectTimeout = setTimeout(this.connect.bind(this), 3000)