2018-12-21 16:07:53 -07:00
|
|
|
const settings = require('../util/settings');
|
|
|
|
const logger = require('../util/logger');
|
2019-04-29 11:38:40 -07:00
|
|
|
const data = require('../util/data');
|
2019-05-29 11:11:40 -07:00
|
|
|
const utils = require('../util/utils');
|
2019-04-29 11:38:40 -07:00
|
|
|
const fs = require('fs');
|
|
|
|
const diff = require('deep-diff');
|
2018-12-21 16:07:53 -07:00
|
|
|
|
2018-12-27 10:43:34 -07:00
|
|
|
const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/group/.+/(remove|add|remove_all)$`);
|
2019-06-17 12:17:29 -07:00
|
|
|
const topicRegexRemoveAll = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/group/(remove|add|remove_all)$`);
|
2018-12-21 16:07:53 -07:00
|
|
|
|
|
|
|
class Groups {
|
2019-02-04 10:36:49 -07:00
|
|
|
constructor(zigbee, mqtt, state, publishEntityState) {
|
2018-12-21 16:07:53 -07:00
|
|
|
this.zigbee = zigbee;
|
|
|
|
this.mqtt = mqtt;
|
|
|
|
this.state = state;
|
2019-02-04 10:36:49 -07:00
|
|
|
this.publishEntityState = publishEntityState;
|
2019-04-29 11:38:40 -07:00
|
|
|
this.onStateChange = this.onStateChange.bind(this);
|
|
|
|
|
|
|
|
this.groupsCacheFile = data.joinPathStorage('groups_cache.json');
|
|
|
|
this.groupsCache = this.readGroupsCache();
|
|
|
|
}
|
|
|
|
|
|
|
|
readGroupsCache() {
|
|
|
|
return fs.existsSync(this.groupsCacheFile) ? JSON.parse(fs.readFileSync(this.groupsCacheFile, 'utf8')) : {};
|
|
|
|
}
|
|
|
|
|
|
|
|
writeGroupsCache() {
|
|
|
|
fs.writeFileSync(this.groupsCacheFile, JSON.stringify(this.groupsCache), 'utf8');
|
2018-12-21 16:07:53 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
onMQTTConnected() {
|
2018-12-27 10:43:34 -07:00
|
|
|
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`);
|
2019-06-17 12:17:29 -07:00
|
|
|
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/group/remove_all`);
|
2018-12-21 16:07:53 -07:00
|
|
|
}
|
|
|
|
|
2019-04-29 11:38:40 -07:00
|
|
|
apply(from, to) {
|
|
|
|
const sortGroups = (obj) => Object.keys(obj).forEach((key) => obj[key] = obj[key].sort());
|
|
|
|
|
|
|
|
sortGroups(from);
|
|
|
|
sortGroups(to);
|
|
|
|
|
|
|
|
const differences = diff(from, to);
|
|
|
|
if (differences) {
|
|
|
|
differences.forEach((diff) => {
|
|
|
|
const groupID = diff.path[0];
|
|
|
|
|
|
|
|
if (diff.kind === 'N') {
|
2019-05-29 11:11:40 -07:00
|
|
|
diff.rhs.forEach((ID) => this.updateDeviceGroup(ID, 'add', groupID));
|
2019-04-29 11:38:40 -07:00
|
|
|
} else if (diff.kind === 'A') {
|
|
|
|
if (diff.item.lhs) {
|
|
|
|
this.updateDeviceGroup(diff.item.lhs, 'remove', groupID);
|
|
|
|
} else {
|
|
|
|
this.updateDeviceGroup(diff.item.rhs, 'add', groupID);
|
|
|
|
}
|
|
|
|
} else if (diff.kind === 'D') {
|
2019-05-29 11:11:40 -07:00
|
|
|
diff.lhs.forEach((ID) => this.updateDeviceGroup(ID, 'remove', groupID));
|
2019-04-29 11:38:40 -07:00
|
|
|
} else if (diff.kind === 'E') {
|
|
|
|
this.updateDeviceGroup(diff.rhs, 'add', groupID);
|
|
|
|
this.updateDeviceGroup(diff.lhs, 'remove', groupID);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-05-29 11:11:40 -07:00
|
|
|
parseID(ID) {
|
|
|
|
let entityID = ID;
|
|
|
|
let endpointID = null;
|
|
|
|
const postfix = utils.getPostfixes().find((p) => entityID.endsWith(`/${p}`));
|
|
|
|
if (postfix) {
|
|
|
|
// Found a postfix, retrieve the endpoint which correspodns to the postfix
|
|
|
|
entityID = entityID.substring(0, entityID.length - (postfix.length + 1));
|
|
|
|
const endpoint = utils.getEndpointByEntityID(this.zigbee, entityID, postfix);
|
|
|
|
|
|
|
|
if (!endpoint) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
endpointID = endpoint.epId;
|
|
|
|
}
|
|
|
|
|
|
|
|
return {endpointID, entityID};
|
|
|
|
}
|
|
|
|
|
|
|
|
getGroupsOfDevice(entityID) {
|
2019-04-29 11:38:40 -07:00
|
|
|
return Object.keys(settings.getGroups()).filter((groupID) => {
|
2019-05-29 11:11:40 -07:00
|
|
|
return settings.getGroup(groupID).devices.includes(entityID);
|
2019-04-29 11:38:40 -07:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
onStateChange(ieeeAddr, from, to) {
|
|
|
|
const properties = ['state', 'brightness', 'color_temp', 'color'];
|
|
|
|
const payload = {};
|
|
|
|
|
|
|
|
properties.forEach((prop) => {
|
2019-04-30 10:30:26 -07:00
|
|
|
if (to.hasOwnProperty(prop) && (!from || from[prop] != to[prop])) {
|
2019-04-29 11:38:40 -07:00
|
|
|
payload[prop] = to[prop];
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if (Object.keys(payload)) {
|
|
|
|
const groups = this.getGroupsOfDevice(ieeeAddr);
|
|
|
|
groups.forEach((groupID) => {
|
|
|
|
this.publishEntityState(groupID, payload);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
onZigbeeStarted() {
|
|
|
|
this.state.registerOnStateChangeListener(this.onStateChange);
|
|
|
|
|
|
|
|
const settingsGroups = {};
|
|
|
|
Object.keys(settings.getGroups()).forEach((groupID) => {
|
|
|
|
settingsGroups[groupID] = settings.getGroup(groupID).devices;
|
|
|
|
});
|
|
|
|
|
|
|
|
this.apply(this.groupsCache, settingsGroups);
|
|
|
|
}
|
|
|
|
|
2018-12-21 16:07:53 -07:00
|
|
|
parseTopic(topic) {
|
2019-06-17 12:17:29 -07:00
|
|
|
if (!topic.match(topicRegex) && !topic.match(topicRegexRemoveAll)) {
|
2018-12-21 16:07:53 -07:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove base from topic
|
2018-12-27 10:43:34 -07:00
|
|
|
topic = topic.replace(`${settings.get().mqtt.base_topic}/bridge/group/`, '');
|
2018-12-21 16:07:53 -07:00
|
|
|
|
|
|
|
// Parse type from topic
|
|
|
|
const type = topic.substr(topic.lastIndexOf('/') + 1, topic.length);
|
|
|
|
|
|
|
|
// Remove type from topic
|
|
|
|
topic = topic.replace(`/${type}`, '');
|
|
|
|
|
2019-06-17 12:17:29 -07:00
|
|
|
return {friendly_name: type === 'remove_all' ? null : topic, type};
|
2018-12-21 16:07:53 -07:00
|
|
|
}
|
|
|
|
|
2019-05-29 11:11:40 -07:00
|
|
|
updateDeviceGroup(ID, cmd, groupID) {
|
2019-04-29 11:38:40 -07:00
|
|
|
let payload = null;
|
|
|
|
const orignalCmd = cmd;
|
|
|
|
if (cmd === 'add') {
|
|
|
|
payload = {groupid: groupID, groupname: ''};
|
|
|
|
cmd = 'add';
|
|
|
|
} else if (cmd === 'remove') {
|
|
|
|
payload = {groupid: groupID};
|
|
|
|
cmd = 'remove';
|
|
|
|
} else if (cmd === 'remove_all') {
|
|
|
|
payload = {};
|
|
|
|
cmd = 'removeAll';
|
|
|
|
}
|
|
|
|
|
2019-05-29 11:11:40 -07:00
|
|
|
const {entityID, endpointID} = this.parseID(ID);
|
|
|
|
const ieeeAddr = settings.resolveEntity(entityID).ID;
|
|
|
|
|
2019-04-29 11:38:40 -07:00
|
|
|
const cb = (error, rsp) => {
|
|
|
|
if (error) {
|
|
|
|
logger.error(`Failed to ${cmd} ${ieeeAddr} from ${groupID}`);
|
|
|
|
} else {
|
|
|
|
logger.info(`Successfully ${cmd} ${ieeeAddr} to ${groupID}`);
|
|
|
|
|
|
|
|
// Log to MQTT
|
|
|
|
this.mqtt.log({
|
|
|
|
device: settings.getDevice(ieeeAddr).friendly_name,
|
|
|
|
group: groupID,
|
|
|
|
action: orignalCmd,
|
|
|
|
});
|
|
|
|
|
|
|
|
// Update group cache
|
|
|
|
if (cmd === 'add') {
|
|
|
|
if (!this.groupsCache[groupID]) {
|
|
|
|
this.groupsCache[groupID] = [];
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!this.groupsCache[groupID].includes(ieeeAddr)) {
|
|
|
|
this.groupsCache[groupID].push(ieeeAddr);
|
|
|
|
}
|
|
|
|
} else if (cmd === 'remove') {
|
|
|
|
if (this.groupsCache[groupID]) {
|
|
|
|
this.groupsCache[groupID] = this.groupsCache[groupID].filter((device) => device != ieeeAddr);
|
|
|
|
}
|
|
|
|
} else if (cmd === 'removeAll') {
|
|
|
|
Object.keys(this.groupsCache).forEach((groupID_) => {
|
|
|
|
this.groupsCache[groupID_] = this.groupsCache[groupID_].filter((device) => device != ieeeAddr);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
this.writeGroupsCache();
|
|
|
|
|
|
|
|
// Update settings
|
|
|
|
if (cmd === 'add') {
|
|
|
|
settings.addDeviceToGroup(groupID, ieeeAddr);
|
|
|
|
} else if (cmd === 'remove') {
|
|
|
|
settings.removeDeviceFromGroup(groupID, ieeeAddr);
|
|
|
|
} else if (cmd === 'removeAll') {
|
2019-06-17 12:17:29 -07:00
|
|
|
Object.keys(settings.get().groups).forEach((groupID_) => {
|
|
|
|
settings.removeDeviceFromGroup(groupID_, ieeeAddr);
|
2019-04-29 11:38:40 -07:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
this.zigbee.publish(
|
|
|
|
ieeeAddr, 'device', 'genGroups', cmd, 'functional',
|
2019-05-29 11:11:40 -07:00
|
|
|
payload, null, endpointID, cb,
|
2019-04-29 11:38:40 -07:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2018-12-21 16:07:53 -07:00
|
|
|
onMQTTMessage(topic, message) {
|
|
|
|
topic = this.parseTopic(topic);
|
|
|
|
|
|
|
|
if (!topic) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Find ID of this group.
|
2019-06-17 12:17:29 -07:00
|
|
|
let groupID = null;
|
|
|
|
if (topic.type !== 'remove_all') {
|
|
|
|
groupID = settings.getGroupIDByFriendlyName(topic.friendly_name);
|
|
|
|
if (!groupID) {
|
|
|
|
logger.error(`Group with friendly_name '${topic.friendly_name}' doesn't exist`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
groupID = groupID.toString();
|
2018-12-21 16:07:53 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
// Send command to the device.
|
2019-06-17 12:17:29 -07:00
|
|
|
this.updateDeviceGroup(message.toString(), topic.type, groupID);
|
2018-12-21 16:07:53 -07:00
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = Groups;
|