2018-05-11 20:04:15 -07:00
|
|
|
const data = require('./data');
|
2019-12-09 10:27:39 -07:00
|
|
|
const utils = require('./utils');
|
2019-12-16 12:49:25 -07:00
|
|
|
const file = process.env.ZIGBEE2MQTT_CONFIG || data.joinPath('configuration.yaml');
|
2018-09-18 08:51:34 -07:00
|
|
|
const objectAssignDeep = require(`object-assign-deep`);
|
|
|
|
const path = require('path');
|
2019-09-09 10:48:09 -07:00
|
|
|
const yaml = require('./yaml');
|
2019-09-25 03:08:39 -07:00
|
|
|
const Ajv = require('ajv');
|
|
|
|
const ajv = new Ajv({allErrors: true});
|
2019-02-23 08:22:01 -07:00
|
|
|
|
2018-09-18 08:51:34 -07:00
|
|
|
const defaults = {
|
2019-06-25 10:38:36 -07:00
|
|
|
whitelist: [],
|
2019-03-26 13:34:58 -07:00
|
|
|
ban: [],
|
2018-09-22 15:07:31 -07:00
|
|
|
permit_join: false,
|
2018-11-16 12:23:11 -07:00
|
|
|
mqtt: {
|
|
|
|
include_device_information: false,
|
|
|
|
},
|
2019-09-09 10:48:09 -07:00
|
|
|
serial: {
|
|
|
|
disable_led: false,
|
|
|
|
},
|
2018-12-19 09:33:02 -07:00
|
|
|
device_options: {},
|
2019-06-26 10:30:38 -07:00
|
|
|
map_options: {
|
|
|
|
graphviz: {
|
|
|
|
colors: {
|
|
|
|
fill: {
|
|
|
|
enddevice: '#fff8ce',
|
|
|
|
coordinator: '#e04e5d',
|
|
|
|
router: '#4ea3e0',
|
|
|
|
},
|
|
|
|
font: {
|
|
|
|
coordinator: '#ffffff',
|
|
|
|
router: '#ffffff',
|
|
|
|
enddevice: '#000000',
|
|
|
|
},
|
|
|
|
line: {
|
|
|
|
active: '#009900',
|
|
|
|
inactive: '#994444',
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
2019-02-04 10:39:45 -07:00
|
|
|
experimental: {
|
2020-01-12 07:07:06 -07:00
|
|
|
// json or attribute or attribute_and_json
|
2019-03-04 10:13:36 -07:00
|
|
|
output: 'json',
|
2019-02-04 10:39:45 -07:00
|
|
|
},
|
2018-09-18 08:51:34 -07:00
|
|
|
advanced: {
|
2019-11-29 15:36:57 -07:00
|
|
|
log_output: ['console', 'file'],
|
2018-09-18 08:51:34 -07:00
|
|
|
log_directory: path.join(data.getPath(), 'log', '%TIMESTAMP%'),
|
2020-02-05 11:37:13 -07:00
|
|
|
log_file: 'log.txt',
|
2019-09-09 10:48:09 -07:00
|
|
|
log_level: /* istanbul ignore next */ process.env.DEBUG ? 'debug' : 'info',
|
2018-10-02 12:15:12 -07:00
|
|
|
soft_reset_timeout: 0,
|
2018-11-16 12:23:11 -07:00
|
|
|
pan_id: 0x1a62,
|
2019-02-02 12:09:20 -07:00
|
|
|
ext_pan_id: [0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD],
|
2018-11-16 12:23:11 -07:00
|
|
|
channel: 11,
|
|
|
|
baudrate: 115200,
|
|
|
|
rtscts: true,
|
|
|
|
|
2019-01-29 12:17:56 -07:00
|
|
|
// Availability timeout in seconds, disabled by default.
|
|
|
|
availability_timeout: 0,
|
2019-02-02 10:10:25 -07:00
|
|
|
availability_blacklist: [],
|
2019-11-23 03:47:37 -07:00
|
|
|
availability_whitelist: [],
|
2019-01-29 12:17:56 -07:00
|
|
|
|
2018-11-16 12:23:11 -07:00
|
|
|
/**
|
|
|
|
* Home Assistant requires ALL attributes to be present in ALL MQTT messages send by the device.
|
|
|
|
* https://community.home-assistant.io/t/missing-value-with-mqtt-only-last-data-set-is-shown/47070/9
|
|
|
|
*
|
|
|
|
* Therefore zigbee2mqtt BY DEFAULT caches all values and resend it with every message.
|
|
|
|
* advanced.cache_state in configuration.yaml allows to configure this.
|
2019-02-12 13:39:37 -07:00
|
|
|
* https://www.zigbee2mqtt.io/configuration/configuration.html
|
2018-11-16 12:23:11 -07:00
|
|
|
*/
|
|
|
|
cache_state: true,
|
2018-12-24 08:29:06 -07:00
|
|
|
|
2019-01-18 12:31:55 -07:00
|
|
|
/**
|
|
|
|
* Add a last_seen attribute to mqtt messages, contains date/time of zigbee message arrival
|
|
|
|
* "ISO_8601": ISO 8601 format
|
2019-03-02 08:47:36 -07:00
|
|
|
* "ISO_8601_local": Local ISO 8601 format (instead of UTC-based)
|
2019-01-18 12:31:55 -07:00
|
|
|
* "epoch": milliseconds elapsed since the UNIX epoch
|
|
|
|
* "disable": no last_seen attribute (default)
|
|
|
|
*/
|
|
|
|
last_seen: 'disable',
|
|
|
|
|
2019-01-22 12:02:34 -07:00
|
|
|
// Optional: Add an elapsed attribute to MQTT messages, contains milliseconds since the previous msg
|
|
|
|
elapsed: false,
|
|
|
|
|
2018-12-24 08:29:06 -07:00
|
|
|
/**
|
|
|
|
* https://github.com/Koenkk/zigbee2mqtt/issues/685#issuecomment-449112250
|
|
|
|
*
|
|
|
|
* Network key will serve as the encryption key of your network.
|
|
|
|
* Changing this will require you to repair your devices.
|
|
|
|
*/
|
|
|
|
network_key: [1, 3, 5, 7, 9, 11, 13, 15, 0, 2, 4, 6, 8, 10, 12, 13],
|
2019-02-26 12:21:35 -07:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Enables reporting feature
|
|
|
|
*/
|
|
|
|
report: false,
|
2019-03-15 14:41:39 -07:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Home Assistant discovery topic
|
|
|
|
*/
|
|
|
|
homeassistant_discovery_topic: 'homeassistant',
|
2019-06-24 11:52:47 -07:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Home Assistant status topic
|
|
|
|
*/
|
|
|
|
homeassistant_status_topic: 'hass/status',
|
2019-11-17 13:29:53 -07:00
|
|
|
|
2020-03-01 07:55:20 -07:00
|
|
|
/**
|
|
|
|
* Home Assistant legacy triggers, when enabled:
|
|
|
|
* - Zigbee2mqt will send an empty 'action' or 'click' after one has been send
|
|
|
|
* - A 'sensor_action' and 'sensor_click' will be discoverd
|
|
|
|
*/
|
|
|
|
homeassistant_legacy_triggers: true,
|
|
|
|
|
2019-11-17 13:29:53 -07:00
|
|
|
/**
|
|
|
|
* Configurable timestampFormat
|
|
|
|
* https://github.com/Koenkk/zigbee2mqtt/commit/44db557a0c83f419d66755d14e460cd78bd6204e
|
|
|
|
*/
|
|
|
|
timestamp_format: 'YYYY-MM-DD HH:mm:ss',
|
2018-09-18 08:51:34 -07:00
|
|
|
},
|
|
|
|
};
|
2018-05-12 01:58:06 -07:00
|
|
|
|
2019-09-25 03:08:39 -07:00
|
|
|
const schema = {
|
|
|
|
type: 'object',
|
|
|
|
properties: {
|
2020-03-11 12:18:27 -07:00
|
|
|
device_options: {type: 'object'},
|
2019-09-25 03:08:39 -07:00
|
|
|
homeassistant: {type: 'boolean'},
|
|
|
|
permit_join: {type: 'boolean'},
|
|
|
|
mqtt: {
|
|
|
|
type: 'object',
|
|
|
|
properties: {
|
|
|
|
base_topic: {type: 'string'},
|
|
|
|
server: {type: 'string'},
|
2020-01-17 13:38:46 -07:00
|
|
|
keepalive: {type: 'number'},
|
2019-09-25 03:08:39 -07:00
|
|
|
ca: {type: 'string'},
|
|
|
|
key: {type: 'string'},
|
|
|
|
cert: {type: 'string'},
|
|
|
|
user: {type: 'string'},
|
|
|
|
password: {type: 'string'},
|
|
|
|
client_id: {type: 'string'},
|
|
|
|
reject_unauthorized: {type: 'boolean'},
|
|
|
|
include_device_information: {type: 'boolean'},
|
2020-03-12 12:25:37 -07:00
|
|
|
version: {type: 'number'},
|
2019-09-25 03:08:39 -07:00
|
|
|
},
|
|
|
|
required: ['base_topic', 'server'],
|
|
|
|
},
|
|
|
|
serial: {
|
|
|
|
type: 'object',
|
|
|
|
properties: {
|
2019-10-16 12:21:52 -07:00
|
|
|
port: {type: ['string', 'null']},
|
2019-09-25 03:08:39 -07:00
|
|
|
disable_led: {type: 'boolean'},
|
2020-03-19 11:34:11 -07:00
|
|
|
adapter: {type: 'string', enum: ['deconz', 'zstack']},
|
2019-09-25 03:08:39 -07:00
|
|
|
},
|
|
|
|
},
|
|
|
|
ban: {type: 'array', items: {type: 'string'}},
|
|
|
|
whitelist: {type: 'array', items: {type: 'string'}},
|
2019-11-27 14:02:49 -07:00
|
|
|
experimental: {
|
|
|
|
type: 'object',
|
|
|
|
properties: {
|
|
|
|
transmit_power: {type: 'number'},
|
|
|
|
},
|
|
|
|
},
|
2019-09-25 03:08:39 -07:00
|
|
|
advanced: {
|
|
|
|
type: 'object',
|
|
|
|
properties: {
|
|
|
|
pan_id: {type: 'number'},
|
|
|
|
ext_pan_id: {type: 'array', items: {type: 'number'}},
|
|
|
|
channel: {type: 'number', minimum: 11, maximum: 26},
|
|
|
|
cache_state: {type: 'boolean'},
|
|
|
|
log_level: {type: 'string', enum: ['info', 'warn', 'error', 'debug']},
|
2019-11-29 15:36:57 -07:00
|
|
|
log_output: {type: 'array', items: {type: 'string'}},
|
2019-09-25 03:08:39 -07:00
|
|
|
log_directory: {type: 'string'},
|
2020-02-05 11:37:13 -07:00
|
|
|
log_file: {type: 'string'},
|
2019-09-25 03:08:39 -07:00
|
|
|
baudrate: {type: 'number'},
|
|
|
|
rtscts: {type: 'boolean'},
|
|
|
|
soft_reset_timeout: {type: 'number', minimum: 0},
|
|
|
|
network_key: {type: 'array', items: {type: 'number'}},
|
|
|
|
last_seen: {type: 'string', enum: ['disable', 'ISO_8601', 'ISO_8601_local', 'epoch']},
|
|
|
|
elapsed: {type: 'boolean'},
|
|
|
|
availability_timeout: {type: 'number', minimum: 0},
|
|
|
|
availability_blacklist: {type: 'array', items: {type: 'string'}},
|
2019-11-23 03:47:37 -07:00
|
|
|
availability_whitelist: {type: 'array', items: {type: 'string'}},
|
2019-09-25 03:08:39 -07:00
|
|
|
report: {type: 'boolean'},
|
|
|
|
homeassistant_discovery_topic: {type: 'string'},
|
|
|
|
homeassistant_status_topic: {type: 'string'},
|
2019-11-17 13:29:53 -07:00
|
|
|
timestamp_format: {type: 'string'},
|
2019-09-25 03:08:39 -07:00
|
|
|
},
|
|
|
|
},
|
|
|
|
map_options: {
|
|
|
|
type: 'object',
|
|
|
|
properties: {
|
|
|
|
graphviz: {
|
|
|
|
type: 'object',
|
|
|
|
properties: {
|
|
|
|
colors: {
|
|
|
|
type: 'object',
|
|
|
|
properties: {
|
|
|
|
fill: {
|
|
|
|
type: 'object',
|
|
|
|
properties: {
|
|
|
|
enddevice: {type: 'string'},
|
|
|
|
coordinator: {type: 'string'},
|
|
|
|
router: {type: 'string'},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
font: {
|
|
|
|
type: 'object',
|
|
|
|
properties: {
|
|
|
|
enddevice: {type: 'string'},
|
|
|
|
coordinator: {type: 'string'},
|
|
|
|
router: {type: 'string'},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
line: {
|
|
|
|
type: 'object',
|
|
|
|
properties: {
|
|
|
|
active: {type: 'string'},
|
|
|
|
inactive: {type: 'string'},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
devices: {
|
|
|
|
type: 'object',
|
|
|
|
propertyNames: {
|
|
|
|
pattern: '^0x[\\d\\w]{16}$',
|
|
|
|
},
|
|
|
|
patternProperties: {
|
|
|
|
'^.*$': {
|
|
|
|
type: 'object',
|
|
|
|
properties: {
|
|
|
|
friendly_name: {type: 'string'},
|
|
|
|
retain: {type: 'boolean'},
|
2020-03-12 12:25:37 -07:00
|
|
|
retention: {type: 'number'},
|
2019-09-25 03:08:39 -07:00
|
|
|
qos: {type: 'number'},
|
2020-03-02 12:08:51 -07:00
|
|
|
filtered_attributes: {type: 'array', items: {type: 'string'}},
|
2019-09-25 03:08:39 -07:00
|
|
|
},
|
2019-11-06 12:30:33 -07:00
|
|
|
required: ['friendly_name'],
|
2019-09-25 03:08:39 -07:00
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
groups: {
|
|
|
|
type: 'object',
|
|
|
|
propertyNames: {
|
|
|
|
pattern: '^[\\w].*$',
|
|
|
|
},
|
|
|
|
patternProperties: {
|
|
|
|
'^.*$': {
|
|
|
|
type: 'object',
|
|
|
|
properties: {
|
|
|
|
friendly_name: {type: 'string'},
|
|
|
|
retain: {type: 'boolean'},
|
|
|
|
devices: {type: 'array', items: {type: 'string'}},
|
2019-09-29 05:35:05 -07:00
|
|
|
optimistic: {type: 'boolean'},
|
2019-09-25 03:08:39 -07:00
|
|
|
qos: {type: 'number'},
|
2020-03-02 12:08:51 -07:00
|
|
|
filtered_attributes: {type: 'array', items: {type: 'string'}},
|
2019-09-25 03:08:39 -07:00
|
|
|
},
|
2019-11-06 12:30:33 -07:00
|
|
|
required: ['friendly_name'],
|
2019-09-25 03:08:39 -07:00
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
2019-10-12 14:18:01 -07:00
|
|
|
required: ['homeassistant', 'permit_join', 'mqtt'],
|
2019-09-25 03:08:39 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
|
2019-03-08 08:27:09 -07:00
|
|
|
let _settings;
|
2019-09-09 10:48:09 -07:00
|
|
|
let _settingsWithDefaults;
|
2018-05-12 06:54:02 -07:00
|
|
|
|
2018-04-18 09:25:40 -07:00
|
|
|
function write() {
|
2019-09-09 10:48:09 -07:00
|
|
|
const settings = get();
|
2019-02-24 07:49:41 -07:00
|
|
|
const toWrite = objectAssignDeep.noMutate(settings);
|
|
|
|
|
2019-03-09 06:36:06 -07:00
|
|
|
// Read settings to check if we have to split devices/groups into separate file.
|
2019-09-09 10:48:09 -07:00
|
|
|
const actual = yaml.read(file);
|
2019-10-25 10:17:47 -07:00
|
|
|
if (actual.mqtt && actual.mqtt.password && actual.mqtt.user) {
|
|
|
|
toWrite.mqtt.user = actual.mqtt.user;
|
|
|
|
toWrite.mqtt.password = actual.mqtt.password;
|
|
|
|
}
|
|
|
|
|
2019-10-28 10:05:50 -07:00
|
|
|
if (actual.advanced && actual.advanced.network_key) {
|
|
|
|
toWrite.advanced.network_key = actual.advanced.network_key;
|
|
|
|
}
|
|
|
|
|
2019-02-24 07:49:41 -07:00
|
|
|
if (typeof actual.devices === 'string') {
|
2019-10-03 11:06:31 -07:00
|
|
|
yaml.writeIfChanged(data.joinPath(actual.devices), settings.devices);
|
2019-02-24 07:49:41 -07:00
|
|
|
toWrite.devices = actual.devices;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (typeof actual.groups === 'string') {
|
2019-10-03 11:06:31 -07:00
|
|
|
yaml.writeIfChanged(data.joinPath(actual.groups), settings.groups);
|
2019-02-24 07:49:41 -07:00
|
|
|
toWrite.groups = actual.groups;
|
|
|
|
}
|
|
|
|
|
2019-10-03 11:06:31 -07:00
|
|
|
yaml.writeIfChanged(file, toWrite);
|
2019-09-09 10:48:09 -07:00
|
|
|
|
|
|
|
_settings = read();
|
|
|
|
_settingsWithDefaults = objectAssignDeep.noMutate(defaults, get());
|
2019-02-24 07:49:41 -07:00
|
|
|
}
|
|
|
|
|
2019-09-25 03:08:39 -07:00
|
|
|
function validate() {
|
|
|
|
const validate = ajv.compile(schema);
|
|
|
|
const valid = validate(_settings);
|
2019-12-09 10:27:39 -07:00
|
|
|
const postfixes = utils.getPostfixes();
|
2019-09-27 13:54:07 -07:00
|
|
|
|
|
|
|
// Verify that all friendly names are unique
|
|
|
|
const names = [];
|
|
|
|
const check = (name) => {
|
|
|
|
if (names.includes(name)) throw new Error(`Duplicate friendly_name '${name}' found`);
|
2019-12-09 10:27:39 -07:00
|
|
|
if (postfixes.includes(name)) throw new Error(`Following friendly_name are not allowed: '${postfixes}'`);
|
2020-03-07 15:06:22 -07:00
|
|
|
if (name.match(/.*\/\d*$/)) throw new Error(`Friendly name cannot end with a "/DIGIT" ('${name}')`);
|
2020-03-23 11:59:49 -07:00
|
|
|
if (name.includes('#') || name.includes('+')) {
|
|
|
|
throw new Error(`MQTT wildcard (+ and #) not allowed in friendly_name ('${name}')`);
|
|
|
|
}
|
2019-09-27 13:54:07 -07:00
|
|
|
names.push(name);
|
|
|
|
};
|
|
|
|
|
2020-03-25 12:46:20 -07:00
|
|
|
const settingsWithDefaults = getWithDefaults();
|
|
|
|
Object.values(settingsWithDefaults.devices).forEach((d) => check(d.friendly_name));
|
|
|
|
Object.values(settingsWithDefaults.groups).forEach((g) => check(g.friendly_name));
|
|
|
|
|
|
|
|
if (settingsWithDefaults.mqtt.version !== 5) {
|
|
|
|
for (const device of Object.values(settingsWithDefaults.devices)) {
|
2020-03-12 12:25:37 -07:00
|
|
|
if (device.retention) {
|
|
|
|
throw new Error('MQTT retention requires protocol version 5');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-25 12:46:20 -07:00
|
|
|
const checkAvailabilityList = (list, type) => {
|
|
|
|
list.forEach((e) => {
|
|
|
|
if (!getEntity(e)) {
|
|
|
|
throw new Error(`Non-existing entity '${e}' specified in '${type}'`);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
checkAvailabilityList(settingsWithDefaults.advanced.availability_blacklist, 'availability_blacklist');
|
|
|
|
checkAvailabilityList(settingsWithDefaults.advanced.availability_whitelist, 'availability_whitelist');
|
|
|
|
|
2019-09-25 03:08:39 -07:00
|
|
|
return !valid ? validate.errors.map((v) => `${v.dataPath.substring(1)} ${v.message}`) : null;
|
|
|
|
}
|
|
|
|
|
2019-02-24 07:49:41 -07:00
|
|
|
function read() {
|
2019-09-09 10:48:09 -07:00
|
|
|
const s = yaml.read(file);
|
2019-02-24 07:49:41 -07:00
|
|
|
|
2019-10-25 09:43:14 -07:00
|
|
|
// Read !secret MQTT username and password if set
|
|
|
|
const interpetValue = (value) => {
|
|
|
|
const re = /!(.*) (.*)/g;
|
|
|
|
const match = re.exec(value);
|
|
|
|
if (match) {
|
|
|
|
const file = data.joinPath(`${match[1]}.yaml`);
|
|
|
|
const key = match[2];
|
|
|
|
return yaml.read(file)[key];
|
|
|
|
} else {
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
if (s.mqtt && s.mqtt.user && s.mqtt.password) {
|
|
|
|
s.mqtt.user = interpetValue(s.mqtt.user);
|
|
|
|
s.mqtt.password = interpetValue(s.mqtt.password);
|
|
|
|
}
|
|
|
|
|
2019-10-28 10:05:50 -07:00
|
|
|
if (s.advanced && s.advanced.network_key) {
|
|
|
|
s.advanced.network_key = interpetValue(s.advanced.network_key);
|
|
|
|
}
|
|
|
|
|
2019-02-24 07:49:41 -07:00
|
|
|
// Read devices/groups configuration from separate file.
|
|
|
|
if (typeof s.devices === 'string') {
|
|
|
|
const file = data.joinPath(s.devices);
|
2019-09-09 10:48:09 -07:00
|
|
|
s.devices = yaml.readIfExists(file) || {};
|
2019-02-24 07:49:41 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
if (typeof s.groups === 'string') {
|
|
|
|
const file = data.joinPath(s.groups);
|
2019-09-09 10:48:09 -07:00
|
|
|
s.groups = yaml.readIfExists(file) || {};
|
2019-02-24 07:49:41 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
return s;
|
|
|
|
}
|
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
function get() {
|
|
|
|
if (!_settings) {
|
|
|
|
_settings = read();
|
|
|
|
}
|
|
|
|
|
|
|
|
return _settings;
|
|
|
|
}
|
|
|
|
|
|
|
|
function getWithDefaults() {
|
|
|
|
if (!_settingsWithDefaults) {
|
|
|
|
_settingsWithDefaults = objectAssignDeep.noMutate(defaults, get());
|
|
|
|
}
|
|
|
|
|
2019-09-16 10:49:49 -07:00
|
|
|
if (!_settingsWithDefaults.devices) {
|
|
|
|
_settingsWithDefaults.devices = {};
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!_settingsWithDefaults.groups) {
|
|
|
|
_settingsWithDefaults.groups = {};
|
|
|
|
}
|
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
return _settingsWithDefaults;
|
|
|
|
}
|
|
|
|
|
2019-02-18 11:46:19 -07:00
|
|
|
function set(path, value) {
|
2019-09-09 10:48:09 -07:00
|
|
|
let settings = get();
|
2019-02-18 11:46:19 -07:00
|
|
|
|
|
|
|
for (let i = 0; i < path.length; i++) {
|
|
|
|
const key = path[i];
|
|
|
|
if (i === path.length - 1) {
|
2019-09-09 10:48:09 -07:00
|
|
|
settings[key] = value;
|
2019-02-18 11:46:19 -07:00
|
|
|
} else {
|
2019-09-09 10:48:09 -07:00
|
|
|
if (!settings[key]) {
|
|
|
|
settings[key] = {};
|
2019-02-18 11:46:19 -07:00
|
|
|
}
|
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
settings = settings[key];
|
2019-02-18 11:46:19 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
write();
|
2019-02-18 11:46:19 -07:00
|
|
|
}
|
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
function getGroup(IDorName) {
|
|
|
|
const settings = getWithDefaults();
|
|
|
|
const byID = settings.groups[IDorName];
|
|
|
|
if (byID) {
|
2019-09-29 05:35:05 -07:00
|
|
|
return {optimistic: true, devices: [], ...byID, ID: Number(IDorName), friendlyName: byID.friendly_name};
|
2019-09-09 10:48:09 -07:00
|
|
|
}
|
2019-03-08 08:27:09 -07:00
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
for (const [ID, group] of Object.entries(settings.groups)) {
|
|
|
|
if (group.friendly_name === IDorName) {
|
2019-09-29 05:35:05 -07:00
|
|
|
return {optimistic: true, devices: [], ...group, ID: Number(ID), friendlyName: group.friendly_name};
|
2019-09-09 10:48:09 -07:00
|
|
|
}
|
|
|
|
}
|
2019-03-08 09:30:38 -07:00
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
return null;
|
|
|
|
}
|
2019-03-08 09:30:38 -07:00
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
function getGroups() {
|
|
|
|
const settings = getWithDefaults();
|
|
|
|
return Object.entries(settings.groups).map(([ID, group]) => {
|
2019-09-29 05:35:05 -07:00
|
|
|
return {optimistic: true, devices: [], ...group, ID: Number(ID), friendlyName: group.friendly_name};
|
2019-09-09 10:48:09 -07:00
|
|
|
});
|
|
|
|
}
|
2019-04-29 11:38:40 -07:00
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
function getGroupThrowIfNotExists(IDorName) {
|
|
|
|
const group = getGroup(IDorName);
|
|
|
|
if (!group) {
|
|
|
|
throw new Error(`Group '${IDorName}' does not exist`);
|
2019-04-29 11:38:40 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
return group;
|
2019-09-09 10:48:09 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
function getDevice(IDorName) {
|
|
|
|
const settings = getWithDefaults();
|
|
|
|
const byID = settings.devices[IDorName];
|
|
|
|
if (byID) {
|
|
|
|
return {...byID, ID: IDorName, friendlyName: byID.friendly_name};
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const [ID, device] of Object.entries(settings.devices)) {
|
|
|
|
if (device.friendly_name === IDorName) {
|
|
|
|
return {...device, ID, friendlyName: device.friendly_name};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
function getDeviceThrowIfNotExists(IDorName) {
|
|
|
|
const device = getDevice(IDorName);
|
|
|
|
if (!device) {
|
|
|
|
throw new Error(`Device '${IDorName}' does not exist`);
|
|
|
|
}
|
|
|
|
|
|
|
|
return device;
|
|
|
|
}
|
|
|
|
|
|
|
|
function getEntity(IDorName) {
|
|
|
|
const device = getDevice(IDorName);
|
|
|
|
if (device) {
|
|
|
|
return {...device, type: 'device'};
|
|
|
|
}
|
|
|
|
|
|
|
|
const group = getGroup(IDorName);
|
|
|
|
if (group) {
|
|
|
|
return {...group, type: 'group'};
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
2019-03-08 08:27:09 -07:00
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
function addDevice(ID) {
|
|
|
|
if (getDevice(ID)) {
|
|
|
|
throw new Error(`Device '${ID}' already exists`);
|
|
|
|
}
|
|
|
|
|
|
|
|
const settings = get();
|
2019-03-08 08:27:09 -07:00
|
|
|
|
2018-05-25 07:42:29 -07:00
|
|
|
if (!settings.devices) {
|
|
|
|
settings.devices = {};
|
2018-04-25 10:29:03 -07:00
|
|
|
}
|
|
|
|
|
2019-11-06 12:30:33 -07:00
|
|
|
settings.devices[ID] = {friendly_name: ID};
|
2019-09-09 10:48:09 -07:00
|
|
|
write();
|
|
|
|
return getDevice(ID);
|
2018-04-25 10:29:03 -07:00
|
|
|
}
|
2018-04-18 09:25:40 -07:00
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
function whitelistDevice(ID) {
|
|
|
|
const settings = get();
|
2019-06-25 10:38:36 -07:00
|
|
|
if (!settings.whitelist) {
|
|
|
|
settings.whitelist = [];
|
|
|
|
}
|
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
if (settings.whitelist.includes(ID)) {
|
|
|
|
throw new Error(`Device '${ID}' already whitelisted`);
|
|
|
|
}
|
2019-06-25 10:38:36 -07:00
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
settings.whitelist.push(ID);
|
|
|
|
write();
|
2019-06-25 10:38:36 -07:00
|
|
|
}
|
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
function banDevice(ID) {
|
|
|
|
const settings = get();
|
2019-03-26 13:34:58 -07:00
|
|
|
if (!settings.ban) {
|
|
|
|
settings.ban = [];
|
|
|
|
}
|
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
settings.ban.push(ID);
|
|
|
|
write();
|
2019-03-26 13:34:58 -07:00
|
|
|
}
|
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
function removeDevice(IDorName) {
|
|
|
|
const device = getDeviceThrowIfNotExists(IDorName);
|
|
|
|
const settings = get();
|
|
|
|
delete settings.devices[device.ID];
|
|
|
|
write();
|
2018-06-07 10:41:11 -07:00
|
|
|
}
|
|
|
|
|
2019-11-04 09:59:00 -07:00
|
|
|
function addGroup(name, ID=null) {
|
2019-09-27 13:54:07 -07:00
|
|
|
if (getGroup(name) || getDevice(name)) {
|
|
|
|
throw new Error(`friendly_name '${name}' is already in use`);
|
2019-09-09 10:48:09 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
const settings = get();
|
2019-03-12 13:19:04 -07:00
|
|
|
if (!settings.groups) {
|
|
|
|
settings.groups = {};
|
|
|
|
}
|
|
|
|
|
2019-11-04 09:59:00 -07:00
|
|
|
if (ID == null) {
|
|
|
|
// look for free ID
|
|
|
|
ID = '1';
|
|
|
|
while (settings.groups.hasOwnProperty(ID)) {
|
|
|
|
ID = (Number.parseInt(ID) + 1).toString();
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// ensure provided ID is not in use
|
|
|
|
ID = ID.toString();
|
|
|
|
if (settings.groups.hasOwnProperty(ID)) {
|
|
|
|
throw new Error(`group id '${ID}' is already in use`);
|
|
|
|
}
|
2019-03-12 13:19:04 -07:00
|
|
|
}
|
|
|
|
|
2019-11-06 12:30:33 -07:00
|
|
|
settings.groups[ID] = {friendly_name: name};
|
2019-09-09 10:48:09 -07:00
|
|
|
write();
|
2019-03-12 13:19:04 -07:00
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
return getGroup(ID);
|
2019-03-12 13:19:04 -07:00
|
|
|
}
|
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
function groupHasDevice(group, keys) {
|
|
|
|
for (const device of group.devices) {
|
|
|
|
const index = keys.indexOf(device);
|
|
|
|
if (index != -1) {
|
|
|
|
return keys[index];
|
|
|
|
}
|
2019-04-29 11:38:40 -07:00
|
|
|
}
|
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
return false;
|
2019-04-29 11:38:40 -07:00
|
|
|
}
|
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
function addDeviceToGroup(groupIDorName, keys) {
|
|
|
|
const groupID = getGroupThrowIfNotExists(groupIDorName).ID;
|
|
|
|
const settings = get();
|
2019-04-29 11:38:40 -07:00
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
const group = settings.groups[groupID];
|
|
|
|
if (!group.devices) {
|
|
|
|
group.devices = [];
|
2019-04-29 11:38:40 -07:00
|
|
|
}
|
2019-03-12 13:19:04 -07:00
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
if (!groupHasDevice(group, keys)) {
|
|
|
|
group.devices.push(keys[0]);
|
|
|
|
write();
|
2019-03-12 13:19:04 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
function removeDeviceFromGroup(groupIDorName, keys) {
|
|
|
|
const groupID = getGroupThrowIfNotExists(groupIDorName).ID;
|
|
|
|
const settings = get();
|
|
|
|
const group = settings.groups[groupID];
|
2020-03-23 14:24:35 -07:00
|
|
|
if (!group.devices) {
|
|
|
|
group.devices = [];
|
|
|
|
}
|
2018-06-09 03:27:04 -07:00
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
const key = groupHasDevice(group, keys);
|
|
|
|
if (key) {
|
|
|
|
group.devices = group.devices.filter((d) => d != key);
|
|
|
|
write();
|
|
|
|
}
|
2018-12-21 16:07:53 -07:00
|
|
|
}
|
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
function removeGroup(groupIDorName) {
|
|
|
|
const groupID = getGroupThrowIfNotExists(groupIDorName).ID;
|
|
|
|
const settings = get();
|
|
|
|
delete settings.groups[groupID];
|
|
|
|
write();
|
|
|
|
}
|
2019-02-22 12:10:00 -07:00
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
function changeDeviceOptions(IDorName, newOptions) {
|
|
|
|
const device = getDeviceThrowIfNotExists(IDorName);
|
|
|
|
const settings = get();
|
2020-03-28 11:48:05 -07:00
|
|
|
objectAssignDeep(settings.devices[device.ID], newOptions);
|
2019-09-09 10:48:09 -07:00
|
|
|
write();
|
2019-02-22 12:10:00 -07:00
|
|
|
}
|
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
function changeFriendlyName(IDorName, newName) {
|
2019-09-27 13:54:07 -07:00
|
|
|
if (getGroup(newName) || getDevice(newName)) {
|
|
|
|
throw new Error(`friendly_name '${newName}' is already in use`);
|
|
|
|
}
|
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
const settings = get();
|
2020-02-23 13:51:30 -07:00
|
|
|
if (getDevice(IDorName)) {
|
|
|
|
settings.devices[getDevice(IDorName).ID].friendly_name = newName;
|
|
|
|
} else if (getGroup(IDorName)) {
|
|
|
|
settings.groups[getGroup(IDorName).ID].friendly_name = newName;
|
|
|
|
} else {
|
|
|
|
throw new Error(`Device or group '${IDorName}' does not exist`);
|
|
|
|
}
|
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
write();
|
2018-07-21 09:15:56 -07:00
|
|
|
}
|
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
module.exports = {
|
2019-09-25 03:08:39 -07:00
|
|
|
validate,
|
2019-09-09 10:48:09 -07:00
|
|
|
get: getWithDefaults,
|
|
|
|
set,
|
2019-03-08 08:27:09 -07:00
|
|
|
getDevice,
|
|
|
|
getGroup,
|
2019-03-19 11:18:22 -07:00
|
|
|
getGroups,
|
2019-09-09 10:48:09 -07:00
|
|
|
getEntity,
|
|
|
|
whitelistDevice,
|
|
|
|
banDevice,
|
|
|
|
addDevice,
|
|
|
|
removeDevice,
|
|
|
|
addGroup,
|
|
|
|
removeGroup,
|
|
|
|
addDeviceToGroup,
|
|
|
|
removeDeviceFromGroup,
|
|
|
|
changeDeviceOptions,
|
|
|
|
changeFriendlyName,
|
|
|
|
|
|
|
|
// For tests only
|
2019-10-25 10:17:47 -07:00
|
|
|
_write: write,
|
2019-09-09 10:48:09 -07:00
|
|
|
_reRead: () => {
|
|
|
|
_settings = read();
|
|
|
|
_settingsWithDefaults = objectAssignDeep.noMutate(defaults, get());
|
2019-02-24 07:49:41 -07:00
|
|
|
},
|
2019-09-09 10:48:09 -07:00
|
|
|
_getDefaults: () => defaults,
|
2018-05-17 08:20:46 -07:00
|
|
|
};
|