Allow to bind specific endpoint. #176

This commit is contained in:
Koen Kanters
2019-03-15 21:19:42 +01:00
parent d4d85633d2
commit 969585c84e
5 changed files with 229 additions and 41 deletions
+52 -32
View File
@@ -1,7 +1,9 @@
const settings = require('../util/settings');
const logger = require('../util/logger');
const Queue = require('queue');
const utils = require('../util/utils');
const zigbeeShepherdConverters = require('zigbee-shepherd-converters');
const postfixes = utils.getPostfixes();
const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/(bind|unbind)/.+$`);
const allowedClusters = [
@@ -17,16 +19,25 @@ class DeviceBind {
this.mqtt = mqtt;
this.state = state;
this.publishEntityState = publishEntityState;
// Setup queue
this.queue = new Queue();
this.queue.concurrency = 1;
this.queue.autostart = true;
}
onMQTTConnected() {
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/bind/+`);
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/bind/+/+`);
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/unbind/+`);
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/unbind/+/+`);
}
getIDAndPostfix(topic) {
let postfix = null;
if (postfixes.find((p) => topic.endsWith(`/${p}`))) {
postfix = topic.substr(topic.lastIndexOf('/') + 1, topic.length);
// Remove postfix from topic
topic = topic.replace(`/${postfix}`, '');
}
return {ID: topic, postfix};
}
parseTopic(topic) {
@@ -42,8 +53,21 @@ class DeviceBind {
// Remove type from topic
topic = topic.replace(`${type}/`, '');
return {type, ...this.getIDAndPostfix(topic)};
}
return {ID: topic, type};
getEp(ID, postfix) {
let source = null;
if (postfix) {
const device = this.zigbee.getDevice(ID);
const mappedDevice = zigbeeShepherdConverters.findByZigbeeModel(device.modelId);
const epID = mappedDevice.ep(device)[postfix];
source = this.zigbee.getEndpoint(ID, epID);
} else {
source = this.zigbee.getEndpoint(ID);
}
return source;
}
onMQTTMessage(topic, message) {
@@ -55,19 +79,19 @@ class DeviceBind {
// Find source; can only be a device.
const sourceEntity = settings.resolveEntity(topic.ID);
const source = this.zigbee.getEndpoint(sourceEntity.ID);
const source = this.getEp(sourceEntity.ID, topic.postfix);
if (!source) {
logger.error(`Failed to find device '${sourceEntity.ID}'`);
return false;
}
// Find target; can be a device or group.
const targetEntity = settings.resolveEntity(message.toString());
const targetEntityIDPostfix= this.getIDAndPostfix(message.toString());
const targetEntity = settings.resolveEntity(targetEntityIDPostfix.ID);
let target = null;
if (targetEntity.type === 'device') {
target = this.zigbee.getEndpoint(targetEntity.ID);
target = this.getEp(targetEntity.ID, targetEntityIDPostfix.postfix);
if (!target) {
logger.error(`Failed to find target device '${targetEntity.ID}'`);
@@ -94,29 +118,25 @@ class DeviceBind {
// Bind
clusters.forEach((cluster) => {
this.queue.push((queueCallback) => {
logger.debug(`${topic.type}ing cluster '${cluster}' from ${sourceEntity.ID}' to '${targetEntity.ID}'`);
logger.debug(`${topic.type}ing cluster '${cluster}' from ${sourceEntity.ID}' to '${targetEntity.ID}'`);
source[topic.type](cluster, target, (error) => {
if (error) {
logger.error(
`Failed to ${topic.type} cluster '${cluster}' from ${sourceEntity.ID}' to ` +
`'${targetEntity.ID}' (${error})`
);
} else {
logger.info(
`Successfully ${topic.type === 'bind' ? 'bound' : 'unbound'} cluster '${cluster}' from ` +
`${sourceEntity.ID}' to '${targetEntity.ID}'`
);
this.zigbee[topic.type](source, cluster, target, (error) => {
if (error) {
logger.error(
`Failed to ${topic.type} cluster '${cluster}' from ${sourceEntity.ID}' to ` +
`'${targetEntity.ID}' (${error})`
);
} else {
logger.info(
`Successfully ${topic.type === 'bind' ? 'bound' : 'unbound'} cluster '${cluster}' from ` +
`${sourceEntity.ID}' to '${targetEntity.ID}'`
);
this.mqtt.log(
`device_${topic.type}`,
{from: sourceEntity.ID, to: targetEntity.ID, cluster}
);
}
queueCallback();
});
this.mqtt.log(
`device_${topic.type}`,
{from: sourceEntity.ID, to: targetEntity.ID, cluster}
);
}
});
});
+3 -4
View File
@@ -2,12 +2,11 @@
const settings = require('../util/settings');
const zigbeeShepherdConverters = require('zigbee-shepherd-converters');
const logger = require('../util/logger');
const utils = require('../util/utils');
const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/.+/(set|get)$`);
const postfixes = [
'left', 'right', 'center', 'bottom_left', 'bottom_right',
'top_left', 'top_right', 'white', 'rgb', 'system', 'top', 'bottom',
];
const postfixes = utils.getPostfixes();
const maxDepth = 20;
const groupConverters = [
+6
View File
@@ -24,6 +24,11 @@ function toLocalISOString(dDate) {
':' + pad(tzOffset % 60);
}
const postfixes = [
'left', 'right', 'center', 'bottom_left', 'bottom_right',
'top_left', 'top_right', 'white', 'rgb', 'system', 'top', 'bottom',
];
module.exports = {
millisecondsToSeconds: (milliseconds) => milliseconds / 1000,
secondsToMilliseconds: (seconds) => seconds * 1000,
@@ -31,4 +36,5 @@ module.exports = {
isIkeaTradfriDevice: (device) => ikeaTradfriManufacturerID.includes(device.manufId),
isNumeric: (string) => /^\d+$/.test(string),
toLocalISOString: (dDate) => toLocalISOString(dDate),
getPostfixes: () => postfixes,
};
+26 -5
View File
@@ -294,18 +294,39 @@ class Zigbee {
}
}
bind(ep, cluster, target=this.getCoordinator()) {
const log = `for ${ep.device.ieeeAddr} - ${cluster}`;
bind(ep, cluster, target, callback) {
const log = ` ${ep.device.ieeeAddr} - ${cluster}`;
target = !target ? this.getCoordinator() : target;
this.queue.push(ep.device.ieeeAddr, (queueCallback) => {
logger.debug(`Setup binding ${log}`);
logger.debug(`Binding ${log}`);
ep.bind(cluster, target, (error) => {
if (error) {
logger.error(`Failed to setup binding ${log} - (${error})`);
logger.error(`Failed to bind ${log} - (${error})`);
} else {
logger.debug(`Successfully setup binding ${log}`);
logger.debug(`Successfully bound ${log}`);
}
callback(error);
queueCallback(error);
});
});
}
unbind(ep, cluster, target, callback) {
const log = ` ${ep.device.ieeeAddr} - ${cluster}`;
target = !target ? this.getCoordinator() : target;
this.queue.push(ep.device.ieeeAddr, (queueCallback) => {
logger.debug(`Unbinding ${log}`);
ep.unbind(cluster, target, (error) => {
if (error) {
logger.error(`Failed to unbind ${log} - (${error})`);
} else {
logger.debug(`Successfully unbound ${log}`);
}
callback(error);
queueCallback(error);
});
});
+142
View File
@@ -0,0 +1,142 @@
const DeviceBind = require('../lib/extension/deviceBind');
const utils = require('./utils');
const mqtt = {
subscribe: () => {},
log: () => {},
};
const devices = {
bulb: {
getSimpleDesc: () => {
return {inClusterList: [6, 8]};
},
},
remote: {
getSimpleDesc: () => {
return {outClusterList: [5, 6, 8]};
},
},
switch_ep2: {
getSimpleDesc: () => {
return {outClusterList: [5, 6]};
},
},
switch_ep3: {
getSimpleDesc: () => {
return {inClusterList: [5, 6]};
},
},
};
const zigbee = {
bind: jest.fn((ep, cluster, target, callback) => {
callback(false, null);
}),
unbind: jest.fn((ep, cluster, target, callback) => {
callback(false, null);
}),
getEndpoint: (ID, ep) => {
if (ID === 'bulb') {
return devices.bulb;
} else if (ID === 'remote') {
return devices.remote;
} else if (ep == 2 && ID === 'switch_ep2') {
return devices.switch_ep2;
} else if (ep == 3 && ID === 'switch_ep3') {
return devices.switch_ep3;
}
throw new Error(`No mock for ${ID} and ep ${ep}`);
},
getDevice: (ID) => {
if (ID === 'switch_ep2') {
return {modelId: 'lumi.sensor_86sw2.es1'};
} else if (ID == 'switch_ep3') {
return {modelId: 'DNCKAT_S003'};
}
throw new Error(`No mock for ${ID}`);
},
};
describe('DeviceBind', () => {
let deviceBind;
beforeEach(() => {
utils.stubLogger(jest);
deviceBind = new DeviceBind(zigbee, mqtt, null, null);
});
afterEach(() => {
zigbee.bind.mockClear();
zigbee.unbind.mockClear();
jest.restoreAllMocks();
});
describe('Bind devices', () => {
it('Bind', async () => {
deviceBind.onMQTTMessage('zigbee2mqtt/bridge/bind/remote', 'bulb');
expect(zigbee.bind).toHaveBeenCalledTimes(2);
expect(zigbee.bind).toHaveBeenNthCalledWith(1,
devices.remote,
6,
devices.bulb,
expect.any(Function)
);
expect(zigbee.bind).toHaveBeenNthCalledWith(2,
devices.remote,
8,
devices.bulb,
expect.any(Function)
);
});
it('Bind non default ep', async () => {
deviceBind.onMQTTMessage('zigbee2mqtt/bridge/bind/switch_ep2/right', 'bulb');
expect(zigbee.bind).toHaveBeenCalledTimes(1);
expect(zigbee.bind).toHaveBeenNthCalledWith(1,
devices.switch_ep2,
6,
devices.bulb,
expect.any(Function)
);
});
it('Bind non default ep to target with non default ep', async () => {
deviceBind.onMQTTMessage('zigbee2mqtt/bridge/bind/switch_ep2/right', 'switch_ep3/right');
expect(zigbee.bind).toHaveBeenCalledTimes(2);
expect(zigbee.bind).toHaveBeenNthCalledWith(1,
devices.switch_ep2,
5,
devices.switch_ep3,
expect.any(Function)
);
expect(zigbee.bind).toHaveBeenNthCalledWith(2,
devices.switch_ep2,
6,
devices.switch_ep3,
expect.any(Function)
);
});
});
describe('Unbind devices', () => {
it('Unbind', async () => {
deviceBind.onMQTTMessage('zigbee2mqtt/bridge/unbind/remote', 'bulb');
expect(zigbee.unbind).toHaveBeenCalledTimes(2);
expect(zigbee.unbind).toHaveBeenNthCalledWith(1,
devices.remote,
6,
devices.bulb,
expect.any(Function)
);
expect(zigbee.unbind).toHaveBeenNthCalledWith(2,
devices.remote,
8,
devices.bulb,
expect.any(Function)
);
});
});
});