Skip to content

Commit

Permalink
Group configuration via configuration.yaml and group state (Koenkk#1464)
Browse files Browse the repository at this point in the history
Group configuration via configuration.yaml and group state.
  • Loading branch information
Koenkk authored Apr 29, 2019
1 parent 4472c01 commit a54e256
Show file tree
Hide file tree
Showing 9 changed files with 408 additions and 47 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,5 @@ data/log*.txt
data/state.json
data/log
data-backup/
data/coordinator_backup.json
data/coordinator_backup.json
data/.storage
180 changes: 147 additions & 33 deletions lib/extension/groups.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
const settings = require('../util/settings');
const logger = require('../util/logger');
const data = require('../util/data');
const fs = require('fs');
const diff = require('deep-diff');

const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/group/.+/(remove|add|remove_all)$`);

Expand All @@ -9,6 +12,18 @@ class Groups {
this.mqtt = mqtt;
this.state = state;
this.publishEntityState = publishEntityState;
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');
}

onMQTTConnected() {
Expand All @@ -17,6 +32,70 @@ class Groups {
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/group/+/remove_all`);
}

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') {
diff.rhs.forEach((ieeeAddr) => this.updateDeviceGroup(ieeeAddr, 'add', groupID));
} 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') {
diff.lhs.forEach((ieeeAddr) => this.updateDeviceGroup(ieeeAddr, 'remove', groupID));
} else if (diff.kind === 'E') {
this.updateDeviceGroup(diff.rhs, 'add', groupID);
this.updateDeviceGroup(diff.lhs, 'remove', groupID);
}
});
}
}

getGroupsOfDevice(ieeeAddr) {
return Object.keys(settings.getGroups()).filter((groupID) => {
return settings.getGroup(groupID).devices.includes(ieeeAddr);
});
}

onStateChange(ieeeAddr, from, to) {
const properties = ['state', 'brightness', 'color_temp', 'color'];
const payload = {};

properties.forEach((prop) => {
if (to.hasOwnProperty(prop) && from[prop] != to[prop]) {
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);
}

parseTopic(topic) {
if (!topic.match(topicRegex)) {
return null;
Expand All @@ -34,6 +113,73 @@ class Groups {
return {friendly_name: topic, type};
}

updateDeviceGroup(ieeeAddr, cmd, groupID) {
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';
}

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') {
Object.keys(settings.get().groups).forEach((groupID) => {
settings.removeDeviceFromGroup(groupID, ieeeAddr);
});
}
}
};

this.zigbee.publish(
ieeeAddr, 'device', 'genGroups', cmd, 'functional',
payload, null, null, cb,
);
}

onMQTTMessage(topic, message) {
topic = this.parseTopic(topic);

Expand Down Expand Up @@ -62,39 +208,7 @@ 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) => {
if (error) {
logger.error(`Failed to ${topic.type} ${ieeeAddr} from ${topic.friendly_name}`);
} else {
logger.info(`Successfully ${topic.type} ${ieeeAddr} to ${topic.friendly_name}`);

// Log to MQTT
const log = {device: message};
if (['remove', 'add'].includes(topic.type)) {
log.group = topic.friendly_name;
}

this.mqtt.log(log);
}
};

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

return true;
}
Expand Down
16 changes: 12 additions & 4 deletions lib/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class State {
this.state = {};
this.file = data.joinPath('state.json');
this.timer = null;
this.stateChangeListeners = [];

this.handleSettingsChanged = this.handleSettingsChanged.bind(this);
}
Expand All @@ -38,6 +39,10 @@ class State {
this.checkLastSeen();
}

registerOnStateChangeListener(listener) {
this.stateChangeListeners.push(listener);
}

checkLastSeen() {
if (settings.get().advanced.last_seen === 'disable') {
Object.values(this.state).forEach((s) => {
Expand Down Expand Up @@ -90,14 +95,17 @@ class State {
}

set(ieeeAddr, state) {
const s = objectAssignDeep.noMutate(state);
const toState = objectAssignDeep.noMutate(state);
dontCacheProperties.forEach((property) => {
if (s.hasOwnProperty(property)) {
delete s[property];
if (toState.hasOwnProperty(property)) {
delete toState[property];
}
});

this.state[ieeeAddr] = s;
const fromState = this.state[ieeeAddr];
this.stateChangeListeners.forEach((listener) => listener(ieeeAddr, fromState, toState));

this.state[ieeeAddr] = toState;
}

remove(ieeeAddr) {
Expand Down
11 changes: 11 additions & 0 deletions lib/util/data.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const path = require('path');
const fs = require('fs');

let dataPath = null;

Expand All @@ -13,8 +14,18 @@ function load() {

load();

function joinPathStorage(file) {
const storagePath = path.join(dataPath, '.storage');
if (!fs.existsSync(storagePath)) {
fs.mkdirSync(storagePath);
}

return path.join(storagePath, file);
}

module.exports = {
joinPath: (file) => path.join(dataPath, file),
joinPathStorage: (file) => joinPathStorage(file),
getPath: () => dataPath,

// For test only.
Expand Down
43 changes: 41 additions & 2 deletions lib/util/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,17 @@ const getDevices = () => getSettings().devices || [];

const getDevice = (ieeeAddr) => getDevices()[ieeeAddr];

const getGroups = () => getSettings().groups || [];
const getGroups = () => getSettings().groups || {};

const getGroup = (ID) => getGroups()[ID];
const getGroup = (ID) => {
const group = getGroups()[ID];

if (group && !group.hasOwnProperty('devices')) {
group.devices = [];
}

return group;
};


function addDevice(ieeeAddr) {
Expand Down Expand Up @@ -204,6 +212,35 @@ function addGroup(groupName) {
return true;
}

function addDeviceToGroup(ID, ieeeAddr) {
const settings = getSettings();
const group = settings.groups[ID];
if (!group.devices) {
group.devices = [];
}

if (!group.devices.includes(ieeeAddr)) {
group.devices.push(ieeeAddr);
writeRead();
return true;
} else {
return false;
}
}

function removeDeviceFromGroup(ID, ieeeAddr) {
const settings = getSettings();
const group = settings.groups[ID];

if (group.devices && group.devices.includes(ieeeAddr)) {
group.devices = group.devices.filter((d) => d != ieeeAddr);
writeRead();
return true;
} else {
return false;
}
}

function removeGroup(name) {
const settings = getSettings();
if (!settings.groups) return;
Expand Down Expand Up @@ -308,6 +345,8 @@ module.exports = {
removeDevice: (ieeeAddr) => removeDevice(ieeeAddr),
addGroup: (name) => addGroup(name),
removeGroup: (name) => removeGroup(name),
addDeviceToGroup: (ID, ieeeAddr) => addDeviceToGroup(ID, ieeeAddr),
removeDeviceFromGroup: (ID, ieeeAddr) => removeDeviceFromGroup(ID, ieeeAddr),

getIeeeAddrByFriendlyName: (friendlyName) => getIeeeAddrByFriendlyName(friendlyName),
getGroupIDByFriendlyName: (friendlyName) => getGroupIDByFriendlyName(friendlyName),
Expand Down
17 changes: 11 additions & 6 deletions npm-shrinkwrap.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"ziee": "*",
"zigbee-shepherd": "git+https://github.com/Koenkk/zigbee-shepherd.git#30d08aacf50327dc1e2c3f146076b9efb8581192",
"zigbee-shepherd-converters": "8.1.5",
"deep-diff": "*",
"zive": "*"
},
"devDependencies": {
Expand Down
Loading

0 comments on commit a54e256

Please sign in to comment.