mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-08-22 10:39:58 +00:00
Allow to bind specific endpoint. #176
This commit is contained in:
+52
-32
@@ -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}
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user