mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-08-27 21:20:03 +00:00
Simplify groups api.
This commit is contained in:
@@ -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:
|
||||
|
||||
|
||||
@@ -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
@@ -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,
|
||||
);
|
||||
|
||||
|
||||
@@ -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
@@ -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, '');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user