2019-09-09 10:48:09 -07:00
|
|
|
const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
|
2019-02-26 12:21:35 -07:00
|
|
|
const logger = require('../util/logger');
|
2020-01-07 11:59:43 -07:00
|
|
|
const ZNLDP12LM = zigbeeHerdsmanConverters.devices.find((d) => d.model === 'ZNLDP12LM');
|
2019-04-16 08:56:28 -07:00
|
|
|
const utils = require('../util/utils');
|
2020-04-11 09:10:56 -07:00
|
|
|
const Extension = require('./extension');
|
2019-10-24 13:15:40 -07:00
|
|
|
const debounce = require('debounce');
|
|
|
|
const ZigbeeHerdsman = require('zigbee-herdsman');
|
2019-02-01 11:04:49 -07:00
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
const defaultConfiguration = {
|
2019-12-07 09:54:40 -07:00
|
|
|
minimumReportInterval: 3, maximumReportInterval: 300, reportableChange: 1,
|
2019-02-01 11:04:49 -07:00
|
|
|
};
|
|
|
|
|
2020-04-02 09:11:49 -07:00
|
|
|
const devicesNotSupportingReporting = [
|
|
|
|
zigbeeHerdsmanConverters.devices.find((d) => d.model === 'CC2530.ROUTER'),
|
|
|
|
zigbeeHerdsmanConverters.devices.find((d) => d.model === 'BASICZBR3'),
|
|
|
|
zigbeeHerdsmanConverters.devices.find((d) => d.model === 'ZM-CSW032-D'),
|
2020-04-09 11:00:24 -07:00
|
|
|
zigbeeHerdsmanConverters.devices.find((d) => d.model === 'TS0001'),
|
2020-04-02 09:11:49 -07:00
|
|
|
];
|
2020-01-16 14:52:13 -07:00
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
const reportKey = 1;
|
|
|
|
|
2020-04-01 11:33:04 -07:00
|
|
|
const getColorCapabilities = async (endpoint) => {
|
|
|
|
if (endpoint.getClusterAttributeValue('lightingColorCtrl', 'colorCapabilities') === undefined) {
|
|
|
|
await endpoint.read('lightingColorCtrl', ['colorCapabilities']);
|
|
|
|
}
|
|
|
|
|
|
|
|
const value = endpoint.getClusterAttributeValue('lightingColorCtrl', 'colorCapabilities');
|
|
|
|
return {
|
|
|
|
colorTemperature: (value & 1<<4) > 0,
|
|
|
|
colorXY: (value & 1<<3) > 0,
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
const clusters = {
|
|
|
|
'genOnOff': [
|
|
|
|
{attribute: 'onOff', ...defaultConfiguration, minimumReportInterval: 0, reportableChange: 0},
|
|
|
|
],
|
|
|
|
'genLevelCtrl': [
|
|
|
|
{attribute: 'currentLevel', ...defaultConfiguration},
|
|
|
|
],
|
|
|
|
'lightingColorCtrl': [
|
2020-04-01 11:33:04 -07:00
|
|
|
{
|
|
|
|
attribute: 'colorTemperature', ...defaultConfiguration,
|
|
|
|
condition: async (endpoint) => (await getColorCapabilities(endpoint)).colorTemperature,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
attribute: 'currentX', ...defaultConfiguration,
|
|
|
|
condition: async (endpoint) => (await getColorCapabilities(endpoint)).colorXY,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
attribute: 'currentY', ...defaultConfiguration,
|
|
|
|
condition: async (endpoint) => (await getColorCapabilities(endpoint)).colorXY,
|
|
|
|
},
|
2019-09-09 10:48:09 -07:00
|
|
|
],
|
|
|
|
'closuresWindowCovering': [
|
|
|
|
{attribute: 'currentPositionLiftPercentage', ...defaultConfiguration},
|
|
|
|
{attribute: 'currentPositionTiltPercentage', ...defaultConfiguration},
|
|
|
|
],
|
2019-02-01 11:04:49 -07:00
|
|
|
};
|
|
|
|
|
2019-10-24 13:15:40 -07:00
|
|
|
const pollOnMessage = [
|
|
|
|
{
|
|
|
|
// Key is used this.pollDebouncers uniqueness
|
|
|
|
key: 1,
|
|
|
|
// On messages that have the cluster and type of below
|
|
|
|
cluster: {
|
2019-10-25 10:42:47 -07:00
|
|
|
manuSpecificPhilips: [
|
|
|
|
{type: 'commandHueNotification', data: {button: 2}},
|
|
|
|
{type: 'commandHueNotification', data: {button: 3}},
|
|
|
|
],
|
2019-10-24 13:15:40 -07:00
|
|
|
genLevelCtrl: [
|
2019-10-25 10:42:47 -07:00
|
|
|
{type: 'commandStep', data: {}},
|
|
|
|
{type: 'commandStepWithOnOff', data: {}},
|
|
|
|
{type: 'commandStop', data: {}},
|
|
|
|
{type: 'commandMoveWithOnOff', data: {}},
|
|
|
|
{type: 'commandStopWithOnOff', data: {}},
|
|
|
|
{type: 'commandMove', data: {}},
|
2019-10-24 13:15:40 -07:00
|
|
|
],
|
|
|
|
},
|
|
|
|
// Read the following attributes
|
|
|
|
read: {cluster: 'genLevelCtrl', attributes: ['currentLevel']},
|
|
|
|
// When the bound devices/members of group have the following manufacturerID
|
|
|
|
manufacturerID: ZigbeeHerdsman.Zcl.ManufacturerCode.Philips,
|
|
|
|
},
|
2020-02-08 12:53:16 -07:00
|
|
|
{
|
|
|
|
key: 2,
|
|
|
|
cluster: {
|
|
|
|
genOnOff: [
|
|
|
|
{type: 'commandOn', data: {}},
|
|
|
|
{type: 'commandOff', data: {}},
|
2020-02-22 10:45:30 -07:00
|
|
|
{type: 'commandOffWithEffect', data: {}},
|
2020-02-08 12:53:16 -07:00
|
|
|
],
|
2020-02-14 14:41:04 -07:00
|
|
|
manuSpecificPhilips: [
|
|
|
|
{type: 'commandHueNotification', data: {button: 1}},
|
|
|
|
{type: 'commandHueNotification', data: {button: 4}},
|
|
|
|
],
|
2020-02-08 12:53:16 -07:00
|
|
|
},
|
|
|
|
read: {cluster: 'genOnOff', attributes: ['onOff']},
|
|
|
|
manufacturerID: ZigbeeHerdsman.Zcl.ManufacturerCode.Philips,
|
|
|
|
},
|
2019-10-24 13:15:40 -07:00
|
|
|
];
|
|
|
|
|
2020-04-11 09:10:56 -07:00
|
|
|
class DeviceReport extends Extension {
|
2020-01-09 13:47:19 -07:00
|
|
|
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
|
|
|
|
super(zigbee, mqtt, state, publishEntityState, eventBus);
|
2019-09-09 10:48:09 -07:00
|
|
|
this.configuring = new Set();
|
2019-10-12 09:02:15 -07:00
|
|
|
this.failed = new Set();
|
2019-10-24 13:15:40 -07:00
|
|
|
this.pollDebouncers = {};
|
2019-02-01 11:04:49 -07:00
|
|
|
}
|
|
|
|
|
2020-04-05 06:41:24 -07:00
|
|
|
shouldIgnoreClusterForDevice(cluster, definition) {
|
|
|
|
if (definition === ZNLDP12LM && cluster === 'closuresWindowCovering') {
|
2020-01-07 11:59:43 -07:00
|
|
|
// Device announces it but doesn't support it
|
|
|
|
// https://github.com/Koenkk/zigbee2mqtt/issues/2611
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2020-04-05 06:41:24 -07:00
|
|
|
async setupReporting(resolvedEntity) {
|
|
|
|
const {device, definition} = resolvedEntity;
|
|
|
|
|
2019-10-12 09:02:15 -07:00
|
|
|
if (this.configuring.has(device.ieeeAddr) || this.failed.has(device.ieeeAddr)) return;
|
2019-09-09 10:48:09 -07:00
|
|
|
this.configuring.add(device.ieeeAddr);
|
|
|
|
|
|
|
|
try {
|
2020-01-07 11:59:43 -07:00
|
|
|
for (const ep of device.endpoints) {
|
2019-09-09 10:48:09 -07:00
|
|
|
for (const [cluster, configuration] of Object.entries(clusters)) {
|
2020-04-05 06:41:24 -07:00
|
|
|
if (ep.supportsInputCluster(cluster) && !this.shouldIgnoreClusterForDevice(cluster, definition)) {
|
2020-01-07 11:59:43 -07:00
|
|
|
logger.debug(`Setup reporting for '${device.ieeeAddr}' - ${ep.ID} - ${cluster}`);
|
2020-04-01 11:33:04 -07:00
|
|
|
|
|
|
|
const items = [];
|
|
|
|
for (const entry of configuration) {
|
|
|
|
if (!entry.hasOwnProperty('condition') || (await entry.condition(ep))) {
|
|
|
|
items.push({...entry});
|
|
|
|
delete items[items.length - 1].condition;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-07 11:59:43 -07:00
|
|
|
await ep.bind(cluster, this.coordinatorEndpoint);
|
2020-04-01 11:33:04 -07:00
|
|
|
await ep.configureReporting(cluster, items);
|
2019-09-09 10:48:09 -07:00
|
|
|
logger.info(
|
2020-01-08 11:29:22 -07:00
|
|
|
`Successfully setup reporting for '${device.ieeeAddr}' - ${ep.ID} - ${cluster}`,
|
2019-09-09 10:48:09 -07:00
|
|
|
);
|
|
|
|
}
|
2019-02-26 12:21:35 -07:00
|
|
|
}
|
2019-02-01 11:04:49 -07:00
|
|
|
}
|
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
// eslint-disable-next-line
|
|
|
|
device.meta.reporting = reportKey;
|
|
|
|
} catch (error) {
|
|
|
|
logger.error(
|
2019-10-26 09:25:51 -07:00
|
|
|
`Failed to setup reporting for '${device.ieeeAddr}' - ${error.stack}`,
|
2019-09-09 10:48:09 -07:00
|
|
|
);
|
2019-10-12 09:02:15 -07:00
|
|
|
|
|
|
|
this.failed.add(device.ieeeAddr);
|
2019-09-09 10:48:09 -07:00
|
|
|
}
|
2019-02-22 13:06:24 -07:00
|
|
|
|
2019-10-01 11:22:47 -07:00
|
|
|
device.save();
|
2019-09-09 10:48:09 -07:00
|
|
|
this.configuring.delete(device.ieeeAddr);
|
2019-02-01 11:04:49 -07:00
|
|
|
}
|
|
|
|
|
2020-04-05 06:41:24 -07:00
|
|
|
shouldSetupReporting(resolvedEntity, messageType) {
|
|
|
|
if (!resolvedEntity || !resolvedEntity.device || !resolvedEntity.definition ||
|
|
|
|
messageType === 'deviceLeave') return false;
|
2019-09-09 10:48:09 -07:00
|
|
|
|
2020-04-05 06:41:24 -07:00
|
|
|
const {device, definition} = resolvedEntity;
|
2019-02-26 12:21:35 -07:00
|
|
|
// Handle messages of type endDeviceAnnce and devIncoming.
|
2019-02-01 11:04:49 -07:00
|
|
|
// 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
|
2019-09-09 10:48:09 -07:00
|
|
|
if (messageType === 'deviceAnnounce' && utils.isIkeaTradfriDevice(device)) return true;
|
|
|
|
|
|
|
|
if (device.meta.hasOwnProperty('reporting') && device.meta.reporting === reportKey) return false;
|
|
|
|
if (!utils.isRouter(device) || utils.isBatteryPowered(device)) return false;
|
2019-10-15 11:16:04 -07:00
|
|
|
// Gledopto devices don't support reporting.
|
2020-04-05 06:41:24 -07:00
|
|
|
if (devicesNotSupportingReporting.includes(definition) || definition.vendor === 'Gledopto') return false;
|
2019-09-09 10:48:09 -07:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
async onZigbeeStarted() {
|
2019-11-25 09:44:44 -07:00
|
|
|
this.coordinatorEndpoint = this.zigbee.getDevicesByType('Coordinator')[0].getEndpoint(1);
|
2019-09-09 10:48:09 -07:00
|
|
|
|
2019-09-23 13:21:27 -07:00
|
|
|
for (const device of this.zigbee.getClients()) {
|
2020-04-05 06:41:24 -07:00
|
|
|
const resolvedEntity = this.zigbee.resolveEntity(device);
|
|
|
|
if (this.shouldSetupReporting(resolvedEntity, null)) {
|
|
|
|
this.setupReporting(resolvedEntity);
|
2019-09-09 10:48:09 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-05 06:41:24 -07:00
|
|
|
onZigbeeEvent(type, data, resolvedEntity) {
|
|
|
|
if (this.shouldSetupReporting(resolvedEntity, type)) {
|
|
|
|
this.setupReporting(resolvedEntity);
|
2019-02-01 11:04:49 -07:00
|
|
|
}
|
2019-10-24 13:15:40 -07:00
|
|
|
|
|
|
|
if (type === 'message') {
|
|
|
|
this.poll(data);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
poll(message) {
|
|
|
|
/**
|
|
|
|
* This method poll bound endpoints and group members for state changes.
|
|
|
|
*
|
|
|
|
* A use case is e.g. a Hue Dimmer switch bound to a Hue bulb.
|
|
|
|
* Hue bulbs only report their on/off state.
|
|
|
|
* When dimming the bulb via the dimmer switch the state is therefore not reported.
|
|
|
|
* When we receive a message from a Hue dimmer we read the brightness from the bulb (if bound).
|
|
|
|
*/
|
|
|
|
const polls = pollOnMessage.filter((p) =>
|
2019-10-25 10:42:47 -07:00
|
|
|
p.cluster[message.cluster] && p.cluster[message.cluster].find((c) => c.type === message.type &&
|
2019-10-26 09:25:51 -07:00
|
|
|
utils.equalsPartial(message.data, c.data)),
|
2019-10-24 13:15:40 -07:00
|
|
|
);
|
|
|
|
|
|
|
|
if (polls.length) {
|
|
|
|
let toPoll = [];
|
|
|
|
|
|
|
|
// Add bound devices
|
2020-03-30 10:50:35 -07:00
|
|
|
toPoll = toPoll.concat([].concat(...message.device.endpoints.map((e) =>
|
|
|
|
e.binds.map((e) => e).filter((e) => e.target))));
|
2019-10-24 13:15:40 -07:00
|
|
|
toPoll = toPoll.filter((e) => e.target.constructor.name === 'Endpoint');
|
|
|
|
toPoll = toPoll.filter((e) => e.target.getDevice().type !== 'Coordinator');
|
|
|
|
toPoll = toPoll.map((e) => e.target);
|
|
|
|
|
|
|
|
// If message is published to a group, add members of the group
|
|
|
|
const group = message.groupID !== 0 ? this.zigbee.getGroupByID(message.groupID) : null;
|
|
|
|
if (group) {
|
|
|
|
toPoll = toPoll.concat(group.members);
|
|
|
|
}
|
|
|
|
|
|
|
|
toPoll = new Set(toPoll);
|
|
|
|
|
|
|
|
for (const endpoint of toPoll) {
|
|
|
|
for (const poll of polls) {
|
|
|
|
if (poll.manufacturerID !== endpoint.getDevice().manufacturerID) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
const key = `${endpoint.deviceIeeeAddress}_${endpoint.ID}_${poll.key}`;
|
|
|
|
if (!this.pollDebouncers[key]) {
|
|
|
|
this.pollDebouncers[key] = debounce(async () => {
|
|
|
|
await endpoint.read(poll.read.cluster, poll.read.attributes);
|
|
|
|
}, 1000);
|
|
|
|
}
|
|
|
|
|
|
|
|
this.pollDebouncers[key]();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-02-01 11:04:49 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-02-13 12:55:14 -07:00
|
|
|
module.exports = DeviceReport;
|