2018-05-11 20:04:15 -07:00
|
|
|
const data = require('./data');
|
2019-12-09 10:27:39 -07:00
|
|
|
const utils = require('./utils');
|
2020-10-24 07:20:48 -07:00
|
|
|
// DEPRECATED ZIGBEE2MQTT_CONFIG: https://github.com/Koenkk/zigbee2mqtt/issues/4697
|
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');
|
2021-01-16 04:32:15 -07:00
|
|
|
const schema = require('./settings.schema.json');
|
2021-02-06 08:32:20 -07:00
|
|
|
const ajvSetting = new Ajv({allErrors: true}).compile(schema);
|
|
|
|
const ajvRestartRequired = new Ajv({allErrors: true})
|
|
|
|
.addKeyword('requiresRestart', {validate: (v) => !v}).compile(schema);
|
2019-02-23 08:22:01 -07:00
|
|
|
|
2018-09-18 08:51:34 -07:00
|
|
|
const defaults = {
|
2020-07-15 14:22:32 -07:00
|
|
|
passlist: [],
|
|
|
|
blocklist: [],
|
|
|
|
// Deprecated: use block/passlist
|
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,
|
2020-11-16 09:27:49 -07:00
|
|
|
/**
|
|
|
|
* Configurable force disable retain flag on mqtt publish.
|
|
|
|
* https://github.com/Koenkk/zigbee2mqtt/pull/4948
|
|
|
|
*/
|
|
|
|
force_disable_retain: false,
|
2018-11-16 12:23:11 -07:00
|
|
|
},
|
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: {
|
2020-04-04 13:47:23 -07:00
|
|
|
legacy_api: true,
|
2020-04-04 10:46:43 -07:00
|
|
|
log_rotation: true,
|
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',
|
2020-06-03 11:44:11 -07:00
|
|
|
log_syslog: {},
|
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,
|
2020-03-30 12:56:34 -07:00
|
|
|
adapter_concurrent: null,
|
2020-11-18 10:46:13 -07:00
|
|
|
adapter_delay: null,
|
2018-11-16 12:23:11 -07:00
|
|
|
|
2019-01-29 12:17:56 -07:00
|
|
|
// Availability timeout in seconds, disabled by default.
|
|
|
|
availability_timeout: 0,
|
2020-07-15 14:22:32 -07:00
|
|
|
availability_blocklist: [],
|
|
|
|
availability_passlist: [],
|
|
|
|
// Deprecated, use block/passlist
|
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
|
|
|
|
*
|
2020-08-01 01:36:20 -07:00
|
|
|
* Therefore Zigbee2MQTT BY DEFAULT caches all values and resend it with every message.
|
2018-11-16 12:23:11 -07:00
|
|
|
* 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,
|
2020-07-10 13:09:16 -07:00
|
|
|
cache_state_persistent: true,
|
|
|
|
cache_state_send_on_startup: 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
|
|
|
},
|
2021-02-14 10:20:32 -07:00
|
|
|
ota: {
|
|
|
|
/**
|
|
|
|
* Minimal time delta in milliseconds between polling third party server for potential firmware updates
|
|
|
|
*/
|
|
|
|
update_check_interval: 10,
|
|
|
|
/**
|
|
|
|
* Completely disallow Zigbee devices to initiate a search for a potential firmware update.
|
|
|
|
* If set to true, only a user-initiated update search will be possible.
|
|
|
|
*/
|
|
|
|
disable_automatic_update_check: false,
|
|
|
|
},
|
2020-06-29 07:16:16 -07:00
|
|
|
external_converters: [],
|
2018-09-18 08:51:34 -07:00
|
|
|
};
|
2018-05-12 01:58:06 -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
|
|
|
|
2020-04-21 12:58:43 -07:00
|
|
|
// In case the setting is defined in a separte file (e.g. !secret network_key) update it there.
|
2021-01-04 11:32:03 -07:00
|
|
|
for (const path of [
|
|
|
|
['mqtt', 'user'],
|
|
|
|
['mqtt', 'password'],
|
|
|
|
['advanced', 'network_key'],
|
|
|
|
['frontend', 'auth_token'],
|
|
|
|
]) {
|
2020-04-21 12:58:43 -07:00
|
|
|
if (actual[path[0]] && actual[path[0]][path[1]]) {
|
|
|
|
const match = /!(.*) (.*)/g.exec(actual[path[0]][path[1]]);
|
|
|
|
if (match) {
|
|
|
|
yaml.updateIfChanged(data.joinPath(`${match[1]}.yaml`), match[2], toWrite[path[0]][path[1]]);
|
|
|
|
toWrite[path[0]][path[1]] = actual[path[0]][path[1]];
|
|
|
|
}
|
|
|
|
}
|
2019-10-28 10:05:50 -07:00
|
|
|
}
|
|
|
|
|
2020-11-27 09:32:32 -07:00
|
|
|
// Write devices/groups to separate file if required.
|
|
|
|
const writeDevicesOrGroups = (type) => {
|
|
|
|
if (typeof actual[type] === 'string' || Array.isArray(actual[type])) {
|
|
|
|
const fileToWrite = Array.isArray(actual[type]) ? actual[type][0] : actual[type];
|
|
|
|
const content = objectAssignDeep.noMutate(settings[type]);
|
|
|
|
|
|
|
|
// If an array, only write to first file and only devices which are not in the other files.
|
|
|
|
if (Array.isArray(actual[type])) {
|
|
|
|
actual[type].filter((f, i) => i !== 0)
|
|
|
|
.map((f) => yaml.readIfExists(data.joinPath(f), {}))
|
|
|
|
.map((c) => Object.keys(c))
|
|
|
|
.forEach((k) => delete content[k]);
|
|
|
|
}
|
2019-02-24 07:49:41 -07:00
|
|
|
|
2020-11-27 09:32:32 -07:00
|
|
|
yaml.writeIfChanged(data.joinPath(fileToWrite), content);
|
|
|
|
toWrite[type] = actual[type];
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
writeDevicesOrGroups('devices');
|
|
|
|
writeDevicesOrGroups('groups');
|
2019-02-24 07:49:41 -07:00
|
|
|
|
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() {
|
2020-07-23 10:17:26 -07:00
|
|
|
try {
|
|
|
|
get();
|
|
|
|
} catch (error) {
|
|
|
|
if (error.name === 'YAMLException') {
|
|
|
|
return [
|
|
|
|
`Your YAML file: '${error.file}' is invalid ` +
|
|
|
|
`(use https://jsonformatter.org/yaml-validator to find and fix the issue)`,
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
return [error.message];
|
|
|
|
}
|
|
|
|
|
2021-02-06 08:32:20 -07:00
|
|
|
if (!ajvSetting(_settings)) {
|
|
|
|
return ajvSetting.errors.map((v) => `${v.dataPath.substring(1)} ${v.message}`);
|
2020-07-23 10:17:26 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
const errors = [];
|
2020-04-21 12:58:43 -07:00
|
|
|
if (_settings.advanced && _settings.advanced.network_key && typeof _settings.advanced.network_key === 'string' &&
|
|
|
|
_settings.advanced.network_key !== 'GENERATE') {
|
2020-07-23 10:17:26 -07:00
|
|
|
errors.push(`advanced.network_key: should be array or 'GENERATE' (is '${_settings.advanced.network_key}')`);
|
2020-04-21 12:58:43 -07:00
|
|
|
}
|
|
|
|
|
2021-01-18 10:01:10 -07:00
|
|
|
if (_settings.advanced && _settings.advanced.pan_id && typeof _settings.advanced.pan_id === 'string' &&
|
|
|
|
_settings.advanced.pan_id !== 'GENERATE') {
|
|
|
|
errors.push(`advanced.pan_id: should be number or 'GENERATE' (is '${_settings.advanced.pan_id}')`);
|
|
|
|
}
|
|
|
|
|
2019-09-27 13:54:07 -07:00
|
|
|
// Verify that all friendly names are unique
|
|
|
|
const names = [];
|
|
|
|
const check = (name) => {
|
2020-07-23 10:17:26 -07:00
|
|
|
if (names.includes(name)) errors.push(`Duplicate friendly_name '${name}' found`);
|
2020-09-19 01:57:39 -07:00
|
|
|
errors.push(...utils.validateFriendlyName(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) {
|
2020-07-23 10:17:26 -07:00
|
|
|
errors.push('MQTT retention requires protocol version 5');
|
2020-03-12 12:25:37 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-25 12:46:20 -07:00
|
|
|
const checkAvailabilityList = (list, type) => {
|
|
|
|
list.forEach((e) => {
|
|
|
|
if (!getEntity(e)) {
|
2020-07-23 10:17:26 -07:00
|
|
|
errors.push(`Non-existing entity '${e}' specified in '${type}'`);
|
2020-03-25 12:46:20 -07:00
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
checkAvailabilityList(settingsWithDefaults.advanced.availability_blacklist, 'availability_blacklist');
|
|
|
|
checkAvailabilityList(settingsWithDefaults.advanced.availability_whitelist, 'availability_whitelist');
|
2020-07-15 14:22:32 -07:00
|
|
|
checkAvailabilityList(settingsWithDefaults.advanced.availability_blocklist, 'availability_blocklist');
|
|
|
|
checkAvailabilityList(settingsWithDefaults.advanced.availability_passlist, 'availability_passlist');
|
2020-03-25 12:46:20 -07:00
|
|
|
|
2020-07-23 10:17:26 -07:00
|
|
|
return errors;
|
2019-09-25 03:08:39 -07:00
|
|
|
}
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2021-01-04 11:32:03 -07:00
|
|
|
if (s.frontend && s.frontend.auth_token) {
|
|
|
|
s.frontend.auth_token = interpetValue(s.frontend.auth_token);
|
|
|
|
}
|
|
|
|
|
2020-11-27 09:32:32 -07:00
|
|
|
// Read devices/groups configuration from separate file if specified.
|
|
|
|
const readDevicesOrGroups = (type) => {
|
|
|
|
if (typeof s[type] === 'string' || Array.isArray(s[type])) {
|
|
|
|
const files = Array.isArray(s[type]) ? s[type] : [s[type]];
|
|
|
|
s[type] = {};
|
|
|
|
for (const file of files) {
|
|
|
|
const content = yaml.readIfExists(data.joinPath(file), {});
|
|
|
|
s[type] = objectAssignDeep.noMutate(s[type], content);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
2019-02-24 07:49:41 -07:00
|
|
|
|
2020-11-27 09:32:32 -07:00
|
|
|
readDevicesOrGroups('devices');
|
|
|
|
readDevicesOrGroups('groups');
|
2019-02-24 07:49:41 -07:00
|
|
|
|
|
|
|
return s;
|
|
|
|
}
|
|
|
|
|
2020-08-13 14:50:30 -07:00
|
|
|
function applyEnvironmentVariables(settings) {
|
|
|
|
const iterate = (obj, path) => {
|
|
|
|
Object.keys(obj).forEach((key) => {
|
|
|
|
if (key !== 'type') {
|
|
|
|
if (key !== 'properties') {
|
|
|
|
const type = (obj[key].type || 'object').toString();
|
|
|
|
const envPart = path.reduce((acc, val) => `${acc}${val}_`, '');
|
|
|
|
const envVariableName = (`ZIGBEE2MQTT_CONFIG_${envPart}${key}`).toUpperCase();
|
|
|
|
if (process.env[envVariableName]) {
|
|
|
|
const setting = path.reduce((acc, val, index) => {
|
|
|
|
acc[val] = acc[val] || {};
|
|
|
|
return acc[val];
|
|
|
|
}, settings);
|
|
|
|
|
|
|
|
if (type.indexOf('object') >= 0 || type.indexOf('array') >= 0) {
|
|
|
|
setting[key] = JSON.parse(process.env[envVariableName]);
|
2020-12-27 11:47:00 -07:00
|
|
|
} else if (type.indexOf('number') >= 0) {
|
2020-08-13 14:50:30 -07:00
|
|
|
setting[key] = process.env[envVariableName] * 1;
|
2020-12-27 11:47:00 -07:00
|
|
|
} else if (type.indexOf('boolean') >= 0) {
|
2020-08-13 14:50:30 -07:00
|
|
|
setting[key] = process.env[envVariableName].toLowerCase() === 'true';
|
2020-12-27 11:47:00 -07:00
|
|
|
} else {
|
|
|
|
/* istanbul ignore else */
|
|
|
|
if (type.indexOf('string') >= 0) {
|
|
|
|
setting[key] = process.env[envVariableName];
|
|
|
|
}
|
2020-08-13 14:50:30 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (typeof obj[key] === 'object') {
|
|
|
|
const newPath = [...path];
|
|
|
|
if (key !== 'properties') {
|
|
|
|
newPath.push(key);
|
|
|
|
}
|
|
|
|
iterate(obj[key], newPath);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
};
|
|
|
|
iterate(schema.properties, []);
|
|
|
|
}
|
|
|
|
|
2019-09-09 10:48:09 -07:00
|
|
|
function get() {
|
|
|
|
if (!_settings) {
|
|
|
|
_settings = read();
|
2020-08-13 14:50:30 -07:00
|
|
|
applyEnvironmentVariables(_settings);
|
2019-09-09 10:48:09 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
2021-02-06 08:32:20 -07:00
|
|
|
function apply(newSettings) {
|
|
|
|
ajvSetting(newSettings);
|
|
|
|
const errors = ajvSetting.errors && ajvSetting.errors.filter((e) => e.keyword !== 'required');
|
|
|
|
if (errors.length) {
|
|
|
|
const error = errors[0];
|
|
|
|
throw new Error(`${error.dataPath.substring(1)} ${error.message}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
get(); // Ensure _settings is intialized.
|
|
|
|
_settings = objectAssignDeep.noMutate(_settings, newSettings);
|
|
|
|
write();
|
|
|
|
|
|
|
|
ajvRestartRequired(newSettings);
|
|
|
|
const restartRequired = ajvRestartRequired.errors &&
|
|
|
|
!!ajvRestartRequired.errors.find((e) => e.keyword === 'requiresRestart');
|
|
|
|
return restartRequired;
|
|
|
|
}
|
|
|
|
|
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
|
|
|
|
2020-07-15 14:22:32 -07:00
|
|
|
// Legacy: can be removed after bridgeLegacy has been removed
|
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
|
|
|
}
|
|
|
|
|
2020-07-15 14:22:32 -07:00
|
|
|
function blockDevice(ID) {
|
|
|
|
const settings = get();
|
|
|
|
if (!settings.blocklist) {
|
|
|
|
settings.blocklist = [];
|
|
|
|
}
|
|
|
|
|
|
|
|
settings.blocklist.push(ID);
|
|
|
|
write();
|
|
|
|
}
|
|
|
|
|
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) {
|
2021-02-04 10:01:44 -07:00
|
|
|
utils.validateFriendlyName(name, true);
|
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
|
|
|
|
2020-06-13 14:28:06 -07:00
|
|
|
function changeEntityOptions(IDorName, newOptions) {
|
2019-09-09 10:48:09 -07:00
|
|
|
const settings = get();
|
2020-06-13 14:28:06 -07:00
|
|
|
delete newOptions.friendly_name;
|
|
|
|
delete newOptions.devices;
|
|
|
|
if (getDevice(IDorName)) {
|
|
|
|
objectAssignDeep(settings.devices[getDevice(IDorName).ID], newOptions);
|
|
|
|
} else if (getGroup(IDorName)) {
|
|
|
|
objectAssignDeep(settings.groups[getGroup(IDorName).ID], newOptions);
|
|
|
|
} else {
|
|
|
|
throw new Error(`Device or group '${IDorName}' does not exist`);
|
|
|
|
}
|
|
|
|
|
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) {
|
2021-02-04 10:01:44 -07:00
|
|
|
utils.validateFriendlyName(newName, true);
|
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,
|
2021-02-06 08:32:20 -07:00
|
|
|
apply,
|
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,
|
2020-07-15 14:22:32 -07:00
|
|
|
blockDevice,
|
2019-09-09 10:48:09 -07:00
|
|
|
addDevice,
|
|
|
|
removeDevice,
|
|
|
|
addGroup,
|
|
|
|
removeGroup,
|
|
|
|
addDeviceToGroup,
|
|
|
|
removeDeviceFromGroup,
|
2020-06-13 14:28:06 -07:00
|
|
|
changeEntityOptions,
|
2019-09-09 10:48:09 -07:00
|
|
|
changeFriendlyName,
|
2021-01-16 04:32:15 -07:00
|
|
|
schema,
|
2019-09-09 10:48:09 -07:00
|
|
|
|
|
|
|
// For tests only
|
2019-10-25 10:17:47 -07:00
|
|
|
_write: write,
|
2019-09-09 10:48:09 -07:00
|
|
|
_reRead: () => {
|
2020-08-13 14:50:30 -07:00
|
|
|
_settings = null;
|
|
|
|
get();
|
|
|
|
_settingsWithDefaults = null;
|
|
|
|
getWithDefaults();
|
2019-02-24 07:49:41 -07:00
|
|
|
},
|
2020-07-23 10:17:26 -07:00
|
|
|
_clear: () => {
|
|
|
|
_settings = null;
|
2020-08-13 14:50:30 -07:00
|
|
|
_settingsWithDefaults = null;
|
2020-07-23 10:17:26 -07:00
|
|
|
},
|
2019-09-09 10:48:09 -07:00
|
|
|
_getDefaults: () => defaults,
|
2018-05-17 08:20:46 -07:00
|
|
|
};
|