2018-12-21 16:07:53 -07:00
|
|
|
const settings = require('../util/settings');
|
|
|
|
const logger = require('../util/logger');
|
2020-04-11 09:10:56 -07:00
|
|
|
const Extension = require('./extension');
|
2020-04-04 15:05:05 -07:00
|
|
|
const utils = require('../util/utils');
|
|
|
|
const postfixes = utils.getEndpointNames();
|
2018-12-21 16:07:53 -07:00
|
|
|
|
2020-04-12 11:32:14 -07:00
|
|
|
const legacyTopicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/group/(.+)/(remove|add|remove_all)$`);
|
|
|
|
const legacyTopicRegexRemoveAll = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/group/remove_all$`);
|
2018-12-21 16:07:53 -07:00
|
|
|
|
2020-04-11 09:10:56 -07:00
|
|
|
class Groups extends Extension {
|
2020-01-09 13:47:19 -07:00
|
|
|
constructor(zigbee, mqtt, state, publishEntityState, eventBus) {
|
|
|
|
super(zigbee, mqtt, state, publishEntityState, eventBus);
|
2019-09-17 09:32:16 -07:00
|
|
|
this.onStateChange = this.onStateChange.bind(this);
|
2020-04-12 11:32:14 -07:00
|
|
|
this.legacyApi = settings.get().advanced.legacy_api;
|
2018-12-21 16:07:53 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
onMQTTConnected() {
|
2020-04-12 11:32:14 -07:00
|
|
|
/* istanbul ignore else */
|
|
|
|
if (this.legacyApi) {
|
|
|
|
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/group/remove_all`);
|
2019-10-25 09:40:37 -07:00
|
|
|
|
2020-04-12 11:32:14 -07:00
|
|
|
for (let step = 1; step < 20; step++) {
|
|
|
|
const topic = `${settings.get().mqtt.base_topic}/bridge/group/${'+/'.repeat(step)}`;
|
|
|
|
this.mqtt.subscribe(`${topic}remove`);
|
|
|
|
this.mqtt.subscribe(`${topic}add`);
|
|
|
|
this.mqtt.subscribe(`${topic}remove_all`);
|
|
|
|
}
|
2020-03-11 13:24:01 -07:00
|
|
|
}
|
2018-12-21 16:07:53 -07:00
|
|
|
}
|
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
async onZigbeeStarted() {
|
|
|
|
await this.syncGroupsWithSettings();
|
2020-04-05 06:48:23 -07:00
|
|
|
this.eventBus.on('stateChange', this.onStateChange);
|
2019-04-29 11:38:40 -07:00
|
|
|
}
|
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
async syncGroupsWithSettings() {
|
|
|
|
const settingsGroups = settings.getGroups();
|
2019-09-23 13:21:27 -07:00
|
|
|
const zigbeeGroups = this.zigbee.getGroups();
|
2019-09-09 10:48:09 -07:00
|
|
|
for (const settingGroup of settingsGroups) {
|
|
|
|
const groupID = settingGroup.ID;
|
2019-09-23 13:21:27 -07:00
|
|
|
const zigbeeGroup = zigbeeGroups.find((g) => g.groupID === groupID) || this.zigbee.createGroup(groupID);
|
|
|
|
const settingsEntity = settingGroup.devices.map((d) => {
|
|
|
|
const entity = this.zigbee.resolveEntity(d);
|
2019-09-09 10:48:09 -07:00
|
|
|
if (!entity) logger.error(`Cannot find '${d}' of group '${settingGroup.friendlyName}'`);
|
|
|
|
return entity;
|
2019-09-23 13:21:27 -07:00
|
|
|
}).filter((e) => e != null);
|
2019-09-09 10:48:09 -07:00
|
|
|
|
|
|
|
// In settings but not in zigbee
|
|
|
|
for (const entity of settingsEntity) {
|
|
|
|
if (!zigbeeGroup.hasMember(entity.endpoint)) {
|
|
|
|
logger.info(`Adding '${entity.name}' to group '${settingGroup.friendlyName}'`);
|
|
|
|
await entity.endpoint.addToGroup(zigbeeGroup);
|
|
|
|
}
|
2019-05-29 11:11:40 -07:00
|
|
|
}
|
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
// In zigbee but not in settings
|
2019-10-01 11:22:47 -07:00
|
|
|
for (const endpoint of zigbeeGroup.members) {
|
2019-09-09 10:48:09 -07:00
|
|
|
if (!settingsEntity.find((e) => e.endpoint === endpoint)) {
|
2019-10-01 11:22:47 -07:00
|
|
|
const deviceSettings = settings.getDevice(endpoint.getDevice().ieeeAddr);
|
2019-09-09 10:48:09 -07:00
|
|
|
logger.info(`Removing '${deviceSettings.friendlyName}' from group '${settingGroup.friendlyName}'`);
|
|
|
|
await endpoint.removeFromGroup(zigbeeGroup);
|
|
|
|
}
|
|
|
|
}
|
2019-05-29 11:11:40 -07:00
|
|
|
}
|
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
// eslint-disable-next-line
|
|
|
|
for (const zigbeeGroup of zigbeeGroups) {
|
|
|
|
if (!settingsGroups.find((g) => g.ID === zigbeeGroup.groupID)) {
|
2019-10-01 11:22:47 -07:00
|
|
|
for (const endpoint of zigbeeGroup.members) {
|
|
|
|
const deviceSettings = settings.getDevice(endpoint.getDevice().ieeeAddr);
|
2019-09-09 10:48:09 -07:00
|
|
|
logger.info(`Removing '${deviceSettings.friendlyName}' from group ${zigbeeGroup.groupID}`);
|
|
|
|
await endpoint.removeFromGroup(zigbeeGroup);
|
|
|
|
}
|
2019-04-29 11:38:40 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-09-17 09:33:27 -07:00
|
|
|
async onStateChange(data) {
|
2019-09-29 05:35:05 -07:00
|
|
|
const reason = 'group_optimistic';
|
|
|
|
if (data.reason === reason) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-09-17 09:33:27 -07:00
|
|
|
const properties = ['state', 'brightness', 'color_temp', 'color'];
|
|
|
|
const payload = {};
|
|
|
|
|
|
|
|
properties.forEach((prop) => {
|
2019-10-15 07:42:28 -07:00
|
|
|
if (data.to.hasOwnProperty(prop)) {
|
2019-09-17 09:33:27 -07:00
|
|
|
payload[prop] = data.to[prop];
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if (Object.keys(payload).length) {
|
2020-04-12 11:32:14 -07:00
|
|
|
const resolvedEntity = this.zigbee.resolveEntity(data.ID);
|
2019-09-30 12:18:07 -07:00
|
|
|
const zigbeeGroups = this.zigbee.getGroups().filter((zigbeeGroup) => {
|
|
|
|
const settingsGroup = settings.getGroup(zigbeeGroup.groupID);
|
|
|
|
return settingsGroup && settingsGroup.optimistic;
|
|
|
|
});
|
2019-09-17 09:33:27 -07:00
|
|
|
|
2020-04-12 11:32:14 -07:00
|
|
|
if (resolvedEntity.type === 'device') {
|
2019-09-29 05:35:05 -07:00
|
|
|
for (const zigbeeGroup of zigbeeGroups) {
|
2020-04-12 11:32:14 -07:00
|
|
|
if (zigbeeGroup.hasMember(resolvedEntity.endpoint)) {
|
|
|
|
if (!payload || payload.state !== 'OFF' || this.areAllMembersOff(zigbeeGroup)) {
|
2019-10-03 08:41:36 -07:00
|
|
|
await this.publishEntityState(zigbeeGroup.groupID, payload, reason);
|
|
|
|
}
|
2019-09-25 03:51:17 -07:00
|
|
|
}
|
2019-09-17 09:33:27 -07:00
|
|
|
}
|
2019-09-29 05:35:05 -07:00
|
|
|
} else {
|
2019-09-30 12:18:07 -07:00
|
|
|
const groupIDsToPublish = new Set();
|
2020-04-12 11:32:14 -07:00
|
|
|
for (const member of resolvedEntity.group.members) {
|
2019-10-01 11:22:47 -07:00
|
|
|
await this.publishEntityState(member.getDevice().ieeeAddr, payload, reason);
|
2019-09-30 12:18:07 -07:00
|
|
|
for (const zigbeeGroup of zigbeeGroups) {
|
|
|
|
if (zigbeeGroup.hasMember(member)) {
|
2020-04-12 11:32:14 -07:00
|
|
|
if (!payload || payload.state !== 'OFF' || this.areAllMembersOff(zigbeeGroup)) {
|
2019-10-14 09:04:04 -07:00
|
|
|
groupIDsToPublish.add(zigbeeGroup.groupID);
|
|
|
|
}
|
2019-09-30 12:18:07 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-04-12 11:32:14 -07:00
|
|
|
groupIDsToPublish.delete(resolvedEntity.group.groupID);
|
2019-09-30 12:18:07 -07:00
|
|
|
for (const groupID of groupIDsToPublish) {
|
|
|
|
await this.publishEntityState(groupID, payload, reason);
|
2019-09-29 05:35:05 -07:00
|
|
|
}
|
2019-09-17 09:33:27 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-04-12 11:32:14 -07:00
|
|
|
areAllMembersOff(zigbeeGroup) {
|
2019-10-14 09:04:04 -07:00
|
|
|
for (const member of zigbeeGroup.members) {
|
|
|
|
const device = member.getDevice();
|
|
|
|
if (this.state.exists(device.ieeeAddr)) {
|
|
|
|
const state = this.state.get(device.ieeeAddr);
|
|
|
|
if (state && state.state === 'ON') {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2020-04-12 11:32:14 -07:00
|
|
|
parseMQTTMessage(topic, message) {
|
|
|
|
let type = null;
|
|
|
|
let resolvedEntityGroup = null;
|
|
|
|
let resolvedEntityDevice = null;
|
|
|
|
let hasEndpointName = null;
|
|
|
|
|
|
|
|
/* istanbul ignore else */
|
|
|
|
if (this.legacyApi) {
|
|
|
|
const topicMatch = topic.match(legacyTopicRegex);
|
|
|
|
if (topicMatch) {
|
|
|
|
resolvedEntityGroup = this.zigbee.resolveEntity(topicMatch[1]);
|
|
|
|
type = topicMatch[2];
|
|
|
|
|
|
|
|
if (!resolvedEntityGroup || resolvedEntityGroup.type !== 'group') {
|
|
|
|
logger.error(`Group '${topicMatch[1]}' does not exist`);
|
|
|
|
|
|
|
|
/* istanbul ignore else */
|
|
|
|
if (settings.get().advanced.legacy_api) {
|
|
|
|
const payload = {friendly_name: message, group: topicMatch[1], error: 'group doesn\'t exists'};
|
|
|
|
this.mqtt.publish(
|
|
|
|
'bridge/log',
|
|
|
|
JSON.stringify({type: `device_group_${type}_failed`, message: payload}),
|
|
|
|
);
|
|
|
|
}
|
2019-04-29 11:38:40 -07:00
|
|
|
|
2020-04-12 11:32:14 -07:00
|
|
|
return {};
|
|
|
|
}
|
|
|
|
} else if (topic.match(legacyTopicRegexRemoveAll)) {
|
|
|
|
type = 'remove_all';
|
|
|
|
} else {
|
|
|
|
return {};
|
|
|
|
}
|
|
|
|
|
|
|
|
resolvedEntityDevice = this.zigbee.resolveEntity(message);
|
|
|
|
if (!resolvedEntityDevice || !resolvedEntityDevice.type === 'device') {
|
|
|
|
logger.error(`Device '${message}' does not exist`);
|
2020-04-05 09:36:08 -07:00
|
|
|
|
|
|
|
/* istanbul ignore else */
|
|
|
|
if (settings.get().advanced.legacy_api) {
|
2020-04-12 11:32:14 -07:00
|
|
|
const payload = {friendly_name: message, group: topicMatch[1], error: 'entity doesn\'t exists'};
|
2020-04-05 09:36:08 -07:00
|
|
|
this.mqtt.publish(
|
|
|
|
'bridge/log',
|
|
|
|
JSON.stringify({type: `device_group_${type}_failed`, message: payload}),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-04-12 11:32:14 -07:00
|
|
|
return {};
|
2019-09-09 10:48:09 -07:00
|
|
|
}
|
2018-12-21 16:07:53 -07:00
|
|
|
|
2020-04-12 11:32:14 -07:00
|
|
|
hasEndpointName = postfixes.find((p) => message.endsWith(`/${p}`));
|
|
|
|
}
|
2020-04-05 09:36:08 -07:00
|
|
|
|
2020-04-12 11:32:14 -07:00
|
|
|
return {resolvedEntityGroup, resolvedEntityDevice, type, hasEndpointName};
|
|
|
|
}
|
2020-04-05 09:36:08 -07:00
|
|
|
|
2020-04-12 11:32:14 -07:00
|
|
|
async onMQTTMessage(topic, message) {
|
|
|
|
const {resolvedEntityGroup, resolvedEntityDevice, type, hasEndpointName} =
|
|
|
|
this.parseMQTTMessage(topic, message);
|
|
|
|
if (!type) return;
|
2019-04-29 11:38:40 -07:00
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
const keys = [
|
2020-04-12 11:32:14 -07:00
|
|
|
`${resolvedEntityDevice.device.ieeeAddr}/${resolvedEntityDevice.endpoint.ID}`,
|
|
|
|
`${resolvedEntityDevice.name}/${resolvedEntityDevice.endpoint.ID}`,
|
2019-09-09 10:48:09 -07:00
|
|
|
];
|
2019-04-29 11:38:40 -07:00
|
|
|
|
2020-04-12 11:32:14 -07:00
|
|
|
const definition = resolvedEntityDevice.definition;
|
|
|
|
const endpoints = definition && definition.endpoint ? definition.endpoint(resolvedEntityDevice.device) : null;
|
|
|
|
const endpointName = endpoints ?
|
|
|
|
Object.entries(endpoints).find((e) => e[1] === resolvedEntityDevice.endpoint.ID)[0] : null;
|
2020-04-04 15:05:05 -07:00
|
|
|
|
|
|
|
if (endpointName) {
|
2020-04-12 11:32:14 -07:00
|
|
|
keys.push(`${resolvedEntityDevice.device.ieeeAddr}/${endpointName}`);
|
|
|
|
keys.push(`${resolvedEntityDevice.name}/${endpointName}`);
|
2019-09-09 10:48:09 -07:00
|
|
|
}
|
2018-12-21 16:07:53 -07:00
|
|
|
|
2020-04-04 15:05:05 -07:00
|
|
|
if (!hasEndpointName) {
|
2020-04-12 11:32:14 -07:00
|
|
|
keys.push(resolvedEntityDevice.name);
|
|
|
|
keys.push(resolvedEntityDevice.device.ieeeAddr);
|
2018-12-21 16:07:53 -07:00
|
|
|
}
|
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
if (type === 'add') {
|
2020-04-12 11:32:14 -07:00
|
|
|
logger.info(`Adding '${resolvedEntityDevice.name}' to '${resolvedEntityGroup.name}'`);
|
|
|
|
await resolvedEntityDevice.endpoint.addToGroup(resolvedEntityGroup.group);
|
|
|
|
settings.addDeviceToGroup(resolvedEntityGroup.settings.ID, keys);
|
2020-04-05 09:36:08 -07:00
|
|
|
|
|
|
|
/* istanbul ignore else */
|
|
|
|
if (settings.get().advanced.legacy_api) {
|
2020-04-12 11:32:14 -07:00
|
|
|
const payload = {friendly_name: resolvedEntityDevice.name, group: resolvedEntityGroup.name};
|
2020-04-05 09:36:08 -07:00
|
|
|
this.mqtt.publish(
|
|
|
|
'bridge/log',
|
|
|
|
JSON.stringify({type: `device_group_add`, message: payload}),
|
|
|
|
);
|
|
|
|
}
|
2019-09-09 10:48:09 -07:00
|
|
|
} else if (type === 'remove') {
|
2020-04-12 11:32:14 -07:00
|
|
|
logger.info(`Removing '${resolvedEntityDevice.name}' from '${resolvedEntityGroup.name}'`);
|
|
|
|
await resolvedEntityDevice.endpoint.removeFromGroup(resolvedEntityGroup.group);
|
|
|
|
settings.removeDeviceFromGroup(resolvedEntityGroup.settings.ID, keys);
|
2020-04-05 09:36:08 -07:00
|
|
|
|
|
|
|
/* istanbul ignore else */
|
|
|
|
if (settings.get().advanced.legacy_api) {
|
2020-04-12 11:32:14 -07:00
|
|
|
const payload = {friendly_name: resolvedEntityDevice.name, group: resolvedEntityGroup.name};
|
2020-04-05 09:36:08 -07:00
|
|
|
this.mqtt.publish(
|
|
|
|
'bridge/log',
|
|
|
|
JSON.stringify({type: `device_group_remove`, message: payload}),
|
|
|
|
);
|
|
|
|
}
|
2019-09-09 10:48:09 -07:00
|
|
|
} else { // remove_all
|
2020-04-12 11:32:14 -07:00
|
|
|
logger.info(`Removing '${resolvedEntityDevice.name}' from all groups`);
|
|
|
|
await resolvedEntityDevice.endpoint.removeFromAllGroups();
|
2019-09-09 10:48:09 -07:00
|
|
|
for (const settingsGroup of settings.getGroups()) {
|
|
|
|
settings.removeDeviceFromGroup(settingsGroup.ID, keys);
|
2020-04-05 09:36:08 -07:00
|
|
|
|
|
|
|
/* istanbul ignore else */
|
|
|
|
if (settings.get().advanced.legacy_api) {
|
2020-04-12 11:32:14 -07:00
|
|
|
const payload = {friendly_name: resolvedEntityDevice.name};
|
2020-04-05 09:36:08 -07:00
|
|
|
this.mqtt.publish(
|
|
|
|
'bridge/log',
|
|
|
|
JSON.stringify({type: `device_group_remove_all`, message: payload}),
|
|
|
|
);
|
|
|
|
}
|
2019-06-17 12:17:29 -07:00
|
|
|
}
|
2018-12-21 16:07:53 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = Groups;
|