From b8744ce890b6b80c826d6578280e3c61d672007e Mon Sep 17 00:00:00 2001 From: Koen Kanters Date: Fri, 1 Feb 2019 19:04:49 +0100 Subject: [PATCH] Add reporting feature. #966 --- .../device_specific_configuration.md | 2 + lib/controller.js | 2 + lib/extension/bind.js | 4 +- lib/extension/deviceConfigure.js | 8 -- lib/extension/reporting.js | 76 +++++++++++++++++++ lib/util/settings.js | 1 + lib/util/utils.js | 2 + lib/zigbee.js | 43 +++++++++-- 8 files changed, 122 insertions(+), 16 deletions(-) create mode 100644 lib/extension/reporting.js diff --git a/docs/configuration/device_specific_configuration.md b/docs/configuration/device_specific_configuration.md index aa08eecc4..746a6a2ab 100644 --- a/docs/configuration/device_specific_configuration.md +++ b/docs/configuration/device_specific_configuration.md @@ -5,6 +5,7 @@ The `configuration.yaml` allows to set device specific configuration. The follow * `friendly_name`: Used in the MQTT topic of a device. By default this is the device ID (e.g. `0x00128d0001d9e1d2`). * `retain`: Retain MQTT messages of this device. * `qos`: QoS level for MQTT messages of this device. [What is QoS?](https://www.npmjs.com/package/mqtt#about-qos) +* `report`: The device will be setup to report it's changed state when not directly controlled by zigbee2mqtt (e.g. via a remote control). ### Device type specific * `occupancy_timeout`: Timeout (in seconds) after the `occupancy: false` message is sent, only available for occupany sensors. If not set, the timeout is `90` seconds. When set to `0` no `occupancy: false` is send. @@ -20,6 +21,7 @@ devices: retain: true occupancy_timeout: 20 qos: 1 + report: true ``` ### Changing device type specific defaults diff --git a/lib/controller.js b/lib/controller.js index c65778437..f59048ee0 100644 --- a/lib/controller.js +++ b/lib/controller.js @@ -20,6 +20,7 @@ const ExtensionGroups = require('./extension/groups'); const ExtensionDeviceAvailability = require('./extension/deviceAvailability'); const ExtensionBind = require('./extension/bind'); const ExtensionCoordinatorGroup = require('./extension/coordinatorGroup'); +const ExtensionReporting = require('./extension/reporting'); class Controller { constructor() { @@ -45,6 +46,7 @@ class Controller { new ExtensionGroups(this.zigbee, this.mqtt, this.state, this.publishDeviceState), new ExtensionBind(this.zigbee, this.mqtt, this.state, this.publishDeviceState), new ExtensionCoordinatorGroup(this.zigbee, this.mqtt, this.state, this.publishDeviceState), + new ExtensionReporting(this.zigbee, this.mqtt, this.state, this.publishDeviceState), ]; if (settings.get().homeassistant) { diff --git a/lib/extension/bind.js b/lib/extension/bind.js index 898aba398..e70275499 100644 --- a/lib/extension/bind.js +++ b/lib/extension/bind.js @@ -61,7 +61,7 @@ class Bind { // Find source; can only be a device. const sourceEntity = utils.resolveEntity(topic.ID); - const source = this.zigbee.findDevice(sourceEntity.ID); + const source = this.zigbee.getEndpoint(sourceEntity.ID); if (!source) { logger.error(`Failed to find device '${sourceEntity.ID}'`); @@ -73,7 +73,7 @@ class Bind { let target = null; if (targetEntity.type === 'device') { - target = this.zigbee.findDevice(targetEntity.ID); + target = this.zigbee.getEndpoint(targetEntity.ID); if (!target) { logger.error(`Failed to find target device '${targetEntity.ID}'`); diff --git a/lib/extension/deviceConfigure.js b/lib/extension/deviceConfigure.js index b6eeaeca7..9ad6024df 100644 --- a/lib/extension/deviceConfigure.js +++ b/lib/extension/deviceConfigure.js @@ -23,14 +23,6 @@ class DeviceConfigure { onZigbeeMessage(message, device, mappedDevice) { if (device && mappedDevice) { - // endDeviceAnnce is typically send when a device comes online after being - // powered off. In some cases this requires a re-configure of the device. - // Mark this device as not configured. - // https://github.com/Koenkk/zigbee2mqtt/issues/966 - if (message.type === 'endDeviceAnnce') { - this.mark(device.ieeeAddr, false); - } - this.configure(device, mappedDevice); } } diff --git a/lib/extension/reporting.js b/lib/extension/reporting.js new file mode 100644 index 000000000..3b37cdccd --- /dev/null +++ b/lib/extension/reporting.js @@ -0,0 +1,76 @@ +const settings = require('../util/settings'); +const utils = require('../util/utils'); + +const candidates = { + 'genOnOff': ['onOff'], + 'genLevelCtrl': ['currentLevel'], + 'lightingColorCtrl': ['colorTemperature'], +}; + +const reportInterval = { + min: 0, + max: 3600, +}; + +const reportableChange = 0; + +class Reporting { + constructor(zigbee, mqtt, state, publishDeviceState) { + this.zigbee = zigbee; + this.mqtt = mqtt; + this.state = state; + this.publishDeviceState = publishDeviceState; + } + + shouldReport(ieeeAddr) { + const device = settings.getDevice(ieeeAddr); + return device && device.report; + } + + getEndpoints() { + return this.zigbee.getAllClients() + .filter((d) => this.shouldReport(d.ieeeAddr)) + .map((d) => this.zigbee.getEndpoint(d.ieeeAddr)) + .filter((e) => e); + } + + setupReporting(endpoint) { + Object.values(endpoint.clusters).filter((c) => c).forEach((c) => { + const cluster = c.attrs.cid; + if (candidates[cluster]) { + const attributes = candidates[cluster].filter((a) => c.attrs.hasOwnProperty(a)); + attributes.forEach((attribute) => { + this.zigbee.endpointReport( + endpoint, + cluster, + attribute, + reportInterval.max, + reportInterval.max, + reportableChange); + }); + } + }); + } + + onZigbeeStarted() { + const endpoints = this.getEndpoints(); + endpoints.forEach((e) => this.setupReporting(e)); + } + + onZigbeeMessage(message, device, mappedDevice) { + // Handle messages of type endDeviceAnnce. + // This message is typically send when a device comes online after being powered off + // Ikea TRADFRI tend to forget their reporting after powered off. + // Re-setup reporting. + // https://github.com/Koenkk/zigbee2mqtt/issues/966 + if (device && message.type === 'endDeviceAnnce' && utils.isIkeaTradfriDevice(device) && + this.shouldReport(device.ieeeAddr)) { + const endpoint = this.zigbee.getEndpoint(device.ieeeAddr); + if (endpoint) { + this.setupReporting(endpoint); + } + } + } +} + +module.exports = Reporting; diff --git a/lib/util/settings.js b/lib/util/settings.js index ff96760e6..c29517ccd 100644 --- a/lib/util/settings.js +++ b/lib/util/settings.js @@ -123,6 +123,7 @@ module.exports = { write: () => write(), getDevice: (ieeeAddr) => settings.devices ? settings.devices[ieeeAddr] : null, + getDevices: () => settings.devices ? settings.devices : [], addDevice: (ieeeAddr) => addDevice(ieeeAddr), removeDevice: (ieeeAddr) => removeDevice(ieeeAddr), diff --git a/lib/util/utils.js b/lib/util/utils.js index 9168d588a..313ec432f 100644 --- a/lib/util/utils.js +++ b/lib/util/utils.js @@ -2,6 +2,7 @@ const settings = require('./settings'); // Xiaomi uses 4151 and 4447 (lumi.plug) as manufacturer ID. const xiaomiManufacturerID = [4151, 4447]; +const ikeaTradfriManufacturerID = [4476]; // An entity can be either a group or a device. function resolveEntity(ID) { @@ -27,6 +28,7 @@ module.exports = { millisecondsToSeconds: (milliseconds) => milliseconds / 1000, secondsToMilliseconds: (seconds) => seconds * 1000, isXiaomiDevice: (device) => xiaomiManufacturerID.includes(device.manufId), + isIkeaTradfriDevice: (device) => ikeaTradfriManufacturerID.includes(device.manufId), isNumeric: (string) => /^\d+$/.test(string), resolveEntity: (ID) => resolveEntity(ID), }; diff --git a/lib/zigbee.js b/lib/zigbee.js index 4968bd437..b8d64af99 100644 --- a/lib/zigbee.js +++ b/lib/zigbee.js @@ -4,6 +4,8 @@ const settings = require('./util/settings'); const data = require('./util/data'); const utils = require('./util/utils'); const cieApp = require('./zapp/cie'); +const Queue = require('queue'); +const zclId = require('zcl-id'); const advancedSettings = settings.get().advanced; const shepherdSettings = { @@ -24,6 +26,10 @@ const defaultCfg = { disDefaultRsp: 0, }; +const foundationCfg = {manufSpec: 0, disDefaultRsp: 0}; + +const delay = 170; + logger.debug(`Using zigbee-shepherd with settings: '${JSON.stringify(shepherdSettings)}'`); class Zigbee { @@ -32,6 +38,10 @@ class Zigbee { this.onMessage = this.onMessage.bind(this); this.onError = this.onError.bind(this); this.messageHandler = null; + + this.queue = new Queue(); + this.queue.concurrency = 1; + this.queue.autostart = true; } start(messageHandler, callback) { @@ -200,7 +210,7 @@ class Zigbee { publish(entityID, entityType, cid, cmd, cmdType, zclData, cfg=defaultCfg, ep, callback) { let entity = null; if (entityType === 'device') { - entity = this.findDevice(entityID, ep); + entity = this.getEndpoint(entityID, ep); } else if (entityType === 'group') { entity = this.getGroup(entityID); } @@ -245,17 +255,38 @@ class Zigbee { }); } - findDevice(deviceID, ep) { + getEndpoint(ieeeAddr, ep) { + // If no ep is given, the first endpoint will be returned // Find device in zigbee-shepherd - let device = this.getDevice(deviceID); + const device = this.getDevice(ieeeAddr); if (!device || !device.epList || !device.epList.length) { - logger.error(`Zigbee cannot determine endpoint for '${deviceID}'`); + logger.error(`Zigbee cannot determine endpoint for '${ieeeAddr}'`); return null; } ep = ep ? ep : device.epList[0]; - device = this.shepherd.find(deviceID, ep); - return device; + const endpoint = this.shepherd.find(ieeeAddr, ep); + return endpoint; + } + + endpointReport(ep, cluster, attribute, min, max, change) { + const attrId = zclId.attr(cluster, attribute).value; + const dataType = zclId.attrType(cluster, attribute).value; + const cfg = {direction: 0, attrId, dataType, minRepIntval: min, maxRepIntval: max, repChange: change}; + const log = `for ${ep.device.ieeeAddr} - ${cluster} - ${attribute}`; + + this.queue.push((queueCallback) => { + logger.debug(`Setup reporting ${log}`); + ep.foundation('genOnOff', 'configReport', [cfg], foundationCfg, (error) => { + if (error) { + logger.error(`Failed to setup reporting ${log} - (${error})`); + } else { + logger.debug(`Successfully setup reporting ${log}`); + } + }); + + setTimeout(() => queueCallback(), delay); + }); } }