mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-08-28 05:24:29 +00:00
Add reporting feature. #966
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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}'`);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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,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
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user