diff --git a/lib/controller.js b/lib/controller.js index 1db321721..9c828794e 100644 --- a/lib/controller.js +++ b/lib/controller.js @@ -4,6 +4,8 @@ const State = require('./state'); const logger = require('./util/logger'); const settings = require('./util/settings'); const ExtensionNetworkMap = require('./extension/networkMap'); +const ExtensionSoftReset = require('./extension/softReset'); +const ExtensionRouterPollXiaomi = require('./extension/routerPollXiaomi'); const zigbeeShepherdConverters = require('zigbee-shepherd-converters'); const homeassistant = require('./homeassistant'); const objectAssignDeep = require('object-assign-deep'); @@ -12,8 +14,6 @@ const mqttConfigRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/con const mqttDeviceRegex = new RegExp(`${settings.get().mqtt.base_topic}/[\\w\\s\\d.-]+/set`, 'g'); const mqttDevicePrefixRegex = new RegExp(`${settings.get().mqtt.base_topic}/[\\w\\s\\d.-]+/[\\w\\s\\d.-]+/set`, 'g'); -const pollInterval = 60 * 1000; // seconds * 1000. -const softResetTimeout = 3600 * 1000; // seconds * 1000. const allowedLogLevels = ['error', 'warn', 'info', 'debug']; @@ -32,17 +32,19 @@ if (settings.get().homeassistant && !cacheState) { class Controller { constructor() { - this.zigbee = new Zigbee(); + this.handleZigbeeMessage = this.handleZigbeeMessage.bind(this); + this.handleMQTTMessage = this.handleMQTTMessage.bind(this); + + this.zigbee = new Zigbee(this.handleZigbeeMessage); this.mqtt = new MQTT(); this.state = new State(); this.configured = []; - this.handleZigbeeMessage = this.handleZigbeeMessage.bind(this); - this.handleMQTTMessage = this.handleMQTTMessage.bind(this); + this.extensions = []; } start() { this.startupLogVersion(() => { - this.zigbee.start(this.handleZigbeeMessage, (error) => { + this.zigbee.start((error) => { if (error) { logger.error('Failed to start', error); } else { @@ -63,10 +65,6 @@ class Controller { this.zigbee.permitJoin(settings.get().permit_join); - // Start timers. - this.pollTimer(true); - this.softResetTimeout(true); - // Connect to MQTT broker const subscriptions = [ `${settings.get().mqtt.base_topic}/+/set`, @@ -99,6 +97,8 @@ class Controller { // Initialize extensions. this.extensions = [ new ExtensionNetworkMap(this.zigbee, this.mqtt, this.state), + new ExtensionSoftReset(this.zigbee, this.mqtt, this.state), + new ExtensionRouterPollXiaomi(this.zigbee, this.mqtt, this.state), ]; // Resend all cached states. @@ -113,57 +113,10 @@ class Controller { }); } - softResetTimeout(start) { - if (this._softResetTimer) { - clearTimeout(this._softResetTimer); - this._softResetTimer = null; - } - - if (start) { - this._softResetTimer = setTimeout(() => { - this.zigbee.softReset((error) => { - if (error) { - logger.warn('Soft reset error', error); - this.zigbee.stop((error) => { - logger.warn('Zigbee stopped'); - this.zigbee.start(this.handleZigbeeMessage, (error) => { - if (error) { - logger.error('Failed to restart!'); - } - }); - }); - } else { - logger.warn('Soft resetted zigbee'); - } - - this.softResetTimeout(true); - }); - }, softResetTimeout); - } - } - - pollTimer(start) { - // Some routers need polling to prevent them from sleeping. - if (start && !this._pollTimer) { - this._pollTimer = setInterval(() => { - const devices = this.zigbee.getAllClients().filter((d) => { - const power = d.powerSource ? d.powerSource.toLowerCase().split(' ')[0] : 'unknown'; - return power !== 'battery' && power !== 'unknown' && d.type === 'Router'; - }); - - devices.forEach((d) => this.zigbee.ping(d.ieeeAddr)); - }, pollInterval); - } else if (!start && this._pollTimer) { - clearTimeout(this._pollTimer); - this._pollTimer = null; - } - } - stop(callback) { + this.extensions.filter((e) => e.stop).forEach((e) => e.stop()); this.state.save(); this.mqtt.disconnect(); - this.pollTimer(false); - this.softResetTimeout(false); this.zigbee.stop(callback); } @@ -235,8 +188,8 @@ class Controller { } handleZigbeeMessage(message) { - // Zigbee message receieved, reset soft reset timeout. - this.softResetTimeout(true); + // Call extensions. + this.extensions.filter((e) => e.handleZigbeeMessage).forEach((e) => e.handleZigbeeMessage(message)); // Log the message. let logMessage = `Recieved zigbee message of type '${message.type}' ` + diff --git a/lib/extension/routerPollXiaomi.js b/lib/extension/routerPollXiaomi.js new file mode 100644 index 000000000..370bb5738 --- /dev/null +++ b/lib/extension/routerPollXiaomi.js @@ -0,0 +1,40 @@ +const utils = require('../util/utils'); +const interval = utils.secondsToMilliseconds(60); + +/** + * This extensions polls Xiaomi Zigbee routers to keep them awake. + */ +class RouterPollXiaomi { + constructor(zigbee, mqtt, state) { + this.zigbee = zigbee; + this.timer = null; + + this.startTimer(); + } + + startTimer() { + this.clearTimer(); + this.timer = setInterval(() => this.handleInterval(), interval); + } + + clearTimer() { + if (this.timer) { + clearTimeout(this.timer); + this.timer = null; + } + } + + stop() { + this.clearTimer(); + } + + handleInterval() { + this.zigbee.getAllClients() + .filter((d) => utils.isXiaomiDevice(d)) // Filter Xiaomi devices + .filter((d) => d.type === 'Router') // Filter routers + .filter((d) => d.powerSource && d.powerSource !== 'Battery') // Remove battery powered devices + .forEach((d) => this.zigbee.ping(d.ieeeAddr)); // Ping devices. + } +} + +module.exports = RouterPollXiaomi; diff --git a/lib/extension/softReset.js b/lib/extension/softReset.js new file mode 100644 index 000000000..763495d0e --- /dev/null +++ b/lib/extension/softReset.js @@ -0,0 +1,66 @@ +const settings = require('../util/settings'); +const logger = require('../util/logger'); +const utils = require('../util/utils'); + +/** + * This extensions soft resets the ZNP after a certain timeout. + */ +class SoftReset { + constructor(zigbee, mqtt, state) { + this.zigbee = zigbee; + this.timer = null; + this.timeout = utils.secondsToMilliseconds(settings.get().advanced.soft_reset_timeout); + + if (this.timeout === 0) { + logger.debug(`Soft reset timeout disabled`); + } else { + logger.debug(`Soft reset timeout set to ${utils.millisecondsToSeconds(this.timeout)} seconds`); + } + + this.resetTimer(); + } + + clearTimer() { + if (this.timer) { + clearTimeout(this.timer); + this.timer = null; + } + } + + resetTimer() { + if (this.timeout === 0) { + return; + } + + this.clearTimer(); + this.timer = setTimeout(() => this.handleTimeout(), this.timeout); + } + + handleTimeout() { + logger.warn('Soft reset timeout triggered'); + + this.zigbee.softReset((error) => { + if (error) { + logger.warn('Soft reset failed, trying stop/start'); + this.zigbee.stop((error) => { + logger.warn('Zigbee stopped'); + this.zigbee.start((error) => { + if (error) { + logger.error('Failed to restart!'); + } + }); + }); + } else { + logger.warn('Soft resetted ZNP due to timeout'); + } + + this.resetTimer(); + }); + } + + handleZigbeeMessage(message) { + this.resetTimer(); + } +} + +module.exports = SoftReset; diff --git a/lib/util/settings.js b/lib/util/settings.js index 15bac765e..42cf48e62 100644 --- a/lib/util/settings.js +++ b/lib/util/settings.js @@ -10,6 +10,7 @@ const defaults = { advanced: { log_directory: path.join(data.getPath(), 'log', '%TIMESTAMP%'), log_level: process.env.DEBUG ? 'debug' : 'info', + soft_reset_timeout: 0, }, }; diff --git a/lib/util/utils.js b/lib/util/utils.js new file mode 100644 index 000000000..f7267c552 --- /dev/null +++ b/lib/util/utils.js @@ -0,0 +1,8 @@ +// Xiaomi uses 4151 and 4447 (lumi.plug) as manufacturer ID. +const xiaomiManufacturerID = [4151, 4447]; + +module.exports = { + millisecondsToSeconds: (milliseconds) => milliseconds / 1000, + secondsToMilliseconds: (seconds) => seconds * 1000, + isXiaomiDevice: (device) => xiaomiManufacturerID.includes(device.manufId), +}; diff --git a/lib/zigbee.js b/lib/zigbee.js index 174e54520..f6e702375 100644 --- a/lib/zigbee.js +++ b/lib/zigbee.js @@ -3,6 +3,7 @@ const logger = require('./util/logger'); const settings = require('./util/settings'); const data = require('./util/data'); const zclPacket = require('zcl-packet'); +const utils = require('./util/utils'); const advancedSettings = settings.get().advanced; @@ -21,13 +22,14 @@ const shepherdSettings = { logger.debug(`Using zigbee-shepherd with settings: '${JSON.stringify(shepherdSettings)}'`); class Zigbee { - constructor() { + constructor(onMessage) { + this.onMessage = onMessage; this.handleReady = this.handleReady.bind(this); this.handleMessage = this.handleMessage.bind(this); this.handleError = this.handleError.bind(this); } - start(onMessage, callback) { + start(callback) { logger.info(`Starting zigbee-shepherd`); this.shepherd = new ZShepherd(settings.get().serial.port, shepherdSettings); @@ -62,8 +64,6 @@ class Zigbee { this.shepherd.on('ready', this.handleReady); this.shepherd.on('ind', this.handleMessage); this.shepherd.on('error', this.handleError); - - this.onMessage = onMessage; } _logStartupInfo() { @@ -84,12 +84,11 @@ class Zigbee { } handleReady() { - // Set all Xiaomi devices (manufId === 4151) to be online, so shepherd won't try - // to query info from devices (which would fail because they go tosleep). - // Xiaomi lumi.plug has manufId === 4447 and can be in the sleep mode too + // Set all Xiaomi devices to be online, so shepherd won't try + // to query info from devices (which would fail because they go to sleep). const devices = this.getAllClients(); devices.forEach((d) => { - if ((d.manufId === 4151) || (d.manufId === 4447)) { + if (utils.isXiaomiDevice(d)) { const device = this.shepherd.find(d.ieeeAddr, 1); if (device) { device.getDevice().update({ @@ -100,7 +99,7 @@ class Zigbee { } }); - // Check if we have to turn of the led + // Check if we have to turn off the led if (settings.get().serial.disable_led) { this.shepherd.controller.request('UTIL', 'ledControl', {ledid: 3, mode: 0}); } @@ -190,14 +189,16 @@ class Zigbee { return; } - logger.info(`Zigbee publish to '${deviceID}', ${cid} - ${cmd} - - ${JSON.stringify(zclData)} - ${JSON.stringify(cfg)} - ${ep}`); + logger.info( + `Zigbee publish to '${deviceID}', ${cid} - ${cmd} - ` + + `${JSON.stringify(zclData)} - ${JSON.stringify(cfg)} - ${ep}` + ); const callback_ = (error) => { if (error) { logger.error( - `Zigbee publish to '${deviceID}', ${cid} - ${cmd} - ${JSON.stringify(zclData)} - - ${JSON.stringify(cfg)} - ${ep} ` + + `Zigbee publish to '${deviceID}', ${cid} - ${cmd} - ${JSON.stringify(zclData)} ` + + `- ${JSON.stringify(cfg)} - ${ep} ` + `failed with error ${error}`); }