Simplify groups api.

This commit is contained in:
Koen Kanters
2018-12-27 18:43:34 +01:00
parent c4804e574e
commit 3bc2ec8c79
6 changed files with 82 additions and 101 deletions
+18 -8
View File
@@ -1,5 +1,7 @@
# Groups
Zigbee2mqtt has support for Zigbee groups. By using Zigbee groups you can control multiple devices simultaneously.
Zigbee2mqtt has support for Zigbee groups. By using Zigbee groups you can control multiple devices simultaneously with one command.
**NOTE:** to use groups, at least firmware version `20181224` is required!
## Configuration
Add the following to your `configuration.yaml`.
@@ -12,16 +14,24 @@ groups:
friendly_name: group_1
```
## Adding a device to a group
Send an MQTT message to `zigbee2mqtt/bridge/groups/[GROUP_FRIENDLY_NAME]/add` with payload `DEVICE_FRIENDLY_NAME`
## Commands
The group of a node can be configured using the following commands:
## Remove a device from a group
Send an MQTT message to `zigbee2mqtt/bridge/groups/[GROUP_FRIENDLY_NAME]/remove` with payload `DEVICE_FRIENDLY_NAME`
- `zigbee2mqtt/bridge/groups/[GROUP_FRIENDLY_NAME]/add` with payload `DEVICE_FRIENDLY_NAME` will add a device to a group.
- `zigbee2mqtt/bridge/groups/[GROUP_FRIENDLY_NAME]/remove` with payload `DEVICE_FRIENDLY_NAME` will remove a device from a group.
- `zigbee2mqtt/bridge/groups/[GROUP_FRIENDLY_NAME]/remove_all` with payload `DEVICE_FRIENDLY_NAME` will remove a device from **all** groups.
## Controlling
To control a group the following topic should be used. The payload is the same as is used for controlling devices.
Controlling a group is similar to controlling a single device. For example to turn on all devices that are part of group send a MQTT message to `zigbee2mqtt/[GROUP_FRIENDLY_NAME]/set` with payload:
```
zigbee2mqtt/group/[GROUP_FRIENDLY_NAME]/set
```json
{
"state": "ON",
}
```
## How do groups work?
By using the above `add` command above, a device will be added to a group. The device itself is responsible for storing to which groups it belongs. Others, e.g. the coordinator, do not have knowledge to which device a groups belongs.
When using the `set` command, e.g. to turn on all devices in a group, a broadcast request is send to **all* devices in the network. The device itself then determines if it belongs to that group and if it should execute the command.
@@ -32,6 +32,9 @@ Format should be: `{"old": "OLD_FRIENDLY_NAME", "new": "NEW_FRIENDLY_NAME"}`.
## zigbee2mqtt/bridge/networkmap
Allows you to retrieve a map of your zigbee network. Possible payloads are `raw`, `graphviz`. Zigbee2mqtt will send the networkmap to `zigbee2mqtt/bridge/networkmap/[graphviz OR raw]`.
## zigbee2mqtt/bridge/groups/[friendly_name]/(add|remove|remove_all)
See [Groups](../groups.md)
## zigbee2mqtt/[DEVICE_ID]
Where `[DEVICE_ID]` is E.G. `0x00158d0001b79111`. Message published to this topic are **always** in a JSON format. Each device produces a different JSON message, **some** examples:
+15 -34
View File
@@ -56,10 +56,10 @@ class DevicePublish {
topic = topic.replace(`${settings.get().mqtt.base_topic}/`, '');
// Parse type from topic
const cmdType = topic.substr(topic.lastIndexOf('/') + 1, topic.length);
const type = topic.substr(topic.lastIndexOf('/') + 1, topic.length);
// Remove type from topic
topic = topic.replace(`/${cmdType}`, '');
topic = topic.replace(`/${type}`, '');
// Check if we have to deal with a postfix.
let postfix = '';
@@ -70,15 +70,9 @@ class DevicePublish {
topic = topic.replace(`/${postfix}`, '');
}
let entityType = 'device';
if (topic.startsWith('group/')) {
topic = topic.replace('group/', '');
entityType = 'group';
}
const ID = topic;
return {cmdType: cmdType, ID: ID, postfix: postfix, entityType: entityType};
return {type: type, ID: ID, postfix: postfix};
}
onMQTTMessage(topic, message) {
@@ -88,31 +82,18 @@ class DevicePublish {
return false;
}
// Map friendlyName (ID) to entityID if possible.
let entityID = null;
if (topic.entityType === 'group') {
const groupID = settings.getGroupIDByFriendlyName(topic.ID);
if (groupID) {
entityID = Number(groupID);
} else if (utils.isNumeric(topic.ID)) {
entityID = Number(topic.ID);
} else {
logger.error(`Cannot find group '${topic.ID}'`);
return;
}
} else if (topic.entityType === 'device') {
entityID = settings.getIeeeAddrByFriendlyName(topic.ID) || topic.ID;
}
// Resolve the entity
const entity = utils.resolveEntity(topic.ID);
// Get entity details
let endpoint = null;
let converters = null;
let device = null;
if (topic.entityType === 'device') {
device = this.zigbee.getDevice(entityID);
if (entity.type === 'device') {
device = this.zigbee.getDevice(entity.ID);
if (!device) {
logger.error(`Failed to find device with ieeAddr: '${entityID}'`);
logger.error(`Failed to find device with ieeAddr: '${entity.ID}'`);
return;
}
@@ -131,7 +112,7 @@ class DevicePublish {
}
converters = model.toZigbee;
} else if (topic.entityType === 'group') {
} else if (entity.type === 'group') {
converters = groupConverters;
}
@@ -159,7 +140,7 @@ class DevicePublish {
}
// Converter didn't return a result, skip
const converted = converter.convert(key, json[key], json, topic.cmdType);
const converted = converter.convert(key, json[key], json, topic.type);
if (!converted) {
return;
}
@@ -167,8 +148,8 @@ class DevicePublish {
// Add job to queue
this.queue.push((queueCallback) => {
this.zigbee.publish(
entityID,
topic.entityType,
entity.ID,
entity.type,
converted.cid,
converted.cmd,
converted.cmdType,
@@ -177,7 +158,7 @@ class DevicePublish {
endpoint,
(error, rsp) => {
// Devices do not report when they go off, this ensures state (on/off) is always in sync.
if (topic.entityType === 'device' && topic.cmdType === 'set' &&
if (entity.type === 'device' && topic.type === 'set' &&
!error && (key.startsWith('state') || key === 'brightness')) {
const msg = {};
const _key = topic.postfix ? `state_${topic.postfix}` : 'state';
@@ -192,14 +173,14 @@ class DevicePublish {
// When there is a transition in the message the state of the device gets out of sync.
// Therefore; at the end of the transition, read the new state from the device.
if (topic.cmdType === 'set' && converted.zclData.transtime && topic.entityType === 'device') {
if (topic.type === 'set' && converted.zclData.transtime && entity.type === 'device') {
const time = converted.zclData.transtime * 100;
const getConverted = converter.convert(key, json[key], json, 'get');
setTimeout(() => {
// Add job to queue
this.queue.push((queueCallback) => {
this.zigbee.publish(
entityID, topic.entityType, getConverted.cid, getConverted.cmd, getConverted.cmdType,
entity.ID, entity.type, getConverted.cid, getConverted.cmd, getConverted.cmdType,
getConverted.zclData, getConverted.cfg, endpoint, () => queueCallback()
);
});
+12 -5
View File
@@ -1,7 +1,7 @@
const settings = require('../util/settings');
const logger = require('../util/logger');
const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/groups/.+/(remove|add)$`);
const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/group/.+/(remove|add|remove_all)$`);
class Groups {
constructor(zigbee, mqtt, state, publishDeviceState) {
@@ -12,8 +12,9 @@ class Groups {
}
onMQTTConnected() {
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/groups/+/remove`);
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/groups/+/add`);
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/group/+/remove`);
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/group/+/add`);
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/group/+/remove_all`);
}
parseTopic(topic) {
@@ -22,7 +23,7 @@ class Groups {
}
// Remove base from topic
topic = topic.replace(`${settings.get().mqtt.base_topic}/bridge/groups/`, '');
topic = topic.replace(`${settings.get().mqtt.base_topic}/bridge/group/`, '');
// Parse type from topic
const type = topic.substr(topic.lastIndexOf('/') + 1, topic.length);
@@ -57,10 +58,16 @@ class Groups {
// Send command to the device.
let payload = null;
let cmd = null;
if (topic.type === 'add') {
payload = {groupid: groupID, groupname: ''};
cmd = 'add';
} else if (topic.type === 'remove') {
payload = {groupid: groupID};
cmd = 'remove';
} else if (topic.type === 'remove_all') {
payload = {};
cmd = 'removeAll';
}
const callback = (error, rsp) => {
@@ -72,7 +79,7 @@ class Groups {
};
this.zigbee.publish(
ieeeAddr, 'device', 'genGroups', topic.type, 'functional',
ieeeAddr, 'device', 'genGroups', cmd, 'functional',
payload, null, null, callback,
);
+23
View File
@@ -1,9 +1,32 @@
const settings = require('./settings');
// Xiaomi uses 4151 and 4447 (lumi.plug) as manufacturer ID.
const xiaomiManufacturerID = [4151, 4447];
// An entity can be either a group or a device.
function resolveEntity(ID) {
let type = null;
if (settings.getIeeeAddrByFriendlyName(ID)) {
// Check if the ID is a friendly_name of a device.
ID = settings.getIeeeAddrByFriendlyName(ID);
type = 'device';
} else if (settings.getGroupIDByFriendlyName(ID)) {
// Check if the ID is a friendly_name of a group.
ID = Number(settings.getGroupIDByFriendlyName(ID));
type = 'group';
} else {
// By default it is a device with ID as ID.
type = 'device';
}
return {ID: ID, type: type};
}
module.exports = {
millisecondsToSeconds: (milliseconds) => milliseconds / 1000,
secondsToMilliseconds: (seconds) => seconds * 1000,
isXiaomiDevice: (device) => xiaomiManufacturerID.includes(device.manufId),
isNumeric: (string) => /^\d+$/.test(string),
resolveEntity: (ID) => resolveEntity(ID),
};
+11 -54
View File
@@ -245,20 +245,6 @@ describe('DevicePublish', () => {
chai.assert.deepEqual(zigbee.publish.getCall(0).args[6], cfg.default);
chai.assert.deepEqual(zigbee.publish.getCall(0).args[7], null);
});
it('Should publish messages to groups by ID', () => {
zigbee.publish.resetHistory();
devicePublish.onMQTTMessage('zigbee2mqtt/group/2/set', JSON.stringify({state: 'ON'}));
chai.assert.isTrue(zigbee.publish.calledOnce);
chai.assert.strictEqual(zigbee.publish.getCall(0).args[0], 2);
chai.assert.strictEqual(zigbee.publish.getCall(0).args[1], 'group');
chai.assert.strictEqual(zigbee.publish.getCall(0).args[2], 'genOnOff');
chai.assert.strictEqual(zigbee.publish.getCall(0).args[3], 'on');
chai.assert.strictEqual(zigbee.publish.getCall(0).args[4], 'functional');
chai.assert.deepEqual(zigbee.publish.getCall(0).args[5], {});
chai.assert.deepEqual(zigbee.publish.getCall(0).args[6], cfg.default);
chai.assert.deepEqual(zigbee.publish.getCall(0).args[7], null);
});
});
describe('Parse topic', () => {
@@ -295,8 +281,7 @@ describe('DevicePublish', () => {
it('Should parse set topic', () => {
const topic = 'zigbee2mqtt/my_device_id/set';
const parsed = devicePublish.parseTopic(topic);
chai.assert.strictEqual(parsed.cmdType, 'set');
chai.assert.strictEqual(parsed.entityType, 'device');
chai.assert.strictEqual(parsed.type, 'set');
chai.assert.strictEqual(parsed.ID, 'my_device_id');
chai.assert.strictEqual(parsed.postfix, '');
});
@@ -304,8 +289,7 @@ describe('DevicePublish', () => {
it('Should parse get topic', () => {
const topic = 'zigbee2mqtt/my_device_id2/get';
const parsed = devicePublish.parseTopic(topic);
chai.assert.strictEqual(parsed.cmdType, 'get');
chai.assert.strictEqual(parsed.entityType, 'device');
chai.assert.strictEqual(parsed.type, 'get');
chai.assert.strictEqual(parsed.ID, 'my_device_id2');
chai.assert.strictEqual(parsed.postfix, '');
});
@@ -321,8 +305,7 @@ describe('DevicePublish', () => {
const topic = 'zigbee2mqtt/at/my/home/my_device_id2/get';
const parsed = devicePublish.parseTopic(topic);
chai.assert.strictEqual(parsed.cmdType, 'get');
chai.assert.strictEqual(parsed.entityType, 'device');
chai.assert.strictEqual(parsed.type, 'get');
chai.assert.strictEqual(parsed.ID, 'my_device_id2');
chai.assert.strictEqual(parsed.postfix, '');
});
@@ -330,8 +313,7 @@ describe('DevicePublish', () => {
it('Should parse topic with when deviceID has multiple slashes', () => {
const topic = 'zigbee2mqtt/floor0/basement/my_device_id2/set';
const parsed = devicePublish.parseTopic(topic);
chai.assert.strictEqual(parsed.cmdType, 'set');
chai.assert.strictEqual(parsed.entityType, 'device');
chai.assert.strictEqual(parsed.type, 'set');
chai.assert.strictEqual(parsed.ID, 'floor0/basement/my_device_id2');
chai.assert.strictEqual(parsed.postfix, '');
});
@@ -347,8 +329,7 @@ describe('DevicePublish', () => {
const topic = 'zigbee2mqtt/at/my/basement/floor0/basement/my_device_id2/set';
const parsed = devicePublish.parseTopic(topic);
chai.assert.strictEqual(parsed.cmdType, 'set');
chai.assert.strictEqual(parsed.entityType, 'device');
chai.assert.strictEqual(parsed.type, 'set');
chai.assert.strictEqual(parsed.ID, 'floor0/basement/my_device_id2');
chai.assert.strictEqual(parsed.postfix, '');
});
@@ -356,8 +337,7 @@ describe('DevicePublish', () => {
it('Should parse set with ieeAddr topic', () => {
const topic = 'zigbee2mqtt/0x12345689/set';
const parsed = devicePublish.parseTopic(topic);
chai.assert.strictEqual(parsed.cmdType, 'set');
chai.assert.strictEqual(parsed.entityType, 'device');
chai.assert.strictEqual(parsed.type, 'set');
chai.assert.strictEqual(parsed.ID, '0x12345689');
chai.assert.strictEqual(parsed.postfix, '');
});
@@ -365,8 +345,7 @@ describe('DevicePublish', () => {
it('Should parse set with postfix topic', () => {
const topic = 'zigbee2mqtt/0x12345689/left/set';
const parsed = devicePublish.parseTopic(topic);
chai.assert.strictEqual(parsed.cmdType, 'set');
chai.assert.strictEqual(parsed.entityType, 'device');
chai.assert.strictEqual(parsed.type, 'set');
chai.assert.strictEqual(parsed.ID, '0x12345689');
chai.assert.strictEqual(parsed.postfix, 'left');
});
@@ -374,8 +353,7 @@ describe('DevicePublish', () => {
it('Should parse set with postfix topic', () => {
const topic = 'zigbee2mqtt/0x12345689/right/set';
const parsed = devicePublish.parseTopic(topic);
chai.assert.strictEqual(parsed.cmdType, 'set');
chai.assert.strictEqual(parsed.entityType, 'device');
chai.assert.strictEqual(parsed.type, 'set');
chai.assert.strictEqual(parsed.ID, '0x12345689');
chai.assert.strictEqual(parsed.postfix, 'right');
});
@@ -383,8 +361,7 @@ describe('DevicePublish', () => {
it('Should parse set with postfix topic', () => {
const topic = 'zigbee2mqtt/0x12345689/bottom_left/set';
const parsed = devicePublish.parseTopic(topic);
chai.assert.strictEqual(parsed.cmdType, 'set');
chai.assert.strictEqual(parsed.entityType, 'device');
chai.assert.strictEqual(parsed.type, 'set');
chai.assert.strictEqual(parsed.ID, '0x12345689');
chai.assert.strictEqual(parsed.postfix, 'bottom_left');
});
@@ -392,8 +369,7 @@ describe('DevicePublish', () => {
it('Shouldnt parse set with invalid postfix topic', () => {
const topic = 'zigbee2mqtt/0x12345689/invalid/set';
const parsed = devicePublish.parseTopic(topic);
chai.assert.strictEqual(parsed.cmdType, 'set');
chai.assert.strictEqual(parsed.entityType, 'device');
chai.assert.strictEqual(parsed.type, 'set');
chai.assert.strictEqual(parsed.ID, '0x12345689/invalid');
chai.assert.strictEqual(parsed.postfix, '');
});
@@ -409,28 +385,9 @@ describe('DevicePublish', () => {
const topic = 'zigbee2mqtt/at/my/home/my/device/in/basement/sensor/bottom_left/get';
const parsed = devicePublish.parseTopic(topic);
chai.assert.strictEqual(parsed.cmdType, 'get');
chai.assert.strictEqual(parsed.entityType, 'device');
chai.assert.strictEqual(parsed.type, 'get');
chai.assert.strictEqual(parsed.ID, 'my/device/in/basement/sensor');
chai.assert.strictEqual(parsed.postfix, 'bottom_left');
});
it('Should parse group topics', () => {
const topic = 'zigbee2mqtt/group/group_1/set';
const parsed = devicePublish.parseTopic(topic);
chai.assert.strictEqual(parsed.cmdType, 'set');
chai.assert.strictEqual(parsed.entityType, 'group');
chai.assert.strictEqual(parsed.ID, 'group_1');
chai.assert.strictEqual(parsed.postfix, '');
});
it('Should parse group topics with mutiple slashes', () => {
const topic = 'zigbee2mqtt/group/master/child/set';
const parsed = devicePublish.parseTopic(topic);
chai.assert.strictEqual(parsed.cmdType, 'set');
chai.assert.strictEqual(parsed.entityType, 'group');
chai.assert.strictEqual(parsed.ID, 'master/child');
chai.assert.strictEqual(parsed.postfix, '');
});
});
});