diff --git a/lib/controller.js b/lib/controller.js index ccfc3e7ed..c382101dc 100644 --- a/lib/controller.js +++ b/lib/controller.js @@ -59,6 +59,7 @@ class Controller { new ExtensionBind(...args), new ExtensionOnEvent(...args), new ExtensionOTAUpdate(...args), + new ExtensionReport(...args), ]; if (settings.get().experimental.new_api) { @@ -69,10 +70,6 @@ class Controller { this.extensions.push(new ExtensionBridgeLegacy(...args)); } - if (settings.get().advanced.report) { - this.extensions.push(new ExtensionReport(...args)); - } - if (settings.get().homeassistant) { this.extensions.push(new ExtensionHomeAssistant(...args)); } diff --git a/lib/eventBus.js b/lib/eventBus.js index 4aa9e17e8..c3b05c97d 100644 --- a/lib/eventBus.js +++ b/lib/eventBus.js @@ -8,6 +8,7 @@ const allowedEvents = [ 'publishEntityState', // Entity state will be published 'stateChange', // Entity changes its state 'groupMembersChanged', // Members of a group has been changed + 'reportingDisabled', // Reporting is disabled for a device ]; class EventBus extends events.EventEmitter { diff --git a/lib/extension/configure.js b/lib/extension/configure.js index 79ebad716..62cfcf4da 100644 --- a/lib/extension/configure.js +++ b/lib/extension/configure.js @@ -11,10 +11,27 @@ class Configure extends Extension { super(zigbee, mqtt, state, publishEntityState, eventBus); this.configuring = new Set(); + this.onReportingDisabled = this.onReportingDisabled.bind(this); this.attempts = {}; this.topic = `${settings.get().mqtt.base_topic}/bridge/request/device/configure`; this.legacyTopic = `${settings.get().mqtt.base_topic}/bridge/configure`; + this.eventBus.on(`reportingDisabled`, this.onReportingDisabled); + } + + onReportingDisabled(data) { + // Disabling reporting unbinds some cluster which could be bound by configure, re-setup. + const device = data.device; + + const resolvedEntity = this.zigbee.resolveEntity(device); + if (resolvedEntity.device.meta && resolvedEntity.device.meta.hasOwnProperty('configured')) { + delete device.meta.configured; + device.save(); + } + + if (this.shouldConfigure(resolvedEntity)) { + this.configure(resolvedEntity); + } } shouldConfigure(resolvedEntity) { diff --git a/lib/extension/report.js b/lib/extension/report.js index 330f9c657..9f495a1e8 100644 --- a/lib/extension/report.js +++ b/lib/extension/report.js @@ -1,5 +1,6 @@ const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters'); const logger = require('../util/logger'); +const settings = require('../util/settings'); const ZNLDP12LM = zigbeeHerdsmanConverters.devices.find((d) => d.model === 'ZNLDP12LM'); const utils = require('../util/utils'); const Extension = require('./extension'); @@ -106,6 +107,7 @@ class Report extends Extension { this.queue = new Set(); this.failed = new Set(); this.pollDebouncers = {}; + this.enabled = settings.get().advanced.report; } shouldIgnoreClusterForDevice(cluster, definition) { @@ -124,33 +126,46 @@ class Report extends Extension { if (this.queue.has(device.ieeeAddr) || this.failed.has(device.ieeeAddr)) return; this.queue.add(device.ieeeAddr); + const term1 = this.enabled ? 'Setup' : 'Disable'; + const term2 = this.enabled ? 'setup' : 'disabled'; + try { for (const ep of device.endpoints) { for (const [cluster, configuration] of Object.entries(clusters)) { if (ep.supportsInputCluster(cluster) && !this.shouldIgnoreClusterForDevice(cluster, definition)) { - logger.debug(`Setup reporting for '${device.ieeeAddr}' - ${ep.ID} - ${cluster}`); + logger.debug(`${term1} reporting for '${device.ieeeAddr}' - ${ep.ID} - ${cluster}`); const items = []; for (const entry of configuration) { if (!entry.hasOwnProperty('condition') || (await entry.condition(ep))) { - items.push({...entry}); + const toAdd = {...entry}; + if (!this.enabled) toAdd.maximumReportInterval = 0xFFFF; + items.push(toAdd); delete items[items.length - 1].condition; } } - await ep.bind(cluster, this.coordinatorEndpoint); + this.enabled ? + await ep.bind(cluster, this.coordinatorEndpoint) : + await ep.unbind(cluster, this.coordinatorEndpoint); + await ep.configureReporting(cluster, items); logger.info( - `Successfully setup reporting for '${device.ieeeAddr}' - ${ep.ID} - ${cluster}`, + `Successfully ${term2} reporting for '${device.ieeeAddr}' - ${ep.ID} - ${cluster}`, ); } } } - device.meta.reporting = reportKey; + if (this.enabled) { + device.meta.reporting = reportKey; + } else { + delete device.meta.reporting; + this.eventBus.emit('reportingDisabled', {device}); + } } catch (error) { logger.error( - `Failed to setup reporting for '${device.ieeeAddr}' - ${error.stack}`, + `Failed to ${term1.toLowerCase()} reporting for '${device.ieeeAddr}' - ${error.stack}`, ); this.failed.add(device.ieeeAddr); @@ -173,10 +188,18 @@ class Report extends Extension { if (messageType === 'deviceAnnounce' && utils.isIkeaTradfriDevice(device)) return true; if (resolvedEntity.device.interviewing === true) return false; - if (device.meta.hasOwnProperty('reporting') && device.meta.reporting === reportKey) return false; if (device.type !== 'Router' || device.powerSource === 'Battery') return false; // Gledopto devices don't support reporting. if (devicesNotSupportingReporting.includes(definition) || definition.vendor === 'Gledopto') return false; + + if (this.enabled && device.meta.hasOwnProperty('reporting') && device.meta.reporting === reportKey) { + return false; + } + + if (!this.enabled && !device.meta.hasOwnProperty('reporting')) { + return false; + } + return true; } diff --git a/test/configure.test.js b/test/configure.test.js index 746edcfde..205fcfa55 100644 --- a/test/configure.test.js +++ b/test/configure.test.js @@ -68,6 +68,16 @@ describe('Configure', () => { expectRemoteConfigured(); }); + it('Should reconfigure reporting on reportingDisabled event', async () => { + expectRemoteConfigured(); + const device = zigbeeHerdsman.devices.remote; + mockClear(device); + expectRemoteNotConfigured(); + controller.eventBus.emit('reportingDisabled', {device}) + await flushPromises(); + expectRemoteConfigured(); + }); + it('Should not configure twice', async () => { expectRemoteConfigured(); const device = zigbeeHerdsman.devices.remote; diff --git a/test/report.test.js b/test/report.test.js index 5505bf768..85cb9863e 100644 --- a/test/report.test.js +++ b/test/report.test.js @@ -24,6 +24,9 @@ describe('Report', () => { function expectOnOffBrightnessColorReport(endpoint, colorXY) { const coordinatorEndpoint = zigbeeHerdsman.devices.coordinator.getEndpoint(1); + const device = endpoint.getDevice(); + expect(device.meta.reporting).toBe(1); + expect(endpoint.unbind).toHaveBeenCalledTimes(0); expect(endpoint.bind).toHaveBeenCalledTimes(3); expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', coordinatorEndpoint); expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', coordinatorEndpoint); @@ -38,12 +41,32 @@ describe('Report', () => { } } + function expectOnOffBrightnessColorReportDisabled(endpoint, colorXY) { + const coordinatorEndpoint = zigbeeHerdsman.devices.coordinator.getEndpoint(1); + const device = endpoint.getDevice(); + expect(device.meta.reporting).toBe(undefined); + expect(endpoint.unbind).toHaveBeenCalledTimes(3); + expect(endpoint.bind).toHaveBeenCalledTimes(0); + expect(endpoint.unbind).toHaveBeenCalledWith('genOnOff', coordinatorEndpoint); + expect(endpoint.unbind).toHaveBeenCalledWith('genLevelCtrl', coordinatorEndpoint); + expect(endpoint.unbind).toHaveBeenCalledWith('lightingColorCtrl', coordinatorEndpoint); + expect(endpoint.configureReporting).toHaveBeenCalledTimes(3); + expect(endpoint.configureReporting).toHaveBeenCalledWith('genOnOff', [{"attribute": "onOff", "maximumReportInterval": 0xFFFF, "minimumReportInterval": 0, "reportableChange": 0}]); + expect(endpoint.configureReporting).toHaveBeenCalledWith('genLevelCtrl', [{"attribute": "currentLevel", "maximumReportInterval": 0xFFFF, "minimumReportInterval": 3, "reportableChange": 1}]); + if (colorXY) { + expect(endpoint.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [{"attribute": "colorTemperature", "maximumReportInterval": 0xFFFF, "minimumReportInterval": 3, "reportableChange": 1}, {"attribute": "currentX", "maximumReportInterval": 0xFFFF, "minimumReportInterval": 3, "reportableChange": 1}, {"attribute": "currentY", "maximumReportInterval": 0xFFFF, "minimumReportInterval": 3, "reportableChange": 1}]); + } else { + expect(endpoint.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [{"attribute": "colorTemperature", "maximumReportInterval": 0xFFFF, "minimumReportInterval": 3, "reportableChange": 1}]); + } + } + mockClear = (device) => { for (const endpoint of device.endpoints) { endpoint.read.mockClear(); endpoint.write.mockClear(); endpoint.configureReporting.mockClear(); endpoint.bind.mockClear(); + endpoint.unbind.mockClear(); } } @@ -52,6 +75,11 @@ describe('Report', () => { settings._reRead(); data.writeEmptyState(); settings.set(['advanced', 'report'], true); + for (const device of Object.values(zigbeeHerdsman.devices)) { + mockClear(device); + delete device.meta.reporting; + } + controller = new Controller(); await controller.start(); mocksClear.forEach((m) => m.mockClear()); @@ -64,6 +92,31 @@ describe('Report', () => { expectOnOffBrightnessColorReport(endpoint, true); }); + it('Should not configure reporting on startup when disabled', async () => { + const device = zigbeeHerdsman.devices.bulb_color; + const endpoint = device.getEndpoint(1); + mockClear(device); + delete device.meta.report; + settings.set(['advanced', 'report'], false); + controller = new Controller(); + await controller.start(); + await flushPromises(); + expect(device.meta.reporting).toBe(undefined); + expect(endpoint.bind).toHaveBeenCalledTimes(0); + }); + + it('Should disable reporting on startup when enabled earlier', async () => { + const device = zigbeeHerdsman.devices.bulb_color; + device.meta.reporting = 1; + const endpoint = device.getEndpoint(1); + settings.set(['advanced', 'report'], false); + mockClear(device); + controller = new Controller(); + await controller.start(); + await flushPromises(); + expectOnOffBrightnessColorReportDisabled(endpoint, true); + }); + it('Should configure reporting when receicing message from device which has not been setup yet', async () => { const device = zigbeeHerdsman.devices.bulb; const endpoint = device.getEndpoint(1);