Add reporting feature. #966

This commit is contained in:
Koen Kanters
2019-02-01 19:04:49 +01:00
parent dd8ee60535
commit b8744ce890
8 changed files with 122 additions and 16 deletions
@@ -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
+2
View File
@@ -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) {
+2 -2
View File
@@ -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}'`);
-8
View File
@@ -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);
}
}
+76
View File
@@ -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;
+1
View File
@@ -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),
+2
View File
@@ -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),
};
+37 -6
View File
@@ -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);
});
}
}