Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Poll device when device does not support reporting. #2122

Merged
merged 5 commits into from
Oct 24, 2019
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 74 additions & 87 deletions lib/extension/deviceReport.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters');
const logger = require('../util/logger');
const CC2530Router = zigbeeHerdsmanConverters.devices.find((d) => d.model === 'CC2530.ROUTER');
const utils = require('../util/utils');
const settings = require('../util/settings');
const BaseExtension = require('./baseExtension');
const debounce = require('debounce');
const ZigbeeHerdsman = require('zigbee-herdsman');

const defaultConfiguration = {
minimumReportInterval: 3, maximumReportInterval: 300, reportableChange: 0,
Expand All @@ -30,12 +30,31 @@ const clusters = {
],
};

const pollOnMessage = [
{
// Key is used this.pollDebouncers uniqueness
key: 1,
// On messages that have the cluster and type of below
cluster: {
manuSpecificPhilips: ['commandHueNotification'],
genLevelCtrl: [
'commandStep', 'commandStepWithOnOff', 'commandStop', 'commandMoveWithOnOff', 'commandStopWithOnOff',
'commandMove',
],
},
// Read the following attributes
read: {cluster: 'genLevelCtrl', attributes: ['currentLevel']},
// When the bound devices have the following manufacturerID
manufacturerID: ZigbeeHerdsman.Zcl.ManufacturerCode.Philips,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want Geldopto too? IIRC some of them also lack or have not working attReporting?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes we can easily add that later.

},
];

class DeviceReport extends BaseExtension {
constructor(zigbee, mqtt, state, publishEntityState) {
super(zigbee, mqtt, state, publishEntityState);
this.configuring = new Set();
this.failed = new Set();
this.emulators = {};
this.pollDebouncers = {};
}

async setupReporting(device) {
Expand Down Expand Up @@ -87,86 +106,6 @@ class DeviceReport extends BaseExtension {
return true;
}


shouldDoReportingEmulation(device) {
if (device.type == "Coordinator") return false;

const settingsDevice = settings.getDevice(device.ieeeAddr);
return (settingsDevice && settingsDevice.hasOwnProperty("report_emulate") &&
Array.isArray(settingsDevice.report_emulate));
}

async handleReportingEmulation(device, group) {
const endpoints = new Set();

// lookup endpoints bound to device
for (const endpoint of device.endpoints) {
for (const binding of endpoint.binds) {
if (binding.target.hasOwnProperty('deviceID')) {
if (!endpoints.has(binding.target)) {
endpoints.add(binding.target);
}
}
}
}

// lookup endpoints part of group (if provided)
if (group) {
for (const endpoint of group.members) {
if (!endpoints.has(endpoint)) {
endpoints.add(endpoint);
}
}
}

// lookup devices attached to endpoints
if (endpoints.size) {
for (const endpoint of endpoints) {
try {
const endpointDevice = this.zigbee.getDeviceByIeeeAddr(endpoint.deviceIeeeAddress);
if (this.shouldDoReportingEmulation(endpointDevice)) {
// use debounce so we do not flood the network with
// requests to update
if (!this.emulators[endpoint.deviceIeeeAddress]) {
this.emulators[endpoint.deviceIeeeAddress] = debounce(() => {
const settingsDevice = settings.getDevice(endpoint.deviceIeeeAddress);
const model = zigbeeHerdsmanConverters.findByZigbeeModel(endpointDevice.modelID);
if (!model) {
logger.warn(`Could not emulate reporting for ${endpointDevice.ieeeAddr}, unknown device modelID '${endpointDevice.modelID}'`);
return;
}
logger.debug(`Emulating report for ${endpointDevice.ieeeAddr}`);

const converters = model.toZigbee;
const usedConverters = [];
for (const key of settingsDevice.report_emulate) {
const converter = converters.find((c) => c.key.includes(key));

if (converter && converter.convertGet) {
if (usedConverters.includes(converter)) return;
converter.convertGet(endpoint, key, {});
} else {
logger.error(`Cannot find converter to emulate reporting of '${key}' for '${endpointDevice.ieeeAddr}'`);
}

usedConverters.push(converter);
}
}, 1000);
}
this.emulators[endpointDevice.ieeeAddr].clear()
this.emulators[endpointDevice.ieeeAddr]()
}
} catch (error) {
logger.error(
`Failed to emulate reporting for '${endpoint.deviceIeeeAddress}' - ${error.stack}`
);
}
}
} else {
logger.debug(`No devices endpoints discovered, no reporting emulation required`);
}
}

async onZigbeeStarted() {
this.coordinatorEndpoint = this.zigbee.getDevicesByType('Coordinator')[0].endpoints[0];

Expand All @@ -182,11 +121,59 @@ class DeviceReport extends BaseExtension {
if (this.shouldSetupReporting(mappedDevice, data.device, type)) {
this.setupReporting(data.device);
}
if (type == "message" && (data.type != "attributeReport" &&
data.type != "readResponse" &&
data.type != "raw")) {
const group = data.groupID > 0 ? this.zigbee.getGroupByID(data.groupID) : null;
this.handleReportingEmulation(data.device, group);

if (type === 'message') {
this.poll(data);
}
}

poll(message) {
/**
* This method poll bound endpoints 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) =>
p.cluster[message.cluster] && p.cluster[message.cluster].includes(message.type)
);

if (polls.length) {
let toPoll = [];

// Add bound devices
toPoll = toPoll.concat([].concat(...message.device.endpoints.map((e) => e.binds.map((e) => e))));
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]();
}
}
}
}
}
Expand Down