diff --git a/lib/controller.js b/lib/controller.js index c4ff95421..4231da247 100644 --- a/lib/controller.js +++ b/lib/controller.js @@ -6,46 +6,37 @@ const deviceMapping = require('./devices'); const zigbee2mqtt = require('./converters/zigbee2mqtt'); const mqtt2zigbee = require('./converters/mqtt2zigbee'); const homeassistant = require('./homeassistant'); -const debug = require('debug')('zigbee2mqtt'); +const debug = require('debug')('zigbee2mqtt:controller'); const mqttConfigRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/config/\\w+`, 'g'); const mqttDeviceRegex = new RegExp(`${settings.get().mqtt.base_topic}/\\w+/set`, 'g'); const mqttDevicePrefixRegex = new RegExp(`${settings.get().mqtt.base_topic}/\\w+/\\w+/set`, 'g'); const issueLink = 'https://github.com/Koenkk/zigbee2mqtt/issues'; - -function getTimestamp() { - var d = new Date(); - return d.getTime(); -} +const pollInterval = 60 * 1000; // seconds * 1000. +const softResetTimeout = 3600 * 1000; // seconds * 1000. class Controller { constructor() { this.zigbee = new Zigbee(); this.mqtt = new MQTT(); - this.stateCache = {}; + + this.stateCache = {}; // Caches messages from devices. + this.resetTimer = null; // After 1 hour of no message, reset CC2531 timer. + this.handleZigbeeMessage = this.handleZigbeeMessage.bind(this); this.handleMQTTMessage = this.handleMQTTMessage.bind(this); - - this.checkOnlineTimer = null; - this.lastDeviceActivity = {}; // timestamps of last data/activity - this.lastControllerActivity = 0; } start() { this.zigbee.start(this.handleZigbeeMessage, (error) => { - this.lastDeviceActivity = {}; - if (error) { logger.error('Failed to start'); } else { // Log zigbee clients on startup. const devices = this.zigbee.getAllClients(); logger.info(`Currently ${devices.length} devices are joined:`); - devices.forEach((device) => { - logger.info(this.getDeviceStartupLogMessage(device)) - this.setLastDeviceActivity(device.ieeeAddr); - }); + devices.forEach((device) => logger.info(this.getDeviceStartupLogMessage(device))); // Connect to MQTT broker const subscriptions = [ @@ -77,19 +68,58 @@ class Controller { this.zigbee.permitJoin(true); } - // Set timer at interval to check online status of Zigbee routers. - // For example, it prevents Xiaomi routers to go to a deep sleep mode - const interval = 1 * 1000; // seconds * 1000. - this.checkOnlineTimer = setTimeout(this.zigbeeCheckOnline.bind(this), interval); - this.lastControllerActivity = getTimestamp(); + // Start poll timer. + this.pollTimer(true); + this.resetSoftResetTimeout(); + } + + resetSoftResetTimeout() { + if (this._softResetTimer) { + clearTimeout(this._softResetTimer); + this._softResetTimer = null; + } + + 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.resetSoftResetTimeout(); + }); + }, 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.mqtt.disconnect(); - if(this.checkOnlineTimer) { - clearTimeout(this.checkOnlineTimer); - this.checkOnlineTimer = null; - } + this.pollTimer(false); this.zigbee.stop(callback); } @@ -110,6 +140,9 @@ class Controller { } handleZigbeeMessage(message) { + // Zigbee message receieved, reset soft reset timeout. + this.resetSoftResetTimeout(); + debug('Recieved zigbee message with data', message.data); if (message.type == 'devInterview') { @@ -130,8 +163,6 @@ class Controller { return; } - this.setLastDeviceActivity(device.ieeeAddr); - // Check if this is a new device. if (!settings.getDevice(device.ieeeAddr)) { logger.info(`New device with address ${device.ieeeAddr} connected!`); @@ -283,69 +314,6 @@ class Controller { this.mqtt.publish(deviceSettings.friendly_name, JSON.stringify(payload), options); } - - setLastDeviceActivity(ieeeAddr) { - this.lastDeviceActivity[ieeeAddr] = getTimestamp(); - } - - zigbeeCheckOnline() { - var dt = getTimestamp(); - - //TO-DO: may be allow to configure the timeout - if ((dt - this.lastControllerActivity) > 3600000) { - // no data received in 1 hour. - // This problem may occur sometimes with CC2531 (USB devices can be pluged/unpluged by a PnP system) - // try to restart and self-recovery - this.checkOnlineTimer = null; - this.lastControllerActivity = dt; - this.lastDeviceActivity = {}; - logger.warn('Soft restart'); - this.zigbee.shepherd.reset('soft', (err) => { - if(err){ - logger.warn('Soft reset error:', err); - this.zigbee.stop( (err) => { - logger.warn('Stop:', err); - this.zigbee.start(this.zigbee.onMessage, () => {}); - }); - } - else{ - this.checkOnlineTimer = setTimeout(this.zigbeeCheckOnline.bind(this), 1000); - } - }); - return; - } - - var device, devInfo, devType, power, dev_desc; - for (device in this.lastDeviceActivity) { - if ((dt - this.lastDeviceActivity[device]) > 60000) { - this.lastDeviceActivity[device] = dt; - - devInfo = this.zigbee.shepherd._findDevByAddr(device); - if (devInfo) { - // battery powered endpoint devices are in the sleep mode most time - if(devInfo.powerSource){ - power = devInfo.powerSource.toLowerCase().split(' ')[0]; - } - else{ - power = 'unknown'; - } - devType = devInfo.type.toLowerCase(); - if ( - ((power !== 'battery') && (power !== 'unknown')) || - (devType === 'router') - ) { - dev_desc = this.getDeviceStartupLogMessage(devInfo); - logger.info('Data timeout for device:', dev_desc, ' Checking online status.'); - // note: checkOnline has the callback argument but does not call callback - this.zigbee.shepherd.controller.checkOnline(devInfo); - } - } - } - } - - this.checkOnlineTimer = setTimeout(this.zigbeeCheckOnline.bind(this), 1000); - return; - } } module.exports = Controller; diff --git a/lib/zigbee.js b/lib/zigbee.js index d3bbaea10..13dcd811e 100644 --- a/lib/zigbee.js +++ b/lib/zigbee.js @@ -2,6 +2,7 @@ const ZShepherd = require('zigbee-shepherd'); const logger = require('./util/logger'); const settings = require('./util/settings'); const data = require('./util/data'); +const debug = require('debug')('zigbee2mqtt:zigbee'); const shepherdSettings = { net: { @@ -41,6 +42,10 @@ class Zigbee { this.onMessage = onMessage; } + softReset(callback) { + this.shepherd.reset('soft', callback); + } + stop(callback) { this.shepherd.stop((error) => { logger.info('zigbee-shepherd stopped'); @@ -88,6 +93,16 @@ class Zigbee { return this.shepherd.list().filter((device) => device.type !== 'Coordinator'); } + ping(deviceID) { + const device = this.shepherd._findDevByAddr(deviceID); + + if (device) { + // Note: checkOnline has the callback argument but does not call callback + debug(`Check online ${deviceID}`); + this.shepherd.controller.checkOnline(device); + } + } + handleMessage(message) { if (this.onMessage) { this.onMessage(message);