2018-11-16 12:23:11 -07:00
|
|
|
const settings = require('../util/settings');
|
|
|
|
const logger = require('../util/logger');
|
|
|
|
const zigbeeShepherdConverters = require('zigbee-shepherd-converters');
|
2019-03-17 12:04:51 -07:00
|
|
|
const utils = require('../util/utils');
|
2018-11-16 12:23:11 -07:00
|
|
|
|
|
|
|
const configRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/config/\\w+`, 'g');
|
|
|
|
const allowedLogLevels = ['error', 'warn', 'info', 'debug'];
|
|
|
|
|
|
|
|
class BridgeConfig {
|
2019-02-04 10:36:49 -07:00
|
|
|
constructor(zigbee, mqtt, state, publishEntityState) {
|
2018-11-16 12:23:11 -07:00
|
|
|
this.zigbee = zigbee;
|
|
|
|
this.mqtt = mqtt;
|
|
|
|
this.state = state;
|
2019-02-04 10:36:49 -07:00
|
|
|
this.publishEntityState = publishEntityState;
|
2018-11-16 12:23:11 -07:00
|
|
|
|
|
|
|
// Bind functions
|
|
|
|
this.permitJoin = this.permitJoin.bind(this);
|
2019-02-18 11:46:57 -07:00
|
|
|
this.lastSeen = this.lastSeen.bind(this);
|
2019-03-10 06:04:09 -07:00
|
|
|
this.elapsed = this.elapsed.bind(this);
|
2019-02-16 11:40:34 -07:00
|
|
|
this.reset = this.reset.bind(this);
|
2018-11-16 12:23:11 -07:00
|
|
|
this.logLevel = this.logLevel.bind(this);
|
|
|
|
this.devices = this.devices.bind(this);
|
2019-03-19 11:18:22 -07:00
|
|
|
this.groups = this.groups.bind(this);
|
2018-11-16 12:23:11 -07:00
|
|
|
this.rename = this.rename.bind(this);
|
|
|
|
this.remove = this.remove.bind(this);
|
2019-01-08 11:00:02 -07:00
|
|
|
this.ban = this.ban.bind(this);
|
2019-02-22 12:10:00 -07:00
|
|
|
this.deviceOptions = this.deviceOptions.bind(this);
|
2019-03-12 13:19:04 -07:00
|
|
|
this.addGroup = this.addGroup.bind(this);
|
|
|
|
this.removeGroup = this.removeGroup.bind(this);
|
2018-11-16 12:23:11 -07:00
|
|
|
|
|
|
|
// Set supported options
|
|
|
|
this.supportedOptions = {
|
|
|
|
'permit_join': this.permitJoin,
|
2019-02-18 11:46:57 -07:00
|
|
|
'last_seen': this.lastSeen,
|
2019-03-10 06:04:09 -07:00
|
|
|
'elapsed': this.elapsed,
|
2019-02-16 11:40:34 -07:00
|
|
|
'reset': this.reset,
|
2018-11-16 12:23:11 -07:00
|
|
|
'log_level': this.logLevel,
|
|
|
|
'devices': this.devices,
|
2019-03-19 11:18:22 -07:00
|
|
|
'groups': this.groups,
|
2018-11-16 12:23:11 -07:00
|
|
|
'rename': this.rename,
|
|
|
|
'remove': this.remove,
|
2019-01-08 11:00:02 -07:00
|
|
|
'ban': this.ban,
|
2019-02-22 12:10:00 -07:00
|
|
|
'device_options': this.deviceOptions,
|
2019-03-12 13:19:04 -07:00
|
|
|
'add_group': this.addGroup,
|
|
|
|
'remove_group': this.removeGroup,
|
2018-11-16 12:23:11 -07:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2019-02-22 12:10:00 -07:00
|
|
|
deviceOptions(topic, message) {
|
|
|
|
let json = null;
|
|
|
|
try {
|
|
|
|
json = JSON.parse(message.toString());
|
|
|
|
} catch (e) {
|
|
|
|
logger.error('Failed to parse message as JSON');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!json.hasOwnProperty('friendly_name') || !json.hasOwnProperty('options')) {
|
|
|
|
logger.error('Invalid JSON message, should contain "friendly_name" and "options"');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const ieeeAddr = settings.getIeeeAddrByFriendlyName(json.friendly_name);
|
|
|
|
if (!ieeeAddr) {
|
|
|
|
logger.error(`Failed to find device '${json.friendly_name}'`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
settings.changeDeviceOptions(ieeeAddr, json.options);
|
|
|
|
logger.info(`Changed device specific options of '${json.friendly_name}' (${JSON.stringify(json.options)})`);
|
|
|
|
}
|
|
|
|
|
2018-11-16 12:23:11 -07:00
|
|
|
permitJoin(topic, message) {
|
2018-12-01 07:30:59 -07:00
|
|
|
this.zigbee.permitJoin(message.toString().toLowerCase() === 'true', () => this.publish());
|
2018-11-16 12:23:11 -07:00
|
|
|
}
|
|
|
|
|
2019-02-16 11:40:34 -07:00
|
|
|
reset(topic, message) {
|
|
|
|
this.zigbee.softReset((error) => {
|
|
|
|
if (error) {
|
|
|
|
logger.error('Soft reset failed');
|
|
|
|
} else {
|
|
|
|
logger.info('Soft resetted ZNP');
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-02-18 11:46:57 -07:00
|
|
|
lastSeen(topic, message) {
|
2019-03-02 08:47:36 -07:00
|
|
|
const allowed = ['disable', 'ISO_8601', 'epoch', 'ISO_8601_local'];
|
2019-02-18 11:46:57 -07:00
|
|
|
message = message.toString();
|
|
|
|
|
|
|
|
if (!allowed.includes(message)) {
|
|
|
|
logger.error(`${message} is not an allowed value, possible: ${allowed}`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
settings.set(['advanced', 'last_seen'], message);
|
|
|
|
logger.info(`Set last_seen to ${message}`);
|
|
|
|
}
|
|
|
|
|
2019-03-10 06:04:09 -07:00
|
|
|
elapsed(topic, message) {
|
|
|
|
const allowed = ['true', 'false'];
|
|
|
|
message = message.toString();
|
|
|
|
|
|
|
|
if (!allowed.includes(message)) {
|
|
|
|
logger.error(`${message} is not an allowed value, possible: ${allowed}`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-03-10 13:34:20 -07:00
|
|
|
settings.set(['advanced', 'elapsed'], message === 'true');
|
2019-03-10 06:04:09 -07:00
|
|
|
logger.info(`Set elapsed to ${message}`);
|
|
|
|
}
|
|
|
|
|
2018-11-16 12:23:11 -07:00
|
|
|
logLevel(topic, message) {
|
|
|
|
const level = message.toString().toLowerCase();
|
|
|
|
if (allowedLogLevels.includes(level)) {
|
|
|
|
logger.info(`Switching log level to '${level}'`);
|
|
|
|
logger.transports.console.level = level;
|
|
|
|
logger.transports.file.level = level;
|
|
|
|
} else {
|
|
|
|
logger.error(`Could not set log level to '${level}'. Allowed level: '${allowedLogLevels.join(',')}'`);
|
|
|
|
}
|
2018-12-01 07:30:59 -07:00
|
|
|
|
|
|
|
this.publish();
|
2018-11-16 12:23:11 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
devices(topic, message) {
|
2019-03-14 12:30:06 -07:00
|
|
|
const devices = this.zigbee.getDevices().map((device) => {
|
|
|
|
const payload = {
|
2018-11-16 12:23:11 -07:00
|
|
|
ieeeAddr: device.ieeeAddr,
|
|
|
|
type: device.type,
|
|
|
|
};
|
2019-03-14 12:30:06 -07:00
|
|
|
|
|
|
|
if (device.type !== 'Coordinator') {
|
|
|
|
const mappedDevice = zigbeeShepherdConverters.findByZigbeeModel(device.modelId);
|
|
|
|
const friendlyDevice = settings.getDevice(device.ieeeAddr);
|
|
|
|
payload.model = mappedDevice ? mappedDevice.model : device.modelId;
|
|
|
|
payload.friendly_name = friendlyDevice ? friendlyDevice.friendly_name : device.ieeeAddr;
|
2019-03-21 12:00:13 -07:00
|
|
|
payload.nwkAddr = device.nwkAddr;
|
|
|
|
payload.manufId = device.manufId;
|
|
|
|
payload.manufName = device.manufName;
|
|
|
|
payload.powerSource = device.powerSource;
|
|
|
|
payload.modelId = device.modelId;
|
|
|
|
payload.hwVersion = device.hwVersion;
|
|
|
|
payload.swBuildId = device.swBuildId;
|
2019-03-14 12:30:06 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
return payload;
|
2018-11-16 12:23:11 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
this.mqtt.log('devices', devices);
|
|
|
|
}
|
|
|
|
|
2019-03-19 11:18:22 -07:00
|
|
|
groups(topic, message) {
|
|
|
|
this.mqtt.log('groups', settings.getGroups());
|
|
|
|
}
|
|
|
|
|
2018-11-16 12:23:11 -07:00
|
|
|
rename(topic, message) {
|
|
|
|
const invalid = `Invalid rename message format expected {old: 'friendly_name', new: 'new_name} ` +
|
|
|
|
`got ${message.toString()}`;
|
|
|
|
|
|
|
|
let json = null;
|
|
|
|
try {
|
|
|
|
json = JSON.parse(message.toString());
|
|
|
|
} catch (e) {
|
|
|
|
logger.error(invalid);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Validate message
|
|
|
|
if (!json.new || !json.old) {
|
|
|
|
logger.error(invalid);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (settings.changeFriendlyName(json.old, json.new)) {
|
|
|
|
logger.info(`Successfully renamed - ${json.old} to ${json.new} `);
|
2019-02-13 11:49:06 -07:00
|
|
|
this.mqtt.log('device_renamed', {from: json.old, to: json.new});
|
2018-11-16 12:23:11 -07:00
|
|
|
} else {
|
|
|
|
logger.error(`Failed to renamed - ${json.old} to ${json.new}`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-12 13:19:04 -07:00
|
|
|
addGroup(topic, message) {
|
|
|
|
const name = message.toString();
|
|
|
|
const added = settings.addGroup(name);
|
|
|
|
added ? logger.info(`Added group '${name}'`) : logger.error(`Failed to add group '${name}'`);
|
|
|
|
}
|
|
|
|
|
|
|
|
removeGroup(topic, message) {
|
|
|
|
const name = message.toString();
|
|
|
|
const removed = settings.removeGroup(name);
|
|
|
|
removed ? logger.info(`Removed group '${name}'`) : logger.error(`Failed to remove group '${name}'`);
|
|
|
|
}
|
|
|
|
|
2018-11-16 12:23:11 -07:00
|
|
|
remove(topic, message) {
|
2019-01-08 11:00:02 -07:00
|
|
|
this.removeOrBan(false, message);
|
|
|
|
}
|
|
|
|
|
|
|
|
ban(topic, message) {
|
|
|
|
this.removeOrBan(true, message);
|
|
|
|
}
|
|
|
|
|
|
|
|
removeOrBan(ban, message) {
|
2018-11-16 12:23:11 -07:00
|
|
|
message = message.toString();
|
|
|
|
const IDByFriendlyName = settings.getIeeeAddrByFriendlyName(message);
|
|
|
|
const deviceID = IDByFriendlyName ? IDByFriendlyName : message;
|
|
|
|
const device = this.zigbee.getDevice(deviceID);
|
|
|
|
|
|
|
|
const cleanup = () => {
|
|
|
|
// Remove from configuration.yaml
|
|
|
|
settings.removeDevice(deviceID);
|
|
|
|
|
|
|
|
// Remove from state
|
2019-03-19 12:02:17 -07:00
|
|
|
if (this.state) {
|
|
|
|
this.state.remove(deviceID);
|
|
|
|
}
|
2018-11-16 12:23:11 -07:00
|
|
|
|
2019-01-08 11:00:02 -07:00
|
|
|
logger.info(`Successfully ${ban ? 'banned' : 'removed'} ${deviceID}`);
|
|
|
|
this.mqtt.log(ban ? 'device_banned' : 'device_removed', message);
|
2018-11-16 12:23:11 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
// Remove from zigbee network.
|
|
|
|
if (device) {
|
2019-01-08 11:00:02 -07:00
|
|
|
this.zigbee.removeDevice(deviceID, ban, (error) => {
|
2018-11-16 12:23:11 -07:00
|
|
|
if (!error) {
|
|
|
|
cleanup();
|
|
|
|
} else {
|
2019-01-08 11:00:02 -07:00
|
|
|
logger.error(`Failed to ${ban ? 'ban' : 'remove'} ${deviceID}`);
|
2018-11-16 12:23:11 -07:00
|
|
|
}
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
cleanup();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
onMQTTConnected() {
|
|
|
|
this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/config/+`);
|
2019-02-13 11:49:06 -07:00
|
|
|
this.publish();
|
2018-11-16 12:23:11 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
onMQTTMessage(topic, message) {
|
|
|
|
if (!topic.match(configRegex)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
const option = topic.split('/').slice(-1)[0];
|
|
|
|
|
|
|
|
if (!this.supportedOptions.hasOwnProperty(option)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.supportedOptions[option](topic, message);
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
2018-12-01 07:30:59 -07:00
|
|
|
|
|
|
|
publish() {
|
2019-03-17 12:04:51 -07:00
|
|
|
utils.getZigbee2mqttVersion((info) => {
|
|
|
|
const topic = `bridge/config`;
|
|
|
|
const payload = {
|
|
|
|
version: info.version,
|
|
|
|
commit: info.commitHash,
|
|
|
|
coordinator_firmware: this.zigbee.getFirmwareVersion(),
|
|
|
|
log_level: logger.transports.console.level,
|
|
|
|
permit_join: this.zigbee.getPermitJoin(),
|
|
|
|
};
|
2018-12-01 07:30:59 -07:00
|
|
|
|
2019-03-17 12:04:51 -07:00
|
|
|
this.mqtt.publish(topic, JSON.stringify(payload), {retain: true, qos: 0}, null);
|
|
|
|
});
|
2018-12-01 07:30:59 -07:00
|
|
|
}
|
2018-11-16 12:23:11 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = BridgeConfig;
|