const settings = require('../util/settings'); const logger = require('../util/logger'); const utils = require('../util/utils'); const Queue = require('queue'); const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/(bind|unbind)/.+$`); const foundationCfg = {manufSpec: 0, disDefaultRsp: 0}; const clusteReportCfg = { 6: {direction: 0, attrId: 0, dataType: 16, minRepIntval: 0, maxRepIntval: 1000, repChange: 0}, 8: {direction: 0, attrId: 0, dataType: 32, minRepIntval: 0, maxRepIntval: 1000, repChange: 0}, }; const allowedClusters = [ 5, // genScenes 6, // genOnOff 8, // genLevelCtrl 768, // lightingColorCtrl ]; class Bind { constructor(zigbee, mqtt, state, publishDeviceState) { this.zigbee = zigbee; this.mqtt = mqtt; this.state = state; this.publishDeviceState = publishDeviceState; // Setup queue this.queue = new Queue(); this.queue.concurrency = 1; this.queue.autostart = true; } onMQTTConnected() { this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/bind/+`); this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/unbind/+`); } parseTopic(topic) { if (!topic.match(topicRegex)) { return null; } // Remove base from topic topic = topic.replace(`${settings.get().mqtt.base_topic}/bridge/`, ''); // Parse type from topic const type = topic.split('/')[0]; // Remove type from topic topic = topic.replace(`${type}/`, ''); return {ID: topic, type: type}; } onMQTTMessage(topic, message) { topic = this.parseTopic(topic); if (!topic) { return false; } // Find source; can only be a device. const sourceEntity = utils.resolveEntity(topic.ID); const source = this.zigbee.findDevice(sourceEntity.ID); if (!source) { logger.error(`Failed to find device '${sourceEntity.ID}'`); return false; } // Find target; can be a device or group. const targetEntity = utils.resolveEntity(message.toString()); let target = null; if (targetEntity.type === 'device') { target = this.zigbee.findDevice(targetEntity.ID); if (!target) { logger.error(`Failed to find target device '${targetEntity.ID}'`); return false; } } else if (targetEntity.type === 'group') { target = targetEntity.ID; } // Find which clusters are supported by both the source and target. // Groups are assumed to support all clusters (as we don't know which devices are in) let supported = []; if (targetEntity.type === 'device') { supported = target.getSimpleDesc().inClusterList.filter((cluster) => { return allowedClusters.includes(cluster); }); } else if (targetEntity.type === 'group') { supported = allowedClusters; } const clusters = source.getSimpleDesc().outClusterList.filter((cluster) => { return supported.includes(cluster); }); // Bind clusters.forEach((cluster) => { this.queue.push((queueCallback) => { logger.debug(`${topic.type}ing cluster '${cluster}' from ${sourceEntity.ID}' to '${targetEntity.ID}'`); source[topic.type](cluster, target, (error) => { if (error) { logger.error( `Failed to ${topic.type} cluster '${cluster}' from ${sourceEntity.ID}' to ` + `'${targetEntity.ID}' (${error})` ); } else { logger.info( `Successfully ${topic.type === 'bind' ? 'bound' : 'unbound'} cluster '${cluster}' from ` + `${sourceEntity.ID}' to '${targetEntity.ID}'` ); /** * As the target device is now directly controllled by the source device * zigbee2mqtt won't receive any updates of the target device when directly * changed by the source device. * Configure reporting on the target device. * This is not supported for groups yet. */ if (targetEntity.type === 'device' && topic.type === 'bind') { this.setupReporting(target, cluster); } } queueCallback(); }); }); }); } setupReporting(ep, cluster) { this.queue.push((queueCallback) => { if (clusteReportCfg[cluster]) { ep.foundation(cluster, 'configReport', [clusteReportCfg[cluster]], foundationCfg, (error) => { if (error) { logger.error( `Failed to setup reporting for cluster '${cluster}' of device '${ep.device.ieeeAddr}'` ); } else { logger.info( `Configured reporting for cluster '${cluster}' of device '${ep.device.ieeeAddr}'` ); } }); } else { logger.warn(`Don't know how to setup reporting for '${cluster}', skipping..`); } queueCallback(); }); } } module.exports = Bind;