diff --git a/lib/controller.js b/lib/controller.js index a390b0a9b..5f352f554 100644 --- a/lib/controller.js +++ b/lib/controller.js @@ -6,13 +6,15 @@ 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'; +const pollInterval = 60 * 1000; // seconds * 1000. +const softResetTimeout = 3600 * 1000; // seconds * 1000. class Controller { constructor() { @@ -62,10 +64,62 @@ class Controller { logger.warn('Set `permit_join` to `false` once you joined all devices.'); this.zigbee.permitJoin(true); } + + // Start timers. + this.pollTimer(true); + this.softResetTimeout(true); + } + + 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.mqtt.disconnect(); + this.pollTimer(false); + this.softResetTimeout(false); this.zigbee.stop(callback); } @@ -86,6 +140,9 @@ class Controller { } handleZigbeeMessage(message) { + // Zigbee message receieved, reset soft reset timeout. + this.softResetTimeout(true); + debug('Recieved zigbee message with data', message.data); if (message.type == 'devInterview') { diff --git a/lib/converters/zigbee2mqtt.js b/lib/converters/zigbee2mqtt.js index 5556fb4cc..64ab51bb5 100644 --- a/lib/converters/zigbee2mqtt.js +++ b/lib/converters/zigbee2mqtt.js @@ -312,6 +312,23 @@ const parsers = [ return {power: precisionRound(msg.data.data['presentValue'], 2)}; }, }, + { + devices: ['ZNCZ02LM'], + cid: 'genBasic', + type: 'attReport', + convert: (msg) => { + if (msg.data.data['65281']) { + const data = msg.data.data['65281']; + return { + state: data['100'] === 1 ? 'ON' : 'OFF', + power: precisionRound(data['152'], 2), + voltage: precisionRound(data['150'] * 0.1, 1), + consumption: precisionRound(data['149'], 2), + temperature: precisionRound(data['3'], 2), + }; + } + }, + }, { devices: ['QBKG04LM'], cid: 'genOnOff', @@ -343,6 +360,27 @@ const parsers = [ return {smoke: msg.data.zoneStatus === 1}; }, }, + { + devices: ['CC2530.ROUTER'], + cid: 'genOnOff', + type: 'attReport', + convert: (msg) => { + return {state: msg.data.data['onOff'] === 1}; + }, + }, + { + devices: ['CC2530.ROUTER'], + cid: 'genBinaryValue', + type: 'attReport', + convert: (msg) => { + const data = msg.data.data; + return { + description: data['description'], + type: data['inactiveText'], + rssi: data['presentValue'], + }; + }, + }, // Ignore parsers (these message dont need parsing). { @@ -357,7 +395,7 @@ const parsers = [ { devices: [ 'WXKG11LM', 'MCCGQ11LM', 'RTCGQ11LM', 'WSDCGQ11LM', 'SJCGQ11LM', 'MCCGQ01LM', 'RTCGQ01LM', 'WXKG01LM', - 'WSDCGQ01LM', 'JTYJ-GD-01LM/BW', + 'WSDCGQ01LM', 'JTYJ-GD-01LM/BW', 'ZNCZ02LM', ], cid: 'genBasic', type: 'devChange', diff --git a/lib/devices.js b/lib/devices.js index 807aaab2a..6604aaaca 100644 --- a/lib/devices.js +++ b/lib/devices.js @@ -166,6 +166,14 @@ const devices = { description: 'WeMo smart LED bulb', supports: 'on/off, brightness', }, + + // Texax Instruments + 'lumi.router': { + model: 'CC2530.ROUTER', + vendor: 'Texas Instruments', + description: 'CC2530 router [link](http://ptvo.info/cc2530-based-zigbee-coordinator-and-router-112/)', + supports: 'state, description, type, rssi', + }, }; module.exports = devices; diff --git a/lib/homeassistant.js b/lib/homeassistant.js index 1a4cf5b5a..d3f42d78d 100644 --- a/lib/homeassistant.js +++ b/lib/homeassistant.js @@ -46,6 +46,17 @@ const configurations = { json_attributes: ['battery'], }, }, + 'binary_sensor_router': { + type: 'binary_sensor', + object_id: 'router', + discovery_payload: { + payload_on: true, + payload_off: false, + value_template: '{{ value_json.state }}', + device_class: 'connectivity', + json_attributes: ['description', 'type', 'rssi'], + }, + }, // Sensor 'sensor_illuminance': { @@ -104,6 +115,7 @@ const configurations = { unit_of_measurement: 'Watt', icon: 'mdi:flash', value_template: '{{ value_json.power }}', + json_attributes: ['voltage', 'temperature', 'consumption'], }, }, 'sensor_action': { @@ -210,6 +222,7 @@ const mapping = { '7146060PH': [configurations.light_brightness_colortemp_xy], 'F7C033': [configurations.light_brightness], 'JTYJ-GD-01LM/BW': [configurations.binary_sensor_smoke], + 'CC2530.ROUTER': [configurations.binary_sensor_router], }; // A map of all discoverd devices diff --git a/lib/zigbee.js b/lib/zigbee.js index c16fe0f6e..13dcd811e 100644 --- a/lib/zigbee.js +++ b/lib/zigbee.js @@ -2,9 +2,13 @@ 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: {panId: settings.get().advanced.pan_id}, + net: { + panId: settings.get().advanced.pan_id, + channelList: [settings.get().advanced.hasOwnProperty('channel') ? settings.get().advanced.channel : 11], + }, dbPath: data.joinPath('database.db'), }; @@ -12,6 +16,7 @@ class Zigbee { constructor() { this.handleReady = this.handleReady.bind(this); this.handleMessage = this.handleMessage.bind(this); + this.handleError = this.handleError.bind(this); } start(onMessage, callback) { @@ -32,10 +37,15 @@ class Zigbee { // Register callbacks. this.shepherd.on('ready', this.handleReady); this.shepherd.on('ind', this.handleMessage); + this.shepherd.on('error', this.handleError); this.onMessage = onMessage; } + softReset(callback) { + this.shepherd.reset('soft', callback); + } + stop(callback) { this.shepherd.stop((error) => { logger.info('zigbee-shepherd stopped'); @@ -46,9 +56,10 @@ 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 const devices = this.getAllClients(); devices.forEach((device) => { - if (device.manufId === 4151) { + if ((device.manufId === 4151) || (device.manufId === 4447)) { this.shepherd.find(device.ieeeAddr, 1).getDevice().update({ status: 'online', joinTime: Math.floor(Date.now() / 1000), @@ -59,6 +70,11 @@ class Zigbee { logger.info('zigbee-shepherd ready'); } + handleError(message) { + // This event may appear if zigbee-shepherd cannot decode bad packets (invalid checksum). + logger.error(message); + } + permitJoin(permit) { if (permit) { logger.info('Zigbee: allowing new devices to join.'); @@ -77,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);