Skip to content

Commit

Permalink
Simplify groups api.
Browse files Browse the repository at this point in the history
  • Loading branch information
Koenkk committed Dec 27, 2018
1 parent c4804e5 commit 3bc2ec8
Show file tree
Hide file tree
Showing 6 changed files with 82 additions and 101 deletions.
26 changes: 18 additions & 8 deletions docs/information/groups.md
Original file line number Diff line number Diff line change
@@ -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`.
Expand All @@ -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:

```json
{
"state": "ON",
}
```
zigbee2mqtt/group/[GROUP_FRIENDLY_NAME]/set
```

## 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.

3 changes: 3 additions & 0 deletions docs/information/mqtt_topics_and_message_structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
49 changes: 15 additions & 34 deletions lib/extension/devicePublish.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '';
Expand All @@ -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) {
Expand All @@ -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;
}

Expand All @@ -131,7 +112,7 @@ class DevicePublish {
}

converters = model.toZigbee;
} else if (topic.entityType === 'group') {
} else if (entity.type === 'group') {
converters = groupConverters;
}

Expand Down Expand Up @@ -159,16 +140,16 @@ 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;
}

// Add job to queue
this.queue.push((queueCallback) => {
this.zigbee.publish(
entityID,
topic.entityType,
entity.ID,
entity.type,
converted.cid,
converted.cmd,
converted.cmdType,
Expand All @@ -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';
Expand All @@ -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()
);
});
Expand Down
17 changes: 12 additions & 5 deletions lib/extension/groups.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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) {
Expand All @@ -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);
Expand Down Expand Up @@ -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) => {
Expand All @@ -72,7 +79,7 @@ class Groups {
};

this.zigbee.publish(
ieeeAddr, 'device', 'genGroups', topic.type, 'functional',
ieeeAddr, 'device', 'genGroups', cmd, 'functional',
payload, null, null, callback,
);

Expand Down
23 changes: 23 additions & 0 deletions lib/util/utils.js
Original file line number Diff line number Diff line change
@@ -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),
};
Loading

0 comments on commit 3bc2ec8

Please sign in to comment.