Add Environment Variable Override (#4085)

* Add Environment Variable Override

I need to be able to manage settings on this service via Environment Variables.   I've added the ability to override anything in the settings schema with a corresponding environment variable.  

e.g. to override settings.serial.port you can set ZIGBEE2MQTT_SERIAL_PORT=/dev/ttyS0
to override the mqtt username you can set ZIGBEE2MQTT_MQTT_USER=testusername

This new addition will not perform any action on existing installations unless the matching environment variable is set.  I have tested this setting string, number, boolean, object and array values.

* Adding test case for environment variables and slight modification to ensure 100% code coverage.

* Adding a test to confirm that env variables will set non default values.  Also realized that I was errantly applying the env variables to the defaults for testing.  Understanding what this is doing more clearly I realize that should be clean.

* Refactoring to

1. Remove the test variables from the schema and defaults and manually reflect the tests in the test.

2. Rename environment variable base from ZIGBEE2MQTT_ to ZIGBEE2MQTT_CONFIG_

* Small improvements

* Removing the unneeded test.

Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
This commit is contained in:
Tom 2020-08-13 16:50:30 -05:00 committed by GitHub
parent 5de279c20e
commit 08524953bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 80 additions and 2 deletions

View File

@ -438,9 +438,52 @@ function read() {
return s;
}
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]);
}
if (type.indexOf('number') >= 0) {
setting[key] = process.env[envVariableName] * 1;
}
if (type.indexOf('boolean') >= 0) {
setting[key] = process.env[envVariableName].toLowerCase() === 'true';
}
if (type.indexOf('string') >= 0) {
setting[key] = process.env[envVariableName];
}
}
}
if (typeof obj[key] === 'object') {
const newPath = [...path];
if (key !== 'properties') {
newPath.push(key);
}
iterate(obj[key], newPath);
}
}
});
};
iterate(schema.properties, []);
}
function get() {
if (!_settings) {
_settings = read();
applyEnvironmentVariables(_settings);
}
return _settings;
@ -743,11 +786,14 @@ module.exports = {
// For tests only
_write: write,
_reRead: () => {
_settings = read();
_settingsWithDefaults = objectAssignDeep.noMutate(defaults, get());
_settings = null;
get();
_settingsWithDefaults = null;
getWithDefaults();
},
_clear: () => {
_settings = null;
_settingsWithDefaults = null;
},
_getDefaults: () => defaults,
};

View File

@ -9,6 +9,7 @@ const devicesFile = data.joinPath('devices.yaml');
const groupsFile = data.joinPath('groups.yaml');
const secretFile = data.joinPath('secret.yaml');
const yaml = require('js-yaml');
const objectAssignDeep = require(`object-assign-deep`);
const minimalConfig = {
permit_join: true,
@ -27,11 +28,19 @@ describe('Settings', () => {
const remove = (file) => {
if (fs.existsSync(file)) fs.unlinkSync(file);
}
const clearEnvironmentVariables = () => {
Object.keys(process.env).forEach((key) => {
if(key.indexOf('ZIGBEE2MQTT_CONFIG_') >= 0) {
delete process.env[key];
}
});
}
beforeEach(() => {
remove(configurationFile);
remove(devicesFile);
remove(groupsFile);
clearEnvironmentVariables();
});
it('Should return default settings', () => {
@ -53,6 +62,29 @@ describe('Settings', () => {
expect(s).toStrictEqual(expected);
});
it('Should apply environment variables', () => {
process.env['ZIGBEE2MQTT_CONFIG_SERIAL_DISABLE_LED'] = 'true';
process.env['ZIGBEE2MQTT_CONFIG_ADVANCED_SOFT_RESET_TIMEOUT'] = 1;
process.env['ZIGBEE2MQTT_CONFIG_EXPERIMENTAL_OUTPUT'] = 'csvtest';
process.env['ZIGBEE2MQTT_CONFIG_ADVANCED_AVAILABILITY_BLOCKLIST'] = '["0x43597f0dac781b1e", "x223b0aef2ae8d1b0"]';
process.env['ZIGBEE2MQTT_CONFIG_MAP_OPTIONS_GRAPHVIZ_COLORS_FILL'] = '{"enddevice": "#ff0000", "coordinator": "#00ff00", "router": "#0000ff"}';
process.env['ZIGBEE2MQTT_CONFIG_MQTT_BASE_TOPIC'] = 'testtopic';
write(configurationFile, {});
const s = settings.get();
const expected = objectAssignDeep.noMutate({}, settings._getDefaults());
expected.devices = {};
expected.groups = {};
expected.serial.disable_led = true;
expected.advanced.soft_reset_timeout = 1;
expected.experimental.output = 'csvtest';
expected.advanced.availability_blocklist = ['0x43597f0dac781b1e', 'x223b0aef2ae8d1b0'];
expected.map_options.graphviz.colors.fill = {enddevice: '#ff0000', coordinator: '#00ff00', router: '#0000ff'};
expected.mqtt.base_topic = 'testtopic';
expect(s).toStrictEqual(expected);
});
it('Should add devices', () => {
write(configurationFile, {});
settings.addDevice('0x12345678');