2018-12-21 16:07:53 -07:00
|
|
|
const settings = require('../util/settings');
|
|
|
|
const logger = require('../util/logger');
|
2019-09-17 09:32:16 -07:00
|
|
|
const BaseExtension = require('./baseExtension');
|
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
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/group/(.+)/(remove|add|remove_all)$`);
|
|
|
|
const topicRegexRemoveAll = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/group/remove_all$`);
|
2018-12-21 16:07:53 -07:00
|
|
|
|
2019-09-17 09:32:16 -07:00
|
|
|
class Groups extends BaseExtension {
|
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);
|
2018-12-21 16:07:53 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
onMQTTConnected() {
|
2019-06-17 12:17:29 -07:00
|
|
|
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/group/remove_all`);
|
2019-10-25 09:40:37 -07:00
|
|
|
|
2020-03-11 13:24:01 -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`); // DEPRECATED
|
|
|
|
}
|
2018-12-21 16:07:53 -07:00
|
|
|
}
|
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
async onZigbeeStarted() {
|
|
|
|
await this.syncGroupsWithSettings();
|
2019-09-17 09:33:27 -07:00
|
|
|
this.state.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) {
|
2019-09-23 13:21:27 -07:00
|
|
|
const entity = 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
|
|
|
|
2019-09-29 05:35:05 -07:00
|
|
|
if (entity.type === 'device') {
|
|
|
|
for (const zigbeeGroup of zigbeeGroups) {
|
2019-09-30 12:18:07 -07:00
|
|
|
if (zigbeeGroup.hasMember(entity.endpoint)) {
|
2019-10-14 09:04:04 -07:00
|
|
|
if (!payload || payload.state !== 'OFF' || this.allMembersOff(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();
|
2019-10-01 11:22:47 -07:00
|
|
|
for (const member of entity.group.members) {
|
|
|
|
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)) {
|
2019-10-14 09:04:04 -07:00
|
|
|
if (!payload || payload.state !== 'OFF' || this.allMembersOff(zigbeeGroup)) {
|
|
|
|
groupIDsToPublish.add(zigbeeGroup.groupID);
|
|
|
|
}
|
2019-09-30 12:18:07 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
groupIDsToPublish.delete(entity.group.groupID);
|
|
|
|
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
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-14 09:04:04 -07:00
|
|
|
allMembersOff(zigbeeGroup) {
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
async onMQTTMessage(topic, message) {
|
|
|
|
let type;
|
|
|
|
let group;
|
|
|
|
const topicMatch = topic.match(topicRegex);
|
|
|
|
if (topicMatch) {
|
2019-09-23 13:21:27 -07:00
|
|
|
group = this.zigbee.resolveEntity(topicMatch[1]);
|
2019-09-09 10:48:09 -07:00
|
|
|
type = topicMatch[2];
|
2019-04-29 11:38:40 -07:00
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
if (!group || group.type !== 'group') {
|
|
|
|
logger.error(`Group '${topicMatch[1]}' does not exist`);
|
2020-01-07 13:06:48 -07:00
|
|
|
this.mqtt.log(
|
|
|
|
`device_group_${type}_failed`,
|
|
|
|
{friendly_name: message, group: topicMatch[1], error: 'group doesn\'t exists'});
|
2019-09-09 10:48:09 -07:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
} else if (topic.match(topicRegexRemoveAll)) {
|
|
|
|
type = 'remove_all';
|
|
|
|
} else {
|
|
|
|
return;
|
2018-12-21 16:07:53 -07:00
|
|
|
}
|
|
|
|
|
2019-09-23 13:21:27 -07:00
|
|
|
const entity = this.zigbee.resolveEntity(message);
|
2019-09-09 10:48:09 -07:00
|
|
|
if (!entity || !entity.type === 'device') {
|
|
|
|
logger.error(`Device '${message}' does not exist`);
|
2020-01-07 13:06:48 -07:00
|
|
|
this.mqtt.log(
|
|
|
|
`device_group_${type}_failed`,
|
|
|
|
{friendly_name: message, group: topicMatch[1], error: 'entity doesn\'t exists'});
|
2019-09-09 10:48:09 -07:00
|
|
|
return;
|
2019-04-29 11:38:40 -07:00
|
|
|
}
|
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
const keys = [
|
|
|
|
`${entity.device.ieeeAddr}/${entity.endpoint.ID}`,
|
|
|
|
`${entity.name}/${entity.endpoint.ID}`,
|
|
|
|
];
|
2019-04-29 11:38:40 -07:00
|
|
|
|
2020-04-04 15:05:05 -07:00
|
|
|
const definition = entity.definition;
|
|
|
|
const endpoints = definition && definition.endpoint ? definition.endpoint(entity.device) : null;
|
|
|
|
const endpointName = endpoints ? Object.entries(endpoints).find((e) => e[1] === entity.endpoint.ID)[0] : null;
|
|
|
|
|
|
|
|
if (endpointName) {
|
|
|
|
keys.push(`${entity.device.ieeeAddr}/${endpointName}`);
|
|
|
|
keys.push(`${entity.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
|
|
|
const hasEndpointName = postfixes.find((p) => message.endsWith(`/${p}`));
|
|
|
|
if (!hasEndpointName) {
|
2019-09-09 10:48:09 -07:00
|
|
|
keys.push(entity.name);
|
|
|
|
keys.push(entity.device.ieeeAddr);
|
2018-12-21 16:07:53 -07:00
|
|
|
}
|
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
if (type === 'add') {
|
|
|
|
logger.info(`Adding '${entity.name}' to '${group.name}'`);
|
|
|
|
await entity.endpoint.addToGroup(group.group);
|
|
|
|
settings.addDeviceToGroup(group.settings.ID, keys);
|
2019-09-15 00:13:02 -07:00
|
|
|
this.mqtt.log('device_group_add', {friendly_name: entity.name, group: group.name});
|
2019-09-09 10:48:09 -07:00
|
|
|
} else if (type === 'remove') {
|
|
|
|
logger.info(`Removing '${entity.name}' from '${group.name}'`);
|
|
|
|
await entity.endpoint.removeFromGroup(group.group);
|
|
|
|
settings.removeDeviceFromGroup(group.settings.ID, keys);
|
2019-09-15 00:13:02 -07:00
|
|
|
this.mqtt.log('device_group_remove', {friendly_name: entity.name, group: group.name});
|
2019-09-09 10:48:09 -07:00
|
|
|
} else { // remove_all
|
|
|
|
logger.info(`Removing '${entity.name}' from all groups`);
|
|
|
|
await entity.endpoint.removeFromAllGroups();
|
|
|
|
for (const settingsGroup of settings.getGroups()) {
|
|
|
|
settings.removeDeviceFromGroup(settingsGroup.ID, keys);
|
2019-09-15 00:13:02 -07:00
|
|
|
this.mqtt.log('device_group_remove_all', {friendly_name: entity.name});
|
2019-06-17 12:17:29 -07:00
|
|
|
}
|
2018-12-21 16:07:53 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = Groups;
|